ElasticSearch詳細筆記

DepthCh發表於2020-10-26

ElasticSearch詳細筆記


什麼是ElasticSearch

Elasticsearch(簡稱ES)是一個基於Apache Lucene(TM)的開源搜尋引擎,無論在開源還是專有領域,Lucene 可以被認為是迄今為止最先進、效能最好的、功能最全的搜尋引擎庫。注意,Lucene 只是一個庫。想要發揮其強大的作用,你需使用 Java 並要將其整合到你的應用中。

重要特性:

  • 分散式的實時檔案儲存,每個欄位都被索引並可被搜尋
  • 實時分析的分散式搜尋引擎
  • 可以擴充套件到上百臺伺服器,處理PB級結構化或非結構化資料

基本概念&倒排索引

需要了解ElasticSearch中的一些基本概念。

- 索引(indices)
	-- Databases 資料庫
	
- 型別(type)
	-- Table 資料表

- 文件(Document)
	-- Row 行

- 欄位(Field)
	-- Columns 列

ElasticSearch中的倒排索引

ElasticSearch在插入資料的同時還會為這些資料維護了一張倒排索引表,通過這個倒排索引可以大大的提高搜尋的效能

倒排索引(Inverted Index)也叫反向索引,有反向索引必有正向索引。簡單來講,正向索引是通過key找value,反向索引則是通過value找key。

舉個例子:

  1. comment 表有 idcontent 兩個欄位,現在向 comment 表插入如下一條資料:

    id:1
    content:今天天氣很好
    

    ElasticSearch 會把 content 的內容進行分詞,可以分成三個詞:今天天氣很好。倒排索引表就如下:

    今天		[1]
    天氣		[1]
    很好		[1]
    

    表示 "今天"、"天氣"、"很好"這三個詞在 1號記錄中存在。

  2. 再向 comment 表中插入一條資料:

    id: 2
    content: 今天天氣好冷
    

    繼續將 content 的內容進行分詞,得到:今天天氣好冷。將這三個詞新增到倒排索引表中

    今天		[1,2]
    天氣		[1,2]
    很好		[1]
    好冷		[2]
    

    "今天""天氣" 這兩個詞在 1號2號記錄中都存在。

    "很好"1號 記錄存在

    "好冷"2號記錄存在

  3. 現在查詢記錄,檢索條件:今天好冷

    通過 "今天好冷" 這個字串進行檢索記錄,這種就屬於通過value查詢key

    ElasticSearch 首先將 "今天好冷"進行分詞為:"今天""好冷"兩個詞。

    然後在倒排索引表中查詢,發現 "今天" 這個詞命中了 1號2號 記錄,再看 "好冷"這個詞命中了 2號記錄。

    這裡有個評分機制,2號記錄經過對比發現命中 2次1號記錄命中 1次。因此 2號記錄的評分就比 1號 記錄高。查詢出來的結果順序就是:

    id: 2 		content: 今天天氣好冷
    id: 1		content: 今天天氣很好
    

這就是倒排索引的基本邏輯,通過 value 查詢 key。實際上,ElasticSearch引擎建立的倒排索引比這個複雜得多。


安裝ElasticSearch&Kibana

Docker安裝ElasticSearch

  1. 下載映象

    docker pull elasticsearch:7.4.2		#儲存和檢索資料
    
  2. 建立例項需要掛載目錄

    mkdir -p /mydata/elasticsearch/config
    mkdir -p /mydata/elasticsearch/data
    echo "http.host: 0.0.0.0" >> /mydata/elasticsearch/config/elasticsearch.yml
    
    chmod -R 777 /mydata/elasticsearch/
    
  3. 建立執行例項

    docker run --name es -p 9200:9200 -p 9300:9300 \
    -e "discovery.type=single-node" \
    -e ES_JAVA_OPTS="-Xms64m -Xmx512m" \
    -v /mydata/elasticsearch/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml \
    -v /mydata/elasticsearch/data:/usr/share/elasticsearch/data \
    -v /mydata/elasticsearch/plugins:/usr/share/elasticsearch/plugins \
    -d elasticsearch:7.4.2
    

    -e "discovery.type=single-node":單例項模式

    -e ES_JAVA_OPTS="-Xms64m -Xmx512m":設定執行的初始記憶體和最大記憶體

    -v:將本地檔案對映到容器中對應檔案

  4. 瀏覽器訪問主機 9200

    image-20201021155940036


Docker安裝Kibana

Kibana 是一個基於 Node.js 的 Elasticsearch 索引庫資料統計工具,可以利用 Elasticsearch 的聚合功能,生成各種圖表,如柱形圖,線狀圖,餅圖等。

