1. 程式人生 > 實用技巧 >全文檢索Elasticsearch研究

全文檢索Elasticsearch研究

全文檢索Elasticsearch研究

基於虛擬機器伺服器自主部署ELK服務

學習目標

  1. 瞭解Elasticsearch的應用場景
  2. 學習基於伺服器部署ELK服務
  3. 掌握索引維護的方法
  4. 掌握索引維護的方法
  5. 掌握基本的搜尋API的使用方法

約束

需要提前掌握Lucene的索引方法、搜尋方法

ELK的介紹和安裝

1.簡介

​ Elasticsearch是一個基於Lucene的搜尋伺服器,它提供了一個分散式多使用者能力的全文搜尋引擎,基於Restful web介面。Elasticsearch是用java開發的,是當前流行的企業級搜尋引擎。能到達到實時搜尋,穩定、可靠、快速、安裝使用方便。

​ 我們建立一個網站或應用程式,並要新增搜尋功能,如果搜尋的數量非常多,而且分類繁雜,如果使用傳統的資料庫想要完成搜尋工作的建立失非常困難的。我們希望搜尋解決方案要執行速度快,我們希望有一個零配置和完全免費的搜尋模式,能夠簡單的使用JSON通過HTTP來索引資料,而搜尋伺服器始終可用,並且伺服器可以自如擴充套件,我們一般都會使用全文檢索技術,如solr、Elasticsearch等。

2.突出優點

  1. 擴充套件性好,可部署上百臺伺服器叢集,處理PB級資料
  2. 近實時的去索引資料、搜尋資料

3.原理與應用

3.1 索引結構

下圖是ElasticSearch的索引結構,下邊==黑色部分是物理結構==,上邊==黃色部分是邏輯結構==,邏輯結構可以更好的描述ElasticSearch的工作原理及去使用物理結構中的索引檔案。

BQJJij.md.png

邏輯結構部分是一個倒排索引表:

  1. 將要搜尋的文件內容分詞,所有不重複的詞做成分詞列表。
  2. 將搜尋的文件最終以Document方式儲存起來。
  3. 每個詞和document都有關聯。

3.2 RESTFUL應用方法

ElasticSearch提供RESTFUL Api介面進行索引、搜尋、並且支援多種客戶端。

下圖是ElasticSearch在專案中的應用方式:

BQcykR.md.png
  1. 使用者在前端搜尋關鍵字
  2. 專案前端通過http方式請求專案服務端
  3. 專案服務端通過http RESTful方式請求ES叢集進行搜尋
  4. ES叢集從索引庫檢索資料

4.ElasticaSearc安裝

4.1 安裝配置

  1. 安裝ElasticaSearc7.9.0
  2. 該版本要求至少jdk1.8以上
  3. 解壓elasticsearch-7.9.0-linux-x86_64.tar.gz
    • bin:指令碼目錄,包括:啟動、停止等可執行指令碼
    • config:配置檔案目錄
    • data:索引目錄,存放索引檔案的地方
    • modules:模板目錄,包括了es的功能模組
    • plugins:外掛目錄,es支援外掛機制

4.2 配置檔案

ES配置檔案的地址根據安裝形式的不同而不同:

  1. 使用zip、tar安裝,配置檔案的地址在安裝目錄的config下
  2. 使用RPM安裝,配置檔案在/etc/elasticsearch下
  3. 使用MSI安裝,配置檔案的地址在安裝目錄的config下,並且會自動將config目錄地址寫入環境變數ES_PATH_CONF

4.3 安裝

為了模擬真實場景,我們將在linux系統下安裝Elasticsearch

4.3.1 新建一個使用者gavin

處於安全考慮,Elasticsearch預設不允許以root賬號執行

建立使用者:

useradd gavin

設定密碼:

passwd gavin

切換使用者:

su gavin

刪除使用者:

userdel -r gavin

普通使用者增加sudo命令的許可權:

vim /etc/sudoers

gavin   ALL=(ALL)       ALL

改變目錄及其目錄下所有檔案的所有者為當前普通使用者:

chown -R yourname dirname

4.3.3 解壓縮

tar -zxvf elasticsearch-7.9.0-linux-x86_64.tar.gz

4.3.4 目錄重新命名

mv elasticsearch-7.9.0 elasticsearch

4.3.5 修改配置檔案

進入config目錄,修改elasticsearch.ymljvm.options

  1. jvm.options

預設配置:

-Xms1g
-Xmx1g

記憶體佔用太多,設定為不超過實體記憶體的一半:

-Xms512m
-Xmx512m
  1. elasticsearch.yml

修改資料和日誌目錄

# 資料目錄位置
path.data: /home/gavin/elasticsearch/data
# 日誌目錄位置
path.logs: /home/gavin/elasticsearch/logs

elasticsearch的安裝目錄預設只有logs目錄,沒有data目錄,需要手動建立:

mkdir data

修改繫結的ip:

# 繫結0.0.0.0 允許任何ip來訪問,預設只允許本機訪問
network.host: 0.0.0.0

目前我們是學習單機安裝,如果要做叢集,只需要在這個配置檔案中新增節點資訊即可。

