ElasticSearch-基本入门

简介

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的小技巧:

基本概念

倒排索引

倒排索引:倒排索引是相当于正向索引来说的,例如,根据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'

多索引搜索

类似于分库分表,例如,索引中存储着日志数据,日志数据按天分到不同的索引中,如果想要同时从多个索引中搜索,就需要使用多索引搜索语法

多索引搜索的方式:

别名

别名,相当于索引的软链接。

别名的作用:

  • 简化索引名称:一个复杂的索引名可以被替换为一个简单的别名
  • 实现"零停机"管理:索引重建或数据迁移的过程中,用户可以先创建一个新索引,把数据从老索引迁移过去,然后再把别名从老索引指向新索引,整个过程对用户无感知
  • 同时查询多个索引:一个别名可以指向多个索引,当用户查询这个别名时,会自动在所有的索引上进行搜索

创建别名

命令: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"
}'

基于游标机制,每次只会查询指定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个以内,避免故障恢复较慢

大商家数据量大怎么处理: 大商家单独一个集群,物理层隔离,大商家集群根据商家编码路由

参考

相关推荐
海兰2 小时前
Elasticsearch 相关性引擎(ESRE)核心能力
大数据·elasticsearch·搜索引擎
Q鑫1 天前
Elastricsearch部署详解
运维·elasticsearch
Elastic 中国社区官方博客1 天前
Elasticsearch:通过最小分数确保语义精度
大数据·数据库·人工智能·elasticsearch·搜索引擎·ai·全文检索
qq8406122332 天前
Nodejs+vue基于elasticsearch的高校科研期刊信息管理系统_mb8od
前端·vue.js·elasticsearch
Elastic 中国社区官方博客2 天前
在 Kubernetes 上的依赖管理
大数据·elasticsearch·搜索引擎·云原生·容器·kubernetes·全文检索
Elastic 中国社区官方博客2 天前
Agentic CI/CD:使用 Kubernetes 部署门控,结合 Elastic MCP Server
大数据·人工智能·elasticsearch·搜索引擎·ci/cd·容器·kubernetes
pyniu2 天前
Elasticsearch学习
后端·学习·elasticsearch·搜索引擎
Elasticsearch2 天前
Elasticsearch:通过最小分数确保语义精度
elasticsearch
Elasticsearch2 天前
Agentic CI/CD:使用 Kubernetes 部署门控,结合 Elastic MCP Server
elasticsearch