作者 | 蔡柱梁
審校 | 重樓
目錄
- ES是什么
- 倒排索引
- 使用ES必須知道的基本概念
- 了解常用的DSL
1 ES是什么
Elasticsearch 是一個分布式的 RESTful 搜索和分析引擎,可用來集中存儲您的數據,以便您對形形色色、規模不一的數據進行搜索、索引和分析。
上面是??官網-API文檔??對的定位描述。ES 是一個分布式的搜索引擎,數據存儲形式與我們常用的 MySQL 的存儲形式 — rows 不同,ES 會將數據以 JSON 結構存儲到一個文檔。一個文檔寫入 ES 后,我們可以在 1 秒左右查詢到它,因此我們稱 ES 在分布式中數據查詢是準實時的。
提問:那么這種將一行行數據變成
我們傳統的關系型數據庫一般的存儲形式是數據結構不固定,長度不固定。這時如果用關系型數據庫做存儲,那么我們表設計上,只能用一個
為了可以適應高并發,又能快速檢索、分析數據的搜索分析引擎,像倒排索引實現可以通過詞條快速查找文檔的,而倒排索引的實現與這種文檔存儲數據的方式密不可分。
ES 的適用場景所具有的特點:
2 倒排索引
倒排索引是文檔檢索系統中最常用的數據結構。
說到幫助搜索引擎檢索數據的數據結構,我們最熟悉的應該就是倒排索引了。過去很多人喜歡用字典來舉例,因為它的原理和我們使用中文字典查找漢字是相似的。

ES 會在我們保存一份文檔的時候,將文檔根據指定分詞器進行分詞,然后維護關鍵詞和文檔的關系——倒排索引。后面我們通過一些詞條進行檢索的時候,就可以通過這個索引找到對應相關的文檔。
2.1 例子
下面舉個例子。
插入兩份文檔,內容如下:
- we like java java java
- we like lucene lucene lucene
建立倒排索引大體流程如下:
- 首先對所有數據的內容進行拆分,拆分成唯一的一個個詞語(詞條)
- 然后建立詞條和對應文檔的對應關系,具體如下:
詞條? | (文檔ID,頻率)? | 詞條在文檔中的位置? |
we | (1,1) (2,1) | (0) (0) |
like | (1,1) (2,1) | (1) (1) |
java | (1,3) | (2,3,4) |
lucene | (2,3) | (2,3,4) |
注意:這里用表格來展示是為了方便理解,但是倒排索引其實是樹結構。
那這時我檢索詞條:

3 使用ES必須知道的基本概念
這里的概念是我們在使用過程中絕對無法繞開的概念,所以我們需要知道,否則無法和同事交流,哪怕僅僅是使用級別。
3.1 document(文檔)
在 ES 中,一份文檔相當于 MySQL 中的一行記錄,數據以 JSON 格式保存。文檔被更新時,版本號會被增加。
3.2 Index(索引)
存儲文檔的地方,類似 MySQL 中的表。
3.3 Mapping(映射)
映射是定義一個文件和它所包含的字段如何被存儲和索引的過程(??這是官方定義??)。
文檔里面有許多字段,這些字段有自己的類型,采用什么分詞器等等,我們可以通過。
3.4 type(類型)
這是比較老舊版本會用到的定義,在 ES5 的時代,它可以對 Index 做更精細地劃分,那個時代的 Index 更像 MySQL 的實例,而 type
類似 MySQL 的 table。
ES 5.x 中一個index可以有多種type。
ES 6.x 中一個index只能有一種type。
ES 7.x 以后,將逐步移除type這個概念,現在的操作已經不再使用,默認_doc。
4 了解常用的DSL
在 MySQL 中,我們經常使用 SQL 通過客戶端操作 MySQL,而 DSL 正是我們通過客戶端發送給 ES 的操作指令。
下面只寫一些現在我們常常接觸的簡單的 DSL,更多的請看 官網。
4.1 Index
官網API:??https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-index_.html??
4.1.1 創建索引
可以先建索引,再設置 mapping,也可以直接一次完成。
一次建好
PUT goods{
"mappings": {
"properties": {
"brand": {
"type": "keyword"
},
"category": {
"type": "keyword"
},
"num": {
"type": "integer"
},
"price": {
"type": "double"
},
"title": {
"type": "text",
"analyzer": "ik_smart"
},
"id": {
"type": "long"
}
}
}
}
4.1.2 查詢 index 信息
GET index_name
4.1.3 刪除 index
DELETE index_name
4.1.4 關閉 index
POST index_name/_close
當索引進入關閉狀態,是不能操作文檔的。
4.1.5 打開 index
POST index_name/_open
4.1.6 Aliases(別名) & Reindex
實際工作中,有很多情況可能都會需要重建 index,同時將舊的數據遷移到新 index 上,并且期望這個過程可以零停機,那么這時我們就可以用到
aliases 和 reindex 了。
事實上,我們程序訪問 index,很少是訪問真正的 indexName,一般我們會對 index
建別名,程序訪問的是別名。因為如果使用別名,那么此別名背后的索引需要進行更換的時候對程序可以做到無感知。
下面是一個需要添加分詞器而導致需要重建 index 和數據遷移的場景(這里只是舉個簡單場景,方便感受這些命令如何使用而已)。
1)先建立了一個 person,具體如下:
PUT person
{
"mappings" : {
"properties" : {
"address" : {
"type" : "text"
},
"age" : {
"type" : "integer"
},
"name" : {
"type" : "keyword"
}
}
}
}
2)后端程序訪問是用別名
POST _aliases
{
"actions": [
{
"add": {
"index": "person",
"alias": "person_index"
}
}
]
}
3)添加了一些數據
PUT person/_doc/1
{
"name": "test1",
"age": 18,
"address": "test address"
}
4)添加分詞器,更改 mapping 設置
PUT person2
{
"mappings" : {
"properties" : {
"address" : {
"type" : "text",
"analyzer": "ik_smart"
},
"age" : {
"type" : "integer"
},
"name" : {
"type" : "keyword"
}
}
}
}
5)別名操作(支持多個操作,并具有原子性)
POST /_aliases
{
"actions" : [
# 添加別名
{ "add" : { "index" : "person2", "alias" : "person_index" } }
]
}
這時我們后端程序只能對 person_index 進行讀操作,無法進行寫操作。
6)將 person 中的數據導入到 person2 中(如果是不同進程,支持遠程訪問)
POST _reindex
{
"source": {
"index": "person"
},
"dest": {
"index": "person2"
}
}
7)去掉 person
POST /_aliases
{
"actions" : [
# 將 person 從別名 person_index 中移除
{ "remove" : { "index" : "person", "alias" : "person_index" } }
]
}
這時后端程序對 person_index 的讀寫操作均恢復正常。
更多信息可以查閱官網:??reindex??? ??aliases??
4.2 設置 Mapping
添加 index。
PUT person
{
"settings": {
"number_of_shards": 1,
"number_of_replicas": 1
}
}
已經建好索引 person,但是沒有設置 mapping,現在設置。
PUT person/_mapping
{
"properties": {
"name": {
"type": "keyword"
},
"age": {
"type": "integer"
},
"address":{
"type": "text",
"analyzer": "ik_max_word"
}
}
}
index 確定后,不能修改已有字段,只能添加,以下增加一個 test字段作為例子。
PUT person/_mapping
{
"properties": {
"test": {
"type": "text"
}
}
}
查詢 mapping 信息
4.3 使用頻率較高的查詢
這里只寫一些比較常接觸的語句,不過像 wildcard 這種,也有很多公司是禁止使用的,所以用的時候一定要了解公司規范要求。
先設置一個商品 index,具體如下:
PUT goods
{
"mappings": {
"properties": {
"brand": {
"type": "keyword"
},
"category": {
"type": "keyword"
},
"num": {
"type": "integer"
},
"price": {
"type": "double"
},
"title": {
"type": "text",
"analyzer": "ik_smart"
},
"id": {
"type": "long"
}
}
}
}
字段說明:
- title:商品標題
- price:商品價格
- num:商品庫存
- category:商品類別
- brand:品牌名稱
4.3.1 分頁與排序
# GET 索引庫名稱/_search,默認展示10條數據
GET goods/_doc/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"price": {
"order": "desc" # 根據價格降序排序
}
}
],
"from": 0, # 從哪一條開始
"size": 20 # 顯示多少條
}
4.3.1.1 深度翻頁
ES 深度分頁存在的問題:
- 性能問題
- 深度分頁會導致搜索引擎遍歷大量的數據,因此會對性能產生負面影響。尤其是在數據量龐大的情況下,可能會導致搜索請求變得非常慢。
- 排序問題
- 這是由于不同分片上的數據排序不一致所導致的(每個分片需要將自己的處理結果給到協調節點,再由協調節點來計算出最后的結果)。
- 索引更新問題
- 如果在進行深度分頁時,索引被更新了,那么可能會導致部分數據被遺漏或重復顯示(為了避免這個問題,可以使用游標或滾動搜索等機制來遍歷數據)。
- 內存問題
- 在進行深度分頁時,Elasticsearch 需要將所有的搜索結果都存儲在內存中。如果結果集非常大,那么會占用大量的內存,甚至可能導致內存溢出(為了避免這個問題,可以使用游標或滾動搜索等機制來逐步處理數據)。
在 Elasticsearch 7.0 之前,我們是采用 scroll 來解決深度分頁的,但是到了 Elasticsearch 7.0 就開始不再推薦采用
scroll 了,推薦采用 search_after。
4.3.1.1.1 scroll
詳細請看??官方文檔??。
以下例子來自于官網
1)先查詢并生成快照
scroll=1m 是保留1分鐘快照的意思,即是符合當前查詢條件的數據的結果集合保留快照1分鐘
POST /index_name/_search?scroll=1m
{
"size": 100,
"query": {
"match": {
"message": "foo"
}
}
}
假設返回的 scroll_id 是
DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAD4WYm9laVYtZndUQlNsdDcwakFMNjU1QQ==
2)那么,我們就可以使用這個 ID 進行滾動翻頁了
POST /_search/scroll
{
"scroll" : "1m", # 快照保持1分鐘,重新計時
"scroll_id" : "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAD4WYm9laVYtZndUQlNsdDcwakFMNjU1QQ=="
}
3)查詢完后,記得刪除游標
DELETE /_search/scroll
{
"scroll_id" : "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAD4WYm9laVYtZndUQlNsdDcwakFMNjU1QQ=="
}
這里詳細說下游標的工作方式:
當第一次發起 scroll 請求時,ES 會創建一個包含搜索結果的快照,并返回一個唯一的滾動 ID。在接下來的每個 scroll
請求中,都需要帶上這個滾動 ID,表示要獲取與該搜索上下文匹配的下一批結果。因為每個 scroll
請求都使用了相同的搜索上下文,所以每個請求返回的結果都是相同的,只是可能包含不同的文檔。如果 scroll 請求返回的結果集合大小不足以填滿請求的大小限制,則
ES 會在后臺繼續搜索,并將結果添加到當前結果集中,直到結果集合大小達到請求的大小限制或搜索完成為止。
由于 scroll 機制的實現方式,每次請求返回的結果可以是任意大小,可以避免一次性讀取所有結果可能導致的內存問題。同時,由于滾動 ID
只在指定的時間段內有效,所以可以在不消耗過多內存的情況下,分批次處理大量數據。但是,需要注意的是,如果時間段設置得過短,可能會導致滾動 ID
過期,需要重新發起搜索請求。
4.3.1.1.2 search_after
詳細請看??官網??。
以下例子來自于官網
1)先查詢
GET twitter/_search
{
"query": {
"match": {
"title": "elasticsearch"
}
},
"sort": [
{"date": "asc"},
{"tie_breaker_id": "asc"}
]
}
假設響應如下:
{
"took" : 17,
"timed_out" : false,
"_shards" : ...,
"hits" : {
"total" : ...,
"max_score" : null,
"hits" : [
...
{
"_index" : "twitter",
"_id" : "654322",
"_score" : null,
"_source" : ...,
"sort" : [
1463538855,
"654322"
]
},
{
"_index" : "twitter",
"_id" : "654323",
"_score" : null,
"_source" : ...,
"sort" : [
1463538857,
"654323"
]
}
]
}
}
2)接著,使用上面響應結果中最后一個文檔的排序鍵

