Получить документы с тегами в списке, упорядоченном по общему количеству совпадений

Учитывая следующую коллекцию документов MongoDB:

{
 title : 'shirt one'
 tags : [
  'shirt',
  'cotton',
  't-shirt',
  'black'
 ]
},
{
 title : 'shirt two'
 tags : [
  'shirt',
  'white',
  'button down collar'
 ]
},
{
 title : 'shirt three'
 tags : [
  'shirt',
  'cotton',
  'red'
 ]
},
...

Как получить список элементов, соответствующих списку тегов, упорядоченных по общему количеству совпадающих тегов? Например, учитывая этот список тегов в качестве входных данных:

['shirt', 'cotton', 'black']

Я хотел бы получить элементы, ранжированные в порядке описания по общему количеству совпадающих тегов:

item          total matches
--------      --------------
Shirt One     3 (matched shirt + cotton + black)
Shirt Three   2 (matched shirt + cotton)
Shirt Two     1 (matched shirt)

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

Но в Монго...?

Кажется, этот подход может сработать,

  • разбить входные теги на несколько операторов «IN»
  • query for items by "OR"'ing together the tag inputs
    • i.e. where ( 'shirt' IN items.tags ) OR ( 'cotton' IN items.tags )
    • это вернет, например, три экземпляра «Рубашка один», 2 экземпляра «Рубашка три» и т. д.
  • map/reduce that output
    • map: emit(this._id, {...});
    • уменьшить: подсчитать общее количество вхождений _id
    • finalize: сортировать по подсчитанному итогу

Но я не понимаю, как реализовать это как запрос Mongo, или это даже самый эффективный подход.


person Matt    schedule 23.12.2011    source источник
comment
Никакой M/R не является простым в производственном коде, поскольку в текущей реализации отсутствует надлежащий параллелизм. На самом деле, можно привести хорошие аргументы в пользу того, чтобы вообще избегать m/r в ситуациях с высокой пропускной способностью.   -  person Remon van Vliet    schedule 23.12.2011


Ответы (3)


Как я ответил в В MongoDB поиск в массиве и сортировка по количеству совпадений

Это возможно с помощью Aggregation Framework.

Предположения

  • Атрибут tags представляет собой набор (без повторяющихся элементов)

Запрос

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

db.test_col.aggregate(
    {$match: {tags: {$in: ["shirt","cotton","black"]}}}, 
    {$unwind: "$tags"}, 
    {$match: {tags: {$in: ["shirt","cotton","black"]}}},
    {$group: {
        _id:{"_id":1}, 
        matches:{$sum:1}
    }}, 
    {$sort:{matches:-1}}
);

Ожидаемые результаты

{
    "result" : [
        {
            "_id" : {
                "_id" : ObjectId("5051f1786a64bd2c54918b26")
            },
            "matches" : 3
        },
        {
            "_id" : {
                "_id" : ObjectId("5051f1726a64bd2c54918b24")
            },
            "matches" : 2
        },
        {
            "_id" : {
                "_id" : ObjectId("5051f1756a64bd2c54918b25")
            },
            "matches" : 1
        }
    ],
    "ok" : 1
}
person Samuel García    schedule 13.09.2012
comment
Самуил Ответ правильный. Я просто оспариваю дополнительную информацию о том, что это неэффективно. Чтобы сопоставить, кто-то должен будет раскрутить теги в любом случае, выполнение этой задачи в конвейере агрегации может быть самым быстрым подходом для специальных запросов. - person rat; 04.02.2016
comment
Этот ответ отлично сработал для меня, однако мне пришлось внести небольшое изменение в объект $group, чтобы это работало в Mongo 3.0. и используйте это для ID _id:{"_id":"$_id"} - person Binarytales; 12.02.2016
comment
Да, в самом деле. Формат группировки _id изменился в версии 3.0, и теперь вы можете использовать этот формат или вложенный, но также с символом $. - person Samuel García; 12.02.2016

Прямо сейчас это невозможно сделать, если вы не используете MapReduce. Единственная проблема с MapReduce заключается в том, что он медленный (по сравнению с обычным запросом).

Фреймворк агрегации намечен на версию 2.2 (поэтому он должен быть доступен в версии 2.1 для разработчиков) и должен сделать подобные вещи намного проще без использования MapReduce.

Лично я не думаю, что использование M/R является эффективным способом сделать это. Я бы предпочел запросить все документы и выполнить эти расчеты на стороне приложения. Проще и дешевле масштабировать серверы приложений, чем масштабировать серверы баз данных, поэтому пусть серверы приложений выполняют обработку чисел. Из них этот подход может не работать для вас, учитывая ваши шаблоны доступа к данным и требования.

Еще более простой подход может заключаться в том, чтобы просто включить свойство count в каждый из ваших объектов тегов, и всякий раз, когда вы $push добавляете новый тег в массив, вы также $inc добавляете свойство count. Это распространенный шаблон в мире MongoDB, по крайней мере, до появления платформы агрегации.

person Bryan Migliorisi    schedule 23.12.2011
comment
Включение свойства count при $push'ировании нового тега в массив не помогло бы, учитывая эту проблему, поскольку wount может просто указать общее количество тегов (а не общее количество тегов, соответствующих вводу). - person Matt; 23.12.2011

Я присоединюсь к @Bryan, сказав, что MapReduce — это единственный возможный способ на данный момент (и он далек от совершенства). Но если вам это очень нужно, то вот :-)

    var m = function() {
        var searchTerms = ['shirt', 'cotton', 'black'];
        var me = this;
        this.tags.forEach(function(t) {
            searchTerms.forEach(function(st) {
                if(t == st) {
                    emit(me._id, {matches : 1});
                }
            })
        })
    };

    var r = function(k, vals) {
        var result = {matches : 0};
        vals.forEach(function(v) {
            result.matches += v.matches;
        })
        return result;
    };

    db.shirts.mapReduce(m, r, {out: 'found01'});

    db.found01.find();
person Sergio Tulentsev    schedule 23.12.2011
comment
Спасибо, это хорошее начало. Но вместо того, чтобы запускать map/reduce для всех элементов в коллекции, не будет ли быстрее выполнить первоначальный поиск, объединив входные теги с помощью операции ИЛИ? Это уменьшит размер набора, обрабатываемого в m(), а r() может просто вернуть vals.length, поскольку общее количество совпадений? - person Matt; 23.12.2011