Elasticsearch是位於 Elastic Stack 核心的分散式搜索和分析引擎。Elasticsearch 是索引、搜索和分析魔法發生的地方。lasticsearch 為所有類型的數據提供近乎實時的搜索和分析。無論您擁有結構化或非結構化文本、數字數據還是地理空間數據,Elasticsear... ...
ES分散式搜索引擎
註意: 在沒有創建庫的時候搜索,ES會創建一個庫並自動創建該欄位並且設置為String類型也就是text
什麼是elasticsearch?
- 一個開源的分散式搜索引擎,可以用來實現搜索、日誌統計、分析、系統監控等功能
什麼是elastic stack(ELK)?
- 是以elasticsearch為核心的技術棧,包括beats、Logstash、kibana、elasticsearch
什麼是Lucene?
- 是Apache的開源搜索引擎類庫,提供了搜索引擎的核心API
elasticsearch是一款非常強大的開源搜索引擎,具備非常多強大功能,可以幫助我們從海量數據中快速找到需要的內容
ELK技術棧
本文只使用了elasticsearch,以及kibana做可視化界面
elasticsearch結合kibana、Logstash、Beats,也就是elastic stack(ELK)。被廣泛應用在日誌數據分析、實時監控等領域:
而elasticsearch是elastic stack的核心,負責存儲、搜索、分析數據。
初識elasticsearch
1. elasticsearch背景介紹
elasticsearch底層是基於lucene來實現的。
Lucene是一個Java語言的搜索引擎類庫,是Apache公司的頂級項目,由DougCutting於1999年研發。官網地址:https://lucene.apache.org/ 。
elasticsearch的發展歷史:
- 2004年Shay Banon基於Lucene開發了Compass
- 2010年Shay Banon 重寫了Compass,取名為Elasticsearch。
2. 倒排索引
倒排索引的概念是基於MySQL這樣的正向索引而言的。
2.1 正向索引
設置了索引的話挺快的,但要是模糊查詢則就很慢!
那麼什麼是正向索引呢?例如給下表(tb_goods)中的id創建索引:
如果是根據id查詢,那麼直接走索引,查詢速度非常快。
但如果是基於title做模糊查詢,只能是逐行掃描數據,流程如下:
1)用戶搜索數據,條件是title符合"%手機%"
2)逐行獲取數據,比如id為1的數據
3)判斷數據中的title是否符合用戶搜索條件
4)如果符合則放入結果集,不符合則丟棄。回到步驟1
逐行掃描,也就是全表掃描,隨著數據量增加,其查詢效率也會越來越低。當數據量達到數百萬時,就是一場災難。
2.2 倒排索引
倒排索引中有兩個非常重要的概念:
- 文檔(
Document
):用來搜索的數據,其中的每一條數據就是一個文檔。例如一個網頁、一個商品信息 - 詞條(
Term
):對文檔數據或用戶搜索數據,利用某種演算法分詞,得到的具備含義的詞語就是詞條。例如:我是中國人,就可以分為:我、是、中國人、中國、國人這樣的幾個詞條
創建倒排索引是對正向索引的一種特殊處理,流程如下:
- 將每一個文檔的數據利用演算法分詞,得到一個個詞條
- 創建表,每行數據包括詞條、詞條所在文檔id、位置等信息
- 因為詞條唯一性,可以給詞條創建索引,例如hash表結構索引
如圖:
倒排索引的搜索流程如下(以搜索"華為手機"為例):
1)用戶輸入條件"華為手機"
進行搜索。
2)對用戶輸入內容分詞,得到詞條:華為
、手機
。
3)拿著詞條在倒排索引中查找,可以得到包含詞條的文檔id:1、2、3。
4)拿著文檔id到正向索引中查找具體文檔。
如圖:
雖然要先查詢倒排索引,再查詢倒排索引,但是無論是詞條、還是文檔id都建立了索引,查詢速度非常快!無需全表掃描。
2.3 正向和倒排對比
概念區別:
-
正向索引是最傳統的,根據id索引的方式。但根據詞條查詢時,必須先逐條獲取每個文檔,然後判斷文檔中是否包含所需要的詞條,是根據文檔找詞條的過程。
-
而倒排索引則相反,是先找到用戶要搜索的詞條,根據詞條得到保護詞條的文檔的id,然後根據id獲取文檔。是根據詞條找文檔的過程。
優缺點:
正向索引:
- 優點:
- 可以給多個欄位創建索引
- 根據索引欄位搜索、排序速度非常快
- 缺點:
- 根據非索引欄位,或者索引欄位中的部分詞條查找時,只能全表掃描。
倒排索引:
- 優點:
- 根據詞條搜索、模糊搜索時,速度非常快
- 缺點:
- 只能給詞條創建索引,而不是欄位
- 無法根據欄位做排序
3. ES資料庫基本概念
elasticsearch中有很多獨有的概念,與mysql中略有差別,但也有相似之處。
3.1.文檔和欄位
一個文檔就像資料庫里的一條數據,欄位就像資料庫里的列
elasticsearch是面向文檔(Document)存儲的,可以是資料庫中的一條商品數據,一個訂單信息。文檔數據會被序列化為json格式後存儲在elasticsearch中:
而Json文檔中往往包含很多的欄位(Field),類似於mysql資料庫中的列。
3.2.索引和映射
索引就像資料庫里的表,映射就像資料庫中定義的表結構
索引(Index),就是相同類型的文檔的集合【類似mysql中的表】
例如:
- 所有用戶文檔,就可以組織在一起,稱為用戶的索引;
- 所有商品的文檔,可以組織在一起,稱為商品的索引;
- 所有訂單的文檔,可以組織在一起,稱為訂單的索引;
因此,我們可以把索引當做是資料庫中的表。
資料庫的表會有約束信息,用來定義表的結構、欄位的名稱、類型等信息。因此,索引庫中就有映射(mapping),是索引中文檔的欄位約束信息,類似表的結構約束。
3.3.mysql與elasticsearch
各自長處:
Mysql:擅長事務類型操作,可以確保數據的安全和一致性
Elasticsearch:擅長海量數據的搜索、分析、計算
我們統一的把mysql與elasticsearch的概念做一下對比:
MySQL | Elasticsearch | 說明 |
---|---|---|
Table | Index | 索引(index),就是文檔的集合,類似資料庫的表(table) |
Row | Document | 文檔(Document),就是一條條的數據,類似資料庫中的行(Row),文檔都是JSON格式 |
Column | Field | 欄位(Field),就是JSON文檔中的欄位,類似資料庫中的列(Column) |
Schema | Mapping | Mapping(映射)是索引中文檔的約束,例如欄位類型約束。類似資料庫的表結構(Schema) |
SQL | DSL | DSL是elasticsearch提供的JSON風格的請求語句,用來操作elasticsearch,實現CRUD |
在企業中,往往是兩者結合使用:
- 對安全性要求較高的寫操作,使用mysql實現
- 對查詢性能要求較高的搜索需求,使用elasticsearch實現
- 兩者再基於某種方式,實現數據的同步,保證一致性
4. 安裝es、kibana、分詞器
分詞器的作用是什麼?
- 創建倒排索引時對文檔分詞
- 用戶搜索時,對輸入的內容分詞
IK分詞器有幾種模式?
- ik_smart:智能切分,粗粒度
- ik_max_word:最細切分,細粒度
IK分詞器如何拓展詞條?如何停用詞條?
- 利用config目錄的IkAnalyzer.cfg.xml文件添加拓展詞典和停用詞典
- 在詞典中添加拓展詞條或者停用詞條
4.1 部署單點es
4.1.1.創建網路
因為我們還需要部署kibana容器,因此需要讓es和kibana容器互聯。這裡先創建一個網路:
docker network create es-net
4.1.2.載入鏡像
這裡我們採用elasticsearch的7.12.1版本的鏡像,這個鏡像體積非常大,接近1G。不建議大家自己pull。
課前資料提供了鏡像的tar包:
大家將其上傳到虛擬機中,然後運行命令載入即可:
# 導入數據
docker load -i es.tar
註意:同理還有kibana
的tar包也需要這樣做。
4.1.3.運行
運行docker命令,部署單點es:
docker run -d \
--name es \
-e "ES_JAVA_OPTS=-Xms512m -Xmx512m" \
-e "discovery.type=single-node" \
-v es-data:/usr/share/elasticsearch/data \
-v es-plugins:/usr/share/elasticsearch/plugins \
--privileged \
--network es-net \
-p 9200:9200 \
-p 9300:9300 \
elasticsearch:7.12.1
命令解釋:
-e "cluster.name=es-docker-cluster"
:設置集群名稱-e "http.host=0.0.0.0"
:監聽的地址,可以外網訪問-e "ES_JAVA_OPTS=-Xms512m -Xmx512m"
:記憶體大小-e "discovery.type=single-node"
:非集群模式-v es-data:/usr/share/elasticsearch/data
:掛載邏輯捲,綁定es的數據目錄-v es-logs:/usr/share/elasticsearch/logs
:掛載邏輯捲,綁定es的日誌目錄-v es-plugins:/usr/share/elasticsearch/plugins
:掛載邏輯捲,綁定es的插件目錄--privileged
:授予邏輯捲訪問權--network es-net
:加入一個名為es-net的網路中-p 9200:9200
:埠映射配置
在瀏覽器中輸入:http://192.168.194.131/:9200 即可看到elasticsearch的響應結果:
4.2.部署kibana
kibana可以給我們提供一個elasticsearch的可視化界面,便於我們學習。
4.2.1.部署
創建網路後,導入kibana壓縮包,然後創建並啟動相應容器。【和前面部署單點es一樣做法】
再運行docker命令,部署kibana
docker run -d \
--name kibana \
-e ELASTICSEARCH_HOSTS=http://es:9200 \
--network=es-net \
-p 5601:5601 \
kibana:7.12.1
--network es-net
:加入一個名為es-net的網路中,與elasticsearch在同一個網路中-e ELASTICSEARCH_HOSTS=http://es:9200"
:設置elasticsearch的地址,因為kibana已經與elasticsearch在一個網路,因此可以用容器名直接訪問elasticsearch-p 5601:5601
:埠映射配置
kibana啟動一般比較慢,需要多等待一會,可以通過命令:
docker logs -f kibana
查看運行日誌,當查看到下麵的日誌,說明成功:
此時,在瀏覽器輸入地址訪問:http://192.168.194.131:5601,即可看到結果如下圖:
kibana左側中提供了一個DevTools界面:
這個界面中可以編寫DSL來操作elasticsearch。並且對DSL語句有自動補全功能。
4.3.安裝IK分詞器
4.3.1.線上安裝ik插件(較慢)
# 進入容器內部
docker exec -it elasticsearch /bin/bash
# 線上下載並安裝
./bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.12.1/elasticsearch-analysis-ik-7.12.1.zip
#退出
exit
#重啟容器
docker restart elasticsearch
4.3.2.離線安裝ik插件(推薦)
1)查看數據捲目錄
安裝插件需要知道elasticsearch的plugins目錄位置,而我們用了數據捲掛載,因此需要查看elasticsearch的數據捲目錄,通過下麵命令查看:
docker volume inspect es-plugins
顯示結果:
[
{
"CreatedAt": "2022-05-06T10:06:34+08:00",
"Driver": "local",
"Labels": null,
"Mountpoint": "/var/lib/docker/volumes/es-plugins/_data",
"Name": "es-plugins",
"Options": null,
"Scope": "local"
}
]
說明plugins目錄被掛載到了:/var/lib/docker/volumes/es-plugins/_data
這個目錄中。
2)解壓縮分詞器安裝包
下麵我們需要把課前資料中的ik分詞器解壓縮,重命名為ik
3)上傳到es容器的插件數據捲中
也就是/var/lib/docker/volumes/es-plugins/_data
:
4)重啟容器
# 4、重啟容器
docker restart es
# 查看es日誌
docker logs -f es
5)測試:
IK分詞器包含兩種模式:
-
ik_smart
:最少切分 -
ik_max_word
:最細切分
在kibana的Dev tools中輸入以下代碼:
”analyzer“ 就是選擇分詞器模式
GET /_analyze
{
"analyzer": "ik_max_word",
"text": "黑馬程式員學習java太棒了"
}
結果:
{
"tokens" : [
{
"token" : "黑馬",
"start_offset" : 0,
"end_offset" : 2,
"type" : "CN_WORD",
"position" : 0
},
{
"token" : "程式員",
"start_offset" : 2,
"end_offset" : 5,
"type" : "CN_WORD",
"position" : 1
},
{
"token" : "程式",
"start_offset" : 2,
"end_offset" : 4,
"type" : "CN_WORD",
"position" : 2
},
{
"token" : "員",
"start_offset" : 4,
"end_offset" : 5,
"type" : "CN_CHAR",
"position" : 3
},
{
"token" : "學習",
"start_offset" : 5,
"end_offset" : 7,
"type" : "CN_WORD",
"position" : 4
},
{
"token" : "java",
"start_offset" : 7,
"end_offset" : 11,
"type" : "ENGLISH",
"position" : 5
},
{
"token" : "太棒了",
"start_offset" : 11,
"end_offset" : 14,
"type" : "CN_WORD",
"position" : 6
},
{
"token" : "太棒",
"start_offset" : 11,
"end_offset" : 13,
"type" : "CN_WORD",
"position" : 7
},
{
"token" : "了",
"start_offset" : 13,
"end_offset" : 14,
"type" : "CN_CHAR",
"position" : 8
}
]
}
4.3.3 擴展詞詞典
隨著互聯網的發展,“造詞運動”也越發的頻繁。出現了很多新的詞語,在原有的辭彙列表中並不存在。比如:“奧力給”,“白嫖” 等。
所以我們的辭彙也需要不斷的更新,IK分詞器提供了擴展辭彙的功能。
1)打開IK分詞器config目錄:
2)在IKAnalyzer.cfg.xml配置文件內容添加:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
<comment>IK Analyzer 擴展配置</comment>
<!--用戶可以在這裡配置自己的擴展字典 *** 添加擴展詞典-->
<entry key="ext_dict">ext.dic</entry>
</properties>
3)新建一個 ext.dic,可以參考config目錄下複製一個配置文件進行修改
白嫖
奧力給
4)重啟elasticsearch
docker restart es
# 查看 日誌
docker logs -f elasticsearch
日誌中已經成功載入ext.dic配置文件
5)測試效果:
GET /_analyze
{
"analyzer": "ik_max_word",
"text": "傳智播客Java就業超過90%,奧力給!"
}
註意當前文件的編碼必須是 UTF-8 格式,嚴禁使用Windows記事本編輯
4.3.4 停用詞詞典
在互聯網項目中,在網路間傳輸的速度很快,所以很多語言是不允許在網路上傳遞的,如:關於宗教、政治等敏感詞語,那麼我們在搜索時也應該忽略當前辭彙。
IK分詞器也提供了強大的停用詞功能,讓我們在索引時就直接忽略當前的停用辭彙表中的內容。
1)IKAnalyzer.cfg.xml配置文件內容添加:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
<comment>IK Analyzer 擴展配置</comment>
<!--用戶可以在這裡配置自己的擴展字典-->
<entry key="ext_dict">ext.dic</entry>
<!--用戶可以在這裡配置自己的擴展停止詞字典 *** 添加停用詞詞典-->
<entry key="ext_stopwords">stopword.dic</entry>
</properties>
3)在 stopword.dic 添加停用詞
大帥逼
4)重啟elasticsearch
# 重啟服務
docker restart es
docker restart kibana
# 查看 日誌
docker logs -f elasticsearch
日誌中已經成功載入stopword.dic配置文件
5)測試效果:
GET /_analyze
{
"analyzer": "ik_max_word",
"text": "我是真的會謝Java就業率超過95%,大帥逼都點贊白嫖,奧力給!"
}
註意當前文件的編碼必須是 UTF-8 格式,嚴禁使用Windows記事本編輯
索引庫操作
索引庫就類似資料庫表,mapping映射就類似表的結構。
我們要向es中存儲數據,必須先創建“庫”和“表”。
1. Mapping映射屬性
mapping是對索引庫中文檔的約束,常見的mapping屬性包括:
-
type:欄位數據類型,常見的簡單類型有:
-
字元串:text(可分詞的文本)、keyword(精確值,例如:品牌、國家、ip地址)
keyword類型只能整體搜索,不支持搜索部分內容
-
數值:long、integer、short、byte、double、float、
-
布爾:boolean
-
日期:date
-
對象:object
-
-
index:是否創建索引,預設為true
-
analyzer:使用哪種分詞器
-
properties:該欄位的子欄位
例如下麵的json文檔:
{
"age": 21,
"weight": 52.1,
"isMarried": false,
"info": "真相只有一個!",
"email": "[email protected]",
"score": [99.1, 99.5, 98.9],
"name": {
"firstName": "柯",
"lastName": "南"
}
}
對應的每個欄位映射(mapping):
- age:類型為 integer;參與搜索,因此需要index為true;無需分詞器
- weight:類型為float;參與搜索,因此需要index為true;無需分詞器
- isMarried:類型為boolean;參與搜索,因此需要index為true;無需分詞器
- info:類型為字元串,需要分詞,因此是text;參與搜索,因此需要index為true;分詞器可以用ik_smart
- email:類型為字元串,但是不需要分詞,因此是keyword;不參與搜索,因此需要index為false;無需分詞器
- score:雖然是數組,但是我們只看元素的類型,類型為float;參與搜索,因此需要index為true;無需分詞器
- name:類型為object,需要定義多個子屬性
- name.firstName;類型為字元串,但是不需要分詞,因此是keyword;參與搜索,因此需要index為true;無需分詞器
- name.lastName;類型為字元串,但是不需要分詞,因此是keyword;參與搜索,因此需要index為true;無需分詞器
2. 索引庫的CRUD
CRUD簡單描述:
- 創建索引庫:PUT /索引庫名
- 查詢索引庫:GET /索引庫名
- 刪除索引庫:DELETE /索引庫名
- 修改索引庫(添加欄位):PUT /索引庫名/_mapping
這裡統一使用Kibana編寫DSL的方式來演示。
2.1 創建索引庫和映射
基本語法:
- 請求方式:PUT
- 請求路徑:/索引庫名,可以自定義
- 請求參數:mapping映射
格式:
PUT /索引庫名稱
{
"mappings": {
"properties": {
"欄位名":{
"type": "text",
"analyzer": "ik_smart"
},
"欄位名2":{
"type": "keyword",
"index": "false"
},
"欄位名3":{
"properties": {
"子欄位": {
"type": "keyword"
}
}
},
// ...略
}
}
}
示例:
PUT /conan
{
"mappings": {
"properties": {
"column1":{
"type": "text",
"analyzer": "ik_smart"
},
"column2":{
"type": "keyword",
"index": "false"
},
"column3":{
"properties": {
"子欄位1": {
"type": "keyword"
},
"子欄位2": {
"type": "keyword"
}
}
},
// ...略
}
}
}
2.2 查詢索引庫
基本語法:
-
請求方式:GET
-
請求路徑:/索引庫名
-
請求參數:無
格式:
GET /索引庫名
示例:
2.3 修改索引庫
這裡的修改是只能增加新的欄位到mapping中
倒排索引結構雖然不複雜,但是一旦數據結構改變(比如改變了分詞器),就需要重新創建倒排索引,這簡直是災難。因此索引庫一旦創建,無法修改mapping。
雖然無法修改mapping中已有的欄位,但是卻允許添加新的欄位到mapping中,因為不會對倒排索引產生影響。
語法說明:
PUT /索引庫名/_mapping
{
"properties": {
"新欄位名":{
"type": "integer"
}
}
}
示例:
2.4 刪除索引庫
語法:
-
請求方式:DELETE
-
請求路徑:/索引庫名
-
請求參數:無
格式:
DELETE /索引庫名
在kibana中測試:
文檔操作
文檔操作有哪些?
- 創建文檔:POST /{索引庫名}/_doc/文檔id
- 查詢文檔:GET /{索引庫名}/_doc/文檔id
- 刪除文檔:DELETE /{索引庫名}/_doc/文檔id
- 修改文檔:
- 全量修改:PUT /{索引庫名}/_doc/文檔id
- 增量修改:POST /{索引庫名}/_update/文檔id { "doc": {欄位}}
1. 文檔的CRUD
1.1 新增文檔
語法:
POST /索引庫名/_doc/文檔id
{
"欄位1": "值1",
"欄位2": "值2",
"欄位3": {
"子屬性1": "值3",
"子屬性2": "值4"
},
// ...
}
示例:
POST /heima/_doc/1
{
"info": "真相只有一個!",
"email": "[email protected]",
"name": {
"firstName": "柯",
"lastName": "南"
}
}
響應:
1.2 查詢文檔
根據rest風格,新增是post,查詢應該是get,不過查詢一般都需要條件,這裡我們把文檔id帶上。
語法:
GET /{索引庫名稱}/_doc/{id}
//批量查詢:查詢該索引庫下的全部文檔
GET /{索引庫名稱}/_search
通過kibana查看數據:
GET /heima/_doc/1
查看結果:
1.3 刪除文檔
刪除使用DELETE請求,同樣,需要根據id進行刪除:
語法:
DELETE /{索引庫名}/_doc/id值
示例:
# 根據id刪除數據
DELETE /heima/_doc/1
結果:
1.4 修改文檔
修改有兩種方式:
- 全量修改:直接覆蓋原來的文檔
- 增量修改:修改文檔中的部分欄位
1.4.1 全量修改
全量修改是覆蓋原來的文檔,其本質是:
- 根據指定的id刪除文檔
- 新增一個相同id的文檔
註意:如果根據id刪除時,id不存在,第二步的新增也會執行,也就從修改變成了新增操作了。
語法:
PUT /{索引庫名}/_doc/文檔id
{
"欄位1": "值1",
"欄位2": "值2",
// ... 略
}
示例:
PUT /heima/_doc/1
{
"info": "黑馬程式員高級Java講師",
"email": "[email protected]",
"name": {
"firstName": "雲",
"lastName": "趙"
}
}
1.4.2 增量修改
增量修改是只修改指定id匹配的文檔中的部分欄位。
語法:
POST /{索引庫名}/_update/文檔id
{
"doc": {
"欄位名": "新的值",
}
}
示例:
POST /heima/_update/1
{
"doc": {
"email": "[email protected]"
}
}
RestAPI
ES官方提供了各種不同語言的客戶端,用來操作ES。這些客戶端的本質就是組裝DSL語句,通過http請求發送給ES。官方文檔地址:https://www.elastic.co/guide/en/elasticsearch/client/index.html
其中的Java Rest Client又包括兩種:
- Java Low Level Rest Client
- Java High Level Rest Client
我們使用的是Java HighLevel Rest Client客戶端API
API操作索引庫
JavaRestClient操作elasticsearch的流程基本類似。核心是client.indices()方法來獲取索引庫的操作對象。
索引庫操作的基本步驟:【可以根據發送請求那步的第一個參數,發過來判斷需要創建什麼XXXXRequest】
- 初始化RestHighLevelClient
- 創建XxxIndexRequest。XXX是Create、Get、Delete
- 準備DSL( Create時需要,其它是無參)
- 發送請求。調用RestHighLevelClient#indices().xxx()方法,xxx是create、exists、delete
1. mapping映射分析
根據MySQL資料庫表結構(建表語句),去寫索引庫結構JSON。表和索引庫一一對應
註意:地理坐標、組合欄位。索引庫里的地理坐標是一個欄位:
坐標:維度,精度
。copy_to組合欄位作用是供用戶查詢(輸入關鍵字可以查詢多個欄位)
創建索引庫,最關鍵的是mapping映射,而mapping映射要考慮的信息包括:
- 欄位名
- 欄位數據類型
- 是否參與搜索
- 是否需要分詞
- 如果分詞,分詞器是什麼?
其中:
- 欄位名、欄位數據類型,可以參考數據表結構的名稱和類型
- 是否參與搜索要分析業務來判斷,例如圖片地址,就無需參與搜索
- 是否分詞呢要看內容,內容如果是一個整體就無需分詞,反之則要分詞
- 分詞器,我們可以統一使用ik_max_word
來看下酒店數據的索引庫結構:
PUT /hotel
{
"mappings": {
"properties": {
"id": {
"type": "keyword"
},
"name":{
"type": "text",
"analyzer": "ik_max_word",
"copy_to": "all"
},
"address":{
"type": "keyword",
"index": false
},
"price":{
"type": "integer"
},
"score":{
"type": "integer"
},
"brand":{
"type": "keyword",
"copy_to": "all"
},
"city":{
"type": "keyword",
"copy_to": "all"
},
"starName":{
"type": "keyword"
},
"business":{
"type": "keyword"
},
"location":{
"type": "geo_point"
},
"pic":{
"type": "keyword",
"index": false
},
"all":{
"type": "text",
"analyzer": "ik_max_word"
}
}
}
}
幾個特殊欄位說明:
- location:地理坐標,裡面包含精度、緯度
- all:一個組合欄位,其目的是將多欄位的值 利用copy_to合併,提供給用戶搜索
地理坐標說明:
copy_to說明:
2.初始化RestClient
在elasticsearch提供的API中,與elasticsearch一切交互都封裝在一個名為RestHighLevelClient的類中,必須先完成這個對象的初始化,建立與elasticsearch的連接。
分為三步:
1)引入es的RestHighLevelClient依賴:
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
</dependency>
2)因為SpringBoot預設的ES版本是7.6.2,所以我們需要覆蓋預設的ES版本:
<properties>
<java.version>1.8</java.version>
<elasticsearch.version>7.12.1</elasticsearch.version>
</properties>
3)初始化RestHighLevelClient:這裡一般在啟動類或者配置類里註入該Bean,用於告訴Java 訪問ES的ip地址
初始化的代碼如下:
@Bean
public RestHighLevelClient client(){
return new RestHighLevelClient(RestClient.builder(
HttpHost.create("http://192.168.150.101:9200")
));
}
這裡為了單元測試方便,我們創建一個測試類HotelIndexTest,然後將初始化的代碼編寫在@BeforeEach方法中:
package cn.itcast.hotel;
import org.apache.http.HttpHost;
import org.elasticsearch.client.RestHighLevelClient;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.io.IOException;
public class HotelIndexTest {
private RestHighLevelClient client;
@BeforeEach
void setUp() {
this.client = new RestHighLevelClient(RestClient.builder(
HttpHost.create("http://192.168.150.101:9200")
));
}
@AfterEach
void tearDown() throws IOException {
this.client.close();
}
}
3. 索引庫CRUD
3.1 創建索引庫
代碼分為三步:
- 1)創建Request對象。因為是創建索引庫的操作,因此Request是CreateIndexRequest。
- 2)添加請求參數,其實就是DSL的JSON參數部分。因為json字元串很長,這裡是定義了靜態字元串常量MAPPING_TEMPLATE,讓代碼看起來更加優雅。
- 3)發送請求,client.indices()方法的返回值是IndicesClient類型,封裝了所有與索引庫操作有關的方法。
創建索引庫的API如下:
代碼:
在hotel-demo的cn.itcast.hotel.constants包下,創建一個類,定義mapping映射的JSON字元串常量:
package cn.itcast.hotel.constants;
public class HotelConstants {
public static final String MAPPING_TEMPLATE = "{\n" +
" \"mappings\": {\n" +
" \"properties\": {\n" +
" \"id\": {\n" +
" \"type\": \"keyword\"\n" +
" },\n" +
" \"name\":{\n" +
" \"type\": \"text\",\n" +
" \"analyzer\": \"ik_max_word\",\n" +
" \"copy_to\": \"all\"\n" +
" },\n" +
" \"address\":{\n" +
" \"type\": \"keyword\",\n" +
" \"index\": false\n" +
" },\n" +
" \"price\":{\n" +
" \"type\": \"integer\"\n" +
" },\n" +
" \"score\":{\n" +
" \"type\": \"integer\"\n" +
" },\n" +
" \"brand\":{\n" +
" \"type\": \"keyword\",\n" +
" \"copy_to\": \"all\"\n" +
" },\n" +
" \"city\":{\n" +
" \"type\": \"keyword\",\n" +
" \"copy_to\": \"all\"\n" +
" },\n" +
" \"starName\":{\n" +
" \"type\": \"keyword\"\n" +
" },\n" +
" \"business\":{\n" +
" \"type\": \"keyword\"\n" +
" },\n" +
" \"location\":{\n" +
" \"type\": \"geo_point\"\n" +
" },\n" +
" \"pic\":{\n" +
" \"type\": \"keyword\",\n" +
" \"index\": false\n" +
" },\n" +
" \"all\":{\n" +
" \"type\": \"text\",\n" +
" \"analyzer\": \"ik_max_word\"\n" +
" }\n" +
" }\n" +
" }\n" +
"}";
}
在hotel-demo中的HotelIndexTest測試類中,編寫單元測試,實現創建索引:
@Test
void createHotelIndex() throws IOException {
// 1.創建Request對象
CreateIndexRequest request = new CreateIndexRequest("hotel");
// 2.準備請求的參數:DSL語句
request.source(MAPPING_TEMPLATE, XContentType.JSON);
// 3.發送請求
client.indices().create(request, RequestOptions.DEFAULT);
}
3.2 刪除索引庫
三步走:
- 1)創建Request對象。這次是DeleteIndexRequest對象
- 2)準備參數。這裡是無參
- 3)發送請求。改用delete方法
刪除索引庫的DSL語句非常簡單:
DELETE /hotel
在hotel-demo中的HotelIndexTest測試類中,編寫單元測試,實現刪除索引:
@Test
void testDeleteHotelIndex() throws IOException {
// 1.創建Request對象
DeleteIndexRequest request = new DeleteIndexRequest("hotel");
// 2.發送請求
client.indices().delete(request, RequestOptions.DEFAULT);
}
3.3 查詢索引庫
三步走:
- 1)創建Request對象。這次是GetIndexRequest對象
- 2)準備參數。這裡是無參
- 3)發送請求。改用exists方法
判斷索引庫是否存在,本質就是查詢,對應的DSL是:
GET /hotel
@Test
void testExistsHotelIndex() throws IOException {
// 1.創建Request對象
GetIndexRequest request = new GetIndexRequest("hotel");
// 2.發送請求
boolean exists = client.indices().exists(request, RequestOptions.DEFAULT);
// 3.輸出
System.err.println(exists ? "索引庫已經存在!" : "索引庫不存在!");
}
API操作文檔
這裡更多的是先讀取Mysql中的數據,然後再存進ES中。
文檔操作的基本步驟:【可以根據發送請求那步的第一個參數,發過來判斷需要創建什麼XXXXRequest】
- 初始化RestHighLevelClient
- 創建XxxRequest。XXX是Index、Get、Update、Delete、Bulk
- 準備參數(Index、Update、Bulk時需要)
- 發送請求。調用RestHighLevelClient#.xxx()方法,xxx是index、get、update、delete、bulk
- 解析結果(Get時需要)
1. 初始化RestClient
在elasticsearch提供的API中,與elasticsearch一切交互都封裝在一個名為RestHighLevelClient的類中,必須先完成這個對象的初始化,建立與elasticsearch的連接。
分為三步:
1)引入es的RestHighLevelClient依賴:
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
</dependency>
2)因為SpringBoot預設的ES版本是7.6.2,所以我們需要覆蓋預設的ES版本:
<properties>
<java.version>1.8</java.version>
<elasticsearch.version>7.12.1</elasticsearch.version>
</properties>
3)初始化RestHighLevelClient:這裡一般寫在最前面,用於告訴Java 訪問ES的ip地址
初始化的代碼如下:
RestHighLevelClient client = new RestHighLevelClient(RestClient.builder(
HttpHost.create("http://192.168.150.101:9200")
));
這裡為了單元測試方便,我們創建一個測試類HotelIndexTest,然後將初始化的代碼編寫在@BeforeEach方法中:
package cn.itcast.hotel;
import org.apache.http.HttpHost;
import org.elasticsearch.client.RestHighLevelClient;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.io.IOException;
public class HotelIndexTest {
private RestHighLevelClient client;
@BeforeEach
void setUp() {
this.client = new RestHighLevelClient(RestClient.builder(
HttpHost.create("http://192.168.150.101:9200")
));
}
@AfterEach
void tearDown() throws IOException {
this.client.close();
}
}
2. 文檔CRUD
2.0 批量導入文檔
三步走:
- 1)創建Request對象。這裡是BulkRequest
- 2)準備參數。批處理的參數,就是其它Request對象,這裡就是多個IndexRequest
- 3)發起請求。這裡是批處理,調用的方法為client.bulk()方法
案例需求:利用BulkRequest批量將資料庫數據導入到索引庫中。
步驟如下:
-
利用mybatis-plus查詢酒店數據
-
將查詢到的酒店數據(Hotel)轉換為文檔類型數據(HotelDoc)
-
利用JavaRestClient中的BulkRequest批處理,實現批量新增文檔
語法說明:
批量處理BulkRequest,其本質就是將多個普通的CRUD請求組合在一起發送。
其中提供了一個add方法,用來添加其他請求:
可以看到,能添加的請求包括:
- IndexRequest,也就是新增
- UpdateRequest,也就是修改
- DeleteRequest,也就是刪除
因此Bulk中添加了多個IndexRequest,就是批量新增功能了。示例:
我們在導入酒店數據時,將上述代碼改造成for迴圈處理即可。
在hotel-demo的HotelDocumentTest測試類中,編寫單元測試:
@Test
void testBulkRequest() throws IOException {
// 批量查詢酒店數據
List<Hotel> hotels = hotelService.list();
// 1.創建Request
BulkRequest request = new BulkRequest();
// 2.準備參數,添加多個新增的Request
for (Hotel hotel : hotels) {
// 2.1.轉換為文檔類型HotelDoc
HotelDoc hotelDoc = new HotelDoc(hotel);
// 2.2.創建新增文檔的Request對象
request.add(new IndexRequest("hotel")
.id(hotelDoc.getId().toString())
.source(JSON.toJSONString(hotelDoc), XContentType.JSON));
}
// 3.發送請求
client.bulk(request, RequestOptions.DEFAULT);
}
2.1 批量新增文檔
四步走:
- 0)創建索引庫實體類
- 1)創建Request對象
- 2)準備請求參數,也就是DSL中的JSON文檔
- 3)發送請求 (註意:這裡直接使用client.xxx()的API,不再需要client.indices()了)
我們要將資料庫的酒店數據查詢出來,寫入elasticsearch中。
1)創建索引庫實體類
一般實體類里包含經緯度都需要創建一個新的實體類,將經緯度拼成一個欄位
資料庫查詢後的結果是一個Hotel類型的對象。結構如下:
@Data
@TableName("tb_hotel")
public class Hotel {
@TableId(type = IdType.INPUT)
private Long id;
private String name;
private String address;
private Integer price;
private Integer score;
private String brand;
private String city;
private String starName;
private String business;
private String longitude;
private String latitude;
private String pic;
}
與我們的索引庫結構存在差異:
- longitude和latitude需要合併為location
因此,我們需要定義一個新的類型,與索引庫結構吻合:
package cn.itcast.hotel.pojo;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
public class HotelDoc {
private Long id;
private String name;
private String address;
private Integer price;
private Integer score;
private String brand;
private String city;
private String starName;
private String business;
private String location;
private String pic;
public HotelDoc(Hotel hotel) {
this.id = hotel.getId();
this.name = hotel.getName();
this.address = hotel.getAddress();
this.price = hotel.getPrice();
this.score = hotel.getScore();
this.brand = hotel.getBrand();
this.city = hotel.getCity();
this.starName = hotel.getStarName();
this.business = hotel.getBusiness();
this.location = hotel.getLatitude() + ", " + hotel.getLongitude();
this.pic = hotel.getPic();
}
}
2)新增代碼
新增文檔的DSL語句如下:
POST /{索引庫名}/_doc/1
{
"name": "Jack",
"age": 21
}
對應的java代碼如圖:
我們導入酒店數據,基本流程一致,但是需要考慮幾點變化:
- 酒店數據來自於資料庫,我們需要先查詢出來,得到hotel對象
- hotel對象需要轉為HotelDoc對象
- HotelDoc需要序列化為json格式
在hotel-demo的HotelDocumentTest測試類中,編寫單元測試:
@Test
void testAddDocument() throws IOException {
// 批量查詢酒店數據
List<Hotel> hotels = hotelService.list();
// 1.創建Request
BulkRequest request = new BulkRequest();
// 2.準備參數,添加多個新增的Request
for (Hotel hotel : hotels) {
// 2.1.轉換為文檔類型HotelDoc
HotelDoc hotelDoc = new HotelDoc(hotel);
// 2.2.創建新增文檔的Request對象
request.add(new IndexRequest("hotel")
.id(hotelDoc.getId().toString())
.source(JSON.toJSONString(hotelDoc), XContentType.JSON));//實體類轉JSON,指定JSON格式
request.add(new IndexRequest("xxx")...)
}
// 3.發送請求
client.bulk(request, RequestOptions.DEFAULT);
}
2.2 查詢文檔
查詢文檔是根據id查詢的,所以沒有批量查詢
三步走:
- 1)準備Request對象。這次是查詢,所以是GetRequest
- 2)發送請求,得到結果。因為是查詢,這裡調用client.get()方法
- 3)解析結果,就是對JSON做反序列化
查詢的DSL語句如下:
GET /hotel/_doc/{id}
非常簡單,因此代碼大概分兩步:
- 準備Request對象
- 發送請求
不過查詢的目的是得到結果,解析為HotelDoc,因此難點是結果的解析。完整代碼如下:
可以看到,結果是一個JSON,其中文檔放在一個_source
屬性中,因此解析就是拿到_source
,使用工具反序列化為Java對象即可。
在hotel-demo的HotelDocumentTest測試類中,編寫單元測試:
@Test
void testGetDocumentById() throws IOException {
// 1.準備Request
GetRequest request = new GetRequest("hotel", "61082");
// 2.發送請求,得到響應
GetResponse response = client.get(request, RequestOptions.DEFAULT);
// 3.解析響應結果
String json = response.getSourceAsString();
HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
System.out.println(hotelDoc);
}
2.3 批量刪除文檔
三步走:
- 1)準備Request對象,因為是刪除,這次是DeleteRequest對象。要指定索引庫名和id
- 2)準備參數,無參
- 3)發送請求。因為是刪除,所以是client.delete()方法
刪除的DSL為是這樣的:
DELETE /hotel/_doc/{id}
在hotel-demo的HotelDocumentTest測試類中,編寫單元測試:
@Test
void testDeleteDocument() throws IOException {
//0.查詢資料庫中的數據
List<Hotel> list = hotelService.list();
// 1.創建Request
BulkRequest request = new BulkRequest();
//2.批量轉換實體類,順便寫入到ES中
for (Hotel hotel : list) {
//2.1轉換實體類
HotelDoc hotelDoc =new HotelDoc(hotel);
//2.2寫入ES
request.add(new DeleteRequest("hotel")
.id(hotel.getId().toString()));
}
//3.發送請求
client.bulk(request,RequestOptions.DEFAULT);
}
2.4 批量修改文檔
三步走:
- 1)準備Request對象。這次是修改,所以是UpdateRequest
- 2)準備參數。也就是JSON文檔,裡面包含要修改的欄位
- 3)更新文檔。這裡調用client.update()方法
修改有兩種方式:
- 全量修改:本質是先根據id刪除,再新增
- 增量修改:修改文檔中的指定欄位值
在RestClient的API中,全量修改與新增的API完全一致,判斷依據是ID:
- 如果新增時,ID已經存在,則修改
- 如果新增時,ID不存在,則新增
只演示增量修改:
代碼示例如圖:
在hotel-demo的HotelDocumentTest測試類中,編寫單元測試:
@Test
void testUpdateDocument() throws IOException {
//0.查詢資料庫中的數據
List<Hotel> list = hotelService.list();
// 1.創建Request
BulkRequest request = new BulkRequest();
//2.批量轉換實體類,順便寫入到ES中
for (Hotel hotel : list) {
//2.1轉換實體類
HotelDoc hotelDoc =new HotelDoc(hotel);
//2.2寫入ES
request.add(new UpdateRequest("hotel",hotel.getId().toString())
.doc(
"price", "952",
"starName", "四鑽"
));
}
//3.發送請求
client.bulk(request,RequestOptions.DEFAULT);
}
ES搜索引擎
elasticsearch的查詢依然是基於JSON風格的DSL來實現的。
1. DSL設置查詢條件
1.1 DSL查詢分類
Elasticsearch提供了基於JSON的DSL(Domain Specific Language)來定義查詢。常見的查詢類型包括:
-
查詢所有:查詢出所有數據,一般測試用。例如:match_all
-
全文檢索(full text)查詢:利用分詞器對用戶輸入內容分詞,然後去倒排索引庫中匹配。例如:
- match_query
- multi_match_query
-
精確查詢:根據精確詞條值查找數據,一般是查找keyword、數值、日期、boolean等類型欄位。例如:
- ids
- range
- term
-
地理(geo)查詢:根據經緯度查詢。例如:
- geo_distance
- geo_bounding_box
-
複合(compound)查詢:複合查詢可以將上述各種查詢條件組合起來,合併查詢條件。例如:
- bool
- function_score
查詢的語法基本一致:
GET /indexName/_search
{
"query": {
"查詢類型": {
"查詢條件": "條件值"
}
}
}
我們以查詢所有為例,其中:
- 查詢類型為match_all
- 沒有查詢條件
// 查詢所有
GET /indexName/_search
{
"query": {
"match_all": {
}
}
}
其它查詢無非就是查詢類型、查詢條件的變化。
1.2 全文檢索查詢
match和multi_match的區別是什麼?
- match:根據一個欄位查詢【推薦:使用copy_to構造all欄位】
- multi_match:根據多個欄位查詢,參與查詢欄位越多,查詢性能越差
註:搜索欄位越多,對查詢性能影響越大,因此建議採用copy_to,然後單欄位查詢的方式。
1.2.1 使用場景
全文檢索查詢的基本流程如下:
- 對用戶搜索的內容做分詞,得到詞條
- 根據詞條去倒排索引庫中匹配,得到文檔id
- 根據文檔id找到文檔,返回給用戶
比較常用的場景包括:
- 商城的輸入框搜索
- 百度輸入框搜索
例如京東:
因為是拿著詞條去匹配,因此參與搜索的欄位也必須是可分詞的text類型的欄位。
常見的全文檢索查詢包括:
- match查詢:單欄位查詢
- multi_match查詢:多欄位查詢,任意一個欄位符合條件就算符合查詢條件
1.2.2 match查詢
match查詢語法如下:
GET /indexName/_search
{
"query": {
"match": {
"FIELD": "TEXT"
}
}
}
match查詢示例:
1.2.3 mulit_match查詢
mulit_match語法如下:
GET /indexName/_search
{
"query": {
"multi_match": {
"query": "TEXT",
"fields": ["FIELD1", " FIELD12"]
}
}
}
multi_match查詢示例:
1.3 精準查詢
精準查詢類型:
- term查詢:根據詞條精確匹配,一般搜索keyword類型、數值類型、布爾類型、日期類型欄位
- range查詢:根據數值範圍查詢,可以是數值、日期的範圍
精確查詢一般是查找keyword、數值、日期、boolean等類型欄位。所以不會對搜索條件分詞。常見的有:
- term:根據詞條精確值查詢
- range:根據值的範圍查詢
1.3.1 term查詢
因為精確查詢的欄位搜時不分詞的欄位,因此查詢的條件也必須是不分詞的詞條。查詢時,用戶輸入的內容跟自動值完全匹配時才認為符合條件。如果用戶輸入的內容過多,反而搜索不到數據。
語法說明:
// term查詢
GET /indexName/_search
{
"query": {
"term": {
"FIELD": {
"value": "VALUE"
}
}
}
}
示例:
當我搜索的是精確詞條時,能正確查詢出結果:
但是,當我搜索的內容不是詞條,而是多個詞語形成的短語時,反而搜索不到:
1.3.2 range查詢
範圍查詢,一般應用在對數值類型做範圍過濾的時候。比如做價格範圍過濾。
基本語法:
// range查詢
GET /indexName/_search
{
"query": {
"range": {
"FIELD": {
"gte": 10, // 這裡的gte代表大於等於,gt則代表大於
"lte": 20 // lte代表小於等於,lt則代表小於
}
}
}
}
示例:
1.4 地理坐標查詢
所謂的地理坐