目录
- 一. ES简介
- 二. ES安装
- 三. ES核心概念
- 四. ES分词器
- 五. 索引基本操作
- 六. 文档操作
- 七. 文档路由
- 八. 锁和版本控制
- 九. 倒排索引
- 十. 动态映射与动态映射
- 十一. ES字段类型详解
- 11.1 字符串类型
- 11.2 数字类型
- 11.3 日期类型
- 11.4 布尔类型(boolean)
- 11.5 二进制类型(binary)
- 11.6 范围类型
- 11.7 复合类型
- 11.8 地理类型
- 11.9 特殊类型
- 11.10 analyzer
- 11.11 search_analyzer
- 11.12 normalizer
- 11.13 boost
- 11.14 coerce
- 11.15 copy_to
- 11.16 doc_values 和 fielddata
- 11.17 dynamic
- 11.18 enabled
- 11.19 format
- 11.20 ignore_above
- 11.21 ignore_malformed
- 11.22 index
- 11.23 index_options
- 11.24 norms
- 11.25 null_value
- 11.26 position_increment_gap
- 11.27 similarity
- 11.28 store
- 11.29 fields
- 十二. ES文档搜索
- 十三. ES指标聚合
- 十四. ES桶聚合
- 十五. 管道聚合
- 十六. Java操作ES
一. ES简介
1.1 Lucene
Lucene 是一个开源、免费、高性能、纯 Java 编写的全文检索引擎 详情见 Lucene学习笔记,这里不做相关介绍。
1.2 ElasticSearch
ElasticSearch 是一个分布式、可扩展、近实时性、RESTful 风格的搜索和数据分析引擎。ElasticSearch 基于 Java 编写,通过进一步封装 Lucene,将搜索的复杂性屏蔽起来,开发者只需要一套简单的 RESTful API 就可以操作全文检索。
ElasticSearch 在分布式环境下表现优异,这也是它比较受欢迎的原因之一。它支持 PB 级别的结构化或非结构化海量数据处理
整体上来说,ElasticSearch 有三大功能:
- 数据搜集
- 数据分析
- 数据存储
ElasticSearch 的主要特点:
- 分布式文件存储。
- 实时分析的分布式搜索引擎。
- 高可拓展性。
- 可插拔的插件支持。
二. ES安装
2.1 单节点安装
进入ES官网,根据系统下载相对应的版本压缩包
下载后直接解压压缩包,解压后的目录含义如下:
目录 | 含义 |
---|---|
modules | 依赖模块目录 |
lib | 第三方依赖库 |
logs | 输出日志目录 |
plugins | 插件目录 |
bin | 可执行文件目录 |
config | 配置文件目录 |
data | 数据存储目录 |
进入bin
目录下后,直接执行elasticsearch.bat
即可(Windows版本)
看到 started 表示启动成功。
默认监听的端口是 9200
,浏览器直接输入 localhost:9200
可以查看节点信息。
节点的名字以及集群(默认是 elasticsearch)的名字,我们都可以自定义配置。
打开 config/elasticsearch.yml
文件,可以配置集群名称以及节点名称。配置方式如下:
配置完成后,保存配置文件,并重启 es。重启成功后,刷新浏览器 localhost:9200
页面,就可以看到最新信息
Es支持的版本对应矩阵: https://www.elastic.co/cn/support/matrix
2.2 Head插件安装
Elasticsearch-head 插件,可以通过可视化的方式查看集群信息。
2.2.1 浏览器插件安装
下载地址: https://download.csdn.net/download/zxc_123_789/19415225
下载完成后解压直接拖拽至谷歌浏览器即可安装成功
2.2.2 下载插件安装
GitHub地址:https://github.com/mobz/elasticsearch-head
git clone git://github.com/mobz/elasticsearch-head.git
cd elasticsearch-head
npm install
npm run start
启动成功后如下
注意:如果是通过下载插件安装方式的话可能会出现连接不上的问题,此时看不到集群数据。原因在于这里通过跨域的方式请求集群数据的,默认情况下,集群不支持跨域,所以这里就看不到集群数据。
解决办法如下,修改 es 的 config/elasticsearch.yml
配置文件,添加如下内容,使之支持跨域:
http.cors.enabled: true
http.cors.allow-origin: "*"
配置完成后,重启 es,此时 head 上就有数据了。
2.3 分布式集群安装
这里准备一主二从方式
master 的端口是 9200,slave 端口分别是 9201 和 9202
首先修改 master
的 config/elasticsearch.yml
配置文件:
node.name: master
node.master: true
network.host: 127.0.0.1
将 es 的压缩包解压两份,分别命名为 slave01 和 slave02,代表两个从机。并分别对其进行配置。
slave01/config/elasticsearch.yml:
cluster.name: my-application
node.name: slave01
network.host: 127.0.0.1
http.port: 9201
discovery.zen.ping.unicast.hosts: ["127.0.0.1"]
slave02/config/elasticsearch.yml:
cluster.name: my-application
node.name: slave02
network.host: 127.0.0.1
http.port: 9202
discovery.zen.ping.unicast.hosts: ["127.0.0.1"]
然后分别启动 slave01 和 slave02。启动后,可以在 head 插件上查看集群信息。
2.4 Kibana 安装
Kibana 是一个 Elastic 公司推出的一个针对 es 的分析以及数据可视化平台,可以搜索、查看存放在 es 中的数据。
Kibana官网下载地址 : https://www.elastic.co/cn/downloads/kibana
下载解压后直接进入bin
目录下启动即可.
配置 es 的地址信息(可选,如果 es 是默认地址以及端口,可以不用配置,具体的配置文件是 config/kibana.yml
)
启动成功后访问localhost:5601
Kibana 安装好之后,首次打开时,可以选择初始化 es 提供的测试数据,也可以不使用。
注意: es的安装版本和kibana的版本尽量保持一致,否则有可能kibana会启动报错
三. ES核心概念
3.1 集群(Cluster)
一个或者多个安装了 es 节点的服务器组织在一起,就是集群,这些节点共同持有数据,共同提供搜索服务。
一个集群有一个名字,这个名字是集群的唯一标识,该名字成为 cluster name,默认的集群名称是 elasticsearch,具有相同名称的节点才会组成一个集群。
可以在 config/elasticsearch.yml
文件中配置集群名称:
cluster.name: my-application
在集群中,节点的状态有三种:绿色、黄色、红色:
- 绿色:节点运行状态为健康状态。所有的主分片、副本分片都可以正常工作。
- 黄色:表示节点的运行状态为警告状态,所有的主分片目前都可以直接运行,但是至少有一个副本分片是不能正常工作的。
- 红色:表示集群无法正常工作。
3.2 节点 (Node)
集群中的一个服务器就是一个节点,节点中会存储数据,同时参与集群的索引以及搜索功能。一个节点想要加入一个集群,只需要配置一下集群名称即可。默认情况下,如果我们启动了多个节点,多个节点还能够互相发现彼此,那么它们会自动组成一个集群,这是 es 默认提供的,但是这种方式并不可靠,有可能会发生脑裂现象。所以在实际使用中,建议一定手动配置一下集群信息。
3.3 索引(Index)
索引可以从两方面来理解:
名词
具有相似特征文档的集合。
动词
索引数据以及对数据进行索引操作。
3.4 类型(Type)
类型是索引上的逻辑分类或者分区。在 es6 之前,一个索引中可以有多个类型,从 es7 开始,一个索引中,只能有一个类型。在 es6.x 中,依然保持了兼容,依然支持单 index 多个 type 结构,但是已经不建议这么使用。
3.5 文档(Document)
一个可以被索引的数据单元。例如一个用户的文档、一个产品的文档等等。文档都是 JSON 格式的。
3.6 分片(Shards)
索引都是存储在节点上的,但是受限于节点的空间大小以及数据处理能力,单个节点的处理效果可能不理想,此时我们可以对索引进行分片。当我们创建一个索引的时候,就需要指定分片的数量。每个分片本身也是一个功能完善并且独立的索引。
默认情况下,一个索引会自动创建 1 个分片,并且为每一个分片创建一个副本。
3.7 副本(Replicas)
副本也就是备份,是对主分片的一个备份。
3.8 Settings
集群中对索引的定义信息,例如索引的分片数、副本数等等。
3.9 Mapping
Mapping 保存了定义索引字段的存储类型、分词方式、是否存储等信息。
3.10 Analyzer
字段分词方式的定义。
3.11 ElasticSearch Vs 关系型数据库
四. ES分词器
4.1 ES内置分词器
ElasticSearch 核心功能就是数据检索,首先通过索引将文档写入 es。查询分析则主要分为两个步骤:
- 词条化:分词器将输入的文本转为一个一个的词条流。
- 过滤:比如停用词过滤器会从词条中去除不相干的词条(的,嗯,啊,呢等语气助词);另外还有同义词过滤器、小写过滤器等。
ElasticSearch 中内置了多种分词器可以供使用。
内置分词器 :
4.2 中文分词器
在 Es 中,使用较多的中文分词器是 elasticsearch-analysis-ik
,这个是 es 的一个第三方插件,代码托管在 GitHub 上:
https://github.com/medcl/elasticsearch-analysis-ik
打开后下载与es版本对应的中文分词器压缩包,在 es/plugins
目录下,新建 ik 目录,并将解压后的所有文件拷贝到 ik 目录下。然后重启es。
测试 :
首先创建一个名为 test 的索引:
接下来,在该索引中进行分词测试:
4.2.1 自定义扩展词库
-
本地自定义
在
es/plugins/ik/config
目录下,新建ext.dic
文件(文件名任意),在该文件中可以配置自定义的词库。不为谁而做的歌
如果有多个词,换行写入新词即可。
然后在
es/plugins/ik/config/IKAnalyzer.cfg.xml
中配置扩展词典的位置:
重启es后测试分词结果
-
远程词库
也可以配置远程词库,远程词库支持热更新(不用重启 es 就可以生效)。热更新只需要提供一个接口,接口返回扩展词即可。
可以将需自动更新的热词放在一个 UTF-8 编码的 .txt 文件里,放在 nginx 或其他简易 http server 下,当 .txt 文件修改时,http server 会在客户端请求该文件时自动返回相应的 Last-Modified 和 ETag。可以另外做一个工具来从业务系统提取相关词汇,并更新这个 .txt 文件。这里我们以nginx为例,nginx端口号映射为8080,将ext.dic放入nginx的html目录下
配置完成后,重启 es ,即可生效。后续在新添加词库都不需要再重启es了
热更新,主要是响应头的 Last-Modified 或者 ETag 字段发生变化,ik 就会自动重新加载远程扩展
五. 索引基本操作
5.1 新建索引
在 head 插件中,选择索引选项卡,然后点击新建索引。新建索引时,需要填入索引名称、分片数以及副本数。默认的分片数为5,副本数为1.
0、1、2、3、4 分别表示索引的5个分片,粗框表示主分片,细框表示副本(点一下框,通过 primary 属性可以查看是主分片还是副本)
可以通过 postman 或者 kibana 发送请求来创建
PUT test1
创建成功后,可以通过head插件来查看索引信息
在创建索引时,需要注意的是
- 索引名称不能有大写字母
- 索引名是唯一的,不能重复,重复创建会出错
5.2 修改索引
索引创建好之后,可以修改其属性。
5.2.1 修改索引副本
例如 : 修改其副本数为2
PUT test/_settings
{
"number_of_replicas":2
}
更新其他索引参数也类似
5.2.2 修改索引读写权限
索引创建成功后,可以向索引中写入文档:
PUT test/_doc/1
{
"title":"hello1"
}
默认情况下,索引是具备读写权限的,当然这个读写权限可以关闭。
例如,关闭索引的写权限:
PUT test/_settings
{
"blocks.write": true
}
关闭之后,就无法添加文档了。关闭了写权限之后,如果想要再次打开,方式如下:
PUT book/_settings
{
"blocks.write": false
}
其他类似的权限有:
- blocks.write
- blocks.read
- blocks.read_only
5.3 查看索引
可以通过head插件查看
请求查看方式如下:
可以同时查看多个索引信息,中间用逗号隔开
也可以查看所有索引信息:
GET _all/_settings
5.4 删除索引
可以通过head插件删除索引
也可以通过请求方式删除
DELETE test1
5.5 索引的打开/关闭
POST .kibana/_close
POST .kibana/_open
可以同时关闭/打开多个索引,多个索引用,
隔开,或者直接使用 _all
代表所有索引。
5.6 复制索引
POST _reindex
{
"source": {
"index": "test"
},
"dest": {
"index": "test_new"
}
}
索引复制,只会复制数据,不会复制索引配置。复制的时候,可以添加查询条件。
5.7 索引别名
可以为索引创建别名,如果这个别名是唯一的,该别名可以代替索引名称。
POST _aliases
{
"actions": [
{
"add": {
"index": "test",
"alias": "test-alias"
}
}
]
}
删除别名
POST _aliases
{
"actions": [
{
"remove": {
"index": "test",
"alias": "test-alias"
}
}
]
}
5.8 查看索引别名
GET test/_alias
查看所有索引别名
GET _alias
六. 文档操作
6.1 新建文档
向索引中添加一个文档:
_doc
表示文档类型, 1
表示添加文档的ID
PUT test/_doc/1
{
"title": "es文档添加",
"content":"文档添加操作学习"
}
添加成功后,响应的 json 如下
_index 表示文档索引。
_type 表示文档的类型。
_id 表示文档的 id。
_version 表示文档的版本(更新文档,版本会自动加 1,针对一个文档的)。
result 表示执行结果。
_shards 表示分片信息。
_seq_no 和 _primary_term 这两个也是版本控制用的(针对当前 index)。
当然,添加文档时,也可以不指定 id,此时系统会默认给出一个 id,如果不指定 id,则需要使用POST
请求,而不能使用 PUT
请求。
POST test/_doc
{
"title": "es文档添加",
"content":"post文档添加操作学习"
}
6.2 获取文档
根据文档ID获取文档
GET test/_doc/t3RD5XkBJY9c7kUxehIr
如果不存在此文档则返回
如果仅仅只是想探测某一个文档是否存在,可以使用 head 请求:
也可以批量获取文档
GET test/_mget
{
"ids":["t3RD5XkBJY9c7kUxehIr","1","2"]
}
6.3 文档更新
6.3.1 普通文档更新
注意,文档更新一次,version 就会自增 1。
这种方式,更新的文档会覆盖掉原文档。
PUT test/_doc/t3RD5XkBJY9c7kUxehIr
{
"title": "3"
}
如果我们只想更新文档的部门字段
POST test/_update/tHRD5XkBJY9c7kUxQBIJ
{
"script": {
"lang": "painless",
"source": "ctx._source.title=params.title",
"params":{
"title":"单个字段修改es文档"
}
}
}
在脚本中,lang
表示脚本语言,painless
是 es 内置的一种脚本语言。source
表示具体执行的脚本,ctx
是一个上下文对象,通过 ctx
可以访问到 _source
、_title
等。
也可以向文档中添加字段:
POST test/_update/tHRD5XkBJY9c7kUxQBIJ
{
"script": {
"lang": "painless",
"source": "ctx._source.date=params.date",
"params":{
"date":["2021-06-07","2021-06-08"]
}
}
}
通过脚本语言,也可以修改数组。例如再增加一个 date:
POST test/_update/tHRD5XkBJY9c7kUxQBIJ
{
"script": {
"lang": "painless",
"source": "ctx._source.date.add(\"2020-06-09\")"
}
}
也可以使用 if else 构造稍微复杂一点的逻辑。
POST test/_update/tHRD5XkBJY9c7kUxQBIJ
{
"script": {
"lang": "painless",
"source": "if(ctx._source.title.contains(\"单个\")){ctx.op=\"delete\"}else{ctx.op=\"none\"}"
}
}
6.3.2 条件更新
通过条件查询找到文档,然后再去更新
例如将 title 中包含 666 的文档的更新
POST test/_update_by_query
{
"script": {
"lang": "painless",
"source": "ctx._source=params",
"params": {
"title":"777",
"content":"es进阶之路"
}
},
"query":{
"term": {
"title":"666"
}
}
}
6.4 文档删除
6.4.1 根据ID删除
从test索引中删除一个ID为t3RD5XkBJY9c7kUxehIr
的文档
DELETE test/_doc/t3RD5XkBJY9c7kUxehIr
如果在添加文档时指定了路由,则删除文档时也需要指定路由,否则删除失败。
6.4.2 查询删除
如删除 title 中包含 777的文档:
POST test/_delete_by_query
{
"query":{
"term":{
"title":"777"
}
}
}
删除test_new索引下的所有文档:
POST test_new/_delete_by_query
{
"query":{
"match_all":{}
}
}
6.5 批量操作
es 中通过 Bulk API 可以执行批量索引、批量删除、批量更新等操作。
首先需要将所有的批量操作写入一个 JSON 文件中,然后通过 POST 请求将该 JSON 文件上传并执行。
新建一个batch.json的文件
{"index":{"_index":"java","_id":"5"}}
{"title":"es批量操作"}
{"update":{"_index":"java","_id":"5"}}
{"doc":{"title":"es修改操作"}}
首先第一行:index
表示要执行一个索引操作(这个表示一个 action,其他的 action 还有 create,delete,update
)。_index
定义了索引名称,这里表示要创建一个名为 java的索引,_id
表示新建文档的 id 为 666。
第二行是第一行操作的参数。
第三行的 update
则表示要更新。
第四行是第三行的参数。
注意,结尾要空出一行。
文件创建成功后,在该目录下,执行请求命令,如下:
curl -XPOST "http://localhost:9200/java/_bulk" -H "content-type:application/json" --data-binary @batch.json
执行完成后,就会创建一个名为 java 的索引,同时向该索引中添加一条记录,再修改该记录,最终结果如下
七. 文档路由
es 是一个分布式系统,当我们存储一个文档到 es 上之后,这个文档实际上是被存储到 master 节点中的某一个主分片上。
新建一个索引,该索引有两个分片,0个副本
向索引里保存一个文档
PUT luyou/_doc/a
{
"title":"15"
}
文档保存成功后,可以查看该文档被保存到哪个分片中去了:
GET _cat/shards/luyou?v
从这个结果中,可以看出,文档被保存到分片 1 中。
7.1 路由规则
es 中的路由机制是通过哈希算法,将具有相同哈希值的文档放到一个主分片中,分片位置的计算方式如下:
shard=hash(routing) % number_of_primary_shards
routing 可以是一个任意字符串,es 默认是将文档的 id 作为 routing 值,通过哈希函数根据 routing 生成一个数字,然后将该数字和分片数取余,取余的结果就是分片的位置。
默认的这种路由模式,最大的优势在于负载均衡,这种方式可以保证数据平均分配在不同的分片上。但是他有一个很大的劣势,就是查询时候无法确定文档的位置,此时它会将请求广播到所有的分片上去执行。另一方面,使用默认的路由模式,后期修改分片数量不方便。
因此 也可以自定义routing的值
PUT luyou/_doc/e?routing=userId
{
"title":"666"
}
如果文档在添加时指定了 routing,则查询、删除、更新时也需要指定 routing。
GET luyou/_doc/e?routing=userId
自定义 routing 有可能会导致负载不均衡,这个还是要结合实际情况选择。
典型场景:
对于用户数据,我们可以将 userid 作为 routing,这样就能保证同一个用户的数据保存在同一个分片中,检索时,同样使用 userid 作为 routing,这样就可以精准的从某一个分片中获取数据。
八. 锁和版本控制
当我们使用 es 的 API 去进行文档更新时,它首先读取原文档出来,然后对原文档进行更新,更新完成后再重新索引整个文档。不论你执行多少次更新,最终保存在 es 中的是最后一次更新的文档。但是如果有两个线程同时去更新,就有可能出问题。
要解决问题,就是锁。
8.1 锁
悲观锁
很悲观,每一次去读取数据的时候,都认为别人可能会修改数据,所以屏蔽一切可能破坏数据完整性的操作。关系型数据库中,悲观锁使用较多,例如行锁、表锁等等。
乐观锁
很乐观,每次读取数据时,都认为别人不会修改数据,因此也不锁定数据,只有在提交数据时,才会检查数据完整性。这种方式可以省去锁的开销,进而提高吞吐量。
在 es 中,实际上使用的就是乐观锁。
8.2 版本控制
es6.7之前
在 es6.7 之前,使用 version
+version_type
来进行乐观并发控制。根据前面的介绍,文档每被修改一个,version
就会自增一次,es 通过 version
字段来确保所有的操作都有序进行。
version 分为内部版本控制和外部版本控制。
8.2.1 内部版本
es 自己维护的就是内部版本,当创建一个文档时,es 会给文档的版本赋值为 1。
每当用户修改一次文档,版本号就回自增 1。
如果使用内部版本,es 要求 version 参数的值必须和 es 文档中 version 的值相当,才能操作成功。
8.2.2 外部版本
也可以维护外部版本。
在添加文档时,就指定版本号:
PUT luyou/_doc/2?version=666&version_type=external
{
"title":"12354"
}
以后更新的时候,版本要大于已有的版本号。
- vertion_type=external 或者 vertion_type=external_gt 表示以后更新的时候,版本要 > 已有的版本号。
- vertion_type=external_gte 表示以后更新的时候,版本要 >= 已有的版本号。
8.2.3 最新方案(Es6.7 之后)
现在使用 if_seq_no
和 if_primary_term
两个参数来做并发控制。
seq_no 不属于某一个文档,它是属于整个索引的(version 则是属于某一个文档的,每个文档的 version 互不影响)。 现在更新文档时,使用 seq_no
来做并发。由于 seq_no
是属于整个index
的,所以任何文档的修改或者新增,seq_no
都会自增。
现在就可以通过 seq_no
和 primary_term
来做乐观并发控制。
PUT luyou/_doc/2?if_seq_no=15&if_primary_term=1
{
"title":"12567"
}
九. 倒排索引
9.1 "正排"索引
我们在关系型数据库中见到的索引,就是“正排索引”。当我们通过 id 或者标题去搜索文章时,就可以快速搜到。
但是如果我们按照文章内容的关键字去搜索,就只能去内容中做字符匹配了。为了提高查询效率,就要考虑使用倒排索引。
9.2 倒排索引
倒排索引就是以内容的关键字建立索引,通过索引找到文档 id,再进而找到整个文档。
一般来说,倒排索引分为两个部分:
- 单词词典(记录所有的文档词项,以及词项到倒排列表的关联关系)
- 倒排列表(记录单词与对应的关系,由一系列倒排索引项组成,倒排索引项指:文档 id、词频(TF)(词项在文档中出现的次数,评分时使用)、位置(Position,词项在文档中分词的位置)、偏移(记录词项开始和结束的位置))当我们去索引一个文档时,就会建立倒排索引,搜索时,直接根据倒排索引搜索。
十. 动态映射与动态映射
映射就是 Mapping,它用来定义一个文档以及文档所包含的字段该如何被存储和索引。所以,它其实有点类似于关系型数据库中表字段的定义。
10.1 映射分类
动态映射
顾名思义,就是自动创建出来的映射。es 根据存入的文档,自动分析出来文档中字段的类型以及存储方式,这种就是动态映射。
举一个简单例子,新建一个索引,然后插入一条文档
PUT luyou/_doc/2
{
"title":"动态映射",
"remark":"2020-06-11"
}
文档添加成功后,就会自动生成 Mappings:
可以看到,remark 字段的类型为 date,title 的类型有两个,text 和 keyword。
默认情况下,文档中如果新增了字段,mappings 中也会自动新增进来。
有的时候,如果希望新增字段时,能够抛出异常来提醒开发者,这个可以通过 mappings
中 dynamic
属性来配置。
- true,默认即此。自动添加新字段。
- false,忽略新字段。
- strict,严格模式,发现新字段会抛出异常。
具体配置方式如下,创建索引时指定 mappings(这其实就是静态映射):
PUT java
{
"mappings": {
"dynamic": "strict",
"properties": {
"title": {
"type": "text"
},
"id": {
"type": "long"
}
}
}
}
在添加的文档中,多出了一个 name 字段,而该字段没有预定义,所以这个添加操作就会报错:
动态映射还有一个日期检测的问题。
例如新建一个索引,然后添加一个含有日期的文档,如下:
PUT java/_doc/2
{
"title":"动态映射",
"remark":"2020-06-11"
}
添加成功后,remark 字段会被推断是一个日期类型。此时,remark 字段就无法存储其他类型了。
要解决这个问题,可以使用静态映射,即在索引定义时,将 remark 指定为text
类型。也可以关闭日期检测。
PUT java
{
"mappings": {
"date_detection": false
}
}
此时日期类型就回当成文本来处理。
10.2 类型推断
es 中动态映射类型推断方式如下:
存储的数据 | 推断出的类型 |
---|---|
null | 没有字段被添加 |
true/false | boolean |
浮点数字 | float |
数字 | long |
JSON对象 | object |
数组 | 数组中的第一个非空值来决定 |
string | text/keyword/date/double/long…都有可能 |
十一. ES字段类型详解
11.1 字符串类型
- string:这是一个已经过期的字符串类型。在 es5 之前,用这个来描述字符串,现在已经被 text 和 keyword 替代了。
- text:如果一个字段是要被全文检索的,比如说博客内容、新闻内容、产品描述,那么可以使用 text。用了 text 之后,字段内容会被分析,在生成倒排索引之前,字符串会被分词器分成一个个词项。text 类型的字段不用于排序,很少用于聚合。这种字符串也被称为 analyzed 字段。
- keyword:这种类型适用于结构化的字段,例如标签、email 地址、手机号码等等,这种类型的字段可以用作过滤、排序、聚合等。这种字符串也称之为 not-analyzed 字段。
11.2 数字类型
类型 | 取值范围 |
---|---|
byte | -128 ~127 |
short | -2^15 ~ -2^15-1 |
integer | -2^31 ~ -2^31-1 |
long | -2^63 ~ -2^63-1 |
double | 64位的双精度IEEE754浮点型 |
float | 32位的双精度IEEE754浮点型 |
half_float | 16位的双精度IEEE754浮点型 |
scaled_float | 缩放类型的浮点型 |
- 在满足需求的情况下,优先使用范围小的字段。字段长度越短,索引和搜索的效率越高。
- 浮点数,优先考虑使用
scaled_float
PUT subject
{
"mappings": {
"properties": {
"id":{
"type": "short"
},
"name":{
"type": "keyword"
},
"price":{
"type": "scaled_float",
"scaling_factor": 100
}
}
}
}
11.3 日期类型
由于 JSON 中没有日期类型,所以 es 中的日期类型形式就比较多样:
- 2020-11-11 或者 2020-11-11 11:11:11
- 一个从 1970.1.1 零点到现在的一个秒数或者毫秒数。
- es 内部将时间转为 UTC,然后将时间按照 millseconds-since-the-epoch 的长整型来存储。
自定义日期类型:
PUT subject
{
"mappings": {
"properties": {
"id":{
"type": "short"
},
"name":{
"type": "keyword"
},
"price":{
"type": "scaled_float",
"scaling_factor": 100
},
"date":{
"type": "date"
}
}
}
}
PUT subject/_doc/1
{
"id":1,
"name":"java",
"price":19.99,
"date":"2021-06-15",
"remark":"java编程思想"
}
PUT subject/_doc/2
{
"id":1,
"name":"java",
"price":19.99,
"date":"2021-06-15T12:19:29Z",
"remark":"java编程思想"
}
PUT subject/_doc/3
{
"id":1,
"name":"java",
"price":19.99,
"date":"1623752415002",
"remark":"java编程思想"
}
这三个文档中的日期都可以被解析,内部存储的是毫秒计时的长整型数。
11.4 布尔类型(boolean)
JSON 中的 “true”、“false”、true、false 都可以。
11.5 二进制类型(binary)
二进制接受的是 base64
编码的字符串,默认不存储,也不可搜索。
11.6 范围类型
- integer_range
- float_range
- long_range
- double_range
- date_range
- ip_range
PUT subject
{
"mappings": {
"properties": {
"price":{
"type": "float_range"
}
}
}
}
PUT subject/_doc/1
{
"name":"学科",
"price": {
"gt": 10,
"lt": 20
}
}
指定范围的时,可以使用 gt、gte、lt、lte
。
11.7 复合类型
11.7.1 数组类型
es 中没有专门的数组类型。默认情况下,任何字段都可以有一个或者多个值。需要注意的是,数组中的元素必须是同一种类型。
添加数组是,数组中的第一个元素决定了整个数组的类型。
PUT subject/_doc/3
{
"name":["java","php"],
"ext_info":{
"remark":"对象类型"
}
}
11.7.2 对象类型
由于 JSON 本身具有层级关系,所以文档包含内部对象。内部对象中,还可以再包含内部对象。
PUT subject/_doc/2
{
"name":"学科",
"ext_info":{
"remark":"对象类型"
}
}
11.7.3 嵌套类型
nested 是 object 中的一个特例。
如果使用 object 类型,假如有如下一个文档:
{
"user":[
{
"first":"Zhang",
"last":"san"
},
{
"first":"Li",
"last":"si"
}
]
}
由于 Lucene 没有内部对象的概念,所以 es 会将对象层次扁平化,将一个对象转为字段名和值构成的简单列表。即上面的文档,最终存储形式如下:
{
"user.first":["Zhang","Li"],
"user.last":["san","si"]
}
扁平化之后,用户名之间的关系没了。这样会导致如果搜索 Zhang si
这个人,会搜索到。
此时可以 nested
类型来解决问题,nested
对象类型可以保持数组中每个对象的独立性。nested
类型将数组中的每一个对象作为独立隐藏文档来索引,这样每一个嵌套对象都可以独立被索引。
{
{
"user.first":"Zhang",
"user.last":"san"
},{
"user.first":"Li",
"user.last":"si"
}
}
优点 : 文档存储在一起,读取性能高。
缺点 : 更新父或者子文档时需要更新更个文档。
11.8 地理类型
使用场景
- 查找某一个范围内的地理位置
- 通过地理位置或者相对中心点的距离来聚合文档
- 通过距离对文档进行排序
11.8.1 geo_point
geo_point 就是一个坐标点,定义方式如下:
PUT shape
{
"mappings": {
"properties": {
"location":{
"type": "geo_point"
}
}
}
}
创建时指定字段类型,存储的时候,有四种方式:
PUT shape/_doc/2
{
"location":{
"lon":100,
"lat":33
}
}
PUT shape/_doc/1
{
"location":"33,100"
}
PUT shape/_doc/3
{
"location":"uzbrgzfxuzup"
}
PUT shape/_doc/4
{
"location":[100,33]
}
注意,使用数组描述,先经度后纬度。
地址位置转 geo_hash:http://www.csxgame.top/#/
11.8.2 geo_shape
GeoJSON | ES | 备注 |
---|---|---|
Point | point | 一个由经纬度描述的点 |
LineString | linestring | 一条线,由任意2个以上的点组成 |
Polygon | polygon | 一个封闭的多边形 |
MultiPoint | multipoint | 一组不连续的点 |
MultiLineString | multilinestring | 多条不关联的线 |
MutliPolygon | mutlipolygon | 多个多边形 |
GeometryCollection | geometrycollection | 几个对象的集合 |
/ | circle | 圆形 |
/ | envelope | 通过左上角和右下角确定的矩形 |
创建索引时指定 geo_shape 类型:
PUT shape
{
"mappings": {
"properties": {
"location":{
"type": "geo_shape"
}
}
}
}
添加文档时需要指定具体的类型:
PUT shape/_doc/1
{
"location":{
"type":"point",
"coordinates":[100,33]
}
}
PUT shape/_doc/2
{
"location":{
"type":"linestring",
"coordinates":[[100,33],[200,10]]
}
}
11.9 特殊类型
11.9.1 IP
存储 IP 地址,类型是 ip:
PUT shape
{
"mappings": {
"properties": {
"address":{
"type": "ip"
}
}
}
}
添加文档
PUT shape/_doc/1
{
"address":"192.168.1.1"
}
搜索文档
GET shape/_search
{
"query": {
"term": {
"address": "192.168.0.0/16"
}
}
}
11.9.2 token_count
用于统计字符串分词后的词项个数。
PUT shape
{
"mappings": {
"properties": {
"title": {
"type": "text",
"fields": {
"length": {
"type": "token_count",
"analyzer": "standard"
}
}
}
}
}
}
相当于新增了 title.length 字段用来统计分词后词项的个数。
添加文档:
PUT shape/_doc/1
{
"title":"li si"
}
分词后为2个词,因此可以通过 token_count 去查询:
GET shape/_search
{
"query": {
"term": {
"title.length": 2
}
}
}
11.10 analyzer
定义文本字段的分词器。默认对索引和查询都是有效的。
假设不用分词器,先来看一下索引的结果,创建一个索引并添加一个文档:
PUT news
PUT news/_doc/1
{
"title":"她可能笑话这生硬的搭讪,也可能以为我对她有好感"
}
查看词条向量(term vectors)
GET news/_termvectors/1
{
"fields": ["title"]
}
查看结果如下:
{
"_index" : "news",
"_type" : "_doc",
"_id" : "1",
"_version" : 1,
"found" : true,
"took" : 0,
"term_vectors" : {
"title" : {
"field_statistics" : {
"sum_doc_freq" : 19,
"doc_count" : 1,
"sum_ttf" : 22
},
"terms" : {
"为" : {
"term_freq" : 1,
"tokens" : [
{
"position" : 15,
"start_offset" : 16,
"end_offset" : 17
}
]
},
"也" : {
"term_freq" : 1,
"tokens" : [
{
"position" : 11,
"start_offset" : 12,
"end_offset" : 13
}
]
},
"以" : {
"term_freq" : 1,
"tokens" : [
{
"position" : 14,
"start_offset" : 15,
"end_offset" : 16
}
]
},
"可" : {
"term_freq" : 2,
"tokens" : [
{
"position" : 1,
"start_offset" : 1,
"end_offset" : 2
},
{
"position" : 12,
"start_offset" : 13,
"end_offset" : 14
}
]
},
"她" : {
"term_freq" : 2,
"tokens" : [
{
"position" : 0,
"start_offset" : 0,
"end_offset" : 1
},
{
"position" : 18,
"start_offset" : 19,
"end_offset" : 20
}
]
},
"好" : {
"term_freq" : 1,
"tokens" : [
{
"position" : 20,
"start_offset" : 21,
"end_offset" : 22
}
]
},
"对" : {
"term_freq" : 1,
"tokens" : [
{
"position" : 17,
"start_offset" : 18,
"end_offset" : 19
}
]
},
"感" : {
"term_freq" : 1,
"tokens" : [
{
"position" : 21,
"start_offset" : 22,
"end_offset" : 23
}
]
},
"我" : {
"term_freq" : 1,
"tokens" : [
{
"position" : 16,
"start_offset" : 17,
"end_offset" : 18
}
]
},
"搭" : {
"term_freq" : 1,
"tokens" : [
{
"position" : 9,
"start_offset" : 9,
"end_offset" : 10
}
]
},
"有" : {
"term_freq" : 1,
"tokens" : [
{
"position" : 19,
"start_offset" : 20,
"end_offset" : 21
}
]
},
"生" : {
"term_freq" : 1,
"tokens" : [
{
"position" : 6,
"start_offset" : 6,
"end_offset" : 7
}
]
},
"的" : {
"term_freq" : 1,
"tokens" : [
{
"position" : 8,
"start_offset" : 8,
"end_offset" : 9
}
]
},
"硬" : {
"term_freq" : 1,
"tokens" : [
{
"position" : 7,
"start_offset" : 7,
"end_offset" : 8
}
]
},
"笑" : {
"term_freq" : 1,
"tokens" : [
{
"position" : 3,
"start_offset" : 3,
"end_offset" : 4
}
]
},
"能" : {
"term_freq" : 2,
"tokens" : [
{
"position" : 2,
"start_offset" : 2,
"end_offset" : 3
},
{
"position" : 13,
"start_offset" : 14,
"end_offset" : 15
}
]
},
"讪" : {
"term_freq" : 1,
"tokens" : [
{
"position" : 10,
"start_offset" : 10,
"end_offset" : 11
}
]
},
"话" : {
"term_freq" : 1,
"tokens" : [
{
"position" : 4,
"start_offset" : 4,
"end_offset" : 5
}
]
},
"这" : {
"term_freq" : 1,
"tokens" : [
{
"position" : 5,
"start_offset" : 5,
"end_offset" : 6
}
]
}
}
}
}
}
可以看到,默认情况下,中文就是一个字一个字的分,这种分词方式没有任何意义。如果这样分词,查询就只能按照一个字一个字来查,像下面这样:
GET news/_search
{
"query": {
"term": {
"title": {
"value": "可"
}
}
}
}
所以,我们要根据实际情况,配置合适的分词器。
给字段设定分词器:
PUT news
{
"mappings": {
"properties": {
"title":{
"type": "text",
"analyzer": "ik_max_word"
}
}
}
}
存储文档
PUT news/_doc/1
{
"title":"她可能笑话这生硬的搭讪,也可能以为我对她有好感"
}
查看分词后的词条
{
"_index" : "news",
"_type" : "_doc",
"_id" : "1",
"_version" : 4,
"found" : true,
"took" : 2,
"term_vectors" : {
"title" : {
"field_statistics" : {
"sum_doc_freq" : 56,
"doc_count" : 4,
"sum_ttf" : 60
},
"terms" : {
"也" : {
"term_freq" : 1,
"tokens" : [
{
"position" : 7,
"start_offset" : 12,
"end_offset" : 13
}
]
},
"以为" : {
"term_freq" : 1,
"tokens" : [
{
"position" : 10,
"start_offset" : 15,
"end_offset" : 17
}
]
},
"可能" : {
"term_freq" : 2,
"tokens" : [
{
"position" : 1,
"start_offset" : 1,
"end_offset" : 3
},
{
"position" : 8,
"start_offset" : 13,
"end_offset" : 15
}
]
},
"她可" : {
"term_freq" : 1,
"tokens" : [
{
"position" : 0,
"start_offset" : 0,
"end_offset" : 2
}
]
},
"她有" : {
"term_freq" : 1,
"tokens" : [
{
"position" : 13,
"start_offset" : 19,
"end_offset" : 21
}
]
},
"好感" : {
"term_freq" : 1,
"tokens" : [
{
"position" : 14,
"start_offset" : 21,
"end_offset" : 23
}
]
},
"对" : {
"term_freq" : 1,
"tokens" : [
{
"position" : 12,
"start_offset" : 18,
"end_offset" : 19
}
]
},
"我" : {
"term_freq" : 1,
"tokens" : [
{
"position" : 11,
"start_offset" : 17,
"end_offset" : 18
}
]
},
"搭讪" : {
"term_freq" : 1,
"tokens" : [
{
"position" : 6,
"start_offset" : 9,
"end_offset" : 11
}
]
},
"生硬" : {
"term_freq" : 1,
"tokens" : [
{
"position" : 4,
"start_offset" : 6,
"end_offset" : 8
}
]
},
"的" : {
"term_freq" : 1,
"tokens" : [
{
"position" : 5,
"start_offset" : 8,
"end_offset" : 9
}
]
},
"笑话" : {
"term_freq" : 1,
"tokens" : [
{
"position" : 2,
"start_offset" : 3,
"end_offset" : 5
}
]
},
"能以" : {
"term_freq" : 1,
"tokens" : [
{
"position" : 9,
"start_offset" : 14,
"end_offset" : 16
}
]
},
"这" : {
"term_freq" : 1,
"tokens" : [
{
"position" : 3,
"start_offset" : 5,
"end_offset" : 6
}
]
}
}
}
}
}
然后就可以去通过词语搜索了
11.11 search_analyzer
查询时候用的分词器。默认情况下,如果没有配置 search_analyzer
,则查询时,首先查看有没有search_analyzer
,有的话,就用 search_analyzer
来进行分词,如果没有,则看有没有 analyzer
,如果有,则用 analyzer
来进行分词,否则使用 es 默认的分词器。
11.12 normalizer
normalizer 参数用于解析前(索引或者查询)的标准化配置。
比如,在 es 中,对于一些我们不想切分的字符串,我们通常会将其设置为 keyword,搜索时候也是使用整个词进行搜索。如果在索引前没有做好数据清洗,导致大小写不一致,此时,我们就可以使用 normalizer 在索引之前以及查询之前进行文档的标准化。
先来一个反例
PUT subject
{
"mappings": {
"properties": {
"title":{
"type": "keyword"
}
}
}
}
PUT subject/_doc/1
{
"title":"english"
}
PUT subject/_doc/2
{
"title":"ENGLISH"
}
搜索
GET subject/_search
{
"query": {
"term": {
"title": {
"value": "english"
}
}
}
}
大写关键字可以搜到大写的文档,小写关键字可以搜到小写的文档。
如果使用了 normalizer
,可以在索引和查询时,分别对文档进行预处理。
normalizer 定义方式如下:
PUT subject
{
"settings": {
"analysis": {
"normalizer":{
"case_filtering":{
"type":"custom",
"filter":["lowercase"]
}
}
}
},
"mappings": {
"properties": {
"title":{
"type": "keyword",
"normalizer": "case_filtering"
}
}
}
}
在 settings 中定义 normalizer,然后在 mappings 中引用。
测试方式和前面一致。此时查询的时候,大写关键字也可以查询到小写文档,因为无论是索引还是查询,都会将大写转为小写。
11.13 boost
boost 参数可以设置字段的权重
。
boost 有两种使用思路,一种就是在定义 mappings 的时候使用,在指定字段类型时使用;另一种就是在查询时使用。
实际开发中建议使用后者,前者有问题:如果不重新索引文档,权重无法修改。
mapping 中使用 boost(不推荐):
PUT subject
{
"mappings": {
"properties": {
"title": {
"type": "text",
"analyzer": "ik_max_word",
"boost": 3
}
}
}
}
另一种方式就是在查询的时候,指定 boost
GET subject/_search
{
"query": {
"match": {
"title": {
"query":"何处",
"boost": 0.051977858
}
}
}
}
11.14 coerce
coerce 用来清除脏数据,默认为 true。
例如在定义一个索引时,某个字段的映射类型为integer
类型,而在默认情况下插入字符串类型的数字时,也是能插入成功的
PUT subject
{
"mappings": {
"properties": {
"age":{
"type": "integer"
, "coerce": false
}
}
}
}
这样在插入字符串类型的数字时就会报错
PUT subject/_doc/3
{
"age":"100.00"
}
当 coerce 修改为 false 之后,数字就只能是数字了,不可以是字符串,该字段传入字符串会报错。
11.15 copy_to
这个属性,可以将多个字段的值,复制到同一个字段中。
定义方式如下:
PUT subject
{
"mappings": {
"properties": {
"age":{
"type": "text",
"copy_to": "backup"
},
"name":{
"type": "text",
"copy_to": "backup"
},
"backup":{
"type": "text"
}
}
}
}
插入文档
PUT subject/_doc/1
{
"age":100,
"name":"学生"
}
根据backup
字段查询
GET subject/_search
{
"query": {
"term": {
"backup": {
"value": "100"
}
}
}
}
11.16 doc_values 和 fielddata
es 中的搜索主要是用到倒排索引,doc_values
参数是为了加快排序、聚合操作而生的。当建立倒排索引的时候,会额外增加列式存储映射。
doc_values
默认是开启的,如果确定某个字段不需要排序或者不需要聚合,那么可以关闭 doc_values
。
大部分的字段在索引时都会生成 doc_values,除了 text。 text 字段在查询时会生成一个 fielddata 的数据结构,fieldata 在字段首次被聚合、排序的时候生成。
PUT subject
{
"mappings": {
"properties": {
"age": {
"type": "integer"
}
}
}
}
PUT subject/_doc/1
{
"age":100
}
PUT subject/_doc/2
{
"age":99
}
PUT subject/_doc/4
{
"age":101
}
PUT subject/_doc/3
{
"age":98
}
排序查询
GET subject/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"age": {
"order": "desc"
}
}
]
}
查询结果
{
"took" : 29,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 4,
"relation" : "eq"
},
"max_score" : null,
"hits" : [
{
"_index" : "subject",
"_type" : "_doc",
"_id" : "4",
"_score" : null,
"_source" : {
"age" : 101
},
"sort" : [
101
]
},
{
"_index" : "subject",
"_type" : "_doc",
"_id" : "1",
"_score" : null,
"_source" : {
"age" : 100
},
"sort" : [
100
]
},
{
"_index" : "subject",
"_type" : "_doc",
"_id" : "2",
"_score" : null,
"_source" : {
"age" : 99
},
"sort" : [
99
]
},
{
"_index" : "subject",
"_type" : "_doc",
"_id" : "3",
"_score" : null,
"_source" : {
"age" : 98
},
"sort" : [
98
]
}
]
}
}
由于 doc_values 默认时开启的,所以可以直接使用该字段排序,如果想关闭 doc_values ,如下:
PUT subject
{
"mappings": {
"properties": {
"age": {
"type": "integer",
"doc_values": false
}
}
}
}
将doc_values
设置为false后排序查询时就会报错
{
"error" : {
"root_cause" : [
{
"type" : "illegal_argument_exception",
"reason" : "Can't load fielddata on [age] because fielddata is unsupported on fields of type [integer]. Use doc values instead."
}
],
"type" : "search_phase_execution_exception",
"reason" : "all shards failed",
"phase" : "query",
"grouped" : true,
"failed_shards" : [
{
"shard" : 0,
"index" : "subject",
"node" : "2C6CVJeaRAamI41ffmlvzg",
"reason" : {
"type" : "illegal_argument_exception",
"reason" : "Can't load fielddata on [age] because fielddata is unsupported on fields of type [integer]. Use doc values instead."
}
}
],
"caused_by" : {
"type" : "illegal_argument_exception",
"reason" : "Can't load fielddata on [age] because fielddata is unsupported on fields of type [integer]. Use doc values instead.",
"caused_by" : {
"type" : "illegal_argument_exception",
"reason" : "Can't load fielddata on [age] because fielddata is unsupported on fields of type [integer]. Use doc values instead."
}
}
},
"status" : 400
}
doc_values 默认开启,fielddata 默认关闭。
11.17 dynamic
有的时候,如果希望新增字段时,能够抛出异常来提醒开发者,这个可以通过 mappings中 dynamic 属性来配置。
- true,默认即此。自动添加新字段。
- false,忽略新字段。
- strict,严格模式,发现新字段会抛出异常。
11.18 enabled
es 默认会索引所有的字段,但是有的字段可能只需要存储,不需要索引。此时可以通过 enabled 字段来控制:
PUT subject
{
"mappings": {
"properties": {
"age": {
"enabled":false
}
}
}
}
PUT subject/_doc/1
{
"age":100
}
GET subject/_search
{
"query": {
"term": {
"age": {
"value": "100"
}
}
}
}
此时用age来搜索就搜索不到了
11.19 format
日期格式。format 可以规范日期格式,而且一次可以定义多个 format。
PUT subject
{
"mappings": {
"properties": {
"date": {
"type": "date",
"format": "yyyy-MM-dd||yyyy/MM/dd||yyyy/MM/dd HH:mm:ss"
}
}
}
}
PUT subject/_doc/1
{
"date":"2021-06-18"
}
PUT subject/_doc/2
{
"date":"2021/06/18"
}
PUT subject/_doc/3
{
"date":"2021/06/18 16:20:30"
}
多个日期格式之间,使用 || 符号连接,注意没有空格。
如果用户没有指定日期的 format,默认的日期格式是 strict_date_optional_time||epoch_mills
es支持的所有日期格式见官网https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-date-format.html
11.20 ignore_above
igbore_above 用于指定分词和索引的字符串最大长度,超过最大长度的话,该字段将不会被索引,这个字段只适用于 keyword 类型。
PUT subject
{
"mappings": {
"properties": {
"name":{
"type": "keyword",
"ignore_above": 10
}
}
}
}
PUT subject/_doc/2
{
"name":"zhangsanzhangsan"
}
PUT subject/_doc/1
{
"name":"zhangsan"
}
GET subject/_search
{
"query": {
"term": {
"name": {
"value": "zhangsanzhangsan"
}
}
}
}
此时,因为id为2的文档name长度大于10,因此根据name字段查询时会查询不到此文档
11.21 ignore_malformed
ignore_malformed 可以忽略不规则的数据,该参数默认为 false。
PUT subject
{
"mappings": {
"properties": {
"age":{
"type": "integer",
"ignore_malformed": true
}
}
}
}
PUT subject/_doc/2
{
"age":"10"
}
PUT subject/_doc/1
{
"age":"zhangsan"
}
定义索引规则时,如果插入的文档字段类型与预定义不符,默认情况下会报错,但当设置此字段为false时,会插入成功,忽略此类型规则.
11.22 index
PUT subject
{
"mappings": {
"properties": {
"age":{
"type": "integer",
"index": false
}
}
}
}
index 属性指定一个字段是否被索引,该属性为 true 表示字段被索引,false 表示字段不被索引。此时,就不能通过age字段去查询文档了
11.23 index_options
index_options
控制索引时哪些信息被存储到倒排索引中(用在 text
字段中),有四种取值:
index_options | 备注 |
---|---|
docs | 只存储文编号 默认为此 |
freqs | 在docs的基础上 存储词项频率 |
position | 在freqs的基础上 存储词项偏移位置 |
offsets | 在position的基础上 存储词项开始和结束的字符位置 |
11.24 norms
norms 对字段评分有用,text 默认开启 norms,如果不是特别需要,不要开启 norms。
11.25 null_value
PUT subject
{
"mappings": {
"properties": {
"age":{
"type": "integer",
"null_value": 0
}
}
}
}
PUT subject/_doc/2
{
"age":null,
"name":"java"
}
GET subject/_search
{
"query": {
"term": {
"age": {
"value": 0
}
}
}
}
在 es 中,值为 null 的字段不索引也不可以被搜索,null_value
可以让值为 null 的字段显式的可索引、可搜索
11.26 position_increment_gap
被解析的 text 字段会将 term 的位置考虑进去,目的是为了支持近似查询和短语查询,当我们去索引一个含有多个值的 text 字段时,会在各个值之间添加一个假想的空间,将值隔开,这样就可以有效避免一些无意义的短语匹配,间隙大小通过 position_increment_gap 来控制,默认是 100。
PUT subject
{
"mappings": {
"properties": {
"age":{
"type": "text"
}
}
}
}
PUT subject/_doc/2
{
"age":["ceshi","guanjianci"]
}
GET subject/_search
{
"query": {
"match_phrase": {
"age": {
"query": "ceshi guanjianci"
}
}
}
}
此时是搜索不到的 可以通过 slop 指定空隙大小。
GET subject/_search
{
"query": {
"match_phrase": {
"age": {
"query": "ceshi guanjianci",
"slop": 100
}
}
}
}
这样就可以搜索到了 也可以在定义索引的时候,指定空隙:
PUT subject
{
"mappings": {
"properties": {
"age":{
"type": "text",
"position_increment_gap": 0
}
}
}
}
11.27 similarity
similarity
指定文档的评分模型,默认有三种:
similarity | 备注 |
---|---|
BM25 | es 和 lucene默认的评定模型 |
classic | TF/IDF评分 |
boolean | boolean模型评分 |
11.28 store
默认情况下,字段会被索引,也可以搜索,但是不会存储,虽然不会被存储的,但是 _source 中有一个字段的备份。如果想将字段存储下来,可以通过配置 store 来实现。
11.29 fields
fields 参数可以让同一字段有多种不同的索引方式。例如:
PUT subject
{
"mappings": {
"properties": {
"name":{
"type": "text",
"fields": {
"remark":{
"type":"keyword"
}
}
}
}
}
}
GET subject/_search
{
"query": {
"term": {
"name.remark": {
"value": "php"
}
}
}
}
十二. ES文档搜索
首先导入练习数据 数据文件下载
12.1 文档索引(导入)
新建索引,定义索引规则
PUT books
{
"mappings": {
"properties": {
"name":{
"type": "text",
"analyzer": "ik_max_word"
},
"publish":{
"type": "text",
"analyzer": "ik_max_word"
},
"type":{
"type": "text",
"analyzer": "ik_max_word"
},
"author":{
"type": "keyword"
},
"info":{
"type": "text",
"analyzer": "ik_max_word"
},
"price":{
"type": "double"
}
}
}
}
批量导入数据
curl -XPOST "http://localhost:9200/books/_bulk?pretty" -H "content-type:application/json" --data-binary @bookdata.json
12.2 查询全部
搜索分为两个过程:
-
当向索引中保存文档时,默认情况下,es 会保存两份内容,一份是
_source
中的数据,另一份则是通过分词、排序等一系列过程生成的倒排索引文件,倒排索引中保存了词项和文档之间的对应关系。 -
搜索时,当 es 接收到用户的搜索请求之后,就会去倒排索引中查询,通过的倒排索引中维护的倒排记录表找到关键词对应的文档集合,然后对文档进行评分、排序、高亮等处理,处理完成后返回文档
GET books/_search
{
"query": {
"match_all": {}
}
}
默认查询 10 条记录。
12.3 词项查询
12.3.1 term query
GET books/_search
{
"query": {
"term": {
"name": {
"value": "国际"
}
}
},
"min_score":5.2,
"size":10,
"from":0,
"_source": ["name","price"],
"highlight": {
"fields": {
"name": {}
}
}
}
term : 就是根据词去查询,查询指定字段中包含给定单词的文档,term 查询不被解析,只有搜索的词和文档中的词精确匹配,才会返回文档。应用场景如:人名、地名等等。
size : 分页查询参数,每页显示条数
from : 分页偏移量, 从0开始
_source : 返回指定字段
min_score : 只有得分超过最低分的文档才会被查询出来
highlight : 查询关键字高亮显示
12.3.2 terms query
词项查询,但是可以给多个关键词
GET books/_search
{
"query": {
"terms": {
"name":["国家","java"]
}
}
}
12.3.3 range query
范围查询,可以按照日期范围、数字范围等查询。
GET books/_search
{
"query": {
"range": {
"price": {
"gte": 10,
"lte": 20
}
}
},
"sort": [
{
"price": {
"order": "desc"
}
}
]
}
12.3.4 exists query
将该字段非null
的文档返回 (空字符串也非null)
GET books/_search
{
"query": {
"exists": {
"field": "author"
}
}
}
12.3.5 prefix query
给定关键词的前缀去查询:
GET books/_search
{
"query": {
"prefix": {
"author": {
"value": "宋"
}
}
}
}
12.3.6 wildcard query
通配符查询。支持单字符和多字符通配符:
?
表示一个任意字符。*
表示零个或者多个字符。
GET books/_search
{
"query": {
"wildcard": {
"author": {
"value": "宋*"
}
}
}
}
12.3.7 regexp query
支持正则表达式查询。
查询所有姓张并且名字只有两个字的作者的书:
GET books/_search
{
"query": {
"regexp": {
"author": {
"value": "张."
}
}
}
}
12.3.8 fuzzy query
在实际搜索中,有时我们可能会打错字,从而导致搜索不到,在 match query 中,可以通过 fuzziness 属性实现模糊查询。
fuzzy query 返回与搜索关键字相似的文档。怎么样就算相似?以LevenShtein 编辑距离为准。编辑距离是指将一个字符变为另一个字符所需要更改字符的次数,更改主要包括四种:
- 更改字符(javb–>java)
- 删除字符(javva–>java)
- 插入字符(jaa–>java)
- 转置字符(ajva–>java)
为了找到相似的词,模糊查询会在指定的编辑距离中创建搜索关键词的所有可能变化或者扩展的集合,然后进行搜索匹配。
GET books/_search
{
"query": {
"fuzzy": {
"name": {
"value": "jvva"
}
}
}
}
12.3.9 ids query
可以批量查询多个ID集合文档
GET books/_search
{
"query": {
"ids": {
"values": [1,2,3]
}
}
}
12.4 全文查询
12.4.1 match query
GET books/_search
{
"query": {
"match": {
"name": {
"query": "计算机应用",
"operator": "and"
}
}
}
}
match query 会对查询语句进行分词,分词后,如果查询语句中的任何一个词项被匹配,则文档就会被索引到。
这个查询首先会对 计算机应用
进行分词,分词之后,再去查询,只要文档中包含一个分词结果,就回返回文档。换句话说,默认词项之间是 OR
的关系,如果想要修改,也可以改为 AND
。此时就回要求文档中必须同时包含 应用 和 计算机 两个词。
12.4.2 match_phrase query
GET books/_search
{
"query": {
"match_phrase": {
"name": {
"query": "应用计算机",
"slop": 10
}
}
}
}
query 是查询的关键字,会被分词器进行分解,分解之后去倒排索引中进行匹配。
slop 是指关键字之间的最小距离,但是注意不是关键之间间隔的字数。文档中的字段被分词器解析之后,解析出来的词项都包含一个 position 字段表示词项的位置,查询短语分词之后 的 position 之间的间隔要满足 slop 的要求。
match_phrase query 也会对查询的关键字进行分词,但是它分词后有两个特点:
- 分词后的词项顺序必须和文档中词项的顺序一致
- 所有的词都必须出现在文档中
12.4.3 match_phrase_prefix query
这个类似于 match_phrase query,只不过这里多了一个通配符,match_phrase_prefix 支持最后一个词项的前缀匹配,但是由于这种匹配方式效率较低,作为了解即可。
GET books/_search
{
"query": {
"match_phrase_prefix": {
"name": {
"query": "计",
"max_expansions": 10
}
}
}
}
这个查询过程,会自动进行单词匹配,会自动查找以计开始的单词,默认是 50 个,可以通过max_expansions
字段控制
match_phrase_prefix 是针对分片级别的查询,假设 max_expansions 为 1,可能返回多个文档,但是只有一个词,这是我们预期的结果。有的时候实际返回结果和我们预期结果并不一致,原因在于这个查询是分片级别的,不同的分片确实只返回了一个词,但是结果可能来自不同的分片,所以最终会看到多个词。
12.4.4 multi_match query
可以指定多个查询域:
GET books/_search
{
"query": {
"multi_match": {
"query": "学校",
"fields": ["name","info^3"]
}
}
}
这种查询方式还可以指定字段的权重
^3
表示出现在info中的权重是出现在name中的3倍
12.4.5 query_string query
query_string 是一种紧密结合 Lucene 的查询方式,在一个查询语句中可以用到 Lucene 的一些查询语法:
GET books/_search
{
"query": {
"query_string": {
"query": "十一五计算",
"default_field": "name",
"default_operator": "AND"
}
}
}
12.4.6 simple_query_string
GET books/_search
{
"query": {
"simple_query_string": {
"fields": ["name"],
"query": "十一五计算",
"default_operator": "AND"
}
}
}
12.5 复合查询
12.5.1 constant_score query
当我们不关心检索词项的频率(TF)对搜索结果排序的影响时,可以使用 constant_score 将查询语句或者过滤语句包裹起来。
GET books/_search
{
"query": {
"constant_score": {
"filter": {
"term": {
"name": "中国"
}
}
}
}
}
12.5.2 bool query
bool query 可以将任意多个简单查询组装在一起,有四个关键字可供选择,四个关键字所描述的条件可以有一个或者多个。
- must:文档必须匹配 must 选项下的查询条件。
- should:文档可以匹配 should 下的查询条件,也可以不匹配。
- must_not:文档必须不满足 must_not 选项下的查询条件。
- filter:类似于 must,但是 filter 不评分,只是过滤数据。
GET books/_search
{
"query": {
"bool": {
"must": [
{
"term": {
"name": {
"value": "java"
}
}
}
],
"should": [
{"match": {
"info": "程序"
}}
],
"must_not": [
{"range": {
"price": {
"gte": 0,
"lte": 36
}
}}
]
}
}
}
查询 name 属性中必须包含 java,同时书价不在 [0,36] 区间内,info 属性可以包含 程序设计 也可以不包含程序设计
12.5.3 minmum_should_match
GET books/_search
{
"query": {
"match": {
"info": {
"query": "考试中心推出",
"minimum_should_match": 3
}
}
}
}
minmum_should_match
参数在 es 官网上称作最小匹配度。即在查询info
中包含考试中心推出
这个词时,es分词器会对这个词进行分词,例如分为3个词考试 中心 推出,然后会将包含其中任意2(minimum_should_match指定数)个词的文档全部返回,即最小匹配度.
12.5.4 function_score query
场景:例如想要找到附近评分较高的餐厅并倒叙排列,搜索的关键字是餐厅,但是默认的评分策略是没有办法考虑到餐厅评分的,他只是考虑搜索关键字的相关性,这个时候可以通过 function_score query 来实现。
首先准备测试数据
PUT content
{
"mappings": {
"properties": {
"title":{
"type": "text",
"analyzer": "ik_max_word"
},
"votes":{
"type": "double"
}
}
}
}
PUT content/_doc/1
{
"title":"es从入门到精通之es学习笔记",
"votes":10.0
}
PUT content/_doc/2
{
"title":"es从入门到精通",
"votes":100.0
}
此时如果按es 关键字搜索的话,id
为1的文档肯定会出现在最上面,因为该文档title
中包含了2个es,所以相关性更高,而我们想要的预期结果为votes为100的在最上面.
具体的思路,就是在旧的得分基础上,根据 votes
的数值进行综合运算,重新得出一个新的评分。
具体有几种不同的计算方式:
- weight 在
_score
基础上乘以weight的值- random_score 会根据 uid 字段进行 hash 运算,生成分数,使用 random_score 时可以配置一个种子,如果不配置,默认使用当前时间。
- script_score 可以指定自定义评分脚本。
- field_value_factor 这个的功能类似于 script_score,但是不用自己写脚本。
例如 : 使用new_score
=old_score
+votes
GET content/_search
{
"query": {
"function_score": {
"query": {
"match": {
"title": "es"
}
},
"functions": [
{
"script_score": {
"script": "_score + doc['votes'].value"
}
}
],
"boost_mode": "replace"
}
}
}
通过 boost_mode
参数,可以设置最终的计算方式。该参数还有其他取值:
- multiply:分数相乘
- sum:分数相加
- avg:求平均数
- max:最大分
- min:最小分
- replace:不进行二次计算
boost_mode
该参数表示在function中指定函数计算结果new_score
的基础上在与old_score
再做相关的运算后返回
12.5.5 field_value_factor
这个的功能类似于 script_score
,但是不用自己写脚本。
GET content/_search
{
"query": {
"function_score": {
"query": {"match": {
"title": "es"
}},
"functions": [
{
"field_value_factor": {
"field": "votes"
}
}
]
}
}
}
默认的得分就是old_score*votes。
12.5.6 boosting query
表示查询name
字段中包含java
的文档,且如果name
字段中包含2008则将包含的此文档的_source
在原来的基础上乘以negative_boost
指定的值
GET books/_search
{
"query": {
"boosting": {
"positive": {
"match": {
"name": "java"
}
},
"negative": {
"match": {
"name": "2008"
}
},
"negative_boost": 0.5
}
}
}
boosting query 中包含三部分:
- positive:得分不变
- negative:降低得分
- negative_boost:降低的权重
12.6 联合查询
12.6.1 嵌套文档查询
准备测试数据
PUT school
{
"mappings": {
"properties": {
"student": {
"type": "nested"
}
}
}
}
PUT school/_doc/1
{
"class":"一班",
"student": [
{
"name": "zhang san",
"age": 18
},
{
"name": "li si",
"age": 20
}
]
}
查询
GET school/_search
{
"query": {
"nested": {
"path": "student",
"query": {
"bool": {
"must": [
{
"match": {
"student.name": "li san"
}
},
{
"match": {
"student.age": "18"
}
}
]
}
}
}
}
}
发现即是这样搜索也是能搜到的,失去了对象属性间的映射关系,因此推荐定义父子关系查询
12.6.2 父子文档
相比于嵌套文档,父子文档主要有如下优势:
- 更新父文档时,不会重新索引子文档
- 创建、修改或者删除父子文档时,不会影响父文档或者其他的子文档。
- 子文档可以作为搜索结果独立返回。
例如学生和班级的关系:
PUT school
{
"mappings": {
"properties": {
"name": {
"type": "keyword"
},
"c_s": {
"type": "join",
"relations": {
"class": "student"
}
}
}
}
}
c_s
表示父子文档关系的名字,可以自定义。join
表示这是一个父子文档。relations
里边,class
这个位置是 parent,student
这个位置是 child。
接下来,插入两个父文档:
PUT school/_doc/1
{
"name":"一班",
"c_s":{
"name":"class"
}
}
PUT school/_doc/2
{
"name":"二班",
"c_s":{
"name":"class"
}
}
再来添加三个子文档:
PUT school/_doc/3?routing=1
{
"name":"张三",
"c_s":{
"name":"student",
"parent":1
}
}
PUT school/_doc/4?routing=2
{
"name":"李四",
"c_s":{
"name":"student",
"parent":2
}
}
PUT school/_doc/5?routing=2
{
"name":"王五",
"c_s":{
"name":"student",
"parent":2
}
}
子文档都是独立的文档。特别需要注意的地方是,子文档需要和父文档在同一个分片上,所以 routing 关键字的值为父文档的 id。另外,name属性的值为索引定义中child的值
定义父子文档索引时需要注意的地方:
- 每个索引只能定义一个 join filed
- 父子文档需要在同一个分片上(查询,修改需要routing)
- 可以向一个已经存在的 join filed 上新增关系
12.6.3 has_child query
通过子文档查询父文档
GET school/_search
{
"query": {
"has_child": {
"type": "student",
"query": {
"match": {
"name": "张三"
}
}
}
}
}
12.6.4 has_parent query
通过父文档查询子文档:
GET school/_search
{
"query": {
"has_parent": {
"parent_type": "class",
"query": {
"match": {
"name": "二班"
}
}
}
}
}
也可以使用 parent_id 查询子文档:
GET school/_search
{
"query": {
"parent_id":{
"type":"student",
"id":2
}
}
}
通过测试发现:
- 普通子对象实现一对多,会损失子文档的边界,子对象之间的属性关系丢失。
- nested 可以解决第 1 点的问题,但是 nested 有两个缺点:更新主文档的时候要全部更新,不支持子文档属于多个主文档。
- 父子文档解决 1、2 点的问题,但是它主要适用于写多读少的场景。
12.7 地理位置查询
准备数据
PUT geo
{
"mappings": {
"properties": {
"name":{
"type": "keyword"
},
"location":{
"type": "geo_point"
}
}
}
}
准备一个 geo.json 文件:
{"index":{"_index":"geo","_id":1}}
{"name":"西安","location":"34.288991865037524,108.9404296875"}
{"index":{"_index":"geo","_id":2}}
{"name":"北京","location":"39.926588421909436,116.43310546875"}
{"index":{"_index":"geo","_id":3}}
{"name":"上海","location":"31.240985378021307,121.53076171875"}
{"index":{"_index":"geo","_id":4}}
{"name":"天津","location":"39.13006024213511,117.20214843749999"}
{"index":{"_index":"geo","_id":5}}
{"name":"杭州","location":"30.259067203213018,120.21240234375001"}
{"index":{"_index":"geo","_id":6}}
{"name":"武汉","location":"30.581179257386985,114.3017578125"}
{"index":{"_index":"geo","_id":7}}
{"name":"合肥","location":"31.840232667909365,117.20214843749999"}
{"index":{"_index":"geo","_id":8}}
{"name":"重庆","location":"29.592565403314087,106.5673828125"}
执行如下命令,批量导入 geo.json 数据:
curl -XPOST "http://localhost:9200/geo/_bulk?pretty" -H "content-type:application/json" --data-binary @geo.json
12.7.1 geo_distance query
给出一个中心点,查询距离该中心点指定范围内的文档:
GET geo/_search
{
"query": {
"bool": {
"filter": [
{
"geo_distance": {
"distance": "700km",
"location": {
"lat": 34.288991865037524,
"lon": 108.9404296875
}
}
}
]
}
}
}
查询西安周边700km的城市
{
"took" : 1,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 3,
"relation" : "eq"
},
"max_score" : 0.0,
"hits" : [
{
"_index" : "geo",
"_type" : "_doc",
"_id" : "1",
"_score" : 0.0,
"_source" : {
"name" : "西安",
"location" : "34.288991865037524,108.9404296875"
}
},
{
"_index" : "geo",
"_type" : "_doc",
"_id" : "6",
"_score" : 0.0,
"_source" : {
"name" : "武汉",
"location" : "30.581179257386985,114.3017578125"
}
},
{
"_index" : "geo",
"_type" : "_doc",
"_id" : "8",
"_score" : 0.0,
"_source" : {
"name" : "重庆",
"location" : "29.592565403314087,106.5673828125"
}
}
]
}
}
12.7.2 geo_bounding_box query
在某一个矩形内的点,通过两个点锁定一个矩形
GET geo/_search
{
"query": {
"bool": {
"filter": [
{
"geo_bounding_box": {
"location": {
"top_left": {
"lat": 40.84706035607122,
"lon": 111.62109375
},
"bottom_right": {
"lat": 39.30029918615029,
"lon": 119.53125
}
}
}
}
]
}
}
}
以[40.84706035607122,111.62109375]
经纬度作为矩形的左上角,以[39.30029918615029,119.53125]
经纬度作为矩形的右下角,构造出来的矩形中,包含上海和杭州两个城市。
12.7.3 geo_polygon query
在某一个多边形范围内的查询。
GET geo/_search
{
"query": {
"bool": {
"filter": [
{
"geo_polygon": {
"location": {
"points": [
{
"lat":33.99802726234877,
"lon":108.45703125
},
{
"lat":33.99802726234877,
"lon": 109.3798828125
},
{
"lat":34.66935854524543,
"lon": 109.3798828125
},
{
"lat":34.66935854524543,
"lon": 108.45703125
}
]
}
}
}
]
}
}
}
给定多个点,查询由多个点组成的多边形中的数据。
12.7.4 geo_shape query
geo_shape
用来查询图形,针对 geo_shape
,两个图形之间的关系有:相交、包含、不相交。
准备数据
PUT geo_shape
{
"mappings": {
"properties": {
"name":{
"type": "keyword"
},
"location":{
"type": "geo_shape"
}
}
}
}
然后添加一条线:
PUT geo_shape/_doc/1
{
"name": "宝鸡->商洛",
"location": {
"type": "linestring",
"coordinates": [
[
108.21533203125,
34.45221847282654
],
[
109.88525390624999,
34.03445260967645
]
]
}
}
接下来查询某一个图形中是否与该线相交:
GET geo_shape/_search
{
"query": {
"bool": {
"filter": [
{
"geo_shape": {
"location": {
"shape": {
"type": "envelope",
"coordinates": [
[
109.7314453125,
35.11990857099681
],
[
108.369140625,
33.706062655101206
]
]
},
"relation": "intersects"
}
}
}
]
}
}
}
relation 属性表示两个图形的关系:
- within 包含
- intersects 相交
- disjoint 不相交
12.8 特殊查询
12.8.1 more_like_this query
more_like_this query
可以实现基于内容的推荐,给定一篇文章,可以查询出和该文章相似的内容。
GET books/_search
{
"query": {
"more_like_this": {
"fields": [
"info"
],
"like": "国家",
"min_term_freq": 1,
"max_query_terms": 12
}
}
}
- fields:要匹配的字段,可以有多个
- like:要匹配的文本
- min_term_freq:词项的最低频率,默认是 2。特别注意,这个是指词项在要匹配的文本中的频率,而不是 es 文档中的频率
- max_query_terms:query 中包含的最大词项数目
- min_doc_freq:最小的文档频率,搜索的词,至少在多少个文档中出现,少于指定数目,该词会被忽略
- max_doc_freq:最大文档频率
- analyzer:分词器,默认使用字段的分词器
- stop_words:停用词列表
12.8.2 script query
脚本查询,例如查询所有价格大于 100 的图书:
GET books/_search
{
"query": {
"bool": {
"filter": [
{"script": {
"script": {
"lang": "painless",
"source": "if(doc['price'].size()>0){doc['price'].value>100}"
}
}}
]
}
}
}
12.8.3 percolate query
percolate query 译作渗透查询或者反向查询。
- 正常操作:根据查询语句找到对应的文档 query->document
- percolate query:根据文档,返回与之匹配的查询语句,document->query
应用场景:
- 价格监控
- 库存报警
- 股票警告
例如阈值告警,假设指定字段值大于阈值,报警提示。
percolate mapping 定义:
PUT log
{
"mappings": {
"properties": {
"threshold":{
"type": "long"
},
"count":{
"type": "long"
},
"query":{
"type":"percolator"
}
}
}
}
percolator 类型相当于 keyword、long 以及 integer 等。
插入文档
PUT log/_doc/1
{
"threshold":10,
"query":{
"bool":{
"must":{
"range":{
"count":{
"gt":2
}
}
}
}
}
}
查询
{
"query": {
"percolate": {
"field": "query",
"documents": [
{
"count":3
},
{
"count":6
},
{
"count":90
},
{
"count":12
},
{
"count":15
}
]
}
}
}
查询结果如下
{
"took" : 16,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 1,
"relation" : "eq"
},
"max_score" : 1.0,
"hits" : [
{
"_index" : "log",
"_type" : "_doc",
"_id" : "1",
"_score" : 1.0,
"_source" : {
"threshold" : 10,
"query" : {
"bool" : {
"must" : {
"range" : {
"count" : {
"gt" : 2
}
}
}
}
}
},
"fields" : {
"_percolator_document_slot" : [
0,
1,
2,
3,
4
]
}
}
]
}
}
查询结果中会列出count大于2的文档。
查询结果中的 _percolator_document_slot
字段表示文档的 下标,从 0 开始计
12.9 搜索高亮与排序
12.9.1 高亮
通过给搜索关键字添加自定义高亮标签:
GET books/_search
{
"query": {
"match": {
"name": "java"
}
},
"highlight": {
"require_field_match": "false",
"fields": {
"name": {
"pre_tags": ["<font color='red'>"],
"post_tags": ["</font>"]
},
"info": {
"pre_tags": ["<font color='red'>"],
"post_tags": ["</font>"]
}
}
}
}
12.9.2 排序
es默认是按照查询文档的相关度来排序的,即(_score 字段),match_all
查询只是返回所有文档,不评分,默认按照添加顺序返回,可以通过 _doc 字段对其进行排序:
GET books/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"_doc": {
"order": "desc"
}
}
]
}
es 同时也支持多字段排序 :
GET books/_search
{
"query": {
"match": {
"name": "学习"
}
},
"sort": [
{
"price": {
"order": "asc"
}
},
{
"_id": {
"order": "desc"
}
}
],
"size": 20
}
十三. ES指标聚合
13.1 Max Aggregation
统计最大值。
GET books/_search
{
"aggs": {
"max_price": {
"max": {
"field": "price"
}
}
}
}
结果如下
GET books/_search
{
"aggs": {
"max_price": {
"max": {
"field": "price",
"missing": 300
}
}
}
}
如果某个文档中缺少 price 字段,则设置该字段的值为 300。
也可以通过脚本查询
GET books/_search
{
"aggs": {
"max_price": {
"max": {
"script": {
"source": "if(doc.price.size()!=0){doc.price.value}"
}
}
}
}
}
13.2 Min Aggregation
统计最小值
GET books/_search
{
"aggs": {
"min_price": {
"min": {
"field": "price"
}
}
}
}
13.3 Avg Aggregation
统计平均值:
GET books/_search
{
"aggs": {
"min_price": {
"avg": {
"field": "price"
}
}
}
}
13.4 Sum Aggregation
求和 :
GET books/_search
{
"aggs": {
"min_price": {
"avg": {
"field": "price"
}
}
}
}
13.5 Cardinality Aggregation
cardinality aggregation 用于基数统计。类似于 SQL 中的 distinct count(0)
(先去重再统计个数):
text
类型是分析型类型,默认是不允许进行聚合操作的,如果相对text
类型进行聚合操作,需要设置其 fielddata
属性为 true,这种方式虽然可以使 text 类型进行聚合操作,但是无法满足精准聚合,如果需要精准聚合,可以设置字段的类型为 keyword
。
GET books/_search
{
"aggs": {
"publish_count": {
"cardinality": {
"field": "author"
}
}
}
}
13.6 Stats Aggregation
基本统计,一次性返回 count、max、min、avg、sum:
GET books/_search
{
"aggs": {
"agg_status": {
"stats": {
"field": "price"
}
}
}
}
13.7 Extends Stats Aggregation
高级统计,比 stats 多出来:平方和、方差、标准差、平均值加减两个标准差的区间:
GET books/_search
{
"aggs": {
"extends_status": {
"extended_stats": {
"field": "price"
}
}
}
}
13.8 Percentiles Aggregation
百分位统计。统计price
字段的值在各个阶段的百分数
GET books/_search
{
"aggs": {
"per": {
"percentiles": {
"field": "price",
"percents": [
0,
25,
50,
75,
100
]
}
}
}
}
例如 :price
低于18的文档占总文档数的25%左右, 所有的文档价格都低于269
13.9 Value Count Aggregation
可以按照字段统计文档数量(包含指定字段的文档数量):
GET books/_search
{
"aggs": {
"field_count": {
"value_count": {
"field": "price"
}
}
}
}
十四. ES桶聚合
14.1 Terms Aggregation
Terms Aggregation 用于分组聚合,例如,统计各个出版社出版的图书总数量:
GET books/_search
{
"aggs": {
"publish_count": {
"terms": {
"field": "publish",
"size": 10
}
}
},
"size": 0
}
在 terms 分桶的基础上,还可以对每个桶进行指标聚合。
统计不同出版社所出版的图书的平均价格:
GET books/_search
{
"aggs": {
"publish_count": {
"terms": {
"field": "publish",
"size": 10
},
"aggs": {
"price_avg": {
"avg": {
"field": "price"
}
}
}
}
},
"size": 0
}
14.2 Filter Aggregation
过滤器聚合。可以将符合过滤器中条件的文档分到一个桶中,然后可以求其平均值。
例如查询书名中包含 java 的图书的平均价格:
GET books/_search
{
"aggs": {
"java_count": {
"filter": {
"term": {
"name": "java"
}
},
"aggs": {
"avg_price": {
"avg": {
"field": "price"
}
}
}
}
},
"size": 0
}
14.3 Filters Aggregation
多过滤器聚合。过滤条件可以有多个。
例如查询书名中包含 java 或者 国际 的图书的平均价格:
GET books/_search
{
"aggs": {
"java_count": {
"filters": {
"filters": [
{
"term":{
"name":"java"
}
},
{
"term":{
"name":"国际"
}
}
]
},
"aggs": {
"NAME": {
"avg": {
"field": "price"
}
}
}
}
},
"size": 0
}
14.4 Range Aggregation
按照范围聚合,在某一个范围内的文档数统计。
例如统计图书价格在 0-50、50-100、100-150、150以上的图书数量:
GET books/_search
{ "size": 0,
"aggs": {
"NAME": {
"range": {
"field": "price",
"ranges": [
{
"from": 0,
"to": 50
},
{
"from": 50,
"to": 100
},
{
"from": 100,
"to": 150
},
{
"from": 150,
"to": 200
}
]
}
}
}
}
14.5 Date Range Aggregation
准备数据
PUT clock/_doc/1
{
"name":"春天",
"date":"2018-02-11"
}
PUT clock/_doc/2
{
"name":"夏天",
"date":"2019-03-11"
}
PUT clock/_doc/3
{
"name":"秋天",
"date":"2020-03-11"
}
PUT clock/_doc/4
{
"name":"冬天",
"date":"2021-03-11"
}
统计一年前到现在的数量:
GET clock/_search
{
"size": 0,
"aggs": {
"NAME": {
"date_range": {
"field": "date",
"ranges": [
{
"from": "now-1y/y",
"to": "now"
}
]
}
}
}
}
- 12M/M 表示 12 个月。
- 1y/y 表示 1年。
- d 表示天
14.6 Date Histogram Aggregation
时间直方图聚合。
例如统计各个年份的文档
GET clock/_search
{
"size": 0,
"aggs": {
"NAME": {
"date_histogram": {
"field": "date",
"calendar_interval": "year"
}
}
}
}
14.7 Missing Aggregation
空值聚合。
统计没有 price 字段的文档:
GET books/_search
{
"size": 0,
"aggs": {
"NAME": {
"missing": {
"field": "price"
}
}
}
}
14.8 Children Aggregation
可以根据父子文档关系进行分桶。
查询子类型为 student 的文档数量:
GET school/_search
{
"size": 0,
"aggs": {
"NAME": {
"children": {
"type": "student"
}
}
}
}
14.9 Geo Distance Aggregation
对地理位置数据做统计。
例如查询(34.288991865037524,108.9404296875)坐标方圆 600KM内和 超过 600KM-1000KM 的城市数量。
GET geo/_search
{"size": 0,
"aggs": {
"NAME": {
"geo_distance": {
"field": "location",
"origin": "34.288991865037524,108.9404296875",
"unit": "km",
"ranges": [
{
"from": 0,
"to": 600
},
{
"from": 600,
"to": 1000
}
]
}
}
}
}
14.10 IP Range Aggregation
根据IP 地址范围查询 (from->to 为前闭后开区间)
GET address/_search
{
"aggs": {
"NAME": {
"ip_range": {
"field": "ip",
"ranges": [
{
"from": "127.0.0.1",
"to": "127.0.0.6"
}
]
}
}
}
}
十五. 管道聚合
管道聚合相当于在之前聚合的基础上,再次聚合。
15.1 Avg Bucket Aggregation
计算聚合平均值。例如,统计每个出版社所出版图书的平均值,然后再统计所有出版社出版图书的平均值:
GET books/_search
{
"size": 0,
"aggs": {
"avg_count": {
"terms": {
"field": "publish",
"size": 2
},
"aggs": {
"avg_price": {
"avg": {
"field": "price"
}
}
}
},
"avg_book":{
"avg_bucket": {
"buckets_path": "avg_count.avg_price"
}
}
}
}
15.2 Max Bucket Aggregation
统计每个出版社所出版图书的平均值,然后再统计平均值中的最大值:
GET books/_search
{
"size": 0,
"aggs": {
"avg_count": {
"terms": {
"field": "publish",
"size": 2
},
"aggs": {
"avg_price": {
"avg": {
"field": "price"
}
}
}
},
"avg_book":{
"max_bucket": {
"buckets_path": "avg_count.avg_price"
}
}
}
}
15.3 Min Bucket Aggregation
统计每个出版社所出版图书的平均值,然后再统计平均值中的最小值:
GET books/_search
{
"size": 0,
"aggs": {
"avg_count": {
"terms": {
"field": "publish",
"size": 2
},
"aggs": {
"avg_price": {
"avg": {
"field": "price"
}
}
}
},
"avg_book":{
"min_bucket": {
"buckets_path": "avg_count.avg_price"
}
}
}
}
15.4 Sum Bucket Aggregation
统计每个出版社所出版图书的平均值,然后再统计平均值之和:
GET books/_search
{
"size": 0,
"aggs": {
"avg_count": {
"terms": {
"field": "publish",
"size": 2
},
"aggs": {
"avg_price": {
"avg": {
"field": "price"
}
}
}
},
"avg_book":{
"sum_bucket": {
"buckets_path": "avg_count.avg_price"
}
}
}
}
15.5 Stats Bucket Aggregation
统计每个出版社所出版图书的平均值,然后再统计平均值的各种数据:
GET books/_search
{
"size": 0,
"aggs": {
"avg_count": {
"terms": {
"field": "publish",
"size": 2
},
"aggs": {
"avg_price": {
"avg": {
"field": "price"
}
}
}
},
"avg_book":{
"stats_bucket": {
"buckets_path": "avg_count.avg_price"
}
}
}
}
15.6 Extended Stats Bucket Aggregation
15.7 Percentiles Bucket Aggregation
十六. Java操作ES
Java 操作 Es 的方案:
-
直接使用 HTTP 请求
直接使用 HTTP 请求,去操作 Es。HTTP 请求工具,可以使用 Java 自带的HttpUrlConnection,也可以使用一些 HTTP 请求库,例如 HttpClient、OKHttp、Spring 中的RestTemplate 都可以。
这种方式有一个弊端,就是要自己组装请求参数,自己去解析响应的 JSON。
-
Low Level REST Client
用于 Es 的官方的低级客户端。这种方式允许通过 HTTP 与 Es 集群进行通信,但是请求时候的 JSON 参数和响应的JSON 参数交给用户去处理。这种方式好处就是兼容所有的 Es 版本。但是就是数据处理比较麻烦。
-
High Level REST Client
用户 Es 的官方的高级客户端。这种方式允许通过 HTTP 与 Es 集群进行通信,它是基于 Low Level REST Client,但是提供了很多 API,开发者不需要自己去组装参数,也不需要自己去解析响应 JSON 。这种方式使用起来更加直接。但是需要注意,这种方式,所使用的依赖库的版本要和 Es 对应。
-
TransportClient
TransportClient 在 Es7 中已经被弃用,在 Es8 中将被完全删除。
16.1 发送http请求操作es
public class OriginalEsTest {
public static void main(String[] args) throws IOException {
URL url=new URL("http://localhost:9200/books/_search?pretty=true");
HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
if (urlConnection.getResponseCode()==200){
BufferedReader reader = new BufferedReader(new InputStreamReader(urlConnection.getInputStream()));
String str=null;
while ((str=reader.readLine())!=null){
System.out.println(str);
}
reader.close();
}
}
}
16.2 Low Level REST Client
添加maven依赖
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-client</artifactId>
<version>7.9.3</version>
</dependency>
请求调用
public class LowLevelEsTest {
public static void main(String[] args) throws IOException {
//1.构建一个 RestClient 对象
RestClientBuilder builder = RestClient.builder(
new HttpHost("localhost", 9200, "http")
);
//2.如果需要在请求头中设置认证信息等,可以通过 builder 来设置
//builder.setDefaultHeaders(new Header[]{new BasicHeader("key","value")});
final RestClient restClient = builder.build();
//3.构建请求
Request request = new Request("GET", "/books/_search");
//添加请求参数
request.addParameter("pretty","true");
//添加请求体json参数
request.setEntity(new NStringEntity("{\"size\": 10}", ContentType.APPLICATION_JSON));
//4.发起请求,发起请求有两种方式,可以同步,可以异步
//restClient.performRequest(request);
//异步请求
restClient.performRequestAsync(request, new ResponseListener() {
//请求成功的回调
@Override
public void onSuccess(Response response) {
//5.解析 response,获取响应结果
try {
BufferedReader br = new BufferedReader(new InputStreamReader(response.getEntity().getContent()));
String str = null;
while ((str = br.readLine()) != null) {
System.out.println(str);
}
br.close();
//最后记得关闭 RestClient
restClient.close();
} catch (IOException e) {
e.printStackTrace();
}
}
//请求失败的回调
@Override
public void onFailure(Exception e) {
System.out.println(e.getMessage());
}
});
System.out.println("主线程执行...");
}
}
16.3 High Level REST Client
引入依赖
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>7.9.3</version>
</dependency>
请求调用
public class HighLevelEsTest {
public static void main(String[] args) throws IOException {
RestHighLevelClient client = new RestHighLevelClient(RestClient.builder(new HttpHost("localhost", 9200, "http")));
//删除已经存在的索引
DeleteIndexRequest deleteIndexRequest = new DeleteIndexRequest("user");
// client.indices().delete(deleteIndexRequest,RequestOptions.DEFAULT);
//新建索引
CreateIndexRequest request = new CreateIndexRequest("user");
//配置 settings,分片、副本等信息
request.settings(Settings.builder().put("index.number_of_shards", 2).put("index.number_of_replicas", 1));
//配置字段类型,字段类型可以通过 JSON 字符串、Map 以及 XContentBuilder 三种方式来构建
//1.json 字符串的方式
//request.mapping("{\"properties\":{\"name\":{\"type\":\"keyword\"},\"age\":{\"type\": \"integer\"}}}", XContentType.JSON);
//执行请求,创建索引
//2.map字符串的方式
HashMap<String, Object> ageTypeMap = new HashMap<String, Object>();
ageTypeMap.put("type", "integer");
HashMap<String, Object> ageMap = new HashMap<String, Object>();
ageMap.put("age", ageTypeMap);
HashMap<String, Object> propertiesMap = new HashMap<String, Object>();
propertiesMap.put("properties", ageMap);
//3.通过XContentBuilder
XContentBuilder xContentBuilder = XContentFactory.jsonBuilder();
xContentBuilder.startObject();
xContentBuilder.startObject("properties");
xContentBuilder.startObject("age");
xContentBuilder.field("type","integer");
xContentBuilder.endObject();
xContentBuilder.endObject();
xContentBuilder.endObject();
//索引别名
request.alias(new Alias("user_alias"));
request.mapping(xContentBuilder);
client.indices().create(request, RequestOptions.DEFAULT);
//关闭连接
client.close();
}
}
直接封装json参数进行调用
public class HighLevelEs2JsonTest {
public static void main(String[] args) throws IOException {
final RestHighLevelClient client = new RestHighLevelClient(RestClient.builder(new HttpHost("localhost", 9200, "http")));
//新建索引
CreateIndexRequest request = new CreateIndexRequest("user");
request.source(
"{\n" +
" \"settings\": {\n" +
" \"index.number_of_shards\":2,\n" +
" \"index.number_of_replicas\":1\n" +
" } ,\n" +
" \"mappings\": {\n" +
" \"properties\": {\n" +
" \"name\":{\n" +
" \"type\": \"keyword\"\n" +
" },\n" +
" \"age\":{\n" +
" \"type\": \"integer\"\n" +
" }\n" +
" }\n" +
" }\n" +
"}", XContentType.JSON);
//同步调用 会阻塞
//client.indices().create(request, RequestOptions.DEFAULT);
//异步调用
client.indices().createAsync(request, RequestOptions.DEFAULT, new ActionListener<CreateIndexResponse>() {
@Override
public void onResponse(CreateIndexResponse createIndexResponse) {
//关闭连接
try {
client.close();
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void onFailure(Exception e) {
System.out.println(e.getMessage());
}
});
}
}
索引别名相关操作
public class HighLevelEs2AliasTest {
public static void main(String[] args) throws IOException {
final RestHighLevelClient client = new RestHighLevelClient(RestClient.builder(new HttpHost("localhost", 9200, "http")));
//1.查看索引是否存在
GetIndexRequest getIndexRequest=new GetIndexRequest("user1");
boolean exists = client.indices().exists(getIndexRequest, RequestOptions.DEFAULT);
System.out.println("user1索引是否存在 = " + exists+"\r\n"+"---------------");
//2.关闭索引
CloseIndexRequest closeIndexRequest=new CloseIndexRequest("user");
CloseIndexResponse close = client.indices().close(closeIndexRequest, RequestOptions.DEFAULT);
List<CloseIndexResponse.IndexResult> list = close.getIndices();
for (CloseIndexResponse.IndexResult result : list) {
System.out.println("关闭的索引有 = "+result.getIndex()+"\r\n"+"---------------");
}
//3.打开索引
OpenIndexRequest openIndexRequest=new OpenIndexRequest("user");
client.indices().open(openIndexRequest, RequestOptions.DEFAULT);
//4. 修改索引
UpdateSettingsRequest settingsRequest=new UpdateSettingsRequest("user");
settingsRequest.settings(Settings.builder().put("index.blocks.write",true).build());
client.indices().putSettings(settingsRequest,RequestOptions.DEFAULT);
//5. 克隆索引 被克隆的索引必须为只读索引 否则不可克隆
//ResizeRequest resizeRequest=new ResizeRequest("user1","user");
//client.indices().clone(resizeRequest,RequestOptions.DEFAULT);
//6. 查看索引信息
GetSettingsRequest request = new GetSettingsRequest().indices("school");
//设置需要的具体的参数,不设置则返回所有参数
request.names("index.uuid");
GetSettingsResponse response = client.indices().getSettings(request, RequestOptions.DEFAULT);
ImmutableOpenMap<String, Settings> indexToSettings = response.getIndexToSettings();
System.out.println(indexToSettings);
String s = response.getSetting("school", "index.uuid");
System.out.println(s);
//7. 添加/删除别名
IndicesAliasesRequest indicesAliasesRequest = new IndicesAliasesRequest();
IndicesAliasesRequest.AliasActions aliasAction = new IndicesAliasesRequest.AliasActions(IndicesAliasesRequest.AliasActions.Type.ADD);
aliasAction.index("books").alias("books_alias");
//添加一个过滤数据后的别名 当查看这个别名时 查看的是过滤后的数据 类似于mysql中的视图
//aliasAction.index("books").alias("books_alias2").filter("{\"term\": {\"name\": \"java\"}}");
indicesAliasesRequest.addAliasAction(aliasAction);
client.indices().updateAliases(indicesAliasesRequest, RequestOptions.DEFAULT);
//8.删除别名
DeleteAliasRequest deleteAliasRequest = new DeleteAliasRequest("books", "books_alias");
client.indices().deleteAlias(deleteAliasRequest, RequestOptions.DEFAULT);
//9. 获取别名
GetAliasesRequest booksAlias = new GetAliasesRequest("books_alias2");
//指定查看某一个索引的别名,不指定,则会搜索所有的别名
//booksAlias.indices("books");
boolean b = client.indices().existsAlias(booksAlias, RequestOptions.DEFAULT);
System.out.println(b);
//查看别名
GetAliasesResponse response1 = client.indices().getAlias(booksAlias, RequestOptions.DEFAULT);
Map<String, Set<AliasMetadata>> aliases = response1.getAliases();
System.out.println("aliases = " + aliases);
client.close();
}
}
文档相关操作
public class HighLevelEs2DocTest {
public static void main(String[] args) throws IOException {
RestHighLevelClient client = new RestHighLevelClient(RestClient.builder(new HttpHost("localhost", 9200, "http")));
IndexRequest indexRequest = new IndexRequest("user");
indexRequest.source("{\"name\": \"张三\",\"age\": 10}", XContentType.JSON).id("1");
//可以指定文档的添加/更新操作 如果不指定走es默认操作
//indexRequest.opType(DocWriteRequest.OpType.CREATE);
IndexResponse response = client.index(indexRequest, RequestOptions.DEFAULT);
System.out.println("索引为 = " + response.getIndex());
System.out.println("文档ID为:" + response.getId());
DocWriteResponse.Result result = response.getResult();
if (result.equals(DocWriteResponse.Result.CREATED)) {
System.out.println("文档保存成功");
}
//判断文档是否更新成功(如果 id 已经存在)
if (result.equals(DocWriteResponse.Result.UPDATED)) {
System.out.println("文档更新成功");
}
//判断分片操作是否都成功
ReplicationResponse.ShardInfo shardInfo = response.getShardInfo();
if (shardInfo.getTotal()!=shardInfo.getSuccessful()){
System.out.println("有分片未保存成功");
}
//有存在失败的分片
if (shardInfo.getFailed()>0){
ReplicationResponse.ShardInfo.Failure[] failures = shardInfo.getFailures();
//打印错误信息
Arrays.stream(failures).forEach(x-> System.out.println(x.reason()));
}
//查看文档
GetRequest getRequest=new GetRequest("books","1");
GetResponse getResponse = client.get(getRequest, RequestOptions.DEFAULT);
//如果文档存在
if (getResponse.isExists()){
System.out.println("getResponse.getId() = " + getResponse.getId());
System.out.println("getResponse.getIndex() = " + getResponse.getIndex());
System.out.println("getResponse.getVersion() = " + getResponse.getVersion());
System.out.println("getResponse.getSource() = " + getResponse.getSource());
}else {
System.out.println("文档不存在");
}
//删除文档
DeleteRequest deleteRequest=new DeleteRequest("books","1");
DeleteResponse deleteResponse = client.delete(deleteRequest, RequestOptions.DEFAULT);
if (deleteResponse.getResult().equals(DocWriteResponse.Result.DELETED)){
System.out.println("删除成功");
System.out.println("deleteResponse.getResult() = " + deleteResponse.getResult().getLowercase());
}else {
System.out.println("删除失败");
}
//更新文档
UpdateRequest updateRequest=new UpdateRequest("books","0");
//通过json更新
updateRequest.docAsUpsert(true); //为true时,如果索引中未找打id为0的文档 则进行插入文档
updateRequest.doc("{\"name\": \"数文\",\"type\": \"小学\"}",XContentType.JSON);
//通过map更新
//updateRequest.doc(Collections.singletonMap("name","数学"));
//更新未成功 则插入文档
//updateRequest.upsert("{\"name\": \"语文\",\"type\": \"小学\"}",XContentType.JSON);
UpdateResponse update = client.update(updateRequest, RequestOptions.DEFAULT);
if (update.getResult().equals(DocWriteResponse.Result.UPDATED)){
System.out.println("update.getVersion() = " + update.getVersion());
System.out.println("update.getResult().getLowercase() = " + update.getResult().getLowercase());
}else if (update.getResult().equals(DocWriteResponse.Result.CREATED)){
System.out.println("插入成功");
}else {
System.out.println("更新失败");
}
//搜索文档
SearchRequest request = Requests.searchRequest("user");
SearchSourceBuilder builder = new SearchSourceBuilder();
//过滤条件
builder.query(QueryBuilders.termsQuery("name", "zhangsan", "lisi"));
//需要查询的字段
String[] includeFields = new String[]{"name", "age"};
//需要忽略的字段
String[] excludeFields = new String[]{"sex","phone"};
builder.fetchSource(includeFields, excludeFields);
SearchRequest source = request.source(builder);
SearchResponse response = client.search(source, RequestOptions.DEFAULT);
if (response.status().getStatus()==200){
SearchHit[] hits = response.getHits().getHits();
Stream<Map<String, Object>> stream = Arrays.stream(hits).map(documentFields -> {
return documentFields.getSourceAsMap();
});
List<Object> list = stream.collect(Collectors.toList());
}
System.out.println("response.status() = " + response.status());
System.out.println("response.getTook() = " + response.getTook());
System.out.println("response.toString() = " + response.toString());
client.close();
}
}