版权声明:本文为博主原创文章,转载请注明出处: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 类型的数据,并且高亮显示。整个搜索的过程如下:

  1. 检查字段类型
    • 字段 hobby 是 text 类型,且指定了 IK 分析器,那么 hobby 的内容可以被分词。
  2. 分析查询字符串
    • 将所查询的字符串“音乐”放入分词器 IK 中,输出的结果仍是单个项“音乐”,所以底层执行的是单个 term 查询。
  3. 查找匹配文档
    • 用 term 查询在倒排索引中查找“音乐”,然后获取一组包含该项的文档。
  4. 为每个文档评分
    • 用 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篇

林皓伟

《【Elastic Stack系列】第三章:实际部署(三) Elasticsearch篇》有 2 条评论
    1. 我的确写错了,现在已修改。
      感谢提醒,也已经在文中表明你指出的问题,大家也正好以我为戒,以后都注意下这个问题。

回复 林皓伟 取消回复

您的电子邮箱地址不会被公开。