Elasticsearch 搜索技术深入
序号 | 内容 | 链接地址 |
---|---|---|
1 | SpringBoot整合Elasticsearch7.6.1 | https://blog.csdn.net/miaomiao19971215/article/details/105106783 |
2 | Elasticsearch Filter执行原理 | https://blog.csdn.net/miaomiao19971215/article/details/105487446 |
3 | Elasticsearch 倒排索引与重建索引 | https://blog.csdn.net/miaomiao19971215/article/details/105487532 |
4 | Elasticsearch Document写入原理 | https://blog.csdn.net/miaomiao19971215/article/details/105487574 |
5 | Elasticsearch 相关度评分算法 | https://blog.csdn.net/miaomiao19971215/article/details/105487656 |
6 | Elasticsearch Doc values | https://blog.csdn.net/miaomiao19971215/article/details/105487676 |
7 | Elasticsearch 搜索技术深入 | https://blog.csdn.net/miaomiao19971215/article/details/105487711 |
8 | Elasticsearch 聚合搜索技术深入 | https://blog.csdn.net/miaomiao19971215/article/details/105487885 |
9 | Elasticsearch 内存使用 | https://blog.csdn.net/miaomiao19971215/article/details/105605379 |
10 | Elasticsearch ES-Document数据建模详解 | https://blog.csdn.net/miaomiao19971215/article/details/105720737 |
一. 手工控制搜索结果的精准度
1.1 搜索包含查询条件分词后所有词条的数据
搜索包含first或content词条的document:
GET /test_sort/_search
{
"query": {
"content": {
"remark": "first content"
}
}
}
升级: 搜索包含first和content词条的document:
GET /test_sort/_search
{
"query": {
"match": {
"content": {
"query": "first content",
"operator": "and"
}
}
}
}
上述搜索方式中,如果把operator设置成or,则查询的接口与不使用operator没有区别。and表明查询的document中必须既包含first,又包含content。(注意: 并不强制要求first和content必须紧挨着出现)
1.2 搜索包含查询条件分词后一定比例数量的词条的数据
在搜索document时,希望目标数据包含一定比例的查询条件分词后的词条个数,可以使用minimum_should_match,填入百分比或固定数字来实现。百分比代表目标document需要包含搜索条件中词条个数的百分比,如果无法整除,则向下匹配。固定数字代表至少需要包含多少个词条。
GET /test_sort/_search
{
"query": {
"match": {
"content": {
"query": "first content",
"minimum_should_match": "50%"
}
}
}
}
minimum_should_match可以和bool should搭配使用,should本身代表着多个条件只需要满足一个即可,如果使用了minimum_should_match=2,则代表至少满足2个词条才能被视为目标数据。
GET /test_sort/_search
{
"query": {
"bool": {
"should": [
{
"match": {
"content": "first"
}
},
{
"match": {
"content": "C++"
}
},
{
"match": {
"content": "second"
}
}
],
"minimum_should_match": 2
}
}
}
1.3 match的底层转换
官方建议搜索时,尽量使用转换后的形式,执行效率更高(不需要ES自行转换了)。 在条件较少时转换请求的数据结构不会带来太明显的性能提升,但如果条件非常多,那么节约的时间就很可观了。
转换前1:
GET /test_sort/_search
{
"query": {
"content": {
"content": "first content"
}
}
}
转换后1:
GET /test_sort/_search
{
"query": {
"bool": {
"should": [
{
"term": {
"content": {
"value": "first"
}
}
},
{
"term": {
"content": {
"value": "content"
}
}
}
]
}
}
}
转换前2:
GET /test_sort/_search
{
"query": {
"match": {
"content": {
"query": "first content",
"operator": "and"
}
}
}
}
转换后2:
GET /test_sort/_search
{
"query": {
"bool": {
"must": [
{
"term": {
"content": {
"value": "first"
}
}
},
{
"term": {
"content": {
"value": "content"
}
}
}
]
}
}
}
二. boost权重控制
人为的控制搜索条件在进行相关度分数计算时的权重大小。
比如搜索document中content字段内包含first的数据,如果content包含java或C++,则优先显示包含它们的数据,并且java的权重是C++的三倍。
boost权重控制一般用于搜索时搭配相关度排序使用。比如: 电商平台对商品进行综合排序。将一个商品的销量、广告费、评价值、库存、单价等信息进行比较并综合排序。排序时,库存的权重最低,广告费的权重最高。
GET /test_sort/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"content": "first"
}
}
],
"should": [
{
"match": {
"content": {
"query": "java",
"boost": 3
}
}
},
{
"match": {
"content": {
"query": "C++",
"boost": 1
}
}
}
]
}
}
}
搜索结果为:
"hits" : [
{
"_index" : "test_sort",
"_type" : "sort_type",
"_id" : "4",
"_score" : 3.2636642,
"_source" : {
"content" : "first content java"
}
},
{
"_index" : "test_sort",
"_type" : "sort_type",
"_id" : "5",
"_score" : 1.8323578,
"_source" : {
"content" : "first content C++"
}
},
{
"_index" : "test_sort",
"_type" : "sort_type",
"_id" : "1",
"_score" : 0.48120394,
"_source" : {
"content" : "first content",
"order" : 1
}
}
]
2.1 多shard环境中相关度分数不准确问题
在 ES的搜索结果中,相关度分数不是一定准确的。在多个shard环境中,使用相同的搜索条件得到的相关度分数可能会有误差(如果index只有一个主分片,则不会出现误差)。只要数据量达到一定的程度,那么相关度分数的误差就会逐渐趋近于0。
请看以下场景:
现在有两个主分片: shard0和shard1,它们分别持有10000条document数据,其中,shard0含有100个包含java的document,shard1含有10个包含java的document。ES在shard本地计算相关度分数(不是把数据统一上报到coordinate协调节点后,才进行相关度分数的计算),当以java作为条件进行搜索时,虽然经过TF计算出的值相同,但使用IDF算法计算相关度分数时,shard0内的得分比shard1低。
如果数据量足够大,使得含有java的document数据均匀的分布在所有分片上时,那么无论在哪个shard上计算相关度分数,得到的结果就都是相同的了。
在开发测试阶段,我们可以通过在设置settings时,将number_of_shards的值设置为1来解决问题,也可以通过增加dfs_query_then_fetch请求参数来解决问题:
GET /test_sort/_search?search_type=dfs_query_then_fetch
{
"query": {
"bool": {
"must": [
{
"match": {
"content": "first"
}
}
]
}
}
}
search_type=dfs_query_then_fetch不建议在生产环境中使用,因为它会使相关度分数的计算时机从shard本地搜索目标数据,挪到每个shard反馈到协调节点后,由协调节点统一做相关度分数的计算。由于所有的目标结果都会在协调节点汇总,因此协调节点的数据量非常大,而为了计算相关度分数又不得不把数据全部读取到内存中,因此这种方式不仅对内存的压力非常大,还会增加额外的IO开销。这就是ES官网明明知道多shard时相关度分数不准确的问题,却不得不在shard本地计算相关度分数的原因。
三. 基于dis_max实现多字段搜索
3.1 dis_max的基本用法
- best fields策略: 搜索document中的某一个field字段,尽可能多的匹配搜索搜索条件。
best field的实现手段: dis_max 对每个搜索条件进行评估,以单独作为条件进行搜索时,得到的最高相关度分数进行排序。
例子 单独使用条件1和条件2进行搜索时,各document计算出的相关度分数如下:
document1 | document2 | |
---|---|---|
条件1 | 0.5 | 1.2 |
条件2 | 1.1 | 0.3 |
最大值 | 1.1 | 1.2 |
- most fields策略: 尽可能多的让document中多个field参与到计算相关度分数中。显然,这种策略下搜索的精准度没有best field高。比如某个document中,虽然有一个字段包含了非常多的词条(由搜索条件提供),但其它字段没有包含关键字。而其它document与之对应的字段中虽然没有包含这么多关键字,但胜在包含词条的字段数目多,因此在搜索结果返回时,会被排列到上一个document之前。百度就搜索利用了most fields策略,因为往往匹配维度多的document比只匹配了寥寥几个维度(哪怕这些维度匹配的非常精准)的document,对用户更有用。
让我们总结一下
most_fields的优点: 搜索出的结果更均匀,因为命中字段多的document排在了前面,命中少的document排在了后面。
most_fields的缺点: 搜索出的结果没有best fields那么精确。
document1 | document2 | |
---|---|---|
条件1 | 0.5 | 1.2 |
条件2 | 1.1 | 0.3 |
总和 | 1.6 | 1.5 |
beat fields使用案例: 现在想把条件1和条件2结合在一起使用,共同进行搜索,但是希望document1得到的相关度分数为1.1,document2得到的相关度分数为1.2,各个条件互不影响,最终得到的排序结果为: document2 -> document1
做法是在搜索时使用dis_max。 可以注意到,使用dis_max时,不再配合使用should或must,而是使用queries。
GET test_dis_max/_search
{
"query": {
"dis_max": {
"queries": [
{
"条件1"
},
{
"条件2"
}
]
}
}
3.2 使用tie_breaker参数优化dis_max的搜索效果
dis_max是只取多个query条件中相关度分数最高的用于排序,忽略其它的query条件对应的分数。在某些情况下,我们还需将其它query条件对应的分数加入到最后相关度分数的计算上,这个时候就可以使用tie_breaker参数来优化dis_max的功能。
tie_breaker参数的含义是:将其它所有搜索条件对应的相关度分数乘以一个比例,再参与到结果的排序中。默认参数值为0,因此不定义此参数,会导致其它搜索条件不参与到结果的排序中。
GET test_dis_max/_search
{
"query": {
"dis_max": {
"queries": [
{
"条件1"
},
{
"条件2"
}
],
"tie_breaker" : 3
}
}
此时,相关度分数的计算如下:
document1 | document2 | |
---|---|---|
条件1 | 0.5 | 1.2 |
条件2 | 1.1 | 0.3 |
最大值 | 1.1 | 1.2 |
最终结果 | 1.1 + 0.5*3 = 2.6 | 1.2 + 0.3*3 = 2.1 |
最终得到的排序结果为: document1 -> document2
3.3 使用multi_match简化dis_max+tie_breaker
可以通过multi_match整合boost, query, minimum_should_match, tie_breaker以及fields来实现搜索。具体方式如下:
GET java/_search
{
"query": {
"multi_match":{
"query": "rod java developer",
"fields": ["name", "remark^2"],
"type": "best_fields",
"tie_breaker": 3,
"minimum_should_match": "50%"
}
}
}
其中^n代表权重,比如remark ^2 等价于 。注意,给字段起名时,尽量避免使用 ^,ES在解析到这个符号时,会把它当作转义字符。
remark: {
"query": "rot java developer",
"boost": 2
}
四. 使用multi_match+most fields实现multi_field搜索
3.1中已经谈过most_fields策略和best_fields策略的区别,它们没有优劣之分,只是使用的场景不同。
GET java/_search
{
"query": {
"multi_match":{
"query": "rod java developer",
"fields": ["name", "remark^2"],
"type": "most_fields",
"minimum_should_match": "50%"
}
}
}
五. cross fields 跨字段搜索
cross fields会将搜索条件在每一个fields中进行搜索,一般搭配operator使用,具体请看下面的例子:
GET java/_search
{
"query": {
"multi_match":{
"query": "rod java",
"fields": ["name", "remark"],
"type": "cross_fields",
"operator": "and"
}
}
}
当"operator"等于"and"时,ES对查询条件"rod java"进行分词后,"rod"和"java"必须都分别在"name"或"remark"字段中匹配上。
当"operator"等于"or"时,ES对查询条件"rod java"进行分词后,"rod"或"java"只要任意一个词条在"name"或"remark"中匹配,那么匹配的document就是搜索结果之一。
六. copy_to组合field搜索
如果想要实现在多个指定的字段中搜索目标数据,以下三种做法都能满足需求:
- _all ES自动创建_all字段,包含index中所有字段,但由于_all字段包含了许多不需要作为本次搜索目的的字段,因此作为搜索字段过于沉重了。
- multi_match, type=most_field,operator=or 可以实现需求,但排序可能会受到影响,包含搜索条件词条较多field的document可能会被排到前面,排序后体现的搜索精准度会下降。
- copy_to 在索引的设计阶段,通过_mapping copy_to的方式,将多个指定字段同时与某个逻辑字段进行关联,类似java中的推导类型和Mysql中的视图,这个类型在es持久化时可能并不存在,但它可以在搜索时,充当搜索条件,相当于把若干个字段中的值拼在了一起。
copy_to的定义方式如下:
PUT index_name/_mapping
{
"properties": {
"author_first_name": {
"type": "text",
"analyzer": "standard",
"copy_to": "whole_name"
},
"author_last_name": {
"type": "text",
"analyzer": "standard",
"copy_to": "whole_name"
},
"address": {
"type": "text",
"analyzer": "ik_max_word"
}
}
}
来看看index_name的数据结构: GET /index_name/_mapping
{
"index_name" : {
"mappings" : {
"properties" : {
"address" : {
"type" : "text",
"analyzer" : "ik_max_word"
},
"author_first_name" : {
"type" : "text",
"copy_to" : [
"whole_name"
],
"analyzer" : "standard"
},
"author_last_name" : {
"type" : "text",
"copy_to" : [
"whole_name"
],
"analyzer" : "standard"
}
}
}
}
}
author_first_name和author_last_name都与whole_name进行了关联。
在正常的搜索时,whole_name虚拟字段 不会被搜索到:
"hits" : [
{
"_index" : "index_name",
"_type" : "_doc",
"_id" : "lEqYO3EBDfqTlht4Npnl",
"_score" : 1.0,
"_source" : {
"author_first_name" : "Peter",
"author_last_name" : "Smith"
}
},
{
"_index" : "index_name",
"_type" : "_doc",
"_id" : "2",
"_score" : 1.0,
"_source" : {
"author_first_name" : "Smith",
"author_last_name" : "Williams"
}
}
]
搜索时,只需要对whole_name中做条件匹配,ES会对whole_name中拼成的值做倒排索引。ES会自动维护whole_name组合字段,我们作为使用者不需要对它做任何的维护工作。
七. 近似搜索(proximity search)
GET _search
{
"query" : {
"match" : {
"field" : "hello world"
}
}
}
以上写法使用的搜索类型是精确匹配,目标document对应的fields在分词后,必须包含完整的hello或者world词条。倘若搜索hel,显然搜不到任何的数据。
如果我们在搜索时有特殊的要求,比如:
- 使用hello world进行搜索,但目标document中必须完整的包含"hello world"整个词条,不能分割。
- document的fields中包含"hello"和"world"词条,且两个单词离的越近,则相关度分数越高。(比如Java hello world比hello Java world的相关度分数高)
- 使用hel进行搜索,希望能够搜索到包含"hel"的数据。
显然,使用match搜索无法实现这些要求,因此近似匹配就派上用场了。
7.1 match phrase 短语搜索
使用match phrase时,搜索条件不分词(宏观上),ES会在倒排索引中查询是否有document的fields对应的词条与搜索条件完全相同。
比如:
GET index_name/_search
{
"query": {
"match_phrase": {
"tags": "hello world"
}
}
}
在目标document中,必须完整的匹配"hello world"词条。
query match被称为"full-text search",match phrase与它的区别在于,前者会对搜索条件进行分词,而后者不会(宏观上)。
7.1.1 match phrase原理 – term position
ES实现match phrase时,其实也进行了分词,可能这会让人产生疑惑:刚刚不是说了match phrase不分词,而full-text search分词吗?
这就涉及到了倒排索引的建立过程了。倒排索引在创建时,除了使用analyzer分词器将fields分割成若干词条以外,还会记录以下属性(包括但不限于):
- 词条在哪些document中出现过,也就是记录document的id。
- 词条在document中出现的位置(下标 position)
GET _analyze
{
"text": "hello world java",
"analyzer": "standard"
}
{
"tokens" : [
{
"token" : "hello",
"start_offset" : 0,
"end_offset" : 5,
"type" : "<ALPHANUM>",
"position" : 0
},
{
"token" : "world",
"start_offset" : 6,
"end_offset" : 11,
"type" : "<ALPHANUM>",
"position" : 1
},
{
"token" : "java",
"start_offset" : 12,
"end_offset" : 16,
"type" : "<ALPHANUM>",
"position" : 2
}
]
}
因此,在使用match phrase进行搜索时,首先会对搜索条件进行分词(比如拆分成 “hello"和"world”),接着找出既包含"hello"又包含"world"的document,最后比较这两个词条在目标document中的position,若position的值是连续的,则说明"hello" "world"在目标document中连续出现,相当于目标document的fields中包含了一个完整的"hello world"词条,否则代表匹配失败。
7.1.2 match phrase搜索参数 slop
使用match phrase短语搜索时,如果搜索的参数是hello world,但ES中存储的是hello java C world, 那么将查不到任何数据。可能有人会说使用full text search可以解决问题,但如果用户需要在搜索时,搜索的词条在目标document中越接近,则相关度分数越高,排名越靠前,那么full text search便无计可施了。
slop用于计算在进行match phrase短语搜索时,单词移动到目标位置所花费的最少步数。比如 "hello java C world"被analyzer分词后,会得到以下倒排索引信息:
hello | java | C | world | |
---|---|---|---|---|
position | 0 | 1 | 2 | 3 |
但我们搜索的目标是:
hello | world | |
---|---|---|
position | 0 | 1 |
从目标的位置开始,ES至少要把world的下标向右方移动2位才能与当前document中存放的数据重合。 因此,slop=2便是"hello java C world"对应document的移动临界值,如果在搜索中slop<2,那么该document将无法被搜索出来。
举个例子,以下是ES中存储的数据,现在准备搜索"refuse exist",不难发现,_id=1移动到目标位置的步长等于4,_id=2等于1,_id=3等于2。
hits" : [
{
"_index" : "match_phrase_index",
"_type" : "_doc",
"_id" : "1",
"_score" : 1.0,
"_source" : {
"content" : "I refuse to prove that I exist"
}
},
{
"_index" : "match_phrase_index",
"_type" : "_doc",
"_id" : "2",
"_score" : 1.0,
"_source" : {
"content" : "I refuse prove exist"
}
},
{
"_index" : "match_phrase_index",
"_type" : "_doc",
"_id" : "3",
"_score" : 1.0,
"_source" : {
"content" : "I refuse prove cat exist"
}
}
]
因此,如果使用slop=2进行短语搜索,搜索结构如下:
GET match_phrase_index/_search
{
"query": {
"match_phrase": {
"content": {
"query": "refuse exist",
"slop": 2
}
}
}
}
由于(_id=2 -> 1) < (_id=3->2),_id=2花费的步数短,相关度分数高,排名自然更靠前,因此最终得到如下结果:
"hits" : [
{
"_index" : "match_phrase_index",
"_type" : "_doc",
"_id" : "2",
"_score" : 0.19916546,
"_source" : {
"content" : "I refuse prove exist"
}
},
{
"_index" : "match_phrase_index",
"_type" : "_doc",
"_id" : "3",
"_score" : 0.1325897,
"_source" : {
"content" : "I refuse prove cat exist"
}
}
]
如果搜索条件是"exist refuse",ES在搜索时会按照如下方式移动词条:
0 | 1 | 2 | 3 | |
---|---|---|---|---|
尚未移动 | I | refuse | prove | exist |
第一次移动 | I | refuse | exist/prove | |
第二次移动 | I | refuse/exist | prove | |
第三次移动 | I | exist | refuse/prove |
显然,移动三次便能得到搜索条件的顺序。
一般对英文text做slop,如果对中文做slop就比较麻烦了,因为中文分词后不是一空格一个词,如果准确的移动中文词条,需要经过多次测试。
7.2 match和proximity search搭配使用 (召回率和精准度)
首先抛出几个概念
- 召回率: 召回率就是搜索结果的比率。比如索引A中有1000个document,如果返回的结果中包含了900个document,那么召回率就是90%。为了提升召回率的效果,ES会尽可能的扩大搜索范围,返回更多的document。
- 精准度: 准确的搜索结果,ES会尽可能的不返回无关的文档。目的是在结果的第一页中为用户呈现最为相关的文档。
如果在搜索中只是用match phrase,会导致召回率底下,错过许多有用的document,因为搜索结果中必须包含短语(就算是proximity search近似搜索也是如此)。
如果在搜索中只使用match,会导致精准度底下,只要包含任意一个词条的document都能被返回。
那么如果我们向兼顾召回率和精准度,就需要把match和proximity search搭配在一起使用了。
GET /test_index/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"f": "java spark"
}
}
],
"should": [
{
"match_phrase": {
"f": {
"query": "java spark",
"slop" : 50
}
}
}
]
}
}
}
以上搜索中,既能返回包含java或spark的document,又能把java和spark更临近的数据排列在结果的最前列,所以兼顾了召回率和精准度。
7.3 proximity search性能优化
full text search(match)和match phrase(包含proximity search)相比较时,match的效率要高得多,毕竟对document进行分词并创建倒排索引后,match只需要匹配document中是否含有指定词条,而match phrase还需要比较position是否连续,proximity search就更麻烦了,它还要尝试着移动词条的位置。
一般来说,match的性能比match phrase高出10倍左右,比proximity search高出20倍左右。在中小型项目无需担心性能问题,因为ES的搜索性能是毫秒级别的,但如果是大型项目,数据量达到PB级,那就需要考虑优化了。
优化的思路是尽量的减少proximity search搜索的document数量。我们可以先使用match尽可能的搜索出需要的数据,再使用proximity search来提高数据的精度,document的相关度分数,从而影响数据的排序,整个过程被称作rescore(重计分)。此外,重计分之前,我们往往会使用分页,进一步的缩小需要重计分的数据范围。因为用户一般只关心前几页,甚至前十几页的数据,无脑的对所有的相关数据进行重计分是没有必要的。
语法如下:
GET _search
{
"query": {
"match": {
"f": "java spark"
}
},
"rescore": {
"window_size": 50,
"query": {
"rescore_query": {
"match_phrase": {
"custom_field_name": {
"query": "java spark",
"slop": 10
}
}
}
}
},
"from": 0,
"size": 10
}
其中,window_size:50指的是在当前的搜索结果之中,对前50条数据进行重计分。from指的是应该跳过的结果数量(默认0),size指的是应该返回的结果数量。custom_field_name指的是字段的名称。
八. 前缀搜索 prefix search
使用前缀匹配实现搜索时,通常针对keyword字段,也就是不分词的字段。
为什么不能配合match使用前缀搜索?
答: 因为没有意义。使用match时,会导致ES在index的倒排索引中搜索数据,既然是倒排索引,那么便伴随着分词,试问在一个document的fields中的所有分词内去匹配前缀还有什么意义呢?
语法:
GET /test_index/_search
{
"query": {
"prefix": {
"f.keyword": {
"value": "Java and"
}
}
}
}
其中,f是字段名称,f.keyword指的是ES为f这个字段默认创建的子字段,value中填写需要搜索的前缀。keyword类型的字段大小写敏感。
前缀搜索的特点:
- 搜索效率极低。
- 不会计算相关度分数
- 前缀越短,效率越低 (因此如果一定要用前缀搜索,就尽可能的使用长前缀)
九. 通配符搜索 wildcard
ES中的通配符常用的有两种: ? 和 *。?匹配1个字符,*匹配0~n个字符。通配符既可以在keyword中使用,也可以在涉及倒排索引、分词的搜索中使用。性能很低,需要扫描完整的索引。
举例,搜索第二个字符起以"jav j"开头的数据。
GET /test_index/_search
{
"query": {
"wildcard": {
"f": {
"value": "?jav j*"
}
}
}
}
十. 正则搜索
ES中常用的正则符号:
- [ ] 范围,如 [0-9] 0~9范围内的数字
- . 一个字符
- + 前面的表达式可以出现多次。
举例,搜索包含大小写字母出现多次的数据:
GET /test_index/_search
{
"query": {
"regexp": {
"f.keyword": "[A-z].+"
}
}
}
[A-z] 代表搜索条件的范围是是大小写字母,[A-z].匹配一个字母和一个任意字符,[A-z].+匹配一个字母和多个任意字符。[A-z]+匹配多个字母,.+匹配1至多个任意字符。
使用正则表达式搜索的性能很低。
十一. 搜索推荐 (重点讲解max_expansions)
又叫做"自动补全"。比如搜索Spring k时,希望搜索框中能够提示Spring kafka和Spring kotlin。
语法:
GET /test_index/_search
{
"query": {
"match_phrase_prefix": {
"f": {
"query": "Spring k",
"slop": 5,
"max_expansions": 5
}
}
}
}
ES在进行搜索推荐时,使用的关键字是match_phrase_prefix。首先,ES会去搜索已经分词的词条中是否包含搜索条件,接着在slop限制的步长次数内,尝试着移动搜索条件中的最后一个单词,使得搜索条件与数据的单词重合。
max_expansions用于限制被收集的最后一个词条的变形单词的个数,这个概念不太容易理解,因此给出一个例子:
有数据如下:
"hits" : [
{
"_index" : "test_match_phrase_prefdix",
"_type" : "_doc",
"_id" : "1",
"_score" : 1.0,
"_source" : {
"content" : "Elasticsearch Jav and Spring boot Testing and Maven and cat Spark are friends"
}
},
{
"_index" : "test_match_phrase_prefdix",
"_type" : "_doc",
"_id" : "2",
"_score" : 1.0,
"_source" : {
"content" : "Tests Elasticsearch cat and Spring boot and Maven and Spark are friends"
}
},
{
"_index" : "test_match_phrase_prefdix",
"_type" : "_doc",
"_id" : "3",
"_score" : 1.0,
"_source" : {
"content" : "Elasticsearch and Spring cat and Tester Maven and Spark are friends"
}
}
]
要求: 以"cat test"为条件进行搜索,让ES返回搜索推荐。
因此,搜索的语句如下:
GET test_match_phrase_prefdix/_search
{
"query": {
"match_phrase_prefix": {
"content": {
"query": "cat test",
"slop": 10,
"max_expansions": 2
}
}
}
}
现在开始分析ES的搜索过程:
- ES将搜索条件进行分词,得到"cat"和"test"。
- ES在倒排索引中找出包含"cat"的文档。
- 在第2步得到的文档列表中找出test词条的变形,放入一个容器中。本例中ES会把"Testing",“Tests”,"Tester "放进容器,并按照字典顺序进行排序。
- 在document中首先定位cat的位置(说白了就是position下标),接着以词条为最小单位,移动"test"对应的词条(比如_id=1的文档中,移动Testing),若在移动步长的限制值(本例中是10步)范围内,文档数据与搜索条件实现了重合(也就是移动成了cat Testing),则当前文档就是命中的、符合要求的数据。
- 搜索语句中max_expansions=2,意味着ES只会搜索容器中前两个test的变形词条(也就是"Testing"和"Tests"),而放弃"Tester",因此,即便_doc=3中,Tester只需要移动3个步长就能实现cat Tester,也不会成为符合要求的数据。
最终搜索出的结果中,不会包含_doc=3:
"hits" : [
{
"_index" : "test_match_phrase_prefdix",
"_type" : "_doc",
"_id" : "2",
"_score" : 0.7947273,
"_source" : {
"content" : "Tests Elasticsearch cat and Spring boot and Maven and Spark are friends"
}
},
{
"_index" : "test_match_phrase_prefdix",
"_type" : "_doc",
"_id" : "1",
"_score" : 0.5328808,
"_source" : {
"content" : "Elasticsearch Jav and Spring boot Testing and Maven and cat Spark are friends"
}
}
]
设计max_expansion的目的在于避免给出过多的搜索推荐。比如用户输入"Spring k",index中包含以"k"为前缀单词的文档可能数以万计,全部返回给用户显然不显示,使用了max_expansion后,就能过滤掉大部分的数据。max_expansion与from size在过滤数据时的侧重点不同,前者侧重于减少搜索条件中最后一个单词变形的可能性,而后者只关注数据的下标,对满足条件的数据进行简单的过滤。
十二. ngram 分词机制
ngram是指在指定的长度下,对已经分词的词条进行再次拆分。
使用方式如下:
PUT /test_ngram
{
"settings": {
"index.max_ngram_diff":30,
"analysis": {
"filter": {
"custom_ngram_filter_mmr": {
"type": "ngram",
"min_gram": 1,
"max_gram": 30
}
},
"analyzer": {
"custom_analyzer_mmr": {
"type": "custom",
"tokenizer": "standard",
"filter": [
"lowercase",
"custom_ngram_filter_mmr"
]
}
}
}
},
"mappings": {
"properties": {
"ff": {
"type": "text",
"analyzer": "custom_analyzer_mmr",
"fields": {
"keyword": {
"type": "keyword"
}
}
}
}
}
}
分析hello这个单词
GET test_ngram/_analyze
{
"text": "hello",
"analyzer": "custom_analyzer_mmr"
}
可以分出如下单词:
h,he,hel,hello
e,el,ell,ello,
l,ll,llo,
l,lo,o
edge ngram在ngram的基础上增加了一条限制,拆分单词时,只会从首字母向后拆分,比如同样对hello进行拆分,edge ngram得到的数据如下: h,he,hel,hello
使用edge ngram对分词后的单词进行再次拆分的目的在于使用前缀搜索或搜索推荐时,效率更高,因为再次拆分的单词与doc_id的对应信息会存储在倒排索引中,ES可以直接在倒排索引中寻找分词后的搜索条件,不再需要遍历每一个词条并逐一匹配,极大的节省了时间。相对的,由于多出了拆分单词的动作,因此新增数据花费的时间会变长,并且拆分后的单词随倒排索引一起存储在硬盘上,并加载至内存中,因此会增加硬盘和内存的负担。
十三. fuzzy模糊搜索(解决拼写错误问题)
搜索条件有时会写错,比如不小心将hello world写成了 hello wold。fuzzy技术用于为错误的拼写进行纠正。其中,fuzziness代表value的值可以通过多少次操作变成目标词条。在英文数据中使用fuzzy搜索很有效,但在中文数据中作用很小。
操作的方式有如下三种:
- 替换字符
- 插入一个字符
- 删除一个字符
举个例子,ES中存储的数据是hello,但搜索时不小心写成了"hleo":
GET /test_ngram/_search
{
"query": {
"fuzzy": {
"ff": {
"value": "hleo",
"fuzziness": 2
}
}
}
}
为了把"hleo"修改成"hello",至少需要经历以下2步操作:
操作步骤 | 0 | 1 | 2 | 3 | 4 |
---|---|---|---|---|---|
初始 | h | l | e | o | |
第一步 (在1号和2号位之间插入字符l) | h | l | l | e | o |
第二步 (将1号位和3号位的字符进行交换) | h | e | l | l | o |
如果在搜索hleo时,将fuzziness设置成1,则查不出任何数据。
十四. suggest搜索建议
suggest search (completion suggest): 就建议搜索(又称搜索建议),也可以叫做自动完成auto-completion。类似百度中的搜索联想提示功能。
Elasticsearch实现suggest的时候,性能非常高,其构建的不是倒排索引,也不是正排索引,就是纯粹的用于进行前缀搜索的一种特殊的数据结构,而且会全部放在内存中,所以使用suggest search进行前缀搜索提示,性能时非常高的。
如果需要使用suggest,则必须在定义index mapping中指定字段开启suggest。具体方式如下:
PUT /baidu
{
"mappings": {
"properties" : {
"title" : {
"type": "text",
"analyzer": "standard",
"fields": {
"suggest" : {
"type" : "completion",
"analyzer": "standard"
}
}
},
"content": {
"type": "text",
"analyzer": "standard"
}
}
}
}
POST /baidu/_doc/1
{
"title": "大话西游电影",
"content": "大话西游的电影时隔20年即将在2017年4月重映"
}
POST /baidu/_doc/2
{
"title": "大话西游小说",
"content": "某知名网络小说作家已经完成了大话西游同名小说的出版"
}
POST /baidu/_doc/3
{
"title": "大话西游手游",
"content": "网易游戏近日出品了大话西游经典IP的手游,正在火爆内测中"
}
“type”:“completion”表示为"title"字段开启了suggest搜索建议功能。"title.suggest"是"title"的子字段。
使用搜索建议的语句如下:
GET /baidu/_search
{
"suggest": {
"suggest_function_name": {
"prefix": "大话西游",
"completion": {
"field": "title.suggest"
}
}
}
}
“suggest_function_name”是搜索建议函数的函数名,人为指定。"prefix"填写搜索内容的前缀。completion field用来指定需要实现搜索建议功能的字段名称。