wangbo123855842 / Learning

15 stars 2 forks source link

ElasticSearch #47

Open wangbo123855842 opened 3 years ago

wangbo123855842 commented 3 years ago
スクリーンショット 2020-12-04 8 59 31

Elastic Stack

包括 Elasticsearch、Kibana、Beats 和 Logstash(也称为 ELK Stack)。能够安全可靠地获取任何来源、任何格式的数据,然后实时地对数据进行搜索、分析和可视化。

スクリーンショット 2020-12-07 9 16 06

ELK 收集日志架构

スクリーンショット 2020-12-07 18 01 51

简介

ElasticSearch 的底层是开源库 Lucene。但是,你没法直接用 Lucene,必须自己写代码去调用它的接口。ElasticSearch 是 Lucene 的封装,提供了 REST API 的操作接口,开箱即用。 简单的说,ElasticSearch 是一款基于 Lucene 的实时分布式搜索和分析引擎。

Elasticsearch 是一个建立在全文搜索引擎 Apache Lucene™ 基础上的搜索引擎,可以说 Lucene 是当今最先进,最高效的全功能开源搜索引擎框架。 但是 Lucene 只是一个框架,要充分利用它的功能,需要使用 JAVA,并且在程序中集成 Lucene。需要很多的学习了解,才能明白它是如何运行的,Lucene 确实非常复杂。 Elasticsearch 使用 Lucene 作为内部引擎,但是在使用它做全文搜索时,只需要使用统一开发好的API即可,而不需要了解其背后复杂的 Lucene 的运行原理

ElasticSearch 和 Lucene

可以理解成,发动机引擎(Lucene)和汽车(ELasticSearch)的关系。

Solr

ElasticSearch 和 Solr

检索速度方面

除了,上面检索方面,二者还有一些区别

ElasticSearch 模块架构图

スクリーンショット 2020-12-07 18 26 39

ES 安装

cd elasticsearch-6.6.1
bin/elasticsearch
bin/elasticsearch -d (后台运行)

config/elasticsearch.yml

比如,为 ES 设置 ip 绑定

network.host: 192.168.20.210

为 ES 设置自定义端口,默认是9200

http.port: 9200

这样就可以通过 192.168.20.210:9200 来访问你的 ES了,浏览器会返回一个 JSON 数据。

因为 HEAD 是一个用于管理 Elasticsearch 的 web 前端插件,该插件在 es5 版本以后采用独立服务的形式进行安装使用。 需要安装nodejs,npm。

下载 head

git clone git://github.com/mobz/elasticsearch-head.git

安装 head

cd elasticsearch-head/
npm install
  1. 修改 Gruntfile.js 配置, 增加 hostname:’*‘ 配置。

  2. 修改 head/_site/app.js 文件,修改 head 连接 es 的地址。

  3. 修改 elasticsearch.yml,增加跨域的配置 (需要重启es才能生效)

    vi config/elasticsearch.yml
    http.cors.enabled: true
    http.cors.allow-origin: "*“
[es@master elasticsearch-head]$ cd node_modules/grunt/bin/ 
[es@master bin]$ ./grunt server &
[es@master ~]$ netstat -ntlp

Head 查看 es 集群状态

http://192.168.20.210:9100/

ES 集群安装

一个集群就是由一个或多个节点组织在一起,它们共同持有整个的数据,并一起提供索引和搜索功能。 一个集群由一个唯一的名字标识,这个名字默认就是 elasticsearch。一个节点只能通过指定某个集群的名字,来加入这个集群。

スクリーンショット 2020-12-11 10 47 45

节点1的配置信息

# 集群名称,保证唯一
cluster.name: my-elasticsearch
# 节点名称,必须不一样
node.name: node-01
# 必须为本机的ip地址
network.host: 127.0.0.1
# 服务端口号,在同一机器下必须不一样
http.port: 9200
# 集群间通信端口号,在同一机器下必须不一样
transport.tcp.port: 9300
# 设置集群自动发现机器 ip 集合
discovery.zen.ping.unicast.hosts: ["127.0.0.1:9300","127.0.0.1:9301","127.0.0.1:9302"]

节点2的配置信息

# 集群名称,保证唯一
cluster.name: my-elasticsearch
# 节点名称,必须不一样
node.name: node-02
# 必须为本机的ip地址
network.host: 127.0.0.1
# 服务端口号,在同一机器下必须不一样
http.port: 9201
# 集群间通信端口号,在同一机器下必须不一样
transport.tcp.port: 9301
# 设置集群自动发现机器ip集合
discovery.zen.ping.unicast.hosts: ["127.0.0.1:9300","127.0.0.1:9301","127.0.0.1:9302"]

节点3的配置信息

# 集群名称,保证唯一
cluster.name: my-elasticsearch
# 节点名称,必须不一样
node.name: node-03
# 必须为本机的ip地址
network.host: 127.0.0.1
# 服务端口号,在同一机器下必须不一样
http.port: 9202
# 集群间通信端口号,在同一机器下必须不一样
transport.tcp.port: 9302
# 设置集群自动发现机器ip集合
discovery.zen.ping.unicast.hosts: ["127.0.0.1:9300","127.0.0.1:9301","127.0.0.1:9302"]
启动三个节点,可以看到三个节点都加入到了集群

ES 基本概念

索引是文档(Document)的容器,是一类文档的集合。

索引这个词在 ElasticSearch 有三种意思:

  1. 索引(名词)

类比传统的关系型数据库领域来说,索引相当于SQL中的一个数据库(Database)。索引由其名称(必须为全小写字符)进行标识。

  1. 索引(动词)

保存一个文档到索引(名词)的过程。这非常类似于SQL语句中的 INSERT关键词。如果该文档已存在时那就相当于数据库的UPDATE。

  1. 倒排索引

关系型数据库通过增加一个B+树索引到指定的列上,以便提升数据检索速度。索引 ElasticSearch 使用了一个叫做 倒排索引 的结构来达到相同的目的。

Type 可以理解成关系数据库中Table。

之前的版本中,索引和文档中间还有个类型的概念,每个索引下可以建立多个类型,文档存储时需要指定index和type。从6.0.0开始单个索引中只能有一个类型,7.0.0以后将将不建议使用,8.0.0 以后完全不支持。

弃用该概念的原因 我们虽然可以通俗的去理解 Index 比作 SQL 的 Database,Type 比作 SQL 的 Table。但这并不准确,因为如果在 SQL 中, Table 之前相互独立,同名的字段在两个表中毫无关系。

但是在 ES 中,同一个 Index 下不同的 Type 如果有同名的字段,他们会被 Lucene 当作同一个字段 ,并且他们的定义必须相同。 所以我觉得Index现在更像一个表, 而 Type 字段并没有多少意义。目前 Type 已经被 Deprecated,在7.0开始,一个索引只能建一个 Type为 _doc

Index 里面单条的记录称为Document(文档)。等同于关系型数据库表中的行。 一个 Document 包括的字段

_index 文档所属索引名称。

_type 文档所属类型名。

_id Doc的主键。在写入的时候,可以指定该Doc的ID值,如果不指定,则系统自动生成一个唯一的UUID值。

_version 文档的版本信息。Elasticsearch通过使用version来保证对文档的变更能以正确的顺序执行,避免乱序造成的数据丢失。

_seq_no 严格递增的顺序号,每个文档一个,Shard级别严格递增,保证后写入的Doc的_seq_no大于先写入的Doc的_seq_no。

primary_term primary_term也和_seq_no一样是一个整数,每当Primary Shard发生重新分配时,比如重启,Primary选举等,_primary_term会递增1

found 查询的ID正确那么ture, 如果 Id 不正确,就查不到数据,found字段就是false。

_source 文档的原始JSON数据。

ES 核心技术

代表一个集群,集群中有多个节点,其中有一个为主节点,这个主节点是可以通过选举产生的,主从节点是对于集群内部来说的。ES 的一个概念就是去中心化,字面上理解就是无中心节点,这是对于集群外部来说的,因为从外部来看 ES 集群,在逻辑上是个整体,你与任何一个节点的通信和与整个 ES 集群通信是等价的

主节点的职责是负责管理集群状态,包括管理分片的状态和副本的状态,以及节点的发现和删除。 注意,主节点不负责对数据的增删改查请求进行处理,只负责维护集群的相关状态信息

集群状态查看

http://192.168.20.210:9200/_cluster/health?pretty

代表索引分片,ES 可以把一个完整的索引分成多个分片,这样的好处是可以把一个大的索引水平拆分成多个,分布到不同的节点上。构成分布式搜索,提高性能和吞吐量。

分片的数量只能在创建索引库时指定,索引库创建后不能更改

curl -H "Content-Type: application/json" -XPUT 'master:9200/test3/' -d '{"settings":{"number_of_shards":3}}'

默认是一个索引库有5个分片。 每个分片中最多存储2,147,483,519条数据。

代表索引副本,ES 可以给索引分片设置副本,当某个节点某个分片损坏或丢失时可以从副本中恢复。 另外可以提高 ES 的查询效率,ES 会自动对搜索请求进行负载均衡

副本的数量可以随时修改,可以在创建索引库的时候指定。

curl -H "Content-Type: application/json" -XPUT 'master:9200/test4/' -d '{"settings":{"number_of_replicas":3}}‘

默认是一个分片有1个副本。 注意,主分片和副本不会存在一个节点中

代表数据恢复或叫数据重新分布,ES 在有节点加入或退出时会根据机器的负载对索引分片进行重新分配,挂掉的节点重新启动时也会进行数据恢复。

代表 ES 索引的持久化存储方式ES 默认是先把索引存放到内存中,当内存满了时再持久化到硬盘当这个 ES 集群关闭再重新启动时就会从 gateway 中读取索引数据。 ES 支持多种类型的 gateway,有本地文件系统(默认),分布式文件系统,Hadoop的HDFS和Amazon的s3云存储服务。

代表 ES 的自动发现节点机制。 它 先通过广播寻找存在的节点,再通过多播协议来进行节点 之间的通信,同时也支持点对点的交互。 Zen Discovery 是与其他模块集成的,例如,节点之间的所有通信都使用 transport 模块完成。 某个节点通过 发现机制 找到其他节点是使用 Ping 的方式实现的

禁用自动发现机制

discovery.zen.ping.multicast.enabled: false

设置集群中master节点的初始列表,可以通过这些节点来自动发现新加入集群的节点。

discovery.zen.ping.unicast.hosts: ["192.168.20.210", "192.168.20.211", "192.168.20.212"]

代表 ES 内部节点或集群与客户端的交互方式,默认内部是 使用 TCP 协议进行交互,同时它支持 HTTP 协议(json格式)、 thirft、servlet、memcached、zeroMQ等的传输协议(通过插件方式集成)

例如,设置分片数量,副本数量。

查看

curl -XGET http://master:9200/test/_settings?pretty

创建索引是添加setting

curl -H "Content-Type: application/json" -XPUT
'http://master:9200/test5/' -d
'{"settings":{"number_of_shards":3,"number_of_replicas":2}}'

修改索引

curl -H "Content-Type: application/json" -XPUT 'http://master:9200/test5/_settings' -d '{"index":{"number_of_replicas":1}}'

映射 (mapping) 即模式定义。一个映射定义了字段类型,每个字段的数据类型,以及字段被 Elasticsearch 处理的方式。 映射还用于设置关联到类型上的元数据。 可以说,映射就是对索引库中索引的字段名称及其数据类型进行定义,类似于 mysql 中的表结构信息

映射可以分为动态映射显式映射

在关系数据库中,需要事先创建数据库,然后在该数据库实例下创建数据表,然后才能在该数据表中插入数据。 而 ElasticSearch 中不需要事先定义映射(Mapping),文档写入 ElasticSearch 时,会根据文档字段自动识别类型,这种机制称之为动态映射。 插入数据之后,可以使用 _mapping 查询索引的 mapping

curl -XGET localhost:9200/index/type/_mapping?pretty

在 ElasticSearch 中也可以事先定义好映射,包含文档的各个字段及其类型等,这种方式称之为显式映射

curl -H "Content-Type: application/json" -XPUT localhost:9200/books?pretty -d '
{
    "settings": {
        "number_of_shards": 3,
        "number_of_replicas": 0
    },
    "mappings": {
        "novel": {
            "properties": {
                "title": {
                    "type": "text"
                },
                "name": {
                    "type": "text",
                    "analyzer": "standard",
                    "search_analyzer": "standard"
                },
                "publish_date": {
                    "type": "date"
                },
            }
        }
    }
}

ES 通过猜测来确定字段类型,动态映射将会很有用,但如果需要对某些字段添加特殊属性(如:定义使用其它分词器、是否分词、是否存储等),就必须显式映射

ES 倒排索引结构

倒排索引(Inverted Index)也叫反向索引,有反向索引必有正向索引。 通俗地来讲,正向索引是通过 key 找 value,反向索引则是通过 value 找 key

当我们插入一个文档的时候

curl -X PUT "localhost:9200/user/_doc/1" -H 'Content-Type: application/json' -d'
{
    "name" : "Jack",
    "gender" : 1,
    "age" : 20
}

其实就是直接 PUT 一个 JSON 的对象,这个对象有多个字段,在插入这些数据到索引的同时,Elasticsearch还为这些字段建立索引—倒排索引,因为Elasticsearch最核心功能是搜索

那么,倒排索引是个什么样子呢?

スクリーンショット 2020-12-09 19 45 51

