【ElasticSearch】客户端选择

【ElasticSearch】客户端选择

【一】springboot整合es的客户端工具

【1】可选工具和客户端

(1)ElasticsearchRepository​

这是Spring Data Elasticsearch提供的一个接口,类似于Spring Data JPA的Repository。它提供了基本的CRUD操作和简单的查询方法(通过方法名或注解定义查询)。适用于简单的CRUD操作和查询,能够快速开发。

(2)​ElasticsearchRestTemplate​(或旧版的ElasticsearchTemplate):

(1)ElasticsearchTemplate是Spring Data Elasticsearch早期版本中的主要类,基于TransportClient(已弃用)。

(2)ElasticsearchRestTemplate是Spring Data Elasticsearch 3.2.x及以上版本推荐的类,基于High Level REST Client。它提供了更底层的操作,可以执行复杂的查询和聚合,适用于需要高度自定义查询的场景。

(3)High Level REST Client​

Elasticsearch官方提供的Java高级 REST 客户端,用于与Elasticsearch集群通信。它提供了所有Elasticsearch操作的方法,但使用起来相对繁琐,需要手动构建请求和解析响应。在Spring Data Elasticsearch中,通常不需要直接使用,因为ElasticsearchRestTemplate已经对其进行了封装。

(4)​Java API Client​

Elasticsearch 7.15及以上版本引入了新的Java API客户端,这是一个基于Jackson的强类型客户端,提供了更好的类型安全和性能。但是,Spring Data Elasticsearch目前(截至3.2.x)还没有完全整合这个新客户端。

【2】如何选择

(1)如果只需要基本的CRUD和简单查询,推荐使用ElasticsearchRepository,因为它使用简单,代码量少。

(2)如果需要执行复杂的查询、聚合、或者需要更灵活地控制查询过程,那么应该使用ElasticsearchRestTemplate。

(3)如果Spring Data Elasticsearch提供的功能无法满足需求(例如,使用一些非常新的Elasticsearch特性),可以考虑直接使用Elasticsearch的Java API Client,但这样会失去Spring Data的便利性。

【3】如何使用

(1)ElasticsearchRepository 使用

(1)创建Repository接口

java 复制代码
public interface ProductRepository extends ElasticsearchRepository<Product, String> {
    // 自定义查询方法
    List<Product> findByName(String name);
    
    List<Product> findByPriceBetween(Double minPrice, Double maxPrice);
    
    Page<Product> findByCategory(String category, Pageable pageable);
    
    // 使用@Query注解自定义DSL
    @Query("{\"match\": {\"name\": \"?0\"}}")
    Page<Product> findByNameCustom(String name, Pageable pageable);
}

(2)使用示例

java 复制代码
@Service
@RequiredArgsConstructor
public class ProductService {
    private final ProductRepository productRepository;
    
    public Page<Product> searchProducts(String keyword, int page, int size) {
        return productRepository.findByNameCustom(
            keyword, 
            PageRequest.of(page, size, Sort.by("price").descending())
        );
    }
    
    public List<Product> findProductsByPriceRange(double min, double max) {
        return productRepository.findByPriceBetween(min, max);
    }
}

(2)ElasticsearchRestTemplate 使用

(1)配置类

java 复制代码
@Configuration
public class ElasticsearchConfig {
    
    @Bean
    public ElasticsearchRestTemplate elasticsearchRestTemplate(
            ElasticsearchRestClient elasticsearchRestClient) {
        return new ElasticsearchRestTemplate(elasticsearchRestClient);
    }
}

(2)复杂查询实现

java 复制代码
@Service
@RequiredArgsConstructor
public class ProductSearchService {
    private final ElasticsearchRestTemplate elasticsearchRestTemplate;
    
    public SearchPage<Product> complexSearch(String keyword, String category, 
                                           Double minPrice, Double maxPrice, 
                                           int page, int size) {
        // 构建布尔查询
        BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
        
        if (StringUtils.hasText(keyword)) {
            boolQuery.must(QueryBuilders.matchQuery("name", keyword).boost(2.0f));
        }
        
        if (StringUtils.hasText(category)) {
            boolQuery.must(QueryBuilders.termQuery("category", category));
        }
        
        if (minPrice != null || maxPrice != null) {
            RangeQueryBuilder rangeQuery = QueryBuilders.rangeQuery("price");
            if (minPrice != null) rangeQuery.gte(minPrice);
            if (maxPrice != null) rangeQuery.lte(maxPrice);
            boolQuery.must(rangeQuery);
        }
        
        // 构建分页和排序
        NativeSearchQuery searchQuery = new NativeSearchQueryBuilder()
                .withQuery(boolQuery)
                .withPageable(PageRequest.of(page, size))
                .withSort(SortBuilders.fieldSort("price").order(SortOrder.DESC))
                .build();
        
        SearchHits<Product> searchHits = elasticsearchRestTemplate.search(searchQuery, Product.class);
        
        return SearchHitSupport.searchPageFor(searchHits, searchQuery.getPageable());
    }
    
    // 聚合查询示例
    public Map<String, Long> getCategoryStats() {
        TermsAggregationBuilder aggregation = AggregationBuilders
                .terms("category_agg")
                .field("category")
                .size(10);
        
        NativeSearchQuery searchQuery = new NativeSearchQueryBuilder()
                .addAggregation(aggregation)
                .build();
        
        SearchHits<Product> searchHits = elasticsearchRestTemplate.search(searchQuery, Product.class);
        
        Terms terms = searchHits.getAggregations().get("category_agg");
        return terms.getBuckets().stream()
                .collect(Collectors.toMap(
                    Terms.Bucket::getKeyAsString, 
                    Terms.Bucket::getDocCount
                ));
    }
}

