搜索引擎通用化开发进度
项目概述
对后端搜索技术 做通用化。 通用化的本质 是 对于各种业务数据、各种检索需求,都可以 用少量定制+配置化 来实现效果。
通用化的本质:对于各种业务数据、各种检索需求,都可以用少量定制+配置化来实现效果。
1. 原始数据层的约定
1.1 店匠主表
所有租户共用以下主表:
shoplazza_product_sku- SKU级别商品数据shoplazza_product_spu- SPU级别商品数据
1.2 索引结构(SPU维度)
统一索引架构:
- 所有客户共享同一个Elasticsearch索引:
search_products - 索引粒度:SPU级别(每个文档代表一个SPU)
- 数据隔离:通过
tenant_id字段实现租户隔离 - 嵌套结构:每个SPU文档包含嵌套的
skus数组
索引文档结构:
{
"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) - 统一索引结构:所有租户共享相同的索引结构,通过
tenant_id隔离数据 - 数据源统一:所有租户使用相同的 MySQL 表结构(店匠标准表)
- 查询配置硬编码:查询相关配置(字段 boost、查询域等)硬编码在
search/query_config.py
索引结构特点:
- 多语言字段:所有文本字段支持中英文(
title_zh/en,brief_zh/en,description_zh/en,vendor_zh/en,category_path_zh/en,category_name_zh/en) - 嵌套字段:
skus: SKU 嵌套数组(包含价格、库存、选项值等)specifications: 规格嵌套数组(包含 name、value、sku_id)image_embedding: 图片向量嵌套数组
- 扁平化字段:
sku_prices,sku_weights,total_inventory等用于过滤和排序 - 向量字段:
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文件中 - 所有租户共享相同的索引结构
- 查询配置硬编码在
search/query_config.py
索引字段结构:
基础字段
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_zh/en(text): 类目名称(中英文)
分析器配置:
- 中文字段:
hanlp_index(索引时)/hanlp_standard(查询时) - 英文字段:
english vendor字段包含keyword子字段(normalizer: lowercase)
分类字段
category_id(keyword): 类目IDcategory_name(keyword): 类目名称category_level(integer): 类目层级category1_name,category2_name,category3_name(keyword): 多级类目名称
规格字段(Specifications)
specifications(nested): 规格嵌套数组sku_id(keyword): SKU IDname(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_codestock,weight,weight_unitoption1_value,option2_value,option3_valueimage_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)
多语言字段映射
支持将不同语言的查询路由到对应的字段:
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"
工作原理:
- 检测查询语言(中文、英文、俄文等)
- 如果查询语言在
language_field_mapping中,使用原始查询搜索对应语言的字段 - 将查询翻译到其他支持的语言,分别搜索对应语言的字段
- 组合多个语言查询的结果,提高召回率
实现模块:
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
数据流程:
数据加载:
- 从MySQL读取
shoplazza_product_spu表(SPU数据) - 从MySQL读取
shoplazza_product_sku表(SKU数据) - 从MySQL读取
shoplazza_product_option表(选项定义)
- 从MySQL读取
数据转换(
indexer/spu_transformer.py):- 按
spu_id和tenant_id关联SPU和SKU数据 - 多语言字段映射:
- MySQL的
title→ ES的title_zh(英文字段设为空) - 其他文本字段类似处理
- MySQL的
- 分类字段映射:
- 从SPU表的
category_path解析多级类目(category1_name,category2_name,category3_name) - 映射
category_id,category_name,category_level
- 从SPU表的
- 规格字段构建(
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字段
- 按
索引创建:
- 从
mappings/search_products.json加载ES mapping - 创建或更新
search_products索引
- 从
批量入库:
- 批量写入ES(默认每批500条)
- 错误处理和重试机制
命令行工具:
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 中配置查询改写规则:
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)
实现情况:
配置方式
query_config:
supported_languages:
- "zh"
- "en"
- "ru"
default_language: "zh"
enable_translation: true
translation_service: "deepl"
translation_api_key: null # 通过环境变量设置
功能特性
- 语言检测:自动检测查询语言
- 智能翻译:
- 如果查询是中文,翻译为英文、俄文
- 如果查询是英文,翻译为中文、俄文
- 如果查询是其他语言,翻译为所有支持的语言
- 域感知翻译:
- 如果域有
language_field_mapping,只翻译到映射中存在的语言 - 避免不必要的翻译,提高效率
- 如果域有
- 翻译缓存:缓存翻译结果,避免重复调用 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会用这个向量参与查询。
实现情况:
配置方式
query_config:
enable_text_embedding: true
功能特性
- 条件生成:
- 仅当
enable_text_embedding=true时生成向量 - 仅对
default域查询生成向量
- 仅当
- 向量模型:BGE-M3 模型(1024维向量)
- 用途:用于语义搜索(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:排除(第一项匹配,第二项不匹配)
- ():括号分组
优先级(从高到低)
()- 括号ANDNOT- 排除AND- 与OR- 或RANK- 排序
示例
laptop AND (gaming OR professional) ANDNOT cheap
实现模块
search/boolean_parser.py- 布尔表达式解析器search/searcher.py- 搜索器(集成布尔解析)
5.2 多语言搜索
实现情况:
工作原理
- 查询解析:
- 提取域(如
title:查询→ 域=title,查询=查询) - 检测查询语言
- 生成翻译
- 提取域(如
- 查询构建(简化架构):
- 结构:
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_zh/en,tags) - 使用
multi_match查询,支持字段 boost - 中文字段使用中文分词器,英文字段使用英文分析器
- 同时搜索中英文字段(
- embedding_recall: 向量召回(KNN)
- 使用
title_embedding字段进行 KNN 搜索 - ES 自动与文本召回合并(OR逻辑)
- 使用
- function_score: 包装召回部分,支持提权字段(新鲜度、销量等)
- 结构:
查询结构示例
{
"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
配置方式
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)
- ✅ Specifications嵌套过滤(单个和多个规格,按维度分组:不同维度AND,相同维度OR)
- ✅ Specifications嵌套分面(所有规格名称和指定规格名称)
- ✅ SKU筛选(按维度过滤,应用层实现)
6.5 API 服务
- ✅ RESTful API(FastAPI)
- ✅ 搜索接口(文本搜索、图片搜索)
- ✅ 文档查询接口
- ✅ 前端界面(HTML + JavaScript)
- ✅ 租户隔离(tenant_id过滤)
6.6 索引结构(店匠通用)
- ✅ SPU级别索引结构
- ✅ 多语言字段支持(中英文)
- ✅ 嵌套字段(skus, specifications, image_embedding)
- ✅ 规格字段(specifications)支持过滤和分面
- ✅ 扁平化字段(价格、库存等)用于过滤和排序
- ✅ 统一索引(search_products)
- ✅ 租户隔离(tenant_id)
- ✅ 硬编码映射(mappings/search_products.json)
- ✅ 硬编码查询配置(search/query_config.py)
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),使用外部友好的格式:
响应结构:
{
"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 对象
[
{
"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 列表
数据流:
- API 层:接收
List[FacetConfig],Pydantic 验证参数 - Searcher 层:透传 FacetConfig 对象列表
- ES Query Builder:解析 FacetConfig 对象
- 检测
"specifications"或"specifications.{name}"格式 - 构建对应的嵌套聚合查询或普通聚合查询
- 检测
- 输出:转换为 ES 聚合查询(包括 specifications 嵌套聚合)
- Result Formatter:格式化 ES 聚合结果,处理 specifications 嵌套结构
8.3.3 Range Filters 数据流
输入格式:Dict[str, RangeFilter]
RangeFilter 模型:
class RangeFilter(BaseModel):
gte: Optional[Union[float, str]] # 大于等于
gt: Optional[Union[float, str]] # 大于
lte: Optional[Union[float, str]] # 小于等于
lt: Optional[Union[float, str]] # 小于
示例:
{
"min_price": {"gte": 50, "lte": 200},
"create_time": {"gte": "2023-01-01T00:00:00Z"}
}
数据流:
- API 层:接收
Dict[str, RangeFilter],Pydantic 自动验证 - Searcher 层:透传
Dict[str, RangeFilter] - ES Query Builder:调用
range_filter.model_dump()转换为字典 - 输出:ES range 查询(支持数值和日期)
特性:
- 自动验证:确保至少指定一个边界值(gte, gt, lte, lt)
- 类型支持:支持数值(float)和日期时间字符串(ISO 格式)
- 统一约定:所有范围过滤都使用 RangeFilter 模型
8.3.3.1 Specifications 过滤数据流
输入格式:Dict[str, Union[Dict[str, str], List[Dict[str, str]]]]
单个规格过滤:
{
"specifications": {
"name": "color",
"value": "white"
}
}
多个规格过滤(按维度分组):
{
"specifications": [
{"name": "color", "value": "white"},
{"name": "size", "value": "256GB"}
]
}
数据流:
- API 层:接收
filters字典,检测specifications键 - Searcher 层:透传
filters字典 - ES Query Builder:检测
specifications键,构建ESnested查询- 单个规格:构建单个
nested查询 - 多个规格:按 name 维度分组,相同维度内使用
should组合(OR逻辑),不同维度之间使用must组合(AND逻辑)
- 单个规格:构建单个
- 输出:ES nested 查询(
nested.path=specifications+bool.must=[term(name), term(value)])
8.3.4 响应 Facets 数据流
输出格式:List[FacetResult]
FacetResult 模型:
class FacetResult(BaseModel):
field: str # 字段名
label: str # 显示标签
type: Literal["terms", "range"] # 分面类型
values: List[FacetValue] # 分面值列表
total_count: Optional[int] # 总文档数
数据流:
- ES Response:返回聚合结果(字典格式,包括specifications嵌套聚合)
- Result Formatter:格式化ES聚合结果
- 处理普通terms聚合
- 处理range聚合
- 处理specifications嵌套聚合:
- 所有规格名称:解析
by_name聚合结构 - 指定规格名称:解析
filter_by_name聚合结构
- 所有规格名称:解析
- Searcher 层:构建
List[FacetResult]对象 - 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匹配
示例:
{
"query": "手机",
"sku_filter_dimension": "color"
}
数据流:
- API 层:接收
sku_filter_dimension字符串参数 - Searcher 层:透传到 Result Formatter
- Result Formatter:在格式化结果时,按指定维度对SKU进行分组
- 如果维度是
option1/2/3,直接使用对应的option1_value/2/3字段 - 如果维度是规格名称,通过
option1_name/2/3匹配找到对应的option1_value/2/3 - 每个分组选择第一个SKU返回
- 如果维度是
- 输出:过滤后的SKU列表(每个维度值一个SKU)
工作原理:
- 系统从ES返回所有SKU(不改变ES查询,保持性能)
- 在结果格式化阶段,按指定维度对SKU进行分组
- 每个分组选择第一个SKU返回
- 如果维度不匹配或未找到,返回所有SKU(不进行过滤)
性能说明:
- ✅ 推荐方案: 在应用层过滤(当前实现)
- ES查询简单,不需要nested查询和join
- 只对返回的结果(通常10-20个SPU)进行过滤,数据量小
- 实现简单,性能开销小
- ❌ 不推荐: 在ES查询时过滤
- 需要nested查询和join,性能开销大
- 实现复杂
- 只对返回的结果需要过滤,不需要在ES层面过滤
8.3.6 统一约定的好处
- 类型安全:使用 Pydantic 模型提供运行时类型检查和验证
- 代码一致性:所有层使用相同的数据模型,减少转换错误
- 自动文档:FastAPI 自动生成 API 文档(基于 Pydantic 模型)
- 易于维护:修改数据结构只需更新模型定义
- 数据验证:自动验证输入数据,减少错误处理代码
实现模块:
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