黑馬商城作為一個電商項目,商品的搜索肯定是訪問頻率最高的頁面之一。目前搜索功能是基于數據庫的模糊搜索來實現的,存在很多問題。
首先,查詢效率較低。
由于數據庫模糊查詢不走索引,在數據量較大的時候,查詢性能很差。黑馬商城的商品表中僅僅有不到9萬條數據,基于數據庫查詢時,搜索接口的表現如圖:
改為基于搜索引擎后,查詢表現如下:
需要注意的是,數據庫模糊查詢隨著表數據量的增多,查詢性能的下降會非常明顯,而搜索引擎的性能則不會隨著數據增多而下降太多。目前僅10萬不到的數據量差距就如此明顯,如果數據量達到百萬、千萬、甚至上億級別,這個性能差距會非常夸張。
其次,功能單一
數據庫的模糊搜索功能單一,匹配條件非常苛刻,必須恰好包含用戶搜索的關鍵字。而在搜索引擎中,用戶輸入出現個別錯字,或者用拼音搜索、同義詞搜索都能正確匹配到數據。
綜上,在面臨海量數據的搜索,或者有一些復雜搜索需求的時候,推薦使用專門的搜索引擎來實現搜索功能。
目前全球的搜索引擎技術排名如下:
排名第一的就是我們今天要學習的elasticsearch.
elasticsearch是一款非常強大的開源搜索引擎,支持的功能非常多,例如:
代碼搜索
商品搜索
解決方案搜索
地圖搜索
通過今天的學習大家要達成下列學習目標:
- 理解倒排索引原理
- 會使用IK分詞器
- 理解索引庫Mapping映射的屬性含義
- 能創建索引庫及映射
- 能實現文檔的CRUD
1.初識elasticsearch
Elasticsearch的官方網站如下:
https://www.elastic.co/cn/elasticsearch/
本章我們一起來初步了解一下Elasticsearch的基本原理和一些基礎概念。
1.1.認識和安裝
Elasticsearch是由elastic公司開發的一套搜索引擎技術,它是elastic技術棧中的一部分。完整的技術棧包括:
- Elasticsearch:用于數據存儲、計算和搜索
- Logstash/Beats:用于數據收集
- Kibana:用于數據可視化
整套技術棧被稱為ELK,經常用來做日志收集、系統監控和狀態分析等等:
整套技術棧的核心就是用來存儲、搜索、計算的Elasticsearch,因此我們接下來學習的核心也是Elasticsearch。
我們要安裝的內容包含2部分:
- elasticsearch:存儲、搜索和運算
- kibana:圖形化展示
首先Elasticsearch不用多說,是提供核心的數據存儲、搜索、分析功能的。
然后是Kibana,Elasticsearch對外提供的是Restful風格的API,任何操作都可以通過發送http請求來完成。不過http請求的方式、路徑、還有請求參數的格式都有嚴格的規范。這些規范我們肯定記不住,因此我們要借助于Kibana這個服務。
Kibana是elastic公司提供的用于操作Elasticsearch的可視化控制臺。它的功能非常強大,包括:
- 對Elasticsearch數據的搜索、展示
- 對Elasticsearch數據的統計、聚合,并形成圖形化報表、圖形
- 對Elasticsearch的集群狀態監控
- 它還提供了一個開發控制臺(DevTools),在其中對Elasticsearch的Restful的API接口提供了語法提示
1.1.1.安裝elasticsearch
通過下面的Docker命令即可安裝單機版本的elasticsearch:
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 hm-net \-p 9200:9200 \-p 9300:9300 \elasticsearch:7.12.1
注意,這里我們采用的是elasticsearch的7.12.1版本,由于8以上版本的JavaAPI變化很大,在企業中應用并不廣泛,企業中應用較多的還是8以下的版本。
如果拉取鏡像困難,可以直接導入課前資料提供的鏡像tar包:
安裝完成后,訪問9200端口,即可看到響應的Elasticsearch服務的基本信息:
1.1.2.安裝Kibana
通過下面的Docker命令,即可部署Kibana:
docker run -d \
--name kibana \
-e ELASTICSEARCH_HOSTS=http://es:9200 \
--network=hm-net \
-p 5601:5601 \
kibana:7.12.1
如果拉取鏡像困難,可以直接導入課前資料提供的鏡像tar包:
安裝完成后,直接訪問5601端口,即可看到控制臺頁面:
選擇Explore on my own
之后,進入主頁面:
然后選中Dev tools
,進入開發工具頁面:
1.2.倒排索引
elasticsearch之所以有如此高性能的搜索表現,正是得益于底層的倒排索引技術。那么什么是倒排索引呢?
倒排索引的概念是基于MySQL這樣的正向索引而言的。
1.2.1.正向索引
我們先來回顧一下正向索引。
例如有一張名為tb_goods
的表:
id | title | price |
1 | 小米手機 | 3499 |
2 | 華為手機 | 4999 |
3 | 華為小米充電器 | 49 |
4 | 小米手環 | 49 |
... | ... | ... |
其中的id
字段已經創建了索引,由于索引底層采用了B+樹結構,因此我們根據id搜索的速度會非常快。但是其他字段例如title
,只在葉子節點上存在。
因此要根據title
搜索的時候只能遍歷樹中的每一個葉子節點,判斷title數據是否符合要求。
比如用戶的SQL語句為:
select * from tb_goods where title like '%手機%';
那搜索的大概流程如圖:
說明:
- 1)檢查到搜索條件為
like '%手機%'
,需要找到title
中包含手機
的數據 - 2)逐條遍歷每行數據(每個葉子節點),比如第1次拿到
id
為1的數據 - 3)判斷數據中的
title
字段值是否符合條件 - 4)如果符合則放入結果集,不符合則丟棄
- 5)回到步驟1
綜上,根據id精確匹配時,可以走索引,查詢效率較高。而當搜索條件為模糊匹配時,由于索引無法生效,導致從索引查詢退化為全表掃描,效率很差。
因此,正向索引適合于根據索引字段的精確搜索,不適合基于部分詞條的模糊匹配。
而倒排索引恰好解決的就是根據部分詞條模糊匹配的問題。
1.2.2.倒排索引
倒排索引中有兩個非常重要的概念:
- 文檔(
Document
):用來搜索的數據,其中的每一條數據就是一個文檔。例如一個網頁、一個商品信息 - 詞條(
Term
):對文檔數據或用戶搜索數據,利用某種算法分詞,得到的具備含義的詞語就是詞條。例如:我是中國人,就可以分為:我、是、中國人、中國、國人這樣的幾個詞條
創建倒排索引是對正向索引的一種特殊處理和應用,流程如下:
- 將每一個文檔的數據利用分詞算法根據語義拆分,得到一個個詞條
- 創建表,每行數據包括詞條、詞條所在文檔id、位置等信息
- 因為詞條唯一性,可以給詞條創建正向索引
此時形成的這張以詞條為索引的表,就是倒排索引表,兩者對比如下:
正向索引
id(索引) | title | price |
1 | 小米手機 | 3499 |
2 | 華為手機 | 4999 |
3 | 華為小米充電器 | 49 |
4 | 小米手環 | 49 |
... | ... | ... |
倒排索引
詞條(索引) | 文檔id |
小米 | 1,3,4 |
手機 | 1,2 |
華為 | 2,3 |
充電器 | 3 |
手環 | 4 |
倒排索引的搜索流程如下(以搜索"華為手機"為例),如圖:
流程描述:
1)用戶輸入條件"華為手機"
進行搜索。
2)對用戶輸入條件分詞,得到詞條:華為
、手機
。
3)拿著詞條在倒排索引中查找(由于詞條有索引,查詢效率很高),即可得到包含詞條的文檔id:1、2、3
。
4)拿著文檔id
到正向索引中查找具體文檔即可(由于id
也有索引,查詢效率也很高)。
雖然要先查詢倒排索引,再查詢倒排索引,但是無論是詞條、還是文檔id都建立了索引,查詢速度非常快!無需全表掃描。
1.2.3.正向和倒排
那么為什么一個叫做正向索引,一個叫做倒排索引呢?
- 正向索引是最傳統的,根據id索引的方式。但根據詞條查詢時,必須先逐條獲取每個文檔,然后判斷文檔中是否包含所需要的詞條,是根據文檔找詞條的過程。
- 而倒排索引則相反,是先找到用戶要搜索的詞條,根據詞條得到保護詞條的文檔的id,然后根據id獲取文檔。是根據詞條找文檔的過程。
是不是恰好反過來了?
那么兩者方式的優缺點是什么呢?
正向索引:
- 優點:
-
- 可以給多個字段創建索引
- 根據索引字段搜索、排序速度非常快
- 缺點:
-
- 根據非索引字段,或者索引字段中的部分詞條查找時,只能全表掃描。
倒排索引:
- 優點:
-
- 根據詞條搜索、模糊搜索時,速度非常快
- 缺點:
-
- 只能給詞條創建索引,而不是字段
- 無法根據字段做排序
1.3.基礎概念
elasticsearch中有很多獨有的概念,與mysql中略有差別,但也有相似之處。
1.3.1.文檔和字段
elasticsearch是面向文檔(Document)存儲的,可以是數據庫中的一條商品數據,一個訂單信息。文檔數據會被序列化為json
格式后存儲在elasticsearch
中:
{"id": 1,"title": "小米手機","price": 3499
}
{"id": 2,"title": "華為手機","price": 4999
}
{"id": 3,"title": "華為小米充電器","price": 49
}
{"id": 4,"title": "小米手環","price": 299
}
因此,原本數據庫中的一行數據就是ES中的一個JSON文檔;而數據庫中每行數據都包含很多列,這些列就轉換為JSON文檔中的字段(Field)。
1.3.2.索引和映射
隨著業務發展,需要在es中存儲的文檔也會越來越多,比如有商品的文檔、用戶的文檔、訂單文檔等等:
所有文檔都散亂存放顯然非常混亂,也不方便管理。
因此,我們要將類型相同的文檔集中在一起管理,稱為索引(Index)。例如:
商品索引
{"id": 1,"title": "小米手機","price": 3499
}{"id": 2,"title": "華為手機","price": 4999
}{"id": 3,"title": "三星手機","price": 3999
}
用戶索引
{"id": 101,"name": "張三","age": 21
}{"id": 102,"name": "李四","age": 24
}{"id": 103,"name": "麻子","age": 18
}
訂單索引
{"id": 10,"userId": 101,"goodsId": 1,"totalFee": 294
}{"id": 11,"userId": 102,"goodsId": 2,"totalFee": 328
}
- 所有用戶文檔,就可以組織在一起,稱為用戶的索引;
- 所有商品的文檔,可以組織在一起,稱為商品的索引;
- 所有訂單的文檔,可以組織在一起,稱為訂單的索引;
因此,我們可以把索引當做是數據庫中的表。
數據庫的表會有約束信息,用來定義表的結構、字段的名稱、類型等信息。因此,索引庫中就有映射(mapping),是索引中文檔的字段約束信息,類似表的結構約束。
1.3.3.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 |
如圖:
那是不是說,我們學習了elasticsearch就不再需要mysql了呢?
并不是如此,兩者各自有自己的擅長之處:
- Mysql:擅長事務類型操作,可以確保數據的安全和一致性
- Elasticsearch:擅長海量數據的搜索、分析、計算
因此在企業中,往往是兩者結合使用:
- 對安全性要求較高的寫操作,使用mysql實現
- 對查詢性能要求較高的搜索需求,使用elasticsearch實現
- 兩者再基于某種方式,實現數據的同步,保證一致性
1.4.IK分詞器
Elasticsearch的關鍵就是倒排索引,而倒排索引依賴于對文檔內容的分詞,而分詞則需要高效、精準的分詞算法,IK分詞器就是這樣一個中文分詞算法。
1.4.1.安裝IK分詞器
方案一:在線安裝
運行一個命令即可:
docker exec -it es ./bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.12.1/elasticsearch-analysis-ik-7.12.1.zip
然后重啟es容器:
docker restart es
方案二:離線安裝
如果網速較差,也可以選擇離線安裝。
首先,查看之前安裝的Elasticsearch容器的plugins數據卷目錄:
docker volume inspect es-plugins
結果如下:
[{"CreatedAt": "2024-11-06T10:06:34+08:00","Driver": "local","Labels": null,"Mountpoint": "/var/lib/docker/volumes/es-plugins/_data","Name": "es-plugins","Options": null,"Scope": "local"}
]
可以看到elasticsearch的插件掛載到了/var/lib/docker/volumes/es-plugins/_data
這個目錄。我們需要把IK分詞器上傳至這個目錄。
找到課前資料提供的ik分詞器插件,課前資料提供了7.12.1
版本的ik分詞器壓縮文件,你需要對其解壓:
然后上傳至虛擬機的/var/lib/docker/volumes/es-plugins/_data
這個目錄:
最后,重啟es容器:
docker restart es
1.4.2.使用IK分詞器
IK分詞器包含兩種模式:
ik_smart
:智能語義切分ik_max_word
:最細粒度切分
我們在Kibana的DevTools上來測試分詞器,首先測試Elasticsearch官方提供的標準分詞器:
POST /_analyze
{"analyzer": "standard","text": "黑馬程序員學習java太棒了"
}
結果如下:
{"tokens" : [{"token" : "黑","start_offset" : 0,"end_offset" : 1,"type" : "<IDEOGRAPHIC>","position" : 0},{"token" : "馬","start_offset" : 1,"end_offset" : 2,"type" : "<IDEOGRAPHIC>","position" : 1},{"token" : "程","start_offset" : 2,"end_offset" : 3,"type" : "<IDEOGRAPHIC>","position" : 2},{"token" : "序","start_offset" : 3,"end_offset" : 4,"type" : "<IDEOGRAPHIC>","position" : 3},{"token" : "員","start_offset" : 4,"end_offset" : 5,"type" : "<IDEOGRAPHIC>","position" : 4},{"token" : "學","start_offset" : 5,"end_offset" : 6,"type" : "<IDEOGRAPHIC>","position" : 5},{"token" : "習","start_offset" : 6,"end_offset" : 7,"type" : "<IDEOGRAPHIC>","position" : 6},{"token" : "java","start_offset" : 7,"end_offset" : 11,"type" : "<ALPHANUM>","position" : 7},{"token" : "太","start_offset" : 11,"end_offset" : 12,"type" : "<IDEOGRAPHIC>","position" : 8},{"token" : "棒","start_offset" : 12,"end_offset" : 13,"type" : "<IDEOGRAPHIC>","position" : 9},{"token" : "了","start_offset" : 13,"end_offset" : 14,"type" : "<IDEOGRAPHIC>","position" : 10}]
}
可以看到,標準分詞器智能1字1詞條,無法正確對中文做分詞。
我們再測試IK分詞器:
POST /_analyze
{"analyzer": "ik_smart","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" : 5,"end_offset" : 7,"type" : "CN_WORD","position" : 2},{"token" : "java","start_offset" : 7,"end_offset" : 11,"type" : "ENGLISH","position" : 3},{"token" : "太棒了","start_offset" : 11,"end_offset" : 14,"type" : "CN_WORD","position" : 4}]
}
1.4.3.拓展詞典
隨著互聯網的發展,“造詞運動”也越發的頻繁。出現了很多新的詞語,在原有的詞匯列表中并不存在。比如:“泰褲辣”,“傳智播客” 等。
IK分詞器無法對這些詞匯分詞,測試一下:
POST /_analyze
{"analyzer": "ik_max_word","text": "傳智播客開設大學,真的泰褲辣!"
}
結果:
{"tokens" : [{"token" : "傳","start_offset" : 0,"end_offset" : 1,"type" : "CN_CHAR","position" : 0},{"token" : "智","start_offset" : 1,"end_offset" : 2,"type" : "CN_CHAR","position" : 1},{"token" : "播","start_offset" : 2,"end_offset" : 3,"type" : "CN_CHAR","position" : 2},{"token" : "客","start_offset" : 3,"end_offset" : 4,"type" : "CN_CHAR","position" : 3},{"token" : "開設","start_offset" : 4,"end_offset" : 6,"type" : "CN_WORD","position" : 4},{"token" : "大學","start_offset" : 6,"end_offset" : 8,"type" : "CN_WORD","position" : 5},{"token" : "真的","start_offset" : 9,"end_offset" : 11,"type" : "CN_WORD","position" : 6},{"token" : "泰","start_offset" : 11,"end_offset" : 12,"type" : "CN_CHAR","position" : 7},{"token" : "褲","start_offset" : 12,"end_offset" : 13,"type" : "CN_CHAR","position" : 8},{"token" : "辣","start_offset" : 13,"end_offset" : 14,"type" : "CN_CHAR","position" : 9}]
}
可以看到,傳智播客
和泰褲辣
都無法正確分詞。
所以要想正確分詞,IK分詞器的詞庫也需要不斷的更新,IK分詞器提供了擴展詞匯的功能。
1)打開IK分詞器config目錄:
注意,如果采用在線安裝的通過,默認是沒有config目錄的,需要把課前資料提供的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)在IK分詞器的config目錄新建一個 ext.dic
,可以參考config目錄下復制一個配置文件進行修改
傳智播客
泰褲辣
4)重啟elasticsearch
docker restart es# 查看 日志
docker logs -f elasticsearch
再次測試,可以發現傳智播客
和泰褲辣
都正確分詞了:
{"tokens" : [{"token" : "傳智播客","start_offset" : 0,"end_offset" : 4,"type" : "CN_WORD","position" : 0},{"token" : "開設","start_offset" : 4,"end_offset" : 6,"type" : "CN_WORD","position" : 1},{"token" : "大學","start_offset" : 6,"end_offset" : 8,"type" : "CN_WORD","position" : 2},{"token" : "真的","start_offset" : 9,"end_offset" : 11,"type" : "CN_WORD","position" : 3},{"token" : "泰褲辣","start_offset" : 11,"end_offset" : 14,"type" : "CN_WORD","position" : 4}]
}
1.4.4.總結
分詞器的作用是什么?
- 創建倒排索引時,對文檔分詞
- 用戶搜索時,對輸入的內容分詞
IK分詞器有幾種模式?
ik_smart
:智能切分,粗粒度ik_max_word
:最細切分,細粒度
IK分詞器如何拓展詞條?如何停用詞條?
- 利用config目錄的
IkAnalyzer.cfg.xml
文件添加拓展詞典和停用詞典 - 在詞典中添加拓展詞條或者停用詞條
2.索引庫操作
Index就類似數據庫表,Mapping映射就類似表的結構。我們要向es中存儲數據,必須先創建Index和Mapping
2.1.Mapping映射屬性
Mapping是對索引庫中文檔的約束,常見的Mapping屬性包括:
type
:字段數據類型,常見的簡單類型有:
-
- 字符串:
text
(可分詞的文本)、keyword
(精確值,例如:品牌、國家、ip地址) - 數值:
long
、integer
、short
、byte
、double
、float
、 - 布爾:
boolean
- 日期:
date
- 對象:
object
- 字符串:
index
:是否創建索引,默認為true
analyzer
:使用哪種分詞器properties
:該字段的子字段
例如下面的json文檔:
{"age": 21,"weight": 52.1,"isMarried": false,"info": "黑馬程序員Java講師","email": "zy@itcast.cn","score": [99.1, 99.5, 98.9],"name": {"firstName": "云","lastName": "趙"}
}
對應的每個字段映射(Mapping):
字段名 | 字段類型 | 類型說明 | 是否 參與搜索 | 是否 參與分詞 | 分詞器 | |
age |
| 整數 | —— | |||
weight |
| 浮點數 | —— | |||
isMarried |
| 布爾 | —— | |||
info |
| 字符串,但需要分詞 | IK | |||
|
| 字符串,但是不分詞 | —— | |||
score |
| 只看數組中元素類型 | —— | |||
name | firstName |
| 字符串,但是不分詞 | —— | ||
lastName |
| 字符串,但是不分詞 | —— |
2.2.索引庫的CRUD
由于Elasticsearch采用的是Restful風格的API,因此其請求方式和路徑相對都比較規范,而且請求參數也都采用JSON風格。
我們直接基于Kibana的DevTools來編寫請求做測試,由于有語法提示,會非常方便。
2.2.1.創建索引庫和映射
基本語法:
- 請求方式:
PUT
- 請求路徑:
/索引庫名
,可以自定義 - 請求參數:
mapping
映射
格式:
PUT /索引庫名稱
{"mappings": {"properties": {"字段名":{"type": "text","analyzer": "ik_smart"},"字段名2":{"type": "keyword","index": "false"},"字段名3":{"properties": {"子字段": {"type": "keyword"}}},// ...略}}
}
示例:
# PUT /heima
{"mappings": {"properties": {"info":{"type": "text","analyzer": "ik_smart"},"email":{"type": "keyword","index": "false"},"name":{"properties": {"firstName": {"type": "keyword"}}}}}
}
2.2.2.查詢索引庫
基本語法:
- 請求方式:GET
- 請求路徑:/索引庫名
- 請求參數:無
格式:
GET /索引庫名
示例:
GET /heima
2.2.3.修改索引庫
倒排索引結構雖然不復雜,但是一旦數據結構改變(比如改變了分詞器),就需要重新創建倒排索引,這簡直是災難。因此索引庫一旦創建,無法修改mapping。
雖然無法修改mapping中已有的字段,但是卻允許添加新的字段到mapping中,因為不會對倒排索引產生影響。因此修改索引庫能做的就是向索引庫中添加新字段,或者更新索引庫的基礎屬性。
語法說明:
PUT /索引庫名/_mapping
{"properties": {"新字段名":{"type": "integer"}}
}
示例:
PUT /heima/_mapping
{"properties": {"age":{"type": "integer"}}
}
2.2.4.刪除索引庫
語法:
- 請求方式:DELETE
- 請求路徑:/索引庫名
- 請求參數:無
格式:
DELETE /索引庫名
示例:
DELETE /heima
2.2.5.總結
索引庫操作有哪些?
- 創建索引庫:PUT /索引庫名
- 查詢索引庫:GET /索引庫名
- 刪除索引庫:DELETE /索引庫名
- 修改索引庫,添加字段:PUT /索引庫名/_mapping
可以看到,對索引庫的操作基本遵循的Restful的風格,因此API接口非常統一,方便記憶。
3.文檔操作
有了索引庫,接下來就可以向索引庫中添加數據了。
Elasticsearch中的數據其實就是JSON風格的文檔。操作文檔自然保護增
、刪
、改
、查
等幾種常見操作,我們分別來學習。
3.1.新增文檔
語法:
POST /索引庫名/_doc/文檔id
{"字段1": "值1","字段2": "值2","字段3": {"子屬性1": "值3","子屬性2": "值4"},
}
示例:
POST /heima/_doc/1
{"info": "黑馬程序員Java講師","email": "zy@itcast.cn","name": {"firstName": "云","lastName": "趙"}
}
響應:
3.2.查詢文檔
根據rest風格,新增是post,查詢應該是get,不過查詢一般都需要條件,這里我們把文檔id帶上。
語法:
GET /{索引庫名稱}/_doc/{id}
示例:
GET /heima/_doc/1
查看結果:
3.3.刪除文檔
刪除使用DELETE請求,同樣,需要根據id進行刪除:
語法:
DELETE /{索引庫名}/_doc/id值
示例:
DELETE /heima/_doc/1
結果:
3.4.修改文檔
修改有兩種方式:
- 全量修改:直接覆蓋原來的文檔
- 局部修改:修改文檔中的部分字段
3.4.1.全量修改
全量修改是覆蓋原來的文檔,其本質是兩步操作:
- 根據指定的id刪除文檔
- 新增一個相同id的文檔
注意:如果根據id刪除時,id不存在,第二步的新增也會執行,也就從修改變成了新增操作了。
語法:
PUT /{索引庫名}/_doc/文檔id
{"字段1": "值1","字段2": "值2",// ... 略
}
示例:
PUT /heima/_doc/1
{"info": "黑馬程序員高級Java講師","email": "zy@itcast.cn","name": {"firstName": "云","lastName": "趙"}
}
由于id
為1
的文檔已經被刪除,所以第一次執行時,得到的反饋是created
:
所以如果執行第2次時,得到的反饋則是updated
:
3.4.2.局部修改
局部修改是只修改指定id匹配的文檔中的部分字段。
語法:
POST /{索引庫名}/_update/文檔id
{"doc": {"字段名": "新的值",}
}
示例:
POST /heima/_update/1
{"doc": {"email": "ZhaoYun@itcast.cn"}
}
執行結果:
3.5.批處理
批處理采用POST請求,基本語法如下:
POST _bulk
{ "index" : { "_index" : "test", "_id" : "1" } }
{ "field1" : "value1" }
{ "delete" : { "_index" : "test", "_id" : "2" } }
{ "create" : { "_index" : "test", "_id" : "3" } }
{ "field1" : "value3" }
{ "update" : {"_id" : "1", "_index" : "test"} }
{ "doc" : {"field2" : "value2"} }
其中:
index
代表新增操作
-
_index
:指定索引庫名_id
指定要操作的文檔id{ "field1" : "value1" }
:則是要新增的文檔內容
delete
代表刪除操作
-
_index
:指定索引庫名_id
指定要操作的文檔id
update
代表更新操作
-
_index
:指定索引庫名_id
指定要操作的文檔id{ "doc" : {"field2" : "value2"} }
:要更新的文檔字段
示例,批量新增:
POST /_bulk
{"index": {"_index":"heima", "_id": "3"}}
{"info": "黑馬程序員C++講師", "email": "ww@itcast.cn", "name":{"firstName": "五", "lastName":"王"}}
{"index": {"_index":"heima", "_id": "4"}}
{"info": "黑馬程序員前端講師", "email": "zhangsan@itcast.cn", "name":{"firstName": "三", "lastName":"張"}}
批量刪除:
POST /_bulk
{"delete":{"_index":"heima", "_id": "3"}}
{"delete":{"_index":"heima", "_id": "4"}}
3.6.總結
文檔操作有哪些?
- 創建文檔:
POST /{索引庫名}/_doc/文檔id { json文檔 }
- 查詢文檔:
GET /{索引庫名}/_doc/文檔id
- 刪除文檔:
DELETE /{索引庫名}/_doc/文檔id
- 修改文檔:
-
- 全量修改:
PUT /{索引庫名}/_doc/文檔id { json文檔 }
- 局部修改:
POST /{索引庫名}/_update/文檔id { "doc": {字段}}
- 全量修改:
4.RestAPI
ES官方提供了各種不同語言的客戶端,用來操作ES。這些客戶端的本質就是組裝DSL語句,通過http請求發送給ES。
官方文檔地址:
Elasticsearch clients | Elastic Docs
由于ES目前最新版本是8.8,提供了全新版本的客戶端,老版本的客戶端已經被標記為過時。而我們采用的是7.12版本,因此只能使用老版本客戶端:
然后選擇7.12版本,HighLevelRestClient版本:
4.1.初始化RestClient
在elasticsearch提供的API中,與elasticsearch一切交互都封裝在一個名為RestHighLevelClient
的類中,必須先完成這個對象的初始化,建立與elasticsearch的連接。
分為三步:
1)在item-service
模塊中引入es
的RestHighLevelClient
依賴:
<dependency><groupId>org.elasticsearch.client</groupId><artifactId>elasticsearch-rest-high-level-client</artifactId>
</dependency>
2)因為SpringBoot默認的ES版本是7.17.10
,所以我們需要覆蓋默認的ES版本:
<properties><maven.compiler.source>11</maven.compiler.source><maven.compiler.target>11</maven.compiler.target><elasticsearch.version>7.12.1</elasticsearch.version></properties>
3)初始化RestHighLevelClient:
初始化的代碼如下:
RestHighLevelClient client = new RestHighLevelClient(RestClient.builder(HttpHost.create("http://192.168.150.101:9200")
));
這里為了單元測試方便,我們創建一個測試類IndexTest
,然后將初始化的代碼編寫在@BeforeEach
方法中:
package com.hmall.item.es;import org.apache.http.HttpHost;
import org.elasticsearch.client.RestClient;
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 IndexTest {private RestHighLevelClient client;@BeforeEachvoid setUp() {this.client = new RestHighLevelClient(RestClient.builder(HttpHost.create("http://192.168.150.101:9200")));}@Testvoid testConnect() {System.out.println(client);}@AfterEachvoid tearDown() throws IOException {this.client.close();}
}
4.1.創建索引庫
由于要實現對商品搜索,所以我們需要將商品添加到Elasticsearch中,不過需要根據搜索業務的需求來設定索引庫結構,而不是一股腦的把MySQL數據寫入Elasticsearch.
4.1.1.Mapping映射
搜索頁面的效果如圖所示:
實現搜索功能需要的字段包括三大部分:
- 搜索過濾字段
-
- 分類
- 品牌
- 價格
- 排序字段
-
- 默認:按照更新時間降序排序
- 銷量
- 價格
- 展示字段
-
- 商品id:用于點擊后跳轉
- 圖片地址
- 是否是廣告推廣商品
- 名稱
- 價格
- 評價數量
- 銷量
對應的商品表結構如下,索引庫無關字段已經劃掉:
結合數據庫表結構,以上字段對應的mapping映射屬性如下:
字段名 | 字段類型 | 類型說明 | 是否 參與搜索 | 是否 參與分詞 | 分詞器 | |
id |
| 長整數 | —— | |||
name |
| 字符串,參與分詞搜索 | IK | |||
price |
| 以分為單位,所以是整數 | —— | |||
stock |
| 字符串,但需要分詞 | —— | |||
image |
| 字符串,但是不分詞 | —— | |||
category |
| 字符串,但是不分詞 | —— | |||
brand |
| 字符串,但是不分詞 | —— | |||
sold |
| 銷量,整數 | —— | |||
commentCount |
| 評價,整數 | —— | |||
isAD |
| 布爾類型 | —— | |||
updateTime |
| 更新時間 | —— |
因此,最終我們的索引庫文檔結構應該是這樣:
PUT /items
{"mappings": {"properties": {"id": {"type": "keyword"},"name":{"type": "text","analyzer": "ik_max_word"},"price":{"type": "integer"},"stock":{"type": "integer"},"image":{"type": "keyword","index": false},"category":{"type": "keyword"},"brand":{"type": "keyword"},"sold":{"type": "integer"},"commentCount":{"type": "integer","index": false},"isAD":{"type": "boolean"},"updateTime":{"type": "date"}}}
}
4.1.2.創建索引
創建索引庫的API如下:
代碼分為三步:
- 1)創建Request對象。
-
- 因為是創建索引庫的操作,因此Request是
CreateIndexRequest
。
- 因為是創建索引庫的操作,因此Request是
- 2)添加請求參數
-
- 其實就是Json格式的Mapping映射參數。因為json字符串很長,這里是定義了靜態字符串常量
MAPPING_TEMPLATE
,讓代碼看起來更加優雅。
- 其實就是Json格式的Mapping映射參數。因為json字符串很長,這里是定義了靜態字符串常量
- 3)發送請求
-
client.
indices
()
方法的返回值是IndicesClient
類型,封裝了所有與索引庫操作有關的方法。例如創建索引、刪除索引、判斷索引是否存在等
在item-service
中的IndexTest
測試類中,具體代碼如下:
@Test
void testCreateIndex() throws IOException {// 1.創建Request對象CreateIndexRequest request = new CreateIndexRequest("items");// 2.準備請求參數request.source(MAPPING_TEMPLATE, XContentType.JSON);// 3.發送請求client.indices().create(request, RequestOptions.DEFAULT);
}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" +" },\n" +" \"price\":{\n" +" \"type\": \"integer\"\n" +" },\n" +" \"stock\":{\n" +" \"type\": \"integer\"\n" +" },\n" +" \"image\":{\n" +" \"type\": \"keyword\",\n" +" \"index\": false\n" +" },\n" +" \"category\":{\n" +" \"type\": \"keyword\"\n" +" },\n" +" \"brand\":{\n" +" \"type\": \"keyword\"\n" +" },\n" +" \"sold\":{\n" +" \"type\": \"integer\"\n" +" },\n" +" \"commentCount\":{\n" +" \"type\": \"integer\"\n" +" },\n" +" \"isAD\":{\n" +" \"type\": \"boolean\"\n" +" },\n" +" \"updateTime\":{\n" +" \"type\": \"date\"\n" +" }\n" +" }\n" +" }\n" +"}";
4.2.刪除索引庫
刪除索引庫的請求非常簡單:
DELETE /hotel
與創建索引庫相比:
- 請求方式從PUT變為DELTE
- 請求路徑不變
- 無請求參數
所以代碼的差異,注意體現在Request對象上。流程如下:
- 1)創建Request對象。這次是DeleteIndexRequest對象
- 2)準備參數。這里是無參,因此省略
- 3)發送請求。改用delete方法
在item-service
中的IndexTest
測試類中,編寫單元測試,實現刪除索引:
@Test
void testDeleteIndex() throws IOException {// 1.創建Request對象DeleteIndexRequest request = new DeleteIndexRequest("items");// 2.發送請求client.indices().delete(request, RequestOptions.DEFAULT);
}
4.3.判斷索引庫是否存在
判斷索引庫是否存在,本質就是查詢,對應的請求語句是:
GET /hotel
因此與刪除的Java代碼流程是類似的,流程如下:
- 1)創建Request對象。這次是GetIndexRequest對象
- 2)準備參數。這里是無參,直接省略
- 3)發送請求。改用exists方法
@Test
void testExistsIndex() throws IOException {// 1.創建Request對象GetIndexRequest request = new GetIndexRequest("items");// 2.發送請求boolean exists = client.indices().exists(request, RequestOptions.DEFAULT);// 3.輸出System.err.println(exists ? "索引庫已經存在!" : "索引庫不存在!");
}
4.4.總結
JavaRestClient操作elasticsearch的流程基本類似。核心是client.indices()
方法來獲取索引庫的操作對象。
索引庫操作的基本步驟:
- 初始化
RestHighLevelClient
- 創建XxxIndexRequest。XXX是
Create
、Get
、Delete
- 準備請求參數(
Create
時需要,其它是無參,可以省略) - 發送請求。調用
RestHighLevelClient#indices().xxx()
方法,xxx是create
、exists
、delete
5.RestClient操作文檔
索引庫準備好以后,就可以操作文檔了。為了與索引庫操作分離,我們再次創建一個測試類,做兩件事情:
- 初始化RestHighLevelClient
- 我們的商品數據在數據庫,需要利用IHotelService去查詢,所以注入這個接口
package com.hmall.item.es;import com.hmall.item.service.IItemService;
import org.apache.http.HttpHost;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;import java.io.IOException;@SpringBootTest(properties = "spring.profiles.active=local")
public class DocumentTest {private RestHighLevelClient client;@Autowiredprivate IItemService itemService;@BeforeEachvoid setUp() {this.client = new RestHighLevelClient(RestClient.builder(HttpHost.create("http://192.168.150.101:9200")));}@AfterEachvoid tearDown() throws IOException {this.client.close();}
}
5.1.新增文檔
我們需要將數據庫中的商品信息導入elasticsearch中,而不是造假數據了。
5.1.1.實體類
索引庫結構與數據庫結構還存在一些差異,因此我們要定義一個索引庫結構對應的實體。
在hm-service
模塊的com.hmall.item.domain.dto
包中定義一個新的DTO:
package com.hmall.item.domain.po;import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;import java.time.LocalDateTime;@Data
@ApiModel(description = "索引庫實體")
public class ItemDoc{@ApiModelProperty("商品id")private String id;@ApiModelProperty("商品名稱")private String name;@ApiModelProperty("價格(分)")private Integer price;@ApiModelProperty("商品圖片")private String image;@ApiModelProperty("類目名稱")private String category;@ApiModelProperty("品牌名稱")private String brand;@ApiModelProperty("銷量")private Integer sold;@ApiModelProperty("評論數")private Integer commentCount;@ApiModelProperty("是否是推廣廣告,true/false")private Boolean isAD;@ApiModelProperty("更新時間")private LocalDateTime updateTime;
}
5.1.2.API語法
新增文檔的請求語法如下:
POST /{索引庫名}/_doc/1
{"name": "Jack","age": 21
}
對應的JavaAPI如下:
可以看到與索引庫操作的API非常類似,同樣是三步走:
- 1)創建Request對象,這里是
IndexRequest
,因為添加文檔就是創建倒排索引的過程 - 2)準備請求參數,本例中就是Json文檔
- 3)發送請求
變化的地方在于,這里直接使用client.xxx()
的API,不再需要client.indices()
了。
5.1.3.完整代碼
我們導入商品數據,除了參考API模板“三步走”以外,還需要做幾點準備工作:
- 商品數據來自于數據庫,我們需要先查詢出來,得到
Item
對象 Item
對象需要轉為ItemDoc
對象ItemDTO
需要序列化為json
格式
因此,代碼整體步驟如下:
- 1)根據id查詢商品數據
Item
- 2)將
Item
封裝為ItemDoc
- 3)將
ItemDoc
序列化為JSON - 4)創建IndexRequest,指定索引庫名和id
- 5)準備請求參數,也就是JSON文檔
- 6)發送請求
在item-service
的DocumentTest
測試類中,編寫單元測試:
@Test
void testAddDocument() throws IOException {// 1.根據id查詢商品數據Item item = itemService.getById(100002644680L);// 2.轉換為文檔類型ItemDoc itemDoc = BeanUtil.copyProperties(item, ItemDoc.class);// 3.將ItemDTO轉jsonString doc = JSONUtil.toJsonStr(itemDoc);// 1.準備Request對象IndexRequest request = new IndexRequest("items").id(itemDoc.getId());// 2.準備Json文檔request.source(doc, XContentType.JSON);// 3.發送請求client.index(request, RequestOptions.DEFAULT);
}
5.2.查詢文檔
我們以根據id查詢文檔為例
5.2.1.語法說明
查詢的請求語句如下:
GET /{索引庫名}/_doc/{id}
與之前的流程類似,代碼大概分2步:
- 創建Request對象
- 準備請求參數,這里是無參,直接省略
- 發送請求
不過查詢的目的是得到結果,解析為ItemDTO,還要再加一步對結果的解析。示例代碼如下:
可以看到,響應結果是一個JSON,其中文檔放在一個_source
屬性中,因此解析就是拿到_source
,反序列化為Java對象即可。
其它代碼與之前類似,流程如下:
- 1)準備Request對象。這次是查詢,所以是
GetRequest
- 2)發送請求,得到結果。因為是查詢,這里調用
client.get()
方法 - 3)解析結果,就是對JSON做反序列化
5.2.2.完整代碼
在item-service
的DocumentTest
測試類中,編寫單元測試:
@Test
void testGetDocumentById() throws IOException {// 1.準備Request對象GetRequest request = new GetRequest("items").id("100002644680");// 2.發送請求GetResponse response = client.get(request, RequestOptions.DEFAULT);// 3.獲取響應結果中的sourceString json = response.getSourceAsString();ItemDoc itemDoc = JSONUtil.toBean(json, ItemDoc.class);System.out.println("itemDoc= " + ItemDoc);
}
5.3.刪除文檔
刪除的請求語句如下:
DELETE /hotel/_doc/{id}
與查詢相比,僅僅是請求方式從DELETE
變成GET
,可以想象Java代碼應該依然是2步走:
- 1)準備Request對象,因為是刪除,這次是
DeleteRequest
對象。要指定索引庫名和id - 2)準備參數,無參,直接省略
- 3)發送請求。因為是刪除,所以是
client.delete()
方法
在item-service
的DocumentTest
測試類中,編寫單元測試:
@Test
void testDeleteDocument() throws IOException {// 1.準備Request,兩個參數,第一個是索引庫名,第二個是文檔idDeleteRequest request = new DeleteRequest("item", "100002644680");// 2.發送請求client.delete(request, RequestOptions.DEFAULT);
}
5.4.修改文檔
修改我們講過兩種方式:
- 全量修改:本質是先根據id刪除,再新增
- 局部修改:修改文檔中的指定字段值
在RestClient的API中,全量修改與新增的API完全一致,判斷依據是ID:
- 如果新增時,ID已經存在,則修改
- 如果新增時,ID不存在,則新增
這里不再贅述,我們主要關注局部修改的API即可。
5.4.1.語法說明
局部修改的請求語法如下:
POST /{索引庫名}/_update/{id}
{"doc": {"字段名": "字段值","字段名": "字段值"}
}
代碼示例如圖:
與之前類似,也是三步走:
- 1)準備
Request
對象。這次是修改,所以是UpdateRequest
- 2)準備參數。也就是JSON文檔,里面包含要修改的字段
- 3)更新文檔。這里調用
client.update()
方法
5.4.2.完整代碼
在item-service
的DocumentTest
測試類中,編寫單元測試:
@Test
void testUpdateDocument() throws IOException {// 1.準備RequestUpdateRequest request = new UpdateRequest("items", "100002644680");// 2.準備請求參數request.doc("price", 58800,"commentCount", 1);// 3.發送請求client.update(request, RequestOptions.DEFAULT);
}
5.5.批量導入文檔
在之前的案例中,我們都是操作單個文檔。而數據庫中的商品數據實際會達到數十萬條,某些項目中可能達到數百萬條。
我們如果要將這些數據導入索引庫,肯定不能逐條導入,而是采用批處理方案。常見的方案有:
- 利用Logstash批量導入
-
- 需要安裝Logstash
- 對數據的再加工能力較弱
- 無需編碼,但要學習編寫Logstash導入配置
- 利用JavaAPI批量導入
-
- 需要編碼,但基于JavaAPI,學習成本低
- 更加靈活,可以任意對數據做再加工處理后寫入索引庫
接下來,我們就學習下如何利用JavaAPI實現批量文檔導入。
5.5.1.語法說明
批處理與前面講的文檔的CRUD步驟基本一致:
- 創建Request,但這次用的是
BulkRequest
- 準備請求參數
- 發送請求,這次要用到
client.bulk()
方法
BulkRequest
本身其實并沒有請求參數,其本質就是將多個普通的CRUD請求組合在一起發送。例如:
- 批量新增文檔,就是給每個文檔創建一個
IndexRequest
請求,然后封裝到BulkRequest
中,一起發出。 - 批量刪除,就是創建N個
DeleteRequest
請求,然后封裝到BulkRequest
,一起發出
因此BulkRequest
中提供了add
方法,用以添加其它CRUD的請求:
可以看到,能添加的請求有:
IndexRequest
,也就是新增UpdateRequest
,也就是修改DeleteRequest
,也就是刪除
因此Bulk中添加了多個IndexRequest
,就是批量新增功能了。示例:
@Test
void testBulk() throws IOException {// 1.創建RequestBulkRequest request = new BulkRequest();// 2.準備請求參數request.add(new IndexRequest("items").id("1").source("json doc1", XContentType.JSON));request.add(new IndexRequest("items").id("2").source("json doc2", XContentType.JSON));// 3.發送請求client.bulk(request, RequestOptions.DEFAULT);
}
5.5.2.完整代碼
當我們要導入商品數據時,由于商品數量達到數十萬,因此不可能一次性全部導入。建議采用循環遍歷方式,每次導入1000條左右的數據。
item-service
的DocumentTest
測試類中,編寫單元測試:
@Test
void testLoadItemDocs() throws IOException {// 分頁查詢商品數據int pageNo = 1;int size = 1000;while (true) {Page<Item> page = itemService.lambdaQuery().eq(Item::getStatus, 1).page(new Page<Item>(pageNo, size));// 非空校驗List<Item> items = page.getRecords();if (CollUtils.isEmpty(items)) {return;}log.info("加載第{}頁數據,共{}條", pageNo, items.size());// 1.創建RequestBulkRequest request = new BulkRequest("items");// 2.準備參數,添加多個新增的Requestfor (Item item : items) {// 2.1.轉換為文檔類型ItemDTOItemDoc itemDoc = BeanUtil.copyProperties(item, ItemDoc.class);// 2.2.創建新增文檔的Request對象request.add(new IndexRequest().id(itemDoc.getId()).source(JSONUtil.toJsonStr(itemDoc), XContentType.JSON));}// 3.發送請求client.bulk(request, RequestOptions.DEFAULT);// 翻頁pageNo++;}
}
5.6.小結
文檔操作的基本步驟:
- 初始化
RestHighLevelClient
- 創建XxxRequest。
-
- XXX是
Index
、Get
、Update
、Delete
、Bulk
- XXX是
- 準備參數(
Index
、Update
、Bulk
時需要) - 發送請求。
-
- 調用
RestHighLevelClient#.xxx()
方法,xxx是index
、get
、update
、delete
、bulk
- 調用
- 解析結果(
Get
時需要)
6.作業
6.1.服務拆分
搜索業務并發壓力可能會比較高,目前與商品服務在一起,不方便后期優化。
需求:創建一個新的微服務,命名為search-service
,將搜索相關功能抽取到這個微服務中
6.2.商品查詢接口
在item-service
服務中提供一個根據id查詢商品的功能,并編寫對應的FeignClient
6.3.數據同步
每當商品服務對商品實現增刪改時,索引庫的數據也需要同步更新。
提示:可以考慮采用MQ異步通知實現。