而且還提供了操作 Elasticsearch 索引資料的控制檯,並且提供了一定的API提示,非常有利於我們學習 Elasticsearch 的語法。

安裝步驟:

  1. 下載映象

    docker pull kibana:7.4.2
    
  2. 檢視 ElasticSearch 例項地址

    docker inspect es
    

    image-20201021155709453

    es:執行的 elasticsearch 容器例項名

  3. 建立執行例項

    docker run --name kibana -e ELASTICSEARCH_HOSTS=http://172.17.0.3:9200 -p 5601:5601 
    -d kibana:7.4.2
    

    172.17.0.3:地址填寫上一步查詢到的地址

  4. 瀏覽器訪問主機 5601

    image-20201021160408049

    注意:kibana啟動可能有點慢,需要等待一會


ES基本操作

_cat

elasticsearch 提供 _cat API 來檢視ElasticSearch狀態

#0. 檢視_cat支援的命令
GET /_cat

#1. 檢視所有節點
GET /_cat/nodes

#2. 檢視es健康狀態
GET /_cat/health

#3. 檢視主節點
GET /_cat/master

#4. 檢視所有索引
GET /_cat/indices

例子:http://192.168.23.6:9200/_cat/indices


新增資料

elasticsearch 中儲存的都是 json 格式的資料

現在向 es 新增一條資料 { "msg": "Hello ElasticSearch" }

  1. 訪問 kibana,選擇 Dev Tools

    image-20201021162052766

  2. 在這個介面操作 es

    image-20201021162155249


  • PUT 方式

    PUT /news/comment/1
    {
     "msg":"Hello ElasticSearch"
    }
    

    執行結果:

    image-20201021162451925

  • POST 方式

    POST /news/comment/1
    {
     "name":"Hello ElasticSearch"
    }
    

    執行結果:

    image-20201021162710889

可以理解為向 news 資料庫的 comment 表中新增了一條記錄,不過這裡叫做索引和型別

分析結果:

_index: 索引,對應就是資料庫名
_type: 型別,對應就是資料表
_id: 資料的id
_version: 版本號,通過運算元據版本號會不斷增加
_result: created表示建立了一條資料,如果重新put一條資料,則該狀態會變為updated,並且版本號也會發生變化。
_shards: 分片資訊
_seq_no: 序列號
_primary_term:	
  • PUT可以新增也可以修改。PUT必須指定id;
  • POST新增資料的時候不指定id,會自動的生成id,並且型別是新增
  • 由於PUT需要指定id,我們一般用來做修改操作,不指定id會報錯。

查詢資料

檢視使用GET請求方式檢索資料

GET /news/comment/1

image-20201021164233574

可以理解為向 news 資料庫的 comment 表中查詢一條id1的記錄

_source:儲存的資料


更新資料

  • POST 方式

    POST /news/comment/1/_update
    {
    	"doc": {
    		"msg": "Hello ES"
    	}
    }
    

    POST /customer/external/1
    {
    	"msg": "Hello ES"
    }
    

    使用 _update 需要加 doc

    區別:

    • 使用 _update修改資料,版本號不會增加
  • PUT 方式

    PUT /news/comment/1
    {
      "msg": "Hello ES"
    }
    

刪除資料

刪除一條資料

DELETE /news/comment/1

刪除一個索引

DELETE /customer

bulk批量API

bulk相當於資料庫裡的bash操作, 其支援的操作型別包括:index, create, update, delete

  • bulk 語法:

    { action: { metadata } }
    { requstbody }
    

批量新增 index

POST /news/comment/_bulk
{ "index": {"_id": 2} }
{ "msg": "zhangsan" }
{ "index": {"_id": 3} }
{ "msg": "lisi" }
{ "index": {"_id": 4} }
{ "msg": "wangwu" }

執行結果:

image-20201021170543125

  • 引數解析:

    { "index": {"_id": 2} }

    { "msg": "zhangsan" }

    這兩行為一次操作,第一行指定了資料的id(還可以指定 indextype;以下劃線開頭)

    第二行是儲存的資料體

  • 結果解析:

    "took": 31:請求執行時間(毫秒)

    "error": false :請求是否出錯,返回flase表示沒有出錯

    "items":操作過的文件的具體資訊

    "static":響應狀態碼


批量新增 create

POST /news/comment/_bulk
{ "create": {"_id": 2} }
{ "msg": "zhangsan" }
{ "create": {"_id": 3} }
{ "msg": "lisi" }
{ "create": {"_id": 4} }
{ "msg": "wangwu" }

執行結果:

image-20201021182310732

新增失敗了,因為id重複問題

  • create方式新增,如果id已存在了就會報錯
  • index方式新增,如果id存在不會報錯,並且 version 增加

批量更新 update

