Spring Boot + ElasticSearch实战之CRUD及多数据源配置

  • Post author:
  • Post category:其他




概述

本文记录工作中使用Spring Boot + ElasticSearch的实战,Spring Boot版本:2.1.6.RELEASE。



基础

Spring Boot已是Java开发标配,使用SB提供的starter,简单高效。



配置

引入依赖:

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>

增加配置文件:

spring:
  data:
    elasticsearch:
      cluster-nodes: 100.200.33.203:9300,100.200.33.204:9300,100.200.33.205:9300
      cluster-name: elasticsearch6
      repositories:
        enabled: true

定义实体类:

import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;

@Data
@Document(indexName="symptom-inquiry", type="symptom", createIndex = false)
public class SymptomEntity {
    /**
     * 根据key由指定hash算法生成
     */
    @Id
    private Long id;
    private String key;
    // 省略其他字段定义
}

主键Id生成算法util工具类:

import com.google.common.base.Charsets;
import com.google.common.hash.HashCode;
import com.google.common.hash.HashFunction;
import com.google.common.hash.Hasher;
import com.google.common.hash.Hashing;

@Slf4j
public class HashUtils {
    /**
     * 针对 SymptomEntity key 计算hash值
     */
    public static Long getHashCode(String key) {
        if (StringUtils.isEmpty(key)) {
            return 0L;
        }
        /*
         * 使用 MURMUR3_128 hash算法计算hash值
         */
        HashFunction function = Hashing.murmur3_128();
        Hasher hasher = function.newHasher();
        HashCode code = hasher.putString(key, Charsets.UTF_8).hash();
        log.info("目标 key = {}, 生成ID = {}", key, code.asLong());
        return code.asLong();
    }
}

基于Repository提供一个空实现的查询接口类:

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;

import java.util.Optional;

@Configuration
public interface SymptomSearchRepository extends ElasticsearchRepository<SymptomEntity, Long> {
}

根据业务需要,上述接口里可以新增自定义查询方法,满足命名规范即可。如,findByFieldname,Filedname是SymptomEntity实体类里面定义的字段,也是ES文档的字段:

    Page<SymptomEntity> findByKey(String key, Pageable pageable);
    Optional<SymptomEntity> findByDescription(String description);

使用:

@Resource
private SymptomSearchRepository symptomSearchRepository;
@Test
public void test() {
	// 基础使用
    symptomSearchRepository.saveAll();
    symptomSearchRepository.save();
    symptomSearchRepository.findAll();
    symptomSearchRepository.findById(1L);

	// 进阶使用, 拼接组装ES SQL
	BoolQueryBuilder allBoolQueryBuilder = QueryBuilders.boolQuery();
	BoolQueryBuilder bodyTagBoolQueryBuilder = this.setBodyTagBoolQueryBuilder(Sets.newHashSet("手", "胳膊"));
	BoolQueryBuilder sexBoolQueryBuilder = this.setSexBoolQueryBuilder(Sex.MALE);
	allBoolQueryBuilder.should(bodyTagBoolQueryBuilder);
	allBoolQueryBuilder.must(sexBoolQueryBuilder);
    symptomSearchRepository.search(allBoolQueryBuilder);
}

private BoolQueryBuilder setSexBoolQueryBuilder(Sex sex) {
    BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
    String notSex = Sex.getNotSex(sex);
    boolQueryBuilder.mustNot(QueryBuilders.matchPhraseQuery("sex", notSex));
    return boolQueryBuilder;
}

private BoolQueryBuilder setBodyTagBoolQueryBuilder(Set<String> allBodyTags) {
    BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
    /*
     * 部位匹配逻辑 封装 bodyTagBoolQueryBuilder
     */
    for (String bodyTag : allBodyTags) {
        QueryBuilder queryBuilderSearchCondition = QueryBuilders.termQuery("bodyTags", bodyTag);
        boolQueryBuilder.should(queryBuilderSearchCondition);
    }
    return boolQueryBuilder;
}

上述API封装得到的ES SQL如下:

{
  "bool" : {
    "must" : [
      {
        "bool" : {
          "must_not" : [
            {
              "match_phrase" : {
                "sex" : {
                  "query" : "FEMALE",
                  "slop" : 0,
                  "zero_terms_query" : "NONE",
                  "boost" : 1.0
                }
              }
            }
          ],
          "adjust_pure_negative" : true,
          "boost" : 1.0
        }
      }
    ],
    "should" : [
      {
        "bool" : {
          "should" : [
            {
              "term" : {
                "bodyTags" : {
                  "value" : "手",
                  "boost" : 1.0
                }
              }
            },
            {
              "term" : {
                "bodyTags" : {
                  "value" : "胳膊",
                  "boost" : 1.0
                }
              }
            }
          ],
          "adjust_pure_negative" : true,
          "boost" : 1.0
        }
      }
    ],
    "adjust_pure_negative" : true,
    "boost" : 1.0
  }
}



