Nodejs和Mongo的分页随机排序

4
我正在运行一个iOS应用程序,其中我显示了当前在线用户列表。
我有一个API端点,返回10个(或N个)随机用户,以便您可以不断滚动并始终看到新用户。因此,我希望确保我不会返回之前已经返回过的用户。
由于用户必须以随机方式返回,因此我无法使用游标或常规分页。
我尝试了两种方法,但我相信有更好的方法:
1. 首先,我将已经查看过的用户的ID作为请求参数发送。
例如:但是,如果用户不断滚动并浏览了200个配置文件,则列表会很长,看起来不干净。
2. 然后,在数据库中,我尝试为每个用户添加一个“online_profiles_already_sent”字段,其中我将存储已发送给用户的ID数组(我正在使用MongoDB)。
我无法找出更好/更干净的方法。
编辑: 我找到了一种使用MySQL的方法,使用RAND(seed),但我无法弄清楚是否有一种方法可以使用Mongo执行相同的操作。 PHP MySQL pagination with random ordering 谢谢 :)

我认为更好的做法是在应用程序内部保留已请求用户的本地数据 - 如果您正在为每个客户端或会话/cookie数据显示不同的用户。或者,如果您为所有客户端返回一个列表 - 那么只需将列表保留在服务器部分 - 您可以创建临时文件或共享列表等。 - ByteMaster
嗨 @ByteMaster,感谢你的答案,但我的应用程序一次有几千个用户,我不认为将其保留在内存中是最好的选择。这就是为什么我尝试将其保存在数据库中,但我确信必须有更好的解决方案。 我发现使用MySQL和RAND以及种子可以解决问题,但我无法弄清楚如何在Mongo中实现。 - David
2个回答

1
我认为确保用户每次只看到独特的用户的唯一方法是存储已经被看过的用户列表。即使在你链接的RAND示例中,也有可能与先前的用户列表相交,因为RAND不一定会排除以前返回的用户。

随机抽样

如果你想采用随机抽样,可以考虑使用从MongoDB中随机选择记录,该方法建议使用聚合$sample操作符。具体实现如下:

const {
    MongoClient
} = require("mongodb");

const
    DB_NAME = "weather",
    COLLECTION_NAME = "readings",
    MONGO_DOMAIN = "localhost",
    MONGO_PORT = "32768",
    MONGO_URL = `mongodb://${MONGO_DOMAIN}:${MONGO_PORT}`;

(async function () {
    const client = await MongoClient.connect(MONGO_URL),
        db = await client.db(DB_NAME),
        collection = await db.collection(COLLECTION_NAME);

    const randomDocs = await collection
        .aggregate([{
            $sample: {
                size: 5
            }
        }])
        .map(doc => {
            return {
                id: doc._id,
                temperature: doc.main.temp
            }
        });

    randomDocs.forEach(doc => console.log(`ID: ${doc.id} | Temperature: ${doc.temperature}`));
    client.close();
}());

以前用户的缓存

如果您选择维护先前查看过的用户列表,可以编写一个使用$nin过滤器实现的方案,并存储以前查看过的用户的_id

以下是一个示例,使用我拥有的天气数据库,每次返回5个条目,直到所有条目都已打印:

const {
    MongoClient
} = require("mongodb");

const
    DB_NAME = "weather",
    COLLECTION_NAME = "readings",
    MONGO_DOMAIN = "localhost",
    MONGO_PORT = "32768",
    MONGO_URL = `mongodb://${MONGO_DOMAIN}:${MONGO_PORT}`;

(async function () {
    const client = await MongoClient.connect(MONGO_URL),
        db = await client.db(DB_NAME),
        collection = await db.collection(COLLECTION_NAME);

    let previousEntries = [], // Track ids of things we have seen
        empty = false;

    while (!empty) {
        const findFilter = {};
        if (previousEntries.length) {
            findFilter._id = {
                $nin: previousEntries
            }
        }

        // Get items 5 at a time
        const docs = await collection
            .find(findFilter, {
                limit: 5,
                projection: {
                    main: 1
                }
            })
            .map(doc => {
                return {
                    id: doc._id,
                    temperature: doc.main.temp
                }
            })
            .toArray();

        // Keep track of already seen items
        previousEntries = previousEntries.concat(docs.map(doc => doc.id));

        // Are we still getting items?
        console.log(docs.length);
        empty = !docs.length;

        // Print out the docs
        docs.forEach(doc => console.log(`ID: ${doc.id} | Temperature: ${doc.temperature}`));
    }
    client.close();
}());

非常感谢您的回答。我想我会采用类似的方法。唯一的区别是我可能会将“previousEntries”存储在数据库中,用户模型上,所以我需要查询两次,因为我不想将其存储在内存中。 - David

1
我遇到了同样的问题,并可以提供一种替代方案。
TL;DR:在首次加载时获取所有集合的对象ID,使用NodeJS进行随机化,稍后再使用它。
缺点:如果有数百万条记录,则首次加载速度较慢。
优点:后续执行可能比其他解决方案更快。

让我们来详细解释一下 :)

为了更好地解释,我将做出以下假设

假设:

  1. 假设编程语言使用的是NodeJS
    • 该解决方案也适用于其他编程语言
  2. 假设您的集合中有4个对象
  3. 假设分页限制为2

步骤:

第一次执行:

  1. 获取所有对象ID

注意:我已经考虑到了性能问题,这个执行只需要几秒钟就可以处理大小为10,000的集合。如果您要解决百万记录问题,则可能首先使用某种形式的分区逻辑/使用其他列出的解决方案。

db.getCollection('my_collection').find({}, {_id:1}).map(function(item){ return item._id; });

或者

db.getCollection('my_collection').find({}, {_id:1}).map(function(item){ return item._id.valueOf(); });

结果:

ObjectId("FirstObjectID"),
ObjectId("SecondObjectID"),
ObjectId("ThirdObjectID"),
ObjectId("ForthObjectID"),
  1. 使用NodeJS随机检索数组

结果:

ObjectId("ThirdObjectID"),
ObjectId("SecondObjectID"),
ObjectId("ForthObjectID"),
ObjectId("FirstObjectID"),
  1. 存储这个随机数组:
  • 如果这是一个为每个用户随机分页的服务器端脚本,请考虑存储在Cookie / Session
    • 出于扩展性考虑,我建议使用Cookie(带有与浏览器关闭相关的超时过期)

每次检索:

  1. 检索存储的数组

  2. 获取分页项(例如前两个项目)

  3. 使用find $in查找这些项的对象

.

db.getCollection('my_collection')
    .find({"_id" : {"$in" : [ObjectId("ThirdObjectID"), ObjectId("SecondObjectID")]}});
  1. 使用NodeJS,根据检索到的分页项对检索到的对象进行排序

完成了!一个基于随机的MongoDB分页查询 :)


重新审视这个答案,将对象ID存储在cookie中可能不太安全,最好将它们存储在会话中。 - Ng Sek Long

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接