1 Preface

在社交 APP 中,经常会看到用户标签功能,那么这个功能如何实现呢? 如微信的用户标签。

user-label

考虑两种情况:

  1. 标签的量级较大,我们需要基于 ES 来加速查询;
  2. 标签量级较小,可以使用数据库实现一个简易版的标签功能;

下面我们使用数据库,实现一个简易的标签功能;

2 需求分析

以上面的截图为例,标签需要支持多选,那么问题的核心是:怎么支持指定标签组过滤
先理清这些标签之间的关系:同一行的标签之间是或关系,多行之间是与关系
那么,这里想要直接通过拼接 sql 实现,就比较复杂了。那就尝试在内存中分步实现。

3 实现思路

3.1 思路一

  1. 从用户和标签关联表中, 查询每个用户的 标签ID列表<userid, [tag_id1, tag_id2…]>;
  2. 循环检查 cids 每个ID group; (组之间是与关系, 组内是或关系)
  3. 找出所有满足条件的 user_ids;

3.2 思路二

根据传入的标签, 查询这些标签下的所有用户 (按category_id 排序)

  1. 然后按 category_id 分组,对同一组的 user_id 合并+去重 (或关系);
    注意点: (要核对分组数量, 因为有的分组可能没有用户)
  2. 把多个 category_id 分组,对 user_id 计算交集(与关系)
  3. 把合法的 userID 当做查询条件,查询即可

4 核心代码实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import "github.com/scylladb/go-set/uset"
// doMapReduce 获取符合条件的 UserIDs 列表
// 按 category_id 分组,然后计算所有分组的交集
func doMapReduce(records []vo.UserTag, categoryCount int) []uint {
	if len(records) <= 0 {
		return nil
	}
	// 1 把同一category_id 下的 userID, 合并+去重
	var prevID = records[0].CategoryTagID
	var current = uset.NewWithSize(100)
	var sets = []*uset.Set{current}
	for _, row := range records {
		if row.CategoryTagID == prevID {
			current.Add(row.UserID)
		} else {
			// new set
			prevID = row.CategoryTagID
			current = uset.New(row.UserID)
			sets = append(sets, current)
		}
	}
	// 2 集合个数, 应该等于请求的标签类别个数
	if len(sets) != categoryCount {
		return nil
	}
	// 3 计算交集,返回列表
	return uset.Intersection(sets...).List()
}

5 总结

参考