Найдите объекты, атрибут ref-to-many которых содержит все элементы ввода

Предположим, у меня есть сущность entry с атрибутом :entry/groups ссылки на многие. Как создать запрос для поиска сущностей, чей атрибут :entry/groups содержит все мои входные внешние идентификаторы?

Следующий псевдокод лучше проиллюстрирует мой вопрос:

[2 3] ; having this as input foreign ids

;; and having these entry entities in db
[{:entry/id "A" :entry/groups  [2 3 4]}  
 {:entry/id "B" :entry/groups  [2]}     
 {:entry/id "C" :entry/groups  [2 3]}  
 {:entry/id "D" :entry/groups  [1 2 3]}
 {:entry/id "E" :entry/groups  [2 4]}] 

;; only A, C, D should be pulled

Будучи новичком в Datomic/Datalog, я исчерпал все варианты, поэтому любая помощь приветствуется. Спасибо!


person Twice_Twice    schedule 04.05.2017    source источник


Ответы (2)


TL;DR

Вы решаете общую проблему «динамического соединения» в журнале данных Datomic.

3 стратегии здесь:

  1. Напишите динамический запрос журнала данных, в котором используются 2 отрицания и 1 дизъюнкция или рекурсивное правило (см. ниже).
  2. Сгенерируйте код запроса (эквивалент ответа Алана Томпсона): недостатки - это обычные недостатки динамического создания предложений Datalog, т. е. вы не получаете преимуществ от кэширование плана запроса.
  3. Используйте индексы напрямую (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
comment
Спасибо за такой развернутый ответ! Два вопроса: 1) Можно ли динамически генерировать запросы Datomic, если кэширование плана запроса не требуется или считается плохой практикой и приводит к некоторым (кроме кэширования) серьезным недостаткам? 2) Как вы называете индекс EAVT в своей функции базы данных? Между прочим, я использую Clojure с Datomic, поэтому, думаю, мне нужно вызвать функцию datoms, верно? - person Twice_Twice; 06.05.2017

Вы можете увидеть пример этого в примере с Джеймсом Бондом из библиотеки Tupelo-Datomic. Вы просто указываете 2 предложения, по одному для каждого желаемого значения в наборе:

; Search for people that match both {:weapon/type :weapon/guile} and {:weapon/type :weapon/gun}
(let [tuple-set   (td/find :let    [$ (live-db)]
                           :find   [?name]
                           :where  {:person/name ?name :weapon/type :weapon/guile }
                                   {:person/name ?name :weapon/type :weapon/gun } ) ]
  (is (= #{["Dr No"] ["M"]} tuple-set )))

В чистом Datomic это будет выглядеть аналогично, но с использованием идентификатора сущности:

[?eid :entry/groups 2]
[?eid :entry/groups 3]

и Datomic выполнит неявную операцию AND (т. е. оба предложения должны совпадать; любые лишние записи игнорируются). Логически это операция «объединения», даже если для обоих значений запрашивается один и тот же объект. Дополнительную информацию можно найти в документации Datomic.

person Alan Thompson    schedule 04.05.2017
comment
Итак, единственный способ — динамически создать запрос с необходимыми предложениями where из моих внешних идентификаторов? Я не могу использовать их в качестве входных данных для запроса, как в :in $ ...? - person Twice_Twice; 05.05.2017
comment
@Twice_Twice динамическое создание запроса - не единственный способ, см. мой ответ ниже - person Valentin Waeselynck; 05.05.2017