Elasticsearch入門及掌握其JavaAPI

zanwensicheng發表於2019-04-23

個人技術部落格:www.zhenganwen.top

環境

安裝ES

ES專案結構

解壓elasticsearch-6.2.1.zip,解壓後得到的目錄為==ES根目錄==,其中各目錄作用如下:

  • bin,存放啟動ES等命令指令碼
  • config,存放ES的配置檔案,ES啟動時會讀取其中的內容
    • elasticsearch.yml,ES的叢集資訊、對外埠、記憶體鎖定、資料目錄、跨域訪問等屬性的配置
    • jvm.options,ES使用Java寫的,此檔案用於設定JVM相關引數,如最大堆、最小堆
    • log4j2.properties,ES使用log4j作為其日誌框架
  • data,資料存放目錄(索引資料)
  • lib,ES依賴的庫
  • logs,日誌存放目錄
  • modules,ES的各功能模組
  • plugins,ES的可擴充套件外掛存放目錄,如可以將ik中文分詞外掛放入此目錄,ES啟動時會自動載入

屬性配置

預設的elasticsearch.yml中的所有屬性都被註釋了,我們需要設定一些必要的屬性值,在文尾新增如下內容(叢集相關的配將在後文詳細說明):

cluster.name: xuecheng #叢集名稱,預設為elasticsearch
node.name: xc_node_1   #節點名稱,一個ES例項就是一個節點(通常一臺機器上只部署一個ES例項)
network.host: 0.0.0.0  #IP繫結,0.0.0.0表示所有IP都可訪問到此ES例項
http.port: 9200	       #通過此埠以RESTful的形式訪問ES
transport.tcp.port: 9300 #ES叢集通訊使用的埠
node.master: true	   #此節點是否能夠作為主節點
node.data: true  	   #此節點是否存放資料
discovery.zen.ping.unicast.hosts: ["0.0.0.0:9300", "0.0.0.0:9301"]	#叢集其他節點的通訊埠,ES啟動時會發現這些節點
discovery.zen.minimum_master_nodes: 1	#主節點數量的最少值,此值的計算公式為(master_eligible_nodes/2)+1,即可作為主節點的節點數/2+1
node.ingest: true	   #此節點是否作為協調節點,當索引庫具有多個分片並且各分片位於不同節點上時,如果收到查詢請求的節點發現要查詢的資料在另一個節點的分片上,那麼作為協調節點的該節點將會轉發此請求並最終響應結果資料
bootstrap.memory_lock: false	#是否鎖住ES佔用的記憶體,此項涉及到OS的swap概念,當ES空閒時作業系統可能會把ES佔用的記憶體資料暫時儲存到磁碟上,當ES活動起來時再調入記憶體,如果要求ES時刻保持迅速響應狀態則可設定為true,那麼ES的執行記憶體永遠不會被交換到磁碟以避免交換過程帶來的延時
node.max_local_storage_nodes: 2	#本機上的最大儲存節點數,多個ES例項可以共享一個資料目錄,這一特性有利於我們在開發環境的一臺機器上測試叢集機制,但在生產環境下建議設定為1,並且官方也建議一臺叢集僅部署一個ES例項

path.data: D:\software\es\elasticsearch-6.2.1\data	#ES的資料目錄
path.logs: D:\software\es\elasticsearch-6.2.1\logs	#ES的日誌目錄

http.cors.enabled: true			#是否允許跨域訪問,後面通過一個視覺化ES管理外掛時需要通過js跨域訪問此ES
http.cors.allow-origin: /.*/	#設定所有域均可跨域訪問此ES
複製程式碼

JVM引數設定

預設ES啟動需要分配的堆記憶體為1G,如果你的機器記憶體較小則可在jvm.options中調整為512M:

-Xms512m
-Xmx512
複製程式碼

啟動ES

雙擊/bin/elasticsearch.bat啟動指令碼即可啟動ES,關閉該命令列視窗即可關閉ES。

啟動後訪問:http://localhost:9200,如果得到如下響應則ES啟動成功:

{
    name: "xc_node_1",
    cluster_name: "xuecheng",
    cluster_uuid: "93K4AFOVSD-kPF2DdHlcow",
    version: {
        number: "6.2.1",
        build_hash: "7299dc3",
        build_date: "2018-02-07T19:34:26.990113Z",
        build_snapshot: false,
        lucene_version: "7.2.1",
        minimum_wire_compatibility_version: "5.6.0",
        minimum_index_compatibility_version: "5.0.0"
    },
    tagline: "You Know, for Search"
}
複製程式碼

elasticsearch-head視覺化外掛

ES是基於Lucene開發的產品級搜尋引擎,封裝了很多內部細節,通過此外掛我們可以通過Web的方式視覺化檢視其內部狀態。

此外掛無需放到ES的/plugins目錄下,因為它是通過JS與ES進行互動的。

git clone git://github.com/mobz/elasticsearch-head.git
cd elasticsearch-head
npm install
npm run start
複製程式碼

瀏覽器開啟:http://localhost:9100,並連線通過ES提供的http埠連線ES:

image

ES快速入門

首先我們要理解幾個概念:索引庫(index)、文件(document)、欄位(field),我們可以類比關係型資料庫來理解:

ES MySQL
索引庫index 資料庫database
type 表table
文件document 行row
欄位field 列column

但是自ES6.x開始,type的概念就慢慢被弱化了,官方將在ES9正式剔除它。因此我們可以將索引庫類比為一張表。一個索引庫用來儲存一系列結構類似的資料。雖然可以通過多個type製造出一個索引庫“多張表”的效果,但官方不建議這麼做,因為這會降低索引和搜尋效能,要麼你就新建另外一個索引庫。類比MySQL來看就是,一個庫就只放一張表。

名詞索引 & 動詞索引

名詞索引指的是索引庫,一個磁碟上的檔案。

一個索引庫就是一張倒排索引表,將資料存入ES的過程就是先將資料分詞然後新增到倒排索引表的過程。

image

以新增“中華人民共和國”、“中華上下五千年”到索引庫為例,倒排索引表的邏輯結構如下:

term doc_id
中華 1、2
人民 1
共和國 1
上下 2
2
千年 2
doc_id doc
1 中華人民共和國
2 中華上下五千年

這種將資料分詞並建立各分詞到文件之間的關聯關係的過程稱為==索引==(動詞)

Postman

Postman是一款HTTP客戶端工具,能否方便地傳送各種形式的RESTful請求。

下文將以Postman來測試ES的RESTful API,請求的根URL為:http://localhost:9200

索引庫管理

建立索引庫

建立一個名為“xc_course”的用於存放學成線上(教育平臺)的課程資料的索引庫:

  • PUT /xc_course
{
	"settings":{
		"number_of_shards":1,		//索引庫分片數量
		"number_of_replicas":0		//每個分片的副本數,關於分片、叢集等在後文詳細介紹
	}
}
複製程式碼

image

建立成功了嗎?我們可以通過elasticsearch-head來檢視,重新整理localhost:9100:

image

刪除索引庫

DELET /xc_course

檢視索引資訊

GET /xc_course

對映管理

建立對映

對映可以類比MySQL的表結構定義,如有哪些欄位,欄位是什麼型別。

建立對映的請求格式為:POST /index_name/type_name/_mapping。

不是說type已經弱化了嗎?為什麼這裡還要指定type的名稱?因為在ES9才正式剔除type的概念,在此之前需要一個過渡期,因此我們可以指定一個無意義的type名,如“doc”:

POST /xc_course/doc/_mapping

{
	"properties":{
		"name":{
			"type":"text"
		},
		"description":{
			"type":"text"
		},
		"price":{
			"type":"double"
		}
	}
}
複製程式碼

檢視對映(類比檢視錶結構)

GET /xc_course/doc/_mapping

{
    "xc_course": {
        "mappings": {
            "doc": {
                "properties": {
                    "description": {
                        "type": "text"
                    },
                    "name": {
                        "type": "text"
                    },
                    "price": {
                        "type": "double"
                    }
                }
            }
        }
    }
}
複製程式碼

也可以通過head外掛檢視:

image

文件管理

新增文件

PUT /index/type/id

如果不指定id,那麼ES會為我們自動生成:

PUT /xc_course/doc

{
    "name" : "Bootstrap開發框架",
    "description" : "Bootstrap是由Twitter推出的一個前臺頁面開發框架,在行業之中使用較為廣泛。此開發框架包含了大量的CSS、JS程式程式碼,可以幫助開發者(尤其是不擅長頁面開發的程式人員)輕鬆的實現一個不受瀏覽器限制的精美介面效果。",
    "price" : 99.9
}
複製程式碼

響應如下:

{
    "_index": "xc_course",
    "_type": "doc",
    "_id": "Hib0QmoB7xBOMrejqjF3",
    "_version": 1,
    "result": "created",
    "_shards": {
        "total": 1,
        "successful": 1,
        "failed": 0
    },
    "_seq_no": 0,
    "_primary_term": 1
}
複製程式碼

根據id查詢文件

GET /index/type/id

於是我們拿到我們剛新增資料生成的id來查詢:

GET /xc_course/doc/Hib0QmoB7xBOMrejqjF3

{
    "_index": "xc_course",
    "_type": "doc",
    "_id": "Hib0QmoB7xBOMrejqjF3",
    "_version": 1,
    "found": true,
    "_source": {
        "name": "Bootstrap開發框架",
        "description": "Bootstrap是由Twitter推出的一個前臺頁面開發框架,在行業之中使用較為廣泛。此開發框架包含了大量的CSS、JS程式程式碼,可以幫助開發者(尤其是不擅長頁面開發的程式人員)輕鬆的實現一個不受瀏覽器限制的精美介面效果。",
        "price": 99.9
    }
}
複製程式碼

查詢全部文件

GET /index/type/_search

{
    "took": 64,				//此次查詢花費時間
    "timed_out": false,		
    "_shards": {
        "total": 1,
        "successful": 1,
        "skipped": 0,
        "failed": 0
    },
    "hits": {			   
        "total": 1,
        "max_score": 1,
        "hits": [			//查詢匹配的文件集合
            {
                "_index": "xc_course",
                "_type": "doc",
                "_id": "Hib0QmoB7xBOMrejqjF3",
                "_score": 1,
                "_source": {
                    "name": "Bootstrap開發框架",
                    "description": "Bootstrap是由Twitter推出的一個前臺頁面開發框架,在行業之中使用較為廣泛。此開發框架包含了大量的CSS、JS程式程式碼,可以幫助開發者(尤其是不擅長頁面開發的程式人員)輕鬆的實現一個不受瀏覽器限制的精美介面效果。",
                    "price": 99.9
                }
            }
        ]
    }
}
複製程式碼

IK中文分詞器

ES預設情況下是不支援中文分詞的,也就是說對於新增的中文資料,ES將會把每個字當做一個term(詞項),這不利於中文檢索。

測試ES預設情況下對中文分詞的結果:

POST /_analyze

你會發現ES的固定API都會帶上_字首,如_mapping_search_analyze

{
	"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-6.4.0並解壓到ES/plugins/目錄下,並將解壓後的目錄改名為==ik==,==重啟ES==,該外掛即會被自動載入。

重啟ES後再測試分詞效果:

POST http://localhost:9200/_analyze

{
	"text":"中華人民共和國",
	"analyzer":"ik_max_word"	//設定分詞器為ik分詞器,否則還是會採用預設分詞器,可選ik_max_word和ik_smart
}
複製程式碼

ik_max_word分詞策略是儘可能的分出多的term,即細粒度分詞:

{
    "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
        }
    ]
}
複製程式碼

而ik_smart則是出粒度分詞(設定"analyzer" : "ik_smart"):

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

自定義詞庫

ik分詞器僅提供了常用中文短語的詞庫,而對於實時性的熱門網路短語則無法識別,因此有時為了增加分詞準確性,我們需要自己擴充套件詞庫。

首先我們測試ik對網路詞彙“藍瘦香菇”的分詞效果:

PUT /_analyze

{
    "text":"藍瘦香菇",
    "analyzer":"ik_smart"
}
複製程式碼

分詞如下:

{
    "tokens": [
        {
            "token": "藍",
            "start_offset": 0,
            "end_offset": 1,
            "type": "CN_CHAR",
            "position": 0
        },
        {
            "token": "瘦",
            "start_offset": 1,
            "end_offset": 2,
            "type": "CN_CHAR",
            "position": 1
        },
        {
            "token": "香菇",
            "start_offset": 2,
            "end_offset": 4,
            "type": "CN_WORD",
            "position": 2
        }
    ]
}
複製程式碼

我們在ES的/plugins/ik/config目錄下增加自定義的詞庫檔案my.dic並新增一行“藍瘦香菇”(詞典檔案的格式是每一個詞項佔一行),並在ik的配置檔案/plugins/ik/config/IKAnalyzer.cfg.xml中引入該自定義詞典:

<!--使用者可以在這裡配置自己的擴充套件字典 -->
<entry key="ext_dict">my.dic</entry>
複製程式碼

==重啟ES==,ik分詞器將會把我們新增的詞項作為分詞標準:

{
    "tokens": [
        {
            "token": "藍瘦香菇",
            "start_offset": 0,
            "end_offset": 4,
            "type": "CN_WORD",
            "position": 0
        }
    ]
}
複製程式碼

對映

新增欄位

PUT /xc_course/doc/_mapping

{
    "properties":{
        "create_time":{
            "type":"date"
        }
    }
}
複製程式碼

GET /xc_course/doc/_mapping

{
    "xc_course": {
        "mappings": {
            "doc": {
                "properties": {
                    "create_time": {
                        "type": "date"
                    },
                    "description": {
                        "type": "text"
                    },
                    "name": {
                        "type": "text"
                    },
                    "price": {
                        "type": "double"
                    }
                }
            }
        }
    }
}
複製程式碼

已有的對映可以新增欄位但不可以更改已有欄位的定義!

PUT /xc_course/doc/_mapping

{
    "properties":{
        "price":{
            "type":"integer"
        }
    }
}
複製程式碼

報錯:已定義的price不能從double型別更改為integer型別:

{
    "error": {
        "root_cause": [
            {
                "type": "illegal_argument_exception",
                "reason": "mapper [price] cannot be changed from type [double] to [integer]"
            }
        ],
        "type": "illegal_argument_exception",
        "reason": "mapper [price] cannot be changed from type [double] to [integer]"
    },
    "status": 400
}
複製程式碼

如果一定要更改某欄位的定義(包括型別、分詞器、是否索引等),那麼只有刪除此索引庫重新建立索引並定義好各欄位,再遷入資料。因此在索引庫建立時要考慮好對映的定義,因為僅可擴充套件欄位但不可重新定義欄位。

