Как оценить сложное дерево выражений по добавочным данным?

У меня есть набор данных и набор поисковых фильтров, которые я хочу использовать для этих данных. Фильтры соответствуют формату фильтра поиска LDAP и анализируются в виде дерева выражений. Данные считываются по одному элементу за раз и обрабатываются всеми фильтрами. Промежуточные результаты сопоставления хранятся в каждом листовом узле дерева до тех пор, пока не будут обработаны все данные. Затем окончательные результаты получаются путем обхода дерева и применения логических операторов к промежуточному результату каждого конечного узла. Например, если у меня есть фильтр (&(a=b)(c=d)), то мое дерево будет выглядеть так:

root = "&"
    left = "a=b"
    right = "c=d"

Итак, если a=b и c=d, то и левый, и правый дочерние узлы совпадают, и, следовательно, фильтр совпадает.

Данные представляют собой набор различных типов объектов, каждый со своими полями. Например, предположим, что коллекция представляет класс в школе:

class { name = "math" room = "12A" }
teacher { name = "John" age = "35" }
student { name = "Billy" age = "6" grade = "A" }
student { name = "Jane" age = "7" grade = "B" }

Таким образом, фильтр может выглядеть как (&(teacher.name=John)(student.age>6)(student.grade=A)) и анализироваться следующим образом:

root = "&"
    left = "teacher.name=John"
    right = "&"
        left = "student.age>6"
        right = "student.grade=A"

Я запускаю против него объект class; нет совпадений. Я запускаю против него объект teacher; root.left соответствует. Я запускаю против него первый узел student; root.right.right совпадает. Я запускаю против него второй узел student; root.right.left совпадает. Затем я просматриваю дерево и определяю, что все узлы совпали, и, таким образом, окончательный результат совпадает.

Проблема заключается в том, что промежуточные совпадения должны быть ограничены на основе общности: фильтры student.age и student.grade необходимо как-то связать вместе, чтобы сохранить промежуточное совпадение, только если они соответствуют одному и тому же объекту. Я не могу на всю жизнь понять, как это сделать.

Мой абстрактный базовый класс узла фильтра:

class FilterNode
{
public:
    virtual void Evaluate(string ObjectName, map<string, string> Attributes) = 0;
    virtual bool IsMatch() = 0;
};

У меня есть класс LogicalFilterNode, который обрабатывает логические операции И, ИЛИ и НЕ; его реализация довольно проста:

void LogicalFilterNode::Evaluate(string ObjectName, map<string, string> Attributes)
{
    m_Left->Evaluate(ObjectName, Attributes);
    m_Right->Evaluate(ObjectName, Attributes);
}

bool LogicalFilterNode::IsMatch()
{
    switch(m_Operator)
    {
    case AND:
        return m_Left->IsMatch() && m_Right->IsMatch();
    case OR:
        return m_Left->IsMatch() || m_Right->IsMatch();
    case NOT:
        return !m_Left->IsMatch();
    }
    return false;
}

Затем у меня есть класс ComparisonFilterNode, который обрабатывает конечные узлы:

void ComparisonFilterNode::Evaluate(string ObjectName, map<string, string> Attributes)
{
    if(ObjectName == m_ObjectName) // e.g. "teacher", "student", etc.
    {
        foreach(string_pair Attribute in Attributes)
        {
            Evaluate(Attribute.Name, Attribute.Value);
        }
    }
}

