ベクトルは意味、BM25 はキーワード、RRF は調整不要。3 つを同一 PostgreSQL に収める。
| 選択肢 | 利点 | 欠点 | 採用 |
|---|---|---|---|
| pgvector | 同一 Postgres、txn、運用簡易 | 超大規模で性能劣り | ✅ |
| Pinecone | マネージド | 追加費用、ベンダーロック | ❌ |
| Qdrant | OSS、Rust 高速 | 別サービス | ❌ |
| Milvus | 大規模 | K8s 重い | ❌ |
| Weaviate | GraphQL | コミュニティ小 | ❌ |
決め手:SPOF 削減と運用シンプル。pgvector は atomic txn、統一バックアップ、RLS 共有、JOIN 便利。1 億超ベクトルで 30% 性能劣るが、テナント規模で無関係。
function chunkByFixedWindow(text, size = 500, overlap = 80) {
const chunks = [];
for (let i = 0; i < text.length; i += size - overlap) {
chunks.push(text.slice(i, i + size));
}
return chunks;
}
見出しで分割し title_hierarchy を chunk metadata に保存。
長編論文、法規文に LLM で境界判定。
CREATE TABLE chunks (
id UUID PRIMARY KEY, tenant_id UUID, document_id UUID,
position INT, content TEXT, title_path TEXT[],
token_count INT,
fts tsvector GENERATED ALWAYS AS (to_tsvector('zh_parser', content)) STORED,
meta JSONB, created_at TIMESTAMPTZ DEFAULT now()
);
CREATE INDEX idx_chunks_fts ON chunks USING GIN(fts);
| モデル | 次元 | USD/1M | 用途 |
|---|---|---|---|
| OpenAI 3-small | 1536 | 0.02 | デフォルト |
| OpenAI 3-large | 3072 | 0.13 | 高精度 |
| BGE-M3 | 1024 | 電気代 | 日本語、多言語 |
次元増で精度 +3%、コスト倍。コレクション内でのモデル混用不可。
CREATE INDEX idx_embeddings_hnsw ON embeddings
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
クエリ時 ef_search 動的調整:低 QPS で 100、高 QPS で 20。
SELECT chunk_id, ts_rank_cd(fts, q, 32) AS score
FROM chunks, plainto_tsquery('zh_parser', $1) q
WHERE tenant_id = $2 AND fts @@ q
ORDER BY score DESC LIMIT 20;
中国語 zh_parser、日本語 mecab、英語 english。多言語テナントは chunks.meta.lang で切替。
function rrfFusion(vec, bm25, k = 60) {
const scores = new Map();
for (const r of vec) scores.set(r.chunk_id, (scores.get(r.chunk_id) ?? 0) + 1/(k + r.rank));
for (const r of bm25) scores.set(r.chunk_id, (scores.get(r.chunk_id) ?? 0) + 1/(k + r.rank));
return [...scores].map(([id, s]) => ({id, s})).sort((a,b) => b.s - a.s);
}
加重平均より RRF:調整最小、スケール非依存、TREC 2009 実証、外れ値にロバスト。
実測:純ベクトルより Recall@10 +8–12%、2 週調整の加重平均より +3%。
Rerank(Cohere / bge-reranker-v2)で Recall@5 +5–10%、+250 ms。デフォルト OFF、高精度テナント(法務、医療、規制)のみ ON。
if (tenant.config.rerank_enabled) {
const reranked = await cohereRerank({
query: question, documents: fused.slice(0, 50).map(r => r.content), top_n: 10,
});
return reranked;
}
ef_search をクエリ時に動的調整