Recupere apenas o elemento consultado em uma matriz de objetos na coleção MongoDB

377

Suponha que você tenha os seguintes documentos em minha coleção:

{  
   "_id":ObjectId("562e7c594c12942f08fe4192"),
   "shapes":[  
      {  
         "shape":"square",
         "color":"blue"
      },
      {  
         "shape":"circle",
         "color":"red"
      }
   ]
},
{  
   "_id":ObjectId("562e7c594c12942f08fe4193"),
   "shapes":[  
      {  
         "shape":"square",
         "color":"black"
      },
      {  
         "shape":"circle",
         "color":"green"
      }
   ]
}

Faça a consulta:

db.test.find({"shapes.color": "red"}, {"shapes.color": 1})

Ou

db.test.find({shapes: {"$elemMatch": {color: "red"}}}, {"shapes.color": 1})

Retorna o documento correspondente (Documento 1) , mas sempre com TODOS os itens da matriz em shapes:

{ "shapes": 
  [
    {"shape": "square", "color": "blue"},
    {"shape": "circle", "color": "red"}
  ] 
}

No entanto, gostaria de obter o documento (Documento 1) apenas com a matriz que contém color=red:

{ "shapes": 
  [
    {"shape": "circle", "color": "red"}
  ] 
}

Como posso fazer isso?

Sebtm
fonte

Respostas:

416

O novo $elemMatchoperador de projeção do MongoDB 2.2 fornece outra maneira de alterar o documento retornado para conter apenas o primeiroshapes elemento correspondente :

db.test.find(
    {"shapes.color": "red"}, 
    {_id: 0, shapes: {$elemMatch: {color: "red"}}});

Devoluções:

{"shapes" : [{"shape": "circle", "color": "red"}]}

Na 2.2, você também pode fazer isso usando o $ projection operator, em que o $nome do campo no objeto de projeção representa o índice do primeiro elemento da matriz correspondente do campo na consulta. O seguinte retorna os mesmos resultados acima:

db.test.find({"shapes.color": "red"}, {_id: 0, 'shapes.$': 1});

Atualização do MongoDB 3.2

A partir da versão 3.2, é possível usar o novo $filteroperador de agregação para filtrar uma matriz durante a projeção, que tem o benefício de incluir todas as correspondências, em vez de apenas a primeira.

db.test.aggregate([
    // Get just the docs that contain a shapes element where color is 'red'
    {$match: {'shapes.color': 'red'}},
    {$project: {
        shapes: {$filter: {
            input: '$shapes',
            as: 'shape',
            cond: {$eq: ['$$shape.color', 'red']}
        }},
        _id: 0
    }}
])

Resultados:

[ 
    {
        "shapes" : [ 
            {
                "shape" : "circle",
                "color" : "red"
            }
        ]
    }
]
JohnnyHK
fonte
16
alguma solução se eu quiser retornar todos os elementos que correspondem a ele, em vez de apenas o primeiro?
Steve Ng
Eu tenho medo Eu estou usando Mongo 3.0.x :-(
charliebrownie
@charliebrownie Em seguida, use uma das outras respostas usadas aggregate.
precisa saber é o seguinte
essa consulta retorna apenas a matriz "shapes" e não retorna outros campos. Alguém sabe como retornar outros campos também?
27616 Mark Thien
11
Isso também funciona:db.test.find({}, {shapes: {$elemMatch: {color: "red"}}});
Paulo
97

O novo Aggregation Framework no MongoDB 2.2+ fornece uma alternativa ao Map / Reduce. O $unwindoperador pode ser usado para separar sua shapesmatriz em um fluxo de documentos que podem ser comparados:

db.test.aggregate(
  // Start with a $match pipeline which can take advantage of an index and limit documents processed
  { $match : {
     "shapes.color": "red"
  }},
  { $unwind : "$shapes" },
  { $match : {
     "shapes.color": "red"
  }}
)

Resulta em:

{
    "result" : [
        {
            "_id" : ObjectId("504425059b7c9fa7ec92beec"),
            "shapes" : {
                "shape" : "circle",
                "color" : "red"
            }
        }
    ],
    "ok" : 1
}
Stennie
fonte
7
@JohnnyHK: Nesse caso, $elemMatché outra opção. Na verdade, cheguei aqui por meio de uma pergunta do Grupo do Google em que $ elemMatch não funcionaria porque retorna apenas a primeira correspondência por documento.
Stennie
11
Obrigado, eu não estava ciente dessa limitação, então é bom saber. Desculpe por excluir o meu comentário ao qual você está respondendo, decidi postar outra resposta e não queria confundir as pessoas.
JohnnyHK
3
@JohnnyHK: Não se preocupe, agora há três respostas úteis para a questão ;-)
Stennie
Para outros pesquisadores, além disso, tentei adicionar { $project : { shapes : 1 } }- o que parecia funcionar e seria útil se os documentos anexos fossem grandes e você desejasse apenas visualizar os shapesvalores-chave.
user1063287
2
@calmbird Atualizei o exemplo para incluir um estágio inicial de $ $. Se você estiver interessado em uma sugestão de recurso mais eficiente, assista / recomende o SERVER-6612: Suporte na projeção de vários valores de matriz em uma projeção como o especificador de projeção $ elemMatch no rastreador de problemas do MongoDB.
Stennie
30

