CloudSearchの内部動作と、それを知らぬがゆえにハマったことを経験し、要件としては「姓名マスタから名前で検索したい」ってだけだったのに何でこんな苦労してんやろ、、
しかもCloudSearch意外に高いし、、インスタンスサイズがどんどん増えて、Multi-AZで数も増えてくとか辛い。辞書のメンテとかindex再構築とかもうやりたくない。
何とか既存システムの乗ってるDynamoDBで完結できないものだろうか?
一晩悩んでみた
もっとも簡単のはscan等で全件データ取得し、フィルターかける方式です。
ええ、はい、検索リクエスト来るたびに全件テーブル舐めていいなら、、
どうにかしてqueryで効率的に検索をしたいところ。
しかし単純に「名前で検索したい」といっても一般ユーザに解放しているサービスなので、漢字、平仮名、片仮名、アルファベット、姓のみ、名のみ、漢字と平仮名混合などなど、色々なパターンでリクエストが来ます。
一番簡単なのは、インデックスを利用する案でしょうか。
案1
Table
partition key | sort key |
---|---|
{GroupId} | {IndividualId} |
LSI-1
partition key | sort key |
---|---|
{GroupId} | {漢字氏名} |
LSI-2
partition key | sort key |
---|---|
{GroupId} | {平仮名氏名} |
LSI-3
partition key | sort key |
---|---|
{GroupId} | {カタカナ氏名} |
LSI-4
partition key | sort key |
---|---|
{GroupId} | {アルファベット氏名} |
検索リクエストが来るたび、各indexにアクセスする案。sort keyをbegins_withで探せばいけそうです。
コストを考えるとLSIが良いかな?
しかしよく考えると氏名を別々に区切ってインデックスを貼らないと名前だけでの検索ができません。となると漢字カタカナ平仮名アルファベットを各々姓と名で用意しなければいけないので8index。LSIの制限を越えるので、GSIを貼る必要があります。
案2
Table
partition key | sort key |
---|---|
{GroupId} | {IndividualId} |
GSI-1
partition key | sort key |
---|---|
{GroupId} | {漢字姓} |
GSI-2
partition key | sort key |
---|---|
{GroupId} | {漢字名} |
GSI-3
partition key | sort key |
---|---|
{GroupId} | {カタカナ姓} |
GSI-4
partition key | sort key |
---|---|
{GroupId} | {カタカナ名} |
GSI-5
partition key | sort key |
---|---|
{GroupId} | {first name} |
GSI-6
partition key | sort key |
---|---|
{GroupId} | {middle name} |
GSI-7
partition key | sort key |
---|---|
{GroupId} | {family name} |
同じような構成なのにLSIとGSIが混じるのも何かトラブルの元になりそうなので、全部GSIと開き直ってみる。
リクエストに平仮名が来た場合はカタカナに変換するロジックを使えば平仮名用indexは貼らずにすみそうです。
しかし、検索リクエストのたびにgsiに7回アクセスするの、、、?
リクエストがカタカナだったらカタカナGSIへ、とかロジック書けば多少マシになりそうですが、無理やり感が否めない、、
将来的に他の属性で検索できる状態にしたい、とも思っていたので、その度にindex増えていくのも辛いです。
やはり全文検索専用のサービス様にはかなわないのか、、
そこで閃き
Table
partition key | sort key |
---|---|
{GroupId} | {IndividualId} |
{GroupId} | 検索語句A: {IndividualId} |
{GroupId} | 検索語句B: {IndividualId} |
じゃじゃーん!
検索語句には漢字姓やカタカナ名など、検索したい属性を入れていきます。検索語句の最後にIndividualIdを入れているのは一意性を担保するためです。
これなら属性がどれだけ増えても大丈夫。indexを増やす必要はありません。query1回、begins_withを利用するだけでほぼ全文検索が可能となります!
結果
この実装を持ってプロダクションリリースしたところ、無事にCloudSearchと同等のレベルのサービスを格安で提供することができるようになりました。やったじゃん!
ただし後から気づいたデメリットも。
- データ量が増えまくって書き込みスループットが増えた
GSIを貼って並列に書き込むか、直列に書き込むかという違いではあるのですが、batchPutを利用してもなかなか終わらない。これは仕方ないよね。
- 登録データ件数管理が必要になった
マスタデータにはカナが抜けてるもの、アルファベットが無いものと様々あるので、単純に登録件数の1/Xが人数、と保証できない。
そのため登録件数管理用のテーブルを用意する必要が出て来ましためんどくさい。
ただ、私の要件からすると十分にこれらのデメリットを補えるシステムとなりました。
DyanmoDBをこんな使い方すること自体色々間違ってるとか素直にCloudSearchに頼っとけとかRDSツカエヨって心の声はシャットダウンし、こんな方法でもプロダクション利用の実績ができました!というお話でした。