# /

# 相似性概念

相似性/相关性是指召回结果用户搜索的预期值的匹配程度。不一定是用户搜索关键词,比如拼音、谐音、别名。

# 相似性算法

# TF/IDF算法

TF是TermFreq词频的缩写,IDF是InverseDocFrequency反词频的缩写。
词频:是关键词在doc中出现的次数,出现次数越多,评分越高。是正相关。
反词频:是关键词在全部doc中出现的次数,出现次数越多,说明权重越低,评分越低。是反相关。
字段长度规范:是关键词所在doc的长度,长度越长,说明权重越低,评分越低。是反相关。

# BM25算法

BM25是TF/IDF的优化算法。

GET my-index-000001/_explain/1
{
  "query": {
    "match": {
      "title": "标"
    }
  }
}
1
2
3
4
5
6
7
8
{
  "_index" : "my-index-000001",
  "_type" : "_doc",
  "_id" : "1",
  "matched" : true,
  "explanation" : {
    "value" : 0.18232156,
    "description" : "weight(title:标 in 0) [PerFieldSimilarity], result of:",
    "details" : [
      {
        "value" : 0.18232156,
        "description" : "score(freq=1.0), computed as boost * idf * tf from:",
        "details" : [
          {
            "value" : 2.2,
            "description" : "boost",
            "details" : [ ]
          },
          {
            "value" : 0.18232156,
            "description" : "idf, computed as log(1 + (N - n + 0.5) / (n + 0.5)) from:",
            "details" : [
              {
                "value" : 2,
                "description" : "n, number of documents containing term",
                "details" : [ ]
              },
              {
                "value" : 2,
                "description" : "N, total number of documents with field",
                "details" : [ ]
              }
            ]
          },
          {
            "value" : 0.45454544,
            "description" : "tf, computed as freq / (freq + k1 * (1 - b + b * dl / avgdl)) from:",
            "details" : [
              {
                "value" : 1.0,
                "description" : "freq, occurrences of term within document",
                "details" : [ ]
              },
              {
                "value" : 1.2,
                "description" : "k1, term saturation parameter",
                "details" : [ ]
              },
              {
                "value" : 0.75,
                "description" : "b, length normalization parameter",
                "details" : [ ]
              },
              {
                "value" : 3.0,
                "description" : "dl, length of field",
                "details" : [ ]
              },
              {
                "value" : 3.0,
                "description" : "avgdl, average length of field",
                "details" : [ ]
              }
            ]
          }
        ]
      }
    ]
  }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71

# 相似性参数(similarity)

Elasticsearch允许您配置每个字段的评分算法或相似性。相似性设置提供了一种选择不同于默认BM25(例如TF/IDF)的相似性算法的简单方式。
相似性主要适用于文本字段,但也适用于其他字段类型。
可以开箱即用的相似性类型:

  • BM25:Okapi BM25算法。Elasticsearch和Lucene中默认使用的算法。
  • classic经典:7.0.0中已弃用。TF/IDF算法,Elasticsearch和Lucene中以前的默认算法。
  • boolean布尔:一个简单的布尔相似性,当不需要全文排名时使用,并且分数应该仅基于查询词是否匹配。布尔相似性为术语提供了与其查询提升相等的分数。

首次创建字段时,可以在similarity字段级别上设置,如下所示:

PUT my-index-000001
{
  "mappings": {
    "properties": {
      "default_field": { 
        "type": "text"
      },
      "boolean_sim_field": {
        "type": "text",
        "similarity": "boolean" //作用域字段
      }
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 相似性模块(Similarity module)

相似性(评分/排名模型)定义了如何对匹配文档进行评分。相似性是每个字段的,这意味着通过映射可以定义每个字段的不同相似性。

# 自定义相似性

大多数现有的或自定义的相似性都有配置选项,可以通过如下所示的索引设置进行配置。可以在创建索引或更新索引设置时提供索引选项。

PUT /index
{
  "settings": {
    "index": {
      "similarity": {
        "my_similarity": {
          "type": "DFR",
          "basic_model": "g",
          "after_effect": "l",
          "normalization": "h2",
          "normalization.h2.c": "3.0"
        }
      }
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

在这里,我们配置DFR相似性,以便在映射中引用为my_silarity,如下例所示:

PUT /index/_mapping
{
  "properties" : {
    "title" : { "type" : "text", "similarity" : "my_similarity" }
  }
}
1
2
3
4
5
6
# 可用的相似性(Available similarities)
# BM25相似性(默认)

型号名称:BM25
基于TF/IDF的相似性,具有内置的TF规范化,应该更适合短字段(如名称)。

# 脚本相似性

类型名称:scripted
一种相似性,允许您使用脚本来指定分数的计算方式。例如,以下示例显示了如何重新实现TF-IDF:

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"
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

其产生:

{
  "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",
        "_type": "_doc",
        "_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": []
                }
              ]
            }
          ]
        }
      }
    ]
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87

以下配置将给出相同的tf-idf分数,但效率略高:

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"
      }
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 更改默认相似性

