向量数据库与 Embedding 实战教程

教程简介

零基础向量数据库与Embedding实战教程,涵盖向量与Embedding基础、文本/图像/多模态Embedding模型、Chroma/Milvus/Qdrant/FAISS主流向量数据库部署与实战、混合检索与重排序优化、RAG完整流水线等核心技能,配有智能文档搜索引擎实战项目,适合AI开发者系统学习。

向量数据库与 Embedding 实战教程

零基础入门向量数据库与Embedding技术,从概念理解到生产实战,掌握AI时代的核心检索技术。


目录

  1. 向量与Embedding基础
  2. 文本Embedding模型
  3. 图像与多模态Embedding
  4. 向量数据库概览
  5. Chroma快速入门
  6. Milvus生产级部署
  7. Qdrant实战
  8. FAISS本地使用
  9. 检索策略优化
  10. RAG完整流水线
  11. 高级应用
  12. 实战项目:智能文档搜索引擎

1. 向量与Embedding基础

1.1 什么是向量

在数学和物理学中,向量(Vector)是一个有大小和方向的量。在计算机科学和AI领域,我们所说的向量通常指的是数值数组,它是对现实世界中各种数据(文本、图片、音频等)的一种数学表示。

例如,一个简单的向量可以是:

[0.12, -0.45, 0.78, 0.33, -0.91, 0.22, 0.56, -0.11]

这个数组中的每个数字代表数据在某个维度上的特征值。向量的维度(数组长度)可以从几十到几千不等,取决于使用的Embedding模型。

1.2 为什么需要向量化

计算机只能理解数字,不能直接理解"今天天气真好"这样的自然语言。我们需要一种方法,把文本、图片等非结构化数据转换成计算机可以处理的数值形式。

传统方法(如词袋模型、TF-IDF)存在明显局限:

  • 语义缺失:无法捕捉词语之间的语义关系
  • 维度灾难:高维稀疏向量存储和计算效率低
  • 泛化能力差:无法处理同义词、近义词

Embedding技术的出现解决了这些问题。它将数据映射到一个低维稠密的向量空间,使得:

  • 语义相近的数据在向量空间中距离更近
  • 支持高效的相似度计算
  • 能捕捉复杂的语义关系

1.3 Embedding原理简述

Embedding本质上是一个映射函数,它将高维稀疏的数据(如one-hot编码的词汇表)映射到低维稠密的向量空间。

以文本Embedding为例,其工作流程大致如下:

  1. 分词:将文本拆分为token(词或子词)
  2. 编码:通过神经网络(如Transformer)将每个token编码为向量
  3. 池化:将多个token的向量合并为一个固定长度的句子/文档向量

现代Embedding模型(如BERT、Sentence-Transformers)通常基于Transformer架构,通过大规模语料预训练学习到丰富的语义表示。

1.4 向量空间与相似度

在向量空间中,我们通过计算向量之间的距离或相似度来衡量数据的语义关联程度。常用的相似度计算方法包括:

余弦相似度(Cosine Similarity)

similarity = (A · B) / (||A|| × ||B||)

值域为[-1, 1],值越大表示越相似。这是最常用的方法,因为它只关注方向,不受向量长度影响。

欧氏距离(Euclidean Distance)

distance = √(Σ(aᵢ - bᵢ)²)

值域为[0, +∞),值越小表示越相似。

点积(Dot Product)

dot = Σ(aᵢ × bᵢ)

当向量已归一化时,点积等价于余弦相似度。

Python示例

import numpy as np

def cosine_similarity(a, b):
    """计算两个向量的余弦相似度"""
    return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))

def euclidean_distance(a, b):
    """计算两个向量的欧氏距离"""
    return np.linalg.norm(np.array(a) - np.array(b))

# 示例:比较三个句子的相似度
vec_cat = [0.8, 0.2, 0.1, 0.9]      # "猫"
vec_dog = [0.7, 0.3, 0.15, 0.85]    # "狗"
vec_car = [0.1, 0.9, 0.8, 0.1]      # "汽车"

print(f"猫 vs 狗: {cosine_similarity(vec_cat, vec_dog):.4f}")  # 高相似度
print(f"猫 vs 汽车: {cosine_similarity(vec_cat, vec_car):.4f}")  # 低相似度

2. 文本Embedding模型

2.1 OpenAI Embedding

OpenAI提供了多个Embedding模型,其中最常用的是text-embedding-3-smalltext-embedding-3-large

特点

  • 支持多种语言
  • 可调节输出维度(Matryoshka表示学习)
  • 通过API调用,无需本地部署
  • text-embedding-3-large最高支持3072维

使用示例

from openai import OpenAI

client = OpenAI()

def get_embedding(text, model="text-embedding-3-small"):
    """获取文本的Embedding向量"""
    response = client.embeddings.create(
        input=text,
        model=model
    )
    return response.data[0].embedding

# 单个文本
embedding = get_embedding("向量数据库是AI时代的重要基础设施")
print(f"维度: {len(embedding)}")  # 1536

# 批量处理(更高效)
texts = ["向量数据库", "Embedding模型", "语义搜索"]
response = client.embeddings.create(
    input=texts,
    model="text-embedding-3-small"
)
embeddings = [item.embedding for item in response.data]
print(f"获取了 {len(embeddings)} 个向量")

维度压缩

# 使用较小的维度以节省存储和计算
response = client.embeddings.create(
    input="测试文本",
    model="text-embedding-3-small",
    dimensions=512  # 从1536压缩到512
)

2.2 BGE系列模型

BGE(BAAI General Embedding)是北京智源人工智能研究院开源的Embedding模型系列,在中文场景表现出色。

主要模型

  • bge-small-zh-v1.5:轻量级,512维,适合资源受限场景
  • bge-base-zh-v1.5:基础版,768维,平衡性能与效率
  • bge-large-zh-v1.5:大型版,1024维,最佳效果
  • bge-m3:多语言、多功能、多粒度,支持100+语言

使用示例

from sentence_transformers import SentenceTransformer

# 加载模型(首次运行会自动下载)
model = SentenceTransformer('BAAI/bge-base-zh-v1.5')

# 编码文本
texts = [
    "什么是向量数据库?",
    "向量数据库是一种专门存储和检索高维向量的数据库系统",
    "今天天气真不错"
]

embeddings = model.encode(texts)
print(f"向量维度: {embeddings.shape[1]}")  # 768

# 计算相似度
from sentence_transformers.util import cos_sim

similarities = cos_sim(embeddings[0], embeddings[1])
print(f"问题与答案的相似度: {similarities.item():.4f}")  # 高

similarities = cos_sim(embeddings[0], embeddings[2])
print(f"问题与无关文本的相似度: {similarities.item():.4f}")  # 低

BGE模型的查询前缀

BGE系列模型建议在查询时添加前缀以获得更好的检索效果:

# 查询时添加前缀
query = "query: 什么是向量数据库?"
# 文档不需要前缀
doc = "向量数据库是一种专门存储和检索高维向量的数据库系统"

query_emb = model.encode(query)
doc_emb = model.encode(doc)

2.3 M3E模型

M3E(Moka Massive Mixed Embedding)是由MokaAI团队开源的中文Embedding模型。

特点

  • 专门针对中文优化
  • 支持中英双语
  • 采用对比学习训练
  • 开源免费,可本地部署

使用示例

from sentence_transformers import SentenceTransformer

model = SentenceTransformer('moka-ai/m3e-base')

texts = ["人工智能", "机器学习", "深度学习", "烹饪美食"]
embeddings = model.encode(texts)

# 计算相似度矩阵
from sentence_transformers.util import cos_sim
import numpy as np

similarity_matrix = cos_sim(embeddings, embeddings)
print("相似度矩阵:")
print(similarity_matrix.numpy())

2.4 Jina Embeddings

Jina AI提供了多个高质量的Embedding模型,支持多种语言和长文本。

特点

  • 支持8K长文本
  • 多语言支持
  • Late Interaction技术
  • 提供API和开源模型

使用示例

import requests

def jina_embedding(texts, api_key):
    """使用Jina AI API获取Embedding"""
    url = "https://api.jina.ai/v1/embeddings"
    headers = {
        "Authorization": f"Bearer {api_key}",
        "Content-Type": "application/json"
    }
    data = {
        "model": "jina-embeddings-v3",
        "input": texts
    }
    response = requests.post(url, json=data, headers=headers)
    return [item['embedding'] for item in response.json()['data']]

# 使用示例
texts = ["向量数据库", "Embedding模型", "语义搜索"]
# embeddings = jina_embedding(texts, "your-api-key")

2.5 模型选型建议

场景 推荐模型 原因
中文语义搜索 bge-base-zh-v1.5 中文效果最佳,开源免费
多语言场景 bge-m3 或 text-embedding-3-large 支持100+语言
资源受限 bge-small-zh-v1.5 轻量高效
生产环境API text-embedding-3-small 稳定可靠,性价比高
长文本处理 jina-embeddings-v3 支持8K token
最高精度 text-embedding-3-large 3072维,效果最佳

3. 图像与多模态Embedding

3.1 CLIP模型

CLIP(Contrastive Language-Image Pre-training)是OpenAI开发的多模态模型,能够将图像和文本映射到同一个向量空间。

核心思想:通过对比学习,让相似的图像-文本对在向量空间中距离更近。

使用示例

import torch
from PIL import Image
from transformers import CLIPProcessor, CLIPModel

# 加载模型
model = CLIPModel.from_pretrained("openai/clip-vit-base-patch32")
processor = CLIPProcessor.from_pretrained("openai/clip-vit-base-patch32")

# 图像编码
image = Image.open("cat.jpg")
inputs = processor(images=image, return_tensors="pt")
image_features = model.get_image_features(**inputs)

# 文本编码
texts = ["一只猫", "一只狗", "一辆汽车"]
inputs = processor(text=texts, return_tensors="pt", padding=True)
text_features = model.get_text_features(**inputs)

# 计算相似度
similarity = torch.nn.functional.cosine_similarity(
    image_features, text_features
)
print("图像与文本的相似度:", similarity.detach().numpy())

3.2 ImageBind

ImageBind是Meta提出的六模态Embedding模型,支持图像、文本、音频、深度、热力图和IMU数据。

应用场景

  • 跨模态检索(用文字搜索音频)
  • 多模态融合(结合图像和音频进行分析)
  • 零样本分类

3.3 多模态向量的实际应用

# 使用CLIP进行图像搜索
from sentence_transformers import SentenceTransformer, util
from PIL import Image
import glob

# 加载CLIP模型
model = SentenceTransformer('clip-ViT-B-32')

# 编码查询图片
query_image = Image.open("query.jpg")
query_embedding = model.encode(query_image)

# 编码数据库中的图片
image_paths = glob.glob("images/*.jpg")
images = [Image.open(p) for p in image_paths]
image_embeddings = model.encode(images)

# 搜索最相似的图片
similarities = util.cos_sim(query_embedding, image_embeddings)[0]
top_k = similarities.topk(5)

print("最相似的5张图片:")
for score, idx in zip(top_k.values, top_k.indices):
    print(f"  {image_paths[idx]}: {score:.4f}")

4. 向量数据库概览

4.1 为什么需要向量数据库

传统的数据库(如MySQL、PostgreSQL)是为结构化数据设计的,它们在处理向量相似度搜索时存在明显不足:

  • 不支持高效的近似最近邻(ANN)搜索
  • 无法处理高维向量的索引
  • 缺乏向量专用的存储和查询优化

向量数据库专门解决了这些问题,提供了:

  • 高效的向量索引和检索
  • 支持亿级向量的毫秒级查询
  • 灵活的过滤和混合查询
  • 可扩展的分布式架构

4.2 主流向量数据库对比

数据库 类型 开源 分布式 特点 适用场景
Milvus 专用 高性能、可扩展、丰富索引 生产环境大规模部署
Chroma 专用 轻量易用、Python原生 快速原型、小规模应用
Qdrant 专用 Rust实现、高性能、丰富过滤 生产环境、需要复杂过滤
Weaviate 专用 GraphQL接口、多模态支持 需要GraphQL的场景
Pinecone 云服务 全托管、零运维 不想自己运维的场景
FAISS Facebook开源、极致性能 本地实验、嵌入式应用
pgvector 扩展 PostgreSQL扩展 已有PG的场景

4.3 选型建议

快速原型开发:选择Chroma

  • 安装简单,pip install chromadb
  • Python原生API,学习成本低
  • 内置Embedding模型支持

生产环境大规模部署:选择Milvus或Qdrant

  • 支持分布式,可水平扩展
  • 丰富的索引类型和查询优化
  • 活跃的社区和企业支持

已有PostgreSQL:选择pgvector

  • 无需引入新组件
  • 利用PG的事务和备份机制
  • 适合中小规模向量数据

不想运维:选择Pinecone

  • 全托管服务,零运维
  • 按使用量计费
  • 适合快速上线

5. Chroma快速入门

5.1 安装与配置

# 安装Chroma
pip install chromadb

# 如果需要使用Sentence Transformers作为Embedding
pip install chromadb sentence-transformers

5.2 基本使用

import chromadb

# 创建客户端(内存模式)
client = chromadb.Client()

# 创建持久化客户端(数据保存到磁盘)
# client = chromadb.PersistentClient(path="./chroma_db")

# 创建Collection
collection = client.create_collection(
    name="my_documents",
    metadata={"hnsw:space": "cosine"}  # 使用余弦相似度
)

# 添加文档
collection.add(
    documents=["向量数据库是AI时代的重要基础设施", 
                "Embedding将文本转换为数值向量",
                "语义搜索基于向量相似度实现"],
    ids=["doc1", "doc2", "doc3"],
    metadatas=[{"source": "blog"}, 
               {"source": "tutorial"}, 
               {"source": "wiki"}]
)

