Will Dx

人世一身霜雪, 归来仍是少年.

Elasticsearch权威指南-读书笔记

Posted April 07, 2017

0.署名出处

本文章仅供个人学习使用, 主要是自己的一份读书笔记, 最优的选择是阅读官方文档, 链接如下:

署名出处

1.前言

世界已然被数据淹没, 我们将大量的数据存储在结构化的数据库里,或者noSQL中. 当然,应用能够正常运行, 但是当我们需要去分析这些数据来做一些决策的时候就不知所措了...

所以此时, 省时省力省钱的方法是对接第三方平台, 比如说友盟

#友盟产品图解

等....

但是毕竟不能帮你做所有的事情, 除非你把数据都提供给他!

Elasticsearch 是一个分布式、可扩展、实时的搜索与数据分析引擎。 它能从项目一开始就赋予你的数据以搜索、分析和探索的能力; 全文搜索 + 结构化数据实时统计相结合, 能解决你大部分的分析运营需求;

当然, Elasticsearch 不仅仅只是全文搜索, 它还支持结构化搜索数据分析复杂的语言处理地理位置和对象间关联关系等, 甚至我们可以数据建模来充分利用 Elasticsearch 的水平伸缩性;

我们常用 ELK stack 来做日志分析(当然, 我们公司也在用), 所以我在想是否能更深入一些, 做一些业务分析(主要是用户行为分析), 来满足一些运营提供的需求(人生苦短, 长痛不如短痛!), 那么, JUST Do it!

2.Elasticsearch导航

1.你知道的, 为了搜索章节 到 分片内部原理

Raw
主要介绍Elastic的数据输入输出, 如何处理你的文档数据, 如何进行基本的搜索操作, 如何管理你的索引;
附加章节:(理论)
集群内的原理,分布式文档存储,执行分布式检索,分片内部原理
  1. 结构化搜索控制相关度

    深入搜索, 如何索引和查询, 如何利用一些高级特性(例如: 邻近词[word proximity] 和 部分匹配[partial matching]); 了解相关度评分是如何工作的以及如何控制它来确保第一页总是返回最佳的搜索结果;

  2. 开始处理各种语言拼写错误

    解决如何有效使用分析器查询器来处理语言的头痛问题. 例如, 排序,词干提取,停用词,同义词,模糊匹配.

  3. 高阶概念Doc Values and Fielddata

    讨论聚合(aggregations)和分析, 对你的数据进行摘要化分组来呈现整体趋势

  4. 地理坐标点地理形状

    介绍 Elasticsearch 支持的两种地理位置检索方式:经纬坐标点复杂的地理形状(geo-shapes)

  5. 关联关系处理扩容设计

    如何为你的数据建模来高效使用 Elasticsearch。在搜索引擎里表达实体间的关系可能不是那么容易,因为它不是用来设计做这个的。这些章节还会阐述如何设计索引来匹配你系统中的数据流

  6. 监控部署后

    讨论生产环境上线的重要配置、监控点以及如何诊断以避免出现问题

3. 在线资源

Elasticsearch Guide

Elasticsearch 参考手册

英文社区 中文社区

4.基础入门

4.1 你知道的, 为了搜索…

Elasticsearch 是一个开源的搜索引擎,建立在一个全文搜索引擎库 Apache Lucene™基础之上。 Lucene 可以说是当下最先进、高性能、全功能的搜索引擎库--无论是开源还是私有.

Elasticsearch 使用 Java 编写, 内部使用 Lucene 做索引与搜索, 提供一套简单一致的 RESTful API 去隐藏 Lucene 的复杂性, 最终提供简单,易用,可扩展,功能强大的实时搜索引擎;

开源协议: Apache 2 license 源码地址: github.com/elastic/elasticsearch 贡献者社区: Contributing to Elasticsearch 讨论组: discuss.elastic.co

4.2 安装并运行Elasticsearch

官方安装文档部分

有几点需要注意:

1.Elasticsearch依赖较新的java版本, 最好的方式是从 www.java.com 获得官方提供的最新版本的 Java

2.启动

Python
cd elasticsearch-<version>
./bin/elasticsearch -d
# -d 表示守护进程运行

3.测试 Elasticsearch 是否启动成功

Python
curl 'http://localhost:9200/?pretty'

{
  "name" : "Tom Foster",
  "cluster_name" : "elasticsearch",
  "version" : {
    "number" : "2.1.0",
    "build_hash" : "72cd1f1a3eee09505e036106146dc1949dc5dc87",
    "build_timestamp" : "2015-11-18T22:40:03Z",
    "build_snapshot" : false,
    "lucene_version" : "5.3.1"
  },
  "tagline" : "You Know, for Search"
}

4.3 和Elasticsearch交互

4.3.1 客户端类型

Elasticsearch客户端

节点客户端(Node client):

节点客户端作为一个非数据节点加入到本地集群中。换句话说,它本身不保存任何数据,但是它知道数据在集群中的哪个节点中,并且可以把请求转发到正确的节点。

传输客户端(Transport client):

轻量级的传输客户端可以将请求发送到远程集群。它本身不加入集群,但是它可以将请求转发到集群中的一个节点上。

注意: 集群中的节点通过端口 9300 彼此通信。如果这个端口没有打开,节点将无法形成一个集群

4.3.2 RESTFul API

Python
curl -X<VERB> '<PROTOCOL>://<HOST>:<PORT>/<PATH>?<QUERY_STRING>' -d '<BODY>'

计算集群中文档的数量:
curl -i -XGET 'http://localhost:9200/_count?pretty' -d '
{
    "query": {
        "match_all": {}
    }
}'
参数 描述
VERB 适当的 HTTP 方法 或 谓词 : GETPOSTPUTHEAD 或者 DELETE
PROTOCOL http 或者 https(如果你在 Elasticsearch 前面有一个https 代理)
HOST Elasticsearch 集群中任意节点的主机名,或者用 localhost 代表本地机器上的节点。
PORT 运行 Elasticsearch HTTP 服务的端口号,默认是 9200
PATH API 的终端路径(例如 count 将返回集群中文档数量)。Path 可能包含多个组件,例如:cluster/stats 和 _nodes/stats/jvm
QUERY_STRING 任意可选的查询字符串参数 (例如 ?pretty 将格式化地输出 JSON 返回值,使其更容易阅读)
BODY 一个 JSON 格式的请求体 (如果请求需要的话)

4.3.3 面向文档

假设有一个包含了其他对象或者数组的很复杂的对象(非简单key-value), 如果你想这个对象存储在关系型数据库中. 那意味着你要把这个对象进行拆分, 一个字段>对应一列, 以适应表结构; 当你存储到数据库中之后, 你又不得不用各种关联重新把对象构造到一起;

Elasticsearch 是面向文档的,意味着它存储整个对象或文档_。Elasticsearch 不仅存储文档,而且 _索引 每个文档的内容使之可以被检索。在 Elasticsearch 中,你 对文档进行索引、检索、排序和过滤--而不是对行列数据。这是一种完全不同的思考数据的方式,也是 Elasticsearch 能支持复杂全文检索的原因。

Elasticsearch 使用JavaScript Object Notation 或者 JSON 作为文档的序列化格式; 建议把对象格式化为JSON之后导入Elassearch;

4.3.4 索引雇员文档

我们受雇于 Megacorp 公司,作为 HR 部门新的 “热爱无人机” (We love our drones!)激励项目的一部分,我们的任务是为此创建一个雇员目录。该目录应当能培养雇员认同感及支持实时、高效、动态协作,因此有一些业务需求:

需求:

  1. 支持包含多值标签、数值、以及全文本的数据
  2. 检索任一雇员的完整信息
  3. 允许结构化搜索,比如查询 30 岁以上的员工
  4. 允许简单的全文搜索以及较复杂的短语搜索
  5. 支持在匹配文档内容中高亮显示搜索片段
  6. 支持基于数据创建和管理分析仪表盘

科普

1.存储数据到Elasticsearch的行为叫做 索引 2.一个文档就是一个雇员对象 3.Elastic集群-多个索引 4.个索引包含多个类型 5.不同的类型存储着多个文档 6.每个文档有多个属性

Elastic文档存储图解

索引在Elastic语境中的多种含义

不同索引 描述
索引(名词) 一个 索引 类似于传统关系数据库中的一个 数据库 ,是一个存储关系型文档的地方。 索引 (index) 的复数词为 indices 或 indexes
索引(动词) 索引一个文档 就是存储一个文档到一个 索引 (名词)中以便它可以被检索和查询到。这非常类似于 SQL 语句中的 INSERT 关键词,除了文档已存在时新文档会替换旧文档情况之外
倒排索引 关系型数据库通过增加一个 索引 比如一个 B树(B-tree)索引 到指定的列上,以便提升数据检索速度。Elasticsearch 和 Lucene 使用了一个叫做 倒排索引 的结构来达到相同的目的

默认的,一个文档中的每一个属性都是 被索引 的(有一个倒排索引)和可搜索的。一个没有倒排索引的属性是不能被搜索到的。

添加文档

#创建索引-添加类型-添加文档

参数 描述
megacorp 索引名称
employee 类型名称
1 特定雇员的ID

再添加2个文档

4.3.5 检索雇员文档

_source 属性,内容是 John Smith 雇员的原始 JSON 文档

4.3.6 轻量搜索

GET操作根据ID直接获取到置顶的文档; 接下来试试, 简单的搜索功能!

1. 搜索所有

