Search code examples
mongodbmongodb-queryaggregation-frameworkaggregatemongodb-atlas

Apply match filter but ignore first element


My app can search through a database of resources using MongoDB's aggregation pipeline. Some of these resources are marked as sponsored via a property.

I want to show these sponsored entries first (already done) but I want to show only one of them.

What I have now is this:

  • [sponsored result #1]
  • [sponsored result #2]
  • [sponsored result #3]
  • [organic result #1]
  • [organic result #2]
  • [organic result #3]
  • [...]

What I want:

  • [sponsored result #1]
  • [organic result #1]
  • [organic result #2]
  • [organic result #3]
  • [...]

Below is my aggregation code (with Mongoose syntax). How can I skip elements with sponsored: true except for the first one?

[...]

const matchFilter: { approved: boolean, type?: QueryOptions, format?: QueryOptions, difficulty?: QueryOptions, language?: QueryOptions }
    = { approved: true }

if (typeFilter) matchFilter.type = { $in: typeFilter };
if (formatFilter) matchFilter.format = { $in: [...formatFilter, 'videoandtext'] };
if (difficultyFilter) matchFilter.difficulty = { $in: difficultyFilter };
if (languageFilter) matchFilter.language = { $in: languageFilter };

const aggregationResult = await Resource.aggregate()
    .search({
        compound: {
            must: [
                [...]
            ],
            should: [
                [...]
            ]
        }
    })
    [...]
    .sort(
        {
            sponsored: -1,
            _id: 1
        }
    )
    .facet({
        results: [
            { $match: matchFilter },
            { $skip: (page - 1) * pageSize },
            { $limit: pageSize },
        ],
        totalResultCount: [
            { $match: matchFilter },
            { $group: { _id: null, count: { $sum: 1 } } }
        ],
        [...]
    })
    .exec();

[...]

Solution

  • One option is to change your $facet a bit:

    1. You can get the $match out of the $facet since it is relevant to all pipelines.
    2. instead of two pipelines, one for the results and one for the counting, we have now three: one more for sponsored documents.
    3. $project to concatenate sponsored and notSponsored docs
    db.collection.aggregate([
      {$sort: {sponsored: -1, _id: 1}},
      {$match: matchFilter },
      {$facet: {
          notSponsored: [
            {$match: {sponsored: false}},
            {$skip: (page - 1) * pageSize },
            {$limit: pageSize },
          ],
          sposerted: [
            {$match: {sponsored: true}},
            {$limit: numberOfSponsoreditemsWanted}
          ],
          count: [
            {$match: {sponsored: false}},
            {$count: "total"}
          ]
        }
      },
      {$project: {
          results: {$concatArrays: ["$sposerted", "$notSponsored"]},
          totalResultCount: {$first: "$count.total"}
        }
      }
    ])
    

    See how it works on the playground example