一、概述
什麼是ElasticSearch?
ElasticSearch,簡稱為ES, ES是一個開源的高擴充的分布式全文搜尋引擎。
它可以近乎實時的存儲、檢索資料;本身擴充性很好,可以擴充到上百台伺服器,處理PB級别的資料。
ES也使用Java開發并使用Lucene作為其核心來實作所有索引和搜尋的功能,但是它的目的是通過簡單的
RESTful API
來隐藏Lucene的複雜性,進而讓全文搜尋變得簡單。
ES核心概念
知道了ES是什麼後,接下來還需要知道ES是如何存儲資料,資料結構是什麼,又是如何實作搜尋的呢?
學習這些之前需要先了解一些ElasticSearch的相關概念。
ElasticSearch是面向文檔型資料庫
相信學習過MySql的同學都知道,MySql是關系型資料庫,那麼ES與關系型資料庫有什麼差別呢?
下面做一下簡單的對比:
關系型資料庫(MySql、Oracle等) | ElasticSearch |
---|---|
資料庫(database) | 索引(indices) |
表(tables) | 類型(types) |
行(rows) | 文檔(documents) |
列(columns) | 字段(fields) |
說明:ElasticSearch(叢集)中可以包含多個索引(資料庫),每個索引中可以包含多個類型(表),每個類型下又包含多 個文檔(行),每個文檔中又包含多個字段(列)。
實體設計:
ElasticSearch 在背景把每個索引劃分成多個分片,每份分片可以在叢集中的不同伺服器間遷移。
邏輯設計:
一個索引類型中,包含多個文檔,比如說文檔1,文檔2,文檔3。
當我們索引一篇文檔時,可以通過這樣的一個順序找到它:
索引 ▷ 類型 ▷ 文檔ID
,通過這個組合我們就能索引到某個具體的文檔。 注意:ID不必是整數,實際上它是個字元串。
索引
索引是映射類型的容器,elasticsearch中的索引是一個非常大的文檔集合。索引存儲了映射類型的字段和其他設定。 然後它們被存儲到了各個分片上了。 我們來研究下分片是如何工作的。
實體設計 :節點和分片 如何工作
一個叢集至少有一個節點,而一個節點就是一個elasricsearch程序,節點可以有多個索引預設的,如果你建立索引,那麼索引将會有個5個分片 ( primary shard ,又稱主分片 ) 構成的,每一個主分片會有一個副本 ( replica shard ,又稱複制分片 )