作為參數傳遞到下一次查詢中(這里其實就是對應了查詢示例中的兩個排序字段 date 和 tie_breaker_id)
GET twitter/_search
{
"query": {
"match": {
"title": "elasticsearch"
}
},
"search_after": [1463538857, "654323"],
"sort": [
{"date": "asc"},
{"tie_breaker_id": "asc"}
]
}
這里有一個問題,如果我在第2頁準備翻到第3頁時,refresh 了可能會打亂排序,那么這個分頁的結果就不對了。為了避免這種情況,我們可以使用 PIT 來保存當前搜索的索引狀態。
具體使用如下:
1)先得到 PIT ID
POST /index_name/_pit?keep_alive=1m
響應如下:
{
"id": "46ToAwMDaWR5BXV1aWQyKwZub2RlXzMAAAAAAAAAACoBYwADaWR4BXV1aWQxAgZub2RlXzEAAAAAAAAAAAEBYQADaWR5BXV1aWQyKgZub2RlXzIAAAAAAAAAAAwBYgACBXV1aWQyAAAFdXVpZDEAAQltYXRjaF9hbGw_gAAAAA=="
}
2)使用 PIT ID 搜索
GET /_search
{
"size": 10000,
"query": {
"match" : {
"user.id" : "elkbee"
}
},
"pit": {
"id": "46ToAwMDaWR5BXV1aWQyKwZub2RlXzMAAAAAAAAAACoBYwADaWR4BXV1aWQxAgZub2RlXzEAAAAAAAAAAAEBYQADaWR5BXV1aWQyKgZub2RlXzIAAAAAAAAAAAwBYgACBXV1aWQyAAAFdXVpZDEAAQltYXRjaF9hbGw_gAAAAA==",
"keep_alive": "1m"
},
"sort": [
{"@timestamp": {"order": "asc", "format": "strict_date_optional_time_nanos", "numeric_type" : "date_nanos" }}
]
}
響應如下:
{
"pit_id" : "46ToAwMDaWR5BXV1aWQyKwZub2RlXzMAAAAAAAAAACoBYwADaWR4BXV1aWQxAgZub2RlXzEAAAAAAAAAAAEBYQADaWR5BXV1aWQyKgZub2RlXzIAAAAAAAAAAAwBYgACBXV1aWQyAAAFdXVpZDEAAQltYXRjaF9hbGw_gAAAAA==",
"took" : 17,
"timed_out" : false,
"_shards" : ...,
"hits" : {
"total" : ...,
"max_score" : null,
"hits" : [
...
{
"_index" : "my-index-000001",
"_id" : "FaslK3QBySSL_rrj9zM5",
"_score" : null,
"_source" : ...,
"sort" : [
"2021-05-20T05:30:04.832Z",
4294967298
]
}
]
}
}
3)pit id + 排序鍵 翻頁
GET /_search
{
"size": 10000,
"query": {
"match" : {
"user.id" : "elkbee"
}
},
"pit": {
"id": "46ToAwMDaWR5BXV1aWQyKwZub2RlXzMAAAAAAAAAACoBYwADaWR4BXV1aWQxAgZub2RlXzEAAAAAAAAAAAEBYQADaWR5BXV1aWQyKgZub2RlXzIAAAAAAAAAAAwBYgACBXV1aWQyAAAFdXVpZDEAAQltYXRjaF9hbGw_gAAAAA==",
"keep_alive": "1m"
},
"sort": [
{"@timestamp": {"order": "asc", "format": "strict_date_optional_time_nanos"}}
],
"search_after": [
"2021-05-20T05:30:04.832Z",
4294967298
],
"track_total_hits": false
}
4)查詢完后,刪除 PIT
DELETE /_pit
{
"id" : "46ToAwMDaWR5BXV1aWQyKwZub2RlXzMAAAAAAAAAACoBYwADaWR4BXV1aWQxAgZub2RlXzEAAAAAAAAAAAEBYQADaWR5BXV1aWQyKgZub2RlXzIAAAAAAAAAAAwBYgACBXV1aWQyAAAFdXVpZDEAAQltYXRjaF9hbGw_gAAAAA=="
}
scroll 和 search after 都是用來處理大數據時避免深度翻頁的,它們區別如下:
- 實現方式不同
- scroll 使用游標來保持搜索上下文,而 search after 使用排序鍵來跟蹤搜索進度。
- 參數設置不同
- scroll 需要指定一個時間段來保持搜索上下文,而 search after 需要指定一個排序字段和一個起始排序鍵來開始搜索。
- 數據處理方式不同
- scroll 適用于一次性處理所有數據的場景,每次請求返回的結果可以是任意大小,直到搜索上下文過期或搜索完成為止。而。
- 排序方式不同
- scroll 可能會導致排序不穩定的問題,而 search after 使用排序鍵來跟蹤搜索進度,可以避免這個問題。
- 兼容性不同
- scroll 是 Elasticsearch 5.x 及之前版本的遺留功能,而 search after 是 Elasticsearch 7.0 中引入的新特性,Elasticsearch 7.0 開始推薦使用 search after。
4.3.2 match
想對搜索關鍵字進行分詞,搜索的結果更全面。
特點
- 會對查詢條件進行分詞
- 然后將分詞后的查詢條件和詞條進行等值匹配
- 默認取并集
GET goods/_search
{
"query": {
"match": {
"title": "華為手機"
}
}
}
# 指定取交集
GET goods/_search
{
"query": {
"match": {
"title": {
"query": "華為手機",
"operator": "and"
}
}
}
}
4.3.3 term
不想對搜索關鍵字進行分詞,搜索的結果更加精確。
GET goods/_search
{
"query": {
"term": {
"title": {
"value": "華為"
}
}
}
}
4.3.4 range
當想對數值類型的字段做區間的搜索,例如商品價格。
# 價格大于等于2000,小于等于3000
# gte: >= lte:<= gt:> lt:<
GET goods/_search
{
"query": {
"range": {
"price": {
"gte": 2000,
"lte": 3000
}
}
}
}
4.3.5 wildcard
當使用match搜索仍然查詢不到數據,可以嘗試使用模糊查詢,范圍更廣。
GET goods/_search
{
"query": {
"match": {
"title": "華"
}
}
}
運行結果:

可以發現查詢的結果中,那些title包含“華為”的數據查不出來,因為那些數據,沒有分出"華"這一個字,而分出的就是"華為",這個時候我們若想把包含"華為"的數據都查出來,就可以使用模糊查詢。
4.3.6 query_string
當不知道搜索的內容存儲在哪個字段時,可以使用字符串搜索。
特點
- 會對查詢條件進行分詞
- 將分詞后的查詢條件和詞條進行等值匹配
- 默認取并集(OR)
- 可以指定多個查詢字段
1)不指定字段
GET goods/_search
{
"query": {
"query_string": {
"query": "華為手機"
}
}
}
2)指定字段
GET goods/_search
{
"query": {
"query_string": {
"fields": ["title", "brand"],
"query": "華為手機"
}
}
}
運行結果:

4.3.7 bool?
當存在多個查詢條件時
語法
must(and):條件必須成立。
must_not(not):條件必須不成立,必須和must或filter連接起來使用。
should(or):條件可以成立。
filter:條件必須成立,性能比must高(不會計算得分)。
# 查詢品牌為華為,并且title包含手機的數據
GET goods/_search
{
"query": {
"bool": {
"must": [
{
"term": {
"brand": {
"value": "華為"
}
}
},
{
"match": {
"title": "手機"
}
}
]
}
}
}
運行結果:

