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(交叉编码器)将查询和文档一起输入模型,进行更精细的相关性判断。
两阶段检索架构:
- 召回阶段:向量检索快速从百万文档中筛选Top-100候选
- 重排序阶段: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模型选择
- 优先选择领域适配的模型:中文场景优先考虑BGE、M3E等
- 关注模型的评估指标:在MTEB等基准上查看模型表现
- 考虑推理效率:大模型不一定适合所有场景
- 测试实际效果:用自己领域的真实数据评估
12.2 索引策略
- 分层索引:热门数据用HNSW,冷数据用IVF
- 增量更新:支持实时添加新文档
- 版本管理:模型更新时的索引迁移策略
12.3 检索质量保障
- 建立评估数据集:收集真实查询和标注
- A/B测试:线上对比不同策略效果
- 监控指标:实时监控MRR、延迟等关键指标
- 持续优化:基于用户反馈迭代改进
十三、常见问题
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: 混合检索中如何调整稀疏和稠密的权重?
建议通过实验确定:
- 在评估数据集上测试不同alpha值
- 典型范围:0.5-0.8(稠密检索权重)
- 不同查询类型可能需要不同权重
- 可以训练一个小模型来预测最优权重
Q4: 如何降低Reranker的延迟?
- 减少召回数量(在recall和latency之间平衡)
- 使用更轻量的Reranker模型
- 实施早期终止策略
- 使用GPU加速推理
- 对Reranker结果进行缓存
Q5: 多模态检索的实际应用场景有哪些?
- 电商搜索:用文字搜商品图片
- 内容平台:图文内容检索
- 安防监控:自然语言搜索视频片段
- 医疗影像:文字描述检索相似病例
十四、总结
AI搜索引擎代表了信息检索技术的重大范式转变——从关键词匹配走向语义理解。构建一个生产级的AI搜索引擎需要综合考虑以下关键环节:
- Embedding模型选择:根据语言、领域和性能需求选择合适的模型
- 向量数据库选型:根据数据规模、部署环境和功能需求选择
- 混合检索策略:结合稀疏和稠密检索的优势
- 查询优化:通过改写和扩展提升检索效果
- 重排序机制:使用Cross-Encoder提升最终结果质量
- 质量评估:建立完善的评估体系持续优化
技术栈推荐:
- 快速原型:ChromaDB + BGE + BGE-Reranker
- 生产环境:Milvus/Qdrant + BGE/E5 + BGE-Reranker + LLM查询改写
- 大规模系统:Milvus集群 + 多模型集成 + 分布式Reranker
语义检索技术仍在快速演进,未来的发展方向包括:
- 更高效的Embedding模型(如Matryoshka Embeddings)
- 端到端的检索生成一体化(RAG)
- 多模态统一检索
- 自适应检索策略
掌握本教程介绍的核心技术和工程实践,你将能够构建出高质量的AI搜索系统,为用户提供精准、智能的搜索体验。