1. 程式人生 > 其它 >ElasticSearch-全文檢索

ElasticSearch-全文檢索

ElasticSearch-全文檢索

簡介

https://www.elastic.co/cn/what-is/elasticsearch
全文搜尋屬於最常見的需求,開源的 Elasticsearch 是目前全文搜尋引擎的首選。
它可以快速地儲存、搜尋和分析海量資料。維基百科、StackOverflow、Github 都採用它

Elastic 的底層是開源庫 Lucene。但是,你沒法直接用 Lucene,必須自己寫程式碼去呼叫它的
介面。Elastic 是Lucene的封裝,提供了 RESTAPI的操作介面,開箱即用。
RESTAPI:天然的跨平臺。
官方文件:https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html


官方中文:https://www.elastic.co/guide/cn/elasticsearch/guide/current/foreword_id.html
社群中文:
https://es.xiaoleilu.com/index.html
http://doc.codingdict.com/elasticsearch/0/

一、基本概念

1 、Index(索引)

動詞,相當於MySQL中的insert;
名詞,相當於MySQL中的Database

2 、Type(型別)

在 Index(索引)中,可以定義一個或多個型別。
類似於MySQL中的Table;每一種型別的資料放在一起;

3 、Document(文件)

儲存在某個索引(Index)下,某種型別(Type)的一個數據(Document),文件是JSON格
式的,Document就像是MySQL中的某個Table裡面的內容;

4 、倒排索引機制

二、Docker 安裝 Es

1、下載映象檔案

docker pull elasticsearch:7.4.2 儲存和檢索資料
docker pull kibana:7.4.2 視覺化檢索資料

2、建立例項

1、ElasticSearch

mkdir -p /mydata/elasticsearch/config
mkdir -p /mydata/elasticsearch/data
echo "http.host:0.0.0.0 " >> /mydata/elasticsearch/config/elasticsearch.yml

chmod -R 777 /mydata/elasticsearch/ 保證許可權
docker run --name elasticsearch -p 9200:9200 -p 9300:9300
-e "discovery.type=single-node"
-e ES_JAVA_OPTS="-Xms64m -Xmx512m"
-v /mydata/elasticsearch/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml
-v /mydata/elasticsearch/data:/usr/share/elasticsearch/data
-v /mydata/elasticsearch/plugins:/usr/share/elasticsearch/plugins
-d elasticsearch:7.4.2

以後再外面裝好外掛重啟即可;

特別注意:
-e ES_JAVA_OPTS="-Xms6m -Xmx256m"\ 測試環境下,設定 ES 的初始記憶體和最大記憶體,否則導致過大啟動不了 ES

2、Kibana

docker run --name kibana -e ELASTICSEARCH_HOSTS=http://192.168.56.10:9200 -p 5601:5601
-d kibana:7.4.2

http://192.168.56.10:9200 一定改為自己虛擬機器的地址

三、初步檢索

1、_cat

GET /_cat/nodes: 檢視所有節點
GET /_cat/health: 檢視es健康狀況
GET /_cat/master: 檢視主節點
GET /_cat/indices: 檢視所有索引 show databases;

2、索引一個文件(儲存)

儲存一個數據,儲存在哪個索引的哪個型別下,指定用哪個唯一標識
PUT customer/external/1;在 customer 索引下的external型別下儲存 1 號資料為

PUT customer/external/1
{
  "name":"John Doe"
}

PUT 和 POST 都可以,
POST 新增。如果不指定id,會自動生成id。指定id就會修改這個資料,並新增版本號

PUT 可以新增可以修改。PUT必須指定id;由於PUT需要指定id,我們一般都用來做修改操作,不指定id會報錯。

3、查詢文件

GET customer/external/1

結果:
{
"_index":"customer", //在哪個索引
"_type":"external", //在哪個型別
"_id":" 1 ", //記錄id
"_version": 2, //版本號
"_seq_no": 1, //併發控制欄位,每次更新就會 +1,用來做樂觀鎖
"_primary_term": 1, //同上,主分片重新分配,如重啟,就會變化
"found":true,
"_source":{ //真正的內容
"name":"John Doe"
}
}
更新攜帶 ?if_seq_no=0&if_primary_term=1