Term(单词):一段文本经过分析器分析以后就会输出一串单词,这一个一个的就叫做Term(直译为:单词) Term Dictionary(单词字典):顾名思义,它里面维护的是Term,可以理解为Term的集合 Term Index(单词索引):为了更快的找到某个单词,我们为单词建立索引 Posting List(倒排列表):倒排列表记录了出现过某个单词的所有文档的文档列表及单词在该文档中出现的位置信息,每条记录称为一个倒排项(Posting)。根据倒排列表,即可获知哪些文档包含某个单词

ES配置详解

elasticsearch 的 config 文件夹里面有两个配置文件:elasticsearch.yml 和 logging.yml。 第一个是 ES 的基本配置文件,第二个是日志配置文件。 ES 也是使用log4j来记录日志的,所以logging.yml里的设置按普通log4j配置文件来设置就行了。

下面主要讲解下elasticsearch.yml这个文件中可配置的东西。

cluster.name: elasticsearch
配置es的集群名称,默认是elasticsearch,es会自动发现在同一网段下的es,如果在同一网段下有多个集群,就可以用这个属性来区分不同的集群。

node.name: "Franz Kafka"
节点名,默认随机指定一个name列表中名字,该列表在es的jar包中config文件夹里name.txt文件中,其中有很多作者添加的有趣名字。

node.master: true
指定该节点是否有资格被选举成为node,默认是true,es是默认集群中的第一台机器为master,如果这台机挂了就会重新选举master。

node.data: true
指定该节点是否存储索引数据,默认为true。

index.number_of_shards: 5
设置默认索引分片个数,默认为5片。

index.number_of_replicas: 1
设置默认索引副本个数,默认为1个副本。

path.conf: /path/to/conf
设置配置文件的存储路径,默认是es根目录下的config文件夹。

path.data: /path/to/data
设置索引数据的存储路径,默认是es根目录下的data文件夹,可以设置多个存储路径,用逗号隔开,例:
path.data: /path/to/data1,/path/to/data2

path.work: /path/to/work
设置临时文件的存储路径,默认是es根目录下的work文件夹。

path.logs: /path/to/logs
设置日志文件的存储路径,默认是es根目录下的logs文件夹

path.plugins: /path/to/plugins
设置插件的存放路径,默认是es根目录下的plugins文件夹

bootstrap.mlockall: true
设置为true来锁住内存。因为当jvm开始swapping时es的效率会降低,所以要保证它不swap,可以把ES_MIN_MEM和ES_MAX_MEM两个环境变量设置成同一个值,并且保证机器有足够的内存分配给es。同时也要允许elasticsearch的进程可以锁住内存,Linux下可以通过`ulimit -l unlimited`命令。

network.bind_host: 192.168.0.1
设置绑定的ip地址,可以是ipv4或ipv6的,默认为0.0.0.0。

network.publish_host: 192.168.0.1
设置其它节点和该节点交互的ip地址,如果不设置它会自动判断,值必须是个真实的ip地址。

network.host: 192.168.0.1
这个参数是用来同时设置bind_host和publish_host上面两个参数。

transport.tcp.port: 9300
设置节点间交互的tcp端口,默认是9300。

transport.tcp.compress: true
设置是否压缩tcp传输时的数据,默认为false,不压缩。

http.port: 9200
设置对外服务的http端口,默认为9200。

http.max_content_length: 100mb
设置内容的最大容量,默认100mb

http.enabled: false
是否使用http协议对外提供服务,默认为true,开启。

gateway.type: local
gateway的类型,默认为local即为本地文件系统,可以设置为本地文件系统,分布式文件系统,Hadoop的HDFS,和amazon的s3服务器,其它文件系统的设置方法下次再详细说。

gateway.recover_after_nodes: 1
设置集群中N个节点启动时进行数据恢复,默认为1。

gateway.recover_after_time: 5m
设置初始化数据恢复进程的超时时间,默认是5分钟。

gateway.expected_nodes: 2
设置这个集群中节点的数量,默认为2,一旦这N个节点启动,就会立即进行数据恢复。

cluster.routing.allocation.node_initial_primaries_recoveries: 4
初始化数据恢复时,并发恢复线程的个数,默认为4。

cluster.routing.allocation.node_concurrent_recoveries: 2
添加删除节点或负载均衡时并发恢复线程的个数,默认为4。

indices.recovery.max_size_per_sec: 0
设置数据恢复时限制的带宽,如入100mb,默认为0,即无限制。

indices.recovery.concurrent_streams: 5
设置这个参数来限制从其它分片恢复数据时最大同时打开并发流的个数,默认为5。

discovery.zen.minimum_master_nodes: 1
设置这个参数来保证集群中的节点可以知道其它N个有master资格的节点。默认为1,对于大的集群来说,可以设置大一点的值(2-4)

discovery.zen.ping.timeout: 3s
设置集群中自动发现其它节点时ping连接超时时间,默认为3秒,对于比较差的网络环境可以高点的值来防止自动发现时出错。

discovery.zen.ping.multicast.enabled: false
设置是否打开多播发现节点,默认是true。

discovery.zen.ping.unicast.hosts: ["host1", "host2:port", "host3[portX-portY]"]
设置集群中master节点的初始列表,可以通过这些节点来自动发现新加入集群的节点。

下面是一些查询时的慢日志参数设置
index.search.slowlog.level: TRACE
index.search.slowlog.threshold.query.warn: 10s
index.search.slowlog.threshold.query.info: 5s
index.search.slowlog.threshold.query.debug: 2s
index.search.slowlog.threshold.query.trace: 500ms

index.search.slowlog.threshold.fetch.warn: 1s
index.search.slowlog.threshold.fetch.info: 800ms
index.search.slowlog.threshold.fetch.debug:500ms
index.search.slowlog.threshold.fetch.trace: 200ms

ES Restful 操作

Index API

索引库名称必须要全部小写,不能以下划线开头,也不能包含逗号。

PUT /my-index-000001
curl -X PUT "localhost:9200/my-index-000001?pretty"

更完整的

PUT /test
{
  "settings": {
    "number_of_shards": 1
  },
  "mappings": {
    "properties": {
      "field1": { "type": "text" }
    }
  }
}

CURL

curl -X PUT "localhost:9200/test?pretty" -H 'Content-Type: application/json' -d'
{
  "settings": {
    "number_of_shards": 1
  },
  "mappings": {
    "properties": {
      "field1": { "type": "text" }
    }
  }
}
DELETE /my-index-000001
curl -X DELETE "localhost:9200/my-index-000001?pretty"
GET /my-index-000001
curl -X GET "localhost:9200/my-index-000001?pretty"
HEAD /my-index-000001
curl -I "localhost:9200/my-index-000001?pretty"

针对部分索引,我们暂时不需要对其进行读写,可以临时关闭索引,以减少es服务器的开销。 索引关闭后, 对集群的相关开销基本降低为 0。但是无法被读取和搜索当需要的时候, 可以重新打开,索引恢复正常

curl -X POST "localhost:9200/my-index-000001/_close?pretty"
curl -X POST "localhost:9200/my-index-000001/_open?pretty"
PUT /<index>/_shrink/<target-index>
curl -X POST "localhost:9200/my-index-000001/_shrink/shrunk-my-index-000001?pretty"

比如

POST /my_source_index/_shrink/my_target_index
{
  "settings": {
    "index.routing.allocation.require._name": null, 
    "index.blocks.write": null 
  }
}

缩小索引是指将原索引分片数缩小到一定数量。 但缩小的数量必须为原数量的因子(即原分片数量是新分片倍数),例如8个分片可以缩小到4、2、1个分片。如果原分片数量为素数则只能缩小到一个分片。在缩小开始时,每个分片的复制都必须在同一节点(node)存在。

将索引的分片数拆分成多个。

POST my_source_index/_split/my_target_index
{
  "settings": {
    "index.number_of_shards": 5 
    "index.blocks.write": true // 只读
  },
  "aliases": {
    "my_search_indices": {}
  }
}
POST /my_source_index/_clone/my_target_index
{
  "settings": {
    "index.number_of_shards": 5 
  },
  "aliases": {
    "my_search_indices": {}
  }
}

注意,前提是原index,需要标注为只读

/my_source_index/_settings
{
  "settings": {
    "index.blocks.write": true
  }
}

Rollover 使您可以根据索引大小,文档数或使用期限自动过渡到新索引。 当 Rollover 触发后,将创建新索引,写别名(write alias)将更新为指向新索引,所有后续更新都将写入新索引

基于大小,文档数或使用期限过渡至新索引是比较适合的。

ElasticSearch 6.3引入了一项新的 Rollover功能,该功能

  1. 以紧凑的聚合格式保存旧数据
  2. 仅保存您感兴趣的数据
スクリーンショット 2020-12-09 16 07 34
POST /alias1/_rollover/my-index-000002
{
  "conditions": {
    "max_age":   "7d",
    "max_docs":  1000,
    "max_size": "5gb"
  }
}

在这里,我们定义了三个条件

  1. 如果时间超过7天,那么自动rollover,也就是使用新的 index
  2. 如果文档的数目超过1000个,那么自动 rollover
  3. 如果index的大小超过5G,那么自动 rollover
POST /my-index-000001/_freeze
POST /my-index-000001/_unfreeze
PUT /my-index-000001/_alias/alias1
curl -X PUT "localhost:9200/my-index-000001/_alias/alias1?pretty"

当我们修改了我们的 index 的 mapping,让后通过 reindex API 来把我们的现有的 index 转移到新的 index 上, 那么如果在我们的应用中,我们利用 alias 就可以很方便地做这件事。在我们成功转移到新的 index 之后,我们只需要重新定义我们的 alias 指向新的 index,而在我们的客户端代码中,我们一直使用 alias 来访问我们的 index,这样我们的代码不需要任何的改动。

スクリーンショット 2020-12-08 16 04 56
PUT /my-index-000001/_mapping
{
  "properties": {
    "email": {
      "type": "keyword"
    }
  }
}
GET /my-index-000001/_mapping
PUT /my-index-000001/_settings
{
  "index" : {
    "number_of_replicas" : 2
  }
}

索引可使用预定义的模板进行创建,这个模板称作Index templates。 模板设置包括 settings 和 mappings,通过模式匹配的方式使得多个索引重用一个模板

curl -XPUT localhost:9200/_template/template_1 -d '
{
    "template" : "te*",
    "order" : 0,
    "settings" : {
        "number_of_shards" : 1
    },
    "mappings" : {
        "type1" : {
            "_source" : {"enabled" : false }
        }
    }
}

上述定义的模板 template_1 将对用 te 开头的新索引都是有效。

删除模板

curl -XDELETE localhost:9200/_template/template_1

查看定义的模板

curl -XGET localhost:9200/_template/template_1

当存在多个索引模板时并且某个索引两者都匹配时,settings 和 mpapings 将合成一个配置应用在这个索引上。 合并的顺序可由索引模板的order属性来控制。(order为1的配置将覆盖order为0的配置)。

可以返回多个索引的状态信息

GET /index1,index2/_stats
curl -X GET "localhost:9200/index1,index2/_stats?pretty"

只返回 merge和refresh 状态

GET /_stats/merge,refresh

ES 的字段可以过滤缓存,那么与之相反的就是清除缓存。 单一索引缓存,多索引缓存和全部缓存的清理

清空全部缓存

curl localhost:9200/_cache/clear?pretty

清除单一索引缓存

curl localhost:9200/index/_cache/clear?pretty

清除多索引缓存

curl localhost:9200/index1,index2,index3/_cache/clear?pretty

fresh 当索引一个文档,文档先是被存储在内存里面默认1秒后,会进入文件系统缓存这样该文档就可以被搜索到但是该文档还没有存储到磁盘上,如果机器宕机了,数据就会丢失。 因此 fresh 实现的是从内存到文件系统缓存的过程

POST /my-index-000001,my-index-000002/_refresh
curl -X POST "localhost:9200/my-index-000001,my-index-000002/_refresh?pretty"

Flush 是用于 translog 的。 ES为了数据的安全,在接受写入文档的时候,在写入内存buffer的同时,会写一份translog日志,从而在出现程序故障或磁盘异常时,保证数据的安全。 Flush 会触发 lucene commit, 并清空 translog 日志文件

translog的 Flush 是 ES 在后头自动运行的。 默认情况下 ES 每隔 5s 会去检测要不要flush translog,默认条件下,每 30 分钟主动进行一次 flush,或者当 translog 文件大小大于 512MB主动进行一次 flush

每次 index、bulk、delete、update 完成的时候,一定触发flush translog 到磁盘上,才给请求返回 200 OK。这个改变提高了数据安全性,但是会对写入的性能造成不小的影响。 在写入效率优先的情况下,可以在 index template 里设置如下参数

"index.translog.durability":"async"  // 这相当于关闭了index、bulk等操作的同步flush translog操作,仅使用默认的定时刷新、文件大小阈值刷新的机制
"index.translog.sync_interval":30s (默认是5s)
POST /my-index-000001/_flush
スクリーンショット 2020-12-09 18 43 48

一个 ES 索引由若干个分片组成,一个分片有若干个 Lucene 分段,较大的 Lucene 分段可以更有效的存储数据。 使用 _forcemerge API 来对分段执行合并操作,通常,我们将分段合并为一个单个的分段 max_num_segments=1

执行 forcemerge, 这个过程可能执行的时间比较久。

POST myindex/_forcemerge?max_num_segments=1

Document API

GET <index>/_doc/<_id>
HEAD <index>/_doc/<_id>
GET <index>/_source/<_id>
HEAD <index>/_source/<_id>

Source Filter

