一、Elasticsearch原理與基本使用

[TOC]

一、Elasticsearch概述

1.1 什麼是搜索

​ 搜索,就是在任何場景下,找尋想要的信息。通過關鍵字檢索出與此關鍵字有關的信息。這和查詢還不太一樣,查詢通常是在表格類型的數據中查找,字段的內容的長度往往不大。

1.2 使用傳統數據庫實現搜索

​ 傳統數據庫的情況下,如果要查詢某個字段是否包含某些關鍵字的話,需要使用到like關鍵字來進行字段匹配,很大概率導致全表掃描,本身來說性能就不算好。如果再加上查詢的字段非常長,那麼使用like匹配的工作量是很大的,另外如果表的行數也很多,那麼性能就更差了。

1.3 全文檢索與倒排索引

​ 全文檢索是指計算機索引程序通過掃描文章中的每一個詞,對每一個詞建立一個索引,指明該詞在文章中出現的次數和位置,當用戶查詢時,檢索程序就根據事先建立的索引進行查找,並將查找的結果反饋給用戶的檢索方式。這個過程類似於通過字典中的檢索字表查字的過程。全文搜索引擎數據庫中的數據。而全文檢索用到的關鍵技術就是倒排索引。什麼是倒排索引?看看例子就知道了

數據庫中有如下數據
id  員工描述
1   優秀論文
2   優秀員工稱號
3   優秀項目
4   優秀團隊

建立倒排索引的步驟:
1、每行切詞, 怎麼切都可以,看實際需要
1 優秀    論文
2 優秀    員工  稱號
3 優秀    項目
4 優秀    團隊

2、建立倒排索引
優秀  1,2,3,4
論文  1
員工  2
稱號  2
項目  3
團隊  4

3、檢索
倒排索引意思簡單就是指定的詞出現在哪些行中,這些行都用唯一id進行標識。
所以這就是爲什麼倒排索引用到全文檢索中,因爲可以直接查詢到包含相關關鍵字的內容有哪些。
比如搜索優秀,可以看到優秀這個詞在1234中都有出現,然後根據id查詢原始數據。

​ 有了倒排索引,當我們需要從很多端很長的內容中檢索包含指定關鍵字的內容時,直接根據倒排索引就知道有沒有指定關鍵字了。而如果使用傳統數據庫,那麼必須掃描全部內容,如果數據有1000行,那工作量就很恐怖了。而倒排索引只是查詢個關鍵字而已,無需掃描全部內容。

1.4 Lucene和Elasticsearch

​ Lucene就是一個jar包,裏面包含了封裝好的各種建立倒排索引,以及進行搜索的代碼,包括各種算法。我們就用java開發的時候,引入lucene jar,然後基於lucene的api進行去進行開發就可以了。但是它只是根據文本做出索引,然後保存下來,但是本身並不提供搜索功能。
​ 由於Lucene使用比較複雜,繁瑣,所以基於Lucene開發了一個新的項目,也就是Elasticsearch(簡稱ES)。

1.5 ES的特點與適用場景

特點:

1)可以作爲一個大型分佈式集羣(數百臺服務器)技術,處理PB級數據,服務大公司;也可以運行在單機上,服務小公司;
2)Elasticsearch不是什麼新技術,主要是將全文檢索、數據分析以及分佈式技術,合併在了一起,才形成了獨一無二的ES;lucene(全文檢索),商用的數據分析軟件(也是有的),分佈式數據庫(mycat);
3)對用戶而言,是開箱即用的,非常簡單,作爲中小型的應用,直接3分鐘部署一下ES,就可以作爲生產環境的系統來使用了,數據量不大,操作不是太複雜;
4)數據庫的功能面對很多領域是不夠用的(事務,還有各種聯機事務型的操作);特殊的功能,比如全文檢索,同義詞處理,相關度排名,複雜數據分析,海量數據的近實時處理;Elasticsearch作爲傳統數據庫的一個補充,提供了數據庫所不能提供的很多功能。

適用場景:

1)維基百科,類似百度百科,牙膏,牙膏的維基百科,全文檢索,高亮,搜索推薦。
2)The Guardian(國外新聞網站),類似搜狐新聞,用戶行爲日誌(點擊,瀏覽,收藏,評論)+ 社交網絡數據(對某某新聞的相關看法),數據分析,給到每篇新聞文章的作者,讓他知道他的文章的公衆反饋(好,壞,熱門,垃圾,鄙視,崇拜)。
3)Stack Overflow(國外的程序異常討論論壇),IT問題,程序的報錯,提交上去,有人會跟你討論和回答,全文檢索,搜索相關問題和答案,程序報錯了,就會將報錯信息粘貼到裏面去,搜索有沒有對應的答案。
4)GitHub(開源代碼管理),搜索上千億行代碼。
5)國內:站內搜索(電商,招聘,門戶,等等),IT系統搜索(OA,CRM,ERP,等等),數據分析(ES熱門的一個使用場景)。

1.6 ES中的相關概念

近實時

兩個意思,從寫入數據到數據可以被搜索到有一個小延遲(大概1秒);基於es執行搜索和分析可以達到秒級。

集羣cluster

ES集羣可以有多個節點,但是每個節點屬於哪個ES集羣中是通過配置集羣名稱來指定的。當然一個集羣只有一個節點也是OK的

節點node

集羣中的一個節點,節點也有一個名稱(默認是隨機分配的),節點名稱很重要(在執行運維管理操作的時候),默認節點會去加入一個名稱爲“elasticsearch”的集羣,如果直接啓動一堆節點,那麼它們會自動組成一個elasticsearch集羣,當然一個節點也可以組成一個elasticsearch集羣。

index--database

索引包含一堆有相似結構的文檔數據,比如可以有一個客戶索引,商品分類索引,訂單索引,索引有一個名稱。一個index包含很多document,一個index就代表了一類類似的或者相同的document。比如說建立一個product index,商品索引,裏面可能就存放了所有的商品數據,所有的商品document。類似於傳統數據庫中的庫的概念

type--table

    每個索引裏都可以有一個或多個type,type是index中的一個邏輯數據分類,一個type下的document,都有相同的field,比如博客系統,有一個索引,可以定義用戶數據type,博客數據type,評論數據type。類似於傳統數據庫中的表的概念。
    要注意:es逐漸拋棄掉這個概念了,到6.x版本中,已經只允許一個index只有一個type了。

document--行

    文檔是es中的最小數據單元,一個document可以是一條客戶數據,一條商品分類數據,一條訂單數據,通常用JSON數據結構表示,每個index下的type中,都可以去存儲多個document。相當於行

field--字段