4、更新文件

POST customer/external/1/_update
{
  "doc":{
    "name":"John Doew"
  }
}

或者

POST customer/external/1/
{
  "name":"John Doe2"
}

或者
PUT customer/external/1
{
"name":"John Doe"
}

  • 不同:POST 操作會對比源文件資料,如果相同不會有什麼操作,文件 version 不增加;PUT操作總會將資料重新儲存並增加 version 版本;

    • 帶 _update 對比元資料如果一樣就不進行任何操作。
    • 看場景:
      • 對於大併發更新,不帶 update;
      • 對於大併發查詢偶爾更新,帶 update;對比更新,重新計算分配規則。
  • 更新同時增加屬性

POST customer/external/1/_update
{
  "doc":{"name":"Jane Doe","age":20 }
}

PUT 和 POST 不帶_update也可以

5、刪除文件&索引

DELETE customer/external/1
DELETE customer

6、bulk 批量 API

POST customer/external/_bulk
{"index":{"_id":"1"}}
{"name":"John Doe"}
{"index":{"_id":"2"}}
{"name":"Jane Doe"}

語法格式:
{action:{metadata}}\n
{request body }\n

{action:{metadata}}\n
{request body }\n

複雜例項:

POST /_bulk
{"delete":{"_index":"website","_type":"blog","_id":"123"}}
{"create":{"_index":"website","_type":"blog","_id":"123"}}

{"title": "My first blog post"}
{"index": {"_index":"website","_type":"blog"}}
{"title": "My second blog post"}
{"update":{"_index":"website","_type":"blog","_id":"123","_retry_on_conflict":3}}
{"doc":{"title":"My updated blog post"}}

bulk API 以此按順序執行所有的 action(動作)。如果一個單個的動作因任何原因而失敗,
它將繼續處理它後面剩餘的動作。當 bulk API 返回時,它將提供每個動作的狀態(與傳送
的順序相同),所以您可以檢查是否一個指定的動作是不是失敗了。

7、樣本測試資料

我準備了一份顧客銀行賬戶資訊的虛構的 JSON 文件樣本。每個文件都有下列的 schema(模式):
{
"account_number":0,
"balance":16623,
"firstname":"Bradshaw",
"lastname":"Mckenzie",
"age":29,
"gender":"F",
"address":"244 Columbus Place",
"employer":"Euron",
"email":"[email protected]",
"city":"Hobucken",
"state":"CO"
}
https://github.com/elastic/elasticsearch/blob/master/docs/src/test/resources/accounts.json?raw=true 匯入測試資料
POST bank/account/_bulk
測試資料

四、進階檢索

1、SearchAPI

ES支援兩種基本方式檢索 :

  • 一個是通過使用 REST request URI 傳送搜尋引數(uri+檢索引數)
  • 另一個是通過使用 REST request body 來發送它們(uri+請求體)

1)、檢索資訊

  • 一切檢索從_search開始

GET bank/_search 檢索 bank 下所有資訊,包括type和docs
GET bank/_search?q=*&sort=account_number:asc 請求引數方式檢索

響應結果解釋:
took-Elasticsearch 執行搜尋的時間(毫秒)
time_out- 告訴我們搜尋是否超時
_shards- 告訴我們多少個分片被搜尋了,以及統計了成功/失敗的搜尋分片
hits- 搜尋結果
hits.total- 搜尋結果
hits.hits- 實際的搜尋結果陣列(預設為前 10 的文件)
sort- 結果的排序key(鍵)(沒有則按 score 排序)
score 和 max_score– 相關性得分和最高得分(全文檢索用)

  • uri+請求體進行檢索

GET bank/_search
{
"query":{
"match_all":{}
},
"sort":[
{
"account_number":{
"order":"desc"
}
}
]
}
HTTP客戶端工具(POSTMAN),get請求不能攜帶請求體,我們變為post也是一樣的
我們 POST 一個 JSON 風格的查詢請求體到 _searchAPI
需要了解,一旦搜尋的結果被返回, Elasticsearch 就完成了這次請求,並且不會維護任何服務端的資源或者結果的 cursor (遊標)

