一文了解 Elasticsearch 及其與 Python 的對接實現

進擊的Coder發表於2018-10-25

什麼是 Elasticsearch

想查資料就免不了搜尋,搜尋就離不開搜尋引擎,百度、谷歌都是一個非常龐大複雜的搜尋引擎,他們幾乎索引了網際網路上開放的所有網頁和資料。然而對於我們自己的業務資料來說,肯定就沒必要用這麼複雜的技術了,如果我們想實現自己的搜尋引擎,方便儲存和檢索,Elasticsearch 就是不二選擇,它是一個全文搜尋引擎,可以快速地儲存、搜尋和分析海量資料。

為什麼要用 Elasticsearch

Elasticsearch 是一個開源的搜尋引擎,建立在一個全文搜尋引擎庫 Apache Lucene™ 基礎之上。

那 Lucene 又是什麼?Lucene 可能是目前存在的,不論開源還是私有的,擁有最先進,高效能和全功能搜尋引擎功能的庫,但也僅僅只是一個庫。要用上 Lucene,我們需要編寫 Java 並引用 Lucene 包才可以,而且我們需要對資訊檢索有一定程度的理解才能明白 Lucene 是怎麼工作的,反正用起來沒那麼簡單。

那麼為了解決這個問題,Elasticsearch 就誕生了。Elasticsearch 也是使用 Java 編寫的,它的內部使用 Lucene 做索引與搜尋,但是它的目標是使全文檢索變得簡單,相當於 Lucene 的一層封裝,它提供了一套簡單一致的 RESTful API 來幫助我們實現儲存和檢索。

所以 Elasticsearch 僅僅就是一個簡易版的 Lucene 封裝嗎?那就大錯特錯了,Elasticsearch 不僅僅是 Lucene,並且也不僅僅只是一個全文搜尋引擎。 它可以被下面這樣準確的形容:

  • 一個分散式的實時文件儲存,每個欄位可以被索引與搜尋

  • 一個分散式實時分析搜尋引擎

  • 能勝任上百個服務節點的擴充套件,並支援 PB 級別的結構化或者非結構化資料

總之,是一個相當牛逼的搜尋引擎,維基百科、Stack Overflow、GitHub 都紛紛採用它來做搜尋。

Elasticsearch 的安裝

我們可以到 Elasticsearch 的官方網站下載 Elasticsearch:https://www.elastic.co/downloads/elasticsearch,同時官網也附有安裝說明。

首先把安裝包下載下來並解壓,然後執行 bin/elasticsearch(Mac 或 Linux)或者 bin\elasticsearch.bat (Windows) 即可啟動 Elasticsearch 了。

我使用的是 Mac,Mac 下個人推薦使用 Homebrew 安裝:

brew install elasticsearch
複製程式碼

Elasticsearch 預設會在 9200 埠上執行,我們開啟瀏覽器訪問
http://localhost:9200/ 就可以看到類似內容:

{
  "name" : "atntrTf",
  "cluster_name" : "elasticsearch",
  "cluster_uuid" : "e64hkjGtTp6_G2h1Xxdv5g",
  "version" : {
    "number": "6.2.4",
    "build_hash": "ccec39f",
    "build_date": "2018-04-12T20:37:28.497551Z",
    "build_snapshot": false,
    "lucene_version": "7.2.1",
    "minimum_wire_compatibility_version": "5.6.0",
    "minimum_index_compatibility_version": "5.0.0"
  },
  "tagline" : "You Know, for Search"
}
複製程式碼

如果看到這個內容,就說明 Elasticsearch 安裝並啟動成功了,這裡顯示我的 Elasticsearch 版本是 6.2.4 版本,版本很重要,以後安裝一些外掛都要做到版本對應才可以。

接下來我們來了解一下 Elasticsearch 的基本概念以及和 Python 的對接。

Elasticsearch 相關概念

