Векторный поиск: геометрия, индексы и качество
Оглавление · Модели хранения · Валидация
Задача: отличить близость векторов от релевантности и достоверности. Нужны скалярное произведение, норма и расстояния.
Что кодирует embedding
Модель отображает объект в вектор E(x) ∈ R^d. Геометрическая близость может
помогать находить похожие тексты, но смысл зависит от обучения и задачи модели.
Вектор не является доказательством истинности текста или права пользователя его видеть.
Все сравниваемые векторы должны принадлежать совместимому пространству: версия модели, размерность и обработка входа — часть контракта. Смена модели может потребовать переиндексации. Смешение векторов одинаковой длины от разных моделей само по себе не имеет осмысленной геометрии.
Метрики и точный baseline
Евклидово расстояние: ||x − y||₂. Косинусное сходство для ненулевых векторов:
<x,y> / (||x||₂ ||y||₂). Косинусная дистанция часто задаётся как 1 − similarity;
название «дистанция» не делает её автоматически метрикой с неравенством треугольника.
Для единичных векторов ||x − y||₂² = 2(1 − <x,y>): ранжирование совпадает,
но численные значения различаются.
Учебный точный поиск в двух измерениях, без модели embeddings и БД:
export type Vector2 = readonly [number, number];
export type VectorItem = Readonly<{ id: string; vector: Vector2 }>;
function unit(v: Vector2): Vector2 {
if (!v.every(Number.isFinite)) throw new Error("Coordinates must be finite");
const scale = Math.max(Math.abs(v[0]), Math.abs(v[1]));
if (scale === 0) throw new Error("Invalid norm");
const x = v[0] / scale,
y = v[1] / scale;
const norm = Math.hypot(x, y);
return [x / norm, y / norm];
}
export function cosineDistance(a: Vector2, b: Vector2): number {
const x = unit(a),
y = unit(b);
const similarity = Math.max(-1, Math.min(1, x[0] * y[0] + x[1] * y[1]));
return 1 - similarity;
}
export function nearest(
items: readonly VectorItem[],
query: Vector2,
k: number,
): ReadonlyArray<Readonly<{ id: string; distance: number }>> {
if (!Number.isInteger(k) || k < 0) throw new Error("Invalid k");
unit(query);
return items
.map((item) => ({
id: item.id,
distance: cosineDistance(query, item.vector),
}))
.sort((a, b) => a.distance - b.distance || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
.slice(0, k);
}
console.log(
nearest(
[
{ id: "same", vector: [1, 0] },
{ id: "side", vector: [0, 1] },
{ id: "opposite", vector: [-1, 0] },
],
[1, 0],
2,
),
);
// same: 0, side: 1
Контракт: уникальные id, конечные координаты и ненулевые векторы. TypeScript-кортеж не проверяет неизвестный JSON в runtime: перед функцией нужен разбор внешнего значения. Clamp ограничивает погрешность скалярного произведения, а не исправляет плохую модель. Масштабирование перед нормализацией уменьшает проблемы переполнения и очень маленьких координат; вычисления остаются floating-point. Код ранжирует все записи даже при k=0.
Точный и приближённый поиск
Точный baseline проверяет весь допустимый набор; стоимость растёт с числом векторов и размерностью. Приближённый индекс уменьшает работу ценой возможности пропустить соседей. HNSW и IVFFlat — примеры доступных индексов pgvector; настройки задают компромисс скорости, памяти и полноты. pgvector.
Исходник схемы
flowchart LR Query["Запрос"] --> Embed["Embedding совместимой моделью"] Embed --> Candidates["Поиск кандидатов"] Candidates --> Access["Проверка допустимости и актуальности"] Access --> Rank["Ранжирование и результат"]
Схема не предписывает фильтрацию только после поиска. Если сначала получить глобальный top-k, а потом удалить закрытые записи, можно потерять подходящих доступных соседей. Нужно проектировать поиск по допустимому набору, а окончательная проверка не должна пропускать чужие данные. У ANN-фильтрации есть собственная цена.
Качество измеряют на задаче
Для k > 0 и набора минимум из k записей сравните ANN top-k с точным top-k:
recall@k = |ANN_k ∩ exact_k| / k. Зафиксируйте правило для равных расстояний.
Это полнота относительно геометрического baseline, не оценка человеческой релевантности.
Для релевантности нужны размеченные запросы и подходящие показатели отдельно.
Проверяйте также задержку, число допустимых результатов, свежесть после изменения и удаления, стоимость построения и память. Укажите версию модели и набор проверки. Один удачный запрос не является оценкой качества поиска.
Векторная БД может хранить векторы и метаданные, а расширение реляционной БД — сочетать поиск со связями и фильтрами. Сравнивайте операции, качество, восстановление и эксплуатацию. В Atmanki ни pgvector, ни отдельная векторная система не подключены.
Практика
Проверьте совпадающие, ортогональные и противоположные векторы; нулевой вектор, неверный k, пустой набор и равные расстояния. Поменяйте масштаб положительным коэффициентом и сравните косинусный порядок.
На бумаге задайте корпус публичных статей и удалённую/закрытую запись. Опишите, где ограничивается область поиска и как обновляется индекс. Затем разделите две проверки: совпадение с exact top-k и релевантность для читателя. Пример не генерирует embeddings и не проверяет работу ANN-индекса.