2、QueryDSL

1)、基本語法格式

Elasticsearch 提供了一個可以執行查詢的 Json 風格的 DSLdomain-specificlanguage 領域特定語言)。這個被稱為QueryDSL。該查詢語言非常全面,並且剛開始的時候感覺有點複雜,真正學好它的方法是從一些基礎的示例開始的。

  • 一個查詢語句 的典型結構

{
QUERY_NAME:{
ARGUMENT:VALUE,
ARGUMENT:VALUE,...
}
}

  • 如果是針對某個欄位,那麼它的結構如下:

{
QUERY_NAME:{
FIELD_NAME:{
ARGUMENT:VALUE,
ARGUMENT:VALUE,...
}
}
}

GET bank/_search
{
"query":{
"match_all":{}
},
"from":0,
"size":5,
"sort":[
{
"account_number":{
"order":"desc"
}
}
]
}

  • query定義如何查詢,
  • match_all 查詢型別【代表查詢所有的所有】,es中可以在query中組合非常多的查詢型別完成複雜查詢
  • 除了 query引數之外,我們也可以傳遞其它的引數以改變查詢結果。如sort,size
  • from+size限定,完成分頁功能
  • sort排序,多欄位排序,會在前序欄位相等時後續欄位內部排序,否則以前序為準

2)、返回部分欄位

GET bank/_search
{
"query":{
"match_all":{}
},
"from":0,
"size":5,
"_source":["age","balance"]
}

3)、match【匹配查詢】

  • 基本型別(非字串),精確匹配

GET bank/_search
{
"query":{
"match":{
"account_number":"20"
}
}
}
match 返回 account_number=20 的

  • 字串,全文檢索

GET bank/_search
{
"query":{
"match":{
"address":"mill"
}
}
}
最終查詢出address中包含mill單詞的所有記錄
match當搜尋字串型別的時候,會進行全文檢索,並且每條記錄有相關性得分。

  • 字串,多個單詞(分詞+全文檢索)
    GET bank/_search
    {
    "query":{
    "match":{
    "address":"mill road"
    }
    }
    }
    最終查詢出address中包含mill或者road或者mill road的所有記錄,並給出相關性得分

4)、match_phrase【短語匹配】

將需要匹配的值當成一個整體單詞(不分詞)進行檢索

GET bank/_search
{
"query":{
"match_phrase":{
"address":"mill road"
}
}
}
查出address中包含mill road的所有記錄,並給出相關性得分

5)、multi_match【多欄位匹配】

GET bank/_search
{
"query":{
"multi_match":{
"query":"mill",
"fields":["state","address"]
}
}
}
state或者address包含mill

6)、bool【複合查詢】

bool用來做複合查詢:
複合語句可以合併任何其它查詢語句,包括複合語句,瞭解這一點是很重要的。這就意味著,複合語句之間可以互相巢狀,可以表達非常複雜的邏輯。

  • must:必須達到 must 列舉的所有條件

GET bank/_search
{
"query":{
"bool":{
"must":[
{"match":{"address":"mill"}},
{"match":{"gender":"M"}}
]
}
}
}

  • should:應該達到 should 列舉的條件,如果達到會增加相關文件的評分,並不會改變查詢的結果。如果query中只有should且只有一種匹配規則,那麼should的條件就會被作為預設匹配條件而去改變查詢結果

GET bank/_search
{
"query":{
"bool":{
"must":[
{"match":{"address":"mill"}},
{"match":{"gender":"M"}}
],
"should":[
{"match":{"address":"lane"}}
]
}
}
}

  • must_not 必須不是指定的情況

GET bank/_search
{
"query":{
"bool":{
"must":[
{"match":{"address":"mill"}},
{"match":{"gender":"M"}}
],
"should":[
{"match":{"address":"lane"}}
],
"must_not":[
{"match":{"email":"baluba.com"}}
]
}
}
}

