简介
elasticsearch:简称es,翻译过来是弹性搜索,一个开源的、分布式、可扩展、近实时的全文检索引擎,它是使用java开发的
es的使用场景:
- 搜索类场景:例如电商商品搜索、文档平台的内容搜索
- 应用日志分析
- 实时报表
es的特点:
- 近实时性:写入数据时,近1秒才会被搜索到,因为内部在进行分词、录入索引;搜索和分析需要秒级出结果
- 支持分布式,可水平扩展:支持动态增加节点,分片自动均衡,性能随节点数量线性提升
- 对外提供Restful API,可以被任何语言调用。
es的核心作用:
- 全文检索:快速从海量文本中匹配关键词,分词器、倒排索引、文本模糊搜索
- 实时数据分析:对数据进行聚合统计,支持近实时响应
- 多维筛选:亿级规模数据使用宽表预构建,消除 join,配合全字段索引,使ES在多维筛选能力上很强
es基于lucence框架,lucence是使用java编写的搜索引擎库。lucence是单机的,es在lucence的基础上增加了分布式的功能。
安装
这里先安装一个单节点的es集群。
下载安装包的地址:https://www.elastic.co/cn/downloads/elasticsearch ,在这里地址,根据自己的平台,下载安装包。es对于机器和操作系统的要求比较高,如果操作系统版本较低,建议安装旧版es。这里使用的版本是7.11.1
安装完成之后,要做一些配置:
1、创建新的用户组和用户,因为es不支持在root用户下启动
- 创建用户组:groupadd esgroup
- 创建用户:useradd -g esgroup elsearch
- 为新用户设置密码:passwd elsearch
- 把es的权限赋值给新用户: chown -R elsearch:esgroup /opt/elasticsearch*
- 登录到新用户:su elsearch
2、进入es的安装目录,修改es的配置文件 config/elasticsearch.yml
yml
# 集群名称
cluster.name: my-es
# 节点名称,现在只有1个节点
node.name: node1
# 数据路径
path.data: /opt/elasticsearch-7.11.1/data
# 日志路径
path.logs: /opt/elasticsearch-7.11.1/logs
# 监听的IP地址,这个的配置表示任意IP
network.host: 0.0.0.0
# 端口号
http.port: 9200
# 节点信息
cluster.initial_master_nodes: ["node1"]
3、调整文件句柄: 编辑 /etc/security/limits.conf ,添加如下内容,这里 elsearch 就是用户名,nofile 表示es可以打开的文件数
text
elsearch - nofile 65536
elsearch hard nofile 65536
elsearch soft nofile 65536
4、修改虚拟内存,编辑 /etc/sysctl.conf,添加如下内容
text
fs.file-max=655360
vm.max_map_count=262144 # 单个进程可以拥有的内存映射区域的最大数量,默认值65530不够用
执行命令 sysctl -p
5、启动es: ${ES_HOME}/bin/elasticsearch ,启动时观察一下日志,会发现,es使用的是它自带的jdk,没有使用系统默认的,看到 current.health="GREEN" ,就算启动成功,添加-d参数,表示后台启动

访问es: curl -XGET 'http://192.168.1.3:9200' ,响应结果
json
{
"name" : "node1",
"cluster_name" : "my-es",
"cluster_uuid" : "-7l55jYbStuUoZxA69-Lzg",
"version" : {
"number" : "9.0.2",
"build_flavor" : "default",
"build_type" : "tar",
"build_hash" : "0a58bc1dc7a4ae5412db66624aab968370bd44ce",
"build_date" : "2025-05-28T10:06:37.834829258Z",
"build_snapshot" : false,
"lucene_version" : "10.1.0",
"minimum_wire_compatibility_version" : "8.18.0",
"minimum_index_compatibility_version" : "8.0.0"
},
"tagline" : "You Know, for Search"
}
结果中包含es上的节点名称、集群名称、节点的版本信息等。
通过http协议访问es的小技巧:
-
添加"v"参数,返回表头:curl -XGET http://192.168.1.3:9200/_cat/health?v
-
添加"help"参数,返回字段的含义:curl -XGET http://192.168.1.3:9200/_cat/health?help
基本概念
倒排索引
倒排索引:倒排索引是相当于正向索引来说的,例如,根据id查找某条数据,id是主键,这时候id就是一个正向索引,而倒排索引,是对文档搜索的特殊处理,例如,要在多个文档中,搜索出包含"张三"这个词条的文档,传统的正向索引,需要全文检索、模糊查询,倒排索引的做法是,将文档中的数据,使用算法分词,得到一个个词条,然后把词条、词条所在文档、词条在文档中的位置等信息,记录在表中,然后用户想要搜索包含"张三"这个词条的文档,只需要检索倒排索引,就可以找到包含这个词条的文档。
es正是基于倒排索引来检索数据的,所以它的速度很快。
数据结构
es中数据的存储结构:
- 文档:es是面向文档存储的,文档就类似于数据库中的一条数据,文档数据会被转换为json格式存储到es中。es中,数据以json的格式存储
- 字段:一个文档中包含很多字段,类似于mysql数据库中的列。每个字段都有自己的数据类型
- 索引:相同文档的集合,类似于mysql中的表
- 索引中的映射:映射类似于数据库中的建表语句,指定了索引的数据结构。es使用一个json对象来描述承载映射信息
字段的数据类型
数据类型:
-
字符串:
- text:字符串,es会使用分词器,text类型的数据进行分词,建立倒排索引
- keyword:精确值,只能整体搜索,不支持搜索部分内容
-
整数:long、integer、short、byte
-
小数:double、float
-
布尔:boolean
-
日期:date,date 类型字段需要使用 ISO 格式日期时间,例如,"2025-11-15T10:30:00",也可以指定date数据的格式。
-
复杂数据类型:
- 数组:默认情况下,任何一个字段都可以包含一个或多个值,也就是说,任何一个字段,默认都是一个数组类型,数组中,每个元素的数据类型都必须一样。数组是开箱即用的,不需要进行任何配置。数组中的数据,不支持单独检索,匹配到一个元素,会视为全部匹配到
- 对象:object,一个字段的值,是一个json文档,这个文档中的每个字段,都是一个子字段。es会把object类型的数据层次结构展开之后,进行存储,例如,如下对象:
json{ "user": { "address": { "province" : "山西", "city": "太原" } } }会被实际存储为 user.address.province = '山西',user.address.city = '太原',不会保留字段之间的关联关系
- 嵌套对象:对象类型的特殊版本,允许不同元素被独立检索,本质上它是一个对象数组,会保留一个文档中各个字段之间的关联关系,对象结构不会被展开存储。
keyword字段的特殊配置:
- ignore_above:长度阈值,超过这个长度的字符串不能被索引,只能被存储,用于保证索引的性能,避免太长的字符串使索引性能变慢
动态映射
es会自动检测文档的数据结构,生成映射信息,称为动态映射。
动态映射的相关配置:
- 默认配置:dynamic: true,ES 会自动检测新字段的类型并创建映射,此时,以第一条文档为准生成动态映射,后续的文档,如果数据结构和之前的有冲突,会报错
- 关闭动态映射:dynamic: false ,ES 会存储新字段,但不索引(不能用于搜索、聚合)
- 使用严格模式的动态映射:dynamic: strict,出现新字段立即报错。生产环境建议选择严格模式
映射中可以配置的属性:
- type:字段的数据类型
- index:是否创建索引,默认为true,如果设置为false,表示这些字段不会被索引,不能用于搜索
- analyzer:使用哪种分词器
数据的物理存储
索引和分片:es以索引为单位进行检索,一个索引,类似于一张数据表,索引中的数据,被分为几个分片,分别存储在不同的分区中,这种设计使es可以支持横向扩展,同时每个分片都有自己的副本,支持高可用。
基本使用
操作索引
创建索引
命令:PUT /索引名
案例:这里创建一个user索引,指定分片数是3,分片的副本是是1,同时指定了索引的映射信息
json
curl -XPUT 'http://192.168.1.3:9200/user/' \
-H 'Content-Type:application/json' \
-d '
{
"settings": {
"number_of_shards": 3,
"number_of_replicas": 1
},
"mappings": {
"properties": {
"id": {
"type": "long"
},
"name": {
"type": "text"
},
"age": {
"type": "integer"
},
"birthday": {
"type": "date"
},
"email": {
"type": "text"
},
"createTime": {
"type": "date",
"index": "false"
},
"createUser": {
"type": "keyword",
"index": "false"
},
"updateTime": {
"type": "date",
"index": "false"
},
"updateUser": {
"type": "keyword",
"index": "false"
},
"yn": {
"type": "integer"
}
}
}
}'
案例2: 把创建索引分为两步
第一步:创建索引,不指定配置信息,curl -XPUT 'http://192.168.1.3:9200/user'
第二步:配置索引的映射信息, 命令是 PUT /索引名/_mapping
json
curl -XPUT 'http://192.168.1.3:9200/user/_mapping' \
-H 'Content-Type:application/json' \
-d '
{
"properties": {
"id": {
"type": "long"
},
"name": {
"type": "text"
},
"age": {
"type": "integer"
},
"birthday": {
"type": "date"
},
"email": {
"type": "text"
},
"createTime": {
"type": "date",
"index": "false"
},
"createUser": {
"type": "keyword",
"index": "false"
},
"updateTime": {
"type": "date",
"index": "false"
},
"updateUser": {
"type": "keyword",
"index": "false"
}
}
}'
查看索引
命令:GET /索引名
案例:curl -XGET 'http://192.168.1.3:9200/user',响应内容中包括索引名称(provided_name)、创建时间、分片数、映射等信息
查看映射
命令:GET /索引名/_mapping
案例: curl -XGET 'http://192.168.1.3:9200/user/_mapping'
查看索引的详细信息
curl -XGET 'http://192.168.1.3:9200/user/_stats?pretty'
查看索引的设置
curl -XGET 'http://192.168.1.3:9200/user/_settings?pretty'
删除索引
命令:DELETE /索引名
案例:curl -XDELETE 'http://192.168.1.3:9200/user',响应如下
json
{
"acknowledged": true
}
修改索引
某些属性不支持修改,例如分片数、已有字段等,因为修改这些内容后,需要重建索引,代价比较大,如果一定要修改,建议重建索引
新增字段
只支持向映射中新增字段,不支持删除字段、修改已有字段的数据类型
案例1:新增字段
json
curl -XPUT 'http://192.168.1.3:9200/user/_mapping' \
-H 'Content-Type:application/json' \
-d '
{
"properties": {
"yn": {
"type": "byte"
}
}
}'
关闭、开启索引
关闭索引后,用户将不可以再访问索引,但是该索引还存在于es中。
关闭索引的命令:POST /索引名/_close
开启索引的命令:POST /索引名/_open
案例1:关闭索引 curl -XPOST 'http://192.168.1.3:9200/user/_close'
json
{
"acknowledged": true,
"shards_acknowledged": true,
"indices": {
"user": {
"closed": true
}
}
}
案例2:开启索引 curl -XPOST 'http://192.168.1.3:9200/user/_open'
json
{
"acknowledged": true,
"shards_acknowledged": true
}
查看所有索引
方式1:GET /indices?v
案例:curl -XGET 'http://192.168.1.3:9200/_cat/indices?v',响应结果:

字段讲解:
- health:索引健康状态,green表示健康,yellow表示有警告信息
- status:索引状态,open表示可以正常打开
- index:索引名称
- uuid:索引的唯一id
- pri:主分片数
- rep:副本分片数
- docs.count:文档数量
- docs.deleted:已删除的文档数量
- store.size:总存储大小,主分片 + 副本分片的总大小
- pri.store.size:主分片的大小
- dataset.size:实际数据大小
方式2:命令 /_all
案例:curl -XGET 'http://192.168.1.3:9200/_all'
判断索引是否存在
命令:HEAD /索引名,返回200,表示存在,返回404,表示不存在
案例: curl --head 'http://192.168.1.3:9200/user' ,--head,指定请求方法是HEAD方法,响应如下:
text
HTTP/1.1 200 OK
X-elastic-product: Elasticsearch
content-type: application/json
Transfer-Encoding: chunked
批量查看索引
命令:GET /索引1,索引2 , 路径中有几个索引,就会返回几个索引的详情
操作文档
文档,就是索引中的一条数据
新建文档
命令:POST /索引名/_doc/id ,id是可选的,如果不指定,由es来生成id,es会使用base64编码的uuid来作为唯一id,如果没有根据id排序的需求,推荐使用默认id,或者也可以手动生成分布式id
案例:向索引中新增一条数据,同时指定数据的id
json
curl -XPOST 'http://192.168.1.3:9200/user/_doc/1' \
-H 'Content-Type:application/json' \
-d '
{
"id": 1001,
"name": "张三",
"age": 28,
"email": "zhangsan@example.com",
"birthday": "1998-05-15",
"createTime": "2025-11-15T10:30:00",
"createUser": "admin",
"updateTime": "2025-11-15T10:30:00",
"updateUser": "admin",
"yn": 1
}'
生成文档id
- 手动生成文档id:
/book/_doc/${id},创建文档时指定id - 自动生成文档id :
/book/_doc
自动生成的id:长度为20个字符,url安全、base64编码,分布式生成不冲突,案例 Jj-K1JsB4UKcpMJ1xIxU
根据id查看文档
命令:GET /索引名/_doc/id
案例:curl -XGET 'http://192.168.1.3:9200/user/_doc/1' ,响应信息
json
{
"_index": "user",
"_id": "1",
"_version": 1,
"_seq_no": 0,
"_primary_term": 1,
"found": true,
"_source": {
"id": 1001,
"name": "张三",
"age": 28,
"email": "zhangsan@example.com",
"birthday": "1998-05-15",
"createTime": "2025-11-15T10:30:00",
"createUser": "admin",
"updateTime": "2025-11-15T10:30:00",
"updateUser": "admin",
"yn": 1
}
}
响应信息中包括索引名、文档id、版本号、文档数据等
修改文档
有两种方式,全量修改和部分修改:
- 全量修改:指定文档id,重新创建文档,它相当于先删后增
- 部分修改:指定需要被修改的字段,执行修改命令
全量修改的命令:重新执行创建操作即可
部分修改的命令:POST /索引名/_update/id
案例:部分修改
text
curl -XPOST 'http://192.168.1.3:9200/user/_update/1' \
-H 'Content-Type:application/json' \
-d '
{
"doc": {
"name": "张三111",
"age": 26
}
}'
删除文档
命令:DELETE /索引名/_odc/id
案例:删除指定id的文档, curl -XDELETE 'http://192.168.1.3:9200/user/_doc/1' ,响应如下:
json
{
"_index": "user",
"_id": "1",
"_version": 2,
"result": "deleted",
"_shards": {
"total": 2,
"successful": 1,
"failed": 0
},
"_seq_no": 1,
"_primary_term": 1
}
批量新增 bulk
命令:POST /索引名/_bulk , Bulk API 要求严格的 NDJSON(Newline Delimited JSON)格式,应该是每行一个完整的 JSON 对象,用换行符分隔。这样做es可以根据换行符来分割请求体,而不是解析json数据,在一个节点上进行大批量的json解析比较耗内存
请求体的基本格式:
text
{ 操作行 }
{ 数据行 }
{ 操作行 }
{ 数据行 }
案例:向user索引中新增两条数据
json
curl -XPOST 'http://192.168.1.3:9200/user/_bulk' \
-H 'Content-Type:application/json' \
-d '
{"index": {"_id": "2"}}
{"id": 1002, "name": "李四", "age": 25, "email": "lisi@example.com", "birthday": "1998-06-15", "createTime": "2024-01-15T10:30:00", "createUser": "admin", "updateTime": "2024-01-15T10:30:00", "updateUser": "admin", "yn": 1}
{"index": {"_id": "3"}}
{"id": 1003, "name": "王五", "age": 30, "email": "wangwu@example.com", "birthday": "1993-08-20", "createTime": "2024-01-15T10:35:00", "createUser": "admin", "updateTime": "2024-01-15T10:35:00", "updateUser": "admin", "yn": 1}
'
内置脚本
es可以使用内置脚本执行复杂操作,例如,想要更新文档中的数据,不使用脚本的话,需要读取数据、计算、然后再写数据,如果使用脚本并且计算逻辑比较简单,可以用脚本来表达,那么可以把计算逻辑放到脚本中,传给es,让es来执行脚本,更新数据。
es内置的脚本:painless。es5.0之后,painless成为默认的脚本语言,7.0之后,出于安全考虑,es仅支持painless,
painless的特点:
- 安全:无反射权限;限制文件系统、网络环境的访问
- 语法上类似于java的子集语法
案例: 指定书的价格加10
json
curl -XPOST 'http://192.168.1.3:9200/book/_doc/2/_update' \
-H 'Content-Type:application/json' \
-d '
{
"script": "ctx._source.price += 10"
}'
_create和_update
_create:执行两次创建命令,第一次创建,第二次是全量替换,为了避免这种情况,创建时可以加上_create参数,它在第二次创建时会报错,避免替换已有数据
案例:
json
curl -XPUT 'http://192.168.1.3:9200/book/_doc/Jj-K1JsB4UKcpMJ1xIxU/_create?pretty' \
-H 'Content-Type:application/json' \
-d '
{
"name": "python爬虫框架",
"description": "学爬虫,好找工作",
"studymodel": "201004",
"price": 100,
"timestamp": "2019-08-25 19:15:35",
"pic": "g1/00/pic1_python.jpg",
"tags": ["python", "dev"]
}'
{
"error" : {
"root_cause" : [
{
"type" : "version_conflict_engine_exception",
"reason" : "[Jj-K1JsB4UKcpMJ1xIxU]: version conflict, document already exists (current version [1])",
"index_uuid" : "VbNunVtSSlW1y8U1R_Sljw",
"shard" : "0",
"index" : "book"
}
],
"type" : "version_conflict_engine_exception",
"reason" : "[Jj-K1JsB4UKcpMJ1xIxU]: version conflict, document already exists (current version [1])",
"index_uuid" : "VbNunVtSSlW1y8U1R_Sljw",
"shard" : "0",
"index" : "book"
},
"status" : 409
}
_update:部分更新文档,可以只传入必要的字段,来更新文档,避免传入全部字段
案例:
json
curl -XPOST 'http://192.168.1.3:9200/book/_doc/1/_update?pretty' \
-H 'Content-Type:application/json' \
-d '
{
"doc": {
"price": 39.9
}
}'
基于_version的版本控制
es对文档的增删改都是基于版本号控制,每次操作,版本号都会加1,
主从同步并发控制: es后台,主从同步是异步多线程的,所以,多个请求是乱序的。es内部主从同步,也是基于版本号的,不管谁先到,最终,只有最新的版本号才会生效。
乐观锁:带着版本号去修改数据,如果发现版本号不一致就放弃,适合并发度低的时候
查询文档
es提供了自己的查询语法,称为DSL,Domain Sepecific Language,领域特定语言。
es中的查询类型:
- 查询所有:查询出所有数据,match_all
- 全文检索查询:使用分词器对用户的输入进行分词,然后去倒排索引库中匹配,分为match、multi_match
- 精确查询:根据精确词条查找数据,一般查找的是keyword、数值、日期、boolean等类型字段,例如range、term
- 复合查询:将以上各种查询条件组合起来,合并查询条件,例如 bool
查询语句的基本语法:
json
{
"query": {
"查询类型": {
"查询条件: "条件值"
}
}
}
查询所有 match_all
查询所有,一般测试时使用,查询类型是match_all。如果查询结果超过10条,es默认只会返回前10条数据,即使没有指定分页参数,这是为了保证查询性能,并且大多数应用场景只关系前几条最相关的数据。
案例 :查询所有文档
json
curl -XPOST 'http://192.168.1.3:9200/user/_search' \
-H 'Content-Type:application/json' \
-d '{
"query": {
"match_all": {}
}
}'
全文检索查询
查询的是text类型的数据,es会对字段类型为text的字符串进行分词,然后建立倒排索引,全文检索查询只可以根据text类型的字段进行检索。
有两种类型的全文检索查询:
- match:根据单个字段进行匹配
- multi_match:根据多个字段进行匹配
全文检索的基本流程:
- 根据用户的输入,进行分词,得到词条
- 根据词条,去倒排索引库中匹配,得到文档id
- 把文档id返回给用户
单个字段匹配 match
案例:根据name字段模糊查询,查询name字段中包含"张"的文档,这里能否查出结果,依赖分词器的配置。
json
curl -XPOST 'http://192.168.1.3:9200/user/_search' \
-H 'Content-Type:application/json' \
-d '
{
"query": {
"match": {
"name": "张"
}
}
}'
es的响应:
json
{
"took": 4,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 1,
"relation": "eq"
},
"max_score": 0.9808291,
"hits": [
{
"_index": "user",
"_id": "1",
"_score": 0.9808291,
"_source": {
"id": 1001,
"name": "张三",
"age": 28,
"email": "zhangsan@example.com",
"birthday": "1998-05-15",
"createTime": "2025-11-15T10:30:00",
"createUser": "admin",
"updateTime": "2025-11-15T10:30:00",
"updateUser": "admin",
"yn": 1
}
}
]
}
}
响应中的字段:
-
took:查询执行时间,单位是毫秒
-
timed_out:查询是否超时
-
_shards:分片信息,
- total:参与查询的分片总数
- successful:成功执行查询的分片数
- skipped:跳过的分片数(通常在使用路由时发生)
- failed:查询失败的分片数
-
hits:命中结果
-
value:匹配查询条件的文档总数
-
relation:总数关系的精确度,
"eq"表示精确值,"gte"表示至少有多少 -
max_score:所有匹配文档中的最高相关性分数,分数越高,表示查询结果与查询条件的匹配度越高
-
hits:表示命中的文档,包括文档的索引、id、分数、文档内容
多个字段匹配 multi_match
案例:根据name字段、email字段模糊查询,查询这两个字段中包含 "example.com" 的文档
json
curl -XPOST 'http://192.168.1.3:9200/user/_search' \
-H 'Content-Type:application/json' \
-d '
{
"query": {
"multi_match": {
"query": "example.com",
"fields": ["name", "email"]
}
}
}'
精确匹配词条 match_phrase
match和match_phrase:
- match关注的是是否包含,它将查询字符串拆分成多个词项,只要文档包含一个或多个就会被匹配到,词项之间的顺序不重要。match的查询要求更宽松,追求更高的召回率
- match_phrase关注的是是否精确包含,它要求文档必须包含所有的词项,并且词项直接的顺序必须严格一致。match_phrase的查询要求更精确,追求精准匹配
案例:
json
curl -XPOST 'http://192.168.1.3:9200/user/_search' \
-H 'Content-Type:application/json' \
-d '
{
"query": {
"match_phrase": {
"email": "zhang"
}
}
}'
前缀匹配 match_phrase_prefix
在text类型的字段上进行智能前缀搜索,匹配的是建立倒排索引后的文本,只要找到匹配指定前缀的词项,那么整个text字段就算匹配成功。
案例:查询用户的email以"z"开头的
json
curl -XPOST 'http://192.168.1.3:9200/user/_search' \
-H 'Content-Type:application/json' \
-d '
{
"query": {
"match_phrase_prefix": {
"email": "z"
}
}
}'
精确查询
查找keyword、数值、日期等数据,不会对输入条件进行分词
单个值相等 term
案例:查询年龄等于25的用户
json
curl -XPOST 'http://192.168.1.3:9200/user/_search' \
-H 'Content-Type:application/json' \
-d '
{
"query": {
"term": {
"age": {
"value": "25"
}
}
}
}'
多个值相等 terms
案例:查询年龄等于25、26、27的用户
json
curl -XPOST 'http://192.168.1.3:9200/user/_search' \
-H 'Content-Type:application/json' \
-d '
{
"query": {
"terms": {
"age": ["25", "26", "27"]
}
}
}'
范围查询 range
范围查询中有几个运算符:
- 大于等于:gte
- 大于:gt
- 小于等于:lte
- 小于:lt
案例:查询年龄在20到30之间的用户
json
curl -XPOST 'http://192.168.1.3:9200/user/_search' \
-H 'Content-Type:application/json' \
-d '
{
"query": {
"range": {
"age": {
"gte": "20",
"lte": "30"
}
}
}
}
存在查询 exists
exists:查询指定字段上有值的文档
案例:查询name字段上有值的文档
json
curl -XPOST 'http://192.168.1.3:9200/user/_search' \
-H 'Content-Type:application/json' \
-d '
{
"query": {
"exists": {
"field": "name"
}
}
}'
前缀查询 prefix
prefix:查询某个字段上有指定前缀的文档,和之前match_phrase_prefix不同的是,它是在keyword类型的字段上进行匹配,匹配的是整个字段。
案例:
json
curl -XPOST 'http://192.168.1.3:9200/user/_search' \
-H 'Content-Type:application/json' \
-d '
{
"query": {
"prefix": {
"email": "z"
}
}
}'
通配符查询 wildcard
通配符查询的语法,*表示任意字符,?表示任意单个字符
案例:查询email字段中包含"s"的数据
json
curl -XPOST 'http://192.168.1.3:9200/user/_search' \
-H 'Content-Type:application/json' \
-d '
{
"query": {
"wildcard": {
"email": "*s*"
}
}
}'
正则表达式查询 regexp
支持通过正则表达式来匹配数据
案例:
json
curl -XPOST 'http://192.168.1.3:9200/user/_search' \
-H 'Content-Type:application/json' \
-d '
{
"query": {
"regexp": {
"email": ".*s.*"
}
}
}'
容错匹配 fuzzy
返回包含与搜索词类似的词的文档,它会使用指定的算法,来匹配关键字和倒排索引的相似度,可以提供更高的召回率,例如,搜索"jave",可以搜出包含"java"的词条
案例:
json
curl -XPOST 'http://192.168.1.3:9200/book/_search' \
-H 'Content-Type:application/json' \
-d '
{
"query": {
"fuzzy": {
"name": "jave"
}
}
}'
复合查询
复合查询就是把之前的全文检索、精确查询组合到一起,共同查询。
常见的复合查询:
- bool:利用逻辑关系组合多个查询,实现复杂搜索
- function score:算分函数查询,可以控制文档相关性算分,控制文档排名
组合多个查询条件 bool
查询中的运算符:这里类似于逻辑运算符
- must:必须匹配每个子查询,类似于and
- should:匹配任意一个子查询,类似于or
- must_not:必须不匹配,类似于not,不参会算分
- filter:必须匹配,不参与算分。如果只是要根据一些条件筛选出数据,不关注其排序,使用过滤器。过滤器因为不需要按照相关度计算排序,通常效率会更高。
普通查询和过滤器:
- 普通查询,会计算搜索条件和每个文档的相关度,并按照相关度进行排序
- 过滤器,仅仅是按照条件过滤出数据,对相关度没有任何影响
案例:查询名字中包含 "张" 的用户,年龄在20到30之间,或者40到50之间,并且数据的id在1000到2000之间,并且用户的生日不等于"1998-05-17"。
json
curl -XPOST 'http://192.168.1.3:9200/user/_search' \
-H 'Content-Type:application/json' \
-d '
{
"query": {
"bool": {
"must": [
{
"match": {
"name": "张"
}
}
],
"should": [
{
"range": {
"age": {
"gt": "20",
"lt": "30"
}
}
},
{
"range": {
"age": {
"gt": "40",
"lt": "50"
}
}
}
],
"filter": [
{
"range": {
"id": {
"gte": "1000",
"lte": "2000"
}
}
}
],
"must_not": [
{
"term": {
"birthday": "1998-05-17"
}
}
]
}
}
}'
算分查询
当用户使用全文检索查询时,es会对文档结果与搜索词条的关联度打分(_score),返回的结果按照分值降序排序。
算分函数查询:在搜索结果的基础上,再与手动指定的数字进行一定的运算来改变算分,从而改变结果的排序,例如,搜索引擎可以根据广告费来改变排序。
算分函数查询的运行流程:
- 原始查询:根据原始条件进行查询,并且计算相关性算分。称为原始算分(query score)
- 过滤出要算分的文档:根据过滤条件,过滤文档
- 算分函数:符合过滤条件的文档,基于算分函数,得到一个分数,称为函数算分(function score)
- 计算结果:将原始算分与函数算分基于运算模式做运算,得到最终结果
常见的算分函数:
- weight:常量
- field_value_factor:以文档中的某个字段值作为函数结果
- random_score:以随机数作为函数结果
- script_score:自定义算分函数算法,这应该是生产中最常用的
运算模式:算分函数的结果、原始查询的相关性算分,两者之间的运算方式,包括:
- multiply:相乘
- replace:用函数算分替换原始算分
- 其它,例如:sum、avg、max、min
算分函数查询的三要素:
- 过滤条件:哪些文档要加分
- 算分函数:如何计算function score
- 加权方式:function score 与 query score如何运算
参与打分的字段越多,查询的性能也越差,因此这种多条件查询时,建议的做法是:
- 搜索框的关键字搜索,是全文检索查询,使用must查询,参与算分
- 其它过滤条件,采用filter查询,不参与算分
案例:在原始查询的基础上,找到名称为"张三"的用户,给他加2分
json
curl -XPOST 'http: //192.168.1.3:9200/user/_search' \
-H 'Content-Type:application/json' \
-d '
{
"query": {
"function_score": {
"query": {
"bool": {
"must": [
{
"match": {
"name": "张"
}
}
],
"should": [
{
"range": {
"age": {
"gt": "20",
"lt": "30"
}
}
},
{
"range": {
"age": {
"gt": "40",
"lt": "50"
}
}
}
],
"filter": [
{
"range": {
"id": {
"gte": "1000",
"lte": "2000"
}
}
}
],
"must_not": [
{
"term": {
"birthday": "1998-05-17"
}
}
]
}
},
"functions": [
{
"filter": {
"match": {
"name": "张三"
}
},
"weight": 2
}
],
"boost_mode": "sum"
}
}
}'
结果排序 order
在使用排序后就不会算分了
案例: 对结果按照id进行倒序排序
json
curl -XGET 'http://192.168.1.3:9200/user/_search' \
-H 'Content-Type:application/json' \
-d '
{
"query": {
"match_all": { }
},
"sort": [ /* 支持多个字段 */
{
"id": "desc"
}
]
}'
结果分页 from size
类似于mysql中的分页,有两个条件:
- from:从第几个文档开始,不包括该文档,相当于文档下标
- size:结果条数
es禁止from + size超过10000的请求,因为分页深度较大时会对内存和cpu产生较大压力
分页查询尽量添加唯一排序字段,否则,按照默认的顺序,如果没有查询条件的话,每行数据的_score都是1,会导致乱序
分页查询的执行方式:例如,每页10条数据,查询第二页,协调节点收到请求后,会发送给所有分片,每个分片上计算出前两页的数据,然后把这20条数据的id、score发送给协调节点,统一进行排序,协调节点排序完成后,再从每个分片上获取原始数据。
案例:查询第一页的数据,一页有10条数据
json
curl -XGET 'http://192.168.1.3:9200/user/_search' \
-H 'Content-Type:application/json' \
-d '
{
"query": {
"match": {
"email": "example.com"
}
},
"from": 0,
"size": 10
}'
结果高亮 highlight
高亮在es中是提取出关键字段。有几个注意事项:
- 高亮的字段,必须与搜索指定的字段一致,否则无法高亮
- 高亮是对关键字高亮,因此搜索条件必须带有关键字,而不能是范围查询
案例:查询email字段中包含"example.com"的数据,对email字段高亮
json
curl -XGET 'http://192.168.1.3:9200/user/_search' \
-H 'Content-Type:application/json' \
-d '
{
"query": {
"match": {
"email": "example.com"
}
},
"highlight": {
"fields": {
"email": { }
}
}
}'
响应中的部分数据:高亮数据会被附加到查询结果的文档中

