写在前面
前面我们介绍了es分词器之ik,发现有一个问题。ik_max_word分词之后,如果只搜索部分单词,那就无法匹配上了。这个时候可以结合standard分词器使用。
示例
原文:我喜欢一家四口的生活
ik_max_word分词结果
分词 | 位置 |
---|---|
我 | 0 |
喜欢 | 1 |
一家 | 2 |
一 | 3 |
家 | 4 |
四口 | 5 |
四 | 6 |
口 | 7 |
检索词: 一家四口
分词 | 位置 |
---|---|
一家 | 0 |
四口 | 1 |
从上面可以看出,查找时ik_smart将语句分为了 一家 和 四口 两个词,位置分别为 0 和 1 ,而ik_max_word建索引时,一家 和 四口 的位置分别是 2 和 5,一个位置相邻,一个位置不相邻,在match_phrase看来,这种是不匹配的,所以用ik_smart分词短语时无法查到或者查全数据。
阅读了es分词器之ik文章的读者可能会发现,为了处理这个问题,使用了模糊匹配,但是模糊匹配的结果是整个匹配到的单词是高亮的。例如搜索”秦”,通过模糊匹配可以搜索出”秦始皇“,但是高亮是整个”秦始皇”部分,实际需求应该只是”秦”字高亮。对于这个问题,作者使用handle函数自己处理了一下。如果使用standard分词器,就比较容易解决这个问题了。
standard分词器大家都比较熟,针对于汉字就是一个一个分,这种肯定是可以查全的。但一个一个字分的话,每个字对应的文档集合非常多,如果数据量达到了百亿,在求交集,计算距离时,效果非常差。而这里我们可以将其跟ik分词器配合使用,既利用ik分词器的优势,又可以利用standard分词器进行兜底。
- 如果不使用standard分词器,对于搜索“一家四口”,结果是空。
- 使用standard分词器,搜索“一家四口”,结果如下
我喜欢<span style='color: red;'>一</span><span style='color: red;'>家</span><span style='color: red;'>四</span><span style='color: red;'>口</span>的生活
显然,这里hit到了standard,因为这四个字是一个一个匹配的。
但是,此时还有一个问题,就是standard分词只是针对单个字的分词,ik_max_word分出了很多词,如果有“好人”,“好人的”这种分词结果,那么搜索“好人”,match_phrase仅能够搜索出“好人”对应的结果,同样符合预期的“好人的”的结果是检索不到的。
针对这种情况,我们利用match_phrase_prefix代替match_phrase,match_phrase_prefix 与 match_phrase查询类似,但是会对最后一个Token在倒排序索引列表中进行通配符搜索。即match_phrase_prefix将会找到“好人”,“好人的”对应的结果。
完整代码
// 高亮搜索
func (e *es) HighlightSearch(ctx context.Context, index, keyword string, size int, sort string, ascending bool) (any, error) {
boolQ := elasticsearch7.NewBoolQuery()
boolZ := elasticsearch7.NewBoolQuery()
// 定义highlight
highlight := elasticsearch7.NewHighlight()
// 指定需要高亮的字段。这里写了2个字段:name和name.keyword,其中name用于分词匹配,name.keyword用于完全匹配
highlight = highlight.Fields(elasticsearch7.NewHighlighterField("name"), elasticsearch7.NewHighlighterField("name.keyword"), elasticsearch7.NewHighlighterField("name.standard"))
// 指定高亮的返回逻辑 <span style='color: red;'>...msg...</span>
highlight = highlight.PreTags(e.preTag).PostTags(e.postTag)
// 分词匹配字段
nameQuery := elasticsearch7.NewWildcardQuery("name", keyword)
// 精确匹配字段
nameKeywordQuery := elasticsearch7.NewTermsQuery("name.keyword", keyword)
// 模糊匹配。TODO:去掉模糊匹配
//nameQuery1 := elasticsearch7.NewWildcardQuery("name.keyword", "*"+keyword+"*")
nameQuery2 := elasticsearch7.NewMatchPhrasePrefixQuery("name.standard", keyword)
boolZ.Filter(boolQ.Should(nameQuery, nameKeywordQuery, nameQuery2))
res, err := e.client.Search(index).Highlight(highlight).Query(boolZ).Sort(sort, ascending).Size(size).Do(ctx)
if err != nil {
return nil, errors.Wrapf(err, "es HighlightSearch. keyword:%s", keyword)
}
// 高亮的输出和doc的输出不一样,这里要注意,我只输出了匹配到高亮的第一个词
result := make([]any, 0)
for _, highliter := range res.Hits.Hits {
tmp := make(map[string]any)
if err := json.Unmarshal(highliter.Source, &tmp); err != nil {
return nil, errors.Wrapf(err, "es HighlightSearch Unmarshal. %v", highliter.Source)
}
tmp1 := make(map[string]any)
// 这里的key可能是name和name.keyword,可以忽略key,只取value的第一个元素
if values, ok := highliter.Highlight["name"]; ok {
tmp1["matchValue"] = values[0]
} else if values, ok = highliter.Highlight["name.keyword"]; ok {
// 模糊匹配已经去掉
//tmp1["matchValue"] = e.handle(values[0], keyword)
tmp1["matchValue"] = values[0]
} else if values, ok = highliter.Highlight["name.standard"]; ok {
tmp1["matchValue"] = values[0]
}
tmp1["id"] = highliter.Id
tmp1["originValue"] = tmp["name"]
result = append(result, tmp1)
}
return result, nil
}
修改mappings
对于使用standard分词器,要修改mappings中的name字段
{
"settings": {
"number_of_shards": 1,
"number_of_replicas": 0
},
"mappings": {
"properties": {
"name":{
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart",
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
},
"standard":{
"type": "text",
"analyzer":"standard"
}
}
},
"created_at":{
"type": "date"
},
"updated_at":{
"type": "date"
},
"count":{
"type": "integer"
}
}
}
}
效果图
搜索:一家四口
搜索:的生
显然这两个搜索hit的是standard分词器。
参考
[1] elasticsearch 精确匹配搜索(高准确度)
[2] es分词器之ik