address包含mill,並且gender是M,如果address裡面有lane最好不過,但是email必須不包含baluba.com

7)、filter【結果過濾】

並不是所有的查詢都需要產生分數,特別是那些僅用於 “filtering”(過濾)的文件。為了不
計算分數 Elasticsearch 會自動檢查場景並且優化查詢的執行。
GET bank/_search
{
"query":{
"bool":{
"must":[
{"match":{"address":"mill"}}
],
"filter":{
"range":{
"balance":{
"gte":10000,
"lte":20000
}
}
}
}
}
}

8)、term

和match一樣。匹配某個屬性的值。全文檢索欄位用 match ,其他非 text 欄位匹配用 term。

GET bank/_search
{
"query":{
"bool":{
"must":[
{"term":{
"age":{
"value":"28"
}
}},
{"match":{
"address":"990 Mill Road"
}}
]
}
}
}

9)、aggregations(執行聚合)

聚合提供了從資料中分組和提取資料的能力。最簡單的聚合方法大致等於 SQL GROUP BYSQL 聚合函式。在 Elasticsearch 中,您有執行搜尋返回 hits (命中結果),並且同時返回聚合結果,把一個響應中的所有 hits (命中結果)分隔開的能力。這是非常強大且有效的,您可以執行查詢和多個聚合,並且在一次使用中得到各自的(任何一個的)返回結果,使用一次簡潔和簡化的 API 來避免網路往返。

  • 搜尋 address 中包含 mill 的所有人的年齡分佈以及平均年齡,但不顯示這些人的詳情。

GET bank/_search
{
"query":{
"match":{
"address":"mill"
}
},
"aggs":{
"group_by_state":{
"terms":{
"field":"age"
}
},
"avg_age":{
"avg":{
"field":"age"
}
}
},
"size": 0
}

size: 0 不顯示搜尋資料
aggs:執行聚合。聚合語法如下
"aggs":{
"aggs_name 這次聚合的名字,方便展示在結果集中":{
"AGG_TYPE 聚合的型別(avg,term,terms)":{}
}
},

複雜:
按照年齡聚合,並且請求這些年齡段的這些人的平均薪資

GET bank/account/_search
{
"query":{
"match_all":{}
},
"aggs":{
"age_avg":{
"terms":{
"field":"age",
"size": 1000
},
"aggs":{
"banlances_avg":{
"avg":{
"field":"balance"
}
}
}
}
}
,
"size": 1000
}

複雜:查出所有年齡分佈,並且這些年齡段中 M 的平均薪資和 F 的平均薪資以及這個年齡段的總體平均薪資

GET bank/account/_search
{
"query":{
"match_all":{}
},
"aggs":{
"age_agg":{
"terms":{
"field":"age",
"size": 100
},
"aggs":{
"gender_agg":{
"terms":{
"field":"gender.keyword",
"size": 100
},
"aggs":{
"balance_avg":{
"avg":{
"field":"balance"
}
}
}
},
"balance_avg":{
"avg":{
"field":"balance"
}
}
}
}
}
,
"size": 1000
}

3 、Mapping

1)、欄位型別

2)、對映

Mapping(對映)
Mapping 是用來定義一個文件( document ),以及它所包含的屬性( field )是如何儲存和
索引的
。比如,使用mapping來定義:

  • 哪些字串屬性應該被看做全文字屬性(full text fields)。
  • 哪些屬性包含數字,日期或者地理位置。
  • 文件中的所有屬性是否都能被索引(_all 配置)。
  • 日期的格式。
  • 自定義對映規則來執行動態新增屬性。
  • 檢視mapping資訊:

GET bank/_mapping

  • 修改mapping資訊

https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping.html

自動猜測的對映型別

3)、新版本改變

Es7 及以上移除了type的概念。

  • 關係型資料庫中兩個資料表示是獨立的,即使他們裡面有相同名稱的列也不影響使用,但ES中不是這樣的。elasticsearch是基於Lucene開發的搜尋引擎,而ES中不同type下名稱相同的filed最終在Lucene中的處理方式是一樣的。
    • 兩個不同type下的兩個user_name,在ES同一個索引下其實被認為是同一個filed,你必須在兩個不同的type中定義相同的filed對映。否則,不同type中的相同欄位名稱就會在處理中出現衝突的情況,導致Lucene處理效率下降。
    • 去掉type就是為了提高ES處理資料的效率。

