1、介紹
結構化搜索(Structured search) 是指有關探詢那些具有內在結構數據的過程。比如日期、時間和數字都是結構化的:它們有精確的格式,我們可以對這些格式進行邏輯操作。
比較常見的操作包括比較數字或時間的范圍,或判定兩個值的大小。
文本也可以是結構化的。如彩色筆可以有離散的顏色集合: 紅(red)
、 綠(green)
、 藍(blue)
。一個博客可能被標記了關鍵詞 分布式(distributed)
和 搜索(search)
。
電商網站上的商品都有 UPCs(通用產品碼 Universal Product Codes)或其他的唯一標識,它們都需要遵從嚴格規定的、結構化的格式。
在結構化查詢中,我們得到的結果 總是 非是即否,要么存於集合之中,要么存在集合之外。結構化查詢不關心文件的相關度或評分;它簡單的對文檔包括或排除處理。
這在邏輯上是能說通的,因為一個數字不能比其他數字 更 適合存於某個相同范圍。結果只能是:存於范圍之中,抑或反之。同樣,對於結構化文本來說,一個值要么相等,要么不等。沒有 更似 這種概念。
當進行精確值查找時, 要使用過濾器(filters)。過濾器很重要,因為它們執行速度非常快,不會計算相關度(直接跳過了整個評分階段)而且很容易被緩存,因此盡可能多的使用過濾式查詢。
2、term查詢數字
最為常用的 term
查詢, 可以用它處理數字(numbers)、布爾值(Booleans)、日期(dates)以及文本(text)
創建並索引一些表示產品的文檔,文檔里有字段 `price` 和 `productID` ( `價格` 和 `產品ID` ):
POST /my_store/products/_bulk { "index": { "_id": 1 }} { "price" : 10, "productID" : "XHDK-A-1293-#fJ3" } { "index": { "_id": 2 }} { "price" : 20, "productID" : "KDKE-B-9947-#kL5" } { "index": { "_id": 3 }} { "price" : 30, "productID" : "JODL-X-1937-#pV7" } { "index": { "_id": 4 }} { "price" : 30, "productID" : "QQPX-R-3956-#aD8" }
通常當查找一個精確值的時候,我們不希望對查詢進行評分計算。只希望對文檔進行包括或排除的計算,所以我們會使用 constant_score
查詢以非評分模式來執行 term
查詢並以一作為統一評分。
最終組合的結果是一個 constant_score
查詢,它包含一個 term
查詢:
GET /my_store/products/_search { "query" : { "constant_score" : { "filter" : { "term" : { "price" : 10 } } } } }
我們用 constant_score
將 term
查詢轉化成為過濾器,這個查詢所搜索到的結果與我們期望的一致:只有文檔 1 命中並作為結果返回(因為只有 1
的價格是 10)
3、term查詢文本
使用 term
查詢匹配字符串和匹配數字一樣容易。例如查詢產品號是XHDK-A-1293-#fJ3 的數據,也就是查詢文檔1
GET /my_store/products/_search { "query" : { "constant_score" : { "filter" : { "term" : { "productID" : "XHDK-A-1293-#fJ3" } } } } }
顯然沒有查詢到想要的結果,為什么呢?問題不在 term
查詢,而在於索引數據的方式,先查看productID的索引方式
GET /my_store/_analyze { "field": "productID", "text": "XHDK-A-1293-#fJ3" }
通過上面的結果,可以看到"XHDK-A-1293-#fJ3"這個數據被分成了四個部分,所以當我們用 term
查詢查找精確值 XHDK-A-1293-#fJ3
的時候,找不到任何文檔,因為它並不在我們的倒排索引中,
顯然這種對 ID 碼或其他任何精確值的處理方式並不是我們想要的。
為了避免這種問題,我們需要告訴 Elasticsearch 該字段具有精確值,要將其設置成 not_analyzed
無需分析的。
DELETE /my_store PUT /my_store { "mappings" : { "products" : { "properties" : { "productID" : { "type" : "string", "index" : "not_analyzed" } } } } }
注意:對Elastic 5.5版本以后的,string被text代替了,不過string還能用,而index對應的值是true或false。對應string類型的數據而言,not_analyzed這個數據還可以用,但是針對string類型數據,其它類型的數據不行。
刪除索引是必須的,因為我們不能更新已存在的映射。
在索引被刪除后,我們可以創建新的索引並為其指定自定義映射。
這里我們告訴 Elasticsearch ,我們不想對 productID
做任何分析。
現在我們可以為文檔重建索引:
POST /my_store/products/_bulk { "index": { "_id": 1 }} { "price" : 10, "productID" : "XHDK-A-1293-#fJ3" } { "index": { "_id": 2 }} { "price" : 20, "productID" : "KDKE-B-9947-#kL5" } { "index": { "_id": 3 }} { "price" : 30, "productID" : "JODL-X-1937-#pV7" } { "index": { "_id": 4 }} { "price" : 30, "productID" : "QQPX-R-3956-#aD8" }
再次查看productID的索引方式:
顯然XHDK-A-1293-#fJ3數據沒有被分析
重新查詢產品號是XHDK-A-1293-#fJ3 的數據
查詢成功
4、查找多個精確值
term
查詢對於查找單個值非常有用,但通常我們可能想搜索多個值。 如果我們想要查找價格字段值為 $20 或 $30 的文檔該如何處理呢?
不需要使用多個 term
查詢,我們只要用單個 terms
查詢(注意末尾的 s ), terms
查詢好比是 term
查詢的復數形式(以英語名詞的單復數做比)。
它幾乎與 term
的使用方式一模一樣,與指定單個價格不同,我們只要將 term
字段的值改為數組即可:
與 term
查詢一樣,也需要將其置入 filter
語句的常量評分查詢中使用:
GET /my_store/products/_search { "query" : { "constant_score" : { "filter" : { "terms" : { "price" : [20, 30] } } } } }
運行結果返回第二、第三和第四個文檔:
包含而不是相等
一定要了解 term
和 terms
是 包含(contains) 操作,而非 等值(equals) (判斷)。 如何理解這句話呢?
如果我們有一個 term(詞項)過濾器 { "term" : { "tags" : "search" } }
,它會與以下兩個文檔 同時匹配
5、范圍查找
實際上,對數字范圍進行過濾有時會更有用。例如,我們可能想要查找所有價格大於 $20 且小於 $40 美元的產品。
在 SQL 中,范圍查詢可以表示為:
Elasticsearch 有 range
查詢, 不出所料地,可以用它來查找處於某個范圍內的文檔:
range
查詢可同時提供包含(inclusive)和不包含(exclusive)這兩種范圍表達式,可供組合的選項如下:
gt
: >
大於(greater than)
lt
: <
小於(less than)
gte
: >=
大於或等於(greater than or equal to)
lte
: <=
小於或等於(less than or equal to)
下面是一個范圍查詢的例子:.
GET /my_store/products/_search { "query" : { "constant_score" : { "filter" : { "range" : { "price" : { "gte" : 20, "lt" : 40 } } } } } }
如果想要范圍無界(比方說 >20 ),只須省略其中一邊的限制:
"range" : { "price" : { "gt" : 20 } }
日期范圍
range
查詢同樣可以應用在日期字段上:
"range" : { "timestamp" : { "gt" : "2014-01-01 00:00:00", "lt" : "2014-01-07 00:00:00" } }
當使用它處理日期字段時, range
查詢支持對 日期計算(date math) 進行操作,比方說,如果我們想查找時間戳在過去一小時內的所有文檔:
"range" : { "timestamp" : { "gt" : "now-1h" } }
這個過濾器會一直查找時間戳在過去一個小時內的所有文檔,讓過濾器作為一個時間 滑動窗口(sliding window) 來過濾文檔。
日期計算還可以被應用到某個具體的時間,並非只能是一個像 now 這樣的占位符。只要在某個日期后加上一個雙管符號 (||
) 並緊跟一個日期數學表達式就能做到:
"range" : { "timestamp" : { "gt" : "2014-01-01 00:00:00", "lt" : "2014-01-01 00:00:00||+1M" } }
早於 2014 年 1 月 1 日加 1 月(2014 年 2 月 1 日 零時)
字符串范圍
range
查詢同樣可以處理字符串字段, 字符串范圍可采用 字典順序(lexicographically) 或字母順序(alphabetically)。例如,下面這些字符串是采用字典序(lexicographically)排序的:
5, 50, 6, B, C, a, ab, abb, abc, b
在倒排索引中的詞項就是采取字典順序(lexicographically)排列的,這也是字符串范圍可以使用這個順序來確定的原因。
如果我們想查找從 a
到 b
(不包含)的字符串,同樣可以使用 range
查詢語法:
"range" : { "title" : { "gte" : "a", "lt" : "b" } }
注意基數
數字和日期字段的索引方式使高效地范圍計算成為可能。 但字符串卻並非如此,要想對其使用范圍過濾,Elasticsearch 實際上是在為范圍內的每個詞項都執行 term
過濾器,這會比日期或數字的范圍過濾慢許多。
字符串范圍在過濾 低基數(low cardinality) 字段(即只有少量唯一詞項)時可以正常工作,但是唯一詞項越多,字符串范圍的計算會越慢。
6、處理Null值
有的文檔有名為 tags
(標簽)的字段,它是個多值字段, 一個文檔可能有一個或多個標簽,也可能根本就沒有標簽。如果一個字段沒有值,那么如何將它存入倒排索引中的呢?
這是個有欺騙性的問題,因為答案是:什么都不存。
如何將某個不存在的字段存儲在這個數據結構中呢?無法做到!簡單的說,一個倒排索引只是一個 token 列表和與之相關的文檔信息,如果字段不存在,那么它也不會持有任何 token,也就無法在倒排索引結構中表現。
最終,這也就意味着 ,null
, []
(空數組)和 [null]
所有這些都是等價的,它們無法存於倒排索引中。
顯然,世界並不簡單,數據往往會有缺失字段,或有顯式的空值或空數組。為了應對這些狀況,Elasticsearch 提供了一些工具來處理空或缺失值
存在查詢
第一件武器就是 exists
存在查詢。 這個查詢會返回那些在指定字段有任何值的文檔,讓我們索引一些示例文檔並用標簽的例子來說明:
1、tags
字段有 1 個值。
2、tags
字段有 2 個值。
3、tags
字段缺失。
4、tags
字段被置為 null
。
5、tags
字段有 1 個值和 1 個 null
。
以上文檔集合中 tags
字段對應的倒排索引如下:
我們的目標是找到那些被設置過標簽字段的文檔,並不關心標簽的具體內容。只要它存在於文檔中即可,用 SQL 的話就是用 IS NOT NULL
非空進行查詢:
在 Elasticsearch 中,使用 exists
查詢的方式如下:
GET /my_index/posts/_search { "query" : { "constant_score" : { "filter" : { "exists" : { "field" : "tags" } } } } } #或 GET /my_index/posts/_search { "query" : { "bool" : { "must":{ "exists" : { "field" : "tags" } } } } }
這個查詢返回 3 個文檔:
盡管文檔 5 有 null 值,但它仍會被命中返回。字段之所以存在,是因為標簽有實際值( search )可以被索引,所以 null 對過濾不會產生任何影響
顯而易見,只要 tags
字段存在項(term)的文檔都會命中並作為結果返回,只有 3 和 4 兩個文檔被排除
缺失查詢
它返回某個特定 _無_ 值字段的文檔,與以下 SQL 表達的意思類似:
轉成ElasticSearch語句如下:
GET /my_index/posts/_search { "query" : { "bool" : { "must_not":{ "exists" : { "field" : "tags" } } } } }
按照期望的那樣,我們得到 3 和 4 兩個文檔(這兩個文檔的 tags
字段沒有實際值):