Динамическая липкая сортировка в Mongo для простого значения или списка

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

{first_name: 'Joe', last_name: 'Blow', offices: ['GA', 'FL']}
{first_name: 'Joe', last_name: 'Johnson', offices: ['FL']}
{first_name: 'Daniel', last_name: 'Aiken', offices: ['TN', 'SC']}
{first_name: 'Daniel', last_name: 'Madison', offices: ['SC', 'GA']}
... a bunch more names ...

Теперь предположим, что я хочу отобразить имена в алфавитном порядке по фамилии, но я хочу привязать все записи с именем «Джо» вверху.

В SQL это довольно прямолинейно:

SELECT * FROM people ORDER first_name == 'Joe' DESC, last_name

Возможность помещать выражения в критерии сортировки делает это тривиальным. Используя структуру агрегации, я могу сделать это:

[
  {$project: {
    first_name: 1,
    last_name: 1
    offices: 1,
    sticky: {$cond: [{$eq: ['$first_name', 'Joe']}, 1, 0]}
  }},
  {$sort: [
    'sticky': -1,
    'last_name': 1
  ]}
]

В основном я создаю динамическое поле со структурой агрегации, которая равна 1, если имя, если Джо, и 0, если имя не Джо, а затем сортируется в обратном порядке. Конечно, при построении моего конвейера агрегации я могу легко изменить «Джо» на «Дэниел», и теперь «Дэниел» будет привязан к вершине. Это отчасти то, что я имею в виду под динамической липкой сортировкой. Значение, по которому я придерживаюсь сортировки, будет меняться от запроса к запросу.

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

[
  {$project: {
    first_name: 1,
    last_name: 1
    offices: 1,
    sticky: {$cond: [{$eq: ['$offices', 'FL']}, 1, 0]}
  }},
  {$sort: [
    'sticky': -1,
    'last_name': 1
  ]}
]

Но это вообще не работает. Я понял, что если я изменю его на следующий, Джо Джонсон (который работает только в офисе Флориды) будет наверху:

[
  {$project: {
    first_name: 1,
    last_name: 1
    offices: 1,
    sticky: {$cond: [{$eq: ['$offices', ['FL']]}, 1, 0]}
  }},
  {$sort: [
    'sticky': -1,
    'last_name': 1
  ]}
]

Но это не сделало Джо Блоу лидером (который работает во Флориде и Джорджии). Я считаю, что это простое совпадение. Итак, моя первая попытка вообще не работает, так как $eq возвращает false, так как мы сравниваем массив со строкой. Вторая попытка работает для Джо Джонсона, потому что мы сравниваем одни и те же массивы. Но Joe Blow не работает, так как ['GA', 'FL'] != ['FL']. Кроме того, если я хочу привязать FL и SC вверху, я не могу указать значение ['FL', 'SC'] для сравнения.

Затем я пытаюсь использовать комбинацию $setUnion и $size.

[
  {$project: {
    first_name: 1,
    last_name: 1
    offices: 1,
    sticky: {$size: {$setUnion: ['$offices', ['FL', 'SC']]}}
  }},
  {$sort: [
    'sticky': -1,
    'last_name': 1
  ]}
]

Я пытался использовать различные комбинации $let и $literal, но он всегда жалуется на то, что я пытаюсь передать литеральный массив в аргументы $setUnion. В частности, говорится:

disallowed field type Array in object expression

Есть какой-либо способ сделать это?


person Eric Anderson    schedule 20.05.2014    source источник


Ответы (2)


Не могу воспроизвести вашу ошибку, но у вас есть несколько «опечаток» в вашем вопросе, поэтому я не могу быть уверен, что у вас на самом деле есть.

Но если вы действительно работаете с MongoDB 2.6 или выше, вам, вероятно, понадобится $setIntersection или $setIsSubset вместо $setUnion. Эти операторы подразумевают «соответствие» содержимого массива, с которым они сравниваются, где $setUnion просто объединяет предоставленный массив с существующим:

db.people.aggregate([
    { "$project": {
        "first_name": 1,
        "last_name": 1,
        "sticky": { 
            "$size": { 
                "$setIntersection": [ "$offices", [ "FL", "SC" ]] 
            }
        },
        "offices": 1
    }},
    { "$sort": {
        "sticky": -1,
        "last_name": 1
    }}
])

В предыдущих версиях, где у вас не было этих операторов набора, вы просто используя $unwind для работы с массивом и такие же $cond как и раньше в пределах $group для собрать все обратно:

db.people.aggregate([
    { "$unwind": "$offices" },
    { "$group": {
        "_id": "$_id",
        "first_name": { "$first": "$first_name" },
        "last_name": { "$first": "$last_name",
        "sticky": { "$sum": { "$cond": [
            { "$or": [
                { "$eq": [ "$offices": "FL" ] },
                { "$eq": [ "$offices": "SC" ] },
            ]},
            1,
            0
        ]}},
        "offices": { "$push": "$offices" }
    }},
    { "$sort": {
        "sticky": -1,
        "last_name": 1
    }}
])

Но вы определенно были на правильном пути. Просто выберите правильную операцию набора или другой метод, чтобы получить точную потребность.


Или, поскольку вы опубликовали свой способ получения того, что хотите, лучший способ написать такое «упорядоченное сопоставление»:

db.people.aggregate([
    { "$project": {
        "first_name": 1,
        "last_name": 1,
        "sticky": { "$cond": [
            { "$anyElementTrue": {
                "$map": {
                    "input": "$offices",
                    "as": "o",
                    "in": { "$eq": [ "$$o", "FL" ] }
                }
            }},
            2,
            { "$cond": [
                { "$anyElementTrue": {
                    "$map": {
                        "input": "$offices",
                        "as": "o",
                        "in": { "$eq": [ "$$o", "SC" ] }
                    }
                }},
                1,
                0
            ]}
        ]},
        "offices": 1
    }},
    { "$sort": {
        "sticky": -1,
        "last_name": 1
    }}
])

И это дало бы ему приоритет документов с «офисами», содержащими «FL», над «SC» и, следовательно, над всеми остальными, и выполнение операции в пределах одного поля. Это также должно быть очень легко для людей, чтобы увидеть, как абстрагировать это в форму, используя $unwind в более ранних версиях без операторов set. Где вы просто предоставляете более высокое значение «веса» элементам, которые хотите разместить вверху, вложив $cond.

person Neil Lunn    schedule 20.05.2014
comment
Спасибо за ответ. Я на последней монго. Я имел в виду setIntersection. Поэтому я опечатал это (как в моем вопросе, так и в коде). Возможно, у меня были и другие опечатки, потому что я пытался просто изолировать свой вопрос от отправленного JSON, а не от PHP, который генерирует этот JSON. Таким образом, использование метода set могло бы сработать, если бы я продолжал его. Альтернативный метод, который я опубликовал, на самом деле работает лучше, потому что он сначала помещает первый элемент, который я хочу сделать липким, затем второй и т. д., а не смешивает их, как это сделали бы операторы набора. Знайте, что я был на правильном пути, и это возможно с помощью set ops. - person Eric Anderson; 20.05.2014
comment
@EricAnderson В основном сосредоточился на операторе, который определенно не собирался делать то, что вы хотели. Я бы предположил, что любые проблемы с литералами на самом деле находятся где-то в вашем генераторе, поскольку показанный выше код просто попадет прямо в оболочку (или правильно проанализирует как JSON) и выполнится с обычно желаемым результатом. Ваш опубликованный ответ по-прежнему имеет недопустимый JSON, и вам не требуется сопоставление переменных. Я бы отследил, где эта проблема на самом деле, но не с MongoDB. - person Neil Lunn; 20.05.2014

Думаю, я нашел лучший способ сделать это.

[
  {$project: {
    first_name: 1,
    last_name: 1
    offices: 1,
    sticky_0: {
      $cond: [{
        $anyElementTrue: {
          $map: {
            input: "$offices",
            as:    "j",
            in:    {$eq: ['$$j', 'FL']}
          }
        }
      }, 0, 1]
    },
    sticky_1": {
      $cond: [{
        $anyElementTrue: {
          $map: {
            input: '$offices',
            as:    'j',
            in:    {$eq: ['$$j', 'SC']}
          }
        }
      }, 0, 1]
    }
  }},
  {$sort: [
    'sticky_0': 1,
    'sticky_1': 1,
    'last_name': 1
  ]}
]

По сути, при построении конвейера я перебираю каждый элемент, который хочу сделать липким, и из этого элемента создаю собственное виртуальное поле, которое проверяет только одно значение. Чтобы проверить только одно значение, я использую комбинацию $cond, $anyElementTrue и $map. Это немного запутанно, но это работает. Хотелось бы услышать, есть ли что-то проще.

person Eric Anderson    schedule 20.05.2014