GET my-index-000001/_doc/0?_source=false
GET my-index-000001/_doc/0?_source_includes=*.id&_source_excludes=entities

查询文档的结果

GET my-index-000001/_doc/0
{
  "_index": "my-index-000001",
  "_type": "_doc",
  "_id": "0",
  "_version": 1,
  "_seq_no": 0,
  "_primary_term": 1,
  "found": true,
  "_source": {
    "@timestamp": "2099-11-15T14:12:12",
    "http": {
      "request": {
        "method": "get"
      },
      "response": {
        "status_code": 200,
        "bytes": 1070000
      },
      "version": "1.1"
    },
    "source": {
      "ip": "127.0.0.1"
    },
    "message": "GET /search HTTP/1.1 200 1070000",
    "user": {
      "id": "kimchy"
    }
  }
}

查询一个文档是否存在,存在返回的状态码是200,不存在返回404。

HEAD my-index-000001/_doc/0

只返回_source字段

GET my-index-000001/_source/1

Elasticsearch 中的 _source、_all、store和 index 属性

_source 字段解释

スクリーンショット 2020-12-09 19 49 36

_source 字段默认是存储的, 什么情况下不用保留 _source 字段? 如果某个字段内容非常多,业务里面只需要能对该字段进行搜索,最后返回文档id,查看文档内容会再次到 mysql 或者 hbase 中取数据,把大字段的内容存在 Elasticsearch 中只会增大索引,这一点文档数量越大结果越明显。 如果想要关闭 _source 字段,在 mapping 中的设置如下

{
    "yourtype":{
        "_source":{
            "enabled":false
        },
        "properties": {
            ... 
        }
    }
}

_all 字段 _all 字段里面包含了一个文档里面的所有信息,是一个超级字段。 _all 字段默认是关闭的

那么文档索引到 Elasticsearch 的时候,默认情况下是对所有字段创建倒排索引的,某个字段是否生成倒排索引是由字段的index属性控制的

store 属性 关键字高亮实质上是根据倒排记录中的词项偏移位置,找到关键词,加上前端的高亮代码。 这里就要说到store 属性,store 属性用于指定是否将原始字段写入索引,默认取值为no。 如果在 Lucene 中,高亮功能和 store 属性是否存储息息相关,因为需要根据偏移位置到原始文档中找到关键字才能加上高亮的片段。在Elasticsearch,因为 _source 中已经存储了一份原始文档,可以根据 _source 中的原始文档实现高亮,在索引中再存储原始文档就多余了,所以 Elasticsearch 默认是把store属性设置为 no

如果想要对某个字段实现高亮功能,_source 和 store 至少保留一个

PUT /<target>/_doc/<_id>
POST /<target>/_doc/
PUT /<target>/_create/<_id>
POST /<target>/_create/<_id>

比如

PUT my-index-000001/_create/1
{
  "@timestamp": "2099-11-15T13:12:00",
  "message": "GET /search HTTP/1.1 200 1070000",
  "user": {
    "id": "kimchy"
  }
}

返回

{
  "_shards": {
    "total": 2,
    "failed": 0,
    "successful": 2
  },
  "_index": "my-index-000001",
   "_type": "_doc",
  "_id": "1",
  "_version": 1,
  "_seq_no": 0,
  "_primary_term": 1,
  "result": "created"
}

更新和删除

POST /<index>/_update/<_id>
DELETE /<index>/_doc/<_id>
GET /_mget
{
  "docs": [
    {
      "_index": "my-index-000001",
      "_id": "1"
    },
    {
      "_index": "my-index-000001",
      "_id": "2"
    }
  ]
}
POST _bulk
{ "index" : { "_index" : "test", "_id" : "1" } }
{ "field1" : "value1" }
{ "delete" : { "_index" : "test", "_id" : "2" } }
{ "create" : { "_index" : "test", "_id" : "3" } }
{ "field1" : "value3" }
{ "update" : {"_id" : "1", "_index" : "test"} }
{ "doc" : {"field2" : "value2"} }
POST /my-index-000001/_delete_by_query
{
  "query": {
    "match": {
      "user.id": "elkbee"
    }
  }
}

比如,将资产表中area为空的字段赋值为'无'

POST soc-system/_update_by_query
{
  "script": {
    "source": "ctx._source['area']='无'" 
  },
  "query": {
    "bool": {
      "must_not": [
        {
          "exists": {
            "field": "area"
          }
        }
      ]
    }
  }
}
  1. 当你的数据量过大,而你的索引最初创建的分片数量不足,导致数据入库较慢的情况,此时需要扩大分片的数量,此时可以尝试使用Reindex

  2. 当数据的mapping需要修改,但是大量的数据已经导入到索引中了,重新导入数据到新的索引太耗时。但是在ES中,一个字段的mapping在定义并且导入数据之后是不能再修改的,这种情况下也可以考虑尝试使用Reindex。

ES 提供了 _reindex 这个API。相对于我们重新导入数据肯定会快不少,实测速度大概是bulk导入数据的5-10倍。

POST _reindex
{
  "source": {
    "index": "old_index"
  },
  "dest": {
    "index": "new_index"
  }
}

Search API

curl -X GET "localhost:9200/bank/_search" -H 'Content-Type: application/json' -d'
{
  "query": { "match_all": {} },
  "sort": [
    { "account_number": "asc" }
  ]
}

一些例子,只匹配account_number=20的文档

GET /bank/_search
{
  "query": { "match": { "account_number": 20 } }
}

只匹配address属性中含有mill这个词的文档(不区分大些写)

GET /bank/_search
{
  "query": { "match": { "address": "mill" } }
}

匹配address属性中含有mill或者lane这些词的文档(不区分大小写)

GET /bank/_search
{
  "query": { "match": { "address": "mill lane" } }
}

匹配address中含有"mill lane"短语的文旦,而不是像上面那样只匹配其中的词

GET /bank/_search
{
  "query": { "match_phrase": { "address": "mill lane" } }
}

同时匹配address中含有的mill和lane词的文档

GET /bank/_search
{
  "query": {
    "bool": {
      "must": [
        { "match": { "address": "mill" } },
        { "match": { "address": "lane" } }
      ]
    }
  }
}

匹配address中既不包含“mill”也不包含“lane”的文档

GET /bank/_search
{
  "query": {
    "bool": {
      "must_not": [
        { "match": { "address": "mill" } },
        { "match": { "address": "lane" } }
      ]
    }
  }
}

当然还有更复杂的搜索,比如和过滤器结合 range是进行范围内的筛选,gt表示大于,gte表示大于等于,相反的,lt表示小于,lte表示小于等于

GET /megacorp/employee/_search
{
    "query": {
        "bool": {
            "must": {
                "match": {
                    "likes": "2"
                }
            },
            "filter": {
        // 过滤处age大于2的结果
                "range": {
                    "age": {
                        "gt": 2
                    }
                }
            }
        }
    }
}

bool 中除了 must 条件还有 should 条件只要匹配上其中的一个条件即可,匹配address中含有mill或者lane的文档

GET /bank/_search
{
  "query": {
    "bool": {
      "should": [
        { "match": { "address": "mill" } },
        { "match": { "address": "lane" } }
      ]
    }
  }
}

使用 bool 搜索可以组成多级逻辑关系的匹配

GET /bank/_search
{
    "query": {
        "bool": {
            "must": [
                {"match": {"age": 40}}
            ],
            "must_not": [
                {"match": {"state": "ID"}}
            ],
            "should": [
                {"match": {"address": "mill"}},
                {"match": {"address": "lane"}}
            ]
        }
    }
}

分页

GET /bank/_search
{
  "query": { "match_all": {} },
  "from": 10,
  "size": 10
}

相当于一次查询,发送多个查询API。

GET my-index-000001/_msearch
{ }
{"query" : {"match" : { "message": "this is a test"}}}
{"index": "my-index-000002"}
{"query" : {"match_all" : {}}}

深度分页,ES 对于 from+size 的个数是有限制的,二者之和不能超过1w。 当所请求的数据总量大于1w时,可用 scroll 来代替from+size

首次查询使用方式如下,scroll=1m 表示这个游标要保持开启1分钟,size 表示每次回传的件数

curl -XGET 'localhost:9200/twitter/tweet/_search?scroll=1m&pretty' -H 'Content-Type: application/json' -d'
{
    "size": 100,
    "query": {
        "match" : {
            "title" : "elasticsearch"
        }
    }
}

使用初始化返回的_scroll_id来进行请求,每一次请求都会继续返回初始化中未读完数据,并且会返回一个_scroll_id,这个_scroll_id可能会改变,因此每一次请求应该带上上一次请求返回的_scroll_id。会返回100条数据,放在 hits 字段。 接下来的查询方式如下

curl -XGET 'localhost:9200/_search/scroll?pretty' -H 'Content-Type: application/json' -d'
{
    "scroll" : "1m", 
    "scroll_id" : "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAD4WYm9laVYtZndUQlNsdDcwakFMNjU1QQ==" 
}

如果没有数据了,就会回传空的hits,可以用这个判断是否遍历完成了数据。

{
    "_scroll_id": "DnF1ZXJ5VGhlbkZldGN......",
    "took": 2,
    "timed_out": false,
    "_shards": {
        "total": 5,
        "successful": 5,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": 84,
        "max_score": null,
        "hits": []
    }
}

Suggest查询建议。 查询建议,能够为用户提供良好的使用体验。主要包括:拼写检查和自动建议查询词(自动补全)。

查询建议也是使用 _search 端点地址,在DSL中suggest节点来定义需要的建议查询。

POST twitter/_search
{
  "query" : {
    "match": {
      "message": "tring out Elasticsearch"
    }
  },
  "suggest" : {
    "my-suggestion" : {  # 一个查询建议名称
      "text" : "tring out Elasticsearch",  #查询文本
      "term" : { 
        "field" : "message"  #指定在哪个字段上获取建议词
      }
    }
  }
}

#多个建议查询可以使用全局的查询文本
POST _search
{
  "suggest": {
    "text" : "tring out Elasticsearch",
    "my-suggest-1" : {
      "term" : {
        "field" : "message"
      }
    },
    "my-suggest-2" : {
       "phrase" : {
        "field" : "user"
       }
    }
  }
}

term suggester term 词项建议器,对给入的文本进行分词,为每个词进行模糊查询提供词项建议。

phrase suggester phrase 短语建议,在term的基础上,会考量多个term之间的关系,比如是否同时出现在索引的原文里,相邻程度,以及词频等

completion suggester 自动补全 针对自动补全场景而设计的建议器。此场景下用户每输入一个字符的时候,就需要即时发送一次查询请求到后端查找匹配项,在用户输入速度较高的情况下对后端响应速度要求比较苛刻。只能用于前缀查找,这也是Completion Suggester的局限所在。

为了使用自动补全,索引中用来提供补全建议的字段需特殊设计,字段类型为 completion。 比如,定义一个索引

PUT music
{
    "mappings": {
        "_doc" : {
            "properties" : {
                "suggest" : {  
                    "type" : "completion"  #定义该字段是自动补全的字段
                },
                "title" : {
                    "type": "keyword"
                }
            }
        }
    }
}

Highlighting 高亮显示

查询单个字段

GET /artisan_index/artisan_type/_search 
{
  "query": {
    "match": {
      "title": "小工匠"
    }
  },
  "highlight": {
    "fields": {
      "title": {}
    }
  }
}

返回结果

スクリーンショット 2020-12-10 15 56 05

查询多个字段

GET /blog_website/blogs/_search 
{
  "query": {
    "bool": {
      "should": [
        {
          "match": {
            "title": "博客"
          }
        },
        {
          "match": {
            "content": "博客"
          }
        }
      ]
    }
  },
  "highlight": {
    "fields": {
      "title": {},
      "content": {}
    }
  }
}

返回结果

スクリーンショット 2020-12-10 15 57 09

JAVA 客户端开发

スクリーンショット 2020-12-10 20 46 33

ES支持的客户端连接方式

  1. REST API http 请求,可以利用 Postman 等工具发起 REST 请求,java 发起 httpClient 请求等。

  2. Transport 连接 socket连接,用官方提供的 Transport 客户端,底层是netty。

注意,ES 的发展规划中在 7.0 版本 开始将废弃 TransportClient,8.0版本中将完全移除 TransportClient,取而代之的是High Level REST Client

Java REST Client

ES 提供了两个JAVA REST client 版本。

低级别的 REST 客户端,通过 http 与集群交互,用户需自己编组请求 JSON 串,及解析响应 JSON 串。兼容所有 ES 版本。

高级别的 REST 客户端,基于低级别的 REST 客户端,增加了编组请求JSON串、解析响应JSON串等相关api。 使用的版本需要保持和 ES 服务端的版本一致,否则会有版本问题。 官方推荐使用高级版。

每个API 支持 同步/异步 两种方式,同步方法直接返回一个结果对象。异步的方法以 async 为后缀,通过 listener 参数来通知结果。 高级java REST 客户端依赖 Elasticsearch core project 和 java1.8

Maven依赖

<dependency>
    <groupId>org.elasticsearch.client</groupId>
    <artifactId>elasticsearch-rest-high-level-client</artifactId>
    <version>6.2.4</version>
</dependency>

Java High Level REST Client 初始化

RestHighLevelClient client = new RestHighLevelClient(
        RestClient.builder(
                new HttpHost("localhost", 9200, "http"),
                new HttpHost("localhost", 9201, "http")));

给定集群的多个节点地址,将客户端负载均衡地向这个节点地址集发请求。Client 不再使用了,记得关闭它。

client.close();

