MongoDB
 sql >> Base de Dados >  >> NoSQL >> MongoDB

Consulta de interseção de matriz aninhada do MongoDB


Existem algumas maneiras de fazer isso usando a estrutura de agregação

Apenas um conjunto simples de dados, por exemplo:
{
    "_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 }
    ]
}

Usando o primeiro "usuário" como exemplo, agora você quer descobrir se algum dos outros dois usuários tem pelo menos dois dos mesmos filmes.

Para MongoDB 2.6 e superior, você pode simplesmente usar o $setIntersection operador junto com o $size operador:
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 } } }

])

Isso ainda é possível em versões anteriores do MongoDB que não possuem esses operadores, bastando seguir mais alguns passos:
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 } } }
])

Em detalhes


Isso pode ser um pouco difícil de assimilar, então podemos dar uma olhada em cada estágio e dividi-los para ver o que eles estão fazendo.

$match :você não deseja operar em todos os documentos da coleção, portanto, esta é uma oportunidade para remover os itens que possivelmente não são correspondentes, mesmo que ainda haja mais trabalho a ser feito para encontrar o exato uns. Portanto, o óbvio é excluir o mesmo "usuário" e depois corresponder apenas os documentos que possuem pelo menos um dos mesmos filmes encontrados para esse "usuário".

A próxima coisa que faz sentido é considerar que, quando você deseja corresponder n entradas, apenas documentos que tenham uma matriz "movies" maior que n-1 pode realmente conter correspondências. O uso de $and aqui parece engraçado e não é necessário especificamente, mas se as correspondências necessárias forem 4 então essa parte real da declaração ficaria assim:
        "$and": [
            { "movies": { "$not": { "$size": 1 } } },
            { "movies": { "$not": { "$size": 2 } } },
            { "movies": { "$not": { "$size": 3 } } }
        ]

Então você basicamente "descarta" arrays que não são longos o suficiente para ter n fósforos. Observando aqui que este $size operador no formulário de consulta é diferente de $size para a estrutura de agregação. Não há como, por exemplo, usar isso com um operador de desigualdade como $gt é seu objetivo é corresponder especificamente ao "tamanho" solicitado. Daí este formulário de consulta para especificar todos os tamanhos possíveis que são menores que.

$projeto :Existem alguns propósitos nesta declaração, dos quais alguns diferem dependendo da versão do MongoDB que você possui. Em primeiro lugar, e opcionalmente, uma cópia do documento está sendo mantida sob o _id valor para que esses campos não sejam modificados pelo restante das etapas. A outra parte aqui é manter o array "movies" na parte superior do documento como uma cópia para o próximo estágio.

O que também está acontecendo na versão apresentada para as versões pré 2.6 é que há um array adicional representando o _id valores para os "filmes" para corresponder. O uso do $cond operador aqui é apenas uma maneira de criar uma representação "literal" da matriz. Engraçado, o MongoDB 2.6 apresenta um operador conhecido como $literal para fazer exatamente isso sem o jeito engraçado que estamos usando $cond bem aqui.

$unwind :Para fazer mais alguma coisa, a matriz de filmes precisa ser desenrolada, pois em ambos os casos é a única maneira de isolar o _id existente valores para as entradas que precisam ser comparadas com o "conjunto". Portanto, para a versão pré 2.6, você precisa "desenrolar" os dois arrays que estão presentes.

$grupo :Para o MongoDB 2.6 e superior, você está apenas agrupando de volta em uma matriz que contém apenas o _id valores dos filmes com as "classificações" removidas.

Pré 2.6 já que todos os valores são apresentados "lado a lado" ( e com muita duplicação ) você está fazendo uma comparação dos dois valores para ver se são iguais. Onde isso é true , isso informa ao $cond instrução do operador para retornar um valor de 1 ou 0 onde a condição é false . Isso é repassado diretamente através de $sum para totalizar o número de elementos correspondentes na matriz para o "conjunto" necessário.

$projeto :Onde esta é a parte diferente para o MongoDB 2.6 e superior é porque você empurrou de volta uma matriz dos "movies" _id valores que você está usando $setIntersection para comparar diretamente essas matrizes. Como o resultado disso é uma matriz contendo os elementos que são os mesmos, isso é envolvido em um $size operador para determinar quantos elementos foram retornados nesse conjunto correspondente.

$match :É o estágio final que foi implementado aqui que faz a etapa clara de combinar apenas os documentos cuja contagem de elementos de interseção foi maior ou igual ao número necessário.

Final


É basicamente assim que você faz. Antes de 2.6 é um pouco mais desajeitado e exigirá um pouco mais de memória devido à expansão que é feita duplicando cada membro da matriz que é encontrado por todos os valores possíveis do conjunto, mas ainda é uma maneira válida de fazer isso.

Tudo o que você precisa fazer é aplicar isso com o maior n valores correspondentes para atender às suas condições e, claro, certifique-se de que sua correspondência de usuário original tenha o n necessário possibilidades. Caso contrário, apenas gere isso em n-1 do comprimento da matriz de "filmes" do "usuário".