(3)新的Java API Client使用(Elasticsearch 7.17+)

(1)添加依赖

xml 复制代码
<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-elasticsearch</artifactId>
    <version>5.1.0</version>
</dependency>
<dependency>
    <groupId>co.elastic.clients</groupId>
    <artifactId>elasticsearch-java</artifactId>
    <version>8.11.4</version>
</dependency>

(2)配置客户端

java 复制代码
@Configuration
public class ElasticsearchClientConfig {
    
    @Value("${spring.elasticsearch.uris}")
    private String[] elasticsearchUris;
    
    @Bean
    public ElasticsearchClient elasticsearchClient() {
        // 创建低级客户端
        RestClient restClient = RestClient
                .builder(HttpHost.create(elasticsearchUris[0]))
                .build();
        
        // 创建传输层
        ElasticsearchTransport transport = new RestClientTransport(
                restClient, new JacksonJsonpMapper());
        
        // 创建API客户端
        return new ElasticsearchClient(transport);
    }
}

(3)使用Java API Client

java 复制代码
@Service
@RequiredArgsConstructor
public class ProductJavaClientService {
    private final ElasticsearchClient elasticsearchClient;
    
    public void createProduct(Product product) throws IOException {
        elasticsearchClient.index(i -> i
                .index("products")
                .id(product.getId())
                .document(product));
    }
    
    public List<Product> searchProducts(String keyword) throws IOException {
        SearchResponse<Product> response = elasticsearchClient.search(s -> s
                .index("products")
                .query(q -> q
                    .match(m -> m
                        .field("name")
                        .query(keyword)
                    )
                ),
            Product.class);
        
        return response.hits().hits().stream()
                .map(Hit::source)
                .collect(Collectors.toList());
    }
}

【二】ElasticsearchRepository使用案例

【1】如何使用

(1)步骤1:定义实体类

java 复制代码
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;

@Document(indexName = "products")
@Data
public class Product {
    
    @Id
    private String id;
    
    @Field(type = FieldType.Text, analyzer = "ik_max_word")
    private String name;
    
    @Field(type = FieldType.Double)
    private Double price;
    
    @Field(type = FieldType.Keyword)
    private String category;
    
    @Field(type = FieldType.Integer)
    private Integer stock;
    
    // 构造方法
    public Product() {}
    
    public Product(String name, Double price, String category, Integer stock) {
        this.name = name;
        this.price = price;
        this.category = category;
        this.stock = stock;
    }

}

(2)步骤2:创建继承ElasticsearchRepository的接口

java 复制代码
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.List;

public interface ProductRepository extends ElasticsearchRepository<Product, String> {
    
    // 1. 方法名派生查询
    List<Product> findByName(String name);
    
    List<Product> findByPriceBetween(Double minPrice, Double maxPrice);
    
    List<Product> findByCategoryAndStockGreaterThan(String category, Integer stock);
    
    Page<Product> findByCategory(String category, Pageable pageable);
    
    // 2. 使用@Query自定义查询
    @Query("{\"match\": {\"name\": \"?0\"}}")
    List<Product> findByNameCustom(String name);
    
    @Query("{\"range\": {\"price\": {\"gte\": ?0, \"lte\": ?1}}}")
    List<Product> findByPriceRange(Double minPrice, Double maxPrice);
    
    @Query("{\"bool\": {\"must\": [{\"match\": {\"category\": \"?0\"}}, {\"range\": {\"stock\": {\"gt\": ?1}}}]}}")
    List<Product> findByCategoryAndStockGreaterThanCustom(String category, Integer stock);
    
    // 3. 聚合查询
    @Query("{\"aggs\": {\"category_count\": {\"terms\": {\"field\": \"category.keyword\"}}}}")
    Map<String, Long> countByCategory();
}

(3)步骤3:使用该接口进行CRUD操作

java 复制代码
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;

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

@Service
public class ProductService {
    
    private final ProductRepository productRepository;
    
    @Autowired
    public ProductService(ProductRepository productRepository) {
        this.productRepository = productRepository;
    }
    
    // 保存产品
    public Product saveProduct(Product product) {
        return productRepository.save(product);
    }
    
    // 批量保存
    public Iterable<Product> saveProducts(List<Product> products) {
        return productRepository.saveAll(products);
    }
    
    // 根据ID查询
    public Optional<Product> findById(String id) {
        return productRepository.findById(id);
    }
    
    // 检查是否存在
    public boolean existsById(String id) {
        return productRepository.existsById(id);
    }
    
    // 根据名称查询
    public List<Product> findByName(String name) {
        return productRepository.findByName(name);
    }
    
    // 价格范围查询
    public List<Product> findByPriceRange(Double minPrice, Double maxPrice) {
        return productRepository.findByPriceBetween(minPrice, maxPrice);
    }
    
    // 分类分页查询
    public Page<Product> findByCategory(String category, int page, int size) {
        return productRepository.findByCategory(category, PageRequest.of(page, size));
    }
    
    // 自定义查询
    public List<Product> findByNameCustom(String name) {
        return productRepository.findByNameCustom(name);
    }
    
    // 删除产品
    public void deleteProduct(String id) {
        productRepository.deleteById(id);
    }
    
