如何将编辑距离算法运用到根据标签进行用户智能匹配的业务场景中
-
什么是最小编辑距离(可以看看下面这位博主的详细介绍)
编辑距离算法详细介绍 -
将代码在java测试类里面进行测试
package com.way.threes_company_backend.service; import com.way.threes_company_backend.utils.AlgoUtils; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @SpringBootTest public class AlgoUtilsTest { public 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]; } @Test void test1() { String str1 = "你是狗"; String str2 = "你是狗吗"; String str3 = "我是你大爷"; System.out.println(minDistance(str1, str2)); // 这里输出的值就是1 表示两个字符串之间str1转换为str2需要多少步骤(增删改) 只需要增加吗 1 System.out.println(minDistance(str1, str3)); // 需要删除狗 增加你大爷 输出的就是4 // 由此可以看出str1和str2肯定是最相似最匹配的,因为它们计算出来的值最小 } }
-
这里我们还需要给代码做一些优化,因为我们的用户标签是这种格式[“java”,“男”, “大二”] 是列表格式,所以我们需要进行稍微的更改源代码
public static int minDistance(List<String> listTag1, List<String> listTag2) { int n = listTag1.size(); int m = listTag2.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 (!listTag1.get(i - 1).equals(listTag2.get(j - 1))) // 比较是否元素相等 集合里面的元素 使用equals进行比较 left_down += 1; d[i][j] = Math.min(left, Math.min(down, left_down)); } } return d[n][m]; } // 再进行测试 ... @Test void test2() { List<String> list1 = Arrays.asList("java", "大二"); List<String> list2 = Arrays.asList("java", "大三"); List<String> list3 = Arrays.asList("java", "大四", "重庆", "男"); System.out.println(AlgoUtils.minDistance(list1, list2)); // 1 System.out.println(AlgoUtils.minDistance(list1, list3)); // 3 // 显然列表1与列表2的匹配度更高 } ...
-
应用场景:我们需要在用户首页进行用户匹配之后推荐出标签最相近的5位用户,然后将信息展示到首页,同时也可以展示另外几条不太匹配的用户信息(随机推荐)
实现步骤
-
获取当前登录用户的id,根据id查询出当前登录用户的tags(标签数据)
-
创建一个查询对象,条件是只查询id和tags并且tags不为空的数据(能够提升查询速度)查询所有用户的tags
-
使用pair创建一个临时的列表存储用户的(id, tags) 和 编辑距离的值(得分)
-
遍历上面查到的所有用户的tags然后调用封装的utils里面的编辑距离算法和当前用户的tags进行计算,然后将结果存储到pairList里面
-
使用集合的stream流方法进行pairList的数据排序,并且只取num个数据(top num)
-
然后将top num的数据里面的id单独提取出来(因为我们最后需要返回这些id对应的详细的用户信息,然后展示到前端
-
创建一个查询对象,条件是用户的id在这个idList里面的用户信息 使用一个map集合进行存储 Map<Integer, List>
-
这里的map会将我们需要的top num的顺序进行打乱,所以最后我们还需要创建一个list集合 + for循环遍历前面的idList取出对应的Map里面的用户数据 才是正确的顺序
代码演示
@Override public List<User> matchUsers(long num, User loginUser) { final int userId = loginUser.getId(); // 01. 创建一个查询对象 只查询id 和 tags 并且过滤掉空的数据 这样可以提升查询的速度(减少了不需要的字段的查询且过滤了空的数据) QueryWrapper<User> queryWrapper = new QueryWrapper<>(); queryWrapper.select("id", "tags"); queryWrapper.isNotNull("tags"); List<User> list = this.list(queryWrapper); // 02. 获取到当前登录用户的tags 它是作为编辑距离算法的根本 将所有的用户的tags和当前登录用户的tag进行比较找出最相似的几个用户即可 (原理) String tags = loginUser.getTags(); // 03. 使用gson将json格式序列化为一个列表形式 因为存储在mysql里面的数据是json格式 Gson gson = new Gson(); List<String> loginUserTagList = gson.fromJson(tags, new TypeToken<List<String>>() { }.getType()); // 04. 使用pair来临时存储用户信息 和相似度分数 List<Pair<User, Long>> pairList = new ArrayList<>(); // 05. for循环依次遍历所有用户信息来计算相似度 for (int i = 0; i < list.size(); i++) { // 05-1. 获取user对象的tags User user = list.get(i); String tagsP = user.getTags(); // 05-2. 不要标签为空的用户 和 自己(它会查到自己) if(StringUtils.isBlank(tagsP) || user.getId() == userId) { continue; // 表示跳过当前循环继续执行 break表示终止循环 } // 05-3. 将当前的tag进行序列化 List<String> tagPList = gson.fromJson(tagsP, new TypeToken<List<String>>() {}.getType()); // 05-4. 调用编辑距离算法进行计算相似度 然后追加到pair里面存储 存储用户信息和分数 long score = AlgoUtils.minDistance(loginUserTagList, tagPList); pairList.add(new Pair<>(user, score)); } // 06. 按照编辑距离由小到大排序 且只取出指定的数据条数 List<Pair<User, Long>> topUserPairList = pairList.stream() .sorted((a, b) -> (int) (a.getValue() - b.getValue())) .limit(num) .collect(Collectors.toList()); // 07. 将pair里面的id单独提取出来 目的是为了我们能够获取到所有的用户信息然后再打印出来 使用map能进行一个新的映射 List<Integer> idList = topUserPairList.stream().map(a -> a.getKey().getId()).collect(Collectors.toList()); // 08. 创建查询对象,查询当前id在id列表里面的用户数据 QueryWrapper<User> userQueryWrapper = new QueryWrapper<>(); // 查询当前id在这个id列表里面的用户数据 userQueryWrapper.in("id", idList); // 09. 将查询出来的结果使用一个map集合进行存储 userid1 -> user1 userid2 -> user2... Map<Integer, List<User>> userIdUserListMap = this.list(userQueryWrapper) .stream() .map(user -> getSafetyUser(user)) // 用户信息脱敏 .collect(Collectors.groupingBy(User::getId)); // 10. 创建一个新的集合对象来将顺序进行规整 需要返回的顺序 1 2 3 前面map的顺序 1 3 2 List<User> finalUserList = new ArrayList<>(); // 使用for循环遍历前面的正确顺序的idList 然后将map里面的数据调用get方法,传递id且获取一个 最后追加到list里面返回即可 for (Integer integerUserId : idList) { finalUserList.add(userIdUserListMap.get(integerUserId).get(0)); } return finalUserList; }
最后前端调用接口就可以返回和自己标签匹配度top num的用户详细数据了
-