qinghuan
← 查找资料并核对引用 / 全部笔记

单元 14 / 查找资料并核对引用

P0 · 必会主线L2 · 进阶

向量检索:选择距离、建立索引并检查召回

先用逐项比较找出正确结果,再检验近似索引节省了多少时间、漏掉了多少相关内容。

本篇目录 · 按需要跳转

读完这篇,你能做什么

能先用精确搜索建立对照,再用相同查询比较近似索引找回了多少近邻、耗时多少,以及是否正确过滤无权读取的资料。

学习理由、准备要求与知识点

为什么学它

向量怎样生成、按什么距离比较、索引会漏掉哪些候选,要一起检查,才能解释为什么搜得准或快。

先读这些内容

知识点清单

  • 编码模型与维度配置
  • 欧氏距离、点积与余弦
  • 距离排序方向
  • 精确近邻搜索
  • HNSW 近似索引
  • 索引参数与召回代价
  • 权限过滤后结果不足
  • 向量版本重建和切换

运行前先确认完整算法使用 Java 17 标准库;JDK 源码基线为 OpenJDK 17 GA。示例是内存中的机制模型,外部服务接入需要另外配置和验证。

01先弄懂它是什么

文本变成向量后,检索就是从许多向量里找最接近查询的几项。先看清用什么标准:欧氏距离越小越近,点积或余弦一般越大越相似,而不同库返回的可能是距离,也可能是换算后的分数。如果排序方向写反,仍能返回 k 条记录,但拿到的可能恰好是最不相关的内容。

最容易核对的方法,是比较所有允许候选,找出真正最近的前 k 项,这叫精确近邻,也能作为测试基准。近似索引用图或聚类等方式减少搜索量,速度可能更快,但也可能漏掉真实最近项。因此要同时测耗时和召回。HNSW 的表现还受参数、数据分布和过滤条件影响,不能保证任意查询都变成对数时间。

索引还绑定编码模型、维度、距离度量和归一化方式。权限过滤必须在授权候选范围内生效,不能先把跨用户结果送入重排或生成再隐藏引用。过滤后不足k条是合法结果,应说明资料不足或调整授权范围内搜索,而不是放宽权限补齐数量。

文中这几个词,先认一认

精确近邻
按既定距离逐一比较所有允许的候选,再确定最接近的前 k 项。
近似召回
近似搜索找回了多少精确搜索的近邻,需要在固定的一组查询上比较。
有界堆
最多保留当前最好的 k 项,并把其中最差的一项放在堆顶,便于新候选到来时替换。

还有陌生词?到术语小抄里看一个具体例子 ↗

02看看它怎么工作

本题要从 n 个候选里选出距离最近的 k 个,距离用欧氏距离,距离相同时再按 ID 决定顺序。可以维护一份最多 k 项的保留名单,把名单里最差的候选放在最容易取出的位置;这样的结构叫最大堆。新候选更好,就替换当前最差者,否则略过。所有候选检查完后,只需把留下的 k 项排好,不必给全部 n 项排序。不过每个向量的距离仍然都算过,因此这里做的是精确查找,没有为了省事跳过部分候选。

比较器先按距离升序,再按ID升序定义“更好”,堆使用该比较器的反向次序。若只反转距离却忘记并列ID,边界上的保留对象可能不稳定。结果排序也必须使用同一个正向比较器,确保选择和展示规则一致。

距离逐维用hypot累计,减少直接平方的溢出风险;有限坐标相减仍可能溢出,程序对此明确拒绝。ID重复、维度不一致与非有限坐标在选择前检查,即使k为零也不让错误数据悄悄通过。本题是内存精确检索,不声称实现向量数据库或HNSW。

正在加载流程图…

图中状态只描述本篇可执行机制;业务调用仍需由外层应用负责。
查看流程图源码(Mermaid)
flowchart LR
  Q["查询向量与版本"] --> A["授权候选集合"]
  A --> D["精确距离计算"]
  D --> H["最多 k 项最大堆"]
  H --> S["按距离和 ID 排序"]
  S --> R["可复核近邻基线"]

按顺序看,每一步做了什么

  1. 固定模型版本、维度和距离排序方向。
  2. 先在可信身份下确定允许候选集合。
  3. 建立精确Top-k基线并定义并列规则。
  4. 用有界最大堆保留当前最佳候选,再正向排序输出。
  5. 比较近似索引召回、过滤后数量与尾延迟。

03对照实现代码