    // 获取所有产品(排序)
    public List<Product> findAllSortedByPrice() {
        return (List<Product>) productRepository.findAll(Sort.by(Sort.Direction.DESC, "price"));
    }
    
    // 分页查询所有产品
    public Page<Product> findAllProducts(int page, int size) {
        return productRepository.findAll(PageRequest.of(page, size));
    }
    
    // 按分类统计产品数量
    public Map<String, Long> countProductsByCategory() {
        return productRepository.countByCategory();
    }
}

(4)配置类

java 复制代码
import org.elasticsearch.client.RestHighLevelClient;
import org.springframework.context.annotation.Bean;
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 org.springframework.data.elasticsearch.repository.config.EnableElasticsearchRepositories;

@Configuration
@EnableElasticsearchRepositories(basePackages = "com.example.repository")
public class ElasticsearchConfig extends AbstractElasticsearchConfiguration {
    
    @Override
    @Bean
    public RestHighLevelClient elasticsearchClient() {
        ClientConfiguration clientConfiguration = ClientConfiguration.builder()
            .connectedTo("localhost:9200")
            .build();
        
        return RestClients.create(clientConfiguration).rest();
    }
}

【2】可用方法

(1)基本 CRUD 方法

save(S entity):保存单个实体

saveAll(Iterable entities):批量保存
findById(ID id):根据ID查询
existsById(ID id):检查是否存在
findAll():查询所有文档
findAllById(Iterable ids):根据ID列表查询
count():统计文档数量
deleteById(ID id):根据ID删除
delete(T entity):删除实体
deleteAll():删除所有文档

(2)分页和排序

findAll(Pageable pageable):分页查询

findAll(Sort sort):排序查询

(3)自定义查询方法

方法名派生查询:findBy[Field][Operation]

@Query注解自定义查询

【三】ElasticsearchRestTemplate使用案例

【1】核心功能

ElasticsearchRestTemplate是 Spring Data Elasticsearch 提供的高级操作类,比 ElasticsearchRepository更灵活,支持更复杂的操作:

文档操作​:索引、更新、删除文档

​查询操作​:执行各种类型的查询(匹配、范围、布尔等)

​聚合分析​:执行统计、分组等聚合操作

索引管理​:创建、删除索引,管理映射

​批量操作​:高效执行批量索引/删除

​脚本支持​:执行脚本更新

​地理空间查询​:执行地理位置相关查询

【2】可用方法

(1)文档操作方法

index(T entity):索引单个文档

save(T entity):保存/更新文档

bulkIndex(List queries):批量索引文档

delete(String id, Class clazz):删除文档

delete(Query query, Class clazz):按查询删除文档

deleteByQuery(Query query, Class clazz):按查询删除文档

get(String id, Class clazz):根据ID获取文档

(2)查询操作方法

search(Query query, Class clazz):执行查询

search(SearchQuery query, Class clazz):执行搜索查询

queryForList(Query query, Class clazz):查询文档列表

queryForPage(Query query, Class clazz):分页查询

suggest(SuggestBuilder suggestion, Class clazz):执行建议查询

(3)聚合分析方法

aggregate(Aggregation aggregation, Class clazz):执行聚合

query(Query query, ResultsExtractor resultsExtractor):自定义结果提取

(4)索引管理方法

indexOps(Class clazz):获取索引操作接口

createIndex(Class clazz):创建索引

deleteIndex(Class clazz):删除索引

putMapping(Class clazz):更新映射

refresh(Class clazz):刷新索引

【3】使用案例

(1)配置类

java 复制代码
import org.elasticsearch.client.RestHighLevelClient;
import org.springframework.context.annotation.Bean;
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.core.ElasticsearchRestTemplate;

@Configuration
public class ElasticsearchConfig {
    
    @Bean
    public RestHighLevelClient restHighLevelClient() {
        ClientConfiguration clientConfiguration = ClientConfiguration.builder()
            .connectedTo("localhost:9200")
            .withConnectTimeout(5000)
            .withSocketTimeout(60000)
            .build();
        
        return RestClients.create(clientConfiguration).rest();
    }
    
    @Bean
    public ElasticsearchRestTemplate elasticsearchRestTemplate() {
        return new ElasticsearchRestTemplate(restHighLevelClient());
    }
}

(2)实体类

java 复制代码
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;
import org.springframework.data.elasticsearch.annotations.GeoPointField;
import org.springframework.data.elasticsearch.core.geo.GeoPoint;

@Document(indexName = "products")
public class Product {
    
    @Id
    private String id;
    
    @Field(type = FieldType.Text, analyzer = "ik_max_word")
    private String name;
    
    @Field(type = FieldType.Double)
    private Double price;
    
    @Field(type = FieldType.Keyword)
    private String category;
    
    @Field(type = FieldType.Integer)
    private Integer stock;
    
    @Field(type = FieldType.Date, format = DateFormat.date_hour_minute_second)
    private Date createdAt;
    
    @GeoPointField
    private GeoPoint location;
    
    // 构造方法、Getter和Setter
    public Product() {}
    
    public Product(String name, Double price, String category, Integer stock) {
        this.name = name;
        this.price = price;
        this.category = category;
        this.stock = stock;
        this.createdAt = new Date();
    }
    
    // 省略Getter和Setter...
}

(3)服务层实现

java 复制代码
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.bucket.terms.Terms;
import org.elasticsearch.search.aggregations.metrics.Avg;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.elasticsearch.core.*;
import org.springframework.data.elasticsearch.core.geo.GeoPoint;
import org.springframework.data.elasticsearch.core.query.*;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.stream.Collectors;

