elasticsearch 自定义分词器

1,305 阅读4分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第13天,点击查看活动详情

前言

承接上次的elasticsearch 搭配canal 处理数组字段。其实在刚开始的时候由于粗心,在canal的配置文件中并没有开启objField字段中的arrays配置,导致我原先期望的映射为数组的字段被映射成功字符串字段。于是引发了一个问题。例如我们在elasticsearch中有个商品的对象,它可以对应上多个标签,我们需要对一个商品的文档存储它所关联的标签id、中文标签、英文标签。由于某种原因这些本该作为数组存储的字段,被压缩成了用标点(例如分号)拼接的字符串。

{
 "labels": "1;2;3",
 "labels_cn": "历史;中国;精品课",
 "labels_en": "history;china;class quality"
 }
{
  "label_cn_names": {
       "type": "text"
   },
  "label_en_names": {
       "type": "text"
   },
  "labels": {
       "type": "text"
   }
}

上面是文档存储的片段,可以发现1、存储是是字符串2、字段类型是text。

text在搜索时会被分词,我们的需求是比如我以标签id或者中文标签或英文标签去搜索。

31 =》 获取绑定31号标签id的对象

中国 =》 获取绑定中文标签是中国的对象

China =》 获取绑定英文标签是China的对象。

我的思考是如果使用匹配查询类似模糊的话,如果标签中容易出现误命中,比如英文下我搜索bag,而标签是“shcool and bag”也会被返回;中文下我搜索国,而标签是“中国”也会被返回。因为text类型在搜索时会被分析分词,而默认的标准分词器或者ik分词器,会在分词后出现误操作。对于这种被分割的字符串,同时我们又想完全匹配“数组”中每个单独的值。那么是不是应该从分词上着手,比如按照分隔符去分词,那每个分词即为数组的每个值,那么即使搜索时是通过分析器分词了,也可以完全匹配上每个分词的值。

自定义分词

首先我们简单的说明一下在elasticsearch中分析器由3部分组成: 1、0个或多个字符过滤器 2、一个分词器 3、0个或多个分词过滤器 我们要做的就是创建一个分词器,官方提供了一个模式分词器的案例。模式分词指的是通过正则将文本进行分隔。

PUT my-index-000001
{
  "settings": {
    "analysis": {
      "analyzer": {
        "my_analyzer": {
          "tokenizer": "my_tokenizer"
        }
      },
      "tokenizer": {
        "my_tokenizer": {
          "type": "pattern",
          "pattern": ","
        }
      }
    }
  }
}

POST my-index-000001/_analyze
{
  "analyzer": "my_analyzer",
  "text": "comma,separated,values"
}

例如这是一个逗号的分词器。所有文本会按照逗号分隔。我们给我们的索引创建一个分号的分词器。 下面来看下比较,首先是没有用自定义分词器的

{
    "query":{
        "match":{
            "labels_cn": "课"
        }
    }
}
--------------------
 "hits": [
            {
                "_index": "book",
                "_type": "_doc",
                "_id": "HjPUJ4ABRHGvpe9lz-Be",
                "_score": 0.2876821,
                "_source": {
                    "book_name": "畅销书",
                    "labels": "1;2;3",
                    "labels_cn": "历史;中国;精品课",
                    "labels_en": "history;china;class quality"
                }
            }
        ]

可以看到有结果返回,可以理论上只有搜索精品课才能返回。这是由于labels_cn被分词了,例如你是用了ik,那么很有可能分词为精品、课。因为它会按照一个常见的汉语分词来进行划分,所以就会被误匹配。因为elasticsearch是基于倒排索引来进行查询的,而倒排索引在elasticsearch中是建立在分词机制下实现的,所以你的分词只要被匹配则会返回。

下面我们增加自定义分词器,然后再进行同样的查询

{
    "query":{
        "match":{
            "labels_cn": "课"
        }
    }
}
------------
"hits": {
        "total": {
            "value": 0,
            "relation": "eq"
        },
        "max_score": null,
        "hits": []
    }

很神奇,没有返回。为了方便理解,我们测试下分词器的作用,使用_analyze API,这个方法是用于进行字符串内容的分析的,可以指定我们的分析器,那么内容就会按照我们的分析器进行分析,我们通过查看结果判断是否符合我们的预期。

{
    "analyzer": "semicolon_analyzer",
    "text": "历史;中国;精品课"
}
{
    "tokens": [
        {
            "token": "历史",
            "start_offset": 0,
            "end_offset": 2,
            "type": "word",
            "position": 0
        },
        {
            "token": "中国",
            "start_offset": 3,
            "end_offset": 5,
            "type": "word",
            "position": 1
        },
        {
            "token": "精品课",
            "start_offset": 6,
            "end_offset": 9,
            "type": "word",
            "position": 2
        }
    ]
}

看到文本只会按照分号进行分词。所以也只可能被完整的标签值匹配。下面输入完整的标签来搜索

{
    "query":{
        "match":{
            "labels_cn": "精品课"
        }
    }
}
----------
 "hits": [
            {
                "_index": "book1",
                "_type": "_doc",
                "_id": "HzPcJ4ABRHGvpe9lHeCW",
                "_score": 0.18232156,
                "_source": {
                    "book_name": "畅销书",
                    "labels": "1;2;3",
                    "labels_cn": "历史;中国;精品课",
                    "labels_en": "history;china;class quality"
                }
            }
            ]

可以看到符合我们的预期了。labels_cn字段内有字符被匹配就可以被查询出来。