SpringBoot + Elasticsearch 配置详解

105 阅读5分钟

SpringBoot + Elasticsearch 配置详解

本文将详细介绍SpringBoot与Elasticsearch的整合配置,包括版本对应关系、依赖引入、连接配置、客户端设置、实体映射、Repository接口使用等内容,帮助开发者快速实现Elasticsearch搜索引擎功能。

一、版本对应关系

SpringBoot与Elasticsearch版本对应非常重要,不匹配的版本可能导致兼容性问题:

SpringBoot版本Elasticsearch版本
2.6.x7.16.x
2.5.x7.12.x
2.4.x7.9.x
2.3.x7.6.x

二、依赖配置

pom.xml中添加Spring Data 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>7.16.2</version> <!-- 与SpringBoot版本匹配 -->
</dependency>
<dependency>
    <groupId>org.elasticsearch.client</groupId>
    <artifactId>elasticsearch-rest-high-level-client</artifactId>
    <version>7.16.2</version>
</dependency> -->

三、基本配置 (application.yml)

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

1. Spring Boot 2.6.x+ 配置方式

spring:
  elasticsearch:
    # Elasticsearch 服务地址(多个地址用逗号分隔)
    uris: http://localhost:9200,http://localhost:9201
    # 连接超时时间
    connection-timeout: 10s
    # 数据读取超时时间
    socket-timeout: 30s
    # 用户名(如果启用了安全认证)
    username: elastic
    # 密码(如果启用了安全认证)
    password: 123456
    # 是否启用SSL
    ssl:
      enabled: false

2. 旧版本配置方式

spring:
  data:
    elasticsearch:
      cluster-name: elasticsearch  # 集群名称
      cluster-nodes: localhost:9300  # 集群节点
      repositories:
        enabled: true  # 启用仓库

四、RestHighLevelClient 配置类

创建自定义配置类,配置Elasticsearch客户端:

import org.apache.http.HttpHost;
import org.elasticsearch.client.RestClient;
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("${spring.elasticsearch.uris:http://localhost:9200}")
    private String uris;
    
    @Bean
    public RestHighLevelClient restHighLevelClient() {
        // 解析uris字符串,支持多个地址
        String[] hosts = uris.split(",");
        HttpHost[] httpHosts = new HttpHost[hosts.length];
        
        for (int i = 0; i < hosts.length; i++) {
            String host = hosts[i].trim();
            // 提取主机和端口
            String hostWithoutProtocol = host.replace("http://", "").replace("https://", "");
            String[] parts = hostWithoutProtocol.split(":");
            String hostname = parts[0];
            int port = parts.length > 1 ? Integer.parseInt(parts[1]) : 9200;
            String scheme = host.startsWith("https") ? "https" : "http";
            
            httpHosts[i] = new HttpHost(hostname, port, scheme);
        }
        
        return new RestHighLevelClient(
                RestClient.builder(httpHosts)
                        // 可以在这里添加认证信息、连接池配置等
                        // .setHttpClientConfigCallback(httpClientBuilder -> 
                        //     httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider))
        );
    }
}

使用 AbstractElasticsearchConfiguration

另一种配置方式,使用Spring Data Elasticsearch提供的抽象类:

import org.springframework.context.annotation.Configuration;
import org.springframework.data.elasticsearch.client.ClientConfiguration;
import org.springframework.data.elasticsearch.client.RestClients;
import org.springframework.data.elasticsearch.config.AbstractElasticsearchConfiguration;

import java.time.Duration;

@Configuration
public class RestClientConfig extends AbstractElasticsearchConfiguration {
    
    @Override
    public RestHighLevelClient elasticsearchClient() {
        ClientConfiguration clientConfiguration = ClientConfiguration.builder()
                .connectedTo("localhost:9200")
                .withConnectTimeout(Duration.ofSeconds(10))
                .withSocketTimeout(Duration.ofSeconds(30))
                // 如果需要认证
                // .withBasicAuth("elastic", "123456")
                .build();
        
        return RestClients.create(clientConfiguration).rest();
    }
}

