Фасетные фильтры Mongo / mongoose $ возвращают все бренды / теги продукта в ответ, если клиент применял фильтры - PullRequest
3 голосов
/ 18 октября 2019

У меня есть эта конечная точка, это начальная конечная точка, когда клиент посещает интернет-магазин:

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);
  }
};

Если фильтры не применяются, все продукты соответствуют запрошенной категории без проблем.

Моя проблема заключается в том, что когда покупатель выбирает марку или тег, фасет возвращает товары, но возвращает только одну марку / метку (как и должно быть, поскольку фильтруемые товары имеют только эту марку).

Что я долженсделать, чтобы сохранить все бренды / теги и позволить пользователю выбрать более одного бренда / тега? Если клиент выбирает марку, то теги должны соответствовать возвращаемым тегам продуктов и наоборот.

Есть ли лучший способ реализовать этап tags в $facet, поскольку теги - это массив, а желаемый результат -: [{_id: 123, name: {label: 'test', value: 123]}]

Запрос выглядит так: (1,2,3,4 представляет _id)

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

Обновление

Это products схема с tags и brands:

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

tags схема:

{
    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 схема:

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

Сценарий:

Пользователь посещает магазин, выбирает категорию, и все подходящие товары должны возвращаться с совпадающими brands, tags, priceRange и разбиение на страницы.

Случай 1:

Пользователь нажимает brand из флажка, затем запрос возвращает совпадение products, tags & priceRanges и все бренды выбранной категории, не соответствующие продуктам

Случай 2:

Пользователь выбираетbrand как в случае 1, но затем решает также проверить tag, тогда запрос должен вернуть все brands и tags снова, но products сопоставляется с ними.

Случай 3:

Пользователь не выбирает brand, но выбирает только tag, запрос должен вернуть все соответствующие products, которые имеют этот тег / теги, и вернуть brands, который соответствует возвращенным продуктам.

Случай 4:

То же, что и в случае 3, но пользователь выбирает brand после выбора tag/tags, запрос должен возвращать совпадения products, brands & tags.

Во всех случаях нумерация страниц должна возвращать правильное итоговое значение, также priceRanges должно соответствовать возвращаемым результатам.

Надеюсь, теперь все ясно, я думаю, что я не пропустил ни одного другого случая. Я мог бы, вероятно, выделить / отключить теги / бренды, которые не соответствуют ответу в интерфейсе, но я не знаю, насколько это удобно для пользователя.

1 Ответ

0 голосов
/ 31 октября 2019

Вот что я закончил:

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);
  }
};

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...