4.3.8 Aggregations?
聚合查詢
聚合類型:
- 指標聚合:相當于MySQL的聚合函數。比如max、min、avg、sum等。
- 桶聚合:相當于MySQL的 group by 操作。(不要對text類型的數據進行分組,會失敗)
4.3.8.1 指標聚合
# 指標聚合:找品牌是華為的商品中價格最高的商品價格
GET goods/_search
{
"query": {
"term": {
"brand": {
"value": "華為"
}
}
},
"aggs": {
"max_price": {
"max": {
"field": "price"
}
}
},
"size": 0
}
運行結果:

4.3.8.2 桶聚合
# 桶聚合:根據品牌聚合,看每個品牌的手機商品數據量
GET goods/_search
{
"query": {
"match": {
"title": "手機"
}
},
"aggs": {
"brand_num": {
"terms": {
"field": "brand"
}
}
},
"size": 0
}
運行結果:

4.3.9 highlight(高亮查詢)
# 高亮: 讓 title 中的“華為”和“手機”高亮起來
GET goods/_search
{
"query": {
"match": {
"title": "華為手機"
}
},
"highlight": {
"fields": {
# 高亮字段
"title": {
# 前綴
"pre_tags": "<font class = 'color_class'>",
# 后綴
"post_tags": "</font>"
}
}
}
}
運行結果:

5 總結
這篇文章的宗旨是希望可以幫助剛接觸ES 的人可以快速了解ES,和掌握ES 的一些常用查詢。