1. 为什么需要分词
分词根据语言环境的不同可以分为英文分词、中文分词等;根据分词实现的不同又分为标准分词器、空格分词器、停用词分词器等。在传统的分词器不能解决特定业务场景的问题时,往往需要自定义分词器。
英语单词相对容易辨认和区分,因为单词之间都会以空格或者标点隔开。而中文在单词、句子甚至段落之间没有空格,并且语义更加难以区分。
中文分词是自然语言处理的基础。搜索引擎之所以需要进行中文分词,主要有如下3个维度的原因。
- 语义维度:单字很多时候表达不了语义,而词往往能表达。分词相当于预处理,能使后面和语义有关的分析更准确。
- 存储维度:如果所有文章按照单字来索引,所需要的存储空间和搜索计算时间就要多得多。
- 时间维度:通过倒排索引,我们能以O(1)的时间复杂度,通过词组找到对应的文章。
设计索引的Mapping阶段,要根据业务用途确定是否需要分词。如果不需要分词,则建议设置keyword类型;如果需要分词,则建议设置为text类型并指定分词器。
2. 分词发生阶段
写入数据阶段
分词发生在数据写入(索引化)阶段,如下。
执行检索阶段
在执行“图书馆”检索时,es会根据倒排索引查找所有包含“图书馆”的文档。
3. 分词器的组成
文档被写入并转换为倒排索引之前,es对文档的操作称为分析。而分析是基于es内置分词器(analyzer)或者自定义分词器实现的。分词器由如下三部分组成
3.1 字符过滤
字符过滤器(character filter)将原始文本作为字符流接收,并通过添加、删除或更改字符来转换字符流。字符过滤器分类如下
- HTML Strip Character Filter:用于删除HTML元素,如删除<b>标签;解码HTML实体,如将&转义为&。
- Mapping Character Filter:用于替换指定的字符。
- 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文本分割算法进行工作,适用于大多数语言。
中文分词器:
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":{}——对应分词器,包含上述三者。
将问题拆解
- 实际检索中名字不带",",即逗号需要通过字符过滤。 方案:在char_filter阶段过滤
- 基于什么进行分词?方案:在Li,LeiLei;Han,MeiMei;的构成中,只能采用基于“;”的分词方式。
- 支持姓名颠倒后的查询,即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"
}
}
}
}
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"
}
}
}