ElasticSearch 是一個開源的搜索引擎,建立在一個全文搜索引擎庫 Apache Lucene™ 基礎之上。 Lucene 可以說是當下最先進、高性能、全功能的搜索引擎庫——無論是開源還是私有。
ElasticSearch 使用 Java 編寫的,它的內部使用的是 Lucene 做索引與搜索,它的目的是使全文檢索變得簡單,通過隱藏 Lucene 的複雜性,取而代之提供了一套簡單一致的 RESTful API。
然而,ElasticSearch 不僅僅是 Lucene,並且也不僅僅只是一個全文搜索引擎,它可以被下面這樣準確地形容:
- 一個分佈式的實時文檔存儲,每個字段可以被索引與搜索
- 一個分佈式實時分析搜索引擎
- 能勝任上百個服務節點的擴展,並支持 PB 級別的結構化或者非結構化數據
ElasticSearch 已經被各大互聯網公司驗證其搶到的檢索能力:
- Wikipedia 使用 ElasticSearch 提供帶有高亮片段的全文搜索,還有 search-as-you-type 和 did-you-mean 的建議;
- 《衛報》使用 ElasticSearch 將網絡社交數據結合到訪客日誌中,實時的給編輯們提供公衆對於新文章的反饋;
- Stack Overflow 將地理位置查詢融入全文檢索中去,並且使用 more-like-this 接口去查找相關的問題與答案;
- GitHub 使用 ElasticSearch 對 1300 億行代碼進行查詢。
小故事
關於 ElasticSearch 有一個小故事,在這裏也分享給大家:
多年前,Shay Banon 是一位剛結婚不久的失業開發者,由於妻子要去倫敦學習廚師,他便跟着也去了。在他找工作的過程中,爲了給妻子構建一個食譜的搜索引擎,他開始構建一個早期版本的 Lucene。
直接基於 Lucene 工作會比較困難,因此 Shay 開始抽象 Lucene 代碼以便 Java 程序員可以在應用中添加搜索功能,他發佈了他的第一個開源項目,叫做“Compass”。
後來 Shay 找到了一份工作,這份工作處在高性能和內存數據網格的分佈式環境中,因此高性能的、實時的、分佈式的搜索引擎也是理所當然需要的,然後他決定重寫 Compass 庫使其成爲一個獨立的服務叫做 ElasticSearch。
第一個公開版本出現在 2010 年 2 月,在那之後 ElasticSearch 已經成爲 GitHub上最受歡迎的項目之一,代碼貢獻者超過 300 人。一家主營 ElasticSearch 的公司就此成立,他們一邊提供商業支持一邊開發新功能,不過 ElasticSearch 將永遠開源且對所有人可用。
Shay 的妻子依舊等待着她的食譜搜索……
在沒有 Spring Boot 之前 Java 程序員使用 ElasticSearch 非常痛苦,需要對接鏈接資源、進行一些列的封裝等操作。 Spring Boot 在 spring-data-elasticsearch 的基礎上進行了封裝,讓 Spring Boot 項目非常方便的去操作 ElasticSearch,如果前面瞭解過 JPA 技術的話,會發現他的操作語法和 JPA 非常的類似。
值得注意的是,Spring Data ElasticSearch 和 ElasticSearch 是有對應關係的,不同的版本之間不兼容,Spring Boot 2.1 對應的是 Spring Data ElasticSearch 3.1.2 版本。
Spring Data ElasticSearch | ElasticSearch |
---|---|
3.1.x | 6.2.2 |
3.0.x | 5.5.0 |
2.1.x | 2.4.0 |
2.0.x | 2.2.0 |
1.3.x | 1.5.2 |
Spring Boot 集成 ElasticSearch
相關配置
在 Pom 中添加 ElasticSearch 的依賴:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
配置 ElasticSearch 集羣地址:
# 集羣名(默認值: elasticsearch,配置文件`cluster.name`: es-mongodb)
spring.data.elasticsearch.cluster-name=es-mongodb
# 集羣節點地址列表,用逗號分隔
spring.data.elasticsearch.cluster-nodes=localhost:9300
相關配置
@Document(indexName = "customer", type = "customer", shards = 1, replicas = 0, refreshInterval = "-1")
public class Customer {
@Id
private String id;
private String userName;
private String address;
private int age;
//省略部分 getter/setter
}
- @Document 註解會對實體中的所有屬性建立索引
- indexName = "customer" 表示創建一個名稱爲 "customer" 的索引
- type = "customer" 表示在索引中創建一個名爲 "customer" 的 type
- shards = 1 表示只使用一個分片
- replicas = 0 表示不使用複製
- refreshInterval = "-1" 表示禁用索引刷新
創建操作的 repository
public interface CustomerRepository extends ElasticsearchRepository<Customer, String> {
public List<Customer> findByAddress(String address);
public Customer findByUserName(String userName);
public int deleteByUserName(String userName);
}
我們創建了兩個查詢和一個刪除的方法,從語法可以看出和前面 JPA 的使用方法非常類似,跟蹤 ElasticsearchRepository 的代碼會發現:
ElasticsearchRepository 繼承於 ElasticsearchCrudRepository:
public interface ElasticsearchRepository<T, ID extends Serializable> extends ElasticsearchCrudRepository<T, ID> {...}
而 ElasticsearchCrudRepository 繼承於 PagingAndSortingRepository:
public interface ElasticsearchCrudRepository<T, ID extends Serializable> extends PagingAndSortingRepository<T, ID>{...}
最後 PagingAndSortingRepository 繼承於 CrudRepository:
public interface PagingAndSortingRepository<T, ID> extends CrudRepository<T, ID>{...}
類圖如下:
通過查看源碼發現,ElasticsearchRepository 最終使用和 JPA 操作數據庫使用的父類是一樣的。通過這些也可以發現,Spring Data 項目中的成員在最上層有着統一的接口標準,只是在最終的實現層對不同的數據庫進行了差異化封裝。
以上簡單配置完成之後我們在業務中就可以使用 ElasticSearch 了。
測試 CustomerRepository
創建一個測試類引入 CustomerRepository:
@RunWith(SpringRunner.class)
@SpringBootTest
public class CustomerRepositoryTest {
@Autowired
private CustomerRepository repository;
}
做一個數據插入測試:
@Test
public void saveCustomers() {
repository.save(new Customer("Alice", "北京",13));
repository.save(new Customer("Bob", "北京",23));
repository.save(new Customer("neo", "西安",30));
repository.save(new Customer("summer", "煙臺",22));
}
repository 已經幫我們默認實現了很多的方法,其中就包括 save(); 。
我們對插入的數據做一個查詢:
@Test
public void fetchAllCustomers() {
System.out.println("Customers found with findAll():");
System.out.println("-------------------------------");
for (Customer customer : repository.findAll()) {
System.out.println(customer);
}
}
輸出:
Customers found with findAll():
-------------------------------
Customer{id='aBVS7WYB8U8_i9prF8qm', userName='Alice', address='北京', age=13}
Customer{id='aRVS7WYB8U8_i9prF8rw', userName='Bob', address='北京', age=23}
Customer{id='ahVS7WYB8U8_i9prGMot', userName='neo', address='西安', age=30}
Customer{id='axVS7WYB8U8_i9prGMp2', userName='summer', address='北京市海淀區西直門', age=22}
通過查詢可以發現,插入時自動生成了 ID 信息。
對插入的數據進行刪除:
@Test
public void fetchAllCustomers() {
@Test
public void deleteCustomers() {
repository.deleteAll();
repository.deleteByUserName("neo");
}
}
可以根據屬性條件來刪除,也可以全部刪除。
對屬性進行修改:
@Test
public void updateCustomers() {
Customer customer= repository.findByUserName("summer");
System.out.println(customer);
customer.setAddress("北京市海淀區西直門");
repository.save(customer);
Customer xcustomer=repository.findByUserName("summer");
System.out.println(xcustomer);
}
輸出:
Customer[id=AWKVYFY4vPQX0UVGnJ7o, userName='summer', address='煙臺']
Customer[id=AWKVYFY4vPQX0UVGnJ7o, userName='summer', address='北京市海淀區西直門']
通過輸出發現 summer 用戶的地址信息已經被變更。
我們可以根據地址信息來查詢在北京的顧客信息:
@Test
public void fetchIndividualCustomers() {
for (Customer customer : repository.findByAddress("北京")) {
System.out.println(customer);
}
}
輸出:
Customer{id='aBVS7WYB8U8_i9prF8qm', userName='Alice', address='北京', age=13}
Customer{id='aRVS7WYB8U8_i9prF8rw', userName='Bob', address='北京', age=23}
Customer{id='axVS7WYB8U8_i9prGMp2', userName='summer', address='北京市海淀區西直門', age=22}
通過輸出可以發現 ElasticSearch 默認給我們進行的就是字段全文(模糊)查詢。
通過以上的示例發現使用 Spring Boot 操作 ElasticSearch 非常簡單,通過少量代碼即可實現我們日常大部分的業務需求。
高級使用
上面演示了在 Spring Boot 項目中對 ElasticSearch 的增、刪、改、查操作,通過上面的操作也可以發現操作 ElasticSearch 的語法和 Spring Data JPA 的語法非常類似,下面介紹一些複雜的使用場景。
分頁查詢
分頁查詢有兩種實現方式,第一種是使用 Spring Data 自帶的分頁方案,另一種是自行組織查詢條件最後封裝進行查詢。我們先來看第一個方案:
@Test
public void fetchPageCustomers() {
Sort sort = new Sort(Sort.Direction.DESC, "address.keyword");
Pageable pageable = PageRequest.of(0, 10, sort);
Page<Customer> customers=repository.findByAddress("北京", pageable);
System.out.println("Page customers "+customers.getContent().toString());
}
這段代碼的含義是,分頁查詢地址包含“北京”的客戶信息,並且按照地址進行排序,每頁顯示 10 條。需要注意的是排序是使用的關鍵字是 address.keyword,而不是 address,屬性後面帶 .keyword 代表了精確匹配。
QueryBuilder
我們也可以使用 QueryBuilder 來構建分頁查詢,QueryBuilder 是一個功能強大的多條件查詢構建工具,可以使用 QueryBuilder 構建出各種各樣的查詢條件。
@Test
public void fetchPage2Customers() {
QueryBuilder customerQuery = QueryBuilders.boolQuery()
.must(QueryBuilders.matchQuery("address", "北京"));
Page<Customer> page = repository.search(customerQuery, PageRequest.of(0, 10));
System.out.println("Page customers "+page.getContent().toString());
}
使用 QueryBuilder 可以構建多條件查詢,再結合 PageRequest 最後使用 search() 方法完成分頁查詢。BoolQueryBuilder 有一些關鍵字和 AND、OR、NOT一一對應:
- must(QueryBuilders):AND
- mustNot(QueryBuilders):NOT
- should::OR
QueryBuilder 是一個強大的多條件構建工具,有以下幾種用法。
精確查詢
單個匹配:
//不分詞查詢 參數1: 字段名,參數2:字段查詢值,因爲不分詞,所以漢字只能查詢一個字,英語是一個單詞
QueryBuilder queryBuilder=QueryBuilders.termQuery("fieldName", "fieldlValue");
//分詞查詢,採用默認的分詞器
QueryBuilder queryBuilder2 = QueryBuilders.matchQuery("fieldName", "fieldlValue");
多個匹配:
//不分詞查詢,參數1:字段名,參數2:多個字段查詢值,因爲不分詞,因此漢字只能查詢一個字,英語是一個單詞
QueryBuilder queryBuilder=QueryBuilders.termsQuery("fieldName", "fieldlValue1","fieldlValue2...");
//分詞查詢,採用默認的分詞器
QueryBuilder queryBuilder= QueryBuilders.multiMatchQuery("fieldlValue", "fieldName1", "fieldName2", "fieldName3");
//匹配所有文件,相當於就沒有設置查詢條件
QueryBuilder queryBuilder=QueryBuilders.matchAllQuery();
模糊查詢
模糊查詢常見的 5 個方法如下:
//1.常用的字符串查詢
QueryBuilders.queryStringQuery("fieldValue").field("fieldName");//左右模糊
//2.常用的用於推薦相似內容的查詢
QueryBuilders.moreLikeThisQuery(new String[] {"fieldName"}).addLikeText("pipeidhua");//如果不指定filedName,則默認全部,常用在相似內容的推薦上
//3.前綴查詢,如果字段沒分詞,就匹配整個字段前綴
QueryBuilders.prefixQuery("fieldName","fieldValue");
//4.fuzzy query:分詞模糊查詢,通過增加 fuzziness 模糊屬性來查詢,如能夠匹配 hotelName 爲 tel 前或後加一個字母的文檔,fuzziness 的含義是檢索的 term 前後增加或減少 n 個單詞的匹配查詢
QueryBuilders.fuzzyQuery("hotelName", "tel").fuzziness(Fuzziness.ONE);
//5.wildcard query:通配符查詢,支持* 任意字符串;?任意一個字符
QueryBuilders.wildcardQuery("fieldName","ctr*");//前面是fieldname,後面是帶匹配字符的字符串
QueryBuilders.wildcardQuery("fieldName","c?r?");
範圍查詢
//閉區間查詢
QueryBuilder queryBuilder0 = QueryBuilders.rangeQuery("fieldName").from("fieldValue1").to("fieldValue2");
//開區間查詢
QueryBuilder queryBuilder1 = QueryBuilders.rangeQuery("fieldName").from("fieldValue1").to("fieldValue2").includeUpper(false).includeLower(false);//默認是 true,也就是包含
//大於
QueryBuilder queryBuilder2 = QueryBuilders.rangeQuery("fieldName").gt("fieldValue");
//大於等於
QueryBuilder queryBuilder3 = QueryBuilders.rangeQuery("fieldName").gte("fieldValue");
//小於
QueryBuilder queryBuilder4 = QueryBuilders.rangeQuery("fieldName").lt("fieldValue");
//小於等於
QueryBuilder queryBuilder5 = QueryBuilders.rangeQuery("fieldName").lte("fieldValue");
多條件查詢
QueryBuilders.boolQuery()
QueryBuilders.boolQuery().must();//文檔必須完全匹配條件,相當於 and
QueryBuilders.boolQuery().mustNot();//文檔必須不匹配條件,相當於 not
聚合查詢
聚合查詢分爲五步來實現,我們以統計客戶總年齡爲例進行演示。
第一步,使用 QueryBuilder 構建查詢條件:
QueryBuilder customerQuery = QueryBuilders.boolQuery()
.must(QueryBuilders.matchQuery("address", "北京"));
第二步,使用 SumAggregationBuilder 指明需要聚合的字段:
SumAggregationBuilder sumBuilder = AggregationBuilders.sum("sumAge").field("age");
第三步,以前兩部分的內容爲參數構建成 SearchQuery:
SearchQuery searchQuery = new NativeSearchQueryBuilder()
.withQuery(customerQuery)
.addAggregation(sumBuilder)
.build();
第四步,使用 Aggregations 進行查詢:
Aggregations aggregations = elasticsearchTemplate.query(searchQuery, new ResultsExtractor<Aggregations>() {
@Override
public Aggregations extract(SearchResponse response) {
return response.getAggregations();
}
});
第五步,解析聚合查詢結果:
//轉換成 map 集合
Map<String, Aggregation> aggregationMap = aggregations.asMap();
//獲得對應的聚合函數的聚合子類,該聚合子類也是個 map 集合,裏面的 value 就是桶 Bucket,我們要獲得 Bucket
InternalSum sumAge = (InternalSum) aggregationMap.get("sumAge");
System.out.println("sum age is "+sumAge.getValue());
以上就是聚合查詢的使用方式。
總結
Spring Boot 對 ElasticSearch 的集成延續了 Spring Data 的思想,通過繼承對應的 repository 默認幫我們實現了很多常用操作,通過註解也非常的方便設置索引映射在 ElasticSearch 的數據使用。在大規模搜索中使用 Spring Boot 操作 ElasticSearch 是一個最佳的選擇。