

公司文档地址：https://www.kdocs.cn/l/cav0b2rdk42n?from=docs&startTime=1763618428738&createDirect=true&newFile=true
飞书地址：https://xp0y6kmku6.feishu.cn/wiki/ToYTwpjkViqPkZkcOz7cTZopnyc

SPU-SKU索引方案选型
1. SKU为单位 + Collapse + inner_hits
1. 优点：
1. sku评分精准 （SKU 级别评分。如果sku影响相关性较多）
2. 可扩展性：如果以后需要返回sku级别结果，索引改动小。
3. 索引更新灵活，单个sku字段变化只会引起单个sku的更新。
2. 缺点：
1. inner_hits性能很差
2. spu级别的查询字段冗余，冗余倍数跟spu下面平均sku数量线性相关
3. 方案介绍
1. 查询性能：
    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的更新。
4. 参考资料
1. [Collapse search results](https://www.elastic.co/guide/en/elasticsearch/reference/current/collapse-search-results.html)  - collapse 在每个分片上独立执行，每个分片为每个折叠键选择代表文档，协调节点merge最佳文档
2. [Retrieve inner hits](https://www.elastic.co/guide/en/elasticsearch/reference/current/inner-hits.html) 每个折叠结果会触发额外的查询。
5. 适用场景：
sku级别的字段对打分影响较大，搜索需要重点关注 sku级别的title、属性的匹配情况。
需要关注spu内部sku排序。
6. 跟我们的适配情况：
1. 我们的 款式 多数情况并不影响搜索结果。
    a. 相关性影响最重的 基本都在spu ：spu_title  brielf description seo_keywords  cate tags vendor 
    b. 款式：需要参与（甚至可以不参与）排序，但是对相关性影响很小
2. 款式
    a. 笛卡尔积，variants多，冗余情况较严重
3. inner_hits的includes字段：我们可以只要ID，也就是只返回spu级别字段。
    a. sku级别字段点击详情页再触发。
    b. 如果需要sku级别字段，也可以将其作为spu的属性字段进行返回，而不用从inner_hits的includes获取。
4. 我们的 inner_hits 可能需要更多的source字段
2. SKU为单位 + Collapse 获得spu级别排序（不执行inner_hits）
没有inner_hits的性能问题，但是仍然有 spu级别检索字段 冗余的问题。
3. spu为单位。SKU字段展开作为SPU属性
1. 索引方案
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，只取最高分，避免累加。
3. 问题
不好得到每个子title的得分以决定sku的排序。方案：
1. 打开explain，会影响性能，不推荐。（只需要记下来sku_titles 中每一项的打分，但是打开explain会记录太多的东西）
2. 修改ES，把 sku_titles 数组中 每个title的匹配得分记下来。
3. 对返回的结果的子sku根据title和属性的匹配情况再做一次匹配，以决定sku的排序。
4. sku 作为nested
如果需要查询nested字段，会需要nested join，本质是是父子文档的join。
性能影响因子：- 子文档数量：线性增长，每个SKU增加约 5-15ms 查询时间- 嵌套深度：指数增长，多级嵌套性能急剧下降- 查询复杂度：nested bool查询比普通查询慢 3-10倍
原因：
1. 内部 Join 操作：ES 为每个 nested 对象创建隐藏文档，查询时需要 join
2. 文档膨胀：1个SPU文档实际存储为 1+N 个文档（N=SKU数量）
3. 内存开销：nested 查询需要加载所有匹配的父子文档到内存
适用场景：
• 查询模式简单（主要是过滤，少用复杂评分）
5. 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属性
其他重点字段
1. Sku title
2. category
1. 数据源
2. Mysql
在spu表中：
Field Type
category  varchar(255)
category_id bigint(20)
category_google_id  bigint(20)
category_level  int(11)
category_path varchar(500)
3. ES索引
1.   输入数据
  设计 1,2,3级分类 三个字段，的 category  （原始文本）
2.   索引方法
  设计要求：
1. 支持facet（精确过滤、keyword聚合），并且性能需要足够高。
2. 支持普通搜索模糊匹配（用户原始query可能包括分类词）。
3. 模糊匹配要考虑多语言
方案：采用方案2
4. categoryPath索引 + Prefix 查询（categoryPath.keyword: "服装/男装"）（如果满足条件的key太多的则性能较差，比如 查询的是一级类目，类目树叶子节点太多时性能较差）
5. 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"
      },
3. tags

1. 数据源
多值
标签

最多输入250个标签，每个不得超过500字符，多个标签请用「英文逗号」隔开

新品,热卖,爆款

耳机,头戴式,爆款

分割后 list形式灌入
2. Mysql
3. ES索引
1.   输入数据
2.   索引方法
4. 供应商
1. 数据源
2. Mysql
3. ES索引
1.   输入数据
2.   索引方法
5. 款式/选项值（options）
1. 数据源
以下区域字段，商品属性为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


2. Mysql
1. API 在 SPU 的维度直接返回3个属性定义，存储在 shoplazza_product_option 中：
2. API在 SKU的维度直接返回3个属性值，存储在 shoplazza_product_sku 表的 option 相关的字段中：
3. ES索引
1.   输入数据
2.   索引方法
  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）。
6. SEO相关字段
1. 数据源
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
多个关键词请用「英文逗号」隔开


2. Mysql
3. ES索引
1.   输入数据
2.   索引方法
7. 供应商
1. 数据源
供应商名称
供应商URL

最多20字符
请输入供应商URL

Amazon
https://www.amazon.com/Legendary-Whitetails-Buck-Flannels-Large/dp/B01KTUMBOI/ref=sr_1_1?s=fashion-mens-intl-ship&ie=UTF8&qid=1543038722&sr=1-1

2. Mysql

3. ES索引
1.   输入数据
2.   索引方法
8. 其他
1. 数据源
2. Mysql
3. ES索引
1.   输入数据
2.   索引方法
索引整体配置
常用查询域（Query Domains）
1. default（默认索引）: 搜索所有文本字段
    a. 包含字段：title, sku_titles（TODO）, brief, description, seo_title(TODO待确认）, seo_keywords, vendor, category_path, tags, category （带具体的语言后缀）
2. title（标题索引）: 仅搜索标题相关字段
    a. 包含字段：title, sku_titles, seo_title  （带具体的语言后缀）
3. vendor（品牌索引）: 仅搜索品牌字段
    a. 包含字段：vendor  （带具体的语言后缀）
4. category_path（类目索引）: 仅搜索类目字段
    a. 包含字段：category_path（带具体的语言后缀）
5. 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

