从存储走到检索:AI向量数据库笔记

AI向量数据库笔记上手并不难,难的是稳定跑起来。

下面只记真正影响结果的部分。

背景和需求

最开始的需求来自一个知识库项目:用户问一个问题,系统要从一堆内部文档和聊天记录里找到最相关的内容片段返回。第一反应是用 Elasticsearch 做关键词检索,但很快就发现问题:

  • 问"怎么配置数据库",“设置数据库”、“数据库配置"这些变体,关键词匹配需要手动维护同义词表,覆盖不全
  • “性能优化"和"调优"在向量空间里很近,但在关键词世界里是完全不同的词
  • 用户用自然语言描述问题时,和文档里的专业术语常常不匹配

这时候就引入了向量检索的思路:把文本通过 embedding 模型转成向量,在向量空间里用距离衡量相似度。

但这个思路落地时有个实际限制条件:

  • 数据量不大(几万到几十万条记录),但查询响应需要在几百毫秒内
  • 需要支持动态增删改,不是一次性导入就完事
  • 团队人力有限,不想自己从零搭建向量检索系统
  • 需要和现有的应用代码集成,而不是一个完全独立的黑盒子

这几个条件就把方案缩小了一圈。

选型过程

常见的向量数据库有 Pinecone、Milvus、Qdrant、Weaviate、Chroma 等等。选型时主要考虑了这些点:

  • Pinecone:托管服务,零运维,有免费层,但国内访问有点慢,数据存储在他们的服务器上
  • Milvus:开源,功能全,但部署复杂,需要单独的服务器资源
  • Qdrant:开源,Rust 写的,性能好,部署相对简单,支持 Docker
  • Weaviate:开源,自带向量模型,但资源占用较高
  • Chroma:轻量,适合本地开发和实验,但生产环境建议用更健壮的方案

最终选了 Pinecone,原因很简单:省心。项目初期重点是验证语义检索的效果,不是自己搭一套基础设施。Pinecone 的 API 也相对简单,直接调用就行。后期如果需要,可以再迁移到自建方案。

实现步骤

整个实现流程可以分成这几个部分:数据准备、向量生成、索引创建、查询接口。

flowchart LR A[原始文本数据] --> B[文本预处理] B --> C[调用 Embedding API] C --> D[生成向量] D --> E[写入向量数据库] E --> F[构建索引] F --> G[查询接口] G --> H[返回结果]

文本预处理

在生成向量之前,需要把文本处理成合适的格式:

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/) 转载或引用必须申明原指尖魔法屋来源及源地址!