Эмбеддер bge-m3: смысловой поиск на русском наконец разделяет смыслы

nomic-embed-text на русском не работал: в замере «велосипедная камера»
набирала мультиметру 0.704 — больше, чем верный ответ на «паяльник» (0.691).
Зазор −0.013, то есть мусор систематически обгонял правильное и порогом это
не лечилось.

bge-m3 на том же наборе: верные не ниже 0.537, мусор не выше 0.460,
зазор +0.077. Префиксы задачи ему не нужны, поэтому обнулены.

Порог откалиброван на настоящей базе, а не на придуманном наборе, и картина
там честнее: абстрактное «чем посветить» набирает лампам 0.411, а посторонняя
«детская коляска» — 0.535. Полностью не разделяются, поэтому SEMANTIC_MIN
выставлен так, чтобы отсечь весь мусор ценой слабых догадок — уверенно
показанное не то хуже честного «не нашёл». Точных запросов это не касается:
они находятся по словам и тегам.

Добавлен tools/reindex.py: смена эмбеддера требует полного пересчёта векторов,
иначе смешанная база тихо портит поиск. Скрипт делает копию перед работой.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-30 18:23:22 +03:00
co-authored by Claude Opus 5
parent 781690365e
commit 39c807d4de
5 changed files with 127 additions and 33 deletions
+71
View File
@@ -0,0 +1,71 @@
# -*- coding: utf-8 -*-
"""Пересчёт векторов после смены модели эмбеддера.
Векторы разных моделей несовместимы: если половина базы посчитана одной
моделью, а половина другой, косинус между ними — шум, и поиск тихо портится,
ничем себя не выдавая. Поэтому при смене EMBED_MODEL нужен полный пересчёт.
Запуск (сервер лучше остановить, чтобы не писал в базу одновременно):
venv\\Scripts\\python.exe tools\\reindex.py
"""
import io
import os
import sys
import time
import shutil
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
try:
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
except Exception:
pass
from hlamingo import store, search, ollama, config # noqa: E402
def main():
if not config.DB_FILE.exists():
print("База пуста — пересчитывать нечего.")
return 0
if not ollama.check():
print("Ollama недоступна (%s) — без неё векторы не посчитать." % config.OLLAMA_HOST)
return 1
db = store.load()
total = len(db["items"])
print("Модель эмбеддера: %s" % config.EMBED_MODEL)
print("Вещей в базе: %d" % total)
if not total:
return 0
# Своя копия рядом с автоматическими: пересчёт трогает каждую запись.
backup = config.DATA_DIR / "backups" / ("db-before-reindex-%s.json"
% time.strftime("%Y%m%d-%H%M%S"))
backup.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(str(config.DB_FILE), str(backup))
print("Копия: %s\n" % backup.name)
t0 = time.time()
done = 0
for item in db["items"].values():
item["emb"] = None
for item in db["items"].values():
vector = ollama.embed(search.item_text(item))
if not vector:
print(" не удалось посчитать: %s" % item.get("name"))
continue
item["emb"] = vector
done += 1
print(" [%d/%d] %s" % (done, total, item.get("name")))
store.save(db)
print("\nГотово: %d из %d за %.1f с." % (done, total, time.time() - t0))
if done < total:
print("Часть векторов не посчиталась — запусти ещё раз, когда Ollama ответит.")
return 1
return 0
if __name__ == "__main__":
sys.exit(main())