MongoDB
 sql >> Baza danych >  >> NoSQL >> MongoDB

MongoDB pomaga w rekomendacjach

Aby uzyskać efekt końcowy, musisz zrobić kilka rzeczy, ale pierwsze etapy są stosunkowo proste. Weź podany obiekt użytkownika:

var user = {
    user_id : 1,
    Friends : [3,5,6],
    Artists : [
        {artist_id: 10 , weight : 345},
        {artist_id: 17 , weight : 378}
    ]
};

Teraz zakładając, że masz już pobrane dane, sprowadza się to do znalezienia tych samych struktur dla każdego „przyjaciela” i odfiltrowania zawartości tablicy „Artyści” na pojedynczą odrębną listę. Przypuszczalnie każda „waga” będzie tutaj brana pod uwagę łącznie.

Jest to prosta operacja agregacji, która najpierw odfiltruje artystów znajdujących się już na liście dla danego użytkownika:

var artists = user.Artists.map(function(artist) { return artist.artist_id });

User.aggregate(
    [ 
        // Find possible friends without all the same artists
        { "$match": {
            "user_id": { "$in": user.Friends },
            "Artists.artist_id": { "$nin": artists }
        }},
        // Pre-filter the artists already in the user list
        { "$project": 
            "Artists": {
                "$setDifference": [
                    { "$map": {
                        "input": "$Artists",
                        "as": "$el",
                        "in": {
                            "$cond": [
                                "$anyElementTrue": {
                                    "$map": {
                                        "input": artists,
                                        "as": "artist",
                                        "in": { "$eq": [ "$$artist", "$el.artist_id" ] }
                                    }
                                },
                                false,
                                "$$el"
                            ]
                        } 
                    }}
                    [false]
                ]
            } 
        }},
        // Unwind the reduced array
        { "$unwind": "$Artists" },
        // Group back by each artist and sum weights
        { "$group": {
            "_id": "$Artists.artist_id",
            "weight": { "$sum": "$Artists.weight" }
        }},
        // Sort the results by weight
        { "$sort": { "weight": -1 } }
    ],
    function(err,results) {
        // more to come here
    }
);

Jedyną naprawdę trudną częścią jest tutaj „filtr wstępny”. Możesz po prostu $unwind tablica i $match ponownie, aby odfiltrować wpisy, których nie chcesz. Nawet jeśli chcemy $unwind wyniki później, aby je połączyć, bardziej efektywne jest usunięcie ich z tablicy „najpierw”, więc jest mniej do rozwinięcia.

Oto $map Operator umożliwia inspekcję każdego elementu tablicy "Artists" użytkownika, a także porównanie z przefiltrowaną listą artystów "user" w celu zwrócenia żądanych szczegółów. $setDifference służy do faktycznego "filtrowania" wszelkich wyników, które nie zostały zwrócone jako zawartość tablicy, ale zwrócone jako false .

Potem jest już tylko $unwind do denormalizacji zawartości tablicy i $group zebrać w sumie na artystę. Dla zabawy używamy $sort aby pokazać, że lista jest zwracana w żądanej kolejności, ale nie będzie to konieczne na późniejszym etapie.

To przynajmniej część drogi tutaj, ponieważ wynikowa lista powinna zawierać tylko innych artystów, którzy nie znajdują się jeszcze na własnej liście użytkownika i posortować według zsumowanej „wagi” spośród wszystkich artystów, którzy mogliby pojawić się u wielu znajomych.

W kolejnej części potrzebne będą dane ze zbioru „artystów”, aby uwzględnić liczbę słuchaczy. Podczas gdy mangusta ma .populate() metody, naprawdę nie chcesz tego tutaj, ponieważ szukasz „odmiennych użytkowników”. Oznacza to inną implementację agregacji, aby uzyskać te odrębne liczby dla każdego wykonawcy.

Kontynuując z listy wyników poprzedniej operacji agregacji, użyjesz $_id wartości takie jak:

// First get just an array of artist id's
var artists = results.map(function(artist) {
    return artist._id;
});

Artist.aggregate(
    [
        // Match artists
        { "$match": {
            "artistID": { "$in": artists }
        }},
        // Project with weight for distinct users
        { "$project": {
            "_id": "$artistID",
            "weight": {
                "$multiply": [
                    { "$size": {
                        "$setUnion": [
                            { "$map": {
                                "input": "$user_tag",
                                "as": "tag",
                                "in": "$$tag.user_id"
                            }},
                            []
                        ]
                    }},
                    10
                ]
            }
        }}
    ],
    function(err,results) {
        // more later
    }
);

Tutaj sztuczka jest wykonywana łącznie za pomocą $map wykonać podobną transformację wartości, która jest przekazywana do $setUnion aby uczynić je wyjątkową listą. Następnie $size operator jest stosowany, aby dowiedzieć się, jak duża jest ta lista. Dodatkowa matematyka ma na celu nadanie tej liczbie znaczenia w porównaniu z już zarejestrowanymi wagami z poprzednich wyników.

Oczywiście musisz to wszystko jakoś połączyć, ponieważ w tej chwili istnieją tylko dwa różne zestawy wyników. Podstawowym procesem jest „Tabela mieszająca”, w której unikalne wartości identyfikatora „wykonawcy” są używane jako klucz, a wartości „wagi” są łączone.

Możesz to zrobić na wiele sposobów, ale ponieważ istnieje chęć „posortowania” połączonych wyników, preferuję coś „MongoDBish”, ponieważ jest to zgodne z podstawowymi metodami, do których już powinieneś być przyzwyczajony.

Wygodnym sposobem na zaimplementowanie tego jest użycie nedb , który zapewnia magazyn „w pamięci”, który wykorzystuje wiele metod tego samego typu, które są używane do odczytu i zapisu w kolekcjach MongoDB.

To również dobrze się skaluje, jeśli musisz użyć rzeczywistej kolekcji w celu uzyskania dużych wyników, ponieważ wszystkie zasady pozostają takie same.

  1. Pierwsza operacja agregacji wstawia nowe dane do sklepu

  2. Druga agregacja „aktualizuje”, że dane zwiększają pole „waga”

Jako kompletna lista funkcji i przy innej pomocy async biblioteka wyglądałaby tak:

Funkcja
function GetUserRecommendations(userId,callback) {

    var async = require('async')
        DataStore = require('nedb');

    User.findOne({ "user_id": user_id},function(err,user) {
        if (err) callback(err);

        var artists = user.Artists.map(function(artist) {
            return artist.artist_id;
        });

        async.waterfall(
            [
                function(callback) {
                    var pipeline =  [ 
                        // Find possible friends without all the same artists
                        { "$match": {
                            "user_id": { "$in": user.Friends },
                            "Artists.artist_id": { "$nin": artists }
                        }},
                        // Pre-filter the artists already in the user list
                        { "$project": 
                            "Artists": {
                                "$setDifference": [
                                    { "$map": {
                                        "input": "$Artists",
                                        "as": "$el",
                                        "in": {
                                            "$cond": [
                                                "$anyElementTrue": {
                                                    "$map": {
                                                        "input": artists,
                                                        "as": "artist",
                                                        "in": { "$eq": [ "$$artist", "$el.artist_id" ] }
                                                    }
                                                },
                                                false,
                                                "$$el"
                                            ]
                                        } 
                                    }}
                                    [false]
                                ]
                            } 
                        }},
                        // Unwind the reduced array
                        { "$unwind": "$Artists" },
                        // Group back by each artist and sum weights
                        { "$group": {
                            "_id": "$Artists.artist_id",
                            "weight": { "$sum": "$Artists.weight" }
                        }},
                        // Sort the results by weight
                        { "$sort": { "weight": -1 } }
                    ];

                    User.aggregate(pipeline, function(err,results) {
                        if (err) callback(err);

                        async.each(
                            results,
                            function(result,callback) {
                                result.artist_id = result._id;
                                delete result._id;
                                DataStore.insert(result,callback);
                            },
                            function(err)
                                callback(err,results);
                            }
                        );

                    });
                },
                function(results,callback) {

                    var artists = results.map(function(artist) {
                        return artist.artist_id;  // note that we renamed this
                    });

                    var pipeline = [
                        // Match artists
                        { "$match": {
                            "artistID": { "$in": artists }
                        }},
                        // Project with weight for distinct users
                        { "$project": {
                            "_id": "$artistID",
                            "weight": {
                                "$multiply": [
                                    { "$size": {
                                        "$setUnion": [
                                            { "$map": {
                                                "input": "$user_tag",
                                                "as": "tag",
                                                "in": "$$tag.user_id"
                                            }},
                                            []
                                        ]
                                    }},
                                    10
                                ]
                            }
                        }}
                    ];

                    Artist.aggregate(pipeline,function(err,results) {
                        if (err) callback(err);
                        async.each(
                            results,
                            function(result,callback) {
                                result.artist_id = result._id;
                                delete result._id;
                                DataStore.update(
                                    { "artist_id": result.artist_id },
                                    { "$inc": { "weight": result.weight } },
                                    callback
                                );
                            },
                            function(err) {
                                callback(err);
                            }
                        );
                    });
                }
            ],
            function(err) {
                if (err) callback(err);     // callback with any errors
                // else fetch the combined results and sort to callback
                DataStore.find({}).sort({ "weight": -1 }).exec(callback);
            }
        );

    });

}

Tak więc po dopasowaniu początkowego źródłowego obiektu użytkownika wartości są przekazywane do pierwszej funkcji agregującej, która jest wykonywana szeregowo i używa async.waterfall aby przekazać jego wynik.

Wcześniej jednak wyniki agregacji są dodawane do DataStore ze zwykłym .insert() instrukcji, dbając o zmianę nazwy _id pola jako nedb nie lubi niczego poza własnym wygenerowanym _id wartości. Każdy wynik jest wstawiany z artist_id i weight właściwości z wyniku agregacji.

Lista ta jest następnie przekazywana do drugiej operacji agregacji, która zwróci każdego określonego „wykonawcę” z obliczoną „wagą” na podstawie odrębnego rozmiaru użytkownika. Istnieją "zaktualizowane" z tym samym .update() oświadczenie w DataStore dla każdego artysty i zwiększenie pola „waga”.

Wszystko idzie dobrze, ostatnią operacją jest .find() te wyniki i .sort() je przez połączoną "wagę" i po prostu zwraca wynik do przekazanego wywołania zwrotnego do funkcji.

Więc użyjesz go w ten sposób:

GetUserRecommendations(1,function(err,results) {
   // results is the sorted list
});

I zwróci wszystkich artystów nie znajdujących się obecnie na liście tego użytkownika, ale na ich listach znajomych i uporządkowanych według łącznej wagi liczby słuchań znajomych plus wynik z liczby różnych użytkowników tego artysty.

W ten sposób radzisz sobie z danymi z dwóch różnych kolekcji, które musisz połączyć w jeden wynik z różnymi zagregowanymi szczegółami. To wiele zapytań i przestrzeń robocza, ale także część filozofii MongoDB, że takie operacje są lepiej wykonywane w ten sposób niż wrzucanie ich do bazy danych w celu „połączenia” wyników.




  1. Redis
  2.   
  3. MongoDB
  4.   
  5. Memcached
  6.   
  7. HBase
  8.   
  9. CouchDB
  1. MongoDB - Różnica między indeksem w polu tekstowym a indeksem tekstowym?

  2. $w operatorze mongoDB z _id w perl

  3. MongoDB $atanh

  4. mongodb multi tenacy zaklęcie z @Document

  5. meteor $pull usuń z tablicy