POST /news/comment/_bulk
{ "update": {"_id": 2} }
{ "doc":{"msg": "zhangsan.cn"} }
{ "update": {"_id": 3} }
{ "doc":{"msg": "lisi.cn"} }
{ "update": {"_id": 4} }
{ "doc":{"msg": "wangwu.cn"} }

執行結果:

image-20201021182742765

更新操作需要多加一層 doc

  • { "update": {"_id": 2} }

  • { "doc":{"msg": "zhangsan.cn"} }

​ 第一行update為更新操作,並指定了更新資料的id

​ 第二行 doc裡面是更新的新資料。


批量刪除 delete

批量刪除不需要請求體(資料體)

POST /news/comment/_bulk
{ "delete": {"_id": 2} }
{ "delete": {"_id": 3} }
{ "delete": {"_id": 4} }

執行結果:

image-20201021171911072


進階檢索

學習之前先為es新增一些測試資料,這裡使用官方提供的測試資料 https://gitee.com/depthch/elasticsearch/blob/master/doc/test/resourses/accounts.json

  1. 開啟上面連結將裡面的資料複製

    image-20201021192145259

  2. 在 kibana 中使用 bulk 批量新增資料

    image-20201021192334733

    在索引為 bank,型別 account中批量插入了資料

ES支援兩種基本方式檢索

  • 通過REST request uri 傳送搜尋引數 (uri +檢索引數)
  • 通過REST request body 來傳送它們(uri+請求體)

請求方式:uri + 檢索引數

1、檢索 bank 下所有資訊

GET /bank/_search

image-20201021192706209

響應結果解析:

  • took:Elasticsearch執行搜尋的時間(毫秒)
  • time_out:告訴我們搜尋是否超時
  • _shards:告訴我們多少個分片被搜尋了,以及統計了成功/失敗的搜尋分片
  • hits:搜尋結果
  • hits.total:搜尋結果數量
  • hits.hits:實際的搜尋結果陣列(預設為前10的文件)
  • sort:結果的排序key (鍵) (沒有則按score排序)
  • score和max score:相關性得分和最高得分(全文檢索用)

2、檢索 bank 下所有資訊,並按照 account_number升序

GET /bank/_search?q=*&sort=account_number:asc

image-20201021193508003

  • q=*:* 是萬用字元,表示查詢所有的資料
  • sort=account_number:asc:按照 account_number 排序,asc 是升序

queryDSL

請求方式:uri + 請求體

基本語法

QUERY_NAME:{
   ARGUMENT:VALUE,
   ARGUMENT:VALUE,...
}

1、檢索 bank 下所有資訊,並按照 account_number降序

GET /bank/_search
{
  "query": {
    "match_all": {}
  },
  "sort": [
    {
      "account_number": {
        "order": "desc"
      }
    }
  ]
}

image-20201021193913924

查詢引數解析:

  • match_all查詢型別【代表查詢所有的所有】,es中可以在query中組合非常多的查詢型別完成複雜查詢;
  • 除了query引數之外,我們可也傳遞其他的引數以改變查詢結果,如sort,size;
  • from+size限定,完成分頁功能;
  • sort排序,多欄位排序,會在前序欄位相等時後續欄位內部排序,否則以前序為準;

_source

_source:返回指定的部分欄位

GET bank/_search
{
  "query": {
    "match_all": {}
  },
  "from": 0,
  "size": 5,
  "sort": [
    {
      "account_number": {
        "order": "desc"
      }
    }
  ],
  "_source": ["balance","firstname"]
}

_source:指定返回的部分欄位


match匹配查詢

  • 基本型別(非字串),精確控制

    GET bank/_search
    {
      "query": {
        "match": {
          "account_number": "20"
        }
      }
    }
    
  • 字串,全文檢索

    GET bank/_search
    {
      "query": {
        "match": {
          "address": "kings"
        }
      }
    }
    

match_phrase

match_phrase: 短句匹配,將需要匹配的值當成一整個單詞(不分詞)進行檢索

GET bank/_search
{
  "query": {
    "match_phrase": {
      "address": "mill road"
    }
  }
}

查出 address 中包含 mill road 的所有記錄,並給出相關性得分

match_phrasematch 的區別:

  • match:匹配時會分詞,如:mill road,會拆分成:mill、road。然後檢索出包含這兩個詞的記錄(包含其中一個詞也滿足條件)
  • match_phrase:匹配時不會分詞,如:mill road,會被當成一個整體來檢索記錄,必須包含整個整體的記錄才會被檢索出來

match.keyword

keyword 是精確匹配,就是說某條記錄必須完全滿足匹配條件才會被檢索出來

GET bank/_search
{
  "query": {
    "match": {
      "address.keyword": "990 Mill Road"
    }
  }
}

