検索サービスアーキテクチャ:Valkeyを使用したスタンドアロンFlaskアプリケーション
この記事では、検索サービスが別サーバー上のスタンドアロンFlaskアプリケーションとしてどのように実行されるか、高性能なベクトル検索、キャッシング、オートコンプリートのためにValkey(Redisフォーク)を使用する方法について説明します。
問題:検索パフォーマンスとスケーラビリティ
検索操作は計算コストが高いです:
-
フィルター抽出: 各クエリに対して2,500以上のフレーズをマッチング
-
関連検索: 65Kのクエリ間で類似性を計算
-
オートコンプリート: 65Kのクエリに対してプレフィックスマッチング
-
製品フィルタリング: 5K以上の製品を複数の基準でフィルタリング
これらの操作をメインWebサーバーで実行すると、以下の問題が発生します:
-
ページ読み込みの遅延: 検索が他のリクエストをブロック
-
メモリ負荷: 埋め込み表現が膨大なメモリフットプリントを占める
-
CPU競合: 類似性計算はCPU集約的
-
スケーリングの困難: 検索を独立してスケールできない
独立してスケールできる専用の検索サービスが必要です。
解決策:スタンドアロン検索サービス
専用サーバー上で別個のFlaskアプリケーションを実行します:
メインWebサーバー
↓ HTTP API呼び出し
検索サービス
↓ Valkeyクエリ
Valkeyサーバー
このアーキテクチャは以下を提供します:
-
独立したスケーリング: メインWebサーバーに影響を与えずに検索サービスをスケール
-
リソース分離: 検索操作がメインWebサーバーに影響を与えない
-
キャッシング: Valkeyが結果をキャッシュし、繰り返しクエリを高速化
-
高可用性: 検索サービスを再起動してもメインWebサーバーに影響なし
検索サービスのコンポーネント
1. フィルター抽出API
-
エンドポイント:
/api/extract_filters -
目的: 自然言語クエリから構造化されたフィルターを抽出
-
例:
GET /api/extract_filters?q=mini+pc+16gb+ram
レスポンス:
{
"Form Factor": "Mini PC",
"Main Memory": "16"
}
実装:
-
フレーズとフィルターのマッピングをValkeyまたはJSONから読み込み
-
単語境界正規表現を使用してフレーズをマッチング
-
構造化されたフィルターを返却
キャッシング: フレーズマッピングはValkeyにキャッシュ(30日TTL)
2. 関連検索API
-
エンドポイント:
/api/related -
目的: ベクトル検索を使用して意味的に類似したクエリを検索
-
例:
POST /api/related
{
"query": "mini pc",
"limit": 10
}
レスポンス:
{
"related": [
{"query": "small computer", "similarity": 0.92},
{"query": "compact desktop", "similarity": 0.89},
{"query": "mini pc 8gb", "similarity": 0.87}
]
}
実装:
-
all-mpnet-base-v2を使用してクエリを埋め込み
-
Valkey RediSearchにクエリを実行し、最近傍を検索
-
類似度でソートされた上位N件の結果を返却
キャッシング: 結果はValkeyにキャッシュ(7日TTL)
3. オートコンプリートAPI
-
エンドポイント:
/api/autocomplete -
目的: ユーザーの入力に基づいてクエリを提案
-
例:
GET /api/autocomplete?q=mini+p&limit=5
レスポンス:
{
"suggestions": [
"mini pc",
"mini pc 16gb",
"mini pc 8gb ram",
"mini pc fanless",
"mini pc windows 11"
]
}
実装:
-
プレフィックスマッチングでValkey RediSearchにクエリを実行
-
人気度(インプレッション+クリックスコア)でランキング
-
上位N件の提案を返却
キャッシング: オートコンプリートインデックスはValkeyに格納(毎日更新)
4. 人気クエリAPI
-
エンドポイント:
/api/popular -
目的: 最も人気のあるクエリを取得
-
例:
GET /api/popular?limit=10
レスポンス:
{
"queries": [
"mini pc",
"thin client",
"industrial pc",
"all in one pc"
]
}
実装:
-
ValkeyまたはJSONからクエリを読み込み
-
トラフィックスコア(インプレッション+クリック)でソート
-
上位N件のクエリを返却
キャッシング: 人気クエリはValkeyにキャッシュ(30日TTL)
Valkey統合
Valkeyは、以下を提供するRedisフォークです:
-
ベクトル検索: 類似性検索のためのRediSearchモジュール
-
キャッシング: 高速なインメモリキーバリューストア
-
オートコンプリート: ソート済みセットを使用したプレフィックスマッチング
-
永続性: 耐久性のためのAOF(Append-Only File)
RediSearchによるベクトル検索
ベクトル類似性検索にValkeyのRediSearchモジュールを使用します:
インデックス作成:
client.ft("queries_idx").create_index([
VectorField("embedding", "FLAT", {
"TYPE": "FLOAT32",
"DIM": 768,
"DISTANCE_METRIC": "COSINE"
}),
TextField("query"),
NumericField("score")
])
ベクトル検索:
query_embedding = model.encode(query)
results = client.ft("queries_idx").search(
Query("*=>[KNN 10 @embedding $vec AS score]")
.sort_by("score")
.return_fields("query", "score")
.dialect(2),
query_params={"vec": query_embedding.tobytes()}
)
これはコサイン類似度による10件の最近傍を返します。
キャッシング戦略
Valkeyに複数のデータタイプをキャッシュします:
フレーズマッピング(30日TTL):
client.setex(
"seo:phrase_mappings",
30 * 24 * 3600,
json.dumps(phrase_mappings)
)
関連検索(7日TTL):
cache_key = f"related:{query_hash}"
client.setex(cache_key, 7 * 24 * 3600, json.dumps(results))
人気クエリ(30日TTL):
client.setex(
"seo:popular_queries",
30 * 24 * 3600,
json.dumps(popular_queries)
)
オートコンプリートインデックス(毎日更新):
for query, score in queries:
client.zadd("autocomplete:mini", {query: score})
ソート済みセットによるオートコンプリート
オートコンプリートにValkeyのソート済みセットを使用します:
インデックス構造:
autocomplete:m → ["mini pc": 5000, "mini computer": 3000]
autocomplete:mi → ["mini pc": 5000, "mini computer": 3000]
autocomplete:min → ["mini pc": 5000, "mini computer": 3000]
autocomplete:mini → ["mini pc": 5000, "mini computer": 3000]
プレフィックス検索:
prefix = "mini"
results = client.zrevrange(f"autocomplete:{prefix}", 0, 9, withscores=True)
これは「mini」で始まるクエリをスコアでソートして上位10件返します。
API通信
メインWebサーバーはHTTP経由で検索サービスを呼び出します:
フィルター抽出
from app.shared.filter_service import extract_filters_from_query
filters = extract_filters_from_query("mini pc 16gb ram")
# 内部的に呼び出し: GET SEARCH_SERVICE_URL/api/extract_filters?q=...
関連検索
import requests
response = requests.post(
"SEARCH_SERVICE_URL/api/related",
json={"query": "mini pc", "limit": 10},
timeout=2
)
related = response.json()["related"]
オートコンプリート
response = requests.get(
"SEARCH_SERVICE_URL/api/autocomplete",
params={"q": "mini p", "limit": 5},
timeout=1
)
suggestions = response.json()["suggestions"]
エラーハンドリングとフォールバック
メインWebサーバーは検索サービスの失敗を適切に処理します:
try:
filters = extract_filters_from_query(query)
except Exception as e:
logger.error(f"Search service failed: {e}")
filters = {} # 空のフィルターにフォールバック
これにより、検索サービスがダウンしていてもメインWebサーバーは機能し続けます。
ネットワーク設定
すべてのサーバーはプライベートネットワーク上にあります:
-
メインWebサーバー: 検索サービスとValkeyにアクセス可能
-
検索サービス: Valkeyにアクセス可能
-
Valkey: メインWebサーバーと検索サービスのみからアクセス可能
検索サービスやValkeyへの外部アクセスはありません。
SEOパイプラインとの統合
検索サービスはSEOパイプラインと統合されています:
ステップ11: Valkeyへの移行
SEOパイプラインはデータをValkeyにロードします:
# クエリ埋め込みをロード
for query, embedding in zip(queries, embeddings):
client.hset(f"query:{query_hash}", mapping={
"query": query,
"embedding": embedding.tobytes(),
"score": score
})
# RediSearchインデックスを作成
client.ft("queries_idx").create_index([...])
詳細はValkey移行を参照してください。
クエリロギング
検索サービスはSEOパイプラインのためにクエリをログに記録します:
log_entry = {
"timestamp": datetime.now(timezone.utc).isoformat(),
"query": query,
"filters_extracted": filters,
"results_count": len(results)
}
with open(SEO_LIVE_QUERIES_LOG, "a") as f:
f.write(json.dumps(log_entry) + "\n")
これらのログはステップ1d: ライブクエリの取得にフィードバックされます。
参考文献
技術的概念
-
Valkey - 公式ウェブサイト
-
Redis - 公式ウェブサイト(Valkeyフォーク)
-
RediSearch - ベクトル検索モジュール
-
コサイン類似度 - Wikipedia
関連記事
-
フィルター抽出 - フィルターの抽出方法
-
関連検索生成 - 関連検索の生成方法
-
埋め込み戦略 - 埋め込みの生成方法
-
SEOパイプライン概要 - 完全なパイプラインアーキテクチャ
-
Valkey移行 - Valkeyへのデータロード
まとめ
検索サービスは別サーバー上のスタンドアロンFlaskアプリケーションとして実行されます:
アーキテクチャ:
-
専用サーバー上のスタンドアロンFlaskアプリ
-
キャッシングとベクトル検索のためのValkey(Redisフォーク)
-
メインWebサーバーとの通信のためのHTTP API
API:
-
/api/extract_filters- クエリからフィルターを抽出 -
/api/related- 類似クエリを検索(ベクトル検索) -
/api/autocomplete- クエリを提案(プレフィックスマッチング) -
/api/popular- 人気クエリを取得
Valkey機能:
-
ベクトル検索(RediSearchモジュール)
-
キャッシング(フレーズマッピングは30日TTL)
-
オートコンプリート(ソート済みセット)
-
永続性(AOF)
利点:
-
独立したスケーリング
-
リソース分離
-
高性能(Valkeyキャッシング)
-
フォールトトレランス(優雅な機能低下)
このアーキテクチャにより、メインWebサーバーの応答性を維持しながら、高速でスケーラブルな検索が可能になります。