spring data elasticsearch詳細接入方法

版本

spingboot 2.2.2.RELEASE

引入 spring-boot-starter-data-elasticsearch 可以不指定版本,工程會自動拉取springboot對應的版本依賴

elasticsearch server 6.8.4

如果指定使用版本,要注意兼容性問題,防止不兼容導致出現千奇百怪的錯誤

spring data 官方版本對照表,如果是新項目建議選用spring推薦的幾個版本,太老的不值得接入和維護,成本太高,

官方版本地址:https://spring.io/projects/spring-data-elasticsearch#learn

如果你的版本較新,請查看官方地址:https://docs.spring.io/spring-data/elasticsearch/docs/3.2.7.RELEASE/reference/html/#new-features.3-2-0

maven依賴

放到你項目的model層,因爲springdata需要進行es實體類註解

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

客戶端配置

先附上官方文檔:https://docs.spring.io/spring-data/elasticsearch/docs/3.2.7.RELEASE/reference/html/#elasticsearch.clients.rest

這裏我們也採用官方推薦的REST Client方式,可能有些人還在使用配置文件的方式配置es連接信息,不過這裏我們不推薦,

該方式已經被官方廢棄了,請看:

還有一些老項目在使用Transport Client,我這裏也不推薦使用,至於原因,官方文檔說的很清楚:

我們着重來看下REST Client配置方式

方法1 :一般來說,不需要驗證的這種方式就可以了,自己可以將host和port定義到配置中

方法2:如果需要驗證,那麼就需要在HttpHeader添加用戶名和密碼,一般公司做了ES中間件或者做了權限的話通常都要驗證

只需要在1中添加方法2框出來的紅色配置即可,沒有驗證的es服務不需要方法1直接使用足以。

我的使用demo,我將配置信息放入配置文件的自定義配置中,便於不同環境的切換,此爲我的本地配置:

配置類注意需要添加:

@EnableElasticsearchRepositories 進行掃包,掃到實體類映射層,原理和jpa、mybatis一樣
@Configuration
@EnableElasticsearchRepositories(basePackages = "com.tino.repository")
public class EsRestClientConfig extends AbstractElasticsearchConfiguration {

    @Value("${custom.elasticsearch.server}")
    private String server;

    @Value("${custom.elasticsearch.username}")
    private String userName;

    @Value("${custom.elasticsearch.password}")
    private String password;

    @Override
    @Bean
    public RestHighLevelClient elasticsearchClient() {
        // 設置用戶名密碼
        HttpHeaders defaultHeaders = new HttpHeaders();
        defaultHeaders.setBasicAuth(userName, password);
        final ClientConfiguration clientConfiguration = ClientConfiguration.builder()
                .connectedTo(server)
                .withDefaultHeaders(defaultHeaders)
                .build();
        return RestClients.create(clientConfiguration).rest();
    }
}

如何使用

實體類:假設爲公司信息,這裏我繼承的實體是mongodb的實體,當然也可以是mybatis和jpa的,爲什麼不直接使用數據庫的實體映射呢?兩點:

1.我需要對多表數據做聚合,products屬性爲公司經營的產品,公司的產品我也希望可以參與到全文檢索中。

2.類和字段映射存在耦合和干擾,可以爲全文檢索業務和數據庫增刪改查業務解偶,比如,products我希望在es中持久化、在mongodb中不持久化,因爲mongodb中produs存在單獨的業務表中,那麼不分開的話,靠@Transient

@Transient
private List<ProductModel> products;

肯定無法解決這個問題,耦合度太高,擴展性被限制死了,想要增加更多內容,就不得不得通過其他方式,繁瑣且低效。

當然我這種方式也不是最好的辦法。數據同步也可以,不過那就更依賴運維和公共組件。

基類,注意兩種@Id配置

mongodb實體

/**
 * 公司信息 mongodb實體
 *
 * @author tino
 * @date 2020/4/14
 */
@org.springframework.data.mongodb.core.mapping.Document(collection = "gls_company_info")
public class CompanyInfoModel extends BaseModel {

    /**
     * 公司編號
     */
    private Long companyCode;

    /**
     * 公司名稱
     */
    private String companyName;

    /**
     * 公司logo
     */
    private String logo;