multi_math

multi_math:多欄位匹配可以在多個欄位中去匹配條件

GET bank/_search
{
  "query": {
    "multi_match": {
      "query": "mill",
      "fields": [
        "state",
        "address"
      ]
    }
  }
}

state 或者 address 中包含 mill,並且在查詢過程中,會對於查詢條件進行分詞。


bool

bool:用來做複合查詢,複合語句可以合併,任何其他查詢語句,包括符合語句。這也就意味著,複合語句之間可以互相巢狀,可以表達非常複雜的邏輯。


must

must:必須達到must所列舉的所有條件

GET bank/_search
{
   "query":{
        "bool":{
             "must":[
              {"match":{"address":"mill"}},
              {"match":{"gender":"M"}}
             ]
         }
    }
}

匹配 genderM並且 address包含 mill的文件


must_not

must_not,必須不匹配must_not所列舉的所有條件。

GET bank/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "gender": "M"
          }
        },
        {
          "match": {
            "address": "mill"
          }
        }
      ],
      "must_not": [
        {
          "match": {
            "age": "38"
          }
        }
      ]
    }
  }

匹配 genderM並且 address包含 mill的文件,但是 age 不等於38的資料


should

should:應該達到should列舉的條件,如果到達會增加相關文件的評分,並不會改變查詢的結果。

如果query中只有should且只有一種匹配規則,那麼should的條件就會被作為預設匹配條件二區改變查詢結果。

GET bank/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "gender": "M"
          }
        },
        {
          "match": {
            "address": "mill"
          }
        }
      ],
      "must_not": [
        {
          "match": {
            "age": "18"
          }
        }
      ],
      "should": [
        {
          "match": {
            "lastname": "Wallace"
          }
        }
      ]
    }
  }
}

should"應該包含"的意思,不是必須包含,也就是除去其他匹配條件即使 lastName不包含 Wallace也能匹配成功。但是如果有資料的 lastName 包含 Wallace ,那麼這條資料的相關性得分會更高,即優先匹配。


Filter

並不是所有的查詢都需要產生分數,特別是哪些僅用於filtering過濾的文件。為了不計算分數,elasticsearch會自動檢查場景並且優化查詢的執行。

GET bank/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "address": "mill"
          }
        }
      ],
      "filter": {
        "range": {
          "balance": {
            "gte": "10000",
            "lte": "20000"
          }
        }
      }
    }
  }
}

這裡先是查詢所有匹配 address 包含 mill 的文件,然後再根據 10000<=balance<=20000進行過濾查詢結果

filter在使用過程中,並不會計算相關性得分,即 "_score" : 0.0


term

match一樣。匹配某個屬性的值。

  • 全文檢索欄位用 match

  • 非text欄位匹配用term

GET bank/_search
{
  "query": {
    "term": {
      "address": "mill Road"
    }
  }
}

使用 term 匹配 text型別資料是匹配不到任何資料的。


Aggregation

聚合提供了從資料中分組和提取資料的能力。最簡單的聚合方法大致等於SQL Group by和SQL聚合函式。在elasticsearch中,執行搜尋返回this(命中結果),並且同時返回聚合結果,把以響應中的所有hits(命中結果)分隔開的能力。這是非常強大且有效的,你可以執行查詢和多個聚合,並且在一次使用中得到各自的(任何一個的)返回結果,使用一次簡潔和簡化的API啦避免網路往返。

聚合語法:

"aggs":{
    "aggs_name這次聚合的名字,方便展示在結果集中":{
        "AGG_TYPE聚合的型別(avg,terms....)":{}
     }
}

常用聚合型別:

  • avg:求平均值
  • max:求最大值
  • min:求最小值
  • sum:求和
  • filter:過濾聚合。基於一個條件,來對當前的文件進行過濾的聚合。
  • terms:詞聚合。基於某個field,該 field 內的每一個【唯一詞元】為一個桶,並計算每個桶內文件個數。預設返回順序是按照文件個數多少排序。

搜尋address中包含mill的所有人的年齡分佈以及平均年齡

GET bank/_search
{
  "query": {
    "match": {
      "address": "Mill"
    }
  },
  "aggs": {
    "ageAgg": {
      "terms": {
        "field": "age",
        "size": 10
      }
    },
    "ageAvg": {
      "avg": {
        "field": "age"
      }
    }
  }
}

查出所有年齡分佈,並且求這些年齡段的這些人的平均薪資

GET bank/_search
{
  "query": {
    "match_all": {}
  },
  "aggs": {
    "ageAgg": {
      "terms": {
        "field": "age",
        "size": 100
      },
      "aggs": {
        "ageAvg": {
          "avg": {
            "field": "balance"
          }
        }
      }
    }
  }
}

