ベクトル検索とAIを組み合わせた自然言語検索機能の実装

image

この記事は、【 可茂IT塾 Advent Calendar 2025 】の5日目の記事です。

はじめに

個人開発で地図でお店を検索するようなアプリを作っています。アプリからお店を探すときに、自然言語で対話式に探せたら面白いなと思い、実験的な機能として導入してみました。

従来のキーワード検索とは異なり、「〇〇地域で〇〇系のお店を探したい」「現在地から3km以内で〇〇向けの店」といった自然な表現で検索できるように、ベクトル検索とAIを組み合わせた自然言語検索システムを実装しました。

結論としてはまだまだ精度が物足りず、今後改善していきたいですが、形にはなったので、実装内容をまとめます。

技術スタック

  • Supabase: PostgreSQLデータベースとpgvector拡張機能を使用
  • OpenAI:
    • text-embedding-3-small モデルで埋め込みベクトルを生成
    • gpt-4o-mini でクエリ最適化や結果要約を実装
  • Firebase Functions: サーバーレス関数として実装
  • TypeScript: 型安全性を確保

実装の全体像

システムアーキテクチャ

システムは以下の3つの主要な処理フローで構成されています。

  1. ベクトル検索: 自然言語クエリをベクトルに変換して類似度検索
  2. クエリ最適化: 位置情報を考慮して検索クエリを最適化
  3. 結果の要約: 検索結果をAIで要約し、ユーザーに分かりやすく提示

ベクトル検索の実装

埋め込みベクトルの生成

店舗データから検索用のテキストを作成し、OpenAIの埋め込みモデルでベクトル化します。

export function createShopContent(shop: Shop, masterData: MasterData): string {
  const parts: string[] = []
  
  // 店名、住所、スタイル、特徴などを結合
  if (shop.name) parts.push(`店名: ${shop.name}`)
  if (shop.address) parts.push(`住所: ${shop.address}`)
  if (styles.length > 0) parts.push(`スタイル: ${styles.join(', ')}`)
  // ... 他の属性も追加
  
  return parts.join('\n')
}

生成したテキストをOpenAI APIでベクトル化し、Supabaseに保存します。

Supabaseでのベクトル検索

pgvector拡張機能を使い、コサイン類似度で検索します。

CREATE INDEX shop_embeddings_embedding_idx
ON shop_embeddings
USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);

検索時は、ユーザーのクエリも同様にベクトル化し、類似度の高い店舗を取得します。さらに、都道府県フィルタや位置情報による距離ソートも実装しました。

export async function searchShopsByVector(
  query: string,
  limit = 5,
  threshold = 0.5,
  prefIdFilter: string | null = null,
  userLocation: { latitude: number; longitude: number } | null = null
): Promise<ShopSearchResult[]> {
  const queryEmbedding = await generateEmbedding(query)
  
  // 位置情報がある場合は距離ソート検索
  if (userLocation) {
    const result = await supabase.rpc('search_shops_by_similarity_with_distance', {
      query_embedding: queryEmbedding,
      user_latitude: userLocation.latitude,
      user_longitude: userLocation.longitude,
      pref_id_filter: prefIdFilter,
      match_threshold: threshold,
      match_count: limit,
    })
    return result.data
  }
  
  // 通常のベクトル検索
  // ...
}

クエリ最適化の実装

ユーザーの自然な表現を、検索に適したクエリに変換します。位置情報も考慮します。

位置情報の抽出

クエリから地名を抽出し、座標を推定する機能も実装しました。AIが地名を認識し、適切な検索半径も自動判定します。

export async function extractLocationFromQuery(
  query: string
): Promise<ExtractedLocation> {
  // GPT-4o-miniで地名と座標を抽出
  const response = await openai.chat.completions.create({
    model: 'gpt-4o-mini',
    messages: [
      {
        role: 'system',
        content: 'クエリから地名と座標を抽出してください...'
      },
    ],
  })
  
  // 駅名→1km、商店街→2km、区→5kmなど、地域に応じて検索半径を決定
  return {
    locationName: result.locationName,
    latitude: result.latitude,
    longitude: result.longitude,
    searchRadiusKm: result.searchRadiusKm,
  }
}

検索結果の要約