Field是Elasticsearch的最小單位。一個document裏面有多個field,每個field就是一個數據字段。
如:
product document
{
  "product_id": "1",
  "product_name": "高露潔牙膏",
  "product_desc": "高效美白",
  "category_id": "2",
  "category_name": "日化用品"  這些就是字段
}

mapping--映射約束

    數據如何存放到索引對象上,需要有一個映射配置,包括:數據類型、是否存儲、是否分詞等。所謂映射是對type的存儲的一些限制。
例子:
    這樣就創建了一個名爲blog的Index。Type不用單獨創建,在創建Mapping 時指定就可以。Mapping用來定義Document中每個字段的類型,即所使用的 analyzer、是否索引等屬性。創建Mapping 的代碼示例如下:
client.indices.putMapping({
    index : 'blog',
    type : 'article',
    這裏還可以設置type的一些工作屬性,比如_source等,後面會講
    body : {
        article: {
            properties: {
                id: {
                    type: 'string',
                    analyzer: 'ik',
                    store: 'yes',
                },
                title: {
                    type: 'string',
                    analyzer: 'ik',
                    store: 'no',
                },
                content: {
                    type: 'string',
                    analyzer: 'ik',
                    store: 'yes',
                }
            }
        }
    }
});

1.7 ES讀寫數據的機制

寫流程:

1、客戶端根據提供的es節點,選擇一個node作爲協調節點,併發送寫請求
2、協調節點對寫入的document進行路由,將document進行分片。每個分片單獨進行寫,每個分片默認都是雙備份,寫在不同的節點上。
3、分片寫入時,主備份由協調節點寫入,副備份則是從主備份所在節點同步數據過去。
4、當分片都寫完後,由協調節點返回寫入完成給客戶端

讀流程:

讀流程就很簡單了,如果通過docid來讀取,直接根據docid進行hash。判斷出該doc存儲在哪個節點上,然後到相應節點上讀取數據即可。

1.8 ES數據存儲結構

一、Elasticsearch原理與基本使用

​ 圖1.1 ES存儲結構

首先分爲兩個區域,一個是索引區域,一個是數據區域。前者用來存儲生成的倒排索引,後者用來存儲原始的document(可以選擇不存,後面有說)。

1)索引對象(index):存儲數據的表結構 ,任何搜索數據,存放在索引對象上 。
2)映射(mapping):數據如何存放到索引對象上,需要有一個映射配置, 包括:數據類型、是否存儲、是否分詞等。
3)文檔(document):一條數據記錄,存在索引對象上 。es會給每個document生成一個唯一的documentID,用於標識該document。當然也可以手動指定docid
4)文檔類型(type):一個索引對象,存放多種類型數據,數據用文檔類型進行標識。

二、ES部署

使用的es版本爲:6.6.2
下載地址:https://www.elastic.co/products/elasticsearch

2.1 單節點部署

解壓程序到指定目錄:

tar zxf elasticsearch-6.6.2.tar.gz -C /opt/modules/

修改配置文件:

cd /opt/modules/elasticsearch-6.6.2/
vim config/elasticsearch.yml 
修改如下內容:
# ---------------------------------- Cluster -------------------------------------
# 集羣名稱
cluster.name: my-application
# ------------------------------------ Node --------------------------------------
# 節點名稱,需要保證全局唯一
node.name: bigdata121
# ----------------------------------- Paths ---------------------------------------
# 配置es數據目錄,以及日誌目錄
path.data: /opt/modules/elasticsearch-6.6.2/data
path.logs: /opt/modules/elasticsearch-6.6.2/logs
# ----------------------------------- Memory -----------------------------------
# 配置es不檢查內存限制,內存不夠時啓動會檢查報錯
bootstrap.memory_lock: false
bootstrap.system_call_filter: false
# ---------------------------------- Network ------------------------------------
# 綁定ip
network.host: 192.168.50.121
# --------------------------------- Discovery ------------------------------------
# 初始發現節點,用來給新添加的節點進行詢問加入集羣
discovery.zen.ping.unicast.hosts: ["bigdata121"]

修改Linux一些內核參數

vim /etc/security/limits.conf
添加如下內容:
Es硬性要求打開最小數目最小爲65536,進程數最小爲4096,否則無法啓動
* soft nofile 65536
* hard nofile 131072
* soft nproc 4096
* hard nproc 4096

vim /etc/security/limits.d/20-nproc.conf 
* soft nproc 1024
#修改爲
* soft nproc 4096
這些內核參數需要重啓才生效

vim /etc/sysctl.conf 
添加下面配置:
vm.max_map_count=655360
並執行命令:
sysctl -p

創建es的數據目錄以及日誌目錄

mkdir /opt/modules/elasticsearch-6.6.2/{logs,data}   

啓動es服務

bin/elasticsearch -d
-d 表示以後臺進程服務的方式啓動,不加此選項就以前臺進程方式啓動

測試es

es會啓動兩個對外端口:
9200:restful api的端口
9300:java api端口

可以直接使用curl訪問9200端口
curl http://bigdata121:9200
{
  "name" : "bigdata121",
  "cluster_name" : "my-application",
  "cluster_uuid" : "DM6wmLzsQv2xVDkLMBJzOQ",
  "version" : {
    "number" : "6.6.2",
    "build_flavor" : "default",
    "build_type" : "tar",
    "build_hash" : "3bd3e59",
    "build_date" : "2019-03-06T15:16:26.864148Z",
    "build_snapshot" : false,
    "lucene_version" : "7.6.0",
    "minimum_wire_compatibility_version" : "5.6.0",
    "minimum_index_compatibility_version" : "5.0.0"
  },
  "tagline" : "You Know, for Search"
}
這樣就正常了

2.2 多節點部署

2.2.1 es集羣節點類型

master node:master 節點主要用於元數據(metadata)的處理,比如索引的新增、刪除、分片分配等。
data node:data 節點上保存了數據分片。它負責數據相關操作,比如分片的 CRUD,以及搜索和整合操作。這些操作都比較消耗 CPU、內存和 I/O 資源;
client node:client 節點起到路由請求的作用,實際上可以看做負載均衡器。

那麼這三種節點該如何配置,例子:

# 配置文件中給出了三種配置高性能集羣拓撲結構的模式,如下: 
# 1. 如果你想讓節點從不選舉爲主節點,只用來存儲數據,可作爲負載器 
# node.master: false 
# node.data: true 

# 2. 如果想讓節點成爲主節點,且不存儲任何數據,並保有空閒資源,可作爲協調器
# node.master: true
# node.data: false

