elasticsearch

1. Elasticsearch 入门

1.1 什么是 Elasticsearch

Elasticsearch 是由 elastic 公司开发的一套搜索引擎技术,它是 elastic 技术栈中的一部分。完整的技术栈包括:

  • Elasticsearch:用于数据存储、计算和搜索
  • Logstash/Beats:用于数据收集
  • Kibana:用于数据可视化

1.1.1 安装 Elasticsearch

1
2
3
4
5
6
7
8
9
10
11
docker run -d \
--name es \
-e "ES_JAVA_OPTS=-Xms512m -Xmx512m" \
-e "discovery.type=single-node" \
-v es-data:/usr/share/elasticsearch/data \
-v es-plugins:/usr/share/elasticsearch/plugins \
--privileged \
--network hmall \
-p 9200:9200 \
-p 9300:9300 \
elasticsearch:7.12.1

1.1.2.安装 Kibana

通过下面的 Docker 命令,即可部署 Kibana:

1
2
3
4
5
6
docker run -d \
--name kibana \
-e ELASTICSEARCH_HOSTS=http://es:9200 \
--network=hmall \
-p 5601:5601 \
kibana:7.12.1

1.2 倒排索引

1.2.1 正序索引

1
select * from tb_goods where title like '%手机%';

image.png

  • 1)检查到搜索条件为 like ‘%手机%’,需要找到 title 中包含手机的数据
  • 2)逐条遍历每行数据(每个叶子节点),比如第 1 次拿到 id 为 1 的数据
  • 3)判断数据中的 title 字段值是否符合条件
  • 4)如果符合则放入结果集,不符合则丢弃
  • 5)回到步骤 1

1.2.2 倒排索引

倒排索引中有两个非常重要的概念:

  • 文档(Document):用来搜索的数据,其中的每一条数据就是一个文档。例如一个网页、一个商品信息
  • 词条(Term):对文档数据或用户搜索数据,利用某种算法分词,得到的具备含义的词语就是词条。例如:我是中国人,就可以分为:我、是、中国人、中国、国人这样的几个词条

创建倒排索引是对正向索引的一种特殊处理和应用,流程如下:

  • 将每一个文档的数据利用分词算法根据语义拆分,得到一个个词条
  • 创建表,每行数据包括词条、词条所在文档 id、位置等信息
  • 因为词条唯一性,可以给词条创建正向索引
    此时形成的这张以词条为索引的表,就是倒排索引表,两者对比如下:

image.png

那么为什么一个叫做正向索引,一个叫做倒排索引呢?

  • 正向索引是最传统的,根据 id 索引的方式。但根据词条查询时,必须先逐条获取每个文档,然后判断文档中是否包含所需要的词条,是根据文档找词条的过程。
  • 而倒排索引则相反,是先找到用户要搜索的词条,根据词条得到保护词条的文档的 id,然后根据 id 获取文档。是根据词条找文档的过程。

1.3 IK 分词器

Elasticsearch 的关键就是倒排索引,而倒排索引依赖于对文档内容的分词,而分词则需要高效、精准的分词算法,IK 分词器就是这样一个中文分词算法。

1.3.1 IK 分词器的安装

首先,查看之前安装的 Elasticsearch 容器的 plugins 数据卷目录:

1
docker volume inspect es-plugins

可以看到 elasticsearch 的插件挂载到了/var/lib/docker/volumes/es-plugins/_data 这个目录。我们需要把 IK 分词器上传至这个目录。
最后重启 Elasticsearch 容器

1
docker restart es

1.3.2 IK 分词器的使用

在 Kibana 的 DevTools 上来测试分词器的效果:

1
2
3
4
5
POST /_analyze
{
"analyzer": "ik_smart",
"text": "黑马程序员学习java太棒了"
}

2.索引库操作

2.1 Mapping 映射属性

