1. 准备测试数据
PUT article
{
"mappings": {
"_doc": {
"properties": {
"articleId": {
"type": "keyword"
}
}
}
}
}
POST /article/_doc/_bulk
{"index": {"_id": 1}}
{"articleId": "XHDK-A-1293-#fJ3", "userId": 1, "hidden": false, "postDate": "2017-01-01", "tag": ["java", "elasticsearch"], "tag_cnt": 2, "view_cnt": 30, "title" : "this is java and elasticsearch blog"}
{"index": {"_id": 2}}
{"articleId": "KDKE-B-9947-#kL5", "userId": 1, "hidden": false, "postDate": "2017-01-02", "tag": ["java"], "tag_cnt": 1, "view_cnt": 50, "title" : "this is java blog"}
{"index": {"_id": 3}}
{"articleId": "JODL-X-1937-#pV7", "userId": 2, "hidden": false, "postDate": "2017-01-01", "tag": ["elasticsearch"], "tag_cnt": 1, "view_cnt": 100, "title": "this is elasticsearch blog"}
{"index": {"_id": 4}}
{"articleId": "QQPX-R-3956-#aD8", "userId": 2, "hidden": true, "postDate": "2017-01-02", "tag": ["java", "elasticsearch", "hadoop"], "tag_cnt": 3, "view_cnt": 80, "title": "this is java, elasticsearch, hadoop blog"}
{"index": {"_id": 5}}
{"articleID": "DHJK-B-1395-#Ky5", "userID": 3, "hidden": false, "postDate": "2017-03-01", "tag": ["spark"], "tag_cnt": 1, "view_cnt": 10, "title": "this is spark blog"}
2. 手动控制搜索精确度
# 搜索标题中包含java或elasticsearch的blog
# 4条结果
GET /article/_doc/_search
{
"query": {
"match": {
"title": "java elasticsearch"
}
}
}
# 搜索标题中包含java和elasticsearch的blog
# 2条结果
# 如果希望所有的搜索关键字都要匹配,那么就用and
GET /article/_doc/_search
{
"query": {
"match": {
"title": {
"query": "java elasticsearch",
"operator": "and"
}
}
}
}
# 搜索包含标题中java,elasticsearch,spark,hadoop,4个关键字中,至少3个的blog
# 1条结果
GET /article/_doc/_search
{
"query": {
"match": {
"title": {
"query": "java elasticsearch spark hadoop",
"minimum_should_match": "75%"
}
}
}
}
# 用bool组合多个搜索条件
# 查询标题中必须包含"java",必须不包含"spark","hadoop"和"elasticsearch"包含不包含都可以的帖子
GET /article/_doc/_search
{
"query": {
"bool": {
"must": {
"match": {"title": "java"}
},
"must_not": {
"match": {"title": "spark"}
},
"should": [
{"match": {"title": "hadoop"}},
{"match": {"title": "elasticsearch"}}
]
}
}
}
# 结果
{
"took" : 10,
"timed_out" : false,
"_shards" : {
"total" : 5,
"successful" : 5,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : 3,
"max_score" : 1.449981,
"hits" : [
{
"_index" : "article",
"_type" : "_doc",
"_id" : "4",
"_score" : 1.449981,
"_source" : {
"articleId" : "QQPX-R-3956-#aD8",
"userId" : 2,
"hidden" : true,
"postDate" : "2017-01-02",
"tag" : [
"java",
"elasticsearch",
"hadoop"
],
"tag_cnt" : 3,
"view_cnt" : 80,
"title" : "this is java, elasticsearch, hadoop blog"
}
},
{
"_index" : "article",
"_type" : "_doc",
"_id" : "1",
"_score" : 0.5753642,
"_source" : {
"articleId" : "XHDK-A-1293-#fJ3",
"userId" : 1,
"hidden" : false,
"postDate" : "2017-01-01",
"tag" : [
"java",
"elasticsearch"
],
"tag_cnt" : 2,
"view_cnt" : 30,
"title" : "this is java and elasticsearch blog"
}
},
{
"_index" : "article",
"_type" : "_doc",
"_id" : "2",
"_score" : 0.19856805,
"_source" : {
"articleId" : "KDKE-B-9947-#kL5",
"userId" : 1,
"hidden" : false,
"postDate" : "2017-01-02",
"tag" : [
"java"
],
"tag_cnt" : 1,
"view_cnt" : 50,
"title" : "this is java blog"
}
}
]
}
}
bool组合多个搜索条件,如何计算relevance score(相关度分数)?
relevance score = must和should搜索对应的分数加起来 / must和should的总数
排名第一:标题包含"java",同时包含should中所有的关键字即"hadoop"和"elasticsearch"
排名第二:标题包含"java",同时包含should中的任何一个关键字
排名第三:标题包含"java",不包含should中的任何关键字
should是可以影响相关度分数的,根据must的条件去计算出document对这个搜索条件的relevance score,在满足must的基础之上,should中的条件,不匹配也可以,但是如果匹配的更多,那么document的relevance score就会更高
# 使用bool组合多个搜索条件,控制全文检索的精确度
# 搜索标题中至少包含"java"、"hadoop"、"spark"、"elasticsearch"其中3个关键字的帖子
GET /article/_doc/_search
{
"query": {
"bool": {
"should": [
{"match": {"title": "java"}},
{"match": {"title": "elasticsearch"}},
{"match": {"title": "hadoop"}},
{"match": {"title": "spark"}}
],
"minimum_should_match": 3
}
}
}
默认情况下,在有must的情况下,should可以不匹配任何条件,在没有must的情况下,多个should的条件必须满足其中一个,可以使用"minimum_should_match"参数来精确控制should的行为。
minimum_should_match:
- 正数,例如3,那么should的多个条件中必须满足3个条件
- 负数,例如-2,代表可以有2个条件不满足,其他都应该满足
- 百分比正数:代表should条件总数的百分比个条件应该满足,例如总共10个条件,百分比为30%,那么至少3个条件应该满足,需满足条件的个数向下取整
- 百分比负数:代表占此比例的条件可以不满足,其余的均需要满足,计算结果向下取整
- 百分比和数字组合:3<90%,如果条件个数<=3,那么必须全部满足,否则,满足90%(向下取整)即可
- 多个组合(空格隔开):2<-25% 9<-3,如果条件个数<=2,则必须都满足,如果条件个数为[3,9],则需要25%的条件满足,否则,只能有3个条件不满足,其余都需要满足
3.match query的自动转化
# 在使用match query进行多值搜索的时候,es会在底层自动将match query转换为bool的语法
{
"match": {"title": "java elasticsearch"}
}
# 转化为
{
"bool": {
"should": [
{"term": {"title": "java"}},
{"term": {"title": "elasticsearch"}},
]
}
}
{
"match": {
"title": {
"query": "java elasticsearch",
"operator": "and"
}
}
}
# 转换为
{
"bool": {
"must": [
{ "term": { "title": "java" }},
{ "term": { "title": "elasticsearch"}}
]
}
}
{
"match": {
"title": {
"query": "java elasticsearch hadoop spark",
"minimum_should_match": "75%"
}
}
}
# 转换为
{
"bool": {
"should": [
{ "term": { "title": "java" }},
{ "term": { "title": "elasticsearch"}},
{ "term": { "title": "hadoop" }},
{ "term": { "title": "spark" }}
],
"minimum_should_match": 3
}
}
4. boost:搜索条件权重控制
需求:搜索标题中包含"blog"的帖子,同时如果标题中包含"java"、hadoop"、"elasticsearch"或者"spark"也可以,但包含"spark"的帖子要求它被优先搜索出来
知识点,搜索条件的权重,boost,可以将某个搜索条件的权重加大,此时当匹配这个搜索条件和匹配另一个搜索条件的document,计算relevance score时,匹配权重更大的搜索条件的document,relevance score会更高,也就会优先被返回
默认情况下,搜索条件的权重都是一样的,都是1
GET /article/_doc/_search
{
"query": {
"bool": {
"must": [
{"match": {"title": "blog"}}
],
"should": [
{"match": {"title": "java"}},
{"match": {"title": "hadoop"}},
{"match": {"title": "elasticsearch"}},
{
"match": {
"title": {
"query": "spark",
"boost": 5.0
}
}
}
]
}
}
}
5. 多shard场景下relevance score不准确问题
如果你的一个index有多个shard的话,可能搜索结果会不准确,原因如下:
当一个搜索请求被转发到一个shard后,会这样计算每条document的relevance score:
- 计算关键词在这条docuemnt的指定field中出现的次数(TF)
- 计算关键词在此shard的所有docuemnt的指定field中出现的次数(IDF)
- 注意:某个shard中只是包含一个index的部分document,而在默认情况下,IDF就是在shard本地进行计算
relevance score与TF成正比,与IDF成反比,在不考虑其他因素的前提下(relevance score不仅与TF和IDF有关),我们可以认为score=TF/IDF
假设在A shard中,所有"title"中包含"java"关键词的doucment,在某一条document中,"java"在"title"字段中出现了10次,但是在A shard中,"java"在所有的document的"title"字段中出现了100次,那么在A shard中,score=10/100=0.1
假设在B shard中,所有"title"中包含"java"关键词的doucment,在某一条document中,"java"在"title"字段中出现了1次,但是在B shard中,"java"在所有的document的"title"字段中也出现了1次,那么在B shard中,score=1/1=1
这样就造成了结果的不准确,应该是A shard中的那条document的score比B shard中的docuemnt的score高,造成这种现象的原因就是IDF是在一个shard的本地计算的,如果是在所有的shard中计算就不会有这个问题
解决办法:
-
生产环境下,数据量大,尽可能实现均匀分配
数据量很大的话,一般情况下,ES都是在多个shard中均匀路由数据的,路由的时候根据_id,进行负载均衡,比如说有10个document,其"title"都包含"java"关键词,一共有5个shard,如果负载均衡的话,每个shard都应该有2个document,这样计算出来的结果就没有问题了
测试环境下,将索引的primary shard设置为1个,如果说只有一个shard,当然也就没有这个问题了
测试环境下,搜索附带
search_type=dfs_query_then_fetch
参数,此参数的作用是计算IDF的时候,计算全局的IDF而非本地的IDF,这样可以解决这个问题,但是会带来性能问题,在生产环境不推荐使用
6. dis_max:实现搜索的best_fields策略
6.1 dis_max
# 为帖子增加"content"字段
POST /article/_doc/_bulk
{"update": {"_id": "1"}}
{"doc": {"content": "i like to write best elasticsearch article"}}
{"update": {"_id": "2"}}
{"doc": {"content": "i think java is the best programming language"}}
{"update": {"_id": "3"}}
{"doc": {"content": "i am only an elasticsearch beginner"}}
{"update": { "_id": "4"}}
{"doc": {"content": "elasticsearch and hadoop are all very good solution, i am a beginner"}}
{"update": { "_id": "5"}}
{"doc": {"content": "spark is best big data solution based on scala ,an programming language similar to java"}}
# 搜索title或content中包含java或solution的帖子
GET /article/_doc/_search
{
"query": {
"bool": {
"should": [
{"match": {"title": "java solution"}},
{"match": {"content": "java solution"}}
]
}
},
"_source": ["title", "content"]
}
# 结果
# 1. doc2(score=0.95): title匹配"java",content匹配"java"
# 2. doc4(score=0.81): title匹配"java",content匹配"solution"
# 3. doc5(score=0.58): title无匹配,content匹配"java"和"solution"
# 4. doc1(score=0.29): title匹配"java",content无匹配
# 我们期望的是doc5排名靠前,因为content同时匹配了"java"和"solution"
# 而实际却是doc2和doc4排名更靠前
# 分析一下doc4和doc5的socre
# score=每个query的分数 * 匹配到条件的个数 / 总条件个数
# 假设在每个查询条件中,匹配到一个单词得分1
# 那么doc4得分为(1+1)*2/2=2
# 同理doc5得分为(0+2)*1/2=1
# 于是doc4就排在了doc5之前
- dis_max query:搜索到的结果,如果某一个field中匹配到了尽可能多的关键词,那么它应该评分更高,而不是尽可能多的field匹配到了少数的关键词就排在了前面
- dis_max的原理是:多个query中,得分最高的query的分数,就是最终的分数
GET /article/_doc/_search
{
"query": {
"dis_max": {
"queries": [
{"match": {"title": "java solution"}},
{"match": {"content": "java solution"}}
]
}
},
"_source": ["title", "content"]
}
# 结果
# 1. doc2(score=0.75): title匹配"java",content匹配"java"
# 2. doc4(score=0.64): title匹配"java",content匹配"solution"
# 3. doc5(score=0.58): title无匹配,content匹配"java"和"solution"
# 4. doc1(score=0.29): title匹配"java",content无匹配
# 说明
# 这里没有达到我的预期效果,预期应该是doc5的排名应该更靠前,这可能与影响评分的其他因素有关
# 但是doc2和doc4的得分都下降了,说明dis_max还是有有效果的
6.2 tie_breaker
# 换一个搜索条件
GET /article/_doc/_search
{
"query": {
"dis_max": {
"queries": [
{"match": {"title": "java beginner"}},
{"match": {"content": "java beginner"}}
]
}
},
"_source": ["title", "content"]
}
# 结果
# 1. doc2(score=0.75): title匹配java,content匹配java
# 2. doc4(score=0.64): title匹配java,content匹配beginner
# 3. doc5(score=0.29): title不匹配,content匹配java
# 4. doc1(score=0.29): title匹配java,content不匹配
# 5. doc3(score=0.29): title不匹配,content匹配beginner
# tie_breaker
# 这里结果还是比较符合预期的,以下是一种不符合预期的结果:
# 1. docA: title匹配java,content不匹配
# 1. docB: title不匹配,content匹配beginner
# 2. docC: title匹配java,content匹配beginner
# 对于上述结果,我们期望的可能是docC排在其他两个doc之前,这就需要使用tie_breaker
# dis_max只取某一个query最大的分数,完全不考虑其他query的分数
# 使用tie_breaker可以将其他query的分数也考虑进去
# tie_breaker参数的作用:
# 将其他query的分数乘以tie_breaker的值
# 然后与最高的分数综合在一起进行计算
# 除了取最高分以外,还会考虑其他的query的分数
# tie_breaker的值,在0~1之间,是个小数
GET /article/_doc/_search
{
"query": {
"dis_max": {
"queries": [
{"match": {"title": "java beginner"}},
{"match": {"content": "java beginner"}}
],
"tie_breaker": 0.3
}
},
"_source": ["title", "content"]
}
# 结果
# 1. doc2(score=0.81): title匹配java,content匹配java
# 2. doc4(score=0.69): title匹配java,content匹配beginner
# 3. doc5(score=0.29): title不匹配,content匹配java
# 4. doc1(score=0.29): title匹配java,content不匹配
# 5. doc3(score=0.29): title不匹配,content匹配beginner
# 可以看出对分数还是有影响的
6.3 multi_match、dis_max和tie_breaker的联合使用
GET /article/_doc/_search
{
"query": {
"multi_match": {
"query": "java solution",
"type": "best_fields", # best fields就是dis_max的策略,即认为一个字段匹配到尽可能多的关键词就评分更高
"fields": ["title^2", "content"], # "title^2:将title字段的权重乘以2
"tie_breaker": 0.3,
"minimum_should_match": "50%"
}
},
"_source": ["title", "content"]
}
# 上面的查询语法与下面的查询语法的结果是一样的
GET /article/_doc/_search
{
"query": {
"dis_max": {
"queries": [
{
"match": {
"title": {
"query": "java solution",
"minimum_should_match": "50%",
"boost": 2
}
}
},
{
"match": {
"content": {
"query": "java solution",
"minimum_should_match": "50%"
}
}
}
],
"tie_breaker": 0.3
}
},
"_source": ["title", "content"]
}
# minimum_should_match的作用:去长尾
# 比如搜索5个关键词,但是很多结果只匹配1个关键词的,这些结果与预期相差甚远,这些结果就是长尾
# minimum_should_match,控制搜索结果的精准度,只有匹配一定数量的关键词的数据,才能返回
7. most-fields策略
- best-fields策略:某一个field匹配尽可能多的关键词的doc评分更高
- most-fields策略:尽可能多的field可以匹配到关键词,那个这个doc的评分会更高
# 增加测试数据
POST /article/_mapping/_doc
{
"properties": {
"sub_title": {
"type": "text",
"analyzer": "english",
"fields": {
"std": {
"type": "text",
"analyzer": "standard"
}
}
}
}
}
POST /article/_doc/_bulk
{"update": {"_id": "1"}}
{"doc": {"sub_title": "learning more courses"}}
{"update": {"_id": "2"}}
{"doc": {"sub_title": "learned a lot of course"}}
{"update": {"_id": "3"}}
{"doc": {"sub_title" : "we have a lot of fun"}}
{"update": {"_id": "4"}}
{"doc": {"sub_title": "both of them are good"}}
{"update": {"_id": "5"}}
{"doc": {"sub_title": "haha, hello world"}}
GET /article/_doc/search
{
"query": {
"match": {
"sub_title": "learning courses"
}
}
}
# 结果,两条:doc1和doc2
# 使用english分词器进行分词后,learning->learn,learned->learn,courses->course
# 所以两条都可以搜索到
# 只有doc1一条结果
# sub_title.std使用standard分词器,按照非字母和非数字字符进行分隔,单词转为小写,不会进行常规化处理
GET /article/_doc/_search
{
"query": {
"match": {
"sub_title.std": "learning courses"
}
}
}
# most_fieds策略
GET /article/_doc/_search
{
"query": {
"multi_match": {
"query": "learning courses",
"type": "most_fields",
"fields": ["sub_title", "sub_title.std"]
}
},
"_source": ["sub_title"]
}}
# 1. doc2(score=1.39): sub_title匹配learn和course,sub_title.std无匹配
# 2. doc1(score=1.15): sub_title匹配learn和course,sub_title.std匹配learning和courses
# 这里结果也是很难理解的,因为我们预期doc1是比doc2优先返回的
# 评分计算时很复杂的, 不只是TF/IDF算法,不同的query,不同的语法,都有不同的计算score的细节,所以这里就不再深究了
best_fields与most_fields策略的区别:
-
best_fields,对多个field进行搜索,挑选某个field匹配度最高的那个分数,同时在多个query最高分相同的情况下,在一定程度上考虑其他query的分数,简单来说,对多个field进行搜索,某一个field尽可能包含更多关键字的doc评分更好
优点:通过best_fields策略,以及综合考虑其他field,还有minimum_should_match支持,可以尽可能精准地将匹配的结果推送到最前面
缺点:除了那些精准匹配的结果,其他差不多大的结果,排序结果不是太均匀,没有什么区分度了
例子:百度,最匹配的排到最前面
-
most_fields,综合多个field一起进行搜索,尽可能多地让所有field的query参与到总分数的计算中来,有越多的field可以匹配到关键词,这条doc的评分就更高
优点:将匹配到更多field的结果推送到最前面,整个排序结果是比较均匀的
缺点:可能那些精准匹配的结果,无法推送到最前面
例子:wiki,明显的most_fields策略,搜索结果比较均匀,但是的确要翻好几页才能找到最匹配的结果
8. cross-fields搜索
cross-fields搜索:
搜索的文本包含在多个field中,比如搜索"James Bob","James"在"first_name"字段中保存,"Bob"在"last_name"字段中保存,或者搜索一个地址文本,这些信息散落在"country","province","city"等字段中,这样的搜索就叫做cross-fields搜索。
使用most_fields策略进行cross-fields搜索是比较合适的,因为cross-fields本来就是需要在多个field中去搜索,而most_fields策略就是尽可能得去多个field中去匹配关键词
# 添加测试数据
POST /article/_doc/_bulk
{"update": {"_id": "1"}}
{"doc" : {"author_first_name": "Peter", "author_last_name": "Smith"}}
{"update": {"_id": "2"}}
{"doc" : {"author_first_name": "Smith", "author_last_name": "Williams"}}
{"update": {"_id": "3"}}
{"doc" : {"author_first_name": "Jack", "author_last_name": "Ma"}}
{"update": {"_id": "4"}}
{"doc" : {"author_first_name": "Robbin", "author_last_name": "Li"}}
{"update": {"_id": "5"}}
{"doc" : {"author_first_name": "Tonny", "author_last_name": "Peter Smith"}}
GET /article/_doc/_search
{
"query": {
"multi_match": {
"query": "Peter Smith",
"type": "most_fields",
"fields": ["author_first_name", "author_last_name"]
}
},
"_source": ["author_first_name", "author_last_name"]
}
# 结果
# 1. doc2(score=0.69): author_first_name匹配Smith,author_last_name无匹配
# 2. doc5(score=0.58): author_first_name不匹配,author_last_name匹配Peter和Smith
# 3. doc1(score=0.58): author_first_name匹配Peter,author_last_name匹配Smith
# 结果分析:为什么只匹配到"Smith"的doc反而排名最高
# score受TF/IDF的影响,IDF就是搜索词在所有文档中出现的次数,这个数字越高,TF/IDF越低
# 在所有文档的 "author_last_name"字段中,Smith出现2次,Peter出现1次
# 在所有文档的 "author_first_name"字段中,Smith出现1次,Peter出现1次
# 所有,在author_last_name中匹配到Smith的分数就不如在author_first_name中匹配到Smith的分数
# 当然影响分数的因素是很多的,这里是说一个普适的规律
使用most_fields进行cross-fields搜索的一些问题:
- 问题1:越多的field的匹配到关键词其分数会高与少量field匹配到多个关键词的分数
- 问题2:没办法用minimum_should_match去掉长尾数据,就是匹配的特别少的数据
- 问题3:TF/IDF算法可能导致结果无法符合预期,比如上面例子中的情况
解决办法一:copy_to,将多个field组合成一个field,用了copy_to语法之后,就可以将多个字段的值拷贝到一个字段中,并建立倒排索引,但是在index中是查不到这个字段的,这是一个隐藏的字段
PUT my_index
{
"mappings": {
"_doc": {
"properties": {
"first_name": {
"type": "text",
"copy_to": "full_name"
},
"last_name": {
"type": "text",
"copy_to": "full_name"
},
"full_name": {
"type": "text"
}
}
}
}
}
PUT my_index/_doc/1
{
"first_name": "Peter",
"last_name": "Smith"
}
PUT my_index/_doc/2
{
"first_name": "Smith",
"last_name": "Williams"
}
PUT my_index/_doc/3
{
"first_name": "Jack",
"last_name": "Ma"
}
PUT my_index/_doc/4
{
"first_name": "Robbin",
"last_name": "Li"
}
PUT my_index/_doc/5
{
"first_name": "Tonny",
"last_name": "Peter Smith"
}
GET my_index/_search
{
"query": {
"match": {
"full_name": "Peter Smith"
}
}
}
# 结果
# 1. doc2(score=0.69): first_name匹配Smith,last_name无匹配
# 2. doc5(score=0.58): first_name不匹配,author_last_name匹配Peter和Smith
# 3. doc1(score=0.58): first_name匹配Peter,last_name匹配Smith
# 这个结果也是不符合预期的,这与ES计算分数的算法有关,影响分数的因素是很多的
GET my_index/_search
{
"query": {
"match": {
"full_name": {
"query": "Peter Smith",
"operator": "and"
}
}
}
}
# 结果
# 1. doc5(score=0.58): first_name不匹配,author_last_name匹配Peter和Smith
# 2. doc1(score=0.58): first_name匹配Peter,last_name匹配Smith
解决办法二:使用cross-fields的原生语法
most-fields,只需要任意一个字段中出现一个关键词即可
GET my_index/_search
{
"query": {
"multi_match": {
"query": "Peter Smith",
"type": "cross_fields",
"operator": "and",
"fields": ["first_name", "last_name"]
}
}
}
# 结果
[
{
"_index" : "my_index",
"_type" : "_doc",
"_id" : "5",
"_score" : 0.5753642,
"_source" : {
"first_name" : "Tonny",
"last_name" : "Peter Smith"
}
},
{
"_index" : "my_index",
"_type" : "_doc",
"_id" : "1",
"_score" : 0.5753642,
"_source" : {
"first_name" : "Peter",
"last_name" : "Smith"
}
}
]
# type=cross_fields的原理
# 要求Peter必须在first_name或last_name中出现
# 要求Smith必须在first_name或last_name中出现
# doc2(first_name=Smith,last_name=Williams)就无法满足条件了
# 而原来的most-fields,只需要任意一个字段中出现一个关键词即可