系列随笔:
(总览)基于商品属性的相似商品推荐算法
(一)基于商品属性的相似商品推荐算法——整体框架及处理流程
(二)基于商品属性的相似商品推荐算法——Flink SQL实时计算实现商品的隐式评分
(三)基于商品属性的相似商品推荐算法——批量处理商品属性,得到属性前缀及完整属性字符串
(四)基于商品属性的相似商品推荐算法——推荐与评分高的商品属性相似的商品
(五)基于商品属性的相似商品推荐算法——算法调优及其他
批量处理商品属性,得到属性前缀及完整属性字符串
一、查询全部商品($lastCode默认0,$limit默认0)
$sql = "SELECT goods_code FROM sj_goods WHERE goods_code>{$lastCode} ORDER BY goods_code ASC" . ($limit>0? " LIMIT $limit":"");
注1:如果商品数量较多,建议配合 $lastCode 和 $limit 做分批处理
注2:查询结果不要直接转数组,用游标操作效率会好一点
二、每100个商品批量查询处理商品属性
$list = $this->db['seller']->getAll($sql); $goodsCodes = []; $count = 0; while ($row = $list->fetch(PDO::FETCH_ASSOC)) { $count++; $goodsCodes[] = $row['goods_code']; if ($count%100 == 0) { // 每100个商品查询一次属性 $properties = $this->genGoodsProperties($goodsCodes); // 属性入库 $this->insertGoodsProperties($properties) or die($goodsCodes[0].' 异常!!'); $goodsCodes = []; } } // 别忘了不够100个商品的情况 if (!empty($goodsCodes)) { $properties = $this->genGoodsProperties($goodsCodes); $this->insertGoodsProperties($properties) or die($goodsCodes[0].' 异常!!'); }
三、具体的属性查询及处理(上面的 genGoodsProperties 方法)
1)批量查询商品的属性code及属性value
$sql = "SELECT t1.goods_code,t1.brand_code,t4.property_name,t3.property_code,t3.property_value FROM sj_goods_product t1 LEFT JOIN sj_product t2 ON t2.product_code = t1.product_code LEFT JOIN sj_style_properties t3 ON t3.style_code = t2.style_code LEFT JOIN sp_product_property t4 ON t4.property_code = t3.property_code WHERE t1.goods_code in ('". implode("','", $goodsCodes) ."')";
注1:主要就是要把商品对应的所有属性值都查询出来,根据自己的项目数据库设计来操作就好
注2:为了提高商品的相关性,这里我还查询了 brand_code,这个稍候再说怎么用
2)确定要用哪些属性组合来做前缀,以及完整的属性组合
// 属性前缀为:适用人群-佩戴场合-机芯类型-价格区间-表盘形状-表盘直径,后面的属性排列顺序可以随意 $needs = [31=>'适用人群', 40=>'佩戴场合', 1=>'机芯类型', 39=>'价格区间', 9=>'表盘形状', 11=>'表盘直径', 13=>'表盘刻度', 17=>'表带材质', 14=>'表盘颜色', 25=>'防水', 3=>'外壳材质', 38=>'表盘宽度', 3=>'表盘厚度', 12=>'镜面材质', 16=>'表壳底盖', 19=>'表带颜色'];
注1:数组的键为属性的code,值为属性的名称
注2:为何这样选以及怎么优化,后面会在 (五)基于商品属性的相似商品推荐算法——算法调优及其他 里做说明。简单来说,属性前缀一样的商品,就是有40%-50%相似的
3)循环处理商品,利用 1) 和 2) 的数据读取商品属性值,拼接属性组字符串
private function genGoodsProperties($goodsCodes) { // 属性前缀为:适用人群-佩戴场合-机芯类型-价格区间-表盘形状-表盘直径,后面的属性排列顺序可以随意 $needs = [31=>'适用人群', 40=>'佩戴场合', 1=>'机芯类型', 39=>'价格区间', 9=>'表盘形状', 11=>'表盘直径', 13=>'表盘刻度', 17=>'表带材质', 14=>'表盘颜色', 25=>'防水', 3=>'外壳材质', 38=>'表盘宽度', 3=>'表盘厚度', 12=>'镜面材质', 16=>'表壳底盖', 19=>'表带颜色']; $properties = $this->getGoodsProperty($goodsCodes); $return = []; foreach ($goodsCodes as $goodsCode) { $prefix = ''; $full = ''; foreach ($needs as $key => $value) { $brandCode = 0; // 这里的 $properties 是第1)步得到的,已转换成了map if (isset($properties[$goodsCode][$key]) && !empty($properties[$goodsCode][$key])) { // 如果商品有此属性就拼接属性值 $full .= $properties[$goodsCode][$key]['property_value'].'|'; $brandCode = $properties[$goodsCode][$key]['brand_code']; } else { // 如果商品没有这个属性,或属性值为空,那拼接一个 0 $full .= '0|'; } // 临界点(拿到属性前缀) if ($value == '表盘直径') { $prefix = $full; // 加入品牌信息 $full .= $brandCode.'|'; } } // 截取掉最后多余的 | $prefix = substr($prefix, 0, -1); $full = substr($full, 0, -1); // 拆分多选属性 $prefix = $this->splitProperties($prefix); $full = $this->splitProperties($full); $return[$goodsCode] = [$prefix, $full]; } return $return; }
四、属性半成品示例
例如:有两个商品的属性是这样的(点击放大查看)
经过第三步的处理,它的属性组值是这样的:
商品A:
$prefix = '96|297,300|195,41|64|46|218'; $full = '96|297,300|195,41|64|46|218|87|220,222|51,242|58|185|198,200|41mm|12mm|276|257';
商品B:
$prefix = '96|298,297,299|195|64|46|218'; $full = '96|298,297,299|195|64|46|218|87|220,222|242|58|185|200|41mm|L619/888|276|257';
注:逗号隔开的属性值表示多选属性,例如商品A的佩戴场合为"运动、商务休闲"(297,300),商品B的佩戴场合为"商务休闲、时尚、正装"(298,297,299)
通过观察属性组,可以看出,其实两个商品是相似的(大部分属性位的值相同);但因为多选属性的影响,在使用属性前缀查询相似商品的时候,不太好办,就是有那么一丢丢不相同(上面示例红色字体部分)
五、拆分多选属性
首先,我们先要想一下自己想得到什么,然后再想办法实现。
例如,我想像中商品A的属性前缀的拆分情况是这样的:
观察上图可得到:
第一步,原属性位 “297,300”,被替换成了“297”和“300”,其他属性位不变,得到两个新属性组字符串;
第二步也一样,两个新属性组字符串的属性位“195,41”,被替换成“195”和“41”,其他属性位不变;
很明显,这就是一个很简单的字符串替换过程,关键是怎么维护和保存原字符串
实现代码如下:
private function splitProperties($propertyPrefixs) { // 没有多选属性,直接返回 if (strpos($propertyPrefixs, ',') === false) { return [$propertyPrefixs]; } // 原属性组(也是结果) $origin = [$propertyPrefixs]; $return = ''; // 拆分属性位 $arr1 = explode('|', $propertyPrefixs); // 循环属性位 foreach ($arr1 as $k => $v) { // 多选属性位 if (strpos($v, ',') !== false) { $new = []; // 拆分多选属性 $arr2 = explode(',', $v); foreach ($arr2 as $kk => $vv) { // 循环原属性组,替换原属性位 foreach ($origin as $kkk => $vvv) { $prefixs = str_replace($v, $vv, $vvv); // 替换结果 $new[] = $prefixs; } } // 更新的原属性组 $origin = $new; } } return $origin; }
六、最终结果展示
商品A:
商品B:
上一节:Flink SQL实时计算实现商品的隐式评分
下一节:(四)基于商品属性的相似商品推荐算法——推荐与评分高的商品属性相似的商品