エンジニアリング

Elasticsearch実践入門

編集者注(2021年8月3日):この投稿は廃止予定の機能を使用しています。現在の手順については、逆ジオコーディングを使用したカスタム地域のマッピングドキュメントを参照してください。

この投稿の目的

最近、私は、ア・コルーニャ大学で大学院の情報検索およびセマンティックWebコースを教えるという機会に恵まれました。この講義の焦点は、Elasticsearchの一般的なビジョンを学生に伝え、学生がコースの課題でElasticsearchを使い始めることができるようにすることでした。出席者は、すでにLuceneの知識が豊富な学生から、初めて情報検索という概念に出会った生徒までさまざまでした。かなり遅い時間の講義(午後7時30分開始)でもあり、課題の1つは、学生の注意力を維持するということ(つまり、学生が居眠りをしないようにすること)でした。教えるときに相手の注意が逸れないようにするには、2つの基本的なアプローチがあります。1つ目は、私は忘れてしまってできなかったのですが、チョコレートを持っていくことです。そして、2つ目は、講義をできるかぎり実践的な内容にすることです。

今日の投稿では、この点に着目し、同じ講義の実践的な部分を取り扱っていきます。Elasticsearchのすべてのコマンドやリクエストを学習することが目的ではありません(そのためにマニュアルがあるのですから)。目的は、予備知識の有無に関係なく、30~60分間のガイド付きチュートリアルで、Elasticsearchを利用する楽しさを体験をしてもらうことです。すべてのリクエストをコピーして貼り付けるだけで、結果が表示されます。そこで、提案された質問に対する解決策を理解できます。

この投稿の対象読者

主要な概念をいくつか導入するために、Elasticの基本的な機能について説明します。場合によっては、より技術的な概念や複雑な概念についても説明します。また、製品マニュアルも参考にします(ただし、これはあくまでも参考です。ここではサンプルの演習を続け、後からマニュアルを確認してもかまいません)。以前にElasticsearchを使用したことがなく、動作しているところを確認し、動作を指示したいという場合には、この投稿が最適です。すでにElasticsearchの経験がある場合は、ここで使用するデータセットを確認してください。誰かにElasticsearchで何ができるのかを聞かれたときに、Shakespeareの演劇の検索を使えば簡単に説明することができます。

この投稿で説明する範囲

まず、ドキュメントをいくつか追加し、そのドキュメントを検索して削除します。その後、Shakespeareデータセットを使用して、検索と集約の本質に関する詳細な見識を提供します。これは「今すぐに動作しているところを見たい」という方のための実践的な投稿です。

構成や、本番デプロイにおけるベストプラクティスに関連することは一切説明しません。このため、Elasticsearchがどのような機能を備えているのかを少し理解し、どのようにお客様のニーズに適合するのかをイメージするための出発点として、この情報をご活用ください。

セットアップ

まず、Elasticsearchが必要です。手順ドキュメントに従って、最新バージョンをダウンロードし、インストールして起動します。基本的に、最新バージョンのJavaが必要です。ご使用のオペレーティングシステムに適したElasticsearchをダウンロードし、インストールします。最後に、デフォルト値のbin/elasticsearchを使用して、Elasticsearchを起動します。この講義では、現時点で最新のバージョンである5.5.0を使用します。

次に、Elasticsearchと通信する必要があります。通信は、REST APIに対してHTTPリクエストを発行して行われます。デフォルトでは、Elasticは9200番ポートで起動します。アクセスするには、ご自身のスキルに最適なツールを使用できます。コマンドラインツール(Linux用のcurlなど)や、ChromeやFirefox用のWebブラウザーRESTプラグインがあります。また、Kibanaをインストールして、コンソールプラグインを使用することもできます。各リクエストは、HTTPメソッド(GET、POST、PUTなど)、URLエンドポイント、オプションの本文から構成されています。ほとんどの場合、本文はJSONオブジェクトです。

たとえば、Elasticsearchが起動していることを確認するには、基本URLに対して次のGETを実行し、基本エンドポイントにアクセスします(本文は不要です)。