Python
GET /megacorp/employee/_search
# 一个搜索默认返回十条结果
# 返回
{
  "took": 3,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "failed": 0
  },
  "hits": {
    "total": 2,
    "max_score": 1,
    "hits": [
      {
        "_index": "megacorp",
        "_type": "employee",
        "_id": "2",
        "_score": 1,
        "_source": {
          "first_name": "Jane",
          "last_name": "Smith",
          "age": 32,
          "about": "I like to collect rock albums",
          "interests": [
            "music"
          ]
        }
      },
      {
        "_index": "megacorp",
        "_type": "employee",
        "_id": "1",
        "_score": 1,
        "_source": {
          "first_name": "John",
          "last_name": "Smith",
          "age": 25,
          "about": "I love to go rock climbing",
          "interests": [
            "sports",
            "music"
          ]
        }
      }
    ]
  }
}

1. 带条件的搜索

Query-string 搜索通过命令非常方便地进行临时性的搜索, 但是, 它有其局限性

轻量搜索

Python
GET /megacorp/employee/_search?q=last_name:Smith

4.3.7 使用DSL查询表达式搜索

领域特定语言(DSL)

返回结果与之前的查询一样,但还是可以看到有一些变化。其中之一是,不再使用 query-string 参数,而是一个请求体替代。这个请求使用 JSON 构造,并使用了一个 match 查询(属于查询类型之一,后续将会了解)

4.3.8 更复杂的搜索

Python
**多个条件**
GET /megacorp/employee/_search
{
    "query" : {
        "bool": {
            "must": {
                "match" : {
                    "last_name" : "smith" 
                }
            },
            "filter": {
                "range" : {
                    "age" : { "gt" : 30 } 
                }
            }
        }
    }
}

4.3.9 全文搜索-传统数据库很难搞定

Python
GET /megacorp/employee/_search
{
    "query" : {
        "match" : {
            "about" : "rock climbing"
        }
    }
}

相关性得分

Elasticsearch 默认按照相关性得分排序,即每个文档跟查询的匹配程度, 匹配程度越高的在前面

4.3.10 短语搜索

Python
# match_phrase: 全文搜索匹配短语的结果
GET /megacorp/employee/_search
{
    "query" : {
        "match_phrase" : {
            "about" : "rock climbing"
        }
    }
}

4.3.11 高亮搜索

Python
GET /megacorp/employee/_search
{
    "query" : {
        "match_phrase" : {
            "about" : "rock climbing"
        }
    },
    "highlight": {
        "fields" : {
            "about" : {}
        }
    }
}

返回:
{
  "took": 8,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "failed": 0
  },
  "hits": {
    "total": 1,
    "max_score": 0.53484553,
    "hits": [
      {
        "_index": "megacorp",
        "_type": "employee",
        "_id": "1",
        "_score": 0.53484553,
        "_source": {
          "first_name": "John",
          "last_name": "Smith",
          "age": 25,
          "about": "I love to go rock climbing",
          "interests": [
            "sports",
            "music"
          ]
        },
        "highlight": {
          "about": [
            "I love to go <em>rock</em> <em>climbing</em>"
          ]
        }
      }
    ]
  }
}
# 其中highlight.about部分是高亮匹配的内容

更多关于高亮片段

4.3.12 聚合分析

Elasticsearch 有一个功能叫聚合(aggregations),允许我们基于数据生成一些精细的分析结果。聚合与 SQL 中的 GROUP BY 类似但更强大

挖掘出雇员中最受欢迎的兴趣爱好

Python
GET /megacorp/employee/_search
{
  "aggs": {
    "all_interests": {
      "terms": { "field": "interests" }
    }
  }
}

illegalargumentexception报错解决

#illegal_argument_exception报错解决

普通查询+聚合=组合查询

Python
GET /megacorp/employee/_search
{
  "query": {
    "match": {
      "last_name": "smith"
    }
  },
  "aggs": {
    "all_interests": {
      "terms": {
        "field": "interests"
      }
    }
  }
}

分级汇总-特定兴趣爱好员工的平均年龄

Python
GET /megacorp/employee/_search
{
    "aggs" : {
        "all_interests" : {
            "terms" : { "field" : "interests" },
            "aggs" : {
                "avg_age" : {
                    "avg" : { "field" : "age" }
                }
            }
        }
    }
}

# 返回:
{
  "took": 28,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "failed": 0
  },
  "hits": { },
  "aggregations": {
    "all_interests": {
      "doc_count_error_upper_bound": 0,
      "sum_other_doc_count": 0,
      "buckets": [
        {
          "key": "music",
          "doc_count": 2,
          "avg_age": {
            "value": 28.5
          }
        },
        {
          "key": "sports",
          "doc_count": 1,
          "avg_age": {
            "value": 25
          }
        }
      ]
    }
  }
}

4.4 教程结语

读完基础入门感觉ELK简直逆天, 后续还有更多高级功能以满足不同场景的数据分析需求, 例如:suggestions、geolocation、percolation、fuzzy 与 partial matching 等特性

另外,如何组合你的数据(难道这就是数据建模?)达到最佳搜索效果, 是个比较复杂的事情, 需要既了解ELK的特性, 还需要了解业务, 并且大量测试才能达到想要的效果;

革命尚未成功, 同志仍需努力!

4.5 分布式特性

ELK天生就是分布式的, 只需要满足3个条件:

1.网络和端口连通性正常 2.配置同样的cluster.name 3.设定单播地址,默认9300端口

详情参见-Elasticsearch重要配置的修改

Elasticsearch 尽可能地屏蔽了分布式系统的复杂性, 一些操作会在后台自动执行:

  1. 分配文档到不同的容器 或 分片 中,文档可以储存在一个或多个节点中
  2. 按集群节点来均衡分配这些分片,从而对索引和搜索过程进行负载均衡
  3. 复制每个分片以支持数据冗余,从而防止硬件故障导致的数据丢失
  4. 将集群中任一节点的请求路由到存有相关数据的节点
  5. 集群扩容时无缝整合新节点,重新分配分片以便从离群节点恢复

master收集到日志后,会把一部分数据碎片到salve上(随机的一部分数据), master和slave又都会各自做副本,并把副本放到对方机器上,这样就保证了数据不会丢失

5.深入搜索

5.1 结构化搜索

搜索具有内在结构数据的过程, 叫做结构化搜索;例如: 比较数据或时间的范围 或 判断两个值的大小;

文本也可以是结构化的;

结构化查询得到的结果总是True/False, 不关心相关度评分;

5.1.1 精确值查找

精确值查找使用, 过滤器(filters); 过滤器不会执行相关度计算, 并且容易被缓存;

在 Elasticsearch 的查询表达式(query DSL)中,我们可以使用 term 查询来 实现 精确值查找;

5.1.2 term查询数字

Python
- 插入一些测试数据
curl -XPOST 'localhost:9200/my_store/products/_bulk?pretty' -H 'Content-Type: application/json' -d'
{ "index": { "_id": 1 }}
{ "price" : 10, "productID" : "XHDK-A-1293-#fJ3" }
{ "index": { "_id": 2 }}
{ "price" : 20, "productID" : "KDKE-B-9947-#kL5" }
{ "index": { "_id": 3 }}
{ "price" : 30, "productID" : "JODL-X-1937-#pV7" }
{ "index": { "_id": 4 }}
{ "price" : 30, "productID" : "QQPX-R-3956-#aD8" }
'
Python
- 使用SQL
SELECT document
FROM   products
WHERE  price = 20
Python
- 使用查询表达式(query DSL)的 ` term查询`

GET /my_store/products/_search

{
    "term" : {
        "price" : 20
    }
}

通常当查找一个精确值的时候,我们不希望对查询进行评分计算。只希望对文档进行包括或排除的计算,所以我们会使用 constant_score查询非评分模式来执行 term 查询并以1作为统一评分;

Python
GET /my_store/products/_search
{
    "query" : {
        # 我们用 constant_score 将 term 查询转化成为过滤器
        "constant_score" : {  
            "filter" : {
                # term 精确查询
                "term" : { 
                    "price" : 20
                }
            }
        }
    }
}

5.1.3 term 查询文本编辑

Python
- SQL

SELECT product
FROM   products
WHERE  productID = "XHDK-A-1293-#fJ3"

- 查询表达式(query DSL)- term查询
- 注意: term是包含的关系,并不是等于的意思

GET /my_store/products/_search
{
    "query" : {
        "constant_score" : {
            "filter" : {
                "term" : {
                    "productID" : "XHDK-A-1293-#fJ3"
                }
            }
        }
    }
}

- 发现无法查询到预想的结果

{
  "took": 3,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "failed": 0
  },
  "hits": {
    "total": 0,
    "max_score": null,
    "hits": [

    ]
  }
}

- 为什么呢?问题不在 term 查询,而在于索引数据的方式。 如果我们使用 analyze API (分析 API),我们可以看到这里的 UPC 码被拆分成多个更小的 token:

GET /my_store/_analyze
{
  "field": "productID",
  "text": "XHDK-A-1293-#fJ3"
}

返回:
{
  "tokens": [
    {
      "token": "xhdk",
      "start_offset": 0,
      "end_offset": 4,
      "type": "<ALPHANUM>",
      "position": 0
    },
    {
      "token": "a",
      "start_offset": 5,
      "end_offset": 6,
      "type": "<ALPHANUM>",
      "position": 1
    },
    {
      "token": "1293",
      "start_offset": 7,
      "end_offset": 11,
      "type": "<NUM>",
      "position": 2
    },
    {
      "token": "fj3",
      "start_offset": 13,
      "end_offset": 16,
      "type": "<ALPHANUM>",
      "position": 3
    }
  ]
}

