Elasticsearch的使用

Elasticsearch

1、認識和安裝

Elasticsearch的官方網站如下:

https://www.elastic.co/cn/elasticsearch

Elasticsearch是由elastic公司開發的一套搜索引擎技術,它是elastic技術棧中的一部分。完整的技術棧包括:

  • Elasticsearch:用于數據存儲、計算和搜索
  • Logstash/Beats:用于數據收集
  • Kibana:用于數據可視化

整套技術棧被稱為ELK,經常用來做日志收集、系統監控和狀態分析等等:

image-20240429090624459

整套技術棧的核心就是用來存儲搜索計算的Elasticsearch,因此我們接下來學習的核心也是Elasticsearch。

我們要安裝的內容包含2部分:

  • elasticsearch:存儲、搜索和運算
  • kibana:圖形化展示

首先Elasticsearch不用多說,是提供核心的數據存儲、搜索、分析功能的。

然后是Kibana,Elasticsearch對外提供的是Restful風格的API,任何操作都可以通過發送http請求來完成。不過http請求的方式、路徑、還有請求參數的格式都有嚴格的規范。這些規范我們肯定記不住,因此我們要借助于Kibana這個服務。

Kibana是elastic公司提供的用于操作Elasticsearch的可視化控制臺。它的功能非常強大,包括:

  • 對Elasticsearch數據的搜索、展示
  • 對Elasticsearch數據的統計、聚合,并形成圖形化報表、圖形
  • 對Elasticsearch的集群狀態監控
  • 它還提供了一個開發控制臺(DevTools),在其中對Elasticsearch的Restful的API接口提供了語法提示

安裝ES

通過下面的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 hmall \-p 9200:9200 \-p 9300:9300 \elasticsearch:7.12.1

注意,這里我們采用的是elasticsearch的7.12.1版本,由于8以上版本的JavaAPI變化很大,在企業中應用并不廣泛,企業中應用較多的還是8以下的版本。

安裝完成后,訪問9200端口,即可看到響應的Elasticsearch服務的基本信息:

image-20240429091604800

安裝Kibana

通過下面的Docker命令,即可部署Kibana:

docker run -d \
--name kibana \
-e ELASTICSEARCH_HOSTS=http://es:9200 \
--network=hmall \
-p 5601:5601  \
kibana:7.12.1

image-20240429091705520

選擇Explore on my own之后,進入主頁面:

image-20240429091820111

然后選中Dev tools,進入開發工具頁面:

image-20240429091806889

2、基礎概念

elasticsearch中有很多獨有的概念,與mysql中略有差別,但也有相似之處。

文檔和字段

elasticsearch是面向**文檔(Document)**存儲的,可以是數據庫中的一條商品數據,一個訂單信息。文檔數據會被序列化為json格式后存儲在elasticsearch中:

image-20240429101444831

{"id": 1,"title": "小米手機","price": 3499
}
{"id": 2,"title": "華為手機","price": 4999
}
{"id": 3,"title": "華為小米充電器","price": 49
}
{"id": 4,"title": "小米手環","price": 299
}

因此,原本數據庫中的一行數據就是ES中的一個JSON文檔;而數據庫中每行數據都包含很多列,這些列就轉換為JSON文檔中的字段(Field)

索引和映射

隨著業務發展,需要在es中存儲的文檔也會越來越多,比如有商品的文檔、用戶的文檔、訂單文檔等等:

elasticsearch之所以有如此高性能的搜索表現,正是得益于底層的倒排索引技術。那么什么是倒排索引呢?

倒排索引的概念是基于MySQL這樣的正向索引而言的。

image-20240429101519084

所有文檔都散亂存放顯然非常混亂,也不方便管理。

因此,我們要將類型相同的文檔集中在一起管理,稱為索引(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),是索引中文檔的字段約束信息,類似表的結構約束

mysql與elasticsearch

我們統一的把mysql與elasticsearch的概念做一下對比:

MySQLElasticsearch說明
TableIndex索引(index),就是文檔的集合,類似數據庫的表(table)
RowDocument文檔(Document),就是一條條的數據,類似數據庫中的行(Row),文檔都是JSON格式
ColumnField字段(Field),就是JSON文檔中的字段,類似數據庫中的列(Column)
SchemaMappingMapping(映射)是索引中文檔的約束,例如字段類型約束。類似數據庫的表結構(Schema)
SQLDSLDSL是elasticsearch提供的JSON風格的請求語句,用來操作elasticsearch,實現CRUD

如圖:

image-20240429101849147

那是不是說,我們學習了elasticsearch就不再需要mysql了呢?

并不是如此,兩者各自有自己的擅長之處:

  • Mysql:擅長事務類型操作,可以確保數據的安全和一致性
  • Elasticsearch:擅長海量數據的搜索、分析、計算

因此在企業中,往往是兩者結合使用:

  • 對安全性要求較高的寫操作,使用mysql實現
  • 對查詢性能要求較高的搜索需求,使用elasticsearch實現
  • 兩者再基于某種方式,實現數據的同步,保證一致性

image-20240429101911482

3、倒排索引

正向索引

我們先來回顧一下正向索引。

例如有一張名為tb_goods的表:

idtitleprice
1小米手機3499
2華為手機4999
3華為小米充電器49
4小米手環49

其中的id字段已經創建了索引,由于索引底層采用了B+樹結構,因此我們根據id搜索的速度會非常快。但是其他字段例如title,只在葉子節點上存在。

因此要根據title搜索的時候只能遍歷樹中的每一個葉子節點,判斷title數據是否符合要求。

比如用戶的SQL語句為:

select * from tb_goods where title like '%手機%';

那搜索的大概流程如圖:

image-20240429092109438

綜上,根據id精確匹配時,可以走索引,查詢效率較高。而當搜索條件為模糊匹配時,由于索引無法生效,導致從索引查詢退化為全表掃描,效率很差。

因此,正向索引適合于根據索引字段的精確搜索,不適合基于部分詞條的模糊匹配。

而倒排索引恰好解決的就是根據部分詞條模糊匹配的問題。

倒排索引

倒排索引中有兩個非常重要的概念:

  • 文檔(Document):用來搜索的數據,其中的每一條數據就是一個文檔。例如一個網頁、一個商品信息
  • 詞條(Term):對文檔數據或用戶搜索數據,利用某種算法分詞,得到的具備含義的詞語就是詞條。例如:我是中國人,就可以分為:我、是、中國人、中國、國人這樣的幾個詞條

創建倒排索引是對正向索引的一種特殊處理和應用,流程如下:

  • 將每一個文檔的數據利用分詞算法根據語義拆分,得到一個個詞條
  • 創建表,每行數據包括詞條、詞條所在文檔id、位置等信息
  • 因為詞條唯一性,可以給詞條創建正向索引

此時形成的這張以詞條為索引的表,就是倒排索引表,兩者對比如下:

正向索引

id(索引)titleprice
1小米手機3499
2華為手機4999
3華為小米充電器49
4小米手環49

倒排索引

詞條(索引)文檔id
小米1,3,4
手機1,2
華為2,3
充電器3
手環4

倒排索引的搜索流程如下(以搜索"華為手機"為例),如圖:

image-20240429092342781

4、IK分詞器

Elasticsearch的關鍵就是倒排索引,而倒排索引依賴于對文檔內容的分詞,而分詞則需要高效、精準的分詞算法,IK分詞器就是這樣一個中文分詞算法。

安裝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分詞器上傳至這個目錄。

然后重啟es

使用IK分詞器

