Как построить запрос, который точно соответствует вектору ссылок в DataScript?

Настройка Рассмотрим следующую базу данных фильмов и актеров DataScript с данными, украденными с learndatalogtoday.org: следующий код может выполняться в JVM/Clojure REPL или ClojureScript REPL, если project.clj содержит [datascript "0.15.0"] в качестве зависимости.

(ns user
  (:require [datascript.core :as d]))

(def data 
  [["First Blood" ["Sylvester Stallone" "Brian Dennehy" "Richard Crenna"]]
   ["Terminator 2: Judgment Day" ["Linda Hamilton" "Arnold Schwarzenegger" "Edward Furlong" "Robert Patrick"]]
   ["The Terminator" ["Arnold Schwarzenegger" "Linda Hamilton" "Michael Biehn"]]
   ["Rambo III" ["Richard Crenna" "Sylvester Stallone" "Marc de Jonge"]]
   ["Predator 2" ["Gary Busey" "Danny Glover" "Ruben Blades"]]
   ["Lethal Weapon" ["Gary Busey" "Mel Gibson" "Danny Glover"]]
   ["Lethal Weapon 2" ["Mel Gibson" "Joe Pesci" "Danny Glover"]]
   ["Lethal Weapon 3" ["Joe Pesci" "Danny Glover" "Mel Gibson"]]
   ["Alien" ["Tom Skerritt" "Veronica Cartwright" "Sigourney Weaver"]]
   ["Aliens" ["Carrie Henn" "Sigourney Weaver" "Michael Biehn"]]
   ["Die Hard" ["Alan Rickman" "Bruce Willis" "Alexander Godunov"]]
   ["Rambo: First Blood Part II" ["Richard Crenna" "Sylvester Stallone" "Charles Napier"]]
   ["Commando" ["Arnold Schwarzenegger" "Alyssa Milano" "Rae Dawn Chong"]]
   ["Mad Max 2" ["Bruce Spence" "Mel Gibson" "Michael Preston"]]
   ["Mad Max" ["Joanne Samuel" "Steve Bisley" "Mel Gibson"]]
   ["RoboCop" ["Nancy Allen" "Peter Weller" "Ronny Cox"]]
   ["Braveheart" ["Sophie Marceau" "Mel Gibson"]]
   ["Mad Max Beyond Thunderdome" ["Mel Gibson" "Tina Turner"]]
   ["Predator" ["Carl Weathers" "Elpidia Carrillo" "Arnold Schwarzenegger"]]
   ["Terminator 3: Rise of the Machines" ["Nick Stahl" "Arnold Schwarzenegger" "Claire Danes"]]])

(def conn (d/create-conn {:film/cast {:db/valueType :db.type/ref
                                      :db/cardinality :db.cardinality/many}
                          :film/name {:db/unique :db.unique/identity
                                      :db/cardinality :db.cardinality/one}
                          :actor/name {:db/unique :db.unique/identity
                                       :db/cardinality :db.cardinality/one}}))
