黑夜給了我黑色的眼睛,我卻用它尋找光明。前端
通過了解簡單的API和簡單搜索,已經基本上能應付大部分的使用場景。但是非關係型數據庫數據的文檔數據每每又多又雜,各類各樣冗餘的字段,組成了一條"記錄"。複雜的數據結構,帶來的就是複雜的搜索。因此在進入本章節前,咱們要構建一個儘量"複雜"的數據結構。java
下面分爲兩個場景,場景1偏向數據結構上的複雜而且介紹聚合查詢、指定字段返回、深分頁,場景2偏向搜索精度上的複雜。git
存儲一個公司的員工,員工信息包含姓名、工號、性別、出生年月日、崗位、上級、下級、所在部門、進入公司時間、修改時間、建立時間。其中員工工號做爲主鍵ID全局惟一,員工只有一個直屬上級,但有多個下級,能夠經過父子文檔實現。員工有可能屬於多個部門(特別是領導可能兼任多個部門的負責人)。程序員
建立索引並定義映射結構:github
PUT http://localhost:9200/company { "mappings":{ "employee":{ "properties":{ "id":{ "type":"keyword" }, "name":{ "type":"text", "analyzer":"ik_smart", "fields":{ "keyword":{ "type":"keyword", "ignore_above":256 } } }, "sex":{ "type":"keyword" }, "age":{ "type":"integer" }, "birthday":{ "type":"date" }, "position":{ "type":"text", "analyzer":"ik_smart", "fields":{ "keyword":{ "type":"keyword", "ignore_above":256 } } }, "level":{ "type":"join", "relations":{ "superior":"staff", "staff":"junior" } }, "departments":{ "type":"text", "analyzer":"ik_smart", "fields":{ "keyword":{ "type":"keyword", "ignore_above":256 } } }, "joinTime":{ "type":"date" }, "modified":{ "type":"date" }, "created":{ "type":"date" } } } } }
接下來是構造數據,咱們構造幾條關鍵數據。spring
更爲全面直觀的數據以下表所示:數據庫
姓名 | 工號 | 性別 | 年齡 | 出生年月日 | 崗位 | 上級 | 下級 | 部門 | 進入公司時間 | 修改時間 | 建立時間 |
---|---|---|---|---|---|---|---|---|---|---|---|
張三 | 1 | 男 | 49 | 1970-01-01 | 董事長 | / | 李四 | / | 1990-01-01 | 1562167817000 | 1562167817000 |
李四 | 2 | 男 | 39 | 1980-04-03 | 總經理 | 張三 | 王5、趙6、孫7、周八 | 市場部、研發部 | 2001-02-02 | 1562167817000 | 1562167817000 |
王五 | 3 | 女 | 27 | 1992-09-01 | 銷售 | 李四 | / | 市場部 | 2010-07-01 | 1562167817000 | 1562167817000 |
趙六 | 4 | 男 | 29 | 1990-10-10 | 銷售 | 李四 | / | 市場部 | 2010-08-08 | 1562167817000 | 1562167817000 |
孫七 | 5 | 男 | 26 | 1993-12-10 | 前端工程師 | 李四 | / | 研發部 | 2016-07-01 | 1562167817000 | 1562167817000 |
周八 | 6 | 男 | 25 | 1994-05-11 | Java工程師 | 李四 | / | 研發部 | 2018-03-10 | 1562167817000 | 1562167817000 |
插入6條數據:json
POST http://localhost:9200/company/employee/1?routing=1 { "id":"1", "name":"張三", "sex":"男", "age":49, "birthday":"1970-01-01", "position":"董事長", "level":{ "name":"superior" }, "joinTime":"1990-01-01", "modified":"1562167817000", "created":"1562167817000" }
POST http://localhost:9200/company/employee/2?routing=1 { "id":"2", "name":"李四", "sex":"男", "age":39, "birthday":"1980-04-03", "position":"總經理", "level":{ "name":"staff", "parent":"1" }, "departments":["市場部","研發部"], "joinTime":"2001-02-02", "modified":"1562167817000", "created":"1562167817000" }
POST http://localhost:9200/company/employee/3?routing=1 { "id":"3", "name":"王五", "sex":"女", "age":27, "birthday":"1992-09-01", "position":"銷售", "level":{ "name":"junior", "parent":"2" }, "departments":["市場部"], "joinTime":"2010-07-01", "modified":"1562167817000", "created":"1562167817000" }
POST http://localhost:9200/company/employee/4?routing=1 { "id":"4", "name":"趙六", "sex":"男", "age":29, "birthday":"1990-10-10", "position":"銷售", "level":{ "name":"junior", "parent":"2" }, "departments":["市場部"], "joinTime":"2010-08-08", "modified":"1562167817000", "created":"1562167817000" }
POST http://localhost:9200/company/employee/5?routing=1 { "id":"5", "name":"孫七", "sex":"男", "age":26, "birthday":"1993-12-10", "position":"前端工程師", "level":{ "name":"junior", "parent":"2" }, "departments":["研發部"], "joinTime":"2016-07-01", "modified":"1562167817000", "created":"1562167817000" }
POST http://localhost:9200/company/employee/6?routing=1 { "id":"6", "name":"周八", "sex":"男", "age":28, "birthday":"1994-05-11", "position":"Java工程師", "level":{ "name":"junior", "parent":"2" }, "departments":["研發部"], "joinTime":"2018-03-10", "modified":"1562167817000", "created":"1562167817000" }
GET http://localhost:9200/company/employee/_search { "query":{ "match":{ "departments":"研發部" } } }
GET http://localhost:9200/company/employee/_search { "query": { "bool":{ "must":[{ "match":{ "departments":"市場部" } },{ "match":{ "departments":"研發部" } }] } } }
*被搜索的字段是一個數組類型,但對查詢語句並無特殊的要求。數組
GET http://localhost:9200/company/employee/_search { "query": { "has_parent":{ "parent_type":"superior", "query":{ "match":{ "name":"張三" } } } } }
GET http://localhost:9200/company/employee/_search { "query": { "has_parent":{ "parent_type":"staff", "query":{ "match":{ "name":"李四" } } } } }
GET http://localhost:9200/company/employee/_search { "query": { "has_child":{ "type":"junior", "query":{ "match":{ "name":"王五" } } } } }
ES中的聚合查詢相似MySQL中的聚合函數(avg、max等),例如計算員工的平均年齡。前端工程師
GET http://localhost:9200/company/employee/_search?pretty { "size": 0, "aggs": { "avg_age": { "avg": { "field": "age" } } } }
指定字段返回值在查詢結果中指定須要返回的字段。例如只查詢張三的生日。
GET http://localhost:9200/company/employee/_search?pretty { "_source":["name","birthday"], "query":{ "match":{ "name":"張三" } } }
ES的深分頁是一個老生常談的問題。用過ES的都知道,ES默認查詢深度不能超過10000條,也就是page * pageSize < 10000。若是須要查詢超過1萬條的數據,要麼經過設置最大深度,要麼經過scroll
滾動查詢。若是調整配置,即便能查出來,性能也會不好。但經過scroll
滾動查詢的方式帶來的問題就是隻能進行"上一頁"、"下一頁"的操做,而不能進行頁碼跳轉。
scroll
原理簡單來說,就是一批一批的查,上一批的最後一個數據,做爲下一批的第一個數據,直到查完全部的數據。
首先須要初始化查詢
GET http://localhost:9200/company/employee/_search?scroll=1m { "query":{ "match_all":{} }, "size":1, "_source": ["id"] }
像普通查詢結果同樣進行查詢,url中的scroll=1m指的是遊標查詢的過時時間爲1分鐘,每次查詢就會更新,設置過長佔會用過多的時間。
接下來就能夠經過上述API返回的_scroll_id
進行滾動查詢,假設上面的結果返回"_scroll_id": "DnF1ZXJ5VGhlbkZldGNoBQAAAAAAAAFBFk1pNzdFUVhDU3hxX3VtSVFUdDJBWlEAAAAAAAABQhZNaTc3RVFYQ1N4cV91bUlRVHQyQVpRAAAAAAAAAUMWTWk3N0VRWENTeHFfdW1JUVR0MkFaUQAAAAAAAAFEFk1pNzdFUVhDU3hxX3VtSVFUdDJBWlEAAAAAAAABRRZNaTc3RVFYQ1N4cV91bUlRVHQyQVpR"
。
GET http://localhost:9200/_search/scroll { "scroll":"1m", "scroll_id": "DnF1ZXJ5VGhlbkZldGNoBQAAAAAAAAFBFk1pNzdFUVhDU3hxX3VtSVFUdDJBWlEAAAAAAAABQhZNaTc3RVFYQ1N4cV91bUlRVHQyQVpRAAAAAAAAAUMWTWk3N0VRWENTeHFfdW1JUVR0MkFaUQAAAAAAAAFEFk1pNzdFUVhDU3hxX3VtSVFUdDJBWlEAAAAAAAABRRZNaTc3RVFYQ1N4cV91bUlRVHQyQVpR" }
這種方式有一個小小的弊端,若是超過過時時間就不能繼續往下查詢,這種查詢適合一次全量查詢全部數據。但現實狀況有多是用戶在一個頁面停留很長時間,再點擊上一頁或者下一頁,此時超過過時時間頁面不能再進行查詢。因此還有另一種方式,範圍查詢。
假設員工數據中的工號ID是按遞增且惟一的順序,那麼咱們能夠經過範圍查詢進行分頁。
例如,按ID遞增排序,第一查詢ID>0的數據,數據量爲1。
GET http://localhost:9200/company/employee/_search { "query":{ "range":{ "id":{ "gt":0 } } }, "size":1, "sort":{ "id":{ "order":"asc" } } }
此時返回ID=1的1條數據,咱們再繼續查詢ID>1的數據,數據量仍然是1。
GET http://localhost:9200/company/employee/_search { "query":{ "range":{ "id":{ "gt":1 } } }, "size":1, "sort":{ "id":{ "order":"asc" } } }
這樣咱們一樣作到了深分頁的查詢,而且沒有過時時間的限制。
存儲商品數據,根據商品名稱搜索商品,要求準確度高,不能搜索洗面奶結果出現麪粉。
因爲這個場景主要涉及的是搜索的精度問題,因此並不會有複雜的數據結構,只有一個title字段。
定義一個只包含title字段且分詞器默認爲standard
的索引:
PUT http://localhost:9200/ware_index { "mappings": { "ware": { "properties": { "title":{ "type":"text" } } } } }
插入兩條數據:
POST http://localhost:9200/ware_index/ware { "title":"洗面奶" }
POST http://localhost:9200/ware_index/ware { "title":"麪粉" }
搜索關鍵字"洗面奶":
POST http://localhost:9200/ware_index/ware/_search { "query":{ "match":{ "title":"洗面奶" } } }
搜索結果出現了"洗面奶"和"麪粉"兩個風馬牛不相及的結果,這顯然不符合咱們的預期。
緣由在分詞一章中已經說明,text
類型默認分詞器爲standard
,它會將中文字符串一個字一個字拆分,也就是將"洗面奶"拆分紅了"洗"、"面"、"奶",將"麪粉"拆分紅了"面"、"粉"。而match
會將搜索的關鍵詞拆分,也就拆分紅了"洗"、"面"、"奶",最後兩個"面"都能匹配上,也就出現了上述結果。因此對於中文的字符串搜索咱們須要指定分詞器,而經常使用的分詞器是ik_smart
,它會按照最大粒度拆分,若是採用ik_max_word
它會將詞按照最小粒度拆分,也有可能形成上述結果。
DELETE http://localhost:9200/ware_index
刪除索引,從新建立並指定title字段的分詞器爲ik_smart
。
PUT http://localhost:9200/ware_index { "mappings":{ "ware":{ "properties":{ "id":{ "type":"keyword" }, "title":{ "type":"text", "analyzer":"ik_smart" } } } } }
這時若是插入「洗面奶」和「麪粉」,搜索「洗面奶」是結果就只有一條。但此時咱們插入如下兩條數據:
POST http://localhost:9200/ware_index/ware { "id":"1", "title":"新但願牛奶" }
POST http://localhost:9200/ware_index/ware { "id":"2", "title":"春秋上新短袖" }
搜索關鍵字」新但願牛奶「:
POST http://localhost:9200/ware_index/ware/_search { "query":{ "match":{ "title":"新但願牛奶" } } }
搜索結果出現了剛插入的2條,顯然第二條」春秋上新短袖「並非咱們想要的結果。出現這種問題的緣由一樣是由於分詞的問題,在ik
插件的詞庫中並無"新但願"一詞,因此它會把搜索的關鍵詞"新但願"拆分爲"新"和"但願",一樣在"春秋上新短袖"中"新"也並無組合成其它詞語,它也被單獨拆成了"新",這就形成了上述結果。解決這個問題的辦法固然能夠在ik
插件中新增"新但願"詞語,若是咱們在分詞中所作的那樣,但也有其它的辦法。
match_phrase
,短語查詢,它會將搜索關鍵字"新但願牛奶"拆分紅一個詞項列表"新 但願 牛奶",對於搜索的結果須要徹底匹配這些詞項,且位置對應,本例中的"新但願牛奶"文檔數據從詞項和位置上徹底對應,故經過match_phrase
短語查詢可搜索出結果,且只有一條數據。
POST http://localhost:9200/ware_index/ware/_search { "query":{ "match_phrase":{ "title":"新但願牛奶" } } }
儘管這能知足咱們的搜索結果,可是用戶實際在搜索中經常多是"牛奶 新但願"這樣的順序,但遺憾的是根據match_phrase
短語匹配的要求是須要被搜索的文檔須要徹底匹配詞項且位置對應,關鍵字"牛奶 新但願"被解析成了"牛奶 新 但願",儘管它與"新但願牛奶"詞項匹配但位置沒有對應,因此並不能搜索出任何結果。同理,此時若是咱們插入"新但願的牛奶"數據時,不管是搜索"新但願牛奶"仍是"牛奶新但願"均不能搜索出"新但願的牛奶"結果,前者的關鍵字是由於詞項沒有徹底匹配,後者的關鍵字是由於詞項和位置沒有徹底匹配。
因此match_phrase
也沒有達到完美的效果。
match_phrase_prefix
,短語前綴查詢,相似MySQL中的like "新但願%"
,它大致上和match_phrase_prefix
一致,也是須要知足文檔數據和搜索關鍵字在詞項和位置上保持一致,一樣若是搜索"牛奶新但願"也不會出現任何結果。它也並無達到咱們想要的結果。
前面兩種查詢中雖然能經過"新但願牛奶"搜索到咱們想要的結果,可是對於"牛奶 新但願"卻無能爲力。接下來的這種查詢方式能"完美"的達到咱們想要的效果。
先來看最低匹配度的查詢示例:
POST http://localhost:9200/ware_index/ware/_search { "query": { "match": { "title": { "query": "新但願牛奶", "minimum_should_match": "80%" } } } }
minimum_should_match
即最低匹配度。"80%"表明什麼意思呢?仍是要從關鍵字"新但願牛奶"被解析成哪幾個詞項提及,前面說到"新但願牛奶"被解析成"新 但願 牛奶"三個詞項,若是經過match
搜索,則含有"新"的數據一樣出如今搜索結果中。"80%"的含義則是3個詞項必須至少匹配80% * 3 = 2.4個詞項纔會出如今搜索結果中,向下取整爲2,即搜索的數據中須要至少包含2個詞項。顯然,"春秋上新短袖"只有1個詞項,不知足最低匹配度2個詞項的要求,故不會出如今搜索結果中。
一樣,若是搜索"牛奶 新但願"也是上述的結果,它並非短語匹配,因此並不會要求詞項所匹配的位置相同。
能夠推出,若是"minimum_should_match":"100%"
也就是要求徹底匹配,此時要求數據中包含全部的詞項,這樣會出現較少的搜索結果;若是"minimun_should_match:0"
此時並不表明一個詞項均可以不包含,而是隻須要有一個詞項就能出如今搜索結果,實際上就是默認的match
搜索,這樣會出現較多的搜索結果。
找到一個合適的值,就能有一個較好的體驗,根據二八原則,以及實踐代表,設置爲"80%"能知足大部分場景,既不會多出無用的搜索結果,也不會少。
基於Java客戶端(上),本文再也不贅述如何建立一個Spring Data ElasticSearch工程,也再也不作過多文字敘述。更多的請必定配合源碼使用,源碼地址https://github.com/yu-linfeng/elasticsearch6.x_tutorial/tree/master/code/spring-data-elasticsearch,具體代碼目錄在complex
包。
本章請必定結合代碼重點關注如何如何經過Java API進行父子文檔的數據插入,以及查詢。
父子文檔在ES中存儲的格式其實是以鍵值對方式存在,例如在定義映射Mapping時,咱們將子文檔定義爲:
{ ...... "level":{ "type":"join", "relations":{ "superior":"staff", "staff":"junior" } } ...... }
在寫入一條數據時:
{ ...... "level":{ "name":"staff", "parent":"1" } ...... }
對於於Java實體,咱們能夠把level
字段設置爲Map<String, Object>
類型。關鍵注意的是,在使用Spring Data ElasticSearch時,咱們不能直接調用sava
或者saveAll
方法。ES規定父子文檔必須屬於同一分片,也就是說在寫入子文檔時,須要定義routing
參數。下面是代碼節選:
BulkRequestBuilder bulkRequestBuilder = client.prepareBulk(); bulkRequestBuilder.add(client.prepareIndex("company", "employee", employeePO.getId()).setRouting(routing).setSource(mapper.writeValueAsString(employeePO), XContentType.JSON)).execute().actionGet();
必定參考源碼一塊兒使用。
ES實在是一個很是強大的搜索引擎。能力有限,實在不能將全部的Java API一一舉例講解,若是你在編寫代碼時,遇到困難也請聯繫做者郵箱hellobug at outlook.com,或者經過公衆號coderbuff,解答得了的必定解答,解答不了的一塊兒解答。
關注公衆號:CoderBuff,回覆「es」獲取《ElasticSearch6.x實戰教程》完整版PDF。