SpringBoot集成Elasticsearch搜索

wen java案例 1

本文目录导读:

SpringBoot集成Elasticsearch搜索

  1. 环境配置
  2. 基础配置类
  3. 文档实体类
  4. Repository层
  5. 搜索服务类
  6. Controller层
  7. ES初始化工具类
  8. 分页查询优化
  9. 完整搜索功能测试
  10. 性能优化建议

我来详细介绍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));
    }
}

性能优化建议

  1. 索引优化

    • 合理设置分片数和副本数
    • 使用合适的字段类型
    • 禁用不需要的索引
  2. 查询优化

    // 使用filter代替query(不计算相关性得分)
    BoolQueryBuilder boolQuery = QueryBuilders.boolQuery()
        .filter(QueryBuilders.termQuery("category", "电子产品"));
    // 使用keyword字段进行精确查询
    QueryBuilders.termQuery("category.keyword", "电子产品");
  3. 批量操作

    // 批量导入时使用bulk
    BulkRequest bulkRequest = new BulkRequest();
    products.forEach(product -> {
        bulkRequest.add(new IndexRequest("products")
            .id(product.getId())
            .source(product, XContentType.JSON));
    });

这套方案提供了完整的Elasticsearch搜索实现,涵盖了基础搜索、高亮显示、过滤查询、聚合分析等常见功能。

抱歉,评论功能暂时关闭!