From 93be98cb287bd879e5e7e6c495e3d505154d5053 Mon Sep 17 00:00:00 2001 From: tangwang Date: Thu, 26 Mar 2026 21:48:06 +0800 Subject: [PATCH] 清理过时的文档 --- config/config.yaml | 2 +- context/request_context.py | 2 -- data/mai_jia_jing_ling/亚马逊到店匠格式转换分析.md | 373 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ data/mai_jia_jing_ling/亚马逊格式数据转店匠商品导入模板.md | 176 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ docs/Search-API-Examples.md | 1350 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ docs/TODO-ES能力提升.md | 1 + docs/TODO-prompts.md | 39 --------------------------------------- docs/TODO-意图判断-2.md | 40 ---------------------------------------- docs/TODO-意图判断-done.md | 268 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ docs/TODO-意图判断.md | 99 --------------------------------------------------------------------------------------------------- docs/Untitled | 38 -------------------------------------- docs/reranker-共享前缀批量推理.md | 460 ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- docs/亚马逊到店匠格式转换分析.md | 373 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- docs/亚马逊格式数据转店匠商品导入模板.md | 176 -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- docs/系统设计文档.md | 894 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ docs/系统设计文档v1.md | 743 ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- example_usage.py | 12 +++--------- frontend/static/js/app.js | 1 - reranker/reranker-共享前缀批量推理.md | 460 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ search/searcher.py | 2 -- 20 files changed, 1282 insertions(+), 4227 deletions(-) create mode 100644 data/mai_jia_jing_ling/亚马逊到店匠格式转换分析.md create mode 100644 data/mai_jia_jing_ling/亚马逊格式数据转店匠商品导入模板.md delete mode 100644 docs/Search-API-Examples.md delete mode 100644 docs/TODO-prompts.md delete mode 100644 docs/TODO-意图判断-2.md create mode 100644 docs/TODO-意图判断-done.md delete mode 100644 docs/TODO-意图判断.md delete mode 100644 docs/Untitled delete mode 100644 docs/reranker-共享前缀批量推理.md delete mode 100644 docs/亚马逊到店匠格式转换分析.md delete mode 100644 docs/亚马逊格式数据转店匠商品导入模板.md delete mode 100644 docs/系统设计文档.md delete mode 100644 docs/系统设计文档v1.md create mode 100644 reranker/reranker-共享前缀批量推理.md diff --git a/config/config.yaml b/config/config.yaml index ba94881..143fcb3 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -278,7 +278,7 @@ services: glossary_id: "" use_cache: true nllb-200-distilled-600m: - enabled: false + enabled: true backend: "local_nllb" model_id: "facebook/nllb-200-distilled-600M" model_dir: "./models/translation/facebook/nllb-200-distilled-600M" diff --git a/context/request_context.py b/context/request_context.py index 4539ad4..de0a9c6 100644 --- a/context/request_context.py +++ b/context/request_context.py @@ -41,7 +41,6 @@ class QueryAnalysisResult: translations: Dict[str, str] = field(default_factory=dict) query_vector: Optional[List[float]] = None boolean_ast: Optional[str] = None - is_simple_query: bool = True @dataclass @@ -280,7 +279,6 @@ class RequestContext: 'query_normalized': self.query_analysis.query_normalized, 'rewritten_query': self.query_analysis.rewritten_query, 'detected_language': self.query_analysis.detected_language, - 'is_simple_query': self.query_analysis.is_simple_query }, 'performance': { 'total_duration_ms': round(self.performance_metrics.total_duration, 2), diff --git a/data/mai_jia_jing_ling/亚马逊到店匠格式转换分析.md b/data/mai_jia_jing_ling/亚马逊到店匠格式转换分析.md new file mode 100644 index 0000000..8ebeea3 --- /dev/null +++ b/data/mai_jia_jing_ling/亚马逊到店匠格式转换分析.md @@ -0,0 +1,373 @@ +# 亚马逊格式到店匠格式转换 - 核心工作内容分析 + +## 一、概述 + +本项目实现了从**亚马逊格式Excel数据**到**店匠(Shoplazza)商品导入模板**的格式转换,主要处理商品的多款式(变体)结构和属性字段映射。 + +**核心脚本**:`scripts/amazon_xlsx_to_shoplazza_xlsx.py` + +--- + +## 二、父子款式处理(M/P/S 结构转换) + +### 2.1 输入格式(亚马逊) + +- **ASIN**:变体ID(SKU级别) +- **父ASIN**:父商品ID(SPU级别) +- 一个父ASIN可以包含多个ASIN(多个变体) + +### 2.2 输出格式(店匠) + +店匠模板定义了三种商品属性类型: + +1. **S(单一款式)**:只有一个变体的商品 + - 输出:**1行** + - 包含所有商品信息(标题、价格、库存等) + +2. **M(主商品)+ P(子款式)**:包含多个变体的商品 + - 输出:**1行M + N行P** + - **关键约束**:同一商品的P行必须紧跟在M行后面(模板导入强约束) + +### 2.3 转换策略 + +```python +# 核心逻辑(简化版) +for 父ASIN in 所有父ASIN: + variants = 获取该父ASIN下的所有ASIN + + if len(variants) == 1: + 生成 S 行(单一款式) + else: + 生成 M 行(主商品)+ 多个 P 行(子款式) +``` + +### 2.4 关键处理点 + +#### 1. 父ASIN排序 +- 确保父ASIN对应的变体在列表最前面 +- 如果找不到父ASIN对应的变体,根据配置决定是否丢弃整个SPU + +#### 2. 标题一致性检查 +- 同一SPU下的所有变体标题必须一致 +- 如果发现不一致: + - 选项1:丢弃标题不一致的SKU(默认) + - 选项2:修正为统一的主商品标题 + +#### 3. M行与P行的字段分工 + +**M行(主商品)填写**: +- ✅ 商品标题、描述、SEO信息 +- ✅ 专辑、标签、供应商信息 +- ✅ 商品主图 +- ✅ 款式维度名(款式1/2/3的key) +- ❌ 不填:价格、库存、重量等SKU级字段(保持为空更安全) + +**P行(子款式)填写**: +- ✅ 商品标题(与M行一致) +- ✅ 款式维度值(款式1/2/3的value) +- ✅ 价格、商品SKU(ASIN)、库存 +- ✅ 重量、尺寸 +- ✅ 子款式图(可选) +- ❌ 不填:描述、SEO、专辑等SPU级字段(保持为空) + +--- + +## 三、属性字段处理(款式维度解析) + +### 3.1 问题背景 + +亚马逊格式中,变体的"颜色/尺码"等信息**并不拆成多个列**,而是集中在 `SKU` 字符串里: + +``` +示例1: "Size: One Size | Color: Black" +示例2: "Color: Red | Style: 2-Pack" +``` + +而店匠模板需要: +- **M行**:`款式1/款式2/款式3` 填写**维度名**(如 Size、Color、Material) +- **P行**:`款式1/款式2/款式3` 填写**维度值**(如 One Size、Black、Cotton) + +### 3.2 SKU解析逻辑 + +```python +def parse_sku_options(sku_text): + """ + 解析 SKU 列,提取 key:value 对 + 输入: "Size: One Size | Color: Black" + 输出: {"Size": "One Size", "Color": "Black"} + """ + # 1. 按 | 分割 + parts = sku_text.split("|") + + # 2. 按 : 拆成 key/value + for part in parts: + if ":" in part: + key, value = part.split(":", 1) + result[key.strip()] = value.strip() + + return result +``` + +### 3.3 维度选择策略(最多3个维度) + +店匠模板只提供 `款式1~3` 三个维度,因此需要从多个变体中**智能选择最多3个维度**: + +#### 优先级规则 + +1. **按预设优先级排序**: + ```python + PREFERRED_OPTION_KEYS = [ + "Size", "Color", "Style", "Pattern", "Material", + "Flavor", "Scent", "Pack", "Pack of", ... + ] + ``` + +2. **按出现频次排序**:统计每个key在所有变体中的出现次数 + +3. **综合排序**: + ```python + def key_sort(k): + return ( + 预设优先级(越小越优先), + -出现频次(越大越优先), + 字母顺序(作为最后排序依据) + ) + ``` + +#### 退化处理 + +如果解析不到任何 key/value,则退化为单维度: +- M行:`款式1 = "Variant"` +- P行:`款式1 = ASIN`(使用ASIN作为维度值) + +### 3.4 维度映射示例 + +**输入数据**(3个变体): +``` +变体1: SKU = "Size: S | Color: Red" +变体2: SKU = "Size: M | Color: Red" +变体3: SKU = "Size: S | Color: Blue" +``` + +**解析结果**: +- 维度统计:Size出现3次,Color出现3次 +- 选择维度:Size(优先级1)、Color(优先级2) +- 最多3个,所以选择前2个 + +**输出格式**: +``` +M行: + 款式1 = "Size" + 款式2 = "Color" + 款式3 = "" + +P行1: + 款式1 = "S" + 款式2 = "Red" + 款式3 = "" + +P行2: + 款式1 = "M" + 款式2 = "Red" + 款式3 = "" + +P行3: + 款式1 = "S" + 款式2 = "Blue" + 款式3 = "" +``` + +--- + +## 四、字段映射总览 + +### 4.1 核心字段映射 + +| 店匠字段 | 亚马逊字段 | 处理逻辑 | +|---------|-----------|---------| +| **商品spu** | `父ASIN` | 无父ASIN则用ASIN | +| **商品SKU** | `ASIN` | 直接映射 | +| **商品标题*** | `商品标题` | 截断至255字符 | +| **商品图片*** | `商品主图` | URL直接映射 | +| **商品售价*** | `prime价格($)` 或 `价格($)` | 优先prime价格 | +| **创建时间** | `上架时间` | 日期格式转换(补齐时分秒) | +| **商品描述** | `商品标题` + `详细参数` | HTML拼接 | +| **专辑名称** | `大类目` | 无则取`类目路径`第一段 | +| **标签** | `品牌,大类目,小类目` | 逗号拼接 | +| **商品重量/重量单位** | `商品重量(单位换算)` | 解析数值和单位(g/kg/lb/oz) | +| **尺寸信息** | `商品尺寸` | 解析前三段数字,拼成 `L,W,H`(英寸) | + +### 4.2 特殊字段处理 + +#### 1. 价格处理 +```python +price = prime价格($) or 价格($) or 9.99 # 默认值9.99 +``` + +#### 2. 库存处理 +- 亚马逊数据源通常**没有库存** +- 脚本默认给每个变体固定库存:**100** + +#### 3. 日期格式转换 +```python +输入: "2018-05-09" 或 datetime对象 +输出: "2018-05-09 00:00:00" # 补齐时分秒 +``` + +#### 4. 重量解析 +```python +输入: "68.04 g" 或 "0.15 pounds" +输出: (68.04, "g") 或 (0.15, "lb") +``` + +#### 5. 尺寸解析 +```python +输入: "7.9 x 7.9 x 2 inches" +输出: "7.9,7.9,2" # L,W,H格式 +``` + +#### 6. SEO URL Handle生成 +```python +输入: "Legendary Whitetails Men's Buck Camp Flannel Shirt" +输出: "products/legendary-whitetails-mens-buck-camp-flannel-shirt" +# 规则:转小写、去特殊字符、空格转横线 +``` + +--- + +## 五、重要工作内容总结 + +### 5.1 数据结构转换 + +1. **父子关系识别**:从扁平化的ASIN列表识别出SPU-SKU层级关系 +2. **分组策略**:按父ASIN分组,决定生成S还是M+P结构 +3. **行序保证**:确保同一SPU的M+P行连续,不被打断 + +### 5.2 属性字段解析 + +1. **SKU字符串解析**:从非结构化字符串中提取key:value对 +2. **维度智能选择**:从多个可能的维度中选择最重要的3个 +3. **优先级算法**:综合考虑预设优先级、出现频次、字母顺序 + +### 5.3 字段映射与转换 + +1. **格式转换**:日期、价格、重量、尺寸等格式标准化 +2. **默认值填充**:库存、价格等缺失字段的默认值策略 +3. **数据清洗**:标题截断、HTML转义、URL生成等 + +### 5.4 数据质量控制 + +1. **标题一致性检查**:确保同一SPU下所有变体标题一致 +2. **父ASIN验证**:检查父ASIN是否存在于变体列表中 +3. **错误处理**:提供配置选项决定是修正还是丢弃异常数据 + +### 5.5 性能优化 + +1. **批量处理**:支持多文件批量转换 +2. **文件拆分**:按最大行数自动拆分输出文件(保证同一SPU不拆分) +3. **快速读写**:使用`iter_rows(values_only=True)`和write_only模式提升性能 + +--- + +## 六、关键技术难点 + +### 6.1 维度选择算法 + +**挑战**:从多个变体的SKU字符串中,智能选择最重要的3个维度 + +**解决方案**: +- 统计所有变体中每个key的出现频次 +- 结合预设优先级列表(Size > Color > Style > ...) +- 综合排序选择前3个 + +### 6.2 数据一致性保证 + +**挑战**:确保同一SPU下的所有变体数据一致 + +**解决方案**: +- 标题一致性检查和修正 +- 父ASIN排序保证 +- M行和P行的字段分工明确 + +### 6.3 文件拆分策略 + +**挑战**:按最大行数拆分文件,但不能拆分同一SPU + +**解决方案**: +- 先按SPU分组生成所有行 +- 拆分时以SPU为单位,不拆分单个SPU的行 + +--- + +## 七、扩展点 + +### 7.1 可配置项 + +- 库存默认值(当前100) +- 价格默认值(当前9.99) +- 上架/收税/物流策略(当前硬编码Y/N) + +### 7.2 可增强功能 + +- **更强的多款式解析**:如果SKU字段不规范,可从`详细参数`中提取Color/Size +- **图片策略优化**:P行可改为使用M行合并的多图(逗号拼接) +- **元字段支持**:支持店匠的元字段导入 + +--- + +## 八、使用示例 + +### 8.1 小批量验证 + +```bash +python scripts/amazon_xlsx_to_shoplazza_xlsx.py \ + --input-dir data/mai_jia_jing_ling/products_data \ + --template docs/商品导入模板.xlsx \ + --output data/mai_jia_jing_ling/amazon_shoplazza_import_SAMPLE.xlsx \ + --max-files 1 --max-products 50 +``` + +### 8.2 全量转换 + +```bash +python scripts/amazon_xlsx_to_shoplazza_xlsx.py \ + --input-dir data/mai_jia_jing_ling/products_data \ + --template docs/商品导入模板.xlsx \ + --output data/mai_jia_jing_ling/amazon_shoplazza_import_ALL.xlsx +``` + +### 8.3 自动拆分文件 + +```bash +python scripts/amazon_xlsx_to_shoplazza_xlsx.py \ + --input-dir data/mai_jia_jing_ling/products_data \ + --template docs/商品导入模板.xlsx \ + --output data/mai_jia_jing_ling/amazon_shoplazza_import_SPLIT.xlsx \ + --max-rows-per-output 40000 +``` + +--- + +## 九、总结 + +亚马逊格式到店匠格式的转换,核心工作包括: + +1. **父子款式结构转换**:从ASIN/父ASIN关系转换为M/P/S结构 +2. **属性字段解析**:从SKU字符串中提取并智能选择款式维度 +3. **字段映射与转换**:40+个字段的格式转换和默认值处理 +4. **数据质量控制**:一致性检查、错误处理、数据清洗 +5. **性能优化**:批量处理、文件拆分、快速读写 + +这是一个典型的**数据格式转换ETL任务**,涉及数据结构重组、字符串解析、智能算法选择等多个技术领域。 + + + + + + + + + + + + diff --git a/data/mai_jia_jing_ling/亚马逊格式数据转店匠商品导入模板.md b/data/mai_jia_jing_ling/亚马逊格式数据转店匠商品导入模板.md new file mode 100644 index 0000000..aef35e3 --- /dev/null +++ b/data/mai_jia_jing_ling/亚马逊格式数据转店匠商品导入模板.md @@ -0,0 +1,176 @@ + +## 数据pipeline + +1. 从 卖家精灵“关键词选品”选出服装品类的关键词,每个关键词会带上top10 asin +2. 汇总asin,做成 每行 1000个 asin(因为卖家精灵每次最多查询1000个)。 这样的话,总共34w+商品,美国站点是12w商品(120行)。 做了个浏览器自动化执行的工具,导出120个文件,然后下载下来。 +3. 用本文档介绍的工具,将卖家精灵导出的excel转化为店匠的导入文件。 +```bash + python scripts/amazon_xlsx_to_shoplazza_xlsx.py --input-dir data/mai_jia_jing_ling/products_data --template docs/商品导入模板.xlsx +``` +5. 导入到店匠后,自动同步到mysql +6. mysql 到 ES : +```bash +python scripts/recreate_and_import.py \ + --tenant-id xxxxxxxxxx +``` + + +## 亚马逊格式数据 → 店匠(Shoplazza)商品导入模板:转换说明 + +本仓库支持把 `data/mai_jia_jing_ling/products_data/*.xlsx`(**亚马逊格式导出**)转换为店匠后台可导入的 `docs/商品导入模板.xlsx` 格式。 + +对应脚本: +- **主入口**:`scripts/amazon_xlsx_to_shoplazza_xlsx.py` +- **历史兼容**:`scripts/competitor_xlsx_to_shoplazza_xlsx.py`(仅名称过时,逻辑一致) +- **模板写入复用工具**:`scripts/shoplazza_excel_template.py` + +--- + +## 一、输入数据(亚马逊格式 xlsx)的关键字段 + +以 `Competitor-US-Last-30-days-363464.xlsx` 为例(文件名不影响:内容是亚马逊维度字段): + +- **ASIN**:变体 id(我们视为 `sku_id`,会写入模板的 `商品SKU`) +- **父ASIN**:父商品 id(我们视为 `spu_id`/`product_id`,会写入模板的 `商品spu`,并用于分组 M/P) +- **商品标题**:商品标题(写入 `商品标题*`、SEO标题等) +- **SKU**:亚马逊变体描述字符串(关键:解析出多款式维度) + - 示例:`Size: One Size | Color: Black` +- **商品主图**:图片 URL(用于 `商品图片*` / `商品主图`) +- **价格($)** / **prime价格($)**:价格(用于 `商品售价*` / `商品原价`) +- **详细参数**:详情参数串(用于拼接 `商品描述`) +- **上架时间**:用于 `创建时间` +- **类目路径/大类目/小类目/品牌/商品详情页链接/品牌链接**:用于专辑、标签、SEO、供应商URL、备注等 +- **商品重量(单位换算)/商品重量/商品尺寸**:用于 `商品重量/重量单位/尺寸信息` + +> 注意:该数据源通常**没有库存**,脚本默认给每个变体一个固定库存(当前默认 100),以满足导入后的可用性。 + +--- + +## 二、输出数据(店匠导入模板)的核心规则(M / P / S) + +店匠模板在 `docs/商品导入模板说明.md` 中定义了三种商品属性(`商品属性*`): + +- **S(单一款式)**:一个商品只有一个变体(只有 1 个 ASIN) + - 输出 **1 行** +- **M(主商品)+ P(子款式)**:一个父商品(父ASIN)包含多个变体(多个 ASIN) + - 输出 **1 行 M + N 行 P** + - 且 **同一商品的 P 行必须紧跟在 M 行后面**(模板导入强约束) + +本仓库的转换策略: +- 对每个 `父ASIN` 分组: + - **分组 size = 1** → 生成 `S` + - **分组 size > 1** → 生成 `M` + 多个 `P` + +--- + +## 三、多款式(变体)是如何构造的(最关键部分) + +### 1)为什么 “SKU” 列是关键 + +亚马逊格式里,变体的“颜色/尺码”等信息往往并不拆成多个列,而是集中在 `SKU` 字符串里,例如: + +- `Size: One Size | Color: Black` +- `Color: Red | Style: 2-Pack` + +店匠模板的多款式需要: +- **M 行**:`款式1/款式2/款式3` 写“维度名”(例如 Size / Color / Material) +- **P 行**:`款式1/款式2/款式3` 写“维度值”(例如 One Size / Black / Cotton) + +### 2)脚本如何从 SKU 解析出维度(key/value) + +脚本会把 `SKU` 以 `|` 分割,再用 `:` 拆成 key/value: + +- 输入:`Size: One Size | Color: Black` +- 解析结果:`{ "Size": "One Size", "Color": "Black" }` + +### 3)如何从多个变体里选出 “最多3个维度” + +店匠模板只提供 `款式1~3` 三个维度,因此脚本会在一个 `父ASIN` 组内统计 key 的出现频次,并按优先级挑选最多 3 个维度: + +- 优先级大致为:`Size`、`Color`、`Style`、`Pattern`、`Material` …… +- 如果一个组里解析不到任何 key/value,则退化为单维度:`Variant` + - M 行 `款式1 = Variant` + - P 行 `款式1 = ASIN` + +### 4)M 行与 P 行分别填什么(避免导入报错) + +根据模板说明,脚本遵循以下分工: + +- **M 行(主商品)**: + - 填:标题/描述/SEO/专辑/标签/主图/款式维度名 + - 不填:价格、库存、重量等 SKU 级字段(保持为空更安全) +- **P 行(子款式)**: + - 填:款式维度值、价格、商品SKU(ASIN)、库存、重量、尺寸、(可选)子款式图 + - 不填:描述/SEO/专辑/供应商等 SPU 级字段(保持为空) + +--- + +## 四、字段映射总览(高频字段) + +- **商品spu** ← `父ASIN`(无父ASIN则用 ASIN) +- **商品SKU** ← `ASIN` +- **商品标题\*** ← `商品标题` +- **商品图片\*** / **商品主图** ← `商品主图` +- **商品售价\*** ← `prime价格($)` 优先,否则 `价格($)` +- **创建时间** ← `上架时间`(仅日期时补齐为 `YYYY-MM-DD 00:00:00`) +- **商品描述** ← `商品标题` + `详细参数`(以 HTML 拼接) +- **专辑名称** ← `大类目`(无则取 `类目路径` 第一段) +- **标签** ← `品牌,大类目,小类目` +- **商品重量/重量单位** ← 优先解析 `商品重量(单位换算)`(如 `68.04 g`) +- **尺寸信息** ← 解析 `商品尺寸` 前三段数字(英寸)拼成 `L,W,H` + +--- + +## 五、如何运行(生成导入文件) + +### 1)先小批量验证(推荐) + +```bash +python scripts/amazon_xlsx_to_shoplazza_xlsx.py \ + --input-dir data/mai_jia_jing_ling/products_data \ + --template docs/商品导入模板.xlsx \ + --output data/mai_jia_jing_ling/amazon_shoplazza_import_SAMPLE.xlsx \ + --max-files 1 --max-products 50 +``` + +### 性能提示(很重要) + +- 旧实现如果用 `ws.cell()` 逐格读取/写入,处理 1 个 xlsx 就可能非常慢(分钟级甚至更久)。 +- 当前脚本已经使用 **`iter_rows(values_only=True)`** + write_only 模式做快速读写(只有这一种实现方式,保持简单)。 + +### 2)生成全量 + +```bash +python scripts/amazon_xlsx_to_shoplazza_xlsx.py \ + --input-dir data/mai_jia_jing_ling/products_data \ + --template docs/商品导入模板.xlsx \ + --output data/mai_jia_jing_ling/amazon_shoplazza_import_ALL.xlsx +``` + +### 3)按 Excel 行数自动拆分文件 + +当单个导出超过一定行数时,可以通过 `--max-rows-per-output` 控制单个 Excel 的最大总行数(包含模板头部行): + +```bash +python scripts/amazon_xlsx_to_shoplazza_xlsx.py \ + --input-dir data/mai_jia_jing_ling/products_data \ + --template docs/商品导入模板.xlsx \ + --output data/mai_jia_jing_ling/amazon_shoplazza_import_SPLIT.xlsx \ + --max-rows-per-output 40000 +``` + +- 若结果只需要 1 个文件,仍输出为 `amazon_shoplazza_import_SPLIT.xlsx` +- 若需要拆分为多个文件,则追加 `_part1/_part2/...` 后缀: + - `amazon_shoplazza_import_SPLIT_part1.xlsx` + - `amazon_shoplazza_import_SPLIT_part2.xlsx` +- **同一个 SPU(同一父ASIN 的 M+P 或 S 行)保证不会被拆到不同文件中** + +--- + +## 六、可扩展点(后续常见需求) + +- **库存/上架/收税策略参数化**:目前是脚本默认值(Y/N/100),可按目标店铺规则改为命令行参数。 +- **更强的多款式解析**:如果未来亚马逊格式 `SKU` 不规范,可补充从 `详细参数` 里挖出 `Color/Size`。 +- **图片策略**:目前 P 行用各自 `商品主图`;也可改为 M 行合并多图(逗号拼接)。 + + diff --git a/docs/Search-API-Examples.md b/docs/Search-API-Examples.md deleted file mode 100644 index b13d723..0000000 --- a/docs/Search-API-Examples.md +++ /dev/null @@ -1,1350 +0,0 @@ -# API 使用示例 - -本文档提供了搜索引擎 API 的详细使用示例,包括各种常见场景和最佳实践。 - ---- - -## 目录 - -1. [基础搜索](#基础搜索) -2. [过滤器使用](#过滤器使用) -3. [分面搜索](#分面搜索) -4. [排序](#排序) -5. [图片搜索](#图片搜索) -6. [布尔表达式](#布尔表达式) -7. [完整示例](#完整示例) - ---- - -## 基础搜索 - -### 示例 1:最简单的搜索 - -```bash -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -H "X-Tenant-ID: 162" \ - -d '{ - "query": "芭比娃娃" - }' -``` - -**响应**: -```json -{ - "hits": [...], - "total": 118, - "max_score": 8.5, - "took_ms": 45, - "query_info": { - "original_query": "芭比娃娃", - "detected_language": "zh", - "translations": {"en": "barbie doll"} - } -} -``` - -### 示例 2:指定返回数量 - -```bash -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -H "X-Tenant-ID: 162" \ - -d '{ - "query": "手机", - "language": "zh", - "size": 50 - }' -``` - -### 示例 3:分页查询 - -```bash -# 第1页(0-19) -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -H "X-Tenant-ID: 162" \ - -d '{ - "query": "手机", - "language": "zh", - "size": 20, - "from": 0 - }' - -# 第2页(20-39) -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -H "X-Tenant-ID: 162" \ - -d '{ - "query": "手机", - "language": "zh", - "size": 20, - "from": 20 - }' -``` - ---- - -## 过滤器使用 - -### 精确匹配过滤器 - -#### 示例 1:单值过滤 - -```bash -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -H "X-Tenant-ID: 162" \ - -d '{ - "query": "手机", - "language": "zh", - "filters": { - "category_name": "手机" - } - }' -``` - -#### 示例 2:多值过滤(OR 逻辑) - -```bash -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -H "X-Tenant-ID: 162" \ - -d '{ - "query": "手机", - "language": "zh", - "filters": { - "category_name": ["手机", "电子产品"] - } - }' -``` - -说明:匹配类目为"玩具" **或** "益智玩具" **或** "儿童玩具"的商品。 - -#### 示例 3:多字段过滤(AND 逻辑) - -```bash -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -H "X-Tenant-ID: 162" \ - -d '{ - "query": "手机", - "language": "zh", - "filters": { - "category_name": "手机", - "vendor.zh.keyword": "奇乐" - } - }' -``` - -说明:必须同时满足"类目=手机" **并且** "品牌=奇乐"。 - -#### 示例 4:Specifications 嵌套过滤(单个规格) - -```bash -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -H "X-Tenant-ID: 162" \ - -d '{ - "query": "手机", - "language": "zh", - "filters": { - "specifications": { - "name": "color", - "value": "white" - } - } - }' -``` - -说明:查询规格名称为"color"且值为"white"的商品。 - -#### 示例 5:Specifications 嵌套过滤(多个规格,OR逻辑) - -```bash -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -H "X-Tenant-ID: 162" \ - -d '{ - "query": "手机", - "language": "zh", - "filters": { - "specifications": [ - {"name": "color", "value": "white"}, - {"name": "size", "value": "256GB"} - ] - } - }' -``` - -说明:查询满足任意一个规格的商品(color=white **或** size=256GB)。 - -#### 示例 6:组合过滤(包含 specifications) - -```bash -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -H "X-Tenant-ID: 162" \ - -d '{ - "query": "手机", - "language": "zh", - "filters": { - "category_name": "手机", - "specifications": { - "name": "color", - "value": "white" - } - } - }' -``` - -说明:同时满足类目=手机 **并且** color=white。 - -### 范围过滤器 - -#### 示例 1:价格范围 - -```bash -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -H "X-Tenant-ID: 162" \ - -d '{ - "query": "手机", - "language": "zh", - "range_filters": { - "min_price": { - "gte": 50, - "lte": 200 - } - } - }' -``` - -说明:价格在 50-200 元之间(包含边界)。 - -#### 示例 2:只设置下限 - -```bash -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -H "X-Tenant-ID: 162" \ - -d '{ - "query": "手机", - "language": "zh", - "range_filters": { - "min_price": { - "gte": 100 - } - } - }' -``` - -说明:价格 ≥ 100 元。 - -#### 示例 3:只设置上限 - -```bash -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -H "X-Tenant-ID: 162" \ - -d '{ - "query": "手机", - "language": "zh", - "range_filters": { - "min_price": { - "lt": 50 - } - } - }' -``` - -说明:价格 < 50 元(不包含50)。 - -#### 示例 4:多字段范围过滤 - -```bash -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -H "X-Tenant-ID: 162" \ - -d '{ - "query": "手机", - "language": "zh", - "range_filters": { - "min_price": { - "gte": 50, - "lte": 200 - }, - "days_since_last_update": { - "lte": 30 - } - } - }' -``` - -说明:价格在 50-200 元 **并且** 最近30天内更新过。 - -### 组合过滤器 - -```bash -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -H "X-Tenant-ID: 162" \ - -d '{ - "query": "手机", - "language": "zh", - "filters": { - "category_name": ["手机", "电子产品"], - "vendor.zh.keyword": "品牌A" - }, - "range_filters": { - "min_price": { - "gte": 50, - "lte": 500 - } - } - }' -``` - -说明:类目是"玩具"或"益智玩具" **并且** 品牌是"乐高" **并且** 价格在 50-500 元之间。 - ---- - -## 分面搜索 - -### 简单模式 - -#### 示例 1:基础分面 - -```bash -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -H "X-Tenant-ID: 162" \ - -d '{ - "query": "手机", - "language": "zh", - "size": 20, - "facets": ["category1_name", "category2_name", "specifications"] - }' -``` - -**响应**: -```json -{ - "results": [...], - "total": 118, - "facets": [ - { - "field": "category1_name", - "label": "category1_name", - "type": "terms", - "values": [ - {"value": "手机", "count": 85, "selected": false}, - {"value": "电子产品", "count": 33, "selected": false} - ] - }, - { - "field": "specifications.color", - "label": "color", - "type": "terms", - "values": [ - {"value": "white", "count": 50, "selected": false}, - {"value": "black", "count": 30, "selected": false} - ] - }, - { - "field": "specifications.size", - "label": "size", - "type": "terms", - "values": [ - {"value": "256GB", "count": 40, "selected": false}, - {"value": "512GB", "count": 20, "selected": false} - ] - } - ] -} -``` - -#### 示例 2:Specifications 分面(所有规格名称) - -```bash -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -H "X-Tenant-ID: 162" \ - -d '{ - "query": "手机", - "language": "zh", - "facets": ["specifications"] - }' -``` - -说明:返回所有规格名称(name)及其对应的值(value)列表。 - -#### 示例 3:Specifications 分面(指定规格名称) - -```bash -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -H "X-Tenant-ID: 162" \ - -d '{ - "query": "手机", - "language": "zh", - "facets": ["specifications.color", "specifications.size"] - }' -``` - -说明:只返回指定规格名称的值列表。 - -### 高级模式 - -#### 示例 1:自定义分面大小 - -```bash -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -H "X-Tenant-ID: 162" \ - -d '{ - "query": "手机", - "language": "zh", - "facets": [ - { - "field": "category1_name", - "size": 20, - "type": "terms" - }, - { - "field": "category2_name", - "size": 30, - "type": "terms" - } - ] - }' -``` - -#### 示例 2:范围分面 - -```bash -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -H "X-Tenant-ID: 162" \ - -d '{ - "query": "手机", - "language": "zh", - "facets": [ - { - "field": "price", - "type": "range", - "ranges": [ - {"key": "0-50", "to": 50}, - {"key": "50-100", "from": 50, "to": 100}, - {"key": "100-200", "from": 100, "to": 200}, - {"key": "200+", "from": 200} - ] - } - ] - }' -``` - -**响应**: -```json -{ - "facets": [ - { - "field": "price", - "label": "price", - "type": "range", - "values": [ - {"value": "0-50", "count": 23, "selected": false}, - {"value": "50-100", "count": 45, "selected": false}, - {"value": "100-200", "count": 38, "selected": false}, - {"value": "200+", "count": 12, "selected": false} - ] - } - ] -} -``` - -#### 示例 3:混合分面(Terms + Range) - -```bash -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -H "X-Tenant-ID: 162" \ - -d '{ - "query": "手机", - "language": "zh", - "facets": [ - {"field": "category.keyword", "size": 15}, - {"field": "vendor.keyword", "size": 15}, - { - "field": "price", - "type": "range", - "ranges": [ - {"key": "低价", "to": 50}, - {"key": "中价", "from": 50, "to": 200}, - {"key": "高价", "from": 200} - ] - } - ] - }' -``` - ---- - -## 排序 - -### 示例 1:按价格排序(升序) - -```bash -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -H "X-Tenant-ID: 162" \ - -d '{ - "query": "手机", - "language": "zh", - "size": 20, - "sort_by": "min_price", - "sort_order": "asc" - }' -``` - -### 示例 2:按创建时间排序(降序) - -```bash -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -H "X-Tenant-ID: 162" \ - -d '{ - "query": "手机", - "language": "zh", - "size": 20, - "sort_by": "create_time", - "sort_order": "desc" - }' -``` - -### 示例 3:排序+过滤 - -```bash -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -H "X-Tenant-ID: 162" \ - -d '{ - "query": "手机", - "language": "zh", - "filters": { - "category.keyword": "益智玩具" - }, - "sort_by": "min_price", - "sort_order": "asc" - }' -``` - ---- - -## 图片搜索 - -### 示例 1:基础图片搜索 - -```bash -curl -X POST "http://localhost:6002/search/image" \ - -H "Content-Type: application/json" \ - -H "X-Tenant-ID: 162" \ - -d '{ - "image_url": "https://example.com/barbie.jpg", - "size": 20 - }' -``` - -### 示例 2:图片搜索+过滤器 - -```bash -curl -X POST "http://localhost:6002/search/image" \ - -H "Content-Type: application/json" \ - -H "X-Tenant-ID: 162" \ - -d '{ - "image_url": "https://example.com/barbie.jpg", - "size": 20, - "filters": { - "category_name": "手机" - }, - "range_filters": { - "min_price": { - "lte": 100 - } - } - }' -``` - ---- - -## 布尔表达式 - -### 示例 1:AND 查询 - -```bash -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -H "X-Tenant-ID: 162" \ - -d '{ - "query": "玩具 AND 乐高" - }' -``` - -说明:必须同时包含"玩具"和"乐高"。 - -### 示例 2:OR 查询 - -```bash -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -H "X-Tenant-ID: 162" \ - -d '{ - "query": "芭比 OR 娃娃" - }' -``` - -说明:包含"芭比"或"娃娃"即可。 - -### 示例 3:ANDNOT 查询(排除) - -```bash -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -H "X-Tenant-ID: 162" \ - -d '{ - "query": "玩具 ANDNOT 电动" - }' -``` - -说明:包含"玩具"但不包含"电动"。 - -### 示例 4:复杂布尔表达式 - -```bash -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -H "X-Tenant-ID: 162" \ - -d '{ - "query": "玩具 AND (乐高 OR 芭比) ANDNOT 电动" - }' -``` - -说明:必须包含"玩具",并且包含"乐高"或"芭比",但不包含"电动"。 - -### 示例 5:域查询 - -```bash -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -H "X-Tenant-ID: 162" \ - -d '{ - "query": "brand:乐高" - }' -``` - -说明:在品牌域中搜索"乐高"。 - ---- - -## 完整示例 - -### Python 完整示例 - -```python -#!/usr/bin/env python3 -import requests -import json - -API_URL = "http://localhost:6002/search/" - -def search_products( - query, - size=20, - from_=0, - filters=None, - range_filters=None, - facets=None, - sort_by=None, - sort_order="desc", - debug=False -): - """执行搜索查询""" - payload = { - "query": query, - "size": size, - "from": from_ - } - - if filters: - payload["filters"] = filters - if range_filters: - payload["range_filters"] = range_filters - if facets: - payload["facets"] = facets - if sort_by: - payload["sort_by"] = sort_by - payload["sort_order"] = sort_order - if debug: - payload["debug"] = debug - - response = requests.post(API_URL, json=payload) - response.raise_for_status() - return response.json() - - -# 示例 1:简单搜索 -result = search_products("芭比娃娃", size=10) -print(f"找到 {result['total']} 个结果") -for hit in result['hits'][:3]: - product = hit['_source'] - print(f" - {product['name']}: ¥{product.get('price', 'N/A')}") - -# 示例 2:带过滤和分面的搜索 -result = search_products( - query="手机", - size=20, - language="zh", - filters={ - "category_name": "手机", - "specifications": {"name": "color", "value": "white"} - }, - range_filters={ - "min_price": {"gte": 50, "lte": 200} - }, - facets=[ - {"field": "category1_name", "size": 15}, - {"field": "category2_name", "size": 15}, - "specifications.color", - "specifications.size", - { - "field": "min_price", - "type": "range", - "ranges": [ - {"key": "0-50", "to": 50}, - {"key": "50-100", "from": 50, "to": 100}, - {"key": "100-200", "from": 100, "to": 200}, - {"key": "200+", "from": 200} - ] - } - ], - sort_by="min_price", - sort_order="asc" -) - -# 显示分面结果 -print(f"\n分面统计:") -for facet in result.get('facets', []): - print(f"\n{facet['label']} ({facet['type']}):") - for value in facet['values'][:5]: - selected_mark = "✓" if value['selected'] else " " - print(f" [{selected_mark}] {value['label']}: {value['count']}") - -# 示例 3:分页查询 -page = 1 -page_size = 20 -total_pages = 5 - -for page in range(1, total_pages + 1): - result = search_products( - query="玩具", - size=page_size, - from_=(page - 1) * page_size - ) - print(f"\n第 {page} 页:") - for hit in result['hits']: - product = hit['_source'] - print(f" - {product['name']}") -``` - -### JavaScript 完整示例 - -```javascript -// 搜索引擎客户端 -class SearchClient { - constructor(baseUrl) { - this.baseUrl = baseUrl; - } - - async search({ - query, - size = 20, - from = 0, - filters = null, - rangeFilters = null, - facets = null, - sortBy = null, - sortOrder = 'desc', - debug = false - }) { - const payload = { - query, - size, - from - }; - - if (filters) payload.filters = filters; - if (rangeFilters) payload.range_filters = rangeFilters; - if (facets) payload.facets = facets; - if (sortBy) { - payload.sort_by = sortBy; - payload.sort_order = sortOrder; - } - if (debug) payload.debug = debug; - - const response = await fetch(`${this.baseUrl}/search/`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(payload) - }); - - if (!response.ok) { - throw new Error(`HTTP ${response.status}: ${response.statusText}`); - } - - return await response.json(); - } - - async searchByImage(imageUrl, options = {}) { - const payload = { - image_url: imageUrl, - size: options.size || 20, - filters: options.filters || null, - range_filters: options.rangeFilters || null - }; - - const response = await fetch(`${this.baseUrl}/search/image`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(payload) - }); - - if (!response.ok) { - throw new Error(`HTTP ${response.status}: ${response.statusText}`); - } - - return await response.json(); - } -} - -// 使用示例 -const client = new SearchClient('http://localhost:6002'); - -// 简单搜索 -const result1 = await client.search({ - query: "芭比娃娃", - size: 20 -}); -console.log(`找到 ${result1.total} 个结果`); - -// 带过滤和分面的搜索(包含规格) -const result2 = await client.search({ - query: "手机", - language: "zh", - size: 20, - filters: { - category_name: "手机", - specifications: { name: "color", value: "white" } - }, - rangeFilters: { - min_price: { gte: 50, lte: 200 } - }, - facets: [ - "category1_name", - "specifications.color", - "specifications.size" - ], - sortBy: "min_price", - sortOrder: "asc" -}); - -// 显示分面结果 -result2.facets.forEach(facet => { - console.log(`\n${facet.label}:`); - facet.values.forEach(value => { - const selected = value.selected ? '✓' : ' '; - console.log(` [${selected}] ${value.label}: ${value.count}`); - }); -}); - -// 显示商品 -result2.hits.forEach(hit => { - const product = hit._source; - console.log(`${product.name} - ¥${product.price}`); -}); -``` - -### 前端完整示例(Vue.js 风格) - -```javascript -// 搜索组件 -const SearchComponent = { - data() { - return { - query: '', - results: [], - facets: [], - filters: {}, - rangeFilters: {}, - total: 0, - currentPage: 1, - pageSize: 20 - }; - }, - methods: { - async search() { - const response = await fetch('http://localhost:6002/search/', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - query: this.query, - size: this.pageSize, - from: (this.currentPage - 1) * this.pageSize, - filters: this.filters, - range_filters: this.rangeFilters, - facets: [ - { field: 'category.keyword', size: 15 }, - { field: 'vendor.keyword', size: 15 } - ] - }) - }); - - const data = await response.json(); - this.results = data.hits; - this.facets = data.facets || []; - this.total = data.total; - }, - - toggleFilter(field, value) { - if (!this.filters[field]) { - this.filters[field] = []; - } - - const index = this.filters[field].indexOf(value); - if (index > -1) { - this.filters[field].splice(index, 1); - if (this.filters[field].length === 0) { - delete this.filters[field]; - } - } else { - this.filters[field].push(value); - } - - this.currentPage = 1; - this.search(); - }, - - setPriceRange(min, max) { - if (min !== null || max !== null) { - this.rangeFilters.price = {}; - if (min !== null) this.rangeFilters.price.gte = min; - if (max !== null) this.rangeFilters.price.lte = max; - } else { - delete this.rangeFilters.price; - } - this.currentPage = 1; - this.search(); - } - } -}; -``` - ---- - -## 调试与优化 - -### 启用调试模式 - -```bash -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -H "X-Tenant-ID: 162" \ - -d '{ - "query": "手机", - "language": "zh", - "debug": true - }' -``` - -**响应包含调试信息**: -```json -{ - "hits": [...], - "total": 118, - "debug_info": { - "query_analysis": { - "original_query": "玩具", - "query_normalized": "玩具", - "rewritten_query": "玩具", - "detected_language": "zh", - "translations": {"en": "toy"} - }, - "es_query": { - "query": {...}, - "size": 10 - }, - "stage_timings": { - "query_parsing": 5.3, - "elasticsearch_search": 35.1, - "result_processing": 4.8 - } - } -} -``` - -### 设置最小分数阈值 - -```bash -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -H "X-Tenant-ID: 162" \ - -d '{ - "query": "手机", - "language": "zh", - "min_score": 5.0 - }' -``` - -说明:只返回相关性分数 ≥ 5.0 的结果。 - ---- - -## 常见使用场景 - -### 场景 1:电商分类页 - -```bash -# 显示某个类目下的所有商品,按价格排序,提供品牌筛选 -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -H "X-Tenant-ID: 162" \ - -d '{ - "query": "*", - "filters": { - "category_name": "手机" - }, - "facets": [ - {"field": "vendor.keyword", "size": 20}, - { - "field": "price", - "type": "range", - "ranges": [ - {"key": "0-50", "to": 50}, - {"key": "50-100", "from": 50, "to": 100}, - {"key": "100-200", "from": 100, "to": 200}, - {"key": "200+", "from": 200} - ] - } - ], - "sort_by": "min_price", - "sort_order": "asc", - "size": 24 - }' -``` - -### 场景 2:搜索结果页 - -```bash -# 用户搜索关键词,提供筛选和排序(包含规格分面) -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -H "X-Tenant-ID: 162" \ - -d '{ - "query": "手机", - "language": "zh", - "facets": [ - {"field": "category1_name", "size": 10}, - {"field": "category2_name", "size": 10}, - "specifications.color", - "specifications.size", - { - "field": "min_price", - "type": "range", - "ranges": [ - {"key": "0-50", "to": 50}, - {"key": "50-100", "from": 50, "to": 100}, - {"key": "100+", "from": 100} - ] - } - ], - "size": 20 - }' -``` - -### 场景 2.1:带规格过滤的搜索结果页 - -```bash -# 用户搜索并选择了规格筛选条件 -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -H "X-Tenant-ID: 162" \ - -d '{ - "query": "手机", - "language": "zh", - "filters": { - "category_name": "手机", - "specifications": { - "name": "color", - "value": "white" - } - }, - "facets": [ - "category1_name", - "specifications.color", - "specifications.size" - ], - "size": 20 - }' -``` - -### 场景 3:促销专区 - -```bash -# 显示特定价格区间的商品 -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -H "X-Tenant-ID: 162" \ - -d '{ - "query": "*", - "range_filters": { - "min_price": { - "gte": 50, - "lte": 100 - } - }, - "facets": ["category1_name", "category2_name", "specifications"], - "sort_by": "min_price", - "sort_order": "asc", - "size": 50 - }' -``` - -### 场景 4:新品推荐 - -```bash -# 最近更新的商品 -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -H "X-Tenant-ID: 162" \ - -d '{ - "query": "*", - "range_filters": { - "days_since_last_update": { - "lte": 7 - } - }, - "sort_by": "create_time", - "sort_order": "desc", - "size": 20 - }' -``` - ---- - -## 错误处理 - -### 示例 1:参数错误 - -```bash -# 错误:range_filters 缺少操作符 -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -H "X-Tenant-ID: 162" \ - -d '{ - "query": "手机", - "language": "zh", - "range_filters": { - "min_price": {} - } - }' -``` - -**响应**: -```json -{ - "error": "Validation error", - "detail": "至少需要指定一个范围边界(gte, gt, lte, lt)", - "timestamp": 1699800000 -} -``` - -### 示例 2:空查询 - -```bash -# 错误:query 为空 -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -H "X-Tenant-ID: 162" \ - -d '{ - "query": "" - }' -``` - -**响应**: -```json -{ - "error": "Validation error", - "detail": "query field required", - "timestamp": 1699800000 -} -``` - ---- - -## 性能优化建议 - -### 1. 合理使用分面 - -```bash -# ❌ 不推荐:请求太多分面 -{ - "facets": [ - {"field": "field1", "size": 100}, - {"field": "field2", "size": 100}, - {"field": "field3", "size": 100}, - // ... 10+ facets - ] -} - -# ✅ 推荐:只请求必要的分面 -{ - "facets": [ - {"field": "category.keyword", "size": 15}, - {"field": "vendor.keyword", "size": 15} - ] -} -``` - -### 2. 控制返回数量 - -```bash -# ❌ 不推荐:一次返回太多 -{ - "size": 100 -} - -# ✅ 推荐:分页查询 -{ - "size": 20, - "from": 0 -} -``` - -### 3. 使用适当的过滤器 - -```bash -# ✅ 推荐:先过滤后搜索 -{ - "query": "玩具", - "filters": { - "category.keyword": "玩具" - } -} -``` - ---- - -## 高级技巧 - -### 技巧 1:获取所有类目 - -```bash -# 使用通配符查询 + 分面 -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -H "X-Tenant-ID: 162" \ - -d '{ - "query": "*", - "size": 0, - "facets": [ - {"field": "category.keyword", "size": 100} - ] - }' -``` - -### 技巧 2:价格分布统计 - -```bash -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -H "X-Tenant-ID: 162" \ - -d '{ - "query": "手机", - "language": "zh", - "size": 0, - "facets": [ - { - "field": "price", - "type": "range", - "ranges": [ - {"key": "0-50", "to": 50}, - {"key": "50-100", "from": 50, "to": 100}, - {"key": "100-200", "from": 100, "to": 200}, - {"key": "200-500", "from": 200, "to": 500}, - {"key": "500+", "from": 500} - ] - } - ] - }' -``` - -### 技巧 3:组合多种查询类型 - -```bash -# 布尔表达式 + 过滤器 + 分面 + 排序 -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -H "X-Tenant-ID: 162" \ - -d '{ - "query": "(玩具 OR 游戏) AND 儿童 ANDNOT 电子", - "filters": { - "category.keyword": ["玩具", "益智玩具"] - }, - "range_filters": { - "min_price": {"gte": 20, "lte": 100}, - "days_since_last_update": {"lte": 30} - }, - "facets": [ - {"field": "vendor.keyword", "size": 20} - ], - "sort_by": "min_price", - "sort_order": "asc", - "size": 20 - }' -``` - ---- - -## 测试数据 - -如果你需要测试数据,可以使用以下查询: - -```bash -# 测试类目:玩具 -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -H "X-Tenant-ID: 162" \ - -d '{"query": "玩具", "size": 5}' - -# 测试品牌:乐高 -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -H "X-Tenant-ID: 162" \ - -d '{"query": "brand:乐高", "size": 5}' - -# 测试布尔表达式 -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -H "X-Tenant-ID: 162" \ - -d '{"query": "玩具 AND 乐高", "size": 5}' -``` - ---- - -**文档版本**: 3.0 -**最后更新**: 2024-11-12 -**相关文档**: `API_DOCUMENTATION.md` - diff --git a/docs/TODO-ES能力提升.md b/docs/TODO-ES能力提升.md index 7e20c4c..1250b29 100644 --- a/docs/TODO-ES能力提升.md +++ b/docs/TODO-ES能力提升.md @@ -67,3 +67,4 @@ text_similarity_reranker 用 NLP 模型对 top-k 结果按语义相似度重新 + diff --git a/docs/TODO-prompts.md b/docs/TODO-prompts.md deleted file mode 100644 index 3cdfe44..0000000 --- a/docs/TODO-prompts.md +++ /dev/null @@ -1,39 +0,0 @@ - -1. debug信息展示方面 -在这次重构过程中,你基本上了解了从query析到 ES 搜索,再到重排的全流程。 -在这些环节中,有哪些重要的信息对搜索结果产生较大影响,先回顾,适当的完善、优化日志。 -debug_info也给前端更充分的调试信息,包括,丰富每条结果的ranking debug。目前只有这些 -Ranking Debug -spu_id -ES score -ES normalized -ES norm (rerank input): -Rerank score -Fused score -title.en -title.zh - -还不够,需要详细思考,设计一个更加完整的展示的体系 -比如: -position(重排前的,重排后最终的) -ES打分,从ES检索原始打分、相关的因子(minmax归一化的因子。到底用了哪些因子,你自己关注下,用到的要体现出来,让人知道从原始打分怎么得到的ES norm (rerank input))。另外ES normalized,ES norm,要梳理清楚,如果代码不清晰则改代码,展示不清晰则要梳理展示。 -Rerank这一块,除了rerank_score,输入的信息比如输入的doc模板,被选中的sku及其用于辅助reranker相关性的、加到title后面的后缀 -融合公式这一块,除了fused_score,还要包含rerank_factor, text_factor, knn_factor等因子,以及text_score, knn_score, text_source_score, text_translation_score, … 等变量,要清晰的展示。 -证据 matched_queries - - -耗时记录方面: -1. Stage Timings: 为每个阶段耗时补充起止时间戳。 -2, 我看到total_search大幅度大于上面各个Stage 的总和,可能漏了一些重要的stage,比如「款式意图 SKU 预筛选(StyleSkuSelector.prepare_hits)」,补上这个stage - -但是也要注意,不要影响不带debug_info的请求的性能。 - - -2. 日志方面也需要完善 -从 query 到 ES 再到重排,对结果影响大的信息,语种检测 + index_languages,Query 向量,ES查询的构建过程中所使用的关键的变量、关键的中间结果,rerank相关信息(intput/window,query/doc 模板,融合公式的详细信息和关键的因子、输入原始值以及中间变量、到最终的融合公式的因子,Page fill,sku_filter_dimension等 - - -3. 测试用例方面 -子串匹配(Direct 阶段)是否可能产生误判 — 与尺码/短词强相关 -_is_direct_match 使用 normalized_value in query_text(见 sku_intent_selector.py)。 -词表里 l、m、s、xl,会在常见英文 query 里产生字符级子串命中,例如"l" 是否会匹配 "large …" 里的 l,注意要用分词匹配,检查分词匹配逻辑是否容易产生badcase。 diff --git a/docs/TODO-意图判断-2.md b/docs/TODO-意图判断-2.md deleted file mode 100644 index 2786544..0000000 --- a/docs/TODO-意图判断-2.md +++ /dev/null @@ -1,40 +0,0 @@ - -一、 增加款式意图识别模块 -意图类型: 颜色,尺码(目前只需要支持这两种) - - -二、 意图判断 -- 意图召回层: -每种意图,有一个召回词集合 -对query(包括原始query、各种翻译query 都做匹配) -- 以颜色意图为例: -有一个词表,每一行 都逗号分割,互为同义词,行内第一个为标准化词 -query匹配了其中任何一个词,都认为,具有颜色意图 -匹配规则: 用细粒度、粗粒度分词,看是否有在词表中的。原始query分词、和每种翻译的分词,都要用。 - - -三、 意图使用: - 当前 SKU 置顶逻辑在「分页 + 详情回填」之后 -流程是:run_rerank → 按 from/size 切片 → page fill → _apply_sku_sorting_for_page_hits → ResultFormatter - 要改为: - 1. 有款式意图的时候,才做sku筛选 - 2. sku筛选的时机,改为在reranker之前,对所有内容(rerank输入的所有spus)做sku筛选 - 3. 从仅 option1 扩展到多个维度,识别的意图,包含意图的维度名(color)和维度名的泛化词list(color、颜色、colour、colors...),遍历spu的option1_name,option2_name,option3_name字段,看哪个能匹配上意图的维度名list,哪个匹配上了,则在这个维度筛选。 - 1. 比如匹配到option2_name,那么取每一个sku的option2_values。如果没匹配到任何一个,那么把三个属性值都用空格拼接起来。这个值要记录下来。有两个作用: - 1. 用来跟query匹配,看哪个更query相关性更高,以此进行最优sku筛选,把选出来的sku置顶,并替换spu的image_url - 2. 用来做rerank doc的title补充,从而参与rerank - 4. Rerank doc (有款式意图的时候)要带上属性后缀,拼接到title后面。在调用 run_rerank 前,对每条 hit 生成「用于重排的 doc 文本」(标题 + 可选后缀) - -- sku筛选的规则也要优化: -现在的逻辑是,先做包含的判断,找到第一个 option_value被query包含的,则直接认为匹配。没有匹配的再用embedding相似度。 -改为: - 1. 第一轮:遍历完,如果有且仅有一个被query包含,那么认为匹配。 - 2. 第二轮:如果有多个符合(被query包含),跳到3。如果没有,对每个词都走泛化词表进行匹配。 - 3. 第三轮:如果有多个,那么对这多个,走embedding相关性取最高的。如果一个也没有,则对所有的走embedding相关性取最高的 - 这个sku筛选也需要提取为一个独立的模块。 - -细节备注: -在重排窗口内,第一次 ES 查询会把 _source 裁成「重排模板需要的字段」,默认只有 title 等,不包含 skus / option*_name。因此,有意图的时候,需要给这一次的_source加上 skus / option*_name - -5. TODO: 搜索接口里,results[].skus 不是全量子 SKU:由 sku_filter_dimension 控制在应用层按维度分组折叠,每个「维度取值组合」只保留一条 SKU(组内第一条)。请求未传该字段时,Pydantic 默认是 ["option1"],等价于只按 option1_value 去重;服务端不会读取店铺主题的「主展示维」,需调用方与装修配置对齐并传入正确维度。因此当用户有款式等更细粒度意图、而款式落在 option2/option3(或对应 option*_name)时,若仍用默认只按 option1(常见为颜色)折叠,同一颜色下多种款式只会出现一条代表 SKU,无法从返回的 skus 里拿到该颜色下的全部款式行。(若业务需要全量子款,需传包含对应维度的 sku_filter_dimension,或传 null/[] 跳过折叠——以当前 ResultFormatter 实现为准。) - diff --git a/docs/TODO-意图判断-done.md b/docs/TODO-意图判断-done.md new file mode 100644 index 0000000..91e82f5 --- /dev/null +++ b/docs/TODO-意图判断-done.md @@ -0,0 +1,268 @@ + + +增加款式意图识别模块。意图类型: 颜色,尺码(目前只需要支持这两种) + +一、 意图判断 +- 意图召回层: +每种意图,有一个召回词集合 +对query(包括原始query、各种翻译query 都做匹配) +- 以颜色意图为例: +有一个词表,每一行 都逗号分割,互为同义词,行内第一个为标准化词 +query匹配了其中任何一个词,都认为,具有颜色意图 +匹配规则: 用细粒度、粗粒度分词,看是否有在词表中的。原始query分词、和每种翻译的分词,都要用。 + +二、 意图使用: + 当前 SKU 置顶逻辑在「分页 + 详情回填」之后 +流程是:run_rerank → 按 from/size 切片 → page fill → _apply_sku_sorting_for_page_hits → ResultFormatter + 要改为: + 1. 有款式意图的时候,才做sku筛选 + 2. sku筛选的时机,改为在reranker之前,对所有内容(rerank输入的所有spus)做sku筛选 + 3. 从仅 option1 扩展到多个维度,识别的意图,包含意图的维度名(color)和维度名的泛化词list(color、颜色、colour、colors...),遍历spu的option1_name,option2_name,option3_name字段,看哪个能匹配上意图的维度名list,哪个匹配上了,则在这个维度筛选。 + 1. 比如匹配到option2_name,那么取每一个sku的option2_values。如果没匹配到任何一个,那么把三个属性值都用空格拼接起来。这个值要记录下来。有两个作用: + 1. 用来跟query匹配,看哪个更query相关性更高,以此进行最优sku筛选,把选出来的sku置顶,并替换spu的image_url + 2. 用来做rerank doc的title补充,从而参与rerank + 4. Rerank doc (有款式意图的时候)要带上属性后缀,拼接到title后面。在调用 run_rerank 前,对每条 hit 生成「用于重排的 doc 文本」(标题 + 可选后缀) + +- sku筛选的规则也要优化: +现在的逻辑是,先做包含的判断,找到第一个 option_value被query包含的,则直接认为匹配。没有匹配的再用embedding相似度。 +改为: + 1. 第一轮:遍历完,如果有且仅有一个被query包含,那么认为匹配。 + 2. 第二轮:如果有多个符合(被query包含),跳到3。如果没有,对每个词都走泛化词表进行匹配。 + 3. 第三轮:如果有多个,那么对这多个,走embedding相关性取最高的。如果一个也没有,则对所有的走embedding相关性取最高的 + 这个sku筛选也需要提取为一个独立的模块。 + +细节备注: +intent 考虑由 QueryParser 编排、具体实现拆成独立模块,主义好,现有的分词等基础设施的复用,缺失的英文分词可以补充。 +在重排窗口内,第一次 ES 查询会把 _source 裁成「重排模板需要的字段」,默认只有 title 等,不包含 skus / option*_name。因此,有意图的时候,需要给这一次的_source加上 skus / option*_name + +先仔细理解需求,查看代码,深度思考应该如何设计,和当前的系统较好的融合,给出统一的设计,可以根据需要适当改造当前的实现,降低整个系统的复杂度,提高模块化程度,而不是打补丁。修改后的最终状态应该是要足够简单、清晰、无冗余和分叉,模块间低耦合。多步思考确认最佳施工方案之后才进行代码修改。 + +5. TODO: 搜索接口里,results[].skus 不是全量子 SKU:由 sku_filter_dimension 控制在应用层按维度分组折叠,每个「维度取值组合」只保留一条 SKU(组内第一条)。请求未传该字段时,Pydantic 默认是 ["option1"],等价于只按 option1_value 去重;服务端不会读取店铺主题的「主展示维」,需调用方与装修配置对齐并传入正确维度。因此当用户有款式等更细粒度意图、而款式落在 option2/option3(或对应 option*_name)时,若仍用默认只按 option1(常见为颜色)折叠,同一颜色下多种款式只会出现一条代表 SKU,无法从返回的 skus 里拿到该颜色下的全部款式行。(若业务需要全量子款,需传包含对应维度的 sku_filter_dimension,或传 null/[] 跳过折叠——以当前 ResultFormatter 实现为准。) + + + + + +GPT5.4 主要思路: +我准备按这条线改,尽量把逻辑收束成两个新模块而不是继续堆在 Searcher 里: + +QueryParser 负责产出统一的“款式意图分析结果”,包含:命中的意图类型、标准化值、命中的 query 变体、分词结果、维度候选词。 +独立的 SKU 选择模块负责:维度解析、候选值抽取、三阶段匹配、选中 SKU 后的主图替换,以及生成 rerank 用的属性后缀。 +Searcher 只编排:解析 query -> 首次 ES 召回 -> 若有款式意图则对 rerank 窗口内全部 hits 做 SKU 预筛选并补充 rerank doc 文本 -> run_rerank -> 分页 -> page fill -> ResultFormatter。这样旧的页内 SKU 置顶逻辑可以下沉或删除,避免前后两个阶段重复做同一件事。我再补看一下你最近写的意图草稿文档,确认有没有现成词表示例或边界条件,避免我这边把词表格式和你预期做偏。 + + + + + +意图匹配的性能优化: +上面一版实现,性能上完全无法接受。因此进行了一轮策略简化 + +prompt +style_sku_prepare_hits阶段耗时太长。请根据需求,思考优化的方法,给出性能优化的方案。 +1. _select_by_embedding,有缓存吗,option_value的值是有限的,之前已经算过的,就不用再算了。不仅仅是embedding相似的结果,整个option_value的匹配结果,是有包含、还是没包含,相似度多少,都不用重新计算。比如之前已经有一个sku的某个属性值叫做“卡其色”,已经算出来是否文本匹配了,那么不需要再去做文本匹配。如果已经算出来向量的相似度,那么不需要再去取向量以及计算相似度。 +2. 匹配可以适当的优化: +匹配流程简化: +1)找到第一个文本匹配的,如果有直接匹配成功。不需要考虑匹配多个的情况。 +2)如果全部都没有匹配,那么进行embedding筛选。 + +匹配规则: +option_name的匹配,直接看规范化后的option_name是不是意图维度的泛化词之一(比如颜色、color、colour),如果没有匹配的,现在应该是把所有维度都算上,这样匹配成本和比较成本太高了,去掉这些逻辑,这种情况不需要加后缀、不需要选择sku。 +ption_value的匹配。意图检测的时候,有匹配的query中的命中的词,这个词被包含在属性值中,那么就算匹配。属性值被包含在query(包括翻译文本)中,也算匹配。提高匹配的覆盖率。 + +3. 这一阶段得到sku选择的结果即可(选中的sku的id,也可以为空值表示没找到匹配成功的,这种情况不需要拼接title后缀给重排输入),但是不用着急做image_url的替换和sku的置顶。等最后填充的时候判断有选中sku的时候直接做替换和置顶即可。 +请你思考如何进行设计,提高性能的时候不带来复杂度的提升,可以适当的重构以降低修改后的代码行数。 +@search/sku_intent_selector.py @query/style_intent.py + + +修改结果: +核心变化在 `search/sku_intent_selector.py` 和 `search/searcher.py`。 + +现在的策略是: +- `option_name` 只做规范化后 alias 精确匹配;任何意图维度没 resolve 到字段,就直接不选 SKU、不加 suffix。 +- `option_value` 文本匹配改成顺序扫描 SKU,找到第一个命中的就直接返回,不再收集多个候选再比较。 +- 文本匹配规则支持两种: + - query 命中的意图词被属性值包含 + - 属性值被 query/translation 包含 +- 只有当所有 SKU 都没有文本命中时,才做 embedding 选择。 +- `prepare_hits()` 现在只产出决策和 `_style_rerank_suffix`,不再提前改 `_source`;真正的 `image_url` 替换和 SKU 置顶统一后移到 `apply_precomputed_decisions()`。 +- 加了请求级缓存,避免同一个 `option_value`/`selection_text` 在一次请求里重复做文本判断、取向量和算相似度。 +- 顺手删掉了旧的 generalized match / fallback_text 路径,代码比之前更短也更直。 + +另外补了回归测试,覆盖了: +- 翻译文本命中后正确选 SKU +- 多个文本命中时取第一个 +- `option_name` 不命中 alias 时不做 SKU 选择 +- 无文本命中时走 embedding fallback + +验证过: +- `pytest tests/test_search_rerank_window.py -q` 通过 +- 变更文件 lint 无报错 + + +------------------------------ + + + +--- + +## 1. 现状(与需求的差距) + +**流水线**(`search/searcher.py`)大致是: + +1. `QueryParser.parse` → `ParsedQuery`(含 `translations`、`query_tokens` 等) +2. 组 ES 查询;若在重排窗口内,第一次查询把 `_source` 裁成「重排模板所需字段」(`_resolve_rerank_source_filter`) +3. ES 搜索 → `run_rerank`(`rerank_client.build_docs_from_hits` 用 `{title}` 等拼 doc) +4. 按 `from/size` 切片 → **page fill** 用 `ids` 查询把当前页 `_source` 补全 +5. `_apply_sku_sorting_for_page_hits`(仅 **option1**,先子串包含命中第一个,否则全量 option1 embedding) +6. `ResultFormatter`(`sku_filter_dimension` 只做**展示层**按维度折叠 SKU,与置顶逻辑独立) + + +**与需求冲突但必须一起解决的一点**:page fill 会用 ES 拉回来的 `_source`**整份覆盖**当前 hit(约 841–842 行)。若在 rerank **之前**只改内存里的 `skus` 顺序/`image_url`,**不**在 fill 后再处理一次,最终响应会被覆盖掉。因此「rerank 前对所有 window 内 hit 做 SKU 决策」和「用户看到的最终列表」之间,必须有一条**明确的数据契约**(见下文 §4)。 + +--- + +## 2. 模块划分(建议:`intent` + `sku_intent` 两层) + +避免继续在 `Searcher` 里堆方法,建议新建小包,职责清晰、由 `Searcher` 编排。 + +| 模块 | 职责 | +|------|------| +| **`query/intent/`**(或 `search/intent/`,二选一以「离谁更近」为准;更推荐 **`query/intent`**,因为输入完全是 query 侧事实) | 加载词表、**意图召回**、多 query 变体 + 粗细分词、输出结构化 **`IntentProfile`** | +| **`search/sku_intent/`**(或 `intent/sku_selection.py`) | 根据 `IntentProfile` 解析 **option1/2/3** 哪一维、生成每 SKU 的**匹配文本**、三轮匹配规则、embedding 批处理、对 `_source` 做 **promote + image_url** | +| **`search/rerank_client.py`(薄扩展)** | 支持「每条 hit 的 doc 文本」:模板扩展或 **显式传入 per-hit 字符串列表**,避免把业务塞进 format 字符串 | + +**`IntentProfile`(概念模型)建议包含**: + +- `active_intents: Set[Literal["color","size"]]`(可扩展) +- 每种意图:`canonical_terms`(命中行的标准词)、`matched_surface_forms`(可选,用于 debug) +- **维度别名**:如 color → `{"color","颜色","colour",...}`(配置或独立小词表) +- 原始用于匹配的 token 集合:每个 query 变体 ×(细粒度 | 粗粒度),便于日志与单测 + +**词表**: + +- **意图召回表**:每行逗号分隔同义词,首词标准化;颜色、尺码各一份(路径放 `config/` 或 `resources/intent/` + `config.yaml` 指路径)。 +- **SKU 第二轮「泛化」表**(对 **option 取值** 做同义扩展):与意图召回表分开,避免语义混在一起。 + +--- + +## 3. 意图判断(与 `QueryParser` 的衔接) + +需求:对 **原始 query + 各类翻译** 都做匹配;**细粒度 + 粗粒度** 分词。 + +现状: + +- `ParsedQuery` 里 **`query_tokens` 只对 rewritten 后的 `query_text` 跑了一次 HanLP**(`query_parser.py` 269–274 行附近),**没有**对 `original_query`、各 `translations` 的 token 缓存。 +- 已有 **`simple_tokenize_query`**(粗粒度)在 `query_parser.py`。 + +**建议**: + +- 在 **`IntentDetector.detect(parsed_query, tokenizer_fn)`** 内统一生成「query 变体列表」:至少包含 `original_query`、`query_normalized`、`rewritten_query`、`translations` 的值(与当前 `_build_sku_query_texts` 思路一致,但升级为**结构化**)。 +- 细粒度:复用 `QueryParser._get_query_tokens`(需把该方法暴露为公开 API 或注入同一 HanLP callable),对每个变体字符串调用。 +- 粗粒度:对每个变体调用 `simple_tokenize_query`。 +- 匹配逻辑:**任意变体 × 任意粒度** 的 token 落在「标准化 → 同义词闭包」上即视为命中该意图(与你描述的行内同义一致)。 + +**可选优化**:在 `parse()` 里顺带产出 `intent_profile`,减少一次遍历;但为控制 `QueryParser` 体积,更稳妥的是 **parse 之后**单独调 `IntentDetector`,依赖清晰。 + +--- + +## 4. 流水线改造(与 page fill 的契约) + +目标顺序变为: + +`ES(window)`(有意图时 `_source` 含 `skus` + `option*_name`) +→ **对每个 hit:SKU 决策 + 生成 rerank 用后缀/全文** +→ `run_rerank`(doc = 标题 + 后缀) +→ 切片 +→ page fill +→ **最终响应前再应用一次 SKU 决策(或与 prefetch 结果合并)** +→ `ResultFormatter` + +**为何最后还要一次?** 因为 page fill 会覆盖 `_source`,rerank 前内存里的 `skus` 顺序不能当作最终真相。 + +**推荐契约(降低复杂度)**: + +1. **Rerank 前**:对 window 内每个 hit 计算 `SkuIntentDecision`(至少包含:`option_slot` 1/2/3、`candidate_sku_index` 或 `sku_id`、`rerank_suffix` 字符串)。可挂在 hit 的**非 ES 字段**上,例如 `hit["_intent_sku"] = {...}`(或只存 `rerank_doc_text` 全文)。 +2. **`run_rerank`**:`build_docs_from_hits` 若发现 hit 上已有 `rerank_doc_text`(或 `style_suffix` + 模板),则优先使用,否则走原模板。 +3. **Page fill 之后**:对**当前页** hit 再调用**同一** `SkuIntentSelector.apply(source, parsed_query, intent_profile)`(或根据 `_id` 合并 prefetch 决策)。这样最终 `image_url` / SKU 顺序与 rerank 一致,且不被 fill 冲掉。 + +若担心算两次 embedding:**第一次**在 window 全量上算 query 向量 + option 向量;第二次仅对当前页且可带缓存(按 `embed_key` 去重),一般量很小。 + +**不在重排窗口内**:没有「rerank 前全 window」这一步;可在 **ResultFormatter 前**对当前页 `es_hits` 用同一 `SkuIntentSelector`(仅当有意图时),与「有意图才做 SKU 筛选」一致。 + +--- + +## 5. `_resolve_rerank_source_filter` 与 ES 字段 + +需求:有意图时预取需包含 `skus`、`option1_name`、`option2_name`、`option3_name`。 + +建议签名扩展为: + +`_resolve_rerank_source_filter(doc_template, intent_profile: Optional[IntentProfile])` + +- 若 `intent_profile` 非空且含 color/size(或任意「款式意图」),在 `includes` 中**合并**上述字段(并与模板解析出的 `title` 等取并集)。 +- 注意与全局 `source_fields` 的 tri-state 语义(`_apply_source_filter`)是否冲突:若租户配置 `_source` 白名单且不含 `skus`,需定义优先级——**建议**:「款式意图所需字段」作为**最低保证**合并进本次请求的 fetch includes,或在文档中写明限制。 + +--- + +## 6. 多维度 option 与「未匹配维度名」 + +需求逻辑可落到纯函数: + +1. 对每个意图类型,有 **维度别名集合**(如 color)。 +2. 依次与 `option1_name`、`option2_name`、`option3_name`(字符串,注意多语言:与 indexer 一致,可能是纯英文或中文)做 **casefold / 规范化** 后匹配别名表。 +3. 命中则该 SKU 行的匹配字段为 `option{k}_value`;用于 embedding key 时继续用 `name:value` 形式(沿用现有 `_sku_option1_embedding_key` 思路,泛化为 `option_slot`)。 +4. **若三个 name 都不匹配意图维度**:用 `option1_value`、`option2_value`、`option3_value` **空格拼接**成一条「兜底描述字符串」,供: + - 与 query 的包含/泛化/embedding 比较; + - 作为 `rerank_suffix` 的一部分(若你希望无明确维度时仍加强 rerank)。 + +**多意图同时存在**(如同时颜色+尺码):需要在产品层定规则,例如: + +- 只对「主意图」排序(配置优先级 color > size),或 +- 要求两个维度都满足的 SKU 优先,否则退化为单意图。 + +实现上可在 `SkuIntentSelector` 输入 `List[IntentType]` 与策略枚举,避免写死 if-else 散落。 + +--- + +## 7. 三轮 SKU 匹配规则(独立模块内) + +从当前「第一个包含就返回」改为: + +1. **第一轮**:统计「option 匹配文本被 **整条 query 文本** **包含**」的 SKU(或对每个 query 变体分别计,再合并——建议与你现有 `_build_sku_query_texts` 对齐);**若恰好 1 个** → 选中。 +2. **第二轮**:若 0 个,对每个 SKU 的候选词走 **取值泛化表**(同义词行),再跑包含判断;仍统计「多个 / 零个」。 +3. **第三轮**: + - 若 **多个** 满足包含(第一轮或第二轮)→ 仅在这多个上算 embedding,取相似度最高; + - 若 **仍 0 个** → 对 **全部** SKU 算 embedding,取最高。 + +实现上保持 **批量 encode**(与当前 `option1_values_to_encode` 去重逻辑类似),只是把「embed_key」从固定 option1 改为按 slot 动态生成。 + +--- + +## 8. `sku_filter_dimension`(API)与意图的关系 + +- **`sku_filter_dimension`**:客户端指定「结果里 SKU 列表如何按维度折叠」,在 `ResultFormatter._filter_skus_by_dimensions` 中实现。 +- **意图 SKU 置顶**:服务端根据 query 推断维度与取值,改顺序与主图。 + +建议约定: + +- **置顶 / 换图**仅在意图开启时执行; +- **`sku_filter_dimension` 仍只影响返回 SKU 条数结构**;若与意图维度冲突(例如意图命中 color,客户端只按 size 折叠),应用**文档说明优先级**:常见做法是 **先意图置顶,再 filter**(或相反,需在 PRD 写清)。 + +避免在 `ResultFormatter` 里再猜意图;意图结论由上游传入或在 Formatter 前已完成 `_source` 调整。 + +--- + +## 9. 配置与观测 + +- `config.yaml`:`intent.enabled`、`intent.lexicon_paths`、`intent.dimension_aliases`(或按类型分块)。 +- `RequestContext` / `debug`:写入 `intent_profile`、`sku_intent_decision`、rerank 用的 doc 摘要,便于与 `docs/TODO-意图判断.md` 对齐。 + +--- + +## 10. 小结 + +- **核心架构**:**`IntentDetector`(query 侧)** + **`SkuIntentSelector`(search 侧)** + **`run_rerank` 的 per-hit doc 覆盖** + **`_resolve_rerank_source_filter` 条件 includes**。 +- **必须处理 page fill 覆盖 `_source`**:rerank 前决策与 **fill 后再 apply 一次**(或等价合并策略),否则会出现「重排用了带后缀的 doc、返回结果却是未置顶 SKU」的不一致。 +- **与现有系统融合点**:`ParsedQuery` 变体列表、HanLP + `simple_tokenize_query`、`TextEmbeddingEncoder`、`ResultFormatter` / `sku_filter_dimension` 的边界清晰,避免把意图逻辑复制到 `api/` 层。 + +若你后续希望把「多意图优先级」或「rerank 后缀格式」定成唯一产品规则,可以在实现前写进同一份 spec,模块接口会很好稳定下来。 \ No newline at end of file diff --git a/docs/TODO-意图判断.md b/docs/TODO-意图判断.md deleted file mode 100644 index ce70c55..0000000 --- a/docs/TODO-意图判断.md +++ /dev/null @@ -1,99 +0,0 @@ - - -增加款式意图识别模块。意图类型: 颜色,尺码(目前只需要支持这两种) - -一、 意图判断 -- 意图召回层: -每种意图,有一个召回词集合 -对query(包括原始query、各种翻译query 都做匹配) -- 以颜色意图为例: -有一个词表,每一行 都逗号分割,互为同义词,行内第一个为标准化词 -query匹配了其中任何一个词,都认为,具有颜色意图 -匹配规则: 用细粒度、粗粒度分词,看是否有在词表中的。原始query分词、和每种翻译的分词,都要用。 - -二、 意图使用: - 当前 SKU 置顶逻辑在「分页 + 详情回填」之后 -流程是:run_rerank → 按 from/size 切片 → page fill → _apply_sku_sorting_for_page_hits → ResultFormatter - 要改为: - 1. 有款式意图的时候,才做sku筛选 - 2. sku筛选的时机,改为在reranker之前,对所有内容(rerank输入的所有spus)做sku筛选 - 3. 从仅 option1 扩展到多个维度,识别的意图,包含意图的维度名(color)和维度名的泛化词list(color、颜色、colour、colors...),遍历spu的option1_name,option2_name,option3_name字段,看哪个能匹配上意图的维度名list,哪个匹配上了,则在这个维度筛选。 - 1. 比如匹配到option2_name,那么取每一个sku的option2_values。如果没匹配到任何一个,那么把三个属性值都用空格拼接起来。这个值要记录下来。有两个作用: - 1. 用来跟query匹配,看哪个更query相关性更高,以此进行最优sku筛选,把选出来的sku置顶,并替换spu的image_url - 2. 用来做rerank doc的title补充,从而参与rerank - 4. Rerank doc (有款式意图的时候)要带上属性后缀,拼接到title后面。在调用 run_rerank 前,对每条 hit 生成「用于重排的 doc 文本」(标题 + 可选后缀) - -- sku筛选的规则也要优化: -现在的逻辑是,先做包含的判断,找到第一个 option_value被query包含的,则直接认为匹配。没有匹配的再用embedding相似度。 -改为: - 1. 第一轮:遍历完,如果有且仅有一个被query包含,那么认为匹配。 - 2. 第二轮:如果有多个符合(被query包含),跳到3。如果没有,对每个词都走泛化词表进行匹配。 - 3. 第三轮:如果有多个,那么对这多个,走embedding相关性取最高的。如果一个也没有,则对所有的走embedding相关性取最高的 - 这个sku筛选也需要提取为一个独立的模块。 - -细节备注: -intent 考虑由 QueryParser 编排、具体实现拆成独立模块,主义好,现有的分词等基础设施的复用,缺失的英文分词可以补充。 -在重排窗口内,第一次 ES 查询会把 _source 裁成「重排模板需要的字段」,默认只有 title 等,不包含 skus / option*_name。因此,有意图的时候,需要给这一次的_source加上 skus / option*_name - -先仔细理解需求,查看代码,深度思考应该如何设计,和当前的系统较好的融合,给出统一的设计,可以根据需要适当改造当前的实现,降低整个系统的复杂度,提高模块化程度,而不是打补丁。修改后的最终状态应该是要足够简单、清晰、无冗余和分叉,模块间低耦合。多步思考确认最佳施工方案之后才进行代码修改。 - -5. TODO: 搜索接口里,results[].skus 不是全量子 SKU:由 sku_filter_dimension 控制在应用层按维度分组折叠,每个「维度取值组合」只保留一条 SKU(组内第一条)。请求未传该字段时,Pydantic 默认是 ["option1"],等价于只按 option1_value 去重;服务端不会读取店铺主题的「主展示维」,需调用方与装修配置对齐并传入正确维度。因此当用户有款式等更细粒度意图、而款式落在 option2/option3(或对应 option*_name)时,若仍用默认只按 option1(常见为颜色)折叠,同一颜色下多种款式只会出现一条代表 SKU,无法从返回的 skus 里拿到该颜色下的全部款式行。(若业务需要全量子款,需传包含对应维度的 sku_filter_dimension,或传 null/[] 跳过折叠——以当前 ResultFormatter 实现为准。) - - - - - -GPT5.4 主要思路: -我准备按这条线改,尽量把逻辑收束成两个新模块而不是继续堆在 Searcher 里: - -QueryParser 负责产出统一的“款式意图分析结果”,包含:命中的意图类型、标准化值、命中的 query 变体、分词结果、维度候选词。 -独立的 SKU 选择模块负责:维度解析、候选值抽取、三阶段匹配、选中 SKU 后的主图替换,以及生成 rerank 用的属性后缀。 -Searcher 只编排:解析 query -> 首次 ES 召回 -> 若有款式意图则对 rerank 窗口内全部 hits 做 SKU 预筛选并补充 rerank doc 文本 -> run_rerank -> 分页 -> page fill -> ResultFormatter。这样旧的页内 SKU 置顶逻辑可以下沉或删除,避免前后两个阶段重复做同一件事。我再补看一下你最近写的意图草稿文档,确认有没有现成词表示例或边界条件,避免我这边把词表格式和你预期做偏。 - - - - - -意图匹配的性能优化: -上面一版实现,性能上完全无法接受。因此进行了一轮策略简化 - -prompt -style_sku_prepare_hits阶段耗时太长。请根据需求,思考优化的方法,给出性能优化的方案。 -1. _select_by_embedding,有缓存吗,option_value的值是有限的,之前已经算过的,就不用再算了。不仅仅是embedding相似的结果,整个option_value的匹配结果,是有包含、还是没包含,相似度多少,都不用重新计算。比如之前已经有一个sku的某个属性值叫做“卡其色”,已经算出来是否文本匹配了,那么不需要再去做文本匹配。如果已经算出来向量的相似度,那么不需要再去取向量以及计算相似度。 -2. 匹配可以适当的优化: -匹配流程简化: -1)找到第一个文本匹配的,如果有直接匹配成功。不需要考虑匹配多个的情况。 -2)如果全部都没有匹配,那么进行embedding筛选。 - -匹配规则: -option_name的匹配,直接看规范化后的option_name是不是意图维度的泛化词之一(比如颜色、color、colour),如果没有匹配的,现在应该是把所有维度都算上,这样匹配成本和比较成本太高了,去掉这些逻辑,这种情况不需要加后缀、不需要选择sku。 -ption_value的匹配。意图检测的时候,有匹配的query中的命中的词,这个词被包含在属性值中,那么就算匹配。属性值被包含在query(包括翻译文本)中,也算匹配。提高匹配的覆盖率。 - -3. 这一阶段得到sku选择的结果即可(选中的sku的id,也可以为空值表示没找到匹配成功的,这种情况不需要拼接title后缀给重排输入),但是不用着急做image_url的替换和sku的置顶。等最后填充的时候判断有选中sku的时候直接做替换和置顶即可。 -请你思考如何进行设计,提高性能的时候不带来复杂度的提升,可以适当的重构以降低修改后的代码行数。 -@search/sku_intent_selector.py @query/style_intent.py - - -修改结果: -核心变化在 `search/sku_intent_selector.py` 和 `search/searcher.py`。 - -现在的策略是: -- `option_name` 只做规范化后 alias 精确匹配;任何意图维度没 resolve 到字段,就直接不选 SKU、不加 suffix。 -- `option_value` 文本匹配改成顺序扫描 SKU,找到第一个命中的就直接返回,不再收集多个候选再比较。 -- 文本匹配规则支持两种: - - query 命中的意图词被属性值包含 - - 属性值被 query/translation 包含 -- 只有当所有 SKU 都没有文本命中时,才做 embedding 选择。 -- `prepare_hits()` 现在只产出决策和 `_style_rerank_suffix`,不再提前改 `_source`;真正的 `image_url` 替换和 SKU 置顶统一后移到 `apply_precomputed_decisions()`。 -- 加了请求级缓存,避免同一个 `option_value`/`selection_text` 在一次请求里重复做文本判断、取向量和算相似度。 -- 顺手删掉了旧的 generalized match / fallback_text 路径,代码比之前更短也更直。 - -另外补了回归测试,覆盖了: -- 翻译文本命中后正确选 SKU -- 多个文本命中时取第一个 -- `option_name` 不命中 alias 时不做 SKU 选择 -- 无文本命中时走 embedding fallback - -验证过: -- `pytest tests/test_search_rerank_window.py -q` 通过 -- 变更文件 lint 无报错 diff --git a/docs/Untitled b/docs/Untitled deleted file mode 100644 index 2603c24..0000000 --- a/docs/Untitled +++ /dev/null @@ -1,38 +0,0 @@ - -一、 增加款式意图识别模块 -意图类型: 颜色,尺码(目前只需要支持这两种) - - -二、 意图判断 -- 意图召回层: -每种意图,有一个召回词集合 -对query(包括原始query、各种翻译query 都做匹配) -- 以颜色意图为例: -有一个词表,每一行 都逗号分割,互为同义词,行内第一个为标准化词 -query匹配了其中任何一个词,都认为,具有颜色意图 -匹配规则: 用细粒度、粗粒度分词,看是否有在词表中的。原始query分词、和每种翻译的分词,都要用。 - - -三、 意图使用: - 当前 SKU 置顶逻辑在「分页 + 详情回填」之后 -流程是:run_rerank → 按 from/size 切片 → page fill → _apply_sku_sorting_for_page_hits → ResultFormatter - 要改为: - 1. 有款式意图的时候,才做sku筛选 - 2. sku筛选的时机,改为在reranker之前,对所有内容(rerank输入的所有spus)做sku筛选 - 3. 从仅 option1 扩展到多个维度,识别的意图,包含意图的维度名(color)和维度名的泛化词list(color、颜色、colour、colors...),遍历spu的option1_name,option2_name,option3_name字段,看哪个能匹配上意图的维度名list,哪个匹配上了,则在这个维度筛选。 - 1. 比如匹配到option2_name,那么取每一个sku的option2_values。如果没匹配到任何一个,那么把三个属性值都用空格拼接起来。这个值要记录下来。有两个作用: - 1. 用来跟query匹配,看哪个更query相关性更高,以此进行最优sku筛选,把选出来的sku置顶,并替换spu的image_url - 2. 用来做rerank doc的title补充,从而参与rerank - 4. Rerank doc (有款式意图的时候)要带上属性后缀,拼接到title后面。在调用 run_rerank 前,对每条 hit 生成「用于重排的 doc 文本」(标题 + 可选后缀) - -- sku筛选的规则也要优化: -现在的逻辑是,先做包含的判断,找到第一个 option_value被query包含的,则直接认为匹配。没有匹配的再用embedding相似度。 -改为: - 1. 第一轮:遍历完,如果有且仅有一个被query包含,那么认为匹配。 - 2. 第二轮:如果有多个符合(被query包含),跳到3。如果没有,对每个词都走泛化词表进行匹配。 - 3. 第三轮:如果有多个,那么对这多个,走embedding相关性取最高的。如果一个也没有,则对所有的走embedding相关性取最高的 - 这个sku筛选也需要提取为一个独立的模块。 - -细节备注: -intent 考虑由 QueryParser 编排、具体实现拆成独立模块,主义好,现有的分词等基础设施的复用,缺失的英文分词可以补充。 -在重排窗口内,第一次 ES 查询会把 _source 裁成「重排模板需要的字段」,默认只有 title 等,不包含 skus / option*_name。因此,有意图的时候,需要给这一次的_source加上 skus / option*_name diff --git a/docs/reranker-共享前缀批量推理.md b/docs/reranker-共享前缀批量推理.md deleted file mode 100644 index bf51fe9..0000000 --- a/docs/reranker-共享前缀批量推理.md +++ /dev/null @@ -1,460 +0,0 @@ - -## 共享前缀+批量后缀 - -**"共享前缀批量推理"**(Shared-Prefix Batch Inference): -- 输入结构:`[Prefix A] + [Suffix B1]`、`[Prefix A] + [Suffix B2]`、`[Prefix A] + [Suffix B3]`... -- 痛点:Prefix A被重复计算成千上万次,浪费算力和时间 -- 理想方案:Prefix A只推理一次,KV Cache复用给所有后缀 - ---- - -## 一、顶级推荐方案 - -### 1. **FlashInfer + Cascade Attention** ⭐ 最强技术 -- **核心创新**:Cascade Inference(级联推理) -- **性能提升**:相比vLLM的PageAttention,在32K token共享前缀、batch size 256场景下可达 **31倍加速** -- **原理**: - - 将Attention计算分解为两个阶段: - 1. **多查询阶段**:对共享前缀使用Multi-Query Kernel,只计算一次,结果存入Shared Memory - 2. **批量解码阶段**:对每个唯一后缀使用Batch Decode Kernel - - 使用结合律算子合并部分Attention状态,类似FlashAttention的分块策略 -- **适用场景**:文档QA、系统提示词复用、RAG批量检索 -- **集成**:已集成到SGLang和vLLM中作为后端 -- **GitHub**: https://github.com/flashinfer-ai/flashinfer - -### 2. **SGLang + RadixAttention** ⭐ 最实用框架 -- **核心创新**:RadixTree(基数树)管理KV Cache -- **自动前缀复用**:无需手动配置,自动识别共享前缀并复用KV Cache -- **性能**:相比vLLM、LMQL等基线系统,结构化工作负载上可达 **6.4倍吞吐量提升** 和 **3.7倍延迟降低** -- **关键特性**: - - **In-Batch Prefix Caching**:同一batch内自动共享前缀(如你的A+B1, A+B2场景) - - **Multi-Item Scoring (MIS)**:LinkedIn用于推荐排序的优化,将多个候选项合并为单次前向传播 - - **Zero-Overhead CPU Scheduler**:GPU计算时CPU并行准备下一batch,利用率接近100% -- **特别适合**:Agent系统、工具链、RAG应用 -- **GitHub**: https://github.com/sgl-project/sglang - -### 3. **vLLM + Automatic Prefix Caching** ⭐ 最成熟稳定 -- **核心机制**:基于哈希表的块级前缀缓存 -- **工作原理**: - - 将KV Cache按块(默认16 tokens)哈希 - - 新请求先查哈希表,命中则直接复用,只计算新tokens - - 使用LRU策略管理缓存 eviction -- **使用方式**: - ```python - from vllm import LLM, SamplingParams - # 启用prefix caching - llm = LLM(model="your-model", enable_prefix_caching=True) - - # 第一次调用缓存前缀 - outputs = llm.generate(long_prefix + prompt_1, sampling_params) - # 第二次调用自动命中缓存,prefix部分零计算 - outputs = llm.generate(long_prefix + prompt_2, sampling_params) - ``` -- **注意事项**:vLLM 0.6.3之前调度器未考虑缓存命中率,高并发长序列场景可能性能下降,建议升级到0.6.5+ - ---- - -## 二、其他重要方案 - -### 4. **TensorRT-LLM + In-Flight Batching** -- **优势**:NVIDIA官方优化,与FlashInfer深度集成 -- **特性**: - - 支持Prefix Caching(具体实现闭源,但概念类似vLLM) - - XQA Kernel(Flash Attention 3变体)优化内存访问 - - 层融合技术减少中间结果存储 -- **性能**:在共享前缀数据集上,吞吐量提升 **~34.7%**,TPOT降低 **~20.9%** -- **适用**:NVIDIA GPU生产环境,追求极致性能 - -### 5. **LMDeploy + TurboMind** -- **定位**:纯C++引擎,消除Python开销 -- **性能**:与SGLang相当,在H100上可达 **~16,200 tok/s**(vLLM为12,553 tok/s) -- **优化**:支持KV Cache量化(8-bit)、Continuous Batching -- **适用**:高吞吐生产部署,对延迟敏感的场景 - -### 6. **Daft + Dynamic Prefix Bucketing**(大数据场景) -- **创新点**:动态前缀分桶 + 流式Continuous Batching -- **解决痛点**: - - 全局排序分组会导致GPU空闲 - - 动态分桶在推理同时进行前缀分组,实现流水线 -- **性能**:128 GPU集群上,20万prompts(1.28亿tokens)处理速度提升 **50.7%** -- **适用**:大规模离线批处理(如数据标注、合成数据生成) - ---- - -## 三、针对你的具体场景建议 - -### 场景1:在线服务(RAG检索、实时重排序) -**推荐栈**:**SGLang** 或 **vLLM + FlashInfer后端** - -```python -# SGLang示例:自动前缀复用 -import sglang as sgl - -@sgl.function -def rerank(s, query, docs): - # query是共享前缀,docs是批量后缀 - s += "Query: " + query + "\n" - s += "Document: " + sgl.arg(docs) + "\nRelevance:" - s += sgl.gen("score", max_tokens=5) - -# 批量执行,自动共享query部分的KV Cache -docs = ["doc1 content", "doc2 content", "doc3 content", ...] # 成千上万个 -state = rerank.run_batch( - [{"query": "user query", "docs": d} for d in docs], - max_new_tokens=5 -) -``` - -### 场景2:离线批量处理(数据标注、索引构建) -**推荐栈**:**Daft** 或 **FlashInfer原生API** - -```python -# Daft示例:动态前缀分桶 -import daft -from daft.functions import prompt - -df = daft.from_pydict({ - "query": ["shared query"] * 10000, - "doc": ["doc1", "doc2", ...] # 不同后缀 -}) - -df = df.with_column("score", - prompt( - df["query"] + "\n" + df["doc"], - provider="vllm-prefix-caching", # 利用前缀缓存 - model="your-model" - ) -) -``` - -### 场景3:Embedding/Reranker模型(Bi-Encoder/Cross-Encoder) -**推荐栈**:**Sentence-Transformers优化** + **ONNX/TensorRT** - -```python -# Cross-Encoder批量重排序优化 -from sentence_transformers import CrossEncoder -import numpy as np - -class OptimizedReranker: - def __init__(self, model_name="cross-encoder/ms-marco-MiniLM-L-6-v2"): - self.model = CrossEncoder(model_name, max_length=512, device="cuda") - - def rerank_batch(self, query, documents, batch_size=32): - # 构建所有pairs:[query, doc1], [query, doc2], ... - pairs = [[query, doc] for doc in documents] - - # 单次批量推理,自动共享query的编码计算 - scores = self.model.predict( - pairs, - batch_size=batch_size, - convert_to_numpy=True - ) - return np.argsort(scores)[::-1] -``` - ---- - -## 四、性能优化关键技巧 - -### 1. **Prompt构造策略** -- **对齐块边界**:确保共享前缀长度是KV Cache块大小(通常是16或32)的整数倍,避免部分块重计算 -- **静态前缀前置**:将不变的指令、系统提示放在最前面 - -### 2. **Batch构造策略** -- **前缀分桶(Prefix Bucketing)**:将相同前缀的请求分到同一batch,最大化缓存命中率 -- **长度排序**:同batch内按后缀长度排序,减少padding浪费 - -### 3. **硬件/精度优化** -- **FP16/BF16**:混合精度推理,吞吐量提升1.5-2倍 -- **KV Cache量化**:8-bit量化减少内存带宽压力,提升20%吞吐 -- **CUDA Graphs**:捕获静态计算图,减少CPU开销(适合固定batch size场景) - ---- - -## 五、方案对比总结 - -| 方案 | 前缀复用方式 | 适用场景 | 性能提升 | 易用性 | 成熟度 | -|------|-------------|---------|---------|--------|--------| -| **FlashInfer** | Cascade Attention | 超长前缀(32K+) | 31x | 中(需集成) | 高 | -| **SGLang** | RadixTree自动缓存 | Agent/RAG/工具链 | 6.4x | 高 | 高 | -| **vLLM** | 哈希块缓存 | 通用在线服务 | 2-5x | 高 | 极高 | -| **TensorRT-LLM** | 内部优化 | NVIDIA生产环境 | 1.3-2x | 中 | 高 | -| **LMDeploy** | TurboMind C++引擎 | 低延迟高吞吐 | 1.3x | 中 | 高 | -| **Daft** | 动态分桶 | 大规模离线批处理 | 50%+ | 高 | 中 | - ---- - -## 六、推荐实施路径 - -1. **快速验证**:先用 **vLLM** 开启`enable_prefix_caching=True`验证效果,零代码改动 -2. **性能突破**:切换到 **SGLang**,利用RadixAttention和自动batch优化,获得显著性能提升 -3. **极致优化**:对超长前缀场景,使用 **FlashInfer** 的Cascade Attention内核,或集成到自定义推理引擎中 - -需要针对具体框架的详细配置代码或部署架构建议吗? - - - -## 天然支持Prefix Caching的Reranker模型清单 - -最新版本 (v2 系列):混合架构 -在 BGE-Reranker-v2 中,BAAI 为了提升模型上限,引入了不同的底座: - -BGE-Reranker-v2-m3: 依然保持了 Encoder-only 架构(基于 XLM-RoBERTa),支持多语言和长文本,具有极高的性价比。 - -BGE-Reranker-v2-Gemma/Llama: 采用了 Decoder-only 架构。 - -这些模型是基于 Gemma-2b 或 Llama-3-8B 等大语言模型(LLM)微调而来的。 - -虽然原生 LLM 是单向(Causal)注意力的,但在作为 Reranker 使用时,模型通常会通过特殊的 Prompt 引导,并取最后一个 Token 的输出经过一个线性层来计算相关性分数。 - - -### 1. **BGE-Reranker-V2/V2.5 系列** ⭐ 强烈推荐 -基于Gemma/MiniCPM等Decoder-only架构,FlagEmbedding官方实现已优化 - -| 模型 | 架构 | 参数量 | 特点 | -|------|------|--------|------| -| `BAAI/bge-reranker-v2-gemma` | Gemma-2B (Decoder-only) | 2B | 多语言强,基础版 | -| `BAAI/bge-reranker-v2-minicpm-layerwise` | MiniCPM-2B (Decoder-only) | 2B | **支持层选择**,可截断到第24层加速 | -| `BAAI/bge-reranker-v2.5-gemma2-lightweight` | Gemma2-9B (Decoder-only) | 9B | **Token压缩+层选择**,极致效率 | - -**Prefix Caching友好特性** : -- 输入格式:`[Query A] + [SEP] + [Document B] + [Prompt]` -- Query部分作为前缀,可被所有Document共享 -- 官方代码中已实现`compute_score_single_gpu`的batch处理,自动对齐长度排序减少padding - -**使用示例**: -```python -from FlagEmbedding import FlagAutoReranker - -# 启用vLLM后端 + Prefix Caching -reranker = FlagAutoReranker.from_finetuned( - 'BAAI/bge-reranker-v2-gemma', - model_class='decoder-only-base', - use_fp16=True, - devices=['cuda:0'] -) - -# 批量推理:Query自动复用KV Cache -pairs = [ - ('what is panda?', 'The giant panda is a bear species...'), # A+B1 - ('what is panda?', 'Pandas are popular zoo animals.'), # A+B2 (Query复用) - ('what is panda?', 'Pandas eat bamboo and live in China.'), # A+B3 (Query复用) -] -scores = reranker.compute_score(pairs, batch_size=32) -``` - -### 2. **Qwen3-Reranker 系列** ⭐ 国产最强 -基于Qwen3 Decoder-only架构,阿里云官方支持 - -| 模型 | 架构 | 参数量 | 特点 | -|------|------|--------|------| -| `Qwen/Qwen3-Reranker-0.6B` | Qwen3-0.6B (Decoder-only) | 0.6B | 超轻量,快速 | -| `Qwen/Qwen3-Reranker-4B` | Qwen3-4B (Decoder-only) | 4B | 性能均衡 | -| `Qwen/Qwen3-Reranker-8B` | Qwen3-8B (Decoder-only) | 8B | 精度最高 | - -**架构细节** : -- **纯Decoder-only架构**:使用因果注意力,天然支持Prefix Caching -- **输入模板**: - ``` - <|im_start|>system - You are a helpful assistant. - <|im_end|> - <|im_start|>user - Query: {query} - Document: {document} - Does the document answer the query? Please answer Yes or No. - <|im_end|> - <|im_start|>assistant - Yes - ``` -- **输出**:只生成"Yes"或"No"的logits,作为相关性分数 - -**vLLM部署优化** : -```bash -# 启动vLLM服务,启用Prefix Caching -python -m vllm.entrypoints.openai.api_server \ - --model Qwen/Qwen3-Reranker-8B \ - --tensor-parallel-size 1 \ - --dtype half \ - --max-model-len 32768 \ - --enable-prefix-caching # 关键参数!提速40% -``` - -### 3. **Jina-Reranker-V3** ⭐ 创新架构 -基于Qwen3-0.6B的Listwise重排序器,支持跨Document注意力 - -| 模型 | 架构 | 参数量 | 特点 | -|------|------|--------|------| -| `jinaai/jina-reranker-v3` | Qwen3-0.6B (Decoder-only) | 0.6B | **Listwise**,单次处理64个docs | - -**独特优势** : -- **"Last but Not Late"交互**:在单个context window中同时处理Query+所有Documents -- **跨Document注意力**:通过因果注意力实现Document间交互,捕捉相对相关性 -- **Prefix Caching优化**:Query放在序列开头,可被所有Document共享 - -**输入格式** : -```xml -<|im_start|>system -You are a search relevance expert... -<|im_end|> -<|im_start|>user -Rank the passages based on their relevance to query: [QUERY] - -[DOC_1]<|doc_emb|> -[DOC_2]<|doc_emb|> -... -[DOC_k]<|doc_emb|> - -[QUERY]<|query_emb|> -<|im_end|> -``` - -**性能**:BEIR nDCG@10达61.94,超过Qwen3-Reranker-4B,体积小6倍 - -### 4. **E5-Mistral / NV-Embed-v2 / SFR-Embedding-Mistral** -基于Mistral-7B Decoder-only架构的Embedding/Reranker - -| 模型 | 架构 | 用途 | 特点 | -|------|------|------|------| -| `intfloat/e5-mistral-7b-instruct` | Mistral-7B (Decoder-only) | Embedding | 指令微调,支持多任务 | -| `nvidia/NV-Embed-v2` | Mistral-7B (Decoder-only) | Embedding | 潜在注意力层优化 | -| `Salesforce/SFR-Embedding-Mistral` | Mistral-7B (Decoder-only) | Embedding | 长上下文优化 | - -**注意**:这些主要是**Embedding模型**(Bi-encoder),但可作为Reranker使用(如通过余弦相似度)。若需Cross-encoder式重排序,需配合其他技术。 - -### 5. **RankGPT / RankZephyr / RankLLaMA** -基于LLM的生成式重排序器 - -| 模型 | 架构 | 特点 | -|------|------|------| -| `RankGPT` (GPT-3.5/4) | Decoder-only API | 通过Prompt让LLM判断相关性 | -| `RankZephyr` | Zephyr-7B (Decoder-only) | 蒸馏自RankGPT,开源可部署 | -| `RankLLaMA` | LLaMA-2/3 (Decoder-only) | 本地部署,隐私友好 | - -**实现方式**: -- 使用LLM的logits或生成"Yes/No"判断相关性 -- 完全基于Decoder-only架构,天然支持Prefix Caching - ---- - -## 不支持Prefix Caching的Reranker(双向架构) - -### ❌ **BGE-Reranker-V1 / BGE-Reranker-Base/Large** -- **架构**:基于XLM-RoBERTa(Encoder-only,双向注意力) -- **问题**:Query和Document拼接后`[CLS]` token的表示依赖于整个序列,无法分离缓存 -- **适用场景**:轻量级、短文本,对延迟不敏感 - -### ❌ **Cross-Encoder (BERT-based)** -- **架构**:BERT/RoBERTa等Encoder-only模型 -- **问题**: - - 每个Query-Document对必须**联合编码** - - 前缀的KV Cache与后续token强耦合,无法复用 - - 计算复杂度O((|Q|+|D|)²),无法分解为O(|Q|²) + O(|D|²) - -### ❌ **ColBERT / ColPali / Late Interaction模型** -- **架构**:基于BERT的双向编码 + 后期MaxSim交互 -- **问题**: - - **独立编码,但双向注意力**:Query和Document分别编码,但各自内部仍是双向 - - **无法Prefix Cache**:虽然Query可独立编码,但Document的编码不依赖于Query,所以不存在"共享前缀"场景 - - **优化点**:Document可**预计算并离线存储**,Query实时编码,然后做MaxSim - - **本质区别**:这是"离线预计算"而非"Prefix Caching",适用于Document固定、Query变化的场景 - -**ColBERT的优化策略** : -```python -# ColBERT流程:Document预计算(离线) + Query实时编码(在线) -document_embeddings = encode_documents(docs) # 离线,一次性 -query_embedding = encode_query(query) # 在线,每次查询 -scores = maxsim(query_embedding, document_embeddings) # 轻量级交互 -``` - ---- - -## 实际部署建议 - -### 场景1:高并发在线服务(Query多变,Document固定) -**推荐**:**Jina-Reranker-V3** 或 **Qwen3-Reranker + vLLM Prefix Caching** - -```python -# vLLM配置优化 -from vllm import LLM, SamplingParams - -llm = LLM( - model="Qwen/Qwen3-Reranker-8B", - enable_prefix_caching=True, # 关键! - max_num_seqs=256, # 高并发 - max_model_len=32768 -) - -# 批量构造Prompts:共享Query前缀 -query = "What is the capital of France?" -docs = ["Paris is the capital...", "France is a country...", "Berlin is the capital of Germany..."] - -prompts = [ - f"Query: {query}\nDocument: {doc}\nRelevant:" - for doc in docs -] - -# vLLM自动识别共享前缀,只计算一次Query的KV Cache -sampling_params = SamplingParams(max_tokens=1, temperature=0) -outputs = llm.generate(prompts, sampling_params) -``` - -### 场景2:离线批量重排序(Query固定,Document多变) -**推荐**:**ColBERT / ColPali**(Document预计算策略) - -```python -from rankify.models.reranking import Reranking -from rankify.dataset.dataset import Document, Question, Context - -# ColBERT:Document预计算,Query实时编码 -model = Reranking(method='colbert_ranker', model_name='Colbert') - -# Documents已预计算并存储 -document = Document( - question=Question("What is RAG?"), - contexts=[Context(text=doc, id=i) for i, doc in enumerate(docs)] -) - -# 只需编码Query,然后MaxSim计算 -model.rank([document]) -``` - -### 场景3:极致性能 + 高精度 -**推荐**:**BGE-Reranker-V2.5-Gemma2-Lightweight**(Token压缩 + 层选择) - -```python -from FlagEmbedding import LightWeightFlagLLMReranker - -reranker = LightWeightFlagLLMReranker( - 'BAAI/bge-reranker-v2.5-gemma2-lightweight', - devices=["cuda:0"], - use_fp16=True -) - -# 综合优化:层截断 + Token压缩 -scores = reranker.compute_score( - pairs, - cutoff_layers=[28], # 只用前28层 - compress_ratio=4, # Token压缩4倍 - compress_layers=[24, 40] # 特定层压缩 -) -``` - ---- - -## 总结对比表 - -| 模型 | 架构 | Prefix Caching | 适用场景 | 性能/效率 | -|------|------|---------------|---------|----------| -| **BGE-Reranker-V2/V2.5** | Decoder-only ✅ | ✅ 原生支持 | 多语言、生产环境 | 高 | -| **Qwen3-Reranker** | Decoder-only ✅ | ✅ vLLM支持 | 中文优先、高精度 | 极高 | -| **Jina-Reranker-V3** | Decoder-only ✅ | ✅ Listwise优化 | 跨Doc交互、Top-K排序 | 极高 | -| **E5-Mistral/NV-Embed** | Decoder-only ✅ | ✅ 需配合框架 | Embedding+轻量重排 | 高 | -| **RankGPT/Zephyr** | Decoder-only ✅ | ✅ API/本地 | 生成式判断 | 中 | -| **BGE-Reranker-V1** | Encoder-only ❌ | ❌ 不支持 | 轻量、短文本 | 低 | -| **ColBERT/ColPali** | 双向+后期交互 | ❌ 不适用(预计算替代) | Document固定场景 | 中 | - -**最终建议**: -- 如果追求**Prefix Caching加速** + **高精度**:选择 **Qwen3-Reranker-8B** 或 **BGE-Reranker-V2.5**,配合 **vLLM + FlashInfer** 部署 -- 如果需要**跨Document比较**(Listwise):选择 **Jina-Reranker-V3** -- 如果**Document固定且量大**:选择 **ColBERT** 预计算策略(虽非Prefix Caching,但效率类似) \ No newline at end of file diff --git a/docs/亚马逊到店匠格式转换分析.md b/docs/亚马逊到店匠格式转换分析.md deleted file mode 100644 index 8ebeea3..0000000 --- a/docs/亚马逊到店匠格式转换分析.md +++ /dev/null @@ -1,373 +0,0 @@ -# 亚马逊格式到店匠格式转换 - 核心工作内容分析 - -## 一、概述 - -本项目实现了从**亚马逊格式Excel数据**到**店匠(Shoplazza)商品导入模板**的格式转换,主要处理商品的多款式(变体)结构和属性字段映射。 - -**核心脚本**:`scripts/amazon_xlsx_to_shoplazza_xlsx.py` - ---- - -## 二、父子款式处理(M/P/S 结构转换) - -### 2.1 输入格式(亚马逊) - -- **ASIN**:变体ID(SKU级别) -- **父ASIN**:父商品ID(SPU级别) -- 一个父ASIN可以包含多个ASIN(多个变体) - -### 2.2 输出格式(店匠) - -店匠模板定义了三种商品属性类型: - -1. **S(单一款式)**:只有一个变体的商品 - - 输出:**1行** - - 包含所有商品信息(标题、价格、库存等) - -2. **M(主商品)+ P(子款式)**:包含多个变体的商品 - - 输出:**1行M + N行P** - - **关键约束**:同一商品的P行必须紧跟在M行后面(模板导入强约束) - -### 2.3 转换策略 - -```python -# 核心逻辑(简化版) -for 父ASIN in 所有父ASIN: - variants = 获取该父ASIN下的所有ASIN - - if len(variants) == 1: - 生成 S 行(单一款式) - else: - 生成 M 行(主商品)+ 多个 P 行(子款式) -``` - -### 2.4 关键处理点 - -#### 1. 父ASIN排序 -- 确保父ASIN对应的变体在列表最前面 -- 如果找不到父ASIN对应的变体,根据配置决定是否丢弃整个SPU - -#### 2. 标题一致性检查 -- 同一SPU下的所有变体标题必须一致 -- 如果发现不一致: - - 选项1:丢弃标题不一致的SKU(默认) - - 选项2:修正为统一的主商品标题 - -#### 3. M行与P行的字段分工 - -**M行(主商品)填写**: -- ✅ 商品标题、描述、SEO信息 -- ✅ 专辑、标签、供应商信息 -- ✅ 商品主图 -- ✅ 款式维度名(款式1/2/3的key) -- ❌ 不填:价格、库存、重量等SKU级字段(保持为空更安全) - -**P行(子款式)填写**: -- ✅ 商品标题(与M行一致) -- ✅ 款式维度值(款式1/2/3的value) -- ✅ 价格、商品SKU(ASIN)、库存 -- ✅ 重量、尺寸 -- ✅ 子款式图(可选) -- ❌ 不填:描述、SEO、专辑等SPU级字段(保持为空) - ---- - -## 三、属性字段处理(款式维度解析) - -### 3.1 问题背景 - -亚马逊格式中,变体的"颜色/尺码"等信息**并不拆成多个列**,而是集中在 `SKU` 字符串里: - -``` -示例1: "Size: One Size | Color: Black" -示例2: "Color: Red | Style: 2-Pack" -``` - -而店匠模板需要: -- **M行**:`款式1/款式2/款式3` 填写**维度名**(如 Size、Color、Material) -- **P行**:`款式1/款式2/款式3` 填写**维度值**(如 One Size、Black、Cotton) - -### 3.2 SKU解析逻辑 - -```python -def parse_sku_options(sku_text): - """ - 解析 SKU 列,提取 key:value 对 - 输入: "Size: One Size | Color: Black" - 输出: {"Size": "One Size", "Color": "Black"} - """ - # 1. 按 | 分割 - parts = sku_text.split("|") - - # 2. 按 : 拆成 key/value - for part in parts: - if ":" in part: - key, value = part.split(":", 1) - result[key.strip()] = value.strip() - - return result -``` - -### 3.3 维度选择策略(最多3个维度) - -店匠模板只提供 `款式1~3` 三个维度,因此需要从多个变体中**智能选择最多3个维度**: - -#### 优先级规则 - -1. **按预设优先级排序**: - ```python - PREFERRED_OPTION_KEYS = [ - "Size", "Color", "Style", "Pattern", "Material", - "Flavor", "Scent", "Pack", "Pack of", ... - ] - ``` - -2. **按出现频次排序**:统计每个key在所有变体中的出现次数 - -3. **综合排序**: - ```python - def key_sort(k): - return ( - 预设优先级(越小越优先), - -出现频次(越大越优先), - 字母顺序(作为最后排序依据) - ) - ``` - -#### 退化处理 - -如果解析不到任何 key/value,则退化为单维度: -- M行:`款式1 = "Variant"` -- P行:`款式1 = ASIN`(使用ASIN作为维度值) - -### 3.4 维度映射示例 - -**输入数据**(3个变体): -``` -变体1: SKU = "Size: S | Color: Red" -变体2: SKU = "Size: M | Color: Red" -变体3: SKU = "Size: S | Color: Blue" -``` - -**解析结果**: -- 维度统计:Size出现3次,Color出现3次 -- 选择维度:Size(优先级1)、Color(优先级2) -- 最多3个,所以选择前2个 - -**输出格式**: -``` -M行: - 款式1 = "Size" - 款式2 = "Color" - 款式3 = "" - -P行1: - 款式1 = "S" - 款式2 = "Red" - 款式3 = "" - -P行2: - 款式1 = "M" - 款式2 = "Red" - 款式3 = "" - -P行3: - 款式1 = "S" - 款式2 = "Blue" - 款式3 = "" -``` - ---- - -## 四、字段映射总览 - -### 4.1 核心字段映射 - -| 店匠字段 | 亚马逊字段 | 处理逻辑 | -|---------|-----------|---------| -| **商品spu** | `父ASIN` | 无父ASIN则用ASIN | -| **商品SKU** | `ASIN` | 直接映射 | -| **商品标题*** | `商品标题` | 截断至255字符 | -| **商品图片*** | `商品主图` | URL直接映射 | -| **商品售价*** | `prime价格($)` 或 `价格($)` | 优先prime价格 | -| **创建时间** | `上架时间` | 日期格式转换(补齐时分秒) | -| **商品描述** | `商品标题` + `详细参数` | HTML拼接 | -| **专辑名称** | `大类目` | 无则取`类目路径`第一段 | -| **标签** | `品牌,大类目,小类目` | 逗号拼接 | -| **商品重量/重量单位** | `商品重量(单位换算)` | 解析数值和单位(g/kg/lb/oz) | -| **尺寸信息** | `商品尺寸` | 解析前三段数字,拼成 `L,W,H`(英寸) | - -### 4.2 特殊字段处理 - -#### 1. 价格处理 -```python -price = prime价格($) or 价格($) or 9.99 # 默认值9.99 -``` - -#### 2. 库存处理 -- 亚马逊数据源通常**没有库存** -- 脚本默认给每个变体固定库存:**100** - -#### 3. 日期格式转换 -```python -输入: "2018-05-09" 或 datetime对象 -输出: "2018-05-09 00:00:00" # 补齐时分秒 -``` - -#### 4. 重量解析 -```python -输入: "68.04 g" 或 "0.15 pounds" -输出: (68.04, "g") 或 (0.15, "lb") -``` - -#### 5. 尺寸解析 -```python -输入: "7.9 x 7.9 x 2 inches" -输出: "7.9,7.9,2" # L,W,H格式 -``` - -#### 6. SEO URL Handle生成 -```python -输入: "Legendary Whitetails Men's Buck Camp Flannel Shirt" -输出: "products/legendary-whitetails-mens-buck-camp-flannel-shirt" -# 规则:转小写、去特殊字符、空格转横线 -``` - ---- - -## 五、重要工作内容总结 - -### 5.1 数据结构转换 - -1. **父子关系识别**:从扁平化的ASIN列表识别出SPU-SKU层级关系 -2. **分组策略**:按父ASIN分组,决定生成S还是M+P结构 -3. **行序保证**:确保同一SPU的M+P行连续,不被打断 - -### 5.2 属性字段解析 - -1. **SKU字符串解析**:从非结构化字符串中提取key:value对 -2. **维度智能选择**:从多个可能的维度中选择最重要的3个 -3. **优先级算法**:综合考虑预设优先级、出现频次、字母顺序 - -### 5.3 字段映射与转换 - -1. **格式转换**:日期、价格、重量、尺寸等格式标准化 -2. **默认值填充**:库存、价格等缺失字段的默认值策略 -3. **数据清洗**:标题截断、HTML转义、URL生成等 - -### 5.4 数据质量控制 - -1. **标题一致性检查**:确保同一SPU下所有变体标题一致 -2. **父ASIN验证**:检查父ASIN是否存在于变体列表中 -3. **错误处理**:提供配置选项决定是修正还是丢弃异常数据 - -### 5.5 性能优化 - -1. **批量处理**:支持多文件批量转换 -2. **文件拆分**:按最大行数自动拆分输出文件(保证同一SPU不拆分) -3. **快速读写**:使用`iter_rows(values_only=True)`和write_only模式提升性能 - ---- - -## 六、关键技术难点 - -### 6.1 维度选择算法 - -**挑战**:从多个变体的SKU字符串中,智能选择最重要的3个维度 - -**解决方案**: -- 统计所有变体中每个key的出现频次 -- 结合预设优先级列表(Size > Color > Style > ...) -- 综合排序选择前3个 - -### 6.2 数据一致性保证 - -**挑战**:确保同一SPU下的所有变体数据一致 - -**解决方案**: -- 标题一致性检查和修正 -- 父ASIN排序保证 -- M行和P行的字段分工明确 - -### 6.3 文件拆分策略 - -**挑战**:按最大行数拆分文件,但不能拆分同一SPU - -**解决方案**: -- 先按SPU分组生成所有行 -- 拆分时以SPU为单位,不拆分单个SPU的行 - ---- - -## 七、扩展点 - -### 7.1 可配置项 - -- 库存默认值(当前100) -- 价格默认值(当前9.99) -- 上架/收税/物流策略(当前硬编码Y/N) - -### 7.2 可增强功能 - -- **更强的多款式解析**:如果SKU字段不规范,可从`详细参数`中提取Color/Size -- **图片策略优化**:P行可改为使用M行合并的多图(逗号拼接) -- **元字段支持**:支持店匠的元字段导入 - ---- - -## 八、使用示例 - -### 8.1 小批量验证 - -```bash -python scripts/amazon_xlsx_to_shoplazza_xlsx.py \ - --input-dir data/mai_jia_jing_ling/products_data \ - --template docs/商品导入模板.xlsx \ - --output data/mai_jia_jing_ling/amazon_shoplazza_import_SAMPLE.xlsx \ - --max-files 1 --max-products 50 -``` - -### 8.2 全量转换 - -```bash -python scripts/amazon_xlsx_to_shoplazza_xlsx.py \ - --input-dir data/mai_jia_jing_ling/products_data \ - --template docs/商品导入模板.xlsx \ - --output data/mai_jia_jing_ling/amazon_shoplazza_import_ALL.xlsx -``` - -### 8.3 自动拆分文件 - -```bash -python scripts/amazon_xlsx_to_shoplazza_xlsx.py \ - --input-dir data/mai_jia_jing_ling/products_data \ - --template docs/商品导入模板.xlsx \ - --output data/mai_jia_jing_ling/amazon_shoplazza_import_SPLIT.xlsx \ - --max-rows-per-output 40000 -``` - ---- - -## 九、总结 - -亚马逊格式到店匠格式的转换,核心工作包括: - -1. **父子款式结构转换**:从ASIN/父ASIN关系转换为M/P/S结构 -2. **属性字段解析**:从SKU字符串中提取并智能选择款式维度 -3. **字段映射与转换**:40+个字段的格式转换和默认值处理 -4. **数据质量控制**:一致性检查、错误处理、数据清洗 -5. **性能优化**:批量处理、文件拆分、快速读写 - -这是一个典型的**数据格式转换ETL任务**,涉及数据结构重组、字符串解析、智能算法选择等多个技术领域。 - - - - - - - - - - - - diff --git a/docs/亚马逊格式数据转店匠商品导入模板.md b/docs/亚马逊格式数据转店匠商品导入模板.md deleted file mode 100644 index aef35e3..0000000 --- a/docs/亚马逊格式数据转店匠商品导入模板.md +++ /dev/null @@ -1,176 +0,0 @@ - -## 数据pipeline - -1. 从 卖家精灵“关键词选品”选出服装品类的关键词,每个关键词会带上top10 asin -2. 汇总asin,做成 每行 1000个 asin(因为卖家精灵每次最多查询1000个)。 这样的话,总共34w+商品,美国站点是12w商品(120行)。 做了个浏览器自动化执行的工具,导出120个文件,然后下载下来。 -3. 用本文档介绍的工具,将卖家精灵导出的excel转化为店匠的导入文件。 -```bash - python scripts/amazon_xlsx_to_shoplazza_xlsx.py --input-dir data/mai_jia_jing_ling/products_data --template docs/商品导入模板.xlsx -``` -5. 导入到店匠后,自动同步到mysql -6. mysql 到 ES : -```bash -python scripts/recreate_and_import.py \ - --tenant-id xxxxxxxxxx -``` - - -## 亚马逊格式数据 → 店匠(Shoplazza)商品导入模板:转换说明 - -本仓库支持把 `data/mai_jia_jing_ling/products_data/*.xlsx`(**亚马逊格式导出**)转换为店匠后台可导入的 `docs/商品导入模板.xlsx` 格式。 - -对应脚本: -- **主入口**:`scripts/amazon_xlsx_to_shoplazza_xlsx.py` -- **历史兼容**:`scripts/competitor_xlsx_to_shoplazza_xlsx.py`(仅名称过时,逻辑一致) -- **模板写入复用工具**:`scripts/shoplazza_excel_template.py` - ---- - -## 一、输入数据(亚马逊格式 xlsx)的关键字段 - -以 `Competitor-US-Last-30-days-363464.xlsx` 为例(文件名不影响:内容是亚马逊维度字段): - -- **ASIN**:变体 id(我们视为 `sku_id`,会写入模板的 `商品SKU`) -- **父ASIN**:父商品 id(我们视为 `spu_id`/`product_id`,会写入模板的 `商品spu`,并用于分组 M/P) -- **商品标题**:商品标题(写入 `商品标题*`、SEO标题等) -- **SKU**:亚马逊变体描述字符串(关键:解析出多款式维度) - - 示例:`Size: One Size | Color: Black` -- **商品主图**:图片 URL(用于 `商品图片*` / `商品主图`) -- **价格($)** / **prime价格($)**:价格(用于 `商品售价*` / `商品原价`) -- **详细参数**:详情参数串(用于拼接 `商品描述`) -- **上架时间**:用于 `创建时间` -- **类目路径/大类目/小类目/品牌/商品详情页链接/品牌链接**:用于专辑、标签、SEO、供应商URL、备注等 -- **商品重量(单位换算)/商品重量/商品尺寸**:用于 `商品重量/重量单位/尺寸信息` - -> 注意:该数据源通常**没有库存**,脚本默认给每个变体一个固定库存(当前默认 100),以满足导入后的可用性。 - ---- - -## 二、输出数据(店匠导入模板)的核心规则(M / P / S) - -店匠模板在 `docs/商品导入模板说明.md` 中定义了三种商品属性(`商品属性*`): - -- **S(单一款式)**:一个商品只有一个变体(只有 1 个 ASIN) - - 输出 **1 行** -- **M(主商品)+ P(子款式)**:一个父商品(父ASIN)包含多个变体(多个 ASIN) - - 输出 **1 行 M + N 行 P** - - 且 **同一商品的 P 行必须紧跟在 M 行后面**(模板导入强约束) - -本仓库的转换策略: -- 对每个 `父ASIN` 分组: - - **分组 size = 1** → 生成 `S` - - **分组 size > 1** → 生成 `M` + 多个 `P` - ---- - -## 三、多款式(变体)是如何构造的(最关键部分) - -### 1)为什么 “SKU” 列是关键 - -亚马逊格式里,变体的“颜色/尺码”等信息往往并不拆成多个列,而是集中在 `SKU` 字符串里,例如: - -- `Size: One Size | Color: Black` -- `Color: Red | Style: 2-Pack` - -店匠模板的多款式需要: -- **M 行**:`款式1/款式2/款式3` 写“维度名”(例如 Size / Color / Material) -- **P 行**:`款式1/款式2/款式3` 写“维度值”(例如 One Size / Black / Cotton) - -### 2)脚本如何从 SKU 解析出维度(key/value) - -脚本会把 `SKU` 以 `|` 分割,再用 `:` 拆成 key/value: - -- 输入:`Size: One Size | Color: Black` -- 解析结果:`{ "Size": "One Size", "Color": "Black" }` - -### 3)如何从多个变体里选出 “最多3个维度” - -店匠模板只提供 `款式1~3` 三个维度,因此脚本会在一个 `父ASIN` 组内统计 key 的出现频次,并按优先级挑选最多 3 个维度: - -- 优先级大致为:`Size`、`Color`、`Style`、`Pattern`、`Material` …… -- 如果一个组里解析不到任何 key/value,则退化为单维度:`Variant` - - M 行 `款式1 = Variant` - - P 行 `款式1 = ASIN` - -### 4)M 行与 P 行分别填什么(避免导入报错) - -根据模板说明,脚本遵循以下分工: - -- **M 行(主商品)**: - - 填:标题/描述/SEO/专辑/标签/主图/款式维度名 - - 不填:价格、库存、重量等 SKU 级字段(保持为空更安全) -- **P 行(子款式)**: - - 填:款式维度值、价格、商品SKU(ASIN)、库存、重量、尺寸、(可选)子款式图 - - 不填:描述/SEO/专辑/供应商等 SPU 级字段(保持为空) - ---- - -## 四、字段映射总览(高频字段) - -- **商品spu** ← `父ASIN`(无父ASIN则用 ASIN) -- **商品SKU** ← `ASIN` -- **商品标题\*** ← `商品标题` -- **商品图片\*** / **商品主图** ← `商品主图` -- **商品售价\*** ← `prime价格($)` 优先,否则 `价格($)` -- **创建时间** ← `上架时间`(仅日期时补齐为 `YYYY-MM-DD 00:00:00`) -- **商品描述** ← `商品标题` + `详细参数`(以 HTML 拼接) -- **专辑名称** ← `大类目`(无则取 `类目路径` 第一段) -- **标签** ← `品牌,大类目,小类目` -- **商品重量/重量单位** ← 优先解析 `商品重量(单位换算)`(如 `68.04 g`) -- **尺寸信息** ← 解析 `商品尺寸` 前三段数字(英寸)拼成 `L,W,H` - ---- - -## 五、如何运行(生成导入文件) - -### 1)先小批量验证(推荐) - -```bash -python scripts/amazon_xlsx_to_shoplazza_xlsx.py \ - --input-dir data/mai_jia_jing_ling/products_data \ - --template docs/商品导入模板.xlsx \ - --output data/mai_jia_jing_ling/amazon_shoplazza_import_SAMPLE.xlsx \ - --max-files 1 --max-products 50 -``` - -### 性能提示(很重要) - -- 旧实现如果用 `ws.cell()` 逐格读取/写入,处理 1 个 xlsx 就可能非常慢(分钟级甚至更久)。 -- 当前脚本已经使用 **`iter_rows(values_only=True)`** + write_only 模式做快速读写(只有这一种实现方式,保持简单)。 - -### 2)生成全量 - -```bash -python scripts/amazon_xlsx_to_shoplazza_xlsx.py \ - --input-dir data/mai_jia_jing_ling/products_data \ - --template docs/商品导入模板.xlsx \ - --output data/mai_jia_jing_ling/amazon_shoplazza_import_ALL.xlsx -``` - -### 3)按 Excel 行数自动拆分文件 - -当单个导出超过一定行数时,可以通过 `--max-rows-per-output` 控制单个 Excel 的最大总行数(包含模板头部行): - -```bash -python scripts/amazon_xlsx_to_shoplazza_xlsx.py \ - --input-dir data/mai_jia_jing_ling/products_data \ - --template docs/商品导入模板.xlsx \ - --output data/mai_jia_jing_ling/amazon_shoplazza_import_SPLIT.xlsx \ - --max-rows-per-output 40000 -``` - -- 若结果只需要 1 个文件,仍输出为 `amazon_shoplazza_import_SPLIT.xlsx` -- 若需要拆分为多个文件,则追加 `_part1/_part2/...` 后缀: - - `amazon_shoplazza_import_SPLIT_part1.xlsx` - - `amazon_shoplazza_import_SPLIT_part2.xlsx` -- **同一个 SPU(同一父ASIN 的 M+P 或 S 行)保证不会被拆到不同文件中** - ---- - -## 六、可扩展点(后续常见需求) - -- **库存/上架/收税策略参数化**:目前是脚本默认值(Y/N/100),可按目标店铺规则改为命令行参数。 -- **更强的多款式解析**:如果未来亚马逊格式 `SKU` 不规范,可补充从 `详细参数` 里挖出 `Color/Size`。 -- **图片策略**:目前 P 行用各自 `商品主图`;也可改为 M 行合并多图(逗号拼接)。 - - diff --git a/docs/系统设计文档.md b/docs/系统设计文档.md deleted file mode 100644 index f2668a4..0000000 --- a/docs/系统设计文档.md +++ /dev/null @@ -1,894 +0,0 @@ -# 搜索引擎通用化开发进度 - -## 项目概述 - -对后端搜索技术 做通用化。 -通用化的本质 是 对于各种业务数据、各种检索需求,都可以 用少量定制+配置化 来实现效果。 - - -**通用化的本质**:对于各种业务数据、各种检索需求,都可以用少量定制+配置化来实现效果。 - ---- - -## 1. 原始数据层的约定 - -### 1.1 店匠主表 - -所有租户共用以下主表: -- `shoplazza_product_sku` - SKU级别商品数据 -- `shoplazza_product_spu` - SPU级别商品数据 - -### 1.2 索引结构(SPU维度) - -**索引架构**: -- 每个租户使用独立的 Elasticsearch 索引(索引名称由 `get_tenant_index_name(tenant_id)` 动态生成) -- 索引粒度:SPU级别(每个文档代表一个SPU) -- 数据隔离:通过“分索引 + `tenant_id` 字段”双重隔离(索引级 + 文档级) -- 嵌套结构:每个SPU文档包含嵌套的`skus`数组 - -**索引文档结构**: -```json -{ - "tenant_id": "1", - "spu_id": "123", - "title.zh": "蓝牙耳机", - "title.en": "Bluetooth Headphones", - "brief.zh": "高品质蓝牙耳机", - "brief.en": "High-quality Bluetooth headphones", - "category_name": "电子产品", - "category_path.zh": "电子产品/音频设备/耳机", - "category_path.en": "Electronics/Audio/Headphones", - "category1_name": "电子产品", - "category2_name": "音频设备", - "category3_name": "耳机", - "vendor.zh": "品牌A", - "vendor.en": "Brand A", - "min_price": 199.99, - "max_price": 299.99, - "option1_name": "color", - "option2_name": "size", - "specifications": [ - { - "sku_id": "456", - "name": "color", - "value": "black" - }, - { - "sku_id": "456", - "name": "size", - "value": "large" - } - ], - "skus": [ - { - "sku_id": "456", - "price": 199.99, - "compare_at_price": 249.99, - "sku_code": "SKU-123-1", - "stock": 50, - "weight": 0.2, - "weight_unit": "kg", - "option1_value": "black", - "option2_value": "large", - "option3_value": null, - "image_src": "https://example.com/image.jpg" - } - ], - "title_embedding": [0.1, 0.2, ...], // 1024维向量 - "image_embedding": [ - { - "vector": [0.1, 0.2, ...], // 1024维向量 - "url": "https://example.com/image.jpg" - } - ] -} -``` - -### 1.3 索引结构简化方案 - -**简化原则**: -- **硬编码映射**:ES mapping 结构直接定义在 JSON 文件中(`mappings/search_products.json`),所有租户索引共享相同结构 -- **分索引 + 公共结构**:每个租户拥有独立索引,但索引结构一致 -- **数据源统一**:所有租户使用相同的 MySQL 表结构(店匠标准表) -- **查询配置集中**:查询相关配置(字段 boost、查询域等)集中在配置文件中管理 - -**索引结构特点**: -1. **多语言字段**:所有文本字段支持中英文(`title.zh/en`, `brief.zh/en`, `description.zh/en`, `vendor.zh/en`, `category_path.zh/en`, `category_name_text.zh/en`) -2. **嵌套字段**: - - `skus`: SKU 嵌套数组(包含价格、库存、选项值等) - - `specifications`: 规格嵌套数组(包含 name、value、sku_id) - - `image_embedding`: 图片向量嵌套数组 -3. **扁平化字段**:`sku_prices`, `sku_weights`, `total_inventory` 等用于过滤和排序 -4. **向量字段**:`title_embedding`(1024维)用于语义搜索 - -**实现文件**: -- `mappings/search_products.json` - ES mapping 定义(硬编码) -- `search/query_config.py` - 查询配置(硬编码) -- `indexer/mapping_generator.py` - 加载 JSON mapping 并创建索引 - ---- - -## 2. 索引结构实现 - -### 2.1 硬编码映射方案 - -**实现方式**: -- ES mapping 直接定义在 `mappings/search_products.json` 文件中 -- 所有租户索引共享相同的 mapping 结构 -- 查询配置集中在配置系统中(如 `config/config.yaml` 等) - -**索引字段结构**: - -#### 基础字段 -- `tenant_id` (keyword): 租户ID,用于数据隔离 -- `spu_id` (keyword): SPU唯一标识 -- `create_time`, `update_time` (date): 创建和更新时间 - -#### 多语言文本字段 -- `title.zh/en` (text): 标题(中英文) -- `brief.zh/en` (text): 短描述(中英文) -- `description.zh/en` (text): 详细描述(中英文) -- `vendor.zh/en` (text): 供应商/品牌(中英文) -- `category_path.zh/en` (text): 类目路径(中英文) -- `category_name_text.zh/en` (text): 类目名称(中英文) - -**分析器配置**: -- 中文字段:`hanlp_index`(索引时)/ `hanlp_standard`(查询时) -- 英文字段:`english` -- `vendor` 字段包含 `keyword` 子字段(normalizer: lowercase) - -#### 分类字段 -- `category_id` (keyword): 类目ID -- `category_name` (keyword): 类目名称 -- `category_level` (integer): 类目层级 -- `category1_name`, `category2_name`, `category3_name` (keyword): 多级类目名称 - -#### 规格字段(Specifications) -- `specifications` (nested): 规格嵌套数组 - - `sku_id` (keyword): SKU ID - - `name` (keyword): 规格名称(如 "color", "size") - - `value` (keyword): 规格值(如 "white", "256GB") - -**用途**: -- 支持按规格过滤:`{"specifications": {"name": "color", "value": "white"}}` -- 支持规格分面:`["specifications"]` 或 `["specifications.color"]` - -#### SKU嵌套字段 -- `skus` (nested): SKU嵌套数组 - - `sku_id`, `price`, `compare_at_price`, `sku_code` - - `stock`, `weight`, `weight_unit` - - `option1_value`, `option2_value`, `option3_value` - - `image_src` (index: false) - -#### 选项名称字段 -- `option1_name`, `option2_name`, `option3_name` (keyword): 选项名称(如 "color", "size") - -#### 扁平化字段 -- `min_price`, `max_price`, `compare_at_price` (float): 价格字段 -- `sku_prices` (float[]): 所有SKU价格数组 -- `sku_weights` (long[]): 所有SKU重量数组 -- `total_inventory` (long): 总库存 - -#### 向量字段 -- `title_embedding` (dense_vector, 1024维): 标题向量,用于语义搜索 -- `image_embedding` (nested): 图片向量数组 - - `vector` (dense_vector, 1024维) - - `url` (text) - -**实现模块**: -- `mappings/search_products.json` - ES mapping 定义 -- `indexer/mapping_generator.py` - 加载 JSON mapping 并创建索引 -- `search/query_config.py` - 查询配置(字段 boost、查询域等) - -### 2.2 索引结构配置(查询域配置) - -**配置内容**:定义了 ES 的字段索引 mapping 配置,支持各个域的查询,包括默认域的查询。 - -**实现情况**: - -#### 域(Domain)配置 -每个域定义了: -- 域名称(如 `default`, `title`, `category`, `brand`) -- 域标签(中文描述) -- 搜索字段列表 -- 默认分析器 -- 权重(boost) -- **多语言字段映射**(`language_field_mapping`) - -#### 多语言字段映射 - -支持将不同语言的查询路由到对应的字段: - -```yaml -indexes: - - name: "default" - label: "默认索引" - fields: - - "name" - - "enSpuName" - - "ruSkuName" - - "categoryName" - - "brandName" - analyzer: "chinese_ecommerce" - boost: 1.0 - language_field_mapping: - zh: - - "name" - - "categoryName" - - "brandName" - en: - - "enSpuName" - ru: - - "ruSkuName" - - - name: "title" - label: "标题索引" - fields: - - "name" - - "enSpuName" - - "ruSkuName" - analyzer: "chinese_ecommerce" - boost: 2.0 - language_field_mapping: - zh: - - "name" - en: - - "enSpuName" - ru: - - "ruSkuName" -``` - -**工作原理**: -1. 检测查询语言(中文、英文、俄文等) -2. 如果查询语言在 `language_field_mapping` 中,使用原始查询搜索对应语言的字段 -3. 将查询翻译到其他支持的语言,分别搜索对应语言的字段 -4. 组合多个语言查询的结果,提高召回率 - -**实现模块**: -- `search/es_query_builder.py` - ES 查询构建器(单层架构) -- `query/query_parser.py` - 查询解析器(支持语言检测和翻译) -- `search/query_config.py` - 查询配置(字段 boost、查询域等) - ---- - -## 3. 数据导入流程 - -### 3.1 数据源 - -**店匠标准表**(Base配置使用): -- `shoplazza_product_spu` - SPU级别商品数据 -- `shoplazza_product_sku` - SKU级别商品数据 - -**其他客户表**(tenant1等): -- 使用各自的数据源表和扩展表 - -### 3.2 数据导入方式 - -**数据源统一**: -- 所有租户使用相同的MySQL表结构(店匠标准表) -- 数据转换逻辑写死在转换器代码中 -- 索引结构硬编码,不依赖配置 - -#### 数据导入流程(店匠通用) - -**脚本**:`scripts/ingest_shoplazza.py` - -**数据流程**: -1. **数据加载**: - - 从MySQL读取`shoplazza_product_spu`表(SPU数据) - - 从MySQL读取`shoplazza_product_sku`表(SKU数据) - - 从MySQL读取`shoplazza_product_option`表(选项定义) - -2. **数据转换**(`indexer/spu_transformer.py`): - - 按`spu_id`和`tenant_id`关联SPU和SKU数据 - - **多语言字段映射**: - - MySQL的`title` → ES的`title.zh`(英文字段设为空) - - 其他文本字段类似处理 - - **分类字段映射**: - - 从SPU表的`category_path`解析多级类目(`category1_name`, `category2_name`, `category3_name`) - - 映射`category_id`, `category_name`, `category_level` - - **规格字段构建**(`specifications`): - - 从`shoplazza_product_option`表获取选项名称(`name`) - - 从SKU的`option1/2/3`字段获取选项值(`value`) - - 构建嵌套数组:`[{"sku_id": "...", "name": "color", "value": "white"}, ...]` - - **选项名称映射**: - - 从`shoplazza_product_option`表获取`option1_name`, `option2_name`, `option3_name` - - **SKU嵌套数组构建**: - - 包含所有SKU字段(价格、库存、选项值、图片等) - - **扁平化字段计算**: - - `min_price`, `max_price`: 从所有SKU价格计算 - - `sku_prices`: 所有SKU价格数组 - - `total_inventory`: SKU库存总和 - - 注入`tenant_id`字段 - -3. **索引创建**: - - 从`mappings/search_products.json`加载ES mapping - - 创建或更新`search_products`索引 - -4. **批量入库**: - - 批量写入ES(默认每批500条) - - 错误处理和重试机制 - -**命令行工具**: -```bash -python scripts/ingest_shoplazza.py \ - --db-host localhost \ - --db-port 3306 \ - --db-database saas \ - --db-username root \ - --db-password password \ - --tenant-id "1" \ - --config base \ - --es-host http://localhost:9200 \ - --recreate \ - --batch-size 500 -``` - -#### 其他客户数据导入 - -- 使用各自的数据转换器(如`indexer/data_transformer.py`) -- 数据源映射逻辑写死在各自的转换器中 -- 共享`search_products`索引,通过`tenant_id`隔离 - -**实现模块**: -- `indexer/spu_transformer.py` - SPU数据转换器(Base配置) -- `indexer/data_transformer.py` - 通用数据转换器(其他客户) -- `indexer/bulk_indexer.py` - 批量索引器 -- `scripts/ingest_shoplazza.py` - 店匠数据导入脚本 - ---- - -## 4. QueryParser 实现 - - -### 4.1 查询改写(Query Rewriting) - -配置词典的key是query,value是改写后的查询表达式,比如。比如品牌词 改写为在brand|query OR name|query,类别词、标签词等都可以放进去。纠错、规范化、查询改写等 都可以通过这个词典来配置。 -**实现情况**: - -#### 配置方式 -在 `query_config.rewrite_dictionary` 中配置查询改写规则: - -```yaml -query_config: - enable_query_rewrite: true - rewrite_dictionary: - "芭比": "brand:芭比 OR name:芭比娃娃" - "玩具": "category:玩具" - "消防": "category:消防 OR name:消防" -``` - -#### 功能特性 -- **精确匹配**:查询完全匹配词典 key 时,替换为 value -- **部分匹配**:查询包含词典 key 时,替换该部分 -- **支持布尔表达式**:value 可以是复杂的布尔表达式(AND, OR, 域查询等) - -#### 实现模块 -- `query/query_rewriter.py` - 查询改写器 -- `query/query_parser.py` - 查询解析器(集成改写功能) - -### 4.2 翻译(Translation) - -**实现情况**: - -#### 配置方式(示意) -翻译能力通过统一的 provider 配置管理,支持多种后端实现(如本地服务或外部 API): -```yaml -query_config: - supported_languages: - - "zh" - - "en" - default_language: "en" - # 实际翻译 provider 与模型在通用 services 配置中定义 -``` - -实际代码中,翻译已改为统一的 translator service 架构:业务侧通过 `translation.create_translation_client()` 访问 6006,由 `translation/service.py` 在服务内按 `model + scene` 路由到具体 backend。scene 集合、语言码映射、LLM prompt 模板、本地模型方向约束等翻译域知识位于 `translation/` 内部,不再通过外部 provider 抽象分散管理。 - -#### 功能特性 -1. **语言检测**:自动检测查询语言 -2. **智能翻译**: - - 将源语言 query 翻译到当前租户配置的索引语言集合 -3. **域感知翻译**: - - 如果域有 `language_field_mapping`,只翻译到映射中存在的语言 - - 避免不必要的翻译,提高效率 -4. **翻译缓存**:缓存翻译结果,避免重复调用翻译后端 - -#### 工作流程 -``` -查询输入 → 语言检测 → 翻译 → 查询构建(filters and (text_recall or embedding_recall)) -``` - -#### 实现模块 -- `query/language_detector.py` - 语言检测器 -- `query/translator.py` - 翻译 provider 抽象与调用 -- `query/query_parser.py` - 查询解析器(集成翻译功能) - -### 4.3 文本向量化(Text Embedding) - -如果配置打开了text_embedding查询,并且query 包含了default域的查询,那么要把default域的查询词转向量,后面searcher会用这个向量参与查询。 - -**实现情况**: - -#### 配置方式 -```yaml -query_config: - enable_text_embedding: true -``` - -#### 功能特性 -1. **条件生成**: - - 仅当 `enable_text_embedding=true` 时生成向量 - - 仅对 `default` 域查询生成向量 -2. **向量模型**:使用可配置的文本向量模型(例如 1024 维通用语义 embedding),具体模型通过配置与 embedding 服务选择,不在文档中固定写死 -3. **用途**:用于语义搜索(KNN 检索) - -#### 实现模块 -- `embeddings/text_encoder.py` + `embeddings/server.py` - 文本向量服务客户端与服务端实现(支持可配置的模型后端) -- `query/query_parser.py` - 查询解析器(集成向量生成) - ---- - -## 5. Searcher 实现 - -参考opensearch,他们自己定义的一套索引结构配置、支持自定义的一套检索表达式、排序表达式,这是各个客户进行配置化的基础,包括索引结构配置、排序策略配置。 -比如各种业务过滤策略 可以简单的通过表达式满足,比如brand|耐克 AND cate2|xxx。指定字段排序可以通过排序的表达式实现。 - -查询默认在default域,相也会对这个域的查询做一些相关性的重点优化,包括融合语义相关性、多语言相关性(可以基于配置 将查询翻译到指定语言并在对应的语言的字段进行查询)来弥补传统查询分析手段(比如查询改写 纠错 词权重等)的不足,也支持通过配置一些词表转为泛查询模式来优化相关性。 - -### 5.1 布尔表达式解析 - -**实现情况**: - -#### 支持的运算符 -- **AND**:所有项必须匹配 -- **OR**:任意项匹配 -- **RANK**:排序增强(类似 OR 但影响排序) -- **ANDNOT**:排除(第一项匹配,第二项不匹配) -- **()**:括号分组 - -#### 优先级(从高到低) -1. `()` - 括号 -2. `ANDNOT` - 排除 -3. `AND` - 与 -4. `OR` - 或 -5. `RANK` - 排序 - -#### 示例 -``` -laptop AND (gaming OR professional) ANDNOT cheap -``` - -#### 实现模块 -- `search/boolean_parser.py` - 布尔表达式解析器 -- `search/searcher.py` - 搜索器(集成布尔解析) - -### 5.2 多语言搜索 - -**实现情况**: - -#### 工作原理 -1. **查询解析**: - - 提取域(如 `title:查询` → 域=`title`,查询=`查询`) - - 检测查询语言 - - 生成翻译 -2. **查询构建**(简化架构): - - **结构**: `filters AND (text_recall OR embedding_recall)` - - **filters**: 前端传递的过滤条件(永远起作用,放在 `filter` 中) - - 普通字段过滤:`{"category_name": "手机"}` - - 范围过滤:`{"min_price": {"gte": 50, "lte": 200}}` - - **Specifications嵌套过滤**: - - 单个规格:`{"specifications": {"name": "color", "value": "white"}}` - - 多个规格:`{"specifications": [{"name": "color", "value": "white"}, {"name": "size", "value": "256GB"}]}` - - 过滤逻辑:不同维度(不同name)是AND关系,相同维度(相同name)的多个值是OR关系 - - 使用ES的`nested`查询实现 - - **text_recall**: 文本相关性召回 - - 同时搜索中英文字段(`title.zh/en`, `brief.zh/en`, `description.zh/en`, `vendor.zh/en`, `category_path.zh/en`, `category_name_text.zh/en`, `tags`) - - 使用 `multi_match` 查询,支持字段 boost - - 中文字段使用中文分词器,英文字段使用英文分析器 - - **embedding_recall**: 向量召回(KNN) - - 使用 `title_embedding` 字段进行 KNN 搜索 - - ES 自动与文本召回合并(OR逻辑) - - **function_score**: 包装召回部分,支持提权字段(新鲜度、销量等) - -#### 查询结构示例 -```json -{ - "query": { - "bool": { - "must": [ - { - "function_score": { - "query": { - "multi_match": { - "query": "手机", - "fields": [ - "title.zh^3.0", "title.en^3.0", - "brief.zh^1.5", "brief.en^1.5", - ... - ] - } - }, - "functions": [...] - } - } - ], - "filter": [ - {"term": {"tenant_id": "2"}}, - {"term": {"category_name": "手机"}} - ] - } - }, - "knn": { - "field": "title_embedding", - "query_vector": [...], - "k": 50, - "boost": 0.2 - } -} -``` -> **KNN 自适应策略**:`k`、`num_candidates`、`boost` 会根据 `query_tokens` 动态调整:短查询(≤2 token)减少召回和权重,长查询(≥5 token)增加召回和权重。详见 `docs/相关性检索优化说明.md` 3.6 节。 - -#### 实现模块 -- `search/es_query_builder.py` - ES 查询构建器(单层架构,`build_query` 方法) -- `search/searcher.py` - 搜索器(使用 `ESQueryBuilder`) - -### 5.3 相关性计算(Ranking) - -**实现情况**: - -#### 当前实现 -**公式**:`bm25() + 0.2 * text_embedding_relevance()` - -- **bm25()**:BM25 文本相关性得分 - - 包括多语言打分 - - 内部通过配置翻译为多种语言 - - 分别到对应的字段搜索 - - 中文字段使用中文分词器,英文字段使用英文分词器 -- **text_embedding_relevance()**:文本向量相关性得分(KNN 检索的打分) - - 权重:0.2 - -#### 配置方式 -```yaml -ranking: - expression: "bm25() + 0.2*text_embedding_relevance()" - description: "BM25 text relevance combined with semantic embedding similarity" -``` - -#### 扩展性 -- 支持表达式配置(未来可扩展) -- 支持自定义函数(如 `timeliness()`, `field_value()`) - -#### 实现模块 -- `search/ranking_engine.py` - 排序引擎 -- `search/searcher.py` - 搜索器(集成排序功能) - ---- - -## 6. 已完成功能总结 - -### 6.1 配置系统 -- ✅ 字段定义配置(类型、分析器、来源表/列) -- ✅ 索引域配置(多域查询、多语言映射) -- ✅ 查询配置(改写词典、翻译配置) -- ✅ 排序配置(表达式配置) -- ✅ 配置验证(字段存在性、类型检查、分析器匹配) - -### 6.2 数据索引 -- ✅ 数据转换(字段映射、类型转换) -- ✅ 向量生成(文本向量、图片向量) -- ✅ 向量缓存(避免重复计算) -- ✅ 批量索引(错误处理、重试机制) -- ✅ ES mapping 自动生成 - -### 6.3 查询处理 -- ✅ 查询改写(词典配置) -- ✅ 语言检测 -- ✅ 多语言翻译(DeepL API) -- ✅ 文本向量化(Qwen3-Embedding-0.6B) -- ✅ 域提取(支持 `domain:query` 语法) - -### 6.4 搜索功能 -- ✅ 布尔表达式解析(AND, OR, RANK, ANDNOT, 括号) -- ✅ 多语言查询构建(同时搜索中英文字段) -- ✅ 语义搜索(KNN 检索) -- ✅ 相关性排序(BM25 + 向量相似度) -- ✅ 结果聚合(Faceted Search) -- ✅ Specifications嵌套过滤(单个和多个规格,按维度分组:不同维度AND,相同维度OR) -- ✅ Specifications嵌套分面(所有规格名称和指定规格名称) -- ✅ SKU筛选(按维度过滤,应用层实现) - -### 6.5 API 服务 -- ✅ RESTful API(FastAPI) -- ✅ 搜索接口(文本搜索、图片搜索) -- ✅ 文档查询接口 -- ✅ 前端界面(HTML + JavaScript) -- ✅ 租户隔离(tenant_id过滤) - -### 6.6 索引结构(店匠通用) -- ✅ SPU级别索引结构 -- ✅ 多语言字段支持(中英文) -- ✅ 嵌套字段(skus, specifications, image_embedding) -- ✅ 规格字段(specifications)支持过滤和分面 -- ✅ 扁平化字段(价格、库存等)用于过滤和排序 -- ✅ 按租户分索引(索引名通过 `get_tenant_index_name` 生成),各租户索引结构一致 -- ✅ 文档内仍包含 `tenant_id` 字段,可用于额外校验或跨索引场景 -- ✅ 硬编码映射(mappings/search_products.json) -- ✅ 集中查询配置(config/config.yaml 等) - ---- - -## 7. 技术栈 - -- **后端**:Python 3.6+ -- **搜索引擎**:Elasticsearch -- **数据库**:MySQL(Shoplazza) -- **向量服务**:可配置的文本/图像向量模型(通过 embedding 服务与配置选择具体模型) -- **翻译服务**:可配置的翻译 provider(通过 translation 服务与配置选择具体后端与模型) -- **API 框架**:FastAPI -- **前端**:HTML + JavaScript - ---- - -## 8. API响应格式 - -### 8.1 外部友好格式 - -API返回格式不包含ES内部字段(`_id`, `_score`, `_source`),使用外部友好的格式: - -**响应结构**: -```json -{ - "results": [ - { - "spu_id": "123", - "title": "蓝牙耳机", - "skus": [ - { - "sku_id": "456", - "price": 199.99, - "sku": "SKU-123-1", - "stock": 50 - } - ], - "relevance_score": 0.95 - } - ], - "total": 10, - "facets": [...], - "suggestions": [], - "related_searches": [] -} -``` - -**主要变化**: -- 结构化结果(`SpuResult`和`SkuResult`) -- 嵌套skus数组 -- 无ES内部字段 - -### 8.2 租户隔离 - -所有API请求必须提供`tenant_id`: -- 请求头:`X-Tenant-ID: 1` -- 或查询参数:`?tenant_id=1` - -搜索时自动添加`tenant_id`过滤,确保数据隔离。 - -### 8.3 数据接口约定 - -**统一的数据约定格式**:所有API接口使用 Pydantic 模型进行数据验证和序列化。 - -#### 8.3.1 数据流模式 - -系统采用统一的数据流模式,确保数据在各层之间的一致性: - -**数据流转路径**: -``` -API Request (JSON) - ↓ -Pydantic 验证 → 结构化模型(RangeFilter, FacetConfig 等) - ↓ -Searcher(透传) - ↓ -ES Query Builder → model_dump() 转换为字典 - ↓ -ES Query (字典) - ↓ -Elasticsearch -``` - -#### 8.3.2 Facets 配置数据流 - -**输入格式**:`List[FacetConfig]` - -**配置对象列表**:所有分面配置必须使用 FacetConfig 对象 -```json -[ - { - "field": "category1_name", - "size": 15, - "type": "terms" - }, - { - "field": "specifications.color", - "size": 20, - "type": "terms" - }, - { - "field": "min_price", - "type": "range", - "ranges": [ - {"key": "0-50", "to": 50}, - {"key": "50-100", "from": 50, "to": 100} - ] - } -] -``` - -**Specifications 分面支持**: -- 所有规格名称:`field: "specifications"` - 返回所有 name 及其 value 列表 -- 指定规格名称:`field: "specifications.color"` - 只返回指定 name 的 value 列表 - -**数据流**: -1. API 层:接收 `List[FacetConfig]`,Pydantic 验证参数 -2. Searcher 层:透传 FacetConfig 对象列表 -3. ES Query Builder:解析 FacetConfig 对象 - - 检测 `"specifications"` 或 `"specifications.{name}"` 格式 - - 构建对应的嵌套聚合查询或普通聚合查询 -4. 输出:转换为 ES 聚合查询(包括 specifications 嵌套聚合) -5. Result Formatter:格式化 ES 聚合结果,处理 specifications 嵌套结构 - -#### 8.3.3 Range Filters 数据流 - -**输入格式**:`Dict[str, RangeFilter]` - -**RangeFilter 模型**: -```python -class RangeFilter(BaseModel): - gte: Optional[Union[float, str]] # 大于等于 - gt: Optional[Union[float, str]] # 大于 - lte: Optional[Union[float, str]] # 小于等于 - lt: Optional[Union[float, str]] # 小于 -``` - -**示例**: -```json -{ - "min_price": {"gte": 50, "lte": 200}, - "create_time": {"gte": "2023-01-01T00:00:00Z"} -} -``` - -**数据流**: -1. API 层:接收 `Dict[str, RangeFilter]`,Pydantic 自动验证 -2. Searcher 层:透传 `Dict[str, RangeFilter]` -3. ES Query Builder:调用 `range_filter.model_dump()` 转换为字典 -4. 输出:ES range 查询(支持数值和日期) - -**特性**: -- 自动验证:确保至少指定一个边界值(gte, gt, lte, lt) -- 类型支持:支持数值(float)和日期时间字符串(ISO 格式) -- 统一约定:所有范围过滤都使用 RangeFilter 模型 - -#### 8.3.3.1 Specifications 过滤数据流 - -**输入格式**:`Dict[str, Union[Dict[str, str], List[Dict[str, str]]]]` - -**单个规格过滤**: -```json -{ - "specifications": { - "name": "color", - "value": "white" - } -} -``` - -**多个规格过滤(按维度分组)**: -```json -{ - "specifications": [ - {"name": "color", "value": "white"}, - {"name": "size", "value": "256GB"} - ] -} -``` - -**数据流**: -1. API 层:接收 `filters` 字典,检测 `specifications` 键 -2. Searcher 层:透传 `filters` 字典 -3. ES Query Builder:检测 `specifications` 键,构建ES `nested` 查询 - - 单个规格:构建单个 `nested` 查询 - - 多个规格:按 name 维度分组,相同维度内使用 `should` 组合(OR逻辑),不同维度之间使用 `must` 组合(AND逻辑) -4. 输出:ES nested 查询(`nested.path=specifications` + `bool.must=[term(name), term(value)]`) - -#### 8.3.4 响应 Facets 数据流 - -**输出格式**:`List[FacetResult]` - -**FacetResult 模型**: -```python -class FacetResult(BaseModel): - field: str # 字段名 - label: str # 显示标签 - type: Literal["terms", "range"] # 分面类型 - values: List[FacetValue] # 分面值列表 - total_count: Optional[int] # 总文档数 -``` - -**数据流**: -1. ES Response:返回聚合结果(字典格式,包括specifications嵌套聚合) -2. Result Formatter:格式化ES聚合结果 - - 处理普通terms聚合 - - 处理range聚合 - - **处理specifications嵌套聚合**: - - 所有规格名称:解析 `by_name` 聚合结构 - - 指定规格名称:解析 `filter_by_name` 聚合结构 -3. Searcher 层:构建 `List[FacetResult]` 对象 -4. API 层:直接返回 `List[FacetResult]`(Pydantic 自动序列化为 JSON) - -**优势**: -- 类型安全:使用 Pydantic 模型确保数据结构一致性 -- 自动序列化:模型自动转换为 JSON,无需手动处理 -- 统一约定:所有响应都使用标准化的 Pydantic 模型 - -#### 8.3.5 SKU筛选数据流 - -**输入格式**:`Optional[str]` - -**支持的维度值**: -- `option1`, `option2`, `option3`: 直接使用选项字段 -- 规格名称(如 `color`, `size`): 通过 `option1_name`、`option2_name`、`option3_name` 匹配 - -**示例**: -```json -{ - "query": "手机", - "sku_filter_dimension": "color" -} -``` - -**数据流**: -1. API 层:接收 `sku_filter_dimension` 字符串参数 -2. Searcher 层:透传到 Result Formatter -3. Result Formatter:在格式化结果时,按指定维度对SKU进行分组 - - 如果维度是 `option1/2/3`,直接使用对应的 `option1_value/2/3` 字段 - - 如果维度是规格名称,通过 `option1_name/2/3` 匹配找到对应的 `option1_value/2/3` - - 每个分组选择第一个SKU返回 -4. 输出:过滤后的SKU列表(每个维度值一个SKU) - -**工作原理**: -1. 系统从ES返回所有SKU(不改变ES查询,保持性能) -2. 在结果格式化阶段,按指定维度对SKU进行分组 -3. 每个分组选择第一个SKU返回 -4. 如果维度不匹配或未找到,返回所有SKU(不进行过滤) - -**性能说明**: -- ✅ **推荐方案**: 在应用层过滤(当前实现) - - ES查询简单,不需要nested查询和join - - 只对返回的结果(通常10-20个SPU)进行过滤,数据量小 - - 实现简单,性能开销小 -- ❌ **不推荐**: 在ES查询时过滤 - - 需要nested查询和join,性能开销大 - - 实现复杂 - - 只对返回的结果需要过滤,不需要在ES层面过滤 - -#### 8.3.6 统一约定的好处 - -1. **类型安全**:使用 Pydantic 模型提供运行时类型检查和验证 -2. **代码一致性**:所有层使用相同的数据模型,减少转换错误 -3. **自动文档**:FastAPI 自动生成 API 文档(基于 Pydantic 模型) -4. **易于维护**:修改数据结构只需更新模型定义 -5. **数据验证**:自动验证输入数据,减少错误处理代码 - -**实现模块**: -- `api/models.py` - 所有 Pydantic 模型定义(包括 `SearchRequest`, `FacetConfig`, `RangeFilter` 等) -- `api/result_formatter.py` - 结果格式化器(ES 响应 → Pydantic 模型,包括specifications分面处理和SKU筛选) -- `search/es_query_builder.py` - ES 查询构建器(Pydantic 模型 → ES 查询,包括specifications过滤和分面) - -## 9. 索引结构文件 - -**硬编码映射**(店匠通用):`mappings/search_products.json` - -**查询配置**(硬编码):`search/query_config.py` - ---- diff --git a/docs/系统设计文档v1.md b/docs/系统设计文档v1.md deleted file mode 100644 index c4e57c2..0000000 --- a/docs/系统设计文档v1.md +++ /dev/null @@ -1,743 +0,0 @@ -# 搜索引擎通用化开发进度 - -> 历史版本说明:本文件为 v1 归档文档,部分模型与实现细节(如 BGE-M3)已过时。当前以 `docs/系统设计文档.md` 与 `docs/QUICKSTART.md` 为准。 - -## 项目概述 - -对后端搜索技术 做通用化。 -通用化的本质 是 对于各种业务数据、各种检索需求,都可以 用少量定制+配置化 来实现效果。 - - -**通用化的本质**:对于各种业务数据、各种检索需求,都可以用少量定制+配置化来实现效果。 - ---- - -## 1. 原始数据层的约定 - -### 1.1 店匠主表 - -所有租户共用以下主表: -- `shoplazza_product_sku` - SKU级别商品数据 -- `shoplazza_product_spu` - SPU级别商品数据 - -### 1.2 索引结构(SPU维度) - -**统一索引架构**: -- 所有客户共享同一个Elasticsearch索引:`search_products` -- 索引粒度:SPU级别(每个文档代表一个SPU) -- 数据隔离:通过`tenant_id`字段实现租户隔离 -- 嵌套结构:每个SPU文档包含嵌套的`skus`数组 - -**索引文档结构**: -```json -{ - "tenant_id": "1", - "spu_id": "123", - "title": "蓝牙耳机", - "skus": [ - { - "sku_id": "456", - "title": "黑色", - "price": 199.99, - "sku": "SKU-123-1", - "stock": 50 - } - ], - "min_price": 199.99, - "max_price": 299.99 -} -``` - -### 1.3 配置化方案 - -**配置分离原则**: -- **搜索配置**:只包含ES字段定义、查询域、排序规则等搜索相关配置 -- **数据源配置**:不在搜索配置中,由Pipeline层(脚本)决定 -- **数据导入流程**:写死的脚本,不依赖配置 - -统一通过配置文件定义: -1. ES 字段定义(字段类型、分析器、boost等) -2. ES mapping 结构生成 -3. 查询域配置(indexes) -4. 排序和打分配置(function_score) - -**注意**:配置中**不包含**以下内容: -- `mysql_config` - MySQL数据库配置 -- `main_table` / `extension_table` - 数据表配置 -- `source_table` / `source_column` - 字段数据源映射 - ---- - -## 2. 配置系统实现 - -### 2.1 应用结构配置(字段定义) - -**配置文件位置**:`config/schema/{tenant_id}_config.yaml` - -**配置内容**:定义了 ES 的输入数据有哪些字段、关联 MySQL 的哪些字段。 - -**实现情况**: - -#### 字段类型支持 -- **TEXT**:文本字段,支持多语言分析器 -- **KEYWORD**:关键词字段,用于精确匹配和聚合 -- **TEXT_EMBEDDING**:文本向量字段(1024维,dot_product相似度) -- **IMAGE_EMBEDDING**:图片向量字段(1024维,dot_product相似度) -- **INT/LONG**:整数类型 -- **FLOAT/DOUBLE**:浮点数类型 -- **DATE**:日期类型 -- **BOOLEAN**:布尔类型 - -#### 分析器支持 -- **chinese_ecommerce**:中文电商分词器(index_ik/query_ik) -- **english**:英文分析器 -- **russian**:俄文分析器 -- **arabic**:阿拉伯文分析器 -- **spanish**:西班牙文分析器 -- **japanese**:日文分析器 -- **standard**:标准分析器 -- **keyword**:关键词分析器 - -#### 字段配置示例(Base配置) - -```yaml -fields: - # 租户隔离字段(必需) - - name: "tenant_id" - type: "KEYWORD" - required: true - index: true - store: true - - # 商品标识字段 - - name: "spu_id" - type: "KEYWORD" - required: true - index: true - store: true - - # 文本搜索字段 - - name: "title" - type: "TEXT" - analyzer: "chinese_ecommerce" - boost: 3.0 - index: true - store: true - - - name: "seo_keywords" - type: "TEXT" - analyzer: "chinese_ecommerce" - boost: 2.0 - index: true - store: true - - # 嵌套skus字段 - - name: "skus" - type: "JSON" - nested: true - nested_properties: - sku_id: - type: "keyword" - price: - type: "float" - sku: - type: "keyword" -``` - -**注意**:配置中**不包含**`source_table`和`source_column`,数据源映射由Pipeline层决定。 - -**实现模块**: -- `config/config_loader.py` - 配置加载器 -- `config/field_types.py` - 字段类型定义 -- `indexer/mapping_generator.py` - ES mapping 生成器 -- `indexer/data_transformer.py` - 数据转换器 - -### 2.2 索引结构配置(查询域配置) - -**配置内容**:定义了 ES 的字段索引 mapping 配置,支持各个域的查询,包括默认域的查询。 - -**实现情况**: - -#### 域(Domain)配置 -每个域定义了: -- 域名称(如 `default`, `title`, `category`, `brand`) -- 域标签(中文描述) -- 搜索字段列表 -- 默认分析器 -- 权重(boost) -- **多语言字段映射**(`language_field_mapping`) - -#### 多语言字段映射 - -支持将不同语言的查询路由到对应的字段: - -```yaml -indexes: - - name: "default" - label: "默认索引" - fields: - - "name" - - "enSpuName" - - "ruSkuName" - - "categoryName" - - "brandName" - analyzer: "chinese_ecommerce" - boost: 1.0 - language_field_mapping: - zh: - - "name" - - "categoryName" - - "brandName" - en: - - "enSpuName" - ru: - - "ruSkuName" - - - name: "title" - label: "标题索引" - fields: - - "name" - - "enSpuName" - - "ruSkuName" - analyzer: "chinese_ecommerce" - boost: 2.0 - language_field_mapping: - zh: - - "name" - en: - - "enSpuName" - ru: - - "ruSkuName" -``` - -**工作原理**: -1. 检测查询语言(中文、英文、俄文等) -2. 如果查询语言在 `language_field_mapping` 中,使用原始查询搜索对应语言的字段 -3. 将查询翻译到其他支持的语言,分别搜索对应语言的字段 -4. 组合多个语言查询的结果,提高召回率 - -**实现模块**: -- `search/es_query_builder.py` - ES 查询构建器(单层架构) -- `query/query_parser.py` - 查询解析器(支持语言检测和翻译) - ---- - -## 3. 数据导入流程 - -### 3.1 数据源 - -**店匠标准表**(Base配置使用): -- `shoplazza_product_spu` - SPU级别商品数据 -- `shoplazza_product_sku` - SKU级别商品数据 - -**其他客户表**(tenant1等): -- 使用各自的数据源表和扩展表 - -### 3.2 数据导入方式 - -**Pipeline层决定数据源**: -- 数据导入流程是写死的脚本,不依赖配置 -- 配置只关注ES搜索相关的内容 -- 数据源映射逻辑写死在转换器代码中 - -#### Base配置数据导入(店匠通用) - -**脚本**:`scripts/ingest_shoplazza.py` - -**数据流程**: -1. **数据加载**:从MySQL读取`shoplazza_product_spu`和`shoplazza_product_sku`表 -2. **数据转换**(`indexer/spu_transformer.py`): - - 按`spu_id`和`tenant_id`关联SPU和SKU数据 - - 将SKU数据聚合为嵌套的`skus`数组 - - 计算扁平化价格字段(`min_price`, `max_price`, `compare_at_price`) - - 字段映射(写死在代码中,不依赖配置) - - 注入`tenant_id`字段 -3. **索引创建**: - - 根据配置生成ES mapping - - 创建或更新`search_products`索引 -4. **批量入库**: - - 批量写入ES(默认每批500条) - - 错误处理和重试机制 - -**命令行工具**: -```bash -python scripts/ingest_shoplazza.py \ - --db-host localhost \ - --db-port 3306 \ - --db-database saas \ - --db-username root \ - --db-password password \ - --tenant-id "1" \ - --config base \ - --es-host http://localhost:9200 \ - --recreate \ - --batch-size 500 -``` - -#### 其他客户数据导入 - -- 使用各自的数据转换器(如`indexer/data_transformer.py`) -- 数据源映射逻辑写死在各自的转换器中 -- 共享`search_products`索引,通过`tenant_id`隔离 - -**实现模块**: -- `indexer/spu_transformer.py` - SPU数据转换器(Base配置) -- `indexer/data_transformer.py` - 通用数据转换器(其他客户) -- `indexer/bulk_indexer.py` - 批量索引器 -- `scripts/ingest_shoplazza.py` - 店匠数据导入脚本 - ---- - -## 4. QueryParser 实现 - - -### 4.1 查询改写(Query Rewriting) - -配置词典的key是query,value是改写后的查询表达式,比如。比如品牌词 改写为在brand|query OR name|query,类别词、标签词等都可以放进去。纠错、规范化、查询改写等 都可以通过这个词典来配置。 -**实现情况**: - -#### 配置方式 -在 `query_config.rewrite_dictionary` 中配置查询改写规则: - -```yaml -query_config: - enable_query_rewrite: true - rewrite_dictionary: - "芭比": "brand:芭比 OR name:芭比娃娃" - "玩具": "category:玩具" - "消防": "category:消防 OR name:消防" -``` - -#### 功能特性 -- **精确匹配**:查询完全匹配词典 key 时,替换为 value -- **部分匹配**:查询包含词典 key 时,替换该部分 -- **支持布尔表达式**:value 可以是复杂的布尔表达式(AND, OR, 域查询等) - -#### 实现模块 -- `query/query_rewriter.py` - 查询改写器 -- `query/query_parser.py` - 查询解析器(集成改写功能) - -### 4.2 翻译(Translation) - -**实现情况**: - -#### 配置方式 -```yaml -query_config: - supported_languages: - - "zh" - - "en" - - "ru" - default_language: "zh" - translation_service: "deepl" - translation_api_key: null # 通过环境变量设置 -``` - -#### 功能特性 -1. **语言检测**:自动检测查询语言 -2. **智能翻译**: - - 如果查询是中文,翻译为英文、俄文 - - 如果查询是英文,翻译为中文、俄文 - - 如果查询是其他语言,翻译为所有支持的语言 -3. **域感知翻译**: - - 如果域有 `language_field_mapping`,只翻译到映射中存在的语言 - - 避免不必要的翻译,提高效率 -4. **翻译缓存**:缓存翻译结果,避免重复调用 API - -#### 工作流程 -``` -查询输入 → 语言检测 → 翻译 → 查询构建(filters and (text_recall or embedding_recall)) -``` - -#### 实现模块 -- `query/language_detector.py` - 语言检测器 -- `query/translator.py` - 翻译器(DeepL API) -- `query/query_parser.py` - 查询解析器(集成翻译功能) - -### 4.3 文本向量化(Text Embedding) - -如果配置打开了text_embedding查询,并且query 包含了default域的查询,那么要把default域的查询词转向量,后面searcher会用这个向量参与查询。 - -**实现情况**: - -#### 配置方式 -```yaml -query_config: - enable_text_embedding: true -``` - -#### 功能特性 -1. **条件生成**: - - 仅当 `enable_text_embedding=true` 时生成向量 - - 仅对 `default` 域查询生成向量 -2. **向量模型**:BGE-M3 模型(1024维向量) -3. **用途**:用于语义搜索(KNN 检索) - -#### 实现模块 -- `embeddings/bge_encoder.py` - BGE 文本编码器 -- `query/query_parser.py` - 查询解析器(集成向量生成) - ---- - -## 5. Searcher 实现 - -参考opensearch,他们自己定义的一套索引结构配置、支持自定义的一套检索表达式、排序表达式,这是各个客户进行配置化的基础,包括索引结构配置、排序策略配置。 -比如各种业务过滤策略 可以简单的通过表达式满足,比如brand|耐克 AND cate2|xxx。指定字段排序可以通过排序的表达式实现。 - -查询默认在default域,相也会对这个域的查询做一些相关性的重点优化,包括融合语义相关性、多语言相关性(可以基于配置 将查询翻译到指定语言并在对应的语言的字段进行查询)来弥补传统查询分析手段(比如查询改写 纠错 词权重等)的不足,也支持通过配置一些词表转为泛查询模式来优化相关性。 - -### 5.1 布尔表达式解析 - -**实现情况**: - -#### 支持的运算符 -- **AND**:所有项必须匹配 -- **OR**:任意项匹配 -- **RANK**:排序增强(类似 OR 但影响排序) -- **ANDNOT**:排除(第一项匹配,第二项不匹配) -- **()**:括号分组 - -#### 优先级(从高到低) -1. `()` - 括号 -2. `ANDNOT` - 排除 -3. `AND` - 与 -4. `OR` - 或 -5. `RANK` - 排序 - -#### 示例 -``` -laptop AND (gaming OR professional) ANDNOT cheap -``` - -#### 实现模块 -- `search/boolean_parser.py` - 布尔表达式解析器 -- `search/searcher.py` - 搜索器(集成布尔解析) - -### 5.2 多语言搜索 - -**实现情况**: - -#### 工作原理 -1. **查询解析**: - - 提取域(如 `title:查询` → 域=`title`,查询=`查询`) - - 检测查询语言 - - 生成翻译 -2. **查询构建**(简化架构): - - **结构**: `filters AND (text_recall OR embedding_recall)` - - **filters**: 前端传递的过滤条件(永远起作用,放在 `filter` 中) - - **text_recall**: 文本相关性召回 - - 同时搜索中英文字段(`title.zh/en`, `brief.zh/en`, `description.zh/en`, `vendor.zh/en`, `category_path.zh/en`, `category_name_text.zh/en`, `tags`) - - 使用 `multi_match` 查询,支持字段 boost - - **embedding_recall**: 向量召回(KNN) - - 使用 `title_embedding` 字段进行 KNN 搜索 - - ES 自动与文本召回合并 - - **function_score**: 包装召回部分,支持提权字段(新鲜度、销量等) - -#### 查询结构示例 -```json -{ - "query": { - "bool": { - "must": [ - { - "function_score": { - "query": { - "multi_match": { - "query": "手机", - "fields": [ - "title.zh^3.0", "title.en^3.0", - "brief.zh^1.5", "brief.en^1.5", - ... - ] - } - }, - "functions": [...] - } - } - ], - "filter": [ - {"term": {"tenant_id": "2"}}, - {"term": {"category_name": "手机"}} - ] - } - }, - "knn": { - "field": "title_embedding", - "query_vector": [...], - "k": 50, - "boost": 0.2 - } -} -``` - -#### 实现模块 -- `search/es_query_builder.py` - ES 查询构建器(单层架构,`build_query` 方法) -- `search/searcher.py` - 搜索器(使用 `ESQueryBuilder`) - -### 5.3 相关性计算(Ranking) - -**实现情况**: - -#### 当前实现 -**公式**:`bm25() + 0.2 * text_embedding_relevance()` - -- **bm25()**:BM25 文本相关性得分 - - 包括多语言打分 - - 内部通过配置翻译为多种语言 - - 分别到对应的字段搜索 - - 中文字段使用中文分词器,英文字段使用英文分词器 -- **text_embedding_relevance()**:文本向量相关性得分(KNN 检索的打分) - - 权重:0.2 - -#### 配置方式 -```yaml -ranking: - expression: "bm25() + 0.2*text_embedding_relevance()" - description: "BM25 text relevance combined with semantic embedding similarity" -``` - -#### 扩展性 -- 支持表达式配置(未来可扩展) -- 支持自定义函数(如 `timeliness()`, `field_value()`) - -#### 实现模块 -- `search/ranking_engine.py` - 排序引擎 -- `search/searcher.py` - 搜索器(集成排序功能) - ---- - -## 6. 已完成功能总结 - -### 6.1 配置系统 -- ✅ 字段定义配置(类型、分析器、来源表/列) -- ✅ 索引域配置(多域查询、多语言映射) -- ✅ 查询配置(改写词典、翻译配置) -- ✅ 排序配置(表达式配置) -- ✅ 配置验证(字段存在性、类型检查、分析器匹配) - -### 6.2 数据索引 -- ✅ 数据转换(字段映射、类型转换) -- ✅ 向量生成(文本向量、图片向量) -- ✅ 向量缓存(避免重复计算) -- ✅ 批量索引(错误处理、重试机制) -- ✅ ES mapping 自动生成 - -### 6.3 查询处理 -- ✅ 查询改写(词典配置) -- ✅ 语言检测 -- ✅ 多语言翻译(DeepL API) -- ✅ 文本向量化(BGE-M3) -- ✅ 域提取(支持 `domain:query` 语法) - -### 6.4 搜索功能 -- ✅ 布尔表达式解析(AND, OR, RANK, ANDNOT, 括号) -- ✅ 多语言查询构建(语言路由、字段映射) -- ✅ 语义搜索(KNN 检索) -- ✅ 相关性排序(BM25 + 向量相似度) -- ✅ 结果聚合(Faceted Search) - -### 6.5 API 服务 -- ✅ RESTful API(FastAPI) -- ✅ 搜索接口(文本搜索、图片搜索) -- ✅ 文档查询接口 -- ✅ 前端界面(HTML + JavaScript) -- ✅ 租户隔离(tenant_id过滤) - -### 6.6 Base配置(店匠通用) -- ✅ SPU级别索引结构 -- ✅ 嵌套skus字段 -- ✅ 统一索引(search_products) -- ✅ 租户隔离(tenant_id) -- ✅ 配置简化(移除MySQL相关配置) - ---- - -## 7. 技术栈 - -- **后端**:Python 3.6+ -- **搜索引擎**:Elasticsearch -- **数据库**:MySQL(Shoplazza) -- **向量模型**:BGE-M3(文本)、CN-CLIP(图片) -- **翻译服务**:DeepL API -- **API 框架**:FastAPI -- **前端**:HTML + JavaScript - ---- - -## 8. API响应格式 - -### 8.1 外部友好格式 - -API返回格式不包含ES内部字段(`_id`, `_score`, `_source`),使用外部友好的格式: - -**响应结构**: -```json -{ - "results": [ - { - "spu_id": "123", - "title": "蓝牙耳机", - "skus": [ - { - "sku_id": "456", - "price": 199.99, - "sku": "SKU-123-1", - "stock": 50 - } - ], - "relevance_score": 0.95 - } - ], - "total": 10, - "facets": [...], - "suggestions": [], - "related_searches": [] -} -``` - -**主要变化**: -- 结构化结果(`SpuResult`和`SkuResult`) -- 嵌套skus数组 -- 无ES内部字段 - -### 8.2 租户隔离 - -所有API请求必须提供`tenant_id`: -- 请求头:`X-Tenant-ID: 1` -- 或查询参数:`?tenant_id=1` - -搜索时自动添加`tenant_id`过滤,确保数据隔离。 - -### 8.3 数据接口约定 - -**统一的数据约定格式**:所有API接口使用 Pydantic 模型进行数据验证和序列化。 - -#### 8.3.1 数据流模式 - -系统采用统一的数据流模式,确保数据在各层之间的一致性: - -**数据流转路径**: -``` -API Request (JSON) - ↓ -Pydantic 验证 → 结构化模型(RangeFilter, FacetConfig 等) - ↓ -Searcher(透传) - ↓ -ES Query Builder → model_dump() 转换为字典 - ↓ -ES Query (字典) - ↓ -Elasticsearch -``` - -#### 8.3.2 Facets 配置数据流 - -**输入格式**:`List[Union[str, FacetConfig]]` - -- **简单模式**:字符串列表(字段名),使用默认配置 - ```json - ["category.keyword", "vendor.keyword"] - ``` - -- **高级模式**:FacetConfig 对象列表,支持自定义配置 - ```json - [ - { - "field": "category.keyword", - "size": 15, - "type": "terms" - }, - { - "field": "price", - "type": "range", - "ranges": [ - {"key": "0-50", "to": 50}, - {"key": "50-100", "from": 50, "to": 100} - ] - } - ] - ``` - -**数据流**: -1. API 层:接收 `List[Union[str, FacetConfig]]` -2. Searcher 层:透传,不做转换 -3. ES Query Builder:只接受 `str` 或 `FacetConfig`,自动处理两种格式 -4. 输出:转换为 ES 聚合查询 - -#### 8.3.3 Range Filters 数据流 - -**输入格式**:`Dict[str, RangeFilter]` - -**RangeFilter 模型**: -```python -class RangeFilter(BaseModel): - gte: Optional[Union[float, str]] # 大于等于 - gt: Optional[Union[float, str]] # 大于 - lte: Optional[Union[float, str]] # 小于等于 - lt: Optional[Union[float, str]] # 小于 -``` - -**示例**: -```json -{ - "price": {"gte": 50, "lte": 200}, - "created_at": {"gte": "2023-01-01T00:00:00Z"} -} -``` - -**数据流**: -1. API 层:接收 `Dict[str, RangeFilter]`,Pydantic 自动验证 -2. Searcher 层:透传 `Dict[str, RangeFilter]` -3. ES Query Builder:调用 `range_filter.model_dump()` 转换为字典 -4. 输出:ES range 查询(支持数值和日期) - -**特性**: -- 自动验证:确保至少指定一个边界值(gte, gt, lte, lt) -- 类型支持:支持数值(float)和日期时间字符串(ISO 格式) -- 统一约定:所有范围过滤都使用 RangeFilter 模型 - -#### 8.3.4 响应 Facets 数据流 - -**输出格式**:`List[FacetResult]` - -**FacetResult 模型**: -```python -class FacetResult(BaseModel): - field: str # 字段名 - label: str # 显示标签 - type: Literal["terms", "range"] # 分面类型 - values: List[FacetValue] # 分面值列表 - total_count: Optional[int] # 总文档数 -``` - -**数据流**: -1. ES Response:返回聚合结果(字典格式) -2. Searcher 层:构建 `List[FacetResult]` 对象 -3. API 层:直接返回 `List[FacetResult]`(Pydantic 自动序列化为 JSON) - -**优势**: -- 类型安全:使用 Pydantic 模型确保数据结构一致性 -- 自动序列化:模型自动转换为 JSON,无需手动处理 -- 统一约定:所有响应都使用标准化的 Pydantic 模型 - -#### 8.3.5 统一约定的好处 - -1. **类型安全**:使用 Pydantic 模型提供运行时类型检查和验证 -2. **代码一致性**:所有层使用相同的数据模型,减少转换错误 -3. **自动文档**:FastAPI 自动生成 API 文档(基于 Pydantic 模型) -4. **易于维护**:修改数据结构只需更新模型定义 -5. **数据验证**:自动验证输入数据,减少错误处理代码 - -**实现模块**: -- `api/models.py` - 所有 Pydantic 模型定义 -- `api/result_formatter.py` - 结果格式化器(ES 响应 → Pydantic 模型) -- `search/es_query_builder.py` - ES 查询构建器(Pydantic 模型 → ES 查询) - -## 9. 配置文件示例 - -**Base配置**(店匠通用):`config/schema/base/config.yaml` - -**其他客户配置**:`config/schema/tenant1/config.yaml` - ---- diff --git a/example_usage.py b/example_usage.py index e1d7f25..68c3135 100644 --- a/example_usage.py +++ b/example_usage.py @@ -38,13 +38,7 @@ def example_basic_usage(): translations={"en": "red dress"} ) - # 步骤2: 布尔解析 - if not context.query_analysis.is_simple_query: - context.start_stage(RequestContextStage.BOOLEAN_PARSING) - time.sleep(0.02) - context.end_stage(RequestContextStage.BOOLEAN_PARSING) - - # 步骤3: ES查询构建 + # 步骤2: ES查询构建 context.start_stage(RequestContextStage.QUERY_BUILDING) time.sleep(0.03) context.end_stage(RequestContextStage.QUERY_BUILDING) @@ -53,7 +47,7 @@ def example_basic_usage(): "size": 10 }) - # 步骤4: ES搜索 + # 步骤3: ES搜索 context.start_stage(RequestContextStage.ELASTICSEARCH_SEARCH) time.sleep(0.1) # 模拟ES响应时间 context.end_stage(RequestContextStage.ELASTICSEARCH_SEARCH) @@ -62,7 +56,7 @@ def example_basic_usage(): "took": 45 }) - # 步骤5: 结果处理 + # 步骤4: 结果处理 context.start_stage(RequestContextStage.RESULT_PROCESSING) time.sleep(0.02) context.end_stage(RequestContextStage.RESULT_PROCESSING) diff --git a/frontend/static/js/app.js b/frontend/static/js/app.js index 1e04e45..c597d96 100644 --- a/frontend/static/js/app.js +++ b/frontend/static/js/app.js @@ -950,7 +950,6 @@ function displayDebugInfo(data) { html += `
detected_language: ${escapeHtml(debugInfo.query_analysis.detected_language || 'N/A')}
`; html += `
index_languages: ${escapeHtml((debugInfo.query_analysis.index_languages || []).join(', ') || 'N/A')}
`; html += `
query_tokens: ${escapeHtml((debugInfo.query_analysis.query_tokens || []).join(', ') || 'N/A')}
`; - html += `
is_simple_query: ${debugInfo.query_analysis.is_simple_query ? 'yes' : 'no'}
`; if (debugInfo.query_analysis.translations && Object.keys(debugInfo.query_analysis.translations).length > 0) { html += '
translations: '; diff --git a/reranker/reranker-共享前缀批量推理.md b/reranker/reranker-共享前缀批量推理.md new file mode 100644 index 0000000..bf51fe9 --- /dev/null +++ b/reranker/reranker-共享前缀批量推理.md @@ -0,0 +1,460 @@ + +## 共享前缀+批量后缀 + +**"共享前缀批量推理"**(Shared-Prefix Batch Inference): +- 输入结构:`[Prefix A] + [Suffix B1]`、`[Prefix A] + [Suffix B2]`、`[Prefix A] + [Suffix B3]`... +- 痛点:Prefix A被重复计算成千上万次,浪费算力和时间 +- 理想方案:Prefix A只推理一次,KV Cache复用给所有后缀 + +--- + +## 一、顶级推荐方案 + +### 1. **FlashInfer + Cascade Attention** ⭐ 最强技术 +- **核心创新**:Cascade Inference(级联推理) +- **性能提升**:相比vLLM的PageAttention,在32K token共享前缀、batch size 256场景下可达 **31倍加速** +- **原理**: + - 将Attention计算分解为两个阶段: + 1. **多查询阶段**:对共享前缀使用Multi-Query Kernel,只计算一次,结果存入Shared Memory + 2. **批量解码阶段**:对每个唯一后缀使用Batch Decode Kernel + - 使用结合律算子合并部分Attention状态,类似FlashAttention的分块策略 +- **适用场景**:文档QA、系统提示词复用、RAG批量检索 +- **集成**:已集成到SGLang和vLLM中作为后端 +- **GitHub**: https://github.com/flashinfer-ai/flashinfer + +### 2. **SGLang + RadixAttention** ⭐ 最实用框架 +- **核心创新**:RadixTree(基数树)管理KV Cache +- **自动前缀复用**:无需手动配置,自动识别共享前缀并复用KV Cache +- **性能**:相比vLLM、LMQL等基线系统,结构化工作负载上可达 **6.4倍吞吐量提升** 和 **3.7倍延迟降低** +- **关键特性**: + - **In-Batch Prefix Caching**:同一batch内自动共享前缀(如你的A+B1, A+B2场景) + - **Multi-Item Scoring (MIS)**:LinkedIn用于推荐排序的优化,将多个候选项合并为单次前向传播 + - **Zero-Overhead CPU Scheduler**:GPU计算时CPU并行准备下一batch,利用率接近100% +- **特别适合**:Agent系统、工具链、RAG应用 +- **GitHub**: https://github.com/sgl-project/sglang + +### 3. **vLLM + Automatic Prefix Caching** ⭐ 最成熟稳定 +- **核心机制**:基于哈希表的块级前缀缓存 +- **工作原理**: + - 将KV Cache按块(默认16 tokens)哈希 + - 新请求先查哈希表,命中则直接复用,只计算新tokens + - 使用LRU策略管理缓存 eviction +- **使用方式**: + ```python + from vllm import LLM, SamplingParams + # 启用prefix caching + llm = LLM(model="your-model", enable_prefix_caching=True) + + # 第一次调用缓存前缀 + outputs = llm.generate(long_prefix + prompt_1, sampling_params) + # 第二次调用自动命中缓存,prefix部分零计算 + outputs = llm.generate(long_prefix + prompt_2, sampling_params) + ``` +- **注意事项**:vLLM 0.6.3之前调度器未考虑缓存命中率,高并发长序列场景可能性能下降,建议升级到0.6.5+ + +--- + +## 二、其他重要方案 + +### 4. **TensorRT-LLM + In-Flight Batching** +- **优势**:NVIDIA官方优化,与FlashInfer深度集成 +- **特性**: + - 支持Prefix Caching(具体实现闭源,但概念类似vLLM) + - XQA Kernel(Flash Attention 3变体)优化内存访问 + - 层融合技术减少中间结果存储 +- **性能**:在共享前缀数据集上,吞吐量提升 **~34.7%**,TPOT降低 **~20.9%** +- **适用**:NVIDIA GPU生产环境,追求极致性能 + +### 5. **LMDeploy + TurboMind** +- **定位**:纯C++引擎,消除Python开销 +- **性能**:与SGLang相当,在H100上可达 **~16,200 tok/s**(vLLM为12,553 tok/s) +- **优化**:支持KV Cache量化(8-bit)、Continuous Batching +- **适用**:高吞吐生产部署,对延迟敏感的场景 + +### 6. **Daft + Dynamic Prefix Bucketing**(大数据场景) +- **创新点**:动态前缀分桶 + 流式Continuous Batching +- **解决痛点**: + - 全局排序分组会导致GPU空闲 + - 动态分桶在推理同时进行前缀分组,实现流水线 +- **性能**:128 GPU集群上,20万prompts(1.28亿tokens)处理速度提升 **50.7%** +- **适用**:大规模离线批处理(如数据标注、合成数据生成) + +--- + +## 三、针对你的具体场景建议 + +### 场景1:在线服务(RAG检索、实时重排序) +**推荐栈**:**SGLang** 或 **vLLM + FlashInfer后端** + +```python +# SGLang示例:自动前缀复用 +import sglang as sgl + +@sgl.function +def rerank(s, query, docs): + # query是共享前缀,docs是批量后缀 + s += "Query: " + query + "\n" + s += "Document: " + sgl.arg(docs) + "\nRelevance:" + s += sgl.gen("score", max_tokens=5) + +# 批量执行,自动共享query部分的KV Cache +docs = ["doc1 content", "doc2 content", "doc3 content", ...] # 成千上万个 +state = rerank.run_batch( + [{"query": "user query", "docs": d} for d in docs], + max_new_tokens=5 +) +``` + +### 场景2:离线批量处理(数据标注、索引构建) +**推荐栈**:**Daft** 或 **FlashInfer原生API** + +```python +# Daft示例:动态前缀分桶 +import daft +from daft.functions import prompt + +df = daft.from_pydict({ + "query": ["shared query"] * 10000, + "doc": ["doc1", "doc2", ...] # 不同后缀 +}) + +df = df.with_column("score", + prompt( + df["query"] + "\n" + df["doc"], + provider="vllm-prefix-caching", # 利用前缀缓存 + model="your-model" + ) +) +``` + +### 场景3:Embedding/Reranker模型(Bi-Encoder/Cross-Encoder) +**推荐栈**:**Sentence-Transformers优化** + **ONNX/TensorRT** + +```python +# Cross-Encoder批量重排序优化 +from sentence_transformers import CrossEncoder +import numpy as np + +class OptimizedReranker: + def __init__(self, model_name="cross-encoder/ms-marco-MiniLM-L-6-v2"): + self.model = CrossEncoder(model_name, max_length=512, device="cuda") + + def rerank_batch(self, query, documents, batch_size=32): + # 构建所有pairs:[query, doc1], [query, doc2], ... + pairs = [[query, doc] for doc in documents] + + # 单次批量推理,自动共享query的编码计算 + scores = self.model.predict( + pairs, + batch_size=batch_size, + convert_to_numpy=True + ) + return np.argsort(scores)[::-1] +``` + +--- + +## 四、性能优化关键技巧 + +### 1. **Prompt构造策略** +- **对齐块边界**:确保共享前缀长度是KV Cache块大小(通常是16或32)的整数倍,避免部分块重计算 +- **静态前缀前置**:将不变的指令、系统提示放在最前面 + +### 2. **Batch构造策略** +- **前缀分桶(Prefix Bucketing)**:将相同前缀的请求分到同一batch,最大化缓存命中率 +- **长度排序**:同batch内按后缀长度排序,减少padding浪费 + +### 3. **硬件/精度优化** +- **FP16/BF16**:混合精度推理,吞吐量提升1.5-2倍 +- **KV Cache量化**:8-bit量化减少内存带宽压力,提升20%吞吐 +- **CUDA Graphs**:捕获静态计算图,减少CPU开销(适合固定batch size场景) + +--- + +## 五、方案对比总结 + +| 方案 | 前缀复用方式 | 适用场景 | 性能提升 | 易用性 | 成熟度 | +|------|-------------|---------|---------|--------|--------| +| **FlashInfer** | Cascade Attention | 超长前缀(32K+) | 31x | 中(需集成) | 高 | +| **SGLang** | RadixTree自动缓存 | Agent/RAG/工具链 | 6.4x | 高 | 高 | +| **vLLM** | 哈希块缓存 | 通用在线服务 | 2-5x | 高 | 极高 | +| **TensorRT-LLM** | 内部优化 | NVIDIA生产环境 | 1.3-2x | 中 | 高 | +| **LMDeploy** | TurboMind C++引擎 | 低延迟高吞吐 | 1.3x | 中 | 高 | +| **Daft** | 动态分桶 | 大规模离线批处理 | 50%+ | 高 | 中 | + +--- + +## 六、推荐实施路径 + +1. **快速验证**:先用 **vLLM** 开启`enable_prefix_caching=True`验证效果,零代码改动 +2. **性能突破**:切换到 **SGLang**,利用RadixAttention和自动batch优化,获得显著性能提升 +3. **极致优化**:对超长前缀场景,使用 **FlashInfer** 的Cascade Attention内核,或集成到自定义推理引擎中 + +需要针对具体框架的详细配置代码或部署架构建议吗? + + + +## 天然支持Prefix Caching的Reranker模型清单 + +最新版本 (v2 系列):混合架构 +在 BGE-Reranker-v2 中,BAAI 为了提升模型上限,引入了不同的底座: + +BGE-Reranker-v2-m3: 依然保持了 Encoder-only 架构(基于 XLM-RoBERTa),支持多语言和长文本,具有极高的性价比。 + +BGE-Reranker-v2-Gemma/Llama: 采用了 Decoder-only 架构。 + +这些模型是基于 Gemma-2b 或 Llama-3-8B 等大语言模型(LLM)微调而来的。 + +虽然原生 LLM 是单向(Causal)注意力的,但在作为 Reranker 使用时,模型通常会通过特殊的 Prompt 引导,并取最后一个 Token 的输出经过一个线性层来计算相关性分数。 + + +### 1. **BGE-Reranker-V2/V2.5 系列** ⭐ 强烈推荐 +基于Gemma/MiniCPM等Decoder-only架构,FlagEmbedding官方实现已优化 + +| 模型 | 架构 | 参数量 | 特点 | +|------|------|--------|------| +| `BAAI/bge-reranker-v2-gemma` | Gemma-2B (Decoder-only) | 2B | 多语言强,基础版 | +| `BAAI/bge-reranker-v2-minicpm-layerwise` | MiniCPM-2B (Decoder-only) | 2B | **支持层选择**,可截断到第24层加速 | +| `BAAI/bge-reranker-v2.5-gemma2-lightweight` | Gemma2-9B (Decoder-only) | 9B | **Token压缩+层选择**,极致效率 | + +**Prefix Caching友好特性** : +- 输入格式:`[Query A] + [SEP] + [Document B] + [Prompt]` +- Query部分作为前缀,可被所有Document共享 +- 官方代码中已实现`compute_score_single_gpu`的batch处理,自动对齐长度排序减少padding + +**使用示例**: +```python +from FlagEmbedding import FlagAutoReranker + +# 启用vLLM后端 + Prefix Caching +reranker = FlagAutoReranker.from_finetuned( + 'BAAI/bge-reranker-v2-gemma', + model_class='decoder-only-base', + use_fp16=True, + devices=['cuda:0'] +) + +# 批量推理:Query自动复用KV Cache +pairs = [ + ('what is panda?', 'The giant panda is a bear species...'), # A+B1 + ('what is panda?', 'Pandas are popular zoo animals.'), # A+B2 (Query复用) + ('what is panda?', 'Pandas eat bamboo and live in China.'), # A+B3 (Query复用) +] +scores = reranker.compute_score(pairs, batch_size=32) +``` + +### 2. **Qwen3-Reranker 系列** ⭐ 国产最强 +基于Qwen3 Decoder-only架构,阿里云官方支持 + +| 模型 | 架构 | 参数量 | 特点 | +|------|------|--------|------| +| `Qwen/Qwen3-Reranker-0.6B` | Qwen3-0.6B (Decoder-only) | 0.6B | 超轻量,快速 | +| `Qwen/Qwen3-Reranker-4B` | Qwen3-4B (Decoder-only) | 4B | 性能均衡 | +| `Qwen/Qwen3-Reranker-8B` | Qwen3-8B (Decoder-only) | 8B | 精度最高 | + +**架构细节** : +- **纯Decoder-only架构**:使用因果注意力,天然支持Prefix Caching +- **输入模板**: + ``` + <|im_start|>system + You are a helpful assistant. + <|im_end|> + <|im_start|>user + Query: {query} + Document: {document} + Does the document answer the query? Please answer Yes or No. + <|im_end|> + <|im_start|>assistant + Yes + ``` +- **输出**:只生成"Yes"或"No"的logits,作为相关性分数 + +**vLLM部署优化** : +```bash +# 启动vLLM服务,启用Prefix Caching +python -m vllm.entrypoints.openai.api_server \ + --model Qwen/Qwen3-Reranker-8B \ + --tensor-parallel-size 1 \ + --dtype half \ + --max-model-len 32768 \ + --enable-prefix-caching # 关键参数!提速40% +``` + +### 3. **Jina-Reranker-V3** ⭐ 创新架构 +基于Qwen3-0.6B的Listwise重排序器,支持跨Document注意力 + +| 模型 | 架构 | 参数量 | 特点 | +|------|------|--------|------| +| `jinaai/jina-reranker-v3` | Qwen3-0.6B (Decoder-only) | 0.6B | **Listwise**,单次处理64个docs | + +**独特优势** : +- **"Last but Not Late"交互**:在单个context window中同时处理Query+所有Documents +- **跨Document注意力**:通过因果注意力实现Document间交互,捕捉相对相关性 +- **Prefix Caching优化**:Query放在序列开头,可被所有Document共享 + +**输入格式** : +```xml +<|im_start|>system +You are a search relevance expert... +<|im_end|> +<|im_start|>user +Rank the passages based on their relevance to query: [QUERY] + +[DOC_1]<|doc_emb|> +[DOC_2]<|doc_emb|> +... +[DOC_k]<|doc_emb|> + +[QUERY]<|query_emb|> +<|im_end|> +``` + +**性能**:BEIR nDCG@10达61.94,超过Qwen3-Reranker-4B,体积小6倍 + +### 4. **E5-Mistral / NV-Embed-v2 / SFR-Embedding-Mistral** +基于Mistral-7B Decoder-only架构的Embedding/Reranker + +| 模型 | 架构 | 用途 | 特点 | +|------|------|------|------| +| `intfloat/e5-mistral-7b-instruct` | Mistral-7B (Decoder-only) | Embedding | 指令微调,支持多任务 | +| `nvidia/NV-Embed-v2` | Mistral-7B (Decoder-only) | Embedding | 潜在注意力层优化 | +| `Salesforce/SFR-Embedding-Mistral` | Mistral-7B (Decoder-only) | Embedding | 长上下文优化 | + +**注意**:这些主要是**Embedding模型**(Bi-encoder),但可作为Reranker使用(如通过余弦相似度)。若需Cross-encoder式重排序,需配合其他技术。 + +### 5. **RankGPT / RankZephyr / RankLLaMA** +基于LLM的生成式重排序器 + +| 模型 | 架构 | 特点 | +|------|------|------| +| `RankGPT` (GPT-3.5/4) | Decoder-only API | 通过Prompt让LLM判断相关性 | +| `RankZephyr` | Zephyr-7B (Decoder-only) | 蒸馏自RankGPT,开源可部署 | +| `RankLLaMA` | LLaMA-2/3 (Decoder-only) | 本地部署,隐私友好 | + +**实现方式**: +- 使用LLM的logits或生成"Yes/No"判断相关性 +- 完全基于Decoder-only架构,天然支持Prefix Caching + +--- + +## 不支持Prefix Caching的Reranker(双向架构) + +### ❌ **BGE-Reranker-V1 / BGE-Reranker-Base/Large** +- **架构**:基于XLM-RoBERTa(Encoder-only,双向注意力) +- **问题**:Query和Document拼接后`[CLS]` token的表示依赖于整个序列,无法分离缓存 +- **适用场景**:轻量级、短文本,对延迟不敏感 + +### ❌ **Cross-Encoder (BERT-based)** +- **架构**:BERT/RoBERTa等Encoder-only模型 +- **问题**: + - 每个Query-Document对必须**联合编码** + - 前缀的KV Cache与后续token强耦合,无法复用 + - 计算复杂度O((|Q|+|D|)²),无法分解为O(|Q|²) + O(|D|²) + +### ❌ **ColBERT / ColPali / Late Interaction模型** +- **架构**:基于BERT的双向编码 + 后期MaxSim交互 +- **问题**: + - **独立编码,但双向注意力**:Query和Document分别编码,但各自内部仍是双向 + - **无法Prefix Cache**:虽然Query可独立编码,但Document的编码不依赖于Query,所以不存在"共享前缀"场景 + - **优化点**:Document可**预计算并离线存储**,Query实时编码,然后做MaxSim + - **本质区别**:这是"离线预计算"而非"Prefix Caching",适用于Document固定、Query变化的场景 + +**ColBERT的优化策略** : +```python +# ColBERT流程:Document预计算(离线) + Query实时编码(在线) +document_embeddings = encode_documents(docs) # 离线,一次性 +query_embedding = encode_query(query) # 在线,每次查询 +scores = maxsim(query_embedding, document_embeddings) # 轻量级交互 +``` + +--- + +## 实际部署建议 + +### 场景1:高并发在线服务(Query多变,Document固定) +**推荐**:**Jina-Reranker-V3** 或 **Qwen3-Reranker + vLLM Prefix Caching** + +```python +# vLLM配置优化 +from vllm import LLM, SamplingParams + +llm = LLM( + model="Qwen/Qwen3-Reranker-8B", + enable_prefix_caching=True, # 关键! + max_num_seqs=256, # 高并发 + max_model_len=32768 +) + +# 批量构造Prompts:共享Query前缀 +query = "What is the capital of France?" +docs = ["Paris is the capital...", "France is a country...", "Berlin is the capital of Germany..."] + +prompts = [ + f"Query: {query}\nDocument: {doc}\nRelevant:" + for doc in docs +] + +# vLLM自动识别共享前缀,只计算一次Query的KV Cache +sampling_params = SamplingParams(max_tokens=1, temperature=0) +outputs = llm.generate(prompts, sampling_params) +``` + +### 场景2:离线批量重排序(Query固定,Document多变) +**推荐**:**ColBERT / ColPali**(Document预计算策略) + +```python +from rankify.models.reranking import Reranking +from rankify.dataset.dataset import Document, Question, Context + +# ColBERT:Document预计算,Query实时编码 +model = Reranking(method='colbert_ranker', model_name='Colbert') + +# Documents已预计算并存储 +document = Document( + question=Question("What is RAG?"), + contexts=[Context(text=doc, id=i) for i, doc in enumerate(docs)] +) + +# 只需编码Query,然后MaxSim计算 +model.rank([document]) +``` + +### 场景3:极致性能 + 高精度 +**推荐**:**BGE-Reranker-V2.5-Gemma2-Lightweight**(Token压缩 + 层选择) + +```python +from FlagEmbedding import LightWeightFlagLLMReranker + +reranker = LightWeightFlagLLMReranker( + 'BAAI/bge-reranker-v2.5-gemma2-lightweight', + devices=["cuda:0"], + use_fp16=True +) + +# 综合优化:层截断 + Token压缩 +scores = reranker.compute_score( + pairs, + cutoff_layers=[28], # 只用前28层 + compress_ratio=4, # Token压缩4倍 + compress_layers=[24, 40] # 特定层压缩 +) +``` + +--- + +## 总结对比表 + +| 模型 | 架构 | Prefix Caching | 适用场景 | 性能/效率 | +|------|------|---------------|---------|----------| +| **BGE-Reranker-V2/V2.5** | Decoder-only ✅ | ✅ 原生支持 | 多语言、生产环境 | 高 | +| **Qwen3-Reranker** | Decoder-only ✅ | ✅ vLLM支持 | 中文优先、高精度 | 极高 | +| **Jina-Reranker-V3** | Decoder-only ✅ | ✅ Listwise优化 | 跨Doc交互、Top-K排序 | 极高 | +| **E5-Mistral/NV-Embed** | Decoder-only ✅ | ✅ 需配合框架 | Embedding+轻量重排 | 高 | +| **RankGPT/Zephyr** | Decoder-only ✅ | ✅ API/本地 | 生成式判断 | 中 | +| **BGE-Reranker-V1** | Encoder-only ❌ | ❌ 不支持 | 轻量、短文本 | 低 | +| **ColBERT/ColPali** | 双向+后期交互 | ❌ 不适用(预计算替代) | Document固定场景 | 中 | + +**最终建议**: +- 如果追求**Prefix Caching加速** + **高精度**:选择 **Qwen3-Reranker-8B** 或 **BGE-Reranker-V2.5**,配合 **vLLM + FlashInfer** 部署 +- 如果需要**跨Document比较**(Listwise):选择 **Jina-Reranker-V3** +- 如果**Document固定且量大**:选择 **ColBERT** 预计算策略(虽非Prefix Caching,但效率类似) \ No newline at end of file diff --git a/search/searcher.py b/search/searcher.py index f5fcea7..718ede2 100644 --- a/search/searcher.py +++ b/search/searcher.py @@ -396,7 +396,6 @@ class Searcher: detected_language=parsed_query.detected_language, translations=parsed_query.translations, query_vector=parsed_query.query_vector.tolist() if parsed_query.query_vector is not None else None, - is_simple_query=True ) context.metadata["feature_flags"]["style_intent_active"] = self._has_style_intent(parsed_query) @@ -865,7 +864,6 @@ class Searcher: "translations": context.query_analysis.translations, "has_vector": context.query_analysis.query_vector is not None, "query_tokens": getattr(parsed_query, "query_tokens", []), - "is_simple_query": context.query_analysis.is_simple_query, "intent_detection": context.get_intermediate_result("style_intent_profile"), }, "es_query": context.get_intermediate_result('es_query', {}), -- libgit2 0.21.2