目录

一、概述

本文介绍了在 SpringBoot 项目中集成 Elasticsearch 的完整流程。

  • Elasticsearch 是一个基于 Lucene 的分布式全文搜索引擎,通过 elasticsearch-rest-high-level-client 可以轻松实现与 SpringBoot 的整合,为应用提供强大的搜索、分析和数据可视化能力。本次集成将涵盖环境准备、依赖配置、索引管理、文档操作、高级查询等核心功能,确保系统具备高可用、高性能的搜索服务能力。

二、技术栈与版本要求

2.1 当前项目版本配置

组件版本说明
SpringBoot2.2.13.RELEASE基础框架,提供自动配置
Spring Data Elasticsearch3.2.12.RELEASESpring 官方数据访问模块
Elasticsearch6.3.2搜索引擎服务端
JDK1.8Java 运行环境
Maven3.6.3+项目构建工具

版本兼容性说明:Spring Data Elasticsearch 3.2.x 版本对应 Elasticsearch 6.x 版本。本项目使用Elasticsearch 6.3.2版本,确保API兼容性。

2.2 SpringBoot与Elasticsearch版本对应关系

SpringBoot版本Spring Data Elasticsearch版本Elasticsearch版本说明
1.5.x.RELEASE2.1.x2.4.x早期版本
2.0.x.RELEASE3.0.x5.6.xES 5.x系列
2.1.x.RELEASE3.1.x6.2.xES 6.x早期版本
2.2.x.RELEASE3.2.x6.3.x-6.8.x当前项目使用版本
2.3.x.RELEASE4.0.x7.6.xES 7.x早期版本
2.4.x.RELEASE4.1.x7.9.xES 7.x
2.5.x.RELEASE4.2.x7.12.xES 7.x
2.6.x.RELEASE4.3.x7.15.xES 7.x
2.7.x.RELEASE4.4.x7.17.xES 7.x最终版本

重要提示:由于本项目使用Elasticsearch 6.3.2,需要手动指定相关依赖版本以确保兼容性。Spring Boot 2.2.x默认管理的版本可能高于6.3.2。

2.3 版本兼容性问题解决方案

2.3.1 方案一:降低Spring Boot版本(推荐)

将Spring Boot版本从2.2.x降低到2.1.x,使其与Elasticsearch 6.3.2版本兼容:

<properties>
    <spring.boot.version>2.1.0.RELEASE</spring.boot.version>
</properties>

<dependencies>
    <!-- 只保留高级REST客户端依赖,让Spring Boot管理版本 -->
    <dependency>
        <groupId>org.elasticsearch.client</groupId>
        <artifactId>elasticsearch-rest-high-level-client</artifactId>
    </dependency>
</dependencies>

优势

  1. 利用Spring Boot的依赖管理,自动解决版本兼容性问题
  2. 代码更简洁,无需手动指定所有Elasticsearch依赖版本
  3. 减少依赖冲突的可能性
2.3.2 方案二:手动指定所有依赖版本

移除spring-boot-starter-data-elasticsearch依赖,并明确指定所有Elasticsearch相关依赖的版本:

<!-- 明确指定所有Elasticsearch依赖的版本 -->
<dependency>
    <groupId>org.elasticsearch</groupId>
    <artifactId>elasticsearch</artifactId>
    <version>6.3.2</version>
</dependency>
<dependency>
    <groupId>org.elasticsearch.client</groupId>
    <artifactId>elasticsearch-rest-client</artifactId>
    <version>6.3.2</version>
</dependency>
<dependency>
    <groupId>org.elasticsearch.client</groupId>
    <artifactId>elasticsearch-rest-high-level-client</artifactId>
    <version>6.3.2</version>
</dependency>

三、环境准备

3.1 安装 Elasticsearch 服务

推荐使用 Docker 快速部署 Elasticsearch 服务:

# 拉取 Elasticsearch 镜像
docker pull elasticsearch:6.8.13

# 启动 Elasticsearch 容器
docker run -d \
  --name elasticsearch \
  -p 9200:9200 \
  -p 9300:9300 \
  -e "discovery.type=single-node" \
  -e "ES_JAVA_OPTS=-Xms512m -Xmx512m" \
  elasticsearch:6.8.13

3.2 验证服务状态

启动后,通过以下命令验证 Elasticsearch 是否正常运行:

# 检查服务健康状态
curl -X GET "http://localhost:9200/_cluster/health?pretty"

# 查看节点信息
curl -X GET "http://localhost:9200/_cat/nodes?v"

如果 Elasticsearch 配置了账号密码,使用以下命令:

# 检查服务健康状态(带认证)
curl -X GET -u username:password "http://localhost:9200/_cluster/health?pretty"

# 查看节点信息(带认证)
curl -X GET -u username:password "http://localhost:9200/_cat/nodes?v"

# 或者使用 --user 参数
curl -X GET --user username:password "http://localhost:9200/_cluster/health?pretty"

预期返回 status: green 表示集群健康。


四、项目集成步骤

4.1 添加 Maven 依赖

pom.xml 中添加 Elasticsearch 依赖(移除Spring Data Elasticsearch依赖,手动实现所有功能):

<dependencies>
    <!-- Elasticsearch 高级 REST 客户端 -->
    <dependency>
        <groupId>org.elasticsearch.client</groupId>
        <artifactId>elasticsearch-rest-high-level-client</artifactId>
    </dependency>
    
    <!-- FastJSON -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>1.2.83</version>
    </dependency>
    
    <!-- Lombok -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.16</version>
        <scope>compile</scope>
        <optional>true</optional>
    </dependency>
</dependencies>

4.2 配置 application.yml

application.yml 中配置 Elasticsearch 连接信息:

项目实际配置(Spring Boot 2.2.x + Elasticsearch 6.3.2)

server:
  port: 8080

# Elasticsearch配置
elasticsearch:
  host: 111.228.59.250
  port: 9200
  username: elastic
  password: XXXX
  scheme: http

配置说明

  • elasticsearch.host:Elasticsearch服务器地址
  • elasticsearch.port:Elasticsearch服务器端口(HTTP端口9200)
  • elasticsearch.username/password:Elasticsearch认证信息
  • elasticsearch.scheme:连接协议(http或https)

4.3 配置类说明

由于移除了Spring Data Elasticsearch,需要创建配置类手动配置RestHighLevelClient:

package com.demo.config;

import org.apache.http.HttpHost;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestClientBuilder;
import org.elasticsearch.client.RestHighLevelClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ElasticsearchConfig {

    @Value("${elasticsearch.host}")
    private String host;

    @Value("${elasticsearch.port}")
    private int port;

    @Value("${elasticsearch.username}")
    private String username;

    @Value("${elasticsearch.password}")
    private String password;

    @Value("${elasticsearch.scheme}")
    private String scheme;

    @Bean
    public RestHighLevelClient client() {
        // 创建认证提供者
        CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
        credentialsProvider.setCredentials(AuthScope.ANY,
                new UsernamePasswordCredentials(username, password));

        // 创建REST客户端构建器
        RestClientBuilder builder = RestClient.builder(
                        new HttpHost(host, port, scheme))
                .setHttpClientConfigCallback(httpClientBuilder ->
                        httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider));

        return new RestHighLevelClient(builder);
    }
}

4.4 Elasticsearch 索引 DSL

在创建实体类之前,我们先了解本次实战中涉及的 Elasticsearch 索引 DSL。这些 DSL 语句用于手动创建索引和定义映射。

创建 products 索引的 DSL

PUT /products
{
  "settings": {
    "number_of_shards": 1,
    "number_of_replicas": 1,
    "analysis": {
      "analyzer": {
        "ik_max_word": {
          "type": "custom",
          "tokenizer": "ik_max_word"
        },
        "ik_smart": {
          "type": "custom",
          "tokenizer": "ik_smart"
        }
      }
    }
  },
  "mappings": {
    "product": {
      "properties": {
        "id": {
          "type": "keyword"
        },
        "title": {
          "type": "text",
          "analyzer": "ik_max_word",
          "search_analyzer": "ik_smart"
        },
        "category": {
          "type": "keyword"
        },
        "price": {
          "type": "double"
        },
        "createTime": {
          "type": "date",
          "format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis"
        }
      }
    }
  }
}

常用查询 DSL 示例

// 1. 根据ID查询
GET /products/product/{id}

// 2. 根据标题模糊查询
GET /products/product/_search
{
  "query": {
    "match": {
      "title": "搜索关键词"
    }
  }
}

// 3. 根据分类和价格范围查询
GET /products/product/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "term": {
            "category": "电子产品"
          }
        },
        {
          "range": {
            "price": {
              "gte": 100,
              "lte": 1000
            }
          }
        }
      ]
    }
  }
}

// 4. 聚合查询 - 按分类统计商品数量
GET /products/product/_search
{
  "size": 0,
  "aggs": {
    "categories": {
      "terms": {
        "field": "category"
      }
    }
  }
}

注意:上述 DSL 中的 ik_max_wordik_smart 是 IK 分词器的两种模式,需要提前安装 IK 分词器插件。

