ff5325fa
tangwang
修复:直接在 Searcher 层...
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
|
# 最佳实践重构总结
**重构日期**: 2025-11-12
**重构人员**: AI Assistant
**重构原则**: 代码简洁、类型安全、单一职责
---
## 重构目标
1. **移除兼容代码**:不再支持多种数据格式,统一使用最佳实践
2. **修复前端422错误**:支持日期时间字符串的 range filter
3. **保持代码简洁**:单一数据流,清晰的类型定义
---
## 修改文件
### 1. `/home/tw/SearchEngine/api/models.py`
#### 修改:RangeFilter 模型
**之前**:只支持数值 (float)
```python
gte: Optional[float] = Field(None, description="大于等于 (>=)")
```
**之后**:支持数值和日期时间字符串
```python
gte: Optional[Union[float, str]] = Field(None, description="大于等于 (>=)。数值或ISO日期时间字符串")
```
**理由**:
- 价格字段需要数值过滤:`{"gte": 50, "lte": 200}`
- 时间字段需要字符串过滤:`{"gte": "2023-01-01T00:00:00Z"}`
- 使用 `Union[float, str]` 同时支持两种类型
---
### 2. `/home/tw/SearchEngine/search/es_query_builder.py`
#### 修改:build_facets 方法
**之前**:兼容字典和 Pydantic 模型
```python
else:
if isinstance(config, dict):
field = config['field']
facet_type = config.get('type', 'terms')
...
else:
# Pydantic模型
field = config.field
facet_type = config.type
...
```
**之后**:只支持标准格式(str 或 FacetConfig)
```python
# 简单模式:只有字段名(字符串)
if isinstance(config, str):
field = config
...
# 高级模式:FacetConfig 对象
else:
field = config.field
facet_type = config.type
...
```
**理由**:
- API 层已经定义标准格式:`List[Union[str, FacetConfig]]`
- 移除字典支持,保持单一数据流
- 代码更简洁,意图更清晰
---
### 3. `/home/tw/SearchEngine/search/searcher.py`
#### 修改:_standardize_facets 方法
**之前**:兼容字典和 Pydantic 模型
```python
else:
field = config.get('field') if isinstance(config, dict) else config.field
facet_type = config.get('type', 'terms') if isinstance(config, dict) else getattr(config, 'type', 'terms')
```
**之后**:只支持标准格式
```python
else:
# FacetConfig 对象
field = config.field
facet_type = config.type
```
**理由**:
- 与 build_facets 保持一致
- 移除冗余的类型检查
- 代码更清晰易读
---
## 数据流设计
### 标准数据流(最佳实践)
```
API Request
↓
Pydantic 验证 (SearchRequest)
↓ facets: List[Union[str, FacetConfig]]
Searcher.search()
↓
QueryBuilder.build_facets()
↓ 只接受 str 或 FacetConfig
ES Query (aggs)
↓
ES Response (aggregations)
↓
Searcher._standardize_facets()
↓ 只处理 str 或 FacetConfig
List[FacetResult] (Pydantic 模型)
↓
SearchResponse
↓
API Response (JSON)
```
### 类型定义
```python
# API 层
facets: Optional[List[Union[str, FacetConfig]]] = None
# Query Builder 层
def build_facets(facet_configs: Optional[List[Union[str, 'FacetConfig']]])
# Searcher 层
def _standardize_facets(...) -> Optional[List[FacetResult]]
# Response 层
facets: Optional[List[FacetResult]] = None
```
---
## 测试结果
### ✅ 所有测试通过
| 测试场景 | 状态 | 说明 |
|---------|------|------|
| 字符串 facets | ✓ | `["categoryName_keyword"]` |
| FacetConfig 对象 | ✓ | `{"field": "price", "type": "range", ...}` |
| 数值 range filter | ✓ | `{"gte": 50, "lte": 200}` |
| 日期时间 range filter | ✓ | `{"gte": "2023-01-01T00:00:00Z"}` |
| 混合使用 | ✓ | filters + range_filters + facets |
### ✅ 前端 422 错误已修复
**问题**:前端筛选 listing time 时返回 422 Unprocessable Entity
**原因**:`RangeFilter` 只接受 `float`,不接受日期时间字符串
**解决**:修改 `RangeFilter` 支持 `Union[float, str]`
**验证**:
```bash
curl -X POST /search/ \
-d '{"query": "玩具", "range_filters": {"create_time": {"gte": "2023-01-01T00:00:00Z"}}}'
# ✓ 200 OK
```
---
## 代码质量
### ✅ 无 Linter 错误
```bash
No linter errors found.
```
### ✅ 类型安全
- 所有类型明确定义
- Pydantic 自动验证
- IDE 类型提示完整
### ✅ 代码简洁
- 移除所有兼容代码
- 单一数据格式
- 清晰的控制流
---
## 最佳实践原则
### 1. **单一职责**
每个方法只处理一种标准格式,不兼容多种输入
### 2. **类型明确**
使用 Pydantic 模型而不是字典,类型在编译时就确定
### 3. **数据流清晰**
API → Pydantic → 业务逻辑 → Pydantic → Response
### 4. **早期验证**
在 API 层就验证数据,不在内部做多重兼容检查
### 5. **代码可维护**
- 删除冗余代码
- 保持一致性
- 易于理解和修改
---
## 对比总结
| 方面 | 重构前 | 重构后 |
|------|-------|--------|
| **数据格式** | 字典 + Pydantic(兼容) | 只有 Pydantic |
| **类型检查** | 运行时多重检查 | 编译时类型明确 |
| **代码行数** | 更多(兼容代码) | 更少(单一逻辑) |
| **可维护性** | 复杂(多种路径) | 简单(单一路径) |
| **错误处理** | 隐式容错 | 明确验证 |
| **RangeFilter** | 只支持数值 | 支持数值+字符串 |
| **前端兼容** | 422 错误 | 完全兼容 |
---
## 后续建议
### 1. 代码审查
- [x] 移除所有字典兼容代码
- [x] 统一使用 Pydantic 模型
- [x] 修复前端 422 错误
### 2. 测试覆盖
- [x] 字符串 facets
- [x] FacetConfig 对象
- [x] 数值 range filter
- [x] 日期时间 range filter
- [x] 混合场景
### 3. 文档更新
- [x] 最佳实践文档
- [x] 数据流设计
- [ ] API 文档更新
### 4. 性能优化
- [ ] 添加请求缓存
- [ ] 优化 Pydantic 验证
- [ ] 监控性能指标
---
## 结论
本次重构成功实现了以下目标:
✅ **代码简洁**:移除所有兼容代码,保持单一数据流
✅ **类型安全**:统一使用 Pydantic 模型,编译时类型检查
✅ **功能完整**:修复前端 422 错误,支持所有筛选场景
✅ **可维护性**:代码清晰,易于理解和修改
**核心原则**:*不做兼容多种方式的代码,定义一种最佳实践,所有模块都适配这种新方式*
---
**版本**: v3.1
**状态**: ✅ 完成并通过测试
**下次更新**: 根据业务需求扩展
|