Search code examples
mongodb

Mongo Compass find elements containing a list where properties match


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"
} } }

Solution

    1. $match on type=Graph

    2. $map each edge to a string in the form "<edge.type>|<edge.end>".

      • If 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"].
      • The pipe | separator is optional, it could even be like ["A3", "A3"] or ["A3", "B3", "A4"].
    3. $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.

    4. Use $setIntersection to convert that array into a set - actually an array of unique values, by intersecting (or union) with itself.

      • This is why I'm converting each type & edge into a single string - they can be used as values in a Set.
    5. 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"
                      }
                    }
                  ]
                }
              }
            ]
          }
        ]
      }
    })
    

    Mongo Playground demo of bad way to write this