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 地理坐標查詢
所謂的地理坐標查詢,其實就是根據經緯度查詢,官方檔案:https://www.elastic.co/guide/en/elasticsearch/reference/current/geo-queries.html
常見的使用場景包括:
- 攜程:搜索我附近的酒店
- 滴滴:搜索我附近的出租車
- 微信:搜索我附近的人
附近的酒店:

附近的車:

1.4.1 矩形范圍查詢
很少有業務有這種需求
矩形范圍查詢,也就是geo_bounding_box查詢,查詢坐標落在某個矩形范圍的所有檔案:

查詢時,需要指定矩形的左上、右下兩個點的坐標,然后畫出一個矩形,落在該矩形內的都是符合條件的點,
語法如下:
// geo_bounding_box查詢
GET /indexName/_search
{
"query": {
"geo_bounding_box": {
"FIELD": {
"top_left": { // 左上點
"lat": 31.1,
"lon": 121.5
},
"bottom_right": { // 右下點
"lat": 30.9,
"lon": 121.7
}
}
}
}
}
1.4.2 附近(圓形)查詢
附近查詢,也叫做距離查詢(geo_distance):查詢到指定中心點小于某個距離值的所有檔案,
換句話來說,在地圖上找一個點作為圓心,以指定距離為半徑,畫一個圓,落在圓內的坐標都算符合條件:

語法說明:
// geo_distance 查詢
GET /indexName/_search
{
"query": {
"geo_distance": {
"distance": "15km", // 半徑
"FIELD": "31.21,121.5" // 圓心
}
}
}
示例:
我們先搜索陸家嘴附近15km的酒店:

發現共有47家酒店,
1.5 復合查詢
復合(compound)查詢:復合查詢可以將其它簡單查詢組合起來,實作更復雜的搜索邏輯,常見的有兩種:
- fuction score:算分函式查詢,可以控制檔案相關性算分,控制檔案排名
- bool query:布爾查詢,利用邏輯關系組合多個其它的查詢,實作復雜搜索
1.5.0 復合查詢歸納
GET /hotel/_search
{
"query": {
"function_score": {
"query": { // 原始查詢,可以是任意條件
"bool": {
"must": [
{"term": {"city": "上海" }}
],
"should": [
{"term": {"brand": "皇冠假日" }},
{"term": {"brand": "華美達" }}
],
"must_not": [
{ "range": { "price": { "lte": 500 } }}
],
"filter": [
{ "range": {"score": { "gte": 45 } }}
]
}
},
"functions": [ // 算分函式
{
"filter": { // 滿足的條件,品牌必須是如家【品牌是如家的才加分,這里是加分條件】
"term": {
"brand": "如家"
}
},
"weight": 2 // 算分權重為2
}
],
"boost_mode": "sum" // 加權模式,求和
}
}
}
1.5.1 相關性算分
elasticsearch會根據詞條和檔案的相關度做打分,演算法由兩種:
- TF-IDF演算法
- BM25演算法,elasticsearch5.1版本后采用的演算法
當我們利用match查詢時,檔案結果會根據與搜索詞條的關聯度打分(_score),回傳結果時按照分值降序排列,
例如,我們搜索 "虹橋如家",結果如下:
[
{
"_score" : 17.850193,
"_source" : {
"name" : "虹橋如家酒店真不錯",
}
},
{
"_score" : 12.259849,
"_source" : {
"name" : "外灘如家酒店真不錯",
}
},
{
"_score" : 11.91091,
"_source" : {
"name" : "迪士尼如家酒店真不錯",
}
}
]
在elasticsearch中,早期使用的打分演算法是TF-IDF演算法,公式如下:

在后來的5.1版本升級中,elasticsearch將演算法改進為BM25演算法,公式如下:

TF-IDF演算法有一各缺陷,就是詞條頻率越高,檔案得分也會越高,單個詞條對檔案影響較大,而BM25則會讓單個詞條的算分有一個上限,曲線更加平滑:

1.5.2 算分函式查詢
在搜索出來的結果的分數基礎上,再手動與指定的數字進行一定運算來改變算分,從而改變結果的排序,
function score query定義的三要素是什么?
- 過濾條件:哪些檔案要加分
- 算分函式:如何計算function score
- 加權方式:function score 與 query score如何運算
根據相關度打分是比較合理的需求,但合理的不一定是產品經理需要的,
以百度為例,你搜索的結果中,并不是相關度越高排名越靠前,而是誰掏的錢多排名就越靠前,如圖:

要想認為控制相關性算分,就需要利用elasticsearch中的function score 查詢了,
function score 查詢
1)語法說明

function score 查詢中包含四部分內容:
- 原始查詢條件:query部分,基于這個條件搜索檔案,并且基于BM25演算法給檔案打分,原始算分(query score)
- 過濾條件:filter部分,符合該條件的檔案才會重新算分
- 算分函式:符合filter條件的檔案要根據這個函式做運算,得到的函式算分(function score),有四種函式
- weight:函式結果是常量
- field_value_factor:以檔案中的某個欄位值作為函式結果
- random_score:以亂數作為函式結果
- script_score:自定義算分函式演算法
- 運算模式:算分函式的結果、原始查詢的相關性算分,兩者之間的運算方式,包括:
- multiply:相乘
- replace:用function score替換query score
- 其它,例如:sum、avg、max、min
function score的運行流程如下:
- 1)根據原始條件查詢搜索檔案,并且計算相關性算分,稱為原始算分(query score)
- 2)根據過濾條件,過濾檔案
- 3)符合過濾條件的檔案,基于算分函式運算,得到函式算分(function score)
- 4)將原始算分(query score)和函式算分(function score)基于運算模式做運算,得到最終結果,作為相關性算分,
2)舉例
需求:給“如家”這個品牌的酒店排名靠前一些
翻譯一下這個需求,轉換為之前說的四個要點:
- 原始條件:不確定,可以任意變化
- 過濾條件:brand = "如家"
- 算分函式:可以簡單粗暴,直接給固定的算分結果,weight
- 運算模式:比如求和
因此最終的DSL陳述句如下:
GET /hotel/_search
{
"query": {
"function_score": {
"query": { .... }, // 原始查詢,可以是任意條件
"functions": [ // 算分函式
{
"filter": { // 滿足的條件,品牌必須是如家【品牌是如家的才加分,這里是加分條件】
"term": {
"brand": "如家"
}
},
"weight": 2 // 算分權重為2
}
],
"boost_mode": "sum" // 加權模式,求和
}
}
}
測驗,在未添加算分函式時,如家得分如下:

