AI搜索引擎与语义检索完全教程

教程简介

本教程系统讲解AI语义检索的核心技术,涵盖Embedding原理、向量数据库选型、混合检索策略、ColBERT/BGE/E5等模型详解、查询改写与扩展、多模态检索、Reranker重排序、搜索质量评估指标,并通过完整实战案例帮助开发者构建自己的AI搜索引擎。

AI搜索引擎与语义检索完全教程

一、概述

在信息爆炸的时代,搜索引擎是连接用户与知识的核心桥梁。传统搜索引擎依赖关键词匹配,用户必须精确描述需求才能找到目标内容。然而,人类的思维方式本质上是语义化的——我们理解概念、上下文和意图,而非简单的词汇组合。

AI搜索引擎的核心突破在于语义理解:系统不再逐字匹配查询词与文档,而是理解查询的真实意图,在语义空间中找到最相关的答案。这一转变使得搜索从"字面匹配"进化为"意义理解",从根本上改变了信息检索的范式。

本教程将从基础原理出发,系统讲解语义检索的核心技术、主流工具、工程实践,并通过完整的实战案例帮助开发者构建自己的AI搜索引擎。

目标读者:有Python基础和一定机器学习知识的开发者,希望理解并构建现代语义检索系统。

你将学到

  • 语义检索的底层原理与数学基础
  • 主流向量数据库的选型与使用
  • 混合检索策略的设计与实现
  • 查询改写与扩展技术
  • 多模态检索的工程实现
  • 搜索质量评估方法
  • 从零构建一个完整的AI搜索引擎

二、传统搜索 vs AI搜索

2.1 传统关键词搜索的局限

传统搜索引擎(如Elasticsearch的BM25算法)基于词频-逆文档频率(TF-IDF) 和概率检索模型。其核心逻辑是:

相关性 = f(查询词在文档中出现的频率, 查询词在整个语料库中的稀有程度)

这种方式存在几个根本性问题:

词汇不匹配问题(Vocabulary Mismatch):用户搜索"如何让代码跑得更快",但文档中写的是"程序性能优化"。两者语义相同,但关键词完全不同,传统搜索无法关联。

无法理解同义词和上下文:"苹果"在不同语境下可能指水果或公司,传统搜索难以区分。

长查询理解困难:当用户输入一段描述性问题时,传统搜索倾向于将其拆解为独立关键词,丢失了整体语义。

跨语言障碍:中文搜索"机器学习入门"无法直接匹配英文文档"Introduction to Machine Learning"。

2.2 AI语义搜索的优势

AI搜索引擎通过深度学习模型将文本编码为稠密向量(Dense Vector),在高维语义空间中进行相似度计算:

# 传统搜索:基于词频匹配
from collections import Counter
import math

def bm25_score(query, document, k1=1.5, b=0.75):
    """简化的BM25评分"""
    query_terms = query.lower().split()
    doc_terms = document.lower().split()
    doc_len = len(doc_terms)
    avg_doc_len = 100  # 假设平均文档长度

    score = 0
    for term in query_terms:
        tf = doc_terms.count(term)
        if tf > 0:
            idf = math.log((1000 - tf + 0.5) / (tf + 0.5) + 1)
            tf_norm = (tf * (k1 + 1)) / (tf + k1 * (1 - b + b * doc_len / avg_doc_len))
            score += idf * tf_norm
    return score

# AI搜索:基于语义相似度
import numpy as np

def cosine_similarity(vec_a, vec_b):
    """余弦相似度计算"""
    dot_product = np.dot(vec_a, vec_b)
    norm_a = np.linalg.norm(vec_a)
    norm_b = np.linalg.norm(vec_b)
    return dot_product / (norm_a * norm_b)

def semantic_search(query_embedding, doc_embeddings):
    """语义检索"""
    similarities = [cosine_similarity(query_embedding, doc_emb) 
                    for doc_emb in doc_embeddings]
    return sorted(enumerate(similarities), key=lambda x: x[1], reverse=True)

2.3 核心差异对比

维度 传统搜索 AI语义搜索
匹配方式 关键词/词频 语义向量相似度
同义词处理 需要人工词典 自动学习语义关联
上下文理解 有限 强大的上下文编码
跨语言 需要翻译 多语言模型原生支持
查询类型 关键词查询 自然语言查询
计算资源 CPU即可 通常需要GPU
可解释性 高(词频统计) 较低(黑盒向量)
精确匹配 较弱(需要补充)

关键洞察:实际生产系统中,最优方案通常是混合检索——结合传统搜索的精确匹配能力和语义搜索的理解能力。


三、语义检索核心原理

3.1 文本嵌入(Text Embedding)

文本嵌入是将自然语言文本映射到高维连续向量空间的过程。在这个空间中,语义相近的文本对应的向量距离更近。

Embedding模型的工作原理

现代Embedding模型通常基于Transformer架构,通过大规模语料预训练学习文本的语义表示:

# 使用sentence-transformers进行文本嵌入
from sentence_transformers import SentenceTransformer

# 加载预训练模型
model = SentenceTransformer('BAAI/bge-base-zh-v1.5')  # 中文模型

# 文本编码
texts = [
    "机器学习是人工智能的一个子领域",
    "深度学习是机器学习的一种方法",
    "今天天气很好,适合出去玩"
]

embeddings = model.encode(texts)
print(f"向量维度: {embeddings.shape}")  # 例如 (3, 768)

# 计算相似度
from sentence_transformers.util import cos_sim
similarities = cos_sim(embeddings, embeddings)
print(similarities)

3.2 向量空间与距离度量

在高维向量空间中,衡量两个向量的"距离"或"相似度"有多种方法:

余弦相似度(Cosine Similarity):衡量两个向量的方向一致性,值域[-1, 1],是最常用的方法。

import numpy as np

def cosine_similarity(a, b):
    return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))

欧氏距离(Euclidean Distance):衡量两个向量在空间中的绝对距离。

