Оптимальный способ моделирования иерархии документов в CouchDB

Я пытаюсь моделировать документ иерархии в CouchDB для использования в моей системе, которая концептуально похожа на блог. Каждое сообщение в блоге относится как минимум к одной категории, и каждая категория может иметь много сообщений. Категории являются иерархическими, что означает, что если сообщение принадлежит CatB в иерархии CatA CatB "(" CatB находится в CatA "), он также относится к CatA.

Пользователи должны иметь возможность быстро находить все сообщения в категории (и все ее дочерние элементы).

Решение 1 Каждый документ типа post содержит массив "category", представляющий его позицию в иерархии (см. 2).

{
   "_id": "8e7a440862347a22f4a1b2ca7f000e83",
   "type": "post",
   "author": "dexter",
   "title": "Hello",
   "category":["OO","Programming","C++"]
}

Решение 2 Каждый документ типа post содержит строку "category", представляющую ее путь в иерархии (см. 4).

{
   "_id": "8e7a440862347a22f4a1b2ca7f000e83",
   "type": "post",
   "author": "dexter",
   "title": "Hello",
   "category": "OO/Programming/C++"
}

Решение 3 Каждый документ типа post содержит свой родительский идентификатор категории, представляющий его путь в иерархии (см. 3). Структура иерархической категории строится по связанным типам документов категории.

{
   "_id": "8e7a440862347a22f4a1b2ca7f000e83",
   "type": "post",
   "author": "dexter",
   "title": "Hello",
   "category_id": "3"
}

{
   "_id": "1",
   "type": "category",
   "name": "OO"
}


{
   "_id": "2",
   "type": "category",
   "name": "Programming",
   "parent": "1"
}


{
   "_id": "3",
   "type": "category",
   "name": "C++",
   "parent": "2"
}

Вопрос

Какой лучший способ сохранить такие отношения в CouchDB? Какое наиболее эффективное решение с точки зрения дискового пространства, масштабируемости и скорости поиска?

Можно ли смоделировать такое отношение, чтобы учесть локализованные имена категорий?

Отказ

Я знаю, что этот вопрос был задан несколько раз здесь, на SO, но, похоже, нет окончательного ответа на него или ответа, который касается плюсов и минусов каждого решения. Извините за длину вопроса:)

Прочитайте до сих пор

CouchDB - окончательное руководство

Сохранение иерархических данных в CouchDB

Получение иерархических/вложенных данных из CouchDB

Использование CouchDB group_level для иерархических данных

Ответ 1

Нет правильного ответа на этот вопрос, отсюда и отсутствие окончательного ответа. В основном это зависит от того, какое использование вы хотите оптимизировать.

Вы указываете, что скорость поиска документов, относящихся к определенной категории (и их детям), наиболее важна. Первые два решения позволяют создать представление, которое выдает сообщение в блоге несколько раз, один раз для каждой категории в цепочке от листа до корня. Таким образом, выбор всех документов может быть выполнен с использованием одного (и, следовательно, быстрого) запроса. Единственное отличие второго решения от первого решения заключается в том, что вы переносите разбор категории "путь" на компоненты из кода, который вставляет документ в функцию карты вида. Я бы предпочел первое решение, так как было проще реализовать функцию карты и немного более гибкой (например, она позволяет названию категории содержать символ косой черты).

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

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

Решение 4 Я предлагаю четвертое решение, в котором документы в блоге содержат ссылки на документы категорий, но все же ссылаются на всех предков категории сообщений. Это позволяет переименовывать категории, не касаясь сообщений в блоге, и позволяет хранить дополнительные метаданные с категорией (например, переводы названия категории или описания):

{
    "_id": "8e7a440862347a22f4a1b2ca7f000e83",
    "type": "post",
    "author": "dexter",
    "title": "Hello",
    "category_ids": [3, 2, 1]
}

{
    "_id": "1",
    "type": "category",
    "name": "OO"
}

{
    "_id": "2",
    "type": "category",
    "name": "Programming",
    "parent": "1"
}


{
    "_id": "3",
    "type": "category",
    "name": "C++",
    "parent": "2"
}

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

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

Предположим, что существует дополнительная категория с именем "Ajax" с оркестрами "JavaScript", "Программирование" и "OO". Чтобы упростить следующий пример, я выбрал идентификаторы документов категорий, чтобы они соответствовали названию категории.

{
    "_id": "8e7a440862347a22f4a1b2ca7f000e83",
    "type": "post",
    "author": "dexter",
    "title": "Hello",
    "category_ids": ["C++", "Ajax"],
    "category_anchestor_ids": ["C++", "Programming", "OO", "Ajax", "JavaScript"]
}

Чтобы разрешить категории иметь несколько родителей, просто сохраните несколько родительских идентификаторов с категорией. Вам нужно будет удалить дубликаты при поиске всех предков категории.

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

{ "_id": "100", "type": "category", "name": "OO"                              }
{ "_id": "101", "type": "category", "name": "Programming", "parent_id": "100" }
{ "_id": "102", "type": "category", "name": "C++",         "parent_id": "101" }
{ "_id": "103", "type": "category", "name": "JavaScript",  "parent_id": "101" }
{ "_id": "104", "type": "category", "name": "AJAX",        "parent_id": "103" }

{ "_id": "200", "type": "post", "title": "OO Post",          "category_id": "104", "category_anchestor_ids": ["100"]                      }
{ "_id": "201", "type": "post", "title": "Programming Post", "category_id": "101", "category_anchestor_ids": ["101", "100"]               }
{ "_id": "202", "type": "post", "title": "C++ Post",         "category_id": "102", "category_anchestor_ids": ["102", "101", "100"]        }
{ "_id": "203", "type": "post", "title": "AJAX Post",        "category_id": "104", "category_anchestor_ids": ["104", "103", "101", "100"] }

В дополнение к этому мы используем представление под названием posts_by_category в проектном документе под названием _design/blog со следующей функцией карты:

function (doc) {
    if (doc.type == 'post') {
        for (i in doc.category_anchestor_ids) {
            emit([doc.category_anchestor_ids[i]], doc)
        }
    }
}

Затем мы можем получить все сообщения в категории Programming (с идентификатором "101") или одну из подкатегорий с помощью GET запросов к следующему URL.

http://localhost:5984/so/_design/blog/_view/posts_by_category?reduce=false&key=["101"]

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

function (keys, values, rereduce) {
    if (rereduce) {
        return sum(values)
    } else {
        return values.length
    }
}

И затем мы используем следующий URL-адрес:

http://localhost:5984/so/_design/blog/_view/posts_by_category?group_level=1

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