添加了算分函式后,如家得分就提升了:

1.5.3 布爾查詢
布爾查詢是一個或多個查詢子句的組合,每一個子句就是一個子查詢,子查詢的組合方式有:
- must:必須匹配每個子查詢,類似“與”
- should:選擇性匹配子查詢,類似“或”
- must_not:必須不匹配,不參與算分,類似“非”
- filter:必須匹配,不參與算分
注意:盡量在篩選的時候多使用不參與算分的must_not和filter,以保證性能良好
比如在搜索酒店時,除了關鍵字搜索外,我們還可能根據品牌、價格、城市等欄位做過濾:

每一個不同的欄位,其查詢的條件、方式都不一樣,必須是多個不同的查詢,而要組合這些查詢,就必須用bool查詢了,
需要注意的是,搜索時,參與打分的欄位越多,查詢的性能也越差,因此這種多條件查詢時,建議這樣做:
- 搜索框的關鍵字搜索,是全文檢索查詢,使用must查詢,參與算分
- 其它過濾條件,采用filter查詢,不參與算分
bool查詢
1)語法
GET /hotel/_search
{
"query": {
"bool": {
"must": [
{"term": {"city": "上海" }}
],
"should": [
{"term": {"brand": "皇冠假日" }},
{"term": {"brand": "華美達" }}
],
"must_not": [
{ "range": { "price": { "lte": 500 } }}
],
"filter": [
{ "range": {"score": { "gte": 45 } }}
]
}
}
}
2)示例
需求:搜索名字包含“如家”,價格不高于400,在坐標31.21,121.5周圍10km范圍內的酒店,
分析:
- 名稱搜索,屬于全文檢索查詢,應該參與算分,放到must中
- 價格不高于400,用range查詢,屬于過濾條件,不參與算分,放到must_not中
- 周圍10km范圍內,用geo_distance查詢,屬于過濾條件,不參與算分,放到filter中

2. 設定搜索結果
搜索的結果可以按照用戶指定的方式去處理或展示,
2.0 搜索結果種類
查詢的DSL是一個大的JSON物件,包含下列屬性:
- query:查詢條件
- from和size:分頁條件
- sort:排序條件
- highlight:高亮條件
- aggs:定義聚合
示例:


2.1 排序
在使用排序后就不會進行算分了,根據排序設定的規則排列
普通欄位是根據字典序排序
地理坐標是根據舉例遠近排序
2.1.1普通欄位排序
keyword、數值、日期型別排序的排序語法基本一致,
語法:
排序條件是一個陣列,也就是可以寫多個排序條件,按照宣告的順序,當第一個條件相等時,再按照第二個條件排序,以此類推
(可以參考下面的圖片案例)
GET /indexName/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"FIELD": "desc" // 排序欄位、排序方式ASC、DESC
}
]
}
示例:
需求描述:酒店資料按照用戶評價(score)降序排序,評價相同的按照價格(price)升序排序

2.1.2 地理坐標排序
地理坐標排序略有不同,
語法說明:
GET /indexName/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"_geo_distance" : {
"FIELD" : "緯度,經度", // 檔案中geo_point型別的欄位名、目標坐標點
"order" : "asc", // 排序方式
"unit" : "km" // 排序的距離單位
}
}
]
}
這個查詢的含義是:
- 指定一個坐標,作為目標點
- 計算每一個檔案中,指定欄位(必須是geo_point型別)的坐標 到目標點的距離是多少
- 根據距離排序
示例:
需求描述:實作對酒店資料按照到你的位置坐標的距離升序排序
提示:獲取你的位置的經緯度的方式:https://lbs.amap.com/demo/jsapi-v2/example/map/click-to-get-lnglat/
假設我的位置是:31.034661,121.612282,尋找我周圍距離最近的酒店,

2.2 分頁
elasticsearch會禁止from+ size 超過10000的請求
elasticsearch 默認情況下只回傳top10的資料,而如果要查詢更多資料就需要修改分頁引數了,elasticsearch中通過修改from、size引數來控制要回傳的分頁結果:
- from:從第幾個檔案開始
- size:總共查詢幾個檔案
類似于mysql中的limit ?, ?
2.2.1 基本分頁
分頁的基本語法如下:
GET /hotel/_search
{
"query": {
"match_all": {}
},
"from": 0, // 分頁開始的位置,默認為0
"size": 10, // 期望獲取的檔案總數
"sort": [
{"price": "asc"}
]
}
2.2.2 深度分頁
原理:elasticsearch內部分頁時,必須先查詢 0~1000條,然后截取其中的990 ~ 1000的這10條
現在,我要查詢990~1000的資料,查詢邏輯要這么寫:
GET /hotel/_search
{
"query": {
"match_all": {}
},
"from": 990, // 分頁開始的位置,默認為0
"size": 10, // 期望獲取的檔案總數
"sort": [
{"price": "asc"}
]
}
這里是查詢990開始的資料,也就是 第990~第1000條 資料,
集群情況的深度分頁
針對深度分頁,ES提供了兩種解決方案,官方檔案:
- search after:分頁時需要排序,原理是從上一次的排序值開始,查詢下一頁資料,【官方推薦】
- scroll:原理將排序后的檔案id形成快照,保存在記憶體,
不過,elasticsearch內部分頁時,必須先查詢 0~1000條,然后截取其中的990 ~ 1000的這10條:

查詢TOP1000,如果es是單點模式,這并無太大影響,
但是elasticsearch將來一定是集群,例如我集群有5個節點,我要查詢TOP1000的資料,并不是每個節點查詢200條就可以了,
因為節點A的TOP200,在另一個節點可能排到10000名以外了,
因此要想獲取整個集群的TOP1000,必須先查詢出每個節點的TOP1000,匯總結果后,重新排名,重新截取TOP1000,

那如果我要查詢9900~10000的資料呢?是不是要先查詢TOP10000呢?那每個節點都要查詢10000條?匯總到記憶體中?
當查詢分頁深度較大時,匯總資料過多,對記憶體和CPU會產生非常大的壓力,因此elasticsearch會禁止from+ size 超過10000的請求,
2.3 高亮
注意:
- 高亮是對關鍵字高亮,因此搜索條件必須帶有關鍵字,而不能是范圍這樣的查詢,
- 默認情況下,高亮的欄位,必須與搜索指定的欄位一致,否則無法高亮
- 如果要對非搜索欄位高亮,則需要添加一個屬性:required_field_match=false
使用場景:在百度等搜索后,會對結果中出現搜索欄位的部分進行高亮處理,
高亮原理
高亮顯示的實作分為兩步:
- 1)給檔案中的所有關鍵字都添加一個標簽,例如
<em>標簽 - 2)頁面給
<em>標簽撰寫CSS樣式
實作高亮
1)語法
GET /hotel/_search
{
"query": {
"match": {
"FIELD": "TEXT" // 查詢條件,高亮一定要使用全文檢索查詢
}
},
"highlight": {
"fields": { // 指定要高亮的欄位
"FIELD": { //【要和上面的查詢欄位FIELD一致】
"pre_tags": "<em>", // 用來標記高亮欄位的前置標簽
"post_tags": "</em>" // 用來標記高亮欄位的后置標簽
}
}
}
}
2)示例:組合欄位all的案例

