Подведение итогов
В этой статье рассказывается о том, как в Milvus реализована функция удаления. Функция удаления была представлена в Milvus в версии 0.7.0 как долгожданная функция для многих пользователей. Мы не стали вызывать remove_ids в FAISS напрямую, вместо этого мы придумали совершенно новый дизайн, чтобы сделать удаление более эффективным и поддерживать больше типов индексов.
В статье "Как Milvus реализует динамическое обновление данных и запросы" мы представили весь процесс от вставки данных до их удаления. Давайте вспомним некоторые основы. MemManager управляет всеми буферами для вставки данных, при этом каждая MemTable соответствует коллекции (мы переименовали "таблицу" в "коллекцию" в Milvus v0.7.0). Milvus автоматически разделяет данные, вставляемые в память, на несколько MemTableFiles. Когда данные сбрасываются на диск, каждый MemTableFile сериализуется в необработанный файл. Мы сохранили эту архитектуру при разработке функции удаления.
Мы определяем функцию метода delete как удаление всех данных, соответствующих указанным идентификаторам сущностей в определенной коллекции. При разработке этой функции мы разработали два сценария. Первый - удаление данных, которые все еще находятся в буфере вставки, и второй - удаление данных, которые были выгружены на диск. Первый сценарий более интуитивно понятен. Мы можем найти MemTableFile, соответствующий указанному идентификатору, и удалить данные в памяти напрямую (рис. 1). Поскольку удаление и вставка данных не могут выполняться одновременно, а также из-за механизма, который меняет MemTableFile с мутабельного на мутабельный при промывке данных, удаление выполняется только в мутабельном буфере. Таким образом, операция удаления не пересекается с операцией промывки данных, что обеспечивает согласованность данных.
1-delete-request-milvus.jpg
Второй сценарий более сложный, но более распространенный, поскольку в большинстве случаев данные остаются в буфере вставки на короткое время перед тем, как быть выгруженными на диск. Учитывая, что загружать вымытые данные в память для жесткого удаления неэффективно, мы решили применить более эффективный подход - мягкое удаление. Вместо того чтобы удалять данные, выводимые на диск, мягкое удаление сохраняет удаленные идентификаторы в отдельном файле. Таким образом, мы можем отфильтровать удаленные идентификаторы при операциях чтения, например при поиске.
Когда дело доходит до реализации, нам нужно рассмотреть несколько вопросов. В Milvus данные становятся видимыми, или, другими словами, восстанавливаемыми, только когда они стираются на диск. Поэтому сброшенные данные удаляются не во время вызова метода delete, а во время следующей операции flush. Причина в том, что файлы данных, которые были смыты на диск, больше не будут содержать новых данных, поэтому мягкое удаление не влияет на данные, которые были смыты. При вызове функции delete можно непосредственно удалить данные, которые все еще находятся в буфере вставки, в то время как для промытых данных необходимо записать идентификатор удаленных данных в память. При сбросе данных на диск Milvus записывает идентификатор удаленных данных в файл DEL, чтобы записать, какая сущность в соответствующем сегменте была удалена. Эти обновления будут видны только после завершения промывки данных. Этот процесс показан на рисунке 2. До версии 0.7.0 у нас был только механизм автоматической промывки; то есть Milvus сериализовывал данные в буфере вставки каждую секунду. В нашем новом дизайне мы добавили метод flush, позволяющий разработчикам вызывать его после метода delete, гарантируя, что новые вставленные данные будут видны, а удаленные данные уже невозможно будет восстановить.
2-delete-request-milvus.jpg
Вторая проблема заключается в том, что файл необработанных данных и индексный файл - это два отдельных файла в Milvus и две независимые записи в метаданных. При удалении заданного идентификатора нам нужно найти файл исходных данных и индексный файл, соответствующие этому идентификатору, и записать их вместе. Соответственно, мы ввели понятие сегмента. Сегмент содержит необработанный файл (включающий необработанные векторные файлы и файлы идентификаторов), индексный файл и файл DEL. Сегмент - это самая основная единица для чтения, записи и поиска векторов в Milvus. Коллекция (рисунок 3) состоит из нескольких сегментов. Таким образом, под папкой коллекции на диске находится несколько папок с сегментами. Поскольку наши метаданные основаны на реляционных базах данных (SQLite или MySQL), очень просто записать отношения внутри сегмента, и операция удаления больше не требует отдельной обработки исходного файла и индексного файла.
3-delete-request-milvus.jpg
Третий вопрос - как отфильтровать удаленные данные при поиске. На практике идентификатор, записанный DEL, является смещением соответствующих данных, хранящихся в сегменте. Поскольку удаляемый сегмент не содержит новых данных, смещение не изменится. Структура данных DEL представляет собой битовую карту в памяти, где активный бит обозначает удаленное смещение. Мы также соответствующим образом обновили FAISS: при поиске в FAISS вектор, соответствующий активному биту, больше не будет включаться в расчет расстояния (рис. 4). Изменения в FAISS здесь подробно рассматриваться не будут.
4-delete-request-milvus.jpg
Последний вопрос касается повышения производительности. При удалении промытых данных сначала нужно выяснить, в каком сегменте коллекции находится удаляемый идентификатор, а затем записать его смещение. Самый простой подход - перебрать все идентификаторы в каждом сегменте. Оптимизация, о которой мы думаем, заключается в добавлении блум-фильтра в каждый сегмент. Фильтр Блума - это случайная структура данных, используемая для проверки того, является ли элемент членом множества. Поэтому мы можем загружать только фильтр цветения каждого сегмента. Только когда bloom-фильтр определит, что удаленный идентификатор находится в текущем сегменте, мы сможем найти соответствующее смещение в сегменте; в противном случае мы можем игнорировать этот сегмент (рис. 5). Мы выбрали bloom-фильтр, потому что он занимает меньше места и более эффективен в поиске, чем многие его аналоги, такие как хэш-таблицы. Хотя фильтр bloom имеет определенный процент ложных срабатываний, мы можем уменьшить количество сегментов, которые необходимо перебрать, до идеального числа, чтобы скорректировать вероятность. Между тем, фильтр цветения также должен поддерживать удаление. В противном случае идентификатор удаленной сущности все равно может быть найден в цветочном фильтре, что приведет к увеличению числа ложных срабатываний. По этой причине мы используем счетный цветочный фильтр, поскольку он поддерживает удаление. В этой статье мы не будем подробно останавливаться на том, как работает bloom-фильтр. Если вам интересно, вы можете обратиться к Википедии.
5-delete-request-milvus.jpg
Подведение итогов
Итак, мы кратко рассказали вам о том, как Milvus удаляет векторы по ID. Как вы знаете, мы используем мягкое удаление для удаления выгруженных данных. По мере увеличения количества удаленных данных нам необходимо уплотнять сегменты в коллекции, чтобы освободить место, занимаемое удаленными данными. Кроме того, если сегмент уже был проиндексирован, при уплотнении удаляется предыдущий индексный файл и создаются новые индексы. Пока что разработчикам необходимо вызывать метод compact для уплотнения данных. В будущем мы надеемся внедрить механизм проверки. Например, когда количество удаленных данных достигнет определенного порога или распределение данных изменится после удаления, Milvus автоматически уплотнит сегмент.
Теперь мы представили философию проектирования функции удаления и ее реализацию. Безусловно, есть куда совершенствоваться, и мы будем рады любым вашим комментариям и предложениям.
Познакомьтесь с Milvus: https://github.com/milvus-io/milvus. Вы также можете присоединиться к нашему сообществу Slack для обсуждения технических вопросов!
Try Managed Milvus for Free
Zilliz Cloud is hassle-free, powered by Milvus and 10x faster.
Get StartedLike the article? Spread the word