版本
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>
客戶端配置
這裏我們也採用官方推薦的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,不懂看官方
這裏只講我使用的方法1,如果你對jpa或者springdata比較陌生,又想可以惡補一下官方文檔對於查詢的指引:
爲了便於我們重複使用,這裏定義一個
基類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對數據進行序列化會找不到無參構造方法導致序列化失敗)