Перейти к основному содержанию
В этом руководстве вы:
  • Кратко познакомитесь с векторным поиском
  • Узнаете о приближённом поиске ближайших соседей (ANN) и Hierarchical Navigable Small World (HNSW)
  • Узнаете о Quantised Bit (QBit)
  • Используете QBit для векторного поиска на наборе данных DBPedia

Кратко о векторном поиске

В математике и физике вектор формально определяется как объект, имеющий и величину, и направление. Обычно он изображается как отрезок или стрелка в пространстве и может использоваться для представления таких величин, как скорость, сила и ускорение. В информатике вектор — это конечная последовательность чисел. Иными словами, это структура данных для хранения числовых значений. В машинном обучении векторы — это те же структуры данных, о которых говорят в информатике, но хранящиеся в них числовые значения имеют особый смысл. Когда мы берем блок текста или изображение и сводим их к ключевым понятиям, которые они выражают, этот процесс называется кодированием. Полученный результат — это машинное представление этих ключевых понятий в числовой форме. Это называется эмбеддингом и хранится в векторе. Иначе говоря, когда контекстный смысл закодирован в векторе, такой вектор можно назвать эмбеддингом. Векторный поиск сейчас используется повсюду. Он лежит в основе музыкальных рекомендаций, retrieval-augmented generation (RAG) для больших языковых моделей, где для улучшения ответов извлекаются внешние знания, и даже поиск Google в некоторой степени опирается на векторный поиск. Пользователи часто предпочитают обычные базы данных с возможностями векторного поиска узкоспециализированным векторным хранилищам, несмотря на преимущества последних. ClickHouse поддерживает поиск по векторам полным перебором, а также методы приблизительного поиска ближайших соседей (ANN), включая HNSW — текущий стандарт быстрого поиска по векторам.

Что такое эмбеддинги

Давайте рассмотрим простой пример, чтобы понять, как работает векторный поиск. Возьмём эмбеддинги (векторные представления) слов: Создайте таблицу ниже с несколькими эмбеддингами для примера:
Вы можете найти слова, наиболее близкие к заданному эмбеддингу:
Эмбеддинг запроса ближе всего к “apple” (расстояние минимально), что вполне логично, если посмотреть на оба эмбеддинга рядом:

Приближённый поиск ближайших соседей (ANN)

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

Квантизация

Квантизация подразумевает приведение к числовым типам меньшего размера. Чем меньше числа, тем меньше объём данных, а чем меньше объём данных, тем быстрее вычисляются расстояния. Движок векторизованного выполнения запросов ClickHouse может за одну операцию размещать больше значений в регистрах процессора, что напрямую повышает пропускную способность. У вас есть два варианта:
  1. Хранить квантизованную копию рядом с исходным столбцом - Это удваивает объём хранилища, зато безопасно: мы всегда можем вернуться к полной точности
  2. Полностью заменить исходные значения (за счёт приведения при вставке) - Это экономит место и I/O, но обратного пути уже не будет

Hierarchical Navigable Small World (HNSW)

HNSW состоит из нескольких слоёв узлов (векторов). Каждый узел случайным образом назначается одному или нескольким слоям, при этом вероятность попасть на более высокий слой экспоненциально уменьшается. При поиске мы начинаем с узла на верхнем слое и жадно движемся к ближайшим соседям. Когда найти более близкий узел уже не удаётся, мы спускаемся на следующий, более плотный слой. Благодаря такой многоуровневой структуре HNSW обеспечивает логарифмическую сложность поиска относительно числа узлов.
Ограничение HNSWОсновное узкое место — память. ClickHouse использует реализацию HNSW usearch, которая представляет собой структуру данных в оперативной памяти и не поддерживает разбиение. В результате более крупные датасеты требуют пропорционально большего объёма оперативной памяти.

Сравнение подходов

Подробно о QBit

Quantised Bit (QBit)

QBit — это новая структура данных, способная хранить значения BFloat16, Float32 и Float64, используя особенности представления чисел с плавающей запятой — в виде битов. Вместо хранения каждого числа целиком QBit разбивает значения на битовые плоскости: все первые биты, все вторые биты, все третьи биты и так далее. Этот подход устраняет главное ограничение традиционного квантования. Не нужно хранить дублирующиеся данные или рисковать тем, что значения потеряют смысл. Он также позволяет избежать узких мест HNSW, связанных с оперативной памятью, поскольку QBit работает напрямую с хранимыми данными, а не поддерживает индекс в памяти.
ПреимуществоЧто особенно важно, не нужно принимать решения заранее. Точность и производительность можно динамически настраивать во время выполнения запроса, что позволяет пользователям с минимальными усилиями находить баланс между точностью и скоростью.
ОграничениеХотя QBit ускоряет векторный поиск, его вычислительная сложность по-прежнему составляет O(n). Иными словами, если ваш набор данных достаточно мал и индекс HNSW без проблем помещается в оперативной памяти, это всё ещё самый быстрый вариант.

Тип данных

Ниже показано, как создать столбец QBit:
При вставке данных в столбец QBit они транспонируются так, чтобы все первые биты располагались вместе, все вторые — вместе и так далее. Мы называем их группами. Каждая группа хранится в отдельном столбце FixedString(N): в виде строк фиксированной длины по N байт, последовательно расположенных в памяти без разделителей между ними. Затем все такие группы объединяются в один Tuple, который лежит в основе структуры QBit. Пример: Если взять вектор из 8 элементов Float64, каждая группа будет содержать 8 бит. Поскольку Float64 состоит из 64 бит, в итоге получается 64 группы (по одной на каждый бит). Следовательно, внутренняя структура QBit(Float64, 8) выглядит как Tuple из 64 столбцов FixedString(1).
Если длина исходного вектора не делится на 8 без остатка, структура дополняется невидимыми элементами, чтобы длина стала кратной 8. Это обеспечивает совместимость с FixedString, который работает строго с полными байтами.

