0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

空白のない日本語を RAG で検索可能にする:PGroonga TokenBigram、pgvector、RRF の設計と運用

0
Last updated at Posted at 2026-09-07

「検品報告書 QC-2031 はどこにありますか」。この質問に、稼働中のシステムは4秒以内に、該当ページの原文を添えて答えます。1年前は、担当者に聞くほうが早い状態でした。

日本語とタイ語が混在する製造業の社内資料を対象に、出典付きで答える検索基盤を作って運用しています。拠点は3か国、対象人数はおよそ5,000人。この記事では、その中で最初に決めて最後まで効いた設計判断を一つだけ書きます。翻訳してから検索するのをやめ、言語ごとに索引を作って横断する、その実装です。

PostgreSQL で RAG を組んでいて、日本語の全文検索が「なんとなく当たらない」と感じている方に向けて書いています。

1. 何が問題か

英語向けに作られた全文検索は、空白で単語を切ります。日本語とタイ語には、その空白がありません。

ここで多くのプロジェクトが取る回避策が二つあります。

回避策 A:ベクトル検索だけで済ませる。 意味の近さは拾えますが、型番や社内用語のような「意味を持たない文字列」に弱い。「検品報告書 QC-2031」を探しているのに「品質関連の報告書」が上位に並びます。

回避策 B:全部英語に翻訳してから索引化する。 翻訳の過程で型番と社内の呼び方が落ちます。しかも回答の根拠として示す原文が、利用者の読めない言語になります。

両方を捨てました。原文のまま、言語を意識した索引を作ります。

2. 全体の構成

質問(日本語 or タイ語)
  │
  ▼
Query Planner(ドメイン語彙・型番フィルターを含む検索プランに分解)
  │
  ├──▶ 全文検索     PGroonga TokenBigram   ← この記事の主題
  ├──▶ ベクトル検索  pgvector(HNSW, cosine)
  └──▶ 知識グラフ   LightRAG
  │
  ▼
Reciprocal Rank Fusion(順位ベースの統合)
  │
  ▼
Rerank(Cohere Rerank v3.5)
  │
  ▼
回答生成(出典付き。該当がなければ「ない」と答える)

コアは3系統です。実案件ではここに表データ検索、メール検索、エンティティ検索、カンバン検索を足して最大7系統を並列に走らせていますが、骨格は上の3本で決まります。

3. PGroonga TokenBigram:重なり合う2文字で索引を作る

PGroonga は PostgreSQL の拡張で、Groonga の全文検索エンジンをインデックスとして使えます。日本語では、トークナイザーの選択でほぼ決まります。

  • 形態素解析(MeCab 系):辞書に基づいて単語に切る。文章としての精度は高いが、辞書にない語(新製品の型番、社内略語、タイ語からの借用語、取引先の略称)が切れない。
  • TokenBigram:文字を2文字ずつ重ねて切る。「品質管理」は「品質」「質管」「管理」になる。辞書が要らず、未知語に強い。再現率が高い。

社内資料は未知語の塊です。TokenBigram を選びました。

CREATE EXTENSION IF NOT EXISTS pgroonga;
CREATE EXTENSION IF NOT EXISTS vector;

CREATE TABLE chunks (
  id         bigserial PRIMARY KEY,
  doc_id     bigint      NOT NULL,
  lang       text        NOT NULL,   -- 'ja' | 'th'
  content    text        NOT NULL,   -- 原文のまま。翻訳しない
  embedding  vector(1536)
);

-- 全文検索:2文字の重なりで索引化。NFKC で全角半角・カナの揺れを吸収
CREATE INDEX chunks_content_pgroonga_idx ON chunks
  USING pgroonga (content)
  WITH (tokenizer   = 'TokenBigram',
        normalizers = 'NormalizerNFKC150');

-- ベクトル検索
CREATE INDEX chunks_embedding_hnsw_idx ON chunks
  USING hnsw (embedding vector_cosine_ops);

検索は &@~ 演算子でクエリ構文を使います。空白区切りは AND です。

SELECT id, pgroonga_score(tableoid, ctid) AS score
FROM   chunks
WHERE  content &@~ '海外工場 検品 報告'
ORDER  BY score DESC
LIMIT  20;

タイ語も同じ索引で動く

TokenBigram は文字種で分割するため、空白のないタイ語にもそのまま効きます。言語ごとに別のトークナイザーを用意していません。lang 列は、回答をどの言語で組み立てるかを決めるためだけにあります。

型番で一度つまずいた

