电商商品搜索设计与算法分享——自定义实现

前言

      这个搜索方案小猿也花了比较多时间去优化和完善,同时觉得比较有意思,所以放在这里记录一下,同时也给有相关需要的朋友提供一些思路。

说明

      当前项目中并未使用传统的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个关键词,你一个

                都没给输对,那说明这个商品很可能不是用户要的,用户其实是不知道这个商品在系统上叫什么,商家肯定知道自己的商品叫什么

原文地址:https://www.cnblogs.com/liuyq/p/14941510.html