ElasticSearch 全文检索(下)

一、封装Elasticsearch客户端

在上篇博客中介绍了 Elasticsearch的Java客户端怎么使用,但是在实际项目中应用,需要进行合理封装,下面主要讲一下在Spring Boot项目中封装。

1.封装配置参数对象 ElasticsearchConfig

@Configuration
@ConfigurationProperties(
        prefix = "elasticsearch.config"
)
public class ElasticsearchConfig {
    private String hostname;
    private int port;
    private String certPassword;
    private String trustStorePath;
    private String keyStorePath;
    ...

然后在application.properties 文件中对应参数进行配置

使用ConfigurationProperties 别忘了pom中加入 spring-boot-configuration-processor依赖

2.将elasticsearch客户端封装为spring bean

目的是统一的客户端调用,方便使用时注入,调用时同一个对象(单例)。在相关spring容器管理的类中 @Bean注解创建客户端的方法即可(参考上篇)

@Configuration
public class ElasticsearchBean {

    private final ElasticsearchConfig elasticsearchConfig;

    @Autowired
    public ElasticsearchBean(ElasticsearchConfig elasticsearchConfig) {
        this.elasticsearchConfig = elasticsearchConfig;
    }

    /**
     * 根据指定jks文件路径生成keyStore
     */
    private KeyStore loadKeyStore(String keyStorePath) throws KeyStoreException, CertificateException, NoSuchAlgorithmException, IOException {
        KeyStore keyStore = KeyStore.getInstance("jks");
        InputStream is = ElasticsearchBean.class.getResourceAsStream(keyStorePath);
        keyStore.load(is,elasticsearchConfig.getCertPassword().toCharArray());
        return keyStore;
    }

    /**
     * 封装 buildSSLContext
     */
    private SSLContext buildSSLContext() throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException, UnrecoverableKeyException, KeyManagementException {
        SSLContextBuilder sslBuilder = SSLContexts.custom()
                .loadTrustMaterial(loadKeyStore(elasticsearchConfig.getTrustStorePath()), null)
                .loadKeyMaterial(loadKeyStore(elasticsearchConfig.getKeyStorePath()),
                        elasticsearchConfig.getCertPassword().toCharArray());
        return sslBuilder.build();
    }

    /**
     * @return 封装 RestClient
     */
    @Bean(destroyMethod = "close")
    public RestHighLevelClient restClient(){
        RestClientBuilder builder = RestClient.builder(new HttpHost(elasticsearchConfig.getHostname(), elasticsearchConfig.getPort(), "https"));
        SSLContext sslContext = buildSSLContext();
        builder.setHttpClientConfigCallback(httpClientBuilder ->
                    httpClientBuilder.setSSLContext(sslContext));
        return new RestHighLevelClient(builder);
    }
}

这里也可以通过 RestClientBuilder 设置一些监听回调的方法,超时设置等

这里使用了 RestHighLevelClient ,所以注意添加依赖
elasticsearch-rest-high-level-client

3.封装Elasticsearch 操作service

包括一些对文档的增删改查操作,对于elasticsearch的索引,类型等根据自己情况设置,我这里是统一的,所以用了常量进行定义

@Component
public class ElasticsearchService {

    private final RestHighLevelClient restClient;

    @Autowired
    public ElasticsearchService(RestHighLevelClient restClient) {
        this.restClient = restClient;
    }

    /**
     * 保存doc
     * @param id 文档id
     * @param documentJson 文档json
     * @throws IOException IO异常
     */
    private void saveDocument(String id,String documentJson) throws IOException {
        IndexRequest request = new IndexRequest(
                ElasticsearchConstant.INDEX_NAME,
                ElasticsearchConstant.INDEX_TYPE,
                id);
        request.source(documentJson, XContentType.JSON);
        restClient.index(request);
    }

    /**
     * 根据id获取文档
     * @param id id
     * @throws IOException IO异常
     */
    private GetResponse getDocument(String id) throws IOException {
        GetRequest getRequest = new GetRequest(
                ElasticsearchConstant.INDEX_NAME,
                ElasticsearchConstant.INDEX_TYPE,
                id);
        return restClient.get(getRequest);
    }

    /**
     * 删除文档
     * @param id id
     * @return 结果
     * @throws IOException IO异常
     */
    private DeleteResponse deleteDocument(String id) throws IOException {
        DeleteRequest request = new DeleteRequest(
                ElasticsearchConstant.INDEX_NAME,
                ElasticsearchConstant.INDEX_TYPE,
                id);
        return restClient.delete(request);
    }

    /**
     * 更新文档内容
     * @param id id
     * @param contentJson json内容
     * @return 结果
     * @throws IOException IO异常
     */
    private UpdateResponse updateDocument(String id,String contentJson)throws IOException{
        UpdateRequest updateRequest = new UpdateRequest(
                ElasticsearchConstant.INDEX_NAME,
                ElasticsearchConstant.INDEX_TYPE,
                id);
        updateRequest.doc(contentJson, XContentType.JSON);
        return restClient.update(updateRequest);
    }

}

将要存入的内容封装为统一对象(ContentWord),存入该对象的json即可