    /**
     * 組織機構代碼
     */
    private String orgCode;

    public Long getCompanyCode() {
        return companyCode;
    }

    public void setCompanyCode(Long companyCode) {
        this.companyCode = companyCode;
    }

    public String getCompanyName() {
        return companyName;
    }

    public void setCompanyName(String companyName) {
        this.companyName = companyName;
    }

    public String getLogo() {
        return logo;
    }

    public void setLogo(String logo) {
        this.logo = logo;
    }

    public String getOrgCode() {
        return orgCode;
    }

    public void setOrgCode(String orgCode) {
        this.orgCode = orgCode;
    }
}

es實體

/**
 * 全文檢索公司信息實體
 *
 * @author tino
 * @date 2020/4/22
 */
@org.springframework.data.elasticsearch.annotations.Document(indexName = "gls", type = "gls_company_info", shards = 1, replicas = 0)
public class EsCompanyModel extends CompanyInfoModel {

    public EsCompanyModel() {
    }

    public EsCompanyModel(List<ProductModel> products) {
        this.products = products;
    }

    /**
     * 公司產品
     */
    private List<ProductModel> products;

    public List<ProductModel> getProducts() {
        return products;
    }

    public void setProducts(List<ProductModel> products) {
        this.products = products;
    }

 

repository

方法1:和jpa一樣,需要什麼繼承什麼,我這裏需要分頁和自定義檢索功能,所以我繼承

ElasticsearchRepository 
PagingAndSortingRepository

方法2:如果你需要springdata 根據方法名稱查詢的功能,繼承 

ElasticsearchCrudRepository

方法3:elasticsearchTemplate,喜歡的可以用,靈活性高。需要注入elasticsearchTemplate bean,不懂看官方

https://docs.spring.io/spring-data/elasticsearch/docs/3.2.7.RELEASE/reference/html/#elasticsearch.query-methods

這裏只講我使用的方法1,如果你對jpa或者springdata比較陌生,又想可以惡補一下官方文檔對於查詢的指引:

https://docs.spring.io/spring-data/elasticsearch/docs/3.2.7.RELEASE/reference/html/#elasticsearch.query-methods

爲了便於我們重複使用,這裏定義一個

基類Repository

/**
 * es基礎接口
 *
 * @author tino
 * @date 2020/4/22
 */
public interface BaseEsDao<E, ID extends Serializable> extends ElasticsearchRepository<E, ID>, PagingAndSortingRepository<E, ID> {
}

如果你想使用方法2,可以再定義一個基類 繼承 ElasticsearchCrudRepository:

BaseCrudRepository

命名爲dao只是個人習慣,個人可以Repository、Mapper隨意。

公司Repository

@Repository注入到spring容器,如果是mybatis項目也不要使用@Mapper,應該沒人那麼傻吧,哈哈哈!
/**
 * es公司信息接口
 *
 * @author tino
 * @date 2020/4/22
 */
@Repository
public interface EsCompanyDao extends BaseEsDao<EsCompanyModel, String> {
}

service層

/**
 * 公司信息搜素接口
 *
 * @author tino
 * @date 2020/4/22
 */
public interface EsSearchService extends BaseService {

    /**
     * 保存索引
     *
     * @param companyInfoModel
     */
    void saveIndex(CompanyInfoModel companyInfoModel);

    /**
     * 根據內容分頁檢索結果
     *
     * @param categoryType
     * @param content
     * @param pageInfo
     * @return
     */
    Page<EsCompanyModel> search(Integer categoryType, String content, PageInfo pageInfo);
}

BaseService寫不寫無所謂,裏面我只放了logger,擅用基類是一種很好的設計模式

service實現

/**
 * es搜索
 *
 * @author tino
 * @date 2020/4/22
 */
@Service
public class EsSearchServiceImpl extends BaseServiceImpl implements EsSearchService {

    @Autowired
    private EsCompanyDao esCompanyDao;

    @Autowired
    private ProductService productService;

