优化简单的规则匹配逻辑

Optimise simple rule-matching logic

提问人:jon hanson 提问时间:9/16/2023 最后编辑:jon hanson 更新时间:9/16/2023 访问量:66

问:

我有一个简单的算法用于一些规则匹配逻辑,想看看是否有办法优化它。

问题如下 - 我有一组规则,我需要确定每个传入记录的规则(如果有)与记录匹配。该记录实质上是字符串属性到字符串值的映射。该规则由整数 ID 和条件列表组成。每个条件都有一个属性、一个运算符(IN 或 NOT_IN)和一组字符串值。

因此,示例规则可能如下所示:

{
    id: "1",
    conditions: [
        {
            attribute: "firstname",
            operator: "IN",
            values: ["Jon", "Fred"]
        },{
            attribute: "surname",
            operator: "NOT_IN",
            values: ["Smith"]
        }
    ]
}

上述规则将匹配 record.get(“firstname”) 为 “Jon” 或 “Fred”,而 record.get(“surname”) 为 null 或不是 “Smith” 的任何记录。

应用于单条记录时的规则匹配逻辑如下:

  1. 遍历每个规则:
    1. 循环访问规则的每个条件:
      1. 从记录属性映射中查找条件属性。
      2. 如果条件运算符为 IN,则应用以下检查:
        1. 如果映射包含该属性,并且查找值在条件值列表中,则条件将通过,否则如果失败。
      3. 否则,如果条件运算符NOT_IN则应用以下检查:
        1. 如果映射不包含该属性,或者查找值不在条件值列表中,则条件将通过,否则将失败。
    2. 如果所有条件都通过,则该规则是匹配项,并且不考虑其他规则。

如果不清楚,则列表中规则的顺序很重要,因为将选择匹配项的第一条规则。

我在下面包含了一些实现 Rules 数据模型和上述算法的 Java 代码。

上述逻辑实现起来相对简单,但它涉及遍历每条记录的规则。我的问题是是否有更有效的算法。

更多数据点:

  1. 这些规则基本上是静态的。
  2. 大约有 20 亿条记录需要处理。
  3. 这些记录将分批到达,大小不一,但通常为 100,000 - 1,000,000。
  4. 这些属性是预先知道的 - 总共大约有 500 个,但规则只使用相对较小的子集 - 可能不超过 20 个。
  5. 属性值通常不是事先知道的。
  6. 每个条件通常包含少量值 - 通常不超过 6 个。
  7. 组成规则的条件将分别使用不同的属性 - 即,您永远不会获得具有两个或多个条件使用相同属性的规则。
  8. 构成规则的条件数量通常很少,通常不超过 4 个。

为了优化算法,我研究了将规则预编译为数据结构的方法,这可能会加快规则处理速度。起初,我认为决策树结构可能更理想,但目前尚不清楚如何构建决策树结构,因为每个规则可能都在测试不同的属性集。我想到的另一种方法是将每个规则编码为数字,可能是位图或哈希码。

import java.util.*;

public class RulesTest {
    
    // Data model.

    enum Operator {IN, NOT_IN}

    record Condition(String attribute, Operator operator, Set<String> values) {}

    record Rule(int id, List<Condition> conditions) {}

    // Test data.
    
    private static final List<Rule> TEST_RULES = List.of(
            new Rule(
                    1,
                    List.of(
                            new Condition("B", Operator.IN, Set.of("b1", "b2")),
                            new Condition("C", Operator.IN, Set.of("c2", "c4"))
                    )
            ), new Rule(
                    2,
                    List.of(
                            new Condition("A", Operator.IN, Set.of("a1", "a3")),
                            new Condition("C", Operator.NOT_IN, Set.of("c1", "c4")),
                            new Condition("D", Operator.IN, Set.of("d1", "d2", "d3"))
                    )
            ), new Rule(
                    3,
                    List.of(
                            new Condition("B", Operator.NOT_IN, Set.of("b1", "b4")),
                            new Condition("D", Operator.NOT_IN, Set.of("d1", "d2")),
                            new Condition("E", Operator.IN, Set.of("e1"))
                    )
            )
    );

    // Matching logic.
    private static OptionalInt match(List<Rule> rules, Map<String, String> attrValues) {
        for (Rule rule : rules) {
            final boolean pass = rule.conditions
                    .stream()
                    .allMatch(condition -> {
                        final String attrValue = attrValues.get(condition.attribute);
                        return (attrValue != null && condition.values().contains(attrValue))
                                == (condition.operator == Operator.IN);
                    });
            if (pass) {
                return OptionalInt.of(rule.id);
            }
        }

        return OptionalInt.empty();
    }

    private static void test(List<Rule> rules, Map<String, String> attrValues) {
        final OptionalInt optRuleId = match(rules, attrValues);

        System.out.println(attrValues + " -> " + optRuleId);
    }

    public static void main(String[] args) {
        {
            final Map<String, String> attrValues = Map.of(
                    "A", "a1",
                    "B", "b1",
                    "C", "c1"
            );

            test(TEST_RULES, attrValues);
        }

        {
            final Map<String, String> attrValues = Map.of(
                    "B", "b2",
                    "C", "c2"
            );

            test(TEST_RULES, attrValues);
        }

        {
            final Map<String, String> attrValues = Map.of(
                    "A", "a3",
                    "C", "c3",
                    "D", "d3"
            );

            test(TEST_RULES, attrValues);
        }
    }
}
java 匹配 规则引擎

评论

1赞 QBrute 9/16/2023
如果你的代码有效,但你只是在寻找优化它的方法,那么这个问题可能更适合 codereview.stackexchange.com
0赞 Amit Jain 9/16/2023
属性值如何加载到内存中?它是否根据某些条件动态刷新?
0赞 jon hanson 9/16/2023
@AmitJain 属性值映射由各种扩充逻辑以增量方式填充在内存中。
1赞 pacmaninbw 9/16/2023
@QBrute 此代码是演示代码,在代码审查中会被认为过于理论化。

答:

0赞 Amit Jain 9/16/2023 #1

我们可以遍历属性,而不是遍历规则,并只运行那些检查特定属性的规则。

如果为可应用于 attribute() 的规则构建映射,则可以稍微提高性能。attributeRuleMap

如下所示

class Rule {
    int id;
    Map<String, Condition> conditionsMap = new HashMap<>();

    public Rule(int id, List<Condition> conditions) {
        this.id = id;
        conditions.forEach(condition -> {
            attributeRuleMap.computeIfAbsent(condition.attribute, x -> new ArrayList<>()).add(id);
            conditionsMap.put(condition.attribute, condition);
        });
        ruleMap.put(id, this);
    }
}

conditionsMap仅用于访问我们正在检查规则的特定属性的条件。

那么你的匹配函数可以如下所示

private static List<Integer> match(Map<String, String> attrValues) {
    List<Integer> rules = new ArrayList<>();
    attrValues.forEach((key, value) ->
                           attributeRuleMap.get(key)
                                           .forEach(rule -> {
                                                        Condition condition = ruleMap.get(rule).conditionsMap.get(key);
                                                        if (condition.values.contains(value) && condition.operator == Operator.IN) {
                                                            rules.add(rule);
                                                        }
                                                    }
                                           ));
    return rules;

}

评论

0赞 jon hanson 9/17/2023
Not sure what your code is doing. I don't think it would work, and even if it did I don't think it would be faster than the simple algorithm.