ElasticSearch最全查询姿势
ElasticSearch简介
Lucene
Lucene 是一个开源、免费、高性能、纯 Java 编写的全文检索引擎,可以算作是开源领域最好的全文检索工具包。
在实际开发中,Lucene 几乎适用于任何需要全文检索的场景,所以 Lucene 先后发展出好多语言版本,例如 C++、C#、Python 等。
早在 2005 年,Lucene 就升级为 Apache 顶级开源项目。它的作者是 Doug Cutting,有的人可能没听过这这个人,不过你肯定听过他的另一个大名鼎鼎的作品 Hadoop。
不过需要注意的是,Lucene 只是一个工具包,并非一个完整的搜索引擎,开发者可以基于 Lucene 来开发完整的搜索引擎。比较著名的有 Solr、ElasticSearch,不过在分布式和大数据环境下,ElasticSearch 更胜一筹。
Lucene 主要有如下特点:
- 简单
- 跨语言
- 强大的搜索引擎
- 索引速度快
- 索引文件兼容不同平台
ElasticSearch
ElasticSearch 是一个分布式、可扩展、近实时性的高性能搜索与数据分析引擎。ElasticSearch 基于 Java 编写,通过进一步封装 Lucene,将搜索的复杂性屏蔽起来,开发者只需要一套简单的 RESTful API 就可以操作全文检索。
ElasticSearch 在分布式环境下表现优异,这也是它比较受欢迎的原因之一。它支持 PB 级别的结构化或非结构化海量数据处理
整体上来说,ElasticSearch 有三大功能:
- 数据搜集
- 数据分析
- 数据存储
ElasticSearch 的主要特点:
分布式文件存储。
实时分析的分布式搜索引擎。
高可拓展性。
可插拔的插件支持。
ElasticSearch的各种安装
单节点安装
首先打开 Es 官网,找到 Elasticsearch:
然后点击下载按钮,选择合适的版本直接下载即可,这里我们下载Elasticsearch 7.9.3
版本来操作
ES版本和操作系统版本支持对照:
将下载的文件解压,解压后的目录含义如下:
目录 | 含义 |
---|---|
modules | 依赖模块目录 |
lib | 第三方依赖库 |
logs | 输出日志目录 |
plugins | 插件目录 |
bin | 可执行文件目录 |
config | 配置文件目录 |
data | 数据存储目录 |
启动方式:
进入到 bin
目录下,直接执行 ./elasticsearch
或者直接执行bin目录下的elasticsearch.bat
命令启动即可。
9200
作为Http协议,主要用于外部通讯
9300
作为Tcp协议,jar之间就是通过tcp协议通讯
ES集群之间是通过9300进行通讯
默认监听的端口是 9200,所以浏览器直接输入 localhost:9200
可以查看节点信息。
name:
为节点名称
cluster_name:
为集群名称
节点的名字(默认为当前电脑主机名)以及集群(默认是 elasticsearch)的名字,我们都可以自定义配置。
打开 config/elasticsearch.yml
文件,可以配置集群名称以及节点名称。配置方式如下:
node.name: master
cluster.name: louc-es
重启服务后,修改成功!
Head 插件
Elasticsearch-head 插件,可以通过可视化的方式查看集群信息。
Chrom插件安装
直接在Chrom商店搜索Elasticsearch-head即可
下载插件安装
四个步骤
git clone git://github.com/mobz/elasticsearch-head.git
cd elasticsearch-head
npm install
npm run start
通过访问 http://localhost:9100
插件地址,我们发现访问不上,因为两个端口存在跨域问题,所以我们需要配置支持跨域
修改elasticsearch
下的bin目录下的elasticsearch.yml
文件
#支持跨域
http.cors.enabled: true
#允许跨域地址所有
http.cors.allow-origin: "*"
- 基本配置
node.name: master
cluster.name: louc-es
#支持跨域
http.cors.enabled: true
#允许跨域地址所有
http.cors.allow-origin: "*"
#是否为主机
node.master: true
#允许其他访问的地址 0.0.0.0 允许所有地址访问
network.host: 0.0.0.0
重启后,再次访问http://localhost:9100
访问成功
分布式集群安装
假设:
- 一主二从
- master 的端口是 9200,slave 端口分别是 9201 和 9202
首先修改 master 的 config/elasticsearch.yml 配置文件:
#节点名称
node.name: master
#集群名称
cluster.name: louc-es
#支持跨域
http.cors.enabled: true
#允许跨域地址所有
http.cors.allow-origin: "*"
#是否为主机
node.master: true
#允许其他访问的地址 0.0.0.0 允许所有地址访问
network.host: 127.0.0.1
#设置在集群中的所有节点名称,这个节点名称就是之前所修改的,当然你也可以采用默认的也行,目前是单机,放入一个节点即可
cluster.initial_master_nodes: ["master"]
#数据和日志的存储目录
#path.data: /usr/local/elasticsearch-7.1.1/data
#path.logs: /usr/local/elasticsearch-7.1.1/logs
**注意:**通过为cluster.initial_master_nodes参数设置一系列符合主节点条件的节点的主机名或IP地址来引导启动集群。如果未设置initial_master_nodes,那么在启动新节点时会尝试发现已有的集群。
配置完成后,重启 master。
将 es 的压缩包解压两份,分别命名为 slave01 和 slave02,代表两个从机。
分别对其进行配置。
slave01/config/elasticsearch.yml
# 集群名称必须保持一致
cluster.name: louc-es
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: louc-es
node.name: slave02
network.host: 127.0.0.1
http.port: 9202
#主机位置
discovery.zen.ping.unicast.hosts: ["127.0.0.1"]
然后分别启动 slave01 和 slave02(先启动主机,再启动从机)。启动后,可以在 head 插件上查看集群信息。如果集群连接不上,想清除data目录的下的所有数据
Kibana 安装
- 下载 Kibana:https://www.elastic.co/cn/downloads/kibana
- 解压
- 配置 es 的地址信息(可选,如果 es 是默认地址以及端口,可以不用配置,具体的配置文件是 config/kibana.yml)
- 执行 ./bin/kibana 文件启动
- localhost:5601
来到控制台页面:http://localhost:5601/app/dev_tools#/console
SpringBoot和ElasticSearch版本对应
ElasticSearch核心概念
集群(Cluster)
一个或者多个安装了 es 节点的服务器组织在一起,就是集群,这些节点共同持有数据,共同提供搜索服务。
一个集群有一个名字,这个名字是集群的唯一标识,该名字成为 cluster name,默认的集群名称是 elasticsearch,具有相同名称的节点才会组成一个集群。
可以在 config/elasticsearch.yml 文件中配置集群名称:
cluster.name: louc-es
在集群中,节点的状态有三种:绿色、黄色、红色:
- 绿色:节点运行状态为健康状态。所有的主分片、副本分片都可以正常工作。
- 黄色:表示节点的运行状态为警告状态,所有的主分片目前都可以直接运行,但是至少有一个副本分片是不能正常工作的。
- 红色:表示集群无法正常工作。
节点(Node)
集群中的一个服务器就是一个节点,节点中会存储数据,同时参与集群的索引以及搜索功能。一个节点想要加入一个集群,只需要配置一下集群名称即可。默认情况下,如果我们启动了多个节点,多个节点还能够互相发现彼此,那么它们会自动组成一个集群,这是 es 默认提供的,但是这种方式并不可靠,有可能会发生脑裂现象。所以在实际使用中,建议一定手动配置一下集群信息。
索引(Index)
索引可以从两方面来理解:
名词
具有相似特征文档的集合。
动词
索引数据以及对数据进行索引操作。
类型(Type)
类型是索引上的逻辑分类或者分区。在 es6 之前,一个索引中可以有多个类型,从 es7 开始,一个索引中,只能有一个类型。在 es6.x 中,依然保持了兼容,依然支持单 index 多个 type 结构,但是已经不建议这么使用。
文档(Document)
一个可以被索引的数据单元。例如一个用户的文档、一个产品的文档等等。文档都是 JSON 格式的。
分片(Shards)
索引都是存储在节点上的,但是受限于节点的空间大小以及数据处理能力,单个节点的处理效果可能不理想,此时我们可以对索引进行分片。当我们创建一个索引的时候,就需要指定分片的数量。每个分片本身也是一个功能完善并且独立的索引。
默认情况下,一个索引会自动创建 1 个分片,并且为每一个分片创建一个副本。
新建索引时:可以指定分片数
和副本数
例如:指定2个分片数,2个副本数。那么创建的索引就是1个分片
就对应2个副本数
下面就是:test索引创建5个分片数,1个副本数。那么1个分片对应1个副本。0粗框
代表第1个主分片(粗框都是主分片),0细框
代表第1个分片的副本(细框都是副本)。副本和主分片分别分布在集群的节点上,当主分片不能使用时,就使用副本当作主分片使用
**新建文档时:**索引中创建的文档自动根据文档id进行hash分配到不同的分片中,也可以指定存储的分片
副本(Replicas)
副本也就是备份,是对主分片的一个备份。
Settings
集群中对索引的定义信息,例如索引的分片数、副本数等等。
Mapping
Mapping 保存了定义索引字段的存储类型、分词方式、是否存储等信息。
Analyzer
字段分词方式的定义。
ElasticSearch Vs 关系型数据库
ElasticSearch分词器
内置分词器
ElasticSearch 核心功能就是数据检索,首先通过索引将文档写入 es。查询分析则主要分为两个步骤:
词条化:分词器将输入的文本转为一个一个的词条流。
过滤:比如停用词过滤器会从词条中去除不相干的词条(的,嗯,啊,呢);另外还有同义词过滤器、小写过滤器等。
ElasticSearch 中内置了多种分词器可以供使用。
内置分词器:
中文分词器
在 Es 中,使用较多的中文分词器是 elasticsearch-analysis-ik,这个是 es 的一个第三方插件,代码托管在 GitHub 上:
https://github.com/medcl/elasticsearch-analysis-ik
安装
两种使用方式:
第一种:
- 首先打开分词器官网:https://github.com/medcl/elasticsearch-analysis-ik。
- 在 https://github.com/medcl/elasticsearch-analysis-ik/releases 页面找到最新的正式版,下载下来。我们这里的下载链接是 https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.9.3/elasticsearch-analysis-ik-7.9.3.zip。
- 将下载文件解压。
- 在 es/plugins 目录下,新建 ik 目录,并将解压后的所有文件拷贝到 ik 目录下。
- 重启 es 服务。
第二种:
./bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.9.3/elasticsearch-analysis-ik-7.9.3.zip
测试
es 重启成功后,首先创建一个名为 test 的索引:
分词器可以在查询和索引定义的时候使用,这里是使用的是查询时候的分词器
接下来,在该索引中进行分词测试,请求参数示例:
对要查询的词进行分词:POST http://localhost:9200/索引名/_analyze
请求JSON参数:
- analyzer:
ik_smart
/ik_max_word
(粒度更细) - text: "内容"
自定义扩展词库
在 es/plugins/ik/config 目录下,新建 ext.dic 文件(文件名任意),在该文件中可以配置自定义的词库。
如果有多个词,换行写入新词即可。
然后在 es/plugins/ik/config/IKAnalyzer.cfg.xml 中配置扩展词典的位置:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
<comment>IK Analyzer 扩展配置</comment>
<!--用户可以在这里配置自己的扩展字典 -->
<entry key="ext_dict">ext.dic</entry>
<!--用户可以在这里配置自己的扩展停止词字典-->
<entry key="ext_stopwords"></entry>
<!--用户可以在这里配置远程扩展字典 -->
<!-- <entry key="remote_ext_dict">words_location</entry> -->
<!--用户可以在这里配置远程扩展停止词字典-->
<!-- <entry key="remote_ext_stopwords">words_location</entry> -->
</properties>
es重启之后,我们再次请求,我们自定义扩展的一个词已经变为一个分词
远程词库(热更新)
也可以配置远程词库,远程词库支持热更新(不用重启 es 就可以生效)。
热更新只需要提供一个接口,接口返回扩展词即可。 详情参考 https://github.com/medcl/elasticsearch-analysis-ik
具体使用方式如下,新建一个 Spring Boot 项目,引入 Web 依赖即可。然后在 resources/stastic 目录下新建 ext.dic 文件,写入扩展词:
接下来,在 es/plugins/ik/config/IKAnalyzer.cfg.xml 文件中配置远程扩展词接口:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
<comment>IK Analyzer 扩展配置</comment>
<!--用户可以在这里配置自己的扩展字典 -->
<entry key="ext_dict">ext.dic</entry>
<!--用户可以在这里配置自己的扩展停止词字典-->
<entry key="ext_stopwords"></entry>
<!--用户可以在这里配置远程扩展字典 -->
<entry key="remote_ext_dict">http://localhost:8080/ext.dic</entry>
<!--用户可以在这里配置远程扩展停止词字典-->
<!-- <entry key="remote_ext_stopwords">words_location</entry> -->
</properties>
配置完成后,重启 es ,即可生效。
我们修改SpringBoot项目中的ext.dic文件增加新的分词后重启SpringBoot服务器,不用重启es,等待一段时间发现我们在ext.dic添加的新词生效。
热更新,主要是响应头的 Last-Modified
或者 ETag
字段发生变化,ik 就会自动重新加载远程扩展
ElasticSearch索引管理
新建索引
通过 head 插件新建索引
在 head 插件中,选择 索引选项卡,然后点击新建索引。新建索引时,需要填入索引名称、分片数以及副本数。
索引创建成功后,如下图:
在上述图片中:1个分片对应1个副本,若副本数为2,则一个分片对应2个副本。生成的粗框代表主分片,细框代表副本
0、1、2、3、4 分别表示索引的分片(5个主分片并创建了5个副本),粗框表示主分片,细框表示副本(点一下框,通过 primary 属性可以查看是主分片还是副本)
例如:这里的master中的2就是主分片。 Primary:true 主分片 Primary:false 副本
通过postman创建
PUT请求 http://localhost:9200/book 则创建一个book
索引
通过Kibana创建
创建成功后,可以查看索引信息:
注意事项
- 索引名称不能有大写字母
- 索引名是唯一的,不能重复,重复创建会出错
更新索引
索引创建好之后,可以修改其属性。
- 修改索引的副本数(默认是一个分片和一个副本):
PUT book/_settings
{
"number_of_replicas": 2
}
更新分片数也是一样。
找不到更新的字段名,直接查看索引信息的json值即可
修改索引的读写权限
- 向索引中写入文档:
向book
索引中添加type
为_doc
(文档)类型的编号为1的文档
PUT book/_doc/1
{
"title":"三国演义"
}
默认情况下,索引是具备读写权限的,当然这个读写权限可以关闭。
- 关闭索引的写权限:
PUT book/_settings
{
"blocks.write": true
}
blocks.write(阻塞写),关闭之后,就无法添加文档了。
- 打开索引的写权限:
PUT book/_settings
{
"blocks.write": true
}
其他类似的权限有:
- blocks.write 是否阻塞写
- blocks.read 是否阻塞读
- blocks.read_only 是否只读
查看索引
- head 插件查看方式如下:
- 请求查看方式如下:
GET book/_settings
- 也可以同时查看多个索引信息:
GET book,test/_settings
- 查看所有索引信息
GET _all/_settings
删除索引
- head 插件可以删除索引:
- 请求删除如下:
DELETE test
删除一个不存在的索引会报错。
索引打开/关闭
- 关闭索引
POST book/_close
- 打开索引:
POST book/_open
当然,可以同时关闭/打开多个索引,多个索引用 , 隔开,或者直接使用 _all 代表所有索引。
复制索引
索引复制,只会复制数据,不会复制索引配置(例如设置的分片和副本数)。
POST _reindex
{
"source": {"index":"book"},
"dest": {"index":"book_mark"}
}
复制的时候,可以添加查询条件。
索引别名
可以为索引创建别名,如果这个别名是唯一的,该别名可以代替索引名称。
POST /_aliases
{
"actions": [
{
"add": {
"index": "book",
"alias": "book_mybook"
}
}
]
}
- 将 add 改为 remove 就表示移除别名:
POST /_aliases
{
"actions": [
{
"remove": {
"index": "book",
"alias": "book_mybook"
}
}
]
}
- 查看某一个索引的别名:
GET /book/_alias
- 查看某一个别名对应的索引(book_mybook 表示一个别名),如果有多个别名相同的索引,则会查出所有
GET book_mybook/_alias
- 可以查看集群上所有可用别名:
GET /_alias
ElasticSearch文档的操作
添加文档
首先新建一个索引 blog,若添加文档索引不存在则会创建一个索引
PUT blog/_doc/1
{
"title":"git详解2",
"data":"2021-1-3",
"content":"分支管理,克隆操作,拉取操作,推送操作等内容"
}
1
表示添加文档的id,添加成功后,响应的json如下:
{
"_index" : "blog",
"_type" : "_doc",
"_id" : "1",
"_version" : 9,
"result" : "created",
"_shards" : {
"total" : 2,
"successful" : 2,
"failed" : 0
},
"_seq_no" : 14,
"_primary_term" : 1
}
- _index 表示文档索引
- _type 表示文档的类型
- _id 表示文档的id
- _version 表示文档的版本(更新文档,版本会自动加1,针对当前文档的修改操作版本),每个文档都有独立的 _version 并且互不影响
- result 表示执行结果
- _shards 表示分片信息
_seq_no
(针对当前索引的修改操作版本)和_primary_term
版本控制
- 使用PUT请求添加文档时必须携带文档id,否则会出错
- 使用POST请求添加文档可以不携带文档id,会自动创建一个文档id
POST blog/_doc
{
"title":"git详解3",
"data":"2021-1-3",
"content":"分支管理,克隆操作,拉取操作,推送操作等内容"
}
查找文档
GET blog/_doc/1
从blog索引中获取编号为1的文档,如果不存在此编号则会返回以下错误:
{
"_index" : "blog",
"_type" : "_doc",
"_id" : "1",
"found" : false
}
存在该文档:
{
"_index" : "blog",
"_type" : "_doc",
"_id" : "1",
"_version" : 15,
"_seq_no" : 20,
"_primary_term" : 1,
"found" : true,
"_source" : {
"title" : "git详解1",
"data" : "2021-1-3",
"content" : "分支管理,克隆操作,拉取操作,推送操作等内容"
}
}
查看索引中所有文档内容
GET {index}/_search
判断文档是否存在
根据id判断指定文档是否存在
存在:
Head blog/_doc/1
不存在:
批量获取文档
获取blog索引中编号为1和2的文档
GET blog/_mget
{
"ids":["1","2"]
}
所以这里我们直接用POST请求批量查询比较好。
文档更新
普通更新(根据id)
文档更新一次,_version 就会加1
这种方式更新某个字段的值会覆盖整个文档:
PUT blog/_doc/1
{
"title":"git详解1"
}
通过查询 GET blog/_doc/1
发现,字段已经被覆盖
{
"_index" : "blog",
"_type" : "_doc",
"_id" : "1",
"_version" : 16,
"_seq_no" : 24,
"_primary_term" : 1,
"found" : true,
"_source" : {
"title" : "git详解1"
}
}
大多数时候,我们只需要更新某一个字段的值,所以我们采取以下方式:
请求格式: POST {index}/_update/{id}
POST blog/_update/2
{
"script": {
"lang": "painless",
"source": "ctx._source.title=params.title",
"params": {
"title":"我是一个标题"
}
}
}
在脚本中,lang表示脚本语言,painless是es内置的一种脚本语言。source表示具体执行的脚本,ctx是一个上下文对象,通过ctx._source
可以访问到文档中的数据信息,那么通过ctx._source.属性=params.属性
指定要更新的数据,然后在params参数
中指定属性值
即可。
- 增加一个文档的字段并为其赋值
增加tags标签并为其赋值 java,js
POST blog/_update/2
{
"script": {
"lang": "painless",
"source": "ctx._source.tags=[\"java\",\"js\"]"
}
}
获取文档内容:
{
"_index" : "blog",
"_type" : "_doc",
"_id" : "2",
"_version" : 4,
"_seq_no" : 28,
"_primary_term" : 1,
"found" : true,
"_source" : {
"title" : "我是一个标题",
"data" : "2021-1-3",
"content" : "分支管理,克隆操作,拉取操作,推送操作等内容",
"tags" : [
"java",
"js"
]
}
}
使用add方法
为tags标签添加一个元素css
POST blog/_update/2
{
"script": {
"lang": "painless",
"source": "ctx._source.tags.add(\"css\")"
}
}
再次获取文档内容:
{
"_index" : "blog",
"_type" : "_doc",
"_id" : "2",
"_version" : 5,
"_seq_no" : 29,
"_primary_term" : 1,
"found" : true,
"_source" : {
"title" : "我是一个标题",
"data" : "2021-1-3",
"content" : "分支管理,克隆操作,拉取操作,推送操作等内容",
"tags" : [
"java",
"js",
"css"
]
}
}
也可以使用一些复杂的 if-else逻辑:
如果文档中的tags标签包含css内容,则删除此文档,否则什么都不做
POST blog/_update/2
{
"script": {
"lang": "painless",
"source": "if(ctx._source.tags.contains(\"css\")){ctx.op=\"delete\"}else{ctx.op=\"none\"}"
}
}
查询更新
通过条件查询找到文档,然后进行更新
例如:将title包含666的文档的content修改为888
文档内容:
POST blog/_update_by_query
{
"script": {
"source": "ctx._source.content=\"888\"",
"lang": "painless"
},
"query": {
"term": {
"title": "666"
}
}
}
更新结果:
删除文档
根据id删除
语法:DELETE {index}/_doc/{id}
从索引中删除一个文档
删除一个id
为 60j2xnYBuZwDbvuAkj-C
的文档
DELETE blog/_doc/60j2xnYBuZwDbvuAkj-C
如果在添加文档时指定了路由,则删除文档时也需要指定路由,否则删除失败。
查询删除
查询删除是 POST 请求。
语法:POST {index}/_delete_by_query
例如:删除 title 中包含 666 的文档:
POST blog/_delete_by_query
{
"query":{
"term":{
"title":"666"
}
}
}
- 删除某一个索引下的所有文档:
POST blog/_delete_by_query
{
"query":{
"match_all":{}
}
}
批量操作
es 中通过 Bulk API 可以执行批量索引、批量删除、批量更新等操作。
首先需要将所有的批量操作写入一个 JSON 文件中,然后通过 POST 请求将该 JSON 文件上传并执行。
例如新建一个名为 aaa.json
的文件,内容如下:
{"index":{"_index":"user","_id":"666"}}
{"name":"javaboy"}
{"update":{"_index":"user","_id":"666"}}
{"doc":{"name":"louc-es555"}}
首先第一行:index 表示要执行一个索引操作(这个表示一个 action,其他的 action 还有 create,delete,update)。
_index
定义了索引名称,这里表示要创建一个名为 user 的索引,_id
表示新建文档的 id 为 666。第二行是第一行操作的参数。
第三行的 update 则表示要更新。
第四行是第三行的参数。
注意,结尾要空出一行。
aaa.json
文件创建成功后,在该目录下,执行请求命令,如下:
curl -XPOST "http://localhost:9200/user/_bulk" -H "content-type:application/json" --data-binary @aaa.json
执行完成后,就会创建一个名为 user 的索引,同时向该索引中添加一条记录,再修改该记录,最终结果如下:
ElasticSearch文档路由
es 是一个分布式系统,当我们存储一个文档到 es 上之后,这个文档实际上是被存储到 master 节点中的某一个主分片上。
这里我们新建一个blog索引,并创建2个主分片数,每个主分片有1个副本
生成的结果:
我们可以从上图看出,第一个主分片在 savle02 上,该主分片上的副本在master。第二个主分片在 salve01 上,该主分片上的副本在 slave02 上。
- 新增两个文档:
PUT blog/_doc/1
{
"title":"java"
}
PUT blog/_doc/2
{
"title":"java2"
}
新增的结果:
从以上结果我们并不能查看到新增的文档在哪个分片。
通过以下命令可查看blog索引中所有文档分片信息
GET _cat/shards/blog?v
结果:
index shard prirep state docs store ip node
blog 1 r STARTED 0 208b 127.0.0.1 slave02
blog 1 p STARTED 0 208b 127.0.0.1 slave01
blog 0 p STARTED 2 7.2kb 127.0.0.1 slave02
blog 0 r STARTED 2 3.7kb 127.0.0.1 master
index:属于哪个索引
shard: 分片编号
prirep: 是否为主分片(p为主分片,r为副本)
docs: 该分片下的文档数
node:所属节点
从上述可以得知,我们的存储的两个文档都在 salve02节点的编号为0的主分片上,副本复制主分片上的内容做备份。
分片规则
es 中的路由机制是通过哈希算法,将具有相同哈希值的文档放到一个主分片中,分片位置的计算方式如下:
shard=hash(routing) % number_of_primary_shards
routing 可以是一个任意字符串,es 默认是将文档的 id 作为 routing 值,通过哈希函数根据 routing 生成一个数字,然后将该数字和分片数取余,取余的结果就是分片的位置。
默认的这种路由模式,最大的优势在于负载均衡,这种方式可以保证数据平均分配在不同的分片上。但是他有一个很大的劣势,就是查询时候无法确定文档的位置,此时它会将请求广播到所有的分片上去执行。另一方面,使用默认的路由模式,后期修改分片数量不方便。
当然开发者也可以自定义 routing
的值,方式如下:
PUT blog/_doc/b?routing=louc
{
"title":"hello...world"
}
如果文档在添加时指定了 routing,则查询、删除、更新时也需要指定 routing。否则操作失败。
GET blog/_doc/b?routing=louc
自定义 routing 有可能会导致负载不均衡,这个还是要结合实际情况选择。
典型场景:
对于用户数据,我们可以将 userid 作为 routing,这样就能保证同一个用户的数据保存在同一个分片中,检索时,同样使用 userid 作为 routing,这样就可以精准的从某一个分片中获取数据。
ElasticSearch 锁和版本控制
当我们使用 es 的 API 去进行文档更新时,它首先读取原文档出来,然后对原文档进行更新,更新完成后再重新索引整个文档。不论你执行多少次更新,最终保存在 es 中的是最后一次更新的文档。但是如果有两个线程同时去更新,就有可能出问题。
要解决问题,就是锁
锁
悲观锁
很悲观,每一次去读取数据的时候,都认为别人可能会修改数据,所以屏蔽一切可能破坏数据完整性的操作。关系型数据库中,悲观锁使用较多,例如行锁、表锁等等。
乐观锁
很乐观,每次读取数据时,都认为别人不会修改数据,因此也不锁定数据,只有在提交数据时,才会检查数据完整性。这种方式可以省去锁的开销,进而提高吞吐量。
在 es 中,实际上使用的就是乐观锁。
版本控制
es6.7之前
在 es6.7
之前,使用 version+version_type 来进行乐观并发控制。根据前面的介绍,文档每被修改一个,version 就会自增一次,es 通过 version 字段来确保所有的操作都有序进行。
version 分为内部版本控制和外部版本控制。
内部版本
es 自己维护的就是内部版本,当创建一个文档时,es 会给文档的版本赋值为 1。
每当用户修改一次文档,版本号就回自增 1。
如果使用内部版本,es 要求 version 参数的值必须和 es 文档中 version 的值相当,才能操作成功。
外部版本
也可以维护外部版本。
在添加文档时,就指定版本号:
PUT blog/_doc/1?version=200&version_type=external
{
"title":"2222"
}
以后更新的时候,版本要大于已有的版本号。
- vertion_type=external 或者 vertion_type=external_gt 表示以后更新的时候,版本要大于已有的版本号。
- vertion_type=external_gte 表示以后更新的时候,版本要大于等于已有的版本号。
最新方案(Es6.7 之后)
现在使用 if_seq_no
和 if_primary_term
两个参数来做并发控制。
seq_no
不属于某一个文档,它是属于整个索引的
_version
则是属于某一个文档的,每个文档的 _version
互不影响。
现在更新文档时,使用 seq_no
来做并发。由于 seq_no
是属于整个 index
的,所以任何文档的修改或者新增,seq_no
都会自增。
现在就可以通过 seq_no
和 primary_term
来做乐观并发控制。
例如:我们查询 GET blog/_doc/2
{
"_index" : "blog",
"_type" : "_doc",
"_id" : "2",
"_version" : 3,
"_seq_no" : 6,
"_primary_term" : 1,
"found" : true,
"_source" : {
"title" : "这是修改的内容111"
}
}
_seq_no
的版本为6
,_primary_term
的版本为1
,所以我们在修改该文档时可以携带两个参数才能修改成功,否则会报错
PUT blog/_doc/2?if_seq_no=6&if_primary_term=1
{
"title":"6666"
}
修改结果:
{
"_index" : "blog",
"_type" : "_doc",
"_id" : "2",
"_version" : 4,
"_seq_no" : 7,
"_primary_term" : 1,
"found" : true,
"_source" : {
"title" : "哈哈"
}
}
我们发现修改成功。_seq_no
版本号加1
ElasticSearch 中的倒排索引
倒排索引是 es 中非常重要的索引结构,是从文档词项到文档 ID 的一个映射过程。
正排索引
正排索引实际上是不存在的,这里我们便于理解,抽象为正排索引
我们在关系型数据库中见到的索引,就是“正排索引”。
关系型数据库中的索引如下,假设我有一个博客表:
id | author | title | content |
---|---|---|---|
1 | 张三 | java技术大全 | 容器,集合,并发 |
2 | 李四 | SpringBoot进阶 | 自定义starter,继承框架 |
我们可以针对这个表建立索引(正排索引):
索引 | 内容 |
---|---|
1 | 容器,集合,并发 |
2 | 自定义starter,继承框架 |
java技术大全 | 容器,集合,并发 |
SpringBoot进阶 | 自定义starter,继承框架 |
当我们通过 id 或者标题去搜索文章时,就可以快速搜到。
但是如果我们按照文章内容的关键字去搜索,就只能去内容中做字符匹配了。为了提高查询效率,就要考虑使用倒排索引。
倒排索引
倒排索引就是以内容的关键字建立索引,通过索引找到文档 id,再进而找到整个文档。
索引 | 文档id=1 | 文档id=2 |
---|---|---|
容器 | √ | |
框架 | √ | √ |
集合 | √ | |
并发 | √ |
一般来说,倒排索引分为两个部分:
单词词典(记录所有的文档词项,以及词项到倒排列表的关联关系)
倒排列表(记录单词与对应的关系,由一系列倒排索引项组成,倒排索引项指:文档 id、词频(TF)(词项在文档中出现的次数,评分时使用)、位置(Position,词项在文档中分词的位置)、偏移(记录词项开始和结束的位置))
当我们去索引一个文档时,就回建立倒排索引,搜索时,直接根据倒排索引搜索。
ElasticSearch动态映射与静态映射
映射就是 Mapping,它用来定义一个文档以及文档所包含的字段该如何被存储和索引。所以,它其实有点类似于关系型数据库中表的定义。
映射分类
动态映射
顾名思义,就是自动创建出来的映射。es 根据存入的文档,自动分析出来文档中字段的类型以及存储方式,这种就是动态映射。
举一个简单例子,新建一个索引,然后查看索引信息:
在创建好的索引信息中,可以看到,mappings
为空,这个 mappings
中保存的就是映射信息。
现在我们向索引中添加一个文档,如下:
PUT blog/_doc/1
{
"title":"hellworld",
"date":"2021-01-01",
"isEnable":true
}
- 注意:这里的data日期类型必须为 xxxx-xx-xx格式,若为2021-1-1则动态映射为text类型
文档添加成功后,就会自动生成 Mappings:
可以看到,date 字段的类型为 date,title 的类型有两个,text (用于全文检索) 和 keyword(用于关键词检索)。
默认情况下,文档中如果新增了字段,mappings 中也会自动新增进来。
严格模式
有的时候,如果希望新增字段时,能够抛出异常来提醒开发者,这个可以通过 mappings 中 dynamic 属性来配置。
- dynamic 属性有三种取值:
- true,默认值。自动添加新字段。
- false,忽略新字段(即使新加入字段,也不会添加到文档中)。
- strict,严格模式(在索引创建时定义好字段类型),若后续添加新字段或者添加的值不符合定义时的类型都会抛出异常。
具体配置方式如下,创建索引时指定 mappings(这其实就是静态映射),创建后不可更改
。
PUT blog
{
"mappings": {
"dynamic":"strict",
"properties": {
"title":{
"type": "text"
},
"age":{
"type": "long"
}
}
}
}
若dynamic
为strict
模式,那么向blog添加一条文档:
PUT blog/_doc/1
{
"title":"1111",
"date":"2020-11-11",
"age":99
}
在添加的文档中,多出了一个 date 字段,而该字段没有预定义,所以这个添加操作就回报错:
{
"error" : {
"root_cause" : [
{
"type" : "strict_dynamic_mapping_exception",
"reason" : "mapping set to strict, dynamic introduction of [date] within [_doc] is not allowed"
}
],
"type" : "strict_dynamic_mapping_exception",
"reason" : "mapping set to strict, dynamic introduction of [date] within [_doc] is not allowed"
},
"status" : 400
}
日期检测
动态映射还有一个日期检测的问题。
非strict
模式下,例如新建一个索引,然后添加一个含有日期的文档,如下:
PUT blog/_doc/1
{
"remark":"2020-11-11"
}
添加成功后,remark 字段会被推断是一个日期类型。
注意:这里的data日期类型必须为 xxxx-xx-xx格式,若为2021-1-1则动态映射为text类型
此时,remark 字段就无法存储其他类型了。
若我们想添加其他类型的内容在remark字段上则:
PUT blog/_doc/1
{
"remark":"javaboy"
}
此时报错如下:
{
"error" : {
"root_cause" : [
{
"type" : "mapper_parsing_exception",
"reason" : "failed to parse field [remark] of type [date] in document with id '1'. Preview of field's value: 'javaboy'"
}
],
"type" : "mapper_parsing_exception",
"reason" : "failed to parse field [remark] of type [date] in document with id '1'. Preview of field's value: 'javaboy'",
"caused_by" : {
"type" : "illegal_argument_exception",
"reason" : "failed to parse date field [javaboy] with format [strict_date_optional_time||epoch_millis]",
"caused_by" : {
"type" : "date_time_parse_exception",
"reason" : "Failed to parse with all enclosed parsers"
}
}
},
"status" : 400
}
要解决这个问题,可以使用静态映射,即在索引定义时,不能设置严格模式
,将 remark 指定为 text 类型。也可以关闭日期检测。
PUT blog
{
"mappings": {
"date_detection": false
}
}
此时日期类型就回当成文本来处理。
静态映射
通过以上在索引创建时就指定字段类型的就为静态映射。例如以上dynamic
的strict
模式,但是无论什么模式下,只要是创建后的映射mapping以后永远不可更改。
若在dynamic为默认(true)
情况下,则添加新的字段时可以自动动态添加映射,若添加已存在的字段时,就必须符合定义时的类型,否则会报错。
若dynamic为strict
模式下,那么不能添加不存在的字段,并且添加的字段必须要符合创建时的mapping类型,否则都会报错。
基本语法:
PUT 索引
{
"mappings":{
"dynamic":"true/false/strict",
"properties":{
字段:{
"type":"text/keyword",
"analyzer":"standard/ik_smart/ik_max_word"
}
}
}
}
类型推断对照表
es 中动态映射类型推断方式如下:
JSON中的数据 | 自动推断出来的数据类型 |
---|---|
null | 没有字段被添加 |
true/false | boolean |
浮点数字 | float |
数字 | long |
JSON对象 | object |
数组 | 数组中的第一个非空值来决定 |
string | text /keyword /date 都有可能 |
ElasticSearch 四种字段类型详解
核心类型
字符串类型
string:这是一个已经过期的字符串类型。在 es5 之前,用这个来描述字符串,现在的话,它已经被 text 和 keyword 替代了。text:如果一个字段是要被**全文检索(需要分词的)**的,比如说博客内容、新闻内容、产品描述,那么可以使用 text。用了 text 之后,字段内容会被分析,在生成倒排索引之前,字符串会被分词器分成一个个词项。text 类型的字段不用于排序,很少用于聚合。这种字符串也被称为 analyzed 字段
keyword:这种类型适用于结构化的字段,例如标签、email 地址、手机号码等等,这种类型的字段可以用作过滤、排序、聚合等。这种字符串也称之为 not-analyzed(不进行分词) 字段。
数字类型
类型 | 取值范围 |
---|---|
long | -263 ~ 263-1 |
interger | -231 ~ 231-1 |
short | -215 ~ 215-1 |
byte | -27 ~ 27-1 |
double | 64位的双精度 IEEE754 浮点类型 |
float | 32位的双精度 IEEE754 浮点类型 |
half_float | 16位的双精度 IEEE754 浮点类型 |
scaled_float | 缩放类型的浮点类型 |
- 在满足需求的情况下,优先使用范围小的字段。字段长度越短,索引和搜索的效率越高。
- 浮点数,优先考虑使用 scaled_float。比如价格只需要精确到分,price为57.34的字段缩放因子为100,存起来就是5734 优先考虑使用带缩放因子的scaled_float浮点类型。
scaled_float
举例:
PUT product
{
"mappings": {
"properties": {
"name":{
"type": "text"
},
"price":{
"type": "scaled_float",
"scaling_factor": 100
}
}
}
}
日期类型 date
由于 JSON 中没有日期类型,所以 es 中的日期类型形式就比较多样:
可接收的类型:
- 2020-11-11 或者 2020-11-11 11:11:11
- 一个从 1970.1.1 零点到现在的一个秒数或者毫秒数。
es 内部将时间转为 UTC,然后将时间按照 millseconds-since-the-epoch 的长整型来存储。
自定义日期类型:
PUT product
{
"mappings": {
"properties": {
"date":{
"type": "date"
}
}
}
}
能够存入并解析的日期:内部存储的是毫秒计时的长整型数。
PUT product/_doc/1
{
"date":"2020-11-11"
}
PUT product/_doc/2
{
"date":"2020-11-11T11:11:11Z"
}
PUT product/_doc/3
{
"date":"1604672099958"
}
布尔类型 boolean
JSON 中的 “true”、“false”、true、false 都可以。
- 若提前定义好文档的某个字段类型为boolean,则存入的 "true" 和 true 都是boolean类型
- 若没有提前定义好的文档的字段类型,那么存入 "true" 的字符串推断则是 text/keyword类型, 存入 true 不为字符串,才为boolean类型
二进制类型 binary
二进制接受的是 base64 编码的字符串,默认不存储,也不可搜索。
范围类型
- integer_range
- float_range
- long_range
- double_range
- date_range
- ip_range
定义的时候,指定范围类型即可:
PUT product
{
"mappings": {
"properties": {
"date":{
"type": "date"
},
"price":{
"type":"float_range"
}
}
}
}
在插入文档的时候,必须知道范围类型字段的范围才能插入成功,否则会报错:
PUT product/_doc/1
{
"price":{
"gt":10,
"lt":"20"
}
}
更多的范围表示: gt
大于,gte
大于等于,lt
小于,lte
小于等于。
复合类型
数组类型
es 中没有专门的数组类型。默认情况下,任何字段都可以有一个或者多个值。需要注意的是,数组中的元素必须是同一种类型。
添加数组是,数组中的第一个非空元素决定了整个数组的类型。
对象类型 object
由于 JSON 本身具有层级关系,所以文档包含内部对象。内部对象中,还可以再包含内部对象。
PUT product/_doc/2
{
"date":"2020-11-11T11:11:11Z",
"ext_info":{
"address":"China"
}
}
嵌套类型 nested
nested 是 object 中的一个特例。
如果使用 object 类型,假如有如下一个文档:
{
"user":[
{
"first":"Zhang",
"last":"san"
},
{
"first":"Li",
"last":"si"
}
]
}
由于 Lucene 没有内部对象的概念,所以 es 会将对象层次扁平化,将一个对象转为字段名和值构成的简单列表。即上面的文档,最终存储形式如下:
{
"user.first":["Zhang","Li"],
"user.last":["san","si"]
}
扁平化之后,用户名之间的关系没了。这样会导致如果搜索 Zhang si
或者 Li san
会搜索到,显然不符合要求。
那么这里可以在定义索引的时候定义user字段的类型为nested
,然后用专门的嵌套查询方式去查询数据。
nested 对象类型可以保持数组中每个对象的独立性。nested 类型将数组中的每一组对象作为独立隐藏文档来索引,这样每一个嵌套对象都可以独立被索引。
嵌套类型的es内部存储优化:
{
{
"user.first":"Zhang",
"user.last":"san"
},
{
"user.first":"Li",
"user.last":"si"
}
}
优点
文档存储在一起,读取性能高。
缺点
更新父或者子文档时需要更新更个文档。
地理类型
使用场景:
- 查找某一个范围内的地理位置
- 通过地理位置或者相对中心点的距离来聚合文档
- 把距离整个到文档的评分中
- 通过距离对文档进行排序
geo_point 坐标点
geo_point 就是一个坐标点,定义方式如下:
PUT people
{
"mappings": {
"properties": {
"location":{
"type": "geo_point"
}
}
}
}
创建时指定字段类型,存储的时候,有四种方式:
- lat 纬度
- lon 经度
PUT people/_doc/1
{
"location":{
"lat": 34.27,
"lon": 108.94
}
}
PUT people/_doc/2
{
"location":"34.27,108.94"
}
- 地址位置转 geo_hash:
PUT people/_doc/3
{
"location":"uzbrgzfxuzup"
}
- 若为数组描述,先
经度
后纬度
PUT people/_doc/4
{
"location":[108.94,34.27]
}
geo_shape 图形
创建索引时指定字段的 geo_shape 类型:
PUT people
{
"mappings": {
"properties": {
"location":{
"type": "geo_shape"
}
}
}
}
添加文档时需要使用type
指定具体的类型(上面图中的Elasticsearch
):
PUT people/_doc/1
{
"location":{
"type":"point",
"coordinates": [108.94,34.27]
}
}
如果是 linestring,如下:
PUT people/_doc/2
{
"location":{
"type":"linestring",
"coordinates": [[108.94,34.27],[100,33]]
}
}
特殊类型
IP
存储 IP 地址,类型是 ip:
PUT blog
{
"mappings": {
"properties": {
"address":{
"type": "ip"
}
}
}
}
添加文档:
PUT blog/_doc/1
{
"address":"192.168.91.1"
}
搜索文档:
query:指定查询条件
term:指定查询属性
GET blog/_search
{
"query": {
"term": {
"address": "192.168.0.0/16"
}
}
}
token_count
用于统计字符串分词后的词项个数。
PUT blog
{
"mappings": {
"properties": {
"title":{
"type": "text",
"fields": {
"length":{
"type":"token_count",
"analyzer":"standard"
}
}
}
}
}
}
相当于新增了 title.length 字段用来统计分词后词项的个数。
添加文档:
PUT blog/_doc/1
{
"title":"zhang san"
}
可以通过 token_count 去查询:查询title分词后个数为2的数据
GET blog/_search
{
"query": {
"term": {
"title.length": 2
}
}
}
ElasticSearch 映射参数mapping
analyzer 分词器
定义文本字段的分词器,默认对索引
和查询
都是有效的。
先不适应分词器创建索引并添加文档:
POST blog/_doc/1
{
"text":"美国留给伊拉克的是个烂摊子吗"
}
查看词条向量
,也就是查看默认的分词结果:
GET blog/_termvectors/1
{
"fields": ["text"]
}
结果如下:
{
"_index" : "blog",
"_type" : "_doc",
"_id" : "1",
"_version" : 2,
"found" : true,
"took" : 57,
"term_vectors" : {
"text" : {
"field_statistics" : {
"sum_doc_freq" : 28,
"doc_count" : 2,
"sum_ttf" : 28
},
"terms" : {
"个" : {
"term_freq" : 1,
"tokens" : [
{
"position" : 9,
"start_offset" : 9,
"end_offset" : 10
}
]
},
"伊" : {
"term_freq" : 1,
"tokens" : [
{
"position" : 4,
"start_offset" : 4,
"end_offset" : 5
}
]
},
"克" : {
"term_freq" : 1,
"tokens" : [
{
"position" : 6,
"start_offset" : 6,
"end_offset" : 7
}
]
},
"吗" : {
"term_freq" : 1,
"tokens" : [
{
"position" : 13,
"start_offset" : 13,
"end_offset" : 14
}
]
},
"国" : {
"term_freq" : 1,
"tokens" : [
{
"position" : 1,
"start_offset" : 1,
"end_offset" : 2
}
]
},
"子" : {
"term_freq" : 1,
"tokens" : [
{
"position" : 12,
"start_offset" : 12,
"end_offset" : 13
}
]
},
"拉" : {
"term_freq" : 1,
"tokens" : [
{
"position" : 5,
"start_offset" : 5,
"end_offset" : 6
}
]
},
"摊" : {
"term_freq" : 1,
"tokens" : [
{
"position" : 11,
"start_offset" : 11,
"end_offset" : 12
}
]
},
"是" : {
"term_freq" : 1,
"tokens" : [
{
"position" : 8,
"start_offset" : 8,
"end_offset" : 9
}
]
},
"烂" : {
"term_freq" : 1,
"tokens" : [
{
"position" : 10,
"start_offset" : 10,
"end_offset" : 11
}
]
},
"留" : {
"term_freq" : 1,
"tokens" : [
{
"position" : 2,
"start_offset" : 2,
"end_offset" : 3
}
]
},
"的" : {
"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" : 1,
"tokens" : [
{
"position" : 0,
"start_offset" : 0,
"end_offset" : 1
}
]
}
}
}
}
}
可以从上面得知,分词向量是一个字一个字的分,这样没有任何意义。
通过分词查询的时候也只能通过一个字一个字的作为查询条件才能查询到该条数据,例如:
GET blog/_search
{
"query": {
"term": {
"text": {
"value": "美国"
}
}
}
}
以上什么也查不到。只要通过分词向量得出分词去查才能查找,例如:
GET blog/_search
{
"query": {
"term": {
"text": {
"value": "美"
}
}
}
}
所以需要在创建索引时指定字段的分词器:
创建blog索引
时,为text字段
指定ik_smart分词器
PUT blog
{
"mappings": {
"properties": {
"text":{
"type": "text",
"analyzer": "ik_smart"
}
}
}
}
再次添加文档并查看词条向量:
POST blog/_doc/1
{
"text":"美国留给伊拉克的是个烂摊子吗"
}
GET blog/_termvectors/1
{
"fields": ["text"]
}
结果如下:
{
"_index" : "blog",
"_type" : "_doc",
"_id" : "1",
"_version" : 1,
"found" : true,
"took" : 52,
"term_vectors" : {
"text" : {
"field_statistics" : {
"sum_doc_freq" : 8,
"doc_count" : 1,
"sum_ttf" : 8
},
"terms" : {
"个" : {
"term_freq" : 1,
"tokens" : [
{
"position" : 5,
"start_offset" : 9,
"end_offset" : 10
}
]
},
"伊拉克" : {
"term_freq" : 1,
"tokens" : [
{
"position" : 2,
"start_offset" : 4,
"end_offset" : 7
}
]
},
"吗" : {
"term_freq" : 1,
"tokens" : [
{
"position" : 7,
"start_offset" : 13,
"end_offset" : 14
}
]
},
"是" : {
"term_freq" : 1,
"tokens" : [
{
"position" : 4,
"start_offset" : 8,
"end_offset" : 9
}
]
},
"烂摊子" : {
"term_freq" : 1,
"tokens" : [
{
"position" : 6,
"start_offset" : 10,
"end_offset" : 13
}
]
},
"留给" : {
"term_freq" : 1,
"tokens" : [
{
"position" : 1,
"start_offset" : 2,
"end_offset" : 4
}
]
},
"的" : {
"term_freq" : 1,
"tokens" : [
{
"position" : 3,
"start_offset" : 7,
"end_offset" : 8
}
]
},
"美国" : {
"term_freq" : 1,
"tokens" : [
{
"position" : 0,
"start_offset" : 0,
"end_offset" : 2
}
]
}
}
}
}
}
然后我们可以通过分词向量中的词汇去索引中查找符合条件的文档了,例如:
GET blog/_search
{
"query": {
"term": {
"text": {
"value": "美国"
}
}
}
}
search_analyzer 查询分词器
查询时候的分词器。
默认情况下,如果没有配置 search_ analyzer
,则查询时,首先查看有没有 search_analyzer
,有的话,就用search_analyzer
来进行分词,如果没有,则看有没有 analyzer
,如果有,则用analyzer
来进行分词,否则使用es默认的分词器。
normalizer 规范化器
normalizer参数用于解析前(索引或者查询)的标准化配置。
比如,在es中,对于一些我们不想切分的字符串,我们通常会将其设置为 keyword
,搜索时候也是使用整个词进行搜索。如果在索引前没有做好数据清洗,导致大小写不一致,例如 louchen和 LOUCHEN,此时,我们就可以使用normalizer
在索引之前以及查询之前进行文档的标准化。
例如:新建一个索引blog,并添加字段映射,将属性title类型设置为keyword,并添加文档:
PUT blog
{
"mappings": {
"properties": {
"title":{
"type": "keyword"
}
}
}
}
POST blog/_doc/1
{
"title":"louchen"
}
POST blog/_doc/2
{
"title":"LOUCHEN"
}
然后查询索引中title为louchen的文档:
GET blog/_search
{
"query": {
"term": {
"title": "louchen"
}
}
}
可以发现,结果为只有一条文档id为1的结果。内容区分大小写。
如果我们想在索引和查询时对属性进行预处理,那么可以在创建索引的时候定义normailzer(规范化器)
,例如忽略大小写查询:
PUT blog
{
"settings": {
"analysis": {
"normalizer":{
"my_normalizer":{
"type":"custom",
"filter":["lowercase"]
}
}
}
},
"mappings": {
"properties": {
"title":{
"type": "keyword",
"normalizer": "my_normalizer"
}
}
}
}
上面的代码含义是,创建一个blog索引,然后定义一个名为my_normalizer
规范化器,类型为自定义,过滤器为全部转换为小写查询
。然后创建title
字段并指定规范化器为normalizer
。
⚠️ 字段类型的type
必须为keyword
然后我们创建文档
POST blog/_doc/1
{
"title":"louchen"
}
POST blog/_doc/2
{
"title":"LOUCHEN"
}
可以发现可以查找到文档id为1和2的两条记录。title为louchen 和 LOUCHEN 都能查到。即能将查询到参数和查询到结果统一转为小写进行查询
GET blog/_search
{
"query": {
"term": {
"title": "louchen"
}
}
}
boost 权重
boost可以设置字段的权重
boost使用的两种方式:
可以在索引定义mapping的字段属性的时候使用。一般不要在创建的文档的时候定义权重,因为无法更改。
创建blog索引,并定义title字段,boost权重为3
PUT blog { "mappings": { "properties": { "title":{ "type": "text", "boost": 3 } } } }
创建文档
POST blog/_doc { "title":"你好啊louchen 你好啊louchen 你好啊louchen 你好啊louchen 你好啊louchen 你好啊louchen 你好啊louchen 你好啊louchen 你好啊louchen 你好啊louchen " } POST blog/_doc { "title":"你好啊louchen " } POST blog/_doc { "title":"你好啊louchen 你好啊louchen 你好啊louchen " }
查询
GET blog/_search { "query": { "match": { "title":{ "query": "louchen" } } } }
结果为:匹配次数的多的文档越靠前
{ "took" : 34, "timed_out" : false, "_shards" : { "total" : 1, "successful" : 1, "skipped" : 0, "failed" : 0 }, "hits" : { "total" : { "value" : 4, "relation" : "eq" }, "max_score" : 0.18071684, "hits" : [ { "_index" : "blog", "_type" : "_doc", "_id" : "CLJFToUBraehae8eOId8", "_score" : 0.18071684, "_source" : { "title" : "你好啊louchen 你好啊louchen 你好啊louchen 你好啊louchen 你好啊louchen 你好啊louchen 你好啊louchen 你好啊louchen 你好啊louchen 你好啊louchen " } }, { "_index" : "blog", "_type" : "_doc", "_id" : "CrJFToUBraehae8eTYft", "_score" : 0.17136458, "_source" : { "title" : "你好啊louchen 你好啊louchen 你好啊louchen " } }, { "_index" : "blog", "_type" : "_doc", "_id" : "2", "_score" : 0.1700413, "_source" : { "title" : "LOUCHEN" } }, { "_index" : "blog", "_type" : "_doc", "_id" : "CbJFToUBraehae8eQ4ea", "_score" : 0.14929049, "_source" : { "title" : "你好啊louchen " } } ] } }
也可以在查询时指定字段属性的权重
GET blog/_search { "query": { "match": { "title":{ "query": "louchen", "boost": 3 } } } }
coerce 类型限制
coerce默认为true
,可以用来清除脏数据。
- 例如:索引blog定义age字段后指定类型为integer
PUT blog
{
"mappings": {
"properties": {
"age":{
"type": "integer",
"coerce": true
}
}
}
}
可以存入以下不同类型的数据均能成功:
POST blog/_doc
{
"age":10
}
POST blog/_doc
{
"age":"10.0"
}
POST blog/_doc
{
"age":10.2
}
- 但是将coerce设置为false后,指定的字段类型后,那么存入的也只能为该类型。
PUT blog
{
"mappings": {
"properties": {
"age":{
"type": "integer",
"coerce": true
}
}
}
}
存入以下内容成功:
POST blog/_doc
{
"age":10
}
POST blog/_doc
{
"age":10.0
}
存入以下内容报错:
POST blog/_doc
{
"age":10.2
}
POST blog/_doc
{
"age":"10"
}
copy_to 字段复制
将本字段的值复制到copy_to
指定的字段上
例如:定义full_content字段,将content1和content2的内容复制到full_content字段上
PUT blog { "mappings": { "properties": { "full_content":{ "type": "text", "analyzer": "ik_smart" }, "content1":{ "type": "text", "analyzer": "ik_smart", "copy_to": ["full_content"] }, "content2":{ "type": "text", "analyzer": "ik_smart", "copy_to": ["full_content"] } } } }
添加文档并查询
POST blog/_doc/1 { "content1":"hello", "content2":"world" } GET blog/_search { "query": { "match": { "full_content": "hello" } } }
doc_values 和 fielddata 排序聚合开启
es 中的搜索主要是用到倒排索引,doc_values 参数是为了加快排序、聚合操作而生的。当建立倒排索引的时候,会额外增加列式存储映射。
doc_values:
默认是开启的
,如果确定某个字段不需要排序或者不需要聚合,那么可以关闭 doc_values。大部分的字段在索引时都会生成 doc_values,除了 text。fielddata:
默认是关闭的
,若想要使一个text字段实现排序和聚合,那么在查询时会生成一个 fielddata 的数据结构,fieldata 在字段首次被聚合、排序的时候生成。
示例:
doc_values
创建
user索引
,并添加文档。然后查询usr索引
中的所有文档,并按照age字段
升序排列PUT user PUT user/_doc/1 { "age":10 } PUT user/_doc/2 { "age":8 } PUT user/_doc/3 { "age":12 } GET user/_search { "query": { "match_all": {} }, "sort": [ { "age": { "order": "asc" } } ] }
查询结果:
{ "took" : 5, "timed_out" : false, "_shards" : { "total" : 1, "successful" : 1, "skipped" : 0, "failed" : 0 }, "hits" : { "total" : { "value" : 3, "relation" : "eq" }, "max_score" : null, "hits" : [ { "_index" : "user", "_type" : "_doc", "_id" : "2", "_score" : null, "_source" : { "age" : 8 }, "sort" : [ 8 ] }, { "_index" : "user", "_type" : "_doc", "_id" : "1", "_score" : null, "_source" : { "age" : 10 }, "sort" : [ 10 ] }, { "_index" : "user", "_type" : "_doc", "_id" : "3", "_score" : null, "_source" : { "age" : 12 }, "sort" : [ 12 ] } ] } }
如果关闭该字段的
doc_values
,那么再使用该字段进行排序
或者聚合
查询的时候就会报错PUT user { "mappings": { "properties": { "age":{ "type": "integer", "doc_values": false } } } }
dynamic 动态映射
dynamic 属性有三种取值:
- true,默认值。自动添加新字段。
- false,忽略新字段(即使新加入字段,也不会添加到文档中)。
- strict,严格模式(在索引创建时定义好字段类型),若后续添加新字段或者添加的值不符合定义时的类型都会抛出异常。
例如:
PUT blog
{
"mappings": {
"dynamic":"strict",
"properties": {
"title":{
"type": "text"
},
"age":{
"type": "long"
}
}
}
}
enabled 开启字段索引
es 默认会索引所有的字段,但是有的字段可能只需要存储,不需要索引。此时可以通过 enabled 字段来控制:
示例:
新建索引user
并添加header_url字段
,并关闭该字段的索引。默认为true, 若设置为false,则不能设置字段的任意类型,否则会报错。
PUT user
{
"mappings": {
"properties": {
"header_url": {
"enabled": false
}
}
}
}
创建文档并进行搜索,发现搜索不到
PUT user/_doc/1
{
"header_url":"www.louchen.top"
}
GET user/_search
{
"query": {
"term": {
"header_url": {
"value": "www.louchen.top"
}
}
}
}
format 日期格式化
日期格式。format 可以规范日期格式,而且一次可以定义多个 format。
PUT user
{
"mappings": {
"properties": {
"birthday":{
"type": "date",
"format": "yyyy-MM-dd||yyyy-MM-dd HH:mm:ss"
}
}
}
}
PUT user/_doc/1
{
"birthday":"2022-01-01"
}
PUT user/_doc/2
{
"birthday":"2022-01-01 00:00:00"
}
- 多个日期格式之间,使用 || 符号连接,注意没有空格。
- 如果用户没有指定日期的 format,默认的日期格式是
strict_date_optional_time||epoch_mills
- 若定义好格式化的日期后,若后续添加后的文档不符合定义时的日期格式类型,那么会报错。
另外,所有的日期格式,可以在 https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-date-format.html 网址查看。
ignore_above 超长忽略
igbore_above 用于指定分词和索引的字符串最大长度,超过最大长度的话,该字段将不会被索引,这个字段只适用于 keyword 类型。
示例:
创建blog索引
并指定类型为keyword,设置ignore_above为10,代表如果储存的值长度如果超过10,那么该值将不会被索引,也就是查不到,但是可以被存储。
PUT blog
{
"mappings": {
"properties": {
"title":{
"type": "keyword",
"ignore_above": 10
}
}
}
}
只有id为1的文档才能搜索到,id为2的文档搜索不到
PUT blog/_doc/1
{
"title":"你好啊"
}
PUT blog/_doc/2
{
"title":"你好啊你好啊你好啊你好啊你好啊你好啊"
}
GET blog/_search
{
"query": {
"term": {
"title": {
"value": "你好啊"
}
}
}
}
GET blog/_search
{
"query": {
"term": {
"title": {
"value": "你好啊你好啊你好啊你好啊你好啊你好啊"
}
}
}
}
ignore_malformed 不规则忽略
ignore_malformed 可以忽略不规则的数据,该参数默认为 false。
示例:
创建user索引,并创建age字段的类型为integer,如添加的文档类型为不符合则会报错。
PUT user
{
"mappings": {
"properties": {
"age":{
"type": "integer"
}
}
}
}
PUT user/_doc/1
{
"age":"aa"
}
设置ignore_malformed
为true
,如果添加的文档不符合定义时的类型,那么不会报错,也会存储,但是不能被索引和查找。
PUT user
{
"mappings": {
"properties": {
"age":{
"type": "integer",
"ignore_malformed": true
}
}
}
}
PUT user/_doc/1
{
"age":"aa"
}
include_in_all 作废(copy_to替代)
这个是针对 _all
字段的,但是在 es7 中,该字段已经被废弃了。
index 是否被索引
index 属性指定一个字段是否被索引,默认为true
。该属性为 true 表示字段被索引,false 表示字段不被索引。
示例:
创建user索引,并定定义age
属性,类型为integer,index为false,即不能被索引。添加文档后,使用使用age进行搜索,则报错
PUT user
{
"mappings": {
"properties": {
"age":{
"type": "integer",
"index": false
}
}
}
}
PUT user/_doc/1
{
"age":10
}
GET user/_search
{
"query": {
"term": {
"age": {
"value": "10"
}
}
}
}
搜索报错
{
"error" : {
"root_cause" : [
{
"type" : "query_shard_exception",
"reason" : "failed to create query: Cannot search on field [age] since it is not indexed.",
"index_uuid" : "355g47LKScmAu6dNhJ9_YA",
"index" : "user"
}
],
"type" : "search_phase_execution_exception",
"reason" : "all shards failed",
"phase" : "query",
"grouped" : true,
"failed_shards" : [
{
"shard" : 0,
"index" : "user",
"node" : "IY19ZZsdT1-Ab9nGQflPbw",
"reason" : {
"type" : "query_shard_exception",
"reason" : "failed to create query: Cannot search on field [age] since it is not indexed.",
"index_uuid" : "355g47LKScmAu6dNhJ9_YA",
"index" : "user",
"caused_by" : {
"type" : "illegal_argument_exception",
"reason" : "Cannot search on field [age] since it is not indexed."
}
}
}
]
},
"status" : 400
}
index_options 索引存储选项
index_options 控制索引时哪些信息被存储到倒排索引中(用在 text 字段中),有四种取值:
norms 字段评分
norms 对字段评分有用,text 默认开启 norms,如果不是特别需要,不要开启 norms。
null_value 空值搜索
在 es 中,值为 null 的字段不索引也不可以被搜索,null_value 可以让值为 null 的字段显式的可索引、可搜索。
示例:
添加索引user,并定义name属性,并且type的类型要和null_value的类型保持一致,并且不能为text类型。null_value不能为空字符串。
PUT user
{
"mappings": {
"properties": {
"name":{
"type": "keyword",
"null_value": "name_null"
}
}
}
}
添加文档时需要显式指定name的值为null
PUT user/_doc/1
{
"age":10,
"name": null
}
查询时可通过name字段定义的null_value的值进行查找即可查找到name为null的文档
GET user/_search
{
"query": {
"term": {
"name": {
"value": "name_null"
}
}
}
}
结果:
{
"took" : 7,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 1,
"relation" : "eq"
},
"max_score" : 0.2876821,
"hits" : [
{
"_index" : "user",
"_type" : "_doc",
"_id" : "1",
"_score" : 0.2876821,
"_source" : {
"age" : 10,
"name" : null
}
}
]
}
}
position_increment_gap 短语间隙
被解析的 text 字段会将 term 的位置考虑进去,目的是为了支持近似查询和短语查询,当我们去索引一个含有多个值的 text 字段时,会在各个值之间添加一个假想的空间,将值隔开,这样就可以有效避免一些无意义的短语匹配,间隙大小通过 position_increment_gap
来控制,默认是 100
。
示例:
新建user索引,并创建文档。搜索时发现搜索不到,因为每个短语直接默认有100的间隙
PUT user/_doc/1
{
"name":["zhang san","li si"]
}
GET user/_search
{
"query": {
"match_phrase": {
"name": "san li"
}
}
}
- 在查询时,
slop
指定溢出值大于等于100时,可以查到:
GET user/_search
{
"query": {
"match_phrase": {
"name": {
"query": "san li",
"slop": 100
}
}
}
}
- 在索引定义时指定,
position_increment_gap
指定间隙值为0,可以查到:
PUT user
{
"mappings": {
"properties": {
"name": {
"type": "text",
"position_increment_gap": 0
}
}
}
}
PUT user/_doc/1
{
"name":["zhang san","li si"]
}
GET user/_search
{
"query": {
"match_phrase": {
"name": {
"query": "san li"
}
}
}
}
properties 属性定义
similarity 评分模型
similarity 指定文档的评分模型,默认有三种:
store 字段存储
默认情况下,字段会被索引,也可以搜索,但是不会存储,虽然不会被存储的,但是 _source
中有一个字段的备份。如果想将字段存储下来,可以通过配置 store 来实现。
term_vectors 分词器向量
term_vectors 是通过分词器产生的信息,包括:
- 一组 terms
- 每个 term 的位置
- term 的首字符/尾字符与原始字符串原点的偏移量
term_vectors 取值:
fields 字段索引方式
fields 参数可以让同一字段有多种不同的索引方式。例如:
PUT blog
{
"mappings": {
"properties": {
"title":{
"type": "text",
"fields": {
"raw":{
"type":"keyword"
}
}
}
}
}
}
PUT blog/_doc/1
{
"title":"javaboy"
}
GET blog/_search
{
"query": {
"term": {
"title.raw": "javaboy"
}
}
}
参考文档
https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-params.html
ElasticSearch 映射模版
es 中有动态映射,但是有的时候默认的动态映射规则并不能满足我们的需求,这个时候可以通过映射模版来解决。
示例1:
例如:直接创建user索引并添加文档,那么age默认动态映射到字段类型就是long
POST user/_doc/1
{
"age":10
}
如果想要创建的age值为integer类型的话:
定义模版名为longToInteger
,match_mapping_type
指定为要匹配的类型为long
,并将类型转换为integer
PUT user
{
"mappings": {
"dynamic_templates": [
{
"longToInteger": {
"match_mapping_type": "long",
"mapping": {
"type": "integer"
}
}
}
]
}
}
POST user/_doc/1
{
"age":10
}
示例2:
类型为string类型
的才会进入名为stringTolong
的模版中转换
- 类型匹配成功后,只有以
num_
开头的任意字段,那么会进行匹配,并切如果类型转换失败则会抛出错误。如果不匹配也不会进行转换。 - 是
string
类型并且以_text
结尾的字段不会进行转换
PUT user
{
"mappings": {
"dynamic_templates": [
{
"stringTolong": {
"match_mapping_type": "string",
"match":"num_*",
"unmatch":"*_text",
"mapping": {
"type": "integer"
}
}
}
]
}
}
ElasticSearch 搜索数据导入
创建索引:
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
ElasticSearch 搜索入门
搜索分为两个过程:
- 当向索引中保存文档时,默认情况下,es 会保存两份内容,一份是
_source
中的数据,另一份则是通过分词、排序等一系列过程生成的倒排索引文件,倒排索引中保存了词项和文档之间的对应关系。 - 搜索时,当 es 接收到用户的搜索请求之后,就会去倒排索引中查询,通过的倒排索引中维护的倒排记录表找到关键词对应的文档集合,然后对文档进行评分、排序、高亮等处理,处理完成后返回文档。
查询所有文档信息
GET books/_search
或者
GET books/_search
{
"query": {
"match_all": {}
}
}
结果:
- hits包含查询结果
- total中的value为总数
- 默认查询是10条记录
{
"took" : 8,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 888,
"relation" : "eq"
},
"max_score" : 1.0,
"hits" : [
{
"_index" : "books",
"_type" : "_doc",
"_id" : "1",
"_score" : 1.0,
"_source" : {
"name" : "普通高等教育“十一五”国家级规划教材:简明无机化学",
"publish" : "高等教育出版社",
"type" : "大学教材",
"author" : [
"宋天佑"
],
"info" : "《简明无机化学》为普通高等教育“十一五”国家级规划教材。全书分十五章,精选化学原理和元素化学中最重要的知识奉献给火学一年级的新生。《简明无机化学》内容简明,刻意体现课堂教学的适用性。《简明无机化学》可供70~100学时的课堂教学使用,可作为综合大学化学类各专业的无机化学教材和普通化学教材,亦可作为其他高等院校相关专业的教学参考书。",
"price" : 39
}
}
}
]
}
}
词项查询
即 term 查询,就是根据词去查询,查询指定字段中包含给定单词的文档,term 查询
不被解析,也就是说要查询的词不会进行分词,只有搜索的词和文档中的分词精确匹配,才会返回文档。应用场景如:人名、地名等等。
例如:查询 name 字段中包含 十一五 的文档。
GET books/_search
{
"query": {
"term": {
"name": {
"value": "十一五"
}
}
}
}
分页查询
默认返回前 10 条数据,es 中也可以像关系型数据库一样,给一个分页参数:
- 从第一条记录开始查询2条记录
- from:索引从0开始,表示第一条。若为1,则代表从第二条开始查询(包含第二条)
- size:要查询的条数
GET books/_search
{
"query": {
"term": {
"name": {
"value": "十一五"
}
}
},
"size": 2,
"from": 0
}
过滤返回字段
如果返回的字段比较多,又不需要这么多字段,此时可以指定返回的字段:
- 查询name包含
十一五
的文档,_source
指定返回的文档只包含name
和author
字段。从第一条开始查询两条记录。
GET books/_search
{
"query": {
"term": {
"name": {
"value": "十一五"
}
}
},
"size": 2,
"from": 0,
"_source": ["name","author"]
}
结果:
{
"took" : 29,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 142,
"relation" : "eq"
},
"max_score" : 1.7939949,
"hits" : [
{
"_index" : "books",
"_type" : "_doc",
"_id" : "7",
"_score" : 1.7939949,
"_source" : {
"author" : [ ],
"name" : "普通高等教育“十一五”国家级规划教材:体育游戏"
}
},
{
"_index" : "books",
"_type" : "_doc",
"_id" : "444",
"_score" : 1.7939949,
"_source" : {
"author" : [
"李文联",
"杨绍先"
],
"name" : "全国高职高专教育“十一五”规划教材:摄影摄像基础"
}
}
]
}
}
最小评分
有的文档得分特别低,说明这个文档和我们查询的关键字相关度很低。我们可以设置一个最低分,只有得分超过最低分的文档才会被返回。
- min_socre指定的
最小分数
。即文档的得分如果小于1.75,则不会被查询出来
GET books/_search
{
"query": {
"term": {
"name": {
"value": "十一五"
}
}
},
"min_score":1.75,
"size": 2,
"from":0,
"_source": ["name","author"]
}
高亮显示
查询关键字高亮:
- highlight的fields中指定高亮的字段,例如以下指定
name
中包含的查询字段高亮
GET books/_search
{
"query": {
"term": {
"name": {
"value": "十一五"
}
}
},
"min_score":1.75,
"size": 2,
"from":0,
"_source": ["name","author"],
"highlight": {
"fields": {
"name": {}
}
}
}
结果:
- 高亮字段被
<em>「查询的内容」</em>
字段包裹
{
"took" : 85,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 7,
"relation" : "eq"
},
"max_score" : 1.7939949,
"hits" : [
{
"_index" : "books",
"_type" : "_doc",
"_id" : "7",
"_score" : 1.7939949,
"_source" : {
"author" : [ ],
"name" : "普通高等教育“十一五”国家级规划教材:体育游戏"
},
"highlight" : {
"name" : [
"普通高等教育“<em>十一五</em>”国家级规划教材:体育游戏"
]
}
},
{
"_index" : "books",
"_type" : "_doc",
"_id" : "444",
"_score" : 1.7939949,
"_source" : {
"author" : [
"李文联",
"杨绍先"
],
"name" : "全国高职高专教育“十一五”规划教材:摄影摄像基础"
},
"highlight" : {
"name" : [
"全国高职高专教育“<em>十一五</em>”规划教材:摄影摄像基础"
]
}
}
]
}
}
ElasticSearch 全文查询(分词查询)
match query 查询分词匹配
match_query
会对要查询的词进行分词,如果查询的词分词后,只要有任意其中一个词语被匹配到,那么都会查询出来。
例如要查询的词语:十一五计算机
,我们对要查询的词语进行分词查看:
GET books/_analyze
{
"text": "十一五计算机",
"analyzer": "ik_max_word"
}
结果:
{
"tokens" : [
{
"token" : "十一五",
"start_offset" : 0,
"end_offset" : 3,
"type" : "CN_WORD",
"position" : 0
},
{
"token" : "十一",
"start_offset" : 0,
"end_offset" : 2,
"type" : "CN_WORD",
"position" : 1
},
{
"token" : "五",
"start_offset" : 2,
"end_offset" : 3,
"type" : "CN_CHAR",
"position" : 2
},
{
"token" : "计算机",
"start_offset" : 3,
"end_offset" : 6,
"type" : "CN_WORD",
"position" : 3
},
{
"token" : "计算",
"start_offset" : 3,
"end_offset" : 5,
"type" : "CN_WORD",
"position" : 4
},
{
"token" : "算机",
"start_offset" : 4,
"end_offset" : 6,
"type" : "CN_WORD",
"position" : 5
}
]
}
要查询的词语分词出来的有以上结果。
- 查询name中包含
十一五计算机
分词后的文档中name
包含任意一个分词的所有文档。也就是or
的关系 - 例如:文档中的name字段只要包含
十一五
或者计算机
的文档都会被查询出来
GET books/_search
{
"query": {
"match": {
"name": "十一五计算机"
}
}
}
若查询的文档想要and关系,也就时查询分词后,查询的字段内容要包含所有的分词内容。也就是and关系
例如:文档中的name字段必须要同时包含
十一五
和计算机
的文档的才会被查询出来
GET books/_search
{
"query": {
"match": {
"name": {
"query": "十一五计算机",
"operator": "and"
}
}
}
}
minmum_should_match 最小匹配数
查询的词进行分词后,若只需要指定满足最小匹配多少个,那么可以使用minmum_should_match
指定查询分词后最小匹配的个数
例如:
- 从上面可以得知,查询的词语
十一五计算机
进行分词后有6
个词语,那么可以指定minimum_should_match
指定需要匹配的最少词语个数。 - 若
minimum_should_match
为6,那么等价于"operator": "and"
- 也可以指定
minimum_should_match
为50%
,代表至少匹配一般的词语
- 也可以指定
GET books/_search
{
"query": {
"match": {
"name": {
"query": "十一五计算机",
"minimum_should_match": 6
}
}
}
}
等价于,所有的查询分词进行的bool词语查询的结果
GET books/_search
{
"query": {
"bool": {
"should": [
{
"term": {
"name": {
"value": "十一五"
}
}
},
{
"term": {
"name": {
"value": "十一"
}
}
},
{
"term": {
"name": {
"value": "五"
}
}
},
{
"term": {
"name": {
"value": "计算机"
}
}
},
{
"term": {
"name": {
"value": "计算"
}
}
},
{
"term": {
"name": {
"value": "算机"
}
}
}
]
}
}
}
match_phrase query 查询分词匹配
match_phrase query 也会对查询的关键字
进行分词
,但是它分词后有两个特点:
- 查询的语句分词后所有的分词都必须出现在文档中
- 分词后的词项顺序必须和文档中词项的顺序一致
示例:
要查询的词计算机应用
被分为 计算机
和应用
两个词,那么这两个词必须连续和都要出现在name
字段中的文档才能查询出来
GET books/_search
{
"query": {
"match_phrase": {
"name": "计算机应用"
}
}
}
- 若想要出现的分词不连续也可以,那么可以指定
slop
即溢出范围,指定的是对文档中的语句进行分词后的position
的位置。若小于的position的位置则不会查出来,超过position才会被查询出来。但是注意不是关键之间间隔的字数。文档中的字段被分词器解析之后,解析出来的词项都包含一个 position 字段表示词项的位置
GET books/_search
{
"query": {
"match_phrase": {
"name":{
"query": "计算机应用",
"slop": 10
}
}
}
}
结果:name中的内容被进行分词,其中最小分词position距离为10才能被查询出来。
"name" : "计算机基础课程系列教材:数据库技术及应用 SQL Server"
match_phrase_prefix query 前缀匹配查询
这个类似于 match_phrase query,只不过这里多了一个通配符,match_phrase_prefix 支持最后一个词项的前缀匹配,但是由于这种匹配方式效率较低,因此大家作为了解即可。
示例:
这个查询过程,会自动进行单词匹配,会自动查找以计开始的单词,默认是 50 个,通过max_expansions
指定最大的匹配的单词数
。
GET books/_search
{
"query": {
"match_phrase_prefix": {
"name": {
"query": "计",
"max_expansions": 1
}
}
}
}
match_phrase_prefix 是针对分片级别的查询,假设 max_expansions 为 1,可能返回多个文档,但是只有一个词,这是我们预期的结果。
有的时候实际返回结果和我们预期结果并不一致,原因在于这个查询是分片级别的,不同的分片确实只返回了一个词,但是结果可能来自不同的分片,所以最终会看到多个词。
multi_match query 查询域
match 查询的升级版,可以指定多个查询域:
例如:
- 查询
name
字段或者info
字段中包含Java
的文档
GET books/_search
{
"query": {
"multi_match": {
"query": "Java",
"fields": ["name","info"]
}
}
}
- 查询
info
字段中的关键字是name
中4倍
权重的文档
GET books/_search
{
"query": {
"multi_match": {
"query": "计算机",
"fields": ["name","info^4"]
}
}
}
query_string query 查询分词匹配
query_string 是一种紧密结合 Lucene 的查询方式,在一个查询语句中可以用到 Lucene 的一些查询语法:
例如:
- 查询name字段中包含
十一五
并且包含计算机
的文档 - and:并且
- or: 或者
- not: 不包含
GET books/_search
{
"query": {
"query_string": {
"default_field": "name",
"query": "(十一五) AND (计算机)"
}
}
}
simple_query_string 查询分词匹配
这个是 query_string 的升级,可以直接使用 +、|、- 代替 AND、OR、NOT 等。
- 查询结果和上面的结果一致
GET books/_search
{
"query": {
"simple_query_string": {
"query": "(十一五) + (计算机)",
"fields": ["name"]
}
}
}
ElasticSearch 词项查询
term query 单词项查询
词项查询。词项查询不会分析查询字符,直接拿查询字符去倒排索引中比对。
- 查询name字段包含
基础教程
的文档。不会对查询的进行分词
GET books/_search
{
"query": {
"term": {
"name": {
"value": "基础教程"
}
}
}
}
terms query 多词项查询
可以指定多个词项查询,也不会进行对查询的词语进行分词,为或者关系。
- 查询name中包含
程序设计
或者Java
的文档
GET books/_search
{
"query": {
"terms": {
"name": [
"程序设计",
"Java"
]
}
}
}
range query 范围查询
范围查询,可以按照日期范围、数字范围等查询。
range query 中的参数主要有四个:
- gt 大于
- lt 小于
- gte 大于等于
- lte 小于等于
例如:
- 查询price在10到20之间的文档,并按照price降序排列。
GET books/_search
{
"query": {
"range": {
"price": {
"gte": 10,
"lte": 20
}
}
},
"sort": [
{
"price": {
"order": "desc"
}
}
]
}
exists query 是否为空查询
exists query 会返回指定字段中至少有一个非空值的文档:
- 查询info字段不为null的文档。若info为空字符串,那么也算不为null.
GET books/_search
{
"query": {
"exists": {
"field": "info"
}
}
}
prefix query 前缀查询
前缀查询,效率略低,除非必要,一般不太建议使用。
给定关键词的前缀去查询。
- 查询
name
中包含以计
开头的文档
GET books/_search
{
"query": {
"prefix": {
"name": {
"value": "计"
}
}
}
}
wildcard query 通配符查询
wildcard query 即通配符查询。支持单字符和多字符通配符:
?
表示一个任意字符。*
表示零个或者多个字符。
例如:
- 查询出
author
字段中以李
开头的名字
GET books/_search
{
"query": {
"wildcard": {
"author": {
"value": "李*"
}
}
}
}
- 查询出
author
字段中以李
开头后面只有一个字符
的名字
GET books/_search
{
"query": {
"wildcard": {
"author": {
"value": "李?"
}
}
}
}
regexp query 正则表达式
支持正则表达式查询。
查询所有姓张并且名字只有两个字的作者的书:
GET books/_search
{
"query": {
"regexp": {
"author": "张."
}
}
}
fuzzy query 模糊查询
在实际搜索中,有时我们可能会打错字,从而导致搜索不到,在 match query 中,可以通过 fuzziness 属性实现模糊查询。
fuzzy query 返回与搜索关键字相似的文档。怎么样就算相似?以LevenShtein 编辑距离为准。编辑距离是指将一个字符变为另一个字符所需要更改字符的次数,更改主要包括四种:
- 更改字符(javb --> java)
- 删除字符(javva --> java)
- 插入字符(jaa --> java)
- 转置字符(ajva --> java)
为了找到相似的词,模糊查询会在指定的编辑距离中创建搜索关键词的所有可能变化或者扩展的集合,然后进行搜索匹配。
- 能够查找
name
字段中包含java
的文档
GET books/_search
{
"query": {
"fuzzy": {
"name": "jaav"
}
}
}
ids query 根据id查询
GET books/_search
{
"query": {
"ids":{
"values": [1,2]
}
}
}
结果:查询的是以_id
字段值
{
"took" : 8,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 2,
"relation" : "eq"
},
"max_score" : 1.0,
"hits" : [
{
"_index" : "books",
"_type" : "_doc",
"_id" : "1",
"_score" : 1.0,
"_source" : {
"name" : "普通高等教育“十一五”国家级规划教材:简明无机化学",
"publish" : "高等教育出版社",
"type" : "大学教材",
"author" : [
"宋天佑"
],
"info" : "《简明无机化学》为普通高等教育“十一五”国家级规划教材。全书分十五章,精选化学原理和元素化学中最重要的知识奉献给火学一年级的新生。《简明无机化学》内容简明,刻意体现课堂教学的适用性。《简明无机化学》可供70~100学时的课堂教学使用,可作为综合大学化学类各专业的无机化学教材和普通化学教材,亦可作为其他高等院校相关专业的教学参考书。",
"price" : 39
}
},
{
"_index" : "books",
"_type" : "_doc",
"_id" : "2",
"_score" : 1.0,
"_source" : {
"name" : "测试商品",
"publish" : "农村读物出版社",
"type" : "小学通用",
"author" : [ ],
"info" : "",
"price" : 5
}
}
]
}
}
ElasticSearch 复合查询
constant_score query 忽略分数查询
当我们不关心检索词项的频率(TF)对搜索结果排序的影响时,可以使用 constant_score
将查询语句或者过滤语句包裹起来。
- 查询出name中包含
程序设计
的文档,会忽略文档的分数值,即所有的文档分数都是boost
指定的固定值
GET books/_search
{
"query": {
"constant_score": {
"filter": {
"term": {
"name": "程序设计"
}
},
"boost": 1.2
}
}
}
bool query 布尔条件查询
bool query 可以将任意多个简单查询组装在一起,有四个关键字可供选择,四个关键字所描述的条件可以有一个或者多个。
- must:文档必须匹配 must 选项下的查询条件。
- should:文档可以匹配 should 下的查询条件,也可以不匹配。
- must_not:文档必须不满足 must_not 选项下的查询条件。
- filter:类似于 must,但是 filter 不评分,只是过滤数据。
例如:
- 查询name字段中包含java,并且pirce不在10到30之间,并且price在30到40之间,type字段无论是否满足为111都可以的文档
GET books/_search
{
"query": {
"bool": {
"must": [
{
"term": {
"name": {
"value": "java"
}
}
}
],
"must_not": [
{
"range": {
"price": {
"gte": 10,
"lte": 30
}
}
}
],
"filter": [
{
"range": {
"price": {
"gte": 30,
"lte": 40
}
}
}
],
"should": [
{
"term": {
"type": {
"value": "111"
}
}
}
]
}
}
}
dis_max query 分离最大化查询
假设现在有两本书:
PUT blog
{
"mappings": {
"properties": {
"title":{
"type": "text",
"analyzer": "ik_max_word"
},
"content":{
"type": "text",
"analyzer": "ik_max_word"
}
}
}
}
POST blog/_doc
{
"title":"如何通过Java代码调用ElasticSearch",
"content":"松哥力荐,这是一篇很好的解决方案"
}
POST blog/_doc
{
"title":"初识 MongoDB",
"content":"简单介绍一下 MongoDB,以及如何通过 Java 调用 MongoDB,MongoDB 是一个不错 NoSQL 解决方案"
}
现在假设搜索 Java解决方案 关键字,但是不确定关键字是在 title 还是在 content,所以两者都搜索:
GET blog/_search
{
"query": {
"bool": {
"should": [
{
"match": {
"title": "java解决方案"
}
},
{
"match": {
"content": "java解决方案"
}
}
]
}
}
}
搜索结果如下:
{
"took" : 17,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 2,
"relation" : "eq"
},
"max_score" : 1.1972204,
"hits" : [
{
"_index" : "blog",
"_type" : "_doc",
"_id" : "JdKydoUBdflL4IM45uJ6",
"_score" : 1.1972204,
"_source" : {
"title" : "如何通过Java代码调用ElasticSearch",
"content" : "松哥力荐,这是一篇很好的解决方案"
}
},
{
"_index" : "blog",
"_type" : "_doc",
"_id" : "J9KydoUBdflL4IM47eIz",
"_score" : 1.1069256,
"_source" : {
"title" : "初识 MongoDB",
"content" : "简单介绍一下 MongoDB,以及如何通过 Java 调用 MongoDB,MongoDB 是一个不错 NoSQL 解决方案"
}
}
]
}
}
肉眼观察,感觉第二个和查询关键字相似度更高,但是实际查询结果并非这样。
要理解这个原因,我们需要来看下 should query 中的评分策略:
- 首先会执行 should 中的两个查询
- 对两个查询结果的评分求和
- 对求和结果乘以匹配语句总数
- 在对第三步的结果除以所有语句总数
反映到具体的查询中:
前者
- title 中 包含 java,假设评分是 1.1
- content 中包含解决方案,假设评分是 1.2
- 有得分的 query 数量,这里是 2
- 总的 query 数量也是 2
最终结果:(1.1+1.2)*2/2=2.3
后者
- title 中 不包含查询关键字,没有得分
- content 中包含解决方案和 java,假设评分是 2
- 有得分的 query 数量,这里是 1
- 总的 query 数量也是 2
最终结果:2*1/2=1
在这种查询中,title 和 content 相当于是相互竞争的关系,所以我们需要找到一个最佳匹配字段。
为了解决这一问题,就需要用到 dis_max query(disjunction max query,分离最大化查询):匹配的文档依然返回,但是只将最佳匹配的评分作为查询的评分。
GET blog/_search
{
"query": {
"dis_max": {
"queries": [
{
"match": {
"title": "java解决方案"
}
},
{
"match": {
"content": "java解决方案"
}
}
]
}
}
}
查询结果如下:
{
"took" : 9,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 2,
"relation" : "eq"
},
"max_score" : 1.1069256,
"hits" : [
{
"_index" : "blog",
"_type" : "_doc",
"_id" : "J9KydoUBdflL4IM47eIz",
"_score" : 1.1069256,
"_source" : {
"title" : "初识 MongoDB",
"content" : "简单介绍一下 MongoDB,以及如何通过 Java 调用 MongoDB,MongoDB 是一个不错 NoSQL 解决方案"
}
},
{
"_index" : "blog",
"_type" : "_doc",
"_id" : "JdKydoUBdflL4IM45uJ6",
"_score" : 0.62177753,
"_source" : {
"title" : "如何通过Java代码调用ElasticSearch",
"content" : "松哥力荐,这是一篇很好的解决方案"
}
}
]
}
}
在 dis_max query 中,还有一个参数 tie_breaker
(取值在0~1),在 dis_max query 中,是完全不考虑其他 query 的分数,只是将最佳匹配的字段的评分返回。但是,有的时候,我们又不得不考虑一下其他 query 的分数,此时,可以通过 tie_breaker
来优化 dis_max query。tie_breaker
会将其他 query 的分数,乘以 tie_breaker
,然后和分数最高的 query 进行一个综合计算。
function_score query 自定义分数规则查询
场景:例如想要搜索标题包含java
的文档,搜索的关键字是java
,但是我希望能够将投票较高的java相关文档优先展示出来,也就是需要关联其他字段查询。但是默认的评分策略是没有办法考虑到java相关文档评分的,他只是考虑相关性,这个时候可以通过 function_score query 来实现。
准备两条测试数据:
PUT blog
{
"mappings": {
"properties": {
"title":{
"type": "text",
"analyzer": "ik_max_word"
},
"votes":{
"type": "integer"
}
}
}
}
PUT blog/_doc/1
{
"title":"Java集合详解",
"votes":100
}
PUT blog/_doc/2
{
"title":"Java多线程详解,Java锁详解",
"votes":10
}
现在搜索标题中包含 java 关键字的文档:
结果如下:
"hits" : [
{
"_index" : "blog",
"_type" : "_doc",
"_id" : "2",
"_score" : 0.22534126,
"_source" : {
"title" : "Java多线程详解,Java锁详解",
"votes" : 10
}
},
{
"_index" : "blog",
"_type" : "_doc",
"_id" : "1",
"_score" : 0.21799318,
"_source" : {
"title" : "Java集合详解",
"votes" : 100
}
}
]
可以看到默认是按照java
匹配的关键字的次数来决定分数的
这里希望能够充分考虑 votes 字段,将 votes 较高的文档优先展示,就可以通过 function_score 来实现。
具体的思路,就是在旧的得分基础上,根据 votes 的数值进行综合运算,重新得出一个新的评分。
具体有几种不同的计算方式:
- weight
- random_score
- script_score
- field_value_factor
weight 权重
weight 可以对评分设置权重,就是在旧的评分基础上乘以 weight,他其实无法解决我们上面所说的问题。具体用法如下:
GET blog/_search
{
"query": {
"function_score": {
"query": {
"match": {
"title": "java"
}
},
"functions": [
{
"weight": 10
}
]
}
}
}
查询结果如下:
- 即在原来的得分情况下
*10
"hits" : [
{
"_index" : "blog",
"_type" : "_doc",
"_id" : "2",
"_score" : 2.2534127,
"_source" : {
"title" : "Java多线程详解,Java锁详解",
"votes" : 10
}
},
{
"_index" : "blog",
"_type" : "_doc",
"_id" : "1",
"_score" : 2.1799319,
"_source" : {
"title" : "Java集合详解",
"votes" : 100
}
}
]
random_score 随机分数
random_score
会根据 uid 字段进行 hash 运算,生成分数,使用 random_score
时可以配置一个种子,如果不配置,默认使用当前时间。
- 每次查询的分数会不一致,但是匹配度高(匹配的次数多)的文档永远比匹配度低的分数大。
GET blog/_search
{
"query": {
"function_score": {
"query": {
"match": {
"title": "java"
}
},
"functions": [
{
"random_score": {}
}
]
}
}
}
script_score 自定义评分脚本(常用)
自定义评分脚本。假设每个文档的最终得分是旧的分数加上votes。查询方式如下:
- 默认得分的计算方式为:
(oldScore旧的分数+votes的值)*oldScore旧的分数
GET blog/_search
{
"query": {
"function_score": {
"query": {
"match": {
"title": "java"
}
},
"functions": [
{
"script_score": {
"script": {
"lang": "painless",
"source": "_score + doc['votes'].value"
}
}
}
]
}
}
}
boost_mode 计算方式
如果不想乘以 oldScore
,可以通过boost_mode
参数配置处理方式:
- multiply:分数相乘
- sum:分数相加
- avg:求平均数
- max:最大分
- min:最小分
- replace:不进行二次计算
则计算方式:oldScore旧的分数+votes的值
GET blog/_search
{
"query": {
"function_score": {
"query": {
"match": {
"title": "java"
}
},
"functions": [
{
"script_score": {
"script": {
"lang": "painless",
"source": "_score + doc['votes'].value"
}
}
}
],
"boost_mode": "replace"
}
}
}
field_value_factor 自定义计算方式(常用)
这个的功能类似于 script_score
,但是不用自己写脚本。
假设每个文档的最终得分是旧的分数乘以votes。查询方式如下:
- 分数计算结果为:
oldScore*votes
默认 - 这里也可以设置
boost_mode
计算方式,默认值为multiply
GET blog/_search
{
"query": {
"function_score": {
"query": {
"match": {
"title": "java"
}
},
"functions": [
{
"field_value_factor": {
"field": "votes"
}
}
],
"boost_mode": "multiply"
}
}
}
还可以利用 es 内置的函数进行一些更复杂的运算:但是需要先将boost_mode
设置为replace
例如:
- 分数的结果为:
sqrt(votes)
,也就是对votes开方
的结果就是最终的得分
GET blog/_search
{
"query": {
"function_score": {
"query": {
"match": {
"title": "java"
}
},
"functions": [
{
"field_value_factor": {
"field": "votes",
"modifier": "sqrt"
}
}
],
"boost_mode": "replace"
}
}
}
另外还有个参数 factor ,影响因子。字段值先乘以影响因子,然后再进行计算。以 sqrt 为例,计算方式为 sqrt(factor*votes)
:
GET blog/_search
{
"query": {
"function_score": {
"query": {
"match": {
"title": "java"
}
},
"functions": [
{
"field_value_factor": {
"field": "votes",
"modifier": "sqrt",
"factor": 10
}
}
],
"boost_mode": "replace"
}
}
}
还有一个参数 max_boost
,指定计算结果的最大值。max_boost
参数表示 functions 模块中,最终的计算结果上限。如果超过上限,就按照上线计算。
- 计算方式:
oldScore+votes
的值,但是不会超过100+oldScore
的值
GET blog/_search
{
"query": {
"function_score": {
"query": {
"match": {
"title": "java"
}
},
"functions": [
{
"field_value_factor": {
"field": "votes"
}
}
],
"boost_mode": "sum",
"max_boost": 100
}
}
}
boosting query 降低得分查询
boosting query 中包含三部分:
- positive:得分不变
- negative:复合条件的文档降低得分
- negative_boost:降低的权重
示例:
- 查询name中包含java的文档,对于符合条件name中包含2008的文档降低得分,降低权重为0.5
- 必须指定negative_boost
- 分数结果计算方式为:
oldScore*0.5
GET books/_search
{
"query": {
"boosting": {
"positive": {
"match": {
"name": "java"
}
},
"negative": {
"match": {
"name": "2008"
}
},
"negative_boost": 0.5
}
}
}
ElasticSearch 嵌套查询
嵌套文档 nested
问题复现:有一个电影文档,每个电影都有演员信息:
- 默认创建的
actors
为text
类型。因为es会对嵌套的文档进行扁平化处理,会出现查询的问题,参考上面的ElasticSearch的嵌套类型
POST movies/_doc/1
{
"name": "人在囧途",
"actors": [
{
"name":"王宝强",
"gender":"男"
},
{
"name":"张欣艺",
"gender":"女"
}
]
}
- 查询
name
中姓王
,并且gender
为男
的电影,可以发现能够查出id为1的文档 - 但是 查询
name
中姓王
,并且gender
为女
的电影,也能够查出id为1的文档,这就是存在的问题。
GET movies/_search
{
"query": {
"bool": {
"must": [
{
"wildcard": {
"actors.name": {
"value": "王*"
}
}
},
{
"term": {
"actors.gender": {
"value": "男"
}
}
}
]
}
}
}
新建字段类型为nested的索引
- 新建
actors
字段为nested
类型
PUT movies
{
"mappings": {
"properties": {
"actors":{
"type": "nested"
}
}
}
}
那么查询的时候就不能用以上的方式进行查询,会查询不到。需要用专门的嵌套查询
但是,嵌套类型的文档也有缺点:
查看文档数量:
GET _cat/indices?
这是因为 nested 文档在 es 内部其实也是独立的 lucene 文档,只是在我们查询的时候,es 内部帮我们做了 join 处理,所以最终看起来就像一个独立文档一样。因此这种方案性能并不是特别好。
嵌套查询
上面的嵌套类型的文档需要使用专门的嵌套查询:
- 指定查询路径为
actors
,查询actors.name
的字段姓王
的,并且gender
为男
的文档。那么会查询出id为1的文档。 - 若查询的
actors.name
的字段姓王
的,并且gender
为女
的文档,那么查询结果为空。
GET movies/_search
{
"query": {
"nested": {
"path": "actors",
"query": {
"bool": {
"must": [
{
"wildcard": {
"actors.name": {
"value": "王*"
}
}
},
{
"term": {
"actors.gender": {
"value": "男"
}
}
}
]
}
}
}
}
}
父子文档
相比于嵌套文档,父子文档主要有如下优势:
- 更新父文档时,不会重新索引子文档
- 创建、修改或者删除子文档时,不会影响父文档或者其他的子文档。
- 子文档可以作为搜索结果独立返回。
例如学生和班级的关系:
- s_c 表示父子文档的关系名称,可以自定义
- join 表示这是一个父子文档
- relations 中 class:student 表示,class为父文档,student为子文档
PUT stu_class
{
"mappings": {
"properties": {
"name":{
"type": "keyword"
},
"s_c":{
"type": "join",
"relations":{
"class":"student"
}
}
}
}
}
添加父文档:
- 添加文档的同时,指定父子映射关系,表明该文档为父文档
PUT stu_class/_doc/1?routing=1
{
"name":"一班",
"s_c":{
"name":"class"
}
}
PUT stu_class/_doc/2?routing=2
{
"name":"二班",
"s_c":{
"name":"class"
}
}
添加子文档:
- 添加文档的同时,指定父子映射关系名称,映射名称中的
name
指定定义时的子文档名称,并使用parent
指定父文档的id。 - 子文档需要和父文档在同一个分片上,所以 routing 关键字的值为父文档的添加时的分片id
- 子文档都是独立的文档
PUT stu_class/_doc/3?routing=1
{
"name":"zhangsan",
"s_c":{
"name":"student",
"parent":1
}
}
PUT stu_class/_doc/4?routing=1
{
"name":"lisi",
"s_c":{
"name":"student",
"parent":1
}
}
PUT stu_class/_doc/5?routing=2
{
"name":"wangwu",
"s_c":{
"name":"student",
"parent":2
}
}
父子文档需要注意的地方:
每个索引只能定义一个 join filed
父子文档需要在同一个分片上(查询,修改需要routing)
可以向一个已经存在的 join filed 上新增关系
has_child query 子文档查询父文档
通过子文档查询父文档使用 has_child
query。
- 查询wangwu所属的父文档,也就是班级
GET stu_class/_search
{
"query": {
"has_child": {
"type": "student",
"query": {
"term": {
"name": {
"value": "wangwu"
}
}
}
}
}
}
has_parent query 父文档查询子文档
通过父文档查询子文档:
- 查询
父文档name
为一班
下的所有的子文档 - 这种查询没有评分
GET stu_class/_search
{
"query": {
"has_parent": {
"parent_type": "class",
"query": {
"term": {
"name": {
"value": "一班"
}
}
}
}
}
}
- 通过父文档id查询该父文档下的所有子文档
- 这种查询有得分
GET stu_class/_search
{
"query": {
"parent_id":{
"type":"student",
"id":"1"
}
}
}
小结
整体上来说:
父子文档:普通子对象实现一对多,会损失子文档的边界,子对象之间的属性关系丢失。
nested 可以解决第 1 点的问题,但是 nested 有两个缺点:更新主文档的时候要全部更新,不支持子文档属于多个主文档。
父子文档解决 1、2 点的问题,但是它主要适用于写多读少的场景。
ElasticSearch 地理位置查询
数据准备
创建一个索引:
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
**百度经纬度坐标查询:**http://api.map.baidu.com/lbsapi/getpoint/index.html
**百度测距查询:**https://map.baidu.com/@13225544,3740470,11.51z
geo_distance query 坐标距离范围查询
给出一个中心点,查询距离该中心点指定范围内的文档:
- 以下的
lat纬度32.068925
和lon经度118.789593
对应的坐标为南京
,即查询所有的文档中,过滤出距离南京为200km为半径
的的所有坐标对应的文档 - 结果为:
合肥
GET geo/_search
{
"query": {
"bool": {
"must": [
{
"match_all": {}
}
],
"filter": [
{
"geo_distance": {
"distance": "200km",
"location": {
"lat": 32.068925,
"lon": 118.789593
}
}
}
]
}
}
}
geo_bounding_box query 构造矩形范围查询
在某一个矩形内的点,通过两个点锁定一个矩形:
- 以(32.068925纬度,118.789593经度)的
南京
为左上角
,以(29.969629纬度,122.239086经度)的舟山
为右下角
构造一个矩形,查询矩形范围内的所有文档 - 结果为:上海、杭州
GET geo/_search
{
"query": {
"bool": {
"must": [
{
"match_all": {}
}
],
"filter": [
{
"geo_bounding_box": {
"location": {
"top_left": {
"lat": 32.068925,
"lon": 118.789593
},
"bottom_right": {
"lat": 29.969629,
"lon": 122.239086
}
}
}
}
]
}
}
}
geo_polygon query 构造多边形范围内的查询
在某一个多边形范围内的查询。
- 以(32.068925纬度,118.789593经度)的
南京
,以(34.753266纬度,113.647549)的郑州
,以(28.167502纬度,112.985246)的长沙
三个点构造一个三角形,查询三角形中所有的文档 - 查询结果为:武汉、合肥
GET geo/_search
{
"query": {
"bool": {
"must": [
{
"match_all": {}
}
],
"filter": [
{
"geo_polygon": {
"location": {
"points": [
{
"lat": 34.753266,
"lon": 113.647549
},
{
"lat": 28.167502,
"lon": 112.985246
},
{
"lat": 32.068925,
"lon": 118.789593
}
]
}
}
}
]
}
}
}
geo_shape query 图形关系查询
geo_shape
用来查询图形,针对 geo_shape
,两个图形之间的关系有:相交、包含、不相交。
relation 属性表示两个图形的关系:
- within 包含
- intersects 相交
- disjoint 不相交
新建索引:
PUT geo_shape
{
"mappings": {
"properties": {
"name":{
"type": "keyword"
},
"location":{
"type": "geo_shape"
}
}
}
}
然后添加一条线:
- type:linestring 代表为一条线,即两点确定一条线
PUT geo_shape/_doc/1
{
"name":"西安-郑州",
"location":{
"type":"linestring",
"coordinates":[
[108.9404296875,34.279914398549934],
[113.66455078125,34.768691457552706]
]
}
}
接下来查询某一个图形中是否包含该线:
GET geo_shape/_search
{
"query": {
"bool": {
"must": [
{
"match_all": {}
}
],
"filter": [
{
"geo_shape": {
"location": {
"shape": {
"type": "envelope",
"coordinates": [
[
106.5234375,
36.80928470205937
],
[
115.33447265625,
32.24997445586331
]
]
},
"relation": "within"
}
}
}
]
}
}
}
ElasticSearch 特殊查询
more_like_this query 相似内容查询
more_like_this
query 可以实现基于内容的推荐,给定一篇文章,可以查询出和该文章相似的内容。
GET books/_search
{
"query": {
"more_like_this": {
"fields": [
"info"
],
"like": "计算机是一门重要的课程,所以学好计算机非常有必要",
"min_term_freq": 2,
"max_query_terms": 25
}
}
}
查询参数解释:
fields 要匹配的字段,可以有多个
like 要匹配的文本
min_term_freq 在要查询的文本中,分词中最小的频率。例如在
like
中要查询的文本中,计算机是一门重要的课程,所以学好计算机非常有必要
:其中计算机
出现的频率为2
,所以会忽略其它频率小于2的词项,那么实际要查询的内容为计算机
max_query_terms:要查询的文本分词后最大词项数目
min_doc_freq:最小的文档频率,搜索的词,至少在多少个文档中出现,少于指定数目,该词会被忽略
max_doc_freq:最大文档频率
analyzer:分词器,默认使用查询字段的分词器
stop_words:停用词列表
minmum_should_match 最小匹配查询的分词数
script query 自定义脚本查询
自定义脚本查询
- 查询
price不等于0
,并且price大于200
,小于250
的所有文档。
GET books/_search
{
"query": {
"bool": {
"filter": [
{
"script": {
"script": {
"lang": "painless",
"source": "if(doc['price'].size()!=0){doc['price'].value > 200 && doc['price'].value < 250}"
}
}
}
]
}
}
}
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":10
}
}
}
}
}
}
最后查询:
GET log/_search
{
"query": {
"percolate": {
"field": "query",
"documents": [
{
"count":3
},
{
"count":6
},
{
"count":90
},
{
"count":12
},
{
"count":15
}
]
}
}
}
结果:
- 查询结果中会列出不满足条件的文档。
- 查询结果中的
_percolator_document_slot
字段表示文档的 position,从 0 开始计。以下结果表示索引2,3,4 会不满足条件,即文档 90、12、15
{
"took" : 1111,
"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" : 10
}
}
}
}
}
},
"fields" : {
"_percolator_document_slot" : [
2,
3,
4
]
}
}
]
}
}
ElasticSearch 搜索高亮和排序
搜索高亮
普通高亮
默认会自动添加 em 标签
:
因为
match name
会为搜索的文本进行分词。所有name中包含
计算机
和十一五
的关键字加em
加标签。
GET books/_search
{
"query": {
"match": {
"name": "计算机十一五"
}
},
"highlight": {
"fields": {
"name":{}
}
}
}
结果:
"highlight" : {
"name" : [
"普通高等教育“<em>十一五</em>”国家级规划教材:<em>计算机</em>控制系统(第2版)(配光盘)"
]
自定义高亮标签
- 为name中要搜索的关键字加自定义标签
- pre_tags:指定前置标签
- post_tags:指定后置标签
GET books/_search
{
"query": {
"match": {
"name": "计算机十一五"
}
},
"highlight": {
"fields": {
"name":{
"pre_tags": ["<strong>"],
"post_tags": ["</strong>"]
}
}
}
}
结果:
"highlight" : {
"name" : [
"普通高等教育“<strong>十一五<strong>”国家级规划教材:大学<strong>计算机<strong>基础"
]
}
指定多字段高亮
有的时候,虽然我们是在 name 字段中搜索的,但是我们希望 info 字段中,相关的关键字也能高亮:
- 指定name搜索的关键字
- 同时在name和info搜索关键字并加高亮。
- 需要
require_field_match
:false
,表示不止在name中搜索。
GET books/_search
{
"query": {
"match": {
"name": "计算机十一五"
}
},
"highlight": {
"require_field_match": "false",
"fields": {
"name":{
"pre_tags": "<strong>",
"post_tags": "<strong>"
},
"info":{
"pre_tags": "<strong>",
"post_tags": "<strong>"
}
}
}
}
结果:
"highlight" : {
"name" : [
"普通高等教育“<strong>十一五<strong>”国家级规划教材:大学<strong>计算机<strong>基础"
],
"info" : [
"《大学<strong>计算机<strong>基础》是普通高等教育“<strong>十一五<strong>”国家级规划教材,根据教育部制定的《关于进一步加强高等学校<strong>计算机<strong>基础教学的几点意见》中有关“大学<strong>计算机<strong>基础”课程的教学要求编写。",
"《大学<strong>计算机<strong>基础》共10章,分别介绍了<strong>计算机<strong>基础知识、微型<strong>计算机<strong>系统、操作系统、办公软件、<strong>计算机<strong>网络基础、程序设计基础、数据库技术基础、多媒体技术基础、<strong>计算机<strong>信息安全及社会责任和实用软件介绍。",
"《大学<strong>计算机<strong>基础》每章都由基本内容、本章小结、习题组成,习题包括思考题、单项选择题、填空题、判断题。同时在课题网站上提供了丰富的教学资源。",
"《大学<strong>计算机<strong>基础》可作为普通高等学校非<strong>计算机<strong>专业学生“大学<strong>计算机<strong>基础”课程的教学用书,也可作为广大<strong>计算机<strong>爱好者的参考用书。"
]
}
排序
默认排序
排序很简单,默认是按照查询文档的相关度来排序的,即(_score
字段):
- 默认按照
_socre
字段的分数降序来排列
GET books/_search
{
"query": {
"match": {
"name": "计算机十一五"
}
}
}
等价于:
GET books/_search
{
"query": {
"match": {
"name": "计算机十一五"
}
},
"sort": [
{
"_score": {
"order": "desc"
}
}
]
}
match_all
查询只是返回所有文档,不评分,默认按照添加顺序返回,可以通过 _doc
字段对其进行排序:
- 即下面查询会按照
_id
字段升序
进行排列
GET books/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"_doc": {
"order": "asc"
}
}
],
"size": 20
}
多字段排序
- 先按照
price
升序,再按照_id
字段降序
GET books/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"price": {
"order": "asc"
}
},
{
"_doc": {
"order": "desc"
}
}
],
"size": 20
}
ElasticSearch 指标聚合
Max Aggregation 最大值
统计最大值。例如查询价格最高的书:
price_max
为查询的自定义名称max
表示查询为最大值filed
指定查询的字段
GET books/_search
{
"aggs": {
"price_max": {
"max": {
"field": "price"
}
}
}
}
结果:
"aggregations" : {
"price_max" : {
"value" : 269.0
}
}
指定缺失字段默认值
missing
指定pirce字段缺失值,若price缺省,那么则将该文档的price设置为300
GET books/_search
{
"aggs": {
"price_max": {
"max": {
"field": "price",
"missing": 300
}
}
}
}
通过脚本查询最大值
- 若有的文档
price
不存在,则需要判断price
不为0
GET books/_search
{
"aggs": {
"price_max": {
"max": {
"script": "if(doc.price.size()!=0){doc.price.value}"
}
}
}
}
Min Aggregation 最小值
统计最小值,用法和 Max Aggregation 基本一致:
GET books/_search
{
"aggs": {
"price_min": {
"min": {
"field": "price"
}
}
}
}
结果:
"aggregations" : {
"price_min" : {
"value" : 0.0
}
}
Avg Aggregation 平均值
统计平均值:
GET books/_search
{
"aggs": {
"price_avg": {
"avg": {
"field": "price"
}
}
}
}
结果:
"aggregations" : {
"price_avg" : {
"value" : 29.40765765765766
}
}
Sum Aggregation 求和
求和:
GET books/_search
{
"aggs": {
"price_sum": {
"sum": {
"field": "price"
}
}
}
}
结果:
"aggregations" : {
"price_sum" : {
"value" : 26114.0
}
}
Cardinality Aggregation 去重总数统计
cardinality aggregation 用于基数统计。类似于 SQL 中的 distinct count(0):也就是先去重,再统计总数
text 类型是分析型类型,默认是不允许进行聚合操作的,如果相对 text 类型进行聚合操作,需要设置其 fielddata 属性为 true,这种方式虽然可以使 text 类型进行聚合操作,但是无法满足精准聚合,如果需要精准聚合,可以设置字段的子域为 keyword。
方式一:
重新定义 books 索引:
PUT books
{
"mappings": {
"properties": {
"name":{
"type": "text",
"analyzer": "ik_max_word"
},
"publish":{
"type": "text",
"analyzer": "ik_max_word",
"fielddata": true
},
"type":{
"type": "text",
"analyzer": "ik_max_word"
},
"author":{
"type": "keyword"
},
"info":{
"type": "text",
"analyzer": "ik_max_word"
},
"price":{
"type": "double"
}
}
}
}
定义完成后,重新插入数据
接下来就可以查询出版社
的总数量:
GET books/_search
{
"aggs": {
"publish_count": {
"cardinality": {
"field": "publish"
}
}
}
}
查询结果如下:
- 这种结果可能不准确,因为text类型会对其进行分词
"aggregations" : {
"publish_count" : {
"value" : 43
}
}
方式二:
可以将 publish 设置为 keyword 类型或者设置子域为 keyword。
PUT books
{
"mappings": {
"properties": {
"name":{
"type": "text",
"analyzer": "ik_max_word"
},
"publish":{
"type": "keyword"
},
"type":{
"type": "text",
"analyzer": "ik_max_word"
},
"author":{
"type": "keyword"
},
"info":{
"type": "text",
"analyzer": "ik_max_word"
},
"price":{
"type": "double"
}
}
}
}
查询结果如下:
"aggregations" : {
"publish_count" : {
"value" : 13
}
}
对比查询结果可知,使用 fileddata 的方式,查询结果不准确。
Stats Aggregation 基本数据统计
基本统计,一次性返回 count、max、min、avg、sum:
GET books/_search
{
"aggs": {
"price_stats": {
"stats": {
"field": "price"
}
}
}
}
结果:
"aggregations" : {
"price_stats" : {
"count" : 888,
"min" : 0.0,
"max" : 269.0,
"avg" : 29.40765765765766,
"sum" : 26114.0
}
}
Extends Stats Aggregation 高级统计
高级统计,比 stats 多出来:平方和、方差、标准差、平均值加减两个标准差的区间:
GET books/_search
{
"aggs": {
"price_stats_extends": {
"extended_stats": {
"field": "price"
}
}
}
}
结果:
"aggregations" : {
"price_stats_extends" : {
"count" : 888,
"min" : 0.0,
"max" : 269.0,
"avg" : 29.40765765765766,
"sum" : 26114.0,
"sum_of_squares" : 1230586.0,
"variance" : 520.984716135054,
"variance_population" : 520.984716135054,
"variance_sampling" : 521.572072072072,
"std_deviation" : 22.825089619430937,
"std_deviation_population" : 22.825089619430937,
"std_deviation_sampling" : 22.83795244920332,
"std_deviation_bounds" : {
"upper" : 75.05783689651953,
"lower" : -16.242521581204215,
"upper_population" : 75.05783689651953,
"lower_population" : -16.242521581204215,
"upper_sampling" : 75.0835625560643,
"lower_sampling" : -16.268247240748984
}
}
}
Percentiles Aggregation 百分位数据统计
百分位统计。
- 统计在 1%、5%...99%、100% 位置的数据
GET books/_search
{
"aggs": {
"percent_data_count": {
"percentiles": {
"field": "price",
"percents": [
1,
5,
10,
25,
50,
75,
95,
99,
100
]
}
}
}
}
结果:
"aggregations" : {
"percent_data_count" : {
"values" : {
"1.0" : 0.0,
"5.0" : 0.0,
"10.0" : 0.0,
"25.0" : 18.0,
"50.0" : 28.0,
"75.0" : 37.0,
"95.0" : 62.19999999999982,
"99.0" : 98.0,
"100.0" : 269.0
}
}
}
Value Count Aggregation 按照字段统计文档
可以按照字段统计文档数量(包含指定字段的文档数量):
GET books/_search
{
"aggs": {
"doc_count": {
"value_count": {
"field": "price"
}
}
}
}
结果:
"aggregations" : {
"doc_count" : {
"value" : 888
}
}
ElasticSearch 桶聚合(bucket)
Terms Aggregation 分组聚合
- ⚠️ 注意:这里的publish不能为text类型,必须改为keyword,即不能分词的类型。若为text类型则会报错
PUT books
{
"mappings": {
"properties": {
"name":{
"type": "text",
"analyzer": "ik_max_word"
},
"publish":{
"type": "keyword"
},
"type":{
"type": "text",
"analyzer": "ik_max_word"
},
"author":{
"type": "keyword"
},
"info":{
"type": "text",
"analyzer": "ik_max_word"
},
"price":{
"type": "double"
}
}
}
}
- 统计各个出版社出版的图书总数量
- 还对每个桶进行指标聚合,统计不同出版社所出版的图书的平均价格:
GET books/_search
{
"aggs": {
"publish_group_count": {
"terms": {
"field": "publish",
"size": 20
},
"aggs": {
"publish_price_avg": {
"avg": {
"field": "price"
}
}
}
}
}
}
结果:
"aggregations" : {
"publish_group_count" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "高等教育出版社",
"doc_count" : 742,
"publish_price_avg" : {
"value" : 28.579514824797844
}
},
{
"key" : "百花文艺出版社",
"doc_count" : 65,
"publish_price_avg" : {
"value" : 32.76923076923077
}
},
{
"key" : "科学出版社",
"doc_count" : 46,
"publish_price_avg" : {
"value" : 48.23913043478261
}
},
{
"key" : "新蕾出版社",
"doc_count" : 23,
"publish_price_avg" : {
"value" : 14.391304347826088
}
},
{
"key" : "",
"doc_count" : 3,
"publish_price_avg" : {
"value" : 7.0
}
},
{
"key" : "农村读物出版社",
"doc_count" : 2,
"publish_price_avg" : {
"value" : 5.0
}
},
{
"key" : "D.),(美)科达斯(Csordas",
"doc_count" : 1,
"publish_price_avg" : {
"value" : 0.0
}
},
{
"key" : "J.R)",
"doc_count" : 1,
"publish_price_avg" : {
"value" : 40.0
}
},
{
"key" : "S)",
"doc_count" : 1,
"publish_price_avg" : {
"value" : 20.0
}
},
{
"key" : "中国社会科学院出版社",
"doc_count" : 1,
"publish_price_avg" : {
"value" : 46.0
}
},
{
"key" : "科学出版社,北京科海电子出版社",
"doc_count" : 1,
"publish_price_avg" : {
"value" : 38.0
}
},
{
"key" : "科学出版社,北京科海电子电子出版社",
"doc_count" : 1,
"publish_price_avg" : {
"value" : 53.0
}
},
{
"key" : "高等教育出版社,经济日报出版社",
"doc_count" : 1,
"publish_price_avg" : {
"value" : 0.0
}
}
]
}
}
Filter Aggregation 过滤器聚合
过滤器聚合。可以将符合过滤器中条件的文档分到一个桶中,然后可以求其平均值。
- 例如查询书名中包含 java 的图书的平均价格:
GET books/_search
{
"aggs": {
"name_filter": {
"filter": {
"term": {
"name": "java"
}
},
"aggs": {
"name_filter_prive_avg": {
"avg": {
"field": "price"
}
}
}
}
}
}
结果:
"aggregations" : {
"name_filter" : {
"doc_count" : 2,
"name_filter_prive_avg" : {
"value" : 33.5
}
}
}
Filters Aggregation 多过滤器聚合
多过滤器聚合。过滤条件可以有多个。
- 例如查询书名中包含
java
或者计算机
的图书的平均价格:
GET books/_search
{
"aggs": {
"name_filter": {
"filters": {
"filters": [
{
"term": {
"name": "java"
}
},
{
"term": {
"name": "计算机"
}
}
]
},
"aggs": {
"name_filter_prive_avg": {
"avg": {
"field": "price"
}
}
}
}
}
}
结果:
"aggregations" : {
"name_filter" : {
"buckets" : [
{
"doc_count" : 2,
"name_filter_prive_avg" : {
"value" : 33.5
}
},
{
"doc_count" : 38,
"name_filter_prive_avg" : {
"value" : 25.342105263157894
}
}
]
}
}
Range Aggregation 范围聚合
按照范围聚合,在某一个范围内的文档数统计。
- 例如统计图书价格在 0-50、50-100、100-200、200-500、500以上的图书数量
- from:开始位置
- to: 结束位置(不包含此位置)
GET books/_search
{
"aggs": {
"price_range_count": {
"range": {
"field": "price",
"ranges": [
{
"to": 50
},
{
"from": 50,
"to": 100
},
{
"from": 100,
"to": 200
},
{
"from": 200,
"to": 500
},
{
"from": 500
}
]
}
}
}
}
结果:
"aggregations" : {
"price_range_count" : {
"buckets" : [
{
"key" : "*-50.0",
"to" : 50.0,
"doc_count" : 804
},
{
"key" : "50.0-100.0",
"from" : 50.0,
"to" : 100.0,
"doc_count" : 77
},
{
"key" : "100.0-200.0",
"from" : 100.0,
"to" : 200.0,
"doc_count" : 4
},
{
"key" : "200.0-500.0",
"from" : 200.0,
"to" : 500.0,
"doc_count" : 3
},
{
"key" : "500.0-*",
"from" : 500.0,
"doc_count" : 0
}
]
}
}
Date Range Aggregation 日期范围聚合
Range Aggregation 也可以用来统计日期,但是也可以使用 Date Range Aggregation,后者的优势在于可以使用日期表达式。
造数据:
PUT blog/_doc/1
{
"title":"java",
"date":"2018-12-30"
}
PUT blog/_doc/2
{
"title":"java",
"date":"2020-12-30"
}
PUT blog/_doc/3
{
"title":"java",
"date":"2022-10-30"
}
- 统计一年之前到现在时间的所有的文档
- 现在时间为 2023-01-06,那么一年之前的时间就是 2022-01-06。
- 12M/M 表示 12 个月。
- 1y/y 表示 1年。
- 1d/1d 表示 1 天。
GET blog/_search
{
"aggs": {
"date_range_query_name": {
"date_range": {
"field": "date",
"ranges": [
{
"from": "now-1y/y",
"to": "now"
}
]
}
}
}
}
结果:
- 结果只有id为3的文档
"aggregations" : {
"date_range_query_name" : {
"buckets" : [
{
"key" : "2022-01-01T00:00:00.000Z-2023-01-06T08:46:07.755Z",
"from" : 1.6409952E12,
"from_as_string" : "2022-01-01T00:00:00.000Z",
"to" : 1.672994767755E12,
"to_as_string" : "2023-01-06T08:46:07.755Z",
"doc_count" : 1
}
]
}
}
Date Histogram Aggregation 直方图数据统计
时间直方图聚合。
- 例如统计各个年份的博客数量
GET blog/_search
{
"aggs": {
"date_histogram_query": {
"date_histogram": {
"field": "date",
"interval": "year"
}
}
}
}
结果:
- 会从文档中的最小时间开始,一直到最大时间为止,每一年生成一个桶
"aggregations" : {
"date_histogram_query" : {
"buckets" : [
{
"key_as_string" : "2018-01-01T00:00:00.000Z",
"key" : 1514764800000,
"doc_count" : 1
},
{
"key_as_string" : "2019-01-01T00:00:00.000Z",
"key" : 1546300800000,
"doc_count" : 0
},
{
"key_as_string" : "2020-01-01T00:00:00.000Z",
"key" : 1577836800000,
"doc_count" : 1
},
{
"key_as_string" : "2021-01-01T00:00:00.000Z",
"key" : 1609459200000,
"doc_count" : 0
},
{
"key_as_string" : "2022-01-01T00:00:00.000Z",
"key" : 1640995200000,
"doc_count" : 1
}
]
}
}
Missing Aggregation 空值聚合
空值聚合。
- 统计所有没有 price 字段的文档:
GET books/_search
{
"aggs": {
"missing_price": {
"missing": {
"field": "price"
}
}
}
}
结果:
"aggregations" : {
"missing_price" : {
"doc_count" : 0
}
}
Children Aggregation 父子文档统计
- 查询子类型为 student 的文档数量
- 数据参考ElasticSearch嵌套查询到父子文档查询
GET stu_class/_search
{
"aggs": {
"children_count_query": {
"children": {
"type": "student"
}
}
}
}
结果:
"aggregations" : {
"children_count_query" : {
"doc_count" : 3
}
}
Geo Distance Aggregation 地理范围统计
对地理位置数据做统计。
- 例如查询(34.288991865037524,108.9404296875)坐标方圆 600KM 和 超过 600KM 的城市数量。
- 查询数据参考Elastic地理位置查询数据准备
GET geo/_search
{
"aggs": {
"geo_distance_query": {
"geo_distance": {
"field": "location",
"origin": "34.288991865037524,108.9404296875",
"unit": "km",
"ranges": [
{
"to": 600
},{
"from": 600
}
]
}
}
}
}
结果:
"aggregations" : {
"geo_distance_query" : {
"buckets" : [
{
"key" : "*-600.0",
"from" : 0.0,
"to" : 600.0,
"doc_count" : 2
},
{
"key" : "600.0-*",
"from" : 600.0,
"doc_count" : 6
}
]
}
}
IP Range Aggregation IP范围查询
IP 地址范围查询。
新建索引:
PUT blog
{
"mappings": {
"properties": {
"ip":{
"type": "ip"
}
}
}
}
PUT blog/_doc/1
{
"ip":"127.0.0.1"
}
PUT blog/_doc/2
{
"ip":"127.0.0.5"
}
PUT blog/_doc/3
{
"ip":"127.0.0.10"
}
PUT blog/_doc/4
{
"ip":"127.0.0.15"
}
- 查询ip地址范围并统计 [127.0.0.0,127.0.0.3)、[127.0.0.3,127.0.0.7)、[127.0.0.7,127.0.0.20)的范围内的ip数量
GET blog/_search
{
"aggs": {
"ip_range_query": {
"ip_range": {
"field": "ip",
"ranges": [
{
"from": "127.0.0.0",
"to": "127.0.0.3"
},
{
"from": "127.0.0.3",
"to": "127.0.0.7"
},
{
"from": "127.0.0.7",
"to": "127.0.0.20"
}
]
}
}
}
}
结果:
"aggregations" : {
"ip_range_query" : {
"buckets" : [
{
"key" : "127.0.0.0-127.0.0.3",
"from" : "127.0.0.0",
"to" : "127.0.0.3",
"doc_count" : 1
},
{
"key" : "127.0.0.3-127.0.0.7",
"from" : "127.0.0.3",
"to" : "127.0.0.7",
"doc_count" : 1
},
{
"key" : "127.0.0.7-127.0.0.20",
"from" : "127.0.0.7",
"to" : "127.0.0.20",
"doc_count" : 2
}
]
}
}
ElasticSearch 管道聚合
管道聚合相当于在之前聚合的基础上,再次聚合。
Avg Bucket Aggregation 聚合平均值
计算聚合平均值。
- 例如,统计每个出版社所出版图书的平均值,然后再统计所有出版社的平均值
- 这里查询前3个出版社每个出版社的平均值后,再统计这3个出版社的平均值
GET books/_search
{
"aggs": {
"publish_count": {
"terms": {
"field": "publish",
"size": 3
},
"aggs": {
"publish_count_avg": {
"avg": {
"field": "price"
}
}
}
},
"publish_count_avg_avg": {
"avg_bucket": {
"buckets_path": "publish_count>publish_count_avg"
}
}
}
}
结果:
"aggregations" : {
"publish_count" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 35,
"buckets" : [
{
"key" : "高等教育出版社",
"doc_count" : 742,
"publish_count_avg" : {
"value" : 28.579514824797844
}
},
{
"key" : "百花文艺出版社",
"doc_count" : 65,
"publish_count_avg" : {
"value" : 32.76923076923077
}
},
{
"key" : "科学出版社",
"doc_count" : 46,
"publish_count_avg" : {
"value" : 48.23913043478261
}
}
]
},
"publish_count_avg_avg" : {
"value" : 36.52929200960374
}
}
Max Bucket Aggregation 聚合最大值
- 统计每个出版社所出版图书的平均值,然后再统计平均值中的最大值:
GET books/_search
{
"aggs": {
"publish_count": {
"terms": {
"field": "publish",
"size": 3
},
"aggs": {
"publish_count_avg": {
"avg": {
"field": "price"
}
}
}
},
"publish_count_avg_max": {
"max_bucket": {
"buckets_path": "publish_count>publish_count_avg"
}
}
}
}
结果:
"aggregations" : {
"publish_count" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 35,
"buckets" : [
{
"key" : "高等教育出版社",
"doc_count" : 742,
"publish_count_avg" : {
"value" : 28.579514824797844
}
},
{
"key" : "百花文艺出版社",
"doc_count" : 65,
"publish_count_avg" : {
"value" : 32.76923076923077
}
},
{
"key" : "科学出版社",
"doc_count" : 46,
"publish_count_avg" : {
"value" : 48.23913043478261
}
}
]
},
"publish_count_avg_max" : {
"value" : 48.23913043478261,
"keys" : [
"科学出版社"
]
}
}
Min Bucket Aggregation 聚合最小值
- 统计每个出版社所出版图书的平均值,然后再统计平均值中的最小值:
GET books/_search
{
"aggs": {
"publish_count": {
"terms": {
"field": "publish",
"size": 3
},
"aggs": {
"publish_count_avg": {
"avg": {
"field": "price"
}
}
}
},
"publish_count_avg_min": {
"min_bucket": {
"buckets_path": "publish_count>publish_count_avg"
}
}
}
}
结果:
"aggregations" : {
"publish_count" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 35,
"buckets" : [
{
"key" : "高等教育出版社",
"doc_count" : 742,
"publish_count_avg" : {
"value" : 28.579514824797844
}
},
{
"key" : "百花文艺出版社",
"doc_count" : 65,
"publish_count_avg" : {
"value" : 32.76923076923077
}
},
{
"key" : "科学出版社",
"doc_count" : 46,
"publish_count_avg" : {
"value" : 48.23913043478261
}
}
]
},
"publish_count_avg_min" : {
"value" : 28.579514824797844,
"keys" : [
"高等教育出版社"
]
}
}
Sum Bucket Aggregation 聚合求和
- 统计每个出版社所出版图书的平均值,然后再统计平均值之和:
GET books/_search
{
"aggs": {
"publish_count": {
"terms": {
"field": "publish",
"size": 3
},
"aggs": {
"publish_count_avg": {
"avg": {
"field": "price"
}
}
}
},
"publish_count_avg_sum": {
"sum_bucket": {
"buckets_path": "publish_count>publish_count_avg"
}
}
}
}
结果:
"aggregations" : {
"publish_count" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 35,
"buckets" : [
{
"key" : "高等教育出版社",
"doc_count" : 742,
"publish_count_avg" : {
"value" : 28.579514824797844
}
},
{
"key" : "百花文艺出版社",
"doc_count" : 65,
"publish_count_avg" : {
"value" : 32.76923076923077
}
},
{
"key" : "科学出版社",
"doc_count" : 46,
"publish_count_avg" : {
"value" : 48.23913043478261
}
}
]
},
"publish_count_avg_sum" : {
"value" : 109.58787602881122
}
}
Stats Bucket Aggregation 聚合基本数据
- 统计每个出版社所出版图书的平均值,然后再统计平均值的各种数据:
GET books/_search
{
"aggs": {
"publish_count": {
"terms": {
"field": "publish",
"size": 3
},
"aggs": {
"publish_count_avg": {
"avg": {
"field": "price"
}
}
}
},
"publish_count_avg_sum": {
"stats_bucket": {
"buckets_path": "publish_count>publish_count_avg"
}
}
}
}
结果:
"aggregations" : {
"publish_count" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 35,
"buckets" : [
{
"key" : "高等教育出版社",
"doc_count" : 742,
"publish_count_avg" : {
"value" : 28.579514824797844
}
},
{
"key" : "百花文艺出版社",
"doc_count" : 65,
"publish_count_avg" : {
"value" : 32.76923076923077
}
},
{
"key" : "科学出版社",
"doc_count" : 46,
"publish_count_avg" : {
"value" : 48.23913043478261
}
}
]
},
"publish_count_avg_sum" : {
"count" : 3,
"min" : 28.579514824797844,
"max" : 48.23913043478261,
"avg" : 36.52929200960374,
"sum" : 109.58787602881122
}
}
Extended Stats Bucket Aggregation 聚合高级数据
GET books/_search
{
"aggs": {
"publish_count": {
"terms": {
"field": "publish",
"size": 3
},
"aggs": {
"publish_count_avg": {
"avg": {
"field": "price"
}
}
}
},
"publish_count_avg_sum": {
"extended_stats_bucket": {
"buckets_path": "publish_count>publish_count_avg"
}
}
}
}
结果:
"publish_count_avg_sum" : {
"count" : 3,
"min" : 28.579514824797844,
"max" : 48.23913043478261,
"avg" : 36.52929200960374,
"sum" : 109.58787602881122,
"sum_of_squares" : 4217.62485793191,
"variance" : 71.48577792107032,
"variance_population" : 71.48577792107032,
"variance_sampling" : 107.22866688160548,
"std_deviation" : 8.454926251663602,
"std_deviation_population" : 8.454926251663602,
"std_deviation_sampling" : 10.355127564719108,
"std_deviation_bounds" : {
"upper" : 53.43914451293095,
"lower" : 19.61943950627654,
"upper_population" : 53.43914451293095,
"lower_population" : 19.61943950627654,
"upper_sampling" : 57.23954713904196,
"lower_sampling" : 15.819036880165527
}
}
}
Percentiles Bucket Aggregation 聚合百分位数据统计
GET books/_search
{
"aggs": {
"publish_count": {
"terms": {
"field": "publish",
"size": 3
},
"aggs": {
"publish_count_avg": {
"avg": {
"field": "price"
}
}
}
},
"publish_count_avg_percentiles": {
"percentiles_bucket": {
"buckets_path": "publish_count>publish_count_avg"
}
}
}
}
结果:
"aggregations" : {
"publish_count" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 35,
"buckets" : [
{
"key" : "高等教育出版社",
"doc_count" : 742,
"publish_count_avg" : {
"value" : 28.579514824797844
}
},
{
"key" : "百花文艺出版社",
"doc_count" : 65,
"publish_count_avg" : {
"value" : 32.76923076923077
}
},
{
"key" : "科学出版社",
"doc_count" : 46,
"publish_count_avg" : {
"value" : 48.23913043478261
}
}
]
},
"publish_count_avg_percentiles" : {
"values" : {
"1.0" : 28.579514824797844,
"5.0" : 28.579514824797844,
"25.0" : 32.76923076923077,
"50.0" : 32.76923076923077,
"75.0" : 48.23913043478261,
"95.0" : 48.23913043478261,
"99.0" : 48.23913043478261
}
}
}