商品分類システムにおける複雑な分類条件(AND/OR)のDB設計

商品分類システムにおける複雑な条件式(AND/OR混在)をDB設計とバックエンドでどう扱うか

商品分類システムを開発しているのですが、特徴による分類のために「AかつB」「AまたはB」「(AまたはB)かつC」など、AND/ORが混在した複雑な特徴を登録する必要が出てきました。 本記事では、こうした複雑な条件式をデータベース設計レベルでどのように実装したのかについて解説します。

1. なぜAND/OR混在の条件式が必要なのか?

例えばロレックスの時計モデル分類で説明すると、

  • 「文字盤がブラックまたはブルー」かつ「ジュビリーブレスレット」
  • 「ベゼルがフルーテッドベゼルまたはスムースベゼル」かつ「限定モデル」

など、複数属性に対してOR条件を組み合わせてANDで判定したいケースが多くあります。

2. DB設計:条件式を柔軟に表現する

従来の設計

従来は「variation_property_conditions」テーブルに条件をフラットに登録し、すべてAND判定していました。

variation_property_conditions
----------------------------
variation_id | property_key_id | property_value_id | value
----------------------------------------------------------
1            | dial           | black            | NULL
1            | bracelet       | jubilee          | NULL

AND/OR対応の拡張

複雑な条件式を扱うために、operator(AND/OR)カラムやgroup_idカラムを追加します。

  • operator: この条件がANDかORかを指定
  • group_id: OR条件をグループ化し、(A or B) and (C or D) のような式を表現

例:(黒文字盤 OR 青文字盤) AND (ジュビリーブレスレット OR オイスターブレスレット) AND フルーテッドベゼル

variation_property_conditions
---------------------------------------------------------------
variation_id | property_key_id | property_value_id | value | operator | group_id
-----------------------------------------------------------------------------
1            | dial           | black            | NULL  | OR       | 1
1            | dial           | blue             | NULL  | OR       | 1
1            | bracelet       | jubilee          | NULL  | OR       | 2
1            | bracelet       | oyster           | NULL  | OR       | 2
1            | bezel          | fluted           | NULL  | AND      | 3

3. 分類ロジック設計:条件式の判定ロジック

バックエンド側では、DBから取得した条件をgroup_idごとにグループ化し、 operatorに応じて判定します。

  • ANDグループは全て一致
  • ORグループは1つでも一致すればOK
  • すべてのグループの判定結果をANDでまとめることで、複雑な論理式も表現可能
from collections import defaultdict

def match_variation(variation_id, classification_result, db):
    conds = db.query(...).filter(...).all()
    groups = defaultdict(list)
    for cond in conds:
        groups[cond.group_id].append(cond)

    for group_id, cond_list in groups.items():
        operator = cond_list[0].operator
        results = [is_condition_matched(cond, classification_result, db) for cond in cond_list]
        if operator == "AND" and not all(results):
            return False
        if operator == "OR" and not any(results):
            return False
    return True

4. まとめ

実は実際のプロダクトでは現状group_idは導入しておらず、ORグループは1つしか使えない設計です。 これは

  1. 実用の上でORで判断したいグループが複数必要な場合がほぼ無いこと
  2. 運用上、複数のORグループに対応してもユーザー目線で管理画面が煩雑になるデメリットの方が大きい

というのが主な理由です。








5. さらなる高みへ

じつは、3で説明したoperatorとgroup_idの運用では、「入れ子の括弧がある条件式(例:(A or (B and C)) and D)」には対応できていません。

さらに複雑な条件をDBで管理するにはdepthなどの概念を持ち込む必要がありそうですが、今回の開発には必要ないので、また別の機会に考えることにします。