溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

一、Elasticsearch原理與基本使用

發(fā)布時間:2020-02-25 21:48:31 來源:網(wǎng)絡(luò) 閱讀:1117 作者:隔壁小白 欄目:大數(shù)據(jù)

[TOC]

一、Elasticsearch概述

1.1 什么是搜索

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

1.2 使用傳統(tǒng)數(shù)據(jù)庫實現(xiàn)搜索

? 傳統(tǒng)數(shù)據(jù)庫的情況下,如果要查詢某個字段是否包含某些關(guān)鍵字的話,需要使用到like關(guān)鍵字來進(jìn)行字段匹配,很大概率導(dǎo)致全表掃描,本身來說性能就不算好。如果再加上查詢的字段非常長,那么使用like匹配的工作量是很大的,另外如果表的行數(shù)也很多,那么性能就更差了。

1.3 全文檢索與倒排索引

? 全文檢索是指計算機(jī)索引程序通過掃描文章中的每一個詞,對每一個詞建立一個索引,指明該詞在文章中出現(xiàn)的次數(shù)和位置,當(dāng)用戶查詢時,檢索程序就根據(jù)事先建立的索引進(jìn)行查找,并將查找的結(jié)果反饋給用戶的檢索方式。這個過程類似于通過字典中的檢索字表查字的過程。全文搜索引擎數(shù)據(jù)庫中的數(shù)據(jù)。而全文檢索用到的關(guān)鍵技術(shù)就是倒排索引。什么是倒排索引?看看例子就知道了

數(shù)據(jù)庫中有如下數(shù)據(jù)
id  員工描述
1   優(yōu)秀論文
2   優(yōu)秀員工稱號
3   優(yōu)秀項目
4   優(yōu)秀團(tuán)隊

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

2、建立倒排索引
優(yōu)秀  1,2,3,4
論文  1
員工  2
稱號  2
項目  3
團(tuán)隊  4

3、檢索
倒排索引意思簡單就是指定的詞出現(xiàn)在哪些行中,這些行都用唯一id進(jìn)行標(biāo)識。
所以這就是為什么倒排索引用到全文檢索中,因為可以直接查詢到包含相關(guān)關(guān)鍵字的內(nèi)容有哪些。
比如搜索優(yōu)秀,可以看到優(yōu)秀這個詞在1234中都有出現(xiàn),然后根據(jù)id查詢原始數(shù)據(jù)。

? 有了倒排索引,當(dāng)我們需要從很多端很長的內(nèi)容中檢索包含指定關(guān)鍵字的內(nèi)容時,直接根據(jù)倒排索引就知道有沒有指定關(guān)鍵字了。而如果使用傳統(tǒng)數(shù)據(jù)庫,那么必須掃描全部內(nèi)容,如果數(shù)據(jù)有1000行,那工作量就很恐怖了。而倒排索引只是查詢個關(guān)鍵字而已,無需掃描全部內(nèi)容。

1.4 Lucene和Elasticsearch

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

1.5 ES的特點與適用場景

特點:

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

適用場景:

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

1.6 ES中的相關(guān)概念

近實時

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

集群cluster

ES集群可以有多個節(jié)點,但是每個節(jié)點屬于哪個ES集群中是通過配置集群名稱來指定的。當(dāng)然一個集群只有一個節(jié)點也是OK的

節(jié)點node

集群中的一個節(jié)點,節(jié)點也有一個名稱(默認(rèn)是隨機(jī)分配的),節(jié)點名稱很重要(在執(zhí)行運維管理操作的時候),默認(rèn)節(jié)點會去加入一個名稱為“elasticsearch”的集群,如果直接啟動一堆節(jié)點,那么它們會自動組成一個elasticsearch集群,當(dāng)然一個節(jié)點也可以組成一個elasticsearch集群。

index--database

