案例
Spring Boot 提供了 spring-data-elasticsearch
模塊,可以方便地集成 Elasticsearch。 下面我們將詳細講解如何在 Spring Boot 中使用 Elasticsearch 8,并提供示例代碼。
1. 添加依賴:
首先,需要在 pom.xml
文件中添加 spring-data-elasticsearch
的依賴。 注意,你需要選擇與你的 Spring Boot 版本和 Elasticsearch 版本兼容的 spring-data-elasticsearch
版本
<dependencies>
? ? <dependency>
? ? ? ? <groupId>org.springframework.boot</groupId>
? ? ? ? <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
? ? </dependency>
? ? <dependency>
? ? ? ? <groupId>org.springframework.boot</groupId>
? ? ? ? <artifactId>spring-boot-starter-web</artifactId>
? ? </dependency>? ? <!-- 其他依賴 -->
</dependencies>
2. 配置 Elasticsearch:
在 application.properties
或 application.yml
文件中配置 Elasticsearch 的連接信息。
spring.elasticsearch.uris=http://localhost:9200
# 如果 Elasticsearch 開啟了安全認證,需要配置用戶名和密碼
#spring.elasticsearch.username=elastic
#spring.elasticsearch.password=your_password?
3. 創建實體類:
創建一個實體類,用于映射 Elasticsearch 中的文檔結構。 使用 @Document
注解指定索引名稱,使用 @Id
注解指定文檔 ID 字段。
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") // 指定索引名稱
public class Product {@Id // 指定文檔 ID 字段private String id;@Field(type = FieldType.Text, name = "name")private String name;@Field(type = FieldType.Double, name = "price")private Double price;// 必須要有默認構造函數public Product() {}public Product(String id, String name, Double price) {this.id = id;this.name = name;this.price = price;}public String getId() {return id;}public void setId(String id) {this.id = id;}public String getName() {return name;}public void setName(String name) {this.name = name;}public Double getPrice() {return price;}public void setPrice(Double price) {this.price = price;}
}
4. 創建 Repository 接口:
創建一個 Repository 接口,用于操作 Elasticsearch 中的文檔。 繼承 ElasticsearchRepository
接口,并指定實體類和 ID 的類型。
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import org.springframework.stereotype.Repository;@Repository
public interface ProductRepository extends ElasticsearchRepository<Product, String> {// 可以自定義查詢方法,例如根據名稱查詢商品// Spring Data Elasticsearch 會根據方法名自動生成查詢語句Iterable<Product> findByName(String name);
}
5. 創建 Service 類:
創建一個 Service 類,用于調用 Repository 接口,實現業務邏輯。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;@Service
public class ProductService {@Autowiredprivate ProductRepository productRepository;// 創建索引(可選,通常 Elasticsearch 會自動創建)public void createIndex() {// 注意: Spring Data Elasticsearch 提供了自動索引管理的機制,通常不需要手動創建索引// 如果需要自定義索引設置,可以使用 ElasticsearchClient 手動創建// 例如設置 Settings 和 Mappings。}// 添加商品public Product addProduct(Product product) {return productRepository.save(product);}// 根據ID查找商品public Product findProductById(String id) {return productRepository.findById(id).orElse(null);}// 根據名稱查找商品public Iterable<Product> findProductsByName(String name) {return productRepository.findByName(name);}// 更新商品public Product updateProduct(Product product) {return productRepository.save(product); // save 方法在 ID 存在時更新文檔}// 刪除商品public void deleteProductById(String id) {productRepository.deleteById(id);}// 列出所有商品public Iterable<Product> getAllProducts() {return productRepository.findAll();}
}
6. 創建 Controller 類:
創建一個 Controller 類,提供 RESTful API 接口。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;@RestController
@RequestMapping("/products")
public class ProductController {@Autowiredprivate ProductService productService;@PostMappingpublic Product addProduct(@RequestBody Product product) {return productService.addProduct(product);}@GetMapping("/{id}")public Product findProductById(@PathVariable String id) {return productService.findProductById(id);}@GetMapping("/name/{name}")public Iterable<Product> findProductsByName(@PathVariable String name) {return productService.findProductsByName(name);}@PutMappingpublic Product updateProduct(@RequestBody Product product) {return productService.updateProduct(product);}@DeleteMapping("/{id}")public void deleteProductById(@PathVariable String id) {productService.deleteProductById(id);}@GetMappingpublic Iterable<Product> getAllProducts() {return productService.getAllProducts();}
}
7. 完整示例與注意事項:
-
自動索引管理:
spring-data-elasticsearch
提供了自動索引管理的機制。 當你的應用程序啟動時,它會自動檢查 Elasticsearch 中是否存在與你的實體類對應的索引。 如果不存在,它會自動創建索引,并根據實體類的注解設置 mappings。 如果需要更細粒度的控制,你可以禁用自動索引管理,并使用ElasticsearchClient
手動創建索引。 -
ElasticsearchClient 的使用 (高級):
spring-data-elasticsearch
默認提供了ElasticsearchRestTemplate
,可以方便地執行 Elasticsearch 操作。 如果你需要更高級的功能,例如手動創建索引、執行復雜的查詢等,可以使用ElasticsearchClient
。
import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.elasticsearch.indices.CreateIndexRequest;
import co.elastic.clients.elasticsearch.indices.CreateIndexResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.util.Map;@Service
public class ElasticsearchIndexService {@Autowiredprivate ElasticsearchOperations elasticsearchOperations;public void createIndex(String indexName, Map<String, Object> mappings) throws IOException {ElasticsearchClient client = elasticsearchOperations.getClient();CreateIndexRequest request = new CreateIndexRequest.Builder().index(indexName).mappings(m -> m.properties(mappings)) // 這里 mappings 是一個 Map<String,Object>.build();CreateIndexResponse response = client.indices().create(request);if (response.acknowledged()) {System.out.println("Index '" + indexName + "' created successfully.");} else {System.out.println("Failed to create index '" + indexName + "'.");}}
}
企業中使用
ElasticsearchClient
是 Elasticsearch Java 客戶端庫中的核心組件,用于與 Elasticsearch 集群進行交互。它提供了一系列方法來執行各種操作,如索引文檔、搜索數據、更新文檔、刪除文檔等。
刪
//根據查詢條件刪除
@Overridepublic DeleteByQueryResponse delete(@NonNull Class<?> model, @NonNull Query query) {ModelContext context = ModelContext.of(model);DeleteByQueryRequest request = new DeleteByQueryRequest.Builder().index(context.getIndex()).query(query).conflicts(Conflicts.Proceed).build();try {return this.client.deleteByQuery(request);} catch (IOException e) {throw new RuntimeException(e);}}Query query = QueryBuilders.term(t -> t.field("resumeId").value(resume.getId()));
DeleteByQueryResponse response = this.elasticsearchHandler.delete(Vitae.class, query);//使用
DeleteRequest request = new DeleteRequest.Builder().index(context.getIndex()).id(id).build();
改
前期學習
修改文檔:
全量修改:PUT/索引庫名/_doc/文檔id{json文檔}增量修改:POST/索引庫名/_update/文檔id{"doc":{字段}}
@Overridepublic UpdateByQueryResponse update(@NonNull Class<?> model, @NonNull Query query,@NonNull Map<String, ?> parameters) {ModelContext context = ModelContext.of(model);StringBuilder source = new StringBuilder();Map<String, JsonData> params = Maps.newHashMapWithExpectedSize(parameters.size());parameters.forEach((key, value) -> {if (source.length() > 0) {source.append(";");}if (value == null) {source.append("ctx._source.").append(key).append("=null");} else {source.append("ctx._source.").append(key).append("=params.").append(key);params.put(key, JsonData.of(value));}});Script script = new Script.Builder().lang(ScriptLanguage.Painless).source(source.toString()).params(params).build();UpdateByQueryRequest request = new UpdateByQueryRequest.Builder().index(context.getIndex()).script(script).query(query).conflicts(Conflicts.Proceed).build();try {return this.client.updateByQuery(request);} catch (IOException e) {throw new RuntimeException(e);}}//使用
UpdateByQueryResponse response = this.elasticsearchHandler.update(Vitae.class, query, parameters);
@Overridepublic UpdateByQueryResponse update(@NonNull Class<?> model, @NonNull Query query, @NonNull String field,Object value) {ModelContext context = ModelContext.of(model);String source = "ctx._source." + field + (value == null ? "=null" : ("=params." + field));Map<String, JsonData> params = value == null ? Collections.emptyMap() :ImmutableMap.of(field, JsonData.of(value));Script script = new Script.Builder().lang(ScriptLanguage.Painless).source(source).params(params).build();UpdateByQueryRequest request = new UpdateByQueryRequest.Builder().index(context.getIndex()).script(script).query(query).conflicts(Conflicts.Proceed).build();try {return this.client.updateByQuery(request);} catch (IOException e) {throw new RuntimeException(e);}}//使用
this.elasticsearchHandler.update(Vitae.class, query, "topping", document);
@Overridepublic <T> UpdateResponse<T> update(@NonNull Class<T> model, @NonNull String id, @NonNull Object document,Long primary, Long sequence) {ModelContext context = ModelContext.of(model);UpdateRequest<T, ?> request = new UpdateRequest.Builder<T, Object>().index(context.getIndex()).id(id).doc(document).ifPrimaryTerm(primary).ifSeqNo(sequence).retryOnConflict(3).build();try {return this.client.update(request, model);} catch (IOException e) {throw new RuntimeException(e);}}//使用
this.elasticsearchHandler.update(Job.class, id, ImmutableMap.of("recruiting", recruiting));
查
前期學習
springboot操作es的DSL
QueryBuilders 常用方法
全文查詢
-
matchQuery(String field, Object value)
: 全文匹配 -
multiMatchQuery(Object value, String... fields)
: 多字段匹配 -
matchPhraseQuery(String field, Object value)
: 短語匹配 -
matchPhrasePrefixQuery(String field, Object value)
: 短語前綴匹配
詞項查詢
-
termQuery(String field, Object value)
: 精確詞項匹配 -
termsQuery(String field, String... values)
: 多詞項精確匹配 -
rangeQuery(String field)
: 范圍查詢(如數值、日期) -
existsQuery(String field)
: 檢查字段是否存在 -
prefixQuery(String field, String value)
: 前綴匹配 -
wildcardQuery(String field, String value)
: 通配符匹配(如*
或?
) -
regexpQuery(String field, String value)
: 正則表達式匹配 -
fuzzyQuery(String field, String value)
: 模糊匹配(容錯匹配)
復合查詢
-
boolQuery()
: 布爾查詢(組合多個子查詢) -
disMaxQuery()
: 取多個查詢中的最佳匹配 -
constantScoreQuery(QueryBuilder query)
: 固定評分查詢 -
nestedQuery(String path, QueryBuilder query, ScoreMode scoreMode)
: 嵌套對象查詢 -
functionScoreQuery(QueryBuilder query, FunctionScoreBuilder... functions)
: 自定義評分函數
地理位置查詢
-
geoDistanceQuery(String field)
: 地理距離范圍查詢 -
geoBoundingBoxQuery(String field)
: 地理邊界框查詢
其他查詢
-
scriptQuery(Script script)
: 腳本查詢 -
idsQuery().addIds(String... ids)
: 根據ID查詢
綜合示例一
假設有一個products
索引,包含以下字段:
-
name
(文本) -
price
(整數) -
status
(關鍵字) -
tags
(關鍵字數組) -
location
(地理坐標) -
metadata
(嵌套類型,包含key
和value
)
import org.elasticsearch.index.query.*;
import org.elasticsearch.common.geo.GeoPoint;
import org.elasticsearch.script.Script;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.action.search.SearchRequest;public class QueryExample {public static void main(String[] args) {// 1. 布爾查詢(組合多個子查詢)BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();// 2. 全文匹配:搜索name中包含"phone"boolQuery.must(QueryBuilders.matchQuery("name", "phone"));// 3. 多字段匹配:在name和description中搜索"smart"boolQuery.must(QueryBuilders.multiMatchQuery("smart", "name", "description"));// 4. 短語匹配:description中精確匹配"high quality"boolQuery.must(QueryBuilders.matchPhraseQuery("description", "high quality"));// 5. 精確詞項匹配:status為"active"boolQuery.must(QueryBuilders.termQuery("status", "active"));// 6. 多詞項匹配:tags包含"electronics"或"gadgets"boolQuery.must(QueryBuilders.termsQuery("tags", "electronics", "gadgets"));// 7. 范圍查詢:price在100到500之間boolQuery.must(QueryBuilders.rangeQuery("price").gte(100).lte(500));// 8. 存在性檢查:stock字段必須存在boolQuery.filter(QueryBuilders.existsQuery("stock"));// 9. 前綴匹配:description以"latest"開頭boolQuery.should(QueryBuilders.prefixQuery("description", "latest"));// 10. 通配符匹配:tags字段匹配"tec*"(如"tech")boolQuery.should(QueryBuilders.wildcardQuery("tags", "tec*"));// 11. 正則表達式匹配:name匹配正則"pho.*e"boolQuery.should(QueryBuilders.regexpQuery("name", "pho.*e"));// 12. 模糊查詢:name容錯匹配"fone"boolQuery.should(QueryBuilders.fuzzyQuery("name", "fone"));// 13. 地理距離查詢:location距離(37.7749, -122.4194)10公里內boolQuery.filter(QueryBuilders.geoDistanceQuery("location").point(37.7749, -122.4194).distance("10km"));// 14. 嵌套查詢:metadata.key為"color"且metadata.value為"black"boolQuery.must(QueryBuilders.nestedQuery("metadata",QueryBuilders.boolQuery().must(QueryBuilders.termQuery("metadata.key", "color")).must(QueryBuilders.matchQuery("metadata.value", "black")),ScoreMode.Total));// 15. 腳本查詢:price大于200Script script = new Script("doc['price'].value > 200");boolQuery.filter(QueryBuilders.scriptQuery(script));// 構建搜索請求SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();sourceBuilder.query(boolQuery);SearchRequest searchRequest = new SearchRequest("products");searchRequest.source(sourceBuilder);// 執行搜索(需Elasticsearch客戶端)// SearchResponse response = client.search(searchRequest, RequestOptions.DEFAULT);}
}
綜合案例二?
@Overridepublic List<Job> listSearchJobs(@NonNull TextSearch search) {List<Query> pairs = Lists.newLinkedList();if (search.getRegion() != null) {pairs.add(this.jobRegionQuery("regionId", search.getRegion()));}if (StringUtils.notEmpty(search.getKeyword())) {pairs.add(QueryBuilders.match(b -> b.field("description").query(search.getKeyword())));}if (search.getType() != null) {// 指定職位類型pairs.add(QueryBuilders.term(t -> t.field("type").value(search.getType().name())));}if (ObjectUtils.notEmpty(search.getDriveLicenses())) {List<FieldValue> values = search.getDriveLicenses().stream().map(driveLicense -> FieldValue.of(driveLicense.name())).collect(Collectors.toList());pairs.add(QueryBuilders.terms(t -> t.field("driveLicense").terms(v -> v.value(values))));}if (ObjectUtils.isEmpty(pairs)) {return Collections.emptyList();}pairs.add(QueryBuilders.term(t -> t.field("status").value(ContentStatus.ONLINE.name())));Query query = QueryBuilders.bool(b -> b.must(pairs));SearchResponse<Job> response = this.elasticsearchHandler.search(Job.class, query,b -> b.sort(s -> s.field(f -> f.field("refreshTime").order(SortOrder.Desc))).sort(s -> s.field(f -> f.field("_score").order(SortOrder.Desc))).from(search.getPaging().getIndex()).size(search.getPaging().getSize()));return ElasticsearchUtils.sources(response);}
根據id查詢:
@Overridepublic <T> MgetResponse<T> get(@NonNull Class<T> model, @NonNull List<String> ids) {ModelContext context = ModelContext.of(model);MgetRequest request = new MgetRequest.Builder().index(context.getIndex()).ids(ids).build();try {return this.client.mget(request, model);} catch (IOException e) {throw new RuntimeException(e);}}//使用
MgetResponse<Vitae> response = this.elasticsearchHandler.get(Vitae.class, vitaeIds);
Set<Long> exists = response.docs().stream().map(doc -> doc.result().source()).filter(Objects::nonNull).map(Vitae::getId).collect(Collectors.toSet());
?滾動:
@Overridepublic <T> ScrollResponse<T> scroll(@NonNull Class<T> model, @NonNull String id, @NonNull String time) {ScrollRequest request = new ScrollRequest.Builder().scrollId(id).scroll(s -> s.time(time)).build();try {return this.client.scroll(request, model);} catch (IOException e) {throw new RuntimeException(e);}}//使用
scrollResponse = elasticsearchHandler.scroll(Job.class, scrollId, keyLive + "m");
ElasticsearchUtils.sources(scrollResponse)