OpenJDK 17 PriorityQueue 源码 (新标签页)↗

OpenJDK 17 GA,jdk-17+35 · PriorityQueue.offer / poll / siftUp / siftDown

下方为原创 Java 教学片段,调用已核验的 JDK API 说明局部机制;不是该类原样源码,也不实现神经网络。完整程序见算法模块。

核心机制代码 · java
Comparator<Hit> bestFirst = Comparator.comparingDouble(Hit::distance)
        .thenComparing(Hit::id); // 距离相等时,ID给出稳定全序。
PriorityQueue<Hit> worstFirst = new PriorityQueue<>(bestFirst.reversed());
worstFirst.offer(candidate); // 内部上浮维持堆序。
if (worstFirst.size() > k) worstFirst.poll(); // 删除当前最差者,保留最好k项。

PriorityQueue以数组堆维护局部顺序,offer插入后上浮,poll移除根并下沉调整。默认根是比较器意义上的最小元素,所以本题反转比较器让最差候选成为根。

堆序只保证根与子树关系,不保证迭代器输出全局排序。不能直接遍历堆就声称结果从近到远,必须再排序或逐个按相应顺序弹出。

有界堆减少的是排序与保留成本,不能省略向量距离计算。本篇精确扫描每个允许候选,不把O(log k)堆操作错写成整体检索复杂度。

真实向量库还包含持久存储、事务、并发与近似索引。本例只用JDK集合实现应用层基线,源代码桥梁与数据库内核实现语言无关。

代码先调用谁,再调用谁

  1. 查询与候选向量校验
  2. 计算每项精确距离
  3. offer后用poll淘汰最差
  4. 保留最多k项
  5. 按距离和ID正向排序

04动手写一个例子

精确近邻的有界最大堆

输入查询向量、唯一ID候选和k,返回欧氏距离最小的最多k个ID,距离并列按ID升序。非法维度或非有限中间值拒绝;Java17程序运行固定自测。

输入 1
查询[0],a=[2],b=[1],c=[-1],k=2
输出 1
b,c
输入 2
空候选,k=3
输出 2
空

每次插入一个候选后,如果大小超过k就移除最差项。已经被移除的项至少有k个不差于它的已见候选,因此不可能进入当前最佳k项。

当新候选到来,只需与当前集合共同重新保留最佳k项,历史被丢弃项无需恢复。由归纳可知扫描完全部候选后,堆中正是全局最佳k项。

输出需要单独排序,因为堆只维护最差根。k大于候选数时自然返回全部;k为零返回空,但输入合法性仍检查,便于保持一致接口契约。

为什么这样写是对的处理前i个候选后,堆保存其中按距离升序、ID升序排列的前min(i,k)项,根是这些保留项中最差者。

时间 O(nd+n log(k+1)+k log(k+1))额外空间 O(n+k)

d为维度,n为候选数;本程序为拒绝重复ID额外使用O(n)集合,堆本身O(k)。若上游已经保证唯一ID,可省去该集合。ID比较另有字符串长度成本。

保存为 Main.java,运行 javac --release 17 -encoding UTF-8 Main.java,再运行 java Main。程序包含固定输入自测,断言失败时会抛出错误。

展开收起代码 · Main.java · 完整可运行程序Java · 36 行

长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。

