Commit 577ec9724155f6d2cc94fa08e4e368a87245fcc0

Authored by tangwang
1 parent bf89b597

返回给前端的字段、格式适配。主要包括字段配置、前端补充一个语言字段处理title_en title_zh等语言选择、分面信息的提取等

@@ -69,6 +69,10 @@ class SearchRequest(BaseModel): @@ -69,6 +69,10 @@ class SearchRequest(BaseModel):
69 query: str = Field(..., description="搜索查询字符串,支持布尔表达式(AND, OR, RANK, ANDNOT)") 69 query: str = Field(..., description="搜索查询字符串,支持布尔表达式(AND, OR, RANK, ANDNOT)")
70 size: int = Field(10, ge=1, le=100, description="返回结果数量") 70 size: int = Field(10, ge=1, le=100, description="返回结果数量")
71 from_: int = Field(0, ge=0, alias="from", description="分页偏移量") 71 from_: int = Field(0, ge=0, alias="from", description="分页偏移量")
  72 + language: Literal["zh", "en"] = Field(
  73 + "zh",
  74 + description="响应语言:'zh'(中文)或 'en'(英文),用于选择 title/description/vendor 等多语言字段"
  75 + )
72 76
73 # 过滤器 - 精确匹配和多值匹配 77 # 过滤器 - 精确匹配和多值匹配
74 filters: Optional[Dict[str, Union[str, int, bool, List[Union[str, int]]]]] = Field( 78 filters: Optional[Dict[str, Union[str, int, bool, List[Union[str, int]]]]] = Field(
@@ -175,28 +179,53 @@ class FacetResult(BaseModel): @@ -175,28 +179,53 @@ class FacetResult(BaseModel):
175 class SkuResult(BaseModel): 179 class SkuResult(BaseModel):
176 """SKU 结果""" 180 """SKU 结果"""
177 sku_id: str = Field(..., description="SKU ID") 181 sku_id: str = Field(..., description="SKU ID")
178 - title: Optional[str] = Field(None, description="SKU标题") 182 + # 与 ES nested skus 结构对齐
179 price: Optional[float] = Field(None, description="价格") 183 price: Optional[float] = Field(None, description="价格")
180 compare_at_price: Optional[float] = Field(None, description="原价") 184 compare_at_price: Optional[float] = Field(None, description="原价")
181 - sku: Optional[str] = Field(None, description="SKU编码") 185 + sku_code: Optional[str] = Field(None, description="SKU编码")
182 stock: int = Field(0, description="库存数量") 186 stock: int = Field(0, description="库存数量")
183 - options: Optional[Dict[str, Any]] = Field(None, description="选项(颜色、尺寸等)") 187 + weight: Optional[float] = Field(None, description="重量")
  188 + weight_unit: Optional[str] = Field(None, description="重量单位")
  189 + option1_value: Optional[str] = Field(None, description="选项1取值(如颜色)")
  190 + option2_value: Optional[str] = Field(None, description="选项2取值(如尺码)")
  191 + option3_value: Optional[str] = Field(None, description="选项3取值")
  192 + image_src: Optional[str] = Field(None, description="SKU图片地址")
184 193
185 194
186 class SpuResult(BaseModel): 195 class SpuResult(BaseModel):
187 """SPU 搜索结果""" 196 """SPU 搜索结果"""
188 spu_id: str = Field(..., description="SPU ID") 197 spu_id: str = Field(..., description="SPU ID")
189 title: Optional[str] = Field(None, description="商品标题") 198 title: Optional[str] = Field(None, description="商品标题")
  199 + brief: Optional[str] = Field(None, description="商品短描述")
190 handle: Optional[str] = Field(None, description="商品handle") 200 handle: Optional[str] = Field(None, description="商品handle")
191 description: Optional[str] = Field(None, description="商品描述") 201 description: Optional[str] = Field(None, description="商品描述")
192 vendor: Optional[str] = Field(None, description="供应商/品牌") 202 vendor: Optional[str] = Field(None, description="供应商/品牌")
193 - category: Optional[str] = Field(None, description="类目") 203 + category: Optional[str] = Field(None, description="类目(兼容字段,等同于category_name)")
  204 + category_path: Optional[str] = Field(None, description="类目路径(多级,用于面包屑)")
  205 + category_name: Optional[str] = Field(None, description="类目名称(展示用)")
  206 + category_id: Optional[str] = Field(None, description="类目ID")
  207 + category_level: Optional[int] = Field(None, description="类目层级")
  208 + category1_name: Optional[str] = Field(None, description="一级类目名称")
  209 + category2_name: Optional[str] = Field(None, description="二级类目名称")
  210 + category3_name: Optional[str] = Field(None, description="三级类目名称")
194 tags: Optional[List[str]] = Field(None, description="标签列表") 211 tags: Optional[List[str]] = Field(None, description="标签列表")
195 price: Optional[float] = Field(None, description="价格(min_price)") 212 price: Optional[float] = Field(None, description="价格(min_price)")
196 compare_at_price: Optional[float] = Field(None, description="原价") 213 compare_at_price: Optional[float] = Field(None, description="原价")
197 currency: str = Field("USD", description="货币单位") 214 currency: str = Field("USD", description="货币单位")
198 image_url: Optional[str] = Field(None, description="主图URL") 215 image_url: Optional[str] = Field(None, description="主图URL")
199 in_stock: bool = Field(True, description="是否有库存") 216 in_stock: bool = Field(True, description="是否有库存")
  217 + # SKU 扁平化信息
  218 + sku_prices: Optional[List[float]] = Field(None, description="所有SKU价格列表")
  219 + sku_weights: Optional[List[int]] = Field(None, description="所有SKU重量列表")
  220 + sku_weight_units: Optional[List[str]] = Field(None, description="所有SKU重量单位列表")
  221 + total_inventory: Optional[int] = Field(None, description="总库存")
  222 + option1_name: Optional[str] = Field(None, description="选项1名称(如颜色)")
  223 + option2_name: Optional[str] = Field(None, description="选项2名称(如尺码)")
  224 + option3_name: Optional[str] = Field(None, description="选项3名称")
  225 + specifications: Optional[List[Dict[str, Any]]] = Field(
  226 + None,
  227 + description="规格列表(与 ES specifications 字段对应)"
  228 + )
200 skus: List[SkuResult] = Field(default_factory=list, description="SKU列表") 229 skus: List[SkuResult] = Field(default_factory=list, description="SKU列表")
201 relevance_score: float = Field(..., ge=0.0, description="相关性分数(ES原始分数)") 230 relevance_score: float = Field(..., ge=0.0, description="相关性分数(ES原始分数)")
202 231
api/result_formatter.py
@@ -12,7 +12,8 @@ class ResultFormatter: @@ -12,7 +12,8 @@ class ResultFormatter:
12 @staticmethod 12 @staticmethod
13 def format_search_results( 13 def format_search_results(
14 es_hits: List[Dict[str, Any]], 14 es_hits: List[Dict[str, Any]],
15 - max_score: float = 1.0 15 + max_score: float = 1.0,
  16 + language: str = "zh"
16 ) -> List[SpuResult]: 17 ) -> List[SpuResult]:
17 """ 18 """
18 Convert ES hits to SpuResult list. 19 Convert ES hits to SpuResult list.
@@ -25,6 +26,18 @@ class ResultFormatter: @@ -25,6 +26,18 @@ class ResultFormatter:
25 List of SpuResult objects 26 List of SpuResult objects
26 """ 27 """
27 results = [] 28 results = []
  29 + lang = (language or "zh").lower()
  30 + if lang not in ("zh", "en"):
  31 + lang = "en"
  32 +
  33 + def pick_lang_field(src: Dict[str, Any], base: str) -> Optional[str]:
  34 + """从 *_zh / *_en 字段中按语言选择一个值,若目标语言缺失则回退到另一种。"""
  35 + zh_val = src.get(f"{base}_zh")
  36 + en_val = src.get(f"{base}_en")
  37 + if lang == "zh":
  38 + return zh_val or en_val
  39 + else:
  40 + return en_val or zh_val
28 41
29 for hit in es_hits: 42 for hit in es_hits:
30 source = hit.get('_source', {}) 43 source = hit.get('_source', {})
@@ -40,6 +53,14 @@ class ResultFormatter: @@ -40,6 +53,14 @@ class ResultFormatter:
40 except (ValueError, TypeError): 53 except (ValueError, TypeError):
41 relevance_score = 0.0 54 relevance_score = 0.0
42 55
  56 + # Multi-language fields
  57 + title = pick_lang_field(source, "title")
  58 + brief = pick_lang_field(source, "brief")
  59 + description = pick_lang_field(source, "description")
  60 + vendor = pick_lang_field(source, "vendor")
  61 + category_path = pick_lang_field(source, "category_path")
  62 + category_name = pick_lang_field(source, "category_name")
  63 +
43 # Extract SKUs 64 # Extract SKUs
44 skus = [] 65 skus = []
45 skus_data = source.get('skus', []) 66 skus_data = source.get('skus', [])
@@ -62,17 +83,33 @@ class ResultFormatter: @@ -62,17 +83,33 @@ class ResultFormatter:
62 # Build SpuResult 83 # Build SpuResult
63 spu = SpuResult( 84 spu = SpuResult(
64 spu_id=str(source.get('spu_id', '')), 85 spu_id=str(source.get('spu_id', '')),
65 - title=source.get('title'), 86 + title=title,
  87 + brief=brief,
66 handle=source.get('handle'), 88 handle=source.get('handle'),
67 - description=source.get('description'),  
68 - vendor=source.get('vendor'),  
69 - category=source.get('category'), 89 + description=description,
  90 + vendor=vendor,
  91 + category=category_name,
  92 + category_path=category_path,
  93 + category_name=category_name,
  94 + category_id=source.get('category_id'),
  95 + category_level=source.get('category_level'),
  96 + category1_name=source.get('category1_name'),
  97 + category2_name=source.get('category2_name'),
  98 + category3_name=source.get('category3_name'),
70 tags=source.get('tags'), 99 tags=source.get('tags'),
71 price=source.get('min_price'), 100 price=source.get('min_price'),
72 compare_at_price=source.get('compare_at_price'), 101 compare_at_price=source.get('compare_at_price'),
73 currency="USD", # Default currency 102 currency="USD", # Default currency
74 image_url=source.get('image_url'), 103 image_url=source.get('image_url'),
75 in_stock=in_stock, 104 in_stock=in_stock,
  105 + sku_prices=source.get('sku_prices'),
  106 + sku_weights=source.get('sku_weights'),
  107 + sku_weight_units=source.get('sku_weight_units'),
  108 + total_inventory=source.get('total_inventory'),
  109 + option1_name=source.get('option1_name'),
  110 + option2_name=source.get('option2_name'),
  111 + option3_name=source.get('option3_name'),
  112 + specifications=source.get('specifications'),
76 skus=skus, 113 skus=skus,
77 relevance_score=relevance_score 114 relevance_score=relevance_score
78 ) 115 )
api/routes/search.py
@@ -94,7 +94,8 @@ async def search(request: SearchRequest, http_request: Request): @@ -94,7 +94,8 @@ async def search(request: SearchRequest, http_request: Request):
94 context=context, 94 context=context,
95 sort_by=request.sort_by, 95 sort_by=request.sort_by,
96 sort_order=request.sort_order, 96 sort_order=request.sort_order,
97 - debug=request.debug 97 + debug=request.debug,
  98 + language=request.language,
98 ) 99 )
99 100
100 # Include performance summary in response 101 # Include performance summary in response
docs/ES常用表达式.md
@@ -7,3 +7,14 @@ GET /search_products/_search @@ -7,3 +7,14 @@ GET /search_products/_search
7 } 7 }
8 } 8 }
9 9
  10 +
  11 +curl -u 'essa:4hOaLaf41y2VuI8y' -X GET 'http://localhost:9200/search_products/_search?pretty' -H 'Content-Type: application/json' -d '{
  12 + "size": 5,
  13 + "query": {
  14 + "bool": {
  15 + "filter": [
  16 + { "term": { "tenant_id": "162" } }
  17 + ]
  18 + }
  19 + }
  20 + }'
