搜索标签serviceimpl

搜索标签

要实现什么:

    允许用户传入多个标签,多个标签都存在才搜索出来 (and)

    允许用户传入多个标签,有任何一个存在就搜索出来 (or)

怎么实现?

1、sql查询(实现简单)

/**
     * 根据标签搜索用户
     *
     * @param tagNameList 用户要拥有的标签
     * @return
     */
    @Override
    public List<User> searchUserByTags(List<String> tagNameList){
        if(CollectionUtils.isEmpty(tagNameList)){
            throw new BusinessException(ErrorCode.PARAMS_ERROR);
        }
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        //拼接and查询
        //like '%java%' and like '%python%'
        for(String tagName : tagNameList){
            queryWrapper = queryWrapper.like("tags",tagName);
        }
        List<User> 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<User> 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%'

    这种查询方式会返回同时包含 javapython 标签的用户。

  4. 执行查询

    List<User> userList = userMapper.selectList(queryWrapper);

    根据构建的 queryWrapper 查询数据库,获取符合条件的所有用户列表。

  5. 返回处理后的用户列表

    return userList.stream().map(this::getSafetyUser).collect(Collectors.toList());

    对查询到的用户列表进行处理(通过 getSafetyUser 方法),去除敏感信息。

    最后将处理后的用户列表返回。

image-a4974768 image-517b04de

2、内存查询(灵活)

java json 序列化库有很多:

  1. gson(google 的,推荐)
  2. fastjson alibaba(阿里出品,快,但是漏洞太多)
  3. jackson
  4. kryo(性能极高的序列化库)

这里用gson

添加依赖

    <!-- https://mvnrepository.com/artifact/com.google.code.gson/gson -->
        <dependency>
            <groupId>com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
            <version>2.8.9</version>
        </dependency>
  /**
     * 根据标签搜索用户
     *
     * @param tagNameList 用户要拥有的标签
     * @return
     */
    @Override
    public List<User> searchUserByTags(List<String> tagNameList) {
        if (CollectionUtils.isEmpty(tagNameList)) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR);
        }

        //查出所有用户
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        List<User> userList = userMapper.selectList(queryWrapper);
        Gson gson = new Gson();
        //在内存中判断是否包含要求的标签
        return userList.stream().filter(user -> {
            String tagsStr = user.getTags();
            if(StringUtils.isBlank(tagsStr)){
                return false;
            }
            Set<String> tempTagNameSet = gson.fromJson(tagsStr, new TypeToken<Set<String>>() {
            }.getType());
            for (String tagName : tagNameList) {
                if(!tempTagNameSet.contains(tagName)){
                    return false;
                }
            }
            return true;
        }).map(this::getSafetyUser).collect(Collectors.toList());
    }

代码解释:

查询所有用户

QueryWrapper<User> queryWrapper = new QueryWrapper<>();

