第 4-8 課:Spring Boot 集成 ElasticSearch

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 是一個最佳的選擇。

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