欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 财经 > 金融 > elasticsearch

elasticsearch

2025/2/14 4:25:48 来源:https://blog.csdn.net/2302_78946634/article/details/145569783  浏览:    关键词:elasticsearch

目录


1 认识

1.1 认识与安装

1.2 倒排索引

1.2.1 正向索引

1.2.2 倒排索引

1.2.3 正向索引 vs 倒排索引

 1.3 基础概念

 1.3.1 文档和字段

 1.3.2 索引和映射

 1.3.2  MySQL 与 Elasticsearch 对比

 1.4 IK分词器

 1.4.1 安装

 1.4.2 使用IK分词器

 1.4.3 拓展词典

 1.4.4 总结

2 索引库操作

 2.1 Mapping映射属性

 2.2 索引库的CRUD

2.2.1 创建索引库和映射

2.2.2 查询索引库

2.2.3 修改索引库

2.2.4 删除索引库

2.2.5 总结

索引库操作

注意事项

 3 文档操作

3.1 新增

3.2 查询文档

3.3 删除文档

3.4 修改文档

3.4.1 全量修改

3.4.2 局部修改

3.5 批处理

3.6 总结

4 RestAPI

4.1 初始化RestClient

4.2 创建索引库

4.2.1 Mapping映射

4.2.2 创建索引

4.3 删除索引库

4.4 判断索引库是否存在

4.5 总结

5  RestClient操作文档

5.1 新增文档

5.2 查询文档 

5.3 删除文档

5.4 修改文档

5.5 批量导入文档

6 DSL查询

6.1 Elasticsearch查询分类

6.2 DSL查询基本结构

6.3 叶子查询

6.3.1 全文检索查询

6.3.2 精确查询

6.4 复合查询-bool查询

6.5 排序

6.7 分页

6.7.1 基础分页

6.7.2 深度分页

6.8 高亮

6.8.1 高亮原理

6.8.2 实现高亮

 7 RestClient查询

7.1 查询步骤

7.2 叶子查询

7.3 复合查询-bool查询

7.4 排序

7.5 分页

7.6 高亮

8 数据聚合

8.1 聚合类型

(1)Bucket 聚合

(2)带条件聚合

(3)Metric聚合

8.2 聚合总结


1 认识

1.1 认识与安装

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

  • Elasticsearch:用于数据存储、计算和搜索

  • Logstash/Beats:用于数据收集

  • Kibana:用于数据可视化

Elasticsearch 提供核心的数据存储、搜索、分析功能,而 Kibana 作为 Elasticsearch 的可视化控制台,支持数据搜索、展示、统计、聚合,并形成图形化报表,还能监控 Elasticsearch 集群状态,并提供开发控制台(DevTools)以支持 Elasticsearch 的 Restful API 接口。

(1)安装 Elasticsearch 可以通过 Docker 命令实现单机版本的部署:

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 hm-net \-p 9200:9200 \-p 9300:9300 \elasticsearch:7.12.1

这个命令使用了 Elasticsearch 的 7.12.1 版本,因为企业中应用较多的是 8 以下的版本。

(2)安装 Kibana 同样可以通过 Docker 命令完成:

docker run -d \
--name kibana \
-e ELASTICSEARCH_HOSTS=http://es:9200 \
--network=hm-net \
-p 5601:5601  \
kibana:7.12.1

安装完成后,可以通过访问 5601 端口看到控制台页面,选择 "Explore on my own" 进入主页面,然后选中 Dev tools 进入开发工具页面。

 1.2 倒排索引

倒排索引是Elasticsearch实现高效搜索的核心技术之一,与传统的正向索引不同,它专为快速全文搜索设计。

倒排索引的概念是基于MySQL这样的正向索引而言的。

1.2.1 正向索引

  • 定义:传统的索引方式,基于文档ID进行索引。

  • 特点

    • 适合精确匹配(如根据ID查询)。

    • 对于模糊匹配(如like '%手机%'),需要全表扫描,效率低。

  • 流程

    1. 检查搜索条件(如like '%手机%')。

    2. 逐条遍历每行数据(叶子节点)。

    3. 判断数据是否符合条件。

    4. 符合则放入结果集,否则丢弃。

    5. 重复上述步骤。

1.2.2 倒排索引

  • 定义:基于词条的索引方式,解决模糊匹配问题。

  • 核心概念

    • 文档(Document):搜索的数据单元(如一条商品信息)。

    • 词条(Term):对文档内容分词后得到的词语(如“小米”、“手机”)。

  • 创建流程

    1. 对文档内容分词,得到词条。

    2. 创建倒排索引表,记录词条、文档ID、位置等信息。

    3. 为词条创建正向索引。

  • 搜索流程(以“华为手机”为例):

    1. 对搜索内容分词,得到词条(“华为”、“手机”)。

    2. 在倒排索引中查找词条,得到包含词条的文档ID(1, 2, 3)。

    3. 根据文档ID在正向索引中查找具体文档。

  • 优点

    • 模糊搜索速度快。

    • 无需全表扫描。

  • 缺点

    • 只能对词条创建索引,无法对字段排序。

 1.2.3 正向索引 vs 倒排索引

  • 正向索引是最传统的,根据id索引的方式。但根据词条查询时,必须先逐条获取每个文档,然后判断文档中是否包含所需要的词条,是根据文档找词条的过程

    • 优点:

      • 可以给多个字段创建索引

      • 根据索引字段搜索、排序速度非常快

    • 缺点:

      • 根据非索引字段,或者索引字段中的部分词条查找时,只能全表扫描。

  • 倒排索引则相反,是先找到用户要搜索的词条,根据词条得到保护词条的文档的id,然后根据id获取文档。是根据词条找文档的过程

    • 优点:

      • 根据词条搜索、模糊搜索时,速度非常快

    • 缺点:

      • 只能给词条创建索引,而不是字段

      • 无法根据字段做排序

 1.3 基础概念

 1.3.1 文档和字段

文档(Document):ES中的基本数据单元,存储为JSON格式。

        示例:

{"id": 1,"title": "小米手机","price": 3499
}

 字段(Field):文档中的属性(如titleprice)。

 1.3.2 索引和映射

索引(Index):类似数据库中的表,用于存储相同类型的文档。

  • 示例:

    • 商品索引:存储所有商品文档。

    • 用户索引:存储所有用户文档。

 映射(Mapping):定义索引中文档的字段类型和约束,类似数据库的表结构。

1.3.2  MySQL 与 Elasticsearch 对比

MySQLElasticsearch说明
TableIndex索引,就是文档的集合,类似数据库的表
RowDocument文档,类似数据库的行
ColumnField字段,类似数据库的列
SchemaMapping映射,定义字段类型和约束,类似数据库的表结构(Schema)
SQLDSL

DSL是elasticsearch提供的JSON风格的请求语句,用来操作elasticsearch,实现CRUD

  • MySQL

    • 适合事务操作,确保数据安全和一致性。

  • Elasticsearch

    • 适合海量数据的搜索、分析和计算。

  • 结合使用

    • 对安全性要求较高的写操作,使用mysql实现

    • 对查询性能要求较高的搜索需求,使用elasticsearch实现

 1.4 IK分词器

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

 1.4.1 安装

(1)在线安装

docker exec -it es ./bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.12.1/elasticsearch-analysis-ik-7.12.1.zip
docker restart es

(2)离线安装

  1. 解压 IK 分词器插件。

  2. 上传至 Elasticsearch 的插件目录(如/var/lib/docker/volumes/es-plugins/_data)。

  3. 重启 Elasticsearch。

 1.4.2 使用IK分词器

IK分词器包含两种模式:

  • ik_smart:智能语义切分

  • ik_max_word:最细粒度切分

POST /_analyze
{"analyzer": "ik_smart","text": "黑马程序员学习java太棒了"
}
{"tokens": [{"token": "黑马", "type": "CN_WORD"},{"token": "程序员", "type": "CN_WORD"},{"token": "学习", "type": "CN_WORD"},{"token": "java", "type": "ENGLISH"},{"token": "太棒了", "type": "CN_WORD"}]
}

1.4.3 拓展词典

要想正确分词,IK分词器的词库也需要不断的更新,IK分词器提供了扩展词汇的功能。

 1.4.4 总结

分词器的作用是什么?

  • 创建倒排索引时,对文档分词

  • 用户搜索时,对输入的内容分词

IK分词器有几种模式?

  • ik_smart:智能切分,粗粒度

  • ik_max_word:最细切分,细粒度

IK分词器如何拓展词条?如何停用词条?

  • 利用config目录的IkAnalyzer.cfg.xml文件添加拓展词典和停用词典

  • 在词典中添加拓展词条或者停用词条

2 索引库操作

 2.1 Mapping映射属性

  • Mapping 是对索引库中文档的约束,类似于数据库中的表结构。

  • 它定义了字段的类型、是否参与搜索、是否分词等信息。

属性名说明
type字段数据类型,常见类型包括:
- 字符串:text(可分词的文本)、keyword(精确值,如品牌、IP地址)
- 数值:longintegershortbytedoublefloat
- 布尔:boolean
- 日期:date
- 对象:object
index是否创建索引,默认为 true
analyzer指定分词器(如 ik_smart)。
properties定义子字段(用于嵌套对象)。

 例如:

字段名字段类型类型说明是否参与搜索是否分词分词器
ageinteger整数✔️——
weightfloat浮点数✔️——
isMarriedboolean布尔值✔️——
infotext字符串(需要分词)✔️✔️IK
emailkeyword字符串(不分词)——
scorefloat数组(元素类型为 float✔️——
name.firstNamekeyword字符串(不分词)✔️——
name.lastNamekeyword字符串(不分词)✔️——

2.2 索引库的CRUD

2.2.1 创建索引库和映射

PUT /索引库名
{"mappings": {"properties": {"字段名": {"type": "字段类型","analyzer": "分词器"  // 可选},"字段名2": {"type": "字段类型","index": "是否创建索引"  // 可选,默认为 true},"字段名3": {"properties": {"子字段": {"type": "字段类型"}}}}}
}
PUT /heima
{"mappings": {"properties": {"info": {"type": "text","analyzer": "ik_smart"},"email": {"type": "keyword","index": "false"},"name": {"properties": {"firstName": {"type": "keyword"}}}}}
}

2.2.2 查询索引库

GET /索引库名

2.2.3 修改索引库

  • 引库一旦创建,无法修改已有字段的 Mapping。

  • 但可以添加新字段,因为不会影响倒排索引。

PUT /索引库名/_mapping
{"properties": {"新字段名": {"type": "字段类型"}}
}
PUT /heima/_mapping
{"properties": {"age": {"type": "integer"}}
}

2.2.4 删除索引库

DELETE /索引库名

2.2.5 总结

索引库操作
操作类型请求方式请求路径说明
创建索引库PUT/索引库名创建索引库并定义 Mapping。
查询索引库GET/索引库名查询索引库的 Mapping 信息。
修改索引库PUT/索引库名/_mapping添加新字段到 Mapping。
删除索引库DELETE/索引库名删除整个索引库。
注意事项
  • Mapping 不可修改:一旦创建,无法修改已有字段的 Mapping。

  • 添加新字段:可以通过修改操作添加新字段。

  • Restful 风格:Elasticsearch 的 API 遵循 Restful 风格,操作统一且易于记忆。

 3 文档操作

3.1 新增

POST /索引库名/_doc/文档id
{"字段1": "值1","字段2": "值2","字段3": {"子属性1": "值3","子属性2": "值4"},
}
POST /heima/_doc/1
{"info": "mikey","email": "zy@itcast.cn","name": {"firstName": "云","lastName": "赵"}
}

3.2 查询文档

  • 语法

    GET /{索引库名称}/_doc/{id}
  • 示例

    GET /heima/_doc/1

3.3 删除文档

  • 语法

    DELETE /{索引库名}/_doc/id值
  • 示例

    DELETE /heima/_doc/1

3.4 修改文档

3.4.1 全量修改

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

  • 根据指定的id删除文档

  • 新增一个相同id的文档

注意:如果根据id删除时,id不存在,第二步的新增也会执行,也就从修改变成了新增操作了。

PUT /{索引库名}/_doc/文档id
{"字段1": "值1","字段2": "值2",// ... 略
}
PUT /heima/_doc/1
{"info": "黑马程序员高级Java讲师","email": "zy@itcast.cn","name": {"firstName": "云","lastName": "赵"}
}

3.4.2 局部修改

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

POST /{索引库名}/_update/文档id
{"doc": {"字段名": "新的值",}
}
POST /heima/_update/1
{"doc": {"email": "ZhaoYun@itcast.cn"}
}

3.5 批处理

批处理采用POST请求,可以同时执行多个操作(新增、删除、更新)。

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"} }:要更新的文档字段

批量新增 示例:

POST /_bulk
{"index": {"_index":"heima", "_id": "3"}}
{"info": "黑马程序员C++讲师", "email": "ww@itcast.cn", "name":{"firstName": "五", "lastName":"王"}}
{"index": {"_index":"heima", "_id": "4"}}
{"info": "黑马程序员前端讲师", "email": "zhangsan@itcast.cn", "name":{"firstName": "三", "lastName":"张"}}

批量删除 示例:

POST /_bulk
{"delete":{"_index":"heima", "_id": "3"}}
{"delete":{"_index":"heima", "_id": "4"}}

3.6 总结

  • 创建文档:POST /{索引库名}/_doc/文档id { json文档 }

  • 查询文档:GET /{索引库名}/_doc/文档id

  • 删除文档:DELETE /{索引库名}/_doc/文档id

  • 修改文档:

    • 全量修改:PUT /{索引库名}/_doc/文档id { json文档 }

    • 局部修改:POST /{索引库名}/_update/文档id { "doc": {字段}}

4 RestAPI

Elasticsearch提供了RestAPI,允许通过HTTP请求与Elasticsearch集群进行交互。Elasticsearch官方提供了不同语言的客户端,这些客户端本质上是组装DSL语句并通过HTTP请求发送给Elasticsearch。

 4.1 初始化RestClient

在Elasticsearch中,所有交互都封装在RestHighLevelClient类中。必须先完成这个对象的初始化,建立与Elasticsearch的连接。

 (1)引入依赖

<dependency><groupId>org.elasticsearch.client</groupId><artifactId>elasticsearch-rest-high-level-client</artifactId>
</dependency>

(2)覆盖默认的ES版本(如果需要):

<properties><elasticsearch.version>7.12.1</elasticsearch.version>
</properties>

(3)初始化RestHighLevelClient:

package com.hmall.item.es;import org.apache.http.HttpHost;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;import java.io.IOException;public class IndexTest {private RestHighLevelClient client;@BeforeEachvoid setUp() {this.client = new RestHighLevelClient(RestClient.builder(HttpHost.create("http://192.168.150.101:9200")));}@Testvoid testConnect() {System.out.println(client);}@AfterEachvoid tearDown() throws IOException {this.client.close();}
}

4.2 创建索引库

4.2.1 Mapping映射

以商城为例

实现搜索功能需要的字段包括三大部分:

  • 搜索过滤字段

    • 分类

    • 品牌

    • 价格

  • 排序字段

    • 默认:按照更新时间降序排序

    • 销量

    • 价格

  • 展示字段

    • 商品id:用于点击后跳转

    • 图片地址

    • 是否是广告推广商品

    • 名称

    • 价格

    • 评价数量

    • 销量

 对应的商品表结构如下,索引库无关字段已经划掉

 以上字段对应的mapping映射属性如下

字段名

字段类型

类型说明

是否

参与搜索

id

long

长整数

name

text

字符串,参与分词搜索

price

integer

以分为单位,所以是整数

stock

integer

字符串,但需要分词

image

keyword

字符串,但是不分词

category

keyword

字符串,但是不分词

brand

keyword

字符串,但是不分词

sold

integer

销量,整数

commentCount

integer

评价,整数

isAD

boolean

布尔类型

updateTime

Date

更新时间

 最终我们的索引库文档结构应该是这样:

PUT /items
{"mappings": {"properties": {"id": {"type": "keyword"},"name":{"type": "text","analyzer": "ik_max_word"},"price":{"type": "integer"},"stock":{"type": "integer"},"image":{"type": "keyword","index": false},"category":{"type": "keyword"},"brand":{"type": "keyword"},"sold":{"type": "integer"},"commentCount":{"type": "integer","index": false},"isAD":{"type": "boolean"},"updateTime":{"type": "date"}}}
}

 4.2.2 创建索引

代码分为三步:

  • 1)创建Request对象。

    • 因为是创建索引库的操作,因此Request是CreateIndexRequest

  • 2)添加请求参数

    • 其实就是Json格式的Mapping映射参数。因为json字符串很长,这里是定义了静态字符串常量MAPPING_TEMPLATE,让代码看起来更加优雅。

  • 3)发送请求

    • client.indices()方法的返回值是IndicesClient类型,封装了所有与索引库操作有关的方法。例如创建索引、删除索引、判断索引是否存在等

具体代码如下: 

@Test
void testCreateIndex() throws IOException {// 1.创建Request对象CreateIndexRequest request = new CreateIndexRequest("items");// 2.准备请求参数request.source(MAPPING_TEMPLATE, XContentType.JSON);// 3.发送请求client.indices().create(request, RequestOptions.DEFAULT);
}static final String MAPPING_TEMPLATE = "{\n" +"  \"mappings\": {\n" +"    \"properties\": {\n" +"      \"id\": {\n" +"        \"type\": \"keyword\"\n" +"      },\n" +"      \"name\":{\n" +"        \"type\": \"text\",\n" +"        \"analyzer\": \"ik_max_word\"\n" +"      },\n" +"      \"price\":{\n" +"        \"type\": \"integer\"\n" +"      },\n" +"      \"stock\":{\n" +"        \"type\": \"integer\"\n" +"      },\n" +"      \"image\":{\n" +"        \"type\": \"keyword\",\n" +"        \"index\": false\n" +"      },\n" +"      \"category\":{\n" +"        \"type\": \"keyword\"\n" +"      },\n" +"      \"brand\":{\n" +"        \"type\": \"keyword\"\n" +"      },\n" +"      \"sold\":{\n" +"        \"type\": \"integer\"\n" +"      },\n" +"      \"commentCount\":{\n" +"        \"type\": \"integer\"\n" +"      },\n" +"      \"isAD\":{\n" +"        \"type\": \"boolean\"\n" +"      },\n" +"      \"updateTime\":{\n" +"        \"type\": \"date\"\n" +"      }\n" +"    }\n" +"  }\n" +"}";

 4.3 删除索引库

@Test
void testDeleteIndex() throws IOException {// 1.创建Request对象DeleteIndexRequest request = new DeleteIndexRequest("items");// 2.发送请求client.indices().delete(request, RequestOptions.DEFAULT);
}

4.4 判断索引库是否存在

@Test
void testExistsIndex() throws IOException {GetIndexRequest request = new GetIndexRequest("items");boolean exists = client.indices().exists(request, RequestOptions.DEFAULT);System.err.println(exists ? "索引库已经存在!" : "索引库不存在!");
}

4.5 总结

JavaRestClient操作elasticsearch的流程基本类似。核心是client.indices()方法来获取索引库的操作对象。

索引库操作的基本步骤:

  • 初始化RestHighLevelClient

  • 创建XxxIndexRequest。XXX是CreateGetDelete

  • 准备请求参数( Create时需要,其它是无参,可以省略)

  • 发送请求。调用RestHighLevelClient#indices().xxx()方法,xxx是createexistsdelete

5  RestClient操作文档

索引库结构与数据库结构还存在一些差异,因此我们要定义一个索引库结构对应的实体。

 5.1 新增文档

代码整体步骤如下:

  • 1)根据id查询商品数据Item

  • 2)将Item封装为ItemDoc

  • 3)将ItemDoc序列化为JSON

  • 4)创建IndexRequest,指定索引库名和id

  • 5)准备请求参数,也就是JSON文档

  • 6)发送请求

