问题
问卷中有如下这样的文档,开发者想通过 match query 搜索这些文档来使用分数。
1. POST sample-index-test/_doc/1
2. {
3. "first_name": "James",
4. "last_name" : "Osaka"
5. }
以下是对上述文档的示例查询:
1. GET sample-index-test/_explain/1
2. {
3. "query": {
4. "match": {
5. "first_name": "James"
6. }
7. }
8. }
上述命令给出来的结果是:
1. {
2. "_index": "sample-index-test",
3. "_id": "1",
4. "matched": true,
5. "explanation": {
6. "value": 0.2876821,
7. "description": "weight(first_name:james in 0) [PerFieldSimilarity], result of:",
8. "details": [
9. {
10. "value": 0.2876821,
11. "description": "score(freq=1.0), computed as boost * idf * tf from:",
12. "details": [
13. {
14. "value": 2.2,
15. "description": "boost",
16. "details": []
17. },
18. {
19. "value": 0.2876821,
20. "description": "idf, computed as log(1 + (N - n + 0.5) / (n + 0.5)) from:",
21. "details": [
22. {
23. "value": 1,
24. "description": "n, number of documents containing term",
25. "details": []
26. },
27. {
28. "value": 1,
29. "description": "N, total number of documents with field",
30. "details": []
31. }
32. ]
33. },
34. {
35. "value": 0.45454544,
36. "description": "tf, computed as freq / (freq + k1 * (1 - b + b * dl / avgdl)) from:",
37. "details": [
38. {
39. "value": 1,
40. "description": "freq, occurrences of term within document",
41. "details": []
42. },
43. {
44. "value": 1.2,
45. "description": "k1, term saturation parameter",
46. "details": []
47. },
48. {
49. "value": 0.75,
50. "description": "b, length normalization parameter",
51. "details": []
52. },
53. {
54. "value": 1,
55. "description": "dl, length of field",
56. "details": []
57. },
58. {
59. "value": 1,
60. "description": "avgdl, average length of field",
61. "details": []
62. }
63. ]
64. }
65. ]
66. }
67. ]
68. }
69. }
如你所知,Elasticsearch 根据相关性对文档进行评分。 在为该文档建立索引后,让我们现在搜索索引。我们目前只有一份关于该索引的文档。
1. GET sample-index-test/_search
2. {
3. "query": {
4. "match": {
5. "first_name": "James"
6. }
7. }
8. }
搜索后,你将看到以下结果:
1. {
2. "took": 0,
3. "timed_out": false,
4. "_shards": {
5. "total": 1,
6. "successful": 1,
7. "skipped": 0,
8. "failed": 0
9. },
10. "hits": {
11. "total": {
12. "value": 1,
13. "relation": "eq"
14. },
15. "max_score": 0.2876821,
16. "hits": [
17. {
18. "_index": "sample-index-test",
19. "_id": "1",
20. "_score": 0.2876821,
21. "_source": {
22. "first_name": "James",
23. "last_name": "Osaka"
24. }
25. }
26. ]
27. }
28. }
我想提请你注意结果的 _score 字段。 如你所见,我们文档的 _score 值为 0.2876821 。 例如,当你多次更新文档时,假设我们使用以下请求更新了记录 10 次:
1. POST sample-index-test/_update/1
2. {
3. "script" : "ctx._source.first_name = 'James'; ctx._source.last_name = 'Cena';"
4. }
6. 或者
8. POST sample-index-test/_doc/1
9. {
10. "first_name": "James",
11. "last_name" : "Cena"
12. }
不会有任何添加到索引中。 我们又有了一份文件,没有了。 我们刚刚更新了文档的 last_name 字段。 让我们再次进行精确搜索并尝试查看结果:
1. GET sample-index-test/_search
2. {
3. "query": {
4. "match": {
5. "first_name": "James"
6. }
7. }
8. }
上面的命令显示的结果是:
1. {
2. "took": 0,
3. "timed_out": false,
4. "_shards": {
5. "total": 1,
6. "successful": 1,
7. "skipped": 0,
8. "failed": 0
9. },
10. "hits": {
11. "total": {
12. "value": 1,
13. "relation": "eq"
14. },
15. "max_score": 0.046520013,
16. "hits": [
17. {
18. "_index": "sample-index-test",
19. "_id": "1",
20. "_score": 0.046520013,
21. "_source": {
22. "first_name": "James",
23. "last_name": "Cena"
24. }
25. }
26. ]
27. }
28. }
正如你在此处看到的,分数发生了变化。 该文档的分数现在为 0.046520013 。 但根据 TF/IDF 计算,我们需要看到与我们的第一个搜索响应相同的分数。 因为当我们将它与文档的第一个状态进行比较时,没有任何变化。 即使我没有更改 first name 字段,我也只是更改了 last_name 字段并继续搜索 first_name 。 让我们对 _explain 端点进行更多挖掘。
1. GET sample-index-test/_explain/1
2. {
3. "query": {
4. "match": {
5. "first_name": "James"
6. }
7. }
8. }
Explain API 端点将为查询和特定文档计算得分解释。 上述请求的结果如下所示:
1. {
2. "_index": "sample-index-test",
3. "_id": "1",
4. "matched": true,
5. "explanation": {
6. "value": 0.046520013,
7. "description": "weight(first_name:james in 0) [PerFieldSimilarity], result of:",
8. "details": [
9. {
10. "value": 0.046520013,
11. "description": "score(freq=1.0), computed as boost * idf * tf from:",
12. "details": [
13. {
14. "value": 2.2,
15. "description": "boost",
16. "details": []
17. },
18. {
19. "value": 0.046520017,
20. "description": "idf, computed as log(1 + (N - n + 0.5) / (n + 0.5)) from:",
21. "details": [
22. {
23. "value": 10,
24. "description": "n, number of documents containing term",
25. "details": []
26. },
27. {
28. "value": 10,
29. "description": "N, total number of documents with field",
30. "details": []
31. }
32. ]
33. },
34. {
35. "value": 0.45454544,
36. "description": "tf, computed as freq / (freq + k1 * (1 - b + b * dl / avgdl)) from:",
37. "details": [
38. {
39. "value": 1,
40. "description": "freq, occurrences of term within document",
41. "details": []
42. },
43. {
44. "value": 1.2,
45. "description": "k1, term saturation parameter",
46. "details": []
47. },
48. {
49. "value": 0.75,
50. "description": "b, length normalization parameter",
51. "details": []
52. },
53. {
54. "value": 1,
55. "description": "dl, length of field",
56. "details": []
57. },
58. {
59. "value": 1,
60. "description": "avgdl, average length of field",
61. "details": []
62. }
63. ]
64. }
65. ]
66. }
67. ]
68. }
69. }
去掉一些对我们来说是可选的部分。 现在让我们关注 IDF 计算。 如你所知,反向文档频率(Inverse Document Frequency)查看一个词在语料库中的常见(或不常见)程度。 这意味着我们将使用索引中的文档数来计算 IDF。有关 IDF 的更多知识,请阅读文章 “Elasticsearch:分布式计分”。
idf, computed as log(1 + (N - n + 0.5) / (n + 0.5))
正如你在上面看到的,我们使用的是文档总数,但问题是我们在索引中有一个文档,但它显示的是 10。
1. {
2. "value": 10,
3. "description": "n, number of documents containing term",
4. "details": []
5. },
6. {
7. "value": 10,
8. "description": "N, total number of documents with field",
9. "details": []
10. }
因此,如果你使用此分数来计算其他服务的内容,这就是问题所在。
为什么会这样?
Elasticsearch 使用 Lucene 并将所有文档存储在段中。 段(segment)是不可变的,文档更新操作有两步过程。 更新文档时,将创建一个新文档,并将旧文档标记为已删除。 所以,当你在 Elasticsearch 索引中创建第一个文档时,Elasticsearch 会将它保存在一个段中,并且只有一个文档。 然后你更新同一个文档 10 次; 在任何更新操作中,Elasticsearch 都会在一个段中创建另一个文档,并将最旧的文档标记为已删除。 但是当你搜索索引时,你会从段中找到最新的文档状态。 暂时删除的文档数量为10。你会再次搜索到文档的最新状态,但 Elasticsearch 会继续在内部统计它们以进行IDF 计算。 因此,每次更新后,“the number of documents with field” 和 “number of documents containing term” 都会发生变化。
解决方案
如你所知,如果你知道什么是段,这个问题会在一段时间后自行解决。 所以,如果你想自己做这件事而不等待,你需要使用 _forcemerge。 我需要在这里放一个来自 Elasticsearch 文档的解释。在我们稍微等一段时间后,我们再去搜索,我们将会看到最终的分数和我们刚开始搜索的结果是一样的。
合并通过将其中的一些合并在一起来减少每个分片中的段数,并且还释放已删除文档所使用的空间。 合并通常会自动发生,但有时手动触发合并很有用。
我们建议只强制合并只读索引(意味着索引不再接收写入)。
为了对我们的索引执行 _forcemerge,我们使用了以下请求:
POST sample-index-test/_forcemerge
根据你的索引大小,此请求可能需要一些时间,你可以通过在 Kibana 上执行以下请求来完成任务:
GET _tasks?actions=*forcemerge*&detailed
另一种方法就是等待。 Elasticsearch 还有一个调度程序和合并策略来自动合并段。 在使用强制合并之前,我建议仔细阅读相关的官方文档。
最后,还有一个索引生命周期操作,用于使用策略执行强制合并操作。 根据你的逻辑,你可以使用不同的解决方案来获得更好的搜索评分结果。