這裏寫目錄標題

  • ElasticSearch
  • 1 DSL查詢
  • 1.1 快速入門
  • 1.2 葉子查詢
  • 1.2.1 全文檢索
  • 1.2.2 精確查詢
  • 1.3 複合查詢
  • 1.3.1 算法函數查詢(瞭解)
  • 1.3.2 bool查詢
  • 1.4 排序
  • 1.5 分頁
  • 1.5.1 基礎分頁
  • 1.5.2 深度分頁
  • 1.6 高亮
  • 1.6.1 高亮原理
  • 1.6.2 實現高亮
  • 1.7 總結
  • 2 RestClient查詢
  • 2.1 快速入門
  • 2.1.1 發送請求
  • 2.1.2 解析響應結果
  • 2.1.3 總結
  • 2.2 葉子查詢
  • 2.3 複合查詢
  • 2.4 排序和分頁
  • 2.5 高亮
  • 3 數據聚合
  • 3.1 DSL實現聚合
  • 3.1.1 Bucket聚合
  • 3.1.2 帶條件聚合
  • 3.1.3 Metric聚合
  • 3.1.4 總結
  • 3.2 RestClient實現聚合
  • 4 作業
  • 4.1 實現搜索接口
  • 4.1.1 SearchController
  • 4.1.2 ISearchService
  • 4.1.3 SearchServiceImpl
  • 4.2 過濾條件聚合
  • 4.2.1 SearchController
  • 4.2.2 ISearchService
  • 4.2.3 SearchServiceImpl
  • 4.3 競價排名(瞭解)

ElasticSearch

在前面ES的學習中,我們已經導入了大量數據到elasticsearch中,實現了商品數據的存儲。不過查詢商品數據時依然採用的是根據id查詢,而非模糊搜索。

所以今天,我們來研究下elasticsearch的數據搜索功能。Elasticsearch提供了基於JSON的DSL(Domain Specific Language)語句來定義查詢條件,其JavaAPI就是在組織DSL條件。

因此,我們先學習DSL的查詢語法,然後再基於DSL來對照學習JavaAPI,就會事半功倍。

1 DSL查詢

Elasticsearch的查詢可以分為兩大類:

  • 葉子查詢(Leaf query clauses):一般是在特定的字段裏查詢特定值,屬於簡單查詢,很少單獨使用。
  • 複合查詢(Compound query clauses):以邏輯方式組合多個葉子查詢或者更改葉子查詢的行為方式。

1.1 快速入門

我們依然在Kibana的DevTools中學習查詢的DSL語法。首先來看查詢的語法結構:

GET /{索引庫名}/_search
{
  "query": {
    "查詢類型": {
      // .. 查詢條件
    }
  }
}

説明:

  • GET /{索引庫名}/_search:其中的_search是固定路徑,不能修改

例如,我們以最簡單的無條件查詢為例,無條件查詢的類型是:match_all,因此其查詢語句如下:

GET /items/_search
{
  "query": {
    "match_all": {
      
    }
  }
}

由於match_all無條件,所以條件位置不寫即可。

執行結果如下:

ElasticSearch:組合查詢或複合查詢 - 個人文章_#elasticsearch

你會發現雖然是match_all,但是響應結果中並不會包含索引庫中的所有文檔,而是僅有10000條。這是因為處於安全考慮,elasticsearch設置了默認的查詢頁數。

1.2 葉子查詢

葉子查詢的類型也可以做進一步細分,詳情大家可以查看官方文檔:https://www.elastic.co/guide/en/elasticsearch/reference/7.12/query-dsl.html

如圖:

ElasticSearch:組合查詢或複合查詢 - 個人文章_#jenkins_02

這裏列舉一些常見的,例如:

  • 全文檢索查詢(Full Text Queries):利用分詞器對用户輸入搜索條件先分詞,得到詞條,然後再利用倒排索引搜索詞條。例如:
  • match
  • multi_match
  • 精確查詢(Term-level queries):不對用户輸入搜索條件分詞,根據字段內容精確值匹配。但只能查找keyword、數值、日期、boolean類型的字段。例如:
  • ids
  • term
  • range
  • 地理座標查詢: 用於搜索地理位置,搜索方式很多,例如:
  • geo_bounding_box:按矩形搜索
  • geo_distance:按點和半徑搜索
  • …略

1.2.1 全文檢索

全文檢索的種類也很多,詳情可以參考官方文檔:https://www.elastic.co/guide/en/elasticsearch/reference/7.12/full-text-queries.html

以全文檢索中的match為例,語法如下:

GET /{索引庫名}/_search
{
  "query": {
    "match": {
      "字段名": "搜索條件"
    }
  }
}

示例:

ElasticSearch:組合查詢或複合查詢 - 個人文章_elasticsearch_03

match類似的還有multi_match,區別在於可以同時對多個字段搜索,有一個字段符合條件即可,語法示例:

GET /{索引庫名}/_search
{
  "query": {
    "multi_match": {
      "query": "搜索條件",
      "fields": ["字段1", "字段2"]
    }
  }
}

示例:

ElasticSearch:組合查詢或複合查詢 - 個人文章_#大數據_04

1.2.2 精確查詢

精確查詢,英文是Term-level query,顧名思義,詞條級別的查詢。也就是説不會對用户輸入的搜索條件再分詞,而是作為一個詞條,與搜索的字段內容精確值匹配。因此推薦查找keyword、數值、日期、boolean類型的字段。例如:

  • id
  • price
  • 城市
  • 地名
  • 人名

等等,作為一個整體才有含義的字段。

詳情可以查看官方文檔:https://www.elastic.co/guide/en/elasticsearch/reference/7.12/term-level-queries.html

term查詢為例,其語法如下:

GET /{索引庫名}/_search
{
  "query": {
    "term": {
      "字段名": {
        "value": "搜索條件"
      }
    }
  }
}

示例:

ElasticSearch:組合查詢或複合查詢 - 個人文章_搜索_05

當你輸入的搜索條件不是詞條,而是短語時,由於不做分詞,你反而搜索不到:

ElasticSearch:組合查詢或複合查詢 - 個人文章_#elasticsearch_06

再來看下range查詢,語法如下:

GET /{索引庫名}/_search
{
  "query": {
    "range": {
      "字段名": {
        "gte": {最小值},
        "lte": {最大值}
      }
    }
  }
}

range是範圍查詢,對於範圍篩選的關鍵字有:

  • gte:大於等於
  • gt:大於
  • lte:小於等於
  • lt:小於

示例:

ElasticSearch:組合查詢或複合查詢 - 個人文章_搜索_07

1.3 複合查詢

複合查詢大致可以分為兩類:

  • 第一類:基於邏輯運算組合葉子查詢,實現組合條件,例如
  • bool
  • 第二類:基於某種算法修改查詢時的文檔相關性算分,從而改變文檔排名。例如:
  • function_score
  • dis_max

其它複合查詢及相關語法可以參考官方文檔:https://www.elastic.co/guide/en/elasticsearch/reference/7.12/compound-queries.html

1.3.1 算法函數查詢(瞭解)

當我們利用match查詢時,文檔結果會根據與搜索詞條的關聯度打分_score),返回結果時按照分值降序排列。

例如,我們搜索 “4G手機”,結果如下:

ElasticSearch:組合查詢或複合查詢 - 個人文章_搜索_08

從elasticsearch5.1開始,採用的相關性打分算法是BM25算法,公式如下:

ElasticSearch:組合查詢或複合查詢 - 個人文章_elasticsearch_09

基於這套公式,就可以判斷出某個文檔與用户搜索的關鍵字之間的關聯度,還是比較準確的。但是,在實際業務需求中,常常會有競價排名的功能。不是相關度越高排名越靠前,而是掏的錢多的排名靠前。

例如在百度中搜索Java培訓,排名靠前的就是廣告推廣。

要想認為控制相關性算分,就需要利用elasticsearch中的function score 查詢了。

基本語法

function score 查詢中包含四部分內容:

  • 原始查詢條件:query部分,基於這個條件搜索文檔,並且基於BM25算法給文檔打分,原始算分(query score)
  • 過濾條件:filter部分,符合該條件的文檔才會重新算分
  • 算分函數:符合filter條件的文檔要根據這個函數做運算,得到的函數算分(function score),有四種函數
  • weight:函數結果是常量
  • field_value_factor:以文檔中的某個字段值作為函數結果
  • random_score:以隨機數作為函數結果
  • script_score:自定義算分函數算法
  • 運算模式:算分函數的結果、原始查詢的相關性算分,兩者之間的運算方式,包括:
  • multiply:相乘
  • replace:用function score替換query score
  • 其它,例如:sum、avg、max、min

function score的運行流程如下:

  • 1)根據原始條件查詢搜索文檔,並且計算相關性算分,稱為原始算分(query score)
  • 2)根據過濾條件,過濾文檔
  • 3)符合過濾條件的文檔,基於算分函數運算,得到函數算分(function score)
  • 4)將原始算分(query score)和函數算分(function score)基於運算模式做運算,得到最終結果,作為相關性算分。

因此,其中的關鍵點是:

  • 過濾條件:決定哪些文檔的算分被修改
  • 算分函數:決定函數算分的算法
  • 運算模式:決定最終算分結果

示例:給IPhone這個品牌的手機算分提高十倍,分析如下:

  • 過濾條件:品牌必須為IPhone
  • 算分函數:常量weight,值為10
  • 算分模式:相乘multiply

對應代碼如下:

GET /items/_search
{
  "query": {
    "function_score": {
      "query": {  .... }, // 原始查詢,可以是任意條件
      "functions": [ // 算分函數
        {
          "filter": { // 滿足的條件,品牌必須是Iphone
            "term": {
              "brand": "Iphone"
            }
          },
          "weight": 10 // 算分權重為2
        }
      ],
      "boost_mode": "multipy" // 加權模式,求乘積
    }
  }
}

1.3.2 bool查詢

bool查詢,即布爾查詢。就是利用邏輯運算來組合一個或多個查詢子句的組合。bool查詢支持的邏輯運算有:

  • must:必須匹配每個子查詢,類似“與”
  • should:選擇性匹配子查詢,類似“或”
  • must_not:必須不匹配,不參與算分,類似“非”
  • filter:必須匹配,不參與算分

bool查詢的語法如下:

GET /items/_search
{
  "query": {
    "bool": {
      "must": [
        {"match": {"name": "手機"}}
      ],
      "should": [
        {"term": {"brand": { "value": "vivo" }}},
        {"term": {"brand": { "value": "小米" }}}
      ],
      "must_not": [
        {"range": {"price": {"gte": 2500}}}
      ],
      "filter": [
        {"range": {"price": {"lte": 1000}}}
      ]
    }
  }
}

出於性能考慮,與搜索關鍵字無關的查詢儘量採用must_not或filter邏輯運算,避免參與相關性算分。

例如黑馬商城的搜索頁面:

ElasticSearch:組合查詢或複合查詢 - 個人文章_elasticsearch_10

其中輸入框的搜索條件肯定要參與相關性算分,可以採用match。但是價格範圍過濾、品牌過濾、分類過濾等儘量採用filter,不要參與相關性算分。

比如,我們要搜索手機,但品牌必須是華為,價格必須是900~1599,那麼可以這樣寫:

GET /items/_search
{
  "query": {
    "bool": {
      "must": [
        {"match": {"name": "手機"}}
      ],
      "filter": [
        {"term": {"brand": { "value": "華為" }}},
        {"range": {"price": {"gte": 90000, "lt": 159900}}}
      ]
    }
  }
}

1.4 排序

elasticsearch默認是根據相關度算分(_score)來排序,但是也支持自定義方式對搜索結果排序。不過分詞字段無法排序,能參與排序字段類型有:keyword類型、數值類型、地理座標類型、日期類型等。

詳細説明可以參考官方文檔:https://www.elastic.co/guide/en/elasticsearch/reference/7.12/sort-search-results.html

語法説明:

GET /indexName/_search
{
  "query": {
    "match_all": {}
  },
  "sort": [
    {
      "排序字段": {
        "order": "排序方式asc和desc"
      }
    }
  ]
}

示例,搜索商品,按照銷量排序,銷量一樣則按照價格升序:

GET /items/_search
{
  "query": {
    "match_all": {}
  }
  , "sort": [
    {
      "sold": {
        "order": "desc"
      },
      "price": {
        "order": "asc"
      }
    }
  ]
}

1.5 分頁

1.5.1 基礎分頁

elasticsearch中通過修改fromsize參數來控制要返回的分頁結果:

  • from:從第幾個文檔開始
  • size:總共查詢幾個文檔

類似於mysql中的limit ?, ?

官方文檔如下:https://www.elastic.co/guide/en/elasticsearch/reference/7.12/paginate-search-results.html

示例語法如下;搜索商品,查詢出銷量排名前10的商品,銷量一樣時按照價格升序:

GET /items/_search
{
  "query": {
    "match_all": {}
  }
  , "sort": [
    {
      "sold": {
        "order": "desc"
      },
      "price": {
        "order": "asc"
      }
    }
  ]
  , "from": 0
  , "size": 10
}

1.5.2 深度分頁

elasticsearch的數據一般會採用分片存儲,也就是把一個索引中的數據分成N份,存儲到不同節點上。這種存儲方式比較有利於數據擴展,但給分頁帶來了一些麻煩。

比如一個索引庫中有100000條數據,分別存儲到4個分片,每個分片25000條數據。現在每頁查詢10條,查詢第99頁。那麼分頁查詢的條件如下:

GET /items/_search
{
  "from": 990, // 從第990條開始查詢
  "size": 10, // 每頁查詢10條
  "sort": [
    {
      "price": "asc"
    }
  ]
}

從語句來分析,要查詢第990~1000名的數據。

從實現思路來分析,肯定是將所有數據排序,找出前1000名,截取其中的990~1000的部分。但問題來了,我們如何才能找到所有數據中的前1000名呢?

要知道每一片的數據都不一樣,第1片上的第9001000,在另1個節點上並不一定依然是9001000名。所以我們只能在每一個分片上都找出排名前1000的數據,然後彙總到一起,重新排序,才能找出整個索引庫中真正的前1000名,此時截取990~1000的數據即可。

如圖:

ElasticSearch:組合查詢或複合查詢 - 個人文章_#jenkins_11

試想一下,假如我們現在要查詢的是第999頁數據呢,是不是要找第9990~10000的數據,那豈不是需要把每個分片中的前10000名數據都查詢出來,彙總在一起,在內存中排序?如果查詢的分頁深度更深呢,需要一次檢索的數據豈不是更多?

由此可知,當查詢分頁深度較大時,彙總數據過多,對內存和CPU會產生非常大的壓力。

因此elasticsearch會禁止from+ size 超過10000的請求。

針對深度分頁,elasticsearch提供了兩種解決方案:

  • search after:分頁時需要排序,原理是從上一次的排序值開始,查詢下一頁數據。(意思是要記錄上一次查詢的最後一條記錄的排序值,然後攜帶到下一次查詢。)官方推薦使用的方式。
  • scroll:原理將排序後的文檔id形成快照,保存下來,基於快照做分頁。官方已經不推薦使用。

詳情見文檔:https://www.elastic.co/guide/en/elasticsearch/reference/7.12/paginate-search-results.html

總結:
大多數情況下,我們採用普通分頁就可以了。查看百度、京東等網站,會發現其分頁都有限制。例如百度最多支持77頁,每頁不足20條。京東最多100頁,每頁最多60條。
因此,一般我們採用限制分頁深度的方式即可,無需實現深度分頁。

1.6 高亮

1.6.1 高亮原理

什麼是高亮顯示呢?

我們在百度,京東搜索時,關鍵字會變成紅色,比較醒目,這叫高亮顯示:

ElasticSearch:組合查詢或複合查詢 - 個人文章_#jenkins_12

觀察頁面源碼,你會發現兩件事情:

  • 高亮詞條都被加了``標籤
  • em標籤都添加了紅色樣式

css樣式肯定是前端實現頁面的時候寫好的,但是前端編寫頁面的時候是不知道頁面要展示什麼數據的,不可能給數據加標籤。而服務端實現搜索功能,要是有elasticsearch做分詞搜索,是知道哪些詞條需要高亮的。

因此詞條的高亮標籤肯定是由服務端提供數據的時候已經加上的

因此實現高亮的思路就是:

  • 用户輸入搜索關鍵字搜索數據
  • 服務端根據搜索關鍵字到elasticsearch搜索,並給搜索結果中的關鍵字詞條添加html標籤
  • 前端提前給約定好的html標籤添加CSS樣式

1.6.2 實現高亮

事實上elasticsearch已經提供了給搜索關鍵字加標籤的語法,無需我們自己編碼。

基本語法如下:

GET /{索引庫名}/_search
{
  "query": {
    "match": {
      "搜索字段": "搜索關鍵字"
    }
  },
  "highlight": {
    "fields": {
      "高亮字段名稱": {
        "pre_tags": "<em>",
        "post_tags": "</em>"
      }
    }
  }
}

注意

  • 搜索必須有查詢條件,而且是全文檢索類型的查詢條件,例如match
  • 參與高亮的字段必須是text類型的字段
  • 默認情況下參與高亮的字段要與搜索字段一致,除非添加:required_field_match=false

示例:

ElasticSearch:組合查詢或複合查詢 - 個人文章_#jenkins_13

1.7 總結

查詢的DSL是一個大的JSON對象,包含下列屬性:

  • query:查詢條件
  • fromsize:分頁條件
  • sort:排序條件
  • highlight:高亮條件

示例:

ElasticSearch:組合查詢或複合查詢 - 個人文章_#大數據_14

2 RestClient查詢

文檔的查詢依然使用昨天學習的 RestHighLevelClient對象,查詢的基本步驟如下:

  • 1)創建request對象,這次是搜索,所以是SearchRequest
  • 2)準備請求參數,也就是查詢DSL對應的JSON參數
  • 3)發起請求
  • 4)解析響應,響應結果相對複雜,需要逐層解析

2.1 快速入門

之前説過,由於Elasticsearch對外暴露的接口都是Restful風格的接口,因此JavaAPI調用就是在發送Http請求。而我們核心要做的就是利用利用Java代碼組織請求參數解析響應結果

這個參數的格式完全參考DSL查詢語句的JSON結構,因此我們在學習的過程中,會不斷的把JavaAPI與DSL語句對比。大家在學習記憶的過程中,也應該這樣對比學習。

2.1.1 發送請求

首先以match_all查詢為例,其DSL和JavaAPI的對比如圖:

ElasticSearch:組合查詢或複合查詢 - 個人文章_搜索_15

代碼解讀:

  • 第一步,創建SearchRequest對象,指定索引庫名
  • 第二步,利用request.source()構建DSL,DSL中可以包含查詢、分頁、排序、高亮等
  • query():代表查詢條件,利用QueryBuilders.matchAllQuery()構建一個match_all查詢的DSL
  • 第三步,利用client.search()發送請求,得到響應

這裏關鍵的API有兩個,一個是request.source(),它構建的就是DSL中的完整JSON參數。其中包含了querysortfromsizehighlight等所有功能:

ElasticSearch:組合查詢或複合查詢 - 個人文章_搜索_16

