java-web系列(九)---SpringBoot整合ElasticSearch

前言

這個項目的github地址:extensible項目的github地址

extensible項目當前功能模塊如下:

java-web系列(一)—搭建一個基於SSM框架的java-web項目
java-web系列(二)—以dockerfile的方式發佈java-web項目
java-web系列(三)—(slf4j + logback)進行日誌分層
java-web系列(四)—幾種常見的加密算法
java-web系列(五)—SpringBoot整合Redis
java-web系列(六)—Mybatis動態多數據源配置
java-web系列(七)—SpringBoot整合Quartz實現多定時任務
java-web系列(八)—RabbitMQ在java-web中的簡單應用
java-web系列(九)—SpringBoot整合ElasticSearch

如對該項目有疑問,可在我的博客/github下面留言,也可以以郵件的方式告知。
我的聯繫方式:[email protected]

ElasticSearch介紹

ElasticSearch是一個開源的高擴展的分佈式全文檢索引擎。

它可以近乎實時(延遲1秒)的存儲、檢索並處理PB級別的數據。

它是一個基於Lucene的搜索服務器。使用Java開發通過簡單的RestFul API提供全文檢索功能,這種做法隱藏了全文檢索功能內部實現的複雜性。

核心概念

Document文檔文檔ElasticSearch中數據存儲的基礎單元。我們可以將文檔理解爲一條以json格式存儲信息的記錄。例如:我們可以在ElasticSearch中用文檔這樣描述一條商品信息:

{
    "name": "羽絨服",
    "brand": "南極人",
    "season": "冬季款",
    "size": ["S", "M", "XL", "XXL", "XXXL"],
    "price": 998
}

Type類型類型是將一類相似的文檔進行歸類。如:可以將京東的商品信息歸爲類型type = jd,將天貓的商品信息歸爲類型type = tmall,將亞馬遜的商品信息歸爲類型type = amazon等等。
ElasticSearch已經不推薦使用類型,當前最新的ElasticSearch版本是6.6。ElasticSearch-6.x版本中一個索引只允許有一個類型,7.x版本中會徹底刪除類型

Index索引索引就是存放文檔的地方,目前可以存放一個或多個類型。數據必須要指定存放的索引,才能被檢索到。

爲了便於理解,我們可以簡單的類比:索引相當於數據庫,類型相當於表,文檔相當於表記錄。但是ElasticSearch並不是關係型數據庫,索引中可以不存放類型,直接存放文檔

Cluster和Node集羣與節點。由於ElasticSearch可以存放並檢索PB級別的數據,一臺服務器是存放不了這麼多數據的,而且從ElasticSearch的高可用以及容災性來考慮,必定是用多個服務器協調存儲數據的。節點就是具備ElasticSearch環境並存放有數據的單個服務器。集羣就是所有可用的節點組成的網狀圖。

它們的關係圖我們可以理解如下:

核心概念圖

Primary shard和Replica shard主分片和副本分片分片就是對索引的切分。一個索引默認會被分成5個主分片副本分片就是主分片的數據備份,主分片可以有對應的零個或多個副本分片主分片和其對應的副本分片是不會放在一個服務器的。這個很好理解,數據檢索時,檢索的是主分片上的數據,當主分片所在節點出現故障時,其對應的副本節點就會升級爲主節點,保證ElasticSearch的高可用

準備工作

這篇博客需要的所有安裝包,我都下載好並分享在https://pan.baidu.com/s/1a6w50_IROqJii3Wo_0SOlA,分享碼是: gmac

由於要保證ElasticSearch的高可用,搭建的ElasticSearch集羣至少需要2個節點。本篇文章使用的ElasticSearch集羣中會有3個節點,也就是需要在3個虛擬機中分別搭建ElasticSearch所需要的環境。

以下命令在3個空白的虛擬機中都需要執行一次,保證每個虛擬機都具備同樣的ElasticSearch環境

ps: 說個題外話,一不小心把之前的虛擬機數據刪除並清空回收站了。感覺自己沒帶腦子。。。因此下面的命令都是親測有效。

通過CentOS-7鏡像新建虛擬機

通過VM工作站中的新建虛擬機來創建我們需要的虛擬機。在安裝過程中,虛擬機的配置大部分採納提示所給的默認配置,這裏需要如下兩個地方自定義配置:

  1. 選用從我的網盤中下載的映像文件

選用映像文件

  1. 虛擬機命名以及選取虛擬機文件數據的存放位置

虛擬機命名

由於虛擬機的映像文件和文件數據較大,一般不放在C盤,防止系統文件所在磁盤空間不足,導致電腦性能降低。如可以把映像文件放在F:\linux\images,把虛擬機數據文件放在F:\linux\virtualmachines,這樣也方便後期對虛擬機數據的管理。

安裝完成後,虛擬機的默認硬件配置如下:

虛擬機默認硬件配置

也就是說這裏有一個名爲“elastic-1”的虛擬機,它的配置信息是:內存1G、CPU1個、硬盤20G、網絡適配器NAT模式。如果我們的工作環境不是經常變動(在家辦公與在公司辦公連接的局域網不一樣,無法使用“橋接模式”),我們可以考慮將網絡適配器選用“橋接模式”,這樣虛擬機的ip地址就不會經常變動。

開啓虛擬機後,還需要對虛擬機進行一些簡單的配置。這些配置會以視圖的方式進行提示:

配置提示

如:編輯DATE & TIME選擇所在時區;編輯LANGUAGE SUPPORT選擇支持語言;編輯INSTALLATION DESTINATION選擇磁盤位置及大小;編輯NETWORK & HOST NAME選擇可以聯網;編輯ROOT PASSWORD保存root用戶的登錄密碼。

