蟻地獄

Twitterに書ききれない長めの文とか書くよ

CloudSearchのdomainを複数環境で共有したい

この記事は VALU Advent Calendar 2019 20日目のエントリです。

株式会社VALUのバックエンドエンジニアのMito Memelです。前回のエントリで、渾身のギャグ「これでキューが急に詰まっても安心ですね!」が誰にも拾ってもらえなくて悲しいです。

VALUのサーバインフラはAWSですが、AWS全文検索システムを構築する場合、Elasticsearch ServiceとCloudSearchどちらを使えば良いか迷いますよね。Elasticsearchにしか無い機能を使いたい場合は選択の余地はありませんが、そうでない場合、CloudSearchのオートスケーリングは魅力的です。Elasticsearch Serviceは自動フェイルオーバーはしてくれるものの、インスタンスタイプ・ノード数の計画は自分で立てる必要があります。何も考えずにdomain(CloudSearchクラスタの設定単位)を1個作るだけで良いCloudSearchはとてもお手軽です。

しかし、CloudSearchにも欠点があります。スケールアウトは得意なんですが、性能がそれほど必要では無い場合、スケールダウンしたくても最小構成で1domainにつき$60/月程度(東京リージョンの場合)掛かります。1domainだけならまだ良いんですが、例えば開発環境が複数あったりすると環境ごとにdomainを作らなければならず、リソースは有り余っているのに料金がかさむという状況になりがちです。その点Elasticsearchだと1domain内でもindexで分けることができるので、複数環境でdomainを共有するのも簡単です。残念ながらCloudSearchにはElasticsearchで言うindexのようなものは無いのですが、どうにかして1domainを複数環境で共有できるようにしたいものです。そこで、実際に試してみました。

fieldでがんばる

まず最初に思い付くのは、index みたいな名前のfieldを追加して、検索時に毎回そのfieldを条件に含めるというやり方です。この方法でもできなくは無いのですが、いくつか欠点があります。

idフィールドの被り

CloudSearchにはMySQLで言うauto_incrementのような自動採番は無く、documentのアップロード時にユニークなidを明示的に指定する必要があります。このidは例えばユーザー検索ならRDB側で振っているユーザーIDを使うなどすれば良いのですが、1domainを複数環境で共有する場合、idが被ってしまう可能性があります。なので、idのprefixに環境名を付けるなどの工夫が必要となります。

field名のルールが増える

index みたいな名前のフィールドを追加する」と先ほど書きましたが、documentの検索対象fieldでうっかり同じ名前を使ってしまうみたいな事故が考えられます。なので環境識別のための名前空間として使用するfieldはsuffixに _ を付けて index_ みたいな名前にし、検索対象fieldではそのsuffixを避ける、みたいなルールが必要になります。これでも良いんですが、なんかこう、もうちょっとスマートにやりたいですよね。

idを構造化する

idフィールドの被りを避けるためにidにprefixを付けるなどの工夫が必要なのであれば、いっそのことidフィールドのprefix検索だけで名前空間を分けられないか?と考えました。

idフィールドに使える記号は​ _ - = # ; : / ? @ です。これを使って、例えば {環境名}/{データ種別}/{連番} のような階層構造のidを付ければ、idの被りを防いだ上で、余分なfieldを追加しなくてもidのprefix検索で環境を特定することが可能です。実際のコードは以下のようになります。

documentのアップロード

public function uploadDocuments(CloudSearchDomainClient $client, string $index, string $type, array $documents)
{
    // idを置換
    $formatted_documents = array_map(function($document) use ($index, $type){
        $document['id'] = "{$index}/{$type}/{$document['id']}";
        return $document;
    }, $documents);

    $client->uploadDocuments([
        'contentType' => 'application/json',
        'documents' => json_encode($formatted_documents),
    ]);
}

documentの検索

public function search(CloudSearchDomainClient $client, string $index, string $type, string $query)
{        
    // クエリにidのprefix条件を追加
    $formatted_query = "$query _id:{$index}/{$type}/*";

    $response = $client->search([
        'query' => $formatted_query,
        'queryParser' => 'lucene',
    ]);

    // id部分のみを返す
    return array_map(function($id){
        return explode('/', $id)[2];
    }, array_column(array_get($response, 'hits.hit', []), 'id'));
}

まとめ

idのprefix検索を利用することで、CloudSearchのdomainを複数環境で共有することができました。これで開発環境では料金を抑えつつ、本番環境ではCloudSearchのオートスケールのメリットを享受することができます。