从结果中我们发现:
1. Elasticsearch  4 个不同的 token 而不是单个 token 来表示这个 UPC 
2. 所有字母都是小写的
3. 丢失了连字符和哈希符 # 

- 所以当我们用 term 查询查找精确值 XHDK-A-1293-#fJ3 的时候,找不到任何文档,因为它并不在我们的倒排索引中,正如前面呈现出的分析结果,索引里有四个 token; 
- 我们对文本进行精确查询的时候, 需要告诉Elasticsearch该字段具有精确值, 要将其设置成 not_analyzed 无需分析的;
-为了修正搜索结果,我们需要首先删除旧索引(因为它的映射不再正确)然后创建一个能正确映射的新的索引;

> 删除索引是必须的,因为我们不能更新已存在的映射
DELETE /my_store
> 在索引被删除后,我们可以创建新的索引并为其指定自定义映射
PUT /my_store 
{
    "mappings" : {
        "products" : {
            "properties" : {
                "productID" : {
                    "type" : "string",
                    # 这里我们告诉 Elasticsearch ,我们不想对 productID 做任何分析
                    "index" : "not_analyzed" 
                }
            }
        }
    }

}

- 再次写入数据, 就可以查询了; not_analyzed 其实是关闭了这个字段的分词了;

- 内部过滤器的操作
在内部, ElasticSearch会在运行非评分查询时执行多个操作:
1. 查找匹配文档
    term 查询在倒排索引中查找 XHDK-A-1293-#fJ3 然后获取包含该 term 的所有文档。本例中,只有文档 1 满足我们要求

2. 创建位集合(bitset)
    过滤器会创建一个 bitset (一个包含 0  1 的数组),它描述了哪个文档会包含该 term 。匹配文档的标志位是 1 。本例中,bitset 的值为 [1,0,0,0] 。在内部,它表示成一个 "roaring bitmap",可以同时对稀疏或密集的集合进行高效编码

3. 迭代位集合(bitset)

    一旦为每个查询生成了 bitsets Elasticsearch 就会循环迭代 bitsets 从而找到满足所有过滤条件的匹配文档的集合。执行顺序是启发式的,但一般来说先迭代稀疏的 bitset (因为它可以排除掉大量的文档)
    
4. 增量使用计数

    Elasticsearch 能够缓存非评分查询从而获取更快的访问,但是它也会不太聪明地缓存一些使用极少的东西。非评分计算因为倒排索引已经足够快了,所以我们只想缓存那些我们 知道 在将来会被再次使用的查询,以避免资源的浪费

    理论上非评分查询 先于 评分查询执行。非评分查询任务旨在降低那些将对评分查询计算带来更高成本的文档数量,从而达到快速搜索的目的。

5.1.4 组合过滤器

怎样用 Elasticsearch 来表达下面的 SQL ?

Python
SELECT product
FROM   products
WHERE  (price = 20 OR productID = "XHDK-A-1293-#fJ3")
  AND  (price != 30)

布尔过滤器

一个 bool 过滤器由三部分组成:

Python
{
   "bool" : {
      "must" :     [], # 所有的语句都 必须(must) 匹配,与 AND 等价
      "should" :   [], # 所有的语句都 不能(must not) 匹配,与 NOT 等价
      "must_not" : [], # 至少有一个语句要匹配,与 OR 等价
   }
}

- bool 过滤器的每个部分都是可选的(例如,我们可以只有一个 must 语句),而且每个部分内部可以只有一个或一组过滤器

匹配上文提到的SQL查询例子:

Python
GET /my_store/products/_search
{
   "query" : {
      # 需要一个 filtered 查询将所有的东西包起来
      "filtered" : { 
         "filter" : {
            "bool" : {
              # should中的term查询条件是 or 的关系
              "should" : [
                 { "term" : {"price" : 20}}, 
                 { "term" : {"productID" : "XHDK-A-1293-#fJ3"}} 
              ],
              # must_not表示非的关系
              "must_not" : {
                 "term" : {"price" : 30} 
              }
           }
         }
      }
   }
}

布尔过滤器支持嵌套:

Raw
一个SQL例子:
SELECT document
FROM   products
WHERE  productID      = "KDKE-B-9947-#kL5"
  OR (     productID = "JODL-X-1937-#pV7"
       AND price     = 30 )
       
转换为bool过滤器:
GET /my_store/products/_search
{
   "query" : {
      "filtered" : {
         "filter" : {
            "bool" : {
              "should" : [
                { "term" : {"productID" : "KDKE-B-9947-#kL5"}}, 
                { "bool" : { 
                  "must" : [
                    { "term" : {"productID" : "JODL-X-1937-#pV7"}}, 
                    { "term" : {"price" : 30}} 
                  ]
                }}
              ]
           }
         }
      }
   }
}

5.1.5 查找多个精确值 terms

与 term 查询一样,也需要将其置入 filter 语句的常量评分查询中使用:

Python
GET /my_store/products/_search
{
    "query" : {
        "constant_score" : {
            "filter" : {
                "terms" : { 
                    "price" : [20, 30]
                }
            }
        }
    }
}

5.1.6 term包含而不是相等

term 和 terms 是 包含(contains)操作,而非 等值(equals)判断

精确相等(term+计数器)

添加标签数量限制, 例如, 匹配search字段,并且只匹配search字段

Python
- 假设现在有一条数据有一个标签, 标签数量为1; 另一个有2个标签, 标签数量为2
{ "tags" : ["search"], "tag_count" : 1 }
{ "tags" : ["search", "open_source"], "tag_count" : 2 }

- DSL可以这么写
GET /my_index/my_type/_search
{
    "query": {
        "constant_score" : {
            "filter" : {
                 "bool" : {
                    "must" : [
                        { "term" : { "tags" : "search" } }, # 精确匹配tags字段
                        { "term" : { "tag_count" : 1 } } # 并且tag子弹的计数器为1
                    ]
                }
            }
        }
    }
}

5.1.7 查找范围

数字范围

Python
SELECT document
FROM   products
WHERE  price BETWEEN 20 AND 40

Elasticsearch 有 range 查询, 不出所料地,可以用它来查找处于某个范围内的文档:

Raw
"range" : {
    "price" : {
        "gte" : 20,
        "lte" : 40
    }
}
  • gt: > 大于(greater than)
  • lt: < 小于(less than)
  • gte: >= 大于或等于(greater than or equal to)
  • lte: <= 小于或等于(less than or equal to)
Python
- 例子:
GET /my_store/products/_search
{
    "query" : {
        "constant_score" : {
            "filter" : {
                "range" : {
                    "price" : {
                        "gte" : 20,
                        "lt"  : 40
                    }
                }
            }
        }
    }
}

- 如果想要范围无界(比方说 >20 ),只须省略其中一边的限制:
"range" : {
    "price" : {
        "gt" : 20
    }
}

日期范围

Python
- 日期范围
"range" : {
    "timestamp" : {
        "gt" : "2014-01-01 00:00:00",
        "lt" : "2014-01-07 00:00:00"
    }
}

- range 查询支持对 日期计算(date math)进行操作,比方说,如果我们想查找时间戳在过去一小时内的所有文档:

- 相对时间
"range" : {
    "timestamp" : {
        "gt" : "now-1h"
    }
}
- 绝对时间
"range" : {
    "timestamp" : {
        "gt" : "2014-01-01 00:00:00",
        "lt" : "2014-01-01 00:00:00||+1M" 
    }
}

时间格式参考文档

字符串范围

range 查询同样可以处理字符串字段, 字符串范围可采用 字典顺序(lexicographically) 或字母顺序(alphabetically)。例如,下面这些字符串是采用字典序(lexicographically)排序的;

在倒排索引中的词项就是采取字典顺序(lexicographically)排列的,这也是字符串范围可以使用这个顺序来确定的原因;

字符串计算比较缓慢;

Python
- 需求:  a  b(不包含b)的字符串
"range" : {
    "title" : {
        "gte" : "a",
        "lt" :  "b"
    }
}

5.1.8 处理Null值

世界并不简单,数据往往会有缺失字段,或有显式的空值或空数组。为了应对这些状况,Elasticsearch 提供了一些工具来处理空或缺失值;

存在查询(IS NOT NULL)

Raw
- 用SQL表示
SELECT tags
FROM   posts
WHERE  tags IS NOT NULL

- 用DSL表示
GET /my_index/posts/_search
{
    "query" : {
        "constant_score" : {
            "filter" : {
                "exists" : { "field" : "tags" }
            }
        }
    }
}

缺失查询(IS NULL)

Python
- SQL
SELECT tags
FROM   posts
WHERE  tags IS NULL

- DSL
GET /my_index/posts/_search
{
    "query" : {
        "constant_score" : {
            "filter": {
                "missing" : { "field" : "tags" }
            }
        }
    }
}

我们可以自定义 占位符(placeholder)

对象上的存在与缺失

exits / missing 条件也可以有mapping一样的工作方式;

例如,需要判断一个用户的first name是否存在

Python
- 文档
 {
    "name" : {
      "first" : "John",
      "last" :  "Smith"
   }
}
- 可以使用name.first 或者 name.last 去查询; 
{
    "exists" : { "field.first" : "name" }
}
- 如果只是用name查询
{
    "exists" : { "field" : "name" }
}
-- 实际执行的是:
{
    "bool": {
        "should": [
            { "exists": { "field": "name.first" }},
            { "exists": { "field": "name.last" }}
        ]
    }
}
如果 first  last 都是空,那么 name 这个命名空间才会被认为不存在

