@leolee9086/hnsw
Version:
JavaScript HNSW (Hierarchical Navigable Small World) 向量索引库,支持动态操作和泛型搜索
506 lines (384 loc) • 12.8 kB
Markdown
# @leolee9086/hnsw
JavaScript HNSW (Hierarchical Navigable Small World) 向量索引库,用于快速相似性搜索。纯 JavaScript 实现,支持动态操作和泛型搜索。
## 🚀 特性
- **动态操作**: 支持插入、搜索、删除操作,软删除设计
- **泛型支持**: 完全泛型化,支持任意数据类型和自定义距离函数
- **轻量级**: 最小化依赖,专注于核心功能
- **易用性**: 简洁的API设计,快速上手
- **可扩展**: 支持动态插入和实时搜索
- **类型安全**: 完整的TypeScript支持
## 📦 安装
```bash
npm install @leolee9086/hnsw
# 或使用 pnpm
pnpm add @leolee9086/hnsw
# 或使用 yarn
yarn add @leolee9086/hnsw
```
## 🚀 快速开始
### 基本使用
```typescript
import { hnsw } from '@leolee9086/hnsw';
// 创建HNSW索引
const index = hnsw.createIndex({
M: 16, // 每个节点的最大连接数
efConstruction: 200, // 构建时的搜索参数
metricType: 'cosine' // 距离度量类型: 'cosine' | 'l2'
});
// 插入向量
const vectors = [
[1, 2, 3, 4],
[2, 3, 4, 5],
[3, 4, 5, 6],
// ... 更多向量
];
vectors.forEach(vector => {
index.insertNode(vector);
});
// 搜索最近邻
const queryVector = [1.5, 2.5, 3.5, 4.5];
const results = index.search(queryVector, 5); // 返回5个最近邻
console.log(results);
// [
// { idx: 0, distance: 0.1 },
// { idx: 1, distance: 0.2 },
// ...
// ]
```
### 高级使用
```typescript
// 使用L2距离
const l2Index = hnsw.createIndex({
M: 32,
efConstruction: 400,
metricType: 'l2'
});
// 自定义搜索参数
const results = index.search(queryVector, 10, 300); // 使用efSearch=300
// 删除节点
index.deleteNode(0); // 删除索引为0的节点
// 获取统计信息
const stats = index.getStats();
console.log(stats);
// {
// nodeCount: 100,
// activeNodeCount: 99,
// deletedNodeCount: 1,
// entryPoint: { idx: 5, level: 3 }
// }
```
### 🎨 泛型版使用
本库提供了泛型化版本,支持任意数据类型和自定义距离函数:
```typescript
import { hnsw } from '@leolee9086/hnsw';
// 1. 向量相似性搜索(使用自定义距离函数)
const cosineDistance = (a: number[], b: number[]): number => {
let dotProduct = 0;
let normA = 0;
let normB = 0;
for (let i = 0; i < a.length; i++) {
dotProduct += a[i]! * b[i]!;
normA += a[i]! * a[i]!;
normB += b[i]! * b[i]!;
}
normA = Math.sqrt(normA);
normB = Math.sqrt(normB);
if (normA === 0 || normB === 0) return 1.0;
return 1.0 - dotProduct / (normA * normB);
};
const vectorIndex = hnsw.createIndexGeneric({
M: 16,
efConstruction: 200,
distanceFunction: cosineDistance
});
// 2. 字符串相似性搜索(使用编辑距离)
const editDistance = (a: string, b: string): number => {
const matrix: number[][] = [];
for (let i = 0; i <= a.length; i++) {
matrix[i] = [];
matrix[i]![0] = i;
}
for (let j = 0; j <= b.length; j++) {
matrix[0]![j] = j;
}
for (let i = 1; i <= a.length; i++) {
for (let j = 1; j <= b.length; j++) {
if (a[i - 1] === b[j - 1]) {
matrix[i]![j] = matrix[i - 1]![j - 1]!;
} else {
matrix[i]![j] = Math.min(
matrix[i - 1]![j]! + 1, // 删除
matrix[i]![j - 1]! + 1, // 插入
matrix[i - 1]![j - 1]! + 1 // 替换
);
}
}
}
return matrix[a.length]![b.length]!;
};
const stringIndex = hnsw.createIndexGeneric({
M: 16,
efConstruction: 200,
distanceFunction: editDistance
});
// 插入字符串
const strings = ['hello', 'world', 'help', 'hell', 'hero'];
strings.forEach(str => stringIndex.insertNode(str));
// 搜索相似字符串
const results = stringIndex.search('help', 3);
console.log(results); // 找到编辑距离最小的字符串
// 3. 自定义对象相似性搜索
interface Point {
x: number;
y: number;
label: string;
}
const euclideanDistance = (a: Point, b: Point): number => {
const dx = a.x - b.x;
const dy = a.y - b.y;
return Math.sqrt(dx * dx + dy * dy);
};
const pointIndex = hnsw.createIndexGeneric({
M: 16,
efConstruction: 200,
distanceFunction: euclideanDistance
});
// 插入点对象
const points: Point[] = [
{ x: 0, y: 0, label: 'origin' },
{ x: 1, y: 0, label: 'right' },
{ x: 0, y: 1, label: 'up' },
{ x: 1, y: 1, label: 'diagonal' }
];
points.forEach(point => pointIndex.insertNode(point));
// 搜索最近的点
const queryPoint: Point = { x: 0.1, y: 0.1, label: 'near_origin' };
const nearestPoints = pointIndex.search(queryPoint, 2);
// 4. 优化距离函数(支持预计算)
const optimizedCosineDistance = (a: number[], b: number[]): number => {
let dotProduct = 0;
let i = 0;
// 循环展开优化
for (; i < a.length - 3; i += 4) {
dotProduct +=
a[i]! * b[i]! +
a[i + 1]! * b[i + 1]! +
a[i + 2]! * b[i + 2]! +
a[i + 3]! * b[i + 3]!;
}
// 处理剩余部分
for (; i < a.length; i++) {
dotProduct += a[i]! * b[i]!;
}
// 假设向量已归一化,简化计算
return 1.0 - dotProduct;
};
const optimizedIndex = hnsw.createIndexGeneric({
M: 16,
efConstruction: 200,
distanceFunction: optimizedCosineDistance
});
```
## 📚 API 文档
### 标准版 API
#### `hnsw.createIndex(config)`
创建HNSW索引实例(标准版,仅支持向量)。
**参数:**
- `config.M` (number): 每个节点的最大连接数,影响图的连接密度
- 推荐值: 16-64
- 值越大构建越慢但搜索越快
- `config.efConstruction` (number): 构建时的搜索参数,影响构建质量
- 推荐值: 100-400
- 值越大构建质量越高但速度越慢
- `config.metricType` ('cosine' | 'l2'): 距离度量类型
- `'cosine'`: 余弦距离,适用于归一化向量
- `'l2'`: 欧几里得距离的平方
**返回:** HNSWIndex实例
### 🎨 泛型版 API
#### `hnsw.createIndexGeneric<T>(config)`
创建泛型HNSW索引实例,支持任意数据类型。
**参数:**
- `config.M` (number): 每个节点的最大连接数
- `config.efConstruction` (number): 构建时的搜索参数
- `config.distanceFunction` (a: T, b: T) => number: **必需**,自定义距离函数
- `config.distanceToQuery?` (query: T, target: T) => number: **可选**,查询专用距离函数
**返回:** HNSWIndex<T>实例
**距离函数要求:**
- 必须返回非负数
- 必须满足对称性:distance(a, b) === distance(b, a)
- 必须满足自反性:distance(a, a) === 0
- 建议满足三角不等式
**示例距离函数:**
```typescript
// 余弦距离
const cosineDistance = (a: number[], b: number[]): number => {
// 实现余弦距离计算
return 1.0 - dotProduct / (normA * normB);
};
// 编辑距离
const editDistance = (a: string, b: string): number => {
// 实现编辑距离计算
return matrix[a.length]![b.length]!;
};
// 欧几里得距离
const euclideanDistance = (a: Point, b: Point): number => {
const dx = a.x - b.x;
const dy = a.y - b.y;
return Math.sqrt(dx * dx + dy * dy);
};
```
### `index.insertNode(item)`
向索引中插入新项目。
**标准版参数:**
- `item` (number[]): 要插入的向量
- 必须是非空数组
- 所有向量的维度必须一致
**泛型版参数:**
- `item` (T): 要插入的项目
- 类型必须与索引创建时指定的类型一致
**注意:** 插入操作是O(log n)复杂度,适合实时插入
### `index.search(queryItem, k, efSearch?)`
搜索最近邻。
**标准版参数:**
- `queryItem` (number[]): 查询向量
- 必须与索引中向量维度一致
**泛型版参数:**
- `queryItem` (T): 查询项目
- 类型必须与索引创建时指定的类型一致
**通用参数:**
- `k` (number): 返回的最近邻数量
- 必须大于0
- `efSearch` (number, 可选): 搜索时的参数
- 默认使用构建时的efConstruction
- 推荐值: k的2-4倍
**返回:** Neighbor[] - 包含idx和distance的对象数组,按距离升序排列
### `index.deleteNode(nodeIdx)`
删除指定节点。
**参数:**
- `nodeIdx` (number): 要删除的节点索引
**返回:** boolean - 删除是否成功
**注意:**
- 使用软删除机制,不影响搜索性能
- 删除的节点不会出现在搜索结果中
- 删除入口点时会自动重新选择入口点
### `index.getStats()`
获取索引统计信息。
**返回:**
```typescript
{
nodeCount: number, // 节点总数(包括已删除的)
activeNodeCount: number, // 活跃节点数(未删除的)
deletedNodeCount: number, // 已删除节点数
entryPoint: { // 入口点信息
idx: number, // 入口点索引
level: number // 入口点层级
}
}
```
## 🎯 使用建议
### 参数调优
| 参数 | 推荐值 | 说明 |
|------|--------|------|
| M | 16-64 | 连接数,影响搜索精度和速度 |
| efConstruction | 100-400 | 构建质量,影响索引质量 |
| efSearch | k的2-4倍 | 搜索精度,影响召回率 |
### 向量预处理
- **归一化**: 使用余弦距离时确保向量已归一化
- **维度**: 推荐100-1000维,过高维度会影响性能
- **稀疏性**: 避免使用稀疏向量,使用密集向量
### 最佳实践
1. **批量插入**: 对于大量数据,建议批量插入而非逐个插入
2. **参数平衡**: 根据应用场景平衡精度和速度
3. **监控性能**: 使用`getStats()`监控索引状态
4. **内存管理**: 大规模应用注意内存使用
### 🎨 泛型版最佳实践
1. **距离函数优化**:
- 使用循环展开优化向量运算
- 预计算常用值(如范数)
- 避免在距离函数中进行复杂计算
2. **类型安全**:
- 确保距离函数返回正确的数值类型
- 验证距离函数的数学性质(对称性、自反性)
3. **性能考虑**:
- 对于复杂对象,考虑使用缓存机制
- 避免在距离函数中创建大量临时对象
4. **应用场景**:
- **向量搜索**: 使用标准版API
- **字符串搜索**: 使用编辑距离、Jaccard距离等
- **图节点搜索**: 使用图距离函数
- **自定义对象**: 根据业务需求定义距离函数
## ⚠️ 限制说明
### 当前限制
#### 标准版限制
1. **向量维度**: 所有向量必须具有相同维度
2. **数据类型**: 仅支持number[]类型的向量
3. **距离度量**: 仅支持余弦距离和L2距离
4. **内存使用**: 大规模数据集需要足够内存
5. **并发安全**: 当前版本不支持并发操作
#### 泛型版限制
1. **距离函数性能**: 自定义距离函数可能影响搜索性能
2. **类型一致性**: 所有插入和搜索的项目类型必须一致
3. **距离函数要求**: 必须满足对称性和自反性
4. **内存使用**: 复杂对象可能增加内存占用
5. **并发安全**: 当前版本不支持并发操作
### 性能限制
- **构建时间**: 大规模数据集构建时间较长
- **内存占用**: 索引会占用额外内存存储图结构
- **搜索精度**: 近似搜索,不保证100%召回率
## 🔧 开发
### 安装依赖
```bash
pnpm install
```
### 运行测试
```bash
# 运行所有测试
pnpm test
# 运行测试并监听变化
pnpm test:watch
# 运行基准测试
pnpm bench
# 生成覆盖率报告
pnpm test:coverage
```
### 构建
```bash
# 构建项目
pnpm build
# 开发模式构建
pnpm dev
```
## 📊 基准测试
项目包含完整的基准测试套件,对比HNSWLib等主流实现:
```bash
# 运行性能基准测试
pnpm bench
```
基准测试包括:
- 索引构建性能
- 搜索性能
- 召回率测试
- 内存使用分析
## 🤝 贡献
欢迎提交Issue和Pull Request!
### 开发指南
1. Fork项目
2. 创建功能分支
3. 提交更改
4. 运行测试确保通过
5. 提交Pull Request
## 📄 许可证
MIT License - 详见 [LICENSE](LICENSE) 文件。
## 📝 更新日志
### 1.0.0
- 🎉 初始版本发布
- ⚡ 高性能HNSW算法实现
- 🎯 支持余弦距离和L2距离
- 🔧 支持插入、搜索、删除操作
- 📦 完整的TypeScript支持
- 🧪 全面的测试覆盖
- 📊 性能基准测试套件
## 🔗 相关链接
- [GitHub Repository](https://github.com/leolee9086/hnsw)
- [Issue Tracker](https://github.com/leolee9086/hnsw/issues)
- [HNSW论文](https://arxiv.org/abs/1603.09320)