在线商店的分面在首次选择筛选条件前看似简单。

在目录中,它们是导航的一部分。已选颜色不应从列表中消失。其他颜色应保持可用:用户可能想切换到其他颜色或扩大选择范围。没有匹配商品的选项在显示为不可用时更有用。在单个分面内,通常应用 OR:红色或蓝色。跨不同分面,则是 AND:同时按品牌、颜色和尺寸筛选。

首次选择后的部分开始变得复杂。想象一个产品目录,用户已选择了品牌、颜色和尺寸。主结果集必须保持窄范围,仅显示同时匹配这三个条件的产品。但筛选面板遵循不同规则。它需要保留已选颜色,同时显示在相同品牌和尺寸下可切换的颜色。

如果每个分面继承主查询的所有筛选条件,它会迅速退化为已选值。如果每个分面的筛选条件都需手动重建,最终会出现为颜色、尺寸、品牌、可用性、卖家以及每个其他属性创建单独查询分支的情况。

Manticore Search 25.12.0 引入了 facet_filter_mode,将此行为移入分面 API。现在查询可以描述商店筛选面板应如何行为,而无需应用为每个分面手动构建几乎相同的查询。

电商分面需要什么

对于筛选面板,回答“有多少产品有此值?”是不够的。它需要界面可直接使用的状态:

  • 已选值保持可见且可快速清除;
  • 同一分面中的其他值保持可用,因为它们通过 IN (...) 扩大筛选范围;
  • 其他分面中的值指示选择它们是否会返回至少一个产品;
  • 计数保持可预测:界面知道数字是指当前结果集还是更宽的选择范围;
  • 在长品牌列表、类别、价格范围、SEO 规则和查询分析中搜索仍然重要;它们是搜索设计的独立部分。

facet_filter_mode 处理此问题最常见的部分:在用户已选择多个筛选条件后重新计算分面。

变更内容

分面现在有三种筛选继承模式:

模式 行为
strict 将主查询的所有筛选条件应用于分面。这是之前的行为,保持为默认值。
auto 应用除该分面自身筛选条件外的所有筛选条件,并添加 status:已选值获得 selected,其他值获得 available
max 在宽泛的基础结果集上计算桶,并添加 status 以区分已选、可用和不可用的值。

你也可以手动控制:

  • ALL FILTERS - 将所有筛选条件应用于分面;
  • FILTERS color_id, size_id - 仅应用列出的筛选条件;
  • EXCLUDE FILTERS color_id - 应用除列出条件外的所有筛选条件;
  • ZEROES - 从 Manticore Search 27.3.0 开始,在 SQL max 模式下,即使当前分面的 count(*)0,也保留宽泛 max 范围中的桶。

相同选项在 JSON API 中通过 facet_filter_modemodefiltersexclude_filterszeroes: true 可用。

简而言之,strict 回答“当前结果集中有什么?”,auto 回答“如果此特定筛选条件改变会怎样?”,max 显示带有 status 字段的宽泛桶列表:selectedavailableunavailable

最小示例

考虑一个包含品牌、颜色、尺寸和 SKU 的小型目录:

CREATE TABLE products(
  title text,
  brand_id int,
  color_id int,
  color_name string,
  size_id int,
  size_name string,
  sku string
);

INSERT INTO products(id,title,brand_id,color_id,color_name,size_id,size_name,sku) VALUES
(1,'p1',7,1,'red',10,'small','sku1'),
(2,'p2',7,1,'red',20,'large','sku2'),
(3,'p3',7,2,'blue',10,'small','sku3'),
(4,'p4',7,3,'green',30,'xlarge','sku4'),
(5,'p5',8,1,'red',10,'small','sku5'),
(6,'p6',8,4,'black',20,'large','sku6'),
(7,'p7',9,5,'white',10,'small','sku7');

进入全屏模式 退出全屏模式

用户已选择:

brand_id=7 AND color_id=1 AND size_id=10

进入全屏模式 退出全屏模式

在这些条件下,结果集包含一个产品:p1。让我们看看分面会发生什么。

strict:之前的行为

在没有额外选项的情况下,Manticore Search 使用 strict:每个分面接收主查询的所有筛选条件。

