好友关系必须用引用式建模,即单独建friendships集合并为user_id和friend_id分别建索引;查二度好友须用$graphLookup并设maxDepth;推荐需预计算+实时修正;地理推荐必须用2dsphere索引。

好友关系必须用引用式建模,别嵌入
MongoDB 里用户互相关注这种多对多关系,不能靠 friends 数组直接嵌在用户文档里存对方 _id——看似简单,但更新、查询、一致性全会出问题。真实场景中,一个用户可能有几万好友,光是文档大小就容易撞上 16MB 上限;更麻烦的是,每次加好友/取关,你得原子性地双向更新两个用户文档,而 MongoDB 单文档写是原子的,跨文档不是。
- 嵌入式写法(错误示范):
{ _id: "u1", name: "Alice", friends: ["u2", "u3"] }→ 无法高效查“谁关注了 u2”,也无法加索引加速反向遍历 - 正确做法是单独建
friendships集合,每条记录表示一次关注行为:{ _id: ObjectId, user_id: "u1", friend_id: "u2", created_at: ISODate(...) } - 必须为
user_id和friend_id分别建索引:db.friendships.createIndex({ user_id: 1 })和db.friendships.createIndex({ friend_id: 1 }),否则查“u2 的粉丝列表”会全表扫描
$graphLookup 是查二度/三度好友的唯一靠谱方案
想推荐“朋友的朋友”,就得从当前用户出发,递归展开关系图。MongoDB 没有原生图数据库的遍历能力,$graphLookup 就是为此设计的聚合阶段——但它极易写错或超时,尤其没配好深度限制和索引时。
- 必须指定
maxDepth,比如二度好友设为1(从 u1 → u2 → u3,共两跳),不设会无限递归直到内存溢出 -
connectFromField和connectToField要严格对应:若集合字段是user_id/friend_id,就不能写成from/to - 示例片段(查 u1 的二度好友,排除自己和一度好友):
db.friendships.aggregate([<br> { $match: { user_id: "u1" } },<br> { $graphLookup: {<br> from: "friendships",<br> startWith: "$friend_id",<br> connectFromField: "friend_id",<br> connectToField: "user_id",<br> as: "second_degree",<br> maxDepth: 1<br> }<br> }<br>])
推荐排序要预计算 + 实时修正,别全靠聚合实时算
线上推荐接口要求毫秒级响应,$graphLookup 加上权重计算(比如共同好友数、最近互动时间)一并塞进聚合里,很容易超 100ms。真实系统里,这部分必须拆开:离线预热基础关系分,线上只做轻量修正。
- 每天跑定时任务,用 MapReduce 或 Spark 计算每个用户对的
common_friends_count,存到recommend_scores集合,带复合索引{ user_id: 1, target_id: 1, score: -1 } - 用户刚给某人点赞/评论,立刻发消息触发轻量更新:
db.recommend_scores.updateOne({ user_id: "u1", target_id: "u5" }, { $inc: { score: 0.5 } }) - 查推荐时只做简单查询+排序:
db.recommend_scores.find({ user_id: "u1" }).sort({ score: -1 }).limit(20),不碰$graphLookup
地理+社交混合推荐必须用 2dsphere 索引,别信 GeoHash 字符串匹配
如果推荐规则含“同城好友优先”,很多人会把城市名当字符串查,或者自己算 GeoHash 后做前缀匹配——这既不准(边界切割失真),又没法算真实距离。MongoDB 原生 2dsphere 索引才是正解,但配置稍有偏差就会查不到数据。
- 用户集合里必须存标准 GeoJSON 格式的
loc字段:{ type: "Point", coordinates: [116.4, 39.9] },不是{ lng: 116.4, lat: 39.9 } - 建索引必须用
2dsphere类型:db.users.createIndex({ loc: "2dsphere" }),用2d会报错或结果异常 - 查询时用
$geoWithin或$near,例如找北京五公里内、且是二度好友的人:db.users.find({ loc: { $near: { $geometry: { type: "Point", coordinates: [116.4, 39.9] }, $maxDistance: 5000 } }, _id: { $in: [/* 二度好友 ID 列表 */] } })
关系图谱建模最易被忽略的点,是默认把“关注”当成对称操作——其实取消关注、拉黑、隐私设置都会让边失效,这些状态必须落库、带时间戳、可回溯,不能只靠存在/不存在判断。










