使用 MongoDB 的用户细分引擎
User Segmentation Engine using MongoDB
我有一个分析系统,以事件的形式跟踪客户及其属性以及他们的行为。它是使用 Node.js 和 MongoDB(使用 Mongoose)实现的。
现在我需要实现一个分段功能,允许根据特定条件将存储的用户分组。例如 purchases > 3 AND country = 'Netherlands'
在前端看起来像这样:
这里的一个重要要求是分段实时更新,而不仅仅是定期更新。这基本上意味着,每当用户的属性发生变化或触发新事件时,我得再查一下他到底属于哪个段
我目前的方法是将段的条件存储为 MongoDB 查询,然后我可以在用户集合上执行以确定哪些用户属于某个段。
例如,过滤掉所有使用 Gmail 的用户的部分如下所示:
{
_id: '591638bf833f8c843e4fef24',
name: 'Gmail Users',
condition: {'email': { $regex : '.*gmail.*'}}
}
当用户符合条件时,我会直接在用户文档中存储他属于 'Gmail Users' 段:
{
username: 'john.doe',
email: 'john.doe@gmail.com',
segments: ['591638bf833f8c843e4fef24']
}
但是,如果这样做,每次用户数据更改时,我都必须对所有段执行所有查询,因此我可以检查他是否属于该段。从性能的角度来看,这感觉有点复杂和繁琐。
你能想出任何替代方法来解决这个问题吗?也许使用规则引擎并在应用程序中而不是在数据库中进行处理?
很遗憾,我不知道更好的方法,但您可以稍微优化一下这个解决方案。
我也会这样做:
- 将分段条件存储在一个集合中
- 找到匹配的用户后,将细分 ID 存储在用户的文档中 (
segments
)
An important requirement here is that the segments get updated in realtime and not just periodically.
你别无选择,每次段变化时你都需要运行分段查询。
I would have to execute all queries for all segments every time a user's data changes
这是我要更改您的解决方案的地方,实际上只是稍微优化一下:
您不需要 运行 对整个集合进行分段查询。如果您将用户 ID 放入带有 $and
的查询中,Mongodb 将首先获取用户,然后再检查其余的分段条件。您需要确保 Mongodb 使用用户的 _id 作为索引,为此您可以使用 .explain()
to check it or .hint()
强制它。不幸的是,如果您有 N 个细分,则需要 运行 N+1 个查询(+1 用于用户更新)
我会获取每个段并将它们存储在缓存 (redis) 中。如果有人更改了段,我也会更新缓存。 (或者只是使缓存无效,下一个查询将处理其余部分,具体取决于实现)。关键是我将在不获取数据库的情况下拥有每个段,如果用户更新了一条记录,我将使用 Node.js 遍历每个段并根据条件验证用户,然后我可以更新用户的 segments
原始更新查询中的数组,因此不需要任何额外的数据库操作。
我知道实现这样的东西可能会很痛苦,但它不会使数据库过载...
更新
关于我的第二个建议,让我给你一些技术细节:
(这只是一个伪代码!)
段缓存
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 具有以下功能:_.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;
}
您已经将整个段 "query" 存储在段集合中的文档中 - 为什么不在同一文档中包含一个字段,该字段将枚举用户文档中的哪些字段影响特定段中的成员资格。
由于更改用户数据的操作将知道正在更改哪些字段,因此它可以仅获取使用正在更改的字段计算的段,从而显着减少分段的大小"queries"您必须重新运行.
请注意,用户数据的更改可能会将他们添加到他们当前不属于的段中,因此仅检查当前存储在用户中的段是不够的。
我有一个分析系统,以事件的形式跟踪客户及其属性以及他们的行为。它是使用 Node.js 和 MongoDB(使用 Mongoose)实现的。
现在我需要实现一个分段功能,允许根据特定条件将存储的用户分组。例如 purchases > 3 AND country = 'Netherlands'
在前端看起来像这样:
这里的一个重要要求是分段实时更新,而不仅仅是定期更新。这基本上意味着,每当用户的属性发生变化或触发新事件时,我得再查一下他到底属于哪个段
我目前的方法是将段的条件存储为 MongoDB 查询,然后我可以在用户集合上执行以确定哪些用户属于某个段。
例如,过滤掉所有使用 Gmail 的用户的部分如下所示:
{
_id: '591638bf833f8c843e4fef24',
name: 'Gmail Users',
condition: {'email': { $regex : '.*gmail.*'}}
}
当用户符合条件时,我会直接在用户文档中存储他属于 'Gmail Users' 段:
{
username: 'john.doe',
email: 'john.doe@gmail.com',
segments: ['591638bf833f8c843e4fef24']
}
但是,如果这样做,每次用户数据更改时,我都必须对所有段执行所有查询,因此我可以检查他是否属于该段。从性能的角度来看,这感觉有点复杂和繁琐。
你能想出任何替代方法来解决这个问题吗?也许使用规则引擎并在应用程序中而不是在数据库中进行处理?
很遗憾,我不知道更好的方法,但您可以稍微优化一下这个解决方案。
我也会这样做:
- 将分段条件存储在一个集合中
- 找到匹配的用户后,将细分 ID 存储在用户的文档中 (
segments
)
An important requirement here is that the segments get updated in realtime and not just periodically.
你别无选择,每次段变化时你都需要运行分段查询。
I would have to execute all queries for all segments every time a user's data changes
这是我要更改您的解决方案的地方,实际上只是稍微优化一下:
您不需要 运行 对整个集合进行分段查询。如果您将用户 ID 放入带有
$and
的查询中,Mongodb 将首先获取用户,然后再检查其余的分段条件。您需要确保 Mongodb 使用用户的 _id 作为索引,为此您可以使用.explain()
to check it or.hint()
强制它。不幸的是,如果您有 N 个细分,则需要 运行 N+1 个查询(+1 用于用户更新)我会获取每个段并将它们存储在缓存 (redis) 中。如果有人更改了段,我也会更新缓存。 (或者只是使缓存无效,下一个查询将处理其余部分,具体取决于实现)。关键是我将在不获取数据库的情况下拥有每个段,如果用户更新了一条记录,我将使用 Node.js 遍历每个段并根据条件验证用户,然后我可以更新用户的
segments
原始更新查询中的数组,因此不需要任何额外的数据库操作。 我知道实现这样的东西可能会很痛苦,但它不会使数据库过载...
更新
关于我的第二个建议,让我给你一些技术细节: (这只是一个伪代码!)
段缓存
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 具有以下功能:_.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;
}
您已经将整个段 "query" 存储在段集合中的文档中 - 为什么不在同一文档中包含一个字段,该字段将枚举用户文档中的哪些字段影响特定段中的成员资格。
由于更改用户数据的操作将知道正在更改哪些字段,因此它可以仅获取使用正在更改的字段计算的段,从而显着减少分段的大小"queries"您必须重新运行.
请注意,用户数据的更改可能会将他们添加到他们当前不属于的段中,因此仅检查当前存储在用户中的段是不够的。