思考——Elasticsearch 8.X 如何实现更精准的检索?
1、线上问题
这个问题涉及到业务细节,至今没有定论。不过,该问题引发了我的思考。
2、我的一点思考
我们使用 Elasticsearch 到底用来做什么?
除了 Elasticsearch 早已不是10年前因“菜谱”而火出技术圈的搜索引擎组件,它早已不是“单兵作战”,而是 ELKB 形成的 Elastic Stack “行走江湖”。
但,至少技术选型涉及到大数据的检索几乎无一例外 Elasticsearch 都是“首发阵容”。
2.1 关于全文检索,用户更关注什么?
关于全文搜索,《这就是搜索引擎》张俊林博士从搜索引擎的角度阐述了用户的关注点,核心就是两个。
- 其一:精准率;
- 其二:召回率。
通俗点讲:
- 精准率是站在用户角度,召回的数据贴合用户的预期,越准确越好。
当然,大数据时代的今天,单纯的字、词匹配早已跟不上时代的步伐,基于用户行为的推荐(如:抖音、网易云音乐)往往更吊起用户的胃口。
- 而召回率是满足检索条件的语句都尽可能的召回,到底要什么,让用户在结果中二次再做选择。
这两种都有应用场景,无所谓谁对谁错。
提到 Elasticsearch 精准召回数据,先不谈“精准”,先说一下召回。
如下图所示,可以分两部分看:数据的写入过程、数据检索过程。
数据写入过程要比图中复杂,我们着重关注建立倒排索引的过程,因为后面我们要基于倒排索引做全文检索。
2.2 数据写入过程
写入的文本如下:
基于 ik_smart 分词后,倒排索引的中的分词词典如下所示:
2.3 数据检索过程
Elasticsearch 提到检索,这其实是一个大的概念,不信你看下面的脑图。
Elasticsearch 检索从大的角度可以看成黑盒,类似:Google、baidu 搜索框。用户搜索框输入内容,检索召回数据。
但是,技术人员的眼里看搜索,更关注用哪种类型搜索。这涉及到检索分类。
Elasticsearch 检索大致可以分为:全文检索、精准匹配检索、多表关联检索、组合检索、不常用检索。
精准匹配检索回答的是“是和否、存在或不存在”问题?比如:小明的手机号为:“13566668888”,多一位、少一位、错一位都不能被召回!完全满足检索条件就召回,不满足检索条件就不要召回。不存在中间情况。
而全文检索回答的是“相关度”的问题?如文章开头提到的“手表”、“手表带”、“表带”就有相关度,哪些数据该召回?谁优先被召回(也就是谁排在前面)。
match_phrase 和 match 等实现检索的机制是不一样的,“profile:true" API 能帮我们更精细的看到底层的实现。
简单点说:match_phrase 走的是短语检索匹配,而 match 走的是多字段拆解后的 term query 的 bool 语句组合体。
2.4 如何理解精准?
其实这个没有普适的标准,不同的业务系统是不一样的。
建议,结合业务需求、产品经理和技术经理、项目经理、核心技术人员共同敲定。
满足用户要求的“精准”才算是精准。
比如文章开头提到的“手表”,其实有多种理解?
- 其一:只有“手表”两个字,没有任何其他,这种叫精准。
- 其二:分词词典里有“手表”,就要召回,这种也叫精准。
- 其三:只要文本里有就要召回,这也是某种意义的精准。比如:“南京市长江大桥”,搜索“江大桥”也要求召回。
等等......不一而足。
有了上面的思考,我们尝试解读一下开篇的问题。
3、Elasticsearch 8.X 更精准检索实现
如下示例,拿数据说话!
3.1 字词混合索引检索方案
- 词索引——使用 ik_smart 分词。
- 字索引——使用 standard 标准分词。
保留了 keyword 类型,便于方案二的精准字符匹配。
DELETE test-20220928
PUT test-20220928
{
"mappings": {
"properties": {
"title": {
"type": "text",
"analyzer": "ik_smart",
"fields": {
"standard": {
"type": "text",
"analyzer": "standard"
},
"keyword": {
"type": "keyword"
}
}
}
}
}
}
POST test-20220928/_bulk
{"index":{"_id":1}}
{"title":"手表带真好看"}
{"index":{"_id":2}}
{"title":"手表最近卖的不好,咋整"}
{"index":{"_id":3}}
{"title":"卡西欧手表不错哦"}
{"index":{"_id":4}}
{"title":"手表"}
先看“手表带真好看”这个文档 ik_smart 的分词结果。
其他几个文档{“2”,“3”,“4”} 都包含手表的分词,大家可以自己验证,篇幅原因,没有截图。
如下检索是 bool 组合混合体。
对于:must 条件要求单字相连的多字(可以理解为短语,但不见得是有意义的短语,如:江大桥)必须满足,用 短语 match_phrase 进行检索。
对于:should 条件满足 ik_smart 分词存在结果,则召回数据,且极大的提升评分权重。
POST test-20220928/_search
{
"query": {
"bool": {
"should": [
{
"match_phrase": {
"title": {
"query": "手表",
"boost": 50
}
}
}
],
"must": [
{
"match": {
"title.standard": "手表"
}
}
]
}
}
}
明显看出来:包含手表要排在前面。
3.2 自定义评分实现精准检索
使用前提:针对是 keyword 类型。
大家记住:sort 排序、aggregation 聚合、script 脚本都只能针对 keyword 类型,text 类型都是不支持的,除非开启“fielddata”(必要性非常小,使用场景也小,不建议开启)。
如下脚本的含义,如果字段精准匹配,没有多余字符,则评分极高,设置为1000;如果字段以给定关键词开头,则评分高,设置为500;如果属于包含关系,则评分也较高,设置为100;如果没有包含,那评分为10。
POST test-20220928/_search
{
"query": {
"script_score": {
"query": {
"match_all": {}
},
"script": {
"source": "if(doc['title.keyword'].value == params.keyword) { return 1000; } else if(doc['title.keyword'].value.startsWith(params.keyword)){ return 500; } else if(doc['title.keyword'].value.contains(params.keyword)) { return 100; } else { return 10;}",
"params": {
"keyword": "手表"
}
}
}
}
}
相当于我们人工干预了评分,基于字段精准匹配的情况实现了评分的区分。这样,最先召回的结果数据就是我们最期望的精准匹配结果了。
4、小结
针对企业级实战问题,引发了思考,并根据思考尝试做了解答。
当然,这道业务题目会有具体的细节业务场景,还需要进一步沟通交流。
本文涉及的技术点都不复杂。包含如下:
- 分词(中文分词器、默认分词器)
- 组合分词(fields)
- 组合检索
- 排序(评分)+ 全文检索+召回
- 自定义评分(自己定义的规则来进行数据的评分,进而将评分高的优先返回,排在前面进行返回!)
仅就本文的讨论和实现,相信你也遇到过类似问题,欢迎留言交流下你的思考!
文章转载自公众号:铭毅天下Elasticsearch