全文検索システムを搭載した検索エンジンである Elasticsearch を使用するにあたって抑えておくべきことをまとめました。
今回は簡単な仕組みを説明した後に、実際に Elastisearch を用いてデータ操作してみながら要点を解説します。
この記事で解説すること
- Elasticsearch の基本的な用語
- Elasticsearch のCRUD処理
- Elasticsearch の基本的な検索クエリ
- Kibana の使い方
この記事で解説しないこと
- Elasticsearch の内部での細かな処理(検索方法、インデックス、形態素解析)
- 最適なクラスタ構成、各種設定
Elasticsearch とは
Elasticsearchとは検索処理に特化したオープンソースのソフトウェアです。
Javaで記述されており、同じくJavaで記述された検索エンジンライブラリであるApache Luceneがベースとなっています。
データを格納できるので、DBとして使用する方もいるようです。
ちなみに前身であるCompassという検索ソフトウェアは、Elasticsearchの生みの親である Shay Banon 氏が妻の料理レシピを検索するために開発したものだそう。
基礎用語
ドキュメント
Elasticsearchにおけるデータの単位です。JSON形式で表現されます。
RDBにおけるレコードに相当します。
フィールド
ドキュメントのkeyとvalueの組み合わせのことです。
ドキュメントの例
{
"name": "Beats",
"age" : 26,
"birthday": "1994-10-29",
"blog_url": "https://beatsbeats.hatenablog.com/",
"job": ["monk","developer"],
"favorite_song": {"title":"fav_song","created_at":"2000-01-01"}
}
インデックス
ドキュメントを格納する領域です。
RDBにおけるテーブルに相当します。
02/06追記
RDBにおけるデータベース(スキーマ)に相当します。
と記述していましたが、タイプが使えないならテーブルに相当するよなと思って編集しました。
タイプ
Elasticsearch 5.X までは1つのインデックスに複数のタイプを定義できたので、格納されたドキュメントの分類を示すRDBでいうテーブルに相当する概念でした。
Elasticではバージョン5.0よりElasticsearchのタイプ廃止に向けて取り組んできました。そして7.0より、タイプは完全に廃止されます。
ノード
Elasticsearchが動作するサーバです。
複数のノードによって構成される、ノードのグループです。
時と場合によっては、インデックスを分割して保存することもあります。
その際の分割されたデータの単位を指します。
各インデックスに対するシャードの数はインデックス作成時に指定しておく必要があります。
特徴
作成したデータはシャード単位で複数のノードに分散して配置されます。これによって検索が並行処理で行われ検索パフォーマンスが向上します。また各シャードに対してレプリカを作成することができ、データの可用性を高めることも可能です。
REST API
REAT APIによるアクセスが可能であり、リソースをURLで指定して操作内容をHTTPメソッドで指定してCRUD処理を行えます。ドキュメントの取得・更新、クラスタの管理などがAPIによって操作可能です。よって外部システムとの連携も取りやすいです。
スキーマ定義不要
作成するドキュメントのスキーマ定義をする必要がありません。新たにインデックスを作成する際に、データの値から自動的にスキーマを生成してくれます。また、明示的にスキーマを定義することも可能です。
実践
まずは実際にリクエストとレスポンスを確認していきます。
公式のAPIドキュメントを参考にします。
https://www.elastic.co/guide/en/elasticsearch/reference/current/rest-apis.html
環境
動作環境はElasticsearch Service 7.7 です。
ローカルで動作環境を作りたい場合はこちらの記事が参考になります。
はじめての Elasticsearch
なお今回はKibanaというElasticsearchで扱うデータを可視化するツールを利用して、動作を確認します。
ローカルでElasticsearchと一緒にKibanaを起動してください。
Kibanaの使い方
KibanaにアクセスできたらダッシュボードのStack Managementを選択しindex patternを作成します。*を用いて複数のindexを対象とするパターンを作成することもできます。
ここで作成できるindexはすでにElasticsearch内に存在するindexのみです。
ダッシュボードのDiscoverを選択すると作成したindex paternの一覧が表示された画面に遷移します。
インデックス内のデータやフィールドを確認するのに便利です。
ダッシュボードのDev Toolsを選択するとAPI操作をコンソール上で行える画面に遷移できます。
補完も効いてリクエストヘッダの設定等も不要なの非常に便利です。
Create
- Documentの新規作成はPUTもしくはPOSTを利用します。
PUTの場合
PUT /member/_doc/1
{
"name": "Beats",
"age" : 26,
"birthday": "1994-10-29",
"blog_url": "https://beatsbeats.hatenablog.com/",
"job": ["monk","developer"],
"favorite_song": {"title":"fav_song","created_at":"2000-01-01"}
}
{
"_index" : "member",
"_type" : "_doc",
"_id" : "1",
"_version" : 1,
"result" : "created",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 0,
"_primary_term" : 1
}
以下の形式でも結果は同じです。
PUT /member/_create/1
{
"name": "Beats",
"age" : 26,
"birthday": "1994-10-29",
"blog_url": "https://beatsbeats.hatenablog.com/",
"job": ["monk","developer"],
"favorite_song": {"title":"fav_song","created_at":"2000-01-01"}
}
POSTの場合
POSTを用いる場合はIDを指定しなくても良いです。指定がない場合は自動採番されます。
POST /member/_doc/
{
"name": "Beats",
"age" : 26,
"birthday": "1994-10-29",
"blog_url": "https://beatsbeats.hatenablog.com/",
"job": ["monk","developer"],
"favorite_song": {"title":"fav_song","created_at":"2000-01-01"}
}
{
"_index" : "member",
"_type" : "_doc",
"_id" : "4_1FfHcBun3pb7RqaiAK",
"_version" : 1,
"result" : "created",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 2,
"_primary_term" : 1
}
以下の形式でも結果は同じです
POST /member/_create/1
{
"name": "Beats",
"age" : 26,
"birthday": "1994-10-29",
"blog_url": "https://beatsbeats.hatenablog.com/",
"job": ["monk","developer"],
"favorite_song": {"title":"fav_song","created_at":"2000-01-01"}
}
READ
GET /member/_doc/1
{
"_index" : "member",
"_type" : "_doc",
"_id" : "1",
"_version" : 1,
"_seq_no" : 0,
"_primary_term" : 1,
"found" : true,
"_source" : {
"name" : "Beats",
"age" : 26,
"birthday" : "1994-10-29",
"blog_url" : "https://beatsbeats.hatenablog.com/",
"job" : [
"monk",
"developer"
],
"favorite_song" : {
"title" : "fav_song",
"created_at" : "2000-01-01"
}
}
}
こっちの形式を使えばインデックスの名前などのメタデータを除いた形で取得可能です。
GET /member/_source/1
{
"name" : "Beats",
"age" : 26,
"birthday" : "1994-10-29",
"blog_url" : "https://beatsbeats.hatenablog.com/",
"job" : [
"monk",
"developer"
],
"favorite_song" : {
"title" : "fav_song",
"created_at" : "2000-01-01"
}
}
UPDATE
フィールドを指定して更新する場合は以下の形式のリクエストを実行します。
POST /member/_update/1
{
"doc": {
"name": "Beeeats",
"age" : 27,
"birthday": "1995-10-29",
"blog_url": "",
"job": ["developer"],
"favorite_song": {"title":"fav_song","created_at":"2000-01-03"}
}
}
{
"_index" : "member",
"_type" : "_doc",
"_id" : "1",
"_version" : 2,
"result" : "updated",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 1,
"_primary_term" : 1
}
Document全体を新たなDocumentに更新する場合は以下の形式のリクエストを実行します。
PUT /member/_doc/1
{
"name": "Beats",
"age" : 27,
"birthday": "1994-10-29",
"blog_url": "https://beatsbeats.hatenablog.com/",
"job": ["monk","developer"],
"favorite_song": {"title":"fav_song","created_at":"2000-01-01"}
}
POST /member/_doc/1
{
"name": "Beats",
"age" : 27,
"birthday": "1994-10-29",
"blog_url": "https://beatsbeats.hatenablog.com/",
"job": ["monk","developer"],
"favorite_song": {"title":"fav_song","created_at":"2000-01-01"}
}
DELETE
DELETE /member/_doc/1
{
"_index" : "membe",
"_type" : "_doc",
"_id" : "1",
"_version" : 3,
"result" : "deleted",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 2,
"_primary_term" : 1
}
検索(GET or POST)
検索処理にはGETメソッドもしくはPOSTメソッドを使用して、Bodyにクエリを埋め込みます。
検索用のクエリも他のデータ操作時と同じようにJSON形式で記述します。
検索用のクエリのフォーマットをクエリDSLと呼びます。
URLの末尾には_search
をつける必要があり、URLのパスパラメータにより検索対象範囲を指定します。
今回はmember
というインデックスに対して検索をかけたいので、パスはmember/_doc/_search
となります。
全てのインデックスに対して検索をかけたい場合はインデックスやタイプはパスに含めず、末に/_search
のみを付けます。
またクエリDSLには複数種類のクエリが存在するのですが、今回はmatch_all
,match
,term/terms
,from/size
,sort
,range
,bool
を紹介します。
結果が見やすいように事前に以下二件のドキュメントを作成しておきます。
POST /post/_doc
{
"title": "this is a pen",
"body": "this pen is cool",
"created_at": "2020-10-29",
}
POST /post/_doc
{
"title": "that is a pen",
"body": "that pen is cute"
"created_at": "2021-11-24",
}
match_all
全件取得するクエリです。
Request
POST /post/_doc/_search
{
"query": {
"match_all": {}
}
}
Response
{
"took" : 10,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 3,
"relation" : "eq"
},
"max_score" : 1.0,
"hits" : [
{
"_index" : "post",
"_type" : "_doc",
"_id" : "o3XwfHcBFY01GkKP0sAj",
"_score" : 1.0,
"_source" : {
"title" : "this is a pen",
"body" : "this pen is cool",
"created_at" : "2020-10-29"
}
},
{
"_index" : "post",
"_type" : "_doc",
"_id" : "pXXwfHcBFY01GkKP3MDy",
"_score" : 1.0,
"_source" : {
"title" : "that is a pen",
"body" : "that pen is cute",
"created_at" : "2021-11-24"
}
},
{
"_index" : "post",
"_type" : "_doc",
"_id" : "LnX2fHcBFY01GkKPgsG5",
"_score" : 1.0,
"_source" : {
"query" : {
"match_all" : {
"field" : ""
}
}
}
}
]
}
}
hitsフィールド内が検索結果のメタデータであり、続いてヒットしたデータが並んでいます。
hits.total.valueはヒットした件数を示し、hits.hits._scoreはクエリ条件とデータの一致度を数値化したものです。
match
特定のフィールドを対象として、キーワード検索を行うクエリです。
またキーワードを空白文字で区切ることで複数キーワードを用いた検索が可能です。(OR条件)
Request
POST /post/_doc/_search/
{
"query": {
"match": {
"title": "this pen"
}
}
}
Response
{
"took" : 65,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 2,
"relation" : "eq"
},
"max_score" : 0.8754687,
"hits" : [
{
"_index" : "post",
"_type" : "_doc",
"_id" : "o3XwfHcBFY01GkKP0sAj",
"_score" : 0.8754687,
"_source" : {
"title" : "this is a pen",
"body" : "this pen is cool",
"created_at" : "2020-10-29"
}
},
{
"_index" : "post",
"_type" : "_doc",
"_id" : "pXXwfHcBFY01GkKP3MDy",
"_score" : 0.18232156,
"_source" : {
"title" : "that is a pen",
"body" : "that pen is cute",
"created_at" : "2021-11-24"
}
}
]
}
}
this
OR pen
で検索したことと同義なのでもちろん二件ともヒットします。
なお、検索結果はデフォルトではスコア順でソートされています。
ORだけでなくANDに変更することも可能です。
Request
POST /post/_doc/_search/
{
"query": {
"match": {
"title": {
"query": "this pen",
"operator": "and"
}
}
}
}
Response
{
"took" : 15,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 1,
"relation" : "eq"
},
"max_score" : 0.8754687,
"hits" : [
{
"_index" : "post",
"_type" : "_doc",
"_id" : "o3XwfHcBFY01GkKP0sAj",
"_score" : 0.8754687,
"_source" : {
"title" : "this is a pen",
"body" : "this pen is cool",
"created_at" : "2020-10-29"
}
}
]
}
}
titleにthis
もpen
も含む片方のドキュメントのみ取得できました。
しかしmatchで検索した場合、指定した文字列を解析して検索が実行されてしまいます。
野球を検索ワードに指定した場合、野と球に分割して検索が実行されるので卓球や野鳥もヒットします。
検索ワードの解析を行わずに検索を実行したい場合は、term/terms
を使用します。
range
範囲を指定して検索を実行するクエリです。
値の範囲指定には以下のオペレータを用います。
- get(~以上)
- lte(~以下)
- gt(より大きい)
- lt(より小さい)
Request
POST /post/_doc/_search
{
"query": {
"range": {
"created_at": {
"gte": "2020-10-01",
"lte": "2020-11-01"
}
}
}
}
Response
{
"took" : 9,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 1,
"relation" : "eq"
},
"max_score" : 1.0,
"hits" : [
{
"_index" : "post",
"_type" : "_doc",
"_id" : "o3XwfHcBFY01GkKP0sAj",
"_score" : 1.0,
"_source" : {
"title" : "this is a pen",
"body" : "this pen is cool",
"created_at" : "2020-10-29"
}
}
]
}
}
bool
基本クエリを組み合わせたクエリです。
複数条件を組み合わせた細かな検索をしたいときに用いる、少し複雑なクエリです。
boolクエリの中でさらに4つのクエリが用いられます。
{
"query": {
"bool": {
"must": {},
"should": {},
"must_not": {},
"fileter": {}
}
}
}
must
基本クエリのAND条件となるクエリです。
should
基本クエリのOR条件となるクエリです。
must_not
基本クエリのNOT条件となるクエリです。
filter
基本クエリのAND条件となるクエリですがスコアの加算がされません。
つまり、スコアの取得が必要なければmustの代わりにこちらを使用しても良いということです。
完全一致検索時もスコアは必要ないのでfilterの使用がベストです。
これら4つを組み合わせることでより細かな検索もできます。
titleにis
とpen
を含み、bodyに"cute"を含まないドキュメントを絞り込んでみます。
Request
POST /post/_doc/_search/
{
"query": {
"bool": {
"must": [
{"match": {"title": "is"}},
{"match": {"title": "pen"}}
],
"must_not": [
{"match": {"body": "cute"}}
]
}
}
}
Response
{
"took" : 15,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 1,
"relation" : "eq"
},
"max_score" : 0.36464313,
"hits" : [
{
"_index" : "post",
"_type" : "_doc",
"_id" : "o3XwfHcBFY01GkKP0sAj",
"_score" : 0.36464313,
"_source" : {
"title" : "this is a pen",
"body" : "this pen is cool",
"created_at" : "2020-10-29"
}
}
]
}
}
titleにis
もpen
も含み、bodyに"cute"を含まない片方のドキュメントのみ取得できました。
おまけ
サンプルなどの不要なインデックスを掃除しましょう。
DELETE /post/
簡単ですね。
参考
Elasticsearch実践ガイド
はじめての Elasticsearch
【基礎編】Elasticsearchの検索クエリを使いこなそう
初心者のためのElasticsearchその2 -いろいろな検索-
Elasticsearchの確認系コマンド6つ