def euclidean_distance(a, b):
    return np.sqrt(np.sum((a - b) ** 2))

内积(Inner Product / Dot Product):当向量已归一化时,等价于余弦相似度。

def dot_product(a, b):
    return np.dot(a, b)

选择建议

  • 大多数Embedding模型推荐使用余弦相似度
  • 如果向量已L2归一化,内积与余弦相似度等价
  • 某些特定场景(如推荐系统)可能更适合内积

3.3 近似最近邻搜索(ANN)

精确的最近邻搜索在大规模数据集上计算成本过高(O(n)复杂度),因此实际系统使用近似最近邻(ANN)算法:

HNSW(Hierarchical Navigable Small World):基于图的索引结构,构建多层导航图,查询时从顶层开始逐层下降定位最近邻。查询速度快,内存占用较高。

IVF(Inverted File Index):先用聚类将向量空间划分为多个区域,查询时只在最近的几个区域中搜索。适合大规模数据,需要训练阶段。

PQ(Product Quantization):将高维向量压缩为低维编码,大幅减少内存占用,但会损失一定精度。

# 使用faiss进行ANN搜索
import faiss
import numpy as np

# 创建示例数据
d = 768  # 向量维度
nb = 100000  # 数据库向量数量
nq = 10  # 查询数量

np.random.seed(42)
database_vectors = np.random.random((nb, d)).astype('float32')
query_vectors = np.random.random((nq, d)).astype('float32')

# L2归一化(余弦相似度等价于归一化后的内积)
faiss.normalize_L2(database_vectors)
faiss.normalize_L2(query_vectors)

# 方法1: HNSW索引
index_hnsw = faiss.IndexHNSWFlat(d, 32)  # 32为图的连接数
index_hnsw.add(database_vectors)
distances, indices = index_hnsw.search(query_vectors, k=10)

# 方法2: IVF索引
nlist = 100  # 聚类数量
quantizer = faiss.IndexFlatIP(d)  # 使用内积
index_ivf = faiss.IndexIVFFlat(quantizer, d, nlist, faiss.METRIC_INNER_PRODUCT)
index_ivf.train(database_vectors)
index_ivf.add(database_vectors)
index_ivf.nprobe = 10  # 查询时搜索的聚类数量
distances, indices = index_ivf.search(query_vectors, k=10)

print(f"Top-10最近邻索引: {indices[0]}")
print(f"对应距离: {distances[0]}")

四、主流向量数据库对比与选型

4.1 向量数据库核心能力

一个成熟的向量数据库需要具备以下能力:

  • 高效的向量索引与检索:支持多种ANN算法
  • 元数据过滤:在向量搜索的同时进行结构化过滤
  • 持久化存储:数据持久化与故障恢复
  • 水平扩展:支持分布式部署
  • 混合搜索:同时支持向量搜索和关键词搜索

4.2 主流方案对比

ChromaDB

轻量级嵌入式向量数据库,适合原型开发和小规模应用。

import chromadb

# 创建客户端(持久化存储)
client = chromadb.PersistentClient(path="./chroma_db")

# 创建或获取集合
collection = client.get_or_create_collection(
    name="documents",
    metadata={"hnsw:space": "cosine"}
)

# 添加文档
collection.add(
    documents=["机器学习入门指南", "深度学习实战教程", "自然语言处理基础"],
    metadatas=[
        {"category": "ML", "level": "beginner"},
        {"category": "DL", "level": "intermediate"},
        {"category": "NLP", "level": "beginner"}
    ],
    ids=["doc1", "doc2", "doc3"]
)

# 查询
results = collection.query(
    query_texts=["如何开始学习AI"],
    n_results=2,
    where={"level": "beginner"}
)
print(results)

优点:API简洁、嵌入式部署、自动Embedding 缺点:不适合大规模数据、单机部署、性能有限

Milvus

开源分布式向量数据库,适合生产环境大规模部署。

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="title", dtype=DataType.VARCHAR, max_length=512),
    FieldSchema(name="embedding", dtype=DataType.FLOAT_VECTOR, dim=768),
    FieldSchema(name="category", dtype=DataType.VARCHAR, max_length=64)
]
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)

# 插入数据
import numpy as np
data = [
    ["机器学习入门", "深度学习实战", "NLP基础"],
    np.random.random((3, 768)).tolist(),
    ["ML", "DL", "NLP"]
]
collection.insert(data)

# 搜索
collection.load()
search_params = {"metric_type": "COSINE", "params": {"ef": 100}}
results = collection.search(
    data=[np.random.random(768).tolist()],
    anns_field="embedding",
    param=search_params,
    limit=10,
    expr='category == "ML"'
)

优点:分布式架构、高性能、丰富的索引类型 缺点:部署复杂、资源消耗大

Weaviate

内置向量化模块的向量数据库,支持自动Embedding和混合搜索。

import weaviate

# 连接Weaviate
client = weaviate.Client("http://localhost:8080")

# 创建类(Schema)
class_obj = {
    "class": "Document",
    "vectorizer": "text2vec-transformers",
    "properties": [
        {"name": "title", "dataType": ["text"]},
        {"name": "content", "dataType": ["text"]},
        {"name": "category", "dataType": ["text"]}
    ]
}
client.schema.create_class(class_obj)

# 插入数据
data_obj = {
    "title": "机器学习入门",
    "content": "机器学习是人工智能的核心分支...",
    "category": "ML"
}
client.data_object.create(data_obj, "Document")

# 混合搜索(向量 + BM25)
result = (
    client.query
    .get("Document", ["title", "content"])
    .with_hybrid(
        query="如何学习AI",
        alpha=0.75  # 0=纯BM25, 1=纯向量
    )
    .with_limit(10)
    .do()
)

优点:内置向量化、混合搜索、GraphQL API 缺点:内存消耗大、社区相对较小

Qdrant

Rust编写的高性能向量数据库,支持丰富的过滤和有效载荷。

