Elasticsearch初探搜索與分析

一、Elasticsearch簡介

Elasticsearch 是一個實時的分佈式搜索分析引擎,建立在一個全文搜索引擎庫 Apache Lucene 基礎之上。 它面向文檔存儲,天生分佈式,有效隱藏了分佈式的複雜性,它能讓你以前所未有的速度和規模,去探索數據。 它被用作全文檢索、結構化搜索、分析以及這三個功能的組合。它可以被下面這樣準確的形容:

  • 一個分佈式的實時文檔存儲,每個字段可以被索引與搜索。
  • 一個分佈式實時分析搜索引擎。
  • 能勝任上百個服務節點的擴展,並支持 PB 級別的結構化或者非結構化數據。

二、和Elasticsearch的交互方式

①使用 RESTful API 通過端口 9200 和 Elasticsearch 進行通信,比如:可以使用 curl 命令來和 Elasticsearch 交互。 一個 Elasticsearch 請求和任何 HTTP 請求一樣由若干相同的部分組成:

curl -X<VERB> '<PROTOCOL>://<HOST>:<PORT>/<PATH>?<QUERY_STRING>' -d '<BODY>'

被 < > 標記的部分解釋如下:

VERB 適當的 HTTP 方法 或 謂詞 : GET、 POST、 PUT、 HEAD 或者 DELETE。
PROTOCOL http 或者 https(如果你在 Elasticsearch 前面有一個 https 代理)
HOST Elasticsearch 集羣中任意節點的主機名或者IP,或者用 localhost 代表本地機器上的節點。
PORT 運行 Elasticsearch HTTP 服務的端口號,默認是 9200 。
PATH API 的終端路徑(例如 _count 將返回集羣中文檔數量)。Path 可能包含多個組件,例如:_cluster/stats 和 _nodes/stats/jvm 。
QUERY_STRING 任意可選的查詢字符串參數 (例如 ?pretty 將格式化地輸出 JSON 返回值,使其更容易閱讀)
BODY 一個 JSON 格式的請求體 (如果請求需要的話)

②根據應用使用的編程語言選擇使用官方提供的各個客戶端API:Elasticsearch Clients

三、基本概念

Elasticsearch是面向文檔進行存儲,一個文檔就是一條數據,類似於關係型數據庫表中的一條記錄,不同的是我們以JSON格式數據表示一個文檔,所以更加靈活,每一個文檔的存儲需要指定一個索引以及類型。索引、類型、文檔之前的關係如下:

索引下有多個類型,每個類型下有多個文檔。

以上關係和常用的關係型數據庫可以類比如下:

索引 相當於 數據庫
類型 相當於 表
文檔 相當於 表中的一條記錄

但是最新版的Elasticsearch已經不支持一個索引下有多個類型,默認類型都是_doc。這是因爲同一個索引下的多個類型間並不像數據庫表之間是完全獨立的,比如:兩個不同類型的文檔含有相同的字段,並且這兩個文檔對應的兩個類型在同一個索引下面,此時Elasticsearch 底層的Apache Lucene在建立索引的時候會把這兩個字段當成是同一個來處理,因此要求這兩個字段的映射類型必須一樣,否則就會造成映射衝突,這顯然是不合理的。

索引除了上面的含義之外,我們把一個文檔新增存儲到Elasticsearch中的這個過程也叫做索引,此時索引是一個動詞。

四、搜索與分析

1.準備數據

我們通過Kibana的控制檯建立一個索引users存儲用戶信息,並放入幾條數據:
在這裏插入圖片描述
注意,PUT後面的路徑 /users/_doc/1 包含了三部分的信息:
users:索引名稱
_doc:類型名稱
1:文檔id

/users/_create/2中的_create表示新增,只能執行一次,再次執行就會出錯,而 /users/_doc/1 可以多次執行,當ID爲1的用戶不存在時候新增,存在則會更新。

無需進行執行管理任務,如創建一個索引或指定每個屬性的數據類型之類的,可以直接索引一個文檔。Elasticsearch 默認地完成其他一切,因此所有必需的管理任務都在後臺使用默認設置完成。

2.搜索單個文檔

簡單地執行 一個 HTTP GET 請求並指定文檔的地址——索引庫、類型和ID。 使用這三個信息可以返回原始的 JSON 文檔:
在這裏插入圖片描述

返回結果包含了文檔的一些元數據,以及 _source 屬性,內容是原始 JSON 文檔。

3.輕量搜索

查詢所有的用戶:

GET /users/_search

與指定一個文檔 ID 不同,這次使用 _search 。返回結果包括了所有三個文檔,放在數組 hits 中,一個搜索默認返回十條結果。