常用的對映型別——type

ES6.2的核心資料型別如下:

image

keyword

此型別的欄位不會被分詞,該欄位內容被表示為就是一個短語不可分割。如各大商標和品牌名可使用此型別。並且在該欄位查詢內容時是精確匹配,如在type為keyword的brand欄位搜尋“華為”不會搜出欄位值為“華為榮耀”的文件。

date

type為date的欄位還可以額外指定一個format,如

{
    "properties":{
        "create_time":{	
            "type":"date",
            "format":"yyyy-MM-dd HH:mm:ss||yyyy-MM-dd"	
        }
    }
}
複製程式碼

新增文件的create_time欄位值可以是日期+時間或僅日期

數值型別

image

1、儘量選擇範圍小的型別,提高搜尋效率 2、對於浮點數儘量用比例因子,比如一個價格欄位,單位為元,我們將比例因子設定為100這在ES中會按==分==存 儲,對映如下:

"price": {
    "type": "scaled_float",       
    "scaling_factor": 100
}
複製程式碼

由於比例因子為100,如果我們輸入的價格是23.45則ES中會將23.45乘以100儲存在ES中。如果輸入的價格是23.456,ES會將23.456乘以100再取一個接近原始值的數,得出2346。

使用比例因子的好處是整型比浮點型更易壓縮,節省磁碟空間。

是否建立索引——index

index預設為true,即需要分詞並根據分詞所得詞項建立倒排索引(詞項到文件的關聯關係)。有些欄位的資料是無實際意義的,如課程圖片的url僅作展示圖片之用,不需要分詞建立索引,那麼可以設定為false:

PUT /xc_course/doc/_mapping

{
    "properties":{
        "pic":{
            "type":"text"
            "index":"false"
        }
    }
}
複製程式碼

索引分詞器 & 搜尋分詞器

索引分詞器——analyzer

將資料新增到索引庫時使用的分詞器,建議使用ik_max_word,比如“中華人民共和國”,如果使用ik_smart,那麼整個“中華人民共和國”將被作為一個term(此項)存入倒排索引表,那麼在搜尋“共和國”時就搜不到此資料(詞項與詞項之間是精確匹配的)。

搜尋分詞器——search_analyzer

搜尋分詞器則是用於將使用者的檢索輸入分詞的分詞器。

建議使用ik_smart,比如搜尋“中華人民共和國”,不應該出現“喜馬拉雅共和國”的內容。

是否額外儲存——store

是否在source之外儲存,每個文件索引後會在 ES中儲存一份原始文件,存放在_source中,一般情況下不需要設定 store為true,因為在_source中已經有一份原始文件了。

綜合實戰

建立一個課程集合的對映:

  1. 首先刪除已建立對映的索引

    DELET /xc_course

  2. 新增索引

    PUT /xc_course

  3. 建立對映

    PUT /xc_course/doc/_mapping

    {
        "properties": {
            "name": {
                "type": "text",
                "analyzer": "ik_max_word",
                "search_analyzer": "ik_smart"
            },
            "description": {
                "type": "text",
                "analyzer": "ik_max_word",
                "search_analyzer": "ik_smart"
            },
            "price": {
                "type": "scaled_float",
                "scaling_factor": 100
            },
            "studypattern": {
                "type": "keyword"
            },
            "pic": {
                "type": "text",
                "index": false
            },
            "timestamp": {
                "type": "date",
                "format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd"
            }
        }
    }
    複製程式碼
  4. 新增文件

    POST /xc_course/doc

    {
        "name": "Java核心技術",
        "description": "深入淺出講解Java核心技術以及相關原理",
        "price": 99.9,
        "studypattern": "20101",
        "pic": "http://xxx.xxx.xxx/skllsdfsdflsdfk.img",
        "timestamp": "2019-4-1 13:16:00"
    }
    複製程式碼
  5. 檢索Java

    GET http://localhost:9200/xc_course/doc/_search?q=name:java

  6. 檢索學習模式

    GET http://localhost:9200/xc_course/doc/_search?q=studypattern:20101

索引管理和Java客戶端

從此章節開始我們將對ES的每個RESTful API實現配套的Java程式碼。畢竟雖然前端可以通過HTTP訪問ES,但是ES的管理和定製化業務還是需要一個後端作為樞紐。

ES提供的Java客戶端——RestClient

RestClient是官方推薦使用的,它包括兩種:Java Low Level REST Client和 Java High Level REST Client。ES在6.0之後提供 Java High Level REST Client, 兩種客戶端官方更推薦使用 Java High Level REST Client,不過當前它還處於完善中,有些功能還沒有(如果它有不支援的功能,則使用Java Low Level REST Client。)。

依賴如下:

<dependency>
    <groupId>org.elasticsearch.client</groupId>
    <artifactId>elasticsearch-rest-high-level-client</artifactId>
    <version>6.2.1</version>
</dependency>
<dependency>
    <groupId>org.elasticsearch</groupId>
    <artifactId>elasticsearch</artifactId>
    <version>6.2.1</version>
</dependency>
複製程式碼

Spring整合ES

依賴

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

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
</dependency>
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-io</artifactId>
</dependency>	
複製程式碼

配置檔案

application.yml

server:
  port: ${port:40100}
spring:
  application:
    name: xc-service-search
xuecheng:	#自定義屬性項
  elasticsearch:
    host-list: ${eshostlist:127.0.0.1:9200} #多個節點中間用逗號分隔
複製程式碼

啟動類

@SpringBootApplication
public class SearchApplication {
    public static void main(String[] args){
        SpringApplication.run(SearchApplication.class, args);
    }
}
複製程式碼

ES配置類

package com.xuecheng.search.config;

import org.apache.http.HttpHost;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * ElasticsearchConfig class
 *
 * @author : zaw
 * @date : 2019/4/22
 */
@Configuration
public class ElasticsearchConfig {

    @Value("${xuecheng.elasticsearch.host-list}")
    private String hostList;

    @Bean
    public RestHighLevelClient restHighLevelClient() {
        return new RestHighLevelClient(RestClient.builder(getHttpHostList(hostList)));
    }

    private HttpHost[] getHttpHostList(String hostList) {
        String[] hosts = hostList.split(",");
        HttpHost[] httpHostArr = new HttpHost[hosts.length];
        for (int i = 0; i < hosts.length; i++) {
            String[] items = hosts[i].split(":");
            httpHostArr[i] = new HttpHost(items[0], Integer.parseInt(items[1]), "http");
        }
        return httpHostArr;
    }

    // rest low level client
    @Bean
    public RestClient restClient() {
        return RestClient.builder(getHttpHostList(hostList)).build();
    }
}
複製程式碼

測試類

package com.xuecheng.search;

import org.elasticsearch.action.admin.indices.create.CreateIndexRequest;
import org.elasticsearch.action.admin.indices.create.CreateIndexResponse;
import org.elasticsearch.client.IndicesClient;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.xcontent.XContentType;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import java.io.IOException;

/**
 * TestES class
 *
 * @author : zaw
 * @date : 2019/4/22
 */
@SpringBootTest
@RunWith(SpringRunner.class)
public class TestESRestClient {

    @Autowired
    RestHighLevelClient restHighLevelClient;    //ES連線物件

    @Autowired
    RestClient restClient;
}

複製程式碼

ES客戶端API

首先我們將之前建立的索引庫刪除:

DELETE /xc_course

