TL;DR
Вы решаете общую проблему «динамического соединения» в журнале данных Datomic.
3 стратегии здесь:
- Напишите динамический запрос журнала данных, в котором используются 2 отрицания и 1 дизъюнкция или рекурсивное правило (см. ниже).
- Сгенерируйте код запроса (эквивалент ответа Алана Томпсона): недостатки - это обычные недостатки динамического создания предложений Datalog, т. е. вы не получаете преимуществ от кэширование плана запроса.
- Используйте индексы напрямую (EAVT или AVET).
Запрос динамического журнала данных
В журнале данных нет прямого способа выражения динамической конъюнкции (логическое И/'для всех...'/множественное пересечение). Однако вы можете добиться этого в чистом Datalog, комбинируя одну дизъюнкцию (логическое ИЛИ/'существует...'/объединение множеств) и два отрицания, т.е. (For all ?g in ?Gs p(?e,?g)) <=> NOT(Exists ?g in ?Gs, such that NOT(p(?e, ?g)))
В вашем случае это может быть выражено так:
[:find [?entry ...] :in $ ?groups :where
;; these 2 clauses are for restricting the set of considered datoms, which is more efficient (and necessary in Datomic's Datalog, which will refuse to scan the whole db)
;; NOTE: this imposes ?groups cannot be empty!
[(first ?groups) ?group0]
[?entry :entry/groups ?group0]
;; here comes the double negation
(not-join [?entry ?groups]
[(identity ?groups) [?group ...]]
(not-join [?entry ?group]
[?entry :entry/groups ?group]))]
Хорошие новости: это можно выразить в виде очень общего правила Datalog (которое я могу добавить в Datofu). ):
[(matches-all ?e ?a ?vs)
[(first ?vs) ?v0]
[?e ?a ?v0]
(not-join [?e ?a ?vs]
[(seq ?vs) [?v ...]]
(not-join [?e ?a ?v]
[?e ?a ?v]))]
... что означает, что ваш запрос теперь может быть выражен как:
[:find [?entry ...] :in % $ ?groups :where
(matches-all ?entry :entry/groups ?groups)]
ПРИМЕЧАНИЕ. Существует альтернативная реализация с использованием рекурсивного правила:
[[(matches-all ?e ?a ?vs)
[(seq ?vs)]
[(first ?vs) ?v]
[?e ?a ?v]
[(rest ?vs) ?vs2]
(matches-all ?e ?a ?vs2)]
[(matches-all ?e ?a ?vs)
[(empty? ?vs)]]]
Это имеет то преимущество, что принимает пустую коллекцию ?vs
(пока ?e
и ?a
каким-то другим образом связаны в запросе).
Генерация кода запроса
Преимущество создания кода запроса состоит в том, что в данном случае это относительно просто и, вероятно, может сделать выполнение запроса более эффективным, чем более динамичный вариант. Недостаток создания запросов Datalog в Datomic заключается в том, что вы можете потерять преимущества кэширования плана запроса; поэтому, даже если вы собираетесь генерировать запросы, вы все равно хотите сделать их как можно более общими (т.е. в зависимости только от количества значений v
)
(defn q-find-having-all-vs
[n-vs]
(let [v-syms (for [i (range n-vs)]
(symbol (str "?v" i)))]
{:find '[[?e ...]]
:in (into '[$ ?a] v-syms)
:where
(for [?v v-syms]
['?e '?a ?v])}))
;; examples
(q-find-having-all-vs 1)
=> {:find [[?e ...]],
:in [$ ?a ?v0],
:where
([?e ?a ?v0])}
(q-find-having-all-vs 2)
=> {:find [[?e ...]],
:in [$ ?a ?v0 ?v1],
:where
([?e ?a ?v0]
[?e ?a ?v1])}
(q-find-having-all-vs 3)
=> {:find [[?e ...]],
:in [$ ?a ?v0 ?v1 ?v2],
:where
([?e ?a ?v0]
[?e ?a ?v1]
[?e ?a ?v2])}
;; executing the query: note that we're passing the attribute and values!
(apply d/q (q-find-having-all-vs (count groups))
db :entry/group groups)
Используйте индексы напрямую
Я вообще не уверен, насколько эффективны описанные выше подходы в текущей реализации Datomic Datalog. Если ваш бенчмаркинг показывает, что это медленно, вы всегда можете вернуться к прямому доступу к индексу.
Вот пример в Clojure с использованием индекса AVET:
(defn find-having-all-vs
"Given a database value `db`, an attribute identifier `a` and a non-empty seq of entity identifiers `vs`,
returns a set of entity identifiers for entities which have all the values in `vs` via `a`"
[db a vs]
;; DISCLAIMER: a LOT can be done to improve the efficiency of this code!
(apply clojure.set/intersection
(for [v vs]
(into #{}
(map :e)
(d/datoms db :avet a v)))))
person
Valentin Waeselynck
schedule
05.05.2017