IK分詞器包含兩種模式:

  • ik_smart:智能語義切分
  • ik_max_word:最細粒度切分

我們在Kibana的DevTools上來測試分詞器,首先測試Elasticsearch官方提供的標準分詞器:

POST /_analyze
{"analyzer": "standard","text": "你他娘的真是個天才"
}

image-20240429102954822

{"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" : "天","start_offset" : 7,"end_offset" : 8,"type" : "<IDEOGRAPHIC>","position" : 7},{"token" : "才","start_offset" : 8,"end_offset" : 9,"type" : "<IDEOGRAPHIC>","position" : 8}]
}

可以看到原本的分詞器對中文分詞是不太友好的

我們使用Ikun分詞器看看

POST /_analyze
{"analyzer": "ik_smart","text": "你他娘的真是個天才"
}

執行結果如下:

image-20240429103840259

可以看到明顯的差別,更符合我們國內自己人的使用

拓展詞典

隨著互聯網的發展,“造詞運動”也越發的頻繁。出現了很多新的詞語,在原有的詞匯列表中并不存在。比如:“Ikun”,“雞你太美” 等。

IK分詞器無法對這些詞匯分詞,測試一下:

POST /_analyze
{"analyzer": "ik_max_word","text": "中分頭背帶褲,我是Ikun你記住,雞你太美"
}

image-20240429104101129

可以看到我們后面暗藏玄坤的詞語并沒有被分到一起。

所以要想正確分詞,IK分詞器的詞庫也需要不斷的更新,IK分詞器提供了擴展詞匯的功能。

1)打開IK分詞器config目錄:

image-20240429104220718

注意,如果采用在線安裝的通過,默認是沒有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目錄下復制一個配置文件進行修改

Ikun
雞你太美

4)重啟elasticsearch

docker restart es# 查看 日志
docker logs -f elasticsearch

次測試,可以發現Ikun雞你太美都正確分詞了:

image-20240429104642078

總結

分詞器的作用是什么?

  • 創建倒排索引時,對文檔分詞
  • 用戶搜索時,對輸入的內容分詞

IK分詞器有幾種模式?

  • ik_smart:智能切分,粗粒度
  • ik_max_word:最細切分,細粒度

IK分詞器如何拓展詞條?如何停用詞條?

  • 利用config目錄的IkAnalyzer.cfg.xml文件添加拓展詞典和停用詞典
  • 在詞典中添加拓展詞條或者停用詞條

5、索引庫操作

Index就類似數據庫表,Mapping映射就類似表的結構。我們要向es中存儲數據,必須先創建Index和Mapping

Mapping映射屬性

Mapping是對索引庫中文檔的約束,常見的Mapping屬性包括:

  • type:字段數據類型,常見的簡單類型有:
    • 字符串:text(可分詞的文本)、keyword(精確值,例如:品牌、國家、ip地址)
    • 數值:longintegershortbytedoublefloat
    • 布爾:boolean
    • 日期:date
    • 對象:object
  • index:是否創建索引,默認為true
  • analyzer:使用哪種分詞器
  • properties:該字段的子字段
{"age": 21,"weight": 52.1,"isMarried": false,"info": "中分頭背帶褲","email": "zy@123.cn","score": [99.1, 99.5, 98.9],"name": {"firstName": "徐坤","lastName": "蔡"}
}

對應的每個字段映射(Mapping):

字段名字段類型類型說明是否****參與搜索是否****參與分詞分詞器
ageinteger整數——
weightfloat浮點數——
isMarriedboolean布爾——
infotext字符串,但需要分詞IK
emailkeyword字符串,但是不分詞——
scorefloat只看數組中元素類型——
namefirstNamekeyword字符串,但是不分詞——
lastNamekeyword字符串,但是不分詞——

索引庫的CRUD

創建索引庫和映射

基本語法

  • 請求方式:PUT
  • 請求路徑:/索引庫名,可以自定義
  • 請求參數:mapping映射

格式

PUT /索引庫名稱
{"mappings": {"properties": {"字段名":{"type": "text","analyzer": "ik_smart"},"字段名2":{"type": "keyword","index": "false"},"字段名3":{"properties": {"子字段": {"type": "keyword"}}},// ...略}}
}

image-20240502184845777

查詢索引庫

基本語法

  • 請求方式:GET
  • 請求路徑:/索引庫名
  • 請求參數:無

格式

GET /索引庫名

image-20240502185004003

刪除索引庫

語法:

  • 請求方式:DELETE
  • 請求路徑:/索引庫名
  • 請求參數:無

格式:

DELETE /索引庫名

修改索引庫

倒排索引結構雖然不復雜,但是一旦數據結構改變(比如改變了分詞器),就需要重新創建倒排索引,這簡直是災難。因此索引庫一旦創建,無法修改mapping

雖然無法修改mapping中已有的字段,但是卻允許添加新的字段到mapping中,因為不會對倒排索引產生影響。因此修改索引庫能做的就是向索引庫中添加新字段,或者更新索引庫的基礎屬性。

PUT /索引庫名/_mapping
{"properties": {"新字段名":{"type": "integer"}}
}
  1. 錯誤操作

那我們先來試一試錯誤的操作,直接修改已有的字段看看是否會報錯

image-20240502190211304

可以看到提示我們不能將firstName字段 keyword 修改成text

  1. 正確操作

image-20240502190315559

image-20240502190326392

6、文檔操作

文檔的CRUD

新增文檔

語法:

POST /索引庫名/_doc/文檔id
{"字段1": "值1","字段2": "值2","字段3": {"子屬性1": "值3","子屬性2": "值4"},
}

image-20240503090949840

查詢文檔

根據rest風格,新增是post,查詢應該是get,不過查詢一般都需要條件,這里我們把文檔id帶上。

語法:

GET /{索引庫名稱}/_doc/{id}

image-20240503091113115

刪除文檔

刪除使用DELETE請求,同樣,需要根據id進行刪除:

語法:

DELETE /{索引庫名}/_doc/id值

image-20240503091201397

修改文檔

修改有兩種方式:

  • 全量修改:直接覆蓋原來的文檔
  • 局部修改:修改文檔中的部分字段
全量修改

全量修改是覆蓋原來的文檔,其本質是兩步操作:

  • 根據指定的id刪除文檔
  • 新增一個相同id的文檔

注意:如果根據id刪除時,id不存在,第二步的新增也會執行,也就從修改變成了新增操作了。

語法:

PUT /{索引庫名}/_doc/文檔id
{"字段1": "值1","字段2": "值2",// ... 略
}

由于id1的文檔已經被刪除,所以第一次執行時,得到的反饋是created

image-20240503091408661

所以如果執行第2次時,得到的反饋則是updated

image-20240503091453430

局部修改

局部修改是只修改指定id匹配的文檔中的部分字段。

語法:

POST /{索引庫名}/_update/文檔id
{"doc": {"字段名": "新的值",}
}

image-20240503091740382

批處理

批處理采用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" : "ikun","_id":"3"}}
{"info":"雞你太美2","age":"11","name":{"firstName": "只因","lastName": "蔡"}}
{"index" : { "_index" : "ikun","_id":"4"}}
{"info":"雞你太美","age":"11","name":{"firstName": "只因","lastName": "蔡"}}

image-20240503092753817

批量刪除:

POST /_bulk
{"delete":{"_index":"ikun", "_id": "3"}}
{"delete":{"_index":"ikun", "_id": "4"}}

image-20240503092821358

7、RestAPI

ES官方提供了各種不同語言的客戶端,用來操作ES。這些客戶端的本質就是組裝DSL語句,通過http請求發送給ES。

官方文檔地址:

https://www.elastic.co/guide/en/elasticsearch/client/index.html

初始化RestClient

在elasticsearch提供的API中,與elasticsearch一切交互都封裝在一個名為RestHighLevelClient的類中,必須先完成這個對象的初始化,建立與elasticsearch的連接。

分為三步:

1)在item-service模塊中引入esRestHighLevelClient依賴:

<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();}
}

