Retorna apenas os elementos do sub-documento correspondente dentro de um array nested

A principal coleção é varejista, que contém uma matriz para lojas. Cada loja contém uma série de ofertas (você pode comprar nesta loja). Isso oferece matriz tem uma matriz de tamanhos. (Veja o exemplo abaixo)

Agora eu tento encontrar todas as ofertas, que estão disponíveis no tamanho L

 { "_id" : ObjectId("56f277b1279871c20b8b4567"), "stores" : [ { "_id" : ObjectId("56f277b5279871c20b8b4783"), "offers" : [ { "_id" : ObjectId("56f277b1279871c20b8b4567"), "size": [ "XS", "S", "M" ] }, { "_id" : ObjectId("56f277b1279871c20b8b4567"), "size": [ "S", "L", "XL" ] } ] } } 

Eu tentei esta consulta: db.getCollection('retailers').find({'stores.offers.size': 'L'})

Espero que alguns resultados sejam assim:

  { "_id" : ObjectId("56f277b1279871c20b8b4567"), "stores" : [ { "_id" : ObjectId("56f277b5279871c20b8b4783"), "offers" : [ { "_id" : ObjectId("56f277b1279871c20b8b4567"), "size": [ "S", "L", "XL" ] } ] } } 

Mas a saída da minha consulta contém também a oferta não correspondente com size XS, X e M.

Como posso forçar o MongoDB a retornar apenas as ofertas, que correspondem à minha consulta?

Saudações e obrigado

Portanto, a consulta seleciona o “documento” exatamente como deveria. Mas o que você está procurando é “filtrar as matrizes” contidas para que os elementos retornados correspondam apenas à condição da consulta.

A resposta real é claro que, a menos que você esteja realmente economizando muita largura de banda filtrando tais detalhes, você não deve nem tentar, ou pelo menos além da primeira correspondência posicional.

O MongoDB possui um operador $ posicional que retornará um elemento de matriz no índice correspondente de uma condição de consulta. No entanto, isso só retorna o índice “primeiro” correspondente do elemento “array” mais externo.

 db.getCollection('retailers').find( { 'stores.offers.size': 'L'}, { 'stores.$': 1 } ) 

Neste caso, significa apenas a posição do array "stores" . Portanto, se houver várias inputs de “lojas”, apenas “um” dos elementos que continham sua condição correspondente será retornado. Mas , isso não faz nada para a matriz interna de "offers" e, como tal, cada “oferta” dentro da matriz de "stores" matchd ainda seria retornada.

O MongoDB não tem como “filtrar” isso em uma consulta padrão, portanto, o seguinte não funciona:

 db.getCollection('retailers').find( { 'stores.offers.size': 'L'}, { 'stores.$.offers.$': 1 } ) 

As únicas ferramentas que o MongoDB realmente tem para fazer este nível de manipulação é com a estrutura de agregação. Mas a análise deve mostrar por que você “provavelmente” não deve fazer isso e, em vez disso, apenas filtrar o array no código.


Em ordem de como você pode conseguir isso por versão.

Primeiro com o MongoDB 3.2.x com o uso da operação $filter :

 db.getCollection('retailers').aggregate([ { "$match": { "stores.offers.size": "L" } }, { "$project": { "stores": { "$filter": { "input": { "$map": { "input": "$stores", "as": "store", "in": { "_id": "$$store._id", "offers": { "$filter": { "input": "$$store.offers", "as": "offer", "cond": { "$setIsSubset": [ ["L"], "$$offer.size" ] } } } } } }, "as": "store", "cond": { "$ne": [ "$$store.offers", [] ]} } } }} ]) 

Então, com o MongoDB 2.6.xe acima com $map e $setDifference :

 db.getCollection('retailers').aggregate([ { "$match": { "stores.offers.size": "L" } }, { "$project": { "stores": { "$setDifference": [ { "$map": { "input": { "$map": { "input": "$stores", "as": "store", "in": { "_id": "$$store._id", "offers": { "$setDifference": [ { "$map": { "input": "$$store.offers", "as": "offer", "in": { "$cond": { "if": { "$setIsSubset": [ ["L"], "$$offer.size" ] }, "then": "$$offer", "else": false } } }}, [false] ] } } } }, "as": "store", "in": { "$cond": { "if": { "$ne": [ "$$store.offers", [] ] }, "then": "$$store", "else": false } } }}, [false] ] } }} ]) 

E finalmente em qualquer versão acima do MongoDB 2.2.x onde a estrutura de agregação foi introduzida.

 db.getCollection('retailers').aggregate([ { "$match": { "stores.offers.size": "L" } }, { "$unwind": "$stores" }, { "$unwind": "$stores.offers" }, { "$match": { "stores.offers.size": "L" } }, { "$group": { "_id": { "_id": "$_id", "storeId": "$stores._id", }, "offers": { "$push": "$stores.offers" } }}, { "$group": { "_id": "$_id._id", "stores": { "$push": { "_id": "$_id.storeId", "offers": "$offers" } } }} ]) 

Vamos quebrar as explicações.

MongoDB 3.2.xe maior

Então, de maneira geral, o $filter é o caminho a seguir, já que foi projetado com o objective em mente. Como existem vários níveis do array, você precisa aplicar isso em cada nível. Então, primeiro você está mergulhando em cada "offers" dentro de "stores" para examinar e $filter esse conteúdo.

A comparação simples aqui é “A matriz "size" contém o elemento que estou procurando” . Nesse contexto lógico, a coisa curta a fazer é usar a operação $setIsSubset para comparar uma matriz (“set”) de ["L"] com a matriz de destino. Onde essa condição é true (contém “L”), o elemento da matriz para "offers" é retido e retornado no resultado.

No $filter nível superior $filter , você está olhando para ver se o resultado do $filter anterior retornou uma matriz vazia [] para "offers" . Se não estiver vazio, o elemento será retornado ou será removido.

MongoDB 2.6.x

Isso é muito semelhante ao processo moderno, exceto que, como não há nenhum $filter nesta versão, você pode usar $map para inspecionar cada elemento e, em seguida, usar $setDifference para filtrar quaisquer elementos retornados como false .

Então, $map vai retornar toda a matriz, mas a operação $cond apenas decide se deve retornar o elemento ou, em vez disso, um valor false . Na comparação de $setDifference para um único elemento “conjunto” de [false] todos os elementos false na matriz retornada seriam removidos.

De todas as outras maneiras, a lógica é a mesma que acima.

MongoDB 2.2.x e superior

Portanto, abaixo do MongoDB 2.6, a única ferramenta para trabalhar com arrays é o $unwind , e só com esse propósito você não deve usar a estrutura de agregação “justa” para essa finalidade.

O processo, na verdade, parece simples, simplesmente “desmontando” cada array, filtrando as coisas que você não precisa, em seguida, montá-lo novamente. O cuidado principal está nos “dois” estágios de $group , com o “primeiro” para reconstruir o array interno, e o próximo para reconstruir o array externo. Existem valores distintos de _id em todos os níveis, portanto, eles precisam ser incluídos em todos os níveis de agrupamento.

Mas o problema é que o $unwind é muito caro . Embora ainda tenha um propósito, a principal intenção de uso não é fazer esse tipo de filtragem por documento. De fato, em lançamentos modernos, o uso deve ser apenas quando um elemento do (s) array (s) precisa (m) tornar-se parte da “chave de agrupamento”.


Conclusão

Portanto, não é um processo simples obter correspondências em vários níveis de uma matriz como essa e, na verdade, ela pode ser extremamente cara se implementada incorretamente.

Somente as duas listagens modernas devem ser usadas para essa finalidade, já que elas empregam um estágio “único” de pipeline, além da correspondência “query” $match para fazer a “filtragem”. O efeito resultante é um pouco mais sobrecarregado do que os formulários padrão de .find() .

Em geral, porém, essas listagens ainda têm uma complexidade, e, a menos que você realmente reduza drasticamente o conteúdo retornado por essa filtragem de forma a melhorar significativamente a largura de banda usada entre o servidor e o cliente, é melhor de filtrar o resultado da consulta inicial e projeção básica.

 db.getCollection('retailers').find( { 'stores.offers.size': 'L'}, { 'stores.$': 1 } ).forEach(function(doc) { // Technically this is only "one" store. So omit the projection // if you wanted more than "one" match doc.stores = doc.stores.filter(function(store) { store.offers = store.offers.filter(function(offer) { return offer.size.indexOf("L") != -1; }); return store.offers.length != 0; }); printjson(doc); }) 

Portanto, trabalhar com o object retornado “pós” processamento de consulta é muito menos obtuso do que usar o pipeline de agregação para fazer isso. E como declarado a única diferença “real” seria que você está descartando os outros elementos no “servidor” ao invés de removê-los “por documento” quando recebidos, o que pode economizar um pouco de largura de banda.

Mas a menos que você esteja fazendo isso em uma versão moderna com apenas $match e $project , o “custo” de processamento no servidor superará em muito o “ganho” de reduzir essa sobrecarga de rede, removendo primeiro os elementos não correspondentes.

Em todos os casos, você obtém o mesmo resultado:

 { "_id" : ObjectId("56f277b1279871c20b8b4567"), "stores" : [ { "_id" : ObjectId("56f277b5279871c20b8b4783"), "offers" : [ { "_id" : ObjectId("56f277b1279871c20b8b4567"), "size" : [ "S", "L", "XL" ] } ] } ] } 

como sua matriz é incorporada, não podemos usar $ elemMatch, em vez disso, você pode usar a estrutura de agregação para obter seus resultados:

 db.retailers.aggregate([ {$match:{"stores.offers.size": 'L'}}, //just precondition can be skipped {$unwind:"$stores"}, {$unwind:"$stores.offers"}, {$match:{"stores.offers.size": 'L'}}, {$group:{ _id:{id:"$_id", "storesId":"$stores._id"}, "offers":{$push:"$stores.offers"} }}, {$group:{ _id:"$_id.id", stores:{$push:{_id:"$_id.storesId","offers":"$offers"}} }} ]).pretty() 

O que essa consulta faz é desenrolar matrizes (duas vezes), em seguida, corresponde ao tamanho e, em seguida, remodela o documento para o formulário anterior. Você pode remover as etapas do $ group e ver como ele é impresso. Divirta-se!