from qdrant_client import QdrantClient
from qdrant_client.models import Distance, VectorParams, PointStruct, Filter, FieldCondition, MatchValue

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

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

# 插入数据
import numpy as np
points = [
    PointStruct(
        id=1,
        vector=np.random.random(768).tolist(),
        payload={"title": "机器学习入门", "category": "ML"}
    ),
    PointStruct(
        id=2,
        vector=np.random.random(768).tolist(),
        payload={"title": "深度学习实战", "category": "DL"}
    )
]
client.upsert(collection_name="documents", points=points)

# 搜索
results = client.search(
    collection_name="documents",
    query_vector=np.random.random(768).tolist(),
    query_filter=Filter(
        must=[FieldCondition(key="category", match=MatchValue(value="ML"))]
    ),
    limit=10
)

4.3 选型建议

场景 推荐方案
原型开发/小数据量(<100K) ChromaDB
中等规模生产环境(100K-10M) Qdrant / Weaviate
大规模分布式(>10M) Milvus / Pinecone
需要混合搜索 Weaviate / Elasticsearch + 向量插件
云原生/Serverless Pinecone / Zilliz Cloud

五、混合检索策略

5.1 为什么需要混合检索

纯语义检索虽然能理解查询意图,但在以下场景中表现不如传统搜索:

  • 精确匹配:搜索特定ID、代码、型号时
  • 稀有词汇:专业术语、新造词
  • 否定查询:搜索"不含XX的内容"

混合检索通过结合稀疏检索(BM25)和稠密检索(向量),取长补短。

5.2 稀疏+稠密融合策略

import numpy as np
from typing import List, Dict, Tuple

class HybridSearchEngine:
    def __init__(self, dense_model, sparse_index, alpha=0.5):
        """
        alpha: 稠密检索权重(0=纯稀疏, 1=纯稠密)
        """
        self.dense_model = dense_model
        self.sparse_index = sparse_index
        self.alpha = alpha
    
    def search(self, query: str, top_k: int = 10) -> List[Dict]:
        # 稠密检索(语义)
        query_embedding = self.dense_model.encode(query)
        dense_results = self.dense_search(query_embedding, top_k * 2)
        
        # 稀疏检索(BM25)
        sparse_results = self.sparse_search(query, top_k * 2)
        
        # 归一化分数
        dense_scores = self.normalize_scores(dense_results)
        sparse_scores = self.normalize_scores(sparse_results)
        
        # 融合(RRF: Reciprocal Rank Fusion)
        fused_scores = self.reciprocal_rank_fusion(
            dense_results, sparse_results, k=60
        )
        
        # 排序并返回Top-K
        sorted_results = sorted(
            fused_scores.items(), 
            key=lambda x: x[1], 
            reverse=True
        )[:top_k]
        
        return sorted_results
    
    def reciprocal_rank_fusion(
        self, 
        *result_lists, 
        k=60
    ) -> Dict[str, float]:
        """RRF融合算法"""
        fused_scores = {}
        for results in result_lists:
            for rank, (doc_id, score) in enumerate(results, 1):
                if doc_id not in fused_scores:
                    fused_scores[doc_id] = 0
                fused_scores[doc_id] += 1.0 / (k + rank)
        return fused_scores
    
    def normalize_scores(self, results):
        """Min-Max归一化"""
        if not results:
            return {}
        scores = [s for _, s in results]
        min_s, max_s = min(scores), max(scores)
        if max_s == min_s:
            return {doc_id: 1.0 for doc_id, _ in results}
        return {
            doc_id: (s - min_s) / (max_s - min_s) 
            for doc_id, s in results
        }

5.3 RRF(Reciprocal Rank Fusion)详解

RRF是一种无需分数归一化的融合方法,仅依赖排名:

RRF_score(d) = Σ 1 / (k + rank_i(d))

其中k是平滑常数(通常取60),rank_i(d)是文档d在第i个检索器中的排名。

优点

  • 不需要分数归一化
  • 对异常分数鲁棒
  • 简单高效

适用场景

  • 多个检索器的分数尺度差异大
  • 需要快速集成的场景
  • 对精度要求不是极致的场景

六、主流检索模型详解

6.1 BGE(BAAI General Embedding)

由智源研究院开发的中文Embedding模型系列,在中文检索任务上表现优异。

from sentence_transformers import SentenceTransformer
import numpy as np

# 加载BGE模型
model = SentenceTransformer('BAAI/bge-large-zh-v1.5')

# BGE模型推荐在查询前添加指令前缀
query_instruction = "为这个句子生成表示以用于检索相关文章:"
query = "机器学习的基本原理是什么"
query_with_instruction = query_instruction + query

# 编码
query_embedding = model.encode(query_with_instruction, normalize_embeddings=True)

# 文档编码(不需要指令前缀)
documents = [
    "机器学习是人工智能的一个分支,它使计算机能够从数据中学习。",
    "深度学习使用多层神经网络来模拟人脑的学习过程。",
    "今天北京的天气非常晴朗。"
]
doc_embeddings = model.encode(documents, normalize_embeddings=True)

# 计算相似度
similarities = np.dot(doc_embeddings, query_embedding)
for doc, sim in zip(documents, similarities):
    print(f"相似度: {sim:.4f} | {doc[:30]}...")

6.2 E5(EmbEdding from bidirEctional Encoder representations)

微软发布的Embedding模型,使用对比学习训练。

from sentence_transformers import SentenceTransformer

model = SentenceTransformer('intfloat/multilingual-e5-large')

# E5模型使用前缀区分查询和文档
query = "query: 什么是机器学习"
docs = [
    "passage: 机器学习是AI的子领域,使计算机能够从数据中自动学习模式。",
    "passage: 今天天气很好,适合户外活动。"
]

query_emb = model.encode(query, normalize_embeddings=True)
doc_embs = model.encode(docs, normalize_embeddings=True)

similarities = np.dot(doc_embs, query_emb)
print(similarities)

6.3 ColBERT(Contextualized Late Interaction over BERT)