五、实体类映射

使用Spring Data Elasticsearch注解定义实体类与索引的映射关系:

import lombok.Data;
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.util.Date;
import java.util.List;

@Data
@Document(indexName = "users", shards = 3, replicas = 1)  // 索引名称、分片数、副本数
public class User {
    
    @Id  // 文档ID
    private String id;
    
    @Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_smart")  // 全文本字段,使用中文分词器
    private String username;
    
    @Field(type = FieldType.Keyword)  // 关键字字段,不分词
    private String email;
    
    @Field(type = FieldType.Integer)
    private Integer age;
    
    @Field(type = FieldType.Date, format = DateFormat.date_time)  // 日期字段
    private Date createdAt;
    
    @Field(type = FieldType.Nested)  // 嵌套对象字段
    private List<Order> orders;
    
    @Data
    public static class Order {
        private String orderId;
        private Double amount;
        private Date orderDate;
    }
}

六、Repository接口

Spring Data Elasticsearch支持Repository模式,简化数据访问层开发:

import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import org.springframework.stereotype.Repository;

import java.util.List;
import java.util.Optional;

@Repository
public interface UserRepository extends ElasticsearchRepository<User, String> {
    
    // 基于方法名自动生成查询
    Optional<User> findByUsername(String username);
    
    List<User> findByAgeBetween(Integer minAge, Integer maxAge);
    
    List<User> findByUsernameContaining(String keyword);
    
    // 分页查询
    org.springframework.data.domain.Page<User> findByAgeGreaterThanEqual(Integer age, 
                                          org.springframework.data.domain.Pageable pageable);
}

七、使用RestHighLevelClient操作

对于复杂查询,使用RestHighLevelClient:

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.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
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.Service;

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

@Service
public class ElasticsearchService {
    
    @Autowired
    private RestHighLevelClient restHighLevelClient;
    
    @Autowired
    private UserRepository userRepository;
    
    // 创建或更新文档
    public String saveDocument(String index, String id, Map<String, Object> document) throws IOException {
        IndexRequest request = new IndexRequest(index).id(id).source(document);
        IndexResponse response = restHighLevelClient.index(request, RequestOptions.DEFAULT);
        return response.getId();
    }
    
    // 复杂查询示例
    public List<User> searchUsers(String keyword, Integer minAge, Integer maxAge) throws IOException {
        SearchRequest searchRequest = new SearchRequest("users");
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        
        BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
        
        // 关键字搜索
        if (keyword != null && !keyword.isEmpty()) {
            boolQuery.must(QueryBuilders.multiMatchQuery(keyword, "username", "email"));
        }
        
        // 年龄范围过滤
        BoolQueryBuilder filterQuery = QueryBuilders.boolQuery();
        if (minAge != null) {
            filterQuery.must(QueryBuilders.rangeQuery("age").gte(minAge));
        }
        if (maxAge != null) {
            filterQuery.must(QueryBuilders.rangeQuery("age").lte(maxAge));
        }
        boolQuery.filter(filterQuery);
        
        sourceBuilder.query(boolQuery);
        sourceBuilder.from(0);
        sourceBuilder.size(100);
        
        searchRequest.source(sourceBuilder);
        
        SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
        
        List<User> users = new ArrayList<>();
        for (SearchHit hit : searchResponse.getHits().getHits()) {
            // 将搜索结果转换为实体类
            Map<String, Object> sourceAsMap = hit.getSourceAsMap();
            // 这里可以手动映射或使用ObjectMapper转换
            // User user = objectMapper.convertValue(sourceAsMap, User.class);
            // users.add(user);
        }
        
        return users;
    }
    
    // 使用Repository进行简单查询
    public List<User> findUsersByUsername(String username) {
        return userRepository.findByUsernameContaining(username);
    }
}

八、控制器实现

创建RESTful API控制器:

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 java.util.List;
import java.util.Optional;

@RestController
@RequestMapping("/api/users")
public class UserController {
    
    @Autowired
    private UserRepository userRepository;
    
