Статьи

Индексирование документов без схемы в MongoDB

Одна из лучших частей MongoDB — это ее полностью бессхемная природа. Мы можем хранить любые данные, которые нам нужны, не беспокоясь о том, откуда эти данные поступают или как их следует размещать.

Тем не менее, иногда сложно проиндексировать эти данные таким образом, чтобы они были производительными с использованием схемы Монго без схемы. Для данных любого разумного размера запросы должны попадать в индекс, чтобы сузить пространство поиска и избежать многочисленных обращений к диску, если полный набор данных превышает доступную оперативную память.

В идеале наши пользователи могут загружать непредвиденные типы данных в нашу систему, и мы можем быстро сопоставлять элементы на основе одного индекса. Основной вариант использования для такого рода вещей — это если мы хотим связать произвольные данные ключ / значение с различными документами. Затем мы хотим запросить эти ключи, значения или оба! Я хотел бы обрисовать в общих чертах несколько различных подходов к тому, как Mongo можно эффективно запрашивать такого рода заданные пользователем пары ключ / значение.

Встроенные документы

Собственный подход состоит в том, чтобы структурировать интересующие нас свойства как поля, хранящиеся во встроенном документе. Это простой подход, который хорошо моделирует наши данные.

Предположим, мы создаем музыкальный сервис и хотим создать метаданные о песнях, а затем внедрить их в объект песни. Пользователи могут загружать метаданные любого типа о песне, хотя приложение может не знать, какая информация будет сохранена раньше времени. Наша коллекция песен может содержать документы, которые выглядят примерно так:

{
    title : "Don't stop me now",
    artist : "Queen",
    metadata : {
        genre : "Rock",
        duration : 120,
        bps : 120,
        key : "A",
    }
}

В этом случае мы обеспечим следующие индексы:

В этом случае мы обеспечим следующие индексы:

  • 'metadata.genre'
  • 'metadata.duration'
  • 'metadata.bps'
  • 'metadata.key'

Они могут быть установлены как разреженные индексы, так что документы без этих конкретных полей в их метаданных не будут включены в индекс, что еще больше сузит наше пространство поиска.

Однако основная проблема заключается в том, что по мере добавления пользователями новых типов полей в метаданные все песни должны проходиться для каждого нового индекса. Поскольку Mongo не знает, содержат ли текущие документы недавно проиндексированное поле, он должен просмотреть всю коллекцию. Затем мы должны быть осторожны с тем, какие ключи пользователи могут загружать или ожидают, что в течение длительного времени будут добавляться новые индексы.

Плюсы:
  • Нормальная схема
  • Простота в операциях обновления
Минусы:
  • Нужно много индексов
  • Индексы создаются во время выполнения (каждый может занять много времени)

Простой Multikeying

К счастью для нас, мы можем воспользоваться функцией монго, известной как многопользовательский . По сути, он группирует элементы в массиве как часть одного поля, что позволяет нам запрашивать документ, содержащий любой из этих элементов в одном запросе. Что еще более важно, это дает нам возможность индексировать только одно поле и иметь возможность запрашивать его непосредственно по индексу!

Какой пример того, как это работает? Учебное пособие по многопользовательской игре — это тегирование. Если у нас есть документ публикации в блоге, теги могут быть текстовыми полями, хранящимися как часть массива для документа:

{
    title : "My first post",
    text  : "Some kind of text",
    tags  : [ "mongodb", "segment.io", "coding" ]
}

Тогда очень легко найти все сообщения в блоге, соответствующие определенному тегу. Mongo будет пытаться сопоставить многопользовательское поле для каждого элемента в массиве:

db.posts.find({ tags : "segment.io" }); // finds all posts tagged segment.io

Итак, используя простой подход mutikeying — как выглядят наши документы сейчас? Все наши теги могут храниться в одном поле, но в виде массива, а не встроенного документа.

{
    title : "Don't stop me now",
    artist : "Queen",
    metadata : [
        { genre : "Rock" },
        { duration : 120 },
        { bps : 120 },
        { key : "A" }
    ]
}