ColBERT采用"晚期交互"机制,保留了Token级别的细粒度匹配能力:

# ColBERT的核心思想示意
class ColBERTScorer:
    """ColBERT评分机制的简化实现"""
    
    def __init__(self, model):
        self.model = model
    
    def encode_tokens(self, text):
        """获取每个Token的上下文表示"""
        # 实际使用ColBERT库
        # from colbert import Indexer, Searcher
        tokens = self.model.tokenize(text)
        embeddings = self.model.encode(tokens)  # shape: (num_tokens, dim)
        return embeddings
    
    def late_interaction_score(self, query_tokens, doc_tokens):
        """MaxSim操作:对每个查询Token,找到文档中最相似的Token"""
        score = 0
        for q_vec in query_tokens:
            # 计算查询token与所有文档token的相似度
            sim_scores = np.dot(doc_tokens, q_vec)
            # 取最大值(MaxSim)
            score += np.max(sim_scores)
        return score

# 使用PyLate库(ColBERT的简化封装)
# pip install pylate
from pylate.models import ColBERT

model = ColBERT("colbert-ir/colbertv2.0")
queries_embeddings = model.encode(["什么是机器学习"], is_query=True)
documents_embeddings = model.encode(["机器学习是AI的分支"])

# 计算晚期交互分数
scores = model.rank(queries_embeddings, documents_embeddings)
print(scores)

6.4 模型选型建议

模型 维度 中文支持 特点 适用场景
BGE-large-zh 1024 优秀 中文优化,指令增强 中文检索首选
E5-large 1024 良好 多语言,对比学习 多语言场景
GTE-large 1024 良好 通用性强 通用场景
ColBERTv2 128 良好 细粒度匹配 精度要求高
OpenAI text-embedding-3 3072 良好 商业API 快速集成

七、查询改写与扩展技术

7.1 查询改写的价值

用户的原始查询往往是简短、模糊甚至有歧义的。查询改写通过扩展和优化查询表达,提高检索的召回率和精确率。

7.2 基于LLM的查询改写

import openai

class QueryRewriter:
    def __init__(self, llm_client):
        self.client = llm_client
    
    def rewrite(self, query: str, num_rewrites: int = 3) -> list:
        """生成多个查询改写版本"""
        prompt = f"""你是一个搜索查询优化专家。请为以下查询生成{num_rewrites}个不同的改写版本,
        使每个版本从不同角度表达相同的搜索意图。
        
        原始查询: {query}
        
        请返回{num_rewrites}个改写版本,每行一个:"""
        
        response = self.client.chat.completions.create(
            model="gpt-4o-mini",
            messages=[{"role": "user", "content": prompt}],
            temperature=0.7
        )
        
        rewrites = response.choices[0].message.content.strip().split('\n')
        return [r.strip() for r in rewrites if r.strip()]
    
    def expand_with_hyde(self, query: str) -> str:
        """HyDE: 生成假设性文档来增强检索"""
        prompt = f"""请根据以下查询,写一段可能包含答案的文档内容(约100-200字)。
        这个文档应该像是一个真实的技术文档或知识库文章。
        
        查询: {query}
        
        假设性文档:"""
        
        response = self.client.chat.completions.create(
            model="gpt-4o-mini",
            messages=[{"role": "user", "content": prompt}],
            temperature=0.3
        )
        
        return response.choices[0].message.content.strip()
    
    def decompose_query(self, query: str) -> list:
        """将复杂查询分解为子查询"""
        prompt = f"""请将以下复杂查询分解为多个简单的子查询,每个子查询关注问题的一个方面。
        
        复杂查询: {query}
        
        子查询(每行一个):"""
        
        response = self.client.chat.completions.create(
            model="gpt-4o-mini",
            messages=[{"role": "user", "content": prompt}],
            temperature=0.3
        )
        
        sub_queries = response.choices[0].message.content.strip().split('\n')
        return [q.strip() for q in sub_queries if q.strip()]

7.3 HyDE(Hypothetical Document Embeddings)

HyDE是一种创新的查询增强技术:不直接用查询去检索,而是先让LLM生成一个"假设性答案文档",然后用这个文档的Embedding去检索。

class HyDESearch:
    """基于HyDE的检索"""
    
    def __init__(self, llm, embedding_model, vector_store):
        self.llm = llm
        self.embedding_model = embedding_model
        self.vector_store = vector_store
    
    def search(self, query: str, top_k: int = 10):
        # 步骤1: 生成假设性文档
        hypothetical_doc = self.generate_hypothetical_doc(query)
        
        # 步骤2: 对假设性文档进行Embedding
        hyde_embedding = self.embedding_model.encode(hypothetical_doc)
        
        # 步骤3: 用假设性文档的Embedding进行检索
        results = self.vector_store.search(hyde_embedding, top_k)
        
        return results
    
    def generate_hypothetical_doc(self, query: str) -> str:
        prompt = f"""请写一段详细的技术文档,回答以下问题。
        要求:内容准确、结构清晰、约200字。
        
        问题: {query}"""
        
        response = self.llm.generate(prompt)
        return response

HyDE的优势

  • 假设性文档通常比短查询包含更丰富的语义信息
  • 生成的文档更接近目标文档的表述方式
  • 特别适合知识库问答场景

HyDE的局限

  • 依赖LLM的质量,可能生成错误信息
  • 增加了一次LLM调用的延迟
  • 对于事实性查询效果更好,创意性查询可能不适用

7.4 多查询融合

class MultiQuerySearch:
    """多查询融合检索"""
    
    def __init__(self, rewriter, search_engine):
        self.rewriter = rewriter
        self.search_engine = search_engine
    
    def search(self, query: str, top_k: int = 10):
        # 生成多个查询变体
        queries = [query] + self.rewriter.rewrite(query, num_rewrites=3)
        
        # 对每个查询进行检索
        all_results = []
        for q in queries:
            results = self.search_engine.search(q, top_k=top_k * 2)
            all_results.append(results)
        
        # 融合结果(RRF)
        fused = self.rrf_fusion(all_results)
        
        return fused[:top_k]
    
    def rrf_fusion(self, result_lists, k=60):
        scores = {}
        for results in result_lists:
            for rank, (doc_id, score) in enumerate(results, 1):
                if doc_id not in scores:
                    scores[doc_id] = 0
                scores[doc_id] += 1.0 / (k + rank)
        
        return sorted(scores.items(), key=lambda x: x[1], reverse=True)