既定の TokenBigram は、連続する英字や数字をまとめて一つのトークンにします。QC-2031 は「QC」「-」「2031」に分かれるので、2031 では当たりますが、203 のような数字の一部では当たりません。型番の部分一致が業務上必要なら、英数字も2文字ずつに切る TokenBigramSplitSymbolAlphaDigit を検討してください。

私たちは別の道を取りました。Query Planner 側で型番を正規表現で抜き出し、専用列への完全一致フィルターに回しています。索引を細かくするより、質問を分解するほうが速かった。それだけの理由です。

4. pgvector 側は素直に

ベクトル検索に凝った工夫はしていません。OpenAI Embeddings で vector(1536)、HNSW、cosine 距離。

SELECT id, 1 - (embedding <=> $1) AS similarity
FROM   chunks
ORDER  BY embedding <=> $1
LIMIT  20;

意味の近さはこちらが拾い、文字列の一致は PGroonga が拾う。役割を分けたので、それぞれ単純でいられます。

5. Reciprocal Rank Fusion:順位で統合する

全文検索のスコアとベクトル検索の類似度は尺度が違います。正規化して足すのは危ういので、順位だけを使って統合します。

WITH ft AS (
  SELECT id,
         row_number() OVER (ORDER BY pgroonga_score(tableoid, ctid) DESC) AS r
  FROM   chunks
  WHERE  content &@~ $query
  LIMIT  50
),
vec AS (
  SELECT id,
         row_number() OVER (ORDER BY embedding <=> $embedding) AS r
  FROM   chunks
  ORDER  BY embedding <=> $embedding
  LIMIT  50
)
SELECT   id, SUM(1.0 / (60 + r)) AS rrf_score
FROM     (SELECT * FROM ft UNION ALL SELECT * FROM vec) AS u
GROUP BY id
ORDER BY rrf_score DESC
LIMIT    20;

定数 60 は Cormack、Clarke、Büttcher の原論文(SIGIR 2009)の値をそのまま使っています。調整で得られた改善は誤差の範囲でした。

この上位20件を Cohere Rerank v3.5 に渡し、最終順位を決めます。両方の検索で上位に来た文書が自然に押し上げられるので、Rerank に渡す候補の質が安定します。

6. 段階的な応答:画面は一番遅い処理を待たない

すべての質問に同じ深さで答えると、単純な質問まで遅くなります。Query Planner が質問を分類し、段階を分けています。

段階 対象 稼働中の実測
Flash / Standard 単一資料で答えられる質問 4秒未満
Deep 複数資料をまたぐ分析 最長およそ35秒

Deep の間は途中経過を返します。回答生成は Claude で、段階に応じて Haiku 4.5 と Sonnet 4.6 を使い分けています。

7. 稼働してからが本番

ナレッジ検索は資料とともに古びます。手を入れなければ、1年で目に見えて当たらなくなります。

そこで、実際の利用者と同じ経路で8カテゴリ・100シナリオの自動テストを、稼働中のパイプラインに対して定期的に流しています。現在の総合スコアは 1.0 中 0.9。現場から苦情が来る前に劣化を見つけるためのものです。

採点するのは次の3点です。「正解の文章」との一致は見ません。

  1. 出典が正しい文書を指しているか
  2. 該当なしのときに「ない」と言えているか
  3. 型番・数値が原文と一致しているか

利用者が回答を訂正したら、その内容を Learned Facts として抽出し、以後の回答生成に渡しています。

8. 振り返って残ったこと

翻訳を挟まない。原文のまま索引を作り、問い合わせの言語で答え、根拠は原文で示す。これで型番と社内用語の問題は消えました。

トークナイザーは未知語で選ぶ。社内資料は辞書にない語の塊で、TokenBigram の再現率の高さは Rerank と組むと欠点にならなくなります。

索引を賢くするより、質問を分解する。型番や日付のように構造化できる条件は Query Planner で抜き出してフィルターに回し、全文検索は本来の仕事だけさせる。

使っているものはすべて公開された技術です。pgvector、PGroonga、LightRAG、Cohere Rerank。誰も追えない独自方式はありません。

このシステムの事例は Kuroko Labs のサイトに公開しています。
https://kurokolabs.ai/ja/case-studies/rag-knowledge-agent

多言語ナレッジ検索の考え方そのものはこちらです。
https://kurokolabs.ai/ja/tagengo-knowledge


Kuroko Labs GmbH(ドイツ・ミュンヘン)。日本語とドイツ語で、製造業向けの AI エージェントと検索基盤を作っています。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?