Основным преимуществом здесь является то, что нам нужен только один индекс, и нет необходимости индексировать во время выполнения. Так как мы можем обеспечить индекс до того, как наше приложение начнет хранить данные, mongo может построить его с течением времени без необходимости выполнять дорогостоящую операцию по созданию индекса одновременно. Наш единственный индекс тогда . { metadata : true }

Но подождите — есть небольшая проблема с этим подходом! При рассмотрении того, как выполняются наши запросы, существует разница между точными запросами к многопользовательским данным и запросами, которые должны соответствовать определенному документу. Вот быстрый пример, который должен продемонстрировать суть:

// Direct match, find songs of duration : 120
db.songs.find({metadata : {duration : 120}}).explain()
// cursor : "BtreeCursor metadata_1"


// Range match, find songs of duration greater than 120
db.songs.find({metadata : {$elemMatch : {duration : {$gt : 120}}}}).explain()
// cursor : "BasicCursor"

Что тут происходит?

Mongo хранит свои индексы как часть BTree, где значения отдельных индексированных ключей хранятся в виде узлов в дереве. Однако на сами документы ссылаются листы BTree под этими узлами значений. Это означает, что наш индекс может выполнить достойную работу по сопоставлению непосредственно со значениями документа в нашей мультиключе, но когда мы пытаемся найти соответствие части или условию документа, нам не повезло. Все, кроме прямого соответствия элемента, заставляет нас вернуться к тому BasicCursor, который сканирует все документы в коллекции. Вкратце:

Pros
  • Единый индекс для многопользовательского запроса
  • Индексы для точного запроса
Минусы:
  • Обновление немного сложнее, но может быть сделано с помощью позиционного оператора
  • Не работает с произвольным сопоставлением запросов с использованием $ elemMatch

Структурированный Multikeying

Как мы решаем эту проблему? Благодаря более структурированному использованию мультиключей.

Мы хотим иметь только один индекс, но хотим, чтобы этот индекс был достаточно мощным, чтобы соответствовать его полям, независимо от того, как эти поля представлены. В идеале мы можем выполнить любой запрос на эти значения, и при этом он попадет в созданный нами индекс. Очевидно, что некоторые виды запросов не смогут использовать индекс, но, надеюсь, любой запрос, который идет только на один уровень в своем соответствии, будет.

Ключом к решению этой проблемы является индексирование — это объединение двух концепций, которые мы уже видели, а также добавление идеи составных индексов . Составные индексы позволяют полям, которые могут запрашиваться вместе, использовать один и тот же индекс, если в запросе указаны оба поля.

Используя мультиключи и составные индексы, мы можем создать массив элементов, которые будут иметь ключи с известным именем, но с переменным количеством элементов . Для этого мы структурируем наши данные как массив пар ключ-значение — но теперь с явно именованными полями для элементов в массиве.

Теперь наша структура документа выглядит примерно так:

{
    title : "Don't stop me now",
    artist : "Queen",
    metadata : [
        { key : "genre", value : "Rock" },
        { key : "duration", value : 120 },
        { key : "bps", value : 120 },
        { key : "key", value : "A" }
    ]
}

Наш окончательный индекс { 'metadata.key' : true, 'metadata.value' : true }. В конце нам нужен только один индекс, чтобы выполнить все соответствующие запросы, которые мы хотели бы!

Плюсы:
  • Запрашивать произвольно, используя $elemMatch
  • Нужен один индекс
Минусы:
  • Больше данных, необходимых для того же представления
  • Обновление сложнее

Очевидным недостатком этого подхода является то, что теперь наши документы выглядят гораздо более сложными и гораздо менее простыми, чем простая схема, с которой мы начали. Кроме того, хранение документов требует больше места , так как keyи valueимена полей повторяются с каждым документом.

Он не идеален, но он дает нам то, что мы хотели: единый индекс, который поддерживает различные типы совпадающих запросов.

Как то, что вы прочитали? Следите за мной в твиттере или посмотрите, что я создаю на Segment.io .

Благодарности: Части этого поста были вдохновлены решениями схемы MongoDB в Chemeo .