事实上是挑战杯要搭一个文书搜索网站…暂时需要用 BM25 算法顶一下。

Elasticsearch 的默认相似度算法就是 BM25,嘛嘛,大胜利。

搜索的对象是…数目约在 $10^8$ 规模左右的文档…

嘛嘛,反正都是大调库。缝合就完事了。

// WIP: 应该不会咕

索引算法

我们首先介绍下 Sparse Retrieval 的主要算法,TF-IDF 算法和 BM25 算法。

TF-IDF

TF 是指归一化的词频,IDF 是指逆文档频率。给定文档集合 $D$,有 $d_i \in D, 1 \le i \le n$.

文档集合总共包含 $m$​ 个词,去除一些十分常见的词作为停用词(Stop Words),有 $w_i \in W, 1 \le i \le m$​.

定义 TF 如下,即一篇文档中某个词出现的频率:

TF 只能描述词在文档中的频率,但假设现在有个词为“我们”,这个词可能在文档集 $D$ 中每篇文档中都会出现,并且有较高的频率。那么这一类词就不具有很好的区分文档的能力,为了降低这种通用词的作用,引入了 IDF:

于是我们综合这两部分, 便可以得到 TF-IDF:

TF 可以计算在一篇文档中词出现的频率,而 IDF 可以降低一些通用词的作用。因此对于一篇文档我们可以用文档中每个词的 TF−IDF 组成的向量来表示该文档,再根据余弦相似度这类的方法来计算文档之间的相关性。

BM25

BM25 是信息索引领域用来计算 query 与文档相似度得分的经典算法。

不同于 TF-IDF,BM25 的公式主要由三个部分组成:

  1. query 中每个单词 $q_i$ 与文档 $d$ 之间的相关性
  2. 单词 $q_i$ 与 query 之间的相似性
  3. 每个单词的权重

BM25 算法的一般公式:

其中 $Q$ 表示 query,$q_i \in Q$,$d$​ 表示 document.

下展开介绍各部分公式:

  • $W_i$

其中 $N$​​ 是 document 总数,$df_i$​ 表示含有 $q_i$​ 的文档总数。

依据 IDF 的作用,对于某个 $q_i$​ ,包含 $q_i$ 的文档数越多,说明 $q_i$ 重要性越小,或者区分度越低,IDF 越小,因此 IDF 可以用来刻画 $q_i$ 与文档的相似性。

  • $R(q_i, d)$​

BM25 的设计依据一个重要的发现:词频和相关性之间的关系是非线性的,也就是说,每个词对于文档的相关性分数不会超过一个特定的阈值,当词出现的次数达到一个阈值后,其影响就不在线性增加了,而这个阈值会跟文档本身有关。

我们可以分成两部分来看待上述公式,其中 $f_i$​ 为 $q_i$​ 在 $d$​ 中出现的次数,$k_1, k_2, K$​ 是常数。

后一部分 $\dfrac {qf_i \cdot(k_2+1)}{qf_i+k_2}$ 在控制 $q_i$​ 和 Query 的相似度。

前一部分在计算 $q_i$ 与 $d$​ 的相似度,其中 $K = k_1 \cdot (1-b+b\cdot \dfrac {|d|}{AVG_n(|d|)})$,参数 $b$ 在调节文本长度对相关性的影响。

不失一般性地我们可以取 $k_1 = 2, k_2 = 0, b = 0.75$​.

反正在接下来的运用也是大调库,调参数可以通过更改配置文件来进行。

网站搭建纪实:Elasticsearch

Elasticsearch is the distributed search and analytics engine at the heart of the Elastic Stack. Logstash and Beats facilitate collecting, aggregating, and enriching your data and storing it in Elasticsearch. Kibana enables you to interactively explore, visualize, and share insights into your data and manage and monitor the stack. Elasticsearch is where the indexing, search, and analysis magic happens.

Elasticsearch 是一个开源的搜索引擎,建立在一个全文搜索引擎库 Apache Lucene™ 基础之上。

那 Lucene 又是什么?Lucene 可能是目前存在的,不论开源还是私有的,拥有最先进,高性能和全功能搜索引擎功能的库,但也仅仅只是一个库。

要用上 Lucene,我们需要编写 Java 并引用 Lucene 包才可以,而且我们需要对信息检索有一定程度的理解才能明白 Lucene 是怎么工作的,反正用起来没那么简单。

