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个以内,避免故障恢复较慢

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

参考

相关推荐
Elastic 中国社区官方博客6 小时前
不到 5 分钟完成本地部署:Jina embedding 模型现已支持本地部署
大数据·人工智能·elasticsearch·搜索引擎·embedding·jina
二进制流水搬运工6 小时前
入职第一天拉了23个仓库,我写了个脚本解放双手
大数据·elasticsearch·搜索引擎
Elasticsearch7 小时前
在不到一小时内将 Datadog Kubernetes 仪表板迁移到 Elastic Observability
elasticsearch
.柒宇.8 小时前
Elasticsearch 核心概念与系统架构详解
elasticsearch·系统架构
风行無痕10 小时前
单机Elasticsearch 8.19.18安全升级到9.4.3的过程
大数据·elasticsearch·搜索引擎
Elastic 中国社区官方博客11 小时前
使用重新设计的 AutoOps 更快地进行 Elasticsearch 问题排查
大数据·运维·前端·人工智能·elasticsearch·搜索引擎·全文检索
阿拉雷️13 小时前
【搜索实战】Spring Boot 3.3 + AI Agent × Elasticsearch:让AI自动优化搜索策略,查询速度从2秒压到50毫秒
人工智能·spring boot·elasticsearch
阿里云大数据AI技术1 天前
阿里云 ES AI 引擎版:面向 Agent 场景,为亿级租户、千亿规模向量设计的搜索引擎
人工智能·elasticsearch·agent
阿里云大数据AI技术1 天前
FalconSeek 技术解析:阿里云 Elasticsearch 云原生内核如何让查询性能飙升600%
人工智能·elasticsearch
Elastic 中国社区官方博客1 天前
将你的 Grafana Kubernetes 仪表板迁移到 Elastic Observability:相同的 PromQL,30 倍更快的查询
大数据·人工智能·elasticsearch·搜索引擎·容器·kubernetes·grafana