
其实日常应用过程中,向量数据库已经上手用了很长时间,日常做RAG原型、搭建私有知识库,API调用写得很熟练。但静下心来梳理底层时,心里始终堆着一堆没有彻底搞透的疑问。向量数据库真正的核心原理究竟是什么?向量集合构建完成之后,内部会不会存在缓存机制?每一次检索请求到来,向量数据又是以什么样的方式加载参与计算?业务源源不断产生新的数据源,该如何完成向量的追加写入?向量数据依靠什么机制长久保存?文档向量是每次用户请求都要重新执行向量化,还是仅在初始化阶段计算一次?当服务程序停止运行、或是机器重启之后,存量的数据又会得到怎样的处理?
搜索的资料零零散散,大量教程大多聚焦接口调用演示,很少完整串联数据的全生命周期。很多线上故障、性能问题、数据异常,根源恰恰就是对这些底层机制一知半解。比如冷查询性能暴跌、向量重复入库、重启后数据丢失、增量更新效率低下等问题,都常常由此引发。今天我也带着这一连串实际开发中产生的疑问,由浅入深拆解向量数据库。顺着数据从生成、写入、缓存、检索、增量更新、持久化存储再到服务重启恢复的完整链路展开,探索这些由来已久的困惑。

大模型不能直接读懂文字,需要把文本、图片转换为多维浮点数组,这就是通常我们所了解的向量Embedding。一段文字被Embedding模型处理之后输出固定长度数组,例如 text‑embedding‑ada‑002输出1536维向量。向量可以理解成语义的数字化身,语义越接近,向量之间空间距离就越近。
普通关系型数据库擅长精确匹配,无法高效完成海量向量相似度计算。向量数据库就是专门为高维向量设计的专用数据库,核心做两件事:
两种核心近邻算法,简单说明:
向量数据库不等于单纯的向量索引。这里极容易混淆概念:直接用FAISS仅仅是向量索引库,不自带持久化、元数据管理、增量更新;Chroma、Qdrant属于完整向量数据库,封装索引、存储、元数据、持久化整套能力。
向量构成一条完整记录一般包含三部分:
索引是向量数据库性能核心,索引本质就是向量的组织结构,用来加速相似度查找。
索引不是构建完成就一成不变,新增数据需要对索引执行增量构建或者合并操作。索引和原始向量数据,是两个独立实体。
做项目选型向量数据库,不能只看开源热度,要重点评估几个维度:
部署模式的核心差异:
下面提供一段简易Embedding生成代码,也是向量数据库一切数据的源头。
from openai import OpenAI
# 初始化embedding客户端
client = OpenAI(base_url="https://api.example.com/v1", api_key="demo-key")
def get_embedding(text: str, model_name="text-embedding-ada-002") -> list[float]:
"""文本转为向量,向量化只需要执行一次"""
resp = client.embeddings.create(input=[text], model=model_name)
return resp.data[0].embedding
if __name__ == "__main__":
test_text = "向量数据库的缓存机制详解"
vec = get_embedding(test_text)
print(f"向量维度:{len(vec)}")
print(f"向量样例:{vec[:5]}")Embedding模型输出向量,这一步计算开销大,业务上不会每次检索请求重新执行,只在文档新增或变更的时候执行一次。
向量数据库存在多层缓存机制,分为操作系统页缓存、数据库自身内存缓存、索引缓存、查询结果缓存。很多人误以为向量全部常驻内存,这是误区。
向量数据分为两大块:
缓存分层:
向量数据的存储模式:
缓存带来现象: 刚启动向量库,第一次查询速度很慢;连续查询相同数据集,速度变快。不是向量全部加载完成,是操作系统PageCache生效。
检索请求完整流程:

这里有个核心要注意:向量数据库不会把全部数据集一次性加载进内存;
两种加载模式:
这里实践应用中很容易出现问题,很多本地向量库默认全量加载,当向量规模几十万上百万,直接把业务服务内存打满OOM。
下面使用 Chroma 复现冷热查询现象,观察缓存带来速度差异。
import chromadb
import time
# 使用持久化本地向量库
client = chromadb.PersistentClient(path="./chroma_db")
collection = client.get_or_create_collection(name="demo_cache")
def query_vector_db(query_text:str, top_n=3):
start = time.time()
res = collection.query(
query_texts=[query_text],
n_results=top_n
)
cost = time.time() - start
print(f"查询耗时 {cost:.4f}s")
return res
if __name__ == "__main__":
# 第一次查询:冷查询,磁盘读,触发操作系统缓存
print("====第一次冷查询====")
query_vector_db("向量数据库缓存原理")
# 第二次相同查询:热查询,走缓存
print("====第二次热查询====")
query_vector_db("向量数据库缓存原理")运行现象:第一次查询耗时明显高于第二次。进程完全重启之后,又回到慢的冷查询状态。
缓存也会失效,在以下情况下缓存会失效,要注意提前考虑做好应对措施,要注意缓存仅仅是加速手段,缓存里面不是真实数据源,磁盘持久化文件才是真相。
业务系统源源不断新增文档,比如上传新知识库文件、新增聊天记录。向量数据库支持增量追加,不需要重建全部库。新增数据源完整步骤:

新增数据,不会重新处理历史存量向量,历史向量不会重复Embedding。向量数据库内部很多采用Segment段机制。新写入数据写入新段,不会修改旧段。后台异步任务定期合并小segment,优化索引,提升查询性能。
不只是新增,业务还会遇到文档修改、文档删除场景:

import chromadb
from openai import OpenAI
client = OpenAI(base_url="https://api.example.com/v1", api_key="demo-key")
chroma_client = chromadb.PersistentClient(path="./chroma_db")
coll = chroma_client.get_or_create_collection(name="demo_increment")
def get_embedding(text):
return client.embeddings.create(input=[text], model="text-embedding-ada-002").data[0].embedding
def add_new_documents(doc_list:list[dict]):
"""增量新增文档向量,旧数据不受影响"""
ids = []
texts = []
embeddings = []
metas = []
for idx, item in enumerate(doc_list):
text_chunk = item["chunk"]
doc_id = item["doc_id"]
meta_info = {"source": item["source"]}
vec = get_embedding(text_chunk)
ids.append(doc_id)
texts.append(text_chunk)
embeddings.append(vec)
metas.append(meta_info)
# 增量写入
coll.add(
ids=ids,
documents=texts,
embeddings=embeddings,
metadatas=metas
)
print(f"成功新增 {len(doc_list)} 条向量")
if __name__ == "__main__":
# 第一批数据
batch1 = [
{"doc_id":"doc_001","chunk":"向量数据库增量写入原理","source":"知识库A"},
{"doc_id":"doc_002","chunk":"向量数据库持久化存储机制","source":"知识库A"},
]
add_new_documents(batch1)
# 后续又来了新数据源,增量追加,不需要处理doc_001 doc_002
batch2 = [
{"doc_id":"doc_003","chunk":"RAG应用向量数据库最佳实践","source":"知识库B"}
]
add_new_documents(batch2)
print(f"当前集合总数量:{coll.count()}")向量数据库数据存储分为:元数据存储、向量二进制存储、索引文件。
嵌入式向量库Chroma/Qdrant本地模式,磁盘目录可以看到一堆文件:
服务型向量数据库Milvus底层借助MinIO对象存储保存向量与索引文件,etcd保存元数据 Schema。
向量以二进制浮点数组持久化落盘,不是保存在内存。内存缓存只是加速访问,不是数据源。
这里是一个核心疑惑点,到底是在什么时机做向量化,向量化是初始化一次还是每次请求:
在明确这个答案之前,一直都是错误理解:每次检索请求,向量库把库里面全部文档文本重新Embedding一遍,开销巨大完全不可行。

