Repository: CSCfi/rems
Pull request #1471 (2019-08-08 12:55:33)

Search by applicant, API only
POC of #1352 and #873


Patch:
diff --git a/dev-config.edn b/dev-config.edn
index 8635ee346..147319116 100644
--- a/dev-config.edn
+++ b/dev-config.edn
@@ -2,6 +2,7 @@
  :nrepl-port 7000
  :database-url "postgresql://localhost/rems?user=rems"
  :test-database-url "postgresql://localhost/rems_test?user=rems_test"
+ :search-index-path "target/search-index-dev"
  :theme-path "example-theme/theme.edn"
  :authentication :fake-shibboleth
  :public-url "http://localhost:3000/"
diff --git a/project.clj b/project.clj
index 5c731567f..db3c9af55 100644
--- a/project.clj
+++ b/project.clj
@@ -35,9 +35,10 @@
                  [metosin/compojure-api "2.0.0-alpha28" :exclusions [cheshire com.fasterxml.jackson.core/jackson-core]]
                  [metosin/jsonista "0.2.2"]
                  [metosin/komponentit "0.3.7"]
-                 [metosin/ring-swagger "0.26.2"]
-                 [metosin/ring-swagger-ui "3.20.1"]
+                 [metosin/ring-swagger "0.26.1"]
                  [mount "0.1.15"]
+                 [org.apache.lucene/lucene-core "8.2.0"]
+                 [org.apache.lucene/lucene-queryparser "8.2.0"]
                  [org.clojars.luontola/garden "1.3.6-patch1"] ;; TODO: waiting for a new release with https://github.com/noprompt/garden/pull/172
                  [org.clojars.luontola/ns-tracker "0.3.1-patch3"] ;; TODO: waiting for a new release with https://github.com/weavejester/ns-tracker/pull/24
                  [org.clojars.pntblnk/clj-ldap "0.0.16"]
diff --git a/resources/config-defaults.edn b/resources/config-defaults.edn
index 9ab658bd0..6a19ee834 100644
--- a/resources/config-defaults.edn
+++ b/resources/config-defaults.edn
@@ -11,6 +11,9 @@
  :database-url nil
  :database-jndi-name "java:comp/env/jdbc/rems"
 