5.1.9 关于缓存

在 Elasticsearch 的较早版本中,默认的行为是缓存一切可以缓存的对象; 有一些缓存并不是特别合理, 比如我有3百万的用户, 每个具体用户ID出现的概率都很小, 为所有用户ID做bitset缓存对性能有较大的影响;

为了解决问题,Elasticsearch 会基于使用频次自动缓存查询。如果一个非评分查询在最近的 256 词查询中被使用过(次数取决于查询类型),那么这个查询就会作为缓存的候选。但是,并不是所有的片段都能保证缓存 bitset 。只有那些文档数量超过 10,000 (或超过总文档数量的 3% )才会缓存 bitset 。因为小的片段可以很快的进行搜索和合并,这里缓存的意义不大;

一旦缓存了,非评分计算的 bitset 会一直驻留在缓存中直到它被剔除。剔除规则是基于 LRU 的:一旦缓存满了,最近最少使用的过滤器会被剔除;

5.2 全文搜索

相关性(Relevance):

Raw
它是评价查询与其结果间的相关程度,并根据这种相关程度对结果排名的一种能力,这种计算方式可以是 TF/IDF 方法(参见 相关性的介绍)、地理位置邻近、模糊相似,或其他的某些算法;

分析(Analysis):

Raw
它是将文本块转换为有区别的、规范化的 token 的一个过程,(参见 分析的介绍) 目的是为了(a)创建倒排索引以及(b)查询倒排索引;

5.2.1 全文搜索

  • 数据
Raw
DELETE /my_index 

PUT /my_index
{ "settings": { "number_of_shards": 1 }} 

POST /my_index/my_type/_bulk
{ "index": { "_id": 1 }}
{ "title": "The quick brown fox" }
{ "index": { "_id": 2 }}
{ "title": "The quick brown fox jumps over the lazy dog" }
{ "index": { "_id": 3 }}
{ "title": "The quick brown fox jumps over the quick dog" }
{ "index": { "_id": 4 }}
{ "title": "Brown fox brown dog" }
  • 单个词查询
Python
GET /my_index/my_type/_search
{
    "query": {
        "match": {
            "title": "QUICK!"
        }
    }
}
查询的步骤是:检查字段类型-分析查询字符串-查找匹配文档-为每个文档评分
  • 多词查询
Python
GET /my_index/my_type/_search
{
    "query": {
        "match": {
            "title": "BROWN DOG!"
        }
    }
}
- 任何文档只要 title 字段里包含 指定词项中的至少一个词 就能匹配,被匹配的词项越多,文档就越相关; 这是一种 `or 搜索`, 为了提高精度, 我们可以更改默认的多词匹配模式为`and`;  
GET /my_index/my_type/_search
{
    "query": {
        "match": {
            "title": {      
                "query":    "BROWN DOG!",
                "operator": "and"
            }
        }
    }
}

- 还有一种需求是: 多个匹配项只匹配其中一部分
    match 查询支持 `minimum_should_match 最小匹配参数` 这让我们可以指定必须匹配的词项数用来表示一个文档是否相关。我们可以将其设置为某个具体数字,更常用的做法是将其设置为一个百分数,因为我们无法控制用户搜索时输入的单词数量;
    
GET /my_index/my_type/_search
{
  "query": {
    "match": {
      "title": {
        "query":                "quick brown dog",
        "minimum_should_match": "75%"
      }
    }
  }
}
注意: 这个百分比也不一定完全是你设置的这个数字, minimum_should_match 会做合适的事情,上例子中, 三词项的示例中, 75% 会自动被截断成 66.6% ,即三个里面两个词;

match 的多词查询其实是, 先执行 term 查询,然后将查询的结果合并作为最终结果输出。它将两个 term 查询包入一个 bool 查询中;
  • 组合查询

search也支持bool查询, 和组合过滤器类型, 但有区别:

Raw
过滤器做二元判断:文档是否应该出现在结果中?但查询更精妙,它除了决定一个文档是否应该被包括在结果中,还会计算文档的 相关程度
Python
GET /my_index/my_type/_search
{
  "query": {
    "bool": {
      "must":     { "match": { "title": "quick" }},
      "must_not": { "match": { "title": "lazy"  }},
      "should": [  # 用于计算相关性
                  { "match": { "title": "brown" }},
                  { "match": { "title": "dog"   }}
      ]
    }
  }
}

 - 评分计算

_注释: master not 语句不会影响评分_

 1.bool 查询为每个文档计算相关度评分_sore
 2.再将所有匹配的mustshould语句的分数 _score 求和
 3.最后除以mustshould语句的总数, 得到的记录按评分排序
 
 - 控制精度
 
 所有 must 语句必须匹配,所有 must_not 语句都必须不匹配,但有多少 should 语句应该匹配呢? 默认情况下,没有 should 语句是必须匹配的,只有一个例外:那就是当没有 must 语句的时候,至少有一个 should 语句必须匹配;
 
我们可以通过 minimum_should_match 参数控制需要匹配的 should 语句的数量, 它既可以是一个绝对的数字,又可以是个百分比:

GET /my_index/my_type/_search
{
  "query": {
    "bool": {
      "should": [
        { "match": { "title": "brown" }},
        { "match": { "title": "fox"   }},
        { "match": { "title": "dog"   }}
      ],
      "minimum_should_match": 2 
    }
  }
}
  • 如何使用布尔匹配

多词 match 查询只是简单地将生成的 term 查询包裹 在一个 bool 查询中

Python
{
    "match": { "title": "brown fox"}
}
等价于:
{
  "bool": {
    "should": [
      { "term": { "title": "brown" }},
      { "term": { "title": "fox"   }}
    ]
  }
}
Python
{
    "match": {
        "title": {
            "query":    "brown fox",
            "operator": "and"
        }
    }
}
等价于:
{
  "bool": {
    "must": [
      { "term": { "title": "brown" }},
      { "term": { "title": "fox"   }}
    ]
  }
}
Python
{
    "match": {
        "title": {
            "query": "quick brown fox",
            "minimum_should_match": "75%"
        }
    }
}
等价于:
{
  "bool": {
    "should": [
      { "term": { "title": "brown" }},
      { "term": { "title": "fox"   }},
      { "term": { "title": "quick" }}
    ],
    "minimum_should_match": 2 
  }
}
  • 查询语句提升权重
Python
GET /_search
{
    "query": {
        "bool": {
            "must": {
                "match": {
                    "content": { 
                        "query":    "full text search",
                        # content 字段必须包含 full 、 text 和 search 所有三个词
                        "operator": "and"
                    }
                }
            },
            # 如果 content 字段也包含 Elasticsearch 或 Lucene ,文档会获得更高的评分 _score
            "should": [ 
                { "match": { "content": "Elasticsearch" }},
                { "match": { "content": "Lucene"        }}
            ]
        }
    }
}
- 如果我们想提高Elasticsearch的权重, 使用boost来提升`相对权重`(默认为1)
GET /_search
{
    "query": {
        "bool": {
            "must": {
                "match": {  
                    "content": {
                        "query":    "full text search",
                        "operator": "and"
                    }
                }
            },
            "should": [
                { "match": {
                    "content": {
                        "query": "Elasticsearch",
                        "boost": 3 
                    }
                }},
                { "match": {
                    "content": {
                        "query": "Lucene",
                        "boost": 2 
                    }
                }}
            ]
        }
    }
}
  • 控制分析

控制分析中文版原文

在不同的环境中有不同的分词, 具体见: Controlling Analysis

Python
- 新增一个字段, 使用英文分词
PUT /my_index/_mapping/my_type
{
    "my_type": {
        "properties": {
            "english_title": {
                "type":     "string",
                "analyzer": "english"
            }
        }
    }
}
- 分析API,分析title字段在不同的分词下的表现
GET /my_index/_analyze
{
  "field": "my_type.title",   
  "text": "Foxes"
}
返回词项 foxes


GET /my_index/_analyze
{
  "field": "my_type.english_title",   
  "text": "Foxes"
}
返回词项 fox

说明默认分词,和英文分词, 经过分析之后的结果不一样';
这意味着,如果使用底层 term 查询精确项 fox , english_title 字段会匹配但 title 字段不会;

- match查询知道字段映射的关系, 能为每个被查询的字段应用正确的分析器
GET /my_index/my_type/_validate/query?explain
{
    "query": {
        "bool": {
            "should": [
                { "match": { "title":         "Foxes"}},
                { "match": { "english_title": "Foxes"}}
            ]
        }
    }
}
返回:
(title:foxes english_title:fox)


- 默认分析器

索引时的顺序如下:

    字段映射里定义的 analyzer ,否则
    索引设置中名为 default 的分析器,默认为
    standard 标准分析器

一个搜索时的 完整 顺序会是下面这样:

    查询自己定义的 analyzer ,否则
    字段映射里定义的 search_analyzer ,否则
    字段映射里定义的 analyzer ,否则
    索引设置中名为 default_search 的分析器,默认为
    索引设置中名为 default 的分析器,默认为
    standard 标准分析器
    
    
