- Точный векторный поиск вычисляет расстояние между заданной точкой и всеми точками векторного пространства. Это обеспечивает максимально возможную точность, то есть возвращённые точки гарантированно являются истинными ближайшими соседями. Поскольку векторное пространство просматривается полностью, точный векторный поиск может быть слишком медленным для практического применения.
- Приближённый векторный поиск — это группа методов (например, специальные структуры данных, такие как графы и случайные леса), которые позволяют получать результаты гораздо быстрее, чем точный векторный поиск. Точность результата обычно является “достаточно хорошей” для практического использования. Многие приближённые методы предоставляют параметры для настройки компромисса между точностью результата и временем поиска.
vectors типа Array(Float64), Array(Float32) или Array(BFloat16).
Для практического использования мы обычно рекомендуем массивы 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 наиболее похожих векторов для заданного опорного вектора.
При настройке vector_search_with_rescoring = 1 ClickHouse считывает исходные векторы полной точности для строк-кандидатов и вычисляет итоговое расстояние в обычном SQL-конвейере.
Когда это допускает план запроса, ClickHouse перед итоговым вычислением расстояния ограничивает сканирование строками-кандидатами, возвращёнными векторным индексом.
Этот шаг называется пересчётом оценок и может повысить точность, особенно при использовании квантизованных векторных индексов, поскольку итоговое ранжирование использует сохранённые векторы, а не расстояния из индекса.
Если дополнительные фильтры отбрасывают слишком много кандидатов или требуется более высокая полнота, увеличьте значение настройки vector_search_index_fetch_multiplier, чтобы векторный индекс возвращал больше строк-кандидатов для пересчёта оценок.
Поэтому ClickHouse предоставляет оптимизацию, которая отключает пересчёт оценок и возвращает наиболее похожие векторы и расстояния до них напрямую из индекса.
Эта оптимизация включена по умолчанию, см. настройку vector_search_with_rescoring.
В общих чертах это работает так: ClickHouse делает наиболее похожие векторы и расстояния до них доступными в виде виртуального столбца _distance.
Чтобы увидеть это, выполните запрос векторного поиска с 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 ближайшими точками в пределах своего индексного блока.
Для запросов с vector_search_with_rescoring = 1 ClickHouse может использовать эти позиции строк для фильтрации строк перед вычислением итогового расстояния по сохранённым векторам, когда план запроса допускает такую оптимизацию.
Без пересчёта оценок ClickHouse использует расстояния из векторного индекса напрямую через виртуальный столбец _distance.
В обоих режимах для планирования чтения всё равно используются окружающие диапазоны гранул, что отличается от обычных индексов пропуска данных, которые пропускают данные на уровне индексных блоков.
Параметр GRANULARITY определяет, сколько подиндексов векторного сходства будет создано.
Чем больше значение GRANULARITY, тем меньше создается подиндексов векторного сходства, но тем они крупнее, вплоть до случая, когда у столбца (или части данных столбца) имеется только один подиндекс.
В этом случае подиндекс имеет «глобальное» представление обо всех строках столбца и может напрямую вернуть все гранулы столбца (части), содержащие релевантные строки (таких гранул не более LIMIT [N]).
При vector_search_with_rescoring = 1 ClickHouse затем может прочитать позиции совпадающих строк и вычислить точное расстояние для этих строк.
При небольшом значении GRANULARITY каждый подиндекс может вернуть до LIMIT N строк-кандидатов.
В результате может потребоваться прочитать больше строк-кандидатов и выполнить дополнительную постфильтрацию.
Обратите внимание, что точность поиска в обоих случаях одинакова, различается только производительность обработки.
Обычно для индексов векторного сходства рекомендуется использовать большое значение GRANULARITY, а к меньшим значениям GRANULARITY прибегать только при возникновении проблем, например чрезмерного потребления памяти структурами векторного сходства.
Если для индексов векторного сходства GRANULARITY не указана, по умолчанию используется значение 100 миллионов.
Пример
Query
Response
Векторный поиск с квантизованными кодеками
Quantized, сначала выполните SET allow_experimental_codecs = 1.
Если у вас возникнут проблемы, пожалуйста, создайте issue в репозитории ClickHouse.
Введение
- Высокая стоимость построения. Построение индексов векторного сходства — дорогостоящий процесс.
- Высокое потребление памяти. Индексы векторного сходства потребляют значительный объём памяти и вместе с самими векторами становятся основной статьёй затрат.
- Фильтрация. При селективном фильтре
WHEREобход графа становится неэффективным: он либо не может достичь небольшого набора строк, удовлетворяющих предикату, либо вынужден проверять непропорционально большое число кандидатов, чтобы найти их.
Float32 или BFloat16, преобладают операции I/O, поскольку с диска необходимо загрузить весь столбец векторов.
Этот недостаток устраняет столбцовый кодек Quantized.
Он хранит каждый вектор дважды: исходные значения с полной точностью и компактное квантованное представление.
Запрос векторного поиска сначала сканирует квантованные коды, используя недорогую SIMD-оптимизированную функцию расстояния, чтобы сформировать шорт-лист наиболее перспективных кандидатов.
На втором этапе эти кандидаты повторно ранжируются относительно векторов с полной точностью.
Первоначальное сканирование квантованных кодов считывает из хранилища значительно меньше байтов, чем сканирование исходных векторов.
Кодек хорошо подходит для ClickHouse, поскольку дорогостоящая часть — сканирование — именно то, для чего создан движок ClickHouse:
- Векторизация. Ядра сканирования используют SIMD-инструкции для снижения потребления CPU.
- Параллельность между ядрами и частями. Сканирование легко распараллеливается: расстояния одновременно вычисляются во всех доступных потоках и по всем частям таблицы.
- Распределённость. В кластере с сегментированием работа распределяется между машинами: каждый сегмент параллельно сканирует свою часть данных, а координатор объединяет шорт-листы.
- Без дополнительных затрат на построение. Квантованные коды создаются при записи векторов. Не требуется создавать, настраивать или перестраивать дополнительный индекс, поэтому таблица готова к поиску сразу после поступления данных.
Применение кодека
Quantized(...) для столбца Array(Float32) (или Array(Float64) / Array(BFloat16)).
ALTER TABLE.
Методы квантования
dimensions задаёт длину вектора.
Quantized('rabitq', dimensions)— один знаковый бит на координату плюс несмещённый коэффициент коррекции косинуса (dimensions/8 + 4байта). Небольшой, быстрый дляpopcount, хороший вариант по умолчанию. ТолькоcosineDistance.Quantized('turboquant', dimensions)— два бита на координату (1-битный код MSE и 1-битный код остатка) для кандидатов с более высокой точностью (dimensions/4 + 4байта). ТолькоcosineDistance.Quantized('int8', dimensions)— один кодInt8на координату плюс норма вектора (dimensions + 4байта); самый крупный, но и наиболее точно передающий значения плоский код. ПоддерживаетL2DistanceиcosineDistance.Quantized('prefix', dimensions, leading_dimensions, 'int8'|'bf16')— Matryoshka: сохраняет только первыеleading_dimensionsкоординат в форматеInt8(с масштабом для каждого вектора) илиBFloat16. Очень компактные коды для эмбеддингов, обученных с использованием Matryoshka Representation Learning. ПоддерживаетL2DistanceиcosineDistance.Quantized('product', dimensions, nbits, m)— Product Quantization: кодовая книга для каждой части, обученная методом k-means; каждый вектор преобразуется вmкодов поnbitsбит (поэтомуdimensionsдолжно быть кратноm). Самый компактный вариант и максимальная полнота на байт, но требует этапа обучения во время вставки. ПоддерживаетL2DistanceиcosineDistance.
rabitq и turboquant значение dimensions должно быть кратно 8.
Использование кодека
k (см. точный поиск):
vector_search_use_quantized_codes = 1 позволяет оптимизатору преобразовать запрос в двухфазное сканирование.
По умолчанию настройка отключена.
Без неё тот же запрос выполняется как обычное точное сканирование по исходным векторам.
Все методы поддерживают функцию расстояния cosineDistance; методы int8, prefix и product дополнительно поддерживают функцию расстояния L2Distance.
Настройка vector_search_index_fetch_multiplier определяет, сколько кандидатов попадает в шорт-лист относительно LIMIT запроса.
Бо́льшие множители повышают полноту ценой дополнительного пересчёта оценок.
Значение по умолчанию — 1 (без оверсэмплинга); для достижения хорошей полноты обычно требуется увеличить его, например до 10.
Квантованный бит (QBit)
Array(BFloat16) вместо Array(Float32), объём данных уменьшается вдвое, и время выполнения запросов, как ожидается, сокращается пропорционально.
Этот метод называется квантованием. Хотя он ускоряет вычисления, точность результатов может снижаться, несмотря на полный перебор всех векторов.
При традиционном квантовании мы теряем точность и во время поиска, и при хранении данных. В примере выше мы бы хранили BFloat16 вместо Float32, а значит, позже уже не смогли бы выполнить более точный поиск, даже если бы захотели. Один из альтернативных подходов — хранить две копии данных: квантованную и с полной точностью. Хотя это работает, такой подход требует избыточного хранения. Рассмотрим сценарий, в котором исходные данные имеют формат Float64, а мы хотим выполнять поиск с разной точностью (16 бит, 32 бита или полные 64 бита). В этом случае пришлось бы хранить три отдельные копии данных.
ClickHouse предлагает тип данных Quantized Bit (QBit), который решает эти проблемы за счёт следующего:
- Хранения исходных данных с полной точностью.
- Возможности задавать точность квантования во время выполнения запроса.
QBit, используйте следующий синтаксис:
element_type– тип каждого элемента вектора. Поддерживаемые типы:Int8,BFloat16,Float32иFloat64dimension– количество элементов в каждом вектореstride– необязательно. Делительdimension, который разбивает размерности наdimension / strideсмежных групп, хранящихся в отдельных потоках, так что при поиске только по первым размерностям считывается меньше потоков (полезно для эмбеддинг-векторов Matryoshka). По умолчанию используетсяdimension; в этом случае тип побайтно идентиченQBitбезstride. Подробности см. на странице типа данныхQBit.
Создание таблицы QBit и добавление в неё данных
Векторный поиск с QBit
QBit приведены здесь.
Поиск с полной точностью (64 бита):
Особенности производительности
QBit с точки зрения производительности связано со снижением числа операций I/O: при меньшей точности из хранилища нужно считывать меньше данных. Кроме того, если QBit содержит данные Float32 и параметр точности равен 16 или меньше, дополнительный выигрыш достигается за счет сокращения объема вычислений. Параметр точности напрямую задает компромисс между точностью и скоростью:
- Более высокая точность (ближе к исходной разрядности данных): более точные результаты, но запросы выполняются медленнее
- Более низкая точность: более быстрые запросы с приближенными результатами, меньшее использование памяти