Outra maneira interessante é usar $ redact , que é um dos novos recursos de agregação do MongoDB 2.6 . Se você estiver usando o 2.6, não precisará de um $ desenrolar, o que pode causar problemas de desempenho se você tiver grandes matrizes.

db.test.aggregate([
    { $match: { 
         shapes: { $elemMatch: {color: "red"} } 
    }},
    { $redact : {
         $cond: {
             if: { $or : [{ $eq: ["$color","red"] }, { $not : "$color" }]},
             then: "$$DESCEND",
             else: "$$PRUNE"
         }
    }}]);

$redact "restringe o conteúdo dos documentos com base nas informações armazenadas nos próprios documentos" . Portanto, ele será executado apenas dentro do documento . Basicamente, ele digitaliza seu documento de cima para baixo e verifica se ele corresponde à sua ifcondição $cond, se houver, ele manterá o conteúdo ( $$DESCEND) ou removerá ($$PRUNE ).

No exemplo acima, primeiro $matchretorna todoshapes matriz e $ redact a reduz para o resultado esperado.

Observe que {$not:"$color"}é necessário, porque ele também digitalizará o documento superior e, se $redactnão encontrar um colorcampo no nível superior, isso retornará, o falseque poderá remover o documento inteiro que não queremos.

anvarik
fonte
11
resposta perfeita. Como você mencionou, $ unwind consumirá muita RAM. Portanto, isso será melhor quando comparado.
precisa
Tenho uma dúvida. No exemplo, "formas" é uma matriz. O "$ redact" varrerá todos os objetos na matriz "shapes" ?? Como isso será bom em relação ao desempenho?
precisa
nem tudo, mas o resultado da sua primeira partida. Essa é a razão por que você colocou $matchcomo seu primeiro estágio agregado
anvarik
okkk .. se um índice criado no campo "color", mesmo assim ele varrerá todos os objetos no array "shapes" ??? Qual poderia ser a maneira eficiente de combinar vários objetos em uma matriz ???
precisa saber é
2
Brilhante! Eu não entendo como $ eq funciona aqui. Deixei-o originalmente e isso não funcionou para mim. De alguma forma, ele procura na matriz de formas encontrar a correspondência, mas a consulta nunca especifica em qual matriz procurar. Como se os documentos tivessem formas e, por exemplo, tamanhos; $ eq procuraria nas duas matrizes por correspondências? $ Redact está apenas procurando algo no documento que corresponda à condição 'if'?
Onosa
30

Cuidado: Esta resposta fornece uma solução relevante naquele momento , antes da introdução dos novos recursos do MongoDB 2.2 ou superior. Veja as outras respostas se você estiver usando uma versão mais recente do MongoDB.

O parâmetro do seletor de campo é limitado às propriedades completas. Não pode ser usado para selecionar parte de uma matriz, apenas a matriz inteira. Eu tentei usar o operador posicional $ , mas isso não funcionou.

A maneira mais fácil é filtrar apenas as formas no cliente .

Se você realmente precisa da saída correta diretamente do MongoDB, pode usar uma redução de mapa para filtrar as formas.

