作者:来自 Elastic Kathleen DeRusso
语义文本搜索变得更加强大,现已原生支持 match、knn 和 sparse_vector 查询。这让我们能够保留语义查询的简洁性,同时提供 Elasticsearch 查询 DSL 的灵活性。
Elasticsearch 的语义查询功能非常强大,允许用户对配置为 [semantic_text](https://www.elastic.co/guide/en/elasticsearch/reference/current/semantic-text.html "semantic_text")
字段的数据执行语义搜索。这种强大的核心在于其简洁性:你只需设置一个带有推理端点的 semantic_text
字段,然后像向普通 text 字段一样写入内容即可。推理过程会自动且透明地进行,使得构建带有语义功能的搜索索引变得非常简单。
这种易用性确实带来了一些权衡:我们通过对默认行为做出一些判断,简化了 semantic_text
的语义搜索,这些默认设置适合大多数使用场景。但遗憾的是,这也意味着一些传统向量搜索查询所支持的自定义选项并未包含在语义查询(semantic query)中。我们并不想直接将所有这些选项添加到语义查询中,因为这会破坏我们追求的简洁性。因此,我们扩展了支持 semantic_text
字段的查询类型,交由你来选择最适合自己需求的查询方式。
接下来,我们一起看看这些变化,从创建一个包含 semantic_text
字段的简单索引开始:
perl
1. PUT index-songs-semantic
2. {
3. "mappings": {
4. "properties": {
5. "song_title": {
6. "type": "text"
7. },
8. "artist": {
9. "type": "keyword"
10. },
11. "lyric": {
12. "type": "semantic_text"
13. }
14. }
15. }
16. }
18. // Now index a sample document
19. POST index-songs-semantic/_doc/1
20. {
21. "song_title": "...Baby One More Time",
22. "artist": "Britney Spears",
23. "lyric": "When I'm with you, I lose my mind, give me a sign"
24. }
我们让 match 也支持了!
首先,也是最重要的一点,match 查询现在可以用于 semantic_text
字段了!
这意味着你可以将之前的语义查询改成这样:
bash
1. GET index-songs-semantic/_search
2. {
3. "query": {
4. "semantic": {
5. "field": "lyric",
6. "query": "song lyrics about love"
7. }
8. }
9. }
改成一个简单的 match 查询:
bash
1. GET index-songs-semantic/_search
2. {
3. "query": {
4. "match": {
5. "lyric": "song lyrics about love"
6. }
7. }
8. }
我们可以从这个示例中看到语义搜索的优势,因为我们搜索的是 "song lyrics about love",而这些词并没有直接出现在被索引的文档中。这正是因为 ELSER 进行了文本扩展(text expansion)。
但先别急,事情还没完!
如果你有多个索引,并且同一个字段名在一个索引里是 semantic_text
,而在另一个索引里可能是普通的 text
,你依然可以对这些字段运行 match 查询。
下面是一个简单的示例,展示如何创建另一个索引,这个索引使用相同的字段名,但字段类型是 text
(而不是 semantic_text
):
perl
1. // Setup - Create a similar index without semantic fields
2. PUT index-songs-lexical
3. {
4. "mappings": {
5. "properties": {
6. "song_title": {
7. "type": "text"
8. },
9. "artist": {
10. "type": "keyword"
11. },
12. "lyric": {
13. "type": "text"
14. }
15. }
16. }
17. }
19. POST index-songs-lexical/_doc/2
20. {
21. "song_title": "Crazy",
22. "artist": "Britney Spears",
23. "lyric": "You drive me crazy, I just can't sleep, I'm so excited, I'm in too deep"
24. }
26. GET index-songs-semantic,index-songs-lexical/_search
27. {
28. "query": {
29. "match": {
30. "lyric": "crazy"
31. }
32. }
33. }
在这个例子中,搜索 "crazy" 时,既能找到标题(lyric 字段)中包含 "crazy" 的词汇匹配结果,也能找到语义匹配到的歌词 "lose my mind"。
不过,在使用 match
查询结合 semantic_text
字段时,有一些注意事项:
-
底层的
semantic_text
字段有一个限制:同一个字段不能使用多个推理 ID(inference ID) 。这个限制同样适用于match
查询 ------ 也就是说,如果你有两个同名的semantic_text
字段,它们必须使用相同的推理 ID ,否则会报错。你可以通过创建不同的字段名,并在一个 bool 查询 或组合检索器(compound retriever)中查询它们来规避这个问题。
-
不同模型的分数范围可能差异很大,所以词汇匹配(lexical match)和语义匹配 的得分往往不可直接比较。为了获得最佳的结果排序,我们建议使用二阶段重排序 (second stage rerankers),比如语义重排序(semantic reranking)或 RRF(Reciprocal Rank Fusion)。
另外,使用 match
查询进行语义搜索,现在也支持 ES|QL !下面是一个和前面类似的例子,不过这次使用的是 ES|QL:
python
1. POST _query?format=txt
2. {
3. "query": """
4. FROM index-songs-semantic,index-songs-lexical METADATA _score
5. | WHERE MATCH(lyric, "crazy")
6. | KEEP artist, song_title, lyric, _score
7. | SORT _score
8. | LIMIT 5
9. """
10. }
专业级语义搜索:支持 knn 和 sparse_vector
match
查询非常方便,但有时候,你可能希望指定比语义查询(semantic query)更多的向量搜索选项。还记得吗?我们为了让语义查询尽可能简单,做了一些默认行为的取舍。
这意味着,如果你想利用一些更高级的向量搜索功能,比如 knn 查询中的 num_candidates 或 filter ,或者在 sparse_vector 查询中使用 token pruning(分词裁剪),你是没办法通过语义查询直接实现的。
过去,我们确实提供过一些变通方案,但这些方案比较复杂,需要你深入了解 semantic_text
字段的内部结构和架构,然后手动构建对应的嵌套查询。如果你现在还在用这种方式,其实它依然可行 ------ 不过,现在你已经可以直接通过查询 DSL,使用 knn
或 sparse_vector
查询对 semantic_text
字段进行搜索。
全都是 dense(向量),毫无压力
下面是一个示例脚本,用于填充 text_embedding
模型并使用 knn
查询对 semantic_text
字段进行查询:
perl
1. PUT index-dense-semantic-songs
2. {
3. "mappings": {
4. "properties": {
5. "song_title": {
6. "type": "text"
7. },
8. "artist": {
9. "type": "keyword"
10. },
11. "lyric": {
12. "type": "semantic_text",
13. "inference_id": ".multilingual-e5-small-elasticsearch"
14. }
15. }
16. }
17. }
19. // Index sample documents
20. POST index-dense-semantic-songs/_doc/4
21. {
22. "song_title": "Oops! ...I Did It Again",
23. "artist": "Britney Spears",
24. "lyric": "Oops, I did it again, I played with your heart, got lost in the game."
25. }
27. POST index-dense-semantic-songs/_doc/5
28. {
29. "song_title": "Poker Face",
30. "artist": "Lady Gaga",
31. "lyric": "Can't read my, can't read my, no, he can't read my poker face"
32. }
34. GET index-dense-semantic-songs/_search
35. {
36. "query": {
37. "knn": {
38. "field": "lyric",
39. "k": 10,
40. "num_candidates": 100,
41. "query_vector_builder": {
42. "text_embedding": {
43. "model_text": "game"
44. }
45. }
46. }
47. }
48. }
knn
查询可以通过额外的选项进行修改,以便在 semantic_text
字段上执行更高级的查询。在这里,我们执行相同的查询,但增加了对 semantic_text
字段的预过滤:
bash
1. GET index-dense-semantic-songs/_search
2. {
3. "query": {
4. "knn": {
5. "field": "lyric",
6. "k": 10,
7. "num_candidates": 100,
8. "query_vector_builder": {
9. "text_embedding": {
10. "model_text": "game"
11. }
12. },
13. "filter": {
14. "term": {
15. "artist": "Britney Spears"
16. }
17. }
18. }
19. }
20. }
保持稀疏(向量),保持真实
类似地,稀疏嵌入模型也可以通过 semantic_text
字段进行更具体的查询。下面是一个示例脚本,展示如何添加一些文档,并使用 sparse_vector
查询:
bash
1. POST index-songs-semantic/_doc/6
2. {
3. "song_title": "Crazy In Love",
4. "artist": "Beyoncé",
5. "lyric": "Looking so crazy, your love's got me looking, got me looking so crazy in love"
6. }
9. POST index-songs-semantic/_doc/7
10. {
11. "song_title": "Complicated",
12. "artist": "Avril Lavigne",
13. "lyric": "Why'd you have to go and make things so complicated?, I see the way you're acting like you're somebody else"
14. }
16. GET index-songs-semantic/_search
17. {
18. "query": {
19. "sparse_vector": {
20. "field": "lyric",
21. "query": "crazy"
22. }
23. }
24. }
sparse_vector
查询可以通过额外的选项进行修改,以便在 semantic_text
字段上执行更高级的查询。在这里,我们执行相同的查询,但对 semantic_text
字段添加了 token pruning(分词裁剪):
bash
1. GET index-songs-semantic/_search
2. {
3. "query": {
4. "sparse_vector": {
5. "field": "lyric",
6. "query": "crazy",
7. "prune": true,
8. "pruning_config": {
9. "tokens_freq_ratio_threshold": 1,
10. "tokens_weight_threshold": 0.4,
11. "only_score_pruned_tokens": false
12. }
13. }
14. }
15. }
这个例子显著降低了进行分词裁剪所需的词频比率,这有助于我们在如此小的数据集上展示差异,尽管这些设置可能比你在生产环境中希望看到的更为激进(请记住,分词裁剪 的目的是裁剪无关的分词,以提高性能,而不是大幅改变召回率或相关性)。在这个例子中,我们可以看到 Avril Lavigne 的歌曲不再被返回,且由于裁剪了分词,得分发生了变化。(请注意,这只是一个示例,我们仍然建议在大多数使用场景中进行重排序,将裁剪掉的分词重新加入评分过程。)
你会注意到,在所有这些查询中,如果你仅查询 semantic_text
字段,就不再需要在 knn
的 query_vector_builder
或 sparse_vector
查询中指定推理 ID。这是因为推理 ID 将从 semantic_text
字段中自动推断出来。不过,如果你想为某些原因覆盖并使用不同的(兼容的)推理 ID,或者如果你在搜索同时包含 semantic_text
和 [sparse_vector](https://www.elastic.co/guide/en/elasticsearch/reference/current/sparse-vector.html "sparse_vector")
或 [dense_vector](https://www.elastic.co/guide/en/elasticsearch/reference/current/dense-vector.html "dense_vector")
字段的联合索引时,你仍然可以指定推理 ID。
亲自试试
我们保持原始的语义查询简单,同时扩展了语义搜索的功能,以支持更多的使用场景,并无缝地将语义搜索与现有工作流集成。这些增强功能是 Elasticsearch 原生支持的,并且已经在 Serverless 中可用。从 8.18 版本开始,它们将可以在堆栈托管的 Elasticsearch 中使用。
今天就试试吧!
Elasticsearch 拥有众多新功能,帮助你为特定场景构建最佳搜索解决方案。深入了解我们的示例笔记本,开始免费的云试用,或者现在就尝试在本地机器上使用 Elastic。