Mapping 是对索引库中文档的约束,常见的 Mapping 属性包括:

  • type:字段数据类型,常见的简单类型有:
    • 字符串:text(可分词的文本)、keyword(精确值,例如:品牌、国家、ip 地址)
    • 数值:long、integer、short、byte、double、float、
    • 布尔:boolean
    • 日期:date
    • 对象:object
  • index:是否创建索引,默认为 true
  • analyzer:使用哪种分词器
  • properties:该字段的子字段

2.2.索引库的 CRUD

由于 Elasticsearch 采用的是 Restful 风格的 API,因此其请求方式和路径相对都比较规范,而且请求参数也都采用 JSON 风格。
我们直接基于 Kibana 的 DevTools 来编写请求做测试,由于有语法提示,会非常方便。

2.2.1.创建索引库和映射

基本语法:

  • 请求方式:PUT
  • 请求路径:/索引库名,可以自定义
  • 请求参数:mapping 映射
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
PUT /heima
{
"mappings": {
"properties": {
"info":{
"type": "text",
"analyzer": "ik_smart"
},
"email":{
"type": "keyword",
"index": "false"
},
"name":{
"properties": {
"firstName": {
"type": "keyword"
}
}
}
}
}
}

2.2.2.查询索引库

基本语法:

  • 请求方式:GET
  • 请求路径:/索引库名
  • 请求参数:无
1
GET /heima

2.2.3.修改索引库

倒排索引结构虽然不复杂,但是一旦数据结构改变(比如改变了分词器),就需要重新创建倒排索引,这简直是灾难。因此索引库一旦创建,无法修改 mapping。

虽然无法修改 mapping 中已有的字段,但是却允许添加新的字段到 mapping 中,因为不会对倒排索引产生影响。因此修改索引库能做的就是向索引库中添加新字段,或者更新索引库的基础属性。

1
2
3
4
5
6
7
8
PUT /heima/_mapping
{
"properties": {
"age":{
"type": "integer"
}
}
}

2.2.4.删除索引库

语法:

  • 请求方式:DELETE
  • 请求路径:/索引库名
  • 请求参数:无
1
DELETE /heima

3.文档操作

有了索引库,接下来就可以向索引库中添加数据了。
Elasticsearch 中的数据其实就是 JSON 风格的文档。操作文档自然保护增、删、改、查等几种常见操作

3.1 新增文档

1
2
3
4
5
6
7
8
9
POST /heima/_doc/1
{
"info": "黑马程序员Java讲师",
"email": "zy@itcast.cn",
"name": {
"firstName": "云",
"lastName": "赵"
}
}

3.2.查询文档

1
GET /heima/_doc/1

3.3 删除文档

1
DELETE /heima/_doc/1

3.4 修改文档

3.4.1.全量修改

全量修改是覆盖原来的文档,其本质是两步操作:

  • 根据指定的 id 删除文档
  • 新增一个相同 id 的文档
    注意:如果根据 id 删除时,id 不存在,第二步的新增也会执行,也就从修改变成了新增操作了。
1
2
3
4
5
6
7
8
9
PUT /heima/_doc/1
{
"info": "黑马程序员高级Java讲师",
"email": "zy@itcast.cn",
"name": {
"firstName": "云",
"lastName": "赵"
}
}

3.4.2.局部修改

局部修改是只修改指定 id 匹配的文档中的部分字段。

1
2
3
4
5
6
POST /heima/_update/1
{
"doc": {
"email": "ZhaoYun@itcast.cn"
}
}

3.5 批处理

1
2
3
4
5
6
7
8
POST _bulk
{ "index" : { "_index" : "test", "_id" : "1" } }
{ "field1" : "value1" }
{ "delete" : { "_index" : "test", "_id" : "2" } }
{ "create" : { "_index" : "test", "_id" : "3" } }
{ "field1" : "value3" }
{ "update" : {"_id" : "1", "_index" : "test"} }
{ "doc" : {"field2" : "value2"} }
  • index 代表新增操作
    • _index:指定索引库名
    • _id 指定要操作的文档 id
    • { “field1” : “value1” }:则是要新增的文档内容
  • delete 代表删除操作
    • _index:指定索引库名
    • _id 指定要操作的文档 id
  • update 代表更新操作
    • _index:指定索引库名
    • _id 指定要操作的文档 id
    • { “doc” : {“field2” : “value2”} }:要更新的文档字段