屬性名 說明
cluster.name 配置elasticsearch的叢集名稱,預設是elasticsearch。建議修改成一個有意義的名稱。
node.name 節點名,es會預設隨機指定一個名字,建議指定一個有意義的名稱,方便管理
path.conf 設定配置檔案的儲存路徑,tar或zip包安裝預設在es根目錄下的config資料夾,rpm安裝預設在/etc/ elasticsearch
path.data 設定索引資料的儲存路徑,預設是es根目錄下的data資料夾,可以設定多個儲存路徑,用逗號隔開
path.logs 設定日誌檔案的儲存路徑,預設是es根目錄下的logs資料夾
path.plugins 設定外掛的存放路徑,預設是es根目錄下的plugins資料夾
bootstrap.memory_lock 設定為true可以鎖住ES使用的記憶體,避免記憶體進行swap
network.host 設定bind_host和publish_host,設定為0.0.0.0允許外網訪問
http.port 設定對外服務的http埠,預設為9200。
transport.tcp.port 叢集結點之間通訊埠
discovery.zen.ping.timeout 設定ES自動發現節點連線超時的時間,預設為3秒,如果網路延遲高可設定大些
discovery.zen.minimum_master_nodes 主結點數量的最少值 ,此值的公式為:(master_eligible_nodes / 2) + 1 ,比如:有3個符合要求的主結點,那麼這裡要設定為2

4.4 執行

進入elasticsearch/bin*目錄下,可以看到如下的可執行檔案:

然後輸入執行命令:

./elasticsearch

4.5 啟動報錯

4.5.1 JDK版本過低 並且 不支援 root使用者啟動

解決方案:

1.因為elasticsearch7.9.X內建了jdk,預設是jdk11,但是向下相容,所以可以不用處理

2.切換到普通使用者進行啟動,此時需要修改檔案目錄下所有檔案的所有者為當前使用者。

chown-Rgavin/home/gavin/elasticsearch

4.5.2 叢集節點導致啟動報錯

解決:

vim elasticsearch.yml

ip替換host1等,多節點請新增多個ip地址,單節點可寫按預設來
#配置以下三者,最少其一
#[discovery.seed_hosts, discovery.seed_providers, cluster.initial_master_nodes]
cluster.initial_master_nodes: ["node-1"] #這裡的node-1為node-name配置的值

啟動成功:

可以看到綁定了兩個埠:

  • 9200:客戶端訪問埠
  • 9300:叢集節點之間通訊埠

我們在瀏覽器中訪問:http://192.168.15.100:9200/

5.安裝kibana

kibana是一個基於Node.js的Elasticsearch索引庫資料統計工具,可以利用Elasticsearch的聚合功能,生成各種圖表,如柱狀圖、線狀圖、餅圖等。

而且還提供了操作Elasticsearch索引資料的控制檯,並且提供了一定的API提示,非常有利於學習Elasticsearch的語法。

5.1 安裝

因為Kibana是依賴於node

檢視是否伺服器是否安裝nodejs

[root@centos logs]# node -v
v9.3.0

如果沒有安裝,則安裝步驟如下:

1.可以在下載頁面https://nodejs.org/en/download/中找到下載地址,然後執行指令

wget https://nodejs.org/dist/v9.3.0/node-v9.3.0-linux-x64.tar.xz

2.解壓縮

xz-dnode-v9.3.0-linux-x64.tar.xz
tar-xfnode-v9.3.0-linux-x64.tar

3.部署bin檔案

根據自己nodejs的實際路徑,依次執行下面命令,建立軟連線:

ln -s /usr/local/software/node/bin/node /usr/bin/node
ln -s /usr/local/software/node/bin/npm /usr/bin/npm
ln -s /usr/local/software/node/bin/npx /usr/bin/npx

4.測試

node -v
npm -v
npx -v

5.1.1 解壓縮kibana

tar -zxvf kibana-7.9.0-linux-x86_64.tar.gz

5.1.2 重新命名安裝包

mvkibana-7.9.0-linux-x86_64kibana

5.1.3 修改配置

vim /home/gavin/kibana/config/kibana.yml

elasticsearch.hosts:["http://192.168.15.100:9200"]

5.2 啟動

cd /home/gavin/kibana/bin

./kibana

6.安裝ik分析器

Lucene的IK分詞器早在2012年就已經沒有維護了,現在我們要使用的是在其基礎上維護升級的版本,並且開發為ElasticSearch的繼承外掛了,與ElasticSearch一起維護升級了,版本也保持一致。

6.1 解壓縮

unzipelasticsearch-analysis-ik-7.9.0.zip-d/home/gavin/kibana/plugins/ik-analyzer

6.2 重啟elasticsearch

載入IK分詞器外掛、

6.3 測試

在Dev Tools --> console 中輸入下面請求:

POST_analyze
{
"analyzer":"ik_max_word",
"text":"我是中國人"
}

API

Elasticsearch提供了Rest風格的API,即http請求介面,而且也提供了各種語言的客戶端API

文件地址:https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html

1.客戶端API

Elasticsearch支援的客戶端非常多,如:https://www.elastic.co/guide/en/elasticsearch/client/index.html

