一个纯 PHP 向量数据库,实现了 HNSW(Hierarchical Navigable Small World)用于近似最近邻搜索,以及 BM25 用于全文检索。两种引擎可以组合成一个单一的 混合搜索(hybrid search) 管道。
composer require ezimuel/phpvector
Document 对象包含密集嵌入向量、可选的原始文本(用于 BM25)以及你希望随结果返回的任意元数据。id 字段可选——如果省略,将自动分配一个随机的 UUID v4。
use PHPVector\Document;
use PHPVector\VectorDatabase;
$db = new VectorDatabase();
$db->addDocuments([
new Document(
id: 1,
vector: [0.12, 0.85, 0.44, 0.67],
text: 'PHP vector database with HNSW index',
metadata: ['url' => 'https://example.com/1', 'lang' => 'en'],
),
new Document(
id: 2,
vector: [0.91, 0.23, 0.78, 0.05],
text: 'Approximate nearest neighbour search in PHP',
metadata: ['url' => 'https://example.com/2', 'lang' => 'en'],
),
new Document(
id: 3,
vector: [0.33, 0.61, 0.19, 0.88],
text: 'BM25 full-text ranking algorithm explained',
metadata: ['url' => 'https://example.com/3', 'lang' => 'en'],
),
// 未提供 id 时,会自动分配 UUID v4
new Document(
vector: [0.55, 0.42, 0.71, 0.30],
text: 'Hybrid search with Reciprocal Rank Fusion',
),
]);
使用 HNSW 查找与查询向量最相似的 k 个文档。
$queryVector = [0.10, 0.80, 0.50, 0.60];
$results = $db->vectorSearch(vector: $queryVector, k: 2);
foreach ($results as $result) {
echo sprintf(
"[%d] score=%.4f %s\n",
$result->rank,
$result->score,
$result->document->metadata['url'],
);
}
// 输出示例:
// [1] score=0.9987 https://example.com/1
// [2] score=0.8341 https://example.com/3
使用 BM25 根据文本查询对文档进行相关性排序。
$results = $db->textSearch(query: 'nearest neighbour PHP', k: 2);
foreach ($results as $result) {
echo sprintf(
"[%d] score=%.4f %s\n",
$result->rank,
$result->score,
$result->document->metadata['url'],
);
}
// 输出示例:
// [1] score=1.2430 https://example.com/2
// [2] score=0.8761 https://example.com/1
将向量相似度分数和 BM25 分数融合成一个统一的排序列表。
RRF 基于排名且与分数尺度无关,无需调参。
use PHPVector\HybridMode;
$results = $db->hybridSearch(
vector: $queryVector,
text: 'vector database PHP',
k: 3,
mode: HybridMode::RRF,
);
foreach ($results as $result) {
echo sprintf(
"[%d] score=%.4f %s\n",
$result->rank,
$result->score,
$result->document->metadata['url'],
);
}
将两种分数都归一化到 [0, 1] 区间,然后应用显式权重。
$results = $db->hybridSearch(
vector: $queryVector,
text: 'vector database PHP',
k: 3,
mode: HybridMode::Weighted,
vectorWeight: 0.7,
textWeight: 0.3,
);
HNSW 和 BM25 引擎均可完全配置。将配置对象传递给 VectorDatabase 构造函数即可。
use PHPVector\BM25\ConfigasBM25Config;
use PHPVector\BM25\SimpleTokenizer;
use PHPVector\Distance;
use PHPVector\HNSW\ConfigasHNSWConfig;
use PHPVector\VectorDatabase;
$db = new VectorDatabase(
hnswConfig: new HNSWConfig(
M: 16, // 每层每个节点的最大连接数。越高 → 召回率越好,但内存占用增加。
efConstruction: 200, // 构建索引时的束宽。越高 → 图质量越好,但插入变慢。
efSearch: 50, // 查询时的束宽。越高 → 召回率越好,但查询变慢。
distance: Distance::Cosine, // 支持:Cosine | Euclidean | DotProduct | Manhattan
useHeuristic: true, // 多样化邻居选择(推荐启用)。
),
bm25Config: new BM25Config(
k1: 1.5, // TF 饱和度。推荐范围 1.2–2.0。
b: 0.75, // 长度归一化。0 = 不归一化,1 = 完全归一化。
),
tokenizer: new SimpleTokenizer(
stopWords: SimpleTokenizer::DEFAULT_STOP_WORDS,
minTokenLength: 2,
),
);
度量 | 最适用场景 |
|---|---|
Distance::Cosine | 文本嵌入、已归一化向量 |
Distance::Euclidean | 原始未归一化向量 |
Distance::DotProduct | 单位归一化向量(比 Cosine 更快) |
Distance::Manhattan | 稀疏向量,对异常值更鲁棒 |
目标 | 调整参数 |
|---|---|
提高召回率 | 增大 efSearch 或 efConstruction |
加快查询速度 | 减小 efSearch |
减少内存占用 | 减小 M |
处理聚类数据时获得更好图结构 | 保持 useHeuristic: true |
PHPVector 采用基于文件夹的持久化模型。每个数据库对应一个独立目录,里面包含 HNSW 图、BM25 索引以及每个文档单独的文件。这种设计有两个主要优势:
docs/{n}.bin)采用懒加载,仅在搜索结果中出现时才读取。addDocument() 调用会立即返回。/var/data/mydb/
meta.json — 距离度量、维度、文档 ID 映射
hnsw.bin — HNSW 图(向量 + 连接关系)
bm25.bin — BM25 倒排索引
docs/
0.bin — 文档 0(id、文本、元数据)
1.bin — 文档 1
…
在构造函数中传入 path 参数即可启用持久化。每次 addDocument() 调用都会将文档文件写入 docs/ 目录(启用 ext-pcntl 时异步进行)。最后调用一次 save() 将 HNSW 图和 BM25 索引刷新到磁盘(会等待所有异步写入完成)。
use PHPVector\Document;
use PHPVector\VectorDatabase;
$db = new VectorDatabase(path: '/var/data/mydb');
$db->addDocuments([
new Document(id: 1, vector: [0.12, 0.85, 0.44], text: 'PHP vector search', metadata: ['source' => 'blog']),
new Document(id: 2, vector: [0.91, 0.23, 0.78], text: 'Approximate nearest neighbour'),
// ... 可以插入数千个文档
]);
// 将 HNSW 图和 BM25 索引刷盘(文档文件已异步写入)
$db->save();
使用 VectorDatabase::open() 加载已保存的数据库(文档加载部分此处原文被截断,实际使用时可参考源码或后续更新)。