(def all-datoms (mapcat (fn [[film actors]]
                          (into [{:film/name film}] 
                                (map #(hash-map :actor/name %) actors)))
                        data))
(def all-relations (mapv (fn [[film actors]]
                           {:db/id [:film/name film]
                            :film/cast (mapv #(vector :actor/name %) actors)}) data))

(d/transact! conn all-datoms)
(d/transact! conn all-relations)

Описание Вкратце, в этой базе данных есть два типа сущностей — фильмы и актеры (слово, предназначенное для определения пола) — и три вида данных:

  • сущность фильма: :film/name (уникальная строка)
  • объект фильма: :film/cast (несколько ссылок)
  • сущность актера: :actor/name (уникальная строка)

Вопрос Я хотел бы составить запрос, который спрашивает: в каких фильмах эти N актеров, и только эти N актеров, появились в качестве единственных звезд, для N>=2?

Например, в «Робокопе» снимались Нэнси Аллен, Питер Веллер и Ронни Кокс, но ни в одном фильме снимались только первые два из них, Аллен и Веллер. Поэтому я ожидаю, что следующий запрос создаст пустой набор:

(d/q '[:find ?film-name
       :where
       [?film :film/name ?film-name]
       [?film :film/cast ?actor-1]
       [?film :film/cast ?actor-2]
       [?actor-1 :actor/name "Nancy Allen"]
       [?actor-2 :actor/name "Peter Weller"]]
     @conn)
; => #{["RoboCop"]}

Однако запрос ошибочен, потому что я не знаю, как выразить, что любые совпадения должны исключать любых актеров, которые не являются Алленом или Веллером — опять же, я хочу найти фильмы, в которых только Аллен и Веллер сотрудничали без каких-либо других актеров, поэтому Я хочу адаптировать приведенный выше запрос для создания пустого набора. Как я могу настроить этот запрос, чтобы обеспечить соблюдение этого требования?


person Ahmed Fasih    schedule 13.05.2016    source источник


Ответы (2)


Поскольку в DataScript нет отрицания (по состоянию на май 2016 г.), я не верю, что это возможно с одним статическим запросом в «чистом» журнале данных.

Мой путь был бы таким:

  1. программно создайте запрос, чтобы добавить N предложений, указывающих, что приведение должно содержать N актеров
  2. Добавьте функцию-предикат, которая, учитывая фильм, базу данных и набор идентификаторов актеров, использует индекс EAVT, чтобы определить, есть ли в каждом фильме актер, которого нет в наборе.

Вот базовая реализация

(defn only-those-actors? [db movie actors]
  (->> (datoms db :eavt movie :film/cast) seq
    (every? (fn [[_ _ actor]]
                (contains? actors actor)))
    ))

(defn find-movies-with-exact-cast [db actors-names]
  (let [actors (set (d/q '[:find [?actor ...] :in $ [?name ...] ?only-those-actors :where
                           [?actor :actor/name ?name]]
                      db actors-names))
        query {:find '[[?movie ...]]
               :in '[$ ?actors ?db]
               :where
               (concat
                 (for [actor actors]
                   ['?movie :film/cast actor])
                 [['(only-those-actors? ?db ?movie ?actors)]])}]
    (d/q query db actors db only-those-actors?)))
person Valentin Waeselynck    schedule 13.05.2016
comment
Извините за задержку (и извините, что беспокою вас в slack)! Есть ли какая-то польза от проверки типа only-those-actors? в запросе? Вместо того, чтобы просто получать все обращения и выполнять постобработку вне запроса, согласно >gist.github.com/fasiha/ ? Пострадает ли эта реализация, если не пойдет по маршруту datoms? - person Ahmed Fasih; 17.05.2016
comment
Ваша реализация тоже кажется хорошей! Вы умело используете отличное. В качестве оптимизации, возможно, вы можете просто подсчитать количество различных участников, что более эффективно, чем сортировка. О том, использовать ли datoms или нет: я не буду делать никаких заявлений о производительности, вы должны запустить тесты. - person Valentin Waeselynck; 17.05.2016
comment
Мне пришлось убрать одну или две дополнительные вещи в коде, как сейчас: nofollow noreferrer">gist.github.com/fasiha/ (на случай, если вы захотите обновить ответ), но это работает! Спасибо!! - person Ahmed Fasih; 19.05.2016

Вы можете использовать предикат fun и d/entity вместе для фильтрации данных по :film/cast полю сущности. Этот подход выглядит намного проще, пока Datascript не поддерживает отрицание (оператор not и т. д.).

Посмотрите на строку (= a (:age (d/entity db e)) в контрольном примере Datascript здесь

[{:db/id 1 :name "Ivan" :age 10}
 {:db/id 2 :name "Ivan" :age 20}
 {:db/id 3 :name "Oleg" :age 10}
 {:db/id 4 :name "Oleg" :age 20}]

...

(let [pred (fn [db e a]
             (= a (:age (d/entity db e))))]
  (is (= (q/q '[:find ?e
                :in $ ?pred
                :where [?e :age ?a]
                       [(?pred $ ?e 10)]]
              db pred)
         #{[1] [3]})))))

В вашем случае тело предиката может выглядеть примерно так

(clojure.set/subset? actors (:film/cast (d/entity db e))

Что касается производительности, вызов d/entity работает быстро, потому что это поиск по индексу.

person Aleksei Sotnikov    schedule 19.12.2017