正在加载文章,请稍候…
十万文档、384 维向量,前端怎么检索?从 IndexedDB 与余弦遍历到完整检索架构
内容提要
从“IndexedDB 存向量、前端算余弦”的面试追问出发,计算十万条 384 维向量的真实成本,拆解主线程、Worker、ANN、向量数据库、权限过滤与检索评估,给出有工程边界的完整回答。
面试官问:“前端怎么实现文档检索?向量数据库用过哪个?”候选人回答:“用 IndexedDB 存向量,前端算余弦。”面试官继续追问:“十万文档、384 维向量,你每次搜索遍历,主线程不卡死?”
这道题考察的是你能否把一个演示方案放进真实规模、设备约束和业务要求中分析。IndexedDB 可以存向量,浏览器也可以计算相似度;问题在于存储、索引、计算调度和完整检索服务分别由谁承担,以及你能拿出什么证据说明方案适用。
应当先承认风险:在主线程中每次全量读取并遍历十万条向量,会有明显的交互阻塞与内存风险。但不能仅凭“十万、384 维”就断言任何浏览器都必然卡死,更不能认为“加个 Worker”已经解决容量、延迟、召回和权限问题。
下面从成本计算、浏览器机制、ANN 索引、服务端架构和面试表达逐层展开。文中的容量是可复算的理论值;延迟目标、参数和候选数量均是设计示例,没有宣称对某台设备或数据库完成了性能实测。
一、面试官真正考察的七件事
| 考察点 | 追问的实际含义 | 回答应体现什么 |
|---|---|---|
| 概念边界 | 你是否把能存向量当成向量检索系统? | 区分存储、近邻索引、计算内核和服务治理 |
| 规模意识 | 数据扩大后会怎样? | 估算 N、维度、字节数、分块倍数与查询成本 |
| 浏览器机制 | 异步 API 是否代表计算不阻塞? | 理解主线程、Worker、消息传递和内存复制 |
| 算法与数据结构 | 必须每次与所有向量比较吗? | 精确扫描、Top-K、HNSW、IVF 与量化的取舍 |
| 架构判断 | 为什么放前端,何时应放后端? | 结合离线、隐私、终端能力、并发与维护成本 |
| 检索质量 | 搜得快就算做好了吗? | 理解切块、模型一致性、混合召回和评估 |
| 工程真实性 | 你究竟用过什么,做过哪些验证? | 如实描述职责、版本、数据规模、指标与限制 |
如果只回答“用 Web Worker 就不会卡”,解释了计算位置,尚未解释为什么搜索足够快。如果只说“换成某向量数据库”,给出了产品名称,尚未解释数据如何进入系统、权限如何参与检索、质量怎么验收。
二、先澄清条件:前端做检索,不等于全部在浏览器里完成
- 需求是哪一种:标题匹配、全文关键词搜索、语义搜索,还是带引用的 RAG 问答?普通关键词搜索未必需要向量。
- 十万指文档数,还是切块后的向量数?每篇长度、平均块数、语言和附件类型是什么?
- 是否要求离线?数据是否必须留在本机?是否允许查询文本发送到服务器或模型服务?
- 目标终端是办公桌面、低配手机,还是受控设备?能接受多大的下载量和常驻内存?
- 是单用户私人资料,还是多人共享、持续更新、带文档级权限的企业知识库?
- 延迟、吞吐和质量目标是什么?冷启动、热查询和首次模型下载是否分别有预算?
这些问题直接改变方案。个人离线工具可以优先考虑端侧索引;多人在线知识库通常更适合由后端承担共享检索,前端负责查询交互、取消、结果呈现和引用导航。即使后端负责检索,前端也需要理解接口语义、状态和性能预算。
三、先算账:十万条 384 维向量到底多大
1. 先按“一篇文档一条向量”建立下界
N = 100,000
d = 384
Float32 每维 = 4 bytes
向量分量数 = N × d = 38,400,000
原始向量字节数 = N × d × 4 = 153,600,000 bytes
= 153.6 MB(十进制)
≈ 146.48 MiB(二进制)这个值只包含紧凑存储的 Float32 向量,不包含文本、标题、文档 ID、块 ID、权限元数据、ANN 图结构、数据库记录开销、模型权重和运行时工作区。普通 JavaScript 数组和对象还会有额外成本,不能按 Float32Array 的原始字节数估算整页内存。
2. 真正需要数的是 chunk,而不只是 document
假设每篇平均切成十个块,十万篇文档就可能变成一百万条向量。此时仅 Float32 数据就是 1,536,000,000 bytes,即 1.536 GB,约 1.43 GiB。这是用于说明分块影响的假设,不是固定分块比例。
长文档通常需要分块才能定位具体内容,但块大小、重叠和数量必须结合文档结构、模型输入限制和检索质量决定。不能为降低向量数量,把长文档粗暴压成一条向量后就认定质量仍然成立。
3. 每次全量计算的成本
精确扫描所有向量:O(N × d)
只计算十万次 384 维点积:
乘法 = 100,000 × 384 = 38,400,000 次
加法 = 100,000 × 383 = 38,300,000 次
还没计入:
读取/解码、范数处理、Top-K 选择、消息通信和结果渲染。紧凑 Float32 矩阵的一次完整逻辑扫描涉及约 153.6 MB 向量数据;实际物理内存流量受缓存与实现影响。十次完整扫描的逻辑数据量约为 1.536 GB,不能直接把这个值转换成某台设备的耗时。
同步计算持续占用主线程时,输入、布局和绘制等工作会被推迟。现代桌面可能很快完成某些优化扫描,低端设备、对象化数据或重复查询则可能明显变慢,必须以目标环境实测判断。
Long Tasks API 将占用 UI 线程至少 50 毫秒的任务列为长任务;50 毫秒是该指标的定义,不是“低于它就一定流畅”的承诺。监控接口还需要做支持检测。参见 MDN:PerformanceLongTaskTiming。
4. Top-K 也有代价
把所有分数收集后全量排序,需要额外的 O(N log N) 排序时间和 O(N) 结果空间。只要前 K 条时,可以用大小为 K 的最小堆,将选择部分控制在 O(N log K),最终仅对堆中结果排序。整体仍包含 O(Nd) 的距离扫描,Top-K 优化不会消除这部分成本。
四、IndexedDB 能存向量,但不原生提供向量近邻检索
IndexedDB 是浏览器端结构化数据存储 API,支持对象仓库、主键、索引和键范围访问,也可在 Worker 中使用。它适合保存向量块、元数据及索引快照。参见 MDN:IndexedDB API。
它的常规索引用于按键查找和范围筛选,不会因为你存了一个 384 维数组,就自动获得余弦距离 Top-K、HNSW 或 IVF。即使逐维建立键索引,也不能等价替代高维近邻搜索。
| 能力层 | 它负责什么 | 一个端侧方案中的实现 |
|---|---|---|
| 持久化 | 刷新或重启后保留可恢复数据 | IndexedDB 中的二进制分片与 manifest |
| 近邻索引 | 减少需要计算距离的候选 | 经过验证的 HNSW / IVF 实现,或精确扫描基线 |
| 数值计算 | 高效执行点积或距离计算 | TypedArray、适用的 WASM/SIMD 内核 |
| 执行调度 | 避免重计算占用 UI 主线程 | Dedicated Worker、队列与取消策略 |
| 检索服务 | 版本、更新、权限、质量与可观测性 | 应用自身协议,或后端检索服务 |
准确的项目描述可以是:“我做的是 IndexedDB 持久化、Worker 中精确向量扫描的端侧原型。”如果后来接入 ANN,再说清楚使用了哪个实现、怎样构建和恢复索引。这样既承认工作成果,也保留清晰的适用边界。
五、完整文档检索链路:相似度计算只是其中一步
文档入库:
采集/上传
→ 解析文本(扫描件可能需要 OCR)
→ 清洗、按结构切块
→ 生成 chunk ID、文档版本和来源定位
→ 生成 embedding
→ 写入向量、元数据、权限信息
→ 构建/更新索引并发布可检索版本
查询:
用户输入
→ 查询处理与 embedding
→ 在授权范围内检索候选
→ 可选:与关键词召回融合
→ 可选:重排、按文档去重或分组
→ 返回片段、来源、位置和版本
→ 前端展示与跳转
RAG 问答会在检索之后继续:
选择证据 → 组装上下文 → 生成回答与引用。检索可以独立存在,不必每次调用大模型生成答案。RAG 则还需要检查引用是否支持结论、上下文是否过期,以及是否把无权内容送给生成模型。
向量契约需要明确到什么程度
- 模型标识与版本:文档和查询必须使用相互兼容的向量空间,维度相同并不能证明可比较。
- 编码方式:有些模型对 query 与 document 使用不同提示前缀或编码入口,应遵循模型约定。
- 维度、dtype、相似度度量、归一化和量化方式:这些必须与索引配置一致。
- 数据质量:拒绝 NaN、Infinity、错误维度;余弦相似度要明确处理零向量。
- 可追溯性:保留 docId、chunkId、sourceVersion、位置、内容哈希和权限范围。
384 维只是向量长度,不能据此判断检索效果、中文能力或模型身份。模型替换、切块规则变化和文本清洗变化都可能需要重建索引或重新评估。
六、小规模端侧方案:先建立正确且可测量的基线
1. 使用连续数据,并避免每次搜索读取全库
可以把一批向量连续保存在 Float32Array 中,用单独数组保存行号到 chunkId 的映射。启动时分批加载必要分片,让 Worker 持有热数据或索引;查询时只发送查询向量和过滤条件,返回少量 ID 与分数。
每次按十万条记录逐条读取,会增加异步请求、反序列化和对象管理成本。一次 getAll 虽然减少了读取调用,也可能造成较高峰值内存。应根据工作集采用分片、懒加载或受控预加载,并分别测量冷启动与热查询。
2. 归一化后,余弦可以转成点积
cos(q, x) = (q · x) / (||q|| × ||x||)
若 q、x 均为非零向量,且都已经 L2 归一化:
cos(q, x) = q · x
对单位向量:
||q - x||² = 2 - 2(q · x)文档向量可以在建库时归一化,查询向量每次归一化一次,避免每次比较重复计算文档范数。单位向量的内积与平方欧氏距离存在上述关系,可用于一致的排序;实现仍需明确返回的是相似度还是距离。参见 Faiss:MetricType and distances。
归一化和紧凑内存布局主要降低常数成本;它们没有改变全量扫描的 O(Nd)。量化后的表示也不能未经验证就直接假定仍然保持单位范数与相同排序。
3. Worker 解决执行位置,不负责减少比较次数
Dedicated Worker 可以执行 CPU 计算并访问 IndexedDB,避免这段同步计算直接占用 UI 主线程。多个线程仍会共享设备 CPU 和内存带宽,Worker 不会使计算、功耗和内存成本消失。参见 MDN:Using Web Workers。
不要把 async、Promise 或异步 IndexedDB 请求误认为数值循环自动并行。代码在主线程恢复执行后,里面的同步 for 循环仍然占用主线程。React 的 useMemo 和 useTransition 也不能把任意同步扫描自动迁移到后台线程。
4. 数据尽量只驻留在必要的位置
普通 postMessage 对数据通常使用结构化克隆。传输大型 ArrayBuffer 时,可以通过 transfer list 转移所有权,避免额外复制;发送端的原缓冲区会被分离。TypedArray 应传输其 backing buffer,而非把 TypedArray 本身放入 transfer list。参见 MDN:Transferable objects。
// 一次性初始化的数据传输示例。
// vectors 为准备交给 Worker 独占的 Float32Array。
// Worker 必须实现 init/search 协议;本段不是完整检索引擎。
worker.postMessage(
{
type: "init",
dimension: 384,
vectorBuffer: vectors.buffer,
rowCount: vectors.length / 384
},
[vectors.buffer]
);
// 转移后不要继续读写发送端的 vectors。
// 更常见的做法:让 Worker 自己从 IndexedDB 加载向量分片。还要检查 JS → WASM 是否再次复制、是否同时保留解压缓冲与索引内存、多标签页是否各驻留一整份,以及重建时新旧索引是否短时并存。避免一份 153.6 MB 数据在不同层重复存活,往往比优化一小段乘法更有意义。
SharedArrayBuffer 可以共享内存,但普通网页使用它需要安全上下文与跨源隔离,且要设计同步规则。不应把开启它视为所有项目都能立即采用的优化。参见 MDN:SharedArrayBuffer。
5. 忽略旧结果与中断旧计算,是两种能力
前端为每次查询分配递增 requestId,只接收当前请求的结果,可以防止旧结果覆盖新输入。但若 Worker 仍在执行旧的完整扫描,新查询仍会排队。
向正在同步执行长循环的 Worker postMessage 一个 cancel 消息,消息处理器通常要等该任务让出执行权后才能运行。取消需要分块扫描并让出任务队列、支持中断检查的原生/WASM 接口、适用的共享取消标志,或在可接受重初始化代价时终止 Worker。
循环里仅 await Promise.resolve() 主要让出到微任务,并不等于给普通消息任务或渲染提供充分执行机会。分块方案需要有意识地让出任务队列,并在目标环境验证调度开销。参见 MDN:Microtasks 与运行时。
调度伪代码(不依赖某个具体 ANN 库):
主线程:
输入变化 → 防抖或显式触发
生成新的 requestId
发送 search(requestId, queryVector, indexVersion)
仅接收最新 requestId 且版本匹配的结果
Worker:
保留“正在执行的任务 + 最新一条待执行任务”
新查询覆盖尚未执行的旧查询
对可分块任务:
扫描一批候选 → 检查失效标记 → 让出任务队列
对不可中断任务:
有界排队,任务完成后丢弃过期结果
返回少量 chunkId、score 和版本
退出或身份变化:
清理相关任务、引用、缓存和展示状态。防抖能减少查询次数;分块能改善调度;有界队列能避免积压。三者都不能代替更合适的索引,也不应通过把等待时间越设越长来掩盖检索本身的性能问题。
七、数据增长后:ANN 如何减少搜索工作量
ANN 是 Approximate Nearest Neighbor,即近似最近邻检索。核心思路是减少要做完整距离比较的候选,以可评估的召回损失换取速度或资源收益。近似算法、压缩和硬件加速承担不同职责,不要混为一个“优化开关”。
| 方案 | 如何工作 | 主要代价与适用判断 |
|---|---|---|
| Flat 精确扫描 | 对所有候选计算距离 | 实现简单,可作为近邻真值基线;O(Nd),适合较小或过滤后很小的集合 |
| HNSW | 利用分层邻近图导航搜索 | 通常有良好的延迟与召回折中;需要图内存、构建时间和参数评估 |
| IVF | 先分配到粗聚类,再搜索部分倒排桶 | 依赖代表性训练数据与探测桶数;分布变化和召回需要关注 |
| PQ / 标量量化 | 用更紧凑编码表示向量或近似距离 | 降低存储或带宽,可能损失排序精度;训练、解码和精排成本依实现而异 |
| WASM / SIMD / GPU | 加速计算内核 | 不自动改变算法复杂度;需要兼容性、数据搬运与端到端收益验证 |
HNSW 的原始设计通过多层图组织近邻搜索。实际查询开销与数据分布、图参数和召回要求有关,不应把“平均表现较好”说成对所有输入都保证 O(log N),更不能承诺完全等价于精确 Top-K。参见 HNSW 原论文。
以 HNSW 为例,M 影响图连接与内存,efConstruction 影响构建搜索范围,efSearch 影响查询候选范围。一般需要在延迟、内存、构建代价和召回之间调参,具体命名、约束及删除能力取决于实现。
Faiss 提供 Flat、HNSW、IVF、PQ 等不同索引,其索引表明确区分精确扫描与非穷举方法,并列出不同的内存开销。HNSW 的图结构会在原始向量之外增加存储,不能把 ANN 当成必然节省内存的方案。参见 Faiss:Indexes。
如果原始十万条向量全部转成 Float16,纯分量存储约 76.8 MB;标量量化成每维一个字节则约 38.4 MB。这只是理论分量大小,尚未包含 scale、zero point、索引和工作区,也不代表直接更换数组类型就能正确量化。
端侧使用 ANN,需要选择确实支持目标浏览器的实现,核实 WASM 构建、索引序列化、ID 映射、更新删除、内存上限和许可。Faiss 的原生库不能仅凭浏览器里写一个 import 就直接使用;原生能力、WASM 移植和服务端调用是不同接入方式。
十万条并不是必须切换 ANN 或后端的硬阈值。若低频查询、设备受控、精确扫描实测达标,Flat 可能更简单;若切块后百万条、高频查询、终端内存有限或数据持续变化,方案就需要重新评估。
八、企业在线场景:把共享检索能力放到服务端
前端:
输入与搜索条件
→ 已认证的业务检索 API
→ 加载/失败/取消/结果列表/来源跳转
业务后端:
认证身份
→ 派生租户与文档可访问范围
→ 查询 embedding
→ 带授权范围的向量/关键词召回
→ 必要的重排与文档聚合
→ 返回少量有权片段、来源与版本
检索与数据层:
PostgreSQL + pgvector,或专用向量数据库
+ 原文/文档元数据存储
+ 更新队列、索引版本、监控与恢复。服务端共享索引可以减少每个客户端重复下载、构建与驻留全库数据的成本,方便统一更新与治理。但它也引入网络、服务成本与查询隐私问题,必须按产品约束选择。
前端通常不应直接携带数据库管理密钥或模型服务秘密。授权范围由服务端根据可信身份计算,不能接受客户端自行声明“我是某租户管理员”。向量相似度、租户过滤和文档权限是不同概念。
检索接口可以接受 query、用户可选择的筛选项和受限 topK;响应返回 requestId、chunkId、docId、snippet、sourceVersion、位置与必要分数。不要每次把全量向量或全量正文发回浏览器。查询日志与摘要同样应按敏感数据策略处理。
如果服务端将候选发给外部 reranker 或生成服务,应在发送前完成必要的权限约束。检索接口返回前再核验权限可以作为防御补充;不能把无权文本已经外发后的删除,当作完整的授权控制。
九、“向量数据库用过哪个?”应该怎样回答
| 技术 | 准确定位 | 选型时应讲清楚 |
|---|---|---|
| IndexedDB | 浏览器端结构化持久化 | 离线数据与索引快照如何存;它本身没有通用向量 ANN 查询 API |
| Faiss | 向量相似度搜索与聚类库 | 选了什么索引,如何封装持久化、更新、权限和服务接口 |
| PostgreSQL + pgvector | 关系数据库加向量扩展 | 复用现有数据与事务体系;关注执行计划、过滤、索引与资源竞争 |
| Qdrant | 提供向量检索与元数据等能力的向量数据库 | 集合、payload、过滤索引、检索参数、部署与数据一致性需求 |
pgvector 支持精确检索以及 HNSW、IVFFlat 等近似索引。已有 PostgreSQL 的项目可以将它列为候选,但最终应基于业务过滤条件、数据量、并发与运维成本实测。参见 pgvector 官方项目。
Qdrant 的检索可结合 payload 与 ID 条件,官方建议为频繁过滤的字段建立 payload 索引。产品提供这些能力,并不自动证明某个项目已正确配置权限或达到性能目标。参见 Qdrant:Filtering。
回答时应给出自己真实用过的技术、版本、职责、索引、数据规模与指标。如果只做过浏览器原型,可以直接说:“生产向量数据库我还没有实际运维过;我实现过端侧精确检索,下面说明它的约束,以及在线场景的选型依据。”
如果只阅读或试验过某产品,就明确说“做过验证”或“了解其机制”。不要为了填上一个产品名,把阅读经验包装成生产经验。下面的 SQL 是教学示例,不能拿来宣称个人已有某项目实践。
十、以 pgvector 为例:会建索引,还要懂查询与过滤
以下示例以 PostgreSQL 与 pgvector 为前提,展示 384 维余弦检索的基本结构。真实项目还需要迁移、批量导入、连接管理、文档级授权和监控等完整设计。
CREATE EXTENSION IF NOT EXISTS vector;
CREATE TABLE doc_chunks (
id BIGSERIAL PRIMARY KEY,
tenant_id BIGINT NOT NULL,
doc_id BIGINT NOT NULL,
source_version TEXT NOT NULL,
content TEXT NOT NULL,
embedding VECTOR(384) NOT NULL
);
CREATE INDEX doc_chunks_tenant_idx
ON doc_chunks (tenant_id);
CREATE INDEX doc_chunks_embedding_hnsw_idx
ON doc_chunks
USING hnsw (embedding vector_cosine_ops);
-- $1: 查询向量;$2: 后端确认的租户 ID
-- $3: 后端校验并限制大小的结果数量
SELECT id, doc_id, source_version, content,
embedding <=> $1::vector AS distance
FROM doc_chunks
WHERE tenant_id = $2
ORDER BY embedding <=> $1::vector
LIMIT $3;这里 <=> 表示余弦距离,距离越小越相近;应用应统一距离与相似度的语义,不能把分数直接展示成“答案正确概率”。ORDER BY 应匹配所建索引的度量,实际是否走索引、访问多少行,需要通过执行计划和测量确认。
示例只演示租户条件,不代表已经实现同一租户内的文档 ACL。应用还需要在查询层、数据库策略或其他可信服务层加入真实授权规则,并在读取来源文档时再次执行相应授权。
pgvector 的近似索引查询中,过滤可能发生在索引扫描之后,过滤较严格时会出现返回数量不足或召回下降。支持的版本可以评估 iterative scans,也可以结合合适的过滤索引、分区等策略;必须按实际权限过滤分布验证。参见 pgvector:Filtering 与 Iterative Index Scans。
这意味着:SQL 写了 WHERE,保证符合执行语义的返回行满足条件;但“有条件过滤”并不等于一定高效地找到该范围内的最佳 K 条。若过滤后只剩很小集合,先利用元数据条件缩小候选再精确计算,可能更合适。
授权不能依靠前端拿全库 Top-10 后删掉无权条目。这既可能泄露数据,也会导致用户明明有相关文档,却因候选被别人的内容占满而搜不到。
十一、检索质量:余弦 Top-K 不等于用户找到了答案
语义搜索擅长表达近似含义,但错误码、函数名、产品型号、合同编号等精确字符串可能更需要关键词检索。中文文档还需要评估语言覆盖、分词与领域术语。
可以结合稠密向量召回与关键词或稀疏召回,再通过 RRF 等方法融合排序,必要时使用 reranker 对少量候选重排。不同检索通道的原始分数尺度不一定可比,不能随意直接相加。参见 Qdrant:Hybrid Queries。
例如,设计上可以各取一批候选、合并去重、重排后展示前十条。每个阶段取多少只是待验证参数,需要看质量、延迟和成本,不能把“各取 100 条”当成通用标准。重排只能调整已经召回的候选,补不回所有阶段都漏掉的内容。
还要处理同一文档多个相邻块占满结果的问题。展示文档搜索结果时,可以按 docId 聚合并保留最有用片段;RAG 证据选择则要兼顾覆盖、冗余与上下文预算。块级召回指标和文档级体验要分别评估。
十二、生命周期:更新、删除、配额和离线隐私
1. 更新文档,也是在更新一组索引记录
每个文档版本应能定位到对应 chunk 集合。文本变化后,识别需要重嵌入的块;新版本就绪后切换可见版本,并清除旧块。否则同一搜索可能同时返回过期段落和新段落。删除原文也不能忘记向量、缓存和引用片段。
模型升级通常应使用新的索引版本或集合完成重建与评估,确认 query 与文档的编码契约一致后再切换。避免把同为 384 维、却来自不同向量空间的记录混合检索。
2. 端侧索引发布需要完整性与恢复路径
可以用 manifest 记录模型、维度、度量、分片清单、校验和与数据版本。分片全部就绪、检查通过后,再切换 activeVersion;查询绑定一个完整版本,避免加载一半的新索引。构建被中断时,应能继续、清理临时数据或恢复旧快照。
浏览器存储存在配额与驱逐策略,navigator.storage.estimate() 提供的是估计值。写入失败、QuotaExceededError、用户清理数据和缓存重建都要有处理路径;申请持久存储也不能理解为永不丢失。参见 MDN:Storage quotas and eviction criteria。
持久化体积、常驻内存和首次下载量是三个不同预算。磁盘放得下,不代表内存装得下;内存装得下,也不代表用户愿意等待首次下载与建索引。
3. “在浏览器里”不自动等于隐私安全
真正离线要求解析、embedding、检索和必要的展示都不依赖远程传输。若查询 embedding 仍由云端生成,查询内容仍会离开设备。端侧模型本身也有下载、初始化、内存和语言质量成本。
文档块和向量应按数据敏感性对待,向量不是匿名化保证。离线客户端已经获得的数据,权限撤销后无法保证立即从所有副本消失;需要在产品层明确离线授权、保留与撤销策略。XSS、共享设备、账户切换和日志也属于边界。
十三、怎样证明方案可用:同时测性能、近邻召回与业务质量
1. 性能拆解,避免只测余弦函数
串行查询链路可以近似拆成:
总耗时 =
输入触发等待/排队
+ 查询 embedding
+ 传输与数据读取
+ 向量/关键词检索
+ 融合与重排
+ 文本获取
+ 结果渲染
若部分步骤并行,应按实际关键路径计时,
不要把重叠耗时简单相加。| 类别 | 建议记录 | 需要区分的条件 |
|---|---|---|
| 容量 | 向量数、块数、持久化大小、常驻与峰值内存 | 仅向量与整套索引;构建、恢复和更新峰值 |
| 延迟 | 端到端及各阶段 P50/P95/P99 | 冷启动/热查询、设备档位、查询长度、过滤比例 |
| 响应性 | 输入延迟、滚动与动画、主线程长任务 | 计算在 Worker 中仍可能争用设备资源 |
| 吞吐与调度 | 服务端并发/QPS,端侧队列与取消生效时间 | 持续输入、并发请求、丢弃旧任务、后台恢复 |
| 稳定性 | 崩溃、内存增长、配额与恢复失败 | 多标签页、长会话、索引更新与存储丢失 |
明确记录硬件、系统、浏览器或服务版本、模型、dtype、索引参数、语料及查询集。不能把服务端原生库的基准、桌面浏览器的热缓存结果,直接当作低端手机端到端表现。
2. ANN Recall@K:索引找回了多少精确近邻
E_K(q) = 在相同向量与度量下精确搜索得到的 Top-K
A_K(q) = ANN 返回的 Top-K
ANN Recall@K(q) = |E_K(q) ∩ A_K(q)| / K
例如 K=10,两者重合 9 条:
该查询 ANN Recall@10 = 0.9。对一批有代表性的查询取平均,并关注难例。带权限过滤时,精确真值必须在同一可访问候选集合中计算;可访问记录不足 K 时,要约定有效分母;相同分数的并列结果也要有一致处理规则。
3. 业务相关性:精确近邻也可能不相关
ANN Recall@K 高,说明近似索引接近这套 embedding 的精确结果,并不能证明模型理解了业务。业务层需要标注相关文档或答案证据,评估相关文档 Recall@K、MRR、nDCG、无答案问题表现,以及必要的引用准确性。
先在固定模型与语料上建立精确基线,再评估索引近似造成的损失;另用标注查询评估 embedding、切块、混合检索和重排。把“算法漏了近邻”与“模型找到的近邻不相关”分开,才能知道该调 efSearch 还是改模型与数据。
验收可以设置目标,例如某类设备 P95 端到端延迟、峰值内存和 ANN Recall@10 的最低要求,但具体阈值应来自产品预算与实测。没有测试数据时,应说清验证计划,避免承诺“保证几十毫秒”。
十四、这些常见补救答案,为什么还会被继续追问
| 候选人的补充回答 | 仍然缺少的判断 |
|---|---|
| IndexedDB 是异步的,所以不卡 | 存储请求异步不代表主线程上的相似度循环不阻塞 |
| 我加个 Web Worker | 仍需解释 O(Nd)、内存、数据搬运、队列和冷启动 |
| 我做防抖和缓存 | 未解决首次冷查询、不同查询及索引版本失效 |
| 分批扫描就行 | 改善调度,但总计算量未必减少,端到端延迟可能增加 |
| 用 HNSW 就变成 O(log N) | 忽略实际召回、参数、数据分布、图内存与更新代价 |
| 用 WebGPU 一次全算完 | 未评估设备支持、初始化、传输、读回和小批查询收益 |
| 浏览器至少能存几百 MB | 混淆配额、内存、模型与索引工作集,没有目标设备证据 |
| 把所有内容发给前端后再做权限过滤 | 泄露数据,且过滤后的候选质量可能严重下降 |
| 余弦分数 0.8 就是 80% 可信 | 相似度没有这种通用概率解释 |
| 直接换某个向量数据库 | 还没说明切块、权限、索引、更新、观测和质量评估 |
十五、面试现场:一段可以据实调整的完整回答
下面是一段适用于“当前只做过端侧原型”的回答示例。实际使用时,务必用自己的真实经历替换,不要把方案建议说成已经完成的生产实践。
您指出的风险成立。我前面的表述不完整:IndexedDB 在我的方案中只是持久化层,向量相似度计算由应用实现;如果在主线程每次全量读取再扫描,这只能算有限规模下的原型,不能据此证明适合十万文档。
我会先确认十万是文档还是切块后的向量数。按十万条、384 维 Float32 算,原始向量就是 153.6 MB;一次点积扫描大约有 3840 万次乘法,还不包含读取、Top-K 和 embedding。切块后如果变成百万条,容量会继续放大。因此我会把延迟、内存和交互响应分别测量,而不是直接断言一定能跑或一定卡死。
如果必须离线,我会让 Worker 持有紧凑向量或索引,避免查询时把全库搬来搬去;文档向量预归一化,Top-K 用有界结构选择,并处理最新查询优先、过期结果和可取消计算。Worker 只解决计算位置;如果精确扫描达不到目标,再评估浏览器可用的 ANN 实现、量化和分片,用精确基线测召回损失。
如果是多人在线知识库,我会优先评估服务端共享检索。已有 PostgreSQL 可以评估 pgvector,有独立检索服务需求可以评估 Qdrant 等产品,前端调用带认证的业务接口。权限范围、切块、模型版本、过滤召回、增量更新和混合检索都要一起设计。生产向量数据库的实际使用经历我会如实说明,再用真实测试结果解释选型。
这段回答体现了四个动作:纠正概念、量化成本、给出有条件的架构方案、说明验证方式。它比临时补一个技术名词更能证明你理解工程边界。
十六、这道题对前端工程师的要求
前端工程师不必为了这道题假装成为所有向量数据库的专家,但需要能解释:哪些工作必须在终端执行,哪些适合共享服务;数据如何流动,为什么会复制;用户输入时任务如何调度;搜索结果为何可信;权限与更新如何持续成立。
面对十万文档、384 维向量,专业回答应能够交代规模、约束、方案边界和证据。IndexedDB、Worker、ANN 与向量数据库都可以成为方案的一部分,关键在于让它们各自承担正确的职责,并通过目标环境与真实查询集验证。
参考资料
以下为文中技术机制的主要核对资料。容量估算来自文中给出的 N × d × bytes 计算;架构建议与面试表达属于基于这些机制整理的工程分析。
MDN:Storage quotas and eviction criteria
