LoginSignup
11
11

More than 3 years have passed since last update.

Amazon Elasticsearch Serviceについてまとめてみる

Last updated at Posted at 2020-05-19

はじめに

Amazon Elasticsearch Service(Amazon ES)を使ったなかで学んだことをまとめてみました。
Elasticsearch自体に関しては簡単にしか説明しないので、ご注意ください。

全文検索とElasticsearchについて

Amazon ESを使ってみようと思われる方はおそらく全文検索エンジンに関心があり、その中でも特にElasticsearchに興味をお持ちのはずです。

簡単にではありますが、前提知識として全文検索Elasticsearchについて説明してみます。

全文検索とは

端的に言うと、特定の保存場所(データベースなど)に保存されている複数のファイルから特定の文字列を検索するということです。

grep型とインデックス型があり、検索速度はインデックス型の方が高速らしい。
RDBでインデックスを設定した方が処理速度が高速になることを踏まえれば分かりやすいと思います。
Elasticsearchはインデックス型になります。

参照:全文検索:Wikipedia

全文検索を実現するエンジンは複数あり、代表的なものがApache Luceneです。

ElasticsearchやApache Solrの検索エンジン部はLuceneでできています。
Luceneに様々な機能を付加したシステムがElasticsearchやSolrになります。

Elasticsearchについて

ElasticsearchはApache Luceneを基盤とした検索エンジンです。
どのような特徴があるのでしょうか。

  • スケーラビリティ
    おそらくElasticsearch最大の長所です。相当大規模なシステムでの利用にも耐えるスケーラビリティを有します。
    幾つものリクエストに対して、膨大な文書を検索し高速で結果を出力できます。
    また、複数のサーバをクラスタリングする際もSolr等に比べて容易だと言われています。

  • スキーマレス
    RDBと異なり、格納するデータ構造をより柔軟に決めることができます。形式はJSONです。

  • 豊富なクエリ
    Elasticsearchは全文検索用クエリmatchを始め、様々なクエリ(Query DSL)を用意しています。
    クエリそれぞれで使用できるデータ型などが決まっているなどの違いがあります。
    全文検索をする場合、クエリはFull text queriesのいずれかを使用する必要があります。
    クエリのオプションも豊富であり、クエリを使い分けるだけで様々なユースケースに対応できます。

    参照:Query DSL
    参照:Full text queries

  • Elastic Stack(ELK Stack)
    ElasticsearchLogstashKibanaなどからなる製品群です。まとめてElastic StackまたはELK Stackと呼ばれています。
    全てElastic社が主体となって開発されている製品であり、相互連携が優れています。
    特にKibanaはデータの可視化・分析、Elasticsearchの操作に大変有効であり、Elasticsearchのデータをより有効活用するのに役立ちます。

    参照:Kibana
    参照:Elastic Stackって何?

  • 企業が中心となっているOSS
    営利企業であるElastic社が中心となって開発が進められています。
    他のオープンソースの検索エンジンと比較すると開発が停止せず、システムの改良が継続的に行われていく可能性が高いと言えます。

Elasticsearchにまつわる専門用語や全文検索の処理過程に関しては以下の記載が分かりやすかったです。
参考:Elasticsearchの用語が覚えられないのでまとめた
参考:ElasticSearch の全文検索での analyzer について

差し当たり、インデックスがRDBで言うデータベースであること日本語の文書を全文検索する際はanalyzerの働きや構成要素を理解する必要があることを抑えていただければ、Amazon ESを利用するにあたっては十分だと思います。

Amazon ESとは

