news 2026/9/13 22:12:40

turbovec LlamaIndex 集成实战:基于 TurboQuantVectorStore 构建量化向量检索管线

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
turbovec LlamaIndex 集成实战:基于 TurboQuantVectorStore 构建量化向量检索管线

turbovec LlamaIndex 集成实战:基于 TurboQuantVectorStore 构建量化向量检索管线

【免费下载链接】turbovecA vector index built on TurboQuant, written in Rust with Python bindings项目地址: https://gitcode.com/GitHub_Trending/tu/turbovec

导读

本文讲解 turbovec 官方提供的 LlamaIndex 向量存储适配器 ——turbovec.llama_index.TurboQuantVectorStore。它是一个继承自 LlamaIndexBasePydanticVectorStore的向量存储实现,底层由IdMapIndex(Rust 核心 + Python 绑定)支撑,向量被量化为 2~4 bit/维。它的公共接口与llama_index.core.vector_stores.simple.SimpleVectorStore完全一致,可作为纯内存简单向量存储的零改动替换品,适用于希望在 RAG 管线中大幅压缩 embedding 内存占用的场景。读完本文,你将掌握该存储的安装、构造方式、相似度模式、双删除入口、过滤查询、异步调用、持久化与线程安全契约,并理解其背后的源码实现细节。


安装

TurboQuantVectorStore是 turbovec 的可选集成模块,需要连同llama-index-core一起安装:

pip install turbovec[llama-index]

在 turbovec-python/pyproject.toml 中可以看到该 extra 的依赖声明:llama-index = ["llama-index-core>=0.12.1"]。如果直接 importturbovec.llama_index而环境中没有安装llama-index-core,turbovec-python/python/turbovec/llama_index.py 会抛出带安装提示的ImportError,不会留下费解的运行时错误。

基础用法

TurboQuantVectorStore接入标准的 LlamaIndex 构建流程,只需把它作为StorageContextvector_store传入:

from llama_index.core import VectorStoreIndex, StorageContext from turbovec.llama_index import TurboQuantVectorStore vector_store = TurboQuantVectorStore() storage_context = StorageContext.from_defaults(vector_store=vector_store) index = VectorStoreIndex.from_documents(documents, storage_context=storage_context) retriever = index.as_retriever(similarity_top_k=5)

向量维度(dim)不需要你手动指定:首次add()时从 embedding 模型输出的维度自动推断并锁定。这一"惰性构造"设计在 turbovec-python/python/turbovec/llama_index.py 的构造函数中实现——未传入index时创建一个惰性IdMapIndextest_lazy_dim_locked_on_first_add等测试在 turbovec-python/tests/test_llama_index.py 中验证了该行为(首次 add 后dim被锁定为 64)。

构造方式

TurboQuantVectorStore支持三种构造路径,分别适用于"完全惰性""显式量化位宽""加载既有索引"三种场景:

# No-arg: lazy. dim is inferred from the first add. vector_store = TurboQuantVectorStore() # from_params: same lazy behaviour, plus an explicit bit_width. vector_store = TurboQuantVectorStore.from_params(bit_width=4) # Pre-built index: bring your own IdMapIndex (e.g. one you loaded from disk). from turbovec import IdMapIndex vector_store = TurboQuantVectorStore(index=IdMapIndex(1536, 4))
  • bit_width取值限定为{2, 3, 4},索引创建后不可变更;clear()之后依然保持该值(见下文"清空"小节)。
  • from_params(dim=None, bit_width=4, similarity="cosine")在传入dim时立即构造非惰性索引(后续 add 的 embedding 维度必须匹配,否则在 llama_index.py 抛出带维度说明的ValueError),省略dim时与无参构造等价。
  • bit_width=3同样受支持,test_bit_width_3_round_trip 验证了 3-bit 下的写入与查询闭环。

相似度模式

similarity关键字(构造函数与from_params均支持)决定query返回的similarities如何计算,并在 store 生命周期内固定:

  • "cosine"(默认)。节点 embedding 在 add 时做 L2 归一化,查询 embedding 在查询时做 L2 归一化,因此result.similarities是落在[-1, 1]区间的真余弦相似度,排序结果不受 embedding 量级影响、与SimpleVectorStore一致,可以安全地喂给基于相似度阈值截断的后处理逻辑。零向量无法归一化,原样保留,对所有查询都打 0 分。
  • "dot_product"。向量原样存储与查询:result.similarities是原始内积,排序结果对 embedding 量级敏感(适用于量级本身携带语义信息的场景)。