點開Java Rest Client後,會有兩個:

  • Java Low Level REST Client:是低級別封裝,提供一些基礎功能,但更靈活
  • Java High Level REST Client:是在Low Level Rest Client基礎上進行的高級別封裝,功能更豐富和完善,而且API會變得簡單

2.如何學習

2.1 操作索引

Elasticsearch是基於Lucene的全文檢索庫,本質也是儲存資料,很多概念與mysql類似

對比關係:

索引(indices) --------------------------------------------- Databases 資料庫

​ 型別(type) ------------------------------------------ Table 資料表

​ 文件(Document) ---------------------------Row 行

​ 欄位(field) -----------------------------Columns 列

說明:

概念 說明
索引庫(indices) indices是index的複數,代表許多的索引
型別(type) 型別是模擬mysql中的table概念,一個索引庫下可以有不同型別的索引,比如商品索引、訂單索引,其資料格式不同。不過這會導致索引庫混亂,因此未來版本中會移除這個概念
文件(document) 存入索引庫原始的資料。比如每一條商品資訊,就是一個文件
欄位(field) 文件中的屬性
對映配置(mappings) 欄位的資料型別,屬性、是否索引、是否儲存等特性

特別說明:

Elasticsearch本身就是分散式的,因此即便你只有一個節點,Elasticsearch預設也會對你的資料進行分片和副本操作,當你向叢集新增新資料時,資料也會在新加入的節點中進行平衡

2.2語法

Elasticsearch採用Rest風格API,因此其API就是一次http請求,可以使用任何工具進行發起http請求

2.1.1 索引庫設定
  1. 建立索引索引庫設定:
  • 請求方式:PUT

  • 請求路徑:/索引名

  • 請求引數:json格式:

    {
    "settings":{
    "number_of_shards":3,
    "number_of_replicas":2
    }
    }

    settings:索引庫的設定

    number_of_shards:分片數量

    number_of_replicas:副本數量

    測試

    1. 使用postman進行建立索引並對索引庫進行設定測試

​ 2. 使用kibana進行建立索引並對索引庫進行設定測試

  1. 檢視索引庫設定

語法:

GETceshi

或者,使用*來查詢所有索引配置:

  1. 刪除索引庫設定

語法:

DELETE/索引庫名

再次檢視

2.1.2 對映配置

索引庫建立好之後就是新增資料,再新增資料之前必須定義對映

對映:

定義文件的過程,文件包含哪些欄位,這些欄位是否儲存、是否索引、是否分詞等

  1. 建立對映欄位

    語法:

    請求方式是PUT,型別名稱和_mapping可以互換位置

    PUT/索引庫名稱/_doc/型別名稱
    {
    "properties":{
    "type":"型別",
    "index":true,
    "store":true,
    "analyzer":"分詞器"
    }
    }

    型別:就是前面提過的type的概念,類似於資料庫中的不同表

    欄位名:任意填寫,可以指定很多屬性,如:

    • type:型別,可以是text、long、short、date、integer、object等
    • index:是否索引,預設為true
    • store:是否儲存,預設是false
    • analyzer:分詞器,這裡的ik_max_word即表示使用ik分詞器

示例:

​ 請求:

PUTceshi/_doc/goods
{
"properties":{
"title":{
"type":"text",
"analyzer":"ik_max_word"
},
"images":{
"type":"keyword",
"index":false
},
"price":{
"type":"float"
}
}
}

特別說明:

傳統ES6建立對映的時候是把上面的**_doc換成_mapping**

ES7這個**_mapping已經移除了,使用_doc**代替

否則會報如下錯誤:

​ 響應結果:

{
"_index":"ceshi",
"_type":"_doc",
"_id":"goods",
"_version":3,
"result":"created",
"_shards":{
"total":2,
"successful":1,
"failed":0
},
"_seq_no":2,
"_primary_term":1
}
  1. 檢視對映關係

    語法:

    GET索引庫名/_mapping

    示例:

    GETceshi/_mapping

    結果:

    1. 欄位屬性詳解

3.1 type

Elasticsearch中支援的資料型別非常豐富

常用的說明下:

  • String型別,分為兩種:
    • text:可分詞,不可參與聚合
    • keyword:不可分詞,資料會作為完整欄位進行匹配,可以參與聚合
  • Numerical:數值型別,分兩類
    • 基本資料型別:long、integer、short、byte、double、float、half_float
    • 浮點數的高精度型別:scaled_float
      • 需要指定一個精度因子,比如10或100。Elasticsearch會把真實值乘以這個因子以後儲存,取出時再還原。
  • Date:日期型別
    • Elasticsearch可以對日期格式化為字串儲存,但是建議我們儲存為毫秒值,儲存為long,節省空間。

3.2 index

index影響欄位的索引情況

  • true:欄位會被索引,則可以用來進行搜尋。預設值是true
  • false:欄位不會被索引,不能用來搜尋

特別說明:

index的預設值就是true,也就是說你不進行任何配置,所有欄位都會被索引。但是有些欄位我們不希望索引的,就需要手動設定index為false

3.3 store

是否將資料額外儲存

在lucene和solr中,我們設定store欄位為false,那麼這個欄位在文件列表中就不會有這個欄位的值,使用者搜尋結果中就不會顯示出來。

