- Точный векторный поиск вычисляет расстояние между заданной точкой и всеми точками векторного пространства. Это обеспечивает максимально возможную точность, то есть возвращённые точки гарантированно являются истинными ближайшими соседями. Поскольку векторное пространство просматривается полностью, точный векторный поиск может быть слишком медленным для практического применения.
- Приближённый векторный поиск — это группа методов (например, специальные структуры данных, такие как графы и случайные леса), которые позволяют получать результаты гораздо быстрее, чем точный векторный поиск. Точность результата обычно является “достаточно хорошей” для практического использования. Многие приближённые методы предоставляют параметры для настройки компромисса между точностью результата и временем поиска.
vectors с типом Array, например Array(Float64), Array(Float32) или Array(BFloat16).
Опорный вектор — это константный массив, заданный в виде общего табличного выражения.
<DistanceFunction> вычисляет расстояние между опорной точкой и всеми сохранёнными точками.
Для этого можно использовать любую из доступных функций расстояния.
<N> указывает, сколько соседей нужно вернуть.
Точный векторный поиск
Пример
Приближённый векторный поиск
Индексы векторного сходства
Индексы векторного сходства доступны в ClickHouse версии 25.8 и выше.
Если у вас возникнут проблемы, пожалуйста, создайте issue в репозитории ClickHouse.
Создание индекса векторного сходства
ALTER TABLE строит индекс только для новых данных, которые будут вставлены в таблицу в будущем.
Чтобы построить индекс и для существующих данных, его нужно материализовать:
<distance_function> должна принимать одно из следующих значений:
L2Distance— евклидово расстояние, то есть длину отрезка между двумя точками в евклидовом пространстве,cosineDistance— косинусное расстояние, то есть угол между двумя ненулевыми векторами, илиdotProduct— скалярное произведение (внутреннее произведение), то есть сумму попарных произведений элементов двух векторов. Для нормализованных данных эквивалентноcosineDistance.
L2Distance обычно является оптимальным выбором; в противном случае рекомендуется cosineDistance, чтобы компенсировать различия в масштабе.
Для функций расстояния
L2Distance и cosineDistance меньшее значение означает более высокое сходство, тогда как для dotProduct большее значение означает более высокое сходство.
Поэтому векторные индексы с L2Distance и cosineDistance могут использоваться только в запросах SELECT [...] ORDER BY [...] ASC (ASC — значение по умолчанию для ORDER BY), тогда как векторные индексы, построенные для dotProduct, могут использоваться только в запросах SELECT [...] ORDER BY [...] DESC.<dimensions> задаёт мощность массива (число элементов) в исходном столбце.
Если ClickHouse обнаружит массив с другой мощностью во время создания индекса, индекс будет отброшен и возвращена ошибка.
Необязательный параметр GRANULARITY <N> задаёт размер гранул индекса (см. здесь).
В отличие от обычных индексов пропуска данных, для которых гранулярность индекса по умолчанию равна 1, индексы векторного сходства по умолчанию используют гранулярность 100 миллионов.
Это значение позволяет гарантировать, что даже для больших частей будет внутренне построено лишь небольшое число индексов.
Мы рекомендуем изменять гранулярность индекса только опытным пользователям, которые понимают последствия своих действий (см. ниже).
Индексы векторного сходства являются универсальными в том смысле, что поддерживают разные методы приближённого поиска.
Фактически используемый метод задаётся параметром <type>.
На данный момент доступен только метод HNSW (научная статья) — популярная современная техника приближённого векторного поиска, основанная на иерархических графах близости.
Если в качестве типа используется HNSW, пользователь при желании может указать дополнительные параметры, специфичные для HNSW:
<quantization>управляет квантованием векторов в графе близости. Возможные значения:f64,f32,f16,bf16,i8илиb1. Значение по умолчанию —bf16. Обратите внимание, что этот параметр не влияет на представление векторов в исходном столбце.<hnsw_max_connections_per_layer>управляет числом соседей для каждого узла графа, также известным как гиперпараметр HNSWM. Значение по умолчанию —32. Значение0означает использование значения по умолчанию.<hnsw_candidate_list_size_for_construction>управляет размером динамического списка кандидатов при построении графа HNSW, также известным как гиперпараметр HNSWef_construction. Значение по умолчанию —128. Значение0означает использование значения по умолчанию.
- Индексы векторного сходства можно создавать только для столбцов типа Array(Float32), Array(Float64) или Array(BFloat16). Массивы nullable- и low-cardinality-значений с плавающей точкой, такие как
Array(Nullable(Float32))иArray(LowCardinality(Float32)), не допускаются. - Индексы векторного сходства должны создаваться для одиночных столбцов.
- Индексы векторного сходства можно создавать для вычисляемых выражений (например,
INDEX index_name arraySort(vectors) TYPE vector_similarity([...])), но такие индексы впоследствии нельзя использовать для приближённого поиска соседей. - Индексы векторного сходства требуют, чтобы все массивы в исходном столбце содержали по
<dimension>элементов — это проверяется при создании индекса. Чтобы как можно раньше выявлять нарушения этого требования, пользователи могут добавить ограничение для векторного столбца, напримерCONSTRAINT same_length CHECK length(vectors) = 256. - Аналогично, значения массива в исходном столбце не должны быть пустыми (
[]) или иметь значение по умолчанию (тоже[]).
Использование индекса векторного сходства
Чтобы использовать индексы векторного сходства, параметр compatibility должен быть равен
'' (значение по умолчанию), либо '25.1' или выше.SELECT [...] SETTINGS hnsw_candidate_list_size_for_search = <value>).
Значение по умолчанию 256 хорошо подходит для большинства сценариев использования.
Более высокие значения параметра обеспечивают лучшую точность за счёт снижения производительности.
Если запрос может использовать индекс векторного сходства, ClickHouse проверяет, что значение LIMIT <N>, указанное в SELECT-запросах, находится в допустимых пределах.
В частности, возвращается ошибка, если <N> превышает значение параметра max_limit_for_vector_search_queries со значением по умолчанию 100.
Слишком большие значения LIMIT могут замедлить поиск и, как правило, указывают на ошибку в использовании.
Чтобы проверить, использует ли запрос SELECT индекс векторного сходства, можно добавить EXPLAIN indexes = 1 перед запросом.
В качестве примера выполним запрос
Skip, а также имя и тип векторного индекса (в примере — idx и vector_similarity).
В данном случае индекс векторного сходства отсеял две из четырёх гранул, то есть 50% данных.
Чем больше гранул удаётся отсеять, тем эффективнее используется индекс.
Постфильтрация и префильтрация
При необходимости можно указать секцию WHERE с дополнительными условиями фильтрации для запроса SELECT.
ClickHouse вычисляет эти условия фильтрации, используя стратегию постфильтрации или префильтрации.
Если кратко, обе стратегии определяют порядок применения фильтров:
- Постфильтрация означает, что сначала используется индекс векторного сходства, а затем ClickHouse применяет дополнительные фильтры, указанные в предложении
WHERE. - Префильтрация означает, что порядок применения фильтра становится обратным.
- У постфильтрации есть общая проблема: она может вернуть меньше строк, чем указано в секции
LIMIT <N>. Это происходит, когда одна или несколько строк результата, возвращённых индексом векторного сходства, не проходят дополнительные фильтры. - Префильтрация в общем случае остаётся нерешённой задачей. Некоторые специализированные векторные базы данных поддерживают алгоритмы префильтрации, но большинство реляционных баз данных (включая ClickHouse) переходят к точному поиску ближайших соседей, то есть к полному перебору без индекса.
year, и выполняется следующий запрос:
- условие фильтрации отбрасывает хотя бы одну строку в пределах части, ClickHouse переключится на префильтрацию для “оставшихся” диапазонов внутри этой части,
- условие фильтрации не отбрасывает ни одной строки в пределах части, ClickHouse выполнит постфильтрацию для этой части.
auto, реализующий описанные выше эвристики) можно установить в значение prefilter.
Это полезно, если нужно принудительно включить префильтрацию в случаях, когда дополнительные условия фильтрации крайне избирательны.
Например, следующий запрос может выиграть от префильтрации:
SETTINGS vector_search_filter_strategy = 'prefilter' в запрос), ClickHouse сначала находит все книги с ценой ниже 2 долларов, а затем выполняет векторный поиск полным перебором по найденным книгам.
В качестве альтернативного способа решить описанную выше проблему параметр vector_search_index_fetch_multiplier (по умолчанию: 1.0, максимум: 1000.0) можно установить в значение > 1.0 (например, 2.0).
Число ближайших соседей, извлекаемых из векторного индекса, умножается на значение этого параметра, после чего к этим строкам применяется дополнительный фильтр, чтобы вернуть число строк, соответствующее LIMIT.
Например, можно снова выполнить запрос, но с множителем 3.0:
vector_search_index_fetch_multiplier может смягчить эту проблему, но в крайних случаях (при очень селективном условии WHERE) всё же возможно, что будет возвращено менее N запрошенных строк.
Пересчёт оценок
Индекс пропуска данных в ClickHouse обычно фильтрует на уровне гранул, то есть поиск в индексе пропуска данных (внутренне) возвращает список потенциально подходящих гранул, что уменьшает объём читаемых данных при последующем сканировании.
В целом это хорошо работает для индексов пропуска данных, но в случае индексов векторного сходства возникает “несоответствие гранулярности”.
Если говорить подробнее, индекс векторного сходства определяет номера строк N наиболее похожих векторов для заданного эталонного вектора, но затем ему нужно экстраполировать эти номера строк в номера гранул.
Затем ClickHouse загрузит эти гранулы с диска и повторно вычислит расстояние для всех векторов в этих гранулах.
Этот шаг называется пересчётом оценок, и хотя теоретически он может повысить точность — помните, что индекс векторного сходства возвращает лишь приблизительный результат, — с точки зрения производительности он явно не оптимален.
Поэтому ClickHouse предоставляет оптимизацию, которая отключает пересчёт оценок и возвращает наиболее похожие векторы и расстояния до них напрямую из индекса.
Эта оптимизация включена по умолчанию, см. настройку vector_search_with_rescoring.
В общих чертах это работает так: ClickHouse делает наиболее похожие векторы и расстояния до них доступными в виде виртуального столбца _distances.
Чтобы увидеть это, выполните запрос векторного поиска с EXPLAIN header = 1:
Запрос, выполняемый без пересчёта оценок (
vector_search_with_rescoring = 0) и при включенных параллельных репликах, может всё же перейти к пересчёту оценок.Настройка производительности
CODEC(NONE) для векторного столбца следующим образом:
system.text_log) указывают на то, что индекс векторного сходства загружается.
Если такие сообщения повторяются для разных запросов векторного поиска, это указывает на то, что размер кэша слишком мал.
Кэш индекса векторного сходства хранит гранулы векторного индекса.
Если размер отдельных гранул векторного индекса превышает размер кэша, они не будут кэшироваться.
Поэтому обязательно вычислите размер векторного индекса (по формуле из “Оценка потребления хранилища и памяти” или system.data_skipping_indices) и соответствующим образом подберите размер кэша.
Квантование снижает точность векторного поиска по сравнению с поиском по исходным значениям с полной точностью (
f32) и плавающей запятой.
Однако на большинстве датасетов квантование half-precision brain float (bf16) даёт пренебрежимо малую потерю точности, поэтому индексы векторного сходства по умолчанию используют именно его.
Квантование с четвертной точностью (i8) и двоичное квантование (b1) приводят к заметной потере точности при векторном поиске.
Мы рекомендуем оба этих варианта только в том случае, если размер индекса векторного сходства значительно превышает доступный объём DRAM.
В этом случае мы также рекомендуем включить пересчёт оценок (vector_search_index_fetch_multiplier, vector_search_with_rescoring), чтобы повысить точность.
Двоичное квантование рекомендуется только 1) для нормализованных эмбеддингов (то есть длина вектора = 1; модели OpenAI обычно нормализованы) и 2) если в качестве функции расстояния используется косинусное расстояние.
При построении и поиске в графе близости двоичное квантование внутренне использует расстояние Хэмминга.
На этапе пересчёта оценок используются исходные векторы полной точности, хранящиеся в таблице, чтобы определить ближайших соседей по косинусному расстоянию.
Настройка передачи данных
Опорный вектор в запросе векторного поиска задаётся пользователем и обычно получается вызовом большой языковой модели (LLM).
Типичный код Python, выполняющий векторный поиск в ClickHouse, может выглядеть так
search_v в приведённом выше фрагменте) могут иметь очень большую размерность.
Например, OpenAI предоставляет модели, которые генерируют эмбеддинг-векторы размерностью 1536 или даже 3072.
В приведённом выше коде Python-драйвер ClickHouse подставляет эмбеддинг-вектор в виде человекочитаемой строки, а затем отправляет запрос SELECT целиком как строку.
Если предположить, что эмбеддинг-вектор состоит из 1536 значений с плавающей точкой одинарной точности, длина отправляемой строки достигает 20 кБ.
Это приводит к высокой загрузке процессора из-за токенизации, разбора и тысяч преобразований строк в числа с плавающей точкой.
Кроме того, файл журнала сервера ClickHouse тоже занимает значительный объём, что также вызывает разрастание system.query_log.
Обратите внимание, что большинство LLM-моделей возвращают эмбеддинг-вектор в виде списка или массива NumPy из native float.
Поэтому мы рекомендуем Python-приложениям передавать параметр опорного вектора в бинарной форме, используя следующий стиль:
system.query_log.
Администрирование и мониторинг
Отличия от обычных индексов пропуска данных
GRANULARITY = [N] гранул ([N] = 1 по умолчанию для обычных индексов пропуска данных).
Например, если гранулярность первичного индекса таблицы равна 8192 (настройка index_granularity = 8192) и GRANULARITY = 2, то каждый индексируемый блок будет содержать 16384 строки.
Однако структуры данных и алгоритмы приблизительного поиска ближайших соседей по своей природе ориентированы на строки.
Они хранят компактное представление набора строк, а также возвращают строки для запросов векторного поиска.
Из-за этого в поведении индексов векторного сходства возникают довольно неочевидные отличия по сравнению с обычными индексами пропуска данных.
Когда пользователь определяет индекс векторного сходства для столбца, ClickHouse внутренне создает «подиндекс» векторного сходства для каждого индексного блока.
Подиндекс является «локальным» в том смысле, что он знает только о строках своего индексного блока.
В предыдущем примере, если предположить, что столбец содержит 65536 строк, мы получим четыре индексных блока (охватывающих восемь гранул) и по одному подиндексу векторного сходства для каждого индексного блока.
Теоретически подиндекс способен напрямую вернуть строки с N ближайшими точками в пределах своего индексного блока.
Однако, поскольку ClickHouse загружает данные с диска в память с точностью до гранул, подиндексы экстраполируют совпадающие строки до уровня гранул.
Это отличается от обычных индексов пропуска данных, которые пропускают данные на уровне индексных блоков.
Параметр GRANULARITY определяет, сколько подиндексов векторного сходства будет создано.
Чем больше значение GRANULARITY, тем меньше создается подиндексов векторного сходства, но тем они крупнее, вплоть до случая, когда у столбца (или части данных столбца) имеется только один подиндекс.
В этом случае подиндекс имеет «глобальное» представление обо всех строках столбца и может напрямую вернуть все гранулы столбца (части), содержащие релевантные строки (таких гранул не более LIMIT [N]).
На втором этапе ClickHouse загрузит эти гранулы и определит действительно лучшие строки, выполнив полный перебор с вычислением расстояния для всех строк в гранулах.
При небольшом значении GRANULARITY каждый из подиндексов возвращает до LIMIT N гранул.
В результате приходится загружать больше гранул и выполнять дополнительную постфильтрацию.
Обратите внимание, что точность поиска в обоих случаях одинакова, различается только производительность обработки.
Обычно для индексов векторного сходства рекомендуется использовать большое значение GRANULARITY, а к меньшим значениям GRANULARITY прибегать только при возникновении проблем, например чрезмерного потребления памяти структурами векторного сходства.
Если для индексов векторного сходства GRANULARITY не указана, по умолчанию используется значение 100 миллионов.
Пример
Query
Response
Квантованный бит (QBit)
Array(BFloat16) вместо Array(Float32), объём данных уменьшается вдвое, и время выполнения запросов, как ожидается, сокращается пропорционально.
Этот метод называется квантованием. Хотя он ускоряет вычисления, точность результатов может снижаться, несмотря на полный перебор всех векторов.
При традиционном квантовании мы теряем точность и во время поиска, и при хранении данных. В примере выше мы бы хранили BFloat16 вместо Float32, а значит, позже уже не смогли бы выполнить более точный поиск, даже если бы захотели. Один из альтернативных подходов — хранить две копии данных: квантованную и с полной точностью. Хотя это работает, такой подход требует избыточного хранения. Рассмотрим сценарий, в котором исходные данные имеют формат Float64, а мы хотим выполнять поиск с разной точностью (16 бит, 32 бита или полные 64 бита). В этом случае пришлось бы хранить три отдельные копии данных.
ClickHouse предлагает тип данных Quantized Bit (QBit), который решает эти проблемы за счёт следующего:
- Хранения исходных данных с полной точностью.
- Возможности задавать точность квантования во время выполнения запроса.
QBit, используйте следующий синтаксис:
element_type– тип каждого элемента вектора. Поддерживаемые типы:BFloat16,Float32иFloat64dimension– количество элементов в каждом векторе
Создание таблицы QBit и добавление в неё данных
Векторный поиск с QBit
QBit приведены здесь.
Поиск с полной точностью (64 бита):
Особенности производительности
QBit с точки зрения производительности связано со снижением числа операций I/O: при меньшей точности из хранилища нужно считывать меньше данных. Кроме того, если QBit содержит данные Float32 и параметр точности равен 16 или меньше, дополнительный выигрыш достигается за счет сокращения объема вычислений. Параметр точности напрямую задает компромисс между точностью и скоростью:
- Более высокая точность (ближе к исходной разрядности данных): более точные результаты, но запросы выполняются медленнее
- Более низкая точность: более быстрые запросы с приближенными результатами, меньшее использование памяти