聚合是可以巢狀聚合的。


查出所有年齡分佈,並且這些年齡段中M的平均薪資和F的平均薪資以及這個年齡段的總體平均薪資

GET bank/_search
{
  "query": {
    "match_all": {}
  },
  "aggs": {
    "ageAgg": {
      "terms": {
        "field": "age",
        "size": 100
      },
      "aggs": {
        "genderAgg": {
          "terms": {
            "field": "gender.keyword"
          },
          "aggs": {
            "balanceAvg": {
              "avg": {
                "field": "balance"
              }
            }
          }
        },
        "ageBalanceAvg": {
          "avg": {
            "field": "balance"
          }
        }
      }
    }
  }
}

先按年齡聚合查出了所有分佈情況,再巢狀聚合 按性別聚合查出分佈情況,最後再巢狀聚合 按薪資聚合查出平均薪資。

ageBalanceAvg:根據年齡分佈計算出平均工資,這個聚合跟性別無關。


mapping

maping是用來定義一個文件(document),以及它所包含的屬性(field)是如何儲存和索引的。比如:使用maping來定義:

  • 哪些字串屬性應該被看做全文字屬性(full text fields);
  • 哪些屬性包含數字,日期或地理位置;
  • 文件中的所有屬性是否都能被索引(all 配置);
  • 日期的格式;
  • 自定義對映規則來執行動態新增屬性;

檢視 bank 索引的 mapping 對映資訊

GET bank/_mapping

執行結果:

image-20201022093044246

properties 中可以看到每個 field 的欄位型別


新版本的改變

ElasticSearch7 去掉了type(表)概念

  1. 關係型資料庫中兩個資料表示是獨立的,即使他們裡面有相同名稱的列也不影響使用,但ES中不是這樣的。elasticsearch是基於Lucene開發的搜尋引擎,而ES中不同type下名稱相同的filed最終在Lucene中的處理方式是一樣的。
    • 兩個不同type(表)下的兩個名稱 user_name(欄位),在ES同一個索引下其實被認為是同一個filed,你必須在兩個不同的type(表)中定義相同的 filed 對映。否則,不同 type(表)中的相同欄位名稱就會在處理中出現衝突的情況,導致Lucene處理效率下降。
    • 去掉type(表)就是為了提高ES處理資料的效率。
  2. Elasticsearch 7.x URL中的type引數為可選。比如,索引一個文件不再要求提供文件型別。
  3. Elasticsearch 8.x 不再支援URL中的type引數。

建立對映

PUT /student
{
  "mappings": {
    "properties": {
      "age": {
        "type": "integer"
      },
      "email": {
        "type": "keyword"
      },
      "name": {
        "type": "text"
      }
    }
  }
}

執行結果:

image-20201022093735268

建立 student 索引並指定了索引的 mapping 對映資訊。

properties:指定對映的欄位和欄位型別


檢視對映

GET /student

執行結果:

image-20201022094212471


新增新的欄位對映

student索引新增一個新的欄位對映

PUT /student/_mapping
{
  "properties": {
    "id": {
      "type": "keyword",
      "index": false
    }
  }
}

執行結果:

image-20201022094407963

再次檢視對映:
image-20201022094455600

"index": false:表明新增的欄位不能被檢索,只是一個冗餘欄位。


資料遷移

由於ElasticSearch是不支援修改對映欄位的,只能新增對映欄位。如果必須修改就需要資料遷移。

需求:將 bank 索引的所有資料遷移到 newbank 索引下,並將 age 欄位型別改為 integer。修改 city、email、employer、gender等欄位型別改為 keyword


具體步驟:

  1. 檢視 bank 的對映資訊

    GET /bank
    

    image-20201022100337166

  2. 建立一個跟 bank 索引欄位相同 mapping 對映的索引,並且改變欄位型別

    PUT /newbank
    {
      "mappings": {
        "properties": {
          "account_number": {
            "type": "long"
          },
          "address": {
            "type": "text"
          },
          "age": {
            "type": "integer"
          },
          "balance": {
            "type": "long"
          },
          "city": {
            "type": "keyword"
          },
          "email": {
            "type": "keyword"
          },
          "employer": {
            "type": "keyword"
          },
          "firstname": {
            "type": "text"
          },
          "gender": {
            "type": "keyword"
          },
          "lastname": {
            "type": "text",
            "fields": {
              "keyword": {
                "type": "keyword",
                "ignore_above": 256
              }
            }
          },
          "state": {
            "type": "keyword"
          }
        }
      }
    }
    

    在指定對映資訊時改變了欄位的型別。

  3. 可以檢視到 newbank 索引的對映資訊

    GET /newbank
    

    執行結果:
    image-20201022100846045

    欄位型別都已經改變。

  4. bank 索引中的資料遷移到 newbank 索引中

    POST _reindex
    {
      "source": {
        "index": "bank",
        "type": "account"
      },
      "dest": {
        "index": "newbank"
      }
    }
    

    執行結果:

    image-20201022100929894

    source:指定舊索引資訊

    • index:指定舊的索引名
    • type:指定 type,如果沒有 type 可以不指定。

    dest:指定新索引資訊

    • index:指定新的索引名
  5. 檢視 newbank 中的資料

    GET /newbank/_search
    

    執行結果:

    image-20201022101505363

    檢索了1000條資料,資料遷移成功。發現 type: _doc,我們在建立對映關係時並沒有設定 type,這是因為ElasticSearch7 去掉了type(表)概念,但是有個預設的 type 就是 _doc