然後回顧一下建立索引庫的RESTful形式:

PUT /xc_course

{
	"settings":{
		"index":{
			"number_of_shards":1,
			"number_of_replicas":0
		}
	}
}
複製程式碼

建立索引庫

@Test
public void testCreateIndex() throws IOException {
    CreateIndexRequest request = new CreateIndexRequest("xc_course");
    /**
         * {
         * 	"settings":{
         * 		"index":{
         * 			"number_of_shards":1,
         * 			"number_of_replicas":0
         *       }
         *    }
         * }
         */
    request.settings(Settings.builder().put("number_of_shards", 1).put("number_of_replicas", 0));
    IndicesClient indicesClient = restHighLevelClient.indices();    //通過ES連線物件獲取索引庫管理物件
    CreateIndexResponse response = indicesClient.create(request);
    System.out.println(response.isAcknowledged());  //操作是否成功
}
複製程式碼

對比RESTful形式,通過CreateIndexRequest方式發起此次請求,第3行通過建構函式指明瞭要建立的索引庫名(對應URI /xc_course),第14行構造了請求體(你會發現settings方法和JSON請求格式很相似)。

操作索引庫需要使用IndicesClient物件。

刪除索引庫

@Test
public void testDeleteIndex() throws IOException {
    DeleteIndexRequest request = new DeleteIndexRequest("xc_course");
    IndicesClient indicesClient = restHighLevelClient.indices();
    DeleteIndexResponse response = indicesClient.delete(request);
    System.out.println(response.isAcknowledged());
}
複製程式碼

建立索引庫時指定對映

@Test
public void testCreateIndexWithMapping() throws IOException {
    CreateIndexRequest request = new CreateIndexRequest("xc_course");
    request.settings(Settings.builder().put("number_of_shards", 1).put("number_of_replicas", 0));
    request.mapping("doc", "{\n" +
                    "    \"properties\": {\n" +
                    "        \"name\": {\n" +
                    "            \"type\": \"text\",\n" +
                    "            \"analyzer\": \"ik_max_word\",\n" +
                    "            \"search_analyzer\": \"ik_smart\"\n" +
                    "        },\n" +
                    "        \"price\": {\n" +
                    "            \"type\": \"scaled_float\",\n" +
                    "            \"scaling_factor\": 100\n" +
                    "        },\n" +
                    "        \"timestamp\": {\n" +
                    "            \"type\": \"date\",\n" +
                    "            \"format\": \"yyyy-MM-dd HH:mm:ss||yyyy-MM-dd\"\n" +
                    "        }\n" +
                    "    }\n" +
                    "}", XContentType.JSON);
    IndicesClient indicesClient = restHighLevelClient.indices();
    CreateIndexResponse response = indicesClient.create(request);
    System.out.println(response.isAcknowledged());
}
複製程式碼

新增文件

新增文件的過程就是“索引”(動詞)。需要使用IndexRequest物件進行索引操作。

public static final SimpleDateFormat FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
@Test
public void testAddDocument() throws IOException {
    Map<String, Object> jsonMap = new HashMap<>();
    jsonMap.put("name", "Java核心技術");
    jsonMap.put("price", 66.6);
    jsonMap.put("timestamp", FORMAT.format(new Date(System.currentTimeMillis())));
    IndexRequest request = new IndexRequest("xc_course", "doc");
    request.source(jsonMap);
    IndexResponse response = restHighLevelClient.index(request);
    System.out.println(response);
}
複製程式碼

響應結果包含了ES為我們生成的文件id,這裡我測試得到的id為fHh6RWoBduPBueXKl_tz

根據id查詢文件

@Test
public void testFindById() throws IOException {
    GetRequest request = new GetRequest("xc_course", "doc", "fHh6RWoBduPBueXKl_tz");
    GetResponse response = restHighLevelClient.get(request);
    System.out.println(response);
}
複製程式碼

根據id更新文件

ES更新文件有兩種方式:全量替換和區域性更新

全量替換:ES首先會根據id查詢文件並刪除然後將該id作為新文件的id插入。

區域性更新:只會更新相應欄位

全量替換:

POST /index/type/id

區域性更新:

POST /index/type/_update

Java客戶端提供的是區域性更新,即僅對提交的欄位進行更新而其他欄位值不變

@Test
public void testUpdateDoc() throws IOException {
    UpdateRequest request = new UpdateRequest("xc_course", "doc", "fHh6RWoBduPBueXKl_tz");
    Map<String, Object> docMap = new HashMap<>();
    docMap.put("name", "Spring核心技術");
    docMap.put("price", 99.8);
    docMap.put("timestamp", FORMAT.format(new Date(System.currentTimeMillis())));
    request.doc(docMap);
    UpdateResponse response = restHighLevelClient.update(request);
    System.out.println(response);
    testFindById();
}
複製程式碼

根據id刪除文件

@Test
public void testDeleteDoc() throws IOException {
    DeleteRequest request = new DeleteRequest("xc_course", "doc", "fHh6RWoBduPBueXKl_tz");
    DeleteResponse response = restHighLevelClient.delete(request);
    System.out.println(response);
}
複製程式碼

搜尋管理

準備環境

為了有資料可搜,我們重新建立對映並新增一些測試資料

建立對映

DELETE /xc_course

PUT /xc_course

{
    "settings":{
        "number_of_shards":1,
        "number_of_replicas":0
    }
}
複製程式碼

PUT /xc_course/doc/_mapping

{
    "properties": {
        "name": {
            "type": "text",
            "analyzer": "ik_max_word",
            "search_analyzer": "ik_smart"
        },
        "description": {
            "type": "text",
            "analyzer": "ik_max_word",
            "search_analyzer": "ik_smart"
        },
        "studymodel":{
			"type":"keyword"	//授課模式,值為資料字典代號
		},
        "pic": {
            "type": "text",
            "index": false
        },
        "price": {
            "type": "float"
        },
        "timestamp": {
            "type": "date",
            "format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd"
        }
    }
}
複製程式碼

新增測試資料

PUT /xc_course/doc/1

{
    "name": "Bootstrap開發",
    "description": "Bootstrap是由Twitter推出的一個前臺頁面開發框架,是一個非常流行的開發框架,此框架整合了多種頁面效果。此開發框架包含了大量的CSS、JS程式程式碼,可以幫助開發者(尤其是不擅長頁面開發的程式人員)輕鬆的實現一個不受瀏覽器限制的精美介面效果。",
    "studymodel": "201002",
    "price": 38.6,
    "pic": "group1/M00/00/00/wKhlQFs6RCeAY0pHAAJx5ZjNDEM428.jpg",
    "timestamp": "2018-04-25 19:11:35"
}
複製程式碼

PUT /xc_course/doc/2

{
    "name": "java程式設計基礎",
    "description": "java語言是世界第一程式語言,在軟體開發領域使用人數最多。",
    "studymodel": "201001",
    "price": 68.6,
    "pic": "group1/M00/00/00/wKhlQFs6RCeAY0pHAAJx5ZjNDEM428.jpg",
    "timestamp": "2018-03-25 19:11:35"
}
複製程式碼

PUT /xc_course/doc/3

{
    "name": "spring開發基礎",
    "description": "spring 在java領域非常流行,java程式設計師都在用。",
    "studymodel": "201001",
    "price": 88.6,
    "pic": "group1/M00/00/00/wKhlQFs6RCeAY0pHAAJx5ZjNDEM428.jpg",
    "timestamp": "2018-02-24 19:11:35"
}
複製程式碼

