※この記事は生成AI(Claude Code)によって作成されています。
はじめに
AWS DynamoDBで全文検索機能を実装する際、多くの開発者がAmazon OpenSearch Service(旧Elasticsearch Service)の導入を検討します。しかし、OpenSearchは強力な検索機能を提供する一方で、月額コストが数万円〜数十万円になることも珍しくありません。
本記事では、OpenSearchを使用せずにDynamoDB単体で実用的な検索機能を実現する方法を、実際のプロジェクト(フィギュア写真共有サービス)での実装経験をもとに解説します。
OpenSearchを使わない理由
コスト比較
| サービス | 最小構成の月額目安 | 本番推奨構成の月額目安 |
|---|---|---|
| Amazon OpenSearch Service | 約$50〜100/月 | 約$200〜500/月(マルチAZ) |
| DynamoDB(オンデマンド) | 使用量に応じて課金 | 数$〜数十$/月(中規模サービス) |
特にスタートアップや個人開発では、この差は大きな負担となります。
DynamoDBで検索を実現するアーキテクチャ
1. GSI(Global Secondary Index)の活用
DynamoDBでは、GSIを使用することで様々な検索パターンに対応できます。
// GSI設計例(JSON形式)
{
"GSI2-CategorySearch": {
"PK": "GSI2PK", // CATEGORY#cat_1
"SK": "GSI2SK" // 作成日時(降順ソート用)
},
"GSI3-LicenseFilter": {
"PK": "GSI3PK", // LICENSE#cc-by
"SK": "GSI3SK"
},
"GSI4-SearchSupport": {
"PK": "GSI4PK", // SEARCH#keyword
"SK": "GSI4SK" // EntityType#EntityId
},
"GSI5-PopularitySort": {
"PK": "GSI5PK", // POPULAR#2026-01
"SK": "GSI5SK" // スコア(人気順ソート)
}
}
2. 検索インデックスの事前構築
キーワード検索を実現するために、データ登録時に検索用インデックスを構築します。
// TypeScript: 写真登録時にキーワードインデックスを作成
const keywords = extractKeywords(photo.title + ' ' + photo.description);
for (const keyword of keywords) {
await docClient.put({
TableName: TABLE_NAME,
Item: {
PK: `SEARCH#${keyword.toLowerCase()}`,
SK: `PHOTO#${photo.id}`,
GSI4PK: `SEARCH#${keyword.toLowerCase()}`,
GSI4SK: `PHOTO#${photo.id}`,
EntityType: 'Photo',
EntityId: photo.id,
Type: 'SearchIndex'
}
});
}
FilterExpressionによるセキュリティフィルタリング
検索結果から非公開データを除外するために、FilterExpressionを活用します。
// TypeScript: カテゴリ検索の実装例
const params: QueryCommandInput = {
TableName: this.tableName,
IndexName: 'GSI2-CategorySearch',
KeyConditionExpression: 'GSI2PK = :pk',
// PRIVATE写真をフィルタ(検索APIは常にpublic写真のみ)
FilterExpression: '#data.#privacy = :public',
ExpressionAttributeNames: {
'#data': 'Data',
'#privacy': 'privacy',
},
ExpressionAttributeValues: {
':pk': `CATEGORY#${categoryId}`,
':public': 'public',
},
Limit: query.limit ?? 20,
ScanIndexForward: false, // 新しい順
};
重要:二重防御の実装
セキュリティ上重要なフィルタリングは、FilterExpressionとアプリケーション層の両方で実装することを推奨します。
// TypeScript: DynamoDBクエリ実行後のアプリケーション層フィルタ
const items = (result.Items ?? [])
.filter((item) => {
const data = item.Data as { privacy?: string };
const isPublic = data?.privacy === 'public';
return item.Type === 'PhotoInfo' && isPublic;
})
.map((item) => this.transformToSearchResult(item));
GraphQLとの統合
AWS AppSyncを使用してGraphQL APIを提供する場合、入力値の検証とマッピングが重要です。
# GraphQL Schema
input PhotoSearchQuery {
keyword: String
category_ids: [Int!]
tags: [String!]
license_ids: [Int!]
safe_for_work: Boolean
privacy: Privacy # ← 悪意のある入力に注意
min_date: String
max_date: String
sort: PhotoSortOption
limit: Int
next_key: String
}
// TypeScript: Resolver実装 - セキュリティ対策
const searchQuery: SearchQuery = {
q: query.keyword,
category: query.category_ids?.[0]?.toString(),
filters: {
// safe_for_work の反転マッピング
includeNsfw: query.safe_for_work !== undefined
? !query.safe_for_work
: undefined,
// 重要: privacy引数に関わらず常にpublicOnly=true
publicOnly: true,
},
};
パフォーマンス考慮事項
1. FilterExpressionの制限
FilterExpressionはクエリ後にフィルタリングを行うため、読み取りキャパシティは消費されます。大量のデータがフィルタで除外される場合は、GSI設計を見直すことを検討してください。
2. バッチ取得の最適化
// TypeScript: 並列クエリでバッチ取得
const BATCH_SIZE = 25;
for (let i = 0; i < photoIds.length; i += BATCH_SIZE) {
const batch = photoIds.slice(i, i + BATCH_SIZE);
const queries = batch.map((id) =>
this.docClient.send(new QueryCommand({
TableName: this.tableName,
KeyConditionExpression: 'PK = :pk AND SK = :sk',
ExpressionAttributeValues: {
':pk': `PHOTO#${id}`,
':sk': 'INFO',
},
}))
);
const results = await Promise.all(queries);
// 結果を処理...
}
実装のポイントまとめ
- GSI設計を事前に検討:検索パターンに応じた適切なGSI設計が鍵
- 検索インデックスの事前構築:データ登録時にキーワードインデックスを作成
- FilterExpressionの活用:クエリ時にセキュリティフィルタリング
- 二重防御:DB層とアプリ層の両方でフィルタリング
- 入力値の検証:GraphQL引数の悪意ある入力を無視する実装
この方法が向いているケース
- 検索パターンが限定的(カテゴリ、タグ、日付範囲など)
- 曖昧検索や自然言語検索が不要
- データ量が中規模(数十万件程度まで)
- コスト重視のプロジェクト
OpenSearchを検討すべきケース
- 曖昧検索(typo許容)が必要
- 自然言語処理が必要
- 複雑なアグリゲーション(集計)が必要
- データ量が大規模(数百万件以上)
参考リンク
- Amazon DynamoDB グローバルセカンダリインデックス(公式)
- DynamoDB FilterExpression(公式)
- DynamoDB インデックスのベストプラクティス(公式)
- DynamoDB シングルテーブル設計とGraphQL(AWS Blog)
- AppSync DynamoDB リゾルバー(公式)
まとめ
DynamoDB単体でも、適切なGSI設計とFilterExpressionの活用により、実用的な検索機能を実現できます。OpenSearchの導入は強力ですが、コストとのトレードオフを考慮し、プロジェクトの要件に合った選択をしましょう。
本記事で紹介した実装パターンは、実際にフィギュア写真共有サービス「Figure Fan」のバックエンド開発で使用しているものです。月額コストを抑えながら、セキュリティを確保した検索機能を提供しています。
コメント