image-20240504113230329

創建索引庫

由于要實現對商品搜索,所以我們需要將商品添加到Elasticsearch中,不過需要根據搜索業務的需求來設定索引庫結構,而不是一股腦的把MySQL數據寫入Elasticsearch.

Mapping映射

搜索頁面的效果如圖所示:

image-20240504113300055

實現搜索功能需要的字段包括三大部分:

  • 搜索過濾字段
    • 分類
    • 品牌
    • 價格
  • 排序字段
    • 默認:按照更新時間降序排序
    • 銷量
    • 價格
  • 展示字段
    • 商品id:用于點擊后跳轉
    • 圖片地址
    • 是否是廣告推廣商品
    • 名稱
    • 價格
    • 評價數量
    • 銷量

對應的商品表結構如下,索引庫無關字段已經劃掉:

image-20240504113315646

結合數據庫表結構,以上字段對應的mapping映射屬性如下:

字段名字段類型類型說明是否****參與搜索是否****參與分詞分詞器
idlong長整數1——
nametext字符串,參與分詞搜索11IK
priceinteger以分為單位,所以是整數1——
stockinteger字符串,但需要分詞1——
imagekeyword字符串,但是不分詞——
categorykeyword字符串,但是不分詞1——
brandkeyword字符串,但是不分詞1——
soldinteger銷量,整數1——
commentCountinteger評價,整數——
isADboolean布爾類型1——
updateTimeDate更新時間1——

因此,最終我們的索引庫文檔結構應該是這樣:

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"}}}
}

創建索引

創建索引庫的API如下:

image-20240504115111358

代碼分為三步:

  • 1)創建Request對象。
    • 因為是創建索引庫的操作,因此Request是CreateIndexRequest
  • 2)添加請求參數
    • 其實就是Json格式的Mapping映射參數。因為json字符串很長,這里是定義了靜態字符串常量MAPPING_TEMPLATE,讓代碼看起來更加優雅。
  • 3)發送請求
    • client.indices()方法的返回值是IndicesClient類型,封裝了所有與索引庫操作有關的方法。例如創建索引、刪除索引、判斷索引是否存在等

那么我們看試一下是否能創建成功索引庫

    @Testvoid testCreateIndex() throws IOException {CreateIndexRequest request = new CreateIndexRequest("items");// 2.準備請求參數request.source(MAPPING_TEMPLATE, XContentType.JSON);client.indices().create(request, RequestOptions.DEFAULT);}private 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" +"        \"index\": false\n" +"      },\n" +"      \"isAD\":{\n" +"        \"type\": \"boolean\"\n" +"      },\n" +"      \"updateTime\":{\n" +"        \"type\": \"date\"\n" +"      }\n" +"    }\n" +"  }\n" +"}";

image-20240504120346566

這里的MAPPING_TEMPLATE代表了你索引庫的映射,這里我們用代碼和圖形化分別查一下

代碼查詢

    @Testvoid testGetIndex() throws IOException {GetIndexRequest request = new GetIndexRequest("items");client.indices().get(request,RequestOptions.DEFAULT);}

image-20240504121615037

刪除索引庫

刪除索引庫的請求非常簡單:

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);
}

image-20240504115608452

判斷索引庫是否存在

判斷索引庫是否存在,本質就是查詢,對應的請求語句是:

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 ? "索引庫已經存在!" : "索引庫不存在!");
}

8.RestClient操作文檔

索引庫準備好以后,就可以操作文檔了。為了與索引庫操作分離,我們再次創建一個測試類,做兩件事情:

  • 初始化RestHighLevelClient
  • 我們的商品數據在數據庫,需要利用IHotelService去查詢,所以注入這個接口
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;
}

查詢文檔

我們以根據id查詢文檔為例

我們導入商品數據,除了參考API模板“三步走”以外,還需要做幾點準備工作:

  • 商品數據來自于數據庫,我們需要先查詢出來,得到Item對象
  • Item對象需要轉為ItemDoc對象
  • ItemDTO需要序列化為json格式

因此,代碼整體步驟如下:

  • 1)根據id查詢商品數據Item
  • 2)將Item封裝為ItemDoc
  • 3)將ItemDoc序列化為JSON
  • 4)創建IndexRequest,指定索引庫名和id
  • 5)準備請求參數,也就是JSON文檔
  • 6)發送請求

item-serviceDocumentTest測試類中,編寫單元測試:

@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);
}

image-20240507170615159

語法說明

查詢的請求語句如下:

GET /{索引庫名}/_doc/{id}

image-20240507121128647

刪除文檔

刪除的請求語句如下:

DELETE /hotel/_doc/{id}

與查詢相比,僅僅是請求方式從DELETE變成GET,可以想象Java代碼應該依然是2步走:

  • 1)準備Request對象,因為是刪除,這次是DeleteRequest對象。要指定索引庫名和id
  • 2)準備參數,無參,直接省略
  • 3)發送請求。因為是刪除,所以是client.delete()方法

item-serviceDocumentTest測試類中,編寫單元測試:

@Test
void testDeleteDocument() throws IOException {// 1.準備Request,兩個參數,第一個是索引庫名,第二個是文檔idDeleteRequest request = new DeleteRequest("item", "100002644680");// 2.發送請求client.delete(request, RequestOptions.DEFAULT);
}

image-20240507170758207

修改文檔

修改我們講過兩種方式:

  • 全量修改:本質是先根據id刪除,再新增
  • 局部修改:修改文檔中的指定字段值

在RestClient的API中,全量修改與新增的API完全一致,判斷依據是ID:

  • 如果新增時,ID已經存在,則修改
  • 如果新增時,ID不存在,則新增

這里不再贅述,我們主要關注局部修改的API即可。

語法說明

局部修改的請求語法如下:

POST /{索引庫名}/_update/{id}
{"doc": {"字段名": "字段值","字段名": "字段值"}
}

代碼示例如圖:

image-20240507170913172

與之前類似,也是三步走:

  • 1)準備Request對象。這次是修改,所以是UpdateRequest
  • 2)準備參數。也就是JSON文檔,里面包含要修改的字段
  • 3)更新文檔。這里調用client.update()方法

完整代碼

item-serviceDocumentTest測試類中,編寫單元測試:

@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);
}

image-20240507171354934

image-20240507171410875

批量導入文檔

在之前的案例中,我們都是操作單個文檔。而數據庫中的商品數據實際會達到數十萬條,某些項目中可能達到數百萬條。

我們如果要將這些數據導入索引庫,肯定不能逐條導入,而是采用批處理方案。常見的方案有:

  • 利用Logstash批量導入
    • 需要安裝Logstash
    • 對數據的再加工能力較弱
    • 無需編碼,但要學習編寫Logstash導入配置
  • 利用JavaAPI批量導入
    • 需要編碼,但基于JavaAPI,學習成本低
    • 更加靈活,可以任意對數據做再加工處理后寫入索引庫

接下來,我們就學習下如何利用JavaAPI實現批量文檔導入。

語法說明

批處理與前面講的文檔的CRUD步驟基本一致:

  • 創建Request,但這次用的是BulkRequest
  • 準備請求參數
  • 發送請求,這次要用到client.bulk()方法

