ElasticSearch 之 文本搜索
1. 文本搜索简介
- 作为一款搜索引擎框架,文本搜索是其核心功能。
- ES在文本索引的建立和搜索过程中依赖两大组件,即Lucene和分析器。
- Lucene负责进行倒排索引的物理构建,分析器负责在建立倒排索引前和搜索前对文本进行分词和语法处理。
2. 倒排索引建立过程
为了完成对文本的快速搜索,ES使用了一种称为“倒排索引”的数据结构。倒排索引中的所有词语存储在词典中,每个词语又指向包含它的文档信息列表。
假设需要对下面两个酒店的信息进行倒排索引的创建:文档ID为001,酒店名称为“金都嘉怡假日酒店”;文档ID为002,酒店名称为“金都欣欣酒店”。
- 首先,ES将文档交给分析器进行处理,处理的过程包括字符过滤、分词和分词过滤,最终的处理结果是文档内容被表示为一系列关键词信息的集合。
这里的关键词信息指的是关键词本身以及它在文档中出现的位置信息和词性信息,如图所示为文档001的分析结果示意图。 - 其次,ES根据分析结果建立文档-词语矩阵,用以表示词语和文档的包含关系,本例中的文档-词语矩阵如图所示。
通过上面的文档-词语矩阵可知,ES从文档001中提取出4个词语,从文档002中提取出3个词语。 - 文档-词语矩阵建立完成之后,接着需要建立基于词语的倒排索引。
- ES会遍历文档词语矩阵中的每一个词语,然后将包含该词语的文档信息与该词语建立一种映射关系。
- 映射关系中的词语集合叫作Term Dictionary,即“词典”。
- 映射中的文档集合信息不仅包含文档ID,还包含词语在文档中的位置和词频信息,包含这些文档信息的结构叫作Posting List。
- 对于一个规模很大的文档集合来说,可能包含几十万甚至上百万的词语集合,能否快速定位某个词语,直接影响搜索时的响应速度。因此需要一种高效的数据结构对映射关系中的词语集合进行索引,这种结构叫作TermIndex。
- 上述3种结构结合在一起就构成了ES的倒排索引结构,倒排索引与三者之间的逻辑关系如图所示。
- 本例中的倒排索引结构如图所示。
3. 文本的搜索过程
在ES中,一般使用match查询对文本字段进行搜索。match查询过程一般分为如下几步:
- ES将查询的字符串传入对应的分析器中,分析器的主要作用是对查询文本进行分词,并把分词后的每个词语变换为对应的底层lucene term查询。
- ES用term查询在倒排索引中查找每个term,然后获取一组包含该term的文档集合。
- ES根据文本相关度对每个文档进行打分计算,打分完毕后,ES把文档按照相关性进行倒序排序。
- ES根据得分高低返回匹配的文档。
4. 分析器简介
- 分析器一般用在下面两个场景中:
- 创建或更新文档时(合称索引时),对相应的文本字段进行分词处理;
- 查询文本字段时,对查询语句进行分词。
- ES中的分析器有很多种,但是所有分析器的结构都遵循三段式原则,即字符过滤器、分词器和词语过滤器。
- 其中,字符过滤器可以有0个或多个,分词器必须只有一个,词语过滤器可以有0个或多个。
- 从整体上来讲,三个部分的数据流方向为字符过滤器→分词器→分词过滤器。
- 文本先以字符流的形式流经字符过滤器,字符过滤器处理完字符后将结果传递给分词器,分词器对文本进行分词处理后将结果又传递给分词过滤器。最终,分析器输出分词后每个词的信息,至此,一个分析器的处理流程结束。
- 对于不同的分析器,上述三部分的工作内容是不同的,为了正确匹配,如果在数据写入时指定了某个分析器,那么在匹配查询时也需要设定相同的分析器对查询语句进行分析。
4.1. 字符过滤器
- 字符过滤器是分析器处理文本数据的第一道工序,它接收原始的字符流,对原始字符流中的字符进行添加、删除或者转换操作,进而改变原始的字符流。
- 例如,原始数据中可能包含来自爬虫的结果,字符过滤器可以去除文本中的HTML标签,也可以将原始文本中的一些特殊字符进行转义,如把“&”转换为and。
- 总而言之,字符过滤器就是对原始文本做一些粗加工的工作,为后续的分词做准备。
- 常用的ES内置的字符过滤器:
4.2. 分词器
- 分词器在分析器中负责非常重要的一环工作——按照规则来切分词语。
- 对于英文来说,简单的分词器通常是根据空格及标点符号进行切分。
- 然而对于中文分词来说,字符之间往往没有空格,因此采用英文的切分规则是不可取的。
- 中文分词有多种切分方案,不同的分词器采用的方案不同,处理后的结果也可能不同。
- 分词器对文本进行切分后,需要保留词语与原始文本之间的对应关系,因此分词器还负责记录每个Token的位置,以及开始和结束的字符偏移量。
- 常用的ES内置的分词器:
4.3. 分词过滤器
- 分词过滤器接收分词器的处理结果,并可以将切分好的词语进行加工和修改,进而对分词结果进行规范化、统一化和优化处理。
- 例如,它可以将文本中的字母全部转换为小写形式,还可以删除停用词(如的、这、那等),还可以为某个分词增加同义词。
- 常用的ES内置的分词过滤器:
5. 分析器使用
- ES提供了分析器的调用API,使用户可以方便地对比不同分析器的分析结果。
- 另外,ES提供了一些开箱即用的内置分析器,这些分析器其实就是字符过滤器、分词器和分词过滤器的组合体,可以在索引建立时和搜索时指定使用这些分析器。
- 当然,如果这些分析器不符合需求,用户还可以自定义分析器。
5.1. 测试分析API
- 为了更好地理解分析器的运行结果,可以使用ES提供的分析API进行测试。
- 在DSL中可以直接使用参数analyzer来指定分析器的名称进行测试,分析API的请求形式如下:
POST _analyze { "analyzer": ${analyzer_name}, //指定分析器名称 "text":${analyzer_text} //待分析文本 }
- 以下示例使用standard分析器分析一段英文:
POST _analyze { "analyzer": "standard", "text": "The letter tokenizer is not configurable." }
- 使用standard分析器对中文进行分析时,由于中文没有空格,无法根据空格进行切分,因此只能按单字进行切分,并给出了每个单字的词性。在中文里,两个单字的词性和每个单字的词性是不同的,因此使用standard分析器分析中文时给出的词性不具备参考价值。不仅如此,ES内置的其他分析器也不适合分析中文。
- 除了指定分析器进行请求分析外,用户还可以指定某个索引的字段,使用这个字段对应的分析器对目标文本进行分析。下面使用酒店索引的title字段对应的分析器分析文本。
POST /hotel/_analyze { "field": "title", "text": "金都嘉怡假日酒店" }
- 另外,用户还可以在API中自定义分析器对文本进行分析。在下面的示例中自定义了一个分析器,该分析器的分词器使用standard,分词过滤器使用Lower Case,其将分词后的结果转换为小写形式。
GET _analyze { "tokenizer": "standard", "filter": [ "lowercase" ], "text": "JinDu JiaYi Holiday Hotel" }
5.2. 内置分析器
- ES已经内置了一些分析器供用户使用,在默认情况下,一个索引的字段类型为text时,该字段在索引建立时和查询时的分析器是standard。
- standard分析器是由standard分词器、LowerCase分词过滤器和Stop Token分词过滤器构成的。
- 注意,standard分析器没有字符过滤器。
- 除了standard分析器之外,ES还提供了simple分析器、language分析器、whitespace分析器及pattern分析器等。
- 另外,用户也可以自定义分析器,并且可以在索引建立或搜索时指定自定义分析器。
5.3. 索引时使用分析器
- 文本字段在索引时需要使用分析器进行分析,ES默认使用的是standard分析器。
- 如果需要指定分析器,
- 一种方式是在索引的settings参数中设置当前索引的所有文本字段的分析器:
PUT /hotel { "settings": { "analysis": { "analyzer": { //指定所有text字段索引时使用simple分析器 "default": { "type": "simple" } } } }, "mappings": { "properties": { … } } }
- 另一种方式是在索引的mappings参数中设置当前字段的分析器:
PUT /hotel { "mappings": { "properties": { "title": { "type": "text", //指定索引中的title字段索引时使用whitespace分析器 "analyzer": "whitespace" }, … } } }
- 一种方式是在索引的settings参数中设置当前索引的所有文本字段的分析器:
5.4. 搜索时使用分析器
- 为了搜索时更加协调,在默认情况下,ES对文本进行搜索时使用的分析器和索引时使用的分析器保持一致。
- 当然,用户也可以在mappings参数中指定字段在搜索时使用的分析器。如下示例展示了这种用法:
PUT /hotel { "mappings": { "properties": { "title": { "type": "text", "analyzer": "whitespace", //索引时使用whitespace分析器 "search_analyzer": "whitespace" //搜索时使用whitespace分析器 }, … } } }
- 注意,这里指定的搜索分析器和索引时的分析器是一致的,但是在大多数情况下是没有必要指定的,因为在默认情况下二者就是一致的。
- 如果指定的搜索分析器和索引时的分析器不一致,则ES在搜索时可能出现有不符合预期的匹配情况,因此该设置在使用时需要慎重选择。
5.5. 自定义分析器
- 当系统内置的分析器不满足需求时,用户可以使用自定义分析器。
- 在有些场景中,某个文本字段不是自然语言而是在某种规则下的编码。例如,在酒店索引中有个sup_env字段,其值为“APP,H5,WX”,表示当前酒店可以在App、Web端和微信小程序端上显示。假设当前搜索用户使用的是H5或App客户端,则需要过滤掉不支持在这两个客户端上显示的酒店。
- 首先,需要在索引创建的DSL中定义分析器comma_analyzer,该分析器中只有一个分词组件,该分词组件使用逗号进行词语切分;
- 然后在mappings中使用analyzer参数指定字段sup_env的分析器为定义好的comma_analyzer分析器。
6. 中文分析器
- 分词工作是搜索引擎的基础性工作,分词结果的质量高低对后面的搜索结果匹配起着非常关键的作用。
- 中文相对于英文等西方语言有独有的一些特点,ES内置的分析器一般很难适用于中文搜索,因此针对中文的ES分词器插件应运而生。
6.1. 中文分词介绍
- 对于英文来说,一个文档很容易被切分成关键词的集合,因为除了标点符号外都是由空格把各个英文单词进行分隔的。
- 例如I have a red car,用空格进行切分的结果为I/have/a/red/car。
- 对于中文来说,一般由一个或多个字组合在一起形成一个词语,并且句子中没有词的界限。
- 根据不同的使用场景,对于词语切分颗粒度的需求也是不一样的,请看如下示例。
- 例句:我来到北京清华大学。
- 分词结果1:我/来到/北京/清华/华大/大学/清华大学
- 分词结果2:我/来到/北京/清华大学
- 上面的两种分词方式都是正确的,它们可以应用在不同的场景中。
- 中文分词根据实现原理和特点,分词的切分算法主要有两种,即基于词典的分词算法和基于统计的机器学习算法。
- 基于词典的分词算法是按照某种策略将提前准备好的词典和待匹配的字符串进行匹配,当匹配到词典中的某个词时,说明该词分词成功。该算法是匹配算法中最简单、速度最快的算法,其分词算法分为3种,即正向最大化匹配法、逆向最大化匹配法和双向最大化匹配法。
- 基于统计的机器学习算法基于统计的机器学习算法的主要思想是事先构建一个语料库,该语料库中是标记好的分词形式的语料,然后统计每个词出现的频率或者词与词之间共现的频率等,基于统计结果给出某种语境下应该切分出某个词的先验概率。后续进行分词时,使用先验概率给出文本应该切分的结果。这类算法中代表的算法有HMM、CRF、深度学习等,比如结巴分词基于HMM算法、HanLP分词工具基于CRF算法等。
- 当前,中文分词的难点主要有以下三方面:
- 分词标准:不同的分词器使用的分词标准不同,分词的结果也不同。例如,在分词的颗粒度方面,对“中华人民共和国”进行切分时,粗粒度的分词就是“中华人民共和国”,细粒度的分词可能是“中华”“人民”“共和国”。
- 分词歧义:使用分词器对文本进行切分,切分后的结果和原来的字面意义不同。例如,在“郑州天和服装厂”中,“天和”是厂名,是一个专有词,“和服”也是一个词,它们共用了“和”字。如果分词器不够精准,则很容易切分成“郑州、和服、服装、服装厂”,但是原文中并没有与“和服”有关的含义,因此这里就产生了歧义。
- 新词识别:新词也称未登录词,即该词没有在词典或者训练语料中出现过。在这种情况下,分词器很难识别出该词。目前,新词识别问题的解决依赖于人们对分词技术和中文结构的进一步认识。
- ES通过安装插件的方式来支持第三方分析器。比较常用的第三方中文分析器是HanLP和IK分析器。
6.2. IK分析器
- IK分析器是一个开源的、基于Java语言开发的轻量级的中文分词工具包,它提供了多种语言的调用库。
- 在ES中,IK分析器通过第三方插件的方式来使用,其代码托管到了GitHub上,项目地址为https://github.com/medcl/elasticsearch-analysis-ik。
- IK分析器实现了词典的冷更新和热更新,用户可以选择适合自己的方式进行词典的更新。
- ES中IK分析器的安装过程
- 新建目录。进入ES的plugins目录,然后新建目录,目录名称为ik-analysis。
- 下载插件。进入ik-analysis目录,然后运行wget命令下载ES的IK分词插件,下载地址为https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.10.2/elasticsearch-analysis-ik7.10.2.zip。注意,插件版本要和当前的ES版本保持一致,否则可能会导致ES启动报错。
- 解压插件。使用unzip命令对下载的插件文件进行解压缩。
- 重启ES。重新启动ES,观察其运行日志,当打印出类似loadedplugin[analysis-ik]的日志内容时,说明IK插件安装成功。
- IK分析器提供了两个子分析器,即ik_smart和ik_max_word,另外它还提供了两个和分析器同名的子分词器。
- ik_max_word和ik_smart分析器的主要区别在于切分词语的粒度上,ik_smart的切分粒度比较粗,而ik_max_word将文本进行了最细粒度的拆分,甚至穷尽了各种可能的组合。
- 有的词没有在IK分析器的词典里,因此被切分成了两个单字,这需要为IK分析器添加词典来解决该问题。
- 在IK分析器的安装目录下的config子目录中创建文件my.dict,在其中添加“嘉怡”即可。如果有更多的词语需要添加,则每个词语单独一行。
- 添加完成后修改IK分析器的配置文件,路径为config/IKAnalyzer.cfg.xml,将新建的字典文件加入ext_dict选项中。
- 配置完成后重启ES,然后使用分析器重新分析即可。
6.3. HanLP分析器
- HanLP是由一系列模型与算法组成的Java工具包,它从中文分词开始,覆盖词性标注、命名实体识别、句法分析、文本分类等常用的NLP任务,提供了丰富的API,被广泛用于Lucene、Solr和ES等搜索平台。
- 就分词算法来说,它支持最短路分词、N-最短路分词和CRF分词等分词算法。
- 用户可以在ES中安装HanLP分析器插件进行使用。
- HanLP分析器插件的安装比较简单,在ES的安装目录下执行bin/elasticsearch-plugin install${URL}命令即可,其中的URL是HanLP的安装文件链接。需要HanLP分析器使用与ES相同的版本,安装时可根据当前的版本进行选择。
- HanLP分析器提供了众多的子分析器,如hanlp、hanlp_standard、hanlp_crf和hanlp_n_short等。
7. 使用同义词
- 在搜索场景中,同义词用来处理不同的查询词,有可能是表达相同搜索目标的场景。
- 例如,当用户的查询词为“带浴缸的酒店”和“带浴池的酒店”时,其实是想搜索有单独泡澡设施的酒店。
- 再例如,在电商搜索中,同义词更是应用广泛,如品牌同义词Adidas和“阿迪达斯”,产品同义词“投影仪”和“投影机”,修饰同义词“大码”和“大号”等。
- 用户在使用这些与同义词相关的关键词进行搜索时,搜索引擎返回的搜索结果应该是一致的。
- 用户还可以通过ES中的分析器来使用同义词,使用方式分为两种,
- 一种是在建立索引时指定同义词并构建同义词的倒排索引,
- 另一种是在搜索时指定字段的search_analyzer查询分析器使用同义词。
7.1. 建立索引时使用同义词
- 在ES内置的分词过滤器中,有一种分词过滤器叫作synonyms,它是一种支持用户自定义同义词的分词过滤器。
PUT /hotel { "settings": { "analysis": { "filter": { //定义分词过滤器 "ik_synonyms_filter": { "type": "synonym", "synonyms": [ //在分词过滤器中定义近义词 "北京,首都", "天津,天津卫", "假日,度假" ] } }, "analyzer": { //自定义分析器 "ik_analyzer_synonyms": { "tokenizer": "ik_max_word", //指定分词器 "filter": [ //指定分词过滤器 "lowercase", "ik_synonyms_filter" ] } } } }, "mappings": { "properties": { "title": { "type": "text", "analyzer": "ik_analyzer_synonyms" //指定索引时使用自定义的分析器 }, … } } }
7.2. 查询时使用同义词
- 在ES内置的分词过滤器中还有个分词过滤器叫作synonym_graph,它是一种支持查询时用户自定义同义词的分词过滤器。
PUT /hotel { "settings": { "analysis": { "filter": { //定义分词过滤器 "ik_synonyms_graph_filter": { "type": "synonym_graph", "synonyms": [ //在分词过滤器中定义近义词 "北京,首都", "天津,天津卫", "假日,度假" ] } }, "analyzer": { //自定义分析器 "ik_analyzer_synonyms_graph": { "tokenizer": "ik_max_word", //指定分词器 "filter": [ //指定分词过滤器 "lowercase", "ik_synonyms_graph_filter" ] } } } }, "mappings": { "properties": { "title": { "type": "text", "analyzer": "ik_max_word", //指定查询时使用自定义的分析器 "search_analyzer": "ik_analyzer_synonyms_graph" }, … } } }
- 命中的结果集和索引时使用的同义词一致,但是结果的排序却不同。
- 这是因为在索引时使用同义词会计算全部的同义词的TF/IDF值,在搜索时进行的相关性计算,是将同义词和其他词同等对待,也就是将其TF/IDF值计算在内。
- 而在搜索时使用同义词,需要ES将同义词转换后再进行相关性计算。
- 根据explain的结果来看,ES将查询改写为title字段匹配关键字“假日”或者“度假”。
- 如果有更新同义词的需求,则只能使用查询时使用同义词的这种方式。
- 首先需要先关闭当前索引:
POST /hotel/_close
- 更改索引的settings信息;
- 打开索引。
- 首先需要先关闭当前索引:
- 如果同义词比较多,在settings中进行配置时将非常烦琐。
- ES支持用户将同义词放在文件中,文件的位置必须是在${ES_HOME}/config目录及其子目录下,注意该文件必须存在于ES集群中的每一个节点上。
- 在${ES_HOME}/config目录下建立一个子目录mydict,然后在该目录下创建一个名称为synonyms.dict的文件。
- 然后在创建酒店索引时,在settings中指定同义词文件及其路径。
- 当同义词词典文件内容更新时,例如,在其中添加“酒店,旅店”后,则需要执行如下请求:
执行上述请求后,同义词得到更新,后续就可以在查询中使用新添加的同义词了。POST /hotel/_reload_search_analyzers