向量数据库

向量数据库里通常不只是“一个向量数组”,而是至少会包含下面几类信息:

  • 原始内容:例如文本正文 page_content
  • 向量值:Embedding 模型算出来的高维数组
  • 元数据:例如 sourcepagesegment_id、业务标签等
  • 索引结构:用于加速相似度检索

向量数据库真正做的是:把“内容 + 向量 + 元数据”组织起来,并支持按相似度查最相关内容。

一个可检索的数据对象,往往同时包含稠密向量、标量字段,某些系统里还会额外使用稀疏向量。

字段 作用 能力
稠密向量(Dense Vector) 最常见,就是 Embedding 模型输出的一长串浮点数。它擅长表达整体语义,所以“意思相近”的文本通常会更接近 负责“按语义找相近内容”
稀疏向量(Sparse Vector) 不是每一维都有值,而是只有少数维度非零。它更像“关键词及权重”的表达方式,常和词项匹配、倒排索引、BM25 一类思路联系在一起 更偏“按关键词和词权重找相关内容”
标量字段(Scalar Fields)
  • 例如 sourceauthorcategorycreated_atdoc_id 这类普通字段。它们不参与语义向量计算,但非常适合做过滤、排序、权限控制和结果展示
负责“按业务条件过滤结果”

常见数据库分类:

名称 简要说明
FAISS 偏本地、偏算法库,适合快速做向量检索实验
Chroma 轻量、好上手,适合本地原型验证
Milvus 开源专业向量数据库,适合大规模生产环境
Pgvector PostgreSQL 扩展,适合已有 PG 体系的项目
Redis / Redis Stack 既能做缓存,也能做向量检索,适合工程整合
Elasticsearch / OpenSearch 搜索体系成熟,也支持向量搜索

用Redis Stack作为向量存储

  • Redis:基础内存数据库 / 键值存储
  • Redis Stack:在 Redis 基础上打包了搜索、JSON、时间序列、布隆过滤器等扩展能力
  • RediSearch:Redis Stack 中和搜索、全文检索、向量检索强相关的模块

我们之所以能用 Redis 做向量检索,关键是 Redis Stack / RediSearch 提供了向量字段、索引和 KNN 搜索能力。

Redis 可以:

  • 创建向量索引
  • 存储向量和元数据
  • 做 KNN(最近邻)向量搜索
  • 结合元数据过滤做混合检索

Embedding文本向量化

一句话概括: Embeddings 用来衡量文本之间的相关性。

常见应用包括:

  • 搜索:按与查询的相关性对结果排序
  • 聚类:按文本相似性分组
  • 推荐:根据相关文本推荐内容
  • 异常检测:找出与多数内容相关性较低的异常点
  • 多样性测量:分析相似性分布
  • 分类:按与标签的相似性对文本分类

LangChain 官方则进一步强调了两个 API:

  • embed_query(text):把单条查询文本转成向量
  • embed_documents(texts):把多条文档文本批量转成向量

这一组区分非常重要,因为它正好对应真实项目里的两个阶段:

  • 索引阶段:把文档片段批量向量化,通常用 embed_documents
  • 查询阶段:把用户问题向量化,通常用 embed_query

简单小案例

import os
import dashscope
from http import HTTPStatus
from dotenv import load_dotenv

load_dotenv()
dashscope.api_key = os.getenv("aliQwen-api")
# 待向量化的单句文本
input_text = "衣服的质量杠杠的"
# 调用百炼文本嵌入接口:先看清“请求长什么样、返回结构长什么样”
resp = dashscope.TextEmbedding.call(
    model="text-embedding-v4",
    input=input_text,
)
if resp.status_code == HTTPStatus.OK:
    # 这里直接打印完整响应,是为了先观察响应结构;后续案例再逐步只取 embedding 向量使用
    print(resp)
# {"status_code": 200, "request_id": "0a76a5db-f4af-4e5a-b0c4-1689d81ba154", "code": "", "message": "", "output": {"embeddings": [{"embedding": [0.02258586511015892, -0.08700370043516159, -0.013521800749003887, -0.05904024466872215, 0.027100207284092903, -0.03104848973453045, 0.01432843878865242, -0.0008265386568382382,……], "text_index": 0}]}, "usage": {"total_tokens": 6}}

OpenAI兼容写法 