索引包含一堆有相似結(jié)構(gòu)的文檔數(shù)據(jù),比如可以有一個客戶索引,商品分類索引,訂單索引,索引有一個名稱。一個index包含很多document,一個index就代表了一類類似的或者相同的document。比如說建立一個product index,商品索引,里面可能就存放了所有的商品數(shù)據(jù),所有的商品document。類似于傳統(tǒng)數(shù)據(jù)庫中的庫的概念

type--table

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

document--行

    文檔是es中的最小數(shù)據(jù)單元,一個document可以是一條客戶數(shù)據(jù),一條商品分類數(shù)據(jù),一條訂單數(shù)據(jù),通常用JSON數(shù)據(jù)結(jié)構(gòu)表示,每個index下的type中,都可以去存儲多個document。相當(dāng)于行

field--字段

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

mapping--映射約束

    數(shù)據(jù)如何存放到索引對象上,需要有一個映射配置,包括:數(shù)據(jù)類型、是否存儲、是否分詞等。所謂映射是對type的存儲的一些限制。
例子:
    這樣就創(chuàng)建了一個名為blog的Index。Type不用單獨創(chuàng)建,在創(chuàng)建Mapping 時指定就可以。Mapping用來定義Document中每個字段的類型,即所使用的 analyzer、是否索引等屬性。創(chuàng)建Mapping 的代碼示例如下:
client.indices.putMapping({
    index : 'blog',
    type : 'article',
    這里還可以設(shè)置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讀寫數(shù)據(jù)的機(jī)制

寫流程:

1、客戶端根據(jù)提供的es節(jié)點,選擇一個node作為協(xié)調(diào)節(jié)點,并發(fā)送寫請求
2、協(xié)調(diào)節(jié)點對寫入的document進(jìn)行路由,將document進(jìn)行分片。每個分片單獨進(jìn)行寫,每個分片默認(rèn)都是雙備份,寫在不同的節(jié)點上。
3、分片寫入時,主備份由協(xié)調(diào)節(jié)點寫入,副備份則是從主備份所在節(jié)點同步數(shù)據(jù)過去。
4、當(dāng)分片都寫完后,由協(xié)調(diào)節(jié)點返回寫入完成給客戶端

讀流程:

讀流程就很簡單了,如果通過docid來讀取,直接根據(jù)docid進(jìn)行hash。判斷出該doc存儲在哪個節(jié)點上,然后到相應(yīng)節(jié)點上讀取數(shù)據(jù)即可。

1.8 ES數(shù)據(jù)存儲結(jié)構(gòu)

一、Elasticsearch原理與基本使用

? 圖1.1 ES存儲結(jié)構(gòu)

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

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

二、ES部署

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

2.1 單節(jié)點部署

解壓程序到指定目錄:

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

修改配置文件:

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

修改Linux一些內(nèi)核參數(shù)

vim /etc/security/limits.conf
添加如下內(nèi)容:
Es硬性要求打開最小數(shù)目最小為65536,進(jìn)程數(shù)最小為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
這些內(nèi)核參數(shù)需要重啟才生效

vim /etc/sysctl.conf 
添加下面配置:
vm.max_map_count=655360
并執(zhí)行命令:
sysctl -p

創(chuàng)建es的數(shù)據(jù)目錄以及日志目錄

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

啟動es服務(wù)

bin/elasticsearch -d
-d 表示以后臺進(jìn)程服務(wù)的方式啟動,不加此選項就以前臺進(jìn)程方式啟動

測試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 多節(jié)點部署

2.2.1 es集群節(jié)點類型

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

那么這三種節(jié)點該如何配置,例子:

# 配置文件中給出了三種配置高性能集群拓?fù)浣Y(jié)構(gòu)的模式,如下: 
# 1. 如果你想讓節(jié)點從不選舉為主節(jié)點,只用來存儲數(shù)據(jù),可作為負(fù)載器 
# node.master: false 
# node.data: true 

# 2. 如果想讓節(jié)點成為主節(jié)點,且不存儲任何數(shù)據(jù),并保有空閑資源,可作為協(xié)調(diào)器
# node.master: true
# node.data: false

# 3. 如果想讓節(jié)點既不成為主節(jié)點,又不成為數(shù)據(jù)節(jié)點,那么可將他作為搜索器,從節(jié)點中獲取數(shù)據(jù),生成搜索結(jié)果等 
# node.master: false 
# node.data: false

# 4. 節(jié)點是數(shù)據(jù)節(jié)點,也是master節(jié)點,這是默認(rèn)配置
# node.master: true
# node.data: true

2.2.2 es集群常用部署方案

1、默認(rèn)情況下,一個節(jié)點是數(shù)據(jù)節(jié)點,也是master節(jié)點。對于3-5個節(jié)點的小集群來講,通常讓所有節(jié)點存儲數(shù)據(jù)和具有獲得主節(jié)點的資格。你可以將任何請求發(fā)送給任何節(jié)點,并且由于所有節(jié)點都具有集群狀態(tài)的副本,它們知道如何路由請求。多個master的元數(shù)據(jù)也會同步,不用擔(dān)心不一致。要注意,master節(jié)點的數(shù)量最好最少為3,且為單數(shù)

2、當(dāng)集群節(jié)點數(shù)量比較大時,那么通常就會將主節(jié)點、數(shù)據(jù)節(jié)點分開,專門部署在對應(yīng)的節(jié)點上,然后主節(jié)點是多個都可用的,形成HA的結(jié)構(gòu)。要注意,master節(jié)點的數(shù)量最好最少為3,且為單數(shù)

實際部署其實和單節(jié)點差不多,主要看部署的方案選哪個,master有幾個,數(shù)據(jù)節(jié)點有幾個,設(shè)置下角色即可,這里不多說

2.3 安裝head插件

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

?

三、java api操作ES

3.1 基本操作

3.1.1 maven依賴準(zhǔn)備

<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進(jìn)行運行測試,不會用的自己百度

3.1.2 創(chuàng)建ES連接操作對象

public class ESDemo1 {
    private TransportClient client;

    @Before
    public void getClient() throws UnknownHostException {
        //1、創(chuàng)建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() 表示觸發(fā)操作
@Test
    public void createBlog() {
        //創(chuàng)建索引blog
        //創(chuàng)建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\":\"山海經(jīng)\"}";

        //導(dǎo)入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組織數(shù)據(jù)
        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、一次請求內(nèi)部添加多個document
        BulkRequestBuilder bulkRequestBuilder = client.prepareBulk();
        bulkRequestBuilder.add(
                client.prepareIndex("blog2", "comment").setSource(
                    XContentFactory.jsonBuilder()
                    .startObject()
                    .field("name", "山海經(jīng)")
                    .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了,如果創(chuàng)建多個type會有以下報錯

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

3.1.5 搜索doc

根據(jù)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();
        //結(jié)果打印
        for (MultiGetItemResponse itemResponse : multiGetResponse) {
            System.out.println( itemResponse.getResponse().getSourceAsString());
        }
        client.close();
    }

3.1.6 更新doc

@Test
    public void updateData() throws IOException {
        //更新數(shù)據(jù)方式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 {
        //更新數(shù)據(jù)方式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

關(guān)鍵性一個類是 org.elasticsearch.index.query.QueryBuilders;

3.2.1 查詢指定index所有doc

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

3.2.2 全部字段進(jìn)行全文檢索

搜索全部字段中包含指定字符的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());
        }

    }

這個方法用于匹配某個字段的整個內(nèi)容,類似like操作

3.2.4 對指定字段進(jìn)行分詞搜索

@Test
    public void matchField() {
        //這是對分詞結(jié)果進(jìn)行等值操作的方法,不是對整個字段,而是對字段的分詞結(jié)果
        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();
    }

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

3.2.5對指定字段進(jìn)行模糊檢索

@Test
public void fuzzy() {

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

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

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

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

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

3.3 映射mapping

3.3.1 mapping的定義

? 映射是規(guī)定index中的一些屬性,以及各自type下的字段的屬性(再強(qiáng)調(diào)一遍,現(xiàn)在6.x版本一個index下只能有一個type,其實就是變相地去除掉了type)。Elasticsearch映射雖然有idnex和type兩層關(guān)系,但是實際索引時是以index為基礎(chǔ)的。如果同一個index下不同type的字段出現(xiàn)mapping不一致的情況,雖然數(shù)據(jù)依然可以成功寫入并生成各自的mapping,但實際上fielddata中的索引結(jié)果卻依然是以index內(nèi)第一個mapping類型來生成的

3.3.2 mapping的寫法

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

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

}

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

3.3.3 數(shù)據(jù)類型

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

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

3.3.4mapping屬性字段

元數(shù)據(jù)字段:

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

_source :文檔信息 
包含在文檔在創(chuàng)建時的實際主體,它會被存儲但不會被索引,用于get或search是返回主體。如果你并不關(guān)系數(shù)據(jù)的主體,只注重數(shù)量,那可以將此字段禁用

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

shard_num = hash(_routing) % num_primary_shards

_meta 自定義的元數(shù)據(jù) ,因為元數(shù)據(jù)是每個文檔都會帶的,索引如果你想要在每個文檔上標(biāo)注一些信息,就可以使用此屬性,自定義一些元數(shù)據(jù)。

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

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

字段屬性:

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

analyzer 分析器 
用于指定索引創(chuàng)建時使用的分析器是什么,即對同一段內(nèi)容,不同的分析器會用不同的方式分詞,最后在倒排索引上的值是不同的。

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

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

fielddata 現(xiàn)場數(shù)據(jù) 
如果你要對一個text類型進(jìn)行聚合操作,你必須設(shè)置這個參數(shù)為true。

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

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

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

coerce 是否轉(zhuǎn)換 
在插入數(shù)據(jù)時,在插入數(shù)據(jù)類型和映射類型不一致的情況下是否強(qiáng)制轉(zhuǎn)換數(shù)據(jù)類型。默認(rèn)是開啟的

normalizer 轉(zhuǎn)換器 
因為keyword類型的字段是不進(jìn)行分析的,但是我們又想要將其統(tǒng)一成一個規(guī)則,比如都是小寫,比如用ASCILL進(jìn)行編碼,其實就是個給keyword用的分析器??梢栽趕etting下的analysis下定義自己的normalizer使用

copy_to 同步復(fù)制 
在插入值是,會把值一同放到另一個字段中。主要用于自己定義一個類似于_all字段的字端。

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

enabled 是否啟動 
這個值是否要用于搜索

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

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

include_in_all 是否保存在_all字段中

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

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

norms 規(guī)范 
如果一個字段只用于聚合,可以設(shè)置為false

3.3.5 _all,_sources,store的區(qū)別

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

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

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

=======================
store:true/false,默認(rèn)為false
這個屬性用于指定是否保存document中對應(yīng)字段的value,這個的概念和上面的source有點類似了,只不過這里store是針對某個field的原始數(shù)據(jù),source是針對整個document的原始數(shù)據(jù)。

當(dāng)執(zhí)行想獲取一個document的數(shù)據(jù)時,
1、采用source方式時:
只需產(chǎn)生一次磁盤IO,因為_source存儲的時候,直接把整個doc當(dāng)做一個字段來存儲。當(dāng)我們需要doc中的某個字段時,是先從source讀取數(shù)據(jù),然后再解析成json,獲取到指定字段內(nèi)容
2、采用store方式時,
因為每個字段都單獨存儲了,當(dāng)需要獲得整個doc的數(shù)據(jù)時,就需要單獨每個字段進(jìn)行取值,有多少個字段就產(chǎn)生多少次磁盤IO。
3、store和source混合使用時
如果操作是獲取整個doc的數(shù)據(jù),那么es會優(yōu)先從source讀取數(shù)據(jù)。
如果操作是獲取某些字段的數(shù)據(jù),那么es會優(yōu)先從store存儲中讀取數(shù)據(jù)。因為這樣讀取的數(shù)據(jù)量相對較少,無需讀取整個doc的數(shù)據(jù)再解析。
但是注意的是,這兩個屬性都是單獨自己保存數(shù)據(jù)的,所以如果兩個啟用的話,相當(dāng)于數(shù)據(jù)存儲了兩次,挺浪費存儲空間的,增大了索引的體積

3.3.6 api操作mapping

創(chuàng)建mapping,要注意,mapping創(chuàng)建之后不能更改

@Test
    public void createMapping() throws Exception {

        // 1設(shè)置mapping,使用jsonbuilder構(gòu)建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 關(guān)閉資源
        client.close();
    }

查看map

@Test
    public void getIndexMapping() throws ExecutionException, InterruptedException {
       //構(gòu)建查看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數(shù)據(jù)
        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();
    }

/*
結(jié)果如下:
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版本的依賴,導(dǎo)致有些方法不存在,就報錯。解決方式很簡答,直接指定使用新版本的就好,如下:

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

四、分詞器

4.1 默認(rèn)分詞器

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

[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
    }
  ]
}

可以看到,標(biāo)準(zhǔn)的中文分詞器只是單純將字分開,其實并不智能,沒有詞語考慮進(jìn)去。所以需要更加強(qiáng)大的分詞器。常用的有ik分詞器

4.2 安裝ik分詞器

cd /opt/modules/elasticsearch-6.6.2
執(zhí)行下面的命令安裝,需要聯(lián)網(wǎng)
bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v6.6.2/elasticsearch-analysis-ik-6.6.2.zip

注意要根據(jù)ES的版本安裝對應(yīng)版本的ik

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

分兩種模式:ik_smart 和 ik_max_word

1、 ik_smart  模式,智能解析詞語結(jié)構(gòu)
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” 屬性,指定使用的分詞器而已。其他都沒有區(qū)別,這里不重復(fù)

五、優(yōu)化

5.1 背景

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

5.2 優(yōu)化1--filesystemCache

5.2.1 基本原理

一、Elasticsearch原理與基本使用

? 圖5.1 ES filesytem cache

? 你往 ES 里寫的數(shù)據(jù),實際上都寫到磁盤文件里去了,查詢的時候,操作系統(tǒng)會將磁盤文件里的數(shù)據(jù)自動緩存到 Filesystem Cache 里面去。ES 的搜索引擎嚴(yán)重依賴于底層的 Filesystem Cache,你如果給 Filesystem Cache 更多的內(nèi)存,盡量讓內(nèi)存可以容納所有的 IDX Segment File 索引數(shù)據(jù)文件,那么你搜索的時候就基本都是走內(nèi)存的,性能會非常高。

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

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

5.2.2 優(yōu)化具體方式

? 首先要知道一點:歸根結(jié)底,你要讓 ES 性能好,最佳的情況下,就是你的機(jī)器的內(nèi)存,至少可以容納你的總數(shù)據(jù)量的一半。當(dāng)然如果內(nèi)存能容納全部數(shù)據(jù),自然是最好,然而基本生產(chǎn)中沒有那么多錢的啦。走內(nèi)存可以滿足秒級以內(nèi)的查詢要求

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

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

5.3 優(yōu)化2--數(shù)據(jù)預(yù)熱

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

5.4 優(yōu)化3--冷熱分離

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

5.5 優(yōu)化4--避免關(guān)聯(lián)查詢

? 對于 MySQL,我們經(jīng)常有一些復(fù)雜的關(guān)聯(lián)查詢,在 ES 里該怎么玩兒?ES 里面的復(fù)雜的關(guān)聯(lián)查詢盡量別用,一旦用了性能一般都不太好。最好是先在 Java 系統(tǒng)里就完成關(guān)聯(lián),將關(guān)聯(lián)好的數(shù)據(jù)直接寫入 ES 中。搜索的時候,就不需要利用 ES 的搜索語法來完成 Join 之類的關(guān)聯(lián)搜索了。

5.6 優(yōu)化5--document模型設(shè)計

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

5.7 優(yōu)化6--分頁性能優(yōu)化

背景:

    ES 的分頁是較坑的,為啥呢?舉個例子吧,假如你每頁是 10 條數(shù)據(jù),你現(xiàn)在要查詢第 100 頁,實際上是會把每個 Shard 上存儲的前 1000 條數(shù)據(jù)都查到一個協(xié)調(diào)節(jié)點上。如果你有 5 個 Shard,那么就有 5000 條數(shù)據(jù),接著協(xié)調(diào)節(jié)點對這 5000 條數(shù)據(jù)進(jìn)行一些合并、處理,再獲取到最終第 100 頁的 10 條數(shù)據(jù)。
    由于是分布式的,你要查第 100 頁的 10 條數(shù)據(jù),不可能說從 5 個 Shard,每個 Shard 就查 2 條數(shù)據(jù),最后到協(xié)調(diào)節(jié)點合并成 10 條數(shù)據(jù)吧?你必須得從每個 Shard 都查 1000 條數(shù)據(jù)過來,然后根據(jù)你的需求進(jìn)行排序、篩選等等操作,最后再次分頁,拿到里面第 100 頁的數(shù)據(jù)。
    也就是說,你翻頁的時候,翻的越深,每個 Shard 返回的數(shù)據(jù)就越多,而且協(xié)調(diào)節(jié)點處理的時間越長,非??拥K杂?ES 做分頁的時候,你會發(fā)現(xiàn)越翻到后面,就越是慢。
    我們之前也是遇到過這個問題,用 ES 作分頁,前幾頁就幾十毫秒,翻到 10 頁或者幾十頁的時候,基本上就要 5~10 秒才能查出來一頁數(shù)據(jù)了。

解決方案:

1、不允許深度分頁(默認(rèn)深度分頁性能很差)。跟產(chǎn)品經(jīng)理說,你系統(tǒng)不允許翻那么深的頁,默認(rèn)翻的越深,性能就越差。

2、類似于 App 里的推薦商品不斷下拉出來一頁一頁的;類似于微博中,下拉刷微博,刷出來一頁一頁的,你可以用 Scroll API,關(guān)于如何使用,大家可以自行上網(wǎng)搜索學(xué)習(xí)一下。
    Scroll是如何做的呢?它會一次性給你生成所有數(shù)據(jù)的一個快照,然后每次滑動向后翻頁就是通過游標(biāo) scroll_id 移動,獲取下一頁、下一頁這樣子,性能會比上面說的那種分頁性能要高很多很多,基本上都是毫秒級的。
    但是,唯一的一點就是,這個適合于那種類似微博下拉翻頁的,不能隨意跳到任何一頁的場景。也就是說,你不能先進(jìn)入第 10 頁,然后去第 120 頁,然后又回到第 58 頁,不能隨意亂跳頁。所以現(xiàn)在很多產(chǎn)品,都是不允許你隨意翻頁的,你只能往下拉,一頁一頁的翻。
    使用時需要注意,初始化必須指定 Scroll 參數(shù),告訴 ES 要保存此次搜索的上下文多長時間。你需要確保用戶不會持續(xù)不斷翻頁翻幾個小時,否則可能因為超時而失敗。
    除了用 Scroll API,你也可以用 search_after 來做。search_after 的思想是使用前一頁的結(jié)果來幫助檢索下一頁的數(shù)據(jù)。
    顯然,這種方式也不允許你隨意翻頁,你只能一頁頁往后翻。初始化時,需要使用一個唯一值的字段作為 Sort 字段。
向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI