Векторный поиск является важнейшим компонентом инструментов генеративного искусственного интеллекта, поскольку технология дополненной генерации (RAG)
В 2023 году произошел взрывной рост количества продуктов и проектов векторного поиска, поэтому выбор среди них требует серьезных усилий. Изучая варианты, вам нужно будет рассмотреть следующие сложные проблемы и различные подходы к их решению. Здесь я расскажу вам об этих проблемах и опишу, как DataStax решил их при реализации векторного поиска для DataStax Astra DB и Apache Cassandra.
В основе этих сложных проблем лежит то, что исследователи называют «
Многие алгоритмы векторного поиска были разработаны для наборов данных, которые помещаются в память одной машины, и это до сих пор единственный сценарий, протестированный
Как и в любом другом домене, горизонтальное масштабирование требует как репликации, так и секционирования, а также подсистем для замены мертвых реплик, их восстановления после сетевого раздела и т. д.
Для нас это было несложно: масштабируемая репликация — это хлеб с маслом Cassandra, и объединение ее с новым SAI в Cassandra-5.0 (индексирование с подключением к хранилищу — см.
Под «сборкой мусора» я подразумеваю удаление устаревшей информации из индекса — очистку удаленных строк и работу со строками, значение индексированного вектора которых изменилось. Может показаться, что об этом не стоит упоминать – эта проблема в мире реляционных баз данных была более или менее решена на протяжении сорока лет – но векторные индексы уникальны.
Ключевая проблема заключается в том, что все известные нам алгоритмы, которые обеспечивают как поиск с малой задержкой, так и результаты с высокой запоминаемостью, основаны на графах. Доступно множество других алгоритмов индексации векторов:
Проблема с индексами графов заключается в том, что вы не можете просто вырвать старый (связанный с вектором) узел при изменении строки или документа; если вы сделаете это более нескольких раз, ваш граф больше не сможет выполнять свою задачу — направлять BFS (поиск в ширину) к областям, более похожим на вектор запроса.
Итак, в какой-то момент вам придется перестроить индексы, чтобы выполнить эту сборку мусора, но когда и как ее организовать? Если вы всегда перестраиваете все при внесении изменений, вы значительно увеличиваете количество выполняемых физических операций записи; это называется усилением записи. С другой стороны, если вы никогда не перестраиваете, вам придется выполнять дополнительную работу по фильтрации устаревших строк во время запроса («усиление чтения»).
Это еще одно проблемное пространство, над которым Кассандра работает уже много лет. Поскольку индексы SAI привязаны к жизненному циклу основного хранилища, они также участвуют в Cassandra.
DataStax Astra DB построен на основе Apache Cassandra и предоставляет платформу для рабочих нагрузок облачных приложений.
Это означает, что рабочие нагрузки:
Массовое одновременное выполнение тысяч и миллионов запросов в секунду, обычно для получения всего нескольких строк за штуку. Вот почему вы не могли бы запустить Netflix на Snowflake, даже если бы могли себе это позволить: Snowflake и подобные аналитические системы предназначены для обработки лишь нескольких одновременных запросов, каждый из которых выполняется от многих секунд до минут или даже дольше.
Больше, чем память Если ваш набор данных умещается в памяти на одном компьютере, практически не имеет значения, какие инструменты вы используете. Sqlite, MongoDB, MySQL — все они будут работать нормально. Когда это не так, ситуация усложняется — и плохая новость заключается в том, что векторные вложения обычно занимают несколько КБ, или примерно на порядок больше, чем типичные документы базы данных, поэтому вы относительно быстро доберетесь до размеров, превышающих объем памяти.
Ядро приложения Если вас не волнует, потеряете ли вы свои данные, либо потому, что это не так важно, либо потому, что вы можете восстановить их из фактического источника записи, то опять же не имеет значения, какие инструменты вы используете. Такие базы данных, как Cassandra и Astra DB, созданы для того, чтобы ваши данные оставались доступными и надежными, несмотря ни на что.
Выше я упоминал, что известный
Связанная с этим проблема заключается в том, что ann-benchmarks одновременно выполняет только один тип операций: сначала он создает индекс, затем запрашивает его. Работа с обновлениями, чередующимися с поиском, не является обязательной и, вероятно, даже помехой; если вы знаете, что вам не нужно иметь дело с обновлениями, вы можете сделать множество упрощающих предположений, которые хорошо смотрятся на искусственных тестах.
Если вас волнует возможность выполнять несколько одновременных операций с вашим индексом или обновлять его после создания, вам нужно посмотреть немного глубже, чтобы понять, как это работает и какие компромиссы при этом возникают.
Во-первых, все известные мне векторные базы данных общего назначения используют индексы на основе графов. Это потому, что вы можете начать запрашивать индекс графа, как только будет вставлен первый вектор. Большинство других вариантов требуют, чтобы вы либо построили весь индекс перед его запросом, либо, по крайней мере, выполнили предварительное сканирование данных, чтобы узнать некоторые статистические свойства.
Однако даже в категории индексов графов все еще существуют важные детали реализации. Например, сначала мы думали, что сможем сэкономить время, используя реализацию индекса HNSW от Lucene, как это делают MongoDB Elastic и Solr. Но мы быстро поняли, что Lucene предлагает только однопоточное непараллельное построение индексов. То есть вы не можете ни запрашивать ее во время ее построения (что должно быть одной из основных причин использования этой структуры данных!), ни позволять нескольким потокам создавать ее одновременно.
В документе HNSW предполагается, что подход с детальной блокировкой может работать, но мы пошли еще дальше и создали неблокирующий индекс. Это открытый исходный код в
JVector линейно масштабирует одновременные обновления как минимум до 32 потоков. Этот график имеет логарифмическое масштабирование по осям X и Y, показывая, что удвоение числа потоков сокращает время сборки вдвое.
Что еще более важно, неблокирующий параллелизм JVector способствует более реалистичным рабочим нагрузкам за счет сочетания поиска с обновлениями. Вот сравнение производительности Astra DB (с использованием JVector) по сравнению с Pinecone в разных наборах данных. Хотя Astra DB примерно на 10% быстрее, чем Pinecone, для статического набора данных, она в 8–15 раз быстрее, а также индексирует новые данные. Мы выбрали лучший доступный уровень модулей с Pinecone (тип модуля: p2 и размер модуля: x8, с 2 модулями на реплику), основываясь на их рекомендациях по более высокой пропускной способности и меньшей задержке. Сосновая шишка не раскрывает, каким физическим ресурсам это соответствует. Что касается Astra DB, мы выбрали модель развертывания PAYG по умолчанию, и нам не пришлось беспокоиться о выборе ресурсов, поскольку она бессерверная. Испытания проводились с использованием
Astra DB делает это, сохраняя при этом более высокую полноту и точность (
Мы начали с
Индекс HNSW представляет собой серию слоев, где каждый уровень над базовым слоем примерно на 10 % превышает количество узлов предыдущего. Это позволяет верхним слоям действовать как список пропуска, позволяя поиску сосредоточиться на правой окрестности нижнего слоя, который содержит все векторы.
Однако такая конструкция означает, что (как и во всех индексах графа) вам не сойдет с рук фраза: «Дисковый кеш нас спасет», потому что, в отличие от обычных рабочих нагрузок запросов к базе данных, каждый вектор в графе имеет почти равную вероятность быть релевантным для поиска. (Исключением являются верхние уровни, которые мы можем и кэшируем.)
Вот как выглядел профиль обслуживания 100-мегабайтного набора векторных данных фрагментов статей Википедии (около 120 ГБ на диске) на моем настольном компьютере с 64 ГБ ОЗУ, когда мы использовали Lucene:
Кассандра почти все время проводит в ожидании чтения векторов с диска.
Чтобы решить эту проблему, мы реализовали более продвинутый алгоритм под названием DiskANN и открыли его исходный код как отдельную встроенную векторную поисковую систему.
Вот как выглядит сравнение HNSW и DiskANN в чисто встроенном сценарии без компонентов клиент/сервер. Это измеряет скорость поиска в наборе данных Deep100M с помощью Lucene (HNSW) и JVector (DiskANN). Индекс Lucene составляет 55 ГБ, включая индекс и необработанные векторы. Индекс JVector составляет 64 ГБ. Поиск проводился на моем MacBook емкостью 24 ГБ, объем памяти которого составляет примерно одну треть от объема, необходимого для хранения индекса в оперативной памяти.
Компонуемость в контексте систем баз данных означает способность плавно интегрировать различные функции и возможности последовательным образом. Это особенно важно при обсуждении интеграции новой категории возможностей, такой как векторный поиск. Неигрушечные приложения всегда потребуют как классических
Рассмотрим простой
В более реалистичном варианте использования один из наших инженеров по решениям недавно работал с компанией в Азии, которая хотела добавить семантический поиск в свой каталог продуктов, но также хотела включить сопоставление на основе терминов. Например, если пользователь ищет [шаровой кран «красный»], то он хочет ограничить поиск элементами, описание которых соответствует термину «красный», независимо от того, насколько семантически схожи векторные вложения. Тогда ключевой новый запрос (помимо классических функций, таких как управление сеансами, история заказов и обновления корзины покупок) заключается в следующем: Ограничить продукты теми, которые содержат все цитируемые термины, а затем найти наиболее похожий на поиск пользователя.
Этот второй пример ясно показывает, что приложениям не только необходимы как классические функции запросов, так и векторный поиск, но им часто необходимо иметь возможность использовать элементы каждого из них в одном и том же запросе.
Текущий уровень развития этой молодой области состоит в том, чтобы попытаться выполнить то, что я называю классическими запросами в «обычной» базе данных, векторными запросами в векторной базе данных, а затем сшить их вместе специальным образом, когда оба будут требуется одновременно. Это подвержено ошибкам, медленно и дорого; его единственное достоинство в том, что вы можете заставить его работать до тех пор, пока не найдете лучшее решение.
В Astra DB мы создали (и открыли исходный код) лучшее решение на базе Cassandra SAI. Поскольку SAI позволяет создавать собственные типы индексов, которые привязаны к стабильному жизненному циклу Cassandra и жизненному циклу сжатия, Astra DB легко позволяет разработчикам смешивать и сопоставлять логические предикаты, поиск на основе терминов и векторный поиск без каких-либо дополнительных затрат. управление и синхронизация отдельных систем. Это дает разработчикам, создающим генеративные приложения искусственного интеллекта, более сложные возможности запросов, которые повышают производительность и ускоряют выход на рынок.
Векторные поисковые системы — это важная новая функция базы данных, имеющая множество архитектурных проблем, включая масштабирование, сборку мусора, параллелизм, эффективное использование диска и возможность компоновки. Я считаю, что при создании векторного поиска для Astra DB мы смогли использовать возможности Cassandra, чтобы предоставить лучшие в своем классе возможности для разработчиков генеративных приложений искусственного интеллекта.
- Джонатан Эллис, DataStax