オンラインストアのファセットは、最初のフィルターを選択するまではシンプルに見えます。

カタログでは、それらはナビゲーションの一部です。選択した色がリストから消えてはなりません。他の色は引き続き利用可能であるべきです。ユーザーは他の色に切り替えたり、選択を広げたりしたいかもしれません。商品に一致しないオプションは、利用できないものとして表示する方がより役立ちます。単一のファセット内では、通常 OR が適用されます:赤または青です。異なるファセット間では、AND です:ブランド、色、サイズを同時に。

最初の選択以降が難しい部分になります。ユーザーがブランド、色、サイズを選択した商品カタログを想像してください。主要な結果セットは狭く保たれ、3つの条件すべてに一致する商品のみを表示する必要があります。しかし、フィルターパネルは異なるルールに従います。選択された色を保持しつつ、同じブランドとサイズの下でユーザーが切り替え可能な色も表示する必要があります。

すべてのファセットがメインクエリからすべてのフィルターを継承する場合、すぐにすでに選択された値に縮小してしまいます。すべてのファセットのフィルターを手動で再構築する必要がある場合、色、サイズ、ブランド、在庫状況、販売者、その他のすべての属性ごとに個別のクエリブランチができてしまいます。

Manticore Search 25.12.0 で facet_filter_mode が導入され、この動作がファセット API に移動しました。アプリケーションがほぼ同一のクエリをすべてのファセット用に手動で構築することなく、クエリでストアのフィルターパネルの動作を記述できるようになりました。

eコマースのファセットに必要なもの

フィルターパネルでは、「この値を持つ商品は何件か?」に答えるだけでは不十分です。インターフェースが直接使用できるステータスが必要です:

  • 選択された値は表示されたままになり、すぐにクリアできること;
  • 同じファセット内の他の値は、IN (...) を通じてフィルターを広げることで利用可能なままになること;
  • 他のファセット内の値は、それらを選択した場合に少なくとも1つの商品が返されるかどうかを示すこと;
  • カウントは予測可能であること:インターフェースは数値が現在の結果セットを指すのか、より広い選択を指すのかを知ること;
  • 長いブランドリスト、カテゴリ、価格帯、SEOルール、クエリ分析内の検索は依然として重要であり、検索設計の別の部分であること。

facet_filter_mode はこの問題の最も一般的な部分を処理します:ユーザーがすでに複数のフィルターを選択した後のファセットの再計算です。

変更点

ファセットには3つのフィルター継承モードがあります:

モード 動作
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 は「この特定のフィルターを変更するとどうなるか?」に答え、maxstatus フィールド(selectedavailable、または unavailable)付きの幅広いバケットリストを表示します。

最小例

ブランド、色、サイズ、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

フルスクリーンモードに入る フルスクリーンモードを終了する

これらの条件の下で、結果セットには1つの商品 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_idbrand_id=7 AND size_id=10 を適用しましたが、color_id=1 は適用しませんでした;
  • FACET size_idbrand_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 はカタログ全体で3つの商品に存在するため、max でのカウントは 3 です。これはフィルターにすでに参加しているため selected とマークされます。他の色は available とマークされます:ユーザーがそのうちの1つを選択した場合、色フィルターはより広くなり、例えば 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 |
+---------+----------+-----------+

フルスクリーンモードに入る フルスクリーンモードを終了する

インターフェースは、選択された small と並んで largexlarge を表示できます:数値は現在の厳密な結果セットを指しますが、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 のフィルターがアクティブで、結果セットは厳密なままですが、ファセットは自身のフィルターが除外された場合に表示されるオプションを表示する必要があります。

エンジン 1つのクエリで自身のフィルターを除外 ネイティブバケットステータス アプリケーションに残るもの
Manticore Search はい、auto/max を通じて はい、auto/max で;unavailablemax のみ インターフェースのレンダリング、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=10 のみです。代替の color_id=2size_id=20 の値はこのレスポンスには表示されません。インターフェースがそれらを必要とする場合、アプリケーションは追加のクエリを実行して結果をマージする必要があります。

Elasticsearch と OpenSearch では、似た動作を1つのクエリで構築できますが、各ファセットに独自の明示的な集約ブランチが必要です。color の場合は brand_idsize_id を保持し、size の場合は brand_idcolor_id を保持し、以下同様です。これは動作しますが、クエリはすぐに大きくなり、バケットステータスは依然としてアプリケーションで計算する必要があります。

主な違いは、このロジックがどこに存在するかです。Manticore Search ではファセット API で直接設定されますが、他のシステムでは通常、いくつかの類似したフィルターツリーから組み立てられます。

パフォーマンスと制限

strict はデフォルトのままですべての以前の動作を保持します。現在の結果セット内でのみファセットが必要な場合、何も変更する必要はありません。

auto は通常 eコマースフィルターにより適しています:各ファセット内の代替値を表示し、選択された値を selected とマークし、他の値を available とマークします。

インターフェースが現在の結果セットよりも幅広い値リストを必要とし、利用不可のオプションを表示する必要がある場合に max を使用してください。このモードはコストが高くなります:Manticore Search は広範なスコープに対してバケットを計算し、次にそれらの status を個別に決定します。これは大規模なデータセットと多数のファセットがある場合に考慮する価値があります。

いくつかの制限もあります:

  • ファセットのローカルフィルター書き換えは、AND で結合された属性フィルターのみをサポートします;
  • 複雑な AND/OR ツリーは個別のファセットに対して自動的に書き換えられません;
  • 選択された値は現在、=IN のような明示的な値フィルターに対してのみ確実に一致します;
  • ステータスは価格などの範囲に対してはまだ自動的に計算されません。

価格、割引、評価については、範囲を明示的に定義する方が良いです。例えば、ソートとスライダー用に数値フィールドを保持し、ファセット用に price_band または discount_band を追加します。これにより、インターフェースは事前定義された範囲に対して明確なステータスを表示できます。

ファセットURLのSEO、A/Bテスト、マーチャンダイジングルール、クエリ分析は、検索を取り巻くアプリケーションまたはプラットフォームの責任のままです。

実際には、代替値が必要な場合は auto から始め、インターフェースが selectedavailableunavailable のステータス付きの幅広いバケットリストを必要とする場合は max に移行してください。

参考文献

Manticore Search でのファセット検索の入門には、以前の Faceted search の記事から始めることをおすすめします。基本的な FACET クエリ、ソート、制限、式に対するファセットについて説明しています。

詳細については、FACET ドキュメント を参照してください。

ファセットのインタラクティブな入門には、Manticore Faceting コースを受講してください。