天天看點

ElasticSearch基礎學習(SpringBoot內建ES)

一、概述

什麼是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 ,又稱複制分片 )

ElasticSearch基礎學習(SpringBoot內建ES)

上圖是一個有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

兩個文檔都比對,但是第一個文檔比第二個比對程度更高。如果沒有别的條件,現在,這兩個包含關鍵字的文檔都将傳回。

再來看一個示例,比如我們通過部落格标簽來搜尋部落格文章。那麼反向索引清單就是這樣的一個結構 :

ElasticSearch基礎學習(SpringBoot內建ES)

如果要搜尋含有 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 目錄中。

ElasticSearch基礎學習(SpringBoot內建ES)

3、重新啟動 ElasticSearch 服務,在啟動過程中,你可以看到正在加載"analysis-ik"插件的提示資訊,服務啟動後,在指令行運作

elasticsearch-plugin list

指令,确認 ik 插件安裝成功。

ElasticSearch基礎學習(SpringBoot內建ES)

IK提供了兩個分詞算法:

ik_smart

ik_max_word

,其中

ik_smart

為最少切分,

ik_max_word

為最細粒度劃分!

  • ik_max_word

    : 細粒度分詞,會窮盡一個語句中所有分詞可能。
  • ik_smart

    : 粗粒度分詞,優先比對最長詞,隻有1個詞!

如果某些詞語,在預設的詞庫中不存在,比如我們想讓“我愛學習”被識别是一個詞,這時就需要我們編輯自定義詞庫。

步驟:

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