八、多模态检索

8.1 多模态Embedding

多模态检索将文本、图片、音频等不同模态的数据映射到同一个向量空间,实现跨模态检索。

# 使用CLIP进行图文跨模态检索
from PIL import Image
import torch

# 伪代码示意(使用transformers库)
from transformers import CLIPProcessor, CLIPModel

model = CLIPModel.from_pretrained("openai/clip-vit-base-patch32")
processor = CLIPProcessor.from_pretrained("openai/clip-vit-base-patch32")

# 图文跨模态检索
def cross_modal_search(text_query, image_paths):
    """用文本搜索图片"""
    # 处理文本
    text_inputs = processor(text=[text_query], return_tensors="pt", padding=True)
    text_features = model.get_text_features(**text_inputs)
    text_features = text_features / text_features.norm(dim=-1, keepdim=True)
    
    # 处理图片
    images = [Image.open(p) for p in image_paths]
    image_inputs = processor(images=images, return_tensors="pt", padding=True)
    image_features = model.get_image_features(**image_inputs)
    image_features = image_features / image_features.norm(dim=-1, keepdim=True)
    
    # 计算相似度
    similarities = (text_features @ image_features.T).squeeze(0)
    
    # 排序
    ranked = sorted(
        zip(image_paths, similarities.tolist()),
        key=lambda x: x[1],
        reverse=True
    )
    return ranked

# 示例
results = cross_modal_search(
    "一只可爱的猫咪",
    ["cat.jpg", "dog.jpg", "landscape.jpg"]
)

8.2 音频检索

# 使用Whisper进行音频转录后检索
# 思路:先将音频转为文本,再进行文本检索

import whisper

model = whisper.load_model("base")

def audio_to_text(audio_path):
    """音频转文本"""
    result = model.transcribe(audio_path)
    return result["text"]

def audio_search(query, audio_files):
    """音频内容检索"""
    results = []
    for audio_path in audio_files:
        # 转录音频
        transcript = audio_to_text(audio_path)
        
        # 计算文本相似度(使用Embedding模型)
        # similarity = compute_similarity(query, transcript)
        results.append({
            "file": audio_path,
            "transcript": transcript,
            # "similarity": similarity
        })
    
    return results

8.3 多模态向量数据库集成

# 在向量数据库中存储多模态数据
class MultiModalVectorStore:
    def __init__(self, text_encoder, image_encoder):
        self.text_encoder = text_encoder
        self.image_encoder = image_encoder
        self.store = {}  # 简化示意
    
    def add_text(self, doc_id, text, metadata=None):
        embedding = self.text_encoder.encode(text)
        self.store[doc_id] = {
            "type": "text",
            "embedding": embedding,
            "content": text,
            "metadata": metadata or {}
        }
    
    def add_image(self, doc_id, image_path, caption=None, metadata=None):
        embedding = self.image_encoder.encode(image_path)
        self.store[doc_id] = {
            "type": "image",
            "embedding": embedding,
            "content": image_path,
            "caption": caption,
            "metadata": metadata or {}
        }
    
    def search(self, query, query_type="text", top_k=10):
        if query_type == "text":
            query_emb = self.text_encoder.encode(query)
        elif query_type == "image":
            query_emb = self.image_encoder.encode(query)
        
        results = []
        for doc_id, doc in self.store.items():
            sim = cosine_similarity(query_emb, doc["embedding"])
            results.append((doc_id, sim, doc))
        
        results.sort(key=lambda x: x[1], reverse=True)
        return results[:top_k]

九、搜索结果排序与重排序(Reranker)

9.1 为什么需要Reranker

向量检索(双塔模型)的局限在于:查询和文档是独立编码的,无法捕捉它们之间的细粒度交互。Reranker(交叉编码器)将查询和文档一起输入模型,进行更精细的相关性判断。

两阶段检索架构

  1. 召回阶段:向量检索快速从百万文档中筛选Top-100候选
  2. 重排序阶段:Reranker对候选文档精排,输出Top-10

9.2 Cross-Encoder Reranker

from sentence_transformers import CrossEncoder

# 加载重排序模型
reranker = CrossEncoder('BAAI/bge-reranker-large', max_length=512)

def rerank(query: str, documents: list, top_k: int = 10):
    """对检索结果进行重排序"""
    # 构造(query, document)对
    pairs = [(query, doc) for doc in documents]
    
    # 计算相关性分数
    scores = reranker.predict(pairs)
    
    # 排序
    scored_docs = list(zip(documents, scores))
    scored_docs.sort(key=lambda x: x[1], reverse=True)
    
    return scored_docs[:top_k]

# 示例
query = "如何优化Python代码性能"
documents = [
    "使用NumPy进行向量化计算可以大幅提升性能",
    "Python是一种解释型编程语言",
    "使用多线程和异步IO可以提高并发性能",
    "今天天气很好",
    "使用Cython将关键代码编译为C扩展"
]

results = rerank(query, documents, top_k=3)
for doc, score in results:
    print(f"Score: {score:.4f} | {doc}")

9.3 ColBERT风格的晚期交互Reranker

class LateInteractionReranker:
    """ColBERT风格的晚期交互重排序"""
    
    def __init__(self, model):
        self.model = model
    
    def rerank(self, query, documents, top_k=10):
        # 获取Token级别表示
        query_tokens = self.model.encode(query, return_token_embeddings=True)
        
        results = []
        for doc in documents:
            doc_tokens = self.model.encode(doc, return_token_embeddings=True)
            
            # MaxSim评分
            score = self.compute_maxsim(query_tokens, doc_tokens)
            results.append((doc, score))
        
        results.sort(key=lambda x: x[1], reverse=True)
        return results[:top_k]
    
    def compute_maxsim(self, query_tokens, doc_tokens):
        """ColBERT的MaxSim操作"""
        # 对每个查询token,找文档中最相似的token
        score_matrix = np.dot(query_tokens, doc_tokens.T)
        max_sim_per_query = np.max(score_matrix, axis=1)
        return np.sum(max_sim_per_query)

9.4 完整的两阶段检索Pipeline

class TwoStageRetriever:
    """两阶段检索:向量召回 + Reranker重排"""
    
    def __init__(self, vector_store, reranker, recall_top_k=100, final_top_k=10):
        self.vector_store = vector_store
        self.reranker = reranker
        self.recall_top_k = recall_top_k
        self.final_top_k = final_top_k
    
    def search(self, query: str):
        # 阶段1: 向量召回
        candidates = self.vector_store.search(query, top_k=self.recall_top_k)
        
        # 阶段2: Reranker重排
        docs = [doc for doc, _ in candidates]
        reranked = self.reranker.rerank(query, docs, top_k=self.final_top_k)
        
        return reranked

十、搜索质量评估指标

10.1 常用评估指标

MRR(Mean Reciprocal Rank)

MRR衡量的是第一个相关结果的排名质量:

def mrr(queries_results, ground_truth):
    """
    queries_results: {query_id: [doc_id1, doc_id2, ...]}
    ground_truth: {query_id: [relevant_doc_ids]}
    """
    rr_sum = 0
    for qid, results in queries_results.items():
        relevant = ground_truth.get(qid, set())
        for rank, doc_id in enumerate(results, 1):
            if doc_id in relevant:
                rr_sum += 1.0 / rank
                break
    return rr_sum / len(queries_results)

# 示例
queries_results = {
    "q1": ["doc3", "doc1", "doc2"],  # doc1是相关文档
    "q2": ["doc5", "doc4", "doc6"],  # doc4是相关文档
}
ground_truth = {
    "q1": {"doc1"},
    "q2": {"doc4"},
}
print(f"MRR: {mrr(queries_results, ground_truth):.4f}")  # (1/2 + 1/2) / 2 = 0.5

NDCG(Normalized Discounted Cumulative Gain)

NDCG考虑了相关性等级和排名位置:

import numpy as np

def dcg_at_k(relevances, k):
    """计算DCG@k"""
    relevances = np.array(relevances[:k])
    if len(relevances) == 0:
        return 0
    return np.sum(relevances / np.log2(np.arange(2, len(relevances) + 2)))

def ndcg_at_k(predicted_relevances, ideal_relevances, k):
    """计算NDCG@k"""
    dcg = dcg_at_k(predicted_relevances, k)
    idcg = dcg_at_k(sorted(ideal_relevances, reverse=True), k)
    if idcg == 0:
        return 0
    return dcg / idcg

# 示例:0=不相关, 1=部分相关, 2=高度相关
predicted = [2, 1, 0, 1, 0]  # 检索结果的相关性
ideal = [2, 2, 1, 1, 0]      # 理想排序
print(f"NDCG@5: {ndcg_at_k(predicted, ideal, 5):.4f}")

Recall@K

def recall_at_k(predicted, relevant, k):
    """计算Recall@K"""
    predicted_at_k = set(predicted[:k])
    relevant = set(relevant)
    if len(relevant) == 0:
        return 0
    return len(predicted_at_k & relevant) / len(relevant)

# 示例
predicted = ["doc1", "doc3", "doc5", "doc2", "doc4"]
relevant = ["doc1", "doc2", "doc6"]
print(f"Recall@3: {recall_at_k(predicted, relevant, 3):.4f}")  # 1/3
print(f"Recall@5: {recall_at_k(predicted, relevant, 5):.4f}")  # 2/3

10.2 构建评估数据集

class SearchEvaluator:
    """搜索系统评估器"""
    
    def __init__(self, search_engine):
        self.search_engine = search_engine
    
    def evaluate(self, test_data, k_values=[1, 3, 5, 10]):
        """
        test_data: list of {"query": str, "relevant_docs": list}
        """
        metrics = {f"MRR": 0, **{f"NDCG@{k}": 0 for k in k_values}, 
                   **{f"Recall@{k}": 0 for k in k_values}}
        
        for item in test_data:
            query = item["query"]
            relevant = set(item["relevant_docs"])
            
            # 检索
            results = self.search_engine.search(query, top_k=max(k_values))
            predicted = [doc_id for doc_id, _ in results]
            
            # 计算MRR
            for rank, doc_id in enumerate(predicted, 1):
                if doc_id in relevant:
                    metrics["MRR"] += 1.0 / rank
                    break
            
            # 计算各K值指标
            for k in k_values:
                # Recall@K
                pred_at_k = set(predicted[:k])
                metrics[f"Recall@{k}"] += len(pred_at_k & relevant) / len(relevant) if relevant else 0
                
                # NDCG@K
                relevances = [1 if doc_id in relevant else 0 for doc_id in predicted[:k]]
                ideal_relevances = sorted(relevances, reverse=True)
                dcg = sum(r / np.log2(i + 2) for i, r in enumerate(relevances))
                idcg = sum(r / np.log2(i + 2) for i, r in enumerate(ideal_relevances))
                metrics[f"NDCG@{k}"] += dcg / idcg if idcg > 0 else 0
        
        # 取平均
        n = len(test_data)
        return {k: v / n for k, v in metrics.items()}

十一、实战案例:构建完整的AI搜索引擎

11.1 项目架构

我们将构建一个完整的AI搜索引擎,包含以下组件:

ai-search-engine/
├── config.py          # 配置
├── embedder.py        # Embedding服务
├── vector_store.py    # 向量存储
├── retriever.py       # 检索器
├── reranker.py        # 重排序
├── query_processor.py # 查询处理
├── search_engine.py   # 搜索引擎主类
├── evaluator.py       # 评估
└── main.py           # 入口