vector_store = TurboQuantVectorStore(similarity="dot_product")

similarity是 turbovec 的扩展参数:参考实现SimpleVectorStore无条件计算余弦,因此在默认参数下,按参考实现编写的代码行为完全不变。底层实现位于 turbovec-python/python/turbovec/_similarity.py,其中l2_normalize_rows用归一化因子替换技巧保证零向量行原样通过;cosine 模式下归一化在写锁之外完成(纯计算),不会阻塞并发读。

两个 delete 入口

LlamaIndex 的向量存储协议定义了两种语义不同的删除入口,TurboQuantVectorStore均按参考实现对齐实现:

delete(ref_doc_id: str)—— 删除整个源文档

删除ref_doc_id匹配的每一个节点(一个父文档及其所有切块),一次调用搞定:

vector_store.delete("my-source-document-123")

不存在的ref_doc_id被静默忽略。注意一个与参考实现对齐的边界语义(见 test_delete_none_is_a_noop_matching_reference):无父节点的节点会被登记到字面字符串"None"下(node.ref_doc_id or "None"),因此delete(None)是 no-op 而非清空所有无父节点;要定向删除无父节点请调用delete("None")

delete_nodes(node_ids, filters)—— 删除指定切块

node_idsfilters或两者求交的结果删除节点。缺失的node_id静默忽略:

# By node_id vector_store.delete_nodes(node_ids=["abc-123", "def-456"]) # By metadata filter from llama_index.core.vector_stores.types import ( MetadataFilter, MetadataFilters, FilterOperator, ) filters = MetadataFilters( filters=[MetadataFilter(key="tier", value="archived", operator=FilterOperator.EQ)], ) vector_store.delete_nodes(filters=filters) # Both: intersect — delete only nodes in this list that ALSO match the filter vector_store.delete_nodes(node_ids=["abc-123"], filters=filters)

与参考实现有两处刻意的差异(源码注释见 llama_index.py):

  • 两个参数均为None时是no-op。参考实现此时会删除全部节点(其build_metadata_filter_fn(None)恒真),而协议本身并未规定 delete-all,且库已提供专门的clear(),因此 no-op 是更安全的解读,避免误删。
  • filters中包含嵌套MetadataFilters组时本实现会递归求值,而参考实现直接抛ValueError(这是 turbovec 的超集扩展,见 test_query_nested_filter_groups_are_supported_superset)。

clear()—— 全部清空

vector_store.clear()

重置整个 store,同时保留配置的bit_width。清空后的 store 立即可用,dim由下一批 add 重新推断(test_clear_resets_store 验证了bit_width保留、dim回到惰性状态)。

查询

LlamaIndex 内部调用query(VectorStoreQuery)。如果你走VectorStoreIndex.from_documents(...)流程,通常不会直接调用它——由 retriever 代劳。直接使用示例:

from llama_index.core.vector_stores.types import VectorStoreQuery result = vector_store.query(VectorStoreQuery( query_embedding=[...], similarity_top_k=5, )) # result.nodes, result.similarities, result.ids

query_embedding是必填项。turbovec 不负责对查询文本做 embedding,那是 retriever / query engine 的职责。此外,query.mode只支持VectorStoreQueryMode.DEFAULT;MMR / SVM / hybrid 等模式需要全精度向量,而 turbovec 量化后即丢弃全精度,因此 query() 会显式抛出NotImplementedError(而不是旧实现那样静默按 DEFAULT 处理)。

过滤查询

VectorStoreQuery接受filtersnode_idsdoc_ids三个过滤维度,同时提供多个时全部取交

