VectorStoreIndex

切好的 Node 若每次进程启动都重新 embedding,本地 Ollama 会把 CPU 打满,云端则重复计费。VectorStoreIndex 把 Node 的向量放进向量库、原文放进 docstore;一次 from_documents 之后用 persist 落盘,下次 load_index_from_storage 直接问。

段末注释VectorStoreIndex = 默认的语义索引:对每个 Node 算 embedding,按 query 向量取 top-k。它不等于整条 RAG 产品,只覆盖「向量召回」这一段。

from_documents 切块嵌入、persist 落盘、retrieve/query 带回 source_nodes(科普示意)


1. 一句话定位

维度 内容
角色 知识层的向量索引:Node ↔ embedding ↔ 相似度召回
输入 → 输出 list[Document]list[Node]VectorStoreIndexNodeWithScore / Response
典型调用入口 from_documents()storage_context.persist()as_retriever()as_query_engine().query()
与 LangChain / LangGraph 近邻是 LangChain VectorStore;LangGraph 节点里调用 retriever.retrieve / qe.query,不要在图里重建索引

出现背景from_documents 把切分 + 嵌入 + 入库收成一条捷径,默认用内存 SimpleVectorStore。生产把 vector_store 换成 Qdrant / pgvector 等,索引 API 不变


2. 前置依赖与环境

1
2
3
pip install -U llama-index-core llama-index-llms-ollama llama-index-embeddings-ollama
ollama pull qwen3.5:9b
ollama pull nomic-embed-text
  • Python 3.10+;版本锚点 llama-index-core 0.14.x
  • 必须同时设 Settings.embed_modelSettings.llm,否则会打 OpenAI
  • 示例用同步 query();FastAPI 用 await qe.aquery(...)

3. 实现逻辑

1
2
3
4
5
6
7
8
1. Settings.embed_model / Settings.llm 设成本地 Ollama
2. 准备 Document(稳定 doc_id + metadata)
3. VectorStoreIndex.from_documents(docs, transformations=[SentenceSplitter...])
内部:切 Node → embed → 写入 vector_store + docstore
4. index.storage_context.persist("./storage")
5. 新进程:load_index_from_storage(StorageContext.from_defaults(persist_dir="./storage"))
6. retriever.retrieve(q) → list[NodeWithScore]
7. qe.query(q) → Response;引用在 response.source_nodes

字段级变形

1
2
3
4
5
6
Document(text="……GAPDH……小鼠……", metadata={species:"小鼠"})
→ Node.embedding = list[float] # 维数由 nomic-embed-text 决定
→ retrieve("实验对象是什么物种?")
NodeWithScore(node=TextNode(...), score=0.8x)
→ query
Response(response="小鼠……", source_nodes=[NodeWithScore, ...])

4. 原理说明

主轴是:from_documents 一次走完切分与嵌入;persist 只是把已经算好的向量和 Node 快照写出去。

1
2
3
4
5
6
7
8
9
10
11
1. from_documents 读取 transformations 或 Settings.text_splitter,得到 TextNode[]
2. 对每个 Node 调 embed_model.get_text_embedding(get_content(EMBED))
3. 向量写入 vector_store;Node 正文与 metadata 写入 docstore;图结构写入 index_store
4. persist(persist_dir) 把上述三个 store 序列化到目录(默认内存店是 JSON 类文件)
5. load_index_from_storage 按同一 persist_dir 重建 StorageContext 再加载 index
6. as_retriever:把 query 编码成向量,相似度 top-k,返回 NodeWithScore
7. as_query_engine:retrieve 之后把 Node 文本交给 llm 合成;source_nodes 仍挂检索结果
8. 外部向量库:StorageContext.from_defaults(vector_store=...);向量在远端,persist 可能只存 index 元数据
少了第 2 步(未设 embed_model)→ 请求 OpenAI embedding
少了第 4 步 → 每次启动重复第 1~3 步
少了第 7 步只 print(str(response)) → 无法审计引用

VectorStoreIndex.from_documents(documents, ...)(类方法)出现在步骤 1。功能:切分 + 嵌入 + 建索引。可选 show_progress=Truestorage_context=embed_model= 局部覆盖。

StorageContext 出现在步骤 3~5。功能:凑齐 vector_store / docstore / index_store。默认全是 Simple* 内存实现。

as_retriever(similarity_top_k=...) 出现在步骤 6。retrieve(str) → list[NodeWithScore]score 含义随后端(余弦 / 内积 / 距离),不要跨后端比较绝对分数

as_query_engine(similarity_top_k=..., response_mode=...) 出现在步骤 7。query / aquery 返回 Response

换 Qdrant 只换 vector_store=,不要换一套 from_documents 调用习惯。对接具体库是向量库集成包的事,本篇用默认 Simple 店讲清契约。


5. 最小可运行示例

1
pip install -U llama-index-core llama-index-llms-ollama llama-index-embeddings-ollama
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
from llama_index.core import (
Document,
Settings,
StorageContext,
VectorStoreIndex,
load_index_from_storage,
)
from llama_index.core.node_parser import SentenceSplitter
from llama_index.embeddings.ollama import OllamaEmbedding
from llama_index.llms.ollama import Ollama

# 关键参数:关掉默认 OpenAI
Settings.llm = Ollama(model="qwen3.5:9b", request_timeout=120.0, temperature=0)
Settings.embed_model = OllamaEmbedding(
model_name="nomic-embed-text",
base_url="http://localhost:11434",
)

docs = [
Document(
text="定量 PCR 以小鼠肝脏 GAPDH 为内参。实验对象为 SPF 级 C57BL/6 小鼠。",
doc_id="paper_001",
metadata={"species": "小鼠", "section": "methods"},
),
Document(
text="Python 的列表推导式用一行从可迭代对象生成列表。",
doc_id="py_001",
metadata={"species": "无关", "section": "lang"},
),
]

splitter = SentenceSplitter(chunk_size=128, chunk_overlap=20)
index = VectorStoreIndex.from_documents(
docs,
transformations=[splitter],
show_progress=True,
)
index.storage_context.persist("./storage_vs_demo")

# 模拟新进程加载,避免重复 embedding
index2 = load_index_from_storage(
StorageContext.from_defaults(persist_dir="./storage_vs_demo")
)

retriever = index2.as_retriever(similarity_top_k=2)
hits = retriever.retrieve("实验对象是什么物种?") # 输入
print([(h.score, h.node.metadata, h.node.text[:24]) for h in hits])
# 输出:NodeWithScore;预期第一条 metadata.species == 小鼠

qe = index2.as_query_engine(similarity_top_k=2)
resp = qe.query("实验对象是什么物种?")
print(resp)
for n in resp.source_nodes:
print("cite", n.node.metadata, n.score)
# 预期形态:答案含「小鼠」;source_nodes 至少一条 section=methods

6. 重要配置参数

参数(API 名) 类型 / 默认值 功能说明 作用与影响 参考起点 / 常用范围 配置指导
Settings.embed_model Embedding 实例 / 默认 OpenAI 入库与 query 共用的编码器 未设打云端;换模型维数变了必须重建 OllamaEmbedding(model_name="nomic-embed-text") 与生成模型分开 pull
Settings.llm LLM 实例 / 默认 OpenAI as_query_engine 合成答案 未设则 query 打云端;retriever 不需要它 Ollama(model="qwen3.5:9b") 只检索时可后设
transformations list,可选 from_documents 用的切分等变换 不传则用 Settings 默认 splitter 显式 SentenceSplitter 与线上 query 无关,只影响入库
similarity_top_k int,retriever/qe 默认常为 2 召回 Node 条数 过小漏证据;过大噪声与费用升 5~10 入门 有 rerank 可先放大
response_mode str,qe 默认 "compact" 多 Node 如何喂给 LLM refine 更稳更贵;tree_summarize 适合汇总 compact / refine / tree_summarize 单事实用 compact
persist_dir str,默认 ./storage Simple 店序列化目录 指错目录等于空索引 项目内固定路径 与 embed 模型 ID 一起写进 README
storage_context.vector_store 向量库适配器,可选 换成 Qdrant 等远端 远端库 persist 语义变了,向量不在本地 JSON 生产必换 API 仍 from_documents / from_vector_store
show_progress bool,默认 False 嵌入进度条 只影响可观测性 本地调试 True 生产日志用 callback

7. 适用 / 不适用

维度 适用 不适用
任务形态 语义问答、同义改写查询 必须精确命中基因号 / 货号——要加 sparse/hybrid,不是本索引单独能解决
集成约束 可接受一次全库 embedding 小时级增量且文件频繁改——配 IngestionPipeline + 外部向量库
工程阶段 原型到单机落盘 多实例并发写同一 Simple 目录——换带锁的外部库

工具环、审批、会话恢复:索引仍用本对象,编排走 LangGraph。


8. 易踩坑

  1. 未设 Settings.embed_model:本地 LLM 已通,嵌入仍打 OpenAI。
  2. 每次启动 from_documents:persist 形同虚设。
  3. persist 后换了嵌入模型还 load:维数/空间不一致,分数乱、召回崩,需要重建。
  4. print(resp):丢掉 source_nodes,无法证明答的是 methods 那条 Node。

小结

  • from_documents = 切分 + 嵌入 + 写入 store;persist / load_index_from_storage 避免重复嵌入。
  • 检索用 as_retriever;问答用 as_query_engine,引用看 source_nodes
  • 换 Qdrant 只换 vector_store,不要换调用习惯。
  • 混合检索、Router、重排是后续专篇;本篇只保证向量索引契约正确。

参考链接

-------------本文结束感谢您的阅读-------------