點擊Begin Installation會進行虛擬機的初始化。需要虛擬機初始化完成後,點擊Reboot,等虛擬機重新啓動後,我們才能用剛剛保存的root用戶的密碼登錄虛擬機並進行操作。因此這個密碼一定要保證足夠簡單好記。

以root用戶登錄成功後,然後需要執行如下命令。

# 以centos7自帶工具yum安裝net-tools
yum install net-tools.x86_64

如下圖所示:

獲取本機ip

安裝net-tools工具後,通過ifconfig命令獲知該虛擬機的ip爲:192.168.139.148。此時我們可以在該虛擬機的命令行界面操作,也可以通過ssh協議連接並登錄虛擬機後,在Xshell的命令行界面操作。

# centos7自帶的yum工具版本可能較低,需要更新至最新的版本
yum update
# 安裝wget工具
yum install wget

JDK8環境配置

ElasticSearch官網明確指出,使用ElasticSearch需要jdk8環境。

JDK8壓縮包下載方式:

方式一:我們可以用wget --no-check-certificate --no-cookies --header "Cookie: oraclelicense=accept-securebackup-cookie" http://download.oracle.com/otn-pub/java/jdk/8u172-b11/a58eab1ec242421181065cdc37240b08/jdk-8u172-linux-x64.tar.gz命令直接在虛擬機中下載JDK8。

方式二:去oracle官網下載jdk-8u201-linux-x64.tar.gz預先在本地電腦下載JDK8。

這裏更推薦使用第二種方式。本地電腦中下載網速更穩定,並且我們可以留以備份(3個虛擬機中都需要安裝JDK8)。

# 新建存放JDK的目錄
mkdir -p /home/env/jdk
cd /home/env/jdk
# 這裏將在本地電腦下載好的jdk-8u161-linux-x64.tar.gz,通過WinSCP工具複製到當前目錄,複製成功後,解壓
tar -xzvf jdk-8u161-linux-x64.tar.gz

解壓成功後,當前目錄會多出一個目錄jdk1.8.0_161,如圖所示:

jdk8目錄

還需要執行如下命令,將jdk8加到$PATH中,保證ElasticSearch能找到。

# 安裝vim文本編輯器工具
yum install vim
# 編輯配置文件
vim /etc/profile
# 在該配置文件尾部追加如下內容
export JAVA_HOME=/home/env/jdk/jdk1.8.0_161
export JRE_HOME=/home/env/jdk/jdk1.8.0_161/jre
export CLASSPATH=.:$JAVA_HOME/lib:$JRE_HOME/lib:$CLASSPATH
export PATH=$PATH:$JAVA_HOME/bin
# 保存退出後,執行如下命令,使配置即時生效(默認重啓虛擬機才能生效)
source /etc/profile
# 通過如下命令驗證jdk環境變量是否配置成功
java -version

命令效果如下圖所示則說明jdk環境變量配置成功。

在這裏插入圖片描述

新建es用戶

elasticsearch的開發者認爲以root用戶使用elasticsearch服務不安全,因此這裏統一使用es用戶使用elasticsearch服務。

# 添加一個es用戶
useradd es
# 配置es的密碼,爲了方便也可以不配置密碼。
passwd es
# 通過該命令能查看es用戶的信息
id es

es用戶創建成功後,系統會自動在/home目錄下再創建一個同名目錄es,這個目錄就是es用戶的工作目錄,其所有者就是es。如下圖所示:

es工作目錄

ElasticSearch及其常用插件安裝

ElasticSearch安裝

方式一:我們可以通過wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-6.3.2.tar.gz下載。(6.3.2是版本號)

方式二:我們可以根據需要,到es官網的歷史版本記錄下載我們需要的版本。

這兩種方式沒有任何差異。我們必須要注意的是確定好elasticsearch的版本。elasticsearch的版本迭代非常快,版本之間差異較大。現在最新elasticsearch穩定版本是6.6.1,然而網上大部分的博客教程等資料都是基於5.x甚至是2.x的版本。本篇博客選用的版本是6.3.2

# 安裝zip/unzip工具
yum install -y unzip zip
# 切換當前用戶爲es
su es
# 新建存放elasticsearch的目錄
mkdir -p /home/es/elasticsearch
cd /home/es/elasticsearch
# 可以通過wget命令下載,也可以先在本地電腦下載好通過WinSCP將elasticsearch壓縮包傳輸到當前目錄
# 解壓該壓縮包
unzip elasticsearch-6.3.2.zip
# 通過es用戶執行./elasticsearch-6.3.2/bin/elasticsearch,即可啓動elasticsearch服務
./elasticsearch-6.3.2/bin/elasticsearch
# 通過curl命令能夠查看elasticsearch服務是否已經啓動
curl -XGET "localhost:9200"

/elasticsearch-6.3.2的目錄結構如下:

elasticsearch目錄結構

看這些目錄的名稱很容易就知道目錄的作用。這裏需要着重關注的是configlogsplugins

當需要修改elasticsearch的配置時,主要是修改配置文件config/elasticsearch.ymlconfig/jvm.options

當elasticsearch服務報錯,我們需要進行錯誤排查時,需要查看日誌文件logs/elasticsearch.log

elasticsearch還提供了一些比較好用的插件,我們可以在github下載插件源碼,存放到plugins後重啓elasticsearch服務即可。

通過執行bin/elasticsearch命令,我們可以查看elasticsearch啓動日誌如下圖所示:

elasticsearch啓動日誌

elasticsearch啓動日誌顯示:elasticsearch服務已經成功啓動。

通過curl -XGET 'localhost:9200'命令的執行結果也表明:elasticsearch服務已經成功啓動。

elasticsearch的基本信息