# 3. 如果想讓節點既不成爲主節點,又不成爲數據節點,那麼可將他作爲搜索器,從節點中獲取數據,生成搜索結果等 
# node.master: false 
# node.data: false

# 4. 節點是數據節點,也是master節點,這是默認配置
# node.master: true
# node.data: true

2.2.2 es集羣常用部署方案

1、默認情況下,一個節點是數據節點,也是master節點。對於3-5個節點的小集羣來講,通常讓所有節點存儲數據和具有獲得主節點的資格。你可以將任何請求發送給任何節點,並且由於所有節點都具有集羣狀態的副本,它們知道如何路由請求。多個master的元數據也會同步,不用擔心不一致。要注意,master節點的數量最好最少爲3,且爲單數

2、當集羣節點數量比較大時,那麼通常就會將主節點、數據節點分開,專門部署在對應的節點上,然後主節點是多個都可用的,形成HA的結構。要注意,master節點的數量最好最少爲3,且爲單數

實際部署其實和單節點差不多,主要看部署的方案選哪個,master有幾個,數據節點有幾個,設置下角色即可,這裏不多說

2.3 安裝head插件

用qq瀏覽器或者chrome,直接到應用商店搜索elasticsearch-head,直接安裝插件即可

三、java api操作ES

3.1 基本操作

3.1.1 maven依賴準備

<dependencies>
<dependency>
            <groupId>org.elasticsearch</groupId>
            <artifactId>elasticsearch</artifactId>
            <version>6.6.2</version>
        </dependency>

        <dependency>
            <groupId>org.elasticsearch.client</groupId>
            <artifactId>transport</artifactId>
            <version>6.6.2</version>
        </dependency>

        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-core</artifactId>
            <version>2.9.0</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
        </dependency>
</dependencies>

另外需要自己添加一個log4j2的日誌格式配置文件,添加到resource目錄下
log4j2.xml
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="warn">
    <Appenders>
        <Console name="Console" target="SYSTEM_OUT">
            <PatternLayout pattern="%m%n"/>
        </Console>
    </Appenders>
    <Loggers>
        <Root level="INFO">
            <AppenderRef ref="Console"/>
        </Root>
    </Loggers>
</Configuration>

下面代碼中使用 junit進行運行測試,不會用的自己百度

3.1.2 創建ES連接操作對象

public class ESDemo1 {
    private TransportClient client;

    @Before
    public void getClient() throws UnknownHostException {
        //1、創建es配置對象
        Settings settings = Settings.builder().put("cluster.name", "my-application").build();

        //2、連接es集羣
        client = new PreBuiltTransportClient(settings);
        //配置es集羣地址
        client.addTransportAddress(new TransportAddress(
                InetAddress.getByName("192.168.50.121"),
                9300
        ));

        System.out.println(client.toString());

    }
}

3.1.3 索引操作

// .get() 表示觸發操作
@Test
    public void createBlog() {
        //創建索引blog
        //創建index需要admin用戶
        client.admin().indices().prepareCreate("blog").get();
        client.close();
    }

//刪除索引
    @Test
    public void deleteIndex() {
        client.admin().indices().prepareDelete("blog").get();
        client.close();
    }

3.1.4 添加doc

@Test
    public void addDocument() {
        //1、json方式添加document
        String d = "{\"id\":1, \"name\":\"山海經\"}";

        //導入document,並指定源的格式爲 json.
        IndexResponse indexResponse = client.prepareIndex("blog", "article").setSource(d, XContentType.JSON).execute().actionGet();

        System.out.println(indexResponse.getId());
        client.close();
    }

    @Test
    public void addDocument2() throws IOException {
        //2、另外一種方式添加document
        IndexResponse indexResponse = client.prepareIndex("blog3", "article")
                .setSource(XContentFactory.jsonBuilder()
                        .startObject()
                        .field("name","靜夜思")
                        .field("id",4)
                        .endObject()
                ).execute().actionGet();
        System.out.println(indexResponse.getResult());
        client.close();
    }

    @Test
    public void addDocument3() throws IOException {
        //3、通過hashmap組織數據
        HashMap<String, Object> json = new HashMap<>();
        json.put("name","spark從入門到放棄");
        json.put("id","6");

        IndexResponse indexResponse = client.prepareIndex("blog", "article")
                .setSource(json).execute().actionGet();
        System.out.println(indexResponse.getResult());
        client.close();
    }

    @Test
    public void addMoreDocument() throws IOException {
        //4、一次請求內部添加多個document
        BulkRequestBuilder bulkRequestBuilder = client.prepareBulk();
        bulkRequestBuilder.add(
                client.prepareIndex("blog2", "comment").setSource(
                    XContentFactory.jsonBuilder()
                    .startObject()
                    .field("name", "山海經")
                    .field("id",1)
                    .field("commentValue","這是一部很好的作品")
                    .endObject())
        );

        bulkRequestBuilder.add(
                client.prepareIndex("blog2", "comment").setSource(
                    XContentFactory.jsonBuilder()
                    .startObject()
                    .field("name", "駱駝祥子")
                    .field("id",2)
                    .field("commentValue","這是講一個人的故事")
                    .endObject())
        );
        BulkResponse bulkItemResponses = bulkRequestBuilder.get();
        System.out.println(bulkItemResponses);
        client.close();
    }

要注意的是,從6.x版本開始,一個index中只能有一個type了,如果創建多個type會有以下報錯

Rejecting mapping update to [blog] as the final mapping would have more than 1

3.1.5 搜索doc

根據docid搜索document
//搜索單個document
    @Test
    public void getType() {
        GetResponse documentFields = client.prepareGet().setIndex("blog3").setType("article").setId("2OlH9WwBaToKuF8JhwB5").get();
        System.out.println(documentFields.getSourceAsString());
        client.close();
    }

//查詢多個doc
    @Test
    public void getDocFromMoreIndex() {
        MultiGetResponse multiGetResponse = client.prepareMultiGet()
                .add("blog", "article", "1")
                .add("blog", "article", "2")
                .get();
        //結果打印
        for (MultiGetItemResponse itemResponse : multiGetResponse) {
            System.out.println( itemResponse.getResponse().getSourceAsString());
        }
        client.close();
    }

3.1.6 更新doc

