全文檢索Elasticsearch研究
全文檢索Elasticsearch研究
基於虛擬機器伺服器自主部署ELK服務
學習目標
瞭解Elasticsearch的應用場景 學習基於伺服器部署ELK服務 掌握索引維護的方法 掌握索引維護的方法 掌握基本的搜尋API的使用方法
約束
需要提前掌握Lucene的索引方法、搜尋方法
ELK的介紹和安裝
1.簡介
Elasticsearch是一個基於Lucene的搜尋伺服器,它提供了一個分散式多使用者能力的全文搜尋引擎,基於Restful web介面。Elasticsearch是用java開發的,是當前流行的企業級搜尋引擎。能到達到實時搜尋,穩定、可靠、快速、安裝使用方便。
我們建立一個網站或應用程式,並要新增搜尋功能,如果搜尋的數量非常多,而且分類繁雜,如果使用傳統的資料庫想要完成搜尋工作的建立失非常困難的。我們希望搜尋解決方案要執行速度快,我們希望有一個零配置和完全免費的搜尋模式,能夠簡單的使用JSON通過HTTP來索引資料,而搜尋伺服器始終可用,並且伺服器可以自如擴充套件,我們一般都會使用全文檢索技術,如solr、Elasticsearch等。
2.突出優點
擴充套件性好,可部署上百臺伺服器叢集,處理PB級資料 近實時的去索引資料、搜尋資料
3.原理與應用
3.1 索引結構
下圖是ElasticSearch的索引結構,下邊==黑色部分是物理結構==,上邊==黃色部分是邏輯結構==,邏輯結構可以更好的描述ElasticSearch的工作原理及去使用物理結構中的索引檔案。
邏輯結構部分是一個倒排索引表:
將要搜尋的文件內容分詞,所有不重複的詞做成分詞列表。 將搜尋的文件最終以Document方式儲存起來。 每個詞和document都有關聯。
3.2 RESTFUL應用方法
ElasticSearch提供RESTFUL Api介面進行索引、搜尋、並且支援多種客戶端。
下圖是ElasticSearch在專案中的應用方式:
使用者在前端搜尋關鍵字 專案前端通過http方式請求專案服務端 專案服務端通過http RESTful方式請求ES叢集進行搜尋 ES叢集從索引庫檢索資料
4.ElasticaSearc安裝
4.1 安裝配置
安裝ElasticaSearc7.9.0 該版本要求至少jdk1.8以上 解壓elasticsearch-7.9.0-linux-x86_64.tar.gz bin:指令碼目錄,包括:啟動、停止等可執行指令碼 config:配置檔案目錄 data:索引目錄,存放索引檔案的地方 modules:模板目錄,包括了es的功能模組 plugins:外掛目錄,es支援外掛機制
4.2 配置檔案
ES配置檔案的地址根據安裝形式的不同而不同:
使用zip、tar安裝,配置檔案的地址在安裝目錄的config下 使用RPM安裝,配置檔案在/etc/elasticsearch下 使用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.yml
和jvm.options
jvm.options
預設配置:
-Xms1g
-Xmx1g
記憶體佔用太多,設定為不超過實體記憶體的一半:
-Xms512m
-Xmx512m
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 索引庫設定
建立索引索引庫設定:
請求方式:PUT
請求路徑:/索引名
請求引數:json格式:
{
"settings":{
"number_of_shards":3,
"number_of_replicas":2
}
}settings:索引庫的設定
number_of_shards:分片數量
number_of_replicas:副本數量
測試
使用postman進行建立索引並對索引庫進行設定測試
2. 使用kibana進行建立索引並對索引庫進行設定測試
檢視索引庫設定
語法:
GETceshi
或者,使用*來查詢所有索引配置:
刪除索引庫設定
語法:
DELETE/索引庫名
再次檢視
2.1.2 對映配置
索引庫建立好之後就是新增資料,再新增資料之前必須定義對映
對映:
定義文件的過程,文件包含哪些欄位,這些欄位是否儲存、是否索引、是否分詞等
建立對映欄位
語法:
請求方式是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
}
檢視對映關係
語法:
GET索引庫名/_mapping
示例:
GETceshi/_mapping
結果:
欄位屬性詳解
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
來選擇哪些要顯示,哪些不顯示。
而如果設定store
為true
,就會在_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_all
、match
、term
、range
等查詢條件:
查詢條件會根據型別的不同,寫法也有差異,後面詳細講解
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
}
特別說明:增加資料使用
POST
和PUT
的區別
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)
match
和multi_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
的過濾。
直接指定欄位
示例:
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的模板封裝,後續有需要可以研究下。