4.5 IK 分词器插件安装与配置

如果在执行索引创建时遇到以下错误:

{
  "error": {
    "root_cause": [
      {
        "type": "illegal_argument_exception",
        "reason": "Custom Analyzer [ik_max_word] failed to find tokenizer under name [ik_max_word]"
      }
    ],
    "type": "illegal_argument_exception",
    "reason": "Custom Analyzer [ik_max_word] failed to find tokenizer under name [ik_max_word]"
  },
  "status": 400
}

这是因为 Elasticsearch 没有找到 IK 分词器插件。以下是解决方案:

4.5.1 方案一:安装 IK 分词器插件

对于 Elasticsearch 6.3.2 版本,需要安装对应版本的 IK 分词器插件:

# 进入 Elasticsearch 容器
docker exec -it elasticsearch /bin/bash

# 进入 Elasticsearch 安装目录(通常是 /usr/share/elasticsearch)
cd /usr/share/elasticsearch

# 安装 IK 分词器插件
./bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v6.3.2/elasticsearch-analysis-ik-6.3.2.zip

# 退出容器并重启 Elasticsearch
exit
docker restart elasticsearch

插件默认安装位置
IK 分词器插件默认安装在 Elasticsearch 的 plugins 目录下,具体路径为:

/usr/share/elasticsearch/plugins/analysis-ik/

在这个目录下,你可以找到插件的配置文件和词典文件:

  • config/ 目录:包含 IK 分词器的配置文件
  • config/main.dic:主词典文件
  • config/stopword.dic:停用词词典
  • config/quantifier.dic:量词词典

如果需要自定义词典,可以在 config 目录下添加自己的词典文件,格式为每行一个词。

安装完成后,可以通过以下命令验证插件是否安装成功:

# 查看已安装的插件
curl -X GET "localhost:9200/_cat/plugins?v"

# 测试 IK 分词器
curl -X POST "localhost:9200/_analyze" -H 'Content-Type: application/json' -d'
{
  "analyzer": "ik_max_word",
  "text": "中华人民共和国"
}'
4.5.2 Analyzer 与 Tokenizer 的关系

在 Elasticsearch 中:

  • Analyzer(分析器):是一个完整的文本处理管道,包含字符过滤器(character filters)、分词器(tokenizer)和词元过滤器(token filters)
  • Tokenizer(分词器):是分析器的一部分,负责将文本分割成词元(tokens)

在索引 DSL 中的配置:

"analyzer": {
  "ik_max_word": {
    "type": "custom",
    "tokenizer": "ik_max_word"
  },
  "ik_smart": {
    "type": "custom",
    "tokenizer": "ik_smart"
  }
}

这是在定义两个自定义分析器,它们都使用了 IK 分词器作为 tokenizer。

4.5.3 IK 分词器的种类

IK 分词器是一个第三方中文分词插件,它提供了两种分词模式:

  1. ik_max_word:最细粒度分词

    • 会将文本做最细粒度的拆分
    • 例如:"中华人民共和国国歌"会被分词为:“中华人民共和国,中华人民,中华,华人,人民共和国,人民,人,民,共和国,共和,和,国国,国歌”
  2. ik_smart:智能分词

    • 会做最粗粒度的拆分
    • 例如:"中华人民共和国国歌"会被分词为:“中华人民共和国,国歌”
4.5.4 使用建议
  • 索引时:通常使用 ik_max_word,确保尽可能多的词被索引,提高召回率
  • 搜索时:通常使用 ik_smart,确保搜索词尽可能准确,提高准确率

这就是为什么在实体类注解中你会看到:

@Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_smart")
4.5.5 方案二:使用标准分词器(不安装插件)

如果不想安装 IK 分词器插件,可以使用 Elasticsearch 内置的标准分词器:

PUT /products
{
  "settings": {
    "number_of_shards": 1,
    "number_of_replicas": 1
  },
  "mappings": {
    "product": {
      "properties": {
        "id": {
          "type": "keyword"
        },
        "name": {
          "type": "text",
          "analyzer": "standard"
        },
        "category": {
          "type": "keyword"
        },
        "price": {
          "type": "double"
        },
        "createTime": {
          "type": "date",
          "format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis"
        }
      }
    }
  }
}

如果使用标准分词器,需要相应修改实体类中的注解:

@Field(type = FieldType.Text, analyzer = "standard")
private String title;

4.6 实体类中分词器使用方式说明

Product 实体类中,name 字段使用了特殊的注解:

@Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_smart")
private String title;