BulkRequest本身其實并沒有請求參數,其本質就是將多個普通的CRUD請求組合在一起發送。例如:

  • 批量新增文檔,就是給每個文檔創建一個IndexRequest請求,然后封裝到BulkRequest中,一起發出。
  • 批量刪除,就是創建N個DeleteRequest請求,然后封裝到BulkRequest,一起發出

因此BulkRequest中提供了add方法,用以添加其它CRUD的請求:

image-20240507171446846

可以看到,能添加的請求有:

  • 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);
}

完整代碼

當我們要導入商品數據時,由于商品數量達到數十萬,因此不可能一次性全部導入。建議采用循環遍歷方式,每次導入1000條左右的數據。

item-serviceDocumentTest測試類中,編寫單元測試:

@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++;}
}

image-20240507183321790

9.DSL查詢

Elasticsearch提供了基于JSON的DSL(Domain Specific Language)語句來定義查詢條件,其JavaAPI就是在組織DSL條件。

Elasticsearch的查詢可以分為兩大類:

  • 葉子查詢(Leaf query clauses):一般是在特定的字段里查詢特定值,屬于簡單查詢,很少單獨使用。
  • 復合查詢(Compound query clauses):以邏輯方式組合多個葉子查詢或者更改葉子查詢的行為方式。

在查詢以后,還可以對查詢的結果做處理

包括:

  • 排序:按照1個或多個字段值做排序
  • 分頁:根據from和size做分頁,類似MySQL
  • 高亮:對搜索結果中的關鍵字添加特殊樣式,使其更加醒目
  • 聚合:對搜索結果做數據統計以形成報表

快速入門

我們依然在Kibana的DevTools中學習查詢的DSL語法。首先來看查詢的語法結構:

GET /{索引庫名}/_search
{"query": {"查詢類型": {// .. 查詢條件}}
}

說明:

  • GET /{索引庫名}/_search:其中的_search是固定路徑,不能修改

例如,我們以最簡單的無條件查詢為例,無條件查詢的類型是:match_all,因此其查詢語句如下:

GET /items/_search
{"query": {"match_all": {}}
}

由于match_all無條件,所以條件位置不寫即可。

執行結果如下:

image-20240507183626167

你會發現雖然是match_all,但是響應結果中并不會包含索引庫中的所有文檔,而是僅有10條。這是因為處于安全考慮,elasticsearch設置了默認的查詢頁數。

葉子查詢

葉子查詢的類型也可以做進一步細分,詳情大家可以查看官方文檔:

https://www.elastic.co/guide/en/elasticsearch/reference/7.12/query-dsl.html

如圖:

image-20240507183655492

這里列舉一些常見的,例如:

  • 全文檢索查詢(Full Text Queries):利用分詞器對用戶輸入搜索條件先分詞,得到詞條,然后再利用倒排索引搜索詞條。例如:
    • match
    • multi_match
  • 精確查詢(Term-level queries):不對用戶輸入搜索條件分詞,根據字段內容精確值匹配。但只能查找keyword、數值、日期、boolean類型的字段。例如:
    • ids
    • term
    • range
  • **地理坐標查詢:**用于搜索地理位置,搜索方式很多,例如:
    • geo_bounding_box:按矩形搜索
    • geo_distance:按點和半徑搜索
  • …略

全文檢索查詢

全文檢索的種類也很多,詳情可以參考官方文檔:

https://www.elastic.co/guide/en/elasticsearch/reference/7.12/full-text-queries.html

以全文檢索中的match為例,語法如下:

GET /{索引庫名}/_search
{"query": {"match": {"字段名": "搜索條件"}}
}

示例:

image-20240507184419208

match類似的還有multi_match,區別在于可以同時對多個字段搜索,而且多個字段都要滿足,語法示例:

GET /{索引庫名}/_search
{"query": {"multi_match": {"query": "搜索條件","fields": ["字段1", "字段2"]}}
}

示例:

image-20240507184924383

精確查詢

精確查詢,英文是Term-level query,顧名思義,詞條級別的查詢。也就是說不會對用戶輸入的搜索條件再分詞,而是作為一個詞條,與搜索的字段內容精確值匹配。因此推薦查找keyword、數值、日期、boolean類型的字段。例如:

  • id
  • price
  • 城市
  • 地名
  • 人名

等等,作為一個整體才有含義的字段。

詳情可以查看官方文檔:

https://www.elastic.co/guide/en/elasticsearch/reference/7.12/term-level-queries.html

term查詢為例,其語法如下:

GET /{索引庫名}/_search
{"query": {"term": {"字段名": {"value": "搜索條件"}}}
}

示例:

image-20240507191738496

復合查詢

復合查詢大致可以分為兩類:

  • 第一類:基于邏輯運算組合葉子查詢,實現組合條件,例如
    • bool
  • 第二類:基于某種算法修改查詢時的文檔相關性算分,從而改變文檔排名。例如:
    • function_score
    • dis_max

其它復合查詢及相關語法可以參考官方文檔:

https://www.elastic.co/guide/en/elasticsearch/reference/7.12/compound-queries.html

算分函數查詢

當我們利用match查詢時,文檔結果會根據與搜索詞條的關聯度打分_score),返回結果時按照分值降序排列。

例如,我們搜索 “拉桿箱”,結果如下:

image-20240507192001714

從elasticsearch5.1開始,采用的相關性打分算法是BM25算法,公式如下:

image-20240507192013074

基于這套公式,就可以判斷出某個文檔與用戶搜索的關鍵字之間的關聯度,還是比較準確的。但是,在實際業務需求中,常常會有競價排名的功能。不是相關度越高排名越靠前,而是掏的錢多的排名靠前。

例如在百度中搜索Java培訓,排名靠前的就是廣告推廣:

image-20240507192222303

要想認為控制相關性算分,就需要利用elasticsearch中的function score 查詢了。

基本語法

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)基于運算模式做運算,得到最終結果,作為相關性算分。

因此,其中的關鍵點是:

  • 過濾條件:決定哪些文檔的算分被修改
  • 算分函數:決定函數算分的算法
  • 運算模式:決定最終算分結果

示例:給IPhone這個品牌的手機算分提高十倍,分析如下:

  • 過濾條件:品牌必須為IPhone
  • 算分函數:常量weight,值為10
  • 算分模式:相乘multiply

對應代碼如下:

image-20240507193132090

示例

需求:給“SCOGOLF”這個品牌的行李箱排名靠前一些

翻譯一下這個需求,轉換為之前說的四個要點:

  • 原始條件:不確定,可以任意變化
  • 過濾條件:brand = “SCOGOLF”
  • 算分函數:可以簡單粗暴,直接給固定的算分結果,weight
  • 運算模式:比如求和

完整代碼

GET /items/_search
{"query": {"function_score": {"query": {"match": {"name": "行李箱"}},"functions": [{"filter": {"term": {"brand": "拉桿箱"}},"weight": 10}],"boost_mode": "sum"}}
}

測試,在未添加算分函數時,SCOGOLF得分如下:

image-20240507194155180

添加了算分函數后,SCOGOLF得分就提升了:

image-20240507194758958

bool查詢

bool查詢,即布爾查詢。就是利用邏輯運算來組合一個或多個查詢子句的組合。bool查詢支持的邏輯運算有:

  • must:必須匹配每個子查詢,類似“與”
  • should:選擇性匹配子查詢,類似“或”
  • must_not:必須不匹配,不參與算分,類似“非”
  • filter:必須匹配,不參與算分

bool查詢的語法如下:

GET /items/_search
{"query": {"bool": {"must": [{"match": {"name": "手機"}}],"should": [{"term": {"brand": { "value": "vivo" }}},{"term": {"brand": { "value": "小米" }}}],"must_not": [{"range": {"price": {"gte": 2500}}}],"filter": [{"range": {"price": {"lte": 1000}}}]}}
}

出于性能考慮,與搜索關鍵字無關的查詢盡量采用must_not或filter邏輯運算,避免參與相關性算分。

例如黑馬商城的搜索頁面:

image-20240507194933835

其中輸入框的搜索條件肯定要參與相關性算分,可以采用match。但是價格范圍過濾、品牌過濾、分類過濾等盡量采用filter,不要參與相關性算分。

比如,我們要搜索手機,但品牌必須是華為,價格必須是900~1599,那么可以這樣寫:

GET /items/_search
{"query": {"bool": {"must": [{"match": {"name": "手機"}}],"filter": [{"term": {"brand": { "value": "華為" }}},{"range": {"price": {"gte": 90000, "lt": 159900}}}]}}
}

image-20240507195026066

排序

elasticsearch默認是根據相關度算分(_score)來排序,但是也支持自定義方式對搜索結果排序。不過分詞字段無法排序,能參與排序字段類型有:keyword類型、數值類型、地理坐標類型、日期類型等。

詳細說明可以參考官方文檔:

https://www.elastic.co/guide/en/elasticsearch/reference/7.12/sort-search-results.html

語法說明:

GET /indexName/_search
{"query": {"match_all": {}},"sort": [{"排序字段": {"order": "排序方式asc和desc"}}]
}

示例,我們按照商品價格排序:

GET /items/_search
{"query": {"match_all": {}},"sort": [{"price": {"order": "desc"}}]
}

image-20240507195202962

分頁

elasticsearch 默認情況下只返回top10的數據。而如果要查詢更多數據就需要修改分頁參數了。

基礎分頁

elasticsearch中通過修改fromsize參數來控制要返回的分頁結果:

  • from:從第幾個文檔開始
  • size:總共查詢幾個文檔

類似于mysql中的limit ?, ?

官方文檔如下:

https://www.elastic.co/guide/en/elasticsearch/reference/7.12/paginate-search-results.html

語法如下:

GET /items/_search
{"query": {"match_all": {}},"from": 0, // 分頁開始的位置,默認為0"size": 10,  // 每頁文檔數量,默認10"sort": [{"price": {"order": "desc"}}]
}

image-20240507195345385

深度分頁

elasticsearch的數據一般會采用分片存儲,也就是把一個索引中的數據分成N份,存儲到不同節點上。這種存儲方式比較有利于數據擴展,但給分頁帶來了一些麻煩。

比如一個索引庫中有100000條數據,分別存儲到4個分片,每個分片25000條數據。現在每頁查詢10條,查詢第99頁。那么分頁查詢的條件如下:

GET /items/_search
{"from": 990, // 從第990條開始查詢"size": 10, // 每頁查詢10條"sort": [{"price": "asc"}]
}

從語句來分析,要查詢第990~1000名的數據。

從實現思路來分析,肯定是將所有數據排序,找出前1000名,截取其中的990~1000的部分。但問題來了,我們如何才能找到所有數據中的前1000名呢?

要知道每一片的數據都不一樣,第1片上的第9001000,在另1個節點上并不一定依然是9001000名。所以我們只能在每一個分片上都找出排名前1000的數據,然后匯總到一起,重新排序,才能找出整個索引庫中真正的前1000名,此時截取990~1000的數據即可。

如圖:

image-20240507195444933

試想一下,假如我們現在要查詢的是第999頁數據呢,是不是要找第9990~10000的數據,那豈不是需要把每個分片中的前10000名數據都查詢出來,匯總在一起,在內存中排序?如果查詢的分頁深度更深呢,需要一次檢索的數據豈不是更多?

由此可知,當查詢分頁深度較大時,匯總數據過多,對內存和CPU會產生非常大的壓力。

因此elasticsearch會禁止from+ size 超過10000的請求。

針對深度分頁,elasticsearch提供了兩種解決方案:

  • search after:分頁時需要排序,原理是從上一次的排序值開始,查詢下一頁數據。官方推薦使用的方式。
  • scroll:原理將排序后的文檔id形成快照,保存下來,基于快照做分頁。官方已經不推薦使用。

詳情見文檔:

https://www.elastic.co/guide/en/elasticsearch/reference/7.12/paginate-search-results.html

image-20240507200103474

總結:

大多數情況下,我們采用普通分頁就可以了。查看百度、京東等網站,會發現其分頁都有限制。例如百度最多支持77頁,每頁不足20條。京東最多100頁,每頁最多60條。

因此,一般我們采用限制分頁深度的方式即可,無需實現深度分頁。

高亮

高亮原理

什么是高亮顯示呢?

我們在百度,京東搜索時,關鍵字會變成紅色,比較醒目,這叫高亮顯示:

image-20240507200612842

觀察頁面源碼,你會發現兩件事情:

  • 高亮詞條都被加了<em>標簽
  • <em>標簽都添加了紅色樣式

css樣式肯定是前端實現頁面的時候寫好的,但是前端編寫頁面的時候是不知道頁面要展示什么數據的,不可能給數據加標簽。而服務端實現搜索功能,要是有elasticsearch做分詞搜索,是知道哪些詞條需要高亮的。

因此詞條的高亮標簽肯定是由服務端提供數據的時候已經加上的

因此實現高亮的思路就是:

  • 用戶輸入搜索關鍵字搜索數據
  • 服務端根據搜索關鍵字到elasticsearch搜索,并給搜索結果中的關鍵字詞條添加html標簽
  • 前端提前給約定好的html標簽添加CSS樣式

實現高亮

事實上elasticsearch已經提供了給搜索關鍵字加標簽的語法,無需我們自己編碼。

基本語法如下:

GET /{索引庫名}/_search
{"query": {"match": {"搜索字段": "搜索關鍵字"}},"highlight": {"fields": {"高亮字段名稱": {"pre_tags": "<em>","post_tags": "</em>"}}}
}

注意

  • 搜索必須有查詢條件,而且是全文檢索類型的查詢條件,例如match
  • 參與高亮的字段必須是text類型的字段
  • 默認情況下參與高亮的字段要與搜索字段一致,除非添加:required_field_match=false

示例

image-20240507201035590

10.RestClient查詢

文檔的查詢依然使用昨天學習的 RestHighLevelClient對象,查詢的基本步驟如下:

  • 1)創建request對象,這次是搜索,所以是SearchRequest
  • 2)準備請求參數,也就是查詢DSL對應的JSON參數
  • 3)發起請求
  • 4)解析響應,響應結果相對復雜,需要逐層解析

快速入門

文檔搜索的基本步驟是:

  1. 創建SearchRequest對象
  2. 準備request.source(),也就是DSL。
    1. QueryBuilders來構建查詢條件
    2. 傳入request.source()query()方法
  3. 發送請求,得到結果
  4. 解析結果(參考JSON結果,從外到內,逐層解析)

之前說過,由于Elasticsearch對外暴露的接口都是Restful風格的接口,因此JavaAPI調用就是在發送Http請求。而我們核心要做的就是利用利用Java代碼組織請求參數解析響應結果

這個參數的格式完全參考DSL查詢語句的JSON結構,因此我們在學習的過程中,會不斷的把JavaAPI與DSL語句對比。大家在學習記憶的過程中,也應該這樣對比學習。

發送請求