AWSによるElasticsearchのマネージドサービスです。それだけでは身も蓋もないので特徴を挙げてみます。

  • 利用開始が簡単
    AWSのマネジメントコンソールからGUIを使って立ち上げることもできますし、AWS CLIやCloudFormation、AWS CDKを使用して開始することもできます。

  • 運用コストが削減される
    マネージドサービスなので当然なのですが、Amazon ESの場合、日本語用プラグインのkuromojiが最初から利用できたりします。
    Elasticsearch稼働に必要なJavaのインストール・アップデートの手間も不要です。

  • AWSの他サービスとの連携
    Lambdaを使用したデータの前処理、CloudWatchによる監視なども簡単に行えます。

  • オリジナルのElasticsearchでは有料であるサービスを利用できる
    2020/5/22 追記:
    @n0z0me さまよりOpen Distro for Elasticsearchの説明についてご指摘をいただきました。
    当初の記述を修正いたします。
    参考:AWS、OSSだけで構成される「Open Distro for Elasticsearch」公開

    Elasticsearchはオープンソースですが、JDBCやSQLを使うためにはElastic社へ料金を支払う必要がありました。
    AWSはElasticsearchのプロプライエタリな部分を Open Distro for Elasticsearchとしてオープンソース化しました。

    OSSとプロプライエタリなコードが混在していたGitHub上のElasticsearchのコードのうち、オープンソースの部分とそのディストリビューションをOpen Distro for ElasticsearchとしてApache 2.0ライセンスの下公開しました。
    Amazon ESではOpen Distroに含まれる機能も簡単に利用できます。

  • 最新版のElasticsearchは使えない
    マネージドサービスの宿命として、最新版をすぐに使用するのが難しいという問題があります。
    どうしても最新版のElasticsearchを使用したい場合は、EC2等に自身でElasticsearchを導入するか、Elastic社が提供しているElastic Cloudを利用する必要があります。

  • 細かい設定はできない
    最新版を使えないのと同様、細かい設定はできません。
    日本語の形態素解析エンジンで最も一般的なのはMeCabですが、Amazon ESの場合はkuromojiに限定されます。
    辞書もkuromojiデフォルトのIPA辞書であり、NEologdも使えません。

立ち上げてみる

コンソールやCLIからでも立ち上げられますが、AWSのサービスであることを最大限活かすためにAWS CDKを使って立ち上げてみます。
AWS CDK自体については以下のリンクが参考になると思います。
参考:AWS CDK が GA! さっそく TypeScript でサーバーレスアプリケーションを構築するぜ【 Cloud Development Kit 】

また、CDKからAmazon ESドメインを立ち上げる方法は以下のページに詳しく書かれています。
参照:AWS CDKでAmazon Elasticsearch Serviceのドメイン(クラスタ)を作ってみた

CDKやその他必要なもののバージョンは以下の通りです。

% cdk version                                                                                                                                                                              [~]
1.37.0 (build e4709de)
# TypeScriptが必要
% tsc -v                                                                                                                                                                                   [~]
Version 3.4.5
% node -v                                                                                                                                                                                  [~]
v12.14.1

CDKのプロジェクトを作ります。

% cdk init --language typescript
Applying project template app for typescript
Initializing a new git repository...
Executing npm install...

# 中略

# Welcome to your CDK TypeScript project!

This is a blank project for TypeScript development with CDK.

The `cdk.json` file tells the CDK Toolkit how to execute your app.

# 中略

% exa                                      
drwxr-xr-x    - test  6 5 15:07 bin
.rw-r--r--  160 test  6 5 15:07 cdk.json
.rw-r--r--  130 test  6 5 15:07 jest.config.js
drwxr-xr-x    - test  6 5 15:07 lib
drwxr-xr-x    - test  6 5 15:08 node_modules
.rw-r--r-- 275k test  6 5 15:08 package-lock.json
.rw-r--r--  545 test  6 5 15:07 package.json
.rw-r--r--  543 test  6 5 15:07 README.md
drwxr-xr-x    - test  6 5 15:07 test
.rw-r--r--  598 test  6 5 15:07 tsconfig.json

CDKのコード内でAmazon ESを立ち上げられるようにするために以下のパッケージをインストールします。

% yarn add @aws-cdk/aws-elasticsearch

CDKを始めてデプロイする場合は、CloudFormationで利用するデプロイ用S3バケットを作成する以下のコマンドを入力します。