- 分析器配置最佳实践

    简单: 在创建索引或者增加类型映射时,为每个需要自定义的全文字段设置分析器; 可以在索引级别设置中,为绝大部分的字段设置你想指定的 default 默认分析器。然后在字段级别设置中,对某一两个字段配置需要指定的分析器;
    
    通常,多数字符串字段都是 not_analyzed 精确值字段,比如标签(tag)或枚举(enum),而且更多的全文字段会使用默认的 standard 分析器或 english 或其他某种语言的分析器。这样只需要为少数一两个字段指定自定义分析:或许标题 title 字段需要以支持 输入即查找(find-as-you-type 的方式进行索引;

    对于和时间相关的日志数据,通常的做法是每天自行创建索引,由于这种方式不是从头创建的索引,仍然可以用 索引模板(Index Template 为新建的索引指定配置和映射;

索引模板

  • 被破坏的相关度

    ** 科普小知识: 词频/逆向文档频率(TF/IDF) **

    词频是计算某个词在当前被查询文档里某个字段中出现的频率,出现的频率越高,文档越相关。 逆向文档频率 将 某个词在索引内所有文档出现的百分数 考虑在内,出现的频率越高,它的权重就越低

    简单描述问题场景: 做一个简单的查询, 结果相关性低的结果出现在了相关性高的结果前面;

    为什么会出现这种情况呢?

    由于性能的原因, Elastic不在所有分片的文档的索引中计算IDF(不计算全局IDF, 只计算本地), 导致的问题是同样的条件在不同的分片上的频率是不同的, 导致相关性混乱的问题;

    强制解决方法: 在搜索请求后添加 ?searchtype=dfsquerythenfetch , dfs 是指 分布式频率搜索(Distributed Frequency Search) , 它告诉 Elasticsearch ,先分别获得每个分片本地的 IDF ,然后根据结果再计算整个索引的全局 IDF; 不要在生产环境上使用 dfs_query_then_fetch 。完全没有必要。只要有足够的数据就能保证词频是均匀分布的。没有理由给每个查询额外加上 DFS 这步

5.2.2 多字段检索

多字符串检索

Raw
- bool 查询采取 more-matches-is-better 匹配越多越好的方式,所以每条 match 语句的评分结果会被加在一起,从而为每个文档提供最终的分数 _score;
- dis_max 则是以最匹配查询的得分作为_score的值;

GET /_search
{
  "query": {
    "bool": {
      "should": [
        { "match": { "title":  "War and Peace" }},
        { "match": { "author": "Leo Tolstoy"   }}
      ]
    }
  }
}

语句的优先级-boost

Raw
GET /_search
{
  "query": {
    "bool": {
      "should": [
        { "match": { 
            "title":  {
              "query": "War and Peace",
              "boost": 2
        }}},
        { "match": { 
            "author":  {
              "query": "Leo Tolstoy",
              "boost": 2
        }}},
        { "bool":  { 
            "should": [
              { "match": { "translator": "Constance Garnett" }},
              { "match": { "translator": "Louise Maude"      }}
            ]
        }}
      ]
    }
  }
}

单字符串查询-多个搜索项堆积到单个字段

当用户输入了单个字符串查询的时候,通常会遇到以下三种情形: 1. 最佳字段 2. 多数字段 3. 混合字段

最佳字段

文档在 相同字段 中包含的词越多越好,评分也来自于 最匹配字段;

Python
场景: 我们搜索一个词组 Brown fox
内容: 
PUT /my_index/my_type/1
{
    "title": "Quick brown rabbits",
    "body":  "Brown rabbits are commonly seen."
}

PUT /my_index/my_type/2
{
    "title": "Keeping pets healthy",
    "body":  "My quick brown fox eats rabbits on a regular basis."
}

- bool 查询
{
    "query": {
        "bool": {
            "should": [
                { "match": { "title": "Brown fox" }},
                { "match": { "body":  "Brown fox" }}
            ]
        }
    }
}
肉眼判断应该是条目2更优, 结果却是条目1更优; 为什么呢? 因为`bool计算评分的方式`是综合计算2个查询的评分, 文档 1 的两个字段都包含 brown 这个词, 而文档 2 只有 body里面包含, 最终计算文档1的评分更高;

- 使用 `dis_max` 查询: 以`单个最佳匹配的语句的评分来作为整体评分;
{
    "query": {
        "dis_max": {
            "queries": [
                { "match": { "title": "Brown fox" }},
                { "match": { "body":  "Brown fox" }}
            ]
        }
    }
}

返回:
{
    "query": {
        "dis_max": {
            "queries": [
                { "match": { "title": "Brown fox" }},
                { "match": { "body":  "Brown fox" }}
            ]
        }
    }
}

此时又会出现其他问题, 例如, 我们搜索 "Quick pets", 使用最佳匹配评分的时候, 虽然文档2 titlebody分别匹配quick  pets字段, 文档1只是匹配quick字段, 但是按照dis_max查询, 取最优的单条查询评分得到的结果是一样的;

但是此时我们需要在多个match, 同时匹配的查询的文档有更高的优先级, 那么我们应该怎么做呢?

答案是使用: tie_breaker参数, 将其他匹配语句的评分也考虑其中, 得出的结果 文档2的评分比文档1略高;

{
    "query": {
        "dis_max": {
            "queries": [
                { "match": { "title": "Quick pets" }},
                { "match": { "body":  "Quick pets" }}
            ],
            "tie_breaker": 0.3
        }
    }
}
tie_breaker 参数提供了一种 dis_max  bool 之间的折中选择: 最佳_score + 其它语句评分结果*tie_breaker

tie_breaker 可以是 0  1 之间的浮点数,其中 0 代表使用 dis_max 最佳匹配语句的普通逻辑, 1 表示所有匹配语句同等重要。最佳的精确值需要根据数据与查询调试得出,但是合理值应该与零接近(处于 0.1 - 0.4 之间),这样就不会颠覆 dis_max 最佳匹配性质的根本; 总之设置一个权重, 来做微调;

multi_match 查询

multi_match 查询为能在多个字段上反复执行相同查询提供了一种便捷方式

Python
{
  "dis_max": {
    "queries":  [
      {
        "match": {
          "title": {
            "query": "Quick brown fox",
            "minimum_should_match": "30%"
          }
        }
      },
      {
        "match": {
          "body": {
            "query": "Quick brown fox",
            "minimum_should_match": "30%"
          }
        }
      },
    ],
    "tie_breaker": 0.3
  }
}
上面这个查询用 multi_match 重写成更简洁的形式:

{
    "multi_match": {
        "query":                "Quick brown fox",
        # best_fields 类型是默认值,可以不指定
        "type":                 "best_fields", 
        "fields":               [ "title", "body" ],
        "tie_breaker":          0.3,
        # 如 minimum_should_match 或 operator 这样的参数会被传递到生成的 match 查询中
        "minimum_should_match": "30%" 
    }
}

- 查询字段名称的模糊匹配
{
    "multi_match": {
        "query":  "Quick brown fox",
        "fields": "*_title"
    }
}

- 提升单个字段的权重
- 可以使用 ^ 字符语法为单个字段提升权重,在字段名称的末尾添加 ^boost  其中 boost 是一个浮点数
{
    "multi_match": {
        "query":  "Quick brown fox",
        "fields": [ "*_title", "chapter_title^2" ] 
    }
}

多数字段查询

相同的文本被索引到其他字段,以提供更精确的匹配; 其他字段是作为匹配每个文档时提高相关度的 信号;

召回率: 返回所有的相关文档; 精确率: 不返回无关文档; 目的: 在结果的第一页中为用户呈现最为相关的文档;

Python
- 多字段映射
- 我们将字段索引两次, 一次使用词干模式以及一次非词干模式
DELETE /my_index

PUT /my_index
{
    # 参考 被破坏的相关度
    "settings": { "number_of_shards": 1 }, 
    "mappings": {
        "my_type": {
            "properties": {
                # title 字段使用 english 英语分析器来提取词干
                "title": { 
                    "type":     "string",
                    "analyzer": "english",
                    "fields": {
                        # title.std 字段使用 standard 标准分析器,所以没有词干提取
                        "std":   {
                            "type":     "string",
                            "analyzer": "standard"
                        }
                    }
                }
            }
        }
    }
}

-接着索引一些文档
PUT /my_index/my_type/1
{ "title": "My rabbit jumps" }

PUT /my_index/my_type/2
{ "title": "Jumping jack rabbits" }

- 简单查询
GET /my_index/_search
{
   "query": {
        "match": {
            "title": "jumping rabbits"
        }
    }
}
 
- 由于title使用了 English 分析器, 所以查询语句会被英文分词, 
- 2条文档都匹配, 并且评分相同
{
  "hits": [
     {
        "_id": "1",
        "_score": 0.42039964,
        "_source": {
           "title": "My rabbit jumps"
        }
     },
     {
        "_id": "2",
        "_score": 0.42039964,
        "_source": {
           "title": "Jumping jack rabbits"
        }
     }
  ]
}

- 如果同时查询两个字段,然后使用 bool 查询将评分结果 合并 ,那么两个文档都是匹配的( title 字段的作用),而且文档 2 的相关度评分更高( title.std 字段的作用)
GET /my_index/_search
{
   "query": {
        "multi_match": {
            "query":  "jumping rabbits",
            "type":   "most_fields", 
            "fields": [ "title", "title.std" ]
        }
    }
}
结果: 文档 2 现在的评分要比文档 1 
{
  "hits": [
     {
        "_id": "2",
        "_score": 0.8226396, 
        "_source": {
           "title": "Jumping jack rabbits"
        }
     },
     {
        "_id": "1",
        "_score": 0.10741998, 
        "_source": {
           "title": "My rabbit jumps"
        }
     }
  ]
}

总结: 
    用广度匹配字段 title 包括尽可能多的文档——以提升召回率, 同时又使用字段 title.std 作为 信号 将相关度更高的文档置于结果顶部

每个字段对于最终评分的贡献可以通过自定义值 boost 来控制:
GET /my_index/_search
{
   "query": {
        "multi_match": {
            "query":       "jumping rabbits",
            "type":        "most_fields",
            # title 字段的 boost 的值为 10 使它比 title.std 更重要
            "fields":      [ "title^10", "title.std" ] 
        }
    }
}

跨字段搜索

对于某些实体,我们需要在多个字段中确定其信息,单个字段都只能作为整体的一部分; 例如: Person: firstname 和 lastname (人名/姓)

  • 跨字段实体搜索(cross-fields entity search)

多个字段唯一标识一个对象, 查询时我们在多个字段中进行搜索

例如:

Python
- Person
{
    "firstname":  "Peter",
    "lastname":   "Smith"
}

- Address
{
    "street":   "5 Poland Street",
    "city":     "London",
    "country":  "United Kingdom",
    "postcode": "W1V 3DG"
}

- 使用most_fields
{
  "query": {
    "multi_match": {
      "query":       "Poland Street W1V",
      "type":        "most_fields",
      "fields":      [ "street", "city", "country", "postcode" ]
    }
  }
}
  • 使用most_fields方式问题

  • 它是为多数字段匹配 任意 词设计的,而不是在 所有字段 中找到最匹配的;

  • 它不能使用 operator 或 minimumshouldmatch 参数来降低次相关结果造成的长尾效应;

  • 词频对于每个字段是不一样的,而且它们之间的相互影响会导致不好的排序结果;

字段中心式查询

以上三个源于 mostfields 的问题都因为它是 字段中心式(field-centric) 而不是 词中心(term-centric) 的:当真正感兴趣的是匹配词的时候,它为我们查找的是最匹配的 字段; bestfields 类型也是字段中心式的, 它也存在类似的问题;

  • 问题1: 在多个字段中匹配相同的值

most_fields 查询:Elasticsearch 为每个字段生成独立的 match 查询,再用 bool 查询将他们包起来

可以通过explain API查看解析: GET /validate/query?explain { query: { "multimatch: { query:Poland Street W1V, type:most_fields, fields: [street,city,country,postcode" ] } } } 返回: (street:poland street:street street:w1v) (city:poland city:street city:w1v) (country:poland country:street country:w1v) (postcode:poland postcode:street postcode:w1v)

可以发现, 两个 字段都与 poland 匹配的文档要比一个字段同时匹配 poland 与 street 文档的评分高

  • 问题 2: 剪掉长尾, 使用and或者minimumshouldmatch参数来消除结果中几乎不相关的长尾;

    { query: { multi_match: { query: Poland Street W1V, type: most_fields, operator: and, fields: [ street, city, country, postcode ] } } } 查询的 explanation 解释如下: 所有词都必须匹配, 可能就不存在能与这个查询匹配的文档!!! (+street:poland +street:street +street:w1v) (+city:poland +city:street +city:w1v) (+country:poland +country:street +country:w1v) (+postcode:poland +postcode:street +postcode:w1v)

  • 问题3: 词频/逆向文档频率

词频 : 一个词在单个文档的某个字段中出现的频率越高,这个文档的相关度就越高;

逆向文档频率: 一个词在所有文档某个字段索引中出现的频率越高,这个词的相关度就越低;

当搜索多个字段时,TF/IDF 会带来某些令人意外的结果;

解决方案:

  • 使用冗余字段降低复杂度 存在这些问题仅仅是因为我们在处理着多个字段,如果将所有这些字段组合成单个字段,问题就会消失。可以为 person 文档添加 full_name 字段来解决这个问题;

  • 自定义 _all 字段: 需在索引文档前为其设置好映射, 否者需要重新定义索引 all字段的索引方式是将所有其他字段的值作为一个大字符串索引的; 类似的, 我们可以为人名/地名添加一个自定义的all字段; Elasticsearch 在字段映射中为我们提供 copy_to 参数来实现这个功能;

    注意: copyto 设置对multi-field无效。如果尝试这样配置映射,Elasticsearch 会抛异常; 为什么呢?多字段只是以不同方式简单索引“主”字段;它们没有自己的数据源。也就是说没有可供 copyto 到另一字段的数据源; 只要对“主”字段 copy_to 就能轻而易举的达到相同的效果;

Python
PUT /my_index
    {
        "mappings": {
            "person": {
                "properties": {
                    "first_name": {
                        "type":     "string",
                        "copy_to":  "full_name" 
                    },
                    "last_name": {
                        "type":     "string",
                        "copy_to":  "full_name" 
                    },
                    "full_name": {
                        "type":     "string"
                    }
                }
            }
        }
    }
  • 使用cross-fields 跨字段查询

    自定义 all 的方式是一个好的解决方案,只需在索引文档前为其设置好映射。 不过, Elasticsearch 还在搜索时提供了相应的解决方案:使用 crossfields 类型进行 multimatch 查询。 crossfields 使用词中心式(term-centric)的查询方式,这与 bestfields 和 mostfields 使用字段中心式(field-centric)的查询方式非常不同,它将所有字段当成一个大字段,并在 每个字段 中查找 每个词;

cross-fields 跨字段查询

Python
- 字段中心式(field-centric
GET /_validate/query?explain
{
    "query": {
        "multi_match": {
            "query":       "peter smith",
            "type":        "most_fields",
            "operator":    "and", 
            "fields":      [ "first_name", "last_name" ]
        }
    }
}
返回:
(+first_name:peter +first_name:smith)
(+last_name:peter  +last_name:smith)
解释: 对于匹配的文档,peter  smith 都必须同时出现在相同字段中,要么是 first_name 字段,要么 last_name 字段;
Python
词中心式 会使用以下逻辑:
+(first_name:peter last_name:peter)
+(first_name:smith last_name:smith)
解释:  peter  smith 都必须出现,但是可以出现在任意字段中;

cross_fields 类型首先分析查询字符串并生成一个词列表,然后它从所有字段中依次搜索每个词。这种不同的搜索方式很自然的解决了 字段中心式 查询三个问题中的二个;

另外, 逆向文档频率问题, 可以通过 混合 不同字段逆向索引文档频率的方式解决, 为了让 cross_fields 查询以最优方式工作,所有的字段都须使用相同的分析器, 具有相同分析器的字段会被分组在一起作为混合字段使用;

Python
GET /_validate/query?explain
{
    "query": {
        "multi_match": {
            "query":       "peter smith",
            "type":        "cross_fields", # 用 cross_fields 词中心式匹配
            "operator":    "and",
            "fields":      [ "first_name", "last_name" ]
        }
    }
}

它通过 混合 不同字段逆向索引文档频率的方式解决了词频的问题:
+blended("peter", fields: [first_name, last_name])
+blended("smith", fields: [first_name, last_name])
它会同时在 first_name  last_name 两个字段中查找 smith  IDF ,然后用两者的最小值作为两个字段的 IDF 。结果实际上就是 smith 会被认为既是个平常的姓,也是平常的名

5.2.3 近似匹配

短语匹配match_phrase

match_phrase 查询首先将查询字符串解析成一个词项列表,然后对这些词项进行搜索,但只保留那些包含 全部 搜索词项,且 位置 与搜索词项相同的文档。 比如对于 quick fox 的短语搜索可能不会匹配到任何文档,因为没有文档包含的 quick 词之后紧跟着 fox;

Raw
GET /my_index/my_type/_search
{
    "query": {
        "match_phrase": {
            "title": "quick brown fox"
        }
    }
}
等同于:
"match": {
    "title": {
        "query": "quick brown fox",
        "type":  "phrase"
    }
}

- 词项的位置
当一个字符串被分词后,这个分析器不但会 返回一个词项列表,而且还会返回各词项在原始字符串中的 位置 或者顺序关系
GET /_analyze?analyzer=standard
Quick brown fo
返回:
{
   "tokens": [
      {
         "token": "quick",
         "start_offset": 0,
         "end_offset": 5,
         "type": "",
         "position": 1 
      },
      {
         "token": "brown",
         "start_offset": 6,
         "end_offset": 11,
         "type": "",
         "position": 2 
      },
      {
         "token": "fox",
         "start_offset": 12,
         "end_offset": 15,
         "type": "",
         "position": 3 
      }
   ]
}

- 满足短语查询的3个条件

例如: quick brown fox 
* quick 、 brown 和 fox 需要全部出现在域中
* brown 的位置应该比 quick 的位置大 1
* fox 的位置应该比 quick 的位置大 2 

本质上来讲,match_phrase 查询是利用一种低级别的 span 查询族(query family)去做词语位置敏感的匹配。 Span 查询是一种词项级别的查询,所以它们没有分词阶段;它们只对指定的词项进行精确搜索

混合起来

精确短语匹配 或许是过于严格了。也许我们想要包含 “quick brown fox” 的文档也能够匹配 “quick fox,” , 尽管情形不完全相同

Python
GET /my_index/my_type/_search
{
    "query": {
        "match_phrase": {
            "title": {
                "query": "quick fox",
                "slop":  1
            }
        }
    }
}

多值字段

TODO:

越近越好

TODO:

使用邻近度提高相关度

TODO:

性能优化

TODO:

寻找相关词

TODO:

5.2.4 部分匹配

邮编与结构化数据

假设将邮编作为 not_analyzed 的精确值字段索引,所以可以为其创建索引,如下:

Python
PUT /my_index
{
    "mappings": {
        "address": {
            "properties": {
                "postcode": {
                    "type":  "string",
                    "index": "not_analyzed"
                }
            }
        }
    }
}

然后索引一些邮编:

Python
PUT /my_index/address/1
{ "postcode": "W1V 3DG" }

PUT /my_index/address/2
{ "postcode": "W2F 8HW" }

PUT /my_index/address/3
{ "postcode": "W1F 7HW" }

PUT /my_index/address/4
{ "postcode": "WC1N 1LZ" }

PUT /my_index/address/5
{ "postcode": "SW5 0BE" }

prefix前缀查询

Python
GET /my_index/address/_search
{
    "query": {
        "prefix": {
            "postcode": "W1"
        }
    }
}

prefix 查询是一个词级别的底层的查询,它不会在搜索之前分析查询字符串,它假定传入前缀就正是要查找的前缀;


默认状态下, prefix 查询不做相关度评分计算,它只是将所有匹配的文档返回,并为每条结果赋予评分值 1 。它的行为更像是过滤器而不是查询。 prefix 查询和 prefix 过滤器这两者实际的区别就是过滤器是可以被缓存的,而查询不行;


prefix 查询或过滤对于一些特定的匹配是有效的,但使用方式还是应当注意。 当字段中词的集合很小时,可以放心使用,但是它的伸缩性并不好,会对我们的集群带来很多压力。可以使用较长的前缀来限制这种影响,减少需要访问的量

通配符与正则表达式(wildcard 和 regexp)

与 prefix 前缀查询的特性类似, wildcard 通配符查询也是一种底层基于词的查询, 与前缀查询不同的是它允许指定匹配的正则式。它使用标准的 shell 通配符查询: ? 匹配任意字符, * 匹配 0 或多个字符


wildcard 和 regexp 查询的工作方式与 prefix 查询完全一样,它们也需要扫描倒排索引中的词列表才能找到所有匹配的词,然后依次获取每个词相关的文档 ID ,与 prefix 查询的唯一不同是:它们能支持更为复杂的匹配模式;


这也意味着需要同样注意前缀查询存在性能问题,对有很多唯一词的字段执行这些查询可能会消耗非常多的资源,所以要避免使用左通配这样的模式匹配(如: foo 或 .foo 这样的正则式)

Python
GET /my_index/address/_search
{
    "query": {
        "wildcard": {
            "postcode": "W?F*HW" 
        }
    }
}

prefix 、 wildcard 和 regexp 查询是基于词操作的,如果用它们来查询 analyzed 字段,它们会检查字段里面的每个词,而不是将字段作为整体来处理;


比方说包含 “Quick brown fox” (快速的棕色狐狸)的 title 字段会生成词: quick 、 brown 和 fox

会匹配以下这个查询: { "regexp": { "title": "br.*" }} 但是不会匹配以下两个查询: { "regexp": { "title": "Qu.*" }} # 在索引里的词是 quick 而不是 Quick { "regexp": { "title": "quick br*" }} # quick 和 brown 在词表中是分开的

查询时输入即搜索

  • 在输完查询内容之前,就能为他们展现搜索结果, 这就是所谓的 即时搜索(instant search) 或 输入即搜索(search-as-you-type)
Python
-  match_phrase 查询一致,不同的是它将查询字符串的最后一个词作为前缀使用
{
    "match_phrase_prefix" : {
        "brand" : "johnnie walker bl"
    }
}

- 接受slop, 让相对词序位置不那么严格
{
    "match_phrase_prefix" : {
        "brand" : {
            "query": "walker johnnie bl", 
            "slop":  10
        }
    }
}

- 可以通过设置 max_expansions 参数来限制前缀扩展的影响, 一个合理的值是可能是 50 
{
    "match_phrase_prefix" : {
        "brand" : {
            "query":          "johnnie walker bl",
            "max_expansions": 50
        }
    }
}
参数 max_expansions 控制着可以与前缀匹配的词的数量,它会先查找第一个与前缀 bl 匹配的词,然后依次查找搜集与之匹配的词(按字母顺序),直到没有更多可匹配的词或当数量超过 max_expansions 时结束;

索引时优化

可以通过在索引时处理数据提高搜索的灵活性以及提升系统性能。为此仍然需要付出应有的代价:增加的索引空间与变慢的索引能力,但这与每次查询都需要付出代价不同,索引时的代价只用付出一次

Ngrams 在部分匹配的应用

  • 在搜索之前准备好供部分匹配的数据可以提高搜索的性能

  • 部分匹配使用的工具是 n-gram

可以将 n-gram 看成一个在词语上 滑动窗口 , n 代表这个 “窗口” 的长度。如果我们要 n-gram quick 这个词 —— 它的结果取决于 n 的选择长度

  • 长度 1(unigram): [ q, u, i, c, k ]
  • 长度 2(bigram): [ qu, ui, ic, ck ]
  • 长度 3(trigram): [ qui, uic, ick ]
  • 长度 4(four-gram): [ quic, uick ]
  • 长度 5(five-gram): [ quick ]

对于输入即搜索(search-as-you-type)这种应用场景, 使用一种特殊的 n-gram 称为 边界 n-grams (edge n-grams):

它会固定词语开始的一边,以单词 quick 为例,它的边界 n-gram 的结果为:

  • q
  • qu
  • qui
  • quic
  • quick

索引时输入即搜索

  • 设置索引时输入即搜索的第一步是需要定义好分析链

配置分析器

  • 准备索引

  1. 自定义edge_ngram token 过滤器,称为 autocomplete_filter:
Raw
{
    "filter": {
        "autocomplete_filter": {
            "type":     "edge_ngram",
            "min_gram": 1,
            "max_gram": 20
        }
    }
}

这个配置的意思是:对于这个 token 过滤器接收的任意词项,过滤器会为之生成一个最小固定值为 1 ,最大为 20 的 n-gram

  1. 然后会在一个自定义分析器 autocomplete 中使用上面这个 token 过滤器
Python
{
    "analyzer": {
        "autocomplete": {
            "type":      "custom",
            "tokenizer": "standard",
            "filter": [
                "lowercase",
                # 自定义的 edge-ngram token 过滤器
                "autocomplete_filter" 
            ]
        }
    }
}

这个分析器使用 standard 分词器将字符串拆分为独立的词,并且将它们都变成小写形式,然后为每个词生成一个边界 n-gram,这要感谢 autocomplete_filter 起的作用;

  1. 完整示例如下
Python
PUT /my_index
{
    "settings": {
        "number_of_shards": 1,
        # 首先自定义 token 过滤器 
        "analysis": {
            "filter": {
                "autocomplete_filter": { 
                    "type":     "edge_ngram",
                    "min_gram": 1,
                    "max_gram": 20
                }
            },
            # 然后在分析器中使用它
            "analyzer": {
                "autocomplete": {
                    "type":      "custom",
                    "tokenizer": "standard",
                    "filter": [
                        "lowercase",
                        "autocomplete_filter" 
                    ]
                }
            }
        }
    }
}
  1. 验证自定义分析器正常
Raw
curl -XGET 'localhost:9200/my_index/_analyze?analyzer=autocomplete&pretty' -H 'Content-Type: application/json' -d'
quick brown
'
  1. update-mapping API 将这个分析器应用到具体字段
Raw
PUT /my_index/_mapping/my_type
{
    "my_type": {
        "properties": {
            "name": {
                "type":     "string",
                "analyzer": "autocomplete"
            }
        }
    }
}

Ngrams 在复合词的应用

Ngrams 在复合词的应用

5.2.5 控制相关度

相关度评分背后的理论

Lucene的实用评分函数

查询时权重提升

使用查询结构修改相关度

Not Quite Not

忽略TF/IDF

function_score 查询

按受欢迎度提升权重

过滤集提升权重

随机评分

越近越好

理解price价格语句

脚本评分

可拔插的相似度算法

更改相似度

调试相关度最后10%要做的事情

6.处理人类语言

略, 详情参见处理人类语言

7.聚合

7.1 高阶概念

  1. 桶(Buckets) 满足特定条件的文档的集合, 类似SQL的 (Group by)

  2. 指标(Metrics) 对桶内的文档进行统计计算, 类似SQL的(count, min, max等统计方法)

  • 用SQL来解释
Python
SELECT COUNT(color) # 相当于指标
FROM table
GROUP BY color   # 相当于桶

7.2 尝试聚合

  • 汽车销售数据
Python
curl -XPOST 'localhost:9200/cars/transactions/_bulk?pretty' -H 'Content-Type: application/json' -d'
{ "index": {}}
{ "price" : 10000, "color" : "red", "make" : "honda", "sold" : "2014-10-28" }
{ "index": {}}
{ "price" : 20000, "color" : "red", "make" : "honda", "sold" : "2014-11-05" }
{ "index": {}}
{ "price" : 30000, "color" : "green", "make" : "ford", "sold" : "2014-05-18" }
{ "index": {}}
{ "price" : 15000, "color" : "blue", "make" : "toyota", "sold" : "2014-07-02" }
{ "index": {}}
{ "price" : 12000, "color" : "green", "make" : "toyota", "sold" : "2014-08-19" }
{ "index": {}}
{ "price" : 20000, "color" : "red", "make" : "honda", "sold" : "2014-11-05" }
{ "index": {}}
{ "price" : 80000, "color" : "red", "make" : "bmw", "sold" : "2014-01-01" }
{ "index": {}}
{ "price" : 25000, "color" : "blue", "make" : "ford", "sold" : "2014-02-12" }
'
  • 聚合搜索
Python
GET /cars/transactions/_search
{
    "size" : 0,
    "aggs" : { 
        "popular_colors" : { 
            "terms" : { 
              "field" : "color"
            }
        }
    }
}
  • 返回
Python
{
...
   "hits": {
      "hits": [] 
   },
   "aggregations": {
      "popular_colors": { 
         "buckets": [
            {
               "key": "red", 
               "doc_count": 4 
            },
            {
               "key": "blue",
               "doc_count": 2
            },
            {
               "key": "green",
               "doc_count": 2
            }
         ]
      }
   }
}
  • 添加度量指标-计算平均值
Python
GET /cars/transactions/_search
{
   "size" : 0,
   "aggs": {
      "colors": {
         "terms": {
            "field": "color"
         },
         "aggs": { 
            "avg_price": { 
               "avg": {
                  "field": "price" 
               }
            }
         }
      }
   }
}

返回:

Raw
{
...
   "aggregations": {
      "colors": {
         "buckets": [
            {
               "key": "red",
               "doc_count": 4,
               "avg_price": { 
                  "value": 32500
               }
            },
            {
               "key": "blue",
               "doc_count": 2,
               "avg_price": {
                  "value": 20000
               }
            },
            {
               "key": "green",
               "doc_count": 2,
               "avg_price": {
                  "value": 21000
               }
            }
         ]
      }
   }
...
}
  • 嵌套桶来获得更详细的桶分布信息
Python
GET /cars/transactions/_search
{
   "size" : 0,
   "aggs": {
      "colors": {
         "terms": {
            "field": "color"
         },
         "aggs": {
            "avg_price": { 
               "avg": {
                  "field": "price"
               }
            },
            "make": { 
                "terms": {
                    "field": "make" 
                }
            }
         }
      }
   }
}

返回:

Python
{
...
   "aggregations": {
      "colors": {
         "buckets": [
            {
               "key": "red",
               "doc_count": 4,
               "make": { 
                  "buckets": [
                     {
                        "key": "honda", 
                        "doc_count": 3
                     },
                     {
                        "key": "bmw",
                        "doc_count": 1
           ```python          }
                  ]
               },
               "avg_price": {
                  "value": 32500 
               }
            },