GET localhost:9200

そうすると、次のような応答が返されます。ここでは何も構成していないため、インスタンスの名前がランダムな7文字の文字列になります。

{
    "name": "t9mGYu5",
    "cluster_name": "elasticsearch",
    "cluster_uuid": "xq-6d4QpSDa-kiNE4Ph-Cg",
    "version": {
        "number":"5.5.0",
        "build_hash":"260387d",
        "build_date":"2017-06-30T23:16:05.735Z",
        "build_snapshot": false,
        "lucene_version":"6.6.0"
    },
    "tagline":"You Know, for Search"
}

基本的な例

すでにクリーンなElasticsearchインスタンスを初期化して実行しています。まず、ドキュメントを追加し、そのドキュメントを取得します。ElasticsearchのドキュメントはJSON形式で表されます。また、ドキュメントはインデックスに追加され、タイプが割り当てられます。ここでは、accountsというインデックスに、id 1の人物タイプのドキュメントを追加しています。まだインデックスが存在していないため、Elasticsearchによって自動的にインデックスが作成されます。

POST localhost:9200/accounts/person/1 
{
    "name" :"John",
    "lastname" :"Doe",
    "job_description" :"Systems administrator and Linux specialit"
}

ドキュメント作成に関する情報が応答として返されます。

{
    "_index": "accounts",
    "_type": "person",
    "_id":"1",
    "_version":1,
    "result": "created",
    "_shards": {
        "total":2,
        "successful":1,
        "failed":0
    },
    "created": true
}

ドキュメントが存在するため、取得できます。

GET localhost:9200/accounts/person/1 

結果には、メタデータと完全なドキュメント(「_source」フィールドに表示)が含まれます。

{
    "_index": "accounts",
    "_type": "person",
    "_id":"1",
    "_version":1,
    "found": true,
    "_source": {
        "name":"John",
        "lastname":"Doe",
        "job_description":"Systems administrator and Linux specialit"
    }
}

よく見ている方は、職務記述書に入力ミス(specialit)があることにもう気付いていることでしょう。では、ドキュメントを更新(「_update」)して、その誤りを修正しましょう。

POST localhost:9200/accounts/person/1/_update
{
      "doc":{
          "job_description" :"Systems administrator and Linux specialist"
       }
}

処理が成功すると、ドキュメントが変更されます。もう一度ドキュメントを取得し、応答を確認してみましょう。

{
    "_index": "accounts",
    "_type": "person",
    "_id":"1",
    "_version":2,
    "found": true,
    "_source": {
        "name":"John",
        "lastname":"Doe",
        "job_description":"Systems administrator and Linux specialist"
    }
}

次の処理の準備をするために、id 2の別のドキュメントを追加します。

POST localhost:9200/accounts/person/2
{
    "name" :"John",
    "lastname" :"Smith",
    "job_description" :"Systems administrator"
}

ここまでは、idでドキュメントを取得してきましたが、検索は実行していませんでした。REST APIを使用してクエリを実行するときには、特定の構文を使用すると、リクエストの本文または直接URLでクエリを渡すことができます。このセクションでは、「/_search?q=something」という形式で直接URLで検索を実行します。

GET localhost:9200/_search?q=john

両方のドキュメントに「john」が含まれているため、両方のドキュメントが返されます。

{
    "took":58,
    "timed_out": false,
    "_shards": {
        "total":5,
        "successful":5,
        "failed":0
    },
    "hits": {
        "total":2,
        "max_score":0.2876821,
        "hits": [
            {
                "_index": "accounts",
                "_type": "person",
                "_id":"2",
                "_score":0.2876821,
                "_source": {
                    "name":"John",
                    "lastname":"Smith",
                    "job_description":"Systems administrator"
                }
            },
            {
                "_index": "accounts",
                "_type": "person",
                "_id":"1",
                "_score":0.28582606,
                "_source": {
                    "name":"John",
                    "lastname":"Doe",
                    "job_description":"Systems administrator and Linux specialist"
                }
            }
        ]
    }
}

