ProductController.java
package com.example.controller;
import com.example.service.IProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
/**
* <p>
* 前端控制器
* </p>
*
* @author dd
* @since 2024-05-07
*
*/
//测试:http://localhost:8080/es_demo/product/kw/纯棉四件套/page/1
@RestController
@RequestMapping("product")
public class ProductController {
@Autowired
private IProductService productService;
// 将mysql中的product数据保存到es中
@GetMapping("saveProductToES")
public String saveProductToES(){
productService.saveProductFromDBToES();
return "ok";
}
//在生产环境中,直接操作Elasticsearch通常需要更复杂的错误处理和事务管理逻辑,以确保数据的一致性和完整性。
@GetMapping("delete/{productId}")
public String deleteProduct(@PathVariable Integer productId){
return productService.deleteProduct(productId);
}
// 搜索引擎:输入关键字
//返回 商品信息 + 页码信息
@GetMapping("kw/{kw}/page/{pageNum}")
public Map<String, Object> getByKeyword(@PathVariable("kw") String keyword,
@PathVariable("pageNum") Integer pageNum){
if(pageNum == null)
pageNum = 1;
Map<String, Object> result = productService.getByNameAndInfo(keyword, keyword, pageNum);
return result;
}
}
Product.java
package com.example.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.DateFormat;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* <p>
*
* </p>
*
* @author dd
* @since 2024-05-07
*/
// @Field(index = false, store = true,
// type = FieldType.Date,format = DateFormat.custom,
// pattern = "yyyy-MM-dd HH:mm:ss")
@Document(indexName = "myproduct")
public class Product implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "product_id",type = IdType.AUTO)
@Id
private Integer productId;
@Field(type = FieldType.Keyword)
private String productName;
private BigDecimal productPrice;
private String productImg;
private Integer productCount;
@Field( type = FieldType.Date,name = "update_time",format = {},
pattern = "yyyy-MM-dd HH:mm:ss || yyyy-MM-dd'T'HH:mm:ss'+08:00' || strict_date_opotional_time || epoch_millis")
private LocalDateTime createTime;
@Field( type = FieldType.Date,name = "update_time",format = {},
pattern = "yyyy-MM-dd HH:mm:ss || yyyy-MM-dd'T'HH:mm:ss'+08:00' || strict_date_opotional_time || epoch_millis")
private LocalDateTime updateTime;
@Field(type = FieldType.Text,analyzer = "ik_smart",searchAnalyzer = "ik_max_word")
private String productInfo;
public Integer getProductId() {
return productId;
}
public void setProductId(Integer productId) {
this.productId = productId;
}
public String getProductName() {
return productName;
}
public void setProductName(String productName) {
this.productName = productName;
}
public BigDecimal getProductPrice() {
return productPrice;
}
public void setProductPrice(BigDecimal productPrice) {
this.productPrice = productPrice;
}
public String getProductImg() {
return productImg;
}
public void setProductImg(String productImg) {
this.productImg = productImg;
}
public Integer getProductCount() {
return productCount;
}
public void setProductCount(Integer productCount) {
this.productCount = productCount;
}
public LocalDateTime getCreateTime() {
return createTime;
}
public void setCreateTime(LocalDateTime createTime) {
this.createTime = createTime;
}
public LocalDateTime getUpdateTime() {
return updateTime;
}
public void setUpdateTime(LocalDateTime updateTime) {
this.updateTime = updateTime;
}
public String getProductInfo() {
return productInfo;
}
public void setProductInfo(String productInfo) {
this.productInfo = productInfo;
}
@Override
public String toString() {
return "Product{" +
"productId=" + productId +
", productName=" + productName +
", productPrice=" + productPrice +
", productImg=" + productImg +
", productCount=" + productCount +
", createTime=" + createTime +
", updateTime=" + updateTime +
", productInfo=" + productInfo +
"}";
}
}
ElasticsearchSyncListener.java
package com.example.listener;
import com.example.service.impl.ProductDeletedEvent;
import com.example.mapper.ProductElasticSearchMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
@Component
public class ElasticsearchSyncListener {
@Autowired
private ProductElasticSearchMapper productElasticSearchMapper;
@EventListener
public void handleProductDeletedEvent(ProductDeletedEvent event) {
Integer productId = event.getProductId();
// 删除Elasticsearch中的记录
productElasticSearchMapper.deleteById(productId);
}
}
ProductElasticSearchMapper.java
package com.example.mapper;
import com.example.entity.Product;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository //父接口已经 实现对象在es中的基础CRUD操作
public interface ProductElasticSearchMapper extends ElasticsearchRepository<Product,Integer> {
//方法名必须是:findBy + pojo类属性名(首字母大写)
public List<Product> findByProductName(String productName);
public List<Product> findByProductInfo(String productInfo);
}
ProductMapper.java
package com.example.mapper;
import com.example.entity.Product;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
/**
* <p>
* Mapper 接口
* </p>
*
* @author dd
* @since 2024-05-07
*/
public interface ProductMapper extends BaseMapper<Product> {
}
ProductDeletedEvent.java
package com.example.service.impl;
import org.springframework.context.ApplicationEvent;
//定义一个事件类来表示删除操作
public class ProductDeletedEvent extends ApplicationEvent {
private final Integer productId;
public ProductDeletedEvent(Object source, Integer productId) {
super(source);
this.productId = productId;
}
public Integer getProductId() {
return productId;
}
}
ProductServiceImpl.java
package com.example.service.impl;
import com.example.entity.Product;
import com.example.mapper.ProductElasticSearchMapper;
import com.example.mapper.ProductMapper;
import com.example.service.IProductService;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.MatchQueryBuilder;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.sort.FieldSortBuilder;
import org.elasticsearch.search.sort.SortBuilder;
import org.elasticsearch.search.sort.SortBuilders;
import org.elasticsearch.search.sort.SortOrder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.event.EventListener;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.elasticsearch.core.*;
import org.springframework.data.elasticsearch.core.query.NativeSearchQuery;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
//空值检查:
//在处理高亮字段时,已经对highlightFields.get("productName")和highlightFields.get("productInfo")进行了空值检查,确保Product类的getProductName和getProductInfo方法也能处理可能的空值情况。
//
//字段名匹配:
//确保在Elasticsearch中定义的字段名(如productName和productInfo)与在Java代码中使用的字段名相匹配。
//
//性能考虑:
//如果productMapper.selectList(null)返回大量的产品数据,并且一次性将它们全部保存到Elasticsearch中,这可能会导致性能问题。考虑分批保存或使用Elasticsearch的批量API来提高性能。
//
//查询字符串分析:
//使用了queryStringQuery,这可能会对输入的productName进行分词查询。如果希望精确匹配整个字段值,而不是基于分词,可能需要使用termQuery或matchPhraseQuery。但是,由于处理的是全文搜索场景,queryStringQuery通常是合适的。
//安全性:
//如果productName或productInfo参数来自不受信任的源(如用户输入),请确保对它们进行适当的验证和清理,以防止SQL注入或Elasticsearch注入攻击。不过,由于是在应用层进行Elasticsearch查询,而不是直接拼接查询字符串,因此SQL注入的风险较低。但仍然需要注意Elasticsearch注入的风险。
//返回类型:
//返回了一个包含分页信息和搜索结果的Map。确保前端或调用此服务的客户端知道如何解析这个Map。考虑定义一个DTO(数据传输对象)来封装返回的数据,以提供更明确的契约。
/**
* <p>
* 服务实现类
* </p>
*
* @author dd
* @since 2024-05-07
*/
@Service
public class ProductServiceImpl implements IProductService {
@Autowired
private ProductMapper productMapper;
@Autowired
private ProductElasticSearchMapper productElasticSearchMapper;
@Autowired
private ElasticsearchRestTemplate restTemplate;
@Autowired
private ApplicationEventPublisher applicationEventPublisher;
// 把mysql中的数据查询出来,保存商品数据到es中
//当前代码逻辑中的saveProductFromDBToES方法设计为一个全量同步操作,它会从MySQL数据库中查询所有产品记录,并将它们全部保存到Elasticsearch中。因此,当从数据库中删除一条记录时,该方法不会意识到有记录被删除,因为它仅仅是再次执行了全量查询和保存操作。
//
//为了解决这个问题,可以采取以下几种策略之一:
//
//增量同步:设计一个机制来跟踪数据库中的更改(如使用数据库的binlog日志),并仅同步自上次同步以来发生的更改。这通常比较复杂,但可以实现实时或近实时的数据同步。
//
//定期全量同步:可以定期(如每小时、每天)运行saveProductFromDBToES方法来进行全量同步。这种方法比较简单,但可能会导致数据在一定时间窗口内不同步。
//
//删除操作同步:在应用程序中添加逻辑,以便在数据库记录被删除时,也在Elasticsearch中删除相应的文档。这通常需要在数据库删除操作的地方添加额外的代码或使用触发器。
//
//使用监听器或事件驱动:如果使用的是支持事件驱动或变更数据捕获(CDC)的数据库或框架,可以配置监听器来捕获数据库更改事件,并据此更新Elasticsearch中的数据。
@Override
public boolean saveProductFromDBToES() {
//1. select product from mysql
List<Product> productList = productMapper.selectList(null);
//2. save to es
Iterable<Product> products = productElasticSearchMapper.saveAll(productList);
return true;
}
@Override
public String deleteProduct(Integer productId) {
// 这里有一个方法来删除数据库中的记录
int rows = productMapper.deleteById(productId);
if(rows>0){
// 发布删除事件
applicationEventPublisher.publishEvent(new ProductDeletedEvent(this, productId));
return "删除成功";
}
return "删除失败";
}
//创建一个事件监听器来监听ProductDeletedEvent并执行删除Elasticsearch中的记录的操作
//ElasticsearchSyncListener类是一个Spring组件,它使用@EventListener注解来标记handleProductDeletedEvent方法作为事件监听器。当ProductDeletedEvent事件被发布时,这个方法将被调用,并执行删除Elasticsearch中相应记录的操作。
//
//请注意,需要确保ApplicationEventPublisher和ProductElasticSearchMapper都被正确地注入到相应的类中。此外,可能还需要配置Spring的事件支持,但通常情况下,在Spring Boot应用程序中,事件支持是默认启用的。
//
//最后,当调用ProductService的deleteProduct方法时,它将删除数据库中的记录并发布一个ProductDeletedEvent事件。这个事件将被ElasticsearchSyncListener监听并处理,从而删除Elasticsearch中的相应记录。
// 实现接口方法,基于Elasticsearch的全文搜索,包括分词和高亮功能
@Override
public Map<String,Object> getByNameAndInfo(String productName, String productInfo, Integer pageNum) {
//设置分页信息,每页显示3条记录,pageNum-1是因为页码通常从1开始,而数组索引从0开始
PageRequest page = PageRequest.of(pageNum - 1, 3);
//创建一个布尔查询构建器,用于组合多个查询条件
BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder();
//如果productName不为空,则创建一个基于productName的查询,并将其添加到布尔查询中
if(productName !=null){
QueryBuilder queryBuilder = QueryBuilders.queryStringQuery(productName);
boolQueryBuilder.must(queryBuilder);
}else{
//如果productName为空且productInfo不为空,则创建一个基于productInfo的匹配查询,并将其添加到布尔查询中
if(productInfo !=null)
boolQueryBuilder.must(new MatchQueryBuilder("productInfo",productInfo));
}
//根据商品价格降序排序
SortBuilder sortBuilder = SortBuilders.fieldSort("productPrice").order(SortOrder.DESC);
//FieldSortBuilder priceSort = SortBuilders.fieldSort("productPrice"); // 按产品价格排序(需要指定升序或降序)
//构建高亮查询,设置高亮字段和高亮标签
NativeSearchQueryBuilder builder=new NativeSearchQueryBuilder();
NativeSearchQuery query=builder
.withQuery(boolQueryBuilder)
.withPageable(page)
.withSort(sortBuilder)
.withHighlightFields(
new HighlightBuilder.Field("productInfo"),
new HighlightBuilder.Field("productName"))
.withHighlightBuilder(new HighlightBuilder()
.preTags("<span style='color:red'>")
.postTags("</span>"))
.build();
// 执行查询,获取搜索结果
SearchHits<Product> search = restTemplate.search(query, Product.class);
List<Product> productList= new ArrayList<>();
//遍历搜索结果,处理高亮信息,并将处理后的产品添加到列表中
for(SearchHit<Product> searchHit:search){
//获取高亮字段信息
Map<String ,List<String>> highlightFields = searchHit.getHighlightFields();
//将高亮的内容填充到content中
//处理产品名称和产品信息的高亮信息,并将其设置回产品对象中
String highLightProName = highlightFields.get("productName") ==null ?searchHit.getContent().getProductName() :highlightFields.get("productName").get(0);
String highLightProInfo = highlightFields.get("productInfo") ==null ?searchHit.getContent().getProductInfo() :highlightFields.get("productInfo").get(0);
searchHit.getContent().setProductName(highLightProName );
searchHit.getContent().setProductInfo(highLightProInfo);
//将处理后的产品添加到列表中
productList.add(searchHit.getContent());
}
//创建SearchPage对象,用于获取分页信息(假设SearchHitSupport是有效的)
SearchPage<Product> searchPage= SearchHitSupport.searchPageFor(search,query.getPageable());
//总记录数
long totalElements=searchPage.getTotalElements();
//总页数
int totalPages=searchPage.getTotalPages();
//当前页数
int currentPageForDisplay=searchPage.getPageable().getPageNumber() + 1;// 通常前端期望页码从1开始
System.out.println(currentPageForDisplay);
//创建一个Map对象,用于返回查询结果和分页信息
Map<String,Object> map=new HashMap<>();
map.put("totalElements",totalElements); // 总记录数
map.put("totalPages",totalPages); //总页数
map.put("currentPage",currentPageForDisplay); // 当前页码
map.put("productList",productList); // 商品数据信息
//返回查询结果和分页信息的Map对象
return map;
}
}
SyncProductService.java
package com.example.service.impl;
import com.example.service.IProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
@Service
public class SyncProductService {
@Autowired
private IProductService productService;
@Scheduled(fixedRate = 2000) // 单位是毫秒
public void syncProductsFromDBToES() {
productService.saveProductFromDBToES();
}
}
IProductService.java
package com.example.service;
import com.example.entity.Product;
import com.baomidou.mybatisplus.extension.service.IService;
import java.util.Map;
/**
* <p>
* 服务类
* </p>
*
* @author dd
* @since 2024-05-07
*/
public interface IProductService {
public boolean saveProductFromDBToES();
public String deleteProduct(Integer productId);
// 搜索引擎中的关键字
// 返回满足条件的商品的数据+分页信息
public Map<String,Object> getByNameAndInfo(String productName, String productInfo, Integer pageNum);
}
ElasticSearchSpringDemoApplication.java
package com.example;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@MapperScan("com.example.mapper")
@EnableScheduling
public class ElasticSearchSpringDemoApplication {
public static void main(String[] args) {
SpringApplication.run(ElasticSearchSpringDemoApplication.class, args);
}
}
ServletInitializer.java
package com.example;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
public class ServletInitializer extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(ElasticSearchSpringDemoApplication.class);
}
}
product.sql
/*
Navicat Premium Data Transfer
Source Server : rootWindows
Source Server Type : MySQL
Source Server Version : 80036
Source Host : localhost:3306
Source Schema : cloud_product_db
Target Server Type : MySQL
Target Server Version : 80036
File Encoding : 65001
Date: 07/05/2024 18:49:44
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for product
-- ----------------------------
DROP TABLE IF EXISTS `product`;
CREATE TABLE `product` (
`product_id` int(0) NOT NULL AUTO_INCREMENT,
`product_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`product_price` decimal(10, 2) NULL DEFAULT NULL,
`product_img` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`product_count` int(0) NULL DEFAULT NULL,
`create_time` datetime(0) NULL DEFAULT NULL,
`update_time` datetime(0) NULL DEFAULT NULL,
`product_info` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
PRIMARY KEY (`product_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of product
-- ----------------------------
SET FOREIGN_KEY_CHECKS = 1;
同步
增量同步:设计一个机制来跟踪数据库中的更改(如使用数据库的binlog日志),并仅同步自上次同步以来发生的更改。这通常比较复杂,但可以实现实时或近实时的数据同步。
定期全量同步:可以定期(如每小时、每天)运行saveProductFromDBToES方法来进行全量同步。这种方法比较简单,但可能会导致数据在一定时间窗口内不同步。
删除操作同步:在应用程序中添加逻辑,以便在数据库记录被删除时,也在Elasticsearch中删除相应的文档。这通常需要在数据库删除操作的地方添加额外的代码或使用触发器。
使用监听器或事件驱动:如果使用的是支持事件驱动或变更数据捕获(CDC)的数据库或框架,可以配置监听器来捕获数据库更改事件,并据此更新Elasticsearch中的数据。
ProductMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.mapper.ProductMapper">
</mapper>
application.yaml
# ???
server:
servlet:
context-path: /es_demo
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/cloud_product_db?useSSL=true&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: root
password: 123456
elasticsearch:
uris: localhost:9200
connection-timeout: 5s
socket-timeout: 30s
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.6</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>elasticSearchSpringDemo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>war</packaging>
<name>elasticSearchSpringDemo</name>
<description>elasticSearchSpringDemo</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.28</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.baomidou/mybatis-plus-boot-starter -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.baomidou/mybatis-plus-generate -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.5.1</version>
</dependency>
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.31</version>
</dependency>
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-core</artifactId>
<version>8.11.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>