第5章 商品搜索
學習目標
-
Elasticsearch安裝
docker安裝Elasticsearch 系統參數問題 跨域操作
-
IK分詞器配置
-
Kibana的使用->DSL語句
Kibana->DSL語句操作->Elasticsearch
-
ES導入商品搜索數據
Sku數據導入到Elasticsearch Map數據類型->Object
-
關鍵詞搜索->能夠實現搜索流程代碼的編寫
-
分類統計搜索
1. Elasticsearch 安裝
我們之前已經使用過elasticsearch了,這裏不再對它進行介紹了,直接下載安裝,本章節將採用Docker安裝,不過在市面上還有很多采用linxu安裝,關於linux安裝,已經提供了安裝手冊,這裏就不講了。
(1)docker鏡像下載
docker pull elasticsearch:5.6.8
注意:由於鏡像有570MB,所以提供的虛擬機裏已經下載好了該鏡像,如下圖:
(2)安裝es容器
docker run -di --name=changgou_elasticsearch -p 9200:9200 -p 9300:9300 elasticsearch:5.6.8
9200端口(Web管理平臺端口) 9300(服務默認端口)
瀏覽器輸入地址訪問:http://192.168.211.132:9200/
(3)開啓遠程連接
上面完成安裝後,es並不能正常使用,elasticsearch從5版本以後默認不開啓遠程連接,程序直接連接會報如下錯誤:
failed to load elasticsearch nodes : org.elasticsearch.client.transport.NoNodeAvailableException: None of the configured nodes are available: [{#transport#-1}{5ttLpMhkRjKLkvoY7ltUWg}{192.168.211.132}{192.168.211.132:9300}]
我們需要修改es配置開啓遠程連接,代碼如下:
登錄容器
docker exec -it changgou_elasticsearch /bin/bash
查看目錄結構 輸入: dir
root@07f22eb41bb5:/usr/share/elasticsearch# dir
NOTICE.txt README.textile bin config data lib logs modules plugins
進入config目錄
cd config
查看文件
root@07f22eb41bb5:/usr/share/elasticsearch/config# ls
elasticsearch.yml log4j2.properties scripts
修改elasticsearch.yml文件
root@07f22eb41bb5:/usr/share/elasticsearch/config# vi elasticsearch.yml
bash: vi: command not found
vi命令無法識別,因爲docker容器裏面沒有該命令,我們可以安裝該編輯器。
安裝vim編輯器
apt-get update
apt-get install vim
安裝好了後,修改elasticsearch.yml配置,如下圖:
vi elasticsearch.yml
修改如下圖:
同時添加下面一行代碼:
cluster.name: my-application
重啓docker
docker restart changgou_elasticsearch
(4)系統參數配置
重啓後發現重啓啓動失敗了,這時什麼原因呢?這與我們剛纔修改的配置有關,因爲elasticsearch在啓動的時候會進行一些檢查,比如最多打開的文件的個數以及虛擬內存區域數量等等,如果你放開了此配置,意味着需要打開更多的文件以及虛擬內存,所以我們還需要系統調優
修改vi /etc/security/limits.conf ,追加內容 (nofile是單個進程允許打開的最大文件個數 soft nofile 是軟限制 hard nofile是硬限制 )
* soft nofile 65536
* hard nofile 65536
修改vi /etc/sysctl.conf,追加內容 (限制一個進程可以擁有的VMA(虛擬內存區域)的數量 )
vm.max_map_count=655360
執行下面命令 修改內核參數馬上生效
sysctl -p
重新啓動虛擬機,再次啓動容器,發現已經可以啓動並遠程訪問
reboot
(5)跨域配置
修改elasticsearch/config下的配置文件:elasticsearch.yml,增加以下三句命令,並重啓:
http.cors.enabled: true
http.cors.allow-origin: "*"
network.host: 192.168.211.132
其中:
http.cors.enabled: true:此步爲允許elasticsearch跨域訪問,默認是false。
http.cors.allow-origin: “*”:表示跨域訪問允許的域名地址(*表示任意)。
重啓
docker restart changgou_elasticsearch
小提示:如果想讓容器開啓重啓,可以執行下面命令
docker update --restart=always 容器名稱或者容器id
2. IK分詞器安裝
(1)安裝ik分詞器
IK分詞器下載地址https://github.com/medcl/elasticsearch-analysis-ik/releases
將ik分詞器上傳到服務器上,然後解壓,並改名字爲ik
unzip elasticsearch-analysis-ik-5.6.8.zip
mv elasticsearch ik
將ik目錄拷貝到docker容器的plugins目錄下
docker cp ./ik changgou_elasticsearch:/usr/share/elasticsearch/plugins
(2)IK分詞器測試
訪問:http://192.168.211.132:9200/_analyze?analyzer=ik_smart&pretty=true&text=我是程序員
訪問:http://192.168.211.132:9200/_analyze?analyzer=ik_max_word&pretty=true&text=我是程序員
3. Kibana使用-掌握DSL語句
我們上面使用的是elasticsearch-head插件實現數據查找的,但是elasticsearch-head的功能比較單一,我們這裏需要一個更專業的工具實現對日誌的實時分析,也就是我們接下來要講的kibana。
Kibana 是一款開源的數據分析和可視化平臺,它是 Elastic Stack 成員之一,設計用於和 Elasticsearch 協作。您可以使用 Kibana 對 Elasticsearch 索引中的數據進行搜索、查看、交互操作。您可以很方便的利用圖表、表格及地圖對數據進行多元化的分析和呈現。
Kibana 可以使大數據通俗易懂。它很簡單,基於瀏覽器的界面便於您快速創建和分享動態數據儀表板來追蹤 Elasticsearch 的實時數據變化。
搭建 Kibana 非常簡單。您可以分分鐘完成 Kibana 的安裝並開始探索 Elasticsearch 的索引數據 — 沒有代碼、不需要額外的基礎設施。
3.1 Kibana下載安裝
我們項目中不再使用linux,直接使用Docker,所有這裏就不演示在windows的下載安裝了。
(1)鏡像下載
docker pull docker.io/kibana:5.6.8
爲了節省時間,虛擬機中已經存在該版本的鏡像了.
(2)安裝kibana容器
執行如下命令,開始安裝kibana容器
docker run -it -d -e ELASTICSEARCH_URL=http://192.168.211.132:9200 --name kibana --restart=always -p 5601:5601 kibana:5.6.8
ELASTICSEARCH_URL=http://192.168.211.132:9200:是指鏈接的ES地址
restart=always:每次服務都會重啓,也就是開啓啓動
5601:5601:端口號
(3)訪問測試
訪問http://192.168.211.132:5601
如下:
3.2 Kibana使用
3.2.1 配置索引
要使用Kibana,您必須至少配置一個索引。索引用於標識Elasticsearch索引以運行搜索和分析。它們還用於配置字段。
我們修改索引名稱的匹配方式即可,下面2個選項不用勾選。點擊create,會展示出當前配置的索引的域信息,如下圖:
域的每個標題選項分別代表如下意思:
3.2.2 數據搜索
Discover爲數據搜索部分,可以對日誌信息進行搜索操作。
可以使用Discover實現數據搜索過濾和搜索條件顯示以及關鍵詞搜索,如下圖:
3.2.3 DSL語句使用
3.2.3.1 Query DSL結構化查詢介紹
Query DSL是一個Java開源框架用於構建類型安全的SQL查詢語句。採用API代替傳統的拼接字符串來構造查詢語句。目前Querydsl支持的平臺包括JPA,JDO,SQL,Java Collections,RDF,Lucene,Hibernate Search。elasticsearch提供了一整套基於JSON的查詢DSL語言來定義查詢。
Query DSL當作是一系列的抽象的查詢表達式樹(AST)特定查詢能夠包含其它的查詢,(如 bool ), 有些查詢能夠包含過濾器(如 constant_score), 還有的可以同時包含查詢和過濾器 (如 filtered). 都能夠從ES支持查詢集合裏面選擇任意一個查詢或者是從過濾器集合裏面挑選出任意一個過濾器, 這樣的話,我們就可以構造出任意複雜(maybe 非常有趣)的查詢了。
3.2.3.2 索引操作
(1)查詢所有索引
GET /_cat/indices?v
結果如下:
(2)刪除某個索引
DELETE /skuinfo
效果如下:
(3)新增索引
PUT /user
效果如下:
(4)創建映射
PUT /user/userinfo/_mapping
{
"properties": {
"name":{
"type": "text",
"analyzer": "ik_smart",
"search_analyzer": "ik_smart",
"store": false
},
"city":{
"type": "text",
"analyzer": "ik_smart",
"search_analyzer": "ik_smart",
"store": false
},
"age":{
"type": "long",
"store": false
},
"description":{
"type": "text",
"analyzer": "ik_smart",
"search_analyzer": "ik_smart",
"store": false
}
}
}
效果如下:
(5)新增文檔數據
PUT /user/userinfo/1
{
"name":"李四",
"age":22,
"city":"深圳",
"description":"李四來自湖北武漢!"
}
效果如下:
我們再增加3條記錄:
#新增文檔數據 id=2
PUT /user/userinfo/2
{
"name":"王五",
"age":35,
"city":"深圳",
"description":"王五家住在深圳!"
}
#新增文檔數據 id=3
PUT /user/userinfo/3
{
"name":"張三",
"age":19,
"city":"深圳",
"description":"在深圳打工,來自湖北武漢"
}
#新增文檔數據 id=4
PUT /user/userinfo/4
{
"name":"張三丰",
"age":66,
"city":"武漢",
"description":"在武漢讀書,家在武漢!"
}
#新增文檔數據 id=5
PUT /user/userinfo/5
{
"name":"趙子龍",
"age":77,
"city":"廣州",
"description":"趙子龍來自深圳寶安,但是在廣州工作!",
"address":"廣東省茂名市"
}
#新增文檔數據 id=6
PUT /user/userinfo/6
{
"name":"趙毅",
"age":55,
"city":"廣州",
"description":"趙毅來自廣州白雲區,從事電子商務8年!"
}
#新增文檔數據 id=7
PUT /user/userinfo/7
{
"name":"趙哈哈",
"age":57,
"city":"武漢",
"description":"武漢趙哈哈,在深圳打工已有半年了,月薪7500!"
}
(6)修改數據
a.替換操作
更新數據可以使用之前的增加操作,這種操作會將整個數據替換掉,代碼如下:
#更新數據,id=4
PUT /user/userinfo/4
{
"name":"張三丰",
"description":"在武漢讀書,家在武漢!在深圳工作!"
}
效果如下:
使用GET命令查看:
#根據ID查詢
GET /user/userinfo/4
效果如下:
b.更新操作
我們先使用下面命令恢復數據:
#恢復文檔數據 id=4
PUT /user/userinfo/4
{
"name":"張三丰",
"age":66,
"city":"武漢",
"description":"在武漢讀書,家在武漢!"
}
使用POST更新某個列的數據
#使用POST更新某個域的數據
POST /user/userinfo/4/_update
{
"doc":{
"name":"張三丰",
"description":"在武漢讀書,家在武漢!在深圳工作!"
}
}
效果如下:
使用GET命令查看:
#根據ID查詢
GET /user/userinfo/4
效果如下:
(7)刪除Document
#刪除數據
DELETE user/userinfo/7
3.2.3.3 數據查詢
(1)查詢所有數據
#查詢所有
GET /user/_search
效果如下:
(2)根據ID查詢
#根據ID查詢
GET /user/userinfo/2
效果如下:
(3)Sort排序
#搜索排序
GET /user/_search
{
"query":{
"match_all": {}
},
"sort":{
"age":{
"order":"desc"
}
}
}
效果如下:
(4)分頁
#分頁實現
GET /user/_search
{
"query":{
"match_all": {}
},
"sort":{
"age":{
"order":"desc"
}
},
"from": 0,
"size": 2
}
解釋:
from:從下N的記錄開始查詢
size:每頁顯示條數
效果如下:
3.2.3.4 過濾查詢
(1)term過濾
term主要用於分詞精確匹配,如字符串、數值、日期等(不適合情況:1.列中除英文字符外有其它值 2.字符串值中有冒號或中文 3.系統自帶屬性如_version)
如下案例:
#過濾查詢-term
GET _search
{
"query":{
"term":{
"city":"武漢"
}
}
}
效果如下:
(2)terms 過濾
terms 跟 term 有點類似,但 terms 允許指定多個匹配條件。 如果某個字段指定了多個值,那麼文檔需要一起去做匹配 。
案例如下:
#過濾查詢-terms 允許多個Term
GET _search
{
"query":{
"terms":{
"city":
[
"武漢",
"廣州"
]
}
}
}
果如下:
(3) range 過濾
range過濾允許我們按照指定範圍查找一批數據。例如我們查詢年齡範圍
案例如下:
#過濾-range 範圍過濾
#gt表示> gte表示=>
#lt表示< lte表示<=
GET _search
{
"query":{
"range": {
"age": {
"gte": 30,
"lte": 57
}
}
}
}
上圖效果如下:
(4)exists過濾
exists 過濾可以用於查找擁有某個域的數據
案例如下:
#過濾搜索 exists:是指包含某個域的數據檢索
GET _search
{
"query": {
"exists":{
"field":"address"
}
}
}
效果如下:
(5) bool 過濾
bool 過濾可以用來合併多個過濾條件查詢結果的布爾邏輯,它包含一下操作符:
- must : 多個查詢條件的完全匹配,相當於 and。
- must_not : 多個查詢條件的相反匹配,相當於 not。
- should : 至少有一個查詢條件匹配, 相當於 or。
這些參數可以分別繼承一個過濾條件或者一個過濾條件的數組:
案例如下:
#過濾搜索 bool
#must : 多個查詢條件的完全匹配,相當於 and。
#must_not : 多個查詢條件的相反匹配,相當於 not。
#should : 至少有一個查詢條件匹配, 相當於 or。
GET _search
{
"query": {
"bool": {
"must": [
{
"term": {
"city": {
"value": "深圳"
}
}
},
{
"range":{
"age":{
"gte":20,
"lte":99
}
}
}
]
}
}
}
效果如下:
(6) match_all 查詢
可以查詢到所有文檔,是沒有查詢條件下的默認語句。
案例如下:
#查詢所有 match_all
GET _search
{
"query": {
"match_all": {}
}
}
(7) match 查詢
match查詢是一個標準查詢,不管你需要全文本查詢還是精確查詢基本上都要用到它。
如果你使用 match 查詢一個全文本字段,它會在真正查詢之前用分析器先分析match一下查詢字符:
案例如下:
#字符串匹配
GET _search
{
"query": {
"match": {
"description": "武漢"
}
}
}
效果如下:
(8)prefix 查詢
以什麼字符開頭的,可以更簡單地用 prefix ,例如查詢所有以張開始的用戶描述
案例如下:
#前綴匹配 prefix
GET _search
{
"query": {
"prefix": {
"name": {
"value": "趙"
}
}
}
}
效果如下:
(9)multi_match 查詢
multi_match查詢允許你做match查詢的基礎上同時搜索多個字段,在多個字段中同時查一個
案例如下:
#多個域匹配搜索
GET _search
{
"query": {
"multi_match": {
"query": "深圳",
"fields": [
"city",
"description"
]
}
}
}
效果如下:
3.2.3.5 完整DSL語句代碼
#查看所有索引
GET /_cat/indices?v
#刪除某個索引
DELETE /skuinfo
#新增索引
PUT /user
#創建映射
PUT /user/userinfo/_mapping
{
"properties": {
"name":{
"type": "text",
"analyzer": "ik_smart",
"search_analyzer": "ik_smart",
"store": false
},
"city":{
"type": "text",
"analyzer": "ik_smart",
"search_analyzer": "ik_smart",
"store": false
},
"age":{
"type": "long",
"store": false
},
"description":{
"type": "text",
"analyzer": "ik_smart",
"search_analyzer": "ik_smart",
"store": false
}
}
}
#新增文檔數據 id=1
PUT /user/userinfo/1
{
"name":"李四",
"age":22,
"city":"深圳",
"description":"李四來自湖北武漢!"
}
#新增文檔數據 id=2
PUT /user/userinfo/2
{
"name":"王五",
"age":35,
"city":"深圳",
"description":"王五家住在深圳!"
}
#新增文檔數據 id=3
PUT /user/userinfo/3
{
"name":"張三",
"age":19,
"city":"深圳",
"description":"在深圳打工,來自湖北武漢"
}
#新增文檔數據 id=4
PUT /user/userinfo/4
{
"name":"張三丰",
"age":66,
"city":"武漢",
"description":"在武漢讀書,家在武漢!"
}
#新增文檔數據 id=5
PUT /user/userinfo/5
{
"name":"趙子龍",
"age":77,
"city":"廣州",
"description":"趙子龍來自深圳寶安,但是在廣州工作!",
"address":"廣東省茂名市"
}
#新增文檔數據 id=6
PUT /user/userinfo/6
{
"name":"趙毅",
"age":55,
"city":"廣州",
"description":"趙毅來自廣州白雲區,從事電子商務8年!"
}
#新增文檔數據 id=7
PUT /user/userinfo/7
{
"name":"趙哈哈",
"age":57,
"city":"武漢",
"description":"武漢趙哈哈,在深圳打工已有半年了,月薪7500!"
}
#更新數據,id=4
PUT /user/userinfo/4
{
"name":"張三丰",
"description":"在武漢讀書,家在武漢!在深圳工作!"
}
#根據ID查詢
GET /user/userinfo/4
#恢復文檔數據 id=4
PUT /user/userinfo/4
{
"name":"張三丰",
"age":66,
"city":"武漢",
"description":"在武漢讀書,家在武漢!"
}
#使用POST更新某個域的數據
POST /user/userinfo/4/_update
{
"doc":{
"name":"張三丰",
"description":"在武漢讀書,家在武漢!在深圳工作!"
}
}
#根據ID查詢
GET /user/userinfo/4
#刪除數據
DELETE user/userinfo/4
#查詢所有
GET /user/_search
#根據ID查詢
GET /user/userinfo/2
#搜索排序
GET /user/_search
{
"query":{
"match_all": {}
},
"sort":{
"age":{
"order":"desc"
}
}
}
#分頁實現
GET /user/_search
{
"query":{
"match_all": {}
},
"sort":{
"age":{
"order":"desc"
}
},
"from": 0,
"size": 2
}
#過濾查詢-term
GET _search
{
"query":{
"term":{
"city":"武漢"
}
}
}
#過濾查詢-terms 允許多個Term
GET _search
{
"query":{
"terms":{
"city":
[
"武漢",
"廣州"
]
}
}
}
#過濾-range 範圍過濾
#gt表示> gte表示=>
#lt表示< lte表示<=
GET _search
{
"query":{
"range": {
"age": {
"gte": 30,
"lte": 57
}
}
}
}
#過濾搜索 exists:是指包含某個域的數據檢索
GET _search
{
"query": {
"exists":{
"field":"address"
}
}
}
#過濾搜索 bool
#must : 多個查詢條件的完全匹配,相當於 and。
#must_not : 多個查詢條件的相反匹配,相當於 not。
#should : 至少有一個查詢條件匹配, 相當於 or。
GET _search
{
"query": {
"bool": {
"must": [
{
"term": {
"city": {
"value": "深圳"
}
}
},
{
"range":{
"age":{
"gte":20,
"lte":99
}
}
}
]
}
}
}
#查詢所有 match_all
GET _search
{
"query": {
"match_all": {}
}
}
#字符串匹配
GET _search
{
"query": {
"match": {
"description": "武漢"
}
}
}
#前綴匹配 prefix
GET _search
{
"query": {
"prefix": {
"name": {
"value": "趙"
}
}
}
}
#多個域匹配搜索
GET _search
{
"query": {
"multi_match": {
"query": "深圳",
"fields": [
"city",
"description"
]
}
}
}
4. 數據導入ES
4.1 SpringData Elasticsearch介紹
4.1.1 SpringData介紹
Spring Data是一個用於簡化數據庫訪問,並支持雲服務的開源框架。其主要目標是使得對數據的訪問變得方便快捷,並支持map-reduce框架和雲計算數據服務。 Spring Data可以極大的簡化JPA的寫法,可以在幾乎不用寫實現的情況下,實現對數據的訪問和操作。除了CRUD外,還包括如分頁、排序等一些常用的功能。
Spring Data的官網:http://projects.spring.io/spring-data/
4.1.2 SpringData ES介紹
Spring Data ElasticSearch 基於 spring data API 簡化 elasticSearch操作,將原始操作elasticSearch的客戶端API 進行封裝 。Spring Data爲Elasticsearch項目提供集成搜索引擎。Spring Data Elasticsearch POJO的關鍵功能區域爲中心的模型與Elastichsearch交互文檔和輕鬆地編寫一個存儲庫數據訪問層。 官方網站:http://projects.spring.io/spring-data-elasticsearch/
4.2 搜索工程搭建
創建搜索微服務工程,changgou-service-search,該工程主要提供搜索服務以及索引數據的更新操作。
(1)API工程搭建
首先創建search的API工程,在changgou-service-api中創建changgou-service-search-api,如下圖:
pom.xml如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>changgou-service-api</artifactId>
<groupId>com.changgou</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>changgou-service-search-api</artifactId>
<dependencies>
<!--goods API依賴-->
<dependency>
<groupId>com.changgou</groupId>
<artifactId>changgou-service-goods-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!--SpringDataES依賴-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
</dependencies>
</project>
(2)搜索微服務搭建
在changgou-service中搭建changgou-service-search微服務,並進行相關配置。
pom.xml配置
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>changgou-service</artifactId>
<groupId>com.changgou</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>changgou-service-search</artifactId>
<dependencies>
<!--依賴search api-->
<dependency>
<groupId>com.changgou</groupId>
<artifactId>changgou-service-search-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
application.yml配置
server:
port: 18086
spring:
application:
name: search
data:
elasticsearch:
cluster-name: my-application
cluster-nodes: 192.168.211.132:9300
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:7001/eureka
instance:
prefer-ip-address: true
feign:
hystrix:
enabled: true
#超時配置
ribbon:
ReadTimeout: 300000
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 10000
配置說明:
connection-timeout:服務連接超時時間
socket-connect:HTTP請求超時時間
ribbon.ReadTimeout: Feign請求讀取數據超時時間
timeoutInMilliseconds:feign連接超時時間
cluster-name:Elasticsearch的集羣節點名稱,這裏需要和Elasticsearch集羣節點名稱保持一致
cluster-nodes:Elasticsearch節點通信地址
(3)啓動類
創建SearchApplication作爲搜索微服務工程的啓動類,代碼如下:
@SpringBootApplication(exclude={DataSourceAutoConfiguration.class})
@EnableEurekaClient
public class SearchApplication {
public static void main(String[] args) {
/**
* Springboot整合Elasticsearch 在項目啓動前設置一下的屬性,防止報錯
* 解決netty衝突後初始化client時還會拋出異常
* availableProcessors is already set to [12], rejecting [12]
***/
System.setProperty("es.set.netty.runtime.available.processors", "false");
SpringApplication.run(SearchApplication.class,args);
}
}
分別創建對應的包,dao、service、controller,如下圖:
4.3 數據導入
現在需要將數據從數據庫中查詢出來,然後將數據導入到ES中。
數據導入流程如下:
1.請求search服務,調用數據導入地址
2.根據註冊中心中的註冊的goods服務的地址,使用Feign方式查詢所有已經審覈的Sku
3.使用SpringData Es將查詢到的Sku集合導入到ES中
4.3.1 文檔映射Bean創建
搜索商品的時候,會根據如下屬性搜索數據,並且不是所有的屬性都需要分詞搜索,我們創建JavaBean,將JavaBean數據存入到ES中要以搜索條件和搜索展示結果爲依據,部分關鍵搜索條件分析如下:
1.可能會根據商品名稱搜素,而且可以搜索商品名稱中的任意一個詞語,所以需要分詞
2.可能會根據商品分類搜索,商品分類不需要分詞
3.可能會根據商品品牌搜索,商品品牌不需要分詞
4.可能會根據商品商家搜索,商品商家不需要分詞
5.可能根據規格進行搜索,規格時一個鍵值對結構,用Map
根據上面的分析,我們可以在changgou-service-search-api工程中創建com.changgou.search.pojo.SkuInfo,如下
@Document(indexName = "skuinfo",type = "docs")
public class SkuInfo implements Serializable {
//商品id,同時也是商品編號
@Id
private Long id;
//SKU名稱
@Field(type = FieldType.Text, analyzer = "ik_smart")
private String name;
//商品價格,單位爲:元
@Field(type = FieldType.Double)
private Long price;
//庫存數量
private Integer num;
//商品圖片
private String image;
//商品狀態,1-正常,2-下架,3-刪除
private String status;
//創建時間
private Date createTime;
//更新時間
private Date updateTime;
//是否默認
private String isDefault;
//SPUID
private Long spuId;
//類目ID
private Long categoryId;
//類目名稱
@Field(type = FieldType.Keyword)
private String categoryName;
//品牌名稱
@Field(type = FieldType.Keyword)
private String brandName;
//規格
private String spec;
//規格參數
private Map<String,Object> specMap;
//...略
}
4.3.2 搜索審覈通過Sku
修改changgou-service-goods微服務,添加搜索審覈通過的Sku,供search微服務調用。下面都是針對goods微服務的操作。
修改SkuService接口,添加根據狀態查詢Sku方法,代碼如下:
/**
* 根據狀態查詢SKU列表
*/
List<Sku> findByStatus(String status);
修改SkuServiceImpl,添加根據狀態查詢Sku實現方法,代碼如下:
/***
* 根據狀態查詢SKU列表
* @return
*/
@Override
public List<Sku> findByStatus(String status) {
Sku sku = new Sku();
sku.setStatus(status);
return skuMapper.select(sku);
}
修改com.changgou.goods.controller.SkuController,添加根據審覈狀態查詢Sku方法,代碼如下:
/***
* 根據審覈狀態查詢Sku
* @param status
* @return
*/
@GetMapping("/status/{status}")
public Result<List<Sku>> findByStatus(@PathVariable String status){
List<Sku> list = skuService.findByStatus(status);
return new Result<List<Sku>>(true,StatusCode.OK,"查詢成功",list);
}
4.3.3 Sku導入ES實現
(1) Feign配置
修改changgou-service-goods-api工程,在com.changgou.goods.feign.SkuFeign上添加findSkuList方法,代碼如下:
@FeignClient(name="goods")
@RequestMapping(value = "/sku")
public interface SkuFeign {
/***
* 根據審覈狀態查詢Sku
* @param status
* @return
*/
@GetMapping("/status/{status}")
Result<List<Sku>> findByStatus(@PathVariable String status);
}
(2) Dao創建
修改changgou-service-search工程,創建com.changgou.search.dao.SkuEsMapper,該接口主要用於索引數據操作,主要使用它來實現將數據導入到ES索引庫中,代碼如下:
@Repository
public interface SkuEsMapper extends ElasticsearchRepository<Sku,Long> {
}
(3) 服務層創建
修改changgou-service-search工程,創建com.changgou.search.service.SkuService,代碼如下:
public interface SkuService {
/***
* 導入SKU數據
*/
void importSku();
}
修改changgou-service-search工程,創建com.changgou.search.service.impl.SkuServiceImpl,實現Sku數據導入到ES中,代碼如下:
@Service
public class SkuServiceImpl implements SkuService {
@Autowired
private SkuFeign skuFeign;
@Autowired
private SkuEsMapper skuEsMapper;
/**
* 導入sku數據到es
*/
@Override
public void importSku(){
//調用changgou-service-goods微服務
Result<List<Sku>> skuListResult = skuFeign.findByStatus("1");
//將數據轉成search.Sku
List<SkuInfo> skuInfos= JSON.parseArray(JSON.toJSONString(skuListResult.getData()),SkuInfo.class);
for(SkuInfo skuInfo:skuInfos){
Map<String, Object> specMap= JSON.parseObject(skuInfo.getSpec()) ;
skuInfo.setSpecMap(specMap);
}
skuEsMapper.saveAll(skuInfos);
}
}
(4)控制層配置
修改changgou-service-search工程,在com.changgou.search.controller.SkuController類中添加如下方法調用上述導入方法,代碼如下:
@RestController
@RequestMapping(value = "/search")
@CrossOrigin
public class SkuController {
@Autowired
private SkuService skuService;
/**
* 導入數據
* @return
*/
@GetMapping("/import")
public Result search(){
skuService.importSku();
return new Result(true, StatusCode.OK,"導入數據到索引庫中成功!");
}
}
(5)修改啓動類
啓動類中需要開啓Feign客戶端,並且需要添加ES包掃描,代碼如下:
@SpringBootApplication(exclude={DataSourceAutoConfiguration.class})
@EnableEurekaClient
@EnableFeignClients(basePackages = "com.changgou.goods.feign")
@EnableElasticsearchRepositories(basePackages = "com.changgou.search.dao")
public class SearchApplication {
public static void main(String[] args) {
/**
* Springboot整合Elasticsearch 在項目啓動前設置一下的屬性,防止報錯
* 解決netty衝突後初始化client時還會拋出異常
* java.lang.IllegalStateException: availableProcessors is already set to [12], rejecting [12]
***/
System.setProperty("es.set.netty.runtime.available.processors", "false");
SpringApplication.run(SearchApplication.class,args);
}
}
(6)測試
調用http://localhost:18085/search/import進行測試
打開es-head可以看到如下數據:
5. 關鍵字搜索
我們先使用SpringDataElasticsearch實現一個簡單的搜索功能,先實現根據關鍵字搜索,從上面搜索圖片可以看得到,每次搜索的時候,除了關鍵字外,還有可能有品牌、分類、規格等,後臺接收搜索條件使用Map接收比較合適。
5.1 服務層實現
修改search服務的com.changgou.search.service.SkuService,添加搜索方法,代碼如下:
/***
* 搜索
* @param searchMap
* @return
*/
Map search(Map<String, String> searchMap);
修改search服務的com.changgou.search.service.impl.SkuServiceImpl,添加搜索實現方法,代碼如下:
@Autowired
private ElasticsearchTemplate esTemplate;
public Map search(Map<String, String> searchMap) {
//1.獲取關鍵字的值
String keywords = searchMap.get("keywords");
if (StringUtils.isEmpty(keywords)) {
keywords = "華爲";//賦值給一個默認的值
}
//2.創建查詢對象 的構建對象
NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();
//3.設置查詢的條件
nativeSearchQueryBuilder.withQuery(QueryBuilders.matchQuery("name", keywords));
//4.構建查詢對象
NativeSearchQuery query = nativeSearchQueryBuilder.build();
//5.執行查詢
AggregatedPage<SkuInfo> skuPage = esTemplate.queryForPage(query, SkuInfo.class);
//6.返回結果
Map resultMap = new HashMap<>();
resultMap.put("rows", skuPage.getContent());
resultMap.put("total", skuPage.getTotalElements());
resultMap.put("totalPages", skuPage.getTotalPages());
return resultMap;
}
5.2 控制層實現
修改com.changgou.search.controller.SkuController,在控制層調用Service層即可,代碼如下:
/**
* 搜索
* @param searchMap
* @return
*/
@PostMapping
public Map search(@RequestBody(required = false) Map searchMap){
return skuService.search(searchMap);
}
5.3 測試
使用Postman工具,輸入http://localhost:18085/search
選中POST提交
6. 分類統計
6.1 分類統計分析
看下面的SQL語句,我們在執行搜索的時候,第1條SQL語句是執行搜,第2條語句是根據分類名字分組查看有多少分類,大概執行了2個步驟就可以獲取數據結果以及分類統計,我們可以發現他們的搜索條件完全一樣。
-- 查詢所有
SELECT * FROM tb_sku WHERE name LIKE '%手機%';
-- 根據分類名字分組查詢
SELECT category_name FROM tb_sku WHERE name LIKE '%手機%' GROUP BY category_name;
我們每次執行搜索的時候,需要顯示商品分類名稱,這裏要顯示的分類名稱其實就是符合搜素條件的所有商品的分類集合,我們可以按照上面的實現思路,使用ES根據分組名稱做一次分組查詢即可實現。
6.2 分類分組統計實現
修改search微服務的com.changgou.search.service.impl.SkuServiceImpl類,整體代碼如下:
public Map search(Map<String, String> searchMap) {
//1.獲取關鍵字的值
String keywords = searchMap.get("keywords");
if (StringUtils.isEmpty(keywords)) {
keywords = "華爲";//賦值給一個默認的值
}
//2.創建查詢對象 的構建對象
NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();
//3.設置查詢的條件
//設置分組條件 商品分類
nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms("skuCategorygroup").field("categoryName").size(50));
nativeSearchQueryBuilder.withQuery(QueryBuilders.matchQuery("name", keywords));
//4.構建查詢對象
NativeSearchQuery query = nativeSearchQueryBuilder.build();
//5.執行查詢
AggregatedPage<SkuInfo> skuPage = esTemplate.queryForPage(query, SkuInfo.class);
//獲取分組結果
StringTerms stringTerms = (StringTerms) skuPage.getAggregation("skuCategorygroup");
List<String> categoryList = new ArrayList<>();
if (stringTerms != null) {
for (StringTerms.Bucket bucket : stringTerms.getBuckets()) {
String keyAsString = bucket.getKeyAsString();//分組的值
categoryList.add(keyAsString);
}
}
//6.返回結果
Map resultMap = new HashMap<>();
resultMap.put("categoryList", categoryList);
resultMap.put("rows", skuPage.getContent());
resultMap.put("total", skuPage.getTotalElements());
resultMap.put("totalPages", skuPage.getTotalPages());
return resultMap;
}
添加的代碼如下:
6.3 測試
請求http://localhost:18086/search
6.4 代碼優化
如上,可以將獲取分組的代碼進行提取,如下代碼所示:
/**
* 獲取分類列表數據
*
* @param stringTerms
* @return
*/
private List<String> getStringsCategoryList(StringTerms stringTerms) {
List<String> categoryList = new ArrayList<>();
if (stringTerms != null) {
for (StringTerms.Bucket bucket : stringTerms.getBuckets()) {
String keyAsString = bucket.getKeyAsString();//分組的值
categoryList.add(keyAsString);
}
}
return categoryList;
}
在search方法中進行調用:
List<String> categoryList = getStringsCategoryList(stringTermsCategory);
整體代碼如下:
public Map search(Map<String, String> searchMap) {
//1.獲取關鍵字的值
String keywords = searchMap.get("keywords");
if (StringUtils.isEmpty(keywords)) {
keywords = "華爲";//賦值給一個默認的值
}
//2.創建查詢對象 的構建對象
NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();
//3.設置查詢的條件
//設置分組條件 商品分類
nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms("skuCategorygroup").field("categoryName").size(50));
nativeSearchQueryBuilder.withQuery(QueryBuilders.matchQuery("name", keywords));
//4.構建查詢對象
NativeSearchQuery query = nativeSearchQueryBuilder.build();
//5.執行查詢
AggregatedPage<SkuInfo> skuPage = esTemplate.queryForPage(query, SkuInfo.class);
//獲取分組結果
StringTerms stringTermsCategory = (StringTerms) skuPage.getAggregation("skuCategorygroup");
List<String> categoryList =getStringsCategoryList(stringTermsCategory);
//6.返回結果
Map resultMap = new HashMap<>();
resultMap.put("categoryList", categoryList);
resultMap.put("rows", skuPage.getContent());
resultMap.put("total", skuPage.getTotalElements());
resultMap.put("totalPages", skuPage.getTotalPages());
return resultMap;
}
private List<String> getStringsCategoryList(StringTerms stringTerms) {
List<String> categoryList = new ArrayList<>();
if (stringTerms != null) {
for (StringTerms.Bucket bucket : stringTerms.getBuckets()) {
String keyAsString = bucket.getKeyAsString();//分組的值
categoryList.add(keyAsString);
}
}
return categoryList;
}