ElastSearch全文搜索机制和项目案例简析
一、前言
近期,营销业务开发部参与的一个项目是对 SCRM 顶部 Banner 位置的全局搜索功能进行用户体验的优化。
如下图所示:

产品的需求是,用户可以直接在全局搜索框里输入关键字对联系人、会话存档内容等进行搜索,并快速跳转到对应的业务功能页面。并且全局搜索的结果与对应业务功能页的列表中查询到的内容必须保持一致。
后端研发在进行技术实现时,为了提高搜索的响应速度,部分搜索项是通过在 ElasticSearch 里进行全文搜索来实现的,而具体业务功能页中的查询结果列表,则是在基于 MongoDB 的业务数据库里进行的查询。
因此产生的一个问题就是在查询的关键字相同的情况下,可能会出现 ElasticSearch 全文搜索的结果和 MongoDB 查询结果不一致的现象,非常影响用户体验。
这一问题困扰研发多时,而最终解决这一问题的方法也比较简单,仅仅是将 ElasticSearch 文本搜索时使用的分词器由“IK” 换成了“N-Gram”。
那么为什么仅仅是换了一个分词器(Tokenizer),就对搜索的结果产生这么大的影响?
本文将对这其中涉及的技术原理做一个简单的讲解
二、全文搜索
通过 ElasticSearch 来实现搜索功能,就是利用它为人所熟知的“全文搜索”(Full-Text Search)能力。
那么,什么是“全文搜索”呢?以下是百度百科上的解释
全文检索是指计算机索引程序通过扫描文章中的每一个词,对每一个词建立一个索引,指明该词在文章中出现的次数和位置,当用户查询时,检索程序就根据事先建立的索引进行查找,并将查找的结果反馈给用户的检索方式。这个过程类似于通过字典中的检索字表查字的过程。全文搜索搜索引擎数据库中的数据。
简单的说,全文检索大体分为创建索引(Indexing)和搜索索引(Search)两个过程。
- 创建索引:将现实世界中所有的结构化和非结构化数据提取信息,创建索引的过程
- 搜索索引:就是得到用户的查询请求,搜索创建的索引,然后返回结果的过程。
这里就不对底层的倒排索引、Trie树等概念和数据结构进行详细的说明了,有兴趣的同学可以自行阅读相关文档资料。
三、文本分析详解
本文将重点探讨的是全文搜索在创建和搜索索引的过程中,一个重要的步骤————文本分析(Text Analysis)
3.1、概念与术语
首先,先对一些关键的概念与术语做一个简单的介绍:
文档(Document)
索引与搜索的主要数据载体,它包含一个或多个字段,存放将要写入索引或将从索引搜索出来的数据。
字段(Field)
文档的一个片段,包括两个部分:字段的名称和内容;
词项(Term)
搜索时的一个单位,代表文本中的某个词。
词条(Token)
词项在字段中的一次出现,包括词项的文本,开始和结束的位移以及类型。
倒排索引(Inverted Index)
倒排索引面向词项,而不是面向文档。是一种将词项映射到文档的结构。
分析器(Analyzer)
文本分析由分析器来执行。
分析器就是由分词器(Tokenizer)、过滤器(Filter)和字符映射器(Character mapper)组成的一个功能集合。
3.2、文本分析简介
通俗的讲,文本分析(Text Analysis)就是将一段文字,比如一篇文章或者一个句子,转换为一组词条(Token)或词项(Term),并添加到倒排索引中进行搜索的过程。
文本分析包含了下面的过程:
- 分词(Tokenization):将一段文本分解为适合于倒排索引使用的独立的词条;
- 规范化(Normalization):将这些词条调整为标准格式以提高它们的可搜索性或者召回率(Recall)
在 Elasticsearch 中,文本分析的过程是通过分析器(Analyzer)来执行的,我们可以使用内置的一系列分析器,也可以按自己的需要为索引指定自定义的分析器。
3.3、分析器(Analyzer)
通俗的将,分析器就是将三个功能组合封装在一起的一个整体:
- 字符过滤器(Char Filter)
- 分词器(Tokenizer)
- 词条过滤器(Token Filter)
Elasticsearch 提供了多个开箱即用的字符过滤器、分词器和 Token 过滤器。 可以把它们组合起来形成自定义的分析器以用于不同的用途。
3.3.1 工作机制
分析器的工作机制及这三个功能子集间的关系,大致如下图所示:

字符过滤器(Character Filter)
首先,原始的字符串文本按顺序通过每个字符过滤器。
他们的任务是在分词前整理字符串,对内容进行添加、删除或调整等一系列操作。
例如:一个字符过滤器可以用来去掉HTML,一个字符过滤器用来将 “&”、“|” 等符号转化成单词 and、or。
分词器(Tokenizer)
然后,字符串被分词器分解为单个的词条(Token)。
比如:一个简单的分词器遇到空格和标点的时候,可能会将文本拆分成词条。
Token 过滤器(Token Filter)
最后,是对词条进行规范化(Normalization)的过程,词条按顺序通过每个 Token 过滤器。
这个过程可能会对词条做一些调整,例如:大小写转换(大写的 Quick 转为小写的 quick ),删除词条(像 a, and, the 等无用的停止词),或者增加词条(像 jump 和 leap 这种同义词)。
需要注意的一点是,Token 过滤器不会改变每个词条的 Position。
对于一个分析器来说, Token 过滤器并非必不可少的组件。它可以不包含 Token 过滤器,也可以包含多个 Token 过滤器;
自定义分析器
下图是一个自定义的分析器示例:

这个分析器的工作过程是:
- 字符过滤:
- 按正则匹配并替换字符内容(Pattern Replace)
- 按Key-Value规则替换字符内容(Mapping)
- 剔除 HTML 标记(HTML Strip)
- 分词:
按空格进行分词(Whitespace 分词器) - Token 过滤:
- 大写转小写(Lowercase);
- 剔除停止词(Stop)
- 词干提取与除梗(Snowball)
词干提取(Stemming)
为了提高搜索的精确度,往往会使用各种 “Stemmer Token Filter” 对分词后的词条进行词干的提取和除梗。如上面示例中的 Snowball Token 过滤器,就是一种基于 Snowball 算法的除梗器。
词干提取就是将一个单词还原为其词根的过程。这可以确保在搜索过程中匹配不同的单词。
例如,walking 和 walked 可以来源于同一个词根 walk。
从1968年开始在计算机科学领域就出现了词干提取的相应算法。很多搜索引擎在处理词汇时,对同义词采用相同的词干作为查询拓展,该过程叫做归并。
词干提取项目一般涉及到词干提取算法或词干提取器。词干提取的方法与语言有关,但通常都会涉及到去除单词的前缀和后缀过程。
在某些情况下,一个词根可能就不是一个真正的单词。例如,jumping和 jumpiness 都可以还原为词根 jumpi。虽然 jumpi 不是一个真正的英语单词,但这一点对搜索来说并不重要,因为只要一个单词的所有变体都被简化为相同的词根形式,那么它们就可以正确的被检索到。
ElasticSearch 已经内置了一系列的除梗器,这些除梗器通常可以分为两类:
- 基于算法的除梗器(Algorithmic Stemmers)
- 基于字典的除梗器(Dictionary Stemmers)
词条图谱化(Token Graphs)
一段文本在经过分词器处理后,分解为一系列词条(Token)时,除了包含它代表的词项(Term),还会包含它在这段文本中的位置(Position)和长度(Position Length)信息。
例如以下这段文本:
1 | "Quick brown fox!" |
在经过各种词干提取与除梗的 Token 过滤器处理后,就可以构建为类似如下的有向无环图:

再经过基于同义词的 Token 过滤器处理后:

构建为这个结构后,基本就可以用于最后检索阶段的匹配规则了。
3.3.2 内置的标准分析器
ElasticSearch 已经内置了一系列开箱即用的标准分析器:
- Standard Analyzer
- Simple Analyzer
- Whitespace Analyzer
- Stop Analyzer
- Keyword Analyzer
- Pattern Analyzer
- Language Analyzer(这是一大类,特定于语言分析器)
- Fingerprint Analyzer
3.3.3 内置的标准分词器
上面列出的分析器,都是通过 Elasticsearch 的一系列内置的分词器等组件来实现的,其内置的分词器,基本可以分为三类:
面向单词(Word Oriented)的分词器:
- Standard Tokenizer
- Letter Tokenizer
- Lowercase Tokenizer
- Whitespace Tokenizer
- UAX URL Email Tokenizer
- Classic Tokenizer
- Thai Tokenizer
基于单词局部匹配(Partial Word)的分词器:
- N-Gram Tokenizer
- Edge N-Gram Tokenizer
结构化文本(Structured Text)分词器
- Keyword Tokenizer
- Pattern Tokenizer
- Simple Pattern Tokenizer
- Char Group Tokenizer
- Simple Pattern Split Tokenizer
- Path Tokenizer
每个分词器的具体功能和使用方法,有兴趣的同学,可以参考 Elastic 官方的文档;
四、N-Gram 分词器
4.1 什么是 N-Gram 模型
N-Gram 常用于自然语言处理(NLP)领域,它是一种基于统计语言模型的算法。
N-Gram 的基本思想就是,将文本里面的内容按照字节进行大小为 N 的滑动窗口操作,形成一系列长度是 N 的字节片段序列。每一个字节片段称为 Gram,对所有 Gram 的出现频度进行统计,并且按照事先设定好的阈值进行过滤,形成关键 Gram 列表,也就是这个文本的向量特征空间,列表中的每一种 Gram 就是一个特征向量维度。
该模型基于这样一种假设,第 N 个词的出现只与前面 N-1 个词相关,而与其它任何词都不相关,整句的概率就是各个词出现概率的乘积。这些概率可以通过直接从语料中统计 N 个词同时出现的次数得到。常用的是二元的 Bi-Gram 和三元的 Tri-Gram。
4.2 常见应用
说完了 N-Gram 模型的概念之后,下面讲解 N-Gram 的一般应用——搜索引擎、或者输入法中的猜想或者提示功能。
我们在用Google或百度时,输入一个或几个词,搜索框通常会以下拉菜单的形式给出几个像下图一样的备选,这些备选其实就是在猜想你想要搜索的那个词串。
再者,当你用输入法输入一个汉字的时候,输入法通常可以联系出一个完整的词,例如我输入一个“刘”字,通常输入法会提示我是否要输入的是“刘备”。
上面介绍的两个场景,其实就是以 N-Gram 模型为基础来实现的。
比如下图所示:

4.3 N-Gram 分词器
由于 N-Gram 模型非常实用的这些特性,在 Elasticsearch 中,N-Gram 分词器是作为标准的分词器而提供的。也就是说 N-Gram 分词器是 Elasticsearch 中开箱即用的标准功能,不像 IK 等分词器,还需要自己安装插件。
实用 N-Gram 分词器时,要注意的一点是,必须在创建索引时,就对它进行初始化,其中有几个关键参数:
| 序号 | 参数名 | 默认值 | 说明 |
|---|---|---|---|
| 1 | min_gram | 1 | 分词后的最小长度 |
| 2 | max_gram | 2 | 分词后的最大长度 |
| 3 | token_chars | 设置分词的形式,例如,是数字(digit)还是文字(letter)。分词器将根据分词的形式对文本进行分词。 |
以下是一个初始化 N-Gram 分词器的示例:
1 | PUT test-index-03 |
五、实战
上边对全文分析的底层原理做了简单说明,现在回到我们在项目中遇到的问题,来看看使用 N-Gram 分词器的实际效果。
以下面这段文字为例:
1 | 我近期准备去内蒙古旅行。 |
5.1 使用 IK 分词器
我们先用 IK 分词器来构建我们索引中的分析器。
首先,创建一个索引my-index-05,并且指定article字段为text类型,并使用ik_smart作为该字段的分析器;
1 | PUT /my-index-05 |
然后,向该索引中插入我们测试用的这段文字 :
1 | POST /my-index-05/_doc |
现在,尝试在这个索引上使用“内蒙古”作为关键字搜索一下:
1 | GET /my-index-05/_search |
成功搜索到了刚才插入索引的这段文字:
1 | { |
现在,把关键字换成“蒙古”再试一下:
1 | GET /my-index-05/_search |
很遗憾,居然没有搜索到我们刚才插入的这段文字
1 | { |
为什么会有这样的结果呢?
先来看看 IK 分词器,对刚才这段话,分词之后的词条(Token)都有哪些呢:
1 | POST /_analyze |
下边是分词后的词条信息:
1 | { |
真相大白,为什么用关键字“蒙古”搜不到想要的结果,是因为在分词后的结果里,没有“蒙古”这个词条。
所以我们当然搜不到咯。
5.2 使用 N-Gram 分词器
现在,把分词器换成“N-Gram”分词器试一下。
首先,用创建一个使用N-Gram分词器的索引。
1 | PUT my-index-04 |
然后,向索引中插入我们测试的文字:
1 | POST /my-index-04/_doc |
现在,试一下用“内蒙古”作为关键字,进行搜索:
1 | GET /my-index-04/_search |
很顺利的搜到了结果:
1 | { |
再试试,把关键字换成“蒙古”,看看会不会翻车:
1 | GET /my-index-04/_search |
仍然,很顺利搜到了想要的结果:
1 | { |
为什么换成 N-Gram 分词器后,能正常搜到关键字了呢?
来看一下 N-Gram 分词器是怎么分词的吧:
1 | POST my-index-04/_analyze |
分词的结果如下:
1 | { |
6、结语
ElasticSearch 的文本搜索能力非常强大,合理的使用内置的各种分析器、分词器和 Token过滤器,即可实现很多强大的搜索功能。当然它还提供了强大的扩展能力,方便我们扩展自己的分词器和 Token过滤器,构建完全符合我们业务需要的分析器。
