SpringBoot集成Elasticsearch 使用(二)

388 阅读6分钟

SpringBoot集成Elasticsearch的方式

  1. Java REST API
  2. Java high level rest client
  3. Spring Data Elasticsearch

本文中使用的是第2种Java high level rest client

具体安装步骤见上篇

添加pom依赖

<!-- springboot版本是2.2.10.RELEASE -->
<parent>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-parent</artifactId>
   <version>2.2.10.RELEASE</version>
   <relativePath/> 
</parent>

<!-- elasticsearch版本是6.8.20 -->
<dependency>
   <groupId>org.elasticsearch</groupId>
   <artifactId>elasticsearch</artifactId>
   <version>6.8.20</version>
</dependency>
<dependency>
   <groupId>org.elasticsearch.client</groupId>
   <artifactId>elasticsearch-rest-client</artifactId>
   <version>6.8.20</version>
</dependency>
<dependency>
   <groupId>org.elasticsearch.client</groupId>
   <artifactId>elasticsearch-rest-high-level-client</artifactId>
   <version>6.8.20</version>
</dependency>

<!-- fastjson,看自己需要添加 -->
<dependency>
   <groupId>com.alibaba</groupId>
   <artifactId>fastjson</artifactId>
   <version>1.2.83</version>
</dependency>

在application.properties中添加elasticsearch连接配置

elasticsearch.host: 127.0.0.1
elasticsearch.port: 9200
elasticsearch.connTimeout: 3000
elasticsearch.socketTimeout: 5000
elasticsearch.connectionRequestTimeout: 5000

创建elasticsearch配置类

import org.apache.http.HttpHost;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestClientBuilder;
import org.elasticsearch.client.RestHighLevelClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ElasticsearchConfiguration {
    @Value("${elasticsearch.host}")
    private String host;

    @Value("${elasticsearch.port}")
    private int port;

    @Value("${elasticsearch.connTimeout}")
    private int connTimeout;

    @Value("${elasticsearch.socketTimeout}")
    private int socketTimeout;

    @Value("${elasticsearch.connectionRequestTimeout}")
    private int connectionRequestTimeout;

    @Bean(destroyMethod = "close", name = "client")
    public RestHighLevelClient initRestClient() {
        RestClientBuilder builder = RestClient.builder(new HttpHost(host, port))
                .setRequestConfigCallback(requestConfigBuilder -> requestConfigBuilder
                        .setConnectTimeout(connTimeout)
                        .setSocketTimeout(socketTimeout)
                        .setConnectionRequestTimeout(connectionRequestTimeout));
        return new RestHighLevelClient(builder);
    }
}

创建Elasticsearch调用类ElasticsearchService

import com.abel.demo.common.BusinessException;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequest;
import org.elasticsearch.action.bulk.BulkRequest;
import org.elasticsearch.action.bulk.BulkResponse;
import org.elasticsearch.action.delete.DeleteRequest;
import org.elasticsearch.action.delete.DeleteResponse;
import org.elasticsearch.action.get.GetRequest;
import org.elasticsearch.action.get.GetResponse;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.index.IndexResponse;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.action.support.master.AcknowledgedResponse;
import org.elasticsearch.action.update.UpdateRequest;
import org.elasticsearch.action.update.UpdateResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.client.indices.CreateIndexRequest;
import org.elasticsearch.client.indices.CreateIndexResponse;
import org.elasticsearch.client.indices.GetIndexRequest;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.rest.RestStatus;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.collapse.CollapseBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

/**
 * 索引:相当于数据库中的表名
 * mapping:是用于定义 ES 对索引中字段的存储类型、分词方式和是否存储等信息,就像数据库中的 Schema ,描述了文档可能具有的字段或属性、每个字段的数据类型
 * 文档:相当于表中的每行记录数据
 */
@Slf4j
@Component
public class ElasticsearchService {
    @Autowired
    private RestHighLevelClient client;

    private final String DOC_TYPE = "_doc";

