本地语义搜索适合“记得大意,却忘了当时用哪个词”的场景。与按字面查找不同,它把查询和笔记转成向量,再比较语义接近程度。本文只处理三条演示笔记,不搭建复杂数据库,也不生成答案,先把检索过程看清楚。需要运行中的 Ollama、Python 和一个可用的嵌入模型。

一、下载专门的嵌入模型
嵌入模型不是聊天模型。本文使用官方示例中的 embeddinggemma,实际所需 Ollama 版本和许可条件请看模型页。首次下载需要网络。
ollama pull embeddinggemma
ollama list
选中文资料时也要用自己的样本检验效果,不能只凭“支持多语言”就断言能准确找到所有专业术语。
二、准备三条能人工判断的笔记
下面的样本分别讲备份、字幕和模型目录。查询“照片丢了怎么恢复”时,重点看检索结果是否把备份笔记排在合理位置,再打开原文确认,而不是把最高分当作正确答案。
保存代码为 search_notes.py。示例每次运行重新生成向量,便于入门检查;大规模资料应保存索引并记录模型版本。
import json
import math
import urllib.request
notes = [
{"id": "N01", "text": "照片应保留独立备份,并定期恢复到测试目录验证。"},
{"id": "N02", "text": "字幕校对需要检查时间码、人名和中文断句。"},
{"id": "N03", "text": "模型目录可以迁到其他磁盘,修改后重启模型服务。"},
]
query = input("Search: ").strip()
if not query:
raise SystemExit("Query is empty")
payload = {"model": "embeddinggemma", "truncate": False,
"input": [item["text"] for item in notes] + [query]}
request = urllib.request.Request(
"http://127.0.0.1:11434/api/embed",
data=json.dumps(payload).encode("utf-8"),
headers={"Content-Type": "application/json"})
with urllib.request.urlopen(request, timeout=180) as response:
vectors = json.load(response)["embeddings"]
if len(vectors) != len(notes) + 1:
raise SystemExit("Unexpected vector count")
def cosine(a, b):
if len(a) != len(b):
raise ValueError("Vector dimensions differ")
denominator = math.sqrt(sum(x*x for x in a) * sum(x*x for x in b))
return sum(x*y for x, y in zip(a, b)) / denominator if denominator else 0.0
scores = [(cosine(vector, vectors[-1]), note)
for vector, note in zip(vectors[:-1], notes)]
for score, note in sorted(scores, key=lambda item: item[0], reverse=True):
print(f'{score:.4f} {note["id"]}: {note["text"]}')
三、运行与理解分数
py search_notes.py
分别搜索照片恢复、字幕校对和模型换盘,查看原文是否相关。这里显示的是相似度排序,不是模型判断“答案正确”的概率。0.8 不能直接解释为 80% 正确率,分数阈值需要用自己的检索任务校准。
四、扩展到文件夹时保留来源
每个文本块至少保存文件名、章节、块编号和内容哈希。长文件要按小节分块,别把整个文档硬塞进单次输入。示例设置 truncate 为 False,是为了让超长输入明确失败,而不是悄悄丢掉后文。
建立索引和执行查询必须使用同一嵌入模型与兼容维度。更换模型后重建向量,不要将两种模型的向量直接混在一起比较。
五、与关键词搜索配合
设备型号、错误码、订单编号等精确字符串,关键词搜索通常更直接;语义搜索更适合描述性问题。实际资料库可以先找关键词,再用向量补充候选,始终展示来源而不是只给一个神秘分数。
需要基于检索内容生成答案时,再增加引用与事实检查。不要从“找到了相关段落”直接跳到“系统已经理解所有资料”。
六、常见问题与排错
为什么返回了不相关的最高分?
只要强行排序,总有第一名。增加无答案查询,检查资料里根本没有相关内容时系统能否停止,而不是硬给结论。
用聊天模型为什么调用失败?
确认下载的是嵌入模型,并调用 /api/embed。聊天生成与嵌入的输入输出结构不同。
更换模型后旧索引还能用吗?
通常需要重新生成,至少核对模型、维度和预处理规则。不要只改查询端的模型名。
原理与接口见 Ollama Embeddings及嵌入接口说明。管理笔记摘要可配合本地批量摘要。