elasticsearch 深入 —— 近似匹配
近似匹配
使用 TF/IDF 的标准全文检索将文档或者文档中的字段作一大袋的词语处理。 match
查询可以告知我们这大袋子中是否包含查询的词条,但却无法告知词语之间的关系。
思考下面这几个句子的不同:
- Sue ate the alligator.
- The alligator ate Sue.
- Sue never goes anywhere without her alligator-skin purse.
用 match
搜索 sue alligator
上面的三个文档都会得到匹配,但它却不能确定这两个词是否只来自于一种语境,甚至都不能确定是否来自于同一个段落。
理解分词之间的关系是一个复杂的难题,我们也无法通过换一种查询方式去解决。但我们至少可以通过出现在彼此附近或者仅仅是彼此相邻的分词来判断一些似乎相关的分词。
每个文档可能都比我们上面这个例子要长: Sue
和 alligator
这两个词可能会分散在其他的段落文字中,我们可能会希望得到尽可能包含这两个词的文档,但我们也同样需要这些文档与分词有很高的相关度。
这就是短语匹配或者近似匹配的所属领域。
首先,我们使用 bulk
API 创建一些新的文档和索引:
DELETE /my_index
PUT /my_index
{ "settings": { "number_of_shards": 1 }}
POST /my_index/my_type/_bulk
{ "index": { "_id": 1 }}
{ "title": "The quick brown fox" }
{ "index": { "_id": 2 }}
{ "title": "The quick brown fox jumps over the lazy dog" }
{ "index": { "_id": 3 }}
{ "title": "The quick brown fox jumps over the quick dog" }
{ "index": { "_id": 4 }}
{ "title": "Brown fox brown dog" }
短语匹配
就像 match
查询对于标准全文检索是一种最常用的查询一样,当你想找到彼此邻近搜索词的查询方法时,就会想到 match_phrase
查询 。
GET /my_index/my_type/_search
{
"query": {
"match_phrase": {
"title": "quick brown fox"
}
}
}
类似 match
查询, match_phrase
查询首先将查询字符串解析成一个词项列表,然后对这些词项进行搜索,但只保留那些包含 全部 搜索词项,且 位置 与搜索词项相同的文档。 比如对于 quick fox
的短语搜索可能不会匹配到任何文档,因为没有文档包含的 quick
词之后紧跟着 fox
。
match_phrase
查询同样可写成一种类型为 phrase
的 match
查询:
"match": {
"title": {
"query": "quick brown fox",
"type": "phrase"
}
}
词项的位置
当一个字符串被分词后,这个分析器不但会 返回一个词项列表,而且还会返回各词项在原始字符串中的 位置 或者顺序关系:
PSOT /_analyze
{
"text": "Quick brown fox",
"analyzer":"standard"
}
返回信息如下:
{
"tokens": [
{
"token": "quick",
"start_offset": 0,
"end_offset": 5,
"type": "<ALPHANUM>",
"position": 0
},
{
"token": "brown",
"start_offset": 6,
"end_offset": 11,
"type": "<ALPHANUM>",
"position": 1
},
{
"token": "fox",
"start_offset": 12,
"end_offset": 15,
"type": "<ALPHANUM>",
"position": 2
}
]
}
position
代表各词项在原始字符串中的位置。
位置信息可以被存储在倒排索引中,因此 match_phrase
查询这类对词语位置敏感的查询, 就可以利用位置信息去匹配包含所有查询词项,且各词项顺序也与我们搜索指定一致的文档,中间不夹杂其他词项。
什么是短语
一个被认定为和短语 quick brown fox
匹配的文档,必须满足以下这些要求:
quick
、brown
和fox
需要全部出现在域中。brown
的位置应该比quick
的位置大1
。fox
的位置应该比quick
的位置大2
。
如果以上任何一个选项不成立,则该文档不能认定为匹配。
本质上来讲,
match_phrase
查询是利用一种低级别的span
查询族(query family)去做词语位置敏感的匹配。 Span 查询是一种词项级别的查询,所以它们没有分词阶段;它们只对指定的词项进行精确搜索。值得庆幸的是,
match_phrase
查询已经足够优秀,大多数人是不会直接使用span
查询。 然而,在一些专业领域,例如专利检索,还是会采用这种低级别查询去执行非常具体而又精心构造的位置搜索。
混合起来
精确短语匹配 或许是过于严格了。也许我们想要包含 “quick brown fox” 的文档也能够匹配 “quick fox,” , 尽管情形不完全相同。
我们能够通过使用 slop
参数将灵活度引入短语匹配中:
POST /my_index/my_type/_search
{
"query": {
"match_phrase": {
"title": {
"query": "quick fox",
"slop": 1
}
}
}
}
slop
参数告诉 match_phrase
查询词条相隔多远时仍然能将文档视为匹配 。 相隔多远的意思是为了让查询和文档匹配你需要移动词条多少次?
我们以一个简单的例子开始吧。 为了让查询 quick fox
能匹配一个包含 quick brown fox
的文档, 我们需要 slop
的值为 1
:
Pos 1 Pos 2 Pos 3
-----------------------------------------------
Doc: quick brown fox
-----------------------------------------------
Query: quick fox
Slop 1: quick ↳ fox
尽管在使用了 slop
短语匹配中所有的单词都需要出现, 但是这些单词也不必为了匹配而按相同的序列排列。 有了足够大的 slop
值, 单词就能按照任意顺序排列了。
POST /my_index/my_type/_search
{
"query": {
"match_phrase": {
"body": {
"query": "fox quick",
"slop": 3
}
}
}
}
"hits": {
"total": 1,
"max_score": 0.43569252,
"hits": [
{
"_index": "my_index",
"_type": "my_type",
"_id": "2",
"_score": 0.43569252,
"_source": {
"title": "Keeping pets healthy",
"body": "My quick brown fox eats rabbits on a regular basis."
}
}
]
}
为了使查询 fox quick
匹配我们的文档, 我们需要 slop
的值为 3
:
Pos 1 Pos 2 Pos 3
-----------------------------------------------
Doc: quick brown fox
-----------------------------------------------
Query: fox quick
Slop 1: fox|quick ↵
Slop 2: quick ↳ fox
Slop 3: quick ↳ fox
注意
fox
和quick
在这步中占据同样的位置。 因此将fox quick
转换顺序成quick fox
需要两步, 或者值为2
的slop
。
多值字段
对多值字段使用短语匹配时会发生奇怪的事。 想象一下你索引这个文档:
PUT /my_index/groups/1
{
"names": [ "John Abraham", "Lincoln Smith"]
}
然后运行一个对 Abraham Lincoln
的短语查询:
POST /my_index/groups/_search
{
"query": {
"match_phrase": {
"names": "Abraham Lincoln"
}
}
}
令人惊讶的是, 即使 Abraham
和 Lincoln
在 names
数组里属于两个不同的人名, 我们的文档也匹配了查询。 这一切的原因在Elasticsearch数组的索引方式。
在分析 John Abraham
的时候, 产生了如下信息:
- Position 1:
john
- Position 2:
abraham
然后在分析 Lincoln Smith
的时候, 产生了:
- Position 3:
lincoln
- Position 4:
smith
换句话说, Elasticsearch对以上数组分析生成了与分析单个字符串 John Abraham Lincoln Smith
一样几乎完全相同的语汇单元。 我们的查询示例寻找相邻的 lincoln
和 abraham
, 而且这两个词条确实存在,并且它们俩正好相邻, 所以这个查询匹配了。
幸运的是, 在这样的情况下有一种叫做 position_increment_gap
的简单的解决方案, 它在字段映射中配置 。
DELETE /my_index/groups/
PUT /my_index/_mapping/groups
{
"properties": {
"names": {
"type": "text",
"position_increment_gap": 100
}
}
}
首先删除映射 groups 以及这个类型内的所有文档。
然后创建一个有正确值的新的映射 groups
position_increment_gap
设置告诉 Elasticsearch 应该为数组中每个新元素增加当前词条 position
的指定值。 所以现在当我们再索引 names 数组时,会产生如下的结果:
- Position 1:
john
- Position 2:
abraham
- Position 103:
lincoln
- Position 104:
smith
现在我们的短语查询可能无法匹配该文档因为 abraham
和 lincoln
之间的距离为 100 。 为了匹配这个文档你必须添加值为 100 的 slop
。
越近越好
鉴于一个短语查询仅仅排除了不包含确切查询短语的文档, 而 邻近查询 — 一个 slop
大于 0
— 的短语查询将查询词条的邻近度考虑到最终相关度 _score
中。 通过设置一个像 50
或者 100
这样的高 slop
值, 你能够排除单词距离太远的文档, 但是也给予了那些单词临近的的文档更高的分数。
下列对 quick dog
的邻近查询匹配了同时包含 quick
和 dog
的文档, 但是也给了与 quick 和 dog 更加临近的文档更高的分数 :
POST /my_index/my_type/_search
{
"query": {
"match_phrase": {
"body": {
"query": "brown rabbits",
"slop": 50
}
}
}
}
注意高
slop
值。
"hits": {
"total": 2,
"max_score": 0.42221835,
"hits": [
{
"_index": "my_index",
"_type": "my_type",
"_id": "1",
"_score": 0.42221835,
"_source": {
"title": "Quick brown rabbits",
"body": "Brown rabbits are commonly seen."
}
},
{
"_index": "my_index",
"_type": "my_type",
"_id": "2",
"_score": 0.14585726,
"_source": {
"title": "Keeping pets healthy",
"body": "My quick brown fox eats rabbits on a regular basis."
}
}
]
}
分数较高因为 quick 和 dog 很接近
分数较低因为 quick 和 dog 分开较远
使用邻近度提高相关度
虽然邻近查询很有用, 但是所有词条都出现在文档的要求过于严格了。 我们讨论 全文搜索 一章的 控制精度 也是同样的问题: 如果七个词条中有六个匹配, 那么这个文档对用户而言就已经足够相关了, 但是 match_phrase
查询可能会将它排除在外。
相比将使用邻近匹配作为绝对要求, 我们可以将它作为 信号— 使用, 作为许多潜在查询中的一个, 会对每个文档的最终分值做出贡献 (参考 多数字段)。
实际上我们想将多个查询的分数累计起来意味着我们应该用 bool
查询将它们合并。
我们可以将一个简单的 match
查询作为一个 must
子句。 这个查询将决定哪些文档需要被包含到结果集中。 我们可以用 minimum_should_match
参数去除长尾。 然后我们可以以 should
子句的形式添加更多特定查询。 每一个匹配成功的都会增加匹配文档的相关度。
post /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
}
}
}
}
}
}
must 子句从结果集中包含或者排除文档。
should 子句增加了匹配到文档的相关度评分。
我们当然可以在 should
子句里面添加其它的查询, 其中每一个查询只针对某一特定方面的相关度。
性能优化
短语查询和邻近查询都比简单的 query
查询代价更高 。 一个 match
查询仅仅是看词条是否存在于倒排索引中,而一个 match_phrase
查询是必须计算并比较多个可能重复词项的位置。
Lucene nightly benchmarks 表明一个简单的 term
查询比一个短语查询大约快 10 倍,比邻近查询(有 slop
的短语 查询)大约快 20 倍。当然,这个代价指的是在搜索时而不是索引时。
通常,短语查询的额外成本并不像这些数字所暗示的那么吓人。事实上,性能上的差距只是证明一个简单的
term
查询有多快。标准全文数据的短语查询通常在几毫秒内完成,因此实际上都是完全可用,即使是在一个繁忙的集群上。在某些特定病理案例下,短语查询可能成本太高了,但比较少见。一个典型例子就是DNA序列,在序列里很多同样的词项在很多位置重复出现。在这里使用高
slop
值会到导致位置计算大量增加。
那么我们应该如何限制短语查询和邻近近查询的性能消耗呢?一种有用的方法是减少需要通过短语查询检查的文档总数。
结果集重新评分
在先前的章节中 ,我们讨论了而使用邻近查询来调整相关度,而不是使用它将文档从结果列表中添加或者排除。 一个查询可能会匹配成千上万的结果,但我们的用户很可能只对结果的前几页感兴趣。
一个简单的 match
查询已经通过排序把包含所有含有搜索词条的文档放在结果列表的前面了。事实上,我们只想对这些 顶部文档 重新排序,来给同时匹配了短语查询的文档一个额外的相关度升级。
search
API 通过 重新评分 明确支持该功能。重新评分阶段支持一个代价更高的评分算法--比如 phrase
查询--只是为了从每个分片中获得前 K
个结果。 然后会根据它们的最新评分 重新排序。
该请求如下所示:
POST /my_index/my_type/_search
{
"query": {
"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
}
}
}
}
}
}
match 查询决定哪些文档将包含在最终结果集中,并通过 TF/IDF 排序。
window_size 是每一分片进行重新评分的顶部文档数量。
目前唯一支持的重新打分算法就是另一个查询,但是以后会有计划增加更多的算法
寻找相关词
短语查询和邻近查询都很好用,但仍有一个缺点。它们过于严格了:为了匹配短语查询,所有词项都必须存在,即使使用了 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 需要在索引时作为分析过程的一部分被创建。 我们可以将 unigrams 和 bigrams 都索引到单个字段中, 但将它们分开保存在能被独立查询的字段会更清晰。unigrams 字段将构成我们搜索的基础部分,而 bigrams 字段用来提高相关度。
首先,我们需要在创建分析器时使用 shingle
语汇单元过滤器:
DELETE /my_index
PUT /my_index
{
"settings": {
"number_of_shards": 1,
"analysis": {
"filter": {
"my_shingle_filter": {
"type": "shingle",
"min_shingle_size": 2,
"max_shingle_size": 2,
"output_unigrams": false
}
},
"analyzer": {
"my_shingle_analyzer": {
"type": "custom",
"tokenizer": "standard",
"filter": [
"lowercase",
"my_shingle_filter"
]
}
}
}
}
}
参考 被破坏的相关度! 。
默认最小/最大的 shingle 大小是
2
,所以实际上不需要设置。
shingle
语汇单元过滤器默认输出 unigrams ,但是我们想让 unigrams 和 bigrams 分开。
my_shingle_analyzer
使用我们常规的my_shingles_filter
语汇单元过滤器。
首先,用 analyze
API 测试下分析器:
POST /my_index/_analyze
{
"text":"Sue ate the alligator",
"analyzer":"my_shingle_analyzer"
}
果然, 我们得到了 3 个词项:
sue ate
ate the
the alligator
现在我们可以继续创建一个使用新的分析器的字段。
多字段
我们曾谈到将 unigrams 和 bigrams 分开索引更清晰,所以 title
字段将创建成一个多字段(参考 字符串排序与多字段 ):
PUT /my_index/_mapping/my_type
{
"my_type": {
"properties": {
"title": {
"type": "text",
"fields": {
"shingles": {
"type": "text",
"analyzer": "my_shingle_analyzer"
}
}
}
}
}
}
通过这个映射, JSON 文档中的 title
字段将会被以 unigrams (title
)和 bigrams (title.shingles
)被索引,这意味着可以独立地查询这些字段。
最后,我们可以索引以下示例文档:
POST /my_index/my_type/_bulk
{ "index": { "_id": 1 }}
{ "title": "Sue ate the alligator" }
{ "index": { "_id": 2 }}
{ "title": "The alligator ate Sue" }
{ "index": { "_id": 3 }}
{ "title": "Sue never goes anywhere without her alligator skin purse" }
搜索 Shingles
为了理解添加 shingles
字段的好处 ,让我们首先来看 The hungry alligator ate Sue
进行简单 match
查询的结果:
POST /my_index/my_type/_search
{
"query": {
"match": {
"title": "the hungry alligator ate sue"
}
}
}
{
"hits": [
{
"_id": "1",
"_score": 0.44273707,
"_source": {
"title": "Sue ate the alligator"
}
},
{
"_id": "2",
"_score": 0.44273707,
"_source": {
"title": "The alligator ate Sue"
}
},
{
"_id": "3",
"_score": 0.046571054,
"_source": {
"title": "Sue never goes anywhere without her alligator skin purse"
}
}
]
}
两个文档都包含
the
、alligator
和ate
,所以获得相同的评分。我们可以通过设置
minimum_should_match
参数排除文档 3 ,参考 控制精度 。
现在在查询里添加 shingles
字段。不要忘了在 shingles
字段上的匹配是充当一 种信号--为了提高相关度评分--所以我们仍然需要将基本 title
字段包含到查询中:
POST /my_index/my_type/_search
{
"query": {
"bool": {
"must": {
"match": {
"title": "the hungry alligator ate sue"
}
},
"should": {
"match": {
"title.shingles": "the hungry alligator ate sue"
}
}
}
}
}
仍然匹配到了所有的 3 个文档, 但是文档 2 现在排到了第一名因为它匹配了 shingled 词项 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 里会经常碰到的话题:不需要任何前期进行过多的设置,就能够在搜索的时候有很好的效果。 一旦更清晰的理解了自己的需求,就能在索引时通过正确的为你的数据建模获得更好结果和性能。
elasticsearch 深入 —— 近似匹配的更多相关文章
- (转)ElasticSearch教程——汇总篇
https://blog.csdn.net/gwd1154978352/article/details/82781731 环境搭建篇 ElasticSearch教程——安装 ElasticSearch ...
- ElasticSearch 2 (24) - 语言处理系列之停用词:性能与精度
ElasticSearch 2 (24) - 语言处理系列之停用词:性能与精度 摘要 在信息检索早期,磁盘和内存相较我们今天的使用只是很小的一部分.将索引空间保持在一个较小的水平是至关重要的,节省每个 ...
- ElasticSearch 2 (16) - 深入搜索系列之近似度匹配
ElasticSearch 2 (16) - 深入搜索系列之近似度匹配 摘要 标准的全文搜索使用TF/IDF处理文档.文档里的每个字段或一袋子词.match 查询可以告诉我们哪个袋子里面包含我们搜索的 ...
- ElasticSearch深入搜索
一. 结构化搜索 结构化搜索(Structured search) 是指有关探询那些具有内在结构数据的过程.比如日期.时间和数字都是结构化的:它们有精确的格式,我们可以对这些格式进行逻辑操作.比较常见 ...
- Elasticsearch 知识点整理 一
极力推荐: 官网地址: https://www.elastic.co/guide/en/elasticsearch/reference/6.0 肺腑之言,学ES先学原生的语法,SpringData封装 ...
- 白日梦的Elasticsearch实战笔记,ES账号免费借用、32个查询案例、15个聚合案例、7个查询优化技巧。
目录 一.导读 二.福利:账号借用 三._search api 搜索api 3.1.什么是query string search? 3.2.什么是query dsl? 3.3.干货!32个查询案例! ...
- 白日梦的Elasticsearch实战笔记,32个查询案例、15个聚合案例、7个查询优化技巧。
目录 一.导读 三._search api 搜索api 3.1.什么是query string search? 3.2.什么是query dsl? 3.3.干货!32个查询案例! 四.聚合分析 4.1 ...
- Elasticsearch之java的基本操作一
摘要 接触ElasticSearch已经有一段了.在这期间,遇到很多问题,但在最后自己的不断探索下解决了这些问题.看到网上或多或少的都有一些介绍ElasticSearch相关知识的文档,但个人觉得 ...
- Elasticsearch 5.0 中term 查询和match 查询的认识
Elasticsearch 5.0 关于term query和match query的认识 一.基本情况 前言:term query和match query牵扯的东西比较多,例如分词器.mapping ...
随机推荐
- bzoj3123 [Sdoi2013]森林 树上主席树+启发式合并
题目传送门 https://lydsy.com/JudgeOnline/problem.php?id=3123 题解 如果是静态的查询操作,那么就是直接树上主席树的板子. 但是我们现在有了一个连接两棵 ...
- hdu 6035:Colorful Tree (2017 多校第一场 1003) 【树形dp】
题目链接 单独考虑每一种颜色,答案就是对于每种颜色至少经过一次这种的路径条数之和.反过来思考只需要求有多少条路径没有经过这种颜色即可. 具体实现过程比较复杂,很神奇的一个树形dp,下面给出一个含较详细 ...
- Jenkins修改升级配置
更换升级配置如下: http://mirror.esuni.jp/jenkins/updates/update-center.json
- Centos7网卡绑定的两种方法
https://blog.51cto.com/youdong/1963416 选择模式4,并且layer选择3+4,交换机要配置LAG 一.传统的bond方式 (1)bond几种主要模式介绍 ü mo ...
- redis学习 --Sorted Set
一.概述: Sorted Set(有序集合)和Set类型极为相似,它们都是字符串的集合,都不允许重复的成员出现在一个Set中.它们之间的主要差别是Sorted Set中的每一个成员都会有一个分数(sc ...
- Spring JDBCTemplate 简单使用
Spring JDBCTemplate applicationContext.xml配置 <?xml version="1.0" encoding="UTF-8&q ...
- (转)Window 中杀死指定端口 cmd 命令行 taskkill
Windows平台 两步方法 : 1 查询端口占用,2 强行杀死进程 netstat -aon|findstr "8080" taskkill /pid 4136-t -f ...
- 理解CSS中position的各个值
static position的默认值,没有定位,元素在normal flow中: fixed 相对于浏览器左上角定位: relative 相对定位元素,其位置根据其在normal flow中的位置来 ...
- java反射机制-简单实例
public class Car { private String brand; private String color; private int maxSpeed; public Car() { ...
- HTML中margin与padding的区别!(转)
我们以DIV为一个盒子为例,既然和现实生活中的盒子一样,那我们想一下,生活中的盒子内部是不是空的好用来存放东西,而里面存放东西的区域我们给他起个名字叫“content(内容)”,而盒子的纸壁给他起个名 ...