#    对真实项目来说,这种写法很常见,因为保留同一套调用方式后,切换厂商时通常只需要调整 base_url、api_key、model。
#    client.embeddings.create() 的 input 可以是单字符串或字符串列表;返回结果中的 data[i].embedding 就是向量。
import os
from openai import OpenAI
from dotenv import load_dotenv

load_dotenv()
input_text = "衣服的质量杠杠的"

# 使用 OpenAI 兼容接口连接阿里百炼:调用方式仍是 OpenAI SDK,只是连接地址改成百炼的兼容网关
client = OpenAI(
    api_key=os.getenv("aliQwen-api"),
    base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
)

# 与 OpenAI Embedding 调用方式一致:model 为百炼模型名,input 为待向量化的文本
completion = client.embeddings.create(model="text-embedding-v4", input=input_text)
print(completion.model_dump_json())

# {"data":[{"embedding":[0.02258586511015892,-0.08700370043516159,-0.013521800749003887,-0.05904024466872215,0.027100207284092903,-0.03104848973453045,0.01432843878865242,0.01706676371395588,'.....'],"index":0,"object":"embedding"}],"model":"text-embedding-v4","object":"list","usage":{"prompt_tokens":6,"total_tokens":6},"id":"37989997-27b1-9416-98af-091ae0b5c118"}

 用langchain的统一接口做单条与批量向量化

#    这是最贴近后续 LangChain 检索器、向量库、RAG 用法的 Embedding 案例,因为它使用的是 LangChain 统一接口。
#    embed_query(text):更偏“查询阶段”,常用于把用户问题转成向量。
#    embed_documents(texts):更偏“索引阶段”,常用于把文档片段批量转成向量
#    返回值分别是“单个向量”和“向量列表”;向量维度由当前模型决定,建索引和查询时应保持模型一致

import os
from langchain_community.embeddings import DashScopeEmbeddings
from dotenv import load_dotenv
# 使用项目统一的 aliQwen-api;DashScopeEmbeddings 默认只读 DASHSCOPE_API_KEY,故显式传入
embeddings = DashScopeEmbeddings(
    model="text-embedding-v4",
    dashscope_api_key=os.getenv("aliQwen-api"),
)

text = "This is a test document."
# 单条文本 → 一个向量(列表);这类写法更贴近“把用户问题转成查询向量”
query_result = embeddings.embed_query(text)
# sep="":print 多个参数时用空字符串连接,默认是空格;这里让「文本向量长度:」和数字紧挨着输出,中间不留空
print("文本向量长度:", len(query_result), sep="")

# 多条文本 → 多个向量(列表的列表);这类写法更贴近“批量建索引”
doc_results = embeddings.embed_documents(
    [
        "Hi there!",
        "Oh, hello!",
        "What's your name?",
        "My friends call me World",
        "Hello World!",
    ]
)
print(
    "文本向量数量:", len(doc_results), ",文本向量长度:", len(doc_results[0]), sep=""
)
# 文本向量数量:5,文本向量长度:1024

 

 向量相似度计算常见指标

  • 余弦相似度(Cosine Similarity)
  • 欧氏距离(Euclidean Distance)
  • 点积(Dot Product)

 其中最适合入门先建立直觉的,通常是 余弦相似度。它关注的重点不是“两个向量长度有多像”,而是“两个向量方向有多接近。

余弦相似度公式:

cos(theta) = (A · B) / (|A| |B|)

 它的结果通常在 [-1, 1] 范围内:

  • 越接近 1,通常表示越相似
  • 越接近 0,表示相关性较弱
  • 越接近 -1,表示方向相反

 其他单位

  • COSINE:更关注向量方向是否接近,是文本语义检索里最常见、也最容易理解的一类度量。
  • L2:欧氏距离,关注两个点在空间里的直线距离。距离越小,通常表示越接近。
  • IP(Inner Product):内积。某些模型或系统会直接用它做相似度计算;在向量已归一化时,它和余弦相似度往往会非常接近。

真正要记住的不是“谁永远最好”,而是这条工程规则:

Embedding 模型的特性、索引建立时选择的度量方式、查询时传入的 metric,三者必须保持一致。

所以当你看到某个向量库示例里写的是 COSINEL2 或 IP,不要把它当成无关紧要的参数;它本质上决定了系统如何理解“相似”。

