Language: English | 简体中文
与 Qdrant 的差异
MiniQdrant 保留了选定的单节点不变量,同时有意省略产品兼容性。与 Qdrant 共用的名称描述的是教学主题,并不表示实现或性能完全相同。
有关模块之间的正向对应关系,请参阅 docs/qdrant-mapping.md。
产品范围
| Area | MiniQdrant | Qdrant |
|---|---|---|
| Access | Python API and thin local CLI | REST, gRPC, official clients |
| Vectors | one dense vector per point | dense, sparse, named, multi-vector |
| Payload indexes | keyword, integer, float, bool | broader production index set |
| Retrieval | exact, deterministic HNSW, scalar quantization | production HNSW/filtering/quantization stack |
| Storage | custom teaching WAL/segment/manifest | Qdrant formats and storage engines |
| Deployment | one process, one writer per collection | shards, replicas, distributed coordination |
| Optimizer | explicit deterministic full rebuild | adaptive background optimizers |
| Snapshot | collection root, custom checksum format | Qdrant snapshot compatibility |
未实现的功能包括:Qdrant 传输协议或存储兼容性、稀疏或混合搜索、推荐/融合/分组、滚动查询和批量搜索、地理/文本/日期时间索引、mmap/RocksDB、分片、复制、Raft、身份验证、TLS、配额、多租户、GPU、SIMD/原生内核,以及在线模式迁移。
已声明的语义和性能差异
-
过滤式 HNSW 是后过滤加固定过采样。
HnswIndex.search()在遍历图时不会查询allowed_ids;只有在收集候选时才检查成员资格,并且分段最多请求limit * 4个候选。这正是 Qdrant 的过滤感知 HNSW 设计所批评的后过滤模式:选择性过滤器可能在遍历后丢弃最近的候选,导致返回的命中更少或召回率更差;增加过采样则会浪费工作,却不能保证得到足够的匹配。Qdrant 将过滤约束纳入图遍历,并针对困难过滤器采用其他策略。 -
标量量化路径是解码浮点全扫描,而不是 HNSW 评分器。 MiniQdrant 会扫描每个符合条件的量化码,将量化查询和存储的编码都解码回浮点向量,对这些浮点数进行评分、过采样,并对保留下来的候选执行精确重评分。真正的 Qdrant 可以在 HNSW 遍历期间使用 int8 量化评分器,再对选定候选进行重评分。因此,MiniQdrant 教授的是两阶段近似/重评分的结构,但既不提供整数评分,也不具备生产实现的渐近优势。
-
每次集合搜索都有 O(N) 的视图捕获成本。
capture_view()会扫描所有不可变和可变记录来重建_latest_records,并重建可变快照,其中包括载荷索引。随后,_stale_live_count()会再次扫描每个已搜索的分段。因此,即使每个分段的 HNSW 只访问很少节点,端到端搜索仍然随集合大小线性增长。Qdrant 维护增量式 ID 和分段跟踪,而不是为每次查询重建全局可见性。 -
同一个 upsert 批次内的重复 ID 采用首次写入胜出。 批次中的每个项目都会收到相同的 WAL 序列号/版本,而
MutableSegment会拒绝同版本替换。Qdrant 对重复点 ID 的批次/更新行为采用最后写入胜出。若调用者想要类似 Qdrant 的行为,就必须自行对 MiniQdrant 批次去重。 -
载荷字段路径会自动展开数组。 MiniQdrant 的路径遍历器无需显式标记就会向下遍历列表元素。Qdrant 使用显式的
[]路径记法,并且具有不同的嵌套数组匹配语义,包括元素局部的嵌套条件。因此,在两个系统间复制的过滤器可能匹配到不同的点集合。 -
欧几里得分数是负平方距离。 MiniQdrant 的所有度量都采用统一的内部约定,即分数越高越好,因此欧几里得度量暴露的是
-(distance²)。score_threshold始终按score >= threshold应用;欧几里得调用者必须传入负平方距离阈值。Qdrant API 会根据所选度量解释阈值方向,因此数值阈值不可直接移植。 -
QUANTIZED_HNSW_RESCORE是一个具有误导性的策略名称。 选择这个计划会完全绕过_hnsw,并调用ScalarQuantizedIndex的全扫描。结果中的plan可用于观察分支,但不能证明 HNSW 确实运行过。 -
持久化的
hnsw.bin在验证后会被丢弃。SegmentCodec.read()将图解码到SegmentImage.hnsw_graph,但SegmentImage.to_segment()只传递indexed=True;构建不可变分段时会从所有存活点重建 HNSW。Qdrant 的持久化索引可以复用,而这里会让打开/重启承担图构建成本,并使hnsw.bin成为只写的语义数据。 -
HNSW 搜索可以跨不连通分量进行穷举式重启。 当第一次第 0 层搜索得到的分数少于
breadth时,MiniQdrant 会反复从尚未访问的最小点 ID 开始。这并不是标准 HNSW 行为。在碎片化图或较大的ef_search下,它可能访问每个向量,从而在召回率测试中掩盖较差的图连通性,并使近似搜索退化为全扫描。 -
优化器策略和自动刷写阈值没有接入运行时。
choose_optimization()和flush_threshold_points只用于测试/配置。upsert 不会触发刷写或后台工作,而显式调用merge()、vacuum()和optimize()都会重写捕获到的完整分段集合。规划器还将indexing_threshold_points同时用于普通扫描和过滤扫描决策,而 Qdrant 暴露了独立的全扫描阈值。 -
WAL 从不截断、轮转或回收已发布的历史。 刷写和优化会推进
replay_boundary,因此恢复时会跳过已经覆盖的帧,但单个00000000000000000001.wal文件会无限增长。它的编号文件名并不代表已经实现多文件轮转。Qdrant 会回收已被持久化状态安全覆盖的 WAL 历史。
其他有意简化
为了便于检查,HNSW 实现采用确定性的层级、平局判定和仅保留最近邻的剪枝规则,而不是追求生产级召回率与吞吐量。分段 .bin 文件包含分帧 JSON,而不是原生二进制或 mmap 布局。非精确载荷基数估计使用固定的教学启发式方法。SearchRequest.filter 的类型是 object | None 并在运行时检查,而不是暴露一个完全自说明的客户端模式。
持久性策略暴露的是本地崩溃边界,而不是分布式写入一致性契约。载荷字段合并/删除有意采用浅层方式;嵌套补丁语言和 Qdrant API 兼容性不在范围内。