Elasticsearch Java API Client 入门实践

搜索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;
    }
}

如果以上内容有错误欢迎各位批评指正,如果不清楚欢迎私信提问。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值