在學習Elasticsearch的過程中想找一些可以系統的描述es操作的文章,但是官網沒有中文頁面,ES中文指南的排版和翻譯又很突兀和不協調,因此決定自己看一遍官方的maunal總結一下,由於沒時間把所有章節全部翻一遍,所以寫一篇學習筆記以便完成初步的學習。 概念總覽: 在描述ES的基本操作之前,首 ...
在學習Elasticsearch的過程中想找一些可以系統的描述es操作的文章,但是官網沒有中文頁面,ES中文指南的排版和翻譯又很突兀和不協調,因此決定自己看一遍官方的maunal總結一下,由於沒時間把所有章節全部翻一遍,所以寫一篇學習筆記以便完成初步的學習。
概念總覽:
在描述ES的基本操作之前,首先來介紹幾個概念:
Relational DB -> Databases -> Tables -> Rows -> Columns Elasticsearch -> Indices -> Types -> Documents -> Fields
以上是早期的官方文檔貼出的一個概念介紹圖,其含義不用多說,其實ES更適合與MongoDB類比:
MongoDB -> DBs -> Collections -> Documents -> Fields Elasticsearch -> Indices -> Types -> Documents -> Fields
ES里的Index可以看做一個庫,Documents相當於表的行,而Types相當於表。
但是Types的概念將會被逐漸弱化並可能在未來版本中刪除,而在Elasticsearch 6中,一個index下已經只能包含一個type了,因此可以將index理解為一個表,types意如其名僅用於展示一個document所屬的分類,實際上在本文對ES進行操作時由於index和type的一對一關係,許多時候查詢document已經只需要指定index而無需再指定type了。
本文使用Elasticsearch 6.5.4和Kibana 6.5.4下的環境進行演示。
一、Kibana命令行操作
使用Kibana操作ES是當前最簡單的一種方式,且提供命令補全、index名稱補全等便捷的功能。同時console界面的小扳手點進去還有和官方手冊里一樣的“copy as CURL”選擇,將選中的命令copy之後粘貼到linux中就會轉換為curl命令的格式,對於想要瞭解curl直接操作ES的同學是很有幫助的。
我個人並不建議直接使用curl操作ES,因為很多時候需要自己設置header,麻煩且低效。
Elasticsearch官方操作手冊地址:https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html
這裡參考官網的reference手冊對內置API進行詳細梳理,由於官方手冊的介紹方式不適用於我這種新手,我只能打亂順序學習,本部分的介紹基本遵循學習傳統資料庫的流程,主要分為以下7個部分:
Note:本文所有命令都是在Kibana console操作的,關於Kibana的安裝配置和使用,參考《Kibana安裝配置》一文。
1.數據結構搭建
結構的搭建主要包含index的創建和刪除、查詢等等,types無需創建。
#創建名為test的index,兩種寫法等同,名字不能包含特殊字元,只能小寫,不能以-, _, +開頭,不能超過255位元組。 PUT test PUT /test --PUT /test的本質是PUT http://ip:9200/test,kibana做了優化因此寫不寫之前的/無所謂 #當然你還可以直接插入一條數據,index會自動被創建 PUT leo/dramas/1 { "name":"權力的游戲" } #查看創建好的index的詳細信息 GET leo #刪除index DELETE leo #查詢當前所有的index,這裡調用了_cat的API GET _cat/indices
上圖為我測試創建的多個indices,每列的列名分別是:
health status index uuid pri rep docs.count docs.deleted store.size pri.store.size --其中pri表示number of shards,rep表示number of replicas,新建的index health為yellow的原因是未搭建集群。
在使用GET <index_name>查看index詳細信息時可以看到,每個index下都有一個名為mapping的屬性,這個屬性用於描述當前type下的大致field有哪些,當然也別忘了在6.5.4版本里一個index下只有一種type了。
2.增
即向ES插入數據:
#插入單條數據,用PUT或POST都可以 PUT test/books/1 {"name":"《阿Q正傳》","price":100} PUT test/books/2 {"name":"《鋼鐵是怎樣煉成的》","price":200} PUT test/books/3 {"name":"《西游記》","price":300}
插入多條數據,目前只能用_bulk API來實現,index表示新插入數據,create同理,在python的index()方法中op_type=create表示如果index不存在那麼直接創建index並插入數據,而op_type=index表示向已存在的index中插數據,此外還可以一起bulk delete、update等操作。
PUT test/books/_bulk { "index":{"_id":4}} {"name":"《圍城》","price":101} { "index":{"_id":5}} {"name":"《格林童話》","price":108} } #如果你不想設置主鍵_id,那麼可以直接置空,系統會創建預設主鍵,寫法如下: PUT test/books/_bulk { "index":{}} {"name":"《圍城》","price":101} { "index":{}} {"name":"《格林童話》","price":108}
查看下插入的數據:
GET test/books/_search {"query":{"match_all":{}}} GET test/books/_search {"query":{"match":{"_id":1}}} GET test/_search {"query": {"range": {"price": {"lte":1000} } } }
index下也有_search API因此這裡你也可以省略books直接查詢整個index所有types下的記錄,實際上在6版本中由於types概念的弱化(一個index只能有一種type)許多查詢都可以直接不寫type名了。
這裡的query和range以及lte都是DSL關鍵字,其實query只相當於模糊查詢或全文搜索。關於查詢,更系統的DSL(domain specific language)關鍵字及示例會在第5部分“查”補充。
3.刪
記錄的刪除通常由2個API,直接DELETE和POST _delete_by_query完成,示例如下:
#DELETE只能根據ID進行刪除,本例中刪除的是系統自定義的ID因此比較奇怪。 DELETE test/books/_mbEdGgBH8b_BYBmOW-C #_delete_by_query API允許你刪除符合query條件的記錄,其query body與上邊的查詢過濾的query body規則一樣。 POST test/_delete_by_query {"query": {"range": {"price": {"lte":1000} } } } #其實刪除、修改和查詢還涉及到多版本控制的概念,這個概念在傳統資料庫中已經很熟悉了,就是為了保證數據一致性的。 #關於版本控制的內容會在第6部分“版本控制”補充。
4.改
記錄更新也是2個API,_update和_update_by_query,前者根據ID進行更新,後者可以更新指定的query結果。此外你還可以不使用這兩個API直接像新插入數據那樣更新數據,只是此時你的body部分必須包含所有的fields了,否則操作完畢後你會發現document只剩下你所更新的那幾個fields,其他的全沒了。
至於為什麼刪除使用DELETE命令,而更新只能用_update的API,只是因為ES是RESTFUL風格的,http的指令有DELETE但並沒有UPDATE關鍵字。
更新涉及到版本控制以便維護數據一致性,其實分為兩個操作:get和reindex,大致步驟是:首先取到相應的document,然後執行更新script,最後返回執行的結果。至於具體的多版本控制機制將在第6部分解釋。
更新涉及的DSL語言也與其他操作很不一樣:
#_update API,表示將id為5的document的price改為100 POST test/books/5/_update {"script": {"source":"ctx._source.price=params.price", "lang":"painless", "params":{ "price":100 } }
這裡的script,source,lang,params都是DSL關鍵字,lang=painless表示使用painless腳本語言來編寫script來完成。
ctx我暫理解為當前事務,ctx._source表示當前定位的document,params表示本次更新用到的數據,source則表示更新操作,通俗來講就是用params的數據+source的操作一起完成更新。
#如果只是簡單的增加新field和刪除field那麼格式就比較簡單: POST test/books/5/_update { "script":"ctx._source.booktype='少兒童話'" } POST test/books/5/_update { "script":"ctx._source.remove('少兒童話')" } #此外ctx._source或ctx._source.<field_name>還有很多其他的方法和屬性,這裡貼一個官網的示例來作出引申,更多的示例慢慢實踐吧。 POST test/_doc/1/_update { "script" : { "source": "if (ctx._source.tags.contains(params.tag)) { ctx.op = 'delete' } else { ctx.op = 'none' }", "lang": "painless", "params" : { "tag" : "green" } } }
這個示例的含義就是:對於id=1的document,如果tags包含green字元,那麼刪掉這個document,否則不操作。至於contains是模糊匹配還是精確匹配,有興趣的可以花幾十秒做個測試。
5.查
前4個部分的示例中已經有許多查詢的示例了,這裡在之前的基礎上介紹一些比較複雜的查詢,首先來瞭解一個DSL的概念:
DSL:Domain Specific Language,ES提供一種基於JSON的查詢語言,這種查詢語言包含兩種子句模式:
1.Leaf query clauses
2.Compound query clauses
好吧,其實這裡介紹這兩個概念對理解複雜查詢毫無作用,我只是照搬下官方手冊,防止某天頓悟時找不到概念,接下來再看兩個DSL的概念:
Query一般來說包含兩各部分:query context 或 filter context:
舉例來說:
GET /_search { "query": { "bool": { "must": [ { "match": { "title": "Search" }}, { "match": { "content": "Elasticsearch" }} ], "filter": [ { "term": { "status": "published" }}, { "range": { "publish_date": { "gte": "2015-01-01" }}} ] } } }
這個例子的query就包含了所有2種context,可以見到他們的關係並不是並行的,filter包含在query.bool中,那麼filter和前邊的must匹配有什麼區別呢?
1.must--match進行的匹配是模糊匹配
比如上邊的title": "Search"不僅會把‘Search’匹配到,‘Search 123’也會匹配到,並且ES會為匹配到的每個記錄打分,稱作scoring,表示匹配程度。
2.而filter是精確匹配
表示在以上模糊匹配的結果中尋找精確匹配到的值,且filter到的記錄不會進行打分,term和range都是filter context的關鍵字,前者表示等值匹配,後者表示範圍匹配。
到這裡DSL的4個概念就介紹完了,是的全部介紹完了。官網總共也就這幾行,更多關於關鍵字的具體應用需要到特定的頁面且也通常都是一個簡單的示例完事,因此只能靠日常實踐了。
介紹完DSL那麼回到實際應用中來,用於查詢的API一般也是2種:直接通過GET index/doc_type/doc_id獲取,以及_search API
#GET獲取比較簡單,只要有id就可以了,沒id請使用_search API GET test/books/1 #_search API是查詢使用的核心API,包含諸如聚合、排序、集群查詢、explain API等等等等,這裡只貼個官方鏈接和一個示例算啦,重在實踐掌握。 https://www.elastic.co/guide/en/elasticsearch/reference/current/search.html POST /twitter/_search?routing=kimchy { "query": { "bool" : { "must" : { "query_string" : { "query" : "some query string here" } }, "filter" : { "term" : { "user" : "kimchy" } } } } } #這裡的?routing=kimchy是指在集群中查詢時可以指定名為kimchy的shard。
6.版本控制
Versionning,在官網中暫未找到獨立的說明頁面,只找到2篇古老的博客,分別是2011年和2013年的,地址如下:
https://www.elastic.co/blog/versioning
https://www.elastic.co/blog/elasticsearch-versioning-support
第一篇:
內容顯示versioning是由elasticsearch在0.15版本引入的新特性”樂觀併發控制“引申出來的,只介紹了每個document都會有個由系統控制自增的_version屬性,並未對版本控制機製作出細節解釋。
不過既然是樂觀併發控制我們可以參考傳統RDBMS資料庫中的樂觀鎖來理解,即資料庫伺服器會自動進行document快照存儲以便實現事務一致性,接下來看下第二篇博客(實際上看完第二篇博客,裡邊也確實介紹了樂觀鎖定)。
第二篇:
以一個經典的丟失更新示例來描述下樂觀併發控制的必要性:
#首先造一條數據 PUT bank_account/shanghai/1 { "name":"leo", "deposit":100 } GET bank_account/shanghai/1 #如下為插入的數據,可以看到_version屬性值為1 { "_index" : "bank_account", "_type" : "shanghai", "_id" : "1", "_version" : 1, "found" : true, "_source" : { "name" : "leo", "deposit" : 100 } } #如果這時候兩個商戶同時要從我賬戶里扣1塊錢,結果就是兩家同時取到我賬戶餘額為100,各扣了一元並把99的餘額寫入ES,這顯然是錯的。因此ES推出了versioning特性。
對於index中的每條記錄都會有一個_version的屬性,其取值範圍為:[1,2^63),插入數據時預設的_version都是1,每次對這個document進行修改或刪除操作都會使其+1,這個過程是由ES自己控制的。
總結一下Versioning的工作機制其實是這樣的,我們以一個投票計數案例為例,1表示球員的ID,每次有人為id=1的球員投票都將投票計數votes+1:
POST NBA/all_star_votes/1/_update?retry_on_conflict=5 {"script":"ctx._source.votes += 1"}
1.首先查詢到你要更新的documents。
2.然後進行version check,記下你查詢到的documents的_version。
3.更新時指定_version=<第二步中查到的version>
4.ES server端收到更新請求後開始進行衝突檢測,如果發現有人在這期間成功投了票(那麼_version就會變化),那麼直接返回一個http的409 conflict錯誤碼,如果可以更新那麼自然返回200 ok就好。
5.如果你顯式的設置了retry_on_conflict參數,那麼步驟四的表現還會有所變化:在發現記錄被更改後,server端會嘗試根據scripts將votes+1,然後將_version也+1,然後使用新的_version值和votes值進行更新,如果再次衝突那麼重覆之前的操作直到成功更新或達到retry_on_conflict的重覆次數。
以上操作據官方手冊說是節省了頻繁獲取/釋放鎖的開銷,versioning特性並非強制開啟的,只有你指定了version參數或者retry_on_conflict參數時,ES才會啟用versioning特性為你進行version check和衝突檢測。因此對於類似投票計數這種field的更新你可以開啟versionging特性,對於不規則的併發更新你可以棄用此特性直接使用程式隊列或者乾脆用關係型資料庫存儲數據,對於存款更新這種不規則併發更新的金融場景,併發請求之間不可能每次都增減相同的金額,使用retry_on_conflict顯然是無效的,這種場景用關係型資料庫顯然更安全。
當然對於delete操作來說versioning的表現又有所不同,因為如果一個系統頻繁的進行數據的刪除,那麼保存大量的舊version會導致資源迅速被耗盡,因此對於delete的記錄ES的預設保存version的時間是1min,這被稱作GC(垃圾回收),你可以通過修改index.gc_deletes參數來擴大此超時時間。
PS:官網沒說update操作留下的舊version是否也會被定期清除,這個可以試驗來驗證,插入一條數據多次更新後進行指定_version的查詢即可驗證,這裡節省時間懶的測了。
7.集群操作
集群操作這裡省略,會寫在單獨的集群搭建筆記中。
二、Python介面操作
你可以使用Python內置的REST API:requests module來進行es的操作,但是es提供了一種更加貼近elasticsearch概念體系的API:elasticsearch-py,因此這裡使用elasticsearch-py來進行演示。
elasticsearch API詳述:https://elasticsearch-py.readthedocs.io/en/master/api.html
Note:為與Python語言相容,避免出現關鍵字衝突,使用from_代替from,doc_type代替type參數。且為保持一致性和安全性,本介面推薦使用關鍵字傳參,不建議使用位置傳參。
先來一個簡單的演示示例:
# -*- coding: utf-8 -*- from elasticsearch import Elasticsearch es = Elasticsearch(hosts='http://10.0.1.49:9200/') es.delete_by_query(index="test",doc_type="books",body={"query": { "match_all":{}}}) #這裡的id=1/2在進入ES後就變為了預設主鍵,查詢時不能用id來查,而是要用_id。當然這裡的主鍵概念其實是借用了mongo或其他傳統關係型資料庫的概念,方便理解而已。 es.index(index="test", doc_type="books",id=1,body={"name": "《鋼鐵是怎樣煉成的》","price":100}) es.index(index="test", doc_type="books",id=2,body={"name": "《狂人日記》","price":200}) # res=es.search(index="test",doc_type="books",body={"query": {"match_all": {}}}) # print(res) res=es.search(index="test", doc_type="books", body={"query": {"range": {"price": { "lt":400} } }, "sort":{ "_id": {} # {"order":"desc"} } } ) print("%d documents found" % res['hits']['total']) for doc in res['hits']['hits']: print("%s) %s" % (doc['_id'], doc['_source']['name']))
這裡邊涉及到一些基礎的method,這些method的詳細參數和用法都可以在上邊貼出的elasticsearch API詳述網址中找到。
elasticsearch module包含CatClient, ClusterClient, IndicesClient, IngestClient, NodesClient, SnapshotClient and TasksClient等7個client子類以及一些其他暫無需介紹的類,此外還有一個底層訪問介面Elasticsearch類,你能且也只能通過Elasticsearch來訪問前述的7種介面。
定義Elasticsearch class的部分相關代碼為:
...... from ..transport import Transport from .indices import IndicesClient from .ingest import IngestClient from .cluster import ClusterClient from .cat import CatClient from .nodes import NodesClient from .remote import RemoteClient from .snapshot import SnapshotClient from .tasks import TasksClient class Elasticsearch(object): def __init__(self, hosts=None, transport_class=Transport, **kwargs): """ :arg transport_class: :class:`~elasticsearch.Transport` subclass to use. """ self.transport = transport_class(_normalize_hosts(hosts), **kwargs) # namespaced clients for compatibility with API names self.indices = IndicesClient(self) self.ingest = IngestClient(self) self.cluster = ClusterClient(self) self.cat = CatClient(self) self.nodes = NodesClient(self) self.remote = RemoteClient(self) self.snapshot = SnapshotClient(self) self.tasks = TasksClient(self) ......
另一種通俗的解釋方式就是:
當你定義了一個Elasticsearch實例後,會衍生N種諸如IndicesClient、IngestClient等實例,你可以根據自己的需求通過調用Elasticsearch的屬性來獲取這些實例,進而調用他們的各種method,這些屬性值可以是__init__方法中任意屬性,調用這些屬性後你就可以使用這些屬性實例的特有method了,這些client子類實例的屬性可以在上邊貼出的網址里學習,這裡只簡略貼一下核心介面類Elasticsearch的相關解釋:
class elasticsearch.Elasticsearch(hosts=None, transport_class=<class 'elasticsearch.transport.Transport'>, **kwargs)
hosts參數使用RESTFUL風格定義,即URL格式,類似上邊的'http://10.0.1.49:9200/'
除此之外你還可以使用SSL協議創建連接,其參數官網並未單獨列出,但可以通過其SSL連接示例獲知使用方式。
此class全部的method包含:
bulk(**kwargs) clear_scroll(**kwargs) count(**kwargs) create(**kwargs) delete(**kwargs) delete_by_query(**kwargs) delete_script(**kwargs) exists(**kwargs) exists_source(**kwargs) explain(**kwargs) field_caps(**kwargs) get(**kwargs) get_script(**kwargs) get_source(**kwargs) index(**kwargs) info(**kwargs) mget(**kwargs) msearch(**kwargs) msearch_template(**kwargs) mtermvectors(**kwargs) ping(**kwargs) put_script(**kwargs) reindex(**kwargs) reindex_rethrottle(**kwargs) render_search_template(**kwargs) scroll(**kwargs) search(**kwargs) search_shards(**kwargs) search_template(**kwargs) termvectors(**kwargs) update(**kwargs) update_by_query(**kwargs)