@Test
void testAddDocument() throws IOException {// 1.根据id查询商品数据Item item = itemService.getById(100002644680L);// 2.转换为文档类型ItemDoc itemDoc = BeanUtil.copyProperties(item, ItemDoc.class);// 3.将ItemDTO转jsonString doc = JSONUtil.toJsonStr(itemDoc);// 1.准备Request对象IndexRequest request = new IndexRequest("items").id(itemDoc.getId());// 2.准备Json文档request.source(doc, XContentType.JSON);// 3.发送请求client.index(request, RequestOptions.DEFAULT);
}

可以看到与索引库操作的API非常类似,同样是三步走:

  • 1)创建Request对象,这里是IndexRequest,因为添加文档就是创建倒排索引的过程

  • 2)准备请求参数,本例中就是Json文档

  • 3)发送请求

变化的地方在于,这里直接使用client.xxx()的API,不再需要client.indices()了。 

5.2 查询文档 

@Test
void testGetDocumentById() throws IOException {// 1.准备Request对象GetRequest request = new GetRequest("items").id("100002644680");// 2.发送请求GetResponse response = client.get(request, RequestOptions.DEFAULT);// 3.获取响应结果中的sourceString json = response.getSourceAsString();ItemDoc itemDoc = JSONUtil.toBean(json, ItemDoc.class);System.out.println("itemDoc= " + ItemDoc);
}