但在Elasticsearch中,即便設定為false,也可以搜尋結果。

原因是Elasticsearch在建立文件索引庫時,會將文件中的原始資料備份,儲存到一個叫_source的屬性中,而且我們可以通過過濾_source來選擇哪些要顯示,哪些不顯示。

而如果設定storetrue,就會在_source以外額外儲存一份資料,多餘,因此我們一般都會講store設定為false,事實上,store的預設是就是false

2.1.3 新增資料

1.隨機生成id

通過POST請求,可以向一個已經存在的索引庫中新增資料

語法:

POST/索引庫名/_doc/型別名
{
"key":"value"
}

示例:

POST/ceshi/_doc/goods
{
"title":"小米手機",
"images":"http://image.leyou.com/12479122.jpg",
"price":2688.01
}

執行結果:

檢視新增資料結果

GET/ceshi/_search
{
"query":{
"match_all":{}
}
}
  • _source:源文件資訊,所有資料都在裡面
  • _id:這條文件的唯一標識,與文件自己的id沒有關聯

2.自定義id

如果我們在新增資料的時候指定id,可以按如下操作:

語法:

POST/索引庫名/_doc/id
{
"key":"value"
}

示例:

POST/ceshi/_doc/2
{
"title":"小米手機",
"images":"http://image.leyou.com/12479122.jpg",
"price":2988.02
}
2.1.4 智慧判斷

在學習solr時,我們在新增資料時,智慧使用提前批配置好對映屬性的欄位,否則就會報錯

不過在Elasticsearch中,可以不需要給索引庫設定任何的對映屬性的欄位,它也可以根據輸入的資料來判斷型別,動態新增資料對映

示例:

POST/ceshi/_doc/3
{
"title":"超米手機",
"images":"http://image.leyou.com/12479122.jpg",
"price":2899.00,
"stock":200,
"saleable":true
}

我們發現,ceshi索引庫額外增加了stock庫存和saleable是否上架兩個欄位。

檢視此時的索引庫對映關係

GETceshi/_mapping
2.1.5 修改資料

把剛才新增請求的方式改為PUT,就是修改了,不過修改必須指定id

  • id對應文件存在,則修改
  • id對應文件不存在,則新增

比如,我們把id為3的資料進行修改:

PUT/ceshi/_doc/3
{
"title":"超大米手機",
"images":"http://image.leyou.com/12479122.jpg",
"price":3899.00,
"stock":100,
"saleable":true
}
2.1.6 刪除資料

刪除資料使用DELETE請求方式,同樣,根據id進行刪除

語法:

DELETE/索引庫名/_doc/id

示例:

DELETE/ceshi/_doc/1

結果:刪除id為1的索引庫資料

2.1.7 查詢資料
  • 基本查詢
  • _source過濾
  • 結果過濾
  • 高階查詢
  • 排序

1.基本查詢

基本語法

GET/索引庫名/_search
{
"query":{
"查詢型別":{
"查詢條件":"查詢條件值"
}
}
}
  • query:表示一個查詢物件,裡面可以有不同的查詢屬性

  • 查詢型別:如,mastch_allmatchtermrange

  • 查詢條件:

  • 查詢條件會根據型別的不同,寫法也有差異,後面詳細講解

2.查詢所有(match_all)

GET/ceshi/_search
{
"query":{
"match_all":{}
}
}
  • query:代表查詢物件
  • match_all:代表查詢所有
  • took:查詢花費時間,單位是毫秒
  • time_out:是否超時
  • _shards:分片資訊
  • hits:搜尋結果總覽物件
    • total:搜尋到的總條數
    • max_score:所有結果中文件得分的最高分
    • hits:搜尋結果的文件物件陣列,每個元素是一條搜尋到的文件資訊
      • _index:索引庫
      • _type:文件型別
      • _id:文件id
      • _score:文件得分
      • _source:文件的源資料

3.匹配查詢(match)

先增加一條資料,便於測試

PUT/ceshi/_doc/1
{
"title":"小米電視4A",
"images":"http://image.leyou.com/12479122.jpg",
"price":1899.00
}

特別說明:增加資料使用POSTPUT的區別

  • PUT:需要指定id,否則會報錯,冪等操作
  • POST:指定的id存在,則更新資料,不存在要麼自定義id,要麼隨機生成,非冪等操作

從結果中看到,索引庫中有2部手機,1臺電視

  • or關係

match型別查詢,會把查詢條件進行分詞。然後進行查詢,多個詞條之間是or的關係

GET/ceshi/_search
{
"query":{
"match":{
"title":"小米電視"
}
}
}

預設情況下,是會通過分詞,使多個詞之間是or的關係。

  • and關係

某些時候需要精確查詢,需要將多個詞關係設定為and

GET/ceshi/_search
{
"query":{
"match":{
"title":{
"query":"小米電視",
"operator":"and"
}
}
}
}

本例中,只有同時包含小米電視的詞條才會被搜尋到

  • or and and 之間

場景:如果使用者給定的條件分詞後有5個查詢詞項,想查詢只包含其中4個詞的文件,該如何處理?將operator操作符設定成and只會將此文件排除。