Вычисление расстояния

Чтобы выполнять запросы с помощью QBit, используйте функцию L2DistanceTransposed с параметром точности:
Третий параметр (16) задаёт точность в битах.

Оптимизация I/O

Прежде чем вычислять расстояния, нужно прочитать необходимые данные с диска, а затем выполнить обратное транспонирование (то есть преобразовать сгруппированное битовое представление обратно в полные векторы). Поскольку QBit хранит значения в битово-транспонированном виде по уровням точности, ClickHouse может считывать только те старшие битовые плоскости, которые нужны для восстановления чисел с требуемой точностью. В запросе выше мы используем уровень точности 16. Поскольку Float64 состоит из 64 бит, мы считываем только первые 16 битовых плоскостей, пропуская 75 % данных. После чтения мы восстанавливаем только старшую часть каждого числа по загруженным битовым плоскостям, а непрочитанные биты оставляем обнулёнными.

Оптимизация вычислений

Можно задаться вопросом, позволит ли приведение к меньшему типу, например Float32 или BFloat16, избавиться от этой неиспользуемой части. Это действительно работает, но явные приведения слишком затратны, если выполнять их для каждой строки. Вместо этого можно понизить тип только для опорного вектора и обрабатывать данные QBit так, как будто они содержат значения более узких типов («забывая» о существовании некоторых столбцов), поскольку их структура часто соответствует усечённой версии этих типов.

Оптимизация BFloat16

BFloat16 — это Float32, усечённый вдвое. Он сохраняет тот же знаковый бит и 8-битный экспонент, но только старшие 7 бит из 23-битной мантиссы. Благодаря этому чтение первых 16 битовых плоскостей из столбца QBit фактически воспроизводит структуру значений BFloat16. Поэтому в этом случае мы можем безопасно преобразовать эталонный вектор в BFloat16 — и именно так и делаем.

Сложность Float64

Однако с Float64 всё обстоит иначе. Он использует 11-битный порядок и 52-битную мантиссу, то есть это не просто Float32 с вдвое большим количеством битов. Его структура и смещение порядка полностью отличаются. Преобразование Float64 в меньший формат, такой как Float32, требует полноценного преобразования по IEEE-754, при котором каждое значение округляется до ближайшего представимого значения Float32. Этот этап округления требует значительных вычислительных затрат.
Если вас интересует более глубокий разбор аспектов производительности QBit, см. “Let’s vectorize”

Пример с DBpedia

Посмотрим, как QBit работает на реальном примере с использованием набора данных DBpedia, содержащего 1 миллион статей из Википедии, представленных в виде векторов Float32.

Настройка

Сначала создайте таблицу
Вставьте данные через командную строку:
Вставка данных может занять некоторое время. Самое время сделать перерыв на кофе!
Либо можно выполнить отдельные SQL-команды, как показано ниже, чтобы загрузить каждый из 25 файлов Parquet:
Убедитесь, что в таблице dbpedia виден 1 миллион строк:
Затем добавьте столбец QBit:

Поисковый запрос

Мы будем искать понятия, наиболее связанные со всеми поисковыми терминами на космическую тему: Moon, Apollo 11, Space Shuttle, Astronaut, Rocket:
Запрос ищет 1000 записей, наиболее семантически близких к каждому из пяти понятий. Он возвращает записи, которые встречаются как минимум в трёх из этих результатов, ранжируя их по числу понятий, которым они соответствуют, и по минимальному расстоянию до любого из этих понятий (исключая исходные записи). Используя всего 5 битов (1 бит знака + 4 бита экспоненты, нулевая мантисса):
Производительность: 10 строк в результате. Время выполнения: 0.271 сек. Обработано 8.46 млн строк, 4.54 GB (31.19 млн строк/с, 16.75 GB/с.) Пиковое потребление памяти: 739.82 MiB.
Производительность: 10 строк в наборе. Время выполнения: 1,157 сек. Обработано 10,00 млн строк, 32,76 ГБ (8,64 млн строк/с., 28,32 ГБ/с.) Пиковое потребление памяти: 6,05 ГиБ.

Ключевая идея

Каков результат? Не просто хороший. Удивительно хороший. Неочевидно, что числа с плавающей запятой, полностью лишённые мантиссы и половины экспоненты, всё ещё могут сохранять значимую информацию. Ключевая идея QBit в том, что векторный поиск продолжает работать, даже если игнорировать незначащие биты. Использование памяти сократилось с 6.05 GB до 740 MB при сохранении отличного качества семантического поиска!

Заключение

QBit — это тип столбца, который хранит числа с плавающей точкой в виде битовых плоскостей. Он позволяет выбирать, сколько битов считывать при векторном поиске, настраивая полноту и производительность без изменения данных. У каждого метода векторного поиска есть свои параметры, которые задают компромисс между полнотой, точностью и производительностью. Обычно их нужно выбирать заранее. Если ошибиться, можно впустую потратить много времени и ресурсов, а позже сменить подход будет непросто. С QBit принимать такие решения заранее не нужно. Вы можете настраивать компромисс между точностью и скоростью прямо во время выполнения запроса, подбирая нужный баланс по ходу дела.
Адаптировано из поста в блоге Рауфса Дунамалиевса, опубликованного 28 октября 2025 года
Последнее изменение 19 июня 2026 г.