From 4503d8bfb503652d7e57d83390c8789508ac1bc7 Mon Sep 17 00:00:00 2001 From: tangwang Date: Thu, 20 Nov 2025 14:39:14 +0800 Subject: [PATCH] 更新文档《索引方案.md》 --- config/query_rewrite.dict | 5 ++--- docs/image-dress1.png | Bin 0 -> 127788 bytes docs/image-dress2.png | Bin 0 -> 27975 bytes docs/image-shoes1.png | Bin 0 -> 87242 bytes docs/索引spu-sku层级结构设计.md | 388 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ docs/索引字段说明.md | 50 +++++++++++++++----------------------------------- 6 files changed, 405 insertions(+), 38 deletions(-) create mode 100644 docs/image-dress1.png create mode 100644 docs/image-dress2.png create mode 100644 docs/image-shoes1.png create mode 100644 docs/索引spu-sku层级结构设计.md diff --git a/config/query_rewrite.dict b/config/query_rewrite.dict index 7fb87fe..321979f 100644 --- a/config/query_rewrite.dict +++ b/config/query_rewrite.dict @@ -1,4 +1,3 @@ -芭比 vendor.keyword:芭比 OR title:芭比娃娃 -玩具 category.keyword:玩具 OR title:玩具 -消防 category.keyword:消防 OR title:消防 +玩具 category.keyword:玩具 OR default:玩具 +消防 category.keyword:消防 OR default:消防 diff --git a/docs/image-dress1.png b/docs/image-dress1.png new file mode 100644 index 0000000..7b786de Binary files /dev/null and b/docs/image-dress1.png differ diff --git a/docs/image-dress2.png b/docs/image-dress2.png new file mode 100644 index 0000000..deaf810 Binary files /dev/null and b/docs/image-dress2.png differ diff --git a/docs/image-shoes1.png b/docs/image-shoes1.png new file mode 100644 index 0000000..c2b67ef Binary files /dev/null and b/docs/image-shoes1.png differ diff --git a/docs/索引spu-sku层级结构设计.md b/docs/索引spu-sku层级结构设计.md new file mode 100644 index 0000000..a1c5f85 --- /dev/null +++ b/docs/索引spu-sku层级结构设计.md @@ -0,0 +1,388 @@ + + +公司文档地址: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 + diff --git a/docs/索引字段说明.md b/docs/索引字段说明.md index 2a6e9c3..f0119bd 100644 --- a/docs/索引字段说明.md +++ b/docs/索引字段说明.md @@ -171,28 +171,31 @@ category_path varchar(500) ### 属性 1. 组织ES输入数据的时候,需要为sku拼接spu的 option1 option2 option3,作为属性名称(比如“颜色”),sku的 option1 option2 option3 作为属性值(比如“白色”) 2. 有以下方案: TODO 可以选择其中一种,或者2用于填充3用于搜索: -1)铺平展开,只支持三个 +1)铺平展开,只支持三个,支持查询。缺点是,查询逻辑跟租户的属性维度绑定,不灵活 attr1 attr2 attr3 option1 option2 option3 -2)nested,支持多个,动态。 查询性能低 +2)nested,支持超过3个属性(动态)。支持查询 "specifications": { "type": "nested", "properties": { - "name": { "type": "keyword" }, // "颜色", "容量" - "value": { "type": "keyword" } // "白色", "256GB" + "name": { "type": "keyword" }, // "颜色", "容量" + "value": { "type": "keyword" } // "白色", "256GB" } }, -3)平铺展开。写入时从 specifications 提取并填充这些字段,查询性能高。 -"properties": { - "color": { "type": "keyword" }, - "capacity": { "type": "keyword" }, - "network": { "type": "keyword" }, - "edition": { "type": "keyword" } -} +3)nested,支持超过3个属性(动态)。只用作返回,不能查询。节省索引空间 +"specifications": { + "type": "nested", + "properties": { + "name": { "type": "keyword","index": false }, + "value": { "type": "keyword","index": false } + } +}, + +考虑使用方案(3)。 ### status 1. 商品下架等状态 @@ -218,30 +221,7 @@ SEO关键词 ## SPU 与 SKU 的协同设计 -以下方案: -1. sku为索引单位。使用 collapse 按 spu_id 折叠 -需要考虑大量的字段冗余 - -2. spu为单位。 sku的title作为 spu 的sku_titles 属性。 - 除了title, brielf description seo相关 cate tags vendor所有影响相关性的字段都在spu。 sku只有一个title。所以,可以以spu为单位,sku的title作为spu的一个字段,以list形式灌入,假设一个spu有三个sku,那么这个sku_titles字段有三个值,打分的时候按max取得打分,并且我们可以得到这三个sku的title匹配的得分,因此好决定sku的排序。 - -3. sku 作为nested - - - -参考 [](https://blog.csdn.net/csdn_tom_168/article/details/150432666) -方案一:独立索引 -spu-catalog-*:用于品牌、类目、商品介绍等宏观搜索。 -sku-catalog-*:用于具体规格搜索、下单。 -优势:职责分离,查询更高效。 - -方案二:联合查询 -GET /spu-read,sku-read/_search -适用于“模糊搜索 → 跳转详情页”的场景。 - -方案三:父子文档(不推荐) -join 类型维护 SPU-SKU 关系。 -性能差,维护复杂,不适用于高并发搜索场景。 +参考《索引spu-sku层级结构设计.md》 ## rank - 相关性 -- libgit2 0.21.2