但是日誌裏面有一些警告信息不能忽略。

  1. -Xms1g, -Xmx1g顯示:elasticsearch默認指定的堆內存爲1G。前面說過該虛擬機的硬件配置總內存只有1G。把所有的內存全分配給elasticsearch肯定是不可取的。
  2. max file descriptors [4096] for elasticsearch process is too low, increase to at least [65536]max number of threads [3851] for user [es] is too low, increase to at least [4096]max virtual memory areas vm.max_map_count [65530] is too low, increase to at least [262144]等警告日誌信息表明:elasticsearch進程的文件解釋器不夠,分配給es用戶的最大線程數也不夠,當前es用戶擁有的內存權限也不夠。

具體解決辦法如下:

# 修改jvm配置
vim /home/es/elasticsearch/elasticsearch-6.3.2/config/jvm.options
# 修改如下堆內存配置信息
-Xms512m
-Xmx512m
# 以下配置只能由root用戶進行修改。需要切換當前用戶爲root
su root
# 編輯安全限制配置文件
vim /etc/security/limits.conf
# 並添加內容如下:
* soft nofile 65536
* hard nofile 131072
* soft nproc 2048
* hard nproc 4096
# 編輯內存限制配置文件
vim /etc/sysctl.conf
# 並添加內容如下:
vm.max_map_count=262144
# 保存退出後執行如下命令
sysctl -p

重啓elasticsearch服務,無上述警告日誌信息,則配置生效。

head插件

從elasticsearch更新到6.*版本後,不再支持以elasticsearch插件的形式安裝head插件。也就是下載的head插件不能放在/elasticsearch/elasticsearch-6.3.2/plugins目錄。我選擇把其放在/elasticseach的目錄下。具體安裝如下:

# 切換當前用戶爲root
su root
# 安裝git
yum install git
cd /home/es/elasticsearch
# 將head插件的源碼克隆到當前目錄
git clone git://github.com/mobz/elasticsearch-head.git

克隆成功後,目錄結構如下圖所示:

克隆head插件

# 配置node環境
mkdir -p /home/env/node
cd /home/env/node
# 將從node官網下載最新版本的node壓縮包傳輸到該目錄中,然後解壓
tar -xvf node-v10.15.3-linux-x64.tar.xz
# 將node加入環境變量
vim /etc/profile
# 添加內容如下:
export NODE_HOME=/home/env/node/node-v10.15.3-linux-x64
export PATH=$PATH:$NODE_HOME/bin/
export NODE_PATH=$NODE_HOME/lib/node_modules
# 使配置即時生效
source /etc/profile
# 測試node是否已經添加到環境變量中
node -v
npm -v

如下圖所示則說明node環境配置完成:

node環境配置

# 切換工作目錄
cd /home/es/elasticsearch/elasticsearch-head/
# 在當前目錄下安裝構建工具grunt
npm install -g grunt-cli --registry=https://registry.npm.taobao.org
# 獲取head需要的依賴包
npm install --registry=https://registry.npm.taobao.org

安裝依賴包會報錯如下:

install報錯

查詢資料可知:由於權限問題,當前用戶爲root。而實際管理elasticsearch服務的用戶爲es。

# 將head目錄的所有者改爲es
cd /home/es/elasticsearch/
chown -R es:es elasticsearch-head/
cd elasticsearch-head/
# 切換當前用戶爲es
su es
# 再安裝依賴包,由於此依賴包過大,選擇從阿里雲鏡像拉取依賴包,能明顯加快依賴包的下載速度
npm install --registry=https://registry.npm.taobao.org
# 添加head相關的配置文件如下:
cd /home/es/elasticsearch/elasticsearch-6.3.2/config/
vim elasticsearch.yml
# 自定義配置如下:
#
# 集羣名稱
cluster.name: elasticsearch
# 當前節點名稱
node.name: node-1
# 設置任何人都能夠訪問
network.host: 0.0.0.0
# 默認就是該配置,設置http端口爲9200
http.port: 9200 
# 集羣發現
#集羣節點ip或者主機(這裏的數組元素就是該集羣中各節點的ip)
discovery.zen.ping.unicast.hosts: ["192.168.139.149", "192.168.139.150","192.168.139.151"]     
#設置這個參數來保證集羣中的節點可以知道其它N個有master資格的節點。默認爲1。這裏不宜設置過大。測試表示這裏設置爲2時,只有當前集羣中活躍的節點不小於2個,head插件才能監測集羣的狀態(否則連接不到集羣)。
discovery.zen.minimum_master_nodes: 2
# 提供向外交互的tcp端口
transport.tcp.port: 9300
# head插件相關的配置
http.cors.enabled: true
http.cors.allow-origin: "*"

配置完成後,執行如下命令:

# 關閉當前虛擬機的防火牆
su root
systemctl stop firewalld
systemctl disable firewalld
# 切換當前用戶爲es
su es
cd /home/es/elasticsearch/
# 後臺啓動elasticsearch服務
./elasticsearch-6.3.2/bin/elasticsearch -d
cd elasticsearch-head/
# 啓動head插件
grunt server

在本地電腦瀏覽器中訪問該虛擬機的9100端口,出現如下圖所示:

head插件

則說明當前單節點elasticsearch服務後臺啓動,且head插件已經安裝成功。

端口解釋:9200端口是elasticsearch集羣提供訪問的http端口。9300端口是elasticsearch集羣提供訪問的tcp端口。只要集羣處於正常狀態,以相應協議連接集羣中任何一個存活節點的對應端口即可使用該集羣提供的服務。9100端口是head插件提供服務的http端口。

其他節點的配置基本完全一致。必須要確保的是cluster.name要一致,且node.name要不一樣。

這裏的集羣名稱是elasticsearch,節點名稱是node-1。因此需要把另外兩個節點定義不同的名稱,這裏就是簡單地定義爲:node-2node-3

VMWare Workstation中,我們只需要完整地配置一個虛擬機即可。另外兩個虛擬機可以利用其克隆的功能,直接配置好兩個虛擬機。此時只需要修改另外兩個虛擬機的elasticsearch.yml文件的node.name即可。

啓動3個節點,預先在命令行工具中使用curl命令放入一些文檔到該集羣中,文檔信息如下:

curl -XPUT 'localhost:9200/twitter/_doc/1?pretty' -H 'Content-Type: application/json' -d '
{
    "user": "kimchy",
    "post_date": "2009-11-15T13:12:00",
    "message": "Trying out Elasticsearch, so far so good?"
}'

curl -XPUT 'localhost:9200/twitter/_doc/2?pretty' -H 'Content-Type: application/json' -d '
{
    "user": "kimchy",
    "post_date": "2009-11-15T14:12:12",
    "message": "Another tweet, will it be indexed?"
}'

curl -XPUT 'localhost:9200/twitter/_doc/3?pretty' -H 'Content-Type: application/json' -d '
{
    "user": "elastic",
    "post_date": "2010-01-15T01:46:38",
    "message": "Building the site, should be kewl"
}'

從head插件查看集羣信息如下:

集羣狀態-1

我們這裏放了3個文檔到該集羣中,從“集羣預覽”中可以看出,該機器有10分片(5主5複製)並且主分片的複製分片必定是別的節點之上。

當通過kill -9 $pid停掉node-2節點的elasticsearch後,集羣狀態立馬變成如下:

集羣狀態-2

過一段時間之後,集羣狀態又自動變成如下:

集羣狀態-3

我們可以通過多次重啓某個節點的elasticsearch服務,會發現:節點的分配規則是elasticsearch幫我們訂好了的。只要保證當前集羣至少有兩個節點,elasticsearch中都會保證每個索引有10分片(5主5從),並且主從分片不在一個節點之上。
這種做法,使得我們不需要關注elasticsearch集羣中某一個節點宕機是否會導致丟失數據。實際只要當前集羣至少有兩個節點,一定能夠保證該集羣的數據安全即elasticsearch的高可用

我們也可以通過nohup grunt server &命令,將grunt構建變爲Linux系統的一個服務。這樣關閉了當前命令行工具,只有虛擬機不關機,我們就可以通過9100端口使用該虛擬機的head可視化插件了。當要關閉服務時,可以通過ps -ef | grep grunt以及ps -ef | grep elastic查到對應服務的進程號後,通過kill -9 $pid的方式殺死對應進程即可停掉服務。

中文ik分詞器

討論分詞器之前,我們先要知道ElasticSearch具體是如何實現全文檢索功能的呢?

我先說一說自己的見解,這樣應該對如何選用合適的分詞器有一定的幫助。

ElasticSearch的全文檢索,可以簡單分爲兩項:存儲優化分詞和檢索優化分詞。
存儲優化分詞,就是將ElasticSearch文檔中字段值按照一定的切分規則分爲多個待匹配靶點,只有搜索關鍵詞命中其中的靶點纔會將該字段值對應的文檔放在返回結果中。

檢索優化分詞,就是將用戶輸入的搜索關鍵詞按照一定的切分規則分爲多個靶點,然後根據這些靶點去匹配ElasticSearch中的所有待匹配靶點。根據這些靶點的匹配程度,ElasticSearch會計算得分,根據匹配度即得分情況從高到底返回搜索結果。如果一個靶點都沒有匹配上,得分爲0即不返回任何結果。

具體的思路如下圖所示:

全文檢索流程圖

選用不同的分詞器,就是選用不同的分詞規則。然後我們先試試elasticsearch默認分詞器的效果怎麼樣,我們就知道爲什麼需要安裝ik分詞器了。

我們再在命令行工具中通過curl命令放入一條測試數據,並測試分詞效果的命令如下:

# 放入中文內容
curl -XPUT 'localhost:9200/twitter/_doc/4?pretty' -H 'Content-Type: application/json' -d '
{
    "user": "測試",
    "post_date": "2019-03-07T10:31:00",
    "message": "中華人民共和國國歌"
}'

# 測試默認中文分詞器效果
curl -XGET 'localhost:9200/twitter/_doc/4/_termvectors?fields=message&pretty'
# 測試默認英文分詞器效果
curl -XGET 'localhost:9200/twitter/_doc/1/_termvectors?fields=message&pretty'

分詞器效果如下圖所示:

默認分詞效果圖

從這裏的分詞效果來看:所有的英文以及中文單詞都分成了單個。對於英文單詞這樣劃分,我們可以接受,但中文的分詞效果我們實在難以忍受。這裏我們就考慮到要採用中文ik分詞器。

IK分詞器的版本必須與ES的版本相對應,具體詳情參見IK分詞器README。本篇博客的ES版本爲6.3.2,因此我們需要到ik分詞器版本記錄中去下載該版本的ik分詞器壓縮包。

su root
cd /home/es/elasticsearch/elasticsearch-6.3.2/plugins
# ik分詞器應該放在plugins目錄下,這裏爲了方便管理,將其放在plugins/ik下面
mkdir ik
cd ik
# 將下載好的ik壓縮包傳輸到該目錄後,解壓
unzip elasticsearch-analysis-ik-6.3.2.zip
# 將文件所有者改爲es
cd /home/es/elasticsearch/elasticsearch-6.3.2/plugins
chown -R es:es ik/
# 重新啓動elaticsearch服務
ps -ef | grep grunt
kill -9 $pid
ps -ef | grep elastic
kill -9 $pid
su es
cd /home/es/elasticsearch/
./elasticsearch-6.3.2/bin/elasticsearch -d
cd elasticsearch-head/
nohup grunt server &

要保證elasticsearch集羣的中文分詞能達到預期效果,所有的節點都必須要安裝ik分詞器並重啓elasticsearch服務。如果該集羣中某一個存活狀態的節點沒有安裝ik分詞器,可能會報錯如下:analyzer [ik_max_word] not found for field[]。同時要注意,由於elasticsearch的索引中具體字段一旦存放了文檔,就不能再更改字段的結構(即無法通過_mapping更改字段的分詞規則)。

