Commit 985d7fe3445c9ccc53390de172438681849ed042

Authored by tangwang
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)。
@@ -81,7 +81,7 @@ class SearchRequest(BaseModel): @@ -81,7 +81,7 @@ class SearchRequest(BaseModel):
81 # 过滤器 - 精确匹配和多值匹配 81 # 过滤器 - 精确匹配和多值匹配
82 filters: Optional[Dict[str, Union[str, int, bool, List[Union[str, int]], Dict[str, Any], List[Dict[str, Any]]]]] = Field( 82 filters: Optional[Dict[str, Union[str, int, bool, List[Union[str, int]], Dict[str, Any], List[Dict[str, Any]]]]] = Field(
83 None, 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 json_schema_extra={ 85 json_schema_extra={
86 "examples": [ 86 "examples": [
87 { 87 {
docs/常用查询 - ES.md
@@ -31,7 +31,44 @@ curl -u 'essa:4hOaLaf41y2VuI8y' -X GET 'http://localhost:9200/search_products/ @@ -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 "size": 1, 72 "size": 1,
36 "_source": ["title"], 73 "_source": ["title"],
37 "query": { 74 "query": {
@@ -39,8 +76,8 @@ curl -u 'essa:4hOaLaf41y2VuI8y' -X GET 'http://localhost:9200/search_products/ @@ -39,8 +76,8 @@ curl -u 'essa:4hOaLaf41y2VuI8y' -X GET 'http://localhost:9200/search_products/
39 "must": [ 76 "must": [
40 { 77 {
41 "match": { 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,18 +201,20 @@ curl -X POST "http://120.76.41.98:6002/search/" \
201 201
202 #### 3.3.1 精确匹配过滤器 (filters) 202 #### 3.3.1 精确匹配过滤器 (filters)
203 203
204 -用于精确匹配或多值匹配。对于普通字段,数组表示 OR 逻辑(匹配任意一个值);对于 specifications 字段,按维度分组处理。 204 +用于精确匹配或多值匹配。对于普通字段,数组表示 OR 逻辑(匹配任意一个值);对于 specifications 字段,按维度分组处理。**任意字段名加 `_all` 后缀**表示多值 AND 逻辑(必须同时匹配所有值)。
205 205
206 **格式**: 206 **格式**:
207 ```json 207 ```json
208 { 208 {
209 "filters": { 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 // specifications 嵌套过滤(特殊格式) 218 // specifications 嵌套过滤(特殊格式)
217 "specifications": { 219 "specifications": {
218 "name": "color", 220 "name": "color",
@@ -226,9 +228,14 @@ curl -X POST "http://120.76.41.98:6002/search/" \ @@ -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 - 对象:specifications 嵌套过滤(见下文) 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 **Specifications 嵌套过滤**: 239 **Specifications 嵌套过滤**:
233 240
234 `specifications` 是嵌套字段,支持按规格名称和值进行过滤。 241 `specifications` 是嵌套字段,支持按规格名称和值进行过滤。
@@ -286,6 +293,7 @@ curl -X POST "http://120.76.41.98:6002/search/" \ @@ -286,6 +293,7 @@ curl -X POST "http://120.76.41.98:6002/search/" \
286 - `tags`: 标签(keyword类型,支持数组) 293 - `tags`: 标签(keyword类型,支持数组)
287 - `option1_name`, `option2_name`, `option3_name`: 选项名称 294 - `option1_name`, `option2_name`, `option3_name`: 选项名称
288 - `specifications`: 规格过滤(嵌套字段,格式见上文) 295 - `specifications`: 规格过滤(嵌套字段,格式见上文)
  296 +- 以上任意字段均可加 `_all` 后缀表示多值 AND,如 `tags_all`、`category1_name_all`。
289 297
290 #### 3.3.2 范围过滤器 (range_filters) 298 #### 3.3.2 范围过滤器 (range_filters)
291 299
search/es_query_builder.py
@@ -712,7 +712,46 @@ class ESQueryBuilder: @@ -712,7 +712,46 @@ class ESQueryBuilder:
712 }) 712 })
713 continue 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 if isinstance(value, list): 755 if isinstance(value, list):
717 # 多值匹配(OR) 756 # 多值匹配(OR)
718 filter_clauses.append({ 757 filter_clauses.append({