@Service
public class ProductService {
    
    private final ElasticsearchRestTemplate elasticsearchRestTemplate;
    
    @Autowired
    public ProductService(ElasticsearchRestTemplate elasticsearchRestTemplate) {
        this.elasticsearchRestTemplate = elasticsearchRestTemplate;
    }
    
    // 1. 索引单个文档
    public String indexProduct(Product product) {
        IndexQuery indexQuery = new IndexQueryBuilder()
            .withObject(product)
            .build();
        
        return elasticsearchRestTemplate.index(indexQuery, IndexCoordinates.of("products"));
    }
    
    // 2. 批量索引文档
    public List<String> bulkIndexProducts(List<Product> products) {
        List<IndexQuery> queries = products.stream()
            .map(product -> new IndexQueryBuilder()
                .withObject(product)
                .build())
            .collect(Collectors.toList());
        
        return elasticsearchRestTemplate.bulkIndex(queries, IndexCoordinates.of("products"));
    }
    
    // 3. 根据ID获取文档
    public Product getProductById(String id) {
        return elasticsearchRestTemplate.get(id, Product.class);
    }
    
    // 4. 根据ID删除文档
    public String deleteProductById(String id) {
        return elasticsearchRestTemplate.delete(id, Product.class);
    }
    
    // 5. 简单匹配查询
    public List<Product> searchByName(String name) {
        Query query = new NativeSearchQueryBuilder()
            .withQuery(QueryBuilders.matchQuery("name", name))
            .build();
        
        SearchHits<Product> hits = elasticsearchRestTemplate.search(query, Product.class);
        return hits.stream().map(SearchHit::getContent).collect(Collectors.toList());
    }
    
    // 6. 分页查询
    public Page<Product> searchByCategory(String category, int page, int size) {
        Pageable pageable = PageRequest.of(page, size);
        
        Query query = new NativeSearchQueryBuilder()
            .withQuery(QueryBuilders.termQuery("category", category))
            .withPageable(pageable)
            .build();
        
        SearchHits<Product> hits = elasticsearchRestTemplate.search(query, Product.class);
        List<Product> products = hits.stream().map(SearchHit::getContent).collect(Collectors.toList());
        
        return new PageImpl<>(products, pageable, hits.getTotalHits());
    }
    
    // 7. 范围查询
    public List<Product> findByPriceRange(double minPrice, double maxPrice) {
        Query query = new NativeSearchQueryBuilder()
            .withQuery(QueryBuilders.rangeQuery("price").gte(minPrice).lte(maxPrice))
            .build();
        
        SearchHits<Product> hits = elasticsearchRestTemplate.search(query, Product.class);
        return hits.stream().map(SearchHit::getContent).collect(Collectors.toList());
    }
    
    // 8. 布尔查询
    public List<Product> complexSearch(String keyword, String category, double minPrice) {
        Query query = new NativeSearchQueryBuilder()
            .withQuery(QueryBuilders.boolQuery()
                .must(QueryBuilders.matchQuery("name", keyword))
                .must(QueryBuilders.termQuery("category", category))
                .must(QueryBuilders.rangeQuery("price").gte(minPrice))
            )
            .build();
        
        SearchHits<Product> hits = elasticsearchRestTemplate.search(query, Product.class);
        return hits.stream().map(SearchHit::getContent).collect(Collectors.toList());
    }
    
    // 9. 聚合分析 - 按类别统计数量
    public Map<String, Long> countByCategory() {
        Query query = new NativeSearchQueryBuilder()
            .addAggregation(AggregationBuilders.terms("by_category").field("category.keyword"))
            .build();
        
        SearchHits<Product> hits = elasticsearchRestTemplate.search(query, Product.class);
        Terms terms = hits.getAggregations().get("by_category");
        
        return terms.getBuckets().stream()
            .collect(Collectors.toMap(
                Terms.Bucket::getKeyAsString,
                Terms.Bucket::getDocCount
            ));
    }
    
    // 10. 聚合分析 - 计算平均价格
    public double averagePrice() {
        Query query = new NativeSearchQueryBuilder()
            .addAggregation(AggregationBuilders.avg("avg_price").field("price"))
            .build();
        
        SearchHits<Product> hits = elasticsearchRestTemplate.search(query, Product.class);
        Avg avg = hits.getAggregations().get("avg_price");
        
        return avg.getValue();
    }
    
    // 11. 更新文档 - 部分更新
    public void updateProductStock(String id, int newStock) {
        Map<String, Object> params = new HashMap<>();
        params.put("stock", newStock);
        
        UpdateQuery updateQuery = UpdateQuery.builder(id)
            .withParams(params)
            .build();
        
        elasticsearchRestTemplate.update(updateQuery, IndexCoordinates.of("products"));
    }
    
    // 12. 脚本更新
    public void increaseProductPrice(String id, double amount) {
        Map<String, Object> params = new HashMap<>();
        params.put("amount", amount);
        
        UpdateQuery updateQuery = UpdateQuery.builder(id)
            .withScript("ctx._source.price += params.amount")
            .withParams(params)
            .build();
        
        elasticsearchRestTemplate.update(updateQuery, IndexCoordinates.of("products"));
    }
    