在 Elasticsearch 中有幾個基本的概念,如節點、索引、文件等等,下面來分別說明一下,理解了這些概念對熟悉 Elasticsearch 是非常有幫助的。

Node 和 Cluster

Elasticsearch 本質上是一個分散式資料庫,允許多臺伺服器協同工作,每臺伺服器可以執行多個 Elasticsearch 例項。

單個 Elasticsearch 例項稱為一個節點(Node)。一組節點構成一個叢集(Cluster)。

Index

Elasticsearch 會索引所有欄位,經過處理後寫入一個反向索引(Inverted Index)。查詢資料的時候,直接查詢該索引。

所以,Elasticsearch 資料管理的頂層單位就叫做 Index(索引),其實就相當於 MySQL、MongoDB 等裡面的資料庫的概念。另外值得注意的是,每個 Index (即資料庫)的名字必須是小寫。

Document

Index 裡面單條的記錄稱為 Document(文件)。許多條 Document 構成了一個 Index。

Document 使用 JSON 格式表示,下面是一個例子。

同一個 Index 裡面的 Document,不要求有相同的結構(scheme),但是最好保持相同,這樣有利於提高搜尋效率。

Type

Document 可以分組,比如 weather 這個 Index 裡面,可以按城市分組(北京和上海),也可以按氣候分組(晴天和雨天)。這種分組就叫做 Type,它是虛擬的邏輯分組,用來過濾 Document,類似 MySQL 中的資料表,MongoDB 中的 Collection。

不同的 Type 應該有相似的結構(Schema),舉例來說,id 欄位不能在這個組是字串,在另一個組是數值。這是與關係型資料庫的表的一個區別。性質完全不同的資料(比如 products 和 logs)應該存成兩個 Index,而不是一個 Index 裡面的兩個 Type(雖然可以做到)。

根據規劃,Elastic 6.x 版只允許每個 Index 包含一個 Type,7.x 版將會徹底移除 Type。

Fields

即欄位,每個 Document 都類似一個 JSON 結構,它包含了許多欄位,每個欄位都有其對應的值,多個欄位組成了一個 Document,其實就可以類比 MySQL 資料表中的欄位。

在 Elasticsearch 中,文件歸屬於一種型別(Type),而這些型別存在於索引(Index)中,我們可以畫一些簡單的對比圖來類比傳統關係型資料庫:

Relational DB -> Databases -> Tables -> Rows -> Columns
Elasticsearch -> Indices   -> Types  -> Documents -> Fields
複製程式碼

以上就是 Elasticsearch 裡面的一些基本概念,通過和關係性資料庫的對比更加有助於理解。

Python 對接 Elasticsearch

Elasticsearch 實際上提供了一系列 Restful API 來進行存取和查詢操作,我們可以使用 curl 等命令來進行操作,但畢竟命令列模式沒那麼方便,所以這裡我們就直接介紹利用 Python 來對接 Elasticsearch 的相關方法。

Python 中對接 Elasticsearch 使用的就是一個同名的庫,安裝方式非常簡單:

pip3 install elasticsearch
複製程式碼

官方文件是:https://elasticsearch-py.readthedocs.io/,所有的用法都可以在裡面查到,文章後面的內容也是基於官方文件來的。

建立 Index

我們先來看下怎樣建立一個索引(Index),這裡我們建立一個名為 news 的索引:

from elasticsearch import Elasticsearch

es = Elasticsearch()
result = es.indices.create(index='news', ignore=400)
print(result)
複製程式碼

如果建立成功,會返回如下結果:

{'acknowledged': True, 'shards_acknowledged': True, 'index': 'news'}
複製程式碼

返回結果是 JSON 格式,其中的 acknowledged 欄位表示建立操作執行成功。

但這時如果我們再把程式碼執行一次的話,就會返回如下結果:

{'error': {'root_cause': [{'type': 'resource_already_exists_exception', 'reason': 'index [news/QM6yz2W8QE-bflKhc5oThw] already exists', 'index_uuid': 'QM6yz2W8QE-bflKhc5oThw', 'index': 'news'}], 'type': 'resource_already_exists_exception', 'reason': 'index [news/QM6yz2W8QE-bflKhc5oThw] already exists', 'index_uuid': 'QM6yz2W8QE-bflKhc5oThw', 'index': 'news'}, 'status': 400}
複製程式碼

它提示建立失敗,status 狀態碼是 400,錯誤原因是 Index 已經存在了。

注意這裡我們的程式碼裡面使用了 ignore 引數為 400,這說明如果返回結果是 400 的話,就忽略這個錯誤不會報錯,程式不會執行丟擲異常。

假如我們不加 ignore 這個引數的話:

es = Elasticsearch()
result = es.indices.create(index='news')
print(result)
複製程式碼

再次執行就會報錯了:

raise HTTP_EXCEPTIONS.get(status_code, TransportError)(status_code, error_message, additional_info)
elasticsearch.exceptions.RequestError: TransportError(400, 'resource_already_exists_exception', 'index [news/QM6yz2W8QE-bflKhc5oThw] already exists')
複製程式碼

這樣程式的執行就會出現問題,所以說,我們需要善用 ignore 引數,把一些意外情況排除,這樣可以保證程式的正常執行而不會中斷。

刪除 Index

刪除 Index 也是類似的,程式碼如下:

from elasticsearch import Elasticsearch

es = Elasticsearch()
result = es.indices.delete(index='news', ignore=[400, 404])
print(result)
複製程式碼

這裡也是使用了 ignore 引數,來忽略 Index 不存在而刪除失敗導致程式中斷的問題。

如果刪除成功,會輸出如下結果:

{'acknowledged': True}
複製程式碼

如果 Index 已經被刪除,再執行刪除則會輸出如下結果:

{'error': {'root_cause': [{'type': 'index_not_found_exception', 'reason': 'no such index', 'resource.type': 'index_or_alias', 'resource.id': 'news', 'index_uuid': '_na_', 'index': 'news'}], 'type': 'index_not_found_exception', 'reason': 'no such index', 'resource.type': 'index_or_alias', 'resource.id': 'news', 'index_uuid': '_na_', 'index': 'news'}, 'status': 404}
複製程式碼

這個結果表明當前 Index 不存在,刪除失敗,返回的結果同樣是 JSON,狀態碼是 400,但是由於我們新增了 ignore 引數,忽略了 400 狀態碼,因此程式正常執行輸出 JSON 結果,而不是丟擲異常。

插入資料

Elasticsearch 就像 MongoDB 一樣,在插入資料的時候可以直接插入結構化字典資料,插入資料可以呼叫 create() 方法,例如這裡我們插入一條新聞資料:

from elasticsearch import Elasticsearch

es = Elasticsearch()
es.indices.create(index='news', ignore=400)

data = {'title': '美國留給伊拉克的是個爛攤子嗎', 'url': 'http://view.news.qq.com/zt2011/usa_iraq/index.htm'}
result = es.create(index='news', doc_type='politics', id=1, body=data)
print(result)
複製程式碼

這裡我們首先宣告瞭一條新聞資料,包括標題和連結,然後通過呼叫 create() 方法插入了這條資料,在呼叫 create() 方法時,我們傳入了四個引數,index 引數代表了索引名稱,doc_type 代表了文件型別,body 則代表了文件具體內容,id 則是資料的唯一標識 ID。

執行結果如下:

{'_index': 'news', '_type': 'politics', '_id': '1', '_version': 1, 'result': 'created', '_shards': {'total': 2, 'successful': 1, 'failed': 0}, '_seq_no': 0, '_primary_term': 1}
複製程式碼

結果中 result 欄位為 created,代表該資料插入成功。

