Home > Mobile >  Mongo db - How perform unwind and match with condition
Mongo db - How perform unwind and match with condition

Time:11-27

Let's say my products collection include products that each one has items of array as below.

 [
      {
        "_id": "1",
        "score": 200,
        "items": [
          {
            "_id": "1",
            "title": "title1",
            "category": "sport"
          },
          {
            "_id": "2",
            "title": "title2",
            "category": "sport"
          },
          {
            "_id": "3",
            "title": "title3",
            "category": "tv"
          },
          {
            "_id": "4",
            "title": "title4",
            "category": "movies"
          }
        ]
      },
      {
        "_id": "2",
        "score": 1000000000,
        "items": [
          {
            "_id": "9",
            "title": "titleBoo",
            "category": "food"
          },
          {
            "title": "title4",
            "category": "movies"
          },
          {
            "title": "titlexx",
            "category": "food"
          },
          {
            "title": "titl113",
            "category": "sport"
          }
        ]
      },
      {
        "_id": "3",
        "score": 500,
        "items": [
          {
            "title": "title3",
            "category": "movies"
          },
          {
            "title": "title3",
            "category": "food"
          },
          {
            "title": "title3",
            "category": "sport"
          },
          {
            "title": "title3",
            "category": "sport"
          }
        ]
      }
    ]

I want to return Single Item by category that has the highest score by category, and if no category matched just return random/first product that have max score.

Example for category "food", the result should be:

   {
     "_id" : "9",
      "title": "titleBoo",
      "category": "food"
    }

because it has the max score of 1000000000

and for other non exists category "Foo" the result should be some random from highest score product item let's say

{
      "title": "titlexx",
      "category": "food"
    },

Basically what I did using java spring data aggregation pipeline

Aggregation agg1 = newAggregation(
            unwind("items"),
            match(Criteria.where("items.category").is(category)),
            group().max("score").as("score")
    );

 BasicDBObject result =    mongoTemplate.aggregate(
            agg1, "products", BasicDBObject.class).getUniqueMappedResult();


 if (result empty) { // didn't find any matched category so without match step !

   Aggregation agg2 = newAggregation(
                unwind("items"),
                group().max("score").as("score")
        );

    // take some item inside max  "score"
     BasicDBObject res2 =    mongoTemplate.aggregate(
                agg2, "products", BasicDBObject.class).getUniqueMappedResult();

    System.out.print(res2);

}

This code not ideal as I need to perform "unwind" twice (if not matched) do another time .. I know there is $cond / switch function, I'm wondering if I can use after unwind some switch case operation like here:

 Aggregation agg = newAggregation(
                unwind("items"),
               // switch-case {
                      a. match(Criteria.where("items.category").is(category)),
                          if (result or size > 0) {
                               group().max("score").as("score") // max on matched result by category
                           } 
                 
                      b. group().max("score").as("score"). // max on random unwind score
                 }
        );

     BasicDBObject result =    mongoTemplate.aggregate(
                agg, "products", BasicDBObject.class).getUniqueMappedResult();

Any hints ?

CodePudding user response:

Following the advice by @user20042973, one option is using $setWindowFields and $facet. It can also be done without steps 1-3, but since $unwind is considered as a less-efficient step, and $facet is not using the index, adding steps 1-3 may reduce a large part of the documents before these operations, and leave you with only two documents. After the $match step, we only left one document with the best score and documents that contain the wanted category (if there are any), sorted by the score (from the $setWindowFields step). This means we only want the first document (best score) or the second document if exist, which is highest score that guaranteed to have the category in it. So we can limit the reset of our search to these 2 documents:

db.collection.aggregate([
  {$setWindowFields: {
      sortBy: {score: -1},
      output: {bestScore: {$max: "$score"}}
  }},
  {$match: {$expr: {
        $or: [
          {$eq: ["$score", "$bestScore"]}, 
          {$in: [category, "$items.category"]}
        ]
  }}},
  {$limit: 2},
  {$unwind: "$items"},
  {$facet: {
      category: [{$match: {"items.category": category}}, {$limit: 1}],
      other: [{$limit: 1}]
  }},
  {$replaceRoot: {newRoot: {
        $cond: [
          {$eq: [{$size: "$category"}, 1]},
          {$first: "$category"},
          {$first: "$other"}
        ]
  }}}
])

See how it works on the playground example.

You can also use $reduce to avoid the $unwind step altogether, but at this point it should have a minor effect.

  • Related