...
}
  • 优化返回-为每个汽车生产商计算最低和最高的价格
Python
GET /cars/transactions/_search
{
   "size" : 0,
   "aggs": {
      "colors": {
         "terms": {
            "field": "color"
         },
         "aggs": {
            "avg_price": { "avg": { "field": "price" }
            },
            "make" : {
                "terms" : {
                    "field" : "make"
                },
                "aggs" : { 
                    "min_price" : { "min": { "field": "price"} }, 
                    "max_price" : { "max": { "field": "price"} } 
                }
            }
         }
      }
   }
}
  • 条形图
Python
GET /cars/transactions/_search
{
   "size" : 0,
   "aggs":{
      "price":{
         "histogram":{ 
            "field": "price",
            "interval": 20000
         },
         "aggs":{
            "revenue": {
               "sum": { 
                 "field" : "price"
               }
             }
         }
      }
   }
}

7.3 按时间统计

Python
GET /cars/transactions/_search
{
   "size" : 0,
   "aggs": {
      "sales": {
         "date_histogram": {
            "field": "sold",
            "interval": "month", 
            "format": "yyyy-MM-dd" 
         }
      }
   }
}
  • 返回空Buckets
Python
GET /cars/transactions/_search
{
   "size" : 0,
   "aggs": {
      "sales": {
         "date_histogram": {
            "field": "sold",
            "interval": "month",
            "format": "yyyy-MM-dd",
            "min_doc_count" : 0, # 这个参数强制返回空 buckets
            "extended_bounds" : { # 这个参数强制返回整年
                "min" : "2014-01-01",
                "max" : "2014-12-31"
            }
         }
      }
   }
}
  • 扩展例子