ベクトル検索で取得した候補(最大100件)を、AIが2〜3件に絞り込み、自然な文章で要約します。

export async function summarizeWithContext(
  userMessage: string,
  searchResults: ShopSearchResult[]
): Promise<Summary> {
  // 検索結果をテキスト形式に変換
  const resultsText = searchResults
    .slice(0, 50)
    .map((result, index) => {
      return `${index + 1}. ID: ${result.id}, 店舗名: ${result.shopName}
内容: ${result.content}
類似度: ${Math.round(result.similarity * 100)}%`
    })
    .join('\n\n')
  
  // GPT-4o-miniで結果を要約
  const response = await openai.chat.completions.create({
    model: 'gpt-4o-mini',
    messages: [
      {
        role: 'system',
        content: '検索結果を要約し、ユーザーに分かりやすく提示してください...'
      },
      {
        role: 'user',
        content: `ユーザーメッセージ: "${userMessage}"\n\n検索結果:\n${resultsText}`
      },
    ],
    response_format: { type: 'json_object' },
  })
  
  return {
    summary: parsed.summary,
    selectedShopIds: parsed.selectedShopIds,
  }
}

AIが選択した店舗のみFirestoreから詳細データを取得することで、コストを最適化しました。

解決した課題

1. 自然言語での検索

「〇〇地域で〇〇系のお店」という自然な表現でも、ベクトル検索により意味的に近い店舗を検索できるようになりました。キーワードマッチングでは難しかった、表現のバリエーションにも対応できます。

2. 位置情報を考慮した検索

「現在地から3km以内」や「〇〇周辺」という検索にも対応。クエリから地名を抽出し、適切な検索半径でフィルタリングします。距離によるソートも実装し、ユーザーの位置に近い店舗を優先表示できます。

3. 検索精度の向上

単純なベクトル検索だけでなく、以下の最適化を実装しました:

  • 地域名のキーワードマッチでスコアをブースト
  • 類似度と距離の両方を考慮したソート
  • AIによる結果の絞り込み(100件→2〜3件)

4. コストの最適化

  • 検索候補からAIが選んだ店舗のみ詳細データを取得
  • 事前生成されたウェルカムメッセージを使用(リアルタイム生成を回避)

技術的な学び

pgvectorの活用

Supabaseのpgvector拡張機能により、PostgreSQL上でベクトル検索が可能になりました。ivfflatインデックスを使うことで、大量のデータでも高速な検索を実現できます。

ハイブリッド検索

ベクトル検索に加えて、都道府県フィルタや距離ソートを組み合わせることで、より精度の高い検索が可能になりました。SQL関数として実装することで、データベース側で効率的に処理できます。

段階的なデータ取得

最初にベクトル検索でメタデータのみ取得し、AIが絞り込んだ後に詳細データを取得する方式により、Firestoreの読み取りコストを削減できました。

まとめ

ベクトル検索とAIを組み合わせることで、自然言語での検索が実現できました。ユーザーは「〇〇地域で〇〇系のお店を探したい」という自然な表現で検索でき、システムが位置情報を理解して結果を返す形になっています。

まだまだ精度が物足りない部分もあり、検索結果が期待通りでないケースもありますが、実験的な機能としては形になったと思います。今後の改善点として、検索精度の向上や、検索結果のフィードバックを学習に活用したり、より高度なパーソナライズ機能を追加したりすることも検討しています。

お知らせ

可茂IT塾ではFlutter/Reactのインターンを募集しています!(募集停止中)

可茂IT塾ではFlutter/Reactのインターンを募集しています!(募集停止中)

可茂IT塾ではFlutter/Reactのインターンを募集しています!可茂IT塾のエンジニアの判断で、一定以上のスキルをを習得した方には有給でのインターンも受け入れています。

Read More
U30可茂ITインターンハッカソン

U30可茂ITインターンハッカソン

12月28,29日開催。2日間でアプリ開発の企画から完成までを目指す!U30可茂ITインターンハッカソンを開催します。

Read More

タグ