Main.java · 完整可运行程序 · Java
import java.util.*;
public class Main {
    record Point(String id,double[] v){} record Hit(String id,double distance){}
    static double distance(double[] a,double[] b){
        if(a.length==0||a.length!=b.length)throw new IllegalArgumentException("dimension");
        double r=0;for(int i=0;i<a.length;i++){
            if(!Double.isFinite(a[i])||!Double.isFinite(b[i]))throw new IllegalArgumentException("coordinate");
            double delta=a[i]-b[i];if(!Double.isFinite(delta))throw new IllegalArgumentException("difference overflow");
            r=Math.hypot(r,delta); // 逐维稳定累计欧氏长度。
        }
        if(!Double.isFinite(r))throw new IllegalArgumentException("distance overflow");return r;
    }
    static List<String> top(double[] q,List<Point> points,int k){
        if(k<0)throw new IllegalArgumentException("k");distance(q,q);
        Comparator<Hit> order=Comparator.comparingDouble(Hit::distance).thenComparing(Hit::id);
        PriorityQueue<Hit> heap=new PriorityQueue<>(order.reversed());Set<String> ids=new HashSet<>();
        for(Point p:points){
            if(p.id()==null||!ids.add(p.id()))throw new IllegalArgumentException("duplicate id");
            Hit hit=new Hit(p.id(),distance(q,p.v()));
            heap.offer(hit);if(heap.size()>k)heap.poll(); // 根是最差者,淘汰后仍保留最佳k项。
        }
        List<Hit> sorted=new ArrayList<>(heap);sorted.sort(order); // 堆迭代不是排序结果。
        return sorted.stream().map(Hit::id).toList();
    }
    static void eq(List<String> e,List<String> a){if(!e.equals(a))throw new AssertionError(a);}
    public static void main(String[] args){
        List<Point> p=List.of(new Point("a",new double[]{2}),new Point("c",new double[]{-1}),new Point("b",new double[]{1}));
        eq(List.of("b","c"),top(new double[]{0},p,2));
        eq(List.of(),top(new double[]{0},List.of(),3));
        eq(List.of(),top(new double[]{0},p,0));
        eq(List.of("b","c","a"),top(new double[]{0},p,10));
        try{top(new double[]{0},List.of(p.get(0),p.get(0)),1);throw new AssertionError();}catch(IllegalArgumentException expected){}
        try{top(new double[]{0,1},p,1);throw new AssertionError();}catch(IllegalArgumentException expected){}
        System.out.println("PASS 6 tests");
    }
}

这些特殊情况也要试一试

  • 距离相同必须在选择和输出两处使用一致ID规则。
  • k为零仍校验数据,不把非法向量隐藏。
  • 有限输入相减或最终长度仍可能溢出,明确拒绝。
进阶选读 · 后续再读:HNSW参数与过滤召回

场景数据量增大,精确扫描无法满足延迟预算。

做法用精确结果作为gold近邻,对不同构建和搜索参数测Recall@k与延迟;加入真实租户过滤,观察候选不足,避免只测无过滤理想数据。

复杂度查询复杂度依图结构、搜索宽度和数据分布而变;经验加速不等于任意数据的对数时间最坏界。

好处与代价搜索宽度提高通常增加计算并可能改善召回;内存、构建时间、更新行为与业务质量需要共同权衡。

05项目里怎么用

迁移编码模型时建立独立索引版本,抽样核对维度和归一化,再用同一查询集比较。新查询编码器与旧语料混用时,系统可能仍返回看似正常分数,因此上线前不能只依赖无异常启动或健康接口。

权限过滤应贯穿候选检索、重排和引用展示。若近似检索后再过滤导致不足k条,可在授权条件内增加搜索范围,但不能删除租户约束。缓存键也应包含身份范围和索引版本,避免把别人的Top-k直接复用。

报告性能时区分Embedding生成、数据库检索、重排与总耗时,保留失败与尾延迟。精确堆基线让错误更容易定位:若两种方式向量和过滤一致却差异大,先检查距离方向、索引参数与并列处理。

容易踩的坑

  • 把距离越小越好与相似度越大越好写反。
  • 直接迭代PriorityQueue当作有序结果。
  • 只报告堆操作O(log k),忽略全部距离计算。
  • 过滤不足时放宽权限以填满k条。

几种做法,怎么选

表格有四列,窄屏可左右滑动查看代价和适用条件。

方案优势代价与限制适用条件
精确扫描加堆结果可证明,适合对照每次读取全部候选小语料与质量基线
近似索引大数据下通常更快可能漏近邻且需调参有召回与延迟评测的服务
全量排序实现直观,可得到全部次序多余排序与内存成本需要完整排名或小规模调试

06检验是否读懂了

先自己想、自己写,再展开答案核对。场景题是假设的练习情境;遇到还没学过的内容,可以做完基础后再回来。

先用自己的话解释

01堆为什么反转比较器?

展开参考答案与推导

希望保留项中最差者在根,超限时可以快速淘汰;最终展示再用正向比较器排序。

02近似召回与答案正确率相同吗?

展开参考答案与推导

不同。近似召回衡量与精确近邻的覆盖,精确近邻本身也可能缺乏回答证据;还需任务相关性和答案支持评估。

03维度检查通过能证明编码兼容吗?

展开参考答案与推导

不能,还要核对模型、预处理、归一化和版本;数值形状一致只是必要条件。

换个情况,还会处理吗

01结果顺序偶尔变化,应先查什么?

展开参考答案与推导

检查并列距离是否有稳定ID规则、堆是否直接迭代、上游数据是否重复;先固定这些确定性因素再看近似索引波动。