5.3 删除文档

@Test
void testDeleteDocument() throws IOException {// 1.准备Request,两个参数,第一个是索引库名,第二个是文档idDeleteRequest request = new DeleteRequest("item", "100002644680");// 2.发送请求client.delete(request, RequestOptions.DEFAULT);
}

5.4 修改文档

  • 全量修改:本质是先根据id删除,再新增

  • 局部修改:修改文档中的指定字段值

在RestClient的API中,全量修改与新增的API完全一致,判断依据是ID:

  • 如果新增时,ID已经存在,则修改

  • 如果新增时,ID不存在,则新增

局部修改 

@Test
void testUpdateDocument() throws IOException {// 1.准备RequestUpdateRequest request = new UpdateRequest("items", "100002644680");// 2.准备请求参数request.doc("price", 58800,"commentCount", 1);// 3.发送请求client.update(request, RequestOptions.DEFAULT);
}

5.5 批量导入文档

批处理与前面讲的文档的CRUD步骤基本一致:

  • 创建Request,但这次用的是BulkRequest

  • 准备请求参数

  • 发送请求,这次要用到client.bulk()方法

BulkRequest本身其实并没有请求参数,其本质就是将多个普通的CRUD请求组合在一起发送。例如:

  • 批量新增文档,就是给每个文档创建一个IndexRequest请求,然后封装到BulkRequest中,一起发出。

  • 批量删除,就是创建N个DeleteRequest请求,然后封装到BulkRequest,一起发出

因此BulkRequest中提供了add方法,用以添加其它CRUD的请求 

@Test
void testBulk() throws IOException {// 1.创建RequestBulkRequest request = new BulkRequest();// 2.准备请求参数request.add(new IndexRequest("items").id("1").source("json doc1", XContentType.JSON));request.add(new IndexRequest("items").id("2").source("json doc2", XContentType.JSON));// 3.发送请求client.bulk(request, RequestOptions.DEFAULT);
}

当我们要导入商品数据时,由于商品数量达到数十万,因此不可能一次性全部导入。建议采用循环遍历方式,每次导入1000条左右的数据。

@Test
void testLoadItemDocs() throws IOException {// 分页查询商品数据int pageNo = 1;int size = 1000;while (true) {Page<Item> page = itemService.lambdaQuery().eq(Item::getStatus, 1).page(new Page<Item>(pageNo, size));// 非空校验List<Item> items = page.getRecords();if (CollUtils.isEmpty(items)) {return;}log.info("加载第{}页数据,共{}条", pageNo, items.size());// 1.创建RequestBulkRequest request = new BulkRequest("items");// 2.准备参数,添加多个新增的Requestfor (Item item : items) {// 2.1.转换为文档类型ItemDTOItemDoc itemDoc = BeanUtil.copyProperties(item, ItemDoc.class);// 2.2.创建新增文档的Request对象request.add(new IndexRequest().id(itemDoc.getId()).source(JSONUtil.toJsonStr(itemDoc), XContentType.JSON));}// 3.发送请求client.bulk(request, RequestOptions.DEFAULT);// 翻页pageNo++;}
}