另外其實我們也可以使用 index() 方法來插入資料,但與 create() 不同的是,create() 方法需要我們指定 id 欄位來唯一標識該條資料,而 index() 方法則不需要,如果不指定 id,會自動生成一個 id,呼叫 index() 方法的寫法如下:

es.index(index='news', doc_type='politics', body=data)
複製程式碼

create() 方法內部其實也是呼叫了 index() 方法,是對 index() 方法的封裝。

更新資料

更新資料也非常簡單,我們同樣需要指定資料的 id 和內容,呼叫 update() 方法即可,程式碼如下:

from elasticsearch import Elasticsearch

es = Elasticsearch()
data = {
    'title': '美國留給伊拉克的是個爛攤子嗎',
    'url': 'http://view.news.qq.com/zt2011/usa_iraq/index.htm',
    'date': '2011-12-16'
}
result = es.update(index='news', doc_type='politics', body=data, id=1)
print(result)
複製程式碼

這裡我們為資料增加了一個日期欄位,然後呼叫了 update() 方法,結果如下:

{'_index': 'news', '_type': 'politics', '_id': '1', '_version': 2, 'result': 'updated', '_shards': {'total': 2, 'successful': 1, 'failed': 0}, '_seq_no': 1, '_primary_term': 1}
複製程式碼

可以看到返回結果中,result 欄位為 updated,即表示更新成功,另外我們還注意到有一個欄位 _version,這代表更新後的版本號數,2 代表這是第二個版本,因為之前已經插入過一次資料,所以第一次插入的資料是版本 1,可以參見上例的執行結果,這次更新之後版本號就變成了 2,以後每更新一次,版本號都會加 1。

另外更新操作其實利用 index() 方法同樣可以做到,寫法如下:

es.index(index='news', doc_type='politics', body=data, id=1)
複製程式碼

可以看到,index() 方法可以代替我們完成兩個操作,如果資料不存在,那就執行插入操作,如果已經存在,那就執行更新操作,非常方便。

刪除資料

如果想刪除一條資料可以呼叫 delete() 方法,指定需要刪除的資料 id 即可,寫法如下:

from elasticsearch import Elasticsearch

es = Elasticsearch()
result = es.delete(index='news', doc_type='politics', id=1)
print(result)
複製程式碼

執行結果如下:

{'_index': 'news', '_type': 'politics', '_id': '1', '_version': 3, 'result': 'deleted', '_shards': {'total': 2, 'successful': 1, 'failed': 0}, '_seq_no': 2, '_primary_term': 1}
複製程式碼

可以看到執行結果中 result 欄位為 deleted,代表刪除成功,_version 變成了 3,又增加了 1。

查詢資料

上面的幾個操作都是非常簡單的操作,普通的資料庫如 MongoDB 都是可以完成的,看起來並沒有什麼了不起的,Elasticsearch 更特殊的地方在於其異常強大的檢索功能。

對於中文來說,我們需要安裝一個分詞外掛,這裡使用的是 elasticsearch-analysis-ik,GitHub 連結為:https://github.com/medcl/elasticsearch-analysis-ik,這裡我們使用 Elasticsearch 的另一個命令列工具 elasticsearch-plugin 來安裝,這裡安裝的版本是 6.2.4,請確保和 Elasticsearch 的版本對應起來,命令如下:

elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v6.2.4/elasticsearch-analysis-ik-6.2.4.zip
複製程式碼

這裡的版本號請替換成你的 Elasticsearch 的版本號。

安裝之後重新啟動 Elasticsearch 就可以了,它會自動載入安裝好的外掛。

首先我們新建一個索引並指定需要分詞的欄位,程式碼如下:

from elasticsearch import Elasticsearch

es = Elasticsearch()
mapping = {
    'properties': {
        'title': {
            'type': 'text',
            'analyzer': 'ik_max_word',
            'search_analyzer': 'ik_max_word'
        }
    }
}
es.indices.delete(index='news', ignore=[400, 404])
es.indices.create(index='news', ignore=400)
result = es.indices.put_mapping(index='news', doc_type='politics', body=mapping)
print(result)
複製程式碼