{
  "took" : 1,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 3,
      "relation" : "eq"
    },
    "max_score" : 1.0,
    "hits" : [
      {
        "_index" : "users",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 1.0,
        "_source" : {
          "name" : "張三",
          "age" : 25,
          "about" : "我喜歡爬山",
          "interests" : [
            "跑步",
            "爬山",
            "音樂",
            "打籃球"
          ]
        }
      },
      {
        "_index" : "users",
        "_type" : "_doc",
        "_id" : "2",
        "_score" : 1.0,
        "_source" : {
          "name" : "張四",
          "age" : 32,
          "about" : "我喜歡爬黃山",
          "interests" : [
            "音樂"
          ]
        }
      },
      {
        "_index" : "users",
        "_type" : "_doc",
        "_id" : "3",
        "_score" : 1.0,
        "_source" : {
          "name" : "李四",
          "age" : 35,
          "about" : "我喜歡打網球",
          "interests" : [
            "跳舞",
            "運動"
          ]
        }
      }
    ]
  }
}

搜索姓張的用戶,通過一個URL參數來傳遞查詢條件信息給搜索接口,仍然在請求路徑中使用 _search ,並將查詢條件本身賦值給參數 q:

GET /users/_search?q=name:張

返回了所有姓張的用戶:

{
  "took" : 1,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 2,
      "relation" : "eq"
    },
    "max_score" : 0.47000363,
    "hits" : [
      {
        "_index" : "users",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 0.47000363,
        "_source" : {
          "name" : "張三",
          "age" : 25,
          "about" : "我喜歡爬山",
          "interests" : [
            "跑步",
            "爬山",
            "音樂",
            "打籃球"
          ]
        }
      },
      {
        "_index" : "users",
        "_type" : "_doc",
        "_id" : "2",
        "_score" : 0.47000363,
        "_source" : {
          "name" : "張四",
          "age" : 32,
          "about" : "我喜歡爬黃山",
          "interests" : [
            "音樂"
          ]
        }
      }
    ]
  }
}

4.查詢表達式搜索

前面將查詢條件放在URL參數中的做法有一定的侷限性,所以Elasticsearch 提供了豐富靈活的查詢語言叫做查詢表達式 , 它支持構建更加複雜和健壯的查詢。
前面的搜索等價於下面的查詢表達式:

GET /users/_search
{
  "query": {
    "match": {
      "name": "張"
    }
  }
}

返回結果與之前的查詢一樣,不同的是不再使用URL 參數,而是用一個JSON請求體表示查詢條件,這裏使用了一個 match 查詢(屬於查詢類型之一,更多的查詢類型後續進行詳細介紹)。

5.複雜查詢

搜索姓張的用戶,但這次加上年齡大於 30 的條件。查詢需要稍作調整,使用過濾器 filter ,它支持高效地執行一個結構化查詢。

GET /users/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "name": "張"
          }
        }
      ],
      "filter": {
        "range": {
          "age": {
            "gt": 30    //gt表示大於
          }
        }
      }
    }
  }
}

現在結果只返回了一名員工,年齡爲32:

{
  "took" : 1,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 1,
      "relation" : "eq"
    },
    "max_score" : 0.47000363,
    "hits" : [
      {
        "_index" : "users",
        "_type" : "_doc",
        "_id" : "2",
        "_score" : 0.47000363,
        "_source" : {
          "name" : "張四",
          "age" : 32,
          "about" : "我喜歡爬黃山",
          "interests" : [
            "音樂"
          ]
        }
      }
    ]
  }
}

6.全文搜索

目前的搜索相對都很簡單:單個姓名,通過年齡過濾。現在嘗試下稍微高級點兒的全文搜索(傳統數據庫確實很難搞定的任務) 。
搜索下所有喜歡爬山的員工:

GET /users/_search
{
  "query": {
   "match": {
     "about": "爬山"
   }
  }
}

返回兩個匹配的文檔:

{
  "took" : 101,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 2,
      "relation" : "eq"
    },
    "max_score" : 0.9875357,
    "hits" : [
      {
        "_index" : "users",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 0.9875357,
        "_source" : {
          "name" : "張三",
          "age" : 25,
          "about" : "我喜歡爬山",
          "interests" : [
            "跑步",
            "爬山",
            "音樂",
            "打籃球"
          ]
        }
      },
      {
        "_index" : "users",
        "_type" : "_doc",
        "_id" : "2",
        "_score" : 0.9179183,
        "_source" : {
          "name" : "張四",
          "age" : 32,
          "about" : "我喜歡爬黃山",
          "interests" : [
            "音樂"
          ]
        }
      }
    ]
  }
}

