首先,我們創建如下索引:
{
"settings": {
"index": {
"number_of_shards": 3,
"number_of_replicas": 2
}
},
"mappings": {
"properties": {
"brandName": {
"type": "text"
},
"categoryName": {
"type": "text"
},
"description": {
"type": "text"
},
"id": {
"type": "long"
},
"productName": {
"type": "text",
"fields": {
"raw": {
"type": "keyword",
"ignore_above": 256
}
}
},
"utime": {
"type": "date_nanos",
"index": false
}
}
}
}
es數據的插入
PUT /product/_doc/1
其中的product是索引名;_doc是類型(ES7后type都是_doc);1代表這條數據的主鍵;如果id不填也可以,ES會自動生成一條主鍵,不過這時就不能用PUT了,需要使用POST添加。
{
"id": 10001,
"productName": "牛肉片(花卉牌)",
"brandName": "花卉牌",
"categoryName": "零食",
"utime": 1614666414
}
查詢插入結果:
GET /product/_doc/1 //查詢ID為1的數據
插入多條數據:
{
"id": 10002,
"productName": "森馬秋冬小腳庫",
"brandName": "森馬",
"categoryName": "長褲",
"utime": 1614667414
}
{
"id": 10003,
"productName": "加絨加厚打底衫",
"brandName": "森馬",
"categoryName": "長衣",
"utime": 1614686414
}
es數據的修改
PUT /product/_doc/1 //PUT方式進行修改,這種是把原來對應文檔覆蓋掉
{
"id": 10001,
"productName": "牛肉片(潮汕集錦)",
"brandName": "潮汕集錦",
"categoryName": "零食",
"utime": 1614666414
}
POST /product/_doc/1/_update //POST方式修改的話,可以針對對應field來修改,比PUT要輕量
{
"doc": {
"categoryName":"小吃"
}
}
es數據的刪除
DELETE product/_doc/1 //刪除一個文檔
DELETE product //刪除索引
批量操作之bulk
bulk是es提供的一種批量增刪改的操作API。
bulk對JSON串有着嚴格的要求。每個JSON串不能換行,只能放在同一行,同時,相鄰的JSON串之間必須要有換行(Linux下是\n;Window下是\r\n)。bulk的每個操作必須要一對JSON串(delete語法除外)。
{ action: { metadata }}
{ request body }
{ action: { metadata }}
{ request body }
例如,假如現在要給example的docs中新增一個文檔。其表示如下:
POST _bulk
{"create": {"_index": "example", "_type": "_doc", "_id": 11}}
{"name": "test_bulk", "counter":"100"}
#查詢example所有數據,發現id為11的已經添加成功
GET example/_doc/_search
{
"query": {
"match_all": {}
}
}
bulk的操作類型
- create 如果文檔不存在就創建,但如果文檔存在就返回錯誤
- index 如果文檔不存在就創建,如果文檔存在就更新
- update 更新一個文檔,如果文檔不存在就返回錯誤
- delete 刪除一個文檔,如果要刪除的文檔id不存在,就返回錯誤
其實可以看得出來index是比較常用的。還有bulk的操作,某一個操作失敗,是不會影響其他文檔的操作的,它會在返回結果中告訴你失敗的詳細的原因。
索引及mapping准備
PUT example
{
"settings": {
"number_of_shards": 3,
"number_of_replicas": 2
},
"mappings": {
"properties": {
"id": {
"type": "long"
},
"name": {
"type": "text"
},
"counter": {
"type": "integer"
},
"tags": {
"type": "text"
}
}
}
}
批量新增
注:使用Postman執行。
PUT example/_bulk
{"index": {"_id": 1}}
{"id":1, "name": "admin", "counter":"10", "tags":["red", "black"]}
{"index": {"_id": 2}}
{"id":2, "name": "張三", "counter":"20", "tags":["green", "purple"]}
{"index": {"_id": 3}}
{"id":3, "name": "李四", "counter":"30", "tags":["red", "blue"]}
{"index": {"_id": 4}}
{"id":4, "name": "tom", "counter":"40", "tags":["orange"]}
#這里也有個換行的
注意:bulk語法要求必須兩行json后換行,末尾也有一個換行的。
批量修改
注:使用Postman執行。
POST example/_bulk
{"update": {"_id": 1}}
{"doc": {"id":1, "name": "admin-02", "counter":"11"}}
{"update": {"_id": 2}}
{"script":{"lang":"painless","source":"ctx._source.counter += params.num","params": {"num":2}}}
{"update":{"_id": 3}}
{"doc": {"name": "test3333name", "counter": 999}}
{"update":{"_id": 4}}
{"doc": {"name": "test444name", "counter": 888}, "doc_as_upsert" : true}
#這里有換行符
批量刪除
注:使用Postman執行。
POST example/_bulk
{"delete": {"_id": 1}}
{"delete": {"_id": 2}}
{"delete": {"_id": 3}}
{"delete": {"_id": 4}}
#這里有換行符
es數據的查詢
es中的查詢請求有兩種方式,一種是簡易版的查詢,另外一種是使用JSON完整的請求體,叫做結構化查詢(DSL)。
由於DSL查詢更為直觀也更為簡易,所以大都使用這種方式。
DSL查詢是POST過去一個json,由於post的請求是json格式的,所以存在很多靈活性,也有很多形式。
這里有一個地方注意的是官方文檔里面給的例子的json結構只是一部分,並不是可以直接黏貼復制進去使用的。一般要在外面加個query為key的結構。
查詢一條數據
GET /product/_doc/1
查詢結果:
{
"_index": "product",
"_type": "_doc",
"_id": "1",
"_version": 2,
"_seq_no": 1,
"_primary_term": 1,
"found": true,
"_source": {
"id": 10001,
"productName": "牛肉片(花卉牌)",
"brandName": "花卉牌",
"categoryName": "小吃",
"utime": 1614666414
}
}
查詢所有數據
GET /product/_doc/_search
{
"query": {
"match_all": {}
}
}
查詢結果:
{
"took": 6,
"timed_out": false,
"_shards": {
"total": 3,
"successful": 3,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 2,
"relation": "eq"
},
"max_score": 1,
"hits": [
{
"_index": "product",
"_type": "_doc",
"_id": "2",
"_score": 1,
"_source": {
"id": 10002,
"productName": "森馬秋冬小腳庫",
"brandName": "森馬",
"categoryName": "長褲",
"utime": 1614666414
}
},
{
"_index": "product",
"_type": "_doc",
"_id": "1",
"_score": 1,
"_source": {
"id": 10001,
"productName": "牛肉片(花卉牌)",
"brandName": "花卉牌",
"categoryName": "小吃",
"utime": 1614666414
}
}
]
}
}
返回結果說明:
- took字段表示該操作的耗時(單位為毫秒)。
- timed_out字段表示是否超時。
- hits字段表示搜到的記錄,數組形式。
- total:返回記錄數,本例是2條。
- max_score:最高的匹配程度,本例是1.0。
查詢結果只返回部分屬性
POST product/_doc/_search
{
"_source": [
"id", "productName"
],
"query": {
"match_all": {}
}
}
通過_source 字段來指定需要返回的字段。
將 _source 設置為 false, 可以不顯示原始字段,部分特殊場景下會用到。
或者
{
"_source": {
"includes": [
"id"
],
"excludes": [
"utime"
]
},
"query": {
"match_all": {}
}
}
其中includes代表需要返回的字段,excludes代表不要返回的字段。
注:這里是POST請求。
分頁查詢
Elasticsearch中數據都存儲在分片中,當執行搜索時每個分片獨立搜索后,數據再經過整合返回。那么,如何實現分頁查詢呢?
按照一般的查詢流程來說,如果我想查詢前10條數據:
- 客戶端請求發給某個節點
- 節點轉發給各個分片,查詢每個分片上的前10條
- 結果返回給節點,整合數據,提取前10條
- 返回給請求客戶端
那么當我想要查詢第10條到第20條的數據該怎么辦呢?這個時候就用到分頁查詢了。
在ElasticSearch中實現分頁查詢的方式有兩種,分別為深度分頁(from-size)和快照分頁(scroll)。
深度分頁(from-size)
深度分頁原理很簡單,就是查詢前20條數據,然后截斷前10條,只返回10-20的數據。這樣其實白白浪費了前10條的查詢。
查詢API如下:
POST product/_doc/_search
{
"from": 0,
"size": 2,
"query": {
"match_all": {}
}
}
其中,from定義了目標數據的偏移值,size定義當前返回的事件數目。默認from為0,size為10,即所有的查詢默認僅僅返回前10條數據。
在實際測試過程中,當此方式的訪問頁碼越高,其執行的查詢效率就越低。假設我們現在需要獲取第20頁的數據,ElasticSearch不得不取出所有分片上的第1頁到第20頁的所有文檔,並對其進行合並排序,最終再取出from后的size條作為最終的返回結果;假設我們現在服務器上有16個分片,則我們需要匯總到shards*(from+size)條記錄,即需要16*(20+10)條記錄后,對其進行整合再做一次全局排序。因此,當索引非常大時,我們是無法使用from+size方式做深分頁的,分頁越深越容易OOM或者消耗內存,所以ES使用index.max_result_window:10000作為保護措施來避免這種情況的發生。但實際上當訪問數據非常大時,我們采用scroll游標的方式來獲取數據是更好地一種選擇。
- 優點:數據量小的情況使用最方便,靈活性好,實現簡單
- 缺點:內存消耗大,速度一般,數據量大的情況面臨深度分頁問題
大數據量的快照分頁(scroll)
相對於from&size的分頁來說,使用scroll可以模擬一個傳統的游標來記錄當前讀取的文檔信息位置。采用此分頁方法,不是為了實時查詢數據,而是為了查詢大量甚至全部的數據。此方式相當於維護了一份當前索引的快照信息,在執行數據查詢時,scroll將會從這個快照信息中獲取數據。它相對於傳統的分頁方式來說,不是查詢所有數據再剔除掉不需要的部分,而是記錄一個讀取的位置來保證下次對數據的繼續獲取。
scroll是一種快照的查詢形式,快照一旦形成,本次滾動查詢內便無法查出來新增的那些數據,而且scroll是無法進行排序的,也無法指定from,那么我們想查看指定頁碼的數據就必須將該頁數據之前的全部數據取出來再進行丟棄,所以scroll一般用於導出全量數據。
可以把 scroll 分為初始化和遍歷兩步:
(1)初始化時將所有符合搜索條件的搜索結果緩存起來,可以想象成快照;
POST /product/_doc/_search?scroll=3m
{
"query": {"match_all": {}},
"size": 100
}
初始化的時候就像是普通的search一樣,其中的scroll=3m代表當前查詢的數據緩存3分鍾。
size:100 代表當前查詢100條數據
(2)遍歷時,從這個快照里取數據;
在遍歷時候,拿到上一次遍歷中的_scroll_id,然后帶scroll參數,重復上一次的遍歷步驟,直到返回的數據為空,表示遍歷完成。
每次都要傳參數scroll,刷新搜索結果的緩存時間,另外不需要指定index和type(不要把緩存的時時間設置太長,占用內存)。
POST /product/_doc/_search?scroll=3m
{
"scroll_id" : "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAD4WYm9laVYtZndUQlNsdDcwakFMNjU1QQ=="
}
- 優點:導出全量數據時性能最高
- 缺點:無法反應數據的實時性(快照版本),維護成本高,需要維護一個 scroll_id,並且不支持排序,只能按照文檔id排序
search_after
search_after有點類似scroll,但是和scroll又不一樣,它提供一個活動的游標,通過上一次查詢最后一條數據來進行下一次查詢同時可以實時查詢出來新增的數據且支持排序。但是不支持跳頁查詢,即每一次的查詢都需要依賴上一次的查詢結果,只能一頁一頁的往下翻。
- 優點:查詢性能最好,不存在深度分頁問題,能夠反映數據的實時變更
- 缺點:實現復雜,需要有一個全局唯一的字段,連續分頁的實現會比較復雜,因為每一次查詢都需要上次查詢的結果。
小結
在這幾種方式中,scroll方式適用於ES中索引數據很大的情況,因為scroll第一次請求數據時的時間相對於后面請求size大小的時間大得多(原因是因為此種方式會將滿足條件的所有索引數據都以快照的方式保存在內存中,然后后續的數據請求都直接可以獲取,因此第一次和之后的請求時間會差別比較大);當數據量比較小時,采用傳統的from&size方式的效率就會比較高。
無論是哪種方式,都要避免深分頁查詢。
排序
POST product/_doc/_search
{
"from": 0,
"size": 2,
"query": {
"match_all": {}
},
"sort": {
"id": {
"order": "desc"
}
}
}
上面按id降序排序。
假定我們想要結合使用id和 _score進行查詢,並且匹配的結果首先按照id排序,然后按照相關性得分排序。
POST product/_doc/_search
{
"from": 0,
"size": 2,
"query": {
"match_all": {}
},
"sort": [
{
"id": {
"order": "desc"
}
},
{
"_score": {
"order": "desc"
}
}
]
}
term查詢
精確查詢,不會對輸入做分詞,如果輸入的是"某某人",則直接查詢"某某人",如果輸入的是"某某事",則直接查詢"某某事"。
POST product/_doc/_search
{
"from": 0,
"size": 2,
"query": {
"term": {
"id": 10002
}
}
}
text類型term查詢不到
① 我想要進行查詢的字段在創建mapping時使用的“text”數據類型進行創建。
② 眾所周知text類型的數據在elasticsearch中會進行分詞並建立倒排索引,因此它會對每個詞進行索引,而不會建立整個句子的索引。
③ term搜索時會對整個句子作為關鍵詞進行搜索,由於沒有建立整個句子的關鍵詞索引,因此無法查找到東西。
match查詢
match搜索會先對搜索詞進行分詞,對於最基本的match搜索來說,只要搜索詞的分詞集合中的一個或多個存在於文檔中即可,例如,當我們搜索中國杭州,搜索詞會先分詞為中國和杭州,只要文檔中包含搜索和杭州任意一個詞,都會被搜索到。
POST /product/_doc/_search
{
"from": 0,
"size": 2,
"query": {
"match": {
"productName": "森馬"
}
}
}
match單字段多條件or/and查詢
我們知道match查詢會對輸入進行分詞,我們可以指定對分詞后的結果進行and和or查詢。
and:代表分詞后的所有結果都得匹配,or:代表分詞后只要有一個結果匹配就行(默認是or)
POST /product/_doc/_search
-- or查詢
{
"from": 0,
"size": 2,
"query": {
"match": {
"productName": {
"query": "森馬牛肉",
"operator": "or"
}
}
}
}
-- and查詢
{
"from": 0,
"size": 2,
"query": {
"match": {
"productName": {
"query": "森馬牛肉",
"operator": "and"
}
}
}
}
match_phrase短語查詢
match_phrase為按短語搜索,比如根據一個文本搜索:“我的寶馬多少馬力”,這個文本可能會被分詞成寶馬、多少、馬力三個短語,只有同時滿足這三個才能被搜索出來。
POST /product/_doc/_search
{
"from": 0,
"size": 2,
"query": {
"match_phrase": {
"productName": {
"query": "森馬秋冬"
}
}
}
}
完全匹配可能比較嚴,我們會希望有個可調節因子,少匹配一個也滿足,那就需要使用到slop。
POST /product/_doc/_search
{
"from": 0,
"size": 2,
"query": {
"match_phrase": {
"productName": {
"query": "森馬",
"slop" : "1"
}
}
}
}
multi_match查詢
多字段模糊查詢,和match類似都是模糊查詢,但multi_match可以指定多字段進行模糊查詢。
{
"from": 0,
"size": 2,
"query": {
"multi_match": {
"query": "森馬",
"fields": [
"brandName",
"categoryName"
]
}
}
}
這里根據brandName和categoryName字段進行模糊查詢。
但是multi_match就涉及到匹配評分的問題了。
我們希望完全匹配的文檔占的評分比較高,則需要使用best_fields
POST /product/_doc/_search
{
"from": 0,
"size": 2,
"query": {
"multi_match": {
"query": "森馬花卉牌",
"fields": [
"brandName",
"categoryName"
],
"type": "best_fields",
"tie_breaker": 0.3
}
}
}
意思就是完全匹配"森馬花卉牌"的文檔評分會比較靠前,如果只匹配森馬的文檔評分乘以0.3的系數。
我們希望越多字段匹配的文檔評分越高,就要使用most_fields。
POST /product/_doc/_search
{
"from": 0,
"size": 2,
"query": {
"multi_match": {
"query": "森馬花卉牌",
"fields": [
"brandName",
"categoryName"
],
"type": "most_fields"
}
}
}
我們會希望這個詞條的分詞詞匯是分配到不同字段中的,那么就使用cross_fields
POST /product/_doc/_search
{
"from": 0,
"size": 2,
"query": {
"multi_match": {
"query": "森馬花卉牌",
"fields": [
"brandName",
"categoryName"
],
"type": "cross_fields"
}
}
}
query_string查詢
query_string和match類似,但是match需要指定字段名,query_string是在所有字段中搜索,范圍更廣泛(當然query_string也支持指定字段查詢)。
POST /product/_doc/_search
{
"from": 0,
"size": 20,
"query": {
"query_string": {
"query": "森馬"
}
}
}
-- 指定字段
{
"from": 0,
"size": 20,
"query": {
"query_string": {
"query": "森馬",
"fields": [
"brandName",
"categoryName"
]
}
}
}
類似mysql的like查詢
創建一個索引:
PUT testEs
{
"mappings" : {
"properties" : {
"num" : {
"type" : "keyword"
},
"name" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
},
"englishName" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
},
"msg" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
}
}
}
}
插入一條數據:
PUT /testEs/_doc/1
{
"num": "123456",
"name": "小明",
"englishName": "xiaoMing",
"msg": "我愛學習"
}
使用Wildcard Query的通配符進行查詢
前提是查詢的字段類型是string類型,對應ES中的text,keyword(這種查詢方式會慢,查詢不進行分詞處理)
GET /testEs/_search
{
"query": {
"wildcard": {
"msg.keyword": "*愛學*"
}
}
}
使用match_phrase進行查詢
GET /testEs/_search
{
"query": {
"match_phrase": {
"msg": "愛學"
}
}
}
GET /testEs/_search
{
"query": {
"match_phrase": {
"num": "23"
}
}
}
查詢name和msg字段發現可以查詢出來,但是查詢num和englishName會發現查詢不出來數據。
這是因為ES默認會把中文進行單個字的分詞拆分,而對於英文和數字是基於空格進行拆分的,這顯然不符合我們對於mysql的like查詢,對此我們可以建立一個分詞器來解決。
建立一個帶分詞的索引
PUT /testEs2
{
"mappings" : {
"properties" : {
"num" : {
"type" : "text",
"analyzer": "my_analyzer"
},
"name" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
},
"analyzer": "my_analyzer"
},
"englishName" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
},
"analyzer": "my_analyzer"
},
"msg" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
},
"analyzer": "my_analyzer"
}
}
},
"settings": {
"index": {
"analysis": {
"analyzer": {
"my_analyzer": {
"type": "custom",
"tokenizer": "my_tokenizer"
}
},
"tokenizer": {
"my_tokenizer": {
"type": "ngram",
"min_gram": "1",
"max_gram": "2"
}
}
}
}
}
}
插入數據
PUT /testEs2/_doc/1
{
"num": "123456",
"name": "小明",
"englishName": "xiaoMing",
"msg": "我愛學習"
}
再進行測試查詢,發現可以查詢了
GET /testEs2/_search
{
"query": {
"match_phrase": {
"englishName": "in"
}
}
}
bool聯合查詢
如果我們想要請求"productName中帶寶馬,但是brandName中不帶寶馬"這樣類似的需求,就需要用到bool聯合查詢。
bool查詢包括四種子句:
- must:文檔必須完全匹配條件
- should:只要有一個或部分條件滿足
- must_not:文檔必須不匹配條件
- filter:返回的文檔必須滿足filter子句的條件。但是跟must不一樣的是,不會計算分值, 並且可以使用緩存
比如上面那個需求:
POST /product/_doc/_search
{
"query": {
"bool": {
"must": {
"term": {
"productName": "寶馬"
}
},
"must_not": {
"term": {
"brandName": "寶馬"
}
}
}
}
}
我們再來說filter過濾條件。從上面的描述來看,你應該已經知道,如果只看查詢的結果,must和filter是一樣的。區別是場景不一樣。如果結果需要算分就使用must,否則可以考慮使用filter。
使用filter過濾時間范圍:
{
"size": 1000,
"query": {
"bool": {
"must": [
{"term": {
"currency": "EUR"
}}
],
"filter": {
"range": {
"order_date": {
"gte": "2020-01-25T23:45:36.000+00:00",
"lte": "2020-02-01T23:45:36.000+00:00"
}
}
}
}
}
}
使用must過濾時間范圍
{
"size": 1000,
"query": {
"bool": {
"must": [
{"term": {
"currency": "EUR"
}},
{"range": {
"order_date": {
"gte": "2020-01-25T23:45:36.000+00:00",
"lte": "2020-02-01T23:45:36.000+00:00"
}
}}
]
}
}
}
filter比較高效的原理
為了說明filter查詢高效的原因,我們需要引入ES的一個概念 query context和 filter context。
query context關注的是,文檔到底有多匹配查詢的條件,這個匹配的程度是由相關性分數決定的,分數越高自然就越匹配。所以這種查詢除了關注文檔是否滿足查詢條件,還需要額外的計算相關性分數。
filter context關注的是,文檔是否匹配查詢條件,結果只有兩個,是和否。沒有其它額外的計算。它常用的一個場景就是過濾時間范圍。並且filter context會自動被ES緩存結果,效率進一步提高。
對於bool查詢,must使用的就是query context,而filter使用的就是filter context。