一、前言
在如今的数字时代,数据的形式正以前所未有的速度变得多样化。文本、图片、音频、视频等非结构化数据占据了数据总量的80%以上。传统数据库(如MySQL)擅长处理“张三的年龄是25岁”这类结构化数据,但对“一张有夕阳、狗和海滩的图片”或“一篇讨论量子计算前景的文章”却无能为力。
我们可以轻松理解这些内容背后的含义和关联,但计算机需要一种方式来“理解”和“比较”它们。向量数据库(Vector Database) 就是专为解决这一问题而生的新型数据库,它是AI基础设施中至关重要的一环,被誉为AI应用的“长期记忆体”和“检索大脑”。
二、什么是向量数据库
首先需要回顾一下向量和Embedding的含义,具体可以参考章节《构建AI智能体:十二、给词语绘制地图:Embedding如何构建机器的认知空间》
向量:在AI和机器学习领域,向量是一组数字的有序列表,可以表示任何数据对象(如一段文字、一张图片)在高维空间中的位置。
嵌入(Embedding):通过AI模型(如BERT、CNN、CLIP等)将非结构化数据转换为向量的过程,称为“嵌入”。这个转换过程捕获了数据的深层语义特征。
示例:单词“国王”通过模型转换后,可能得到一个包含300个数字的向量。在数学上,这个向量与“男人”、“女王”、“女人”的向量存在某种关系(如“国王” - “男人” + “女人” ≈ “女王”)。
向量数据库是一种专门用于存储、索引和查询高维向量的数据库。它的核心功能是执行近似最近邻(ANN)搜索,即快速找到与查询向量最相似的向量集合。
三、与传统数据库区别
| 特性 | 传统数据库 (SQL/NoSQL) | 向量数据库 |
| 数据模型 | 结构化数据(行/列)、文档、键值对 | 高维向量 |
| 查询方式 | 精确匹配(WHERE age = 25)、范围查询 | 相似性搜索(找到与这个图片最像的10张图片) |
| 索引技术 | B树、哈希索引等 | HNSW、IVF-PQ、LSH等专门为高维空间设计的ANN索引 |
| 核心能力 | 事务一致性、完整性 | 语义理解、相似性检索 |
| 适用场景 | 电商订单、用户信息管理等 | AI推荐、语义搜索、图像识别等 |
四、向量数据库的核心原理
在高维空间中(维度可达成百上千维),进行精确的最邻近搜索计算量极大,速度极慢。向量数据库使用近似最近邻(ANN)算法,在可接受的精度损失下,极大提升搜索速度。
- 索引(Indexing):数据库不会暴力比对所有向量,而是预先构建索引,将向量组织成一种易于搜索的结构。
- HNSW:像构建一个多层的高速公路网络,先从顶层进行粗粒度搜索,再逐层细化,快速逼近目标。
- IVF-PQ:先将向量空间聚类成多个“单元”,搜索时只找最可能的几个单元。再用产品量化技术压缩向量,减少计算和存储开销。
- 距离度量(Distance Metric):如何衡量两个向量的相似性?常用方法有:
- 余弦相似度(Cosine Similarity):衡量向量方向上的差异,忽略其大小。非常适合文本数据。
- 欧氏距离(Euclidean Distance):衡量向量在空间中的实际距离。
- 点积(Dot Product):与余弦相似度相关,但也受向量大小影响。
扩展:通俗的介绍ANN算法
想象一下,你是一个图书管理员,管理着一个有100万本书的图书馆。一个顾客问你:“请帮我找一本和《三体》最相似的书。”
- 愚蠢但精确的方法(暴力搜索):你拿起《三体》,然后一页一页地和其他999,999本书逐字逐句地对比。这绝对能找到最相似的那本,但你找到的时候,顾客可能已经老了。这就像计算机中的精确最近邻搜索,结果完美,但速度慢到无法接受。
- 聪明但近似的方法(ANN算法):你是一个聪明的管理员。你事先做了功课:你给书分了类(建立了索引):科幻区、文学区、历史区……你知道《三体》是科幻小说。你还给书贴了标签:有“外星人”、“物理学”、“悬疑”标签的书大概率在科幻区。当顾客提出同样的问题时,你不会去检查所有100万本书。你直接冲向科幻区,然后只在这个区的几千本书里快速寻找和《三体》最像的。你甚至可能只看了有“外星人”标签的那个书架。
- 结果就是:你极快地找到了一本非常相似的书(比如《银河帝国》),但它是不是100万本中绝对最相似的那本?不一定。可能历史区有一本讲科学史的书某个角度也很像,但你忽略了它。
这个“聪明的办法”就是 ANN 算法的核心思想:ANN(Approximate Nearest Neighbor),近似最近邻。它的精髓就是:用极高的效率(速度)和可接受的内存占用,换来一个“差不多”的、非常接近正确的搜索结果。它不保证找到的是绝对最近的,但能保证找到的是非常近的。
为什么需要它? 因为在处理高维数据(比如图片、文本的1024维向量)时,暴力搜索的计算量是灾难性的,ANN 是唯一可行的解决方案。
五、常见的向量数据库
- Pinecone:全托管的云端向量数据库,以易用性、高性能和强大的API著称,是快速构建原型和应用的理想选择。
- Chroma:轻量级、开源,特别专注于AI原生应用和LLM(大语言模型)生态,易于集成和本地部署。
- Weaviate:开源向量搜索引擎,不仅支持向量搜索,还具备GraphQL接口,可以像图数据库一样处理数据对象之间的关系。
- Milvus / Zilliz Cloud:Milvus是开源领域功能最全面、最受欢迎的向量数据库之一,专为海量向量数据设计。Zilliz是其背后的商业公司,提供全托管云服务。
- Qdrant:一个用Rust编写的高性能、开源向量数据库和搜索引擎,提供丰富的API和云服务。
- FAISS:核心算法库,非数据库。由Meta AI开发。提供最广泛、最前沿的ANN索引算法。支持CPU和GPU。在内存中进行裸向量检索时速度极快,尤其是GPU版本,是衡量其他数据库性能的基准。
注意:FAISS是一个极其高效的向量相似性搜索库(Library),而不是一个完整的、功能齐备的数据库(Database)。FAISS 是 Facebook AI 团队开源的一个用于高效相似性搜索和密集向量聚类的库。它提供了大量的算法,针对不同的数据集大小和精度要求,可以组合出最优的索引和搜索方式。
六、向量数据库的运用
1. 将数据导入到向量数据库
- 第一步:数据清洗与准备
确保原始数据(如文本文档、图片)的质量,进行必要的预处理。
- 第二步:数据向量化(Embedding)
使用预训练的Embedding Model将原始数据转换成向量。
- 文本:可使用bge-m, Qwen3-Embedding, Jina-Embedding 等模型。
- 图片:可使用CLIP, ResNet等模型。
选择合适的模型至关重要,它直接决定了向量的质量和后续检索的效果。
- 第三步:数据与元数据一同导入
将生成的向量及与其关联的元数据(Metadata)一同存入向量数据库。
- 向量(Vector):生成的Embedding数字数组。
- 唯一ID(ID):用于唯一标识每个数据点,方便后续的更新或删除。
- 元数据(Metadata):描述向量的附加信息,是实现高级检索的关键。例如:文本来源的文件名、章节、URL、商品的类别、品牌、价格等
示例:指定模型将数据向量化
# 导入必要的库 import os # 用于访问操作系统环境变量 from openai import OpenAI # 导入OpenAI客户端库 # 初始化OpenAI客户端,配置为连接阿里云百炼服务 client = OpenAI( # 从环境变量DASHSCOPE_API_KEY获取API密钥,用于身份验证 # 如果没有设置环境变量,可以在此直接替换为您的API密钥字符串(但不建议在代码中硬编码密钥) api_key=os.getenv("DASHSCOPE_API_KEY"), # 指定API的基础URL,这里指向阿里云百炼服务的兼容模式端点 # 这使得可以使用OpenAI库的标准格式调用阿里云的API服务 base_url="https://dashscope.aliyuncs.com/compatible-mode/v1" ) # 创建文本嵌入向量 completion = client.embeddings.create( model="text-embedding-v4", # 指定使用的嵌入模型版本 input='我想知道迪士尼的退票政策', # 输入需要转换为向量的文本内容 # 指定生成的向量维度(长度),1024维是常见的选择 # 此参数仅text-embedding-v3及更高版本支持 dimensions=1024, # 指定向量值的编码格式为浮点数(float) # 这是最常用的格式,提供高精度表示 encoding_format="float" ) # 将API响应结果转换为JSON格式字符串并打印输出 # model_dump_json()方法将返回的对象序列化为JSON字符串 print(completion.model_dump_json())
执行步骤:
1. 导入必要的库:os 和 openai(需要安装openai库,如果使用百炼,需要确保版本支持)
2. 创建OpenAI客户端,指定api_key和base_url
3. 调用embeddings.create方法,传入模型名、输入文本、向量维度和编码格式
4. 将返回的嵌入对象转换为JSON字符串并打印
注意:
这里我们使用 embeddings 模型 "text-embedding-v4",并指定维度为1024。 同时,我们打印出返回的JSON字符串。
返回结果:
{"data":[{"embedding":[0.0026341788470745087,-0.03762197867035866,-0.024403059855103493,-0.010764381848275661,0.01541021279990673,0.0007359159062616527,0.018554862588644028,-0.02278093248605728,-0.05350175499916077,0.023905038833618164,-0.02632400020956993,-0.009014192037284374,0.001929833902977407,0.0883917286992073,0.003021924290806055,0.015310607850551605,0.05139583349227905,-0.04928991198539734,-0.053074877709150314, 中间省略部分.... -0.004830809775739908,-0.0026359574403613806,-0.021955639123916626,0.01802838407456875,0.0019369485089555383,0.0010609639575704932,-0.01535329595208168,0.018014153465628624,-0.04493578150868416,0.0027284470852464437,-0.05347329378128052,-0.047639328986406326,-0.03625597432255745],"index":0,"object":"embedding"}],"model":"text-embedding-v4","object":"list","usage":{"prompt_tokens":8,"total_tokens":8},"id":"4f3c37d0-1b7b-99bb-ba7d-878b0e73a153"}
2. 将Embedding 和元数据一起存储在FAISS
- FAISS 适合探索和实验,适合于数据不需要频繁更新的场景,同时拥有极致的向量搜索速度,且不需要复杂的元数据过滤;
- FAISS 本身只存储和检索向量,不存储元数据,我们需要在FAISS 之外维护一个元数据的“查找表”,并通过向量在FAISS 中的唯一ID将两者关联起来。
- 最直接有效的方法是使用FAISS 的IndexIDMap,允许我们为每个向量指定一个自定义的、唯一的64位整数ID。然后,可以用这个ID作为元数据存储的键。
示例:构建一个迪士尼政策权限相关的检索系统
- 第一步:准备数据,创建示例文本和对应的元数据。
- 第二步:生成向量,基于百炼text-embedding-v4 生成每个文本的向量。
- 第三步:创建元数据存储,使用一个简单的Python 列表来存储元数据。列表的索引将作为每个数据点的唯一ID。
- 第四步:构建FAISS 索引:
- 使用faiss.IndexFlatL2 创建一个基础的索引,这里使用L2距离(欧氏距离)进行精确搜索。
- 用faiss.IndexIDMap将基础索引包装起来,这样就可以添加带有自定义ID的向量了。
- 第五步:添加数据到索引:将生成的向量和对应的ID(即元数据列表的索引)添加到IndexIDMap中。
- 第六步:执行搜索:
- 对一个新的查询文本生成向量。
- 在FAISS 索引中搜索最相似的向量。
- FAISS 会返回最相似向量的ID。
- 第七步:检索元数据,使用返回的ID,从元数据存储中查找到原始文本和元数据。
import os import numpy as np import faiss from openai import OpenAI # Step1. 初始化 API 客户端 try: client = OpenAI( api_key=os.getenv("DASHSCOPE_API_KEY"), base_url="https://dashscope.aliyuncs.com/compatible-mode/v1" ) except Exception as e: print("初始化OpenAI客户端失败,请检查环境变量'DASHSCOPE_API_KEY'是否已设置。") print(f"错误信息: {e}") exit() # Step2. 准备示例文本和元数据 # 在实际应用中,这些数据可能来自数据库、文件等 documents = [ { "id": "doc1", "text": "迪士尼乐园的门票一经售出,原则上不予退换。但在特殊情况下,如恶劣天气导致园区关闭,可在官方指引下进行改期或退款。", "metadata": {"source": "official_faq_v1.pdf", "category": "退票政策", "author": "Admin"} }, { "id": "doc2", "text": "购买“奇妙年卡”的用户,可以享受一年内多次入园的特权,并且在餐饮和购物时有折扣。", "metadata": {"source": "annual_pass_rules.docx", "category": "会员权益", "author": "MarketingDept"} }, { "id": "doc3", "text": "对于在线购买的迪士尼门票,如果需要退票,必须在票面日期前48小时通过原购买渠道提交申请,并可能收取手续费。", "metadata": {"source": "online_policy.html", "category": "退票政策", "author": "E-commerceTeam"} }, { "id": "doc4", "text": "园区内的“加勒比海盗”项目因年度维护,将于下周暂停开放。", "metadata": {"source": "maintenance_notice.txt", "category": "园区公告", "author": "OpsDept"} } ] # Step3. 创建元数据存储和向量列表 # 我们使用一个简单的列表来存储元数据。列表的索引将作为FAISS的ID。 # 这种方式简单直接,适用于中小型数据集。 # 对于大型数据集,可以考虑使用字典或数据库(如Redis, SQLite) metadata_store = [] vectors_list = [] vector_ids = [] print("正在为文档生成向量...") for i, doc in enumerate(documents): try: # 调用API生成向量 completion = client.embeddings.create( model="text-embedding-v4", input=doc["text"], dimensions=1024, encoding_format="float" ) # 获取向量 vector = completion.data[0].embedding vectors_list.append(vector) # 存储元数据,并使用列表索引作为唯一ID metadata_store.append(doc) vector_ids.append(i) # 自定义ID与列表索引一致 print(f" - 已处理文档 {i+1}/{len(documents)}") except Exception as e: print(f"处理文档 '{doc['id']}' 时出错: {e}") continue # 将向量列表转换为NumPy数组,FAISS需要这种格式 vectors_np = np.array(vectors_list).astype('float32') vector_ids_np = np.array(vector_ids) # Step4. 构建并填充 FAISS 索引 dimension = 1024 # 向量维度 k = 3 # 查找最近的3个邻居 # 创建一个基础的L2距离索引 index_flat_l2 = faiss.IndexFlatL2(dimension) # 使用IndexIDMap来包装基础索引,能够映射我们自定义的ID # 这就是关联向量和元数据的关键! index = faiss.IndexIDMap(index_flat_l2) # 将向量和它们对应的ID添加到索引中 index.add_with_ids(vectors_np, vector_ids_np) print(f"\nFAISS 索引已成功创建,共包含 {index.ntotal} 个向量。") # Step5. 执行搜索并检索元数据 query_text = "我想了解一下迪士尼门票的退款流程" print(f"\n正在为查询文本生成向量: '{query_text}'") try: # 为查询文本生成向量 query_completion = client.embeddings.create( model="text-embedding-v4", input=query_text, dimensions=1024, encoding_format="float" ) query_vector = np.array([query_completion.data[0].embedding]).astype('float32') # 在FAISS索引中执行搜索 # search方法返回两个NumPy数组: # D: 距离 (distances) # I: 索引/ID (indices/IDs) distances, retrieved_ids = index.search(query_vector, k) # Step6. 展示结果 print("\n--- 搜索结果 ---") # `retrieved_ids[0]` 包含与查询最相似的k个向量的ID for i in range(k): doc_id = retrieved_ids[0][i] # 检查ID是否有效 if doc_id == -1: print(f"\n排名 {i+1}: 未找到更多结果。") continue # 使用ID从我们的元数据存储中检索信息 retrieved_doc = metadata_store[doc_id] print(f"\n--- 排名 {i+1} (相似度得分/距离: {distances[0][i]:.4f}) ---") print(f"ID: {doc_id}") print(f"原始文本: {retrieved_doc['text']}") print(f"元数据: {retrieved_doc['metadata']}") except Exception as e: print(f"执行搜索时发生错误: {e}")
执行结果:
正在为文档生成向量... -已处理文档1/4 -已处理文档2/4 -已处理文档3/4 -已处理文档4/4 FAISS 索引已成功创建,共包含4 个向量。 正在为查询文本生成向量: '我想了解一下迪士尼门票的退款流程' ---搜索结果--- ---排名1 (相似度得分/距离: 0.3222) --- ID: 2 原始文本: 对于在线购买的迪士尼门票,如果需要退票,必须在票面日期前48小时通过原购买渠道提交申请,并可能收取手续费。 元数据: {'source': 'online_policy.html', 'category': '退票政策', 'author': 'E-commerceTeam'} ---排名2 (相似度得分/距离: 0.3312) --- ID: 0 原始文本: 迪士尼乐园的门票一经售出,原则上不予退换。但在特殊情况下,如恶劣天气导致园区关闭,可在官方指引下进行改期或退款。 元数据: {'source': 'official_faq_v1.pdf', 'category': '退票政策', 'author': 'Admin'} ---排名3 (相似度得分/距离: 1.0135) --- ID: 1 原始文本: 购买“奇妙年卡”的用户,可以享受一年内多次入园的特权,并且在餐饮和购物时有折扣。 元数据: {'source': 'annual_pass_rules.docx', 'category': '会员权益', 'author': 'MarketingDept'}
距离越小表示越相近,越大表示越不相关;
七、总结
向量数据库并非要取代传统数据库,而是对其能力的重要补充。它将数据从简单的字符和数字提升到了富含语义的数学表示,使计算机能够真正地“理解”和“联想”非结构化数据。随着生成式AI和大语言模型的爆发,向量数据库作为其记忆和知识检索的核心组件,正在成为现代AI技术栈中不可或缺的基础设施。