分詞

一個 tokenizer(分詞器)接收一個字元流,將之分割為獨立的 tokens(詞元,通常是獨立的單詞),然後輸出tokens 流。例如:hello world 遇到空白字元時分割文字。它會將文字 "hello world" 分割為 [hello, world]

tokenizer(分詞器)還負責記錄各個terms(詞條)的順序或position位置(用於phrase短語和word proximity詞近鄰查詢),以及term(詞條)所代表的原始word(單詞)start(起始)end(結束)character offsets(字串偏移量) (用於高亮顯示搜尋的內容)

elasticsearch 提供了很多內建的分詞器,可以用來構建 custom analyzers(自定義分詞器)


使用分詞器

POST _analyze
{
  "analyzer": "standard",
  "text": "Nothing is impossible!"
}

執行結果:

image-20201022123521832

standard:ElasticSearch預設的分詞器,預設就是按空格進行分詞的


安裝 ik 分詞器

所有的語言分詞,預設使用的都是“Standard Analyzer”,但是這些分詞器針對於中文的分詞,並不友好。為此需要安裝中文的分詞器。

具體步驟:

  1. 檢視 ElasticSearch 的版本

    image-20201022125054594

    訪問es主機的 9200 埠檢視es版本

  2. 下載對應版本的 ik 分詞器

    在前面安裝的elasticsearch時,我們已經將elasticsearch容器的“/usr/share/elasticsearch/plugins”目錄,對映到本地主機的“ /mydata/elasticsearch/plugins”目錄下.

    2.1)進入 /mydata/elasticsearch/plugins目錄

    cd /mydata/elasticsearch/plugins
    

    2.2)下載 ik 分詞器

    wget https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.4.2/elasticsearch-analysis-ik-7.4.2.zip
    

    注意 7.4.2是自己es對應的版本。

  3. 解壓下載好的檔案

    unzip elasticsearch-analysis-ik-7.4.2.zip -d ik
    
  4. 重啟es

    docker restart es
    
  5. 檢視安裝好的ik

    GET _cat/plugins
    

    執行結果:

    image-20201022130421168

    可以看到我們的ik分詞器已經配置成功了


使用ik分詞器

ik分詞器有兩種分詞模式:ik_max_wordik_smart 模式。

  • ik_max_word

    會將文字做最細粒度的拆分,比如會將“中華人民共和國人民大會堂”拆分為“中華人民共和國、中華人民、中華、華人、人民共和國、人民、共和國、大會堂、大會、會堂等詞語。

  • ik_smart

    會做最粗粒度的拆分,比如會將“中華人民共和國人民大會堂”拆分為中華人民共和國、人民大會堂。


先來看看預設的分詞器

GET _analyze
{
   "text":"每天都要努力"
}

執行結果:

image-20201022135515279



ik 分詞器

GET _analyze
{
   "analyzer": "ik_smart", 
   "text":"每天都要努力"
}

執行結果:

image-20201022135630622

可以看到使用 ik 分詞器可以把一些常用的中文詞分出來了。


自定義詞庫

雖然使用 ik 分詞器預設的詞庫已經可以實現常用的中文分詞了,但是如果我們要分的詞不常用,如:張明想學Java

GET _analyze
{
  "analyzer": "ik_smart",
   "text":"張明想學Java"
}

執行結果:

image-20201022140850506

可以看到這裡把 "張明" 拆成了 "張""明",這並我是預想的效果,"張明" 應該拆成整體。

使用自定義詞庫,因為要使用 "遠端擴充套件字典",因此就需要一個遠端的字典檔案。這裡可以使用 nginx來配置遠端擴充套件字典檔案。文件最後有nginx安裝和配置步驟


