Commit cadc77b6bfe6292708ce8c0db83554b133886510

Authored by tangwang
1 parent 3cf1c64d

索引字段名、变量名、API数据结构字段名都对齐spu/sku表

@@ -43,7 +43,7 @@ class FacetConfig(BaseModel): @@ -43,7 +43,7 @@ class FacetConfig(BaseModel):
43 json_schema_extra = { 43 json_schema_extra = {
44 "examples": [ 44 "examples": [
45 { 45 {
46 - "field": "categoryName_keyword", 46 + "field": "category.keyword",
47 "size": 15, 47 "size": 15,
48 "type": "terms" 48 "type": "terms"
49 }, 49 },
@@ -77,8 +77,8 @@ class SearchRequest(BaseModel): @@ -77,8 +77,8 @@ class SearchRequest(BaseModel):
77 json_schema_extra={ 77 json_schema_extra={
78 "examples": [ 78 "examples": [
79 { 79 {
80 - "categoryName_keyword": ["玩具", "益智玩具"],  
81 - "brandName_keyword": "乐高", 80 + "category.keyword": ["玩具", "益智玩具"],
  81 + "vendor.keyword": "乐高",
82 "in_stock": True 82 "in_stock": True
83 } 83 }
84 ] 84 ]
@@ -110,10 +110,10 @@ class SearchRequest(BaseModel): @@ -110,10 +110,10 @@ class SearchRequest(BaseModel):
110 json_schema_extra={ 110 json_schema_extra={
111 "examples": [ 111 "examples": [
112 # 简单模式:只指定字段名,使用默认配置 112 # 简单模式:只指定字段名,使用默认配置
113 - ["categoryName_keyword", "brandName_keyword"], 113 + ["category.keyword", "vendor.keyword"],
114 # 高级模式:详细配置 114 # 高级模式:详细配置
115 [ 115 [
116 - {"field": "categoryName_keyword", "size": 15}, 116 + {"field": "category.keyword", "size": 15},
117 { 117 {
118 "field": "price", 118 "field": "price",
119 "type": "range", 119 "type": "range",
@@ -172,10 +172,10 @@ class FacetResult(BaseModel): @@ -172,10 +172,10 @@ class FacetResult(BaseModel):
172 total_count: Optional[int] = Field(None, description="该字段的总文档数") 172 total_count: Optional[int] = Field(None, description="该字段的总文档数")
173 173
174 174
175 -class VariantResult(BaseModel):  
176 - """商品变体结果"""  
177 - variant_id: str = Field(..., description="变体ID")  
178 - title: Optional[str] = Field(None, description="变体标题") 175 +class SkuResult(BaseModel):
  176 + """SKU 结果"""
  177 + sku_id: str = Field(..., description="SKU ID")
  178 + title: Optional[str] = Field(None, description="SKU标题")
179 price: Optional[float] = Field(None, description="价格") 179 price: Optional[float] = Field(None, description="价格")
180 compare_at_price: Optional[float] = Field(None, description="原价") 180 compare_at_price: Optional[float] = Field(None, description="原价")
181 sku: Optional[str] = Field(None, description="SKU编码") 181 sku: Optional[str] = Field(None, description="SKU编码")
@@ -183,21 +183,21 @@ class VariantResult(BaseModel): @@ -183,21 +183,21 @@ class VariantResult(BaseModel):
183 options: Optional[Dict[str, Any]] = Field(None, description="选项(颜色、尺寸等)") 183 options: Optional[Dict[str, Any]] = Field(None, description="选项(颜色、尺寸等)")
184 184
185 185
186 -class ProductResult(BaseModel):  
187 - """商品搜索结果"""  
188 - product_id: str = Field(..., description="商品ID") 186 +class SpuResult(BaseModel):
  187 + """SPU 搜索结果"""
  188 + spu_id: str = Field(..., description="SPU ID")
189 title: Optional[str] = Field(None, description="商品标题") 189 title: Optional[str] = Field(None, description="商品标题")
190 handle: Optional[str] = Field(None, description="商品handle") 190 handle: Optional[str] = Field(None, description="商品handle")
191 description: Optional[str] = Field(None, description="商品描述") 191 description: Optional[str] = Field(None, description="商品描述")
192 vendor: Optional[str] = Field(None, description="供应商/品牌") 192 vendor: Optional[str] = Field(None, description="供应商/品牌")
193 - product_type: Optional[str] = Field(None, description="商品类型") 193 + category: Optional[str] = Field(None, description="类目")
194 tags: Optional[str] = Field(None, description="标签") 194 tags: Optional[str] = Field(None, description="标签")
195 price: Optional[float] = Field(None, description="价格(min_price)") 195 price: Optional[float] = Field(None, description="价格(min_price)")
196 compare_at_price: Optional[float] = Field(None, description="原价") 196 compare_at_price: Optional[float] = Field(None, description="原价")
197 currency: str = Field("USD", description="货币单位") 197 currency: str = Field("USD", description="货币单位")
198 image_url: Optional[str] = Field(None, description="主图URL") 198 image_url: Optional[str] = Field(None, description="主图URL")
199 in_stock: bool = Field(True, description="是否有库存") 199 in_stock: bool = Field(True, description="是否有库存")
200 - variants: List[VariantResult] = Field(default_factory=list, description="变体列表") 200 + skus: List[SkuResult] = Field(default_factory=list, description="SKU列表")
201 relevance_score: float = Field(..., ge=0.0, description="相关性分数(ES原始分数)") 201 relevance_score: float = Field(..., ge=0.0, description="相关性分数(ES原始分数)")
202 202
203 203
@@ -205,7 +205,7 @@ class SearchResponse(BaseModel): @@ -205,7 +205,7 @@ class SearchResponse(BaseModel):
205 """搜索响应模型(外部友好格式)""" 205 """搜索响应模型(外部友好格式)"""
206 206
207 # 核心结果 207 # 核心结果
208 - results: List[ProductResult] = Field(..., description="搜索结果列表") 208 + results: List[SpuResult] = Field(..., description="搜索结果列表")
209 total: int = Field(..., description="匹配的总文档数") 209 total: int = Field(..., description="匹配的总文档数")
210 max_score: float = Field(..., description="最高相关性分数") 210 max_score: float = Field(..., description="最高相关性分数")
211 211
api/result_formatter.py
@@ -3,7 +3,7 @@ Result formatter for converting ES internal format to external-friendly format. @@ -3,7 +3,7 @@ Result formatter for converting ES internal format to external-friendly format.
3 """ 3 """
4 4
5 from typing import List, Dict, Any, Optional 5 from typing import List, Dict, Any, Optional
6 -from .models import ProductResult, VariantResult, FacetResult, FacetValue 6 +from .models import SpuResult, SkuResult, FacetResult, FacetValue
7 7
8 8
9 class ResultFormatter: 9 class ResultFormatter:
@@ -13,16 +13,16 @@ class ResultFormatter: @@ -13,16 +13,16 @@ class ResultFormatter:
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 - ) -> List[ProductResult]: 16 + ) -> List[SpuResult]:
17 """ 17 """
18 - Convert ES hits to ProductResult list. 18 + Convert ES hits to SpuResult list.
19 19
20 Args: 20 Args:
21 es_hits: List of ES hit dictionaries (with _id, _score, _source) 21 es_hits: List of ES hit dictionaries (with _id, _score, _source)
22 max_score: Maximum score (unused, kept for compatibility) 22 max_score: Maximum score (unused, kept for compatibility)
23 23
24 Returns: 24 Returns:
25 - List of ProductResult objects 25 + List of SpuResult objects
26 """ 26 """
27 results = [] 27 results = []
28 28
@@ -40,44 +40,44 @@ class ResultFormatter: @@ -40,44 +40,44 @@ class ResultFormatter:
40 except (ValueError, TypeError): 40 except (ValueError, TypeError):
41 relevance_score = 0.0 41 relevance_score = 0.0
42 42
43 - # Extract variants  
44 - variants = []  
45 - variants_data = source.get('variants', [])  
46 - if isinstance(variants_data, list):  
47 - for variant_data in variants_data:  
48 - variant = VariantResult(  
49 - variant_id=str(variant_data.get('variant_id', '')),  
50 - title=variant_data.get('title'),  
51 - price=variant_data.get('price'),  
52 - compare_at_price=variant_data.get('compare_at_price'),  
53 - sku=variant_data.get('sku'),  
54 - stock=variant_data.get('stock', 0),  
55 - options=variant_data.get('options') 43 + # Extract SKUs
  44 + skus = []
  45 + skus_data = source.get('skus', [])
  46 + if isinstance(skus_data, list):
  47 + for sku_entry in skus_data:
  48 + sku = SkuResult(
  49 + sku_id=str(sku_entry.get('sku_id', '')),
  50 + title=sku_entry.get('title'),
  51 + price=sku_entry.get('price'),
  52 + compare_at_price=sku_entry.get('compare_at_price'),
  53 + sku=sku_entry.get('sku'),
  54 + stock=sku_entry.get('stock', 0),
  55 + options=sku_entry.get('options')
56 ) 56 )
57 - variants.append(variant) 57 + skus.append(sku)
58 58
59 - # Determine in_stock (any variant has stock > 0)  
60 - in_stock = any(v.stock > 0 for v in variants) if variants else True 59 + # Determine in_stock (any sku has stock > 0)
  60 + in_stock = any(sku.stock > 0 for sku in skus) if skus else True
61 61
62 - # Build ProductResult  
63 - product = ProductResult(  
64 - product_id=str(source.get('product_id', '')), 62 + # Build SpuResult
  63 + spu = SpuResult(
  64 + spu_id=str(source.get('spu_id', '')),
65 title=source.get('title'), 65 title=source.get('title'),
66 handle=source.get('handle'), 66 handle=source.get('handle'),
67 description=source.get('description'), 67 description=source.get('description'),
68 vendor=source.get('vendor'), 68 vendor=source.get('vendor'),
69 - product_type=source.get('product_type'), 69 + category=source.get('category'),
70 tags=source.get('tags'), 70 tags=source.get('tags'),
71 price=source.get('min_price'), 71 price=source.get('min_price'),
72 compare_at_price=source.get('compare_at_price'), 72 compare_at_price=source.get('compare_at_price'),
73 currency="USD", # Default currency 73 currency="USD", # Default currency
74 image_url=source.get('image_url'), 74 image_url=source.get('image_url'),
75 in_stock=in_stock, 75 in_stock=in_stock,
76 - variants=variants, 76 + skus=skus,
77 relevance_score=relevance_score 77 relevance_score=relevance_score
78 ) 78 )
79 79
80 - results.append(product) 80 + results.append(spu)
81 81
82 return results 82 return results
83 83
@@ -99,6 +99,7 @@ class ResultFormatter: @@ -99,6 +99,7 @@ class ResultFormatter:
99 facets = [] 99 facets = []
100 100
101 for field_name, agg_data in es_aggregations.items(): 101 for field_name, agg_data in es_aggregations.items():
  102 + display_field = field_name[:-6] if field_name.endswith("_facet") else field_name
102 # Handle terms aggregation 103 # Handle terms aggregation
103 if 'buckets' in agg_data: 104 if 'buckets' in agg_data:
104 values = [] 105 values = []
@@ -112,8 +113,8 @@ class ResultFormatter: @@ -112,8 +113,8 @@ class ResultFormatter:
112 values.append(value) 113 values.append(value)
113 114
114 facet = FacetResult( 115 facet = FacetResult(
115 - field=field_name,  
116 - label=field_name, # Can be enhanced with field labels 116 + field=display_field,
  117 + label=display_field, # Can be enhanced with field labels
117 type="terms", 118 type="terms",
118 values=values, 119 values=values,
119 total_count=agg_data.get('sum_other_doc_count', 0) + len(values) 120 total_count=agg_data.get('sum_other_doc_count', 0) + len(values)
@@ -134,8 +135,8 @@ class ResultFormatter: @@ -134,8 +135,8 @@ class ResultFormatter:
134 values.append(value) 135 values.append(value)
135 136
136 facet = FacetResult( 137 facet = FacetResult(
137 - field=field_name,  
138 - label=field_name, 138 + field=display_field,
  139 + label=display_field,
139 type="range", 140 type="range",
140 values=values 141 values=values
141 ) 142 )
@@ -146,7 +147,7 @@ class ResultFormatter: @@ -146,7 +147,7 @@ class ResultFormatter:
146 @staticmethod 147 @staticmethod
147 def generate_suggestions( 148 def generate_suggestions(
148 query: str, 149 query: str,
149 - results: List[ProductResult] 150 + results: List[SpuResult]
150 ) -> List[str]: 151 ) -> List[str]:
151 """ 152 """
152 Generate search suggestions. 153 Generate search suggestions.
@@ -164,7 +165,7 @@ class ResultFormatter: @@ -164,7 +165,7 @@ class ResultFormatter:
164 @staticmethod 165 @staticmethod
165 def generate_related_searches( 166 def generate_related_searches(
166 query: str, 167 query: str,
167 - results: List[ProductResult] 168 + results: List[SpuResult]
168 ) -> List[str]: 169 ) -> List[str]:
169 """ 170 """
170 Generate related searches. 171 Generate related searches.
config/config.yaml
@@ -22,7 +22,7 @@ fields: @@ -22,7 +22,7 @@ fields:
22 return_in_source: true 22 return_in_source: true
23 23
24 # 商品标识字段 24 # 商品标识字段
25 - - name: "product_id" 25 + - name: "spu_id"
26 type: "KEYWORD" 26 type: "KEYWORD"
27 required: true 27 required: true
28 index: true 28 index: true
@@ -87,61 +87,29 @@ fields: @@ -87,61 +87,29 @@ fields:
87 87
88 # 分类和标签字段(TEXT + KEYWORD双重索引) 88 # 分类和标签字段(TEXT + KEYWORD双重索引)
89 - name: "vendor" 89 - name: "vendor"
90 - type: "TEXT" 90 + type: "HKText"
91 analyzer: "chinese_ecommerce" 91 analyzer: "chinese_ecommerce"
92 boost: 1.5 92 boost: 1.5
93 index: true 93 index: true
94 store: true 94 store: true
95 return_in_source: true 95 return_in_source: true
96 96
97 - - name: "vendor_keyword"  
98 - type: "KEYWORD"  
99 - index: true  
100 - store: false  
101 - return_in_source: false # keyword字段通常只用于过滤,不需要返回  
102 -  
103 - - name: "product_type"  
104 - type: "TEXT"  
105 - analyzer: "chinese_ecommerce"  
106 - boost: 1.5  
107 - index: true  
108 - store: true  
109 - return_in_source: true  
110 -  
111 - - name: "product_type_keyword"  
112 - type: "KEYWORD"  
113 - index: true  
114 - store: false  
115 - return_in_source: false  
116 -  
117 - name: "tags" 97 - name: "tags"
118 - type: "TEXT" 98 + type: "HKText"
119 analyzer: "chinese_ecommerce" 99 analyzer: "chinese_ecommerce"
120 boost: 1.0 100 boost: 1.0
121 index: true 101 index: true
122 store: true 102 store: true
123 return_in_source: true 103 return_in_source: true
124 104
125 - - name: "tags_keyword"  
126 - type: "KEYWORD"  
127 - index: true  
128 - store: false  
129 - return_in_source: false  
130 -  
131 - name: "category" 105 - name: "category"
132 - type: "TEXT" 106 + type: "HKText"
133 analyzer: "chinese_ecommerce" 107 analyzer: "chinese_ecommerce"
134 boost: 1.5 108 boost: 1.5
135 index: true 109 index: true
136 store: true 110 store: true
137 return_in_source: true 111 return_in_source: true
138 112
139 - - name: "category_keyword"  
140 - type: "KEYWORD"  
141 - index: true  
142 - store: false  
143 - return_in_source: false  
144 -  
145 # 价格字段(扁平化) 113 # 价格字段(扁平化)
146 - name: "min_price" 114 - name: "min_price"
147 type: "FLOAT" 115 type: "FLOAT"
@@ -202,13 +170,13 @@ fields: @@ -202,13 +170,13 @@ fields:
202 store: true 170 store: true
203 return_in_source: false # 通常不需要返回 171 return_in_source: false # 通常不需要返回
204 172
205 - # 嵌套variants字段  
206 - - name: "variants" 173 + # 嵌套skus字段
  174 + - name: "skus"
207 type: "JSON" 175 type: "JSON"
208 nested: true 176 nested: true
209 return_in_source: true 177 return_in_source: true
210 nested_properties: 178 nested_properties:
211 - variant_id: 179 + sku_id:
212 type: "keyword" 180 type: "keyword"
213 index: true 181 index: true
214 store: true 182 store: true
@@ -249,7 +217,6 @@ indexes: @@ -249,7 +217,6 @@ indexes:
249 - "seo_description" 217 - "seo_description"
250 - "seo_keywords" 218 - "seo_keywords"
251 - "vendor" 219 - "vendor"
252 - - "product_type"  
253 - "tags" 220 - "tags"
254 - "category" 221 - "category"
255 analyzer: "chinese_ecommerce" 222 analyzer: "chinese_ecommerce"
@@ -323,9 +290,9 @@ rerank: @@ -323,9 +290,9 @@ rerank:
323 expression: "" 290 expression: ""
324 description: "Local reranking (disabled, use ES function_score instead)" 291 description: "Local reranking (disabled, use ES function_score instead)"
325 292
326 -# SPU配置(已启用,使用嵌套variants) 293 +# SPU配置(已启用,使用嵌套skus)
327 spu_config: 294 spu_config:
328 enabled: true 295 enabled: true
329 - spu_field: "product_id" 296 + spu_field: "spu_id"
330 inner_hits_size: 10 297 inner_hits_size: 10
331 298
config/config_loader.py
@@ -277,11 +277,13 @@ class ConfigLoader: @@ -277,11 +277,13 @@ class ConfigLoader:
277 """Parse field configuration from dictionary.""" 277 """Parse field configuration from dictionary."""
278 name = field_data["name"] 278 name = field_data["name"]
279 field_type_str = field_data["type"] 279 field_type_str = field_data["type"]
  280 + field_type_raw = field_type_str
280 281
281 # Map field type string to enum 282 # Map field type string to enum
282 if field_type_str not in FIELD_TYPE_MAP: 283 if field_type_str not in FIELD_TYPE_MAP:
283 raise ConfigurationError(f"Unknown field type: {field_type_str}") 284 raise ConfigurationError(f"Unknown field type: {field_type_str}")
284 field_type = FIELD_TYPE_MAP[field_type_str] 285 field_type = FIELD_TYPE_MAP[field_type_str]
  286 + is_hktext = field_type_str.lower() == "hktext"
285 287
286 # Map analyzer string to enum (if provided) 288 # Map analyzer string to enum (if provided)
287 analyzer = None 289 analyzer = None
@@ -309,7 +311,9 @@ class ConfigLoader: @@ -309,7 +311,9 @@ class ConfigLoader:
309 embedding_dims=field_data.get("embedding_dims", 1024), 311 embedding_dims=field_data.get("embedding_dims", 1024),
310 embedding_similarity=field_data.get("embedding_similarity", "dot_product"), 312 embedding_similarity=field_data.get("embedding_similarity", "dot_product"),
311 nested=field_data.get("nested", False), 313 nested=field_data.get("nested", False),
312 - nested_properties=field_data.get("nested_properties") 314 + nested_properties=field_data.get("nested_properties"),
  315 + keyword_subfield=field_data.get("keyword_subfield", is_hktext),
  316 + keyword_ignore_above=field_data.get("keyword_ignore_above", 256)
313 ) 317 )
314 318
315 def _parse_index_config(self, index_data: Dict[str, Any]) -> IndexConfig: 319 def _parse_index_config(self, index_data: Dict[str, Any]) -> IndexConfig:
config/field_types.py
@@ -72,6 +72,10 @@ class FieldConfig: @@ -72,6 +72,10 @@ class FieldConfig:
72 nested: bool = False 72 nested: bool = False
73 nested_properties: Optional[Dict[str, Any]] = None 73 nested_properties: Optional[Dict[str, Any]] = None
74 74
  75 + # Hybrid Keyword Text (HKText) support
  76 + keyword_subfield: bool = False
  77 + keyword_ignore_above: int = 256
  78 +
75 79
76 def get_es_mapping_for_field(field_config: FieldConfig) -> Dict[str, Any]: 80 def get_es_mapping_for_field(field_config: FieldConfig) -> Dict[str, Any]:
77 """ 81 """
@@ -102,6 +106,13 @@ def get_es_mapping_for_field(field_config: FieldConfig) -> Dict[str, Any]: @@ -102,6 +106,13 @@ def get_es_mapping_for_field(field_config: FieldConfig) -> Dict[str, Any]:
102 if field_config.search_analyzer: 106 if field_config.search_analyzer:
103 mapping["search_analyzer"] = field_config.search_analyzer.value 107 mapping["search_analyzer"] = field_config.search_analyzer.value
104 108
  109 + if field_config.keyword_subfield:
  110 + mapping.setdefault("fields", {})
  111 + mapping["fields"]["keyword"] = {
  112 + "type": "keyword",
  113 + "ignore_above": field_config.keyword_ignore_above
  114 + }
  115 +
105 elif field_config.field_type == FieldType.KEYWORD: 116 elif field_config.field_type == FieldType.KEYWORD:
106 mapping = { 117 mapping = {
107 "type": "keyword", 118 "type": "keyword",
@@ -256,6 +267,9 @@ def get_default_similarity() -> Dict[str, Any]: @@ -256,6 +267,9 @@ def get_default_similarity() -> Dict[str, Any]:
256 FIELD_TYPE_MAP = { 267 FIELD_TYPE_MAP = {
257 "text": FieldType.TEXT, 268 "text": FieldType.TEXT,
258 "TEXT": FieldType.TEXT, 269 "TEXT": FieldType.TEXT,
  270 + "HKText": FieldType.TEXT,
  271 + "hktext": FieldType.TEXT,
  272 + "HKTEXT": FieldType.TEXT,
259 "keyword": FieldType.KEYWORD, 273 "keyword": FieldType.KEYWORD,
260 "KEYWORD": FieldType.KEYWORD, 274 "KEYWORD": FieldType.KEYWORD,
261 "LITERAL": FieldType.KEYWORD, 275 "LITERAL": FieldType.KEYWORD,
config/query_rewrite.dict
1 -芭比 vendor_keyword:芭比 OR title:芭比娃娃  
2 -玩具 category_keyword:玩具 OR title:玩具  
3 -消防 category_keyword:消防 OR title:消防 1 +芭比 vendor.keyword:芭比 OR title:芭比娃娃
  2 +玩具 category.keyword:玩具 OR title:玩具
  3 +消防 category.keyword:消防 OR title:消防
4 4
docs/INDEX_FIELDS_DOCUMENTATION.md
@@ -6,7 +6,7 @@ @@ -6,7 +6,7 @@
6 6
7 - **索引名称**: `search_products` 7 - **索引名称**: `search_products`
8 - **索引级别**: SPU级别(商品级别) 8 - **索引级别**: SPU级别(商品级别)
9 -- **数据结构**: SPU文档包含嵌套的variants(SKU)数组 9 +- **数据结构**: SPU文档包含嵌套的skus(SKU)数组
10 10
11 ## 字段说明表 11 ## 字段说明表
12 12
@@ -15,7 +15,7 @@ @@ -15,7 +15,7 @@
15 | 索引字段名 | ES字段类型 | 是否索引 | 索引方式 | 数据来源表 | 表中字段名 | 表中字段类型 | 说明 | 15 | 索引字段名 | ES字段类型 | 是否索引 | 索引方式 | 数据来源表 | 表中字段名 | 表中字段类型 | 说明 |
16 |-----------|-----------|---------|---------|-----------|-----------|-------------|------| 16 |-----------|-----------|---------|---------|-----------|-----------|-------------|------|
17 | tenant_id | keyword | 是 | 精确匹配 | SPU表 | tenant_id | BIGINT | 租户ID,用于多租户隔离 | 17 | tenant_id | keyword | 是 | 精确匹配 | SPU表 | tenant_id | BIGINT | 租户ID,用于多租户隔离 |
18 -| product_id | keyword | 是 | 精确匹配 | SPU表 | id | BIGINT | 商品ID(SPU ID) | 18 +| spu_id | keyword | 是 | 精确匹配 | SPU表 | id | BIGINT | 商品ID(SPU ID) |
19 | handle | keyword | 是 | 精确匹配 | SPU表 | handle | VARCHAR(255) | 商品URL handle | 19 | handle | keyword | 是 | 精确匹配 | SPU表 | handle | VARCHAR(255) | 商品URL handle |
20 20
21 ### 文本搜索字段 21 ### 文本搜索字段
@@ -39,13 +39,13 @@ @@ -39,13 +39,13 @@
39 | 索引字段名 | ES字段类型 | 是否索引 | 索引方式 | 数据来源表 | 表中字段名 | 表中字段类型 | Boost权重 | 是否返回 | 说明 | 39 | 索引字段名 | ES字段类型 | 是否索引 | 索引方式 | 数据来源表 | 表中字段名 | 表中字段类型 | Boost权重 | 是否返回 | 说明 |
40 |-----------|-----------|---------|---------|-----------|-----------|-------------|-----------|---------|------| 40 |-----------|-----------|---------|---------|-----------|-----------|-------------|-----------|---------|------|
41 | vendor | TEXT | 是 | english | SPU表 | vendor | VARCHAR(255) | 1.5 | 是 | 供应商/品牌(文本搜索) | 41 | vendor | TEXT | 是 | english | SPU表 | vendor | VARCHAR(255) | 1.5 | 是 | 供应商/品牌(文本搜索) |
42 -| vendor_keyword | keyword | 是 | 精确匹配 | SPU表 | vendor | VARCHAR(255) | - | 否 | 供应商/品牌(精确匹配,用于过滤) | 42 +| vendor.keyword | keyword | 是 | 精确匹配 | SPU表 | vendor | VARCHAR(255) | - | 否 | 供应商/品牌(精确匹配,用于过滤) |
43 | product_type | TEXT | 是 | english | SPU表 | category | VARCHAR(255) | 1.5 | 是 | 商品类型(文本搜索) | 43 | product_type | TEXT | 是 | english | SPU表 | category | VARCHAR(255) | 1.5 | 是 | 商品类型(文本搜索) |
44 | product_type_keyword | keyword | 是 | 精确匹配 | SPU表 | category | VARCHAR(255) | - | 否 | 商品类型(精确匹配,用于过滤) | 44 | product_type_keyword | keyword | 是 | 精确匹配 | SPU表 | category | VARCHAR(255) | - | 否 | 商品类型(精确匹配,用于过滤) |
45 | tags | TEXT | 是 | english | SPU表 | tags | VARCHAR(1024) | 1.0 | 是 | 标签(文本搜索) | 45 | tags | TEXT | 是 | english | SPU表 | tags | VARCHAR(1024) | 1.0 | 是 | 标签(文本搜索) |
46 -| tags_keyword | keyword | 是 | 精确匹配 | SPU表 | tags | VARCHAR(1024) | - | 否 | 标签(精确匹配,用于过滤) | 46 +| tags.keyword | keyword | 是 | 精确匹配 | SPU表 | tags | VARCHAR(1024) | - | 否 | 标签(精确匹配,用于过滤) |
47 | category | TEXT | 是 | english | SPU表 | category | VARCHAR(255) | 1.5 | 是 | 类目(文本搜索) | 47 | category | TEXT | 是 | english | SPU表 | category | VARCHAR(255) | 1.5 | 是 | 类目(文本搜索) |
48 -| category_keyword | keyword | 是 | 精确匹配 | SPU表 | category | VARCHAR(255) | - | 否 | 类目(精确匹配,用于过滤) | 48 +| category.keyword | keyword | 是 | 精确匹配 | SPU表 | category | VARCHAR(255) | - | 否 | 类目(精确匹配,用于过滤) |
49 49
50 ### 价格字段 50 ### 价格字段
51 51
@@ -86,26 +86,26 @@ @@ -86,26 +86,26 @@
86 | shoplazza_created_at | DATE | 是 | 日期范围 | SPU表 | shoplazza_created_at | DATETIME | 否 | 店匠系统创建时间 | 86 | shoplazza_created_at | DATE | 是 | 日期范围 | SPU表 | shoplazza_created_at | DATETIME | 否 | 店匠系统创建时间 |
87 | shoplazza_updated_at | DATE | 是 | 日期范围 | SPU表 | shoplazza_updated_at | DATETIME | 否 | 店匠系统更新时间 | 87 | shoplazza_updated_at | DATE | 是 | 日期范围 | SPU表 | shoplazza_updated_at | DATETIME | 否 | 店匠系统更新时间 |
88 88
89 -### 嵌套Variants字段(SKU级别) 89 +### 嵌套Skus字段(SKU级别)
90 90
91 | 索引字段名 | ES字段类型 | 是否索引 | 索引方式 | 数据来源表 | 表中字段名 | 表中字段类型 | 说明 | 91 | 索引字段名 | ES字段类型 | 是否索引 | 索引方式 | 数据来源表 | 表中字段名 | 表中字段类型 | 说明 |
92 |-----------|-----------|---------|---------|-----------|-----------|-------------|------| 92 |-----------|-----------|---------|---------|-----------|-----------|-------------|------|
93 -| variants | JSON (nested) | 是 | 嵌套对象 | SKU表 | - | - | 商品变体数组(嵌套结构) | 93 +| skus | JSON (nested) | 是 | 嵌套对象 | SKU表 | - | - | 商品变体数组(嵌套结构) |
94 94
95 -#### Variants子字段 95 +#### Skus子字段
96 96
97 | 索引字段名 | ES字段类型 | 是否索引 | 索引方式 | 数据来源表 | 表中字段名 | 表中字段类型 | 说明 | 97 | 索引字段名 | ES字段类型 | 是否索引 | 索引方式 | 数据来源表 | 表中字段名 | 表中字段类型 | 说明 |
98 |-----------|-----------|---------|---------|-----------|-----------|-------------|------| 98 |-----------|-----------|---------|---------|-----------|-----------|-------------|------|
99 -| variants.variant_id | keyword | 是 | 精确匹配 | SKU表 | id | BIGINT | 变体ID(SKU ID) |  
100 -| variants.title | text | 是 | english | SKU表 | title | VARCHAR(500) | 变体标题 |  
101 -| variants.price | float | 是 | float | SKU表 | price | DECIMAL(10,2) | 变体价格 |  
102 -| variants.compare_at_price | float | 是 | float | SKU表 | compare_at_price | DECIMAL(10,2) | 变体原价 |  
103 -| variants.sku | keyword | 是 | 精确匹配 | SKU表 | sku | VARCHAR(100) | SKU编码 |  
104 -| variants.stock | long | 是 | float | SKU表 | inventory_quantity | INT(11) | 库存数量 |  
105 -| variants.options | object | 是 | 对象 | SKU表 | option1/option2/option3 | VARCHAR(255) | 选项(颜色、尺寸等) |  
106 -  
107 -**Variants结构说明**:  
108 -- `variants` 是一个嵌套对象数组,每个元素代表一个SKU 99 +| skus.sku_id | keyword | 是 | 精确匹配 | SKU表 | id | BIGINT | 变体ID(SKU ID) |
  100 +| skus.title | text | 是 | english | SKU表 | title | VARCHAR(500) | 变体标题 |
  101 +| skus.price | float | 是 | float | SKU表 | price | DECIMAL(10,2) | 变体价格 |
  102 +| skus.compare_at_price | float | 是 | float | SKU表 | compare_at_price | DECIMAL(10,2) | 变体原价 |
  103 +| skus.sku | keyword | 是 | 精确匹配 | SKU表 | sku | VARCHAR(100) | SKU编码 |
  104 +| skus.stock | long | 是 | float | SKU表 | inventory_quantity | INT(11) | 库存数量 |
  105 +| skus.options | object | 是 | 对象 | SKU表 | option1/option2/option3 | VARCHAR(255) | 选项(颜色、尺寸等) |
  106 +
  107 +**Skus结构说明**:
  108 +- `skus` 是一个嵌套对象数组,每个元素代表一个SKU
109 - 使用ES的nested类型,支持对嵌套字段进行独立查询和过滤 109 - 使用ES的nested类型,支持对嵌套字段进行独立查询和过滤
110 - `options` 对象包含 `option1`、`option2`、`option3` 三个字段,分别对应SKU表中的选项值 110 - `options` 对象包含 `option1`、`option2`、`option3` 三个字段,分别对应SKU表中的选项值
111 111
@@ -165,7 +165,7 @@ @@ -165,7 +165,7 @@
165 165
166 ### 数据类型转换 166 ### 数据类型转换
167 167
168 -1. **BIGINT → keyword**: 数字ID转换为字符串(如 `product_id`, `variant_id`) 168 +1. **BIGINT → keyword**: 数字ID转换为字符串(如 `spu_id`, `sku_id`)
169 2. **DECIMAL → FLOAT**: 价格字段从DECIMAL转换为FLOAT 169 2. **DECIMAL → FLOAT**: 价格字段从DECIMAL转换为FLOAT
170 3. **INT → LONG**: 库存数量从INT转换为LONG 170 3. **INT → LONG**: 库存数量从INT转换为LONG
171 4. **DATETIME → DATE**: 时间字段转换为ISO格式字符串 171 4. **DATETIME → DATE**: 时间字段转换为ISO格式字符串
@@ -179,7 +179,7 @@ @@ -179,7 +179,7 @@
179 ## 注意事项 179 ## 注意事项
180 180
181 1. **多租户隔离**: 所有查询必须包含 `tenant_id` 过滤条件 181 1. **多租户隔离**: 所有查询必须包含 `tenant_id` 过滤条件
182 -2. **嵌套查询**: 查询variants字段时需要使用nested查询语法 182 +2. **嵌套查询**: 查询skus字段时需要使用nested查询语法
183 3. **字段命名**: 用于过滤的字段应使用 `*_keyword` 后缀的字段 183 3. **字段命名**: 用于过滤的字段应使用 `*_keyword` 后缀的字段
184 4. **向量搜索**: title_embedding字段用于语义搜索,需要配合文本查询使用 184 4. **向量搜索**: title_embedding字段用于语义搜索,需要配合文本查询使用
185 5. **Boost权重**: 不同字段的boost权重影响搜索结果的相关性排序 185 5. **Boost权重**: 不同字段的boost权重影响搜索结果的相关性排序
@@ -210,7 +210,7 @@ @@ -210,7 +210,7 @@
210 ### SKU表(shoplazza_product_sku) 210 ### SKU表(shoplazza_product_sku)
211 211
212 主要字段: 212 主要字段:
213 -- `id`: BIGINT - 主键ID(对应variant_id) 213 +- `id`: BIGINT - 主键ID(对应sku_id)
214 - `spu_id`: BIGINT - SPU ID(关联字段) 214 - `spu_id`: BIGINT - SPU ID(关联字段)
215 - `title`: VARCHAR(500) - 变体标题 215 - `title`: VARCHAR(500) - 变体标题
216 - `price`: DECIMAL(10,2) - 价格 216 - `price`: DECIMAL(10,2) - 价格
docs/Search-API-Examples.md
@@ -90,7 +90,7 @@ curl -X POST "http://localhost:6002/search/" \ @@ -90,7 +90,7 @@ curl -X POST "http://localhost:6002/search/" \
90 -d '{ 90 -d '{
91 "query": "玩具", 91 "query": "玩具",
92 "filters": { 92 "filters": {
93 - "categoryName_keyword": "玩具" 93 + "category.keyword": "玩具"
94 } 94 }
95 }' 95 }'
96 ``` 96 ```
@@ -103,7 +103,7 @@ curl -X POST "http://localhost:6002/search/" \ @@ -103,7 +103,7 @@ curl -X POST "http://localhost:6002/search/" \
103 -d '{ 103 -d '{
104 "query": "娃娃", 104 "query": "娃娃",
105 "filters": { 105 "filters": {
106 - "categoryName_keyword": ["玩具", "益智玩具", "儿童玩具"] 106 + "category.keyword": ["玩具", "益智玩具", "儿童玩具"]
107 } 107 }
108 }' 108 }'
109 ``` 109 ```
@@ -118,8 +118,8 @@ curl -X POST "http://localhost:6002/search/" \ @@ -118,8 +118,8 @@ curl -X POST "http://localhost:6002/search/" \
118 -d '{ 118 -d '{
119 "query": "娃娃", 119 "query": "娃娃",
120 "filters": { 120 "filters": {
121 - "categoryName_keyword": "玩具",  
122 - "brandName_keyword": "美泰" 121 + "category.keyword": "玩具",
  122 + "vendor.keyword": "美泰"
123 } 123 }
124 }' 124 }'
125 ``` 125 ```
@@ -209,8 +209,8 @@ curl -X POST "http://localhost:6002/search/" \ @@ -209,8 +209,8 @@ curl -X POST "http://localhost:6002/search/" \
209 -d '{ 209 -d '{
210 "query": "玩具", 210 "query": "玩具",
211 "filters": { 211 "filters": {
212 - "categoryName_keyword": ["玩具", "益智玩具"],  
213 - "brandName_keyword": "乐高" 212 + "category.keyword": ["玩具", "益智玩具"],
  213 + "vendor.keyword": "乐高"
214 }, 214 },
215 "range_filters": { 215 "range_filters": {
216 "price": { 216 "price": {
@@ -237,7 +237,7 @@ curl -X POST "http://localhost:6002/search/" \ @@ -237,7 +237,7 @@ curl -X POST "http://localhost:6002/search/" \
237 -d '{ 237 -d '{
238 "query": "玩具", 238 "query": "玩具",
239 "size": 20, 239 "size": 20,
240 - "facets": ["categoryName_keyword", "brandName_keyword"] 240 + "facets": ["category.keyword", "vendor.keyword"]
241 }' 241 }'
242 ``` 242 ```
243 243
@@ -248,8 +248,8 @@ curl -X POST "http://localhost:6002/search/" \ @@ -248,8 +248,8 @@ curl -X POST "http://localhost:6002/search/" \
248 "total": 118, 248 "total": 118,
249 "facets": [ 249 "facets": [
250 { 250 {
251 - "field": "categoryName_keyword",  
252 - "label": "categoryName_keyword", 251 + "field": "category.keyword",
  252 + "label": "category.keyword",
253 "type": "terms", 253 "type": "terms",
254 "values": [ 254 "values": [
255 {"value": "玩具", "count": 85, "selected": false}, 255 {"value": "玩具", "count": 85, "selected": false},
@@ -257,8 +257,8 @@ curl -X POST "http://localhost:6002/search/" \ @@ -257,8 +257,8 @@ curl -X POST "http://localhost:6002/search/" \
257 ] 257 ]
258 }, 258 },
259 { 259 {
260 - "field": "brandName_keyword",  
261 - "label": "brandName_keyword", 260 + "field": "vendor.keyword",
  261 + "label": "vendor.keyword",
262 "type": "terms", 262 "type": "terms",
263 "values": [ 263 "values": [
264 {"value": "乐高", "count": 42, "selected": false}, 264 {"value": "乐高", "count": 42, "selected": false},
@@ -280,12 +280,12 @@ curl -X POST "http://localhost:6002/search/" \ @@ -280,12 +280,12 @@ curl -X POST "http://localhost:6002/search/" \
280 "query": "玩具", 280 "query": "玩具",
281 "facets": [ 281 "facets": [
282 { 282 {
283 - "field": "categoryName_keyword", 283 + "field": "category.keyword",
284 "size": 20, 284 "size": 20,
285 "type": "terms" 285 "type": "terms"
286 }, 286 },
287 { 287 {
288 - "field": "brandName_keyword", 288 + "field": "vendor.keyword",
289 "size": 30, 289 "size": 30,
290 "type": "terms" 290 "type": "terms"
291 } 291 }
@@ -342,8 +342,8 @@ curl -X POST "http://localhost:6002/search/" \ @@ -342,8 +342,8 @@ curl -X POST "http://localhost:6002/search/" \
342 -d '{ 342 -d '{
343 "query": "玩具", 343 "query": "玩具",
344 "facets": [ 344 "facets": [
345 - {"field": "categoryName_keyword", "size": 15},  
346 - {"field": "brandName_keyword", "size": 15}, 345 + {"field": "category.keyword", "size": 15},
  346 + {"field": "vendor.keyword", "size": 15},
347 { 347 {
348 "field": "price", 348 "field": "price",
349 "type": "range", 349 "type": "range",
@@ -395,7 +395,7 @@ curl -X POST "http://localhost:6002/search/" \ @@ -395,7 +395,7 @@ curl -X POST "http://localhost:6002/search/" \
395 -d '{ 395 -d '{
396 "query": "玩具", 396 "query": "玩具",
397 "filters": { 397 "filters": {
398 - "categoryName_keyword": "益智玩具" 398 + "category.keyword": "益智玩具"
399 }, 399 },
400 "sort_by": "min_price", 400 "sort_by": "min_price",
401 "sort_order": "asc" 401 "sort_order": "asc"
@@ -426,7 +426,7 @@ curl -X POST "http://localhost:6002/search/image" \ @@ -426,7 +426,7 @@ curl -X POST "http://localhost:6002/search/image" \
426 "image_url": "https://example.com/barbie.jpg", 426 "image_url": "https://example.com/barbie.jpg",
427 "size": 20, 427 "size": 20,
428 "filters": { 428 "filters": {
429 - "categoryName_keyword": "玩具" 429 + "category.keyword": "玩具"
430 }, 430 },
431 "range_filters": { 431 "range_filters": {
432 "price": { 432 "price": {
@@ -560,14 +560,14 @@ result = search_products( @@ -560,14 +560,14 @@ result = search_products(
560 query="玩具", 560 query="玩具",
561 size=20, 561 size=20,
562 filters={ 562 filters={
563 - "categoryName_keyword": ["玩具", "益智玩具"] 563 + "category.keyword": ["玩具", "益智玩具"]
564 }, 564 },
565 range_filters={ 565 range_filters={
566 "price": {"gte": 50, "lte": 200} 566 "price": {"gte": 50, "lte": 200}
567 }, 567 },
568 facets=[ 568 facets=[
569 - {"field": "brandName_keyword", "size": 15},  
570 - {"field": "categoryName_keyword", "size": 15}, 569 + {"field": "vendor.keyword", "size": 15},
  570 + {"field": "category.keyword", "size": 15},
571 { 571 {
572 "field": "price", 572 "field": "price",
573 "type": "range", 573 "type": "range",
@@ -697,14 +697,14 @@ const result2 = await client.search({ @@ -697,14 +697,14 @@ const result2 = await client.search({
697 query: "玩具", 697 query: "玩具",
698 size: 20, 698 size: 20,
699 filters: { 699 filters: {
700 - categoryName_keyword: ["玩具", "益智玩具"] 700 + category.keyword: ["玩具", "益智玩具"]
701 }, 701 },
702 rangeFilters: { 702 rangeFilters: {
703 price: { gte: 50, lte: 200 } 703 price: { gte: 50, lte: 200 }
704 }, 704 },
705 facets: [ 705 facets: [
706 - { field: "brandName_keyword", size: 15 },  
707 - { field: "categoryName_keyword", size: 15 } 706 + { field: "vendor.keyword", size: 15 },
  707 + { field: "category.keyword", size: 15 }
708 ], 708 ],
709 sortBy: "price", 709 sortBy: "price",
710 sortOrder: "asc" 710 sortOrder: "asc"
@@ -755,8 +755,8 @@ const SearchComponent = { @@ -755,8 +755,8 @@ const SearchComponent = {
755 filters: this.filters, 755 filters: this.filters,
756 range_filters: this.rangeFilters, 756 range_filters: this.rangeFilters,
757 facets: [ 757 facets: [
758 - { field: 'categoryName_keyword', size: 15 },  
759 - { field: 'brandName_keyword', size: 15 } 758 + { field: 'category.keyword', size: 15 },
  759 + { field: 'vendor.keyword', size: 15 }
760 ] 760 ]
761 }) 761 })
762 }); 762 });
@@ -868,10 +868,10 @@ curl -X POST "http://localhost:6002/search/" \ @@ -868,10 +868,10 @@ curl -X POST "http://localhost:6002/search/" \
868 -d '{ 868 -d '{
869 "query": "*", 869 "query": "*",
870 "filters": { 870 "filters": {
871 - "categoryName_keyword": "玩具" 871 + "category.keyword": "玩具"
872 }, 872 },
873 "facets": [ 873 "facets": [
874 - {"field": "brandName_keyword", "size": 20}, 874 + {"field": "vendor.keyword", "size": 20},
875 { 875 {
876 "field": "price", 876 "field": "price",
877 "type": "range", 877 "type": "range",
@@ -898,8 +898,8 @@ curl -X POST "http://localhost:6002/search/" \ @@ -898,8 +898,8 @@ curl -X POST "http://localhost:6002/search/" \
898 -d '{ 898 -d '{
899 "query": "芭比娃娃", 899 "query": "芭比娃娃",
900 "facets": [ 900 "facets": [
901 - {"field": "categoryName_keyword", "size": 10},  
902 - {"field": "brandName_keyword", "size": 10}, 901 + {"field": "category.keyword", "size": 10},
  902 + {"field": "vendor.keyword", "size": 10},
903 {"field": "price", "type": "range", "ranges": [ 903 {"field": "price", "type": "range", "ranges": [
904 {"key": "0-50", "to": 50}, 904 {"key": "0-50", "to": 50},
905 {"key": "50-100", "from": 50, "to": 100}, 905 {"key": "50-100", "from": 50, "to": 100},
@@ -924,7 +924,7 @@ curl -X POST "http://localhost:6002/search/" \ @@ -924,7 +924,7 @@ curl -X POST "http://localhost:6002/search/" \
924 "lte": 100 924 "lte": 100
925 } 925 }
926 }, 926 },
927 - "facets": ["categoryName_keyword", "brandName_keyword"], 927 + "facets": ["category.keyword", "vendor.keyword"],
928 "sort_by": "min_price", 928 "sort_by": "min_price",
929 "sort_order": "asc", 929 "sort_order": "asc",
930 "size": 50 930 "size": 50
@@ -1017,8 +1017,8 @@ curl -X POST "http://localhost:6002/search/" \ @@ -1017,8 +1017,8 @@ curl -X POST "http://localhost:6002/search/" \
1017 # ✅ 推荐:只请求必要的分面 1017 # ✅ 推荐:只请求必要的分面
1018 { 1018 {
1019 "facets": [ 1019 "facets": [
1020 - {"field": "categoryName_keyword", "size": 15},  
1021 - {"field": "brandName_keyword", "size": 15} 1020 + {"field": "category.keyword", "size": 15},
  1021 + {"field": "vendor.keyword", "size": 15}
1022 ] 1022 ]
1023 } 1023 }
1024 ``` 1024 ```
@@ -1045,7 +1045,7 @@ curl -X POST "http://localhost:6002/search/" \ @@ -1045,7 +1045,7 @@ curl -X POST "http://localhost:6002/search/" \
1045 { 1045 {
1046 "query": "玩具", 1046 "query": "玩具",
1047 "filters": { 1047 "filters": {
1048 - "categoryName_keyword": "玩具" 1048 + "category.keyword": "玩具"
1049 } 1049 }
1050 } 1050 }
1051 ``` 1051 ```
@@ -1064,7 +1064,7 @@ curl -X POST "http://localhost:6002/search/" \ @@ -1064,7 +1064,7 @@ curl -X POST "http://localhost:6002/search/" \
1064 "query": "*", 1064 "query": "*",
1065 "size": 0, 1065 "size": 0,
1066 "facets": [ 1066 "facets": [
1067 - {"field": "categoryName_keyword", "size": 100} 1067 + {"field": "category.keyword", "size": 100}
1068 ] 1068 ]
1069 }' 1069 }'
1070 ``` 1070 ```
@@ -1102,14 +1102,14 @@ curl -X POST "http://localhost:6002/search/" \ @@ -1102,14 +1102,14 @@ curl -X POST "http://localhost:6002/search/" \
1102 -d '{ 1102 -d '{
1103 "query": "(玩具 OR 游戏) AND 儿童 ANDNOT 电子", 1103 "query": "(玩具 OR 游戏) AND 儿童 ANDNOT 电子",
1104 "filters": { 1104 "filters": {
1105 - "categoryName_keyword": ["玩具", "益智玩具"] 1105 + "category.keyword": ["玩具", "益智玩具"]
1106 }, 1106 },
1107 "range_filters": { 1107 "range_filters": {
1108 "price": {"gte": 20, "lte": 100}, 1108 "price": {"gte": 20, "lte": 100},
1109 "days_since_last_update": {"lte": 30} 1109 "days_since_last_update": {"lte": 30}
1110 }, 1110 },
1111 "facets": [ 1111 "facets": [
1112 - {"field": "brandName_keyword", "size": 20} 1112 + {"field": "vendor.keyword", "size": 20}
1113 ], 1113 ],
1114 "sort_by": "min_price", 1114 "sort_by": "min_price",
1115 "sort_order": "asc", 1115 "sort_order": "asc",
docs/Usage-Guide.md
@@ -299,7 +299,7 @@ curl -X POST http://localhost:6002/search/ \ @@ -299,7 +299,7 @@ curl -X POST http://localhost:6002/search/ \
299 "query": "玩具", 299 "query": "玩具",
300 "size": 10, 300 "size": 10,
301 "filters": { 301 "filters": {
302 - "categoryName_keyword": ["玩具", "益智玩具"] 302 + "category.keyword": ["玩具", "益智玩具"]
303 }, 303 },
304 "range_filters": { 304 "range_filters": {
305 "price": {"gte": 50, "lte": 200} 305 "price": {"gte": 50, "lte": 200}
@@ -317,8 +317,8 @@ curl -X POST http://localhost:6002/search/ \ @@ -317,8 +317,8 @@ curl -X POST http://localhost:6002/search/ \
317 "query": "玩具", 317 "query": "玩具",
318 "size": 10, 318 "size": 10,
319 "facets": [ 319 "facets": [
320 - {"field": "categoryName_keyword", "size": 15},  
321 - {"field": "brandName_keyword", "size": 15} 320 + {"field": "category.keyword", "size": 15},
  321 + {"field": "vendor.keyword", "size": 15}
322 ] 322 ]
323 }' 323 }'
324 ``` 324 ```
docs/基础配置指南.md
@@ -8,7 +8,7 @@ Base閰嶇疆鏄簵鍖狅紙Shoplazza锛夐氱敤閰嶇疆锛岄傜敤浜庢墍鏈変娇鐢ㄥ簵鍖犳爣 @@ -8,7 +8,7 @@ Base閰嶇疆鏄簵鍖狅紙Shoplazza锛夐氱敤閰嶇疆锛岄傜敤浜庢墍鏈変娇鐢ㄥ簵鍖犳爣
8 8
9 ## 鏍稿績鐗规 9 ## 鏍稿績鐗规
10 10
11 -- **SPU绾у埆绱㈠紩**锛氭瘡涓狤S鏂囨。浠h〃涓涓猄PU锛屽寘鍚祵濂楃殑variants鏁扮粍 11 +- **SPU绾у埆绱㈠紩**锛氭瘡涓狤S鏂囨。浠h〃涓涓猄PU锛屽寘鍚祵濂楃殑skus鏁扮粍
12 - **缁熶竴绱㈠紩**锛氭墍鏈夊鎴峰叡浜玚search_products`绱㈠紩 12 - **缁熶竴绱㈠紩**锛氭墍鏈夊鎴峰叡浜玚search_products`绱㈠紩
13 - **绉熸埛闅旂**锛氶氳繃`tenant_id`瀛楁瀹炵幇鏁版嵁闅旂 13 - **绉熸埛闅旂**锛氶氳繃`tenant_id`瀛楁瀹炵幇鏁版嵁闅旂
14 - **閰嶇疆绠鍖**锛氶厤缃彧鍖呭惈ES鎼滅储鐩稿叧閰嶇疆锛屼笉鍖呭惈MySQL鏁版嵁婧愰厤缃 14 - **閰嶇疆绠鍖**锛氶厤缃彧鍖呭惈ES鎼滅储鐩稿叧閰嶇疆锛屼笉鍖呭惈MySQL鏁版嵁婧愰厤缃
@@ -41,12 +41,12 @@ Base閰嶇疆**鍙寘鍚**锛 @@ -41,12 +41,12 @@ Base閰嶇疆**鍙寘鍚**锛
41 41
42 ### 涓昏瀛楁 42 ### 涓昏瀛楁
43 43
44 -- `product_id` - 鍟嗗搧ID 44 +- `spu_id` - SPU ID
45 - `title`, `brief`, `description` - 鏂囨湰鎼滅储瀛楁 45 - `title`, `brief`, `description` - 鏂囨湰鎼滅储瀛楁
46 - `seo_title`, `seo_description`, `seo_keywords` - SEO瀛楁 46 - `seo_title`, `seo_description`, `seo_keywords` - SEO瀛楁
47 -- `vendor`, `product_type`, `tags`, `category` - 鍒嗙被鍜屾爣绛惧瓧娈 47 +- `vendor`, `tags`, `category` - 鍒嗙被鍜屾爣绛惧瓧娈碉紙HKText锛屾敮鎸 `.keyword` 绮剧‘鍖归厤锛
48 - `min_price`, `max_price`, `compare_at_price` - 浠锋牸瀛楁 48 - `min_price`, `max_price`, `compare_at_price` - 浠锋牸瀛楁
49 -- `variants` (nested) - 宓屽鍙樹綋鏁扮粍 49 +- `skus` (nested) - 宓屽SKU鏁扮粍
50 50
51 ## 鏁版嵁瀵煎叆娴佺▼ 51 ## 鏁版嵁瀵煎叆娴佺▼
52 52
@@ -109,9 +109,9 @@ Content-Type: application/json @@ -109,9 +109,9 @@ Content-Type: application/json
109 "size": 10, 109 "size": 10,
110 "from": 0, 110 "from": 0,
111 "filters": { 111 "filters": {
112 - "category_keyword": "鐢靛瓙浜у搧" 112 + "category.keyword": "鐢靛瓙浜у搧"
113 }, 113 },
114 - "facets": ["category_keyword", "vendor_keyword"] 114 + "facets": ["category.keyword", "vendor.keyword"]
115 } 115 }
116 ``` 116 ```
117 117
@@ -120,20 +120,20 @@ Content-Type: application/json @@ -120,20 +120,20 @@ Content-Type: application/json
120 { 120 {
121 "results": [ 121 "results": [
122 { 122 {
123 - "product_id": "1", 123 + "spu_id": "1",
124 "title": "钃濈墮鑰虫満 Sony", 124 "title": "钃濈墮鑰虫満 Sony",
125 "handle": "product-1", 125 "handle": "product-1",
126 "description": "楂樺搧璐ㄦ棤绾胯摑鐗欒虫満", 126 "description": "楂樺搧璐ㄦ棤绾胯摑鐗欒虫満",
127 "vendor": "Sony", 127 "vendor": "Sony",
128 - "product_type": "鐢靛瓙浜у搧", 128 + "category": "鐢靛瓙浜у搧",
129 "price": 199.99, 129 "price": 199.99,
130 "compare_at_price": 299.99, 130 "compare_at_price": 299.99,
131 "currency": "USD", 131 "currency": "USD",
132 "image_url": "//cdn.example.com/products/1.jpg", 132 "image_url": "//cdn.example.com/products/1.jpg",
133 "in_stock": true, 133 "in_stock": true,
134 - "variants": [ 134 + "skus": [
135 { 135 {
136 - "variant_id": "1", 136 + "sku_id": "1",
137 "title": "榛戣壊", 137 "title": "榛戣壊",
138 "price": 199.99, 138 "price": 199.99,
139 "compare_at_price": 299.99, 139 "compare_at_price": 299.99,
@@ -151,8 +151,8 @@ Content-Type: application/json @@ -151,8 +151,8 @@ Content-Type: application/json
151 "max_score": 1.0, 151 "max_score": 1.0,
152 "facets": [ 152 "facets": [
153 { 153 {
154 - "field": "category_keyword",  
155 - "label": "category_keyword", 154 + "field": "category.keyword",
  155 + "label": "category.keyword",
156 "type": "terms", 156 "type": "terms",
157 "values": [ 157 "values": [
158 { 158 {
@@ -176,31 +176,31 @@ Content-Type: application/json @@ -176,31 +176,31 @@ Content-Type: application/json
176 #### 涓昏鍙樺寲 176 #### 涓昏鍙樺寲
177 177
178 1. **`results`鏇夸唬`hits`**锛氳繑鍥炲瓧娈典粠`hits`鏀逛负`results` 178 1. **`results`鏇夸唬`hits`**锛氳繑鍥炲瓧娈典粠`hits`鏀逛负`results`
179 -2. **缁撴瀯鍖栫粨鏋**锛氭瘡涓粨鏋滃寘鍚玚product_id`, `title`, `variants`, `relevance_score`绛夊瓧娈 179 +2. **缁撴瀯鍖栫粨鏋**锛氭瘡涓粨鏋滃寘鍚玚spu_id`, `title`, `skus`, `relevance_score`绛夊瓧娈
180 3. **鏃燛S鍐呴儴瀛楁**锛氫笉鍖呭惈`_id`, `_score`, `_source`绛塃S鍐呴儴瀛楁 180 3. **鏃燛S鍐呴儴瀛楁**锛氫笉鍖呭惈`_id`, `_score`, `_source`绛塃S鍐呴儴瀛楁
181 -4. **宓屽variants**锛氭瘡涓晢鍝佸寘鍚玽ariants鏁扮粍锛屾瘡涓獀ariant鍖呭惈瀹屾暣鐨勫彉浣撲俊鎭 181 +4. **宓屽skus**锛氭瘡涓晢鍝佸寘鍚玸kus鏁扮粍锛屾瘡涓猻ku鍖呭惈瀹屾暣鐨勫彉浣撲俊鎭
182 5. **鐩稿叧鎬у垎鏁**锛歚relevance_score`鏄疎S鍘熷鍒嗘暟锛堜笉杩涜褰掍竴鍖栵級 182 5. **鐩稿叧鎬у垎鏁**锛歚relevance_score`鏄疎S鍘熷鍒嗘暟锛堜笉杩涜褰掍竴鍖栵級
183 183
184 -#### ProductResult瀛楁 184 +#### SpuResult瀛楁
185 185
186 -- `product_id` - 鍟嗗搧ID 186 +- `spu_id` - SPU ID
187 - `title` - 鍟嗗搧鏍囬 187 - `title` - 鍟嗗搧鏍囬
188 - `handle` - 鍟嗗搧handle 188 - `handle` - 鍟嗗搧handle
189 - `description` - 鍟嗗搧鎻忚堪 189 - `description` - 鍟嗗搧鎻忚堪
190 - `vendor` - 渚涘簲鍟/鍝佺墝 190 - `vendor` - 渚涘簲鍟/鍝佺墝
191 -- `product_type` - 鍟嗗搧绫诲瀷 191 +- `category` - 绫荤洰
192 - `tags` - 鏍囩 192 - `tags` - 鏍囩
193 - `price` - 鏈浣庝环鏍硷紙min_price锛 193 - `price` - 鏈浣庝环鏍硷紙min_price锛
194 - `compare_at_price` - 鍘熶环 194 - `compare_at_price` - 鍘熶环
195 - `currency` - 璐у竵鍗曚綅锛堥粯璁SD锛 195 - `currency` - 璐у竵鍗曚綅锛堥粯璁SD锛
196 - `image_url` - 涓诲浘URL 196 - `image_url` - 涓诲浘URL
197 - `in_stock` - 鏄惁鏈夊簱瀛 197 - `in_stock` - 鏄惁鏈夊簱瀛
198 -- `variants` - 鍙樹綋鍒楄〃 198 +- `skus` - SKU鍒楄〃
199 - `relevance_score` - 鐩稿叧鎬у垎鏁帮紙ES鍘熷鍒嗘暟锛 199 - `relevance_score` - 鐩稿叧鎬у垎鏁帮紙ES鍘熷鍒嗘暟锛
200 200
201 -#### VariantResult瀛楁 201 +#### SkuResult瀛楁
202 202
203 -- `variant_id` - 鍙樹綋ID 203 +- `sku_id` - SKU ID
204 - `title` - 鍙樹綋鏍囬 204 - `title` - 鍙樹綋鏍囬
205 - `price` - 浠锋牸 205 - `price` - 浠锋牸
206 - `compare_at_price` - 鍘熶环 206 - `compare_at_price` - 鍘熶环
@@ -242,7 +242,7 @@ A: 浣跨敤`test_base.py`鑴氭湰锛屾寚瀹氫袱涓笉鍚岀殑`--tenant-id`锛屾鏌ユ悳绱 @@ -242,7 +242,7 @@ A: 浣跨敤`test_base.py`鑴氭湰锛屾寚瀹氫袱涓笉鍚岀殑`--tenant-id`锛屾鏌ユ悳绱
242 242
243 ### Q: API杩斿洖鏍煎紡涓负浠涔堟病鏈塦_id`鍜宍_score`锛 243 ### Q: API杩斿洖鏍煎紡涓负浠涔堟病鏈塦_id`鍜宍_score`锛
244 244
245 -A: 涓轰簡鎻愪緵澶栭儴鍙嬪ソ鐨凙PI鏍煎紡锛屾垜浠Щ闄や簡ES鍐呴儴瀛楁锛屼娇鐢╜product_id`鍜宍relevance_score`鏇夸唬銆 245 +A: 涓轰簡鎻愪緵澶栭儴鍙嬪ソ鐨凙PI鏍煎紡锛屾垜浠Щ闄や簡ES鍐呴儴瀛楁锛屼娇鐢╜spu_id`鍜宍relevance_score`鏇夸唬銆
246 246
247 ### Q: 濡備綍娣诲姞鏂扮殑鎼滅储瀛楁锛 247 ### Q: 濡備綍娣诲姞鏂扮殑鎼滅储瀛楁锛
248 248
docs/搜索API对接指南.md
@@ -43,7 +43,7 @@ curl -X POST "http://localhost:6002/search/" \ @@ -43,7 +43,7 @@ curl -X POST "http://localhost:6002/search/" \
43 "size": 5, 43 "size": 5,
44 "from": 10, 44 "from": 10,
45 "filters": { 45 "filters": {
46 - "vendor_keyword": ["乐高", "孩之宝"] 46 + "vendor.keyword": ["乐高", "孩之宝"]
47 }, 47 },
48 "sort_by": "min_price", 48 "sort_by": "min_price",
49 "sort_order": "asc" 49 "sort_order": "asc"
@@ -58,7 +58,7 @@ curl -X POST "http://localhost:6002/search/" \ @@ -58,7 +58,7 @@ curl -X POST "http://localhost:6002/search/" \
58 -d '{ 58 -d '{
59 "tenant_id": "demo-tenant", 59 "tenant_id": "demo-tenant",
60 "query": "芭比娃娃", 60 "query": "芭比娃娃",
61 - "facets": ["category_keyword", "vendor_keyword"], 61 + "facets": ["category.keyword", "vendor.keyword"],
62 "min_score": 0.2, 62 "min_score": 0.2,
63 "debug": true 63 "debug": true
64 }' 64 }'
@@ -134,9 +134,9 @@ curl -X POST "http://localhost:6002/search/" \ @@ -134,9 +134,9 @@ curl -X POST "http://localhost:6002/search/" \
134 ```json 134 ```json
135 { 135 {
136 "filters": { 136 "filters": {
137 - "category_keyword": "玩具", // 单值:精确匹配  
138 - "vendor_keyword": ["乐高", "孩之宝"], // 数组:匹配任意值(OR)  
139 - "product_type_keyword": "益智玩具" // 单值:精确匹配 137 + "category.keyword": "玩具", // 单值:精确匹配
  138 + "vendor.keyword": ["乐高", "孩之宝"], // 数组:匹配任意值(OR)
  139 + "tags.keyword": "益智玩具" // 单值:精确匹配
140 } 140 }
141 } 141 }
142 ``` 142 ```
@@ -148,10 +148,9 @@ curl -X POST "http://localhost:6002/search/" \ @@ -148,10 +148,9 @@ curl -X POST "http://localhost:6002/search/" \
148 - 数组:匹配任意值(OR 逻辑) 148 - 数组:匹配任意值(OR 逻辑)
149 149
150 **常用过滤字段**: 150 **常用过滤字段**:
151 -- `category_keyword`: 类目  
152 -- `vendor_keyword`: 品牌/供应商  
153 -- `product_type_keyword`: 商品类型  
154 -- `tags_keyword`: 标签 151 +- `category.keyword`: 类目
  152 +- `vendor.keyword`: 品牌/供应商
  153 +- `tags.keyword`: 标签
155 154
156 #### 2. 范围过滤器 (range_filters) 155 #### 2. 范围过滤器 (range_filters)
157 156
@@ -197,7 +196,7 @@ curl -X POST "http://localhost:6002/search/" \ @@ -197,7 +196,7 @@ curl -X POST "http://localhost:6002/search/" \
197 **简单模式**(字符串数组): 196 **简单模式**(字符串数组):
198 ```json 197 ```json
199 { 198 {
200 - "facets": ["category_keyword", "vendor_keyword"] 199 + "facets": ["category.keyword", "vendor.keyword"]
201 } 200 }
202 ``` 201 ```
203 202
@@ -206,7 +205,7 @@ curl -X POST "http://localhost:6002/search/" \ @@ -206,7 +205,7 @@ curl -X POST "http://localhost:6002/search/" \
206 { 205 {
207 "facets": [ 206 "facets": [
208 { 207 {
209 - "field": "category_keyword", 208 + "field": "category.keyword",
210 "size": 15, 209 "size": 15,
211 "type": "terms" 210 "type": "terms"
212 }, 211 },
@@ -270,21 +269,21 @@ curl -X POST "http://localhost:6002/search/" \ @@ -270,21 +269,21 @@ curl -X POST "http://localhost:6002/search/" \
270 { 269 {
271 "results": [ 270 "results": [
272 { 271 {
273 - "product_id": "12345", 272 + "spu_id": "12345",
274 "title": "芭比时尚娃娃", 273 "title": "芭比时尚娃娃",
275 "handle": "barbie-doll", 274 "handle": "barbie-doll",
276 "description": "高品质芭比娃娃", 275 "description": "高品质芭比娃娃",
277 "vendor": "美泰", 276 "vendor": "美泰",
278 - "product_type": "玩具", 277 + "category": "玩具",
279 "tags": "娃娃, 玩具, 女孩", 278 "tags": "娃娃, 玩具, 女孩",
280 "price": 89.99, 279 "price": 89.99,
281 "compare_at_price": 129.99, 280 "compare_at_price": 129.99,
282 "currency": "USD", 281 "currency": "USD",
283 "image_url": "https://example.com/image.jpg", 282 "image_url": "https://example.com/image.jpg",
284 "in_stock": true, 283 "in_stock": true,
285 - "variants": [ 284 + "skus": [
286 { 285 {
287 - "variant_id": "67890", 286 + "sku_id": "67890",
288 "title": "粉色款", 287 "title": "粉色款",
289 "price": 89.99, 288 "price": 89.99,
290 "compare_at_price": 129.99, 289 "compare_at_price": 129.99,
@@ -303,8 +302,8 @@ curl -X POST "http://localhost:6002/search/" \ @@ -303,8 +302,8 @@ curl -X POST "http://localhost:6002/search/" \
303 "max_score": 8.5, 302 "max_score": 8.5,
304 "facets": [ 303 "facets": [
305 { 304 {
306 - "field": "category_keyword",  
307 - "label": "category_keyword", 305 + "field": "category.keyword",
  306 + "label": "category.keyword",
308 "type": "terms", 307 "type": "terms",
309 "values": [ 308 "values": [
310 { 309 {
@@ -335,11 +334,11 @@ curl -X POST "http://localhost:6002/search/" \ @@ -335,11 +334,11 @@ curl -X POST "http://localhost:6002/search/" \
335 334
336 | 字段 | 类型 | 说明 | 335 | 字段 | 类型 | 说明 |
337 |------|------|------| 336 |------|------|------|
338 -| `results` | array | 搜索结果列表(ProductResult对象数组) |  
339 -| `results[].product_id` | string | 商品ID | 337 +| `results` | array | 搜索结果列表(SpuResult对象数组) |
  338 +| `results[].spu_id` | string | SPU ID |
340 | `results[].title` | string | 商品标题 | 339 | `results[].title` | string | 商品标题 |
341 | `results[].price` | float | 价格(min_price) | 340 | `results[].price` | float | 价格(min_price) |
342 -| `results[].variants` | array | 变体列表(SKU列表) | 341 +| `results[].skus` | array | SKU列表 |
343 | `results[].relevance_score` | float | 相关性分数 | 342 | `results[].relevance_score` | float | 相关性分数 |
344 | `total` | integer | 匹配的总文档数 | 343 | `total` | integer | 匹配的总文档数 |
345 | `max_score` | float | 最高相关性分数 | 344 | `max_score` | float | 最高相关性分数 |
@@ -347,31 +346,31 @@ curl -X POST "http://localhost:6002/search/" \ @@ -347,31 +346,31 @@ curl -X POST "http://localhost:6002/search/" \
347 | `query_info` | object | 查询处理信息 | 346 | `query_info` | object | 查询处理信息 |
348 | `took_ms` | integer | 搜索耗时(毫秒) | 347 | `took_ms` | integer | 搜索耗时(毫秒) |
349 348
350 -### ProductResult字段说明 349 +### SpuResult字段说明
351 350
352 | 字段 | 类型 | 说明 | 351 | 字段 | 类型 | 说明 |
353 |------|------|------| 352 |------|------|------|
354 -| `product_id` | string | 商品ID(SPU ID) | 353 +| `spu_id` | string | SPU ID |
355 | `title` | string | 商品标题 | 354 | `title` | string | 商品标题 |
356 | `handle` | string | 商品URL handle | 355 | `handle` | string | 商品URL handle |
357 | `description` | string | 商品描述 | 356 | `description` | string | 商品描述 |
358 | `vendor` | string | 供应商/品牌 | 357 | `vendor` | string | 供应商/品牌 |
359 -| `product_type` | string | 商品类型 | 358 +| `category` | string | 类目 |
360 | `tags` | string | 标签 | 359 | `tags` | string | 标签 |
361 | `price` | float | 价格(min_price) | 360 | `price` | float | 价格(min_price) |
362 | `compare_at_price` | float | 原价 | 361 | `compare_at_price` | float | 原价 |
363 | `currency` | string | 货币单位(默认USD) | 362 | `currency` | string | 货币单位(默认USD) |
364 | `image_url` | string | 主图URL | 363 | `image_url` | string | 主图URL |
365 -| `in_stock` | boolean | 是否有库存(任意变体有库存即为true) |  
366 -| `variants` | array | 变体列表 | 364 +| `in_stock` | boolean | 是否有库存(任意SKU有库存即为true) |
  365 +| `skus` | array | SKU 列表 |
367 | `relevance_score` | float | 相关性分数 | 366 | `relevance_score` | float | 相关性分数 |
368 367
369 -### VariantResult字段说明 368 +### SkuResult字段说明
370 369
371 | 字段 | 类型 | 说明 | 370 | 字段 | 类型 | 说明 |
372 |------|------|------| 371 |------|------|------|
373 -| `variant_id` | string | 变体ID(SKU ID) |  
374 -| `title` | string | 变体标题 | 372 +| `sku_id` | string | SKU ID |
  373 +| `title` | string | SKU标题 |
375 | `price` | float | 价格 | 374 | `price` | float | 价格 |
376 | `compare_at_price` | float | 原价 | 375 | `compare_at_price` | float | 原价 |
377 | `sku` | string | SKU编码 | 376 | `sku` | string | SKU编码 |
@@ -405,7 +404,7 @@ curl -X POST "http://localhost:6002/search/" \ @@ -405,7 +404,7 @@ curl -X POST "http://localhost:6002/search/" \
405 "query": "玩具", 404 "query": "玩具",
406 "size": 20, 405 "size": 20,
407 "filters": { 406 "filters": {
408 - "category_keyword": "益智玩具" 407 + "category.keyword": "益智玩具"
409 }, 408 },
410 "range_filters": { 409 "range_filters": {
411 "min_price": { 410 "min_price": {
@@ -425,8 +424,8 @@ curl -X POST "http://localhost:6002/search/" \ @@ -425,8 +424,8 @@ curl -X POST "http://localhost:6002/search/" \
425 "query": "玩具", 424 "query": "玩具",
426 "size": 20, 425 "size": 20,
427 "facets": [ 426 "facets": [
428 - "category_keyword",  
429 - "vendor_keyword" 427 + "category.keyword",
  428 + "vendor.keyword"
430 ] 429 ]
431 } 430 }
432 ``` 431 ```
@@ -440,7 +439,7 @@ curl -X POST "http://localhost:6002/search/" \ @@ -440,7 +439,7 @@ curl -X POST "http://localhost:6002/search/" \
440 "query": "玩具", 439 "query": "玩具",
441 "size": 20, 440 "size": 20,
442 "filters": { 441 "filters": {
443 - "vendor_keyword": ["乐高", "孩之宝", "美泰"] 442 + "vendor.keyword": ["乐高", "孩之宝", "美泰"]
444 }, 443 },
445 "range_filters": { 444 "range_filters": {
446 "min_price": { 445 "min_price": {
@@ -450,7 +449,7 @@ curl -X POST "http://localhost:6002/search/" \ @@ -450,7 +449,7 @@ curl -X POST "http://localhost:6002/search/" \
450 }, 449 },
451 "facets": [ 450 "facets": [
452 { 451 {
453 - "field": "category_keyword", 452 + "field": "category.keyword",
454 "size": 15 453 "size": 15
455 }, 454 },
456 { 455 {
@@ -575,7 +574,7 @@ curl "http://localhost:6002/search/instant?q=玩具&size=5" @@ -575,7 +574,7 @@ curl "http://localhost:6002/search/instant?q=玩具&size=5"
575 "source": { 574 "source": {
576 "title": "芭比时尚娃娃", 575 "title": "芭比时尚娃娃",
577 "min_price": 89.99, 576 "min_price": 89.99,
578 - "category_keyword": "玩具" 577 + "category.keyword": "玩具"
579 } 578 }
580 } 579 }
581 ``` 580 ```
@@ -646,15 +645,15 @@ curl "http://localhost:6002/search/12345" @@ -646,15 +645,15 @@ curl "http://localhost:6002/search/12345"
646 645
647 | 字段名 | 类型 | 描述 | 646 | 字段名 | 类型 | 描述 |
648 |--------|------|------| 647 |--------|------|------|
649 -| `product_id` | keyword | 商品 ID(SPU) | 648 +| `spu_id` | keyword | SPU ID |
650 | `sku_id` | keyword/long | SKU ID(主键) | 649 | `sku_id` | keyword/long | SKU ID(主键) |
651 | `title` | text | 商品名称(中文) | 650 | `title` | text | 商品名称(中文) |
652 | `en_title` | text | 商品名称(英文) | 651 | `en_title` | text | 商品名称(英文) |
653 | `ru_title` | text | 商品名称(俄文) | 652 | `ru_title` | text | 商品名称(俄文) |
654 -| `category_keyword` | keyword | 类目(精确匹配) |  
655 -| `vendor_keyword` | keyword | 品牌/供应商(精确匹配) |  
656 -| `product_type_keyword` | keyword | 商品类型 |  
657 -| `tags_keyword` | keyword | 标签 | 653 +| `category.keyword` | keyword | 类目(精确匹配) |
  654 +| `vendor.keyword` | keyword | 品牌/供应商(精确匹配) |
  655 +| `category` | HKText | 类目(支持 `category.keyword` 精确匹配) |
  656 +| `tags.keyword` | keyword | 标签 |
658 | `min_price` | double | 最低价格 | 657 | `min_price` | double | 最低价格 |
659 | `max_price` | double | 最高价格 | 658 | `max_price` | double | 最高价格 |
660 | `compare_at_price` | double | 原价 | 659 | `compare_at_price` | double | 原价 |
@@ -664,7 +663,7 @@ curl "http://localhost:6002/search/12345" @@ -664,7 +663,7 @@ curl "http://localhost:6002/search/12345"
664 | `text_embedding` | dense_vector | 文本向量(1024 维) | 663 | `text_embedding` | dense_vector | 文本向量(1024 维) |
665 | `image_embedding` | dense_vector | 图片向量(1024 维) | 664 | `image_embedding` | dense_vector | 图片向量(1024 维) |
666 665
667 -> 不同租户可自定义字段名称,但最佳实践是对可过滤字段建立 `*_keyword` 版本,对可排序字段显式建 keyword/数值映射 666 +> 不同租户可自定义字段名称。推荐将可过滤的文本字段配置为 HKText,这样即可同时支持全文检索和 `field.keyword` 精确过滤;数值字段单独建索引以用于排序/Range
668 667
669 --- 668 ---
670 669
@@ -672,12 +671,11 @@ curl "http://localhost:6002/search/12345" @@ -672,12 +671,11 @@ curl "http://localhost:6002/search/12345"
672 671
673 ### 常用字段列表 672 ### 常用字段列表
674 673
675 -#### 过滤字段(使用 `*_keyword` 后缀 674 +#### 过滤字段(使用 HKText 的 keyword 子字段
676 675
677 -- `category_keyword`: 类目  
678 -- `vendor_keyword`: 品牌/供应商  
679 -- `product_type_keyword`: 商品类型  
680 -- `tags_keyword`: 标签 676 +- `category.keyword`: 类目
  677 +- `vendor.keyword`: 品牌/供应商
  678 +- `tags.keyword`: 标签
681 679
682 #### 范围字段 680 #### 范围字段
683 681
docs/搜索API速查表.md
@@ -17,8 +17,8 @@ POST /search/ @@ -17,8 +17,8 @@ POST /search/
17 ```bash 17 ```bash
18 { 18 {
19 "filters": { 19 "filters": {
20 - "categoryName_keyword": "玩具", // 单值  
21 - "brandName_keyword": ["乐高", "美泰"] // 多值(OR) 20 + "category.keyword": "玩具", // 单值
  21 + "vendor.keyword": ["乐高", "美泰"] // 多值(OR)
22 } 22 }
23 } 23 }
24 ``` 24 ```
@@ -48,7 +48,7 @@ POST /search/ @@ -48,7 +48,7 @@ POST /search/
48 48
49 ```bash 49 ```bash
50 { 50 {
51 - "facets": ["categoryName_keyword", "brandName_keyword"] 51 + "facets": ["category.keyword", "vendor.keyword"]
52 } 52 }
53 ``` 53 ```
54 54
@@ -57,7 +57,7 @@ POST /search/ @@ -57,7 +57,7 @@ POST /search/
57 ```bash 57 ```bash
58 { 58 {
59 "facets": [ 59 "facets": [
60 - {"field": "categoryName_keyword", "size": 15}, 60 + {"field": "category.keyword", "size": 15},
61 { 61 {
62 "field": "price", 62 "field": "price",
63 "type": "range", 63 "type": "range",
@@ -115,14 +115,14 @@ POST /search/ @@ -115,14 +115,14 @@ POST /search/
115 "size": 20, 115 "size": 20,
116 "from": 0, 116 "from": 0,
117 "filters": { 117 "filters": {
118 - "categoryName_keyword": ["玩具", "益智玩具"] 118 + "category.keyword": ["玩具", "益智玩具"]
119 }, 119 },
120 "range_filters": { 120 "range_filters": {
121 "price": {"gte": 50, "lte": 200} 121 "price": {"gte": 50, "lte": 200}
122 }, 122 },
123 "facets": [ 123 "facets": [
124 - {"field": "brandName_keyword", "size": 15},  
125 - {"field": "categoryName_keyword", "size": 15} 124 + {"field": "vendor.keyword", "size": 15},
  125 + {"field": "category.keyword", "size": 15}
126 ], 126 ],
127 "sort_by": "min_price", 127 "sort_by": "min_price",
128 "sort_order": "asc" 128 "sort_order": "asc"
@@ -147,7 +147,7 @@ POST /search/ @@ -147,7 +147,7 @@ POST /search/
147 "took_ms": 45, 147 "took_ms": 45,
148 "facets": [ 148 "facets": [
149 { 149 {
150 - "field": "categoryName_keyword", 150 + "field": "category.keyword",
151 "label": "商品类目", 151 "label": "商品类目",
152 "type": "terms", 152 "type": "terms",
153 "values": [ 153 "values": [
@@ -194,9 +194,9 @@ import requests @@ -194,9 +194,9 @@ import requests
194 194
195 result = requests.post('http://localhost:6002/search/', json={ 195 result = requests.post('http://localhost:6002/search/', json={
196 "query": "玩具", 196 "query": "玩具",
197 - "filters": {"categoryName_keyword": "玩具"}, 197 + "filters": {"category.keyword": "玩具"},
198 "range_filters": {"price": {"gte": 50, "lte": 200}}, 198 "range_filters": {"price": {"gte": 50, "lte": 200}},
199 - "facets": ["brandName_keyword"], 199 + "facets": ["vendor.keyword"],
200 "sort_by": "min_price", 200 "sort_by": "min_price",
201 "sort_order": "asc" 201 "sort_order": "asc"
202 }).json() 202 }).json()
@@ -214,9 +214,9 @@ const result = await fetch('http://localhost:6002/search/', { @@ -214,9 +214,9 @@ const result = await fetch('http://localhost:6002/search/', {
214 headers: {'Content-Type': 'application/json'}, 214 headers: {'Content-Type': 'application/json'},
215 body: JSON.stringify({ 215 body: JSON.stringify({
216 query: "玩具", 216 query: "玩具",
217 - filters: {categoryName_keyword: "玩具"}, 217 + filters: {category.keyword: "玩具"},
218 range_filters: {price: {gte: 50, lte: 200}}, 218 range_filters: {price: {gte: 50, lte: 200}},
219 - facets: ["brandName_keyword"], 219 + facets: ["vendor.keyword"],
220 sort_by: "min_price", 220 sort_by: "min_price",
221 sort_order: "asc" 221 sort_order: "asc"
222 }) 222 })
docs/系统设计文档.md
@@ -24,17 +24,17 @@ @@ -24,17 +24,17 @@
24 - 鎵鏈夊鎴峰叡浜悓涓涓狤lasticsearch绱㈠紩锛歚search_products` 24 - 鎵鏈夊鎴峰叡浜悓涓涓狤lasticsearch绱㈠紩锛歚search_products`
25 - 绱㈠紩绮掑害锛歋PU绾у埆锛堟瘡涓枃妗d唬琛ㄤ竴涓猄PU锛 25 - 绱㈠紩绮掑害锛歋PU绾у埆锛堟瘡涓枃妗d唬琛ㄤ竴涓猄PU锛
26 - 鏁版嵁闅旂锛氶氳繃`tenant_id`瀛楁瀹炵幇绉熸埛闅旂 26 - 鏁版嵁闅旂锛氶氳繃`tenant_id`瀛楁瀹炵幇绉熸埛闅旂
27 -- 宓屽缁撴瀯锛氭瘡涓猄PU鏂囨。鍖呭惈宓屽鐨刞variants`鏁扮粍锛圫KU鍙樹綋锛 27 +- 宓屽缁撴瀯锛氭瘡涓猄PU鏂囨。鍖呭惈宓屽鐨刞skus`鏁扮粍
28 28
29 **绱㈠紩鏂囨。缁撴瀯**锛 29 **绱㈠紩鏂囨。缁撴瀯**锛
30 ```json 30 ```json
31 { 31 {
32 "tenant_id": "1", 32 "tenant_id": "1",
33 - "product_id": "123", 33 + "spu_id": "123",
34 "title": "钃濈墮鑰虫満", 34 "title": "钃濈墮鑰虫満",
35 - "variants": [ 35 + "skus": [
36 { 36 {
37 - "variant_id": "456", 37 + "sku_id": "456",
38 "title": "榛戣壊", 38 "title": "榛戣壊",
39 "price": 199.99, 39 "price": 199.99,
40 "sku": "SKU-123-1", 40 "sku": "SKU-123-1",
@@ -108,7 +108,7 @@ fields: @@ -108,7 +108,7 @@ fields:
108 store: true 108 store: true
109 109
110 # 鍟嗗搧鏍囪瘑瀛楁 110 # 鍟嗗搧鏍囪瘑瀛楁
111 - - name: "product_id" 111 + - name: "spu_id"
112 type: "KEYWORD" 112 type: "KEYWORD"
113 required: true 113 required: true
114 index: true 114 index: true
@@ -129,12 +129,12 @@ fields: @@ -129,12 +129,12 @@ fields:
129 index: true 129 index: true
130 store: true 130 store: true
131 131
132 - # 宓屽variants瀛楁  
133 - - name: "variants" 132 + # 宓屽skus瀛楁
  133 + - name: "skus"
134 type: "JSON" 134 type: "JSON"
135 nested: true 135 nested: true
136 nested_properties: 136 nested_properties:
137 - variant_id: 137 + sku_id:
138 type: "keyword" 138 type: "keyword"
139 price: 139 price:
140 type: "float" 140 type: "float"
@@ -246,7 +246,7 @@ indexes: @@ -246,7 +246,7 @@ indexes:
246 1. **鏁版嵁鍔犺浇**锛氫粠MySQL璇诲彇`shoplazza_product_spu`鍜宍shoplazza_product_sku`琛 246 1. **鏁版嵁鍔犺浇**锛氫粠MySQL璇诲彇`shoplazza_product_spu`鍜宍shoplazza_product_sku`琛
247 2. **鏁版嵁杞崲**锛坄indexer/spu_transformer.py`锛夛細 247 2. **鏁版嵁杞崲**锛坄indexer/spu_transformer.py`锛夛細
248 - 鎸塦spu_id`鍜宍tenant_id`鍏宠仈SPU鍜孲KU鏁版嵁 248 - 鎸塦spu_id`鍜宍tenant_id`鍏宠仈SPU鍜孲KU鏁版嵁
249 - - 灏哠KU鏁版嵁鑱氬悎涓哄祵濂楃殑`variants`鏁扮粍 249 + - 灏哠KU鏁版嵁鑱氬悎涓哄祵濂楃殑`skus`鏁扮粍
250 - 璁$畻鎵佸钩鍖栦环鏍煎瓧娈碉紙`min_price`, `max_price`, `compare_at_price`锛 250 - 璁$畻鎵佸钩鍖栦环鏍煎瓧娈碉紙`min_price`, `max_price`, `compare_at_price`锛
251 - 瀛楁鏄犲皠锛堝啓姝诲湪浠g爜涓紝涓嶄緷璧栭厤缃級 251 - 瀛楁鏄犲皠锛堝啓姝诲湪浠g爜涓紝涓嶄緷璧栭厤缃級
252 - 娉ㄥ叆`tenant_id`瀛楁 252 - 娉ㄥ叆`tenant_id`瀛楁
@@ -518,7 +518,7 @@ ranking: @@ -518,7 +518,7 @@ ranking:
518 518
519 ### 6.6 Base閰嶇疆锛堝簵鍖犻氱敤锛 519 ### 6.6 Base閰嶇疆锛堝簵鍖犻氱敤锛
520 - 鉁 SPU绾у埆绱㈠紩缁撴瀯 520 - 鉁 SPU绾у埆绱㈠紩缁撴瀯
521 -- 鉁 宓屽variants瀛楁 521 +- 鉁 宓屽skus瀛楁
522 - 鉁 缁熶竴绱㈠紩锛坰earch_products锛 522 - 鉁 缁熶竴绱㈠紩锛坰earch_products锛
523 - 鉁 绉熸埛闅旂锛坱enant_id锛 523 - 鉁 绉熸埛闅旂锛坱enant_id锛
524 - 鉁 閰嶇疆绠鍖栵紙绉婚櫎MySQL鐩稿叧閰嶇疆锛 524 - 鉁 閰嶇疆绠鍖栵紙绉婚櫎MySQL鐩稿叧閰嶇疆锛
@@ -548,11 +548,11 @@ API杩斿洖鏍煎紡涓嶅寘鍚獷S鍐呴儴瀛楁锛坄_id`, `_score`, `_source`锛夛紝浣跨敤 @@ -548,11 +548,11 @@ API杩斿洖鏍煎紡涓嶅寘鍚獷S鍐呴儴瀛楁锛坄_id`, `_score`, `_source`锛夛紝浣跨敤
548 { 548 {
549 "results": [ 549 "results": [
550 { 550 {
551 - "product_id": "123", 551 + "spu_id": "123",
552 "title": "钃濈墮鑰虫満", 552 "title": "钃濈墮鑰虫満",
553 - "variants": [ 553 + "skus": [
554 { 554 {
555 - "variant_id": "456", 555 + "sku_id": "456",
556 "price": 199.99, 556 "price": 199.99,
557 "sku": "SKU-123-1", 557 "sku": "SKU-123-1",
558 "stock": 50 558 "stock": 50
@@ -569,8 +569,8 @@ API杩斿洖鏍煎紡涓嶅寘鍚獷S鍐呴儴瀛楁锛坄_id`, `_score`, `_source`锛夛紝浣跨敤 @@ -569,8 +569,8 @@ API杩斿洖鏍煎紡涓嶅寘鍚獷S鍐呴儴瀛楁锛坄_id`, `_score`, `_source`锛夛紝浣跨敤
569 ``` 569 ```
570 570
571 **涓昏鍙樺寲**锛 571 **涓昏鍙樺寲**锛
572 -- 缁撴瀯鍖栫粨鏋滐紙`ProductResult`鍜宍VariantResult`锛  
573 -- 宓屽variants鏁扮粍 572 +- 缁撴瀯鍖栫粨鏋滐紙`SpuResult`鍜宍SkuResult`锛
  573 +- 宓屽skus鏁扮粍
574 - 鏃燛S鍐呴儴瀛楁 574 - 鏃燛S鍐呴儴瀛楁
575 575
576 ### 8.2 绉熸埛闅旂 576 ### 8.2 绉熸埛闅旂
@@ -610,14 +610,14 @@ Elasticsearch @@ -610,14 +610,14 @@ Elasticsearch
610 610
611 - **绠鍗曟ā寮**锛氬瓧绗︿覆鍒楄〃锛堝瓧娈靛悕锛夛紝浣跨敤榛樿閰嶇疆 611 - **绠鍗曟ā寮**锛氬瓧绗︿覆鍒楄〃锛堝瓧娈靛悕锛夛紝浣跨敤榛樿閰嶇疆
612 ```json 612 ```json
613 - ["categoryName_keyword", "brandName_keyword"] 613 + ["category.keyword", "vendor.keyword"]
614 ``` 614 ```
615 615
616 - **楂樼骇妯″紡**锛欶acetConfig 瀵硅薄鍒楄〃锛屾敮鎸佽嚜瀹氫箟閰嶇疆 616 - **楂樼骇妯″紡**锛欶acetConfig 瀵硅薄鍒楄〃锛屾敮鎸佽嚜瀹氫箟閰嶇疆
617 ```json 617 ```json
618 [ 618 [
619 { 619 {
620 - "field": "categoryName_keyword", 620 + "field": "category.keyword",
621 "size": 15, 621 "size": 15,
622 "type": "terms" 622 "type": "terms"
623 }, 623 },
@@ -714,11 +714,3 @@ class FacetResult(BaseModel): @@ -714,11 +714,3 @@ class FacetResult(BaseModel):
714 **鍏朵粬瀹㈡埛閰嶇疆**锛歚config/schema/tenant1/config.yaml` 714 **鍏朵粬瀹㈡埛閰嶇疆**锛歚config/schema/tenant1/config.yaml`
715 715
716 --- 716 ---
717 -  
718 -## 9. 鐩稿叧鏂囨。  
719 -  
720 -- `MULTILANG_FEATURE.md` - 澶氳瑷鍔熻兘璇︾粏璇存槑  
721 -- `QUICKSTART.md` - 蹇熷紑濮嬫寚鍗  
722 -- `HighLevelDesign.md` - 楂樺眰璁捐鏂囨。  
723 -- `IMPLEMENTATION_SUMMARY.md` - 瀹炵幇鎬荤粨  
724 -- `鍟嗗搧鏁版嵁婧愬叆ES閰嶇疆瑙勮寖.md` - 鏁版嵁婧愰厤缃鑼  
docs/索引字段说明.md
@@ -11,7 +11,7 @@ @@ -11,7 +11,7 @@
11 - 原始数据与用户环境均为多语言,需根据语言路由到不同分析器/索引方式,在线搜索时也要考虑多语言的适配。 11 - 原始数据与用户环境均为多语言,需根据语言路由到不同分析器/索引方式,在线搜索时也要考虑多语言的适配。
12 4. **搜索接口适配** 12 4. **搜索接口适配**
13 - 接口简单,自动为多语言的数据源和 query 适配最优检索策略。 13 - 接口简单,自动为多语言的数据源和 query 适配最优检索策略。
14 - - 返回的结果格式约定为店匠系列的 SPU/SKU(products/variants)嵌套结构。 14 +- 返回的结果格式约定为店匠系列的 SPU/SKU嵌套结构。
15 - 支撑 facet/过滤/排序业务需求:用户可以选择任何一个 keyword 或 HKText 类型的字段做筛选、聚合;也可以选择任何一个数值型字段做 Range 过滤或排序。 15 - 支撑 facet/过滤/排序业务需求:用户可以选择任何一个 keyword 或 HKText 类型的字段做筛选、聚合;也可以选择任何一个数值型字段做 Range 过滤或排序。
16 16
17 本文档详细说明了 Elasticsearch 索引中所有字段的类型、索引方式、数据来源等信息。 17 本文档详细说明了 Elasticsearch 索引中所有字段的类型、索引方式、数据来源等信息。
@@ -20,7 +20,7 @@ @@ -20,7 +20,7 @@
20 20
21 - **索引名称**: `search_products` 21 - **索引名称**: `search_products`
22 - **索引级别**: SPU级别(商品级别) 22 - **索引级别**: SPU级别(商品级别)
23 -- **数据结构**: SPU文档包含嵌套的variants(SKU)数组 23 +- **数据结构**: SPU文档包含嵌套的skus数组
24 24
25 ## 索引类型与处理说明 25 ## 索引类型与处理说明
26 26
@@ -131,7 +131,7 @@ @@ -131,7 +131,7 @@
131 | 索引字段名 | ES字段类型 | 是否索引 | 数据来源表 | 表中字段名 | 表中字段类型 | 数据预处理 | 说明 | 131 | 索引字段名 | ES字段类型 | 是否索引 | 数据来源表 | 表中字段名 | 表中字段类型 | 数据预处理 | 说明 |
132 |-----------|-----------|---------|-----------|-----------|-------------|-------------|------| 132 |-----------|-----------|---------|-----------|-----------|-------------|-------------|------|
133 | tenant_id | KEYWORD | 是 | SPU表 | tenant_id | BIGINT | BIGINT转字符串 | 租户ID,用于多租户隔离 | 133 | tenant_id | KEYWORD | 是 | SPU表 | tenant_id | BIGINT | BIGINT转字符串 | 租户ID,用于多租户隔离 |
134 -| product_id | KEYWORD | 是 | SPU表 | id | BIGINT | BIGINT转字符串 | 商品ID(SPU ID) | 134 +| spu_id | KEYWORD | 是 | SPU表 | id | BIGINT | BIGINT转字符串 | SPU ID(主键) |
135 | handle | KEYWORD | 是 | SPU表 | handle | VARCHAR(255) | | 商品URL handle | 135 | handle | KEYWORD | 是 | SPU表 | handle | VARCHAR(255) | | 商品URL handle |
136 136
137 数据预处理列留空表示该字段无需额外处理。 137 数据预处理列留空表示该字段无需额外处理。
@@ -156,14 +156,9 @@ @@ -156,14 +156,9 @@
156 156
157 | 索引字段名 | ES字段类型 | 是否索引 | 数据来源表 | 表中字段名 | 表中字段类型 | Boost权重 | 是否返回 | 数据预处理 | 说明 | 157 | 索引字段名 | ES字段类型 | 是否索引 | 数据来源表 | 表中字段名 | 表中字段类型 | Boost权重 | 是否返回 | 数据预处理 | 说明 |
158 |-----------|-----------|---------|-----------|-----------|-------------|-----------|---------|-------------|------| 158 |-----------|-----------|---------|-----------|-----------|-------------|-----------|---------|-------------|------|
159 -| vendor | TEXT | 是 | SPU表 | vendor | VARCHAR(255) | 1.5 | 是 | | 供应商/品牌(文本搜索) |  
160 -| vendor_keyword | HKText | 是 | SPU表 | vendor | VARCHAR(255) | - | 否 | 按逗号分割为list,去除空白项 | 供应商/品牌(HKText类型,keyword子字段用于过滤) |  
161 -| product_type | TEXT | 是 | SPU表 | category | VARCHAR(255) | 1.5 | 是 | | 商品类型(文本搜索) |  
162 -| product_type_keyword | HKText | 是 | SPU表 | category | VARCHAR(255) | - | 否 | 按逗号分割为list,去除空白项 | 商品类型(HKText类型) |  
163 -| tags | HKText | 是 | SPU表 | tags | VARCHAR(1024) | 1.0 | 是 | | 标签(HKText类型,支持搜索+过滤) |  
164 -| tags_keyword | HKText | 是 | SPU表 | tags | VARCHAR(1024) | - | 否 | 按逗号分割为list,去除空白项 | 标签keyword别名(HKText类型) |  
165 -| category | TEXT | 是 | SPU表 | category | VARCHAR(255) | 1.5 | 是 | | 类目(文本搜索) |  
166 -| category_keyword | HKText | 是 | SPU表 | category | VARCHAR(255) | - | 否 | 按逗号分割为list,去除空白项 | 类目(HKText类型) | 159 +| vendor | HKText | 是 | SPU表 | vendor | VARCHAR(255) | 1.5 | 是 | | 供应商/品牌,HKText字段自动提供 `vendor.keyword` 用于过滤、聚合 |
  160 +| tags | HKText | 是 | SPU表 | tags | VARCHAR(1024) | 1.0 | 是 | | 标签字段,支持模糊搜索;使用 `tags.keyword` 进行精确过滤 |
  161 +| category | HKText | 是 | SPU表 | category | VARCHAR(255) | 1.5 | 是 | | 类目字段,使用 `category.keyword` 进行过滤/分面 |
167 162
168 ### 价格字段 163 ### 价格字段
169 164
@@ -204,23 +199,23 @@ @@ -204,23 +199,23 @@
204 | shoplazza_created_at | DATE | 是 | SPU表 | shoplazza_created_at | DATETIME | 否 | 转换为UTC ISO8601字符串 | 店匠系统创建时间 | 199 | shoplazza_created_at | DATE | 是 | SPU表 | shoplazza_created_at | DATETIME | 否 | 转换为UTC ISO8601字符串 | 店匠系统创建时间 |
205 | shoplazza_updated_at | DATE | 是 | SPU表 | shoplazza_updated_at | DATETIME | 否 | 转换为UTC ISO8601字符串 | 店匠系统更新时间 | 200 | shoplazza_updated_at | DATE | 是 | SPU表 | shoplazza_updated_at | DATETIME | 否 | 转换为UTC ISO8601字符串 | 店匠系统更新时间 |
206 201
207 -### 嵌套Variants字段(SKU级别) 202 +### 嵌套SKUs字段(SKU级别)
208 203
209 | 索引字段名 | ES字段类型 | 是否索引 | 数据来源表 | 表中字段名 | 表中字段类型 | 数据预处理 | 说明 | 204 | 索引字段名 | ES字段类型 | 是否索引 | 数据来源表 | 表中字段名 | 表中字段类型 | 数据预处理 | 说明 |
210 |-----------|-----------|---------|-----------|-----------|-------------|-------------|------| 205 |-----------|-----------|---------|-----------|-----------|-------------|-------------|------|
211 -| variants | JSON (nested) | 是 | SKU表 | - | - | 汇总同SPU下SKU记录,构建nested数组 | 商品变体数组(嵌套结构) | 206 +| skus | JSON (nested) | 是 | SKU表 | - | - | 汇总同SPU下SKU记录,构建nested数组 | SKU数组(嵌套结构) |
212 207
213 -#### Variants子字段 208 +#### SKUs子字段
214 209
215 | 索引字段名 | ES字段类型 | 是否索引 | 数据来源表 | 表中字段名 | 表中字段类型 | 数据预处理 | 说明 | 210 | 索引字段名 | ES字段类型 | 是否索引 | 数据来源表 | 表中字段名 | 表中字段类型 | 数据预处理 | 说明 |
216 |-----------|-----------|---------|-----------|-----------|-------------|-------------|------| 211 |-----------|-----------|---------|-----------|-----------|-------------|-------------|------|
217 -| variants.variant_id | keyword | 是 | SKU表 | id | BIGINT | BIGINT转字符串 | 变体ID(SKU ID) |  
218 -| variants.title | text | 是 | SKU表 | title | VARCHAR(500) | | 变体标题 |  
219 -| variants.price | float | 是 | SKU表 | price | DECIMAL(10,2) | DECIMAL转FLOAT | 变体价格 |  
220 -| variants.compare_at_price | float | 是 | SKU表 | compare_at_price | DECIMAL(10,2) | DECIMAL转FLOAT | 变体原价 |  
221 -| variants.sku | keyword | 是 | SKU表 | sku | VARCHAR(100) | | SKU编码 |  
222 -| variants.stock | long | 是 | SKU表 | inventory_quantity | INT(11) | INT转LONG | 库存数量 |  
223 -| variants.options | object | 是 | SKU表 | option1/option2/option3 | VARCHAR(255) | 合并option1/2/3并去除空值 | 选项(颜色、尺寸等) | 212 +| skus.sku_id | keyword | 是 | SKU表 | id | BIGINT | BIGINT转字符串 | SKU ID |
  213 +| skus.title | text | 是 | SKU表 | title | VARCHAR(500) | | SKU标题 |
  214 +| skus.price | float | 是 | SKU表 | price | DECIMAL(10,2) | DECIMAL转FLOAT | SKU价格 |
  215 +| skus.compare_at_price | float | 是 | SKU表 | compare_at_price | DECIMAL(10,2) | DECIMAL转FLOAT | 原价 |
  216 +| skus.sku | keyword | 是 | SKU表 | sku | VARCHAR(100) | | SKU编码 |
  217 +| skus.stock | long | 是 | SKU表 | inventory_quantity | INT(11) | INT转LONG | 库存数量 |
  218 +| skus.options | object | 是 | SKU表 | option1/option2/option3 | VARCHAR(255) | 合并option1/2/3并去除空值 | 选项(颜色、尺寸等) |
224 219
225 **Variants结构说明**: 220 **Variants结构说明**:
226 - `variants` 是一个嵌套对象数组,每个元素代表一个SKU 221 - `variants` 是一个嵌套对象数组,每个元素代表一个SKU
@@ -285,7 +280,7 @@ @@ -285,7 +280,7 @@
285 280
286 ### 数据类型转换 281 ### 数据类型转换
287 282
288 -1. **BIGINT → KEYWORD**: 数字ID转换为字符串(如 `product_id`, `variant_id`) 283 +1. **BIGINT → KEYWORD**: 数字ID转换为字符串(如 `spu_id`, `sku_id`)
289 2. **DECIMAL → FLOAT**: 价格字段从DECIMAL转换为FLOAT 284 2. **DECIMAL → FLOAT**: 价格字段从DECIMAL转换为FLOAT
290 3. **INT → LONG**: 库存数量从INT转换为LONG 285 3. **INT → LONG**: 库存数量从INT转换为LONG
291 4. **DATETIME → DATE**: 时间字段转换为ISO格式字符串 286 4. **DATETIME → DATE**: 时间字段转换为ISO格式字符串
@@ -327,6 +322,53 @@ @@ -327,6 +322,53 @@
327 - `shoplazza_created_at`: DATETIME - 店匠创建时间 322 - `shoplazza_created_at`: DATETIME - 店匠创建时间
328 - `shoplazza_updated_at`: DATETIME - 店匠更新时间 323 - `shoplazza_updated_at`: DATETIME - 店匠更新时间
329 324
  325 +spu表全部字段
  326 +"Field" "Type" "Null" "Key" "Default" "Extra"
  327 +"id" "bigint(20)" "NO" "PRI" "auto_increment"
  328 +"shop_id" "bigint(20)" "NO" "MUL" ""
  329 +"shoplazza_id" "varchar(64)" "NO" "" ""
  330 +"handle" "varchar(255)" "YES" "MUL" ""
  331 +"title" "varchar(500)" "NO" "" ""
  332 +"brief" "varchar(1000)" "YES" "" ""
  333 +"description" "text" "YES" "" ""
  334 +"spu" "varchar(100)" "YES" "" ""
  335 +"vendor" "varchar(255)" "YES" "" ""
  336 +"vendor_url" "varchar(500)" "YES" "" ""
  337 +"seo_title" "varchar(500)" "YES" "" ""
  338 +"seo_description" "text" "YES" "" ""
  339 +"seo_keywords" "text" "YES" "" ""
  340 +"image_src" "varchar(500)" "YES" "" ""
  341 +"image_width" "int(11)" "YES" "" ""
  342 +"image_height" "int(11)" "YES" "" ""
  343 +"image_path" "varchar(255)" "YES" "" ""
  344 +"image_alt" "varchar(500)" "YES" "" ""
  345 +"inventory_policy" "varchar(50)" "YES" "" ""
  346 +"inventory_quantity" "int(11)" "YES" "" "0" ""
  347 +"inventory_tracking" "tinyint(1)" "YES" "" "0" ""
  348 +"published" "tinyint(1)" "YES" "" "0" ""
  349 +"published_at" "datetime" "YES" "MUL" ""
  350 +"requires_shipping" "tinyint(1)" "YES" "" "1" ""
  351 +"taxable" "tinyint(1)" "YES" "" "0" ""
  352 +"fake_sales" "int(11)" "YES" "" "0" ""
  353 +"display_fake_sales" "tinyint(1)" "YES" "" "0" ""
  354 +"mixed_wholesale" "tinyint(1)" "YES" "" "0" ""
  355 +"need_variant_image" "tinyint(1)" "YES" "" "0" ""
  356 +"has_only_default_variant" "tinyint(1)" "YES" "" "0" ""
  357 +"tags" "text" "YES" "" ""
  358 +"note" "text" "YES" "" ""
  359 +"category" "varchar(255)" "YES" "" ""
  360 +"shoplazza_created_at" "datetime" "YES" "" ""
  361 +"shoplazza_updated_at" "datetime" "YES" "MUL" ""
  362 +"tenant_id" "bigint(20)" "NO" "MUL" ""
  363 +"creator" "varchar(64)" "YES" "" "" ""
  364 +"create_time" "datetime" "NO" "" "CURRENT_TIMESTAMP" ""
  365 +"updater" "varchar(64)" "YES" "" "" ""
  366 +"update_time" "datetime" "NO" "" "CURRENT_TIMESTAMP" "on update CURRENT_TIMESTAMP"
  367 +"deleted" "bit(1)" "NO" "" "b'0'" ""
  368 +
  369 +
  370 +
  371 +
330 ### SKU表(shoplazza_product_sku) 372 ### SKU表(shoplazza_product_sku)
331 373
332 主要字段: 374 主要字段:
@@ -341,6 +383,40 @@ @@ -341,6 +383,40 @@
341 - `option2`: VARCHAR(255) - 选项2 383 - `option2`: VARCHAR(255) - 选项2
342 - `option3`: VARCHAR(255) - 选项3 384 - `option3`: VARCHAR(255) - 选项3
343 385
  386 +sku全部字段
  387 +"Field" "Type" "Null" "Key" "Default" "Extra"
  388 +"id" "bigint(20)" "NO" "PRI" "auto_increment"
  389 +"spu_id" "bigint(20)" "NO" "MUL" ""
  390 +"shop_id" "bigint(20)" "NO" "MUL" ""
  391 +"shoplazza_id" "varchar(64)" "NO" "" ""
  392 +"shoplazza_product_id" "varchar(64)" "NO" "MUL" ""
  393 +"shoplazza_image_id" "varchar(64)" "YES" "" ""
  394 +"title" "varchar(500)" "YES" "" ""
  395 +"sku" "varchar(100)" "YES" "MUL" ""
  396 +"barcode" "varchar(100)" "YES" "" ""
  397 +"position" "int(11)" "YES" "" "0" ""
  398 +"price" "decimal(10,2)" "YES" "" ""
  399 +"compare_at_price" "decimal(10,2)" "YES" "" ""
  400 +"cost_price" "decimal(10,2)" "YES" "" ""
  401 +"option1" "varchar(255)" "YES" "" ""
  402 +"option2" "varchar(255)" "YES" "" ""
  403 +"option3" "varchar(255)" "YES" "" ""
  404 +"inventory_quantity" "int(11)" "YES" "" "0" ""
  405 +"weight" "decimal(10,2)" "YES" "" ""
  406 +"weight_unit" "varchar(10)" "YES" "" ""
  407 +"image_src" "varchar(500)" "YES" "" ""
  408 +"wholesale_price" "json" "YES" "" ""
  409 +"note" "text" "YES" "" ""
  410 +"extend" "json" "YES" "" ""
  411 +"shoplazza_created_at" "datetime" "YES" "" ""
  412 +"shoplazza_updated_at" "datetime" "YES" "" ""
  413 +"tenant_id" "bigint(20)" "NO" "MUL" ""
  414 +"creator" "varchar(64)" "YES" "" "" ""
  415 +"create_time" "datetime" "NO" "" "CURRENT_TIMESTAMP" ""
  416 +"updater" "varchar(64)" "YES" "" "" ""
  417 +"update_time" "datetime" "NO" "" "CURRENT_TIMESTAMP" "on update CURRENT_TIMESTAMP"
  418 +"deleted" "bit(1)" "NO" "" "b'0'" ""
  419 +
344 420
345 ## TODO 421 ## TODO
346 多语言问题。 422 多语言问题。
docs/设计文档.md 0 → 100644
@@ -0,0 +1,716 @@ @@ -0,0 +1,716 @@
  1 +# 搜索引擎通用化开发进度
  2 +
  3 +## 项目概述
  4 +
  5 +对后端搜索技术 做通用化。
  6 +通用化的本质 是 对于各种业务数据、各种检索需求,都可以 用少量定制+配置化 来实现效果。
  7 +
  8 +
  9 +**通用化的本质**:对于各种业务数据、各种检索需求,都可以用少量定制+配置化来实现效果。
  10 +
  11 +---
  12 +
  13 +## 1. 原始数据层的约定
  14 +
  15 +### 1.1 店匠主表
  16 +
  17 +所有租户共用以下主表:
  18 +- `shoplazza_product_sku` - SKU级别商品数据
  19 +- `shoplazza_product_spu` - SPU级别商品数据
  20 +
  21 +### 1.2 索引结构(SPU维度)
  22 +
  23 +**统一索引架构**:
  24 +- 所有客户共享同一个Elasticsearch索引:`search_products`
  25 +- 索引粒度:SPU级别(每个文档代表一个SPU)
  26 +- 数据隔离:通过`tenant_id`字段实现租户隔离
  27 +- 嵌套结构:每个SPU文档包含嵌套的`skus`数组(SKU变体)
  28 +
  29 +**索引文档结构**:
  30 +```json
  31 +{
  32 + "tenant_id": "1",
  33 + "spu_id": "123",
  34 + "title": "蓝牙耳机",
  35 + "skus": [
  36 + {
  37 + "sku_id": "456",
  38 + "title": "黑色",
  39 + "price": 199.99,
  40 + "sku": "SKU-123-1",
  41 + "stock": 50
  42 + }
  43 + ],
  44 + "min_price": 199.99,
  45 + "max_price": 299.99
  46 +}
  47 +```
  48 +
  49 +### 1.3 配置化方案
  50 +
  51 +**配置分离原则**:
  52 +- **搜索配置**:只包含ES字段定义、查询域、排序规则等搜索相关配置
  53 +- **数据源配置**:不在搜索配置中,由Pipeline层(脚本)决定
  54 +- **数据导入流程**:写死的脚本,不依赖配置
  55 +
  56 +统一通过配置文件定义:
  57 +1. ES 字段定义(字段类型、分析器、boost等)
  58 +2. ES mapping 结构生成
  59 +3. 查询域配置(indexes)
  60 +4. 排序和打分配置(function_score)
  61 +
  62 +**注意**:配置中**不包含**以下内容:
  63 +- `mysql_config` - MySQL数据库配置
  64 +- `main_table` / `extension_table` - 数据表配置
  65 +- `source_table` / `source_column` - 字段数据源映射
  66 +
  67 +---
  68 +
  69 +## 2. 配置系统实现
  70 +
  71 +### 2.1 应用结构配置(字段定义)
  72 +
  73 +**配置文件位置**:`config/schema/{tenant_id}_config.yaml`
  74 +
  75 +**配置内容**:定义了 ES 的输入数据有哪些字段、关联 MySQL 的哪些字段。
  76 +
  77 +**实现情况**:
  78 +
  79 +#### 字段类型支持
  80 +- **TEXT**:文本字段,支持多语言分析器
  81 +- **KEYWORD**:关键词字段,用于精确匹配和聚合
  82 +- **TEXT_EMBEDDING**:文本向量字段(1024维,dot_product相似度)
  83 +- **IMAGE_EMBEDDING**:图片向量字段(1024维,dot_product相似度)
  84 +- **INT/LONG**:整数类型
  85 +- **FLOAT/DOUBLE**:浮点数类型
  86 +- **DATE**:日期类型
  87 +- **BOOLEAN**:布尔类型
  88 +
  89 +#### 分析器支持
  90 +- **chinese_ecommerce**:中文电商分词器(index_ansj/query_ansj)
  91 +- **english**:英文分析器
  92 +- **russian**:俄文分析器
  93 +- **arabic**:阿拉伯文分析器
  94 +- **spanish**:西班牙文分析器
  95 +- **japanese**:日文分析器
  96 +- **standard**:标准分析器
  97 +- **keyword**:关键词分析器
  98 +
  99 +#### 字段配置示例(Base配置)
  100 +
  101 +```yaml
  102 +fields:
  103 + # 租户隔离字段(必需)
  104 + - name: "tenant_id"
  105 + type: "KEYWORD"
  106 + required: true
  107 + index: true
  108 + store: true
  109 +
  110 + # 商品标识字段
  111 + - name: "spu_id"
  112 + type: "KEYWORD"
  113 + required: true
  114 + index: true
  115 + store: true
  116 +
  117 + # 文本搜索字段
  118 + - name: "title"
  119 + type: "TEXT"
  120 + analyzer: "chinese_ecommerce"
  121 + boost: 3.0
  122 + index: true
  123 + store: true
  124 +
  125 + - name: "seo_keywords"
  126 + type: "TEXT"
  127 + analyzer: "chinese_ecommerce"
  128 + boost: 2.0
  129 + index: true
  130 + store: true
  131 +
  132 + # 嵌套skus字段
  133 + - name: "skus"
  134 + type: "JSON"
  135 + nested: true
  136 + nested_properties:
  137 + sku_id:
  138 + type: "keyword"
  139 + price:
  140 + type: "float"
  141 + sku:
  142 + type: "keyword"
  143 +```
  144 +
  145 +**注意**:配置中**不包含**`source_table`和`source_column`,数据源映射由Pipeline层决定。
  146 +
  147 +**实现模块**:
  148 +- `config/config_loader.py` - 配置加载器
  149 +- `config/field_types.py` - 字段类型定义
  150 +- `indexer/mapping_generator.py` - ES mapping 生成器
  151 +- `indexer/data_transformer.py` - 数据转换器
  152 +
  153 +### 2.2 索引结构配置(查询域配置)
  154 +
  155 +**配置内容**:定义了 ES 的字段索引 mapping 配置,支持各个域的查询,包括默认域的查询。
  156 +
  157 +**实现情况**:
  158 +
  159 +#### 域(Domain)配置
  160 +每个域定义了:
  161 +- 域名称(如 `default`, `title`, `category`, `brand`)
  162 +- 域标签(中文描述)
  163 +- 搜索字段列表
  164 +- 默认分析器
  165 +- 权重(boost)
  166 +- **多语言字段映射**(`language_field_mapping`)
  167 +
  168 +#### 多语言字段映射
  169 +
  170 +支持将不同语言的查询路由到对应的字段:
  171 +
  172 +```yaml
  173 +indexes:
  174 + - name: "default"
  175 + label: "默认索引"
  176 + fields:
  177 + - "name"
  178 + - "enSpuName"
  179 + - "ruSkuName"
  180 + - "categoryName"
  181 + - "brandName"
  182 + analyzer: "chinese_ecommerce"
  183 + boost: 1.0
  184 + language_field_mapping:
  185 + zh:
  186 + - "name"
  187 + - "categoryName"
  188 + - "brandName"
  189 + en:
  190 + - "enSpuName"
  191 + ru:
  192 + - "ruSkuName"
  193 +
  194 + - name: "title"
  195 + label: "标题索引"
  196 + fields:
  197 + - "name"
  198 + - "enSpuName"
  199 + - "ruSkuName"
  200 + analyzer: "chinese_ecommerce"
  201 + boost: 2.0
  202 + language_field_mapping:
  203 + zh:
  204 + - "name"
  205 + en:
  206 + - "enSpuName"
  207 + ru:
  208 + - "ruSkuName"
  209 +```
  210 +
  211 +**工作原理**:
  212 +1. 检测查询语言(中文、英文、俄文等)
  213 +2. 如果查询语言在 `language_field_mapping` 中,使用原始查询搜索对应语言的字段
  214 +3. 将查询翻译到其他支持的语言,分别搜索对应语言的字段
  215 +4. 组合多个语言查询的结果,提高召回率
  216 +
  217 +**实现模块**:
  218 +- `search/multilang_query_builder.py` - 多语言查询构建器
  219 +- `query/query_parser.py` - 查询解析器(支持语言检测和翻译)
  220 +
  221 +---
  222 +
  223 +## 3. 数据导入流程
  224 +
  225 +### 3.1 数据源
  226 +
  227 +**店匠标准表**(Base配置使用):
  228 +- `shoplazza_product_spu` - SPU级别商品数据
  229 +- `shoplazza_product_sku` - SKU级别商品数据
  230 +
  231 +**其他客户表**(tenant1等):
  232 +- 使用各自的数据源表和扩展表
  233 +
  234 +### 3.2 数据导入方式
  235 +
  236 +**Pipeline层决定数据源**:
  237 +- 数据导入流程是写死的脚本,不依赖配置
  238 +- 配置只关注ES搜索相关的内容
  239 +- 数据源映射逻辑写死在转换器代码中
  240 +
  241 +#### Base配置数据导入(店匠通用)
  242 +
  243 +**脚本**:`scripts/ingest_shoplazza.py`
  244 +
  245 +**数据流程**:
  246 +1. **数据加载**:从MySQL读取`shoplazza_product_spu`和`shoplazza_product_sku`表
  247 +2. **数据转换**(`indexer/spu_transformer.py`):
  248 + - 按`spu_id`和`tenant_id`关联SPU和SKU数据
  249 + - 将SKU数据聚合为嵌套的`skus`数组
  250 + - 计算扁平化价格字段(`min_price`, `max_price`, `compare_at_price`)
  251 + - 字段映射(写死在代码中,不依赖配置)
  252 + - 注入`tenant_id`字段
  253 +3. **索引创建**:
  254 + - 根据配置生成ES mapping
  255 + - 创建或更新`search_products`索引
  256 +4. **批量入库**:
  257 + - 批量写入ES(默认每批500条)
  258 + - 错误处理和重试机制
  259 +
  260 +**命令行工具**:
  261 +```bash
  262 +python scripts/ingest_shoplazza.py \
  263 + --db-host localhost \
  264 + --db-port 3306 \
  265 + --db-database saas \
  266 + --db-username root \
  267 + --db-password password \
  268 + --tenant-id "1" \
  269 + --config base \
  270 + --es-host http://localhost:9200 \
  271 + --recreate \
  272 + --batch-size 500
  273 +```
  274 +
  275 +#### 其他客户数据导入
  276 +
  277 +- 使用各自的数据转换器(如`indexer/data_transformer.py`)
  278 +- 数据源映射逻辑写死在各自的转换器中
  279 +- 共享`search_products`索引,通过`tenant_id`隔离
  280 +
  281 +**实现模块**:
  282 +- `indexer/spu_transformer.py` - SPU数据转换器(Base配置)
  283 +- `indexer/data_transformer.py` - 通用数据转换器(其他客户)
  284 +- `indexer/bulk_indexer.py` - 批量索引器
  285 +- `scripts/ingest_shoplazza.py` - 店匠数据导入脚本
  286 +
  287 +---
  288 +
  289 +## 4. QueryParser 实现
  290 +
  291 +
  292 +### 4.1 查询改写(Query Rewriting)
  293 +
  294 +配置词典的key是query,value是改写后的查询表达式,比如。比如品牌词 改写为在brand|query OR name|query,类别词、标签词等都可以放进去。纠错、规范化、查询改写等 都可以通过这个词典来配置。
  295 +**实现情况**:
  296 +
  297 +#### 配置方式
  298 +在 `query_config.rewrite_dictionary` 中配置查询改写规则:
  299 +
  300 +```yaml
  301 +query_config:
  302 + enable_query_rewrite: true
  303 + rewrite_dictionary:
  304 + "芭比": "brand:芭比 OR name:芭比娃娃"
  305 + "玩具": "category:玩具"
  306 + "消防": "category:消防 OR name:消防"
  307 +```
  308 +
  309 +#### 功能特性
  310 +- **精确匹配**:查询完全匹配词典 key 时,替换为 value
  311 +- **部分匹配**:查询包含词典 key 时,替换该部分
  312 +- **支持布尔表达式**:value 可以是复杂的布尔表达式(AND, OR, 域查询等)
  313 +
  314 +#### 实现模块
  315 +- `query/query_rewriter.py` - 查询改写器
  316 +- `query/query_parser.py` - 查询解析器(集成改写功能)
  317 +
  318 +### 4.2 翻译(Translation)
  319 +
  320 +**实现情况**:
  321 +
  322 +#### 配置方式
  323 +```yaml
  324 +query_config:
  325 + supported_languages:
  326 + - "zh"
  327 + - "en"
  328 + - "ru"
  329 + default_language: "zh"
  330 + enable_translation: true
  331 + translation_service: "deepl"
  332 + translation_api_key: null # 通过环境变量设置
  333 +```
  334 +
  335 +#### 功能特性
  336 +1. **语言检测**:自动检测查询语言
  337 +2. **智能翻译**:
  338 + - 如果查询是中文,翻译为英文、俄文
  339 + - 如果查询是英文,翻译为中文、俄文
  340 + - 如果查询是其他语言,翻译为所有支持的语言
  341 +3. **域感知翻译**:
  342 + - 如果域有 `language_field_mapping`,只翻译到映射中存在的语言
  343 + - 避免不必要的翻译,提高效率
  344 +4. **翻译缓存**:缓存翻译结果,避免重复调用 API
  345 +
  346 +#### 工作流程
  347 +```
  348 +查询输入 → 语言检测 → 确定目标语言 → 翻译 → 多语言查询构建
  349 +```
  350 +
  351 +#### 实现模块
  352 +- `query/language_detector.py` - 语言检测器
  353 +- `query/translator.py` - 翻译器(DeepL API)
  354 +- `query/query_parser.py` - 查询解析器(集成翻译功能)
  355 +
  356 +### 4.3 文本向量化(Text Embedding)
  357 +
  358 +如果配置打开了text_embedding查询,并且query 包含了default域的查询,那么要把default域的查询词转向量,后面searcher会用这个向量参与查询。
  359 +
  360 +**实现情况**:
  361 +
  362 +#### 配置方式
  363 +```yaml
  364 +query_config:
  365 + enable_text_embedding: true
  366 +```
  367 +
  368 +#### 功能特性
  369 +1. **条件生成**:
  370 + - 仅当 `enable_text_embedding=true` 时生成向量
  371 + - 仅对 `default` 域查询生成向量
  372 +2. **向量模型**:BGE-M3 模型(1024维向量)
  373 +3. **用途**:用于语义搜索(KNN 检索)
  374 +
  375 +#### 实现模块
  376 +- `embeddings/bge_encoder.py` - BGE 文本编码器
  377 +- `query/query_parser.py` - 查询解析器(集成向量生成)
  378 +
  379 +---
  380 +
  381 +## 5. Searcher 实现
  382 +
  383 +参考opensearch,他们自己定义的一套索引结构配置、支持自定义的一套检索表达式、排序表达式,这是各个客户进行配置化的基础,包括索引结构配置、排序策略配置。
  384 +比如各种业务过滤策略 可以简单的通过表达式满足,比如brand|耐克 AND cate2|xxx。指定字段排序可以通过排序的表达式实现。
  385 +
  386 +查询默认在default域,相也会对这个域的查询做一些相关性的重点优化,包括融合语义相关性、多语言相关性(可以基于配置 将查询翻译到指定语言并在对应的语言的字段进行查询)来弥补传统查询分析手段(比如查询改写 纠错 词权重等)的不足,也支持通过配置一些词表转为泛查询模式来优化相关性。
  387 +
  388 +### 5.1 布尔表达式解析
  389 +
  390 +**实现情况**:
  391 +
  392 +#### 支持的运算符
  393 +- **AND**:所有项必须匹配
  394 +- **OR**:任意项匹配
  395 +- **RANK**:排序增强(类似 OR 但影响排序)
  396 +- **ANDNOT**:排除(第一项匹配,第二项不匹配)
  397 +- **()**:括号分组
  398 +
  399 +#### 优先级(从高到低)
  400 +1. `()` - 括号
  401 +2. `ANDNOT` - 排除
  402 +3. `AND` - 与
  403 +4. `OR` - 或
  404 +5. `RANK` - 排序
  405 +
  406 +#### 示例
  407 +```
  408 +laptop AND (gaming OR professional) ANDNOT cheap
  409 +```
  410 +
  411 +#### 实现模块
  412 +- `search/boolean_parser.py` - 布尔表达式解析器
  413 +- `search/searcher.py` - 搜索器(集成布尔解析)
  414 +
  415 +### 5.2 多语言搜索
  416 +
  417 +**实现情况**:
  418 +
  419 +#### 工作原理
  420 +1. **查询解析**:
  421 + - 提取域(如 `title:查询` → 域=`title`,查询=`查询`)
  422 + - 检测查询语言
  423 + - 生成翻译
  424 +2. **多语言查询构建**:
  425 + - 如果域有 `language_field_mapping`:
  426 + - 使用检测到的语言查询对应字段(boost * 1.5)
  427 + - 使用翻译后的查询搜索其他语言字段(boost * 1.0)
  428 + - 如果域没有 `language_field_mapping`:
  429 + - 使用所有字段进行搜索
  430 +3. **查询组合**:
  431 + - 多个语言查询组合为 `should` 子句
  432 + - 提高召回率
  433 +
  434 +#### 示例
  435 +```
  436 +查询: "芭比娃娃"
  437 +域: default
  438 +检测语言: zh
  439 +
  440 +生成的查询:
  441 +- 中文查询 "芭比娃娃" → 搜索 name, categoryName, brandName (boost * 1.5)
  442 +- 英文翻译 "Barbie doll" → 搜索 enSpuName (boost * 1.0)
  443 +- 俄文翻译 "Кукла Барби" → 搜索 ruSkuName (boost * 1.0)
  444 +```
  445 +
  446 +#### 实现模块
  447 +- `search/multilang_query_builder.py` - 多语言查询构建器
  448 +- `search/searcher.py` - 搜索器(使用多语言构建器)
  449 +
  450 +### 5.3 相关性计算(Ranking)
  451 +
  452 +**实现情况**:
  453 +
  454 +#### 当前实现
  455 +**公式**:`bm25() + 0.2 * text_embedding_relevance()`
  456 +
  457 +- **bm25()**:BM25 文本相关性得分
  458 + - 包括多语言打分
  459 + - 内部通过配置翻译为多种语言
  460 + - 分别到对应的字段搜索
  461 + - 中文字段使用中文分词器,英文字段使用英文分词器
  462 +- **text_embedding_relevance()**:文本向量相关性得分(KNN 检索的打分)
  463 + - 权重:0.2
  464 +
  465 +#### 配置方式
  466 +```yaml
  467 +ranking:
  468 + expression: "bm25() + 0.2*text_embedding_relevance()"
  469 + description: "BM25 text relevance combined with semantic embedding similarity"
  470 +```
  471 +
  472 +#### 扩展性
  473 +- 支持表达式配置(未来可扩展)
  474 +- 支持自定义函数(如 `timeliness()`, `field_value()`)
  475 +
  476 +#### 实现模块
  477 +- `search/ranking_engine.py` - 排序引擎
  478 +- `search/searcher.py` - 搜索器(集成排序功能)
  479 +
  480 +---
  481 +
  482 +## 6. 已完成功能总结
  483 +
  484 +### 6.1 配置系统
  485 +- ✅ 字段定义配置(类型、分析器、来源表/列)
  486 +- ✅ 索引域配置(多域查询、多语言映射)
  487 +- ✅ 查询配置(改写词典、翻译配置)
  488 +- ✅ 排序配置(表达式配置)
  489 +- ✅ 配置验证(字段存在性、类型检查、分析器匹配)
  490 +
  491 +### 6.2 数据索引
  492 +- ✅ 数据转换(字段映射、类型转换)
  493 +- ✅ 向量生成(文本向量、图片向量)
  494 +- ✅ 向量缓存(避免重复计算)
  495 +- ✅ 批量索引(错误处理、重试机制)
  496 +- ✅ ES mapping 自动生成
  497 +
  498 +### 6.3 查询处理
  499 +- ✅ 查询改写(词典配置)
  500 +- ✅ 语言检测
  501 +- ✅ 多语言翻译(DeepL API)
  502 +- ✅ 文本向量化(BGE-M3)
  503 +- ✅ 域提取(支持 `domain:query` 语法)
  504 +
  505 +### 6.4 搜索功能
  506 +- ✅ 布尔表达式解析(AND, OR, RANK, ANDNOT, 括号)
  507 +- ✅ 多语言查询构建(语言路由、字段映射)
  508 +- ✅ 语义搜索(KNN 检索)
  509 +- ✅ 相关性排序(BM25 + 向量相似度)
  510 +- ✅ 结果聚合(Faceted Search)
  511 +
  512 +### 6.5 API 服务
  513 +- ✅ RESTful API(FastAPI)
  514 +- ✅ 搜索接口(文本搜索、图片搜索)
  515 +- ✅ 文档查询接口
  516 +- ✅ 前端界面(HTML + JavaScript)
  517 +- ✅ 租户隔离(tenant_id过滤)
  518 +
  519 +### 6.6 Base配置(店匠通用)
  520 +- ✅ SPU级别索引结构
  521 +- ✅ 嵌套skus字段
  522 +- ✅ 统一索引(search_products)
  523 +- ✅ 租户隔离(tenant_id)
  524 +- ✅ 配置简化(移除MySQL相关配置)
  525 +
  526 +---
  527 +
  528 +## 7. 技术栈
  529 +
  530 +- **后端**:Python 3.6+
  531 +- **搜索引擎**:Elasticsearch
  532 +- **数据库**:MySQL(Shoplazza)
  533 +- **向量模型**:BGE-M3(文本)、CN-CLIP(图片)
  534 +- **翻译服务**:DeepL API
  535 +- **API 框架**:FastAPI
  536 +- **前端**:HTML + JavaScript
  537 +
  538 +---
  539 +
  540 +## 8. API响应格式
  541 +
  542 +### 8.1 外部友好格式
  543 +
  544 +API返回格式不包含ES内部字段(`_id`, `_score`, `_source`),使用外部友好的格式:
  545 +
  546 +**响应结构**:
  547 +```json
  548 +{
  549 + "results": [
  550 + {
  551 + "spu_id": "123",
  552 + "title": "蓝牙耳机",
  553 + "skus": [
  554 + {
  555 + "sku_id": "456",
  556 + "price": 199.99,
  557 + "sku": "SKU-123-1",
  558 + "stock": 50
  559 + }
  560 + ],
  561 + "relevance_score": 0.95
  562 + }
  563 + ],
  564 + "total": 10,
  565 + "facets": [...],
  566 + "suggestions": [],
  567 + "related_searches": []
  568 +}
  569 +```
  570 +
  571 +**主要变化**:
  572 +- 结构化结果(`SpuResult`和`SkuResult`)
  573 +- 嵌套skus数组
  574 +- 无ES内部字段
  575 +
  576 +### 8.2 租户隔离
  577 +
  578 +所有API请求必须提供`tenant_id`:
  579 +- 请求头:`X-Tenant-ID: 1`
  580 +- 或查询参数:`?tenant_id=1`
  581 +
  582 +搜索时自动添加`tenant_id`过滤,确保数据隔离。
  583 +
  584 +### 8.3 数据接口约定
  585 +
  586 +**统一的数据约定格式**:所有API接口使用 Pydantic 模型进行数据验证和序列化。
  587 +
  588 +#### 8.3.1 数据流模式
  589 +
  590 +系统采用统一的数据流模式,确保数据在各层之间的一致性:
  591 +
  592 +**数据流转路径**:
  593 +```
  594 +API Request (JSON)
  595 + ↓
  596 +Pydantic 验证 → 结构化模型(RangeFilter, FacetConfig 等)
  597 + ↓
  598 +Searcher(透传)
  599 + ↓
  600 +ES Query Builder → model_dump() 转换为字典
  601 + ↓
  602 +ES Query (字典)
  603 + ↓
  604 +Elasticsearch
  605 +```
  606 +
  607 +#### 8.3.2 Facets 配置数据流
  608 +
  609 +**输入格式**:`List[Union[str, FacetConfig]]`
  610 +
  611 +- **简单模式**:字符串列表(字段名),使用默认配置
  612 + ```json
  613 + ["categoryName_keyword", "brandName_keyword"]
  614 + ```
  615 +
  616 +- **高级模式**:FacetConfig 对象列表,支持自定义配置
  617 + ```json
  618 + [
  619 + {
  620 + "field": "categoryName_keyword",
  621 + "size": 15,
  622 + "type": "terms"
  623 + },
  624 + {
  625 + "field": "price",
  626 + "type": "range",
  627 + "ranges": [
  628 + {"key": "0-50", "to": 50},
  629 + {"key": "50-100", "from": 50, "to": 100}
  630 + ]
  631 + }
  632 + ]
  633 + ```
  634 +
  635 +**数据流**:
  636 +1. API 层:接收 `List[Union[str, FacetConfig]]`
  637 +2. Searcher 层:透传,不做转换
  638 +3. ES Query Builder:只接受 `str` 或 `FacetConfig`,自动处理两种格式
  639 +4. 输出:转换为 ES 聚合查询
  640 +
  641 +#### 8.3.3 Range Filters 数据流
  642 +
  643 +**输入格式**:`Dict[str, RangeFilter]`
  644 +
  645 +**RangeFilter 模型**:
  646 +```python
  647 +class RangeFilter(BaseModel):
  648 + gte: Optional[Union[float, str]] # 大于等于
  649 + gt: Optional[Union[float, str]] # 大于
  650 + lte: Optional[Union[float, str]] # 小于等于
  651 + lt: Optional[Union[float, str]] # 小于
  652 +```
  653 +
  654 +**示例**:
  655 +```json
  656 +{
  657 + "price": {"gte": 50, "lte": 200},
  658 + "created_at": {"gte": "2023-01-01T00:00:00Z"}
  659 +}
  660 +```
  661 +
  662 +**数据流**:
  663 +1. API 层:接收 `Dict[str, RangeFilter]`,Pydantic 自动验证
  664 +2. Searcher 层:透传 `Dict[str, RangeFilter]`
  665 +3. ES Query Builder:调用 `range_filter.model_dump()` 转换为字典
  666 +4. 输出:ES range 查询(支持数值和日期)
  667 +
  668 +**特性**:
  669 +- 自动验证:确保至少指定一个边界值(gte, gt, lte, lt)
  670 +- 类型支持:支持数值(float)和日期时间字符串(ISO 格式)
  671 +- 统一约定:所有范围过滤都使用 RangeFilter 模型
  672 +
  673 +#### 8.3.4 响应 Facets 数据流
  674 +
  675 +**输出格式**:`List[FacetResult]`
  676 +
  677 +**FacetResult 模型**:
  678 +```python
  679 +class FacetResult(BaseModel):
  680 + field: str # 字段名
  681 + label: str # 显示标签
  682 + type: Literal["terms", "range"] # 分面类型
  683 + values: List[FacetValue] # 分面值列表
  684 + total_count: Optional[int] # 总文档数
  685 +```
  686 +
  687 +**数据流**:
  688 +1. ES Response:返回聚合结果(字典格式)
  689 +2. Searcher 层:构建 `List[FacetResult]` 对象
  690 +3. API 层:直接返回 `List[FacetResult]`(Pydantic 自动序列化为 JSON)
  691 +
  692 +**优势**:
  693 +- 类型安全:使用 Pydantic 模型确保数据结构一致性
  694 +- 自动序列化:模型自动转换为 JSON,无需手动处理
  695 +- 统一约定:所有响应都使用标准化的 Pydantic 模型
  696 +
  697 +#### 8.3.5 统一约定的好处
  698 +
  699 +1. **类型安全**:使用 Pydantic 模型提供运行时类型检查和验证
  700 +2. **代码一致性**:所有层使用相同的数据模型,减少转换错误
  701 +3. **自动文档**:FastAPI 自动生成 API 文档(基于 Pydantic 模型)
  702 +4. **易于维护**:修改数据结构只需更新模型定义
  703 +5. **数据验证**:自动验证输入数据,减少错误处理代码
  704 +
  705 +**实现模块**:
  706 +- `api/models.py` - 所有 Pydantic 模型定义
  707 +- `api/result_formatter.py` - 结果格式化器(ES 响应 → Pydantic 模型)
  708 +- `search/es_query_builder.py` - ES 查询构建器(Pydantic 模型 → ES 查询)
  709 +
  710 +## 9. 配置文件示例
  711 +
  712 +**Base配置**(店匠通用):`config/schema/base/config.yaml`
  713 +
  714 +**其他客户配置**:`config/schema/tenant1/config.yaml`
  715 +
  716 +---
frontend/README.md
@@ -105,7 +105,7 @@ POST http://120.76.41.98:6002/search/ @@ -105,7 +105,7 @@ POST http://120.76.41.98:6002/search/
105 "size": 20, // 每页结果数 105 "size": 20, // 每页结果数
106 "from": 0, // 偏移量(分页) 106 "from": 0, // 偏移量(分页)
107 "filters": { // 筛选条件 107 "filters": { // 筛选条件
108 - "categoryName_keyword": ["玩具"], 108 + "category.keyword": ["玩具"],
109 "price": {"from": 50, "to": 100} 109 "price": {"from": 50, "to": 100}
110 }, 110 },
111 "aggregations": {...}, // 聚合配置 111 "aggregations": {...}, // 聚合配置
frontend/static/js/app.js
@@ -74,17 +74,17 @@ async function performSearch(page = 1) { @@ -74,17 +74,17 @@ async function performSearch(page = 1) {
74 // Define facets (简化配置) 74 // Define facets (简化配置)
75 const facets = [ 75 const facets = [
76 { 76 {
77 - "field": "category_keyword", 77 + "field": "category.keyword",
78 "size": 15, 78 "size": 15,
79 "type": "terms" 79 "type": "terms"
80 }, 80 },
81 { 81 {
82 - "field": "vendor_keyword", 82 + "field": "vendor.keyword",
83 "size": 15, 83 "size": 15,
84 "type": "terms" 84 "type": "terms"
85 }, 85 },
86 { 86 {
87 - "field": "tags_keyword", 87 + "field": "tags.keyword",
88 "size": 10, 88 "size": 10,
89 "type": "terms" 89 "type": "terms"
90 }, 90 },
@@ -219,13 +219,13 @@ function displayFacets(facets) { @@ -219,13 +219,13 @@ function displayFacets(facets) {
219 let containerId = null; 219 let containerId = null;
220 let maxDisplay = 10; 220 let maxDisplay = 10;
221 221
222 - if (facet.field === 'category_keyword') { 222 + if (facet.field === 'category.keyword') {
223 containerId = 'categoryTags'; 223 containerId = 'categoryTags';
224 maxDisplay = 10; 224 maxDisplay = 10;
225 - } else if (facet.field === 'vendor_keyword') { 225 + } else if (facet.field === 'vendor.keyword') {
226 containerId = 'brandTags'; 226 containerId = 'brandTags';
227 maxDisplay = 10; 227 maxDisplay = 10;
228 - } else if (facet.field === 'tags_keyword') { 228 + } else if (facet.field === 'tags.keyword') {
229 containerId = 'supplierTags'; 229 containerId = 'supplierTags';
230 maxDisplay = 8; 230 maxDisplay = 8;
231 } 231 }
@@ -351,7 +351,7 @@ function updateClearFiltersButton() { @@ -351,7 +351,7 @@ function updateClearFiltersButton() {
351 351
352 // Update product count 352 // Update product count
353 function updateProductCount(total) { 353 function updateProductCount(total) {
354 - document.getElementById('productCount').textContent = `${total.toLocaleString()} products found`; 354 + document.getElementById('productCount').textContent = `${total.toLocaleString()} SPUs found`;
355 } 355 }
356 356
357 // Sort functions 357 // Sort functions
frontend/static/js/app_base.js
@@ -72,17 +72,17 @@ async function performSearch(page = 1) { @@ -72,17 +72,17 @@ async function performSearch(page = 1) {
72 // Define facets (简化配置) 72 // Define facets (简化配置)
73 const facets = [ 73 const facets = [
74 { 74 {
75 - "field": "category_keyword", 75 + "field": "category.keyword",
76 "size": 15, 76 "size": 15,
77 "type": "terms" 77 "type": "terms"
78 }, 78 },
79 { 79 {
80 - "field": "vendor_keyword", 80 + "field": "vendor.keyword",
81 "size": 15, 81 "size": 15,
82 "type": "terms" 82 "type": "terms"
83 }, 83 },
84 { 84 {
85 - "field": "tags_keyword", 85 + "field": "tags.keyword",
86 "size": 10, 86 "size": 10,
87 "type": "terms" 87 "type": "terms"
88 }, 88 },
@@ -168,14 +168,14 @@ function displayResults(data) { @@ -168,14 +168,14 @@ function displayResults(data) {
168 168
169 let html = ''; 169 let html = '';
170 170
171 - data.results.forEach((product) => {  
172 - const score = product.relevance_score; 171 + data.results.forEach((spu) => {
  172 + const score = spu.relevance_score;
173 html += ` 173 html += `
174 <div class="product-card"> 174 <div class="product-card">
175 <div class="product-image-wrapper"> 175 <div class="product-image-wrapper">
176 - ${product.image_url ? `  
177 - <img src="${escapeHtml(product.image_url)}"  
178 - alt="${escapeHtml(product.title)}" 176 + ${spu.image_url ? `
  177 + <img src="${escapeHtml(spu.image_url)}"
  178 + alt="${escapeHtml(spu.title)}"
179 class="product-image" 179 class="product-image"
180 onerror="this.src='data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%22100%22 height=%22100%22%3E%3Crect fill=%22%23f0f0f0%22 width=%22100%22 height=%22100%22/%3E%3Ctext x=%2250%25%22 y=%2250%25%22 font-size=%2214%22 text-anchor=%22middle%22 dy=%22.3em%22 fill=%22%23999%22%3ENo Image%3C/text%3E%3C/svg%3E'"> 180 onerror="this.src='data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%22100%22 height=%22100%22%3E%3Crect fill=%22%23f0f0f0%22 width=%22100%22 height=%22100%22/%3E%3Ctext x=%2250%25%22 y=%2250%25%22 font-size=%2214%22 text-anchor=%22middle%22 dy=%22.3em%22 fill=%22%23999%22%3ENo Image%3C/text%3E%3C/svg%3E'">
181 ` : ` 181 ` : `
@@ -184,21 +184,21 @@ function displayResults(data) { @@ -184,21 +184,21 @@ function displayResults(data) {
184 </div> 184 </div>
185 185
186 <div class="product-price"> 186 <div class="product-price">
187 - ${product.price ? `$${product.price.toFixed(2)}` : 'N/A'}${product.compare_at_price && product.compare_at_price > product.price ? `<span style="text-decoration: line-through; color: #999; font-size: 0.9em; margin-left: 8px;">$${product.compare_at_price.toFixed(2)}</span>` : ''} 187 + ${spu.price ? `$${spu.price.toFixed(2)}` : 'N/A'}${spu.compare_at_price && spu.compare_at_price > spu.price ? `<span style="text-decoration: line-through; color: #999; font-size: 0.9em; margin-left: 8px;">$${spu.compare_at_price.toFixed(2)}</span>` : ''}
188 </div> 188 </div>
189 189
190 <div class="product-stock"> 190 <div class="product-stock">
191 - ${product.in_stock ? '<span style="color: green;">In Stock</span>' : '<span style="color: red;">Out of Stock</span>'}  
192 - ${product.variants && product.variants.length > 0 ? `<span style="color: #666; font-size: 0.9em;">(${product.variants.length} variants)</span>` : ''} 191 + ${spu.in_stock ? '<span style="color: green;">In Stock</span>' : '<span style="color: red;">Out of Stock</span>'}
  192 + ${spu.skus && spu.skus.length > 0 ? `<span style="color: #666; font-size: 0.9em;">(${spu.skus.length} skus)</span>` : ''}
193 </div> 193 </div>
194 194
195 <div class="product-title"> 195 <div class="product-title">
196 - ${escapeHtml(product.title || 'N/A')} 196 + ${escapeHtml(spu.title || 'N/A')}
197 </div> 197 </div>
198 198
199 - <div class="product-meta">${product.vendor ? escapeHtml(product.vendor) : ''}${product.product_type ? ' | ' + escapeHtml(product.product_type) : ''}${product.category ? ' | ' + escapeHtml(product.category) : ''} ${product.tags ? ` 199 + <div class="product-meta">${spu.vendor ? escapeHtml(spu.vendor) : ''}${spu.category ? ' | ' + escapeHtml(spu.category) : ''} ${spu.tags ? `
200 <div class="product-tags"> 200 <div class="product-tags">
201 - Tags: ${escapeHtml(product.tags)} 201 + Tags: ${escapeHtml(spu.tags)}
202 </div> 202 </div>
203 ` : ''} 203 ` : ''}
204 </div> 204 </div>
@@ -217,13 +217,13 @@ function displayFacets(facets) { @@ -217,13 +217,13 @@ function displayFacets(facets) {
217 let containerId = null; 217 let containerId = null;
218 let maxDisplay = 10; 218 let maxDisplay = 10;
219 219
220 - if (facet.field === 'category_keyword') { 220 + if (facet.field === 'category.keyword') {
221 containerId = 'categoryTags'; 221 containerId = 'categoryTags';
222 maxDisplay = 10; 222 maxDisplay = 10;
223 - } else if (facet.field === 'vendor_keyword') { 223 + } else if (facet.field === 'vendor.keyword') {
224 containerId = 'brandTags'; 224 containerId = 'brandTags';
225 maxDisplay = 10; 225 maxDisplay = 10;
226 - } else if (facet.field === 'tags_keyword') { 226 + } else if (facet.field === 'tags.keyword') {
227 containerId = 'supplierTags'; 227 containerId = 'supplierTags';
228 maxDisplay = 8; 228 maxDisplay = 8;
229 } 229 }
@@ -349,7 +349,7 @@ function updateClearFiltersButton() { @@ -349,7 +349,7 @@ function updateClearFiltersButton() {
349 349
350 // Update product count 350 // Update product count
351 function updateProductCount(total) { 351 function updateProductCount(total) {
352 - document.getElementById('productCount').textContent = `${total.toLocaleString()} products found`; 352 + document.getElementById('productCount').textContent = `${total.toLocaleString()} SPUs found`;
353 } 353 }
354 354
355 // Sort functions 355 // Sort functions
indexer/spu_transformer.py
1 """ 1 """
2 SPU data transformer for Shoplazza products. 2 SPU data transformer for Shoplazza products.
3 3
4 -Transforms SPU and SKU data from MySQL into SPU-level ES documents with nested variants. 4 +Transforms SPU and SKU data from MySQL into SPU-level ES documents with nested skus.
5 """ 5 """
6 6
7 import pandas as pd 7 import pandas as pd
@@ -165,8 +165,8 @@ class SPUTransformer: @@ -165,8 +165,8 @@ class SPUTransformer:
165 # Tenant ID (required) 165 # Tenant ID (required)
166 doc['tenant_id'] = str(self.tenant_id) 166 doc['tenant_id'] = str(self.tenant_id)
167 167
168 - # Product ID  
169 - doc['product_id'] = str(spu_row['id']) 168 + # SPU ID
  169 + doc['spu_id'] = str(spu_row['id'])
170 170
171 # Handle 171 # Handle
172 if pd.notna(spu_row.get('handle')): 172 if pd.notna(spu_row.get('handle')):
@@ -195,23 +195,14 @@ class SPUTransformer: @@ -195,23 +195,14 @@ class SPUTransformer:
195 # Vendor 195 # Vendor
196 if pd.notna(spu_row.get('vendor')): 196 if pd.notna(spu_row.get('vendor')):
197 doc['vendor'] = str(spu_row['vendor']) 197 doc['vendor'] = str(spu_row['vendor'])
198 - doc['vendor_keyword'] = str(spu_row['vendor'])  
199 -  
200 - # Product type (from category or tags)  
201 - if pd.notna(spu_row.get('category')):  
202 - doc['product_type'] = str(spu_row['category'])  
203 - doc['product_type_keyword'] = str(spu_row['category'])  
204 198
205 # Tags 199 # Tags
206 if pd.notna(spu_row.get('tags')): 200 if pd.notna(spu_row.get('tags')):
207 - tags_str = str(spu_row['tags'])  
208 - doc['tags'] = tags_str  
209 - doc['tags_keyword'] = tags_str 201 + doc['tags'] = str(spu_row['tags'])
210 202
211 # Category 203 # Category
212 if pd.notna(spu_row.get('category')): 204 if pd.notna(spu_row.get('category')):
213 doc['category'] = str(spu_row['category']) 205 doc['category'] = str(spu_row['category'])
214 - doc['category_keyword'] = str(spu_row['category'])  
215 206
216 # Image URL 207 # Image URL
217 if pd.notna(spu_row.get('image_src')): 208 if pd.notna(spu_row.get('image_src')):
@@ -220,27 +211,27 @@ class SPUTransformer: @@ -220,27 +211,27 @@ class SPUTransformer:
220 image_src = f"//{image_src}" if image_src.startswith('//') else image_src 211 image_src = f"//{image_src}" if image_src.startswith('//') else image_src
221 doc['image_url'] = image_src 212 doc['image_url'] = image_src
222 213
223 - # Process variants  
224 - variants = [] 214 + # Process SKUs
  215 + skus_list = []
225 prices = [] 216 prices = []
226 compare_prices = [] 217 compare_prices = []
227 218
228 for _, sku_row in skus.iterrows(): 219 for _, sku_row in skus.iterrows():
229 - variant = self._transform_sku_to_variant(sku_row)  
230 - if variant:  
231 - variants.append(variant)  
232 - if 'price' in variant and variant['price'] is not None: 220 + sku_data = self._transform_sku_row(sku_row)
  221 + if sku_data:
  222 + skus_list.append(sku_data)
  223 + if 'price' in sku_data and sku_data['price'] is not None:
233 try: 224 try:
234 - prices.append(float(variant['price'])) 225 + prices.append(float(sku_data['price']))
235 except (ValueError, TypeError): 226 except (ValueError, TypeError):
236 pass 227 pass
237 - if 'compare_at_price' in variant and variant['compare_at_price'] is not None: 228 + if 'compare_at_price' in sku_data and sku_data['compare_at_price'] is not None:
238 try: 229 try:
239 - compare_prices.append(float(variant['compare_at_price'])) 230 + compare_prices.append(float(sku_data['compare_at_price']))
240 except (ValueError, TypeError): 231 except (ValueError, TypeError):
241 pass 232 pass
242 233
243 - doc['variants'] = variants 234 + doc['skus'] = skus_list
244 235
245 # Calculate price ranges 236 # Calculate price ranges
246 if prices: 237 if prices:
@@ -286,55 +277,55 @@ class SPUTransformer: @@ -286,55 +277,55 @@ class SPUTransformer:
286 277
287 return doc 278 return doc
288 279
289 - def _transform_sku_to_variant(self, sku_row: pd.Series) -> Optional[Dict[str, Any]]: 280 + def _transform_sku_row(self, sku_row: pd.Series) -> Optional[Dict[str, Any]]:
290 """ 281 """
291 - Transform a SKU row into a variant object. 282 + Transform a SKU row into a SKU object.
292 283
293 Args: 284 Args:
294 sku_row: SKU row from database 285 sku_row: SKU row from database
295 286
296 Returns: 287 Returns:
297 - Variant dictionary or None 288 + SKU dictionary or None
298 """ 289 """
299 - variant = {} 290 + sku_data = {}
300 291
301 - # Variant ID  
302 - variant['variant_id'] = str(sku_row['id']) 292 + # SKU ID
  293 + sku_data['sku_id'] = str(sku_row['id'])
303 294
304 # Title 295 # Title
305 if pd.notna(sku_row.get('title')): 296 if pd.notna(sku_row.get('title')):
306 - variant['title'] = str(sku_row['title']) 297 + sku_data['title'] = str(sku_row['title'])
307 298
308 # Price 299 # Price
309 if pd.notna(sku_row.get('price')): 300 if pd.notna(sku_row.get('price')):
310 try: 301 try:
311 - variant['price'] = float(sku_row['price']) 302 + sku_data['price'] = float(sku_row['price'])
312 except (ValueError, TypeError): 303 except (ValueError, TypeError):
313 - variant['price'] = None 304 + sku_data['price'] = None
314 else: 305 else:
315 - variant['price'] = None 306 + sku_data['price'] = None
316 307
317 # Compare at price 308 # Compare at price
318 if pd.notna(sku_row.get('compare_at_price')): 309 if pd.notna(sku_row.get('compare_at_price')):
319 try: 310 try:
320 - variant['compare_at_price'] = float(sku_row['compare_at_price']) 311 + sku_data['compare_at_price'] = float(sku_row['compare_at_price'])
321 except (ValueError, TypeError): 312 except (ValueError, TypeError):
322 - variant['compare_at_price'] = None 313 + sku_data['compare_at_price'] = None
323 else: 314 else:
324 - variant['compare_at_price'] = None 315 + sku_data['compare_at_price'] = None
325 316
326 # SKU 317 # SKU
327 if pd.notna(sku_row.get('sku')): 318 if pd.notna(sku_row.get('sku')):
328 - variant['sku'] = str(sku_row['sku']) 319 + sku_data['sku'] = str(sku_row['sku'])
329 320
330 # Stock 321 # Stock
331 if pd.notna(sku_row.get('inventory_quantity')): 322 if pd.notna(sku_row.get('inventory_quantity')):
332 try: 323 try:
333 - variant['stock'] = int(sku_row['inventory_quantity']) 324 + sku_data['stock'] = int(sku_row['inventory_quantity'])
334 except (ValueError, TypeError): 325 except (ValueError, TypeError):
335 - variant['stock'] = 0 326 + sku_data['stock'] = 0
336 else: 327 else:
337 - variant['stock'] = 0 328 + sku_data['stock'] = 0
338 329
339 # Options (from option1, option2, option3) 330 # Options (from option1, option2, option3)
340 options = {} 331 options = {}
@@ -346,7 +337,7 @@ class SPUTransformer: @@ -346,7 +337,7 @@ class SPUTransformer:
346 options['option3'] = str(sku_row['option3']) 337 options['option3'] = str(sku_row['option3'])
347 338
348 if options: 339 if options:
349 - variant['options'] = options 340 + sku_data['options'] = options
350 341
351 - return variant 342 + return sku_data
352 343
scripts/generate_test_data.py
@@ -2,7 +2,7 @@ @@ -2,7 +2,7 @@
2 """ 2 """
3 Generate test data for Shoplazza SPU and SKU tables. 3 Generate test data for Shoplazza SPU and SKU tables.
4 4
5 -Generates 100 SPU records with 1-5 SKU variants each. 5 +Generates 100 SPU records with 1-5 SKUs each.
6 """ 6 """
7 7
8 import sys 8 import sys
@@ -145,15 +145,15 @@ def generate_sku_data(spus: list, start_sku_id: int = 1): @@ -145,15 +145,15 @@ def generate_sku_data(spus: list, start_sku_id: int = 1):
145 145
146 for spu in spus: 146 for spu in spus:
147 spu_id = spu['id'] 147 spu_id = spu['id']
148 - num_variants = random.randint(1, 5) 148 + num_skus = random.randint(1, 5)
149 149
150 # Base price 150 # Base price
151 base_price = random.uniform(50, 500) 151 base_price = random.uniform(50, 500)
152 152
153 - for i in range(num_variants): 153 + for i in range(num_skus):
154 # Generate variant options 154 # Generate variant options
155 - color = random.choice(colors) if num_variants > 1 else None  
156 - size = random.choice(sizes) if num_variants > 2 else None 155 + color = random.choice(colors) if num_skus > 1 else None
  156 + size = random.choice(sizes) if num_skus > 2 else None
157 157
158 # Generate title 158 # Generate title
159 title_parts = [] 159 title_parts = []
@@ -400,7 +400,7 @@ def main(): @@ -400,7 +400,7 @@ def main():
400 start_sku_id = 1 400 start_sku_id = 1
401 print(f"Using start SPU ID: {start_spu_id}, start SKU ID: {start_sku_id}") 401 print(f"Using start SPU ID: {start_spu_id}, start SKU ID: {start_sku_id}")
402 402
403 - print(f"Generating {args.num_spus} SPUs with variants...") 403 + print(f"Generating {args.num_spus} SPUs with skus...")
404 404
405 # Generate SPU data 405 # Generate SPU data
406 spus = generate_spu_data(args.num_spus, args.tenant_id, start_spu_id) 406 spus = generate_spu_data(args.num_spus, args.tenant_id, start_spu_id)
scripts/ingest_shoplazza.py
@@ -141,7 +141,7 @@ def main(): @@ -141,7 +141,7 @@ def main():
141 indexer = BulkIndexer(es_client, index_name, batch_size=args.batch_size) 141 indexer = BulkIndexer(es_client, index_name, batch_size=args.batch_size)
142 142
143 try: 143 try:
144 - results = indexer.index_documents(documents, id_field="product_id", show_progress=True) 144 + results = indexer.index_documents(documents, id_field="spu_id", show_progress=True)
145 print(f"\nIngestion complete:") 145 print(f"\nIngestion complete:")
146 print(f" Success: {results['success']}") 146 print(f" Success: {results['success']}")
147 print(f" Failed: {results['failed']}") 147 print(f" Failed: {results['failed']}")
scripts/test_base.py
@@ -85,7 +85,7 @@ def validate_response_format(data: dict): @@ -85,7 +85,7 @@ def validate_response_format(data: dict):
85 # Validate first result structure 85 # Validate first result structure
86 if results: 86 if results:
87 result = results[0] 87 result = results[0]
88 - required_fields = ['product_id', 'title', 'variants', 'relevance_score'] 88 + required_fields = ['spu_id', 'title', 'skus', 'relevance_score']
89 for field in required_fields: 89 for field in required_fields:
90 if field not in result: 90 if field not in result:
91 errors.append(f"Result missing required field: {field}") 91 errors.append(f"Result missing required field: {field}")
@@ -96,17 +96,17 @@ def validate_response_format(data: dict): @@ -96,17 +96,17 @@ def validate_response_format(data: dict):
96 if field in result: 96 if field in result:
97 errors.append(f"Result contains ES internal field: {field}") 97 errors.append(f"Result contains ES internal field: {field}")
98 98
99 - # Validate variants  
100 - if 'variants' in result:  
101 - variants = result['variants']  
102 - if not isinstance(variants, list):  
103 - errors.append("'variants' should be a list")  
104 - elif variants:  
105 - variant = variants[0]  
106 - variant_required = ['variant_id', 'price', 'sku', 'stock']  
107 - for field in variant_required:  
108 - if field not in variant:  
109 - errors.append(f"Variant missing required field: {field}") 99 + # Validate skus
  100 + if 'skus' in result:
  101 + skus = result['skus']
  102 + if not isinstance(skus, list):
  103 + errors.append("'skus' should be a list")
  104 + elif skus:
  105 + sku = skus[0]
  106 + sku_required = ['sku_id', 'price', 'sku', 'stock']
  107 + for field in sku_required:
  108 + if field not in sku:
  109 + errors.append(f"SKU missing required field: {field}")
110 110
111 # Check for suggestions and related_searches 111 # Check for suggestions and related_searches
112 if 'suggestions' not in data: 112 if 'suggestions' not in data:
@@ -136,7 +136,7 @@ def test_facets(base_url: str, tenant_id: str): @@ -136,7 +136,7 @@ def test_facets(base_url: str, tenant_id: str):
136 payload = { 136 payload = {
137 "query": "商品", 137 "query": "商品",
138 "size": 10, 138 "size": 10,
139 - "facets": ["category_keyword", "vendor_keyword"] 139 + "facets": ["category.keyword", "vendor.keyword"]
140 } 140 }
141 141
142 print(f"\nTesting facets:") 142 print(f"\nTesting facets:")
@@ -179,8 +179,8 @@ def test_tenant_isolation(base_url: str, tenant_id_1: str, tenant_id_2: str): @@ -179,8 +179,8 @@ def test_tenant_isolation(base_url: str, tenant_id_1: str, tenant_id_2: str):
179 data2 = test_search_api(base_url, tenant_id_2, "商品") 179 data2 = test_search_api(base_url, tenant_id_2, "商品")
180 180
181 if data1 and data2: 181 if data1 and data2:
182 - results1 = set(r.get('product_id') for r in data1.get('results', []))  
183 - results2 = set(r.get('product_id') for r in data2.get('results', [])) 182 + results1 = set(r.get('spu_id') for r in data1.get('results', []))
  183 + results2 = set(r.get('spu_id') for r in data2.get('results', []))
184 184
185 overlap = results1 & results2 185 overlap = results1 & results2
186 if overlap: 186 if overlap:
search/searcher.py
@@ -29,7 +29,7 @@ class SearchResult: @@ -29,7 +29,7 @@ class SearchResult:
29 29
30 def __init__( 30 def __init__(
31 self, 31 self,
32 - results: List[Any], # List[ProductResult] 32 + results: List[Any], # List[SpuResult]
33 total: int, 33 total: int,
34 max_score: float, 34 max_score: float,
35 took_ms: int, 35 took_ms: int,