2.3 检索算法实战 检索算法是知识库系统的核心,决定了用户查询的准确性和响应速度。本章通过详细的算法分析、代码实现和性能优化,帮助读者掌握主流检索技术在实际项目中的应用。 2.3.1 主流检索算法详解 不同的检索算法适用于不同的应用场景,下面详细介绍几种主流的检索算法及其数学原理。 余弦相似度(Cosine Similarity) 余弦相似度是向量检索中最常用的算法,通过计算向量间的夹角余弦值来衡量相似程度。 数学原理: 余弦相似度的计算基于向量空间中的夹角概念,两个向量间的夹角越小,表示它们越相似。 公式推导: 对于两个n维向量A和B,它们的余弦相似度定义为: 其中: A·B = a₁b₁ + a₂b₂ + ... + aₙbₙ (向量内积) A| = √(a₁² + a₂² + ...
检索算法是知识库系统的核心,决定了用户查询的准确性和响应速度。本章通过详细的算法分析、代码实现和性能优化,帮助读者掌握主流检索技术在实际项目中的应用。
不同的检索算法适用于不同的应用场景,下面详细介绍几种主流的检索算法及其数学原理。
余弦相似度是向量检索中最常用的算法,通过计算向量间的夹角余弦值来衡量相似程度。
数学原理:
余弦相似度的计算基于向量空间中的夹角概念,两个向量间的夹角越小,表示它们越相似。
公式推导:
对于两个n维向量A和B,它们的余弦相似度定义为:
cos(θ) = (A·B) / (|A|·|B|)
其中:
核心优势:
主要缺点:
适用场景:
欧氏距离是最直观的距离度量方法,计算向量在多维空间中的直线距离。
数学原理:
欧氏距离基于欧几里得几何中的距离公式,表示两点之间的直线距离。
公式推导:
对于两个n维向量A和B,它们的欧氏距离定义为:
d(A,B) = √∑(aᵢ - bᵢ)² = √[(a₁-b₁)² + (a₂-b₂)² + ... + (aₙ-bₙ)²]
核心优势:
主要缺点:
适用场景:
内积相似度是一种计算效率较高的相似度度量方法,特别适合归一化向量的计算。
数学原理:
内积相似度直接计算两个向量的点积,不需要计算向量的模长。
公式推导:
对于两个n维向量A和B,它们的内积定义为:
sim(A,B) = A·B = a₁b₁ + a₂b₂ + ... + aₙbₙ
核心优势:
主要缺点:
适用场景:
Jaccard相似度基于集合理论,适合处理离散数据和文本匹配。
数学原理:
Jaccard相似度计算两个集合的交集与并集之比,衡量集合的重叠程度。
公式推导:
对于两个集合A和B,它们的Jaccard相似度定义为:
J(A,B) = |A∩B| / |A∪B| = |A∩B| / (|A| + |B| - |A∩B|)
其中:
核心优势:
主要缺点:
适用场景:
BM25是改进的TF-IDF算法,是传统文本检索的黄金标准。
数学原理:
BM25算法结合了词频(TF)和逆文档频率(IDF),并引入了文档长度归一化和参数调节。
公式推导:
对于查询Q和文档D,BM25得分定义为:
Score(Q,D) = ∑[IDF(qᵢ) × f(qᵢ,D) × (k₁ + 1) / (f(qᵢ,D) + k₁ × (1 - b + b × |D|/avgdl))]
其中:
核心优势:
主要缺点:
适用场景:
不同的检索算法在多个维度上各有特点,下面详细分析它们的性能差异。
| 算法 | 计算复杂度 | 准确率 | 速度 | 内存占用 | 适用场景 | 特点 |
|---|---|---|---|---|---|---|
| 余弦相似度 | O(n) | 高 | 中 | 中 | 语义检索 | 对长度不敏感 |
| 欧氏距离 | O(n) | 中 | 快 | 中 | 距离计算 | 几何意义明确 |
| 内积相似度 | O(n) | 中 | 最快 | 低 | 归一化向量 | 计算效率高 |
| Jaccard相似度 | O(n) | 中 | 中 | 低 | 文本相似度 | 集合理论基础 |
| BM25算法 | O(n) | 中 | 快 | 低 | 关键词搜索 | 传统检索标准 |
计算效率对比:
准确性对比:
内存占用对比:
选择算法的考虑因素:
数据特征:
性能要求:
资源限制:
在实际应用中,检索算法的优化对于提升系统性能至关重要。下面介绍几种有效的优化策略。
多阶段检索是一种平衡准确率和速度的策略,特别适合大规模数据集。
工作原理:
将检索过程分为多个阶段,每个阶段使用不同的策略和参数,逐步筛选和优化结果。
实现步骤:
优势分析:
代码实现:
import numpy as np from typing import List, Tuple, Dict from dataclasses import dataclass @dataclass class SearchResult: """搜索结果数据类""" doc_id: int score: float content: str class MultiStageRetriever: """多阶段检索器""" def __init__(self): self.stage1_retriever = CosineSimilarityRetriever() self.stage2_retriever = DotProductRetriever() self.stage3_retriever = BusinessRuleRetriever() def search(self, query: str, top_k: int = 10) -> List[SearchResult]: """执行多阶段检索""" # 第一阶段:粗检索 stage1_results = self.stage1_retriever.search(query, top_k * 5) # 第二阶段:精检索 stage2_results = [] for result in stage1_results: refined_score = self.stage2_retriever.score(query, result.content) result.score = refined_score stage2_results.append(result) # 排序并选择top candidates stage2_results.sort(key=lambda x: x.score, reverse=True) top_candidates = stage2_results[:top_k * 2] # 第三阶段:业务规则重排序 final_results = [] for result in top_candidates: business_score = self.stage3_retriever.re_score(query, result) result.score = business_score final_results.append(result) # 最终排序 final_results.sort(key=lambda x: x.score, reverse=True) return final_results[:top_k] class CosineSimilarityRetriever: """余弦相似度检索器""" def search(self, query: str, top_k: int) -> List[SearchResult]: """执行余弦相似度检索""" # 这里简化处理,实际需要向量数据库支持 query_vector = self._text_to_vector(query) doc_vectors = self._get_all_doc_vectors() similarities = [] for doc_id, doc_vector in doc_vectors.items(): sim = self._cosine_similarity(query_vector, doc_vector) similarities.append(SearchResult(doc_id, sim, self._get_doc_content(doc_id))) similarities.sort(key=lambda x: x.score, reverse=True) return similarities[:top_k] def _cosine_similarity(self, vec1: np.ndarray, vec2: np.ndarray) -> float: """计算余弦相似度""" return np.dot(vec1, vec2) / (np.linalg.norm(vec1) * np.linalg.norm(vec2)) class DotProductRetriever: """内积检索器""" def score(self, query: str, content: str) -> float: """计算内积分数""" query_vec = self._text_to_vector(query) content_vec = self._text_to_vector(content) return np.dot(query_vec, content_vec) class BusinessRuleRetriever: """业务规则重排序器""" def re_score(self, query: str, result: SearchResult) -> float: """应用业务规则重排序""" base_score = result.score # 时间衰减因子 time_factor = self._get_time_decay(result.doc_id) # 重要性权重 importance_factor = self._get_importance_weight(result.doc_id) # 业务规则调整 business_factor = self._apply_business_rules(query, result) # 综合评分 final_score = base_score * time_factor * importance_factor * business_factor return final_score def _get_time_decay(self, doc_id: int) -> float: """获取时间衰减因子""" # 简化的时间衰减计算 age_days = self._get_doc_age(doc_id) return np.exp(-age_days / 30) # 30天衰减 def _get_importance_weight(self, doc_id: int) -> float: """获取重要性权重""" # 根据文档类型或标签获取权重 doc_type = self._get_doc_type(doc_id) importance_map = { 'article': 1.0, 'tutorial': 1.2, 'reference': 1.5, 'announcement': 0.8 } return importance_map.get(doc_type, 1.0) def _apply_business_rules(self, query: str, result: SearchResult) -> float: """应用业务规则""" # 这里可以添加各种业务规则 rule_factor = 1.0 # 检查关键词匹配 if any(keyword in query.lower() for keyword in ['urgent', 'important']): rule_factor *= 1.1 # 检查文档类型 if 'tutorial' in result.content.lower() and 'learn' in query.lower(): rule_factor *= 1.2 return rule_factor
向量压缩是一种通过降低向量维度来减少计算和存储开销的技术。
压缩方法:
实现示例:
import numpy as np from sklearn.decomposition import PCA from sklearn.preprocessing import StandardScaler class VectorCompressor: """向量压缩器""" def __init__(self, target_dim: int = 128, method: str = 'pca'): self.target_dim = target_dim self.method = method self.pca = None self.scaler = None self.is_fitted = False def fit(self, vectors: np.ndarray): """训练压缩器""" if self.method == 'pca': # 标准化数据 self.scaler = StandardScaler() scaled_vectors = self.scaler.fit_transform(vectors) # PCA降维 self.pca = PCA(n_components=self.target_dim) self.pca.fit(scaled_vectors) self.is_fitted = True def compress(self, vectors: np.ndarray) -> np.ndarray: """压缩向量""" if not self.is_fitted: raise ValueError("Compressor must be fitted before compression") if self.method == 'pca': # 标准化 scaled_vectors = self.scaler.transform(vectors) # PCA投影 compressed = self.pca.transform(scaled_vectors) return compressed.astype(np.float32) def decompress(self, compressed_vectors: np.ndarray) -> np.ndarray: """解压缩(近似)""" if not self.is_fitted: raise ValueError("Compressor must be fitted before decompression") if self.method == 'pca': # PCA逆投影 reconstructed = self.pca.inverse_transform(compressed_vectors) # 反标准化 decompressed = self.scaler.inverse_transform(reconstructed) return decompressed # 使用示例 def demonstrate_vector_compression(): """演示向量压缩""" # 创建示例数据 original_dim = 768 target_dim = 128 num_samples = 1000 # 生成高维向量 original_vectors = np.random.random((num_samples, original_dim)) print(f"原始向量维度: {original_vectors.shape}") print(f"原始大小: {original_vectors.nbytes / 1024 / 1024:.2f} MB") # 创建压缩器 compressor = VectorCompressor(target_dim=target_dim, method='pca') # 训练压缩器 compressor.fit(original_vectors) # 压缩向量 compressed_vectors = compressor.compress(original_vectors) print(f"压缩后维度: {compressed_vectors.shape}") print(f"压缩后大小: {compressed_vectors.nbytes / 1024 / 1024:.2f} MB") # 计算压缩比 compression_ratio = original_vectors.nbytes / compressed_vectors.nbytes print(f"压缩比: {compression_ratio:.2f}x") # 评估压缩效果 reconstructed_vectors = compressor.decompress(compressed_vectors) reconstruction_error = np.mean(np.linalg.norm(original_vectors - reconstructed_vectors, axis=1)) print(f"重构误差: {reconstruction_error:.4f}") return compression_ratio, reconstruction_error
缓存机制是一种通过存储热门查询结果来提升响应速度的技术。
缓存策略:
实现示例:
import time import hashlib from typing import Dict, Optional, Tuple from dataclasses import dataclass from collections import OrderedDict import json @dataclass class CacheEntry: """缓存条目""" result: list timestamp: float access_count: int = 0 ttl: Optional[float] = None # Time To Live in seconds class RetrieverCache: """检索结果缓存""" def __init__(self, max_size: int = 1000, default_ttl: Optional[float] = None): self.max_size = max_size self.default_ttl = default_ttl self.cache: Dict[str, CacheEntry] = {} self.access_order = OrderedDict() # 用于LRU def _get_cache_key(self, query: str, top_k: int, algorithm: str) -> str: """生成缓存键""" key_data = { 'query': query, 'top_k': top_k, 'algorithm': algorithm, 'timestamp': time.time() } key_str = json.dumps(key_data, sort_keys=True) return hashlib.md5(key_str.encode()).hexdigest() def get(self, query: str, top_k: int, algorithm: str) -> Optional[list]: """从缓存获取结果""" cache_key = self._get_cache_key(query, top_k, algorithm) if cache_key in self.cache: entry = self.cache[cache_key] # 检查TTL if entry.ttl and (time.time() - entry.timestamp) > entry.ttl: self._remove_entry(cache_key) return None # 更新访问信息 entry.access_count += 1 self.access_order.move_to_end(cache_key) return entry.result return None def set(self, query: str, top_k: int, algorithm: str, result: list, ttl: Optional[float] = None) -> bool: """设置缓存""" cache_key = self._get_cache_key(query, top_k, algorithm) # 检查缓存大小 if len(self.cache) >= self.max_size: self._evict_lru() # 创建缓存条目 entry = CacheEntry( result=result, timestamp=time.time(), ttl=ttl or self.default_ttl ) # 添加到缓存 self.cache[cache_key] = entry self.access_order[cache_key] = time.time() return True def _evict_lru(self): """淘汰LRU条目""" if self.access_order: oldest_key = next(iter(self.access_order)) self._remove_entry(oldest_key) def _remove_entry(self, cache_key: str): """删除缓存条目""" if