具體步驟:

  1. 配置好 nginx 後,建立詞庫檔案

    vi /mydata/nginx/html/fenci.txt
    

    新增如下內容並儲存:

    張明
    學java
    

    image-20201022143225338

  2. 修改es-plugins配置檔案

    vi /mydata/elasticsearch/plugins/ik/config/IKAnalyzer.cfg.xml
    
    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
    <properties>
            <comment>IK Analyzer 擴充套件配置</comment>
            <!--使用者可以在這裡配置自己的擴充套件字典 -->
            <entry key="ext_dict"></entry>
             <!--使用者可以在這裡配置自己的擴充套件停止詞字典-->
            <entry key="ext_stopwords"></entry>
            <!--使用者可以在這裡配置遠端擴充套件字典 -->
            <entry key="remote_ext_dict">http://192.168.16.6/fenci.txt</entry>
            <!--使用者可以在這裡配置遠端擴充套件停止詞字典-->
            <!-- <entry key="remote_ext_stopwords">words_location</entry> -->
    </properties>
    

    注意:修改第10行需要改成 nginx 服務的地址(這行預設是註釋的)。

  3. 修改了配置檔案需要重啟 es

    docker restart es
    

  1. 再次使用 ik 分詞器

    GET /_analyze
    {
      "analyzer": "ik_max_word",
       "text":"張明想學Java"
    }
    

//TODO 執行結果

......



SpringBoot 整合 ElasticSearch

SpringBoot可以通過 92009300埠來呼叫 ElasticSearch,它們之間的區別:

  • 9300:TCP

    SpringBoot提供了 spring-data-elasticsearch:transport-api.jar;來對 ES呼叫。這種方式有些缺陷:

    • springboot版本不同,transport-api.jar不同,不能適配es版本
    • es7.x已經不建議使用,es8以後就要廢棄
  • 9200:HTTP

    jestClient:非官方,更新慢;

    RestTemplate:模擬HTTP請求,ES很多操作需要自己封裝,麻煩;

    HttpClient:模擬HTTP請求,ES很多操作需要自己封裝,麻煩;


    Elasticsearch-Rest-Client:官方RestClient,封裝了ES操作,API層次分明,上手簡單;

根據上面分析,我們最終選擇 Elasticsearch-Rest-Client 來進行呼叫es


具體步驟:

  1. 新增依賴

    <dependency>
        <groupId>org.elasticsearch.client</groupId>
        <artifactId>elasticsearch-rest-high-level-client</artifactId>
        <version>7.4.2</version>
    </dependency>
    
    <dependency>
        <groupId>org.elasticsearch</groupId>
        <artifactId>elasticsearch</artifactId>
        <version>7.4.2</version>
    </dependency>
    
    <dependency>
        <groupId>org.elasticsearch.client</groupId>
        <artifactId>elasticsearch-rest-client</artifactId>
        <version>7.4.2</version>
    </dependency>
    
  2. 建立一個 ElasticSearch的配置類

    @Configuration
    public class ElasticSearchConfig {
        public static final RequestOptions COMMON_OPTIONS;
        static {
            RequestOptions.Builder builder = RequestOptions.DEFAULT.toBuilder();
            COMMON_OPTIONS = builder.build();
        }
    
        @Bean
        public RestHighLevelClient esRestClient(){
            RestHighLevelClient client = new RestHighLevelClient(
                    RestClient.builder(new HttpHost("192.168.16.6", 9200, "http")));
            return client;
        }
    }
    

    HttpHost("192.168.16.6", 9200, "http")

    • 192.168.16.6:es服務的地址
    • 9200:es服務9200埠
    • http:使用http協議

    配置類中建立了一個 JavaBen,之後通過這個 JavaBean 來呼叫 ElasticSearch 的相關 API


儲存資料

@RunWith(SpringRunner.class)
@SpringBootTest
public class MallSearchApplicationTests {
    @Autowired
    RestHighLevelClient client;

    /**
     * 測試儲存資料
     */
    @Test
    public void contextLoads() throws IOException {
        IndexRequest request = new IndexRequest("users");  //建立索引物件
        request.id("10");  //設定id
        //source方法可以直接傳入多個鍵值對值儲存
		//request.source("name", "lisi", "age", 24, "gender", "男");  

        User user = new User();   //建立一個實體類user
        user.setName("java");
        user.setAge(24);
        user.setGender("男");
        
        String jsonString = JSON.toJSONString(user);  //解析實體轉成json字串

        request.source(jsonString, XContentType.JSON);  //傳入json格式字串儲存
        IndexResponse response = client.index(request, ElasticSearchConfig.COMMON_OPTIONS);
        
        System.out.println(response);	//列印結果
    }
	
    /**
   	 * 定義 user 實體類
   	 */
    @Data
    class User{
        private String name;
        private int age;
        private String gender;
 }
}

執行結果:

image-20201022151529191

   IndexResponse[index=users,type=_doc,id=10,version=2,result=updated,seqNo=3,primaryTerm=6,shards={"total":2,"successful":1,"failed":0}]

kibana檢視:

image-20201022151648229

資料測試新增成功。


檢索資料

搜尋address中包含mill的所有人的年齡分佈以及平均年齡

QueryDSL 實現:

GET bank/_search
{
  "query": {
    "match": {
      "address": "Mill"
    }
  },
  "aggs": {
    "ageAgg": {
      "terms": {
        "field": "age",
        "size": 10
      }
    },
    "ageAvg": {
      "avg": {
        "field": "age"
      }
    }
  }
}

執行結果:image-20201022153055301


java程式碼實現:

首先需要生成實體類,因為從es獲取的資料在java中最終都會儲存為java物件。

image-20201022154412377

需要根據 _source 中的欄位生成 java 類 account

測試類程式碼:

@RunWith(SpringRunner.class)
@SpringBootTest
public class MallSearchApplicationTests {

    @Autowired
    RestHighLevelClient client;

    /**
     * 按照 bank 索引裡的 _source 資料欄位建立對應的實體類
     */
    @Data
    @ToString
    static class Account{
        private int account_number;
        private int balance;
        private String firstname;
        private String lastname;
        private int age;
        private String gender;
        private String address;
        private String employer;
        private String email;
        private String city;
        private String state;
    }
    
    /**
     * 檢索資料
     */
    @Test
    public void searchData() throws IOException {
        //1、建立檢索物件
            SearchRequest searchRequest = new SearchRequest();
            //指定索引
            searchRequest.indices("bank");
            //指定DSL&檢索條件
            SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();

            //構建檢索條件
            //sourceBuilder.query();
            //sourceBuilder.from();
            //sourceBuilder.size();
            //sourceBuilder.aggregation();
            sourceBuilder.query(QueryBuilders.matchQuery("address", "mill"));

            //構建聚合條件: 按照年齡值分佈進行聚合
            TermsAggregationBuilder ageAgg = AggregationBuilders.terms("ageAgg").field("age").size(10);
            sourceBuilder.aggregation(ageAgg);

            //構建聚合條件: 計算平均工資
            AvgAggregationBuilder ageAvgAgg = AggregationBuilders.avg("ageAvgAgg").field("age");
            sourceBuilder.aggregation(ageAvgAgg);

            searchRequest.source(sourceBuilder);

            SearchResponse searchResponse = client.search(searchRequest, ElasticSearchConfig.COMMON_OPTIONS);

            //結果分析
            SearchHits hits = searchResponse.getHits();
            SearchHit[] hitsHits = hits.getHits();
            for(SearchHit hit : hitsHits){
                String sourceAsString = hit.getSourceAsString();
                
                //將結果轉成 javeBean
                Account account = JSON.parseObject(sourceAsString, Account.class);
                System.out.println("account:" + account);
            }

            //獲取檢索到的聚合資訊
            Aggregations aggregations = searchResponse.getAggregations();
            Terms ageAgg1 = aggregations.get("ageAgg");

            // 列印聚合結果
            for (Terms.Bucket bucket : ageAgg1.getBuckets()) {
                String keyAsString = bucket.getKeyAsString();
                System.out.println("年齡:" + keyAsString + "===> " + bucket.getDocCount());
            }

            Avg balanceAgg1 = aggregations.get("ageAvgAgg");
            System.out.println("平均年齡:" + balanceAgg1.getValueAsString());
    }
}

執行結果:

image-20201022153827676

這裡執行結果跟queryDSL查詢是相同的。


附:Docker 安裝 Nginx

  1. 隨便啟動一個nginx例項,只是為了複製出配置

    docker run -p 80:80 --name nginx -d nginx:1.10
    
  2. 建立目錄並且將容器內部配置檔案拷貝到外部

    mkdir -p /mydata/nginx/html
    mkdir -p /mydata/nginx/logs
    mkdir -p /mydata/nginx/conf
    docker container cp nginx:/etc/nginx/*  /mydata/nginx/conf/ 
    #由於拷貝完成後會在conf中存在一個nginx資料夾,所以需要將它的內容移動到conf中
    mv /mydata/nginx/conf/nginx/* /mydata/nginx/conf/
    rm -rf /mydata/nginx/conf/nginx
    
  3. 終止容器&刪除原來的容器

    docker stop nginx
    docker rm nginx
    
  4. 建立新的 nginx 容器

    docker run -p 80:80 --name nginx \
    -v /mydata/nginx/html:/usr/share/nginx/html \
    -v /mydata/nginx/logs:/var/log/nginx \
    -v /mydata/nginx/conf:/etc/nginx \
    -d nginx:1.10
    

相關文章