检索

  • 精确检索(Exact KNN / FLAT):把查询向量和库里的所有向量都算一遍,结果最直接,也最容易理解。
  • 近似检索(ANN, Approximate Nearest Neighbor):通过索引结构加速搜索,只近似地找到“最可能接近”的一批结果。
  • FLAT:更偏暴力搜索,准确但慢
  • HNSW:工程里很常见的 ANN 索引,通常能在召回率和延迟之间取得较好平衡

 入门阶段可以先记住一句话:索引的作用不是改变“语义相似”的定义,而是让“找最相似内容”这件事在大规模数据下也能跑得动。

#    案例的重点不是特定模型,而是“向量一旦拿到手,就可以做数学比较”,这是语义检索的底层基础
#    公式:cos(theta) = (A·B) / (|A||B|);在 Python 里常用 np.dot 和 np.linalg.norm 实现。

import dashscope
import os
from http import HTTPStatus
import numpy as np

from dotenv import load_dotenv
load_dotenv()


# 准备多句文本,用于观察“语义越接近,相似度通常越高”
texts = ["我喜欢吃苹果", "苹果是我最喜欢吃的水果", "我喜欢用苹果手机"]
embeddings = []
# 这里选用多模态 embedding 接口来处理文本输入,主要是为了演示“拿到向量后如何做比较”
# 若你在真实项目里只处理文本,也完全可以换成常规文本 embedding 模型
for text in texts:
    input_data = [{"text": text}]
    resp = dashscope.MultiModalEmbedding.call(
        model="multimodal-embedding-v1",
        api_key=os.getenv("aliQwen-api"),
        base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
        input=input_data,
    )
    if resp.status_code == HTTPStatus.OK:
        embedding = resp.output["embeddings"][0]["embedding"]
        embeddings.append(embedding)
        
#    计算余弦相似度
def cosine_similarity(vec1,vec2):
    """计算两个向量的余弦相似度:点积 / (模长之积),结果越接近 1 一般越相似"""
    dot_product = np.dot(vec1,vec2)
    # np.dot() 计算两个向量的点积。点积越大,通常说明两个向量方向越接近,但它也会受到向量长度影响
    norm_vec1 = np.linalg.norm(vec1)
    # np.linalg.norm()计算 vec1 的模长,也叫向量长度
    # vec1 = [1, 2, 3]
    # 模长是:sqrt(1² + 2² + 3²)= sqrt(14)
    norm_vec2 = np.linalg.norm(vec2)
    return dot_product / (norm_vec1 * norm_vec2)
    # 返回余弦相似度,点积 / 两个向量模长的乘积
print("文本相似度比较结果:")
print("=" * 60)    
    
for i in range(len(texts)):
    for j in range(i + 1, len(texts)):
        similarity = cosine_similarity(embeddings[i], embeddings[j])
        print(f"文本{i+1} vs 文本{j+1}:")
        print(f"  文本{i+1}: {texts[i]}")
        print(f"  文本{j+1}: {texts[j]}")
        print(f"  余弦相似度: {similarity:.4f}")
        print("-" * 40)    

相似度在项目中如何使用

  • 语义检索排序:把最相关的文本排在前面
  • 文本去重:判断两段内容是否高度重复
  • 推荐:找相似商品、相似文章、相似问题
  • 聚类分析:把语义相近的内容分成一组

向量库的写入和检索

它做的事情可以概括成:

  1. 先准备若干 Documentpage_content + metadata,在完整 RAG 中常由加载器与切分器产生,见 第 19 章
  2. 用 DashScopeEmbeddings 把 page_content 向量化
  3. 用 Redis.from_documents(...) 一次性写入 Redis
  4. 再通过 as_retriever() 生成检索器
  5. 对查询文本做相似度检索
#    演示的是:先准备 Document,再向量化,再写入 Redis,最后按相似度检索。
#    Redis.from_documents() 会自动读取每个 Document 的 page_content,调用 embedding 做向量化,并把原文、向量、metadata 一起写入 Redis。
#    as_retriever() 得到的是检索器;invoke(查询文本) 时,LangChain 会先把查询文本转成向量,再去库里找最相关的 Document
#    这个案例是 RAG 的底层能力演示,不包含文档加载器、文本分割器和“检索后交给大模型生成答案”的完整流程
#    redis_url 和 index_name 要与本地环境一致;如果要复用已有索引,查询端也必须使用同一个 index_name

import os
from langchain_community.embeddings import DashScopeEmbeddings
from langchain_community.vectorstores import Redis
from langchain_core.documents import Document