@Test
    public void updateData() throws IOException {
        //更新數據方式1:通過 prepareupdate方法
        UpdateResponse updateResponse = client.prepareUpdate("blog", "article", "4")
                .setDoc(XContentFactory.jsonBuilder()
                        .startObject()
                        .field("name", "天黑")
                        .field("id", "5")
                        .endObject()
                ).get();
        System.out.println(updateResponse.getResult());
    }

    @Test
    public void updateData2() throws IOException, ExecutionException, InterruptedException {
        //更新數據方式2:通過update方法
        UpdateRequest updateRequest = new UpdateRequest().index("blog").type("article").id("4");
        updateRequest.doc(XContentFactory.jsonBuilder()
                .startObject()
                .field("name", "亞瑟")
                .field("id", "7")
                .endObject());
        UpdateResponse updateResponse = client.update(updateRequest).get();
        System.out.println(updateResponse.getResult());
    }

    @Test
    public void upsertData() throws IOException, ExecutionException, InterruptedException {
        //指定doc不存在時就插入,存在就修改
        //不存在就插入這個
        IndexRequest indexRequest = new IndexRequest("blog","article","6").source(
                XContentFactory.jsonBuilder().startObject()
                .field("name","wang")
                .field("id","10")
                .endObject()
        );

        //存在就更新這個,注意最後的那個 upsert操作,意思就是不存在就插入上面的 indexrequest
        UpdateRequest updateRequest = new UpdateRequest().index("blog").type("article").id("6");
        updateRequest.doc(XContentFactory.jsonBuilder()
                .startObject()
                .field("name", "king")
                .field("id", "7")
                .endObject()).upsert(indexRequest);

        UpdateResponse updateResponse = client.update(updateRequest).get();
        System.out.println(updateResponse.getResult());
        client.close();
    }

3.1.7 刪除doc

@Test
    public void deleteDocument() {
        //刪除document
        DeleteResponse deleteResponse = client.prepareDelete("blog", "article", "6").get();
        System.out.println(deleteResponse.getResult());
        client.close();
    }

3.2 條件查詢doc

關鍵性一個類是 org.elasticsearch.index.query.QueryBuilders;

3.2.1 查詢指定index所有doc

@Test
    public void matchAll() {
    //構建全部查詢
        SearchResponse searchResponse = client.prepareSearch("blog").setQuery(QueryBuilders.matchAllQuery()).get();
        //從返回結構中解析doc
        SearchHits hits = searchResponse.getHits();
        for (SearchHit hit:hits){
            System.out.println(hit.getSourceAsString());
        }
        client.close();
    }

3.2.2 全部字段進行全文檢索

搜索全部字段中包含指定字符的document
@Test
    public void matchSome() {
        //直接全文檢索指定字符
        SearchResponse searchResponse = client.prepareSearch("blog3").setQuery(QueryBuilders.queryStringQuery("思")).get();

        SearchHits hits = searchResponse.getHits();
        for(SearchHit hit:hits) {
            System.out.println(hit.getId());
            System.out.println();
        }
    }

3.2.3 通配符字段全文檢索

@Test
    public void wildMatch() {
        //通配符查詢,*表示0或者多個字符,?表示單個字符
        SearchResponse searchResponse = client.prepareSearch("blog").setTypes("article").setQuery(QueryBuilders.wildcardQuery("name", "wa*")).get();
        SearchHits hits = searchResponse.getHits();

        for(SearchHit h:hits) {
            System.out.println(h.getSourceAsString());
        }

    }

這個方法用於匹配某個字段的整個內容,類似like操作

3.2.4 對指定字段進行分詞搜索

@Test
    public void matchField() {
        //這是對分詞結果進行等值操作的方法,不是對整個字段,而是對字段的分詞結果
        SearchResponse searchResponse = client.prepareSearch("blog").setQuery(QueryBuilders.termQuery("name", "山")).get();
        SearchHits hits = searchResponse.getHits();
        for(SearchHit hit:hits) {
            System.out.println(hit.getSourceAsString());
        }
        client.close();
    }

這個方法一定要注意:
比如有一個字段內容如下: 我愛中國
假設分詞如下: 我 愛  中國
如果使用 QueryBuilders.termQuery("name", "中") 也就是搜索“中”這個字時,實際上沒有結果返回的。因爲分詞中並沒有含有單獨的“中”。
所以這個方法是用於完整匹配分詞結果中的某個分詞的。
由此,可以得出,即便是用整個字段的內容來搜索,這個方法也不會返回任何結果的,因爲分詞結果不包含。

3.2.5對指定字段進行模糊檢索

@Test
public void fuzzy() {

// 1 模糊查詢
SearchResponse searchResponse = client.prepareSearch("blog").setTypes("article")
.setQuery(QueryBuilders.fuzzyQuery("title", "lucene")).get();

// 2 打印查詢結果
SearchHits hits = searchResponse.getHits(); // 獲取命中次數,查詢結果有多少對象

for(SearchHit hit:hits) {
System.out.println(hit.getSourceAsString());
}

// 3 關閉連接
client.close();
}

這個方法和 termQuery很類似,但是有區別。感興趣的話可以自己查找資料。這個方法比較少用

3.3 映射mapping

3.3.1 mapping的定義

​ 映射是規定index中的一些屬性,以及各自type下的字段的屬性(再強調一遍,現在6.x版本一個index下只能有一個type,其實就是變相地去除掉了type)。Elasticsearch映射雖然有idnex和type兩層關係,但是實際索引時是以index爲基礎的。如果同一個index下不同type的字段出現mapping不一致的情況,雖然數據依然可以成功寫入並生成各自的mapping,但實際上fielddata中的索引結果卻依然是以index內第一個mapping類型來生成的

3.3.2 mapping的寫法

定義mapping時,依舊是使用json格式定義定義。一般格式如下:

{
    元數據屬性字段,如:
    _type:是哪個type的mapping,還是那句話,type基本不怎麼提了
    _index:屬於哪個index
    。。。。。。。
    properties:{
        "field1":{
            字段屬性字段,如:
            type:字段數據類型
        }
         "field2":{
            字段屬性字段,如:
            type:字段數據類型
        }
         。。。。。。。。。
    }

}

基本格式就是這樣,分爲兩大部分,一個是整個index 的元數據信息,一個是針對具體type中的字段信息。

3.3.3 數據類型

核心數據類型
字符串:text,keyword
數字:long, integer, short, byte, double, float, half_float, scaled_float
布爾值:boolean
時間:date
二進制:binary
範圍:integer_range, float_range, long_range, double_range, date_range

複雜數據類型
數組:array
對象:object
堆疊/嵌套對象: nested
地理:geo_point,geo_point
IP: ip
字符個數:token_count(輸入一個字符串,保存的是它的長度)

3.3.4mapping屬性字段

元數據字段:

_all : 它是文檔中所有字段的值整合成的一個大字符串,用空格分割。它進行了索引但沒有存儲,所以我們只能對他進行搜索不能獲取。如果我們沒有指定搜索的字段,就默認是在_all字段上進行搜索。