聚合 aggs
聚合,类似于mysql中的分组函数,根据某个分组,求某个字段的最大值、最小值、平均值等。参与聚合的字段必须是keyword、日期、数值、布尔类型等。
聚合中有两个概念:
- 分桶:bucket,一个数据分组
- 指标:metric,对一个bucket进行聚合分析,比如求平均值,
例如,一张学生成绩表,求出每个学生的总分,假设学生的每科成绩占一行,那么,一个学生,就是一个分桶,学生的总分,就是指标。
分桶类型:
- terms:按字段值分组,然后统计每个分组下的数目
- range:按范围分组
- histogram:直方图,按指定间隔分组。直方图用一系列宽度相等、高度不等的长方形来表示数据,其宽度代表组距,高度代表指定组距内的数据数(频数),用于对数值型字段进行分组统计
- date_histogram:按时间间隔分组
指标类型:
- avg、sum、min、max、count:类似于mysql中的聚合函数
- stats:基本统计信息,包括avg、sum、max、min、count
- cardinality:去重计数
聚合结果默认会排序,默认的排序规则:
- 桶聚合(如 date_histogram、histogram、terms):
- terms 聚合:默认按 doc_count 降序排序(文档数最多的桶排在最前面)
- date_histogram/histogram: 默认按 _key 升序排序(对于日期就是时间先后顺序,对于数值就是从小到大)
聚合的基本语法:
json
{
"size": 0, /* 不返回原始文档,只返回聚合结果 */
"query": { /* 可选的查询条件 */
},
"aggs": { /* 定义聚合 */
"聚合名称": {
"聚合类型": {
"参数": "值"
},
"aggs": {} // 可选的子聚合
}
}
}
聚合可配置的属性有:
- field:计算哪个字段的聚合结果,例如field是age,表示计算age的聚合结果,最大值、最小值、平均值等
- size:聚合结果的数量
- order:聚合结果的排序方式
案例1:查询每个年龄的用户数
json
curl -XGET 'http://192.168.1.3:9200/user/_search' \
-H 'Content-Type:application/json' \
-d '
{
"size": 0,
"query": {
"match": {
"email": "example.com"
}
},
"aggs": {
"ageCount": {
"terms": {
"field": "age"
}
}
}
}'
响应中的聚合部分:
json
"aggregations": {
"ageCount": {
"doc_count_error_upper_bound": 0,
"sum_other_doc_count": 0,
"buckets": [ /* 根据年龄的分桶结果,这里是桶中只有1条数据,表示年龄在28的用户有3条文档 */
{
"key": 28,
"doc_count": 3
}
]
}
}
案例2:查询邮箱中包含 "example.com" 的用户,年龄在20到30的用户数
json
curl -XGET 'http://192.168.1.3:9200/user/_search' \
-H 'Content-Type:application/json' \
-d '
{
"size": 0,
"query": {
"match": {
"email": "example.com"
}
},
"aggs": {
"ageRanges": {
"range": {
"field": "age",
"ranges": {
"from": 20,
"to": 30
}
}
}
}
}'
响应中的聚合部分:
json
"aggregations": {
"ageRanges": {
"buckets": [
{
"key": "20.0-30.0",
"from": 20.0,
"to": 30.0,
"doc_count": 3
}
]
}
}
案例3:聚合结果排序。聚合完之后按照价格降序排序
json
curl -XPOST 'http://192.168.1.3:9200/book/_search?pretty' \
-H 'Content-Type:application/json' \
-d '
{
"size": 0,
"query": {
"match_all": {}
},
"aggs": {
"group_by_tags": {
"terms": {
"field": "tags",
"order": {
"avg_price": "desc" /* 在聚合中可以直接调用子聚合的属性 */
}
},
"aggs": {
"avg_price": {
"avg": {
"field": "price"
}
}
}
}
}
}'
在url上拼接查询参数
除了上面提到的把查询语句写在请求体中,es还支持把查询语句拼接在参数上,不过这种方式应该比较古老了,而且如果是复杂的查询语句,会很难看。
案例1:全局搜索: curl -XGET 'http://192.168.1.3:9200/book/_search?pretty'
案例2:指定参数的搜索: curl -XGET 'http://192.168.1.3:9200/book/_search?pretty\&q=tags:dev'
案例3:指定结果的排序规则: curl -XGET 'http://192.168.1.3:9200/book/_search?pretty\&q=tags:dev\&sort=price:desc'
案例4:分页搜索, curl -XGET 'http://192.168.1.3:9200/book\*/_search?from=0\&size=2' , from,从一条数据开始,这里是数据的下标,size,取几条数据
案例5:返回指定字段,_source :curl -XGET 'http://192.168.1.3:9200/book/_search?pretty\&q=tags:dev\&sort=price:desc\&timeout=10ms\&_source=name,price'
案例6:一个复杂的案例,index1/_search?q=userCode:1111 AND bizType:1 AND operateType:2 AND bizId:10 AND operateTime:["2026-02-02 00:00:00" TO "2026-02-02 23:59:59"]&sort=operateTime:desc
案例7:搜索全部字段。直接输入value,不指定字段就会去全部字段中搜索。搜索全部字段涉及到一个 _all 字段,在向es中插入数据时,会对所有的字段进行分词,把这些分词,放到 _all 字段中,在搜索的时候,如果没有指定具体的字段,就在 _all 字段中搜索。案例:curl -XGET 'http://192.168.1.3:9200/book/_search?pretty&q=java'
正向搜索和逆向搜索
正向搜索:普通搜索就是正向搜索,语法是 +key:value
逆向搜索:和正向搜索相反,例如,我想搜索不包含指定内容的数据,语法是 -key:value
其它查询方式
根据文档id查询 _mget命令
命令:POST /索引名/_mget
案例:
json
curl -XPOST 'http://192.168.1.3:9200/user/_mget' \
-H 'Content-Type:application/json' \
-d '{
"docs": [
{
"_id": "1"
}
]
}'
也可以不指定索引名,在docs中使用 _index 字段。
响应:
json
{
"docs": [
{
"_index": "user",
"_id": "1",
"_version": 1,
"_seq_no": 2,
"_primary_term": 1,
"found": true,
"_source": {
"id": 1001,
"name": "张三",
"age": 28,
"email": "zhangsan@example.com",
"birthday": "1998-05-15",
"create_time": "2025-11-15T10:30:00",
"create_user": "admin",
"update_time": "2025-11-15T10:30:00",
"update_user": "admin",
"yn": 1
}
}
]
}
响应结果中的字段:
- _index:文档所在的索引
- _id:文档id
- _version:文档的版本号,每次更新,版本号都会增加
- _seq_no:序列号,用于乐观并发控制,每次索引操作都会递增
_primary_term:主分片任期号,当主分片重新分配时会增加,与_seq_no一起实现并发控制
查询指定索引下的数据量 _count
命令:GET /索引名/_count
案例:curl -XGET 'http://192.168.1.3:9200/user/_count'
timeout机制
指定每个分片只能在给定的时间内查询数据,能有几条就返回几条,避免数据量太大,搜索时间太长,丢失业务
指定超时时间: curl -XGET 'http://192.168.1.3:9200/book/_search?pretty\&q=tags:dev\&sort=price:desc\&timeout=10ms'
多索引搜索
类似于分库分表,例如,索引中存储着日志数据,日志数据按天分到不同的索引中,如果想要同时从多个索引中搜索,就需要使用多索引搜索语法
多索引搜索的方式:
- 方式1:使用逗号分割多个索引, curl -XGET 'http://192.168.1.3:9200/book,book1/_search?pretty'
- 方式2:使用星号匹配多个索引, curl -XGET 'http://192.168.1.3:9200/book\*/_search?pretty'
别名
别名,相当于索引的软链接。
别名的作用:
- 简化索引名称:一个复杂的索引名可以被替换为一个简单的别名
- 实现"零停机"管理:索引重建或数据迁移的过程中,用户可以先创建一个新索引,把数据从老索引迁移过去,然后再把别名从老索引指向新索引,整个过程对用户无感知
- 同时查询多个索引:一个别名可以指向多个索引,当用户查询这个别名时,会自动在所有的索引上进行搜索
创建别名
命令:POST _aliases
案例1:创建一个普通别名
json
curl -XPOST 'http://192.168.1.3:9200/_aliases' \
-H 'Content-Type:application/json' \
-d '
{
"actions": [
{
"add": {
"index": "user",
"alias": "user_v1"
}
}
]
}'
案例2:创建一个带有过滤条件的别名
json
curl -XPOST 'http://192.168.1.3:9200/_aliases' \
-H 'Content-Type:application/json' \
-d '
{
"actions": [
{
"add": {
"index": "user",
"alias": "user_v2",
"filter": {
"term": {
"yn": "0"
}
}
}
}
]
}'
查看别名
1、查看某个索引有哪些别名: GET 索引/_alias
案例:curl -XGET 'http://192.168.1.3:9200/user/_alias'
2、查看某个别名指向哪些索引: GET _alias/别名
案例:curl -XGET 'http://192.168.1.3:9200/_alias/user_v1'
3、查看所有别名:GET /_cat/aliases
案例:curl -XGET 'http://192.168.1.3:9200/_cat/aliases?v'
text
alias index filter routing.index routing.search is_write_index
user_v1 user - - - -
4、通过别名来查询索引中的文档,和普通索引的使用方式一样
案例:
json
curl -XPOST 'http://192.168.1.3:9200/user_v1/_search' \
-H 'Content-Type:application/json' \
-d '{
"query": {
"match_all": {}
}
}'
删除别名
1、方式1 命令:POST _aliases
案例:
json
curl -XPOST 'http://192.168.1.3:9200/_aliases' \
-H 'Content-Type:application/json' \
-d '
{
"actions": [
{
"remove": {
"index": "user",
"alias": "user_v1"
}
}
]
}'
2、 方式2 命令: DELETE /索引/_alias/别名
案例: curl -XDELETE 'http://192.168.1.3:9200/user/_alias/user_v2'
切换别名 更换别名指向的索引
切换别名(零停机重建索引),这是别名最强大的功能。
假设用户要重建索引,他首先要创建好一个新的索引,并且完成索引的数据迁移,然后,执行一个原子操作,把别名指向的索引从旧索引切换到新索引,这个操作是瞬间完成的,应用程序在下一秒通过别名查询到的就是新索引中的数据了,实现了零停机切换。
案例:
json
curl -XPOST 'http://192.168.1.3:9200/_aliases' \
-H 'Content-Type:application/json' \
-d '
{
"actions": [
{
"remove": {
"index": "user",
"alias": "user_v1"
}
},
{
"add": {
"index": "user2",
"alias": "user_v1"
}
}
]
}'
reindex 迁移数据
将索引中的数据从旧索引迁移到新索引,重建索引时使用,这个命令执行过程中,新增的数据不会被迁移。
案例:
json
curl -XPOST 'http://192.168.1.3:9200/_reindex/' \
-H 'Content-Type:application/json' \
-d '
{
"source": {
"index": "user-index",
"size": 5000 // 批量大小,根据ES性能调整(建议5000-10000)
},
"dest": {
"index": "user-index-v2",
"op_type": "create" // 关键:只创建新文档,避免覆盖(如果重复会报错,便于发现重复数据)
}
}'
模板
模板:用于创建相同结构的索引,例如,存储日志数据的索引,通常每天新建一个索引,此时,就可以使用模板来指定这些索引的结构,然后在创建索引时,就无需重复指定,只要检测到新索引的名称符合指定模板的pattern,就会自动使用该模板定义的结构
普通模板
es 7.x 之前使用的模板类型,7.x之后新增了组合模板,但是也还支持普通模板
案例:user索引的模板
定义模板:
json
curl -XPUT 'http://192.168.1.3:9200/_index_template/user-index-template?pretty' \
-H 'Content-Type:application/json' \
-d '
{
"index_patterns": ["user-index-*"],
"template": {
"settings": {
"number_of_shards": 4,
"number_of_replicas": 1,
"routing_partition_size": 2
},
"mappings": {
"_routing": {
"required": true
},
"properties": {
"id": { "type": "long" },
"name": { "type": "text" },
"birthday": { "type": "date", "format": "yyyy-MM-dd || epoch_millis" },
"email": { "type": "keyword" },
"createTime": { "type": "date", "index": "false" },
"createUser": { "type": "keyword", "index": "false" },
"updateTime": { "type": "date", "index": "false" },
"updateUser": { "type": "keyword", "index": "false" },
"yn": { "type": "integer" }
}
}
},
"priority": 10 // 优先级,数值越大优先级越高
}'
根据模板创建索引: curl -XPUT 'http://192.168.1.3:9200/user-index-v6/?pretty'
基本操作
查看模板详情
命令:GET _index_template/${模板名称}
案例:curl -XGET 'http://192.168.1.3:9200/_index_template/user-index-template?pretty'
删除模板
命令:DELETE _index_template/${模板名称}
案例:curl -XDELETE 'http://192.168.1.3:9200/_index_template/user-index-template?pretty'
修改模板
再次执行创建语句,就会覆盖原先的配置
查看所有模板
命令:GET _cat/templates
案例:curl -XGET 'http://192.168.1.3:9200/_cat/templates'
集群搭建
搭建步骤
1、单个节点的配置信息
properties
# 集群名称
cluster.name: es-cluster1
# 节点名称
node.name: node1
# 节点的角色,es中的节点,根据不同的功能,分为不同的节点,这里是为一个节点配置了所有角色,生产环境下,建议一个节点一个角色,方便管理。
node.roles: [master, data, ingest]
# 数据路径
path.data: /opt/elasticsearch-7.11.1/data
# 日志路径
path.logs: /opt/elasticsearch-7.11.1/logs
# 监听的IP地址,这个的配置表示任意IPc
network.host: 0.0.0.0
# 端口号
http.port: 9200
# 数据交换的端口号,默认值
transport.port: 9300
# 用于服务发现,告诉当前节点可以去哪里发现当前集群中的其它节点,
discovery.seed_hosts: ["192.168.1.3", "192.168.1.4", "192.168.1.5"]
# 集群第一次启动时使用,指定哪些节点有资格参加主节点选举
cluster.initial_master_nodes: ["node1", "node2", "node3"]
# 关闭安全验证
xpack.security.enabled: false
xpack.security.transport.ssl.enabled: false
xpack.security.http.ssl.enabled: false
这份配置新增中描述了单个节点的信息和集群信息,把这份配置信息同步到其它节点,需要修改的只有 node.name 。
2、依次启动每个节点,${ES_HOME}/bin/elasticsearch -d ,-d表示后台运行,节点启动后,会根据配置信息,寻找其它节点,然后选举主节点,最终对外提供服务。
操作集群的命令
查询集群基本信息,包括版本之类的
直接访问集群主页即可。
案例:curl -XGET 'http://192.168.1.3:9200'
查看当前集群的配置信息
命令:GET _cluster/settings
案例:curl -XGET 'http://192.168.1.3:9200/_cluster/settings?pretty\&include_defaults=true',这里加上了默认配置
查看集群的运行情况
集群健康状态
命令: GET /_cluster/health
案例:查询集群健康情况 curl -XGET 'http://192.168.1.3:9200/_cluster/health'
响应:
json
{
"cluster_name": "my-es", /* 集群名称 */
"status": "green", /* 集群状态,绿色,表示所有主分片和副本分片都正常运行 */
"timed_out": false, /* 集群健康检查是否超时,false表示没有超时,集群正常工作 */
"number_of_nodes": 3, /* 集群总节点数,总结点包含数据节点和用于其它功能的节点 */
"number_of_data_nodes": 3, /* 集群中数据节点数,数据节点仅仅用于存储数据,是上述总结点的一部分 */
"active_shards": 4, /* 所有活跃分片总数(主+副本) */
"active_primary_shards": 4, /* 活跃的主分片数量 */
"relocating_shards": 0, /* 正在迁移的分片数 */
"initializing_shards": 0, /* 正在初始化的分片数 */
"unassigned_shards": 0, /* 未分配的分片数 */
"unassigned_primary_shards": 0, /* 未分配的主分片数 */
"delayed_unassigned_shards": 0, /* 由于延迟分配机制而暂时未分配的分片数量 */
"number_of_pending_tasks": 0, /* 待处理任务数 */
"number_of_in_flight_fetch": 0, /* 正在进行的抓取操作 */
"task_max_waiting_in_queue_millis": 0, /* 任务最大等待时间 */
"active_shards_percent_as_number": 100.0 /* 健康百分比,100%是最健康的 */
}
集群状态:
- green:每个主分片和副本分片都是active状态
- yellow:所有主分片是active状态,部分副本分片不可用
- red:集群不可用,部分索引有数据丢失
集群统计信息
包括集群中节点、索引的统计信息
命令:GET _cluster/stats
案例:curl -XGET 'http://192.168.1.3:9200/_cluster/stats?pretty'
集群状态
命令:GET _cluster/state
案例:curl -XGET 'http://192.168.1.3:9200/_cluster/state?pretty\&filter_path=nodes',查看集群中的节点信息,因为返回的结果比较多,这里只关注节点信息
节点信息
命令:GET _cat/nodes
案例:curl -XGET 'http://192.168.1.3:9200/_cat/nodes?v\&h=name,node.role,master,ip,heap.percent,ram.percent,cpu,load_1m,disk.used_percent',这里只查看指定字段,包括每个节点的内存、cpu、磁盘消耗情况
节点的详细信息
命令:GET /_nodes/stats
案例:curl -XGET 'http://192.168.1.3:9200/_nodes/stats',响应中的内容很多,包括节点角色、属性、操作系统详情、cpu详情、jvm详情等
查看主节点
查看当前主节点: GET /_cat/master?v
案例:curl -XGET 'http://192.168.1.3:9200/_cat/master?v'
结果:
text
id host ip node
yDRp-HIrS6iEubIsNsICvA 192.168.1.4 192.168.1.4 node2
节点上磁盘的使用情况
命令:GET _cat/allocation
案例:curl -XGET 'http://192.168.1.3:9200/_cat/allocation?v\&h=node,shards,disk.indices,disk.used,disk.avail,disk.total,disk.percent\&pretty'
查询分片信息
每个索引下分片的分配情况:
命令:GET /_cat/shards,
案例: curl -XGET 'http://192.168.1.3:9200/_cat/shards?v'
text
index shard prirep state docs store dataset ip node
user2 0 r STARTED 0 249b 249b 192.168.1.5 node3
user2 0 p STARTED 0 249b 249b 192.168.1.4 node2
user 0 p STARTED 3 13.4kb 13.4kb 192.168.1.5 node3
user 0 r STARTED 3 13.4kb 13.4kb 192.168.1.3 node1
查看指定索引下每个分片的数据量:
命令:GET _cat/shards/索引
案例:curl -XGET 'http://192.168.1.3:9200/_cat/shards/stock-flow-index/?v\&h=index,shard,prirep,state,docs,store,node\&pretty'
当前正在执行的任务
命令:GET _cat/tasks
案例:curl -XGET 'http://192.168.1.3:9200/_cat/tasks?v'
待处理任务
命令:GET _cluster/pending_tasks
案例:curl -XGET 'http://192.168.1.3:9200/_cluster/pending_tasks'
线程池状态
命令:GET _cat/thread_pool
案例:curl -XGET 'http://192.168.1.3:9200/_cat/thread_pool?v'
关闭集群
最新的版本,9.x,是找到每个节点的进程id,ps -ef | grep 'el',直接使用kill命令关闭。
java api
springboot整合es
这里学习通过springboot来操作es,我使用的springboot版本是2.1.5,es的版本是7.11.1
第一步:配置依赖
xml
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<elasticsearch.version>7.11.1</elasticsearch.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.1.5.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!--web开发的起步依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.elasticsearch</groupId>
<artifactId>elasticsearch</artifactId>
<version>7.11.1</version>
</dependency>
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-client</artifactId>
<version>7.11.1</version>
</dependency>
<!-- Elasticsearch High Level REST Client -->
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>${elasticsearch.version}</version>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Jackson Databind -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- 测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
第二步:在springboot中添加es的连接配置,application.properties
properties
# ========== 应用配置 ==========
server.port=8080
spring.application.name=springboot-es-demo
# ========== Elasticsearch 配置 ==========
# ES节点地址
elasticsearch.hosts=192.168.1.3:9200
# 连接超时配置(毫秒)
elasticsearch.connect-timeout=5000
elasticsearch.socket-timeout=30000
elasticsearch.connection-request-timeout=5000
# 连接池配置
elasticsearch.max-conn-total=100
elasticsearch.max-conn-per-route=50
第三步:配置es客户端
java
import org.apache.http.HttpHost;
import org.apache.http.impl.nio.client.HttpAsyncClientBuilder;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestClientBuilder;
import org.elasticsearch.client.RestHighLevelClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import javax.annotation.PreDestroy;
import java.io.IOException;
@Configuration
public class ElasticsearchConfig {
@Value("${elasticsearch.hosts}")
private String hosts;
@Value("${elasticsearch.connect-timeout:3000}")
private int connectTimeout;
@Value("${elasticsearch.socket-timeout:3000}")
private int socketTimeout;
@Value("${elasticsearch.connection-request-timeout:3000}")
private int connectionRequestTimeout;
@Value("${elasticsearch.max-conn-total:100}")
private int maxConnTotal;
@Value("${elasticsearch.max-conn-per-route:50}")
private int maxConnPerRoute;
private RestHighLevelClient client;
/**
* 创建 RestHighLevelClient
*/
@Bean
@Primary
public RestHighLevelClient restHighLevelClient() {
try {
// 解析主机地址(支持多个节点)
String[] hostArray = hosts.split(",");
HttpHost[] httpHosts = new HttpHost[hostArray.length];
for (int i = 0; i < hostArray.length; i++) {
String host = hostArray[i].trim();
String[] hostParts = host.split(":");
String hostname = hostParts[0];
int port = hostParts.length > 1 ? Integer.parseInt(hostParts[1]) : 9200;
httpHosts[i] = new HttpHost(hostname, port, "http");
}
// 构建 RestClientBuilder
RestClientBuilder builder = RestClient.builder(httpHosts);
// 配置请求超时
builder.setRequestConfigCallback(requestConfigBuilder -> {
requestConfigBuilder
.setConnectTimeout(connectTimeout)
.setSocketTimeout(socketTimeout)
.setConnectionRequestTimeout(connectionRequestTimeout);
return requestConfigBuilder;
});
// 配置 HTTP 客户端
builder.setHttpClientConfigCallback(new RestClientBuilder.HttpClientConfigCallback() {
@Override
public HttpAsyncClientBuilder customizeHttpClient(HttpAsyncClientBuilder httpClientBuilder) {
// 配置认证(如果有用户名密码)
// 这里没有认证信息,所以不需要配置
// 配置连接池
httpClientBuilder.setMaxConnTotal(maxConnTotal);
httpClientBuilder.setMaxConnPerRoute(maxConnPerRoute);
return httpClientBuilder;
}
});
// 创建 RestHighLevelClient
client = new RestHighLevelClient(builder);
return client;
} catch (Exception e) {
throw new RuntimeException("Failed to create Elasticsearch client", e);
}
}
/**
* 应用关闭时关闭客户端连接
*/
@PreDestroy
public void close() {
if (client != null) {
try {
client.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
新增、修改、删除文档
案例:新增、删除文档
java
@Slf4j
@Component
public class ElasticsearchRepository {
@Resource
private RestHighLevelClient client;
private final ObjectMapper objectMapper = new ObjectMapper();
/**
* 创建用户文档
*/
public String createUserDocument(String indexName, Long id, User user) throws IOException {
// 将对象转换为 JSON
String json = objectMapper.writeValueAsString(user);
// 创建索引请求
IndexRequest request = new IndexRequest(indexName, "_doc", String.valueOf(id))
.source(json, XContentType.JSON);
// 执行索引操作
IndexResponse response = client.index(request, RequestOptions.DEFAULT);
log.info("创建文档 {} 到索引 {},ID: {},结果: {}",
json, indexName, response.getId(), response.getResult());
return response.getId();
}
/**
* 简单查询文档(根据ID)
*/
public User getUserById(String indexName, String id) throws IOException {
org.elasticsearch.action.get.GetRequest request =
new org.elasticsearch.action.get.GetRequest(indexName, "_doc", id);
org.elasticsearch.action.get.GetResponse response =
client.get(request, RequestOptions.DEFAULT);
if (response.isExists()) {
String sourceAsString = response.getSourceAsString();
return objectMapper.readValue(sourceAsString, User.class);
}
return null;
}
/**
* 删除文档
*/
public boolean deleteUserDocument(String indexName, String id) throws IOException {
org.elasticsearch.action.delete.DeleteRequest request =
new org.elasticsearch.action.delete.DeleteRequest(indexName, "_doc", id);
org.elasticsearch.action.delete.DeleteResponse response =
client.delete(request, RequestOptions.DEFAULT);
log.info("删除文档 {},结果: {}", id, response.getResult());
return response.getResult() == org.elasticsearch.action.DocWriteResponse.Result.DELETED;
}
}
查询文档
案例:
java
@RunWith(SpringRunner.class)
@SpringBootTest
public class RestHighLevelClientTest {
@Autowired
private RestHighLevelClient client;
// match
@Test
public void test1() throws IOException {
SearchRequest searchRequest = new SearchRequest("user");
SearchSourceBuilder builder = new SearchSourceBuilder();
builder.query(QueryBuilders. matchQuery("name", "张"));
searchRequest.source(builder);
SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
System.out.println("searchResponse = " + searchResponse);
}
// match_all
@Test
public void test2() throws IOException {
SearchRequest searchRequest = new SearchRequest("user");
SearchSourceBuilder builder = new SearchSourceBuilder();
builder.query(QueryBuilders.matchAllQuery());
searchRequest.source(builder);
SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
SearchHits hits = searchResponse.getHits();
for (SearchHit hit : hits) {
Map<String, Object> sourceAsMap = hit.getSourceAsMap();
System.out.println("sourceAsMap = " + JsonUtil.toJson(sourceAsMap));
}
}
// term
@Test
public void test3() throws IOException {
SearchRequest searchRequest = new SearchRequest("user");
SearchSourceBuilder builder = new SearchSourceBuilder();
builder.query(QueryBuilders.termQuery("age", "25"));
searchRequest.source(builder);
SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
System.out.println("searchResponse = " + searchResponse);
}
// range
@Test
public void test4() throws IOException {
SearchRequest searchRequest = new SearchRequest("user");
SearchSourceBuilder builder = new SearchSourceBuilder();
builder.query(QueryBuilders.rangeQuery("age").gt(20).lt(30));
searchRequest.source(builder);
SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
System.out.println("searchResponse = " + searchResponse);
}
// 聚合查询,查询所有用户的平均年龄
@Test
public void test5() throws IOException {
SearchRequest searchRequest = new SearchRequest("user");
SearchSourceBuilder builder = new SearchSourceBuilder();
builder.query(QueryBuilders.matchAllQuery());
builder.aggregation(AggregationBuilders.avg("ageAvg").field("age").missing(0));
builder.size(0);
searchRequest.source(builder);
SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
System.out.println("searchResponse = " + searchResponse);
}
// 分页查询
@Test
public void testPageQuery() throws IOException {
SearchRequest searchRequest = new SearchRequest("user");
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.from(0);
searchSourceBuilder.size(2);
searchRequest.source(searchSourceBuilder);
SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
SearchHits hits = searchResponse.getHits();
if (hits.getTotalHits() != null) {
long totalCount = hits.getTotalHits().value;
System.out.println("totalCount = " + totalCount);
}
for (SearchHit hit : hits.getHits()) {
Map<String, Object> map = hit.getSourceAsMap();
System.out.println("hit.getId() = " + hit.getId());
System.out.println("map = " + map);
}
}
// bool查询
@Test
public void testQuery() throws IOException {
SearchRequest searchRequest = new SearchRequest("book");
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.fetchSource(new String[]{"name", "description", "price"}, new String[0]);
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
boolQueryBuilder.must(QueryBuilders.matchQuery("name", "java程序员"));
boolQueryBuilder.should(QueryBuilders.termQuery("pushDate", "201001"));
boolQueryBuilder.filter(QueryBuilders.rangeQuery("price").gt("10").lt("100"));
searchSourceBuilder.query(boolQueryBuilder);
searchRequest.source(searchSourceBuilder);
SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
SearchHits hits = searchResponse.getHits();
if (hits.getTotalHits() != null) {
long totalCount = hits.getTotalHits().value;
System.out.println("totalCount = " + totalCount);
}
for (SearchHit hit : hits.getHits()) {
Map<String, Object> map = hit.getSourceAsMap();
System.out.println("hit.getId() = " + hit.getId());
System.out.println("map = " + map);
}
}
}
聚合
实战案例:一个索引中,存储了用户的生日,要求统计用户的年龄分布,0到18岁、19到30岁等
java
/**
* 获取年龄分布统计
*/
@Test
public void testAgeStatistics() throws IOException {
SearchRequest request = new SearchRequest(INDEX);
SearchSourceBuilder builder = new SearchSourceBuilder();
builder.size(0);
// 年龄范围聚合,这里需要对日期进行计算,例如,0到18岁的用户,当前生日应该在什么日期范围内。
DateRangeAggregationBuilder dateRangeAggregationBuilder = AggregationBuilders.dateRange("age_ranges")
.field("birthday")
.addRange("0-18",
ZonedDateTime.of(LocalDate.now().minusYears(18),
LocalTime.of(0, 0, 0),
ZoneId.systemDefault()),
ZonedDateTime.of(LocalDate.now().minusYears(0),
LocalTime.of(0, 0, 0),
ZoneId.systemDefault()))
.addRange("19-30",
ZonedDateTime.of(LocalDate.now().minusYears(30),
LocalTime.of(0, 0, 0),
ZoneId.systemDefault()),
ZonedDateTime.of(LocalDate.now().minusYears(19),
LocalTime.of(0, 0, 0),
ZoneId.systemDefault()));
builder.aggregation(dateRangeAggregationBuilder);
request.source(builder);
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
Range range = response.getAggregations().get("age_ranges");
Map<String, Long> distribution = new LinkedHashMap<>();
for (Range.Bucket bucket : range.getBuckets()) {
distribution.put(bucket.getKeyAsString(), bucket.getDocCount());
}
System.out.println("年龄分布: " + distribution);
}
深入理解
集群的运行机制
节点的角色
节点的角色:每个角色都有自己的功能
- 主节点:master node,负责管理集群,如创建或删除索引、跟踪集群的节点、决定分片分配等,一个集群只能有一个主节点
- 备选主节点:主节点和备选主节点的配置完全相同,只是在主节点选举时,选举成功的节点成为主节点,选举失败的节点成为备选主节点。集群中只有一个主节点,备选主节点会在主节点挂掉后参与选举,重新选举出主节点
- 协调节点: coordinating node,一个请求,首先经过协调节点,协调节点来分发请求,如果是查询请求,分发给数据节点,数据节点返回的数据协调节点还会进一步聚合;如果是增删改请求,需要涉及到集群、分片或者副本的变化,就分发给主节点,
- 摄入节点:ingest node,处理写入请求,执行预处理。
协调节点和摄入节点的区别:
- 协调节点:集群的门户,负责接收所有请求,并且分发请求到数据节点、主节点或协调节点,依据请求的类型,协调节点不负责数据计算和数据存储
- 摄入节点:在写数据时被调用,对数据进行预处理,然后交给协调节点进入下一步。
创建集群时如何指定节点的类型:
旧版本:7.x之前,通过以下三个配置指定
text
node.master: true
node.data: false
node.ingest: false
新版本:
text
# 组合角色(不推荐用于生产环境)
node.roles: [ master, data, ingest ]
# 专用主节点
node.roles: [ master ]
# 专用数据节点
node.roles: [ data ]
# 专用协调节点
node.roles: [ ]
# 专用摄取节点
node.roles: [ ingest ]
节点角色的配置规范:
- 主节点通常配置3到5个,保证是奇数,可以支持选举,主节点和数据节点尽量不要混合。
在生产环境下,一个节点只应该有一种类型,方便规划和管理
分片和副本
分片:分片是数据的物理存储位置,一个索引可以有多个分片,通过分片可以实现索引的横向扩展,每个分片都有自己的备份,称为分片的副本。副本分片从不与它的主分片在同一个节点上
案例:创建索引时指定分片的数量和每个分片的副本数量
text
PUT /user
{
"settings": {
"number_of_shards": 3, //设置分片数
"number_of_replicas": 1 //设置副本数
}
}
分片副本机制:
- 一个索引包含一个或多个分片
- 每个分片都是一个最小工作单元,承载部分数据、lucence实例,完整的建立索引、处理请求的能力
- 新增节点时,分片会自动在节点间负载均衡
- 主分片的数量不可以修改,副本分片可以。主分片的数量默认是1,副本分片也是
增加节点时,分片自动负载均衡,向空闲的机器转移
写请求的流程
写请求的流程:
- 协调节点接收请求:客户端将写请求发送到协调节点。
- 预处理:如果请求需要预处理,会将请求转发给集群中任何一个可用的摄入节点来执行预处理。摄入节点对原始文档进行清洗、转换,然后输出处理后的文档。预处理后的文档会被发回给协调节点。
- 路由:协调节点根据文档id或用户指定的路由规则,决定这个文档应该被发送到哪个分片,然后将文档转发给持有该主分片的数据节点。
- 路由规则:分片编号 = hash(_id) % 主分片数量
- 保存文档:数据节点接收文档,并将其写入到本地分片中。
数据节点保存数据的流程:
- 内核缓冲区:文档被写入到内核缓冲区,此时文档还不可以被检索
- transLog:将写入操作记录到transLog中,它用于崩溃恢复。此时文档已持久化
- 副本同步:主分片写完后将数据发生给副本,副本同样把数据写入内核缓冲区、transLog,然后向主分片返回成功
- 完成。此时在用户层面,文档已经写完
- 以下操作为异步的:
- 刷新:es默认每1秒执行一次刷新操作,将内存缓冲区中的文档生成一个新的段文件,并清空缓冲区。生成段文件后,文档就可以被检索到了。
- flush:将段文件强制落盘
- 合并:把多个小的段文件合并为一个大的段文件
文档写入成功后,需要等待1秒钟才可以被搜索到
更新文档的流程
文档的更新操作:因为段文件不支持修改,所以文档更新操作,依赖版本控制和标记删除。es会先读取旧文档的数据,和用户请求合并,生成新的文档,文档的id和旧文档的一致,只是更新了版本号,然后把旧文档标记为已删除,再把新版本的文档保存到段文件中。
把旧版本标记为删除这一步,依赖versionMap。
versionMap: 每个分片在内存中维护的一个哈希表,用于快速检索最新版本的文档,key是文档的id,value是文档的元数据,包括最新的版本号、删除标记、存储在哪个段文件中。versionMap是懒加载的,长期未访问的文档,可能不会被记录到versionMap中,所以首次更新或删除时,需要遍历段文件,会有延迟。
只有在合并段文件时,才会删除旧版本的数据
读请求的流程
- 客户端发送请求到协调节点
- 协调节点解析请求,获取目标索引的分片配置,并且向分片所在主机发送查询请求,随机向主分片、副本分片发送请求,以实现负载均衡。
- 数据节点执行查询请求:
- 查询阶段:遍历分片内的段文件,执行查询语句,得到符合条件的文档id列表,对匹配到的文档进行评分,返回文档id列表、评分结果等到协调节点
- 获取阶段:协调节点收到所有分片的结果后,重新汇总并排序,然后向对应分片发送获取完整文档的请求,
- 协调节点将查询结果返回
对于包含聚合计算的查询,聚合计算在查询阶段完成,每个分片都进行自己的聚合计算,由协调节点进行汇总计算
对于分页查询,例如,1页查10条数据,每个节点都查出本节点的10条数据,返回给协调节点,由协调节点汇总,所以,分页查询,存在请求膨胀的问题
数据的存储
段文件:segment file,lucence索引的最小存储和检索单元,本质是一个不可变的倒排索引文件集合
数据路由
把文档分发到不同分片的方式。
默认的文档路由规则: shard = hash(_id) % 分片数
指定路由字段时的路由规则: shard = hash(路由字段) % 分片数
故障恢复流程
故障恢复流程:
- 主分片所在节点宕机,master检测到该节点失联
- 将该节点上的主分片标记为未分配
- 提升副本分片为新主分片
- 重新为主分片分配副本
分词器
分词器:用于将文本转换为词项,用于建立倒排索引。
分词器的组成:
- 字符串过滤器:过滤某些不需要的字符,例如,去除html标签
- 分词器:按照指定逻辑,对文本进行分词,例如,按照空格分词、按照单词分词
- 词项过滤器:对拆分后的词条进行二次处理,例如,过滤某些停用词、大小写转换、添加同义词
es内置的分词器:
- 标准分词器:standard analyzer,默认的分词器,如果没有指定分词器,就使用该分词器
- 简单分词器:simple,只要遇到不是字母的字符,就将文本解析为词条,所有的词条都是小写的
- 空白字符分词器:whitespace,只要遇到空白,就将文本解析为词条
案例1:查看默认分词器的效果
json
curl -XPOST 'http://192.168.1.3:9200/_analyze' \
-H 'Content-Type:application/json' \
-d '
{
"analyzer": "standard",
"text": "zhangsan lisi@qq.com"
}'
默认分词器的结果:根据空格、@符分组。
json
{
"tokens": [
{
"token": "zhangsan",
"start_offset": 0,
"end_offset": 8,
"type": "<ALPHANUM>",
"position": 0
},
{
"token": "lisi",
"start_offset": 9,
"end_offset": 13,
"type": "<ALPHANUM>",
"position": 1
},
{
"token": "qq.com",
"start_offset": 14,
"end_offset": 20,
"type": "<ALPHANUM>",
"position": 2
}
]
}
ik分词器
ik分词器,一个对中文比较友好的分词器。
安装ik分词器的地址: https://github.com/infinilabs/analysis-ik/releases , ik分词器的版本和es的版本要一致
重启es,它才可以加载ik分词器
测试ik分词器:
json
curl -XGET 'http://192.168.1.3:9200/_analyze' \
-H 'Content-Type:application/json' \
-d '
{
"analyzer": "ik_smart", /* 最粗粒度,分词结果比较少 */
"text": "倚天屠龙记"
}'
curl -XGET 'http://192.168.1.3:9200/_analyze' \
-H 'Content-Type:application/json' \
-d '
{
"analyzer": "ik_max_word", /* 最细粒度,分词结果比较多 */
"text": "倚天屠龙记"
}'
通常的使用方式时,构建索引时,使用ik_max_word,将字段尽可能拆的细碎,搜索时,使用ik_smart,这样即使用户输入了一个长句,系统也能取出其中的核心词汇去匹配索引
查看已安装的插件
命令:GET _cate/plugins
案例:curl -XGET 'http://192.168.1.3:9200/_cat/plugins?v'
定制分词器
案例1:自定义分词器,切分邮箱数据,例如,aaa@qq.com
json
PUT /email_index
{
"settings": {
"analysis": {
"analyzer": {
"email_analyzer": {
"type": "pattern",
"pattern": "@", // 按@分割
"lowercase": true // 小写化
}
}
}
},
"mappings": {
"properties": {
"email": {
"type": "text",
"analyzer": "email_analyzer" // 创建索引时指定分词器
}
}
}
}
案例2:定制分词器,这个分词器,把"&"转换为"and",同时过滤掉the、a这两个单词、过滤掉html标签
json
PUT /my_index
curl -XPUT 'http://192.168.1.3:9200/my_index' \
-H 'Content-Type:application/json' \
-d '
{
"settings": {
"analysis": {
"char_filter": {
"&_to_and": {
"type": "mapping",
"mappings": ["& => and"]
}
},
"filter": {
"my_stopwords": {
"type": "stop",
"stopwords": ["the", "a"]
}
},
"analyzer": {
"my_analyzer": {
"type": "custom",
"char_filter": ["html_strip", "&_to_and"],
"tokenizer": "standard",
"filter": ["lowercase", "my_stopwords"]
}
}
}
}
}'
验证分词器的效果:
json
curl -XGET 'http://192.168.1.3:9200/my_index/_analyze' \
-H 'Content-Type:application/json' \
-d '
{
"analyzer": "my_analyzer",
"text": "My & the first a ARTICLE <a>超链接</a>"
}'
分词结果:从结果中可以看到,&被转换为and,the、a、html标签都被过滤掉了
{
"tokens": [
{
"token": "my",
"start_offset": 0,
"end_offset": 2,
"type": "<ALPHANUM>",
"position": 0
},
{
"token": "and",
"start_offset": 3,
"end_offset": 4,
"type": "<ALPHANUM>",
"position": 1
},
{
"token": "first",
"start_offset": 9,
"end_offset": 14,
"type": "<ALPHANUM>",
"position": 3
},
{
"token": "article",
"start_offset": 17,
"end_offset": 24,
"type": "<ALPHANUM>",
"position": 5
},
{
"token": "超",
"start_offset": 28,
"end_offset": 29,
"type": "<IDEOGRAPHIC>",
"position": 6
},
{
"token": "链",
"start_offset": 29,
"end_offset": 30,
"type": "<IDEOGRAPHIC>",
"position": 7
},
{
"token": "接",
"start_offset": 30,
"end_offset": 35,
"type": "<IDEOGRAPHIC>",
"position": 8
}
]
}
doc value
es中的一种列式存储的数据结构,相当于正排索引,用于排序、过滤、聚合等操作,默认情况下,keyword、integer、date类型的字段,会开启doc value,text类型的字段则不会开启。
开启doc value后,如果用户想要在keyword类型的数据上进行聚合计算,例如,计算用户的平均年龄,那么,es只要从doc value中读取age字段的全部值即可,否则,es需要读取全部文档,解析json字符串,提取age字段的值,进行聚合,效率会大大降低,本质上,doc value是用空间换时间
如果内存不足,正排索引会被写入磁盘
评分机制
es会使用相关度算法,计算出一个文档中的内容和搜索内容之间的关联匹配程度,匹配程度高的优先返回。它使用的相关度算法是TF/IDF算法
TF/IDF算法:term frequency/inverse document frequcency,词频/逆向文档频率算法。
- term frequency:搜索文本中的词条在每个文档中出现了多少次,出现的次数越多,就越相关
- inverse document frequency:搜索文本中的各个词条在整个索引的所有文档中出现的多少次,出现次数越多,就越不相关
字段长度:field length norm,搜索词条越长,相关度越低
分数是如何被计算出来的:
- 对用户输入的关键字分词
- 每个分词分别计算对每个匹配文档的tf值和idf值
- 综合每个分词的tf/idf值,利用公式计算每个文档的总分
查询搜索的执行计划:explain=true
json
curl -XPOST 'http://192.168.1.3:9200/user/_search?explain=true' \
-H 'Content-Type:application/json' \
-d '
{
"query": {
"match": {
"name": "java程序员"
}
}
}'
数据倾斜的优化参数 routing_partition_size
默认情况下,同一个路由值的数据会被分片到相同的分片上,如果配置上当前参数,同一个路由值的参数会被分配到多个分片上,
例如,某个索引下,如果按照不同的商家进行路由,把同一个商家的数据放在同一个分片上,那么,如果某些商家数据量比较大,会造成数据倾斜,这个时候,使用当前参数,把同一个路由值的数据放到多个分片上,可以缓解数据倾斜
指定分区数时的路由计算规则: shard = (hash(routing_filed) + hash(_id) % routing_partition_size) % number_of_shards
在设置分区数时,最好是分片数能够整除分区数,因为这样设置,数据分布会更加均匀。
配置了这个参数,查询、新增数据时,必须指定routing字段
多字段特性
允许对同一个字段以不同的方式建立索引,用于满足不同的查询要求,
案例:
json
curl -XPUT 'http://192.168.1.3:9200/book-v1/' \
-H 'Content-Type:application/json' \
-d '
{
"settings": {
"number_of_shards": 3,
"number_of_replicas": 1
},
"mappings": {
"properties": {
"id": {
"type": "long"
},
"title": {
"type": "text",
"fields": { // 子字段用于精确匹配
"keyword": {
"type": "keyword",
"ignore_above": 256 // 超过256个字符的文本将不会被索引到keyword子字段中,避免过长的字符串占用大量内存,而且超长文本很少需要精确匹配
}
}
}
}
}
}'
实际应用:使用子字段进行聚合分析
json
curl -XGET 'http://192.168.1.3:9200/book-v1/_search' \
-H 'Content-Type:application/json' \
-d '
{
"size": 0,
"aggs": {
"titleCount": {
"terms": {
"field": "title.keyword"
}
}
}
}'
分页查询的另一种方式
scroll api 分批查询
分批查询的使用场景:当用户想要导出es中的所有数据时,如果数据量比较大,一次全部查询出来会导致oom,分页查询,又会有深分页的问题,所以es提供了scroll api,它类似于分页查询,只不过在查完一页之后,会带上下一页的标识,下一次查询时直接从指定位置开始,避免深分页。
scroll会在第一次搜索的时候,保存一个当前视图的快照,之后只会根据该旧的视图快照提供数据搜索,如果这个期间数据变更,是不会让用户看到的。
案例:
json
-- 第一次查询
curl -XPOST 'http://192.168.1.3:9200/book/_search?scroll=10m' \ /* scroll=10m,指分页视图保持10分钟 */
-H 'Content-Type:application/json' \
-d '
{
"query": {
"match_all": {}
},
"size": 1
}'
-- 第二次查询,带上上一步返回的scroll_id
curl -XPOST 'http://192.168.1.3:9200/_search/scroll' \
-H 'Content-Type:application/json' \
-d '
{
"scroll": "1m", "scroll_id":"FGluY2x1ZGVfY29udGV4dF91dWlkDXF1ZXJ5QW5kRmV0Y2gBFkFkcm5jaTZtUWlLTTRxajNLMGF4cHcAAAAAAAAANRZBYlVtTUt5RVQ1U2hmR3hoekFYdTF3"
}'
-- 第三次查询
curl -XPOST 'http://192.168.1.3:9200/_search/scroll' \
-H 'Content-Type:application/json' \
-d '
{
"scroll": "1m", "scroll_id":"FGluY2x1ZGVfY29udGV4dF91dWlkDXF1ZXJ5QW5kRmV0Y2gBFkFkcm5jaTZtUWlLTTRxajNLMGF4cHcAAAAAAAAANRZBYlVtTUt5RVQ1U2hmR3hoekFYdTF3"
}'
-- 当数据全部获取完后,清理 scroll 上下文
curl -XDELETE 'http://192.168.1.3:9200/_search/scroll' \
-H 'Content-Type:application/json' \
-d '
{
"scroll_id": "FGluY2x1ZGVfY29udGV4dF91dWlkDXF1ZXJ5QW5kRmV0Y2gBFkFkcm5jaTZtUWlLTTRxajNLMGF4cHcAAAAAAAAANBZBYlVtTUt5RVQ1U2hmR3hoekFYdTF3"
}'
search after
基于游标机制,每次只会查询指定id之后的数据,强制要求排序。
适用场景:
- scroll api适合离线导出
- search after适合分页
案例:
json
curl -XGET 'http://192.168.1.3:9200/book/_search' \
-H 'Content-Type:application/json' \
-d '
{
"query": {
"match_all": {}
},
"size": 100,
"sort": [
{"_id": "asc"}
],
"search_after": [1]
}'
Q&A
keyword和integer在查询时有什么区别?
keyword是string类型,integer是整数类型。integer作为数字会更加节约内存;如果需要进行数值计算,使用integer,如果要支持模糊查询,使用keyword
低效索引是怎么回事?
数据写入es时速度慢、资源消耗高
什么样的请求需要预处理?
预处理:数据在写入es前对数据进行转换、清洗等操作。es中有专门的节点来执行预处理操作,通常,每个节点都可以作为预处理节点。
在实际生产中,预处理主要是由外部组件来做,例如,在java代码中处理好数据,直接写入es,或者在logstash中解析好日志,再写入es。
集群数据量比较大怎么办?
将集群扩展为多个,在应用层进行路由。ES官方建议单分片数据控制在50-100G,同时ES集群节点(网关、主节点、数据节点)数量控制在100个以内,避免故障恢复较慢
大商家数据量大怎么处理: 大商家单独一个集群,物理层隔离,大商家集群根据商家编码路由