簡單搜尋

  • 搜尋指定索引庫中的所有文件

    GET /xc_course/_search

  • 搜尋指定type中的所有文件

    GET /xc_course/doc/_search

DSL搜尋

DSL(Domain Specific Language)是ES提出的基於json的搜尋方式,在搜尋時傳入特定的json格式的資料來完成不同的搜尋需求。

DSL比URI搜尋方式功能強大,在專案中建議使用DSL方式來完成搜尋。

DSL搜尋方式是使用POST提交,URI為以_search結尾(在某index或某type範圍內搜尋),而在JSON請求體中定義搜尋條件。

查詢所有文件——matchAllQuery

POST /xc_course/doc/_search

{
	"query":{
		"match_all":{}
	},
	"_source":["name","studymodel"]
}
複製程式碼

query用來定義搜尋條件,_source用來指定返回的結果集中需要包含哪些欄位。這在文件本身資料量較大但我們只想獲取其中特定幾個欄位資料時有用(既可過濾掉不必要欄位,又可提高傳輸效率)。

結果說明:

  • took,本次操作花費的時間,單位毫秒
  • time_out,請求是否超時(ES不可用或網路故障時會超時)
  • _shard,本次操作共搜尋了哪些分片
  • hits,命中的結果
  • hits.total,符合條件的文件數
  • hits.hits,命中的文件集
  • hits.max_score,hits.hits中各文件得分的最高分,文件得分即查詢相關度
  • _source,文件源資料
{
    "took": 57,
    "timed_out": false,
    "_shards": {
        "total": 1,
        "successful": 1,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": 3,
        "max_score": 1,
        "hits": [
            {
                "_index": "xc_course",
                "_type": "doc",
                "_id": "1",
                "_score": 1,
                "_source": {
                    "studymodel": "201002",
                    "name": "Bootstrap開發"
                }
            },
            {
                "_index": "xc_course",
                "_type": "doc",
                "_id": "2",
                "_score": 1,
                "_source": {
                    "studymodel": "201001",
                    "name": "java程式設計基礎"
                }
            },
            {
                "_index": "xc_course",
                "_type": "doc",
                "_id": "3",
                "_score": 1,
                "_source": {
                    "studymodel": "201001",
                    "name": "spring開發基礎"
                }
            }
        ]
    }
}
複製程式碼

Java程式碼實現:

@Test
public void testMatchAll() throws IOException {
    // POST     /xc_course/doc
    SearchRequest request = new SearchRequest("xc_course");    //DSL搜尋請求物件
    request.types("doc");
    SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();    //DSL請求體構造物件
    /**
         * {
         * 	"from":2,"size":1,
         * 	"query":{
         * 		"match_all":{
         *
         *                }* 	},
         * 	"_source":["name","studymodel"]
         * }
         */
    searchSourceBuilder.query(QueryBuilders.matchAllQuery());
    //引數1:要返回哪些欄位   引數2:不要返回哪些欄位    兩者通常指定其一
    searchSourceBuilder.fetchSource(new String[]{"name", "studymodel"}, null);
    //將請求體設定到請求物件中
    request.source(searchSourceBuilder);
    //發起DSL請求
    SearchResponse response = restHighLevelClient.search(request);
    System.out.println(response);
}
複製程式碼

DSL核心API

  • new SearchRequest(index),指定要搜尋的索引庫
  • searchRequest.type(type),指定要搜尋的type
  • SearchSourceBuilder,構建DSL請求體
  • searchSourceBuilder.query(queryBuilder),構造請求體中“query”:{}部分的內容
  • QueryBuilders,靜態工廠類,方便構造queryBuilder,如searchSourceBuilder.query(QueryBuilders.matchAllQuery())就相當於構造了“query”:{ "match_all":{} }
  • searchRequest.source(),將構造好的請求體設定到請求物件中

分頁查詢

PUT http://localhost:9200/xc_course/doc/_search

{
	"from":0,"size":1,
	"query":{
		"match_all":{
			
		}
	},
	"_source":["name","studymodel"]
}
複製程式碼

其中from的含義是結果集偏移,而size則是從偏移位置開始之後的size條結果。

{
    "took": 80,
    "timed_out": false,
    "_shards": {
        "total": 1,
        "successful": 1,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": 3,
        "max_score": 1,
        "hits": [
            {
                "_index": "xc_course",
                "_type": "doc",
                "_id": "1",
                "_score": 1,
                "_source": {
                    "studymodel": "201002",
                    "name": "Bootstrap開發"
                }
            }
        ]
    }
}
複製程式碼

這裡雖然hits.total為3,但是隻返回了第一條記錄。因此我們在做分頁功能時需要用到一個公式:from = (page-1)*size

Java程式碼實現

@Test
public void testPaginating() throws IOException {
    SearchRequest request = new SearchRequest("xc_course");
    request.types("doc");

    int page = 1, size = 1;
    SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
    searchSourceBuilder.from((page - 1) * size);
    searchSourceBuilder.size(size);
    searchSourceBuilder.query(QueryBuilders.matchAllQuery());
    searchSourceBuilder.fetchSource(new String[]{"name", "studymodel"}, null);

    request.source(searchSourceBuilder);
    SearchResponse response = restHighLevelClient.search(request);
    System.out.println(response);
}
複製程式碼

提取結果集中的文件

SearchResponse response = restHighLevelClient.search(request);
SearchHits hits = response.getHits();				//hits
if (hits != null) {
    SearchHit[] results = hits.getHits();			//hits.hits
    for (SearchHit result : results) {
        System.out.println(result.getSourceAsMap()); //hits.hits._source
    }
}
複製程式碼

詞項匹配——termQuery

詞項匹配是==精確匹配==,只有當倒排索引表中存在我們指定的詞項時才會返回該詞項關聯的文件集。

如搜尋課程名包含java詞項的文件

{
	"from":0,"size":1,
	"query":{
		"term":{ "name":"java" }
	},
	"_source":["name","studymodel"]
}
複製程式碼

結果如下:

"hits": {
    "total": 1,
    "max_score": 0.9331132,
    "hits": [
        {
            "_index": "xc_course",
            "_type": "doc",
            "_id": "2",
            "_score": 0.9331132,
            "_source": {
                "studymodel": "201001",
                "name": "java程式設計基礎"
            }
        }
    ]
}
複製程式碼

但如果你指定"term"{ "name":"java程式設計" }就搜尋不到了:

"hits": {
    "total": 0,
    "max_score": null,
    "hits": []
}
複製程式碼

因為“java程式設計基礎”在索引時會被分為“java”、“程式設計”、“基礎”三個詞項新增到倒排索引表中,因此沒有一個叫“java程式設計”的詞項和此次查詢匹配。

term查詢是精確匹配,term.name不會被search_analyzer分詞,而是會作為一個整體和倒排索引表中的詞項進行匹配。

根據id精確匹配——termsQuery

查詢id為1和3的文件

POST http://localhost:9200/xc_course/doc/_search

{
	"query":{
		"ids":{
			"values":["1","3"]
		}
	}
}
複製程式碼

Java實現

@Test
public void testQueryByIds(){
    SearchRequest request = new SearchRequest("xc_course");
    request.types("doc");

    SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
    List<String> ids = Arrays.asList(new String[]{"1", "3"});
    sourceBuilder.query(QueryBuilders.termsQuery("_id", ids));

    printResult(request, sourceBuilder);
}

