ElasticSearch
- 官方文件:https://www.elastic.co/guide/en/elasticsearch/reference/current/getting-started.html
- 非官方中文文件:https://learnku.com/docs/elasticsearch73/7.3
- 極簡概括:基於Apache Lucene構建開源的分散式搜尋引擎。
- 解決問題:MySQL like中文全文搜尋不走索引,或大資料搜尋效能低下的問題。
- 適用場景:
- 大資料檢索:在大資料量的查詢場景下,ES查詢效能依然保持優勢,常用於替代MySQL由於效能不足而做一些複雜的查詢。
- 大資料開發:大資料開發幾乎離不開Spark、Fink、Hadoop、ElasticSearch、MySQL、Redis、ZooKeeper這些元件。
- ELK結合:ES結合LK作為ELK(Elasticsearch(搜尋), Logstash(採集轉換), Kibana(分析))組合,可用於實時監控、分析和視覺化大量日誌和事件資料,如系統日誌、應用程式日誌、網路流量日誌等。
- 優點:
- 跨平臺:元件支援在Linux、Windows、MacOS上執行。
- 查詢效能優異:在超大資料量的查詢場景下,ES查詢效能依然保持優勢。
- 支援全文檢索:替代MySQL中文全文檢索不走索引的查詢弱項。
- 生態繁榮:是面向開發者的主流的搜尋引擎,文件,解決方案,疑難雜症,非0day漏洞,基本都有成熟的解決方案。
- 支援分散式:每個ES節點,都可以執行一部分搜尋任務,然後將結果合併。累加的算力效果如虎添翼。
- 支援複雜查詢:支援,模糊匹配,範圍查詢,布林搜尋。
- 缺點:
- ES沒有事務機制,對於MySQL的合作呢,也是最終一致性,所以強一致性的搜尋環境下並不適用,推薦Redis。
- json請求體父子格式反人類:果然技術厲害的程式設計師往往不會是一個好的產品經理。
- json響應體格式反人類,按照["成功或失敗的code", "data資料", "msg補充說明"]這種格式返回就好了。
- PHP API經常性異常:APi介面,寫操作失敗返回false也行,非要返回異常,異常若沒有處理,會中斷程式執行。
- 查詢方式受mapping限制:相比於MySQL,哪怕是個數字,都可以用like強制查詢,但是ES不行。
- 同類元件:Apache Solr、Apache Lucene、Algolia、Sphinx、XunSearch。
正排索引和倒排索引
ES用的倒排索引演算法。正倒兩種索引都是用於快速檢索資料的實現方案,我沒有太官方的解釋,所以舉例說明:
- 正排索引:有一個文章表,有文章id、標題、詳情3個欄位,透過文章列表功能獲取文章,透過id作為索引值獲取文章內容,這是很普遍的業務邏輯。想要搜尋包含指定關鍵詞的文章,資料庫就需要對文章的標題和內容逐一做對比,因為不走索引,資料量不大還好,資料量一大效能降低。
- 倒排索引:用於加速文字的檢索,文章內容利用分詞器拆分,將拆分好的關鍵詞與文章id做關聯,然後儲存。類比MySQL表的兩個列,一列是關鍵詞,另一列是包含這個關鍵詞的文章id,多個倒排索引資料集組成一個倒排表。再查詢時,不需要針對資料來源本身做查詢,而是變成了,關鍵詞為xxx的id為多少。
分詞
分詞就是把字串拆分成有用的關鍵詞,用於提供高質量搜尋的資料來源。
- 對英文:分詞直接用空格就行,I love you,可直接利用空格分成3個詞,對中文顯然不適用。
- 對中文:例如“今天溫度很高”,能用的詞彙可以拆分成“今天”、“溫度”、“很高”,可程式不知道怎麼拆分,若拆分為“今天溫”、“天溫”、“”度很”這樣的關鍵詞就顯得很怪異。
所以也就誕生了語法分析+字典的解決方案,用人工干涉+詞典的方式實現分詞器的邏輯。
至於利用NLP語義分析,上下文預測,的AI模式,不屬於ES的範疇,不展開。 - 若搜尋關鍵詞為語句或短語:需要利用TF-IDF和BM25演算法(等更高階的演算法),先對句子進行分詞,然後根據這多個分詞的再對結果集進行分詞查詢,然後評分,組合,最終返回結果。
安裝ES 8.14.1
- 系統配置,用於開啟防火牆,建立使用者,和大資料情況下提升效能。
Java寫的元件吃記憶體,建議VM虛擬機器記憶體設定大一點,系統設定為1G記憶體。
開兩個埠,並重啟防火牆
firewall-cmd --add-port=9200/tcp --zone=public --permanent
firewall-cmd --add-port=9300/tcp --zone=public --permanent
systemctl restart firewalld
新建一個es使用者,以非root形式執行,否則執行es會報錯,java.lang.RuntimeException: can not run elasticsearch as root
useradd -M es
passwd es 密碼為123456
vim /etc/security/limits.conf
文末新增兩行配置,最佳化檔案描述符軟硬限制,對提高效能非常重要,檔案描述符用於標識和管理每個程序都可以開啟檔案的數量
es soft nofile 65536
es hard nofile 65536
vim /etc/security/limits.d/20-nproc.conf
文末新增兩行配置,最佳化檔案描述符軟硬限制,對提高效能非常重要,檔案描述符用於標識和管理每個程序都可以開啟檔案的數量
es soft nofile 65536
es hard nofile 65536
vim /etc/sysctl.conf
定義系統中可以同時開啟的最大檔案描述符數量。
fs.file-max=655350
定義Linux核心中程序可以擁有的最大記憶體對映區域數量
vm.max_map_count=262144
重啟
sysctl -p
- 安裝相關
下載tar包並解壓,這個包地址來源於官網,並非java原始碼包
wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-8.14.1-linux-x86_64.tar.gz
tar zxf elasticsearch-8.14.1-linux-x86_64.tar.gz
更改所屬的使用者和使用者組
chown -R es:es elasticsearch-8.14.1
切換使用者
su es
啟動ES,如果發現報錯,請清空bin目錄同級的data目錄
./bin/elasticsearch
啟動後,直到看到如下字樣,說明能成功啟動,但是輸入它生成的使用者名稱密碼,登不進去
然後Ctrl + C強制停止,因為啟動一次之後,config/elasticsearch.yml配置檔案,會發生變化,這一步不可少
Elasticsearch security features have been automatically configured!
登不進去,那就改配置
vim config/elasticsearch.yml
把91~103行的true全部改為false,如下,注意配置格式,key: value之間要留出空格,否則ES不識別對應的值。
# Enable security features
xpack.security.enabled: false
xpack.security.enrollment.enabled: false
# Enable encryption for HTTP API client connections, such as Kibana, Logstash, and Agents
xpack.security.http.ssl:
enabled: false
keystore.path: certs/http.p12
# Enable encryption and mutual authentication between cluster nodes
xpack.security.transport.ssl:
enabled: false
儲存退出後,清除初始化的data資料
rm -rf elasticsearch-8.14.1/data/*
再次執行,並使其後臺執行
./bin/elasticsearch -d
檢視程序,確定ES是否成功執行
ps aux | grep elastic
es 49044 30.2 64.3 8291804 640416 pts/0 Sl 05:08 0:26 /test/elasticsearch-8.14.1/jdk/bin/java -Des.networkaddress.cache.ttl=60 -Des.networkaddress.cache.negative.ttl=10 -Djava.security.manager=allow -XX:+AlwaysPreTouch -Xss1m -Djava.awt.headless=true -Dfile.encoding=UTF-8 -Djna.nosys=true -XX:-OmitStackTraceInFastThrow -Dio.netty.noUnsafe=true -Dio.netty.noKeySetOptimization=true -Dio.netty.recycler.maxCapacityPerThread=0 -Dlog4j.shutdownHookEnabled=false -Dlog4j2.disable.jmx=true -Dlog4j2.formatMsgNoLookups=true -Djava.locale.providers=SPI,COMPAT --add-opens=java.base/java.io=org.elasticsearch.preallocate --add-opens=org.apache.lucene.core/org.apache.lucene.store=org.elasticsearch.vec --enable-native-access=org.elasticsearch.nativeaccess -XX:ReplayDataFile=logs/replay_pid%p.log -Djava.library.path=/test/elasticsearch-8.14.1/lib/platform/linux-x64:/usr/java/packages/lib:/usr/lib64:/lib64:/lib:/usr/lib -Djna.library.path=/test/elasticsearch-8.14.1/lib/platform/linux-x64:/usr/java/packages/lib:/usr/lib64:/lib64:/lib:/usr/lib -Des.distribution.type=tar -XX:+UnlockDiagnosticVMOptions -XX:G1NumCollectionsKeepPinned=10000000 -XX:+UseG1GC -Djava.io.tmpdir=/tmp/elasticsearch-13971958964404181235 --add-modules=jdk.incubator.vector -XX:+HeapDumpOnOutOfMemoryError -XX:+ExitOnOutOfMemoryError -XX:HeapDumpPath=data -XX:ErrorFile=logs/hs_err_pid%p.log -Xlog:gc*,gc+age=trace,safepoint:file=logs/gc.log:utctime,level,pid,tags:filecount=32,filesize=64m -Xms389m -Xmx389m -XX:MaxDirectMemorySize=204472320 -XX:G1HeapRegionSize=4m -XX:InitiatingHeapOccupancyPercent=30 -XX:G1ReservePercent=15 --module-path /test/elasticsearch-8.14.1/lib --add-modules=jdk.net --add-modules=ALL-MODULE-PATH -m org.elasticsearch.server/org.elasticsearch.bootstrap.Elasticsearch
es 49075 0.0 0.0 55180 880 pts/0 Sl 05:09 0:00 /test/elasticsearch-8.14.1/modules/x-pack-ml/platform/linux-x86_64/bin/controller
es 49230 0.0 0.0 112828 968 pts/0 R+ 05:10 0:00 grep elastic
訪問:
http://IP:9200/
設定密碼(推薦新增)
上文配置的是沒有密碼的方案,倘若伺服器IP和埠對外暴露,這不是一種安全的行為。
注意,要部署叢集,各個節點密碼應當一致。
注意配置格式,key: value之間要留出空格,否則ES不識別對應的值。
vim es根目錄/config/elasticsearch.yml
修改以下配置
xpack.security.enabled: true
非root使用者下啟動es
./bin/elasticsearch -d
啟動一個互動式命令列介面,從而設定密碼,期間的幾個互動,全部設定為123456
./bin/elasticsearch-setup-passwords interactive
預設使用者名稱:elastic
密碼:123456
概念輔助類比
ES中有些新的概念,可透過MySQL的概念去輔助記憶。
ES | MySQL | 備註 |
---|---|---|
Index(索引) | 庫表 | / |
Type(型別) | 表 | 7及以上的版本被移除,原先是對標MySQL表的理念,後來發現這對於ES並非必須,就移除了 |
Documents(文件) | 行資料 | / |
Fields(欄位) | 欄位 | / |
Mapping(對映) | 表結構 | / |
Shards(分片) | 分表 | 顧名思義,當資料量太大單個節點都裝不下的時候,就拆分到其它節點上 |
預設頁說明
- 預設頁:
GET請求IP:9200/
{
"name": "lnmp",
"cluster_name": "elasticsearch",
"cluster_uuid": "k61PBMDqTKO31rZeV-ENGA",
"version": {
"number": "8.14.1",
"build_flavor": "default",
"build_type": "tar",
"build_hash": "93a57a1a76f556d8aee6a90d1a95b06187501310",
"build_date": "2024-06-10T23:35:17.114581191Z",
"build_snapshot": false,
"lucene_version": "9.10.0",
"minimum_wire_compatibility_version": "7.17.0",
"minimum_index_compatibility_version": "7.0.0"
},
"tagline": "You Know, for Search"
}
"name": "lnmp":系統標識
"cluster_name": "elasticsearch":Elasticsearch 叢集的名稱為 “elasticsearch”。
"cluster_uuid": "k61PBMDqTKO31rZeV-ENGA":Elasticsearch叢集的唯一識別符號。
"version":版本資訊:
"number": 版本號
"build_flavor": "default":構建的型別,這裡是預設的。
"build_type": "tar":構建型別為 tar 包。
"build_hash": "93a57a1a76f556d8aee6a90d1a95b06187501310":構建的雜湊值,用於唯一標識這個特定的構建。
"build_date": "2024-06-10T23:35:17.114581191Z":構建的日期和時間。
"build_snapshot": false:表示這個構建不是一個快照版本。
"lucene_version": "9.10.0":基於Lucene 9.10.0的版本。
"minimum_wire_compatibility_version": "7.17.0":最低相容的網路傳輸版本。
"minimum_index_compatibility_version": "7.0.0":最低相容的索引版本。
"tagline": "You Know, for Search":Elasticsearch 的標語,說明其用途是進行搜尋。
索引增刪查操作
- 建立索引:
PUT請求 IP:9200/索引名
{
"acknowledged": true,
"shards_acknowledged": true,
"index": "zs_index"
}
"acknowledged": true:指示請求是否被成功接受和處理。
"shards_acknowledged": true:指示所有分片是否已經確認請求。
"index": "zs_index":這表示操作涉及的索引名稱為 “zs_index”。
- 建立索引:
重複建立,報錯說明:
{
"error": {
"root_cause": [
{
"type": "resource_already_exists_exception",
"reason": "index [zs_index/dCMAgdlqTeaihB4JSH1gNw] already exists",
"index_uuid": "dCMAgdlqTeaihB4JSH1gNw",
"index": "zs_index"
}
],
"type": "resource_already_exists_exception",
"reason": "index [zs_index/dCMAgdlqTeaihB4JSH1gNw] already exists",
"index_uuid": "dCMAgdlqTeaihB4JSH1gNw",
"index": "zs_index"
},
"status": 400
}
"error":這個物件包含了發生的錯誤資訊。
"root_cause":根本原因的陣列,指示導致問題的具體原因。
"type": "resource_already_exists_exception":錯誤的型別,表示嘗試建立的索引已經存在。
"reason": "index [zs_index/dCMAgdlqTeaihB4JSH1gNw] already exists":錯誤的詳細原因,指明索引 “zs_index” 和其唯一識別符號 “dCMAgdlqTeaihB4JSH1gNw” 已經存在。
"index_uuid": "dCMAgdlqTeaihB4JSH1gNw":已存在索引的 UUID。
"index": "zs_index":已存在索引的名稱。
"type": "resource_already_exists_exception":總體錯誤型別,與根本原因相同。
"reason": "index [zs_index/dCMAgdlqTeaihB4JSH1gNw] already exists":再次指明索引已經存在的原因。
"index_uuid": "dCMAgdlqTeaihB4JSH1gNw":重複指定已存在索引的 UUID。
"index": "zs_index":重複指定已存在索引的名稱。
"status": 400:HTTP 狀態碼,表示客戶端請求錯誤
- 檢視索引:
GET請求 IP:9200/索引名
{
"zs_index": {
"aliases": {},
"mappings": {},
"settings": {
"index": {
"routing": {
"allocation": {
"include": {
"_tier_preference": "data_content"
}
}
},
"number_of_shards": "1",
"provided_name": "zs_index",
"creation_date": "1719699272706",
"number_of_replicas": "1",
"uuid": "dCMAgdlqTeaihB4JSH1gNw",
"version": {
"created": "8505000"
}
}
}
}
}
"aliases": {}:索引的別名列表為空,表示該索引當前沒有別名。
"mappings": {}:索引的對映為空物件,即沒有定義特定的欄位對映。
"settings":索引的設定資訊:
"index":
"routing":
"allocation":
"include":
"_tier_preference": "data_content":指定索引分配時偏好的資料內容層級。
"number_of_shards": "1":該索引被分成了一個分片。
"provided_name": "zs_index":索引的提供的名稱為 “zs_index”。
"creation_date": "1719699272706":索引的建立日期的時間戳形式。
"number_of_replicas": "1":該索引有一個副本。
"uuid": "dCMAgdlqTeaihB4JSH1gNw":索引的唯一識別符號 UUID。
"version":
"created": "8505000":索引的版本資訊,表示索引在 Elasticsearch 版本 “8505000” 中建立。
- 檢視所有索引:
GET請求 IP:9200/_cat/indices?v
health status index uuid pri rep docs.count docs.deleted store.size pri.store.size dataset.size
yellow open zs_index dCMAgdlqTeaihB4JSH1gNw 1 1 0 0 249b 249b 249b
health: 索引的健康狀態,此處為 “yellow”,表示所有預期的分片都可用,但副本尚未分配。
status: Elasticsearch 的狀態指示符,這裡是 “open”,表示索引是開啟狀態,可以接收讀寫操作。
index: 索引名。
uuid: 索引的唯一識別符號。
pri: 主分片數為 1,即索引被分成了一個主分片。
rep: 副本數為 1,表示每個主分片有一個副本。
docs.count: 文件數量為 0,當前索引中的文件總數。
docs.deleted: 已刪除的文件數量為 0。
store.size: 儲存大小為 249b,索引佔用的物理儲存空間。
pri.store.size: 主分片的儲存大小,也是 249b。
dataset.size: 資料集大小為 249b,即索引的資料集大小。
- 刪除索引 DELETE方式 IP:9200/索引名
{
"acknowledged": true
}
返回true表示成功執行。
文件增刪改查操作
- 增文件(資料):
方式1:POST請求 IP:9200/索引名/_doc/可選引數,資料唯一標識
方式2:PUT請求 IP:9200/索引名/_create/必填唯一識別符號 由於方式2的put請求是冪等,所以再次請求會報錯
這是存入的資料
{
"id":1,
"content":"C是世界上最好的程式語言"
}
這是返回的資料,若使用者指定id,則id處顯示的是使用者指定的id
{
"_index": "zs_index",
"_id": "0mMsZpABZdTHCHXLZQhu",
"_version": 1,
"result": "created",
"_shards": {
"total": 2,
"successful": 1,
"failed": 0
},
"_seq_no": 0,
"_primary_term": 1
}
"_index": "zs_index": 表示文件被新增到了名為zs_index的索引中。
"_id": "0mMsZpABZdTHCHXLZQhu": 是新新增的文件的ID。在Elasticsearch中,每個文件都有一個唯一的ID,用於唯一標識和檢索該文件。
"_version": 1: 表示該文件的版本號是1。每當文件被更新時,版本號會增加,這有助於跟蹤文件的更改歷史。
"result": "created": 表示操作的結果是建立了一個新的文件。
"_shards": 這個欄位提供了關於索引操作的分片資訊。
"total": 2: 表示總共有2個分片參與了這次索引操作(通常是一個主分片和其副本)。
"successful": 1: 表示有1個分片成功完成了索引操作。在yellow健康狀態的索引中,這通常意味著主分片成功了,但副本分片可能還沒有資料(因為它是yellow狀態,副本可能還沒有分配或同步)。
"failed": 0: 表示沒有分片失敗。
"_seq_no": 0: 是文件在Lucene段中的序列號,用於在內部跟蹤文件的版本和順序。
"_primary_term": 1: 主要術語(primary term)是與_seq_no一起使用的,用於確保文件版本的一致性,特別是在主節點更換時。
- 改文件(資料):
方式1(用於覆蓋老資料):POST請求 IP:9200/索引名/_doc/唯一標識
方式2(用於覆蓋老資料):PUT請求 IP:9200/索引名/_doc/唯一標識
方式3(用於修改區域性資料):POST請求 IP:9200/索引名/_update/唯一標識
方式1,若有id號,再次執行增文件操作,可自動將create操作程式設計update操作。
更新資料
{
"id":1,
"content":"C是世界上最好的程式語言"
}
方式2,請求內容同方式1
方式3,因為要修改區域性資料,所以必須告知ES修改那塊的區域性資料,以下:第一層花括號和doc是固定格式。
{
"doc" : {
"content": "C是最好的程式語言"
}
}
3種方式的響應格式一致:
{
"_index": "zs_index",
"_id": "1",
"_version": 17,
"result": "updated",
"_shards": {
"total": 2,
"successful": 1,
"failed": 0
},
"_seq_no": 24,
"_primary_term": 1
}
"_index": "zs_index": 表示被更新的文件位於名為zs_index的索引中。
"_id": "1": 是被更新的文件的唯一ID。
"_version": 17: 表示該文件的版本號已更新為17。版本號在每次更新時增加,用於跟蹤文件的變化歷史。
"result": "updated": 表示更新操作已成功執行,文件被更新了。
"_shards": 提供了關於更新操作涉及的分片資訊。
"total": 2: 表示總共有2個分片參與了更新操作(通常是一個主分片和其副本)。
"successful": 1: 表示有1個分片成功完成了更新操作。在yellow健康狀態的索引中,這意味著主分片成功了,但副本分片可能尚未同步資料。
"failed": 0: 表示沒有分片失敗。
"_seq_no": 24: 是文件在Lucene段中的序列號,用於內部跟蹤文件版本和順序。
"_primary_term": 1: 主要術語(primary term)與_seq_no一起使用,確保文件版本的一致性,特別是在主節點更換時。
- 查詢單條資料:
GET請求 IP:9200/索引名/_doc/唯一標識
{
"_index": "zs_index",
"_id": "1",
"_version": 8,
"_seq_no": 14,
"_primary_term": 1,
"found": true,
"_source": {
"id": 1,
"content": "C是世界上最好的程式語言"
}
}
"_seq_no": 14: 是文件在Lucene段中的序列號,用於內部跟蹤文件版本和順序。
"_primary_term": 1: 主要術語(primary term)與_seq_no一起使用,確保文件版本的一致性,尤其是在主節點更換時。
"found": true: 表示Elasticsearch成功找到了指定ID的文件,若為false,表示未找到。
"_source": 包含了文件的實際內容。
- 查詢多條資料:
GET請求 IP:9200/索引名/_search
{
"took": 137,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 8,
"relation": "eq"
},
"max_score": 1,
"hits": [
{
"_index": "zs_index",
"_id": "1",
"_score": 1,
"_source": {
"id": 1,
"content": "C是世界上最好的程式語言"
}
},
{
"_index": "zs_index",
"_id": "02M0ZpABZdTHCHXLjAgN",
"_score": 1,
"_source": {
"id": 1,
"content": "C是世界上最好的程式語言"
}
}
]
}
}
"took": 137: 表示搜尋操作耗費了137毫秒。
"timed_out": false: 表示搜尋操作未超時。
"_shards": 提供了關於搜尋操作涉及的分片資訊。
"total": 1: 表示總共有1個分片參與了搜尋操作。
"successful": 1: 表示所有參與的分片都成功完成了搜尋。
"skipped": 0: 表示沒有分片被跳過。
"failed": 0: 表示沒有分片失敗。
"hits": 包含了搜尋結果的詳細資訊。
"total": {"value": 8, "relation": "eq"}: 表示符合搜尋條件的文件總數為8個。
"value": 8: 具體的文件數。
"relation": "eq": 表示與總數值相等,即已經獲取了所有匹配的文件。
"hits"陣列: 包含了每個匹配文件的詳細資訊。
每個文件物件包括了:
"_index": "zs_index": 文件所屬的索引名稱。
"_id": 文件的唯一ID。
"_score": 1: 文件的匹配分數,此處為1(最高分)。
"_source": 包含了文件的實際內容。
- 刪除資料
DELETE請求 IP:9200/索引名/_doc/唯一標識
{
"_index": "zs_index",
"_id": "1",
"_version": 24,
"result": "not_found",
"_shards": {
"total": 2,
"successful": 1,
"failed": 0
},
"_seq_no": 31,
"_primary_term": 1
}
"result": "not_found": 表示更新操作未找到指定的文件,若是deleted,表示成功刪除。
_shards": 提供了關於更新操作涉及的分片資訊。
"total": 2: 表示總共有 2 個分片參與了更新操作(通常是一個主分片和其副本)。
"successful": 1: 表示有 1 個分片成功完成了更新操作。在索引狀態為 yellow 時,這可能意味著主分片成功了,但副本分片可能尚未同步資料。
"failed": 0: 表示沒有分片失敗。
"_seq_no": 31: 是文件在 Lucene 段中的序列號,用於內部跟蹤文件版本和順序。
"_primary_term": 1: 主要術語(primary term)與 _seq_no 一起使用,確保文件版本的一致性,特別是在主節點更換時。
文件複雜查詢操作
- 透過關鍵詞查詢:
方式1:GET請求 IP:9200/索引名/_search?q=文件欄位名:要搜尋的關鍵字
方式2:GET請求 IP:9200/索引名/_search
並新增請求body{ "query":{ "match": { "文件欄位名":"要搜尋的關鍵字" } } }
{
"took": 8,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 6,
"relation": "eq"
},
"max_score": 0.074107975,
"hits": [
{
"_index": "zs_index",
"_id": "0mMsZpABZdTHCHXLZQhu",
"_score": 0.074107975,
"_source": {
"id": 1,
"content": "C是世界上最好的程式語言"
}
}
]
}
}
took: 查詢花費的時間,單位為毫秒。在這個例子中,值為8,表示查詢執行花費了8毫秒時間。
timed_out: 表示查詢是否超時。在這個例子中,值為false,表示查詢未超時。
_shards: 分片相關資訊,包括:
total: 總分片數,這裡是1個分片。
successful: 成功的分片數,這裡是1個分片。
skipped: 被跳過的分片數,這裡是0個分片。
failed: 失敗的分片數,這裡是0個分片。
hits: 查詢命中的結果集資訊,包含:
total: 總命中數,這裡是6。
max_score: 結果集中最高得分,這裡是0.074107975。
hits: 包含具體的命中文件陣列。
每個文件包含以下資訊:
_index: 文件所在的索引。
_id: 文件的唯一識別符號。
_score: 文件的得分。
_source: 儲存實際資料的欄位。
-
分頁查詢:
GET請求 IP:9200/索引名/_search
body體新增{ "query": { "match": { "文件欄位名":"要搜尋的關鍵字" } }, "from":0, "size":2 }
其中,from為起始位置偏移量,size為每頁顯示的條數。
from演算法:(頁碼 -1)* size = form。
第1頁:(1 - 1)* 2 = 0,所以from為0。
第2頁:(2 - 1)* 2 = 2,所以from為2。
響應結果同上。 -
只顯示資料的部分欄位:
GET請求 IP:9200/索引名/_search
body體新增_source項即可{ "query": { "match": { "文件欄位名":"要搜尋的關鍵字" } }, "_source":["id"] }
響應結果同上。 -
排序:
GET請求 IP:9200/索引名/_search
body體新增sort項即可{ "query": { "match": { "文件欄位名":"要搜尋的關鍵字" } }, "sort":{ "排序的欄位名":{ "order":"asc" } } }
注意,這個將要排序的欄位,可以不被展示出來也能排序(_source控制項)
響應結果同上。 -
多條件and或or查詢,區間查詢
GET請求 IP:9200/索引名/_search
如下,需新增以下body,表示查詢content欄位為C語言和(&&)C++語言(C++語言會被拆分),並且content>1(隨意測試)的資料。
若替換must為should,則表示或(or)之意。
{
"query": {
"bool": {
"must": [
{
"match": {
"content": "C語言"
}
},
{
"match": {
"content": "C++語言"
}
}
],
"filter": {
"range": {
"content": {
"gt": 1
}
}
}
}
}
}
響應結果同上。
- 全文精準匹配
GET請求 IP:9200/索引名/_search
仍需新增如下body{ "query":{ "match_phrase" :{ "欄位名":"要搜尋的關鍵字" } } }
響應結果同上。 - 查詢到的結果高亮顯示
GET請求 IP:9200/索引名/_search
仍需新增如下body{ "query":{ "match_phrase" :{ "欄位名":"要搜尋的關鍵字" } }, "highlight":{ "fields":{ "欄位名":{} } } }
響應結果同上。
聚合查詢
- 求指定欄位平均值
由於聚合函式過多,逐一說明會讓篇幅變的很長,因此推薦看官方文件:https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics.html
GET請求 IP:9200/索引名/_search
仍需新增如下body{ "aggs" : { "id_group_avg" : { "avg" : { "field" : "欄位名" } } }, "size":0 }
其中,id_group_avg為自定義名稱,size:0表示去掉對文件資料的返回。
{
"took": 35,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 6,
"relation": "eq"
},
"max_score": null,
"hits": []
},
"aggregations": {
"id_group_avg": {
"value": 1
}
}
}
took: 查詢花費的時間,單位是毫秒。這裡是 35 毫秒。
timed_out: 查詢是否超時。這裡顯示為 false,表示查詢在規定時間內完成。
_shards: 這個物件提供關於查詢在分片上的執行情況的詳細資訊:
total: 總分片數。
successful: 成功完成查詢的分片數。
skipped: 跳過的分片數。
failed: 查詢失敗的分片數。
在這個例子中,總分片數為 1,且成功完成了查詢。
hits: 包含有關查詢匹配的文件資訊:
total: 文件匹配的總數。
value: 匹配的文件數,這裡是 6。
relation: 匹配關係,這裡是 “eq” 表示精確匹配。
max_score: 最高得分,如果不需要計算得分則為 null。
hits: 實際匹配的文件陣列。在這個例子中是空的,因為沒有具體的文件資料。
aggregations: 聚合結果資訊:
id_group_avg: 聚合名稱,這裡的值為 1。具體的聚合結果會根據你的查詢和聚合定義而有所不同。
分詞與不分詞的控制
這塊由於涉及到欄位的改動,所以需要重新建立索引,並且新增了對映(mapping)的概念
重新建立一個people索引
PUT請求 IP:9200/people
再次請求,新增對映
IP:9200/people/_mapping
{
"properties" :{
"name" : {
"type":"text",
"index":true
},
"sex" : {
"type":"keyword",
"index":true
},
"tel" : {
"type":"keyword",
"index":false
}
}
}
上方的index指的是是否為這條資料新增索引。
type是索引型別,text代表支援分詞查詢(MySQL like '%kw%'),keyword代表不可分詞查詢 (MySQL = 'kw')。
然後新增三條資料
PUT IP:9200/people/_create/1
{
"name":"張三",
"sex":"男性",
"tel":"18888888888"
}
PUT IP:9200/people/_create/2
{
"name":"李四",
"sex":"女性",
"tel":"16666666666"
}
PUT IP:9200/people/_create/3
{
"name":"王五",
"sex":"男性",
"tel":"18866668888"
}
搜尋
GET IP:9200/people/_search
{
"query" :{
"match" :{
"sex" : "男" 把性去掉,搜尋不到資料
}
}
}
GET IP:9200/people/_search
{
"query" :{
"match" :{
"name" : "張" 把三去掉,可以搜尋到資料
}
}
}
GET IP:9200/people/_search
{
"query" :{
"match" :{
"tel" : "188" 若輸入手機號前3位,則搜不到資料,輸入完整的手機號,則可以搜尋到資料
}
}
}
PHP Api呼叫
官方文件:https://www.elastic.co/guide/en/elasticsearch/client/php-api/current/getting-started-php.html#_installation
某些ES Api(例如建立索引)不能重複執行,重複執行會報錯,所以在執行寫操作的上游做判斷,或者使用try catch。
composer require elasticsearch/elasticsearch
推薦安裝symfony/var-dumper,用於dd()或dump()執行,美化輸出。
新建PHP檔案,以下程式碼資料為公共部分。
include './vendor/autoload.php';
use Elastic\Elasticsearch\ClientBuilder;
//連線ES
$client = ClientBuilder::create()->setHosts(['192.168.0.183:9200'])->build();
//若es有密碼,則需要新增一個setBasicAuthentication()方法。
$client = ClientBuilder::create()->setHosts(['192.168.0.183:9200'])->setBasicAuthentication('elastic', '123456')->build();
PHP ES Api針對Index增刪改查
- 建立
返回bool
$response = $client->indices()->create([
'index' => 'php_index'
]);
$response->asBool();
- 查詢 判斷索引是否存在
返回bool
$response = $client->indices()->exists(['index' => 'php_index']);
dd($response->asBool());
- 查詢 檢視索引相關資訊
返回array
$response = $client->indices()->get(['index' => 'php_index']);
dd($response->asArray());
- 刪除
返回bool
$response = $client->indices()->delete(['index' => 'php_index']);
dd($response->asBool());
- 修改
索引作為基礎性的資料支撐,一般不做改動。
PHP ES Api針對Mapping增刪改查
- 型別可參考官方文件:https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-types.html
- 增 建立索引後
返回bool
$params = [
'index' => 'php_index',
'body' => [
'properties' => [
'name' => [
'type' => 'text',
],
]
]
];
$response = $client->indices()->putMapping($params);
dd($response->asBool());
- 增 建立索引時
返回bool
$params = [
'index' => 'php_index',
'body' => [
'mappings' => [
'properties' => [
'title' => [
'type' => 'text',
],
'content' => [
'type' => 'text',
],
]
]
]
];
$response = $client->indices()->create($params);
dd($response->asBool());
- 查 所有索引
返回陣列
$response = $client->indices()->getMapping();
dd($response->asArray());
- 查 指定索引
返回陣列
$response = $client->indices()->getMapping(['index' => 'php_index']);
dd($response->asArray());
- 刪
請直接刪除索引。 - 改
請重新建立索引,在新索引基礎上做對映的修改。
PHP ES Api針對Doc增刪改
- 索引與對映如下:
準備四個直轄市的名稱,簡介,人口和麵積大小。
$params = [
'index' => 'php_index',
'body' => [
'mappings' => [
'properties' => [
'city' => [
'type' => 'keyword',
],
'description' => [
'type' => 'text',
],
'population' => [
'type' => 'integer'
],
'area' => [
'type' => 'integer'
],
]
]
]
];
$response = $client->indices()->create($params);
dd($response->asArray());
- 增 單條 請記憶這4個直轄市的資料儲存格式,下文基本每個演示都要用
一級陣列下有個id屬性,若省去,ES會預設給這條資料加一個id。不推薦。推薦使用MySQL的資料id作為ES的id。
返回bool
$params = [
'index' => 'php_index',
'id' => 1,
'body' => [
'id' => 1,
'city' => '北京市',
'description' => '北京市(Beijing),簡稱“京”,古稱燕京、北平,是中華人民共和國首都、直轄市、國家中心城市、超大城市, 國務院批覆確定的中國政治中心、文化中心、國際交往中心、科技創新中心, 中國歷史文化名城和古都之一,世界一線城市',
'population' => '2186',
'area' => '16411',
]
];
$response = $client->index($params);
dd($response->asBool());
再增加3條資料
$params = [
'index' => 'php_index',
'id' => 2,
'body' => [
'id' => 2,
'city' => '上海市',
'description' => '上海市(Shanghai City),簡稱“滬” ,別稱“申”,中華人民共和國直轄市、國家中心城市、超大城市、上海大都市圈核心城市、國家歷史文化名城 [206],是中國gcd的誕生地。上海市入圍世界Alpha+城市, 基本建成國際經濟、金融、貿易、航運中心,形成具有全球影響力的科技創新中心基本框架。截至2022年12月底,上海市轄16個區,107個街道、106個鎮、2個鄉。',
'population' => '2487',
'area' => '6341',
]
];
$params = [
'index' => 'php_index',
'id' => 3,
'body' => [
'id' => 3,
'city' => '天津市',
'description' => '天津市(Tianjin City),簡稱“津”,別稱津沽、津門,是中華人民共和國省級行政區、直轄市、國家中心城市、超大城市 [222],地處中華人民共和國華北地區,海河流域下游,東臨渤海,北依燕山,西靠首都北京市,其餘均與河北省相鄰。截至2023年10月,天津市共轄16個區。',
'population' => '1364',
'area' => '11966',
]
];
$params = [
'index' => 'php_index',
'id' => 4,
'body' => [
'id' => 4,
'city' => '重慶市',
'description' => '重慶市,簡稱“渝”, 別稱山城、江城,是中華人民共和國直轄市、國家中心城市、超大城市,國務院批覆的國家重要中心城市之一、長江上游地區經濟中心, 國際消費中心城市,全國先進製造業基地、西部金融中心、西部科技創新中心、 國際性綜合交通樞紐城市和對外開放門戶,轄38個區縣',
'population' => '3191',
'area' => '82400',
]
];
- 增 多條
返回陣列
//假設MySQL查詢出來的資料如下
$mysql_data = [
[
'id' => 1024,
'city' => 'xx市',
'description' => 'xxxx',
'population' => '6666',
'area' => '6666',
],
[
'id' => 1025,
'city' => 'yy市',
'description' => 'yyyy',
'population' => '8888',
'area' => '8888',
]
];
//由於ES插入的要求,需要將插入資料的格式轉化,為此可以封裝一個方法
function esBatchInsert($index_name, $mysql_data) {
$params = [];
foreach($mysql_data as $v) {
$params['body'][] = ['index' => ['_index' => $index_name, '_id' => $v['id']],];
$params['body'][] = $v;
}
return $params;
}
$response = $client->bulk(esBatchInsert('php_index', $mysql_data));
dd($response->asArray());
可根據返回的資料再次迴圈,排查失敗掉的漏網之魚
- 刪 單條
返回bool
$params = [
'index' => 'php_index',
'id' => '1025'
];
$response = $client->delete($params);
dd($response->asBool());
- 刪 多條
方式1:
返回mixed
for($i = 1000; $i < 1050; $i++) { //模擬要刪除這些資料
$params = [
'index' => 'php_index',
'id' => $i
];
if(! $client->exists($params)->asBool()) {
continue;
}
$response = $client->delete($params)->asBool();
if(! $response) {
//若刪除失敗,請新增其它操作,記錄日誌或存入佇列,進行重試或者人工介入
}
}
方式2:
返回mixed
for($i = 1000; $i < 1050; $i++) { //模擬要刪除這些資料
$params['body'][] = [
'delete' => [
'_index' => 'php_index',
'_id' => $i,
]
];
}
$response = $client->bulk($params)->asArray();
if ($response['errors']) {
foreach ($response['items'] as $item) {
if (isset($item['delete']['status']) && ($item['delete']['status'] != 200)) {
//若刪除失敗,請新增其它操作,記錄日誌或存入佇列,進行重試或者人工介入
}
}
} else {
echo "批次刪除成功!";
}
- 刪 文件的某個欄位
返回bool
$params = [
'index' => 'php_index',
'id' => 1,
'body' => [
'script' => [
'source' => 'ctx._source.remove(params.field)',
'params' => [
'field' => '要刪除的欄位名'
]
]
]
];
$response = $client->update($params);
dd($response->asBool());
- 改 直接修改
返回bool
$params = [
'index' => 'php_index',
'id' => 1,
'body' => [
'doc' => [
'city' => '北京' //這裡是要修改的欄位,把北京市改為北京
]
]
];
$response = $client->update($params);
dd($response->asBool());
- 改 自增
返回bool
官方文件演示有誤,請按照以下正確寫法。
$params = [
'index' => 'php_index',
'id' => 1,
'body' => [
'script' => [
//表示式
'source' => 'ctx._source.population += params.population', //給北京人口加4萬,population為自定義文件欄位,其餘字元固定寫法。
//表示式所使用的變數
'params' => [
'population' => 4
],
],
]
];
$response = $client->update($params);
dd($response->asBool());
- 改 若文件不存在,則插入
$params = [
'index' => 'php_index',
'id' => 60, //若id對應的文件不存在,則利用upsert段的資料,重新生成一個id為60的文件。
'body' => [
'doc' => [
'city' => '臺北市'
],
'upsert' => [
'append_field' => 1
],
]
];
$response = $client->update($params);
dd($response->asBool());
- 改 批次
//假設以下資料時資料表中查詢出來的欄位,要修改以下內容
$mysql_data = [
['id' => 1, 'city' => '北京'],
['id' => 2, 'city' => '上海'],
];
//可以封裝一個方法,格式化資料
function esBatchUpdate($index_name, $update_data) {
if(! $update_data) {
return [];
}
$arr = [];
foreach($update_data as $v) {
$arr[] = ['update' => ['_index' => $index_name, '_id' => $v['id']]];
unset($v['id']);
$arr[] = ['doc' => $v];
}
return ['body' => $arr];
}
$response = $client->bulk(esBatchUpdate('php_index', $mysql_data));
$response = $response->asArray();
//處理
if ($response['errors']) {
foreach ($response['items'] as $item) {
if (isset($item['update']['status']) && ($item['update']['status'] != 200)) {
//若刪除失敗,請新增其它操作,記錄日誌或存入佇列,進行重試或者人工介入
}
}
} else {
echo "批次刪除成功!";
}
- 改 追加新的欄位
$params = [
'index' => 'php_index',
'id' => '1',
'body' => [
'doc' => [
'new_field' => 'new_value'
],
]
];
$response = $client->update($params);
- 改 刪除某些欄位
返回bool
$params = [
'index' => 'php_index',
'id' => 1,
'body' => [
'script' => [
'source' => 'ctx._source.remove(params.field)',
'params' => [
'field' => '要刪除的欄位名'
]
]
]
];
$response = $client->update($params);
dd($response->asBool());
PHP ES Api針對Doc高階查詢
查詢關鍵詞官方文件:https://www.elastic.co/guide/en/elasticsearch/reference/current/term-level-queries.html
- 指定id查詢
返回string
$params = [
'index' => 'php_index',
'id' => 1,
];
$response = $client->get($params);
echo $response->asString();
得到以下結果
{
"_index": "php_index",
"_id": "1",
"_version": 1,
"_seq_no": 0,
"_primary_term": 1,
"found": true,
"_source": {
"id": 1,
"city": "北京市",
"description": "北京市(Beijing),簡稱“京”,古稱燕京、北平,是中華人民共和國首都、直轄市、國家中心城市、超大城市, 國務院批覆確定的中國政治中心、文化中心、國際交往中心、科技創新中心, 中國歷史文化名城和古都之一,世界一線城市",
"population": "2186",
"area": "16411"
}
}
- 查詢全部
返回array
$response['hits']['total']['value']可獲取條數
$params = [
'index' => 'php_index',
'body' => [
'query' => [
'match_all' => new StdClass
]
]
];
$response = $client->search($params);
dd($response->asArray());
- 指定指定部分id的資料。
返回陣列
$response['hits']['total']['value']可獲取條數
$params = [
'index' => 'php_index',
'body' => [
'query' => [
'ids' => [
'values' => [1, 2]
]
]
]
];
$response = $client->search($params);
dd($response->asArray());
- 分頁查詢
返回陣列
//傳輸的頁碼
$page = 2;
$size = 2;
//偏移量演算法
$offset = ($page -1 ) * $size;
$params = [
'index' => 'php_index',
'body' => [
'from' => $offset,
'size' => $size,
// 可以新增其他查詢條件
'query' => [
'match_all' => new \stdClass()
]
]
];
$response = $client->search($params);
dd($response->asArray());
- 返回指定欄位
返回陣列
$params = [
'index' => 'php_index',
'body' => [
'_source' => ['description'], //自定義欄位
'query' => [
'match_all' => new \stdClass()
]
]
];
$response = $client->search($params);
dd($response->asArray());
- 判斷是否存在
返回bool
$params = [
'index' => 'php_index',
'id' => 10
];
$response = $client->exists($params);
- 獲取條數
返回int
$params = [
'index' => 'php_index',
'body' => [
'query' => [
'match_all' => new StdClass
]
]
];
$response = $client->count($params);
dd($response->asArray()['count'] ?? 0);
- 高亮查詢(類比百度詞條對關鍵字的標紅行為)
返回string
echo "<style>em{color:red}</style>";
$params = [
'index' => 'php_index',
'body' => [
'query' => [
'match' => [
'description' => '北京' //返回該欄位含有北京或北或京的文字。
]
],
'highlight' => [
'fields' => [
'city' => ['pre_tags' => ['<em>'], 'post_tags' => ['</em>'],], //配置要高亮的欄位
'description' => ['pre_tags' => ['<em>'], 'post_tags' => ['</em>'],] //配置要高亮的欄位
]
]
]
];
$response = $client->search($params);
print_r($response->asString());
返回格式如下,具體要用那個欄位,看具體需求
<style>em{color:red}</style>
{
"took":13,
"timed_out":false,
"_shards":{
"total":1,
"successful":1,
"skipped":0,
"failed":0
},
"hits":{
"total":{
"value":2,
"relation":"eq"
},
"max_score":2.9070516,
"hits":[
{
"_index":"php_index",
"_id":"1",
"_score":2.9070516,
"_source":{
"id":1,
"city":"北京",
"description":"北京市(Beijing),簡稱“京”,古稱燕京、北平,是中華人民共和國首都、直轄市、國家中心城市、超大城市, 國務院批覆確定的中國政治中心、文化中心、國際交往中心、科技創新中心, 中國歷史文化名城和古都之一,世界一線城市",
"population":2198,
"area":"16411",
"new_field":"new_value"
},
"highlight":{
"description":["<em>北</em><em>京</em>市(Beijing),簡稱“<em>京</em>”,古稱燕<em>京</em>、<em>北</em>平,是中華人民共和國首都、直轄市、國家中心城市、超大城市, 國務院批覆確定的中國政治中心、文化中心、國際交往中心、科技創新中心, 中國歷史文化名城和古都之一"]
}
},
{
"_index":"php_index",
"_id":"3",
"_score":2.5460577,
"_source":{
"id":3,
"city":"天津市",
"description":"天津市(Tianjin City),簡稱“津”,別稱津沽、津門,是中華人民共和國省級行政區、直轄市、國家中心城市、超大城市 [222],地處中華人民共和國華北地區,海河流域下游,東臨渤海,北依燕山,西靠首都北京市,其餘均與河北省相鄰。截至2023年10月,天津市共轄16個區。",
"population":"1364",
"area":"11966"
},
"highlight":{
"description":["天津市(Tianjin City),簡稱“津”,別稱津沽、津門,是中華人民共和國省級行政區、直轄市、國家中心城市、超大城市 [222],地處中華人民共和國華<em>北</em>地區,海河流域下游,東臨渤海,<em>北</em>依燕山,西靠首都<em>北</em><em>京</em>市",",其餘均與河<em>北</em>省相鄰。"]
}
}
]
}
}
- 限量 可參考分頁邏輯(類比MySQL limit)
返回array
$params = [
'index' => 'php_index',
'body' => [
'query' => [
'match_all' => new stdClass
],
'from' => 0,
'size' => 1,
]
];
$response = $client->search($params);
dd($response->asArray());
- 定值查詢 (類比MySQL wher filed = ‘kw’)
keyword 或 integer 等非分詞欄位:可用 term 精確匹配。如果欄位是 text 型別,那麼 term 查詢無法找到預期的匹配結果。
text 型別並且你想要精確匹配,可以使用 match_phrase 查詢
方式1 針對integer欄位的精準匹配
$params = [
'index' => 'php_index',
'body' => [
'query' => [
'term' => [
'city' => '北京市' //北京或北或京無法查詢出指定資料
]
]
]
];
$response = $client->search($params);
dd($response->asArray());
- 分詞查詢(類比MySQL where filed like '%kw%' or filed like '%k%' or filed like '%w%')
方式1
返回array
這種方式僅支援text型別
$params = [
'index' => 'php_index',
'body' => [
'query' => [
'match' => [
'description' => '北京'
]
]
]
];
$response = $client->search($params);
dd($response->asArray());
方式2
返回array
非text型別,可手動分詞
$params = [
'index' => 'php_index',
'body' => [
'query' => [
'bool' => [
'should' => [ //or
[
'match' => ['city' => '北京']
],
[
'match' => ['city' => '北京市']
]
],
'minimum_should_match' => 1
//minimum_should_match 設定為 1,表示至少需要匹配一個 should 子句中的條件
]
]
]
];
$response = $client->search($params);
dd($response->asArray());
- 模糊匹配 (類比MySQL where filed like '%kw%')wildcard效能可能不如其它型別的查詢,如match查詢,因為wildcard查詢需要對每個文件的欄位值進行模式匹配
方式1,針對keyword mapping
返回array
$params = [
'index' => 'php_index',
'body' => [
'query' => [
'wildcard' => [
'city' => '*北京*' //*表示任意字元,?表示任意一個字元
]
]
]
];
$response = $client->search($params);
dd($response->asArray());
方式2,針對text mapping,並非嚴格意義上的MySQL where filed like '%kw%',而是 where filed like '%kw%' or filed like '%k%' or filed like '%w%'
返回array
$params = [
'index' => 'php_index',
'body' => [
'query' => [
'match' => [
'description' => '北京'
]
]
]
];
$response = $client->search($params);
dd($response->asArray());
- 字首查詢 (類比MySQL where filed like 'kw%')針對keyword型別的欄位有效
返回array
$params = [
'index' => 'php_index',
'body' => [
'query' => [
'prefix' => [
'city' => '北'
]
]
]
];
$response = $client->search($params);
- 字尾查詢 (類比MySQL where filed like '%kw')針對keyword欄位有效
$params = [
'index' => 'php_index',
'body' => [
'query' => [
'wildcard' => [
'city' => '*京市'
]
]
]
];
$response = $client->search($params);
- 區間查詢(類比MySQL where field <、<=、>、>=、between)
返回array
<是lt、<=是lte、>是gt、>=是gte
$params = [
'index' => 'php_index',
'body' => [
'query' => [
'range' => [
'area' => [ //面積大於1000平方千米的城市
'gt' => 1000
]
]
]
]
];
$response = $client->search($params);
dd($response->asArray());
返回array
between
$params = [
'index' => 'php_index',
'body' => [
'query' => [
'range' => [
'area' => [ //獲取面積大於1000平方千米,但在10000平方千米以內的城市資料
'gt' => 1000,
'lt' => 10000,
]
]
]
]
];
$response = $client->search($params);
dd($response->asArray());
- 正則匹配(類比MySQL where field regexp 'xxx')針對keyword欄位有效
$params = [
'index' => 'php_index',
'body' => [
'query' => [
'regexp' => [
'city' => '.*北京.*' //搜尋包含北京關鍵字的欄位
]
]
]
];
$response = $client->search($params);
dd($response->asArray());
.*: 匹配任意數量的任意字元
.: 匹配任意單個字元。
*: 匹配前面的元素零次或多次。
+: 匹配前面的元素一次或多次。
?: 匹配前面的元素零次或一次。
^: 匹配字串的開頭。
$: 匹配字串的結尾。
[...]: 匹配方括號中的任意字元。
{n}: 匹配前面的元素恰好 n 次。
{n,}: 匹配前面的元素至少 n 次。
{n,m}: 匹配前面的元素至少 n 次,但不超過 m 次。
- 取反查詢(類比MySQL where filed != 'kw')針對text型別的欄位無效
返回bool
$params = [
'index' => 'php_index',
'body' => [
'query' => [
'bool' => [
'must_not' => [
'term' => [
'city' => '北京市' //返回不是北京市的資料
]
]
]
]
]
];
$response = $client->search($params);
dd($response->asArray());
$params = [
'index' => 'php_index',
'body' => [
'query' => [
'bool' => [
'must_not' => [
'range' => [
'area' => [ //面積不小於1000平方千米的城市
'lt' => 1000
]
]
]
]
]
]
];
$response = $client->search($params);
dd($response->asArray());
- 多條件and查詢(類比MySQL where expression1 and expression2)
返回array
$params = [
'index' => 'php_index',
'body' => [
'query' => [
'bool' => [
'must' => [ //返回city欄位是北京市,並且描述帶有首都的資料
['term' => ['city' => '北京市']],
['match' => ['description' => '首都']],
]
]
]
]
];
$response = $client->search($params);
dd($response->asArray());
- 多條件or查詢(類比MySQL where expression1 or expression2)
返回array
$params = [
'index' => 'php_index',
'body' => [
'query' => [
'bool' => [
'should' => [ //查詢城市名北京市,或者描述含有滬的描述內容
['term' => ['city' => '北京市']],
['match' => ['description' => '滬']],
]
]
]
]
];
$response = $client->search($params);
dd($response->asArray());
- and 和 or 共同使用
$params = [
'index' => 'php_index',
'body' => [
'query' => [
'bool' => [ //查詢城市名為北京市或上海市,並且描述帶有京字的資料
'must' => [
[
'bool' => [
'should' => [
['term' => ['city' => '北京市']],
['term' => ['city' => '上海市']]
]
]
],
['match' => ['description' => '京']]
]
]
]
]
];
$response = $client->search($params);
dd($response->asArray());
- 排序(類比MySQL Order By)
單欄位排序
$params = [
'index' => 'php_index',
'body' => [
'query' => [
'match_all' => new StdClass,
],
'sort' => [ //四個直轄市資料按照區域大小排名
['area' => ['order' => 'asc']] //asc或desc
]
]
];
$response = $client->search($params);
dd($response->asArray());
多欄位排序
$params = [
'index' => 'php_index',
'body' => [
'query' => [
'match_all' => new StdClass,
],
'sort' => [ //區域按照降序,人口按照升序排,條件不會衝突,回想MySQL order by那樣,合併處理。
['area' => ['order' => 'asc']], //asc或desc
['population' => ['order' => 'desc']], //asc或desc
]
]
];
- 聚合函式(類比MySQL聚合函式)
返回bool
$params = [
'index' => 'php_index',
'body' => [
'size' => 0, // 設定為0表示不返回實際的文件,僅返回聚合結果
'aggs' => [
'population_data' => [ //這個key為自定義名稱
'avg' => [ //返回4個直轄市平均人口
'field' => 'population'
]
]
]
]
];
$response = $client->search($params);
dd($response->asArray());
avg : 平均值
sum :總和
min : 最小值
max :最大值
沒有count。
- 分組(類比MySQL Group By)
返回string
$params = [
'index' => 'php_index',
'body' => [
'size' => 0, // 不返回文件,只返回聚合結果
'aggs' => [
'city_group' => [ //自定義名稱
'terms' => [
'field' => 'city',
'size' => 10 // 聚合結果的數量限制
]
]
]
]
];
$response = $client->search($params)->asArray();
$aggregations = $response['aggregations']['city_group']['buckets'];
foreach ($aggregations as $bucket) {
echo "城市名:" . $bucket['key'] . " - 本組組對應的數量:" . $bucket['doc_count'] . "\n";
}
城市名:上海市 - 本組組對應的數量:1
城市名:北京市 - 本組組對應的數量:1
城市名:天津市 - 本組組對應的數量:1
城市名:重慶市 - 本組組對應的數量:1
- 合併(類比MySQL union)
用PHP array_merge實現吧,這對於ES不適用。 - 指定資料靠前(類比競價排名)
返回array
個人還是推薦使用自定義欄位,因為['hits']['_score']欄位得出來分數不可控。
搜尋城市,原先是北京靠前,現在透過修改權重,使其上海靠前
$params = [
'index' => 'php_index',
'body' => [
'query' => [
'function_score' => [
'query' => [
'bool' => [
'should' => [
['term' => ['city' => '北京市']],
['match' => ['description' => '滬']]
]
]
],
'functions' => [
[
'filter' => [
'match' => ['description' => '滬']
],
'weight' => 2 // 增加包含“滬”的文件的權重
]
],
'boost_mode' => 'sum'
]
],
'sort' => [
'_score' => [
'order' => 'desc' // 按照得分降序排序
]
]
]
];
$response = $client->search($params);
dd($response->asArray());
boost_mode設定了如何將查詢的基礎得分(由 query 部分確定)與功能得分(由 functions 部分計算)進行組合。以下是幾種常用的 boost_mode 設定:
multiply: 基礎得分與功能得分相乘。
replace: 功能得分替代基礎得分。
sum: 基礎得分與功能得分相加。
avg: 基礎得分與功能得分的平均值。
max: 取基礎得分與功能得分中的最大值。
Painless
- 極簡概括:是一種簡單、安全的、服務於Elasticsearch的指令碼語言。類比Redis或Nginx中的Lua,某些元件嵌入指令碼語言用於實現複雜的邏輯,這並不罕見。
- 官方文件:https://www.elastic.co/guide/en/elasticsearch/painless/current/painless-guide.html
- 使用場景:針對ES,例如上文在更新文件中,請求文件script段中的source段,都用的painless表示式。
- 額外補充:由於painless語法內容過多,且比較簡單,整個記錄下來需要2萬字,成本問題,因此讀者推薦看手冊。
- 簡單舉例:
//counter子對岸自增
ctx._source.counter += params.count
//if else 判斷
if (ctx._source.someField > 10) {
ctx._source.anotherField = ctx._source.someField * params.multiplier;
} else {
ctx._source.anotherField = params.defaultValue;
}
IK中問分詞與高階索引建立
- 使用理由:ES預設的分詞器對中文不友好,英文分詞器會把中文每個字分開,因此需要專門的中文分詞器。
- 分詞器的服務物件是對映,而不是索引。
- 安裝:
關閉ES
執行以下程式碼,注意版本號的問題
bin/elasticsearch-plugin install https://get.infini.cloud/elasticsearch/analysis-ik/8.14.1
進入互動介面輸入Y。
之後啟動ES
- ik_max_word與ik_smart分詞精度控制演示:
演示分詞:
GET IP:9200/_analyze
傳入以下內容
{
"text":"射鵰英雄傳",
"analyzer":"ik_smart"
}
返回
{
"tokens": [
{
"token": "射鵰英雄傳",
"start_offset": 0,
"end_offset": 5,
"type": "CN_WORD",
"position": 0
}
]
}
若使用ik_max_word
{
"text":"射鵰英雄傳",
"analyzer":"ik_max_word"
}
則返回
{
"tokens": [
{
"token": "射鵰英雄傳",
"start_offset": 0,
"end_offset": 5,
"type": "CN_WORD",
"position": 0
},
{
"token": "射鵰",
"start_offset": 0,
"end_offset": 2,
"type": "CN_WORD",
"position": 1
},
{
"token": "英雄傳",
"start_offset": 2,
"end_offset": 5,
"type": "CN_WORD",
"position": 2
},
{
"token": "英雄",
"start_offset": 2,
"end_offset": 4,
"type": "CN_WORD",
"position": 3
},
{
"token": "傳",
"start_offset": 4,
"end_offset": 5,
"type": "CN_CHAR",
"position": 4
}
]
}
- 配置自定義分詞:
有些場景,有很多的專業用語,但是IK分詞器把它拆分開,就顯得不是很精準,因此可以新增自定義分詞解決。
vim ES安裝目錄/config/analysis-ik/IKAnalyzer.cfg.xml
在<entry key="ext_dict"></entry>的雙標籤中間寫入檔名,例如
<entry key="ext_dict">self_words.dic</entry>
vim self_words.dic
逐行新增自定義詞彙
重啟ES。
- PHP使用:
返回bool
$params = [
'index' => 'test_index',
'body' => [
'settings' => [
'analysis' => [
'analyzer' => [
'analyzer_ik_max_word' => [
'type' => 'ik_max_word' //ik分詞器內建關鍵配置,更多的分詞結果
],
'analyzer_ik_smart' => [
'type' => 'ik_smart' //ik分詞器內建關鍵配置,更快的分詞結果
]
]
]
],
'mappings' => [
'properties' => [
'content' => [
'type' => 'text',
'analyzer' => 'analyzer_ik_smart', // 設定索引時的分詞器
'search_analyzer' => 'analyzer_ik_smart' // 設定搜尋時的分詞器
]
]
]
]
];
$response = $client->indices()->delete(['index' => 'test_index']);
dump($response->asBool());
- 進階用法,新增過濾(不生效):
返回bool
這裡嘗試建立了一個更復雜的索引,新增了過濾器,但是不生效,不知道是那裡的問題。
如下,按照以下索引的配置,過濾後的結果,應當是"C世界上最好程式語言",然後再分詞,可卻不生效。
GET IP:9200/test_index/_analyze
{
"analyzer":"self_ik_max_word",
"text" : "PHP是世界上最好的程式語言"
}
$params = [
'index' => 'test_index', // 指定要建立的索引名稱
'body' => [
'settings' => [ // 配置索引的設定
'analysis' => [ // 分析器設定
'char_filter' => [ // 字元過濾器設定
'self_char_filter' => [ // 自定義字元過濾器名稱
'type' => 'mapping', // 過濾器型別為對映
'mappings' => ['PHP => C'] // 替換分詞的字元
]
],
'filter' => [ // 過濾器設定
'self_filter' => [ // 自定義停用詞過濾器名稱
'type' => 'stop', // 過濾器型別為停用詞
'stopwords' => ['是', '的'] // 停用詞列表
]
],
'analyzer' => [ // 分析器設定
'self_ik_max_word' => [ // IK 分詞器名稱
'type' => 'ik_max_word', // 使用 IK 分詞器的最大分詞模式
'char_filter' => ['html_strip', 'self_char_filter'], // html_strip過濾器會把html標籤忽略,但html跳脫字元仍舊生效( 仍舊是空格),且會把<br/>轉化為\n
'filter' => ['lowercase', 'self_filter'] //lowercase過濾器是將大寫字母變為小寫
],
'self_ik_smart' => [ // IK 分詞器名稱
'type' => 'ik_smart', // 使用 IK 分詞器的快速分詞模式
'char_filter' => ['html_strip', 'self_char_filter'], // html_strip過濾器會把html標籤忽略,但html跳脫字元仍舊生效( 仍舊是空格),且會把<br/>轉化為\n
'filter' => ['lowercase', 'self_filter'] //lowercase過濾器是將大寫字母變為小寫
]
]
]
],
'mappings' => [ // 配置索引的對映
'properties' => [ // 文件欄位的屬性設定
'content' => [ // 文件中的欄位名稱
'type' => 'text', // 欄位型別為文字
'analyzer' => 'self_ik_max_word', // 設定索引時的分詞器
'search_analyzer' => 'self_ik_max_word' // 設定搜尋時的分詞器
]
]
]
]
];
$response = $client->indices()->create($params);
dd($response->asBool());
ELK
- 概念:ES結合LK作為ELK(Elasticsearch(搜尋), Logstash(採集轉換), Kibana(分析))組合,可用於實時監控、分析和視覺化大量日誌和事件資料,如系統日誌、應用程式日誌、網路流量日誌等。
- 構成
- Elasticsearch:一個分散式搜尋引擎,提供強大的搜尋功能和實時的資料分析能力。
- Logstash:一個資料處理管道,用於收集、解析和轉發日誌資料。
- Kibana:一個資料視覺化工具,幫助使用者透過圖形化介面檢視和分析 Elasticsearch 中的資料。
- 作用:
- 日誌管理:集中化日誌收集:透過Logstash收集來自不同系統和應用的日誌,統一儲存在Elasticsearch中。
- 日誌分析:利用Kibana對日誌資料進行實時分析和視覺化,幫助發現系統問題和異常。
- 實時監控:跟蹤應用程式的效能指標,實時檢視應用的健康狀況。
- 效能瓶頸檢測:透過分析日誌資料,識別和解決效能瓶頸。
- 安全事件分析:監控和分析系統中的安全事件,檢測異常行為。
- 合規性審計:記錄和分析系統日誌,以滿足合規性要求。
- 資料視覺化:透過Kibana建立各種圖表和儀表盤,幫助業務分析師理解資料趨勢和模式。
- 使用者行為分析:分析使用者的操作日誌,最佳化使用者體驗和產品設計。
- 伺服器監控:跟蹤伺服器的效能指標,如CPU使用率、記憶體使用情況和磁碟空間。
- 應用狀態監控:監控應用程式的執行狀態和日誌,以確保正常執行。
- 問題診斷:利用Elasticsearch儲存的日誌資料,快速定位和解決系統故障。
- 根因分析:分析相關日誌,幫助找到問題的根本原因。
- 對於PHP而言:幾乎用不到,這是Java和大資料方向的。
Kibana
- 極簡概括:開源的視覺化控制ES的元件。
- 官方文件:https://www.elastic.co/guide/en/kibana/current/index.html
- 安裝:
保證ES服務已啟動。
防火牆開啟5601埠
firewall-cmd --add-port=5601/tcp --zone=public --permanent
systemctl restart firewalld
下載與解壓
curl -O https://artifacts.elastic.co/downloads/kibana/kibana-8.14.1-linux-x86_64.tar.gz
tar zxf kibana-8.14.1-linux-x86_64.tar.gz
許可權配置
chown -R es:es kibana-8.14.1
切換使用者
su es
kibana不支援elastic使用者,所以需要建立新使用者,並賦予超級管理員角色,並賦予kibana_system角色
elasticsearch-users useradd zs
elasticsearch-users roles -a superuser zs
少了這一步報錯,讓我搞了4個小時。
elasticsearch-users roles -a kibana_system zs
修改配置
vim kibana-8.14.1/config/kibana.yml
elasticsearch.username: "zs"
elasticsearch.password: "123456"
elasticsearch.hosts: ["ES IP:9200"]
i18n.locale: "zh-CN"
啟動
kibana-8.14.1/bin/kibana
過2分鐘後,訪問http://IP:5601
4種和資料庫同步方案
- 不妨先講一講業務層是怎麼使用ES的讀功能的:
以電商系統為例,用到ES的原因,一個是商品數量龐大,一個是分詞有助於展示更好的結果,上架的商品因為關鍵詞誤差搜不到,這就是損失。
例如商品列表資料的展示,可將價格,名稱,描述,主圖片,標籤,id等其他資料存入ES,然後展示。
當使用者點選某個商品時,根據id進行雜湊運算,獲取商品資料在那個MySQL分表中,利用id主鍵索引極速查詢的特性,快速獲取商品資料。 - 同步雙寫:MySQL和ES同步更新
- 優點:實現簡單,實時性高。
- 缺點:耦合度高,其中一個元件異常可能會影響另一個。
- 非同步雙寫:先同步MySQL,再用MQ同步ES。
- 優點:優雅,由於MQ(非Redis實現的MQ)具有高可用機制,因此ES消費失敗可以重試。
- 缺點:多了一個MQ,就多了一層運維成本。有延遲。
- 自動化任務,定時遍歷SQL:用時間戳做識別符號,用於區分哪些資料未同步,沒有同步就用指令碼定時同步到ES。
- 優點:業務邏輯層不需要額外的針對ES做寫操作。
- 缺點:實時性不夠,對MySQL壓力大。
- 使用Canal基於Binlog進行接近實時的同步,使用Canal監聽MySQL Binlog,並部署同步ES資料的指令碼,從而自動化保持同步。也可直接利用Canal同步ES。相關連結:https://github.com/alibaba/canal/wiki/Sync-ES。
- 優點:實時性高,對業務層程式碼無侵入。
- 缺點:多了一個Canal,就多了一層運維成本。
高併發下ES本身一致性解決方案
- 問題:與上文講的資料庫一致性,不是一個東西。這裡講的是併發下ES本身更新資料導致的一致性問題。例如併發過來的兩個請求,查詢到結果是10,都想要-1,等兩個執行完畢後,結果不是8而是9,那麼就出現了資料一致性問題。
- ES之外的解決方案:分散式鎖。或非分散式環境下程式語言自帶的具有排它性的鎖。
- ES樂觀鎖解決方案1:
背景:先建立一個num_test索引,並新增名為num的int型別的對映。並插入一條資料。
流程:當進行資料更新時,先做一次查詢(get方法,不是search方法),獲取相關的_primary_term,_seq_no值。
當更新資料時,新增對應的版本號,如果ES檢測到版本號不對,則會報錯,如下:
$params = [
'index' => 'num_test',
'id' => 1,
'body' => [
'doc' => [
'num' => 10
]
],
'if_seq_no' => 3, // 使用序列號
'if_primary_term' => 1, // 使用主分片術語
];
try {
$response = $client->update($params);
} catch (\Exception $exception) {
echo '出錯了,這裡重試查詢後再更新,或者記錄錯誤等其它操作。。。'
}
- ES樂觀鎖解決方案2(不生效,請勿使用):
$params = [
'index' => 'num_test',
'id' => 1,
'body' => [
'doc' => [
'num' => 1800
]
],
'version' => 40, // 提供外部版本號
'version_type' => 'external' // 使用外部版本號
];
try {
$response = $client->update($params); //版本不生效的方案,不推薦使用
} catch (\Exception $exception) {
dump('出錯了,這裡進行重試,或者記錄錯誤,等其它操作');
}
- ES應對高併發寫的報錯問題(和上文內容不是一回事):ES針對大量的併發過來的寫請求,ES支援的並不好,ES底層採用樂觀鎖的形式,這會導致ES內部在頻繁併發寫入時內部維護版本號衝突,也就是說更新前查詢出來的版本號,比當前實際的版本號小(被其它併發過來的請求增加了版本號),那就會報錯,這也就是所謂的ES報版本衝突的錯誤的問題,對於這種場景,可新增重試次數,和業務層的異常獲取作為兜底策略。重試程式碼示例如下:
$params = [
'index' => 'index',
'id' => '10',
'body' => [
'doc' => [
'field1' => 'new value1',
'field2' => 'new value2'
]
],
'retry_on_conflict' => 3 // 設定重試次數
];
try {
$response = $client->update($params);
} catch (Exception $e) {
// 處理異常,可以選擇記錄日誌或執行其它操作,這個catch是用來重試3次還報錯的兜底策略。
}
為什麼不用ES替代MySQL
- ES沒有MySQL的事務機制,高可用無法保證。
- ES沒有MySQL的關係型側重,MySQL有強大的關聯策略,MySQL join多張表時,ES需要手動實現。
- ES的定位是快速索引快速查詢,並非有過多高可用儲存的機制,還是需要配合MySQL使用。
EQL
- 極簡概括:Event Query Language用於在ES中進行事件資料查詢的類SQL語言。
- 解決問題:為了更方便地分析時間序列資料和事件流資料,特別適用於安全事件、日誌資料和監控資料的分析。
- 棄用原因:多用於快速除錯。畢竟ES不是MySQL,SQL API 並不是ES中所有功能的完整替代品,有些複雜的查詢和功能可能需要使用原生的ES查詢 DSL(ES領域或問題域設計的程式語言或語法)。
- 簡單示例:要查詢索引下的一條資料
POST IP:9200/_sql?format=json //型別可未txt,用製表符更直觀的展示
{
"query": "SELECT * FROM php_index WHERE city = '北京市'"
}
結果:
{
"columns": [
{
"name": "_boost",
"type": "float"
},
{
"name": "area",
"type": "integer"
},
{
"name": "city",
"type": "keyword"
},
{
"name": "description",
"type": "text"
},
{
"name": "id",
"type": "long"
},
{
"name": "population",
"type": "integer"
}
],
"rows": [
[
null,
16411,
"北京市",
"北京市(Beijing),簡稱“京”,古稱燕京、北平,是中華人民共和國首都、直轄市、國家中心城市、超大城市, 國務院批覆確定的中國政治中心、文化中心、國際交往中心、科技創新中心, 中國歷史文化名城和古都之一,世界一線城市",
1,
2186
]
]
}
- 演示2:查詢所有索引:
POST IP:9200/_sql?format=txt
{
"query": "show tables"
}
catalog | name | type | kind
---------------+--------------------------------------------------+---------------+---------------
zs_es_cluster |.alerts-default.alerts-default |VIEW |ALIAS
zs_es_cluster |.alerts-ml.anomaly-detection-health.alerts-default|VIEW |ALIAS
zs_es_cluster |.alerts-ml.anomaly-detection.alerts-default |VIEW |ALIAS
zs_es_cluster |.alerts-observability.apm.alerts-default |VIEW |ALIAS
zs_es_cluster |.alerts-observability.logs.alerts-default |VIEW |ALIAS
zs_es_cluster |.alerts-observability.metrics.alerts-default |VIEW |ALIAS
zs_es_cluster |.alerts-observability.slo.alerts-default |VIEW |ALIAS
zs_es_cluster |.alerts-observability.threshold.alerts-default |VIEW |ALIAS
zs_es_cluster |.alerts-observability.uptime.alerts-default |VIEW |ALIAS
zs_es_cluster |.alerts-security.alerts-default |VIEW |ALIAS
zs_es_cluster |.alerts-stack.alerts-default |VIEW |ALIAS
zs_es_cluster |.alerts-transform.health.alerts-default |VIEW |ALIAS
zs_es_cluster |.kibana-observability-ai-assistant-conversations |VIEW |ALIAS
zs_es_cluster |.kibana-observability-ai-assistant-kb |VIEW |ALIAS
zs_es_cluster |.siem-signals-default |VIEW |ALIAS
zs_es_cluster |my_index |TABLE |INDEX
zs_es_cluster |num_test |TABLE |INDEX
zs_es_cluster |php_index |TABLE |INDEX
zs_es_cluster |test_index |TABLE |INDEX
zs_es_cluster |zs_index |TABLE |INDEX