🔎 AI幻覚を解決する:ハイブリッド検索 (Hybrid Search) 実践

前章では、ドキュメントのチャンキング (Chunking)、ベクトル変換 (Embeddings)、そしてベクトルデータベースを使った「意味検索 (Vector/Semantic Search)」を学びました。

最初はこの技術に驚いたはずです! 「アップル社の最新決算」と検索すると、「Apple 2023 Q4 収益」のドキュメントが見つかります。AIが「アップル社」と「Apple」の意味的類似性を理解しているからです。

しかし、このシステムを実際の顧客に提供すると災難が訪れます


1. ベクトル検索 (Vector Search) の致命的弱点

純粋な意味検索が苦手とするものがあります:**「固有名詞」や「製品型番」**です。

例:データベースに以下の2つのドキュメントがある場合

  • ドキュメント A:製品型番 XG-999-Pro は高性能ゲーミングノートPCで、32GBメモリを搭載...
  • ドキュメント B:製品型番 XG-998-Lite は薄型軽量のビジネスノートPC...

ユーザーが「XG-999-Pro 仕様」と検索した時、どちらがヒットするでしょうか? 答え:両方とも類似と判定される可能性が高い! 場合によってはドキュメントBの方が高スコアになることも!

なぜか?AIの頭(Embedding空間)では、XG-999-ProXG-998-Liteは「見慣れない英数字の組み合わせ」であり、意味的に非常に近いと判断されるからです。AIはProの有無が重要な違いだとは理解できません。

この場合、古くても正確な**「キーワード検索 (Keyword Search / BM25)」**が最も有効です。文字列が一致しなければスコアは0になります!


2. ハイブリッド検索 (Hybrid Search) とは?

意味検索が「意味」を理解し、キーワード検索が「文字列」を理解するなら、両者を組み合わせれば良いのではないか?

ハイブリッド検索 (Hybrid Search) のコンセプトはシンプルです:

  1. ユーザーのクエリに対して、まず「意味検索」を実行し、Top 10のリストを取得(スコア例:0.8)
  2. 同時に、従来の「キーワード検索 (BM25)」も実行し、別のTop 10リストを取得(スコア例:0.9)
  3. 両リストを**RRF (Reciprocal Rank Fusion, 逆順位融合)**アルゴリズムに渡す
  4. RRFが両方のスコアを重み付け計算し、最終的な「最強のTop 5リスト」を生成

これは現在、業界の最先端RAGシステム(Pinecone、Supabase Vectorなど)の標準的な手法です。


3. Supabaseでのハイブリッド検索実装

Supabaseをベクトルデータベースとして使用している場合、ハイブリッド検索の実装は簡単です。PostgreSQLは全文検索 (Full-Text Search) をネイティブでサポートしています。

手順1:キーワード検索インデックスの作成

Supabase SQLエディタで、ドキュメントテーブルに全文検索用のカラムを追加:

-- documentsテーブルにfts(全文検索)カラムを追加
alter table documents add column fts tsvector generated always as (to_tsvector('english', content)) stored;

-- 検索用インデックスを作成
create index on documents using gin (fts);

手順2:ハイブリッド検索用ストアドプロシージャの作成

Supabase内で両方の検索を実行し、スコアを統合するRPC (Remote Procedure Call) 関数:

create or replace function match_documents_hybrid(
  query_embedding vector(1536), -- OpenAIのベクトル
  query_text text,              -- 元のキーワード
  match_count int,              -- 取得件数
  full_text_weight float default 1,  -- キーワード検索の重み
  semantic_weight float default 1    -- 意味検索の重み
)
returns table (
  id uuid,
  content text,
  similarity float
)
language plpgsql
as $$
begin
  return query
  with semantic_search as (
    -- 1. 意味検索 (Cosine Similarity)
    select documents.id, documents.content, 1 - (documents.embedding <=> query_embedding) as semantic_score
    from documents
    order by documents.embedding <=> query_embedding
    limit match_count * 2
  ),
  keyword_search as (
    -- 2. キーワード検索 (Full Text Search)
    select documents.id, documents.content, ts_rank(documents.fts, websearch_to_tsquery('english', query_text)) as keyword_score
    from documents
    where documents.fts @@ websearch_to_tsquery('english', query_text)
    order by keyword_score desc
    limit match_count * 2
  )
  -- 3. スコア統合 (簡易加重法)
  select 
    coalesce(semantic_search.id, keyword_search.id) as id,
    coalesce(semantic_search.content, keyword_search.content) as content,
    -- 両方のスコアを加算 (正規化が必要な場合あり、ここでは簡略化)
    (coalesce(semantic_search.semantic_score, 0.0) * semantic_weight + 
     coalesce(keyword_search.keyword_score, 0.0) * full_text_weight) as similarity
  from semantic_search
  full outer join keyword_search on semantic_search.id = keyword_search.id
  order by similarity desc
  limit match_count;
end;
$$;

手順3:LangChainやNext.jsからの呼び出し

ユーザーの検索クエリを受け取ったら、このRPCを呼び出します:

import { createClient } from '@supabase/supabase-js';
import { OpenAIEmbeddings } from 'langchain/embeddings/openai';

const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_ROLE_KEY!);
const embeddings = new OpenAIEmbeddings();

async function performHybridSearch(question: string) {
  // 1. 質問をベクトル化
  const queryEmbedding = await embeddings.embedQuery(question);

  // 2. ハイブリッド検索RPCを呼び出し
  const { data, error } = await supabase.rpc('match_documents_hybrid', {
    query_embedding: queryEmbedding, // 意味検索用
    query_text: question,            // キーワード検索用
    match_count: 5,                  // トップ5を取得
    full_text_weight: 1.2,           // キーワード検索の重みを高く設定
    semantic_weight: 1.0
  });

  if (error) {
    console.error("検索失敗:", error);
    return [];
  }

  return data;
}

4. 上級テクニック:Cohere Rerank

SQLでのスコア統合が面倒、または精度不足と感じる場合、業界では**Rerank (再ランキング)**という手法が使われています。

手順:

  1. 通常のベクトル検索で上位20件のドキュメントを取得
  2. これらのドキュメントとユーザーの質問を、スコアリング専門のAIモデル(例:Cohere Rerank)に送信
  3. AIが20件のドキュメントを読み、「質問に実際に答えられるか」で再ランキング
  4. 上位3件をGPTに渡して最終回答を生成

LangChainでCohere Rerankを統合する例:

import { CohereRerank } from "@langchain/cohere";
import { ContextualCompressionRetriever } from "langchain/retrievers/contextual_compression";

// 1. ベースのベクトル検索器を設定(例:pineconeやsupabase)
const baseRetriever = vectorStore.asRetriever(20); // 20件取得

// 2. Cohereの圧縮/再ランキング器を設定
const compressor = new CohereRerank({
  apiKey: process.env.COHERE_API_KEY,
  model: "rerank-multilingual-v2.0", // 日本語対応!
  topN: 3 // 再ランキング後はトップ3のみ
});

// 3. 最強の検索器に組み立て
const hybridRetriever = new ContextualCompressionRetriever({
  baseCompressor: compressor,
  baseRetriever: baseRetriever,
});

// 4. 検索実行!
const docs = await hybridRetriever.getRelevantDocuments("XG-999-Pro の冷却仕様は?");

Hybrid SearchRerankを導入すると、AIの回答精度が70%から95%以上に急上昇します。 「明らかな型番を間違える」といった顧客の不満はなくなります。これが「おもちゃ」と「商用製品」を分ける核心技術です。

完全なチュートリアルをロック解除

このチャプターは有料コンテンツです。プロジェクトに参加して、10以上の神レベルのPromptや実際のソースコード例を含む、5000字以上の深い分析をロック解除してください!