有時候這正是我們期望的,但在全文搜尋的大多數應用場景下,我們既想包含那些可能相關的文件,同時又排除那些不太相關的。換句話說,我們想要處於中間某種結果。

match查詢支援minimum_should_match最小匹配引數,這讓我們可以指定匹配的詞項數用來表示一個文件是否相關。我們可以將其設定為某個具體數字,更常用的做法是將其設定為一個百分數,因為我們無法控制使用者搜尋時輸入的單詞數量。

示例:

GET/ceshi/_search
{
"query":{
"match":{
"title":{
"query":"小米曲面電視",
"minimum_should_match":"75%"
}
}
}
}
  • 多欄位查詢(multi_match)

matchmulti_match類似,不同的是multi_match可以在多個欄位中查詢

示例:

GET/ceshi/_search
{
"query":{
"multi_match":{
"query":"小",
"fields":["title","subTitle"]
}
}
}
  • 詞條匹配(term)

term查詢被用於精確值匹配,這些精確值可能是數字、時間、布林或者那些未分詞的字串

示例:

GET/ceshi/_search
{
"query":{
"term":{
"price":"1899"
}
}
}
  • 多詞條精確匹配(terms)

terms查詢和term查詢一樣,但它允許你指定多值進行匹配。如果這個欄位中包含了指定值中的任何一個值,那麼這個文件滿足條件:

  • 結果過濾

預設情況下,elasticsearch在搜尋結果中,會把文件中儲存在_source的所有欄位返回。但是,如果我們只想要獲取其中的部分欄位,我們可以新增_source的過濾。

  1. 直接指定欄位

    示例:

    GET/ceshi/_search
    {
    "_source":["title","price"],
    "query":{
    "term":{
    "price":1899
    }
    }
    }

    2. 指定includes和excludes

    我們也可以通過“

    • includes:來指定想要顯示的欄位
    • excludes:來指定不想要顯示的欄位

    示例:

    GET/ceshi/_search
    {
    "_source":{
    "includes":["title","images"]
    },
    "query":{
    "term":{
    "price":1899
    }
    }
    }

    GET/ceshi/_search
    {
    "_source":{
    "excludes":["title","images"]
    },
    "query":{
    "term":{
    "price":1899
    }
    }
    }
2.18 高階查詢

1.布林組合(bool)

bool把各種其他查詢通過must(與)、must_not(非)、shoud(或)的當時組合

示例

#查詢title可能包含“大米”,但一定包含“手機”的資料
GET/ceshi/_search
{
"query":{
"bool":{
"must":{"match":{"title":"大米"}},
"must_not":{"match":{"title":"電視"}},
"should":{"match":{"title":"手機"}}
}
}
}

2.範圍查詢(range)

range查詢找出那些落在指定區間內的數字或時間

示例

#查詢price在1000-2900範圍內的資料
GET/ceshi/_search
{
"query":{
"range":{
"price":{
"gte":1000,
"lte":2900
}
}
}
}

range查詢允許以下字元:

操作符 說明
gt 大於
gte 大於等於
lt 小於
lte 小於等於

3.模糊查詢(fuzzy)

新增一條資料

POST/ceshi/_doc/4
{
"title":"apple手機",
"images":"http://image.leyou.com/12479122.jpg",
"price":6899.00
}

fuzzy查詢時term查詢的模糊等價。它允許使用者搜尋詞條與實際詞條的拼寫出現偏差,但是偏差的編輯距離不得超過2

POST/ceshi/_doc/4
{
"title":"apple手機",
"images":"http://image.leyou.com/12479122.jpg",
"price":6899.00
}

上面查詢也是可以查到apple手機資料的

我們可以通過fuzziness屬性來指定允許的偏差距離:

GET/ceshi/_search
{
"query":{
"fuzzy":{
"title":{
"value":"appaa",
"fuzziness":3
}
}
}
}

也是可以查到資料

注意:fuzzinexx值越大,偏差距離也越大,模糊查詢的範圍也越大,反之。

4.過濾(filter)

條件查詢找那個進行過濾

所有的查詢都會影響到文件的評分及排名。如果我們需要在查詢結果中進行過濾,並且不希望過濾條件影響評分,那麼就不要吧過濾條件作為查詢條件來用。而是使用filter方式:

GET/ceshi/_search
{
"query":{
"bool":{
"must":{"match":{"title":"手機"}},
"filter":{
"range":{
"price":{
"gte":1000,
"lte":5000
}
}
}
}
}
}

注意:filet中還可以再次進行bool組合條件過濾

無查詢條件,直接過濾

如果一個查詢只有過濾,沒有查詢條件,不希望進行評分,我們可以使用constant_score取代只有filter語句的bool查詢。在效能上時完全相同的,但對於提高查詢簡潔性和清晰度有很大幫助。

示例:

GET/ceshi/_search
{
"query":{
"constant_score":{
"filter":{
"range":{
"price":{
"gte":1000,
"lte":4000
}
}
}
}
}
}

5.排序

需求:想要將查詢條件title和price範圍過濾出來結果,進行首先按照價格排序,然後按照得分排序:

GET/ceshi/_search
{
"query":{
"bool":{
"must":{"match":{"title":"手機"}},
"filter":{
"range":{
"price":{
"gte":1000,
"lte":7000
}
}
}
}
},
"sort":[
{"price":{"order":"desc"}},
{"_score":{"order":"desc"}}
]
}