Flutter (124)初心者向け (32)イベント (19)Google Apps Script (17)可茂IT塾 (12)Nextjs (12)AI (8)React (8)riverpod (7)デザイン (7)Firebase (7)Figma (6)VSCode (6)JavaScript (6)ChatGPT (5)新卒 (4)就活 (4)Slack (4)Dart (4)TypeScript (4)お知らせ (4)FlutterWeb (3)Prisma (3)NestJS (3)アプリ開発 (3)ワーケーション (3)インターン (3)Web (2)Obsidian (2)設計 (2)線型計画法 (2)事例 (2)Git (2)CSS (2)Freezed (2)Image (2)File (2)Material Design (2)経験談 (2)画像 (2)iOS (2)React Hooks (2)tailwindcss (2)社会人 (2)大学生 (2)RSS (1)Google (1)CodeRunner (1)NotebookLM (1)個人開発 (1)Android (1)Unity (1)WebView (1)Twitter (1)フルリモート (1)TextScaler (1)textScaleFactor (1)学生向け (1)supabase (1)Java (1)Spring Boot (1)shell script (1)正規表現 (1)table (1)テーブル (1)hooks (1)パワーポイント (1)ブックマーク (1)Pocket (1)ブクマク (1)MCPサーバー (1)OpenAI (1)Supabase (1)ベクトル検索 (1)趣味 (1)モンスターボール (1)SCSS (1)Swift (1)MapBox (1)Cupertino (1)gpt-oss (1)生成AI (1)llama.cpp (1)LLM (1)ListView (1)就活浪人 (1)既卒 (1)保守性 (1)iPad (1)シェアハウス (1)スクレイピング (1)PageView (1)画面遷移 (1)flutter_hooks (1)Gmail (1)GoogleWorkspace (1)ShaderMask (1)google map (1)Google Places API (1)GCPコンソール (1)Google_ML_Kit (1)Vercel (1)Google Domains (1)DeepLeaning (1)深層学習 (1)Google Colab (1)Firebase Analytics (1)Gemini AI (1)コード生成 (1)GitHub Copilot (1)GitHub Actions (1)gemini (1)オンラインオフィス (1)html (1)オブジェクト指向 (1)クラスの継承 (1)ポリモーフィズム (1)LINE Messaging API (1)LINE Notify (1)LINE (1)Bitcoin (1)bitFlyer (1)コミュニティー (1)文系エンジニア (1)build_runner (1)ヒーター (1)作業効率 (1) (1)Flutter実践開発 (1) (1)permission_handler (1)flutter_local_notifications (1)markdown (1)GlobalKey (1)ValueKey (1)Key (1)アイコン (1)go_router (1)FireStorage (1)debug (1)datetime_picker (1)Apple Store Connect (1)FlutterGen (1)デバッグ (1)Widget Inspector (1)VRChat (1)API (1)検索機能 (1)ローディング (1)Skeletonizer (1)Simmer (1)Shader (1)SharedPreferences (1)オフラインサポート (1)Navigator (1)メール送信 (1)FlutterFlow (1)Firebase App Distribution (1)Fastlane (1)Dio (1)CustomClipper (1)ClipPath (1)video_player (1)IMA (1)カスタム認証 (1)アニメーション (1)Arduino (1)ESP32 (1)フリーランス (1)会社員 (1)mac (1)csv (1)docker (1)GithubActions (1)Dialog (1)BI (1)sora2 (1)Gemini CLI (1)Claude Code (1)LifeHack (1)ショートカット (1)Chrome (1)高校生 (1)キャリア教育 (1)非同期処理 (1)生体認証 (1)BackdropFilter (1)レビュー (1)Antigravity (1)getAuth (1)クローズドテスト (1)PlayConsole (1)Algolia (1)コンサルティング (1)Symbol (1)

お知らせ

可茂IT塾ではFlutter/Reactのインターンを募集しています!(募集停止中)

可茂IT塾ではFlutter/Reactのインターンを募集しています!(募集停止中)

可茂IT塾ではFlutter/Reactのインターンを募集しています!可茂IT塾のエンジニアの判断で、一定以上のスキルをを習得した方には有給でのインターンも受け入れています。

Read More
U30可茂ITインターンハッカソン

U30可茂ITインターンハッカソン

12月28,29日開催。2日間でアプリ開発の企画から完成までを目指す!U30可茂ITインターンハッカソンを開催します。

Read More