2.4 資料聚合
類似于mysql中的【度量(Metric)聚合】聚合陳述句實作AVG,MAX,MIN;以及【桶(Bucket)聚合】GroupBy實作分組
聚合(aggregations)可以讓我們極其方便的實作對資料的統計、分析、運算,例如:
- 什么品牌的手機最受歡迎?
- 這些手機的平均價格、最高價格、最低價格?
- 這些手機每月的銷售情況如何?
實作這些統計功能的比資料庫的sql要方便的多,而且查詢速度非常快,可以實作近實時搜索效果,
aggs代表聚合,與query同級,此時query的作用是?
- 限定聚合的的檔案范圍
聚合必須的三要素:
- 聚合名稱
- 聚合型別
- 聚合欄位
聚合可配置屬性有:
- size:指定聚合結果數量
- order:指定聚合結果排序方式
- field:指定聚合欄位
2.4.1 聚合種類
注意:參加聚合的欄位必須是keyword、日期、數值、布爾型別
聚合常見的有三類:
-
桶(Bucket)聚合:用來對檔案做分組
- TermAggregation:按照檔案欄位值分組,例如按照品牌值分組、按照國家分組
- Date Histogram:按照日期階梯分組,例如一周為一組,或者一月為一組
-
度量(Metric)聚合:用以計算一些值,比如:最大值、最小值、平均值等
- Avg:求平均值
- Max:求最大值
- Min:求最小值
- Stats:同時求max、min、avg、sum等
-
管道(pipeline)聚合:其它聚合的結果為基礎做聚合
如:用桶聚合實作種類排序,然后使用度量聚合實作各個桶的最大值、最小值、平均值等
2.4.2 桶(Bucket)聚合
以統計酒店品牌種類,并對其進行資料分組
GET /hotel/_search
{
"query": { //限定要聚合的檔案范圍,只要添加query條件【一般在沒搜索關鍵字時不寫query】
"range": {
"price": {
"lte": 200 // 只對200元以下的檔案聚合
}
}
},
"size": 0, // 設定size為0,結果中不包含查詢結果檔案,只包含聚合結果
"aggs": { // 定義聚合
"brandAgg": { //給聚合起個名字
"terms": { // 聚合的型別,按照品牌值聚合,所以選擇term
"field": "brand", // 參與聚合的欄位
"order": {
"doc_count": "asc" // 對聚合結果按照doc_count升序排列
},
"size": 20 // 希望獲取的聚合結果數量【設定多少就最多只顯示多少】
}
}
}
}

2.4.3 度量(Metric) and 管道(pipeline)聚合
度量聚合很少單獨使用,一般是和桶聚合一并結合使用
我們對酒店按照品牌分組,形成了一個個桶,現在我們需要對桶內的酒店做運算,獲取每個品牌的用戶評分的min、max、avg等值,
這就要用到Metric聚合了,例如stat聚合:就可以獲取min、max、avg等結果,
語法如下:
這次的score_stats聚合是在brandAgg的聚合內部嵌套的子聚合,因為我們需要在每個桶分別計算,
GET /hotel/_search
{
"size": 0,
"aggs": {
"brandAgg": {
"terms": {
"field": "brand",
"order": {
"scoreAgg.avg": "desc" // 對聚合結果按照指定欄位降序排列
},
"size": 20
},
"aggs": { // 是brands聚合的子聚合,也就是分組后對每組分別計算
"score_stats": { // 聚合名稱
"stats": { // 聚合型別,這里stats可以計算min、max、avg等
"field": "score" // 聚合欄位,這里是score
}
}
}
}
}
}
另外,我們還可以給聚合結果做個排序,例如按照每個桶的酒店平均分做排序:

3. RestClient查詢檔案
檔案的查詢同樣適用昨天學習的 RestHighLevelClient物件,基本步驟包括:
- 1)準備Request物件
- 2)準備請求引數
- 3)發起請求
- 4)決議回應
3.1 快速入門
查詢的基本步驟是:
創建SearchRequest物件
準備Request.source(),也就是DSL,
① QueryBuilders來構建查詢條件
② 傳入Request.source() 的 query() 方法
發送請求,得到結果
決議結果(參考JSON結果,從外到內,逐層決議)
3.1.1 發送查詢請求

代碼解讀:
-
第一步,創建
SearchRequest物件,指定索引庫名 -
第二步,利用
request.source()構建DSL,DSL中可以包含查詢、分頁、排序、高亮等query():代表查詢條件,利用QueryBuilders.matchAllQuery()構建一個match_all查詢的DSL
-
第三步,利用client.search()發送請求,得到回應
這里關鍵的API有兩個,一個是request.source(),其中包含了查詢、排序、分頁、高亮等所有功能:

另一個是QueryBuilders,其中包含match、term、function_score、bool等各種查詢:

3.1.2 決議回應結果
回應結果的決議:

elasticsearch回傳的結果是一個JSON字串,結構包含:
hits:命中的結果total:總條數,其中的value是具體的總條數值max_score:所有結果中得分最高的檔案的相關性算分hits:搜索結果的檔案陣列,其中的每個檔案都是一個json物件_source:檔案中的原始資料,也是json物件
因此,我們決議回應結果,就是逐層決議JSON字串,流程如下:
SearchHits:通過response.getHits()獲取,就是JSON中的最外層的hits,代表命中的結果SearchHits#getTotalHits().value:獲取總條數資訊SearchHits#getHits():獲取SearchHit陣列,也就是檔案陣列SearchHit#getSourceAsString():獲取檔案結果中的_source,也就是原始的json檔案資料
3.1.3 完整代碼
完整代碼如下:
@Test
void testMatchAll() throws IOException {
// 1.準備Request
SearchRequest request = new SearchRequest("hotel");
// 2.準備DSL
request.source()
.query(QueryBuilders.matchAllQuery());
// 3.發送請求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 4.決議回應
handleResponse(response);
}
private void handleResponse(SearchResponse response) {
// 4.決議回應
SearchHits searchHits = response.getHits();
// 4.1.獲取總條數
long total = searchHits.getTotalHits().value;
System.out.println("共搜索到" + total + "條資料");
// 4.2.檔案陣列
SearchHit[] hits = searchHits.getHits();
// 4.3.遍歷
for (SearchHit hit : hits) {
// 獲取檔案source
String json = hit.getSourceAsString();
// 反序列化
HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
System.out.println("hotelDoc = " + hotelDoc);
}
}
3.2 設定查詢條件
3.2.1 全文檢索查詢
全文檢索的match和multi_match查詢與match_all的API基本一致,差別是查詢條件,也就是query的部分,