创建索引,创建文档,查询文档,Search,bulk,highlight,suggest建议,聚合等使用实例,可以参考 -> 常用的API事例

ElasticSearch JAVA 官方 API

Elastic Stack 应用宝典 笔记

第2章 ElasticSearch 原理与实现

ElasticSearch 具有强大的文档检索和文档分析能力,ElasticSearch 是一中全文检索和分析引擎。 ElasticSearch 也包含了强大的数据存储能力,它所检索的数据不依赖于外部数据源,而是由 ElasticSearch 统一管理的,因此,ElasticSearch 也可以被归类为一种基于文档的 NoSQL 数据库

全文检索和倒排索引

非结构化的数据的检索,像文章,网页,邮件这种全文本数据的检索,就是全文检索

ElasticSearch 中的索引是倒排索引,是一种专门应用于全文检索的索引类型。 倒排索引就是先将文档中包含的关键字全部提取出来,然后再将关键字与文档的对应关系保存起来。 最后再对关键字本身做索引排序。用户在检索一个关键字时,可以先对关键字的索引进行查询,再通过关键字与文档的对应关系找到所在文档。有了倒排索引,用户检索就可以快速定位到包含关键字的文档

另外,全文检索中,提取关键字是非常重要的一步,这些预先提取的关键字,在 ElasticSearch 及全文检索中,一般称为词项Term)。文档的词项提取在 ElasticSearch 中称为文档分析。

ElasticSearch 索引

在 ElasticSearch 中,添加或者更新文档时,最重要的动作就是将它们编入倒排索引没有被编入倒排索引的文档将不能被检索。也就是说,ElasticSearch 中所有的数据的检索都必须要通过倒排索引来检索,离开了倒排索引,文档就相当于不存在。

为了提高性能,文档在添加到 ElasticSearch 中并不会立即被编入索引。默认会 1s 统一处理一次新加入的文档。 可以通过 index.refresh_interval 参数修改。ElasticSearch 也提供了强制刷新的方法,包括使用 _refresh 接口和使用 refres h参数。

ElasticSearch 映射

索引是存储文档的容器,文档在存储前会做文档分析并编入倒排索引,文档从全文数据到索引的转变是由映射 Mapping 定义的。 映射介于文档与文档之间。

数据存储和检索的基本单元是文档。ElasticSearch 的文档采用 JSON 格式。 ElasticSearch 支持全文检索,为什么要预先定义文档字段和数据类型呢? 首先,全文检索在存储钱需要做分析并提取词项,但在文档中并不是所有的数据都需要这么做。比如,文档创建时间,文章标题,作者等,这些数据本来就是结构化的,没有不要再做分析。此外,一些结构化的数据在检索时需要做精确匹配,如果做了文档分析并提取词项后,反而做不了精确匹配了。当然,ElasticSearch 也不是一定要预先定义文档字段,也支持动态映射文档字段

映射类型 Mapping type 是定义文档与索引映射关系的一种方式。但是这个概念,在 ElasticSearch 官方已经准备废弃。 在7.0开始,一个索引只能建一个 Type为 _doc。8.0 之后会删除这个概念。

文档字段

由于文档中的数据是分散在各个字段中,所以索引文档肯定都是针对文档字段进行的。索引文档应该是以字段为单位对文档做索引,而并非是以整个文档的内容做索引。

在默认情况下,文档所有的字段都会创建倒排索引。这可以通过 index 参数来设置,默认值是 true。即字段会被编入索引。 对于 text 类型的字段,他们会被解析为词项后再以词项为单位编入索引。