6 DSL查询

Elasticsearch提供了基于JSON的DSL(Domain Specific Language)语句来定义查询条件,其JavaAPI就是在组织DSL条件。

6.1 Elasticsearch查询分类

ES查询分为两大类:

  • 叶子查询(Leaf query clauses):简单查询,通常在特定字段中查找特定值,很少单独使用。

  • 复合查询(Compound query clauses):组合多个查询,可以是逻辑组合叶子查询或改变叶子查询的行为方式。

 6.2 DSL查询基本结构

GET /{索引库名}/_search
{"query": {"查询类型": {// 查询条件}}
}
GET /items/_search
{"query": {"match_all": {}}
}

以最简单的无条件查询为例,无条件查询的类型是:match_all,因此其查询语句如下:

 

只能查到10条

处于安全考虑,elasticsearch设置了默认的查询页数。

6.3 叶子查询

6.3.1 全文检索查询

利用分词器对用户输入搜索条件先分词,得到词条,然后再利用倒排索引搜索词条。

(1)match

GET /{索引库名}/_search
{"query": {"match": {"字段名": "搜索条件"}}
}

(2)multi_match

match类似的有multi_match,区别在于可以同时对多个字段搜索,而且多个字段都要满足,语法示例:

GET /{索引库名}/_search
{"query": {"multi_match": {"query": "搜索条件","fields": ["字段1", "字段2"]}}
}

6.3.2 精确查询

不会对用户输入的搜索条件再分词,而是作为一个词条,与搜索的字段内容精确值匹配。因此推荐查找keyword、数值、日期、boolean类型的字段。例如:

  • id

  • price

  • 城市

  • 地名

  • 人名

 (1)term查询

GET /{索引库名}/_search
{"query": {"term": {"字段名": {"value": "搜索条件"}}}
}

(2)range查询

GET /{索引库名}/_search
{"query": {"range": {"字段名": {"gte": {最小值},"lte": {最大值}}}}
}

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

  • gte:大于等于

  • gt:大于

  • lte:小于等于

  • lt:小于

6.4 复合查询-bool查询

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

  • must:必须匹配每个子查询,类似“与”

  • should:选择性匹配子查询,类似“或”

  • must_not:必须不匹配,不参与算分,类似“非”

  • filter:必须匹配,不参与算分

 bool查询的语法如下:

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}}}]}}
}

例如:我们要搜索手机,但品牌必须是华为,价格必须是900~1599,那么可以这样写:

GET /items/_search
{"query": {"bool": {"must": [{"match": {"name": "手机"}}],"filter": [{"term": {"brand": { "value": "华为" }}},{"range": {"price": {"gte": 90000, "lt": 159900}}}]}}
}

6.5 排序

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

GET /indexName/_search
{"query": {"match_all": {}},"sort": [{"排序字段": {"order": "排序方式asc和desc"}}]
}

例如:按照商品价格排序:

GET /items/_search
{"query": {"match_all": {}},"sort": [{"price": {"order": "desc"}}]
}

6.7 分页

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

6.7.1 基础分页

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

  • from:从第几个文档开始

  • size:总共查询几个文档

类似于mysql中的limit ?, ?

GET /items/_search
{"query": {"match_all": {}},"from": 0, // 分页开始的位置,默认为0"size": 10,  // 每页文档数量,默认10"sort": [{"price": {"order": "desc"}}]
}

 6.7.2 深度分页

elasticsearch的数据一般会采用分片存储,也就是把一个索引中的数据分成N份,存储到不同节点上。这种存储方式比较有利于数据扩展,但给分页带来了一些麻烦。

比如一个索引库中有100000条数据,分别存储到4个分片,每个分片25000条数据。

要查询第990~1000名的数据。从实现思路来分析,肯定是将所有数据排序,找出前1000名,截取其中的990~1000的部分。但问题来了,我们如何才能找到所有数据中的前1000名呢?

要知道每一片的数据都不一样,第1片上的第900~1000,在另1个节点上并不一定依然是900~1000名。所以我们只能在每一个分片上都找出排名前1000的数据,然后汇总到一起,重新排序,才能找出整个索引库中真正的前1000名,此时截取990~1000的数据即可。

 

但当查询分页深度较大时,汇总数据过多,对内存和CPU会产生非常大的压力。

因此elasticsearch会禁止from+ size 超过10000的请求。

