Учебник веб-разработки
Разделы учебника
На этой странице

VIII. Системы хранения

Векторный поиск: геометрия, индексы и качество

Оглавление · Модели хранения · Валидация

Задача: отличить близость векторов от релевантности и достоверности. Нужны скалярное произведение, норма и расстояния.

Что кодирует 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-индекса.