2.3聚合aggregations

聚合可以讓我們及其方便的實現對資料的統計、分析。例如:

  • 什麼品牌的手機最受歡迎
  • 這些手機的平均價格、最高價格、最低價格
  • 這些手機每月的銷售情況如何

實現這些統計功能要比資料庫的sql方便的多,而且查詢速度非常快,可以實現實時搜尋效果。

2.3.1 基本概念

Elasticsearch中的聚合,包含多種型別,最常用的兩種:

  • 度量
1.桶(bucket)

桶的作用,是按照某種方式對資料進行分組,每一組資料在ES中稱為一個桶。例如,我們根據國籍對人劃分,可以得到中國桶、英國桶、美國桶。。。

Elasticsearch中提供的劃分桶的方式很多:

  • Date Histogram Aggregation:根據日期階梯分組,例如給定階梯為周,會自動每週分一組
  • Histogram Aggregation:根據數值階梯分組,與日期類似
  • Terms Aggregation:根據詞條內容分組,詞條內容完全匹配分為一組
  • Range Aggregation:數值和日期的範圍分組,指定開始和結束,然後分段分組
  • 。。。

綜上所述,我們發現bucket aggregations只負責對資料進行分組,並不進行計算,因此bucket中往往會巢狀另一種聚合:metrics aggregations 即度量

2.度量(metrics)

分組完成以後,我們一般會對組中的資料進行聚合運算,例如求平均值、最大、最小、求和等操作。這些在ES中稱為度量

比較常用的一些度量聚合方式:

  • Avg Aggregation:求平均值
  • Max Aggregation:求最大值
  • MIn Aggregation:求最小值
  • Percentiles Aggregation:求百分比
  • Stats Aggregation:同時返回avg、max、min、sum、count等
  • Sum Aggregation:求和
  • Top hits Aggregation:求前幾
  • Value Count Aggregation:求總數
  • 。。。

開始測試

為了方便測試,我們首先批量匯入測試資料

2.3.2 建立索引庫
PUT/cars
{
"settings":{
"number_of_shards":1,
"number_of_replicas":0
},
"mappings":{
"properties":{
"color":{"type":"keyword"},
"make":{"type":"keyword"}
}
}
}

檢視索引庫對映關係:GET /cars/_mapping

注意:在ES中,需要進行聚合、排序、過濾的欄位其處理方式比較特殊,因此不能被分詞。這裡我們將color和make這兩個欄位型別設定為keyword型別,這個型別不會被分詞,將來就可以參與聚合

匯入資料:

POST/cars/_bulk
{"index":{}}
{"price":10000,"color":"red","make":"honda","sold":"2014-10-28"}
{"index":{}}
{"price":20000,"color":"red","make":"honda","sold":"2014-11-05"}
{"index":{}}
{"price":30000,"color":"green","make":"ford","sold":"2014-05-18"}
{"index":{}}
{"price":15000,"color":"blue","make":"toyota","sold":"2014-07-02"}
{"index":{}}
{"price":12000,"color":"green","make":"toyota","sold":"2014-08-19"}
{"index":{}}
{"price":20000,"color":"red","make":"honda","sold":"2014-11-05"}
{"index":{}}
{"price":80000,"color":"red","make":"bmw","sold":"2014-01-01"}
{"index":{}}
{"price":25000,"color":"blue","make":"ford","sold":"2014-02-12"}

檢視cars索引庫中的資料:

GET/cars/_search
{
"query":{
"match_all":{}
}
}
2.3.3 聚合為桶
1.按照cars中的color欄位來劃分桶
GET/cars/_search
{
"size":0,
"aggs":{
"popular_colors":{
"terms":{
"field":"color"
}
}
}
}
  • size:查詢條數,這裡設定為0,因為我們不關心搜尋到的資料,只關心聚合結果,提高效率
  • aggs:宣告這是一個聚合查詢,是aggregations的縮寫
    • popular_color:給這次聚合起一個名字,任意。
      • terms:劃分桶的方式,這裡是根據詞條劃分
        • field:劃分桶的欄位
  • hits:查詢結果為空,因為我們設定了size為0
  • aggregations:聚合的結果
    • popular_clor:我們定義的聚合名稱
      • buckets:查詢到的桶,每個不同的color欄位值都會形成一個桶
        • key:這個桶對應的color欄位的值
        • doc_count:這個桶中的文件數量

總結:通過聚合的結果我們發現,目前紅色的小車比較暢銷

2.3.4 桶內度量

前面的例子告訴我們每個桶裡面的文件數量。但通常,我們的應用需要提供更為複雜的文件度量。例如,每種顏色騎車的平均價格是多少?

因此,我們需要告訴Elasticsearch使用哪個欄位,使用何種度量方式進行運算,這些資訊要巢狀在==桶內==,度量的運算會基於==桶內==的文件進行。

示例:

需求:按照cars中的color欄位劃分桶,並求相應每個桶中的平均價格

