Firestore技术咨询:如何查询用户未浏览过的随机动物文档
哈哈,这个问题我之前帮好几个开发者捋过思路,Firestore的查询限制有时候确实会让人卡在这里——尤其是要处理“排除已浏览内容”这类需求的时候。你担心的「给每个用户存储所有动物ID字典」的扩展性问题完全正确,新增动物时要遍历更新所有用户的文档,这种方案在用户或动物数量上来之后绝对会崩,所以咱们直接pass,看看几个更靠谱的替代方案:
方案一:给动物文档维护已浏览用户集合
实现思路
在每个Animals文档里添加一个viewedBy字段(可以是数组,或者更适合大数据量的子集合Viewers),用来记录所有浏览过该动物的用户ID。当用户浏览某只动物时,把用户ID添加到对应的viewedBy数组/子集合中。
要获取用户未浏览的随机动物时:
- 先统计
Animals集合的总文档数,生成一个0到总数量-1之间的随机数 - 用
limit(1)和offset(随机数)取出对应的动物文档 - 检查该动物的
viewedBy是否包含当前用户ID:- 如果不包含,直接返回这个动物
- 如果包含,重复步骤1-3,直到找到未浏览的动物
如果用子集合Viewers的话,可以提前用聚合查询判断该动物是否被当前用户浏览过,避免拉取整个文档的开销。
优缺点
- ✅ 新增动物时无需更新任何用户数据,扩展性拉满
- ✅ 实现简单,不需要复杂的索引
- ❌ 如果用户浏览过的动物占比很高(比如用户已经看了80%的动物),重试次数会变多,影响性能
- ❌ 当
viewedBy数组超过Firestore文档的1MB限制时(比如某只动物被几十万用户浏览),需要切换到子集合存储
适用场景
适合用户浏览率较低的场景(比如大部分动物用户都没看过),或者动物的热门程度不高,不会出现单个动物被海量用户浏览的情况。
方案二:用户端维护已浏览动物ID列表
实现思路
在Users文档里添加一个viewedAnimalIds数组,只存储用户已经浏览过的动物ID(而不是所有动物的字典)。当用户浏览某只动物时,把该动物的ID追加到这个数组里。
要获取未浏览的随机动物时:
- 先获取当前用户的
viewedAnimalIds数组 - 如果数组长度≤10,直接使用
whereNotIn("id", viewedAnimalIds)查询Animals集合,再结合随机排序取结果 - 如果数组长度>10,就用
limit(20)随机取一批动物,在客户端过滤掉已浏览的,选第一个未浏览的即可
⚠️ 注意:Firestore的whereNotIn最多支持10个参数值,超过数量会直接报错,所以需要客户端兜底过滤。
优缺点
- ✅ 用户文档的数据量极小(只存已浏览的ID),读写开销低
- ✅ 新增动物时完全不需要操作用户数据
- ❌ 当用户浏览过的动物数量较多时,需要客户端做过滤操作
- ❌ 如果动物数量极大,随机取的一批里可能全是用户已浏览的,需要重试
适用场景
适合用户浏览过的动物数量不多的场景,或者可以接受客户端少量过滤的业务需求。
方案三:结合随机种子字段的分页查询
实现思路
给每个Animals文档添加一个randomSeed字段,生成一个0到1之间的随机浮点数(可以在创建动物时自动生成)。
要获取用户未浏览的随机动物时:
- 给当前用户生成一个随机数
userRandom(比如0.5) - 查询
Animals集合,条件为randomSeed > userRandom且viewedBy不包含当前用户ID,然后limit(1)取第一个结果 - 如果没有结果,就查询
randomSeed < userRandom的文档,同样过滤掉已浏览的 - 还是没结果的话,说明用户已经浏览了所有动物
优缺点
- ✅ 可以快速定位到随机范围的动物,避免全量遍历
- ✅ 结合方案一的
viewedBy字段,不需要客户端大量过滤 - ❌ 需要给
randomSeed和viewedBy创建复合索引,否则查询会报错 - ❌ 当用户浏览过某一范围内的所有动物时,可能需要切换范围查询
适用场景
适合动物数量较大,且需要快速获取随机结果的场景,配合复合索引可以获得不错的性能。
总结
如果你的用户浏览率低,优先选方案一;如果用户浏览过的动物不多,方案二最省心;如果动物数量极大且需要高性能随机查询,方案三是更好的选择。绝对不要用给每个用户存所有动物ID字典的方案,扩展性问题真的会让你后期头疼到爆炸。
内容的提问来源于stack exchange,提问作者eforkin