% cdk bootstrap

lib配下のtsファイル(stackname-stack.ts)を以下のように記述します。Amazon ESの設定を定義しています。

import * as cdk from "@aws-cdk/core";
import * as es from "@aws-cdk/aws-elasticsearch";

export class AmazonesPjStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // The code that defines your stack goes here

    // Amazon ESのバージョン
    const esVersion: string = "7.4";

    // アクセスポリシー設定のためのIPアドレス
    // ローカルのIPアドレスは curl -s https://checkip.amazonaws.com で分かる
    const sourceIP: string = `${MyIP}`;

    // ドメイン名
    const domainName: string = "test-es-domain";

    // Amazon ESドメインの詳細設定
    const domain = new es.CfnDomain(this, domainName, {
      // アクセスポリシー設定
      accessPolicies: {
        Version: "2012-10-17",
        Statement: [
          {
            Effect: "Allow",
            Principal: {
              AWS: ["*"],
            },
            Action: ["es:*"],
            Resource: `arn:aws:es:${cdk.Stack.of(this).region}:${
              cdk.Stack.of(this).account
            }:domain/${domainName}/*`,
            Condition: {
              IpAddress: {
                "aws:SourceIp": `${sourceIP || "127.0.0.1"}`,
              },
            },
          },
          {
            Effect: "Allow",
            Principal: {
              AWS: [cdk.Stack.of(this).account],
            },
            Action: ["es:*"],
            Resource: `arn:aws:es:${cdk.Stack.of(this).region}:${
              cdk.Stack.of(this).account
            }:domain/${domainName}/*`,
          },
        ],
      },
      domainName: domainName,
      ebsOptions: {
        ebsEnabled: true,
        volumeSize: 10,
        volumeType: "gp2",
      },
      elasticsearchClusterConfig: {
        instanceCount: 1,
        instanceType: "t2.small.elasticsearch",
      },
      elasticsearchVersion: esVersion,
      encryptionAtRestOptions: {
        enabled: false,
      },
      nodeToNodeEncryptionOptions: {
        enabled: false,
      },
      snapshotOptions: {
        automatedSnapshotStartHour: 0,
      },
    });
  }
}

実際にデプロイします。デプロイには10分程度時間がかかります。

% cdk deploy                                                                                                [~/Documents/code_test/amazones_pj]+[master]
yarn run v1.22.4
warning package.json: No license field
✨  Done in 1.32s.
AmazonesPjStack: deploying...
AmazonesPjStack: creating CloudFormation changeset...
 0/3 | 20:25:25 | CREATE_IN_PROGRESS   | AWS::Elasticsearch::Domain | test-es-domain (testesdomain) 
 0/3 | 20:25:25 | CREATE_IN_PROGRESS   | AWS::CDK::Metadata         | CDKMetadata 
 0/3 | 20:25:26 | CREATE_IN_PROGRESS   | AWS::CDK::Metadata         | CDKMetadata Resource creation Initiated
 1/3 | 20:25:26 | CREATE_COMPLETE      | AWS::CDK::Metadata         | CDKMetadata 
 1/3 | 20:25:27 | CREATE_IN_PROGRESS   | AWS::Elasticsearch::Domain | test-es-domain (testesdomain) Resource creation Initiated
1/3 Currently in progress: testesdomain
 2/3 | 20:39:32 | CREATE_COMPLETE      | AWS::Elasticsearch::Domain | test-es-domain (testesdomain) 
 3/3 | 20:39:33 | CREATE_COMPLETE      | AWS::CloudFormation::Stack | AmazonesPjStack 

 ✅  AmazonesPjStack

ドメインを作れました。

% aws es list-domain-names                                                                                                                                                                 [~]
{
    "DomainNames": [
        {
            "DomainName": "test-es-domain"
        }
    ]
}

設定してみる

