2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

DynamoDBで文字列の部分一致検索をする (無料利用枠に収める)

Last updated at Posted at 2024-10-27

目的

  • 任意の属性値に対して部分一致検索(contains)を行う
  • 無料利用枠内に必ず収める
  • 速度は気にしない

結論

  • ExecuteStatementCommandを使う
  • ReturnConsumedCapacityで消費キャパシティを確認する
  • 最適なLimitをかけて、取得処理をループさせる

はじめに

無料利用枠の難しさ

そんな魅力的なDynamoDBだが、無料利用枠内に必ず収めようとするとさまざまな制約が生じてくる。オートスケーリングはoff、読み込み/書き込みキャパシティの割り当ては低めに設定...。この場合に、レコード数の多いテーブルに対してScanCommand(全件取得)を実行すると、読み込みキャパシティが足りずにコケる場合がある。消費するキャパシティの手綱を完全に掌握しつつ、意図した動作をさせるには工夫が要る。

部分一致検索ゆえの難しさ

AWS DynamoDBで部分一致検索をしようとすると、FilterExpressioncontainsを使うしかない(他にあったら教えてください...)。
FilterExpressionによる絞り込みは、取得されたレコードに対して行われる。そのため、全レコードに対して部分位置検索をかけようとすると、QueryでもScanでも内部的には一度全件取得をする必要がある。この点において、Queryの旨味である「消費キャパシティを抑えられる」ことを活かすことができない。

解決策

「消費キャパシティを抑えつつ全件取得することが難しい」「containsを使うなら全件取得が要る」双方を解決するのが、ExecuteStatementCommandだ。最大の特徴は、ScanCommandQueryCommandではできない「ループして全件取得戦法」が使える点で、Limitで溢れたレコードを再取得するためのtokenが返り値に含まれる。このtokenがundefinedになるまで再取得すれば、条件にあうレコードを全件取得することができる。
また、ReturnConsumedCapacityを設定することで、そのリクエストで消費したキャパシティの量が表示される。この値はLimitに依存するので、その環境に合ったLimitを経験的に設定し、最大消費キャパシティを管理することができる。

実装

条件

  • ローカル環境で実行
  • 実行ユーザはMFAなしでリソースにアクセスできる
  • アクセスキーなどは環境変数から読み込み
  • 対象テーブル名はtablename
  • 検索対象キーtarget_keyに対してsearching_stringで部分一致検索をする
import { fromEnv } from "@aws-sdk/credential-providers";
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import {
    DynamoDBDocumentClient,
    ExecuteStatementCommand,
    ExecuteStatementCommandInput
} from "@aws-sdk/lib-dynamodb";

// クライアント定義
const client = new DynamoDBClient({
    region: process.env.AWS_REGION,
    credentials: fromEnv(),
});
const docClient = DynamoDBDocumentClient.from(client);

// DynamoDBアクセス
const accessor = async (statement: string, nextToken?: string) => {
    const inputs: ExecuteStatementCommandInput = {
        Statement: statement,
        ReturnConsumedCapacity: "TOTAL",
        Limit: 10// ここの値は、目標のキャパシティ数に応じて調整する
    };
    if (nextToken) inputs.NextToken = nextToken;
    const command = new ExecuteStatementCommand(inputs);
    const response = await docClient.send(command);
    return response;
};

// ループして取得
const main = async () => {
    const statement = `SELECT * FROM "tablename" WHERE contains(target_key, 'searching_string')`;
    const responses: Array<object> = [];
    let nextToken = undefined;
    while (true) {
        const res = await accessor(statement, nextToken);
        res.Items?.map((item) => responses.push(item));
        if (res.NextToken) {
            nextToken = res.NextToken;
        } else {
            break;
        }
    };
};

main();

テスト

  • DynamoDBの生responseの例
{
  '$metadata': {
    httpStatusCode: 200,
    requestId: 'hogehgoehogehogehogehogehogehogehogehoge',
    extendedRequestId: undefined,
    cfId: undefined,
    attempts: 1,
    totalRetryDelay: 0
  },
  ConsumedCapacity: { CapacityUnits: 0.5, TableName: 'tablename' },
  Items: [
    {
      id: 1,
      name: 'ほげげ'
    }
  ],
  LastEvaluatedKey: { id: 23 },
  NextToken: 'kokonitottemonagaitokengahairuyo'
}

おわりに

DynamoDBで部分一致検索をしつつ、必ず無料利用枠内におさめるには、

  • ExecuteStatementCommandを使う
  • ReturnConsumedCapacityで消費キャパシティを確認する
  • 最適なLimitをかけて、取得処理をループさせる

おまけ

ぼやき

  • こんな感じでループするのRedisぽい

LIKEは使えない

このドキュメントでは用例としてLIKE表現を用いた検索方法が紹介されているが、DynamoDBではLIKE表現が使えなかった。
よく調べてみると、このドキュメントにはLIKEがなかった。
(わかりづらいよッ...!!!)

検索対象文字列のクオテーション

次のPartiQL文は期待した動作をしない

SELECT * FROM "table_name" WHERE contain(target_key, "searching_string");

正しいのはこっち

SELECT * FROM "table_name" WHERE contain(target_key, 'searching_string');

おそらくこのあたりが原因だと思われる(SQL初心者)。

2
2
0

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?