    // 13. 按查询更新
    public long increaseStockForCategory(String category, int amount) {
        UpdateQuery updateQuery = UpdateQuery.builder(
            QueryBuilders.termQuery("category", category)
        )
        .withScript("ctx._source.stock += params.amount")
        .withParams(Collections.singletonMap("amount", amount))
        .build();
        
        ByQueryResponse response = elasticsearchRestTemplate.updateByQuery(
            updateQuery, 
            IndexCoordinates.of("products")
        );
        
        return response.getUpdated();
    }
    
    // 14. 按查询删除
    public long deleteByCategory(String category) {
        Query query = new NativeSearchQueryBuilder()
            .withQuery(QueryBuilders.termQuery("category", category))
            .build();
        
        return elasticsearchRestTemplate.delete(query, Product.class);
    }
    
    // 15. 地理位置查询 - 附近的产品
    public List<Product> findNearbyProducts(double lat, double lon, double distance) {
        Query query = new NativeSearchQueryBuilder()
            .withQuery(QueryBuilders.geoDistanceQuery("location")
                .point(lat, lon)
                .distance(distance + "km"))
            .build();
        
        SearchHits<Product> hits = elasticsearchRestTemplate.search(query, Product.class);
        return hits.stream().map(SearchHit::getContent).collect(Collectors.toList());
    }
    
    // 16. 高亮显示
    public List<SearchHit<Product>> searchWithHighlight(String keyword) {
        Query query = new NativeSearchQueryBuilder()
            .withQuery(QueryBuilders.matchQuery("name", keyword))
            .withHighlightFields(
                new HighlightBuilder.Field("name")
                    .preTags("<em>")
                    .postTags("</em>")
            )
            .build();
        
        return elasticsearchRestTemplate.search(query, Product.class).getSearchHits();
    }
    
    // 17. 自动补全
    public List<String> suggestNames(String prefix) {
        SuggestBuilder suggestBuilder = new SuggestBuilder()
            .addSuggestion("name-suggest", 
                SuggestBuilders.completionSuggestion("suggest")
                    .prefix(prefix)
                    .skipDuplicates(true)
                    .size(10));
        
        SuggestResponse response = elasticsearchRestTemplate.suggest(suggestBuilder, Product.class);
        return response.getSuggest()
            .getSuggestion("name-suggest")
            .getEntries()
            .get(0)
            .getOptions()
            .stream()
            .map(Suggest.Suggestion.Entry.Option::getText)
            .collect(Collectors.toList());
    }
    
    // 18. 索引管理 - 创建索引
    public boolean createProductIndex() {
        return elasticsearchRestTemplate.indexOps(Product.class).create();
    }
    
    // 19. 索引管理 - 更新映射
    public boolean updateProductMapping() {
        return elasticsearchRestTemplate.indexOps(Product.class).putMapping();
    }
    
    // 20. 索引管理 - 刷新索引
    public void refreshProductIndex() {
        elasticsearchRestTemplate.indexOps(Product.class).refresh();
    }
}

【4】实践优化

(1)批量操作优化

java 复制代码
public void bulkInsert(List<Product> products) {
    List<IndexQuery> queries = new ArrayList<>();
    
    for (Product product : products) {
        IndexQuery query = new IndexQuery();
        query.setId(product.getId());
        query.setObject(product);
        queries.add(query);
    }
    
    // 分批处理,每批1000条
    int batchSize = 1000;
    for (int i = 0; i < queries.size(); i += batchSize) {
        int end = Math.min(i + batchSize, queries.size());
        List<IndexQuery> batch = queries.subList(i, end);
        elasticsearchRestTemplate.bulkIndex(batch, Product.class);
    }
}

(2)高校分页查询

java 复制代码
public List<Product> efficientPagination(String category, int page, int size) {
    // 使用search_after实现高效分页
    Query query = new NativeSearchQueryBuilder()
        .withQuery(QueryBuilders.termQuery("category", category))
        .withSort(SortBuilders.fieldSort("price").order(SortOrder.DESC))
        .withPageable(PageRequest.of(0, size)) // 第一页
        .build();
    
    SearchHits<Product> hits = elasticsearchRestTemplate.search(query, Product.class);
    List<Object> searchAfter = hits.getSearchHits().get(hits.getSearchHits().size() - 1).getSortValues();
    
    // 获取下一页
    Query nextPageQuery = new NativeSearchQueryBuilder()
        .withQuery(QueryBuilders.termQuery("category", category))
        .withSort(SortBuilders.fieldSort("price").order(SortOrder.DESC))
        .withSearchAfter(searchAfter.toArray())
        .withPageable(PageRequest.of(0, size))
        .build();
    
    return elasticsearchRestTemplate.search(nextPageQuery, Product.class)
        .stream()
        .map(SearchHit::getContent)
        .collect(Collectors.toList());
}

【四】​Java API Client​使用案例

【1】介绍

Elasticsearch Java API Client 是 Elasticsearch 官方推出的新一代 Java 客户端,具有以下特点:

强类型​:所有请求和响应都是类型安全的

现代化设计​:基于 Elasticsearch DSL 构建

异步支持​:内置异步操作支持

模块化​:按需引入不同功能模块

与 REST API 完全对应​:1:1 映射 REST API

【2】核心功能

(1)主要模块

elasticsearch-java:核心模块

elasticsearch-java-api:API 模块

elasticsearch-transport:传输层

elasticsearch-x-content:JSON 处理

(2)主要类

ElasticsearchClient:主客户端类

*Request:各种操作请求(IndexRequest, SearchRequest 等)

*Response:各种操作响应

Query:查询构建器