# 添加索引index
curl -XPUT "localhost:9200/index"

#通過_mapping設置字段content的分詞規則爲細粒度分詞,字段message的分詞規則爲粗粒度分詞
curl -XPOST "localhost:9200/index/text/_mapping" -H 'Content-Type:application/json' -d'
{
    "properties": {
        "content": {
            "type": "text",
            "analyzer": "ik_max_word",
            "search_analyzer": "ik_max_word"
        },
        "message": {
            "type": "text",
            "analyzer": "ik_smart",
            "search_analyzer": "ik_smart"
        }
    }

}'

# 添加一條測試數據
curl -XPOST "localhost:9200/index/text/1" -H 'Content-Type:application/json' -d'
{
    "content":"中華人民共和國國歌",
    "message":"中華人民共和國國歌"
}
'
# 查看ik_max_word(細粒度)的分詞效果
curl -XGET "localhost:9200/index/text/1/_termvectors?fields=content&pretty"
# 查看ik_smart(粗粒度)的分詞效果
curl -XGET "localhost:9200/index/text/1/_termvectors?fields=message&pretty"

實際的分詞效果圖如下:

ik分詞效果圖

以"中華人民共和國國歌"爲例,ik分詞的兩種分詞規則的效果如下:

  • ik_max_word分詞效果:“中華”,“中華人民”,“中華人民共和國”,“人民”,“人民共和國”,“共和”,“共和國”,“華人”,“國”,“國歌”。
  • ik_smart分詞效果:“中華人民共和國”,“國歌”。

通過_mapping設置某個字段的分詞規則的命令解釋,以設置字段content的分詞規則語法爲例:

"content": {
    "type": "text",
    "analyzer": "ik_max_word",
    "search_analyzer": "ik_max_word"
}
# content 就是對應的字段名稱
# type 的值常用備選項有:text/keyword/integer/float/array/boolean/date等等。text表示該字段會分詞即倒排索引;keyword表示該字段不分詞(比如郵箱、郵政編碼等信息分詞沒有意義,就需要設置爲keyword)。
# analyzer 的值表示存儲的數據用哪種規則進行分詞。
# search_analyzer 的值表示我們輸入的搜索關鍵詞用哪種規則進行分詞。

# 只有輸入關鍵詞的分詞備選項,與最終保存的分詞結果索引相匹配,才能檢索到。
# 以"中華人民共和國國歌"的`ik_smart`分詞效果(message):"中華人民共和國","國歌"爲例。我們搜索關鍵詞q="中華人民"是查不到這條記錄的,我們可以通過以下命令進行測試驗證。
curl -XGET 'localhost:9200/index/text/_search' -H 'Content-Type:application/json' -d'
{
    "query": {
        "bool": {
            "must": [
                {
                    "term": {
                        "message": "中華人民"
                    }
                }
            ]
        }
    }
}
'
# 這樣是找不到對應的記錄的。如果把"message"改爲"content",就可以查到結果的,具體原因參考上面的解釋。

分詞規則對比測試效果如下:

分詞規則對比效果圖

拼音分詞器

我們在淘寶和京東商城可以通過拼音搜到對應的商品,同樣ElasticSearch也有對應的拼音分詞器來完成檢索功能。

與IK分詞器一樣,拼音分詞器的版本必須與ES的版本相對應,具體詳情參見拼音分詞器README。我們需要到拼音分詞器版本記錄中去下載對應版本的拼音分詞器壓縮包。安裝過程也類似,同樣所有的節點都要安裝拼音分詞器。命令如下:

su root
cd /home/es/elasticsearch/elasticsearch-6.3.2/plugins
# 拼音分詞器應該放在plugins目錄下,這裏爲了方便管理,將其放在plugins/pinyin下面
mkdir pinyin
cd pinyin
# 將下載好的拼音分詞器壓縮包傳輸到該目錄後,解壓
unzip elasticsearch-analysis-pinyin-6.3.2.zip
# 將文件所有者改爲es
cd /home/es/elasticsearch/elasticsearch-6.3.2/plugins
chown -R es:es pinyin/
# 重新啓動elaticsearch服務
# ... ... 具體命令參考上面ik分詞器的啓用

安裝拼音分詞器,併成功啓動集羣后,測試拼音分詞器效果的命令如下:

# 爲索引test1創建一個自定義規則的拼音分詞器pinyin_analyzer,具體規則就是my_pinyin中定義的內容。這些字段的含義可參考elasticsearch-analysis-pinyin中REAMDE說明
curl -XPUT "localhost:9200/test1" -H 'Content-Type:application/json' -d'
{
    "index" : {
        "analysis" : {
            "analyzer" : {
                "pinyin_analyzer" : {
                    "tokenizer" : "my_pinyin"
                    }
            },
            "tokenizer" : {
                "my_pinyin" : {
                    "type" : "pinyin",
                    "keep_separate_first_letter" : false,
                    "keep_full_pinyin" : true,
                    "keep_original" : true,
                    "limit_first_letter_length" : 16,
                    "lowercase" : true,
                    "remove_duplicated_term" : true
                }
            }
        }
    }
}
'

# 測試這個自定義分詞器pinyin_analyzer的分詞效果命令如下:
curl -XGET "localhost:9200/test1/_analyze?pretty" -H 'Content-Type:application/json' -d'
{
    "text": ["劉德華"],
  "analyzer": "pinyin_analyzer"
}
'

該自定義拼音分詞器的分詞效果圖如下:

拼音分詞器效果圖

也就是說,經過該分詞器分詞後,通過"liu",“de”,“hua”,“ldh”,"劉德華"能檢索到這條記錄。

繁體字分詞器

