5. 分词

284 阅读4分钟

1. 为什么需要分词

分词根据语言环境的不同可以分为英文分词、中文分词等;根据分词实现的不同又分为标准分词器、空格分词器、停用词分词器等。在传统的分词器不能解决特定业务场景的问题时,往往需要自定义分词器。

英语单词相对容易辨认和区分,因为单词之间都会以空格或者标点隔开。而中文在单词、句子甚至段落之间没有空格,并且语义更加难以区分。

中文分词是自然语言处理的基础。搜索引擎之所以需要进行中文分词,主要有如下3个维度的原因。

  • 语义维度:单字很多时候表达不了语义,而词往往能表达。分词相当于预处理,能使后面和语义有关的分析更准确。
  • 存储维度:如果所有文章按照单字来索引,所需要的存储空间和搜索计算时间就要多得多。
  • 时间维度:通过倒排索引,我们能以O(1)的时间复杂度,通过词组找到对应的文章。

设计索引的Mapping阶段,要根据业务用途确定是否需要分词。如果不需要分词,则建议设置keyword类型;如果需要分词,则建议设置为text类型并指定分词器。

2. 分词发生阶段

写入数据阶段

分词发生在数据写入(索引化)阶段,如下。

image.png

执行检索阶段

在执行“图书馆”检索时,es会根据倒排索引查找所有包含“图书馆”的文档。

3. 分词器的组成

文档被写入并转换为倒排索引之前,es对文档的操作称为分析。而分析是基于es内置分词器(analyzer)或者自定义分词器实现的。分词器由如下三部分组成

image.png

3.1 字符过滤

字符过滤器(character filter)将原始文本作为字符流接收,并通过添加、删除或更改字符来转换字符流。字符过滤器分类如下

  1. HTML Strip Character Filter:用于删除HTML元素,如删除<b>标签;解码HTML实体,如将&转义为&。
  2. Mapping Character Filter:用于替换指定的字符。
  3. Pattern Replace Character Filter:可以基于正则表达式替换指定的字符。

3.2 文本切分为分词

若进行了字符过滤,则系统将接收过滤后的字符流;若未进行过滤,则系统接收原始字符流。

在接收字符流后,系统将对其进行分词,并记录分词后的顺序或位置(position)、起始值(start_offset)以及偏移量(end_offset-start_offset)。而tokenizer负责初步文本分词。

tokenizer分类如下

  • Standard Tokenizer(标准分词器)
  • Letter Tokenizer(字母分词器)
  • Lowercase Tokenizer(小写转化分词器)

3.3 分词后再过滤

在对tokenizer处理后的字符流进行进一步处理时,例如进行转换为小写、删除(去除停用词)和新增(添加同义词)等操作

4. 分词器分类

默认分词器:standard分词器。将词汇单元转换成小写,并去除停用词和标点符号。基于Unicode文本分割算法进行工作,适用于大多数语言。

image.png

中文分词器: image.png

5. 特殊场景自定义分词器案例

业务需求:有一个作者字段,比如Li,LeiLei;Han,MeiMei以及LeiLei Li……现在要对其进行精确匹配。

如果通过分号分词,检索Li,LeiLei,那么LeiLei Li就不能被搜索到,而我们希望LeiLei Li也被搜索到。

5.1 问题拆解

首先来看自定义分词器在映射的Settings部分中的设置。

PUT lwy_index
{
  "settings": {
    "analysis": {
      "char_filter": {},
      "tokenizer": {},
      "filter": {},
      "analyzer": {}
    }
  }
}
  • "char_filter":{},——对应字符过滤部分。
  • "tokenizer":{},——对应文本切分为分词部分。
  • "filter":{},——对应分词后再过滤部分。
  • "analyzer":{}——对应分词器,包含上述三者。

将问题拆解

  1. 实际检索中名字不带",",即逗号需要通过字符过滤。 方案:在char_filter阶段过滤
  2. 基于什么进行分词?方案:在Li,LeiLei;Han,MeiMei;的构成中,只能采用基于“;”的分词方式。
  3. 支持姓名颠倒后的查询,即LeileiLi也能被检索到。方案:需要结合同义词实现。在分词后的过滤阶段,将LiLeiLei和LeiLeiLi设定为同义词。

5.2 实现方案

PUT lwy_index
{
  "settings": {
    "analysis": {
      "char_filter": {
         "my_char_filter": {
          "type": "mapping",
          "mappings": [
            " , => "
            ]
        }
      },
      "filter": {
        "my_synonym_filter": {
          "type":"synonym",
          "expand": true,
          "synonyms": [
            "leileili => lileilei",
            "meimeihan => hanmeimei"
            ]
        }
      },
      "tokenizer": {
        "my_tokenizer": {
          "type":"pattern",
          "pattern": """\;"""
        }
      }, 
      "analyzer": {
        "my_analyzer": {
          "tokenizer": "my_tokenizer",
          "char_filter": "my_char_filter",
          "filter": [
            "lowercase",
            "my_synonym_filter"
            ]
        }
      }
    }
  },
    "mappings": {
        "properties":{
          "name":{
            "type":"text",
            "analyzer":"my_analyzer"
          }
        }
      }
}

image.png

5.3 结果验证

借助analyzer API验证分词结果是否正确。

analyzer API的用途如下。

  • 实际业务场景中,用以检验分词的正确性。
  • 排查检索结果和预期的一致性与否
# 直接验证分词结果
POST lwy_index/_analyze
{
  "analyzer": "my_analyzer",
  "text": "li,LeiLei;Han,MeiMei"
}
# 基于索引字段验证分词结果
POST lwy_index/_analyze
{
  "field": "name",
  "text": "li,LeiLei;Han,MeiMei"
}

写入数据再来看看检索结果

POST lwy_index/_bulk
{"index":{"_id":1}}
{"name":"Li,LeiLei;MeiMei,Han"}
{"index":{"_id":2}}
{"name":"LeiLei,Li;Han,MeiMei"}

POST lwy_index/_search
{
  "query": {
    "match_phrase": {
      "name": "lileilei"
    }
  }
}