ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Utopia 决策记录 0035 深度解读:HNSW 向量索引为何必须由 build_vector_index 任务构建

Utopia 决策记录 0035 深度解读:HNSW 向量索引为何必须由 build_vector_index 任务构建 后端前端人工智能RAG知识图谱知识管理搜索引擎【免费下载链接】utopiaWorlds first open-source enterprise world model.项目地址https://gitcode.com/gh_mirrors/ont/utopia点击查看免费下载本文是 Utopia 开源项目决策记录 0035 的扩展解读。该记录回答一个具体工程问题当向量列本身不知道维度、索引又不能在迁移事务里建时如何在多租户共享表上为 HNSW 索引找到一个既能落地、又不牺牲写入与查询一致性的路径。读完本文你将掌握 Utopia 中索引按维度惰性构建的完整机制、pgvector 0.8 迭代扫描三个参数的实测调优结论以及为什么维度写成 SQL 字面量是这里不可省的一步。背景无索引的向量列根因是维度未知Utopia 的两条向量检索路径在 0035 落地之前都处于全表扫描状态chunks.embedding混合检索hybrid query对库中每一个嵌入分块计算余弦距离再排序取前十entities.profile_embedding类型消解按主语逐个查询近邻——每轮 60 个主语、每个 1 次、一个任务跑 10 轮也就是同一张表被逐主语扫 600 次对应 0016 中每个主语扫一次实体表的 C2 循环。根因在列定义本身embedding是vector类型不带(N)维度标注。维度跟随工作区选择的嵌入模型例如 OpenAI 的text-embedding-3-small是 1536 维、text-embedding-3-large是 3072 维而任何一条编号迁移都不知道未来会用哪个模型因此迁移阶段无法为列建一个带固定维度的 HNSW 索引。这就是让任务来建而不是让迁移来建的第一推动力。记录还特别指出0019第二时钟可以回拨要求记录轴record-axis过滤保留在查询上索引必须去适配这个过滤而不是反过来把过滤去掉——这是后面部分索引 relaxed_order设计的前提之一。先测量再决定两组实测数据记录以Measured before deciding开篇这是本仓库决策记录的一贯纪律见约定代码记录建了什么这里记录为什么这么建)。第一组同一表内三库 6 万行、1024 维随机向量pgvector 0.8.6。场景结果现状查询5 万行库顺序扫描195 ms现状查询1 万行库chunks_kb_idx 排序39 ms表达式 HNSW 索引6 万行串行构建87 s469 MB改写后的查询5 万行库4.5 ms10k 库强制走 HNSWiterative_scan off24 行里只回 3 行同上relaxed_order24 行全回7.9 ms由此推算每千个被检索的分块每条查询大约付出 4 ms。几百个分块时无感但 10 万行的库每问一条就要 0.4 秒而类型消解每轮要付出 60 次——这就是必须引入索引的直接证据。第二组真实代码落库后在副本上的复测。531 个真实分块 3269 个真实实体画像1024 维旁边再写入 5 万个合成分块场景结果改写后vector_search无索引5 万行库顺序扫描 top-N 排序378.6 ms同上索引就位索引扫描6.0 ms默认maintenance_work_mem64 MB 串行建 50,531 行4 分 20 秒13,930 元组后图装不下同上512 MB60.6 s394 MB1415 画像库上 200 个真实实体查询强制 HNSWrecall10 0.997按距离按 id 为 0.917差距来自第十位并列522 个真实分块查询各库约占索引 1%scan_mem_multiplier 10.705relaxed_order154 个列表回不满32 ms/问不开relaxed_order为 0.075同上max_scan_tuples100,000不变0.705154 个短列表同上scan_mem_multiplier 4recall10 1.0无短列表74 ms/问16 与 4 相同229 个真实分块、查询向量远离库19,345 元组后回 0 行105 ms恢复chunks_kb_idx让规划器自选chunks_kb_idx 排序1.5 ms旁边的 5 万行库仍走 HNSW3.8 ms记录强调所有强制走 HNSW的行都是在副本上删掉kb_id索引、剥夺规划器选择权后测得的——它们代表的是下限floor而不是日常预期。这直接决定了下面扫描参数不能沿用默认值的结论。决策一任务是任务不是迁移为什么不能用编号迁移两个原因叠加CREATE INDEX CONCURRENTLY不能运行在 sqlx 给迁移包裹的事务内部不带CONCURRENTLY的普通CREATE INDEX会在整个构建期间对chunks持ACCESS EXCLUSIVE锁多租户共享表被锁死数分钟不可接受。方案第一次写入某个维度的向量时调用vector_index::request见 vector_index.rs为该表 维度组合排一条build_vector_index任务JOB_KIND build_vector_index除非同种任务已经在排队enqueue_unless_queued见 jobs.rs任务在事务外、普通连接上执行CREATE INDEX CONCURRENTLY IF NOT EXISTS。构建会话的三个关键设置build 函数SET maintenance_work_mem 512MBHNSW 构建要把图放进内存。默认 64 MB 到约一万四千行 1024 维就装不下了之后每行都落盘再读5 万行要 4 分 20 秒512 MB 时 60.6 秒。该设置只在构建的瞬间占用结束后RESETSET max_parallel_maintenance_workers 0并行构建需要共享内存而 Docker 默认 64 MB 的/dev/shm会报could not resize shared memory segment。串行 6 万行约 90 秒到处能跑失败恢复上次构建到一半留下的无效索引indisvalid false先DROP INDEX CONCURRENTLY再建——否则IF NOT EXISTS会把它当成已经建好而跳过。写入路径的代价分层索引建好后进程内记住OnceLockMutexHashSet见 known()之后每次写入只是一次 HashSet 查找索引未建的那一两分钟里每次写入多一次目录查询和一次没排着才排的插入代价可忽略。写入触发点位于 documents.rs 的 set_embeddingsUPDATE chunks SET embedding $2之后按首条向量的长度调vector_index::request。决策二按维度的部分表达式索引索引 DDL 形状源码 build 中原样拼出CREATE INDEX CONCURRENTLY IF NOT EXISTS chunks_embedding_hnsw_1024 ON chunks USING hnsw ((embedding::vector(1024)) vector_cosine_ops) WHERE vector_dims(embedding) 1024;cast 给维度(embedding::vector(N))把列类型缺的维度补上pgvector 才知道建几维的图谓词隔离维度WHERE vector_dims(embedding) N把其他维度的行挡在索引外。工作区换嵌入模型后新维度写出时自动再排一条任务同表上出现两个索引、两个种群而不是一次注定失败的构建算子匹配vector_cosine_ops与查询用的严格对应命名可读{table}_{column}_hnsw_{dims}例如chunks_embedding_hnsw_1024、entities_profile_embedding_hnsw_768有单测钉住见 vector_index.rs 测试。决策三维度必须写成 SQL 字面量这是三条读路径规矩中最反直觉的一条。若写成vector_dims(embedding) $2绑定参数自定义计划custom plan能用上部分索引但 sqlx 的预处理语句在执行五次后切换为通用计划generic plan通用计划会退回顺序扫描——第六次查询起索引就静默失效。因此same_dims与distance两个辅助函数把整数格式化进 SQL 字符串见 vector_index.rs并有单测the_sql_writes_the_dimension_as_a_literal钉住输出形状pub fn same_dims(column: str, dims: usize) - String { format!(vector_dims({column}) {dims}) // vector_dims(c.embedding) 1024 } pub fn distance(column: str, param: usize, dims: usize) - String { format!({column}::vector({dims}) ${param}::vector({dims})) // c.embedding::vector(1024) $2::vector(1024) }同时ORDER BY表达式必须与索引表达式逐字符一致cast 两侧、字面量维度否则规划器认不出这个表达式是索引能接住的。决策四每条近邻读都SET LOCAL三条迭代扫描参数HNSW 的读取模型是先取ef_search个候选再应用WHERE。多租户共享表上小库的行在候选里占不到几个LIMIT 10可能只回 3 行甚至 0 行——实测里这就是普通情况而非角落情况。pgvector 0.8 的迭代扫描iterative scan会继续往下走直到凑够 LIMIT它有两个停止条件都在这里放宽relaxed_order 中三条SET LOCAL一起发参数默认Utopia 取值实测依据hnsw.iterative_scanoffrelaxed_order10k 库 6 万行表上关着回 3/24开着回满 7.9 mshnsw.scan_mem_multiplier14这是实测中真正绑住扫描的那条1 倍时 522 问中 154 问回不满、recall 0.705、32 ms/问4 倍时全部回满、recall 1.0、74 ms/问16 倍与 4 倍无异hnsw.max_scan_tuples20,000100,000副本上没绑住两值同为 0.705放宽的理由是把到顶留成看得见的慢而不是沉默的少回——约半秒封顶三条设置都用SET LOCAL只在事务内生效因此两条近邻读路径都包在一个事务里见下方源码。对 pgvector 早于 0.8 的部署进程启动后探测一次current_setting(hnsw.iterative_scan, true)iterative_scan_available探测不到就不设——查询依然正确只是共享表上的小库可能少回几行那正是 0.8 修掉的问题。例外情况当规划器保有选择权时小库会留在精确路径上229 个真实分块旁放 5 万合成库规划器选chunks_kb_idx 排序 1.5 ms。因此上述强制数据是下限而非预期。决策五路径由规划器选应用侧不设阈值有chunks_kb_idx时规划器自己为 20 行、1 万行的小库选索引 排序精确路径只在 5 万行的大库上选 HNSW。应用侧不设任何阈值——阈值是对规划器的猜测猜错了精确路径和索引路径两边都会变慢。这解释了为何chunks_kb_idx必须保留它不只是普通的租户过滤索引还是规划器走精确的判断依据。决策六实体近邻走同一套机制nearest_typed_entitiesresolution.rs先读出主语自己的profile_embedding——因为维度要写进 SQL子查询给不出这个数——然后拼出同样形状的查询WITH my_docs AS ( ... ), nearest AS MATERIALIZED ( SELECT e.id, e.canonical_name, t.id AS type_id, t.key, (e.profile_embedding::vector(N) $3::vector(N))::float8 AS distance, EXISTS (...) AS same_document FROM entities e JOIN entity_types t ON t.id e.type_id -- 内连接就是那道门没判出类型的实体不是答案 WHERE e.kb_id $1 AND e.merged_into IS NULL AND e.id $2 AND e.profile_embedding IS NOT NULL AND vector_dims(e.profile_embedding) N ORDER BY e.profile_embedding::vector(N) $3::vector(N) LIMIT $4 ) SELECT canonical_name, type_id, key, distance, same_document FROM nearest ORDER BY distance 0, id; -- RESORT见下两个细节值得注意外层再排序RESORT distance 0, id常量定义relaxed_order下索引返回的次序只是大致按距离外层要补一次真排序距离并列时精确路径和 HNSW 各排各的一模一样的两条向量居首都可能不同由实体 id 定死 0不能省否则规划器会认 CTE 的次序为已排Presorted Key: distance只在并列组内排 id乱序部分原样漏出去#652维度守卫一个库里若存在两种维度的画像旧代码在上会直接报错现在vector_dims(...) N让两种维度共存且各自正确。决策七循环先收集邻居再按输入顺序推理#514一个批次 60 个主语的近邻查询彼此独立之前串行只是因为循环是串行的。nearest_typed_for_eachresolution.rs用futures_util的stream::iter(...).buffered(at_once)有界并发取回buffered按输入顺序产出下游裁决按这个顺序读推理顺序因此不变并发度 8NEIGHBOUR_SCANS常量池是 32db.rs按同时跑的短查询定的8 远低于池上限留足余量60 个全表扫一起上就是池被吃空的样子——慢请求和超时且没有任何日志说池小了DescendantsMemo按批记忆后代集合resolution.rs粗类来自抽取的小词表person、organization、product 等同批 60 个主语里同一个coarse_id反复出现同一个递归 CTE 就反复发。DescendantsMemo让同一批里每个粗类只查一次键是Uuid无粗类的主语不进 memo它没有后代这个轴见 0009未定类不设类型整张类表都是候选用Option当键会把没有类和某个真实类混在一起所以显式None → 空集短路调用侧type_resolution.rs先nearest_typed_for_each取回全部邻居列表再进入逐主语推理循环并在循环内通过 memo 取后代集。决策八2000 维以上没有索引查询保持精确MAX_DIMS 2000vector_index.rs 常量是 pgvector 对vector类型的 HNSW 上限text-embedding-3-large是 3072 维超出上限。这类维度永不排任务request直接返回None查询照常走精确路径代价是慢但正确。halfvec精度能到 4000 维记录注明那是另一种精度等有人需要再说。死路四条被测量拒绝的路记录如实保留了被否定的方案这是本仓库决策记录的硬性约定ivfflat建表时要代表性样本定列表语料增长超出样本后召回率静默下跌——对一个以可追溯证据为卖点的系统静默掉召回是错误的失败模式编号迁移两个分支各自加下一个编号合并后都干净但都不跑且见上面的事务限制丢掉记录轴过滤让索引干净适用重放replay是产品本身0019索引必须去适配过滤把连接池扩到批次大小60 个并发扫描要池 64把负载挪到 Postgresdb.rs早已反对按 worker 数量定池跨主语缓存邻居每个主语的查询向量都不同没有可共享的东西。测试钉住的契约有索引与无索引必须同答案记录每个问题问两遍——无索引一遍、有索引一遍答案必须一致覆盖最近分块、小库旁放大库把LIMIT填满、租户隔离、同表第二维度、被取代的分块、记录轴上的某个时刻。实体侧批次在并发 1 和 8 下都按输入顺序返回、有索引无索引一致双连接池能完成 60 个主语memo 返回与查询一致、批中间新增类不改变结果。测试只断言答案计划变化保持安静——这是规划器选路设计的自然推论应用层不承诺用哪条路径。对应的仓库测试包括the_nearest_chunk_is_found_however_it_is_reached.rsa_batch_gathers_its_neighbours_in_order.rsa_search_reads_the_base_as_it_was.rs未决问题两个诚实的空白真实双租户的召回率实体侧的 0.997 是真实画像 强制索引测的分块侧是在一个对抗性表上测的合成租户的向量比查询自己的库更靠近每个查询只有提高扫描内存才到 1.0。两个大租户、主题不同、由规划器把库送去索引的场景仍未测。第一个可调旋钮是hnsw.ef_search本记录用 40200 在合成跑里多花 10 ms、在副本上无变化离开的维度工作区换模型后旧维度的索引一直留着直到有人删。危害小但目前没有清理机制。如何在当前仓库中验证通读决策原文0035-a-vector-index-is-built-by-a-job.md以及记录撰写约定 docs/decisions/README.md核心实现crates/utopia-store/src/vector_index.rsrequest/build/relaxed_order三个入口、documents.rs写入触发与vector_search、resolution.rs实体近邻与批次并发任务队列build_vector_index任务经 jobs.rs 的enqueue_unless_queued去重排队worker 与 API 同进程、FOR UPDATE SKIP LOCKED消费失败按30s * attempts²退避重试批次侧调用crates/utopia-server/src/type_resolution.rs。一句话总结这套设计维度未知 → 惰性请求事务受限 → 任务构建通用计划陷阱 → 字面量维度候选不足 → 迭代扫描放宽多租户共享表 → 规划器选路、部分索引分维度共存。五条决策环环相扣任何一条被简化掉索引都会静默失效或静默少回——这正是本记录把静默失败当作第一敌的原因。赞分享后端前端人工智能RAG知识图谱知识管理搜索引擎【免费下载链接】utopiaWorlds first open-source enterprise world model.项目地址https://gitcode.com/gh_mirrors/ont/utopia点击查看免费下载相关推荐utopia 知识库中的“无名关系”为什么 related_to 必须被删除决策记录 0010 深度解读utopia 知识库中的“无名关系”为什么 related_to 必须被删除决策记录 0010 深度解读 本篇技术指南围绕 utopia 项目决策记录 0后端前端人工智能RAG知识图谱知识管理搜索引擎utopia 决策记录 0036 深度解读探索为何从构建本体转向对齐本体——Schema 对齐提案、转换表达式树与口径规则utopia 决策记录 0036 深度解读探索为何从构建本体转向对齐本体——Schema 对齐提案、转换表达式树与口径规则 本文是对 utopia 仓后端前端人工智能RAG知识图谱知识管理搜索引擎Coze Studio 的 OceanBase 向量库 HNSW 向量索引如何检查与重建Coze Studio 的 OceanBase 向量库 HNSW 向量索引如何检查与重建 当 Coze Studio 使用 OceanBase 作为向量存储人工智能AI Agent低代码RAG后端前端工作流自动化创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表