10
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

触って学ぶElasticsearch

Last updated at Posted at 2020-12-29

全文検索システムを搭載した検索エンジンである 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が動作するサーバです。

クラスタ
スクリーンショット 2021-02-07 0.36.51.png

複数のノードによって構成される、ノードのグループです。

シャード
スクリーンショット 2021-02-07 0.37.30.png

時と場合によっては、インデックスを分割して保存することもあります。
その際の分割されたデータの単位を指します。
各インデックスに対するシャードの数はインデックス作成時に指定しておく必要があります。

特徴

分散配置機能
cluster_om_r_shard.png

作成したデータはシャード単位で複数のノードに分散して配置されます。これによって検索が並行処理で行われ検索パフォーマンスが向上します。また各シャードに対してレプリカを作成することができ、データの可用性を高めることも可能です。

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の一覧が表示された画面に遷移します。
インデックス内のデータやフィールドを確認するのに便利です。
スクリーンショット 2020-12-27 20.09.21.png

ダッシュボードのDev Toolsを選択するとAPI操作をコンソール上で行える画面に遷移できます。
補完も効いてリクエストヘッダの設定等も不要なの非常に便利です。
スクリーンショット 2020-12-27 20.12.08.png

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にthispenも含む片方のドキュメントのみ取得できました。

しかし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にispenを含み、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にispenも含み、bodyに"cute"を含まない片方のドキュメントのみ取得できました。

おまけ

サンプルなどの不要なインデックスを掃除しましょう。

DELETE /post/

簡単ですね。

参考

Elasticsearch実践ガイド
はじめての Elasticsearch
【基礎編】Elasticsearchの検索クエリを使いこなそう
初心者のためのElasticsearchその2 -いろいろな検索-
Elasticsearchの確認系コマンド6つ

10
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?