- 1. 短语匹配
- 2. 邻近查询
- 3. 多值字段
- 4. 越近越好
- 5. 性能优化
思考下面这几个句子的不同:
- Sue ate the alligator.
- The alligator ate Sue.
- Sue never goes anywhere without her alligator-skin purse.
用 match 搜索 sue alligator 上面的三个文档都会得到匹配,但它却不能确定这两个词是否只来自于一种语境,甚至都不能确定是否来自于同一个段落。
理解分词之间的关系是一个复杂的难题,我们也无法通过换一种查询方式去解决。但我们至少可以通过出现在彼此附近或者仅仅是彼此相邻的分词来判断一些似乎相关的分词。
每个文档可能都比我们上面这个例子要长: Sue 和 alligator 这两个词可能会分散在其他的段落文字中,我们可能会希望得到尽可能包含这两个词的文档,但我们也同样需要这些文档与分词有很高的相关度。
这就是短语匹配或者近似匹配的所属领域。
1. 短语匹配1.1 短语匹配match_phrase
就像 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"
}
}
1.2 词项的位置
当一个字符串被分词后,这个分析器不但会返回一个词项列表,而且还会返回各词项在原始字符串中的 位置 或者顺序关系:
GET /_analyze?analyzer=standard Quick brown fox
返回信息如下:
{
"tokens": [
{
"token": "quick",
"start_offset": 0,
"end_offset": 5,
"type": "",
"position": 1 // position 代表各词项在原始字符串中的位置
},
{
"token": "brown",
"start_offset": 6,
"end_offset": 11,
"type": "",
"position": 2
},
{
"token": "fox",
"start_offset": 12,
"end_offset": 15,
"type": "",
"position": 3
}
]
}
位置信息可以被存储在倒排索引中,因此 match_phrase 查询这类对词语位置敏感的查询, 就可以利用位置信息去匹配包含所有查询词项,且各词项顺序也与我们搜索指定一致的文档,中间不夹杂其他词项。
1.3 什么是短语?
一个被认定为和短语 quick brown fox 匹配的文档,必须满足以下这些要求:
- quick 、 brown 和 fox 需要全部出现在域中。
- brown 的位置应该比 quick 的位置大 1 。
- fox 的位置应该比 quick 的位置大 2 。
如果以上任何一个选项不成立,则该文档不能认定为匹配。
hello world, java spark doc1 hi, spark java doc2 ----------------------------------------------------------- hello doc1(1) wolrd doc1(2) java doc1(3) doc2(3) spark doc1(4) doc2(2) ----------------------------------------------------------- java spark --> match phrase ----------------------------------------------------------- java --> doc1(3) doc2(3) spark --> doc1(4) doc2(2)
要找到每个term都在的一个共有的那些doc,就是要求一个doc,必须包含每个term,才能拿出来继续计算
doc1 --> java和spark–>doc1符合条件 :
java的position是2,spark的position是3,满足条件。
doc2 --> java和spark --> doc2不匹配 :
2. 邻近查询java的position是2,spark的position是1,不满足条件。
精确短语匹配或许是过于严格了。也许我们想要包含 “quick brown fox” 的文档也能够匹配 “quick fox,” , 尽管情形不完全相同。
我们能够通过使用 slop 参数将灵活度引入短语匹配中:
GET /my_index/my_type/_search
{
"query": {
"match_phrase": {
"title": {
"query": "quick fox",
"slop": 1
}
}
}
}
slop 参数告诉 match_phrase 查询词条相隔多远时仍然能将文档视为匹配 。 相隔多远的意思是为了让查询和文档匹配你需要移动词条多少次?
我们来看几个例子吧。
为了让查询 quick fox 能匹配一个包含 quick brown fox 的文档, 我们需要 slop 的值为 1: 即让fox
Pos 1 Pos 2 Pos 3 ----------------------------------------------- Doc: quick brown fox ----------------------------------------------- Query: quick fox Slop 1: quick ↳ fox
尽管在使用了 slop 短语匹配中所有的单词都需要出现, 但是这些单词也不必为了匹配而按相同的序列排列。 有了足够大的 slop 值, 单词就能按照任意顺序排列了。
为了使查询 fox quick 匹配我们的文档, 我们需要 slop 的值为 3: quick先移动到Pos 1,fox再移动到Pos 2,fox再移动到Pos 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
搜索关键词中的几个term,要经过几次移动才能与一个document匹配,这个移动的次数,就是slop
doc:hello world, java is very good, spark is also very good.
query string : java spark
match phrase搜不到结果,如果我们指定了slop,那么就允许java spark进行移动,来尝试与doc进行匹配
java is very good spark is java spark java --> spark java --> spark java --> spark
这里的slop=3,因为java spark这个短语,spark移动了3次,就可以跟一个doc匹配上了。
slop不仅是一个query string terms移动几次跟一个doc匹配上,而是最多可以移动几次去尝试跟一个doc匹配。
3. 多值字段对多值字段使用短语匹配时会发生奇怪的事。 想象一下你索引这个文档:
PUT /my_index/groups/1
{
"names": [ "John Abraham", "Lincoln Smith"]
}
然后运行一个对 Abraham Lincoln 的短语查询:
GET /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 的简单的解决方案, 它在字段映射中配置。
PUT /my_index/_mapping/groups
{
"properties": {
"names": {
"type": "text",
"position_increment_gap": 100
}
}
}
position_increment_gap 设置告诉 Elasticsearch 应该为数组中每个新元素增加当前词条 position 的指定值。 所以现在当我们再索引 names 数组时,会产生如下的结果:
- Position 1: john
- Position 2: abraham
- Position 103: lincoln
- Position 104: smith
现在我们的短语查询可能无法匹配该文档因为 abraham 和 lincoln 之间的距离为 100 。 为了匹配这个文档你必须添加值为 100 的 slop 。
4. 越近越好鉴于一个短语查询仅仅排除了不包含确切查询短语的文档, 而邻近查询(一个 slop 大于 0) 的短语查询将查询词条的邻近度考虑到最终相关度 _score 中。 通过设置一个像 50 或者 100 这样的高 slop 值, 你能够排除单词距离太远的文档, 但是也给予了那些单词临近的的文档更高的分数。
下列对 quick dog 的邻近查询匹配了同时包含 quick 和 dog 的文档, 但是也给了与 quick 和 dog 更加临近的文档更高的分数 :
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"
}
}
]
}
5. 性能优化
短语查询和邻近查询都比简单的 query 查询代价更高。 一个 match 查询仅仅是看词条是否存在于倒排索引中,而一个 match_phrase 查询是必须计算并比较多个可能重复词项的位置。
Lucene nightly benchmarks 表明一个简单的 term 查询比一个短语查询大约快 10 倍,比邻近查询(有 slop 的短语查询)大约快 20 倍。当然,这个代价指的是在搜索时而不是索引时。
通常,短语查询的额外成本并不像这些数字所暗示的那么吓人。事实上,性能上的差距只是证明一个简单的 term 查询有多快。标准全文数据的短语查询通常在几毫秒内完成,因此实际上都是完全可用,即使是在一个繁忙的集群上。
那么我们应该如何限制短语查询和邻近近查询的性能消耗呢?一种有用的方法是减少需要通过短语查询检查的文档总数。
结果集重新评分 :
一个查询可能会匹配成千上万的结果,但我们的用户很可能只对结果的前几页感兴趣。一个简单的 match 查询已经通过排序把包含所有含有搜索词条的文档放在结果列表的前面了。事实上,我们只想对这些 顶部文档 重新排序,来给同时匹配了短语查询的文档一个额外的相关度升级。
search API 通过重新评分明确支持该功能。重新评分阶段支持一个代价更高的评分算法,只是为了从每个分片中获得前 K 个结果, 然后会根据它们的最新评分重新排序。
该请求如下所示:
GET /my_index/my_type/_search
{
"query": {
"match": { // match 查询决定哪些文档将包含在最终结果集中,并通过 TF/IDF 排序。
"title": {
"query":"quick brown fox",
"minimum_should_match": "30%"
}
}
},
"rescore": {
"window_size": 50, // window_size 是每一分片进行重新评分的顶部文档数量。
"query": {
"rescore_query": {
"match_phrase": {
"title": {
"query": "quick brown fox",
"slop": 50
}
}
}
}
}
}