_source :文檔信息 
包含在文檔在創建時的實際主體,它會被存儲但不會被索引,用於get或search是返回主體。如果你並不關係數據的主體,只注重數量,那可以將此字段禁用

_routing :路由字段 
es會使用下面的計算公式計算數據應保存在哪個分片,索引指定一個路由字段可以自己來控制哪些值放在一起。

shard_num = hash(_routing) % num_primary_shards

_meta 自定義的元數據 ,因爲元數據是每個文檔都會帶的,索引如果你想要在每個文檔上標註一些信息,就可以使用此屬性,自定義一些元數據。

_field_names :保存着非空值得屬性名集合,可以通過它查詢包含某個字段非空值的文檔

_id :主鍵
_index :索引
_type :類型
_uid :類型和id的組合 uid字段的值可以在查詢、聚合、腳本和排序中訪問:
_parent :父類,可用於關聯兩個索引

字段屬性:

type 數據類型 
改屬性用來指定字段的數據類型,一但指點後就不能再修改,如果數據不是以設置的數據類型傳入,es會去轉換數據,裝換不成功則報錯。具體可配置的參數,可看前面的數據類型說明。

analyzer 分析器 
用於指定索引創建時使用的分析器是什麼,即對同一段內容,不同的分析器會用不同的方式分詞,最後在倒排索引上的值是不同的。

index 是否索引
索引選項控制字段值是否被索引。它接受true或false,默認爲true。沒有索引的字段不是可查詢的。

store 
屬性值是否被存儲,默認情況下字段是可以被搜索但是內容不存儲的,值一般都是保存在_source中。但比如一篇文章你有它的內容和原網址,現在需要對內容進行檢索,但查看是跳轉到它原網址的,那這時就不需要存儲內容了。

fielddata 現場數據 
如果你要對一個text類型進行聚合操作,你必須設置這個參數爲true。

doc_values 文檔數據 
建立一個文檔對應字段的“正排索引”,其實就是把文檔的字段按列存儲了,它不會保存分析的字段。方便聚合排序時訪問。
format 默認格式 
一般用於時間格式的數據,指定默認的數據格式, “yyyy-MM-dd HH:mm:ss”

search_analyzer 搜索分析器 
指定搜索時使用的分析器,一般不設置在搜索時就會使用創建索引時使用的分析器,如果要自己指定不同的也只要配置即可。

boost 分值 
指定字段的相關性評分默認是1.0,數值越大,搜索時排序時使用。也可以直接通過查詢時指定分值的方式

coerce 是否轉換 
在插入數據時,在插入數據類型和映射類型不一致的情況下是否強制轉換數據類型。默認是開啓的

normalizer 轉換器 
因爲keyword類型的字段是不進行分析的,但是我們又想要將其統一成一個規則,比如都是小寫,比如用ASCILL進行編碼,其實就是個給keyword用的分析器。可以在setting下的analysis下定義自己的normalizer使用

copy_to 同步複製 
在插入值是,會把值一同放到另一個字段中。主要用於自己定義一個類似於_all字段的字端。

dynamic 動態映射控制 
該字段是用來控制動態映射的,它有三個值 
-true-自動添加映射 
-false-新值不索引,不能被搜索,但返回的命中源字段中會存在這個值 
-strict-遇到新值拋出異常

enabled 是否啓動 
這個值是否要用於搜索

ignore_above 忽視上限 
一個字符串超過指定長度後就不會索引了

ignore_malformed 忽視錯誤數據 
比如一個文檔數據傳過來,只用一個字段的數據時不能被存儲,ES會拋出異常並且不會存儲此數據。我們就可以配置此屬性保證數據被存儲

include_in_all 是否保存在_all字段中

fields 多字段配置 
比如出現標題既要索引,又有不用索引的情景。我們不能對一個字段設置兩個類型,又不想再建一個不同類型的相同字段。我們可以使用多字段的方式,在保存數據時,我們只需保存一個字段,ES會默認將數據保存到這個字段下的多字段上。

null_value 空值 
假如你插入的數據爲空,或者數據中沒有這個字段的值。那這個文檔的這個字段就不參與搜索了。我們可以通過指定一個顯示的空值來讓他能夠參與搜索

norms 規範 
如果一個字段只用於聚合,可以設置爲false

3.3.5 _all,_sources,store的區別

背景:
    首先,我們要知道一點,當doc傳入es時,es會根據配置給doc的每個字段生成索引,並且會將生成的索引保存到es中。但是至於doc的原始數據是否保存到es中,是可以選擇的。這點要先搞清楚,並一定非得把doc的原始數據保存在es中的,es非保存不可的是生成的索引,而不是原始數據

========================
_all:
這是一個特殊字段,是把所有其它字段中的值,以空格爲分隔符組成一個大字符串,然後被分析和索引,但是不存儲原始數據,也就是說它能被查詢,但不能被取回顯示。注意這個字段是可以被索引的。默認情況下,如果要進行全文檢索,需要指定在哪個字段上檢索,如果不知道在哪個字段上,那麼_all就起到作用了。_all能讓你在不知道要查找的內容是屬於哪個具體字段的情況下進行搜索

======================
_source: true/false,默認爲true
保存的是doc的本來的原數數據,也就是是json格式的doc。他和_all不同,他是json格式的字符串。而且這個字段不會被索引。
當我們執行檢索操作時,是到倒排索引中查詢,然後獲得含有指定關鍵字的doc的id,
當 _source 設置爲 true時
可以根據上面查詢到的docid,返回對應id的document的原始數據。
當  _source 設置爲 false時
就只能返回對應的document的id,無法回顯對應document的原始數據
這種情況下,一般是使用額外的方式來保存document的原始數據的,比如hbase。而es就單純保存索引而已

=======================
store:true/false,默認爲false
這個屬性用於指定是否保存document中對應字段的value,這個的概念和上面的source有點類似了,只不過這裏store是針對某個field的原始數據,source是針對整個document的原始數據。

當執行想獲取一個document的數據時,
1、採用source方式時:
只需產生一次磁盤IO,因爲_source存儲的時候,直接把整個doc當做一個字段來存儲。當我們需要doc中的某個字段時,是先從source讀取數據,然後再解析成json,獲取到指定字段內容
2、採用store方式時,
因爲每個字段都單獨存儲了,當需要獲得整個doc的數據時,就需要單獨每個字段進行取值,有多少個字段就產生多少次磁盤IO。
3、store和source混合使用時
如果操作是獲取整個doc的數據,那麼es會優先從source讀取數據。
如果操作是獲取某些字段的數據,那麼es會優先從store存儲中讀取數據。因爲這樣讀取的數據量相對較少,無需讀取整個doc的數據再解析。
但是注意的是,這兩個屬性都是單獨自己保存數據的,所以如果兩個啓用的話,相當於數據存儲了兩次,挺浪費存儲空間的,增大了索引的體積

