前言
这个搜索方案小猿也花了比较多时间去优化和完善,同时觉得比较有意思,所以放在这里记录一下,同时也给有相关需要的朋友提供一些思路。
说明
当前项目中并未使用传统的ES搜索,考虑到ES对机器配置比较高,同时相对比较重。当前业务场景数据量并不算高,暂时的实现机制,使用redis + 数据库索引 + MP二级缓存,商品在20w左右可以支撑。
1. 该算法主要基于相似搜索,并基于相似度得分进行排序,最终结果进行高亮显示。
2. 实现商品聚合
3. 支持多维度排序,并可动态排序
4. 基于内存分页
搜索设计
1. 匹配维度
1. 商品名 : 模糊匹配 + 精准匹配 + word分词 + mysql 正则匹配, 如果输入精准商品名,匹配唯一结果
2. sku : 模糊匹配 + 精准匹配, 如果输入精准sku,匹配唯一结果
3. 关键字: 模糊匹配 + word分词 + mysql 正则匹配, 主要为了提高精准度,影响得分生成的权重
4. 分类(去掉,与算法无关)
5. 区域(去掉,与算法无关)
2. 使用算法和工具
1. 相似算法 : 相似余弦(不理想)、aerfa(不理想) 、 自定义
2. 分词 : nlp(英文不友好,并依赖于词库), ik(英文不友好,并依赖于词库) ,word
实现技术点
1. 商品聚合算法
场景:一般商品设计的时候民,同一个商品,都会有很多维度的分类,但其实在数据库中,他们都是基于唯一的sku,只不过sku会根据一定的维度生成,其实商品名
也类似,我们在商城上看到的名字,其实是由很多部分组合而 成的一个唯一名称(可以去参考京东); 我们在搜索商品的时候,每个商品只会显示一个sku,并且如果有
关键字,则返回符合条件一个sku返回。可能会有人说,这个设置父子表,随机取一条,其实大型电商里面,一般都只有sku的子表
这里简单举一例:比如iphone12手机,
型号:有min版本,max版本, pro版本
颜色:黑色、银色、玫瑰金、白色
内存:64G, 256G、128G
模式: 联通、电信、移动、全网通
虽然在界面上显示只有一个商品,其实在存储上,是有很多商品的,4(型号) * 4( 颜色) * 内存(3) * 模式(4) ,最多可能会有132种商品,大型的电商里面,这个算法会非常复杂
这里分分享一个比较简单的算法,实现 分组中随机取一条或者取指定条的算法,基于mysql实现
#sql逻辑说明: 1. 先查出所有商品的同一个sku,使用group_concat基于material_head_id分组,并组合成 逗号分割 sku 字符串,按上架时间排序(直接使用max时间取得最新上架商品),这里默认需要找到每个商品中最新上架的sku,
# 如果有指定条件的话,需要找到符合条件的最新上架sku 2. 将1按分组排好序的sku字符串切割,获得第一个sku 3,通过sku关联该sku对应的详细信息即可
SELECT pf.material_id, pf.goods_id, pf.material_head_id, pf.on_shelf_time FROM put_shelf pf,( SELECT tf.material_head_id, max( tf.on_shelf_time ) AS on_shelf_time, substring_index(group_concat( tf.id ),',', 1) on_shelf_id FROM ( SELECT f.* FROM put_shelf f, material mt WHERE mt.id = f.material_id AND f.on_shelf_flag = '1' ORDER BY f.on_shelf_time DESC ) tf GROUP BY tf.material_head_id
2. 分词实现与集成
2.1 引入依赖
<dependency> <groupId>org.apdplat</groupId> <artifactId>word</artifactId> <version>1.3</version> <exclusions> <exclusion> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> </exclusion> <exclusion> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> </exclusion> </exclusions> </dependency>
2.2 初始化词库
@Component public class WordInitConfig { static { WordSegmenter.segWithStopWords("初始化分词"); } }
2.3 使用分词,并拼接成正则格式
/** * 基于word分词 * @param keyword 搜索关键字 "不锈钢水杯"
* @return "不锈钢|水杯|不锈钢水杯" */ private String handleKeywordWord(String keyword) { //新建分词器 List<Word> words2 = WordSegmenter.segWithStopWords(keyword); List<String> nameStr = Utils.map(words2, t->t.getText()); nameStr.add(keyword); log.info("基本分词:"+ StringUtils.join(nameStr,"|")); return StringUtils.join(nameStr,"|"); }
2.4 sql 查询, 使用正则(这里补充一个知识点,很多人说正则效率很低(正则效率问题:其实是争对要匹配的原字符,如果字符是一篇文章,你需要匹配一个特定模式,那么效率确实很慢), 当然正常时候sql不推荐使用正则
# regName格式为 “不锈钢|水杯|不锈钢水杯”
select a.* from test t where t.label_name REGEXP #{regName}
3. 相似算法
说明 :通过上一步搜索,其实只是找到了与我们关键字匹配的结果,但是这些结果排序是很乱的,有些匹配到的结果跟预期相关性很小,有些可能就完全没关系;而另外一些,可能由于商品名称设置不合理,本应该出现的,又没匹配到,可能会给用户带来很不好的体验,这里做一些优化。
示例说明:比如用户在商城搜索“不锈钢水杯”, 由于之前的分词+模糊+正则匹配,可能会匹配到 诸如“不锈钢衣架”,“钢丝球”, “酒杯”, “水桶”,这些结果呢,由于商品池比较丰富,原始的结果可能会有100个,而我们要的水杯被排在了好几页之后;又或者商家把商品取名成了“保温壶”,这样的话,传统的做法,压根就没有把这个结果匹配出来。当然也有人说,这个“保温壶”搜索不到,本来就不是系统问题。但是细想一下,如果你是用户,你是不是每次都能输入一个准确的关键词去查找呢,或者说我们是不是可做一些小的优化,让系统更加智能,同时让用户有更好的体验。
算法讲解: 这里增加关键字,一个商品可以有很多关键字,这些关键字其实就是标签,商家设置的一些更宽泛的商品分类,或者快速匹配商品的方式,并且可以根据用户的习惯设置一些用户常用的关键字,这样再加上之前的标题搜索,通过两个维度,同时设置权重,让结果更加准确,并将结果量化,生成得分,通过一个综合的得分结果排序,那这样的一个结果,应该是更能满足用户需求的。
先上算法如下:
/** * 基于关键字加权实现 * @param keyword 关键字 * @param productName 搜索内容 * @param regex 分词,以|分割 * @param label 标签,以,分割 * @return */ public static Double getSimilarity(String keyword, String productName, String regex, String label) { //设置分词权重表 Map<String, Integer> rm = Maps.newHashMap(); String[] arr = regex.split("\|"); for(String a : arr){ if(!rm.containsKey(a)){ int len = a.length(); rm.put(a, len * 10); } } double score = 0; for(String a : arr){ int index = productName.lastIndexOf(a); if(index > -1){ score += rm.get(a); } } if(label == null || label == ""){ return score; } //设置标签权重表,加分项,并设置惩罚措施 Map<String, Integer> labelRm = Maps.newHashMap(); String[] labelArr = label.split(","); for(String a : labelArr){ if(!labelRm.containsKey(a)){ labelRm.put(a, 20); } } double labelScore = 0; for(String a : labelArr){ int index = keyword.lastIndexOf(a); if(index > -1){ labelScore += labelRm.get(a); }else{ labelScore += -5; } } return score + labelScore; }
算法讲解:
1. 设置商品名权重表 我们为关键词分词后的分词表设置权重,一个字10分,n个字 n*10,
2. 计算商品名匹配计分,每匹配到一个字,则加10分,如果匹配到了多个关键词,则分数累加
3. 设置标签名权重表,一个字20分,因为这里的词匹配到,就说明其实商家更希望你输入关键字表里的信息去查找到商品;
4. 计算标签匹配计分, 这里标签会有很多,以逗号分割的字符串存储,如果匹配到一个关键字,则加20分,2个字则40分;
这里有一些不一样,这里设置奖惩机制,如果匹配到,则给一个大一点的奖励,如果匹配不到,则要减分,每一个减少10
分,这个可以根据具体去调整; 同时,如果出现负分,则将结果舍弃掉,这个也很好理解,如果商家设置20个关键词,你一个
都没给输对,那说明这个商品很可能不是用户要的,用户其实是不知道这个商品在系统上叫什么,商家肯定知道自己的商品叫什么