I have following documents:
doc1: {
'array': [
{'field': 'ABC', 'enabled': 'false'},
{'field': 'BCD', 'enabled': 'true'},
{'field': 'DEF', 'enabled': 'false'},
{'field': 'XYZ', 'enabled': 'true'},
]
}
doc2: {
'array': [
{'field': 'ABC', 'enabled': 'true'},
{'field': 'BCD', 'enabled': 'true'},
{'field': 'DEF', 'enabled': 'false'},
{'field': 'XYZ', 'enabled': 'true'},
]
}
I'm performing search by particular field.
I want to get all documents that have this field enabled, and there are no other fields enabled before this (in the list).
For ex.:
search for field: BCD
, enabled: true - should return only first document (because in second document ABC
is enabled too).
search for XYZ
, enabled: true - should not return any documents at all, because there are other fields enabled on top of this list.
I was trying to do smth with $elemMatch
, but I don't know if it's possible to apply filter on top of element that elemMatch
has matched.
Any suggestions?
Best probably to search using $where
in addition to the normal query, and still keep things on the server:
db.getCollection('collection').find({
"array": {
"$elemMatch": { "field": "BCD", "enabled": "true" },
},
"$where": function() {
return this.array.map((e,i) => Object.assign(e,{ i }))
.filter( e => e.field === "BCD" && e.enabled === "true" )
.map( e => e.i )[0] <=
this.array.map(e => e.enabled).indexOf("true")
}
})
And if you have MongoDB 3.4 with support for $indexOfArray
and $range
, then it may look longer but it actually most efficient with $redact
:
db.getCollection('collection').aggregate([
{ "$match": {
"array": {
"$elemMatch": { "field": "BCD", "enabled": "true" },
}
}},
{ "$redact": {
"$cond": {
"if": {
"$lte": [
{ "$arrayElemAt": [
{ "$map": {
"input": {
"$filter": {
"input": {
"$map": {
"input": {
"$zip": {
"inputs": [
"$array",
{ "$range": [0, { "$size": "$array" }] }
]
}
},
"as": "a",
"in": {
"field": { "$arrayElemAt": [ "$$a.field", 0 ] },
"enabled": { "$arrayElemAt": [ "$$a.enabled", 0 ] },
"index": { "$arrayElemAt": [ "$$a", 1 ] }
}
}
},
"as": "a",
"cond": {
"$and": [
{ "$eq": [ "$$a.field", "BCD" ] },
{ "$eq": [ "$$a.enabled", "true" ] }
]
}
}
},
"as": "a",
"in": "$$a.index"
}},
0
]},
{ "$indexOfArray": [ "$array.enabled", "true" ] }
]
},
"then": "$$KEEP",
"else": "$$PRUNE"
}
}}
])
So there really is no actual query operation that enforces that, but both of these cases keep the selection "on the server" as opposed to sending data over the wire to the client and then filtering.
Because if you do that it kind of negates the purpose of using a database in the first place. So you really want this to happen on the server.
Why not search by enabled
field and then check if field
is appropriate or not?
db.collection("col").findOne({
"array.enabled": true
}, {
array: {
$elemMatch {
enabled: true
}
}
})
.then(function(docs){
docs.forEach(function(doc){
if(doc.array[0].field == "ABC"){
// Ok, we get it
}
})
})
Second argument to find
is projection, so application doesn't download complete array from document, but only first matched element of it.
There is $where
keyword that allow to generate complex conditions for similar purpose.
find($where: 'this.array.filter(function(e){return e.enabled=="true"})[0].field=="ABC"')
As this does not use any indexes, I'll add more conditions to benefit from optimisation.