实现一个基于电商场景的复杂示例,包含商品搜索、过滤、聚合分析、地理位置查询等功能。
一、创建 Spring Boot 项目
使用 Spring Initializr 创建项目,添加以下依赖:
- Spring Web
- Spring Data Elasticsearch
- Lombok (可选)
- Spring Validation
或者手动添加依赖:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
二、配置 Elasticsearch 连接
在application.properties
中添加:
spring.elasticsearch.uris=http://localhost:9200
spring.elasticsearch.username=elastic
spring.elasticsearch.password=your_password
三、定义文档模型
创建商品文档类:
package com.example.esdemo.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.*;
import java.util.Date;
import java.util.List;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Document(indexName = "products")
public class Product {
@Id
private String id;
@Field(type = FieldType.Text, analyzer = "ik_max_word")
private String name; // 商品名称
@Field(type = FieldType.Text, analyzer = "ik_max_word")
private String description; // 商品描述
@Field(type = FieldType.Double)
private double price; // 价格
@Field(type = FieldType.Integer)
private int stock; // 库存
@Field(type = FieldType.Date)
private Date createTime; // 创建时间
@Field(type = FieldType.Keyword)
private String category; // 分类
@Field(type = FieldType.Nested)
private List<Attribute> attributes; // 商品属性
@Field(type = FieldType.GeoPoint)
private GeoPoint location; // 地理位置
@Field(type = FieldType.Boolean)
private boolean onSale; // 是否促销
@Field(type = FieldType.Integer)
private int sales; // 销量
@Field(type = FieldType.Double)
private double score; // 评分
}
@Data
@AllArgsConstructor
@NoArgsConstructor
class Attribute {
@Field(type = FieldType.Keyword)
private String name; // 属性名
@Field(type = FieldType.Keyword)
private String value; // 属性值
}
四、创建自定义 Repository 接口
package com.example.esdemo.repository;
import com.example.esdemo.model.Product;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.elasticsearch.annotations.Query;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import java.util.Date;
import java.util.List;
public interface ProductRepository extends ElasticsearchRepository<Product, String>, CustomProductRepository {
// 基本查询方法
List<Product> findByName(String name);
Page<Product> findByCategory(String category, Pageable pageable);
// 自定义查询(使用JSON格式)
@Query("{\"range\": {\"price\": {\"gte\": ?0, \"lte\": ?1}}}")
List<Product> findByPriceRange(double minPrice, double maxPrice);
// 复杂查询示例
List<Product> findByOnSaleAndCreateTimeAfter(boolean onSale, Date date);
}
五、实现自定义查询接口
package com.example.esdemo.repository;
import com.example.esdemo.model.Product;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import java.util.List;
public interface CustomProductRepository {
// 全文搜索 + 过滤
Page<Product> search(String keyword,
Double minPrice,
Double maxPrice,
String category,
List<String> attributes,
Boolean onSale,
Pageable pageable);
// 地理位置搜索
List<Product> searchByLocation(double lat, double lon, double distance, String unit);
// 聚合分析:统计分类下的商品数量
List<CategoryCount> countByCategory();
// 聚合分析:价格区间分布
List<PriceRange> analyzePriceDistribution();
// 聚合分析:热门属性值
List<AttributeValueCount> findPopularAttributes(int size);
}
// 聚合结果模型
interface CategoryCount {
String getCategory();
long getCount();
}
interface PriceRange {
String getKey();
long getCount();
}
interface AttributeValueCount {
String getAttributeName();
String getAttributeValue();
long getCount();
}
六、实现自定义查询
package com.example.esdemo.repository.impl;
import com.example.esdemo.model.Product;
import com.example.esdemo.repository.CustomProductRepository;
import com.example.esdemo.repository.CategoryCount;
import com.example.esdemo.repository.PriceRange;
import com.example.esdemo.repository.AttributeValueCount;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.index.query.*;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.bucket.filter.Filter;
import org.elasticsearch.search.aggregations.bucket.filter.FilterAggregationBuilder;
import org.elasticsearch.search.aggregations.bucket.nested.Nested;
import org.elasticsearch.search.aggregations.bucket.nested.NestedAggregationBuilder;
import org.elasticsearch.search.aggregations.bucket.terms.ParsedTerms;
import org.elasticsearch.search.aggregations.bucket.terms.Terms;
import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder;
import org.elasticsearch.search.aggregations.metrics.ParsedStats;
import org.elasticsearch.search.aggregations.metrics.StatsAggregationBuilder;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.sort.FieldSortBuilder;
import org.elasticsearch.search.sort.SortBuilders;
import org.elasticsearch.search.sort.SortOrder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
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.stereotype.Repository;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Repository
public class CustomProductRepositoryImpl implements CustomProductRepository {
@Autowired
private RestHighLevelClient client;
@Autowired
private ElasticsearchOperations elasticsearchOperations;
@Override
public Page<Product> search(String keyword, Double minPrice, Double maxPrice, String category,
List<String> attributes, Boolean onSale, Pageable pageable) {
// 构建布尔查询
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
// 关键词搜索
if (keyword != null && !keyword.isEmpty()) {
MultiMatchQueryBuilder multiMatchQuery = QueryBuilders.multiMatchQuery(
keyword, "name^3", "description^2", "attributes.value")
.type(MultiMatchQueryBuilder.Type.BEST_FIELDS)
.boost(1.0f);
boolQuery.must(multiMatchQuery);
}
// 价格过滤
if (minPrice != null || maxPrice != null) {
RangeQueryBuilder priceQuery = QueryBuilders.rangeQuery("price");
if (minPrice != null) priceQuery.gte(minPrice);
if (maxPrice != null) priceQuery.lte(maxPrice);
boolQuery.filter(priceQuery);
}
// 分类过滤
if (category != null && !category.isEmpty()) {
boolQuery.filter(QueryBuilders.termQuery("category.keyword", category));
}
// 属性过滤
if (attributes != null && !attributes.isEmpty()) {
BoolQueryBuilder attrBoolQuery = QueryBuilders.boolQuery();
for (String attr : attributes) {
String[] parts = attr.split(":");
if (parts.length == 2) {
NestedQueryBuilder nestedQuery = QueryBuilders.nestedQuery(
"attributes",
QueryBuilders.boolQuery()
.must(QueryBuilders.termQuery("attributes.name.keyword", parts[0]))
.must(QueryBuilders.termQuery("attributes.value.keyword", parts[1])),
ScoreMode.Avg
);
attrBoolQuery.must(nestedQuery);
}
}
boolQuery.filter(attrBoolQuery);
}
// 促销过滤
if (onSale != null) {
boolQuery.filter(QueryBuilders.termQuery("onSale", onSale));
}
// 构建搜索查询
NativeSearchQuery searchQuery = new NativeSearchQueryBuilder()
.withQuery(boolQuery)
.withPageable(pageable)
.withSorts(
SortBuilders.scoreSort().order(SortOrder.DESC), // 按相关性排序
SortBuilders.fieldSort("sales").order(SortOrder.DESC) // 按销量排序
)
.build();
// 执行搜索
SearchHits<Product> searchHits = elasticsearchOperations.search(searchQuery, Product.class);
List<Product> products = searchHits.map(SearchHit::getContent).toList();
return new PageImpl<>(products, pageable, searchHits.getTotalHits());
}
@Override
public List<Product> searchByLocation(double lat, double lon, double distance, String unit) {
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
// 构建地理位置查询
GeoDistanceQueryBuilder geoQuery = QueryBuilders.geoDistanceQuery("location")
.point(lat, lon)
.distance(distance, unit);
sourceBuilder.query(geoQuery);
sourceBuilder.sort(SortBuilders.geoDistanceSort("location", lat, lon)
.order(SortOrder.ASC)
.unit(unit));
SearchRequest searchRequest = new SearchRequest("products");
searchRequest.source(sourceBuilder);
try {
SearchResponse response = client.search(searchRequest, RequestOptions.DEFAULT);
SearchHit[] hits = response.getHits().getHits();
List<Product> products = new ArrayList<>();
for (SearchHit hit : hits) {
Product product = elasticsearchOperations.getElasticsearchConverter()
.read(Product.class, hit.getSourceAsMap());
products.add(product);
}
return products;
} catch (IOException e) {
throw new RuntimeException("地理位置搜索失败", e);
}
}
@Override
public List<CategoryCount> countByCategory() {
TermsAggregationBuilder aggregation = AggregationBuilders
.terms("categories")
.field("category.keyword")
.size(10);
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
sourceBuilder.aggregation(aggregation);
sourceBuilder.size(0); // 不返回文档,只返回聚合结果
SearchRequest searchRequest = new SearchRequest("products");
searchRequest.source(sourceBuilder);
try {
SearchResponse response = client.search(searchRequest, RequestOptions.DEFAULT);
ParsedTerms categories = response.getAggregations().get("categories");
List<CategoryCount> result = new ArrayList<>();
for (Terms.Bucket bucket : categories.getBuckets()) {
final String category = bucket.getKeyAsString();
final long count = bucket.getDocCount();
result.add(new CategoryCount() {
@Override
public String getCategory() {
return category;
}
@Override
public long getCount() {
return count;
}
});
}
return result;
} catch (IOException e) {
throw new RuntimeException("分类统计失败", e);
}
}
@Override
public List<PriceRange> analyzePriceDistribution() {
// 定义价格区间
Map<String, Range> priceRanges = new HashMap<>();
priceRanges.put("0-500", new Range(0, 500));
priceRanges.put("500-1000", new Range(500, 1000));
priceRanges.put("1000-2000", new Range(1000, 2000));
priceRanges.put("2000+", new Range(2000, null));
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
// 构建聚合查询
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
sourceBuilder.query(boolQuery);
for (Map.Entry<String, Range> entry : priceRanges.entrySet()) {
String key = entry.getKey();
Range range = entry.getValue();
RangeQueryBuilder rangeQuery = QueryBuilders.rangeQuery("price");
if (range.getFrom() != null) rangeQuery.gte(range.getFrom());
if (range.getTo() != null) rangeQuery.lt(range.getTo());
FilterAggregationBuilder filterAgg = AggregationBuilders
.filter(key, rangeQuery);
sourceBuilder.aggregation(filterAgg);
}
sourceBuilder.size(0); // 不返回文档
SearchRequest searchRequest = new SearchRequest("products");
searchRequest.source(sourceBuilder);
try {
SearchResponse response = client.search(searchRequest, RequestOptions.DEFAULT);
List<PriceRange> result = new ArrayList<>();
for (Map.Entry<String, Range> entry : priceRanges.entrySet()) {
String key = entry.getKey();
Filter filter = response.getAggregations().get(key);
final String rangeKey = key;
final long count = filter.getDocCount();
result.add(new PriceRange() {
@Override
public String getKey() {
return rangeKey;
}
@Override
public long getCount() {
return count;
}
});
}
return result;
} catch (IOException e) {
throw new RuntimeException("价格分布分析失败", e);
}
}
@Override
public List<AttributeValueCount> findPopularAttributes(int size) {
NestedAggregationBuilder nestedAgg = AggregationBuilders
.nested("attributes_agg", "attributes");
TermsAggregationBuilder attrNameAgg = AggregationBuilders
.terms("attr_names")
.field("attributes.name.keyword")
.size(10);
TermsAggregationBuilder attrValueAgg = AggregationBuilders
.terms("attr_values")
.field("attributes.value.keyword")
.size(size);
attrNameAgg.subAggregation(attrValueAgg);
nestedAgg.subAggregation(attrNameAgg);
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
sourceBuilder.aggregation(nestedAgg);
sourceBuilder.size(0);
SearchRequest searchRequest = new SearchRequest("products");
searchRequest.source(sourceBuilder);
try {
SearchResponse response = client.search(searchRequest, RequestOptions.DEFAULT);
Nested attributesAgg = response.getAggregations().get("attributes_agg");
ParsedTerms attrNames = attributesAgg.getAggregations().get("attr_names");
List<AttributeValueCount> result = new ArrayList<>();
for (Terms.Bucket nameBucket : attrNames.getBuckets()) {
String attrName = nameBucket.getKeyAsString();
ParsedTerms attrValues = nameBucket.getAggregations().get("attr_values");
for (Terms.Bucket valueBucket : attrValues.getBuckets()) {
final String finalAttrName = attrName;
final String attrValue = valueBucket.getKeyAsString();
final long count = valueBucket.getDocCount();
result.add(new AttributeValueCount() {
@Override
public String getAttributeName() {
return finalAttrName;
}
@Override
public String getAttributeValue() {
return attrValue;
}
@Override
public long getCount() {
return count;
}
});
}
}
return result;
} catch (IOException e) {
throw new RuntimeException("热门属性分析失败", e);
}
}
// 内部类:表示价格区间
private static class Range {
private final Double from;
private final Double to;
public Range(Double from, Double to) {
this.from = from;
this.to = to;
}
public Double getFrom() {
return from;
}
public Double getTo() {
return to;
}
}
}
七、创建 Service 层
package com.example.esdemo.service;
import com.example.esdemo.model.Product;
import com.example.esdemo.repository.ProductRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.List;
@Service
public class ProductService {
@Autowired
private ProductRepository productRepository;
/**
* 保存商品
*/
public Product save(Product product) {
if (product.getCreateTime() == null) {
product.setCreateTime(new Date());
}
return productRepository.save(product);
}
/**
* 根据ID获取商品
*/
public Product getById(String id) {
return productRepository.findById(id).orElse(null);
}
/**
* 获取所有商品
*/
public Page<Product> getAll(Pageable pageable) {
return productRepository.findAll(pageable);
}
/**
* 复杂搜索
*/
public Page<Product> search(String keyword,
Double minPrice,
Double maxPrice,
String category,
List<String> attributes,
Boolean onSale,
Pageable pageable) {
return productRepository.search(keyword, minPrice, maxPrice, category, attributes, onSale, pageable);
}
/**
* 地理位置搜索
*/
public List<Product> searchByLocation(double lat, double lon, double distance, String unit) {
return productRepository.searchByLocation(lat, lon, distance, unit);
}
/**
* 统计分类下的商品数量
*/
public List<CategoryCount> countByCategory() {
return productRepository.countByCategory();
}
/**
* 分析价格分布
*/
public List<PriceRange> analyzePriceDistribution() {
return productRepository.analyzePriceDistribution();
}
/**
* 查找热门属性
*/
public List<AttributeValueCount> findPopularAttributes(int size) {
return productRepository.findPopularAttributes(size);
}
// 聚合结果接口
public interface CategoryCount {
String getCategory();
long getCount();
}
public interface PriceRange {
String getKey();
long getCount();
}
public interface AttributeValueCount {
String getAttributeName();
String getAttributeValue();
long getCount();
}
}
八、创建 Controller 层
package com.example.esdemo.controller;
import com.example.esdemo.model.Product;
import com.example.esdemo.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import java.util.List;
@RestController
@RequestMapping("/api/products")
public class ProductController {
@Autowired
private ProductService productService;
/**
* 添加商品
*/
@PostMapping
public Product addProduct(@RequestBody @Valid Product product) {
return productService.save(product);
}
/**
* 根据ID获取商品
*/
@GetMapping("/{id}")
public Product getProduct(@PathVariable String id) {
return productService.getById(id);
}
/**
* 获取所有商品
*/
@GetMapping
public Page<Product> getAllProducts(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size
) {
return productService.getAll(PageRequest.of(page, size));
}
/**
* 复杂搜索
*/
@GetMapping("/search")
public Page<Product> search(
@RequestParam(required = false) String keyword,
@RequestParam(required = false) Double minPrice,
@RequestParam(required = false) Double maxPrice,
@RequestParam(required = false) String category,
@RequestParam(required = false) List<String> attributes,
@RequestParam(required = false) Boolean onSale,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size
) {
return productService.search(keyword, minPrice, maxPrice, category, attributes, onSale,
PageRequest.of(page, size));
}
/**
* 地理位置搜索
*/
@GetMapping("/search/nearby")
public List<Product> searchByLocation(
@RequestParam double lat,
@RequestParam double lon,
@RequestParam(defaultValue = "10") double distance,
@RequestParam(defaultValue = "km") String unit
) {
return productService.searchByLocation(lat, lon, distance, unit);
}
/**
* 统计分类下的商品数量
*/
@GetMapping("/stats/categories")
public List<ProductService.CategoryCount> countByCategory() {
return productService.countByCategory();
}
/**
* 分析价格分布
*/
@GetMapping("/stats/prices")
public List<ProductService.PriceRange> analyzePriceDistribution() {
return productService.analyzePriceDistribution();
}
/**
* 查找热门属性
*/
@GetMapping("/stats/attributes")
public List<ProductService.AttributeValueCount> findPopularAttributes(
@RequestParam(defaultValue = "5") int size
) {
return productService.findPopularAttributes(size);
}
}
九、测试数据与验证
1. 添加测试数据
package com.example.esdemo;
import com.example.esdemo.model.Attribute;
import com.example.esdemo.model.GeoPoint;
import com.example.esdemo.model.Product;
import com.example.esdemo.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.List;
@SpringBootApplication
public class EsDemoApplication implements CommandLineRunner {
@Autowired
private ProductService productService;
public static void main(String[] args) {
SpringApplication.run(EsDemoApplication.class, args);
}
@Override
public void run(String... args) throws Exception {
// 添加测试数据
List<Product> products = Arrays.asList(
new Product(null, "苹果 iPhone 15", "全新一代智能手机", 7999.0, 100, new Date(),
"手机",
Arrays.asList(
new Attribute("品牌", "苹果"),
new Attribute("颜色", "黑色"),
new Attribute("存储容量", "256GB")
),
new GeoPoint(39.9042, 116.4074), // 北京坐标
true, 500, 4.8),
new Product(null, "华为 Mate 60 Pro", "高端旗舰手机", 6999.0, 80, new Date(),
"手机",
Arrays.asList(
new Attribute("品牌", "华为"),
new Attribute("颜色", "银色"),
new Attribute("存储容量", "512GB")
),
new GeoPoint(30.2741, 120.1551), // 杭州坐标
true, 350, 4.9),
new Product(null, "小米 14", "性能旗舰手机", 4999.0, 120, new Date(),
"手机",
Arrays.asList(
new Attribute("品牌", "小米"),
new Attribute("颜色", "蓝色"),
new Attribute("存储容量", "256GB")
),
new GeoPoint(31.2304, 121.4737), // 上海坐标
false, 280, 4.7),
new Product(null, "Apple MacBook Pro", "专业笔记本电脑", 14999.0, 50, new Date(),
"电脑",
Arrays.asList(
new Attribute("品牌", "苹果"),
new Attribute("屏幕尺寸", "14英寸"),
new Attribute("内存", "16GB")
),
new GeoPoint(39.9042, 116.4074),
true, 180, 4.9),
new Product(null, "联想 ThinkPad X1 Carbon", "商务笔记本电脑", 12999.0, 60, new Date(),
"电脑",
Arrays.asList(
new Attribute("品牌", "联想"),
new Attribute("屏幕尺寸", "14英寸"),
new Attribute("内存", "32GB")
),
new GeoPoint(39.9042, 116.4074),
false, 120, 4.8)
);
// 保存到Elasticsearch
products.forEach(productService::save);
System.out.println("测试数据已添加");
}
}
2. 测试 API
-
全文搜索
bash
GET http://localhost:8080/api/products/search?keyword=苹果&category=手机
-
价格过滤搜索
bash
GET http://localhost:8080/api/products/search?minPrice=5000&maxPrice=10000
-
属性过滤搜索
bash
GET http://localhost:8080/api/products/search?attributes=品牌:华为&attributes=存储容量:512GB
-
地理位置搜索
bash
GET http://localhost:8080/api/products/search/nearby?lat=39.9042&lon=116.4074&distance=1000
-
聚合分析
bash
GET http://localhost:8080/api/products/stats/categories GET http://localhost:8080/api/products/stats/prices GET http://localhost:8080/api/products/stats/attributes
十、高级特性与优化
1. 分词器配置
在application.properties
中添加自定义分词器配置:
# 自定义分词器配置
spring.elasticsearch.rest.connection-timeout=10000
spring.elasticsearch.rest.read-timeout=30000
2. 性能优化
- 批量操作:使用
BulkRequest
批量导入数据 - 预热缓存:对热门查询进行预加载
- 索引分片优化:根据数据量调整分片数和副本数
3. 监控与调优
- 使用 Elasticsearch 提供的监控 API:
java
public ClusterHealthResponse checkClusterHealth() { ClusterHealthRequest request = new ClusterHealthRequest(); try { return client.cluster().health(request, RequestOptions.DEFAULT); } catch (IOException e) { throw new RuntimeException("检查集群健康状态失败", e); } }