private void printResult(SearchRequest request,SearchSourceBuilder sourceBuilder) {
    request.source(sourceBuilder);
    SearchResponse response = null;
    try {
        response = restHighLevelClient.search(request);
    } catch (IOException e) {
        e.printStackTrace();
    }
    SearchHits hits = response.getHits();
    if (hits != null) {
        SearchHit[] results = hits.getHits();
        for (SearchHit result : results) {
            System.out.println(result.getSourceAsMap());
        }
    }
}
複製程式碼

==大坑==

根據id精確匹配也是term查詢的一種,但是呼叫的API是termsQuery("_id", ids),注意是termsQuery而不是termQuery

全文檢索—— matchQuery

輸入的關鍵詞會被search_analyzer指定的分詞器分詞,然後根據所得詞項到倒排索引表中查詢文件集合,每個詞項關聯的文件集合都會被查出來,如查“bootstrap基礎”會查出“java程式設計基礎”:

POST

{
	"query":{
		"match":{
			"name":"bootstrap基礎"
		}
	}
}
複製程式碼

因為“bootstrap基礎”會被分為“bootstrap”和“基礎”兩個詞項,而詞項“基礎”關聯文件“java程式設計基礎”。

@Test
public void testMatchQuery() {

    SearchRequest request = new SearchRequest("xc_course");
    request.types("doc");

    SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
    sourceBuilder.query(QueryBuilders.matchQuery("name", "bootstrap基礎"));

    printResult(request, sourceBuilder);
}
複製程式碼

operator

上述查詢等同於:

{
    "query": {
        "match": {
            "name": {
                "query": "bootstrap基礎",
                "operator": "or"
            }
        }
    }
}
複製程式碼

即對檢索關鍵詞分詞後每個詞項的查詢結果取並集。

operator可取值orand,分別對應取並集和取交集。

如下查詢就只有一結果(課程名既包含“java”又包含“基礎”的只有“java程式設計基礎”):

{
    "query": {
        "match": {
            "name": {
                "query": "java基礎",
                "operator": "and"
            }
        }
    }
}
複製程式碼

Java程式碼

@Test
public void testMatchQuery2() {
    SearchRequest request = new SearchRequest("xc_course");
    request.types("doc");

    SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
    sourceBuilder.query(QueryBuilders.matchQuery("name", "java基礎").operator(Operator.AND));

    printResult(request, sourceBuilder);
}
複製程式碼

minimum_should_match

上邊使用的operator = or表示只要有一個詞匹配上就得分,如果實現三個詞至少有兩個詞匹配如何實現?

使用minimum_should_match可以指定文件匹配詞的佔比,比如搜尋語句如下:

{
    "query": {
        "match": {
            "name": {
                "query": "spring開發框架",
                "minimum_should_match":"80%"
            }
        }
    }
}
複製程式碼

“spring開發框架”會被分為三個詞:spring、開發、框架。

設定"minimum_should_match":"80%"表示,三個詞在文件的匹配佔比為80%,即3*0.8=2.4,向上取整得2,表 示至少有兩個詞在文件中才算匹配成功。

@Test
public void testMatchQuery3() {
    SearchRequest request = new SearchRequest("xc_course");
request.types("doc");

SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
sourceBuilder.query(QueryBuilders.matchQuery("name", "spring開發指南").minimumShouldMatch("70%")); //3*0.7 -> 2

printResult(request, sourceBuilder);
}
複製程式碼

多域檢索——multiMatchQuery

上邊學習的termQuerymatchQuery一次只能匹配一個Field,本節學習multiQuery,一次可以匹配多個欄位(即擴大了檢索範圍,之前一直都是在name欄位中檢索)。

如檢索課程名或課程描述中包含“spring”或“css”的文件:

{
    "query": {
        "multi_match": {
            "query": "spring css",
            "minimum_should_match": "50%",
            "fields": [
                "name",
                "description"
            ]
        }
    },
    "_source":["name","description"]
}
複製程式碼

Java:

@Test
public void testMultiMatchQuery() {
    SearchRequest request = new SearchRequest("xc_course");
    request.types("doc");

    SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
    sourceBuilder.query(QueryBuilders.
                        multiMatchQuery("spring css","name","description").
                        minimumShouldMatch("50%")); 

    printResult(request, sourceBuilder);
}
複製程式碼

boost權重

觀察上述查出的文件得分:

"hits": [
    {
        "_index": "xc_course",
        "_type": "doc",
        "_id": "3",
        "_score": 1.3339276,
        "_source": {
            "name": "spring開發基礎",
            "description": "spring 在java領域非常流行,java程式設計師都在用。"
        }
    },
    {
        "_index": "xc_course",
        "_type": "doc",
        "_id": "1",
        "_score": 0.69607234,
        "_source": {
            "name": "Bootstrap開發",
            "description": "Bootstrap是由Twitter推出的一個前臺頁面開發框架,是一個非常流行的開發框架,此框架整合了多種頁面效果。此開發框架包含了大量的CSS、JS程式程式碼,可以幫助開發者(尤其是不擅長頁面開發的程式人員)輕鬆的實現一個不受瀏覽器限制的精美介面效果。"
        }
    }
]
複製程式碼

你會發現文件3spring詞項在文件出現的次數佔文件詞項總數的比例較高因此得分(_score)較高。那我們猜想,是不是我們在文件1的課程描述中多新增幾個css能否提升其_score呢?

於是我們更新一下文件1:

@Test
public void testUpdateDoc() throws IOException {
    UpdateRequest request = new UpdateRequest("xc_course", "doc", "1");
    Map<String, Object> docMap = new HashMap<>();
    docMap.put("description", "Bootstrap是由Twitter推出的一個css前臺頁面開發框架,是一個非常流行的css開發框架,此框架整合了多種css頁面效果。此開發框架包含了大量的CSS、JS程式程式碼,可以幫助css開發者(尤其是不擅長css頁面開發的程式人員)輕鬆的實現一個不受瀏覽器限制的精美介面效果。");
    request.doc(docMap);
    UpdateResponse response = restHighLevelClient.update(request);
    System.out.println(response);
    testFindById();
}
複製程式碼

再次查詢發現文件1的得分果然變高了:

"hits": [
    {
        "_index": "xc_course",
        "_type": "doc",
        "_id": "1",
        "_score": 1.575484,
        "_source": {
            "name": "Bootstrap開發",
            "description": "Bootstrap是由Twitter推出的一個css前臺頁面開發框架,是一個非常流行的css開發框架,此框架整合了多種css頁面效果。此開發框架包含了大量的CSS、JS程式程式碼,可以幫助css開發者(尤其是不擅長css頁面開發的程式人員)輕鬆的實現一個不受瀏覽器限制的精美介面效果。"
        }
    },
    {
        "_index": "xc_course",
        "_type": "doc",
        "_id": "3",
        "_score": 1.346281,
        "_source": {
            "name": "spring開發基礎",
            "description": "spring 在java領域非常流行,java程式設計師都在用。"
        }
    }
]
複製程式碼

那我們有這樣一個業務需求:課程出現spring或css肯定是與spring或css相關度更大的課程,而課程描述出現則不一定。因此我們想提高課程出現關鍵詞項的得分權重,我們可以這麼辦(在name欄位後追加一個^符號並指定權重,預設為1):