from llama_index.core.vector_stores.types import ( MetadataFilter, MetadataFilters, FilterCondition, FilterOperator, VectorStoreQuery, ) filters = MetadataFilters( filters=[ MetadataFilter(key="tier", value="pro", operator=FilterOperator.EQ), MetadataFilter(key="year", value=2024, operator=FilterOperator.GTE), ], condition=FilterCondition.AND, ) result = vector_store.query(VectorStoreQuery( query_embedding=[...], similarity_top_k=5, filters=filters, node_ids=["chunk-1", "chunk-2", "chunk-3"], # restrict to these chunks doc_ids=["src-doc-42"], # restrict to chunks of this source doc ))

支持的MetadataFilter操作符EQNEGTLTGTELTEINNINTEXT_MATCHTEXT_MATCH_INSENSITIVECONTAINSANYALLIS_EMPTY条件ANDORNOT。嵌套MetadataFilters可用(递归求值,参考实现不支持)。

过滤语义与SimpleVectorStore参考实现保持一致,几个关键细节(实现见 llama_index.py):

  • 缺失 key 的语义:除IS_EMPTY外,所有正操作符在 metadata 缺失该 key 时返回False;而负操作符NE/NIN对缺失 key真空满足("没有颜色"自然是"颜色不是红")——这与 llama-index-core ≥ 0.14 的现行行为一致(见 test_query_ne_filter_keeps_nodes_missing_the_key)。
  • TEXT_MATCH是区分大小写的子串匹配,TEXT_MATCH_INSENSITIVE才做两边lower()的大小写折叠;非字符串操作数会抛TypeError
  • 过滤在打分之前解析为内部 handle 白名单。过滤查询从过滤后的集合中返回最多similarity_top_k条——即使高分候选恰好被过滤排除,你也不会少拿到结果(见 test_query_filter_selective_returns_top_k_from_matches)。
  • node_ids/doc_ids列表不构成任何限制,等价于省略该参数。这是框架自身调用约定的结果:VectorStoreIndex.as_retriever会传入node_ids=list(index_struct.nodes_dict.values()),而stores_text=True的 store 该列表恒为空——它表达的是"不限制",若按"匹配空集"处理则每个 retriever 查询都会返回空结果(issue #130 的维护者裁定)。与此相反,get_nodes/delete_nodesnode_ids就是选择集,显式空列表选择的是"无"。

过滤条件的组合逻辑(AND/OR/NOT、嵌套组、同一 key 多条件)均有对应测试覆盖,如 test_query_contradictive_same_key_and_returns_empty 验证了同 key 两个 EQ 条件 AND 后结果为空。

Get nodes

nodes = vector_store.get_nodes(node_ids=["chunk-1", "chunk-2"]) nodes = vector_store.get_nodes(filters=filters) nodes = vector_store.get_nodes(node_ids=["chunk-1", "chunk-2"], filters=filters) # intersect

从 side-car(见下文持久化小节)重建并返回List[BaseNode],缺失的node_id静默跳过。与SimpleVectorStore不同(参考实现此处直接抛NotImplementedError,因为它不存节点),turbovec 在 side-car 中保存节点文本与元数据,因此能直接返回内容完整的TextNodenode_ids是显式选择集:空列表选择"无"并返回[]delete_nodes同理,空列表是 no-op);传入node_ids时结果按请求顺序返回,否则按存储顺序(test_get_nodes_returns_requested_id_order)。

节点保真度:side-car 存储的是node_to_metadata_dict(node, remove_text=False, flat_metadata=False)的完整序列化字典,因此query/get_nodes/ persist 往返都能还原出完整的BaseNode子类(TextNode/ImageNode/IndexNode),包括PREVIOUS/NEXT/PARENT/CHILD等全部关系、excluded_*_metadata_keys、模板字段、start/end_char_idxmimetype等(见 test_query_returns_node_with_full_field_fidelity)。

Upsert 语义

对已存在node_id调用add()替换既有条目,符合用户在重新索引同一批切块时的预期:

node = TextNode(text="v1", embedding=[...]) vector_store.add([node]) # Same node_id, different text/embedding → replaces. updated = TextNode(text="v2", id_=node.node_id, embedding=[...]) vector_store.add([updated]) assert len(vector_store._index) == 1

同一add()批次内重复node_id会抛ValueError(需先自行去重)。这与 LangChain / Haystack 适配器"静默保留最后一条"的行为不同:这里是硬错误,避免意外重复静默丢弃节点。实现上通过 turbovec-python/python/turbovec/_dedup.py 的resolve_duplicates(ids, DuplicatePolicy.REJECT)在写入前拦截,并在失败时回滚已插入的 map 条目,保证 store 不被写坏(见 test_add_raises_on_intra_batch_duplicate_node_id)。此外,upsert 时若新 embedding 维度校验失败,旧条目会被完整保留(test_add_upsert_dim_mismatch_preserves_existing_node)。

异步接口

每个公开方法都有异步对应版本,可直接用于 LlamaIndex 的异步 retriever / query engine 路径:

await vector_store.async_add(nodes) result = await vector_store.aquery(VectorStoreQuery(...)) fetched = await vector_store.aget_nodes(node_ids=[...]) await vector_store.adelete("ref-doc-id") await vector_store.adelete_nodes(node_ids=[...]) await vector_store.aclear()

实现上,每个方法都通过一次asyncio.to_thread把同步体丢到工作线程执行(llama_index.py),事件循环在大型 add / 查询期间保持响应——而BasePydanticVectorStore的默认异步实现是直接内联调用同步体,会阻塞事件循环整个操作时长。

取消语义是"部分"的,这个区别很重要

  • asyncio.wait_fortask.cancel()或客户端断开连接会立即把控制权交还调用方(旧实现协程会跑完,超时永远不会触发)。
  • 但它不决定 add 的结局:若工作线程已开始执行,调用会完整跑完(Rust 核心内的工作不可中断),add 整体提交;若执行器已饱和、调用尚未开始就被取消,则什么都没有写入。因此被取消的async_add结局未知:可能已完整提交,也可能从未开始。由于重加同一node_id是覆盖写,无论如何重试都是安全的。
  • 唯一确定的是全有或全无:store 永远不会处于撕裂状态。
  • 超时不代表工作消失:事件循环关闭(asyncio.run收尾或loop.shutdown_default_executor())会等待工作线程,因此超时后立即退出进程仍可能阻塞在未完成调用的剩余时长上。

持久化与加载

直接(文件 stem)接口

vector_store.persist("./store/vectors.json") # ... later ... vector_store = TurboQuantVectorStore.from_persist_path("./store/vectors.json")

persist_path被当作路径stem:二进制索引与 JSON side-car 以{stem}.tvim{stem}.nodes.json两个文件并排落盘。persist_path上的扩展名(如 StorageContext 默认的.json)会被替换。节点元数据必须是 JSON 可序列化的。若{stem}.nodes.json{stem}.tvim索引失步(部分拷贝、过期备份、篡改),from_persist_path立即抛ValueError,而不是等查询时深埋一个KeyError——这由 _persist.py 中的check_persisted_handles(side-car handle 集与索引双射校验)、check_sidecar_keysets(两个同 key 结构的一致性校验)与check_schema_version(schema 版本门禁)共同保证,失步、重复 handle、回卷的next_u64水位、遍历性 namespace 等损坏场景均有对应测试覆盖(见 turbovec-python/tests/test_llama_index.py)。

persist对目标是原子的:两个文件都先写入同目录的临时文件再os.replace移入到位(Windows 上对瞬态共享冲突做了指数退避重试),因此一次失败的 persist(例如元数据不可 JSON 序列化)不会破坏同一 stem 下此前持久化的 store,也不会残留临时文件。

相似度模式会被记录在{stem}.nodes.json中并由from_persist_path恢复。在模式字段出现之前持久化的 store保存的是原始未归一化向量,加载时进入"dot_product"模式——这正是它写入时的打分方式,无需迁移。

通过StorageContext

该 store 与SimpleVectorStore一样兼容StorageContext.from_defaults(persist_dir=...)

# Persist storage_context.persist(persist_dir="./store") # Load vector_store = TurboQuantVectorStore.from_persist_dir(persist_dir="./store") storage_context = StorageContext.from_defaults( vector_store=vector_store, persist_dir="./store", )

from_persist_dir(persist_dir, namespace="default", fs=None)构造带命名空间的文件名({persist_dir}/{namespace}__vector_store.json)并委托给from_persist_path。多个命名空间的 store 可以共享同一持久化目录——包括点分命名空间v1.2v1.3),它们映射到不同的文件对(v1.2__vector_store.tvim/v1.2__vector_store.nodes.json等)。namespace命名的是persist_dir内部的一个 store,因此必须非空,且不得包含路径分隔符、..:(形如C:foo的 Windows 盘符相对名会逃逸persist_dir);违反即抛ValueError(校验见 llama_index.py 的_validate_namespace,逃逸测试见 test_from_persist_dir_traversal_does_not_read_outside)。其余字符串(字母数字、连字符、下划线、点)均接受。

旧版兼容:早期 turbovec 在点分命名空间下持久化的 store 以截断文件名落盘(namespacev1.2对应v1.tvim),加载时仅在正确文件名缺失时走旧文件名回退,下一次persist会写回正确文件名(test_from_persist_dir_loads_legacy_mangled_dotted_store)。

仅配置的往返

config = vector_store.to_dict() # {"bit_width": 4, "dim": 1536, "similarity": "cosine"} fresh = TurboQuantVectorStore.from_dict(config) # empty store with the same config

to_dict/from_dict只序列化 store 的配置bit_widthdimsimilarity);节点数据通过persist/from_persist_path往返。

