【商城实战(60)】解锁搜索排序与相关性优化密码(java版)
【商城实战】专栏重磅来袭!这是一份专为开发者与电商从业者打造的超详细指南。从项目基础搭建,运用 uniapp、Element Plus、SpringBoot 搭建商城框架,到用户、商品、订单等核心模块开发,再到性能优化、安全加固、多端适配,乃至运营推广策略,102 章内容层层递进。无论是想深入钻研技术细节,还是探寻商城运营之道,本专栏都能提供从 0 到 1 的系统讲解,助力你打造独具竞争力的电商平台,开启电商实战之旅。
目录
- 一、设计搜索结果排序算法
- 1.1 综合因素考量
- 1.2 前端实现(uniapp 与 Element plus)
- 1.3 后端实现(SpringBoot 与 Mybatis-plus )
- 二、优化搜索相关性
- 2.1 BM25 算法原理
- 2.2 前后端集成实现
- 三、基于用户搜索行为动态调整参数
- 3.1 行为数据收集与存储
- 3.2 参数动态调整策略
- 3.3 实时更新效果展示
一、设计搜索结果排序算法
1.1 综合因素考量
在电商商城中,搜索结果的排序直接影响用户体验和购买转化率。综合考虑商品销量、价格、评分等因素,能够为用户提供更符合其需求的商品排序。高销量商品通常代表着受欢迎程度,价格因素能满足不同预算用户的需求,评分则反映了商品的质量和用户满意度。通过合理设计排序算法,将这些因素有机结合,可以让用户更快速地找到心仪的商品。例如,对于追求性价比的用户,价格和评分的权重可以适当提高;对于追求热门商品的用户,销量的权重可以加大。
1.2 前端实现(uniapp 与 Element plus)
在 uniapp(移动前端)中,通过调用 API 接收后端返回的排序后商品数据。在页面的script部分,定义数据接收函数,如:
export default {
data() {
return {
sortedGoodsList: []
};
},
methods: {
receiveSortedGoods(data) {
this.sortedGoodsList = data;
}
}
};
在template部分,使用v-for指令遍历sortedGoodsList,展示商品信息,如:
<template>
<view>
<view v-for="(item, index) in sortedGoodsList" :key="index">
<text>{{ item.goodsName }}</text>
<text>价格: {{ item.price }}</text>
<text>评分: {{ item.rating }}</text>
</view>
</view>
</template>
同时,提供排序方式切换的交互界面,如通过picker组件让用户选择按销量、价格或评分排序。
在 Element plus(PC 前端)中,同样在script部分定义数据接收和排序切换方法,如:
import { ref } from 'vue';
export default {
setup() {
const sortedGoodsList = ref([]);
const sortType = ref('sales');
const receiveSortedGoods = (data) => {
sortedGoodsList.value = data;
};
const changeSortType = (type) => {
sortType.value = type;
// 发送请求,重新获取对应排序的数据
};
return {
sortedGoodsList,
sortType,
receiveSortedGoods,
changeSortType
};
}
};
在template部分,使用el-table展示商品列表,并添加排序切换按钮,如:
<template>
<el-table :data="sortedGoodsList">
<el-table-column prop="goodsName" label="商品名称"></el-table-column>
<el-table-column prop="price" label="价格"></el-table-column>
<el-table-column prop="rating" label="评分"></el-table-column>
</el-table>
<el-button-group>
<el-button @click="changeSortType('sales')">按销量排序</el-button>
<el-button @click="changeSortType('price')">按价格排序</el-button>
<el-button @click="changeSortType('rating')">按评分排序</el-button>
</el-button-group>
</template>
1.3 后端实现(SpringBoot 与 Mybatis-plus )
SpringBoot 作为后端框架,接收前端发送的搜索请求和排序参数。在 Controller 层,定义请求处理方法,如:
@RestController
@RequestMapping("/search")
public class SearchController {
@Autowired
private GoodsService goodsService;
@GetMapping("/sorted")
public List<Goods> getSortedGoods(@RequestParam String keyword, @RequestParam String sortType) {
return goodsService.getSortedGoods(keyword, sortType);
}
}
在 Service 层,GoodsService实现具体的业务逻辑,调用 Mybatis-plus 从数据库获取数据并进行排序。例如,当按销量排序时:
@Service
public class GoodsServiceImpl implements GoodsService {
@Autowired
private GoodsMapper goodsMapper;
@Override
public List<Goods> getSortedGoods(String keyword, String sortType) {
QueryWrapper<Goods> queryWrapper = new QueryWrapper<>();
queryWrapper.like("goods_name", keyword);
if ("sales".equals(sortType)) {
queryWrapper.orderByDesc("sales_volume");
} else if ("price".equals(sortType)) {
queryWrapper.orderByAsc("price");
} else if ("rating".equals(sortType)) {
queryWrapper.orderByDesc("rating");
}
return goodsMapper.selectList(queryWrapper);
}
}
Mybatis-plus 通过GoodsMapper接口与数据库交互,执行 SQL 查询语句,获取符合条件并排序后的商品数据。
二、优化搜索相关性
2.1 BM25 算法原理
BM25(Best Matching 25)是一种用于估算文档与查询相关性的排名函数,在信息检索领域应用广泛。其核心原理是综合考虑词频(TF)、逆文档频率(IDF)以及文档长度等因素来计算相关性得分。
对于一个查询 Q 和文档 D,BM25 算法计算相关性得分的公式如下:
其中:
- n是查询 Q 中的词项数。
- q_i是查询 Q 中的第i个词项。
- IDF(q_i)是逆文档频率,计算方式通常是
,N是文档总数,n(q_i)是包含词项q_i的文档数 。这一计算方式使得稀有词的 IDF 值较高,在相关性计算中具有更大的权重。
- f(q_i,D)是词项q_i在文档 D 中的出现频率。
- k_1和b是可调节的参数。k_1主要用于调节词频对得分的影响,防止词频过高对得分产生过大影响,通常取值在 1.2 - 2.0 之间,默认值为 1.2;b是文档长度归一化参数,用于控制文档长度对得分的影响,取值范围一般在 0 - 1 之间,默认值为 0.75 。
- len(D)是文档 D 的长度,avg_len是所有文档的平均长度。通过文档长度归一化,避免长文档在搜索结果中总是占据优势。
例如,当用户搜索 “苹果手机” 时,BM25 算法会分别计算 “苹果” 和 “手机” 这两个词项在各个商品文档中的相关性得分,然后累加得到每个商品文档与查询的总相关性得分,根据得分对商品进行排序,得分越高表示相关性越强。
2.2 前后端集成实现
在 SpringBoot 后端集成 BM25 算法,首先需要对商品数据进行预处理,构建文档集合。将商品的名称、描述等文本信息作为文档内容,分词后形成词项集合。例如,使用 HanLP 等中文分词工具对商品数据进行分词处理:
import com.hankcs.hanlp.HanLP;
import com.hankcs.hanlp.seg.common.Term;
import java.util.List;
public class GoodsPreprocessor {
public static List<Term> tokenize(String text) {
return HanLP.segment(text);
}
}
然后,根据 BM25 算法公式实现计算相关性得分的逻辑。可以封装一个BM25Calculator类:
import java.util.List;
public class BM25Calculator {
private final double k1 = 1.2;
private final double b = 0.75;
private final int N;
private final double[] idf;
private final double avgdl;
public BM25Calculator(List<List<String>> corpus) {
N = corpus.size();
// 计算IDF等相关参数
// ...
}
public double calculate(List<String> query, List<String> document) {
double score = 0;
// 根据BM25公式计算得分
// ...
return score;
}
}
在搜索接口中,接收前端传来的查询关键词,调用BM25Calculator计算每个商品与查询的相关性得分,然后根据得分对商品进行排序,并返回排序后的结果给前端。
在 uniapp 前端,通过fetch或axios等工具发送搜索请求,接收后端返回的基于 BM25 算法排序后的商品数据,并展示在页面上。例如:
import axios from 'axios';
export default {
data() {
return {
relevantGoodsList: []
};
},
methods: {
async searchGoods(keyword) {
try {
const response = await axios.get('/search/bm25', {
params: { keyword }
});
this.relevantGoodsList = response.data;
} catch (error) {
console.error('搜索请求失败', error);
}
}
}
};
在 Element plus 前端,同样通过 HTTP 请求获取数据,并在template中展示商品列表:
<template>
<el-table :data="relevantGoodsList">
<el-table-column prop="goodsName" label="商品名称"></el-table-column>
<el-table-column prop="description" label="商品描述"></el-table-column>
</el-table>
</template>
import { ref } from 'vue';
import axios from 'axios';
export default {
setup() {
const relevantGoodsList = ref([]);
const searchGoods = async (keyword) => {
try {
const response = await axios.get('/search/bm25', {
params: { keyword }
});
relevantGoodsList.value = response.data;
} catch (error) {
console.error('搜索请求失败', error);
}
};
return {
relevantGoodsList,
searchGoods
};
}
};
通过以上前后端集成,实现了基于 BM25 算法的搜索相关性优化,为用户提供更精准的搜索结果。
三、基于用户搜索行为动态调整参数
3.1 行为数据收集与存储
在 SpringBoot 后端,通过切面编程(AOP)或过滤器等技术收集用户搜索行为数据。例如,使用 AOP 定义一个切面类SearchBehaviorAspect:
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.Date;
@Aspect
@Component
public class SearchBehaviorAspect {
@Autowired
private SearchBehaviorMapper searchBehaviorMapper;
@Around("execution(* com.example.demo.controller.SearchController.search(..))")
public Object collectSearchBehavior(ProceedingJoinPoint joinPoint) throws Throwable {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
HttpServletRequest request = attributes.getRequest();
String keyword = request.getParameter("keyword");
String userId = request.getHeader("userId");// 假设通过header传递userId
SearchBehavior searchBehavior = new SearchBehavior();
searchBehavior.setUserId(userId);
searchBehavior.setSearchKeyword(keyword);
searchBehavior.setSearchTime(new Date());
// 记录其他行为数据,如搜索结果点击情况等
searchBehaviorMapper.insert(searchBehavior);
return joinPoint.proceed();
}
}
SearchBehaviorMapper使用 Mybatis-plus 将数据插入数据库:
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.demo.entity.SearchBehavior;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface SearchBehaviorMapper extends BaseMapper<SearchBehavior> {
}
数据库表结构设计如下:
CREATE TABLE search_behavior (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id VARCHAR(50),
search_keyword VARCHAR(255),
search_time TIMESTAMP,
-- 其他字段
);
3.2 参数动态调整策略
在后端服务中,定时任务或事件驱动机制触发对用户搜索行为数据的分析。例如,使用 Spring 的@Scheduled注解定时执行数据分析任务:
import com.example.demo.entity.SearchBehavior;
import com.example.demo.mapper.SearchBehaviorMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Component
public class SearchBehaviorAnalyzer {
@Autowired
private SearchBehaviorMapper searchBehaviorMapper;
@Scheduled(cron = "0 0 2 * *?")// 每天凌晨2点执行
public void analyzeSearchBehavior() {
List<SearchBehavior> searchBehaviors = searchBehaviorMapper.selectList(null);
Map<String, Long> keywordCountMap = searchBehaviors.stream()
.collect(Collectors.groupingBy(SearchBehavior::getSearchKeyword, Collectors.counting()));
// 根据关键词搜索次数调整排序与相关性参数
for (Map.Entry<String, Long> entry : keywordCountMap.entrySet()) {
String keyword = entry.getKey();
long count = entry.getValue();
if (count > 1000) {
// 例如,提高该关键词搜索结果中销量因素的权重
}
}
// 分析其他行为数据,如搜索结果点击情况,调整参数
}
}
通过分析用户搜索行为数据,如热门搜索关键词、搜索结果点击次数、搜索结果购买转化率等,动态调整排序与相关性参数。对于经常被搜索且购买转化率高的关键词,提高相关商品在搜索结果中的排序权重;对于用户频繁点击但未购买的商品,降低其相关性得分,以优化搜索结果排序。
3.3 实时更新效果展示
在 uniapp 前端,通过 WebSocket 或轮询机制获取参数调整后的搜索结果更新。例如,使用uni-socket.io实现 WebSocket 连接:
import io from 'uni-socket.io';
export default {
data() {
return {
socket: null
};
},
mounted() {
this.socket = io('http://localhost:3000');// 假设后端WebSocket服务地址
this.socket.on('searchResultUpdate', (data) => {
this.sortedGoodsList = data;
});
},
beforeDestroy() {
this.socket.disconnect();
}
};
在 Element plus 前端,同样使用 WebSocket 实现实时更新:
<template>
<el-table :data="sortedGoodsList">
<el-table-column prop="goodsName" label="商品名称"></el-table-column>
<el-table-column prop="price" label="价格"></el-table-column>
<el-table-column prop="rating" label="评分"></el-table-column>
</el-table>
</template>
import { ref } from 'vue';
import io from'socket.io-client';
export default {
setup() {
const sortedGoodsList = ref([]);
const socket = io('http://localhost:3000');
socket.on('searchResultUpdate', (data) => {
sortedGoodsList.value = data;
});
return {
sortedGoodsList
};
}
};
当后端参数调整后,通过 WebSocket 推送更新后的搜索结果数据给前端,前端实时展示优化后的搜索结果,让用户能够及时体验到更精准、符合其行为习惯的搜索服务。