這裡我們先將之前的索引刪除了,然後新建了一個索引,然後更新了它的 mapping 資訊,mapping 資訊中指定了分詞的欄位,指定了欄位的型別 type 為 text,分詞器 analyzer 和 搜尋分詞器 search_analyzer 為 ik_max_word,即使用我們剛才安裝的中文分詞外掛。如果不指定的話則使用預設的英文分詞器。

接下來我們插入幾條新的資料:

datas = [
    {
        'title': '美國留給伊拉克的是個爛攤子嗎',
        'url': 'http://view.news.qq.com/zt2011/usa_iraq/index.htm',
        'date': '2011-12-16'
    },
    {
        'title': '公安部:各地校車將享最高路權',
        'url': 'http://www.chinanews.com/gn/2011/12-16/3536077.shtml',
        'date': '2011-12-16'
    },
    {
        'title': '中韓漁警衝突調查:韓警平均每天扣1艘中國漁船',
        'url': 'https://news.qq.com/a/20111216/001044.htm',
        'date': '2011-12-17'
    },
    {
        'title': '中國駐洛杉磯領事館遭亞裔男子槍擊 嫌犯已自首',
        'url': 'http://news.ifeng.com/world/detail_2011_12/16/11372558_0.shtml',
        'date': '2011-12-18'
    }
]

for data in datas:
    es.index(index='news', doc_type='politics', body=data)
複製程式碼

這裡我們指定了四條資料,都帶有 title、url、date 欄位,然後通過 index() 方法將其插入 Elasticsearch 中,索引名稱為 news,型別為 politics。

接下來我們根據關鍵詞查詢一下相關內容:

result = es.search(index='news', doc_type='politics')
print(result)
複製程式碼

可以看到查詢出了所有插入的四條資料:

{
  "took": 0,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": 4,
    "max_score": 1.0,
    "hits": [
      {
        "_index": "news",
        "_type": "politics",
        "_id": "c05G9mQBD9BuE5fdHOUT",
        "_score": 1.0,
        "_source": {
          "title": "美國留給伊拉克的是個爛攤子嗎",
          "url": "http://view.news.qq.com/zt2011/usa_iraq/index.htm",
          "date": "2011-12-16"
        }
      },
      {
        "_index": "news",
        "_type": "politics",
        "_id": "dk5G9mQBD9BuE5fdHOUm",
        "_score": 1.0,
        "_source": {
          "title": "中國駐洛杉磯領事館遭亞裔男子槍擊,嫌犯已自首",
          "url": "http://news.ifeng.com/world/detail_2011_12/16/11372558_0.shtml",
          "date": "2011-12-18"
        }
      },
      {
        "_index": "news",
        "_type": "politics",
        "_id": "dU5G9mQBD9BuE5fdHOUj",
        "_score": 1.0,
        "_source": {
          "title": "中韓漁警衝突調查:韓警平均每天扣1艘中國漁船",
          "url": "https://news.qq.com/a/20111216/001044.htm",
          "date": "2011-12-17"
        }
      },
      {
        "_index": "news",
        "_type": "politics",
        "_id": "dE5G9mQBD9BuE5fdHOUf",
        "_score": 1.0,
        "_source": {
          "title": "公安部:各地校車將享最高路權",
          "url": "http://www.chinanews.com/gn/2011/12-16/3536077.shtml",
          "date": "2011-12-16"
        }
      }
    ]
  }
}
複製程式碼

可以看到返回結果會出現在 hits 欄位裡面,然後其中有 total 欄位標明瞭查詢的結果條目數,還有 max_score 代表了最大匹配分數。

另外我們還可以進行全文檢索,這才是體現 Elasticsearch 搜尋引擎特性的地方:

dsl = {
    'query': {
        'match': {
            'title': '中國 領事館'
        }
    }
}

es = Elasticsearch()
result = es.search(index='news', doc_type='politics', body=dsl)
print(json.dumps(result, indent=2, ensure_ascii=False))
複製程式碼

這裡我們使用 Elasticsearch 支援的 DSL 語句來進行查詢,使用 match 指定全文檢索,檢索的欄位是 title,內容是“中國領事館”,搜尋結果如下:

{
  "took": 1,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": 2,
    "max_score": 2.546152,
    "hits": [
      {
        "_index": "news",
        "_type": "politics",
        "_id": "dk5G9mQBD9BuE5fdHOUm",
        "_score": 2.546152,
        "_source": {
          "title": "中國駐洛杉磯領事館遭亞裔男子槍擊,嫌犯已自首",
          "url": "http://news.ifeng.com/world/detail_2011_12/16/11372558_0.shtml",
          "date": "2011-12-18"
        }
      },
      {
        "_index": "news",
        "_type": "politics",
        "_id": "dU5G9mQBD9BuE5fdHOUj",
        "_score": 0.2876821,
        "_source": {
          "title": "中韓漁警衝突調查:韓警平均每天扣1艘中國漁船",
          "url": "https://news.qq.com/a/20111216/001044.htm",
          "date": "2011-12-17"
        }
      }
    ]
  }
}
複製程式碼

這裡我們看到匹配的結果有兩條,第一條的分數為 2.54,第二條的分數為 0.28,這是因為第一條匹配的資料中含有“中國”和“領事館”兩個詞,第二條匹配的資料中不包含“領事館”,但是包含了“中國”這個詞,所以也被檢索出來了,但是分數比較低。

因此可以看出,檢索時會對對應的欄位全文檢索,結果還會按照檢索關鍵詞的相關性進行排序,這就是一個基本的搜尋引擎雛形。

另外 Elasticsearch 還支援非常多的查詢方式,詳情可以參考官方文件:https://www.elastic.co/guide/en/elasticsearch/reference/6.3/query-dsl.html

以上便是對 Elasticsearch 的基本介紹以及 Python 操作 Elasticsearch 的基本用法,但這僅僅是 Elasticsearch 的基本功能,它還有更多強大的功能等待著我們的探索,後面會繼續更新,敬請期待。

本節程式碼:https://github.com/Germey/ElasticSearch。

資料推薦

另外推薦幾個不錯的學習站點:

  • Elasticsearch 權威指南:https://es.xiaoleilu.com/index.html

  • 全文搜尋引擎 Elasticsearch 入門教程:http://www.ruanyifeng.com/blog/2017/08/elasticsearch.html

  • Elastic 中文社群:https://www.elasticsearch.cn/

參考資料

  • https://es.xiaoleilu.com/index.html

  • https://blog.csdn.net/y472360651/article/details/76468327

  • https://elasticsearch-py.readthedocs.io/en/master/

  • https://es.xiaoleilu.com/010_Intro/10_Installing_ES.html

  • https://github.com/medcl/elasticsearch-analysis-ik


大家好,我是崔慶才,微軟中國大資料工程師,在微軟小冰部門。當然這是我的職業,我平時還會從事網路爬蟲、Web開發、深度學習等方向的研究和開發工作。

我個人比較喜歡總結和分享,這次很榮幸在掘金的平臺把自己的爬蟲相關經驗分享給大家,包括爬取、解析、防反爬、加速等等部分,希望大家聽完之後有所收穫。

本週日(10月21日)我會做客掘金Bilibili直播間為大家做一場《健壯高效的網路爬蟲》的直播【網頁連結】。直播中我也會抽出 5 名小夥伴贈送各贈送一本《Python3網路爬蟲開發實戰》書籍一本,希望大家可以支援。

一文了解 Elasticsearch 及其與 Python 的對接實現


相關文章