+ ;; Path to a writable directory where to store the Lucene search index files.
+ :search-index-path "search-index"
+
  ;; Supported authentication methods: :shibboleth, :fake-shibboleth, :ldap
  :authentication :shibboleth
  :ldap {:connection {:host "localhost:2636"
diff --git a/src/clj/rems/api/applications.clj b/src/clj/rems/api/applications.clj
index 91922638b..85ee82c51 100644
--- a/src/clj/rems/api/applications.clj
+++ b/src/clj/rems/api/applications.clj
@@ -6,6 +6,7 @@
             [rems.api.util :as api-util] ; required for route :roles
             [rems.application-util :as application-util]
             [rems.application.commands :as commands]
+            [rems.application.search :as search]
             [rems.auth.util :refer [throw-forbidden]]
             [rems.context :as context]
             [rems.db.applications :as applications]
@@ -128,6 +129,14 @@
   (->> (get-potential-todos user-id)
        (remove todo?)))
 
+(defn- filter-with-search [query apps]
+  (if (str/blank? query)
+    apps
+    (let [app-ids (search/find-applications query)]
+      (filter (fn [app]
+                (contains? app-ids (:application/id app)))
+              apps))))
+
 (defn- coerce-command-from-api [cmd]
   ;; TODO: schema could do these coercions for us
   (update-present cmd :decision keyword))
@@ -161,7 +170,9 @@
       :summary "Get the current user's own applications"
       :roles #{:logged-in}
       :return [ApplicationOverview]
-      (ok (applications/get-my-applications (getx-user-id))))))
+      :query-params [{query :- (describe s/Str "search query") nil}]
+      (ok (->> (applications/get-my-applications (getx-user-id))
+               (filter-with-search query))))))
 
 (def applications-api
   (context "/applications" []
@@ -171,19 +182,25 @@
       :summary "Get all applications which the current user can see"
       :roles #{:logged-in}
       :return [ApplicationOverview]
-      (ok (applications/get-all-applications (getx-user-id))))
+      :query-params [{query :- (describe s/Str "search query") nil}]
+      (ok (->> (applications/get-all-applications (getx-user-id))
+               (filter-with-search query))))
 
     (GET "/todo" []
       :summary "Get all applications that the current user needs to act on."
       :roles #{:logged-in}
       :return [ApplicationOverview]
-      (ok (get-todos (getx-user-id))))
+      :query-params [{query :- (describe s/Str "search query") nil}]
+      (ok (->> (get-todos (getx-user-id))
+               (filter-with-search query))))
 
     (GET "/handled" []
       :summary "Get all applications that the current user no more needs to act on."
       :roles #{:logged-in}
       :return [ApplicationOverview]
-      (ok (get-handled-todos (getx-user-id))))
+      :query-params [{query :- (describe s/Str "search query") nil}]
+      (ok (->> (get-handled-todos (getx-user-id))
+               (filter-with-search query))))
 
     (POST "/create" []
       :summary "Create a new application"
diff --git a/src/clj/rems/application/search.clj b/src/clj/rems/application/search.clj
new file mode 100644
index 000000000..a97fde17c
--- /dev/null
+++ b/src/clj/rems/application/search.clj
@@ -0,0 +1,74 @@
+(ns rems.application.search
+  (:require [clojure.java.io :as io]
+            [clojure.tools.logging :as log]
+            [mount.core :as mount]
+            [rems.config :refer [env]]
+            [rems.db.applications :as applications]
+            [rems.db.events :as events])
+  (:import [org.apache.lucene.analysis.standard StandardAnalyzer]
+           [org.apache.lucene.document Document StringField Field$Store TextField]
+           [org.apache.lucene.index IndexWriter IndexWriterConfig IndexWriterConfig$OpenMode]
+           [org.apache.lucene.queryparser.flexible.standard StandardQueryParser]
+           [org.apache.lucene.search IndexSearcher ScoreDoc TopDocs SearcherManager SearcherFactory]
+           [org.apache.lucene.store Directory NIOFSDirectory]))
+
+(def ^:private analyzer (StandardAnalyzer.))
+
+(mount/defstate ^Directory search-index
+  :start (let [directory (NIOFSDirectory. (.toPath (io/file (:search-index-path env))))]
+           (with-open [writer (IndexWriter. directory (-> (IndexWriterConfig. analyzer)
+                                                          (.setOpenMode IndexWriterConfig$OpenMode/CREATE)))]
+             (.deleteAll writer))
+           (atom {::directory directory
+                  ::searcher-manager (SearcherManager. directory (SearcherFactory.))
+                  ::last-processed-event-id 0}))
+  :stop (do
+          (.close ^SearcherManager (::searcher-manager @search-index))
+          (.close ^Directory (::directory @search-index))))
+
+(defn- index-application! [^IndexWriter writer app]
+  (log/info "Indexing application" (:application/id app))
+  (let [doc (Document.)]
+    (.add doc (StringField. "id" (str (:application/id app)) Field$Store/YES))
+    (.add doc (TextField. "applicant" (str (:application/applicant app)) Field$Store/NO))
+    (.addDocument writer doc)))
+
+(def ^:private refresh-lock (Object.))
+
+(defn refresh! []
+  ;; Only one IndexWriter may use the directory at a time.
+  ;; Otherwise it'll throw a LockObtainFailedException.
+  (locking refresh-lock
+    (let [{::keys [directory ^SearcherManager searcher-manager last-processed-event-id]} @search-index
+          events (events/get-all-events-since last-processed-event-id)]
+      (when-not (empty? events)
+        (with-open [writer (IndexWriter. directory (-> (IndexWriterConfig. analyzer)
+                                                       (.setOpenMode IndexWriterConfig$OpenMode/APPEND)))]
+          (doseq [app-id (set (map :application/id events))]
+            (index-application! writer (applications/get-unrestricted-application app-id))))
+        (.maybeRefresh searcher-manager)
+        (swap! search-index assoc ::last-processed-event-id (:event/id (last events)))))))
+
+(defn- with-searcher [f]
+  (let [searcher-manager ^SearcherManager (::searcher-manager @search-index)
+        searcher ^IndexSearcher (.acquire searcher-manager)]
+    (try
+      (f searcher)
+      (finally
+        (.release searcher-manager searcher)))))
+
+(defn- get-application-ids [^IndexSearcher searcher ^TopDocs results]
+  (doall (for [^ScoreDoc hit (.-scoreDocs results)]
+           (let [doc (.doc searcher (.-doc hit))
+                 id (.get doc "id")]
+             (Long/parseLong id)))))
+
+(defn find-applications [^String query]
+  (refresh!) ; TODO: call from a background thread asynchronously?
+  (with-searcher
+   (fn [^IndexSearcher searcher]
+     (let [results (.search searcher
+                            (-> (StandardQueryParser. analyzer)
+                                (.parse query "applicant")) ; TODO: change defaultField to full text search
+                            Integer/MAX_VALUE)]
+       (set (get-application-ids searcher results))))))
diff --git a/src/clj/rems/css/styles.clj b/src/clj/rems/css/styles.clj
index f202e3fd7..96ac56870 100644
--- a/src/clj/rems/css/styles.clj
+++ b/src/clj/rems/css/styles.clj
@@ -282,6 +282,8 @@
            :flex-direction :column
            :padding-top (u/px 56)}]
    [:h1 :h2 {:font-weight 400}]
