Elasticsearch 全文搜索引擎的个人学习笔记

这篇文章是我对自己在自学探索 Elasticsearch 全文搜索功能时学到的概念的一个总结,大概算是 A Quick Glimpse Into Elasticsearch (Full-Text Search Part)。然而隔日来看实属浅见,仅可作为导读,并不奢求能帮助读者建构起对于 Elasticsearch 本身客观成体系的认识。(实际上如果只是为了使用 Elasticsearch,也并不需要完全了解其工作机制。)如有需要可参考文末的官方文档和参考资料。

为什么用 Elasticsearch 来做全文检索,而不是 MySQL 等数据库自带的检索功能?

  • 速度更快——在 full-text search 领域性能更佳,数据量越大时会越明显。

  • 效果更好——搜索条件可以非常灵活,分布式设计易于扩展。

(当然 Elasticsearch 还有分析、聚合等其他功能,而 MySQL 等关系型数据库也有其更广泛丰富的需求,两者的定位并不相同。一般推荐将常规数据库作为业务数据的主要存储处,向 Elasticsearch 导入其中的部分数据进行索引以方便搜索。)

怎么安装?

https://www.elastic.co/cn/downloads/elasticsearch 有详细的逐步介绍。

只需要下载解压运行,或者 yum / apt / homebrew,抑或 docker。

安装完成后,Elasticsearch Server 默认运行于本机 9200 端口,提供 REST API——这意味着现在可以通过普通的浏览器来尝试访问。http://localhost:9200/ (另有 9300 端口供 TCP 协议客户端使用和节点间通信)

可以通过 bulk 来批量导入 JSON 形式的数据,或者使用 Logstash 等从已有数据库中导入。

基础概念简介

Document 文档

序列化为 JSON 格式存储、可被索引的一个数据单元,被指定了唯一 ID。一篇文档可包含多个字段。

Field 字段

键-值对,字段可被定义为某一类型,如 text(适用于全文搜索),keyword(适用于仅会用于精确搜索的用途,如"status": "published"),integer,date,boolean,nested 等。

Index 索引

所谓的“全文搜索”不是真的在每次搜索时读取一遍全部文本,而是先建立索引,需要搜索时再查询索引。这里指的是 inverted index,而 Elasticsearch index 作为名词是另一个概念,见下文。

Mapping 映射

文档结构,类似于 MySQL 的 Schema,在创建 index 时指定(之后可修改),定义了文档包含字段的名称及类型。Elasticsearch 也可以在不定义 mapping 的情况下使用(schema-less),此时它会通过第一个存入的数据推断字段类型。GET /\<index>/_mapping 可以查看当前\<index>的 mapping。

Analyzer 分词器

索引时首先要分词才能建立索引。搜索时也是如此,若搜索的不止一个词,而是包含多个词的短语或句子,则先用分词器分离,再进行搜索。英文的分词显然较为容易,中文分词推荐 ik_max_word(最细粒度拆分出尽可能多的词语组合,官方说“适合 Term Query”)和 ik_smart(最粗粒度的拆分,“适合 Phrase 查询”)。搜索和索引的分词器可以不同,若在搜索时未指定分词器,则默认为索引的默认分词器。

Cluster 集群,Node 节点,Index (Indices) 索引, Shard 切片, Replica 副本切片

Elasticsearch 是分布式设计,一个对外提供服务的 Elasticsearch Server 即是一个 Cluster 集群,可以添加 Node 节点以提升性能、容量与冗余,存入的数据与收到的查询请求将被自动分配。(在同一台机器上运行多个 Node 也是可行的。)一个 Elasticsearch index 是文档的集合,这些文档一般具有相似特征。在增删改查这批文档时需要指定 index 的名字。Indices 是 index 的复数形式。一个 Elasticsearch Index 包含一个或多个 Shard 切片,一个切片实际上是一个 Lucene Index 实例(Apache Lucene 是 Elasticsearch 的底层基础),可以说 Shard 是数据的容器,但与使用 ES 服务的应用程序直接交互的是 Index。Shard 分为 primary shard 主切片和 replica shard 副本切片,文档存储于主切片中,而副本切片是主切片的一份拷贝,提供冗余并处理请求。

Near Real-Time 准实时

在数据存入后,它就在 1 秒内被索引而可查询。

Query DSL 查询专用语言

一套描述搜索条件的语言,格式为 JSON。

Score

搜索结果中的某篇文档与搜索要求的契合程度。一般情况下是相关度,目前 Elasticsearch 默认采用 BM25 算法,过去曾使用过 TF/IDF(词频/逆向文档频率)。也可以在搜索请求中指定,当满足某条件时增加或减少评分,或直接给出固定分值。

