索引方法

如果没有索引,每次你在数据库中搜索“类似图片”或“相关文档”时,计算机必须进行暴力搜索(Flat Search / Brute Force),即把你的查询向量和库里的 1 亿个向量逐一对比计算距离。这在数据量大时是不可接受的

索引的作用,就是通过牺牲一点点精度(从 100% 降到 99%),换取几百倍甚至几千倍的搜索速度提升。 这被称为 ANN(Approximate Nearest Neighbor,近似最近邻搜索)

Elasticsearch (文本的王者): 倒排索引 (Inverted Index)

  • 原理: 就像书籍末尾的“索引页”。
  • 存储方式: 它不存“文章 1 -> 关键词”,而是存“关键词 -> 出现在哪些文章里”。
  • 例子: 单词 “Python” -> [Doc_1, Doc_5, Doc_9]。
  • 优势: 它是为了精确匹配 (Exact Match) 设计的。查找包含特定单词的文档,速度是 O(1) 级别的。
  • 是字面匹配,不是语义理解,虽然也有同义词库等技术,但是还是存在词汇鸿沟

IVF_FLAT倒排索引

和上面的不一样,上面是离散的哈希匹配,这个是连续的距离计算匹配

第一步:聚类(Clustering)—— 分桶

在建立索引时,系统会先用 K-Means 算法,把你的 1 亿个向量数据,按照空间距离聚成 $nlist$ 个簇(Cluster/Bucket)。

  • 比如把数据分成 1000 个桶。
  • 算出每个桶的中心点(Centroid)

第二步:归位(Assignment)

把每一个向量,贴上标签,扔进离它最近的那个桶里。

可以看出构建的很快,不需要额外占用空间,无需压缩距离计算是 100%精确的。

第三步:搜索(Searching)

当查询向量过来时:

  1. 找桶: 先算出查询向量和那 1000 个中心点的距离,找出最近的 $nprobe$ 个桶(比如最近的 5 个)。(耗时有点高)
  2. 暴力搜: 系统只加载这 5 个桶里的所有向量。
  3. 精算: 在这就这几个桶里进行暴力扫描(Flat Search),算出最终结果。

查询时间中等,虽然一下子过滤了很多,但是 桶内还是暴力搜。召回率不确定,有可能在隔壁桶。

核心参数:

  • nlist (桶的数量): 建索引时定好的。桶越多,每个桶里的数据越少,查询越快,但聚类耗时变长。
  • nprobe (探针数量): 查询时定的用来搜隔壁的。搜的桶越多($nprobe$ 越大),结果越准(Recall 高),但速度越慢。
    参数调优麻烦。查询时必须调 nprobe 来平衡速度和精度。

IVF_PQ (Product Quantization)

IVF_FLAT (Raw Data) 向量被分配到桶里后,系统直接把原始的 Float32 向量存进去。

  • 存储内容: 向量进桶之前,先经过 PQ(乘积量化) 压缩。

SPARSE_INVERTED_INDEX稀疏检索的索引

IVF_FLAT是基于“空间位置”的剪枝,可能会漏掉处于边界的最近邻
SPARSE_INVERTED_INDEX (WAND/MaxScore)是基于“分数上限”的剪枝。算出来最大可能分都超不过第十的分,那系统就直接跳过这个文档,根本不算具体的距离。

ANNOY

树索引,构建速度快,内存占用地,无法增量更新,必须重建索引。

