-
Notifications
You must be signed in to change notification settings - Fork 1
01 Release Notes_zh.md

产品定位:纯 .NET 实现的嵌入式向量数据库 —— 零原生依赖,进程内运行,无需独立部署数据库服务器
框架版本:.NET 10
命名空间:Vorcyc.Quiver
设计理念:类似 EF Core 的DbContext模式,通过声明式属性标记实现向量数据库的自动发现、索引构建和持久化
核心特性:Code-First 声明式实体定义 · 多种 ANN 索引(Flat / HNSW / IVF / KDTree) · 9 种内置距离度量 + 自定义相似度 · 二进制主存储 + JSON/XML 导出/导入 · Schema Migration(属性重命名 / 值转换) · 读写分离锁并发安全 · SIMD 加速相似度计算 · 向量内存模式(InMemory / MemoryMapped / Auto / PerField) 关键字:嵌入式向量数据库纯 .NETANN近似最近邻搜索相似度检索HNSWIVFKDTreeCode-FirstEF Core 风格Embedding语义搜索人脸识别以图搜图RAGSIMDSchema MigrationISimilarity自定义度量释名:Quiver —— 箭袋,装箭(Arrow)的容器,向量的数学本质就是箭头
| 改进项 | 说明 |
|---|---|
| 导入 / 批量 CRUD 的 HNSW 批量建图 |
AddRange、UpsertRange 与 ImportAsync 先写入实体与向量,再对每个向量字段调用一次 BuildBulk(与二进制 LoadEntities 相同的延迟建图路径)。大规模 JSON/XML 导入 HNSW 时不再逐条串行 index.Add。 |
| JSON 向量 Base64 编码 |
float[] / Half[] 导出为 Base64 字符串(与 XML 一致),显著缩小导出体积并加快反序列化;导入仍兼容旧版数字数组 JSON。 |
| JSON 导入并行解析 |
JsonExportProvider.LoadAsync 使用 PipeReader 流式扫描,按实体拷贝 JSON 片段,并由 worker 池并行反序列化,带在途内存背压。 |
UpsertRange 批量 API |
QuiverSet<T>.UpsertRange 在单次写锁内批量 upsert;目标集合非空时 ImportAsync 自动走此路径。 |
| XML 导入属性读取修复 | 修复 XmlExportProvider 在 ReadElementContentAsStringAsync 后误跳过后续同级元素的问题(如 Name 等标量字段)。 |
| 截断 JSON 的错误提示 | 不完整导出文件(如 ExportAsync 中断)会抛出带文件大小的明确 InvalidDataException,而非难懂的 depth 错误。 |
| 说明项 | 内容 |
|---|---|
| JSON 导出默认格式 |
WriteIndented 默认改为 false,导出更紧凑。 |
| 变更项 | 说明 |
|---|---|
移除向量 LazyLoad 模式 |
已删除 VectorMemoryMode.LazyLoad 与 GlobalVectorMemoryMode.LazyLoad。对堆存储而言,向量懒加载无法降低进程内存——向量 store 必须保留全部向量以供索引遍历与相似度搜索,因此 LazyLoad 行为与 InMemory 完全一致。请使用 InMemory(最低延迟)或 MemoryMapped(真正的低堆内存路径)。大字段的 LazyLoad / PagedCache 不受影响——大字段不参与搜索,仍是真正的懒加载。 |
| 改进项 | 说明 |
|---|---|
Half[] 向量支持 MemoryMapped |
fp16(Half[])向量字段现在可声明为 VectorMemoryMode.MemoryMapped(以及解析到该模式的全局模式)。在 partial 类型中将属性声明为 public partial Half[]? Name { get; set; },源生成器会生成由 LazyVectorAccessor.MaterializeHalf 支撑的懒加载访问器,落盘时以 Float16 编码。 |
| 懒加载属性 getter 每次访问都重新分配 | 源码生成器生成的 getter 使用 ?? 而非 ??=:__backing ?? Materialize(this, fieldName),导致物化结果不会写回 backing field,每次读取都分配新的大字段 byte[]。修复:生成器改为 ??=,首次物化后缓存到 backing field,后续直接返回。 |
本次变更不保留旧 API 兼容层。内存管理语义从“实体分页缓存”改为“按载荷类型管理”:实体始终直接保存在托管内存中;需要控制内存占用的是向量载荷和大字段载荷。
| 移除项 | 说明 |
|---|---|
EntityCacheMode / 实体分页缓存 |
删除实体级 LazyPaging / LRU page-cache 抽象,实体集合改为直接内存字典存储。 |
QuiverDbOptions.MaxCachedPages / PageSize
|
实体分页缓存已删除,这两个配置项不再存在。 |
QuiverSet<TEntity>.IsLazyLoading |
不再存在实体懒加载模式标志。 |
QuiverSet<TEntity>.CompactMemory() / CompactMemoryAsync()
|
实体页逐出能力已删除。 |
QuiverDbContext.CompactAllMemoryAsync() |
上下文级实体页压缩 API 已删除。 |
QuiverDbContext.RewriteAsync() / CompactAsync()
|
快照重写/碎片整理别名已删除;请直接调用 SaveAsync(path?) 完成全量原子快照和周期性多段整理。 |
QuiverBlobAttribute |
更名为 QuiverLargeFieldAttribute。 |
VectorStoreMode 及 VectorStore 配置命名 |
更名为 VectorMemoryMode / VectorMemoryMode 配置,语义聚焦“向量载荷内存策略”。 |
Optional 属性命名 |
更名为 Nullable,用于明确字段是否允许空载荷。 |
[QuiverVector(..., Lazy = true)] |
删除 Lazy 开关,改由全局或字段级 VectorMemoryMode / VectorMemoryMode 决定访问方式。 |
| 新 API | 用途 |
|---|---|
LargeFieldMemoryMode |
全局大字段载荷内存策略,支持 InMemory、LazyLoad、PagedCache、PerField。LazyLoad / PagedCache 需要有效 DatabasePath,且对应属性需声明为 partial 以便源生成器接入访问器。 |
LargeFieldMemoryMode |
字段级大字段内存策略覆盖。 |
QuiverDbOptions.LargeFields.MaxCachedPayloads |
PagedCache 模式下每个 QuiverSet 最多缓存的大字段 payload 数量,默认 128,必须大于 0。 |
VectorMemoryMode |
全局向量载荷内存策略,支持 InMemory、MemoryMapped、Auto、PerField。 |
VectorMemoryMode |
字段级向量内存策略覆盖。 |
QuiverLargeFieldAttribute.Nullable / QuiverVectorAttribute.Nullable
|
显式声明大字段或向量字段是否允许 null。 |
向量载荷(VectorBlob)和大字段载荷(Blob)现在先进入统一的内部 payload descriptor / validation 管线,再分发到各自的二进制编码器。这样 Nullable、字段名、载荷类型、内存模式等元数据不再由两套路径各自解释。
大字段 LazyLoad / PagedCache 已接入该管线:加载 Blob 段时不再立即把命中字段写入实体,而是登记文件切片;用户首次读取 source-generated partial byte[] 属性时按需从文件读取。PagedCache 在此基础上增加内部 LRU 缓存,重复读取同一 payload 时复用缓存副本。
保存优化:如果 LazyLoad / PagedCache 大字段在加载后未被用户读取或重新赋值,SaveAsync 会直接从原 .vdb 的 Blob slice 复制字节到新快照,避免先物化成托管 byte[]。如果属性已被赋新值,则优先写入新值,不复用旧 slice。
// 旧写法(已删除)
new QuiverDbOptions
{
VectorStore = VectorStoreMode.Mmap,
MaxCachedPages = 16,
PageSize = 512
};
[QuiverBlob(Optional = true)]
public byte[]? Payload { get; set; }
[QuiverVector(768, Lazy = true, Optional = true)]
public partial float[]? Embedding { get; set; }
// 4.0.1 新写法
new QuiverDbOptions
{
Vectors.MemoryMode = GlobalVectorMemoryMode.MemoryMapped,
LargeFields.MemoryMode = GlobalLargeFieldMemoryMode.PagedCache,
LargeFields.MaxCachedPayloads = 256
};
[QuiverLargeField(Nullable = true)]
public byte[]? Payload { get; set; }
[QuiverVector(768, Nullable = true, MemoryMode = VectorMemoryMode.MemoryMapped)]
public partial float[]? Embedding { get; set; }快照文件(
.vdb)依然完全向后兼容 v1.x、v2.x、v3.0.x、v3.1.x、v3.2.x、v3.3.x;但.wal旁路文件不再被读取或写入。
升级前先做这一步:在 3.2.x 应用上跑一次 LoadAsync() + SaveAsync(),把所有挂起的 .wal 增量回放并合并进主快照。否则升级到 4.0.1 后,首次加载会静默丢弃这些未压缩的 WAL 增量。
移除内容:QuiverDbOptions.EnableWal / WalCompactionThreshold / WalFlushToDisk 三个配置项、SaveChangesAsync() 方法、内部 WriteAheadLog / WalEntry 类型,以及 QuiverSet<T> 内部用于追踪增量变更的 _changeLog 队列。
原因:WAL 增量持久化路径在大规模写入时会使内存峰值翻倍 —— change-log 队列对所有排队的实体保留强引用,同时索引层向量副本仍然存在,两者叠加导致 200 万条写入下出现 20+ GB 峰值。
4.0.1 推荐用法:
// 3.2.x 旧写法
new QuiverDbOptions { DatabasePath = "x.vdb", EnableWal = true, WalCompactionThreshold = 10_000, WalFlushToDisk = true };
await db.SaveChangesAsync();
// 4.0.1 新写法
new QuiverDbOptions { DatabasePath = "x.vdb" };
await db.SaveAsync(); // 全量原子快照保存离线格式升级时的 Schema Migration:
ConfigureMigration<T>()只会在QuiverDbContext.LoadAsync()读取当前运行时支持的格式时生效。如果使用QuiverMigrator.MigrateAsync把 v1/v2/v3 文件离线升级到 v4,需要把同样的规则通过migrationRules参数显式传入;否则旧文件解码阶段遇到已重命名字段时会跳过旧值。
using Vorcyc.Quiver.Migration;
var rule = MigrationBuilder<Document>.Build(m => m
.RenameProperty("OldTitle", "Title"));
await QuiverMigrator.MigrateAsync(
sourceFile: "old.vdb",
destinationFile: "data.vdb",
typeMap: new Dictionary<string, Type>
{
[typeof(Document).FullName!] = typeof(Document)
},
migrationRules: new Dictionary<string, SchemaMigrationRule>
{
[typeof(Document).FullName!] = rule
});4.0.1 使用 v4 段式二进制磁盘格式。v1/v2/v3 文件可通过迁移路径升级;新写入一律使用 v4。
[Magic "QDB\x04"][HeaderLen u32][Header bytes]
[Segment 1] [Segment 2] ... [Segment N]
[FooterTopMagic "QDBF"][SegmentCount u32]
每段: [TypeName][Offset u64][Length u64][EntityCount u32][CRC32 u32]
[FooterOffset u64][TrailerMagic "QDBE"]
由此带来三个文件层面的新能力,不需要重新引入 WAL:
| API | 行为 | 代价 |
|---|---|---|
QuiverDbContext.AppendAsync() |
把当前内存中的实体作为新段追加到已有 v4 文件,仅重写 footer。 | O(Δ) 字节。真正意义上的增量写入——取代 WAL 的使用场景,但没有 WAL 的内存翻倍问题。 |
QuiverDbContext.SaveAsync() |
写入全量快照,并将多段文件碎片整理为单段。 | O(N)。建议周期性调用。 |
QuiverDbFile.MergeAsync(sources, dest, options, typeMap?) |
合并多个 v4 文件。MergeConflictPolicy.Append 是纯字节拷贝段;LastWriterWins / FirstWriterWins 按 [QuiverKey] 去重。 |
Append:O(I/O),不解码。LWW/FWW:解码-重写。 |
QuiverDbFile.InspectAsync(path, verifyCrc) |
返回 QuiverFileInfo(版本、段列表、每段 CRC 校验结果、每类型实体计数)。 |
校验 CRC 时为 O(file size)。 |
// 增量批量入库 — 取代 4.0 之前的 SaveChangesAsync 工作流
await using var db = new MyDb("data.vdb");
await db.LoadAsync();
db.Faces.AddRange(batch);
await db.AppendAsync(); // 仅写入本批,无全量重写
// 定期碎片整理
await db.SaveAsync();
// 合并多个归档文件,按 [QuiverKey] 去重,靠后者覆盖
var typeMap = new Dictionary<string, Type>
{
[typeof(FaceFeature).FullName!] = typeof(FaceFeature)
};
await QuiverDbFile.MergeAsync(
sourceFiles: ["a.vdb", "b.vdb", "c.vdb"],
destinationFile: "merged.vdb",
options: new MergeOptions { ConflictPolicy = MergeConflictPolicy.LastWriterWins },
typeMap: typeMap);
// 诊断
var info = await QuiverDbFile.InspectAsync("merged.vdb");
Console.WriteLine($"v{info.FormatVersion}, {info.Segments.Count} 段, crcValid={info.CrcValid}");下方涉及 WAL、
SaveChangesAsync、EnableWal、WriteAheadLog、WalEntry、.wal文件的章节描述的是 4.0 之前的架构,仅作为历史参考保留。
文件格式兼容性:v3.2.1 完全向后兼容 v1.x、v2.x、v3.0.0、v3.1.0 和 v3.2.0 的所有数据文件,无需任何迁移。
| 修复项 | 说明 |
|---|---|
EntityPageCache 线程安全修复 |
修复了 LazyPaging 模式下的数据竞争问题:当多线程同时通过 Parallel.ForEach 调用 Find / Search 时,内部 LRU 结构(_loadedPages、_lru、_lruNodes)会发生并发写冲突。现已在 GetOrLoadPage()、FlushDirty()、CompactMemory()、Clear() 中通过 _pageLock 对所有 LRU 状态变更进行保护。FullMemory 模式不受影响(零开销)。 |
基于 v4(
QDB\x04)段 + Footer 格式之上扩展。已有 v4 文件可被透明读取;新写入会把 footer 升级到 schema v2(每段附带Kind/FieldName/Dim/FirstId)。
本次更新把向量和大字段从实体元数据中物理拆开,并用 "tombstone + merge" 模型替代原地删除。目标是即便在百万级高维向量场景下也保持托管堆平稳。
磁盘上的 VectorBlob 段通过 MemoryMappedFile 映射进进程,向量直接从 OS 页缓存读取,不再进入托管堆。
new QuiverDbOptions
{
DatabasePath = "data.vdb",
VectorStore = VectorStoreMode.Mmap, // Heap(默认)/ Mmap / Auto
VectorStoreMmapThresholdBytes = 64L * 1024 * 1024, // 仅 Auto 模式:超过此大小切换为 mmap
};| 模式 | 后端 | 适用 |
|---|---|---|
Heap(默认) |
HeapVectorStore(Dictionary<int, float[]>) |
小规模 / 写多读少;无需 DatabasePath
|
Mmap |
MmapVectorStore(对 VectorBlob 段的只读视图) |
大规模读多场景(人脸库、RAG 向量库),稳定的托管堆占用 |
Auto |
小于阈值用 Heap,大于切到 Mmap | 混合工作负载 |
SaveAsync / AppendAsync 会在替换文件前自动释放 mmap 视图,写完后再次绑定到新的 VectorBlob 区域 —— 对调用方完全透明。
只有当用户读取实体属性时才把向量从 mmap 物化出来。把属性声明为 partial,Vorcyc.Quiver.SourceGenerators 会生成调用 LazyVectorAccessor.Materialize(this, "PropertyName") 的 getter。
public partial class AudioEntity
{
[QuiverKey] public string Id { get; set; } = "";
[QuiverVector(1024, DistanceMetric.Cosine, Lazy = true)]
public partial float[]? Embedding { get; set; } // backing field + getter 由源生成器产生
}搜索热路径仍直接从 mmap 区域读向量(零分配);用户访问 entity.Embedding 时才触发一次性的拷贝出列。
懒向量源生成要求向量属性以及它所在的整条嵌套类型链都声明为 partial,属性类型必须是 float[] 或 float[]?。无效声明会产生 analyzer 诊断:QVR001(属性不是 partial)、QVR002(包含类型链并非全部 partial)、QVR003(属性类型无效)。
使用方项目需引用 analyzer:
<ProjectReference Include="..\Vorcyc.Quiver.SourceGenerators\Vorcyc.Quiver.SourceGenerators.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
inline byte[](缩略图、原始音频、序列化特征)以往会撑大 EntityMeta 段并在 load 时膨胀工作集。加上 [QuiverBlob] 后,这类字段会写到独立的 SegmentKind.Blob 段。
public class Photo
{
[QuiverKey] public string Id { get; set; } = "";
[QuiverVector(512, DistanceMetric.Cosine, Lazy = true)] public partial float[]? Embedding { get; set; }
[QuiverBlob] public byte[]? Thumbnail { get; set; } // ← 写入独立的 Blob 段
}[QuiverBlob] 仅可用于 byte[],且与 [QuiverVector] 互斥。
之前 RemoveByKey 后再 AppendAsync 没有磁盘表达。Wave 2 增加了 SegmentKind.Tombstone 段,记录死亡的内部行 id,加载时由读取层先行过滤。
await using var db = new MyDb("data.vdb");
await db.LoadAsync();
db.Faces.RemoveByKey("F0001");
db.Faces.RemoveByKey("F0002");
// 仅写出 Tombstone 段;不会把当前内存里的活实体重新作为新段追加。
await db.FlushTombstonesAsync();| API | 写入内容 | 适用场景 |
|---|---|---|
AppendAsync() |
为所有当前内存实体写出新的 EntityMeta / VectorBlob / Blob 段,同时附带一个 Tombstone 段(如有待删除项)。 |
批量入库 + 顺便刷掉挂起删除。 |
FlushTombstonesAsync() |
仅写出 Tombstone 段。 | 加载 → 原地修改 → 只刷删除,不重写活实体。 |
SaveAsync() |
单段原子快照,所有历史 tombstone 在物理上被丢弃。 | 周期性碎片整理。 |
QuiverDbOptions 新增三个阈值,每次 AppendAsync / FlushTombstonesAsync 之后做尽力而为的自动 Rewrite:
| 选项 | 默认 | 用途 |
|---|---|---|
EnableBackgroundMerge |
false |
总开关 |
AutoMergeMaxSegments |
32 |
footer 段数达到该值后触发 SaveAsync()
|
AutoMergeTombstoneRatio |
0.25 |
tombstone / live ≥ ratio 时触发 |
自动 merge 内部的任何异常都会被吞掉,绝不会冒泡到用户的 AppendAsync 调用。
SegmentInfo 暴露新增列:Kind(Mixed / EntityMeta / VectorBlob / Blob / Tombstone)、FieldName、Dim。同类型跨多个 VectorBlob / Blob / Tombstone 段时实体数不再重复计数。
[FooterTopMagic "QDB2"][SegmentCount u32]
每段:
[TypeName s][Offset u64][Length u64][EntityCount u32][CRC32 u32]
[Kind u8][FieldName s][Dim i32][FirstId i32]
[FooterOffset u64][TrailerMagic "QDBE"]
"QDBF"(v1 footer)仍然向后兼容读取,新写入一律使用 "QDB2"。
基于段式文件格式和 mmap 向量存储继续演进。已有 raw float32
VectorBlob段可被透明读取;新写入会在每段头部加VectorBlobEncoding与可选 SQ8 scale 表。索引拓扑结构与公共 API 保持不变。
本次更新聚焦于"在源端 embedding 模型未知的前提下"也能稳定控制磁盘体积、托管堆体积与运行期内存:通过字段级量化与 Matryoshka 截断在 I/O 路径上压缩向量,通过堆字节预算在运行期自动从 Heap 升级到 Mmap。
QuiverVectorAttribute 新增 Quantization 属性,目前支持两种编码:
| 取值 | 磁盘体积 | 说明 |
|---|---|---|
VectorQuantization.None(默认) |
dim × 4B |
Raw float32,与既有 v4 原始向量段行为一致 |
VectorQuantization.Sq8 |
dim × 1B + 4B scale |
按行 SQ8 标量量化(int8 + 单 scale),磁盘体积约 1/4,搜索时通过 Sq8Codec.DecodeRow 解码到 thread-local 缓冲,零分配 |
public partial class FaceFeature
{
[QuiverKey] public string Id { get; set; } = "";
// SQ8 + Matryoshka:1024 维 embedding 仅用前 512 维参与索引/搜索,磁盘体积 ≈ 1024×1B + 4B
[QuiverVector(1024, DistanceMetric.Cosine,
Lazy = true,
Quantization = VectorQuantization.Sq8,
EffectiveDimensions = 512)]
public partial float[]? Embedding { get; set; }
}编码信息按段持久化在 v4 VectorBlob 段头中(VectorBlobEncoding enum + 版本号),加载时由 MmapVectorStore 与 BinaryStorageProvider 自动按段解码。源端 embedding 模型可未知。
QuiverVectorAttribute.EffectiveDimensions 允许在不修改源 embedding 的前提下,只取向量前 N 维参与索引与搜索:
- 写入路径:
PrepareVectors在EffectiveDimensions < Dimensions时复制前 N 维到新数组(避免修改实体本身),可选 L2 归一化后入库与建索引。 - 查询路径:
Search/SearchKnn自动对查询向量做同样截断与归一化,保证查询向量与底层 store 的几何对齐。 - 索引拓扑:所有索引(Flat / HNSW / IVF / KDTree)按
EffectiveDimensions构建,距离计算成本随之线性下降。
适合 Matryoshka 系列 embedding 模型(如 OpenAI text-embedding-3-large、Nomic 等),也可用于"先用低维快速召回 + 再用全维精排"的两阶段管线。
QuiverDbOptions 新增两个运行期内存控制项:
| 选项 | 默认 | 用途 |
|---|---|---|
MaxHeapVectorBytes |
0 (禁用) |
单个 QuiverSet 中所有 HeapVectorStore 的字节合计上限 |
AutoPromoteToMmap |
false |
越限时是否自动把该 set 的 Heap 向量 store 升级为 Mmap |
工作流程:
-
QuiverSet的Add/AddRange/Upsert写路径在写锁尾部调用NotifyHeapBytes(),把当前IVectorStore.HeapByteSize合计上报给QuiverDbContext。 -
QuiverDbContext(实现内部接口IPromotionCoordinator)按(AutoPromoteToMmap && bytes ≥ MaxHeapVectorBytes && DatabasePath != null)判定,并对每个 entity type 用 CAS 做单飞排队。 - 后台任务执行
SaveAsync()(保证磁盘内容与内存一致),随后用新VectorBlob段的 mmap 视图通过QuiverSet.PromoteFieldsToMmap(...)替换原HeapVectorStore。 - 替换通过新引入的
VectorStoreSlot间接层完成 —— 索引持有的是稳定的 slot 引用,索引拓扑无需重建,搜索热路径不中断。
new QuiverDbOptions
{
DatabasePath = "audio.vdb",
VectorStore = VectorStoreMode.Heap, // 起步用 Heap
MaxHeapVectorBytes = 512L * 1024 * 1024, // 单集合堆向量超 512 MiB
AutoPromoteToMmap = true, // 自动升级到 Mmap
};升级失败(如磁盘不可写)会被 Trace.TraceWarning 记录并把 in-flight 标志复位,不会冒泡到用户写路径。
| 新增 | 位置 | 说明 |
|---|---|---|
VectorQuantization enum |
Vorcyc.Quiver.Quantization |
字段级量化策略 |
VectorBlobEncoding enum |
Vorcyc.Quiver.Storage |
VectorBlob 段编码版本 |
Sq8Codec |
Vorcyc.Quiver.Storage |
SQ8 行编码/解码(thread-local 缓冲) |
IVectorStore.HeapByteSize |
Vorcyc.Quiver.Indexing |
当前 store 的托管堆字节合计 |
IVectorStore.EffectiveDim |
Vorcyc.Quiver.Indexing |
索引/搜索实际使用的维度 |
QuiverDbOptions.MaxHeapVectorBytes |
同上 | 堆字节预算 |
QuiverDbOptions.AutoPromoteToMmap |
同上 | 自动提升总开关 |
QuiverVectorAttribute.Quantization |
Vorcyc.Quiver |
字段量化策略 |
QuiverVectorAttribute.EffectiveDimensions |
同上 | Matryoshka 截断目标维度 |
- 文件格式:v4
VectorBlob段加入了 encoding 字节 + 可选 SQ8 scale 区,仍嵌入在QDB\x04容器与 footer schema v2 内;既有 raw float32 段会被透明读取。 - 公共 API:
QuiverDbOptions、QuiverVectorAttribute、IVectorStore、QuiverSet<T>仅做加法式扩展,已有调用方代码无需修改。 - 索引:
VectorStoreSlot是QuiverSet<T>内部包装,对索引实现完全透明。
本次更新解决大库加载时 HNSW 图需要逐点重建的问题。SaveAsync() 会为支持快照的索引写出独立的 SegmentKind.IndexSnapshot 段;LoadAsync() 会优先恢复索引拓扑,再只为快照未覆盖的新行补建索引。
HNSW 快照保存入口点、最大层级、节点层数、每层邻居列表以及快照覆盖的 NextId,避免每次加载都按 Add(id) 重跑 O(N log N) 的图构建流程。对于几十万级向量库,加载耗时主要从“重建图”转为“读取并反序列化拓扑”。
快照包含相似度类型、HNSW 参数、有效维度等指纹。若运行时模型、维度、量化/截断后的有效维度或索引参数不匹配,加载器会拒绝该快照并自动回退到旧的重建路径;旧文件没有 IndexSnapshot 段时也保持完全兼容。
在 VectorStore = Mmap / Auto 时,加载流程现在保证先把 VectorBlob 段绑定到 MmapVectorStore,再对快照未覆盖的 id 执行索引补建,避免 HNSW 在 mmap 尚未绑定时读取向量导致 KeyNotFoundException: Vector id ... not found in mmap store.。
同时,mmap 区域匹配会同时接受 [QuiverEntity("稳定名称")] 和旧的 Type.FullName 别名,防止给实体添加稳定名后旧 v4 文件的 mmap 段被静默跳过。
- 文件格式:新增可选
SegmentKind.IndexSnapshot段,旧 v4 文件照常读取;不支持快照的索引类型继续按原逻辑重建。 - 懒加载:实体分页、懒向量、
[QuiverBlob]大对象懒加载均不受影响;快照只保存索引拓扑,不保存实体或向量副本。 - mmap:快照恢复与 mmap 绑定解耦,搜索热路径仍直接从 mmap 读取向量。
文件格式兼容性:v3.2.0 完全向后兼容 v1.x、v2.x、v3.0.0 和 v3.1.0 的数据文件,无需任何迁移。
| 功能 | 说明 |
|---|---|
CompactMemory() / CompactMemoryAsync() |
在 QuiverSet<T> 上调用,将所有脏页刷写到磁盘后驱逐全部内存页,按需最小化内存占用。在 FullMemory 模式下为空操作。向量索引始终驻留内存,不受影响。 |
CompactAllMemoryAsync() |
在 QuiverDbContext 上调用,对上下文中所有 QuiverSet 一次性执行内存压缩。 |
文件格式兼容性:v3.1.0 完全向后兼容 v1.x、v2.x 和 v3.0.0 的数据文件,无需任何迁移。
| 变更项 | v3.0.0 | v3.1.0 |
|---|---|---|
VectorStorageMode 已移除 |
QuiverDbOptions.VectorStorage = VectorStorageMode.MemoryMapped —— 可选的内存映射向量存储(MmapVectorStore) |
已完全移除。向量始终存储在 GC 堆(HeapVectorStore)。LazyPaging 分页缓存已能有效控制实体内存,独立的 mmap 层不再必要。 |
QuiverSet 构造函数简化 |
接受 DistanceMetric defaultMetric 参数 |
已移除该参数。每个向量字段通过 [QuiverVector(dim, metric)] 独立声明其度量。 |
如果你之前在 QuiverDbOptions 中设置了 VectorStorage = VectorStorageMode.MemoryMapped,直接删除该行即可,无需其他修改,数据文件完全兼容:
// v3.0.0(删除 VectorStorage 行)
var options = new QuiverDbOptions
{
DatabasePath = "mydata.vdb",
// VectorStorage = VectorStorageMode.MemoryMapped, ← 删除此行
EntityCache = EntityCacheMode.LazyPaging,
MaxCachedPages = 32,
PageSize = 512
};文件格式兼容性:v3.0.0 完全向后兼容 v1.x 和 v2.x 的数据文件。
| 功能 | 说明 |
|---|---|
| 懒加载分页缓存 |
EntityCache = EntityCacheMode.LazyPaging —— 实体对象不再全量常驻内存,而是按固定大小的页(PageSize 条/页)按需从页文件加载,并通过 LRU 策略淘汰冷页。 |
| 可控内存上限 | 实体对象的工作集上限约为 MaxCachedPages × PageSize × 单实体大小,与数据集总大小无关。 |
| 向量索引仍常驻内存 | HNSW / IVF / KDTree 索引拓扑结构始终驻留内存,搜索性能不受懒加载影响。 |
IsLazyLoading 属性 |
QuiverSet<T>.IsLazyLoading 可用于运行时诊断当前缓存模式。 |
| 透明 API |
EntityPageCache<T> 与旧版 Dictionary<int, TEntity> 接口对齐,调用方代码无需任何修改。 |
| 内存映射向量存储 |
VectorStorage = VectorStorageMode.MemoryMapped(在 3.0.0 引入,已在 3.1.0 移除,见上方说明)。 |
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
EntityCache |
EntityCacheMode |
FullMemory |
实体缓存模式:FullMemory(全量常驻)/ LazyPaging(LRU 分页),须设置 DatabasePath
|
MaxCachedPages |
int |
16 |
每个 QuiverSet 最多在内存中保留的页数 |
PageSize |
int |
512 |
每页最多容纳的实体数量 |
var options = new QuiverDbOptions
{
DatabasePath = "mydata.vdb",
EntityCache = EntityCacheMode.LazyPaging, // ← 启用懒加载分页
MaxCachedPages = 32, // 内存中最多保留 32 页
PageSize = 512 // 每页 512 条实体
// 内存上限 ≈ 32 × 512 × 单实体大小
};页文件存储于
{DatabasePath}.pages/{EntityTypeName}/page_XXXXXXXX.qvpg(自定义二进制格式,无外部依赖)。页文件二进制布局(v1):
[4B uint32] Magic = 0x51565047 ("QVPG" 文件标识) [1B byte] Version = 0x01 [4B int32] PropCount ← 属性描述符数量 PropDescriptor × PropCount: [string] PropName ← BinaryWriter 长度前缀 UTF-8 [4B int32] EntityCount ← 本页实体数 Entity × EntityCount: [4B int32] InternalId 按描述符顺序逐字段:[1B bool null标志] + 字段值 (类型编码同 BinaryStorageProvider)
文件格式兼容性:v2.0.0 完全向后兼容 v1.x 的数据文件。三种存储格式(JSON / XML / Binary)和 WAL 文件均可直接加载,无需任何迁移。
| 变更项 | v1.x | v2.0.0 |
|---|---|---|
| 相似度计算 |
SimilarityFunc 委托 |
ISimilarity<T> 静态抽象接口 —— JIT 为每个具体类型生成特化机器码,零虚分派 |
| 向量数据所有权 | 各索引内部各自存储向量 |
IVectorStore 抽象 —— 索引仅管理拓扑结构(图/树/倒排),向量由存储层统一管理 |
| 功能 | 说明 |
|---|---|
| 6 种新距离度量 | Manhattan(L1)、Chebyshev(L∞)、Pearson 相关、Hamming、Jaccard、Canberra —— 加上原有 3 种(Cosine / Euclidean / DotProduct),共 9 种内置度量 |
| 自定义相似度 |
[QuiverVector(128, CustomSimilarity = typeof(MySimilarity))] —— 接入任意 ISimilarity<float> 结构体 |
| IVectorStore 抽象 |
HeapVectorStore(GC 堆)—— 可插拔的向量存储后端 |
| 优化项 | 详情 |
|---|---|
| 全度量 SIMD 加速 | 全部 9 种相似度实现均使用内部 VectorMath / Vector<float> 路径,自动适配 SSE4 / AVX2 / AVX-512 寄存器宽度,无额外 NuGet 依赖 |
| 零开销分派 |
ISimilarity<T> 配合 static abstract + readonly struct,JIT 在调用站点直接内联 TSim.Compute() —— 无委托间接调用 |
| # | 章节 |
|---|---|
| 01 | 版本说明 |
| 02 | 产品概述 |
| 03 | 架构概述 |
| 04 | 快速开始 |
| 05 | 核心概念 |
| 06 | 距离度量 |
| 07 | 索引类型 |
| 08 | CRUD 操作 |
| 09 | 向量搜索 |
| 10 | 持久化存储 |
| 11 | 迁移系统 |
| 11a | 模式迁移 |
| 12 | 多向量字段支持 |
| 13 | 线程安全与并发 |
| 14 | 生命周期管理 |
| 15 | 配置选项 |
| 16 | 内部实现细节 |
| 17 | 完整示例 |
| 18 | API 参考速查表 |
| 19 | 使用建议 |