喜歡“爬山”和喜歡“爬黃山”的兩個用戶都被搜索出來了,返回結果中有一個“_score”屬性,它表示相關性得分(跟查詢的匹配程度),Elasticsearch 默認按照相關性得分由高到低進行排序,相關性的概念與傳統數據庫完全不同的地方,數據庫中的一條記錄要麼匹配要麼不匹配。

7.短語搜索

想要精確匹配一系列單詞或者短語 。 比如, 我們想執行這樣一個查詢,僅匹配興趣愛好中有“爬山”(“爬黃山”不符合,因爲多了一個“黃”字)的用戶,此時只需要使用一個match_phrase查詢:

GET /users/_search
{
  "query": {
   "match_phrase": {
     "about": "爬山"
   }
  }
}

返回結果只有一個:

{
  "took" : 2,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 1,
      "relation" : "eq"
    },
    "max_score" : 0.9875357,
    "hits" : [
      {
        "_index" : "users",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 0.9875357,
        "_source" : {
          "name" : "張三",
          "age" : 25,
          "about" : "我喜歡爬山",
          "interests" : [
            "跑步",
            "爬山",
            "音樂",
            "打籃球"
          ]
        }
      }
    ]
  }
}

8.高亮搜索

有的應用傾向於在每個搜索結果中高亮部分文本片段,以便讓用戶知道爲何該文檔符合查詢條件,在 Elasticsearch 中檢索出高亮片段也很容易,再次執行前面的查詢,並增加一個新的 highlight 參數:

GET /users/_search
{
  "query": {
   "match_phrase": {
     "about": "爬山"
   }
  },
  "highlight": {
    "fields": {
      "about": {}
    }
  }
}

返回結果與之前一樣,與此同時結果中還多了一個叫做 highlight 的部分。這個部分包含了 about 屬性匹配的文本片段,並以 HTML 標籤<em></em>封裝:

{
  "took" : 8,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 1,
      "relation" : "eq"
    },
    "max_score" : 0.9875357,
    "hits" : [
      {
        "_index" : "users",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 0.9875357,
        "_source" : {
          "name" : "張三",
          "age" : 25,
          "about" : "我喜歡爬山",
          "interests" : [
            "跑步",
            "爬山",
            "音樂",
            "打籃球"
          ]
        },
        "highlight" : {
          "about" : [
            "我喜歡<em>爬</em><em>山</em>"
          ]
        }
      }
    ]
  }
}

9.分析

有時候需要對查詢結果做一些分析,比如有這樣的需求:根據interests字段挖掘出用戶中最受歡迎的興趣愛好;此時就要用到 Elasticsearch 提供的聚合功能(aggregations),允許我們基於數據生成一些精細的分析結果。聚合與 SQL 中的 GROUP BY 類似但更強大:

GET /users/_search
{
 "aggs": {
   "all_interests": {
     "terms": {
       "field": "interests.keyword"
     }
   }
 }
}

返回結果:

{
  "took" : 170,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 3,
      "relation" : "eq"
    },
    "max_score" : 1.0,
    "hits" : [
      {
        "_index" : "users",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 1.0,
        "_source" : {
          "name" : "張三",
          "age" : 25,
          "about" : "我喜歡爬山",
          "interests" : [
            "跑步",
            "爬山",
            "音樂",
            "打籃球"
          ]
        }
      },
      {
        "_index" : "users",
        "_type" : "_doc",
        "_id" : "2",
        "_score" : 1.0,
        "_source" : {
          "name" : "張四",
          "age" : 32,
          "about" : "我喜歡爬黃山",
          "interests" : [
            "音樂"
          ]
        }
      },
      {
        "_index" : "users",
        "_type" : "_doc",
        "_id" : "3",
        "_score" : 1.0,
        "_source" : {
          "name" : "李四",
          "age" : 35,
          "about" : "我喜歡打網球",
          "interests" : [
            "跳舞",
            "運動"
          ]
        }
      }
    ]
  },
  "aggregations" : {
    "all_interests" : {
      "doc_count_error_upper_bound" : 0,
      "sum_other_doc_count" : 0,
      "buckets" : [
        {
          "key" : "音樂",
          "doc_count" : 2
        },
        {
          "key" : "打籃球",
          "doc_count" : 1
        },
        {
          "key" : "爬山",
          "doc_count" : 1
        },
        {
          "key" : "跑步",
          "doc_count" : 1
        },
        {
          "key" : "跳舞",
          "doc_count" : 1
        },
        {
          "key" : "運動",
          "doc_count" : 1
        }
      ]
    }
  }
}

可以看到聚合的結果數據在aggregations中,統計出了每種興趣愛好的用戶數目,如果想知道姓張的員工中最受歡迎的興趣愛好,可以直接構造一個組合查詢:

GET /users/_search
{
  "query": {
    "match": {
      "name": "張"
    }
  }, 
 "aggs": {
   "all_interests": {
     "terms": {
       "field": "interests.keyword"
     }
   }
 }
}

返回結果:

{
  "took" : 15,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 2,
      "relation" : "eq"
    },
    "max_score" : 0.47000363,
    "hits" : [
      {
        "_index" : "users",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 0.47000363,
        "_source" : {
          "name" : "張三",
          "age" : 25,
          "about" : "我喜歡爬山",
          "interests" : [
            "跑步",
            "爬山",
            "音樂",
            "打籃球"
          ]
        }
      },
      {
        "_index" : "users",
        "_type" : "_doc",
        "_id" : "2",
        "_score" : 0.47000363,
        "_source" : {
          "name" : "張四",
          "age" : 32,
          "about" : "我喜歡爬黃山",
          "interests" : [
            "音樂"
          ]
        }
      }
    ]
  },
  "aggregations" : {
    "all_interests" : {
      "doc_count_error_upper_bound" : 0,
      "sum_other_doc_count" : 0,
      "buckets" : [
        {
          "key" : "音樂",
          "doc_count" : 2
        },
        {
          "key" : "打籃球",
          "doc_count" : 1
        },
        {
          "key" : "爬山",
          "doc_count" : 1
        },
        {
          "key" : "跑步",
          "doc_count" : 1
        }
      ]
    }
  }
}

聚合還支持分級彙總 。比如要統計每種興趣愛好的用戶的平均年齡:

GET /users/_search
{
 "aggs": {
   "all_interests": {
     "terms": {
       "field": "interests.keyword"
     },
     "aggs": {
       "avg_age": {
         "avg": {
           "field": "age"
         }
       }
     }
   }
 }
}

返回結果:

{
  "took" : 532,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 3,
      "relation" : "eq"
    },
    "max_score" : 1.0,
    "hits" : [
      {
        "_index" : "users",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 1.0,
        "_source" : {
          "name" : "張三",
          "age" : 25,
          "about" : "我喜歡爬山",
          "interests" : [
            "跑步",
            "爬山",
            "音樂",
            "打籃球"
          ]
        }
      },
      {
        "_index" : "users",
        "_type" : "_doc",
        "_id" : "2",
        "_score" : 1.0,
        "_source" : {
          "name" : "張四",
          "age" : 32,
          "about" : "我喜歡爬黃山",
          "interests" : [
            "音樂"
          ]
        }
      },
      {
        "_index" : "users",
        "_type" : "_doc",
        "_id" : "3",
        "_score" : 1.0,
        "_source" : {
          "name" : "李四",
          "age" : 35,
          "about" : "我喜歡打網球",
          "interests" : [
            "跳舞",
            "運動"
          ]
        }
      }
    ]
  },
  "aggregations" : {
    "all_interests" : {
      "doc_count_error_upper_bound" : 0,
      "sum_other_doc_count" : 0,
      "buckets" : [
        {
          "key" : "音樂",
          "doc_count" : 2,
          "avg_age" : {
            "value" : 28.5
          }
        },
        {
          "key" : "打籃球",
          "doc_count" : 1,
          "avg_age" : {
            "value" : 25.0
          }
        },
        {
          "key" : "爬山",
          "doc_count" : 1,
          "avg_age" : {
            "value" : 25.0
          }
        },
        {
          "key" : "跑步",
          "doc_count" : 1,
          "avg_age" : {
            "value" : 25.0
          }
        },
        {
          "key" : "跳舞",
          "doc_count" : 1,
          "avg_age" : {
            "value" : 35.0
          }
        },
        {
          "key" : "運動",
          "doc_count" : 1,
          "avg_age" : {
            "value" : 35.0
          }
        }
      ]
    }
  }
}

輸出的聚合結果中,依然有一個興趣及數量的列表,只不過每個興趣都有了一個附加的 avg_age 屬性,代表有這個興趣愛好的所有員工的平均年齡。

五、分佈式特性

Elasticsearch 可以橫向擴展至數百(甚至數千)的服務器節點,同時可以處理PB級數據, Elasticsearch 天生就是分佈式的,並且在設計時儘可能的屏蔽了分佈式的複雜性,比如以下一些在後臺自動執行的操作:

  • 分配文檔到不同的容器 或 分片 中,文檔可以儲存在一個或多個節點中。
  • 按集羣節點來均衡分配這些分片,從而對索引和搜索過程進行負載均衡
  • 複製每個分片以支持數據冗餘,從而防止硬件故障導致的數據丟失
  • 將集羣中任一節點的請求路由到存有相關數據的節點
  • 集羣擴容時無縫整合新節點,重新分配分片以便從離羣節點恢復
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章