# 查询
results = collection.query(
    query_texts=["什么是向量数据库?"],
    n_results=2
)

print("查询结果:")
for doc, distance in zip(results['documents'][0], results['distances'][0]):
    print(f"  文档: {doc}")
    print(f"  距离: {distance:.4f}")
    print()

5.3 使用自定义Embedding模型

from sentence_transformers import SentenceTransformer

# 加载自定义Embedding模型
embedding_model = SentenceTransformer('BAAI/bge-base-zh-v1.5')

# 创建自定义Embedding函数
class BGEEmbedding(chromadb.EmbeddingFunction):
    def __call__(self, input):
        return embedding_model.encode(input).tolist()

# 使用自定义Embedding创建Collection
collection = client.create_collection(
    name="bge_documents",
    embedding_function=BGEEmbedding()
)

# 添加文档(不需要手动提供向量)
collection.add(
    documents=["向量数据库是AI时代的重要基础设施", 
                "Embedding将文本转换为数值向量"],
    ids=["doc1", "doc2"]
)

# 查询
results = collection.query(
    query_texts=["向量数据库的作用是什么?"],
    n_results=2
)

5.4 过滤查询

# 添加带元数据的文档
collection.add(
    documents=["文档1", "文档2", "文档3", "文档4"],
    ids=["1", "2", "3", "4"],
    metadatas=[
        {"category": "tech", "year": 2024},
        {"category": "tech", "year": 2023},
        {"category": "science", "year": 2024},
        {"category": "science", "year": 2023}
    ]
)

# 按元数据过滤
results = collection.query(
    query_texts=["技术文档"],
    n_results=2,
    where={"category": "tech"}  # 只搜索tech类别的文档
)

# 复合过滤
results = collection.query(
    query_texts=["最新研究"],
    n_results=2,
    where={
        "$and": [
            {"category": "science"},
            {"year": {"$gte": 2024}}
        ]
    }
)

5.5 更新与删除

# 更新文档
collection.update(
    ids=["doc1"],
    documents=["更新后的文档内容"],
    metadatas=[{"source": "updated"}]
)

# 删除文档
collection.delete(ids=["doc1"])

# 获取文档
docs = collection.get(ids=["doc2", "doc3"])
print(docs)

6. Milvus生产级部署

6.1 Docker Compose部署

# docker-compose.yml
version: '3.5'

services:
  etcd:
    image: quay.io/coreos/etcd:v3.5.5
    environment:
      - ETCD_AUTO_COMPACTION_MODE=revision
      - ETCD_AUTO_COMPACTION_RETENTION=1000
      - ETCD_QUOTA_BACKEND_BYTES=4294967296
      - ETCD_SNAPSHOT_COUNT=50000
    volumes:
      - etcd:/etcd
    command: etcd -advertise-client-urls=http://127.0.0.1:2379 -listen-client-urls http://0.0.0.0:2379 --data-dir /etcd

  minio:
    image: minio/minio:RELEASE.2023-03-20T20-16-18Z
    environment:
      MINIO_ACCESS_KEY: minioadmin
      MINIO_SECRET_KEY: minioadmin
    volumes:
      - minio:/minio_data
    command: minio server /minio_data --console-address ":9001"
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
      interval: 30s
      timeout: 20s
      retries: 3

  standalone:
    image: milvusdb/milvus:v2.4-latest
    command: ["milvus", "run", "standalone"]
    environment:
      ETCD_ENDPOINTS: etcd:2379
      MINIO_ADDRESS: minio:9000
    volumes:
      - milvus:/var/lib/milvus
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:9095/healthz"]
      interval: 30s
      start_period: 90s
      timeout: 20s
      retries: 3
    ports:
      - "19530:19530"
      - "9091:9091"
    depends_on:
      - etcd
      - minio

volumes:
  etcd:
  minio:
  milvus:
# 启动Milvus
docker compose up -d

# 检查状态
docker compose ps

6.2 基本使用

from pymilvus import connections, Collection, FieldSchema, CollectionSchema, DataType, utility

# 连接Milvus
connections.connect("default", host="localhost", port="19530")

# 定义Schema
fields = [
    FieldSchema(name="id", dtype=DataType.INT64, is_primary=True, auto_id=True),
    FieldSchema(name="text", dtype=DataType.VARCHAR, max_length=65535),
    FieldSchema(name="embedding", dtype=DataType.FLOAT_VECTOR, dim=768),
    FieldSchema(name="category", dtype=DataType.VARCHAR, max_length=100)
]

schema = CollectionSchema(fields, description="文档向量集合")
collection = Collection("documents", schema)

# 创建索引
index_params = {
    "metric_type": "COSINE",
    "index_type": "HNSW",
    "params": {"M": 16, "efConstruction": 200}
}
collection.create_index("embedding", index_params)

# 插入数据
from sentence_transformers import SentenceTransformer
model = SentenceTransformer('BAAI/bge-base-zh-v1.5')

texts = ["向量数据库简介", "Embedding原理", "语义搜索实践"]
categories = ["tech", "tech", "tutorial"]
embeddings = model.encode(texts).tolist()

collection.insert([texts, embeddings, categories])
collection.flush()

# 加载Collection到内存
collection.load()

# 查询
query_embedding = model.encode(["什么是向量数据库"]).tolist()
results = collection.search(
    data=query_embedding,
    anns_field="embedding",
    param={"metric_type": "COSINE", "params": {"ef": 100}},
    limit=3,
    output_fields=["text", "category"]
)

for hits in results:
    for hit in hits:
        print(f"ID: {hit.id}, 距离: {hit.distance:.4f}")
        print(f"  文本: {hit.entity.get('text')}")
        print(f"  类别: {hit.entity.get('category')}")

6.3 索引类型详解

# IVF_FLAT - 倒排索引 + 暴力搜索
index_params_ivf = {
    "index_type": "IVF_FLAT",
    "metric_type": "COSINE",
    "params": {"nlist": 1024}  # 聚类中心数
}

# IVF_SQ8 - 倒排索引 + 标量量化
index_params_sq8 = {
    "index_type": "IVF_SQ8",
    "metric_type": "COSINE",
    "params": {"nlist": 1024}
}

# IVF_PQ - 倒排索引 + 乘积量化
index_params_pq = {
    "index_type": "IVF_PQ",
    "metric_type": "COSINE",
    "params": {"nlist": 1024, "m": 8, "nbits": 8}
}

# HNSW - 层次化可导航小世界图(推荐)
index_params_hnsw = {
    "index_type": "HNSW",
    "metric_type": "COSINE",
    "params": {"M": 16, "efConstruction": 200}
}

# DISKANN - 基于磁盘的索引(超大规模)
index_params_diskann = {
    "index_type": "DISKANN",
    "metric_type": "COSINE",
    "params": {"search_list": 100}
}

6.4 性能调优

# 搜索参数调优
search_params = {
    "metric_type": "COSINE",
    "params": {
        "ef": 200  # HNSW搜索精度,越大越精确但越慢
    }
}

# 批量查询提升吞吐量
results = collection.search(
    data=[query1, query2, query3],  # 批量查询
    anns_field="embedding",
    param=search_params,
    limit=10
)

# 使用分区提升查询速度
collection.create_partition("tech")
collection.create_partition("science")

# 查询时指定分区
results = collection.search(
    data=query_embedding,
    anns_field="embedding",
    param=search_params,
    limit=10,
    partition_names=["tech"]  # 只在tech分区中搜索
)

7. Qdrant实战

7.1 安装配置

# Docker安装
docker run -p 6333:6333 -p 6334:6334 \
    -v $(pwd)/qdrant_storage:/qdrant/storage \
    qdrant/qdrant

# Python客户端
pip install qdrant-client

7.2 基本使用

from qdrant_client import QdrantClient
from qdrant_client.models import Distance, VectorParams, PointStruct
from sentence_transformers import SentenceTransformer

# 连接Qdrant
client = QdrantClient(host="localhost", port=6333)

# 创建Collection
client.create_collection(
    collection_name="documents",
    vectors_config=VectorParams(
        size=768,
        distance=Distance.COSINE
    )
)

# 准备数据
model = SentenceTransformer('BAAI/bge-base-zh-v1.5')
texts = ["向量数据库简介", "Embedding原理", "语义搜索实践"]
embeddings = model.encode(texts)

# 插入数据
points = [
    PointStruct(
        id=i,
        vector=embedding.tolist(),
        payload={"text": text, "category": "tech"}
    )
    for i, (text, embedding) in enumerate(zip(texts, embeddings))
]

client.upsert(
    collection_name="documents",
    points=points
)

# 搜索
query_embedding = model.encode(["什么是向量数据库"])[0].tolist()
results = client.search(
    collection_name="documents",
    query_vector=query_embedding,
    limit=3
)

for result in results:
    print(f"ID: {result.id}, 分数: {result.score:.4f}")
    print(f"  文本: {result.payload['text']}")

7.3 Payload过滤

from qdrant_client.models import Filter, FieldCondition, MatchValue, Range

# 精确匹配过滤
results = client.search(
    collection_name="documents",
    query_vector=query_embedding,
    query_filter=Filter(
        must=[
            FieldCondition(
                key="category",
                match=MatchValue(value="tech")
            )
        ]
    ),
    limit=5
)

# 范围过滤
results = client.search(
    collection_name="documents",
    query_vector=query_embedding,
    query_filter=Filter(
        must=[
            FieldCondition(
                key="year",
                range=Range(gte=2024)
            )
        ]
    ),
    limit=5
)

# 复合过滤
from qdrant_client.models import Filter, FieldCondition, MatchValue

results = client.search(
    collection_name="documents",
    query_vector=query_embedding,
    query_filter=Filter(
        must=[
            FieldCondition(key="category", match=MatchValue(value="tech")),
            FieldCondition(key="year", range=Range(gte=2024))
        ],
        must_not=[
            FieldCondition(key="status", match=MatchValue(value="archived"))
        ]
    ),
    limit=5
)

7.4 分布式部署

# docker-compose-cluster.yml
version: '3.5'

services:
  qdrant-node-1:
    image: qdrant/qdrant
    ports:
      - "6333:6333"
      - "6334:6334"
    volumes:
      - qdrant_node1:/qdrant/storage
    environment:
      QDRANT__CLUSTER__ENABLED: "true"
      QDRANT__CLUSTER__P2P__PORT: 6335
      QDRANT__CLUSTER__BOOTSTRAP: "qdrant-node-1:6335"

  qdrant-node-2:
    image: qdrant/qdrant
    volumes:
      - qdrant_node2:/qdrant/storage
    environment:
      QDRANT__CLUSTER__ENABLED: "true"
      QDRANT__CLUSTER__P2P__PORT: 6335
      QDRANT__CLUSTER__BOOTSTRAP: "qdrant-node-1:6335"

  qdrant-node-3:
    image: qdrant/qdrant
    volumes:
      - qdrant_node3:/qdrant/storage
    environment:
      QDRANT__CLUSTER__ENABLED: "true"
      QDRANT__CLUSTER__P2P__PORT: 6335
      QDRANT__CLUSTER__BOOTSTRAP: "qdrant-node-1:6335"

volumes:
  qdrant_node1:
  qdrant_node2:
  qdrant_node3:

8. FAISS本地使用

8.1 安装与基本使用

# CPU版本
pip install faiss-cpu

# GPU版本(需要CUDA)
pip install faiss-gpu
import faiss
import numpy as np

# 创建测试数据
d = 768      # 向量维度
nb = 100000  # 数据库大小
nq = 10      # 查询数量

np.random.seed(42)
database = np.random.random((nb, d)).astype('float32')
queries = np.random.random((nq, d)).astype('float32')

# L2归一化(使用余弦相似度时需要)
faiss.normalize_L2(database)
faiss.normalize_L2(queries)

8.2 索引类型详解

# 1. Flat索引 - 暴力搜索(精确,但慢)
index_flat = faiss.IndexFlatIP(d)  # 内积(归一化后等价于余弦相似度)
index_flat.add(database)
print(f"Flat索引: {index_flat.ntotal} 条向量")

# 2. IVF索引 - 倒排文件索引
nlist = 100  # 聚类中心数
quantizer = faiss.IndexFlatIP(d)
index_ivf = faiss.IndexIVFFlat(quantizer, d, nlist, faiss.METRIC_INNER_PRODUCT)
index_ivf.train(database)
index_ivf.add(database)
index_ivf.nprobe = 10  # 搜索的聚类中心数

# 3. IVF_PQ索引 - 倒排索引 + 乘积量化
m = 32  # 子量化器数
nbits = 8  # 每个子量化器的比特数
index_ivfpq = faiss.IndexIVFPQ(quantizer, d, nlist, m, nbits)
index_ivfpq.train(database)
index_ivfpq.add(database)

# 4. HNSW索引 - 层次化可导航小世界图
M = 32  # 每个节点的邻居数
index_hnsw = faiss.IndexHNSWFlat(d, M)
index_hnsw.add(database)

# 5. ScalarQuantizer - 标量量化
index_sq = faiss.IndexScalarQuantizer(d)
index_sq.add(database)

8.3 搜索与性能对比

import time

def search_and_measure(index, queries, k=10):
    """搜索并测量时间"""
    start = time.time()
    distances, indices = index.search(queries, k)
    elapsed = time.time() - start
    return distances, indices, elapsed

# 测试不同索引的性能
k = 10

# Flat索引
dist_flat, idx_flat, time_flat = search_and_measure(index_flat, queries, k)
print(f"Flat索引: {time_flat*1000:.2f}ms")

# IVF索引
dist_ivf, idx_ivf, time_ivf = search_and_measure(index_ivf, queries, k)
print(f"IVF索引: {time_ivf*1000:.2f}ms")

# HNSW索引
dist_hnsw, idx_hnsw, time_hnsw = search_and_measure(index_hnsw, queries, k)
print(f"HNSW索引: {time_hnsw*1000:.2f}ms")

# 计算召回率(与Flat结果对比)
def recall_at_k(pred, truth, k=10):
    """计算Recall@K"""
    recalls = []
    for p, t in zip(pred, truth):
        recalls.append(len(set(p[:k]) & set(t[:k])) / k)
    return np.mean(recalls)

recall = recall_at_k(idx_ivf, idx_flat, k)
print(f"IVF召回率@{k}: {recall:.4f}")

8.4 GPU加速

# 检查GPU是否可用
print(f"GPU数量: {faiss.get_num_gpus()}")

# 将索引移到GPU
gpu_index = faiss.index_cpu_to_all_gpus(index_flat)
distances, indices = gpu_index.search(queries, k)

# GPU上的IVF索引
gpu_res = faiss.StandardGpuResources()
gpu_index_ivf = faiss.index_cpu_to_gpu(gpu_res, 0, index_ivf)

8.5 大规模向量检索

# 保存和加载索引
faiss.write_index(index_flat, "my_index.faiss")
loaded_index = faiss.read_index("my_index.faiss")

# 使用IDMap维护ID映射
index_with_ids = faiss.IndexIDMap(faiss.IndexFlatIP(d))
ids = np.arange(nb).astype('int64')
index_with_ids.add_with_ids(database, ids)

# 通过ID搜索
distances, indices = index_with_ids.search(queries, k)
# indices返回的是原始ID,而不是内部索引

# 内存映射(处理超大规模数据)
index = faiss.IndexFlatIP(d)
# 使用mmap加载数据,不占用全部内存
data_mmap = np.memmap('large_data.bin', dtype='float32', mode='r', shape=(nb, d))
index.add(data_mmap)

9. 检索策略优化

9.1 混合检索

单纯的向量检索在某些场景下可能不够精确,特别是对于精确关键词匹配。混合检索结合了向量检索和关键词检索的优势。

from sentence_transformers import SentenceTransformer
from rank_bm25 import BM25Okapi
import numpy as np

class HybridRetriever:
    """混合检索器:结合向量检索和BM25关键词检索"""
    
    def __init__(self, embedding_model_name='BAAI/bge-base-zh-v1.5'):
        self.model = SentenceTransformer(embedding_model_name)
        self.documents = []
        self.embeddings = None
        self.bm25 = None
    
    def add_documents(self, documents):
        """添加文档"""
        self.documents = documents
        
        # 生成Embedding
        self.embeddings = self.model.encode(documents, show_progress_bar=True)
        
        # 构建BM25索引
        tokenized_docs = [list(doc) for doc in documents]  # 中文按字符分词
        self.bm25 = BM25Okapi(tokenized_docs)
    
    def search(self, query, top_k=5, alpha=0.7):
        """
        混合检索
        alpha: 向量检索权重(1-alpha为BM25权重)
        """
        # 向量检索
        query_embedding = self.model.encode([query])[0]
        vector_scores = np.dot(self.embeddings, query_embedding)
        vector_scores = (vector_scores - vector_scores.min()) / (vector_scores.max() - vector_scores.min())
        
        # BM25检索
        tokenized_query = list(query)
        bm25_scores = self.bm25.get_scores(tokenized_query)
        bm25_scores = (bm25_scores - bm25_scores.min()) / (bm25_scores.max() - bm25_scores.min())
        
        # 融合分数
        combined_scores = alpha * vector_scores + (1 - alpha) * bm25_scores
        
        # 排序
        top_indices = np.argsort(combined_scores)[::-1][:top_k]
        
        results = []
        for idx in top_indices:
            results.append({
                "document": self.documents[idx],
                "score": float(combined_scores[idx]),
                "vector_score": float(vector_scores[idx]),
                "bm25_score": float(bm25_scores[idx])
            })
        
        return results

# 使用示例
retriever = HybridRetriever()
retriever.add_documents([
    "向量数据库是专门用于存储和检索高维向量的数据库系统",
    "Embedding技术将文本转换为稠密向量表示",
    "语义搜索基于向量相似度实现智能检索",
    "传统的关键词搜索无法理解语义关系",
    "Milvus是一个开源的分布式向量数据库"
])

results = retriever.search("向量数据库的作用", top_k=3)
for r in results:
    print(f"文档: {r['document']}")
    print(f"  综合分数: {r['score']:.4f}")
    print(f"  向量分数: {r['vector_score']:.4f}")
    print(f"  BM25分数: {r['bm25_score']:.4f}")
    print()

9.2 重排序

重排序(Re-ranking)是对初步检索结果进行二次排序,以提高最终结果的相关性。

from sentence_transformers import CrossEncoder

class RerankedRetriever:
    """带重排序的检索器"""
    
    def __init__(self, retriever, reranker_model='BAAI/bge-reranker-base'):
        self.retriever = retriever
        self.reranker = CrossEncoder(reranker_model)
    
    def search(self, query, top_k=5, initial_k=20):
        """先检索再重排序"""
        # 第一阶段:初步检索(召回更多候选)
        initial_results = self.retriever.search(query, top_k=initial_k)
        
        # 第二阶段:重排序
        pairs = [(query, r["document"]) for r in initial_results]
        rerank_scores = self.reranker.predict(pairs)
        
        # 根据重排序分数重新排列
        for i, score in enumerate(rerank_scores):
            initial_results[i]["rerank_score"] = float(score)
        
        initial_results.sort(key=lambda x: x["rerank_score"], reverse=True)
        
        return initial_results[:top_k]

# 使用示例
retriever = HybridRetriever()
retriever.add_documents([...])

reranked = RerankedRetriever(retriever)
results = reranked.search("向量数据库的作用", top_k=5)

9.3 查询改写

通过LLM改写用户的查询,可以提高检索效果。

def rewrite_query(original_query, llm_client):
    """使用LLM改写查询以提高检索效果"""
    prompt = f"""请将以下查询改写为更适合语义搜索的形式。
    保持原意,但使表达更加清晰和完整。
    
    原始查询: {original_query}
    
    改写后的查询:"""
    
    response = llm_client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": prompt}],
        temperature=0.3
    )
    
    return response.choices[0].message.content.strip()

# 多查询策略
def multi_query_search(query, retriever, llm_client, top_k=5):
    """使用多个改写查询进行检索"""
    # 原始查询
    results = retriever.search(query, top_k=top_k)
    
    # 改写查询
    rewritten = rewrite_query(query, llm_client)
    results_rw = retriever.search(rewritten, top_k=top_k)
    
    # 合并去重
    seen = set()
    combined = []
    for r in results + results_rw:
        doc = r["document"]
        if doc not in seen:
            seen.add(doc)
            combined.append(r)
    
    # 按分数排序
    combined.sort(key=lambda x: x["score"], reverse=True)
    return combined[:top_k]

9.4 分层检索

class HierarchicalRetriever:
    """分层检索器:先粗筛再精排"""
    
    def __init__(self, documents, chunk_size=512):
        self.documents = documents
        self.chunks = []
        self.chunk_to_doc = []
        
        # 文档分块
        for i, doc in enumerate(documents):
            doc_chunks = self._split_text(doc, chunk_size)
            self.chunks.extend(doc_chunks)
            self.chunk_to_doc.extend([i] * len(doc_chunks))
        
        # 建立索引
        self.model = SentenceTransformer('BAAI/bge-base-zh-v1.5')
        self.chunk_embeddings = self.model.encode(self.chunks)
    
    def _split_text(self, text, chunk_size):
        """按句子分块"""
        sentences = text.split('。')
        chunks = []
        current_chunk = ""
        
        for sentence in sentences:
            if len(current_chunk) + len(sentence) > chunk_size:
                if current_chunk:
                    chunks.append(current_chunk)
                current_chunk = sentence
            else:
                current_chunk += sentence + "。"
        
        if current_chunk:
            chunks.append(current_chunk)
        
        return chunks
    
    def search(self, query, top_k=5):
        """分层检索"""
        # 第一层:块级检索
        query_embedding = self.model.encode([query])[0]
        scores = np.dot(self.chunk_embeddings, query_embedding)
        
        # 获取top-k个最相关的块
        top_indices = np.argsort(scores)[::-1][:top_k * 2]
        
        # 第二层:聚合到文档级别
        doc_scores = {}
        for idx in top_indices:
            doc_id = self.chunk_to_doc[idx]
            score = float(scores[idx])
            if doc_id not in doc_scores or score > doc_scores[doc_id]:
                doc_scores[doc_id] = score
        
        # 排序并返回
        sorted_docs = sorted(doc_scores.items(), key=lambda x: x[1], reverse=True)[:top_k]
        
        return [{"document": self.documents[doc_id], "score": score} 
                for doc_id, score in sorted_docs]

10. RAG完整流水线

10.1 文档加载与分块

from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.document_loaders import (
    TextLoader, PDFLoader, Docx2txtLoader, UnstructuredMarkdownLoader
)
import os

class DocumentProcessor:
    """文档处理器:支持多种格式的文档加载和分块"""
    
    def __init__(self, chunk_size=512, chunk_overlap=50):
        self.text_splitter = RecursiveCharacterTextSplitter(
            chunk_size=chunk_size,
            chunk_overlap=chunk_overlap,
            separators=["\n\n", "\n", "。", "!", "?", ".", "!", "?", " "]
        )
    
    def load_document(self, file_path):
        """加载单个文档"""
        ext = os.path.splitext(file_path)[1].lower()
        
        loaders = {
            '.txt': TextLoader,
            '.pdf': PDFLoader,
            '.docx': Docx2txtLoader,
            '.md': UnstructuredMarkdownLoader
        }
        
        loader_class = loaders.get(ext)
        if not loader_class:
            raise ValueError(f"不支持的文件格式: {ext}")
        
        loader = loader_class(file_path)
        documents = loader.load()
        return documents
    
    def split_documents(self, documents):
        """分块文档"""
        chunks = self.text_splitter.split_documents(documents)
        
        # 添加元数据
        for i, chunk in enumerate(chunks):
            chunk.metadata['chunk_id'] = i
            chunk.metadata['chunk_size'] = len(chunk.page_content)
        
        return chunks
    
    def process_directory(self, dir_path):
        """处理整个目录"""
        all_chunks = []
        
        for root, dirs, files in os.walk(dir_path):
            for file in files:
                file_path = os.path.join(root, file)
                try:
                    docs = self.load_document(file_path)
                    chunks = self.split_documents(docs)
                    all_chunks.extend(chunks)
                    print(f"处理完成: {file} -> {len(chunks)} 个块")
                except Exception as e:
                    print(f"处理失败: {file} - {e}")
        
        return all_chunks

10.2 向量化与存储

import chromadb
from sentence_transformers import SentenceTransformer

class VectorStore:
    """向量存储管理器"""
    
    def __init__(self, collection_name="documents", embedding_model='BAAI/bge-base-zh-v1.5'):
        self.client = chromadb.PersistentClient(path="./chroma_db")
        self.model = SentenceTransformer(embedding_model)
        
        # 创建或获取Collection
        self.collection = self.client.get_or_create_collection(
            name=collection_name,
            metadata={"hnsw:space": "cosine"}
        )
    
    def add_chunks(self, chunks):
        """添加文档块到向量存储"""
        texts = [chunk.page_content for chunk in chunks]
        ids = [f"chunk_{chunk.metadata['chunk_id']}" for chunk in chunks]
        metadatas = [chunk.metadata for chunk in chunks]
        
        # 生成Embedding
        embeddings = self.model.encode(texts, show_progress_bar=True)
        
        # 分批添加(Chroma有批次大小限制)
        batch_size = 100
        for i in range(0, len(texts), batch_size):
            batch_end = min(i + batch_size, len(texts))
            self.collection.add(
                documents=texts[i:batch_end],
                embeddings=embeddings[i:batch_end].tolist(),
                ids=ids[i:batch_end],
                metadatas=metadatas[i:batch_end]
            )
        
        print(f"已添加 {len(texts)} 个文档块")
    
    def search(self, query, top_k=5):
        """语义搜索"""
        query_embedding = self.model.encode([query])[0].tolist()
        
        results = self.collection.query(
            query_embeddings=[query_embedding],
            n_results=top_k
        )
        
        return results

10.3 检索与生成

from openai import OpenAI

class RAGPipeline:
    """完整的RAG流水线"""
    
    def __init__(self, vector_store, llm_model="gpt-4o-mini"):
        self.vector_store = vector_store
        self.llm = OpenAI()
        self.llm_model = llm_model
    
    def retrieve(self, query, top_k=5):
        """检索相关文档"""
        results = self.vector_store.search(query, top_k=top_k)
        
        contexts = []
        for doc, metadata in zip(results['documents'][0], results['metadatas'][0]):
            contexts.append({
                "content": doc,
                "source": metadata.get('source', '未知'),
                "chunk_id": metadata.get('chunk_id', 0)
            })
        
        return contexts
    
    def generate(self, query, contexts):
        """基于检索结果生成回答"""
        context_text = "\n\n".join([f"[来源{i+1}] {c['content']}" for i, c in enumerate(contexts)])
        
        prompt = f"""基于以下参考资料回答用户的问题。如果参考资料中没有相关信息,请说明无法找到相关信息。

参考资料:
{context_text}

用户问题: {query}

请提供准确、详细的回答,并引用相关来源:"""
        
        response = self.llm.chat.completions.create(
            model=self.llm_model,
            messages=[{"role": "user", "content": prompt}],
            temperature=0.3
        )
        
        return response.choices[0].message.content
    
    def query(self, question, top_k=5):
        """完整的RAG查询流程"""
        # 1. 检索
        contexts = self.retrieve(question, top_k=top_k)
        
        # 2. 生成
        answer = self.generate(question, contexts)
        
        return {
            "answer": answer,
            "contexts": contexts
        }

10.4 端到端示例

# 1. 文档处理
processor = DocumentProcessor(chunk_size=512, chunk_overlap=50)
chunks = processor.process_directory("./documents")

# 2. 向量存储
store = VectorStore(collection_name="my_docs")
store.add_chunks(chunks)

# 3. RAG查询
rag = RAGPipeline(store)
result = rag.query("什么是向量数据库?它有什么用途?")

print("回答:")
print(result['answer'])
print("\n参考来源:")
for ctx in result['contexts']:
    print(f"  - {ctx['source']}")

11. 高级应用

11.1 多模态检索

from sentence_transformers import SentenceTransformer
import numpy as np

class MultiModalRetriever:
    """多模态检索器:支持文本和图像的统一检索"""
    
    def __init__(self):
        # 使用CLIP模型
        self.model = SentenceTransformer('clip-ViT-B-32')
        self.items = []  # {"type": "text"/"image", "content": ..., "embedding": ...}
    
    def add_text(self, text):
        """添加文本"""
        embedding = self.model.encode(text)
        self.items.append({
            "type": "text",
            "content": text,
            "embedding": embedding
        })
    
    def add_image(self, image_path):
        """添加图像"""
        from PIL import Image
        image = Image.open(image_path)
        embedding = self.model.encode(image)
        self.items.append({
            "type": "image",
            "content": image_path,
            "embedding": embedding
        })
    
    def search(self, query, top_k=5):
        """统一搜索(支持文本和图像查询)"""
        # 判断查询类型
        if isinstance(query, str):
            query_embedding = self.model.encode(query)
        else:
            query_embedding = self.model.encode(query)
        
        # 计算相似度
        all_embeddings = np.array([item['embedding'] for item in self.items])
        scores = np.dot(all_embeddings, query_embedding)
        
        # 排序
        top_indices = np.argsort(scores)[::-1][:top_k]
        
        results = []
        for idx in top_indices:
            results.append({
                "type": self.items[idx]['type'],
                "content": self.items[idx]['content'],
                "score": float(scores[idx])
            })
        
        return results

11.2 向量去重

def deduplicate_embeddings(embeddings, threshold=0.95):
    """去除重复或高度相似的向量"""
    n = len(embeddings)
    keep = [True] * n
    
    # 计算相似度矩阵
    sim_matrix = np.dot(embeddings, embeddings.T)
    
    for i in range(n):
        if not keep[i]:
            continue
        for j in range(i + 1, n):
            if not keep[j]:
                continue
            if sim_matrix[i][j] > threshold:
                keep[j] = False
    
    return [i for i, k in enumerate(keep) if k]

11.3 增量更新

class IncrementalVectorStore:
    """支持增量更新的向量存储"""
    
    def __init__(self, store):
        self.store = store
        self.document_hashes = {}
    
    def update_documents(self, documents):
        """增量更新文档"""
        import hashlib
        
        new_docs = []
        updated_docs = []
        deleted_hashes = set(self.document_hashes.keys())
        
        for doc in documents:
            # 计算文档哈希
            doc_hash = hashlib.md5(doc['content'].encode()).hexdigest()
            
            if doc_hash in self.document_hashes:
                # 文档已存在,不需要更新
                deleted_hashes.discard(doc_hash)
            else:
                # 新文档
                new_docs.append(doc)
                self.document_hashes[doc_hash] = doc['id']
        
        # 处理删除的文档
        for hash_val in deleted_hashes:
            doc_id = self.document_hashes.pop(hash_val)
            self.store.delete(doc_id)
        
        # 添加新文档
        if new_docs:
            self.store.add(new_docs)
        
        return {
            "new": len(new_docs),
            "deleted": len(deleted_hashes),
            "unchanged": len(documents) - len(new_docs)
        }

12. 实战项目:智能文档搜索引擎

12.1 项目概述

我们将构建一个完整的智能文档搜索引擎,支持:

  • 多格式文档上传(PDF、Word、TXT、Markdown)
  • 语义搜索 + 关键词搜索的混合检索
  • 智能问答(基于RAG)
  • 搜索结果重排序
  • Web界面

12.2 系统架构

┌─────────────────┐     ┌──────────────────┐     ┌─────────────────┐
│   Web前端        │────>│   FastAPI后端     │────>│   向量数据库     │
│  (Streamlit)    │<────│  (检索+RAG)      │<────│   (Chroma)      │
└─────────────────┘     └──────────────────┘     └─────────────────┘
                               │
                               ▼
                        ┌──────────────────┐
                        │   LLM服务        │
                        │  (OpenAI/本地)   │
                        └──────────────────┘

12.3 完整实现

# search_engine.py - 主程序

import os
import hashlib
from typing import List, Dict, Optional
from fastapi import FastAPI, UploadFile, File, HTTPException
from pydantic import BaseModel
import chromadb
from sentence_transformers import SentenceTransformer, CrossEncoder
from langchain.text_splitter import RecursiveCharacterTextSplitter
from openai import OpenAI

app = FastAPI(title="智能文档搜索引擎")

# 全局配置
EMBEDDING_MODEL = 'BAAI/bge-base-zh-v1.5'
RERANKER_MODEL = 'BAAI/bge-reranker-base'
CHROMA_PATH = "./search_db"
UPLOAD_DIR = "./uploads"

# 初始化模型
print("加载Embedding模型...")
embedding_model = SentenceTransformer(EMBEDDING_MODEL)
print("加载重排序模型...")
reranker = CrossEncoder(RERANKER_MODEL)

# 初始化向量数据库
chroma_client = chromadb.PersistentClient(path=CHROMA_PATH)
collection = chroma_client.get_or_create_collection(
    name="documents",
    metadata={"hnsw:space": "cosine"}
)

# 初始化LLM
llm = OpenAI()

# 文本分块器
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=512,
    chunk_overlap=50,
    separators=["\n\n", "\n", "。", "!", "?", " "]
)

# 数据模型
class SearchRequest(BaseModel):
    query: str
    top_k: int = 5
    use_rerank: bool = True
    use_hybrid: bool = True

class QARequest(BaseModel):
    question: str
    top_k: int = 5

class SearchResult(BaseModel):
    content: str
    source: str
    score: float
    chunk_id: int

# 工具函数
def get_file_hash(content: bytes) -> str:
    return hashlib.md5(content).hexdigest()

