知识库工具的核心能力之一是根据语义相似度找到相关内容,而支撑这个能力的基础组件就是向量数据库。它存储的不是简单的字符串或数字,而是由Embedding模型生成的高维浮点数向量,每个向量对应一段文本、图片或其他非结构化数据。对向量数据库的操作也就不能沿用传统SQL的思路,需要重新理解增删改查在向量空间中的实现方式。

以常见的技术文档知识库为例,当用户提出问题时,系统会将问题转换为查询向量,然后在库中寻找距离最近的若干条记录。这个过程中涉及的写入、删除、更新和查询操作,每一步都有独特的约束和优化空间。下面分别展开说明。
一、向量数据的写入:从Embedding到批量插入
写入向量数据库的第一步通常发生在离线或在线流程中。原始文档经过文本切分(chunking)后,调用Embedding模型得到向量表示,然后与文档原文、元数据一起写入数据库。向量本身是一个浮点数数组,维度从几百到几千不等,例如常见的OpenAI text-embedding-3-small输出1536维向量。一个知识库可能包含数十万甚至上百万个这样的向量,单条插入的性能会很差,因此批量写入是标准做法。
以Python环境下的Chroma客户端为例,创建集合并批量添加数据的基本代码如下:
import chromadb
client = chromadb.Client()
collection = client.create_collection(name="knowledge_base")
documents = [
"如何配置Nginx反向代理",
"Docker部署最佳实践",
"Kubernetes节点故障排查"
]
ids = ["doc_001", "doc_002", "doc_003"]
embeddings = [
[0.12, 0.33, 0.41, 0.08, 0.55], # 实际为高维向量,这里仅示意
[0.45, 0.21, 0.67, 0.12, 0.31],
[0.22, 0.74, 0.18, 0.60, 0.44]
]
metadatas = [
{"category": "运维", "source": "内部wiki"},
{"category": "容器", "source": "内部wiki"},
{"category": "容器", "source": "博客"}
]
collection.add(
ids=ids,
documents=documents,
embeddings=embeddings,
metadatas=metadatas
)
批量插入时,向量数据库会在后台构建索引结构。常见的索引类型包括HNSW(分层可导航小世界图)、IVF(倒排文件)等,它们通过牺牲少量精确度来换取极快的近似最近邻搜索速度。如果使用HNSW,写入过程中会逐步构建图结构,新向量会被连接到最近的若干个节点上,批量写入可以减少图重建的次数,从而大幅提升整体吞吐量。在实际项目中,通常建议每批写入100到1000条记录,批次太小会导致网络和索引构建开销占比过高,批次太大会占用过多内存。
还需要注意一点:向量本身是从模型输出的浮点数组,不要对向量做任何标准化或归一化处理后再存储,除非你的检索算法明确要求预处理。数据库内部会根据选择的距离度量(欧氏距离、余弦相似度、内积等)自动处理。
二、删除与更新:按主键和按条件操作
删除向量数据最直接的方式是按主键ID执行删除。每个写入的向量都绑定一个全局唯一的ID,这个ID可由业务方自行指定(如文档ID加上分块序号)。删除操作会立即从索引中移除对应节点,在HNSW中表现为断开该节点的所有连接,并触发局部图修复。对于大规模删除,建议批量传入ID列表,避免逐条删除造成的重复索引更新。
除了按ID删除,支持元数据过滤的向量数据库也允许按条件删除。例如从知识库中移除所有分类为过期文档的记录,可以这样做:
# 按ID删除
collection.delete(ids=["doc_001"])
# 按元数据条件删除
collection.delete(where={"category": "过期文档"})
更新操作比删除更复杂一些。向量的更新通常意味着文档内容发生了变化,需要重新生成Embedding,而不是简单修改某个字段。如果只更新元数据而不改变向量本身,可以直接覆盖元数据字段;但若要更新向量值,则需要提供完整的新向量数组以及可能变化的文档文本。以Chroma为例,更新接口如下:
collection.update(
ids=["doc_002"],
documents=["Docker Compose部署指南修订版"],
embeddings=[[0.47, 0.22, 0.69, 0.11, 0.29]], # 重新生成的向量
metadatas=[{"category": "容器", "version": "v2"}]
)
更新向量会触发索引中对应节点的替换操作。在HNSW中,这并非简单的原地修改,而是需要从图中摘除旧节点、插入新节点,并重新建立邻居关系,开销远大于普通数据库的UPDATE语句。因此对于频繁更新的场景,更推荐的策略是采用删除加新增的组合,或者使用支持原地更新的索引类型。另外需要注意,更新操作如果传入了不存在的ID,有些数据库会抛异常,有些则会静默忽略,开发时要确认具体产品的行为。
三、向量检索:相似度查询与过滤条件
向量数据库最重要的操作是相似度检索,也叫近似最近邻搜索。它接收一个查询向量,返回距离最近的前K条记录,同时可以附加元数据过滤条件。查询向量同样由Embedding模型生成,例如用户输入一个自然语言问题后,先调用模型得到向量,再提交给数据库。检索结果通常包含文档文本、元数据和相似度得分,开发者可以基于得分做二次排序或阈值截断。
以下是执行Top-K查询并附加类别过滤的示例:
query_text = "容器编排平台有哪些"
query_embedding = [0.23, 0.75, 0.19, 0.61, 0.45] # 模型生成
results = collection.query(
query_embeddings=[query_embedding],
n_results=5,
where={"category": "容器"}
)
for doc_id, distance, metadata in zip(
results["ids"][0], results["distances"][0], results["metadatas"][0]
):
print(f"ID: {doc_id}, 距离: {distance}, 分类: {metadata['category']}")
查询参数中的n_results控制返回数量,where条件用于标量过滤。在支持混合查询的数据库中,过滤条件会在向量搜索之前或之后应用,不同实现会影响召回率。例如先过滤后搜索可以保证返回结果都满足条件,但可能因为过滤后候选集过小而漏掉一些真正相似的向量;先搜索后过滤则可能返回不足K条。开发者需要根据业务需求选择合适的策略,部分数据库允许通过参数显式指定过滤时机。
相似度得分和距离度量密切相关。使用余弦相似度时,得分越接近1表示越相似;使用欧氏距离时,数值越小越相似。不同的Embedding模型可能对度量方式有偏好,例如使用归一化向量的模型通常配合内积或余弦相似度使用。在知识库场景中,务必保持写入和查询阶段使用相同的距离度量,否则检索结果会毫无意义。
四、实践中的性能与一致性考虑
向量数据库的写入和查询性能强烈依赖于索引类型和参数配置。HNSW在小规模到中等规模数据集(百万级以下)上表现非常出色,查询延迟低且召回率高,但内存占用较大;IVF类索引更适合亿级向量,通过聚类划分空间来减少搜索范围,但需要定期训练或重建。对于知识库工具来说,如果数据量在几十万以内,默认使用HNSW配合合理的M和efConstruction参数即可获得很好的效果。
一致性问题同样值得关注。许多向量数据库为了追求高吞吐,写入后并不会立即持久化到磁盘,而是先写入内存或WAL日志,查询可能暂时看不到最新插入的数据。对于知识库场景,如果要求用户上传文档后马上能被搜索到,需要检查数据库的持久化配置,必要时手动触发flush。此外,删除和更新操作在部分分布式向量数据库中会有秒级甚至分钟级的传播延迟,设计系统时要考虑是否需要强一致读。
最后总结一下:向量数据库的增删改查在接口语义上与传统数据库有相似之处,但底层机制完全不同。写入关注批量与索引构建,删除和更新要留意索引重建成本,查询则围绕近似最近邻算法与过滤条件展开。只有理解了这些差异,才能让知识库工具在语义检索上稳定高效地运行。