2、ElasticSearch高級搜索 Elasticsearch提供了基於JSON的DSL(Domain Specific Language)來定義查詢。常見的查詢類型如下所示 ①、查詢所有 查詢出所有數據,一般測試用;例如 match_all 如下圖所示 ②、全文檢索(full text)查詢 ...
2、ElasticSearch高級搜索
- Elasticsearch提供了基於JSON的DSL(Domain Specific Language)來定義查詢。常見的查詢類型如下所示
- ①、查詢所有
- 查詢出所有數據,一般測試用;例如
match_all
- 如下圖所示
- 查詢出所有數據,一般測試用;例如
- ②、全文檢索(full text)查詢
- 利用分詞器對用戶輸入內容分詞,然後去倒排索引庫中匹配,例如
match_query
multi_match_query
- 利用分詞器對用戶輸入內容分詞,然後去倒排索引庫中匹配,例如
- ③、精確查詢
- 根據精確詞條值查找數據,一般是查找keyword、數值、日期、boolean等類型的欄位,例如
ids
range
term
- 根據精確詞條值查找數據,一般是查找keyword、數值、日期、boolean等類型的欄位,例如
- ④、地理(geo)查詢
- 根據經緯度查詢,例如
geo_distance
geo_bounding_box
- 根據經緯度查詢,例如
- ⑤、複合(compound)查詢
- 複合查詢可以將上述各種查詢條件組合起來,合併查詢條件,例如
bool
function_score
- ①、查詢所有
2.1、全文檢索查詢
2.1.1、使用場景
- 全文檢索查詢的基本流程如下所示
- ①、對用戶搜索的內容做分詞,得到詞條
- ②、根據詞條去倒排索引庫中匹配,得到文檔id
- ③、根據文檔id找到文檔,把所有匹配結果以並集或交集返回給用戶
- 比較常用的場景包括
- 商城的輸入框搜索
- 百度搜索框搜索
- 因為是拿著詞條去匹配,因此參與搜索的欄位也必須是可分詞的text類型的欄位
2.1.2、DSL語句格式
-
常見的全文檢索查詢包括
match
查詢:單欄位查詢multi_match
查詢:多欄位查詢,任意一個欄位符合條件就算符合查詢條件
-
match
查詢語法如下所示-
GET /indexName/_search { "query": { "match":{ "FIELD": "TEXT" } } }
-
-
match_all
查詢語法如下-
GET /indexName/_search { "query": { "multi_match": { "query": "TEXT", "fileds": ["FILED1", "FILED2"] } } }
-
2.1.3、match查詢DSL語句示例&&RestAPI示例
①、DSL語句
-
比如要搜索
name
欄位中存在如家酒店
,DSL語句如下所示-
GET hotel/_search { "query": { "match": { "name": "如家酒店" } }, "size": 2 # size的意思是只顯示n條數據 }
-
-
搜索結果如下所示
-
結果分析
-
因為
name
欄位是類型是text
,搜索的時候會對這個欄位進行分詞 -
如搜索
如家酒店
,那麼就會分詞稱為如家
,酒店
,相當於會搜索三次,並取這三次搜索的並集(ES預設的是並集),所以搜索的命中率才會如此之高- 通俗的來說
- 並集就相當於搜索到
name like %如家%
算一條數據,搜索到酒店
也算一條數據 - 那麼交集就跟它相反,必須是
name like %如家酒店%
才能算是一條數據
- 並集就相當於搜索到
- 通俗的來說
-
那麼如何取交集呢?,如下所示
-
DSL
-
# 取交集,並集是or GET hotel/_search { "query": { "match": { "name": { "query": "如家酒店", "operator": "and" } } } }
-
-
運行結果
-
-
②、RestAPI
math_all
-
代碼如下所示
-
package com.coolman.hotel.test; import com.coolman.hotel.pojo.HotelDoc; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.lucene.search.TotalHits; import org.elasticsearch.action.search.SearchRequest; import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.client.RequestOptions; import org.elasticsearch.client.RestHighLevelClient; import org.elasticsearch.index.query.MatchAllQueryBuilder; import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.search.SearchHit; import org.elasticsearch.search.SearchHits; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import java.io.IOException; @SpringBootTest public class FullTextSearchDemo { // 註入 RestHighLevelClient對象 @Autowired private RestHighLevelClient restHighLevelClient;
// jackson private final ObjectMapper objectMapper = new ObjectMapper(); /** * 查詢所有測試 */ @Test public void testMatchAll() throws IOException { // 1. 創建一個查詢請求對象 SearchRequest searchRequest = new SearchRequest("hotel"); // 指定索引 // 2. 添加查詢的類型 MatchAllQueryBuilder matchAllQueryBuilder = QueryBuilders.matchAllQuery(); searchRequest.source().query(matchAllQueryBuilder); // source就相當於{} searchRequest.source().size(100); // RestAPI預設返回的是10條數據,可以更改size的屬性,即可自定義返回的數據量 // 3. 發出查詢的請求,得到響應結果 SearchResponse response = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT); // 4. 處理響應的結果 handlerResponse(response); } /** * 用來處理響應數據(相當於解析返回的JSON數據) * @param response */ private void handlerResponse(SearchResponse response) throws JsonProcessingException { // 1. 得到命中的數量(即總記錄數量) SearchHits hits = response.getHits(); long totalCount = hits.getTotalHits().value;// 總記錄數 System.out.println("總記錄數量為:" + totalCount); // 2. 獲取本次查詢出來的列表數據 SearchHit[] hitsArray = hits.getHits(); for (SearchHit hit : hitsArray) { // 得到json字元串 String json = hit.getSourceAsString(); // 將json字元串轉換為實體類對象 HotelDoc hotelDoc = objectMapper.readValue(json, HotelDoc.class); System.out.println(hotelDoc); } }
}
~~~
-
match
-
代碼如下所示
-
/** * 單欄位查詢 */ @Test public void testMatch() throws IOException { // 1. 創建查詢請求對象 SearchRequest searchRequest = new SearchRequest("hotel"); // 2. 添加查詢的類型 MatchQueryBuilder matchQueryBuilder = QueryBuilders.matchQuery("name", "如家酒店"); searchRequest.source().query(matchQueryBuilder); // 3. 發出查詢請求,得到響應數據 SearchResponse response = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT); // 4. 處理響應的結果 handlerResponse(response); }
-
自行運行查看結果即可
2.1.4、multi_match查詢DSL語句示例&&RestAPI示例
DSL語句
-
比如搜索
name
和brand
欄位中出現如家酒店
的數據-
DSL語句如下所示
-
GET hotel/_search { "query": { "multi_match": { "query": "如家酒店", "fields": ["name", "brand"] } } }
-
-
運行結果如下所示
-
不過多欄位查詢的使用很少,因為多欄位查詢會使得查詢效率變慢
-
一般都會在創建映射的時候,使用
copy_to
將指定欄位的值拷貝到另一個欄位,如自定義的all
欄位 -
這樣子就可以使用單欄位查詢,提高查詢效率
-
RestAPI
跟單欄位查詢差不多,只不過使用QueryBuilders創建的對象略有不同罷了
-
代碼如下所示
-
/** * 多欄位查詢 */ @Test public void testMultiMatch() throws IOException { // 1. 創建查詢請求球體對象 SearchRequest searchRequest = new SearchRequest("hotel"); // 2. 添加要查詢的欄位 // MultiMatchQueryBuilder multiMatchQueryBuilder = QueryBuilders.multiMatchQuery("如家酒店", "name", "brand", "bussiness"); // searchRequest.source().query(multiMatchQueryBuilder); // 因為在創建映射的時候使用了copy_to,索引上面的多欄位查詢等價於下麵的單欄位查詢 MatchQueryBuilder matchQueryBuilder = QueryBuilders.matchQuery("all", "如家酒店"); searchRequest.source().query(matchQueryBuilder); // 3. 執行查詢操作,得到響應對象 SearchResponse response = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT); // 4. 處理響應對象 handlerResponse(response); }
-
2.2、精準查詢
2.2.1、使用場景
- 精確查詢一般是查找
keyword
、數值、日期、boolean
等類型的欄位,所以不會對搜索條件分詞,常見的有如下term
- 根據詞條精確值查詢,相當於
equals
、=
- 根據詞條精確值查詢,相當於
range
- 根據值的範圍查詢,相當於
>=
、<=
、between
、and
- 根據值的範圍查詢,相當於
2.2.2、DSL語句格式
①、term
查詢
-
因為精確查詢的欄位搜索的是不分詞的欄位,因此查詢的條件也必須是不分詞的詞條
-
查詢的時候,用戶輸入的內容跟自動值完全匹配的時候才認為符合條件
-
如果用戶輸入的內容過多,反而搜索不到數據
-
語法說明
-
# term 精確查詢 GET /indexName/_search { "query": { "term": { "FILED": { "value": "VALUE" } } } }
-
-
示例
- 輸入精確詞條
- 輸入非精確詞條
- 輸入精確詞條
②、range
查詢
-
範圍查詢,一般應用在對數值類型做範圍過濾的時候。比如做價格範圍過濾
-
基本語法
-
# range 精確查詢 # gte表示大於等於;gt表示大於 # lte表示小於等於;lt表示小於 GET /indexName/_search { "query": { "range": { "FIELD": { "gte": 10, "lte": 20 } } } }
-
-
示例
- 查詢
price
大於等於200,小於等於500的酒店
- 查詢
2.2.3、RestAPI
-
term
查詢-
代碼如下所示
-
/** * term 精確查詢 */ @Test public void testTermQuery() throws IOException { // 1. 創建查詢請求對象 SearchRequest searchRequest = new SearchRequest("hotel"); // 2. 添加要查詢的欄位 TermQueryBuilder termQueryBuilder = QueryBuilders.termQuery("brand", "如家"); searchRequest.source().query(termQueryBuilder); // 3. 發出查詢的請求,獲取響應結果 SearchResponse response = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT); // 4. 處理響應的結果 handlerResponse(response); }
-
-
range
查詢-
代碼如下所示
-
/** * range 精確查詢 */ @Test public void testRangeQuery() throws IOException { // 1. 創建查詢請求對象 SearchRequest searchRequest = new SearchRequest("hotel"); // 2. 添加查詢的欄位 RangeQueryBuilder rangeQuery = QueryBuilders.rangeQuery("price"); rangeQuery.gte(300); // 大於等於300 rangeQuery.lte(500); // 小於等於500 searchRequest.source().query(rangeQuery); // 3. 執行查詢操作,獲取響應結果 SearchResponse response = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT); // 4. 處理響應結果 handlerResponse(response); }
-
2.3、地理坐標查詢
2.3.1、使用場景
- 所謂的地理坐標查詢,其實就是根據經緯度查詢
- 官方文檔
- 常見的使用場景如下所示
- 攜程:搜索附近的酒店
- 滴滴:搜索附近的計程車
- 微信:搜索附近的人
2.3.2、DSL語句格式
①、矩形範圍查詢
-
矩形範圍查詢,也就是
geo_bounding_box
查詢,查詢坐標落在某個矩形範圍的所有文檔 -
查詢的時候,需要指定矩形的左上、右下兩個點的坐標,然後畫出一個矩形,落在該矩形內的都是符合條件的點,如下所示
-
語法如下所示
-
# 地理位置查詢(矩形查詢) GET hotel/_search { "query": { "geo_bounding_box": { "location": { "top_left": { "lat": 31.1, "lon": 121.5 }, "bottom_right": { "lat": 30.9, "lon": 121.7 } } } } }
-
-
示例
②、附近查詢
-
附近查詢,也叫做距離查詢(geo_distance)
- 查詢到指定中心小於某個距離值的所有文檔
-
換句話來說,在地圖上找一個點作為圓心,以指定距離為半徑,畫一個圓,落在圓內的坐標都算符合條件,如下所示
-
語法如下所示
-
GET hotel/_search { "query": { "geo_distance": { "distance": "15km", "location": "31.21,121.5" } } }
-
-
示例
2.3.3、RestAPI
①、矩形範圍查詢
-
代碼如下所示
-
/** * 地理坐標矩形查詢 */ @Test public void testGeoBoundingBoxSearch() throws IOException { // 1. 創建查詢請求對象 SearchRequest searchRequest = new SearchRequest("hotel"); // 2. 添加要查詢的欄位 // 指定要查詢的欄位為 location GeoBoundingBoxQueryBuilder geoBoundingBoxQueryBuilder = QueryBuilders.geoBoundingBoxQuery("location"); // 指定 topLeft的坐標 geoBoundingBoxQueryBuilder.topLeft().resetLat(31.1); geoBoundingBoxQueryBuilder.topLeft().resetLon(121.5); // 指定 bottom_right的坐標 geoBoundingBoxQueryBuilder.bottomRight().resetLat(30.9); geoBoundingBoxQueryBuilder.bottomRight().resetLon(121.7); searchRequest.source().query(geoBoundingBoxQueryBuilder); // 3. 發起請求 SearchResponse response = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT); // 4. 處理返回的數據 handlerResponse(response); }
-
②、附近查詢
-
代碼如下所示
-
/** * 地理坐標附近查詢(圓形) */ @Test public void testGeoDistanceSearch() throws IOException { // 1. 創建查詢請求對象 SearchRequest searchRequest = new SearchRequest("hotel"); // 2. 添加要查詢的欄位 // 指定要查詢的欄位是 location GeoDistanceQueryBuilder geoDistanceQueryBuilder = QueryBuilders.geoDistanceQuery("location"); // 指定中心點坐標 geoDistanceQueryBuilder.point(new GeoPoint(31.21, 121.5)); // 指定要查詢的範圍距離 geoDistanceQueryBuilder.distance("15km"); searchRequest.source().query(geoDistanceQueryBuilder); // 3. 發起查詢請求 SearchResponse response = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT); // 4. 處理返回的數據 handlerResponse(response); }
-
2.4、複合查詢之布爾查詢
2.4.1、使用場景
- 布爾查詢是一個或多個查詢子句的組合,每一個子句就是一個子查詢,子查詢的組合方式有如下幾種
- ①、
must
:必須匹配每個子查詢,類似"與"(and),must
的條件參與算分 - ②、
should
:選擇性匹配子查詢,類似"或"(or) - ③、
must_not
:必須不匹配,不參與算分,類似"非"(not) - ④、
filter
:效果和must
一樣,都是and。必須匹配,filter的條件不參與算分
- ①、
- 常見的應用場景
- 比如在搜索酒店的時候,除了關鍵字搜索以外,我們還可能根據品牌、價格、城市等欄位過濾
- 每一個不同的欄位,其查詢的條件、方式都不一樣,必須是多個不同的查詢,而要組合這些查詢,就必須使用bool查詢
- 比如在搜索酒店的時候,除了關鍵字搜索以外,我們還可能根據品牌、價格、城市等欄位過濾
- 註意事項
- 不過需要註意的是,搜索的時候,參與算分的欄位越多,查詢的性能也越差;因此這種多條件查詢的時候,可以按照如下類似方法解決
- 搜索框的關鍵字搜索,是全文檢索查詢,使用
must
查詢,參與算分 - 其他過濾條件,採用
filter
查詢,不參與算分
- 搜索框的關鍵字搜索,是全文檢索查詢,使用
- 不過需要註意的是,搜索的時候,參與算分的欄位越多,查詢的性能也越差;因此這種多條件查詢的時候,可以按照如下類似方法解決
2.4.2、DSL語句格式
-
DSL語句如下所示
-
GET hotel/_search { "query": { "bool": { "must": [ { "term": { "city": { "value": "上海" } } } ], "should": [ { "term": { "brand": { "value": "皇冠假日" } } }, { "term": { "brand": { "value": "華美達" } } } ], "must_not": [ { "range": { "price": { "lte": 500 } } } ], "filter": { "range": { "score": { "gte": 45 } } } } } }
-
這個DSL語句的意思通俗來說就是
- ①、城市必須是上海
- ②、品牌可以是皇冠假日或者華美達
- ③、價格必須小於等於500
- ④、得分必須大於等於45
-
-
示例
-
需求如下所示
- 搜索名字包含"如家酒店",價格不高於400,在坐標31.21,121.5,周圍10km範圍的酒店
-
分析
- ①、名稱搜索,屬於全文檢索查詢,應該參與算分
- ②、價格不高於400,用
range
過濾查詢,不參與算分(可以放到must_not
中,當然也可以放到filter
中,使用lte
表示小於等於400) - ③、周圍10km範圍內,用
geo_distance
查詢,屬於過濾條件,不參與算分,放到filter
中
-
DSL語句如下所示
-
GET hotel/_search { "query": { "bool": { "must": [ { "match": { "name": "如家酒店" } } ], "must_not": [ { "range": { "price": { "gt": 400 } } } ], "filter": { "geo_distance": { "distance": "10km", "location": { "lat": 31.21, "lon": 121.5 } } } } } }
-
PS:在kibana中編寫
filter
中的坐標信息的時候自動補全有些bug,kibana會報錯distacne_unit
和location
不能共存,所以應該把這個單位刪除,然後在distance
欄位上添加雙引號的同時帶上單位
-
-
2.4.3、RestAPI
-
代碼如下所示
-
/** * 複合查詢之布爾查詢 */ @Test public void testBooleanQuery() throws IOException { // 1. 創建查詢請求對象 SearchRequest searchRequest = new SearchRequest("hotel"); // 2. 添加要查詢的欄位 BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery(); // must MatchQueryBuilder brand = QueryBuilders.matchQuery("name", "如家酒店"); boolQueryBuilder.must(brand); // must_not RangeQueryBuilder price = QueryBuilders.rangeQuery("price").gt(400); boolQueryBuilder.mustNot(price); // filter GeoDistanceQueryBuilder location = QueryBuilders.geoDistanceQuery("location").point(new GeoPoint(31.21, 121.5)).distance("10km"); boolQueryBuilder.filter(location); searchRequest.source().query(boolQueryBuilder); // 3. 執行查詢,得到響應數據 SearchResponse response = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT); // 4. 處理響應數據 handlerResponse(response); }
-
2.5、複合查詢之算分函數查詢
2.5.1、使用場景
- 當我們使用
match
查詢的時候,文檔結果會根據搜索詞條的關聯度打分(_score
),返回結果時按照分值降序排序- 可以自行查詢查看驗證
- 在Elasticsearch中,早期使用的打分演算法是TF-IDF演算法,公式如下
- TF(詞條頻率):描述某一詞在一篇文檔中出現的頻繁程度。出現越多,分值越高,反之,分值月底
- IDF(逆文檔頻率):通過公式可以看到,詞條出現的文檔數量越多,分值越低,反之越高
- 在後來的5.1版本升級後,Elasticsearch將演算法改進為BM25演算法,公式如下
- TF-IDF演算法有一個缺陷,就是詞條頻率越高,文檔得分也會越高,單個詞條對文檔影響較大。而BM25則會讓單個詞條的演算法有一個上限,曲線更加平滑
- 根據相關度打分是比較合理的需求,但合理的不一定是產品經理需要的
- 以某度為例,在搜索的結果中,並不是相關度越高,排名越靠前;而是誰掏的錢多,排名就越靠前
- 要想人為控制相關性算分,就需要利用Elasticsearch中的function score查詢
2.5.2、DSL語句格式
- 可以通過下圖來理解算分函數查詢的DSL語句基本格式
- function score 查詢中包含四部分內容
- ①、原始查詢條件
- query部分,基於這個條件搜索文檔,並且基於BM25演算法給文檔打分,原始算分(query score)
- ②、過濾條件
- filter部分,符合該條件的文檔才會重新算分
- ③、算分函數
- 符合filter條件的文檔要根據這個函數做運算,得到的函數算分(function score),有四種函數
weight
:函數結果是常量field_value_factor
:以文檔中的某個欄位值作為函數結果random_score
:以隨機數作為函數結果script_score
:自定義算分函數演算法
- 符合filter條件的文檔要根據這個函數做運算,得到的函數算分(function score),有四種函數
- ④、運算模式
- 算分函數的結果、原始查詢的相關性算分,兩者之間的運算方式,包括
multiply
:相乘replace
:用function score替換query score- 其它,例如:
sum
、avg
、max
、min
- 算分函數的結果、原始查詢的相關性算分,兩者之間的運算方式,包括
- ①、原始查詢條件
- function score 的運行流程如下所示
- a. 根據原始條件查詢搜索文檔,並且計算相關性算分,稱為原始算分(query score)
- b. 根據過濾條件,過濾文檔
- c. 符合過濾條件的文檔,基於算分函數的運算,得到函數算分(function score)
- d. 將原始算分(query score)和函數算分(function score)基於運算模式做運算,得到最終結果,作為相關性算分
- 因此,其中的關鍵點是
- 過濾條件:決定哪些文檔的算分被修改
- 算分函數:決定函數算分的演算法
- 運算模式:決定最終算分結果
需求
-
讓"如家"這個品牌的酒店排名靠前一點
-
這個需求很簡單,可以理解為如下幾部分
- ①、原始條件:不確定,可以任意變化
- ②、過濾條件:
brand = "如家"
- ③、算分函數:可以簡單粗暴,直接使用
weight
給固定的算分結果 - ④、運算模式:比如求和
-
因此DSL語句如下所示
-
GET hotel/_search { "query": { "function_score": { "query": { "match": { "name": "酒店" } }, "functions": [ { "filter": { "term": { "brand": "如家" } }, "weight": 10 } ], "boost_mode": "sum" } } }
-
-
結果如下所示
-
原始搜索結果如下所示
2.5.3、RestAPI
-
代碼如下所示,可以對照著DSL語句進行編寫
-
/** * 複合查詢之算分函數查詢 */ @Test public void testFunctionScoreQuery() throws IOException { // 1. 創建查詢請求對象 SearchRequest searchRequest = new SearchRequest("hotel"); // 2. 添加查詢的請求體 searchRequest.source().query( // query QueryBuilders.functionScoreQuery( // function_score QueryBuilders.matchQuery("name", "酒店"), // match new FunctionScoreQueryBuilder.FilterFunctionBuilder[]{ // functions new FunctionScoreQueryBuilder.FilterFunctionBuilder( // filter QueryBuilders.termQuery("brand", "如家"), // term ScoreFunctionBuilders.weightFactorFunction(10) // weight ) } ).boostMode(CombineFunction.SUM) // boost_mode ); // 3. 執行查詢,獲取響應數據 SearchResponse response = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT); // 4. 處理響應數據 handlerResponse(response); }
-