本文目录导读:

我来详细介绍SpringBoot集成Elasticsearch实现搜索功能的完整方案。
环境配置
添加依赖
<!-- SpringBoot 2.x 使用 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
<!-- SpringBoot 3.x 使用 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
<dependency>
<groupId>co.elastic.clients</groupId>
<artifactId>elasticsearch-java</artifactId>
</dependency>
配置文件
spring:
elasticsearch:
uris: http://localhost:9200
connection-timeout: 10s
socket-timeout: 30s
username: elastic # 如果有认证
password: changeme
# 或者使用RestHighLevelClient配置
data:
elasticsearch:
cluster-name: docker-cluster
cluster-nodes: localhost:9300
基础配置类
@Configuration
@EnableElasticsearchRepositories(basePackages = "com.example.repository")
public class ElasticsearchConfig {
@Bean
public ElasticsearchClient elasticsearchClient(RestClient restClient) {
ElasticsearchTransport transport = new RestClientTransport(restClient, new JacksonJsonpMapper());
return new ElasticsearchClient(transport);
}
@Bean
public RestClient restClient() {
RestClientBuilder builder = RestClient.builder(
new HttpHost("localhost", 9200, "http")
);
return builder.build();
}
}
文档实体类
@Data
@Document(indexName = "products", createIndex = true)
@Setting(shards = 3, replicas = 1)
public class Product {
@Id
private String id;
@Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_smart")
private String name;
@Field(type = FieldType.Text, analyzer = "ik_max_word")
private String description;
@Field(type = FieldType.Keyword)
private String category;
@Field(type = FieldType.Double)
private Double price;
@Field(type = FieldType.Integer)
private Integer stock;
@Field(type = FieldType.Date, format = DateFormat.date_time)
private LocalDateTime createTime;
@Field(type = FieldType.Boolean)
private Boolean status;
}
Repository层
@Repository
public interface ProductRepository extends ElasticsearchRepository<Product, String> {
// 基础查询
List<Product> findByName(String name);
List<Product> findByNameAndPrice(String name, Double price);
// 模糊查询
List<Product> findByNameContaining(String name);
// 范围查询
List<Product> findByPriceBetween(Double min, Double max);
// 多条件查询
List<Product> findByCategoryAndPriceLessThan(String category, Double price);
// 排序查询
List<Product> findByNameContainingOrderByPriceDesc(String name);
}
搜索服务类
@Service
@Slf4j
public class SearchService {
@Autowired
private ElasticsearchRestTemplate elasticsearchRestTemplate;
@Autowired
private ProductRepository productRepository;
@Autowired
private ElasticsearchClient elasticsearchClient;
/**
* 基础搜索
*/
public List<Product> searchByKeyword(String keyword) {
NativeSearchQuery searchQuery = new NativeSearchQueryBuilder()
.withQuery(QueryBuilders.matchQuery("name", keyword))
.withPageable(PageRequest.of(0, 10))
.build();
SearchHits<Product> searchHits = elasticsearchRestTemplate.search(searchQuery, Product.class);
return searchHits.stream()
.map(SearchHit::getContent)
.collect(Collectors.toList());
}
/**
* 高亮搜索
*/
public List<Map<String, Object>> searchWithHighlight(String keyword) {
NativeSearchQuery searchQuery = new NativeSearchQueryBuilder()
.withQuery(QueryBuilders.matchQuery("name", keyword))
.withHighlightFields(
new HighlightBuilder.Field("name")
.preTags("<span style='color:red'>")
.postTags("</span>")
)
.withPageable(PageRequest.of(0, 10))
.build();
SearchHits<Product> searchHits = elasticsearchRestTemplate.search(searchQuery, Product.class);
return searchHits.stream().map(hit -> {
Map<String, Object> result = new HashMap<>();
result.put("product", hit.getContent());
result.put("highlight", hit.getHighlightFields());
result.put("score", hit.getScore());
return result;
}).collect(Collectors.toList());
}
/**
* 布尔查询
*/
public List<Product> searchWithFilters(String keyword, String category, Double priceMin, Double priceMax) {
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
// 关键词搜索
if (StringUtils.hasText(keyword)) {
boolQuery.must(QueryBuilders.matchQuery("name", keyword));
}
// 分类过滤
if (StringUtils.hasText(category)) {
boolQuery.filter(QueryBuilders.termQuery("category", category));
}
// 价格范围
if (priceMin != null || priceMax != null) {
RangeQueryBuilder rangeQuery = QueryBuilders.rangeQuery("price");
if (priceMin != null) rangeQuery.gte(priceMin);
if (priceMax != null) rangeQuery.lte(priceMax);
boolQuery.filter(rangeQuery);
}
NativeSearchQuery searchQuery = new NativeSearchQueryBuilder()
.withQuery(boolQuery)
.withPageable(PageRequest.of(0, 10))
.withSort(Sort.by(Sort.Direction.DESC, "price"))
.build();
SearchHits<Product> searchHits = elasticsearchRestTemplate.search(searchQuery, Product.class);
return searchHits.stream()
.map(SearchHit::getContent)
.collect(Collectors.toList());
}
/**
* 聚合查询
*/
public Map<String, Object> aggregateSearch(String category) {
NativeSearchQuery searchQuery = new NativeSearchQueryBuilder()
.withQuery(StringUtils.hasText(category) ?
QueryBuilders.termQuery("category", category) : null)
.addAggregation(AggregationBuilders.terms("category_agg").field("category"))
.addAggregation(AggregationBuilders.stats("price_stats").field("price"))
.build();
SearchHits<Product> searchHits = elasticsearchRestTemplate.search(searchQuery, Product.class);
Map<String, Object> result = new HashMap<>();
result.put("products", searchHits.stream().map(SearchHit::getContent).collect(Collectors.toList()));
// 解析聚合结果
Aggregations aggregations = searchHits.getAggregations();
if (aggregations != null) {
ParsedStringTerms categoryAgg = aggregations.get("category_agg");
ParsedStats priceStats = aggregations.get("price_stats");
result.put("categories", categoryAgg.getBuckets().stream()
.map(bucket -> Map.of("key", bucket.getKey(), "count", bucket.getDocCount()))
.collect(Collectors.toList()));
result.put("priceStats", Map.of(
"min", priceStats.getMin(),
"max", priceStats.getMax(),
"avg", priceStats.getAvg(),
"sum", priceStats.getSum()
));
}
return result;
}
}
Controller层
@RestController
@RequestMapping("/api/search")
@Slf4j
public class SearchController {
@Autowired
private SearchService searchService;
@Autowired
private ProductRepository productRepository;
/**
* 基础搜索
*/
@GetMapping("/basic")
public Result<List<Product>> basicSearch(@RequestParam String keyword) {
return Result.success(searchService.searchByKeyword(keyword));
}
/**
* 高亮搜索
*/
@GetMapping("/highlight")
public Result<List<Map<String, Object>>> highlightSearch(@RequestParam String keyword) {
return Result.success(searchService.searchWithHighlight(keyword));
}
/**
* 高级搜索
*/
@GetMapping("/advanced")
public Result<List<Product>> advancedSearch(
@RequestParam(required = false) String keyword,
@RequestParam(required = false) String category,
@RequestParam(required = false) Double priceMin,
@RequestParam(required = false) Double priceMax) {
return Result.success(searchService.searchWithFilters(keyword, category, priceMin, priceMax));
}
/**
* 聚合查询
*/
@GetMapping("/aggregate")
public Result<Map<String, Object>> aggregateSearch(
@RequestParam(required = false) String category) {
return Result.success(searchService.aggregateSearch(category));
}
/**
* 添加文档
*/
@PostMapping("/index")
public Result<Product> indexDocument(@RequestBody Product product) {
product.setCreateTime(LocalDateTime.now());
Product saved = productRepository.save(product);
return Result.success(saved);
}
/**
* 批量导入
*/
@PostMapping("/batch-index")
public Result<String> batchIndex(@RequestBody List<Product> products) {
products.forEach(p -> p.setCreateTime(LocalDateTime.now()));
productRepository.saveAll(products);
return Result.success("批量导入成功,共" + products.size() + "条");
}
/**
* 删除文档
*/
@DeleteMapping("/{id}")
public Result<String> deleteDocument(@PathVariable String id) {
productRepository.deleteById(id);
return Result.success("删除成功");
}
}
ES初始化工具类
@Component
@Slf4j
public class ElasticsearchInitializer implements CommandLineRunner {
@Autowired
private ElasticsearchRestTemplate elasticsearchRestTemplate;
@Override
public void run(String... args) throws Exception {
// 检查索引是否存在,不存在则创建
boolean indexExists = elasticsearchRestTemplate.indexOps(IndexCoordinates.of("products")).exists();
if (!indexExists) {
// 创建索引
elasticsearchRestTemplate.indexOps(Product.class).create();
// 设置IK分词器
String settings = "{\n" +
" \"analysis\": {\n" +
" \"analyzer\": {\n" +
" \"ik_smart_analyzer\": {\n" +
" \"type\": \"custom\",\n" +
" \"tokenizer\": \"ik_smart\"\n" +
" },\n" +
" \"ik_max_word_analyzer\": {\n" +
" \"type\": \"custom\",\n" +
" \"tokenizer\": \"ik_max_word\"\n" +
" }\n" +
" }\n" +
" }\n" +
"}";
log.info("Elasticsearch索引初始化完成");
}
}
}
分页查询优化
@Service
public class PagedSearchService {
@Autowired
private ElasticsearchRestTemplate elasticsearchRestTemplate;
/**
* 带分页的搜索
*/
public Page<Product> searchWithPage(String keyword, int page, int size) {
Pageable pageable = PageRequest.of(page, size);
NativeSearchQuery searchQuery = new NativeSearchQueryBuilder()
.withQuery(QueryBuilders.matchQuery("name", keyword))
.withPageable(pageable)
.build();
SearchHits<Product> searchHits = elasticsearchRestTemplate.search(searchQuery, Product.class);
List<Product> products = searchHits.stream()
.map(SearchHit::getContent)
.collect(Collectors.toList());
return new PageImpl<>(products, pageable, searchHits.getTotalHits());
}
/**
* 游标查询(深度分页)
*/
public List<Product> searchWithScroll(String keyword, int size) {
NativeSearchQuery searchQuery = new NativeSearchQueryBuilder()
.withQuery(QueryBuilders.matchQuery("name", keyword))
.withPageable(PageRequest.of(0, size))
.build();
// 设置游标超时时间
long scrollTimeInMillis = 60000;
ScrolledPage<Product> scroll = elasticsearchRestTemplate.searchScrollStart(
scrollTimeInMillis, searchQuery, Product.class);
String scrollId = scroll.getScrollId();
List<Product> allProducts = new ArrayList<>();
while (scroll.hasContent()) {
allProducts.addAll(scroll.getContent());
scroll = elasticsearchRestTemplate.searchScrollContinue(scrollId, scrollTimeInMillis, Product.class);
}
elasticsearchRestTemplate.searchScrollClear(scrollId);
return allProducts;
}
}
完整搜索功能测试
@SpringBootTest
@Slf4j
public class SearchServiceTest {
@Autowired
private SearchService searchService;
@Autowired
private ProductRepository productRepository;
@BeforeEach
public void setup() {
// 准备测试数据
List<Product> products = Arrays.asList(
new Product(null, "iPhone 15", "Apple最新款智能手机", "电子产品", 6999.0, 100, LocalDateTime.now(), true),
new Product(null, "华为Mate 60", "华为旗舰手机", "电子产品", 5999.0, 200, LocalDateTime.now(), true),
new Product(null, "MacBook Pro", "Apple笔记本电脑", "电子产品", 14999.0, 50, LocalDateTime.now(), true)
);
productRepository.saveAll(products);
}
@Test
public void testSearch() {
List<Product> results = searchService.searchByKeyword("手机");
assertThat(results).isNotEmpty();
log.info("搜索结果:{}", results);
}
@Test
public void testHighlightSearch() {
List<Map<String, Object>> results = searchService.searchWithHighlight("iPhone");
assertThat(results).isNotEmpty();
results.forEach(r -> log.info("高亮结果:{}", r));
}
@Test
public void testFilterSearch() {
List<Product> results = searchService.searchWithFilters("手机", "电子产品", 5000.0, 8000.0);
assertThat(results).isNotEmpty();
results.forEach(p -> log.info("过滤结果:{}", p));
}
}
性能优化建议
-
索引优化
- 合理设置分片数和副本数
- 使用合适的字段类型
- 禁用不需要的索引
-
查询优化
// 使用filter代替query(不计算相关性得分) BoolQueryBuilder boolQuery = QueryBuilders.boolQuery() .filter(QueryBuilders.termQuery("category", "电子产品")); // 使用keyword字段进行精确查询 QueryBuilders.termQuery("category.keyword", "电子产品"); -
批量操作
// 批量导入时使用bulk BulkRequest bulkRequest = new BulkRequest(); products.forEach(product -> { bulkRequest.add(new IndexRequest("products") .id(product.getId()) .source(product, XContentType.JSON)); });
这套方案提供了完整的Elasticsearch搜索实现,涵盖了基础搜索、高亮显示、过滤查询、聚合分析等常见功能。