安顺市网站建设_网站建设公司_数据备份_seo优化
2026/1/9 15:34:40 网站建设 项目流程

作者:来自 Elastic Ioana Tagirta

亲身体验 Elasticsearch:深入了解我们的示例 notebooks,开始免费的 cloud 试用,或立即在本地机器上试用 Elastic。


在 Elasticsearch 9.2 中,我们引入了在 Elasticsearch Query Language( ES|QL )中进行密集 vector search 和 hybrid search 的能力。这延续了我们将 ES|QL 打造成解决现代 search 用例的最佳 search 语言的投入。

多阶段检索:现代搜索的挑战

现代搜索已经不再只是简单的关键词匹配。今天的搜索应用需要理解意图,处理自然语言,并结合多个排序信号来提供最佳结果。

最相关结果的检索是在多个阶段中完成的,每个阶段都会逐步细化结果集。过去并非如此,当时大多数用例只需要一到两个阶段的检索:一次初始查询来获取结果,以及一个可能的重新评分阶段。

我们从初始检索开始,在这一阶段我们广泛搜索以收集与我们查询相关的结果。由于需要筛选所有数据,我们应该使用能够快速返回结果的技术,即使在索引数十亿文档时也能高效。

因此,我们采用可靠的技术,例如 Elasticsearch 从一开始就支持并优化的词汇搜索,或者 Elasticsearch 在速度和准确性上表现出色的 vector search。

使用 BM25 的 lexical search 非常快,最适合精确的术语匹配或短语匹配,而 vector 或 semantic search 更适合处理自然语言查询。Hybrid search 将 lexical 和 vector search 结果结合起来,以发挥两者的优势。Hybrid search 解决的挑战在于 vector 和 lexical search 拥有完全不同且不兼容的评分函数,它们生成的值在不同区间,并遵循不同分布。vector search 得分接近 1 可能意味着非常匹配,但 lexical search 并非如此。Hybrid search 方法,如 reciprocal rank fusion (RRF) 和 scores 的线性组合,会分配新的分数,将 lexical 和 vector search 的原始分数融合。

在 hybrid search 之后,我们可以使用 semantic reranking 和 Learning To Rank (LTR) 等技术,这些技术使用专门的 machine learning 模型对结果进行重新排序。

利用最相关的结果,我们可以使用 large language models (LLMs) 进一步丰富我们的响应,或在 Elastic Agent Builder 等工具的 agentic 工作流中,将最相关结果作为上下文传递给 LLMs。

ES|QL 能够处理检索的所有这些阶段。ES|QL 本身是一个管道语言,每条命令会转换输入并将输出发送到下一条命令。每个检索阶段由一个或多个连续的 ES|QL 命令表示。本文展示了 ES|QL 如何支持每个阶段。

Vector search - 向量搜索

在 Elasticsearch 9.2 中,我们在 ES|QL 中引入了密集 vector search 的技术预览支持。这和调用 knn 函数一样简单,只需要一个 dense_vector 字段和一个 query vector:

FROM books METADATA _score | WHERE KNN(description_vector, ?query_vector) | SORT _score DESC | LIMIT 100

此查询执行近似最近邻搜索,检索与 query_vector 最相似的 100 个文档。

混合搜索:Reciprocal rank fusion/RRF

在 Elasticsearch 9.2 中,我们在 ES|QL 中引入了使用 RRF 和结果线性组合的 hybrid search 支持。

这允许将 vector search 和 lexical search 结果合并为单一的结果集。

要在 ES|QL 中实现这一点,我们需要使用 FORK 和 FUSE 命令。FORK可以运行多个执行分支,FUSE则合并结果,并使用 RRF 或线性组合分配新的相关性分数。

在下面的示例中,我们使用 FORK 运行两个独立的分支,其中一个使用 match 函数进行 lexical search,另一个使用 knn 函数进行 vector search。然后我们使用 FUSE 将结果合并:

FROM books METADATA _score, _id, _index | FORK (WHERE KNN(description_vector, ?query_vector) | SORT _score DESC | LIMIT 100) (WHERE MATCH(description, ?query) | SORT _score DESC | LIMIT 100) | FUSE // uses RRF by default | SORT _score DESC

让我们分解查询以更好地理解执行模型,首先来看 FORK 命令的输出:

FROM books METADATA _score, _id, _index | FORK (WHERE KNN(description_vector, ?query_vector) | SORT _score DESC | LIMIT 100) (WHERE MATCH(description, ?query) | SORT _score DESC | LIMIT 100)

FORK 命令输出来自两个分支的结果,并添加了一个 _fork 鉴别器列:

_idtitle_score_fork
4001The Hobbit0.88fork1
3999The Fellowship of the Ring0.88fork1
4005The Two Towers0.86fork1
4006The Return of the King0.84fork1
4123The Silmarillion0.78fork1
4144The Children of Húrin0.79fork1
4001The Hobbit4.55fork2
3999The Fellowship of the Ring4.25fork2
4123The Silmarillion4.11fork2
4005The Two Towers3.8fork2
4006The Return of the King4.1fork2

正如你会注意到的,某些文档会出现两次,这就是我们随后使用 FUSE 来合并表示相同文档的行并分配新的相关性分数的原因。FUSE 分两阶段执行:

  • 对每一行,FUSE 根据所使用的 hybrid search 算法分配新的相关性分数。
  • 表示相同文档的行会被合并,并计算新的分数。

在我们的示例中,我们使用 RRF。第一步,FUSE 使用 RRF 公式为每一行分配新的分数:

score(doc) = 1 / (rank_constant + rank(doc))

其中 rank_constant 的默认值为 60,rank(doc) 表示文档在结果集中的位置。

在第一阶段,我们的结果变为:

_idtitle_score_fork
4001The Hobbit1 / (60 + 1) = 0.01639fork1
3999The Fellowship of the Ring1 / (60 + 2) = 0.01613fork1
4005The Two Towers1 / (60 + 3) = 0.01587fork1
4006The Return of the King1 / (60 + 4) = 0.01563fork1
4123The Silmarillion1 / (60 + 5) = 0.01538fork1
4144The Children of Húrin1 / (60 + 6) = 0.01515fork1
4001The Hobbit1 / (60 + 1) = 0.01639fork2
3999The Fellowship of the Ring1 / (60 + 2) = 0.01613fork2
4123The Silmarillion1 / (60 + 3) = 0.01587fork2
4005The Two Towers1 / (60 + 4) = 0.01563fork2
4006The Return of the King1 / (60 + 5) = 0.01538fork2

然后这些行会被合并,并分配新的分数。由于 FUSE 命令后跟 SORT _score DESC,最终结果为:

_idtitle_score
4001The Hobbit0.01639 + 0.01639 = 0.03279
3999The Fellowship of the Ring0.01613 + 0.01613 = 0.03226
4005The Two Towers0.01587 + 0.01563 = 0.0315
4123The Silmarillion0.01538 + 0.01587 = 0.03125
4006The Return of the King0.01563 + 0.01538 = 0.03101
4144The Children of Húrin0.01515

混合搜索:scores 的线性组合

Reciprocal rank fusion 是执行 hybrid search 最简单的方法,但它不是我们在 ES|QL 中支持的唯一 hybrid search 方法。

在下面的示例中,我们使用 FUSE 通过 scores 的线性组合来合并 lexical 和 semantic search 结果:

FROM books METADATA _score, _id, _index | FORK (WHERE MATCH(semantic_description, ?query) | SORT _score DESC | LIMIT 100) (WHERE MATCH(description, ?query) | SORT _score DESC | LIMIT 100) | FUSE LINEAR WITH { "weights": { "fork1": 0.7, "fork2": 0.3 } } | SORT _score DESC

让我们先分解查询,并查看当只运行 FORK 命令时 FUSE 命令的输入。

