当前位置: 首页 > news >正文

【ElasticSearch】springboot整合es案例

【ElasticSearch】springboot整合es案例

  • 【一】安装
    • 【1】Mac安装ElasticSearch:8.8.1
    • 【2】Mac安装可视化界面Kibana:8.8.1
    • 【3】安装IK分词器:8.8.1
    • 【4】安装拼音检索插件:8.8.1
  • 【二】环境准备
    • 【1】建表
    • 【2】脚手架生成基础代码
    • 【3】脚本造2000w数据(先生成1w条测试)
  • 【三】Spring Boot 集成 Elasticsearch
    • 【1】实现的核心功能​
    • 【2】配置pom和yml
    • 【3】创建Elasticsearch索引
    • 【4】数据同步
    • 【5】Elasticsearch基本操作测试
      • (1)管理索引
        • 1-创建索引
        • 2-删除索引
        • 3-查看索引
      • (2)增删改查
        • 1-创建文档
        • 2-更新文档
        • 3-删除文档
        • 4-简单搜索
      • (3)高级查询
        • 1-多字段搜索
        • 2-范围查询
        • 3-组合查询
        • 4-聚合分析
        • 5-高亮显示
        • 6-分页和排序
        • 7-拼音搜索
        • 8-同义词搜索
        • 9-嵌套聚合
        • 10-停用词过滤
    • 【6】性能测试
    • 【7】添加Elasticsearch实体
    • 【8】添加实体类的MapStruct映射接口
    • 【9】添加Repository接口
    • 【10】添加Service
    • 【9】数据同步服务
    • 【10】测试Elasticsearch控制器
      • (1)控制器代码
      • (2)测试场景设计
      • (3)测试用例

【一】安装

【1】Mac安装ElasticSearch:8.8.1

(1)Elasticsearch 依赖于JDK, 并且JDK 版本1.8+
(2)下载Elasticsearch安装包
去官网下载:官网

注意ES和JDK的对应版本要求
查看连接:注意ES和JDK的对应版本要求
在这里插入图片描述
(3)解压安装
(4)启动

cd /Library/Java/AllenElasticSearch/elasticsearch-8.8.1/bin./elasticsearch
./elasticsearch -d (这是后台启动)

(5)检验是否启动成功
http://127.0.0.1:9200/

(6)网址访问报错
received plaintext http traffic on an https channel, closing connection Netty4HttpChannel
ES8默认开启了ssl认证,导致无法访问9200端口
找到config/目录下面的elasticsearch.yml配置文件,把安全认证开关从原先的true都改成false,实现免密登录访问即可,修改这两处都为false后:

在这里插入图片描述

重新启动,重新访问
在这里插入图片描述

(6)熟悉Elasticsearch的文件
1)bin 目录下是一些脚本文件,包括 Elasticsearch 的启动执行文件。
2)config 目录下是一些配置文件。
3)jdk 目录下是内置的 Java 运行环境。
4)lib 目录下是一些 Java 类库文件。
5)logs 目录下会生成一些日志文件。
6)modules 目录下是一些 Elasticsearch 的模块。
7)plugins 目录下可以放一些 Elasticsearch 的插件。

直接双击 bin 目录下的 elasticsearch.bat 文件就可以启动 Elasticsearch 服务了。

启动后输出了很多信息,只需要看启动日志中是否有started字眼,就表示启动成功了。
确认是否真正启动成功,可以在浏览器的地址栏里输入 http://localhost:9200 进行查看(9200 是 Elasticsearch 的默认端口号)。

【2】Mac安装可视化界面Kibana:8.8.1

(1)简介
Kibana是一个针对ElasticSearch的开源分析及可视化平台,用来搜索、查看交互存储在ElasticSearch索引中的数据。使用Kibana,可以通过各种图表进行高级数据分析及展示。Kibana让海量数据更容易理解。它操作简单,基于浏览器的用户界面可以快速创建仪表板(dashboard)实时显示ElasticSearch查询动态。设置设置Kibana非常简单。无需编码或者额外的基础架构,几分钟内就可以完成Kibana安装并启动ElasticSearch索引监测。

(2)下载
首先要知道自己ES的版本,去官网下载与ES版本一致的Kibana。
官网:https://www.elastic.co/cn/kibana
首先要知道自己ES的版本,去官网下载与ES版本一致的Kibana。

(3)修改配置文件
解压之后,修改配置文件kibana.yml

在这里插入图片描述
默认是注释掉的,将注释去掉,名字自己起。

(4)启动

cd /Library/Java/AllenKibana/kibana-8.8.1/bin./kibana
./kibana -d (这是后台启动)

直接点击bin目录下的kibana.bat即可启动
当看到 [Kibana][http] http server running 的信息后,说明服务启动成功了。

(5)验证启动是否成功
在浏览器地址栏输入 http://localhost:5601 查看 Kibana 的图形化界面。

成功,点击Dev tools 就可以使用ElasticSecrch。

在这里插入图片描述

【3】安装IK分词器:8.8.1

(1)下载地址
https://release.infinilabs.com/analysis-ik/stable/
在这里插入图片描述

(2)把压缩包放到es的plugins文件夹下
在这里插入图片描述

(3)解压,删除压缩包
(4)重启es和Kibana
在这里插入图片描述

(5)测试ik分词器
ik_smart分词器

GET _analyze
{"analyzer": "ik_smart","text": "中华人民共和国万岁"
}

在这里插入图片描述

ik_max_word分词器进行最细粒度的分词测试

GET _analyze
{"analyzer": "ik_max_word","text": "中华人民共和国万岁"
}

在这里插入图片描述

【4】安装拼音检索插件:8.8.1

(1)下载地址
https://release.infinilabs.com/analysis-pinyin/stable/
在这里插入图片描述