    /**
     * 删除索引
     * 注意:删除索引时会把所属的文档数据一并删除
     * @param index
     * @return
     * @throws IOException
     */
    public boolean deleteUserIndex(String index) throws IOException {
        DeleteIndexRequest deleteIndexRequest = new DeleteIndexRequest(index);
        AcknowledgedResponse deleteIndexResponse = client.indices().delete(deleteIndexRequest, RequestOptions.DEFAULT);
        return deleteIndexResponse.isAcknowledged();
    }

    /**
     * 索引是否存在
     * @param index
     * @return
     * @throws IOException
     */
    public boolean existsIndex(String index) throws IOException {
        GetIndexRequest getRequest = new GetIndexRequest(index);
        boolean exists = client.indices().exists(getRequest, RequestOptions.DEFAULT);
        return exists;
    }

    /**
     * 创建索引
     * @param index 索引名称,相当于数据库表名。必须是小写字母
     * @param builderMapping mapping配置,定义字段的数据类型和分词器指定
     *                       中文IK分词器:[ik_smart:最小分词,ik_max_word:最大分词]
     * @return
     * @throws IOException
     */
    public boolean createIndex(String index, XContentBuilder builderMapping) throws IOException {
        if(existsIndex(index)){
            throw new BusinessException(String.format("索引[%s]已存在", index));
        }

        CreateIndexRequest createIndexRequest = new CreateIndexRequest(index);

        //设置mapping参数
        createIndexRequest.mapping(builderMapping);

        /**
         * 设置分片和副本
         * 分片:当索引上的数据量太大的时候,ES 通过水平拆分的方式将一个索引上的数据拆分出来分配到不同的数据块上,拆分出来的数据库块称之为一个分片
         *      分片相当于mysql数据中的水平分表
         * 副本:副本就是对分片的 Copy,每个主分片都有一个或多个副本分片,当主分片异常时,副本可以提供数据的查询等操作
         */
        createIndexRequest.settings(Settings.builder().put("number_of_shards", 1).put("number_of_replicas", 0));

        CreateIndexResponse createIndexResponse = client.indices().create(createIndexRequest, RequestOptions.DEFAULT);
        return createIndexResponse.isAcknowledged();
    }

    /**
     * 创建文档
     * @param index 索引
     * @param docId 文档id
     * @param document 文档内容
     * @return
     * @throws IOException
     */
    public RestStatus createDoc(String index, String docId, Object document) throws IOException {
        IndexRequest indexRequest = new IndexRequest(index, DOC_TYPE)
                .id(docId)
                .source(JSON.toJSONString(document), XContentType.JSON);

        IndexResponse indexResponse = client.index(indexRequest, RequestOptions.DEFAULT);

        return indexResponse.status();
    }

    /**
     * 批量生成文档
     * @param index 索引
     * @param documentList 文档集合
     * @param idField 文档对象中的id字段名
     * @return
     * @throws IOException
     */
    public boolean createDoc(String index, List<?> documentList, String idField) throws IOException {
        BulkRequest request = new BulkRequest();

        for (Object doc : documentList) {
            String jsonStr = JSON.toJSONString(doc);
            String id = JSON.parseObject(jsonStr).getString(idField);
            request.add(new IndexRequest(index, DOC_TYPE)
                    .id(id)
                    .source(jsonStr, XContentType.JSON));
        }

        BulkResponse bulkResponse = client.bulk(request, RequestOptions.DEFAULT);

        return !bulkResponse.hasFailures();
    }

    /**
     * 更新文档
     * @param index 索引
     * @param docId 文档id
     * @param document 文档内容
     * @return
     * @throws IOException
     */
    public RestStatus updateDoc(String index, String docId, Object document) throws IOException {
        UpdateRequest updateRequest = new UpdateRequest(index, DOC_TYPE, docId);
        updateRequest.doc(JSON.toJSONString(document), XContentType.JSON);

        UpdateResponse response = client.update(updateRequest, RequestOptions.DEFAULT);

        return response.status();
    }

    /**
     * 删除文档
     * @param index 索引
     * @param docId 文档id
     * @return
     * @throws IOException
     */
    public RestStatus delDoc(String index, String docId) throws IOException {
        if(existsDoc(index, docId)) {
            DeleteRequest deleteRequest = new DeleteRequest(index, DOC_TYPE, docId);
            DeleteResponse response = client.delete(deleteRequest, RequestOptions.DEFAULT);

            return response.status();
        }
        return null;
    }