首先以match_all查詢為例,其DSL和JavaAPI的對比如圖:

image-20240507214352930

代碼解讀:

  • 第一步,創建SearchRequest對象,指定索引庫名
  • 第二步,利用request.source()構建DSL,DSL中可以包含查詢、分頁、排序、高亮等
  • query():代表查詢條件,利用QueryBuilders.matchAllQuery()構建一個match_all查詢的DSL
  • 第三步,利用client.search()發送請求,得到響應

這里關鍵的API有兩個,一個是request.source(),它構建的就是DSL中的完整JSON參數。其中包含了querysortfromsizehighlight等所有功能:

image-20240507214406897

另一個是QueryBuilders,其中包含了我們學習過的各種葉子查詢復合查詢等:

image-20240507214418867

解析響應結果

在發送請求以后,得到了響應結果SearchResponse,這個類的結構與我們在kibana中看到的響應結果JSON結構完全一致:

{"took" : 0,"timed_out" : false,"hits" : {"total" : {"value" : 2,"relation" : "eq"},"max_score" : 1.0,"hits" : [{"_index" : "heima","_type" : "_doc","_id" : "1","_score" : 1.0,"_source" : {"info" : "Java講師","name" : "趙云"}}]}
}

因此,我們解析SearchResponse的代碼就是在解析這個JSON結果,對比如下:

image-20240508150312740

代碼解讀

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文檔數據

完整代碼如下:

@Test
void testMatchAll() throws IOException {// 1.創建RequestSearchRequest request = new SearchRequest("items");// 2.組織請求參數request.source().query(QueryBuilders.matchAllQuery());// 3.發送請求SearchResponse response = client.search(request, RequestOptions.DEFAULT);// 4.解析響應handleResponse(response);
}private void handleResponse(SearchResponse response) {SearchHits searchHits = response.getHits();// 1.獲取總條數long total = searchHits.getTotalHits().value;System.out.println("共搜索到" + total + "條數據");// 2.遍歷結果數組SearchHit[] hits = searchHits.getHits();for (SearchHit hit : hits) {// 3.得到_source,也就是原始json文檔String source = hit.getSourceAsString();// 4.反序列化并打印ItemDoc item = JSONUtil.toBean(source, ItemDoc.class);System.out.println(item);}
}

image-20240508150355151

葉子查詢

所有的查詢條件都是由QueryBuilders來構建的,葉子查詢也不例外。因此整套代碼中變化的部分僅僅是query條件構造的方式,其它不動。

例如match查詢:

@Test
void testMatch() throws IOException {// 1.創建RequestSearchRequest request = new SearchRequest("items");// 2.組織請求參數request.source().query(QueryBuilders.matchQuery("name", "行李箱"));// 3.發送請求SearchResponse response = client.search(request, RequestOptions.DEFAULT);// 4.解析響應handleResponse(response);
}

image-20240508150557021

再比如multi_match查詢:

@Test
void testMultiMatch() throws IOException {// 1.創建RequestSearchRequest request = new SearchRequest("items");// 2.組織請求參數request.source().query(QueryBuilders.multiMatchQuery("脫脂牛奶", "name", "category"));// 3.發送請求SearchResponse response = client.search(request, RequestOptions.DEFAULT);// 4.解析響應handleResponse(response);
}

還有range查詢:

@Test
void testRange() throws IOException {// 1.創建RequestSearchRequest request = new SearchRequest("items");// 2.組織請求參數request.source().query(QueryBuilders.rangeQuery("price").gte(10000).lte(30000));// 3.發送請求SearchResponse response = client.search(request, RequestOptions.DEFAULT);// 4.解析響應handleResponse(response);
}

還有term查詢:

@Test
void testTerm() throws IOException {// 1.創建RequestSearchRequest request = new SearchRequest("items");// 2.組織請求參數request.source().query(QueryBuilders.termQuery("brand", "華為"));// 3.發送請求SearchResponse response = client.search(request, RequestOptions.DEFAULT);// 4.解析響應handleResponse(response);
}

復合查詢

復合查詢也是由QueryBuilders來構建,我們以bool查詢為例,DSL和JavaAPI的對比如圖:

image-20240508150702194

完整代碼如下:

@Test
void testBool() throws IOException {// 1.創建RequestSearchRequest request = new SearchRequest("items");// 2.組織請求參數// 2.1.準備bool查詢BoolQueryBuilder bool = QueryBuilders.boolQuery();// 2.2.關鍵字搜索bool.must(QueryBuilders.matchQuery("name", "脫脂牛奶"));// 2.3.品牌過濾bool.filter(QueryBuilders.termQuery("brand", "德亞"));// 2.4.價格過濾bool.filter(QueryBuilders.rangeQuery("price").lte(30000));request.source().query(bool);// 3.發送請求SearchResponse response = client.search(request, RequestOptions.DEFAULT);// 4.解析響應handleResponse(response);
}

image-20240508152043831

排序和分頁

之前說過,requeset.source()就是整個請求JSON參數,所以排序、分頁都是基于這個來設置,其DSL和JavaAPI的對比如下:

image-20240508152058532

完整示例代碼:

@Test
void testPageAndSort() throws IOException {int pageNo = 1, pageSize = 5;// 1.創建RequestSearchRequest request = new SearchRequest("items");// 2.組織請求參數// 2.1.搜索條件參數request.source().query(QueryBuilders.matchQuery("name", "脫脂牛奶"));// 2.2.排序參數request.source().sort("price", SortOrder.ASC);// 2.3.分頁參數request.source().from((pageNo - 1) * pageSize).size(pageSize);// 3.發送請求SearchResponse response = client.search(request, RequestOptions.DEFAULT);// 4.解析響應handleResponse(response);
}

image-20240508152437950

高亮

高亮查詢與前面的查詢有兩點不同:

  • 條件同樣是在request.source()中指定,只不過高亮條件要基于HighlightBuilder來構造
  • 高亮響應結果與搜索的文檔結果不在一起,需要單獨解析

首先來看高亮條件構造,其DSL和JavaAPI的對比如圖:

image-20240508152451439

示例代碼如下:

@Test
void testHighlight() throws IOException {// 1.創建RequestSearchRequest request = new SearchRequest("items");// 2.組織請求參數// 2.1.query條件request.source().query(QueryBuilders.matchQuery("name", "脫脂牛奶"));// 2.2.高亮條件request.source().highlighter(SearchSourceBuilder.highlight().field("name").preTags("<em>").postTags("</em>"));// 3.發送請求SearchResponse response = client.search(request, RequestOptions.DEFAULT);// 4.解析響應handleResponse(response);
}

image-20240508153238223

再來看結果解析,文檔解析的部分不變,主要是高亮內容需要單獨解析出來,其DSL和JavaAPI的對比如圖:

image-20240508152517098

代碼解讀:

  • 3、4步:從結果中獲取_sourcehit.getSourceAsString(),這部分是非高亮結果,json字符串。還需要反序列為ItemDoc對象
  • 5步:獲取高亮結果。hit.getHighlightFields(),返回值是一個Map,key是高亮字段名稱,值是HighlightField對象,代表高亮值
  • 5.1步:從Map中根據高亮字段名稱,獲取高亮字段值對象HighlightField
  • 5.2步:從HighlightField中獲取Fragments,并且轉為字符串。這部分就是真正的高亮字符串了
  • 最后:用高亮的結果替換ItemDoc中的非高亮結果

完整代碼如下:

