Qdrant: как измерить потери приближённого поиска через exact

После настройки HNSW поиск стал быстрее, но несколько знакомых документов исчезли из первой десятки. Вопросы, эмбеддинги и содержимое коллекции остались прежними. Прежде чем менять модель или переписывать документы, полезно проверить более узкую гипотезу: сколько ближайших соседей теряет приближённый индекс.

Для этого один и тот же запрос выполняют двумя способами. Обычный поиск использует доступные индексы и выбирает эффективный план. Точный поиск вычисляет ближайших соседей без приближённого обхода HNSW. Если списки сильно расходятся, причину нужно искать в поисковых настройках. Если совпадают, следующий вопрос уже другой: почему сами ближайшие векторы плохо соответствуют задаче пользователя.

Два эталона, которые нельзя смешивать

Предположим, точная выдача содержит точки 1, 2 и 3, а приближённая — 1, 2 и 4. Совпали две точки из трёх: доля пересечения равна 2/3. В этой проверке эталон задаёт точный поиск по выбранной метрике. Человек ещё не оценивал, есть ли в этих документах ответ. Поэтому в отчёте назовём показатель ANN overlap@K и отдельно сохраним метрику релевантности по ручной разметке.

Такое разделение экономит время. Идеальное совпадение с точным поиском не исправит эмбеддинги, которые путают правила возврата и правила обмена. Зато низкое совпадение позволяет проверить индекс без генератора ответов: LLM для этого эксперимента вообще не нужна.

Сравнение через Query API

В Query API Qdrant точный режим включается полем params.exact. Ниже программа для коллекции manuals с одним безымянным вектором. Она только читает данные. В файл queries.json положите массив настоящих векторов запросов той же модели, которой индексировали документы. Для именованного вектора добавьте одинаковое поле using в оба запроса.

import json
import sys
from urllib.request import Request, urlopen

def overlap(approx, exact):
    reference = set(exact)
    if not reference:
        return None
    return len(set(approx) & reference) / len(reference)

def search(url, vector, exact, filters=None):
    body = {"query": vector, "limit": 10,
            "params": {"exact": exact},
            "with_payload": False, "with_vector": False}
    if filters is not None:
        body["filter"] = filters
    request = Request(url, json.dumps(body).encode(),
                      {"Content-Type": "application/json"})
    with urlopen(request, timeout=30) as response:
        return [p["id"] for p in json.load(response)["result"]["points"]]

if __name__ == "__main__":
    if sys.argv[1:] == ["--self-test"]:
        assert overlap([1, 2, 4], [1, 2, 3]) == 2 / 3
        assert overlap([1, 1], [1, 2]) == 0.5
        assert overlap([], []) is None
        print(f"ANN overlap@3 = {overlap([1, 2, 4], [1, 2, 3]):.3f}")
    else:
        # queries.json: массив векторов вашей модели эмбеддингов
        with open(sys.argv[1], encoding="utf-8") as f:
            vectors = json.load(f)
        url = "http://localhost:6333/collections/manuals/points/query"
        values = []
        for vector in vectors:
            value = overlap(search(url, vector, False),
                            search(url, vector, True))
            if value is not None:
                values.append(value)
        if not values:
            raise ValueError("Нет запросов с непустой эталонной выдачей")
        print(f"ANN overlap@10 = {sum(values) / len(values):.3f}")

Сохраните код как ann_overlap.py. Команда python ann_overlap.py --self-test проверяет расчёт и выводит ANN overlap@3 = 0.667. Для замера своей коллекции выполните python ann_overlap.py queries.json. Адрес в примере рассчитан на локальный Qdrant без авторизации; подключение к защищённому стенду настройте штатным способом своего приложения.

Если после фильтра доступно всего семь точек, программа делит на семь точных результатов. Деление на запрошенные десять записало бы отсутствие данных в потери алгоритма. Пустая точная выдача исключается из среднего: такие запросы посчитайте отдельно, чтобы не потерять ошибку выбора коллекции или фильтра.

Как получить сравнимый замер

Запускайте обе ветки на неподвижном тестовом корпусе. Между запросами не должны добавляться и удаляться точки. Передавайте одинаковые фильтры, имя вектора и размер выдачи. Не ставьте score_threshold в первый эксперимент: он добавляет ещё одну причину исчезновения результатов. Если рабочий сценарий использует фильтры, повторите сравнение именно с ними после базового замера.

При большом числе одинаковых векторов граница первой десятки может проходить через группу с равными оценками. Тогда разные ID на последней позиции не обязательно означают худшую близость. Сохраните оценки точек возле границы и проверьте наличие таких ничьих перед жёстким запретом релиза. Для обычного корпуса с различимыми расстояниями сравнение множеств остаётся удобной диагностикой.

Для набора запросов используйте реальные пользовательские формулировки, а не векторы случайно выбранных документов. Сохраните их заранее, чтобы второй прогон использовал те же числа. Если сравниваете два значения параметров, перемешивайте порядок запросов и отделяйте первый проход от повторных: прогрев памяти способен изменить время ответа. Само пересечение ID от прогрева меняться не должно. Рядом с итоговым средним сохраните значение для каждого вопроса — так можно вернуться к конкретной потере и прочитать исчезнувший фрагмент.

Что менять после результата

Сначала сохраните исходный overlap и задержку на одном наборе вопросов. Затем увеличьте hnsw_ef в приближённой ветке, повторите замер и сопоставьте выигрыш с ростом времени ответа. Это параметр поиска: он позволяет исследовать больше кандидатов. Выберите настройку по допустимой задержке вашего сервиса. Если поиск и так выбирает полный перебор для маленького набора, эксперимент может дать полное совпадение при всех значениях.

Разбейте отчёт хотя бы на запросы без фильтра и с узким фильтром. Среднее по всем вопросам способно скрыть потери в небольшой, но важной группе. Когда точность приближённого поиска устраивает, переходите к ручным оценкам полезности документов и проверке ответа. О том, как разделять эти уровни, есть отдельный разбор оценки RAG.

В платном курсе Vector DB & RAG Developer вы отработаете выбор индекса, поиск с фильтрами и измерение качества, затем подключите найденные документы к RAG. Это путь от отдельного замера к собственному API поиска. Откройте программу курса и начните с пробного урока «Qdrant на практике».