11.2 核心实现

# config.py
from dataclasses import dataclass

@dataclass
class SearchConfig:
    embedding_model: str = "BAAI/bge-base-zh-v1.5"
    reranker_model: str = "BAAI/bge-reranker-base"
    vector_db: str = "chromadb"  # chromadb / milvus / qdrant
    embedding_dim: int = 768
    recall_top_k: int = 50
    rerank_top_k: int = 10
    use_hybrid: bool = True
    hybrid_alpha: float = 0.7  # 稠密检索权重
    enable_query_rewrite: bool = True
# embedder.py
from sentence_transformers import SentenceTransformer
import numpy as np
from typing import List

class EmbeddingService:
    def __init__(self, model_name: str):
        self.model = SentenceTransformer(model_name)
        self.query_prefix = ""
        
        # 根据模型设置前缀
        if "bge" in model_name.lower():
            self.query_prefix = "为这个句子生成表示以用于检索相关文章:"
        elif "e5" in model_name.lower():
            self.query_prefix = "query: "
    
    def encode_query(self, query: str) -> np.ndarray:
        text = self.query_prefix + query
        return self.model.encode(text, normalize_embeddings=True)
    
    def encode_documents(self, documents: List[str]) -> np.ndarray:
        return self.model.encode(documents, normalize_embeddings=True, 
                                  batch_size=32, show_progress_bar=True)
    
    def encode_query_batch(self, queries: List[str]) -> np.ndarray:
        texts = [self.query_prefix + q for q in queries]
        return self.model.encode(texts, normalize_embeddings=True)
# vector_store.py
import chromadb
import numpy as np
from typing import List, Dict, Optional

class VectorStore:
    def __init__(self, collection_name: str = "documents", persist_dir: str = "./data/chroma"):
        self.client = chromadb.PersistentClient(path=persist_dir)
        self.collection = self.client.get_or_create_collection(
            name=collection_name,
            metadata={"hnsw:space": "cosine"}
        )
    
    def add_documents(self, documents: List[Dict]):
        """批量添加文档"""
        ids = [doc["id"] for doc in documents]
        texts = [doc["text"] for doc in documents]
        metadatas = [doc.get("metadata", {}) for doc in documents]
        embeddings = [doc["embedding"].tolist() for doc in documents]
        
        self.collection.add(
            ids=ids,
            documents=texts,
            embeddings=embeddings,
            metadatas=metadatas
        )
    
    def search(self, query_embedding: np.ndarray, top_k: int = 10,
               filters: Optional[Dict] = None) -> List[Dict]:
        """向量检索"""
        params = {
            "query_embeddings": [query_embedding.tolist()],
            "n_results": top_k
        }
        if filters:
            params["where"] = filters
        
        results = self.collection.query(**params)
        
        return [
            {
                "id": results["ids"][0][i],
                "text": results["documents"][0][i],
                "distance": results["distances"][0][i],
                "metadata": results["metadatas"][0][i]
            }
            for i in range(len(results["ids"][0]))
        ]
    
    def count(self) -> int:
        return self.collection.count()
# reranker.py
from sentence_transformers import CrossEncoder
from typing import List, Tuple

class Reranker:
    def __init__(self, model_name: str):
        self.model = CrossEncoder(model_name, max_length=512)
    
    def rerank(self, query: str, documents: List[Dict], top_k: int = 10) -> List[Dict]:
        if not documents:
            return []
        
        pairs = [(query, doc["text"]) for doc in documents]
        scores = self.model.predict(pairs)
        
        for doc, score in zip(documents, scores):
            doc["rerank_score"] = float(score)
        
        reranked = sorted(documents, key=lambda x: x["rerank_score"], reverse=True)
        return reranked[:top_k]
# search_engine.py
from typing import List, Dict, Optional
import numpy as np

class AISearchEngine:
    def __init__(self, config):
        self.config = config
        self.embedder = EmbeddingService(config.embedding_model)
        self.vector_store = VectorStore()
        self.reranker = Reranker(config.reranker_model)
        self.query_rewriter = QueryRewriter() if config.enable_query_rewrite else None
    
    def index_documents(self, documents: List[Dict]):
        """索引文档"""
        texts = [doc["text"] for doc in documents]
        embeddings = self.embedder.encode_documents(texts)
        
        docs_to_add = []
        for doc, emb in zip(documents, embeddings):
            docs_to_add.append({
                "id": doc["id"],
                "text": doc["text"],
                "embedding": emb,
                "metadata": doc.get("metadata", {})
            })
        
        self.vector_store.add_documents(docs_to_add)
        print(f"已索引 {len(documents)} 个文档")
    
    def search(self, query: str, top_k: int = 10, 
               filters: Optional[Dict] = None) -> List[Dict]:
        """执行搜索"""
        # 步骤1: 查询改写
        queries = [query]
        if self.query_rewriter:
            rewrites = self.query_rewriter.rewrite(query)
            queries.extend(rewrites)
        
        # 步骤2: 向量召回
        all_candidates = []
        for q in queries:
            q_emb = self.embedder.encode_query(q)
            candidates = self.vector_store.search(
                q_emb, 
                top_k=self.config.recall_top_k,
                filters=filters
            )
            all_candidates.extend(candidates)
        
        # 步骤3: 去重(按ID)
        seen = set()
        unique_candidates = []
        for doc in all_candidates:
            if doc["id"] not in seen:
                seen.add(doc["id"])
                unique_candidates.append(doc)
        
        # 步骤4: Reranker重排
        reranked = self.reranker.rerank(query, unique_candidates, top_k=top_k)
        
        return reranked
    
    def add_document(self, doc_id: str, text: str, metadata: Dict = None):
        """添加单个文档"""
        embedding = self.embedder.encode_documents([text])[0]
        self.vector_store.add_documents([{
            "id": doc_id,
            "text": text,
            "embedding": embedding,
            "metadata": metadata or {}
        }])