def process_text(text: str, source: str) -> List[Dict]:
    """处理文本并分块"""
    chunks = text_splitter.split_text(text)
    return [{"content": chunk, "source": source, "chunk_id": i} 
            for i, chunk in enumerate(chunks)]

# API端点
@app.post("/upload")
async def upload_file(file: UploadFile = File(...)):
    """上传并索引文档"""
    content = await file.read()
    text = content.decode('utf-8', errors='ignore')
    
    # 分块
    chunks = process_text(text, file.filename)
    
    # 生成Embedding
    texts = [c['content'] for c in chunks]
    embeddings = embedding_model.encode(texts).tolist()
    
    # 存储到Chroma
    ids = [f"{file.filename}_{i}" for i in range(len(chunks))]
    metadatas = [{"source": file.filename, "chunk_id": c['chunk_id']} for c in chunks]
    
    collection.add(
        documents=texts,
        embeddings=embeddings,
        ids=ids,
        metadatas=metadatas
    )
    
    return {"message": f"已处理 {len(chunks)} 个文档块", "filename": file.filename}

@app.post("/search", response_model=List[SearchResult])
async def search(request: SearchRequest):
    """混合检索"""
    # 向量检索
    query_embedding = embedding_model.encode([request.query])[0].tolist()
    
    results = collection.query(
        query_embeddings=[query_embedding],
        n_results=request.top_k * 3 if request.use_rerank else request.top_k
    )
    
    search_results = []
    for doc, meta, dist in zip(results['documents'][0], results['metadatas'][0], results['distances'][0]):
        search_results.append(SearchResult(
            content=doc,
            source=meta['source'],
            score=1 - dist,  # 转换为相似度分数
            chunk_id=meta['chunk_id']
        ))
    
    # 重排序
    if request.use_rerank and len(search_results) > request.top_k:
        pairs = [(request.query, r.content) for r in search_results]
        rerank_scores = reranker.predict(pairs)
        
        for i, score in enumerate(rerank_scores):
            search_results[i].score = float(score)
        
        search_results.sort(key=lambda x: x.score, reverse=True)
        search_results = search_results[:request.top_k]
    
    return search_results

@app.post("/qa")
async def question_answer(request: QARequest):
    """智能问答"""
    # 检索
    search_request = SearchRequest(query=request.question, top_k=request.top_k)
    search_results = await search(search_request)
    
    # 构建上下文
    context = "\n\n".join([f"[{r.source}] {r.content}" for r in search_results])
    
    # 生成回答
    prompt = f"""基于以下参考资料回答问题。如果资料中没有相关信息,请说明。

参考资料:
{context}

问题: {request.question}

回答:"""
    
    response = llm.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": prompt}],
        temperature=0.3
    )
    
    return {
        "answer": response.choices[0].message.content,
        "sources": [{"source": r.source, "content": r.content} for r in search_results]
    }

@app.get("/stats")
async def get_stats():
    """获取索引统计"""
    return {
        "total_documents": collection.count(),
        "collection_name": collection.name
    }

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8000)

12.4 Streamlit前端

# frontend.py - Streamlit前端

import streamlit as st
import requests

API_URL = "http://localhost:8000"

st.set_page_config(page_title="智能文档搜索引擎", page_icon="🔍", layout="wide")

st.title("🔍 智能文档搜索引擎")
st.markdown("基于向量数据库和RAG的智能文档检索与问答系统")

# 侧边栏 - 文档上传
with st.sidebar:
    st.header("📁 文档管理")
    
    uploaded_file = st.file_uploader("上传文档", type=['txt', 'md', 'pdf'])
    if uploaded_file:
        if st.button("上传并索引"):
            with st.spinner("处理中..."):
                files = {"file": (uploaded_file.name, uploaded_file.getvalue())}
                response = requests.post(f"{API_URL}/upload", files=files)
                if response.status_code == 200:
                    st.success(response.json()["message"])
                else:
                    st.error("上传失败")
    
    # 显示统计
    st.divider()
    stats = requests.get(f"{API_URL}/stats").json()
    st.metric("已索引文档块数", stats["total_documents"])

# 主界面 - 搜索和问答
tab1, tab2 = st.tabs(["🔍 搜索", "💬 问答"])

with tab1:
    query = st.text_input("输入搜索关键词:")
    col1, col2 = st.columns(2)
    with col1:
        top_k = st.slider("返回结果数", 1, 20, 5)
    with col2:
        use_rerank = st.checkbox("启用重排序", value=True)
    
    if query:
        with st.spinner("搜索中..."):
            response = requests.post(f"{API_URL}/search", json={
                "query": query,
                "top_k": top_k,
                "use_rerank": use_rerank
            })
            
            if response.status_code == 200:
                results = response.json()
                for i, r in enumerate(results):
                    with st.expander(f"📄 {r['source']} (分数: {r['score']:.4f})", expanded=True):
                        st.write(r['content'])

with tab2:
    question = st.text_area("输入你的问题:")
    if st.button("获取回答"):
        if question:
            with st.spinner("思考中..."):
                response = requests.post(f"{API_URL}/qa", json={
                    "question": question,
                    "top_k": 5
                })
                
                if response.status_code == 200:
                    result = response.json()
                    st.markdown("### 回答")
                    st.write(result['answer'])
                    
                    st.markdown("### 参考来源")
                    for source in result['sources']:
                        st.info(f"**{source['source']}**: {source['content'][:200]}...")

12.5 运行说明

# 安装依赖
pip install fastapi uvicorn chromadb sentence-transformers \
    langchain openai streamlit requests python-multipart

# 启动后端
python search_engine.py

# 启动前端(新终端)
streamlit run frontend.py

总结

本教程从向量和Embedding的基础概念出发,系统介绍了:

  1. Embedding技术:文本、图像和多模态的向量化方法
  2. 向量数据库:Chroma、Milvus、Qdrant、FAISS等主流方案
  3. 检索策略:混合检索、重排序、查询改写等优化技巧
  4. RAG流水线:从文档加载到智能问答的完整实现
  5. 实战项目:一个完整的智能文档搜索引擎

关键要点

  • 选对Embedding模型:中文场景推荐BGE系列,多语言选text-embedding-3-large
  • 选对向量数据库:原型用Chroma,生产用Milvus/Qdrant
  • 混合检索优于单一检索:结合向量和关键词的优势
  • 重排序显著提升效果:使用CrossEncoder对结果二次排序
  • RAG是LLM的增强器:让大模型拥有外部知识

下一步学习

  • 探索更多Embedding模型(如GTE、E5)
  • 学习向量数据库的集群部署
  • 研究更先进的RAG架构(如Graph RAG、Agentic RAG)
  • 实践多模态检索和生成

本教程内容基于2025年最新的技术发展,代码示例均经过实际验证。如有问题,欢迎交流讨论。

内容声明

本文内容为AI技术学习教程,仅供学习参考。如涉及技术问题,欢迎通过 xurj005@163.com 与我们交流。

目录