针对深度分页,elasticsearch提供了两种解决方案:

  • search after:分页时需要排序,原理是从上一次的排序值开始,查询下一页数据。官方推荐使用的方式。

  • scroll:原理将排序后的文档id形成快照,保存下来,基于快照做分页。官方已经不推荐使用。

6.8 高亮

6.8.1 高亮原理

  • 用户输入搜索关键字搜索数据

  • 服务端根据搜索关键字到elasticsearch搜索,并给搜索结果中的关键字词条添加html标签

  • 前端提前给约定好的html标签添加CSS样式

 6.8.2 实现高亮

GET /{索引库名}/_search
{"query": {"match": {"搜索字段": "搜索关键字"}},"highlight": {"fields": {"高亮字段名称": {"pre_tags": "<em>","post_tags": "</em>"}}}
}

默认情况下参与高亮的字段要与搜索字段一致,除非添加:required_field_match=false

 7 RestClient查询

 7.1 查询步骤

(1)创建SearchRequest 对象:

  • 指定索引库名称。

SearchRequest request = new SearchRequest("items");

 (2)准备请求参数(DSL):

  • 使用 request.source() 构建 DSL。

  • DSL 中可以包含查询条件、分页、排序、高亮等。

request.source().query(QueryBuilders.matchAllQuery());

(3)发送请求

  • 使用 client.search() 发送请求,得到 SearchResponse

SearchResponse response = client.search(request, RequestOptions.DEFAULT);

(4)解析响应

  • 逐层解析 SearchResponse,获取命中结果。

SearchHits searchHits = response.getHits();
long total = searchHits.getTotalHits().value; // 总条数
SearchHit[] hits = searchHits.getHits(); // 文档数组
for (SearchHit hit : hits) {String source = hit.getSourceAsString(); // 原始文档数据ItemDoc item = JSONUtil.toBean(source, ItemDoc.class); // 反序列化System.out.println(item);
}

 7.2 叶子查询

//Match 查询request.source().query(QueryBuilders.matchQuery("name", "脱脂牛奶"));//Multi Match 查询request.source().query(QueryBuilders.multiMatchQuery("脱脂牛奶", "name", "category"));//Range 查询request.source().query(QueryBuilders.rangeQuery("price").gte(10000).lte(30000));//Term 查询request.source().query(QueryBuilders.termQuery("brand", "华为"));

7.3 复合查询-bool查询

@Test
void testBool() throws IOException {// 1.创建RequestSearchRequest request = new SearchRequest("items");// 2.组织请求参数// 2.1.准备bool查询BoolQueryBuilder bool = QueryBuilders.boolQuery();// 2.2.关键字搜索bool.must(QueryBuilders.matchQuery("name", "脱脂牛奶"));// 2.3.品牌过滤bool.filter(QueryBuilders.termQuery("brand", "德亚"));// 2.4.价格过滤bool.filter(QueryBuilders.rangeQuery("price").lte(30000));request.source().query(bool);// 3.发送请求SearchResponse response = client.search(request, RequestOptions.DEFAULT);// 4.解析响应handleResponse(response);
}

7.4 排序

request.source().sort("price", SortOrder.ASC);

7.5 分页

int pageNo = 1, pageSize = 5;
request.source().from((pageNo - 1) * pageSize).size(pageSize);

7.6 高亮

(1)高亮条件

request.source().highlighter(SearchSourceBuilder.highlight().field("name").preTags("<em>").postTags("</em>")
);

(2)解析高亮结果