{
PUT s9
{
  "mappings": {
    "properties": {
      "t1":{
        "type": "text",
        "index": true      
      },
      "t2": {
        "type": "text",
        "index": false    # index为false时es不会为该属性创建索引
      }
    }
  }
}

在Elasticsearch 5之前,index属性的取值有三个:

  1. analyzed:字段被索引,会做分词,可搜索。反过来,如果需要根据某个字段进搜索,index属性就应该设置为analyzed
  2. not_analyzed:字段值不分词,会被原样写入索引。反过来,如果某些字段需要完全匹配,比如人名、地名,index属性设置为not_analyzed为佳。
  3. no:字段不写入索引,当然也就不能搜索。反过来,有些业务要求某些字段不能被搜索,那么index属性设置为no即可。

编入索引的信息包括文档ID,词频,词项在字段中的次序,词项在字段的起止偏移量。 默认情况下,text 类型的字段会保存文档 ID,词频,词序。其余类型的字段只保存文档 ID。 可以在映射字段时,通过 index_option 参数来设定,docs, freqs, postions, offsets 四个选项。 其中, docs 只有文档ID会编入索引,freqs是文档ID+词频,postions是文档ID+词频+词序,offsets是文档ID+词频+词序+偏移量。

上面知道,只有文档ID,词频等信息会被编入索引,字段原始值不会被编入索引。 但索引提供了一个叫 _source 的字段用于存储整个文档的原始值。 不需要保存原始值的话,可以设置

PUT /users
{
  "mappings": {
    "_source": {
      "enabled" : false
    }
  }
}

通常,不推荐关闭 _source 字段。关闭 _source 字段,比如 reindex,高亮检索结果等功能将无法使用。 _source 字段保存的源文档信息是 JSON 形式的最原始文档。因为是整体的数据,如果需要使用文档中某一个字段进一步聚合运算就可以麻烦,针对这个情况,ElasticSearch 提供了另外一种机制保存字段值,就是文档值 ( Doc Value ) 机制。 简单的说,_source 是将源文档揉在一起保存,而文档值则将他们按照字段分别保存在不同的列中

默认情况下,所有非 text 类型的字段都是支持文档值机制,并且都是开启的。text类型目前不支持。 可以通过字段 doc_values 参数开关。

对于 text 类型的字段来说,ElasticSearch 提供了另外一种称为 fielddata 的机制。 默认情况下,fielddata机制是关闭的,因为非常消费资源,而且使用 text 类型做聚合,排序也不合理,尽量不要使用 fielddata 机制

文档字段可以分为两类,一类是元字段,一类是用户自定义字段。 元字段不需要定义,在任一文档中都存在,比如_id 就是一个元字段。元字段都是已下划线开头。

_id, _source, _index(文档所属索引),_size(_source的字节数),_type(文档所属映射类型),_all(6.0 之后被废弃) 等

index.mapping.total_fields.limit 定义了索引中最大的字段数。默认是 1000。

字段数据类型

通过映射类型的 properties 字段,可以定义映射类型包含的字段及其数据类型。

text 和 keyword 两种类型。两者区别在于,text 类型在存储前会做词项分析,而 keyword 类型则不会text 类型可以通过 analyzer 设置该字段的分析器,而 keyword 没有这个参数。 由于词项分析,text 类型在编入索引后,可以通过词项做检索,但不能通过字段整体整体值做检索。keyword 则相反。 所以,text 一般用于存储全文数据,比如日志信息,文章正文,邮件内容等,而 keyword 用于存储结构化的文本数据

long, integer, short, byte, double, float, half_float, scaled_float

date 和 data_nanos

boolean

integer_range, float_range, long_range, double_range, date_range, ip_range 。

定义

PUT /users
{
  "mappings": {
    "properties": {
      "age_range" : {
        "type": "integer_range"
      }
    }
  }
}

添加

POST /users/_doc
{
  "age_range": {
    "lt": 23,
    "gt": 7,
  }
}

查询

POST /users/_search
{
  "query": {
    "term": {
      "age_range": {
        "value": 10
      }
    }
  }
}

不需要使用类似 array 这样的名称声明数组类型。而是通过添加文档时候,使用 [ ] 来确认该字段是数组。

PUT /person/doc/1
{
  "relation": ["12","2"]
}

和数组类似,没有定义 object 类型,而是通过添加文档时候,使用 { } 来确认该字段是对象。 但是,对象类型在定义索引的时候是可以声明的。

PUT colleges
{
  "mappings": {
    "properties": {
      "address": {
        "properties": {
          "country" :{
            "type": "keyword"
          }
          "city": {
            "type": "keyword"
          }
        }
      }
    }
  }
}

有一些字段,可能会以不同的方式来检索,比如标题。如果设置为 text,那么都会提取词项,不能整体检索,如果设置为 keyword,就不能分词项检索。所以针对 text 和 keyword,ElasticSearch 提供了用于配置字段多数据类型的参数 fields,它能让一个字段同时具有两种类型的特征

PUT colleges
{
  "mappings": {
    "properties": {
      "title": {
        "type": "text",
        "fields": {
          "raw" :{
            "type": "keyword"
          }
          "length": {
            "type": "token_count",
            "analyzer": "standard"
          }
        }
      }
    }
  }
}

title 字段是 text,同时通过 fields 参数又为该字段添加了两个子字段,一个是 raw,是 keyword 类型,一个是 length,是 token_count 类型。使用 fields 设置的子字段,添加文档的时候,不需要单独设置值,他们与 title 共享同样的数据。 这样,title 字段在编入索引的时候,会将字段值做分析并提取词项,title.raw 则是按照 keyword 将整个值编入索引token_count 类型,会将字符串做分析并提取词项,然后把词项的数量保存下来。所以,token_count 类型字段必须要通过 analyzer 参数设置提取词项的分析器

分片与复制

ElasticSearch 必须解决海量文档存储的问题,不仅要解决文档能存得下的问题,还要保证他们不会因为故障而丢失,同时还要保证文档的检索速度尽可能不受文档数量增加的影响。

解决大数据存储的通用方法称为分片,它的核心思想是将数据分解成大小合适的片段,然后再将它们存储到集群中不同的节点上。因为 ElasticSearch 支持分片,所以理论上它存储的文档容量上是没有上限的。 分片的基础是要创建集群,ElasticSearch 创建集群非常简单,只要集群中的节点在相互连接的网络中,并且具有相同的集群名即可。elasticsearch.yml 文件中,cluster.name 就是集群名,默认是 elasticsearch 。 创建了 ElasticSearch 集群后,就需要确定索引分片的数量,分片一般会均匀地分散在不同的节点中,这就将存储和负载分散到了集群中不同的节点。索引分片数量是在创建索引时通过 number_of_shards 参数设置的。

PUT /my_source_index
{
  "settings": {
    "index.number_of_shards": 5 
  }

在 ElasticSearch 中,确定文档存储在哪一个分片中的机制被称为路由。 一条数据是如何落地到对应的shard上的? 当索引一个文档的时候,文档会被存储到一个主分片中。 Elasticsearch 如何知道一个文档应该存放到哪个分片中呢?

スクリーンショット 2020-12-09 20 44 52

这个过程是根据下面这个算法决定的

shard_num = hash(_routing) % num_primary_shards

其中 _routing 是一个可变值,默认是文档的 _id 的值 ,也可以设置成一个自定义的值。 _routing 通过 hash 函数生成一个数字,然后这个数字再除以 num_of_primary_shards (主分片的数量)后得到余数 。 这个分布在 0 到 number_of_primary_shards-1 之间的余数,就是我们所寻求的文档所在分片的位置

这就解释了为什么我们要在创建索引的时候就确定好主分片的数量,并且永远不会改变这个数量。因为如果数量变化了,那么所有之前路由的值都会无效,文档也再也找不到了。

为了提升检索效率,ElasticSearch 在检索文档时,并不会将所有的分片整合在一起做检索,而是先根据路由规则路由到具体的分片,然后再在分片上根据检索条件查找文档

スクリーンショット 2020-12-09 20 48 55

比如,发送一个索引或者删除的请求给 node1,node1 接收到请求之后,计算得到了分片的位置是,shard0,然后就是 node1 通过元数据信息可以知道 shard0 在具体哪一个节点,于是 node1 会把请求转发给 node3。 node3 接收到请求之后会将请求并行的分发给 shard0 的所有 replica shard 之上,也就是存在于 node 1 和 node 2 中的 replica shard。如果所有的 replica shard 都成功地执行了请求,那么将会向 node 3 回复一个成功确认,当 node 3 收到了所有 replica shard 的确认信息后,则最后向用户返回一个 Success 的消息。

自定义路由

自定义路由的方式非常简单,只需要在插入数据的时候指定路由的 key 即可。

PUT route_test/_doc/c?routing=key1&refresh
{
  "data": "C"
}

我们指定了路由,路由的值是一个字符串 "key1"。通过查看 shard 信息,能看出这条数据路由到了0号shard。也就是说用"key1"做路由时,文档会写入到0号shard。

查询

GET route_test/_doc/b?routing=key1

很多时候自定义路由是为了减少查询时扫描 shard 的个数,从而提高查询效率。 默认查询接口会搜索所有的 shard,但也可以指定routing字段,这样就只会查询 routing 计算出来的 shard,提高查询速度

副本是主分片的复制品,它于主分片的数据完全一致,能够在主分片故障时迅速恢复数据。 所以,主分片与副本分片永远不会在同一个节点上。默认情况,ElasticSearch 为每个索引都设置了一个副本,这就意味着集群中应该至少有两个节点,如果集群中只要一个节点,副本分片就永远不会被创建,这时,ElasticSearch 集群健康状态是黄色。 通过 number_of_replicas 参数设置副本数。

客户端 API 概览

ElasticSearch 最基本的访问方式还是通过 REST 接口,以 HTTP 协议的形式操作文档数据。 除此之外,ElasticSearch 还内置了一种称为 Painless 的脚本语言。

简单的说,POST 为新增资源,PUT 为新增或修改资源,GET 是查询资源,DELETE 是删除资源,HEAD 是做存在性查询。

POST 和 PUT 的区别 一般来说,PUT 请求是幂等,即对同一资源多次请求的结果是一样的。PUT 对同一个 URI 的多次请求,只有第一次是新增操作,剩下都是更新操作。

多索引 REST 接口的 URI 一般对应一个资源,但是 ELasticSearch 在操作索引时可以在 URI 中指定多个。 比如

DELETE test1, test2, test3
DELETE test*
GET _all/_search
GET */_search

通用参数

?pretty=true
?format=true
?human=true

Painless 则是 ElasticSearch 支持的最好的脚本语言,Painless 以 Java 为基础,最终被编译为字节码并运行在 JVM 上。

由于 REST 接口基于 HTTP 协议而与客户端语言无关,所以使用 Java 调用 ElasticSearch 完全可以使用 Apache HttpClient 这样的框架实现。 另外,ElasticSearch 官方提供的 Java 客户端 API 底层也是基于 HttpClient 框架的,在此基础上做了封装并提供了两种解决方法。

低级别 REST 客户端

开放出来的接口还是 REST 调用形式,但使用的客户端,请求,响应等对象已经被封装为 ElasticSearch 相关的类型

RestClient lowClient = RestClient.builder(new HttpHost("localhost", 9200, "http")).build();
Request request = new Request("GET", "/test");
Response response = lowClient.performRequest(request);
String body = EntityUtils.toString(response.getEntity());
lowClient.close();

建立在低级别 REST 客户端基础之上,它将几乎所有 ElasticSearch 访问接口都封装为对象和方法,在调用过程中,没有明显的 REST 语法形式了

RestHighLevelClient highClient = new RestHighLevelClient(RestClient.builder(new HttpHost("localhost", 9200, "http")));
GetIndexResponse response = highClient.indices().get(new GetIndexRequest().indices("test"), RequestOptions.DEFAULT);
System.out.println(response.toString());
highClient.close();

第3章 ElasticSearch 索引与文档

索引别名与配置

在 ElasticSearch 中,可以使用四种 HTTP 方法请求索引,即 PUT,GET,DELETE 和 HEAD。 任何一个索引都包括别名,映射和配置三个参数,他们可以在创建索引的时候,通过 aliases,mappings 和 setting 设置

与视图类似,索引别名一般都会与一些过滤条件相关联。比如

PUT students
{
  "aliases": {
    "girls": {
      "filter": {
        "term": {
            "gender": "F"
        } 
      }
      "routing": "F",
    },
    "boys": {
      "filter": {
        "term": {
            "gender": "M"
        } 
      }
      "routing": "M",
    },
  },
}

students 索引一共关联了 girls 和 boys 两个别名。在别名定义中,filter 定义过滤器,routing 定义路由规则。 在定义过滤条件时通常要指定路由规则。这样会将同一个别名的文档路由到相同的分片上,可以有效减少使用别名检索时分片操作但这也要求在存储文档时,必要要通过别名,否则在使用别名检索的时候可能会漏掉文档。

别名也可以与多个索引关联

在索引创建后,_alias_aliases 接口都可以用于添加或删除别名。 _alias 针对某一个具体的索引。

PUT /students/_alias/grade1
{
  "filter": {
    "term": {
        "grade": "1"
    } 
  }
}

_aliases 可以多多个索引做批量处理。

PUT _aliases
{
  "action": [
    {
      "add": {
        "index": "students",
        "alias": "grade1",
        "filter": {
          "term": {
              "grade": "1"
          } 
        }
      }
    }
  ]
}

_rollover 接口用于根据一系列条件,将别名指向一个新的索引,这些条件包括存续时间,文档数量和存储容量等。 这与日志文件使用的文件滚动类似,文件滚动是通过不断创建新文件并滚动旧文件来保证日志文件不会过于庞大,_rollover 接口则通过不断将别名指向新索引以保证索引容量不会太大

别名的滚动条件,通过 conditions 参数设置,包括 max_age, max_docs,max_size

创建一个索引 logs-1 并分配别名 logs

PUT /logs-1
{
  "aliases":{
    "logs":{}
  }
}

调用 logs 别名的 _rollover 接口设置别名滚动条件。

POST /logs/_rollover
{
  conditions: {
    "max_age": "14d",
    "max_docs": 1000,
    "max_size": "4gb"
  }
}

当超过这些条件的时候,会产生新的索引 log-000002,新索引命名规则会在原索引名字数字的基础加1,并且数字的长度保存在6位。所以,使用 _rollover 接口,要求索引名称必须是数字结尾。 这样,logs-1的别名已经被清空,而log-000002的别名中则添加了 logs 别名。 由于 _rollover 接口在滚动新索引时,会将别名与原索引的关联取消,所以通过别名再想查找已经编入索引的文档就不可以了

为了保证原文档可以检索,可以设置 is_write_index 参数来设置哪一个是写索引

PUT /logs-1
{
  "aliases":{
    "logs":{
      "is_wirte_index": true
    }
  }
}

这样,索引滚动的时候,将不会取消别名与原索引之间的关系产生新索引的时候,会将原索引的 is_wirte_index 设置为false,新索引设置为 true

PUT test1
{
  "settings": {
        "number_of_shards": 3,
        "number_of_replicas": 2
    }
}

索引可以被关闭,关闭后的索引除了维护自身的数据信息以外,基本上不会再占用集群资源,通过也不能再被用户读写,索引关闭后可以再次打开,所以通过关闭索引可以实现索引存档的目的

POST /test/_close
POST /test/_open

索引静态配置 索引静态配置主要有索引的主分片,压缩解码,路由等相关信息,他们只能在创建索引时设置,一旦索引创建完成就不能再修改静态配置,比如,number_of_shards 参数。

索引动态配置 以 GET 请求 _settings接口可以获取索引配置信息,以 PUT 请求访问则可以修改配置。

PUT /test1/_settings
{
  "number_of_replicas": 2
}

动态映射与索引模板

索引并非一定要创建才能存储文档,可以在不创建索引的情况下直接向索引中添加文档。 ElasticSearch 的动态机制会根据文档内容,并依据索引模板自动创建一个与文档相匹配的索引。

即使在索引创建后,ElasticSearch 依然支持向索引中增加新字段,这种动态增加字段的特性可以通过 dynamic 参数来修改。 dynamic 可以是 true,false,和strict。默认值是 true。 设置为 strict 的时候,新添加字段会报出异常。 当设置为 false,在添加新文档时出现的新字段依然会保存到文档中,只是这个字段的定义并不会被添加到索引的映射的字段定义中。设置为 true,新添加字段才会按一定的数据类型映射规则,将它们添加到索引映射的定义中。

索引可使用预定义的模板进行创建,这个模板称作Index templates。 模板设置包括 settings 和 mappings,通过模式匹配的方式使得多个索引重用一个模板

curl -XPUT localhost:9200/_template/template_1 -d '
{
    "template" : "te*",
    "order" : 0,
    "settings" : {
        "number_of_shards" : 1
    },
    "mappings" : {
        "type1" : {
            "_source" : {"enabled" : false }
        }
    }
}

上述定义的模板 template_1 将对用 te 开头的新索引都是有效。

删除模板

curl -XDELETE localhost:9200/_template/template_1

查看定义的模板

curl -XGET localhost:9200/_template/template_1

当存在多个索引模板时并且某个索引两者都匹配时,settings 和 mpapings 将合成一个配置应用在这个索引上。 合并的顺序可由索引模板的order属性来控制。(order为1的配置将覆盖order为0的配置)。

索引的映射关系在索引创建后,可以通过 _mapping 接口查看或修改。

/索引/_mapping/field/字段名

比如

GET /students/_mapping
GET /students/_mapping/field/gender

容量控制和缓存文档

索引容量由分片数量和分片容量决定,分片数量可以通过 _split 接口扩容,可以通过 _shrink 接口锁容。 需要注意的是,这种扩容,锁容的方式,依然是将原索引,扩容锁容到新索引上,并不是在原索引上做扩展

使用 _split 接口时,分片总量都是成倍增加而不能逐个增加。 使用 _split 接口成功分裂分片后,原索引并不会被自动删除。通过原索引和新索引都可以查看到相同的文档数据。

索引在扩容的期间,必须设置为只读

PUT /my_source_index/_settings
{
    "block.write": true // 只读
}

扩容,文档和 setting 都会复制,可以重新设置新索引的 settings 和 alias。 当然,也可以在地址中添加 copy_settings=false 来禁止从原索引复制配置。

POST my_source_index/_split/my_target_index
{
  "settings": {
    "index.number_of_shards": 4 
    "index.blocks.write": false 
  },
  "aliases": {
    "my_search_indices": {}
  }
}

_split 接口相反,_shrink 接口用于缩减索引分片。 _shrink 接口缩小索引分片数量也要求,原始分片数量是缩小后的整倍数。也要求在锁容期间必须是只读。

ElasticSearch 提供的 _reindex 接口支持将文档从一个索引重新索引到另一个索引中。 但是性能消耗比较大,所以尽量不要使用这种方法。

POST _reindex
{
    "source": {
        "index": "users"
    },
    "dest": {
        "index": "users_copy"
    }
}

需要注意的是,在重新索引时,不会将原索引的配置信息复制到新索引中。另外,使用 _reindex 接口必须将索引的 _source 字段开启。

在实际应用中,_reindex 接口并不是应用于扩容和锁容,而是主要应用于索引数据的合并。 所以在 _reindex 接口还提供了一些借口,比如添加 term 查询过滤文档,还可以设置只添加不存在的文档。

为了提升数据检索时的性能,ElasticSearch 为索引提供了三种缓存。 第一种称为节点查询缓存,负责存储节点查询结果。 节点查询缓存是节点级别的,一个节点只有一个缓存,同一节点上的分片共享同一缓存。默认节点查询缓存是开启的(index.queries.cache.enabled),默认使用节点内存 10% 作为缓存容量上限(index.queries.cache_size)。

第二种称为分片请求缓存,负责存储分片接受到的查询结果。一般只会缓存聚集查询的相关结果分片请求缓存使用的键是作为查询条件的JSON字符串,所以如果查询条件 JSON 完全相同,文档的查询几乎可以达到实时。

最后一个缓存就是 text 类型在开启 fielddata 机制后使用的缓存。

使用缓存,使用检索性能得到提升,但有两个主要问题:1. 如果保持缓存和实际数据一致。2. 当缓存容量超出时如何清理缓存。

数据一致性的问题,ElasticSearch 通过让缓存与索引刷新频率保持一致实现的。索引默认会以每秒一次的频率将文档编入索引,ElasticSearch 会在索引更新同时让缓存失效,来保证一致性。

缓存容量问题,通过 LRU 方式,将最近最少使用的缓存条目清除。同时,ElasticSearch 还提供了一个 _cache 接口来主动清理缓存。

_refresh 接口

_refresh 接口用于主动刷新一个或多个索引,将已经添加的文档编入索引以使它们在检索时可见。 使用 _all 可以刷新所以的索引。以下的调用都可以。

GET employee/_refresh
POST _refresh
GET _all/_refresh
POST employee,students/_refresh

_cache 接口

_cache 接口用于主动清理缓存,在 _cache 后需要附加关键字 clear。

POST /employee/_cache/clear

最好在,主动刷新索引后 _refresh,再主动清理缓存 _clear

_stat 接口用于查看索引上不同操作的统计数据。 _shard_stores 接口用于查询索引分片存储情况。 _segments 接口用于查看底层 Lucene 的分段情况。

操作文档

尽管映射类型在 ElasticSearch 版本7中已经废止,但是在操作文档的时候仍需要指明映射类型。 在版本7中映射类型只能是 _doc。所以可以不再把它当成映射类型。

值得注意的是,使用 PUT 更新文档的时候,不能只更新某一个字段,要更新就必须更新整个文档。 如果更新文档只发送变更的字段,那么整个文档,将只保留变更字段的数据,其他字段的数据将被删除。

文档版本实际上提供了一种控制并发更新的锁机制。这就是常说的乐观锁。 如果要使用这种机制,需要在请求中添加 version 参数,例如 PUT test/_doc/1?version=1只有文档版本是1时才会更新成功,否则将报版本冲突异常

如果不想让 PUT 请求在文档存在时更新文档,可以通过设定操作类型来禁止。这样在文档存在时也会报版本冲突异常。

PUT /test/_doc/1?op_type=create

从 ElasticSearch 实现机制上来说,旧版本的文档只会标识为已删除而不会立即做物理上的删除。 被标识为已删除的文档,用户就不能再访问了,这些被标识的旧版本文档将在后台统一删除。

GET /test/_doc/1
HEAD /test/_doc/1

HEAD 方法用于存在性检查。 GET 会返回元字段和源文档。元字段包括,_index, _id, _type, _version, found(true|false 代表 ID 标识的文档是否存在)。 如果只想返回源数据,可以在路径后添加 _source 参数。

GET /test/_doc/1/_source
GET /test/_doc/1?_source=name,age
GET /test/_doc/1?_source_includes=age&_source_excludes=name

_mget 接口根据索引名和文档 _id 获取多个文档。

GET /_mget
{
  "docs": [
    {
      "_index": "my-index-000001",
      "_id": "1",
      "_source": {
        "include": ["name"],
        "exclude": ["age"]
      }
    },
    {
      "_index": "my-index-000001",
      "_id": "2"
    }
  ]
}

可以根据文档 _id 从索引中删除文档,还可以根据查询条件找到满足条件的文档并删除。

根据文档 _id 从索引中删除文档

DELETE /users/_doc/1

根据查询条件找到满足条件的文档并删除

POST /users/_detele_by_query
{
  "query": {
    "match": {
      "name": "tom"
    }
  }
}

虽然 PUT 方法请求文档可以更新文档,但是不能只更新文档中的某一个字段值,而且必须知道文档的 _id 值。 如果只更新部分字段,使用 _update 接口。注意,POST /students/_doc/1/_update这种形式是版本7之前的方式,已经废止。

POST /students/_update/1
{
  "doc": {
    "gender": "M"
  }
}

如果希望文档不存在的时候创建文档。

POST /students/_update/1
{
  "doc": {
    "gender": "M"
  },
  "doc_as_upsert": true
}

或者

POST /students/_update/1
{
  "doc": {
    "gender": "M"
  },
  "upsert": {
    "name": "josh",
    "gender": "M"
  }
}

根据 _id 条件以外的条件更新文档使用 _update_by_query 接口。

POST /students/_update_by_query
{
  "script": {
    "source": "ctx._source.age++"
  }
  "query": {
    "exists": {
      "field": "age"
    }
  }
}

使用 _bulk 接口,_bulk 接口是一组请求体,请求体一般每两个一组,第一个请求体代表操作文档的类型,第二个请求体是参数。有时候不需要参数,则可能只有一个请求体。

POST _bulk
{ "index" : { "_index" : "test", "_id" : "1" } }
{ "field1" : "value1" }
{ "delete" : { "_index" : "test", "_id" : "2" } }
{ "create" : { "_index" : "test", "_id" : "3" } }
{ "field1" : "value3" }
{ "update" : {"_id" : "1", "_index" : "test"} }
{ "doc" : {"field2" : "value2"} }

第4章 ElasticSearch 分析与检索

虽然通过文档 _id 可以获取到文档,但是 _id 一般都是一个无意义的值,在应用中更多的使用文档其他有意义的字段做检索。 ElasticSearch 提供了一个专门用于检索的 _search 接口,这个接口可以根据查询条件做检索

_search 接口

ElasticSearch 为了使用这个接口定义了一种查询语言 DSL。 DSL 是一套基于 JSON 的查询语言。

_search 接口有两种请求方式,一种是基于 URI 的请求方式,另一种则是基于请求体的请求方式。

DSL 查询条件以请求参数 q 传递给接口。

GET test/_search?q=message:chrome firefox

参数 q 定义的内容叫查询字符串,它的含义是检索message字段值中包含chrome和firefox的文档。 查询字符串属于全文检索,这意味着查询字符串在检索前会被解析为一系列的词项,上面的就会被解析成 chrome 和 firefox。 在检索时,只要字段中包含任意一个词项就视为满足条件

最好使用 POST 方法请求基于请求体的 _search 接口。

POST test/_search
{
  "query": {
    "term": {
      "DestCountry": "CN"
    }
  }
}

上面采用了 DSL 基于词项 Term 的查询。DSL 最简单的查询关键字是 match_all 和 match_none。

POST test/_search
{
  "query": {
    "match_all": {}
  }
}

DSL 更多语法将在第5章介绍。

分页与排序

_search 接口提供了 from 和 size 两个参数可以实现分页

POST test/_search
{
  "from": 100,
  "size": 20,
  "query": {
    "term": {
      "DestCountry": "CN"
    }
  }
}

它提供了一种类似于数据库游标的文档遍历的机制,一般用于非实时性的海量文档处理需求。例如将一个索引中的文档导入另一个文档中,或者将索引中的文档导入到 MySQL 中。

使用 scroll 机制有两个步骤,第一步是创建游标,第二步则是对游标遍历

POST test/_search?scroll=2m&size=1000
{

  "query": {
    "term": {
      "message": "chrome"
    }
  }
}

scroll 参数只能在 URI 中使用,不能出现在请求体。它定义了检索生成的游标需要保留多长时间。2m 代表 2分钟。 scroll保留时长不是处理完所有数据所需要的时长,而是处理单次遍历所需要的时长

返回结果将包含一个名为 _scroll_id 的字段,它代表了一个 scroll 查询的结果。 接下来,根据这个 _scroll_id 就可以对结果进行遍历了

之后反复调用 _seach/scroll 接口就可以实现对结果的遍历了。参数 scroll_id 就是第一查询得到的 _scroll_id

POST _seach/scroll
{
    "scroll" : "2m", 
    "scroll_id" : "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAD4WYm9laVYtZndUQlNsdDcwakFMNjU1QQ==" 
}

定义检索应该在文档某些字段的值之后查询其他的文档。

sort 字段可以使用文档中的字段以外,还可以使用两个虚拟字段,_score 和 _doc 。 _score 就是按照文档相似度得分排序,_doc 则是按照索引次序排序。 默认情况下,_score 是按照降序,其他字段是升序

字段投影

投影的概念源于关系型数据库,简单的说,就是在查询表中不将所有的字段返回,只返回其中的部分字段。 ElasticSearch 主要使用 _source 字段和 fields 字段来实现。

"_source" : ["name", "age"]

还可以使用前面介绍过的 include ,exclude。

还可以使用 stored_fields 参数指定哪些被存储的字段会出现在结果中。 前提是,这些字段的 store 参数设置为 true。

"stored_fields" : ["name", "age"]

在返回结果中增加一个 fields 字段,包含了 stored_fields 配置的字段值,另外,使用 stored_fields 之后,将不会返回 _source 字段

分析器与规整器

在 ElasticSearch 中,文档编入索引时会从全文数据中提取词项,这个过程被称为文档分析文档分析不仅存在于文档索引时,也存在于文档检索时文档分析会从查询条件的全文数据中提取词项,然后再根据这些词项检索文档。

文档分析器是 ElasticSearch 中用于文档分析的组件,通常由 字符过滤器分词器分词过滤器 组成。

字符过滤器 读入最原始的全文数据,对全文数据中字符做预处理,比如从 HTML 文档中将类似 <b> 这样的标签删除。 字符过滤器可以没有也可以有多个。

分词器 接受字符过滤器处理完的全文数据,将他们根据一定的规则拆分成词项。英文规则比较简单,中文规则比较复杂,需要根据词意做分词并且需要字典支持。对于一个分析器,分词器是必不可少的而且只能有一个。 在使用分析器的时候,可以根据文档内容更换分词器

分词过滤器 接受分词器提取出来的所有词项,然后对这些分词做规范化处理,比如转化成小写,去除 "的,地" 等词。 分词过滤器可以没有也可以有多个。

除了分析器,ElasticSearch 还提供了一个规整器规整器没有分词器,只有字符过滤器和分词过滤器,只能应用于 keyword 的字段

由于文档分析通过分析器完成词项提取,所以想要影响文档索引和文档检索时的词项提取,就要修改它们使用的分析器。 对于所有的 text 类型的字段,都可以在创建索引的时候为它们指定分析器

PUT articles
{
  "mappings": {
    "properties": {
      "title": {
        "type": "text",
        "analyzer": "standard",
        "search_analyzer": "simple"
      }
    }
  }
}

在创建索引时如果没有指定分析器,ELasticSearch 会查找名为 default 的分析器,如果没有则使用 standard 的分析器。 查询的时候,请求参数也可以指定分析器,会覆盖创建时指定的分析器。比如,下面的请求,虽然 title 指定了 search_analyzer 是 simple,但是请求参数中指定了 english,就会使用 english。

POST /articles/_search
{
  "query": {
    "match": {
      "title": {
        "query": "elastic search analyzer",
        "analyzer": "english"
      }
    }
  }
}

ElasticSearch 提供了一个 _analyze 接口,可以用于查看分析器处理结果。

POST _analyze
{
  "analyzer": "standard",
  "text": "elsticsearch logstash kibana beats"
}

上面的请求会返回分析的结果。比如,提取了哪些词项,词项在文中的起始位和终止位置等。

内置分析器和中文分析器

ElasticSearch 内置了很多分析器,比如 standard,simple,english 等都是内置分析器。 这些分析器都可以直接使用,或者通过配置生成自定义的分析器再使用

PUT /analyzer_test
{
  "setting": {
    "analysis": {
      "analyzer": {
        "my_analyzer": {
          "type": "standard",
          "max_token_length": 5,      // 词项的最大长度
          "stopwords": ["the", "a", "an", "this"]      // 停止词数组
        }
      }
    }
  }
}

然后就可以使用自定义的分析器 my_analyzer 了。

默认的分析器,standard 分析器没有字符过滤器,包含了三个词项过滤器,分别是,标准词项过滤器(只是占位,没做任何实际处理),小写字母词项过滤器(转换成小写),停止词过滤器(将停止词删除)。 standard 分析器的实际分词效果是使用 Unicode 文本分割规范提取词项并全部转换为小写

分词规则是使用所有非字母分割单词,在词项结果中去除所有的英文停止词,并且会将提取的词项转换成小写

使用 Java 正则表达式匹配文本以提取词项。

另外,上面的分析器都是可以配置的分析器,还有几个不可配置的分析器,simple,whitespace 和 keyword

中文分析器比较有名的就是 IK。包括 ik_smartik_max_word 两种。 两者的区别在于它们提取词项的粒度上。前者提取粒度最粗,后者最细。 比如 "中文分析器" 使用 ik_smart 会被分解成 "中文" 和 "分析器" 。使用 ik_max_word 会被分解成 "中文" "分析器" "分析" "器"。

ElasticSearch 默认不支持 IK,需要安装到 ElasticSearch。

其他检索接口

前面主要都是围绕这 _search 接口,ElasticSearch 还提供了许多与文档检索相关的接口。

查看文档总数的 _count 接口。可以使用 GET 和 POST 发送请求。

GET _count
// 增加 DSL 查询条件
POST /test/_count
{
  "query": {
    "match": {
      "message": "chrome"
    }
  }
}

类似于 _bulk 接口,可以在一次接口中执行多次查询。请求体每两行为一组,视为一个查询

POST /test/_msearch
{
  {}
  {"query": {"match_all" : {}}}
  {"index": "test"}
  {"query": {"match_all" : {}}}
}

辅助接口

_validate 接口用于在不执行查询的情况下,评估一个查询是否合法可执行_validate 接口执行或,会在返回结果中包含一个 valid 字段,true 代表查询合法可执行。

_explain 接口用于给单个文档的查询相似度评分做解释,使用 _explain 接口必须要指定索引和文档_id。 返回结果包含 matched 和 explanation 字段,matched 代表 DSL 查询条件是否匹配当前文档,explanation 字段包含相似度评分及评分计算依据。

查看某一个字段支持的功能,主要包括是否可检索,是否可聚集等

_search_shards 接口返回查询基于哪些节点,索引和分片执行。这些信息有助于分析查询时出现的各种问题。

第5章 叶子查询与模糊查询

叶子查询是指指定的字段中匹配查询条件。 叶子查询大致包括 基于词项的查询基于全文的查询 两个类。

基于词项的查询

基于词项的查询属于叶子查询,所以这种查询语句一般只能针对于一个字段设置条件。 基于词项的查询会精确匹配查询条件不会对查询条件做分词,规范化等预处理

由于 text 类型会做分词处理,所以不能直接匹配字段的内容。比如 text 字段值是 "tom smith" ,会被分成 tom 和 smith 两个词,如果使用 "tom smith" 做词项查询,无法搜索到这个字段。而且注意,分析器会词项过滤器,用大写字母也搜索不到。

因此,基于词项的查询一般不对 text 类型字段做检索,而是用于类似数值,日期,枚举类型等结构化数据的精确查询

这三个查询都是对单个字段做词项

term 查询

相当于 SQL 语句 where 条件的等号。

POST /test/_search
{
  "query": {
    "term": {
      "OriginCountry": "CN"
    }
  }
}

terms 查询

相当于 SQL 中的 in。只要字段满足这些词项的一个就认为是满足条件。

POST /test/_search
{
  "query": {
    "terms": {
      "OriginCountry": ["CN","JA"]
    }
  }
}

ElasticSearch 在 terms 还支持跨索引查询,类似于关系型数据库的一对多后者多对多的关系。

POST /articles/_search
{
  "query": {
    "_id": {
      "index": "users",
      "id": 1,
      "path": "articles" 
    }
  }
}

先会到 users 索引中查询 id 是 1的用户,然后查询这个用户的 articles 字段的值和 articles 索引库的 _id 做对比,这样将用户 1 的所有文章都取出来了。

terms_set 查询

跟 terms 查询类似,不同的是,被匹配的字段类型是数组。

POST /kibana_sample_data_logs/_search
{
  "query": {
    "terms_set": {
      "tags": {
        "terms": ["success","info"],
        "miniumn_should_match_script": {
          "source": "2"
        }
      }
    }
  }
}

匹配一个字段是否在指定的范围里,一般应用于数值,日期类型的字段。

POST /kibana_sample_data_logs/_search
{
  "query": {
    "range": {
      "FlightDelayMin": {
        "gte": 100,
        "lte": 200
      }
    }
  }
}

用于检索指定字段值不为空的文档。exists 查询需要使用 field 字段设置需要检查非空的字段名称,只能设置一个字段,不能设置多个字段。 POST /kibana_sample_data_logs/_search

{
  "query": {
    "exists": {
      "field": "DestCountry"
    }
  }
}

使用模式匹配

前面都是精确匹配,ElasticSearch 也支持使用通配符,正则表达式对词项进行模糊匹配。

POST /kibana_sample_data_logs/_search
{
  "query": {
    "prefix": {
      "message": "mo"
    }
  }
}

wildcard 查询允许在字段查询条件中使用通配符 *?* 代表 0 个或者多个字符,? 则代表单个字符。

POST /kibana_sample_data_logs/_search
{
  "query": {
    "wildcard": {
      "message": "f*f?x"
    }
  }
}

可以使用正则表达式,正则表达式的语法和 lucene 使用的正则表达式一致,但是 Java 的正则里的 \w , \d 之类的并不支持。

POST /kibana_sample_data_logs/_search
{
  "query": {
    "regexp": {
      "message": "f.*f.x"
    }
  }
}

允许根据一组 ID 查询多个文档

POST /kibana_sample_data_logs/_search
{
  "query": {
    "ids": {
      "value": ["Z7ysdnsiyBSsaf"]
    }
  }
}

请求路径中也可以不指定索引名称,由于 _id 仅在索引内唯一,所以有可能通过 _id 检索到所有文档。

停止词和 Common 查询

有些词项出现的频率很高,比如 the, of, to 等,中文的,虽然,的,得 等字。多数跟文档表达的意思关联性不大,只有将它们剔除才更接近期望的结果。

处理这个问题,最简单的方法就是文档编入索引的时候将它们剔除。 这类出现在文档中,但是不会编入索引的词项就是停止词停止词一般在定制分析器时预定义好,文档在编入索引时,分析器就会将这些停止词剔除。

common 查询将词项分为重要词项非重要词项。 重要词项是出现频率低的词项,所以也称为低频词项。 非重要词项是出现频率高的词项,也称为高频词项。 这里说的频率不是指词项在单个文档字段中出现的次数,而是指在某个字段中出现了该词项的文档数量

在 common 查询中使用 cutoff_frequency 设置词项频率,可以设置为一个决定数量,代表出现了词项的文档个数。也可以设置百分比,代表出现词项的文档数占总文档数量的百分比。

基于全文的查询

基于 全文的查询 与 基于词项 的查询最显著的区别是 前者会对查询条件做分析。 使用的分析器,可以在索引创建的时候,通过 analyzer 参数,search_analyzer 参数设置,也可以在检索的时候,通过 _search 接口的 analyzer 参数动态修改。

词项匹配

match 查询和 multi_match 查询都是用查询条件中提取出来的词项与字段做匹配,前者只对一个字段做匹配,后者可以针对多个字段查询

match 查询接受文本,数值和日期类型,在检索时将查询条件做分词处理,在以提出的词项与字段做匹配。 如果取出来的词项为多个,默认只要一个匹配成功即认为满足查询条件

POST /kibana_sample_data_logs/_search
{
    "query": {
        "match": {
            "message": "firefox chrome"
        }
    }
}

词项匹配的运算逻辑和匹配的个数通过 operatorminmum_should_match 两个参数来改变。

POST /kibana_sample_data_logs/_search
{
    "query": {
        "match": {
            "message": {
                "query": "firefox chrome",
                "operator": "and",
                "minmum_should_match": 2
            }
        }
    }
}

与 match 查询类似,但可以实现对多个字段同时匹配

POST /kibana_sample_data_logs/_search
{
    "query": {
        "multi_query": {
            "query": "AT",
            "fields":["DestCountry", "OriginCountry"]
        }
    }
}

短语匹配

短语匹配并不是用整个查询条件与字段做匹配。 短语匹配跟普通的 match 查询并没有本质的区别,它在执行检索前也会分析并提取查询条件中的词项。只是在检索过程中,match 查询只要包含一个就视为满足条件,短语匹配不仅要求全部词项都要包含,还要保证他们在原始文档出现的次序和查询条件中的次序一致。 ElasticSearch 提供了两种基于全文的短语查询,match_phrasematch_phrase_prefix 查询。

会将查询条件按顺序分词,然后再查看它们在字段中的位置之差,只有差值都为 1 才满足条件。 也就是说,这些词项要在字段中依次出现,并且是紧挨的

POST /kibana_sample_data_logs/_search
{
    "query": {
        "match_phrase": {
            "message": "firefox 6.0a1"
        }
    }
}

可以指定 slop 用于控制词项之间的位置差。默认是1。 短语匹配,需要文档编入索引的时候,将字段中的位置也编入索引,默认情况下,只有 text 类型会自动将词项位置编入索引。

查询字符串

查询字符串是具有一定逻辑含义的字符串。它不会直接使用分析器提取词项,而是先通过某种类型的解析器解析逻辑操作符和更小的字符串。 比如,查询字符串 "(firefox 6.0a1) OR (chrome 11.0.696)" 中,OR 是逻辑操作符,先解析为 "(firefox 6.0a1)" 和 "(chrome 11.0.696)" 两部分,然后这两部分在使用字段分析器提取词项。注意,操作符 OR 和 AND 必须大写。

POST /kibana_sample_data_logs/_search
{
    "query": {
        "query_string": {
            "default_field": "message",
            "query": "(firefox 6.0a1) OR (chrome 11.0.696)"
        }
    }
}
POST /kibana_sample_data_logs/_search
{
    "query": {
        "query_string": {
            "default_field": "message",
            "query": "(firefox 6.0a1) | (chrome 11.0.696)"
        }
    }
}

间隔查询是 ElasticSearch 7 才引入的查询方法,类似于短语查询,但是更加强大。

模糊查询

在 ElasticSearch 基于全文的查询中,除了短语相关的查询外,其余查询都包含一个名为 fuzziness 的参数用于支持模糊查询。 ElasticSearch 支持的模糊查询,比数据库的模糊查询强大的多,它可以根据一个拼写错误的词项匹配正确的结果。 比如根据 firefix 匹配 firefox。

根据 Levenshterin 算法,在文档字段中匹配不超过编辑距离的词项。

POST /kibana_sample_data_logs/_search
{
    "query": {
        "fuzzy": {
            "message": {
                "value": "firefix",
                "fuzziness": 1 // 编辑距离
            }
        }
    }
}

firefix 到 firefox 的编辑距离是1,可以查找到,但是firefit 和 firefox 的编辑距离是2,就查找不到了。 模糊查询的开销比精确查询大的多。

纠错与提示

纠错是在用户提交了错误的词项时给出正确的词项的提示。 提示则是在用户输入关键字给出的智能提示或内容自动补全。

ElasticSearch 同时支持纠错和提示功能。这两个功能从实现的角度来说并没有本质的区别,他们都是由一种呗称为提示器或建议器的特殊检索实现的

因为输入的同时给出提示词,所以这种功能要求速度必须要快。

在使用上,提示器是通过检索接口 _search 的一个参数设置的。

POST /kibana_sample_data_logs/_search?filter_path=suggest
{
  "suggest": {
    "my-suggest": {
      "text": "firefit chrom",
      "term": {
        "field": "message"
      }
    }
  }
}

返回结果中,会包含一个 suggest 字段,列举了依照 term 提示器找到的提示词项。

ElasticSearch 一共提供了三种提示器,本质上都是基于编辑距离算法的

term 提示器会将需要提示的文本拆分成词项,然后对每一个词项做单独的提示。

phrase 提示器会使用整个文本做提示。

POST /kibana_sample_data_logs/_search?filter_path=suggest
{
  "suggest": {
    "my-suggest": {
      "text": "firefit chrom",
      "phrase": {
        "field": "message",
        "highlight": {
          "pre_tag": "<em>",
          "post-tag": "</em>"
        }
      }
    }
  }
}

上面还使用 highlight 参数定义了高亮,所以提示词在返回结果中,都会用 em 标签标识为高亮。

一般用于输入提示和自动补全。首先要求提示词产生的字段为 completion 类型。这种类型会在内存中创建特殊的数据结构以满足快速生成提示词的要求。

PUT articles
{
  "mappings": {
    "properties" : {
        "suggestions" : {  
            "type" : "completion"  # 定义该字段是自动补全的字段
        },
        "title" : {
            "type": "keyword"
        },
        "content" : {
            "type": "text"
        }
    }
  }
}

向 completion 类型的字段添加内容时可以使用两个参数,input 保存实际的提示词,而 weight 参数则设置了这些提示词的权重,权重越高它在返回的提示词中越靠前。

POST articles/_doc
{
  "title" : "book",
  "content": "this is a good book",
  "suggestions": {
    [{"input": "elasticsearch", "weight": 30},
    {"input": "elastic stack", "weight": 10}]
  }
}

注意,completion 类型的字段保存时不会分析词项

POST /articles/_search
{
  "_source": "suggest",
  "suggest": {
    "articles-suggestion": {
      "prefix": "ela",
      "completion": {
        "field": "suggestions"
      }
    }
  }
}

总结一下,term 和 phrase 提示器主要用于纠错,term 提示器用于对单个单词的纠错,phrase 则针对短语做纠错。 completion 提示器是专门用于输入提示和自动补全的提示器

第6章 相关性评分与组合查询

全文检索与数据库查询的一个显著区别,就是它并不一定会根据查询条件做完全精确的匹配。 全文检索会根据查询条件给文档的相关性打分并排序,将那些与查询条件相关性高的文档排在最前面在 ElasticSearch 返回的每一个结果中,都会包含一个 _score 字段。这个字段的值就是当前文档匹配检索请求的相关性评分。

相关度权重

在一些情况下,需要将某些字段的相关度权重提升,以增加这些字段对检索结果相关度评分的影响。 比如,同时对 title 和 content 内容做检索,title 字段在相关度评分中的权重应该比 content 字段高一些。 一般,相关度权重提升都是在多个查询条件下设置的。可以在创建索引字段的时候,就设置这个字段的权重,但是这样不够灵活,更好的方式是检索时提升查询条件的相关度权重通过 boost 参数改变权重

GET /forum/article/_search 
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "title": "blog"
          }
        }
      ],
      "should": [
        {
          "match": {
            "title": {
              "query": "elasticsearch"
            }
          }
        },
        {
          "match": {
            "title": {
              "query": "spark",
              "boost": 5
            }
          }
        }
      ]
    }
  }
}