Elasticsearch 7.x

  • URL中的type引數為可選。比如,索引一個文件不再要求提供文件型別。

Elasticsearch 8.x

  • 不再支援URL中的type引數。

解決:
1)、將索引從多型別遷移到單型別,每種型別文件一個獨立索引
2)、將已存在的索引下的型別資料,全部遷移到指定位置即可。詳見資料遷移

1 、建立對映

1、建立索引並指定對映

PUT /my-index
{
"mappings":{
"properties":{
"age": {"type":"integer"},
"email": {"type":"keyword" },
"name": {"type":"text" }
}
}
}

2、新增新的欄位對映

PUT /my-index/_mapping
{
"properties":{
"employee-id":{
"type":"keyword",
"index":false
}
}
}

3、更新對映

對於已經存在的對映欄位,我們不能更新。更新必須建立新的索引進行資料遷移

4、資料遷移

先創建出 new_twitter 的正確對映。然後使用如下方式進行資料遷移

POST _reindex [固定寫法]
{
"source":{
"index":"twitter"
},
"dest":{
"index":"new_twitter"
}
}

將舊索引的 type 下的資料進行遷移
POST _reindex
{
"source":{
"index":"twitter",
"type":"tweet"
},
"dest":{
"index":"tweets"
}
}

4 、分詞

一個 tokenizer (分詞器)接收一個字元流,將之分割為獨立的 tokens (詞元,通常是獨立的單詞),然後輸出 tokens 流。
例如,whitespace tokenizer 遇到空白字元時分割文字。它會將文字" Quickbrownfox! "分割為[ Quick , brown , fox! ]。
tokenizer (分詞器)還負責記錄各個 term (詞條)的順序或 position 位置(用於 phrase 短語和 wordproximity 詞近鄰查詢),以及 term (詞條)所代表的原始 word (單詞)的 start(起始)和 end (結束)的 characteroffsets (字元偏移量)(用於高亮顯示搜尋的內容)。
Elasticsearch 提供了很多內建的分詞器,可以用來構建customanalyzers(自定義分詞器)。

1)、安裝 ik 分詞器

注意:不能用預設elasticsearch-plugin installxxx.zip 進行自動安裝
https://github.com/medcl/elasticsearch-analysis-ik/releases?after=v6.4.2 對應es版本安裝

進入es容器內部 plugins目錄
dockerexec-it 容器 id/bin/bash
wget
https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.4.2/elasticsearch-analysis-ik-7.4.2.zip
unzip 下載的檔案
rm –rf *.zip
mv elasticsearch/ ik

可以確認是否安裝好了分詞器
cd ../bin
elasticsearch plugin list:即可列出系統的分詞器

2)、測試分詞器

使用預設

POST _analyze
{
"text":"我是中國人"
}

請觀察結果
使用分詞器
POST _analyze
{
"analyzer":"ik_smart",
"text":"我是中國人"
}

請觀察結果
另外一個分詞器
ik_max_word
POST _analyze
{
"analyzer":"ik_max_word",
"text":"我是中國人"
}

請觀察結果
能夠看出不同的分詞器,分詞有明顯的區別,所以以後定義一個索引不能再使用預設的mapping了,要手工建立mapping,因為要選擇分詞器。

3)、自定義詞庫

修改/usr/share/elasticsearch/plugins/ik/config/中的IKAnalyzer.cfg.xml
/usr/share/elasticsearch/plugins/ik/config

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
  <comment>IK Analyzer 擴充套件配置</comment>
  <!--使用者可以在這裡配置自己的擴充套件字典 -->
  <entrykey="ext_dict"></entry>
  <!--使用者可以在這裡配置自己的擴充套件停止詞字典-->
  <entrykey="ext_stopwords"></entry>
  <!--使用者可以在這裡配置遠端擴充套件字典 -->
  <entrykey="remote_ext_dict">http://192.168.128.130/fenci/myword.txt</entry>
  <!--使用者可以在這裡配置遠端擴充套件停止詞字典-->
  <!--<entrykey="remote_ext_stopwords">words_location</entry>-->