List<User> userList = userMapper.selectList(queryWrapper);

  • 使用 QueryWrapper<User> 构建查询条件对象,这里没有任何具体条件,所以它会查询出数据库中所有的用户。
  • 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<String> tempTagNameSet = gson.fromJson(tagsStr, new TypeToken<Set<String>>() {}.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<String>

  • Set<String> tempTagNameSet = gson.fromJson(tagsStr, new TypeToken<Set<String>>() {}.getType());:使用 Gson 将 tagsStr 字符串反序列化为 Set<String> 对象。假设 tagsStr 是一个 JSON 数组(如 ["tag1", "tag2"]),该操作将其转换为一个 Set<String> 集合,包含所有标签。

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<User> 集合并返回。
image-e01f2890

对比

1. 查询方式
  • 第一种方法:通过 SQL 查询直接在数据库中进行过滤。
    • 使用 QueryWrapperlike 方法来拼接多个 LIKE 条件,查询符合标签条件的用户。
    • 通过 tagNameList 中的每个标签拼接 like "tags" '%tag%' 查询条件,数据库会返回符合这些条件的用户列表。
    • 优点:所有的查询操作在数据库中进行,数据库通常比内存处理更高效,尤其是数据量大的时候。
    • 缺点:对于多个 LIKE 查询条件拼接的性能可能较差,尤其是在没有索引的字段上。如果标签数据存储格式比较复杂,可能会遇到效率瓶颈。
  • 第二种方法:查询所有用户后,在内存中对用户的标签进行过滤。
    • 通过 userMapper.selectList(queryWrapper) 获取所有用户数据,然后使用 Java 流(Stream)对每个用户的标签字符串进行处理。
    • 使用 Gson.fromJson 将标签字符串反序列化为 Set<String>,然后在内存中判断用户是否包含所有目标标签。
    • 优点:逻辑简单,适合小规模数据,避免了在数据库中拼接复杂的 SQL 查询。
    • 缺点:对于大规模用户数据,性能较差,因为需要查询所有用户并在内存中进行过滤,数据量大时可能导致内存消耗和响应时间过长。
2. 过滤条件
  • 第一种方法
    • 通过 like 操作符进行标签匹配,查询条件是基于 SQL 语法的模糊匹配(LIKE)。
    • 通过多个 LIKE 条件拼接实现类似 "包含所有标签" 的功能。此方式的 LIKE 查询是一个或条件,只要用户的标签字段中包含任意一个目标标签,就会被匹配。
    • 缺点:这种方法仅能处理简单的标签包含匹配,不能像第二种方法那样精确地匹配所有标签。
  • 第二种方法
    • 通过内存中的反序列化来处理标签字段,逐个检查每个用户的标签是否包含目标标签列表中的所有标签。
    • 只要用户的标签包含所有目标标签才算符合条件。
    • 该方法支持更复杂的标签匹配逻辑(如标签存储为 JSON 字符串),可以处理更灵活的标签格式。
3. 性能与扩展性
  • 第一种方法:通过数据库层面的 SQL 查询,可以充分利用数据库的索引等优化措施,性能更好,适合数据量较大时使用。尤其是对标签字段添加合适的索引,性能将有显著提升。

    但它也有局限性,复杂的标签结构可能难以通过 SQL 查询完成复杂的匹配,尤其是在标签存储格式较为复杂时。

  • 第二种方法:由于所有的数据都需要被查询到并加载到内存中,处理大量数据时性能较差。内存消耗大,适合数据量较小或标签匹配逻辑较复杂的情况。

4. 标签存储格式
  • 第一种方法:假设标签存储为简单的文本字段,可以直接使用 LIKE 查询。但这种方法不适合复杂的结构(如 JSON 格式的标签)。
  • 第二种方法:假设标签存储为 JSON 格式的字符串(例如 ["java", "python"]),需要将其反序列化为 Set<String> 类型,这样可以处理更复杂的标签结构。
5. 灵活性
  • 第一种方法:更适合简单的文本字段搜索,查询操作较为简单,但难以处理复杂的标签结构或高精度的标签匹配(例如需要严格匹配所有标签)。

    第二种方法:更灵活,支持复杂的数据结构(如 JSON 格式的标签),以及对每个标签做更加精细的处理(例如标签的顺序、数量等)。

适用场景总结
  • 第一种方法:适合数据量较大且标签存储结构较为简单的场景,尤其是当标签是简单文本字段时,使用数据库进行查询将显著提高性能。

    第二种方法:适合数据量较小、标签格式较为复杂或者需要精确匹配标签列表的场景。例如,如果标签是以 JSON 字符串形式存储的,或者标签需要进行更复杂的匹配(如包含多个标签的组合),使用这种方法更为灵活。

最终选择
  • 如果数据量大并且标签存储格式简单(如纯文本标签),第一种方法会提供更好的性能。
  • 如果数据量较小或标签结构复杂,第二种方法会更合适。

鱼和熊掌都要

考虑结合两种方法 比如当数据量达到多少时选择哪种 或者哪个先返回结果就用哪个返回给客户端

  • 如果参数可以分析,根据用户的参数去选择查询方式,比如标签数
  • 如果参数不可分析,并且数据库连接足够、内存空间足够,可以并发同时查询,谁先返回用谁。
  • 还可以 SQL 查询与内存计算相结合,比如先用 SQL 过滤掉部分 tag

在代码中引入一个动态选择策略,根据数据量、标签格式、查询速度等因素来决定使用哪种方法。这样不仅能够兼顾性能,还能确保灵活性。

  1. 根据数据量选择查询方式
  • 小数据量: 当数据量较小时,完全可以使用内存过滤方法,因为查询时间短,内存消耗相对较低。这时可以优先使用内存过滤的方法,减少数据库查询的负担。
  • 大数据量: 当数据量很大时,查询所有用户并在内存中进行过滤将变得非常低效,应该优先使用数据库查询方式,借助数据库的索引等优化手段,快速筛选符合条件的用户。
  1. 标签存储格式和复杂度
  • 简单标签格式(如纯文本): 如果标签是纯文本或简单的字符串,可以直接使用 LIKE 查询来进行过滤,减少复杂的内存计算,提升性能。
  • 复杂标签格式(如 JSON 字符串): 如果标签是 JSON 格式或嵌套结构,使用数据库查询方法可能无法精确匹配。这时需要先查询所有用户,再在内存中进行复杂的标签解析和匹配。
  1. 混合使用策略:优先返回最快的结果
  • 如果数据量较小,可以先使用内存过滤,快速得到结果。
  • 如果数据量大且标签较为简单,可以优先使用数据库查询。
  • 如果两种方法都能执行,采用最先返回结果的方式,即返回查询最先完成的结果。
根据数据量选择
@Override

public List<User> searchUserByTags(List<String> tagNameList) {

    if (CollectionUtils.isEmpty(tagNameList)) {

        throw new BusinessException(ErrorCode.PARAMS_ERROR);

    }

    // 首先查询数据库,获取所有用户数量

    QueryWrapper<User> queryWrapper = new QueryWrapper<>();

    int userCount = userMapper.selectCount(queryWrapper);  // 获取用户总数

    // 如果用户数量较少,优先使用内存过滤方法

    if (userCount <= 1000) {  // 假设1000为一个阈值,具体根据实际情况调整

        return searchUserByTagsInMemory(tagNameList);  // 使用内存过滤

    }

    // 否则,使用数据库查询方法

    return searchUserByTagsInDatabase(tagNameList);  // 使用SQL查询

}

/**

 * 基于内存过滤的方式

 */

private List<User> searchUserByTagsInMemory(List<String> tagNameList) {

    // 查出所有用户

    List<User> 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<String> tempTagNameSet = gson.fromJson(tagsStr, new TypeToken<Set<String>>() {}.getType());

                // 判断用户的标签集合是否包含所有目标标签

                return tagNameList.stream().allMatch(tempTagNameSet::contains);

            })

            .map(this::getSafetyUser)

            .collect(Collectors.toList());

}

