【AWS Amplify ノウハウ】 5. List API は DynamoDB で, Search API は Elasticsearch で!

2020.07.31

こんにちは!コンサル部のテウです。

AWS Amplifyシリーズの5番目の記事です!今回は、List API と Search APIがそれぞれ異なるデータソースにQueryを実行ことによって生じる差について確認し、それぞれをどういった状況でどう使えるべきかについてお伝えします。

それでは始めます!

List は List、Search は Search じゃない?

先ず、List と Search をなぜ比べているのか疑問を持たれた方々が多くいらっしゃるかと思います。当初、これらの用途はそれぞれ異なるのでしょう。ところで、Amplify が提供してくれる List API は、DynamoDB をデータソースとして用い、Search API は Elasticsearch をデータソースとして用いていることをご存じであれば、活用方法についても少し悩む必要があることに気づく方がいらっしゃるかと思います。

突然ですが、私は個人的に DynamoDB が好きです。Elasticsearch も好きです。ですが、DynamoDB と Elasticsearch を一緒に使うことは大好きです。

DynamoDB は AWS の代表的な KVS (Key-Value Storage) であり、使った分だけ課金されるサーバレスデータベースプラットフォームとして知られています。AWS Lambda を使うと、ほぼ GO-TO データベースとして選択されるサービスでもありかと思います。DynamoDB は KVSの特性上、どんな Query に対してもミリ秒(ms)で結果を取得できる優秀なパフォーマンスを持った一方、複雑な Query や aggregation 作業ではサポートされてない機能も多いですよね。なので、JSONデータを DynamoDB の一つの Column に保存することはできるとしても、この Column の JSON 内部フィールドに対しては Query ができなかったり、あるいは、Queryの対象となる全体データ数を count してくれる機能が無かったりします。

はい、DynamoDB だけではこういった「検索」機能が足りてないかもしれません。ですが、DynamoDB は Streams という強力な機能をサポートしています。すなわち、DynamoDB にデータが生成/修正/削除されたら、DynamoDB Streamsにて該当データ操作の履歴が他のデータソースに連携されることも可能です!Eventual Consistency(結果整合性)と呼ばれるテクニックでもあるこの方法を活用することで、DynamoDB で入力されたデータを全て Elasticsearch に投げて、DynamoDB と Elasticsearch のデータを合わせる(Sync)ことが可能になります。

なので、簡単な Query やデータ操作は DynamoDB で、複雑な Query は Elasticsearch で処理させる、といったアプローチで非常に多様なユーズケースが満たされます。

ちなみに、以下の記事をご覧になると、DynamoDB Streams と AWS Lambda で Elasticsearch にデータを同期化させる方法が理解できるかと思います。

本論に戻り、もう一度 DynamoDBの制約事項について確認してみたいと思います。

結論から言いますと、DynamoDB は count() を支援しないで、カーソル基盤のページネーションだけを提供するので、ページ番号を指定する等の offset基盤のページネーションは不可能となります。これは何を意味しますでしょうか?次のような状況を考えてみましょう。

Facebook や Twitter などのアプリのタイムラインにアップされたポスティングを読むためにスクロールを下にすると、ページの一番下に行く前に、新しいポスティングを追加でローディングしてくれます。また、スクロールを下にし続けると、最後のポスティングを基準とし、また新しいポスティングを追加でローディングしてくれます。

つまり、ページ単位をポスティング20個に仮定すると (page_size = 20)、最新の順番で整理された状態で90番目の古いポスティングを読むためには必ずその先の 2、3、4 ページの次に、5ページになってから 90番目のポスティングを確認することができます。こういった動作方式は、アプリ側では当然であるものの、何も問題になることはありません。ですが、このようなアプリの管理者用CMS等で、下のイメージのようにページのステータスバーを入れようとする要件を考えてみましょう。

こんな場合、DynamoDB は count() 機能がないので、最初から何番目のページが最後のページかを分かる方法がありません。また、各ページをクリックした際に、前ページをすべて一つずつ呼び出してからやっと移動したかったページに行けるので、極端的なケースを仮定してみると、最初のページから999番目ページに移動したい場合、大変なパフォーマンス問題が生じる可能性が高いでしょう。(当然コード複雑度も高くなり、コストも高くなります)

すなわち、上のような要件があった場合は、DynamoDB で頑張る必要なく、Elasticsearch を活用した方がより簡単で、かつ、効果的だということを頭の中に浮かべるでしょう。

