MongoDB
 sql >> Database >  >> NoSQL >> MongoDB

Abbina almeno N elementi di una matrice a un elenco di condizioni

La tua domanda ha due possibilità per me, ma forse qualche spiegazione per iniziare.

Prima di tutto devo spiegarti che hai frainteso l'intento di $elemMatch e in questo caso viene utilizzato in modo improprio.

L'idea di $elemMatch è creare un "documento di query" che viene effettivamente applicato agli elementi dell'array. L'intento è quello di avere "condizioni multiple" su un documento all'interno dell'array per abbinarlo in modo discreto all'interno del documento membro e non all'interno dell'intero array del documento esterno. cioè:

{
   "data": [
       { "a": 1, "b": 3 },
       { "a": 2, "b": 2 }
   ]
}

E la seguente query funzionerà, anche se nessun singolo elemento effettivo in quell'array corrisponde, ma l'intero documento lo fa:

db.collection.find({ "data.a": 1, "data.b": 2 })

Ma per verificare se un elemento effettivo soddisfa entrambe queste condizioni, è qui che usi $elemMatch :

db.collection.find({ "data": { "a": 1, "b": 2 } })

Quindi nessuna corrispondenza in quel campione e corrisponderà solo al punto in cui uno specifico elemento dell'array aveva entrambi questi elementi.

Ora abbiamo $elemMatch spiegato, ecco la tua query semplificata:

db.collection.find({ "tracks.artist": { "$in": arr } })

Molto più semplice e funziona esaminando tutti i membri dell'array in base a un singolo campo e restituendo dove qualsiasi elemento nel documento contiene almeno uno di quei possibili risultati.

Ma non quello che stai chiedendo, quindi avanti con la tua domanda. Se leggi quest'ultima affermazione dovresti renderti conto che $in è in realtà un $or condizione. È solo una forma abbreviata per chiedere "o" sullo stesso elemento nel documento.

Con questo in mente, al centro di ciò che stai chiedendo c'è un "e" operazione in cui sono contenuti tutti i "tre" valori. Supponendo che tu stia inviando solo "tre" elementi nel test, puoi utilizzare una forma di $and che è nella forma abbreviata di $all :

db.collection.find({ "tracks.artist": { "$all": arr } })

Ciò ti restituirebbe solo i documenti che avevano l'elemento all'interno dei membri di quell'array che corrisponde a "tutti" gli elementi specificati nella condizione di test. Potrebbe essere quello che vuoi, ma c'è il caso in cui ovviamente vuoi specificare un elenco di artisti, diciamo "quattro o più" da testare e vuoi solo "tre" o un numero inferiore, nel qual caso an $all l'operatore è troppo conciso.

Ma c'è un modo logico per risolvere questo problema, ci vuole solo un po' più di elaborazione con operatori non disponibili per le query di base ma che sono disponibili per framework di aggregazione :

var arr = ["A","B","C","D"];     // List for testing

db.collection.aggregate([
    // Match conditions for documents to narrow down
    { "$match": {
        "tracks.artist": { "$in": arr },
        "tracks.2": { "$exists": true }      // you would construct in code
    }},

    // Test the array conditions
    { "$project": {
        "user": 1,
        "tracks": 1,                         // any fields you want to keep
        "matched": {
            "$gte": [
                 { "$size": {
                     "$setIntersection": [
                         { "$map": {
                             "input": "$tracks",
                             "as": "t",
                             "in": { "$$t.artist" }
                         }},
                         arr
                     ]
                 }},
                 3
             ]
        }
    }},

    // Filter out anything that did not match
    { "$match": { "matched": true } }
])

La prima fase implementa una query standard $match condizione per filtrare i documenti solo per quelli che "probabilmente" corrispondono alle condizioni. Il caso logico qui è usare $in come prima troverà quei documenti in cui almeno uno degli elementi presenti nell'array "test" è presente all'interno di almeno uno dei campi membro nell'array dei documenti.

La prossima clausola è qualcosa che dovresti idealmente costruire nel codice in quanto si riferisce alla "lunghezza" dell'array. L'idea qui è dove vuoi almeno "tre" corrispondenze, quindi l'array che stai testando nel documento deve avere almeno "tre" elementi per soddisfarlo, quindi non ha senso recuperare documenti con "due" o meno elementi dell'array poiché non possono mai corrispondere a "tre".

Dal momento che tutte le query MongoDB sono essenzialmente solo una rappresentazione di una struttura di dati, è molto facile costruirla. cioè, per JavaScript:

var matchCount = 3;    // how many matches we want

var match1 = { "$match": { "tracks.artist": { "$in": arr } } };

match1["$match"]["tracks."+ (matchCount-1)] = { "$exits": true };

La logica è che il modulo "notazione punto" con $exists verifica la presenza di un elemento all'indice specificato ( n-1 ) e deve essere presente affinché l'array sia almeno di quella lunghezza.

Il resto del restringimento utilizza idealmente il $setIntersection metodo per restituire gli elementi abbinati tra l'array effettivo e l'array testato. Poiché l'array nel documento non corrisponde alla struttura dell'"array di test", deve essere trasformato tramite $map operazione che è impostata per restituire solo il campo "artista" da ciascun elemento dell'array.

Quando viene creata l'"intersezione" di questi due array, viene infine testata per $size dell'elenco risultante di elementi comuni in cui viene applicato il test per verificare che "almeno tre" di tali elementi siano risultati in comune.

Alla fine puoi semplicemente "filtrare" tutto ciò che non era vero usando un $match condizione.

Idealmente stai usando MongoDB 2.6 o versioni successive per avere quegli operatori disponibili. Per le versioni precedenti di 2.2.xe 2.4.x, è ancora possibile, ma solo un po' più di lavoro e sovraccarico di elaborazione:

db.collection.aggregate([
    // Match conditions for documents to narrow down
    { "$match": {
        "tracks.artist": { "$in": arr },
        "tracks.2": { "$exists": true }      // you would construct in code
    }},

    // Unwind the document array
    { "$unwind": "$tracks" },

    // Filter the content
    { "$match": { "tracks.artist": { "$in": arr } }},

    // Group for distinct values
    { "$group": {
        "_id": { 
           "_id": "$_id",
           "artist": "$tracks.artist"
        }
    }},

    // Make arrays with length
    { "$group": {
        "_id": "$_id._id",
        "artist": { "$push": "$_id.artist" },
        "length": { "$sum": 1 }
    }},

    // Filter out the sizes
    { "$match": { "length": { "$gte": 3 } }}
])