通过 indices_boost 参数可以调整多索引查询条件时,每个索引的权重。

组合查询与相关度组合

组合查询可以将通过某种逻辑将叶子查询组合起来,实现对多个字段与多个查询条件的任意组合。

和 SQL 语句查询数据时不同,SQL的话,如果不满足 where 子句的查询条件,这条记录不会作为结果返回。 但 ElasticSearch 的 bool 组合查询不同,在它的子句中,一些子句的确会决定文档是否作为结果返回,而另一些子句则不决定文档是否可以作为结果,但会影响到结果的相关度

bool 组合查询可用的布尔类型子句包括 must , filter , should 和 must_not 四种。

  1. must: 查询结果中必须要包含的内容,影响相关度
  2. filter: 查询结果中必须要包含的内容,不会影响相关度
  3. should: 查询结果中非必须要包含的内容,包含了会提高分数,影响相关度
  4. must_not: 查询结果中不能包含的内容,不会影响相关度

其中,filter 和 must_not 单纯只是过滤文档,对文档相关度没有任何影响。也就是说,对查询结果排序没有作用

should 有一些复杂,should 子句与 must 子句和 must_not 子句同时出现的话,should 子句不会过滤结果。也就是说,即使 should 子句不满足,结果也会返回。 如果只有 should 单独出现的话,should 子句至少要满足一条。