Query DSL 中和全文搜索相关的一些查询关键词

建议使用 curl 命令来测试下面的查询。官方给出的示例:

curl -XGET 'http://localhost:9200/twitter/_search?pretty=true' -H 'Content-Type: application/json' -d '
{
    "query" : {
        "match" : { "user": "kimchy" }
    }
}'

如果要用浏览器 URI 进行简单搜索则需要用到 Lucene 查询语法(参考:https://www.elastic.co/guide/en/elasticsearch/reference/current/search-uri-request.htmlhttps://lucene.apache.org/core/2_9_4/queryparsersyntax.html),给个例子(注意 AND):http://localhost:9200/post_index/_search?q=user:CoolSpring%20AND%20content:Surface&size=30&pretty

  • match: 最常用的查询方式。若输入的是短语或句子,则 Elasticsearch 会先用分词器分开,再进行搜索。只关心这些词是否都出现,而不考虑它们之间的距离。

    {
      "query": {
        "match": {
          "content": "quick fox"
        }
      }
    }
  • match_phrase: 短语匹配。将查询内容分词成为词的列表后,搜索时考虑它们在文本中的相对位置。可以指定一个 slop 值,即允许的最大距离。slop 默认为 0,此时可以看作“完全匹配”(均出现,且相邻)。

    {
      "query": {
        "match_phrase": {
          "content": "包含一字不差的内容才显示为结果"
        }
      }
    }
  • term: 精确匹配。比较适合用在 keyword 类型的字段,或者 board_id, topic_id, user_id 这种精确值字段(exact value string field)。

    {
      "query": {
        "term": {
          "user": {
            "value": "Kimchy",
            "boost": 1.0
          }
        }
      }
    }
  • bool: 用以连接多个布尔查询(must, must_not, should, filter)

    • must: 如其名,必须出现。

    • must_not: 不能出现。

    • should: “应该”出现。

    • filter: 必须出现。与 must 不同的是查询的评分将被忽略。

包含多个词的 match 查询经过分词后可以改写,前述第一个 match 查询示例在内部等效于

{
  "query": {
    "bool": {
      "should": [{ "term": { "text": "quick" } }, { "term": { "text": "fox" } }]
    }
  }
}
  • minimum_should_match: 至少有几个/百分之几的 should 语句匹配才可出现在结果中。

  • _source: 在索引时传递的原始 JSON 文档数据。索引与原内容并不相同,_source 本身不被索引、不可搜索,只是保存。

  • from, size: 易于理解,用于分页搜索中。

  • rescore: 对于 query 返回的前 n 个搜索结果(如 100-500 个)按照已有分数和新的条件进行重新评分排列。“match 返回的搜索结果不考虑词的距离所以效果不好”的问题可能很常见,一种方法时可以用简单的 bool{must: match, should: match_phrase}“使用邻近度提高相关度”的方式来解决:

    {
      "query": {
        "bool": {
          "must": {
            "match": {
              "message": {
                "query": "the quick brown",
                "minimum_should_match": "30%"
              }
            }
          },
          "should": {
            "match_phrase": {
              "message": {
                "query": "the quick brown",
                "slop": 2
              }
            }
          }
        }
      }
    }

    但是由于 match_phrase 比较消耗性能(相对于普通的 match 而言),并且用户一般只关心结果前几页,所以可以选择用 rescore 仅对前 50 项进行处理。

    {
      "query": {
        "match": {
          "message": {
            "operator": "or",
            "query": "the quick brown"
          }
        }
      },
      "rescore": {
        "window_size": 50,
        "query": {
          "rescore_query": {
            "match_phrase": {
              "message": {
                "query": "the quick brown",
                "slop": 2
              }
            }
          },
          "query_weight": 0.7,
          "rescore_query_weight": 1.2
        }
      }
    }

结语

Elasticsearch 的使用实在很难穷尽,笔者在自学过程中还只能管窥一二,许多功能还没来得及探索。在写这篇文章的时候,关于导入文档的具体操作等重要内容也没有提及。不过有句话说得好,文档和代码是最好的学习资源。Elasticsearch 基于 Apache Lucene,分为 Apache 2.0 授权下的开源版、Elastic License 下的免费版,和商业版。(不过就普通使用的小规模搜索用途而言不大需要研究它的代码。)官方英文文档见:https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html

参考资料

  1. 引用了来自官方文档的一些例子;

  2. What is ElasticSearch? Why ElasticSearch? Advantages of ElasticSearch!: https://medium.com/@AIMDekTech/what-is-elasticsearch-why-elasticsearch-advantages-of-elasticsearch-47b81b549f4d (写了一半查资料时看到这篇文章,深受启发。一部分内容参考了它,在此感谢并推荐。)