Amazon ESは最近のElasticsearch同様、インデックスにデータを格納する前にmapping(RDBでいうカラム)を定義する必要はありません。
とはいえ、やはりmappingは事前に定義することをお勧めします。日本語の文章に対して適切なアナライザを使用しなければ全文検索を有効に行えなくなってしまいます。

そこで、以下のようなファイルを用意し、mappingとアナライザの定義を行います。

settings_es.json
{
  "settings": {
    "analysis": {
      "analyzer": {
        "index_analyzer": {
          "type": "custom",
          "tokenizer": "kuromoji_tokenizer",
          "mode": "search",
          "char_filter": ["icu_normalizer", "kuromoji_iteration_mark"],
          "filter": [
            "cjk_width",
            "kuromoji_baseform",
            "kuromoji_part_of_speech",
            "ja_stop",
            "lowercase",
            "kuromoji_number",
            "kuromoji_stemmer"
          ]
        }
      },
      "tokenizer": {
        "kuromoji_tokenizer": {
          "type": "kuromoji_tokenizer",
          "mode": "normal"
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "company": {
        "type": "text",
        "analyzer": "index_analyzer"
      },
      "products": {
        "type": "text",
        "analyzer": "index_analyzer"
      }
    }
  }
}

mappingsでインデックスに含まれるプロパティのデータ型ならびに使用するアナライザを定義します。
アナライザ部に含まれているのは日本語を全文検索するにあたり必要なプラグインです。各プラグインの詳細は以下のリンク先に詳しく書かれています。
参照:Elasticsearchを日本語で使う設定のまとめ

今回は2つのプロパティを用意します。両方text型です。他にもデータ型はありますが、全文検索を行う場合はtext型を使用する必要があります。

上記のJSONを以下の通りに適用します。オリジナルのElasticsearchと同様です。
エンドポイント以下にインデックス名を付加します。

% curl -XPUT "search-test-es-domain-mhwk6krh6562mjoa6wnlk3m5yu.ap-northeast-1.es.amazonaws.com/test?pretty" -H "Content-type: application/json" -d @settings_es.json     
{
  "acknowledged" : true,
  "shards_acknowledged" : true,
  "index" : "test"
}
% curl -XGET "search-test-es-domain-mhwk6krh6562mjoa6wnlk3m5yu.ap-northeast-1.es.amazonaws.com/test/_mapping?pretty"
{
  "test" : {
    "mappings" : {
      "properties" : {
        "company" : {
          "type" : "text",
          "analyzer" : "index_analyzer"
        },
        "products" : {
          "type" : "text",
          "analyzer" : "index_analyzer"
        }
      }
    }
  }
}

投入するデータは以下の通りです。

data.json
{"index": {"_index": "news","_type": "_doc","_id": "1"}}
{"company": "Microsoft", "products": "Windows .NET Office 365 Visual Studio Azure"}
{"index": {"_index": "news","_type": "_doc","_id": "2"}}
{"company": "Google", "products": "G Suite GCP Golang Flutter"}
{"index": {"_index": "news","_type": "_doc","_id": "3"}}
{"company": "Apple", "products": "MacOS iOS Swift iPhone"}

ちなみにElasticsearchに投入するJSONは末行を空行にする必要があります。空行にしないとThe bulk request must be terminated by a newline [\n]というエラーが出ます。

投入してみます。

% curl -XPOST "search-test-es-domain-mhwk6krh6562mjoa6wnlk3m5yu.ap-northeast-1.es.amazonaws.com/_bulk?pretty" -H "Content-type: application/json" --data-binary @data.json
{
  "took" : 500,
  "errors" : false,
  "items" : [
    {
      "index" : {
        "_index" : "test",
        "_type" : "_doc",
        "_id" : "1",
        "_version" : 1,
        "result" : "created",
        "_shards" : {
          "total" : 2,
          "successful" : 1,
          "failed" : 0
        },
        "_seq_no" : 0,
        "_primary_term" : 1,
        "status" : 201
      }
    },
    {
      "index" : {
        "_index" : "test",
        "_type" : "_doc",
        "_id" : "2",
        "_version" : 1,
        "result" : "created",
        "_shards" : {
          "total" : 2,
          "successful" : 1,
          "failed" : 0
        },
        "_seq_no" : 0,
        "_primary_term" : 1,
        "status" : 201
      }
    },
    {
      "index" : {
        "_index" : "test",
        "_type" : "_doc",
        "_id" : "3",
        "_version" : 1,
        "result" : "created",
        "_shards" : {
          "total" : 2,
          "successful" : 1,
          "failed" : 0
        },
        "_seq_no" : 0,
        "_primary_term" : 1,
        "status" : 201
      }
    }
  ]
}

以上で設定が終わりました。

使ってみる

格納されているデータに対してクエリを投げつけてみます。

クエリは全文検索用クエリで一般的なmatchを使用してみます。

query.json
{
  "query": { "match": { "products": "MacOS" } }
}
% curl -XGET "search-test-es-domain-mhwk6krh6562mjoa6wnlk3m5yu.ap-northeast-1.es.amazonaws.com/test/_search?pretty" -H "Content-type: application/json" -d @query.json
{
  "took" : 395,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 1,
      "relation" : "eq"
    },
    "max_score" : 0.2876821,
    "hits" : [
      {
        "_index" : "test",
        "_type" : "_doc",
        "_id" : "3",
        "_score" : 0.2876821,
        "_source" : {
          "company" : "Apple",
          "products" : "MacOS iOS Swift iPhone"
        }
      }
    ]
  }
}