    @Autowired
    private ElasticsearchService elasticsearchService;
    
    // 保存用户
    @PostMapping
    public User saveUser(@RequestBody User user) {
        return userRepository.save(user);
    }
    
    // 根据ID获取用户
    @GetMapping("/{id}")
    public Optional<User> getUserById(@PathVariable String id) {
        return userRepository.findById(id);
    }
    
    // 搜索用户
    @GetMapping("/search")
    public List<User> searchUsers(
            @RequestParam(required = false) String keyword,
            @RequestParam(required = false) Integer minAge,
            @RequestParam(required = false) Integer maxAge) {
        try {
            return elasticsearchService.searchUsers(keyword, minAge, maxAge);
        } catch (Exception e) {
            e.printStackTrace();
            return List.of();
        }
    }
    
    // 分页查询
    @GetMapping("/page")
    public Page<User> getUsersByPage(
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "10") int size) {
        return userRepository.findAll(PageRequest.of(page, size));
    }
    
    // 删除用户
    @DeleteMapping("/{id}")
    public void deleteUser(@PathVariable String id) {
        userRepository.deleteById(id);
    }
}

九、索引操作

创建索引

import org.elasticsearch.action.admin.indices.create.CreateIndexRequest;
import org.elasticsearch.action.admin.indices.create.CreateIndexResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.xcontent.XContentType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Component
public class IndexOperation {
    
    @Autowired
    private RestHighLevelClient restHighLevelClient;
    
    // 创建索引
    public boolean createIndex(String indexName) throws IOException {
        CreateIndexRequest request = new CreateIndexRequest(indexName);
        
        // 设置索引参数
        request.settings(Settings.builder()
                .put("number_of_shards", 3)
                .put("number_of_replicas", 1)
        );
        
        // 设置映射
        String mapping = "{\n" +
                "  \"properties\": {\n" +
                "    \"username\": {\"type\": \"text\", \"analyzer\": \"ik_max_word\"},\n" +
                "    \"email\": {\"type\": \"keyword\"},\n" +
                "    \"age\": {\"type\": \"integer\"},\n" +
                "    \"createdAt\": {\"type\": \"date\"}\n" +
                "  }\n" +
                "}";
        
        request.mapping(mapping, XContentType.JSON);
        
        CreateIndexResponse response = restHighLevelClient.indices().create(request, RequestOptions.DEFAULT);
        return response.isAcknowledged();
    }
}

十、常见问题与解决方案

1. 版本兼容性问题

  • 确保SpringBoot版本与Elasticsearch版本匹配
  • 检查依赖版本是否一致

2. 中文分词配置

安装IK分词器插件到Elasticsearch:

  1. 下载与Elasticsearch版本匹配的IK分词器
  2. 解压到Elasticsearch的plugins目录
  3. 重启Elasticsearch服务

3. 连接超时问题

spring:
  elasticsearch:
    uris: http://localhost:9200
    connection-timeout: 30s  # 增加连接超时时间
    socket-timeout: 60s      # 增加读取超时时间

4. 安全认证配置

如果Elasticsearch启用了安全认证,需要配置用户名和密码:

spring:
  elasticsearch:
    uris: https://localhost:9200
    username: elastic
    password: changeme
    ssl:
      enabled: true

5. 索引不存在问题

在操作前检查索引是否存在,或者配置自动创建索引:

@Configuration
public class ElasticsearchConfig {
    
    @Bean
    public ElasticsearchOperations elasticsearchTemplate(RestHighLevelClient client) {
        ElasticsearchRestTemplate template = new ElasticsearchRestTemplate(client);
        template.setIndexOperationsInterceptor(indexOperations -> 
            new IndexOperationsAdapter(indexOperations) {
                @Override
                public boolean create() {
                    if (!exists()) {
                        return super.create();
                    }
                    return true;
                }
            }
        );
        return template;
    }
}

通过以上配置和示例,您可以成功实现SpringBoot与Elasticsearch的整合,并根据项目需求进行灵活的配置和使用。