作者:来自 Elastic Panagiotis Bailis
Elasticsearch 检索器经过了重大改进,现在可供所有人使用。了解其架构和用例。
在这篇博文中,我们将再次深入探讨检索器(retrievers)。我们已经在之前的博文中讨论过它们,从介绍到使用检索器进行语义重新排序。现在,我们很高兴地宣布,检索器已随 Elasticsearch 8.16.0 已正式发布,在这篇博文中,我们将从技术角度介绍如何实现它们,并有机会讨论新推出的功能!
检索器 - retrievers
检索器(retriever)的主要概念与最初版本相同;检索器是一个框架,它提供可以分层堆叠的基本构建块,以构建多阶段复杂检索和排名管道。例如,一个简单的 standard 检索器,它只返回所有文档:
1. GET retrievers_example/_search
2. {
3. "retriever": {
4. "standard": {
5. "query": {
6. "match_all": {}
7. }
8. }
9. }
10. }
非常简单,对吧?除了 standard 检索器(本质上只是 standard query 搜索 API 元素的包装器)之外,我们还支持以下类型:
- knn - 从 kNN(k 最近邻)搜索中返回排名靠前的文档
- rrf - 根据 RRF(倒数排名融合)排名公式组合来自不同检索器的结果
- text_similarity_reranker - 使用 rerank 类型推断端点对嵌套检索器的排名靠前结果进行重新排名
还可以在 Elasticsearch 文档中找到更多详细信息以及每个检索器的特定参数。
让我们首先简要介绍一些技术细节,这将有助于我们了解架构、发生了什么变化以及为什么所有这些以前的限制现在都已解除!
技术深入研究
我们想要解决的最重要的(也是要求的)问题之一是能够在任何嵌套级别使用任何检索器。无论这意味着将 2 个或更多 text_similarity_reranker 堆叠在一起,还是将 rrf 检索器与 text_similarity_reranker 一起在另一个 rrf 之上运行,或者你能想到的任何组合和嵌套,我们都希望确保这是可以用检索器表达的东西!
为了解决这个问题,我们对检索器执行计划进行了一些重大更改。到目前为止,检索器是作为 standard 搜索执行流程的一部分进行评估的,其中(在简化的场景中为了说明目的)我们两次接触分片(shard):
- 一次用于查询分片并从每个分片中带回 from + size 文档,
- 一次用于获取所有字段数据并执行任何其他操作(例如突出显示/highlighting)以获得真正的顶级 [from, from+size] 结果。
这是一个不错的线性执行流程,(相对)容易遵循,但如果我们想要执行多个查询、对不同的结果集进行操作等,就会引入一些重大限制。为了解决这个问题,我们在查询执行的早期阶段就转向了对检索器管道的所有子检索器进行急切评估。这意味着,如果需要,我们会以递归方式将任何检索器查询重写为更简单的形式,具体形式取决于检索器类型。
- 对于非复合检索器(non-compound retrievers),我们重写的方式与在 standard 查询中类似,因为它们仍然可以遵循线性执行计划。
- 对于复合检索器(compound retrievers),即在其他检索器之上操作的检索器,我们将它们展平为单个 rank_window_size 结果集,它本质上是一个 <doc, shard> 元组列表,表示此检索器的排名靠前的文档。
让我们通过以下(相当复杂的)检索器请求来看看它实际上是什么样子:
1. {
2. "retriever": {
3. "rrf": { [1]
4. "retrievers": [
5. {
6. "knn": { [2]
7. "field": "emb1",
8. "query_vector_builder": {
9. "text_embedding": {
10. "model_id": "my-text-embedding-model",
11. "model_text": "LLM applications in information retrieval"
12. }
13. }
14. }
15. },
16. {
17. "standard": { [3]
18. "query": {
19. "term": {
20. "topic": "science"
21. }
22. }
23. }
24. },
25. {
26. "rrf": { [4]
27. "retrievers": [
28. {
29. "standard": { [5]
30. "query": {
31. "range": {
32. "year": {
33. "gte": 2020
34. }
35. }
36. }
37. }
38. },
39. {
40. "knn": { [6]
41. "field": "emb2",
42. "query_vector_builder": {
43. "text_embedding": {
44. "model_id": "my-text-embedding-model",
45. "model_text": "Vector scale on production systems"
46. }
47. }
48. }
49. }
50. ],
51. "rank_window_size": 100,
52. "rank_constant": 10
53. }
54. }
55. ],
56. "rank_window_size": 10,
57. "rank_constant": 1
58. }
59. }
60. }
上面的 rrf 检索器是一个复合检索器,因为它对其他一些检索器的结果进行操作,因此我们将尝试将其重写为更简单、扁平的 <doc, shard> 元组列表,其中每个元组指定一个文档和它所在的分片。此重写还将强制执行严格的排名,因此当前不支持不同的排序选项。
现在让我们继续识别所有组件并描述如何评估它的过程:
- [1] 顶级 rrf 检索器;这是所有子检索器的父级,它将最后被重写和评估,因为我们首先需要知道每个子检索器的前 10 个结果(基于 rank_window_size)。
- [2] 这个 knn 检索器是顶级 rrf 检索器的第一个子级,并使用嵌入服务(my-text-embedding-model)来计算将使用的实际查询向量。这将通过向嵌入服务发出异步请求来计算给定 model_text 的向量,从而将其重写为通常的 knn 查询。
- [3] standard 检索器,也是顶级 rrf 检索器的子项的一部分,它返回与主题匹配的所有文档:science。
- [4] 顶级 rrf 检索器的最后一个子项,也是需要展平的 rrf 检索器。
- [5] [6] 与 [2] 和 [3] 类似,这些检索器是 rrf 检索器的直接子项,我们将为每个检索器获取前 100 个结果(基于 rrf 检索器的 rank_window_size [4]),使用 rrf 公式将它们组合起来,然后重写为真正的前 100 个结果的展平 <doc, shard> 列表。
检索器的更新执行流程现在如下:
- 我们将从重写所有我们可以重写的叶子开始。这意味着我们将重写 knn 检索器 [2] 和 [6] 来计算查询向量,一旦我们有了它,我们就可以在树中向上移动一层。
- 在下一个重写步骤中,我们现在准备评估嵌套的 rrf 检索器 [4],我们最终会将其重写为扁平化的 RankDocsQuery 查询(即 <doc, shard> 元组的列表)。
- 最后,顶级 rrf 检索器 [1] 的所有内部重写步骤都将完成,因此我们应该准备好按照要求合并和排名真正的前 10 个结果。即使是这个顶级 rrf 检索器也会将自身重写为扁平化的 RankDocsQuery,稍后将用于继续执行 standard 线性搜索执行流程。
将以上所有内容可视化,我们有:
查看上面的示例,我们可以看到如何将分层检索器树异步重写为简单的 RankDocsQuery。这种简化为我们带来了良好的(也是我们想要的!)副作用,即最终执行具有明确排名的正常请求,除此之外,我们还可以执行我们选择的任何补充操作。
玩转(golden)检索器!
正如我们上面简要提到的,有了这一重构,我们现在可以支持大量额外的搜索功能!在本节中,我们将介绍一些示例和使用场景,但更多内容也可以在文档中找到。
我们从最受欢迎的功能 —— 组合性开始,也就是说,在检索器树的任何级别都可以使用任意检索器的选项。
组合性 - composabilty
在以下示例中,我们想执行一个语义查询(使用像 ELSER 这样的嵌入服务),然后结合这些结果与 knn 查询,使用 rrf 进行合并。最后,我们希望使用 text_similarity_reranker 检索器进行重新排名。表达上述操作的检索器如下所示:
1. GET /retrievers_example/_search
2. {
3. "retriever": {
4. "text_similarity_retriever": {
5. "retriever": {
6. "rrf": {
7. "retrievers": [
8. {
9. "standard": {
10. "query": {
11. "semantic": {
12. "field": "inference_field",
13. "query": "Can I use generative AI to identify user intent and improve search relevance?"
14. }
15. }
16. }
17. },
18. {
19. "knn": {
20. "field": "vector",
21. "query_vector": [
22. 0.23,
23. 0.67,
24. 0.89
25. ],
26. "k": 3,
27. "num_candidates": 5
28. }
29. }
30. ],
31. "rank_window_size": 10,
32. "rank_constant": 1
33. }
34. },
35. "field": "text",
36. "inference_text": "LLM applications on production search applications",
37. "inference_id": "my-reranker-model",
38. "rank_window_size": 10
39. }
40. },
41. "_source": [
42. "text",
43. "topic"
44. ]
45. }
聚合 - aggregation
回想一下,在我们讨论的重做中,我们将复合检索器重写为 RankDocsQuery(即扁平的显式排名结果列表)。然而,这并不妨碍我们计算聚合,因为我们还会跟踪复合检索器中的源查询。这意味着我们可以回退到下面的嵌套 standard 检索器,以根据两个嵌套检索器结果的并集正确计算 topic 字段的聚合。
1. GET retrievers_example/_search
2. {
3. "retriever": {
4. "rrf": {
5. "retrievers": [
6. {
7. "standard": {
8. "query": {
9. "range": {
10. "year": {
11. "gt": 2023
12. }
13. }
14. }
15. }
16. },
17. {
18. "standard": {
19. "query": {
20. "term": {
21. "topic": "elastic"
22. }
23. }
24. }
25. }
26. ],
27. "rank_window_size": 10,
28. "rank_constant": 1
29. }
30. },
31. "_source": [
32. "text",
33. "topic"
34. ],
35. "aggs": {
36. "topics": {
37. "terms": {
38. "field": "topic"
39. }
40. }
41. }
42. }
因此,在上面的例子中,我们将计算 topic 字段的术语聚合,其中年份字段大于 2023,或者文档具有与之关联的主题 elastic。
折叠 - collapsing
除了我们上面讨论的聚合选项之外,我们现在还可以折叠结果,就像我们对 standard 查询请求所做的那样。在下面的示例中,我们计算 rrf 检索器的前 10 个结果,然后将它们折叠在 year 字段下。与 standard 搜索的主要区别在于,这里我们只折叠排名靠前的结果,而不是嵌套检索器中的结果。
1. GET /retrievers_example/_search
2. {
3. "retriever": {
4. "rrf": {
5. "retrievers": [
6. {
7. "text_similarity_reranker": {
8. "retriever": {
9. "standard": {
10. "query": {
11. "term": {
12. "topic": "ai"
13. }
14. }
15. }
16. },
17. "field": "text",
18. "inference_text": "Can I use generative AI to identify user intent and improve search relevance?",
19. "rank_window_size": 10,
20. "inference_id": "my-reranker-model"
21. }
22. },
23. {
24. "knn": {
25. "field": "vector",
26. "query_vector":
27. [
28. 0.23,
29. 0.67,
30. 0.89
31. ],
32. "k": 3,
33. "num_candidates": 5
34. }
35. }
36. ],
37. "rank_window_size": 10,
38. "rank_constant": 1
39. }
40. },
41. "collapse": {
42. "field": "year",
43. "inner_hits": {
44. "name": "year_results",
45. "_source": [
46. "text",
47. "year"
48. ]
49. }
50. },
51. "_source": [
52. "text",
53. "topic"
54. ]
55. }
分页 - pagination
正如文档中所述,复合检索器也支持分页。与 standard 查询相比,复合检索器有一个显著的区别,与上面的折叠类似,rank_window_size 参数是我们可以执行导航的整个结果集。这意味着,如果 from + size > rank_window_size,那么我们将不会返回任何结果(但我们仍会返回聚合)。
1. GET /retrievers_example/_search
2. {
3. "retriever": {
4. "rrf": {
5. "retrievers": [
6. {
7. "standard": {
8. "query": {
9. "term": {
10. "topic": "elastic"
11. }
12. }
13. }
14. },
15. {
16. "knn": {
17. "field": "vector",
18. "query_vector":
19. [
20. 0.23,
21. 0.67,
22. 0.89
23. ],
24. "k": 3,
25. "num_candidates": 5
26. }
27. }
28. ],
29. "rank_window_size": 10,
30. "rank_constant": 1
31. }
32. },
33. "from": 2,
34. "size": 2
35. "_source": [
36. "text",
37. "topic"
38. ]
39. }
在上面的例子中,我们将从两个嵌套检索器(standard 和 knn)的组合中计算前 10 个结果(如 rrf 的 rank_window_size 中定义),然后我们将通过查阅 from 和 size 参数来执行分页。因此,在这种情况下,我们将跳过前 2 个结果(from)并选择接下来的 2 个结果(size)。
现在考虑一个不同的场景,在上面的相同查询中,我们将改为使用 from:10 和 size:2。假设 rank_window_size 为 10,并且这些将是我们可以分页的所有结果,在跳过前 10 个结果后请求获取 2 个结果将超出可导航结果集,因此我们将返回空结果。在 rrf 检索器的文档中还可以找到其他示例和更详细的细分。
解释 - explain
我们知道能力越大,责任越大。鉴于我们现在可以任意组合检索器,因此可能很难理解为什么最终会首先返回某个结果,以及如何优化我们的检索策略。出于这个非常具体的原因,我们努力确保检索器请求的解释输出(即通过指定 explain: true)将传达所有子检索器的所有必要信息,以便我们能够正确理解导致结果最终排名的所有因素。以 Collapsing 部分中相当复杂的查询为例,第一个结果的解释如下所示:
1. {
2. "_explanation":{
3. "value": 0.8333334,
4. "description": "sum of:",
5. "details": [
6. {
7. "value": 0.8333334,
8. "description": "rrf score: [0.8333334] computed for initial ranks [2, 1] with rankConstant: [1] as sum of [1 / (rank + rankConstant)] for each query",
9. "details": [
10. {
11. "value": 2,
12. "description": "rrf score: [0.33333334], for rank [2] in query at index [0] computed as [1 / (2 + 1)], for matching query with score",
13. "details": [
14. {
15. "value": 0.0011925492,
16. "description": "text_similarity_reranker match using inference endpoint: [my-awesome-rerank-model] on document field: [text] matching on source query ",
17. "details": [
18. {
19. "value": 0.3844723,
20. "description": "weight(topic:ai in 1) [PerFieldSimilarity], result of:",
21. "details":
22. [
23. ...
24. ]
25. }
26. ]
27. }
28. ]
29. },
30. {
31. "value": 1,
32. "description": "rrf score: [0.5], for rank [1] in query at index [1] computed as [1 / (1 + 1)], for matching query with score",
33. "details":
34. [
35. {
36. "value": 1,
37. "description": "doc [1] with an original score of [1.0] is at rank [1] from the following source queries.",
38. "details":
39. [
40. {
41. "value": 1,
42. "description": "found vector with calculated similarity: 1.0",
43. "details":
44. []
45. }
46. ]
47. }
48. ]
49. }
50. ]
51. }
52. ]
53. }
54. }
仍然有点冗长,但它传达了有关文档为何位于特定位置的所有必要信息。对于顶级 rrf 检索器,我们指定了 2 个details 信息,每个嵌套检索器一个。第一个是 text_similarity_reranker 检索器,我们可以在其中看到重新排序操作的权重,第二个是 knn 查询,告知我们文档与查询向量的计算相似性。可能需要一点时间才能熟悉,但每个检索器都会确保输出你可能需要评估和优化搜索场景的所有信息!
结论
现在就这些了!我们希望您一直关注我们,并且喜欢这个主题!我们对 retriever 框架的发布以及我们现在可以支持的所有新用例感到非常兴奋!检索器是为了支持从非常简单的搜索到高级 RAG 和混合搜索场景而构建的!如上所述,请关注此空间,更多功能即将推出!
Elasticsearch 包含新功能,可帮助您为你的用例构建最佳搜索解决方案。立即开始免费云试用或在你的本地机器上试用 Elastic。
原文:Elasticsearch retrievers are generally available with Elasticsearch 8.16.0! - Search Labs