きちんと取り出せました。

インデックスの削除はDELETEメソッドを使用して行います。

% curl -XDELETE "search-test-es-domain-mhwk6krh6562mjoa6wnlk3m5yu.ap-northeast-1.es.amazonaws.com/test?pretty"   
{
  "acknowledged" : true
}

シノニムやユーザー辞書を使用する

パッケージ機能登場以前

文章を全文検索するにあたりシノニム(類義語)やユーザー辞書(業界独自の用語のような専門的な単語を搭載した辞書)を使えた方が便利です。
従来、Amazon ESではシノニムの利用は不便であり、ユーザー辞書は使えませんでした。

例えば以下のようなデータをAmazon ESに投入します。

{"index": {"_index": "test","_type": "_doc","_id": "1"}}
{"company": "Microsoft", "text": "Microsoftの代表的な製品はWindowsです"}
{"index": {"_index": "test","_type": "_doc","_id": "2"}}
{"company": "マイクロソフト", "text": "マイクロソフトの代表的な製品はWindowsです"}

言うまでもなく「Microsoft」と「マイクロソフト」は同一の企業を指します。

以下のようなクエリを投げてみます。

query.json
{
  "query": { "match": { "text": "マイクロソフト" } }
}

残念なことにこのままではMicrosoftが含まれる文書を取り出すことはできません。

% curl -XGET "search-test-es-domain-mhwk6krh6562mjoa6wnlk3m5yu.ap-northeast-1.es.amazonaws.com/test/_search?pretty" -H "Content-type: application/json" -d @query.json
{
  "took" : 8,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 1,
      "relation" : "eq"
    },
    "max_score" : 0.2876821,
    "hits" : [
      {
        "_index" : "test",
        "_type" : "_doc",
        "_id" : "2",
        "_score" : 0.2876821,
        "_source" : {
          "company" : "マイクロソフト",
          "text" : "マイクロソフトの代表的な製品はWindowsです"
        }
      }
    ]
  }
}

パッケージ機能登場

しかし、2020年4月になってシノニムやユーザー辞書をパッケージというファイルで扱えるようになりました。

[新機能]Amazon Elasticsearch Service でファイルベースのシノニム、ユーザー辞書などに対応するカスタムパッケージを利用可能になりました

今回はシノニム機能を使ってみます。

シノニム用のファイルは以下のものを使用します。

synonym.txt
Microsoft,マイクロソフト