void ComparisonFilterNode::Evaluate(string AttributeName, string AttributeValue)
{
    if(AttributeName == m_AttributeName) // e.g. "age", "grade", etc.
    {
        if(Compare(AttributeValue, m_AttributeValue) // e.g. "6", "A", etc.
        {
            m_IsMatch = true;
        }
    }
}

bool ComparisonFilterNode::IsMatch() { return m_IsMatch; }

Как это используется:

FilterNode* Root = Parse(...);
foreach(Object item in Data)
{
    Root->Evaluate(item.Name, item.Attributes);
}
bool Match = Root->IsMatch();

По сути, мне нужны операторы AND, в которых дочерние элементы имеют одно и то же имя объекта, оператор AND должен совпадать только в том случае, если дочерние элементы соответствуют одному и тому же объекту.


person Luke    schedule 23.10.2013    source источник
comment
Я только дочитал до Проблема в том, что промежуточные совпадения должны быть ограничены на основе общности до сих пор, но мне кажется, что вы не делаете то, что говорите предложение № 3, данные считываются по одному элементу за раз и обработано всеми фильтрами. Просто сделай это! Вы хотите протестировать каждый элемент коллекции, верно? В этом случае вам не нужно и не нужно сохранять какое-либо состояние между элементами! Вам нужен самый внешний цикл, который выполняет итерацию по каждому элементу в коллекции, применяет к каждому все дерево выражений и получает ответ «да» или «нет», указывающий, следует ли сохранить этот элемент.   -  person j_random_hacker    schedule 23.10.2013
comment
Нет. Фильтр может содержать выражения для нескольких типов элементов. Я читаю только один элемент за раз, который может быть либо teacher, либо student. Такой фильтр, как teacher.name=John AND student.grade=A, никогда не будет соответствовать одному элементу.   -  person Luke    schedule 23.10.2013
comment
Если фильтр может содержать выражения для нескольких типов элементов, то я не понимаю, что вы пытаетесь сделать. Например. откуда вы знаете, какой учитель соответствует каким детям? Фильтрация обычно означает выбор подмножества элементов, где каждый элемент рассматривается независимо от других.   -  person j_random_hacker    schedule 23.10.2013
comment
Учитель и ученик были плохим примером для использования, потому что фактические элементы данных не связаны друг с другом. Инвентаризация различных типов деталей была бы лучшим примером. Фильтр, наверное, тоже неправильное слово. Я хочу определить, существует ли в наборе данных группа элементов, соответствующих критерию.   -  person Luke    schedule 24.10.2013
comment
А, кажется, я понимаю, что ты сейчас пытаешься сделать. Я внесу свое предложение в ответ.   -  person j_random_hacker    schedule 24.10.2013


Ответы (1)


Создайте новый унарный «оператор», назовем его thereExists, который:

  1. Есть состояние и
  2. Объявляет, что его дочернее подвыражение должно удовлетворяться одной входной записью.

В частности, для каждого экземпляра оператора thereExists в дереве выражения вы должны хранить один бит, указывающий, удовлетворяется ли подвыражение под этим узлом дерева какой-либо из входных записей, просмотренных до сих пор. Первоначально эти флаги будут установлены на false.

Чтобы продолжить эффективную обработку вашего набора данных (т. е. входную запись за входной записью, без необходимости загружать весь набор данных в память), вы должны сначала предварительно обработать дерево выражений запроса, чтобы получить список всех экземпляров оператора thereExists. Затем, когда вы читаете каждую входную запись, проверяйте ее на соответствие дочернему подвыражению каждого из этих операторов, флаг satisfied которого по-прежнему установлен на false. Любое подвыражение, которое теперь удовлетворено, должно переключить флаг satisfied его родительского узла thereExists на true — и было бы неплохо также прикрепить копию удовлетворяющей записи к вновь удовлетворенному узлу thereExists, если вы действительно хотите увидеть больше, чем ответ «да» или «нет» на общий вопрос.

Вам нужно всего лишь оценить узлы дерева выше thereExists узла один раз, после того как все входные записи будут обработаны, как описано выше. Обратите внимание, что все, что относится к свойствам отдельной записи, должно появляться где-то под узлом thereExists в дереве. Все, что находится выше узла thereExists в дереве, может тестировать только «глобальные» свойства коллекции или объединять результаты узлов thereExists с помощью логических операторов (И, ИЛИ, XOR, НЕ и т. д.). Сами логические операторы могут появляться в любом месте дерева.

Используя это, теперь вы можете оценивать такие выражения, как

root = "&"
    left = thereExists
        child = "teacher.name=John"
    right = "|"
        left = thereExists
            child = "&"
                left = "student.age>6"
                right = "student.grade=A"
        right = thereExists
            child = "student.name = Billy"

Это сообщит «да», если коллекция записей содержит как учителя по имени «Джон», так и ученика по имени «Билли» или отличника в возрасте старше 6 лет, или «нет» в противном случае. Если вы отследите удовлетворительные записи, как я предложил, вы также сможете вывести их в случае положительного ответа.

Вы также можете добавить второй тип оператора, forAll, который проверяет, что его подвыражение истинно для каждой входной записи. Но это, вероятно, не так полезно, и в любом случае вы можете смоделировать forAll(expr) с помощью not(thereExists(not(expr))).

person j_random_hacker    schedule 23.10.2013
comment
Не могли бы вы просто удалить узлы thereExists и сохранить флаг satisfied в узлах логических операторов (в дополнение к моей текущей реализации m_IsMatch)? В любом случае это потребует организации дерева по типам объектов. В настоящее время синтаксический анализатор является наивным и строит дерево в том порядке, в котором анализируются выражения. Я бы не знал, как нормализовать дерево, если это вообще возможно. - person Luke; 24.10.2013
comment
@Luke: Вы не можете полностью исключить thereExists, потому что, если бы вы это сделали, как бы вы смогли определить, должно ли подвыражение быть истинным для одной записи или его можно сделать истинным для нескольких записей? Например. в моем примере | объединяет подвыражения, охватывающие несколько входных записей, а наиболее вложенное & объединяет подвыражения, которые должны применяться к одной и той же записи. - person j_random_hacker; 24.10.2013
comment
@Luke: Пример того, почему вы не можете это исключить, заключается в том, что существует ученик с оценкой A и возрастом › 6? отличается от запроса Существует ли учащийся с оценкой A и учащийся в возрасте › 6 лет? это разные запросы. Без thereExists как бы вы по-разному кодировали эти 2 запроса? - person j_random_hacker; 24.10.2013
comment
Я все еще думаю о том, как я сейчас их обрабатываю. Вы правы для метода, который вы описали выше. Но проблема все еще остается: как убедиться, что дерево правильно упорядочено? Например, (teacher.name=John AND (student.age>6 AND student.grade=A)) и ((teacher.name=John AND student.age>6) AND student.grade=A) генерируют разные деревья, но имеют логическую эквивалентность. Ваш метод не будет работать во втором случае, поскольку элемент ученика не будет соответствовать выражению учителя. - person Luke; 24.10.2013
comment
@Luke: 2 запроса, которые вы дали в своем последнем комментарии, логически эквивалентны, потому что логический оператор AND является ассоциативным: x AND (y AND z) = (x AND y) AND z всегда для всех возможных значений x, y и z. Любая правильно реализованная система запросов должна давать на них идентичные ответы. - person j_random_hacker; 24.10.2013
comment
@Luke: Перечитывая ваш последний комментарий, я не уверен, о чем вы спрашиваете. Ваш текущий способ не работает, потому что он не может выразить два разных запроса в моем втором комментарии к этому ответу. Вам нужно сделать его способным выражать эти 2 запроса, и один из способов сделать это — использовать мой оператор thereExists. Могут быть и другие способы, но что не так с тем, что я предлагаю? - person j_random_hacker; 24.10.2013
comment
Предположим, у вас есть дерево вида ((teacher.name=John AND student.age>6) AND student.grade=A). Предположим, у вас есть ввод формы teacher { name = John }, student { age = 0, grade = A }, student { age = 10, grade = B }. Возможно, я что-то неправильно понимаю, но, пройдя через логику, кажется, что ваше решение подскажет мне, что есть совпадение (учитель с именем = Джон и ученик с возрастом › 6 и оценкой = A), хотя на самом деле это не так. - person Luke; 24.10.2013
comment
@Luke: это дерево недействительно в моей схеме (т. е. оно должно быть отклонено с сообщением об ошибке), потому что (как я сказал в своем ответе) оно содержит подвыражение, которое ссылается на свойство отдельной записи (например, teacher.name) и который не содержится в подвыражении thereExists. - person j_random_hacker; 24.10.2013
comment
давайте продолжим обсуждение в чате - person Luke; 24.10.2013