この結果では、一致するドキュメントと、クエリの結果の合計件数といった一部のメタデータを確認できます。さらに検索を実行してみましょう。検索を実行する前に、どのようなドキュメントが取得されるのかを自分で考えてみてください(応答はコマンドの後に示されます)。

GET localhost:9200/_search?q=smith

この検索では、追加した最後のドキュメントのうち、「smith」を含むドキュメントが返されます。

GET localhost:9200/_search?q=job_description:john

この検索では、どのドキュメントも返されません。この場合、用語が含まれていない「job_description」フィールドに検索を制限しています。読者のための演習として、次のことを行ってみてください。

  • そのフィールドでid 1のドキュメントのみを返す検索
  • そのフィールドで両方のドキュメントを返す検索
  • そのフィールドでid 2のドキュメントのみを返す検索。ヒントがあります。「q」パラメーターは、クエリ文字列と同じ構文を使用します。

この最後の例では、関連する疑問が生じます。特定のフィールドで検索を実行できますが、特定のインデックス内でのみ検索することはできるのでしょうか。答えは、検索できます。URLでインデックスとタイプを指定することができます。次の検索を試してください。

GET localhost:9200/accounts/person/_search?q=job_description:linux

1つのインデックスで検索するだけではありません。カンマ区切りのリストとしてインデックスを指定すると、同時に複数のインデックスで検索を実行できます。タイプについても、同じです。そのほかにもオプションがあります。詳細については、複数のインデックス、複数のタイプを参照してください。読者のための演習として、ドキュメントを2つ目の(別の)インデックスに追加し、同時に両方のインデックスで検索を実行します。

このセクションを閉じるためには、ドキュメントを削除し、その後にインデックス全体を削除します。ドキュメントを削除した後は、検索で取得するか検索してください。

DELETE localhost:9200/accounts/person/1

応答は確認になります。

{
    "found": true,
    "_index": "accounts",
    "_type": "person",
    "_id":"1",
    "_version":3,
    "result": "deleted",
    "_shards": {
        "total":2,
        "successful":1,
        "failed":0
    }
}

最後に、インデックスをすべて削除できます。

DELETE localhost:9200/accounts

これで最初のセクションを終わります。これまでの内容をまとめてみましょう。

1.ドキュメントを追加しました。暗黙的に、インデックスが作成されました(インデックスは 前から存在していませんでした)。 2.ドキュメントを取得しました。 3.ドキュメントを更新し、入力ミスを修正して、修正されたことを確認しました。 4.2つ目のドキュメントを追加しました。 5.暗黙的にすべてのフィールドを使用した検索を含め、検索を実行しました。 検索は1つのフィールドのみを対象としました。 6.複数の検索の演習を提案しました。 7.同時に複数のインデックスとタイプで検索する際の基本事項を説明しました 。 8.同時に複数のインデックスで検索することを提案しました。 9.1つのドキュメントを削除しました。 10.インデックス全体を削除しました。

このセクションのトピックの詳細については、次のリンクを参照してください。

その他の興味深いデータの操作

これまでは、架空のデータを操作してきました。このセクションでは、シェイクスピアの演劇を探索します。まず、Kibana:サンプルデータの読み込みで提供されているshakespeare.jsonファイルをダウンロードします。ElasticsearchにはBulk APIがあり、処理を一括で追加、削除、更新、作成できます。つまり、一度に多数の処理を実行できます。このファイルに含まれているデータは、このAPIを使用してすぐにインジェストでき、幕、場、台詞のタイプのドキュメントを含むShakespeareというインデックスでインデックス化できます。Bulk APIに対するリクエストの本文は、各行に1つのJSONオブジェクトが記述されています。ファイルの処理のような追加処理の場合は、追加処理に関するメタデータを示す1つのJSONオブジェクトがあります。そして、追加するドキュメントを含む2つ目のJSONオブジェクトが次の行に記述されます。

{"index":{"_index":"shakespeare","_type":"act","_id":0}}
{"line_id":1,"play_name":"Henry IV","speech_number":"","line_number":"","speaker":"","text_entry":"ACT I"}