SELECT count(*)
FROM products
WHERE brand_id=7 AND color_id=1 AND size_id=10
LIMIT 0
FACET color_id ORDER BY color_id ASC
FACET size_id ORDER BY size_id ASC;

进入全屏模式 退出全屏模式

响应:

+----------+----------+
| color_id | count(*) |
+----------+----------+
|        1 |        1 |
+----------+----------+
+---------+----------+
| size_id | count(*) |
+---------+----------+
|      10 |        1 |
+---------+----------+

进入全屏模式 退出全屏模式

这是一个准确的 SQL 响应,但对于在线商店通常过于狭窄。用户只看到已选的 color_id=1size_id=10。看起来好像没有其他选项,尽管数据包含同一品牌和尺寸但不同颜色(color_id=2)的产品,以及同一品牌和颜色但不同尺寸(size_id=20)的另一个产品。

auto:分面忽略自身筛选条件

auto 模式保留除当前计算的分面筛选条件外的所有筛选条件。

SELECT count(*)
FROM products
WHERE brand_id=7 AND color_id=1 AND size_id=10
LIMIT 0
OPTION facet_filter_mode='auto'
FACET color_id ORDER BY color_id ASC
FACET size_id ORDER BY size_id ASC;

进入全屏模式 退出全屏模式

响应:

+----------+----------+-----------+
| color_id | count(*) | status    |
+----------+----------+-----------+
|        1 |        1 | selected  |
|        2 |        1 | available |
+----------+----------+-----------+
+---------+----------+-----------+
| size_id | count(*) | status    |
+---------+----------+-----------+
|      10 |        1 | selected  |
|      20 |        1 | available |
+---------+----------+-----------+

进入全屏模式 退出全屏模式

发生了什么:

  • FACET color_id 应用了 brand_id=7 AND size_id=10,但未应用 color_id=1
  • FACET size_id 应用了 brand_id=7 AND color_id=1,但未应用 size_id=10

这为界面提供了替代值,而无需为每个分面单独查询。已选桶标记为 selected,同一分面中的其他值标记为 available:它们可以通过 IN (...) 作为更宽的选择添加到当前筛选条件中。

max:带有状态的宽泛桶列表

auto 仅显示特定分面当前筛选范围内的值。max 更进一步:在基础结果集上计算桶,并为界面单独标记其状态。

SELECT count(*)
FROM products
WHERE brand_id=7 AND color_id=1 AND size_id=10
LIMIT 0
OPTION facet_filter_mode='max'
FACET color_id ORDER BY color_id ASC
FACET size_id ORDER BY size_id ASC;

进入全屏模式 退出全屏模式

响应:

+----------+----------+-----------+
| color_id | count(*) | status    |
+----------+----------+-----------+
|        1 |        3 | selected  |
|        2 |        1 | available |
|        3 |        1 | available |
|        4 |        1 | available |
|        5 |        1 | available |
+----------+----------+-----------+
+---------+----------+-----------+
| size_id | count(*) | status    |
+---------+----------+-----------+
|      10 |        4 | selected  |
|      20 |        2 | available |
|      30 |        1 | available |
+---------+----------+-----------+

进入全屏模式 退出全屏模式

status 直接来自 Manticore Search:

  • selected - 该值已存在于此分面的筛选条件中;
  • available - 该值可以被选择;对于同一分面中的另一个值,这通过 IN (...) 扩大筛选范围;
  • unavailable - 该值存在于分面的宽泛结果集中,但选择它在当前筛选条件下不会返回任何文档。

在此示例中,color_id=1 在目录中出现在三个产品中,因此其在 max 中的计数为 3。它被标记为 selected,因为它已参与筛选。其他颜色被标记为 available:如果用户选择其中一个,颜色筛选会变得更宽,例如 color_id IN (1,2)。当宽泛结果集中的桶在当前筛选条件下无法返回文档时,会出现不可用值;下面的 sku 示例演示了这种情况。

手动设置筛选范围

全局 facet_filter_mode 覆盖了大多数常见情况,但有时不同分面需要不同行为。例如,颜色可以严格受所有筛选条件约束,尺寸可以使用 max,SKU 可以仅使用颜色和尺寸计算,品牌可以在没有颜色筛选条件的情况下计算。