function map() {
  filteredShapes = [];

  this.shapes.forEach(function (s) {
    if (s.color === "red") {
      filteredShapes.push(s);
    }
  });

  emit(this._id, { shapes: filteredShapes });
}

function reduce(key, values) {
  return values[0];
}

res = db.test.mapReduce(map, reduce, { query: { "shapes.color": "red" } })

db[res.result].find()
Niels van der Rest
fonte
24

Melhor você pode consultar o elemento da matriz correspondente usando, $sliceé útil retornar o objeto significativo em uma matriz.

db.test.find({"shapes.color" : "blue"}, {"shapes.$" : 1})

$sliceé útil quando você conhece o índice do elemento, mas às vezes deseja que qualquer elemento da matriz corresponda aos seus critérios. Você pode retornar o elemento correspondente com o $operador.

Narendran
fonte
19
 db.getCollection('aj').find({"shapes.color":"red"},{"shapes.$":1})

SAÍDAS

{

   "shapes" : [ 
       {
           "shape" : "circle",
           "color" : "red"
       }
   ]
}
Viral Patel
fonte
12

A sintaxe para encontrar no mongodb é

    db.<collection name>.find(query, projection);

e a segunda consulta que você escreveu, ou seja,

    db.test.find(
    {shapes: {"$elemMatch": {color: "red"}}}, 
    {"shapes.color":1})

nisto você usou o $elemMatchoperador na parte de consulta, enquanto que se você usar esse operador na parte de projeção, obterá o resultado desejado. Você pode anotar sua consulta como

     db.users.find(
     {"shapes.color":"red"},
     {_id:0, shapes: {$elemMatch : {color: "red"}}})

Isso lhe dará o resultado desejado.

Vicky
fonte
11
Isso funciona para mim. No entanto, parece que "shapes.color":"red"o parâmetro de consulta (o primeiro parâmetro do método find) não é necessário. Você pode substituí-lo {}e obter os mesmos resultados.
Erik Olson
2
@ErikOlson Sua sugestão é a correta no caso acima, onde precisamos encontrar todo o documento que estiver em vermelho e aplicar a projeção apenas neles. Mas digamos que se alguém precisar descobrir todo o documento que possui a cor azul, mas ele deve retornar apenas os elementos dessa matriz de formas que possuem a cor vermelha. Neste caso, a consulta acima pode ser referenciado por outra pessoa também ..
Vicky
Parece ser o mais fácil, mas não posso fazê-lo funcionar. Ele retorna apenas o primeiro subdocumento correspondente.
newman
8

Graças a JohnnyHK .

Aqui eu só quero adicionar um uso mais complexo.

// Document 
{ 
"_id" : 1
"shapes" : [
  {"shape" : "square",  "color" : "red"},
  {"shape" : "circle",  "color" : "green"}
  ] 
} 

{ 
"_id" : 2
"shapes" : [
  {"shape" : "square",  "color" : "red"},
  {"shape" : "circle",  "color" : "green"}
  ] 
} 


// The Query   
db.contents.find({
    "_id" : ObjectId(1),
    "shapes.color":"red"
},{
    "_id": 0,
    "shapes" :{
       "$elemMatch":{
           "color" : "red"
       } 
    }
}) 


//And the Result

{"shapes":[
    {
       "shape" : "square",
       "color" : "red"
    }
]}
Eddy
fonte
7

Você só precisa executar a consulta

db.test.find(
{"shapes.color": "red"}, 
{shapes: {$elemMatch: {color: "red"}}});

a saída desta consulta é

{
    "_id" : ObjectId("562e7c594c12942f08fe4192"),
    "shapes" : [ 
        {"shape" : "circle", "color" : "red"}
    ]
}

conforme o esperado, ele fornece o campo exato da matriz que corresponde à cor: 'vermelho'.

Vaibhav Patil
fonte
3

junto com $ project, será mais apropriado que outros elementos de correspondência sábios sejam combinados com outros elementos no documento.

db.test.aggregate(
  { "$unwind" : "$shapes" },
  { "$match" : {
     "shapes.color": "red"
  }},
{"$project":{
"_id":1,
"item":1
}}
)
shakthydoss
fonte
você pode descrever que isso é realizado com um conjunto de entrada e saída?
Alexander Mills
2

Da mesma forma, você pode encontrar para os múltiplos