类型名称:default
创建索引时,可以更改索引中所有字段的默认相似性:

PUT /index
{
  "settings": {
    "index": {
      "similarity": {
        "default": {
          "type": "boolean"
        }
      }
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12

如果您想在创建索引后更改默认相似性,则必须关闭索引,然后发送以下请求并再次打开:

POST /index/_close?wait_for_active_shards=0

PUT /index/_settings
{
  "index": {
    "similarity": {
      "default": {
        "type": "boolean"
      }
    }
  }
}

POST /index/_open
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 获得一致的得分(Getting consistent scoring)

事实上,Elasticsearch使用碎片和副本进行操作,这增加了获得好分good scoring的挑战。

# 连续两次分数不同(Scores are not reproducible)

假设同一个用户连续两次运行同一个请求,而文档两次都没有以相同的顺序返回,这是一种非常糟糕的体验,不是吗?不幸的是,如果您有副本(大于0),则可能会发生这种情况。原因是Elasticsearch以循环方式选择查询应该去的碎片,所以如果你连续运行同一个查询两次,它很可能会去到同一个碎片的不同副本
为什么这是个问题?指数统计是得分的重要组成部分。由于删除了文档,这些索引统计数据在同一碎片的副本之间可能会有所不同。正如您可能知道的,当文档被删除或更新时,旧文档不会立即从索引中删除,它只是被标记为已删除,并且只有在下次合并此旧文档所属的段时才会从磁盘中删除。然而,由于实际原因,索引统计中会考虑到那些被删除的文件。因此,假设主碎片刚刚完成了一次大合并,删除了大量已删除的文档,那么它可能具有与副本(仍有大量已删除文档)足够不同的索引统计信息,因此得分也不同。

# 解决此问题

解决此问题的建议方法是使用一个字符串,该字符串将登录的用户(例如用户id或会话id)标识为首选项。这确保了给定用户的所有查询总是会碰到相同的碎片,因此查询之间的分数保持更加一致。
这种解决方法还有另一个好处:当两个文档具有相同的分数时,默认情况下,它们将按照内部Lucene文档id(与无关)进行排序。然而,这些文档ID在同一碎片的副本之间可能不同。因此,通过总是命中同一个碎片,我们将获得具有相同分数的文档的更一致的排序_id

# 相关性看起来不对(Relevancy looks wrong)

如果您注意到具有相同内容的两个文档的得分不同,或者完全匹配的文档没有排在第一位,那么问题可能与分片有关。默认情况下,Elasticsearch让每个碎片负责生成自己的分数。然而,由于索引统计数据是得分的重要贡献者,只有当碎片具有类似的索引统计数据时,这才有效。假设是,由于默认情况下文档被均匀地路由到碎片,因此索引统计数据应该非常相似,评分也会如预期那样工作。但是,如果您:

  • 在索引时间使用路由,
  • 查询多个索引,
  • 或者索引中的数据太少

那么很有可能搜索请求中涉及的所有碎片都没有类似的索引统计信息,并且相关性可能很差。

# 解决这个问题

如果你有一个小的数据集,解决这个问题的最简单方法是将所有内容索引到一个只有一个shard()的索引中,这是默认的。然后,所有文档的索引统计信息都将相同,分数也将一致。index.number_of_shards:1
否则,解决此问题的建议方法是使用dfs_query_then_fetch搜索类型。这将使Elasticsearch对所有涉及的碎片执行初始往返,询问它们相对于查询的索引统计信息,然后协调节点将合并这些统计信息,并在要求碎片执行阶段时将合并的统计信息与请求一起发送,以便碎片可以使用这些全局统计信息,而不是它们自己的统计信息来执行scoring.query
在大多数情况下,这种额外的往返旅行应该非常便宜。但是,如果您的查询包含大量字段/术语或模糊查询,请注意,单独收集统计信息可能并不便宜,因为必须在术语词典中查找所有术语才能查找统计信息。

# 修改默认得分权重

# multi_match query
GET /_search
{
  "query": {
    "multi_match" : {
      "query":    "this is a test", 
      "fields": [ "subject", "message" ] 
    }
  }
}
1
2
3
4
5
6
7
8
9
# best_fields

当您在同一字段中搜索多个最佳单词时,best_fields类型最有用。例如,一个领域中的“棕色狐狸”比一个领域的“棕色”和另一领域的“狐狸”更有意义。

GET /_search
{
  "query": {
    "multi_match" : {
      "query":      "brown fox",
      "type":       "best_fields", //默认值
      "fields":     [ "subject", "message" ],
      "tie_breaker": 0.3 //改变权重为1.0-0.3=0.7,得分会将会降低。
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
# most_fields

每个匹配子句的得分相加,然后除以匹配子句的数量。

GET /_search
{
  "query": {
    "multi_match" : {
      "query":      "quick brown fox",
      "type":       "most_fields",
      "fields":     [ "title", "title.original", "title.shingles" ]
    }
  }
}
1
2
3
4
5
6
7
8
9
10
# cross_fields

cross_fields类型对于多个字段应该匹配的结构化文档特别有用。例如,当在first_name和last_name字段中查询“Will Smith”时,最佳匹配可能是一个字段中有“Will”,另一个字段为“Smith”。
这听起来像是most_fields的工作,但这种方法有两个问题。第一个问题是operator 和minimum_should_match是按字段而不是按项应用的。第二个问题与相关性有关:first_name和last_name字段中的不同词条频率可能会产生意想不到的结果。
例如,假设我们有两个人:“Will Smith”和“Smith Jones”。“Smith”作为姓氏很常见(因此重要性很低),但“Smith”这个名字很少见(因此重要性很大)。如果我们搜索“Will Smith”,“Smith Jones”文档可能会出现在更好匹配的“Will Smith”之上,因为first_name:Smith的得分超过了first_name:Will+last_name:smitth的综合得分。
cross_field类型试图在查询时采用以术语为中心的方法来解决这些问题。它首先将查询字符串分析为各个术语,然后在任何字段中查找每个术语,就好像它们是一个大字段一样。

PUT example/_doc/1
{
  "first_name":"Will",
  "last_name":"Smith"
}
PUT example/_doc/2
{
  "first_name":"Will",
  "last_name":"Smith"
}

GET example/_validate/query?explain=true
{
  "query": {
    "multi_match" : {
      "query":      "Will Smith",
      "type":       "cross_fields",
      "fields":     [ "first_name", "last_name" ],
      "operator":   "and"
    }
  }
}

#执行计划
{
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "failed" : 0
  },
  "valid" : true,
  "explanations" : [
    {
      "index" : "example",
      "valid" : true,
      "explanation" : "+blended(terms:[last_name:will, first_name:will]) +blended(terms:[last_name:smith, first_name:smith])"
    }
  ]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
GET example/_validate/query?explain=true
{
  "query": {
    "multi_match" : {
      "query":      "Will Smith",
      "type":       "most_fields",
      "fields":     [ "first_name", "last_name" ],
      "operator":   "and"
    }
  }
}

#执行计划
{
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "failed" : 0
  },
  "valid" : true,
  "explanations" : [
    {
      "index" : "example",
      "valid" : true,
      "explanation" : "((+last_name:will +last_name:smith) | (+first_name:will +first_name:smith))~1.0"
    }
  ]
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# boosting query

返回与肯定查询匹配的文档,同时降低与否定查询也匹配的文档的相关性得分。

GET /_search
{
  "query": {
    "boosting": {//增强查询
      "positive": {
        "term": {
          "text": "apple"
        }
      },
      "negative": {
        "term": {
          "text": "pie tart fruit crumble tree"
        }
      },
      "negative_boost": 0.5//0到1.0之间,降低与否定查询也匹配的文档的相关性得分
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# function_score query

允许您修改查询检索到的文档的分数。

PUT example/_doc/1
{
  "test":"bar 1"
}
PUT example/_doc/2
{
  "test":"cat 1"
}

GET example/_search
{
  "query": {
    "function_score": {
      "query": { "match_all": {} },
      "boost": "5", 
      "functions": [
        {
          "filter": { "match": { "test": "bar" } },
          //"random_score": {}, 
          "weight": 23
        },
        {
          "filter": { "match": { "test": "cat" } },
          "weight": 42
        }
      ],
      "max_boost": 42,
      //"score_mode": "max",
      "boost_mode": "multiply",//乘
      "min_score": 42
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# constant_score query

恒定得分。

GET /_search
{
  "query": {
    "constant_score": {
      "filter": {
        "term": { "user.id": "kimchy" }
      },
      "boost": 1.2
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
# dis_max query

析取最大查询。

GET /_search
{
  "query": {
    "dis_max": {
      "queries": [
        { "term": { "title": "Quick pets" } },
        { "term": { "body": "Quick pets" } }
      ],
      "tie_breaker": 0.7 //改变权重为1.0-0.7=0.3,得分会将会降低。
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12