buckets 可以嵌套进 buckets 中从而得到更复杂的分析。 作为例子,我们构建聚合以便按季度展示所有汽车品牌总销售额。同时按季度、按每个汽车品牌计算销售总额,以便可以找出哪种品牌最赚钱:

Python
GET /cars/transactions/_search
{
   "size" : 0,
   "aggs": {
      "sales": {
         "date_histogram": {
            "field": "sold",
            "interval": "quarter", 
            "format": "yyyy-MM-dd",
            "min_doc_count" : 0,
            "extended_bounds" : {
                "min" : "2014-01-01",
                "max" : "2014-12-31"
            }
         },
         "aggs": {
            "per_make_sum": {
               "terms": {
                  "field": "make"
               },
               "aggs": {
                  "sum_price": {
                     "sum": { "field": "price" } 
                  }
               }
            },
            "total_sum": {
               "sum": { "field": "price" } 
            }
         }
      }
   }
}

#kibana的效果图

  • 为聚合添加范围

聚合可以与搜索请求同时执行,但是我们需要理解一个新概念: 范围 。 默认情况下,聚合与查询是对同一范围进行操作的,也就是说,聚合是基于我们查询匹配的文档集合进行计算的

Python
GET /cars/transactions/_search
{
    "size" : 0,
    "aggs" : {
        "colors" : {
            "terms" : {
              "field" : "color"
            }
        }
    }
}
等同于:
GET /cars/transactions/_search
{
    "size" : 0,
    "query" : {
        "match_all" : {}
    },
    "aggs" : {
        "colors" : {
            "terms" : {
              "field" : "color"
            }
        }
    }
}
限定范围:
GET /cars/transactions/_search
{
    "query" : {
        "match" : {
            "make" : "ford"
        }
    },
    "aggs" : {
        "colors" : {
            "terms" : {
              "field" : "color"
            }
        }
    }
}
  • 全局桶