注意,我们使用 match 函数,它不仅可以查询 lexical 字段,如 text 或 keyword,还可以查询 semantic_text 字段。

第一个 FORK 分支通过查询 semantic_text 字段执行 semantic query,而第二个分支执行 lexical query:

FROM books METADATA _score, _id, _index | FORK (WHERE MATCH(semantic_description, ?query) | SORT _score DESC | LIMIT 100) (WHERE MATCH(description, ?query) | SORT _score DESC | LIMIT 100)

FORK 命令的输出可能包含具有相同 _id 和 _index 值的行,这些行表示同一个 Elasticsearch 文档:

_idtitle_score_fork
4001The Hobbit0.88fork1
3999The Fellowship of the Ring0.88fork1
4005The Two Towers0.86fork1
4006The Return of the King0.84fork1
4123The Silmarillion0.78fork1
4144The Children of Húrin0.79fork1
4001The Hobbit4.55fork2
3999The Fellowship of the Ring4.25fork2
4123The Silmarillion4.11fork2
4005The Two Towers3.8fork2
4006The Return of the King4.1fork2

在下一步,我们使用 FUSE 合并具有相同 _id 和 _index 值的行,并分配新的相关性分数。

新的分数是该行在每个 FORK 分支中的分数的线性组合:

_score = 0.7 *_score1 + 0.3 * _score2

这里,_score1 和 _score2 分别表示文档在第一个 FORK 分支和第二个 FORK 分支中的分数。

注意,我们还应用了自定义权重,对 semantic score 给予比 lexical score 更高的权重,得到这一组文档:

_idtitle_score
4001The Hobbit0.7 * 0.88 + 0.3 * 4.55 = 1.981
3999The Fellowship of the Ring0.7 * 0.88 + 0.3 * 4.25 = 1.891
4006The Return of the King0.7 * 0.84 + 0.3 * 4.1 = 1.818
4123The Silmarillion0.7 * 0.78 + 0.3 * 4.11 = 1.779
4005The Two Towers0.7 * 0.86 + 0.3 * 3.8 = 1.742
4144The Children of Húrin0.7 * 0.79 + 0.3 * 0 = 0.553

一个挑战是 semantic score 和 lexical score 可能不兼容直接进行线性组合,因为它们可能遵循完全不同的分布。为缓解这一问题,我们首先需要对分数进行归一化,使用 score normalization 方法,如 minmax。这样可以确保每个 FORK 分支的分数在应用线性组合公式前先被归一化到 0 到 1 之间的值。

要使用 FUSE 实现这一点,我们需要指定 normalizer 选项:

FROM books METADATA _score, _id, _index | FORK (WHERE MATCH(semantic_description, ?query) | SORT _score DESC | LIMIT 100) (WHERE MATCH(description, ?query) | SORT _score DESC | LIMIT 100) | FUSE LINEAR WITH { "weights": { "fork1": 0.7, "fork2": 0.3 }, "normalizer": "minmax" } | SORT _score DESC

Semantic reranking

在这一阶段,经过 hybrid search 后,我们应该只剩下最相关的文档。我们现在可以使用 semantic reranking 通过 RERANK 命令重新排序结果。默认情况下,RERANK 使用最新的 Elastic semantic reranking 机器学习模型,因此无需额外配置:

FROM books METADATA _score, _id, _index | FORK (WHERE KNN(description_vector, ?query_vector) | SORT _score DESC | LIMIT 100) (WHERE MATCH(description, ?query) | SORT _score DESC | LIMIT 100) | FUSE | SORT _score DESC | LIMIT 100 | RERANK ?query ON description | SORT _score DESC

我们现在得到了按相关性排序的最佳结果。

RERANK 命令区别于其他提供 semantic reranking 集成的产品的一个关键特性是,它不要求输入必须是索引中的映射字段。RERANK 只需要一个能求值为字符串的表达式,因此可以使用多个字段进行 semantic reranking:

FROM books METADATA _score, _id, _index | FORK (WHERE KNN(description_vector, ?query_vector) | SORT _score DESC | LIMIT 100) (WHERE MATCH(description, ?query) | SORT _score DESC | LIMIT 100) | FUSE | SORT _score DESC | LIMIT 100 | RERANK ?query ON CONCAT(title, "\n", description) | SORT _score DESC

LLM completions

现在我们有了一组高度相关、重新排序的结果。

在这一阶段,你可以选择直接将结果返回给你的应用,也可以使用 LLM completions 进一步增强结果。

如果你在 retrieval-augmented generation (RAG) 工作流中使用 ES|QL,你可以选择直接从 ES|QL 调用你喜欢的 LLM。
为此,我们新增了 COMPLETION 命令,它接受一个 prompt、一个 completion inference ID(指定调用哪个 LLM)以及一个列标识符(指定 LLM 响应输出到哪一列)。

在下面的示例中,我们使用 COMPLETION 添加了一个新的 _completion 列,其中包含 content 列的摘要:

FROM books METADATA _score, _id, _index | FORK (WHERE KNN(description_vector, ?query_vector) | SORT _score DESC | LIMIT 100) (WHERE MATCH(description, ?query) | SORT _score DESC | LIMIT 100) | FUSE | SORT _score DESC | LIMIT 100 | RERANK ?query ON description | SORT _score DESC | LIMIT 10 | COMPLETION CONCAT("Summarize the following:\n", description) WITH { "inference_id" : "my_inference_endpoint" }

每一行现在都包含一个摘要:

_idtitle_scoresummary
4001The Hobbit0.03279Bilbo helps dwarves reclaim Erebor from the dragon Smaug.
3999The Fellowship of the Ring0.03226Frodo begins the quest to destroy the One Ring.
4005The Two Towers0.0315The Fellowship splits; war comes to Rohan; Frodo nears Mordor.
4123The Silmarillion0.03125Ancient myths and history of Middle-earth's First Age.
4006The Return of the King0.3101Sauron is defeated and Aragorn is crowned King.
4144The Children of Húrin0.01515The tragic tale of Túrin Turambar's cursed life.

在另一种用例中,你可能只是想使用自己在 Elasticsearch 中索引的专有数据来回答问题。在这种情况下,我们在前一阶段计算出的最佳搜索结果可以作为 prompt 的上下文:

FROM books METADATA _score, _id, _index | FORK (WHERE KNN(description_vector, ?query_vector) | SORT _score DESC | LIMIT 100) (WHERE MATCH(description, ?query) | SORT _score DESC | LIMIT 100) | FUSE | SORT _score DESC | LIMIT 100 | RERANK ?query ON description | SORT _score DESC | LIMIT 10 | STATS context = VALUES(CONCAT(title, "\n", description) | COMPLETION CONCAT("Answer the following question ", ?query, "based on:\n", context) WITH { "inference_id" : "my_inference_endpoint" }

由于 COMPLETION 命令解锁了向 LLM 发送任意 prompt 的能力,可能性是无穷的。虽然我们只展示了一些示例,但 COMPLETION 命令可以用于各种场景,从安全分析师根据日志事件是否可能表示恶意行为来分配分数,到数据科学家用它分析数据,甚至到仅仅根据你的数据生成 Chuck Norris 事实的情况。

这只是开始

未来,我们将扩展 ES|QL,以改进长文档的 semantic reranking,更好地使用多个 FORK 命令进行 ES|QL 查询的条件执行,支持 sparse vector 查询,去除近似重复结果以提高结果多样性,允许对运行时生成的列进行全文搜索,以及更多其他场景。

更多教程和指南:

  • ES|QL for search
  • ES|QL for search tutorial
  • Semantic_text field type
  • FORK and FUSE 文档
  • ES|QL search functions

原文:https://www.elastic.co/search-labs/blog/hybrid-search-multi-stage-retrieval-esql

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询