ElasticSearch總結

前段時間對分佈式追蹤相關的實現方案進行了一些調研,瞭解到近期對於大數據的日誌檢索、分析從原來基於hadoop的實現逐漸過渡到基於es的方案上來。近期在消息審計追蹤相關的項目上也嘗試的使用了類似的方案。這裏對es的一些瞭解以及常用的一些使用整理於此。

1. 全文索引

全文索引是指計算機搜索程序通過掃描文件中的每個單詞,對每個詞建立一個索引,指明該詞在文章中出現的次數和位置,當用戶查詢時,搜索程序就根據事先建立的索引進行查找,並將查找結果反饋給用戶。這個過程類似於通過字典中的搜索字表查字的過程。

Lucene倒排索引

倒排索引原語實際應用中需要更具屬性的值來查找記錄。這種索引表中的每一項都包括一個屬性值和具有該屬性值的各記錄的地址。由於不是由記錄類確定屬性值,而是由屬性值來確定記錄的位置,因而稱爲倒排索引(inverted index)。帶有倒排索引的文件我們稱爲倒排索引文件,簡稱倒排文件(inverted file)

倒排索引爲什麼叫倒排索引?

  • 渣翻譯的例子之一。英文原名Inverted index,大概因爲 Invert 有顛倒的意思,就被翻譯成了倒排。但是倒排這個名稱很容易讓人理解爲從A-Z顛倒成Z-A。個人認爲翻譯成轉置索引可能比較合適。一個未經處理的數據庫中,一般是以文檔ID作爲索引,以文檔內容作爲記錄。而Inverted index 指的是將單詞或記錄作爲索引,將文檔ID作爲記錄,這樣便可以方便地通過單詞或記錄查找到其所在的文檔。

  • 倒排索引對應的英文術語爲inverted index,有的papers裏也成爲inverted files,說的都是同一種東西。倒排索引是區別於正排索引(forward index)來說的。
    文檔是有許多的單詞組成的,其中每個單詞也可以在同一個文檔中重複出現很多次,當然,同一個單詞也可以出現在不同的文檔中。
    正排索引(forward index):從文檔角度看其中的單詞,表示每個文檔(用文檔ID標識)都含有哪些單詞,以及每個單詞出現了多少次(詞頻)及其出現位置(相對於文檔首部的偏移量)。
    倒排索引(inverted index,或inverted files):從單詞角度看文檔,標識每個單詞分別在那些文檔中出現(文檔ID),以及在各自的文檔中每個單詞分別出現了多少次(詞頻)及其出現位置(相對於該文檔首部的偏移量)。
    簡單記爲:
    正排索引:文檔 —> 單詞
    倒排索引:單詞 —> 文檔

倒排索引中的索引對象是文檔或者文檔集合中的單詞等,用來存儲這些單詞在一個文檔或者一組文檔中的存儲位置,是對文檔或者文檔集合的一種最常見的索引機制。

搜索引擎的關鍵步驟就是建立倒排索引,倒排索引一般表示爲一個關鍵詞,然後是他的頻度(出現次數)、位置(出現在哪一篇文章或者網頁中,及有關的日期,作者信息等),好比一本書的目錄、標籤。讀者想看哪一個主題相關的章節,直接根據目錄即可找到相關的頁面。不必再從書的第一頁到最後一頁,一頁一頁的查找。

示例

假設有兩篇文章1和文章2
文章1的內容爲:Tom lives in Guangzhou, I live in Guangzhou too.
文章2的內容爲:He once lived in Shanghai.

步驟1:取得關鍵詞

由於lucene是基於關鍵詞索引和查詢的,首先我們要取得這兩篇文章的關鍵詞,通常我們需要如下處理措施:

  1. 我們現在有的是文章內容,即一個字符串,我們先要找出字符串中的所有單詞,即分詞。英文單詞由於用空格分隔,比較好處理。中文單詞間是連在一起的需要特殊的分詞處理。
  2. 文章中的”in”, “once” “too”等詞沒有什麼實際意義,中文中的“的”“是”等字通常也無具體含義,這些不代表概念的詞可以過濾掉
  3. 用戶通常希望查“He”時能把含“he”,“HE”的文章也找出來,所以所有單詞需要統一大小寫。
  4. 用戶通常希望查“live”時能把含“lives”,“lived”的文章也找出來,所以需要把“lives”,“lived”還原成“live”
  5. 文章中的標點符號通常不表示某種概念,也可以過濾掉