该 store 还支持带完整数据保真度的pickle(例如用于multiprocessingworker)与copy.copy/copy.deepcopy——两者都返回完全独立的 store(不存在共享底层索引的浅拷贝)。这是通过自定义__getstate__/__setstate__(索引经IdMapIndex.to_bytes()/from_bytes()走内存.tvim字节格式)实现的,避免了默认实现静默丢弃 Rust 索引导致反序列化出空 store 的坑(llama_index.py)。

线程安全

该 store 对并发多线程使用是安全的,契约如下:

  • 读并发且可扩展queryget_nodes不取锁;底层索引在打分期间释放 GIL,因此多个线程的独立查询可以真正重叠并扩展。
  • 写串行化adddeletedelete_nodesclearpersist在 per-store 锁上串行。async_add/a*变体委托给同一套加锁体,因此并发 add 会签发唯一 handle——任何批次都不会因 handle 冲突被拒绝或丢失(锁还保证_next_u64 += 1的原子性,见 llama_index.py)。
  • 读与写重叠时看到的是写前或写后状态,绝无撕裂态。在高并发 churn 下,查询可能瞬时返回少于similarity_top_k条结果(查询中途被删除的命中会被跳过)。

契约覆盖的部分:

  • 无跨调用原子性。调用侧的"先检查后执行"序列(如get_nodesdelete_nodes)可能与其他写者交错。批量写对读者也不是原子的:与对既有node_id的重新add重叠的查询可能短暂看到该 id 同时挂在新旧两个条目下。
  • persist与写串行化(因此总能拍到一致快照);persist 期间读可以继续。
  • 两个 store 写同一路径是安全的。多个线程对同一目的地并发persist,各自原子发布,最后写入者胜出;调用方永远不会看到撕裂文件,也不会看到仅由另一写者造成的错误。谁胜出不定义。
  • 多进程访问不支持

