搜索标签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());
}
代码解释:
-
空值检查:
if(CollectionUtils.isEmpty(tagNameList)){throw new BusinessException(ErrorCode.PARAMS_ERROR);}检查
tagNameList是否为空,如果为空则抛出一个业务异常 (BusinessException),并返回错误码PARAMS_ERROR。 -
构建查询条件:
QueryWrapper<User> queryWrapper = new QueryWrapper<>();创建一个
QueryWrapper对象,用于构建 SQL 查询条件。 -
拼接多个
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标签的用户。 -
执行查询:
List<User> userList = userMapper.selectList(queryWrapper);根据构建的
queryWrapper查询数据库,获取符合条件的所有用户列表。 -
返回处理后的用户列表:
return userList.stream().map(this::getSafetyUser).collect(Collectors.toList());对查询到的用户列表进行处理(通过
getSafetyUser方法),去除敏感信息。最后将处理后的用户列表返回。
2、内存查询(灵活)
java json 序列化库有很多:
- gson(google 的,推荐)
- fastjson alibaba(阿里出品,快,但是漏洞太多)
- jackson
- 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>集合并返回。
对比
1. 查询方式
- 第一种方法:通过 SQL 查询直接在数据库中进行过滤。
- 使用
QueryWrapper的like方法来拼接多个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
在代码中引入一个动态选择策略,根据数据量、标签格式、查询速度等因素来决定使用哪种方法。这样不仅能够兼顾性能,还能确保灵活性。
- 根据数据量选择查询方式
- 小数据量: 当数据量较小时,完全可以使用内存过滤方法,因为查询时间短,内存消耗相对较低。这时可以优先使用内存过滤的方法,减少数据库查询的负担。
- 大数据量: 当数据量很大时,查询所有用户并在内存中进行过滤将变得非常低效,应该优先使用数据库查询方式,借助数据库的索引等优化手段,快速筛选符合条件的用户。
- 标签存储格式和复杂度
- 简单标签格式(如纯文本): 如果标签是纯文本或简单的字符串,可以直接使用
LIKE查询来进行过滤,减少复杂的内存计算,提升性能。 - 复杂标签格式(如 JSON 字符串): 如果标签是 JSON 格式或嵌套结构,使用数据库查询方法可能无法精确匹配。这时需要先查询所有用户,再在内存中进行复杂的标签解析和匹配。
- 混合使用策略:优先返回最快的结果
- 如果数据量较小,可以先使用内存过滤,快速得到结果。
- 如果数据量大且标签较为简单,可以优先使用数据库查询。
- 如果两种方法都能执行,采用最先返回结果的方式,即返回查询最先完成的结果。
根据数据量选择
@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或线程池来并行执行: 这允许我们异步启动两个查询,并在其中一个查询完成时立即返回结果。 - 最快返回: 通过使用
CompletableFuture的anyOf()方法,哪个查询先完成就返回哪个结果。
@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;
}
💬 评论