4. RestApi

4.1 引入依赖

1
2
3
4
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
</dependency>

因为 SpringBoot 默认的 ES 版本是 7.17.10,所以我们需要覆盖默认的 ES 版本

1
2
3
4
5
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<elasticsearch.version>7.12.1</elasticsearch.version>
</properties>

5. DSL 查询

5.1 叶子查询

5.1.1.全文检索查询

  • 全文检索查询(Full Text Queries):利用分词器对用户输入搜索条件先分词,得到词条,然后再利用倒排索引搜索词条。例如:
    • match:
    • multi_match
  • 精确查询(Term-level queries):不对用户输入搜索条件分词,根据字段内容精确值匹配。但只能查找 keyword、数值、日期、boolean 类型的字段。例如:
    • ids
    • term
    • range
  • 地理坐标查询:用于搜索地理位置,搜索方式很多,例如:
    • geo_bounding_box:按矩形搜索
    • geo_distance:按点和半径搜索
1
2
3
4
5
6
7
8
9
GET /{索引库名}/_search
{
"query": {
"multi_match": {
"query": "搜索条件",
"fields": ["字段1", "字段2"]
}
}
}

5.1.2.精确查询

精确查询,英文是 Term-level query,顾名思义,词条级别的查询。也就是说不会对用户输入的搜索条件再分词,而是作为一个词条,与搜索的字段内容精确值匹配。因此推荐查找 keyword、数值、日期、boolean 类型的字段。例如:

  • id
  • price
  • 城市
  • 地名
  • 人名
    等等,作为一个整体才有含义的字段。
1
2
3
4
5
6
7
8
9
10
GET /{索引库名}/_search
{
"query": {
"term": {
"字段名": {
"value": "搜索条件"
}
}
}
}

5.1.3 range 查询

再来看下 range 查询,语法如下:

1
2
3
4
5
6
7
8
9
10
11
GET /{索引库名}/_search
{
"query": {
"range": {
"字段名": {
"gte": {最小值},
"lte": {最大值}
}
}
}
}

range 是范围查询,对于范围筛选的关键字有:

  • gte:大于等于
  • gt:大于
  • lte:小于等于
  • lt:小于

5.2 复合查询

5.2.1 bool 查询

bool 查询,即布尔查询。就是利用逻辑运算来组合一个或多个查询子句的组合。bool 查询支持的逻辑运算有:

  • must:必须匹配每个子查询,类似“与”
  • should:选择性匹配子查询,类似“或”
  • must_not:必须不匹配,不参与算分,类似“非”
  • filter:必须匹配,不参与算分
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
GET /items/_search
{
"query": {
"bool": {
"must": [
{"match": {"name": "手机"}}
],
"should": [
{"term": {"brand": { "value": "vivo" }}},
{"term": {"brand": { "value": "小米" }}}
],
"must_not": [
{"range": {"price": {"gte": 2500}}}
],
"filter": [
{"range": {"price": {"lte": 1000}}}
]
}
}
}

5.3 排序

elasticsearch 默认是根据相关度算分(_score)来排序,但是也支持自定义方式对搜索结果排序。不过分词字段无法排序,能参与排序字段类型有:keyword 类型、数值类型、地理坐标类型、日期类型等。

1
2
3
4
5
6
7
8
9
10
11
12
13
GET /indexName/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"排序字段": {
"order": "排序方式asc和desc"
}
}
]
}

5.4 分页

elasticsearch 默认情况下只返回 top10 的数据。而如果要查询更多数据就需要修改分页参数了。

5.4.1 基础分页

elasticsearch 中通过修改 from、size 参数来控制要返回的分页结果:

  • from:从第几个文档开始
  • size:总共查询几个文档
    类似于 mysql 中的 limit ?, ?

5.4.2 深度分页