/**

 * 基于SQL查询的方式

 */

private List<User> searchUserByTagsInDatabase(List<String> tagNameList) {

    QueryWrapper<User> queryWrapper = new QueryWrapper<>();

    // 拼接 AND 查询条件

    for (String tagName : tagNameList) {

        queryWrapper = queryWrapper.like("tags", tagName);

    }

    List<User> userList = userMapper.selectList(queryWrapper);

    return userList.stream()

            .map(this::getSafetyUser)

            .collect(Collectors.toList());

}
并发返回最快结果
  • 创建两个线程: 一个线程执行内存过滤方法,另一个线程执行数据库查询方法。
  • 使用CompletableFuture或线程池来并行执行: 这允许我们异步启动两个查询,并在其中一个查询完成时立即返回结果。
  • 最快返回: 通过使用 CompletableFutureanyOf() 方法,哪个查询先完成就返回哪个结果。
@Override
public List<User> searchUserByTags(List<String> tagNameList) {
    if (CollectionUtils.isEmpty(tagNameList)) {
        throw new BusinessException(ErrorCode.PARAMS_ERROR);
    }

    // 使用CompletableFuture并发执行查询
    CompletableFuture<List<User>> dbQueryFuture = CompletableFuture.supplyAsync(() -> searchUserByTagsInDatabase(tagNameList));
    CompletableFuture<List<User>> memoryQueryFuture = CompletableFuture.supplyAsync(() -> searchUserByTagsInMemory(tagNameList));

    // 使用anyOf选择最快返回的查询结果
    CompletableFuture anyOf = CompletableFuture.anyOf(dbQueryFuture, memoryQueryFuture);

    try {
        // 获取最快返回的结果
        return (List<User>) anyOf.get();  // 这里会返回最快完成的Future
    } catch (InterruptedException | ExecutionException e) {
        throw new RuntimeException("查询执行失败", e);
    }
}

/**
 * 基于内存过滤的方式
 */
private List<User> searchUserByTagsInMemory(List<String> tagNameList) {
    // 查出所有用户
    List<User> 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<String> tempTagNameSet = gson.fromJson(tagsStr, new TypeToken<Set<String>>() {}.getType());
                // 判断用户的标签集合是否包含所有目标标签
                return tagNameList.stream().allMatch(tempTagNameSet::contains);
            })
            .map(this::getSafetyUser)
            .collect(Collectors.toList());
}

/**
 * 基于SQL查询的方式
 */
private List<User> searchUserByTagsInDatabase(List<String> tagNameList) {
    QueryWrapper<User> queryWrapper = new QueryWrapper<>();

    // 拼接 AND 查询条件
    for (String tagName : tagNameList) {
        queryWrapper = queryWrapper.like("tags", tagName);
    }

    List<User> userList = userMapper.selectList(queryWrapper);
    return userList.stream()
            .map(this::getSafetyUser)
            .collect(Collectors.toList());
}

前端

整合路由

https://router.vuejs.org/zh/guide/#html

npm install vue-router@4

main.ts

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

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

<template>

  <van-nav-bar

      :title="title"

      left-arrow

      @click-left="onClickLeft"

      @click-right="onClickRight"

  >

    <template #right>

      <van-icon name="search" size="18"/>

    </template>

  </van-nav-bar>

  <div id="content">

    <router-view/>

  </div>

  <van-tabbar route @change="onChange">

    <van-tabbar-item to="/" icon="home-o" name="index">主页</van-tabbar-item>

    <van-tabbar-item to="/team" icon="search" name="team">队伍</van-tabbar-item>

    <van-tabbar-item to="/user" icon="friends-o" name="user">个人</van-tabbar-item>

  </van-tabbar>

</template>



import { useRouter } from "vue-router";

import {ref} from "vue";

import routes from "../config/route";

const router = useRouter();

const DEFAULT_TITLE = '伙伴匹配';

const title = ref(DEFAULT_TITLE);

/**

 * 根据路由切换标题

 */

router.beforeEach((to, from) => {

  const toPath = to.path;

  const route = routes.find((route) => {

    return toPath == route.path;

  })

  title.value = route?.title ?? DEFAULT_TITLE;

})

const => {

  router.back();

};

const => {

  router.push('/search')

};





#content {

  padding-bottom: 50px;

}



项目分区导航:⬅️ 11-推荐用户 | 12-搜索标签serviceimpl | ➡️ 13-用户标签接口