kibana + ES 使用



Dev-Tools

上面拼接的SQL,可以在ES可视化平台,执行并查看数据。

ES可视化客户端工具,首选一般就是Kibana。Dev-Tools,提供自动补全提示功能,支持手写SQL语法,适合有要求的开发者。

在这里插入图片描述

常用的语法:

  • 查询
POST symptom-inquiry/_search
{
  "query": {
    "match": {
      "commonName": "腹痛"
    }
  }
}
  • 删除:以主键Id删除
POST symptom-inquiry/_delete_by_query
{
  "query": {
    "match": {
      "_id": "2193474183642781887"
    }
  }
}
  • 新增
PUT symptom-inquiry-stg/symptom/<_id>
{
  "_id": "111",
  "commonName": "aaa",
  "key": "find:ss"
}



Discover

除了上面的Dev-Tools方式,还提供如下简单点点点操作的Discover

在这里插入图片描述



配置2

除了上面这种使用spring-data-jpa提供的封装好的

数据源配置类的另一种方式:

@Configuration
public class EsConfig {
    @Bean(name = "stgHigh", destroyMethod = "close")
    public RestHighLevelClient stgHigh() {
        final CredentialsProvider provider = new BasicCredentialsProvider();
        provider.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials("", ""));
        RestClientBuilder builder = RestClient.builder(
                new HttpHost("100.199.222.111", 9200),
                new HttpHost("100.199.222.112", 9200),
                new HttpHost("100.199.222.113", 9200));
        builder.setHttpClientConfigCallback(inner -> inner.setDefaultCredentialsProvider(provider));
        return new RestHighLevelClient(builder);
    }
}

使用上面的,测试程序如下:

@Autowired
@Qualifier("stgHigh")
private RestHighLevelClient restHighLevelClient;

@Test
public void test() {
	SearchRequest searchRequest = new SearchRequest("symptom-inquiry");
    searchRequest.source().size(1500);
    SearchResponse stgResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
    JSONObject stgJson = (JSONObject) JSON.parse(stgResponse.toString());
    JSONObject stgHits = (JSONObject) stgJson.get("hits");
    Integer total = stgHits.getInteger("total");
    JSONArray stgArray = stgHits.getJSONArray("hits");
    for (int i = 0; i < total; i++) {
        JSONObject item = (JSONObject) stgArray.get(i);
        SymptomEntity stgEntity = JSON.parseObject(item.getString("_source"), SymptomEntity.class);
        if (StringUtils.isEmpty(stgEntity.getKey()) || StringUtils.isEmpty(stgEntity.getCommonName())) {
            log.info("symptomEntity {}", JsonUtil.beanToJson(item));
            continue;
        }
    }
}



多数据配置

如下代码

@Configuration
public class EsConfig {
    @Primary
    @Bean(name = "prod", destroyMethod = "close")
    public RestClient prod() {
        final CredentialsProvider provider = new BasicCredentialsProvider();
        provider.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials("es7", "root"));
        RestClientBuilder clientBuilder = RestClient.builder(
                new HttpHost("170.188.116.203", 9200),
                new HttpHost("170.188.117.204", 9200),
                new HttpHost("170.188.118.205", 9200));
        // 根据需求配置身份验证
        clientBuilder.setHttpClientConfigCallback(builder -> builder.setDefaultCredentialsProvider(provider));
        return clientBuilder.build();
    }

    @Bean(name = "stg", destroyMethod = "close")
    public RestClient stg() {
        final CredentialsProvider provider = new BasicCredentialsProvider();
        provider.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials("es6", "root"));
        RestClientBuilder builder = RestClient.builder(
                new HttpHost("100.119.222.11", 9200),
                new HttpHost("100.119.222.12", 9200),
                new HttpHost("100.119.222.13", 9200));
        builder.setHttpClientConfigCallback(inner -> inner.setDefaultCredentialsProvider(provider));
        return builder.build();
    }
}

使用上面配置好的两个ES数据源,从

stg



prod

两个数据源取数:

@Component
public class queryFromStgAndProdService {
    @Autowired
    @Qualifier("prod")
    private RestClient prodClient;

    @Autowired
    @Qualifier("stg")
    private RestClient stgClient;
}



参考



版权声明:本文为lonelymanontheway原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。