公司文档地址:https://www.kdocs.cn/l/cav0b2rdk42n?from=docs&startTime=1763618428738&createDirect=true&newFile=true 飞书地址:https://xp0y6kmku6.feishu.cn/wiki/ToYTwpjkViqPkZkcOz7cTZopnyc
SPU-SKU索引方案选型
- SKU为单位 + Collapse + inner_hits
- 优点:
- sku评分精准 (SKU 级别评分。如果sku影响相关性较多)
- 可扩展性:如果以后需要返回sku级别结果,索引改动小。
- 索引更新灵活,单个sku字段变化只会引起单个sku的更新。
- 缺点:
- inner_hits性能很差
- spu级别的查询字段冗余,冗余倍数跟spu下面平均sku数量线性相关
- 方案介绍
- 查询性能: a. 主查询执行(召回结果)(比如5000sku) → collapse折叠(按spu_id分组、每组选top文档,比如得到1000sku) → 排序、按spu排序截取分页内结果(比如100) → 执行inner_hits (获取 spu内sku,相当于执行 page_size 次子查询) b. collapse阶段的耗时影响因素: i. 召回数量 、 基数(Cardinality)(去重后的spu数量,高基数字段collapse性能差)。 c. Inter hits(主要开销来源): i. 取决于分页size。(相当于page_size次子查询,"max_concurrent_group_searches"限制并发组查询) ii. 我们在宜采的实践,分页size=100的情况下,Inter hits开销非常大(Inter hits size = 4,只取id 不取任何sku字段、按原始打分排序) d. Inner hits 为什么性能差? i. 不对分桶内结果进行一次排序就行了吗?一个for循环的事情,为什么要用max_concurrent_group_searches线程去并发? ii. Collapse 阶段只保留每个分组的最佳文档,其他文档被丢弃。 iii. Collapse 阶段:每个分片独立执行collapse、最后协调节点做全局collapse。 iv. Inner_hits 阶段:每个spu下的sku可能在多个分片上,因此需要跨分片查询。 v. ES为什么要在collapse阶段丢弃其他文档: 1. 我的理解是,ES可能认为多数时候不需要inner_hits。Lazy Loading。对需要inner_hits的场景很不友好。 e. 即使collapse阶段只保留了最佳文档,为什么inner_hits阶段 为什么做page_size 100次查询、而不是做一次查询:spu_id in [1, 2, 3, ... 100个 ] AND (原始查询条件) 。 我们宜采的搜索就是这么做的。 阶段1:主查询 + Collapse ├─ 每个分片执行查询 ├─ 每个分片执行 collapse(只保留最高分文档) ├─ 协调节点合并分片结果 └─ 输出:1000 个 SPU(每个只有 1 个 SKU)
阶段2:Inner_hits(只对需要返回的分组执行) ├─ 输入:100 个 SPU(size=100) ├─ 对每个 SPU 执行查询: │ ├─ 构建查询:spu_id=123 AND (原始查询条件) │ ├─ 发送到所有分片 │ ├─ 等待分片响应 │ ├─ 合并分片结果 │ └─ 排序和截取(size=3) └─ 输出:100 个 SPU,每个有 3 个 SKU,按要求排序和填充2. 索引: a. 缺点:spu级别的查询字段冗余。比如1个spu下面有2*5个款式,那么 spu_title brielf description seo_keywords cate tags vendor 这些 spu 字段 将被重建10次索引。 b. 优点:索引更新灵活,单个sku字段变化只会引起单个sku的更新。
- 参考资料
- Collapse search results - collapse 在每个分片上独立执行,每个分片为每个折叠键选择代表文档,协调节点merge最佳文档
- Retrieve inner hits 每个折叠结果会触发额外的查询。
- 适用场景: sku级别的字段对打分影响较大,搜索需要重点关注 sku级别的title、属性的匹配情况。 需要关注spu内部sku排序。
- 跟我们的适配情况:
- 我们的 款式 多数情况并不影响搜索结果。 a. 相关性影响最重的 基本都在spu :spu_title brielf description seo_keywords cate tags vendor b. 款式:需要参与(甚至可以不参与)排序,但是对相关性影响很小
- 款式 a. 笛卡尔积,variants多,冗余情况较严重
- inner_hits的includes字段:我们可以只要ID,也就是只返回spu级别字段。 a. sku级别字段点击详情页再触发。 b. 如果需要sku级别字段,也可以将其作为spu的属性字段进行返回,而不用从inner_hits的includes获取。
- 我们的 inner_hits 可能需要更多的source字段
- SKU为单位 + Collapse 获得spu级别排序(不执行inner_hits) 没有inner_hits的性能问题,但是仍然有 spu级别检索字段 冗余的问题。
- spu为单位。SKU字段展开作为SPU属性
索引方案 sku的title作为spu 的sku_titles 属性。 款式字段同样展开,作为 sku 的 list 属性。 除了title, brielf description seo相关 cate tags vendor所有影响相关性的字段都在spu。 sku只有一个title。所以,可以以spu为单位,sku的title作为spu的一个字段,以list形式灌入,假设一个spu有三个sku,那么这个sku_titles字段有三个值,打分的时候按max取得打分。
写入 spu 级别索引
def build_product_document(product, variants): return { "product_id": str(product.id), "title": product.title,
# Variant搜索字段(展开) "variant_titles": [v.title for v in variants], "variant_attributes": self._extract_all_attributes(variants), # Variant详细信息(用于返回) "variants": [ { "id": str(v.id), "title": v.title, "price": float(v.price), "options": v.options } for v in variants ], # titles参与相关性计算 "titles": list(set([v.title for v in variants if v.title])), # 属性字段(扁平化,用于过滤) "option1": list(set([v.option1 for v in variants if v.option1])), "sizes": list(set([v.option2 for v in variants if v.option2])), "min_price": min(v.price for v in variants), "max_price": max(v.price for v in variants)}2. 查询方案 对数组字段使用 dis_max,只取最高分,避免累加。
问题 不好得到每个子title的得分以决定sku的排序。方案:
打开explain,会影响性能,不推荐。(只需要记下来sku_titles 中每一项的打分,但是打开explain会记录太多的东西)
修改ES,把 sku_titles 数组中 每个title的匹配得分记下来。
对返回的结果的子sku根据title和属性的匹配情况再做一次匹配,以决定sku的排序。
sku 作为nested 如果需要查询nested字段,会需要nested join,本质是是父子文档的join。 性能影响因子:- 子文档数量:线性增长,每个SKU增加约 5-15ms 查询时间- 嵌套深度:指数增长,多级嵌套性能急剧下降- 查询复杂度:nested bool查询比普通查询慢 3-10倍 原因:
内部 Join 操作:ES 为每个 nested 对象创建隐藏文档,查询时需要 join
文档膨胀:1个SPU文档实际存储为 1+N 个文档(N=SKU数量)
内存开销:nested 查询需要加载所有匹配的父子文档到内存 适用场景: • 查询模式简单(主要是过滤,少用复杂评分)
Spu sku 独立索引 spu-catalog-:用于品牌、类目、商品介绍等宏观搜索。包括列表页搜索。 sku-catalog-:用于具体规格搜索、下单。涉及到价格、库存筛选的查询。
两者各有各的字段: SPU索引: ├─ 数据:品牌、类目、描述、SEO信息 ├─ 更新频率:低(几天一次) ├─ 查询场景:列表页、品牌搜索、类目浏览 └─ 优化方向:文本搜索、相关性排序
SKU索引: ├─ 数据:规格、价格、库存 ├─ 更新频率:高(实时更新) ├─ 查询场景:详情页、规格筛选、价格排序 └─ 优化方向:精确匹配、数值范围查询 联合查询:使用 multi_search 或应用层合并
这种场景的效果 可以等同于 sku 作为nested方案,如果 sku 的nested字段,只有数值型的用于查询,其他的的只用于返回填充。
店匠商家SPU-SKU数据情况调研 商品(product)的多款式(variants)配置 3种属性/款式,是笛卡尔积的形式生成variants(sku) 服装领域的多款式 服装领域,variants(sku)基本上都是 颜色、size。 size 基本上每个商品都有: XS S M L XL 3XL,有的还有4XL,5XL 颜色: blue/orange/purple/green... 款式:dress1 dress2 shirt1 shirt2 swimsuit1 set1 set2...
当前的多数独立站多款式是否参与搜索 看起来搜索的索引粒度是spu单位的。 query加上sku相关信息,并不会让sku在spu内更靠前
下面的几个案例,都是我用 spu名称 + 其中一款的款式名称 进行搜索,看召回的spu 是否会将我指定的款式前置。 可以看到不管我是否加款式限定,返回结果的每个spu下的子sku顺序都不会受影响。
SPU-SKU索引建议方案 spu为单位。SKU字段展开作为SPU属性 其他重点字段
- Sku title
- category
- 数据源
- Mysql 在spu表中: Field Type category varchar(255) category_id bigint(20) category_google_id bigint(20) category_level int(11) category_path varchar(500)
- ES索引
- 输入数据 设计 1,2,3级分类 三个字段,的 category (原始文本)
- 索引方法 设计要求:
- 支持facet(精确过滤、keyword聚合),并且性能需要足够高。
- 支持普通搜索模糊匹配(用户原始query可能包括分类词)。
- 模糊匹配要考虑多语言 方案:采用方案2
- categoryPath索引 + Prefix 查询(categoryPath.keyword: "服装/男装")(如果满足条件的key太多的则性能较差,比如 查询的是一级类目,类目树叶子节点太多时性能较差)
- categoryPath支撑模糊查询 和 多级cate keyword索引支撑精确查询。 索引阶段冗余,查询性能高。
"category_path_zh": { // 提供模糊查询功能,辅助相关性计算
"type": "text",
"analyzer": "hanlp_index",
"search_analyzer": "hanlp_standard"
},
"category_path_en": { // 提供模糊查询功能,辅助相关性计算
"type": "text",
"analyzer": "english",
"search_analyzer": "english"
},
"category_path": { // 用于多层级的筛选、精确匹配
"type": "keyword",
"normalizer": "lowercase"
}, "category_id": { "type": "keyword" }, "category_name": { "type": "keyword" }, "category_level": { "type": "integer" }, "category1_name": { // 不同层级下 可能有同名的情况,因此提供一二三级分开的查询方式 "type": "keyword" }, "category2_name": { "type": "keyword" }, "category3_name": { "type": "keyword" }, tags
数据源 多值 标签
最多输入250个标签,每个不得超过500字符,多个标签请用「英文逗号」隔开
新品,热卖,爆款
耳机,头戴式,爆款
分割后 list形式灌入
- Mysql
- ES索引
- 输入数据
- 索引方法
- 供应商
- 数据源
- Mysql
- ES索引
- 输入数据
- 索引方法
- 款式/选项值(options)
- 数据源 以下区域字段,商品属性为M(商品主体)的行需填写款式名称,商品属性为P(子款式)的行需填写款式值信息,商品属性为S(单一款式商品)的行无需填写
款式1 款式2 款式3
最多255字符 最多255字符 最多255字符
SIZE COLOR
S red
S black
S army
L red
L black
L army
XL red
XL black
XL army
- Mysql
- API 在 SPU 的维度直接返回3个属性定义,存储在 shoplazza_product_option 中:
- API在 SKU的维度直接返回3个属性值,存储在 shoplazza_product_sku 表的 option 相关的字段中:
- ES索引
- 输入数据
- 索引方法 1)铺平展开,只支持三个,支持查询。缺点是,查询逻辑跟租户的属性维度绑定,不灵活 attr1 attr2 attr3 option1 option2 option3 2)nested,支持超过3个属性(动态)。支持查询 "specifications": { "type": "nested", "properties": { "name": { "type": "keyword" }, // "颜色", "容量" "value": { "type": "keyword" } // "白色", "256GB" } }, 3)nested,支持超过3个属性(动态)。只用作返回,不能查询。节省索引空间 "specifications": { "type": "nested", "properties": { "name": { "type": "keyword","index": false }, "value": { "type": "keyword","index": false } } },
TODO 考虑使用方案(3)。
- SEO相关字段
- 数据源 SEO标题 SEO描述 SEO URL Handle SEO URL 重定向 SEO关键词
最多5000字符 最多5000字符 最多支持输入255字符 (SEO URL handle只对SEO URL的「URL参数」部分进行更改,即“products/”后的内容,如:products/「URL参数」 ) 创建URL重定向,访问修改前链接可跳转到修改后的新链接页面 「Y」:TRUE 「N」:FALSE 多个关键词请用「英文逗号」隔开
- Mysql
- ES索引
- 输入数据
- 索引方法
- 供应商
- 数据源 供应商名称 供应商URL
最多20字符 请输入供应商URL
Mysql
ES索引
输入数据
索引方法
其他
数据源
Mysql
ES索引
输入数据
索引方法 索引整体配置 常用查询域(Query Domains)
default(默认索引): 搜索所有文本字段 a. 包含字段:title, sku_titles(TODO), brief, description, seo_title(TODO待确认), seo_keywords, vendor, category_path, tags, category (带具体的语言后缀)
title(标题索引): 仅搜索标题相关字段 a. 包含字段:title, sku_titles, seo_title (带具体的语言后缀)
vendor(品牌索引): 仅搜索品牌字段 a. 包含字段:vendor (带具体的语言后缀)
category_path(类目索引): 仅搜索类目字段 a. 包含字段:category_path(带具体的语言后缀)
tags(标签索引): 搜索标签和SEO关键词 a. 包含字段:tags, seo_keywords 提权字段 function_score 具体提权字段、权重待定(配置化) "function_score": { "functions": [ { "field_value_factor": { "field": "sales_count", "factor": 0.001, "modifier": "log1p" } }, { "gauss": { "listed_at": { "scale": "30d" } } } ], "boost_mode": "multiply" }facet 分类 tags