from dotenv import load_dotenv
load_dotenv()

# 1. 初始化嵌入模型
embeddings = DashScopeEmbeddings(
    model="text-embedding-v3", dashscope_api_key=os.getenv("aliQwen-api")
)

# 2. 构造 Document 列表:page_content 是正文,metadata 是附加信息
# 在完整 RAG 中,这些 Document 往往来自“加载器 + 分割器”;本案例先用手写数据聚焦理解向量库存取流程
texts = [
    "通义千问是阿里巴巴研发的大语言模型。",
    "Redis 是一个高性能的键值存储系统,支持向量检索。",
    "LangChain 可以轻松集成各种大模型和向量数据库。",
]
documents = [
    Document(page_content=text, metadata={"source": "manual"}) for text in texts
]
# 3. 一次性写入 Redis:内部会对每个 Document 的 page_content 做向量化,并建立可检索索引
# vector_store表示一个 Redis 向量库。前面已经把这 3 条文本转成向量,并写进了 Redis
vector_store = Redis.from_documents(
    documents= documents,
    embedding = embeddings,
    redis_url = "redis://localhost:26379"
    index_name = "my_index11"
)
# 4. 得到检索器:当你 invoke 查询文本时,LangChain 会先把问题向量化,再在库中做相似度检索
#    把向量数据库 vector_store 包装成一个“检索器 retriever”,然后用自然语言问题去检索最相关的文档
#    把 vector_store 转成检索器    search_kwargs={"k": 2}:每次检索时,返回最相关的前 2 条 Document
retriever = vector_store.as_retriever(search_kwargs={"k": 2})
#    这块可以理解为 RAG 里的 “R”,也就是 Retrieval,检索阶段。它还没有调用大模型回答问题,只是先从 Redis 里找出最相关的资料。
#    这句是真正开始检索
results = retriever.invoke("LangChain 和 Redis 怎么结合?")
for res in results:
    print(res.page_content)
    
    

使用langchain_radis的RedisVoctorStore写入文本

这类写法在真实项目里很常见,因为很多时候数据并不是一次性导入,而是:

  • 批量导入
  • 增量追加
  • 定时重建索引
#    案例展示的是纯文本流驱动的入库路线:先创建 `RedisVectorStore`,再通过 `add_texts()` 把字符串列表写入向量库。
#    `add_texts(texts, metadata)` 会在内部调用 `embed_documents(texts)` 做批量向量化,然后把文本、向量和 metadata 一起写入 Redis
#     这条路线和 `from_documents(...)` 并不冲突:前者更适合你手里已经是纯文本列表,后者更适合你已经有 `Document` 列表
#    返回的 ids 可用于后续更新、删除或追踪;index_name 需要和后续检索端保持一致

from langchain_redis import RedisConfig, RedisVectorStore
from langchain_community.embeddings import DashScopeEmbeddings
import os
from dotenv import load_dotenv
load_dotenv()

# 1. 初始化嵌入模型
embeddingsModel = DashScopeEmbeddings(
    model="text-embedding-v3",dashscope_api_key=os.getenv("aliQwen-api")
)
# 2. 待写入的文本及(可选)元数据
texts = [
    "我喜欢吃苹果",
    "苹果是我最喜欢吃的水果",
    "我喜欢用苹果手机",
]
# 批量转成向量:这里只是为了先观察向量维度和内容;真正写入时 add_texts 内部会再次完成向量化
embeddings = embeddingsModel.embed_documents(texts)
# [[0.15256456,54456464,...]]
# enumerate(可迭代对象,起始位置):遍历可迭代对象时同时获取元素的索引和值
for i,vec in enumerate(embeddings, 1):
    print(f"文本 {i}: {texts[i-1]}")
    print(f"向量长度: {len(vec)}")
    print(f"前5个向量值: {vec[:10]}\n")
    
# 定义每条文本对应的元数据信息;真实 RAG 中这些 metadata 往往来自 Document.metadata,也可作为来源展示或过滤条件
metadata = [{"segment_id": str(i)} for i in range(1, len(texts) + 1)]

# 3. Redis 连接与索引名(需与检索案例一致
config = RedisCofig(
    index_name="newsgroups",
    redis_url="redis://localhost:26379",
)
# 创建 Redis 向量存储实例:此时只是“连上库 + 指定索引配置”,还没真正写入文本;真正写入发生在 add_texts()
vector_store = RedisVectorStore(embeddingsModel, config=config)
# 4. 将文本与元数据写入向量库(add_texts 内部会调 embed_documents,无需先算向量)
ids = vector_store.add_texts(texts, metadata)
# 打印前5个存储记录的ID
print(ids[0:5])

