Наивный векторный поиск своими руками

Самый честный способ понять векторный поиск — написать его самому, без библиотек.

Наивный (brute-force) поиск — сравнить запрос с каждым вектором базы и вернуть top-k самых близких. Медленно на больших данных, но идеально точно и понятно.

Что мы строим

Соберём крошечный индекс: список (текст чанка → его игрушечный вектор по осям [спорт, готовка, финансы]). Затем напишем функцию search, которая по вектору запроса вернёт top-k ближайших. Это ровно то, что делает векторная БД — только без оптимизаций.

import math

def cosine(a, b):
    dot = sum(x * y for x, y in zip(a, b))
    na = math.sqrt(sum(x * x for x in a)) or 1e-9
    nb = math.sqrt(sum(x * x for x in b)) or 1e-9
    return dot / (na * nb)

# мини-индекс: (текст чанка, вектор по осям [спорт, готовка, финансы])
index = [
    ("Как варить пасту аль денте",      [0.0, 0.9,  0.1]),
    ("Тренировка ног для бегунов",      [0.9, 0.0,  0.1]),
    ("Куда вложить деньги новичку",     [0.1, 0.0,  0.9]),
    ("Рецепт томатного соуса",          [0.0, 0.95, 0.0]),
    ("Разминка перед марафоном",        [0.85, 0.0, 0.0]),
]

def search(query_vec, k=2):
    scored = [(cosine(query_vec, vec), text) for text, vec in index]
    scored.sort(reverse=True)
    return scored[:k]

query = [0.0, 0.85, 0.15]  # вопрос про готовку
print("Топ-2 ближайших чанка:")
for score, text in search(query, k=2):
    print(f"  {score:.3f}  {text}")

Вывод:

Топ-2 ближайших чанка:
  0.998  Как варить пасту аль денте
  0.985  Рецепт томатного соуса

Запрос «про готовку» вытащил два кулинарных чанка и проигнорировал спорт и финансы — хотя мы нигде не писали слово «готовка» в текстах. Сработала геометрия векторов. Это и есть retrieval-шаг RAG в чистом виде.

Почему это «наивно»

Мы сравниваем запрос со всеми элементами (for по всему индексу). На пяти чанках — мгновенно. На миллионах — медленно. Именно здесь в дело вступают ANN-индексы из прошлого урока: они дают тот же результат почти всегда, но без полного перебора.

Шаг к настоящей базе

Если заменить игрушечные векторы на выход настоящей модели эмбеддингов, а список — на HNSW-индекс, получится продакшн-RAG. Логика top-k поиска при этом не меняется — меняется лишь скорость и масштаб.

Итог

  • Векторный поиск = посчитать близость со всеми и взять top-k.
  • Наивный перебор прост и точен, но линеен по размеру базы.
  • Тот же интерфейс top-k остаётся и в настоящих векторных БД.
Проверьте себя
1. Что делает функция search в наивном векторном поиске?
AСортирует тексты по алфавиту
BСчитает близость запроса со всеми векторами и возвращает top-k
CУдаляет дубликаты
DПереводит запрос в эмбеддинг
2. Почему запрос «про готовку» нашёл кулинарные чанки без слова «готовка» в текстах?
AСовпали буквы
BСработала близость векторов (геометрия смысла), а не совпадение слов
CЭто случайность
DТексты были отсортированы заранее
3. Главный недостаток наивного поиска?
AОн неточен
BОн линеен по размеру базы и медленный на миллионах векторов
CОн требует интернета
DОн не возвращает top-k
Поддержать проект