GET/cars/_search
{
"size":0,
"aggs":{
"popular_color":{
"terms":{
"field":"color"
},
"aggs":{
"avg_price":{
"avg":{
"field":"price"
}
}
}
}
}
}
  • arrgs:我們在aggs(popular_color)中新增新的aggs。可見==度量也是一個聚合,度量是在桶中的聚合==。
  • avg_price:度量聚合的名稱,任意
  • avg:度量的型別,這裡是求平均值
  • field:度量運算的欄位

結果:

我們可以看到每個桶中都有自己的avg_price欄位,這就是度量聚合的結果

2.3.5 桶內巢狀桶

上面示例是桶內巢狀度量運算。事實上桶內不僅可以巢狀運算,還可以巢狀其他桶。也就是說在每個分組中,可以再分更多桶。

示例:

需求:我們想要統計每種顏色的汽車中,分別屬於哪個製造商,按照make欄位在進行分桶

GET/cars/_search
{
"size":0,
"aggs":{
"popular_color":{
"terms":{
"field":"color"
},
"aggs":{
"avg_price":{
"avg":{
"field":"price"
}
},
"maker":{
"terms":{
"field":"make"
}
}
}
}
}
}

結果:

{
"took":14,
"timed_out":false,
"_shards":{
"total":1,
"successful":1,
"skipped":0,
"failed":0
},
"hits":{
"total":{
"value":8,
"relation":"eq"
},
"max_score":null,
"hits":[]
},
"aggregations":{
"popular_color":{
"doc_count_error_upper_bound":0,
"sum_other_doc_count":0,
"buckets":[
{
"key":"red",
"doc_count":4,
"maker":{
"doc_count_error_upper_bound":0,
"sum_other_doc_count":0,
"buckets":[
{
"key":"honda",
"doc_count":3
},
{
"key":"bmw",
"doc_count":1
}
]
},
"avg_price":{
"value":32500.0
}
},
{
"key":"blue",
"doc_count":2,
"maker":{
"doc_count_error_upper_bound":0,
"sum_other_doc_count":0,
"buckets":[
{
"key":"ford",
"doc_count":1
},
{
"key":"toyota",
"doc_count":1
}
]
},
"avg_price":{
"value":20000.0
}
},
{
"key":"green",
"doc_count":2,
"maker":{
"doc_count_error_upper_bound":0,
"sum_other_doc_count":0,
"buckets":[
{
"key":"ford",
"doc_count":1
},
{
"key":"toyota",
"doc_count":1
}
]
},
"avg_price":{
"value":21000.0
}
}
]
}
}
}
  • 我們可以看到,新的聚合maker被巢狀在原來每一個color的桶中。
  • 每個顏色下面都根據make欄位進行了分組
  • 我們從結果中讀到的資訊:
    • 紅色車共有4輛
    • 紅色車的平均售價32500
    • 其中3輛是Honda本田製造,1輛是BMW寶馬製造
2.3.6 階梯分桶(Histogram)

histogram是把數值型別的欄位,按照一定的階梯大小進行分組。需要指定一個階梯值(interval)來劃分階梯大小

示例

需求:比如你有價格欄位,如果你設定interval的值為200.那麼階梯就會是這樣的:

0,200,400,600,。。。

上面列出的是每個階梯的key,也是區間的起點

如果一件商品的價格是450,會落在哪個階梯區間呢?計算公式如下:

bucket_key = Math.floor((value-offset)/interval)*interval+offset

  • value:就是當前資料的值,本例中是450
  • offset:起始偏移值,預設為0
  • interval:階梯間隔,比如200
  • 因此得到的key=Math.floor((450-0)/200)*200+0=400

我們對汽車的價格進行分組,指定間隔interval為5000:

#階梯分桶,對汽車的價格分組,指定間隔interval為5000,並約束桶內的文件最小值為1
GET/cars/_search
{
"size":0,
"aggs":{
"price":{
"histogram":{
"field":"price",
"interval":5000,
"min_doc_count":1
}
}
}
}
2.3.7 範圍分桶(range)

範圍分桶和階梯分桶類似,也是把數字按照階段進行分組,只不過range方式需要你自己指定每一組的起始和結束大小

2.4 Spring Data Elaticsearch

Elasticsearch提供的java客戶端有一些不太方便的地方:

  • 很多地方需要在java中拼接json字串
  • 需要自己把物件序列化為json儲存
  • 查詢到結果也需要自己反序列化為物件

因此,我們可以學習Spring提供的套件:Spring Data Elaticsearch

2.4.1 簡介

Spring Data Elaticsearch是Spring Data專案下的一個子模組

Spring Data官網:http://projects.spring.io/spring-data/

Spring Data的使命是給各種資料訪問提供統一的程式設計介面,不管是關係型資料庫(mysql),還是非關係型資料庫(redis),或者類似Elaticsearch索引資料庫。

Spring Data Elaticsearch的頁面:https://projects.spring.io/spring-data-elasticsearch/

特徵:

  • 支援Spring的基於@configuration的java配置方式,或者XML配置方式
  • 提供了用於操作ES的便捷工具類ElaticsearchTemplate。包括實現文件到POJO之間的自動智慧對映
  • 利用Spring的資料轉換服務實現的功能豐富的物件對映
  • 基於註解的元資料對映方式,而且可擴充套件以支援更多不同的資料格式
  • 根據持久層介面自動生成物件實現方法,無需人工編寫基本操作程式碼(類似mybatis,根據介面自動得到實現,也支援人工定製查詢)