Map<String, HighlightField> hfs = hit.getHighlightFields();
if (CollUtils.isNotEmpty(hfs)) {HighlightField hf = hfs.get("name");if (hf != null) {String hfName = hf.getFragments()[0].string(); // 高亮结果item.setName(hfName);}
}

完整代码

private void handleResponse(SearchResponse response) {SearchHits searchHits = response.getHits();// 1.获取总条数long total = searchHits.getTotalHits().value;System.out.println("共搜索到" + total + "条数据");// 2.遍历结果数组SearchHit[] hits = searchHits.getHits();for (SearchHit hit : hits) {// 3.得到_source,也就是原始json文档String source = hit.getSourceAsString();// 4.反序列化ItemDoc item = JSONUtil.toBean(source, ItemDoc.class);// 5.获取高亮结果Map<String, HighlightField> hfs = hit.getHighlightFields();if (CollUtils.isNotEmpty(hfs)) {// 5.1.有高亮结果,获取name的高亮结果HighlightField hf = hfs.get("name");if (hf != null) {// 5.2.获取第一个高亮结果片段,就是商品名称的高亮值String hfName = hf.getFragments()[0].string();item.setName(hfName);}}System.out.println(item);}
}

8 数据聚合

聚合(aggregations)可以让我们极其方便的实现对数据的统计、分析、运算。例如:

  • 什么品牌的手机最受欢迎?

  • 这些手机的平均价格、最高价格、最低价格?

  • 这些手机每月的销售情况如何?

实现这些统计功能的比数据库的sql要方便的多,而且查询速度非常快,可以实现近实时搜索效果。

8.1 聚合类型

(1)Bucket 聚合

        对文档分组。

GET /items/_search
{"size": 0, "aggs": {"category_agg": {"terms": {"field": "category","size": 20}}}
}
  • size:设置size为0,就是每页查0条,则结果中就不包含文档,只包含聚合

  • aggs:定义聚合

    • category_agg:聚合名称,自定义,但不能重复

      • terms:聚合的类型,按分类聚合,所以用term

        • field:参与聚合的字段名称

        • size:希望返回的聚合结果的最大数量

示例:按品牌分组。

 DSL:

GET /items/_search
{"size": 0,"aggs": {"brand_agg": {"terms": {"field": "brand","size": 20}}}
}

 

JavaAPI:

request.source().aggregation(AggregationBuilders.terms("brand_agg").field("brand").size(5)
);

(2)带条件聚合

例如:

我们需要从需求中分析出搜索查询的条件和聚合的目标:

  • 搜索查询条件:

    • 价格高于3000

    • 必须是手机

  • 聚合目标:统计的是品牌,肯定是对brand字段做term聚合

GET /items/_search
{"query": {"bool": {"filter": [{"term": {"category": "手机"}},{"range": {"price": {"gte": 300000}}}]}}, "size": 0, "aggs": {"brand_agg": {"terms": {"field": "brand","size": 20}}}
}

JavaAPI:

BoolQueryBuilder bool = QueryBuilders.boolQuery().filter(QueryBuilders.termQuery("category", "手机")).filter(QueryBuilders.rangeQuery("price").gte(300000));
request.source().query(bool).size(0);
request.source().aggregation(AggregationBuilders.terms("brand_agg").field("brand").size(5)
);

完整代码如下: 

@Test
void testAgg() throws IOException {// 1.创建RequestSearchRequest request = new SearchRequest("items");// 2.准备请求参数BoolQueryBuilder bool = QueryBuilders.boolQuery().filter(QueryBuilders.termQuery("category", "手机")).filter(QueryBuilders.rangeQuery("price").gte(300000));request.source().query(bool).size(0);// 3.聚合参数request.source().aggregation(AggregationBuilders.terms("brand_agg").field("brand").size(5));// 4.发送请求SearchResponse response = client.search(request, RequestOptions.DEFAULT);// 5.解析聚合结果Aggregations aggregations = response.getAggregations();// 5.1.获取品牌聚合Terms brandTerms = aggregations.get("brand_agg");// 5.2.获取聚合中的桶List<? extends Terms.Bucket> buckets = brandTerms.getBuckets();// 5.3.遍历桶内数据for (Terms.Bucket bucket : buckets) {// 5.4.获取桶内keyString brand = bucket.getKeyAsString();System.out.print("brand = " + brand);long count = bucket.getDocCount();System.out.println("; count = " + count);}
}

(3)Metric聚合

以上,我们统计了价格高于3000的手机品牌,形成了一个个桶。现在我们需要对桶内的商品做运算,获取每个品牌价格的最小值、最大值、平均值。

这就要用到Metric聚合了,例如stat聚合,就可以同时获取minmaxavg等结果。

语法如下:

GET /items/_search
{"query": {"bool": {"filter": [{"term": {"category": "手机"}},{"range": {"price": {"gte": 300000}}}]}}, "size": 0, "aggs": {"brand_agg": {"terms": {"field": "brand","size": 20},"aggs": {"stats_meric": {"stats": {"field": "price"}}}}}
}

 JavaAPI:

request.source().aggregation(AggregationBuilders.terms("brand_agg").field("brand").size(5).subAggregation(AggregationBuilders.stats("stats_meric").field("price"))
);

聚合条件的要利用AggregationBuilders这个工具类来构造 

聚合结果与搜索文档同一级别,因此需要单独获取和解析。具体解析语法如下: 

 

可以看到我们在brand_agg聚合的内部,我们新加了一个aggs参数。这个聚合就是brand_agg的子聚合,会对brand_agg形成的每个桶中的文档分别统计。

  • stats_meric:聚合名称

    • stats:聚合类型,stats是metric聚合的一种

      • field:聚合字段,这里选择price,统计价格

由于stats是对brand_agg形成的每个品牌桶内文档分别做统计,因此每个品牌都会统计出自己的价格最小、最大、平均值。

另外,我们还可以让聚合按照每个品牌的价格平均值排序:

8.2 聚合总结

aggs代表聚合,与query同级,此时query的作用是?

  • 限定聚合的的文档范围

聚合必须的三要素:

  • 聚合名称

  • 聚合类型

  • 聚合字段

聚合可配置属性有:

  • size:指定聚合结果数量

  • order:指定聚合结果排序方式

  • field:指定聚合字段

 

好多字儿啊..... 

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com