dis_max 查询也是一种组合查询,只是它在计算相关性时与 bool 查询不同。 将任何与任一查询匹配的文档作为结果返回,但只将最佳匹配的评分作为查询的评分结果返回 。 也就是,一个文档即使都满足多个查询条件,但是也不定会排在最前面。

constant_score 查询返回结果中文档的相关度为固定值,这个固定值由 boost 参数设置。默认为 1。

GET /_search
{
  "query": {
    "bool": {
      "should": [
        { "constant_score": {
          "query": { "match": { "description": "wifi" }}
        }},
        { "constant_score": {
          "query": { "match": { "description": "garden" }}
        }},
        { "constant_score": {
          "boost":  2
          "query": { "match": { "description": "pool" }}
        }}
      ]
    }
  }
}

上面这样满足第三个条件的,评分是2,其他的是1。

通过 positive 子句设置满足条件的文档,类似于 bool 查询的 must子句。 通过 negative 子句设置需要排除文档的条件,类似于 bool 查询的 must_not 子句。

但是,不会将满足 negative 子句的文档从返回结果中排序,只是会拉低他们的相关度分数。 参数 negative_boost 设置一个系数,满足 negative 条件时,相关度会乘以这个系数作为最终分数。

GET bank/_search
{
  "query": {
    "boosting": {
      "positive": {
        "term": { "state.keyword": { "value": "DC"} }
       },
      "negative": {
        "term": { "age": { "value": 23 } }
      },
      "negative_boost": 0.2
    }
  }
}

相关度组合

叶子查询中,多数只针对一个字段设置查询条件,所有没有相关度组合的问题。 但是叶子查询中有两个特例,query_stringmulti_search。这两个查询可以针对多个字段设置查询条件。 query_stringmulti_search 查询都具有一个 type 参数,用于指定针对多个字段检索时执行逻辑及相关度组合方法。 type 有5个可选值,即 best_fields,most_fields,cross_fields,phrase,phrase_prefix

类似于 dis_max 查询。best_fields 类型在查询时会转化为 dis_max 查询。

-phrase,phrase_prefix

执行逻辑上与 best_fields 相同,只是在转换为 dis_max 查询中,使用 phrase 和 phrase_prefix 而不是 match。

在计算相关度时会将所有的相关度累加起来,然后再除以相关度的个数以得到它们的平均值作为最终的相关度。

第7章 聚集查询

指标聚集

指标聚集是根据文档的某一字段做聚集运算,比如计算所有产品的销售总和,平均值等。

