Mongo/mongoose $facet filters, return all product's brands/tags in response if customer applied filters

Viewed 1248

I have this endpoint, it's the initial endpoint when a customer is visiting the eshop:

export const getAllProductsByCategory = async (req, res, next) => {
  const pageSize = parseInt(req.query.pageSize);
  const sort = parseInt(req.query.sort);
  const skip = parseInt(req.query.skip);
  const { order, filters } = req.query;
  const { brands, tags, pricesRange } = JSON.parse(filters);

  try {
    const aggregate = Product.aggregate();

    aggregate.lookup({
      from: 'categories',
      localField: 'categories',
      foreignField: '_id',
      as: 'categories'
    });

    aggregate.match({
      productType: 'product',
      available: true,
      categories: {
        $elemMatch: {
          url: req.params
        }
      }
    });

    aggregate.lookup({
      from: 'tags',
      let: { tags: '$tags' },
      pipeline: [
        {
          $match: {
            $expr: { $in: ['$_id', '$$tags'] }
          }
        },
        {
          $project: {
            _id: 1,
            name: 1,
            slug: 1
          }
        }
      ],
      as: 'tags'
    });

    aggregate.lookup({
      from: 'brands',
      let: { brand: '$brand' },
      pipeline: [
        {
          $match: {
            $expr: { $eq: ['$_id', '$$brand'] }
          }
        },
        {
          $project: {
            _id: 1,
            name: 1,
            slug: 1
          }
        }
      ],
      as: 'brand'
    });

    if (brands.length > 0) {
      const filterBrands = brands.map((_id) => utils.toObjectId(_id));
      aggregate.match({
        $and: [{ brand: { $elemMatch: { _id: { $in: filterBrands } } } }]
      });
    }

    if (tags.length > 0) {
      const filterTags = tags.map((_id) => utils.toObjectId(_id));
      aggregate.match({ tags: { $elemMatch: { _id: { $in: filterTags } } } });
    }

    if (pricesRange.length > 0 && pricesRange !== 'all') {
      const filterPriceRange = pricesRange.map((_id) => utils.toObjectId(_id));
      aggregate.match({
        _id: { $in: filterPriceRange }
      });
    }

    aggregate.facet({
      tags: [
        { $unwind: { path: '$tags' } },
        { $group: { _id: '$tags', tag: { $first: '$tags' }, total: { $sum: 1 } } },
        {
          $group: {
            _id: '$tag._id',
            name: { $addToSet: '$tag.name' },
            total: { $addToSet: '$total' }
          }
        },
        {
          $project: {
            name: { $arrayElemAt: ['$name', 0] },
            total: { $arrayElemAt: ['$total', 0] },
            _id: 1
          }
        },
        { $sort: { total: -1 } }
      ],
      brands: [
        { $unwind: { path: '$brand' } },
        {
          $group: {
            _id: '$brand._id',
            name: { $first: '$brand.name' },
            slug: { $first: '$brand.slug' },
            total: {
              $sum: 1
            }
          }
        },
        { $sort: { name: 1 } }
      ],

      pricesRange: [
        {
          $bucket: {
            groupBy: {
              $cond: {
                if: { $ne: ['$onSale.value', true] },
                then: '$price',
                else: '$sale.salePrice'
              }
            },
            boundaries: [0, 20.01, 50.01],
            default: 'other',
            output: {
              count: { $sum: 1 },
              products: { $push: '$_id' }
            }
          }
        }
      ],
      products: [
        { $skip: (skip - 1) * pageSize },
        { $limit: pageSize },
        {
          $project: {
            _id: 1,
            images: 1,
            onSale: 1,
            price: 1,
            quantity: 1,
            slug: 1,
            sale: 1,
            sku: 1,
            status: 1,
            title: 1,
            brand: 1,
            tags: 1,
            description: 1
          }
        },
        { $sort: { [order]: sort } }
      ],
      total: [
        {
          $group: {
            _id: null,
            count: { $sum: 1 }
          }
        },
        {
          $project: {
            count: 1,
            _id: 0
          }
        }
      ]
    });

    aggregate.addFields({
      total: {
        $arrayElemAt: ['$total', 0]
      }
    });

    const [response] = await aggregate.exec();
    if (!response.total) {
      response.total = 0;
    }

    res.status(httpStatus.OK);
    return res.json(response);
  } catch (error) {
    console.log(error);
    return next(error);
  }
};

If no filters are applied all products matches the category requested with no problem.

My issue is when a customer selects a brand or tag, then the facet returns the products, but returns only one brand/tag (as it should be since the products filtered have only this brand).

What I must do in order to retain all brands/tags and let the user select more than one brand/tag? If customer selects a brand, then the tags should match the returned products tags and vice versa.

Is there a better way to implement tags stage in $facet since tags is an array and the desired output is: [{_id: 123, name: {label: 'test', value: 123]}]

The request is like:(1,2,3,4 represents _id)