10 \ No newline at end of file 21 \ No newline at end of file
search/query_config.py
@@ -37,17 +37,32 @@ DOMAIN_FIELDS: Dict[str, List[str]] = { @@ -37,17 +37,32 @@ DOMAIN_FIELDS: Dict[str, List[str]] = {
37 } 37 }
38 38
39 # Source fields to return in search results 39 # Source fields to return in search results
  40 +# 注意:为了在后端做多语言选择,_zh / _en 字段仍然需要从 ES 取出,
  41 +# 但不会原样透出给前端,而是统一映射到 title / description / vendor 等字段。
40 SOURCE_FIELDS = [ 42 SOURCE_FIELDS = [
  43 + # 基本标识
41 "tenant_id", 44 "tenant_id",
42 "spu_id", 45 "spu_id",
  46 + "create_time",
  47 + "update_time",
  48 +
  49 + # 多语言文本字段(仅用于后端选择,不直接返回给前端)
43 "title_zh", 50 "title_zh",
  51 + "title_en",
44 "brief_zh", 52 "brief_zh",
  53 + "brief_en",
45 "description_zh", 54 "description_zh",
  55 + "description_en",
46 "vendor_zh", 56 "vendor_zh",
47 - "tags",  
48 - "image_url", 57 + "vendor_en",
49 "category_path_zh", 58 "category_path_zh",
  59 + "category_path_en",
50 "category_name_zh", 60 "category_name_zh",
  61 + "category_name_en",
  62 +
  63 + # 语言无关字段(直接返回给前端)
  64 + "tags",
  65 + "image_url",
51 "category_id", 66 "category_id",
52 "category_name", 67 "category_name",
53 "category_level", 68 "category_level",
@@ -60,11 +75,12 @@ SOURCE_FIELDS = [ @@ -60,11 +75,12 @@ SOURCE_FIELDS = [
60 "min_price", 75 "min_price",
61 "max_price", 76 "max_price",
62 "compare_at_price", 77 "compare_at_price",
  78 + "sku_prices",
  79 + "sku_weights",
  80 + "sku_weight_units",
63 "total_inventory", 81 "total_inventory",
64 - "create_time",  
65 - "update_time",  
66 "skus", 82 "skus",
67 - "specifications" 83 + "specifications",
68 ] 84 ]
69 85
70 # Query processing settings 86 # Query processing settings
search/searcher.py
@@ -134,7 +134,8 @@ class Searcher: @@ -134,7 +134,8 @@ class Searcher:
134 context: Optional[RequestContext] = None, 134 context: Optional[RequestContext] = None,
135 sort_by: Optional[str] = None, 135 sort_by: Optional[str] = None,
136 sort_order: Optional[str] = "desc", 136 sort_order: Optional[str] = "desc",
137 - debug: bool = False 137 + debug: bool = False,
  138 + language: str = "zh",
138 ) -> SearchResult: 139 ) -> SearchResult:
139 """ 140 """
140 Execute search query (外部友好格式). 141 Execute search query (外部友好格式).
@@ -373,7 +374,11 @@ class Searcher: @@ -373,7 +374,11 @@ class Searcher:
373 max_score = es_response.get('hits', {}).get('max_score') or 0.0 374 max_score = es_response.get('hits', {}).get('max_score') or 0.0
374 375
375 # Format results using ResultFormatter 376 # Format results using ResultFormatter
376 - formatted_results = ResultFormatter.format_search_results(es_hits, max_score) 377 + formatted_results = ResultFormatter.format_search_results(
  378 + es_hits,
  379 + max_score,
  380 + language=language
  381 + )
377 382
378 # Format facets 383 # Format facets
379 standardized_facets = None 384 standardized_facets = None