3.3.6 api操作mapping

創建mapping,要注意,mapping創建之後不能更改

@Test
    public void createMapping() throws Exception {

        // 1設置mapping,使用jsonbuilder構建mapping
        XContentBuilder builder = XContentFactory.jsonBuilder()
                .startObject()
                    .startObject("article")
                        .startObject("properties")
                            .startObject("id1")
                                .field("type", "string")
                                .field("store", "yes")
                            .endObject()
                            .startObject("title2")
                                .field("type", "string")
                                .field("store", "no")
                            .endObject()
                            .startObject("content")
                                .field("type", "string")
                                .field("store", "yes")
                            .endObject()
                        .endObject()
                    .endObject()
                .endObject();

        // 2 添加mapping
        PutMappingRequest mapping = Requests.putMappingRequest("blog4").type("article").source(builder);

        client.admin().indices().putMapping(mapping).get();

        // 3 關閉資源
        client.close();
    }

查看map

@Test
    public void getIndexMapping() throws ExecutionException, InterruptedException {
       //構建查看mapping的請求,查看blog3這個index的mapping
        GetMappingsResponse mappingsResponse = client.admin().indices().getMappings(new GetMappingsRequest().indices("blog3")).get();
        //獲取mapping
        ImmutableOpenMap<String, ImmutableOpenMap<String, MappingMetaData>> mappings = mappingsResponse.getMappings();
        //迭代打印mapping數據
        for (ObjectObjectCursor<String, ImmutableOpenMap<String, MappingMetaData>> mapping : mappings) {
            if (mapping.value.isEmpty()) {
                continue;
            }
            //最外層的key是index的名稱
            System.out.println("index key:" + mapping.key);
            //value包裹的是每個type的mapping,裏面以type爲key,mapping爲value
            for (ObjectObjectCursor<String, MappingMetaData> mapValue : mapping.value) {
                System.out.println("type key:" + mapValue.key);
                System.out.println("type value:" + mapValue.value.sourceAsMap());

            }
        }
        client.close();
    }

/*
結果如下:
index key:blog3
type key:article
type value:{_source={enabled=false}, properties={id={type=long}, name={type=text, fields={keyword={type=keyword, ignore_above=256}}}}}
*/

3.4 spark操作es時的報錯

在spark.2.1和es6.6項目中混合使用,報錯:

java.lang.NoSuchMethodError: io.netty.buffer.ByteBuf.retainedSlice(II)Lio/netty/buffer/ByteBuf;

這種問題,一般都是使用的某個依賴包的版本問題。使用mvn dependency:tree 看了下,原來spark和es各自依賴的版本不一致,spark使用的是3.x版本,es使用的是4.1.32.Final版本。但是因爲spark的依賴在pom.xml中寫在前面,迫使es使用的是3.x版本的依賴,導致有些方法不存在,就報錯。解決方式很簡答,直接指定使用新版本的就好,如下:

<dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-all</artifactId>
            <version>4.1.32.Final</version>
        </dependency>

四、分詞器

4.1 默認分詞器

我們知道,建立索引過程中,最重要的一個步驟就是分詞,分詞的策略有很多,我們看看es默認的中文分詞器的效果

[root@bigdata121 elasticsearch-6.6.2]# curl -H "Content-Type:application/json" -XGET 'http://bigdata121:9200/_analyze?pretty' -d '{"analyzer":"standard","text":"中華人民共和國"}' 
{
  "tokens" : [
    {
      "token" : "中",
      "start_offset" : 0,
      "end_offset" : 1,
      "type" : "<IDEOGRAPHIC>",
      "position" : 0
    },
    {
      "token" : "華",
      "start_offset" : 1,
      "end_offset" : 2,
      "type" : "<IDEOGRAPHIC>",
      "position" : 1
    },
    {
      "token" : "人",
      "start_offset" : 2,
      "end_offset" : 3,
      "type" : "<IDEOGRAPHIC>",
      "position" : 2
    },
    {
      "token" : "民",
      "start_offset" : 3,
      "end_offset" : 4,
      "type" : "<IDEOGRAPHIC>",
      "position" : 3
    },
    {
      "token" : "共",
      "start_offset" : 4,
      "end_offset" : 5,
      "type" : "<IDEOGRAPHIC>",
      "position" : 4
    },
    {
      "token" : "和",
      "start_offset" : 5,
      "end_offset" : 6,
      "type" : "<IDEOGRAPHIC>",
      "position" : 5
    },
    {
      "token" : "國",
      "start_offset" : 6,
      "end_offset" : 7,
      "type" : "<IDEOGRAPHIC>",
      "position" : 6
    }
  ]
}

可以看到,標準的中文分詞器只是單純將字分開,其實並不智能,沒有詞語考慮進去。所以需要更加強大的分詞器。常用的有ik分詞器

4.2 安裝ik分詞器

cd /opt/modules/elasticsearch-6.6.2
執行下面的命令安裝,需要聯網
bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v6.6.2/elasticsearch-analysis-ik-6.6.2.zip

注意要根據ES的版本安裝對應版本的ik

4.3 命令行下IK分詞器的使用

分兩種模式:ik_smart 和 ik_max_word

1、 ik_smart  模式,智能解析詞語結構
curl -H "Content-Type:application/json" -XGET 'http://bigdata121:9200/_analyze?pretty' -d '{"analyzer":"ik_smart","text":"中華人民共和國"}'

{
  "tokens" : [
    {
      "token" : "中華人民共和國",
      "start_offset" : 0,
      "end_offset" : 7,
      "type" : "CN_WORD",
      "position" : 0
    }
  ]
}

2、ik_max_word 模式,智能解析字和詞語
curl -H "Content-Type:application/json" -XGET 'http://192.168.109.133:9200/_analyze?pretty' -d '{"analyzer":"ik_max_word","text":"中華人民共和國"}'

