从存储走到检索:AI向量数据库笔记
AI向量数据库笔记上手并不难,难的是稳定跑起来。
下面只记真正影响结果的部分。
背景和需求
最开始的需求来自一个知识库项目:用户问一个问题,系统要从一堆内部文档和聊天记录里找到最相关的内容片段返回。第一反应是用 Elasticsearch 做关键词检索,但很快就发现问题:
- 问"怎么配置数据库",“设置数据库”、“数据库配置"这些变体,关键词匹配需要手动维护同义词表,覆盖不全
- “性能优化"和"调优"在向量空间里很近,但在关键词世界里是完全不同的词
- 用户用自然语言描述问题时,和文档里的专业术语常常不匹配
这时候就引入了向量检索的思路:把文本通过 embedding 模型转成向量,在向量空间里用距离衡量相似度。
但这个思路落地时有个实际限制条件:
- 数据量不大(几万到几十万条记录),但查询响应需要在几百毫秒内
- 需要支持动态增删改,不是一次性导入就完事
- 团队人力有限,不想自己从零搭建向量检索系统
- 需要和现有的应用代码集成,而不是一个完全独立的黑盒子
这几个条件就把方案缩小了一圈。
选型过程
常见的向量数据库有 Pinecone、Milvus、Qdrant、Weaviate、Chroma 等等。选型时主要考虑了这些点:
- Pinecone:托管服务,零运维,有免费层,但国内访问有点慢,数据存储在他们的服务器上
- Milvus:开源,功能全,但部署复杂,需要单独的服务器资源
- Qdrant:开源,Rust 写的,性能好,部署相对简单,支持 Docker
- Weaviate:开源,自带向量模型,但资源占用较高
- Chroma:轻量,适合本地开发和实验,但生产环境建议用更健壮的方案
最终选了 Pinecone,原因很简单:省心。项目初期重点是验证语义检索的效果,不是自己搭一套基础设施。Pinecone 的 API 也相对简单,直接调用就行。后期如果需要,可以再迁移到自建方案。
实现步骤
整个实现流程可以分成这几个部分:数据准备、向量生成、索引创建、查询接口。
文本预处理
在生成向量之前,需要把文本处理成合适的格式:
def prepare_text_for_embedding(text: str, max_length: int = 500) -> str:
# 简单清理:去除多余空白、特殊字符
cleaned = re.sub(r'\s+', ' ', text.strip())
# 超过最大长度就截断,避免浪费 token
if len(cleaned) > max_length:
cleaned = cleaned[:max_length].rsplit(' ', 1)[0] + "..."
return cleaned
这里有个实际权衡:截断长度越长,保留的信息越多,但 embedding API 的成本和延迟都会增加。测试后发现 500 字符是个相对平衡的点,大部分语义都能保留,成本也控制得住。
向量生成
用的是 OpenAI 的 text-embedding-3-small 模型,性价比高,512 维度足够:
from openai import OpenAI
client = OpenAI()
def generate_embedding(text: str) -> list[float]:
response = client.embeddings.create(
model="text-embedding-3-small",
input=text
)
return response.data[0].embedding
一开始用的是 text-embedding-ada-002,后来换了 text-embedding-3-small,效果差不多但成本更低。如果有特殊需求(多语言支持、特定领域),可以换成专门的模型。
索引创建和数据写入
Pinecone 的 API 很直接:
import pinecone
pc = pinecone.Pinecone(api_key="your-api-key")
# 创建索引
pc.create_index(
name="knowledge-base",
dimension=512, # 对应 embedding 模型的维度
metric="cosine", # 余弦相似度,适合文本
spec=pinecone.ServerlessSpec(
cloud="aws",
region="us-east-1"
)
)
index = pc.Index("knowledge-base")
# 批量写入
def upsert_vectors(items: list[dict]):
vectors = []
for item in items:
embedding = generate_embedding(item["text"])
vectors.append({
"id": item["id"],
"values": embedding,
"metadata": {
"text": item["text"],
"source": item.get("source", ""),
"timestamp": item.get("timestamp", "")
}
})
index.upsert(vectors=vectors)
这里的 metadata 很重要:向量本身只是一串数字,真正有用的信息(原文、来源、时间)都存在这里。查询时可以基于 metadata 过滤,比如只查某个时间段的内容。
查询接口
查询时也是先把查询文本转成向量,然后在向量空间里找最近的邻居:
def query_similar(query: str, top_k: int = 5) -> list[dict]:
query_embedding = generate_embedding(query)
results = index.query(
vector=query_embedding,
top_k=top_k,
include_metadata=True,
filter={"source": {"$eq": "documentation"}} # 可选过滤条件
)
return [{
"id": match["id"],
"score": match["score"],
"text": match["metadata"]["text"],
"source": match["metadata"]["source"]
} for match in results["matches"]]
踩坑记录
距离度量选择
一开始用了 euclidean(欧氏距离),但实际发现 cosine(余弦相似度)在文本场景下效果更好。原因很简单:cosine 关注的是向量的方向而不是长度,而文本向量的长度本身不包含额外信息。
Pinecone 支持三种距离度量:
- cosine:余弦相似度,适合文本 embedding
- euclidean:欧氏距离,适合某些数值型向量
- dotproduct:点积,适合已经归一化的向量
创建索引时需要选好,后期不能改。如果选错了,只能重建索引。
向量维度和性能的关系
测试时对比了不同维度下的查询性能和效果:
| 维度 | 存储 | 查询延迟 | 效果(主观) |
|---|---|---|---|
| 256 | 低 | ~50ms | 一般,细节丢失 |
| 512 | 中 | ~80ms | 良好,性价比高 |
| 1536 | 高 | ~150ms | 优秀,但成本高 |
在当前场景下,512 维是平衡点。如果数据量大或者对延迟要求更严格,可以考虑进一步降维。
批量写入的坑
Pinecone 有写入速率限制。一开始单条写入没问题,但批量导入时经常超时。改成批量 upsert 后就好多了:
def batch_upsert(items: list[dict], batch_size: int = 100):
for i in range(0, len(items), batch_size):
batch = items[i:i + batch_size]
upsert_vectors(batch)
time.sleep(0.1) # 避免触发速率限制
索引重建很贵
Pinecone 的 serverless 版本按存储和查询计费,但重建索引的成本不会自动退。所以一开始就要规划好索引结构,避免频繁重建。
结果和效果
部署后的实际效果:
- 查询延迟:平均 80-120ms,满足实时场景需求
- 检索质量:相比关键词检索,相关度明显提升。用户反馈"更懂我了”
- 运维成本:几乎为零,Pinecone 托管服务省了不少事
- 成本:当前数据量下,每月成本在十几美元量级,可接受
但也有局限性:
- 数据量上去后成本会线性增长,不如自建方案划算
- 国内访问偶尔不稳定,需要考虑 CDN 或自建服务
- 复杂的多条件查询不如关系数据库灵活
结语
向量数据库不是万能药,但在解决"语义相似"这个问题上确实比传统方法好很多。这次实践的价值不在于用上了什么新技术,而是验证了一个具体场景下的可行方案——从需求到选型到落地,每一步都基于实际限制条件。
技术选择永远是个权衡问题。Pinecone 合适是因为场景和资源约束,不是因为它"最好”。后期如果业务需要,再迁移到自建方案也是顺理成章的事。
折腾技术的过程中,最有价值的往往不是工具本身,而是那个"我为什么需要它"的问题。把这个问题想清楚,后面的路就好走了。
版权声明: 本文首发于 指尖魔法屋-从存储走到检索:AI向量数据库笔记(https://blog.thinkmoon.cn/post/362-ai-vector-database-storage-retrieval-practice/) 转载或引用必须申明原指尖魔法屋来源及源地址!
评论
使用 GitHub 账号登录后即可留言,支持 Markdown。