ElasticSearch
一 认识es
Elasticsearch是由elastic公司开发的一套搜索引擎技术,它是elastic技术栈中的一部分。完整的技术栈包括:
Elasticsearch:用于数据存储、计算和搜索
Logstash/Beats:用于数据收集
Kibana:用于数据可视化
整套技术栈被称为ELK,经常用来做日志收集、系统监控和状态分析等等
二 安装es
1 简介
需要注意的是,最新的es到了8.x的版本,很多的api发生了变化,我所下载的是7.12.1的版本
我们要安装的内容包含2部分:
elasticsearch:存储、搜索和运算
kibana:图形化展示
首先Elasticsearch不用多说,是提供核心的数据存储、搜索、分析功能的。
然后是Kibana,Elasticsearch对外提供的是Restful风格的API,任何操作都可以通过发送http请求来完成。
不过http请求的方式、路径、还有请求参数的格式都有严格的规范。这些规范我们肯定记不住,因此我们要借助于Kibana这个服务。
Kibana是elastic公司提供的用于操作Elasticsearch的可视化控制台。它的功能非常强大,包括:
1 对Elasticsearch数据的搜索、展示
2 对Elasticsearch数据的统计、聚合,并形成图形化报表、图形
3 对Elasticsearch的集群状态监控
4 它还提供了一个开发控制台(DevTools),在其中对Elasticsearch的Restful的API接口提供了语法提示
2 安装
基于linux安装
es安装
设置了内存,默认是1G
9200是ip访问web端口,9300是集群端口
docker run -d \
--name es \
-e "ES_JAVA_0PTS=-Xms512m -Xmx512m" \
-e "discovery.type=single-node" \
-v es-data:/usr/share/elasticsearch/data \
-v es-plugins:/usr/share/elasticsearch/plugins \
--privileged \
-p 9200:9200 \
-p 9300:9300 \
elasticsearch:7.12.1
es测试
访问 ip:9200
返回的是restful风格的数据
kibana安装
环境是es的路径
docker run -d \
--name kibana \
-e ELASTICSEARCH_HOSTS=http://192.168.6.134:9200 \
-p 5601:5601 \
kibana:7.12.1
kibana测试
ip:5601
三 倒排索引
1 传统数据库
传统数据库采用的是正向索引,会给id或者其他字段加索引提高搜索效率。但是当遇到模糊匹配情况的时候,就需要全表扫描
2 es
es在 正向索引 的基础上加上了 倒排索引 。他会根据每一个不同的词条,记录他们各自出现过的文档的id。当进行模糊匹配的时候会先查询词条,在根据id查找数据。因为两次搜索都是有索引的,所以速度会快很多
3 总结
1 什么是文档和词条?
每一条数据就是一个文档
对文档中的内容分词,得到的词语就是词条
2 什么是正向索引?
基于文档id创建索引。根据id查询快,但是查询词条时必须先找到文档,而后判断是否包含词条
3 什么是倒排索引?
对文档内容分词,对词条创建索引,并记录词条所在文档的id。查询时先根据词条查询到文档id,而后根据文档id查询文档
四 IK分词器
1 简介
中文分词往往需要根据语义分析,比较复杂,这就需要用到中文分词器,例如IK分词器。IK分词器是林良益在2006年开源发布的,其采用的正向迭代最细粒度切分算法一直沿用至今。
可以在github上找对应版本的ik,然后放到插件数据卷中,重启es就生效了
2 简单使用
我们分别使用es自带的分词器和ik分词器来测试一下
es自带的分词器
语法说明:
POST: 请求方式
/analyze: 请求路径,这里省略了http://192.168.150.101:9200,有kibana帮我们补充
请求参数,json风格:
analyzer:分词器类型,这里是默认的standard分词器
text:要分词的内容
结果,可以看到默认的分词器他对中文就是一坨,他把每个字都分开了
ik分词器
ik分词器有两种解析,一种是ik_smart, 另一种是ik_max_word 。 其中ik_max_word 分词会分得更多一些。
ik_max_word:
ik_smart:
3 字典扩展
分词器底层是按照一定规则遍历我们需要分词的字符串,然后将这些字符串与内置的“字典“来匹配。匹配到,就是字典里面有这个词就代表可以这样分。但是,es在2012年之后就没有维护了,字典里面的词得不到更新。所以es支持我们自己引入额外的字典来实现字典的更新。
没引入之前,对一些新词无法区分:
如何引入新词典?
在ik文件夹-》config-》IKAnalyzer.cfg 里面引入
1 创建了新的字典,并在里面加上了新的词(加了“泰酷辣”),之后就可以在这个文件直接添加词汇
2 然后修改 IKAnalyzer.cfg 这个文件,引入刚刚创建的文件
3 重新搜索,可以看到词已经引入,“泰酷辣”变成了一个词
4 总结
1 分词器的作用是什么?
创建倒排索引时,对文档分词用户搜索时,对输入的内容分词
2 IK分词器有几种模式?
ik smart:智能切分,粗粒度
ik max word:最细切分,细粒度
3 IK分词器如何拓展分词器词库中的词条?
利用config目录的IkAnalyzer.cfg.xml文件添加拓展词典
在词典中添加拓展词条
五 基本操作
1 基本概念
elasticsearch中的文档数据会被序列化为json格式后存储在elasticsearch中
索引(index):相同类型的文档的集合
映射(mapping):索引中文档的字段约束信息,类似表的结构约束
基本概念与mysql之间的对应
2 索引库操作(对应mysql-对表的操作)
2.1 Mapping映射
mapping是对索引库中文档的约束,常见的mapping属性包括:
type:字段数据类型,常见的简单类型有:
字符串:text(可分词的文本)、keyword(精确值,例如:品牌、国家、ip地址)
数值:long、integer、short、byte、double、float、
布尔:boolean
日期:date
对象:object
index:是否创建索引,默认为true
analyzer:使用哪种分词器
properties:该字段的子字段
mapping属性指的是冒号右边的,即数值。在下面例子中常用的属性都包含。
2.2 索引库crud操作
Elasticsearch提供的所有API都是Restful的接口,遵循Restful的基本规范:
新增操作
创建索引库和mapping的请求语法如下:
实战:
PUT /shuaiqicjx // 这是一个HTTP PUT请求,用于创建或替换名为`shuaiqicjx`的索引。
{
"mappings": { // mappings定义了索引中文档的结构,包括字段的类型和属性。
"properties": { // properties是一个对象,包含了索引中所有字段的定义。
"name": { // `name`是一个嵌套对象类型字段,可以包含多个子字段。
"type": "object", // 指定字段类型为`object`,用于存储嵌套的JSON对象。
"properties": { // 定义`name`对象内部的字段。
"firstname": { // `firstname`字段,用于存储名字。
"type": "keyword" // 字段类型为`keyword`,意味着这个字段的内容在索引时不会被分词,适合用于精确匹配。
},
"lastname": { // `lastname`字段,用于存储姓氏。
"type": "keyword" // 同`firstname`,字段类型为`keyword`。
}
}
},
"sex": { // `sex`字段,用于存储性别信息。
"type": "byte", // 字段类型为`byte`,这是一个单字节的数字类型,用于存储0到255之间的整数值。
"index": false // 设置`index`为`false`,表示这个字段不会被索引,因此不能被搜索到,但仍然可以被存储和在聚合中使用。
},
"age": { // `age`字段,用于存储年龄信息。
"type": "integer" // 字段类型为`integer`,用于存储整数值。
},
"info": { // `info`字段,用于存储一些文本信息。
"type": "text", // 字段类型为`text`,这种类型的字段在索引时会被分词,适合用于全文搜索。
"analyzer": "ik_smart" // 指定使用`ik_smart`分词器,这是一个智能分词器,适用于中文文本,可以更智能地处理中文分词。
}
}
}
}
运行结果,成功:
查询操作
语法:
GET /索引库名
示例:
GET /shuaiqicjx
修改操作
索引库和mapping一旦创建无法修改,但是可以添加新的字段
语法如下:
PUT /索引库名/_mapping
{
"properties": {
"新字段名":{
"type": "integer"
}
}
}
示例:
PUT /shuaiqicjx/_mapping
{
"properties": {
"id": {
"type": "text"
}
}
}
修改成功:
重新查询,可以看到新增字段:
删除操作
语法:
DELETE /索引库名
实战:
3 文档的操作(对应mysql-对表中数据的操作)
3.1 文档的CRUD
新增操作
语法:
POST /索引库名/_doc/文档id
{
"字段1": "值1",
"字段2": "值2",
"字段3": {
"子属性1": "值3",
"子属性2": "值4"
},
// ...
}
注意注意!!!
这里的id,在操作同一个索引库的时候,如果id存在,es会删除原来的数据,添加新的数据,从而变成数据的覆盖,变成更新的操作!!!
如果ID不存在,es没有东西删除,直接添加新的数据,才是增加的操作!!!
实战:
PUT /shuaiqicjx/_doc/1
{
"age": "19",
"info": "陈奕迅是大帅哥",
"name": {
"firstname": "奕迅",
"lastname": "chen"
},
"sex": "1"
}
结果:
可以看到result那里是create。版本是每修改一次就会加一。7-14行是集群的信息
查询操作
GET /索引库名/_doc/文档id
实战:
Get /shuaiqicjx/_doc/1
结果:
修改操作 方法1
语法:
POST /索引库名/_doc/文档id
{
"字段1": "值1",
"字段2": "值2",
"字段3": {
"子属性1": "值3",
"子属性2": "值4"
},
// ...
}
注意注意!!!
1 这个时候我们输入的id就是要修改的id,从而达到修改的效果。
2 并且里面的信息是要完整的,就是说修改的信息和被修改的信息是要一一对应的,不能缺少任何一个字段,否则会抛错误
实战:
PUT /shuaiqicjx/_doc/1
{
"age": "19",
"info": "陈奕迅是大帅哥",
"name": {
"firstname": "奕迅",
"lastname": "CHEN"
},
"sex": "1"
}
结果:
注意这里的result是update。版本是4因为我之前改了几次
修改操作 方法2
语法:
POST /索引库名/_update/文档id
{
"doc": {
"字段名": "新的值"
}
}
这里只用指定修改的字段,不像第一种要全部字段都要写出来,不管改不改
POST /shuaiqicjx/_update/1
{
"doc": {
"age": "50",
"info": "陈奕迅唱歌好好听"
}
}
结果:
删除操作
语法:
DELETE /索引库名/_doc/文档id
实战:
DELETE /shuaiqicjx/_doc/1
结果:
3.2 批量操作
语法:
POST /_bulk
{ "index" : { "_index" : "索引库名", "_id" : "1" } }
{ "字段1" : "值1", "字段2" : "值2" }
{ "index" : { "_index" : "索引库名", "_id" : "1" } }
{ "字段1" : "值1", "字段2" : "值2" }
{ "index" : { "_index" : "索引库名", "_id" : "1" } }
{ "字段1" : "值1", "字段2" : "值2" }
{ "delete" : { "_index" : "test", "_id" : "2" } }
{ "update" : {"_id" : "1", "_index" : "test"} }
{ "doc" : {"field2" : "value2"} }
要在一行里面操作!
六 es的java客户端-RestHighLevelClient
1 客户端初始化
引入es的RestHighLevelClient依赖
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
</dependency>
因为SpringBoot默认的ES版本是7.17.0,所以我们需要覆盖默认的ES版本
<elasticsearch.version>7.12.1</elasticsearch.version>
在测试单元测试连接
private RestHighLevelClient client;
@Test
void testConnection(){
log.debug("client:{}", client);
}
@BeforeEach
void setup(){
client = new RestHighLevelClient(RestClient.builder(
HttpHost.create("http://192.168.6.134:9200")
));
}
@AfterEach
void drop() throws IOException {
if(client != null){
client.close();
}
}
2 商品Mapping映射分析
我们在创建索引库的时候我们需要去分析到底什么是需要包含在索引库里面的,哪些又是需要去进行排序的。并且我们要对字段做分析。并不是数据库表中的所有数据我们都需要添加到es里面。我们要根据实际情况来。
现在有需求:我们要实现商品搜索。
那么索引库的字段肯定要满足页面搜索的需求。
在我们商品搜索这个需求中,页面展示有三种字段。分别是搜索字段,排序字段,和展示字段。前两种我们就需要存储到es里面并对其进行排序,展示字段仅仅只用存到索引库里面。右边的表就是商品的表,我们需要分析什么字段应该干什么事。比如说 name 他属于搜索字段,那么他要存储并且要排序。
3 索引库操作
创建索引库的JavaAPI与Restful接口API对比。一般对索引库操作是不需要修改的。常用的就CRD操作。
新增操作
创建索引库的语句可以另外设置一个常量来保存
private static final String source = "{\n" +
" \"mappings\": {\n" +
" \"properties\": {\n" +
" \"name\": {\n" +
" \"type\": \"object\",\n" +
" \"properties\": {\n" +
" \"firstname\": {\n" +
" \"type\": \"keyword\"\n" +
" },\n" +
" \"lastname\": {\n" +
" \"type\": \"keyword\"\n" +
" }\n" +
" }\n" +
" },\n" +
" \"sex\": {\n" +
" \"type\": \"byte\",\n" +
" \"index\": false\n" +
" },\n" +
" \"age\": {\n" +
" \"type\": \"integer\"\n" +
" },\n" +
" \"info\": {\n" +
" \"type\": \"text\",\n" +
" \"analyzer\": \"ik_smart\"\n" +
" }\n" +
" }\n" +
" }\n" +
"}";
@Test
public void createIndices() throws IOException {
//1 创建请求
CreateIndexRequest request = new CreateIndexRequest("shuaiqicjx");
//2 添加请求参数
request.source(source, XContentType.JSON);
//3 发送请求
client.indices().create(request, RequestOptions.DEFAULT);
}
查询操作
@Test
public void getIndices() throws IOException {
//1 创建请求
GetIndexRequest request = new GetIndexRequest("shuaiqicjx");
//2 发送请求
GetIndexResponse response = client.indices().get(request, RequestOptions.DEFAULT);
// 3. 获取索引的设置、映射和别名等信息
// 获取索引的设置
Map<String, Settings> indexSettings = response.getSettings();
// 获取索引的映射(包括字段类型、分析器等信息)
Map<String, MappingMetadata> indexMappings = response.getMappings();
// 获取索引的别名
Map<String, List<AliasMetadata>> indexAliases = response.getAliases();
// 打印出索引的设置
System.out.println("Index Settings:");
indexSettings.forEach((index, settings) -> {
System.out.println("Index: " + index);
settings.keySet().forEach(key -> {
System.out.println(key + ": " + settings.get(key));
});
});
// 打印出索引的映射
System.out.println("Index Mappings:");
indexMappings.forEach((index, mappingMetadata) -> {
System.out.println("Index: " + index);
System.out.println(mappingMetadata.source().string());
});
// 打印出索引的别名
System.out.println("Index Aliases:");
indexAliases.forEach((index, aliasMetadataList) -> {
System.out.println("Index: " + index);
aliasMetadataList.forEach(aliasMetadata -> {
System.out.println("Alias: " + aliasMetadata.alias());
});
});
}
删除操作
@Test
void deleteIndices() throws IOException {
//1 创建请求
DeleteIndexRequest request = new DeleteIndexRequest("shuaiqicjx");
//2 发送请求
client.indices().delete(request, RequestOptions.DEFAULT);
}
4 文档的CRUD
新增操作
基本语法:
实战:
这里我们将从数据库查询到的商品的信息设置到es里面。我们先通过ID查询到商品,然后转为json格式作为参数传入请求中的参数中。我们提前将需要加入到es中的数据封装成一个class对象,然后将商品中的信息转入这个对象里面。
@Test
void testAdd() {
// 613357
Item item = itemService.getById(613357);
ItemDoc itemDoc = new ItemDoc();
BeanUtil.copyProperties(item, itemDoc);
//1 请求
IndexRequest request = new IndexRequest("items").id(itemDoc.getId());
//2 设置参数
String jsonStr = JSONUtil.toJsonStr(itemDoc);
request.source(jsonStr, XContentType.JSON);
//3 发送请求
try {
client.index(request, RequestOptions.DEFAULT);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
查询操作
查询文档包含查询和解析响应结果两部分。对应的JavaAPI如下:
实战:
@Test
public void testSelect() throws IOException {
//1 请求体 两种方法都可以
GetRequest request = new GetRequest("shuaiqicjx", "1");
// GetRequest request = new GetRequest("shuaiqicjx").id(String.valueOf(1));
//2 发送查询
GetResponse response = client.get(request, RequestOptions.DEFAULT);
String sourceAsString = response.getSourceAsString();
log.info(sourceAsString);
}
修改操作 方法1
和在操作面板中的修改操作1一样,我们在新增操作的时候,如果传入的id在es中存在的话,那么执行的是修改操作,如果是不存在的话那就是新增操作
实战:
@Test
void testUpdate() throws IOException {
// 613357
Item item = itemService.getById(613357);
ItemDoc itemDoc = new ItemDoc();
BeanUtil.copyProperties(item, itemDoc);
itemDoc.setPrice(29900);
//1 请求 两种方式都可以
IndexRequest request = new IndexRequest("items", String.valueOf(item.getId()));
// IndexRequest request = new IndexRequest("items").id(itemDoc.getId());
//2 设置参数
String jsonStr = JSONUtil.toJsonStr(itemDoc);
request.source(jsonStr, XContentType.JSON);
//3 发送请求
IndexResponse response = client.index(request, RequestOptions.DEFAULT);
log.info(String.valueOf(response));
}
修改操作 方法2
方式二:局部更新。只更新指定部分字段。和操作面板上的修改操作2一样的逻辑
实战:
@Test
void testUpdate2() throws IOException {
//1 请求
UpdateRequest request = new UpdateRequest("items", String.valueOf(613357));
//2 设置参数
request.doc(
"price", "29801",
"name", "莎米特"
);
//3 发送请求
client.update(request, RequestOptions.DEFAULT);
}
删除操作
语法:
实战:
@Test
void testDelete() throws IOException {
//1 请求 两种方法都可以
// DeleteRequest deleteRequest = new DeleteRequest("items").id(String.valueOf(613357));
DeleteRequest deleteRequest = new DeleteRequest("items", String.valueOf(613357));
//2 发送
client.delete(deleteRequest, RequestOptions.DEFAULT);
}
5 批量处理
介绍
批处理代码流程与之前类似,只不过构建请求会用到一个名为BulkRequest来封装普通的CRUD请求:
基本语法
实战
需求:我们需要将商品需要存到es中的数据用批量处理储存
我们先分页查询商品数。因为如果一次性查询的话内存会爆炸。每次查询500条,然后存入es中
@Test
void testBulk() throws IOException {
// 1. 准备文档的数据
int pageNo = 1;
int pageSize = 500;
while (true) {
// 分页查询
Page<Item> page = itemService.lambdaQuery()
.eq(Item::getStatus, 1)
.page(new Page<>(pageNo, pageSize)); // 注意分页从1开始
List<Item> items = page.getRecords();
if (items == null || items.isEmpty()) {
break; // 数据为空时结束循环
}
// 2. 创建BulkRequest
BulkRequest bulkRequest = new BulkRequest();
for (Item item : items) {
// 设置参数
ItemDoc itemDoc = new ItemDoc();
BeanUtil.copyProperties(item, itemDoc);
bulkRequest.add(new IndexRequest("items")
.id(String.valueOf(item.getId()))
.source(JSONUtil.toJsonStr(itemDoc), XContentType.JSON));
}
// 3. 发送请求并处理响应
BulkResponse bulkResponse = client.bulk(bulkRequest, RequestOptions.DEFAULT);
// 检查响应是否有失败的操作
if (bulkResponse.hasFailures()) {
// 处理失败的情况,比如记录日志或抛出异常
System.err.println("Bulk request failed: " + bulkResponse.buildFailureMessage());
}
// 4. 翻页
pageNo++;
}
}
结果
我们使用 GET /索引库名/_count 这个命令查询索引库有多少条信息。
可以看到插入成功,插入的数目一致
七 DSL 查询
1 介绍
Elasticsearch提供了基于JSON的DSL(Domain Specific Language)语句以JSON格式来定义查询条件。类似这样: Java客户端就是利用Java语言组织这样的JSON请求参数。
DSL查询可以分为两大类:
叶子查询(Leaf query clauses):一般是在特定的字段里查询特定值,属于简单查询,很少单独使用。
复合查询(Compound query clauses):以逻辑方式组合多个叶子查询或者更改叶子查询的行为方式。
在查询以后,还可以对查询的结果做处理,包括:
1 排序:按照1个或多个字段值做排序
2 分页:根据from和size做分页,类似MySQL
3 高亮:对搜索结果中的关键字添加特殊样式,使其更加醒目
4 聚合:对搜索结果做数据统计以形成报表
2 快速入门
2.1 基于DSL的查询语法如下
最简单的查询,查询全部:
2.2 分析结果
token 查询后显示的总数为10
timed_out 没有超时
shards 是 与集群有关的信息,由于案例是单点部署,所以不用看
hits 查询到的数据
value:一共查询到10000条数据。
这里或许会有一个疑问:这个库不是有88000多条数据吗,为什么只剩下10000条了?
因为我们es默认一次查询只能查询不超过10000条的数据。
而且,下面的relation的”gte“:great than and equals 表示大于等于,表明实际的数目要大于等于10000.
max_score : 相关度问题,搜索字段和字典匹配度问题,之后会讲
hits 真正的数据,记得不要和上面发了hits搞混了
然后下面就和我们查询一条信息的返回结果一模一样了
3 叶子查询
3.1 介绍
叶子查询还可以进一步细分,常见的有:
全文检索(full text)查询:利用分词器对用户输入内容分词,然后去词条列表中匹配。
例如:
1 match_query
2 multi_match_query
精确查询:不对用户输入内容分词,直接精确匹配,一般是查找keyword、数值、日期、布尔等类型。
例如:
1 ids
2 range
3 term
地理(geo)查询:用于搜索地理位置,搜索方式很多。
例如:
1 geo_distance
2 geo_bounding_box
3.2 全文查询(模糊查询)
match
全文检索查询的一种,会对用户输入内容分词,然后去倒排索引库检索,语法:
实战:
GET /items/_search
{
"query": {
"match": {
"name": "托运箱拉杆箱"
}
}
}
结果:
注意 max_score : 相关度问题,搜索字段和字典匹配度问题。es会给搜索的字段与字典之中的匹配度进行打分,底层实现可以去找相关的资料。并且只有全文搜索才会有分数。
这里的 max_score 分数不在是1,所显示的字段是按分数从高到低进行排序的。
multi_match
与match查询类似,只不过允许同时查询多个字段,语法:
实战:
GET /items/_search
{
"query": {
"multi_match": {
"query": "26寸",
"fields": ["name"]
}
}
}
结果:
和match没什么区别
3.3 精确查询
英文是Term-level query,顾名思义,词条级别的查询。也就是说不会对用户输入的搜索条件再分词,而是作为一个词条,与搜索的字段内容精确值匹配。
因此推荐查找keyword、数值、日期、boolean类型的字段。例如id、price、城市、地名、人名等作为一个整体才有含义的字段。
range
基本语法:
-
gte
:大于等于 -
gt
:大于 -
lte
:小于等于 -
lt
:小于
实战:
搜索价格大于等于900小于等于9000的商品
GET /items/_search
{
"query": {
"range": {
"price": {
"gte": 90000,
"lte": 900000
}
}
}
}
结果:
term
基本语法:
实战:
GET /items/_search
{
"query": {
"term": {
"name": {
"value": "旅行箱"
}
}
}
}
需要注意的是,我们精确查询也是在分词库里面来查询的,并且精确查询不会将我们要搜索的字段进行分词。
就像上面实战查询 name 里面的旅行箱一样
”旅行箱“ 这个字段不用再分就可以从字典分词索引中查询到相关的信息,因为旅行箱本来就有意义。
可以看到查询出来的结果有这一行,在 name 这个字段中有 “万向轮旅行箱” 的字眼。
如果我们用这个查询,就会查询不到,因为字典里面并没有这个词,这个词是没有意义的
ids
根据id去查询字段
语法:
实战:
GET /items/_search
{
"query": {
"ids": {
"values": [5141786, 2881652]
}
}
}
4 复合查询
4.1 介绍
复合查询大致可以分为两类:
第一类:基于逻辑运算组合叶子查询,实现组合条件(就是基于分数和逻辑排序)
例如 bool
第二类:基于某种算法修改查询时的文档相关性算分,从而改变文档排名。(就是如果有人加钱了,可以干更多“非法”的事,比如无视分数提高排名)
例如: function_score dis_max
4.2 布尔查询
布尔查询是一个或多个查询子句的组合。
子查询的组合方式有:
1 must:必须匹配每个子查询,类似“与”
2 should:选择性匹配子查询,类似“或”
3 must_not:必须不匹配,不参与算分,类似“非”
4 filter:必须匹配,不参与算分
语法:
GET /索引库名/_search
{
"query": {
"bool": {
"must": [
// 叶子查询
],
"should": [
// 叶子查询
],
"must_not": [
// 叶子查询
],
"filter": [
// 叶子查询
]
}
}
}
实战:
需求:我们要搜索"智能手机",品牌可以是华为,可以是vivo,不能是小米,价格必须是900~1599。
实现:
GET /items/_search
{
"query": {
"bool": {
"must": [
{"match": {"name": "智能手机"}}
],
"should": [
{"term": {"brand": {"value": "华为"}}},
{"term": {"brand": {"value": "vivo"}}}
],
"must_not": [
{"term": {"brand": {"value": "小米"}}}
],
"filter": [
{"range": {"price": {"gte": 90000,"lte": 150090}}}
]
}
}
}
结果:
4.3 算分函数查询(可以自己去了解)
了解地址:
day09-Elasticsearch02 - 飞书云文档
基本介绍
当我们利用match查询时,文档结果会根据与搜索词条的关联度打分(_score),返回结果时按照分值降序排列。
例如,我们搜索 "手机",结果如下:
从elasticsearch5.1开始,采用的相关性打分算法是BM25算法,公式如下:
基于这套公式,就可以判断出某个文档与用户搜索的关键字之间的关联度,还是比较准确的。
但是,在实际业务需求中,常常会有竞价排名的功能。不是相关度越高排名越靠前,而是掏的钱多的排名靠前。
例如在百度中搜索Java培训,排名靠前的就是广告推广:
实现原理
要想认为控制相关性算分,就需要利用 elasticsearch 中的 function_score 查询了。
基本语法:
function_score 查询中包含四部分内容:
-
原始查询条件:query部分,基于这个条件搜索文档,并且基于BM25算法给文档打分,原始算分(query score)
-
过滤条件:filter部分,符合该条件的文档才会重新算分
-
算分函数:符合filter条件的文档要根据这个函数做运算,得到的函数算分(function score),有四种函数
-
weight:函数结果是常量
-
field_value_factor:以文档中的某个字段值作为函数结果
-
random_score:以随机数作为函数结果
-
script_score:自定义算分函数算法
-
-
运算模式:算分函数的结果、原始查询的相关性算分,两者之间的运算方式,包括:
-
multiply:相乘
-
replace:用 function_score 替换 query_core
-
其它,例如:sum、avg、max、min
-
实现流程
function score的运行流程如下:
-
1)根据原始条件查询搜索文档,并且计算相关性算分,称为原始算分(query score)
-
2)根据过滤条件,过滤文档
-
3)符合过滤条件的文档,基于算分函数运算,得到函数算分(function score)
-
4)将原始算分(query score)和函数算分(function score)基于运算模式做运算,得到最终结果,作为相关性算分。
因此,其中的关键点是:
-
过滤条件:决定哪些文档的算分被修改
-
算分函数:决定函数算分的算法
-
运算模式:决定最终算分结果
具体实现
示例:给IPhone这个品牌的手机算分提高十倍,分析如下:
-
过滤条件:品牌必须为IPhone
-
算分函数:常量weight,值为10
-
算分模式:相乘multiply
对应代码如下:
GET /hotel/_search
{
"query": {
"function_score": {
"query": { .... }, // 原始查询,可以是任意条件
"functions": [ // 算分函数
{
"filter": { // 满足的条件,品牌必须是Iphone
"term": {
"brand": "Iphone"
}
},
"weight": 10 // 算分权重为2
}
],
"boost_mode": "multipy" // 加权模式,求乘积
}
}
}
5 排序和分页
5.1 排序
elasticsearch支持对搜索结果排序,默认是根据相关度算分(_score)来排序,也可以指定字段排序。可以排序字段类型有:keyword类型、数值类型、地理坐标类型、日期类型等。
基本语法:
实战:
需求:搜索商品,按照销量排序降序,销量一样则按照价格升序
GET /items/_search
{
"query": {
"match_all": {}
},
"sort": [
{"sold": {"order": "desc"}},
{"price": {"order": "asc"}}
]
}
5.2 分页
elasticsearch 默认情况下只返回top10的数据。而如果要查询更多数据就需要修改分页参数了。elasticsearch中通过修改from、size参数来控制要返回的分页结果:
1 from:从第几个文档开始
2 size:总共查询几个文档
语法:
实战:
需求:搜索商品,查询出销量排名前10的商品,销量一样时按照价格升序
GET /items/_search
{
"query": {
"match_all": {}
},
"sort": [
{"sold": {"order": "desc"}},
{"price": {"order": "asc"}}
],
"from": 0,
"size": 10
}
结果,可以看到一共有10条
6 深度分页问题
6.1 是什么
假如我们使用es有这样的场景:
我们的一个集群下面有4个服务器。我们用分页查询每页10条数据的第1000页。就是说我们需要读取前10000条数据并且排序然后读取其中最后10条的数据。
那么问题来了,我们怎么去读取前10000条数据呢?
比如一个年级有10个班。我要选取全年级前10名。假设每个班的成绩已经排序好,那么如果只从每个班抽取一个,有可能这个班第一名也进不了其他班的前十。所以我们要从每个班都抽取前10名排序,然后选取成绩最好的10个。
换在es也一样。如果我要读取前10000条数据,我需要在每一个es节点读取10000条,然后合并后再排序来读取最后10条数据。前面的数据都不要了。这就造成了,随着查询数目越大,性能消耗也就越大。
6.2 解决(属于八股)
针对深度分页,ES提供了两种解决方案,
search after:分页时需要排序,原理是从上一次的排序值开始,查询下一页数据。官方推荐使用的方式。
scroll:原理将排序数据形成快照,保存在内存。官方已经不推荐使用。
7 高亮显示
7.1 介绍
高亮显示:就是在搜索结果中把搜索关键字突出显示。
我们去百度搜索java的时候,可以看到java都是高亮的,变成了红色。查看他的编译,我们可以看到 java 这个词被 em 标签标注,然后 css样式 中将 em 设置为红色。这就是高亮的页面的实现。
7.2 实现
基本语法:
实战:
# 需求:我们想将查询到的商品的名字和品牌高亮处理
GET /items/_search
{
"query": {
"match": {
"name": "行李箱"
}
},
"highlight": {
"fields": {
"name":{
"pre_tags": "<em>",
"post_tags": "</em>"
},
"brand": {}
}
}
}
注意!!!
-
搜索必须有查询条件,而且是全文检索类型的查询条件,例如
match
-
参与高亮的字段必须是
text
类型的字段 -
默认情况下参与高亮的字段要与搜索字段一致,除非添加:
required_field_match=false
-
可以不设置标签,es有默认标签em
结果:
我们可以看到结果中与query同级有一个hightlight 这实现了高亮。因为brand是keyword类型,所以没有高亮
八 在客户端的DSL查询
1 快速入门
1.1 简介
数据搜索的Java代码我们分为两部分:
1 构建并发起请求
2 解析查询结果
构建并发起请求
基本对应关系
其中我们对参数的使用就封装到 source 下面。有本文第七块讲到的所有操作。
其中,查询操作封装到 querybuilder 里面。
解析查询结果
解析查询结果的API:
我们可以从返回结果中得到相应的信息。
1.2 简单演示
查询所有商品后,解析里面的总响应数,还有其他参数
/**
* 测试快速进入门的方法。
* 该方法演示了如何使用Elasticsearch客户端发送搜索请求,并处理响应。
*
* @throws IOException 如果在发送请求或处理响应时发生I/O错误。
*/
@Test
void fastEnterDoor() throws IOException {
// 1 创建连接
// 创建一个指向名为"items"的索引的搜索请求对象。
SearchRequest searchRequest = new SearchRequest("items");
// 2 封装查询参数
// 使用SearchSourceBuilder构建查询对象,设置查询为匹配所有文档(matchAllQuery)。
SearchSourceBuilder query = searchRequest
.source() // 获取SearchRequest的源构建器
.query(QueryBuilders.matchAllQuery()); // 设置查询条件为匹配所有文档
// 3 发送请求
// 使用Elasticsearch客户端发送搜索请求,并获取响应对象。
SearchResponse response = client.search(searchRequest, RequestOptions.DEFAULT);
// 4 解析结果
// 4.1 获取总响应数
// 从响应对象中获取总的命中数。
TotalHits totalHits = response.getHits().getTotalHits();
long value = totalHits.value; // 获取命中数的值
log.info(String.valueOf(value)); // 记录日志信息,显示总命中数
// 4.2 获取具体数据
// 从响应对象中获取具体的搜索命中结果。
SearchHits hits = response.getHits();
for(SearchHit hit : hits){ // 遍历每个搜索命中结果
// 将命中结果的源数据转换为字符串。
String sourceAsString = hit.getSourceAsString();
// 将源数据字符串反序列化为ItemDoc对象。
ItemDoc itemDoc = JSONUtil.toBean(sourceAsString, ItemDoc.class);
log.info(String.valueOf(itemDoc)); // 记录日志信息,显示每个ItemDoc对象的详细信息
}
}
结果返回
可以看到返回的和kibana返回的数据都是一样的
11:41:04:505 INFO 11852 --- [ main] com.hmall.Item.query : 10000
11:41:04:564 INFO 11852 --- [ main] com.hmall.Item.query : ItemDoc(id=317578, name=RIMOWA 21寸托运箱拉杆箱 SALSA AIR系列果绿色 820.70.36.4, price=28900, image=https://m.360buyimg.com/mobilecms/s720x720_jfs/t6934/364/1195375010/84676/e9f2c55f/597ece38N0ddcbc77.jpg!q70.jpg.webp, category=拉杆箱, brand=RIMOWA, sold=0, commentCount=0, isAD=false, updateTime=2023-05-06T11:06:17)
11:41:04:566 INFO 11852 --- [ main] com.hmall.Item.query : ItemDoc(id=317580, name=RIMOWA 26寸托运箱拉杆箱 SALSA AIR系列果绿色 820.70.36.4, price=28600, image=https://m.360buyimg.com/mobilecms/s720x720_jfs/t6934/364/1195375010/84676/e9f2c55f/597ece38N0ddcbc77.jpg!q70.jpg.webp, category=拉杆箱, brand=RIMOWA, sold=0, commentCount=0, isAD=false, updateTime=2023-10-07T10:04:39)
11:41:04:566 INFO 11852 --- [ main] com.hmall.Item.query : ItemDoc(id=546872, name=博兿(BOYI)拉杆包男23英寸大容量旅行包户外手提休闲拉杆袋 BY09186黑灰色, price=27500, image=https://m.360buyimg.com/mobilecms/s720x720_jfs/t3301/221/3887995271/90563/bf2cadb/57f9fbf4N8e47c225.jpg!q70.jpg.webp, category=拉杆箱, brand=博兿, sold=0, commentCount=0, isAD=false, updateTime=2019-05-01T00:00)
11:41:04:566 INFO 11852 --- [ main] com.hmall.Item.query : ItemDoc(id=561178, name=RIMOWA 30寸托运箱拉杆箱 SALSA AIR系列果绿色 820.70.36.4, price=13000, image=https://m.360buyimg.com/mobilecms/s720x720_jfs/t6934/364/1195375010/84676/e9f2c55f/597ece38N0ddcbc77.jpg!q70.jpg.webp, category=拉杆箱, brand=RIMOWA, sold=0, commentCount=0, isAD=false, updateTime=2023-10-07T10:04:54)
11:41:04:566 INFO 11852 --- [ main] com.hmall.Item.query : ItemDoc(id=577967, name=莎米特SUMMIT 旅行拉杆箱28英寸PC材质大容量旅行行李箱PC154 黑色, price=71300, image=https://m.360buyimg.com/mobilecms/s720x720_jfs/t30454/163/719393962/79149/13bcc06a/5bfca9b6N493202d2.jpg!q70.jpg.webp, category=拉杆箱, brand=莎米特, sold=0, commentCount=0, isAD=false, updateTime=2019-05-01T00:00)
11:41:04:567 INFO 11852 --- [ main] com.hmall.Item.query : ItemDoc(id=584382, name=美旅AmericanTourister拉杆箱 商务男女超轻PP行李箱时尚大容量耐磨飞机轮旅行箱 25英寸海关锁DL7灰色, price=36600, image=https://m.360buyimg.com/mobilecms/s720x720_jfs/t1/22734/21/2036/130399/5c18af2aEab296c01/7b148f18c6081654.jpg!q70.jpg.webp, category=拉杆箱, brand=美旅箱包, sold=0, commentCount=0, isAD=false, updateTime=2019-05-01T00:00)
11:41:04:567 INFO 11852 --- [ main] com.hmall.Item.query : ItemDoc(id=584387, name=美旅AmericanTourister拉杆箱 商务男女超轻PP行李箱时尚大容量耐磨飞机轮旅行箱 29英寸海关锁DL7灰色, price=16200, image=https://m.360buyimg.com/mobilecms/s720x720_jfs/t1/22734/21/2036/130399/5c18af2aEab296c01/7b148f18c6081654.jpg!q70.jpg.webp, category=拉杆箱, brand=美旅箱包, sold=0, commentCount=0, isAD=false, updateTime=2019-05-01T00:00)
11:41:04:567 INFO 11852 --- [ main] com.hmall.Item.query : ItemDoc(id=584391, name=美旅AmericanTourister拉杆箱 商务男女超轻PP行李箱时尚大容量耐磨飞机轮旅行箱 20英寸海关锁DL7灰色, price=29900, image=https://m.360buyimg.com/mobilecms/s720x720_jfs/t1/22734/21/2036/130399/5c18af2aEab296c01/7b148f18c6081654.jpg!q70.jpg.webp, category=拉杆箱, brand=美旅箱包, sold=0, commentCount=0, isAD=false, updateTime=2019-05-01T00:00)
11:41:04:567 INFO 11852 --- [ main] com.hmall.Item.query : ItemDoc(id=584392, name=美旅AmericanTourister拉杆箱 商务男女超轻PP行李箱时尚大容量耐磨飞机轮旅行箱 29英寸海关锁DL7灰色, price=17000, image=https://m.360buyimg.com/mobilecms/s720x720_jfs/t1/22734/21/2036/130399/5c18af2aEab296c01/7b148f18c6081654.jpg!q70.jpg.webp, category=拉杆箱, brand=美旅箱包, sold=0, commentCount=0, isAD=false, updateTime=2023-10-07T10:04:59)
11:41:04:568 INFO 11852 --- [ main] com.hmall.Item.query : ItemDoc(id=584394, name=美旅AmericanTourister拉杆箱 商务男女超轻PP行李箱时尚大容量耐磨飞机轮旅行箱 25英寸海关锁DL7灰色, price=79400, image=https://m.360buyimg.com/mobilecms/s720x720_jfs/t1/22734/21/2036/130399/5c18af2aEab296c01/7b148f18c6081654.jpg!q70.jpg.webp, category=拉杆箱, brand=美旅箱包, sold=0, commentCount=0, isAD=false, updateTime=2019-05-01T00:00)
2 叶子查询
2.1 全文查询
match
基本语法:
实战:
@Test
void testMatch() throws IOException {
//1 创建连接
SearchRequest searchRequest = new SearchRequest("items");
//2 封装查询参数
searchRequest
.source()
.query(QueryBuilders.matchQuery("name", "牛奶"));
//3 发送请求
SearchResponse response = client.search(searchRequest, RequestOptions.DEFAULT);
//4 解析结果
if(response.getHits() != null){
log.info("ok");
}
}
multi_match
基本语法:
实战:
@Test
void testmultiMatch() throws IOException {
//1 创建连接
SearchRequest searchRequest = new SearchRequest("items");
//2 封装查询参数
searchRequest
.source()
.query(QueryBuilders.multiMatchQuery("手机", "name", "category"));
//3 发送请求
SearchResponse response = client.search(searchRequest, RequestOptions.DEFAULT);
//4 解析结果
//4.1 获取总响应数
TotalHits totalHits = response.getHits().getTotalHits();
long value = totalHits.value;
log.info(String.valueOf(value));
//4.2 获取具体数据
SearchHits hits = response.getHits();
for(SearchHit hit : hits){
String sourceAsString = hit.getSourceAsString();
ItemDoc itemDoc = JSONUtil.toBean(sourceAsString, ItemDoc.class);
log.info(String.valueOf(itemDoc));
}
}
3 精确查询
range
基本语法:
实战:
@Test
void testRange() throws IOException {
//1 创建连接
SearchRequest searchRequest = new SearchRequest("items");
//2 封装查询参数
searchRequest
.source()
.query(QueryBuilders.rangeQuery("price").lte(150000).gte(10000));
//3 发送请求
SearchResponse response = client.search(searchRequest, RequestOptions.DEFAULT);
//4 解析结果
//4.1 获取总响应数
TotalHits totalHits = response.getHits().getTotalHits();
long value = totalHits.value;
log.info(String.valueOf(value));
//4.2 获取具体数据
SearchHits hits = response.getHits();
for(SearchHit hit : hits){
String sourceAsString = hit.getSourceAsString();
ItemDoc itemDoc = JSONUtil.toBean(sourceAsString, ItemDoc.class);
log.info(String.valueOf(itemDoc));
}
}
ids
基本语法
GET /items/_search
{
"query": {
"ids": {
"values": [5068330, 5068332]
}
}
}
// 对应的api
searchRequest
.source()
.query(QueryBuilders.idsQuery("5068330", "5068332"));
实战:
@Test
void testids() throws IOException {
//1 创建连接
SearchRequest searchRequest = new SearchRequest("items");
//2 封装查询参数
searchRequest
.source()
.query(QueryBuilders.idsQuery("5068330", "5068332"));
//3 发送请求
SearchResponse response = client.search(searchRequest, RequestOptions.DEFAULT);
//4 解析结果
//4.1 获取总响应数
TotalHits totalHits = response.getHits().getTotalHits();
long value = totalHits.value;
log.info(String.valueOf(value));
//4.2 获取具体数据
SearchHits hits = response.getHits();
for(SearchHit hit : hits){
String sourceAsString = hit.getSourceAsString();
ItemDoc itemDoc = JSONUtil.toBean(sourceAsString, ItemDoc.class);
log.info(String.valueOf(itemDoc));
}
}
term
基本语法:
实战:
@Test
void testTerm() throws IOException {
//1 创建连接
SearchRequest searchRequest = new SearchRequest("items");
//2 封装查询参数
searchRequest
.source()
.query(QueryBuilders.termQuery("brand", "vivo"));
//3 发送请求
SearchResponse response = client.search(searchRequest, RequestOptions.DEFAULT);
//4 解析结果
//4.1 获取总响应数
TotalHits totalHits = response.getHits().getTotalHits();
long value = totalHits.value;
log.info(String.valueOf(value));
//4.2 获取具体数据
SearchHits hits = response.getHits();
for(SearchHit hit : hits){
String sourceAsString = hit.getSourceAsString();
ItemDoc itemDoc = JSONUtil.toBean(sourceAsString, ItemDoc.class);
log.info(String.valueOf(itemDoc));
}
}
3 复合查询-布尔查询
must
基本语法:
实战:
@Test
void testMust() throws IOException {
//1 创建连接
SearchRequest searchRequest = new SearchRequest("items");
//2 封装查询参数
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
searchRequest
.source()
.query(boolQuery.must(
QueryBuilders.matchQuery("name", "牛奶")
));
//3 发送请求
SearchResponse response = client.search(searchRequest, RequestOptions.DEFAULT);
//4 解析结果
//4.1 获取总响应数
TotalHits totalHits = response.getHits().getTotalHits();
long value = totalHits.value;
log.info(String.valueOf(value));
//4.2 获取具体数据
SearchHits hits = response.getHits();
for(SearchHit hit : hits){
String sourceAsString = hit.getSourceAsString();
ItemDoc itemDoc = JSONUtil.toBean(sourceAsString, ItemDoc.class);
log.info(String.valueOf(itemDoc));
}
}
should
基本语法:
GET /items/_search
{
"query": {
"bool": {
"should": [
{
"term": {
"brand": {
"value": "华为"
}
}
}
]
}
}
}
// 对应api
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
boolQuery.should(QueryBuilders.termQuery("brand", "华为"));
实战:
@Test
void testShould() throws IOException {
//1 创建连接
SearchRequest searchRequest = new SearchRequest("items");
//2 封装查询参数
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
searchRequest
.source()
.query(boolQuery.should
(QueryBuilders.termQuery("brand", "华为"))
);
//3 发送请求
SearchResponse response = client.search(searchRequest, RequestOptions.DEFAULT);
//4 解析结果
//4.1 获取总响应数
TotalHits totalHits = response.getHits().getTotalHits();
long value = totalHits.value;
log.info(String.valueOf(value));
//4.2 获取具体数据
SearchHits hits = response.getHits();
for(SearchHit hit : hits){
String sourceAsString = hit.getSourceAsString();
ItemDoc itemDoc = JSONUtil.toBean(sourceAsString, ItemDoc.class);
log.info(String.valueOf(itemDoc));
}
}
must_not
语法:
GET /items/_search
{
"query": {
"bool": {
"must_not": [
{
"term": {
"brand": {
"value": "华为"
}
}
}
]
}
}
}
// 对应api
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
boolQuery.mustNot(QueryBuilders.termQuery("brand", "华为"));
实战:
@Test
void testMust_not() throws IOException {
//1 创建连接
SearchRequest searchRequest = new SearchRequest("items");
//2 封装查询参数
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
searchRequest
.source()
.query(boolQuery.mustNot
(QueryBuilders.termQuery("brand", "华为"))
);
//3 发送请求
SearchResponse response = client.search(searchRequest, RequestOptions.DEFAULT);
//4 解析结果
//4.1 获取总响应数
TotalHits totalHits = response.getHits().getTotalHits();
long value = totalHits.value;
log.info(String.valueOf(value));
//4.2 获取具体数据
SearchHits hits = response.getHits();
for(SearchHit hit : hits){
String sourceAsString = hit.getSourceAsString();
ItemDoc itemDoc = JSONUtil.toBean(sourceAsString, ItemDoc.class);
log.info(String.valueOf(itemDoc));
}
}
filter
基本语法:
实战:
@Test
void testMust_not() throws IOException {
//1 创建连接
SearchRequest searchRequest = new SearchRequest("items");
//2 封装查询参数
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
searchRequest
.source()
.query(boolQuery.filter
(QueryBuilders.termQuery("brand", "华为"))
);
//3 发送请求
SearchResponse response = client.search(searchRequest, RequestOptions.DEFAULT);
//4 解析结果
//4.1 获取总响应数
TotalHits totalHits = response.getHits().getTotalHits();
long value = totalHits.value;
log.info(String.valueOf(value));
//4.2 获取具体数据
SearchHits hits = response.getHits();
for(SearchHit hit : hits){
String sourceAsString = hit.getSourceAsString();
ItemDoc itemDoc = JSONUtil.toBean(sourceAsString, ItemDoc.class);
log.info(String.valueOf(itemDoc));
}
}
综合使用
需求:利用JavaRestClient实现搜索功能,
条件如下: 搜索关键字为脱脂牛奶 品牌必须为德亚 价格必须低于300
下面价格搜索了30000是因为数据库给价格后面加上了两个0。
@Test
void testAll() throws IOException {
//1 创建连接
SearchRequest searchRequest = new SearchRequest("items");
//2 封装查询参数
//条件如下: 搜索关键字为脱脂牛奶 品牌必须为德亚 价格必须低于300
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
searchRequest
.source()
.query(QueryBuilders.matchQuery("name", "脱脂牛奶"))
.query(boolQuery.filter
(QueryBuilders.termQuery("brand", "德亚"))
)
.query(boolQuery.filter(
QueryBuilders.rangeQuery("price").lt(30000)
));
//3 发送请求
SearchResponse response = client.search(searchRequest, RequestOptions.DEFAULT);
//4 解析结果
//4.1 获取总响应数
TotalHits totalHits = response.getHits().getTotalHits();
long value = totalHits.value;
log.info(String.valueOf(value));
//4.2 获取具体数据
SearchHits hits = response.getHits();
for(SearchHit hit : hits){
String sourceAsString = hit.getSourceAsString();
ItemDoc itemDoc = JSONUtil.toBean(sourceAsString, ItemDoc.class);
log.info(String.valueOf(itemDoc));
}
}
结果:
16:55:36.675 [main] INFO com.hmall.Item.query - 17
16:55:36.739 [main] INFO com.hmall.Item.query - ItemDoc(id=896023, name=德国原装进口牛奶 德亚(Weidendorf)脱脂纯牛奶 1L*12盒 整箱装, price=21400, image=https://m.360buyimg.com/mobilecms/s720x720_jfs/t20791/119/2019391714/283574/1b79da38/5b4415b0N39d92810.jpg!q70.jpg.webp, category=牛奶, brand=德亚, sold=0, commentCount=0, isAD=false, updateTime=2019-05-01T00:00)
16:55:36.739 [main] INFO com.hmall.Item.query - ItemDoc(id=2410764, name=德国进口酸奶 德亚(Weidendorf)常温原味酸牛奶 200ml*10盒 礼盒装(新老包装随机发货), price=14800, image=https://m.360buyimg.com/mobilecms/s720x720_jfs/t1/9629/37/5847/341758/5bdfab96E3aaa1c2b/6f25d15aa1d748e6.jpg!q70.jpg.webp, category=牛奶, brand=德亚, sold=0, commentCount=0, isAD=false, updateTime=2019-05-01T00:00)
16:55:36.740 [main] INFO com.hmall.Item.query - ItemDoc(id=10092200887, name=中粮我买网 德亚 酸奶 200ml*15(德国进口), price=19400, image=https://m.360buyimg.com/mobilecms/s720x720_jfs/t2548/146/995424037/109774/e4e0d37c/567ba0acNb9e93acc.jpg!q70.jpg.webp, category=牛奶, brand=德亚, sold=0, commentCount=0, isAD=false, updateTime=2019-05-01T00:00)
16:55:36.740 [main] INFO com.hmall.Item.query - ItemDoc(id=10792941724, name=德国进口(Weidendorf)酸奶德亚200ml*10/箱酸牛奶营养早餐酸奶礼盒装1箱装, price=3200, image=https://m.360buyimg.com/mobilecms/s720x720_jfs/t3397/141/32932855/208533/94dc977/57fc9236N28b061d5.jpg!q70.jpg.webp, category=牛奶, brand=德亚, sold=0, commentCount=0, isAD=false, updateTime=2019-05-01T00:00)
16:55:36.740 [main] INFO com.hmall.Item.query - ItemDoc(id=10867276763, name=德国进口牛奶 德亚(Weidendorf)200ml*12/箱全脂纯牛奶营养早餐奶, price=22600, image=https://m.360buyimg.com/mobilecms/s720x720_jfs/t3574/252/611937060/202711/f6455b2e/580f1469N80827ecf.jpg!q70.jpg.webp, category=牛奶, brand=德亚, sold=0, commentCount=0, isAD=false, updateTime=2019-05-01T00:00)
16:55:36.741 [main] INFO com.hmall.Item.query - ItemDoc(id=12971632318, name=中粮我买网 德亚 脱脂牛奶1L(德国进口), price=19200, image=https://m.360buyimg.com/mobilecms/s720x720_jfs/t5854/62/3012675862/91720/8fc22a29/5936717dN09a0f550.jpg!q70.jpg.webp, category=牛奶, brand=德亚, sold=0, commentCount=0, isAD=false, updateTime=2019-05-01T00:00)
16:55:36.741 [main] INFO com.hmall.Item.query - ItemDoc(id=12972795066, name=中粮我买网 德亚 全脂牛奶 1L*6(德国进口), price=12400, image=https://m.360buyimg.com/mobilecms/s720x720_jfs/t5926/201/1843138009/138105/43e70ec4/593681feN2631a539.jpg!q70.jpg.webp, category=牛奶, brand=德亚, sold=0, commentCount=0, isAD=false, updateTime=2019-05-01T00:00)
16:55:36.741 [main] INFO com.hmall.Item.query - ItemDoc(id=12972937888, name=中粮我买网 德亚 脱脂牛奶1L*6(德国进口), price=6000, image=https://m.360buyimg.com/mobilecms/s720x720_jfs/t5821/202/3057572883/135865/193c1bd2/5936849dNe21a7d83.jpg!q70.jpg.webp, category=牛奶, brand=德亚, sold=0, commentCount=0, isAD=false, updateTime=2019-05-01T00:00)
16:55:36.741 [main] INFO com.hmall.Item.query - ItemDoc(id=12973053544, name=中粮我买网 德亚 低脂牛奶 1L*6(德国进口), price=3000, image=https://m.360buyimg.com/mobilecms/s720x720_jfs/t5647/89/3018035740/146127/abf6c64c/59368595N86fd4b53.jpg!q70.jpg.webp, category=牛奶, brand=德亚, sold=0, commentCount=0, isAD=false, updateTime=2019-05-01T00:00)
16:55:36.742 [main] INFO com.hmall.Item.query - ItemDoc(id=16925806499, name=德亚 碧滋浓黄桃西番莲低脂风味酸乳 125g*20杯 整箱装 德国进口酸奶, price=14100, image=https://m.360buyimg.com/mobilecms/s720x720_jfs/t24841/7/2692430014/54997/1f416afb/5bed057aN01ae594a.jpg!q70.jpg.webp, category=牛奶, brand=德亚, sold=0, commentCount=0, isAD=false, updateTime=2019-05-01T00:00)
4 分页与排序
基本语法:
与query类似,排序和分页参数都是基于request.source()来设置:
实战,接着刚刚 3 的综合使用的案例,价格升序,查前面 6 - 10 条数据
@Test
void testAll() throws IOException {
//1 创建连接
SearchRequest searchRequest = new SearchRequest("items");
//2 封装查询参数
//条件如下: 搜索关键字为脱脂牛奶 品牌必须为德亚 价格必须低于300
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
searchRequest
.source()
.sort("price", SortOrder.ASC)
.from(1)
.size(5)
.query(QueryBuilders.matchQuery("name", "脱脂牛奶"))
.query(boolQuery.filter
(QueryBuilders.termQuery("brand", "德亚"))
)
.query(boolQuery.filter(
QueryBuilders.rangeQuery("price").lt(30000)
));
//3 发送请求
SearchResponse response = client.search(searchRequest, RequestOptions.DEFAULT);
//4 解析结果
//4.1 获取总响应数
TotalHits totalHits = response.getHits().getTotalHits();
long value = totalHits.value;
log.info(String.valueOf(value));
//4.2 获取具体数据
SearchHits hits = response.getHits();
for(SearchHit hit : hits){
String sourceAsString = hit.getSourceAsString();
ItemDoc itemDoc = JSONUtil.toBean(sourceAsString, ItemDoc.class);
log.info(String.valueOf(itemDoc));
}
}
结果:
17:01:55.376 [main] INFO com.hmall.Item.query - 17
17:01:55.447 [main] INFO com.hmall.Item.query - ItemDoc(id=12973053544, name=中粮我买网 德亚 低脂牛奶 1L*6(德国进口), price=3000, image=https://m.360buyimg.com/mobilecms/s720x720_jfs/t5647/89/3018035740/146127/abf6c64c/59368595N86fd4b53.jpg!q70.jpg.webp, category=牛奶, brand=德亚, sold=0, commentCount=0, isAD=false, updateTime=2019-05-01T00:00)
17:01:55.447 [main] INFO com.hmall.Item.query - ItemDoc(id=10792941724, name=德国进口(Weidendorf)酸奶德亚200ml*10/箱酸牛奶营养早餐酸奶礼盒装1箱装, price=3200, image=https://m.360buyimg.com/mobilecms/s720x720_jfs/t3397/141/32932855/208533/94dc977/57fc9236N28b061d5.jpg!q70.jpg.webp, category=牛奶, brand=德亚, sold=0, commentCount=0, isAD=false, updateTime=2019-05-01T00:00)
17:01:55.448 [main] INFO com.hmall.Item.query - ItemDoc(id=19028883500, name=德国原装进口Weidendorf德亚脱脂牛奶200ml*12盒礼盒装进口早餐纯牛奶, price=3800, image=https://m.360buyimg.com/mobilecms/s720x720_jfs/t11932/46/779156535/159561/a23dd90e/59f829e5N00426a5d.jpg!q70.jpg.webp, category=牛奶, brand=德亚, sold=0, commentCount=0, isAD=false, updateTime=2019-05-01T00:00)
17:01:55.448 [main] INFO com.hmall.Item.query - ItemDoc(id=12972937888, name=中粮我买网 德亚 脱脂牛奶1L*6(德国进口), price=6000, image=https://m.360buyimg.com/mobilecms/s720x720_jfs/t5821/202/3057572883/135865/193c1bd2/5936849dNe21a7d83.jpg!q70.jpg.webp, category=牛奶, brand=德亚, sold=0, commentCount=0, isAD=false, updateTime=2019-05-01T00:00)
17:01:55.448 [main] INFO com.hmall.Item.query - ItemDoc(id=37228712326, name=【包邮】德国进口Weidendorf德亚全脂牛奶200ml*12盒进口早餐纯牛奶全脂 全脂牛奶200ml*12盒, price=12100, image=https://m.360buyimg.com/mobilecms/s720x720_jfs/t30757/169/501371318/588248/ddbb022e/5bf4ef7bNe22a28fa.jpg!q70.jpg.webp, category=牛奶, brand=德亚, sold=0, commentCount=0, isAD=false, updateTime=2019-05-01T00:00)
5 高亮
高亮显示的条件构造API如下:
高亮显示的结果解析API如下:
实战代码,将name高亮:
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.aggregations.metrics.TotalHits;
import cn.hutool.json.JSONUtil;
import cn.hutool.core.collection.CollUtil;
import org.elasticsearch.search.highlight.HighlightField;
import org.elasticsearch.common.logging.Loggers;
import org.junit.Test;
import java.io.IOException;
import java.util.Map;
/**
* 测试高亮显示功能的方法。
* 该方法演示了如何使用Elasticsearch客户端发送带有高亮显示功能的搜索请求,并处理响应。
*
* @throws IOException 如果在发送请求或处理响应时发生I/O错误。
*/
@Test
void testHightlight() throws IOException {
// 1 创建连接
// 创建一个指向名为"items"的索引的搜索请求对象。
SearchRequest searchRequest = new SearchRequest("items");
// 2 封装查询参数
// 方法1:直接new
// 使用HighlightBuilder构建高亮显示配置,设置高亮字段为"name",前缀标签为"<em>",后缀标签为"</em>"。
searchRequest
.source()
.highlighter(new HighlightBuilder()
.field("name") // 设置需要高亮的字段
.preTags("<em>") // 设置高亮前缀标签
.postTags("</em>") // 设置高亮后缀标签
);
// 方法2:用构造器(已注释)
// 另一种构建高亮显示配置的方式,使用SearchSourceBuilder的highlight方法。
// searchRequest
// .source()
// .highlighter(
// SearchSourceBuilder.highlight()
// .field("name")
// .preTags("<em>")
// .postTags("</em>")
// );
// 设置查询条件为匹配字段"name"中包含"脱脂牛奶"的文档。
searchRequest.source()
.query(QueryBuilders.matchQuery("name", "脱脂牛奶"));
// 3 发送请求
// 使用Elasticsearch客户端发送搜索请求,并获取响应对象。
SearchResponse response = client.search(searchRequest, RequestOptions.DEFAULT);
// 调用getResult方法处理响应结果。
getResult(response);
}
/**
* 处理搜索响应结果的方法。
*
* @param response 搜索响应对象。
*/
void getResult(SearchResponse response){
// 4.1 获取总响应数
// 从响应对象中获取总的命中数。
TotalHits totalHits = response.getHits().getTotalHits();
long value = totalHits.value;
log.info(String.valueOf(value)); // 记录日志信息,显示总命中数
// 4.2 获取具体数据
// 从响应对象中获取具体的搜索命中结果。
SearchHits hits = response.getHits();
for(SearchHit hit : hits){
// 将命中结果的源数据转换为字符串。
String sourceAsString = hit.getSourceAsString();
// 将源数据字符串反序列化为ItemDoc对象。
ItemDoc itemDoc = JSONUtil.toBean(sourceAsString, ItemDoc.class);
// 获取命中结果的高亮字段。
Map<String, HighlightField> highlightFields = hit.getHighlightFields();
// 如果存在高亮字段
if(!CollUtil.isEmpty(highlightFields)){
// 获取字段"name"的高亮显示结果。
HighlightField highlightField = highlightFields.get("name");
// 获取第一个高亮片段的字符串表示。
String hl = highlightField.getFragments()[0].toString();
// 将高亮显示结果设置为ItemDoc对象的"name"属性。
itemDoc.setName(hl);
}
// 记录日志信息,显示每个ItemDoc对象的详细信息。
log.info(String.valueOf(itemDoc));
}
}
八 聚合
1 简介
聚合(aggregations)可以实现对文档数据的统计、分析、运算。聚合常见的有三类:
桶(Bucket)聚合:用来对文档做分组
1 TermAggregation:按照文档字段值分组
2 Date Histogram:按照日期阶梯分组,例如一周为一组,或者一月为一组
度量(Metric)聚合:用以计算一些值,比如:最大值、最小值、平均值等
1 Avg:求平均值
2 Max:求最大值
3 Min:求最小值
4 Stats:同时求max、min、avg、sum等
管道(pipeline)聚合:其它聚合的结果为基础做聚合
2 DSL具体实现
2.1 需求1(用到桶聚合)
我们要统计所有商品中共有哪些商品分类,其实就是以分类(category)字段对数据分组。显示10个分组。category值一样的放在同一组,属于Bucket聚合中的Term聚合。
语法:
GET /items/_search
{
"query": {"match_all": {}},
"size": 0,
"aggs": {
"聚合的名字,自己起": {
"聚合的方法,可以选择": {
"field": "聚合的字段",
"size": 聚合返回的数目
}
}
}
}
实战:
GET /items/_search
{
"query": {"match_all": {}},
"size": 0,
"aggs": {
"cate_agg": {
"terms": {
"field": "category",
"size": 10
}
}
}
}
分析:
1 match_all 是默认的,如果不写也是查询全部。
2 size 表示查询的数目。由于我们只要显示聚合的结果,所以我们不想返回查询的结果,可以将size设置成0
结果:
可以看到我们自己起的名字的聚合,还有返回的10条数据
2.2 需求2
默认情况下,Bucket聚合是对索引库的所有文档做聚合,我们可以限定要聚合的文档范围,只要添加query条件即可。 例如,我想知道价格高于3000元的手机品牌有哪些
实战:
GET /items/_search
{
// 主查询部分,定义了查询的逻辑
"query": {
"bool": {
"filter": [
{
// 范围查询,过滤价格大于等于300000的文档
"range": {
"price": {
"gte": 300000
}
}
}
]
}
},
// 设置返回的文档数量为0,因为我们只需要聚合结果,不需要具体的文档
"size": 0,
// 聚合部分,定义了如何对结果进行聚合
"aggs": {
// 定义了一个名为cate_agg的聚合,用于按category字段的值进行分组
"cate_agg": {
"terms": {
// 指定聚合的字段为category,即按照商品的分类进行聚合
"field": "category",
// 设置返回的桶(分类)的最大数量为10,即只返回前10个分类
"size": 10
}
}
}
}
结果:
2.3 需求3(用到度量聚合和管道聚合)
除了对数据分组(Bucket)以外,我们还可以对每个Bucket内的数据进一步做数据计算和统计。 例如:我想知道手机有哪些品牌,每个品牌的价格最小值、最大值、平均值。
实战:
GET /items/_search
{
// 查询部分,定义了查询的逻辑
"query": {
"bool": {
"filter": [
{
// 术语查询,过滤category字段值为"手机"的文档
"term": {
"category": "手机"
}
}
]
}
},
// 设置返回的文档数量为0,因为我们只需要聚合结果,不需要具体的文档
"size": 0,
// 聚合部分,定义了如何对结果进行聚合
"aggs": {
// 定义了一个名为cate_agg的聚合,用于按brand字段的值进行分组
"cate_agg": {
"terms": {
// 指定聚合的字段为brand,即按照手机的品牌进行聚合
"field": "brand",
// 设置返回的桶(品牌)的最大数量为10,即只返回前10个品牌
"size": 10
},
// 在cate_agg聚合内部定义子聚合
"aggs": {
// 定义了一个名为caculate_agg的聚合,用于计算每个品牌的价格统计信息
"caculate_agg": {
"stats": {
// 指定计算统计信息的字段为price,即每个品牌手机的价格
"field": "price"
}
}
}
}
}
}
3 java客户端具体实现
我们以品牌聚合为例:
结果分析
实战,我们需要按分类进行分组
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.bucket.terms.Terms;
import org.elasticsearch.search.SearchRequest;
import org.elasticsearch.search.SearchResponse;
import org.elasticsearch.search.aggregations.Aggregations;
import org.elasticsearch.common.io.Streams;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.junit.Test;
import java.io.IOException;
import java.util.List;
/**
* 测试聚合功能的JUnit测试方法。
* 该方法演示了如何使用Elasticsearch客户端发送聚合查询,并处理响应。
*
* @throws IOException 如果在发送请求或处理响应时发生I/O错误。
*/
@Test
void testAgg() throws IOException {
// 创建一个指向名为"items"的索引的搜索请求对象。
SearchRequest searchRequest = new SearchRequest("items");
// 定义聚合的名称
String aggName = "cate_agg";
// 设置搜索请求的source,指定不需要返回任何文档(size为0)
searchRequest.source().size(0);
// 构建聚合查询
// 使用AggregationBuilders.terms创建一个terms聚合,按"category"字段进行分组,最多返回10个分组
searchRequest.source().aggregation(
AggregationBuilders.terms(aggName)
.field("category") // 设置聚合的字段为"category"
.size(10) // 设置返回的分组数量上限为10
);
// 使用Elasticsearch客户端发送搜索请求,并获取响应对象
SearchResponse response = client.search(searchRequest, RequestOptions.DEFAULT);
// 从响应中获取聚合结果
Aggregations aggregations = response.getAggregations();
// 根据聚合名称获取具体的terms聚合结果
Terms terms = aggregations.get(aggName);
// 获取terms聚合中的所有桶(bucket)
List<? extends Terms.Bucket> buckets = terms.getBuckets();
// 遍历每个桶,并打印出桶的key(即category的值)
for(Terms.Bucket bucket : buckets){
String keyAsString = bucket.getKeyAsString();
System.out.println(keyAsString);
}
}
原文地址:https://blog.csdn.net/2302_80490510/article/details/143589425
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!