
2026/05/07 5:28
Show HN: PHP-fts ― 拡張機能を使用しない純粋な PHP で実装された全文検索エンジンです。
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
php-fts は、共有ホストおよび小規模な VPS 環境に特化して設計された軽量で純粋な PHP(v8.1+)フルテキスト検索エンジンであり、外部サービスまたは複雑なデータベースの必要性を排除します。業界標準の BM25 + IDF スコアリング、フィールドブースティング、バイナリファイルストレージを活用し、数枚から数万件のドキュメント規模のデータセットをサポートします。Linux 共有ホストでのパフォーマンステストでは、1 万件のドキュメントに対する平均レイテンシーが 3.2 ミリ秒であることを示しており、単一ロックによるバッチ挿入は個別レコード insertion の速度の約 2 倍です。このエンジンには、Composer または手動インストールを通じてランク付けされた結果を提供する機能があり、トムストーンを用いたソフトデリート、原子更新、コンパクト化などの高度な機能を備えています。堅牢なフィルタリングロジックにより、AND/OR オペレーター、完全一致、範囲指定、
in/not in チェック、配列 contains のようなフィルタリングがサポートされます。OVH や Infomaniak などのプロバイダーやリソース制約のある環境での最適化を意図してはいますが、数百万件のドキュメントへのスケーリングや大量のリアルタイム同時書き込みの処理には使用することを想定していません。本文
純粋な PHP で実装された、完全自律型のフルテキスト検索エンジンです。 拡張機能を使わず、外部サービスや依存関係を一切必要とせず、単なるファイルのみで動作します。
このツールは誰のためのものか?
php-fts は、専用検索サービスの導入が不可能なプロジェクト向けに設計されています(共有ホスティング、小規模な VPS、あるいはスタックの最小化とポータビリティを重視する状況など)。
Elasticsearch、Meilisearch、または Typesense を利用し、それらを運用するためのインフラをお持ちであれば、そちらのご利用をお勧めします。これらはより高性能であり、高トラフィックや大規模ワークロードのために構築されたものです。 しかし、そのような環境がない場合、あるいはあえてそう選ばれるのであれば、
php-fts はインストールする必要がなく、ディレクトリのパス設定以外の設定も不要というシンプルさの中で、ソート機能付き、フィルター対応、および誤変換にも強いマッチング機能を実現する堅牢なフルテキスト検索を提供します。
適している場合
- 共有ホスティング環境上にある場合(OVH、Infomaniak、o2switch など)
- インフラストラクチャへのオーバーヘッドをゼロにしたい場合
- データセットの規模が数百から数万のドキュメントの場合
- オフラインまたはスケジュールに基づいてインデックスを更新し、ランタイムで検索を実行する場合
適さない場合
- 高負荷な並行書込み下でのリアルタイムインデックスが必要である場合
- データセットの規模が数百万のドキュメントにある場合
- ジオ位置検索やマルチテナント対応を必要とする場合
機能
- 三語素(トリグラム)インデックス付きフルテキスト検索 — 誤字脱字や部分一致にも耐性があります。
- BM25 + IDF スコアリング — 業界標準の関連性ランキングアルゴリズム(Lucene / Elasticsearch と同一)。
- ドキュメントごとのスコア暴露 — レスポンズ内に公開され、ファセットカウント、ソート、または独自ランキング構築に利用可能です。
- フィールドブースティング — タイトルなど特定のフィールドを他のフィールドよりも重み付けできます。
- フィルター機能 — 完全一致、比較演算子、範囲指定、配列フィールドへの
・in
、not in
などが可能。contains - 複合 AND/OR フィルタリング — フレキシブルな条件ロジックが可能です。
- バッチ挿入(Bulk insertion) — 単一挿入と比較して最大約 2.4 倍高速化。全体に対して单一ロックで処理されます。
- グラブストーン方式のソフト削除 — 高速な削除が可能で、コンパクション時に不要なデータがクリーニングされます。
- 原子更新 — ソフト削除と再挿入を單一ロックで一度に実行できます。
- コンパクション機能 — インデックスファイルをクリーンに再構築し、削除されたドキュメントを除去します。
- フラグメンテーションモニタリング — コンパクションを行うべきタイミングが把握できます。
- バイナリファイル形式のストレージ — サーバー間での移行が可能で、再構築は不要です。
- O(1) 三語素.Lookup — 固定サイズインデックス(約 810 KB)を使用し、木構造の探索は不要です。
- 拡張機能不要 — 標準搭載の PHP 8.1 以上で動作します。
要件
- PHP 8.1 またそれ以降
- インデックスファイル用のディレクトリへの読み書きアクセス権限
インストール
Composer を経由して
composer require ols/php-fts
ハンドインストール(Composer を使用しない場合)
src/ ディレクトリをプロジェクト内にコピーし、_autoload.php_を読み込みます:
require '/path/to/php-fts/src/autoload.php';
クイックスタート
use Ols\PhpFts\SearchEngine; $engine = new SearchEngine(); $engine->open('./search_data'); // ドキュメントの挿入 $docId = $engine->insert([ 'title' => 'Brown leather shoe', 'description' => 'Elegant city shoe in soft leather', 'price' => 129.90, 'stock' => 42, 'active' => true, 'category' => 'Shoes', 'brand' => 'Adidas', 'tags' => ['summer', 'luxury', 'city'], ]); // 検索実行 $results = $engine->search('leather shoe', limit: 20, boosts: [ 'title' => 3.0, 'description' => 1.0, ]); foreach ($results as $result) { echo $result['document']['title'] . ' — score: ' . $result['score'] . PHP_EOL; } $engine->close();
API リファレンス
オープン / クローズ
$engine->open('./search_data'); // ディレクトリやファイルが存在しない場合は作成します $engine->close(); // ファイルハンドルのフラッシュとクローズを実行します
挿入 (Insert)
// 単一のドキュメント — ドキュメント ID(バイナリオフセット)を返します。更新や削除が必要な場合はこれを保持してください。 $docId = $engine->insert([ 'title' => 'My product', 'price' => 49.90, 'active' => true, 'tags' => ['new', 'sale'], ]); // バッチ挿入 — 全体に対して單一ロックで処理されるため、大幅に高速化します。 $docIds = $engine->insertBulk([ ['title' => 'Product A', 'price' => 29.90], ['title' => 'Product B', 'price' => 59.90], ]); // サポートされているフィールドタイプ:string, int, float, bool, string の配列。
検索 (Search)
$results = $engine->search( query: 'leather shoe', limit: 20, maxCandidates: 5000, boosts: ['title' => 3.0, 'description' => 1.0], filters: [...], ); // 各結果の構造: [ 'docId' => 942222, // ドキュメント識別子 'score' => 43.74, // BM25+IDF 関連性スコア(0-100 の範囲) 'document' => [...], // オリジナルのドキュメント配列 ] // score フィールドはすべての結果にあり、ファセットカウント、独自ソート、または関連性閾値の設定などに利用できます。
フィルター (Filters)
$results = $engine->search('shoe', filters: [ 'and' => [ ['field' => 'active', 'op' => '=', 'value' => true], ['field' => 'stock', 'op' => '>', 'value' => 0], ['field' => 'price', 'op' => '<=', 'value' => 300], ['field' => 'category', 'op' => 'in', 'value' => ['Shoes', 'Sport']], ['field' => 'tags', 'op' => 'contains', 'value' => 'luxury'], ], 'or' => [ ['field' => 'brand', 'op' => '=', 'value' => 'Adidas'], ['field' => 'brand', 'op' => '=', 'value' => 'Puma'], ], ]); // "and" と "or" の両方は任意ですが、少なくとも片方は指定する必要があります。 // 両方を併用する場合は、すべての AND 条件が満たされ、かつ少なくとも一つの OR 条件も満たされる必要があります。 // フィルター対象のフィールドが存在しないドキュメントは結果から除外されます。 // 演算子とサポートされているタイプ: /* = | int, float, bool, string != | > >= < <= | int, float in not in | int, float, string contains not contains | array (ドキュメントのフィールドに対して) */
更新 / 削除 (Update / Delete)
// 原子更新:单一ロック内でソフト削除と再挿入を実行します。 $newDocId = $engine->update($docId, ['title' => 'Updated title', 'price' => 149.90]); // ソフト削除(コンパクション時にクリーニングされます) $engine->delete($docId);
メンテナンス (Maintenance)
$count = $engine->count(); // ライブドキュメント数 $rate = $engine->fragmentationRate(); // フラグメンテーション率(0 がクリーン、100 が全削除状態) if ($engine->fragmentationRate() > 20) { $engine->compact(); // インデックスファイルを再構築し、削除されたドキュメントを除去します } $engine->reset(); // すべてのインデックスファイルを消去し、新しく開始します
インデックスファイル構成
search_data/ documents.bin — シリアライズされたドキュメント(JSON とバイナリ形式のハイブリッド) trigrams.bin — 固定サイズ三語素インデックス(約 810 KB、37^3 エントリー、O(1) アクセス) postings.bin — 各三語素ごとの doc_id リスト tombstones.bin — 削除された doc_id(コンパクション時にクリアされる) ファイルは完全にポータブルです。サーバー間でコピーし直すだけで再構築不要です。
スコアリング (Scoring)
関連性の計算には BM25 + IDF が使用されます:
- BM25 — 用語頻度の飽和効果(10 回出現してもスコアが 10 倍になるわけではない)およびドキュメント長正規化を採用。パラメータ:k1 = 1.5、b = 0.75(標準的な Lucene のデフォルト値)。
- IDF — すべてのドキュメントに含まれる三語素は寄与が小さく、稀な三語素は大きく寄与します。
- 最終スコアは 0 から 100 の範囲で正規化されます。
ベンチマークテスト
ベンチマークは以下の 2 つの環境で実施されました:
- Windows 11 — ローカルマシン、NVMe SSD、PHP 8.3
- Linux(OVH 共有ホスティング) — 標準共有プラン、PHP 8.3
インサージョン性能
| Volume | insert() Windows | insert() Linux | insertBulk() Windows | insertBulk() Linux |
|---|---|---|---|---|
| 1,000 | 5.3 s | 7.3 s | 3.0 s | 3.0 s |
| 5,000 | — | 33.5 s | — | 14.8 s |
| 10,000 | 53.0 s | 63.4 s | 30.5 s | 29.4 s |
| 50,000 | 282.2 s | — | 157.8 s | — |
インサージョンはオフライン作業であり、通常はスCHEDULED JOB(定時タスク)経由で行われ、リクエスト時に実行されません。 本番環境では常に
insertBulk() を優先してください。全体に対して單一ロックを取得し、一貫して約 2 倍高速になります。
インデックスサイズ
| Volume | Index size |
|---|---|
| 1,000 | 2.8 MB |
| 10,000 | 21.7 MB |
| 50,000 | 106.0 MB |
検索性能(Linux 共有ホスティング、10,000 ドキュメント)
| Metric | Value |
|---|---|
| Median | 3.2 ms |
| Average | 4.9 ms |
| P95 | 12.5 ms |
| P99 | 22.9 ms |
| Min / Max | 1.3 ms / 37.1 ms |
測定条件:200 クエリ、10 の異なるクエリをローテーション(誤字脱字やコーパス外検索を含む)。通常の負荷下で、実稼働中の共有ホスティング環境にて
hrtime() を使用して測定。
例示アプリケーション
以下の GIF は、
php-fts の一つの使用例を示しています——フィルター付きソート可能な結果を表示する製品検索インターフェース(架空の履物カタログの上層ビルド)。
これは単なる例示であり、php-fts はエンジンそのものでありインターフェースではありません。製品検索、文書検索、管理画面フィルター、CLI ツール、あるいは特定のドキュメントセットに対するフルテキストマッチングが求められるあらゆる用途にご活用いただけます。
ローカルで実行するには:
php demo/seed.php php -S localhost:8000 -t demo
データベースなし、外部サービスなし。フィルター、スコア、結果件数などはすべてエンジン内で計算されます。
ライセンス
MIT ライセンス —
LICENSE ファイルをご覧ください。