Firestore - 如何构建提要和关注系统

Posted

技术标签:

【中文标题】Firestore - 如何构建提要和关注系统【英文标题】:Firestore - how to structure a feed and follow system 【发布时间】:2018-04-09 07:33:40 【问题描述】:

我将 Firebase 实时数据库用于我的测试社交网络应用程序,您可以在其中关注并接收您关注的人的帖子。传统的社交网络。 我像这样构建了我的数据库 -

Users
--USER_ID_1
----name
----email
--USER_ID_2
----name
----email

Posts
--POST_ID_1
----image
----userid
----date
--POST_ID_2
----image
----userid
----date

Timeline
--User_ID_1
----POST_ID_2
------date
----POST_ID_1
------date

我还有另一个节点“内容”,它只包含所有用户帖子的 ID。因此,如果“A”跟在“B”之后,那么 B 的所有帖子 ID 都会添加到 A 的时间轴中。如果 B 发布了一些内容,那么它也会被添加到其所有追随者的时间线中。

现在这是我的实时数据库解决方案,但它显然存在一些可扩展性问题

如果某人有 10,000 名关注者,则新帖子会添加到所有 10,000 名关注者的时间轴中。 如果某人的帖子数量多于每个新关注者在其时间轴中收到的所有帖子。

这些是一些问题。

现在,我正在考虑将整个事情转移到 Firestore,因为它被称为“可扩展”。那么我应该如何构建我的数据库,以便在 Firestore 中消除我在实时数据库中遇到的问题。

【问题讨论】:

免责声明:我只准备通过 Firestore 文档。由于 firestore 的查询比 firebase-realtime-db 好得多,因此您不再需要复制数据。所以我要做的是:当用户查看他们的时间线时,创建一个 Firestore 查询,上面写着give me all posts which are from the people i follow。类似于:posts.where(user== john OR mark OR katy OR ...)。我希望这样的事情有效。如果我有时间尝试,我会告诉你。 @jurgenBrandstetter Firestore 目前不支持“或”,如果支持,您的方法也将不起作用。假设某人有 1000 个关注者,而我必须做出 1000 条 OR 语句。 我在想,如果您在文档中输入关注您的人的 ID,可能是这样。例如,UserA 跟随 UserB,然后在 UserB 发布文档中输入 UserAID = true。因此,当您进行查询时,它将类似于 == postDocRef.where(UserAID=true),但我不知道 firestore 中的文档是否可以支持多达 n 百万的关注者 @Zicsus 让我们假设这个例子。我发了 10 000 个帖子。现在你跟着我。您可以按时间戳对帖子进行排序,然后限制为一定数量,例如 15 个帖子并使用 .childAdded 方法。为了以相同的时间顺序加载更多数据,您可以使用以下类型的观察者创建一个方法:ObserveSingleEvent(ofType:Value),限制为 10 个帖子。然后在您的表格视图中实现拉动刷新功能或使用滚动视图偏移量,当您到达表格底部时,只需调用您的ObserveSingleEvent 方法并获取更多项目等等。 @bibscy 如果您只关注一个人,问题不是准备提要,而是如何准备像推特这样按时间顺序排列的提要,您可以在其中看到您关注的所有用户的活动。 【参考方案1】:

如果您的网络上有大量活动(例如关注 1,000 人的人,或发布 1,000 条帖子的人),其他答案将变得非常昂贵

我的解决方案是为每个用户文档添加一个名为“recentPosts”的字段,该字段将是一个数组。

现在,无论何时发布帖子,都有一个检测 onWrite() 的云函数,并在其 userDocument 上更新该发布者的 recentPosts 数组以添加有关该帖子的信息。

因此,您可以将以下地图添加到 recentPosts 数组的前面:


"postId": xxxxxxxxxxx,
"createdAt": tttttt

将 recentPosts 数组限制为 1,000 个对象,超过限制时删除最旧的条目。

现在,假设您正在关注 1,000 个用户并想要填充您的提要...获取所有 1,000 个用户文档。这将计为 1k 次读取。

拥有 1,000 个文档后,每个文档都会有一个 recentPosts 数组。将客户端上的所有这些数组合并到一个主数组中,并按 createdAt 排序。

现在您可能拥有多达 100 万个帖子的 docID,所有这些都按时间顺序排序,仅用于 1000 次阅读。现在,当您的用户滚动他们的提要时,只需根据需要通过他们的 docID 查询这些文档,大概一次 10 个或其他什么。

您现在可以加载来自 Y 个关注者的 X 个帖子供 X + Y 阅读。

因此,来自 100 个关注者的 2,000 条帖子只有 2,100 次阅读。 因此,来自 1,000 名关注者的 1,000 条帖子只有 2,000 次阅读。等等...


