firebase 如何构建一个feed和following系统?

4c8rllxm  于 2022-11-17  发布在  其他
关注(0)|答案(8)|浏览(140)

我正在使用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?

knpiaxh1

knpiaxh11#

我稍后会看到你的问题,但我也会尽力为你提供我能想到的最好的数据库结构。所以希望你会发现这个答案有用。
我考虑的模式有三个顶级集合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作为一个集合而不是作为一个简单的对象/Map来添加的原因。请记住,根据关于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对象添加其他属性,比如赞数、评论数、分享数等等。
如果有人有大量的职位比每个新的追随者收到所有这些职位在他的时间线。
不可能。没有必要做这样的事情。我已经在上面解释了为什么。

编辑日期:2019年5月20日

另一种优化操作的解决方案是将用户应该看到的帖子存储在该用户的文档中,在该操作中,用户应该看到他所关注的每个人的所有最近帖子。
因此,如果我们举一个例子,比如说facebook,你需要有一个包含每个用户的facebook提要的文档,但是,如果一个文档可以容纳太多的数据(1 Mib),你需要把这些数据放在一个集合中,如上所述。

9udxz4iz

9udxz4iz2#

有两种情况
1.您应用中的用户拥有少量关注者。
1.您的应用中的用户拥有大量的关注者。如果我们要在firestore的单个文档中的单个数组中存储所有关注者,那么它将达到firestore的限制,即每个文档1 MiB。
1.在第一种情况下,每个用户必须在一个数组中的一个文档中保存关注者列表。通过使用arrayUnion()arrayRemove()可以有效地管理关注者列表。当你要在时间线上发布内容时,你必须在post文档中添加关注者列表。
并使用下面给出的查询来获取帖子

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

1.在第二种情况下,你只需要根据追随者数组的大小或计数来分解用户追随的文档。当数组的大小达到固定大小后,下一个追随者的id必须添加到下一个文档中。并且第一个文档必须保持字段“hasNext”。其中存储了一个布尔值。当添加一个新的帖子时,你必须复制帖子文档,并且每个文档都由之前断开的追随者列表组成。我们可以进行上面给出的相同查询来获取文档。

44u64gxh

44u64gxh3#

如果您的网络上有大量的活动 (例如,人们关注1,000人,或人们发布1,000个帖子),则其他答案将非常昂贵
我的解决方案是给每个用户文档添加一个名为“recentPosts”的字段,该字段将是一个数组。
现在,无论何时发布帖子,都有一个云函数来检测onWrite(),并更新发布者userDocument上的recentPosts数组,以添加关于该帖子的信息。
因此,您可以将以下Map添加到recentPosts数组的前面:

{
"postId": xxxxxxxxxxx,
"createdAt": tttttt
}

将recentPosts数组限制为1,000个对象,超过限制时删除最旧的条目。
现在,假设你关注了1,000个用户,并希望填充你的提要......获取所有1,000个用户文档。这将计为1 k次阅读。
一旦你有了1,000个文档,每个文档将有一个recentPosts的数组。
现在,您可能有多达100万个帖子的docID,全部按时间顺序排序,仅用于1,000次阅读。现在,当您的用户滚动他们的feed时,只需根据需要按其docID查询这些文档,大概一次10个或更多。
你现在可以从Y个追随者中加载X个帖子的提要,进行Y + X次阅读。
所以100个关注者的2,000个帖子只会有2,100个阅读。
所以1,000个关注者的1,000个帖子只会有2,000个阅读。

  • 等等... *

编辑1)进一步优化:加载userDocuments时,可以使用in查询一次批处理10个userDocuments...通常情况下,这不会有什么区别,因为即使是批处理,仍然是10次读取...但是,您也可以通过像recentPostsLastUpdatedAt这样的字段进行过滤,并检查它是否大于该用户doc的缓存值,那么任何没有更新recentPosts数组的用户文档都不会被读取。
编辑2)您也可以将监听器附加到每个userDocument,以便在其recentPosts发生变化时获取新帖子,而无需在每次需要刷新提要时查询每个追随者。(虽然1,000多个快照监听器可能是一种糟糕的做法,但我不知道它们在幕后是如何工作的)(编辑3:Firebase将一个项目限制为只有1 k个监听器,因此edit 2不是可扩展的优化)

lztngnrs

lztngnrs4#

我一直在努力与她建议的解决方案位,主要是由于技术差距,所以我想出了另一个解决方案,为我工作。
对于每个用户,我都有一个文档,其中包含他们关注的所有帐户,以及关注该用户的所有帐户的列表。
当应用程序启动时,我会获得关注当前用户的帐户列表,当用户发布帖子时,post对象的一部分是关注他们的所有用户的数组。
当用户B也想得到他们正在关注的人的所有帖子时,我只需要在查询中添加一个简单的whereArrayContains("followers", currentUser.uid)
我喜欢这种方法,因为它仍然允许我按任何其他参数对结果进行排序。
依据:

  • 每个文档1 mb,我在谷歌上搜索了一下,似乎有1,048,576个字符。
  • 事实上,Firestore生成的UID似乎大约有28个字符长。
  • 对象中的其余信息不会占用太大的空间。

这种方法应该适用于拥有多达约37,000个追随者的用户。

oyjwcjzk

oyjwcjzk5#