+   [:h1 :h2 :h3 {:letter-spacing (u/rem 0.17)}]
+   [:h4 :h5 :h6 {:letter-spacing (u/rem 0.12)}]
    [:h1 {:margin-bottom (u/rem 2)}]
    [:#app {:min-height (u/percent 100)
            :flex 1
@@ -594,8 +596,8 @@
                     :top "100px"}
     [:.collapse-content {:overflow-y :scroll
                          :overflow-x :hidden
-                         ;; subtract #preview-form top value plus a margin here to stay inside the viewbox
-                         :max-height "calc(100vh - 220px)"}]]
+                         ;; subtract #preview-form top value here to stay inside the viewbox
+                         :max-height "calc(80vh - 100px)"}]]
 
    [:.full {:width "100%"}]
    [:.intro {:margin-bottom (u/rem 2)}]
diff --git a/src/clj/rems/permissions.clj b/src/clj/rems/permissions.clj
index 840fc0dc8..a69dafb86 100644
--- a/src/clj/rems/permissions.clj
+++ b/src/clj/rems/permissions.clj
@@ -1,7 +1,6 @@
 (ns rems.permissions
-  (:require [clojure.test :refer [deftest is testing]]))
-
-(def ^:private conj-set (fnil conj #{}))
+  (:require [clojure.test :refer [deftest is testing]]
+            [rems.util :refer [conj-set]]))
 
 (defn- give-role-to-user [application role user]
   (assert (keyword? role) {:role role})
@@ -106,13 +105,13 @@
                                 :role-2 #{:bar}}}
            (-> {}
                (update-role-permissions {:role-1 [:foo]
-                                      :role-2 [:bar]})))))
+                                         :role-2 [:bar]})))))
   (testing "does not alter unrelated roles"
     (is (= {::role-permissions {:unrelated #{:foo}
                                 :role #{:gazonk}}}
            (-> {}
                (update-role-permissions {:unrelated [:foo]
-                                      :role [:bar]})
+                                         :role [:bar]})
                (update-role-permissions {:role [:gazonk]}))))))
 
 (defn remove-permission-from-all [application permission]
@@ -128,7 +127,7 @@
                                 :role-2 #{}}}
            (-> {}
                (update-role-permissions {:role-1 [:foo]
-                                      :role-2 [:foo]})
+                                         :role-2 [:foo]})
                (remove-permission-from-all :foo)))))
   (testing "leaves unrelated permissions unchanged"
     (is (= {::role-permissions {:role #{:bar}}}
diff --git a/src/clj/rems/standalone.clj b/src/clj/rems/standalone.clj
index da8037e07..d98c60b79 100644
--- a/src/clj/rems/standalone.clj
+++ b/src/clj/rems/standalone.clj
@@ -6,7 +6,9 @@
             [luminus.http-server :as http]
             [luminus.repl-server :as repl]
             [mount.core :as mount]
+            [rems.application.search :as search]
             [rems.config :refer [env]]
+            [rems.db.applications :as applications]
             [rems.db.test-data :as test-data]
             [rems.handler :as handler]
             [rems.validate :as validate])
@@ -39,6 +41,12 @@
   (doseq [component (:stopped (mount/stop))]
     (log/info component "stopped")))
 
+(defn- refresh-caches []
+  (log/info "Refreshing caches")
+  (applications/refresh-all-applications-cache!)
+  (search/refresh!)
+  (log/info "Caches refreshed"))
+
 (defn start-app [& args]
   (doseq [component (-> args
                         (parse-opts cli-options)
@@ -46,7 +54,8 @@
                         :started)]
     (log/info component "started"))
   (.addShutdownHook (Runtime/getRuntime) (Thread. stop-app))
-  (validate/validate))
+  (validate/validate)
+  (refresh-caches))
 
 (defn -main
   "Arguments can be either arguments to mount/start-with-args, or one of
diff --git a/src/clj/rems/util.clj b/src/clj/rems/util.clj
index 15d59d4ca..75e6a3a86 100644
--- a/src/clj/rems/util.clj
+++ b/src/clj/rems/util.clj
@@ -63,6 +63,8 @@
   ([user]
    (:mail user)))
 
+(def conj-set (fnil conj #{}))
+
 (defn update-present
   "Like clojure.core/update, but does nothing if the key `k` does not exist in `m`."
   [m k f & args]
diff --git a/src/clj/rems/validate.clj b/src/clj/rems/validate.clj
index 4ec221694..60a797a14 100644
--- a/src/clj/rems/validate.clj
+++ b/src/clj/rems/validate.clj
@@ -3,7 +3,8 @@
   (:require [clojure.pprint :refer [pprint]]
             [clojure.tools.logging :as log]
             [rems.api.forms]
-            [rems.db.applications :as applications]
+            [rems.application.events :as events]
+            [rems.db.events :as events-db]
             [rems.db.form :as form]
             [schema.core :as s]))
 
@@ -15,8 +16,7 @@
   (log/info "Validating data")
   (try
     (validate-forms)
-    ;; will throw an exception if there are non-valid events
-    (applications/get-all-unrestricted-applications)
+    (events/validate-events (events-db/get-all-events-since 0))
     (log/info "Validations passed")
     (catch Throwable t
       (log/error t "Validations failed" (with-out-str (when-let [data (ex-data t)]
diff --git a/src/cljs/rems/administration/create_form.cljs b/src/cljs/rems/administration/create_form.cljs
index e3782bde4..17504d50e 100644
--- a/src/cljs/rems/administration/create_form.cljs
+++ b/src/cljs/rems/administration/create_form.cljs
@@ -177,47 +177,6 @@
                    :error-handler status-modal/common-error-handler!}))
      {:db (assoc db ::form-errors form-errors)})))
 
-;;;; preview auto-scrolling
-
-(defn visibility-ratio [element]
-  (let [bounds (.getBoundingClientRect element)]
-    (cond (<= (.-bottom bounds) 0)
-          0
-          (>= (.-top bounds) 0)
-          1
-          :else
-          (/ (.-bottom bounds) (.-height bounds)))))
-
-(defn true-height [element]
-  (let [style (.getComputedStyle js/window element)]
-    (+ (.-offsetHeight element)
-       (js/parseInt (.-marginTop style))
-       (js/parseInt (.-marginBottom style)))))
-
-(defn set-visibility-ratio [frame element ratio]
-  (let [element-top (- (.-offsetTop element) (.-offsetTop frame))
-        element-height (true-height element)
-        top-margin (/ (.-offsetHeight frame) 4)
-        position (+ element-top element-height (* -1 ratio element-height) (- top-margin))]
-    (.scrollTo frame 0 position)))
-
-(defn first-partially-visible-edit-field []
-  (let [fields (array-seq (.querySelectorAll js/document "#create-form .form-field"))
-        visible? #(<= 0 (-> % .getBoundingClientRect .-bottom))]
-    (first (filter visible? fields))))
-
-(defn autoscroll []
-  (when-let [edit-field (first-partially-visible-edit-field)]
-    (let [id (.getAttribute edit-field "data-field-id")
-          preview-frame (.querySelector js/document "#preview-form .collapse-content")
-          preview-field (-> js/document
-                            (.getElementById (str "container-field" id)))
-          ratio (visibility-ratio edit-field)]
-      (set-visibility-ratio preview-frame preview-field ratio))))
-
-(defn enable-autoscroll! []
-  (set! (.-onscroll js/window) autoscroll))
-
 ;;;; UI
 
 (def ^:private context
@@ -331,14 +290,29 @@
    "/#/administration/forms"
    (text :t.administration/cancel)])
 
+(defn- view-field-button [field-index]
+  [:a {:href "#"
+       :aria-label (text :t.administration/preview)
+       :title (text :t.administration/preview)
+       :on-click (fn [event]
+                   (.preventDefault event)
+                   (let [id (fields/id-to-name field-index)
+                         elt (. js/document getElementById id)
+                         ;; the input itself is wrapped in a div or fieldset
+                         parent (.-parentElement elt)]
+                     ;; Without :nearest, the browser would sometimes also scroll the main scroll bar for some reason.
+                     ;; TODO :nearest doesn't work on Firefox<58 or Edge
+                     (.scrollIntoView parent (clj->js {:block :nearest}))))}
+   [:i.icon-link.fas.fa-eye {:aria-hidden true}]])
+
 (defn- form-fields [fields]
   (into [:div]
         (for [{id :field/id :as field} fields]
-          [:div.form-field {:key id
-                            :data-field-id id}
+          [:div.form-field {:key id}
            [:div.form-field-header
             [:h3 (text-format :t.create-form/field-n (inc id))]
             [:div.form-field-controls
+             [view-field-button id]
              [move-form-field-up-button id]
              [move-form-field-down-button id]
              [remove-form-field-button id]]]
@@ -363,7 +337,6 @@
                     [fields/field field]))}])
 
 (defn create-form-page []
-  (enable-autoscroll!)
   (let [form @(rf/subscribe [::form])
         edit-form? @(rf/subscribe [::edit-form?])
         loading-form? @(rf/subscribe [::loading-form?])]
diff --git a/src/cljs/rems/fields.cljs b/src/cljs/rems/fields.cljs
index da8d0eddb..79fa88894 100644
--- a/src/cljs/rems/fields.cljs
+++ b/src/cljs/rems/fields.cljs
@@ -73,14 +73,13 @@
     [(if fieldset
        :fieldset.form-group.field
        :div.form-group.field)
-     (merge
-      {:id (str "container-" (id-to-name id))}
-      (when fieldset
-        {:tab-index -1
-         :aria-required (not optional)
-         :aria-invalid (when validation true)
-         :aria-describedby (when validation
-                             (str (id-to-name id) "-error"))}))
+     (when fieldset
+       {:id (id-to-name id)
+        :tab-index -1
+        :aria-required (not optional)
+        :aria-invalid (when validation true)
+        :aria-describedby (when validation
+                            (str (id-to-name id) "-error"))})
      [(if fieldset
         :legend
         :label)
@@ -211,7 +210,6 @@
 (defn label [opts]
   (let [title (:field/title opts)]
     [:div.form-group
-     {:id (str "container-" (id-to-name (:field/id opts)))}
      [:label (localized title)]]))
 
 (defn multiselect-field [{:keys [validation on-change] :as opts}]


Test patch:
diff --git a/test-config.edn b/test-config.edn
index 6d6fae1e9..5edd69d0d 100644
--- a/test-config.edn
+++ b/test-config.edn
@@ -4,4 +4,5 @@
  :public-url "http://localhost:3001/"
  :database-url "postgresql://localhost/rems_test?user=rems_test"
  :test-database-url "postgresql://localhost/rems_test?user=rems_test"
+ :search-index-path "target/search-index-test"
  :authentication :fake-shibboleth}
diff --git a/test/clj/rems/api/test_applications.clj b/test/clj/rems/api/test_applications.clj
index e53229b0a..bd5a8535f 100644
--- a/test/clj/rems/api/test_applications.clj
+++ b/test/clj/rems/api/test_applications.clj
@@ -29,14 +29,14 @@
 (defn- catalogue-item-ids-for-application [application]
   (set (map :catalogue-item/id (:application/resources application))))
 
-(defn- get-my-applications [user-id]
-  (-> (request :get "/api/my-applications")
+(defn- get-my-applications [user-id & [params]]
+  (-> (request :get "/api/my-applications" params)
       (authenticate "42" user-id)
       handler
       read-ok-body))
 
-(defn- get-all-applications [user-id]
-  (-> (request :get "/api/applications")
+(defn- get-all-applications [user-id & [params]]
+  (-> (request :get "/api/applications" params)
       (authenticate "42" user-id)
       handler
       read-ok-body))
@@ -47,14 +47,14 @@
       handler
       read-ok-body))
 
-(defn- get-todos [user-id]
-  (-> (request :get "/api/applications/todo")
+(defn- get-todos [user-id & [params]]
+  (-> (request :get "/api/applications/todo" params)
       (authenticate "42" user-id)
       handler
       read-ok-body))
 
-(defn- get-handled-todos [user-id]
-  (-> (request :get "/api/applications/handled")
+(defn- get-handled-todos [user-id & [params]]
+  (-> (request :get "/api/applications/handled" params)
       (authenticate "42" user-id)
       handler
       read-ok-body))
@@ -622,9 +622,19 @@
       (is (contains? (get-ids (get-my-applications "alice"))
                      app-id)))
 
+    (testing "search user applications"
+      (is (contains? (get-ids (get-my-applications "alice" {:query "applicant:alice"}))
+                     app-id))
+      (is (empty? (get-ids (get-my-applications "alice" {:query "applicant:no-such-user"})))))
+
     (testing "list all applications"
       (is (contains? (get-ids (get-all-applications "alice"))
-                     app-id)))))
+                     app-id)))
+
+    (testing "search all applications"
+      (is (contains? (get-ids (get-all-applications "alice" {:query "applicant:alice"}))
+                     app-id))
+      (is (empty? (get-ids (get-all-applications "alice" {:query "applicant:no-such-user"})))))))
 
 (deftest test-todos
   (let [app-id (test-data/create-application! {:actor "alice"})]
@@ -641,6 +651,11 @@
       (is (not (contains? (get-ids (get-handled-todos "developer"))
                           app-id))))
 
+    (testing "search todos"
+      (is (contains? (get-ids (get-todos "developer" {:query "applicant:alice"}))
+                     app-id))
+      (is (empty? (get-ids (get-todos "developer" {:query "applicant:no-such-user"})))))
+
     (testing "commenter sees application in todos"
       (is (= {:success true} (send-command "developer" {:type :application.command/request-comment
                                                         :application-id app-id
@@ -660,6 +675,11 @@
       (is (contains? (get-ids (get-handled-todos "developer"))
                      app-id)))
 
+    (testing "search handled todos"
+      (is (contains? (get-ids (get-handled-todos "developer" {:query "applicant:alice"}))
+                     app-id))
+      (is (empty? (get-ids (get-handled-todos "developer" {:query "applicant:no-such-user"})))))
+
     (testing "commenter doesn't see accepted application in todos"
       (is (not (contains? (get-ids (get-todos "bob"))
                           app-id)))
diff --git a/test/clj/rems/application/test_search.clj b/test/clj/rems/application/test_search.clj
new file mode 100644
index 000000000..1f9d0e3a0
--- /dev/null
+++ b/test/clj/rems/application/test_search.clj
@@ -0,0 +1,27 @@
+(ns ^:integration rems.application.test-search
+  (:require [clojure.test :refer :all]
+            [rems.application.search :as search]
+            [rems.db.test-data :as test-data]
+            [rems.db.testing :refer [test-db-fixture rollback-db-fixture]]))
+
+(use-fixtures :once test-db-fixture)
+(use-fixtures :each rollback-db-fixture)
+
+(deftest test-application-search
+  ;; unrelated application - it's an error if any of the tests finds this
+  (test-data/create-application! {:actor "developer"})
+  (search/refresh!)
+
+  (testing "find by applicant"
+    (let [app-id (test-data/create-application! {:actor "alice"})]
+      (is (= #{app-id} (search/find-applications "alice")))))
+
+  (testing "find by member")
+
+  (testing "find by title")
+
+  (testing "find by resource")
+
+  (testing "find by state")
+
+  (testing "find by form content"))
diff --git a/test/clj/rems/db/testing.clj b/test/clj/rems/db/testing.clj
index cafbd1df8..c52d0cd10 100644
--- a/test/clj/rems/db/testing.clj
+++ b/test/clj/rems/db/testing.clj
@@ -15,7 +15,8 @@
   (mount/stop) ;; during interactive development, app might be running when tests start. we need to tear it down
   (mount/start-with-args {:test true}
                          #'rems.config/env
-                         #'rems.db.core/*db*)
+                         #'rems.db.core/*db*
+                         #'rems.application.search/search-index)
   (db/assert-test-database!)
   (migrations/migrate ["reset"] {:database-url (:test-database-url env)})
   (f)