{
  "tokens" : [
    {
      "token" : "中華人民共和國",
      "start_offset" : 0,
      "end_offset" : 7,
      "type" : "CN_WORD",
      "position" : 0
    },
    {
      "token" : "中華人民",
      "start_offset" : 0,
      "end_offset" : 4,
      "type" : "CN_WORD",
      "position" : 1
    },
    {
      "token" : "中華",
      "start_offset" : 0,
      "end_offset" : 2,
      "type" : "CN_WORD",
      "position" : 2
    },
    {
      "token" : "華人",
      "start_offset" : 1,
      "end_offset" : 3,
      "type" : "CN_WORD",
      "position" : 3
    },
    {
      "token" : "人民共和國",
      "start_offset" : 2,
      "end_offset" : 7,
      "type" : "CN_WORD",
      "position" : 4
    },
    {
      "token" : "人民",
      "start_offset" : 2,
      "end_offset" : 4,
      "type" : "CN_WORD",
      "position" : 5
    },
    {
      "token" : "共和國",
      "start_offset" : 4,
      "end_offset" : 7,
      "type" : "CN_WORD",
      "position" : 6
    },
    {
      "token" : "共和",
      "start_offset" : 4,
      "end_offset" : 6,
      "type" : "CN_WORD",
      "position" : 7
    },
    {
      "token" : "國",
      "start_offset" : 6,
      "end_offset" : 7,
      "type" : "CN_CHAR",
      "position" : 8
    }
  ]
}

4.4 java api使用ik分詞器

這裏其實和mapping的使用差不多,只是在mapping的字段屬性中添加一個 “analyzer” 屬性,指定使用的分詞器而已。其他都沒有區別,這裏不重複

五、優化

5.1 背景

ES在數十億級別的數據如何提高檢索效率?
​ 這個問題說白了,就是看你有沒有實際用過 ES,因爲啥?其實 ES 性能並沒有你想象中那麼好的。很多時候數據量大了,特別是有幾億條數據的時候,可能你會懵逼的發現,跑個搜索怎麼一下 5~10s,坑爹了。第一次搜索的時候,是 5~10s,後面反而就快了,可能就幾百毫秒。
​ 然後你就很懵,每個用戶第一次訪問都會比較慢,比較卡麼?所以你要是沒玩兒過 ES,或者就是自己玩玩兒 Demo,被問到這個問題容易懵逼,顯示出你對 ES 確實玩的不怎麼樣?說實話,ES 性能優化是沒有銀彈的。啥意思呢?就是不要期待着隨手調一個參數,就可以萬能的應對所有的性能慢的場景。也許有的場景是你換個參數,或者調整一下語法,就可以搞定,但是絕對不是所有場景都可以這樣。
​ 下面看看幾個優化的手段

5.2 優化1--filesystemCache

5.2.1 基本原理

一、Elasticsearch原理與基本使用

​ 圖5.1 ES filesytem cache

​ 你往 ES 裏寫的數據,實際上都寫到磁盤文件裏去了,查詢的時候,操作系統會將磁盤文件裏的數據自動緩存到 Filesystem Cache 裏面去。ES 的搜索引擎嚴重依賴於底層的 Filesystem Cache,你如果給 Filesystem Cache 更多的內存,儘量讓內存可以容納所有的 IDX Segment File 索引數據文件,那麼你搜索的時候就基本都是走內存的,性能會非常高。

問題:直接讀取硬盤數據和從緩存讀取數據,性能差距究竟可以有多大?
回答:
我們之前很多的測試和壓測,如果走磁盤一般肯定上秒,搜索性能絕對是秒級別的,1 秒、5 秒、10 秒。但如果是走 Filesystem Cache,是走純內存的,那麼一般來說性能比走磁盤要高一個數量級,基本上就是毫秒級的,從幾毫秒到幾百毫秒不等。

案例:
​ 來看一個真實的案例:某個公司 ES 節點有 3 臺機器,每臺機器看起來內存很多 64G,總內存就是 64 3 = 192G。每臺機器給 ES JVM Heap 是 32G,那麼剩下來留給 Filesystem Cache 的就是每臺機器才 32G,總共集羣裏給 Filesystem Cache 的就是 32 3 = 96G 內存。
​ 而此時,整個磁盤上索引數據文件,在 3 臺機器上一共佔用了 1T 的磁盤容量,ES 數據量是 1T,那麼每臺機器的數據量是 300G。這樣性能會好嗎?
​ Filesystem Cache 的內存才 100G,十分之一的數據可以放內存,其他的都在磁盤,然後你執行搜索操作,大部分操作都是走磁盤,性能肯定差。

5.2.2 優化具體方式

​ 首先要知道一點:歸根結底,你要讓 ES 性能好,最佳的情況下,就是你的機器的內存,至少可以容納你的總數據量的一半。當然如果內存能容納全部數據,自然是最好,然而基本生產中沒有那麼多錢的啦。走內存可以滿足秒級以內的查詢要求

1、去掉寫入ES的doc中不必要的字段
如果一個doc中有很多字段,但是有些字段壓根是沒用的(也就是說該字段不會用於搜索),但是讀取的時候仍舊會將這些字段都讀取,然後緩存到filesytem cache中,佔據了大量空間,導致後面的數據只能重新從硬盤中讀取。這個時候就要想着取消一些沒怎麼用的字段了。減小索引的體積。從而節省filesytem cache空間

2、採用 ES+HBase架構
​ 之前也說到,es可以只存儲索引,不存儲原始doc數據;或者只存儲某些字段的原始數據。通常完整的原始數據都保存在hbase中,然後通過rowkey作爲docid導入到es中,最終通過這個rowkey進行唯一性關聯。爲什麼要採用這種架構呢?
​ 比如說你現在有一行數據:id,name,age .... 30 個字段。但是你現在搜索,只需要根據 id,name,age 三個字段來搜索。如果你傻乎乎往 ES 裏寫入一行數據所有的字段,就會導致 90% 的數據是不用來搜索的。但是呢,這些數據硬是佔據了 ES 機器上的 Filesystem Cache 的空間,單條數據的數據量越大,就會導致 Filesystem Cahce 能緩存的數據就越少。其實,僅僅寫入 ES 中要用來檢索的少數幾個字段就可以了,比如說就寫入 es id,name,age 三個字段。然後你可以把其他的字段數據存在 MySQL/HBase 裏,我們一般是建議用 ES + HBase 這麼一個架構(官方建議的方案)。
​ HBase是列式數據庫,其特點是適用於海量數據的在線存儲,就是對 HBase 可以寫入海量數據,但是不要做複雜的搜索,做很簡單的一些根據 id 或者範圍進行查詢的這麼一個操作就可以了。hbase非常適合這種簡單通過key直接獲取數據的應用場景。
​ 例如:從 ES 中根據 name 和 age 去搜索,拿到的結果可能就 20 個 doc id,然後根據 doc id 到 HBase 裏去查詢每個 doc id 對應的完整的數據,給查出來,再返回給前端。而寫入 ES 的數據最好小於等於,或者是略微大於 ES 的 Filesystem Cache 的內存容量。然後你從 ES 檢索可能就花費 20ms,然後再根據 ES 返回的 id 去 HBase 裏查詢,查 20 條數據,可能也就耗費個 30ms。如果你像原來那麼玩兒,1T 數據都放 ES,可能會每次查詢都是 5~10s,而現在性能就會很高,每次查詢就是 50ms。

