Commit 362d43b67e9a728680610a07147a27db33adf256

Authored by tangwang
1 parent 4d824a77

店匠体系数据的搜索

... ... @@ -31,3 +31,6 @@ IMAGE_MODEL_DIR=/data/tw/models/cn-clip
31 31  
32 32 # Cache Directory
33 33 CACHE_DIR=.cache
  34 +
  35 +# Frontend API Base URL
  36 +API_BASE_URL=http://120.76.41.98:6002
... ...
config/env_config.py
... ... @@ -37,6 +37,10 @@ CUSTOMER_ID = os.getenv('CUSTOMER_ID', 'customer1')
37 37 # API Service Configuration
38 38 API_HOST = os.getenv('API_HOST', '0.0.0.0')
39 39 API_PORT = int(os.getenv('API_PORT', 6002))
  40 +# API_BASE_URL: 如果未设置,根据API_HOST构建(0.0.0.0使用localhost)
  41 +API_BASE_URL = os.getenv('API_BASE_URL')
  42 +if not API_BASE_URL:
  43 + API_BASE_URL = f'http://localhost:{API_PORT}' if API_HOST == '0.0.0.0' else f'http://{API_HOST}:{API_PORT}'
40 44  
41 45 # Model Directories
42 46 TEXT_MODEL_DIR = os.getenv('TEXT_MODEL_DIR', '/data/tw/models/bge-m3')
... ...
config/schema/base/config.yaml deleted
... ... @@ -1,271 +0,0 @@
1   -# Base Configuration for Shoplazza
2   -# 店匠通用配置文件,所有使用店匠表的客户共用
3   -# 注意:此配置不包含MySQL相关配置,只包含ES搜索相关配置
4   -
5   -customer_name: "Shoplazza Base Configuration"
6   -
7   -# Elasticsearch Index
8   -es_index_name: "search_products"
9   -
10   -# ES Index Settings
11   -es_settings:
12   - number_of_shards: 1
13   - number_of_replicas: 0
14   - refresh_interval: "30s"
15   -
16   -# Field Definitions (SPU级别,只包含对搜索有帮助的字段)
17   -fields:
18   - # 租户隔离字段(必需)
19   - - name: "tenant_id"
20   - type: "KEYWORD"
21   - required: true
22   - index: true
23   - store: true
24   -
25   - # 商品标识字段
26   - - name: "product_id"
27   - type: "KEYWORD"
28   - required: true
29   - index: true
30   - store: true
31   -
32   - - name: "handle"
33   - type: "KEYWORD"
34   - index: true
35   - store: true
36   -
37   - # 文本搜索字段
38   - - name: "title"
39   - type: "TEXT"
40   - analyzer: "chinese_ecommerce"
41   - boost: 3.0
42   - index: true
43   - store: true
44   -
45   - - name: "brief"
46   - type: "TEXT"
47   - analyzer: "chinese_ecommerce"
48   - boost: 1.5
49   - index: true
50   - store: true
51   -
52   - - name: "description"
53   - type: "TEXT"
54   - analyzer: "chinese_ecommerce"
55   - boost: 1.0
56   - index: true
57   - store: true
58   -
59   - # SEO字段(提升相关性)
60   - - name: "seo_title"
61   - type: "TEXT"
62   - analyzer: "chinese_ecommerce"
63   - boost: 2.0
64   - index: true
65   - store: true
66   -
67   - - name: "seo_description"
68   - type: "TEXT"
69   - analyzer: "chinese_ecommerce"
70   - boost: 1.5
71   - index: true
72   - store: true
73   -
74   - - name: "seo_keywords"
75   - type: "TEXT"
76   - analyzer: "chinese_ecommerce"
77   - boost: 2.0
78   - index: true
79   - store: true
80   -
81   - # 分类和标签字段(TEXT + KEYWORD双重索引)
82   - - name: "vendor"
83   - type: "TEXT"
84   - analyzer: "chinese_ecommerce"
85   - boost: 1.5
86   - index: true
87   - store: true
88   -
89   - - name: "vendor_keyword"
90   - type: "KEYWORD"
91   - index: true
92   - store: false
93   -
94   - - name: "product_type"
95   - type: "TEXT"
96   - analyzer: "chinese_ecommerce"
97   - boost: 1.5
98   - index: true
99   - store: true
100   -
101   - - name: "product_type_keyword"
102   - type: "KEYWORD"
103   - index: true
104   - store: false
105   -
106   - - name: "tags"
107   - type: "TEXT"
108   - analyzer: "chinese_ecommerce"
109   - boost: 1.0
110   - index: true
111   - store: true
112   -
113   - - name: "tags_keyword"
114   - type: "KEYWORD"
115   - index: true
116   - store: false
117   -
118   - - name: "category"
119   - type: "TEXT"
120   - analyzer: "chinese_ecommerce"
121   - boost: 1.5
122   - index: true
123   - store: true
124   -
125   - - name: "category_keyword"
126   - type: "KEYWORD"
127   - index: true
128   - store: false
129   -
130   - # 价格字段(扁平化)
131   - - name: "min_price"
132   - type: "FLOAT"
133   - index: true
134   - store: true
135   -
136   - - name: "max_price"
137   - type: "FLOAT"
138   - index: true
139   - store: true
140   -
141   - - name: "compare_at_price"
142   - type: "FLOAT"
143   - index: true
144   - store: true
145   -
146   - # 图片字段(用于显示,不参与搜索)
147   - - name: "image_url"
148   - type: "KEYWORD"
149   - index: false
150   - store: true
151   -
152   - # 嵌套variants字段
153   - - name: "variants"
154   - type: "JSON"
155   - nested: true
156   - nested_properties:
157   - variant_id:
158   - type: "keyword"
159   - index: true
160   - store: true
161   - title:
162   - type: "text"
163   - analyzer: "chinese_ecommerce"
164   - index: true
165   - store: true
166   - price:
167   - type: "float"
168   - index: true
169   - store: true
170   - compare_at_price:
171   - type: "float"
172   - index: true
173   - store: true
174   - sku:
175   - type: "keyword"
176   - index: true
177   - store: true
178   - stock:
179   - type: "long"
180   - index: true
181   - store: true
182   - options:
183   - type: "object"
184   - enabled: true
185   -
186   -# Index Structure (Query Domains)
187   -indexes:
188   - - name: "default"
189   - label: "默认索引"
190   - fields:
191   - - "title"
192   - - "brief"
193   - - "description"
194   - - "seo_title"
195   - - "seo_description"
196   - - "seo_keywords"
197   - - "vendor"
198   - - "product_type"
199   - - "tags"
200   - - "category"
201   - analyzer: "chinese_ecommerce"
202   - boost: 1.0
203   -
204   - - name: "title"
205   - label: "标题索引"
206   - fields:
207   - - "title"
208   - - "seo_title"
209   - analyzer: "chinese_ecommerce"
210   - boost: 2.0
211   -
212   - - name: "vendor"
213   - label: "品牌索引"
214   - fields:
215   - - "vendor"
216   - analyzer: "chinese_ecommerce"
217   - boost: 1.5
218   -
219   - - name: "category"
220   - label: "类目索引"
221   - fields:
222   - - "category"
223   - analyzer: "chinese_ecommerce"
224   - boost: 1.5
225   -
226   - - name: "tags"
227   - label: "标签索引"
228   - fields:
229   - - "tags"
230   - - "seo_keywords"
231   - analyzer: "chinese_ecommerce"
232   - boost: 1.0
233   -
234   -# Query Configuration
235   -query_config:
236   - supported_languages:
237   - - "zh"
238   - - "en"
239   - default_language: "zh"
240   - enable_translation: true
241   - enable_text_embedding: true
242   - enable_query_rewrite: true
243   -
244   - # Translation API (DeepL)
245   - translation_service: "deepl"
246   - translation_api_key: null # Set via environment variable
247   -
248   -# Ranking Configuration
249   -ranking:
250   - expression: "bm25() + 0.2*text_embedding_relevance()"
251   - description: "BM25 text relevance combined with semantic embedding similarity"
252   -
253   -# Function Score配置(ES层打分规则)
254   -function_score:
255   - score_mode: "sum"
256   - boost_mode: "multiply"
257   -
258   - functions: []
259   -
260   -# Rerank配置(本地重排,当前禁用)
261   -rerank:
262   - enabled: false
263   - expression: ""
264   - description: "Local reranking (disabled, use ES function_score instead)"
265   -
266   -# SPU配置(已启用,使用嵌套variants)
267   -spu_config:
268   - enabled: true
269   - spu_field: "product_id"
270   - inner_hits_size: 10
271   -
config/schema/customer1/config.yaml deleted
... ... @@ -1,363 +0,0 @@
1   -# Customer1 Configuration
2   -# Test customer for cross-border e-commerce search
3   -
4   -customer_name: "Customer1 Test Instance"
5   -
6   -# MySQL Database Configuration
7   -mysql_config:
8   - host: "120.79.247.228"
9   - port: 3316
10   - database: "saas"
11   - username: "saas"
12   - password: "P89cZHS5d7dFyc9R"
13   -
14   -# Table Configuration
15   -main_table: "shoplazza_product_sku"
16   -extension_table: "customer1_extension"
17   -
18   -# Elasticsearch Index
19   -es_index_name: "search_customer1"
20   -
21   -# ES Index Settings
22   -es_settings:
23   - number_of_shards: 1
24   - number_of_replicas: 0
25   - refresh_interval: "30s"
26   -
27   -# Field Definitions
28   -fields:
29   - # Primary Key
30   - - name: "skuId"
31   - type: "LONG"
32   - source_table: "main"
33   - source_column: "id"
34   - required: true
35   - index: true
36   - store: true
37   -
38   - # Text Fields - Multi-language
39   - - name: "name"
40   - type: "TEXT"
41   - source_table: "extension"
42   - source_column: "name"
43   - analyzer: "chinese_ecommerce"
44   - boost: 2.0
45   - index: true
46   - store: true
47   -
48   - - name: "name_pinyin"
49   - type: "TEXT"
50   - source_table: "extension"
51   - source_column: "name_pinyin"
52   - analyzer: "standard"
53   - boost: 1.5
54   - index: true
55   - store: false
56   -
57   - - name: "ruSkuName"
58   - type: "TEXT"
59   - source_table: "extension"
60   - source_column: "ruSkuName"
61   - analyzer: "russian"
62   - boost: 2.0
63   - index: true
64   - store: true
65   -
66   - - name: "enSpuName"
67   - type: "TEXT"
68   - source_table: "extension"
69   - source_column: "enSpuName"
70   - analyzer: "english"
71   - boost: 2.0
72   - index: true
73   - store: true
74   -
75   - # Category and Brand
76   - - name: "categoryName"
77   - type: "TEXT"
78   - source_table: "extension"
79   - source_column: "categoryName"
80   - analyzer: "chinese_ecommerce"
81   - boost: 1.5
82   - index: true
83   - store: true
84   -
85   - - name: "brandName"
86   - type: "TEXT"
87   - source_table: "extension"
88   - source_column: "brandName"
89   - analyzer: "chinese_ecommerce"
90   - boost: 1.5
91   - index: true
92   - store: true
93   -
94   - - name: "supplierName"
95   - type: "TEXT"
96   - source_table: "extension"
97   - source_column: "supplierName"
98   - analyzer: "chinese_ecommerce"
99   - boost: 1.0
100   - index: true
101   - store: true
102   -
103   - # Keyword Fields
104   - - name: "categoryName_keyword"
105   - type: "KEYWORD"
106   - source_table: "extension"
107   - source_column: "categoryName"
108   - index: true
109   - store: false
110   -
111   - - name: "brandName_keyword"
112   - type: "KEYWORD"
113   - source_table: "extension"
114   - source_column: "brandName"
115   - index: true
116   - store: false
117   -
118   - - name: "supplierName_keyword"
119   - type: "KEYWORD"
120   - source_table: "extension"
121   - source_column: "supplierName"
122   - index: true
123   - store: false
124   -
125   - # Price Fields
126   - - name: "price"
127   - type: "DOUBLE"
128   - source_table: "extension"
129   - source_column: "price"
130   - index: true
131   - store: true
132   -
133   - # Text Embedding Fields
134   - - name: "name_embedding"
135   - type: "TEXT_EMBEDDING"
136   - source_table: "extension"
137   - source_column: "name"
138   - embedding_dims: 1024
139   - embedding_similarity: "dot_product"
140   - index: true
141   -
142   - - name: "enSpuName_embedding"
143   - type: "TEXT_EMBEDDING"
144   - source_table: "extension"
145   - source_column: "enSpuName"
146   - embedding_dims: 1024
147   - embedding_similarity: "dot_product"
148   - index: true
149   -
150   - # Image Fields
151   - - name: "imageUrl"
152   - type: "KEYWORD"
153   - source_table: "extension"
154   - source_column: "imageUrl"
155   - index: true
156   - store: true
157   -
158   - - name: "image_embedding"
159   - type: "IMAGE_EMBEDDING"
160   - source_table: "extension"
161   - source_column: "imageUrl"
162   - embedding_dims: 1024
163   - embedding_similarity: "dot_product"
164   - nested: false
165   - index: true
166   -
167   - # Metadata Fields
168   - - name: "create_time"
169   - type: "DATE"
170   - source_table: "extension"
171   - source_column: "create_time"
172   - index: true
173   - store: true
174   -
175   - - name: "days_since_last_update"
176   - type: "INT"
177   - source_table: "extension"
178   - source_column: "days_since_last_update"
179   - index: true
180   - store: true
181   -
182   -# Index Structure (Query Domains)
183   -indexes:
184   - - name: "default"
185   - label: "默认索引"
186   - fields:
187   - - "name"
188   - - "enSpuName"
189   - - "ruSkuName"
190   - - "categoryName"
191   - - "brandName"
192   - analyzer: "chinese_ecommerce"
193   - boost: 1.0
194   - example: 'query=default:"消防套"'
195   - language_field_mapping:
196   - zh:
197   - - "name"
198   - - "categoryName"
199   - - "brandName"
200   - en:
201   - - "enSpuName"
202   - ru:
203   - - "ruSkuName"
204   -
205   - - name: "title"
206   - label: "标题索引"
207   - fields:
208   - - "name"
209   - - "enSpuName"
210   - - "ruSkuName"
211   - analyzer: "chinese_ecommerce"
212   - boost: 2.0
213   - example: 'query=title:"芭比娃娃"'
214   - language_field_mapping:
215   - zh:
216   - - "name"
217   - en:
218   - - "enSpuName"
219   - ru:
220   - - "ruSkuName"
221   -
222   - - name: "category"
223   - label: "类目索引"
224   - fields:
225   - - "categoryName"
226   - analyzer: "chinese_ecommerce"
227   - boost: 1.5
228   - example: 'query=category:"玩具"'
229   -
230   - - name: "brand"
231   - label: "品牌索引"
232   - fields:
233   - - "brandName"
234   - analyzer: "chinese_ecommerce"
235   - boost: 1.5
236   - example: 'query=brand:"ZHU LIN"'
237   -
238   -# Query Configuration
239   -query_config:
240   - supported_languages:
241   - - "zh"
242   - - "en"
243   - - "ru"
244   - default_language: "zh"
245   - enable_translation: true
246   - enable_text_embedding: true
247   - enable_query_rewrite: true
248   -
249   - # Translation API (DeepL)
250   - translation_service: "deepl"
251   - translation_api_key: null # Set via environment variable
252   -
253   -# Ranking Configuration(已弃用,保留用于文档说明)
254   -ranking:
255   - expression: "bm25() + 0.2*text_embedding_relevance()"
256   - description: "BM25 text relevance combined with semantic embedding similarity"
257   -
258   -# Function Score配置(ES层打分规则)
259   -# 约定:function_score是查询结构的必需部分
260   -function_score:
261   - score_mode: "sum" # multiply, sum, avg, first, max, min
262   - boost_mode: "multiply" # multiply, replace, sum, avg, max, min
263   -
264   - functions:
265   - # 1. Filter + Weight(条件权重)- 根据条件匹配提权
266   - - type: "filter_weight"
267   - name: "7天新品提权"
268   - filter:
269   - range:
270   - days_since_last_update:
271   - lte: 7
272   - weight: 1.3
273   -
274   - - type: "filter_weight"
275   - name: "30天新品提权"
276   - filter:
277   - range:
278   - days_since_last_update:
279   - lte: 30
280   - weight: 1.15
281   -
282   - - type: "filter_weight"
283   - name: "有视频提权"
284   - filter:
285   - term:
286   - is_video: true
287   - weight: 1.05
288   -
289   - # 示例:特定标签提权
290   - # - type: "filter_weight"
291   - # name: "特定标签提权"
292   - # filter:
293   - # term:
294   - # labelId_by_skuId_essa_3: 165
295   - # weight: 1.1
296   -
297   - # 示例:主力价格段提权
298   - # - type: "filter_weight"
299   - # name: "主力价格段"
300   - # filter:
301   - # range:
302   - # price:
303   - # gte: 50
304   - # lte: 200
305   - # weight: 1.1
306   -
307   - # 2. Field Value Factor(字段值映射)- 将数值字段映射为打分因子
308   - # 示例:在售天数
309   - # - type: "field_value_factor"
310   - # name: "在售天数因子"
311   - # field: "on_sell_days_boost"
312   - # factor: 1.0
313   - # modifier: "none" # none, log, log1p, log2p, ln, ln1p, ln2p, square, sqrt, reciprocal
314   - # missing: 1.0
315   -
316   - # 示例:销量因子
317   - # - type: "field_value_factor"
318   - # name: "销量因子"
319   - # field: "sales_count"
320   - # factor: 0.01
321   - # modifier: "log1p" # 对数映射,避免极端值影响
322   - # missing: 1.0
323   -
324   - # 示例:评分因子
325   - # - type: "field_value_factor"
326   - # name: "评分因子"
327   - # field: "rating"
328   - # factor: 0.5
329   - # modifier: "sqrt"
330   - # missing: 1.0
331   -
332   - # 3. Decay Functions(衰减函数)- 距离原点越远分数越低
333   - # 示例:时间衰减
334   - # - type: "decay"
335   - # name: "时间衰减"
336   - # function: "gauss" # gauss, exp, linear
337   - # field: "create_time"
338   - # origin: "now"
339   - # scale: "30d"
340   - # offset: "0d"
341   - # decay: 0.5
342   -
343   - # 示例:价格衰减
344   - # - type: "decay"
345   - # name: "价格衰减"
346   - # function: "linear"
347   - # field: "price"
348   - # origin: "100"
349   - # scale: "50"
350   - # decay: 0.5
351   -
352   -# Rerank配置(本地重排,当前禁用)
353   -rerank:
354   - enabled: false
355   - expression: ""
356   - description: "Local reranking (disabled, use ES function_score instead)"
357   -
358   -# SPU Aggregation (disabled for customer1)
359   -spu_config:
360   - enabled: false
361   - spu_field: null
362   - inner_hits_size: 3
363   -
config/schema/customer1/query_rewrite.dict deleted
... ... @@ -1,4 +0,0 @@
1   -芭比 brand:芭比 OR name:芭比娃娃
2   -玩具 category:玩具
3   -消防 category:消防 OR name:消防
4   -
frontend/index.html
... ... @@ -121,9 +121,9 @@
121 121 </div>
122 122  
123 123 <footer>
124   - <p>SearchEngine © 2025 | API: <span id="apiUrl">http://120.76.41.98:6002</span></p>
  124 + <p>SearchEngine © 2025 | API: <span id="apiUrl">Loading...</span></p>
