用户分段引擎使用MongoDB
我有一个分析系统,以事件的forms跟踪客户及其属性以及他们的行为。 它使用Node.js和MongoDB(使用Mongoose)来实现。
现在我需要实现一个分段function,允许根据特定的条件将存储的用户分组。 例如, purchases > 3 AND country = 'Netherlands'
在前端,这看起来像这样:
这里一个重要的要求是细分市场实时更新,而不是定期更新。 这基本上意味着,每当用户的属性改变或触发新的事件时,我必须再次检查他属于哪个段。
我目前的做法是将这些段的条件存储为MongoDB查询,然后我可以在用户集合上执行,以确定哪些用户属于某个段。
例如,用于过滤所有使用Gmail的用户的细分将如下所示:
{ _id: '591638bf833f8c843e4fef24', name: 'Gmail Users', condition: {'email': { $regex : '.*gmail.*'}} }
当用户匹配条件时,我会直接在用户的文档中存储他属于“Gmail用户”细分:
{ username: 'john.doe', email: 'john.doe@gmail.com', segments: ['591638bf833f8c843e4fef24'] }
但是通过这样做,每次用户数据发生变化时,我都必须执行所有段的查询,以便我可以检查他是否是段的一部分。 从性能的angular度来看,这感觉有些复杂和繁琐。
你可以想办法解决这个问题吗? 也许使用规则引擎,并在应用程序中而不是在数据库中进行处理?
不幸的是,我不知道更好的方法,但可以稍微优化这个解决scheme。
我会这样做:
- 将分段条件存储在集合中
- 一旦find匹配的用户,将段ID存储在用户的文档(
segments
)
这里一个重要的要求是细分市场实时更新,而不是定期更新。
您别无select,每次段更改时都需要运行分段查询。
每当用户数据发生变化时,我都必须执行所有段的查询
这是我改变你的解决scheme,实际上只是优化一点:
-
您无需在整个集合上运行分段查询。 如果你把你的用户id带到
$and
查询中,Mongodb会首先获取用户,之后将检查剩余的分段条件。 你需要确保Mongodb使用用户的_id作为索引,为此你可以使用.explain()
来检查它或者.hint()
来强制它。 不幸的是,如果你有N个段,你需要运行N + 1个查询(+1用于更新用户) -
我会抓取每个细分,并将其存储在caching(redis)中。 如果有人更改了分段,我也会更新caching。 (或只是使caching无效,下一个查询将处理剩下的,取决于实现)。 重点是,我会有每一个段没有提取数据库,如果用户更新logging,我会通过Node.js通过每个细分,并通过条件validation用户,我可以更新原始更新查询中的用户的
segments
数组所以不需要额外的数据库操作。 我知道这可能是一个痛苦的屁股实现这样的事情,但它不会超载数据库…
更新
让我给你一些关于我的第二个build议的技术细节:( 这只是一个伪代码!)
段caching
module.exporst = function() { return new Promise(resolve) { Redis.get('cache:segments', function(err, segments) { // handle error // Segments are cached if(segments) { segments = JSON.parse(segments); return resolve(segments); } //fetch segments and save it to the cache Segments.find().exec(function(err, segments) { // handle error segments = JSON.stringify(segments); // Save to the database but set 60 seconds as an expiration Redis.set('cache:segments', segments, 'EX', 60, function(err) { // handle error return resolve(segments); }) }); }) } }
用户更新
// ... let user = user.findOne(_id: ObjectId(req.body.userId)); // etc ... // fetch segments from cache or from the database let segments = yield segmentCache(); let userSegments = []; segments.forEach(function(segment) { if(checkSegment(user, segment)) { userSegments.push(segment._id) } }); // Override user's segments with userSegments
这就是魔法发生的地方,不知何故,你需要以一种可以在if语句中使用它们的方式来定义条件。
提示:Lodash有这个function:_.gt,_.gte,_.eq …
检查段
module.exports = function(user, segment) { let keys = Object.keys(segment.condition); keys.forEach(function(key) { if(user[key] === segment.condition[key]) { return false; } }) return true; }
您已经在分段集合中的文档中存储了整个分段“查询” – 为什么不在同一个文档中包含一个字段来枚举用户文档中哪些字段影响特定分段的成员资格。
由于更改用户数据的操作将知道哪些字段正在更改,因此只能获取使用字段被更改的计算的分段,显着减less了必须重新运行的分段“查询”的大小。
请注意,用户数据的更改可能会将它们添加到它们当前不属于的分段,因此仅检查当前存储在用户中的分段是不够的。