Страницы

воскресенье, 8 марта 2015 г.

MongoDB Set Operators

Пример работы с множествами в MongoDB. В качестве множеств используем массивы. Извлечение данных реализуем на Aggregation Pipeline, в частности с помощью выражений для работы с множествами. По ходу создадим структуру данных, имитирующую взаимоотношения между полами. Будет интересно.

Для начала создадим файл по имени app.js следующего содержания:
// load(app.js), populate(), find(),
// findByName('Mike'), findByName('Mary')
// findByRelation(), findByRelation('love', 1)
// findRelations({ _id: "Mike" }, "Mary"), findRelations({ sex: true }, "Mary")
// findCommon('Mike', 'Nick'), findCommon('Mary', 'Lucy')
// find({love: 'Mike'})
// findByRate('Mike', 'love'})
// fck('Mike', 'Mary'), fck('Mike', 'Mary'), fck('Mike', 'Lucy')
// findByRate('Mike', 'love'})
// buildGraph('Mike'), buildGraph('Mary')
// create('Jake')
// buildGraph('Mike'), buildGraph('Mary')
// likes('Jake', 'Anna')
// buildGraph('Mike'), buildGraph('Mary')

var setsCollection = 'sets';

var populate = function() {
  db[setsCollection].remove({});
  return db[setsCollection].insert([
    {"_id": "Mike", "sex": true, "love": ["Mary", "Lucy"], "likes": ["Anna"]},
    {"_id": "Nick", "sex": true, "love": ["Mary"], "likes": ["Lucy"], "liked": ["Anna"]},
    {"_id": "Alex", "sex": true, "likes": ["Anna"], "liked": ["Mary", "Lucy"]},
    {"_id": "Mary", "sex": false, "love": ["Mike", "Nick"], "likes": ["Alex"]},
    {"_id": "Lucy", "sex": false, "love": ["Mike"], "likes": ["Alex"], "liked": ["Nick"]},
    {"_id": "Anna", "sex": false, "likes": ["Nick"], "liked": ["Mike", "Alex"]}
  ]);
};

var find = function(query) {
  return db[setsCollection].find(query || {}).sort({_id: 1});
};

var findByName = function(name) {
  return db[setsCollection].findOne({_id: name});
};

// common relations, num > 0 - playboy || whore
var findByRelation = function(name, num) {
  var arr = [{$match: {}}, {$project: {}}, {$match: {}}, {$sort: {_id: -1}}];
  name = name || 'love';
  num = num || 0;
  arr[0].$match[name] = {$exists: true};
  arr[1].$project[name] = {$size: '$' + name};
  arr[2].$match[name] = {$gt: num};
  return db[setsCollection].aggregate(arr);
};

// match's ({ _id: "Mike" } || { sex: true }) relations with project ("Mary")
var findRelations = function(match, project) {
  return db[setsCollection].aggregate([
    {$match: match || {}},
    {$project: {
      love: {$setIntersection: [ "$love", [project] ]},
      likes: {$setIntersection: [ "$likes", [project] ]},
      liked: {$setIntersection: [ "$liked", [project] ]}
    }},
    {$project: {
      love: { $cond: [ {$and: [{$ne: ["$love", null]}, {$size: "$love"}]} , true, false ] },
      likes: { $cond: [ {$and: [{$ne: ["$likes", null]}, {$size: "$likes"}]} , true, false ] },
      liked: { $cond: [ {$and: [{$ne: ["$liked", null]}, {$size: "$liked"}]} , true, false ] }
    }},
    {$sort: {_id: 1}}
  ]);
};

// common relations of first ("Mike") and second ("Nick")
var findCommon = function(first, second) {
  return db[setsCollection].aggregate([
    {$match: { _id: {$in: [first, second] }}},
    {$group: {
      _id: null,
      alove: { $first: "$love" }, blove: {$last: "$love"},
      alikes: { $first: "$likes" }, blikes: {$last: "$likes"},
      aliked: { $first: "$liked" }, bliked: {$last: "$liked"}
    }},
    {$project: {
      love: { $setIntersection: ["$alove", "$blove"] },
      likes: { $setIntersection: ["$alikes", "$blikes"] },
      liked: { $setIntersection: ["$aliked", "$bliked"] }
    }}
  ]);
};

var fck = function(first, second) {
  var bulk = db[setsCollection].initializeUnorderedBulkOp(),
     first_update = {$inc: {}},
     second_update = {$inc: {}};
  first_update.$inc['rate.' + second] = second_update.$inc['rate.' + first] = 1;
  bulk.find({_id: first, love: second}).update(first_update);
  bulk.find({_id: second, love: first}).update(second_update);
  return bulk.execute();
};

var findByRate = function(rate, connection) {
  var query = {}, sort = {};
  if (rate) {
    sort['rate.' + rate] = -1;
    if (connection) query[connection] = rate;
  }
  return db[setsCollection].find(query || {}).sort(sort);
};

var buildGraph = function(name) {
  return {
    _id: name,
    graph: build(name, Array.isArray(name) ? name : [name])
  };

  function build(arr, diff, found) {
    found = found || [];
    var aggr = [
      {$match: {}},
      {$project: {
        graph: {
          $setDifference: [
            {$setUnion: [
              {$ifNull: ["$love", []]},
              {$ifNull: ["$likes", []]},
              {$ifNull: ["$liked", []]}
            ]},
            diff
          ]
        }
      }}
    ];
    if (Array.isArray(arr)) {
      aggr[0].$match._id = {$in: arr};
      aggr[1].$project.graph.$setDifference[0].$setUnion.unshift(found);
      aggr[2] = {$unwind: "$graph"};
      aggr[3] = {$group: {_id: null, graph: {$addToSet: "$graph"}}};
    } else {
      aggr[0].$match._id = arr;
    }

    var cursor = db[setsCollection].aggregate(aggr);
    if (!cursor.hasNext()) return found;
    var obj = cursor.next();
    if (!obj.graph.length) return found;
    return build(obj.graph, diff.concat(obj.graph), found.concat(obj.graph));
  }
};