02过滤后只有两条而要求五条,怎么办?

展开参考答案与推导

返回实际授权结果并标记不足,或在同一授权范围内提高搜索宽度;不越权补齐。

03更换距离后质量下降如何定位?

展开参考答案与推导

确认向量训练目标、归一化和索引操作符匹配,再用精确基线复核排序方向与数值范围。

写代码试一试

01k超过数据量时应返回什么?

输入 / 条件
三项候选,k=10
输出 / 预期
全部三项按规则排序

提示 无需制造占位候选。

展开参考答案与推导

堆不会超过候选数量,最终排序全部已保留项;结果长度为min(n,k)。

02为什么重复ID要拒绝?

输入 / 条件
同ID两个不同向量
输出 / 预期
非法输入

提示 ID决定结果和引用身份。

展开参考答案与推导

重复会让结果身份不明确,也可能让同一资料占多个名额;先在导入或本函数边界统一去重策略。

03如何核对算法与全量排序?

输入 / 条件
小型固定随机向量集
输出 / 预期
Top-k完全一致

提示 建立朴素对照。

展开参考答案与推导

对同一允许集合计算全部距离并按同一比较器排序取前k,与堆结果比较;这能发现方向和并列处理错误。

读完之后,独立完成

课后作业:自己做一遍

本篇共 12 道作业,先完成 9 道基础与综合任务,再按前置知识挑战进阶题。其中包含 2 道思维拓展,试着换个条件,看看原来的办法是否还管用。不附参考答案。

这次练什么

能先用精确搜索建立对照,再用相同查询比较近似索引找回了多少近邻、耗时多少,以及是否正确过滤无权读取的资料。

本篇重点加练

过滤与版本切换会改变可用候选,重点验证质量报告中的查询资格和语料边界。

  • 权限过滤后结果不足
  • 向量版本重建和切换

重点内容会多练几次。先说清楚怎么做,再试试空数据、出错或条件改变时会发生什么。

先自己做,卡住时记下问题,再回来查。问答题写清“为什么、什么时候适用”;动手题留下输入、输出和运行步骤,方便下次重做。

基础必做

  1. 作业 01原理问答

    为同一组二维向量写出编码配置和三种距离的计算任务,说明比较前必须固定的归一化与版本条件。

    提交内容
    一份独立书面回答或标注图,包含自造例子、概念边界及判断依据。
    怎样算完成
    • 逐项回应题目要求,区分事实、假设与个人判断。
    • 能用自己的例子解释适用条件,记录尚未弄懂的点。

    练到的知识点编码模型与维度配置 · 欧氏距离、点积与余弦

  2. 作业 02代码推演

    自造5个点和1个查询,分别排序距离与相似度,记录排序方向;实现穷举基线并与手算逐项对照。

    提交内容
    手工推演记录,写明输入约定、中间状态、最终判断;可运行时附程序对照记录。
    怎样算完成
    • 推演包含中间过程,数据形状、单位、顺序或状态前后一致。
    • 主动检查至少一个边界或异常条件,能解释手算与运行差异。

    练到的知识点距离排序方向 · 精确近邻搜索

  3. 作业 03原理问答

    设计一次索引切换的查询资格表,同时记录用户过滤条件、语料版本与查询编码版本,说明哪些运行可以纳入同一份质量报告。

    提交内容
    概念或合同说明、关系图与自造案例,明确事实、假设和适用条件。
    怎样算完成
    • 关键概念有明确边界,能用案例检验声明。
    • 未获证据支持的假设单独标注。

    练到的知识点权限过滤后结果不足 · 向量版本重建和切换