{
    "query": {
        "multi_match": {
            "query": "spring css",
            "minimum_should_match": "50%",
            "fields": [
                "name^10",
                "description"
            ]
        }
    },
    "_source":["name","description"]
}
複製程式碼

Java:

@Test
public void testMultiMatchQuery2() {
    SearchRequest request = new SearchRequest("xc_course");
    request.types("doc");

    SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
    MultiMatchQueryBuilder multiMatchQueryBuilder = QueryBuilders.multiMatchQuery("spring css", "name", "description").minimumShouldMatch("50%");
    multiMatchQueryBuilder.field("name", 10);
    sourceBuilder.query(multiMatchQueryBuilder);

    printResult(request, sourceBuilder);
}
複製程式碼

布林查詢——boolQuery

布林查詢對應於Lucene的BooleanQuery查詢,實現==將多個查詢組合起來==。

三個引數

  • must:文件必須匹配must所包括的查詢條件,相當於 “AND”
  • should:文件應該匹配should所包括的查詢條件其中的一個或多個,相當於 "OR"
  • must_not:文件不能匹配must_not所包括的該查詢條件,相當於“NOT”

如查詢課程名包含“spring”==且==課程名或課程描述跟“開發框架”有關的:

{
    "query": {
        "bool":{
            "must":[
                {
                    "term":{
                        "name":"spring"
                    }
                },
                {
                    "multi_match":{
                        "query":"開發框架",
                        "fields":["name","description"]
                    }
                }
            ]
        }
    },
    "_source":["name"]
}
複製程式碼

Java

@Test
public void testBoolQuery() {
    SearchRequest request = new SearchRequest("xc_course");
    request.types("doc");

    SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();	//query
    BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder();		//query.bool

    TermQueryBuilder termQueryBuilder = QueryBuilders.termQuery("name", "spring");
    MultiMatchQueryBuilder multiMatchQueryBuilder = QueryBuilders.multiMatchQuery("開發框架", "name", "description");

    boolQueryBuilder.must(termQueryBuilder);		//query.bool.must
    boolQueryBuilder.must(multiMatchQueryBuilder);	
    sourceBuilder.query(boolQueryBuilder);

    printResult(request, sourceBuilder);
}
複製程式碼

必須滿足的條件放到must中(boolQueryBuilder.must(條件)),必須排斥的條件放到must_not中,只需滿足其一的條件放到should中。

查詢課程名必須包含“開發”但不包含“java”的,且包含“spring”或“boostrap”的課程:

{
    "query": {
       "bool":{
       		"must":[
				{
					"term":{
						"name":"開發"
					}
				}
       		],
       		"must_not":[
				{
					"term":{
						"name":"java"
					}
				}
       		],
       		"should":[
				{
					"term":{
						"name":"bootstrap"
					}
				},
				{
					"term":{
						"name":"spring"
					}
				}
       		]
       }
    },
    "_source":["name"]
}
複製程式碼

當然實際專案不會這麼設定條件,這裡只是為了演示效果,這裡為了演示方便用的都是termQuery,事實可用前面任意一種Query

Java

@Test
public void testBoolQuery2() {
    SearchRequest request = new SearchRequest("xc_course");
    request.types("doc");

    SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
    BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder();

    boolQueryBuilder.must(QueryBuilders.termQuery("name","開發"));
    boolQueryBuilder.mustNot(QueryBuilders.termQuery("name", "java"));
    boolQueryBuilder.should(QueryBuilders.termQuery("name","spring"));
    boolQueryBuilder.should(QueryBuilders.termQuery("name","bootstrap"));

    sourceBuilder.query(boolQueryBuilder);

    printResult(request, sourceBuilder);
}
複製程式碼

過濾器——filter

過濾是針對搜尋的結果進行過濾,==過濾器主要判斷的是文件是否匹配,不去計算和判斷文件的匹配度得分==,所以==過濾器效能比查詢要高,且方便快取==,推薦儘量使用過濾器去實現查詢或者過濾器和查詢共同使用。

過濾器僅能在布林查詢中使用。

全文檢索“spring框架”,並過濾掉學習模式代號不是“201001”和課程價格不在10~100之間的

{
    "query": {
        "bool": {
            "must": [
                {
                    "multi_match": {
                        "query": "spring框架",
                        "fields": [
                            "name",
                            "description"
                        ]
                    }
                }
            ],
            "filter": [
                {
                    "term": {
                        "studymodel": "201001"
                    }
                },
                {
                    "range": {
                        "price": {
                            "gte": "10",
                            "lte": "100"
                        }
                    }
                }
            ]
        }
    }
}
複製程式碼

Java

@Test
public void testBoolQuery3() {
    SearchRequest request = new SearchRequest("xc_course");
    request.types("doc");

    SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
    BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder();

    boolQueryBuilder.must(QueryBuilders.multiMatchQuery("spring框架", "name", "description"));
    boolQueryBuilder.filter(QueryBuilders.termQuery("studymodel", "201001"));
    boolQueryBuilder.filter(QueryBuilders.rangeQuery("price").gte(10).lte(100));

    sourceBuilder.query(boolQueryBuilder);

    printResult(request, sourceBuilder);
}
複製程式碼

排序

查詢課程價格在10~100之間的,並按照價格升序排列,當價格相同時再按照時間戳降序排列

{
    "query": {
        "bool": {
            "filter": [
                {
                    "range": {
                        "price": {
                            "gte": "10",
                            "lte": "100"
                        }
                    }
                }
            ]
        }
    },
    "sort": [
        {
            "price": "asc"
        },
        {
            "timestamp": "desc"
        }
    ],
    "_source": [
        "name",
        "price",
        "timestamp"
    ]
}
複製程式碼

Java

@Test
public void testSort() {
    SearchRequest request = new SearchRequest("xc_course");
    request.types("doc");

    SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();

    BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder();
    boolQueryBuilder.filter(QueryBuilders.rangeQuery("price").gte(10).lte(100));

    sourceBuilder.sort("price", SortOrder.ASC);
    sourceBuilder.sort("timestamp", SortOrder.DESC);

    sourceBuilder.query(boolQueryBuilder);
    printResult(request, sourceBuilder);
}
複製程式碼

高亮

{
    "query": {
        "bool": {
            "filter": [
                {
                    "multi_match": {
                        "query": "bootstrap",
                        "fields": [
                            "name",
                            "description"
                        ]
                    }
                }
            ]
        }
    },
    "highlight":{
        "pre_tags":["<tag>"],
        "post_tags":["</tag>"],
        "fields":{
            "name":{},
            "description":{}
        }
    }
}
複製程式碼

結果:

"hits": [
    {
        "_index": "xc_course",
        "_type": "doc",
        "_id": "1",
        "_score": 0,
        "_source": {
            "name": "Bootstrap開發",
            "description": "Bootstrap是由Twitter推出的一個css前臺頁面開發框架,是一個非常流行的css開發框架,此框架整合了多種css頁面效果。此開發框架包含了大量的CSS、JS程式程式碼,可以幫助css開發者(尤其是不擅長css頁面開發的程式人員)輕鬆的實現一個不受瀏覽器限制的精美介面效果。",
            "studymodel": "201002",
            "price": 38.6,
            "pic": "group1/M00/00/00/wKhlQFs6RCeAY0pHAAJx5ZjNDEM428.jpg",
            "timestamp": "2018-04-25 19:11:35"
        },
        "highlight": {
            "name": [
                "<tag>Bootstrap</tag>開發"
            ],
            "description": [
                "<tag>Bootstrap</tag>是由Twitter推出的一個css前臺頁面開發框架,是一個非常流行的css開發框架,此框架整合了多種css頁面效果。"
            ]
        }
    }
]
複製程式碼