var create = function(name, sex) {
  return db[setsCollection].insert({_id: name, sex: sex || true});
};

var likes = function(first, second) {
  var arr = getRelations(first, second);
  if (arr.length < 2) return printjson({error: 'Invalid params'});
  var first_rel = arr[0], second_rel = arr[1];
  if (first_rel.love || second_rel.love) return printjson({error: 'Already in love'});
  if (first_rel.likes || second_rel.liked) return printjson({error: 'Already likes'});

  var bulk = db[setsCollection].initializeUnorderedBulkOp(),
      first_update = {},
      second_update = {};
  if (first_rel.liked || second_rel.likes) { // love
    first_update.$pull = {liked: second};
    first_update.$push = {love: second};
    second_update.$pull = {likes: first};
    second_update.$push = {love: first};
  } else {
    first_update.$push = {likes: second};
    second_update.$push = {liked: first};
  }
  bulk.find({_id: first}).update(first_update);
  bulk.find({_id: second}).update(second_update);
  return bulk.execute();
};

function getRelations(first, second) {
  var arr = [];
  var relations = db[setsCollection].aggregate([
    {$match: {_id: {$in: [first, second]}}},
    {$project: {
      love: {$setIntersection: [ "$love", {$cond: [{$eq: ["$_id", first]}, [second], [first] ]}  ]},
      likes: {$setIntersection: [ "$likes", {$cond: [{$eq: ["$_id", first]}, [second], [first] ]}  ]},
      liked: {$setIntersection: [ "$liked", {$cond: [{$eq: ["$_id", first]}, [second], [first] ]}  ]}
    }},
    {$project: {
      love: { $cond:  [ {$and: [{$ne: ["$love", null]}, {$size: "$love"}]}, true, false ] },
      likes: { $cond:  [ {$and: [{$ne: ["$likes", null]}, {$size: "$likes"}]}, true, false ] },
      liked: { $cond:  [ {$and: [{$ne: ["$liked", null]}, {$size: "$liked"}]}, true, false ] }
    }}
  ]);
  while (relations.hasNext()) {
    arr.push(relations.next());
  }
  return arr;
}

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

Запускаем mongo Shell. Загружаем созданный выше файл app.js, заполняем коллекцию по имени sets тестовыми данными:

Получаем все документы коллекциии sets:

Что мы имеем: три парня - Майк, Ник, Алекс, три девушки: Мэри, Люси, Анна.
{                           {                       {
  "_id": "Mike",              "_id": "Nick",          "_id": "Alex",
  "love": ["Mary", "Lucy"],   "love": ["Mary"],       
  "likes": ["Anna"]           "likes": ["Lucy"],      "likes": ["Anna"],
                              "liked": ["Anna"]       "liked": ["Mary", "Lucy"]
}                           }                       }

{                           {                       {
  "_id": "Mary",              "_id": "Lucy",          "_id": "Anna",
  "love": ["Mike", "Nick"],   "love": ["Mike"],       
  "likes": ["Alex"]           "likes": ["Alex"],      "likes": ["Nick"],
                              "liked": ["Nick"]       "liked": ["Mike", "Alex"]
}                           }                       }

Схематично взаимоотношения нашей тестовой публики можно изобразить примерно так:

Майк у нас плейбой:

Мэри тоже не особенно старается упорядочить свои связи:

У остальных тоже все непросто, в общем все как у людей :).

Найдем всех у кого есть пара:

Как мы с вами уже успели заметить выше, у Майка и Мэри с этим все более чем в порядке :).

Для того чтобы убедиться в этом еще раз предлагаю выяснить у кого больше одного партнера:

Знакомые все лица :).

Теперь посмотрим как обстоят дела у наших плейбоя и ... нетрудной девушки между собой:

Было бы странно если бы было иначе :).

А как дела у Мэри с мужским полом вообще:

Ее любят Майк и Ник.

Теперь предлагаю подтвердить это, ответив на вопрос что общего у Майка и Ника:

Действительно, они оба любят Мэри, может быть даже одновременно :).

А что общего у Мэри и Люси:

Оказывается у них целых две общих страсти: обе встречаются с Майком, но мечтают об Алексе, представим себе что он успешный коммерс и у него куча бабла :).

Теперь убедимся в том, что Майк действительно любит Мэри и Люси:

Все как надо. Переходим к экшену :).

Попытаемся отсортировать девушек, которых любит Майк в порядке убывания "любви":

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

Позволим ему сделать это пару раз с Мэри и один раз с Люси:

Получаем сортированный список любимых девушек Майка еще раз:

Теперь все по фэн-шую.

И на десерт - графы. Построим социальные графы Майка и Мэри:

Вся публика в сборе. Добавим в нашу компанию еще одного персонажа по имени Джейк:

Построим социальные графы Майка и Мэри еще раз:

И ничего не случилось, т.к. Джейк пока еще не успел завязать отношения ни с одним из героев нашего повествования.

Познакомим Джейка с Анной, допустим в результате он запал не нее:

Построим социальные графы Майка и Мэри еще раз:

Bingo, Джейк появился как в графе Майка так и в графе Мэри, хоть и через тридцать три... в общем спалился :).

Вот такая история. Более откровенные варианты развития событий изобретаем самостоятельно :).