📝

Elasticsearchの使い方

2021/12/04に公開

この記事の要約

  • Elasticsearchの基本概念と操作を公式チュートリアルを軸にまとめてみました。

Elasticsearchとは

  • オープンソースソフトウェアの全文検索エンジン。
  • JSONベースでデータを取り扱う。
  • 基本的にREST APIを介してデータの登録や検索を行う。
  • ショッピングサイトの商品検索のような大規模なデータ検索に適している。
  • 登録や解析のための周辺ツールとセットで使用するケースが多い。(Kibana、Logstash..)

基本用語

  • クラスタ

ノード(サーバー)の集合体。
全てのノードに対して、インデキシング機能と検索機能を提供する。
一意の名前を持つ。

  • ノード

クラスタ内の一台単位のサーバー。
ノードは名前で識別され、起動時にランダムなユニバーサル固有識別子(UUID)が与えられる。
任意の名前を設定出来る。ネットワーク内のサーバーと関連する名前管理に優位。

  • インデックス

データを、送って格納する事をインデックスするという。
ドキュメントのコレクション。
RDBにおけるデータベース。
RDBではデータベースを複数に分けるという状況はあまりないが、Elasticsearchではユーザーごとにインデックスを別々にしたり、時系列のデータをある時間間隔で区切ったりとインデックスを分けることが多い。

  • タイプ(マッピングタイプ)

RDBにおけるテーブル。

  • ドキュメント

RDBのDB内のテーブル内の1レコードに相当。
JSONで表現される。

  • シャードとレプリカ

Elasticsearchは、大量のデータをシャードと呼ばれるユニット単位に分割し、それらのシャードを複数のインスタンスに分散して保持する。シャードには、2種類存在する。

1.Shard (書き込み可能な Shard)
2.Replica Shard (読み取り専用)

デフォルトでは、5つの書き込みシャードと1つのレプリカシャードが設定される。

REST APIでの基本操作

  • クラスタヘルス
curl -X GET "localhost:9200/_cat/health?v&pretty"
  • クラスタ内のノードリスト取得
curl -X GET "localhost:9200/_cat/nodes?v&pretty"
  • 全インデックスのリスト
curl -X GET "localhost:9200/_cat/indices?v&pretty"
  • インデックスの作成
curl -X PUT "localhost:9200/{index_name}?pretty"
  • ドキュメント(レコード)のインデックス(保存)

インデックスにインデキシングする=テーブルにレコードを保存する。

curl -X POST "localhost:9200/{index_name}/{type}/1?pretty" -H 'Content-Type: application/json' -d '{"name":"John Doe"}'

-> {index_name} = データベース名
-> {type} = テーブル名
  • インデキシングしたドキュメントの取得
curl -X GET "localhost:9200/{index_name}/{type}/1?pretty"
  • インデックスの削除
curl -X DELETE "localhost:9200/{index_name}?pretty"
  • ドキュメントの更新
curl -X POST "localhost:9200/{index_name}/{type}/1/_update?pretty" -H 'Content-Type: application/json' -d '{ "doc": {"name":"John Does"}}'

-> 更新はPOST
-> { "doc" {}} 

**さらに更新**
curl -X POST "localhost:9200/{index_name}/{type}/1/_update?pretty" -H 'Content-Type: application/json' -d '{ "doc": {"name":"John Does", "age": 20}}'
  • スクリプトを使用したドキュメントの更新
curl -X POST "localhost:9200/{index_name}/{type}/1/_update?pretty" -H 'Content-Type: application/json' -d '{"script": "ctx._source.age +=5"}'

-> `ctx._source`は更新される現在のソースドキュメントを指しています。
  • バッチ処理