5.3 優化2--數據預熱

​ 從概率上來說,大部分的訪問量往往集中小部分的數據上,也就是我們所說的數據熱點的情況。數據預熱通常就是事先將一些可能有大量訪問的數據先通過手動訪問讓它們提前緩存到cache中,然而後面的用戶訪問這些數據時,就直接走cache查詢了,非常快。而且這些數據因爲訪問量多,所以還需要保證這些熱點數據不要被其他非熱點數據加載到cache時,被覆蓋掉了。這就需要時常手動訪問,加載數據到cache中。
​ 例子:
​ 比如電商,你可以將平時查看最多的一些商品,比如說 iPhone 8,熱數據提前後臺搞個程序,每隔 1 分鐘自己主動訪問一次,刷到 Filesystem Cache 裏去。
​ 總之,就是對於那些你覺得比較熱的、經常會有人訪問的數據,最好做一個專門的緩存預熱子系統。然後對熱數據每隔一段時間,就提前訪問一下,讓數據進入 Filesystem Cache 裏面去。這樣下次別人訪問的時候,性能一定會好很多。

5.4 優化3--冷熱分離

​ 這個也是數據熱點的問題。ES 可以做類似於 MySQL 的水平拆分,就是說將大量的訪問很少、頻率很低的數據,單獨寫一個索引,然後將訪問很頻繁的熱數據單獨寫一個索引。最好是將冷數據寫入一個索引中,然後熱數據寫入另外一個索引中,這樣可以確保熱數據在被預熱之後,儘量都讓他們留在 Filesystem OS Cache 裏,別讓冷數據給沖刷掉。
​ 還是來一個例子,假設你有 6 臺機器,2 個索引,一個放冷數據,一個放熱數據,每個索引 3 個 Shard。3 臺機器放熱數據 Index,另外 3 臺機器放冷數據 Index。這樣的話,你大量的時間是在訪問熱數據 Index,熱數據可能就佔總數據量的 10%,此時數據量很少,幾乎全都保留在 Filesystem Cache 裏面了,就可以確保熱數據的訪問性能是很高的。
​ 但是對於冷數據而言,是在別的 Index 裏的,跟熱數據 Index 不在相同的機器上,大家互相之間都沒什麼聯繫了。如果有人訪問冷數據,可能大量數據是在磁盤上的,此時性能差點,就 10% 的人去訪問冷數據,90% 的人在訪問熱數據,也無所謂了。

5.5 優化4--避免關聯查詢

​ 對於 MySQL,我們經常有一些複雜的關聯查詢,在 ES 裏該怎麼玩兒?ES 裏面的複雜的關聯查詢儘量別用,一旦用了性能一般都不太好。最好是先在 Java 系統裏就完成關聯,將關聯好的數據直接寫入 ES 中。搜索的時候,就不需要利用 ES 的搜索語法來完成 Join 之類的關聯搜索了。

5.6 優化5--document模型設計

​ Document 模型設計是非常重要的,很多操作,不要在搜索的時候纔想去執行各種複雜的亂七八糟的操作。
​ ES 能支持的操作就那麼多,不要考慮用 ES 做一些它不好操作的事情。如果真的有那種操作,儘量在 Document 模型設計的時候,寫入的時候就完成。另外對於一些太複雜的操作,比如 join/nested/parent-child 搜索都要儘量避免,性能都很差的。
​ 總結一句就是說,ES不適合執行復雜查詢操作

5.7 優化6--分頁性能優化

背景:

    ES 的分頁是較坑的,爲啥呢?舉個例子吧,假如你每頁是 10 條數據,你現在要查詢第 100 頁,實際上是會把每個 Shard 上存儲的前 1000 條數據都查到一個協調節點上。如果你有 5 個 Shard,那麼就有 5000 條數據,接着協調節點對這 5000 條數據進行一些合併、處理,再獲取到最終第 100 頁的 10 條數據。
    由於是分佈式的,你要查第 100 頁的 10 條數據,不可能說從 5 個 Shard,每個 Shard 就查 2 條數據,最後到協調節點合併成 10 條數據吧?你必須得從每個 Shard 都查 1000 條數據過來,然後根據你的需求進行排序、篩選等等操作,最後再次分頁,拿到裏面第 100 頁的數據。
    也就是說,你翻頁的時候,翻的越深,每個 Shard 返回的數據就越多,而且協調節點處理的時間越長,非常坑爹。所以用 ES 做分頁的時候,你會發現越翻到後面,就越是慢。
    我們之前也是遇到過這個問題,用 ES 作分頁,前幾頁就幾十毫秒,翻到 10 頁或者幾十頁的時候,基本上就要 5~10 秒才能查出來一頁數據了。

解決方案:

1、不允許深度分頁(默認深度分頁性能很差)。跟產品經理說,你係統不允許翻那麼深的頁,默認翻的越深,性能就越差。

2、類似於 App 裏的推薦商品不斷下拉出來一頁一頁的;類似於微博中,下拉刷微博,刷出來一頁一頁的,你可以用 Scroll API,關於如何使用,大家可以自行上網搜索學習一下。
    Scroll是如何做的呢?它會一次性給你生成所有數據的一個快照,然後每次滑動向後翻頁就是通過遊標 scroll_id 移動,獲取下一頁、下一頁這樣子,性能會比上面說的那種分頁性能要高很多很多,基本上都是毫秒級的。
    但是,唯一的一點就是,這個適合於那種類似微博下拉翻頁的,不能隨意跳到任何一頁的場景。也就是說,你不能先進入第 10 頁,然後去第 120 頁,然後又回到第 58 頁,不能隨意亂跳頁。所以現在很多產品,都是不允許你隨意翻頁的,你只能往下拉,一頁一頁的翻。
    使用時需要注意,初始化必須指定 Scroll 參數,告訴 ES 要保存此次搜索的上下文多長時間。你需要確保用戶不會持續不斷翻頁翻幾個小時,否則可能因爲超時而失敗。
    除了用 Scroll API,你也可以用 search_after 來做。search_after 的思想是使用前一頁的結果來幫助檢索下一頁的數據。
    顯然,這種方式也不允許你隨意翻頁,你只能一頁頁往後翻。初始化時,需要使用一個唯一值的字段作爲 Sort 字段。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章