Engineer's Way

主にソフトウェア関連について色々書くブログです。

ElasticSearchで親-子-孫の3世代ドキュメントを作って、子や孫の内容で検索する

 

f:id:matsnow:20171204030753p:plain

前職の時、AWSのElasticSearchService (Ver5.3) を使っていましたが、日本語どころか英語でもあまり情報がなかったりするので苦労しました。
RDBでよくある、テーブル結合して検索ということがしたかったのですが、手間取ったので方法を残しておきます。

ElasticSearchで3世代ドキュメント構造を持ったindexを作る

たとえば、「商品カテゴリ - 商品 - 商品のレビュアー」みたいに、それぞれのデータに1:nの関係性を持たせたい場合、RDBなら外部キーを使ってJOINすればOKです。 ElasticSearchで、そういった構造を表現したい場合、_typeを使います。

親の方から1,2,3世代とするとき、1世代目、2世代目、3世代目に該当する_typeの名前を決めて、インデックスを設計します。 上の例の場合、名前をcategoryproductreviewerとするなら、以下のようなmappingのJSONをインデックスに適用しておきます。

PUT /market

{
  "mappings": {
    "category": {},
    "product": {
      "_parent": {
        "type": "category" 
      }
    },
    "reviewer": {
      "_parent": {
        "type": "product" 
      }
    }
  }
}

RDBで各テーブルのレコードに外部キーを持たせるのと違う点は、「あくまで1つのindexの中にフラットなデータとして入っている」ということです。 要は_idの重複などを気にしないといけません。

3世代ドキュメント構造を持ったindexにデータを放り込む

  1. 2世代目のドキュメントを放り込む時、_parent: 1世代目の_id(例:_parent: "c1")を追加で渡します。
  2. 3世代目のドキュメントを放り込む時、_parent: 2世代目の_id, _routing: 1世代目の_id(例:_parent: "p1", _routing: "c1")を追加で渡します。

これを_bulkAPIに渡すJSONとして表現するなら

{ "index": { "_index": "market", "_type": "category", "_id": "c1" } }
{"category_name": "clock" }
{ "index": { "_index": "market", "_type": "product", "parent": "c1", "_id": "p1" } }
{"product_name": "clock1" }
{ "index": { "_index": "market", "_type": "reviewer", "parent": "p1", "routing": "c1", "_id": "r1" } }
{"reviewer": "hoge" }

みたいになります。

こうすることで、index内が、図で表すと以下のようなドキュメント構造になります。 f:id:matsnow:20171204030054j:plain

子ドキュメントで検索し、親・孫ドキュメントを含めた一連のドキュメントを取得する

これがあまり情報がなかったのですが、以下のようなクエリを書けば子ドキュメントのデータで検索した上で、3世代分のドキュメントを結合して一気に返してもらうことができます。

{
    "query": {
        "has_child" : {
            "type": "product",
            "query": {
                "bool": {
                    "must": [
                      {
                         "term": {"product_id": "123"}
                      },
                      {
                         "has_child": {
                            "type": "reviewer",
                            "inner_hits": {},
                            "query": {"match_all": {} }
                         }
                      }
                   ]
                }
            },
            "inner_hits": {}
        }
    }
}

boolクエリで、2世代目への検索クエリ(term)と、3世代目の全件取得(has_child.query.match_all)をまとめるのがポイントです。 ※ もちろん、3世代目で更に絞り込みをしたければ、match_allの代わりに別のクエリを使います。

それと、inner_hitsを書くことで、2、3世代目のドキュメント一式を取得できます。 書き忘れると1世代目のドキュメントしか返ってきません。

注意点

公式見解として、今後は1つのindexに対して1つのtypeが推奨されています。
また、親子関係をもたせたいときは5.6から追加されたjoinフィールドを使って欲しいとのこと。
今後のロードマップによると、バージョン8.xあたりでtypeが事実上消滅するようです。(まだまだ先の話でしょうが。。)

www.elastic.co

AWSだと、12/6時点でもまだ5.5までしか選択できないですが、5.6以降がサポートされたら単一type & joinフィールドを使う方向で設計した方が良さそうですね。

Electronでパッケージを作ろうとした時に「May not delete」というエラーが出る

 

electron-packagerでElectronでパッケージを作ろうとした時、以下のエラーが発生しました。  

$  electron-packager . sample --platform=darwin --arch=x64 --overwrite --out build"
npm ERR! May not delete: /private/var/folders/q3/hg9ht5vx5rjddgghnk8z9w480000gn/T/electron-packager/darwin-x64/sample-darwin-x64/Electron.app/Contents/Resources/app/node_modules/.bin

npm ERR! A complete log of this run can be found in:
npm ERR!     /Users/hogehoge/.npm/_logs/2017-11-14T10_35_53_001Z-debug.log

npmのバージョンが問題だったらしく、5.3だとダメでした。
内部で実行しているnpm prune --productionがやらかしている模様。

github.com

なお、最新の5.5.1ならOKでした。

API Gateway + CloudFrontの構成でカスタムドメインを使用すると、IAMでのアクセス制限ができない件

 

AWSAPI Gatewayを使ってプロダクション環境でAPIを公開する時、WAFを噛ませて防御したいと思うことがあります。
今のAPI Gatewayには、残念ながらWAFと直接関連付ける仕組みがないので、そういった場合はAPI Gatewayの前段にCloudFrontを置く必要があります。
(API Gatewayも内部的にはCloudFrontを使っているので、二重になってしまいますが仕方ありません)

ドメイン名もビジネス要件に合ったものを使うことになるので、それをRoute53に登録して、CloudFrontと紐づけることになります。
図で示すと以下のようになりますね。

f:id:matsnow:20171031011417p:plain

一方、API Gatewayにはざっくり以下の3つのアクセス制限方法があります。

  1. IAM認証 (v4署名)
  2. カスタムオーソライザー
  3. Cognitoオーソライザー

docs.aws.amazon.com

このうち、IAM認証を先ほどのAPI Gateway + CloudFront + 任意のドメインの構成上で使おうとしても失敗します。
正確には、v4で署名したリクエストをCloudFront(の先にあるAPI Gateway)に送信しても403エラーで返ってきます。
Amazonのサポートにも問い合わせてみましたが、残念ながら現時点の仕様とのことです。

誠に恐れ入りますが、API Gateway の前段に CloudFront ディストリビューションを配置頂いている場合、 API Gateway 上では生成 SDK による API の IAM 認証をご利用頂くことができません。 すでにご確認を頂いております通り、 SDK 内の invokeUrl を CloudFront にて公開頂いているカスタムドメイン名に編集頂くことで、 署名の生成に使用されるドメイン名と実際の API エンドポイントであるドメイン名が不一致となり、API Gateway 上での署名検証が失敗致します。

よって、API Gateway + WAF + カスタムドメインを使いたい場合、APIの認証はカスタムオーソライザーかCognitoオーソライザーでやることになります。 API Gatewayがバージョンアップして、直接WAFと連携させられるようになるのを期待しましょう。

ほかのサーバレス系の記事

matsnow.hatenablog.com matsnow.hatenablog.com