hits結果集中的每個結果出了給出源文件_source之外,還給出了相應的高亮結果highlight

Java

@Test
public void testHighlight() throws IOException {
    SearchRequest request = new SearchRequest("xc_course");
    request.types("doc");
    SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();

    BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder();
    boolQueryBuilder.filter(QueryBuilders.multiMatchQuery("bootstrap","name","description"));

    HighlightBuilder highlightBuilder = new HighlightBuilder();
    highlightBuilder.preTags("<tag>");
    highlightBuilder.postTags("</tag>");
    highlightBuilder.field("name").field("description");

    sourceBuilder.query(boolQueryBuilder);
    sourceBuilder.highlighter(highlightBuilder);
    request.source(sourceBuilder);

    SearchResponse response = restHighLevelClient.search(request);
    SearchHits hits = response.getHits();       //hits
    if (hits != null) {
        SearchHit[] results = hits.getHits();   //hits.hits
        if (results != null) {
            for (SearchHit result : results) {
                Map<String, Object> source = result.getSourceAsMap();   //_source
                String name = (String) source.get("name");
                Map<String, HighlightField> highlightFields = result.getHighlightFields();  //highlight
                HighlightField highlightField = highlightFields.get("name");
                if (highlightField != null) {
                    Text[] fragments = highlightField.getFragments();
                    StringBuilder stringBuilder = new StringBuilder();
                    for (Text text : fragments) {
                        stringBuilder.append(text.string());
                    }
                    name = stringBuilder.toString();
                }
                System.out.println(name);

                String description = (String) source.get("description");
                HighlightField highlightField2 = highlightFields.get("description");
                if (highlightField2 != null) {
                    Text[] fragments = highlightField2.getFragments();
                    StringBuilder stringBuilder = new StringBuilder();
                    for (Text text : fragments) {
                        stringBuilder.append(text.string());
                    }
                    description = stringBuilder.toString();
                }
                System.out.println(description);
            }
        }
    }
}
複製程式碼

比較難理解的API是HighlightFieldshighlightField.getFragments(),我們需要對比響應JSO的結構來類比理解。

image

我們可以通過highlightFields.get()來獲取highlight.namehighlight.description對應的highlightField,但是為什麼hightField.getFragment返回的是一個Text[]而不是Text呢。我們猜測ES將文件按照句子分成了多個段,僅對出現關鍵詞項的段進行高亮並返回,於是我們檢索css測試一下果然如此:

image

因此你需要注意返回的highlight可能並不包含所有原欄位內容

image

叢集管理

ES通常以叢集方式工作,這樣做不僅能夠提高 ES的搜尋能力還可以處理大資料搜尋的能力,同時也增加了系統的容錯能力及高可用,ES可以實現PB級資料的搜尋。

下圖是ES叢集結構的示意圖:

image

叢集相關概念

節點

ES叢集由多個伺服器組成,每個伺服器即為一個Node節點(該服務只部署了一個ES程式)。

分片

當我們的文件量很大時,由於記憶體和硬碟的限制,同時也為了提高ES的處理能力、容錯能力及高可用能力,我們將索引分成若干分片(可以類比MySQL中的分割槽來看,一個表分成多個檔案),每個分片可以放在不同的伺服器,這樣就實現了多個伺服器共同對外提供索引及搜尋服務。

一個搜尋請求過來,會分別從各各分片去查詢,最後將查詢到的資料合併返回給使用者。

副本

為了提高ES的高可用同時也為了提高搜尋的吞吐量,我們將分片複製一份或多份儲存在其它的伺服器,這樣即使當前的伺服器掛掉了,擁有副本的伺服器照常可以提供服務。

主節點

一個叢集中會有一個或多個主節點,主節點的作用是叢集管理,比如增加節點,移除節點等,主節點掛掉後ES會重新選一個主節點。

節點轉發

每個節點都知道其它節點的資訊,我們可以對任意一個v發起請求,接收請求的節點會轉發給其它節點查詢資料。

節點的三個角色

主節點

master節點主要用於叢集的管理及索引 比如新增節點、分片分配、索引的新增和刪除等。

資料節點

data 節點上儲存了資料分片,它負責索引和搜尋操作。

客戶端節點

client 節點僅作為請求客戶端存在,client的作用也作為負載均衡器,client 節點不存資料,只是將請求均衡轉發到其它節點。

配置

可在/config/elasticsearch.yml中配置節點的功能:

  • node.master: #是否允許為主節點
  • node.data: #允許儲存資料作為資料節點
  • node.ingest: #是否允許成為協調節點(資料不在當前ES例項上時轉發請求)

四種組合方式:

  • master=true,data=true:即是主節點又是資料節點
  • master=false,data=true:僅是資料節點
  • master=true,data=false:僅是主節點,不儲存資料
  • master=false,data=false:即不是主節點也不是資料節點,此時可設定ingest為true表示它是一個客戶端。

搭建叢集

下我們來實現建立一個2節點的叢集,並且索引的分片我們設定2片,每片一個副本。

解壓elasticsearch-6.2.1.zip兩份為es-1es-2

配置檔案elasticsearch.yml

節點1:

cluster.name: xuecheng
node.name: xc_node_1
network.host: 0.0.0.0
http.port: 9200
transport.tcp.port: 9300
node.master: true
node.data: true
discovery.zen.ping.unicast.hosts: ["0.0.0.0:9300", "0.0.0.0:9301"]
discovery.zen.minimum_master_nodes: 1
node.ingest: true
bootstrap.memory_lock: false
node.max_local_storage_nodes: 2

path.data: D:\software\es\cluster\es-1\data
path.logs: D:\software\es\cluster\es-1\logs

http.cors.enabled: true
http.cors.allow-origin: /.*/
複製程式碼

節點2:

cluster.name: xuecheng
node.name: xc_node_2
network.host: 0.0.0.0
http.port: 9201
transport.tcp.port: 9301
node.master: true
node.data: true
discovery.zen.ping.unicast.hosts: ["0.0.0.0:9300", "0.0.0.0:9301"]
discovery.zen.minimum_master_nodes: 1
node.ingest: true
bootstrap.memory_lock: false
node.max_local_storage_nodes: 2

path.data: D:\software\es\cluster\es-2\data
path.logs: D:\software\es\cluster\es-2\logs

http.cors.enabled: true
http.cors.allow-origin: /.*/
複製程式碼

測試分片

建立索引,索引分兩片,每片有一個副本:

PUT http://localhost:9200/xc_course

{
    "settings": {
        "number_of_shards": 2,
        "number_of_replicas": 1
    }
}
複製程式碼

通過head外掛檢視索引狀態:

image

測試主從複製

寫入資料

POST http://localhost:9200/xc_course/doc

{
	"name":"java程式設計基礎"
}
複製程式碼

兩個結點均有資料:

image

image

叢集的健康

通過訪問 GET /_cluster/health 來檢視Elasticsearch 的叢集健康情況。

用三種顏色來展示健康狀態: green 、 yellow 或者 red 。

  • green:所有的主分片和副本分片都正常執行。
  • yellow:所有的主分片都正常執行,但有些副本分片執行不正常。
  • red:存在主分片執行不正常。

相關文章