MongoDB 自定义排序顺序,用于带分页的查询

Stu*_*Man 5 mongoose mongodb aggregation-framework

我在具有此架构的 MongoDB 集合中有一些文档:

{
    "_id": {
        "$oid": "60c1e8e318afd80016ce58b1"
    },
    "searchPriority": 1,
    "isLive": false,
    "vehicleCondition": "USED",
    "vehicleDetails": {
        "city": "Delhi"
    }
},
{
    "_id": {
        "$oid": "60c1f2f418afd80016ce58b5"
    },
    "searchPriority": 2,
    "isLive": false,
    "vehicleCondition": "USED",
    "vehicleDetails": {
        "city": "Delhi"
    }
},
{
    "_id": {
        "$oid": "60cb429eadd33c00139d2be7"
    },
    "searchPriority": 1,
    "isLive": false,
    "vehicleCondition": "USED",
    "vehicleDetails": {
        "city": "Gurugram"
    }
},
{
    "_id": {
        "$oid": "60c21be618afd80016ce5905"
    },
    "searchPriority": 2,
    "isLive": false,
    "vehicleCondition": "USED",
    "vehicleDetails": {
        "city": "New Delhi"
    }
},
{
    "_id": {
        "$oid": "60e306d29e452d00134b978f"
    },
    "searchPriority": 3,
    "isLive": false,
    "vehicleCondition": "USED",
    "vehicleDetails": {
        "city": "New Delhi"
    }
}
Run Code Online (Sandbox Code Playgroud)

vehicleCondition可以是NEWor USEDisLive可以是trueor ,false并且searchPriority是 1 到 3 之间的一个整数。(数字越小意味着它在搜索结果中应该越高)

在这里,除了_id其他字段都不是唯一的。我在isLive,vehicleDetails.city和上创建了一个复合索引 searchPriority

在我的应用程序中,我将执行一些这种形式的查询:

  • 查找所有汽车,其中isLiveis truevehicleDetails.cityisDelhiNew DelhiorGurugramvehicleConditionis USED(or NEW)。

为此,我可以执行这样的查找查询:

db.collection.find({"isLive": true, "vehicleDetails.city": { $in: [ "Gurugram", "Delhi", "New Delhi" ] }, "vehicleCondition": "USED" }, {})
Run Code Online (Sandbox Code Playgroud)

我希望此查询的结果按以下顺序排序:

  • 属于$in查找查询中 arrray 中第一个城市的所有汽车,具有最低优先级
  • $in在查找查询中属于第一个城市的所有汽车,具有第二低的优先级
  • 属于$in查找查询中 arrray 中第一个城市的所有汽车,具有第三低的优先级
  • $in在查找查询中属于第二个城市的所有汽车,具有最低优先级
  • $in在查找查询中属于第二个城市的所有汽车,具有第二低的优先级
  • $in在查找查询中属于第二个城市的所有汽车,具有第三低的优先级 在查找查询中属于第三个城市的所有汽车$in,具有最低的优先级
  • $in在查找查询中属于第三个城市的所有汽车,具有第二低的优先级
  • $in在查找查询中属于第三个城市的所有汽车,具有第三低的优先级

我怎样才能做到这一点?由于此查询返回的文档数量可能非常大,因此我将使用分页来限制返回文档的数量。这个额外的要求对这个问题的可能解决方案有什么影响吗?

Tom*_*ert 1

因此,我已阅读其他答案(提供了技术解决方案),但是根据您的评论和请求,它不合适。

所以首先aggregate在这里使用虽然在技术上解决了问题,但还是存在一些问题。

正如您提到的,查询可以有大量的文档匹配,与该find方法不同,聚合管道确实会将所有文档加载到内存中,这将创造性地导致性能问题,我还看到您提到了有关没有索引的内容。这将导致对每个 API 调用进行“集合”扫描。

我建议你做的是:

  1. 首先,您绝对必须建立一个复合索引,isLive, vehicleCondition, "vehicleDetails.city"以防万一您没有复合索引。这对于大规模使用来说是必须的。

  2. 现在我们已经解决了这个问题,我建议您将调用分成几个部分,我将粘贴一些 puesdo 代码,这些代码可能看起来有点到处都是,但我确实相信这是您可以实现的最佳方法使用 Mongo 因为每个查询都应该通过使用先前构建的索引来提高效率。

我将简要解释该方法,我们希望能够独立于其他城市查询每个城市,这样我们就可以使用“自定义排序”功能,而无需将所有匹配项加载到内存中。

为此,我们需要知道每个城市需要“跳过”和“限制”多少,例如城市#2(德里)限制将是(限制 - 城市#1(古鲁格拉姆)匹配)。

这是伪代码,我故意让它简单,这样就可以理解了。不过,我会在最后添加一些想法来进行一些基本的改进。

let limit = 10; // determined by req?
const skip = 0; // determined by req?
const cities = ['Gurugram', 'Delhi', 'New Delhi'];

// we need this to resolve the proper skip / limit. the last city is not relevant.
const countPromises = [];
for (let i = 0; i < cities.length - 1; i++) {
    countPromises.push(db.collection.countDocuments({
        'isLive': true,
        'vehicleDetails.city': cities[i],
        'vehicleCondition': 'USED',
    }));
}
await Promise.all(countPromises);

// first city initial skip
const citySkips = [skip];

for (let i = 0; i < countPromises.length - 1; i++) {
    // if we have x results in the first city then we need to skip-x skipping for the next city.
    citySkips.push(Math.max(skip - countPromises[0], 0));
}

let finalResults = [];
for (let i = 0; i < cities.length; i++) {
    // assuming we skip over ALL city i results.
    if (citySkips[i] >= countPromises[i]) {
        continue;
    }
    const cityLimit = limit - finalResults.length;
    if (cityLimit <= 0) {
        break;
    }
    const cityResults = await db.collection.find({
        'isLive': true,
        'vehicleDetails.city': cities[i],
        'vehicleCondition': 'USED',
    }).sort({ sortPriority: 1 }).skip(citySkips[i]).limit(cityLimit);
    finalResults = finalResults.concat(cityResults);
}
Run Code Online (Sandbox Code Playgroud)

好的,您可以进行可能的改进:

  • 如果数据库更新得不太频繁/您不关心极端准确性,您可以提前预先计算每个城市的匹配计数(每天一次?每周一次?取决于您的应用程序)。这将加快countDocuments确定每个城市skip所需的部分。limit
  • 最后一个for循环可以类似于Promise.all计数以加速结果。同样,如果城市数量永远不会太多,这可能是一个很好的解决方案。
  • 最后,尚不清楚一辆车是否可以与多个城市相关,但如果是这种情况,那么您需要在已匹配的文档上添加排除条件。