Commit 985d7fe3445c9ccc53390de172438681849ed042
1 parent
ff32d894
为 filters 中所有字段加上 `*_all` 语义
---
1. `search/es_query_builder.py`:`_all` 分支
- **普通字段**(如 `tags_all`, `category1_name_all`):
- 键以 `_all` 结尾时,先去掉后缀得到 ES 字段名。
- 若值为**数组**:生成 `bool.must`,内含多个 `term`,即多值 **AND**。
- 若值为**单值**:生成一个 `term`。
- **specifications_all**:
- 值为 `[{name, value}, ...]` 时,为每一项生成一个 nested 查询,全部放入同一个 `bool.must`,即列表内所有规格条件都要满足(AND)。
原有逻辑不变:不带 `_all` 的字段,数组仍为 OR(`terms`),单值仍为 `term`。
2. `api/models.py`:filters 说明
- 在 `filters` 的 `description` 中补充:
- 字段名加 `_all` 表示 AND(如 `tags_all: ['A','B']` 表示同时包含 A 和 B)。
- `specifications_all` 表示列表内所有规格条件都要满足。
3. `docs/搜索API对接指南.md`:文档
- 在 3.3.1 开头说明:任意字段名可加 `_all` 后缀表示多值 AND。
- 在格式示例中增加 `tags_all`、`category1_name_all` 示例。
- 在「支持的值类型」中说明:数组在带 `_all` 时为 AND。
- 新增小节「`*_all` 语义(多值 AND)」:说明用法及 `specifications_all` 行为。
- 在「常用过滤字段」中补充:以上字段均可加 `_all` 后缀。
---
**使用示例**
```json
{
"filters": {
"tags": ["手机", "促销"],
"tags_all": ["手机", "促销", "新品"]
}
}
```
- `tags`:命中「手机」或「促销」或两者都有(OR)。
- `tags_all`:必须同时包含「手机」「促销」「新品」三个标签(AND)。
Showing
4 changed files
with
97 additions
and
13 deletions
Show diff stats
api/models.py
| ... | ... | @@ -81,7 +81,7 @@ class SearchRequest(BaseModel): |
| 81 | 81 | # 过滤器 - 精确匹配和多值匹配 |
| 82 | 82 | filters: Optional[Dict[str, Union[str, int, bool, List[Union[str, int]], Dict[str, Any], List[Dict[str, Any]]]]] = Field( |
| 83 | 83 | None, |
| 84 | - description="精确匹配过滤器。单值表示精确匹配,数组表示 OR 匹配(匹配任意一个值)。支持 specifications 嵌套过滤:{\"specifications\": {\"name\": \"color\", \"value\": \"green\"}} 或 [{\"name\": \"color\", \"value\": \"green\"}, ...]", | |
| 84 | + description="精确匹配过滤器。单值表示精确匹配,数组表示 OR 匹配(匹配任意一个值)。字段名加 _all 后缀表示 AND(如 tags_all: ['A','B'] 表示同时包含 A 和 B)。支持 specifications 嵌套过滤:{\"specifications\": {\"name\": \"color\", \"value\": \"green\"}} 或 [{\"name\": \"color\", \"value\": \"green\"}, ...];specifications_all 表示列表内所有规格条件都要满足。", | |
| 85 | 85 | json_schema_extra={ |
| 86 | 86 | "examples": [ |
| 87 | 87 | { | ... | ... |
docs/常用查询 - ES.md
| ... | ... | @@ -31,7 +31,44 @@ curl -u 'essa:4hOaLaf41y2VuI8y' -X GET 'http://localhost:9200/search_products/ |
| 31 | 31 | } |
| 32 | 32 | }' |
| 33 | 33 | |
| 34 | -curl -u 'essa:4hOaLaf41y2VuI8y' -X GET 'http://localhost:9200/search_products/_search?pretty' -H 'Content-Type: application/json' -d '{ | |
| 34 | + | |
| 35 | +curl -u 'essa:4hOaLaf41y2VuI8y' -X GET 'http://localhost:9200/search_products_tenant_170/_search?pretty' -H 'Content-Type: application/json' -d '{ | |
| 36 | + "size": 5, | |
| 37 | + "_source": ["title", "keyword", "keyword.zh", "tags"], | |
| 38 | + "query": { | |
| 39 | + "bool": { | |
| 40 | + "filter": [ | |
| 41 | + { "term": { "spu_id": "223167" } } | |
| 42 | + ] | |
| 43 | + } | |
| 44 | + } | |
| 45 | + }' | |
| 46 | + | |
| 47 | + | |
| 48 | +curl -u 'essa:4hOaLaf41y2VuI8y' -X GET 'http://localhost:9200/search_products_tenant_170/_search?pretty' -H 'Content-Type: application/json' -d '{ | |
| 49 | + "size": 1, | |
| 50 | + "_source": ["title", "keyword", "keyword.zh", "tags"], | |
| 51 | + "query": { | |
| 52 | + "bool": { | |
| 53 | + "must": [ | |
| 54 | + { | |
| 55 | + "match": { | |
| 56 | + "title.en": { | |
| 57 | + "query": "Floerns Women Gothic Graphic Ribbed Strapless Tube Top Asymmetrical Ruched Bandeau Tops" | |
| 58 | + } | |
| 59 | + } | |
| 60 | + } | |
| 61 | + ], | |
| 62 | + "filter": [ | |
| 63 | + { "term": { "tenant_id": "170" } }, | |
| 64 | + { "terms": { "tags": ["女装", "派对"] } } | |
| 65 | + ] | |
| 66 | + } | |
| 67 | + } | |
| 68 | +}' | |
| 69 | + | |
| 70 | + | |
| 71 | +curl -u 'essa:4hOaLaf41y2VuI8y' -X GET 'http://localhost:9200/search_products_tenant_170/_search?pretty' -H 'Content-Type: application/json' -d '{ | |
| 35 | 72 | "size": 1, |
| 36 | 73 | "_source": ["title"], |
| 37 | 74 | "query": { |
| ... | ... | @@ -39,8 +76,8 @@ curl -u 'essa:4hOaLaf41y2VuI8y' -X GET 'http://localhost:9200/search_products/ |
| 39 | 76 | "must": [ |
| 40 | 77 | { |
| 41 | 78 | "match": { |
| 42 | - "title.zh": { | |
| 43 | - "query": "裙子" | |
| 79 | + "title.en": { | |
| 80 | + "query": "Floerns Women Gothic Graphic Ribbed Strapless Tube Top Asymmetrical Ruched Bandeau Tops" | |
| 44 | 81 | } |
| 45 | 82 | } |
| 46 | 83 | } | ... | ... |
docs/搜索API对接指南.md
| ... | ... | @@ -201,18 +201,20 @@ curl -X POST "http://120.76.41.98:6002/search/" \ |
| 201 | 201 | |
| 202 | 202 | #### 3.3.1 精确匹配过滤器 (filters) |
| 203 | 203 | |
| 204 | -用于精确匹配或多值匹配。对于普通字段,数组表示 OR 逻辑(匹配任意一个值);对于 specifications 字段,按维度分组处理。 | |
| 204 | +用于精确匹配或多值匹配。对于普通字段,数组表示 OR 逻辑(匹配任意一个值);对于 specifications 字段,按维度分组处理。**任意字段名加 `_all` 后缀**表示多值 AND 逻辑(必须同时匹配所有值)。 | |
| 205 | 205 | |
| 206 | 206 | **格式**: |
| 207 | 207 | ```json |
| 208 | 208 | { |
| 209 | 209 | "filters": { |
| 210 | - "category_name": "手机", // 可以为单值 或者 数组 匹配数组中任意一个 | |
| 211 | - "category1_name": "服装", // 可以为单值 或者 数组 匹配数组中任意一个 | |
| 212 | - "category2_name": "男装", // 可以为单值 或者 数组 匹配数组中任意一个 | |
| 213 | - "category3_name": "衬衫", // 可以为单值 或者 数组 匹配数组中任意一个 | |
| 214 | - "vendor.zh.keyword": ["奇乐", "品牌A"], // 可以为单值 或者 数组 匹配数组中任意一个 | |
| 215 | - "tags": "手机", // 可以为单值 或者 数组 匹配数组中任意一个 | |
| 210 | + "category_name": "手机", // 可以为单值 或者 数组 匹配数组中任意一个(OR) | |
| 211 | + "category1_name": "服装", // 可以为单值 或者 数组 匹配数组中任意一个(OR) | |
| 212 | + "category2_name": "男装", // 可以为单值 或者 数组 匹配数组中任意一个(OR) | |
| 213 | + "category3_name": "衬衫", // 可以为单值 或者 数组 匹配数组中任意一个(OR) | |
| 214 | + "vendor.zh.keyword": ["奇乐", "品牌A"], // 可以为单值 或者 数组 匹配数组中任意一个(OR) | |
| 215 | + "tags": "手机", // 可以为单值 或者 数组 匹配数组中任意一个(OR) | |
| 216 | + "tags_all": ["手机", "促销", "新品"], // *_all:多值为 AND,必须同时包含所有标签 | |
| 217 | + "category1_name_all": ["服装", "男装"], // 同上,适用于任意可过滤字段 | |
| 216 | 218 | // specifications 嵌套过滤(特殊格式) |
| 217 | 219 | "specifications": { |
| 218 | 220 | "name": "color", |
| ... | ... | @@ -226,9 +228,14 @@ curl -X POST "http://120.76.41.98:6002/search/" \ |
| 226 | 228 | - 字符串:精确匹配 |
| 227 | 229 | - 整数:精确匹配 |
| 228 | 230 | - 布尔值:精确匹配 |
| 229 | -- 数组:匹配任意值(OR 逻辑) | |
| 231 | +- 数组:匹配任意值(OR 逻辑);若字段名以 `_all` 结尾,则数组表示 AND 逻辑(必须同时匹配所有值) | |
| 230 | 232 | - 对象:specifications 嵌套过滤(见下文) |
| 231 | 233 | |
| 234 | +**`*_all` 语义(多值 AND)**: | |
| 235 | +- 任意过滤字段均可使用 `_all` 后缀,对应 ES 字段名为去掉 `_all` 后的名称。 | |
| 236 | +- 例如:`tags_all: ["A", "B"]` 表示文档的 `tags` 必须**同时包含** A 和 B;`vendor.zh.keyword_all: ["奇乐", "品牌A"]` 表示同时匹配两个品牌(通常用于 keyword 多值场景)。 | |
| 237 | +- `specifications_all`:传列表 `[{"name":"color","value":"white"},{"name":"size","value":"256GB"}]` 时,表示所有列出的规格条件都要满足(与 `specifications` 多维度时的 AND 一致;若同维度多值则要求文档同时满足多个值,一般用于嵌套多值场景)。 | |
| 238 | + | |
| 232 | 239 | **Specifications 嵌套过滤**: |
| 233 | 240 | |
| 234 | 241 | `specifications` 是嵌套字段,支持按规格名称和值进行过滤。 |
| ... | ... | @@ -286,6 +293,7 @@ curl -X POST "http://120.76.41.98:6002/search/" \ |
| 286 | 293 | - `tags`: 标签(keyword类型,支持数组) |
| 287 | 294 | - `option1_name`, `option2_name`, `option3_name`: 选项名称 |
| 288 | 295 | - `specifications`: 规格过滤(嵌套字段,格式见上文) |
| 296 | +- 以上任意字段均可加 `_all` 后缀表示多值 AND,如 `tags_all`、`category1_name_all`。 | |
| 289 | 297 | |
| 290 | 298 | #### 3.3.2 范围过滤器 (range_filters) |
| 291 | 299 | ... | ... |
search/es_query_builder.py
| ... | ... | @@ -712,7 +712,46 @@ class ESQueryBuilder: |
| 712 | 712 | }) |
| 713 | 713 | continue |
| 714 | 714 | |
| 715 | - # 普通字段过滤 | |
| 715 | + # *_all 语义:多值时为 AND(必须同时匹配所有值) | |
| 716 | + if field.endswith("_all"): | |
| 717 | + es_field = field[:-4] # 去掉 _all 后缀 | |
| 718 | + if es_field == "specifications" and isinstance(value, list): | |
| 719 | + # specifications_all: 列表内每个规格条件都要满足(AND) | |
| 720 | + must_nested = [] | |
| 721 | + for spec in value: | |
| 722 | + if isinstance(spec, dict): | |
| 723 | + name = spec.get("name") | |
| 724 | + spec_value = spec.get("value") | |
| 725 | + if name and spec_value: | |
| 726 | + must_nested.append({ | |
| 727 | + "nested": { | |
| 728 | + "path": "specifications", | |
| 729 | + "query": { | |
| 730 | + "bool": { | |
| 731 | + "must": [ | |
| 732 | + {"term": {"specifications.name": name}}, | |
| 733 | + {"term": {"specifications.value": spec_value}} | |
| 734 | + ] | |
| 735 | + } | |
| 736 | + } | |
| 737 | + } | |
| 738 | + }) | |
| 739 | + if must_nested: | |
| 740 | + filter_clauses.append({"bool": {"must": must_nested}}) | |
| 741 | + else: | |
| 742 | + # 普通字段 _all:多值用 must + 多个 term | |
| 743 | + if isinstance(value, list): | |
| 744 | + if value: | |
| 745 | + filter_clauses.append({ | |
| 746 | + "bool": { | |
| 747 | + "must": [{"term": {es_field: v}} for v in value] | |
| 748 | + } | |
| 749 | + }) | |
| 750 | + else: | |
| 751 | + filter_clauses.append({"term": {es_field: value}}) | |
| 752 | + continue | |
| 753 | + | |
| 754 | + # 普通字段过滤(默认多值为 OR) | |
| 716 | 755 | if isinstance(value, list): |
| 717 | 756 | # 多值匹配(OR) |
| 718 | 757 | filter_clauses.append({ | ... | ... |