Как найти процент группировки, содержащей определенное слово - PullRequest
1 голос
/ 05 мая 2019

Я пытаюсь вычислить процентную долю листингов в MongoDB, которые содержат определенное слово, сгруппированное по объекту коллекции.

Мне удалось сгруппировать количество записей, содержащих слово, но не процент от общего количества записей каждой группы.

Моя коллекция выглядит так:

{
        "_id" : "103456",
        "metadata" : {
                "type" : "Bike",
                "brand" : "Siamoto",
                "model" : "Siamoto  vespa '01 - € 550 EUR (Negotiable)"
        }
},
{
        "_id" : "103457",
        "metadata" : {
                "type" : "Bike",
                "brand" : "BMW",
                "model" : "BMW ADFR '06 - € 5680 EUR"
        }
}

Я хочу спроецировать процентную долю объявлений на каждый metadata.brand, которые содержат слово "Negotiable" в metadata.model.

Я использовал для подсчета что-то вроде:

db.advertisements.aggregate([
  { $match: { $text: { $search: "Negotiable" } } },
  { $group: { _id: "$metadata.brand", Count: { $sum: 1} } }
])

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

1 Ответ

0 голосов
/ 05 мая 2019

Для того, что вы пытаетесь сделать, использование поиска $text или даже $regex является неправильным подходом. Все, что они могут сделать, это вернуть «совпадающие» документы только из коллекции.

Использование Aggregate для подсчета совпадений строк

Вист не столь гибок, как регулярное выражение (и, к сожалению, в настоящее время нет эквивалента оператора агрегирования, но он будет в будущих выпусках. См. SERVER-11947 ), лучше использовать $indexOfCP, чтобы сопоставить вхождение «строки», а затем сосчитать их по «общему количеству» в каждой группе:

db.advertisements.aggregate([
  { "$group": {
    "_id": "$metadata.brand",
    "totalCount": { "$sum": 1 },
    "matchedCount": {
      "$sum": {
        "$cond": [{ "$ne": [{ "$indexOfCP": [ "$metadata.model", "Negotiable" ] }, -1 ] }, 1, 0]
      }
    }
  }},
  { "$addFields": {
    "percentage": {
      "$cond": {
        "if": { "$ne": [ "$matchedCount", 0 ] },
        "then": {
          "$multiply": [
            { "$divide": [ "$matchedCount", "$totalCount" ] },
            100
          ]
        },
        "else": 0
      }
    }
  }},
  { "$sort": { "percentage": -1 } }
])

И результаты:

{ "_id" : "Siamoto", "totalCount" : 1, "matchedCount" : 1, "percentage" : 100 }
{ "_id" : "BMW", "totalCount" : 1, "matchedCount" : 0, "percentage" : 0 }

Обратите внимание, что $group используется для накопления как всех документов, найденных в "brand", так и тех, в которых строка была сопоставлена. Используемый здесь оператор $cond является «троичным» или if/then/else оператором, который вычисляет логическое выражение и затем возвращает либо одно значение где true, либо другое, где false. В этом случае $indexOfCP НЕ возвращает значение -1 или "not found".

«Процент» фактически выполняется на отдельном этапе, который в этом случае мы используем $addFields, чтобы добавить «дополнительное поле». Операция в основном представляет собой $divide над двумя накопленными значениями из предыдущего этапа. $cond просто применяется, чтобы избежать ошибок «деления на 0», а $multiply просто перемещает десятичные разряды в нечто, похожее на «процент». Но основная предпосылка состоит в том, что такие расчеты, которые требуют накопления «итогов» вначале, всегда будут манипуляцией на «более поздней стадии».

MongoDB 4.2 (предлагается) Предварительный просмотр

К вашему сведению, по текущему "нефинализированному" синтаксису для $regexFind из MongoDB 4.2 (предложено, и еще не будет завершено, если оно будет включено в этот выпуск) и далее это будет примерно так:

db.advertisements.aggregate([
  { "$group": {
    "_id": "$metadata.brand",
    "totalCount": { "$sum": 1 },
    "matchedCount": {
      "$sum": {
        "$cond": {
          "if": {
            "$ne": [
              { "$regexFind": {
                "input": "$metadata.model",
                "regex": /Negotiable/i
              }},
              null
            ]
          },
          "then": 1,
          "else": 0
        }
      }
    }
  }},
  { "$addFields": {
    "percentage": {
      "$cond": {
        "if": { "$ne": [ "$matchedCount", 0 ] },
        "then": {
          "$multiply": [
            { "$divide": [ "$matchedCount", "$totalCount" ] },
            100
          ]
        },
        "else": 0
      }
    }
  }},
  { "$sort": { "percentage": -1 } }
])

Еще раз решительно отмечая, что «текущая» реализация может быть изменена ко времени ее выпуска. Вот как это работает в текущей версии разработки 4.1.9-17-g0a856820ba.

Использование MapReduce

Альтернативный подход, при котором ваша версия MongoDB не поддерживает $indexOfCP ИЛИ вам нужно больше гибкости в том, как вы "сопоставляете строку", заключается в использовании вместо агрегирования mapReduce:

db.advertisements.mapReduce(
  function() {
    emit(this.metadata.brand, {
      totalCount: 1,
      matchedCount: (/Negotiable/i.test(this.metadata.model)) ? 1 : 0
    });
  },
  function(key,values) {
    var obj = { totalCount: 0, matchedCount: 0 };
    values.forEach(value => {
      obj.totalCount += value.totalCount;
      obj.matchedCount += value.matchedCount;
    });
    return obj;
  },
  {
    "out": { "inline": 1 },
    "finalize": function(key,value) {
      value.percentage = (value.matchedCount != 0)
        ? (value.matchedCount / value.totalCount) * 100
        : 0;
      return value;
    }
  }
)

Это дает аналогичный результат, но очень специфичным для mapReduce способом:

            {
                    "_id" : "BMW",
                    "value" : {
                            "totalCount" : 1,
                            "matchedCount" : 0,
                            "percentage" : 0
                    }
            },
            {
                    "_id" : "Siamoto",
                    "value" : {
                            "totalCount" : 1,
                            "matchedCount" : 1,
                            "percentage" : 100
                    }
            }

Логика почти такая же. Мы «излучаем», используя «ключ» для "brand", а затем используем другую троичную , чтобы определить, считать ли «совпадение» или нет. В этом случае используется регулярное выражение test(), и даже в качестве примера используется сопоставление без учета регистра.

Часть «редуктор» просто накапливает выданные значения, а функция finalize - это то, где «процент» возвращается тем же процессом деления и умножения.

Основное различие между двумя отличными от основных способностями состоит в том, что mapReduce не может делать "дальнейших действий", кроме накопления и базовых манипуляций в finalize. «Сортировка», продемонстрированная в конвейере агрегации, не может быть выполнена с mapReduce без вывода в отдельную коллекцию и выполнения отдельных find() и sort() для тех документов, которые содержатся.


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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...