赞
踩
在生活中人们有时非常讨厌来自别人的标签,比如 你该 xxxx 你是xxxx 应该 xxxxx ,但是标签化的思想非常便于对资源的查询,根据不同的资源类型进行划分可以更快的帮助用户找到想要搜索的内容.
总结 : 凡是有价值的数据都可以打上标签,标签之间也可以设计权重,实现更自定意化的推荐,比如节假日带有相关标签的商品计算推送时权重更高 , 比如重点用户身上打 vip 标签 ,让其被检索到概率更高(抖音推送) ,比如按照标签分析用户对不同类别的喜好,分析用户的行为决定下一阶段的侧重点, 重点用户使用体验优化(使用程度重的用户优化其资源加载速度) , 标签的作用就是做区分,有时只有做好了区分才能知道从哪里能获得更多价值.总的来说,标签的优化和应用可以在多个方面增加价值,从提升用户体验、个性化推荐,到精准广告投放和业务决策,都能够从中获益。标签不仅是一种分类和区分的方式,更是优化和提升整体平台价值的关键手段之一。
- 一张维护标签的表
- 为资源打上标签
- 同一张表上
- 新建表维护资源标签关系
在这种方法中,你会创建一张独立的数据库表来维护所有标签。每个标签都有一个唯一的标识符,以及与之相关的其他信息,如标签名称、描述等。资源与标签之间的关系通过在资源表中添加一个外键来实现,该外键与标签表中的标识符关联。
优点:
缺点:
适用场景:
在这种方法中,你会创建一个新的数据库表来维护资源与标签之间的关系。该表会记录每个资源与其相关的标签。此外,可能还需要一个标签表来存储标签的信息。
优点:
缺点:
适用场景:
- 固定页面标签
- 请求数据库动态渲染
- 固定 + 请求模式
主要是查询实现,针对标签为资源信息本身固有属性去实现
- 接收 前端查询的标签列表, 模糊查询
- 接收前端拆寻标签列表,查询全部数据内存种计算
- 根据权重查询部分标签 + 剩余标签内存计算
```(比如个性化推荐时候那些标签权重比较高为必查标签,必查标签查到了之后去进一步做匹配排列返回数据先后顺序)``- 两种方法都使用 , 有限采用算的快的进行数据返回
极限性能
具体查询方式好坏需要根据实际数据量情况下进行测试鉴别 , 没有觉得的快慢 , 实际应用时涉及到了用户可选标签的多少, 数据库的性能,(模糊查询的匹配方式 索引大概率是失效的)
涉及到多级标签 ,可以自定义适合场景的规则,比如一级标题代表资源类型,比如视频,音频,文件,文档,二级标题代表这各自领域的东西
ps : 标签的优点在于灵活,给程序打上不同的标签便于用户和程序员两方面更好的操纵资源数据。这里可以给标签一个权重 ,用于在有权中的情况下更加灵活的实现更丰富的功能,同时也可以更加灵活的实现消息推送等功能。
-- 标签 -- auto-generated definition create table tag ( id bigint auto_increment comment 'id' primary key, tagName varchar(256) null comment '标签名称', userId bigint null comment '用户 id', parentId bigint null comment '父标签 id', isParent tinyint null comment '0 - 不是, 1 - 父标签', createTime datetime default CURRENT_TIMESTAMP null comment '创建时间', updateTime datetime default CURRENT_TIMESTAMP null on update CURRENT_TIMESTAMP, isDelete tinyint default 0 not null comment '是否删除', constraint uniIdx_tagName unique (tagName) ) comment '标签';
可扩展字段 [场景] [权重]
以常见交友软件为例 比如soul
会让你给自己打标签,也会让你做题给你分配不同的星球实际上也是一种标签 , 有时用户的个人信息 ,比如年龄性格专业的行为分析之后也会有一套隐形的标签系统 , 比如淘宝从来不给我推便宜货但也不给我推荐特别贵的 , 比如我的抖音总是能刷到适龄女性。所以以用户身上的标签为例。
create table user ( id bigint auto_increment comment 'id' primary key, userAccount varchar(256) not null comment '账号', userPassword varchar(512) not null comment '密码', userName varchar(256) null comment '用户昵称', userAvatar varchar(1024) null comment '用户头像', userRole varchar(256) default 'user' not null comment '用户角色:user/admin', userMailbox varchar(40) null comment '邮箱', tags varchar(1024) null comment '标签 json 列表', accessKey varchar(512) not null comment 'accessKey', secretKey varchar(512) not null comment 'secretKey', createTime datetime default CURRENT_TIMESTAMP not null comment '创建时间', updateTime datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间', isDelete tinyint default 0 not null comment '是否删除' ) comment '用户' collate = utf8mb4_unicode_ci; create index idx_userAccount on user (userAccount);
通过两种方式查询一种使用数据库查询 ,一种使用内存方式去查询,数据库查询时返回信息做分页处理,当然下面的数据库查询换个分页查询的写法也一样。这没什么难度,写入时候将前端的传值转换成 json 串存起来就好,也可以数据库用 Json 格式的数据存,当然那样操作数据库的时候难度会大一些 ,写起来可能不如基础的 crud 好些 。
@Override public Page<UserVO> searchUsersByTags(UserTagsQueryRequest userTagsQueryRequest) { List<String> tagNameList = userTagsQueryRequest.getTags(); ThrowUtils.throwIf(CollectionUtils.isEmpty(tagNameList), ErrorCode.PARAMS_ERROR); QueryWrapper<User> userQueryWrapper = new QueryWrapper<>(); // 设置查询条件 // ... List<User> users = userMapper.selectList(userQueryWrapper); List<UserVO> userVOList = users.stream() .filter(user -> { String tags = user.getTags(); Set<String> tagSet = GSON.fromJson(tags, new TypeToken<Set<String>>() {}.getType()); tagSet = Optional.ofNullable(tagSet).orElse(new HashSet<>()); return tagSet.containsAll(tagNameList); }) .map(this::getUserVO) .collect(Collectors.toList()); Page<UserVO> userVOPage = new Page<>(); long pageSize = userTagsQueryRequest.getPageSize(); long current = userTagsQueryRequest.getCurrent(); int total = userVOList.size(); userVOPage.setTotal(total); userVOPage.setSize(pageSize); userVOPage.setCurrent(current); int index = (int)((current - 1) * pageSize); int toIndex = (int)(current * pageSize); if ( total < index ){ userVOPage.setRecords(new ArrayList<>()); return userVOPage; } toIndex = Math.min(total, toIndex); List<UserVO> userVOS = userVOList.subList(index, toIndex); userVOPage.setRecords(userVOS); return userVOPage; } @Deprecated public List<UserVO> searchUsersByTagsBySQL(List<String> tagNameList) { ThrowUtils.throwIf(CollectionUtils.isEmpty(tagNameList), ErrorCode.PARAMS_ERROR); QueryWrapper<User> userQueryWrapper = new QueryWrapper<>(); // 数据库模糊计算 for (String tagName : tagNameList) { userQueryWrapper.like("tags", tagName); } //查找数据 // TODO 分页 List<User> users = userMapper.selectList(userQueryWrapper); return users.stream().map(this::getUserVO).collect(Collectors.toList()); }
当然这没什么了不起,只是能让用户更快的找到自己的爱好罢了。 有些小网站经常这面做,啊常见的不得了。
有灵性的网站才是用户喜欢的, 审美都是不断变化的,一成不变的情况下用户很容易对网站失去兴趣的, 优质内容的更新 ,用户体验的升级,优良的氪金系统才是用户的所爱 ,好吧对于我这种不开 VIP 的人有的时候真的会为了获取信息或者说有些服务买单。比如稳定的梯子,知识星球,服务器等等。
一个简单的匹配机制,类似于 soul
的推荐 ;
基于用户身上已经存在的标签 , 我们可以对用户进行个性化的推荐划分了 。
实现方式为 编辑距离 算法,在打分的基础上进行排序,实现对标签相似度较高的用户进行推荐 。
@Override public List<User> matchUsers(long num, User loginUser) { QueryWrapper<User> queryWrapper = new QueryWrapper<>(); queryWrapper.select("id", "tags"); queryWrapper.isNotNull("tags"); List<User> userList = this.list(queryWrapper); String tags = loginUser.getTags(); Gson gson = new Gson(); List<String> tagList = gson.fromJson(tags, new TypeToken<List<String>>() { }.getType()); // 用户列表的下标 => 相似度 List<Pair<User, Long>> list = new ArrayList<>(); // 依次计算所有用户和当前用户的相似度 for (int i = 0; i < userList.size(); i++) { User user = userList.get(i); String userTags = user.getTags(); // 无标签或者为当前用户自己 if (StringUtils.isBlank(userTags) || user.getId() == loginUser.getId()) { continue; } List<String> userTagList = gson.fromJson(userTags, new TypeToken<List<String>>() { }.getType()); // 计算分数 long distance = AlgorithmUtils.minDistance(tagList, userTagList); list.add(new Pair<>(user, distance)); } // 按编辑距离由小到大排序 List<Pair<User, Long>> topUserPairList = list.stream() .sorted((a, b) -> (int) (a.getValue() - b.getValue())) .limit(num) .collect(Collectors.toList()); // 原本顺序的 userId 列表 List<Long> userIdList = topUserPairList.stream().map(pair -> pair.getKey().getId()).collect(Collectors.toList()); QueryWrapper<User> userQueryWrapper = new QueryWrapper<>(); userQueryWrapper.in("id", userIdList); // 1, 3, 2 // User1、User2、User3 // 1 => User1, 2 => User2, 3 => User3 Map<Long, List<User>> userIdUserListMap = this.list(userQueryWrapper) .stream() .map(user -> getSafetyUser(user)) .collect(Collectors.groupingBy(User::getId)); List<User> finalUserList = new ArrayList<>(); for (Long userId : userIdList) { finalUserList.add(userIdUserListMap.get(userId).get(0)); } return finalUserList; }
编辑距离算法
public class AlgorithmUtils { /** * 编辑距离算法(用于计算最相似的两组标签) * 原理:https://blog.csdn.net/DBC_121/article/details/104198838 * * @param tagList1 * @param tagList2 * @return */ public static int minDistance(List<String> tagList1, List<String> tagList2) { int n = tagList1.size(); int m = tagList2.size(); if (n * m == 0) { return n + m; } int[][] d = new int[n + 1][m + 1]; for (int i = 0; i < n + 1; i++) { d[i][0] = i; } for (int j = 0; j < m + 1; j++) { d[0][j] = j; } for (int i = 1; i < n + 1; i++) { for (int j = 1; j < m + 1; j++) { int left = d[i - 1][j] + 1; int down = d[i][j - 1] + 1; int left_down = d[i - 1][j - 1]; if (!Objects.equals(tagList1.get(i - 1), tagList2.get(j - 1))) { left_down += 1; } d[i][j] = Math.min(left, Math.min(down, left_down)); } } return d[n][m]; } // [编程学习交流圈](https://www.code-nav.cn/) 连接万名编程爱好者,一起优秀!20000+ 小伙伴交流分享、40+ 大厂嘉宾一对一答疑、100+ 各方向编程交流群、4000+ 编程问答参考 /** * 编辑距离算法(用于计算最相似的两个字符串) * 原理:https://blog.csdn.net/DBC_121/article/details/104198838 * * @param word1 * @param word2 * @return */ public static int minDistance(String word1, String word2) { int n = word1.length(); int m = word2.length(); if (n * m == 0) { return n + m; } int[][] d = new int[n + 1][m + 1]; for (int i = 0; i < n + 1; i++) { d[i][0] = i; } for (int j = 0; j < m + 1; j++) { d[0][j] = j; } for (int i = 1; i < n + 1; i++) { for (int j = 1; j < m + 1; j++) { int left = d[i - 1][j] + 1; int down = d[i][j - 1] + 1; int left_down = d[i - 1][j - 1]; if (word1.charAt(i - 1) != word2.charAt(j - 1)) { left_down += 1; } d[i][j] = Math.min(left, Math.min(down, left_down)); } } return d[n][m]; } }
- Jaccard相似系数: 用于计算集合之间的相似性,尤其在文本、推荐系统等领域常被使用。它衡量两个集合的交集与并集之间的比例,范围在0到1之间。
- 汉明距离(Hamming Distance): 主要用于计算两个等长字符串之间的差异,即在相同位置上不同字符的数量。主要用于处理二进制数据。
- Jaro-Winkler相似度: 用于比较两个字符串的相似性,尤其在姓名匹配等场景中常用。它考虑了字符匹配、字符顺序和字符位置等因素。
- Dice系数: 用于衡量两个集合的相似性,类似于Jaccard相似系数,但在计算时考虑了集合中元素的重复次数。
- TF-IDF(Term Frequency-Inverse Document Frequency): 用于比较文本的相似性,基于词项的出现频率和在文本集合中的重要程度。主要用于信息检索和文本挖掘。
- 皮尔逊相关系数: 用于度量两个变量之间的线性相关性,取值范围在-1到1之间。主要用于统计分析和数据挖掘。
- 曼哈顿距离: 衡量两个点在多维空间中的距离,是各个坐标绝对值差的和。
- 欧几里德距离: 类似曼哈顿距离,但考虑了坐标差的平方和的平方根。
- 标签传播算法(Label Propagation): 用于图数据中的节点分类和相似性传播,将相似节点的标签传播给邻居节点。
- K-Means聚类算法: 用于将数据分成K个簇,相似的数据点被归为同一簇。
- 编辑距离 :
- 应用场景:主要用于比较两个字符串之间的相似性,测量从一个字符串转换为另一个字符串所需的最小编辑操作次数(插入、删除、替换)。
- 功能:常用于拼写检查、自然语言处理中的文本相似度、DNA序列匹配等领域。可以用于判断两个字符串的相似程度以及实现自动纠错。
- 余弦相似度 :
- 应用场景:主要用于比较两个向量之间的相似性,常用于文本挖掘、信息检索等领域。
- 功能:度量两个向量之间的夹角余弦值,值越接近1表示两个向量越相似,越接近0表示越不相似。常用于判断文本、文章等之间的相似性,以及在推荐系统中进行用户相似性计算。
就像是我们给别人打标签一样, 可能表面一套心里一套,总不能让我的 APP 告诉它他把我归类为
穷AC
吧 , 就像是逻辑删除字段一样,有的时候我们不希望这些数据被用户获得,这不同于用户,或者是资源分配者对用户打的标签,而是系统 或者说是在观测用户行为之后,为了给用户更好的服务体验, 暗暗加在用户身上的隐藏标签 ,嗯类似于 隐藏分 , 系统都觉得你菜之类的。明签便于让用户搜索,提供更加精准的搜索,找到想要的信息,那暗签的存在则是为了更好的提高用户的体验,根据历史行为和明签计算重新分批分配用户所属组优化体验,定制广告,好吧就是更魔鬼一些,让用户感受到开发者的温度。数据本来是冰冷的,作为开发者更能体会到产品的趣味性,我们会更注重体验,这是不得不承认的事实,比如定位服务错误,我多付的两块共享单车钱,比如电影院系统故障,充值成功但会员因为系统原因没到账,我们是开发者更能体会到有些有些产品的无趣性,暗签的使用则更能提高产品的温度。当然也会带来更多的收益。
以上示例代码中,每个用户都会计算一遍相似度,然后进行推算,实在是不算靠谱,可以通过缓存的形式去缓解压力,但是难道数据量上去了也这么办?
但是想到的解决办法是 , 统计用户活跃状态,对活跃用户数据进行提前缓存,缓存中放提前算好的相似度匹配最高的前 100 条 id 当传来分页参数的时候先去缓存中获取准备好的 id 然后查询 , 当超过这些数据后在进行相似度匹配计算查询结果分页。
当然有的时候不需要那么的精确,因为我看到了这个
K-Means聚类算法: 用于将数据分成K个簇,相似的数据点被归为同一簇。(这个算法计算经纬度的,emmm 再次用到了)
刷抖音会发现,有的时候我和我的损友其实刷到的东西都蛮像的,那是否意味着所有人的推荐都一样,但是发现有些其他人的推荐与我们相差甚远,答案就产生了,我们不需要去计算所有的相似度只要分类分好了,到时候这些人互相推荐就好了,如果涉及到个用户推荐资源也是一样,用户分类,打上一个标签,对应一份计算好的资源列表,问题解决,当然具体实现还是蛮复杂的, 需要设计好标签,标签计算,如果数据量更大的话还得使用大数据 ,掌握一些大数据相关的知识。
比如说有几亿个商品,难道要查出来所有的商品? 难道要对所有的数据计算一遍相似度?人数据推荐流程:
检索 =>召回 =>粗排 =>精排 =>重排序等等
检索: 尽可能多地查符合要求的数据 (比如按记录查)
召回: 查询可能要用到的数据 (不做运算)
粗排: 粗略排序,简单地运算 (运算相对轻量)
精排: 精细排序,确定固定排位
性能计算, 算时间,实战测试不同的数据量对性能的影响
更好的缓存使用策略,节省空间的情况下实现更好的用户体验(比如对计算出来的推荐缓存,换成分簇的算法,或者说定时新建删除,临时表维护用户推荐关系
标签变动用户增长
)缓存的提升性能极限
如何关闭批量查询时候的日志,很影响性能百万数据查询的情况下有 60% 左右的性能浪费在打日志上
标签化资源是很重要的一点能力,它很通用,他更符合人的习惯,可以更好的利用信息,无论是提高用户检索,使用体验,还是在创收这方面都是很重要的,但是完整的标签化体系的建立不是一朝一夕就能搞定的了的,不过一套标签体系的建立真的就是一招鲜吃遍天的情况,当然在有些领域可能并不重要,但实际上有些时候对数据进行分类也是标签化思想,举个例子,商品中的促销,热卖,优品,样本中的优良可差,还有最常见的状态字段,这些都是通过给数据打标签来解决实际问题,如果换做今天的问题的话就是他们都是一个 tag 表中的一条数据,给它加上一个使用场景就好了(多对多关系);
好的标签体系是不断的总结设计出来的,可以参考前人的实现一点点归纳成熟的标签体系搭建之后是可以多套系统复用的,所以我觉得他是编程中的通用能力。
其中还涉及到新建标签的权限,比如 CSDN 只能让有影响力的博主创建标签,抖音随便创建标签,这些都是要搭建一套好的标签体系要考虑进去的事情。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。