db.getCollection('localData').aggregate([
    // Get just the docs that contain a shapes element where color is 'red'
  {$match: {'shapes.color': {$in : ['red','yellow'] } }},
  {$project: {
     shapes: {$filter: {
        input: '$shapes',
        as: 'shape',
        cond: {$in: ['$$shape.color', ['red', 'yellow']]}
     }}
  }}
])
ashishSober
fonte
Essa resposta é realmente a maneira preferida do 4.x: $matchreduzir o espaço e, em seguida, $filtermanter o que você deseja, sobrescrevendo o campo de entrada (use a saída de $filterno campo shapespara $projectretornar ao shapes. Nota de estilo: melhor não usar o nome do campo como o asargumento porque isso pode levar a confusão mais tarde com $$shapee $shape.. Eu prefiro zzcomo o ascampo porque realmente se destaca.
Buzz Moschetti
1
db.test.find( {"shapes.color": "red"}, {_id: 0})
Poonam Agrawal
fonte
11
Bem-vindo ao Stack Overflow! Obrigado pelo snippet de código, que pode fornecer ajuda imediata e limitada. Uma explicação adequada melhoraria bastante seu valor a longo prazo , descrevendo por que essa é uma boa solução para o problema e a tornaria mais útil para futuros leitores com outras perguntas semelhantes. Edite sua resposta para adicionar alguma explicação, incluindo as suposições que você fez.
sepehr
1

Use a função de agregação e $projectpara obter um campo de objeto específico no documento

db.getCollection('geolocations').aggregate([ { $project : { geolocation : 1} } ])

resultado:

{
    "_id" : ObjectId("5e3ee15968879c0d5942464b"),
    "geolocation" : [ 
        {
            "_id" : ObjectId("5e3ee3ee68879c0d5942465e"),
            "latitude" : 12.9718313,
            "longitude" : 77.593551,
            "country" : "India",
            "city" : "Chennai",
            "zipcode" : "560001",
            "streetName" : "Sidney Road",
            "countryCode" : "in",
            "ip" : "116.75.115.248",
            "date" : ISODate("2020-02-08T16:38:06.584Z")
        }
    ]
}
KARTHIKEYAN.A
fonte
0

Embora a pergunta tenha sido feita há 9,6 anos, isso tem sido de grande ajuda para inúmeras pessoas, sendo eu uma delas. Obrigado a todos por todas as suas perguntas, sugestões e respostas. Pegando uma das respostas aqui .. Descobri que o método a seguir também pode ser usado para projetar outros campos no documento pai. Isso pode ser útil para alguém.

Para o documento a seguir, era necessário descobrir se um funcionário (emp # 7839) tem seu histórico de férias definido para o ano 2020. O histórico de férias é implementado como um documento incorporado ao documento do funcionário.

db.employees.find( {"leave_history.calendar_year": 2020}, 
    {leave_history: {$elemMatch: {calendar_year: 2020}},empno:true,ename:true}).pretty()


{
        "_id" : ObjectId("5e907ad23997181dde06e8fc"),
        "empno" : 7839,
        "ename" : "KING",
        "mgrno" : 0,
        "hiredate" : "1990-05-09",
        "sal" : 100000,
        "deptno" : {
                "_id" : ObjectId("5e9065f53997181dde06e8f8")
        },
        "username" : "none",
        "password" : "none",
        "is_admin" : "N",
        "is_approver" : "Y",
        "is_manager" : "Y",
        "user_role" : "AP",
        "admin_approval_received" : "Y",
        "active" : "Y",
        "created_date" : "2020-04-10",
        "updated_date" : "2020-04-10",
        "application_usage_log" : [
                {
                        "logged_in_as" : "AP",
                        "log_in_date" : "2020-04-10"
                },
                {
                        "logged_in_as" : "EM",
                        "log_in_date" : ISODate("2020-04-16T07:28:11.959Z")
                }
        ],
        "leave_history" : [
                {
                        "calendar_year" : 2020,
                        "pl_used" : 0,
                        "cl_used" : 0,
                        "sl_used" : 0
                },
                {
                        "calendar_year" : 2021,
                        "pl_used" : 0,
                        "cl_used" : 0,
                        "sl_used" : 0
                }
        ]
}
Todos
fonte