Python_頭條專案Elasticsearch(10)

2020-08-08 20:28:45

簡介與原理

You know, for search!

文件 https://www.elastic.co/guide/cn/elasticsearch/guide/current/index.html

1 簡介

Elasticsearch是一個基於Lucene庫的搜尋引擎。

它提供了一個分佈式、支援多使用者的全文搜尋引擎,具有HTTP Web介面和無模式JSON文件。所有其他語言可以使用 RESTful API 通過埠 9200 和 Elasticsearch 進行通訊

Elasticsearch是用Java開發的,並在Apache許可證下作爲開源軟體發佈。官方用戶端在Java、.NET(C#)、PHP、Python、Apache Groovy、Ruby和許多其他語言中都是可用的。

根據DB-Engines的排名顯示,Elasticsearch是最受歡迎的企業搜尋引擎,其次是Apache Solr,也是基於Lucene。

Elasticsearch可以用於搜尋各種文件。它提供可延伸的搜尋,具有接近實時的搜尋,並支援多租戶。

Elasticsearch是分佈式的,這意味着索引可以被分成分片,每個分片可以有0個或多個副本。每個節點託管一個或多個分片,並充當協調器將操作委託給正確的分片。再平衡和路由是自動完成的。相關數據通常儲存在同一個索引中,該索引由一個或多個主分片和零個或多個複製分片組成。一旦建立了索引,就不能更改主分片的數量。

Elasticsearch 是一個實時的分佈式搜尋分析引擎,它被用作全文檢索、結構化搜尋、分析以及這三個功能的組合

  • Wikipedia 使用 Elasticsearch 提供帶有高亮片段的全文搜尋,還有 search-as-you-type 和 did-you-mean 的建議。
  • 衛報 使用 Elasticsearch 將網路社交數據結合到訪客日誌中,實時的給它的編輯們提供公衆對於新文章的反饋。
  • Stack Overflow 將地理位置查詢融入全文檢索中去,並且使用 more-like-this 介面去查詢相關的問題與答案。
  • GitHub 使用 Elasticsearch 對1300億行程式碼進行查詢。

    Lucene 僅僅只是一個庫,然而,Elasticsearch 不僅僅是 Lucene,並且也不僅僅只是一個全文搜尋引擎。 它可以被下面 下麪這樣準確的形容:

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

  • 一個分佈式實時分析搜尋引擎
  • 能勝任上百個服務節點的擴充套件,並支援 PB 級別的結構化或者非結構化數據

屬於面向文件的數據庫

Elasticsearch 是 面向文件 的,意味着它儲存整個物件或 文件。Elasticsearch 不僅儲存文件,而且 索引每個文件的內容使之可以被檢索。在 Elasticsearch 中,你 對文件進行索引、檢索、排序和過濾--而不是對行列數據。

Elasticsearch 有2.x、5.x、6.x 三個大版本,我們在黑馬頭條中使用5.6版本。

2 搜尋的原理——倒排索引(反向索引)、分析、相關性排序

倒排索引

倒排索引(英語:Inverted index),也常被稱爲反向索引、置入檔案或反向檔案,是一種索引方法,被用來儲存在全文搜尋下某個單詞在一個文件或者一組文件中的儲存位置的對映。它是文件檢索系統中最常用的數據結構。

假設我們有兩個文件,每個文件的 content 域包含如下內容:

  1. The quick brown fox jumped over the , lazy+ dog
  2. Quick brown foxes leap over lazy dogs in summer

正向索引: 儲存每個文件的單詞的列表

Doc Quick The brown dog dogs fox foxes in jumped lazy leap over quick summer the
Doc1   X X X   X     X X   X X   X
Doc2 X   X   X   X X   X X X   X

反向索引:

Term      Doc_1  Doc_2
-------------------------
Quick   |       |  X
The     |   X   |
brown   |   X   |  X
dog     |   X   |
dogs    |       |  X
fox     |   X   |
foxes   |       |  X
in      |       |  X
jumped  |   X   |
lazy    |   X   |  X
leap    |       |  X
over    |   X   |  X
quick   |   X   |
summer  |       |  X
the     |   X   |
------------------------

如果我們想搜尋 quick brown ,我們只需要查詢包含每個詞條的文件:

Term      Doc_1  Doc_2
-------------------------
brown   |   X   |  X
quick   |   X   |
------------------------
Total   |   2   |  1

兩個文件都匹配,但是第一個文件比第二個匹配度更高。如果我們使用僅計算匹配詞條數量的簡單 相似性演算法 ,那麼,我們可以說,對於我們查詢的相關性來講,第一個文件比第二個文件更佳。

分析

上面不太合理的地方:

  • Quick 和 quick 以獨立的詞條(token)出現,然而使用者可能認爲它們是相同的詞。
  • fox 和 foxes 非常相似, 就像 dog 和 dogs ;他們有相同的詞根。
  • jumped 和 leap, 儘管沒有相同的詞根,但他們的意思很相近。他們是同義詞。

進行標準化

  • Quick 可以小寫化爲 quick 。
  • foxes 可以 詞幹提取 --變爲詞根的格式-- 爲 fox 。類似的, dogs 可以爲提取爲 dog 。
  • jumped 和 leap 是同義詞,可以索引爲相同的單詞 jump 。

標準化的反向索引:

Term      Doc_1  Doc_2
-------------------------
brown   |   X   |  X
dog     |   X   |  X
fox     |   X   |  X
in      |       |  X
jump    |   X   |  X
lazy    |   X   |  X
over    |   X   |  X
quick   |   X   |  X
summer  |       |  X
the     |   X   |  X
------------------------

對於查詢的字串必須與詞條(token)進行相同的標準化處理,才能 纔能保證搜尋的正確性。

分詞和標準化的過程稱爲 分析 (analysis) :

  • 首先,將一塊文字分成適合於倒排索引的獨立的 詞條 , -> 分詞
  • 之後,將這些詞條統一化爲標準格式以提高它們的「可搜尋性」 -> 標準化

分析工作是由分析器 完成的: analyzer

  • 字元過濾器

    首先,字串按順序通過每個 字元過濾器 。他們的任務是在分詞前整理字串。一個字元過濾器可以用來去掉HTML,或者將 & 轉化成 and

  • 分詞器

    其次,字串被 分詞器 分爲單個的詞條。一個簡單的分詞器遇到空格和標點的時候,可能會將文字拆分成詞條。

  • Token 過濾器 (詞條過濾器)

    最後,詞條按順序通過每個 token 過濾器 。這個過程可能會改變詞條(例如,小寫化 Quick ),刪除詞條(例如, 像 a, and, the 等無用詞),或者增加詞條(例如,像 jump 和 leap 這種同義詞)。

相關性排序

預設情況下,搜尋結果是按照 相關性 進行倒序排序的——最相關的文件排在最前。

相關性可以用相關性評分表示,評分越高,相關性越高。

評分的計算方式取決於查詢型別 不同的查詢語句用於不同的目的: fuzzy 查詢(模糊查詢)會計算與關鍵詞的拼寫相似程度,terms 查詢(詞組查詢)會計算 找到的內容與關鍵詞組成部分匹配的百分比,但是通常我們說的 相關性 是我們用來計算全文字欄位的值相對於全文字檢索詞相似程度的演算法。

Elasticsearch 的相似度演算法 被定義爲檢索詞頻率/反向文件頻率, TF/IDF ,包括以下內容:

  • 檢索詞頻率

    檢索詞在該欄位出現的頻率?出現頻率越高,相關性也越高。 欄位中出現過 5 次要比只出現過 1 次的相關性高。

  • 反向文件頻率

    每個檢索詞在索引中出現的頻率?頻率越高,相關性越低。檢索詞出現在多數文件中會比出現在少數文件中的權重更低。

  • 欄位長度準則

    欄位的長度是多少?長度越長,相關性越低。 檢索詞出現在一個短的 title 要比同樣的詞出現在一個長的 content 欄位權重更大。

===================================

概念與叢集

概念

儲存數據到 Elasticsearch 的行爲叫做 索引 (indexing)

關於數據的概念

Relational DB -> Databases 數據庫 -> Tables 表 -> Rows 行 -> Columns 列
Elasticsearch -> Indices 索引庫 -> Types 型別 -> Documents 文件 -> Fields 欄位/屬性

一個 Elasticsearch 叢集可以 包含多個 索引 (indices 數據庫),相應的每個索引可以包含多個 型別(type 表) 。 這些不同的型別儲存着多個 文件(document 數據行) ,每個文件又有 多個 屬性 (field 列)。

Elasticsearch 叢集(cluster)

Elasticsearch 儘可能地遮蔽了分佈式系統的複雜性。這裏列舉了一些在後台自動執行的操作:

  • 分配文件到不同的容器 或 分片 中,文件可以儲存在一個或多個節點中
  • 按叢集節點來均衡分配這些分片,從而對索引和搜尋過程進行負載均衡
  • 複製每個分片以支援數據冗餘,從而防止硬體故障導致的數據丟失
  • 將叢集中任一節點的請求路由到存有相關數據的節點
  • 叢集擴容時無縫整合新節點,重新分配分片以便從離羣節點恢復

節點(node)

一個執行中的 Elasticsearch 範例稱爲一個 節點,而叢集是由一個或者多個擁有相同 cluster.name 設定的節點組成, 它們共同承擔數據和負載的壓力。當有節點加入叢集中或者從叢集中移除節點時,叢集將會重新平均分佈所有的數據。

當一個節點被選舉成爲  節點(master)時, 它將負責管理叢集範圍內的所有變更,例如增加、刪除索引,或者增加、刪除節點等。 而主節點並不需要涉及到文件級別的變更和搜尋等操作,所以當叢集只擁有一個主節點的情況下,即使流量的增加它也不會成爲瓶頸。 任何節點都可以成爲主節點。我們的範例叢集就只有一個節點,所以它同時也成爲了主節點。

作爲使用者,我們可以將請求發送到 叢集中的任何節點 ,包括主節點。 每個節點都知道任意文件所處的位置,並且能夠將我們的請求直接轉發到儲存我們所需文件的節點。 無論我們將請求發送到哪個節點,它都能負責從各個包含我們所需文件的節點收集回數據,並將最終結果返回給用戶端。 Elasticsearch 對這一切的管理都是透明的。

分片(shard)

一個 分片 是一個底層的 工作單元 ,它僅儲存了 全部數據中的一部分。

索引實際上是指向一個或者多個物理 分片 的 邏輯名稱空間 。

文件被儲存和索引到分片內,但是應用程式是直接與索引而不是與分片進行互動。

Elasticsearch 是利用分片將數據分發到叢集內各處的。分片是數據的容器,文件儲存在分片內,分片又被分配到叢集內的各個節點裏。 當你的叢集規模擴大或者縮小時, Elasticsearch 會自動的在各節點中遷移分片,使得數據仍然均勻分佈在叢集裡。

主分片(primary shard)

索引內任意一個文件都歸屬於一個主分片,所以主分片的數目決定着索引能夠儲存的最大數據量。

複製分片(副分片 replica shard)

一個副本分片只是一個主分片的拷貝。 副本分片作爲硬體故障時保護數據不丟失的冗餘備份,併爲搜尋和返迴文件等讀操作提供服務。

在索引建立的時候就已經確定了主分片數,但是副本分片數可以隨時修改.

初始設定索引的分片方法

PUT /blogs
{
   "settings" : {
      "number_of_shards" : 3,
      "number_of_replicas" : 1
   }
}
  • number_of_shards

    每個索引的主分片數,預設值是 5 。這個設定在索引建立後不能修改。

  • number_of_replicas

    每個主分片的副本數,預設值是 1 。對於活動的索引庫,這個設定可以隨時修改。

2 個節點

3 個節點

分片是一個功能完整的搜尋引擎,它擁有使用一個節點上的所有資源的能力。 我們這個擁有6個分片(3個主分片和3個副本分片)的索引可以最大擴容到6個節點,每個節點上存在一個分片,並且每個分片擁有所在節點的全部資源。

修改複製分片數目的方法

PUT /blogs/_settings
{
   "number_of_replicas" : 2
}

擁有越多的副本分片時,也將擁有越高的吞吐量。

故障轉移 failover

  • 選舉新的主節點
  • 提升複製分片爲主分片

檢視叢集健康狀態

GET /_cluster/health

{
   "cluster_name":          "elasticsearch",
   "status":                "green", 
   "timed_out":             false,
   "number_of_nodes":       1,
   "number_of_data_nodes":  1,
   "active_primary_shards": 0,
   "active_shards":         0,
   "relocating_shards":     0,
   "initializing_shards":   0,
   "unassigned_shards":     0
}

status 欄位指示着當前叢集在總體上是否工作正常。它的三種顏色含義如下:

  • green

    所有的主分片和副本分片都正常執行。

  • yellow

    所有的主分片都正常執行,但不是所有的副本分片都正常執行。

  • red

    有主分片沒能正常執行。

=========================

IK中文分析器

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

將elasticsearch-analysis-ik-5.6.16.zip 複製到虛擬機器中

scp elasticsearch-analysis-ik-5.6.16.zip [email protected]:~/

安裝

sudo /usr/share/elasticsearch/bin/elasticsearch-plugin install file:///home/python/elasticsearch-analysis-ik-5.6.16.zip
-> Downloading file:///home/python/elasticsearch-analysis-ik-5.6.16.zip
[=================================================] 100%
-> Installed analysis-ik

重新啓動

sudo systemctl restart elasticsearch

測試分析器

curl -X GET 127.0.0.1:9200/_analyze?pretty -d '
{
  "analyzer": "standard",
  "text": "我是&中國人"
}'

curl -X GET 127.0.0.1:9200/_analyze?pretty -d '
{
  "analyzer": "ik_max_word",
  "text": "我是&中國人"
}'

============================

索引與型別

索引

檢視索引

curl 127.0.0.1:9200/_cat/indices

請求curl 127.0.0.1:9200/_cat可獲取用於查詢的名稱

建立索引

索引可以在新增文件數據時,通過動態對映的方式自動生成索引與型別。

索引也可以手動建立,通過手動建立,可以控制主分片數目、分析器和型別對映。

PUT /my_index
{
    "settings": { ... any settings ... },
    "mappings": {
        "type_one": { ... any mappings ... },
        "type_two": { ... any mappings ... },
        ...
    }
}

注: 在Elasticsearch 5.x版本中,設定分片與設定索引的型別欄位需要分兩次設定完成。

刪除索引

用以下的請求來 刪除索引:

DELETE /my_index

你也可以這樣刪除多個索引:

DELETE /index_one,index_two
DELETE /index_*

你甚至可以這樣刪除 全部 索引:

DELETE /_all
DELETE /*

建立頭條專案文章索引庫

// 文章索引
curl -X PUT 127.0.0.1:9200/articles -H 'Content-Type: application/json' -d'
{
   "settings" : {
        "index": {
            "number_of_shards" : 3,
            "number_of_replicas" : 1
        }
   }
}
'

型別和對映

型別 在 Elasticsearch 中表示一類相似的文件,型別由 名稱 和 對映 ( mapping)組成。

對映, mapping, 就像數據庫中的 schema ,描述了文件可能具有的欄位或 屬性 、 每個欄位的數據型別—比如 stringinteger 或 date

爲了能夠將時間欄位視爲時間,數位欄位視爲數位,字串欄位視爲全文或精確值字串, Elasticsearch 需要知道每個欄位中數據的型別。

簡單欄位型別:

  • 字串: text (在elaticsearch 2.x版本中,爲string型別)

  • 整數 : byteshortintegerlong
  • 浮點數: floatdouble
  • 布爾型: boolean
  • 日期: date

頭條專案文章型別對映

curl -X PUT 127.0.0.1:9200/articles/_mapping/article -H 'Content-Type: application/json' -d'
{
     "_all": {
          "analyzer": "ik_max_word"
      },
      "properties": {
          "article_id": {
              "type": "long",
              "include_in_all": "false"
          },
          "user_id": {
              "type": "long",
              "include_in_all": "false"
          },
          "title": {
              "type": "text",
              "analyzer": "ik_max_word",
              "include_in_all": "true",
              "boost": 2
          },
          "content": {
              "type": "text",
              "analyzer": "ik_max_word",
              "include_in_all": "true"
          },
          "status": {
              "type": "integer",
              "include_in_all": "false"
          },
          "create_time": {
              "type": "date",
              "include_in_all": "false"
          }
      }
}
'
  • _all欄位是把所有其它欄位中的值,以空格爲分隔符組成一個大字串,然後被分析和索引,但是不儲存,也就是說它能被查詢,但不能被取回顯示。_all允許在不知道要查詢的內容是屬於哪個具體欄位的情況下進行搜尋。

  • analyzer指明使用的分析器

    索引時的順序如下:

    • 欄位對映裡定義的 analyzer
    • 否則索引設定中名爲 default 的分析器,預設爲standard 標準分析器

      在搜尋時,順序有些許不同:

    • 查詢自己定義的 analyzer

    • 否則欄位對映裡定義的 analyzer
    • 否則索引設定中名爲 default 的分析器,預設爲standard 標準分析器
  • include_in_all 參數用於控制 _all 查詢時需要包含的欄位。預設爲 true。

  • boost可以提升查詢時計算相關性分數的權重。例如title欄位將是其他欄位權重的兩倍。

檢視對映

curl 127.0.0.1:9200/articles/_mapping/article?pretty

對映修改

一個型別對映建立好後,可以爲型別增加新的欄位對映

curl -X PUT 127.0.0.1:9200/articles/_mapping/article -H 'Content-Type:application/json' -d '
{
  "properties": {
    "new_tag": {
      "type": "text"
    }
  }
}
'

但是不能修改已有欄位的型別對映,原因在於elasticsearch已按照原有欄位對映生成了反向索引數據,型別對映改變意味着需要重新構建反向索引數據,所以並不能再原有基礎上修改,只能新建索引庫,然後建立型別對映後重新構建反向索引數據。

例如,將status欄位型別由integer改爲byte會報錯

curl -X PUT 127.0.0.1:9200/articles/_mapping/article -H 'Content-Type:application/json' -d '
{
  "properties": {
    "status": {
      "type": "byte"
    }
  }
}
'

需要從新建立索引

curl -X PUT 127.0.0.1:9200/articles_v2 -H 'Content-Type: application/json' -d'
{
   "settings" : {
      "index": {
          "number_of_shards" : 3,
          "number_of_replicas" : 1
       }
   }
}
'

curl -X PUT 127.0.0.1:9200/articles_v2/_mapping/article -H 'Content-Type: application/json' -d'
{
     "_all": {
          "analyzer": "ik_max_word"
      },
      "properties": {
          "article_id": {
              "type": "long",
              "include_in_all": "false"
          },
          "user_id": {
               "type": "long",
              "include_in_all": "false"
          },
          "title": {
              "type": "text",
              "analyzer": "ik_max_word",
              "include_in_all": "true",
              "boost": 2
          },
          "content": {
              "type": "text",
              "analyzer": "ik_max_word",
              "include_in_all": "true"
          },
          "status": {
              "type": "byte",
              "include_in_all": "false"
          },
          "create_time": {
              "type": "date",
              "include_in_all": "false"
          }
      }
}
'

重新索引數據

curl -X POST 127.0.0.1:9200/_reindex -H 'Content-Type:application/json' -d '
{
  "source": {
    "index": "articles"
  },
  "dest": {
    "index": "articles_v2"
  }
}
'

爲索引起別名

爲索引起別名,讓新建的索引具有原索引的名字,可以讓應用程式零停機。

curl -X DELETE 127.0.0.1:9200/articles
curl -X PUT 127.0.0.1:9200/articles_v2/_alias/articles

查詢索引別名

# 檢視別名指向哪個索引
curl 127.0.0.1:9200/*/_alias/articles

# 檢視哪些別名指向這個索引
curl 127.0.0.1:9200/articles_v2/_alias/*

======================================

文件

一個文件的範例

{
    "name":         "John Smith",
    "age":          42,
    "confirmed":    true,
    "join_date":    "2014-06-01",
    "home": {
        "lat":      51.5,
        "lon":      0.1
    },
    "accounts": [
        {
            "type": "facebook",
            "id":   "johnsmith"
        },
        {
            "type": "twitter",
            "id":   "johnsmith"
        }
    ]
}

一個文件不僅僅包含它的數據 ,也包含 元數據(metadata) —— 有關文件的資訊。 三個必須的元數據元素如下:

  • _index

    文件在哪存放

  • _type

    文件表示的物件類別

  • _id

    文件唯一標識

索引文件(儲存文件數據)

使用自定義的文件id

PUT /{index}/{type}/{id}
{
  "field": "value",
  ...
}
curl -X PUT 127.0.0.1:9200/articles/article/150000 -H 'Content-Type:application/json' -d '
{
  "article_id": 150000,
  "user_id": 1,
  "title": "python是世界上最好的語言",
  "content": "確實如此",
  "status": 2,
  "create_time": "2019-04-03"
}'

自動生成文件id

PUT /{index}/{type}
{
  "field": "value",
  ...
}

獲取指定文件

curl 127.0.0.1:9200/articles/article/150000?pretty

# 獲取一部分
curl 127.0.0.1:9200/articles/article/150000?_source=title,content\&pretty

注意:_version 每次修改文件數據,版本都會增加,可以當作樂觀鎖的依賴(判斷標準)使用

判斷文件是否存在

curl -i -X HEAD 127.0.0.1:9200/articles/article/150000
  • 存在 200狀態碼
  • 不存在 404狀態碼

更新文件

在 Elasticsearch 中文件是 不可改變 的,不能修改它們。 相反,如果想要更新現有的文件,需要 重建索引或者進行替換。我們可以使用相同的 index API 進行實現。

例如修改title欄位的內容,不可進行以下操作(僅傳遞title欄位內容)

curl -X PUT 127.0.0.1:9200/articles/article/150000 -H 'Content-Type:application/json' -d '
{
  "title": "python必須是世界上最好的語言"
}'

而是要索引完整文件內容

curl -X PUT 127.0.0.1:9200/articles/article/150000 -H 'Content-Type:application/json' -d '
{
  "article_id": 150000,
  "user_id": 1,
  "title": "python必須是世界上最好的語言",
  "content": "確實如此",
  "status": 2,
  "create_time": "2019-04-03"
}'

注意返回值_version的變化

刪除文件

curl -X DELETE 127.0.0.1:9200/articles/article/150000

取回多個文件

curl -X GET 127.0.0.1:9200/_mget -d '
{
  "docs": [
    {
      "_index": "articles",
      "_type": "article",
      "_id": 150000
    },
    {
      "_index": "articles",
      "_type": "article",
      "_id": 150001
    }
  ]
}'

==============================

Logstash匯入數據

使用logstash 匯入工具從mysql中匯入數據

Logstach安裝

sudo rpm --import https://artifacts.elastic.co/GPG-KEY-elasticsearch

在 /etc/yum.repos.d/ 中建立logstash.repo檔案

[logstash-6.x]
name=Elastic repository for 6.x packages
baseurl=https://artifacts.elastic.co/packages/6.x/yum
gpgcheck=1
gpgkey=https://artifacts.elastic.co/GPG-KEY-elasticsearch
enabled=1
autorefresh=1
type=rpm-md

執行

sudo yum install logstash
cd /usr/share/logstash/bin/
sudo ./logstash-plugin install logstash-input-jdbc
sudo ./logstash-plugin install logstash-output-elasticsearch
scp mysql-connector-java-8.0.13.tar.gz [email protected]:~/
tar -zxvf mysql-connector-java-8.0.13.tar.gz

從MySQL匯入數據到Elasticsearch

建立組態檔logstash_mysql.conf

input{
     jdbc {
         jdbc_driver_library => "/home/python/mysql-connector-java-8.0.13/mysql-connector-java-8.0.13.jar"
         jdbc_driver_class => "com.mysql.jdbc.Driver"
         jdbc_connection_string => "jdbc:mysql://127.0.0.1:3306/toutiao?tinyInt1isBit=false"
         jdbc_user => "root"
         jdbc_password => "mysql"
         jdbc_paging_enabled => "true"
         jdbc_page_size => "1000"
         jdbc_default_timezone =>"Asia/Shanghai"
         statement => "select a.article_id as article_id,a.user_id as user_id, a.title as title, a.status as status, a.create_time as create_time,  b.content as content from news_article_basic as a inner join news_article_content as b on a.article_id=b.article_id"
         use_column_value => "true"
         tracking_column => "article_id"
         clean_run => true
     }
}
output{
      elasticsearch {
         hosts => "127.0.0.1:9200"
         index => "articles"
         document_id => "%{article_id}"
         document_type => "article"
      }
      stdout {
         codec => json_lines
     }
}
sudo /usr/share/logstash/bin/logstash -f ./logstash_mysql.conf

查詢

1 基本查詢

  • 根據文件ID

      curl -X GET 127.0.0.1:9200/articles/article/1
      curl -X GET 127.0.0.1:9200/articles/article/1?_source=title,user_id
      curl -X GET 127.0.0.1:9200/articles/article/1?_source=false
    
  • 查詢所有

      curl -X GET 127.0.0.1:9200/articles/article/_search?_source=title,user_id
    
  • 分頁

    • from 起始
    • size 每頁數量

      curl -X GET 127.0.0.1:9200/articles/article/_search?_source=title,user_id\&size=3
      
      curl -X GET 127.0.0.1:9200/articles/article/_search?_source=title,user_id\&size=3\&from=10
      
  • 全文檢索

      curl -X GET 127.0.0.1:9200/articles/article/_search?q=content:python%20web\&_source=title,article_id\&pretty
    
      curl -X GET 127.0.0.1:9200/articles/article/_search?q=title:python%20web,content:python%20web\&_source=title,article_id\&pretty
    
      curl -X GET 127.0.0.1:9200/articles/article/_search?q=_all:python%20web\&_source=title,article_id\&pretty
    
    • %20 表示空格

2 高階查詢

  • 全文檢索 match

      curl -X GET 127.0.0.1:9200/articles/article/_search -d'
      {
          "query" : {
              "match" : {
                  "title" : "python web"
              }
          }
      }'
    
      curl -X GET 127.0.0.1:9200/articles/article/_search?pretty -d'
      {
          "from": 0,
          "size": 5,
          "_source": ["article_id","title"],
          "query" : {
              "match" : {
                  "title" : "python web"
              }
          }
      }'
    
      curl -X GET 127.0.0.1:9200/articles/article/_search?pretty -d'
      {
          "from": 0,
          "size": 5,
          "_source": ["article_id","title"],
          "query" : {
              "match" : {
                  "_all" : "python web 程式設計"
              }
          }
      }'
    
  • 短語搜尋 match_phrase

      curl -X GET 127.0.0.1:9200/articles/article/_search?pretty -d'
      {
          "size": 5,
          "_source": ["article_id","title"],
          "query" : {
              "match_phrase" : {
                  "_all" : "python web"
              }
          }
      }'
    
  • 精確查詢 term

      curl -X GET 127.0.0.1:9200/articles/article/_search?pretty -d'
      {
          "size": 5,
          "_source": ["article_id","title", "user_id"],
          "query" : {
              "term" : {
                  "user_id" : 1
              }
          }
      }'
    
  • 範圍查詢 range

      curl -X GET 127.0.0.1:9200/articles/article/_search?pretty -d'
      {
          "size": 5,
          "_source": ["article_id","title", "user_id"],
          "query" : {
              "range" : {
                  "article_id": { 
                      "gte": 3,
                      "lte": 5
                  }
              }
          }
      }'
    
  • 高亮搜尋 highlight

      curl -X GET 127.0.0.1:9200/articles/article/_search?pretty -d '
      {
          "size":2,
          "_source": ["article_id", "title", "user_id"],
          "query": {
              "match": {
                   "title": "python web 程式設計"
               }
           },
           "highlight":{
                "fields": {
                    "title": {}
                }
           }
      }
      '
    
  • 組合查詢

    • must

      文件 必須 匹配這些條件才能 纔能被包含進來。

    • must_not

      文件 必須不 匹配這些條件才能 纔能被包含進來。

    • should

      如果滿足這些語句中的任意語句,將增加 _score ,否則,無任何影響。它們主要用於修正每個文件的相關性得分。

    • filter

      必須 匹配,但它以不評分、過濾模式來進行。這些語句對評分沒有貢獻,只是根據過濾標準來排除或包含文件。

      curl -X GET 127.0.0.1:9200/articles/article/_search?pretty -d '
      {
        "_source": ["title", "user_id"],
        "query": {
            "bool": {
                "must": {
                    "match": {
                        "title": "python web"
                    }
                },
                "filter": {
                    "term": {
                        "user_id": 2
                    }
                }
            }
        }
      }
      '
      
  • 排序

      curl -X GET 127.0.0.1:9200/articles/article/_search?pretty -d'
      {
          "size": 5,
          "_source": ["article_id","title"],
          "query" : {
              "match" : {
                  "_all" : "python web"
              }
          },
          "sort": [
              { "create_time":  { "order": "desc" }},
              { "_score": { "order": "desc" }}
          ]
      }'
    
  • boost 提升權重,優化排序

      curl -X GET 127.0.0.1:9200/articles/article/_search?pretty -d'
      {
          "size": 5,
          "_source": ["article_id","title"],
          "query" : {
              "match" : {
                  "title" : {
                      "query": "python web",
                      "boost": 4
                  }
              }
          }
      }'

頭條全文檢索實現

elasticsearch python用戶端使用

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

pip install elasticsearch

對於elasticsearch 5.x 版本 需要按以下方式匯入

from elasticsearch5 import Elasticsearch

# elasticsearch叢集伺服器的地址
ES = [
    '127.0.0.1:9200'
]

# 建立elasticsearch用戶端
es = Elasticsearch(
    ES,
    # 啓動前嗅探es叢集伺服器
    sniff_on_start=True,
    # es叢集伺服器結點連線異常時是否重新整理es結點資訊
    sniff_on_connection_fail=True,
    # 每60秒重新整理結點資訊
    sniffer_timeout=60
)

搜尋使用方式

query = {
    'query': {
        'bool': {
            'must': [
                {'match': {'_all': 'python web'}}
            ],
            'filter': [
                {'term': {'status': 2}}
            ]
        }
    }
}
ret = es.search(index='articles', doc_type='article', body=query)

頭條專案搜尋介面檢視實現

在toutiao-backend/toutiao/resources/search目錄中新建search.py

from flask_restful import Resource
from flask_restful.reqparse import RequestParser
from flask_restful import inputs
from flask import g, current_app
from redis.exceptions import RedisError

from . import constants
from cache import article as cache_article
from cache import user as cache_user
from models.user import Search
from models import db

class SearchResource(Resource):
    """
    搜尋結果
    """
    def get(self):
        """
        獲取搜尋結果
        """
        qs_parser = RequestParser()
        qs_parser.add_argument('q', type=inputs.regex(r'^.{1,50}$'), required=True, location='args')
        qs_parser.add_argument('page', type=inputs.positive, required=False, location='args')
        qs_parser.add_argument('per_page', type=inputs.int_range(constants.DEFAULT_SEARCH_PER_PAGE_MIN, constants.DEFAULT_SEARCH_PER_PAGE_MAX, 'per_page'), required=False, location='args')
        args = qs_parser.parse_args()
        q = args.q
        page = 1 if args.page is None else args.page
        per_page = args.per_page if args.per_page else constants.DEFAULT_SEARCH_PER_PAGE_MIN

        # Search from Elasticsearch
        query = {
            'from': (page-1)*per_page,
            'size': per_page,
            '_source': False,
            'query': {
                'bool': {
                    'must': [
                        {'match': {'_all': q}}
                    ],
                    'filter': [
                        {'term': {'status': 2}}
                    ]
                }
            }
        }
        ret = current_app.es.search(index='articles', doc_type='article', body=query)

        total_count = ret['hits']['total']

        results = []

        hits = ret['hits']['hits']
        for result in hits:
            article_id = int(result['_id'])
            article = cache_article.ArticleInfoCache(article_id).get()
            if article:
                results.append(article)

        # Record user search history
        if g.user_id and page == 1:
            try:
                cache_user.UserSearchingHistoryStorage(g.user_id).save(q)
            except RedisError as e:
                current_app.logger.error(e)

        return {'total_count': total_count, 'page': page, 'per_page': per_page, 'results': results}

在toutiao-backend/toutiao/resources/search目錄中新建constants.py

# 搜尋結果分頁預設每頁數量 下限
DEFAULT_SEARCH_PER_PAGE_MIN = 10

# 搜尋結果頁預設每頁數量 上限
DEFAULT_SEARCH_PER_PAGE_MAX = 50

新增ES新文章索引數據

在自媒體平臺發佈文章介面中,除了儲存文章外,還要向es庫中新增新文章的索引

doc = {
          'article_id': article.id,
          'user_id': article.user_id,
          'title': article.title,
          'content': article.content.content,
          'status': article.status,
          'create_time': article.ctime
      }
current_app.es.index(index='articles', doc_type='article', body=doc, id=article.id)

聯想提示

1 拼寫糾錯

對於已經建立的articles索引庫,elasticsearch還提供了一種查詢模式,suggest建議查詢模式

curl 127.0.0.1:9200/articles/article/_search?pretty -d '
{
    "from": 0,
    "size": 10,
    "_source": false,
    "suggest": {
        "text": "phtyon web",
        "word-phrase": {
            "phrase": {
                "field": "_all",
                "size": 1
            }
        }
    }
}'

當我們輸入錯誤的關鍵詞phtyon web時,es可以提供根據索引庫數據得出的正確拼寫python web

2 自動補全

使用elasticsearch提供的自動補全功能,因爲文件的型別對映要特殊設定,所以原先建立的文章索引庫不能用於自動補全,需要再建立一個自動補全的索引庫

curl -X PUT 127.0.0.1:9200/completions -H 'Content-Type: application/json' -d'
{
   "settings" : {
       "index": {
           "number_of_shards" : 3,
           "number_of_replicas" : 1
       }
   }
}
'
curl -X PUT 127.0.0.1:9200/completions/_mapping/words -H 'Content-Type: application/json' -d'
{
     "words": {
          "properties": {
              "suggest": {
                  "type": "completion",
                  "analyzer": "ik_max_word"
              }
          }
     }
}
'

使用logstash匯入初始數據

編輯logstash_mysql_completion.conf

input{
     jdbc {
         jdbc_driver_library => "/home/python/mysql-connector-java-8.0.13/mysql-connector-java-8.0.13.jar"
         jdbc_driver_class => "com.mysql.jdbc.Driver"
         jdbc_connection_string => "jdbc:mysql://127.0.0.1:3306/toutiao?tinyInt1isBit=false"
         jdbc_user => "root"
         jdbc_password => "mysql"
         jdbc_paging_enabled => "true"
         jdbc_page_size => "1000"
         jdbc_default_timezone =>"Asia/Shanghai"
         statement => "select title as suggest from news_article_basic"
         clean_run => true
     }
}
output{
      elasticsearch {
         hosts => "127.0.0.1:9200"
         index => "completions"
         document_type => "words"
      }
}

執行命令匯入數據

sudo /usr/share/logstash/bin/logstash -f ./logstash_mysql_completion.conf

自動補全建議查詢

curl 127.0.0.1:9200/completions/words/_search?pretty -d '
{
    "suggest": {
        "title-suggest" : {
            "prefix" : "pyth", 
            "completion" : { 
                "field" : "suggest" 
            }
        }
    }
}
'

curl 127.0.0.1:9200/completions/words/_search?pretty -d '
{
    "suggest": {
        "title-suggest" : {
            "prefix" : "python web", 
            "completion" : { 
                "field" : "suggest" 
            }
        }
    }
}
'

=======================

頭條suggest查詢實現

思路

  1. 先將關鍵字在completions 自動補全索引庫中查詢,獲取建議的補全資訊
  2. 如沒有獲取到補全資訊,可能表示使用者輸入的關鍵詞有拼寫錯誤,在articles索引庫中進行糾錯建議查詢

實現

在toutiao-backend/toutiao/resources/search.py中實現自動補全檢視

class SuggestionResource(Resource):
    """
    聯想建議
    """
    def get(self):
        """
        獲取聯想建議
        """
        qs_parser = RequestParser()
        qs_parser.add_argument('q', type=inputs.regex(r'^.{1,50}$'), required=True, location='args')
        args = qs_parser.parse_args()
        q = args.q

        # 先嚐試自動補全建議查詢
        query = {
            'from': 0,
            'size': 10,
            '_source': False,
            'suggest': {
                'word-completion': {
                    'prefix': q,
                    'completion': {
                        'field': 'suggest'
                    }
                }
            }
        }
        ret = current_app.es.search(index='completions', body=query)
        options = ret['suggest']['word-completion'][0]['options']

        # 如果沒得到查詢結果,進行糾錯建議查詢
        if not options:
            query = {
                'from': 0,
                'size': 10,
                '_source': False,
                'suggest': {
                    'text': q,
                    'word-phrase': {
                        'phrase': {
                            'field': '_all',
                            'size': 1
                        }
                    }
                }
            }
            ret = current_app.es.search(index='articles', doc_type='article', body=query)
            options = ret['suggest']['word-phrase'][0]['options']

        results = []
        for option in options:
            if option['text'] not in results:
                results.append(option['text'])

        return {'options': results}

=====================================

單元測試

爲什麼要測試

Web程式開發過程一般包括以下幾個階段:[需求分析,設計階段,實現階段,測試階段]。其中測試階段通過人工或自動來執行測試某個系統的功能。目的是檢驗其是否滿足需求,並得出特定的結果,以達到弄清楚預期結果和實際結果之間的差別的最終目的。

測試的分類

測試從軟件開發過程可以分爲:

  • 單元測試
    • 對單獨的程式碼塊(例如函數)分別進行測試,以保證它們的正確性
  • 整合測試
    • 對大量的程式單元的協同工作情況做測試
  • 系統測試
    • 同時對整個系統的正確性進行檢查,而不是針對獨立的片段

在衆多的測試中,與程式開發人員最密切的就是單元測試,因爲單元測試是由開發人員進行的,而其他測試都由專業的測試人員來完成。所以我們主要學習單元測試。

什麼是單元測試

程式開發過程中,寫程式碼是爲了實現需求。當我們的程式碼通過了編譯,只是說明它的語法正確,功能能否實現則不能保證。 因此,當我們的某些功能程式碼完成後,爲了檢驗其是否滿足程式的需求。可以通過編寫測試程式碼,模擬程式執行的過程,檢驗功能程式碼是否符合預期。

單元測試就是開發者編寫一小段程式碼,檢驗目的碼的功能是否符合預期。通常情況下,單元測試主要面向一些功能單一的模組進行。

舉個例子:一部手機有許多零部件組成,在正式組裝一部手機前,手機內部的各個零部件,CPU、記憶體、電池、攝像頭等,都要進行測試,這就是單元測試。

在Web開發過程中,單元測試實際上就是一些「斷言」(assert)程式碼。

斷言就是判斷一個函數或物件的一個方法所產生的結果是否符合你期望的那個結果。 python中assert斷言是宣告布爾值爲真的判定,如果表達式爲假會發生異常。單元測試中,一般使用assert來斷言結果。

斷言方法的使用:

斷言語句類似於:

if not expression:    
    raise AssertionError
 AssertionError

常用的斷言方法:

assertEqual     如果兩個值相等,則pass
assertNotEqual  如果兩個值不相等,則pass
assertTrue      判斷bool值爲True,則pass
assertFalse     判斷bool值爲False,則pass
assertIsNone    不存在,則pass
assertIsNotNone 存在,則pass

單元測試的基本寫法

首先,定義一個類,繼承自unittest.TestCase

import unittest
class TestClass(unitest.TestCase):
    pass

其次,在測試類中,定義兩個測試方法

import unittest
class TestClass(unittest.TestCase):

    #該方法會首先執行,方法名爲固定寫法
    def setUp(self):
        pass

    #該方法會在測試程式碼執行完後執行,方法名爲固定寫法
    def tearDown(self):
        pass

最後,在測試類中,編寫測試程式碼

import unittest
class TestClass(unittest.TestCase):

    #該方法會首先執行,相當於做測試前的準備工作
    def setUp(self):
        pass

    #該方法會在測試程式碼執行完後執行,相當於做測試後的掃尾工作
    def tearDown(self):
        pass

    #測試程式碼
    def test_app_exists(self):
        pass

單元測試案例

以編寫頭條專案搜尋業務中自動補全介面的單元測試爲例。

在toutiao-backend/toutiao目錄中新建 tests包,並在其中新建test_search.py檔案

import os
import sys

BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
sys.path.insert(0, os.path.join(BASE_DIR))
sys.path.insert(0, os.path.join(BASE_DIR, 'common'))


import unittest
from toutiao import create_app
from settings.testing import TestingConfig
import json


class SuggestionTest(unittest.TestCase):
    """搜尋建議介面測試案例"""

    def setUp(self):
        """
        在執行測試方法前先被執行
        :return:
        """
        self.app = create_app(TestingConfig)
        self.client = self.app.test_client()

    def test_missing_request_q_param(self):
        """
        測試缺少q的請求參數
        """
        resp = self.client.get('/v1_0/suggestion')
        self.assertEqual(resp.status_code, 400)

    def test_request_q_param_error_length(self):
        """
        測試q參數錯誤長度
        """
        resp = self.client.get('/v1_0/suggestion?q='+'e'*51)
        self.assertEqual(resp.status_code, 400)

    def test_normal(self):
        """
        測試正常請求
        """
        resp = self.client.get('/v1_0/suggestion?q=ptyhon')
        self.assertEqual(resp.status_code, 200)

        resp_json = resp.data
        resp_dict = json.loads(resp_json)
        self.assertIn('message', resp_dict)
        self.assertIn('data', resp_dict)
        data = resp_dict['data']
        self.assertIn('options', data)


if __name__ == '__main__':
    unittest.main()

==========================

部署相關

Gunicorn

Gunicorn(綠色獨角獸)是一個Python WSGI的HTTP伺服器。從Ruby的獨角獸(Unicorn )專案移植。該Gunicorn伺服器與各種Web框架相容,實現非常簡單,輕量級的資源消耗。Gunicorn直接用命令啓動,不需要編寫組態檔,相對uWSGI要容易很多。

安裝gunicorn

pip install gunicorn

檢視命令列選項: 安裝gunicorn成功後,通過命令列的方式可以檢視gunicorn的使用資訊。

$gunicorn -h

直接執行:

#直接執行,預設啓動的127.0.0.1::8000
gunicorn 執行檔名稱:Flask程式範例名

指定進程和埠號: -w: 表示進程(worker)。 -b:表示系結ip地址和埠號(bind)。

$gunicorn -w 4 -b 127.0.0.1:5001 執行檔名稱:Flask程式範例名

Supervisor

supervisor是進程管理工具

安裝

supervisor對python3支援不好,须使用python2

sudo pip install supervisor

設定

執行echo_supervisord_conf命令輸出預設的設定項,可以如下操作將預設設定儲存到檔案中

echo_supervisord_conf > supervisord.conf

vim 開啓編輯supervisord.conf檔案,修改

[include]
files = relative/directory/*.ini

[include]
files = /etc/supervisor/*.conf

include選項指明包含的其他組態檔。

將編輯後的supervisord.conf檔案複製到/etc/目錄下

sudo cp supervisord.conf /etc/

然後我們在/etc目錄下新建子目錄supervisor(與組態檔裡的選項相同),並在/etc/supervisor/中新建tuotiao管理的組態檔toutiao.conf。

[group:toutiao]
programs=toutiao-app

[program:toutiao-app]
command=/home/python/scripts/toutiao_app.sh
directory=/home/python/toutiao-backend
user=python
autorestart=true
redirect_stderr=false
loglevel=info
stopsignal=KILL
stopasgroup=true
killasgroup=true

[program:im]
command=/home/python/scripts/im.sh
directory=/home/python/toutiao-backend
user=python
autorestart=true
redirect_stderr=false
loglevel=info
stopsignal=KILL
stopasgroup=true
killasgroup=true

啓動

supervisord -c /etc/supervisord.conf

檢視 supervisord 是否在執行:

ps aux | grep supervisord

supervisorctl

我們可以利用supervisorctl來管理supervisor。

supervisorctl

> status    # 檢視程式狀態
> start apscheduler  # 啓動 apscheduler 單一程式
> stop toutiao:*   # 關閉 toutiao組 程式
> start toutiao:*  # 啓動 toutiao組 程式
> restart toutiao:*    # 重新啓動 toutiao組 程式
> update    # 重新啓動組態檔修改過的程式

執行status命令時,顯示如下資訊說明程式執行正常:

supervisor> status
toutiao:toutiao-app RUNNING pid 32091, uptime 00:00:02