http://locahost:3000/get-products/?filters={brands: [1, 2], tags: [3,4], pricesRange:[]}

Update

This is the products schema with tags and brands:

 brand: {
      ref: 'Brand',
      type: Schema.Types.ObjectId
    },
  tags: [
      {
        ref: 'Tags',
        type: Schema.Types.ObjectId
      }
    ]

tags schema:

{
    metaDescription: {
      type: String
    },
    metaTitle: {
      type: String
    },
    name: {
      label: {
        type: String,
        index: true
      },
      value: {
        type: Schema.Types.ObjectId
      },
    },
    slug: {
      type: String,
      index: true
    },
    status: {
      label: {
        type: String
      },
      value: {
        default: true,
        type: Boolean
      }
    }
  }

brands schema:

description: {
    default: '',
    type: String
  },
  name: {
    required: true,
    type: String,
    unique: true
  },
  slug: {
    type: String,
    index: true
  },
  status: {
    label: {
      default: 'Active',
      type: String
    },
    value: {
      default: true,
      type: Boolean
    }
  }

Scenario:

User visits store, selects a category and all matching products should return with matched brands, tags, priceRange & pagination.

Case 1:

User clicks a brand from checkbox, then the request returns matching products,tags & priceRanges and all brands of the selected category, not of matched products

Case 2:

User selects a brand like Case 1, but then decides to check a tag too, then the request should return all brands and tags again, but products matched against them.

Case 3:

User do not select brand but selects a tag only, the request should return all matching products that have that tag/tags and return the brands that matched the products returned.

Case 4:

Same as case 3, but user selects a brand after selecting a tag/tags, the request should return matching products, brands & tags.

In all cases pagination should return proper total, also priceRanges should match the returned results.

I hope it's clear now, I think I've not missed any other case. I could probably grey out/disable the tags/brands that do not match the response in the front end but I don't know if this is user friendly.

1 Answers

This is what I ended up with:

