Similarity module
editSimilarity module
editA similarity (scoring / ranking model) defines how matching documents are scored. Similarity is per field, meaning that via the mapping one can define a different similarity per field.
Similarity is only applicable for text type and keyword type fields.
Configuring a custom similarity is considered an expert feature and the
builtin similarities are most likely sufficient as is described in
similarity
.
Configuring a similarity
editMost existing or custom Similarities have configuration options which can be configured via the index settings as shown below. The index options can be provided when creating an index or updating index settings.
resp = client.indices.create( index="index", settings={ "index": { "similarity": { "my_similarity": { "type": "DFR", "basic_model": "g", "after_effect": "l", "normalization": "h2", "normalization.h2.c": "3.0" } } } }, ) print(resp)
response = client.indices.create( index: 'index', body: { settings: { index: { similarity: { my_similarity: { type: 'DFR', basic_model: 'g', after_effect: 'l', normalization: 'h2', "normalization.h2.c": '3.0' } } } } } ) puts response
const response = await client.indices.create({ index: "index", settings: { index: { similarity: { my_similarity: { type: "DFR", basic_model: "g", after_effect: "l", normalization: "h2", "normalization.h2.c": "3.0", }, }, }, }, }); console.log(response);
PUT /index { "settings": { "index": { "similarity": { "my_similarity": { "type": "DFR", "basic_model": "g", "after_effect": "l", "normalization": "h2", "normalization.h2.c": "3.0" } } } } }
Here we configure the DFR similarity so it can be referenced as
my_similarity
in mappings as is illustrate in the below example:
resp = client.indices.put_mapping( index="index", properties={ "title": { "type": "text", "similarity": "my_similarity" } }, ) print(resp)
response = client.indices.put_mapping( index: 'index', body: { properties: { title: { type: 'text', similarity: 'my_similarity' } } } ) puts response
const response = await client.indices.putMapping({ index: "index", properties: { title: { type: "text", similarity: "my_similarity", }, }, }); console.log(response);
PUT /index/_mapping { "properties" : { "title" : { "type" : "text", "similarity" : "my_similarity" } } }
Available similarities
editBM25 similarity (default)
editTF/IDF based similarity that has built-in tf normalization and is supposed to work better for short fields (like names). See Okapi_BM25 for more details. This similarity has the following options:
|
Controls non-linear term frequency normalization
(saturation). The default value is |
|
Controls to what degree document length normalizes tf values.
The default value is |
|
Determines whether overlap tokens (Tokens with 0 position increment) are ignored when computing norm. By default this is true, meaning overlap tokens do not count when computing norms. |
Type name: BM25
DFR similarity
editSimilarity that implements the divergence from randomness framework. This similarity has the following options:
|
|
|
|
|
All options but the first option need a normalization value.
Type name: DFR
DFI similarity
editSimilarity that implements the divergence from independence model. This similarity has the following options:
|
Possible values
|
When using this similarity, it is highly recommended not to remove stop words to get good relevance. Also beware that terms whose frequency is less than the expected frequency will get a score equal to 0.
Type name: DFI
IB similarity.
editInformation based model . The algorithm is based on the concept that the information content in any symbolic distribution sequence is primarily determined by the repetitive usage of its basic elements. For written texts this challenge would correspond to comparing the writing styles of different authors. This similarity has the following options:
|
|
|
|
|
Same as in |
Type name: IB
LM Dirichlet similarity.
editLM Dirichlet similarity . This similarity has the following options:
|
Default to |
The scoring formula in the paper assigns negative scores to terms that have fewer occurrences than predicted by the language model, which is illegal to Lucene, so such terms get a score of 0.
Type name: LMDirichlet
LM Jelinek Mercer similarity.
editLM Jelinek Mercer similarity . The algorithm attempts to capture important patterns in the text, while leaving out noise. This similarity has the following options:
|
The optimal value depends on both the collection and the query. The optimal value is around |
Type name: LMJelinekMercer
Scripted similarity
editA similarity that allows you to use a script in order to specify how scores should be computed. For instance, the below example shows how to reimplement TF-IDF:
resp = client.indices.create( index="index", settings={ "number_of_shards": 1, "similarity": { "scripted_tfidf": { "type": "scripted", "script": { "source": "double tf = Math.sqrt(doc.freq); double idf = Math.log((field.docCount+1.0)/(term.docFreq+1.0)) + 1.0; double norm = 1/Math.sqrt(doc.length); return query.boost * tf * idf * norm;" } } } }, mappings={ "properties": { "field": { "type": "text", "similarity": "scripted_tfidf" } } }, ) print(resp) resp1 = client.index( index="index", id="1", document={ "field": "foo bar foo" }, ) print(resp1) resp2 = client.index( index="index", id="2", document={ "field": "bar baz" }, ) print(resp2) resp3 = client.indices.refresh( index="index", ) print(resp3) resp4 = client.search( index="index", explain=True, query={ "query_string": { "query": "foo^1.7", "default_field": "field" } }, ) print(resp4)
response = client.indices.create( index: 'index', body: { settings: { number_of_shards: 1, similarity: { scripted_tfidf: { type: 'scripted', script: { source: 'double tf = Math.sqrt(doc.freq); double idf = Math.log((field.docCount+1.0)/(term.docFreq+1.0)) + 1.0; double norm = 1/Math.sqrt(doc.length); return query.boost * tf * idf * norm;' } } } }, mappings: { properties: { field: { type: 'text', similarity: 'scripted_tfidf' } } } } ) puts response response = client.index( index: 'index', id: 1, body: { field: 'foo bar foo' } ) puts response response = client.index( index: 'index', id: 2, body: { field: 'bar baz' } ) puts response response = client.indices.refresh( index: 'index' ) puts response response = client.search( index: 'index', explain: true, body: { query: { query_string: { query: 'foo^1.7', default_field: 'field' } } } ) puts response
const response = await client.indices.create({ index: "index", settings: { number_of_shards: 1, similarity: { scripted_tfidf: { type: "scripted", script: { source: "double tf = Math.sqrt(doc.freq); double idf = Math.log((field.docCount+1.0)/(term.docFreq+1.0)) + 1.0; double norm = 1/Math.sqrt(doc.length); return query.boost * tf * idf * norm;", }, }, }, }, mappings: { properties: { field: { type: "text", similarity: "scripted_tfidf", }, }, }, }); console.log(response); const response1 = await client.index({ index: "index", id: 1, document: { field: "foo bar foo", }, }); console.log(response1); const response2 = await client.index({ index: "index", id: 2, document: { field: "bar baz", }, }); console.log(response2); const response3 = await client.indices.refresh({ index: "index", }); console.log(response3); const response4 = await client.search({ index: "index", explain: "true", query: { query_string: { query: "foo^1.7", default_field: "field", }, }, }); console.log(response4);
PUT /index { "settings": { "number_of_shards": 1, "similarity": { "scripted_tfidf": { "type": "scripted", "script": { "source": "double tf = Math.sqrt(doc.freq); double idf = Math.log((field.docCount+1.0)/(term.docFreq+1.0)) + 1.0; double norm = 1/Math.sqrt(doc.length); return query.boost * tf * idf * norm;" } } } }, "mappings": { "properties": { "field": { "type": "text", "similarity": "scripted_tfidf" } } } } PUT /index/_doc/1 { "field": "foo bar foo" } PUT /index/_doc/2 { "field": "bar baz" } POST /index/_refresh GET /index/_search?explain=true { "query": { "query_string": { "query": "foo^1.7", "default_field": "field" } } }
Which yields:
{ "took": 12, "timed_out": false, "_shards": { "total": 1, "successful": 1, "skipped": 0, "failed": 0 }, "hits": { "total": { "value": 1, "relation": "eq" }, "max_score": 1.9508477, "hits": [ { "_shard": "[index][0]", "_node": "OzrdjxNtQGaqs4DmioFw9A", "_index": "index", "_id": "1", "_score": 1.9508477, "_source": { "field": "foo bar foo" }, "_explanation": { "value": 1.9508477, "description": "weight(field:foo in 0) [PerFieldSimilarity], result of:", "details": [ { "value": 1.9508477, "description": "score from ScriptedSimilarity(weightScript=[null], script=[Script{type=inline, lang='painless', idOrCode='double tf = Math.sqrt(doc.freq); double idf = Math.log((field.docCount+1.0)/(term.docFreq+1.0)) + 1.0; double norm = 1/Math.sqrt(doc.length); return query.boost * tf * idf * norm;', options={}, params={}}]) computed from:", "details": [ { "value": 1.0, "description": "weight", "details": [] }, { "value": 1.7, "description": "query.boost", "details": [] }, { "value": 2, "description": "field.docCount", "details": [] }, { "value": 4, "description": "field.sumDocFreq", "details": [] }, { "value": 5, "description": "field.sumTotalTermFreq", "details": [] }, { "value": 1, "description": "term.docFreq", "details": [] }, { "value": 2, "description": "term.totalTermFreq", "details": [] }, { "value": 2.0, "description": "doc.freq", "details": [] }, { "value": 3, "description": "doc.length", "details": [] } ] } ] } } ] } }
While scripted similarities provide a lot of flexibility, there is a set of rules that they need to satisfy. Failing to do so could make Elasticsearch silently return wrong top hits or fail with internal errors at search time:
- Returned scores must be positive.
-
All other variables remaining equal, scores must not decrease when
doc.freq
increases. -
All other variables remaining equal, scores must not increase when
doc.length
increases.
You might have noticed that a significant part of the above script depends on
statistics that are the same for every document. It is possible to make the
above slightly more efficient by providing an weight_script
which will
compute the document-independent part of the score and will be available
under the weight
variable. When no weight_script
is provided, weight
is equal to 1
. The weight_script
has access to the same variables as
the script
except doc
since it is supposed to compute a
document-independent contribution to the score.
The below configuration will give the same tf-idf scores but is slightly more efficient:
resp = client.indices.create( index="index", settings={ "number_of_shards": 1, "similarity": { "scripted_tfidf": { "type": "scripted", "weight_script": { "source": "double idf = Math.log((field.docCount+1.0)/(term.docFreq+1.0)) + 1.0; return query.boost * idf;" }, "script": { "source": "double tf = Math.sqrt(doc.freq); double norm = 1/Math.sqrt(doc.length); return weight * tf * norm;" } } } }, mappings={ "properties": { "field": { "type": "text", "similarity": "scripted_tfidf" } } }, ) print(resp)
response = client.indices.create( index: 'index', body: { settings: { number_of_shards: 1, similarity: { scripted_tfidf: { type: 'scripted', weight_script: { source: 'double idf = Math.log((field.docCount+1.0)/(term.docFreq+1.0)) + 1.0; return query.boost * idf;' }, script: { source: 'double tf = Math.sqrt(doc.freq); double norm = 1/Math.sqrt(doc.length); return weight * tf * norm;' } } } }, mappings: { properties: { field: { type: 'text', similarity: 'scripted_tfidf' } } } } ) puts response
const response = await client.indices.create({ index: "index", settings: { number_of_shards: 1, similarity: { scripted_tfidf: { type: "scripted", weight_script: { source: "double idf = Math.log((field.docCount+1.0)/(term.docFreq+1.0)) + 1.0; return query.boost * idf;", }, script: { source: "double tf = Math.sqrt(doc.freq); double norm = 1/Math.sqrt(doc.length); return weight * tf * norm;", }, }, }, }, mappings: { properties: { field: { type: "text", similarity: "scripted_tfidf", }, }, }, }); console.log(response);
PUT /index { "settings": { "number_of_shards": 1, "similarity": { "scripted_tfidf": { "type": "scripted", "weight_script": { "source": "double idf = Math.log((field.docCount+1.0)/(term.docFreq+1.0)) + 1.0; return query.boost * idf;" }, "script": { "source": "double tf = Math.sqrt(doc.freq); double norm = 1/Math.sqrt(doc.length); return weight * tf * norm;" } } } }, "mappings": { "properties": { "field": { "type": "text", "similarity": "scripted_tfidf" } } } }
Type name: scripted
Default Similarity
editBy default, Elasticsearch will use whatever similarity is configured as
default
.
You can change the default similarity for all fields in an index when it is created:
resp = client.indices.create( index="index", settings={ "index": { "similarity": { "default": { "type": "boolean" } } } }, ) print(resp)
response = client.indices.create( index: 'index', body: { settings: { index: { similarity: { default: { type: 'boolean' } } } } } ) puts response
const response = await client.indices.create({ index: "index", settings: { index: { similarity: { default: { type: "boolean", }, }, }, }, }); console.log(response);
PUT /index { "settings": { "index": { "similarity": { "default": { "type": "boolean" } } } } }
If you want to change the default similarity after creating the index you must close your index, send the following request and open it again afterwards:
resp = client.indices.close( index="index", ) print(resp) resp1 = client.indices.put_settings( index="index", settings={ "index": { "similarity": { "default": { "type": "boolean" } } } }, ) print(resp1) resp2 = client.indices.open( index="index", ) print(resp2)
response = client.indices.close( index: 'index' ) puts response response = client.indices.put_settings( index: 'index', body: { index: { similarity: { default: { type: 'boolean' } } } } ) puts response response = client.indices.open( index: 'index' ) puts response
const response = await client.indices.close({ index: "index", }); console.log(response); const response1 = await client.indices.putSettings({ index: "index", settings: { index: { similarity: { default: { type: "boolean", }, }, }, }, }); console.log(response1); const response2 = await client.indices.open({ index: "index", }); console.log(response2);
POST /index/_close PUT /index/_settings { "index": { "similarity": { "default": { "type": "boolean" } } } } POST /index/_open