SELECT count(*)
FROM products
WHERE brand_id=7 AND color_id=1 AND size_id=10
LIMIT 0
OPTION facet_filter_mode='max'
FACET color_id ALL FILTERS ORDER BY color_id ASC
FACET size_id ORDER BY size_id ASC
FACET sku FILTERS color_id, size_id ORDER BY sku ASC
FACET brand_id EXCLUDE FILTERS color_id ORDER BY brand_id ASC;

进入全屏模式 退出全屏模式

响应:

+----------+----------+-----------+
| color_id | count(*) | status    |
+----------+----------+-----------+
|        1 |        1 | selected  |
+----------+----------+-----------+
+---------+----------+-----------+
| size_id | count(*) | status    |
+---------+----------+-----------+
|      10 |        4 | selected  |
|      20 |        2 | available |
|      30 |        1 | available |
+---------+----------+-----------+
+------+----------+-------------+
| sku  | count(*) | status      |
+------+----------+-------------+
| sku1 |        1 | available   |
| sku5 |        1 | unavailable |
+------+----------+-------------+
+----------+----------+----------+
| brand_id | count(*) | status   |
+----------+----------+----------+
|        7 |        2 | selected |
+----------+----------+----------+

进入全屏模式 退出全屏模式

如何阅读此查询:

  • FACET color_id ALL FILTERS 应用所有筛选条件并仅返回已选颜色;
  • FACET size_id 继承查询级别的 max 模式;
  • FACET sku FILTERS color_id, size_id 仅应用颜色和尺寸筛选条件;
  • FACET brand_id EXCLUDE FILTERS color_id 应用除颜色外的所有筛选条件。

当筛选面板同时包含常规和技术分面,且某些值需要根据特殊规则计算时,此模式很有用。

ZEROES:max 中的零计数桶

当计数需要保持严格但值列表需要保持宽泛时,ZEROES 很有用。它与 max 配合使用,无论是通过 OPTION facet_filter_mode='max' 还是通过单个分面上的 MODE max

在没有 ZEROES 的情况下,带有 ALL FILTERS 的分面仅显示通过所有筛选条件的桶:

SELECT count(*)
FROM products
WHERE brand_id=7 AND color_id=1 AND size_id=10
LIMIT 0
OPTION facet_filter_mode='max'
FACET size_id ALL FILTERS ORDER BY size_id ASC;

进入全屏模式 退出全屏模式

响应:

+---------+----------+----------+
| size_id | count(*) | status   |
+---------+----------+----------+
|      10 |        1 | selected |
+---------+----------+----------+

进入全屏模式 退出全屏模式

使用 ZEROES,Manticore Search 保持相同的可见计数,但从宽泛的 max 范围返回剩余桶,计数为零:

SELECT count(*)
FROM products
WHERE brand_id=7 AND color_id=1 AND size_id=10
LIMIT 0
OPTION facet_filter_mode='max'
FACET size_id ALL FILTERS ZEROES ORDER BY size_id ASC;

进入全屏模式 退出全屏模式

响应:

+---------+----------+-----------+
| size_id | count(*) | status    |
+---------+----------+-----------+
|      10 |        1 | selected  |
|      20 |        0 | available |
|      30 |        0 | available |
+---------+----------+-----------+

进入全屏模式 退出全屏模式

然后界面可以显示 largexlarge 与已选的 small 一起:数字是指当前严格结果集,而 status 显示这些值仍然可以被选择以扩大筛选范围。

通过 JSON API 的相同查询

SQL 在这里更短,更清楚地显示了机制,但 JSON API 支持相同的方法:

POST /search
{
  "table": "products",
  "limit": 0,
  "query": {
    "bool": {
      "must": [
        { "equals": { "brand_id": 7 } },
        { "equals": { "color_id": 1 } },
        { "equals": { "size_id": 10 } }
      ]
    }
  },
  "facet_filter_mode": "max",
  "aggs": {
    "colors": {
      "terms": { "field": "color_id", "size": 10 },
      "sort": [ { "color_id": { "order": "asc" } } ]
    },
    "sizes": {
      "terms": { "field": "size_id", "size": 10 },
      "sort": [ { "size_id": { "order": "asc" } } ]
    }
  }
}

进入全屏模式 退出全屏模式

响应:

{
  "took": 0,
  "timed_out": false,
  "hits": {
    "total": 1,
    "total_relation": "eq",
    "hits": []
  },
  "aggregations": {
    "colors": {
      "buckets": [
        { "key": 1, "doc_count": 3, "status": "selected" },
        { "key": 2, "doc_count": 1, "status": "available" },
        { "key": 3, "doc_count": 1, "status": "available" },
        { "key": 4, "doc_count": 1, "status": "available" },
        { "key": 5, "doc_count": 1, "status": "available" }
      ]
    },
    "sizes": {
      "buckets": [
        { "key": 10, "doc_count": 4, "status": "selected" },
        { "key": 20, "doc_count": 2, "status": "available" },
        { "key": 30, "doc_count": 1, "status": "available" }
      ]
    }
  }
}

进入全屏模式 退出全屏模式

在 JSON 聚合中,modefiltersexclude_filterszeroes 也可以为每个单独的聚合设置。

这与 Meilisearch、Elasticsearch 和 OpenSearch 的区别

我们在 Manticore Search、Meilisearch、Elasticsearch 和 OpenSearch 中测试了相同场景:brand_id=7color_id=1size_id=10 筛选条件处于活动状态;结果集保持严格,而分面需要显示排除自身筛选条件时出现的选项。

引擎 在单个查询中排除自身筛选条件 原生桶状态 应用仍需处理的内容
Manticore Search 是的,通过 auto/max 是的,在 auto/max 中;unavailable 仅在 max 渲染界面、SEO、分析和自定义规则。
Meilisearch 不是这种形式 运行额外查询并在应用中组装分面。
OpenSearch 是的,但需通过 global + filter 聚合手动操作 复制筛选分支并在应用中计算 status
Elasticsearch 是的,但需通过 global + filter 聚合手动操作 相同:维护单独的聚合分支和客户端逻辑。

在 Meilisearch 中使用基本分面很容易,但类似这样的查询:

{
  "filter": ["brand_id = 7", "color_id = 1", "size_id = 10"],
  "facets": ["color_id", "size_id"]
}

进入全屏模式 退出全屏模式

返回已筛选结果集的计数。在我们的数据集中,这意味着只有 color_id=1size_id=10color_id=2size_id=20 的替代值不会出现在此响应中。如果界面需要它们,应用必须发出额外查询并合并结果。

在 Elasticsearch 和 OpenSearch 中,类似行为可以在一个查询中构建,但每个分面需要自己的显式聚合分支。对于 color,保留 brand_idsize_id;对于 size,保留 brand_idcolor_id;依此类推。这样可行,但查询会快速增长,且桶状态仍需在应用中计算。

主要区别在于此逻辑的位置。在 Manticore Search 中,它直接在分面 API 中配置;在其他系统中,它通常由几个相似的筛选树组装而成。

性能和限制

strict 保持为默认值并保留之前的行为。如果你只需要当前结果集内的分面,无需更改任何内容。

auto 通常更适合电商筛选条件:它显示每个分面内的替代值,将已选值标记为 selected,并将其他值标记为 available

当界面需要比当前结果集更宽的价值列表并需要显示不可用选项时,使用 max。此模式成本更高:Manticore Search 在宽泛范围内计算桶,然后单独确定它们的 status。在大型数据集和多个分面时值得考虑。

还有一些限制:

  • 分面的本地筛选重写仅支持与 AND 组合的属性筛选条件;
  • 复杂的 AND/OR 树不会为单个分面自动重写;
  • 已选值目前仅对显式值筛选条件(如 =IN)可靠匹配;
  • 状态尚未为价格等范围自动计算。

对于价格、折扣和评分,最好明确定义单独的范围。例如,保留一个用于排序和滑块的数值字段,并添加 price_banddiscount_band 作为分面。这让界面可以为预定义范围显示清晰的状态。

分面 URL 的 SEO、A/B 测试、商品规划规则和查询分析仍由应用或搜索平台负责。

实际上,当你需要替代值时从 auto 开始,当界面需要带有 selectedavailableunavailable 状态的宽泛桶列表时切换到 max

进一步阅读

要了解 Manticore Search 中的分面搜索入门,请从早期的 分面搜索 文章开始。它涵盖基本的 FACET 查询、排序、限制和表达式分面。

有关详细信息,请参阅 FACET 文档

要获得分面的交互式介绍,请参加 Manticore 分面 课程。