    /**
     * 文档是否存在
     * @param index
     * @param id
     * @return
     * @throws IOException
     */
    public boolean existsDoc(String index, String id) throws IOException {
        GetRequest request = new GetRequest(index, DOC_TYPE, id);
        GetResponse response = client.get(request, RequestOptions.DEFAULT);

        return response.isExists();
    }

    /**
     * 根据id返回单个文档对象
     * @param index
     * @param id
     * @param clazz
     * @param <T>
     * @return
     * @throws IOException
     */
    public <T> T getDoc(String index, String id, Class<T> clazz) throws IOException {
        GetRequest request = new GetRequest(index, DOC_TYPE, id);
        GetResponse response = client.get(request, RequestOptions.DEFAULT);
        if(response.isExists() && !StringUtils.isEmpty(response.getSourceAsString())){
            return JSONObject.parseObject(response.getSourceAsString(), clazz);
        }

        return null;
    }



    /**
     * 无条件,搜索
     * @param index 索引
     * @param size 返回条数
     * @return
     * @throws IOException
     */
    public List<SearchHit> searchAll(String index, int size) throws IOException{
        SearchSourceBuilder searchBuilder = new SearchSourceBuilder();
        SearchRequest searchRequest = new SearchRequest(index);
        searchBuilder.query(QueryBuilders.matchAllQuery()).size(size);
        searchRequest.source(searchBuilder);
        // 同步查询
        SearchResponse response = client.search(searchRequest, RequestOptions.DEFAULT);
        SearchHits hits = response.getHits();
        List<SearchHit> list = new ArrayList<>();
        for (SearchHit hit: hits){
            list.add(hit);
        }

        return list;
    }

    /**
     * 条件搜索
     * @param index 索引
     * @param queryBuilder 条件构造器
     * @param page 页码
     * @param size 每页返回条数
     * @return
     * @throws IOException
     */
    public SearchHits search(String index, QueryBuilder queryBuilder, int page, int size) throws IOException {
        SearchSourceBuilder searchBuilder = new SearchSourceBuilder();
        SearchRequest searchRequest = new SearchRequest(index);
        searchBuilder.query(queryBuilder)
//                .sort("field", SortOrder.DESC) //排序方式
                .from((page -1) * size)
                .size(size);

        searchRequest.source(searchBuilder);
        SearchResponse response = client.search(searchRequest, RequestOptions.DEFAULT);
        SearchHits hits = response.getHits();


        return hits;
    }

    /**
     * 条件搜索并去重,此处返回分页还有点问题,其分页结果是去重前的
     * @param index 索引
     * @param queryBuilder 条件构造器
     * @param distinctField 去重字段
     * @param page
     * @param size
     * @return
     * @throws IOException
     */
    public SearchHits searchAndDistinct(String index, QueryBuilder queryBuilder, String distinctField, int page, int size) throws IOException {
        SearchSourceBuilder searchBuilder = new SearchSourceBuilder();
        SearchRequest searchRequest = new SearchRequest(index);
        searchBuilder.query(queryBuilder)
//                .sort("field", SortOrder.DESC) //排序方式
                .from((page -1) * size)
                .size(size);

        CollapseBuilder collapseBuilder = new CollapseBuilder(distinctField);
        searchBuilder.collapse(collapseBuilder);

        searchRequest.source(searchBuilder);
        SearchResponse response = client.search(searchRequest, RequestOptions.DEFAULT);
        SearchHits hits = response.getHits();

        return hits;
    }


}

使用示例

  1. 创建索引并配置mapping
@Autowired
private ElasticsearchService elasticsearchService;

//定义索引名称
private String index = "test_index";