参数说明

  1. type = FieldType.Text

    • 表示该字段是可分词的文本类型
    • 与 Elasticsearch 中的 text 类型对应
  2. analyzer = "ik_max_word"

    • 指定索引时使用的分析器
    • ik_max_word 是 IK 分词器的最细粒度分词模式
    • 在文档被索引时,会使用此分析器处理字段内容
    • 确保尽可能多的词被索引,提高召回率
  3. searchAnalyzer = "ik_smart"

    • 指定搜索时使用的分析器
    • ik_smart 是 IK 分词器的智能分词模式
    • 在执行搜索查询时,会使用此分析器处理搜索关键词
    • 确保搜索词尽可能准确,提高准确率

4.7 创建实体类

定义与 Elasticsearch 索引映射的实体类 Product.java

package com.demo.entity;

import com.fasterxml.jackson.annotation.JsonFormat;

import java.util.Date;

public class Product {
    
    private String id;
    private String title;
    private String category;
    private Double price;

    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private Date createTime;
    
    // 构造函数
    public Product() {}
    
    // Getter和Setter方法
    public String getId() {
        return id;
    }
    
    public void setId(String id) {
        this.id = id;
    }
    
    public String getTitle() {
        return title;
    }
    
    public void setTitle(String title) {
        this.title = title;
    }
    
    public String getCategory() {
        return category;
    }
    
    public void setCategory(String category) {
        this.category = category;
    }
    
    public Double getPrice() {
        return price;
    }
    
    public void setPrice(Double price) {
        this.price = price;
    }
    
    public Date getCreateTime() {
        return createTime;
    }
    
    public void setCreateTime(Date createTime) {
        this.createTime = createTime;
    }
}

字段说明

  • 移除了所有Spring Data Elasticsearch注解,使用纯POJO
  • @JsonFormat:用于处理JSON日期格式解析,解决"yyyy-MM-dd HH:mm:ss"格式问题
  • 提供了完整的getter/setter方法,便于JSON序列化和反序列化

五、核心功能实现

5.1 创建 Repository 类

创建 ProductRepository 类,手动实现所有功能:

package com.demo.repository;

import com.demo.entity.Product;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.http.Header;
import org.apache.http.message.BasicHeader;
import org.elasticsearch.action.delete.DeleteRequest;
import org.elasticsearch.action.delete.DeleteResponse;
import org.elasticsearch.action.get.GetRequest;
import org.elasticsearch.action.get.GetResponse;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.index.IndexResponse;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

@Repository
public class ProductRepository {

    @Autowired
    private RestHighLevelClient client;

    private final ObjectMapper objectMapper = new ObjectMapper();
    private static final String INDEX_NAME = "products";
    private static final String TYPE_NAME = "product";
    
    // 创建默认的Header数组
    private static final Header[] DEFAULT_HEADERS = new Header[]{
            new BasicHeader("Content-Type", "application/json")
    };

    // 保存商品
    public Product save(Product product) throws IOException {
        IndexRequest request;
        if (product.getId() != null) {
            request = new IndexRequest(INDEX_NAME, TYPE_NAME, product.getId())
                    .source(objectMapper.writeValueAsString(product), XContentType.JSON);
        } else {
            request = new IndexRequest(INDEX_NAME, TYPE_NAME)  // 不指定ID,让ES自动生成
                    .source(objectMapper.writeValueAsString(product), XContentType.JSON);
        }
        
        IndexResponse response = client.index(request, DEFAULT_HEADERS);
        
        // 如果ID是自动生成的,将其设置到product对象中
        if (product.getId() == null) {
            product.setId(response.getId());
        }
        
        return product;
    }

    // 根据ID查询
    public Product findById(String id) throws IOException {
        GetRequest request = new GetRequest(INDEX_NAME, TYPE_NAME, id);
        GetResponse response = client.get(request, DEFAULT_HEADERS);
        
        if (response.isExists()) {
            Map<String, Object> source = response.getSourceAsMap();
            Product product = objectMapper.convertValue(source, Product.class);
            // 设置文档ID
            product.setId(id);
            return product;
        }
        return null;
    }

    // 查询所有商品
    public List<Product> findAll() throws IOException {
        SearchRequest searchRequest = new SearchRequest(INDEX_NAME);
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        sourceBuilder.query(QueryBuilders.matchAllQuery());
        searchRequest.source(sourceBuilder);
        
        return executeSearch(searchRequest);
    }

    // 根据商品名称模糊查询
    public List<Product> findByTitleContaining(String title) throws IOException {
        SearchRequest searchRequest = new SearchRequest(INDEX_NAME);
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        sourceBuilder.query(QueryBuilders.matchQuery("title", title));
        searchRequest.source(sourceBuilder);
        
        return executeSearch(searchRequest);
    }