在lucene中以上措施由Analyzer類完成
經過上面處理後

  • 文章1的所有關鍵詞爲:[tom] [live] [guangzhou] [live] [guangzhou]
  • 文章2的所有關鍵詞爲:[he] [live] [shanghai]
步驟2:建立倒排索引

有了關鍵詞後,我們就可以建立倒排索引了。上面的對應關係是:“文章號”對“文章中所有關鍵詞”。倒排索引把這個關係倒過來,變成:“關鍵詞”對“擁有該關鍵詞的所有文章號”。文章1,2經過倒排後的對應關係見表:

關鍵詞 文章號
guangzhou 1
he 2
i 1
live 1, 2
shanghai 2
tom 1

通常僅知道關鍵詞在哪些文章中出現還不夠,我們還需要知道關鍵詞在文章中出現次數和出現的位置,通常有兩種位置:

  1. 字符位置,即記錄該詞是文章中第幾個字符(優點是關鍵詞亮顯時定位快)
  2. 關鍵詞位置,即記錄該詞是文章中第幾個關鍵詞(優點是節約索引空間、詞組(phase)查詢快),lucene 中記錄的就是這種位置。

加上“出現頻率”和“出現位置”信息後,我們的索引結構變爲:

關鍵詞 文章號[出現頻率] 出現位置
guangzhou 12 3,6
he 21 1
i 11 4
live 12, 21 2, 5, 2
shanghai 21 3
tom 11 1

以live 這行爲例我們說明一下該結構:live在文章1中出現了2次,文章2中出現了一次,它的出現位置爲“2,5,2”這表示什麼呢?我們需要結合文章號和出現頻率來分析,文章1中出現了2次,那麼“2,5”就表示live在文章1中出現的兩個位置,文章2中出現了一次,剩下的“2”就表示live是文章2中第 2個關鍵字。

以上就是lucene索引結構中最核心的部分。我們注意到關鍵字是按字符順序排列的(lucene沒有使用B樹結構),因此lucene可以用二元搜索算法快速定位關鍵詞。

實現

實現時 lucene將上面三列分別作爲詞典文件(Term Dictionary)、頻率文件(frequencies)、位置文件 (positions)保存。其中詞典文件不僅保存有每個關鍵詞,還保留了指向頻率文件和位置文件的指針,通過指針可以找到該關鍵字的頻率信息和位置信息。

Lucene中使用了field的概念,用於表達信息所在位置(如標題中,文章中,url中),在建索引中,該field信息也記錄在詞典文件中,每個關鍵詞都有一個field信息(因爲每個關鍵字一定屬於一個或多個field)。

壓縮算法

爲了減小索引文件的大小,Lucene對索引還使用了壓縮技術。首先,對詞典文件中的關鍵詞進行了壓縮,關鍵詞壓縮爲<前綴長度,後綴>,例如:當前詞爲“阿拉伯語”,上一個詞爲“阿拉伯”,那麼“阿拉伯語”壓縮爲<3,語>。其次大量用到的是對數字的壓縮,數字只保存與上一個值的差值(這樣可以減小數字的長度,進而減少保存該數字需要的字節數)。例如當前文章號是16389(不壓縮要用3個字節保存),上一文章號是16382,壓縮後保存7(只用一個字節)。

應用場景

下面我們可以通過對該索引的查詢來解釋一下爲什麼要建立索引。

假設要查詢單詞 “live”,lucene先對詞典二元查找、找到該詞,通過指向頻率文件的指針讀出所有文章號,然後返回結果。詞典通常非常小,因而,整個過程的時間是毫秒級的。

而用普通的順序匹配算法,不建索引,而是對所有文章的內容進行字符串匹配,這個過程將會相當緩慢,當文章數目很大時,時間往往是無法忍受的。

