Commit 362d43b67e9a728680610a07147a27db33adf256

Authored by tangwang
1 parent 4d824a77

店匠体系数据的搜索

@@ -31,3 +31,6 @@ IMAGE_MODEL_DIR=/data/tw/models/cn-clip @@ -31,3 +31,6 @@ IMAGE_MODEL_DIR=/data/tw/models/cn-clip
31 31
32 # Cache Directory 32 # Cache Directory
33 CACHE_DIR=.cache 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,6 +37,10 @@ CUSTOMER_ID = os.getenv('CUSTOMER_ID', 'customer1')
37 # API Service Configuration 37 # API Service Configuration
38 API_HOST = os.getenv('API_HOST', '0.0.0.0') 38 API_HOST = os.getenv('API_HOST', '0.0.0.0')
39 API_PORT = int(os.getenv('API_PORT', 6002)) 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 # Model Directories 45 # Model Directories
42 TEXT_MODEL_DIR = os.getenv('TEXT_MODEL_DIR', '/data/tw/models/bge-m3') 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,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,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,4 +0,0 @@
1 -芭比 brand:芭比 OR name:芭比娃娃  
2 -玩具 category:玩具  
3 -消防 category:消防 OR name:消防  
4 -  
frontend/index.html
@@ -121,9 +121,9 @@ @@ -121,9 +121,9 @@
121 </div> 121 </div>
122 122
123 <footer> 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 </footer> 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 </body> 128 </body>
129 </html> 129 </html>
frontend/static/js/app.js
1 // SearchEngine Frontend - Modern UI (Multi-Tenant) 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 // Get tenant ID from input 8 // Get tenant ID from input
7 function getTenantId() { 9 function getTenantId() {
scripts/create_base_frontend.py deleted
@@ -1,218 +0,0 @@ @@ -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,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,6 +10,15 @@ import sys
10 import logging 10 import logging
11 import time 11 import time
12 from collections import defaultdict, deque 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 # Change to frontend directory 23 # Change to frontend directory
15 frontend_dir = os.path.join(os.path.dirname(__file__), '../frontend') 24 frontend_dir = os.path.join(os.path.dirname(__file__), '../frontend')
@@ -46,19 +55,41 @@ class MyHTTPRequestHandler(http.server.SimpleHTTPRequestHandler, RateLimitingMix @@ -46,19 +55,41 @@ class MyHTTPRequestHandler(http.server.SimpleHTTPRequestHandler, RateLimitingMix
46 """Custom request handler with CORS support and robust error handling.""" 55 """Custom request handler with CORS support and robust error handling."""
47 56
48 def do_GET(self): 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 self.path = '/index.html' + (self.path.split('?', 1)[1] if '?' in self.path else '') 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 def setup(self): 94 def setup(self):
64 """Setup with error handling.""" 95 """Setup with error handling."""
scripts/stop_base.sh deleted
@@ -1,45 +0,0 @@ @@ -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 -