</properties>

原來的xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
  <comment>IK Analyzer 擴充套件配置</comment>
  <!--使用者可以在這裡配置自己的擴充套件字典 -->
  <entrykey="ext_dict"></entry>
  <!--使用者可以在這裡配置自己的擴充套件停止詞字典-->
  <entrykey="ext_stopwords"></entry>
  <!--使用者可以在這裡配置遠端擴充套件字典 -->
  <!--<entrykey="remote_ext_dict">words_location</entry>-->
  <!--使用者可以在這裡配置遠端擴充套件停止詞字典-->
  <!--<entrykey="remote_ext_stopwords">words_location</entry>-->
</properties>

按照標紅的路徑利用nginx釋出靜態資源,按照請求路徑,建立對應的資料夾以及檔案,放在nginx的html下

然後重啟es伺服器,重啟nginx。
在kibana中測試分詞效果

更新完成後,es只會對新增的資料用新詞分詞。歷史資料是不會重新分詞的。如果想要歷史資料重新分詞。需要執行:
POST my_index/_update_by_query?conflicts=proceed

五、Elasticsearch-Rest-Client

1)、9300:TCP

  • spring-data-elasticsearch:transport-api.jar;
    • springboot版本不同, transport-api.jar 不同,不能適配 es 版本
    • 7.x已經不建議使用, 8 以後就要廢棄

2)、9200:HTTP

  • JestClient:非官方,更新慢
  • RestTemplate:模擬發 HTTP 請求,ES 很多操作需要自己封裝,麻煩
  • HttpClient:同上
  • Elasticsearch-Rest-Client:官方 RestClient,封裝了 ES 操作,API 層次分明,上手簡單

最終選擇Elasticsearch-Rest-Client(elasticsearch-rest-high-level-client)
https://www.elastic.co/guide/en/elasticsearch/client/java-rest/current/java-rest-high.html

1、SpringBoot整合

  <dependency>
    <groupId>org.elasticsearch.client</groupId>
    <artifactId>elasticsearch-rest-high-level-client</artifactId>
    <version>7.4.2</version>
  </dependency>

2、配置

  @Bean
  RestHighLevelClientclient(){
    RestClientBuilder builder = RestClient.builder(new HttpHost("192.168.56.10", 9200 ,"http"));
    return new RestHighLevelClient(builder);
  }

3、使用

參照官方文件:

  @Test
  voidtest 1 ()throwsIOException{
    Product product = new Product();
    product.setSpuName("華為");
    product.setId(10L);

    IndexRequest request = new IndexRequest("product").id("20").source("spuName","華為","id", 20L);
    try{
      IndexResponse response = client.index(request,RequestOptions.DEFAULT);
      System.out.println(request.toString());
      IndexResponse response2 = client.index(request,RequestOptions.DEFAULT);
    }catch(ElasticsearchException e){
      if(e.status()==RestStatus.CONFLICT){

      }
    }
  }

六、附錄-安裝 nginx

  • 隨便啟動一個nginx例項,只是為了複製出配置
    • docker run -p 80:80 --name nginx -d nginx:1.10
  • 將容器內的配置檔案拷貝到當前目錄:docker container cp nginx:/etc/nginx .
    • 別忘了後面的點
  • 修改檔名稱:mv nginx conf 把這個conf移動到/mydata/nginx下
  • 終止原容器:docker stop nginx
  • 執行命令刪除原容器:docker rm $ContainerId
  • 建立新的 nginx;執行以下命令

docker run -p 80:80 --name nginx
-v /mydata/nginx/html:/usr/share/nginx/html
-v /mydata/nginx/logs:/var/log/nginx
-v /mydata/nginx/conf:/etc/nginx
-d nginx:1.10

  • 給 nginx 的 html 下面放的所有資源可以直接訪問;