    @Autowired
    private CompanyInfoService companyInfoService;

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void saveIndex(CompanyInfoModel companyInfoModel) {
        if (null == companyInfoModel
                || StringUtils.isBlank(companyInfoModel.getId())
                || null == companyInfoModel.getUserCode()
                || null == companyInfoModel.getCompanyCode()) {
            BaseException.exception(BaseErrorCode.PARAM_ERROR);
            return;
        }
        EsCompanyModel esCompanyModel = new EsCompanyModel();
        BeanUtils.copyProperties(companyInfoModel, esCompanyModel);
        List<ProductModel> productModels = productService.listByCompanyCode(companyInfoModel.getCompanyCode());
        esCompanyModel.setProducts(productModels);
        esCompanyDao.save(esCompanyModel);
    }


    @Override
    public Page<EsCompanyModel> search(Integer categoryType, String content, PageInfo pageInfo) {
        BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
        //  根據產品分裂條件進行精確匹配
        if (null != categoryType) {
            boolQueryBuilder.must(QueryBuilders.multiMatchQuery(categoryType, "products.categoryType"));
        }
        // 構建查詢條件
        if (StringUtils.isNotBlank(content)) {
            boolQueryBuilder.should(QueryBuilders.multiMatchQuery(content, "products.route").boost(4))
                    .should(QueryBuilders.multiMatchQuery(content, "city").boost(3))
                    .should(QueryBuilders.multiMatchQuery(content, "companyName").boost(2))
                    .should(QueryBuilders.multiMatchQuery(content, "address").boost(1));
        }
        // 設置排序規則
        Sort sort = Sort.by(Sort.Direction.DESC, "companyCode", "createDate.keyword");
        // 組織分頁參數
        Pageable pageable = PageRequest.of(pageInfo.getPageNum(), pageInfo.getPageSize(), sort);
        // 搜索,獲取結果
        Page page = new Page(new com.github.pagehelper.Page());
        try {
            // 將分頁對象重構爲與其他模塊相同的分頁數據結構
            org.springframework.data.domain.Page<EsCompanyModel> result = esCompanyDao.search(boolQueryBuilder, pageable);
            page.setList(result.getContent());
            page.setTotal(result.getTotalElements());
            page.setPages(result.getTotalPages());
            page.setPageNum(result.getPageable().getPageNumber());
            page.setPageSize(result.getPageable().getPageSize());
        } catch (Exception e) {
            // 當es中索引爲空時,可能會出現錯誤
            logger.error("在es中未查詢到結果", e);
        }
        return page;
    }
}
BoolQueryBuilder的設計思路和mongodb的 Criteria非常相似,提供鏈式編程方式,使用起來和elasticsearchTemplate一樣靈活
boost(1)表示權重,數字越大越根據該條件查到的結果越靠前

其他查詢的方式有很多種,具體可以查看QueryBuilders的源碼

通過聚合條件搜索,不管是聚合對象還是數組可以通過 【.】 的方式進行連接, 是不是和Criteria非常像?例如products.route

排序需要構建排序對象,支持多條件排序查詢,效果和sql一樣

1.多條件倒敘

Sort sort = Sort.by(Sort.Direction.DESC, "companyCode", "createDate.keyword");

2.多條件多排序

Sort sort = Sort.by(Sort.Direction.DESC, "companyCode")
        .and(Sort.by(Sort.Direction.ASC, "createDate.keyword"));

3.通過子對象種的屬性排序,假設子對象屬性名爲 child

Sort sort = Sort.by(Sort.Direction.DESC, "child.count", "createDate.keyword");

注意:保存索引的時機應該是每次保存公司信息後,如果覺得慢可以通過異步方法更新,注意事務問題。

常見問題

1.根據時間字段排序,出現

Caused by: org.elasticsearch.ElasticsearchException: Elasticsearch exception [type=illegal_argument_exception, reason=Fielddata is disabled on text fields by default. Set fielddata=true on [createDate] in order to load fielddata in memory by uninverting the inverted index. Note that this can however use significant memory. Alternatively use a keyword field instead.]
添加keyword標識後,日期才能參與排序

2.沒有數據是報錯

也是排序字段引起的,排序字段必須存在,此種錯我建議捕獲記錄即可

3.json序列化錯誤

com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `com.yunlsp.gls.api.model.children.CompanyTimesModel` (no Creators, like default construct, exist)

檢查實體類的聚合數組或者對象種是否包含了無參構造方法(如果類中只有有參構造方法時,es對數據進行序列化會找不到無參構造方法導致序列化失敗)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章