avg 聚集

POST /kibana_sample_data_flights/_search?filter_path=aggregations
{
  "aggs": {
    "delay_avg": {
      "avg": { "field" : "FlightDelayMin" }
    }
  }
}

请求参数 filter_path 将返回的结果的其他字段过滤掉,否则在查询结果则则包含了索引库的所有文档。

返回结果中,aggregations 是关键字,代表这是聚集查询的结果。delay_avg 是聚集查询定义的聚集名称。

{
    "aggregations": {
        "delay_avg": {
            "value": 47.1234
        }
    }
}

如果只想其中一部分文档参与运算,也可以使用 query 参数以 DSL 的形式定义查询条件。

POST /kibana_sample_data_flights/_search?filter_path=aggregations
{
  "query": {
    "match": {"DestCountry": "CN"}
  }
  "aggs": {
    "delay_avg": {
      "avg": { "field" : "FlightDelayMin" }
    }
  }
}

weighted_avg 聚集

可以给参与平均值运算的值一个权重,权重越高对最终的结果影响越大。

计数聚集

value_count 和 cardinality 聚集可以归入计数聚集中。前者用于统计从字段中取值的总数,而后者则用于统计不重复数值的总数

POST /kibana_sample_data_flights/_search?filter_path=aggregations
{
  "aggs": {
    "country_code": {
        "cardinality": {"field": "DestCountry"}
    },
    "total_country": {
        "value_count": {"field": "DestCountry"}
    },
  }
}

极值聚集

包括 max 聚集 和 min 聚集。

POST /kibana_sample_data_flights/_search?filter_path=aggregations
{
  "aggs": {
    "max_price": {
        "max": {"field": "AcgTicketPrice"}
    },
    "min_price": {
        "min": {"field": "AcgTicketPrice"}
    },
  }
}

统计聚集是一个多指标聚集,也就是返回结果中会包含多个值。

stats 聚集

返回结果包括字段的最小值,最大值,总和,平均值,数量

POST /kibana_sample_data_flights/_search?filter_path=aggregations
{
  "aggs": {
    "max_price": {
        "stats": {"field": "AcgTicketPrice"}
    },
  }
}

使用范围分桶

桶型聚集是 ElasticSearch 官方对这种聚集的叫法,它起的作用是根据条件对文档进行分组,可以将这里的桶理解成分组的容器。每个桶都都与一个分组标准相关联,满足这个分组标准的文档会落入桶中。

rang,date_range,ip_range 都用于根据字段的值范围内对文档分桶。 每个值范围通过 from 和 to 参数指定,范围包含 from 不包含 to 值

range 聚集

field 参数指定一个数值类型的字段。

POST /kibana_sample_data_flights/_search?filter_path=aggregations
{
    "aggs": {
        "price_ranges": {
            range: {
                "field": "AvgTicketPrice",
                "ranges":[
                    {"to": 300},
                    {"from": 300, "to": 600}
                ]
            }
        }
    }
}

date_range 聚集

针对日期类型。

ip_range 聚集

根据 IP 指定范围。

histogram,date_histogram,auto_date_histogram 跟数值范围很像,也是统计落在某一个范围的文档数量,但是这三类聚集统计的范围由固定的间隔定义,也就是范围的结束值和起始值的差值是固定的

POST /kibana_sample_data_flights/_search?filter_path=aggregations
{
    "aggs": {
        "price_histo": {
            "histogram": {
                "field": "AvgTicketPrice",
                "interval": 100,
                "offset": 50,
                "keyed": false,
                "order": {
                    "_count": "asc" // 排序字段,可选字段是_key和_count
                }
            }
        }
    }
}

使用词项分桶

使用字段值范围分桶主要针对结构化数据的,对于字符串类型的字段来说,使用值范围来分桶显然是不合适的。 字符串类型的分桶一般通过词项来实现。

根据文档字段中的词项做分桶,所有包含同一词项的文档将被归入同一桶中。 结果中包含词项及词频,默认情况下还会根据词频排序。 所以,terms聚集可以应用于热词展示

text 类型如果使用 terms 聚集需要打开 fielddata 机制。fielddata 机制消耗较大,所以 terms 聚集一般针对于 keyword 类型

POST /kibana_sample_data_flights/_search?filter_path=aggregations
{
    "aggs": {
        "country_terms": {
            "terms": {
                "field": "DestCountry",
                "size": 10
            }
        },
        "country_terms_count": {
            "cardinality": {
                "field": DestCountry
            }
        }
    }
}

上面定义了两个聚集,terms 聚集的 field 参数定义了提取词词项的字段是 DestCountry。 它的词项在返回结果中会按词频由高到低依次展示,词频会在返回结果的 doc_count 中展示。 size 表示只返回 10 个词项。这相当于把 DestCountry 字段的词频前 10 项检索出来

如果一个词项在某个文档子集中与在文档全集中占比发生了非常显著的变化,就说明这个词项在这个文档子集中是更重要的词项。 比如,在全部10000个文档中,有 ElasticSearch 的文档只有200篇,但是在标题含有NoSQL的200篇文档中,ElasticSearch 有180篇,这种占比就发生了显著的变化,说明,ElasticSearch 词项在这个子集中更重要。

significant_terms 聚集就是针对上述情况的一种聚集查询。它将文档和词项分为前景集和背景集前景集对应一个文档子集,背景集则对应文档全集。 significant_terms 聚集根据 query 指定前景集。

POST /kibana_sample_data_flights/_search?filter_path=aggregations
{
    "query": {
        "term": {
            "OriginCountry": {"value": "IE"}
        }
    }
    "aggs": {
        "dest": {
            "significant_terms": {
                "field": "DestCountry"
            }
        }
    }
}

query 使用 DSL 指定了前景集为出发国家为 IE 的航班。聚集查询中,使用 significant_terms 统计到达国家的前景集词频和背景集词频。 返回结果中,doc_count 是前景集文档数,bg_count 是背景集文档数。 关键字 GB,在前景集出现的文档数是12,占比 12/119,在背景集出现的文档数是449,占比 449/13059。发生了显著的变化,所以在这个前景词中 GB 可以被视为热词而排在第一位

{
    "aggregations": {
        "dest": {
            "doc_count": 119,
            "bg_count": 13059,
            "buckets": [
            {
                "key": "GB",
                "doc_count": 12,
                "score": 0.194912,
                "bg_count": 449         
            },
            ...
            ]
        }
    }
}

significant_text 聚集是一种专门为 text 类型设计的 significant_terms 聚集。无需开启 fielddata 机制。

单桶聚集和聚集组合

单桶聚集在返回结果中只会形成一个桶,它们都有比较特定的应用场景。

通过定义一个或多个过滤器来区分桶,满足过滤器条件的文档将落入这个过滤器形成的桶中。

POST /kibana_sample_data_flights/_search?filter_path=aggregations
{
    "aggs" : {

        "origin_cn": {
            "filter": {
                "term": {"OriginCountry": "CN"}
            },
            "aggs": {
                "cn_ticket_price": {
                    "avg": {
                        "field": "AvgTicketPrice" // 计算当前桶内文档 AvgTicketPrice 字段的平均值
                    }
                }
            }
        },
        "avg_price": {
            "avg": {
                "field": "AvgTicketPrice" // 计算所有文档的平均值
            }
        }
    }
}

使用 query 和 aggs 结果也能实现类似的功能,区别在于 filter 过滤器不会做相似度计算,效率更高一些。

global 聚集也是一种单桶聚集,它的作用是把索引中所有的文档归入一个桶中。

missing 聚集也是一种单桶聚集,它的作用是将某一字段缺失的文档归入一桶。

composite 聚集可以将不同类型的聚集组合到一起,它从不同的聚集中提取数据并以笛卡尔乘积的形式组合它们,而每一个组合就会形成一个新桶。

例如,想查看平均票价和出发机场天气的对应关系。

POST /kibana_sample_data_flights/_search?filter_path=aggregations
{
    "aggs" : {
        "price_weather": {
            "composite": {
                "sources": [
                    { "avg_price": {"histogram": "field": "AvgTicketPrice", "interval": 500 }},
                    { "weather": {"terms": {"field": "OriginWeather"}}}
                ]
            }
        }
    }
}

管道聚集

管道聚集不是直接从索引中读取文档,而是在其他聚集的基础上在进行聚集运算。可以理解为,在聚集结果上再做聚集运算

包括 avg_bucket,max_bucket,min_bucket, sum_bucket,stats_bucket 等。功能上跟名字一样。

POST /kibana_sample_data_flights/_search?filter_path=aggregations
{
    "aggs" : {
        "carriers" {
            "terms": {
                "field": "Carrier",
                "size": 10
            },
            "aggs": {
                "carrier_stat": {
                    "stats": {
                        "field": "AvgTicketPrice"
                    }
                }
            }
        }
    },
    "all_stat": {
        "avg_bucket": {
            "buckets_path": "carriers > carriers_stat.avg"
        }
    }
}

跳过

矩阵聚集

针对多个字段做多种聚集运算,产生的结果是一个矩阵。还处于开发阶段。

第8章 处理特殊数据类型

父子关系

在 ElasticSearch 中没有外键的概念,文档之间的父子关系通过给索引定义 join 类型字段实现

例如,定义一个 join 类型的 management 字段用于确定管理与被管理的关系。

POST employee
{
    "mappings": {
        "properties": {
            "management": {
                "type": "join",
                "relations": {
                    "manager": "member" // 名字用户自定义,manager 是父,member 是子。
                }
            }
        }
    }
}

在添加文档时,指定文档在父子关系的地位。

PUT /employee/_doc/1
{
    "name": "tom",
    "management": {
        "name": "manager"
    }
}

PUT /employee/_doc/2?routing=1
{
    "name": "smith",
    "management": {
        "name": "member",
        "parent": 1
    }
}

注意,在使用父子关系的时候,要求父子文档必须要映射到同一分片中

根据子文档检索父文档的方法。先根据查询条件将满足条件的子文档检索出来,在最终的结果中,返回具有这些子文档的父文档。

POST /employee/_search
{
    "query": {
        "has_child": {
            "type": "member",
            "query": {
                "match": {
                    "name": "smith"
                }
            }
        }
    }
}

最终结果会根据 smith 所在文档,通过 member 对应的父子关系检索它的父文档。

通过父文档检索子文档的一种方法。先将满足查询条件的父文档检索出来,在最终返回结果中展示具有这些父文档的子文档。

POST /employee/_search
{
    "query": {
        "has_parent": {
            "parent_type": "manager",
            "query": {
                "match": {
                    "name": "tom"
                }
            }
        }
    }
}

与 has_parent 作用相似,parent_id 查询只能通过父文档 _id 做检索。

POST /employee/_search
{
    "query": {
        "parent_id": {
            "type": "member",
            "id": 1
        }
    }
}

通过父文档检索与其关联的所有子文档就可以使用 children 聚集。

与 children 聚集相反,根据子文档查找父文档。

嵌套类型

ElasticSearch 虽然可以按照 JSON 对象格式保存结构化的对象数据,但是由于 lucene 并不支持对象类型,所以 ElasticSearch 在存储这种类型的字段会将它们平铺为单个属性

PUT colleges/_doc/1
{
    "address": {
        "country": "CN",
        "city": "BJ"
    },
    "age": 10
}

address 字段会被平铺为 address.country 和 address.city 两个字段存储。 这种平铺的存储方案在存储单个对象没有什么问题,但如果存储数组会丢失单个对象内部字段的匹配关系。 比如

"address": [
    {
        "country": "CN",
        "city": "BJ"
    },
    {
        "country": "US",
        "city": "NY"
    },
]

实际存储时,会被拆解成 address.country: ["CN", "US"]address.city:["BJ", "NY"]。这样,单个 country 和 city 的匹配关系就丢失了,所以,即使,NY 和 BJ 作为共同条件检索,上面文档也会被检索出来。

为了解决对象类型在数组中丢失内部字段之间匹配关系的问题,ElasticSearch 提供了一个特殊的类型 nested 类型这种类型会为数组中每一个对象创造一个单独文档,以保存对象的字段信息并使它们可以检索。 创建的单独文档并不直接可见,而是藏匿在父文档之中。

PUT colleges
{
    "mappings": {
        "properties": {
            "address": {
                "type": "nested"
            }
        }
    }
}

这样,NY 和 BJ 作为共同条件检索,上面文档就不会被检索出来了。

nested 查询只能针对 nested 类型字段,需要通过 path 参数指定 nested 类型字段的路径。

POST /colleges/_search
{
    "query": {
        "nested": {
            "path": "address",
            "query": {
                "bool": {
                    "must": [
                        {"match": {"address.country": "US"}},
                        {"match": {"address.country": "BJ"}},
                    ]
                }
            }
        }
    }
}

再次使用 US 和 BJ 作为查询条件,不会有文档返回。

可以针对 nested 数组中的对象做各种聚集运算。

POST /colleges/_search?filter_path=aggregations
{
    "aggs": {
        "nested_address": {
            "nested": {
                "path": "address"
            },
            "aggs": {
                "city_names": {
                    "terms": {
                        "filed": "address.city.keyword",
                        "size": 10
                    }
                }
            }
        }
    }
}

处理地理信息

GeoHash , 跳过

使用 SQL 语言

ElasticSearch 在 Basic 授权中支持以 SQL 语句的形式检索文档,SQL 语句在执行时会被翻译为 DSL 执行。 语法上,ElasticSearch 的 SQL 语句与 RDBMS 中的 SQL 语句基本一致。