文章目錄
一、什麼是文檔?
1.如何靈活存儲對象?
在面向對象編程中,幾乎所有的數據都是對象,一個對象可以簡單也可以很複雜,比如有時候想要表達一個複雜的實體,那麼我們的對象中可能會包含其他對象或者數組等等類型的數據。在傳統的關係型數據庫中,我們以行和列的形式去存儲一個對象的數據,在這個過程中我們必須對對象進行處理拆分讓其適應電子表格一樣的存儲媒介,在獲取的時候又需要重新轉化爲對象,這從一定程度上來說丟失了對象的靈活性。在Elasticsearch直接使用JSON格式來存儲對象數據,JSON是一種以人可讀的文本表示對象的方法, 它已經變成 NoSQL 世界交換數據的事實標準,當我們以JSON來存儲對象的時候,能更加專注於使用數據,而不是在電子表格的侷限性下對我們的應用建模。
在 Elasticsearch 中, 每個字段的所有數據 都是默認被索引的 。 即每個字段都有爲了快速檢索設置的專用倒排索引。而且,不像其他多數的數據庫,它能在同一個查詢中使用所有這些倒排索引,並以驚人的速度返回結果。
2.文檔與文檔元數據
在 Elasticsearch中,文檔有着特定的含義。它不是指我們存儲是放入的對象JSON,而是指最頂層或者根對象, 這個根對象還包括了文檔元數據(用來描述文檔的相關信息),根對象被序列化成 JSON 並存儲到 Elasticsearch 中,指定了唯一 ID。如下圖所示:
//一條用戶數據
{
"_index" : "users",
"_type" : "_doc",
"_id" : "1",
"_version" : 1,
"_seq_no" : 0,
"_primary_term" : 1,
"found" : true,
"_source" : {
"name" : "張三",
"age" : 25,
"about" : "我喜歡爬山",
"interests" : [
"跑步",
"爬山",
"音樂",
"打籃球"
]
}
}
_source字段中是我們的用戶數據,其他字段都屬於文檔元數據,其中最重要的三個元數據如下:
- _index:文檔在哪存放
- _type:文檔表示的對象類別
- _id:文檔唯一標識
通過這三個元數據可以唯一確定一個文檔。
二、對文檔的操作
1.索引文檔
通過使用 index API ,文檔可以被索引(存儲和使文檔可被搜索) 。但是首先,我們要確定文檔的位置。一個文檔的 _index 、 _type 和 _id 唯一標識一個文檔。 我們可以提供自定義的 _id 值,或者讓 index API 自動生成。如下圖所示索引一個文檔:
PUT /{index}/{type}/{id}
{
"field": "value",
...
}
或者可以這樣:
PUT /{index}/{type}/{id}/_create
{
"field": "value",
...
}
二者的區別在於前面的方式在文檔存在的時候將會替換更新文檔,而後面的這種方式會報告一個錯誤表示文檔已經存在了,不能創建。
2.修改文檔
在 Elasticsearch 中文檔是不可改變 的,不能修改它們。 相反,如果想要更新現有的文檔,需要重建索引或者進行替換。
2.1.全部替換
PUT /{index}/{type}/{id}
{
"field": "value",
...
}
使用這種方式將會替換整個文檔的內容,所以如果我們需要修改部分文檔內容的話應該先檢索文檔,然後修改,最後再通過這種方式進行替換更新。在內部,Elasticsearch 會將舊文檔標記爲已刪除,,但它並不會立即消失。當繼續索引更多的數據,Elasticsearch 會在後臺清理這些已刪除文檔。
2.2.部分更新
Elasticsearch提供了update API 來進行部分更新文檔,如下圖所示:
POST users/_update/1
{
"doc":{
"name" : "張三13",
"age" : 250,
"about" : "我喜歡爬山",
"interests" : [
"跑步",
"爬山",
"音樂",
"打籃球"
]
}
}
}
文檔是不可變的,他們不能被修改,只能被替換。 update API 必須遵循同樣的規則。 從外部來看,我們在一個文檔的某個位置進行部分更新。然而在內部, update API 簡單使用與之前描述相同的 檢索-修改-重建索引 的處理過程。 區別在於這個過程發生在分片內部,這樣就避免了多次請求的網絡開銷。
3.取回文檔
3.1.取回單個文檔
從 Elasticsearch 中檢索出文檔 ,仍然使用相同的 _index , _type , 和 _id ,但是 HTTP 謂詞 更改爲 GET :
Get users/_doc/1
響應體包括目前已經熟悉了的元數據元素,再加上 _source 字段,這個字段包含我們索引數據時發送給 Elasticsearch 的原始 JSON 文檔:
{
"_index" : "users",
"_type" : "_doc",
"_id" : "1",
"_version" : 6,
"_seq_no" : 9,
"_primary_term" : 7,
"found" : true,
"_source" : {
"name" : "張三13",
"age" : 250,
"about" : "我喜歡爬山",
"interests" : [
"跑步",
"爬山",
"音樂",
"打籃球"
]
}
}
返回文檔的一部分
默認情況下, GET 請求 會返回整個文檔,這個文檔正如存儲在 _source 字段中的一樣。但是也許你只對其中的 name 字段感興趣。單個字段能用 _source 參數請求得到,多個字段也能使用逗號分隔的列表來指定:
Get users/_doc/1?_source=name,age
現在 _source 字段只是包含我們請求的那些字段:
{
"_index" : "users",
"_type" : "_doc",
"_id" : "1",
"_version" : 6,
"_seq_no" : 9,
"_primary_term" : 7,
"found" : true,
"_source" : {
"name" : "張三13",
"age" : 250
}
}
或者,如果你只想得到 _source 字段,不需要任何元數據,你能使用 _source 端點:
Get users/_source/1
返回的的內容如下所示:
{
"name" : "張三13",
"age" : 250,
"about" : "我喜歡爬山",
"interests" : [
"跑步",
"爬山",
"音樂",
"打籃球"
]
}
3.2.取回多個文檔
使用 multi-get 或者 mget API 來將檢索請求放在一個請求中,將比逐個文檔請求更快地檢索到全部文檔,mget API 要求有一個 docs 數組作爲參數:
GET _mget/
{
"docs":[
{
"_index":"users",
"_id":1
},
{
"_index":"users",
"_id":2
},
{
"_index":"users",
"_id":3
},
{
"_index":"megacorp",
"_id":3
}
]
}
響應體也包含一個 docs 數組 , 對於每一個在請求中指定的文檔,這個數組中都包含有一個對應的響應,且順序與請求中的順序相同:
{
"docs" : [
{
"_index" : "users",
"_type" : "_doc",
"_id" : "1",
"_version" : 6,
"_seq_no" : 9,
"_primary_term" : 7,
"found" : true,
"_source" : {
"name" : "張三13",
"age" : 250,
"about" : "我喜歡爬山",
"interests" : [
"跑步",
"爬山",
"音樂",
"打籃球"
]
}
},
{
"_index" : "users",
"_type" : "_doc",
"_id" : "2",
"_version" : 1,
"_seq_no" : 1,
"_primary_term" : 1,
"found" : true,
"_source" : {
"name" : "張四",
"age" : 32,
"about" : "我喜歡爬黃山",
"interests" : [
"音樂"
]
}
},
{
"_index" : "users",
"_type" : "_doc",
"_id" : "3",
"_version" : 2,
"_seq_no" : 3,
"_primary_term" : 1,
"found" : true,
"_source" : {
"name" : "李四",
"age" : 35,
"about" : "我喜歡打網球",
"interests" : [
"跳舞",
"運動"
]
}
},
{
"_index" : "megacorp",
"_type" : "employee",
"_id" : "3",
"_version" : 4,
"_seq_no" : 10,
"_primary_term" : 1,
"found" : true,
"_source" : {
"first_name" : "Douglas",
"last_name" : "Fir",
"age" : 35,
"about" : "I like to build cabinets",
"interests" : [
"forestry"
]
}
}
]
}
每個文檔都是單獨檢索和報告的,即使有某個文檔沒有找到,也不影響其他文檔的檢索。
4.刪除文檔
刪除文檔 的語法和我們所知道的規則相同,只是 使用 DELETE 方法:
DELETE users/_doc/1
刪除文檔不會立即將文檔從磁盤中刪除,只是將文檔標記爲已刪除狀態。隨着你不斷的索引更多的數據,Elasticsearch 將會在後臺清理標記爲已刪除的文檔。
三、衝突處理與併發控制
Elasticsearch 是分佈式的。當文檔創建、更新或刪除時, 新版本的文檔必須複製到集羣中的其他節點。Elasticsearch 也是異步和併發的,這意味着這些複製請求被並行發送,並且到達目的地時也許順序是亂的 ,這個時候可能會出現舊版本的數據覆蓋了新版本數據的情況 ,因此需要一種方法確保文檔數據的正確性。
1.內部版本控制——元數據_version
我們獲取一個文檔時返回的信息是如下這樣的:
{
"_index" : "users",
"_type" : "_doc",
"_id" : "1",
"_version" : 6,
"_seq_no" : 9,
"_primary_term" : 7,
"found" : true,
"_source" : {
"name" : "張三13",
"age" : 250,
"about" : "我喜歡爬山",
"interests" : [
"跑步",
"爬山",
"音樂",
"打籃球"
]
}
}
上面返回的結果中我們可以看到有一個_version的字段,併發控制的關鍵就在於這個字段,每個文檔都有一個 _version (版本)號,當文檔被修改時版本號遞增。 Elasticsearch 使用這個 _version 號來確保變更以正確順序得到執行。利用 _version 號來確保 應用中相互衝突的變更不會導致數據丟失,我們通過指定想要修改文檔的 version 號來達到這個目的, 如果該版本不是當前版本號,我們的修改請求將會失敗。
2.通過外部系統使用版本控制
一個常見的設置是使用其它數據庫作爲主要的數據存儲,使用 Elasticsearch 做數據檢索, 這意味着主數據庫的所有更改發生時都需要被複制到 Elasticsearch ,如果多個進程負責這一數據同步,你可能遇到類似於之前描述的併發問題。
如果你的主數據庫已經有了版本號 或一個能作爲版本號的字段值比如 timestamp ,那麼你就可以在 Elasticsearch 中通過增加 version_type=external 到查詢字符串的方式把外部的版本號作爲當前文檔的版本號, 版本號必須是大於零的整數, 且小於Java 中 long 類型的正值。
外部版本號的處理方式和之前的內部版本號的處理方式有些不同:Elasticsearch 不是檢查當前 _version 和請求中指定的版本號是否相同, 而是檢查當前 _version 是否小於指定的版本號。 如果請求成功,外部的版本號作爲文檔的新 _version 進行存儲。
外部版本號不僅在索引和刪除請求是可以指定,而且在創建新文檔時也可以指定。例如,要創建一個新的具有外部版本號 5 的用戶,我們可以按以下方法進行:
PUT /users/_doc/4?version=5&version_type=external
{
"name": "李四",
"age": 10
}
四、批量操作
與 mget 可以使我們一次取回多個文檔同樣的方式, bulk API 允許在單個步驟中進行多次 create 、 index 、 update 或 delete 請求,bulk 與其他的請求體格式稍有不同,如下所示:
{ action: { metadata }}\n
{ request body }\n
{ action: { metadata }}\n
{ request body }\n
這種格式類似一個有效的單行 JSON 文檔 流 ,它通過換行符(\n)連接到一起。注意兩個要點:
- 每行一定要以換行符(\n)結尾, 包括最後一行 。這些換行符被用作一個標記,可以有效分隔行。
- 這些行不能包含未轉義的換行符,因爲他們將會對解析造成干擾。
action/metadata 行指定 哪一個文檔 做 什麼操作 ,必須是以下選項之一:
- create:如果文檔不存在,那麼就創建它。
- index:創建一個新文檔或者替換一個現有的文檔。
- update:部分更新一個文檔。
- delete:刪除一個文檔。
metadata 應該 指定被索引、創建、更新或者刪除的文檔的 _index 、 _type 和 _id 。例如,一個 delete 請求看起來是這樣的:
{ “delete”: { “_index”: “users”, “_type”: “_doc”, “_id”: “1” }}
request body 行由文檔的 _source 本身組成–文檔包含的字段和值。它是 index 、update 和 create 操作所必需的,delete操作不需要 request body 行。爲了把所有的操作組合在一起,一個完整的 bulk 請求 有以下形式:
POST /_bulk
{ "delete": { "_index": "users", "_type": "_doc", "_id": "1" }}
{ "create": { "_index": "users", "_type": "_doc", "_id": "2" }}
{ "name": "張三" }
{ "index": { "_index": "users", "_type": "_doc" }}
{ "title": "李四" }
{ "update": { "_index": "users", "_type": "_doc", "_id": "1" }
{ "doc" : {"name" : "張三123"} }
注意:delete 動作不能有請求體,它後面跟着的是另外一個操作。
每個子請求都是獨立執行,因此某個子請求的失敗不會對其他子請求的成功與否造成影響。這也意味着 bulk 請求不是原子的: 不能用它來實現事務控制,每個請求是單獨處理的。