版权声明:本文为博主原创文章,转载请注明出处:https://twocups.cn/index.php/2021/02/25/28/
全文搜索
全文搜索(Full-Text Search,缩写 FTS)是一项在一系列文档中搜索文本的技术。文档可以是网页、报刊文章、Email 邮件或者任何结构化的文本。例如平时大家用的谷歌搜索和百度搜索干的就是这个事。我们之前提到过的 Lucene 就是全文搜索引擎(工具包),并且基于 Lucene 构建的 Elasticsearch 和 Solr 也都是全文搜索引擎。
现在比如说,我们自己建立了一个全文搜索引擎,并且我们有一大堆的文档。我们拿“Twocups”这个词举例,搜索引擎如果在一大堆的文档中找到“Twocups”呢?
方案一:把文档加载到内存,然后通过一个循环判断每一篇文档是否包含关键字“Twocups”,这是最简单的方式。
但这个方案很明显有很大的问题,如果说文档的数量非常大、内容非常多,那么搜索的速度就会急剧下降。同时,如果限定“Twocups”必须是完整的单词,又或者我们需要同时搜索多个单词呢?按照方案一的算法,时间复杂度是 O(n),搜索时间只会越来越离谱。但仔细想想,其实这也是正常的。如果现实中需要各位确认几篇文章中有没有特定的单词,那肯定是要每篇文章都看一遍的,所以这个时间复杂度是在情理之中的。那我们有没有什么方案可以加速这个过程呢?
方案有很多,这里我们只说全文搜索的核心数据机制——倒排索引,Lucene 使用的也是这种结构。
倒排索引
倒排索引(Incerted Index,也叫反向索引)是实现“单词-文档矩阵”的一种具体存储形式,通过倒排索引,可以根据单词快速获取包含这个单词的文档列表。通俗点说,传统的索引是通过文档找关键字,即通过文档 ID 打开文档,然后再从中找关键字。而倒排索引则是先做了预处理,即建立了“每个单词出现在哪些文档中”的索引,所以搜索时自然就能迅速通过关键词找到所在的文档。
而我们现实中使用谷歌搜索和百度搜索也是这个原理,倒排索引相当于为互联网上千亿的网页做了一个索引,相当于书的目录,我们想看那个章节,直接根据目录翻到相应的页码就行了。从时间复杂度上来说,倒排索引下的搜索的时间复杂度肯定是要比 O(n) 小得多的,这里的 n 指的是上千亿的网页。
但是在我们开始建立索引之前,我们还需要将文档中的词进行切分,把文档切成一个一个的词项。而我们也是基于这些词项建立索引的,而不是为一大段话建立索引。从这部分开始,就属于文本分析的内容了。一个文本分析器包含了一个分词器和多个过滤器。
中文分词
分词器是文本分析的第一步,其中分词指的是将一个文本转换成一系列单词的过程。在 Elasticsearch 中,分词被称为 Analysis。举个例子:我昨天去了一趟超市 -> 我/昨天/去了/一趟/超市。之前我们提到过的表示字符串的 text 类型和 keyword 类型也和这个有关系。text 类型的字符串是要被全文搜索的,字段内容会被分词器分成一个一个的词项;而 keyword 类型的字符串适用于索引结构化字段,也可以理解为它不能拆。
从这里开始我们回到 Elasticsearch,我们可以指定默认的分词器进行分词。
POST http://192.168.56.101:9200/twocups2/_analyze { "analyzer": "standard", "field": "hobby", "text": "听音乐" }
相对于英文分词,中文分词会稍微麻烦一点。因为英文中,空格可以作为分隔符,而汉语中却没有明显的词汇分界点。在这种情况下,如果词汇分隔不正确的话,就会产生歧义。中文分词器有很多,例如 IK、jieba、THULAC 等,我们这里使用了 IK 分词器(整合包中已经包含 IK 分词器的安装包)。
IK 分词器的 GitHub 地址:https://github.com/medcl/elasticsearch-analysis-ik
安装分词器
用 MobaXterm 将 IK 分词器安装包解压到 Elasticsearch 的插件文件夹下。
# 创建文件夹 ik mkdir /soft/es/elasticsearch-6.5.4/plugins/ik # 将 ik 分词器安装包的内容解压进去 cd /soft/es/elasticsearch-6.5.4/plugins/ik unzip elasticsearch-analysis-ik-6.5.4.zip # 重启 Elasticsearch cd /soft/es/elasticsearch-6.5.4 ./bin/elasticsearch
测试安装好的 IK 分词器。
POST http://192.168.56.101:9200/_analyze { "analyzer": "ik_max_word", "text": "我昨天去了一趟超市" }
单词搜索
全文搜索最重要的是:相关性(Relevance)和分词(Analysis)。相关性是评价查询与其结果间的相关程度,并根据这种相关程度对结果排名的一种能力。其中的计算方式有很多,例如 TF/IDF 方法、地理位置邻近、模糊相似等。分词是将文本块转换为有区别的、规范化的 token 的一个过程,目的是为了创建倒排索引以及查询倒排索引。
下面我们来进行一次单个词语的搜索。
POST http://192.168.56.101:9200/twocups2/person/_search { "query":{ "match":{ "hobby":"音乐" } }, "highlight":{ "fields":{ "hobby":{ } } } }
索引 twocups2 是我们上一篇就建立好的,其中字段 hobby 的类型是 text,分词器是 ik_max_word,即 IK 分词器。以上这个单词搜索的含义是:在索引 twocups 下搜索字段 hobby 中有“音乐”的 person 类型的数据,并且高亮显示。整个搜索的过程如下:
- 检查字段类型
- 字段 hobby 是 text 类型,且指定了 IK 分析器,那么 hobby 的内容可以被分词。
- 分析查询字符串
- 将所查询的字符串“音乐”放入分词器 IK 中,输出的结果仍是单个项“音乐”,所以底层执行的是单个 term 查询。
- 查找匹配文档
- 用 term 查询在倒排索引中查找“音乐”,然后获取一组包含该项的文档。
- 为每个文档评分
- 用 term 查询计算每个文档相关度评分 _score ,这是种将词频(term frequency,即词“音乐”在相关文档的 hobby 字段中出现的频率)和反向文档频率(inverse document frequency,即词“音乐”在所有文档的 hobby 字段中出现的频率),以及字段的长度(即字段越短相关度越高)相结合的计算方式。
多词搜索
下面我们同时搜索“音乐”和“篮球”这两个词。
POST http://192.168.56.101:9200/twocups2/person/_search { "query":{ "match":{ "hobby":"音乐 篮球" } } }
注意,如果不加任何操作符的话,“音乐”和“篮球”这两个词之间默认是“或”的关系。如果想用“与”的逻辑搜索,那么加上操作符即可。(这里的代码原来我少写个逗号导致错误,后来由网友max指出,当前已修改。正好以我自己为错误案例提醒大家一下,同级的field之间除了最后一个以外都要加逗号的。)
POST http://192.168.56.101:9200/twocups2/person/_search
{
"query": {
"match": {
"hobby": "音乐 篮球",
"operator": "and"
}
}
}
“或”操作和“与”操作其实算得上是两种极端,而在实际场景中,只要符合一定的相似度就可以查询到数据。举个简单的例子,有时候你搜索的时候关键词打错了,但还是搜到了你想要的数据,就是因为有相似度存在的原因。在 Elasticsearch 中,我们可以通过 minimum_should_match 来指定匹配度。
POST http://192.168.56.101:9200/twocups2/person/_search { "query":{ "match":{ "hobby":{ "query":"游泳 羽毛球", "minimum_should_match":"80%" } } } }
这里的相似度设定为了80%,而在实际的场景中,是需要进行反复测试,才能够得到合理的相似度。
组合搜索
在搜索时,也可以使用过滤器中讲过的 bool 组合查询。下面搜索的意思是:必须包含篮球,同时必须不包含音乐,同时如果包含游泳,相似度会更高。
POST http://192.168.56.101:9200/twocups2/person/_search { "query":{ "bool":{ "must":{ "match":{ "hobby":"篮球" } }, "must_not":{ "match":{ "hobby":"音乐" } }, "should":[ { "match":{ "hobby":"游泳" } } ] } } }
提一句,如果以上的搜索中不包含 must,而是只有 should 呢?那我们不难猜出,只要会匹配 should 中的一个。如果我们再给 should 加上参数 minimum_should_match 来指定相似度,那么搜索结果就会更好了。
POST http://192.168.56.101:9200/twocups2/person/_search { "query":{ "bool":{ "must":{ "match":{ "hobby":"篮球" } }, "must_not":{ "match":{ "hobby":"音乐" } }, "should":[ { "match":{ "hobby":"游泳" } } ], "minimum_should_match":2 } } }
“minimum_should_match”:2 的意思是 should 中的3个词,至少要满足2个。
权重
除了相似度,我们还可通过对某些词增加权重来影响数据的得分。
POST http://192.168.56.101:9200/twocups2/person/_search { "query":{ "bool":{ "must":{ "match":{ "hobby":{ "query":"游泳篮球", "operator":"and" } } }, "should":[ { "match":{ "hobby":{ "query":"音乐", "boost":10 } } }, { "match":{ "hobby":{ "query":"跑步", "boost":2 } } } ] } } }
Elasticsearch 剩余内容
其实关于 Elasticsearch 的剩余内容还有很多啊,比如 Elasticsearch 的集群部署、Elasticsearch 的两种 Java 客户端的使用等。那么在实际部署和使用方面,集群部署和客户端的使用没有什么太难的地方或者坑。其中,集群部署只要设置节点类型和主机地址的时候注意点就好了。那么剩下的就是关于它们的原理,比如集群的节点分类、全文读写操作的流程,其实都不属于“实际部署”这一大章节的内容,我准备放到后面将原理的章节在和大家细细地说。
下篇继续
【Elastic Stack系列】第三章:实际部署(四) Beats篇
POST http://192.168.56.101:9200/twocups2/person/_search
{
“query”:{
“match”:{
“hobby”:”音乐 篮球”
“operator”:”and”
}
}
}
这个运行报错,版本7.16.1 修正下面的正常
POST twocups2/person/_search
{
“query”:{
“match”:{
“hobby”:{
“query”: “音乐 篮球”,
“operator”: “and”
}
}
}
}
我的确写错了,现在已修改。
感谢提醒,也已经在文中表明你指出的问题,大家也正好以我为戒,以后都注意下这个问题。