最初から話しましたように、Amplify の List API は DynamoDB を、Search API は Elasticsearch をデータソースとして活用しています。なので、管理者用 CMS サービスを開発するときに、ページのステータスバーを入れようとすると、List API ではなく、Search API を使った方が正しい判断ではないかと思っております。また、各フィールド別の検索条件を付けて検索する場合、あるいは、各フィールド別にソーティングしたい場合等にも List API ではなく、Search API を使った方が良いケースが多いかと思います。ある場合は List API と Search API を一緒にコードで使ってももちろん構いませんが、私は結局コードの複雑度を減らすために List API は使わず Search API だけを使った場合もありました。

一方、上のような要件がなく、非常に単純な List API だけで行けるときは、もちろん List API だけを使っても良いでしょう。

Search API について

Elasticsearch の仕様自体は size と from オプションを通じて移動したいページへすぐ移動させる Query を投げることができますが、AWS Amplify が自動的に生成してくれる resolver 及び API ライブラリーコードでは、DynamoDB を活用した API との一貫性のためなのか、トークン(カーソル)基盤のページネーションの形で Query を投げるインターフェースだけを提供しています。

List API と Search API の Query の結果で差があるとしたら、DynamoDB をデータソースとする Query (List) では、items フィールドと共に nextToken だけ教えてくれる一方、Elasticsearch をデータソースとする Query は、これらに加えて count フィールドも一緒に教えてくれています。

つまり、Query の対象となるデータのトータルが分かります!これを知るだけでも管理者用CMSのページのステータスバーの最後のページ番号が簡単に計算できるのでしょう。

もちろん、該当ページに直接アクセスが可能な方法ではないので、何度も Query を投げないと移動したいページに到達できないという致命的なデメリットはまだ解決されませんが、こういった場合は、Amplify で基本的に生成してくれる resolver ではなく、custom resolver を作成することで、このような問題を解決することもできます。custom resolver は AWS Amplify で開発するにあたって、活用の価値が非常に高い機能ですので、下記の記事を読んでいただき、必ず理解し、適材適所に活用するのはいかがでしょうか。

ちなみに、使おうとしている API のデータソースが DynamoDB か Elasticsearch かが分かるためには、API の名前の始まりを見るとすぐ分かります。Search で始まる API だけが Elasticsearch データソースを使う API です。Create, Get, Update, Delete, List 等で始まる API はすべて DynamoDB をデータソースとした API です。

@searchable について

本記事のターゲットは、Amplifyの入門者ではなく、Amplify チュートリアルをやってみた方、もしくは、実際に Amplify を活用するとする方を対象としています。なので、本記事では AWS Amplify の基礎的なことは説明しませんが、 Search API を使うために必要なのは GraphQL Schema で @searchable ディレクティブを type につけておくことしかないことを、この記事を読んでいらっしゃる皆さんは既にご存知だと思います。

ところが、公式ドキュメントを読んでみると、いくつかの注意事項が書いてあります。

AWS Amplify @searchable 使用時の注意事項について

Note: @searchable is not compatible with DataStore but you can use it with the API category.

Note: @searchable is not compatible with Amazon ElasticSearch t2.micro instance as it only works with ElasticSearch version 1.5 and 2.3 and Amplify CLI only supports instances with ElasticSearch version >= 6.x.

Note: Support for adding the @searchable directive does not yet provide automatic indexing for any existing data to Elasticsearch. View the feature request here.

Migration warning: You might observe duplicate records on search operations, if you deployed your GraphQL schema using CLI version older than 4.14.1 and have thereafter updated your schema & deployed the changes with a CLI version between 4.14.1 - 4.16.1. Please use this Python script to remove the duplicate records from your Elasticsearch cluster. This script indexes data from your DynamoDB Table to your Elasticsearch Cluster. View an example of how to call the script with the following parameters here.

1番目に、DataStoreを使うときには @searchable は活用できないこと。

2番目に、Amazon Elasticsearch の t2.micro インスタンスが利用できないこと。

3番目に、DynamoDB をデータソースとして連携しておいた既存の type に @searchable をつけても、既存の DynamoDB に保存されていたデータについては Elasticsearch に同期化されないので indexing ができないこと。 (こんな場合には、直接既存のデータをマイグレーションすると解決できると思います。)

4番目に、CLIバージョンの問題でデータが重複保存される場合もあること。

いずれも軽くパスできないと思いますので、しっかり確認しておいたた方が良いでしょう。

最後に

本記事では、List API と Search API が持っている特徴を理解し、どんな状況でどんな API を使うべきかについての判断基準をお伝えしました。

以上、コンサル部のテウでした!:D