11.3 使用示例

# main.py
from config import SearchConfig
from search_engine import AISearchEngine

# 初始化
config = SearchConfig()
engine = AISearchEngine(config)

# 索引文档
documents = [
    {
        "id": "doc_1",
        "text": "Python是一种解释型、面向对象的高级编程语言,具有简洁的语法和丰富的标准库。",
        "metadata": {"category": "编程", "language": "Python"}
    },
    {
        "id": "doc_2",
        "text": "机器学习是人工智能的一个分支,它使计算机系统能够从数据中自动学习和改进。",
        "metadata": {"category": "AI", "topic": "机器学习"}
    },
    {
        "id": "doc_3",
        "text": "深度学习使用多层神经网络来学习数据的层次化表示,在图像识别和自然语言处理领域取得了突破性进展。",
        "metadata": {"category": "AI", "topic": "深度学习"}
    },
    {
        "id": "doc_4",
        "text": "向量数据库是专门为存储和检索高维向量而设计的数据库,支持高效的近似最近邻搜索。",
        "metadata": {"category": "数据库", "topic": "向量数据库"}
    },
    {
        "id": "doc_5",
        "text": "自然语言处理(NLP)是人工智能领域的重要方向,涉及文本分类、情感分析、机器翻译等任务。",
        "metadata": {"category": "AI", "topic": "NLP"}
    }
]

engine.index_documents(documents)

# 搜索
results = engine.search("什么是AI的核心技术", top_k=3)
for i, result in enumerate(results, 1):
    print(f"\n--- 结果 {i} ---")
    print(f"ID: {result['id']}")
    print(f"文本: {result['text'][:100]}...")
    print(f"重排序分数: {result.get('rerank_score', 'N/A'):.4f}")

11.4 性能优化建议

索引优化

  • 使用批量编码代替逐条编码
  • 预计算Embedding并缓存
  • 选择合适的ANN索引参数(如HNSW的M和efConstruction)

查询优化

  • 缓存热门查询的结果
  • 使用异步并发处理多查询
  • 实施查询路由,简单查询跳过Reranker

存储优化

  • 使用PQ量化减少内存占用
  • 对元数据使用合适的索引
  • 定期清理过期数据

十二、最佳实践

12.1 Embedding模型选择

  1. 优先选择领域适配的模型:中文场景优先考虑BGE、M3E等
  2. 关注模型的评估指标:在MTEB等基准上查看模型表现
  3. 考虑推理效率:大模型不一定适合所有场景
  4. 测试实际效果:用自己领域的真实数据评估

12.2 索引策略

  1. 分层索引:热门数据用HNSW,冷数据用IVF
  2. 增量更新:支持实时添加新文档
  3. 版本管理:模型更新时的索引迁移策略

12.3 检索质量保障

  1. 建立评估数据集:收集真实查询和标注
  2. A/B测试:线上对比不同策略效果
  3. 监控指标:实时监控MRR、延迟等关键指标
  4. 持续优化:基于用户反馈迭代改进

十三、常见问题

Q1: 向量维度越高越好吗?

不一定。更高维度可以表达更丰富的语义信息,但也带来更高的存储和计算成本。实际选择需要在精度和效率之间平衡。一般768或1024维已经能覆盖大多数场景。

Q2: 如何处理文档过长的问题?

主流方法包括:

  • 分块(Chunking):将长文档按段落或语义边界切分
  • 滑动窗口:使用重叠的固定大小窗口
  • 层次化检索:先检索文档,再检索段落
def sliding_window_chunk(text, chunk_size=512, overlap=128):
    """滑动窗口分块"""
    chunks = []
    start = 0
    while start < len(text):
        end = start + chunk_size
        chunks.append(text[start:end])
        start += chunk_size - overlap
    return chunks

Q3: 混合检索中如何调整稀疏和稠密的权重?

建议通过实验确定:

  1. 在评估数据集上测试不同alpha值
  2. 典型范围:0.5-0.8(稠密检索权重)
  3. 不同查询类型可能需要不同权重
  4. 可以训练一个小模型来预测最优权重

Q4: 如何降低Reranker的延迟?

  • 减少召回数量(在recall和latency之间平衡)
  • 使用更轻量的Reranker模型
  • 实施早期终止策略
  • 使用GPU加速推理
  • 对Reranker结果进行缓存

Q5: 多模态检索的实际应用场景有哪些?

  • 电商搜索:用文字搜商品图片
  • 内容平台:图文内容检索
  • 安防监控:自然语言搜索视频片段
  • 医疗影像:文字描述检索相似病例

十四、总结

AI搜索引擎代表了信息检索技术的重大范式转变——从关键词匹配走向语义理解。构建一个生产级的AI搜索引擎需要综合考虑以下关键环节:

  1. Embedding模型选择:根据语言、领域和性能需求选择合适的模型
  2. 向量数据库选型:根据数据规模、部署环境和功能需求选择
  3. 混合检索策略:结合稀疏和稠密检索的优势
  4. 查询优化:通过改写和扩展提升检索效果
  5. 重排序机制:使用Cross-Encoder提升最终结果质量
  6. 质量评估:建立完善的评估体系持续优化

技术栈推荐:

  • 快速原型:ChromaDB + BGE + BGE-Reranker
  • 生产环境:Milvus/Qdrant + BGE/E5 + BGE-Reranker + LLM查询改写
  • 大规模系统:Milvus集群 + 多模型集成 + 分布式Reranker

语义检索技术仍在快速演进,未来的发展方向包括:

  • 更高效的Embedding模型(如Matryoshka Embeddings)
  • 端到端的检索生成一体化(RAG)
  • 多模态统一检索
  • 自适应检索策略

掌握本教程介绍的核心技术和工程实践,你将能够构建出高质量的AI搜索系统,为用户提供精准、智能的搜索体验。

内容声明

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

目录