08 · 全文与向量检索:先定义相关性
目标:区分关键词匹配与向量相似性,并为活动搜索设定可验证的检索规则。
索引应该回答什么问题
按活动 ID 查记录、按日期查范围、按地理区域查位置,以及搜索“适合数据库入门的活动”,是不同的问题。B 树按一维排序很有效;二维区域可能使用空间索引,例如按包围区域组织的 R 树。全文检索则常用倒排索引:从词指向包含该词的文档集合,再计算交集、并集或相关性得分。
向量检索把文档映射成数值向量,通过距离或相似度寻找近邻。语义接近的表达可能没有共同关键词。真正的语义来自训练好的嵌入模型和数据,不能随手画几个数就证明搜索质量。本课使用人工二维向量,只展示检索运算。
精确搜索要比较所有候选;近似最近邻索引(例如 HNSW)减少查找工作,但可能漏掉精确近邻,需要用召回率与延迟衡量。相似度高也不等于满足报名资格、权限或日期要求,这些是独立过滤条件。
让两种检索给出不同答案
保存 search.py。先预测关键词 database 会匹配几条,再比较向量排序。
from math import sqrt
docs = {"a1": "database basics", "a2": "storage workshop", "a3": "city walk"}
vectors = {"a1": (1.0, 0.0), "a2": (0.8, 0.2), "a3": (0.0, 1.0)}
index = {}
for doc_id, title in docs.items():
for term in title.split():
index.setdefault(term, set()).add(doc_id)
def cosine(a, b):
dot = sum(x * y for x, y in zip(a, b))
norm = sqrt(sum(x*x for x in a) * sum(y*y for y in b))
if norm == 0:
raise ValueError("zero vector")
return dot / norm
query = (1.0, 0.0)
ranked = sorted(vectors, key=lambda k: (-cosine(query, vectors[k]), k))
assert index["database"] == {"a1"}
assert ranked[:2] == ["a1", "a2"]
print("keyword:", sorted(index["database"]))
print("vector_top2:", ranked[:2])
uv run --python 3.12 --no-project search.py
预期输出:
keyword: ['a1']
vector_top2: ['a1', 'a2']
倒排列表只有真正包含该词的文档。向量排序按照夹角相似度,第二条虽然没有关键词,仍很接近给定查询。这个实验没有分词、语言理解和近似索引;英文空格切分不适用于一般中文检索。
引导修改:① 把查询向量改为 (0, 1);② 预期前两条变为 a3, a2;③ 关键词结果不变,因为两个检索函数使用不同输入。检查结果时不要混用距离越小越好与相似度越大越好的排序方向。
独立练习:a1 不对当前用户开放,要求返回两个有权查看的候选。修改筛选位置并说明为什么只取前两条后删除 a1 可能不够。
参考修改与验收
设 allowed = {'a2', 'a3'},恢复查询 (1, 0),再执行 visible = [k for k in ranked if k in allowed][:2],结果必须是 ['a2', 'a3']。先取 top 2 再过滤只能留下 a2。本例已对全部文档排序,真实近似索引需要支持过滤或扩大候选集,并在返回时再次执行权限检查。
若相似度超过合理范围,检查向量维度与归一化;本函数假设长度一致,实际接入应显式校验。若线上“很相关但不能报名”,检查业务过滤而不是只换嵌入模型。
在 design.md 记录搜索属于派生系统、权限在哪检查,以及用一组人工标注查询评估关键词、向量或混合检索的计划。搜索不决定剩余名额。