HNSW图索引:

  • 核心原理: 你可以把它想象成城市的交通系统

    • 底层(第 0 层): 是所有数据点的集合,就像城市里密密麻麻的小街道,点与点之间通过近邻关系连接。
    • 上层(第 1, 2, 3… 层): 是高速公路或快速路。我们在上层只保留少量的关键节点(枢纽)。
      内存占用高,构建慢
  • 搜索过程: 当你要找一个目标时,你不会直接钻进小巷子(底层)乱找。

    1. 你先在最顶层(高速公路)快速跳跃,找到距离目标大概最近的那个“枢纽”。
    2. 然后降落到下一层(主干道),在那个枢纽附近更细致地找。
    3. 最后降落到最底层(小巷子),进行精确的近邻查找。
      查询对数复杂度很快,参数调优简单。
  • 优点: 搜索速度极快,召回率(准确度)极高,非常适合对性能要求高的实时搜索。

  • 缺点: 内存占用大(因为它要存储点与点之间的连接关系,像一张巨大的网),且构建索引(插入数据)的速度相对较慢。
    在内存里的物理形态:邻接表(Adjacency List)结构的变体

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    struct Node {
    int id; // 向量的唯一ID
    VectorPayload* data; // 指向实际向量数据的指针(可能是压缩后的数据)
    // 每一层的邻居列表 (HNSW 是分层的)
    // Level 0 是最底层,连接最密
    std::vector<int> neighbors_level_0;
    // Level 1 是上层,连接稀疏
    std::vector<int> neighbors_level_1;
    // ...
    };

    切入可能点,找邻居,计算相似值。从顶层到低层

量化压缩(常与HNSW搭配使用)用来海选。

  • 标量量化 (Scalar Quantization, SQ): 直接把浮点数四舍五入映射到整数区间。简单粗暴。

  • 乘积量化 (Product Quantization, PQ): 把一个长向量切成很多段,每一段用一个代表性的“聚类中心”来代替。这是一种更高级的压缩。128维分4段,每段取一个聚类中心作为一维。128维就压缩成4维了。

  • 优点: 极大地节省内存(这是昂贵资源),计算距离时速度飞快(整数运算比浮点运算快,当你把向量从 Float32 量化为 Int8 时,CPU 一个指令周期处理的数据量变成了原来的 4 倍。把数据从内存搬到 CPU 缓存也更快)。

  • 缺点: 会损失精度。如果压缩太狠,搜索结果可能就不准了。
    注意这里 用的不是通用寄存器是向量寄存器。AVX-512 指令集,它有一组巨大的寄存器(ZMM0 到 ZMM31),每个宽 512 位。

  • 当你发送一条 SIMD 加法指令(比如 VPADDD)时,CPU 不能 同时让 ZMM0 加 ZMM1,ZMM2 加 ZMM3…

  • 但是,它可以在一个指令周期内,把 ZMM0 里面的 16 个 32位整数,同 ZMM1 里面的 16 个 32位整数,一次性全部加完。

局部敏感哈希LSH

分类桶,让相似的东西,大概率掉进同一个桶(Bucket)里。当你要查一个向量时,先算出它的哈希值,看它掉进了哪个桶。然后只把那个桶里的向量拿出来对比,其他的忽略。但是在高维空间中,用哈希函数很难完美地把“语义相似”的东西切分得那么干净,导致要么漏掉结果,要么桶里数据太多。

相似度算法和数据库索引有什么关系

在构建一个 HNSW 索引时,来了一个向量 A,你要把它连到“最近”的邻居 B 上,谁是“最近”的?这取决于你用什么算法(尺子)。

  • 如果用 **欧氏距离 (L2)**:B 可能是坐标位置离 A 最近的点。
  • 如果用 **内积 (Inner Product)**:B 可能是和 A 方向一致且模长很大的点。
    索引在构建阶段(Build Phase),必须调用相似度算法计算几亿次,来决定谁和谁是邻居,谁应该放在谁旁边。

向量搜索遇上过滤筛选,如何选择最优索引组合?

  • 在向量数据库中执行预过滤,往往会显著增加系统的复杂度和优化难度。因为量搜索融合标量过滤其实是一个极其复杂的工程优化问题

  • 先做元数据过滤,再使用图索引算法,经常会导致图结构基本“失联”;可采用Alpha 策略(根据过渡率“智能切换”)、ACORN(故意多连距离远、但是属于不同属性分类,先保证图不断裂) 、动态选择邻居解决(临时查看被ban的邻居的邻居)、元数据感知索引(有意联属性相同的元数据)

  • 落地中,迭代式过滤,适用于高效处理复杂的元数据过滤逻辑,用于复杂/开销大的过滤条件;外部过滤可以使过滤逻辑在客户端侧执行,减少大量数据传输,适用高效处理海量 ID 过滤

过滤:停用词,词干提取器,