検索サービスアーキテクチャ: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にキャッシュ(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: ライブクエリの取得にフィードバックされます。

参考文献

技術的概念

関連記事

まとめ

検索サービスは別サーバー上のスタンドアロンFlaskアプリケーションとして実行されます:

アーキテクチャ:

  • 専用サーバー上のスタンドアロンFlaskアプリ

  • キャッシングとベクトル検索のためのValkey(Redisフォーク)

  • メインWebサーバーとの通信のためのHTTP API

API:

  • /api/extract_filters - クエリからフィルターを抽出

  • /api/related - 類似クエリを検索(ベクトル検索)

  • /api/autocomplete - クエリを提案(プレフィックスマッチング)

  • /api/popular - 人気クエリを取得

Valkey機能:

  • ベクトル検索(RediSearchモジュール)

  • キャッシング(フレーズマッピングは30日TTL)

  • オートコンプリート(ソート済みセット)

  • 永続性(AOF)

利点:

  • 独立したスケーリング

  • リソース分離

  • 高性能(Valkeyキャッシング)

  • フォールトトレランス(優雅な機能低下)

このアーキテクチャにより、メインWebサーバーの応答性を維持しながら、高速でスケーラブルな検索が可能になります。


← ドキュメントインデックスに戻る