    // 根据分类和价格范围查询
    public List<Product> findByCategoryAndPriceBetween(String category, Double minPrice, Double maxPrice) throws IOException {
        SearchRequest searchRequest = new SearchRequest(INDEX_NAME);
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        
        BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
        boolQuery.filter(QueryBuilders.termQuery("category", category));
        boolQuery.filter(QueryBuilders.rangeQuery("price").gte(minPrice).lte(maxPrice));
        
        sourceBuilder.query(boolQuery);
        searchRequest.source(sourceBuilder);
        
        return executeSearch(searchRequest);
    }

    // 删除商品
    public void deleteById(String id) throws IOException {
        DeleteRequest request = new DeleteRequest(INDEX_NAME, TYPE_NAME, id);
        DeleteResponse response = client.delete(request, DEFAULT_HEADERS);
    }

    // 执行搜索并返回结果
    private List<Product> executeSearch(SearchRequest searchRequest) throws IOException {
        SearchResponse response = client.search(searchRequest, DEFAULT_HEADERS);
        SearchHit[] searchHits = response.getHits().getHits();
        
        List<Product> products = new ArrayList<>();
        for (SearchHit hit : searchHits) {
            Map<String, Object> source = hit.getSourceAsMap();
            Product product = objectMapper.convertValue(source, Product.class);
            // 设置文档ID
            product.setId(hit.getId());
            products.add(product);
        }
        
        return products;
    }
}

关键改进点

  1. 添加Header数组:为所有Elasticsearch API调用提供默认的Header数组,解决版本兼容性问题
  2. 统一API调用方式:所有操作都使用带有Header参数的API调用方式
  3. 异常处理:所有方法都声明抛出IOException,由上层服务处理
  4. 手动实现:完全手动实现所有CRUD操作,不依赖Spring Data Elasticsearch
  5. ID处理:正确处理Elasticsearch自动生成的ID,确保返回的数据包含有效的ID字段

5.2 服务层实现

创建服务层接口和实现类,遵循面向接口编程原则。

5.2.1 创建服务接口

创建 ProductService 接口定义业务方法:

package com.demo.service;

import com.demo.entity.Product;
import java.util.List;

public interface ProductService {
    
    // 保存单个商品
    Product saveProduct(Product product);
    
    // 批量保存商品
    Iterable<Product> saveProducts(List<Product> products);
    
    // 根据ID查询
    Product findById(String id);
    
    // 查询所有商品
    Iterable<Product> findAll();
    
    // 根据标题模糊搜索
    List<Product> searchByTitle(String title);
    
    // 根据分类和价格范围搜索
    List<Product> searchByCategoryAndPrice(String category, Double minPrice, Double maxPrice);
    
    // 删除商品
    void deleteProduct(String id);
}
5.2.2 创建服务实现类

创建 ProductServiceImpl 实现类,处理IOException异常:

package com.demo.service.impl;

import com.demo.entity.Product;
import com.demo.repository.ProductRepository;
import com.demo.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.util.List;

@Service
public class ProductServiceImpl implements ProductService {

    @Autowired
    private ProductRepository productRepository;

    @Override
    public Product saveProduct(Product product) {
        try {
            return productRepository.save(product);
        } catch (IOException e) {
            throw new RuntimeException("保存商品失败", e);
        }
    }

    @Override
    public Iterable<Product> saveProducts(List<Product> products) {
        for (Product product : products) {
            saveProduct(product);
        }
        return products;
    }

    @Override
    public Product findById(String id) {
        try {
            return productRepository.findById(id);
        } catch (IOException e) {
            throw new RuntimeException("查询商品失败", e);
        }
    }

    @Override
    public Iterable<Product> findAll() {
        try {
            return productRepository.findAll();
        } catch (IOException e) {
            throw new RuntimeException("查询所有商品失败", e);
        }
    }

    @Override
    public List<Product> searchByTitle(String title) {
        try {
            return productRepository.findByTitleContaining(title);
        } catch (IOException e) {
            throw new RuntimeException("按标题搜索商品失败", e);
        }
    }

    @Override
    public List<Product> searchByCategoryAndPrice(String category, Double minPrice, Double maxPrice) {
        try {
            return productRepository.findByCategoryAndPriceBetween(category, minPrice, maxPrice);
        } catch (IOException e) {
            throw new RuntimeException("按分类和价格搜索商品失败", e);
        }
    }

    @Override
    public void deleteProduct(String id) {
        try {
            productRepository.deleteById(id);
        } catch (IOException e) {
            throw new RuntimeException("删除商品失败", e);
        }
    }
}