Aggregation:聚合构建器

【3】使用案例

(1)maven配置

xml 复制代码
<dependencies>
    <!-- 核心依赖 -->
    <dependency>
        <groupId>co.elastic.clients</groupId>
        <artifactId>elasticsearch-java</artifactId>
        <version>8.11.4</version>
    </dependency>
    
    <!-- JSON 处理 -->
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.15.2</version>
    </dependency>
    
    <!-- HTTP 客户端 -->
    <dependency>
        <groupId>org.apache.httpcomponents.client5</groupId>
        <artifactId>httpclient5</artifactId>
        <version>5.2.1</version>
    </dependency>
</dependencies>

(2)配置类

java 复制代码
import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.json.jackson.JacksonJsonpMapper;
import co.elastic.clients.transport.ElasticsearchTransport;
import co.elastic.clients.transport.rest_client.RestClientTransport;
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.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ElasticsearchConfig {
    
    @Bean
    public ElasticsearchClient elasticsearchClient() {
        // 1. 创建低级客户端
        RestClient restClient = RestClient.builder(
            new HttpHost("localhost", 9200)
        )
        .setHttpClientConfigCallback(httpClientBuilder -> {
            // 认证配置
            CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
            credentialsProvider.setCredentials(
                AuthScope.ANY,
                new UsernamePasswordCredentials("elastic", "your-password")
            );
            
            return httpClientBuilder
                .setDefaultCredentialsProvider(credentialsProvider)
                .setSSLHostnameVerifier((hostname, session) -> true); // 开发环境禁用主机名验证
        })
        .build();
        
        // 2. 创建传输层
        ElasticsearchTransport transport = new RestClientTransport(
            restClient,
            new JacksonJsonpMapper()
        );
        
        // 3. 创建API客户端
        return new ElasticsearchClient(transport);
    }
}

(3)实体类

java 复制代码
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonProperty;

import java.time.LocalDateTime;
import java.util.List;

@Data
public class Product {
    private String id;
    private String name;
    private String description;
    private double price;
    private String category;
    private int stock;
    private List<String> tags;
    
    @JsonProperty("created_at")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createdAt;
    
    private Location location;
    
    // 嵌套对象
    public static class Location {
        private double lat;
        private double lon;
        
        // Getter和Setter
        public double getLat() { return lat; }
        public void setLat(double lat) { this.lat = lat; }
        public double getLon() { return lon; }
        public void setLon(double lon) { this.lon = lon; }
    }

}

(4)服务层实现

java 复制代码
import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.elasticsearch._types.*;
import co.elastic.clients.elasticsearch._types.aggregations.*;
import co.elastic.clients.elasticsearch._types.query_dsl.*;
import co.elastic.clients.elasticsearch.core.*;
import co.elastic.clients.elasticsearch.core.bulk.BulkOperation;
import co.elastic.clients.elasticsearch.core.search.*;
import co.elastic.clients.json.JsonData;
import co.elastic.clients.util.ObjectBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;

@Service
public class ProductService {
    
    private static final String INDEX_NAME = "products";
    private final ElasticsearchClient client;
    
    @Autowired
    public ProductService(ElasticsearchClient client) {
        this.client = client;
    }
    
    // 1. 创建索引
    public void createIndex() throws IOException {
        client.indices().create(c -> c
            .index(INDEX_NAME)
            .settings(s -> s
                .numberOfShards("3")
                .numberOfReplicas("1")
            )
            .mappings(m -> m
                .properties("name", p -> p.text(t -> t.analyzer("ik_max_word")))
                .properties("description", p -> p.text(t -> t.analyzer("ik_smart")))
                .properties("price", p -> p.double_(d -> d))
                .properties("category", p -> p.keyword(k -> k))
                .properties("stock", p -> p.integer(i -> i))
                .properties("tags", p -> p.keyword(k -> k))
                .properties("created_at", p -> p.date(d -> d.format("yyyy-MM-dd HH:mm:ss")))
                .properties("location", p -> p.geoPoint(g -> g))
            )
        );
    }
    
    // 2. 索引单个文档
    public String indexProduct(Product product) throws IOException {
        IndexResponse response = client.index(i -> i
            .index(INDEX_NAME)
            .id(product.getId())
            .document(product)
        );
        return response.id();
    }
    
    // 3. 批量索引文档
    public List<String> bulkIndexProducts(List<Product> products) throws IOException {
        List<BulkOperation> operations = products.stream()
            .map(product -> BulkOperation.of(op -> op
                .index(idx -> idx
                    .index(INDEX_NAME)
                    .id(product.getId())
                    .document(product)
                )
            ))
            .collect(Collectors.toList());
        
        BulkResponse response = client.bulk(b -> b
            .index(INDEX_NAME)
            .operations(operations)
        );
        
        return response.items().stream()
            .map(BulkResponseItem::id)
            .collect(Collectors.toList());
    }
    
    // 4. 根据ID获取文档
    public Product getProductById(String id) throws IOException {
        GetResponse<Product> response = client.get(g -> g
            .index(INDEX_NAME)
            .id(id),
            Product.class
        );
        
        if (response.found()) {
            return response.source();
        }
        return null;
    }
    
    // 5. 根据ID删除文档
    public boolean deleteProductById(String id) throws IOException {
        DeleteResponse response = client.delete(d -> d
            .index(INDEX_NAME)
            .id(id)
        );
        return response.result() == Result.Deleted;
    }
    
