sql >> Databasteknik >  >> NoSQL >> MongoDB

MongoDB Nested Array Intersection Query

Det finns ett par sätt att göra detta med hjälp av aggregeringsramverket

Bara en enkel uppsättning data till exempel:

{
    "_id" : ObjectId("538181738d6bd23253654690"),
    "movies": [
        { "_id": 1, "rating": 5 },
        { "_id": 2, "rating": 6 },
        { "_id": 3, "rating": 7 }
    ]
},
{
    "_id" : ObjectId("538181738d6bd23253654691"),
    "movies": [
        { "_id": 1, "rating": 5 },
        { "_id": 4, "rating": 6 },
        { "_id": 2, "rating": 7 }
    ]
},
{
    "_id" : ObjectId("538181738d6bd23253654692"),
    "movies": [
        { "_id": 2, "rating": 5 },
        { "_id": 5, "rating": 6 },
        { "_id": 6, "rating": 7 }
    ]
}

Med den första "användaren" som exempel vill du nu se om någon av de andra två användarna har minst två av samma filmer.

För MongoDB 2.6 och uppåt kan du helt enkelt använda $setIntersection operatör tillsammans med $size operatör:

db.users.aggregate([

    // Match the possible documents to reduce the working set
    { "$match": {
        "_id": { "$ne": ObjectId("538181738d6bd23253654690") },
        "movies._id": { "$in": [ 1, 2, 3 ] },
        "$and": [
            { "movies": { "$not": { "$size": 1 } } }
        ]
    }},

    // Project a copy of the document if you want to keep more than `_id`
    { "$project": {
        "_id": {
            "_id": "$_id",
            "movies": "$movies"
        },
        "movies": 1,
    }},

    // Unwind the array
    { "$unwind": "$movies" },

    // Build the array back with just `_id` values
    { "$group": {
        "_id": "$_id",
        "movies": { "$push": "$movies._id" }
    }},

    // Find the "set intersection" of the two arrays
    { "$project": {
        "movies": {
            "$size": {
                "$setIntersection": [
                   [ 1, 2, 3 ],
                   "$movies"
                ]
            }
        }
    }},

    // Filter the results to those that actually match
    { "$match": { "movies": { "$gte": 2 } } }

])

Detta är fortfarande möjligt i tidigare versioner av MongoDB som inte har dessa operatörer, bara med några fler steg:

db.users.aggregate([

    // Match the possible documents to reduce the working set
    { "$match": {
        "_id": { "$ne": ObjectId("538181738d6bd23253654690") },
        "movies._id": { "$in": [ 1, 2, 3 ] },
        "$and": [
            { "movies": { "$not": { "$size": 1 } } }
        ]
    }},

    // Project a copy of the document along with the "set" to match
    { "$project": {
        "_id": {
            "_id": "$_id",
            "movies": "$movies"
        },
        "movies": 1,
        "set": { "$cond": [ 1, [ 1, 2, 3 ], 0 ] }
    }},

    // Unwind both those arrays
    { "$unwind": "$movies" },
    { "$unwind": "$set" },

    // Group back the count where both `_id` values are equal
    { "$group": {
        "_id": "$_id",
        "movies": {
           "$sum": {
               "$cond":[
                   { "$eq": [ "$movies._id", "$set" ] },
                   1,
                   0
               ]
           }
        } 
    }},

    // Filter the results to those that actually match
    { "$match": { "movies": { "$gte": 2 } } }
])

I detalj

Det kan vara lite att ta in, så vi kan ta en titt på varje steg och bryta ner dem för att se vad de gör.

$match :Du vill inte operera på alla dokument i samlingen så detta är en möjlighet att ta bort objekt som inte möjligen matchar även om det fortfarande finns mer arbete att göra för att hitta den exakta ettor. Så de uppenbara sakerna är att utesluta samma "användare" och sedan bara matcha de dokument som har minst en av samma filmer som hittades för den "användaren".