 /**
     * 保存内容
     * @param contentWord 内容
     * @throws IOException IO异常
     */
    public void saveContent(ContentWord contentWord) throws IOException {
        saveDocument(contentWord.getId(), Utility.toJson(contentWord));
    }

增删改,都比较简单,下面来讲讲查询(高亮搜索,分页查询)

封装搜索方法

  /**
     * 查询所有文档
     * @param highlightBuilder 高亮封装
     * @param query 查询体
     * @param from 开始数
     * @param size 查询数
     * @return 查询结果
     * @throws IOException IO异常
     */
   private SearchResponse searchDocument(HighlightBuilder highlightBuilder,QueryBuilder query,int from,int size) throws IOException {
       SearchRequest searchRequest = new SearchRequest(ElasticsearchConstant.INDEX_NAME);
       searchRequest.types(ElasticsearchConstant.INDEX_TYPE);
       SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
       if (highlightBuilder != null) {
           searchSourceBuilder.highlighter(highlightBuilder);
       }
       searchSourceBuilder.query(query);
       searchSourceBuilder.from(from);
       searchSourceBuilder.size(size);
       searchRequest.source(searchSourceBuilder);
       return restClient.search(searchRequest);
   }

高亮封装

/**
     * 封装高亮查询字段
     * @param fieldName 字段名
     * @return 高亮字段体
     */
    private HighlightBuilder.Field makeHighlightContent(String fieldName){
        HighlightBuilder.Field highlightContent = new HighlightBuilder.Field(fieldName);
        highlightContent.highlighterType("unified");
        highlightContent.fragmentSize(FRAGMENT_SIZE);
        highlightContent.numOfFragments(FRAGMENT_NUM);
        return highlightContent;
    }

这里可以设置很多属性,比如一次搜索的 碎片(根据关键字检索的上下文片段) 总数,碎片大小(每个片段字符数),高亮时使用的标签(默认 em 标签),高亮类型等等,我这里设置了碎片总数和碎片大小

高亮搜索

/**
     * 高亮分页搜索
     * @param page 页码(从0开始)
     * @param size 每页数量
     * @param text 搜索文本
     * @param fieldNames 搜索字段名
     * @return 搜索结果
     * @throws IOException IO异常
     */
    public SearchResponse searchHighlight(int page, int size,String text,String... fieldNames)throws IOException {
        int from = page * size;
        HighlightBuilder highlightBuilder = new HighlightBuilder();
        for (String fieldName:fieldNames){
            highlightBuilder.field(makeHighlightContent(fieldName));
        }
        return searchDocument(highlightBuilder,QueryBuilders.multiMatchQuery(text,fieldNames)
                .fuzziness(Fuzziness.AUTO),from,size);
    }

一般来说搜索都是 多字段 的 比如搜索博客关键字,同时根据关键字搜索:标题,内容,组织,作者等等字段进行匹配。

二、搜索使用

1.封装碎片转为String

    /**
     * 高亮碎片转为string
     * @param fragments 碎片数组
     * @return 字符串
     */
    private String fragmentsToString(Text[] fragments){
        return Arrays.stream(fragments).map(Text::string)
                .collect(Collectors.joining("\n"));
    }

2.将搜索结果获取的SearchHit转为我们封装的统一对象(ContentWord)

需要搜索的字段(高亮处理的字段)重新赋值,其他普通字段不管,直接使用存储时的值。

    /**
     * 搜索结果转为 ContentWord 对象
     * @param searchHit 搜索结果
     * @return ContentWord
     */
    private ContentWord fromSearchHit(SearchHit searchHit){
        String json = searchHit.getSourceAsString();
        ContentWord contentWord = Utility.toClass(json,ContentWord.class);
        Map<String,HighlightField> highlightFields = searchHit.getHighlightFields();
        if (!highlightFields.isEmpty()){
            HighlightField titleField = highlightFields.get("title");
            if (titleField != null){
                contentWord.setTitle(fragmentsToString(titleField.fragments()));
            }
            HighlightField contentField = highlightFields.get("content");
            if (contentField != null){
                contentWord.setContent(fragmentsToString(contentField.fragments()));
            }
        }
        return contentWord;
    }

这里我只检索了两个字段(高亮处理)titlecontent

3.搜索内容

    /**
     * 搜索内容
     * @param keyWord 关键字
     * @param pageable 分页信息
     * @return 结果
     * @throws IOException EX
     */
    public Page<ContentWord> searchContent(String keyWord,Pageable pageable) throws IOException {
        SearchResponse searchResponse = elasticsearchService.searchHighlight(pageable.getPageNumber(), 
                pageable.getPageSize(),keyWord, "title","content");
        List<ContentWord> list = Arrays.stream(searchResponse.getHits().getHits())
                .map(this::fromSearchHit).collect(Collectors.toList());
        return new PageImpl<>(list,pageable,searchResponse.getHits().getTotalHits());
    }

三、最后

Elasticsearch内容还是很多的,如Elasticsearch服务集群,配置分词器,与Hadoop结合使用,用作服务日志分析,接口调用情况分析等。这次先到这里吧。我这里只是简单的从0到1介绍了实际项目中入门使用。相关的博客也很多了,但是我用到的时候,有些问题没有在中文技术论坛中找到,所以就写了这两篇博客。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值