编辑 1) 进一步优化。加载 userDocuments 时,您可以使用 in query 一次将它们批处理 10 个...通常这没有什么区别,因为即使它已批处理,它仍然是 10 个读取...但是您也可以通过像 @987654327 这样的字段进行过滤@ 并检查它是否大于该用户文档的缓存值,那么任何尚未更新其 recentPosts 数组的用户文档都不会被读取。理论上,这可以为您节省 10 倍的碱基读取。

编辑 2) 您也可以将侦听器附加到每个 userDocument,以便在他们的 recentPosts 更改时获取新帖子,而无需在每次需要刷新提要时查询每个关注者。 (虽然 1,000 多个快照监听器可能是不好的做法,但我不知道它们是如何工作的)(Edit3:Firebase 将一个项目限制为只有 1k 个监听器,因此 edit2 不是可扩展的优化)

【讨论】:

这是迄今为止创建不同用户帖子的时间线最有效的方法。希望我能投票 10 次,以便人们知道这是一个很好的答案。 @Tadreik 谢谢!我们遇到了扩展问题,我想出了这个。我们已经在生产中使用它一年多了,没有任何问题。 我也喜欢添加recentPostsLastUpdatedAt,这对于降低用户查询搜索空间很有意义。【参考方案2】:

更新:21 年 8 月 28 日

我创建了一个理论上可扩展的解决方案。见here。

还有一些其他选项here。


我的可扩展想法是,用户可能有 1,000,000 多个关注者,但真正的用户关注的人不会超过 1000 人。我们可以简单地聚合他们的提要(帖子的集合)。这是我的理论:

收藏

/users
/users/userId/follows
/users/userId/feed
/posts

1。填充提要

填充提要需要首先运行,并且应该在云功能中。为避免费用,它只会将新帖子添加到您的提要中,而不是超过 10 天(或无论多么旧)的帖子。

populateFeed() - 类似这样的......

numFollowing = get('users/numFollowing');
lastUpdate = get('users/lastUpdate');
tenDaysOld = timestamp 10 days ago

// maybe chunk at 20 here...
for (numFollowing) 
  docs = db.collection('posts')
    .where('userId', '==', userId)
    .where('createdAt', '>', lastUpdate)
    .where('createdAt', '<', tenDaysOld);
  db.collection('users/$userId/feed').batch.set(docs);

users/$userId/lastUpdate 更新为当前时间戳...

这样,您不会获得太多文档(例如只有 10 天),也不会浪费阅读已有文档。

2) 阅读提要

Feed 将是聚合的帖子。

loadFeed() - 在populateFeed() 之后调用它

db.collection('/users/$userId/feed').orderBy('createdAt');

feed 中的文档只需要 createdAt 日期和 postId,因为您可以在前端拉出帖子,尽管您可以如果您不希望它发生变化,请存储所有数据:

postId: 
  createdAt: date

您的 userDoc 还将拥有:


  numFollowing: number,
  lastUpdate: date

应用应在加载时自动调用loadFeed()。可能有一个按钮运行populateFeed() 作为可调用的云功能(最好的),或者在本地运行。如果您的提要是可观察到的 firebase,它将在填充时自动更新...

只是一个想法...我认为可能有一些其他更清洁的方法来解决这个规模庞大的问题...

J

更新

我越想越觉得,我确实认为可以将帖子 onWrite 上的字段更新到所有关注者供稿。唯一的限制是时间,通常为 60 秒,最长可达 9min。实际上,您只需要确保异步批量更新即可。查看我的 adv-firestore-functions 包here:

【讨论】:

【参考方案3】:

我稍后才看到您的问题,但我也会尝试为您提供我能想到的最佳数据库结构。所以希望你会发现这个答案很有用。

我正在考虑一个架构,其中包含 usersusers that a user is followingposts 的三个***集合:

Firestore-root
   |
   --- users (collection)
   |     |
   |     --- uid (documents)
   |          |
   |          --- name: "User Name"
   |          |
   |          --- email: "email@email.com"
   |
   --- following (collection)
   |      |
   |      --- uid (document)
   |           |
   |           --- userFollowing (collection)
   |                 |
   |                 --- uid (documents)
   |                 |
   |                 --- uid (documents)
   |
   --- posts (collection)
         |
         --- uid (documents)
              |
              --- userPosts (collection)
                    |
                    --- postId (documents)
                    |     |
                    |     --- title: "Post Title"
                    |     |
                    |     --- date: September 03, 2018 at 6:16:58 PM UTC+3
                    |
                    --- postId (documents)
                          |
                          --- title: "Post Title"
                          |
                          --- date: September 03, 2018 at 6:16:58 PM UTC+3

