USearch
更小 & 更快 的单文件
向量 & 🔜 文本 相似度搜索引擎
空间 • 二进制 • 概率 • 用户自定义度量
C++ 11 •
Python 3 •
JavaScript •
Java •
Rust •
C 99 •
Objective-C •
Swift •
C# •
GoLang •
Wolfram
Linux • MacOS • Windows • iOS • WebAssembly •
SQLite3
- ✅ 比FAISS快10倍 的 HNSW 实现。
- ✅ 简单且可扩展的单个C++11头文件 库。
- ✅ 受到谷歌等巨头和 ClickHouse 与 DuckDB 等数据库的信赖。
- ✅ SIMD优化和带有JIT编译的用户自定义度量。
- ✅ 硬件无关的
f16
和i8
- 半精度和四分之一精度支持。 - ✅ 从磁盘查看大型索引,无需加载到内存。
- ✅ 异构查询、重命名/重标记和即时删除。
- ✅ 用于基因组学和化学应用的二进制Tanimoto和Sorensen系数。
- ✅ 使用
uint40_t
的空间高效点云,可容纳超过4B的大小。 - ✅ 兼容OpenMP和自定义"执行器",实现细粒度并行。
- ✅ 语义搜索和连接。
- 🔄 近实时聚类和子聚类,可处理数千万个簇。
__技术洞察__和相关文章:
- 使用Horner方法进行多项式近似,性能比GCC 12高119倍。
- 使用Arm SVE和x86 AVX-512的掩码加载来消除尾部
for
循环。 - 使用AVX-512 FP16进行半精度运算,很少有编译器能对其进行向量化。
- 用Jan Kadlec常数的位操作替代LibC的
sqrt
调用。 - 为每种语言实现自定义的单独绑定。
- 对于Python,避免使用缓慢的PyBind11,甚至避免使用
PyArg_ParseTuple
以提高速度。 - 对于JavaScript,使用类型化数组和NAPI实现零拷贝调用。
与FAISS的比较
FAISS是广受认可的高性能向量搜索引擎标准。 USearch和FAISS都使用相同的HNSW算法,但在设计原则上有显著差异。 USearch在不牺牲性能的情况下更加紧凑和广泛兼容,主要关注用户自定义度量和更少的依赖。
FAISS | USearch | 改进 | |
---|---|---|---|
索引时间 ⁰ | |||
1亿个96维 f32 、f16 、i8 向量 | 2.6 · 2.6 · 2.6 h | 0.3 · 0.2 · 0.2 h | 9.6 · 10.4 · 10.7 x |
1亿个1536维 f32 、f16 、i8 向量 | 5.0 · 4.1 · 3.8 h | 2.1 · 1.1 · 0.8 h | 2.3 · 3.6 · 4.4 x |
代码库长度 ¹ | 84 K SLOC | 3 K SLOC | 可维护 |
支持的度量 ² | 9种固定度量 | 任意度量 | 可扩展 |
支持的语言 ³ | C++, Python | 10种语言 | 可移植 |
支持的ID类型 ⁴ | 32位, 64位 | 32位, 40位, 64位 | 高效率 |
过滤 ⁵ | 禁用列表 | 任意谓词 | 可组合 |
所需依赖 ⁶ | BLAS, OpenMP | - | 轻量级 |
绑定 ⁷ | SWIG | 原生 | 低延迟 |
Python绑定大小 ⁸ | ~ 10 MB | < 1 MB | 易部署 |
⁰ 在Intel Sapphire Rapids上测试,使用最简单的内积距离,等效召回率和内存消耗,同时还提供了更优的搜索速度。 ¹
usearch/
相比faiss/
更短的代码库使项目更易于维护和审核。 ² 用户自定义度量允许你为各种应用定制搜索,从GIS到为多个AI模型的复合嵌入或混合全文和语义搜索创建自定义度量。 ³ 使用USearch,你可以在各种编程语言中重用同一个预构建索引。 ⁴ 40位整数允许你存储超过4B个向量,而无需为邻近图中的每个邻居引用分配8字节。 ⁵ 使用USearch,索引可以与任意外部容器(如布隆过滤器或第三方数据库)结合,在索引遍历期间过滤掉不相关的键。 ⁶ 缺少必要的依赖使USearch更加可移植。 ⁷ 原生绑定比更简单的方法引入更低的调用延迟。 ⁸ 更轻量的绑定使下载和部署更快。
基本功能与FAISS相同,如果你曾研究过近似最近邻搜索,接口应该很熟悉:
# pip install numpy usearch
import numpy as np
from usearch.index import Index
index = Index(ndim=3)
vector = np.array([0.2, 0.6, 0.4])
index.add(42, vector)
matches = index.search(vector, 10)
assert matches[0].key == 42
assert matches[0].distance <= 0.001
assert np.allclose(index[42], vector, atol=0.1) # 确保在混合精度比较中使用高容差
更多设置始终可用,API设计旨在尽可能灵活。
默认存储/量化级别取决于硬件以提高效率,但对大多数现代CPU推荐使用f16
。
index = Index(
ndim=3, # 定义输入向量的维度数
metric='cos', # 选择'l2sq'、'ip'、'haversine'或其他度量,默认='cos'
dtype='f16', # 存储为'f64'、'f32'、'f16'、'i8'、'b1'...,默认=None
connectivity=16, # 可选:限制每个图节点的邻居数
expansion_add=128, # 可选:控制索引的召回率
expansion_search=64, # 可选:控制搜索质量
multi=False, # 可选:允许每个键多个向量,默认=False
)
序列化和从磁盘服务Index
USearch支持多种序列化形式:
- 序列化到由路径定义的__文件__中。
- 序列化到由回调定义的__流__中,逐步序列化或重建。
- 序列化到固定长度的__缓冲区__或支持随机访问的内存映射文件中。
后者允许你从外部内存提供索引服务,使你能够针对索引速度和服务成本优化服务器选择。 这可以在AWS和其他公共云上实现__20倍的成本降低__。
index.save("index.usearch")
loaded_copy = index.load("index.usearch")
view = Index.restore("index.usearch", view=True)
other_view = Index(ndim=..., metric=...)
other_view.view("index.usearch")
精确与近似搜索
当精确的暴力搜索变得过于资源密集时,通常会使用近似搜索方法,如HNSW。
这通常发生在集合中有数百万个条目时。
对于较小的集合,我们提供了一种更直接的方法,即search
方法。
from usearch.index import search, MetricKind, Matches, BatchMatches
import numpy as np
# 生成10'000个随机向量,每个向量有1024个维度
vectors = np.random.rand(10_000, 1024).astype(np.float32)
vector = np.random.rand(1024).astype(np.float32)
one_in_many: Matches = search(vectors, vector, 50, MetricKind.L2sq, exact=True)
many_in_many: BatchMatches = search(vectors, vectors, 50, MetricKind.L2sq, exact=True)
如果传入exact=True
参数,系统将完全绕过索引,使用来自SimSIMD的SIMD优化相似度度量,对整个数据集执行暴力搜索。
与FAISS的IndexFlatL2
在Google Colab上相比,USearch可能提供高达20倍的性能提升:
faiss.IndexFlatL2
: 55.3毫秒。usearch.index.search
: 2.54毫秒。
用户自定义度量
虽然大多数向量搜索包只专注于两种度量,即"内积距离"和"欧几里德距离",但USearch允许使用任意用户自定义度量。 这种灵活性使您可以为各种应用定制搜索,从使用罕见的Haversine距离计算地理空间坐标,到为多个AI模型的复合嵌入创建自定义度量,如联合图像-文本嵌入。 您可以使用Numba、Cppyy或PeachPy来在Python中定义自定义度量:
from numba import cfunc, types, carray
from usearch.index import Index, MetricKind, MetricSignature, CompiledMetric
@cfunc(types.float32(types.CPointer(types.float32), types.CPointer(types.float32)))
def python_inner_product(a, b):
a_array = carray(a, ndim)
b_array = carray(b, ndim)
c = 0.0
for i in range(ndim):
c += a_array[i] * b_array[i]
return 1 - c
metric = CompiledMetric(pointer=python_inner_product.address, kind=MetricKind.IP, signature=MetricSignature.ArrayArray)
index = Index(ndim=ndim, metric=metric, dtype=np.float32)
在C、C++和Rust接口中实现类似效果甚至更容易。 此外,与KD树和局部敏感哈希等较旧的高维空间索引方法不同,HNSW不要求向量长度相同。 它们只需要可比较。 因此,您可以在奇特的应用中使用它,比如搜索相似集合或模糊文本匹配,使用GZip压缩率作为距离函数。
过滤和谓词函数
有时您可能想要根据某些外部数据库交叉引用搜索结果或根据某些条件过滤它们。 在大多数引擎中,您必须手动执行分页请求,逐步过滤结果。 在USearch中,您只需将谓词函数传递给搜索方法,该函数将直接在图遍历过程中应用。 在Rust中,这看起来像这样:
let is_odd = |key: Key| key % 2 == 1;
let query = vec![0.2, 0.1, 0.2, 0.1, 0.3];
let results = index.filtered_search(&query, 10, is_odd).unwrap();
assert!(
results.keys.iter().all(|&key| key % 2 == 1),
"所有键必须为奇数"
);
内存效率、降精度和量化
训练量化模型和降维是加速向量搜索的常见方法。
然而,这些方法并不总是可靠的,可能会显著影响数据的统计属性,并且如果分布发生变化,需要定期调整。
相反,我们专注于对降精度向量进行高精度运算。
相同的索引、add
和search
操作将自动在f64_t
、f32_t
、f16_t
、i8_t
和单比特表示之间进行降精度或提升精度。
您可以使用以下命令检查是否启用了硬件加速:
$ python -c 'from usearch.index import Index; print(Index(ndim=768, metric="cos", dtype="f16").hardware_acceleration)'
> sapphire
$ python -c 'from usearch.index import Index; print(Index(ndim=166, metric="tanimoto").hardware_acceleration)'
> ice
使用较小的数值类型将节省存储向量所需的RAM,但您也可以压缩形成我们近邻图的邻居列表。
默认情况下,使用32位的uint32_t
来枚举这些列表,但如果需要寻址超过40亿个条目,这是不够的。
对于这种情况,我们提供了一个自定义的uint40_t
类型,它仍然比常用的8字节整数节省37.5%的空间,并且可以扩展到1万亿个条目。
Indexes
用于多索引查找
对于针对数十亿甚至万亿向量的大型工作负载,并行多索引查找变得非常有价值。 您可以构建多个较小的索引,而不是构建一个大型索引,并将它们一起查看。
from usearch.index import Indexes
multi_index = Indexes(
indexes: Iterable[usearch.index.Index] = [...],
paths: Iterable[os.PathLike] = [...],
view: bool = False,
threads: int = 0,
)
multi_index.search(...)
聚类
一旦构建了索引,USearch可以比独立的聚类库(如SciPy、UMap和tSNE)更快地执行K近邻聚类。
对PCA进行降维也是如此。
本质上,Index
本身可以被视为一种聚类,允许迭代深化。
clustering = index.cluster(
min_count=10, # 可选
max_count=15, # 可选
threads=..., # 可选
)
# 获取聚类及其大小
centroid_keys, sizes = clustering.centroids_popularity
# 使用Matplotlib绘制直方图
clustering.plot_centroids_popularity()
# 导出聚类的NetworkX图
g = clustering.network
# 获取特定聚类的成员
first_members = clustering.members_of(centroid_keys[0])
# 深入到该聚类,将其分成更多部分,支持所有相同的参数
sub_clustering = clustering.subcluster(min_count=..., max_count=...)
生成的聚类结果与K-Means或其他传统方法不完全相同,但serve相同的目的。 另外,在使用Scikit-Learn处理100万数据点的数据集时,查询可能需要几分钟到几小时的时间,具体取决于您想要突出显示的聚类数量。 对于50,000个聚类,USearch与传统聚类方法的性能差异可能轻松达到100倍。
连接、一对一、一对多和多对多映射
当今的一个重大问题是人工智能将如何改变数据库和数据管理的世界。 大多数数据库仍在努力实现高质量的模糊搜索,而且它们所知道的唯一连接类型是确定性的。 "连接"与搜索每个条目不同,它需要一对一映射,禁止在不同的搜索结果之间出现冲突。
精确搜索 | 模糊搜索 | 语义搜索? |
---|---|---|
精确连接 | 模糊连接? | 语义连接?? |
使用USearch,可以实现亚二次复杂度的近似、模糊和语义连接。 这在数据库管理软件常见的任何模糊匹配任务中都很有用。
men = Index(...)
women = Index(...)
pairs: dict = men.join(women, max_proposals=0, exact=False)
阅读更多内容,请参阅文章:语义搜索的组合稳定婚姻 💍
功能
目前,所有绑定都支持核心功能。 更广泛的功能按需移植。 在某些情况下,如批处理操作,功能对等是没有意义的,因为宿主语言具有完整的多线程能力,而USearch索引结构在设计上是并发的,因此用户可以以最适合其应用的方式实现批处理/调度/负载平衡。
C++ 11 | Python 3 | C 99 | Java | JavaScript | Rust | GoLang | Swift | |
---|---|---|---|---|---|---|---|---|
添加、搜索、删除 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
保存、加载、查看 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
用户自定义度量 | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
批处理操作 | ❌ | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
过滤谓词 | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ |
连接 | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
可变长度向量 | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
4B+容量 | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
应用示例
USearch + UForm + UCall = 多模态语义搜索
人工智能有越来越多的应用,但最酷的经典想法之一是将其用于语义搜索。 可以使用多模态编码器模型,如UForm,以及Web编程框架,如UCall,仅用20行Python代码就可以构建一个文本到图像的搜索平台。
from ucall import Server
from uform import get_model, Modality
from usearch.index import Index
import numpy as np
import PIL as pil
processors, models = get_model('unum-cloud/uform3-image-text-english-small')
model_text = models[Modality.TEXT_ENCODER]
model_image = models[Modality.IMAGE_ENCODER]
processor_text = processors[Modality.TEXT_ENCODER]
processor_image = processors[Modality.IMAGE_ENCODER]
server = Server()
index = Index(ndim=256)
@server
def add(key: int, photo: pil.Image.Image):
image = processor_image(photo)
vector = model_image(image)
index.add(key, vector.flatten(), copy=True)
@server
def search(query: str) -> np.ndarray:
tokens = processor_text(query)
vector = model_text(tokens)
matches = index.search(vector.flatten(), 3)
return matches.keys
server.run()
类似的体验也可以在其他语言和客户端实现,消除网络延迟。
对于Swift和iOS,请查看ashvardanian/SwiftSemanticSearch
仓库。
GitHub上提供了一个更完整的Streamlit演示。 我们已预处理了一些常用数据集,清理了图像,生成了向量,并预构建了索引。
数据集 | 模态 | 图像数 | 下载 |
---|---|---|---|
Unsplash | 图像和描述 | 25 K | HuggingFace / Unum |
Conceptual Captions | 图像和描述 | 3 M | HuggingFace / Unum |
Arxiv | 标题和摘要 | 2 M | HuggingFace / Unum |
USearch + RDKit = 分子搜索
比较分子图和搜索相似结构是昂贵且缓慢的过程。 这可以被视为NP完全子图同构问题的一个特例。 幸运的是,存在针对特定领域的近似方法。 化学领域常用的方法是从SMILES生成结构,然后将其哈希为二进制指纹。 后者可以使用二进制相似度度量(如Tanimoto系数)进行搜索。 以下是使用RDKit包的示例。
from usearch.index import Index, MetricKind
from rdkit import Chem
from rdkit.Chem import AllChem
import numpy as np
molecules = [Chem.MolFromSmiles('CCOC'), Chem.MolFromSmiles('CCO')]
encoder = AllChem.GetRDKitFPGenerator()
fingerprints = np.vstack([encoder.GetFingerprint(x) for x in molecules])
fingerprints = np.packbits(fingerprints, axis=1)
index = Index(ndim=2048, metric=MetricKind.Tanimoto)
keys = np.arange(len(molecules))
index.add(keys, fingerprints)
matches = index.search(fingerprints, 10)
该方法被用于构建"USearch Molecules",这是最大的化学信息学数据集之一,包含70亿个小分子和280亿个指纹。
USearch + POI坐标 = GIS应用
类似于向量和分子搜索,USearch也可用于地理信息系统。 Haversine距离是现成可用的,但你也可以定义更复杂的关系,如考虑地球扁率的Vincenty公式。
from numba import cfunc, types, carray
import math
# 将维度定义为2,表示纬度和经度
ndim = 2
# 自定义度量的签名
signature = types.float32(
types.CPointer(types.float32),
types.CPointer(types.float32))
# WGS-84椭球参数
a = 6378137.0 # 长半轴(米)
f = 1 / 298.257223563 # 扁率
b = (1 - f) * a # 短半轴
def vincenty_distance(a_ptr, b_ptr):
a_array = carray(a_ptr, ndim)
b_array = carray(b_ptr, ndim)
lat1, lon1, lat2, lon2 = a_array[0], a_array[1], b_array[0], b_array[1]
L, U1, U2 = lon2 - lon1, math.atan((1 - f) * math.tan(lat1)), math.atan((1 - f) * math.tan(lat2))
sinU1, cosU1, sinU2, cosU2 = math.sin(U1), math.cos(U1), math.sin(U2), math.cos(U2)
lambda_, iterLimit = L, 100
while iterLimit > 0:
iterLimit -= 1
sinLambda, cosLambda = math.sin(lambda_), math.cos(lambda_)
sinSigma = math.sqrt((cosU2 * sinLambda) ** 2 + (cosU1 * sinU2 - sinU1 * cosU2 * cosLambda) ** 2)
if sinSigma == 0: return 0.0 # 重合点
cosSigma, sigma = sinU1 * sinU2 + cosU1 * cosU2 * cosLambda, math.atan2(sinSigma, cosSigma)
sinAlpha, cos2Alpha = cosU1 * cosU2 * sinLambda / sinSigma, 1 - (cosU1 * cosU2 * sinLambda / sinSigma) ** 2
cos2SigmaM = cosSigma - 2 * sinU1 * sinU2 / cos2Alpha if not math.isnan(cosSigma - 2 * sinU1 * sinU2 / cos2Alpha) else 0 # 赤道线
C = f / 16 * cos2Alpha * (4 + f * (4 - 3 * cos2Alpha))
lambda_, lambdaP = L + (1 - C) * f * (sinAlpha * (sigma + C * sinSigma * (cos2SigmaM + C * cosSigma * (-1 + 2 * cos2SigmaM ** 2)))), lambda_
if abs(lambda_ - lambdaP) <= 1e-12: break
if iterLimit == 0: return float('nan') # 公式未收敛
u2 = cos2Alpha * (a ** 2 - b ** 2) / (b ** 2)
A = 1 + u2 / 16384 * (4096 + u2 * (-768 + u2 * (320 - 175 * u2)))
B = u2 / 1024 * (256 + u2 * (-128 + u2 * (74 - 47 * u2)))
deltaSigma = B * sinSigma * (cos2SigmaM + B / 4 * (cosSigma * (-1 + 2 * cos2SigmaM ** 2) - B / 6 * cos2SigmaM * (-3 + 4 * sinSigma ** 2) * (-3 + 4 * cos2SigmaM ** 2)))
s = b * A * (sigma - deltaSigma)
return s / 1000.0 # 距离(公里)
# 使用示例:
index = Index(ndim=ndim, metric=CompiledMetric(
pointer=vincenty_distance.address,
kind=MetricKind.Haversine,
signature=MetricSignature.ArrayArray,
))
集成与用户
- ClickHouse:C++,文档。
- DuckDB:文章。
- Google:UniSim,RetSim 论文。
- LanternDB:C++,Rust,文档。
- LangChain:Python 和 JavaScript。
- 微软 Semantic Kernel:Python 和 C#。
- GPTCache:Python。
- Sentence-Transformers:Python 文档。
- Pathway:Rust。
引用
@software{Vardanian_USearch_2023,
doi = {10.5281/zenodo.7949416},
author = {Vardanian, Ash},
title = {{USearch by Unum Cloud}},
url = {https://github.com/unum-cloud/usearch},
version = {2.13.4},
year = {2023},
month = oct,
}