上記テキストファイルをESドメインで使うにはS3バケットにアップロードする必要があります。
バケットに関しては今回はCLIから作成します。

% aws s3 mb s3://amazon-es-bucket-test-20200510 
make_bucket: amazon-es-bucket-test-20200510
% aws s3 cp synonym.txt s3://amazon-es-bucket-test-20200510
upload: ./synonym.txt to s3://amazon-es-bucket-test-20200510/synonym.txt

CLIからパッケージを作成します。

% aws es create-package --package-name amazon-es-test-package --package-type TXT-DICTIONARY --package-source S3BucketName=amazon-es-bucket-test-20200510,S3Key=synonym.txt
{
    "PackageDetails": {
        "PackageID": "F230451472",
        "PackageName": "amazon-es-test-package",
        "PackageType": "TXT-DICTIONARY",
        "PackageStatus": "AVAILABLE",
        "CreatedAt": "2020-05-19T21:43:15.110000+09:00"
    }
}

シノニムを使用する場合、アナライザの設定を以下の通りにします。

setting_es.json
{
  "settings": {
    "analysis": {
      "analyzer": {
        "index_analyzer": {
          "type": "custom",
          "tokenizer": "kuromoji_tokenizer",
          "mode": "search",
          "char_filter": ["icu_normalizer", "kuromoji_iteration_mark"],
          "filter": [
            "cjk_width",
            "synonym_filter",
            "kuromoji_baseform",
            "kuromoji_part_of_speech",
            "ja_stop",
            "lowercase",
            "kuromoji_number",
            "kuromoji_stemmer"
          ]
        }
      },
      "filter": {
        "synonym_filter": {
          "type": "synonym",
          "synonyms_path": "analyzers/F230451472"
        }
      },
      "tokenizer": {
        "kuromoji_tokenizer": {
          "type": "kuromoji_tokenizer",
          "mode": "normal"
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "title": {
        "type": "text",
        "analyzer": "index_analyzer"
      },
      "text": {
        "type": "text",
        "analyzer": "index_analyzer"
      }
    }
  }
}

ポイントは2点あります。トークナイザーのmodeをnormalにすることと、シノニム用のフィルターを可能な限り早めに当てることです。
どうやらトークナイザーが文章を加工する過程で特殊文字が混じるために、シノニム機能がうまく処理を行えなくなることが原因のようです。

コンソールもしくはaws es associate-packageコマンドでドメインとパッケージの関連付けを行います。
こうすることでドメイン内のインデックスからパッケージ中のユーザー辞書やシノニムファイルを使用することができます。

先ほどのクエリを投げてみると、両方の文書を取り出すことができました

% curl -XGET "search-test-es-domain-mhwk6krh6562mjoa6wnlk3m5yu.ap-northeast-1.es.amazonaws.com/test/_search?pretty" -H "Content-type: application/json" -d @query.json
{
  "took" : 41,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 2,
      "relation" : "eq"
    },
    "max_score" : 0.41501677,
    "hits" : [
      {
        "_index" : "test",
        "_type" : "_doc",
        "_id" : "2",
        "_score" : 0.41501677,
        "_source" : {
          "company" : "マイクロソフト",
          "text" : "マイクロソフトの代表的な製品はWindowsです"
        }
      },
      {
        "_index" : "test",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 0.41501677,
        "_source" : {
          "company" : "Microsoft",
          "text" : "Microsoftの代表的な製品はWindowsです"
        }
      }
    ]
  }
}

まとめ

Amazon ESはEC2等のサーバ上で運用する生のElasticsearchと比較して制限が多いです。
場合によってはその制限が致命的で採用を見送る場合もあると思います。

しかし、Query DSLの工夫や、つい最近登場したパッケージ機能でかなり使いやすくなるとも思います。

それでもマネージドなのは魅力的で、AWSも積極的にAmazon ESの改良に動いています。
今後ますます使いやすくなると思いますので皆さまもぜひ使ってみていかがでしょうか。

11
11
1

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
11
11