如果某人有 10,000 名关注者,则将新帖子添加到所有 10,000 名关注者的时间轴中。

这完全没有问题,因为这就是集合在 Firestore 中的原因。根据modeling a Cloud Firestore database的官方文档:

Cloud Firestore 针对存储大量小文档进行了优化。

这就是我将userFollowing 添加为集合而不是可以容纳其他对象的简单对象/映射的原因。请记住,根据有关limits and quota 的官方文档,文档的最大大小是1 MiB (1,048,576 bytes)。在集合的情况下,集合下的文档数量没有限制。事实上,Firestore 已经针对这种结构进行了优化。

因此,以这种方式拥有这 10,000 名追随者,将非常有效。此外,您可以以无需在任何地方复制任何内容的方式查询数据库。

如您所见,数据库非常非规范化,您可以非常简单地查询它。让我们举个例子,但在创建与数据库的连接并使用以下代码行获取用户的uid 之前:

FirebaseFirestore rootRef = FirebaseFirestore.getInstance();
String uid = FirebaseAuth.getInstance().getCurrentUser().getUid();

如果您想查询数据库以获取用户关注的所有用户,您可以在以下参考中使用get() 调用:

CollectionReference userFollowingRef = rootRef.collection("following/" + uid + "/userFollowing");

因此,通过这种方式,您可以获得用户关注的所有用户对象。有了他们的 uid,您就可以轻松获取他们所有的帖子。

假设您想在时间轴上显示每位用户的最新三篇帖子。当使用非常大的数据集时,解决这个问题的关键是以更小的块加载数据。我在这个 post 的回答中解释了一种推荐的方式,您可以通过将查询游标与limit() 方法组合来对查询进行分页。我还建议您查看此 video 以获得更好的理解。因此,要获取每个用户的最新三个帖子,您应该考虑使用此解决方案。因此,首先您需要获取您关注的前 15 个用户对象,然后根据他们的uid,获取他们最近的三个帖子。要获取单个用户的最新三个帖子,请使用以下查询:

Query query = rootRef.collection("posts/" + uid + "/userPosts").orderBy("date", Query.Direction.DESCENDING)).limit(3);

当您向下滚动时,加载其他 15 个用户对象并获取他们最近的三个帖子等等。除了date,您还可以向post 对象添加其他属性,例如点赞数、cmets、分享等。

如果某人的帖子数量多于每个新关注者在其时间轴中收到的所有帖子。

没办法。没有必要做这样的事情。我已经在上面解释了原因。

2019 年 5 月 20 日编辑:

另一个优化用户应该看到他关注的每个人的所有最近帖子的操作的解决方案是将用户应该看到的帖子存储在该用户的文档中。

因此,如果我们举个例子,比如说 facebook,您需要有一个包含每个用户的 facebook 提要的文档。但是,如果单个文档可以容纳的数据过多 (1 Mib),您需要将这些数据放入一个集合中,如上所述。

【讨论】:

@BrendanMcGill 谢谢布伦丹。不,在这种特殊情况下,您应该添加一个新集合来保存所有用户的所有帖子。 Firstore 不允许跨多个集合进行查询。如果您认为我的回答有帮助,请考虑投票。我会很感激的。谢谢! @Socceroos “我提到过非规范化和复制数据是一个更好的解决方案。”正如我在回答中已经提到的那样。 “需要为您关注的每个用户提供一组网络请求”存在并且将会有一个网络请求。 将用户定向到您的其他半相关帖子不是答案。我也不喜欢您检索提要的方法。一个典型的提要不会只返回 15 个用户的帖子。相反,他们按时间顺序查看所有帖子。所以我认为这不是一个好的答案,即使它的长度/细节我认为它没有达到目标。 @Soorya 它总是会花费您执行的操作的确切数量。仅此而已。 @Soorya 你也去看看this【参考方案4】:

我认为一种可能性是创建另一个名为"users_following" 的***集合,其中包含一个名为"user_id" 的文档和一个包含用户关注的所有用户的数组的字段。在该"users_following" 文档中,可以拥有该特定用户的所有帖子的子集合或***集合也可以完成这项工作。下一个重要的事情是必须将最近的一篇帖子存储在"users-following" 文档中作为数组或映射。基本上,这些标准化数据将用于填充关注您的人的提要。但它的缺点是,即使该人最近添加了两个帖子,或者即使您以标准化方式存储两到三个帖子,您也只会看到每个人的一个帖子,而不是一次显示所有三个帖子(比如三个帖子同一用户连续)。但是,如果您只需要为每个用户显示一篇帖子,这仍然是一件好事。

【讨论】:

【参考方案5】:

我一直在为她建议的解决方案苦苦挣扎,主要是由于技术差距,所以我想出了另一个适合我的解决方案。

对于每个用户,我都有一个文档,其中包含他们关注的所有帐户,以及所有关注该用户的所有帐户的列表。

当应用启动时,我获得了关注当前用户的帐户列表,当用户发布帖子时,帖子对象的一部分是关注他们的所有用户的数组。

当用户 B 也想获得他们关注的人的所有帖子时,我只需在查询中添加一个简单的whereArrayContains("followers", currentUser.uid)

我喜欢这种方法,因为它仍然允许我按我想要的任何其他参数对结果进行排序。

基于:

每个文档 1mb,根据我所做的谷歌搜索,它似乎包含 1,048,576 个字符。 Firestore 生成的 UID 的长度似乎约为 28 个字符。 对象中的其余信息不会占用太多大小。

这种方法应该适用于拥有多达 37,000 名关注者的用户。

【讨论】:

我不推荐使用这种方法。该文档的侧面也有 20k 行的限制。这意味着您不能拥有超过 19999k 大小的数组,其中 1 行用于数组名称。这也意味着当达到限制时,您不能在文档中添加任何其他字段 @Sandeep 我认为有 20k 字段限制,而不是行。数组类型数据被视为一个字段。 @Pooja 请仔细检查,因为我很确定这是关于线条的。请分享您的发现【参考方案6】:

有两种情况

    您应用中的用户拥有少量关注者。

    您应用中的用户拥有大量关注者。如果我们要将整个追随者存储在 firestore 的单个文档中的单个数组中。然后它将达到每个文档 1 MiB 的 Firestore 限制。


    在第一种情况下,每个用户必须保存一个文档,该文档将关注者列表存储在单个数组中的单个文档中。通过使用arrayUnion()arrayRemove() 可以有效地管理关注者列表。当您要在时间线中发布内容时,您必须在发布文档中添加关注者列表。

    并使用下面给出的查询来获取帖子

    postCollectionRef.whereArrayContains("followers", userUid).orderBy("date");
    

    在第二种情况下,您只需要根据关注者数组的大小或数量来打破用户关注的文档。在达到固定大小的数组大小后,下一个跟随者的 id 必须添加到下一个文档中。第一个文档必须保留字段“hasNext”,该字段存储一个布尔值。 添加新帖子时,您必须复制帖子文档,并且每个文档都包含较早中断的关注者列表。 我们可以进行上面给出的相同查询来获取文档。

【讨论】:

@Niyas 使用此解决方案,如果用户 A 关注有 B_r 评论的用户 B,我们将为每个关注执行 B_r 写入正确吗? 您说的是you must add the list of followers in post document.,但该解决方案的一个大问题是:当用户获得新的关注者并拥有 10k 个帖子时,您需要更新这 10k 个帖子中的每一个,以在追随者数组。 1 次关注 10k 次写入。这听起来不像是一个好的架构。 我必须说这似乎是最好的答案,尽管它并不理想 我将您的回答作为我申请的基础,并且效果非常好。通过您对“hasNext”标志的建议,我绕过了数组限制,然后为每个包含以下用户数组的额外文档复制了帖子。 @Antoine 我会选择复制 10k 个帖子而不是为 100 万关注者复制帖子的边缘案例【参考方案7】:

我浏览了一些 Firebase 文档,但我很困惑为什么 https://firebase.google.com/docs/database/android/structure-data#fanout 建议的实现不适用于您的情况。像这样的:

users
--userid(somedude)
---name
---etc
---leaders: 
----someotherdude
----someotherotherdude

leaders:
--userid(someotherdude)
---datelastupdated
---followers
----somedude
----thatotherdude
---posts
----postid

posts
--postid
---date
---image
---contentid

postcontent
--contentid
---content

该指南继续提到“这是双向关系的必要冗余。它允许您快速有效地获取 Ada 的成员资格,即使用户或组的列表扩展到数百万。”,所以它没有似乎可扩展性并不是 Firestore 独有的东西。

除非我遗漏了什么,否则主要问题似乎是时间线节点本身的存在。我知道它可以更轻松地生成特定用户时间线的视图,但这是以必须维护所有这些关系为代价的,并且会大大延迟您的项目。根据提交的用户,使用查询从与上述类似的结构中动态构建时间线是否效率太低?

【讨论】:

以上是关于Firestore - 如何构建提要和关注系统的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 MERNG 实现社交媒体提要?

如何构建firestore数据库?

如何在 Swift 中使用 Firestore 为 2 人多人游戏构建数据?

Flutter:Google Maps 如何从 Firestore 设置图标

如何解决“由于上述问题,无法构建插件 cloud_firestore。”

实时创建新闻提要