    // 6. 简单匹配查询
    public List<Product> searchByName(String name) throws IOException {
        SearchResponse<Product> response = client.search(s -> s
            .index(INDEX_NAME)
            .query(q -> q
                .match(m -> m
                    .field("name")
                    .query(name)
                )
            ),
            Product.class
        );
        
        return extractProducts(response);
    }
    
    // 7. 多字段搜索
    public List<Product> multiMatchSearch(String query) throws IOException {
        SearchResponse<Product> response = client.search(s -> s
            .index(INDEX_NAME)
            .query(q -> q
                .multiMatch(m -> m
                    .fields("name", "description", "tags")
                    .query(query)
                )
            ),
            Product.class
        );
        
        return extractProducts(response);
    }
    
    // 8. 布尔查询
    public List<Product> complexSearch(String keyword, String category, double minPrice) throws IOException {
        SearchResponse<Product> response = client.search(s -> s
            .index(INDEX_NAME)
            .query(q -> q
                .bool(b -> b
                    .must(m -> m.match(t -> t.field("name").query(keyword)))
                    .must(m -> m.term(t -> t.field("category").value(category)))
                    .must(m -> m.range(r -> r.field("price").gte(JsonData.of(minPrice))))
                )
            ),
            Product.class
        );
        
        return extractProducts(response);
    }
    
    // 9. 范围查询
    public List<Product> findByPriceRange(double min, double max) throws IOException {
        SearchResponse<Product> response = client.search(s -> s
            .index(INDEX_NAME)
            .query(q -> q
                .range(r -> r
                    .field("price")
                    .gte(JsonData.of(min))
                    .lte(JsonData.of(max))
                )
            ),
            Product.class
        );
        
        return extractProducts(response);
    }
    
    // 10. 分页查询
    public List<Product> findByCategoryPaginated(String category, int page, int size) throws IOException {
        SearchResponse<Product> response = client.search(s -> s
            .index(INDEX_NAME)
            .query(q -> q
                .term(t -> t
                    .field("category")
                    .value(category)
                )
            )
            .from(page * size)
            .size(size),
            Product.class
        );
        
        return extractProducts(response);
    }
    
    // 11. 聚合分析 - 按类别统计数量
    public Map<String, Long> countByCategory() throws IOException {
        SearchResponse<Product> response = client.search(s -> s
            .index(INDEX_NAME)
            .size(0)
            .aggregations("by_category", a -> a
                .terms(t -> t
                    .field("category.keyword")
                    .size(100)
                )
            ),
            Product.class
        );
        
        return response.aggregations().get("by_category").sterms().buckets().array().stream()
            .collect(Collectors.toMap(
                StringTermsBucket::key,
                StringTermsBucket::docCount
            ));
    }
    
    // 12. 聚合分析 - 计算平均价格
    public double averagePrice() throws IOException {
        SearchResponse<Product> response = client.search(s -> s
            .index(INDEX_NAME)
            .size(0)
            .aggregations("avg_price", a -> a
                .avg(av -> av.field("price"))
            ),
            Product.class
        );
        
        AvgAggregate avg = response.aggregations().get("avg_price").avg();
        return avg.value();
    }
    
    // 13. 更新文档 - 部分更新
    public void updateProductStock(String id, int newStock) throws IOException {
        client.update(u -> u
            .index(INDEX_NAME)
            .id(id)
            .doc(Map.of("stock", newStock)),
            Product.class
        );
    }
    
    // 14. 脚本更新
    public void increaseProductPrice(String id, double amount) throws IOException {
        client.update(u -> u
            .index(INDEX_NAME)
            .id(id)
            .script(s -> s
                .inline(i -> i
                    .source("ctx._source.price += params.amount")
                    .params("amount", JsonData.of(amount))
                )
            ),
            Product.class
        );
    }
    
    // 15. 按查询更新
    public long increaseStockForCategory(String category, int amount) throws IOException {
        UpdateByQueryResponse response = client.updateByQuery(u -> u
            .index(INDEX_NAME)
            .query(q -> q
                .term(t -> t
                    .field("category")
                    .value(category)
                )
            )
            .script(s -> s
                .inline(i -> i
                    .source("ctx._source.stock += params.amount")
                    .params("amount", JsonData.of(amount))
                )
            )
        );
        
        return response.updated();
    }
    
    // 16. 按查询删除
    public long deleteByCategory(String category) throws IOException {
        DeleteByQueryResponse response = client.deleteByQuery(d -> d
            .index(INDEX_NAME)
            .query(q -> q
                .term(t -> t
                    .field("category")
                    .value(category)
                )
            )
        );
        
        return response.deleted();
    }
    
    // 17. 地理位置查询 - 附近的产品
    public List<Product> findNearbyProducts(double lat, double lon, double distance) throws IOException {
        SearchResponse<Product> response = client.search(s -> s
            .index(INDEX_NAME)
            .query(q -> q
                .geoDistance(g -> g
                    .field("location")
                    .distance(distance + "km")
                    .location(l -> l.latlon(ll -> ll.lat(lat).lon(lon)))
                )
            ),
            Product.class
        );
        
        return extractProducts(response);
    }
    
    // 18. 高亮显示
    public List<Hit<Product>> searchWithHighlight(String keyword) throws IOException {
        SearchResponse<Product> response = client.search(s -> s
            .index(INDEX_NAME)
            .query(q -> q
                .match(m -> m
                    .field("name")
                    .query(keyword)
                )
            )
            .highlight(h -> h
                .fields("name", f -> f
                    .preTags("<em>")
                    .postTags("</em>")
                )
            ),
            Product.class
        );
        
        return response.hits().hits();
    }
    