已知限制

  • 不支持 MMR。最大边际相关(max-marginal-relevance)检索需要每个候选的全精度 embedding 来计算两两多样性;turbovec 在量化后即丢弃全精度向量。
  • get(text_id)会抛异常而非返回向量——同理,全精度 embedding 不可恢复。需要原始 embedding 请自行维护并行 docstore。
  • 不支持fsspec文件系统persistfrom_persist_pathfrom_persist_dir均接受本地路径,请保持fs=None(默认值),传入非 None 的fs会显式抛NotImplementedError
  • 仅支持 JSON 可序列化元数据。节点元数据以 JSON 存入 side-car,不可序列化的值会在 persist 时报错——与SimpleVectorStore.persist的约束一致(且 turbovec 在 add 时即做 JSON 强制转换,过滤所依据的元数据视图与返回给调用方的视图严格一致、持久化前后不变,见 test_filters_operate_on_the_metadata_that_is_returned)。
  • stores_text = True。与SimpleVectorStore不同,turbovec 在 side-car 中保存节点文本,使查询结果直接返回内容完整的TextNode,无需依赖独立 docstore。若你的管线原本期望文本存于别处,这个差异是无害的——框架仅将stores_text视为信息性字段。

进一步阅读

  • 完整集成源码:turbovec-python/python/turbovec/llama_index.py
  • 共享工具模块:相似度模式 _similarity.py、持久化一致性检查 _persist.py、批内去重 _dedup.py
  • 测试套件:turbovec-python/tests/test_llama_index.py(覆盖协议完整性、过滤语义、异步、持久化往返、损坏检测与端到端框架接线)
  • 其余集成适配器:agno、haystack、langchain

【免费下载链接】turbovecA vector index built on TurboQuant, written in Rust with Python bindings项目地址: https://gitcode.com/GitHub_Trending/tu/turbovec

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/13 22:10:56

UnoCSS MDC Extractor 指南:为 Markdown 组件语法提取原子类

UnoCSS MDC Extractor 指南:为 Markdown 组件语法提取原子类 【免费下载链接】unocss The instant on-demand atomic CSS engine. 项目地址: https://gitcode.com/GitHub_Trending/un/unocss UnoCSS 的 unocss/extractor-mdc 是一个专用于 MDC(Ma…

作者头像 李华
网站建设 2026/9/13 22:10:47

MySQL批量更新不同值的几种实现方案:从CASE WHEN到临时表JOIN

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/13 22:06:59

如何用 dectl backup 和 restore 备份并恢复 DataEase 数据?

如何用 dectl backup 和 restore 备份并恢复 DataEase 数据? 【免费下载链接】dataease 🔥 人人可用的开源 BI 工具,数据可视化神器。An open-source BI tool alternative to Tableau. 项目地址: https://gitcode.com/GitHub_Trending/da/d…

作者头像 李华