(2)解压
现在的下载包可以直接使用
(3)放在es的plugins目录下
(4)重启es和Kibana
在这里插入图片描述
(5)测试拼音插件

GET _analyze
{"analyzer": "pinyin","text": "中华人民共和国万岁"
}

在这里插入图片描述

【二】环境准备

【1】建表

CREATE TABLE product_info (id VARCHAR(100) PRIMARY KEY COMMENT '商品ID',product_name VARCHAR(255) NOT NULL COMMENT '商品名称',price DECIMAL(10, 2) NOT NULL COMMENT '商品价格',description TEXT COMMENT '商品描述',category VARCHAR(100) COMMENT '商品分类',stock INT DEFAULT 0 COMMENT '库存数量',status ENUM('active', 'inactive', 'deleted') DEFAULT 'active' COMMENT '商品状态',origin VARCHAR(100) COMMENT '商品产地',material VARCHAR(100) COMMENT '商品材质',item_code VARCHAR(50) COMMENT '商品条目',CRT_DT_TM DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',CRT_USER_ID VARCHAR(100) NOT NULL COMMENT '创建人ID',UPD_DT_TM DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',UPD_USER_ID VARCHAR(100) NOT NULL COMMENT '更新人ID',INDEX idx_category (category),INDEX idx_price (price),INDEX idx_status (status),INDEX idx_origin (origin),INDEX idx_material (material),INDEX idx_item_code (item_code),INDEX idx_crt_dt_tm (CRT_DT_TM),INDEX idx_upd_dt_tm (UPD_DT_TM)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品信息表';

【2】脚手架生成基础代码

在这里插入图片描述

【3】脚本造2000w数据(先生成1w条测试)

数据量太大了,先生成1w条测试

-- 创建生成中文商品数据的存储过程(使用递增ID)
DELIMITER $$CREATE PROCEDURE GenerateChineseProductData4(IN numRecords INT)
BEGINDECLARE i BIGINT DEFAULT 0;DECLARE current_id BIGINT DEFAULT 0;DECLARE randomPrice DECIMAL(10,2);DECLARE randomStock INT;DECLARE randomCategory VARCHAR(100);DECLARE randomStatus ENUM('active', 'inactive', 'deleted');DECLARE randomName VARCHAR(255);DECLARE randomDescription TEXT;DECLARE randomUserId VARCHAR(100);DECLARE randomOrigin VARCHAR(100);DECLARE randomMaterial VARCHAR(100);DECLARE randomItemCode VARCHAR(50);-- 中文数据定义DECLARE chineseCategories VARCHAR(255) DEFAULT '电子产品,服装,家居用品,图书,体育用品,美妆产品,玩具,食品,汽车用品,健康产品';DECLARE chineseOrigins VARCHAR(255) DEFAULT '中国,美国,德国,日本,韩国,法国,意大利,英国,加拿大,澳大利亚,巴西,印度,俄罗斯,墨西哥,西班牙';DECLARE chineseMaterials VARCHAR(255) DEFAULT '棉,涤纶,皮革,木材,金属,塑料,玻璃,陶瓷,丝绸,羊毛';DECLARE chineseProductNames VARCHAR(255) DEFAULT '智能手机,笔记本电脑,平板电脑,智能手表,蓝牙耳机,数码相机,游戏主机,智能电视,无线充电器,便携音箱';DECLARE chineseAdjectives VARCHAR(255) DEFAULT '高端,智能,便携,超薄,防水,无线,快速充电,高清,节能,环保';DECLARE chineseBrands VARCHAR(255) DEFAULT '华为,苹果,小米,三星,索尼,联想,戴尔,惠普,佳能,尼康';-- 获取当前最大IDSELECT COALESCE(MAX(id), 0) INTO current_id FROM product_info;-- 创建临时表存储生成的数据CREATE TEMPORARY TABLE IF NOT EXISTS temp_products (id BIGINT,product_name VARCHAR(255),price DECIMAL(10,2),description TEXT,category VARCHAR(100),stock INT,status ENUM('active', 'inactive', 'deleted'),origin VARCHAR(100),material VARCHAR(100),item_code VARCHAR(50),CRT_DT_TM DATETIME,CRT_USER_ID VARCHAR(100),UPD_DT_TM DATETIME,UPD_USER_ID VARCHAR(100));-- 清空临时表TRUNCATE TABLE temp_products;-- 生成数据WHILE i < numRecords DO-- 递增IDSET current_id = current_id + 1;-- 生成随机值SET randomPrice = ROUND(RAND() * 1000 + 1, 2);SET randomStock = FLOOR(RAND() * 1000);-- 中文分类SET randomCategory = SUBSTRING_INDEX(SUBSTRING_INDEX(chineseCategories, ',', FLOOR(RAND() * 10) + 1), ',', -1);-- 商品状态SET randomStatus = ELT(FLOOR(RAND() * 3) + 1, 'active', 'inactive', 'deleted');-- 中文商品名称SET randomName = CONCAT(SUBSTRING_INDEX(SUBSTRING_INDEX(chineseBrands, ',', FLOOR(RAND() * 10) + 1), ',', -1),' ',SUBSTRING_INDEX(SUBSTRING_INDEX(chineseAdjectives, ',', FLOOR(RAND() * 10) + 1), ',', -1),SUBSTRING_INDEX(SUBSTRING_INDEX(chineseProductNames, ',', FLOOR(RAND() * 10) + 1), ',', -1));-- 中文描述SET randomDescription = CONCAT('这是一款', SUBSTRING_INDEX(SUBSTRING_INDEX(chineseAdjectives, ',', FLOOR(RAND() * 10) + 1), ',', -1),'的',randomName,',适用于',CASE WHEN randomCategory = '电子产品' THEN '日常办公和娱乐'WHEN randomCategory = '服装' THEN '各种场合穿着'WHEN randomCategory = '家居用品' THEN '家庭日常生活'WHEN randomCategory = '图书' THEN '知识学习和休闲阅读'WHEN randomCategory = '体育用品' THEN '健身运动和户外活动'WHEN randomCategory = '美妆产品' THEN '个人护理和美容'WHEN randomCategory = '玩具' THEN '儿童教育和娱乐'WHEN randomCategory = '食品' THEN '日常饮食和营养补充'WHEN randomCategory = '汽车用品' THEN '车辆维护和驾驶体验提升'ELSE '多种场景使用'END,'。');SET randomUserId = CONCAT('user', FLOOR(RAND() * 1000));-- 中文产地SET randomOrigin = SUBSTRING_INDEX(SUBSTRING_INDEX(chineseOrigins, ',', FLOOR(RAND() * 15) + 1), ',', -1);-- 中文材质SET randomMaterial = SUBSTRING_INDEX(SUBSTRING_INDEX(chineseMaterials, ',', FLOOR(RAND() * 10) + 1), ',', -1);-- 商品条目SET randomItemCode = CONCAT(SUBSTRING('ABCDEFGHIJKLMNOPQRSTUVWXYZ', FLOOR(RAND() * 26) + 1, 1),SUBSTRING('ABCDEFGHIJKLMNOPQRSTUVWXYZ', FLOOR(RAND() * 26) + 1, 1),'-',FLOOR(RAND() * 10000));-- 计算随机日期(过去2年内)SET @randomDate = DATE_SUB(NOW(), INTERVAL FLOOR(RAND() * 730) DAY);-- 插入临时表INSERT INTO temp_products VALUES (current_id, -- 使用递增IDrandomName,randomPrice,randomDescription,randomCategory,randomStock,randomStatus,randomOrigin,randomMaterial,randomItemCode,@randomDate, -- CRT_DT_TMrandomUserId,@randomDate, -- UPD_DT_TM (初始与创建时间相同)randomUserId);SET i = i + 1;-- 每1000条插入一次实际表IF i % 1000 = 0 THEN-- 禁用索引以提高插入速度ALTER TABLE product_info DISABLE KEYS;INSERT INTO product_info SELECT * FROM temp_products;ALTER TABLE product_info ENABLE KEYS;TRUNCATE TABLE temp_products;COMMIT;-- 输出进度SET @progress = ROUND((i * 100.0) / numRecords, 2);SELECT CONCAT('已生成: ', i, ' 条记录 (', @progress, '%)') AS progress;END IF;END WHILE;-- 插入剩余数据IF (SELECT COUNT(*) FROM temp_products) > 0 THENALTER TABLE product_info DISABLE KEYS;INSERT INTO product_info SELECT * FROM temp_products;ALTER TABLE product_info ENABLE KEYS;COMMIT;END IF;DROP TEMPORARY TABLE IF EXISTS temp_products;SELECT CONCAT('数据生成完成,共生成 ', numRecords, ' 条记录') AS completion;
END
$$DELIMITER ;-- 调用存储过程生成2000万条数据
CALL GenerateChineseProductData4(10000);

在这里插入图片描述

【三】Spring Boot 集成 Elasticsearch

【1】实现的核心功能​

(1)全文检索​:支持模糊查询、关键词高亮、分词搜索
(2)聚合分析​:数据统计(如商品销量TOP10、价格分布)
(3)实时监控​:日志收集、系统运行指标可视化(ELK方案)
(4)​自动补全​:搜索框智能提示(Completion Suggester)
(5)​地理位置查询​:附近地点搜索(如"5km内的咖啡厅")
(6)大数据存储​:处理千万级数据的快速查询

【2】配置pom和yml

【3】创建Elasticsearch索引

PUT /product_info
{"settings": {"number_of_shards": 5,"number_of_replicas": 1,"analysis": {"analyzer": {"chinese_analyzer": {"type": "custom","tokenizer": "ik_max_word","filter": ["lowercase", "stop"]}}}},"mappings": {"properties": {"id": {"type": "keyword"},"product_name": {"type": "text","analyzer": "chinese_analyzer","fields": {"keyword": {"type": "keyword"}}},"price": {"type": "float"},"description": {"type": "text","analyzer": "chinese_analyzer"},"category": {"type": "keyword"},"stock": {"type": "integer"},"status": {"type": "keyword"},"origin": {"type": "keyword"},"material": {"type": "keyword"},"item_code": {"type": "keyword"},"CRT_DT_TM": {"type": "date"},"CRT_USER_ID": {"type": "keyword"},"UPD_DT_TM": {"type": "date"},"UPD_USER_ID": {"type": "keyword"}}}
}

【4】数据同步

(1)方案1: 使用Logstash

(2)方案2: 使用Alibaba Canal
1-配置MySQL binlog
2-部署Canal Server和Canal Adapter
3-配置Elasticsearch映射

(3)方案3: 使用Python脚本同步

【5】Elasticsearch基本操作测试

(1)管理索引

1-创建索引

注意:如果内存占用超过90%,就会变成只读状态,创建索引就会超时失败

PUT /product_info
{"settings": {"number_of_shards": 5,"number_of_replicas": 1,"analysis": {"analyzer": {"chinese_analyzer": {"type": "custom","tokenizer": "ik_max_word","filter": ["lowercase", "stop"]}}}},"mappings": {"properties": {"id": {"type": "keyword"},"product_name": {"type": "text","analyzer": "chinese_analyzer","fields": {"keyword": {"type": "keyword"}}},"price": {"type": "float"},"description": {"type": "text","analyzer": "chinese_analyzer"},"category": {"type": "keyword"},"stock": {"type": "integer"},"status": {"type": "keyword"},"origin": {"type": "keyword"},"material": {"type": "keyword"},"item_code": {"type": "keyword"},"CRT_DT_TM": {"type": "date"},"CRT_USER_ID": {"type": "keyword"},"UPD_DT_TM": {"type": "date"},"UPD_USER_ID": {"type": "keyword"}}}
}

在这里插入图片描述

2-删除索引
DELETE /product_info

在这里插入图片描述

3-查看索引
GET /product_info

查询索引下数据的总量

GET /product_info/_count

(2)增删改查

1-创建文档
POST /product_info/_doc/prod-001
{"id": "prod-001","product_name": "华为智能防水手机","price": 5999.00,"description": "这是一款高端防水智能手机,适合户外使用","category": "电子产品","stock": 100,"status": "active","origin": "中国","material": "金属,玻璃","item_code": "HW-2023","CRT_DT_TM": "2023-01-15T10:30:00","CRT_USER_ID": "user123","UPD_DT_TM": "2023-01-15T10:30:00","UPD_USER_ID": "user123"
}
2-更新文档
POST /product_info/_update/prod-001
{"doc": {"price": 5499.00,"stock": 80,"UPD_DT_TM": "2023-06-20T14:45:00","UPD_USER_ID": "user456"}
}
3-删除文档
DELETE /product_info/_doc/prod-003
4-简单搜索
GET /product_info/_search
{"query": {"match": {"description": "防水智能手机"}},"highlight": {"fields": {"description": {}},"pre_tags": ["<strong>"],"post_tags": ["</strong>"]}
}

在这里插入图片描述

(3)高级查询

1-多字段搜索
GET /product_info/_search
{"query": {"multi_match": {"query": "华为 防水","fields": ["product_name", "description"],"type": "best_fields"}}
}
2-范围查询
GET /product_info/_search
{"query": {"bool": {"must": [{"match": {"product_name": "手机"}}],"filter": [{"term": {"origin": "中国"}},{"range": {"price": {"gte": 5000}}}]}}
}

在这里插入图片描述

3-组合查询
GET /product_info/_search
{"query": {"bool": {"must": [{"match": {"category": "电子产品"}}],"should": [{"match": {"product_name": "华为"}},{"match": {"description": "高端"}}],"must_not": [{"range": {"price": {"gte": 10000}}}],"filter": [{"term": {"status": "active"}}]}}
}

在这里插入图片描述

4-聚合分析
GET /product_info/_search
{"size": 0,"aggs": {"by_category": {"terms": {"field": "category","size": 10},"aggs": {"avg_price": {"avg": {"field": "price"}},"by_origin": {"terms": {"field": "origin","size": 5}}}},"price_distribution": {"histogram": {"field": "price","interval": 100}}}
}

在这里插入图片描述

5-高亮显示
GET /product_info/_search
{"query": {"match": {"description": "防水"}},"highlight": {"fields": {"description": {}},"pre_tags": ["<strong>"],"post_tags": ["</strong>"]}
}
6-分页和排序
GET /product_info/_search
{"query": {"match_all": {}},"from": 0,"size": 10,"sort": [{"price": {"order": "desc"}}]
}

在这里插入图片描述

7-拼音搜索

设置支持拼音搜索,需要安装插件

PUT /product_info/_settings
{"analysis": {"analyzer": {"pinyin_analyzer": {"type": "custom","tokenizer": "ik_max_word","filter": ["pinyin_filter"]}},"filter": {"pinyin_filter": {"type": "pinyin","keep_first_letter": true,"keep_full_pinyin": true,"keep_original": true}}},"mappings": {"properties": {"product_name": {"type": "text","analyzer": "chinese_analyzer","fields": {"pinyin": {"type": "text","analyzer": "pinyin_analyzer"}}}}}
}
GET /product_info/_search
{"query": {"match": {"product_name.pinyin": "shouji"  // 搜索拼音"shouji"匹配"手机"}}
}

设置前通过拼音查询不到结果
在这里插入图片描述

8-同义词搜索

同义词扩展设置

PUT /product_info/_settings
{"analysis": {"filter": {"chinese_synonym": {"type": "synonym","synonyms_path": "analysis/synonym.txt"}},"analyzer": {"chinese_analyzer": {"type": "custom","tokenizer": "ik_max_word","filter": ["lowercase","stop","chinese_synonym"]}}}
}
GET /product_info/_search
{"query": {"match": {"description": "移动电话"}}
}
9-嵌套聚合
GET /product_info/_search
{"size": 0,"aggs": {"category_agg": {"terms": {"field": "category"},"aggs": {"material_agg": {"terms": {"field": "material"},"aggs": {"price_stats": {"stats": {"field": "price"}}}}}}}
}
10-停用词过滤
PUT /product_info/_settings
{"analysis": {"filter": {"chinese_stop": {"type": "stop","stopwords_path": "analysis/stopwords.txt"}},"analyzer": {"chinese_analyzer": {"type": "custom","tokenizer": "ik_max_word","filter": ["lowercase","chinese_stop","chinese_synonym"]}}}
}

【6】性能测试

(1)批量插入测试
(2)搜索性能测试
(3)聚合性能测试

【7】添加Elasticsearch实体

package com.allen.study.application.elasticSearch.es_entity;import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;import java.time.LocalDateTime;/*** @ClassName: ProductES* @Author: AllenSun* @Date: 2025/8/24 16:55*/
// @Document表明了要连接到 ElasticSearch 的哪个索引和哪个 type 上
@Document(indexName = "product_info")
@Data
public class ProductES {@Idprivate String id;@Field(type = FieldType.Text, analyzer = "chinese_analyzer")private String productName; // 商品名称@Field(type = FieldType.Float)private Double price; // 商品价格@Field(type = FieldType.Text, analyzer = "chinese_analyzer")private String description; // 商品描述@Field(type = FieldType.Keyword)private String category; // 商品分类@Field(type = FieldType.Integer)private Integer stock; // 库存数量@Field(type = FieldType.Keyword)private String status; // 商品状态@Field(type = FieldType.Keyword)private String origin; // 商品产地@Field(type = FieldType.Keyword)private String material; // 商品材质@Field(type = FieldType.Keyword)private String itemCode; // 商品条目@Field(type = FieldType.Date)private LocalDateTime createdAt; // 创建时间@Field(type = FieldType.Keyword)private String createdBy; // 创建人ID@Field(type = FieldType.Date)private LocalDateTime updatedAt; // 更新时间@Field(type = FieldType.Keyword)private String updatedBy; // 更新人ID
}

【8】添加实体类的MapStruct映射接口

package com.allen.study.application.elasticSearch.es_assembler.mapper;import com.allen.study.application.api.response.ProductInfoQueryResponse;
import com.allen.study.application.elasticSearch.es_entity.ProductES;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;import java.util.List;/*** 商品信息表数据传输对象结构映射** @author AllenSun* @since 2025-08-24 13:47*/
@Mapper
public interface IProductInfoEsEntityStructMapper {/*** 商品信息表数据传输对象结构映射实例*/IProductInfoEsEntityStructMapper INSTANCE = Mappers.getMapper(IProductInfoEsEntityStructMapper.class);/*** 商品信息表实体 转 商品信息表查询响应数据传输对象** @param productInfo 商品信息表实体* @return 商品信息表查询响应数据传输对象*/ProductES toEsEntity(ProductInfoQueryResponse productInfo);/*** 商品信息表实体列表 转 商品信息表查询响应数据传输对象列表** @param productInfoList 商品信息表实体列表* @return 商品信息表查询响应数据传输对象列表*/List<ProductES> toEsEntity(List<ProductInfoQueryResponse> productInfoList);
}
package com.allen.study.application.elasticSearch.es_assembler;import com.allen.study.application.api.response.ProductInfoQueryResponse;
import com.allen.study.application.elasticSearch.es_assembler.mapper.IProductInfoEsEntityStructMapper;
import com.allen.study.application.elasticSearch.es_entity.ProductES;
import org.springframework.stereotype.Component;import java.util.List;/*** 商品信息表类型转换器** @author AllenSun* @since 2025-08-24 13:47*/
@Component
public class ProductInfoEsEntityAssembler {/*** 商品信息表实体 转 商品信息表查询响应数据传输对象** @param productInfo 商品信息表实体* @return 商品信息表查询响应数据传输对象*/public ProductES toEsEntity(ProductInfoQueryResponse productInfo) {return IProductInfoEsEntityStructMapper.INSTANCE.toEsEntity(productInfo);}/*** 商品信息表实体列表 转 商品信息表查询响应数据传输对象列表** @param productInfoList 商品信息表实体列表* @return 商品信息表查询响应数据传输对象列表*/public List<ProductES> toEsEntity(List<ProductInfoQueryResponse> productInfoList) {return IProductInfoEsEntityStructMapper.INSTANCE.toEsEntity(productInfoList);}
}

【9】添加Repository接口

package com.allen.study.application.elasticSearch.es_repository;import com.allen.study.application.elasticSearch.es_entity.ProductES;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.elasticsearch.annotations.Highlight;
import org.springframework.data.elasticsearch.annotations.HighlightField;
import org.springframework.data.elasticsearch.annotations.Query;
import org.springframework.data.elasticsearch.core.SearchHits;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;public interface ProductESRepository extends ElasticsearchRepository<ProductES, String> {// 中文全文搜索Page<ProductES> findByDescriptionContaining(String description, Pageable pageable);Page<ProductES> findByProductNameContaining(String name, Pageable pageable);// 多字段中文搜索@Query("{\"multi_match\": {\"query\": \"?0\", \"fields\": [\"product_name\", \"description\"]}}")Page<ProductES> searchByKeyword(String keyword, Pageable pageable);@Query("{\"bool\": {\"must\": [{\"match\": {\"product_name\": \"?0\"}}, {\"term\": {\"origin.keyword\": \"?1\"}}]}}")Page<ProductES> searchByProductNameAndOrigin(String productName, String origin, Pageable pageable);// 带高亮的中文搜索@Highlight(fields = {@HighlightField(name = "product_name"),@HighlightField(name = "description")})@Query("{\"match\": {\"description\": \"?0\"}}")SearchHits<ProductES> searchWithHighlight(String keyword);// // 按分类聚合// @Aggregation(terms = @Terms(name = "by_category", field = "category"))// AggregatedPage<ProductES> aggregateByCategory(Pageable pageable);
}

【10】添加Service

package com.allen.study.application.elasticSearch.es_service;import cn.hutool.core.util.ObjectUtil;
import com.allen.study.application.api.request.ProductInfoQueryRequest;
import com.allen.study.application.api.response.ProductInfoQueryResponse;
import com.allen.study.application.elasticSearch.es_assembler.ProductInfoEsEntityAssembler;
import com.allen.study.application.elasticSearch.es_entity.ProductES;
import com.allen.study.application.elasticSearch.es_repository.ProductESRepository;
import com.allen.study.application.repository.IProductInfoReadModelRepo;
import com.allen.study.common.base.ApiPageResponse;
import com.allen.study.common.base.Pagination;
import com.allen.study.common.utils.redis.RedisUtils;
import com.allen.study.common.utils.redis.RedissonConfig;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.index.query.RangeQueryBuilder;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.Aggregations;
import org.elasticsearch.search.aggregations.bucket.terms.ParsedStringTerms;
import org.elasticsearch.search.aggregations.bucket.terms.Terms;
import org.elasticsearch.search.aggregations.metrics.Avg;
import org.elasticsearch.search.aggregations.metrics.ParsedAvg;
import org.redisson.api.RLock;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.data.elasticsearch.core.SearchHit;
import org.springframework.data.elasticsearch.core.SearchHitSupport;
import org.springframework.data.elasticsearch.core.SearchHits;
import org.springframework.data.elasticsearch.core.query.NativeSearchQuery;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;/*** @ClassName: ProductSearchService* @Author: AllenSun* @Date: 2025/8/24 17:01*/
@Service
@RequiredArgsConstructor
@Slf4j
public class ProductSearchService {private final ElasticsearchOperations elasticsearchOperations;private final ProductESRepository productESRepository;private final IProductInfoReadModelRepo productInfoReadModelRepo;private final ProductInfoEsEntityAssembler assembler;private final RedisUtils redisUtils;private final RedissonConfig redissonConfig;@Scheduled(fixedRate = 60000) // 每分钟同步一次public void syncToElasticsearch() {final String PRODUCT_ES_SYNC_TIME_KEY = "essync:product:time";final String PRODUCT_ES_SYNC_LOCK_KEY = "essync:product:lock";RLock lock = redissonConfig.redissonClient().getLock(PRODUCT_ES_SYNC_LOCK_KEY);try {// 尝试获取分布式锁,等待时间100ms,锁持有时间5sif (lock.tryLock(100, 50000, TimeUnit.MILLISECONDS)) {// 获取上次同步时间LocalDateTime lastSyncTime = (LocalDateTime)redisUtils.get(PRODUCT_ES_SYNC_TIME_KEY);// LocalDateTime lastSyncTime = getLastSyncTime();if(ObjectUtil.isEmpty(lastSyncTime)){lastSyncTime = null;//全部同步}// 查询更新的数据ProductInfoQueryRequest productInfoQueryRequest = new ProductInfoQueryRequest();productInfoQueryRequest.setUpdateEsStartTime(lastSyncTime.toString());List<ProductInfoQueryResponse> products = productInfoReadModelRepo.query(productInfoQueryRequest);// 转换为ES实体List<ProductES> esProducts = assembler.toEsEntity(products);// 保存到ElasticsearchproductESRepository.saveAll(esProducts);// 更新最后同步时间if (ObjectUtil.isNotEmpty(esProducts)) {LocalDateTime updateDateTime = products.get(products.size() - 1).getUpdateDateTime();redisUtils.set(PRODUCT_ES_SYNC_TIME_KEY, updateDateTime);}} else {log.info("获取锁失败");}} catch (Exception e) {log.info("同步失败:{}",e);} finally {lock.unlock();}}// 简单中文搜索public ApiPageResponse<ProductES> simpleSearch(String keyword, Pagination pagination) {Pageable pageable = PageRequest.of(pagination.getPageNumber(), pagination.getPageSize());Page<ProductES> byDescriptionContaining = productESRepository.findByDescriptionContaining(keyword, pageable);List<ProductES> list = byDescriptionContaining.getContent();pagination.setTotal(byDescriptionContaining.getTotalElements());return ApiPageResponse.ok(pagination, list);}// 多字段中文搜索public ApiPageResponse<ProductES> multiFieldSearch(String keyword, Pagination pagination) {Pageable pageable = PageRequest.of(pagination.getPageNumber(), pagination.getPageSize());Page<ProductES> byDescriptionContaining = productESRepository.searchByKeyword(keyword, pageable);List<ProductES> list = byDescriptionContaining.getContent();pagination.setTotal(byDescriptionContaining.getTotalElements());return ApiPageResponse.ok(pagination, list);}// 带高亮的中文搜索public List<Map<String, Object>> searchWithHighlight(String keyword) {SearchHits<ProductES> searchHits = productESRepository.searchWithHighlight(keyword);return searchHits.stream().map(hit -> {Map<String, Object> result = new HashMap<>();result.put("product", hit.getContent());// 处理高亮Map<String, List<String>> highlightFields = hit.getHighlightFields();if (highlightFields.containsKey("product_name")) {result.put("highlightedName", highlightFields.get("product_name").get(0));}if (highlightFields.containsKey("description")) {result.put("highlightedDescription", highlightFields.get("description").get(0));}return result;}).collect(Collectors.toList());}// 解析排序参数private Sort parseSortParameter(String sort) {if (sort == null || sort.isEmpty()) {return Sort.unsorted();}String[] parts = sort.split(",");if (parts.length != 2) {return Sort.unsorted();}String property = parts[0];Sort.Direction direction = parts[1].equalsIgnoreCase("asc") ?Sort.Direction.ASC : Sort.Direction.DESC;return Sort.by(direction, property);}public ApiPageResponse<ProductES> advancedSearch(String keyword, String origin, Double minPrice, Double maxPrice,Pagination pagination, String sort) {// 解析排序参数Sort sortObj = parseSortParameter(sort);Pageable pageable = PageRequest.of(pagination.getPageNumber(), pagination.getPageSize(), sortObj);BoolQueryBuilder queryBuilder = QueryBuilders.boolQuery();if (StringUtils.hasText(keyword)) {queryBuilder.must(QueryBuilders.multiMatchQuery(keyword, "productName", "description"));}if (StringUtils.hasText(origin)) {queryBuilder.filter(QueryBuilders.termQuery("origin", origin));}if (minPrice != null || maxPrice != null) {RangeQueryBuilder rangeQuery = QueryBuilders.rangeQuery("price");if (minPrice != null) rangeQuery.gte(minPrice);if (maxPrice != null) rangeQuery.lte(maxPrice);queryBuilder.filter(rangeQuery);}NativeSearchQuery searchQuery = new NativeSearchQueryBuilder().withQuery(queryBuilder).withPageable(pageable).build();SearchHits<ProductES> searchHits = elasticsearchOperations.search(searchQuery, ProductES.class);List<ProductES> list = searchHits.map(SearchHit::getContent).stream().collect(Collectors.toList());long totalHits = searchHits.getTotalHits();pagination.setTotal(totalHits);return ApiPageResponse.ok(pagination, list);}// 分类聚合分析// public Map<String, Long> getCategoryAggregation() {//     AggregatedPage<ProductES> aggregatedPage = (AggregatedPage<ProductES>)//             productESRepository.aggregateByCategory(Pageable.unpaged());////     TermsAggregation categoryAggregation = (TermsAggregation)//             aggregatedPage.getAggregation("by_category");////     return categoryAggregation.getBuckets().stream()//             .collect(Collectors.toMap(//                     TermsAggregation.Bucket::getKeyAsString,//                     TermsAggregation.Bucket::getDocCount//             ));// }// 高级搜索public Page<ProductES> advancedSearch(String keyword, String origin, Double minPrice, Double maxPrice, Pageable pageable) {BoolQueryBuilder queryBuilder = QueryBuilders.boolQuery();if (StringUtils.hasText(keyword)) {queryBuilder.must(QueryBuilders.multiMatchQuery(keyword, "product_name", "description"));}if (StringUtils.hasText(origin)) {queryBuilder.filter(QueryBuilders.termQuery("origin", origin));}if (minPrice != null || maxPrice != null) {RangeQueryBuilder rangeQuery = QueryBuilders.rangeQuery("price");if (minPrice != null) rangeQuery.gte(minPrice);if (maxPrice != null) rangeQuery.lte(maxPrice);queryBuilder.filter(rangeQuery);}NativeSearchQuery searchQuery = new NativeSearchQueryBuilder().withQuery(queryBuilder).withPageable(pageable).build();SearchHits<ProductES> searchHits = elasticsearchOperations.search(searchQuery, ProductES.class);return SearchHitSupport.searchPageFor(searchHits, pageable).map(SearchHit::getContent);}// 聚合分析 - 按产地平均价格public Map<String, Double> getAvgPriceByOrigin() {// 构建聚合查询NativeSearchQuery searchQuery = new NativeSearchQueryBuilder().addAggregation(AggregationBuilders.terms("by_origin").field("origin").subAggregation(AggregationBuilders.avg("avg_price").field("price"))).build();// 执行查询SearchHits<ProductES> searchHits = elasticsearchOperations.search(searchQuery, ProductES.class);// 获取聚合结果Aggregations aggregations = (Aggregations) searchHits.getAggregations();if (aggregations == null) {return new HashMap<>();}// 解析聚合结果ParsedStringTerms terms = aggregations.get("by_origin");if (terms == null) {return new HashMap<>();}return terms.getBuckets().stream().collect(Collectors.toMap(Terms.Bucket::getKeyAsString,bucket -> {Avg avg = bucket.getAggregations().get("avg_price");return avg != null ? avg.getValue() : 0.0;}));}// 高亮搜索// public List<Map<String, Object>> highlightSearch(String keyword) {//     NativeSearchQuery searchQuery = new NativeSearchQueryBuilder()//             .withQuery(QueryBuilders.multiMatchQuery(keyword, "productName", "description"))//             .withHighlightFields(//                     new org.springframework.data.elasticsearch.core.query.highlight.HighlightField("productName"),//                     new org.springframework.data.elasticsearch.core.query.highlight.HighlightField("description")//             )//             .build();////     SearchHits<ProductES> searchHits = elasticsearchOperations.search(searchQuery, ProductES.class);////     return searchHits.getSearchHits().stream()//             .map(hit -> {//                 Map<String, Object> result = new HashMap<>();//                 result.put("product", hit.getContent());////                 // 处理高亮//                 Map<String, List<String>> highlightFields = hit.getHighlightFields();//                 if (highlightFields.containsKey("productName")) {//                     result.put("highlightedName", highlightFields.get("productName").get(0));//                 }//                 if (highlightFields.containsKey("description")) {//                     result.put("highlightedDescription", highlightFields.get("description").get(0));//                 }////                 return result;//             })//             .collect(Collectors.toList());// }// 根据商品名称和产地查询价格平均值public Map<String, Double> getAvgPriceByNameAndOrigin(String productName, String origin) {Map<String, Double> resultMap = new HashMap<>();BoolQueryBuilder queryBuilder = QueryBuilders.boolQuery().must(QueryBuilders.matchQuery("productName", productName)).filter(QueryBuilders.termQuery("origin", origin));NativeSearchQuery searchQuery = new NativeSearchQueryBuilder().withQuery(queryBuilder).addAggregation(org.elasticsearch.search.aggregations.AggregationBuilders.avg("avg_price").field("price")).build();SearchHits<ProductES> searchHits = elasticsearchOperations.search(searchQuery, ProductES.class);Aggregations aggregations = (Aggregations) searchHits.getAggregations();if (aggregations == null) {resultMap.put("average_price", 0.0);return resultMap;}ParsedAvg avg = aggregations.get("avg_price");resultMap.put("average_price", avg != null ? avg.getValue() : 0.0);return resultMap;}}

【9】数据同步服务

【10】测试Elasticsearch控制器

(1)控制器代码

package com.allen.study.application.api;import com.allen.study.application.elasticSearch.es_entity.ProductES;
import com.allen.study.application.elasticSearch.es_service.ProductSearchService;
import com.allen.study.common.base.ApiPageResponse;
import com.allen.study.common.base.ApiResponse;
import com.allen.study.common.base.Pagination;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;import javax.validation.Valid;
import java.util.List;
import java.util.Map;/*** @ClassName: ProductInfoTestEsApi* @Author: AllenSun* @Date: 2025/8/24 17:07*/
@RestController
@RequestMapping("/api/products/search")
@RequiredArgsConstructor
public class ProductInfoTestEsApi {private final ProductSearchService productSearchService;// 简单搜索@GetMapping("/simple")public ApiPageResponse<ProductES> simpleSearch(@RequestParam String keyword,@Valid Pagination pagination) {return productSearchService.simpleSearch(keyword, pagination);}// 多字段搜索@GetMapping("/multi-field")public ApiPageResponse<ProductES> multiFieldSearch(@RequestParam String keyword, @Valid Pagination pagination) {return productSearchService.multiFieldSearch(keyword, pagination);}// 高亮搜索@GetMapping("/highlight")public ApiResponse<List<Map<String, Object>>> searchWithHighlight(@RequestParam String keyword) {return ApiResponse.ok(productSearchService.searchWithHighlight(keyword));}// 分类聚合// @GetMapping("/category-aggregation")// public ApiResponse<Map<String, Long>> getCategoryAggregation() {//     return ApiResponse.ok(productSearchService.getCategoryAggregation());// }// 5. 高级搜索@GetMapping("/search")public ApiPageResponse<ProductES> searchProducts(@Valid Pagination pagination,@RequestParam(required = false) String keyword,@RequestParam(required = false) String origin,@RequestParam(required = false) Double minPrice,@RequestParam(required = false) Double maxPrice,@RequestParam(defaultValue = "price,desc") String sort) {return productSearchService.advancedSearch(keyword, origin, minPrice, maxPrice, pagination, sort);}// 6. 聚合分析 - 按产地平均价格@GetMapping("/analytics/avg-price-by-origin")public ApiResponse<Map<String, Double>> getAvgPriceByOrigin() {return ApiResponse.ok(productSearchService.getAvgPriceByOrigin());}// 7. 高亮搜索// @GetMapping("/highlight-search")// public ApiResponse<List<Map<String, Object>>> highlightSearch(//         @RequestParam String keyword) {//     return ApiResponse.ok(productSearchService.highlightSearch(keyword));// }// 8. 根据商品名称和产地查询价格平均值@GetMapping("/analytics/avg-price-by-name-origin")public ApiResponse<Map<String, Double>> getAvgPriceByNameAndOrigin(@RequestParam String productName,@RequestParam String origin) {return ApiResponse.ok(productSearchService.getAvgPriceByNameAndOrigin(productName, origin));}}

(2)测试场景设计

字段查询测试
(1)按产地查询:/api/products/origin/中国
(2)按材质查询:/api/products/material/金属
(3)按商品条目查询:/api/products/item-code/AB-1234
(4)组合查询:/api/products/combined?origin=中国&material=金属

聚合分析测试
(1)按产地分布统计
(2)按材质分布统计
(3)价格区间分布
(4)多维度聚合分析

(3)测试用例

(1)简单中文搜索​
URL: /api/products/search/simple?keyword=防水手机
预期: 返回包含"防水手机"的商品
(2)多字段中文搜索​
URL: /api/products/search/multi-field?keyword=华为 高端
预期: 返回在商品名称或描述中包含"华为"或"高端"的商品
(3)​高亮搜索​
URL: /api/products/search/highlight?keyword=智能手机
预期: 返回包含"智能手机"的商品,并在描述中高亮显示匹配词
(4)​分类聚合​
URL: /api/products/search/category-aggregation
预期: 返回各分类的商品数量统计
(5)​拼音搜索​
URL: /api/products/search/multi-field?keyword=shouji
预期: 返回包含"手机"的商品(通过拼音匹配)
(6)​同义词搜索​
URL: /api/products/search/simple?keyword=移动电话
预期: 返回包含"手机"的商品(通过同义词匹配)

http://www.xdnf.cn/news/1360549.html

相关文章:

  • Smooze Pro for mac 鼠标手势增强软件
  • 【C语言练习】青蛙跳台阶
  • Vue状态管理工具pinia的使用以及Vue组件通讯
  • 强光干扰下检出率↑93%!陌讯多模态融合算法在充电桩车位占用检测的实战解析
  • 力扣【1277. 统计全为1的正方形子矩阵】——从暴力到最优的思考过程
  • 【网络运维】Shell脚本编程:函数
  • 深度学习之第二课PyTorch与CUDA的安装
  • AOSP构建指南:从零开始的Android源码之旅
  • Docker 容器(一)
  • 【Docker基础】Docker-compose常用命令实践(三):镜像与配置管理
  • 【零代码】OpenCV C# 快速开发框架演示
  • 电路学习(四)二极管
  • 【计算机视觉】CaFormer
  • Java:LinkedList的使用
  • 【Protues仿真】基于AT89C52单片机的温湿度测量
  • 【文献阅读】生态恢复项目对生态系统稳定性的影响
  • 在JavaScript中,比较两个数组是否有相同元素(交集)的常用方法
  • 解决编译osgEarth中winsocket2.h找不到头文件问题
  • Node.js自研ORM框架深度解析与实践
  • C++11新特性全面解析(万字详解)
  • Starlink第三代终端和第二代终端的差异性有哪些?
  • Flink SQL执行SQL错误排查
  • MySQL的安装和卸载指南(入门到入土)
  • ZKmall模块商城的推荐数据体系:从多维度采集到高效存储的实践
  • 从“小麻烦”到“大难题”:Spring Boot 配置文件的坑与解
  • 04-ArkTS编程语言入门
  • 使用UE5开发《红色警戒3》类战略养成游戏的硬件配置指南
  • 源码导航页
  • Linux网络启程
  • 毛选一卷解析