实际的正确流程:文档向量提前算好存库;用户搜索文本实时算查询向量,拿查询向量和库中已经保存向量做距离计算。
举个生活化例子:图书馆。
为什么文档向量不能查询时实时算?
只有文档发生修改、新增的时候,才重新调用Embedding。
运行下面代码,写入向量之后关闭程序,重新启动读取,验证磁盘持久化。
import chromadb
# 写入阶段
client_write = chromadb.PersistentClient(path="./chroma_persist_demo")
col_write = client_write.get_or_create_collection("persist_test")
col_write.add(
ids=["id_01"],
documents=["验证向量数据库持久化效果"],
metadatas={"note":"测试数据"}
)
print(f"写入完成,数量:{col_write.count()}")
# =========模拟程序停止,进程退出=========
del client_write
# =========模拟程序重新启动,重新初始化客户端=========
client_read = chromadb.PersistentClient(path="./chroma_persist_demo")
col_read = client_read.get_collection("persist_test")
print(f"重启后读取向量数量:{col_read.count()}")
res = col_read.get(ids=["id_01"])
print(f"读取数据:{res}")运行现象:进程销毁重建,向量数据依旧存在。只要不手动删除磁盘文件夹,数据不会消失。
向量数据库进程停止分为嵌入式(嵌入业务进程)与独立服务进程两种场景:
嵌入式向量库:

如果程序异常崩溃,有可能存在少量内存数据还没刷盘,造成极小部分数据丢失。生产建议开启手动flush刷盘接口。
服务化向量数据库:

注意内存模式向量库:Chroma可以设置内存模式,数据完全只放内存,不写磁盘。一旦程序退出,所有向量全部清空,这个模式只适合临时测试。
重启不会重新执行Embedding。向量全部保存在磁盘,不需要重新跑Embedding。向量库重启启动执行步骤:

import chromadb
cli = chromadb.PersistentClient(path="./chroma_flush_demo")
coll = cli.get_or_create_collection("flush_demo")
coll.add(
ids=["test_001"],
documents=["测试强制刷盘"]
)
# 手动调用flush,确保内存数据落地磁盘,防止异常断电丢失
coll.flush()
print("数据已经强制刷入磁盘")完整简易RAG实践示例,整合前面全部知识点:
import chromadb
from openai import OpenAI
# 初始化
embedding_client = OpenAI(base_url="https://api.example.com/v1", api_key="demo-key")
db_client = chromadb.PersistentClient(path="./rag_vector_db")
rag_collection = db_client.get_or_create_collection("knowledge_base")
def text2vec(text):
return embedding_client.embeddings.create(input=[text], model="text-embedding-ada-002").data[0].embedding
def add_knowledge(doc_id:str, content:str, source:str):
"""知识库新增文档,仅新增时Embedding"""
vec = text2vec(content)
rag_collection.add(
ids=[doc_id],
documents=[content],
embeddings=[vec],
metadatas=[{"source":source}]
)
rag_collection.flush()
def rag_retrieve(user_query:str, top_k=3):
"""用户检索:只对query做Embedding,库中文档向量复用已有"""
res = rag_collection.query(
query_texts=[user_query],
n_results=top_k
)
return res
if __name__ == "__main__":
# 1.业务新增知识库文档,只执行一次Embedding
add_knowledge("kb_001","向量数据库重启后数据保存在磁盘","内部文档")
add_knowledge("kb_002","向量检索查询时只向量化用户提问","内部文档")
# 2.用户请求检索
search_result = rag_retrieve("向量数据库重启之后数据去哪了")
print(search_result["documents"])今天完全是带着疑问去探索,对核心问题先做个全面总结,对应关键的核心要点:
向量数据库有没有缓存?
每次检索如何加载向量?
有新数据源如何增加?
向量如何保存?
什么时候向量化?
程序停止重启,数据怎么处理?
总的来说,向量数据库不是黑盒,RAG系统很多bug根源都来自不理解数据完整生命周期。我们要尽量避免仅仅只停留在调用API层面,遇到性能、数据异常无从排查。搞懂缓存、加载、增量写入、持久化、重启恢复的整个过程,不管使用什么向量数据库,底层逻辑是相通的。在实践做RAG的时候,一些小经验分享,区分冷热数据、控制写入批次、做好持久化刷盘、规避重复Embedding,从而构建稳定可靠检索增强大模型应用。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。