MongoDB
 sql >> Base de données >  >> NoSQL >> MongoDB

Faire correspondre au moins N éléments d'un tableau à une liste de conditions

Votre question a deux possibilités pour moi, mais peut-être une explication pour vous aider à démarrer.

Tout d'abord, je dois vous expliquer que vous comprenez mal l'intention de $elemMatch et il est mal utilisé dans ce cas.

L'idée de $elemMatch est de créer un "document de requête" qui est réellement appliqué aux éléments du tableau. L'intention est l'endroit où vous avez "plusieurs conditions" sur un document dans le tableau afin de le faire correspondre discrètement dans le document membre, et non dans l'ensemble du tableau du document externe. c'est-à-dire :

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

Et la requête suivante fonctionnera, même si aucun élément unique réel dans ce tableau ne correspond, mais le document entier le fait :

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

Mais pour vérifier si un élément réel correspond à ces deux conditions, c'est ici que vous utilisez $elemMatch :

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

Donc, aucune correspondance dans cet échantillon, et il ne correspondra qu'à l'endroit où un élément de tableau spécifique avait ces deux éléments.

Nous avons maintenant $elemMatch expliqué, voici votre requête simplifiée :

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

Beaucoup plus simple, et cela fonctionne en regardant tous les membres du tableau par un seul champ et en retournant où n'importe quel élément du document contient au moins un de ces résultats possibles.

Mais pas ce que vous demandez, ainsi de suite avec votre question. Si vous lisez cette dernière déclaration, vous devriez vous rendre compte que $in est en fait un $or condition. C'est juste une forme raccourcie pour demander "ou" sur le même élément dans le document.

Dans cet esprit, au cœur de ce que vous demandez se trouve un "et" opération où les "trois" valeurs sont contenues. En supposant que vous n'envoyiez que "trois" éléments dans le test, vous pourriez utiliser une forme de $and qui est sous la forme abrégée de $all :

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

Cela ne vous renverrait que les documents dont l'élément dans les membres de ce tableau correspond à "tous" les éléments spécifiés dans la condition de test. C'est peut-être ce que vous voulez, mais il y a le cas où bien sûr vous voulez spécifier une liste de, par exemple, "quatre artistes ou plus" à tester et que vous ne voulez que "trois" ou un nombre inférieur, auquel cas un $all l'opérateur est trop concis.

Mais il existe un moyen logique de résoudre ce problème, cela prend juste un peu plus de traitement avec des opérateurs non disponibles pour les requêtes de base mais qui sont disponibles pour le framework d'agrégation :

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 première étape implémente une requête standard $match condition afin de filtrer les documents uniquement pour ceux qui sont "susceptibles" de correspondre aux conditions. Le cas logique ici est d'utiliser $in comme auparavant, il trouvera les documents où au moins un des éléments présents dans votre tableau "test" est présent dans au moins un des champs membres du propre tableau des documents.

La clause suivante est quelque chose que vous devriez idéalement construire dans le code en ce qui concerne la "longueur" du tableau. L'idée ici est que vous voulez au moins "trois" correspondances, alors le tableau que vous testez dans le document doit avoir au moins "trois" éléments pour y répondre, donc inutile de récupérer des documents avec "deux" ou moins éléments de tableau car ils ne peuvent jamais correspondre à "trois".

Étant donné que toutes les requêtes MongoDB ne sont essentiellement qu'une représentation d'une structure de données, cela rend cela très facile à construire. c'est-à-dire pour JavaScript :

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

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

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

La logique est que la "notation par points" se forme avec $exists teste la présence d'un élément à l'index spécifié ( n-1 ), et il doit être là pour que le tableau ait au moins cette longueur.

Le reste du rétrécissement utilise idéalement le $setIntersection méthode afin de renvoyer les éléments correspondants entre le tableau réel et le tableau testé. Étant donné que le tableau dans le document ne correspond pas à la structure du "tableau de test", il doit être transformé via le $map opération qui est définie pour ne renvoyer que le champ "artiste" de chaque élément du tableau.

Au fur et à mesure que "l'intersection" de ces deux tableaux est faite, elle est finalement testée pour le $size de cette liste résultante d'éléments communs où le test est appliqué pour voir qu'"au moins trois" de ces éléments se sont avérés être en commun.

Enfin, vous venez de "filtrer" tout ce qui n'était pas vrai en utilisant un $match état.

Idéalement, vous utilisez MongoDB 2.6 ou supérieur afin d'avoir ces opérateurs disponibles. Pour les versions antérieures de 2.2.x et 2.4.x, c'est toujours possible, mais juste un peu plus de travail et de surcharge de traitement :

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 } }}
])