Timescaleが開発するPostgreSQL用拡張機能「pg_textsearch」がv1.0.0でリリースされ、プロダクション環境での利用が可能になった。BM25ランキング関数を使用した高速な全文検索機能を提供。シンプルなSQL構文で関連度の高いドキュメントを素早く検出できる。PostgreSQL 17・18に対応し、Linux・macOS(amd64/arm64)向けのプレビルドバイナリが配布されている。
RAGシステム(検索拡張生成)における文書検索の精度向上という観点では、RAGFlowによる検索パイプライン構築と組み合わせることで、PostgreSQLだけで完結した高品質な検索基盤が実現できる。
PostgreSQLの標準全文検索機能は基本的なキーワードマッチングに限定されており、検索品質(ランキング精度)の面で不十分という課題があった。プロジェクトの元の名称「Tapir(Textual Analysis for Postgres Information Retrieval)」が示すように、情報検索技術をPostgreSQLに統合する試みから発展。BM25はElasticsearchやLuceneなど企業向け検索エンジンで標準的なランキング関数であり、これをPostgresで実装することで、複雑な外部検索システムなしに高品質な全文検索が可能になる。
| 機能 | 詳細 |
|---|---|
| BM25ランキング | 調整可能なパラメータ(k1, b)でドキュメント関連度を計算 |
| Block-Max WAND最適化 | TOP-K検索時に競争力のないブロックをスキップして高速化 |
| 言語サポート | English, French, German等PostgreSQL標準テキスト検索設定対応 |
| 並列インデックス構築 | 大規模テーブルで複数ワーカーを活用 |
| パーティションテーブル対応 | 各パーティションが独立してインデックス構築 |
BM25(Best Match 25)は、語頻度(TF)と逆文書頻度(IDF)を組み合わせたランキング関数だ。PostgreSQL標準の ts_rank よりも検索品質が高く、ElasticsearchやLuceneが採用する実績ある手法でもある。
pg_textsearchがクエリを受け取ってから結果を返すまでのフローは以下の通りだ。
flowchart TD
A[SQLクエリ受信<br/>content <@> 'search terms'] --> B[BM25インデックスのスキャン開始]
B --> C{Block-Max WAND最適化}
C -->|スコアが閾値以上| D[ブロックを候補として処理]
C -->|スコアが閾値未満| E[ブロックをスキップ<br/>処理コスト削減]
D --> F[語頻度TFの計算<br/>k1パラメータで飽和調整]
F --> G[逆文書頻度IDFの計算<br/>全体文書数から重み付け]
G --> H[文書長正規化<br/>bパラメータで調整]
H --> I[BM25スコア算出<br/>負値で返却]
E --> J{TOP-K到達?}
I --> J
J -->|未到達| C
J -->|到達| K[昇順ソートで結果返却<br/>低スコア=高関連度]
style C fill:#fff3cd
style E fill:#d4edda
style K fill:#cce5ff
Block-Max WAND最適化(Block-Max Weak AND)は、上位K件の候補が確定した後に残りのブロックを積極的にスキップする技術だ。全文書をスコアリングする必要がなくなるため、大規模テーブルでの検索速度が大幅に向上する。
BM25インデックスの作成は以下のように行う:
-- テーブル作成
CREATE TABLE documents (
id bigserial PRIMARY KEY,
content text
);
-- インデックス作成
CREATE INDEX docs_idx ON documents
USING bm25(content)
WITH (text_config = 'english');
-- 検索実行(<@> オペレータ使用)
SELECT * FROM documents
ORDER BY content <@> 'database system'
LIMIT 5;
検索結果は負のBM25スコアで返される(PostgreSQLは昇順インデックススキャンのみサポートのため)。低いスコアほど関連度が高い。
PL/pgSQLやストアドプロシージャ内では、自動インデックス検出が機能しないため、明示的に指定:
SELECT * FROM documents
WHERE content <@> to_bm25query('database system', 'docs_idx') < -1.0
ORDER BY content <@> 'database system'
LIMIT 10;
BM25ランキングは調整可能なパラメータ(k1, b)をサポート。パラメータはインデックス作成時に指定できる。
CREATE INDEX docs_idx ON documents
USING bm25(content)
WITH (text_config = 'english');
text_configパラメータはPostgreSQL標準テキスト検索設定(english, french等)を指定する。
検索時のフィルタリングは2つのアプローチがある:
1. プリフィルタリング(推奨)
CREATE INDEX ON documents (category_id);
SELECT * FROM documents
WHERE category_id = 123
ORDER BY content <@> 'search terms'
LIMIT 10;
行数が少なければBM25スコアリング負荷が軽減される。
2. ポストフィルタリング
SELECT * FROM documents
WHERE content <@> to_bm25query('search terms', 'docs_idx') < -5.0
ORDER BY content <@> 'search terms'
LIMIT 10;
インデックスが先に上位結果を返してからフィルタリング。結果数が要求値より少なくなる可能性あり。
# ソースからコンパイル
cd /tmp
git clone https://github.com/timescale/pg_textsearch
cd pg_textsearch
make
make install # sudoが必要な場合あり
# PostgreSQLの設定編集
# postgresql.conf に以下を追加
shared_preload_libraries = 'pg_textsearch'
# サーバー再起動後、拡張機能を有効化
CREATE EXTENSION pg_textsearch;
複数PostgreSQL版がある場合、PG_CONFIGを環境変数で指定してコンパイル対象を明示できる:
export PG_CONFIG=/path/to/pg_config
make clean && make && make install
# Ubuntu/Debian
sudo apt install postgresql-server-dev-17 # PostgreSQL 17の場合
sudo apt install postgresql-server-dev-18 # PostgreSQL 18の場合
-- 並列ワーカー数を指定(デフォルト2)
SET max_parallel_maintenance_workers = 4;
SET maintenance_work_mem = '256MB'; -- 並列構築には最低64MB必須
-- インデックス構築
CREATE INDEX docs_idx ON documents
USING bm25(content)
WITH (text_config = 'english');
並列構築が有効な場合、ログに以下のノーティスが出力:
NOTICE: parallel index build: launched 4 of 4 requested workers
BM25インデックスは語位置情報を保有しないため、”database system” のようなフレーズマッチに直接対応していない。回避策:
-- BM25で候補を取得、サブクエリでフレーズ検索
SELECT * FROM (
SELECT *, content <@> 'database system' AS score
FROM documents
ORDER BY score
LIMIT 100 -- 過度にフェッチしてフィルタ損失に備える
) sub
WHERE content ILIKE '%database system%'
ORDER BY score
LIMIT 10;
インデックスは単一列限定。複数列検索には生成カラムを使用:
ALTER TABLE documents
ADD COLUMN search_text text
GENERATED ALWAYS AS (
COALESCE(title, '') || ' ' || COALESCE(content, '')
) STORED;
CREATE INDEX ON documents USING bm25(search_text)
WITH (text_config = 'english');
各パーティションは独立した統計情報(total_docs, avg_doc_len)を保有するため、クロスパーティション検索時のスコアは同一スケールでない:
-- 単一パーティション検索(推奨):スコア正確
SELECT * FROM docs
WHERE created_at >= '2024-01-01' AND created_at < '2025-01-01'
ORDER BY content <@> 'search terms'
LIMIT 10;
-- クロスパーティション検索:パーティション別に独立計算
SELECT * FROM docs
ORDER BY content <@> 'search terms'
LIMIT 10;
PostgreSQL標準の2047文字制限を継承。Base64やURLなど超長語は切り詰められる(INFOメッセージで通知)。
CREATE TABLE articles (
id serial PRIMARY KEY,
title text,
content text,
category text,
published_at timestamp
);
CREATE INDEX articles_content_idx ON articles
USING bm25(content)
WITH (text_config = 'english');
CREATE INDEX ON articles (category); -- プリフィルタリング用
-- テストデータ
INSERT INTO articles (title, content, category) VALUES
('Database Systems',
'PostgreSQL is a powerful relational database system',
'technology'),
('Search Technology',
'Full text search enables finding relevant documents quickly',
'technology'),
('Information Retrieval',
'BM25 is a ranking function used in search engines',
'academic');
-- 検索例:テクノロジーカテゴリから"database search"に関連する記事
SELECT
title,
content <@> 'database search' AS relevance_score
FROM articles
WHERE category = 'technology'
ORDER BY relevance_score
LIMIT 5;
EXPLAIN SELECT * FROM documents
ORDER BY content <@> 'database system'
LIMIT 5;
小規模データセットではPostgreSQLがシーケンシャルスキャンを選択する場合がある。強制的にインデックス使用:
SET enable_seqscan = off;
ただし、BM25スコアリングに必要な統計情報(文書数、平均文書長)は常にインデックスから取得される。
SELECT
schemaname,
tablename,
indexname,
idx_scan,
idx_tup_read,
idx_tup_fetch
FROM pg_stat_user_indexes
WHERE indexrelid::regclass::text ~ 'pg_textsearch';
プロジェクトのROADMAP.mdに記載されている計画中の機能により、全文検索性能の継続的な向上が予定されている。
pg_textsearchのような拡張機能によってPostgreSQL単体での検索品質が高まることで、ElasticsearchやOpenSearchといった専用検索エンジンへの依存を減らす選択肢が現実的になる。特にインフラを簡素化したいチームにとって、Apache Airflowによるデータパイプラインと組み合わせてPostgreSQL中心のアーキテクチャを構築する意義は大きい。
この記事はAI業界の最新動向を速報でお届けする「AI Heartland ニュース」です。