概要
結構化搜索針對日期、時間、數字等結構化數據的搜索,它們有自己的格式,我們可以對它們進行范圍,比較大小等邏輯操作,這些邏輯操作得到的結果非黑即白,要么符合條件在結果集里,要么不符合條件在結果集之外,沒有那種相似的概念。
前言
結構化搜索將會有大量的搜索實例,我們將"音樂APP"作為主要的案例背景,去開發一些跟音樂APP相關的搜索或數據分析,有助力於我們理解實戰的目標,順帶鞏固一下學習的知識。
我們將一首歌需要的字段暫定為:
| name | code | type | remark |
|---|---|---|---|
| ID | id | keyword | 文檔ID |
| 歌手 | author | text | |
| 歌曲名稱 | name | text | |
| 歌詞 | content | text | |
| 語種 | language | text | |
| 標簽 | tags | text | |
| 歌曲時長 | length | long | 記錄秒數 |
| 喜歡次數 | likes | long | 點擊喜歡1次,自增1 |
| 是否發布 | isRelease | boolean | true已發布,false未發布 |
| 發布日期 | releaseDate | date |
我們手動定義的索引mapping信息如下:
PUT /music
{
"mappings": {
"children": {
"properties": {
"id": {
"type": "keyword"
},
"author_first_name": {
"type": "text",
"analyzer": "english"
},
"author_last_name": {
"type": "text",
"analyzer": "english"
},
"author": {
"type": "text",
"analyzer": "english",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"name": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"content": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"language": {
"type": "text",
"analyzer": "english",
"fielddata": true
},
"tags": {
"type": "text",
"analyzer": "english"
},
"length": {
"type": "long"
},
"likes": {
"type": "long"
},
"isRelease": {
"type": "boolean"
},
"releaseDate": {
"type": "date"
}
}
}
}
}
我們預先導入一批數據進去:
POST /music/children/_bulk
{ "index": { "_id": 1 }}
{ "id" : "34116101-7fa2-5630-a1a4-1735e19d2834", "author_first_name":"Peter", "author_last_name":"Gymbo", "author" : "Peter Gymbo", "name": "gymbo", "content":"I hava a friend who loves smile, gymbo is his name", "language":"english", "tags":["enlighten","gymbo","friend"], "length":53, "likes": 5, "isRelease":true, "releaseDate": "2019-12-20" }
{ "index": { "_id": 2 }}
{ "id" : "34117101-54cb-59a1-9b7a-82adb46fa58d", "author_first_name":"John", "author_last_name":"Smith", "author" : "John Smith", "name": "wake me, shark me", "content":"don't let me sleep too late, gonna get up brightly early in the morning", "language":"english", "tags":["wake","early","morning"], "length":55, "likes": 8,"isRelease":true, "releaseDate": "2019-12-21" }
{ "index": { "_id": 3 }}
{ "id" : "34117201-8d01-49d4-a495-69634ae67017", "author_first_name":"Jimmie", "author_last_name":"Davis", "author" : "Jimmie Davis", "name": "you are my sunshine", "content":"you are my sunshine, my only sunshine, you make me happy, when skies are gray", "language":"english", "tags":["sunshine","happy"], "length":65,"likes": 12, "isRelease":true, "releaseDate": "2019-12-22" }
{ "index": { "_id": 4 }}
{ "id" : "55fa74f7-35f3-4313-a678-18c19c918a78", "author_first_name":"Peter", "author_last_name":"Raffi", "author" : "Peter Raffi", "name": "brush your teeth", "content":"When you wake up in the morning it's a quarter to one, and you want to have a little fun You brush your teeth", "language":"english", "tags":"teeth", "length":45,"likes": 17, "isRelease":true, "releaseDate": "2019-12-22" }
{ "index": { "_id": 5 }}
{ "id" : "1740e61c-63da-474f-9058-c2ab3c4f0b0a", "author_first_name":"Jean", "author_last_name":"Ritchie", "author" : "Jean Ritchie", "name": "love somebody", "content":"love somebody, yes I do", "language":"english", "tags":"love", "length":38, "likes": 3,"isRelease":true, "releaseDate": "2019-12-22" }
精確值查找
我們根據文檔的mapping設計,可以按ID、按日期進行查找。
根據ID搜索歌曲
GET /music/children/_search
{
"query" : {
"constant_score" : {
"filter" : {
"term" : {
"id" : "34116101-7fa2-5630-a1a4-1735e19d2834"
}
}
}
}
}
注意ID建立時,類型是指定為keyword,這樣ID在索引時不會進行分詞。如果類型為text,UUID值在索引時會分詞,這樣反而查不到結果了。
按日期搜索歌曲
GET /music/children/_search
{
"query" : {
"constant_score" : {
"filter" : {
"term" : {
"releaseDate" : "2019-12-21"
}
}
}
}
}
按歌曲時長搜索
GET /music/children/_search
{
"query" : {
"constant_score" : {
"filter" : {
"term" : {
"length" : 53
}
}
}
}
}
搜索已發布的歌曲
GET /music/children/_search
{
"query" : {
"constant_score" : {
"filter" : {
"term" : {
"isRelease" : true
}
}
}
}
}
以上3個小例子可以發現:准確值搜索對keyword、日期、數字、boolean值天然支持。
組合過濾
前面的4個小例子都是單條件過濾的,實際的需求肯定會有多個條件,不過萬變不離其宗,再復雜的搜索需求,也是由一個一個的基礎條件復合而成的,我們來看幾個簡單的組合過濾的例子。
復習一下之前學過的邏輯:
- bool 組合多個條件,可以嵌套
- must 必須匹配
- should 可以匹配(類似於or,多個條件在should里)
- must_not 必須不匹配
搜索發布日期為2019-12-20,或歌曲ID為2a8f4288-c0a9-5c9b-8f99-67339b66f4c0,但發布日期不能是2019-12-21的歌曲
GET /music/children/_search
{
"query": {
"constant_score": {
"filter": {
"bool": {
"should": [
{"term":{
"releaseDate":"2019-12-20"
}},
{"term":{
"id":"2a8f4288-c0a9-5c9b-8f99-67339b66f4c0"
}}
],
"must_not": {
"term": {
"releaseDate":"2019-12-21"
}
}
}
}
}
}
}
搜索歌曲ID為2a8f4288-c0a9-5c9b-8f99-67339b66f4c0,或者是歌曲ID為34116101-7fa2-5630-a1a4-1735e19d2834而且發布日期為2019-12-20的帖子
GET /music/children/_search
{
"query": {
"constant_score": {
"filter": {
"bool": {
"should": [
{"term":{
"id":"2a8f4288-c0a9-5c9b-8f99-67339b66f4c0"
}},
{
"bool": {
"must" : [
{
"term" : {
"id":"34116101-7fa2-5630-a1a4-1735e19d2834"
}},
{ "term" : {
"releaseDate":"2019-12-20"
}}
]
}
}
]
}
}
}
}
}
多值搜索
使用語法terms,可以同時搜索多個值,類似mysql的in語句。
GET /music/children/_search
{
"query": {
"constant_score": {
"filter": {
"terms": {
"id": [
"34116101-7fa2-5630-a1a4-1735e19d2834",
"99268c7e-8308-569a-a975-bbce7d3f9a8e"
]
}
}
}
}
}
范圍查詢
針對Long類型和date類型的數據,是支持范圍查詢的,使用gt、lt、gte、lte來完成范圍的判斷。與mysql的>、<、>=、<=以及between...and異曲同工。
搜索時長在45-60秒之間的歌曲
對Long類型的范圍查詢,直接使用范圍表達式:
GET /music/children/_search
{
"query": {
"constant_score": {
"filter": {
"range": {
"length": {
"gte": 45,
"lte": 60
}
}
}
}
}
}
日期的范圍搜索
針對日期的范圍搜索,除了直接寫日期,加上常規的范圍表達式之外,還可以使用+1d、-1d表示對指定日期的加減,如"2019-12-21||-1d"表示"2019-12-20",也可以使用now-1d表示昨天,挺有趣。
給個示例:搜索2019-12-21前一天新發布的歌曲
GET /music/children/_search
{
"query": {
"constant_score": {
"filter": {
"range": {
"releaseDate" :{
"gt":"2019-12-21||-1d"
}
}
}
}
}
}
Null值處理
倒排索引在建立時,是不接受空值的,這就意味着null,[],[null]這些各種形式的null值,不無法存入倒排索引的,那這樣怎么辦?
Elasticsearch提供了兩種查詢,類似於mysql的is not null和not exists。
存在查詢
exists查詢,會返回那些指定字段有值的文檔,與mysql的is not null類似。
案例中的tags字段,就是一個選填項,有些記錄可能是null值,如果我需要查詢所有的tags值的記錄,請求如下:
GET /music/children/_search
{
"query": {
"constant_score": {
"filter": {
"exists": {
"field": "tags"
}
}
}
}
}
缺失查詢
缺失查詢原來是有關鍵字missing表示,效果與exists相反,語法上與mysql的is null類似,但6.x版本就已經廢棄了,我們可以改用must not + exists實現相同的效果。
還是使用tags字段為例,查詢tags為空的文檔:
GET /music/children/_search
{
"query": {
"bool": {
"must_not": {
"exists": {
"field": "tags"
}
}
}
}
}
filter緩存
過濾器為什么效率那么高?除了本身的設計集合來達到高效過濾之外,還將查詢結果適當地緩存化。
filter執行原理
我們了解一下Elasticsearch對過濾器的簡單操作:
- 根據fitler條件查找匹配的文檔,獲取document list。如果有多個過濾條件且涉及多個字段,那么就會有多個document list,document list是按倒排索引來的。
- 根據document list構建bitset(包含0或1的數組),匹配了是1,沒匹配上為0,如[1,0,0,0]。
- 迭代所有的bitset,從最稀疏的開始(可以排除到大量的文檔),取數組相同位置所有值為1的記錄。
- 將bitset緩存在內存中,用於提高性能。
filter比query好處是會caching,下次不用查倒排索引,filter大部分情況下在query之前執行query會計算doc對搜索條件的relevance score,還會根據這個score去排序
filter簡單過濾出想要的數據,不計算relevance score,也不排序
filter緩存
緩存條件
- 最近的256個filter中,某個filter超過一定次數(次數不固定),就會自動緩存這個filter對應的bitset。
- filter針對小segment獲取的結果,可以不緩存,segment<1000條或segment大小<index總大小的 3%。原因是數據量小,重新掃描很快,太小的segment在后台會自動合並到大的segment中,緩存意義不大
緩存更新
緩存的更新非常智能,增量更新的方式,如果有document新增或修改時,會將新文檔加入bitset,而不是刪除緩存或整個重新計算。
小結
本篇前半部分使用了大量的示例,可以快速閱讀,后面介紹了filter的過濾原理及緩存處理機制,可以了解一下,謝謝。
專注Java高並發、分布式架構,更多技術干貨分享與心得,請關注公眾號:Java架構社區
可以掃左邊二維碼添加好友,邀請你加入Java架構社區微信群共同探討技術