上圖是一個有3個節點的叢集,可以看到主分片和對應的複制分片都不會在同一個節點内,這樣有利于某個節點挂掉 了,資料也不至于丢失。 實際上,一個分片是一個Lucene索引,一個包含
反向索引
的檔案目錄,反向索引的結構使得elasticsearch在不掃描全部文檔的情況下,就能告訴你哪些文檔包含特定的關鍵字。 其中,
反向索引
又是什麼呢?
反向索引
elasticsearch使用的是一種稱為
反向索引
的結構,采用Lucene倒排索作為底層。這種結構适用于快速的全文搜尋, 一個索引由文檔中所有不重複的清單構成,對于每一個詞,都有一個包含它的文檔清單。 例如,現在有兩個文檔, 每個文檔包含如下内容:
Study every day, good good up to forever # 文檔1包含的内容
To forever, study every day, good good up # 文檔2包含的内容
為了建立反向索引,我們首先要将每個文檔拆分成獨立的詞(或稱為詞條或者tokens),然後建立一個包含所有不重複的詞條的排序清單,然後列出每個詞條出現在哪個文檔 :
term | doc_1 | doc_2 |
---|---|---|
Study | √ | x |
To | x | x |
every | √ | √ |
forever | √ | √ |
day | √ | √ |
study | x | √ |
good | √ | √ |
every | √ | √ |
to | √ | x |
up | √ | √ |
現在,我們試圖搜尋 to forever,隻需要檢視包含每個詞條的文檔
term | doc_1 | doc_2 |
---|---|---|
to | √ | × |
forever | √ | √ |
total | 2 | 1 |
兩個文檔都比對,但是第一個文檔比第二個比對程度更高。如果沒有别的條件,現在,這兩個包含關鍵字的文檔都将傳回。
再來看一個示例,比如我們通過部落格标簽來搜尋部落格文章。那麼反向索引清單就是這樣的一個結構 :
如果要搜尋含有 python 标簽的文章,那相對于查找所有原始資料而言,查找反向索引後的資料将會快的多。隻需要 檢視标簽這一欄,然後擷取相關的文章ID即可。
ElasticSearch的索引和Lucene的索引對比
在elasticsearch中, 索引這個詞被頻繁使用,這就是術語的使用。 在elasticsearch中,索引被分為多個分片,每份分片是一個Lucene的索引。是以一個elasticsearch索引是由多個Lucene索引組成的。
類型
類型是文檔的邏輯容器,就像關系型資料庫一樣,表格是行的容器。 類型中對于字段的定義稱為映射,比如 name 映射為字元串類型。
我們說文檔是無模式的,它們不需要擁有映射中所定義的所有字段,比如新增一個字段,那麼elasticsearch是怎麼做的呢?elasticsearch會自動的将新字段加入映射,但是這個字段的不确定它是什麼類型,elasticsearch就開始猜,如果這個值是18,那麼elasticsearch會認為它是整形。 但是elasticsearch也可能猜不對, 是以最安全的方式就是提前定義好所需要的映射,這點跟關系型資料庫殊途同歸了,先定義好字段,然後再使用。
文檔
之前說elasticsearch是面向文檔的,那麼就意味着索引和搜尋資料的最小機關是文檔。
elasticsearch中,文檔有幾個重要屬性 :
- 自我包含,一篇文檔同時包含字段和對應的值,也就是同時包含 key:value!
- 可以是層次型的,一個文檔中包含自文檔,複雜的邏輯實體就是這麼來的!
- 靈活的結構,文檔不依賴預先定義的模式,我們知道關系型資料庫中,要提前定義字段才能使用,在elasticsearch中,對于字段是非常靈活的,有時候,我們可以忽略該字段,或者動态的添加一個新的字段。
盡管我們可以随意的新增或者忽略某個字段,但是,每個字段的類型非常重要,比如一個年齡字段類型,可以是字元串也可以是整形。因為elasticsearch會儲存字段和類型之間的映射及其他的設定。這種映射具體到每個映射的每種類型,這也是為什麼在elasticsearch中,類型有時候也稱為映射類型。
二、ES基礎操作
IK分詞器插件
什麼是IK分詞器?
分詞:即把一段中文或者别的劃分成一個個的關鍵字,我們在搜尋時候會把自己的資訊進行分詞,會把資料庫中或者索引庫中的資料進行分詞,然後進行一個比對操作。
預設的中文分詞是将每個字看成一個詞,比如 “我愛學習” 會被分為"我","愛","學","習",這顯然是不符合要求的,是以我們需要安裝中文分詞器ik來解決這個問題。
IK分詞器安裝步驟
1、下載下傳ik分詞器的包,Github位址:https://github.com/medcl/elasticsearch-analysis-ik/ (版本要對應)
2、下載下傳後解壓,并将目錄拷貝到ElasticSearch根目錄下的 plugins 目錄中。
3、重新啟動 ElasticSearch 服務,在啟動過程中,你可以看到正在加載"analysis-ik"插件的提示資訊,服務啟動後,在指令行運作
elasticsearch-plugin list
指令,确認 ik 插件安裝成功。
IK提供了兩個分詞算法:
ik_smart
和
ik_max_word
,其中
ik_smart
為最少切分,
ik_max_word
為最細粒度劃分!
-
: 細粒度分詞,會窮盡一個語句中所有分詞可能。ik_max_word
-
: 粗粒度分詞,優先比對最長詞,隻有1個詞!ik_smart
如果某些詞語,在預設的詞庫中不存在,比如我們想讓“我愛學習”被識别是一個詞,這時就需要我們編輯自定義詞庫。
步驟:
(1)進入elasticsearch/plugins/ik/config目錄
(2)建立一個my.dic檔案,編輯内容:
我愛學習
(3)修改IKAnalyzer.cfg.xml(在ik/config目錄下)
<properties>
<comment>IK Analyzer 擴充配置</comment>
<!-- 使用者可以在這裡配置自己的擴充字典 -->
<entry key="ext_dict">my.dic</entry>
<!-- 使用者可以在這裡配置自己的擴充停止詞字典 -->
<entry key="ext_stopwords"></entry>
</properties>
注意:修改完配置後,需要重新啟動elasticsearch。
增删改查基本指令
Rest風格說明
一種軟體架構風格,而不是标準,隻是提供了一組設計原則和限制條件。它主要用于用戶端和伺服器互動類的軟體。基于這個風格設計的軟體可以更簡潔,更有層次,更易于實作緩存等機制。
基本Rest指令說明(增、删、改、查指令):
method | ur位址 | 描述 |
---|---|---|
PUT | localhost:9200/索引名稱/類型名稱/文檔id | 建立文檔(指定文檔id) |
POST | localhost:9200/索引名稱/類型名稱 | 建立文檔(随機文檔id) |
POST | localhost:9200/索引名稱/類型名稱/文檔id/_update | 修改文檔 |
DELETE | localhost:9200/索名稱/類型名稱/文檔id | 删除文檔 |
GET | localhost:9200/索引名稱/類型名稱/文檔id | 查詢文檔通過文檔id |
POST | localhost:9200/索引名稱/類型名稱/_search | 查詢所有資料 |
三、SpringBoot內建ES
1、建立項目
建立一個springboot(2.2.5版)項目 elasticsearch-demo ,導入web依賴即可。
2、配置依賴
配置elasticsearch的依賴:
<properties>
<java.version>1.8</java.version>
<!-- 這裡SpringBoot預設配置的版本不比對,我們需要自己配置版本! -->
<elasticsearch.version>7.6.1</elasticsearch.version>
</properties>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
3、編寫配置類
編寫elasticsearch的配置類,提供
RestHighLevelClient
這個bean來進行操作。
package com.hzx.config;
import org.apache.http.HttpHost;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ElasticsearchClientConfig {
@Bean
public RestHighLevelClient restHighLevelClient() {
RestHighLevelClient client = new RestHighLevelClient(
RestClient.builder(new HttpHost("127.0.0.1", 9200, "http")));
return client;
}
}
4、配置工具類
封裝ES常用方法工具類
package com.hzx.utils;
import com.alibaba.fastjson.JSON;
import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequest;
import org.elasticsearch.action.bulk.BulkRequest;
import org.elasticsearch.action.bulk.BulkResponse;
import org.elasticsearch.action.delete.DeleteRequest;
import org.elasticsearch.action.delete.DeleteResponse;
import org.elasticsearch.action.get.GetRequest;
import org.elasticsearch.action.get.GetResponse;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.index.IndexResponse;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.action.support.master.AcknowledgedResponse;
import org.elasticsearch.action.update.UpdateRequest;
import org.elasticsearch.action.update.UpdateResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.client.indices.CreateIndexRequest;
import org.elasticsearch.client.indices.CreateIndexResponse;
import org.elasticsearch.client.indices.GetIndexRequest;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.rest.RestStatus;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.fetch.subphase.FetchSourceContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.TimeUnit;
@Component
public class EsUtils<T> {
@Autowired
@Qualifier("restHighLevelClient")
private RestHighLevelClient client;
/**
* 判斷索引是否存在
*
* @param index
* @return
* @throws IOException
*/
public boolean existsIndex(String index) throws IOException {
GetIndexRequest request = new GetIndexRequest(index);
boolean exists = client.indices().exists(request, RequestOptions.DEFAULT);
return exists;
}
/**
* 建立索引
*
* @param index
* @throws IOException
*/
public boolean createIndex(String index) throws IOException {
CreateIndexRequest request = new CreateIndexRequest(index);
CreateIndexResponse createIndexResponse = client.indices()
.create(request, RequestOptions.DEFAULT);
return createIndexResponse.isAcknowledged();
}
/**
* 删除索引
*
* @param index
* @return
* @throws IOException
*/
public boolean deleteIndex(String index) throws IOException {
DeleteIndexRequest deleteIndexRequest = new DeleteIndexRequest(index);
AcknowledgedResponse response = client.indices()
.delete(deleteIndexRequest, RequestOptions.DEFAULT);
return response.isAcknowledged();
}
/**
* 判斷某索引下文檔id是否存在
*
* @param index
* @param id
* @return
* @throws IOException
*/
public boolean docExists(String index, String id) throws IOException {
GetRequest getRequest = new GetRequest(index, id);
//隻判斷索引是否存在不需要擷取_source
getRequest.fetchSourceContext(new FetchSourceContext(false));
getRequest.storedFields("_none_");
boolean exists = client.exists(getRequest, RequestOptions.DEFAULT);
return exists;
}
/**
* 添加文檔記錄
*
* @param index
* @param id
* @param t 要添加的資料實體類
* @return
* @throws IOException
*/
public boolean addDoc(String index, String id, T t) throws IOException {
IndexRequest request = new IndexRequest(index);
request.id(id);
//timeout
request.timeout(TimeValue.timeValueSeconds(1));
request.timeout("1s");
request.source(JSON.toJSONString(t), XContentType.JSON);
IndexResponse indexResponse = client.index(request, RequestOptions.DEFAULT);
RestStatus Status = indexResponse.status();
return Status == RestStatus.OK || Status == RestStatus.CREATED;
}
/**
* 根據id來擷取記錄
*
* @param index
* @param id
* @return
* @throws IOException
*/
public GetResponse getDoc(String index, String id) throws IOException {
GetRequest request = new GetRequest(index, id);
GetResponse getResponse = client.get(request,RequestOptions.DEFAULT);
return getResponse;
}
/**
* 批量添加文檔記錄
* 沒有設定id ES會自動生成一個,如果要設定 IndexRequest的對象.id()即可
*
* @param index
* @param list
* @return
* @throws IOException
*/
public boolean bulkAdd(String index, List<T> list) throws IOException {
BulkRequest bulkRequest = new BulkRequest();
//timeout
bulkRequest.timeout(TimeValue.timeValueMinutes(2));
bulkRequest.timeout("2m");
for (int i = 0; i < list.size(); i++) {
bulkRequest.add(new IndexRequest(index).source(JSON.toJSONString(list.get(i))));
}
BulkResponse bulkResponse = client.bulk(bulkRequest,RequestOptions.DEFAULT);
return !bulkResponse.hasFailures();
}
/**
* 更新文檔記錄
* @param index
* @param id
* @param t
* @return
* @throws IOException
*/
public boolean updateDoc(String index, String id, T t) throws IOException {
UpdateRequest request = new UpdateRequest(index, id);
request.doc(JSON.toJSONString(t));
request.timeout(TimeValue.timeValueSeconds(1));
request.timeout("1s");
UpdateResponse updateResponse = client.update(request, RequestOptions.DEFAULT);
return updateResponse.status() == RestStatus.OK;
}
/**
* 删除文檔記錄
*
* @param index
* @param id
* @return
* @throws IOException
*/
public boolean deleteDoc(String index, String id) throws IOException {
DeleteRequest request = new DeleteRequest(index, id);
//timeout
request.timeout(TimeValue.timeValueSeconds(1));
request.timeout("1s");
DeleteResponse deleteResponse = client.delete(request, RequestOptions.DEFAULT);
return deleteResponse.status() == RestStatus.OK;
}
/**
* 根據某字段來搜尋
*
* @param index
* @param field
* @param key 要收搜的關鍵字
* @throws IOException
*/
public void search(String index, String field, String key, Integer
from, Integer size) throws IOException {
SearchRequest searchRequest = new SearchRequest(index);
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
sourceBuilder.query(QueryBuilders.termQuery(field, key));
//控制搜素
sourceBuilder.from(from);
sourceBuilder.size(size);
//最大搜尋時間。
sourceBuilder.timeout(new TimeValue(60, TimeUnit.SECONDS));
searchRequest.source(sourceBuilder);
SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
System.out.println(JSON.toJSONString(searchResponse.getHits()));
}
}
5、工具類API測試
測試建立索引:
@Test
void testCreateIndex() throws IOException {
CreateIndexRequest request = new CreateIndexRequest("test_index");
CreateIndexResponse createIndexResponse=restHighLevelClient.indices()
.create(request,RequestOptions.DEFAULT);
System.out.println(createIndexResponse);
}
測試擷取索引:
@Test
void testExistsIndex() throws IOException {
GetIndexRequest request = new GetIndexRequest("test_index");
boolean exists = restHighLevelClient.indices()
.exists(request,RequestOptions.DEFAULT);
System.out.println(exists);
}
測試删除索引:
@Test
void testDeleteIndexRequest() throws IOException {
DeleteIndexRequest deleteIndexRequest = new
DeleteIndexRequest("test_index");
AcknowledgedResponse response = restHighLevelClient.indices()
.delete(deleteIndexRequest,
RequestOptions.DEFAULT);
System.out.println(response.isAcknowledged());
}
測試添加文檔記錄:
建立一個實體類User
@Data
@AllArgsConstructor
@NoArgsConstructor
@Component
public class User {
private String name;
private int age;
}
測試添加文檔記錄
@Test
void testAddDocument() throws IOException {
// 建立對象
User user = new User("zhangsan", 3);
// 建立請求
IndexRequest request = new IndexRequest("test_index");
// 規則
request.id("1");
request.timeout(TimeValue.timeValueSeconds(1));
request.timeout("1s");
request.source(JSON.toJSONString(user), XContentType.JSON);
// 發送請求
IndexResponse indexResponse = restHighLevelClient.index(request,
RequestOptions.DEFAULT);
System.out.println(indexResponse.toString());
RestStatus Status = indexResponse.status();
System.out.println(Status == RestStatus.OK || Status ==
RestStatus.CREATED);
}
測試:判斷某索引下文檔id是否存在
@Test
void testIsExists() throws IOException {
GetRequest getRequest = new GetRequest("test_index","1");
// 不擷取_source上下文 storedFields
getRequest.fetchSourceContext(new FetchSourceContext(false));
getRequest.storedFields("_none_");
// 判斷此id是否存在!
boolean exists = restHighLevelClient.exists(getRequest,
RequestOptions.DEFAULT);
System.out.println(exists);
}
測試:根據id擷取文檔記錄
@Test
void testGetDocument() throws IOException {
GetRequest getRequest = new GetRequest("test_index","3");
GetResponse getResponse = restHighLevelClient.get(getRequest,RequestOptions.DEFAULT);
// 列印文檔内容
System.out.println(getResponse.getSourceAsString());
System.out.println(getResponse);
}
測試:更新文檔記錄
@Test
void testUpdateDocument() throws IOException {
UpdateRequest request = new UpdateRequest("test_index","1");
request.timeout(TimeValue.timeValueSeconds(1));
request.timeout("1s");
User user = new User("zhangsan", 18);
request.doc(JSON.toJSONString(user), XContentType.JSON);
UpdateResponse updateResponse = restHighLevelClient.update(request, RequestOptions.DEFAULT);
System.out.println(updateResponse.status() == RestStatus.OK);
}
測試:删除文檔記錄
@Test
void testDelete() throws IOException {
DeleteRequest request = new DeleteRequest("test_index","3");
//timeout
request.timeout(TimeValue.timeValueSeconds(1));
request.timeout("1s");
DeleteResponse deleteResponse = restHighLevelClient.delete(
request, RequestOptions.DEFAULT);
System.out.println(deleteResponse.status() == RestStatus.OK);
}
測試:批量添加文檔
@Test
void testBulkRequest() throws IOException {
BulkRequest bulkRequest = new BulkRequest();
//timeout
bulkRequest.timeout(TimeValue.timeValueMinutes(2));
bulkRequest.timeout("2m");
ArrayList<User> userList = new ArrayList<>();
userList.add(new User("zhangsan1",3));
userList.add(new User("zhangsan2",3));
userList.add(new User("zhangsan3",3));
userList.add(new User("lisi1",3));
userList.add(new User("lisi2",3));
userList.add(new User("lisi3",3));
for (int i =0;i<userList.size();i++){
bulkRequest.add(new IndexRequest("test_index").id(""+(i+1))
.source(JSON.toJSONString(userList.get(i)),XContentType.JSON));
}
// bulk
BulkResponse bulkResponse = restHighLevelClient.bulk(bulkRequest,RequestOptions.DEFAULT);
System.out.println(!bulkResponse.hasFailures());
}
查詢測試:
/**
* 使用QueryBuilder
* termQuery("key", obj) 完全比對
* termsQuery("key", obj1, obj2..) 一次比對多個值
* matchQuery("key", Obj) 單個比對, field不支援通配符, 字首具進階特性
* multiMatchQuery("text", "field1", "field2"..); 比對多個字段, field有通配符忒行
* matchAllQuery(); 比對所有檔案
*/
@Test
void testSearch() throws IOException {
SearchRequest searchRequest = new SearchRequest("test_index");
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
// TermQueryBuilder termQueryBuilder = QueryBuilders.termQuery("name","zhangsan1");
MatchAllQueryBuilder matchAllQueryBuilder = QueryBuilders.matchAllQuery();
sourceBuilder.query(matchAllQueryBuilder);
sourceBuilder.timeout(new TimeValue(60, TimeUnit.SECONDS));
searchRequest.source(sourceBuilder);
SearchResponse response = restHighLevelClient.search(searchRequest,RequestOptions.DEFAULT);
System.out.println(JSON.toJSONString(response.getHits()));
System.out.println("================查詢高亮顯示==================");
for (SearchHit documentFields : response.getHits().getHits()) {
System.out.println(documentFields.getSourceAsMap());
}
}