1. 分词过滤
1.1. 分词器
对文本内容进行一些特定处理,根据处理后的结果再建立倒排索引,主要的处理过程一般如下:
character filter符号过滤,比如hello过滤成hello,I&you过滤成I and you。tokenizer分词,比如,将hello you and me切分成hello、you、and、me。token filter比如,dogs替换为dog,liked替换为like,Tom 替换为 tom,small 替换为 little等等。
内置了以下几种分词器 standard analyzer simple analyzer whitespace analyzer language analyzer。
curl GET ip:port/{index}/_analyze
{
"analyzer": "standard",
"text": "a dog is in the house"
}
# 采用standard分词器对text进行分词
分词器的各种替换行为,也叫做normalization,本质是为了提升命中率,官方叫做recall召回率。
对于document中的不同字段类型,采用不同的分词器进行处理,比如 date 类型压根就不会分词,检索时完全匹配,而对于 text 类型会进行分词处理。
1.1.1. 定制分词器
修改分词器的默认行为。比如,我们修改my_index索引的分词器,启用english停用词:
curl PUT ip:port/my_index
{
"settings": {
"analysis": {
"analyzer": {
"es_std": {
"type": "standard",
"stopwords": "_english_"
}
}
}
}
}
查看分词效果:
curl GET ip:port/my_index/_analyze
{
"analyzer": "es_std",
"text": "a dog is in the house"
}
1.1.2. IK分词器
安装:GitHub上下载预编译好的IK包,与ES版本一致。解压缩放置到 YOUR_ES_ROOT/plugins/ik/ 目录下,重启Elasticsearch。
IK和Elasticsearch主要的版本对照:
| IK version | ES version |
|---|---|
| master | 7.x -> master |
| 6.x | 6.x |
| 5.x | 5.x |
IK分词器有两种analyzer ik_max_word ik_smart 但是一般是选用 ik_max_word 。
ik_max_word 会将文本做最细粒度的拆分,比如会将“中华人民共和国国歌”拆分为“ 中华人民共和国 中华人民 中华 华人 人民共和国 人民 人 民 共和国 共和 和 国国 国歌 ”等等,会穷尽各种可能的组合。
ik_smart 只做最粗粒度的拆分,比如会将“中华人民共和国国歌”拆分为“ 中华人民共和国 国歌 ”。
IK分词器的分词效果,先将改变指定字段的mapping:
curl PUT ip:port/my_index
{
"mappings": {
"properties": {
"text": {
"type": "text",
"analyzer": "ik_max_word"
}
}
}
}
分词效果:
curl GET ip:port/my_index/_analyze
{
"text": "美专家称疫情在美国还未达到顶峰",
"analyzer": "ik_max_word"
}
配置文件
IK的配置文件存在于 YOUR_ES_ROOT/plugins/ik/config 目录下,目录下各个文件作用:
main.dicIK原生内置的中文词库,总共有27万多条,只要是这些单词,都会被分在一起;quantifier.dic放了一些单位相关的词;suffix.dic放了一些后缀;surname.dic中国的姓氏;stopword.dic英文停用词。
如果自定义词库,可以修改 IKAnalyzer.cfg.xml 的 ext_dict ,配置我们扩展的词库,重启 ES 生效。
热更新词库
目前有两种方案,业界一般采用第一种:
- 修改IK分词器源码,然后每隔一定时间,自动从MySQL中加载新的词库。
- 基于IK分词器原生支持的热更新方案:部署一个web服务器,提供一个http接口,通过 modified 和 tag 两个 http 响应头,来提供词语的热更新。
1.2. 分词检索
准备数据
curl POST ip:port/forum/_bulk
{ "index": { "_id": 1 }}
{ "articleID" : "XHDK-A-1293-#fJ3", "userID" : 1, "hidden": false, "postDate": "2017-01-01" }
{ "index": { "_id": 2 }}
{ "articleID" : "KDKE-B-9947-#kL5", "userID" : 1, "hidden": false, "postDate": "2017-01-02" }
{ "index": { "_id": 3 }}
{ "articleID" : "JODL-X-1937-#pV7", "userID" : 2, "hidden": false, "postDate": "2017-01-01" }
{ "index": { "_id": 4 }}
{ "articleID" : "QQPX-R-3956-#aD8", "userID" : 2, "hidden": true, "postDate": "2017-01-02" }
映射结构
# 请求
curl GET ip:port/forum/_mapping
# 响应
{
"forum" : {
"mappings" : {
"properties" : {
"articleID" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
},
"hidden" : {
"type" : "boolean"
},
"postDate" : {
"type" : "date"
},
"userID" : {
"type" : "long"
}
}
}
}
}
postDate默认就是date类型。这里关键说下articleID,它的类型是text,Elasticsearch默认会对 text 类型的字段进行分词,建立倒排索引;其次,还会生成一个keyword字段,这个keyword就是articleID的内容,不会分词,用于建立正排索引, 如果内容过长,只保留256个字符。
查看articleID.keyword字段分词,实际并未分词。
# 请求:分析下articleID.keyword的默认分词结果
curl GET ip:port/forum/_analyze
{
"field": "articleID.keyword",
"text":"XHDK-A-1293-#fJ3"
}
# 响应:可以看到"XHDK-A-1293-#fJ3"压根没被分词,原样返回了
{
"tokens" : [
{
"token" : "XHDK-A-1293-#fJ3",
"start_offset" : 0,
"end_offset" : 16,
"type" : "word",
"position" : 0
}
]
}
根据articleID搜索帖子。由于用了term filter语法,所以不会对搜索关键字进行分词:
# 请求:我们不关心相关度分数,所以用了constant_score,将相关度分数置为1
GET /forum/_search
{
"query" : {
"constant_score" : {
"filter" : {
"term" : {
"articleID" : "XHDK-A-1293-#fJ3"
}
}
}
}
}
# 响应:因为搜索关键字"XHDK-A-1293-#fJ3"不分词,而articleID这个字段本身是分词的,所以查不出结果
{
...
"hits" : {
...
"hits" : [ ]
}
}
对 articleID 进行 term filter 是查不出结果的,需要使用 articleID.keyword :
# 请求:对articleID.keyword进行term filter
GET /forum/_search
{
"query" : {
"constant_score" : {
"filter" : {
"term" : {
"articleID.keyword" : "XHDK-A-1293-#fJ3"
}
}
}
}
}
# 响应:因为articleID.keyword保存在完整text内容,所以可以匹配到
{
...
"hits" : {
...
"hits" : [
{
"_index" : "forum",
"_type" : "article",
"_id" : "1",
"_score" : 1.0,
"_source" : {
"articleID" : "XHDK-A-1293-#fJ3",
"userID" : 1,
"hidden" : false,
"postDate" : "2017-01-01"
}
}
]
}
}
1.3. 底层原理
匹配document
term filter在执行时,首先会在倒排索引中匹配filter条件,也就是我们的搜索关键字,用于获取document list。我们以postDate来举个例子,如果查找“2017-02-02”,会发现”2017-02-02“对应的document list是doc2、doc3:
| word | doc1 | doc2 | doc3 |
|---|---|---|---|
| 2017-01-01 | Y | Y | N |
| 2017-02-02 | N | Y | Y |
| 2017-03-03 | Y | Y | Y |
构建bitset
为每个fiter条件构建 bitset 。bitset 就是一个二进制的数组,数组每个元素都是 0 或 1 ,用来标识某个 doc 对这个 filter 条件是否匹配,匹配就是1,否则为0。例如:doc1不匹配"2017-02-02",而doc2和do3是匹配的,所以"2017-02-02"这个filter条件的bitset就是[0, 1, 1]。
遍历bitset
由于在一个search请求中,可以有多个filter条件,而filter条件都会对应一个bitset。所以这一步,ES会从最稀疏的bitset开始遍历,优先过滤掉尽可能多的数据。比如我们的filter条件是postDate=2017-01-01,userID=1,对应的bitset是:
postDate: [0, 0, 1, 1, 0, 0]
userID: [0, 1, 0, 1, 0, 1]
那么遍历完两个bitset之后,找到匹配所有filter条件只有doc4,就将其作为结果返回给client了。
缓存bitset
Elasticsearch会将一些频繁访问的filter条件和它对应的bitset缓存在内存中,这样就可以提高检索效率了。
注:如果 document 保存在某个很小的 segment 上的话(segment记录数小于1000,或segment小于index总大小的3%),Elasticsearch就不会对其缓存。因为 segment 很小的话,会在后台被自动合并,那么缓存也没有什么意义了,因问 segment 很快就消失了。
这里就可以看出,filter 为什么比 query 的性能更好了,filter 除了不需要计算相关度分数并按其排序外,filter 还会缓存检索结果对应的 bitset。
bitset更新
如果document有新增或修改,那么filter条件对应的cached bitset会被自动更新。例如:假设postDate=2017-01-01对应的 bitset 为[0, 0, 1, 0]。
新增一条doc5;id=5,postDate=2017-01-01,那postDate=2017-01-01这个filter的bitset会全自动更新成[0, 0, 1, 0, 1];
修改doc1;id=1,postDate=2016-12-30,那postDate=2016-01-01这个filter的bitset会全自动更新成[1, 0, 1, 0, 1];
2. 全文检索
Multi-Field 不是搜索exact value,而是对检索关键字进行分词后,实现倒排索引检索。多字段搜索,在多个不同的field中检索关键字。
全文检索时,如果需要针对多个field进行检索,我们一般会使用 match query 或 multi_match 语法。默认情况下,Elasticsearch进行这类多字段检索的策略是 most_fields 。
准备数据
curl PUT ip:port/forum/_bulk
{
"articleID" : "XHDK-A-1293-#fJ3",
"userID" : 1,
"hidden" : false,
"postDate" : "2017-01-01",
"tag" : [
"java",
"hadoop"
],
"view_cnt" : 30,
"title" : "this is java and elasticsearch blog"
},
{
"articleID" : "KDKE-B-9947-#kL5",
"userID" : 1,
"hidden" : false,
"postDate" : "2017-01-02",
"tag" : [
"java"
],
"view_cnt" : 50,
"title" : "this is java blog"
},
{
"articleID" : "JODL-X-1937-#pV7",
"userID" : 2,
"hidden" : false,
"postDate" : "2017-01-01",
"tag" : [
"hadoop"
],
"view_cnt" : 100,
"title" : "this is elasticsearch blog"
},
{
"articleID" : "QQPX-R-3956-#aD8",
"userID" : 2,
"hidden" : true,
"postDate" : "2017-01-02",
"tag" : [
"java",
"elasticsearch"
],
"view_cnt" : 80,
"title" : "this is java, elasticsearch, hadoop blog"
}
示例
# 请求:搜索title中包含关键字“java elasticsearch”的记录
curl GET ip:port/forum/_search
{
"query": {
"match": {
"title": "java elasticsearch"
}
}
}
全文检索时,会对搜索关键字进行拆分,上述"title"字段默认就是text类型,所以最终会以倒排索引的方式查询,只有记录中的“title”包含了“java”或“elasticsearch”,都会被检索出来。使用bool组合多个搜索条件:
curl GET ip:port/forum/_search
{
"query": {
"bool": {
"must": { "match": { "title": "java" }},
"must_not": { "match": { "title": "spark" }},
"should": [
{ "match": { "title": "hadoop" }},
{ "match": { "title": "elasticsearch" }}
]
}
}
minimum_should_match
指定的关键字中,必须至少匹配其中的多少个关键字,才能作为结果返回,可以利用minimum_should_match参数:
curl GET ip:port/forum/article/_search
{
"query": {
"match": {
"title": {
"query": "java elasticsearch spark hadoop",
"minimum_should_match": "75%"
}
}
}
}
上述查询到的结果中,至少会包含“java“、“elasticsearch“、“spark“、“hadoop”中的三个。
boost权重
对检索关键字拆分后的某些词被优先检索。Elasticsearch进行相关度分数计算时,权重越大,相应的 relevance score 会越高,也就会优先被返回。默认情况下,搜索条件的权重都是1。例如:检索出title包含hadoop或elasticsearch的记录,hadoop优先搜索出来,设置hadoop的权重更大:
GET /forum/_search
{
"query": {
"bool": {
"should": [
{
"match": {
"title": {
"query": "hadoop",
"boost": 5
}
}
},
{
"match": {
"title": {
"query": "elasticsearch",
"boost": 2
}
}
}
]
}
}
}
注:如果index有多个shard的话,搜索结果可能不准确。因为对于一个搜索请求, coordinate node 可能会将其转发给任意一个 shard 。Elasticsearch 在计算相关度分数时,采用了 TF/IDF 算法,该算法需要知道关键字在所有 document 中出现的次数,而每个 shard 只包含了部分 document , TF/IDF 算法计算时只采用了当前 shard 中的所有 document 数,所以对于不同 shard 计算出的相关度分数可能都是不同的。
底层原理
使用match query进行检索时,Elasticsearch底层会转换成term形式。例如:
curl GET ip:port/forum/_search
{
"query": {
"match": {
"title": {
"query": "java elasticsearch",
"operator": "and"
}
}
}
}
Elasticsearch会将其转换成如下term形式:
curl GET ip:port/forum/_search
{
"query": {
"bool": {
"should": [
{ "term": { "title": "java" }},
{ "term": { "title": "elasticsearch" }}
]
}
}
}
2.1. best_fields策略
对多个filed进行搜索匹配时,挑选某个field匹配度最高的分数,最高分相同的情况下,考虑其他query的分数。
multi-field搜索
# 准备数据
# 1
{ "doc" : {"title" : "this is java and elasticsearch blog","content" : "i like to write best elasticsearch article"} }
# 2
{ "doc" : {"title" : "this is java blog","content" : "i think java is the best programming language"} }
# 3
{ "doc" : {"title" : "this is elasticsearch blog","content" : "i am only an elasticsearch beginner"} }
# 4
{ "doc" : {"title" : "this is java, elasticsearch, hadoop blog","content" : "elasticsearch and hadoop are all very good solution, i am a beginner"} }
# 5
{ "doc" : {"title" : "this is spark blog","content" : "spark is best big data solution based on scala ,an programming language similar to java"} }
搜索title或content中包含 java 或 solution 关键字的帖子,这其实就是典型的multi-field搜索:
# should相当于SQL语法中的OR
curl GET ip:port/forum/_search
{
"query": {
"bool": {
"should": [
{ "match": { "title": "java solution" }},
{ "match": { "content": "java solution" }}
]
}
}
}
doc5 content字段既包含“java”又包含“solution”应该是匹配度最高。但doc5的相关度分数(relevance score)并不是最高,multi-field搜索默认情况下,Elasticsearch采用的是 most_fields 策略,算法大致为:
- 计算每个query的分数,然后求和。对于上述搜索,就是“should”中的两个field检索条件,比如doc4计算的结果分别是1.1和1.2,相加为2.3;
- 计算matched query的数量,比如对于doc4,两个field都能匹配到,数量就是2;
sum(每个query的分数)* count(matched query) / count(总query数量)作为最终相关度分数。
对于doc4,上述算法的计算结果就是:(1.1+1.2) x 2/2=2.3;而对于doc5,title字段是匹配不到结果的,所以matched query=1,doc5的最终分数可能是(0+2.3) x 1/2=1.15,所以检索结果排在了doc4后面。
dis_max
一个 field 匹配到了尽可能多的关键词,其分数更高;而不是尽可能多的 field 匹配到了少数的关键词,却排在了前面。
Elasticsearch提供了 dis_max 语法,可以直接取多个 query 中,分数最高的那一个 query 的分数,例如:
curl GET ip:port/forum/_search
{
"query": {
"dis_max": {
"queries": [
{ "match": { "title": "java solution" }},
{ "match": { "content": "java solution" }}
]
}
}
}
比如对于上述的doc4,两个field检索的最终分数分别为1.1和1.2,那就取最大值1.2:
{ "match": { "title": "java solution" }} -> 1.1
{ "match": { "content": "java solution" }} -> 1.2
对于doc5,针对“title”的检索没有匹配结果,分数为0,但“content”的分数为2.3,所以取最大值2.3:
{ "match": { "title": "java solution" }} -> 0
{ "match": { "content": "java solution" }} -> 2.3
tie_breaker
dis_max只取多个query中,分数最高query的分数,而完全不考虑其它query的分数。但有时这并不能满足我们的需求,举个例子,我们希望检索title字段包含“java solution”或"content"字段包含“java solution”的帖子,最终满足条件的每个doc的匹配结果如下:
- doc1,title中包含“java“,content不包含“java“、“solution“任何一个关键词;
- doc2,title中不包含任何一个关键词,content中包含“solution”;
- doc3,title中包含“java“,content中包含“solution“。
最终搜索结果是,doc1和doc2排在了doc3的前面。此时我们可以利用 tie_breaker 参数将其他 query 的分数也考虑进去:
curl GET ip:port/forum/_search
{
"query": {
"dis_max": {
"queries": [
{ "match": { "title": "java solution" }},
{ "match": { "content": "java solution" }}
],
"tie_breaker": 0.3
}
}
}
tie_breaker 在 0-1 之间,其意义在于:其他query的分数乘以 tie_breaker ,然后再与最高分数的那个query进行计算,得到最终分数。
multi_match搜索
dis_max 和 tie_breaker 为 bese_fields 策略的核心实现原理了。Elasticsearch还提供了 multi_match 搜索,简化 bese_fields 策略:
curl GET ip:port/forum/_search
{
"query": {
"multi_match": {
"query": "java solution",
"type": "best_fields",
"fields": [ "title", "content" ],
"tie_breaker": 0.3,
"minimum_should_match": "50%"
}
}
}
dis_max 和 tie_breaker 和来实现同样的效果,则是下面这样,可以看到multi_match确实简化了编码:
curl GET ip:port/forum/_search
{
"query": {
"dis_max": {
"queries": [
{
"match": {
"title": {
"query": "java solution",
"minimum_should_match": "50%"
}
}
},
{
"match": {
"body": {
"query": "java solution",
"minimum_should_match": "50%"
}
}
}
],
"tie_breaker": 0.3
}
}
}
优缺点
best_fields策略是最常用,也是最符合人类思维的搜索策略。Google、Baidu之类的搜索引擎,默认就是用的这种策略。
优点通过best_fields策略,以及综合考虑其他field,还有minimum_should_match支持,可以尽可能精准地将匹配的结果推送到最前面。缺点除了那些精准匹配的结果,其他差不多大的结果,排序结果不是太均匀,没有什么区分度了。
2.2. most_fields策略
multi-field 搜索时的默认策略,其实就是综合多个 field 一起进行搜索,尽可能多地让所有query参与到总分的计算中,结果不一定精准。例如:某个document的一个field虽然包含更多的关键字,但是因为其他document有更多field匹配到了,所以其它的doc会排在前面。
curl GET ip:port/forum/_search
{
"query": {
"multi_match": {
"query":"java solution",
"type":"most_fields",
"fields":[ "title", "content" ]
}
}
}
**优缺点
优点将尽可能匹配更多field的结果推送到最前面,整个排序结果是比较均匀的。缺点可能那些精准匹配的结果,无法推送到最前面。
2.3. cross-fields策略
cross-fields搜索,就是跨多个field去搜索一个标识。比如姓名字段可以散落在多个field中,first_name和last_name,地址字段可以散落在country、province、city中,那么搜索人名或者地址,就是cross-fields搜索。
要进行cross-fields搜索,我们可能会立马想到使用上面讲的most_fields策略,因为multi_fields会考虑多个field匹配的分数,而cross-fields搜索本身刚好就是多个field检索的问题。
准备数据
# 1
{ "doc" : {"author_first_name" : "Peter", "author_last_name" : "Smith"} }
# 2
{ "doc" : {"author_first_name" : "Smith", "author_last_name" : "Williams"} }
# 3
{ "doc" : {"author_first_name" : "Jack", "author_last_name" : "Ma"} }
# 4
{ "doc" : {"author_first_name" : "Robbin", "author_last_name" : "Li"} }
# 5
{ "doc" : {"author_first_name" : "Tonny", "author_last_name" : "Peter Smith"} }
检索姓名中包含“Peter Smith”的用户信息:
curl GET ip:port/forum/_search
{
"query": {
"multi_match": {
"query": "Peter Smith",
"type": "most_fields",
"fields": [ "author_first_name", "author_last_name" ]
}
}
}
检索出的结果包含:doc1、doc2、doc5,我们希望的结果应该是doc5排在最前面,然后是doc1,最后才是doc2,即doc5>doc1>doc2,但事实上,doc5可能会排在最后。之所以会出现这种情况,跟 TF/IDF 算法有关。
使用multi_match提供的cross-fields策略:
curl GET ip:port/forum/_search
{
"query": {
"multi_match": {
"query": "Peter Smith",
"type": "cross_fields",
"operator": "and",
"fields": ["author_first_name", "author_last_name"]
}
}
}
使用 cross-fields 策略进行多字段检索时,会要求关键字拆分后的每个 term 必须出现在被检索的字段中。比如上面我们检索“Peter Smith”时,会拆成“Peter”和“Smith”两个term,那就要求:
- Peter必须在author_first_name或author_last_name中出现;
- Smith必须在author_first_name或author_last_name中出现。
3. 近似匹配
Proximity Match 包含两种类型:phrase match和proximity match 。
# doc1
java is my favourite programming language, and I also think spark is a very good big data system.
# doc2
java spark are very related, because scala is spark's programming language and scala is also based on jvm like java.
索出包含“java spark”关键字的doc,但是必须满足以下任一条件:
- java spark,就靠在一起,中间不能插入任何其他字符;
- java和spark两个单词靠的越近,doc的分数越高,排名越靠前。
3.1. 短语匹配
phrase match 将搜索词多个term作为一个短语,只有包含短语的doc为结果返回。与match query不同,任何一个term匹配就会返回结果,Phrase Match的基本语法如下:
curl GET ip:port/forum/_search
{
"query": {
"match_phrase": {
"title": {
"query": "java spark",
"slop": 1
}
}
}
}
基本原理
Elasticsearch在建立倒排索引时,会记录每个term在文本内容中的位置。比如我们有下面两条doc:
doc1: hello world, java spark
doc2: hi, spark java
建立完的倒排索引包含以下内容,hello 这个 term 出现在 doc1 的 position 0,以此类推:
| Term | Doc1中的位置 | Doc2中的位置 |
|---|---|---|
| hello | doc1(0) | N |
| wolrd | doc1(1) | N |
| java | doc1(2) | doc2(2) |
| spark | doc1(3) | doc2(1) |
当使用Phrase Match(短语匹配)时,步骤如下:
- Elasticsearch会首先对短语分词,"java spark"拆分为"java"和"spark" ;
- 筛选出“java”和“spark”都存在的doc,也就是说doc必须包含短语中的所有term,那doc1和doc2都满足;
- 后一个短语的position必须比前一个大1,即“spark”的position要比java的position大1,那只有doc1满足条件。
slop参数
Phrase Match有一个很重要的参数——slop,表示短语中的term,最多经过几次移动才能与一个document匹配,这个移动次数,就是slop。举个例子,假如有下面这样一条doc,搜索的短语是"spark data":
spark is best big data solution based on scala.
slop=3时,就可以匹配到,可以看下面的移动步骤:
spark is best big data...
spark data
-->data
-->data
-->data
注:移动的方向可以是双向的,比如搜索的短语是"data spark"。slop=5时,也可以匹配到:
spark is best big data...
data spark
spark <-->data
spark -->data
spark -->data
spark -->data
当使用Phrase Match时,term靠的越近,相关度分数会越高。
rescore
重打分 通常与match query配合使用。例如:
curl GET ip:port/forum/_search
{
"query": {
"match": {
"content": "java spark"
}
},
"rescore": {
"window_size": 50,
"query": {
"rescore_query": {
"match_phrase": {
"content": {
"query": "java spark",
"slop": 50
}
}
}
}
}
}
对math query匹配到的结果重新打分,上述window_size表示取前50条记录进行打分,配合phrase match可以使term越接近的短语分数更高,从而既提供了精确度,又提升了召回率。
所谓召回率,就是进行检索时,返回的document的数量,数量越多,召回率越高。近似匹配通常会和match query搭配使用以提升召回率。
搜索推荐
利用 match_phrase_prefix 进行搜索推荐。搜索推荐的原理跟 match_phrase 类似,唯一的区别就是把检索词中的最后一个 term 作为前缀去搜索。
curl GET ip:port/forum/_search
{
"query": {
"match_phrase_prefix": {
"content": {
"query": "java s",
"slop": 10,
"max_expansions": 5
}
}
}
}
默认情况下,前缀要扫描所有的倒排索引中的term,去查找"s"打头的单词,但是这样性能太差,所以可以用 max_expansions 限定,"s"前缀最多匹配多少个term,不再继续搜索倒排索引了。
不推荐使用 match_phrase_prefix 来实现搜索推荐,因为Elasticsearch会在扫描倒排索引时实时进行前缀匹配,性能很差。如果要实现搜索推荐功能,建议使用 ngram 分词机制。
3.2. proximity match
phrase match 与 proximity match 底层原理一样,都是通过倒排索引去匹配搜索关键词中的各个term的位置, proximity match 可以看成是加了 slop 参数的 phrase match 。
4. 搜索推荐
N-Gram是大词汇连续语音识别中常用的一种语言模型。
对于单词“quick”,做如下拆分,“quick” term就被拆分成了5种长度下的ngram,每种长度下的拆分项都是一个ngram:
# ngram.length=1
q u i c k
# ngram.length=2
qu ui ic ck
# ngram.length=3
qui uic ick
# ngram.length=4
quic uick
# ngram.length=5
quick
Elasticsearch 使用 edge ngram 分词方法,比如我们有两个下面这样的document:
# doc1
hello world
# doc2
hello what
Elasticsearch对文本中每个 term,按照 edge ngram 机制建立倒排索引:
| term | doc1 | doc2 |
|---|---|---|
| h | Y | Y |
| he | Y | Y |
| hel | Y | Y |
| hell | Y | Y |
| hello | Y | Y |
| w | Y | Y |
| wo | Y | N |
| wor | Y | N |
| worl | Y | N |
| world | Y | N |
| wh | N | Y |
| wha | N | Y |
| what | N | Y |
检索“hello w”时,首先会对“hello”这个term检索,发现doc1和doc2都有,然后对“w”这个term检索,发现doc1和doc2也都有,所以doc1和duc2都会被返回,这样就实现了搜索推荐。检索时完全利用到了倒排索引,并没有去做前缀匹配,ngram机制实现的搜素推荐效率非常高。
示例
建立索引,min_gram和max_gram用于控制ngram的长度:
curl PUT ip:port/my_index
{
"settings": {
"analysis": {
"filter": {
"autocomplete_filter": {
"type": "edge_ngram",
"min_gram": 1,
"max_gram": 20
}
},
"analyzer": {
"autocomplete": {
"type": "custom",
"tokenizer": "standard",
"filter": [
"lowercase",
"autocomplete_filter"
]
}
}
}
}
}
查看下分词结果:
curl GET ip:port/my_index/_analyze
{
"analyzer": "autocomplete",
"text": "quick brown"
}
对需要实现搜索推荐的字段,修改其字段使用的分词器就完成了:
curl PUT ip:port/my_index/_mapping
{
"properties": {
"title": {
"type": "string",
"analyzer": "autocomplete",
"search_analyzer": "standard"
}
}
}
上面analyzer的意思是对title字段的内容建立索引时,使用autocomplete这个分词器,也就是ngram分词;search_analyzer的意思是,对于我们的检索词,比如“hello w”,还是用标准的standard分词器拆分。