1、实战问题
- 场景:电商创业公司(非传统巨头)
- 读者描述需求:
content是一个text类型,用的 ik_max_word 分的词,需要根据关键词做精准匹配,并且按照发布时间倒序。
比如我搜:小米6s,搜出来的结果要精确匹配到:小米6s,并且按照用户的发布时间倒序排序。
现在的问题是用 match_pharse 搜索的时候,有时候文档里明明有这个关键词,但是搜不出来,尝试了好几种手段......
熟悉咱们公众号推文的同学应该知道,咱们分别在 2018年、2020年、2022年 都做过多次类似问题的讨论。
探究 | 明明存在,怎么搜索不出来呢?
Elasticsearch能检索出来,但不能正确高亮怎么办?
由 Elasticsearch 空间换时间的线上问题说开去......
2、重新梳理一下检索认知
2.1 分词和词典的本质
数据索引化的过程是借助分词器完成的,如读者的分词器是 IK 中文分词器。
问题来了?IK 中文分词器能包含全部的词汇吗?
大家看 medcl 大佬开源的 IK 分词器的源码中能找到 main.dic 大小是 2.92 MB。并且这个词典 8 年+ 没有更新过。
图片
显然:IK 默认词典覆盖不了全部词汇,尤其诸如“显眼包”、“小米14”、“奥利给”、“叶氏那拉”等的新词。如下截图是我自定义的词典的词库检索截图。
图片
再来一波举例看看:
PUT my_index_0512
{
"mappings": {
"properties": {
"title": {
"type": "text",
"analyzer": "ik_smart",
"fields": {
"keyword": {
"type": "keyword"
}
}
}
}
}
}
POST my_index_0512/_bulk
{"index":{"_id":1}}
{"title":"奥利给是一个网络流行词,第一次出现在一名快手主播直播时说的正能量语录里。"}
## 分词为:“奥利” 和 “给” 两个词
POST my_index_0512/_analyze
{
"text":"奥利给是一个网络流行词,第一次出现在一名快手主播直播时说的正能量语录里。",
"analyzer":"ik_smart"
}
## 检索不能召回结果,这里用 term 主要说明问题,合理性待商榷!
POST my_index_0512/_search
{
"profile": true,
"query": {
"term": {
"title": "奥利给"
}
}
}
图片
结论:词典决定分词,词典里没有的词,极大可能(有一定概率,比如:match_phrase 词+词组合的情况)检索会检索不到。
ps: 关于 term、match、match_phrase 区别等,推荐阅读:检索选型。
2.2 全文检索的本质
全文检索的本质是查询待检索的关键词在写入所创建的索引中是否存在的过程。
存在,则召回;不存在,则返回空。
2.3 明明有这个关键词,但是搜不出来的本质
表面上可以看出,之前咱们2018年、2020年、2022年讨论的方案用 match、match_phrase、match_phrase_prefix 等再结合 slop,貌似能解决一些问题,好像有些不召回的情况,可以召回了。
但,依然治标不治本。依然会存在一些“新词”、“词典里没有的词”等看似明明一段话里存在的词,就是检索不到的原因。
3、能不能根治呢?
答案:不完全能!
但,可以尝试空间换时间,借助 Ngram 能解决 99% 以上场景的问题。
针对读者的问题,借助 Ngram 分词实操一下:
### 3.1 创建索引
DELETE new_spy_uat2
PUT new_spy_uat2
{
"settings": {
"index.max_ngram_diff": 10,
"analysis": {
"analyzer": {
"my_analyzer": {
"tokenizer": "my_tokenizer",
"char_filter": ["my_char_filter"]
}
},
"char_filter": {
"my_char_filter": {
"type": "pattern_replace",
"pattern": "[^\\p{L}\\p{N}\\s]+",
"replacement": ""
}
},
"tokenizer": {
"my_tokenizer": {
"type": "ngram",
"min_gram": 2,
"max_gram": 10
}
}
}
},
"mappings": {
"properties": {
"content": {
"type": "text",
"analyzer": "my_analyzer",
"fields": {
"keyword": {
"type": "keyword"
}
}
}
}
}
}
在提供的 Elasticsearch 配置中,my_tokenizer 是一个基于 n-gram 的分词器,配置了从最小 2 个字符到最大 10 个字符的 n-gram。
在《一本书讲透 Elasticsearch》第6.4 章节 P111 解读了自定义分词器的三大核心组成:
- character filter
- tokenizer
- token filter
咱们上面的“my_char_filter”定义了文本在分词前进行预处理的字符过滤规则。实际是使用正则表达式删除所有非字母、非数字、非空格字符,只保留字母、数字和空白字符,中文字符是可以保留的。
N-gram 是一种分词方法,通过从文本中提取 n 个连续字符的滑动窗口来创建词元(tokens)。这种方法在处理需要部分匹配和模糊搜索的应用中非常有用,比如搜索建议和拼写错误的容错处理。
在这种配置下,文本会被分解成所有可能的 2 到 10 个字符的组合。
例如,要执行如下检索:
POST new_spy_uat2/_analyze
{
"analyzer":"my_analyzer",
"text":"奥利给这几年才流行"
}
分词结果如下:
图片
这种方法可以大大增加索引的大小因为每个词都被分解成多个子词,但同时也提高了搜索的灵活性和准确性,尤其是在搜索短文本或关键词片段时。
这样的分词器尤其适合于搜索引擎的自动补全功能和处理用户可能的输入错误,因为它能够在用户输入部分信息时就开始匹配相关的词条。
3.2 导入数据
POST new_spy_uat2/_bulk
{ "index" : { "_index" : "new_spy_uat2", "_id" : "1" } }
{ "content" : "新品豪车❗️限1000单食物链巴氏小仙包犬湿粮360g💰16.9,折💰8.4/袋人食级鲜肉泥,健体增肌首选(gkqHWGV0r4J)/ AC01" ,"createTime":1715323447000}
{ "index" : { "_index" : "new_spy_uat2", "_id" : "2" } }
{ "content" : "⚠️抢‼️婴儿包单💰12.3起‼️⚠️抢‼️婴儿包单💰12.3起‼️🔹史D🔥爱贝迪拉包单🥬忦啦‼️小跑快冲,错过大腿拍青" ,"createTime":1714323447000}
{ "index" : { "_index" : "new_spy_uat2", "_id" : "3" } }
{ "content" : "盛夏光年gala青春版34/包‼单拍好價❗💰34/包‼单拍好價❗34/包‼单拍好價❗💌M-3XL碼 纸裤&拉裤⚠无卷 88vip叠+3富代" ,"createTime":1716323447000}
{ "index" : { "_index" : "new_spy_uat2", "_id" : "3" } }
{ "content" : "小米6s真便宜❗34/包‼单拍好價❗💌M-3XL碼 纸裤&拉裤⚠无卷 88vip叠+3富代" ,"createTime":1716323447000}
3.3 验证分词
POST new_spy_uat2/_analyze
{
"analyzer":"my_analyzer",
"text":"小米6s真便宜❗34/包‼单拍好價❗💌M-3XL碼 纸裤&拉裤⚠无卷 88vip叠+3富代"
}
图片
3.4 执行检索
POST new_spy_uat2/_search
{
"query": {
"match": {
"content": "小米6s"
}
}
}
如上检索必然能找回结果。
图片
3.5 结合业务需求再完善检索语句
POST new_spy_uat2/_search
{
"query": {
"match": {
"content": "小米6s"
}
},
"sort": [
"_score", // 先按相关性得分排序
{
"createTime": {
"order": "desc" // 再按发布时间倒序排序
}
}
]
}
此处需要结合业务情况具体问题具体分析。
4、小结
本文是基于实战问题的思路探讨。要说明的是:Ngram 的本质是空间换时间,可能会导致写入的延时以及存储的成倍增长,选型一定要慎重。
只有高精准检索要求的场景才考虑 Ngram 分词。
普通业务场景实际是容许一些数据明明存在但就是无法召回的情况的,现在主流搜索引擎Google、Bing、baidu等都肯定无法召回全部数据的。
所以,还得结合业务场景进行探讨。