萬字長文帶你學習 ElasticSearch
ElasticStack 技術棧
如果你沒有聽說過 Elastic Stack,那你一定聽說過 ELK ,實際上 ELK 是三款軟件的簡稱,分別是 Elasticsearch、 Logstash、Kibana 組成,在發展的過程中,又有新成員 Beats 的加入,所以就形成了 Elastic Stack。所以說,ELK 是舊的稱呼,Elastic Stack 是新的名字。
從 ELK 到 ElasticStack
全系的 ElasticStack 技術棧包括:
ElasticStack 技術棧
Elasticsearch
Elasticsearch 基於 Java,是個開源分佈式搜索引擎,它的特點有:分佈式,零配置,自動發現,索引自動分片,索引副本機制,restful 風格接口,多數據源,自動搜索負載等。
Logstash
Logstash 基於 Java,是一個開源的用於收集, 分析和存儲日誌的工具。
Kibana
Kibana 基於 nodejs,也是一個開源和免費的工具,Kibana 可以爲 Logstash 和 ElasticSearch 提供的日誌分析友好的 Web 界面,可以彙總、分析和搜索重要數據日誌。
Beats
Beats 是 elastic 公司開源的一款採集系統監控數據的代理 agent,是在被監控服務器上以客戶端形式運行的數據收集器的統稱,可以直接把數據發送給 Elasticsearch 或者通過 Logstash 發送給 Elasticsearch,然後進行後續的數據分析活動。Beats 由如下組成:
-
Packetbeat:是一個網絡數據包分析器,用於監控、收集網絡流量信息,Packetbeat 嗅探服務器之間的流量,解析應用層協議,並關聯到消息的處理,其支 持 ICMP (v4 and v6)、DNS、HTTP、Mysql、PostgreSQL、Redis、MongoDB、Memcache 等協議;
-
Filebeat:用於監控、收集服務器日誌文件,其已取代 logstash forwarder;
-
Metricbeat:可定期獲取外部系統的監控指標信息,其可以監控、收集 Apache、HAProxy、MongoDB MySQL、Nginx、PostgreSQL、Redis、System、Zookeeper 等服務;
Beats 和 Logstash 其實都可以進行數據的採集,但是目前主流的是使用 Beats 進行數據採集,然後使用 Logstash 進行數據的分割處理等,早期沒有 Beats 的時候,使用的就是 Logstash 進行數據的採集。
ElasticSearch 快速入門
簡介
官網:https://www.elastic.co/
ElasticSearch 是一個基於 Lucene 的搜索服務器。它提供了一個分佈式多用戶能力的全文搜索引擎,基於 RESTful Web 接口。Elasticsearch 是用 Java 開發的,並作爲 Apache 許可條款下的開放源碼發佈,是當前流行的企業級搜索引擎。設計用於雲計算中,能夠達到實時搜索,穩定,可靠,快速,安裝使用方便。
我們建立一個網站或應用程序,並要添加搜索功能,但是想要完成搜索工作的創建是非常困難的。我們希望搜索解決方案要運行速度快,我們希望能有一個零配置和一個完全免費的搜索模式,我們希望能夠簡單地使用 JSON 通過 HTTP 來索引數據,我們希望我們的搜索服務器始終可用,我們希望能夠從一臺開始並擴展到數百臺,我們要實時搜索,我們要簡單的多租戶,我們希望建立一個雲的解決方案。因此我們利用 Elasticsearch 來解決所有這些問題及可能出現的更多其它問題。
ElasticSearch 是 Elastic Stack 的核心,同時 Elasticsearch 是一個分佈式、RESTful 風格的搜索和數據分析引擎,能夠解決不斷湧現出的各種用例。作爲 Elastic Stack 的核心,它集中存儲您的數據,幫助您發現意料之中以及意料之外的情況。
Elasticsearch 的發展是非常快速的,所以在 ES5.0 之前,ELK 的各個版本都不統一,出現了版本號混亂的狀態,所以從 5.0 開始,所有 Elastic Stack 中的項目全部統一版本號。本篇將基於 6.5.4 版本進行學習。
下載
到官網下載:https://www.elastic.co/cn/downloads/
下載
選擇對應版本的數據,這裏我使用的是 Linux 來進行安裝,所以就先下載好 ElasticSearch 的 Linux 安裝包
拉取 Docker 容器
因爲我們需要部署在 Linux 下,爲了以後遷移 ElasticStack 環境方便,我們就使用 Docker 來進行部署,首先我們拉取一個帶有 ssh 的 Centos 鏡像
# 拉取鏡像
docker pull moxi/centos_ssh
# 製作容器
docker run --privileged -d -it -h ElasticStack --name ElasticStack -p 11122:22 -p 9200:9200 -p 5601:5601 -p 9300:9300 -v /etc/localtime:/etc/localtime:ro moxi/centos_ssh /usr/sbin/init
然後直接遠程連接 11122 端口即可
單機版安裝
因爲 ElasticSearch 不支持 root 用戶直接操作,因此我們需要創建一個 elsearch 用戶
# 添加新用戶
useradd elsearch
# 創建一個soft目錄,存放下載的軟件
mkdir /soft
# 進入,然後通過xftp工具,將剛剛下載的文件拖動到該目錄下
cd /soft
# 解壓縮
tar -zxvf elasticsearch-7.9.1-linux-x86_64.tar.gz
#重命名
mv elasticsearch-7.9.1/ elsearch
因爲剛剛我們是使用 root 用戶操作的,所以我們還需要更改一下 /soft 文件夾的所屬,改爲 elsearch 用戶
chown elsearch:elsearch /soft/ -R
然後在切換成 elsearch 用戶進行操作
# 切換用戶
su - elsearch
然後我們就可以對我們的配置文件進行修改了
# 進入到 elsearch下的config目錄
cd /soft/elsearch/config
然後找到下面的配置
#打開配置文件
vim elasticsearch.yml
#設置ip地址,任意網絡均可訪問
network.host: 0.0.0.0
在 Elasticsearch 中如果 network.host 不是 localhost 或者 127.0.0.1 的話,就會認爲是生產環境,而生產環境的配置要求比較高,我們的測試環境不一定能夠滿足,一般情況下需要修改兩處配置,如下:
# 修改jvm啓動參數
vim conf/jvm.options
#根據自己機器情況修改
-Xms128m
-Xmx128m
然後在修改第二處的配置,這個配置要求我們到宿主機器上來進行配置
# 到宿主機上打開文件
vim /etc/sysctl.conf
# 增加這樣一條配置,一個進程在VMAs(虛擬內存區域)創建內存映射最大數量
vm.max_map_count=655360
# 讓配置生效
sysctl -p
啓動 ElasticSearch
首先我們需要切換到 elsearch 用戶
su - elsearch
然後在到 bin 目錄下,執行下面
# 進入bin目錄
cd /soft/elsearch/bin
# 後臺啓動
./elasticsearch -d
啓動成功後,訪問下面的 URL
http://202.193.56.222:9200/
如果出現了下面的信息,就表示已經成功啓動了
ELastic 啓動成功
如果你在啓動的時候,遇到過問題,那麼請參考下面的錯誤分析~
錯誤分析
錯誤情況 1
如果出現下面的錯誤信息
java.lang.RuntimeException: can not run elasticsearch as root
at org.elasticsearch.bootstrap.Bootstrap.initializeNatives(Bootstrap.java:111)
at org.elasticsearch.bootstrap.Bootstrap.setup(Bootstrap.java:178)
at org.elasticsearch.bootstrap.Bootstrap.init(Bootstrap.java:393)
at org.elasticsearch.bootstrap.Elasticsearch.init(Elasticsearch.java:170)
at org.elasticsearch.bootstrap.Elasticsearch.execute(Elasticsearch.java:161)
at org.elasticsearch.cli.EnvironmentAwareCommand.execute(EnvironmentAwareCommand.java:86)
at org.elasticsearch.cli.Command.mainWithoutErrorHandling(Command.java:127)
at org.elasticsearch.cli.Command.main(Command.java:90)
at org.elasticsearch.bootstrap.Elasticsearch.main(Elasticsearch.java:126)
at org.elasticsearch.bootstrap.Elasticsearch.main(Elasticsearch.java:92)
For complete error details, refer to the log at /soft/elsearch/logs/elasticsearch.log
[root@e588039bc613 bin]# 2020-09-22 02:59:39,537121 UTC [536] ERROR CLogger.cc@310 Cannot log to named pipe /tmp/elasticsearch-5834501324803693929/controller_log_381 as it could not be opened for writing
2020-09-22 02:59:39,537263 UTC [536] INFO Main.cc@103 Parent process died - ML controller exiting
就說明你沒有切換成 elsearch 用戶,因爲不能使用 root 用戶去操作 ElasticSearch
su - elsearch
錯誤情況 2
[1]:max file descriptors [4096] for elasticsearch process is too low, increase to at least[65536]
解決方法:切換到 root 用戶,編輯 limits.conf 添加如下內容
vi /etc/security/limits.conf
# ElasticSearch添加如下內容:
* soft nofile 65536
* hard nofile 131072
* soft nproc 2048
* hard nproc 4096
錯誤情況 3
[2]: max number of threads [1024] for user [elsearch] is too low, increase to at least
[4096]
也就是最大線程數設置的太低了,需要改成 4096
#解決:切換到root用戶,進入limits.d目錄下修改配置文件。
vi /etc/security/limits.d/90-nproc.conf
#修改如下內容:
* soft nproc 1024
#修改爲
* soft nproc 4096
錯誤情況 4
[3]: system call filters failed to install; check the logs and fix your configuration
or disable system call filters at your own risk
解決:Centos6 不支持 SecComp,而 ES5.2.0 默認 bootstrap.system_call_filter 爲 true
vim config/elasticsearch.yml
# 添加
bootstrap.system_call_filter: false
bootstrap.memory_lock: false
錯誤情況 5
[elsearch@e588039bc613 bin]$ Exception in thread "main" org.elasticsearch.bootstrap.BootstrapException: java.nio.file.AccessDeniedException: /soft/elsearch/config/elasticsearch.keystore
Likely root cause: java.nio.file.AccessDeniedException: /soft/elsearch/config/elasticsearch.keystore
at java.base/sun.nio.fs.UnixException.translateToIOException(UnixException.java:90)
at java.base/sun.nio.fs.UnixException.rethrowAsIOException(UnixException.java:111)
at java.base/sun.nio.fs.UnixException.rethrowAsIOException(UnixException.java:116)
at java.base/sun.nio.fs.UnixFileSystemProvider.newByteChannel(UnixFileSystemProvider.java:219)
at java.base/java.nio.file.Files.newByteChannel(Files.java:375)
at java.base/java.nio.file.Files.newByteChannel(Files.java:426)
at org.apache.lucene.store.SimpleFSDirectory.openInput(SimpleFSDirectory.java:79)
at org.elasticsearch.common.settings.KeyStoreWrapper.load(KeyStoreWrapper.java:220)
at org.elasticsearch.bootstrap.Bootstrap.loadSecureSettings(Bootstrap.java:240)
at org.elasticsearch.bootstrap.Bootstrap.init(Bootstrap.java:349)
at org.elasticsearch.bootstrap.Elasticsearch.init(Elasticsearch.java:170)
at org.elasticsearch.bootstrap.Elasticsearch.execute(Elasticsearch.java:161)
at org.elasticsearch.cli.EnvironmentAwareCommand.execute(EnvironmentAwareCommand.java:86)
at org.elasticsearch.cli.Command.mainWithoutErrorHandling(Command.java:127)
at org.elasticsearch.cli.Command.main(Command.java:90)
at org.elasticsearch.bootstrap.Elasticsearch.main(Elasticsearch.java:126)
at org.elasticsearch.bootstrap.Elasticsearch.main(Elasticsearch.java:92)
我們通過排查,發現是因爲 /soft/elsearch/config/elasticsearch.keystore 存在問題
也就是說該文件還是所屬於 root 用戶,而我們使用 elsearch 用戶無法操作,所以需要把它變成 elsearch
chown elsearch:elsearch elasticsearch.keystore
錯誤情況 6
[1]: the default discovery settings are unsuitable for production use; at least one of [discovery.seed_hosts, discovery.seed_providers, cluster.initial_master_nodes] must be configured
ERROR: Elasticsearch did not exit normally - check the logs at /soft/elsearch/logs/elasticsearch.log
繼續修改配置 elasticsearch.yaml
# 取消註釋,並保留一個節點
node.name: node-1
cluster.initial_master_nodes: ["node-1"]
ElasticSearchHead 可視化工具
由於 ES 官方沒有給 ES 提供可視化管理工具,僅僅是提供了後臺的服務,elasticsearch-head 是一個爲 ES 開發的一個頁面客戶端工具,其源碼託管於 Github
Github 地址:https://github.com/mobz/elasticsearch-head
head 提供了以下安裝方式
-
源碼安裝,通過 npm run start 啓動(不推薦)
-
通過 docker 安裝(推薦)
-
通過 chrome 插件安裝(推薦)
-
通過 ES 的 plugin 方式安裝(不推薦)
通過 Docker 方式安裝
#拉取鏡像
docker pull mobz/elasticsearch-head:5
#創建容器
docker create --name elasticsearch-head -p 9100:9100 mobz/elasticsearch-head:5
#啓動容器
docker start elasticsearch-head
通過瀏覽器進行訪問:
瀏覽器訪問
注意: 由於前後端分離開發,所以會存在跨域問題,需要在服務端做 CORS 的配置,如下:
vim elasticsearch.yml
http.cors.enabled: true http.cors.allow-origin: "*"
若通過 Chrome 插件的方式安裝不存在該問題
通過 Chrome 插件安裝
打開 Chrome 的應用商店,即可安裝 https://chrome.google.com/webstore/detail/elasticsearch-head/ffmkiejjmecolpfloofpjologoblkegm
Chrome 插件安裝
我們也可以新建索引
新建索引
推薦使用 Chrome 插件的方式安裝,如果網絡環境不允許,就採用其它方式安裝。
ElasticSearch 中的基本概念
索引
索引是 Elasticsearch 對邏輯數據的邏輯存儲,所以它可以分爲更小的部分。
可以把索引看成關係型數據庫的表,索引的結構是爲快速有效的全文索引準備的,特別是它不存儲原始值。
Elasticsearch 可以把索引存放在一臺機器或者分散在多臺服務器上,每個索引有一或多個分片(shard),每個分片可以有多個副本(replica)。
文檔
-
存儲在 Elasticsearch 中的主要實體叫文檔(document)。用關係型數據庫來類比的話,一個文檔相當於數據庫表中的一行記錄。
-
Elasticsearch 和 MongoDB 中的文檔類似,都可以有不同的結構,但 Elasticsearch 的文檔中,相同字段必須有相同類型。
-
文檔由多個字段組成,每個字段可能多次出現在一個文檔裏,這樣的字段叫多值字段(multivalued)。 每個字段的類型,可以是文本、數值、日期等。字段類型也可以是複雜類型,一個字段包含其他子文檔或者數 組。
映射
所有文檔寫進索引之前都會先進行分析,如何將輸入的文本分割爲詞條、哪些詞條又會被過濾,這種行爲叫做 映射(mapping)。一般由用戶自己定義規則。
文檔類型
-
在 Elasticsearch 中,一個索引對象可以存儲很多不同用途的對象。例如,一個博客應用程序可以保存文章和評論。
-
每個文檔可以有不同的結構。
-
不同的文檔類型不能爲相同的屬性設置不同的類型。例如,在同一索引中的所有文檔類型中,一個叫 title 的字段必須具有相同的類型。
RESTful API
在 Elasticsearch 中,提供了功能豐富的 RESTful API 的操作,包括基本的 CRUD、創建索引、刪除索引等操作。
創建非結構化索引
在 Lucene 中,創建索引是需要定義字段名稱以及字段的類型的,在 Elasticsearch 中提供了非結構化的索引,就是不需要創建索引結構,即可寫入數據到索引中,實際上在 Elasticsearch 底層會進行結構化操作,此操作對用戶是透明的。
創建空索引
PUT /haoke
{
"settings": {
"index": {
"number_of_shards": "2", #分片數
"number_of_replicas": "0" #副本數
}
}
}
刪除索引
#刪除索引
DELETE /haoke
{
"acknowledged": true
}
插入數據
URL 規則: POST /{索引}/{類型}/{id}
POST /haoke/user/1001
#數據
{
"id":1001,
"name":"張三",
"age":20,
"sex":"男"
}
使用 postman 操作成功後
操作成功
我們通過 ElasticSearchHead 進行數據預覽就能夠看到我們剛剛插入的數據了
ElasticSearchHead 插件瀏覽
說明:非結構化的索引,不需要事先創建,直接插入數據默認創建索引。不指定 id 插入數據:
自動生成 ID
更新數據
在 Elasticsearch 中,文檔數據是不能修改的,但是可以通過覆蓋的方式進行更新。
PUT /haoke/user/1001
{
"id":1001,
"name":"張三",
"age":21,
"sex":"女"
}
覆蓋成功後的結果如下:
更新數據
可以看到數據已經被覆蓋了。問題來了,可以局部更新嗎? -- 可以的。前面不是說,文檔數據不能更新嗎? 其實是這樣的:在內部,依然會查詢到這個文檔數據,然後進行覆蓋操作,步驟如下:
-
從舊文檔中檢索 JSON
-
修改它
-
刪除舊文檔
-
索引新文檔
#注意:這裏多了_update標識
POST /haoke/user/1001/_update
{
"doc":{
"age":23
}
}
更新操作
可以看到,數據已經是局部更新了
刪除索引
在 Elasticsearch 中,刪除文檔數據,只需要發起 DELETE 請求即可,不用額外的參數
DELETE 1 /haoke/user/1001
刪除索引
需要注意的是,result 表示已經刪除,version 也增加了。
如果刪除一條不存在的數據,會響應 404
刪除一個文檔也不會立即從磁盤上移除,它只是被標記成已刪除。Elasticsearch 將會在你之後添加更多索引的時候纔會在後臺進行刪除內容的清理。【相當於批量操作】
搜索數據
根據 id 搜索數據
GET /haoke/user/BbPe_WcB9cFOnF3uebvr
#返回的數據如下
{
"_index": "haoke",
"_type": "user",
"_id": "BbPe_WcB9cFOnF3uebvr",
"_version": 8,
"found": true,
"_source": { #原始數據在這裏
"id": 1002,
"name": "李四",
"age": 40,
"sex": "男"
}
}
搜索全部數據
GET 1 /haoke/user/_search
注意,使用查詢全部數據的時候,默認只會返回 10 條
關鍵字搜索數據
#查詢年齡等於20的用戶
GET /haoke/user/_search?q=age:20
結果如下:
DSL 搜索
Elasticsearch 提供豐富且靈活的查詢語言叫做 DSL 查詢 (Query DSL), 它允許你構建更加複雜、強大的查詢。 DSL(Domain Specific Language 特定領域語言) 以 JSON 請求體的形式出現。
POST /haoke/user/_search
#請求體
{
"query" : {
"match" : { #match只是查詢的一種
"age" : 20
}
}
}
實現:查詢年齡大於 30 歲的男性用戶。
POST /haoke/user/_search
#請求數據
{
"query": {
"bool": {
"filter": {
"range": {
"age": {
"gt": 30
}
}
},
"must": {
"match": {
"sex": "男"
}
}
}
}
}
查詢出來的結果
全文搜索
POST /haoke/user/_search
#請求數據
{
"query": {
"match": {
"name": "張三 李四"
}
}
}
高亮顯示:只需要在添加一個 highlight 即可
POST /haoke/user/_search
#請求數據
{
"query": {
"match": {
"name": "張三 李四"
}
}
"highlight": {
"fields": {
"name": {}
}
}
}
聚合
在 Elasticsearch 中,支持聚合操作,類似 SQL 中的 group by 操作。
POST /haoke/user/_search
{
"aggs": {
"all_interests": {
"terms": {
"field": "age"
}
}
}
}
結果如下,我們通過年齡進行聚合
從結果可以看出,年齡 30 的有 2 條數據,20 的有一條,40 的一條。
ElasticSearch 核心詳解
文檔
在 Elasticsearch 中,文檔以 JSON 格式進行存儲,可以是複雜的結構,如:
{
"_index": "haoke",
"_type": "user",
"_id": "1005",
"_version": 1,
"_score": 1,
"_source": {
"id": 1005,
"name": "孫七",
"age": 37,
"sex": "女",
"card": {
"card_number": "123456789"
}
}
}
其中,card 是一個複雜對象,嵌套的 Card 對象
元數據(metadata)
一個文檔不只有數據。它還包含了元數據 (metadata)——關於文檔的信息。三個必須的元數據節點是:
index
索引 (index) 類似於關係型數據庫裏的“數據庫”——它是我們存儲和索引關聯數據的地方。
提示:事實上,我們的數據被存儲和索引在分片 (shards) 中,索引只是一個把一個或多個分片分組在一起的邏輯空間。然而,這只是一些內部細節——我們的程序完全不用關心分片。對於我們的程序而言,文檔存儲在索引 (index) 中。剩下的細節由 Elasticsearch 關心既可。
_type
在應用中,我們使用對象表示一些 “事物”,例如一個用戶、一篇博客、一個評論,或者一封郵件。每個對象都屬於一個類(class),這個類定義了屬性或與對象關聯的數據。user 類的對象可能包含姓名、性別、年齡和 Email 地址。 在關係型數據庫中,我們經常將相同類的對象存儲在一個表裏,因爲它們有着相同的結構。同理,在 Elasticsearch 中,我們使用相同類型(type) 的文檔表示相同的“事物”,因爲他們的數據結構也是相同的。
每個類型 (type) 都有自己的映射 (mapping) 或者結構定義,就像傳統數據庫表中的列一樣。所有類型下的文檔被存儲在同一個索引下,但是類型的映射 (mapping) 會告訴 Elasticsearch 不同的文檔如何被索引。
_type 的名字可以是大寫或小寫,不能包含下劃線或逗號。我們將使用 blog 做爲類型名。
_id
id 僅僅是一個字符串,它與_index 和_type 組合時,就可以在 Elasticsearch 中唯一標識一個文檔。當創建一個文 檔,你可以自定義_id ,也可以讓 Elasticsearch 幫你自動生成(32 位長度)
查詢響應
pretty
可以在查詢 url 後面添加 pretty 參數,使得返回的 json 更易查看。
指定響應字段
在響應的數據中,如果我們不需要全部的字段,可以指定某些需要的字段進行返回。通過添加 _source
GET /haoke/user/1005?_source=id,name
#響應
{
"_index": "haoke",
"_type": "user",
"_id": "1005",
"_version": 1,
"found": true,
"_source": {
"name": "孫七",
"id": 1005
}
}
如不需要返回元數據,僅僅返回原始數據,可以這樣:
GET /haoke/1 user/1005/_source
還可以這樣:
GET /haoke/user/1005/_source?_1 source=id,name
判斷文檔是否存在
如果我們只需要判斷文檔是否存在,而不是查詢文檔內容,那麼可以這樣:
HEAD /haoke/user/1005
通過發送一個 head 請求,來判斷數據是否存在
判斷數據是否存在
HEAD 1 /haoke/user/1006
數據不存在
當然,這隻表示你在查詢的那一刻文檔不存在,但並不表示幾毫秒後依舊不存在。另一個進程在這期間可能創建新文檔。
批量操作
有些情況下可以通過批量操作以減少網絡請求。如:批量查詢、批量插入數據。
批量查詢
POST /haoke/user/_mget
{
"ids" : [ "1001", "1003" ]
}
結果:
批量查詢
如果,某一條數據不存在,不影響整體響應,需要通過 found 的值進行判斷是否查詢到數據。
POST /haoke/user/_mget
{
"ids" : [ "1001", "1006" ]
}
也就是說,一個數據的存在不會影響其它數據的返回
bulk 操作
在 Elasticsearch 中,支持批量的插入、修改、刪除操作,都是通過 bulk 的 api 完成的。
請求格式如下:(請求格式不同尋常)
{ action: { metadata }}
{ request body }
{ action: { metadata }}
{ request body }
...
批量插入數據:
{"create":{"_index":"haoke","_type":"user","_id":2001}}
{"id":2001,"name":"name1","age": 20,"sex": "男"}
{"create":{"_index":"haoke","_type":"user","_id":2002}}
{"id":2002,"name":"name2","age": 20,"sex": "男"}
{"create":{"_index":"haoke","_type":"user","_id":2003}}
{"id":2003,"name":"name3","age": 20,"sex": "男"}
注意最後一行的回車:
批量刪除:
{"delete":{"_index":"haoke","_type":"user","_id":2001}}
{"delete":{"_index":"haoke","_type":"user","_id":2002}}
{"delete":{"_index":"haoke","_type":"user","_id":2003}}
由於 delete 沒有請求體,所以 action 的下一行直接就是下一個 action。
其他操作就類似了。一次請求多少性能最高?
-
整個批量請求需要被加載到接受我們請求節點的內存裏,所以請求越大,給其它請求可用的內存就越小。有一 個最佳的 bulk 請求大小。超過這個大小,性能不再提升而且可能降低。
-
最佳大小,當然並不是一個固定的數字。它完全取決於你的硬件、你文檔的大小和複雜度以及索引和搜索的負 載。
-
幸運的是,這個最佳點 (sweetspot) 還是容易找到的:試着批量索引標準的文檔,隨着大小的增長,當性能開始 降低,說明你每個批次的大小太大了。開始的數量可以在 1000~5000 個文檔之間,如果你的文檔非常大,可以使用較小的批次。
-
通常着眼於你請求批次的物理大小是非常有用的。一千個 1kB 的文檔和一千個 1MB 的文檔大不相同。一個好的 批次最好保持在 5-15MB 大小間。
分頁
和 SQL 使用 LIMIT 關鍵字返回只有一頁的結果一樣,Elasticsearch 接受 from 和 size 參數:
-
size: 結果數,默認 10
-
from: 跳過開始的結果數,默認 0
如果你想每頁顯示 5 個結果,頁碼從 1 到 3,那請求如下:
GET /_search?size=5
GET /_search?size=5&from=5
GET /_search?size=5&from=10
應該當心分頁太深或者一次請求太多的結果。結果在返回前會被排序。但是記住一個搜索請求常常涉及多個分 片。每個分片生成自己排好序的結果,它們接着需要集中起來排序以確保整體排序正確。
GET /haoke/user/_1 search?size=1&from=2
在集羣系統中深度分頁
爲了理解爲什麼深度分頁是有問題的,讓我們假設在一個有 5 個主分片的索引中搜索。當我們請求結果的第一 頁(結果 1 到 10)時,每個分片產生自己最頂端 10 個結果然後返回它們給請求節點 (requesting node),它再 排序這所有的 50 個結果以選出頂端的 10 個結果。
現在假設我們請求第 1000 頁 — 結果 10001 到 10010。工作方式都相同,不同的是每個分片都必須產生頂端的 10010 個結果。然後請求節點排序這 50050 個結果並丟棄 50040 個!
你可以看到在分佈式系統中,排序結果的花費隨着分頁的深入而成倍增長。這也是爲什麼網絡搜索引擎中任何 語句不能返回多於 1000 個結果的原因。
映射
前面我們創建的索引以及插入數據,都是由 Elasticsearch 進行自動判斷類型,有些時候我們是需要進行明確字段類型的,否則,自動判斷的類型和實際需求是不相符的。
自動判斷的規則如下:
Elasticsearch 中支持的類型如下:
-
string 類型在 ElasticSearch 舊版本中使用較多,從 ElasticSearch 5.x 開始不再支持 string,由 text 和 keyword 類型替代。
-
text 類型,當一個字段是要被全文搜索的,比如 Email 內容、產品描述,應該使用 text 類型。設置 text 類型 以後,字段內容會被分析,在生成倒排索引以前,字符串會被分析器分成一個一個詞項。text 類型的字段 不用於排序,很少用於聚合。
-
keyword 類型適用於索引結構化的字段,比如 email 地址、主機名、狀態碼和標籤。如果字段需要進行過 濾 (比如查找已發佈博客中 status 屬性爲 published 的文章)、排序、聚合。keyword 類型的字段只能通過精 確值搜索到。
創建明確類型的索引:
如果你要像之前舊版版本一樣兼容自定義 type , 需要將 include_type_name=true 攜帶
put http://202.193.56.222:9200/itcast?include_type_name=true
{
"settings":{
"index":{
"number_of_shards":"2",
"number_of_replicas":"0"
}
},
"mappings":{
"person":{
"properties":{
"name":{
"type":"text"
},
"age":{
"type":"integer"
},
"mail":{
"type":"keyword"
},
"hobby":{
"type":"text"
}
}
}
}
}
查看映射
GET /itcast/_mapping
插入數據
POST /itcast/_bulk
{"index":{"_index":"itcast","_type":"person"}}
{"name":"張三","age": 20,"mail": "111@qq.com","hobby":"羽毛球、乒乓球、足球"}
{"index":{"_index":"itcast","_type":"person"}}
{"name":"李四","age": 21,"mail": "222@qq.com","hobby":"羽毛球、乒乓球、足球、籃球"}
{"index":{"_index":"itcast","_type":"person"}}
{"name":"王五","age": 22,"mail": "333@qq.com","hobby":"羽毛球、籃球、游泳、聽音樂"}
{"index":{"_index":"itcast","_type":"person"}}
{"name":"趙六","age": 23,"mail": "444@qq.com","hobby":"跑步、游泳"}
{"index":{"_index":"itcast","_type":"person"}}
{"name":"孫七","age": 24,"mail": "555@qq.com","hobby":"聽音樂、看電影"}
測試搜索
POST /itcast/person/_search
{
"query":{
"match":{
"hobby":"音樂"
}
}
}
結構化查詢
term 查詢
term 主要用於精確匹配哪些值,比如數字,日期,布爾值或 not_analyzed 的字符串 (未經分析的文本數據類型):
{ "term": { "age": 26 }}
{ "term": { "date": "2014-09-01" }}
{ "term": { "public": true }}
{ "term": { "tag": "full_text" }}
示例
POST /itcast/person/_search
{
"query":{
"term":{
"age":20
}
}
}
terms 查詢
terms 跟 term 有點類似,但 terms 允許指定多個匹配條件。 如果某個字段指定了多個值,那麼文檔需要一起去 做匹配:
{
"terms":{
"tag":[
"search",
"full_text",
"nosql"
]
}
}
示例:
POST /itcast/person/_search
{
"query":{
"terms":{
"age":[
20,
21
]
}
}
}
range 查詢
range 過濾允許我們按照指定範圍查找一批數據:
{
"range":{
"age":{
"gte":20,
"lt":30
}
}
}
範圍操作符包含:
-
gt : 大於
-
gte:: 大於等於
-
lt : 小於
-
lte: 小於等於
示例:
POST /itcast/person/_search
{
"query":{
"range":{
"age":{
"gte":20,
"lte":22
}
}
}
}
exists 查詢
exists 查詢可以用於查找文檔中是否包含指定字段或沒有某個字段,類似於 SQL 語句中的 IS_NULL 條件
{
"exists": {
"field": "title"
}
}
這兩個查詢只是針對已經查出一批數據來,但是想區分出某個字段是否存在的時候使用。示例:
POST /haoke/user/_search
{
"query": {
"exists": { #必須包含
"field": "card"
}
}
}
match 查詢
match 查詢是一個標準查詢,不管你需要全文本查詢還是精確查詢基本上都要用到它。
如果你使用 match 查詢一個全文本字段,它會在真正查詢之前用分析器先分析 match 一下查詢字符:
{
"match": {
"tweet": "About Search"
}
}
如果用 match 下指定了一個確切值,在遇到數字,日期,布爾值或者 not_analyzed 的字符串時,它將爲你搜索你 給定的值:
{ "match": { "age": 26 }}
{ "match": { "date": "2014-09-01" }}
{ "match": { "public": true }}
{ "match": { "tag": "full_text" }}
bool 查詢
-
bool 查詢可以用來合併多個條件查詢結果的布爾邏輯,它包含一下操作符:
-
must :: 多個查詢條件的完全匹配, 相當於 and 。
-
must_not :: 多個查詢條件的相反匹配,相當於 not 。
-
should :: 至少有一個查詢條件匹配, 相當於 or 。
這些參數可以分別繼承一個查詢條件或者一個查詢條件的數組:
{
"bool":{
"must":{
"term":{
"folder":"inbox"
}
},
"must_not":{
"term":{
"tag":"spam"
}
},
"should":[
{
"term":{
"starred":true
}
},
{
"term":{
"unread":true
}
}
]
}
}
過濾查詢
前面講過結構化查詢,Elasticsearch 也支持過濾查詢,如 term、range、match 等。
示例:查詢年齡爲 20 歲的用戶。
POST /itcast/person/_search
{
"query":{
"bool":{
"filter":{
"term":{
"age":20
}
}
}
}
}
查詢和過濾的對比
-
一條過濾語句會詢問每個文檔的字段值是否包含着特定值。
-
查詢語句會詢問每個文檔的字段值與特定值的匹配程度如何。
-
一條查詢語句會計算每個文檔與查詢語句的相關性,會給出一個相關性評分 _score,並且 按照相關性對匹 配到的文檔進行排序。 這種評分方式非常適用於一個沒有完全配置結果的全文本搜索。
-
一個簡單的文檔列表,快速匹配運算並存入內存是十分方便的, 每個文檔僅需要 1 個字節。這些緩存的過濾結果集與後續請求的結合使用是非常高效的。
-
查詢語句不僅要查找相匹配的文檔,還需要計算每個文檔的相關性,所以一般來說查詢語句要比 過濾語句更耗時,並且查詢結果也不可緩存。
建議:
做精確匹配搜索時,最好用過濾語句,因爲過濾語句可以緩存數據。
中文分詞
什麼是分詞
分詞就是指將一個文本轉化成一系列單詞的過程,也叫文本分析,在 Elasticsearch 中稱之爲 Analysis。
舉例:我是中國人 --> 我 / 是 / 中國人
分詞 api
指定分詞器進行分詞
POST /_analyze
{
"analyzer":"standard",
"text":"hello world"
}
結果如下
在結果中不僅可以看出分詞的結果,還返回了該詞在文本中的位置。
指定索引分詞
POST /itcast/_analyze
{
"analyzer": "standard",
"field": "hobby",
"text": "聽音樂"
}
中文分詞難點
中文分詞的難點在於,在漢語中沒有明顯的詞彙分界點,如在英語中,空格可以作爲分隔符,如果分隔不正確就會造成歧義。如:
-
我 / 愛 / 炒肉絲
-
我 / 愛 / 炒 / 肉絲
常用中文分詞器,IK、jieba、THULAC 等,推薦使用 IK 分詞器。
IK Analyzer 是一個開源的,基於 java 語言開發的輕量級的中文分詞工具包。從 2006 年 12 月推出 1.0 版開始,IKAnalyzer 已經推出了 3 個大版本。最初,它是以開源項目 Luence 爲應用主體的,結合詞典分詞和文法分析算法的中文分詞組件。新版本的 IK Analyzer 3.0 則發展爲面向 Java 的公用分詞組件,獨立於 Lucene 項目,同時提供了對 Lucene 的默認優化實現。
採用了特有的 “正向迭代最細粒度切分算法 “,具有 80 萬字 / 秒的高速處理能力 採用了多子處理器分析模式,支持:英文字母(IP 地址、Email、URL)、數字(日期,常用中文數量詞,羅馬數字,科學計數法),中文詞彙(姓名、地名處理)等分詞處理。 優化的詞典存儲,更小的內存佔用。
IK 分詞器 Elasticsearch 插件地址:
https://github.com/medcl/elasticsearch-analysis-ik
安裝分詞器
首先下載到最新的 ik 分詞器,下載完成後,使用 xftp 工具,拷貝到服務器上
#安裝方法:將下載到的 es/plugins/ik 目錄下
mkdir es/plugins/ik
#解壓
unzip elasticsearch-analysis-ik-7.9.1.zip
#重啓
./bin/elasticsearch
我們通過日誌,發現它已經成功加載了 ik 分詞器插件
測試
POST /_analyze
{
"analyzer": "ik_max_word",
"text": "我是中國人"
}
我們發現 ik 分詞器已經成功分詞完成
全文搜索
全文搜索兩個最重要的方面是:
-
相關性(Relevance) 它是評價查詢與其結果間的相關程度,並根據這種相關程度對結果排名的一種能力,這 種計算方式可以是 TF/IDF 方法、地理位置鄰近、模糊相似,或其他的某些算法。
-
分詞(Analysis) 它是將文本塊轉換爲有區別的、規範化的 token 的一個過程,目的是爲了創建倒排索引以及查詢倒排索引。
構造數據
ES 7.4 默認不在支持指定索引類型,默認索引類型是_doc
http://202.193.56.222:9200/itcast?include_type_name=true
{
"settings":{
"index":{
"number_of_shards":"1",
"number_of_replicas":"0"
}
},
"mappings":{
"person":{
"properties":{
"name":{
"type":"text"
},
"age":{
"type":"integer"
},
"mail":{
"type":"keyword"
},
"hobby":{
"type":"text",
"analyzer":"ik_max_word"
}
}
}
}
}
然後插入數據
POST http://202.193.56.222:9200/itcast/_bulk
{"index":{"_index":"itcast","_type":"person"}}
{"name":"張三","age": 20,"mail": "111@qq.com","hobby":"羽毛球、乒乓球、足球"}
{"index":{"_index":"itcast","_type":"person"}}
{"name":"李四","age": 21,"mail": "222@qq.com","hobby":"羽毛球、乒乓球、足球、籃球"}
{"index":{"_index":"itcast","_type":"person"}}
{"name":"王五","age": 22,"mail": "333@qq.com","hobby":"羽毛球、籃球、游泳、聽音樂"}
{"index":{"_index":"itcast","_type":"person"}}
{"name":"趙六","age": 23,"mail": "444@qq.com","hobby":"跑步、游泳、籃球"}
{"index":{"_index":"itcast","_type":"person"}}
{"name":"孫七","age": 24,"mail": "555@qq.com","hobby":"聽音樂、看電影、羽毛球"}
單詞搜索
POST /itcast/person/_search
{
"query":{
"match":{
"hobby":"音樂"
}
},
"highlight":{
"fields":{
"hobby":{
}
}
}
}
查詢出來的結果如下,並且還帶有高亮
過程說明:
-
檢查字段類型
-
愛好 hobby 字段是一個 text 類型( 指定了 IK 分詞器),這意味着查詢字符串本身也應該被分詞。
-
分析查詢字符串 。
-
將查詢的字符串 “音樂” 傳入 IK 分詞器中,輸出的結果是單個項 音樂。因爲只有一個單詞項,所以 match 查詢執行的是單個底層 term 查詢。
-
查找匹配文檔 。
-
用 term 查詢在倒排索引中查找 “音樂” 然後獲取一組包含該項的文檔,本例的結果是文檔:3 、5 。
-
爲每個文檔評分 。
-
用 term 查詢計算每個文檔相關度評分 _score ,這是種將 詞頻(term frequency,即詞 “音樂” 在相關文檔的 hobby 字段中出現的頻率)和 反向文檔頻率(inverse document frequency,即詞 “音樂” 在所有文檔的 hobby 字段中出現的頻率),以及字段的長度(即字段越短相關度越高)相結合的計算方式。
多詞搜索
POST /itcast/person/_search
{
"query":{
"match":{
"hobby":"音樂 籃球"
}
},
"highlight":{
"fields":{
"hobby":{
}
}
}
}
可以看到,包含了 “音樂”、“籃球” 的數據都已經被搜索到了。可是,搜索的結果並不符合我們的預期,因爲我們想搜索的是既包含 “音樂” 又包含 “籃球” 的用戶,顯然結果返回的 “或” 的關係。在 Elasticsearch 中,可以指定詞之間的邏輯關係,如下:
POST /itcast/person/_search
{
"query":{
"match":{
"hobby":"音樂 籃球"
"operator":"and"
}
},
"highlight":{
"fields":{
"hobby":{
}
}
}
}
通過這樣的話,就會讓兩個關鍵字之間存在 and 關係了
可以看到結果符合預期。
前面我們測試了 “OR” 和 “AND” 搜索,這是兩個極端,其實在實際場景中,並不會選取這 2 個極端,更有可能是選取這種,或者說,只需要符合一定的相似度就可以查詢到數據,在 Elasticsearch 中也支持這樣的查詢,通過 minimum_should_match 來指定匹配度,如:70%;示例如下:
{
"query":{
"match":{
"hobby":{
"query":"游泳 羽毛球",
"minimum_should_match":"80%"
}
}
},
"highlight": {
"fields": {
"hobby": {}
}
}
}
#結果:省略顯示
"hits": {
"total": 4, #相似度爲80%的情況下,查詢到4條數據
"max_score": 1.621458,
"hits": [
}
#設置40%進行測試:
{
"query":{
"match":{
"hobby":{
"query":"游泳 羽毛球",
"minimum_should_match":"40%"
}
}
},
"highlight": {
"fields": {
"hobby": {}
}
}
}
#結果:
"hits": {
"total": 5, #相似度爲40%的情況下,查詢到5條數據
"max_score": 1.621458,
"hits": [
}
相似度應該多少合適,需要在實際的需求中進行反覆測試,纔可得到合理的值。
組合搜索
在搜索時,也可以使用過濾器中講過的 bool 組合查詢,示例:
POST /itcast/person/_search
{
"query":{
"bool":{
"must":{
"match":{
"hobby":"籃球"
}
},
"must_not":{
"match":{
"hobby":"音樂"
}
},
"should":[
{
"match":{
"hobby":"游泳"
}
}
]
}
},
"highlight":{
"fields":{
"hobby":{
}
}
}
}
上面搜索的意思是: 搜索結果中必須包含籃球,不能包含音樂,如果包含了游泳,那麼它的相似度更高。
結果:
評分的計算規則
bool 查詢會爲每個文檔計算相關度評分 _score , 再將所有匹配的 must 和 should 語句的分數 _score 求和,最後除以 must 和 should 語句的總數。
must_not 語句不會影響評分; 它的作用只是將不相關的文檔排除。
默認情況下,should 中的內容不是必須匹配的,如果查詢語句中沒有 must,那麼就會至少匹配其中一個。當然了,也可以通過 minimum_should_match 參數進行控制,該值可以是數字也可以的百分比。示例:
POST /itcast/person/_search
{
"query":{
"bool":{
"should":[
{
"match":{
"hobby":"游泳"
}
},
{
"match":{
"hobby":"籃球"
}
},
{
"match":{
"hobby":"音樂"
}
}
],
"minimum_should_match":2
}
},
"highlight":{
"fields":{
"hobby":{
}
}
}
}
minimum_should_match 爲 2,意思是 should 中的三個詞,至少要滿足 2 個。
權重
有些時候,我們可能需要對某些詞增加權重來影響該條數據的得分。如下:
搜索關鍵字爲 “游泳籃球”,如果結果中包含了“音樂” 權重爲 10,包含了 “跑步” 權重爲 2。
POST /itcast/person/_search
{
"query":{
"bool":{
"must":{
"match":{
"hobby":{
"query":"游泳籃球",
"operator":"and"
}
}
},
"should":[
{
"match":{
"hobby":{
"query":"音樂",
"boost":10
}
}
},
{
"match":{
"hobby":{
"query":"跑步",
"boost":2
}
}
}
]
}
},
"highlight":{
"fields":{
"hobby":{
}
}
}
}
ElasticSearch 集羣
集羣節點
ELasticsearch 的集羣是由多個節點組成的,通過 cluster.name 設置集羣名稱,並且用於區分其它的集羣,每個節點通過 node.name 指定節點的名稱。
在 Elasticsearch 中,節點的類型主要有 4 種:
-
master 節點
-
配置文件中 node.master 屬性爲 true(默認爲 true),就有資格被選爲 master 節點。master 節點用於控制整個集羣的操作。比如創建或刪除索引,管理其它非 master 節點等。
-
data 節點
-
配置文件中 node.data 屬性爲 true(默認爲 true),就有資格被設置成 data 節點。data 節點主要用於執行數據相關的操作。比如文檔的 CRUD。
-
客戶端節點
-
配置文件中 node.master 屬性和 node.data 屬性均爲 false。
-
該節點不能作爲 master 節點,也不能作爲 data 節點。
-
可以作爲客戶端節點,用於響應用戶的請求,把請求轉發到其他節點
-
部落節點
-
當一個節點配置 tribe.* 的時候,它是一個特殊的客戶端,它可以連接多個集羣,在所有連接的集羣上執行 搜索和其他操作。
搭建集羣
#啓動3個虛擬機,分別在3臺虛擬機上部署安裝Elasticsearch
mkdir /itcast/es-cluster
#分發到其它機器
scp -r es-cluster elsearch@192.168.40.134:/itcast
#node01的配置:
cluster.name: es-itcast-cluster
node.name: node01
node.master: true
node.data: true
network.host: 0.0.0.0
http.port: 9200
discovery.zen.ping.unicast.hosts: ["192.168.40.133","192.168.40.134","192.168.40.135"]
# 最小節點數
discovery.zen.minimum_master_nodes: 2
# 跨域專用
http.cors.enabled: true
http.cors.allow-origin: "*"
#node02的配置:
cluster.name: es-itcast-cluster
node.name: node02
node.master: true
node.data: true
network.host: 0.0.0.0
http.port: 9200
discovery.zen.ping.unicast.hosts: ["192.168.40.133","192.168.40.134","192.168.40.135"]
discovery.zen.minimum_master_nodes: 2
http.cors.enabled: true
http.cors.allow-origin: "*"
#node03的配置:
cluster.name: es-itcast-cluster
node.name: node02
node.master: true
node.data: true
network.host: 0.0.0.0
http.port: 9200
discovery.zen.ping.unicast.hosts: ["192.168.40.133","192.168.40.134","192.168.40.135"]
discovery.zen.minimum_master_nodes: 2
http.cors.enabled: true
http.cors.allow-origin: "*"
#分別啓動3個節點
./elasticsearch
查看集羣
創建索引:
查詢集羣狀態:/_cluster/health 響應:
集羣中有三種顏色
分片和副本
爲了將數據添加到 Elasticsearch,我們需要索引 (index)——一個存儲關聯數據的地方。實際上,索引只是一個用來指向一個或多個分片 (shards) 的 邏輯命名空間 (logical namespace).
-
一個分片 (shard) 是一個最小級別 “工作單元 (worker unit),它只是保存了索引中所有數據的一部分。
-
我們需要知道是分片就是一個 Lucene 實例,並且它本身就是一個完整的搜索引擎。應用程序不會和它直接通 信。
-
分片可以是主分片 (primary shard) 或者是複製分片 (replica shard)。
-
索引中的每個文檔屬於一個單獨的主分片,所以主分片的數量決定了索引最多能存儲多少數據。
-
複製分片只是主分片的一個副本,它可以防止硬件故障導致的數據丟失,同時可以提供讀請求,比如搜索或者從別的 shard 取回文檔。
-
當索引創建完成的時候,主分片的數量就固定了,但是複製分片的數量可以隨時調整。
故障轉移
將 data 節點停止
這裏選擇將 node02 停止:
當前集羣狀態爲黃色,表示主節點可用,副本節點不完全可用,過一段時間觀察,發現節點列表中看不到 node02,副本節點分配到了 node01 和 node03,集羣狀態恢復到綠色。
將 node02 恢復
可以看到,node02 恢復後,重新加入了集羣,並且重新分配了節點信息。
將 master 節點停止
接下來,測試將 node01 停止,也就是將主節點停止。
從結果中可以看出,集羣對 master 進行了重新選舉,選擇 node03 爲 master 。並且集羣狀態變成黃色。 等待一段時間後,集羣狀態從黃色變爲了綠色:
image-20200923153343555
恢復 node01 節點:
./node01/1 bin/elasticsearch
重啓之後,發現 node01 可以正常加入到集羣中,集羣狀態依然爲綠色:
image-20200923153415117
特別說明:如果在配置文件中 discovery.zen.minimum_master_nodes 設置的不是 N/2+1 時,會出現腦裂問題,之前宕機 的主節點恢復後不會加入到集羣。
分佈式文檔
路由
首先,來看個問題:
如圖所示:當我們想一個集羣保存文檔時,文檔該存儲到哪個節點呢? 是隨機嗎? 是輪詢嗎?實際上,在 ELasticsearch 中,會採用計算的方式來確定存儲到哪個節點,計算公式如下:
shard = hash(routing) % number_1 of_primary_shards
其中:
-
routing 值是一個任意字符串,它默認是 _id 但也可以自定義。
-
這個 routing 字符串通過哈希函數生成一個數字,然後除以主切片的數量得到一個餘數 (remainder),餘數 的範圍永遠是 0 到 number_of_primary_shards - 1,這個數字就是特定文檔所在的分片
這就是爲什麼創建了主分片後,不能修改的原因。
文檔的寫操作
新建、索引和刪除請求都是寫(write)操作,它們必須在主分片上成功完成才能複製分片上
下面我們羅列在主分片和複製分片上成功新建、索引或刪除一個文檔必要的順序步驟:
-
客戶端給 Node 1 發送新建、索引或刪除請求。
-
節點使用文檔的 _id 確定文檔屬於 分片 0 。它轉發請求到 Node 3 ,分片 0 位於這個節點上。
-
Node 3 在主分片上執行請求,如果成功,它轉發請求到相應的位於 Node 1 和 Node 2 的複製節點上。當所有 的複製節點報告成功, Node 3 報告成功到請求的節點,請求的節點再報告給客戶端。
客戶端接收到成功響應的時候,文檔的修改已經被應用於主分片和所有的複製分片。你的修改生效了。
搜索文檔
文檔能夠從主分片或任意一個複製分片被檢索。
下面我們羅列在主分片或複製分片上檢索一個文檔必要的順序步驟:
-
客戶端給 Node 1 發送 get 請求。
-
節點使用文檔的_id 確定文檔屬於分片 0 。分片 0 對應的複製分片在三個節點上都有。此時,它轉發請求到 Node 2 。
-
Node 2 返回文檔 (document) 給 Node 1 然後返回給客戶端。對於讀請求,爲了平衡負載,請求節點會爲每個請求選擇不同的分片——它會循環所有分片副本。可能的情況是,一個被索引的文檔已經存在於主分片上卻還沒來得及同步到複製分片上。這時複製分片會報告文檔未找到,主分片會成功返回文檔。一旦索引請求成功返回給用戶,文檔則在主分片和複製分片都是可用的。
全文搜索
對於全文搜索而言,文檔可能分散在各個節點上,那麼在分佈式的情況下,如何搜索文檔呢?
搜索,分爲 2 個階段,
-
搜索(query)
-
取回(fetch)
搜索(query)
查詢階段包含以下三步:
-
客戶端發送一個 search(搜索) 請求給 Node 3 , Node 3 創建了一個長度爲 from+size 的空優先級隊
-
Node 3 轉發這個搜索請求到索引中每個分片的原本或副本。每個分片在本地執行這個查詢並且結果將結果到 一個大小爲 from+size 的有序本地優先隊列裏去。
-
每個分片返回 document 的 ID 和它優先隊列裏的所有 document 的排序值給協調節點 Node 3 。Node 3 把這些 值合併到自己的優先隊列裏產生全局排序結果。
取回 fetch
分發階段由以下步驟構成:
-
協調節點辨別出哪個 document 需要取回,並且向相關分片發出 GET 請求。
-
每個分片加載 document 並且根據需要豐富(enrich)它們,然後再將 document 返回協調節點。
-
一旦所有的 document 都被取回,協調節點會將結果返回給客戶端。
Java 客戶端
在 Elasticsearch 中,爲 java 提供了 2 種客戶端,一種是 REST 風格的客戶端,另一種是 Java API 的客戶端
REST 客戶端
Elasticsearch 提供了 2 種 REST 客戶端,一種是低級客戶端,一種是高級客戶端。
-
Java Low Level REST Client:官方提供的低級客戶端。該客戶端通過 http 來連接 Elasticsearch 集羣。用戶在使 用該客戶端時需要將請求數據手動拼接成 Elasticsearch 所需 JSON 格式進行發送,收到響應時同樣也需要將返回的 JSON 數據手動封裝成對象。雖然麻煩,不過該客戶端兼容所有的 Elasticsearch 版本。
-
Java High Level REST Client:官方提供的高級客戶端。該客戶端基於低級客戶端實現,它提供了很多便捷的 API 來解決低級客戶端需要手動轉換數據格式的問題。
構造數據
POST /haoke/house/_bulk
{"index":{"_index":"haoke","_type":"house"}}
{"id":"1001","title":"整租 · 南丹大樓 1居室 7500","price":"7500"}
{"index":{"_index":"haoke","_type":"house"}}
{"id":"1002","title":"陸家嘴板塊,精裝設計一室一廳,可拎包入住誠意租。","price":"8500"}
{"index":{"_index":"haoke","_type":"house"}}
{"id":"1003","title":"整租 · 健安坊 1居室 4050","price":"7500"}
{"index":{"_index":"haoke","_type":"house"}}
{"id":"1004","title":"整租 · 中凱城市之光+視野開闊+景色秀麗+拎包入住","price":"6500"}
{"index":{"_index":"haoke","_type":"house"}}
{"id":"1005","title":"整租 · 南京西路品質小區 21213三軌交匯 配套齊* 拎包入住","price":"6000"}
{"index":{"_index":"haoke","_type":"house"}}
{"id":"1006","title":"祥康裏 簡約風格 *南戶型 拎包入住 看房隨時","price":"7000"}
REST 低級客戶端
創建項目,加入依賴
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>Study_ElasticSearch_Code</artifactId>
<version>1.0-SNAPSHOT</version>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>7</source>
<target>7</target>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-client</artifactId>
<version>6.8.5</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.11.1</version>
</dependency>
</dependencies>
</project>
編寫測試類
/**
* 使用低級客戶端 訪問
*
* @author: 陌溪
* @create: 2020-09-23-16:33
*/
public class ESApi {
private RestClient restClient;
private static final ObjectMapper MAPPER = new ObjectMapper();
/**
* 初始化
*/
public void init() {
RestClientBuilder restClientBuilder = RestClient.builder(new HttpHost("202.193.56.222", 9200, "http"));
this.restClient = restClientBuilder.build();
}
/**
* 查詢集羣狀態
*/
public void testGetInfo() throws IOException {
Request request = new Request("GET", "/_cluster/state");
request.addParameter("pretty", "true");
Response response = this.restClient.performRequest(request);
System.out.println(response.getStatusLine());
System.out.println(EntityUtils.toString(response.getEntity()));
}
/**
* 根據ID查詢數據
* @throws IOException
*/
public void testGetHouseInfo() throws IOException {
Request request = new Request("GET", "/haoke/house/Z3CduXQBYpWein3CRFug");
request.addParameter("pretty", "true");
Response response = this.restClient.performRequest(request);
System.out.println(response.getStatusLine());
System.out.println(EntityUtils.toString(response.getEntity()));
}
public void testCreateData() throws IOException {
Request request = new Request("POST", "/haoke/house");
Map<String, Object> data = new HashMap<>();
data.put("id", "2001");
data.put("title", "張江高科");
data.put("price", "3500");
// 寫成JSON
request.setJsonEntity(MAPPER.writeValueAsString(data));
Response response = this.restClient.performRequest(request);
System.out.println(response.getStatusLine());
System.out.println(EntityUtils.toString(response.getEntity()));
}
// 搜索數據
public void testSearchData() throws IOException {
Request request = new Request("POST", "/haoke/house/_search");
String searchJson = "{\"query\": {\"match\": {\"title\": \"拎包入住\"}}}";
request.setJsonEntity(searchJson);
request.addParameter("pretty","true");
Response response = this.restClient.performRequest(request);
System.out.println(response.getStatusLine());
System.out.println(EntityUtils.toString(response.getEntity()));
}
public static void main(String[] args) throws IOException {
ESApi esApi = new ESApi();
esApi.init();
// esApi.testGetInfo();
// esApi.testGetHouseInfo();
esApi.testCreateData();
}
}
REST 高級客戶端
創建項目,引入依賴
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>6.8.5</version>
</dependency>
編寫測試用例
/**
* ES高級客戶端
*
* @author: 陌溪
* @create: 2020-09-23-16:56
*/
public class ESHightApi {
private RestHighLevelClient client;
public void init() {
RestClientBuilder restClientBuilder = RestClient.builder(
new HttpHost("202.193.56.222", 9200, "http"));
this.client = new RestHighLevelClient(restClientBuilder);
}
public void after() throws Exception {
this.client.close();
}
/**
* 新增文檔,同步操作
*
* @throws Exception
*/
public void testCreate() throws Exception {
Map<String, Object> data = new HashMap<>();
data.put("id", "2002");
data.put("title", "南京西路 拎包入住 一室一廳");
data.put("price", "4500");
IndexRequest indexRequest = new IndexRequest("haoke", "house")
.source(data);
IndexResponse indexResponse = this.client.index(indexRequest,
RequestOptions.DEFAULT);
System.out.println("id->" + indexResponse.getId());
System.out.println("index->" + indexResponse.getIndex());
System.out.println("type->" + indexResponse.getType());
System.out.println("version->" + indexResponse.getVersion());
System.out.println("result->" + indexResponse.getResult());
System.out.println("shardInfo->" + indexResponse.getShardInfo());
}
/**
* 異步創建文檔
* @throws Exception
*/
public void testCreateAsync() throws Exception {
Map<String, Object> data = new HashMap<>();
data.put("id", "2003");
data.put("title", "南京東路 最新房源 二室一廳");
data.put("price", "5500");
IndexRequest indexRequest = new IndexRequest("haoke", "house")
.source(data);
this.client.indexAsync(indexRequest, RequestOptions.DEFAULT, new
ActionListener<IndexResponse>() {
@Override
public void onResponse(IndexResponse indexResponse) {
System.out.println("id->" + indexResponse.getId());
System.out.println("index->" + indexResponse.getIndex());
System.out.println("type->" + indexResponse.getType());
System.out.println("version->" + indexResponse.getVersion());
System.out.println("result->" + indexResponse.getResult());
System.out.println("shardInfo->" + indexResponse.getShardInfo());
}
@Override
public void onFailure(Exception e) {
System.out.println(e);
}
});
System.out.println("ok");
Thread.sleep(20000);
}
/**
* 查詢
* @throws Exception
*/
public void testQuery() throws Exception {
GetRequest getRequest = new GetRequest("haoke", "house",
"GkpdE2gBCKv8opxuOj12");
// 指定返回的字段
String[] includes = new String[]{"title", "id"};
String[] excludes = Strings.EMPTY_ARRAY;
FetchSourceContext fetchSourceContext =
new FetchSourceContext(true, includes, excludes);
getRequest.fetchSourceContext(fetchSourceContext);
GetResponse response = this.client.get(getRequest, RequestOptions.DEFAULT);
System.out.println("數據 -> " + response.getSource());
}
/**
* 判斷是否存在
*
* @throws Exception
*/
public void testExists() throws Exception {
GetRequest getRequest = new GetRequest("haoke", "house",
"GkpdE2gBCKv8opxuOj12");
// 不返回的字段
getRequest.fetchSourceContext(new FetchSourceContext(false));
boolean exists = this.client.exists(getRequest, RequestOptions.DEFAULT);
System.out.println("exists -> " + exists);
}
/**
* 刪除數據
*
* @throws Exception
*/
public void testDelete() throws Exception {
DeleteRequest deleteRequest = new DeleteRequest("haoke", "house",
"GkpdE2gBCKv8opxuOj12");
DeleteResponse response = this.client.delete(deleteRequest,
RequestOptions.DEFAULT);
System.out.println(response.status());// OK or NOT_FOUND
}
/**
* 更新數據
*
* @throws Exception
*/
public void testUpdate() throws Exception {
UpdateRequest updateRequest = new UpdateRequest("haoke", "house",
"G0pfE2gBCKv8opxuRz1y");
Map<String, Object> data = new HashMap<>();
data.put("title", "張江高科2");
data.put("price", "5000");
updateRequest.doc(data);
UpdateResponse response = this.client.update(updateRequest,
RequestOptions.DEFAULT);
System.out.println("version -> " + response.getVersion());
}
/**
* 測試搜索
*
* @throws Exception
*/
public void testSearch() throws Exception {
SearchRequest searchRequest = new SearchRequest("haoke");
searchRequest.types("house");
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
sourceBuilder.query(QueryBuilders.matchQuery("title", "拎包入住"));
sourceBuilder.from(0);
sourceBuilder.size(5);
sourceBuilder.timeout(new TimeValue(60, TimeUnit.SECONDS));
searchRequest.source(sourceBuilder);
SearchResponse search = this.client.search(searchRequest,
RequestOptions.DEFAULT);
System.out.println("搜索到 " + search.getHits().totalHits + " 條數據.");
SearchHits hits = search.getHits();
for (SearchHit hit : hits) {
System.out.println(hit.getSourceAsString());
}
}
public static void main(String[] args) throws Exception {
ESHightApi esHightApi = new ESHightApi();
esHightApi.init();
esHightApi.testCreate();
}
}
結語
陌溪是一個從三本院校一路摸滾翻爬上來的互聯網大廠程序員。獨立做過幾個開源項目,其中蘑菇博客在碼雲上有 2K Star 。目前就職於字節跳動的 Data 廣告部門,是字節跳動全線產品的商業變現研發團隊。本公衆號將會持續性的輸出很多原創小知識以及學習資源。如果你覺得本文對你有所幫助,麻煩給文章點個「贊」和「在看」。同時歡迎各位小夥伴關注陌溪,讓我們一起成長~
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/yiRL9r_FF3C-jpMXnkM5Pg