5.3 控制层接口

创建 ProductController 提供 REST API:

package com.demo.controller;

import com.demo.entity.Product;
import com.demo.service.ProductService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/products")
public class ProductController {

    private static final Logger logger = LoggerFactory.getLogger(ProductController.class);

    @Autowired
    private ProductService productService;

    /**
     * 创建商品
     * POST /api/products/create
     */
    @PostMapping("/create")
    public Product create(@RequestBody Product product) {
        logger.info("创建商品请求,商品信息: {}", product);
        Product savedProduct = productService.saveProduct(product);
        logger.info("商品创建成功,ID: {}", savedProduct.getId());
        return savedProduct;
    }

    /**
     * 根据ID获取商品
     * GET /api/products/get/{id}
     */
    @GetMapping("/get/{id}")
    public Product getById(@PathVariable String id) {
        logger.info("根据ID获取商品请求,ID: {}", id);
        Product product = productService.findById(id);
        if (product != null) {
            logger.info("成功获取商品,ID: {}", id);
        } else {
            logger.warn("未找到商品,ID: {}", id);
        }
        return product;
    }

    /**
     * 获取所有商品
     * GET /api/products/list
     */
    @GetMapping("/list")
    public List<Product> getAll() {
        logger.info("获取所有商品列表请求");
        List<Product> products = (List<Product>) productService.findAll();
        logger.info("成功获取商品列表,共 {} 条记录", products.size());
        return products;
    }

    /**
     * 搜索商品
     * GET /api/products/search
     */
    @GetMapping("/search")
    public List<Product> search(
            @RequestParam(required = false) String name,
            @RequestParam(required = false) String category,
            @RequestParam(required = false) Double minPrice,
            @RequestParam(required = false) Double maxPrice) {

        logger.info("搜索商品请求,参数: title={}, category={}, minPrice={}, maxPrice={}",
                   title, category, minPrice, maxPrice);

        List<Product> result;
        if (name != null && !name.isEmpty()) {
            result = productService.searchByName(name);
            logger.info("按标题搜索商品,关键词: {},结果数量: {}", title, result.size());
        } else if (category != null && minPrice != null && maxPrice != null) {
            result = productService.searchByCategoryAndPrice(category, minPrice, maxPrice);
            logger.info("按分类和价格范围搜索商品,分类: {},价格范围: {}-{},结果数量: {}",
                       category, minPrice, maxPrice, result.size());
        } else {
            result = (List<Product>) productService.findAll();
            logger.info("获取所有商品,结果数量: {}", result.size());
        }
        return result;
    }

    /**
     * 删除商品
     * DELETE /api/products/delete/{id}
     */
    @DeleteMapping("/delete/{id}")
    public void delete(@PathVariable String id) {
        logger.info("删除商品请求,ID: {}", id);
        productService.deleteProduct(id);
        logger.info("商品删除成功,ID: {}", id);
    }
}

控制层特点

  1. 完整的日志记录:每个API调用都有详细的日志记录,便于问题排查
  2. 清晰的接口路径:使用语义化的URL路径(/create, /get/{id}, /list, /search, /delete/{id})
  3. 参数验证:对搜索参数进行合理的条件判断
  4. 响应处理:统一处理各种查询场景的响应

六、REST API 接口文档

注意:部分电脑(如:Mac),curl 命令中包含中文会转换失败,可以使用 --data-urlencode 传参,如下所示:

curl -X GET "http://localhost:8080/api/products/complex-search" \
  --data-urlencode "keyword=华为" 

6.1 商品管理接口

6.1.1 创建商品
  • 接口: POST /api/products/create

  • 描述: 创建新商品

  • curl命令:

    curl -X POST http://localhost:8080/api/products/create \
      -H "Content-Type: application/json" \
      -d '{
        "title": "iPhone 13",
        "category": "手机",
        "price": 5999.00,
        "createTime": "2023-12-01 10:00:00"
      }'
    
  • 执行结果
6.1.2 获取商品详情
  • 接口: GET /api/products/get/{id}

  • 描述: 根据ID获取商品详情

  • curl命令:

    curl -X GET http://localhost:8080/api/products/get/EMpzZ50Bfj08Xy4w0qGD
    
  • 执行结果
6.1.3 获取商品列表
  • 接口: GET /api/products/list

  • 描述: 获取所有商品列表

  • curl命令:

    curl -X GET http://localhost:8080/api/products/list
    
  • 执行结果
6.1.4 搜索商品
  • 接口: GET /api/products/search

  • 描述: 按条件搜索商品

  • curl命令:

    # 按标题搜索
    curl -X GET "http://localhost:8080/api/products/search?title=iPhone"
    
    # 按分类和价格范围搜索
    curl -X GET "http://localhost:8080/api/products/search?category=手机&minPrice=1000&maxPrice=8000"
    
    # 获取所有商品
    curl -X GET http://localhost:8080/api/products/search
    
  • 执行结果

在这里插入图片描述

6.1.5 删除商品
  • 接口: DELETE /api/products/delete/{id}

  • 描述: 删除指定商品

  • curl命令:

    curl -X DELETE http://localhost:8080/api/products/delete/EMpzZ50Bfj08Xy4w0qGD
    
  • 执行结果

在这里插入图片描述

6.1.6 复杂查询
  • 接口: GET /api/products/complex-search

  • 描述: 复杂条件搜索商品

  • curl命令:

    # 按关键词和分类搜索
    curl -X GET "http://localhost:8080/api/products/complex-search?keyword=iPhone&category=手机&minScore=1.0"
    
    # 仅按关键词搜索
    curl -X GET "http://localhost:8080/api/products/complex-search?keyword=华为"
    
    # 获取所有商品(按价格排序)
    curl -X GET http://localhost:8080/api/products/complex-search
    
    
    curl -X GET "http://localhost:8080/api/products/complex-search" \
      --data-urlencode "keyword=华为" 
    
  • 执行结果

在这里插入图片描述

6.1.7 价格分布统计
  • 接口: GET /api/products/stats/price-distribution

  • 描述: 获取商品价格分布统计

  • curl命令:

    curl -X GET http://localhost:8080/api/products/stats/price-distribution
    
  • 执行结果

在这里插入图片描述

6.1.8 分类统计
  • 接口: GET /api/products/stats/category

  • 描述: 获取商品分类统计

  • curl命令:

    curl -X GET http://localhost:8080/api/products/stats/category
    
  • 执行结果

在这里插入图片描述


七、高级查询与聚合实现

高级查询和聚合功能已在ProductService中实现,并通过Controller暴露为REST API接口。这些功能包括:

7.1 复杂查询实现

复杂查询功能通过complexSearch方法实现,支持以下特性:

  • 关键词搜索(支持模糊匹配)
  • 分类筛选
  • 最小评分过滤
  • 结果按价格升序排序

7.2 聚合分析实现

聚合分析功能通过以下方法实现:

  • 价格分布统计:将商品价格分为四个区间(0-100, 100-500, 500-1000, 1000+)并统计每个区间的商品数量
  • 分类统计:统计每个分类下的商品数量

这些功能通过Elasticsearch的聚合API实现,提供了强大的数据分析能力。


八、性能优化与最佳实践

8.1 索引优化

  • 合理设置分片和副本:根据数据量和查询负载设置 number_of_shardsnumber_of_replicas

  • 使用合适的分析器:中文场景推荐 ik 分词器,避免使用默认的 standard 分词器。

  • 禁用不必要的字段:对不需要搜索的字段设置 "index": false

8.2 查询优化

  • 避免深度分页:使用 search_after 替代 from/size 进行深度分页。

  • 使用过滤器上下文:对于不参与评分的条件(如状态、分类),使用 filter 而非 must,利用缓存提升性能。

  • 合理使用批量操作:使用 bulk API 进行批量索引和删除,减少网络开销。

8.3 连接池与超时

application.yml 中配置连接池参数:

spring:
  data:
    elasticsearch:
      properties:
        http:
          max-content-length: 10MB
        transport:
          tcp:
            keep-alive: true
            connect-timeout: 30s

九、常见问题与解决方案

9.1 版本冲突导致启动失败

问题java.lang.NoSuchMethodError: 'boolean org.elasticsearch.client.IndicesClient.exists(...)'

原因:Spring Data Elasticsearch 3.2.x默认管理的版本可能与ES 6.3.2不兼容,导致API方法签名不匹配。

解决方案

<!-- 对于ES 6.3.2,需要明确指定版本以确保兼容性 -->
<dependency>
    <groupId>org.elasticsearch.client</groupId>
    <artifactId>elasticsearch-rest-high-level-client</artifactId>
    <version>6.3.2</version>
</dependency>

9.2 连接拒绝 (Connection refused)

问题java.net.ConnectException: Connection refused: no further information

原因:Elasticsearch 服务未启动或端口未开放。

解决方案

  1. 检查 Elasticsearch 容器或服务是否正常运行。

  2. 使用 docker psnetstat -an | grep 9200 确认端口监听状态。

  3. 检查防火墙设置。

9.3 版本不兼容

