--- title: "12-搜索标签serviceimpl" created: 2025-12-02 tags: - 项目 aliases: - 搜索标签serviceimpl --- # 搜索标签serviceimpl ## 搜索标签 ### 要实现什么: 允许用户传入多个标签,多个标签都存在才搜索出来 (and) 允许用户传入多个标签,有任何一个存在就搜索出来 (or) ### 怎么实现? #### 1、sql查询(实现简单) ```java /** * 根据标签搜索用户 * * @param tagNameList 用户要拥有的标签 * @return */ @Override public List searchUserByTags(List tagNameList){ if(CollectionUtils.isEmpty(tagNameList)){ throw new BusinessException(ErrorCode.PARAMS_ERROR); } QueryWrapper queryWrapper = new QueryWrapper<>(); //拼接and查询 //like '%java%' and like '%python%' for(String tagName : tagNameList){ queryWrapper = queryWrapper.like("tags",tagName); } List userList=userMapper.selectList(queryWrapper); return userList.stream().map(this::getSafetyUser).collect(Collectors.toList()); } ``` **代码解释:** 1. **空值检查**: `if(CollectionUtils.isEmpty(tagNameList)){` `throw new BusinessException(ErrorCode.PARAMS_ERROR);` `}` 检查 `tagNameList` 是否为空,如果为空则抛出一个业务异常 (`BusinessException`),并返回错误码 `PARAMS_ERROR`。 2. **构建查询条件**: `QueryWrapper queryWrapper = new QueryWrapper<>();` 创建一个 `QueryWrapper` 对象,用于构建 SQL 查询条件。 3. **拼接多个** `like` **查询条件**: `queryWrapper.and(wrapper -> {` `for (String tagName : tagNameList) {` `wrapper.like("tags", tagName);` `}` `});` 通过 `queryWrapper.and()` 方法,将多个 `like` 查询条件组合在一起。 每个标签 (`tagName`) 都会被转换为一个 `LIKE` 条件,查询数据库中 `tags` 字段包含这些标签的用户。 例如,如果 `tagNameList` 包含 `"java"` 和 `"python"`,最终的查询条件类似于: `WHERE tags LIKE '%java%' AND tags LIKE '%python%'` 这种查询方式会返回同时包含 `java` 和 `python` 标签的用户。 4. **执行查询**: `List userList = userMapper.selectList(queryWrapper);` 根据构建的 `queryWrapper` 查询数据库,获取符合条件的所有用户列表。 5. **返回处理后的用户列表**: `return userList.stream().map(this::getSafetyUser).collect(Collectors.toList());` 对查询到的用户列表进行处理(通过 `getSafetyUser` 方法),去除敏感信息。 最后将处理后的用户列表返回。 ![[image-a4974768.png]] ![[image-517b04de.png]] #### 2、内存查询(灵活) java json 序列化库有很多: 1. gson(google 的,推荐) 2. fastjson alibaba(阿里出品,快,但是漏洞太多) 3. jackson 4. kryo(性能极高的序列化库) 这里用gson 添加依赖 ```xml com.google.code.gson gson 2.8.9 ``` ```java /** * 根据标签搜索用户 * * @param tagNameList 用户要拥有的标签 * @return */ @Override public List searchUserByTags(List tagNameList) { if (CollectionUtils.isEmpty(tagNameList)) { throw new BusinessException(ErrorCode.PARAMS_ERROR); } //查出所有用户 QueryWrapper queryWrapper = new QueryWrapper<>(); List userList = userMapper.selectList(queryWrapper); Gson gson = new Gson(); //在内存中判断是否包含要求的标签 return userList.stream().filter(user -> { String tagsStr = user.getTags(); if(StringUtils.isBlank(tagsStr)){ return false; } Set tempTagNameSet = gson.fromJson(tagsStr, new TypeToken>() { }.getType()); for (String tagName : tagNameList) { if(!tempTagNameSet.contains(tagName)){ return false; } } return true; }).map(this::getSafetyUser).collect(Collectors.toList()); } ``` 代码解释: 查询所有用户 `QueryWrapper queryWrapper = new QueryWrapper<>();` `List userList = userMapper.selectList(queryWrapper);` - 使用 `QueryWrapper` 构建查询条件对象,这里没有任何具体条件,所以它会查询出数据库中所有的用户。 - `userMapper.selectList(queryWrapper)` 通过 `userMapper` 查询用户数据,并将所有用户信息存入 `userList` 列表。 使用 Gson 解析标签并进行过滤 `Gson gson = new Gson();` `return userList.stream().filter(user -> {` `String tagsStr = user.getTags();` `if (StringUtils.isBlank(tagsStr)) {` `return false;` `}` `Set tempTagNameSet = gson.fromJson(tagsStr, new TypeToken>() {}.getType());` `for (String tagName : tagNameList) {` `if (!tempTagNameSet.contains(tagName)) {` `return false;` `}` `}` `return true;` `}).map(this::getSafetyUser).collect(Collectors.toList());` a. 转换为流并进行过滤 - `userList.stream()`:将 `userList` 转换为流(Stream),以便进行后续的操作。 - `.filter(user -> {...})`:对每个用户执行过滤操作,只有符合条件的用户才会被保留。过滤条件如下: b. 解析用户的标签字符串 - `String tagsStr = user.getTags();`:从每个 `user` 对象中获取 `tags` 字段,该字段存储了用户的标签(通常是一个 JSON 格式的字符串)。 - `if (StringUtils.isBlank(tagsStr)) { return false; }`:如果标签字符串为空或 null,直接跳过该用户。 c. 解析标签字符串为 `Set` - `Set tempTagNameSet = gson.fromJson(tagsStr, new TypeToken>() {}.getType());`:使用 Gson 将 `tagsStr` 字符串反序列化为 `Set` 对象。假设 `tagsStr` 是一个 JSON 数组(如 `["tag1", "tag2"]`),该操作将其转换为一个 `Set` 集合,包含所有标签。 d. 检查每个标签是否匹配 - `for (String tagName : tagNameList) { if (!tempTagNameSet.contains(tagName)) { return false; } }`:遍历传入的 `tagNameList`,检查每个标签是否出现在 `tempTagNameSet`(即用户的标签集合)中。如果其中任何一个标签没有匹配,返回 `false`,表示该用户不符合条件。 e. 如果所有标签匹配,返回该用户 - 如果所有标签都匹配,则 `filter` 会返回 `true`,该用户会被保留下来。 获取安全用户信息并返回结果 `.map(this::getSafetyUser).collect(Collectors.toList());` - `.map(this::getSafetyUser)`:通过 `map` 操作将符合条件的用户对象转换为 "安全用户" 信息,去掉敏感信息、进行数据脱敏。 - `.collect(Collectors.toList())`:将过滤后的用户流转换为一个新的 `List` 集合并返回。 ![[image-e01f2890.png]] #### 对比 ##### 1. **查询方式** - **第一种方法**:通过 SQL 查询直接在数据库中进行过滤。 - 使用 `QueryWrapper` 的 `like` 方法来拼接多个 `LIKE` 条件,查询符合标签条件的用户。 - 通过 `tagNameList` 中的每个标签拼接 `like "tags" '%tag%'` 查询条件,数据库会返回符合这些条件的用户列表。 - **优点**:所有的查询操作在数据库中进行,数据库通常比内存处理更高效,尤其是数据量大的时候。 - **缺点**:对于多个 `LIKE` 查询条件拼接的性能可能较差,尤其是在没有索引的字段上。如果标签数据存储格式比较复杂,可能会遇到效率瓶颈。 - **第二种方法**:查询所有用户后,在内存中对用户的标签进行过滤。 - 通过 `userMapper.selectList(queryWrapper)` 获取所有用户数据,然后使用 Java 流(Stream)对每个用户的标签字符串进行处理。 - 使用 `Gson.fromJson` 将标签字符串反序列化为 `Set`,然后在内存中判断用户是否包含所有目标标签。 - **优点**:逻辑简单,适合小规模数据,避免了在数据库中拼接复杂的 SQL 查询。 - **缺点**:对于大规模用户数据,性能较差,因为需要查询所有用户并在内存中进行过滤,数据量大时可能导致内存消耗和响应时间过长。 ##### 2. **过滤条件** - **第一种方法**: - 通过 `like` 操作符进行标签匹配,查询条件是基于 SQL 语法的模糊匹配(`LIKE`)。 - 通过多个 `LIKE` 条件拼接实现类似 "包含所有标签" 的功能。此方式的 `LIKE` 查询是一个或条件,只要用户的标签字段中包含任意一个目标标签,就会被匹配。 - **缺点**:这种方法仅能处理简单的标签包含匹配,不能像第二种方法那样精确地匹配所有标签。 - **第二种方法**: - 通过内存中的反序列化来处理标签字段,逐个检查每个用户的标签是否包含目标标签列表中的所有标签。 - 只要用户的标签包含所有目标标签才算符合条件。 - 该方法支持更复杂的标签匹配逻辑(如标签存储为 JSON 字符串),可以处理更灵活的标签格式。 ##### 3. **性能与扩展性** - **第一种方法**:通过数据库层面的 SQL 查询,可以充分利用数据库的索引等优化措施,性能更好,适合数据量较大时使用。尤其是对标签字段添加合适的索引,性能将有显著提升。 但它也有局限性,复杂的标签结构可能难以通过 SQL 查询完成复杂的匹配,尤其是在标签存储格式较为复杂时。 - **第二种方法**:由于所有的数据都需要被查询到并加载到内存中,处理大量数据时性能较差。内存消耗大,适合数据量较小或标签匹配逻辑较复杂的情况。 ##### 4. **标签存储格式** - **第一种方法**:假设标签存储为简单的文本字段,可以直接使用 `LIKE` 查询。但这种方法不适合复杂的结构(如 JSON 格式的标签)。 - **第二种方法**:假设标签存储为 JSON 格式的字符串(例如 `["java", "python"]`),需要将其反序列化为 `Set` 类型,这样可以处理更复杂的标签结构。 ##### 5. **灵活性** - **第一种方法**:更适合简单的文本字段搜索,查询操作较为简单,但难以处理复杂的标签结构或高精度的标签匹配(例如需要严格匹配所有标签)。 **第二种方法**:更灵活,支持复杂的数据结构(如 JSON 格式的标签),以及对每个标签做更加精细的处理(例如标签的顺序、数量等)。 ##### 适用场景总结 - **第一种方法**:适合数据量较大且标签存储结构较为简单的场景,尤其是当标签是简单文本字段时,使用数据库进行查询将显著提高性能。 **第二种方法**:适合数据量较小、标签格式较为复杂或者需要精确匹配标签列表的场景。例如,如果标签是以 JSON 字符串形式存储的,或者标签需要进行更复杂的匹配(如包含多个标签的组合),使用这种方法更为灵活。 ##### 最终选择 - 如果数据量大并且标签存储格式简单(如纯文本标签),第一种方法会提供更好的性能。 - 如果数据量较小或标签结构复杂,第二种方法会更合适。 #### 鱼和熊掌都要 考虑结合两种方法 比如当数据量达到多少时选择哪种 或者哪个先返回结果就用哪个返回给客户端 - 如果参数可以分析,根据用户的参数去选择查询方式,比如标签数 - 如果参数不可分析,并且数据库连接足够、内存空间足够,可以并发同时查询,谁先返回用谁。 - 还可以 SQL 查询与内存计算相结合,比如先用 SQL 过滤掉部分 tag 在代码中引入一个动态选择策略,根据数据量、标签格式、查询速度等因素来决定使用哪种方法。这样不仅能够兼顾性能,还能确保灵活性。 1. **根据数据量选择查询方式** - **小数据量:** 当数据量较小时,完全可以使用内存过滤方法,因为查询时间短,内存消耗相对较低。这时可以优先使用内存过滤的方法,减少数据库查询的负担。 - **大数据量:** 当数据量很大时,查询所有用户并在内存中进行过滤将变得非常低效,应该优先使用数据库查询方式,借助数据库的索引等优化手段,快速筛选符合条件的用户。 2. **标签存储格式和复杂度** - **简单标签格式(如纯文本):** 如果标签是纯文本或简单的字符串,可以直接使用 `LIKE` 查询来进行过滤,减少复杂的内存计算,提升性能。 - **复杂标签格式(如 JSON 字符串):** 如果标签是 JSON 格式或嵌套结构,使用数据库查询方法可能无法精确匹配。这时需要先查询所有用户,再在内存中进行复杂的标签解析和匹配。 3. **混合使用策略:优先返回最快的结果** - 如果数据量较小,可以先使用内存过滤,快速得到结果。 - 如果数据量大且标签较为简单,可以优先使用数据库查询。 - 如果两种方法都能执行,采用最先返回结果的方式,即返回查询最先完成的结果。 ##### 根据数据量选择 ```java @Override public List searchUserByTags(List tagNameList) { if (CollectionUtils.isEmpty(tagNameList)) { throw new BusinessException(ErrorCode.PARAMS_ERROR); } // 首先查询数据库,获取所有用户数量 QueryWrapper queryWrapper = new QueryWrapper<>(); int userCount = userMapper.selectCount(queryWrapper); // 获取用户总数 // 如果用户数量较少,优先使用内存过滤方法 if (userCount <= 1000) { // 假设1000为一个阈值,具体根据实际情况调整 return searchUserByTagsInMemory(tagNameList); // 使用内存过滤 } // 否则,使用数据库查询方法 return searchUserByTagsInDatabase(tagNameList); // 使用SQL查询 } /** * 基于内存过滤的方式 */ private List searchUserByTagsInMemory(List tagNameList) { // 查出所有用户 List userList = userMapper.selectList(new QueryWrapper<>()); Gson gson = new Gson(); // 在内存中判断是否包含要求的标签 return userList.stream() .filter(user -> { String tagsStr = user.getTags(); if (StringUtils.isBlank(tagsStr)) { return false; } Set tempTagNameSet = gson.fromJson(tagsStr, new TypeToken>() {}.getType()); // 判断用户的标签集合是否包含所有目标标签 return tagNameList.stream().allMatch(tempTagNameSet::contains); }) .map(this::getSafetyUser) .collect(Collectors.toList()); } /** * 基于SQL查询的方式 */ private List searchUserByTagsInDatabase(List tagNameList) { QueryWrapper queryWrapper = new QueryWrapper<>(); // 拼接 AND 查询条件 for (String tagName : tagNameList) { queryWrapper = queryWrapper.like("tags", tagName); } List userList = userMapper.selectList(queryWrapper); return userList.stream() .map(this::getSafetyUser) .collect(Collectors.toList()); } ``` ##### 并发返回最快结果 - **创建两个线程:** 一个线程执行内存过滤方法,另一个线程执行数据库查询方法。 - **使用**`CompletableFuture`**或线程池来并行执行:** 这允许我们异步启动两个查询,并在其中一个查询完成时立即返回结果。 - **最快返回:** 通过使用 `CompletableFuture` 的 `anyOf()` 方法,哪个查询先完成就返回哪个结果。 ```java @Override public List searchUserByTags(List tagNameList) { if (CollectionUtils.isEmpty(tagNameList)) { throw new BusinessException(ErrorCode.PARAMS_ERROR); } // 使用CompletableFuture并发执行查询 CompletableFuture> dbQueryFuture = CompletableFuture.supplyAsync(() -> searchUserByTagsInDatabase(tagNameList)); CompletableFuture> memoryQueryFuture = CompletableFuture.supplyAsync(() -> searchUserByTagsInMemory(tagNameList)); // 使用anyOf选择最快返回的查询结果 CompletableFuture anyOf = CompletableFuture.anyOf(dbQueryFuture, memoryQueryFuture); try { // 获取最快返回的结果 return (List) anyOf.get(); // 这里会返回最快完成的Future } catch (InterruptedException | ExecutionException e) { throw new RuntimeException("查询执行失败", e); } } /** * 基于内存过滤的方式 */ private List searchUserByTagsInMemory(List tagNameList) { // 查出所有用户 List userList = userMapper.selectList(new QueryWrapper<>()); Gson gson = new Gson(); // 在内存中判断是否包含要求的标签 return userList.stream() .filter(user -> { String tagsStr = user.getTags(); if (StringUtils.isBlank(tagsStr)) { return false; } Set tempTagNameSet = gson.fromJson(tagsStr, new TypeToken>() {}.getType()); // 判断用户的标签集合是否包含所有目标标签 return tagNameList.stream().allMatch(tempTagNameSet::contains); }) .map(this::getSafetyUser) .collect(Collectors.toList()); } /** * 基于SQL查询的方式 */ private List searchUserByTagsInDatabase(List tagNameList) { QueryWrapper queryWrapper = new QueryWrapper<>(); // 拼接 AND 查询条件 for (String tagName : tagNameList) { queryWrapper = queryWrapper.like("tags", tagName); } List userList = userMapper.selectList(queryWrapper); return userList.stream() .map(this::getSafetyUser) .collect(Collectors.toList()); } ``` ## 前端 ### 整合路由 [https://router.vuejs.org/zh/guide/#html](https://router.vuejs.org/zh/guide/#html%EF%BC%8C%E7%9B%B4%E6%8E%A5%E7%9C%8B%E5%AE%98%E6%96%B9%E6%96%87%E6%A1%A3%E5%BC%95%E5%85%A5%E3%80%82) ```bash npm install vue-router@4 ``` main.ts ```python import * as VueRouter from 'vue-router'; import routes from "./config/route"; onst router = VueRouter.createRouter({ // 内部提供了 history 模式的实现。为了简单起见,我们在这里使用 hash 模式。 history: VueRouter.createWebHistory(), routes, // `routes: routes` 的缩写 }) app.use(router); ``` config.route.ts ```python import Index from "../pages/Index.vue"; import Team from "../pages/TeamPage.vue"; const routes = [ { path: '/', component: Index }, { path: '/team', title: '找队伍', component: Team }, …… ] export default routes; ``` BasicLayout ```html ``` --- **项目分区导航**:⬅️ [[11-推荐用户|11-推荐用户]] | 12-搜索标签serviceimpl | ➡️ [[13-用户标签接口|13-用户标签接口]]