那么为了解决这个问题,Elasticsearch 就诞生了。Elasticsearch 也是使用 Java 编写的,它的内部使用 Lucene 做索引与搜索,但是它的目标是使全文检索变得简单,相当于 Lucene 的一层封装,它提供了一套简单一致的 RESTful API 来帮助我们实现存储和检索。

所以 Elasticsearch 仅仅就是一个简易版的 Lucene 封装吗?那就大错特错了,Elasticsearch 不仅仅是 Lucene,并且也不仅仅只是一个全文搜索引擎。它可以被下面这样准确的形容:

  • 一个分布式的实时文档存储,每个字段可以被索引与搜索
  • 一个分布式实时分析搜索引擎
  • 能胜任上百个服务节点的扩展,并支持 PB 级别的结构化或者非结构化数据

总之,是一个相当牛逼的搜索引擎,维基百科、Stack Overflow、GitHub 都纷纷采用它来做搜索。

总之我们先来到了下载网站看看:https://www.elastic.co/cn/downloads/elasticsearch。然后选择了适用 Linux 的安装方式:https://www.elastic.co/guide/en/elasticsearch/reference/current/targz.html。然后就是把 ./config/elasticsearch.yml 的关于安全的设定全部设置成 false(毕竟还要过一层 Django 转接)。然后执行 curl localhost:9200,返回:

{
  "name" : "thunlp-3",
  "cluster_name" : "elasticsearch",
  "cluster_uuid" : "7O7KbQ6hQGOUieAXeKm10g",
  "version" : {
    "number" : "8.0.0",
    "build_flavor" : "default",
    "build_type" : "tar",
    "build_hash" : "1b6a7ece17463df5ff54a3e1302d825889aa1161",
    "build_date" : "2022-02-03T16:47:57.507843096Z",
    "build_snapshot" : false,
    "lucene_version" : "9.0.0",
    "minimum_wire_compatibility_version" : "7.17.0",
    "minimum_index_compatibility_version" : "7.0.0"
  },
  "tagline" : "You Know, for Search"
}

观前提示:官方的中文 Doc 已经过时了,不少接口和格式都已经改了,请阅读最新版的文档。好大的坑

2022-2-17 00:23:38:姑且是把后端调通了,不过这份学习笔记我觉得可以近似作废了,找时间再改吧。下面的教程并不适用 ES 最新版本,虽然大多数思想是相同的,比较便于迁移学习。

Node 与 Cluster

Elastic 本质上是一个分布式数据库,允许多台服务器协同工作,每台服务器可以运行多个 Elastic 实例。

单个 Elastic 实例称为一个节点(node)。一组节点构成一个集群(cluster)。

集群与分布式

