Семантический поиск
Документы получили векторы одной модели и одной политики подготовки текста. Теперь преобразуем запрос в то же пространство и сравним его с каждым материалом. Так можно искать задачу, даже когда её формулировка не совпадает буквально с названием урока.
Первый семантический алгоритм будет перебором небольшого корпуса. Он удобен для объяснения и наблюдения. Затем в гибридном блоке передадим представления готовому движку и сравним подходы на тех же запросах.
Геометрия близости
Для двух векторов можно рассматривать угол. Если направления близки, косинус угла выше. Когда оба вектора нормированы до единичной длины, косинус равен их скалярному произведению — сумме произведений соответствующих координат.
Возьмём условный двумерный пример: один вектор равен [1, 0], другой — [0.8, 0.6]. Оба имеют длину 1, а их скалярное произведение равно 0.8. Эти числа показывают арифметику; они не получены из модели или отчёта ProfessorWeb.
Число близости не является вероятностью правильного ответа. Значение 0.8 не означает, что статья решит задачу в восьми случаях из десяти. Оно описывает отношение представлений в выбранном пространстве.
Распределение значений зависит от модели и данных. Поэтому порог одного проекта нельзя без проверки переносить в другой. Даже при одинаковой размерности координат разные представления могут давать разные шкалы наблюдений.
Тот же преобразователь запроса
Используем load_model из предыдущего урока. Публичная формулировка запроса не проходит наш браузерный словарь синонимов перед кодированием. Для этой модели будем передавать исходный текст и сравнивать последствия отдельно.
Это сохраняет естественную фразу «получить данные с сервера», а не превращает её в случайный набор внутренних технических имён. При использовании другой модели нужно изучить её договор: некоторые различают подготовку запроса и документа.
Пустая строка не запускает семантическую выдачу. Посетителю по-прежнему предлагается ввести задачу. Иначе программа всегда показывала бы несколько статей при отсутствии вопроса, хотя такой режим не был выбран в нашем интерфейсе.
Полная программа поиска
Создайте semantic.py рядом с vectors.json и model_config.py. Ниже полный файл:
from pathlib import Path
import json
import math
import sys
from model_config import MODEL, REVISION, POLICY, load_model
ROOT = Path(__file__).resolve().parent
def load_vectors():
data = json.loads((ROOT / "vectors.json").read_text(encoding="utf-8"))
pointer = json.loads((ROOT / "public" / "manifest.json").read_text(encoding="utf-8"))
if (data["model"], data["revision"], data["policy"]) != (MODEL, REVISION, POLICY):
raise ValueError("Представления другой модели")
if data["corpusVersion"] != pointer["version"]:
raise ValueError("Представления другого корпуса")
for doc in data["documents"]:
vector = doc["vector"]
if len(vector) != 384 or not all(math.isfinite(x) for x in vector):
raise ValueError("Неверный вектор")
if not math.isclose(sum(x * x for x in vector), 1, abs_tol=0.001):
raise ValueError("Вектор не нормирован")
return data
def search(data, model, query, limit=5):
query = query.strip()
if not query:
return []
if len(query) > 200 or not 1 <= limit <= 50:
raise ValueError("Неверный запрос")
query_vector = model.encode(
query, normalize_embeddings=True, convert_to_numpy=True
).tolist()
if len(query_vector) != 384:
raise ValueError("Неверная размерность запроса")
ranked = []
for doc in data["documents"]:
score = sum(a * b for a, b in zip(query_vector, doc["vector"]))
ranked.append({"id": doc["id"], "url": doc["url"],
"title": doc["title"], "similarity": score})
ranked.sort(key=lambda doc: (-doc["similarity"], doc["id"]))
return ranked[:limit]
if __name__ == "__main__":
query = sys.argv[1] if len(sys.argv) > 1 else "получить данные с сервера"
print(json.dumps(search(load_vectors(), load_model(), query),
ensure_ascii=False, indent=2))
Перед сравнением проверяются происхождение представлений, актуальность корпуса, длина массива и его норма. Это не оценка качества модели, а защита от смешивания несовместимых данных.
Скалярное произведение рассчитано простым Python-циклом. Для шести документов его легко разобрать вручную по структуре. Для большого корпуса векторизованные операции или специализированный индекс могут быть эффективнее; это требует замеров.
Вторичная сортировка по ID делает порядок одинаковых значений предсказуемым. URL карточки сохраняется из исходного документа. Семантический алгоритм не создаёт другой путь для найденной статьи.
Ближайший не означает подходящий
Алгоритм возвращает первые пять документов после сортировки. Даже для бессмысленной строки один материал окажется ближе остальных. Поэтому сам факт наличия результата не доказывает, что в библиотеке есть ответ.
Это отличается от строгого пересечения терминов, которое естественным образом может вернуть пустой набор. Семантическая близость обычно требует дополнительного решения, какие результаты вообще показывать.
Вернитесь к отрицательному запросу первого урока. Если он получает пять карточек, не меняйте ожидание на «любая статья подходит». Такой случай выявляет необходимость оценить порог или иной способ отклонения.
Порог следует выбирать по контрольным данным. Не устанавливайте красивое круглое число без разбора положительных и отрицательных запросов. Слишком высокий предел уберёт полезные свободные вопросы, слишком низкий оставит случайные ответы.
Можно также сравнивать разницу между первым и следующими результатами или использовать отдельную оценку пары вопрос–материал. Это новые механизмы, которые требуют собственных примеров и ресурсов. В текущем уроке важно понять ограничение простого top-k.
Разбор свободного вопроса
Для формулировки «получить данные с сервера» содержательно ожидается материал о Fetch. Но реальный порядок зависит от представления коротких учебных текстов и выбранной модели. Он должен быть получен при выполнении программы, а не подставлен автором.
Посмотрите первые ID и объясните, какие темы оказались близкими. Если выше находится материал о Nginx, модель могла сопоставить вопрос с серверной инфраструктурой. Это не обязательно техническая поломка; нужно уточнить, помогает ли такой ответ намерению читателя.
Запрос по C# ставит другую задачу. Точное название языка иногда надёжнее обслуживается буквальным поиском, чем широким смысловым соседством с другими языками. Дополнительный алгоритм не обязан выигрывать у основного по всем категориям.
В итоге контрольный набор должен содержать и свободные вопросы, и точные идентификаторы. Иначе оценка будет заранее склонена в пользу одного устройства поиска.
Длинные уроки и фрагменты
Векторы нашего примера описывают ограниченное начало документа. Если важная тема находится в конце, ближайший результат может её не отражать. Это связано с подготовкой данных, а не исправляется одним увеличением числа показываемых карточек.
При переходе к фрагментам нужно хранить ID фрагмента, ID статьи и исходный URL. Несколько близких частей одного материала могут занять всю выдачу. Поэтому появится задача группировки по родительскому документу.
Смысловой результат может не содержать буквальные слова вопроса. Подсветка из предыдущего интерфейса в таком случае отсутствует, и это ожидаемо. Можно показать описание материала, но нельзя выдумать совпавшую цитату.
Мы пока выдаём ссылки на существующие тексты, а не генерируем ответ пользователю. Генерация с источниками является другой функцией и требует дополнительных правил проверки. Семантический поиск сам по себе уже имеет полезный самостоятельный результат.
Сравнение с обычным поиском
Запишите версию корпуса и модели вместе с результатами тех же запросов. Сравните верхние подходящие документы и лишние ответы. Время первого запуска модели и последующих вычислений тоже рассматривается отдельно.
Не сравнивайте score браузерной суммы с cosine similarity как одну шкалу. Они получены разными механизмами и имеют разный диапазон. Для общего вывода используйте редакционную релевантность ID и метрики предыдущего блока.
При неудаче сначала установите причину: модель, усечённый текст, неоднозначный запрос или неверная ожидаемая статья. Замена модели без такого разбора может скрыть одну проблему и создать другую.
Теперь семантическая реализация прозрачна: совместимые векторы, понятная арифметика и явные ограничения top-k. Последний урок объединит её возможности с точными совпадениями и подготовит общую контрольную версию.