向量检索¶
实现一个基于内存 +
numpy的向量数据库,演示 RAG 中的索引与检索流程: - 文本向量化(确定性哈希向量器) - 余弦相似度检索 - 元数据过滤 生产环境可替换为 Qdrant、Milvus、Chroma 等专业向量数据库。 对应教程:第6卷《RAG》第2章 向量数据库。
文件路径: examples/volume-06-rag\vector_search.py
源代码¶
"""向量检索示例。
实现一个基于内存 + ``numpy`` 的向量数据库,演示 RAG 中的索引与检索流程:
- 文本向量化(确定性哈希向量器)
- 余弦相似度检索
- 元数据过滤
生产环境可替换为 Qdrant、Milvus、Chroma 等专业向量数据库。
对应教程:第6卷《RAG》第2章 向量数据库。
"""
from __future__ import annotations
import hashlib
import math
import re
from dataclasses import dataclass, field
from typing import Any
import numpy as np
def tokenize(text: str) -> list[str]:
"""简易分词:英文单词 + 中文字符。"""
text = text.lower()
words = re.findall(r"[a-z]+", text)
chars = re.findall(r"[\u4e00-\u9fff]", text)
return words + chars
class HashEmbedder:
"""基于哈希的确定性向量器(教学用,归一化后可直接用点积算余弦)。"""
def __init__(self, dim: int = 128, seed: int = 0) -> None:
if dim <= 0:
raise ValueError("dim 必须为正")
self.dim = dim
self.seed = seed
def embed(self, text: str) -> np.ndarray:
"""将文本嵌入为 L2 归一化的 numpy 向量。
Args:
text: 原始文本。
Returns:
形状 ``(dim,)`` 的 float32 归一化向量。
"""
vec = np.zeros(self.dim, dtype=np.float32)
for tok in tokenize(text):
digest = hashlib.md5(f"{self.seed}:{tok}".encode()).hexdigest() # noqa: S324
index = int(digest[:8], 16) % self.dim
sign = 1.0 if int(digest[8:16], 16) % 2 == 0 else -1.0
vec[index] += sign
norm = float(np.linalg.norm(vec))
if norm > 0:
vec /= norm
return vec
def embed_batch(self, texts: list[str]) -> np.ndarray:
"""批量向量化。
Returns:
形状 ``(n, dim)`` 的矩阵。
"""
return np.vstack([self.embed(t) for t in texts]) if texts else np.zeros((0, self.dim))
@dataclass
class VectorRecord:
"""向量记录。
Attributes:
id: 记录 ID。
text: 原始文本。
vector: 向量。
metadata: 附加元数据。
"""
id: str
text: str
vector: np.ndarray
metadata: dict[str, Any] = field(default_factory=dict)
@dataclass
class SearchHit:
"""检索命中结果。
Attributes:
id: 记录 ID。
text: 文本。
score: 相似度分数。
metadata: 元数据。
"""
id: str
text: str
score: float
metadata: dict[str, Any]
class InMemoryVectorStore:
"""内存向量库。
支持:
- ``add`` 批量写入
- ``search`` 余弦相似度 top-k 检索
- 元数据过滤(精确匹配)
"""
def __init__(self, embedder: HashEmbedder | None = None) -> None:
self.embedder = embedder or HashEmbedder()
self._records: list[VectorRecord] = []
self._matrix: np.ndarray | None = None # 缓存的 (n, dim) 矩阵
def add(self, texts: list[str], metadatas: list[dict[str, Any]] | None = None) -> list[str]:
"""批量写入文本。
Args:
texts: 文本列表。
metadatas: 对应元数据列表,可选。
Returns:
生成的 ID 列表。
"""
metadatas = metadatas or [{} for _ in texts]
if len(metadatas) != len(texts):
raise ValueError("metadatas 长度必须与 texts 一致")
ids: list[str] = []
for i, (text, meta) in enumerate(zip(texts, metadatas, strict=True)):
rec_id = meta.get("id") or f"rec-{len(self._records) + i}"
ids.append(rec_id)
self._records.append(
VectorRecord(
id=rec_id, text=text, vector=self.embedder.embed(text), metadata=dict(meta)
)
)
self._matrix = None # 失效缓存
return ids
def _build_matrix(self) -> np.ndarray:
if not self._records:
return np.zeros((0, self.embedder.dim), dtype=np.float32)
self._matrix = np.vstack([r.vector for r in self._records])
return self._matrix
def search(
self,
query: str,
top_k: int = 3,
filter: dict[str, Any] | None = None,
) -> list[SearchHit]:
"""检索与 query 最相似的 top_k 条记录。
Args:
query: 查询文本。
top_k: 返回数量。
filter: 元数据过滤条件(键值精确匹配)。
Returns:
按相似度降序排列的 ``SearchHit`` 列表。
"""
if not self._records:
return []
q_vec = self.embedder.embed(query)
matrix = self._build_matrix()
# 向量已归一化,点积即余弦
scores = matrix @ q_vec
# 应用元数据过滤
candidates: list[tuple[int, float]] = []
for i, rec in enumerate(self._records):
if filter and not all(rec.metadata.get(k) == v for k, v in filter.items()):
continue
candidates.append((i, float(scores[i])))
candidates.sort(key=lambda x: x[1], reverse=True)
hits: list[SearchHit] = []
for i, score in candidates[:top_k]:
rec = self._records[i]
hits.append(SearchHit(id=rec.id, text=rec.text, score=score, metadata=rec.metadata))
return hits
def __len__(self) -> int:
return len(self._records)
def main() -> None:
"""演示向量检索流程。"""
store = InMemoryVectorStore(HashEmbedder(dim=128))
texts = [
"AI Agent 是能自主决策的智能体",
"RAG 通过检索增强大模型生成",
"今天天气晴朗适合户外活动",
"MCP 是连接大模型与工具的协议",
]
metadatas = [
{"topic": "agent"},
{"topic": "rag"},
{"topic": "weather"},
{"topic": "mcp"},
]
store.add(texts, metadatas)
query = "智能体自己做决定"
print(f"查询: {query}\n")
for hit in store.search(query, top_k=2):
print(f" {hit.score:.4f} [{hit.metadata['topic']}] {hit.text}")
print("\n带过滤 (topic=rag):")
for hit in store.search("检索", top_k=2, filter={"topic": "rag"}):
print(f" {hit.score:.4f} {hit.text}")
if __name__ == "__main__":
main()