(只是趁着这次机会顺便就把 Elasticsearch 上手学一遍,这次毕竟只有单节点(x)

事实上,Elasticsearch 被设计出来就是以集群运作为基础的。

ElasticSearch 的主旨是随时可用和按需扩容。 而扩容可以通过购买性能更强大(垂直扩容,或纵向扩容)或者数量更多的服务器(水平扩容,或横向扩容)来实现。虽然 Elasticsearch 可以获益于更强大的硬件设备,但是垂直扩容是有极限的。 真正的扩容能力是来自于水平扩容—为集群添加更多的节点,并且将负载压力和稳定性分散到这些节点中。

  • 结点与集群:一个运行中的 Elasticsearch 实例称为一个节点,而集群是由一个或者多个拥有相同 cluster.name 配置的节点组成, 它们共同承担数据和负载的压力。当有节点加入集群中或者从集群中移除节点时,集群将会重新平均分布所有的数据。
  • 主节点:管理集群范围内的所有变更,例如增加、删除索引,或者增加、删除节点等。用户可以将请求发送到任意结点,每个节点都知道任意文档所处的位置,并且能够将我们的请求直接转发到存储我们所需文档的节点。
  • 集群健康状态:通过 [GET] /_cluster/health 可以查询当前节点所在集群的状态。其中返回值的 status 字段如果为 green,代表所有主分片和副本分片都在正常运行;如果为 yellow,代表所有主分片都在正常运行,副本分片并不是都在正常运行;如果为 red,说明有主分片没能正常运行。
  • 主分片与副本分片:
    • 分片:索引实际上是指向一个或者多个物理分片的逻辑命名空间。一个分片是一个底层的工作单元,它保存了全部数据中的一部分。现在我们只需知道一个分片是一个 Lucene 的实例,以及它本身就是一个完整的搜索引擎。 我们的文档被存储和索引到分片内,但是应用程序是直接与索引而不是与分片进行交互。
    • 主分片:索引内任意一个文档都归属于一个主分片,所以主分片的数目决定着索引能够保存的最大数据量。
    • 副本分片:一个副本分片只是一个主分片的拷贝。副本分片作为硬件故障时保护数据不丢失的冗余备份,并为搜索和返回文档等读操作提供服务。副本分片的存在目的是为了在故障的情况下不至于丢失数据。如在下图这个具有三个节点的集群中,$P_i$ 代表主分片,$R_i$ 代表副本分片,每个主分片拥有 2 个其对应的副本分片。

拥有2份副本分片3个节点的集群

Document

Index 里面单条的记录称为 Document(文档)。许多条 Document 构成了一个 Index。

Document 使用 JSON 格式表示,下面是一个例子。

{
  "user": "张三",
  "title": "工程师",
  "desc": "数据库管理"
}

同一个 Index 里面的 Document,不要求有相同的结构(scheme),但是最好保持相同,这样有利于提高搜索效率。

文档的元数据:

  • _id:文档的 ID 字符串

  • _type:文档的类型名

  • _index:文档所在的索引

  • _uid_type_id 连接在一起构造成 type#id

  • _source:原模原样的 JSON 文件
  • _all:全文拼接

文档路由到分片中的方式为:shard = hash(routing) % number_of_primary_shards,其中默认 routing 是一个可变值,默认是文档的 _id。所有的文档 API(getindexdeletebulkupdate 以及 mget)都接受一个叫做 routing 的路由参数 ,通过这个参数我们可以自定义文档到分片的映射。

Indexing

Elastic 会索引所有字段,经过处理后写入一个倒排索引(Inverted Index)。查找数据的时候,直接查找该索引。

所以,Elastic 数据管理的顶层单位就叫做索引(Index)。它是单个数据库的同义词。每个 Index (即数据库)的名字必须是小写。

索引与中文分析器

  • 索引的创建
PUT /my_index
{
    "settings": {
        "number_of_shards" :   5, // 主分片数
        "number_of_replicas" : 0
    },
    "mappings": {
        "type_one": { ... any mappings ... },
        "type_two": { ... any mappings ... },
        ...
    }
}
  • 索引的删除:[DELETE] /my_index
分析器 Analysis
  • 索引的设置:分析器(analysis),用于将全文字符串转换为适合搜索的倒排索引的工具。

  • 何为分析

Exact Values 与 Full Text

Elasticsearch 中的数据可以分为两类:精确值(Exact Values)和全文本(Full Text)。

精确值是指一些精确的数据,比如日期或者用户 ID。相比较起来,精确值类型的数据很容易查询。结果是二进制的:要么匹配查询,要么不匹配。

全文本数据指的是长文本数据,比如一个推文的内容或者一封邮件的内容。查询全文数据要微妙的多。我们问的不只是“这个文档匹配查询吗”,而是“该文档匹配查询的程度有多大?”换句话说,该文档与给定查询的相关性如何?

为了促进这类在全文本域中的查询,Elasticsearch 首先 分析 文档,之后根据结果创建 倒排索引 。在接下来的两节,我们会讨论倒排索引和分析过程。

倒排索引

Elasticsearch 使用一种称为 倒排索引 的结构,它适用于快速的全文搜索。一个倒排索引由文档中所有不重复词的列表构成,对于其中每个词,有一个包含它的文档列表。

例如,假设我们有两个文档,每个文档的 content 域包含如下内容:

  1. The quick brown fox jumped over the lazy dog
  2. Quick brown foxes leap over lazy dogs in summer

为了创建倒排索引,我们首先将每个文档的 content 域拆分成单独的词(我们称它为 词条tokens ),创建一个包含所有不重复词条的排序列表,然后列出每个词条出现在哪个文档。结果如下所示:

Term      Doc_1  Doc_2
-------------------------
Quick   |       |  X
The     |   X   |
brown   |   X   |  X
dog     |   X   |
dogs    |       |  X
fox     |   X   |
foxes   |       |  X
in      |       |  X
jumped  |   X   |
lazy    |   X   |  X
leap    |       |  X
over    |   X   |  X
quick   |   X   |
summer  |       |  X
the     |   X   |
------------------------

现在,如果我们想搜索 quick brown ,我们只需要查找包含每个词条的文档:

Term      Doc_1  Doc_2
-------------------------
brown   |   X   |  X
quick   |   X   |
------------------------
Total   |   2   |  1

但是,我们目前的倒排索引有一些问题:

  • Quickquick 以独立的词条出现,然而用户可能认为它们是相同的词。
  • foxfoxes 非常相似, 就像 dogdogs ;他们有相同的词根。
  • jumpedleap, 尽管没有相同的词根,但他们的意思很相近。他们是同义词。

如果我们将词条和用户查询都规范为标准模式,那么我们可以找到与用户搜索的词条不完全一致,但具有足够相关性的文档。

这种分词标准化的过程就称为是分析

分析和分析器

分析包含下面的过程:

  • 首先,将一块文本分成适合于倒排索引的独立的 Tokens
  • 之后,将这些词条统一化为标准格式以提高它们的“可搜索性”

分析器执行上面的工作。 分析器 实际上是将三个功能封装到了一个包里:

  • 字符过滤器

    首先,字符串按顺序通过每个 字符过滤器 。他们的任务是在分词前整理字符串。一个字符过滤器可以用来去掉 HTML,或者将 & 转化成 and

  • 分词器

    其次,字符串被 分词器 分为单个的词条。一个简单的分词器遇到空格和标点的时候,可能会将文本拆分成词条。

  • Token 过滤器

    最后,词条按顺序通过每个 token 过滤器 。这个过程可能会改变词条(例如,小写化 Quick ),删除词条(例如, 像 aandthe 等无用词,我们一般称为停用词,Stop Word),或者增加词条(例如,像 jumpleap 这种同义词)。

内置的分析器

  • Standard analyzer:对于西方语种表现较好,标准分析器
  • Simple analyzer:在任何不是字母的地方分隔文本,将词条小写
  • Whitespace analyzer:在空格的地方划分文本
  • Language analyzers:考虑指定语言的特点的分析器

测试分析器

GET /_analyze
{
  "analyzer": "standard",
  "text": "Text to analyze"
}

中文分析器

在本节的最后会介绍一个中文分析器插件 IK Analysis for Elasticsearch:

提供的分析器:

  • ik_smart:将需要分词的文本做最大粒度的拆分。
  • ik_max_word:将需要分词的文本做最小粒度的拆分,尽量分更多的词。

安装:

./elasticsearch-plugin -v install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v8.0.0/elasticsearch-analysis-ik-8.0.0.zip

Type & Mappings

Document 可以分组,比如 weather 这个 Index 里面,可以按城市分组(北京和上海),也可以按气候分组(晴天和雨天)。这种分组就叫做 Type,它是虚拟的逻辑分组,用来过滤 Document。

不同的 Type 应该有相似的结构(schema),举例来说,id 字段不能在这个组是字符串,在另一个组是数值。这是与关系型数据库的表的一个区别。性质完全不同的数据(比如 productslogs)应该存成两个 Index,而不是一个 Index 里面的两个 Type(虽然可以做到)。

类型(Type)由名称(Name)和映射(Mapping)组成。映射,就像数据库中的 schema,描述了文档可能具有的字段或属性以及每个字段的数据类型 —— 比如 stringintegerdate —— 以及 Lucene 是如何索引和存储这些字段的。

映射 Mappings

为了能够将时间域视为时间,数字域视为数字,字符串域视为全文本或精确值字符串, Elasticsearch 需要知道每个域中数据的类型。这个信息包含在映射中。

索引中每个文档都有 类型,每种类型都有它自己的 映射。映射定义了类型中的域,每个域的数据类型,以及 Elasticsearch 如何处理这些域。映射也用于配置与类型有关的元数据。

所有可用的域类型枚举如下:

  • 字符串: string
  • 整数: byte, short, integer, long
  • 浮点数: float, double
  • 布尔型: boolean
  • 日期: date
  • 多层级对象: object

自定义域映射

针对于简单域类型:

{
    "number_of_clicks": {
        "type": "integer", // 指定类型
        
        // analyzed 作为全文本域索引; 
        // not_analyzed 作为精确值索引;
        // no 不索引.
        "index": "not_analyzed", 
        
        "analyzer": "whitespace" // 分析器 默认为 standard

    }
}

针对于多层级对象:

{
  "gb": {
    "tweet": { 
      "properties": {
        "tweet":            { "type": "string" },
        "user": { 
          "type":             "object",
          "properties": {
            "id":           { "type": "string" },
            "gender":       { "type": "string" },
            "age":          { "type": "long"   },
            "name":   { 
              "type":         "object",
              "properties": {
                "full":     { "type": "string" },
                "first":    { "type": "string" },
                "last":     { "type": "string" }
              }
            }
          }
        }
      }
    }
  }
}

事实上这将被索引为:

{
    "tweet":            [elasticsearch, flexible, very],
    "user.id":          [@johnsmith],
    "user.gender":      [male],
    "user.age":         [26],
    "user.name.full":   [john, smith],
    "user.name.first":  [john],
    "user.name.last":   [smith]
}

总结起来,我们可以在 [PUT] /my_index 时使用:

{
  "mappings": {
    "tweet" : { // 类型名
      "properties" : {
        "tweet" : {
          "type" :    "string",
          "analyzer": "english"
        },
        "date" : {
          "type" :   "date"
        },
        "name" : {
          "type" :   "string"
        },
        "user_id" : {
          "type" :   "long"
        }
      }
    }
  }
}

数据操作

新增记录

  • [PUT] /Index/Type/Id

比如,向/accounts/person发送请求,就可以新增一条人员记录。服务器返回的 JSON 对象,会给出 Index、Type、Id、Version 等信息。

{
  "_index":"accounts",
  "_type":"person",
  "_id":"1",
  "_version":1,
  "result":"created",
  "_shards":{"total":2,"successful":1,"failed":0},
  "created":true
}
  • [POST] /Index/Type

新增记录的时候,也可以不指定 Id,这时要改成 POST 请求。这时,服务器返回的 JSON 对象里面,_id字段就是一个随机字符串。

{
  "_index":"accounts",
  "_type":"person",
  "_id":"AV3qGfrC6jMbsbXb6k1p",
  "_version":1,
  "result":"created",
  "_shards":{"total":2,"successful":1,"failed":0},
  "created":true
}

注意,如果没有先创建 Index(这个例子是accounts),直接执行上面的命令,Elastic 也不会报错,而是直接生成指定的 Index。

查看记录

  • [GET] /Index/Type/Id
$ curl 'localhost:9200/accounts/person/1?pretty=true'

上面代码请求查看/accounts/person/1这条记录,URL 的参数pretty=true表示以易读的格式返回。

返回的数据中,found字段表示查询成功,_source字段返回原始记录。

{
  "_index" : "accounts",
  "_type" : "person",
  "_id" : "1",
  "_version" : 1,
  "found" : true,
  "_source" : {
    "user" : "张三",
    "title" : "工程师",
    "desc" : "数据库管理"
  }
}

如果 Id 不正确,就查不到数据,found字段就是false

$ curl 'localhost:9200/weather/beijing/abc?pretty=true'

{
  "_index" : "accounts",
  "_type" : "person",
  "_id" : "abc",
  "found" : false
}

更新记录

  • [PUT] /Index/Type/Id

更新记录就是使用 PUT 请求,重新发送一次数据。比如我们将原始数据从”数据库管理”改成”数据库管理,软件开发”。 返回结果里面,有几个字段发生了变化。

"_version" : 2,
"result" : "updated",
"created" : false

可以看到,记录的 Id 没变,但是版本(version)从 1 变成 2,操作类型(result)从 created 变成 updatedcreated 字段变成 false,因为这次不是新建记录。

部分更新记录

使用 update API 我们还可以部分更新文档,例如在某个请求时对计数器进行累加。

  • [POST] /Index/Type/Id/_update

update 请求最简单的一种形式是接收文档的一部分作为 doc 的参数, 它只是与现有的文档进行合并。对象被合并到一起,覆盖现有的字段,增加新的字段。 例如,我们增加字段 tagsviews 到我们的博客文章:

POST /website/blog/1/_update
{
   "doc" : {
      "tags" : [ "testing" ],
      "views": 0
   }
}

如果请求成功,我们看到类似于 index 请求的响应:

{
   "_index" :   "website",
   "_id" :      "1",
   "_type" :    "blog",
   "_version" : 3
}

删除记录

  • [DELETE] /Index/Type/Id

取回多个文档

将多个请求合并成一个,避免单独处理每个请求花费的网络延时和开销。如果你需要从 Elasticsearch 检索很多文档,那么使用 mget API 来将这些检索请求放在一个请求中,将比逐个文档请求更快地检索到全部文档。每个文档都是单独检索和报告的。要想知道请求数组中某个请求是否确实找到,检查 found 字段。

GET /_mget
{
   "docs" : [
      {
         "_index" : "website",
         "_type" :  "blog",
         "_id" :    2
      },
      {
         "_index" : "website",
         "_type" :  "pageviews",
         "_id" :    1,
         "_source": "views"
      }
   ]
}

返回值:

{
   "docs" : [
      {
         "_index" :   "website",
         "_id" :      "2",
         "_type" :    "blog",
         "found" :    true,
         "_source" : {
            "text" :  "This is a piece of cake...",
            "title" : "My first external blog entry"
         },
         "_version" : 10
      },
      {
         "_index" :   "website",
         "_id" :      "1",
         "_type" :    "pageviews",
         "found" :    true,
         "_version" : 2,
         "_source" : {
            "views" : 2
         }
      }
   ]
}

批量操作

mget 可以使我们一次取回多个文档同样的方式, bulk API 允许在单个步骤中进行多次 createindexupdatedelete 请求。 如果你需要索引一个数据流比如日志事件,它可以排队和索引数百或数千批次。

bulk 与其他的请求体格式稍有不同,如下所示:

{ action: { metadata }}\n
{ request body1        }\n
{ action: { metadata }}\n
{ request body2        }\n
...

这种格式就是每行写一个 JSON 格式的数据,通过换行符(\n)连接到一起。注意两个要点:

  • 每行一定要以换行符(\n)结尾,包括最后一行。这些换行符被用作一个标记,可以有效分隔行。
  • 这些行不能包含未转义的换行符,因为他们将会对解析造成干扰。这意味着这个 JSON不能使用 pretty 参数打印。

action 必须是以下选项之一:

  • create:如果文档不存在,那么就创建它。
  • index:创建一个新文档或者替换一个现有的文档。
  • update:部分更新一个文档。
  • delete:删除一个文档。不需要再另附一行请求体。

metadata 应该指定被索引、创建、更新或者删除的文档的 _index_type_id

举个例子:

POST /_bulk
{ "delete": { "_index": "website", "_type": "blog", "_id": "123" }} 
{ "create": { "_index": "website", "_type": "blog", "_id": "123" }}
{ "title":    "My first blog post" }
{ "index":  { "_index": "website", "_type": "blog" }}
{ "title":    "My second blog post" }
{ "update": { "_index": "website", "_type": "blog", "_id": "123", "_retry_on_conflict" : 3} }
{ "doc" : {"title" : "My updated blog post"} } 

返回值:

{
   "took": 4,
   "errors": false, 
   "items": [
      {  "delete": {
            "_index":   "website",
            "_type":    "blog",
            "_id":      "123",
            "_version": 2,
            "status":   200,
            "found":    true
      }},
      {  "create": {
            "_index":   "website",
            "_type":    "blog",
            "_id":      "123",
            "_version": 3,
            "status":   201
      }},
      {  "create": {
            "_index":   "website",
            "_type":    "blog",
            "_id":      "EiwfApScQiiy7TIKFxRCTw",
            "_version": 1,
            "status":   201
      }},
      {  "update": {
            "_index":   "website",
            "_type":    "blog",
            "_id":      "123",
            "_version": 4,
            "status":   200
      }}
   ]
}

批量大小选择:一个好的办法是开始时将 1,000 到 5,000 个文档作为一个批次, 如果你的文档非常大,那么就减少批量的文档个数。一个好的批量大小在开始处理后所占用的物理大小约为 5-15 MB

数据查询

返回所有记录

  • [GET] /Index/Type/_search
{
  "took":2,
  "timed_out":false,
  "_shards":{"total":5,"successful":5,"failed":0},
  "hits":{
    "total":2,
    "max_score":1.0,
    "hits":[
      {
        "_index":"accounts",
        "_type":"person",
        "_id":"AV3qGfrC6jMbsbXb6k1p",
        "_score":1.0,
        "_source": {
          "user": "李四",
          "title": "工程师",
          "desc": "系统管理"
        }
      },
      {
        "_index":"accounts",
        "_type":"person",
        "_id":"1",
        "_score":1.0,
        "_source": {
          "user" : "张三",
          "title" : "工程师",
          "desc" : "数据库管理,软件开发"
        }
      }
    ]
  }
}

上面代码中,返回结果的 took 字段表示该操作的耗时(单位为毫秒),timed_out 字段表示是否超时,hits 字段表示命中的记录,里面子字段的含义如下。

  • total:返回记录数,本例是 2 条。
  • max_score:最高的匹配程度,本例是 1.0
  • hits:返回的记录组成的数组。

返回的记录中,每条记录都有一个_score字段,表示匹配的程序,默认是按照这个字段降序排列。

分页

  • [GET] /Index/Type?size=10&from=10

分页实现的逻辑:

当我们请求结果的第一页(结果从 1 到 10),每一个分片产生前 10 的结果,并且返回给 协调节点 ,协调节点对 50 个结果排序得到全部结果的前 10 个。

现在假设我们请求第 1000 页 —— 结果从 10001 到 10010。所有都以相同的方式工作除了每个分片不得不产生前 10010 个结果以外。然后协调节点对全部 50050 个结果排序最后丢弃掉这些结果中的 50040 个结果。

可以看到,在分布式系统中,对结果排序的成本随分页的深度成指数上升。这就是 Web 搜索引擎对任何查询都不要返回超过 1000 个结果的原因。

搜索 API:轻量版

有两种形式的 搜索 API:一种是 “轻量的” 查询字符串 版本,要求在查询字符串中传递所有的参数,另一种是更完整的 请求体 版本,要求使用 JSON 格式和更丰富的查询表达式作为搜索语言。

name 字段中包含 john 并且在 tweet 字段中包含 mary 的文档。实际的查询就是这样:

+name:john +tweet:mary

但是查询字符串参数所需要的 URL 编码实际上更加难懂:

[GET] /_search?q=%2Bname%3Ajohn+%2Btweet%3Amary

下面的查询使用以下的条件:

  • name 字段中包含 mary 或者 john
  • date 值大于 2014-09-10
  • _all 字段包含 aggregations 或者 geo(除非设置特定字段,否则查询字符串就使用 _all 字段进行搜索)
+name:(mary john) +date:>2014-09-10 +(aggregations geo)

从之前的例子中可以看出,这种 轻量 的查询字符串搜索效果还是挺让人惊喜的。 它的查询语法在相关参考文档中有详细解释,以便简洁的表达很复杂的查询。对于通过命令做一次性查询,或者是在开发阶段,都非常方便。

但同时也可以看到,这种精简让调试更加晦涩和困难。而且很脆弱,一些查询字符串中很小的语法错误,像 -:/ 或者 " 不匹配等,将会返回错误而不是搜索结果。

最后,查询字符串搜索允许任何用户在索引的任意字段上执行可能较慢且重量级的查询,这可能会暴露隐私信息,甚至将集群拖垮。

相反,我们经常在生产环境中更多地使用功能全面的 request body 查询 API,除了能完成以上所有功能,还有一些附加功能。

搜索 API:请求体

返回所有记录与分页

[GET/POST] /Index/Type/_search

{
  "from": 30,
  "size": 10
}
查询语句

查询语句(Query clauses)就像一些简单的组合块,这些组合块可以彼此之间合并组成更复杂的查询。这些语句可以是如下形式:

  • 叶子语句(Leaf clauses)(比如 match 语句) 被用于将查询字符串和一个字段(或者多个字段)对比。
  • 复合(Compound) 语句主要用于合并其它查询语句。 比如,一个 bool 语句允许在你需要的时候组合其它语句,无论是 must 匹配、 must_not 匹配还是 should 匹配,同时它可以包含不评分的过滤器(filters):
{
    "bool": {
        "must":     { "match": { "tweet": "elasticsearch" }},
        "must_not": { "match": { "name":  "mary" }},
        "should":   { "match": { "tweet": "full text" }},
        "filter":   { "range": { "age" : { "gt" : 30 }} }
    }
}
查询与过滤

我们的搜索一般有以下两种情况:过滤情况(filtering context)和查询情况(query context)。

当使用于 过滤情况 时,查询被设置成一个“不评分”或者“过滤”查询。即,这个查询只是简单的问一个问题:“这篇文档是否匹配?”。回答也是非常的简单,yes 或者 no ,二者必居其一。

当使用于 查询情况 时,查询就变成了一个“评分”的查询。和不评分的查询类似,也要去判断这个文档是否匹配,同时它还需要判断这个文档匹配的有 多好(匹配程度如何)。一个评分查询计算每一个文档与此查询的 相关程度,同时将这个相关程度分配给表示相关性的字段 _score,并且按照相关性对匹配到的文档进行排序。这种相关性的概念是非常适合全文搜索的情况,因为全文搜索几乎没有完全 “正确” 的答案。

在一般情况下,一个 filter 会比一个 query 性能更优异,并且每次都表现的很稳定。

通常的规则是,使用查询(query)语句来进行 全文 搜索或者其它任何需要影响 相关性得分 的搜索。除此以外的情况都使用过滤(filters)。

Leaf Clauses
  • match_all

match_all 查询简单的匹配所有文档。在没有指定查询方式时,它是默认的查询:

{ "match_all": {}}

它经常与 filter 结合使用—例如,检索收件箱里的所有邮件。所有邮件被认为具有相同的相关性,所以都将获得分值为 1 的中性 _score

  • match

无论你在任何字段上进行的是全文搜索还是精确查询,match 查询是你可用的标准查询。如果你在一个全文字段上使用 match 查询,在执行查询前,它将用正确的分析器去分析查询字符串。也就是说,如果在一个精确值的字段上使用它,例如数字、日期、布尔或者一个 not_analyzed 字符串字段,那么它将会精确匹配给定的值。

{ "match": { "tweet": "About Search" }}
  • multi_match

multi_match 查询可以在多个字段上执行相同的 match 查询:

{
    "multi_match": {
        "query":    "full text search",
        "fields":   [ "title", "body" ]
    }
}
  • range

range 查询找出那些落在指定区间内的数字或者时间:

{
    "range": {
        "age": {
            "gte":  20,
            "lt":   30
        }
    }
}

被允许的操作符如下:

  • gt:大于
  • gte:大于等于
  • lt:小于
  • lte:小于等于
  • term

term 查询被用于精确值匹配,这些精确值可能是数字、时间、布尔或者那些 not_analyzed 的字符串:

{ "term": { "age":    26           }}
{ "term": { "date":   "2014-09-01" }}
{ "term": { "public": true         }}
{ "term": { "tag":    "full_text"  }}

term 查询对于输入的文本不分析,所以它将给定的值进行精确查询。

  • terms

terms 查询和 term 查询一样,但它允许你指定多值进行匹配。如果这个字段包含了指定值中的任何一个值,那么这个文档满足条件:

{ "terms": { "tag": [ "search", "full_text", "nosql" ] }}
  • exists / missing

exists 查询和 missing 查询被用于查找那些指定字段中有值 (exists) 或无值 (missing) 的文档。这与SQL中的 IS_NULL (missing) 和 NOT IS_NULL (exists) 在本质上具有共性:

{
    "exists":   {
        "field":    "title"
    }
}
Compound Clauses

现实的查询需求从来都没有那么简单;它们需要在多个字段上查询多种多样的文本,并且根据一系列的标准来过滤。为了构建类似的高级查询,你需要一种能够将多查询组合成单一查询的查询方法。

你可以用 bool 查询来实现你的需求。这种查询将多查询组合在一起,成为用户自己想要的布尔查询。它接收以下参数:

  • must:文档 必须 匹配这些条件才能被包含进来。
  • must_not:文档 必须不 匹配这些条件才能被包含进来。
  • should:如果满足这些语句中的任意语句,将增加 _score ,否则,无任何影响。它们主要用于修正每个文档的相关性得分。
  • filter必须 匹配,但它以不评分、过滤模式来进行。这些语句对评分没有贡献,只是根据过滤标准来排除或包含文档。

例子:

{
    "bool": {
        "must":     { "match": { "title": "how to make millions" }},
        "must_not": { "match": { "tag":   "spam" }},
        "should": [
            { "match": { "tag": "starred" }}
        ],
        "filter": {
          "bool": { 
              "must": [
                  { "range": { "date": { "gte": "2014-01-01" }}},
                  { "range": { "price": { "lte": 29.99 }}}
              ],
              "must_not": [
                  { "term": { "category": "ebooks" }}
              ]
          }
        }
    }
}
{
  "query": {
    "bool": {
      "must": [
        { "match": { "desc": "软件" } },
        { "match": { "desc": "系统" } }
      ]
    }
  }
}

网站搭建纪实:前端

没啥好讲的,用的是自己最熟悉的 React+Redux 框架半天肝了个 Prototype…

嘛嘛,反正前端不是主要部分啦x

参考链接