另一個是QueryBuilders,其中包含了我們學習過的各種葉子查詢複合查詢等:

ElasticSearch:組合查詢或複合查詢 - 個人文章_搜索_17

2.1.2 解析響應結果

在發送請求以後,得到了響應結果SearchResponse,這個類的結構與我們在kibana中看到的響應結果JSON結構完全一致:

{
    "took" : 0,
    "timed_out" : false,
    "hits" : {
        "total" : {
            "value" : 2,
            "relation" : "eq"
        },
        "max_score" : 1.0,
        "hits" : [
            {
                "_index" : "heima",
                "_type" : "_doc",
                "_id" : "1",
                "_score" : 1.0,
                "_source" : {
                "info" : "Java講師",
                "name" : "趙雲"
                }
            }
        ]
    }
}

因此,我們解析SearchResponse的代碼就是在解析這個JSON結果,對比如下:

ElasticSearch:組合查詢或複合查詢 - 個人文章_#大數據_18

代碼解讀

elasticsearch返回的結果是一個JSON字符串,結構包含:

  • hits:命中的結果
  • total:總條數,其中的value是具體的總條數值
  • max_score:所有結果中得分最高的文檔的相關性算分
  • hits:搜索結果的文檔數組,其中的每個文檔都是一個json對象
  • _source:文檔中的原始數據,也是json對象

因此,我們解析響應結果,就是逐層解析JSON字符串,流程如下:

  • SearchHits:通過response.getHits()獲取,就是JSON中的最外層的hits,代表命中的結果
  • SearchHits#getTotalHits().value:獲取總條數信息
  • SearchHits#getHits():獲取SearchHit數組,也就是文檔數組
  • SearchHit#getSourceAsString():獲取文檔結果中的_source,也就是原始的json文檔數據

2.1.3 總結

文檔搜索的基本步驟是:

  1. 創建SearchRequest對象
  2. 準備request.source(),也就是DSL。
  1. QueryBuilders來構建查詢條件
  2. 傳入request.source()query()方法
  1. 發送請求,得到結果
  2. 解析結果(參考JSON結果,從外到內,逐層解析)

完整代碼:創建查詢商品索引庫的搜索測試代碼;如下:

hmall\search-service\src\test\java\com\hmall\search\SearchTest.java 內容如下:

package com.hmall.search;

import cn.hutool.json.JSONUtil;
import com.hmall.common.utils.BeanUtils;
import com.hmall.search.domain.po.ItemDoc;
import org.apache.http.HttpHost;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.io.IOException;

public class SearchTest {

    private RestHighLevelClient client;
    //索引庫名稱
    private static final String INDEX_NAME = "items";

    //初始化client
    @BeforeEach
    public void init() {
        client = new RestHighLevelClient(RestClient.builder(
                HttpHost.create("http://192.168.12.168:9200")));
    }

    //關閉client
    @AfterEach
    public void close() throws IOException {
        client.close();
    }

    //測試match_all搜索
    @Test
    public void testMatchAll() throws IOException {
        //1、創建搜索請求
        SearchRequest request = new SearchRequest(INDEX_NAME);
        //2、設置查詢參數
        request.source().query(QueryBuilders.matchAllQuery());
        //3、發送請求獲取響應結果
        SearchResponse response = client.search(request, RequestOptions.DEFAULT);
        //4、解析響應結果
        handleResponse(response);
    }

    private static void handleResponse(SearchResponse response) {
        System.out.println("共搜索到 " + response.getHits().getTotalHits().value + " 條數據");
        //獲取查詢數組
        SearchHit[] hits = response.getHits().getHits();
        for (SearchHit hit : hits) {
            //獲取_source(原始json數據)
            String jsonStr = hit.getSourceAsString();
            ItemDoc itemDoc = JSONUtil.toBean(jsonStr, ItemDoc.class);
            System.out.println(itemDoc);
        }
    }
}

2.2 葉子查詢

所有的查詢條件都是由QueryBuilders來構建的,葉子查詢也不例外。因此整套代碼中變化的部分僅僅是query條件構造的方式,其它不動。如下的方法都是在 com.hmall.search.SearchTest 添加即可。

例如match查詢:

//測試match搜索
    @Test
    public void testMatch() throws IOException {
        //1、創建搜索請求
        SearchRequest request = new SearchRequest(INDEX_NAME);
        //2、設置查詢參數
        request.source().query(QueryBuilders.matchQuery("name", "脱脂牛奶"));
        //3、發送請求獲取響應結果
        SearchResponse response = client.search(request, RequestOptions.DEFAULT);
        //4、解析響應結果
        handleResponse(response);
    }

再比如multi_match查詢:

//測試multi_match搜索
    @Test
    public void testMultiMatch() throws IOException {
        //1、創建搜索請求
        SearchRequest request = new SearchRequest(INDEX_NAME);
        //2、設置查詢參數
        request.source().query(QueryBuilders.multiMatchQuery("脱脂牛奶", "name", "category"));
        //3、發送請求獲取響應結果
        SearchResponse response = client.search(request, RequestOptions.DEFAULT);
        //4、解析響應結果
        handleResponse(response);
    }

還有range查詢:

//測試range搜索
    @Test
    public void testRange() throws IOException {
        //1、創建搜索請求
        SearchRequest request = new SearchRequest(INDEX_NAME);
        //2、設置查詢參數
        request.source().query(QueryBuilders.rangeQuery("price").gte(10000).lte(20000));
        //3、發送請求獲取響應結果
        SearchResponse response = client.search(request, RequestOptions.DEFAULT);
        //4、解析響應結果
        handleResponse(response);
    }

還有term查詢:

//測試term搜索
    @Test
    public void testTerm() throws IOException {
        //1、創建搜索請求
        SearchRequest request = new SearchRequest(INDEX_NAME);
        //2、設置查詢參數
        request.source().query(QueryBuilders.termQuery("brand", "華為"));
        //3、發送請求獲取響應結果
        SearchResponse response = client.search(request, RequestOptions.DEFAULT);
        //4、解析響應結果
        handleResponse(response);
    }

2.3 複合查詢

複合查詢也是由QueryBuilders來構建,我們以bool查詢為例,DSL和JavaAPI的對比如圖:

ElasticSearch:組合查詢或複合查詢 - 個人文章_#elasticsearch_19

完整代碼如下:

//測試bool搜索
    @Test
    public void testBool() throws IOException {
        //1、創建搜索請求
        SearchRequest request = new SearchRequest(INDEX_NAME);
        //2、設置查詢參數
        //2.1、創建 bool查詢對象
        BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
        //2.2、設置 must、filter、should、must_not等
        //關鍵字搜索
        boolQueryBuilder.must(QueryBuilders.termQuery("name", "手機"));
        //品牌過濾
        boolQueryBuilder.filter(QueryBuilders.termQuery("brand", "華為"));
        //價格過濾
        boolQueryBuilder.filter(QueryBuilders.rangeQuery("price").lte(30000));
        //2.3、將 bool查詢對象設置到search請求中
        request.source().query(boolQueryBuilder);
        //3、發送請求獲取響應結果
        SearchResponse response = client.search(request, RequestOptions.DEFAULT);
        //4、解析響應結果
        handleResponse(response);
    }

2.4 排序和分頁

之前説過,requeset.source()就是整個請求JSON參數,所以排序、分頁都是基於這個來設置,其DSL和JavaAPI的對比如下:

ElasticSearch:組合查詢或複合查詢 - 個人文章_搜索_20

完整示例代碼:

//測試分頁與排序
    @Test
    public void testPageAndSort() throws IOException {
        //頁號
        int pageNo = 1;
        //頁大小
        int pageSize = 5;

        //1、創建搜索請求
        SearchRequest request = new SearchRequest(INDEX_NAME);
        //2、設置查詢參數
        request.source().query(QueryBuilders.matchQuery("name", "華為"));
        //設置分頁參數
        request.source().from((pageNo-1)*pageSize).size(pageSize);
        //設置排序
        request.source().sort("price", SortOrder.DESC);
        //3、發送請求獲取響應結果
        SearchResponse response = client.search(request, RequestOptions.DEFAULT);
        //4、解析響應結果
        handleResponse(response);
    }

2.5 高亮

高亮查詢與前面的查詢有兩點不同:

  • 條件同樣是在request.source()中指定,只不過高亮條件要基於HighlightBuilder來構造
  • 高亮響應結果與搜索的文檔結果不在一起,需要單獨解析

首先來看高亮條件構造,其DSL和JavaAPI的對比如圖:

ElasticSearch:組合查詢或複合查詢 - 個人文章_#jenkins_21

代碼參考如下:

//測試高亮
    @Test
    public void testHighlight() throws IOException {
        //1、創建搜索請求
        SearchRequest request = new SearchRequest(INDEX_NAME);
        //2、設置查詢參數
        request.source().query(QueryBuilders.matchQuery("name", "華為"));
        //設置高亮
        request.source().highlighter(SearchSourceBuilder.highlight()
                .field("name")
                .preTags("<em>")
                .postTags("</em>")
        );
        //3、發送請求獲取響應結果
        SearchResponse response = client.search(request, RequestOptions.DEFAULT);
        //4、解析響應結果
        handleResponse(response);
    }

再來看結果解析,文檔解析的部分不變,主要是高亮內容需要單獨解析出來,其DSL和JavaAPI的對比如圖:

ElasticSearch:組合查詢或複合查詢 - 個人文章_#elasticsearch_22

代碼解讀:

  • 3、4步:從結果中獲取_sourcehit.getSourceAsString(),這部分是非高亮結果,json字符串。還需要反序列為ItemDTO對象
  • 5步:獲取高亮結果。hit.getHighlightFields(),返回值是一個Map,key是高亮字段名稱,值是HighlightField對象,代表高亮值
  • 5.1步:從Map中根據高亮字段名稱,獲取高亮字段值對象HighlightField
  • 5.2步:從HighlightField中獲取Fragments,並且轉為字符串。這部分就是真正的高亮字符串了
  • 最後:用高亮的結果替換ItemDTO中的非高亮結果

改造 handleResponse方法後如下:

private static void handleResponse(SearchResponse response) {
        System.out.println("共搜索到 " + response.getHits().getTotalHits().value + " 條數據");
        //獲取查詢數組
        SearchHit[] hits = response.getHits().getHits();
        for (SearchHit hit : hits) {
            //獲取_source(原始json數據)
            String jsonStr = hit.getSourceAsString();
            ItemDoc itemDoc = JSONUtil.toBean(jsonStr, ItemDoc.class);
            
            //解析高亮結果
            Map<String, HighlightField> highlightFields = hit.getHighlightFields();
            if (CollUtils.isNotEmpty(highlightFields)) {
                HighlightField highlightField = highlightFields.get("name");
                if (highlightField != null) {
                    String highlightStr = highlightField.getFragments()[0].string();
                    itemDoc.setName(highlightStr);
                }
            }
            System.out.println(itemDoc);
        }
    }

3 數據聚合

聚合(aggregations)可以讓我們極其方便的實現對數據的統計、分析、運算。例如:

  • 什麼品牌的手機最受歡迎?
  • 這些手機的平均價格、最高價格、最低價格?
  • 這些手機每月的銷售情況如何?

實現這些統計功能的比數據庫的sql要方便的多,而且查詢速度非常快,可以實現近實時搜索效果。

官方文檔:https://www.elastic.co/guide/en/elasticsearch/reference/7.12/search-aggregations.html

聚合常見的有三類:

  • 桶(Bucket 聚合:用來對文檔做分組
  • TermAggregation:按照文檔字段值分組,例如按照品牌值分組、按照國家分組
  • Date Histogram:按照日期階梯分組,例如一週為一組,或者一月為一組
  • 度量(Metric 聚合:用以計算一些值,比如:最大值、最小值、平均值等
  • Avg:求平均值
  • Max:求最大值
  • Min:求最小值
  • Stats:同時求maxminavgsum
  • 管道(pipeline 聚合:其它聚合的結果為基礎做進一步運算

注意: 參加聚合的字段必須是keyword、日期、數值、布爾類型

3.1 DSL實現聚合

與之前的搜索功能類似,我們依然先學習DSL的語法,再學習JavaAPI.

3.1.1 Bucket聚合

例如我們要統計所有商品中共有哪些商品分類,其實就是以分類(category)字段對數據分組。category值一樣的放在同一組,屬於Bucket聚合中的Term聚合。

基本語法如下:

GET /items/_search
{
  "size": 0, 
  "aggs": {
    "category_agg": {
      "terms": {
        "field": "category",
        "size": 20
      }
    }
  }
}

語法説明:

  • size:設置size為0,就是每頁查0條,則結果中就不包含文檔,只包含聚合
  • aggs:定義聚合
  • category_agg:聚合名稱,自定義,但不能重複
  • terms:聚合的類型,按分類聚合,所以用term
  • field:參與聚合的字段名稱
  • size:希望返回的聚合結果的最大數量

來看下查詢的結果:

ElasticSearch:組合查詢或複合查詢 - 個人文章_搜索_23

3.1.2 帶條件聚合

默認情況下,Bucket聚合是對索引庫的所有文檔做聚合,例如我們統計商品中所有的品牌,結果如下:

ElasticSearch:組合查詢或複合查詢 - 個人文章_#jenkins_24

可以看到統計出的品牌非常多。

但真實場景下,用户會輸入搜索條件,因此聚合必須是對搜索結果聚合。那麼聚合必須添加限定條件。

例如,我想知道價格高於3000元的手機品牌有哪些,該怎麼統計呢?

我們需要從需求中分析出搜索查詢的條件和聚合的目標:

  • 搜索查詢過濾條件:
  • 價格高於3000
  • 分類必須是手機
  • 聚合目標:統計的是品牌,肯定是對brand字段做term聚合

語法如下:

GET /items/_search
{
  "query": {
    "bool": {
      "filter": [
        {"term": {
          "category": "手機"
        }}
        ,{"range": {
            "price": {
              "gte": 300000
            }
          }
        }
      ]
    }
  }, 
  "size": 0,
  "aggs": {
    "brand_agg": {
      "terms": {
        "field": "brand",
        "size": 20
      }
    }
  }
}

聚合結果如下:

{
  "took" : 2,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 11,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  },
  "aggregations" : {
    "brand_agg" : {
      "doc_count_error_upper_bound" : 0,
      "sum_other_doc_count" : 0,
      "buckets" : [
        {
          "key" : "Apple",
          "doc_count" : 7
        },
        {
          "key" : "華為",
          "doc_count" : 2
        },
        {
          "key" : "三星",
          "doc_count" : 1
        },
        {
          "key" : "小米",
          "doc_count" : 1
        }
      ]
    }
  }
}

可以看到,結果中只剩下4個品牌了。

3.1.3 Metric聚合

我們統計了價格高於3000的手機品牌,形成了一個個桶。現在我們需要對桶內的商品做運算,獲取每個品牌價格的最小值、最大值、平均值。

這就要用到Metric聚合了,例如stat聚合,就可以同時獲取minmaxavg等結果。

語法如下:

GET /items/_search
{
  "query": {
    "bool": {
      "filter": [
        {"term": {
          "category": "手機"
        }}
        ,{"range": {
            "price": {
              "gte": 300000
            }
          }
        }
      ]
    }
  }, 
  "size": 0,
  "aggs": {
    "brand_agg": {
      "terms": {
        "field": "brand",
        "size": 20
      }
      , "aggs": {
        "stats_metric": {
          "stats": {
            "field": "price"
          }
        }
      }
    }
  }
}

query部分就不説了,我們重點解讀聚合部分語法。

可以看到我們在brand_agg聚合的內部,我們新加了一個aggs參數。這個聚合就是brand_agg的子聚合,會對brand_agg形成的每個桶中的文檔分別統計。

  • stats_metric:聚合名稱
  • stats:聚合類型,stats是metric聚合的一種
  • field:聚合字段,這裏選擇price,統計價格

由於stats是對brand_agg形成的每個品牌桶內文檔分別做統計,因此每個品牌都會統計出自己的價格最小、最大、平均值。結果如下:

ElasticSearch:組合查詢或複合查詢 - 個人文章_elasticsearch_25

另外,我們還可以讓聚合按照每個品牌的價格平均值排序:

ElasticSearch:組合查詢或複合查詢 - 個人文章_elasticsearch_26

3.1.4 總結

aggs代表聚合,與query同級,此時query的作用是?

  • 限定聚合的的文檔範圍

聚合必須的三要素:

  • 聚合名稱
  • 聚合類型
  • 聚合字段

聚合可配置屬性有:

  • size:指定聚合結果數量
  • order:指定聚合結果排序方式
  • field:指定聚合字段

3.2 RestClient實現聚合

可以看到在DSL中,aggs聚合條件與query條件是同一級別,都屬於查詢JSON參數。因此依然是利用request.source()方法來設置。

不過聚合條件的要利用AggregationBuilders這個工具類來構造。DSL與JavaAPI的語法對比如下:

ElasticSearch:組合查詢或複合查詢 - 個人文章_#大數據_27

聚合結果與搜索文檔同一級別,因此需要單獨獲取和解析。具體解析語法如下:

ElasticSearch:組合查詢或複合查詢 - 個人文章_elasticsearch_28

完整代碼如下:

//測試聚合
    @Test
    public void testAgg() throws IOException {
        //1、創建搜索請求
        SearchRequest request = new SearchRequest(INDEX_NAME);
        //2、設置查詢參數
        //設置查詢過濾條件
        BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery()
                .filter(QueryBuilders.termQuery("category", "手機"))
                .filter(QueryBuilders.rangeQuery("price").gte(300000));
        request.source().query(boolQueryBuilder);
        //不返回文檔
        request.source().size(0);
        //設置品牌聚合
        request.source().aggregation(
                AggregationBuilders.terms("brand_agg").field("brand").size(20)
        );
        //3、發送請求獲取響應結果
        SearchResponse response = client.search(request, RequestOptions.DEFAULT);
        //4、解析響應結果
        Aggregations aggregations = response.getAggregations();
        //獲取品牌聚合;注意下面的 Terms 是 org.elasticsearch.search.aggregations.bucket.terms.Terms
        Terms brandTerms = aggregations.get("brand_agg");
        //獲取桶內數據
        List<? extends Terms.Bucket> buckets = brandTerms.getBuckets();
        //遍歷輸出桶內數據
        buckets.forEach(bucket -> {
            System.out.println("-------------------------------------------------------");
            System.out.println(bucket.getKeyAsString() + ":" + bucket.getDocCount());
        });
    }

Metric聚合代碼參考:

//測試Metric聚合
    @Test
    public void testAgg() throws IOException {
        //1、創建搜索請求
        SearchRequest request = new SearchRequest(INDEX_NAME);
        //2、設置查詢參數
        //設置查詢過濾條件
        BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery()
                .filter(QueryBuilders.termQuery("category", "手機"))
                .filter(QueryBuilders.rangeQuery("price").gte(300000));
        request.source().query(boolQueryBuilder);
        //不返回文檔
        request.source().size(0);
        //設置品牌聚合
        request.source().aggregation(
                AggregationBuilders.terms("brand_agg").field("brand").size(20)
                        .subAggregation(
                                AggregationBuilders.stats("stats_metric").field("price")
                        )
        );
        //3、發送請求獲取響應結果
        SearchResponse response = client.search(request, RequestOptions.DEFAULT);
        //4、解析響應結果
        Aggregations aggregations = response.getAggregations();
        //獲取品牌聚合;注意下面的 Terms 是 org.elasticsearch.search.aggregations.bucket.terms.Terms
        Terms brandTerms = aggregations.get("brand_agg");
        //獲取桶內數據
        List<? extends Terms.Bucket> buckets = brandTerms.getBuckets();
        //遍歷輸出桶內數據
        buckets.forEach(bucket -> {
            System.out.println("-------------------------------------------------------");
            System.out.println(bucket.getKeyAsString() + ":" + bucket.getDocCount());
            Aggregations aggregations1 = bucket.getAggregations();
            if (aggregations1 != null) {
                Stats statsMetric = aggregations1.get("stats_metric");
                System.out.println("平均價格:" + statsMetric.getAvg());
                System.out.println("最大價格:" + statsMetric.getMax());
                System.out.println("最小价格:" + statsMetric.getMin());
            }
        });
    }

4 作業

Elasticsearch的基本語法我們已經學完,足以應對大多數搜索業務需求了。接下來大家就可以基於學習的知識實現商品搜索的業務了。

在昨天的作業中要求大家拆分一個獨立的微服務:search-service,在這個微服務中實現搜索數據的導入、商品數據庫數據與elasticsearch索引庫數據的同步。

接下來的搜索功能也要在search-service服務中實現。

4.1 實現搜索接口

在黑馬商城的搜索頁面,輸入關鍵字,點擊搜索時,會發現前端會發起查詢商品的請求:

請求的接口信息如下:

  • 請求方式GET
  • 請求路徑/search/list
  • 請求參數
  • key:搜索關鍵字
  • pageNo:頁碼
  • pageSize:每頁大小
  • sortBy:排序字段
  • isAsc:是否升序
  • category:分類
  • brand:品牌
  • minPrice:價格最小值
  • maxPrice:價格最大值

4.1.1 SearchController

修改 com.hmall.search.controller.SearchController 代碼如下:

package com.hmall.search.controller;

import com.hmall.search.domain.po.ItemDoc;
import com.hmall.search.domain.query.ItemPageQuery;
import com.hmall.search.domain.vo.PageVO;
import com.hmall.search.service.ISearchService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Api(tags = "搜索相關接口")
@RestController
@RequestMapping("/search")
@RequiredArgsConstructor
public class SearchController {

    private final ISearchService searchService;

    @ApiOperation("搜索商品")
    @GetMapping("/list")
    public PageVO<ItemDoc> search(ItemPageQuery query) {
        return searchService.search(query);
    }
}

4.1.2 ISearchService

修改 com.hmall.search.service.ISearchService 代碼如下:

package com.hmall.search.service;

import com.hmall.search.domain.po.ItemDoc;
import com.hmall.search.domain.query.ItemPageQuery;
import com.hmall.search.domain.vo.PageVO;

public interface ISearchService {
    void saveItemById(Long itemId);

    void deleteItemById(Long itemId);

    PageVO<ItemDoc> search(ItemPageQuery query);
}

4.1.3 SearchServiceImpl

新增 com.hmall.search.service.impl.SearchServiceImpl 搜索方法代碼如下:

@Override
    public PageVO<ItemDoc> search(ItemPageQuery query) {
        PageVO<ItemDoc> pageVO = PageVO.empty(0L, 0L);
        try {
            //1、創建查詢請求
            SearchRequest request = new SearchRequest(INDEX_NAME);
            //2、設置查詢及各類參數
            //創建bool查詢
            BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
            //設置搜索關鍵字
            boolean isHighlight = false;
            if (StrUtil.isNotBlank(query.getKey())) {
                boolQuery.must(QueryBuilders.matchQuery("name", query.getKey()));

                //只有搜索了關鍵字才高亮
                isHighlight = true;
            }
            //設置分類過濾查詢
            if (StrUtil.isNotBlank(query.getCategory())) {
                boolQuery.filter(QueryBuilders.termQuery("category", query.getCategory()));
            }
            //設置品牌過濾查詢
            if (StrUtil.isNotBlank(query.getBrand())) {
                boolQuery.filter(QueryBuilders.termQuery("brand", query.getBrand()));
            }
            //設置價格過濾查詢
            if (query.getMinPrice() != null) {
                boolQueryBuilder.filter(QueryBuilders.rangeQuery("price").gte(query.getMinPrice()));
             }
             if (query.getMaxPrice() != null) {
                boolQueryBuilder.filter(QueryBuilders.rangeQuery("price").lte(query.getMaxPrice()));
             }
            if (isHighlight) {
                //設置高亮
                request.source().highlighter(SearchSourceBuilder.highlight()
                        .field("name")
                        .preTags("<em>")
                        .postTags("</em>")
                );
            }
            //設置分頁
            int pageNo = query.getPageNo();
            int pageSize = query.getPageSize();
            request.source().from((pageNo - 1) * pageSize).size(pageSize);
            //設置排序
            if (StrUtil.isNotBlank(query.getSortBy())) {
                request.source().sort(query.getSortBy(), query.getIsAsc() ? SortOrder.ASC : SortOrder.DESC);
            } else {
                searchRequest.source().sort("updateTime", SortOrder.DESC);
            }
            //設置查詢對象
            request.source().query(boolQuery);
            //3、發送請求
            SearchResponse response = client.search(request, RequestOptions.DEFAULT);
            //4、解析響應結果
            SearchHits hits = response.getHits();
            //總記錄數
            long total = hits.getTotalHits().value;
            pageVO.setTotal(total);
            //通過頁大小和總記錄數計算總頁數
            long pages = (total % pageSize == 0) ? (total / pageSize) : (total / pageSize + 1);
            pageVO.setPages(pages);

            List<ItemDoc> itemDocList = new ArrayList<>(pageSize);
            for (SearchHit hit : hits.getHits()) {
                ItemDoc itemDoc = JSONUtil.toBean(hit.getSourceAsString(), ItemDoc.class);
                //處理高亮
                if (isHighlight) {
                    HighlightField highlightField = hit.getHighlightFields().get("name");
                    if (highlightField != null) {
                        String name = highlightField.getFragments()[0].string();
                        itemDoc.setName(name);
                    }
                }
                itemDocList.add(itemDoc);
            }
            pageVO.setList(itemDocList);

            return pageVO;
        } catch (IOException e) {
            throw new RuntimeException("查詢es中商品失敗!", e);
        }
    }

4.2 過濾條件聚合

搜索頁面的過濾項目前是寫死的:

ElasticSearch:組合查詢或複合查詢 - 個人文章_elasticsearch_29

但是大家思考一下,隨着搜索條件的變化,過濾條件展示的過濾項是不是應該跟着變化。

例如搜索電視,那麼搜索結果中展示的肯定只有電視,而此時過濾條件中的分類就不能還出現手機、拉桿箱等內容。過濾條件的品牌中就不能出現與電視無關的品牌。而是應該展示搜索結果中存在的分類和品牌。

那麼問題來,我們怎麼知道搜索結果中存在哪些分類和品牌呢?

大家應該能想到,就是利用聚合,而且是帶有限定條件的聚合。用户搜索的條件是什麼,我們在對分類、品牌聚合時的條件也就是什麼,這樣就能統計出搜索結果中包含的分類、品牌了。

修改 hmall-nginx\html\hmall-portal\search.html 這個頁面的第 142 行;將 true 修改為 false。修改後:

ElasticSearch:組合查詢或複合查詢 - 個人文章_#elasticsearch_30

再次搜索時,前端已經發出了請求,嘗試搜索欄中除價格以外的過濾項:

ElasticSearch:組合查詢或複合查詢 - 個人文章_#大數據_31

接口信息如下:

  • 請求方式POST
  • 請求路徑/search/filters
  • 請求參數
  • key:搜索關鍵字
  • pageNo:頁碼
  • pageSize:每頁大小
  • sortBy:排序字段
  • isAsc:是否升序
  • category:分類
  • brand:品牌
  • minPrice:價格最小值
  • maxPrice:價格最大值

可見參數與搜索參數一致,不過這裏大家可以忽略分頁和排序參數。

返回值參考這個格式:

{
  "category": ["手機", "曲面電視", "拉桿箱", "休閒鞋", "休閒鞋", "硬盤", "真皮包"],
  "brand": ["希捷", "小米", "華為", "oppo", "新秀麗", "Apple","錘子"]
}

4.2.1 SearchController

修改 com.hmall.search.controller.SearchController 新增如下方法:

@ApiOperation("搜索商品分類、品牌列表")
    @PostMapping("/filters")
    public Map<String, List<String>> filters(@RequestBody ItemPageQuery query) {
        return searchService.filter(query);
    }

4.2.2 ISearchService

修改 com.hmall.search.service.ISearchService 新增如下方法:

Map<String, List<String>> filters(ItemPageQuery query);

4.2.3 SearchServiceImpl

修改 com.hmall.search.service.impl.SearchServiceImpl 新增如下方法:

@Override
    public Map<String, List<String>> filters(ItemPageQuery query) {
        try {
            //只有當分類或品牌沒有選擇的時才有必要去查對應的數據
            if (StrUtil.isBlank(query.getCategory()) || StrUtil.isBlank(query.getBrand())) {
                Map<String, List<String>> resultMap = new HashMap<>();
                //1、創建查詢請求
                SearchRequest request = new SearchRequest(INDEX_NAME);
                //2、設置參數
                //是否需要查詢分類聚合數據
                boolean isNeedCategoryAgg = true;
                //是否需要查詢品牌聚合數據
                boolean isNeedBrandAgg = true;
                //設置搜索關鍵字
                BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
                if (StrUtil.isNotBlank(query.getKey())) {
                    boolQuery.must(QueryBuilders.matchQuery("name", query.getKey()));
                }
                if (StrUtil.isNotBlank(query.getCategory())) {
                    boolQuery.filter(QueryBuilders.termQuery("category", query.getCategory()));
                    isNeedCategoryAgg = false;
                }
                if (StrUtil.isNotBlank(query.getBrand())) {
                    boolQuery.filter(QueryBuilders.termQuery("brand", query.getBrand()));
                    isNeedBrandAgg = false;
                }
                if (query.getMinPrice() != null) {
                    boolQueryBuilder.filter(QueryBuilders.rangeQuery("price").gte(query.getMinPrice()));
                }
                if (query.getMaxPrice() != null) {
                    boolQueryBuilder.filter(QueryBuilders.rangeQuery("price").lte(query.getMaxPrice()));
                }
                request.source().query(boolQuery);

                //設置不返回文檔
                request.source().size(0);

                //設置分類聚合
                if (isNeedCategoryAgg) {
                    TermsAggregationBuilder aggregationBuilder = AggregationBuilders.terms("category_agg").field("category").size(20);
                    request.source().aggregation(aggregationBuilder);
                }
                //設置品牌聚合
                if (isNeedBrandAgg) {
                    TermsAggregationBuilder aggregationBuilder = AggregationBuilders.terms("brand_agg").field("brand").size(20);
                    request.source().aggregation(aggregationBuilder);
                }

                //3、發送請求
                SearchResponse response = client.search(request, RequestOptions.DEFAULT);
                //4、解析響應結果
                Aggregations aggregations = response.getAggregations();
                Terms categoryAgg = aggregations.get("category_agg");
                if (categoryAgg != null) {
                    List<String> categoryList = new ArrayList<>();
                    for (Terms.Bucket bucket : categoryAgg.getBuckets()) {
                        categoryList.add(bucket.getKeyAsString());
                    }
                    resultMap.put("category", categoryList);
                }
                Terms brandAgg = aggregations.get("brand_agg");
                if (brandAgg != null) {
                    List<String> brandList = new ArrayList<>();
                    for (Terms.Bucket bucket : brandAgg.getBuckets()) {
                        brandList.add(bucket.getKeyAsString());
                    }
                    resultMap.put("brand", brandList);
                }
                return resultMap;
            }
        } catch (IOException e) {
            System.out.println("查詢分類、品牌聚合數據失敗!" + e);
        }
        return CollUtils.emptyMap();
    }

4.3 競價排名(瞭解)

elasticsearch的默認排序規則是按照相關性打分排序,而這個打分是可以通過API來控制的。詳情可以參考複合查詢中的算分函數查詢(1.3.1小節)

對應的JavaAPI可以參考文檔:https://www.elastic.co/guide/en/elasticsearch/client/java-api/7.12/java-compound-queries.html

在商品的數據庫表中,已經設計了isAD字段來標記廣告商品,請利用function_score查詢在原本搜索的結果基礎上,讓這些isAD字段值為true的商品排名到最前面。

修改 com.hmall.search.service.impl.SearchServiceImpl#search 方法的如下部分:

ElasticSearch:組合查詢或複合查詢 - 個人文章_#elasticsearch_32