我們還可能遇到這個問題:就是搜索或者存儲的時候,我們用的是繁體字。但搜索結果並不完美。簡體字只能搜索到簡體字,繁體字只能搜索到繁體字。這裏我們希望能夠返回所有的結果,就需要使用繁體字分詞器。

同樣地與IK分詞器一樣,繁體字分詞器的版本必須與ES的版本相對應,具體詳情參見繁體字分詞器README。我們需要到繁體字分詞器版本記錄中去下載對應版本的繁體字分詞器壓縮包。安裝過程也類似,同樣所有的節點都要安裝繁體字分詞器。命令如下:

su root
cd /home/es/elasticsearch/elasticsearch-6.3.2/plugins
# 繁體字分詞器應該放在plugins目錄下,這裏爲了方便管理,將其放在plugins/stconvert下面
mkdir stconvert
cd stconvert
# 將下載好的繁體字分詞器壓縮包傳輸到該目錄後,解壓
unzip elasticsearch-analysis-stconvert-6.3.2.zip
# 將文件所有者改爲es
cd /home/es/elasticsearch/elasticsearch-6.3.2/plugins
chown -R es:es stconvert/
# 重新啓動elaticsearch服務
# ... ... 具體命令參考上面ik分詞器的啓用

安裝繁體字分詞器,併成功啓動集羣后,測試繁體字分詞器效果的命令如下:


# 爲索引test2創建一個自定義分詞器(將繁體字轉爲簡體字),默認是s2t(Simple Chinese To Tradional Chinese)即簡體字轉繁體字
curl -XPUT "localhost:9200/test2" -H 'Content-Type:application/json' -d'
{
    "index" : {
        "analysis" : {
            "analyzer" : {
                "tsconvert" : {
                    "tokenizer" : "tsconvert"
                    }
            },
            "tokenizer" : {
                "tsconvert" : {
                    "type" : "stconvert",
                    "delimiter" : "#",
                    "keep_both" : false,
                    "convert_type" : "t2s"
                }
            },   
             "filter": {
               "tsconvert" : {
                     "type" : "stconvert",
                     "delimiter" : "#",
                     "keep_both" : false,
                     "convert_type" : "t2s"
                 }
             },
            "char_filter" : {
                "tsconvert" : {
                    "type" : "stconvert",
                    "convert_type" : "t2s"
                }
            }
        }
    }
}
'

# 我們應該關注的是字段convert_type的值,備選項只有:s2t和t2s。默認值是s2t。
# 測試分詞效果的命令如下:
curl -XGET "localhost:9200/test2/_analyze" -H "Content-Type:Application/json" -d '
{
    "tokenizer" : "keyword",
    "filter" : ["lowercase"],
    "char_filter" : ["tsconvert"],
    "text" : "國際國際"
}
'

繁體字轉爲簡體字的測試效果圖如下:

繁體字分詞器

上面提到的IK分詞器、拼音分詞器、繁體字分詞器,我們目前都是隻使用其中一個分詞器,我們如何組合使用它們呢?也就是說,我們如何通過組合這些分詞器來自定義分詞規則呢?

即我們需要滿足如下需求:

  1. 搜索關鍵詞爲拼音時,能搜索到相關的對應中文文檔;
  2. 搜索關鍵詞爲簡體字時,能搜索到相關的簡體或繁體文檔;
  3. 搜索關鍵詞爲繁體字時,也能搜索到相關的簡體或繁體文檔。

先給出一個解決方案的案例如下:

# 設置索引goods的settings(該命令會同時創建該索引,如果執行命令之前該索引已創建會報錯,因爲elasticsearch不支持動態修改主分片的settings)
curl -XPUT "localhost:9200/goods" -H "Content-Type:application/json" -d '
{
    "settings": {
        "index": {
            "analysis": {
                "filter": {
                    "myEdgeNgramFilter": {
                    "type": "edge_ngram",
                    "min_gram": 1,
                    "max_gram": 50
                    },
                    "myPinyinFilter": {
                    "type": "pinyin",
                    "first_letter": "prefix",
                    "padding_char": " ",
                    "limit_first_letter_length": 50,
                    "lowercase": true
                    }
                },
                "char_filter": {
                    "tsconvert": {
                    "type": "stconvert",
                    "convert_type": "t2s"
                    }
                },
                "analyzer": {
                    "myIkAnalyzer": {
                        "type": "custom",
                        "tokenizer": "ik_max_word",
                        "char_filter": [
                            "tsconvert"
                        ]
                    },
                    "myPinyinAnalyzer": {
                        "tokenizer": "keyword",
                        "filter": [
                            "myEdgeNgramFilter",
                            "myPinyinFilter",
                            "lowercase"
                        ]
                    }
                }
            }
        }
    }
}
'

# 設置索引goods的mappings(此時該索引中不能有文檔信息。因爲當添加文檔時如果字段沒有設置分詞規則,elasticsearch會爲這些字段設置默認的分詞規則,同時elasticsearch又不支持動態修改字段的分詞規則)
curl -XPUT "localhost:9200/goods/goodsInfo/_mapping" -H "Content-Type:application/json" -d '
{
    "properties": {
        "id": {
            "type": "integer"
        },
        "name": {
            "type": "text",
            "analyzer": "myIkAnalyzer",
            "search_analyzer": "myIkAnalyzer",
            "fields": {
                "pinyin": {
                    "type": "text",
                    "analyzer": "myPinyinAnalyzer",
                    "search_analyzer": "myPinyinAnalyzer"
                }
            }
        },
        "brand": {
            "type": "text",
            "index": "true",
            "analyzer": "myIkAnalyzer"
        },
        "date": {
            "type": "date",
            "format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis"
        }
    }
}
'