通常我们希望聚合是在查询范围内的,但有时我们也想要搜索它的子集,而聚合的对象却是 所有 数据

Python
GET /cars/transactions/_search
{
    "size" : 0,
    "query" : {
        "match" : {
            "make" : "ford"
        }
    },
    "aggs" : {
        "single_avg_price": {
            "avg" : { "field" : "price" } 
        },
        "all": {
            "global" : {}, 
            "aggs" : {
                "avg_price": {
                    "avg" : { "field" : "price" } 
                }

            }
        }
    }
}

7.4 查询结果过滤和聚合

聚合范围限定还有一个自然的扩展就是过滤。因为聚合是在查询结果范围内操作的,任何可以适用于查询的过滤器也可以应用在聚合上

  • 过滤

如果我们想找到售价在 $10,000 美元之上的所有汽车同时也为这些车计算平均售价, 可以简单地使用一个 constant_score 查询和 filter 约束:

Python
GET /cars/transactions/_search
{
    "size" : 0,
    "query" : {
        "constant_score": {
            "filter": {
                "range": {
                    "price": {
                        "gte": 10000
                    }
                }
            }
        }
    },
    "aggs" : {
        "single_avg_price": {
            "avg" : { "field" : "price" }
        }
    }
}
  • 过滤桶

过滤桶

如果我们只想对聚合结果过滤怎么办? 假设我们正在为汽车经销商创建一个搜索页面, 我们希望显示用户搜索的结果,但是我们同时也想在页面上提供更丰富的信息,包括(与搜索匹配的)上个月度汽车的平均售价

Python
GET /cars/transactions/_search
{
   "size" : 0,
   "query":{
      "match": {
         "make": "ford"
      }
   },
   "aggs":{
      "recent_sales": {
         "filter": { 
            "range": {
               "sold": {
                  "from": "now-1M"
               }
            }
         },
         "aggs": {
            "average_price":{
               "avg": {
                  "field": "price" 
               }
            }
         }
      }
   }
}
  • 后过滤器

后过滤器

目前为止,我们可以同时对搜索结果和聚合结果进行过滤(不计算得分的 filter 查询),以及针对聚合结果的一部分进行过滤(filter 桶);

我们可能会想,只过滤搜索结果,不过滤聚合结果呢? 答案是使用 post_filter;

Python
GET /cars/transactions/_search
{
    "size" : 0,
    "query": {
        "match": {
            "make": "ford"
        }
    },
    # post_filter 元素是 top-level 而且仅对命中结果进行过滤
    "post_filter": {    
        "term" : {
            "color" : "green"
        }
    },
    "aggs" : {
        "all_colors": {
            "terms" : { "field" : "color" }
        }
    }
}

注意 当你需要对搜索结果和聚合结果做不同的过滤时,你才应该使用 postfilter , 有时用户会在普通搜索使用 postfilter

小结

选择合适类型的过滤(如:搜索命中、聚合或两者兼有)通常和我们期望如何表现用户交互有关。选择合适的过滤器(或组合)取决于我们期望如何将结果呈现给用户。

在 filter 过滤中的 non-scoring 查询,同时影响搜索结果和聚合结果。 filter 桶影响聚合。 post_filter 只影响搜索结果

7.5 多桶排序

未完待续

7.5 近似聚合

未完待续

7.6 通过聚合发现异常指标

未完待续

7.7 Doc Values and Fielddate

未完待续

7.8 总结

未完待续

8.地理位置

略,详情参见地理位置

9.数据建模

未完待续

9.1 关联关系处理

未完待续

9.2 嵌套对象

未完待续

9.3 父-子关系文档

未完待续

9.4 扩容设计

未完待续

10.管理,监控,部署

略, 详情参见管理,监控,部署-官方

我写的线上部署过程, 可做参考, 如有错误, 以官方文档为准