因此,Java代碼上的差異主要是request.source().query()中的引數了,同樣是利用QueryBuilders提供的方法:

而結果決議代碼則完全一致,可以抽取并共享,
完整代碼如下:
@Test
void testMatch() throws IOException {
// 1.準備Request
SearchRequest request = new SearchRequest("hotel");
// 2.準備DSL
request.source()
.query(QueryBuilders.matchQuery("all", "如家"));
// 3.發送請求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 4.決議回應
handleResponse(response);
}
3.2.2 精準查詢
精確查詢主要是兩者:
- term:詞條精確匹配
- range:范圍查詢
與之前的查詢相比,差異同樣在查詢條件,其它都一樣,
查詢條件構造的API如下:

3.2.3 地理查詢
DSL格式

在cn.itcast.hotel.service.impl的HotelService的search方法中,添加一個排序功能:

完整代碼:
@Override
public PageResult search(RequestParams params) {
try {
// 1.準備Request
SearchRequest request = new SearchRequest("hotel");
// 2.準備DSL
// 2.1.query
buildBasicQuery(params, request);
// 2.2.分頁
int page = params.getPage();
int size = params.getSize();
request.source().from((page - 1) * size).size(size);
// 2.3.排序
String location = params.getLocation();
if (location != null && !location.equals("")) {
request.source().sort(SortBuilders
.geoDistanceSort("location", new GeoPoint(location))
.order(SortOrder.ASC)
.unit(DistanceUnit.KILOMETERS)
);
}
// 3.發送請求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 4.決議回應
return handleResponse(response);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
3.2.4 布爾查詢
布爾查詢是用must、must_not、filter等方式組合其它查詢,代碼示例如下:

可以看到,API與其它查詢的差別同樣是在查詢條件的構建,QueryBuilders,結果決議等其他代碼完全不變,
完整代碼如下:
@Test
void testBool() throws IOException {
// 1.準備Request
SearchRequest request = new SearchRequest("hotel");
// 2.準備DSL
// 2.1.準備BooleanQuery
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
// 2.2.添加term
boolQuery.must(QueryBuilders.termQuery("city", "杭州"));
// 2.3.添加range
boolQuery.filter(QueryBuilders.rangeQuery("price").lte(250));
request.source().query(boolQuery);
// 3.發送請求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 4.決議回應
handleResponse(response);
}
3.2.5 算分函式查詢
java代碼邏輯:添加一個isAD欄位,在算分函式的filter中判斷
isAD=ture就進行重新算分
function_score查詢結構如下:

對應的JavaAPI如下:

我們可以將之前寫的boolean查詢作為原始查詢條件放到query中,接下來就是添加過濾條件、算分函式、加權模式了,
// 算分控制
FunctionScoreQueryBuilder functionScoreQuery =
QueryBuilders.functionScoreQuery(
// 原始查詢,相關性算分的查詢
boolQuery,
// function score的陣列
new FunctionScoreQueryBuilder.FilterFunctionBuilder[]{
// 其中的一個function score 元素
new FunctionScoreQueryBuilder.FilterFunctionBuilder(
// 過濾條件
QueryBuilders.termQuery("isAD", true),
// 算分函式
ScoreFunctionBuilders.weightFactorFunction(10)
)
});
//將查詢請求放入查詢
request.source().query(functionScoreQuery);
3.3 設定搜索結果
3.3.1 排序和分頁
由于這兩個比較簡單,所以一起寫了
搜索結果的排序和分頁是與query同級的引數,因此同樣是使用request.source()來設定,
對應的API如下:

完整代碼示例:
@Test
void testPageAndSort() throws IOException {
// 頁碼,每頁大小
int page = 1, size = 5;
// 1.準備Request
SearchRequest request = new SearchRequest("hotel");
// 2.準備DSL
// 2.1.query
request.source().query(QueryBuilders.matchAllQuery());
// 2.2.排序 sort
request.source().sort("price", SortOrder.ASC);
// 2.3.分頁 from、size
request.source().from((page - 1) * size).size(5);
// 3.發送請求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 4.決議回應
handleResponse(response);
}
3.3.2 高亮
高亮的代碼與之前代碼差異較大,有兩點:
- 查詢的DSL:其中除了查詢條件,還需要添加高亮條件,同樣是與query同級,
- 結果決議:結果除了要決議_source檔案資料,還要決議高亮結果
1)高亮請求構建
高亮請求的構建API如下:

上述代碼省略了查詢條件部分,但是大家不要忘了:高亮查詢必須使用全文檢索查詢,并且要有搜索關鍵字,將來才可以對關鍵字高亮,
完整代碼如下:
@Test
void testHighlight() throws IOException {
// 1.準備Request
SearchRequest request = new SearchRequest("hotel");
// 2.準備DSL
// 2.1.query
request.source().query(QueryBuilders.matchQuery("all", "如家"));
// 2.2.高亮
request.source().highlighter(new HighlightBuilder().field("name").requireFieldMatch(false));
// 3.發送請求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 4.決議回應
handleResponse(response);
}
2)高亮結果決議
高亮的結果與查詢的檔案結果默認是分離的,并不在一起,
因此決議高亮的代碼需要額外處理:

代碼解讀:
- 第一步:從結果中獲取source,hit.getSourceAsString(),這部分是非高亮結果,json字串,還需要反序列為HotelDoc物件
- 第二步:獲取高亮結果,hit.getHighlightFields(),回傳值是一個Map,key是高亮欄位名稱,值是HighlightField物件,代表高亮值
- 第三步:從map中根據高亮欄位名稱,獲取高亮欄位值物件HighlightField
- 第四步:從HighlightField中獲取Fragments,并且轉為字串,這部分就是真正的高亮字串了
- 第五步:用高亮的結果替換HotelDoc中的非高亮結果
完整代碼如下:
private void handleResponse(SearchResponse response) {
// 4.決議回應
SearchHits searchHits = response.getHits();
// 4.1.獲取總條數
long total = searchHits.getTotalHits().value;
System.out.println("共搜索到" + total + "條資料");
// 4.2.檔案陣列
SearchHit[] hits = searchHits.getHits();
// 4.3.遍歷
for (SearchHit hit : hits) {
// 獲取檔案source
String json = hit.getSourceAsString();
// 反序列化
HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
// 獲取高亮結果
Map<String, HighlightField> highlightFields = hit.getHighlightFields();
if (!CollectionUtils.isEmpty(highlightFields)) {
// 根據欄位名獲取高亮結果
HighlightField highlightField = highlightFields.get("name");
if (highlightField != null) {
// 獲取高亮值
String name = highlightField.getFragments()[0].string();
// 覆寫非高亮結果
hotelDoc.setName(name);
}
}
System.out.println("hotelDoc = " + hotelDoc);
}
}
3.3.3 聚合
聚合條件與query條件同級別,因此需要使用request.source()來指定聚合條件,
聚合條件的語法:

聚合的結果也與查詢結果不同,API也比較特殊,不過同樣是JSON逐層決議:

舉例:業務代碼
@Override
public Map<String, List<String>> filters(RequestParams params) {
try {
// 1.準備Request
SearchRequest request = new SearchRequest("hotel");
// 2.準備DSL
// 2.1.query查詢陳述句
buildBasicQuery(params, request);
// 2.2.設定size
request.source().size(0);
// 2.3.聚合
buildAggregation(request);
// 3.發出請求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 4.決議結果
Map<String, List<String>> result = new HashMap<>();
Aggregations aggregations = response.getAggregations();
// 4.1.根據品牌名稱,獲取品牌結果
List<String> brandList = getAggByName(aggregations, "brandAgg");
result.put("品牌", brandList);
// 4.2.根據品牌名稱,獲取品牌結果
List<String> cityList = getAggByName(aggregations, "cityAgg");
result.put("城市", cityList);
// 4.3.根據品牌名稱,獲取品牌結果
List<String> starList = getAggByName(aggregations, "starAgg");
result.put("星級", starList);
return result;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private void buildAggregation(SearchRequest request) {
request.source().aggregation(AggregationBuilders
.terms("brandAgg")
.field("brand")
.size(100)
);
request.source().aggregation(AggregationBuilders
.terms("cityAgg")
.field("city")
.size(100)
);
request.source().aggregation(AggregationBuilders
.terms("starAgg")
.field("starName")
.size(100)
);
}
private List<String> getAggByName(Aggregations aggregations, String aggName) {
// 4.1.根據聚合名稱獲取聚合結果
Terms brandTerms = aggregations.get(aggName);
// 4.2.獲取buckets
List<? extends Terms.Bucket> buckets = brandTerms.getBuckets();
// 4.3.遍歷
List<String> brandList = new ArrayList<>();
for (Terms.Bucket bucket : buckets) {
// 4.4.獲取key
String key = bucket.getKeyAsString();
brandList.add(key);
}
return brandList;
}
自動補全
① 設定創建索引庫(設定一個自動補全欄位,型別為:completion)
② 重新插入資料
③ 查詢(查詢時要設定這個自動補全操作的名稱,并且指定那個型別為completion的欄位)
④ 分解結果(結果也需要根據之前設定這個自動查詢操作的名稱來取)
當用戶在搜索框輸入字符時,我們應該提示出與該字符有關的搜索項,如圖:

這種根據用戶輸入的字母,提示完整詞條的功能,就是自動補全了,
1. 拼音分詞器
下載拼音分詞器記得版本要和ES對應,不對應會報錯
要實作根據字母做補全,就必須對檔案按照拼音分詞,在GitHub上恰好有elasticsearch的拼音分詞插件,地址:https://github.com/medcl/elasticsearch-analysis-pinyin

課前資料中也提供了拼音分詞器的安裝包:

安裝方式與IK分詞器一樣,分三步:
? ①解壓
? ②上傳到虛擬機中,elasticsearch的plugin目錄
? ③重啟elasticsearch
? ④測驗
詳細安裝步驟可以參考IK分詞器的安裝程序,
2. 自定義拼音分詞器
如何使用拼音分詞器?
①下載pinyin分詞器
②解壓并放到elasticsearch的plugin目錄
③重啟即可
如何自定義分詞器?
①創建索引庫時,在settings中配置,可以包含三部分
②character filter
③tokenizer
④filter
拼音分詞器注意事項?
- 為了避免搜索到同音字,搜索時不要使用拼音分詞器
默認的拼音分詞器會將每個漢字單獨分為拼音,而我們希望的是每個詞條形成一組拼音,需要對拼音分詞器做個性化定制,形成自定義分詞器,官網檔案查詢地址:https://github.com/medcl/elasticsearch-analysis-pinyin
elasticsearch中分詞器(analyzer)的組成包含三部分:
- character filters:在tokenizer之前對文本進行處理,例如洗掉字符、替換字符
- tokenizer:將文本按照一定的規則切割成詞條(term),例如keyword,就是不分詞;還有ik_smart
- tokenizer filter:將tokenizer輸出的詞條做進一步處理,例如大小寫轉換、同義詞處理、拼音處理等
檔案分詞時會依次由這三部分來處理檔案:

宣告自定義分詞器的語法如下:
PUT /test
{
"settings": {
"analysis": {
"analyzer": { // 自定義分詞器
"my_analyzer": { // 分詞器名稱
"tokenizer": "ik_max_word",
"filter": "py"
}
},
"filter": { // 自定義tokenizer filter
"py": { // 過濾器名稱
"type": "pinyin", // 過濾器型別,這里是pinyin
"keep_full_pinyin": false,
"keep_joined_full_pinyin": true,
"keep_original": true,
"limit_first_letter_length": 16,
"remove_duplicated_term": true,
"none_chinese_pinyin_tokenize": false
}
}
}
},
"mappings": {
"properties": {
"name": {
"type": "text",
"analyzer": "my_analyzer",
"search_analyzer": "ik_smart"
}
}
}
}
測驗:

3. 自動補全查詢
三步驟:
① 創建索引庫
② 插入資料
③ 查詢的DSL陳述句
elasticsearch提供了Completion Suggester查詢來實作自動補全功能,這個查詢會匹配以用戶輸入內容開頭的詞條并回傳,為了提高補全查詢的效率,對于檔案中欄位的型別有一些約束:
-
參與補全查詢的欄位必須是completion型別,
-
欄位的內容一般是用來補全的多個詞條形成的陣列,
比如,一個這樣的索引庫:
// 創建索引庫
PUT test
{
"mappings": {
"properties": {
"title":{
"type": "completion"
}
}
}
}
然后插入下面的資料:
// 示例資料
POST test/_doc
{
"title": ["Sony", "WH-1000XM3"]
}
POST test/_doc
{
"title": ["SK-II", "PITERA"]
}
POST test/_doc
{
"title": ["Nintendo", "switch"]
}
查詢的DSL陳述句如下:
// 自動補全查詢
GET /test/_search
{
"suggest": {
"title_suggest": { //設定這個自動查詢操作的名稱
"text": "s", // 關鍵字
"completion": {
"field": "title", // 補全查詢的欄位名
"skip_duplicates": true, // 跳過重復的
"size": 10 // 獲取前10條結果
}
}
}
}
4. 自動補全嵌入專案
4.1 修改索引庫映射結構
重點注意:
① all、name欄位等要 分詞設定為自定義分詞器("analyzer": "text_anlyzer")(一般要分詞,然后再對分詞后的詞語進行拼音處理),查詢設定為最精簡分詞器("search_analyzer": "ik_smart")
② 設定一個自動補全欄位(如 suggestion) 型別必須為:completion,并且使用自定義分詞器(一般不分詞直接對整個詞語進行拼音處理)
先洗掉之前的索引庫,再設定如下:
// 酒店資料索引庫
PUT /hotel
{
"settings": {
"analysis": {
"analyzer": {
"text_anlyzer": {
"tokenizer": "ik_max_word",
"filter": "py"
},
"completion_analyzer": {
"tokenizer": "keyword",
"filter": "py"
}
},
"filter": {
"py": {
"type": "pinyin",
"keep_full_pinyin": false,
"keep_joined_full_pinyin": true,
"keep_original": true,
"limit_first_letter_length": 16,
"remove_duplicated_term": true,
"none_chinese_pinyin_tokenize": false
}
}
}
},
"mappings": {
"properties": {
"id":{
"type": "keyword"
},
"name":{
"type": "text",
"analyzer": "text_anlyzer",
"search_analyzer": "ik_smart",
"copy_to": "all"
},
"address":{
"type": "keyword",
"index": false
},
"price":{
"type": "integer"
},
"score":{
"type": "integer"
},
"brand":{
"type": "keyword",
"copy_to": "all"
},
"city":{
"type": "keyword"
},
"starName":{
"type": "keyword"
},
"business":{
"type": "keyword",
"copy_to": "all"
},
"location":{
"type": "geo_point"
},
"pic":{
"type": "keyword",
"index": false
},
"all":{
"type": "text",
"analyzer": "text_anlyzer",
"search_analyzer": "ik_smart"
},
"suggestion":{
"type": "completion",
"analyzer": "completion_analyzer"
}
}
}
}
4.2 修改物體類
型別為completion的欄位需要在改造方法里做組裝
HotelDoc中要添加一個欄位,用來做自動補全,內容可以是酒店品牌、城市、商圈等資訊,按照自動補全欄位的要求,最好是這些欄位的陣列,
因此我們在HotelDoc中添加一個suggestion欄位,型別為List<String>,然后將brand、city、business等資訊放到里面,
代碼如下:
package cn.itcast.hotel.pojo;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
@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;
private Object distance;
private Boolean isAD;
private List<String> suggestion;
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();
// 組裝suggestion
if(this.business.contains("/")){
// business有多個值,需要切割
String[] arr = this.business.split("/");
// 添加元素
this.suggestion = new ArrayList<>();
this.suggestion.add(this.brand);
Collections.addAll(this.suggestion, arr);
}else {
this.suggestion = Arrays.asList(this.brand, this.business);
}
}
}
4.3 重新匯入資料
先洗掉資料,再重新執行之前撰寫的匯入資料功能,可以看到新的酒店資料中包含了suggestion:

4.4 自動補全的JavaAPI
示例:(這兩幅圖有點亂,看不懂就忽略)
查詢代碼如下:

決議結果代碼如下:

1)在cn.itcast.hotel.web包下的HotelController中添加新介面,接收新的請求:
@GetMapping("suggestion")
public List<String> getSuggestions(@RequestParam("key") String prefix) {
return hotelService.getSuggestions(prefix);
}
2)在cn.itcast.hotel.service包下的IhotelService中添加方法:
List<String> getSuggestions(String prefix);
3)在cn.itcast.hotel.service.impl.HotelService中實作該方法:
@Override
public List<String> getSuggestion(String prefix) {
try {
// 1.準備Request
SearchRequest request = new SearchRequest("hotel");
// 2.準備DSL
request.source().suggest(new SuggestBuilder().addSuggestion(
"hotelSuggestion", //設定這個自動補全操作的名稱
SuggestBuilders.completionSuggestion("suggestion") //型別為completion的欄位名
.prefix(prefix)
.skipDuplicates(true)
.size(10)
));
// 3.發起請求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 4.決議結果
Suggest suggest = response.getSuggest();
// 4.1.根據補全查詢名稱,獲取補全結果(這里的引數是索引庫里型別為completion的欄位名)
CompletionSuggestion suggestions = suggest.getSuggestion("hotelSuggestion"); //之前設定的這個自動查詢操作的名稱
// 4.2.獲取options
List<CompletionSuggestion.Entry.Option> options = suggestions.getOptions();
// 4.3.遍歷
List<String> list = new ArrayList<>(options.size());
for (CompletionSuggestion.Entry.Option option : options) {
String text = option.getText().toString();
list.add(text);
}
return list;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
ES與Mysql資料同步
elasticsearch中的酒店資料來自于mysql資料庫,因此mysql資料發生改變時,elasticsearch也必須跟著改變,這個就是elasticsearch與mysql之間的資料同步,

1. 三種方法
常見的資料同步方案有三種:
- 同步呼叫
- 異步通知
- 監聽binlog
方式一:同步呼叫
- 優點:實作簡單,粗暴
- 缺點:業務耦合度高
方式二:異步通知【常用】
- 優點:低耦合,實作難度一般
- 缺點:依賴mq的可靠性
方式三:監聽binlog
- 優點:完全解除服務間耦合
- 缺點:開啟binlog增加資料庫負擔、實作復雜度高
1.1.同步呼叫
方案一:同步呼叫

基本步驟如下:
- hotel-demo對外提供介面,用來修改elasticsearch中的資料
- 酒店管理服務在完成資料庫操作后,直接呼叫hotel-demo提供的介面,
1.2.異步通知
方案二:異步通知

流程如下:
- hotel-admin對mysql資料庫資料完成增、刪、改后,發送MQ訊息
- hotel-demo監聽MQ,接收到訊息后完成elasticsearch資料修改
1.3.監聽binlog
方案三:監聽binlog

流程如下:
- 給mysql開啟binlog功能
- mysql完成增、刪、改操作都會記錄在binlog中
- hotel-demo基于canal監聽binlog變化,實時更新elasticsearch中的內容
2. 實作資料同步
當資料發生增、刪、改時,要求對elasticsearch中資料也要完成相同操作,
步驟:
- 單機部署并啟動MQ(單機部署在MQ部分有講)
- 接收者中宣告exchange、queue、RoutingKey
- 在hotel-admin發送者中的增、刪、改業務中完成訊息發送
- 在hotel-demo接收者中完成訊息監聽,并更新elasticsearch中資料
- 啟動并測驗資料同步功能
2.0 匯入依賴和yaml
對發送者和消費者都添加依賴和yaml資訊
1)引入依賴
<!--amqp-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
2)yaml
spring:
rabbitmq: #MQ配置
host: 192.168.194.131 # 主機名
port: 5672 # 埠
virtual-host: / # 虛擬主機
username: itcast # 用戶名
password: 123321 # 密碼
2.1 宣告交換機、佇列
MQ結構如圖:

1)宣告佇列交換機名稱
在hotel-admin發送者和hotel-demo消費者中的cn.itcast.hotel.constatnts包下新建一個類MqConstants:
package cn.itcast.hotel.constatnts;
public class MqConstants {
/**
* 交換機
*/
public final static String HOTEL_EXCHANGE = "hotel.topic";
/**
* 監聽新增和修改的佇列
*/
public final static String HOTEL_INSERT_QUEUE = "hotel.insert.queue";
/**
* 監聽洗掉的佇列
*/
public final static String HOTEL_DELETE_QUEUE = "hotel.delete.queue";
/**
* 新增或修改的RoutingKey
*/
public final static String HOTEL_INSERT_KEY = "hotel.insert";
/**
* 洗掉的RoutingKey
*/
public final static String HOTEL_DELETE_KEY = "hotel.delete";
}
2)宣告佇列交換機
在hotel-demo消費者中,定義配置類,宣告佇列、交換機:
package cn.itcast.hotel.config;
import cn.itcast.hotel.constants.MqConstants;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MqConfig {
@Bean
public TopicExchange topicExchange(){
return new TopicExchange(MqConstants.HOTEL_EXCHANGE, true, false);
}
@Bean
public Queue insertQueue(){
return new Queue(MqConstants.HOTEL_INSERT_QUEUE, true);
}
@Bean
public Queue deleteQueue(){
return new Queue(MqConstants.HOTEL_DELETE_QUEUE, true);
}
@Bean
public Binding insertQueueBinding(){
return BindingBuilder.bind(insertQueue()).to(topicExchange()).with(MqConstants.HOTEL_INSERT_KEY);
}
@Bean
public Binding deleteQueueBinding(){
return BindingBuilder.bind(deleteQueue()).to(topicExchange()).with(MqConstants.HOTEL_DELETE_KEY);
}
}
2.2 發送MQ訊息
在hotel-admin發送者中的增、刪、改業務中分別發送MQ訊息:

2.3 接收MQ訊息
hotel-demo接收到MQ訊息要做的事情包括:
- 新增訊息:根據傳遞的hotel的id查詢hotel資訊,然后新增一條資料到索引庫
- 洗掉訊息:根據傳遞的hotel的id洗掉索引庫中的一條資料
1)寫SDL業務
首先在hotel-demo的cn.itcast.hotel.service包下的IHotelService中新增新增、洗掉業務
void deleteById(Long id);
void insertById(Long id);
給hotel-demo中的cn.itcast.hotel.service.impl包下的HotelService中實作業務:
@Override
public void deleteById(Long id) {
try {
// 1.準備Request
DeleteRequest request = new DeleteRequest("hotel", id.toString());
// 2.發送請求
client.delete(request, RequestOptions.DEFAULT);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public void insertById(Long id) {
try {
// 0.根據id查詢酒店資料
Hotel hotel = getById(id);
// 轉換為檔案型別
HotelDoc hotelDoc = new HotelDoc(hotel);
// 1.準備Request物件
IndexRequest request = new IndexRequest("hotel").id(hotel.getId().toString());
// 2.準備Json檔案
request.source(JSON.toJSONString(hotelDoc), XContentType.JSON);
// 3.發送請求
client.index(request, RequestOptions.DEFAULT);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
2)撰寫監聽器
在hotel-demo中的cn.itcast.hotel.mq包新增一個類:
package cn.itcast.hotel.mq;
import cn.itcast.hotel.constants.MqConstants;
import cn.itcast.hotel.service.IHotelService;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class HotelListener {
@Autowired
private IHotelService hotelService;
/**
* 監聽酒店新增或修改的業務
* @param id 酒店id
*/
@RabbitListener(queues = MqConstants.HOTEL_INSERT_QUEUE)
public void listenHotelInsertOrUpdate(Long id){
hotelService.insertById(id);
}
/**
* 監聽酒店洗掉的業務
* @param id 酒店id
*/
@RabbitListener(queues = MqConstants.HOTEL_DELETE_QUEUE)
public void listenHotelDelete(Long id){
hotelService.deleteById(id);
}
}
2.4 測驗
用postman呼叫增加/洗掉/修改mysql資料庫的介面,然后去頁面搜索看看洗掉的資料還是否能查到,或者修改/增加的資料能不能查出來
ES集群
1. 搭建ES集群
1.1 創建ES集群
部署es集群可以直接使用docker-compose來完成,不過要求你的Linux虛擬機至少有4G的記憶體空間
首先撰寫一個docker-compose檔案,內容如下:
version: '2.2'
services:
es01:
image: docker.elastic.co/elasticsearch/elasticsearch:7.12.1
container_name: es01
environment:
- node.name=es01
- cluster.name=es-docker-cluster
- discovery.seed_hosts=es02,es03
- cluster.initial_master_nodes=es01,es02,es03
- bootstrap.memory_lock=true
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
ulimits:
memlock:
soft: -1
hard: -1
volumes:
- data01:/usr/share/elasticsearch/data
ports:
- 9200:9200
networks:
- elastic
es02:
image: docker.elastic.co/elasticsearch/elasticsearch:7.12.1
container_name: es02
environment:
- node.name=es02
- cluster.name=es-docker-cluster
- discovery.seed_hosts=es01,es03
- cluster.initial_master_nodes=es01,es02,es03
- bootstrap.memory_lock=true
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
ulimits:
memlock:
soft: -1
hard: -1
volumes:
- data02:/usr/share/elasticsearch/data
networks:
- elastic
es03:
image: docker.elastic.co/elasticsearch/elasticsearch:7.12.1
container_name: es03
environment:
- node.name=es03
- cluster.name=es-docker-cluster
- discovery.seed_hosts=es01,es02
- cluster.initial_master_nodes=es01,es02,es03
- bootstrap.memory_lock=true
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
ulimits:
memlock:
soft: -1
hard: -1
volumes:
- data03:/usr/share/elasticsearch/data
networks:
- elastic
volumes:
data01:
driver: local
data02:
driver: local
data03:
driver: local
networks:
elastic:
driver: bridge
es運行需要修改一些linux系統權限,修改/etc/sysctl.conf檔案
vi /etc/sysctl.conf
添加下面的內容:
vm.max_map_count=262144
然后執行命令,讓配置生效:
sysctl -p

通過docker-compose啟動集群:
docker-compose up -d
1.2 集群狀態監控
kibana可以監控es集群,不過新版本需要依賴es的x-pack 功能,配置比較復雜,
這里推薦使用cerebro來監控es集群狀態,官方網址:https://github.com/lmenezes/cerebro
課前資料已經提供了安裝包:

解壓即可使用,非常方便,
解壓好的目錄如下:

進入對應的bin目錄:

雙擊其中的cerebro.bat檔案即可啟動服務,

訪問http://localhost:9000 即可進入管理界面:

輸入你的elasticsearch的任意節點的地址和埠,點擊connect即可:

綠色的條,代表集群處于綠色(健康狀態),
1.3創建索引庫
創建索引庫的時候需要設定分片數量(其他還有多少個ES服務在該集群)以及副本數量(本服務的資料拷貝幾份)
方法一:利用kibana的DevTools創建索引庫
如果沒有啟動ES的可視化界面Kibana,那就用方法二
在DevTools中輸入指令:
PUT /itcast
{
"settings": {
"number_of_shards": 3, // 分片數量
"number_of_replicas": 1 // 副本數量
},
"mappings": {
"properties": {
// mapping映射定義 ...
}
}
}
方法二:利用cerebro創建索引庫
利用cerebro還可以創建索引庫:

填寫索引庫資訊:

點擊右下角的create按鈕:

查看分片效果
回到首頁,即可查看索引庫分片效果:

2.集群腦裂問題
master eligible節點的作用是什么?
- 參與集群選主
- 主節點可以管理集群狀態、管理分片資訊、處理創建和洗掉索引庫的請求
data節點的作用是什么?
- 資料的CRUD
coordinator節點的作用是什么?
路由請求到其它節點
合并查詢到的結果,回傳給用戶
2.1.集群職責劃分
通過改變組態檔中的 true——> false 來改變職責,如data資料職責節點就只保留data為true其他為false
注意:每個節點都是路由,這樣可以保證不管哪個節點接收到請求可以分給其他人已經從其他人那接收資訊,
elasticsearch中集群節點有不同的職責劃分:

默認情況下,集群中的任何一個節點都同時具備上述四種角色,
但是真實的集群一定要將集群職責分離:(因為不同職責對CPU要求不同)
- master節點:對CPU要求高,但是記憶體要求低
- data節點:對CPU和記憶體要求都高
- coordinating節點:對網路帶寬、CPU要求高
職責分離可以讓我們根據不同節點的需求分配不同的硬體去部署,而且避免業務之間的互相干擾,
一個典型的es集群職責劃分如圖:

2.2.腦裂問題
ES 7.0后默認配置了( eligible節點數量 + 1 )/ 2來解決腦裂問題
腦裂是因為集群中的節點失聯導致的,
例如一個集群中,主節點與其它節點失聯:

此時,node2和node3認為node1宕機,就會重新選主:

當node3當選后,集群繼續對外提供服務,node2和node3自成集群,node1自成集群,兩個集群資料不同步,出現資料差異,
當網路恢復后,因為集群中有兩個master節點,集群狀態的不一致,出現腦裂的情況:

解決腦裂的方案是,要求選票超過 ( eligible節點數量 + 1 )/ 2 才能當選為主,因此eligible節點數量最好是奇數,對應配置項是discovery.zen.minimum_master_nodes,在es7.0以后,已經成為默認配置,因此一般不會發生腦裂問題
例如:3個節點形成的集群,選票必須超過 (3 + 1) / 2 ,也就是2票,node3得到node2和node3的選票,當選為主,node1只有自己1票,沒有當選,集群中依然只有1個主節點,沒有出現腦裂,
3.集群分布式存盤
當新增檔案時,應該保存到不同分片,保證資料均衡,那么coordinating node如何確定資料該存盤到哪個分片呢?
3.1.分片存盤測驗
插入三條資料:



測驗可以看到,三條資料分別在不同分片:

結果:

3.2.分片存盤原理
elasticsearch會通過hash演算法來計算檔案應該存盤到哪個分片:

說明:
- _routing默認是檔案的id
- 演算法與分片數量有關,因此索引庫一旦創建,分片數量不能修改!
新增檔案的流程如下:

解讀:
- 1)新增一個id=1的檔案
- 2)對id做hash運算,假如得到的是2,則應該存盤到shard-2
- 3)shard-2的主分片在node3節點,將資料路由到node3
- 4)保存檔案
- 5)同步給shard-2的副本replica-2,在node2節點
- 6)回傳結果給coordinating-node節點
4. 集群分布式查詢
原理:
elasticsearch的查詢分成兩個階段:
-
scatter phase:分散階段,coordinating node會把請求分發到每一個分片
-
gather phase:聚集階段,coordinating node匯總data node的搜索結果,并處理為最終結果集回傳給用戶

5.集群故障轉移
ES本身已經配置好了有集群故障轉移,不需要我們再去配置
集群的master節點會監控集群中的節點狀態,如果發現有節點宕機,會立即將宕機節點的分片資料遷移到其它節點,確保資料安全,這個叫做故障轉移,
1)例如一個集群結構如圖:

現在,node1是主節點,其它兩個節點是從節點,
2)突然,node1發生了故障:

宕機后的第一件事,需要重新選主,例如選中了node2:

node2成為主節點后,會檢測集群監控狀態,發現:shard-1、shard-0沒有副本節點,因此需要將node1上的資料遷移到node2、node3:

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/543046.html
標籤:Java
上一篇:用GPU來運行Python代碼