Nästa sak som är vettig är att tänka på det när du vill matcha n poster då endast dokument som har en "movies"-array som är större än n-1 kan möjligen faktiskt innehålla tändstickor. Användningen av $and här ser roligt ut och krävs inte specifikt, men om de nödvändiga matchningarna var 4 då skulle den faktiska delen av uttalandet se ut så här:

        "$and": [
            { "movies": { "$not": { "$size": 1 } } },
            { "movies": { "$not": { "$size": 2 } } },
            { "movies": { "$not": { "$size": 3 } } }
        ]

Så du "utesluter" i princip arrayer som inte möjligen är tillräckligt långa för att ha n tändstickor. Notera här att denna $size operatorn i frågeformuläret skiljer sig från $size för aggregeringsramen. Det finns inget sätt att till exempel använda detta med en ojämlikhetsoperator som $gt är dess syfte att specifikt matcha den begärda "storleken". Därav detta frågeformulär för att ange alla möjliga storlekar som är mindre än.

$project :Det finns några syften med detta uttalande, av vilka några skiljer sig beroende på vilken MongoDB-version du har. För det första, och valfritt, hålls en dokumentkopia under _id värde så att dessa fält inte ändras av resten av stegen. Den andra delen här är att hålla "movies"-arrayen överst i dokumentet som en kopia för nästa steg.

Vad som också händer i versionen som presenteras för tidigare versioner 2.6 är att det finns en extra array som representerar _id värden för att "filmerna" ska matcha. Användningen av $cond operatorn här är bara ett sätt att skapa en "bokstavlig" representation av arrayen. Lustigt nog introducerar MongoDB 2.6 en operatör känd som $literal att göra exakt detta utan det roliga sättet vi använder $cond precis här.

$unwind :För att göra något mer måste filmarrayen lindas upp eftersom det i båda fallen är det enda sättet att isolera det befintliga _id värden för de poster som måste matchas mot "uppsättningen". Så för versionen före 2.6 måste du "varva ner" båda arrayerna som finns.

$group :För MongoDB 2.6 och senare grupperar du bara tillbaka till en array som bara innehåller _id värden för filmerna med "betygen" borttagna.

Före 2.6 eftersom alla värden presenteras "sida vid sida" (och med massor av dubbelarbete) gör du en jämförelse av de två värdena för att se om de är samma. Där det är true , detta talar om för $cond operatorsats för att returnera värdet 1 eller 0 där villkoret är false . Detta skickas direkt tillbaka genom $sum för att summera antalet matchande element i arrayen till önskad "uppsättning".

$project :Där detta är den annorlunda delen för MongoDB 2.6 och senare är att eftersom du har tryckt tillbaka en array av "filmerna" _id värden som du då använder $setIntersection för att direkt jämföra dessa arrayer. Eftersom resultatet av detta är en array som innehåller de element som är desamma, lindas detta sedan in i en $size operatorn för att avgöra hur många element som returnerades i den matchande uppsättningen.

$match :Är det sista steget som har implementerats här som gör det tydliga steget att endast matcha de dokument vars antal korsande element var större än eller lika med det antal som krävs.

Final

Det är i princip så man gör. Före 2.6 är lite mer klumpig och kommer att kräva lite mer minne på grund av expansionen som görs genom att duplicera varje arraymedlem som hittas av alla möjliga värden i uppsättningen, men det är fortfarande ett giltigt sätt att göra detta.

Allt du behöver göra är att tillämpa detta med den större n matcha värden för att uppfylla dina villkor, och naturligtvis se till att din ursprungliga användarmatchning har den nödvändiga n möjligheter. Annars genererar du bara detta på n-1 från längden på "användarens" array av "filmer".




  1. Mongoid ordning efter längd på array

  2. Installerar du PHP 7 MongoDB-klienten/drivrutinen?

  3. Mongo med java - hitta sökfråga med batchstorlek

  4. MongoDB InsertBatch JObject - Serialiseringsfel