问题java.lang.NoSuchMethodErrorNoNodeAvailableException

原因:Spring Data Elasticsearch 客户端版本与 Elasticsearch 服务端版本不匹配。

解决方案

  1. 确认版本兼容性矩阵。

  2. 升级或降级客户端/服务端版本至兼容版本。

9.4 中文分词效果差

问题:中文搜索无法分词或匹配不准确。

解决方案

  1. 安装 elasticsearch-analysis-ik 插件。

  2. 在字段映射中指定 ik_max_wordik_smart 分析器。

9.5 Elasticsearch版本兼容性问题

问题描述
在使用Spring Boot 2.2.x与Elasticsearch 6.3.2集成时,遇到了以下错误:

Elasticsearch exception [type=illegal_argument_exception, reason=request [/products/_refresh] contains unrecognized parameter: [ignore_throttled]]

以及后续的版本冲突错误:

java.lang.NoSuchMethodError: 'void org.elasticsearch.client.Request.<init>(java.lang.String, java.lang.String)'

错误分析

  1. Spring Data Elasticsearch 3.2.x与Elasticsearch 6.3.2版本不兼容
  2. Spring Data Elasticsearch在刷新索引时使用了Elasticsearch 6.3.2不支持的ignore_throttled参数
  3. Spring Boot的依赖管理引入了不兼容的elasticsearch-rest-client版本
  4. 存在版本冲突:不同版本的Elasticsearch客户端API不兼容

解决方案
通过两种方案解决了版本兼容性问题:

9.5.1 方案一:降低Spring Boot版本(推荐)

将Spring Boot版本从2.2.x降低到2.1.x,使其与Elasticsearch 6.3.2版本兼容:

<properties>
    <spring.boot.version>2.1.0.RELEASE</spring.boot.version>
</properties>

<dependencies>
    <!-- 只保留高级REST客户端依赖,让Spring Boot管理版本 -->
    <dependency>
        <groupId>org.elasticsearch.client</groupId>
        <artifactId>elasticsearch-rest-high-level-client</artifactId>
    </dependency>
</dependencies>

优势

  1. 利用Spring Boot的依赖管理,自动解决版本兼容性问题
  2. 代码更简洁,无需手动指定所有Elasticsearch依赖版本
  3. 减少依赖冲突的可能性
9.5.2 方案二:手动指定所有依赖版本

移除spring-boot-starter-data-elasticsearch依赖,并明确指定所有Elasticsearch相关依赖的版本:

<!-- 删除Spring Data Elasticsearch依赖 -->
<!--
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
-->

<!-- 明确指定所有Elasticsearch依赖的版本 -->
<dependency>
    <groupId>org.elasticsearch</groupId>
    <artifactId>elasticsearch</artifactId>
    <version>6.3.2</version>
</dependency>
<dependency>
    <groupId>org.elasticsearch.client</groupId>
    <artifactId>elasticsearch-rest-client</artifactId>
    <version>6.3.2</version>
</dependency>
<dependency>
    <groupId>org.elasticsearch.client</groupId>
    <artifactId>elasticsearch-rest-high-level-client</artifactId>
    <version>6.3.2</version>
</dependency>

关键改进

  1. 重构ElasticsearchConfig:不再使用Spring Data Elasticsearch的自动配置,手动配置RestHighLevelClient
  2. 重写ProductRepository:不再继承ElasticsearchRepository接口,使用RestHighLevelClient手动实现所有CRUD操作
  3. Header数组参数:所有API调用都使用Header数组参数,解决Request构造函数签名问题
// 创建默认的Header数组
private static final Header[] DEFAULT_HEADERS = new Header[]{
    new BasicHeader("Content-Type", "application/json")
};

// 所有API调用都使用Header参数
IndexResponse response = client.index(request, DEFAULT_HEADERS);
GetResponse response = client.get(request, DEFAULT_HEADERS);
DeleteResponse response = client.delete(request, DEFAULT_HEADERS);
SearchResponse response = client.search(searchRequest, DEFAULT_HEADERS);

关键注意事项

  1. 必须统一所有Elasticsearch相关依赖的版本
  2. Elasticsearch 6.3.2版本的API与较新版本有差异
  3. 手动实现虽然增加了代码量,但提供了更好的版本控制和错误处理能力
  4. Header数组参数是解决Request构造函数签名不匹配的关键

重构后的优势

  1. 完全避免了版本兼容性问题
  2. 对Elasticsearch操作有更直接的控制
  3. 代码更加清晰,易于理解和维护
  4. 可以精确控制每个操作的参数和行为

整理完毕,完结撒花~🌻

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