125 125 </footer>
126 126  
127   - <script src="/static/js/app.js?v=3.0"></script>
  127 + <script src="/static/js/app.js?v=3.1"></script>
128 128 </body>
129 129 </html>
... ...
frontend/static/js/app.js
1 1 // SearchEngine Frontend - Modern UI (Multi-Tenant)
2 2  
3   -const API_BASE_URL = 'http://localhost:6002';
4   -document.getElementById('apiUrl').textContent = API_BASE_URL;
  3 +const API_BASE_URL = window.API_BASE_URL || 'http://localhost:6002';
  4 +if (document.getElementById('apiUrl')) {
  5 + document.getElementById('apiUrl').textContent = API_BASE_URL;
  6 +}
5 7  
6 8 // Get tenant ID from input
7 9 function getTenantId() {
... ...
scripts/create_base_frontend.py deleted
... ... @@ -1,218 +0,0 @@
1   -#!/usr/bin/env python3
2   -"""
3   -Create frontend JavaScript file for base configuration.
4   -"""
5   -
6   -import sys
7   -import os
8   -import argparse
9   -import re
10   -from pathlib import Path
11   -
12   -# Add parent directory to path
13   -sys.path.insert(0, str(Path(__file__).parent.parent))
14   -
15   -
16   -def create_base_frontend_js(tenant_id: str, api_port: int = 6002, output_file: str = "frontend/static/js/app_base.js"):
17   - """
18   - Create frontend JavaScript file for base configuration.
19   -
20   - Args:
21   - tenant_id: Tenant ID
22   - api_port: API port
23   - output_file: Output file path
24   - """
25   - # Read original app.js
26   - original_file = Path(__file__).parent.parent / "frontend/static/js/app.js"
27   - if not original_file.exists():
28   - print(f"ERROR: Original frontend file not found: {original_file}")
29   - return 1
30   -
31   - with open(original_file, 'r', encoding='utf-8') as f:
32   - content = f.read()
33   -
34   - # Replace API_BASE_URL
35   - api_url = f"http://localhost:{api_port}"
36   - content = content.replace(
37   - "const API_BASE_URL = 'http://120.76.41.98:6002';",
38   - f"const API_BASE_URL = '{api_url}';"
39   - )
40   -
41   - # Add tenant_id constant at the beginning
42   - content = content.replace(
43   - "const API_BASE_URL =",
44   - f"const TENANT_ID = '{tenant_id}';\nconst API_BASE_URL ="
45   - )
46   -
47   - # Update facets for base configuration
48   - base_facets = ''' const facets = [
49   - {
50   - "field": "category_keyword",
51   - "size": 15,
52   - "type": "terms"
53   - },
54   - {
55   - "field": "vendor_keyword",
56   - "size": 15,
57   - "type": "terms"
58   - },
59   - {
60   - "field": "tags_keyword",
61   - "size": 10,
62   - "type": "terms"
63   - },
64   - {
65   - "field": "min_price",
66   - "type": "range",
67   - "ranges": [
68   - {"key": "0-50", "to": 50},
69   - {"key": "50-100", "from": 50, "to": 100},
70   - {"key": "100-200", "from": 100, "to": 200},
71   - {"key": "200+", "from": 200}
72   - ]
73   - }
74   - ];'''
75   -
76   - # Find and replace facets definition (multiline match)
77   - facets_pattern = r'const facets = \[.*?\];'
78   - content = re.sub(facets_pattern, base_facets, content, flags=re.DOTALL)
79   -
80   - # Update fetch to include tenant_id header
81   - content = content.replace(
82   - "headers: {\n 'Content-Type': 'application/json',\n },",
83   - f"headers: {{\n 'Content-Type': 'application/json',\n 'X-Tenant-ID': TENANT_ID,\n }},"
84   - )
85   -
86   - # Replace hits with results throughout
87   - content = re.sub(r'\bdata\.hits\b', 'data.results', content)
88   - content = re.sub(r'!data\.hits', '!data.results', content)
89   -
90   - # Replace hit loop with product loop
91   - content = re.sub(
92   - r'data\.hits\.forEach\(\(hit\) => \{',
93   - 'data.results.forEach((product) => {',
94   - content
95   - )
96   -
97   - # Remove source extraction lines
98   - content = re.sub(r'const source = hit\._source;\s*\n', '', content)
99   - content = re.sub(r'const score = hit\._custom_score \|\| hit\._score;\s*\n', 'const score = product.relevance_score;\n', content)
100   -
101   - # Replace all source. references with product.
102   - content = re.sub(r'\bsource\.', 'product.', content)
103   -
104   - # Replace specific field names for base configuration
105   - # imageUrl -> image_url
106   - content = re.sub(r'product\.imageUrl', 'product.image_url', content)
107   - # name -> title
108   - content = re.sub(r'product\.name', 'product.title', content)
109   - content = re.sub(r'product\.enSpuName', 'product.title', content)
110   - # categoryName -> category
111   - content = re.sub(r'product\.categoryName', 'product.category', content)
112   - # brandName -> vendor
113   - content = re.sub(r'product\.brandName', 'product.vendor', content)
114   - # price -> price (already correct)
115   - # Remove moq and quantity fields (not in base config)
116   - content = re.sub(r'<div class="product-moq">.*?</div>\s*\n', '', content, flags=re.DOTALL)
117   - content = re.sub(r'<div class="product-quantity">.*?</div>\s*\n', '', content, flags=re.DOTALL)
118   -
119   - # Add stock and variants display
120   - # Find the product-price div and add stock info after it
121   - stock_info = ''' <div class="product-stock">
122   - ${product.in_stock ? '<span style="color: green;">In Stock</span>' : '<span style="color: red;">Out of Stock</span>'}
123   - ${product.variants && product.variants.length > 0 ? `<span style="color: #666; font-size: 0.9em;">(${product.variants.length} variants)</span>` : ''}
124   - </div>
125   -
126   -'''
127   - content = re.sub(
128   - r'(<div class="product-price">.*?</div>\s*\n)',
129   - r'\1' + stock_info,
130   - content,
131   - flags=re.DOTALL
132   - )
133   -
134   - # Update price display format
135   - content = re.sub(
136   - r'\$\{product\.price \? `\$\{product\.price\} ₽` : \'N/A\'\}',
137   - '${product.price ? `$${product.price.toFixed(2)}` : \'N/A\'}',
138   - content
139   - )
140   -
141   - # Add compare_at_price if exists
142   - content = re.sub(
143   - r'(\$\{product\.price \? `\$\$\{product\.price\.toFixed\(2\)\}` : \'N/A\'\})',
144   - r'\1${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>` : \'\'}',
145   - content
146   - )
147   -
148   - # Update product-meta to use base config fields
149   - content = re.sub(
150   - r'<div class="product-meta">\s*\$\{product\.category \? escapeHtml\(product\.category\) : \'\'\}\s*\$\{product\.vendor \? \' \| \' \+ escapeHtml\(product\.vendor\) : \'\'\}\s*</div>',
151   - '<div class="product-meta">${product.vendor ? escapeHtml(product.vendor) : \'\'}${product.product_type ? \' | \' + escapeHtml(product.product_type) : \'\'}${product.category ? \' | \' + escapeHtml(product.category) : \'\'}</div>',
152   - content
153   - )
154   -
155   - # Remove create_time display (not in base config)
156   - content = re.sub(
157   - r'\$\{product\.create_time \? `.*?</div>\s*` : \'\'\}',
158   - '',
159   - content,
160   - flags=re.DOTALL
161   - )
162   -
163   - # Add tags display if exists
164   - tags_display = ''' ${product.tags ? `
165   - <div class="product-tags">
166   - Tags: ${escapeHtml(product.tags)}
167   - </div>
168   - ` : ''}'''
169   -
170   - # Add tags before closing product-card div
171   - content = re.sub(
172   - r'(</div>\s*</div>\s*`;\s*\n\s*\}\);)',
173   - tags_display + r'\n </div>\n `;\n });',
174   - content,
175   - count=1
176   - )
177   -
178   - # Update displayFacets for base configuration field names
179   - content = re.sub(
180   - r"facet\.field === 'categoryName_keyword'",
181   - "facet.field === 'category_keyword'",
182   - content
183   - )
184   - content = re.sub(
185   - r"facet\.field === 'brandName_keyword'",
186   - "facet.field === 'vendor_keyword'",
187   - content
188   - )
189   - content = re.sub(
190   - r"facet\.field === 'supplierName_keyword'",
191   - "facet.field === 'tags_keyword'",
192   - content
193   - )
194   -
195   - # Write output file
196   - output_path = Path(__file__).parent.parent / output_file
197   - output_path.parent.mkdir(parents=True, exist_ok=True)
198   -
199   - with open(output_path, 'w', encoding='utf-8') as f:
200   - f.write(content)
201   -
202   - print(f"Created base frontend JavaScript: {output_path}")
203   - return 0
204   -
205   -
206   -def main():
207   - parser = argparse.ArgumentParser(description='Create frontend JavaScript for base configuration')
208   - parser.add_argument('--tenant-id', default='1', help='Tenant ID')
209   - parser.add_argument('--api-port', type=int, default=6002, help='API port')
210   - parser.add_argument('--output', default='frontend/static/js/app_base.js', help='Output file')
211   -
212   - args = parser.parse_args()
213   -
214   - return create_base_frontend_js(args.tenant_id, args.api_port, args.output)
215   -
216   -
217   -if __name__ == '__main__':
218   - sys.exit(main())
scripts/demo_base.sh deleted
... ... @@ -1,198 +0,0 @@
1   -#!/bin/bash
2   -
3   -# Base配置演示流程脚本
4   -# 用于演示店匠通用客户的搜索效果
5   -
6   -set -e
7   -
8   -cd "$(dirname "$0")/.."
9   -source /home/tw/miniconda3/etc/profile.d/conda.sh
10   -conda activate searchengine
11   -
12   -GREEN='\033[0;32m'
13   -YELLOW='\033[1;33m'
14   -RED='\033[0;31m'
15   -NC='\033[0m'
16   -
17   -echo -e "${GREEN}========================================${NC}"
18   -echo -e "${GREEN}Base配置演示流程${NC}"
19   -echo -e "${GREEN}========================================${NC}"
20   -
21   -# 加载.env配置文件
22   -if [ -f .env ]; then
23   - set -a
24   - source .env
25   - set +a
26   -fi
27   -
28   -# 配置参数(从环境变量或默认值)
29   -TENANT_ID=${1:-"1"}
30   -DB_HOST=${DB_HOST:-"120.79.247.228"}
31   -DB_PORT=${DB_PORT:-"3316"}
32   -DB_DATABASE=${DB_DATABASE:-"saas"}
33   -DB_USERNAME=${DB_USERNAME:-"saas"}
34   -DB_PASSWORD=${DB_PASSWORD:-"P89cZHS5d7dFyc9R"}
35   -ES_HOST=${ES_HOST:-"http://localhost:9200"}
36   -API_PORT=${API_PORT:-"6002"}
37   -FRONTEND_PORT=${FRONTEND_PORT:-"6003"}
38   -
39   -echo -e "\n${YELLOW}配置参数:${NC}"
40   -echo " Tenant ID: $TENANT_ID"
41   -echo " MySQL: $DB_HOST:$DB_PORT/$DB_DATABASE"
42   -echo " Elasticsearch: $ES_HOST"
43   -echo " API Port: $API_PORT"
44   -echo " Frontend Port: $FRONTEND_PORT"
45   -
46   -# Step 1: 生成测试数据
47   -echo -e "\n${YELLOW}Step 1/5: 生成测试数据${NC}"
48   -if [ ! -f "test_data_base.sql" ]; then
49   - echo "生成100条SPU测试数据..."
50   - python scripts/generate_test_data.py \
51   - --num-spus 100 \
52   - --tenant-id "$TENANT_ID" \
53   - --start-spu-id 1 \
54   - --start-sku-id 1 \
55   - --output test_data_base.sql
56   - echo -e "${GREEN}✓ 测试数据已生成: test_data_base.sql${NC}"
57   -else
58   - echo -e "${YELLOW}⚠ 测试数据文件已存在,跳过生成${NC}"
59   -fi
60   -
61   -# Step 2: 导入测试数据到MySQL
62   -echo -e "\n${YELLOW}Step 2/5: 导入测试数据到MySQL${NC}"
63   -if [ -z "$DB_PASSWORD" ]; then
64   - echo -e "${RED}ERROR: DB_PASSWORD未设置,请检查.env文件或环境变量${NC}"
65   - exit 1
66   -fi
67   -
68   -python scripts/import_test_data.py \
69   - --db-host "$DB_HOST" \
70   - --db-port "$DB_PORT" \
71   - --db-database "$DB_DATABASE" \
72   - --db-username "$DB_USERNAME" \
73   - --db-password "$DB_PASSWORD" \
74   - --sql-file test_data_base.sql \
75   - --tenant-id "$TENANT_ID"
76   -
77   -echo -e "${GREEN}✓ 测试数据已导入MySQL${NC}"
78   -
79   -# Step 3: 导入数据到Elasticsearch
80   -echo -e "\n${YELLOW}Step 3/5: 导入数据到Elasticsearch${NC}"
81   -python scripts/ingest_shoplazza.py \
82   - --db-host "$DB_HOST" \
83   - --db-port "$DB_PORT" \
84   - --db-database "$DB_DATABASE" \
85   - --db-username "$DB_USERNAME" \
86   - --db-password "$DB_PASSWORD" \
87   - --tenant-id "$TENANT_ID" \
88   - --config base \
89   - --es-host "$ES_HOST" \
90   - --recreate \
91   - --batch-size 500
92   -
93   -echo -e "${GREEN}✓ 数据已导入Elasticsearch${NC}"
94   -
95   -# Step 4: 启动后端服务
96   -echo -e "\n${YELLOW}Step 4/5: 启动后端服务${NC}"
97   -echo "后端服务将在后台运行..."
98   -
99   -# 创建logs目录
100   -mkdir -p logs
101   -
102   -# 启动后端(后台运行)
103   -nohup python api/app.py \
104   - --host 0.0.0.0 \
105   - --port "$API_PORT" \
106   - --customer base \
107   - --es-host "$ES_HOST" > logs/backend_base.log 2>&1 &
108   -
109   -BACKEND_PID=$!
110   -echo $BACKEND_PID > logs/backend_base.pid
111   -echo -e "${GREEN}后端服务已启动 (PID: $BACKEND_PID)${NC}"
112   -echo -e "${GREEN}日志文件: logs/backend_base.log${NC}"
113   -
114   -# 等待后端启动
115   -echo -e "${YELLOW}等待后端服务启动...${NC}"
116   -MAX_RETRIES=15
117   -RETRY_COUNT=0
118   -BACKEND_READY=false
119   -
120   -while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do
121   - sleep 2
122   - if curl -s "http://localhost:$API_PORT/health" > /dev/null 2>&1; then
123   - BACKEND_READY=true
124   - break
125   - fi
126   - RETRY_COUNT=$((RETRY_COUNT + 1))
127   - echo -e "${YELLOW} 等待中... ($RETRY_COUNT/$MAX_RETRIES)${NC}"
128   -done
129   -
130   -if [ "$BACKEND_READY" = true ]; then
131   - echo -e "${GREEN}✓ 后端服务运行正常${NC}"
132   -else
133   - echo -e "${YELLOW}⚠ 后端服务可能还在启动中,请稍后访问${NC}"
134   -fi
135   -
136   -# Step 5: 启动前端服务
137   -echo -e "\n${YELLOW}Step 5/5: 启动前端服务${NC}"
138   -echo "前端服务将在后台运行..."
139   -
140   -# 创建base配置的前端JS文件
141   -echo "创建base配置前端文件..."
142   -python scripts/create_base_frontend.py --tenant-id "$TENANT_ID" --api-port "$API_PORT"
143   -echo -e "${GREEN}✓ 前端文件已创建${NC}"
144   -
145   -# 启动前端(后台运行)
146   -export PORT="$FRONTEND_PORT"
147   -nohup python scripts/frontend_server.py > logs/frontend_base.log 2>&1 &
148   -
149   -FRONTEND_PID=$!
150   -echo $FRONTEND_PID > logs/frontend_base.pid
151   -echo -e "${GREEN}前端服务已启动 (PID: $FRONTEND_PID)${NC}"
152   -echo -e "${GREEN}日志文件: logs/frontend_base.log${NC}"
153   -
154   -# 等待前端启动
155   -echo -e "${YELLOW}等待前端服务启动...${NC}"
156   -MAX_RETRIES=10
157   -RETRY_COUNT=0
158   -FRONTEND_READY=false
159   -
160   -while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do
161   - sleep 2
162   - if curl -s "http://localhost:$FRONTEND_PORT/" > /dev/null 2>&1; then
163   - FRONTEND_READY=true
164   - break
165   - fi
166   - RETRY_COUNT=$((RETRY_COUNT + 1))
167   - echo -e "${YELLOW} 等待中... ($RETRY_COUNT/$MAX_RETRIES)${NC}"
168   -done
169   -
170   -if [ "$FRONTEND_READY" = true ]; then
171   - echo -e "${GREEN}✓ 前端服务运行正常${NC}"
172   -else
173   - echo -e "${YELLOW}⚠ 前端服务可能还在启动中,请稍后访问${NC}"
174   -fi
175   -
176   -echo -e "\n${GREEN}========================================${NC}"
177   -echo -e "${GREEN}演示环境启动完成!${NC}"
178   -echo -e "${GREEN}========================================${NC}"
179   -echo ""
180   -echo -e "访问地址:"
181   -echo -e " ${GREEN}前端界面: http://localhost:$FRONTEND_PORT/base${NC} (或 http://localhost:$FRONTEND_PORT/base.html)"
182   -echo -e " ${GREEN}后端API: http://localhost:$API_PORT${NC}"
183   -echo -e " ${GREEN}API文档: http://localhost:$API_PORT/docs${NC}"
184   -echo ""
185   -echo -e "配置信息:"
186   -echo -e " Tenant ID: $TENANT_ID"
187   -echo -e " Customer Config: base"
188   -echo ""
189   -echo -e "日志文件:"
190   -echo -e " 后端: logs/backend_base.log"
191   -echo -e " 前端: logs/frontend_base.log"
192   -echo ""
193   -echo -e "停止服务:"
194   -echo -e " 所有服务: ./scripts/stop_base.sh"
195   -echo -e " 单独停止后端: kill \$(cat logs/backend_base.pid)"
196   -echo -e " 单独停止前端: kill \$(cat logs/frontend_base.pid)"
197   -echo ""
198   -
scripts/frontend_server.py
... ... @@ -10,6 +10,15 @@ import sys
10 10 import logging
11 11 import time
12 12 from collections import defaultdict, deque
  13 +from pathlib import Path
  14 +from dotenv import load_dotenv
  15 +
  16 +# Load .env file
  17 +project_root = Path(__file__).parent.parent
  18 +load_dotenv(project_root / '.env')
  19 +
  20 +# Get API_BASE_URL from environment
  21 +API_BASE_URL = os.getenv('API_BASE_URL', 'http://localhost:6002')
13 22  
14 23 # Change to frontend directory
15 24 frontend_dir = os.path.join(os.path.dirname(__file__), '../frontend')
... ... @@ -46,19 +55,41 @@ class MyHTTPRequestHandler(http.server.SimpleHTTPRequestHandler, RateLimitingMix
46 55 """Custom request handler with CORS support and robust error handling."""
47 56  
48 57 def do_GET(self):
49   - """Handle GET requests with support for base.html."""
50   - # Parse path (handle query strings)
51   - path = self.path.split('?')[0] # Remove query string if present
  58 + """Handle GET requests with API config injection."""
  59 + path = self.path.split('?')[0]
52 60  
53   - # Route /base to base.html (handle both with and without trailing slash)
54   - if path == '/base' or path == '/base/':
55   - self.path = '/base.html' + (self.path.split('?', 1)[1] if '?' in self.path else '')
56   - # Route / to index.html (default)
57   - elif path == '/' or path == '':
  61 + # Route / to index.html
  62 + if path == '/' or path == '':
58 63 self.path = '/index.html' + (self.path.split('?', 1)[1] if '?' in self.path else '')
59 64  
60   - # Call parent do_GET with modified path
61   - super().do_GET()
  65 + # Inject API config for HTML files
  66 + if self.path.endswith('.html'):
  67 + self._serve_html_with_config()
  68 + else:
  69 + super().do_GET()
  70 +
  71 + def _serve_html_with_config(self):
  72 + """Serve HTML with API_BASE_URL injected."""
  73 + try:
  74 + file_path = self.path.lstrip('/')
  75 + if not os.path.exists(file_path):
  76 + self.send_error(404)
  77 + return
  78 +
  79 + with open(file_path, 'r', encoding='utf-8') as f:
  80 + html = f.read()
  81 +
  82 + # Inject API_BASE_URL before app.js
  83 + config_script = f'<script>window.API_BASE_URL="{API_BASE_URL}";</script>\n '
  84 + html = html.replace('<script src="/static/js/app.js', config_script + '<script src="/static/js/app.js', 1)
  85 +
  86 + self.send_response(200)
  87 + self.send_header('Content-Type', 'text/html; charset=utf-8')
  88 + self.end_headers()
  89 + self.wfile.write(html.encode('utf-8'))
  90 + except Exception as e:
  91 + logging.error(f"Error serving HTML: {e}")
  92 + self.send_error(500)
62 93  
63 94 def setup(self):
64 95 """Setup with error handling."""
... ...
scripts/stop_base.sh deleted
... ... @@ -1,45 +0,0 @@
1   -#!/bin/bash
2   -
3   -# Stop Base配置演示服务
4   -
5   -set -e
6   -
7   -cd "$(dirname "$0")/.."
8   -
9   -GREEN='\033[0;32m'
10   -YELLOW='\033[1;33m'
11   -RED='\033[0;31m'
12   -NC='\033[0m'
13   -
14   -echo -e "${YELLOW}停止Base配置演示服务...${NC}"
15   -
16   -# Stop backend
17   -if [ -f logs/backend_base.pid ]; then
18   - BACKEND_PID=$(cat logs/backend_base.pid)
19   - if ps -p $BACKEND_PID > /dev/null 2>&1; then
20   - kill $BACKEND_PID
21   - echo -e "${GREEN}✓ 后端服务已停止 (PID: $BACKEND_PID)${NC}"
22   - else
23   - echo -e "${YELLOW}⚠ 后端服务进程不存在${NC}"
24   - fi
25   - rm -f logs/backend_base.pid
26   -else
27   - echo -e "${YELLOW}⚠ 后端服务PID文件不存在${NC}"
28   -fi
29   -
30   -# Stop frontend
31   -if [ -f logs/frontend_base.pid ]; then
32   - FRONTEND_PID=$(cat logs/frontend_base.pid)
33   - if ps -p $FRONTEND_PID > /dev/null 2>&1; then
34   - kill $FRONTEND_PID
35   - echo -e "${GREEN}✓ 前端服务已停止 (PID: $FRONTEND_PID)${NC}"
36   - else
37   - echo -e "${YELLOW}⚠ 前端服务进程不存在${NC}"
38   - fi
39   - rm -f logs/frontend_base.pid
40   -else
41   - echo -e "${YELLOW}⚠ 前端服务PID文件不存在${NC}"
42   -fi
43   -
44   -echo -e "${GREEN}所有服务已停止${NC}"
45   -