I can't find anywhere in mongo documentation how to do this kind of query:
I've got a collection containing lots of objects, some of which have the type I want (let's call it "Graph"). They contain a property that contains nodes and edges. I'm trying to find every Graph object that contains at least two edges that have the same end node and the same value for a specific property (let's call it type
).
So my data looks something like this:
[{
type: "Graph",
name: "Graph A",
graph: {
nodes: [
{ id: 1},
{ id: 2},
{ id: 3}
],
edges: [
{ id: 4, type: "A", start: 1, end: 3 },
{ id: 5, type: "A", start: 2, end: 3 }
]
}
},
{
type: "Graph",
name: "Graph B",
graph: {
nodes: [
{ id: 1},
{ id: 2},
{ id: 3},
{ id: 4}
],
edges: [
{ id: 5, type: "A", start: 1, end: 3 },
{ id: 6, type: "B", start: 2, end: 3 },
{ id: 7, type: "A", start: 2, end: 4 }
]
}
}]
I'm looking for a query that gives me Graph A but not Graph B, because Graph A contains two edges where both type
and end
match, and Graph B doesn't.
Edit: Best option I've been able to find so far, is a $where containing a $function, but I can't get it to work. It also doesn't select on { type: "Graph" } yet.
{ $where: { $function: {
body: function(edges) {
return edges.some(a => edges.some(b => a!==b && a.type === b.type && a.end === b.end));
},
args: [ "$graph.edges" ],
lang: "js"
} } }
$match
on type=Graph
$map
each edge to a string in the form "<edge.type>|<edge.end>"
.
type
is always a string and end
is always a number and neither are null
, then the result is an array like ["A|3", "A|3"]
or ["A|3", "B|3", "A|4"]
.|
separator is optional, it could even be like ["A3", "A3"]
or ["A3", "B3", "A4"]
.$filter
the array of strings to exclude any null
values. If the 'type' or 'edge' is null
or missing, the resulting concatenation is null. So this will remove those entries before proceeding.
$map
stage above but I prefer keeping things separate.Use $setIntersection
to convert that array into a set - actually an array of unique values, by intersecting (or union) with itself.
If there is a repetition of type|end
ie. occurred more than once ie "at least two", then the original array and its "set" of unique value would have different lengths; by checking $size
.
db.collection.aggregate([
{ $match: { type: "Graph" } },
{
$set: {
allTypeEnds: {
$map: {
input: "$graph.edges",
in: {
$concat: ["$$this.type", "|", { $toString: "$$this.end" } ]
}
}
}
}
},
{
$set: {
allTypeEnds: {
$filter: { input: "$allTypeEnds", cond: "$$this" }
}
}
},
{
$match: {
$expr: {
$ne: [
{ $size: "$allTypeEnds" },
{ $size: { $setIntersection: ["$allTypeEnds", "$allTypeEnds"] } }
]
}
}
},
{ $unset: "allTypeEnds" }
])
Updated Mongo Playground - note the multiple edges added in the data for Graph B with {end: 3}
and no type
field; and it won't be in the results.
Btw, that aggregation pipeline could be turned into a find
query but it's ugly and really hard to modify later; I DO NOT RECOMMEND writing it like this, just showing that it's possible.
db.collection.find({
$expr: {
$and: [
{ type: "Graph" },
{
$ne: [
{
$size: {
$filter: {
input: {
$map: {
input: "$graph.edges",
in: {
$concat: ["$$this.type", "|", { $toString: "$$this.end" }]
}
}
},
cond: "$$this"
}
}
},
{
$size: {
$setIntersection: [
{
$filter: {
input: {
$map: {
input: "$graph.edges",
in: {
$concat: ["$$this.type", "|", { $toString: "$$this.end" }]
}
}
},
cond: "$$this"
}
},
{
$filter: {
input: {
$map: {
input: "$graph.edges",
in: {
$concat: ["$$this.type", "|", { $toString: "$$this.end" }]
}
}
},
cond: "$$this"
}
}
]
}
}
]
}
]
}
})