private void handleResponse(SearchResponse response) {SearchHits searchHits = response.getHits();// 1.獲取總條數long total = searchHits.getTotalHits().value;System.out.println("共搜索到" + total + "條數據");// 2.遍歷結果數組SearchHit[] hits = searchHits.getHits();for (SearchHit hit : hits) {// 3.得到_source,也就是原始json文檔String source = hit.getSourceAsString();// 4.反序列化ItemDoc item = JSONUtil.toBean(source, ItemDoc.class);// 5.獲取高亮結果Map<String, HighlightField> hfs = hit.getHighlightFields();if (CollUtils.isNotEmpty(hfs)) {// 5.1.有高亮結果,獲取name的高亮結果HighlightField hf = hfs.get("name");if (hf != null) {// 5.2.獲取第一個高亮結果片段,就是商品名稱的高亮值String hfName = hf.getFragments()[0].string();item.setName(hfName);}}System.out.println(item);}
}

11.數據聚合

聚合(aggregations)可以讓我們極其方便的實現對數據的統計、分析、運算。例如:

  • 什么品牌的手機最受歡迎?
  • 這些手機的平均價格、最高價格、最低價格?
  • 這些手機每月的銷售情況如何?

實現這些統計功能的比數據庫的sql要方便的多,而且查詢速度非常快,可以實現近實時搜索效果。

官方文檔:

https://www.elastic.co/guide/en/elasticsearch/reference/7.12/search-aggregations.html

聚合常見的有三類:

  • **桶(Bucket)**聚合:用來對文檔做分組
  • TermAggregation:按照文檔字段值分組,例如按照品牌值分組、按照國家分組
  • Date Histogram:按照日期階梯分組,例如一周為一組,或者一月為一組
  • **度量(Metric)**聚合:用以計算一些值,比如:最大值、最小值、平均值等
  • Avg:求平均值
  • Max:求最大值
  • Min:求最小值
  • Stats:同時求maxminavgsum
  • **管道(pipeline)**聚合:其它聚合的結果為基礎做進一步運算

**注意:**參加聚合的字段必須是keyword、日期、數值、布爾類型

DSL實現聚合

與之前的搜索功能類似,我們依然先學習DSL的語法,再學習JavaAPI.

Bucket聚合

例如我們要統計所有商品中共有哪些商品分類,其實就是以分類(category)字段對數據分組。category值一樣的放在同一組,屬于Bucket聚合中的Term聚合。

基本語法如下:

GET /items/_search
{"size": 0, "aggs": {"category_agg": {"terms": {"field": "category","size": 20}}}
}

語法說明:

  • size:設置size為0,就是每頁查0條,則結果中就不包含文檔,只包含聚合
  • aggs:定義聚合
    • category_agg:聚合名稱,自定義,但不能重復
      • terms:聚合的類型,按分類聚合,所以用term
        • field:參與聚合的字段名稱
        • size:希望返回的聚合結果的最大數量

來看下查詢的結果:

帶條件聚合

默認情況下,Bucket聚合是對索引庫的所有文檔做聚合,例如我們統計商品中所有的品牌,結果如下:

image-20240508155226703

可以看到統計出的品牌非常多。

但真實場景下,用戶會輸入搜索條件,因此聚合必須是對搜索結果聚合。那么聚合必須添加限定條件。

例如,我想知道價格高于3000元的手機品牌有哪些,該怎么統計呢?

我們需要從需求中分析出搜索查詢的條件和聚合的目標:

  • 搜索查詢條件:
    • 價格高于3000
    • 必須是手機
  • 聚合目標:統計的是品牌,肯定是對brand字段做term聚合

語法如下:

GET /items/_search
{"query": {"bool": {"filter": [{"term": {"category": "手機"}},{"range": {"price": {"gte": 300000}}}]}}, "size": 0, "aggs": {"brand_agg": {"terms": {"field": "brand","size": 20}}}
}

聚合結果如下:

image-20240508155335385

可以看到,結果中只剩下3個品牌了。

Metric聚合

上節課,我們統計了價格高于3000的手機品牌,形成了一個個桶。現在我們需要對桶內的商品做運算,獲取每個品牌價格的最小值、最大值、平均值。

這就要用到Metric聚合了,例如stat聚合,就可以同時獲取minmaxavg等結果。

語法如下:

GET /items/_search
{"query": {"bool": {"filter": [{"term": {"category": "手機"}},{"range": {"price": {"gte": 300000}}}]}}, "size": 0, "aggs": {"brand_agg": {"terms": {"field": "brand","size": 20},"aggs": {"stats_meric": {"stats": {"field": "price"}}}}}
}

query部分就不說了,我們重點解讀聚合部分語法。

可以看到我們在brand_agg聚合的內部,我們新加了一個aggs參數。這個聚合就是brand_agg的子聚合,會對brand_agg形成的每個桶中的文檔分別統計。

  • stats_meric:聚合名稱
    • stats:聚合類型,stats是metric聚合的一種
      • field:聚合字段,這里選擇price,統計價格

由于stats是對brand_agg形成的每個品牌桶內文檔分別做統計,因此每個品牌都會統計出自己的價格最小、最大、平均值。

結果如下:

image-20240508155710777

另外,我們還可以讓聚合按照每個品牌的價格平均值排序:

image-20240508155835158

總結

aggs代表聚合,與query同級,此時query的作用是?

  • 限定聚合的的文檔范圍

聚合必須的三要素:

  • 聚合名稱
  • 聚合類型
  • 聚合字段

聚合可配置屬性有:

  • size:指定聚合結果數量
  • order:指定聚合結果排序方式
  • field:指定聚合字段

RestClient實現聚合

可以看到在DSL中,aggs聚合條件與query條件是同一級別,都屬于查詢JSON參數。因此依然是利用request.source()方法來設置。

不過聚合條件的要利用AggregationBuilders這個工具類來構造。DSL與JavaAPI的語法對比如下:

image-20240508155859272

聚合結果與搜索文檔同一級別,因此需要單獨獲取和解析。具體解析語法如下:

image-20240508155921849

完整代碼

@Test
void testAgg() throws IOException {// 1.創建RequestSearchRequest request = new SearchRequest("items");// 2.準備請求參數BoolQueryBuilder bool = QueryBuilders.boolQuery().filter(QueryBuilders.termQuery("category", "手機")).filter(QueryBuilders.rangeQuery("price").gte(300000));request.source().query(bool).size(0);// 3.聚合參數request.source().aggregation(AggregationBuilders.terms("brand_agg").field("brand").size(5));// 4.發送請求SearchResponse response = client.search(request, RequestOptions.DEFAULT);// 5.解析聚合結果Aggregations aggregations = response.getAggregations();// 5.1.獲取品牌聚合Terms brandTerms = aggregations.get("brand_agg");// 5.2.獲取聚合中的桶List<? extends Terms.Bucket> buckets = brandTerms.getBuckets();// 5.3.遍歷桶內數據for (Terms.Bucket bucket : buckets) {// 5.4.獲取桶內keyString brand = bucket.getKeyAsString();System.out.print("brand = " + brand);long count = bucket.getDocCount();System.out.println("; count = " + count);}
}

image-20240508160435956

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/12430.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/12430.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/12430.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

MySQL變量的定義與使用(二)

一、通過變量進行實際的操作 set cityNameRotterdam; SELECT * from city where Name cityName; 二、變量只能處理字符&#xff0c;并不能代替符號或者關鍵字進行使用 set cityName1Rotterdam; set cityName2Zaanstad; set cityName3Zwolle; SELECT * from city where Name…

2024CCPC全國邀請賽(鄭州)暨河南省賽

