Необходима базовая структура данных: инвертированный индекс .Это сопоставляет каждое слово с набором документов, которые его содержат.Допустим, lookup
- это функция от слов к наборам документов: lookup: W -> Pos(D)
(где W
- набор слов, D
набор документов, Pos(D)
набор мощности D
).
Допустим, у вас есть выражение запроса в форме:
Expr ::= Word | AndExpression | OrExpression
AndExpression ::= Expr 'AND' Expr
OrExpression ::= Expr 'OR' Expr
Итак, вы получите абстрактное синтаксическое дерево, представляющее ваш запрос.Это дерево со следующими видами узлов:
abstract class Expression { }
class Word extends Expression {
String word
}
class AndExpression extends Expression {
Expression left
Expression right
}
class OrExpression extends Expression {
Expression left
Expression right
}
Например, foo AND (bar OR baz)
будет переведено в это дерево:
AndExpression
/ \
/ \
Word('foo') OrExpression
/ \
/ \
Word('bar') Word('baz')
Чтобы оценить это дерево, следуйте этим простым правилам, выраженный в псевдокоде:
Set<Document> evaluate(Expr e) {
if (e is Word)
return lookup(e.word)
else if (e is AndExpression)
return intersection(evaluate(e.left), evaluate(e.right))
else if (e is OrExpression)
return union(evaluate(e.left), evaluate(e.right))
//otherwise, throw assertion error: no other case remaining
}
//implemented by the inverted index, not shown
Set<Document> lookup(String word)
Таким образом, выражения AND
в основном транслируются для установки пересечений, в то время как выражения OR
переводятся в выражения объединения, все оцениваются рекурсивно.Я уверен, что если вы посмотрите достаточно долго на вышесказанное, вы увидите его красоту:)
Вы можете представить каждый набор (который возвращает lookup
) как HashSet.Если вы используете Java, вы также можете использовать реализации ленивых union и intersection в guava, что должно быть весело (особенно если вы изучаете код или используете свое воображение, чтобы увидеть, что на самом деле 'lazy')значит в этом контексте).
Насколько я знаю, пересечения редко вычисляются путем пересечения хеш-таблиц - вместо этого обычно происходит следующее: предположим, что 3 набора должны быть пересечены, мы выбираем один (наименьший предпочтительно) и назначаемсчетчик (равный 1) каждому из документов.Затем мы повторяем другие наборы, увеличивая счетчик каждого найденного документа.Наконец, мы сообщаем каждому документу, что его счетчик становится 3 (это означает, что документ появился во всех наборах, таким образом, существует в их пересечении).