# 添加測試數據
curl -XPOST "localhost:9200/goods/goodsInfo/1" -H "Content-Type:application/json" -d '
{
    "id": 1,
    "name": "智能機器人",
    "brand": "小米",
    "date": "2019-03-08"
}'
curl -XPOST "localhost:9200/goods/goodsInfo/2" -H "Content-Type:application/json" -d '
{
    "id": 2,
    "name": "智能牙刷",
    "brand": "小米",
    "date": "2019-03-08"
}'
curl -XPOST "localhost:9200/goods/goodsInfo/3" -H "Content-Type:application/json" -d '
{
    "id": 3,
    "name": "測試機器",
    "brand": "測試",
    "date": "2019-03-08"
}'
curl -XPOST "localhost:9200/goods/goodsInfo/4" -H "Content-Type:application/json" -d '
{
    "id": 4,
    "name": "測試電腦",
    "brand": "測試",
    "date": "2019-03-08"
}'
curl -XPOST "localhost:9200/goods/goodsInfo/5" -H "Content-Type:application/json" -d '
{
    "id": 5,
    "name": "杯子",
    "brand": "測試",
    "date": "2019-03-08"
}'


# 測試查詢的命令如下:($keywords爲我們輸入的搜索關鍵詞,我們可以多次改變搜索關鍵詞看檢索結果)
curl -XGET 'localhost:9200/goods/goodsInfo/_search' -H 'Content-Type:application/json' -d '
{
    "query": {
        "bool": {
            "should": [
                {
                    "match": {
                        "name.pinyin": "$keywords"
                    }
                },
                {
                    "multi_match": {
                        "query": "$keywords",
                        "fields": ["name^3", "brand"]
                    }
                }
                
            ] 
        }
    }
}'

keywords = ceshikeywords = 測試的搜索效果圖如下:

自定義分詞規則檢索結果

案例說明:

  • settings中預先爲索引goods定義了兩個分詞器myIkAnalyzermyPinyinAnalyzermyIkAnalyzer的分詞規則是:先將字段值全部轉化爲簡體中文,然後以最細粒度ik_max_word進行分詞;myPinyinAnalyzer的分詞規則是:先將字段的拼音值進行補全edge_ngram,然後將拼音都轉化爲小寫並參照拼音分詞器pinyin的規則進行分詞。
  • mappings中爲索引goods中字段name指定了分詞規則myIkAnalyzername.pinyin指定了分詞規則myPinyinAnalyzer;爲字段brand指定了分詞規則myIkAnalyzer
  • 按用戶輸入的搜索關鍵詞keywords去檢索時,會分別按字段brandname和name.pinyin的分詞規則去匹配結果。同時爲字段name設置了權重3,這樣檢索返回結果排序主要依賴這個字段的匹配程度。

Springboot整合ElasticSearch

  1. 在項目pom文件中導入依賴包並指定elasticsearch的版本,SpringBoot和ElasticSearch可能會有版本衝突問題,具體SpingBoot和ElasticSearch版本選用請參考spring-data-elasticsearch源碼README
    如:由於我的elasticsearch版本爲6.3.2,因此我的spring-data-elasticsearch版本爲3.1.5
 <?xml version="1.0" encoding="UTF-8"?>
 <project>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.3.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <properties>
        <java.version>1.8</java.version>
        <elasticsearch.version>6.3.2</elasticsearch.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
        </dependency>
    </dependencies>   
 </project>
  1. 解決rediselasticsearch自動配置導致的netty版本衝突,定義如下類:
@Slf4j
@Component
public class ElasticSearchConfig implements InitializingBean {

    static {
        System.setProperty("es.set.netty.runtime.available.processors", "false");
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        log.info("解決由於netty版本衝突導致項目無法啓動");
        log.info("設置es.set.netty.runtime.available.processors的值爲:[{}]",System.getProperty("es.set.netty.runtime.available.processors"));
    }
}

  1. application.yml配置文章中必須要指定elasticsearch集羣以及當前存活的任意一個節點的9300端口
spring:
    data:
        elasticsearch:
            cluster-name: elasticsearch
            cluster-nodes: 192.168.139.149:9300
  1. 在實體類Goods中定義索引、類型以及字段的分詞規則。如果只是簡單地使用某一個分詞器或使用默認分詞器,可以用spring-data-elasticsearch提供的@Field註解來定義字段的分詞規則。如果分詞規則需要自定義,則可使用@Setting@Mapping來自定義分詞器以及具體字段的分詞規則。
@Getter
@Setter
@ToString
@Document(indexName = "goods", type = "goodsInfo")
@Setting(settingPath = "json/goods_setting.json")
@Mapping(mappingPath = "json/goods_mapping.json")
public class Goods {

    @Id
    private Integer id;
//    @Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_max_word")
    private String name;
    private String brand;
    private Date date;

}

Goods實體類中指定的goods_setting.json如下:

{
    "index": {
        "analysis": {
            "filter": {
                "myEdgeNgramFilter": {
                    "type": "edge_ngram",
                    "min_gram": 1,
                    "max_gram": 50
                },
                "myPinyinFilter": {
                    "type": "pinyin",
                    "first_letter": "prefix",
                    "padding_char": " ",
                    "limit_first_letter_length": 50,
                    "lowercase": true
                }
            },
            "char_filter": {
                "tsconvert": {
                    "type": "stconvert",
                    "convert_type": "t2s"
                }
            },
            "analyzer": {
                "myIkAnalyzer": {
                    "type": "custom",
                    "tokenizer": "ik_max_word",
                    "char_filter": [
                        "tsconvert"
                    ]
                },
                "myPinyinAnalyzer": {
                    "tokenizer": "keyword",
                    "filter": [
                        "myEdgeNgramFilter",
                        "myPinyinFilter",
                        "lowercase"
                    ]
                }
            }
        }
    }
}

Goods實體類中指定的goods_mapping.json如下:

{
    "goodsInfo": {
        "id": {
            "type": "integer"
        },
        "name": {
            "type": "text",
            "analyzer": "myIkAnalyzer",
            "search_analyzer": "myIkAnalyzer",
            "fields": {
                "pinyin": {
                    "type": "text",
                    "analyzer": "myPinyinAnalyzer",
                    "search_analyzer": "myPinyinAnalyzer"
                }
            }
        },
        "brand": {
            "type": "text",
            "index": "true",
            "analyzer": "myIkAnalyzer"
        },
        "date": {
            "type": "date",
            "format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis"
        }
    }
}
  1. 定義Goods的數據訪問接口GoodsRepository,由於該接口繼承了ElasticsearchRepository,我們可以使用ElasticsearchRepository默認提供的很多方法,也可以根據自己的需求來自定義方法。
@Repository
public interface GoodsRepository extends ElasticsearchRepository<Goods, Integer> {
}
  1. 測試類ElasticSearchTest如下:
/**
 * @author zhenye 2019/3/11
 */
@RunWith(SpringRunner.class)
@SpringBootTest
@Slf4j
public class ElasticSearchTest {

    @Autowired
    private ElasticsearchTemplate template;
    @Autowired
    private GoodsRepository goodsRepository;

    @Test
    public void addIndexTest() {
        // 創建索引(json/goods_setting.json中的設置開始生效)
        template.createIndex(Goods.class);
        // 設置索引對應字段的分詞規則(json/goods_mapping.json中的設置開始生效)
        template.putMapping(Goods.class);
    }

    @Test
    public void deleteIndexTest () {
        // 刪除索引
        template.deleteIndex(Goods.class);
    }

    @Test
    public void addDataTest() {
        // 添加一些測試數據
        List<Goods> goodsList = new ArrayList<>();
        Goods goods1 = new Goods(1,"智能機器人","小米", new Date());
        Goods goods2 = new Goods(2,"智能牙刷","小米", new Date());
        Goods goods3 = new Goods(3,"測試機器","測試", new Date());
        Goods goods4 = new Goods(4,"測試電腦","測試", new Date());
        Goods goods5 = new Goods(5,"杯子","測試", new Date());
        goodsList.add(goods1);
        goodsList.add(goods2);
        goodsList.add(goods3);
        goodsList.add(goods4);
        goodsList.add(goods5);
        goodsRepository.saveAll(goodsList);
    }

    @Test
    public void simpleSearchTest() {
        // 進行一些簡單搜索的測試
        Iterable<Goods> allGoods = goodsRepository.findAll();
        allGoods.forEach(goods -> log.info(goods.toString()));

        String name = "測試";
        /*
         * 這裏的`findByName`我們不能簡單得理解爲“精確搜索”或“模糊搜索”。
         * 返回的結果依賴於存儲和搜索時各自採用的分詞規則。
         * 根據goods_mapping.json中name的設置可知,存儲和檢索都是ik_max_word。
         * 因此,這裏`findByName("測試")`,就是找  字段值的分詞後有"測試"  的文檔。
         */
        List<Goods> goodsByName = goodsRepository.findByName(name);
        log.info(goodsByName.toString());
    }

    @Test
    public void complexSearchTest() {
        // ElasticsearchRepository接口中,還有一個search()方法,允許我們靈活組裝搜索條件
        /*
         * 以下面一個需求爲例:
         * 1. 檢索的goods中的name和brand字段,排序結果主要依賴name的匹配度
         * 2. 匹配brand時,支持簡體/繁體關鍵字匹配
         * 3. 匹配name時,支持簡體/繁體/中文拼音等關鍵字匹配
         */

//        String keywords = "";
//        String keywords = "測試哈哈哈 啦啦啦";
        String keywords = "ceshi";
//        String keywords = "測試";
        DisMaxQueryBuilder disMaxQueryBuilder = QueryBuilders.disMaxQuery();
        QueryBuilder queryBuilder1 = QueryBuilders.matchQuery("name", keywords).boost(2f);
        QueryBuilder queryBuilder2 = QueryBuilders.matchQuery("name.pinyin", keywords).boost(0.5f);
        disMaxQueryBuilder.add(queryBuilder1);
        disMaxQueryBuilder.add(queryBuilder2);
        SearchQuery searchQuery = new NativeSearchQuery(disMaxQueryBuilder);
        Page<Goods> goodsPage = goodsRepository.search(searchQuery);
        System.out.println("keywords = ["+ keywords + "]的檢索結果爲:" + goodsPage.getContent());
    }
}

複雜搜索的效果圖如下:

複雜搜索效果圖

注意事項

  1. 由於ElasticSearch的版本迭代分詞快,版本之間差異較大。因此確定好使用的ElasticSearch版本後,其插件(如分詞器)和SpringBoot都應該與其對應。
  2. 在服務器上是無法以root用戶使用ElasticSearch服務的。以es用戶啓動服務時,需要將ElasticSearch及其插件所在目錄的所有人也改爲es,否則項目無法啓動。
  3. 以集羣方式啓動ElasticSearch服務,並設置了最小主節點數discovery.zen.minimum_master_nodes: 2時,必須要保證當前存活的節點大於該設置值,否則head插件無法訪問該集羣。
  4. 如果web項目中同時會使用rediselasticsearch,使用springboot的自動配置可能會報錯nested exception is java.lang.IllegalStateException: availableProcessors is already set to [4], rejecting [4]。這實際上是使用的netty版本衝突,我們需要在項目啓動前加入如下代碼進行配置System.setProperty("es.set.netty.runtime.available.processors", "false");.
  5. 我們使用GoodsRepository的自定義方法進行檢索數據時,裏面的過濾條件By...以及By...Like等關鍵字結果可能不太符合預期效果。因爲ElasticSearch爲了提高檢索效率,對字段存儲值以及搜索關鍵詞進行了分詞,具體的查詢效果是依賴於存儲值以及搜索關鍵詞的分詞效果的。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章