Bulk APIについては、これ以上深く説明しません。関心がある場合は、Bulkドキュメントを参照してください。

では、このデータをすべてElasticsearchに取り込みます。このリクエストの本文はかなり大きい(20万行以上)ため、curlなどのファイルからリクエストの本文を読み込めるツールを使用することをお勧めします。

curl -XPOST "localhost:9200/shakespeare/_bulk?pretty" --data-binary @shakespeare.json

データが読み込まれた後は、一部の検索を実行し始めることができます。前のセクションでは、URLでクエリを渡す検索を実行しました。このセクションでは、クエリDSLの基本について説明します。クエリDSLは、クエリを定義するために検索リクエストの本文で使用されるJSON形式を指定します。処理のタイプによっては、GETメソッドとPOSTメソッドの両方を使用して、クエリを発行できます。 では、最もシンプルな処理から始めます。すべてのドキュメントを取得します。このために、「query」キーの本文で、その値に対して、「match_all」クエリを指定します。

GET localhost:9200/shakespeare/_search
{
    "query": {
            "match_all": {}
    }
}

結果には、10個のドキュメントが表示されます。出力の一部は次のとおりです。

{
    "took":7,
    "timed_out": false,
    "_shards": {
        "total":5,
        "successful":5,
        "failed":0
    },
    "hits": {
        "total":111393,
        "max_score":1,
        "hits": [
              ...         
            {
                "_index": "shakespeare",
                "_type": "line",
                "_id":"19",
                "_score":1,
                "_source": {
                    "line_id":20,
                    "play_name":"Henry IV",
                    "speech_number":1,
                    "line_number":"1.1.17",
                    "speaker":"KING HENRY IV",
                    "text_entry":"The edge of war, like an ill-sheathed knife,"
                }
            },
            ...         

検索の形式は非常にわかりやすくなっています。多数の異なる種類の検索を使用できます。Elasticには、直接検索(「この用語を検索」、「この範囲の要素を検索」など)と、複合クエリ(「a AND b」、「a OR b」など)があります。詳細なリファレンスについては、クエリDSLドキュメントを参照してください。ここでは、一部の例のみを実行し、クエリDSLの使用方法に慣れるようにします。

POST localhost:9200/shakespeare/scene/_search/
{
    "query":{
        "match" : {
            "play_name" :"Antony"
        }
    }
}

前のクエリでは、役名にAntonyが含まれるすべての場を検索しています(URLを参照)。この検索を絞り込み、話し手がDemetriusである場も選択します。

POST localhost:9200/shakespeare/scene/_search/
{
    "query":{
        "bool": {
            "must" : [
                {
                    "match" : {
                        "play_name" :"Antony"
                    }
                },
                {
                    "match" : {
                        "speaker" :"Demetrius"
                    }
                }
            ]
        }
    }
}

読者のための1つ目の演習として、前回のクエリを修正し、話し手がDemetriusである場だけではなく、話し手がAntonyである場も検索で返されるようにします。ヒントとして、ブール演算の「should」節を確認してください。 2つ目の演習として、検索するときに、リクエスト本文で使用できる別のオプションを探索します。たとえば、結果内のどの位置から開始し、ページネーションを実行するために取得する結果の件数を選択します。

これまで、クエリDSLを使用して、クエリをいくつかを実行しました。では、探しているコンテンツを検索するだけでなく、分析もできるとしたらどうでしょうか。ここで、集約が登場します。集約により、データをもっと深く分析し、本質を捉えることができます。たとえば、次のようなインサイトが得られます。現在のデータセットに何種類の劇があるのか。1つの作品につき、平均でいくつの場があるのか。最も場が多い作品は何か。

少し理論的な知識がないと時間が無駄になってしまうため、実践的な例に飛び込む前に、もう一度Shakespeareインデックスを作成したときのことを振り返ります。Elasticでは、数値フィールド、キーワードフィールド、テキストフィールドなどの異なるフィールドに対してデータ型を定義するインデックスを作成できます。データ型には、多数の種類があります。インデックスに指定できるデータ型は、マッピングで定義されます。この場合、ドキュメントにインデックスを作成する前には、どのインデックスも作成しませんでした。そのため、Elasticによって各フィールドのタイプが決定されました(インデックスのマッピングが作成されました)。テキストフィールドには、「テキスト」型が選択されました。この型が分析されることで、Antonyを検索するだけで、役名のAntony and Cleopatraを検索できました。デフォルトでは、分析されたフィールドで集約を実行できません。 フィールドで集約を実行できない場合は、どのように集約を表示するのでしょうか。Elasticで、各フィールドの型が決定されると、集約、並べ替え、スクリプトを実行できるように、分析されていないバージョンのテキストフィールド(「キーワード」)が追加されます。集約では、「play_name.keyword」のみを使用できます。演習として、現在のマッピングを調べます。

そして、この比較的短く、比較的理論的な講義の後に、キーワードと集約に戻りましょう。データを調べるには、まず、異なる役がいくつあるのかを確認します。

GET localhost:9200/shakespeare/_search
{
    "size":0,
    "aggs" : {
        "Total plays" : {
            "cardinality" : {
                "field" : "play_name.keyword"
            }
        }
    }
}

ドキュメントには関心がないため、結果の表示は0件にします。また、インデックス全体を探索したいため、クエリセクションはありません。集約は、クエリを満たすすべてのドキュメントを使用して演算されます。この場合は、デフォルトの「match_all」を使用します。最後に、「カーディナリティ」集約を使用することを決定しました。この方法では、「play_name」フィールドの一意の値の数を把握できます。

{
    "took":67,
    "timed_out": false,
    "_shards": {
        "total":5,
        "successful":5,
        "failed":0
    },
    "hits": {
        "total":111393,
        "max_score":0,
        "hits": []
    },
    "aggregations": {
        "Total plays": {
            "value":36
        }
    }
}

ここでは、データセットに2回以上出現する役のリストを出力します。

GET localhost:9200/shakespeare/_search
{
    "size":0,
    "aggs" : {
        "Popular plays" : {
            "terms" : {
                "field" : "play_name.keyword"
            }
        }
    }
}

結果は次のようになります。

{
    "took":35,
    "timed_out": false,
    "_shards": {
        "total":5,
        "successful":5,
        "failed":0
    },
    "hits": {
        "total":111393,
        "max_score":0,
        "hits": []
    },
    "aggregations": {
        "Popular plays": {
            "doc_count_error_upper_bound":2763,
            "sum_other_doc_count":73249,
            "buckets": [
                {
                    "key":"Hamlet",
                    "doc_count":4244
                },
                {
                    "key":"Coriolanus",
                    "doc_count":3992
                },
                {
                    "key":"Cymbeline",
                    "doc_count":3958
                },
                {
                    "key":"Richard III",
                    "doc_count":3941
                },
                {
                    "key":"Antony and Cleopatra",
                    "doc_count":3862
                },
                {
                    "key":"King Lear",
                    "doc_count":3766
                },
                {
                    "key":"Othello",
                    "doc_count":3762
                },
                {
                    "key":"Troilus and Cressida",
                    "doc_count":3711
                },
                {
                    "key":"A Winters Tale",
                    "doc_count":3489
                },
                {
                    "key":"Henry VIII",
                    "doc_count":3419
                }
            ]
        }
    }
}

「play_name」に最も多く出現する10個の値を確認できます。集約で出力する値を増やしたり減らしたりする方法の詳細については、必要に応じて、ドキュメントを参照してください。

ここまで来ると、次のステップが確実にわかります。次は、集約の結合です。インデックスにある場、幕、台詞の数を知ることに関心がありますが、役ごとの同じ値にも関心があります。これを実行するには、集約の中に集約をネストします。

GET localhost:9200/shakespeare/_search
{
    "size":0,
    "aggs" : {
        "Total plays" : {
            "terms" : {
                "field" : "play_name.keyword"
            },
            "aggs" : {
                "Per type" : {
                    "terms" : {
                        "field" : "_type"
                     }
                }
            }
        }
    }
}

応答の一部は次のようになります。

    "aggregations": {
        "Total plays": {
            "doc_count_error_upper_bound":2763,
            "sum_other_doc_count":73249,
            "buckets": [
                {
                    "key":"Hamlet",
                    "doc_count":4244,
                    "Per type": {
                        "doc_count_error_upper_bound":0,
                        "sum_other_doc_count":0,
                        "buckets": [
                            {
                                "key": "line",
                                "doc_count":4219
                            },
                            {
                                "key": "scene",
                                "doc_count":20
                            },
                            {
                                "key": "act",
                                "doc_count":5
                            }
                        ]
                    }
                },
                ...

Elasticsearchには、多数の異なる集約があります。たとえば、集約の結果を使用した集約、「カーディナリティ」などのメトリック集約、「用語」などのバケット集約などです。必要に応じて、一覧を確認し、すでに検討している特定のユースケースに合った集約を決定してください。「重要な用語」集約は例外的な共通点を検出できるかもしれません。

これで第2セクションを終わります。これまでの内容をまとめてみましょう。

1.Bulk APIを使用して、シェイクスピアの演劇を追加しました。 2.簡易検索を実行し、汎用的な形式を調べ、 Query DSLを使用してクエリを実行しました。 3.フィールドのテキストを検索する、詳細検索を実行しました。 4.2つのテキスト検索を組み合わせた、複合検索を実行しました。 5.2つ目の複合検索を追加することを提案しました。 6.リクエスト本文で別のオプションをテストすることを提案しました。 7.集約の概念を説明し、マッピングとフィールド型について簡潔に振り返りました 。 8.データセットにある役の数を計算しました。 9.データセットで2回以上出現する役を取得しました 。 10.複数の集約を結合し、最も頻繁に出現する10個の役それぞれの幕、場、台詞の数を確認しました 。 11.Elasticでその他のいくつかの集約を探索することを提案しました。

その他の追加のアドバイス

これらの演習では、Elasticsearchの型の概念について少し触れました。最後に、型は追加の内部フィールドにすぎません。バージョン6以降では、1つの型のインデックスしか作成できません。バージョン7以降では、型が削除される予定です。詳細については、このブログ投稿をご覧ください。

##まとめ

この投稿では、いくつか例を使ってElasticsearchを少し体験してもらいました。

ElasticsearchとElastic Stackには、この短い記事で紹介したよりも、もっとたくさんの機能を探索できます。最後にもう一つ追加しておきたいことは、関連性です。Elasticsearchは、「このドキュメントは検索要件を満たしているのか?」だけではなく、「このドキュメントはどの程度検索要件を満たしているのか?」まで考慮し、クエリに対して最も関連性が高い検索結果を先に返します。製品マニュアルは幅広い内容を取り扱い、多数の例が記載されています。

新しいカスタム機能を導入する前に、製品マニュアルを参照して、そのような機能がプロジェクトで簡単に活用できるようにすでに実装されているかどうかを確認することをお勧めします。Elasticの開発ロードマップは、ユーザーや開発者からの要望やフィードバックを取り入れているため、一般的に有益だと考えられる機能やアイデアが、すでに提供されている可能性は十分にあります。

認証、アクセス制御、暗号化、監査といった機能が必要な場合は、すでにSecurityで提供されています。クラスターを監視する必要がある場合は、Monitoringで提供されています。結果内でオンデマンドでフィールドを作成する必要がある場合は、スクリプトフィールドを使用することですでに可能になっています。電子メール、Slack、Hipchatなどでアラートを作成する必要がある場合は、Alertingで提供されています。データと集約をグラフで可視化する必要がある場合は、Kibanaというそのための充実した環境がすでに提供されています。データベース、ログファイル、管理キュー、または想像可能なほとんどすべての情報源のデータにインデックスを作成する必要がある場合は、LogstashおよびBeatsでこのような機能が提供されています。何か機能が必要になった場合は、すでに実装されているかどうかを確認してください。