2. 術語

  • term,索引詞。能夠被索引的精確值,索引詞(term)可以通過term搜索進行準確查詢
  • text,文本。是一段普通的非結構化文字。通常文本會被分析稱一個個的索引詞。
  • analysis,分析。分析是將文本轉換爲索引詞的過程,分析的結果依賴於分詞器。
  • index,索引。是具有相同結構的文檔集合。在單個集羣中,可以定義多個你想要的索引。(索引相當於數據庫中的表
  • type,類型。類型是索引的邏輯分區。(相當於數據庫表中子表定義,一個索引(數據庫中的表)可以有多個子表)。在一般情況下,一種類型被定義爲具有一組公共字段的文檔。例如,讓我們假設你運行一個博客平臺,並把所有的數據存儲在一個索引中。在這個索引中,你可以定義一種類型爲“用戶數據”,一種類型爲“博客數據”,另外一種類型爲“評論數據”
  • document,文檔。是存儲在Elasticsearch中的一個JSON格式的字符串(數據庫表中的一行記錄)。
  • mapping,映射。相當於數據庫中的表結構,每一個索引都有一個映射,它定義了索引中每一個字段類型,以及一個索引範圍內的設置。一個映射可以實現被定義,或者在第一次存儲文檔時候被自動識別
  • field,字段。文檔中包含零個或者多個字段,字段可以是一個簡單的值,也可以是一個數組或者是對象的嵌套結構。字段類似於表中的列
  • source field,來源字段。默認情況下,你的源文檔江北存儲在_source這個字段中,當你查詢的時候也是返回這個字段。這允許你可以從搜索結構中訪問原始的對象,這個對象返回一個精確的JSON字符串,這個對象不顯示索引分析後的任何數據
  • id,主鍵。是文件的唯一標識,如果在存庫的時候沒有提供ID,系統會自動生成一個ID,文檔的“index/type/id”必須是唯一的

3. 操作API

elasticsearch提供了豐富的操作方法(API),使得用戶可以很方便的對elasticsearch進行管理,以及通過elasticsearch查詢,彙總各類統計結果。完整的詳細說明可參見Elasticsearch Reference,這裏我主要總結了幾類基礎的,我比較常用的操作API

  • 索引操作相關
    • 創建索引
    • 獲取索引
    • 刪除索引
    • 重建索引
    • 創建索引模板
    • 查看索引模板
    • 刪除索引模板
  • 查詢操作相關
    • 獲取索引上的文檔總數
    • (不)包含某個字段的文檔
    • AND查詢
    • Time Range查詢
  • 聚合操作相關
    • 按某個字段x聚合,獲取x總數count(x)

因爲elasticsearch提供的都是HTTP RESTful API接口,因此很容易通過http工具對各接口進行驗證,我在使用時,一般使用2類工具進行驗證:

  1. kibana的DevTools
    通過kebana的DevTools,可以很方便的根據需要書寫http msg body,同時,kibana還可以給出關鍵字提示,執行命令,返回結果就顯示在右側的結果欄中

  2. curl命令
    強大的curl命令,可以用於構建各類http請求,並獲得反饋結果。某些msgbody內容較大,較複雜的命令,我會使用curl,指定本地文件的形式來執行。例如,創建post索引,索引的映射類型定義在post.json中:curl -XPOST 'http://192.168.149.150:9200/posts' -d @posts.json

索引操作相關

創建索引

創建索引,相當於創建數據庫中的一張表,在創建索引時,我們往往會給出該索引的映射,方便索引文檔的構建。假設我們需要創建一個索引:myIndex,在這個索引上,我們計劃會創建兩類文檔:doc1, doc2。此時,我們會定義一個映射配置文件my_index_map.json,這個文件定義了doc1,doc2應該怎麼被索引:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
{
    "mappings":{
    
        "doc1" : {
            "properties" : {
                "log_timestamp" : {
                    "type" : "date",
                    "store": true,
                    "format" : "MM/dd/yy HH:mm:ss z"
                },
                "pub_appid" : {
                    "store": true,
                    "type" : "integer"
                },
                "remote_ip" : {
                    "store": true,
                    "type" : "text"
                },
                "real_ip" : {
                    "store": true,
                    "type" : "text"
                },
                "topic" : {
                    "store": true,
                    "type" : "keyword"
                },
                "partition" : {
                    "store": true,
                    "type" : "integer"
                },
                "offset" : {
                    "store": true,
                    "type" : "long"
                },
                "async_flag" : {
                    "store": true,
                    "type" : "boolean"
                }
            }
        },

        "doc2" : {
            "properties" : {
                "log_timestamp" : {
                    "type" : "date",
                    "store": true,
                    "format" : "MM/dd/yy HH:mm:ss z"
                },
                "sub_appid" : {
                    "store": true,
                    "type" : "integer"
                },
                "sub_group" : {
                    "store": true,
                    "type" : "keyword"
                },
                "remote_ip" : {
                    "store": true,
                    "type" : "text"
                },
                "real_ip" : {
                    "store": true,
                    "type" : "text"
                },
                "topic" : {
                    "store": true,
                    "type" : "keyword"
                },
                "partition" : {
                    "store": true,
                    "type" : "integer"
                },
                "offset" : {
                    "store": true,
                    "type" : "long"
                },
                "dack_flag" : {
                    "store": true,
                    "type" : "boolean"
                }
            }
        }        
        
    }
}

 

在這個映射文件中,我們定義doc1, doc2的字段映射關係。doc1中有哪些字段:log_timestamp, pub_appid, …,同時,給這些字段定義了類型:date, integer, keyword…等,基於這個映射文件,我們可以創建我們需要的索引:myIndex:
curl -XPOST 'http://192.168.149.150:9200/myIndex' -d @my_index_map.json

獲取索引

通過GET命令,則可以方便的獲取到你的目標索引的定義信息:
curl -XGET 'http://192.168.149.150:9200/myIndex?pretty'

刪除索引

通過DELETE命令,則是刪除目標索引:
curl -XDELETE "http://192.168.149.150:9200/myIndex/"

重建索引

有些時候,我們會希望調整索引的映射配置,在調整映射配置之後,有時候是需要對索引進行重建的。重建索引指令主要用於將一個索引中的數據“搬到”另外一個索引中去。詳細信息可參考:Reindex API的說明

1
2
3
4
5
6
7
8
9
10
curl -XPOST '192.168.149.150:9200/_reindex?pretty' -d'
{
  "source": {
    "index": "myIndex"
  },
  "dest": {
    "index": "myIndex_new"
  }
}
'

當索引中的數據量很大時,重建索引需要一定的時間才能完成,通過下述命令可以查看索引重建的狀態

curl -XGET '192.168.149.150:9200/_tasks?detailed=true&actions=*reindex&pretty'

創建索引模板

隨着時間的推移,如果所有數據都放在同一個索引中,會導致索引變的越來越大,無法控制。爲方便對索引內容進行管理,我們往往會以天爲單位,每天建一個索引,這樣,則可以滾動創建一批索引,例如:myIndex_2017.06.21, myIndex_2017.06.22, myIndex_2017.06.23, myIndex_2017.06.24, …,當整個myIndex_*索引簇過大時,我們可以清理掉若干天的索引,保留最近7天的索引。如何滾動保留最近7天的數據,可參考elasticsearch工具curator的說明:Curator Reference

爲達到按天建立索引的目標,我們需要創建一個索引模板,告知elasticsearch,以某種規律命名的索引,都基於這個索引模板來設置映射。

與創建索引類似,在創建索引模板時,我們也會給出一個索引模板的定義文件:my_index_map_template.json:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
{
    "template": "my_index_*",
    "mappings":{
    
        "doc1" : {
            "properties" : {
                "log_timestamp" : {
                    "type" : "date",
                    "store": true,
                    "format" : "MM/dd/yy HH:mm:ss z"
                },
                "pub_appid" : {
                    "store": true,
                    "type" : "integer"
                },
                "remote_ip" : {
                    "store": true,
                    "type" : "text"
                },
                "real_ip" : {
                    "store": true,
                    "type" : "text"
                },
                "topic" : {
                    "store": true,
                    "type" : "keyword"
                },
                "partition" : {
                    "store": true,
                    "type" : "integer"
                },
                "offset" : {
                    "store": true,
                    "type" : "long"
                },
                "async_flag" : {
                    "store": true,
                    "type" : "boolean"
                }
            }
        },

        "doc2" : {
            "properties" : {
                "log_timestamp" : {
                    "type" : "date",
                    "store": true,
                    "format" : "MM/dd/yy HH:mm:ss z"
                },
                "sub_appid" : {
                    "store": true,
                    "type" : "integer"
                },
                "sub_group" : {
                    "store": true,
                    "type" : "keyword"
                },
                "remote_ip" : {
                    "store": true,
                    "type" : "text"
                },
                "real_ip" : {
                    "store": true,
                    "type" : "text"
                },
                "topic" : {
                    "store": true,
                    "type" : "keyword"
                },
                "partition" : {
                    "store": true,
                    "type" : "integer"
                },
                "offset" : {
                    "store": true,
                    "type" : "long"
                },
                "dack_flag" : {
                    "store": true,
                    "type" : "boolean"
                }
            }
        }        
        
    }
}

這個索引模板中的mapping部分,和表中的索引映射文件一致,需要注意的是,索引模板多了一個字段:"template": "my_index_*",這個字段就是告訴elasticsearch,如果有人想要建立以my_index_開頭的索引,則根據這個索引模板定義個mapping進行創建。

創建索引模板,我們使用命令:
curl -XPUT 'http://192.168.149.150:9200/_template/my_index_per_day' -d @my_index_map_template.json

查看索引模板

查看索引模板的定義,則使用:
curl -XGET 'http://192.168.149.150:9200/_template/my_index_per_day?pretty'

刪除索引模板

需要刪除索引模板時,使用:
curl -XDELETE '192.168.149.150:9200/_template/my_index_per_day?pretty'

查詢操作相關

獲取索引上的文檔總數

當需要獲取一個索引上有多少數據時,需要使用match_all匹配,es會返回totalCount

1
2
3
4
5
6
7
8
GET my_index_*/_search
{
  "from":0,
  "size":0,
  "query": {
    "match_all": {}
  }
}

設置size:0,告訴es不用返回任何一條結果。我們只需要知道totalCount信息

(不)包含某個字段的文檔

通過exists查詢,可以獲取那些包含某個字段值的記錄

1
2
3
4
5
6
7
curl -XGET 'localhost:9200/_search?pretty' -H 'Content-Type: application/json' -d'
{
    "query": {
        "exists" : { "field" : "user" }
    }
}
'

反過來,exists查詢結合bool複合查詢(must_not事件類型)可以搜索不包含某字段的文檔記錄

1
2
3
4
5
6
7
8
9
10
11
12
13
curl -XGET 'localhost:9200/_search?pretty' -H 'Content-Type: application/json' -d'
{
    "query": {
        "bool": {
            "must_not": {
                "exists": {
                    "field": "user"
                }
            }
        }
    }
}
'

AND查詢

and查詢則是找出同時滿足某幾個條件的數據,同樣可以基於bool複合查詢來完成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
GET my_index_2017.05.05/doc1/_search
{
  "from": 0,
  "size": 5,
  "sort": [
    {
      "offset": "asc"
    }
  ],
  "query": {
    "bool" : {
      "must" : [
        {
          "term" : {"topic": "55.userpuid.v1"}
        }, 
        { 
          "term" :{"partition": 0}
        }
      ]
    }
  }
}

 

注意到,上面我們還是用了sort字段,這裏規定了es按哪個字段進行排序並返回結果。

Time Range查詢

對date類型的字段進行time range查詢,可以返回某一時間區間內的結果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
GET my_index_2017.05.08/doc1/_search
{
  "from": 0,
  "size": 10,
  "sort" : [
        { "log_timestamp" : "asc" }
    ],
   "_source": ["topic","partition","offset", "log_timestamp"],
  "query": {
    "bool" : {
      "must" : [
        {
          "term" : {"topic": "51.cda-store.v1"}
        },
        {
          "range": {
            "log_timestamp": {
              "gte": "05/08/17 10:45:00 CST",
              "lte": "05/08/17 10:55:00 CST",
              "format": "MM/dd/yy HH:mm:ss z"
            }
          }
        }
      ]
    }
  }
}

"gte": "05/08/17 10:45:00 CST",gte表示要求時間點大於或等於:”05/08/17 10:45:00 CST”, 同理,"lte": "05/08/17 10:55:00 CST"表示要小於等於”05/08/17 10:55:00 CST”

聚合操作相關

按某個字段x聚合,獲取x總數count(x)

聚合操作會按照指定的聚合指標對數據進行聚合統計,詳細的聚合接口說明可參考Aggregations章節的說明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
GET my_index_*/doc2/_search
{
  "from": 0,
  "size": 0,
  "query": {
    "bool": {
      "must": [
        {
          "term": {
            "topic": "201.OrderStatusDelayMsg.v1"
          }
        },
        {
          "term": {
            "partition": 0
          }
        }
      ]
    }
  },
  "aggs": {
    "cg": {
      "terms": {
        "field": "sub_group",
        "size": "10000"
      }
    }
  }
}

上面定義了一個名爲cg的聚合,該聚合的類型是terms(桶聚合的一種),這個聚合是按”sub_group”這個指標進行的,實際上,上面查詢的是topic=201.OrderStatusDelayMsg.v1&partition=0的記錄,按sub_group進行統計,看每個sub_group分別有多少數據量。這裏需要注意到cg聚合中還有一個字段”size”,之所以將其設置成10000,是希望es在每個shard上都將所有數據進行聚合,獲取到精確數量,而不是僅僅將topN的數據做聚合,size較少時,彙總的數據不精確。具體可參見對size字段的描述:Document counts are approximate

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章