個別ドキュメントのインデキシング、更新、および削除が可能であるだけでなく、Elasticsearchは[bulk API](https://www.elastic.co/guide/en/elasticsearch/reference/5.4/docs-bulk.html)を使用してこれらのオペレーションのいずれかを一括で実行することも出来る。

POST /{index_name}/{type}/_bulk?pretty
{"index":{"_id":"1"}}
{"name": "John Doe" }
{"index":{"_id":"2"}}
{"name": "Jane Doe" }

バルク処理でデータセット

  • データの準備

顧客の銀行口座情報に関するJSONドキュメントを用意します。このJSONオブジェクトは、RDBでDB内のテーブルに保存する1レコードに相当する。

{
    "account_number": 0,
    "balance": 16623,
    "firstname": "Bradshaw",
    "lastname": "Mckenzie",
    "age": 29,
    "gender": "F",
    "address": "244 Columbus Place",
    "employer": "Euron",
    "email": "bradshawmckenzie@euron.com",
    "city": "Hobucken",
    "state": "CO"
}
curl -X POST "localhost:9200/bank/account/_bulk?pretty&refresh" -H "Content-Type: application/json" --data-binary "@accounts.json"

Search APIを使用したデータの検索

  • 検索方法の種類

Elasticsearchで検索を実行するには、

1.RESTリクエストURI形式

curl -X GET "localhost:9200/customer/_search?q=*&sort=name:asc&pretty"

2.RESTリクエストボディ形式

curl -X GET "localhost:9200/customer/_search?pretty" << EOF
{
  "query": { "match_all": {} },
  "sort": [
    { "name": "asc" }
  ]
}
EOF

がある。リクエストボディでは、JSON形式でフィルターを書けるので豊かな表現が可能。

  • Query DSLの一例
{
  // クエリの定義
  "query": { "match_all": {} },
  // ソートする
 "sort": {[ { "name": "asc" }]}
  // サイズの指定(デフォルトは10)
  "size": 1,
  // 開始するドキュメントのインデックス(上位のインデックスではない)デフォルトは10
 "from": 10
   
}
  • 検索の実行結果
{
  "took" : 5,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 3,
      "relation" : "eq"
    },
    "max_score" : 1.0,
    //検索ヒット
    "hits" : [
      {
        "_index" : "customer",
        "_type" : "external",
        "_id" : "1",
        "_score" : 1.0,
     // 目的のデータソース
        "_source" : {
          "name" : "John Dor"
        }
      },
      {
        "_index" : "customer",
        "_type" : "external",
        "_id" : "2",
        "_score" : 1.0,
     // 目的のデータソース
        "_source" : {
          "name" : "John Dor"
        }
      },
      {
        "_index" : "customer",
        "_type" : "external",
        "_id" : "3",
        "_score" : 1.0,
     // 目的のデータソース
        "_source" : {
          "name" : "John Doragon"
        }
      }
    ]
  }
}
  • _source(sqlのselectに相当)
{
  "query": { "match_all": {} },
  "_source": ["account_number", "balance"]
}
  • 条件に一致したレコードを検索
{
  "query": { "match": { "account_number": 20 } }
}

{
  // mill laneの間は、分ける。
  "query": { "match": { "address": "mill lane" } }
}

{
  // アドレスに「mill lane」という句を含む口座をすべて返す`match`の変形(match_phrase)
  "query": { "match_phrase": { "address": "mill lane" } }
}

{ 
  // 2つの`match`クエリを構成して、アドレスに「mill」と「lane」を含む口座をすべて返します。
  "query": {
    "bool": {
      "must": [
        { "match": { "address": "mill" } },
        { "match": { "address": "lane" } }
      ]
    }
  }
}

{
  // 2つの`match`クエリを構成して、アドレスに「mill」または「lane」を含む口座をすべて返します。
  "query": {
    "bool": {
      "should": [
        { "match": { "address": "mill" } },
        { "match": { "address": "lane" } }
      ]
    }
  }
}

{
  //2つの`match`クエリを構成して、アドレスに「mill」と「lane」のどちらも含まない口座をすべて返します。
  "query": {
    "bool": {
      "must_not": [
        { "match": { "address": "mill" } },
        { "match": { "address": "lane" } }
      ]
    }
  }
}

{
  // 40歳ではあるがID(アイダホ)には住んでいない人の口座をすべて返します。
  "query": {
    "bool": {
      "must": [
        { "match": { "age": "40" } }
      ],
      "must_not": [
        { "match": { "state": "ID" } }
      ]
    }
  }
}
  • フィルタの実行

スコアは、そのドキュメントが指定した検索クエリにどの程度一致しているかの相対的な基準となる数値。スコアが高いほどドキュメントの関連性が高く、スコアが低いほどドキュメントの関連性が低くなる。クエリが常にスコアを生成するとは限らない。特にクエリをドキュメントセットの「フィルタリング」にのみ使用した場合は、スコアを生成しない。Elasticsearchはこれらの状況を検知し、無駄なスコアを計算しないようにクエリの実行を自動で最適化する。

boolクエリを使用して、残高が20000以上30000以下の口座をすべて返す。言い換えると、残高が20000以上で30000以下の口座を検索する。

{
  "query": {
    "bool": {
      "must": { "match_all": {} },
      "filter": {
        "range": {
          "balance": {
            "gte": 20000,
            "lte": 30000
          }
        }
      }
    }
  }
}
  • アグリゲーション(集約の実行)

次の例はすべての口座を州ごとにグループ化して、数を降順にソート(デフォルト)した上位10個(デフォルト)の州を返す。

{
  "size": 0,
  "aggs": {
    "group_by_state": {
      "terms": {
        "field": "state.keyword"
      }
    }
  }
}

SELECT state, COUNT(*) FROM bank GROUP BY state ORDER BY COUNT(*) DESC
  • まとめ

公式チュートリアルと、参考リンクを参考にElasticsearchについて取り上げました。
RDBとの関連性を、理解しながらだとスッと分かりやすくなりました。大規模なデータを取り扱うケースだと、シャード管理とか気にする事が多くなりそうですね。

参考

https://www.elastic.co/guide/jp/elasticsearch/reference/current/gs-search-api.html
https://tech.techtouch.jp/entry/2020/12/05/100000#f-4eca810f
https://www.designet.co.jp/ossinfo/elasticsearch/

Discussion