elasticsearch 的数据一般会采用分片存储,也就是把一个索引中的数据分成 N 份,存储到不同节点上。这种存储方式比较有利于数据扩展,但给分页带来了一些麻烦。
比如一个索引库中有 100000 条数据,分别存储到 4 个分片,每个分片 25000 条数据。现在每页查询 10 条,查询第 99 页。那么分页查询的条件如下:

1
2
3
4
5
6
7
8
9
10
GET /items/_search
{
"from": 990, // 从第990条开始查询
"size": 10, // 每页查询10条
"sort": [
{
"price": "asc"
}
]
}

试想一下,假如我们现在要查询的是第 999 页数据呢,是不是要找第 9990~10000 的数据,那岂不是需要把每个分片中的前 10000 名数据都查询出来,汇总在一起,在内存中排序?如果查询的分页深度更深呢,需要一次检索的数据岂不是更多?
由此可知,当查询分页深度较大时,汇总数据过多,对内存和 CPU 会产生非常大的压力。
因此 elasticsearch 会禁止 from+ size 超过 10000 的请求。
针对深度分页,elasticsearch 提供了两种解决方案:

  • search after:分页时需要排序,原理是从上一次的排序值开始,查询下一页数据。官方推荐使用的方式。
  • scroll:原理将排序后的文档 id 形成快照,保存下来,基于快照做分页。官方已经不推荐使用。

假设你有一张按时间排序的日志表,想分页查询每页 10 条记录。

传统分页(from 和 size)
第一页:from=0, size=10
ES 返回第 1 到 10 条记录。

第二页:from=10, size=10
ES 需要从头遍历前 10 条记录,跳过它们,再返回第 11 到 20 条。

第 100 页:from=1000, size=10
ES 需要从头遍历前 1000 条记录,跳过它们,再返回第 1001 到 1010 条。

search_after 分页
第一页:size=10, sort=时间戳
ES 返回第 1 到 10 条记录,并记录第 10 条的时间戳(比如 2023-10-01 12:00:00)。

第二页:size=10, sort=时间戳, search_after=[2023-10-01 12:00:00]
ES 直接从 2023-10-01 12:00:00 之后开始查询,返回第 11 到 20 条记录。

第 100 页:size=10, sort=时间戳, search_after=[上次最后一条的时间戳]
ES 直接从上一次的最后一条记录开始查询,不需要遍历前面的数据。

总结:
大多数情况下,我们采用普通分页就可以了。查看百度、京东等网站,会发现其分页都有限制。例如百度最多支持 77 页,每页不足 20 条。京东最多 100 页,每页最多 60 条。
因此,一般我们采用限制分页深度的方式即可,无需实现深度分页。

5.5 高亮

观察页面源码,你会发现两件事情:

  • 高亮词条都被加了<em>标签
  • <em>标签都添加了红色样式

css 样式肯定是前端实现页面的时候写好的,但是前端编写页面的时候是不知道页面要展示什么数据的,不可能给数据加标签。而服务端实现搜索功能,要是有 elasticsearch 做分词搜索,是知道哪些词条需要高亮的。
因此词条的高亮标签肯定是由服务端提供数据的时候已经加上的。

因此实现高亮的思路就是:

  • 用户输入搜索关键字搜索数据
  • 服务端根据搜索关键字到 elasticsearch 搜索,并给搜索结果中的关键字词条添加 html 标签
  • 前端提前给约定好的 html 标签添加 CSS 样式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
GET /{索引库名}/_search
{
"query": {
"match": {
"搜索字段": "搜索关键字"
}
},
"highlight": {
"fields": {
"高亮字段名称": {
"pre_tags": "<em>",
"post_tags": "</em>"
}
}
}
}
  • 搜索必须有查询条件,而且是全文检索类型的查询条件,例如 match
  • 参与高亮的字段必须是 text 类型的字段
  • 默认情况下参与高亮的字段要与搜索字段一致,除非添加:required_field_match=false

elasticsearch
https://kongshuilinhua.github.io/2025/03/04/elasticsearch/
作者
FireFLy
发布于
2025年3月4日
许可协议