    // 19. 自动补全
    public List<String> suggestNames(String prefix) throws IOException {
        SearchResponse<Product> response = client.search(s -> s
            .index(INDEX_NAME)
            .suggest(su -> su
                .suggesters("name-suggest", sug -> sug
                    .completion(c -> c
                        .field("suggest")
                        .size(10)
                        .skipDuplicates(true)
                    )
                )
                .text(prefix)
            ),
            Product.class
        );
        
        return response.suggest().get("name-suggest").get(0).completion().options().stream()
            .map(CompletionSuggestOption::text)
            .collect(Collectors.toList());
    }
    
    // 20. 批量操作
    public void bulkOperations(List<Product> toCreate, List<String> toDelete) throws IOException {
        List<BulkOperation> operations = new ArrayList<>();
        
        // 添加创建操作
        toCreate.forEach(product -> operations.add(BulkOperation.of(op -> op
            .index(idx -> idx
                .index(INDEX_NAME)
                .id(product.getId())
                .document(product)
            )
        )));
        
        // 添加删除操作
        toDelete.forEach(id -> operations.add(BulkOperation.of(op -> op
            .delete(d -> d
                .index(INDEX_NAME)
                .id(id)
            )
        )));
        
        client.bulk(b -> b
            .index(INDEX_NAME)
            .operations(operations)
        );
    }
    
    // 辅助方法:从响应中提取产品列表
    private List<Product> extractProducts(SearchResponse<Product> response) {
        return response.hits().hits().stream()
            .map(Hit::source)
            .filter(Objects::nonNull)
            .collect(Collectors.toList());
    }
}

【4】高级用法

(1)异步操作

java 复制代码
import co.elastic.clients.util.ApiTypeHelper;
import co.elastic.clients.util.BinaryData;

public void asyncIndexProduct(Product product) {
    BinaryData data = BinaryData.of(
        ApiTypeHelper.jsonBuilder(product).toString(),
        client._transport().jsonpMapper()
    );
    
    client.indexAsync(i -> i
        .index(INDEX_NAME)
        .id(product.getId())
        .document(data)
    ).whenComplete((response, exception) -> {
        if (exception != null) {
            // 处理异常
            System.err.println("索引失败: " + exception.getMessage());
        } else {
            // 处理成功
            System.out.println("文档索引成功,ID: " + response.id());
        }
    });
}

(2)复杂聚合

java 复制代码
public Map<String, Double> avgPriceByCategory() throws IOException {
    SearchResponse<Product> response = client.search(s -> s
        .index(INDEX_NAME)
        .size(0)
        .aggregations("by_category", a -> a
            .terms(t -> t
                .field("category.keyword")
                .size(100)
            )
            .aggregations("avg_price", avg -> avg
                .avg(av -> av.field("price"))
            )
        ),
        Product.class
    );
    
    return response.aggregations().get("by_category").sterms().buckets().array().stream()
        .collect(Collectors.toMap(
            StringTermsBucket::key,
            bucket -> bucket.aggregations().get("avg_price").avg().value()
        ));
}

(3)自定义json映射

java 复制代码
import co.elastic.clients.json.JsonpMapper;
import co.elastic.clients.json.jackson.JacksonJsonpMapper;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;

// 自定义 JSON 映射器
public JsonpMapper customJsonpMapper() {
    ObjectMapper objectMapper = new ObjectMapper();
    objectMapper.registerModule(new JavaTimeModule()); // 支持 Java 8 时间类型
    
    return new JacksonJsonpMapper(objectMapper);
}

// 在配置中使用自定义映射器
@Bean
public ElasticsearchClient elasticsearchClient() {
    RestClient restClient = RestClient.builder(
        new HttpHost("localhost", 9200)
    ).build();
    
    ElasticsearchTransport transport = new RestClientTransport(
        restClient,
        customJsonpMapper() // 使用自定义映射器
    );
    
    return new ElasticsearchClient(transport);
}
相关推荐
皮皮学姐分享-ppx1 小时前
机器人行业工商注册企业基本信息数据(1958-2023年)
大数据·人工智能·python·物联网·机器人·区块链
Elasticsearch1 小时前
使用 ES|QL COMPLETION + 一个 LLM 在 5 分钟内编写一个 Chuck Norris 事实生成器
elasticsearch
喻师傅2 小时前
横扫SQL面试——流量与转化率分类
大数据·数据仓库·sql
武子康3 小时前
大数据-81 Spark 手把手搭建 分布式计算环境:从下载配置到多节点部署
大数据·后端·spark
科技热点圈5 小时前
切入高潜市场,抢占行业先机!ES SHOW 2025展位预订火爆,10月28-30日共启增长新蓝海
大数据·elasticsearch·搜索引擎
低代码布道师6 小时前
UX 设计入门终章:让洞察落地!用用户流程图、IA 和旅程图,设计用户与产品的互动故事
大数据·流程图·ux
专注API从业者14 小时前
基于 Node.js 的淘宝 API 接口开发:快速构建异步数据采集服务
大数据·前端·数据库·数据挖掘·node.js
3Cloudream15 小时前
互联网大厂Java面试深度解析:从基础到微服务云原生的全场景模拟
java·spring boot·redis·elasticsearch·微服务·kafka·电商架构
测试专家16 小时前
ARINC 825板卡的应用
大数据·网络·安全