连接已有索引,做相似性检索

 

这个案例和上一节是一组配套案例。它假设你已经写入过数据,然后再去做查询。

核心动作是:

  • 连接已有的 index_name
  • 把查询文本向量化
  • 在 Redis 中找最相近的若干条文本
  • 返回 (Document, score) 结果
# 相似性检索的核心流程是:查询文本先向量化,再到向量库中找到与查询向量最接近的若干条记录。
# `similarity_search_with_score(query, k)` 返回 `(Document, score)` 列表;
#很多实现里 score 更接近“距离”,通常越小越相似
# 运行前需确保 Redis 中已有数据,例如先执行同目录下的 RedisVectorStore.py;`index_name`、`redis_url` 也必须保持一致
# 在完整 RAG 里,这一步通常不会直接把结果打印完就结束,而是会把查到的 `Document` 进一步组织进 Prompt,再交给 LLM 生成答案

from langchain_redis import RedisConfig, RedisVectorStore
from langchain_community.embeddings import DashScopeEmbeddings
import os
from dotenv import load_dotenv

load_dotenv()

# 1. 嵌入模型(与写入时一致,保证向量空间一致);需在 .env 中配置 aliQwen-api
embeddingsModel = DashScopeEmbeddings(
    model="text-embedding-v3", dashscope_api_key=os.getenv("aliQwen-api")
)

# 2. 连接已有索引(与 RedisVectorStore.py 中 index_name、redis_url 一致)
vector_store = RedisVectorStore(
    embeddingsModel,
    config=RedisConfig(index_name="newsgroups", redis_url="redis://localhost:26379"),
)

# 3. 查询文本 → 向量化 → 在库中做相似度检索;这里取前 3 条结果
query = "我喜欢用什么手机"
results = vector_store.similarity_search_with_score(query, k=3)

print("=== 查询结果 ===")
for i, (doc, score) in enumerate(results, 1):
    # 这里把“距离”近似换算成“相似度”只是为了展示更直观;工程里请以具体返回定义为准
    similarity = 1 - score
    print(f"结果 {i}:")
    print(f"内容: {doc.page_content}")
    print(f"元数据: {doc.metadata}")
    print(f"相似度: {similarity:.4f}")

这里有一个很重要的工程提醒一定要注意:

similarity_search_with_score() 返回的 score,很多实现里本质上是“距离”而不是“越大越好”的概率分数。

  • 有些场景下,score 越小反而表示越相似
  • 代码里把它换算成 1 - score,更多是为了课程演示时方便直觉理解
  • 真正项目里,必须看具体向量库、距离度量方式和返回定义,不能机械地把所有 score 都按“相似度百分比”解释

元数据过滤、混合检索与重排序

很多人在跑完第一个向量检索案例后,会误以为“检索 = 把问题转成向量,然后直接搜 top-k”。这条主线当然没错,但真实项目里,通常还会再叠加三类能力:

  • 元数据过滤(metadata filter):先用 sourcecategory、时间范围、权限字段等条件缩小候选范围,再做向量检索。
  • 混合检索(hybrid retrieval):同时结合语义向量检索和关键词检索,或者结合不同类型的召回路径。
  • 重排序(rerank):先召回一批候选结果,再用更精细的模型或策略重新排序。

这三者可以这样理解:

  • 过滤,解决“只在某个业务范围内找”
  • 混合,解决“既想看语义,也不想丢掉关键词命中”
  • 重排序,解决“已经找回来了,但顺序还不够好”

“重排序”特别值得单独记一下。它并不是重新去全库里搜索,而是对已经召回的一小批候选结果做二次排序优化。很多系统里,向量检索负责第一阶段召回,reranker 负责第二阶段排序,这样往往能兼顾速度和效果。

本章的 Redis 实战主要演示的是稠密向量检索的第一阶段能力。你后面在更完整的 RAG 系统里,如果发现“能搜到,但排序不够稳”或者“关键词信息丢了”,就可以优先从这三类增强手段入手

 

posted @ 2026-05-06 13:52  幻影之舞  阅读(19)  评论(0)    收藏  举报