export const getAllProductsByCategory = async (req, res, next) => {
  const pageSize = parseInt(req.query.pageSize);
  const sort = parseInt(req.query.sort);
  const skip = parseInt(req.query.skip);
  const { order, filters } = req.query;
  const { brands, tags, pricesRange } = JSON.parse(filters);

  try {
    const aggregate = Product.aggregate();

    aggregate.lookup({
      from: 'categories',
      localField: 'categories',
      foreignField: '_id',
      as: 'categories'
    });

    aggregate.match({
      productType: 'product',
      available: true,
      categories: {
        $elemMatch: {
          url: `/${JSON.stringify(req.params['0']).replace(/"/g, '')}`
        }
      }
    });

    aggregate.lookup({
      from: 'tags',
      let: { tags: '$tags' },
      pipeline: [
        {
          $match: {
            $expr: { $in: ['$_id', '$$tags'] }
          }
        },
        {
          $project: {
            _id: 1,
            name: 1,
            slug: 1
          }
        }
      ],
      as: 'tags'
    });

    aggregate.lookup({
      from: 'brands',
      let: { brand: '$brand' },
      pipeline: [
        {
          $match: {
            $expr: { $eq: ['$_id', '$$brand'] }
          }
        },
        {
          $project: {
            _id: 1,
            name: 1,
            slug: 1
          }
        }
      ],
      as: 'brand'
    });

    const filterBrands = brands.map((_id) => utils.toObjectId(_id));
    const filterTags = tags.map((_id) => utils.toObjectId(_id));
    const priceRanges = pricesRange ? pricesRange.match(/\d+/g).map(Number) : '';

    aggregate.facet({
      tags: [
        { $unwind: { path: '$brand' } },
        { $unwind: { path: '$tags' } },
        {
          $match: {
            $expr: {
              $and: [
                filterBrands.length ? { $in: ['$brand._id', filterBrands] } : true
              ]
            }
          }
        },
        { $group: { _id: '$tags', tag: { $first: '$tags' }, total: { $sum: 1 } } },
        {
          $group: {
            _id: '$tag._id',
            name: { $addToSet: '$tag.name' },
            total: { $addToSet: '$total' }
          }
        },
        {
          $project: {
            name: { $arrayElemAt: ['$name', 0] },
            total: { $arrayElemAt: ['$total', 0] },
            _id: 1
          }
        },
        { $sort: { name: 1 } }
      ],
      brands: [
        { $unwind: { path: '$brand' } },
        { $unwind: { path: '$tags' } },
        {
          $match: {
            $expr: {
              $and: [
                filterTags.length ? { $in: ['$tags._id', filterTags] } : true
              ]
            }
          }
        },
        {
          $group: {
            _id: '$brand._id',
            name: { $first: '$brand.name' },
            slug: { $first: '$brand.slug' },
            total: {
              $sum: 1
            }
          }
        },
        { $sort: { name: 1 } }
      ],
      products: [
        { $unwind: { path: '$brand', preserveNullAndEmptyArrays: true } },
        { $unwind: { path: '$tags', preserveNullAndEmptyArrays: true } },
        {
          $match: {
            $expr: {
              $and: [
                filterBrands.length ? { $in: ['$brand._id', filterBrands] } : true,
                filterTags.length ? { $in: ['$tags._id', filterTags] } : true,
                pricesRange.length
                  ? {
                      $and: [
                        {
                          $gte: [
                            {
                              $cond: {
                                if: { $ne: ['$onSale.value', true] },
                                then: '$price',
                                else: '$sale.salePrice'
                              }
                            },
                            priceRanges[0]
                          ]
                        },
                        {
                          $lte: [
                            {
                              $cond: {
                                if: { $ne: ['$onSale.value', true] },
                                then: '$price',
                                else: '$sale.salePrice'
                              }
                            },
                            priceRanges[1]
                          ]
                        }
                      ]
                    }
                  : true
              ]
            }
          }
        },
        { $skip: (skip - 1) * pageSize },
        { $limit: pageSize },
        {
          $project: {
            _id: 1,
            brand: 1,
            description: 1,
            images: 1,
            onSale: 1,
            price: 1,
            quantity: 1,
            sale: 1,
            shipping: 1,
            sku: 1,
            skuThreshold: 1,
            slug: 1,
            status: 1,
            stock: 1,
            tags: 1,
            title: 1
          }
        },
        { $sort: { [order]: sort } }
      ],
      pricesRange: [
        { $unwind: { path: '$brand', preserveNullAndEmptyArrays: true } },
        { $unwind: { path: '$tags', preserveNullAndEmptyArrays: true } },
        {
          $match: {
            $expr: {
              $and: [
                filterBrands.length ? { $in: ['$brand._id', filterBrands] } : true,
                filterTags.length ? { $in: ['$tags._id', filterTags] } : true
              ]
            }
          }
        },
        {
          $project: {
            price: 1,
            onSale: 1,
            sale: 1,
            range: {
              $cond: [
                {
                  $and: [
                    {
                      $gte: [
                        {
                          $cond: {
                            if: { $ne: ['$onSale.value', true] },
                            then: '$price',
                            else: '$sale.salePrice'
                          }
                        },
                        0
                      ]
                    },
                    {
                      $lte: [
                        {
                          $cond: {
                            if: { $ne: ['$onSale.value', true] },
                            then: '$price',
                            else: '$sale.salePrice'
                          }
                        },
                        20
                      ]
                    }
                  ]
                },
                '0-20',
                {
                  $cond: [
                    {
                      $and: [
                        {
                          $gte: [
                            {
                              $cond: {
                                if: { $ne: ['$onSale.value', true] },
                                then: '$price',
                                else: '$sale.salePrice'
                              }
                            },
                            20
                          ]
                        },
                        {
                          $lte: [
                            {
                              $cond: {
                                if: { $ne: ['$onSale.value', true] },
                                then: '$price',
                                else: '$sale.salePrice'
                              }
                            },
                            50
                          ]
                        }
                      ]
                    },
                    '20-50',
                    '50+'
                  ]
                }
              ]
            }
          }
        },
        {
          $group: {
            _id: '$range',
            count: { $sum: 1 }
          }
        },
        {
          $project: {
            _id: 0,
            range: '$_id',
            count: 1
          }
        },
        { $unwind: { path: '$range', preserveNullAndEmptyArrays: true } },
        {
          $sort: {
            range: 1
          }
        }
      ],
      total: [
        { $unwind: { path: '$brand', preserveNullAndEmptyArrays: true } },
        { $unwind: { path: '$tags', preserveNullAndEmptyArrays: true } },
        {
          $match: {
            $expr: {
              $and: [
                filterBrands.length ? { $in: ['$brand._id', filterBrands] } : true,
                filterTags.length ? { $in: ['$tags._id', filterTags] } : true,
                pricesRange.length
                  ? {
                      $and: [
                        {
                          $gte: [
                            {
                              $cond: {
                                if: { $ne: ['$onSale.value', true] },
                                then: '$price',
                                else: '$sale.salePrice'
                              }
                            },
                            priceRanges[0]
                          ]
                        },
                        {
                          $lte: [
                            {
                              $cond: {
                                if: { $ne: ['$onSale.value', true] },
                                then: '$price',
                                else: '$sale.salePrice'
                              }
                            },
                            priceRanges[1]
                          ]
                        }
                      ]
                    }
                  : true
              ]
            }
          }
        },
        {
          $group: {
            _id: null,
            count: { $sum: 1 }
          }
        },
        {
          $project: {
            count: 1,
            _id: 0
          }
        }
      ]
    });

    aggregate.addFields({
      total: {
        $arrayElemAt: ['$total', 0]
      }
    });

    const [response] = await aggregate.exec();
    if (!response.total) {
      response.total = 0;
    }

    res.status(httpStatus.OK);
    return res.json(response);
  } catch (error) {
    console.log(error);
    return next(error);
  }
};

Related