es分词器:ik和standard结合使用


写在前面

前面我们介绍了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



文章作者: Alex
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Alex !
  目录