- Точный векторный поиск вычисляет расстояние между заданной точкой и всеми точками векторного пространства. Это обеспечивает максимально возможную точность, то есть возвращённые точки гарантированно являются истинными ближайшими соседями. Поскольку векторное пространство просматривается полностью, точный векторный поиск может быть слишком медленным для практического применения.
- Приближённый векторный поиск — это группа методов (например, специальные структуры данных, такие как графы и случайные леса), которые позволяют получать результаты гораздо быстрее, чем точный векторный поиск. Точность результата обычно является “достаточно хорошей” для практического использования. Многие приближённые методы предоставляют параметры для настройки компромисса между точностью результата и временем поиска.
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 наиболее похожих векторов для заданного опорного вектора.
При настройке 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, основная нагрузка приходится на I/O подсистемы хранения, поскольку с диска (или из Объектного хранилища) приходится читать весь столбец векторов — а для плотного столбца эмбеддингов это самый большой столбец в таблице, который к тому же плохо сжимается.
Кодек столбца Quantized решает эту проблему.
Каждый вектор хранится дважды: исходные значения полной точности, без изменений, и вместе с ними компактный квантованный код в отдельном потоке.
Запрос векторного поиска сначала сканирует коды с помощью недорогой функции расстояния, хорошо подходящей для SIMD, чтобы собрать shortlist наиболее перспективных кандидатов, а затем заново ранжирует этот shortlist по векторам полной точности.
Поскольку код занимает лишь часть размера исходного вектора, сканирование shortlist считывает из хранилища значительно меньше байтов — и обращается к столбцу полной точности только для небольшого числа кандидатов из shortlist — при этом итоговое ранжирование остаётся точным.
Объявление кодека
Quantized(...) к столбцу типа Array(Float32) (или Array(Float64) / Array(BFloat16)).
Кодек экспериментальный, поэтому сначала включите allow_experimental_codecs:
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 оптимизатор автоматически переписывает запрос в двухстадийный план: он сканирует квантизованные коды, чтобы сформировать shortlist, а затем заново оценивает shortlist по vec с полной точностью.
Этот параметр по умолчанию отключён, поэтому без него тот же запрос выполняется как обычное точное сканирование — кодек никогда не меняет результаты, а лишь даёт более быстрый путь, если вы явно его включите.
Используйте функцию расстояния, которую поддерживает выбранный метод: cosineDistance для всех методов, L2Distance дополнительно для int8, prefix и product.
Настройки
allow_experimental_codecs— должен быть включен, чтобы можно было объявить кодекQuantized(по умолчанию:0).vector_search_use_quantized_codes— включает двухстадийное преобразование с отбором shortlist и пересчётом оценок (по умолчанию:0). Если параметр выключен, поисковые запросы выполняют точное сканирование векторов с полной точностью.vector_search_index_fetch_multiplier— сколько кандидатов включать в shortlist относительноLIMITзапроса: при сканировании сохраняются лучшиеLIMIT × multiplierкодов перед пересчётом оценок. Чем больше значение, тем выше полнота, но тем больше объём пересчёта оценок. Значение по умолчанию —1(без избыточной выборки), поэтому для хорошей полноты его обычно нужно увеличить, например до10или выше.
Рассчитано на масштабирование
- Векторизовано. Ядра сканирования написаны под SIMD, с выбором во время выполнения самых широких инструкций, которые поддерживает CPU: аппаратный
popcountдля методов sign-code (rabitq,turboquant) и широкие инструкции fused-multiply-add для остальных. - Параллельно по ядрам и частям. Линейное сканирование легко распараллеливается, и ClickHouse именно так его и обрабатывает: расстояния вычисляются сразу во всех доступных потоках и по всем частям таблицы, и только финальное слияние top-
kвыполняется последовательно. - Распределённо. В сегментированном кластере работа распределяется по машинам — каждый сегмент параллельно сканирует свою часть данных, а координатор объединяет shortlist’ы.
- Столбцовый формат и удобная фильтрация. Квантованные коды занимают отдельный столбец, сжимаются и читаются по тому же пути I/O, что и любой другой столбец, поэтому выборочное
WHEREпросто оставляет меньше кодов для сканирования. - Без отдельного этапа построения. Коды создаются по мере записи векторов и объединяются конкатенацией — индекс не нужно строить, настраивать или перестраивать, поэтому таблица готова к поиску сразу после поступления данных.
Квантованный бит (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 или меньше, дополнительный выигрыш достигается за счет сокращения объема вычислений. Параметр точности напрямую задает компромисс между точностью и скоростью:
- Более высокая точность (ближе к исходной разрядности данных): более точные результаты, но запросы выполняются медленнее
- Более низкая точность: более быстрые запросы с приближенными результатами, меньшее использование памяти