@Test
public void createdIndexTest() throws IOException {
   /**
    * 配置mapping
    * 常用字段数据类型:
    * 字符串类:
    *    text: 用于索引全文值的字段,这些字段是被分词的,它们通过分词器传递
    *    keyword: 用于索引结构化内容的字段,它们通常用于过滤,排序,和聚合。Keyword 字段只能按其确切值进行搜索
    *
    * 数值类 [long、integer、short、byte、double、float、half_float、scaled_float]
    * 范围类 [integer_range、float_range、long_range、double_range、date_range]
    * 日期类 [date、date_nanos]
    * 布尔 [boolean]
    *
    */
   XContentBuilder builder = XContentFactory.jsonBuilder();
   //builder的每个startObject()方法必须配对一个endObject()
   builder.startObject()
         .startObject("properties")  //此处固定为properties

         .startObject("title")  //定义字段名
         .field("type", "text") //定义字段类型
         .field("analyzer", "ik_smart") //配置分词器[ik_smart:最小分词,ik_max_word:最大分词]
         .endObject()

         .startObject("summary")
         .field("type", "keyword")
         .endObject()

         .startObject("id")
         .field("type", "integer")
         .endObject()

         .endObject()
         .endObject();

   //创建索引
   elasticsearchService.createIndex(index, builder);
}
  1. 创建文档
@Test
public void createdDocTest() throws IOException {
   //创建单条文档
   Map<String, Object> docMap = new HashMap<>();
   docMap.put("id", 1);
   docMap.put("title", "标题");
   docMap.put("summary", "简要描述说明");
   elasticsearchService.createDoc(index, docMap, docMap.get("id").toString());
   
   //批量创建多条文档
   List<Map<String, Object>> list = new ArrayList<>();
   for (int i=1;i<=5;i++) {
      Map<String, Object> docItemMap = new HashMap<>();
      docItemMap.put("id", i);
      docItemMap.put("title", "标题"+i);
      docItemMap.put("summary", "简要描述说明"+i);
      list.add(docItemMap);
   }
   elasticsearchService.createDoc(index, list, "id");
   
}
  1. 搜索
@Test
public void searchTest() throws IOException {
   /**
    * QueryBuilders常用条件说明
    *        QueryBuilders.termQuery:精准匹配相相当于“=”
    *        QueryBuilders.termsQuery:精准匹配多个值相相当于“in”
    *        QueryBuilders.matchQuery:使用分词匹配
    *        QueryBuilders.multiMatchQuery:使用分词匹配多个字段
    *        QueryBuilders.rangeQuery("field").from("min").to("max"):范围查询
    *        QueryBuilders.boolQuery:将多个查询条件组合起来
    *                           must:必须匹配must所包括的查询条件,相当于 “AND”
    *                           should:只须匹配should所包括的查询条件其中的一个或多个,相当于 "OR"
    *                           mustNot:不能匹配must_not所包括的该查询条件,相当于“NOT”
    */

   //单条件查询

   //id in(1,2,3)
   //QueryBuilder singleQueryBuilder = QueryBuilders.termsQuery("id", 1,2,3);

   //id>2 and id <4
   //QueryBuilder singleQueryBuilder = QueryBuilders.rangeQuery("id").from(2).to(4);

   // title like '%标题%' or summary like '%标题%'
   //QueryBuilder singleQueryBuilder = QueryBuilders.multiMatchQuery("标题", "title", "summary");

   //title="标题", 若是还被分词,可加上 .keyword 如 QueryBuilders.termQuery("title.keyword", "标题")
   QueryBuilder singleQueryBuilder = QueryBuilders.termQuery("title", "标题");

   SearchHits result0 = elasticsearchService.search(index, singleQueryBuilder, 1, 20);
   System.out.println("单条件搜索命中条数:"+ result0.getTotalHits());
   System.out.println("======单条件命中结果如下======");
   for (SearchHit hit : result0.getHits()) {
      System.out.println(hit.getSourceAsString());
   }


   //多条件组合查询, id=1 and summary like '%描述%'
   QueryBuilder boolQueryBuilder = QueryBuilders.boolQuery()
         .must(QueryBuilders.termQuery("id", 1))
         .must(QueryBuilders.matchQuery("summary", "描述"));
   SearchHits result1 = elasticsearchService.search(index, boolQueryBuilder, 1, 20);
   System.out.println("多条件组合查询搜索命中条数:"+ result1.getTotalHits());
   System.out.println("======多条件组合查询命中结果如下======");
   for (SearchHit hit : result1.getHits()) {
      System.out.println(hit.getSourceAsString());
   }

}

完整源码见 gitee.com/abel-lin/sp…