我浏览了Firebase的一些文档,我很困惑为什么www.example.com上建议的实现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的事情。
除非我遗漏了什么,否则主要的问题似乎是时间线节点本身的存在。我知道,它使生成特定用户的时间线视图变得更容易,但这是以必须维护所有这些关系为代价的,并且会大大延迟您的项目。使用查询从类似于上面的结构动态构建时间线是否效率太低?基于提交的用户?

pqwbnv8z

pqwbnv8z6#

更新日期:2021年8月28日

我创建了一个理论上可扩展的解决方案。请参阅here
和一些其他选项here
我的可扩展的想法是用户可能有100万以上的关注者,但一个真实的的用户不会关注超过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)读取摘要

提要将是聚合的帖子。
loadFeed()-在populateFeed()之后呼叫这个

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

feed中的文档实际上只需要createdAtdate和postId,因为您可以在前端提取帖子,尽管如果您不希望帖子发生更改,也可以存储所有数据:

postId: {
  createdAt: date
}

您的userDoc还将具有:

{
  numFollowing: number,
  lastUpdate: date
}

应用应该在加载时自动调用loadFeed()。可以有一个按钮来运行populateFeed()作为一个可调用的云函数(最好),或者在本地运行。如果你的提要是一个firebase可观察的,它会在它们填充时自动更新...
只是一个想法...我想可能有一些其他更干净的方法来解决这个问题的规模...
J型

更新

我想得越来越多,我真的认为更新一个帖子onWrite上的字段到所有关注者提要是可能的。唯一的限制是时间,通常是60秒,最多可以是9分钟。真的,你只需要确保你异步批量更新。参见我的adv-firestore-functionshere

9fkzdhlc

9fkzdhlc7#

我认为一种可能性是创建另一个名为"users_following"的顶级集合,其中包含一个名为"user_id"的文档和一个数组字段,该数组包含用户正在关注的所有用户。在该"users_following"文档中,可以有该特定用户的所有帖子的子集合,或者顶级集合也可以完成这项工作。下一个重要的事情是,你必须把最近的一篇文章以数组或Map的形式存储在"users-following"文档中。基本上,这些规范化的数据将被用来填充关注你的人的提要。但它的缺点是,你将只看到一个职位,每个人,即使该人已添加了两个职位最近或即使您存储您的两到三个职位在规范的方式比您的所有三个职位将显示一次(就像同一个用户连续发布三个帖子一样)。但是如果你只需要显示每个用户的一个帖子,这仍然是很好的。

xggvc2p6

xggvc2p68#

好吧,经过一些思考这个问题,我想出了一个理论上的解决方案(因为我还没有测试它).我将使用云Firestore为这个:
我的解决方案由两部分组成:

1.数据库Shema设计:

Firestore-root
     |
      _ _ users (collection):
               |
                _ _ uid (document):
                       |
                        _ _ name: 'Jack'
                       |
                        _ _ posts (sub-collection):
                                 |
                                  _ _ postId (document)
                       |
                        _ _ feed (sub-collection):
                                |
                                 _ _ postId (document)
                       |
                        _ _ following (sub-collection):
                                     |
                                      _ _ userId (document)
                       |
                        _ _ followers (sub-collection):
                                     |
                                      _ _ userId (document)

1.1说明:

正如您在这里看到的,我创建了一个名为users的集合,代表数据库中的每个用户。users集合中的每个uid文档都有自己的字段(例如name)和自己的子集合。每个uid文档都在posts子集合中包含自己创建的帖子。它包含当前用户在feed子集合中关注的人的帖子。最后,它包含表示followingfollowers的两个子集合。

2.使用云函数:

const functions = require("firebase-functions");

const firebaseAuth = require("firebase/auth");

const admin = require("firebase-admin");

admin.initializeApp();

const firestore = admin.firestore();

const uid = firebaseAuth.getAuth().currentUser.uid;

exports.addToUserFeed = 
  
  functions.firestore.document("/users/{uid}/posts/{postId}").onCreate(async 
  (snapshot,context) => {

    const userId = context.params.uid;

    const followers = await firestore.collection('users').doc(userId).collection('followers').where("userId", "==", uid).get();

    const isFollowing = !followers.empty;

    if (isFollowing == true) {

        const docRef = 
        firestore.collection('users').doc(uid).collection('feed').doc();

        const data = snapshot.data();

        firestore.runTransaction(transaction => {
           transaction.create(docRef, data);
       });
    }
});

2.1说明:

这里,每当用户在posts子集合中创建帖子时,我们都会触发一个云函数。(feed子集合),从它所跟随的用户,我们首先检查当前用户是否(我们使用firebase authuid常量的形式获得其id)跟随创建的帖子作者,其id存储在通配符uid中(我们可以通过context.params.uid访问它)。检查是通过执行Query来完成的,检查followers子集合中的任何userId文档是否与当前用户id uid匹配。这将返回一个QuerySnapshot。然后我们检查QuerySnapshot是否为空。如果为空,则意味着当前用户没有跟随context.params.uid用户。否则,它会跟随它。如果它跟随,则我们使用事务将新创建的帖子添加到当前用户feed的子集合中。
好了,就是这样。我希望这对大家有帮助。我还没有测试它,所以也许有些东西不能工作,但希望它会。谢谢!

相关问题