综合应用

  1. 作业 04面试场景

    数据量扩大后准备采用HNSW。提出一个参数对照实验,明确召回基准、构建成本、查询延迟和重复运行条件。

    提交内容
    场景分析与决策记录,包含信息缺口、约束、备选方案和验证计划。
    怎样算完成
    • 方案回应给定约束,列出至少一种替代方案及代价。
    • 每项关键结论说明所需证据,不能核实的部分明确标注。

    练到的知识点HNSW 近似索引 · 索引参数与召回代价

  2. 作业 05编程实践

    用两个用户的微型向量库模拟过滤后结果不足,再切换编码版本;实现查询与索引版本校验,记录补足候选和拒绝不兼容请求的策略。

    提交内容
    可复现的最小程序或实验材料、自写测试、环境说明与运行记录;题目允许纸面实验时提交完整案例表。
    怎样算完成
    • 提交物按说明可以重现;测试覆盖正常、边界与失败场景。
    • 运行证据与断言能核验声明的行为,注明模拟与真实执行范围。

    练到的知识点权限过滤后结果不足 · 向量版本重建和切换

  3. 作业 06排错实验

    故意把“越小越近”和“越大越近”的排序规则对调,构造最小反例并核验检索排名,提交故障定位过程。

    提交内容
    最小故障输入、复现步骤、观测记录、定位依据及回归用例。
    怎样算完成
    • 其他人可按记录复现故障,并区分现象与已验证原因。
    • 定位过程有观测依据,回归用例可以再次暴露原问题。

    练到的知识点欧氏距离、点积与余弦 · 距离排序方向 · 精确近邻搜索

  4. 作业 07编程实践

    实现双索引的本地查询路由模拟器,给不同用户设置不同可见文档;在切换过程中记录每次实际命中的版本、过滤后候选数与拒绝原因。

    提交内容
    可复现的小程序或题目允许的离线实验、自写测试、输入和运行记录。
    怎样算完成
    • 输入、环境和运行过程可重现,测试覆盖声明的重点条件。
    • 验证目标由合同确定,模拟结果不冒充真实服务证据。

    练到的知识点权限过滤后结果不足 · 向量版本重建和切换

  5. 作业 08排错实验

    将一份权限删除事件只应用到新索引、另一份版本更新只应用到旧索引,用固定查询回放检验切换验收能否识别差异。

    提交内容
    最小故障序列、观测记录、原因核对与可再次触发原问题的测试。
    怎样算完成
    • 故障可以稳定重现,现象与已验证原因分开。
    • 观测记录足以判断声明的合同是否被违反。

    练到的知识点权限过滤后结果不足 · 向量版本重建和切换

  6. 作业 09面试场景

    切换后某类用户的结果数量骤降,整体召回指标却未变化。提出按用户权限范围与版本分层的排查方案,区分语料变化与检索策略变化。

    提交内容
    场景评审记录,包含约束、备选方案、未决信息和验证计划。
    怎样算完成
    • 方案回应题目约束,取舍能对应到可核验的事实。
    • 给出失败或条件变化时的检查方式。

    练到的知识点权限过滤后结果不足 · 向量版本重建和切换

进阶挑战 · 掌握前置后选做

  1. 作业 10设计评审

    设计百万量级索引升级方案,但只在本地小数据验证切换流程;分别列出已验证逻辑与尚需真实规模测量的性能项。

    提交内容
    设计评审文档或对照实验报告,包含前置条件、方案取舍、验收证据与尚未验证的范围。
    怎样算完成
    • 比较使用一致条件与事先声明的标准,记录失败与未知结果。
    • 说明方案的适用前提、未覆盖风险及后续验证方法。

    练到的知识点编码模型与维度配置 · HNSW 近似索引 · 向量版本重建和切换

思维拓展

先写出你的猜想,再用例子或实验验证。答案可以不同,但要说清依据、适用情况,以及还有什么没弄懂。

  1. 作业 11思维拓展 · 反例构造

    构造“新旧索引对全量语料的召回相同,所以切换对所有用户等价”的反例,给出最小权限与语料配置。

    提交内容
    具体反例、原主张的适用前提、复现或逻辑证明及结论边界。
    怎样算完成
    • 反例满足原主张所声称的前提,确实针对其结论。
    • 说明一个反例能推翻什么、不能进一步证明什么。

    练到的知识点权限过滤后结果不足 · 向量版本重建和切换

  2. 作业 12思维拓展 · 条件变化

    若索引更新由批量停机切换改为持续增量同步,重新定义查询一致性、候选完整性与回滚验收,说明哪些保证需要放宽或增加成本。

    提交内容
    变更前后条件表、重新评审的方案、检验用例与新增代价。
    怎样算完成
    • 只改变明确列出的条件,比较口径保持一致。
    • 修改方案说明收益、代价与仍未验证的范围。

    练到的知识点权限过滤后结果不足 · 向量版本重建和切换

完成后,再检验一次

给每题标记“独立完成 / 查阅后完成 / 尚未完成”。查过资料或没做出的题,记下卡在哪里。隔一天换一组输入再试,能解释结果和出错原因,再把这个知识点标为掌握。

继续阅读

先看能帮你完成当前练习的部分。例子跑通后,再读更深入的解释。

打开资料库,找下一份学习材料 ↗