2024CCPC全國邀請賽&#xff08;鄭州站&#xff09;暨河南省賽 一銅一銀&#xff0c;雖不是線下第一次參賽但是第一次拿xcpc獎牌&#xff0c;還有個國賽獎真是不戳。感謝學長&#xff0c;感謝隊友&#xff01; 雖然遺憾沒有沖到省賽金&#xff0c;不過還有icpc商丘&#xff08…

SpringBoot項目中使用Redis,Mybatis和JWT

在Spring Boot項目中&#xff0c;結合Redis&#xff0c;MyBatis和JWT的使用可以提供以下功能&#xff1a; Redis的作用&#xff1a; 1.緩存&#xff1a;Redis可以用作緩存存儲&#xff0c;提高應用程序的性能和響應速度。特別是對于頻繁讀取但不經常更新的數據&#xff0c;如配…

Milvus Cloud:打造向量數據庫的Airtable級體驗

向量數據庫Milvus Cloud是一種用于處理和存儲向量數據的數據庫,它通常用于機器學習、圖像和視頻檢索、自然語言處理等領域。要將其升級為類似Airtable那樣易用且一體化的系統,需要考慮以下幾個關鍵方面: 1. 用戶界面(UI)設計 Airtable之所以用戶友好,很大程度上歸功于其直…

整型進制轉換

整型常量的不同進制表示 計算機中只能存儲二進制數&#xff0c;即0和1&#xff0c;而在對應的物理硬件上則是高&#xff0c;低電平。為了更方便地觀察內存中的二進制情況&#xff0c;除我們正常使用的十進制數外&#xff0c;計算機還提供了十六進制數和八進制數。 下面介紹不…

類圖及類的關系

類圖&#xff08;Class Diagram&#xff09;是UML&#xff08;Unified Modeling Language&#xff0c;統一建模語言&#xff09;中的一種圖&#xff0c;用于描述系統中類的靜態結構&#xff0c;包括類的屬性、方法以及類之間的關系。 一、類 類&#xff08;Class&#xff09;…

海外倉混合訂單揀貨策略:人工與海外倉系統的最佳搭配模式

根據訂單高效揀貨是任何海外倉都要面對的問題。只有當訂單可以被高效&#xff0c;準確的揀貨之后&#xff0c;才能繼續走下面的物流流程&#xff0c;所以盡可能的縮短揀貨時間&#xff0c;提升揀貨精準度&#xff0c;才是提升訂單交付率的最佳方法。 海外倉企業都在不斷尋找&am…

Vue如何引入公用方法

文章目錄 1. 在全局范圍內引入2. 在單文件組件中引入3. 使用Vuex或Vue Composition API4. 使用mixins5. 使用插件 1. 在全局范圍內引入 在你的main.js或main.ts文件中引入并注冊你的公用方法&#xff0c;使得它們可以在整個Vue應用中使用。 // 引入你的公用方法文件 import {…

Android動態布局framelayout

功能說明 最近碰到一個需求&#xff0c;要求在網頁端拖控件&#xff0c;動態配置app控件的模塊&#xff0c;大小和位置&#xff0c;顯示不同的功能&#xff0c;然后在app大屏展示。 技術難點&#xff1a; 1.動態控件位置和大小難調&#xff0c;會出現布局混亂&#xff0c;位置錯…

129.哈希表:有效的字母異位詞(力扣)

242. 有效的字母異位詞 - 力扣&#xff08;LeetCode&#xff09; 題目描述 代碼解決以及思路 這個方法的時間復雜度為O(N)&#xff0c;其中N是字符串的長度&#xff0c;空間復雜度為O(1)&#xff08;因為輔助數組的大小是固定的26&#xff09;。 class Solution { public:bo…

python通過ctypes調用C/C++ SDK,當SDK異常時,同時打印C/C++/Python的棧信息

python通過ctypes調用C/C SDK,當SDK異常時,同時打印C/C/Python的棧信息 一.復現步驟二.輸出 本文演示了python通過ctypes調用C/C SDK,當SDK異常時,同時打印C/C/Python的棧信息.基于traceback、addr2line、PyErr_SetString、backtrace_symbols 一.復現步驟 cat > print_bac…

自媒體的發展趨勢:從個人表達到全球話語權

一、引言隨著數字技術的快速發展&#xff0c;信息傳播的方式和格局也在不斷變化。自媒體&#xff0c;作為其中的一股重要力量&#xff0c;正在以它的獨特方式改變著全球的信息傳播和社會發展。本文將從自媒體的定義及發展歷程入手&#xff0c;深入探討自媒體未來的發展趨勢&…

感知局部規劃--似然場局部規劃

系列文章目錄 提示&#xff1a;這里可以添加系列文章的所有文章的目錄&#xff0c;目錄需要自己手動添加 TODO:寫完再整理 文章目錄 系列文章目錄前言感知導航感知似然場局部規劃&#xff08;很像DWA但是不依賴地圖&#xff0c;完全依賴感知&#xff09; 前言 認知有限&#x…

Uniapp開發入門:構建跨平臺應用的全面指南

引言 什么是Uniapp Uniapp是一款由DCloud公司推出的基于Vue.js的跨平臺應用開發框架。它的核心理念是“一套代碼&#xff0c;多端運行”&#xff0c;開發者只需編寫一份代碼&#xff0c;即可生成包括iOS、Android、H5、微信小程序、支付寶小程序、百度小程序等多平臺的應用。…

初識C++ · string的使用(2)

目錄 1 Modifiers部分 1.1 assign的使用 1.2 insert的使用 1.3 erase的使用 1.4 replace的使用 2 capacity部分 2.1 max_size的使用 2.2 capacity的使用 2.3 reserve的使用 2.4 shrink_to_fit簡介 2.5 resize的使用 2.6 clear的使用 3 String operations部分 3.1 …

[數據結構1.0]快速排序

最近學習了快速排序&#xff0c;鼠鼠俺來做筆記了&#xff01; 本篇博客用排升序為例介紹快速排序&#xff01; 1.快速排序 快速排序是Hoare于1962年提出的一種二叉樹結構的交換排序方法&#xff0c;其基本思想為&#xff1a;任取待排序元素序列中的某元素作為基準值&#x…

202103青少年軟件編程(Python)等級考試試卷(一級)

一、單選題&#xff08;共25題&#xff0c;每題2分&#xff0c;共50分&#xff09; 下列哪個操作不能退出IDLE環境&#xff1f;&#xff08; &#xff09; A、AltF4 B、CtrlQ C、按ESC鍵 D、exit() 試題編號&#xff1a;20210124-yfj-003 題型&#xff1a;單選題 答案&#xf…

Java面試八股之一個char類型變量能不能存儲一個中文字符

Java中一個char類型變量能不能存儲一個中文字符&#xff1f;為什么&#xff1f; Java中一個char類型變量可以存儲一個中文字符。原因如下&#xff1a; Unicode編碼支持&#xff1a;Java語言采用Unicode字符集作為其內建字符編碼方式。Unicode是一種廣泛接受的字符編碼標準&am…

兩小時看完花書(深度學習入門篇)

1.深度學習花書前言 機器學習早期的時候十分依賴于已有的知識庫和人為的邏輯規則&#xff0c;需要人們花大量的時間去制定合理的邏輯判定&#xff0c;可以說是有多少人工&#xff0c;就有多少智能。后來逐漸發展出一些簡單的機器學習方法例如logistic regression、naive bayes等…

mybatisplus查詢練習代碼

mybatisplus查詢練習代碼 package com.yase;import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.yase.entity.Student; import com.yase.entity.Teacher; import com.yase…