目录
1.简介
段落匹配(phrase matching)或者模糊匹配(proximity matching)
字词词语之间的关系
1.1.短语匹配
match_phrase 查询首先将查询字符串解析成一个词项列表,然后对这些词项进行搜索,但只保留那些包含 全部 搜索词项,且 位置 与搜索词项相同的文档。
GET /my_index/my_type/_search
{
"query": {
"match_phrase": {
"title": "quick brown fox"
}
}
}
等价
{
"match": {
"title": {
"query": "quick brown fox",
"type": "phrase"
}
}
}
词项的位置
当一个字符串被分词后,这个分析器不但会返回一个词项列表,而且还会返回各词项在原始字符串中的 位置 或者顺序关系
【举例】
GET /_analyze?analyzer=standard
Quick brown fox
【结果】
{
"tokens": [
{
"token": "quick",
"start_offset": 0,
"end_offset": 5,
"type": "<ALPHANUM>",
"position": 1 // 代表各词项在原始字符串中的位置
},
{
"token": "brown",
"start_offset": 6,
"end_offset": 11,
"type": "<ALPHANUM>",
"position": 2
},
{
"token": "fox",
"start_offset": 12,
"end_offset": 15,
"type": "<ALPHANUM>",
"position": 3
}
]
}
【说明】
位置信息可以被存储在倒排索引中,因此 match_phrase
查询这类对词语位置敏感的查询, 就可以利用位置信息去匹配包含所有查询词,且各词项顺序也与我们搜索指定一致的文档,中间不夹杂其他词项
什么是短语
一个被认定为和短语 quick brown fox 匹配的文档,必须满足以下这些要求:
1. quick 、 brown 和 fox 需要全部出现在域中。
2. brown 的位置应该比 quick 的位置大 1。
3. fox 的位置应该比 quick 的位置大 2。
如果以上任何一个选项不成立,则该文档不能认定为匹配。
注意
本质上来讲,match_phrase 查询是利用一种低级别的 span 查询族(query family)去做词语位置敏感的匹配。
Span 查询是一种词项级别的查询,所以它们没有分词阶段;它们只对指定的词项进行精确搜索。
值得庆幸的是,match_phrase 查询已经足够优秀,大多数人是不会直接使用 span 查询。
然而,在一些专业领域,例如专利检索,还是会采用这种低级别查询去执行非常具体而又精心构造的位置搜索。
混合起来
通过使用 slop
参数将灵活度引入短语匹配
【举例】
GET /my_index/my_type/_search
{
"query": {
"match_phrase": {
"title": {
"query": "quick fox",
"slop": 1
}
}
}
}
【说明】
slop
参数告诉 match_phrase
查询词条相隔多远时仍然能将文档视为匹配 。相隔多远的意思是为了让查询和文档匹配你需要移动词条多少次?
1.2.多值字段
【举例】
PUT /my_index/groups/1
{
"names": [ "John Abraham", "Lincoln Smith"]
}
GET /my_index/groups/_search
{
"query": {
"match_phrase": {
"names": "Abraham Lincoln"
}
}
}
【结果】
匹配到结果,但是这不是想要的
在分析 John Abraham
的时候, 产生了如下信息:
-
Position 1:
john
-
Position 2:
abraham
然后在分析 Lincoln Smith
的时候, 产生了:
-
Position 3:
lincoln
-
Position 4:
smith
【解决方法】
DELETE /my_index/groups/
PUT /my_index/_mapping/groups
{
"properties": {
"names": {
"type": "string",
"position_increment_gap": 100
}
}
}
[分析]
-
Position 1:
john
-
Position 2:
abraham
-
Position 103:
lincoln
-
Position 104:
smith
为了匹配这个文档你必须添加值为 100 的 slop 。
匹配词之间的距离越近,相关度 _score越高
【举例】
POST /my_index/my_type/_search
{
"query": {
"match_phrase": {
"title": {
"query": "quick dog",
"slop": 50
}
}
}
}
【结果】
{
"hits": [
{
"_id": "3",
"_score": 0.75, // 分数较高因为 quick
和 dog
很接近
"_source": {
"title": "The quick brown fox jumps over the quick dog"
}
},
{
"_id": "2",
"_score": 0.28347334, // 分数较低因为 quick
和 dog
分开较远
"_source": {
"title": "The quick brown fox jumps over the lazy dog"
}
}
]
}
1.3.使用邻近度提高相关度
虽然邻近查询很有用, 但是所有词条都出现在文档的要求过于严格了。
比如,如果七个词条中有六个匹配, 那么这个文档对用户而言就已经足够相关了, 但是 match_phrase
查询可能会将它排除在外。
实际上我们想将多个查询的分数累计起来意味着我们应该用 bool
查询将它们合并。
可以将一个简单的 match
查询作为一个 must
子句。 这个查询将决定哪些文档需要被包含到结果集中。 可以用 minimum_should_match
参数去除长尾。 然后可以以 should
子句的形式添加更多特定查询。 每一个匹配成功的都会增加匹配文档的相关度。
GET /my_index/my_type/_search
{
"query": {
"bool": {
"must": { // 从结果集中包含或者排除文档
"match": {
"title": {
"query": "quick brown fox",
"minimum_should_match": "30%"
}
}
},
"should": { // 增加了匹配到文档的相关度评分
"match_phrase": {
"title": {
"query": "quick brown fox",
"slop": 50
}
}
}
}
}
}
1.4.性能优化
短语查询和邻近查询都比简单的 query
查询代价更高。 一个 match
查询仅仅是看词条是否存在于倒排索引中,而一个 match_phrase
查询是必须计算并比较多个可能重复词项的位置。
Lucene nightly benchmarks 表明一个简单的 term 查询比一个短语查询大约快 10 倍,比邻近查询(有 slop 的短语 查询)大约快 20 倍。
当然,这个代价指的是在搜索时而不是索引时。
通常,短语查询的额外成本并不像这些数字所暗示的那么吓人。
事实上,性能上的差距只是证明一个简单的 term 查询有多快。
标准全文数据的短语查询通常在几毫秒内完成,因此实际上都是完全可用,即使是在一个繁忙的集群上。
在某些特定病理案例下,短语查询可能成本太高了,但比较少见。
一个典型例子就是DNA序列,在序列里很多同样的词项在很多位置重复出现。
在这里使用高 slop 值会到导致位置计算大量增加。
那么应该如何限制短语查询和邻近近查询的性能消耗呢?一种有用的方法是减少需要通过短语查询检查的文档总数。
结果集重新评分
一个简单的 match
查询已经通过排序把包含所有含有搜索词条的文档放在结果列表的前面了。事实上,我们只想对这些 顶部文档重新排序,来给同时匹配了短语查询的文档一个额外的相关度升级。
search
API 通过 重新评分 明确支持该功能。重新评分阶段支持一个代价更高的评分算法--比如 phrase
查询--只是为了从每个分片中获得前 K
个结果。 然后会根据它们的最新评分 重新排序。
【举例】
GET /my_index/my_type/_search
{
"query": { // 决定哪些文档将包含在最终结果集中,并通过 TF/IDF 排序。
"match": {
"title": {
"query": "quick brown fox",
"minimum_should_match": "30%"
}
}
},
"rescore": {
"window_size": 50, // 每一分片进行重新评分的顶部文档数量
"query": { // 重新打分算法
"rescore_query": {
"match_phrase": {
"title": {
"query": "quick brown fox",
"slop": 50
}
}
}
}
}
}
寻找相关词
短语查询和邻近查询都很好用,但仍有一个缺点。它们过于严格了:为了匹配短语查询,所有词项都必须存在,即使使用了 slop
。
用 slop
得到的单词顺序的灵活性也需要付出代价,因为失去了单词对之间的联系。即使可以识别 sue
、 alligator
和 ate
相邻出现的文档,但无法分辨是 Sue ate 还是 alligator ate 。
当单词相互结合使用的时候,表达的含义比单独使用更丰富。两个子句 I’m not happy I’m working 和 I’m happy I’m not working包含相同 的单词,也拥有相同的邻近度,但含义截然不同。
如果索引单词对而不是索引独立的单词,就能对这些单词的上下文尽可能多的保留。
对句子 Sue ate the alligator
,不仅要将每一个单词(或者 unigram )作为词项索引
["sue", "ate", "the", "alligator"]
也要将每个单词 以及它的邻近词 作为单个词项索引:
["sue ate", "ate the", "the alligator"]
这些单词对(或者 bigrams )被称为 shingles 。
Shingles 不限于单词对;你也可以索引三个单词( trigrams ):
["sue ate the", "ate the alligator"]
Trigrams 提供了更高的精度,但是也大大增加了索引中唯一词项的数量。在大多数情况下,Bigrams 就够了。
当然,只有当用户输入的查询内容和在原始文档中顺序相同时,shingles 才是有用的;对 sue alligator
的查询可能会匹配到单个单词,但是不会匹配任何 shingles 。
幸运的是,用户倾向于使用和搜索数据相似的构造来表达搜索意图。但这一点很重要:只是索引 bigrams 是不够的;我们仍然需要 unigrams ,但可以将匹配 bigrams 作为增加相关度评分的信号。
在 shingles
字段上的匹配是充当一 种信号--为了提高相关度评分
生成 Shingles
Shingles 需要在索引时作为分析过程的一部分被创建。
【举例】
DELETE /my_index
PUT /my_index
{
"settings": {
"number_of_shards": 1,
"analysis": {
"filter": {
"my_shingle_filter": {
"type": "shingle",
"min_shingle_size": 2, // 默认最小/最大的 shingle 大小是 2
,所以实际上不需要设置。
"max_shingle_size": 2,
"output_unigrams": false // shingle
语汇单元过滤器默认输出 unigrams ,但是我们想让 unigrams 和 bigrams 分开。
}
},
"analyzer": {
"my_shingle_analyzer": {
"type": "custom",
"tokenizer": "standard",
"filter": [
"lowercase",
"my_shingle_filter" // 语汇单元过滤器
]
}
}
}
}
}
【测试】
GET /my_index/_analyze?analyzer=my_shingle_analyzer
Sue ate the alligator
【结果】
-
sue ate
-
ate the
-
the alligator
将 unigrams 和 bigrams 分开索引更清晰
PUT /my_index/_mapping/my_type
{
"my_type": {
"properties": {
"title": {
"type": "string",
"fields": {
"shingles": {
"type": "string",
"analyzer": "my_shingle_analyzer"
}
}
}
}
}
}
通过这个映射, JSON 文档中的 title
字段将会被以 unigrams (title
)和 bigrams (title.shingles
)被索引,这意味着可以独立地查询这些字段。
【搜索 Shingles】
GET /my_index/my_type/_search
{
"query": {
"match": {
"title": "the hungry alligator ate sue"
}
}
}
【结果】
{
"hits": [
{
"_id": "1",
"_score": 0.44273707, // 包含 the
、 alligator
和 ate
"_source": {
"title": "Sue ate the alligator"
}
},
{
"_id": "2",
"_score": 0.44273707, // 包含 the
、 alligator
和 ate
"_source": {
"title": "The alligator ate Sue"
}
},
{
"_id": "3",
"_score": 0.046571054,
"_source": {
"title": "Sue never goes anywhere without her alligator skin purse"
}
}
]
}
【添加 shingles
字段】
GET /my_index/my_type/_search
{
"query": {
"bool": {
"must": {
"match": {
"title": "the hungry alligator ate sue"
}
},
"should": {
"match": {
"title.shingles": "the hungry alligator ate sue"
}
}
}
}
}
【结果】
{
"hits": [
{
"_id": "2",
"_score": 0.4883322,
"_source": {
"title": "The alligator ate Sue"
}
},
{
"_id": "1",
"_score": 0.13422975,
"_source": {
"title": "Sue ate the alligator"
}
},
{
"_id": "3",
"_score": 0.014119488,
"_source": {
"title": "Sue never goes anywhere without her alligator skin purse"
}
}
]
}
即使查询包含的单词 hungry
没有在任何文档中出现,我们仍然使用单词邻近度返回了最相关的文档。
Performance性能
shingles 不仅比短语查询更灵活,而且性能也更好。 shingles 查询跟一个简单的 match
查询一样高效,而不用每次搜索花费短语查询的代价。只是在索引期间因为更多词项需要被索引会付出一些小的代价, 这也意味着有 shingles 的字段会占用更多的磁盘空间。 然而,大多数应用写入一次而读取多次,所以在索引期间优化我们的查询速度是有意义的。
这是一个在 Elasticsearch 里会经常碰到的话题:不需要任何前期进行过多的设置,就能够在搜索的时候有很好的效果。 一旦更清晰的理解了自己的需求,就能在索引时通过正确的为你的数据建模获得更好结果和性能。