搜索Elasticsearch Java 相关教程,网上大部分都是基于Rest High Level Client 的,然而
Elasticsearch官方已经将 Rest High Level Cilent 列为 Deprecated , 推荐使用最新的Elasticsearch Java API Client。本文通过使用 Elasticsearch Java API Client 8.10.3 版本,基于自己在真实项目中的实践,实现一个简单的Demo, 满足简单的搜索需求。
具体使用步骤如下
- 引入Elasticsearch Java API Client包
<dependency>
<groupId>co.elastic.clients</groupId>
<artifactId>elasticsearch-java</artifactId>
<version>8.10.3</version>
</dependency>
- 配置Elasticsearch连接
需要配置一个同步client用于实时查询和一个异步client用于增删改操作。以下配置是建立Https安全连接所需配置,如果只是Http连接,就会简单很多,网上有很多教程可以参考。
配置参数中的CA证书指纹获取方式在我之前的Elasticsearch安装教程中有说明。apiKey在Kibana->Security生成。
package com.example.demo.elastic.config;
import javax.net.ssl.SSLContext;
import org.apache.http.Header;
import org.apache.http.HttpHost;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.message.BasicHeader;
import org.elasticsearch.client.RestClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import co.elastic.clients.elasticsearch.ElasticsearchAsyncClient;
import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.json.jackson.JacksonJsonpMapper;
import co.elastic.clients.transport.ElasticsearchTransport;
import co.elastic.clients.transport.TransportUtils;
import co.elastic.clients.transport.rest_client.RestClientTransport;
@Configuration
public class ElasticsearchConfig {
@Value("${elastic.server.fingerprint}")
String fingerprint;
@Value("${elastic.client.apikey}")
String apiKey;
@Value("${elastic.server.node}")
String host;
@Value("${elastic.server.port}")
String port;
private ElasticsearchTransport getTransport(){
SSLContext sslContext = TransportUtils.sslContextFromCaFingerprint(fingerprint);
RestClient restClient=RestClient
.builder(new HttpHost(host, Integer.parseInt(port), "https"))
.setHttpClientConfigCallback(hc -> hc
.setSSLContext(sslContext)
.setSSLHostnameVerifier(NoopHostnameVerifier.INSTANCE))
.setDefaultHeaders(new Header[]{new BasicHeader("Authorization", "ApiKey " + apiKey)})
.build();
return new RestClientTransport(restClient, new JacksonJsonpMapper());
}
@Bean
public ElasticsearchClient elasticsearchClient() {
ElasticsearchTransport transport=getTransport();
return new ElasticsearchClient(transport);
}
@Bean
public ElasticsearchAsyncClient elasticsearchAsyncClient() {
ElasticsearchTransport transport=getTransport();
return new ElasticsearchAsyncClient(transport);
}
}
- 确定索引结构,即 index.json 文件内容
其中用到了ik分词器,ik分词器是一个轻量级中文分词器,关于ik分词器的安装在我之前的 Elasticsearch 安装教程文章中提到过。最后给索引指定别名,这里别名设置为docs,以后CRUD操作只需用别名访问即可, 关于使用别名访问的原因后续文章中会说明。
{
"mappings": {
"properties": {
"id": {
"type": "keyword"
},
"docNumber": {
"type": "keyword"
},
"releaseDate": {
"type": "date",
"format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd"
},
"docName": {
"type": "keyword"
},
"docContent": {
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart"
}
}
},
"settings": {
"number_of_shards": 1,
"number_of_replicas": 0
},
"aliases": {
"docs": {}
}
}
- 创建索引库,批量写入数据到索引库中
package com.example.demo.elastic;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import com.example.demo.elastic.dao.DocumentDAO;
import com.example.demo.elastic.entity.Document;
import com.fasterxml.jackson.databind.ObjectMapper;
import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.elasticsearch._helpers.bulk.BulkIngester;
import co.elastic.clients.elasticsearch.indices.CreateIndexRequest;
import co.elastic.clients.elasticsearch.indices.CreateIndexResponse;
import co.elastic.clients.elasticsearch.indices.ExistsRequest;
import co.elastic.clients.transport.endpoints.BooleanResponse;
@Component
public class ElasticsearchIndex {
private static final Logger log = LoggerFactory.getLogger(ElasticsearchIndex.class);
@Value("index")
private String indexName;
@Autowired
private ElasticsearchClient elasticsearchClient;
@Autowired
private DocumentDAO documentDAO;
public void initElastic() {
try {
ExistsRequest request = ExistsRequest.of(e -> e.index(indexName));
BooleanResponse indexExists = elasticsearchClient.indices().exists(request);
if (!indexExists.value()) {
createIndex();
}
} catch (IOException e) {
//...
}
// 批量写入
List<Document> docs = documentDAO.findAll();
bulkRequest(docs);
}
private void bulkRequest(List<Document> docs) {
BulkIngester<Void> ingester = BulkIngester.of(b -> b
.client(elasticsearchClient)
);
ObjectMapper mapper = new ObjectMapper();
for (Document doc : docs) {
ingester.add(op -> op
.index(idx -> idx
.index(indexName)
.id(doc.getId())
.document(mapper.valueToTree(doc))
)
);
}
ingester.close();
}
private void createIndex() {
// 创建索引
try (InputStream input = getClass().getClassLoader().getResourceAsStream("index.json")) {
CreateIndexRequest req = CreateIndexRequest.of(b -> b
.index(indexName)
.withJson(input)
);
CreateIndexResponse response = elasticsearchClient.indices().create(req);
boolean acknowledged = response.acknowledged();
boolean shardsAcknowledged = response.shardsAcknowledged();
String index = response.index();
log.info("创建索引状态:{}", acknowledged);
log.info("已确认的分片:{}", shardsAcknowledged);
log.info("索引名称:{}", index);
} catch (IOException e) {
log.error("创建索引失败", e);
//...
}
}
}
- 实现增删查改操作
package com.example.demo.elastic;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import com.example.demo.elastic.entity.Document;
import com.example.demo.elastic.util.Page;
import com.fasterxml.jackson.databind.ObjectMapper;
import co.elastic.clients.elasticsearch.ElasticsearchAsyncClient;
import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.elasticsearch._types.FieldValue;
import co.elastic.clients.elasticsearch._types.SortOrder;
import co.elastic.clients.elasticsearch._types.query_dsl.BoolQuery;
import co.elastic.clients.elasticsearch._types.query_dsl.MatchQuery;
import co.elastic.clients.elasticsearch._types.query_dsl.Query;
import co.elastic.clients.elasticsearch.core.SearchResponse;
import co.elastic.clients.elasticsearch.core.search.Hit;
import co.elastic.clients.elasticsearch.core.search.TotalHits;
import co.elastic.clients.transport.endpoints.BooleanResponse;
import io.micrometer.common.util.StringUtils;
@Service
public class ElasticsearchService {
private static final Logger log = LoggerFactory.getLogger(ElasticsearchService.class);
@Value("docs")
private String indexAlias;
private static final List<String> MATCH_FIELDS = Arrays.asList("id", "docNumber", "docName");
@Autowired
private ElasticsearchClient elasticsearchClient;
@Autowired
private ElasticsearchAsyncClient elasticsearchAsyncClient;
public Page findDocByCondition(String keyWord, int start, int limit) {
// 生成查询条件query
Query query = generateQuery(keyWord);
List<Document> procDocList = new ArrayList<>();
if (query == null) {
return new Page(procDocList, 0);
}
SearchResponse<Document> response = null;
try {
response = elasticsearchClient.search(s -> s
.index(indexAlias)
.query(query)
.sort(f -> f
.field(o -> o
.field("releaseDate")
.order(SortOrder.Desc)))
.from(start)
.size(limit),
Document.class
);
} catch (IOException | NullPointerException e) {
log.error("search error ", e);
}
TotalHits total = response.hits().total();
List<Hit<Document>> hits = response.hits().hits();
for (Hit<Document> hit : hits) {
procDocList.add(hit.source());
}
return new Page(procDocList, total != null ? total.value() : 0);
}
public void saveOrUpdateById(Document doc) {
boolean exist = docExists(doc.getId());
if (exist) {
// 若不是第一次保存,直接更新原有数据
updateDoc(doc);
} else {
ObjectMapper mapper = new ObjectMapper();
elasticsearchAsyncClient.index(s -> s
.index(indexAlias)
.id(doc.getId())
.document(mapper.valueToTree(doc)))
.whenComplete((response, exception) -> {
if (exception != null) {
// 异常处理
}
});
}
}
public void deleteDocById(Document doc) {
boolean exist = docExists(doc.getId());
if (exist) {
elasticsearchAsyncClient.delete(s -> s
.index(indexAlias)
.id(doc.getId())).whenComplete((response, exception) -> {
if (exception != null) {
// 异常处理
}
});
}
}
private boolean docExists(String id) {
BooleanResponse exist = new BooleanResponse(false);
try {
exist = elasticsearchClient.exists(e -> e.index(indexAlias).id(id));
} catch (IOException e) {
// 异常处理
}
return exist.value();
}
private void updateDoc(Document doc) {
elasticsearchAsyncClient.update(e -> e
.index(indexAlias)
.id(doc.getId())
.doc(doc)
.detectNoop(false),
Document.class)
.whenComplete((response, exception) -> {
if (exception != null) {
// 异常处理
}
});
}
private Query generateQuery(String keyword) {
if (StringUtils.isNotBlank(keyword)) {
List<Query> boolQueryList = new ArrayList<>();
// 不需要分词的字段用Wildcard字符串匹配
for (String field : MATCH_FIELDS) {
Query byField = Query.of(m -> m
.wildcard(w -> w
.field(field)
.value("*" + keyword + "*")
.caseInsensitive(true)
)
);
boolQueryList.add(byField);
}
// 需要分词全文搜索的字段用 Matchquery
Query byFieldName = MatchQuery.of(m -> m
.field("docContent")
.query(FieldValue.of(keyword))
)._toQuery();
boolQueryList.add(byFieldName);
return BoolQuery.of(b -> b.should(boolQueryList).minimumShouldMatch("1"))._toQuery();
}
return null;
}
}
如果以上内容有错误欢迎各位批评指正,如果不清楚欢迎私信提问。