2.4.2 專案實戰

1.建立一個專案,匯入如下pom依賴:

<!--highclient-->
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>${es.version}</version>
</dependency>

<!--rest-high-level-client依賴如下兩個jar-->
<dependency>
<groupId>org.elasticsearch</groupId>
<artifactId>elasticsearch</artifactId>
<version>${es.version}</version>
</dependency>

<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-client</artifactId>
<version>${es.version}</version>
</dependency>

2.application.yml配置

es:
host:192.168.15.100
port:9200
scheme:http

3.es配置類

@Configuration
publicclassElaticsearchConfig{

@Value("${es.host}")
publicStringhost;
@Value("${es.port}")
publicintport;
@Value("${es.scheme}")
publicStringscheme;

@Bean(destroyMethod="close")
publicRestHighLevelClientrestHighLevelClient(){
returnnewRestHighLevelClient(RestClient.builder(
newHttpHost(host,port,scheme)));
}
}

4.單元測試

  • 建立索引庫
@Test
publicvoidcreateIndexTest()throwsIOException{
CreateIndexRequestindexRequest=newCreateIndexRequest(index);
CreateIndexResponseresponse=client.indices().create(indexRequest,RequestOptions.DEFAULT);
System.out.println(response.isAcknowledged());
}
  • 判斷索引庫是否存在
@Test
publicvoidindexExistsTest()throwsIOException{
GetIndexRequestrequest=newGetIndexRequest(index);
booleanexists=client.indices().exists(request,RequestOptions.DEFAULT);
System.out.println(exists);
}
  • 新增文件
@Test
publicvoidaddDocTest()throwsIOException{
IndexRequestrequest=newIndexRequest(index);
Stringsource=JSONObject.toJSONString(newUsers(1000,"瑟曦",30));
request.source(source,XContentType.JSON);
IndexResponseresponse=client.index(request,RequestOptions.DEFAULT);
System.out.println(response.getResult());//CREATED
}
  • 批量新增文件
@Test
publicvoidbatchAddDocTest()throwsIOException{
BulkRequestbulkRequest=newBulkRequest();
List<IndexRequest>indexRequests=generateRequests();
indexRequests.forEach(x->{
bulkRequest.add(x);
});
BulkResponseresponse=client.bulk(bulkRequest,RequestOptions.DEFAULT);
System.out.println(response.hasFailures());
}

publicList<IndexRequest>generateRequests(){
List<IndexRequest>requests=newArrayList<>();
requests.add(generateNewRequests(newUsers(1,"雪諾",25)));
requests.add(generateNewRequests(newUsers(2,"艾麗婭",20)));
requests.add(generateNewRequests(newUsers(3,"珊莎",23)));
returnrequests;
}

publicIndexRequestgenerateNewRequests(Usersusers){
IndexRequestindexRequest=newIndexRequest(index);
indexRequest.source(JSONObject.toJSONString(users),XContentType.JSON);
returnindexRequest;
}
  • 根據條件搜尋文件
@Test
publicvoidserachTest()throwsIOException{
SearchRequestrequest=newSearchRequest(index);
SearchSourceBuilderbuilder=newSearchSourceBuilder();
BoolQueryBuilderboolQueryBuilder=newBoolQueryBuilder();
boolQueryBuilder.must(newRangeQueryBuilder("age").from(20).to(30))
.mustNot(newTermQueryBuilder("id",1000));
builder.query(boolQueryBuilder);
request.source(builder);
System.out.println("搜尋語句為:"+request.source().toString());
SearchResponseresponse=client.search(request,RequestOptions.DEFAULT);
System.out.println("搜尋結果:"+response);
SearchHitshits=response.getHits();
SearchHit[]hitsArr=hits.getHits();
for(SearchHitdocumentFields:hitsArr){
System.out.println(documentFields.getSourceAsString());
}
}
  • 修改文件
@Test
publicvoidmodifyDocTest()throwsIOException{
UpdateRequestrequest=newUpdateRequest(index,"nqHRknUBJcoqc7s-i-Az");
Map<String,Object>params=newHashMap<>();
params.put("id",4);
request.doc(params);
UpdateResponseresponse=client.update(request,RequestOptions.DEFAULT);
System.out.println(response.getResult());
}
  • 刪除指定ID的文件
@Test
publicvoiddeleteDocTest()throwsIOException{
DeleteRequestrequest=newDeleteRequest(index,"nqHRknUBJcoqc7s-i-Az");
DeleteResponseresponse=client.delete(request,RequestOptions.DEFAULT);
System.out.println(response.getResult());
}
  • 刪除索引庫
@Test
publicvoiddeleteIndex()throwsIOException{
DeleteIndexRequestrequest=newDeleteIndexRequest(index);
AcknowledgedResponseresponse=client.indices().delete(request,RequestOptions.DEFAULT);
System.out.println(response.isAcknowledged());

}

完結

與springboot整合的話也可直接使用ElasticsearchRestTemplate,也是基於RestHighLevelClient的模板封裝,後續有需要可以研究下。