(ns metabase.api.search (:require [clojure.string :as str] [compojure.core :refer [GET]] [java-time.api :as t] [metabase.analytics.prometheus :as prometheus] [metabase.api.common :as api] [metabase.config :as config] [metabase.public-settings :as public-settings] [metabase.public-settings.premium-features :as premium-features] [metabase.request.core :as request] ;; Allowing search.config to be accessed for developer API to set weights ^{:clj-kondo/ignore [:metabase/ns-module-checker]} [metabase.search.config :as search.config] [metabase.search.core :as search] [metabase.task :as task] [metabase.task.search-index :as task.search-index] [metabase.util :as u] [metabase.util.malli.schema :as ms] [ring.util.response :as response])) | |
(set! *warn-on-reflection* true) | |
(def ^:private engine-cookie-name "metabase.SEARCH_ENGINE") | |
(defn- cookie-expiry [] ;; 20 years should be long enough to trial an experimental search engine (t/format :rfc-1123-date-time (t/plus (t/zoned-date-time) (t/years 20)))) | |
(defn- set-engine-cookie! [respond engine] (fn [response] (respond (response/set-cookie response engine-cookie-name engine {:http-only true :path "/" :expires (cookie-expiry)})))) | |
(defn- +engine-cookie [handler] (with-meta (fn [request respond raise] (if-let [new-engine (get-in request [:params :search_engine])] (handler request (set-engine-cookie! respond new-engine) raise) (handler (->> (get-in request [:cookies engine-cookie-name :value]) (assoc-in request [:params :search_engine])) respond raise))) (meta handler))) | |
/re-init | (api/defendpoint POST "This will blow away any search indexes, re-create, and re-populate them." [] (api/check-superuser) (if (search/supports-index?) {:message (search/init-index! {:force-reset? true})} (throw (ex-info "Search index is not supported for this installation." {:status-code 501})))) |
/force-reindex | (api/defendpoint POST "This will trigger an immediate reindexing, if we are using search index." [] (api/check-superuser) (if (search/supports-index?) ;; The job appears to wait on the main thread when run from tests, so, unfortunately, testing this branch is hard. (if (and (task/job-exists? task.search-index/reindex-job-key) (not config/is-test?)) (do (task/trigger-now! task.search-index/reindex-job-key) {:message "task triggered"}) (do (task.search-index/reindex!) {:message "done"})) (throw (ex-info "Search index is not supported for this installation." {:status-code 501})))) |
(defn- set-weights! [context overrides] (api/check-superuser) (when (= context :all) (throw (ex-info "Cannot set weights for all context" {:status-code 400}))) (let [known-ranker? (set (keys (:default @#'search.config/static-weights))) rankers (into #{} (map (fn [k] (keyword (first (str/split (name k) #"/"))))) (keys overrides)) unknown-rankers (seq (remove known-ranker? rankers))] (when unknown-rankers (throw (ex-info (str "Unknown rankers: " (str/join ", " (map name (sort unknown-rankers)))) {:status-code 400}))) (public-settings/experimental-search-weight-overrides! (merge-with merge (public-settings/experimental-search-weight-overrides) {context overrides})))) | |
/weights | (api/defendpoint GET "Return the current weights being used to rank the search results" [:as {overrides :params}] ;; remove cookie (let [context (keyword (:context overrides :default)) overrides (-> overrides (dissoc :search_engine :context) (update-vals parse-double))] (when (seq overrides) (set-weights! context overrides)) (search.config/weights context))) |
/ | (api/defendpoint GET "Search for items in Metabase. For the list of supported models, check [[metabase.search.config/all-models]]. Filters: - `archived`: set to true to search archived items only, default is false - `table_db_id`: search for tables, cards, and models of a certain DB - `models`: only search for items of specific models. If not provided, search for all models - `filters_items_in_personal_collection`: only search for items in personal collections - `created_at`: search for items created at a specific timestamp - `created_by`: search for items created by a specific user - `last_edited_at`: search for items last edited at a specific timestamp - `last_edited_by`: search for items last edited by a specific user - `search_native_query`: set to true to search the content of native queries - `verified`: set to true to search for verified items only (requires Content Management or Official Collections premium feature) - `ids`: search for items with those ids, works iff single value passed to `models` Note that not all item types support all filters, and the results will include only models that support the provided filters. For example: - The `created-by` filter supports dashboards, models, actions, and cards. - The `verified` filter supports models and cards. A search query that has both filters applied will only return models and cards." [q context archived created_at created_by table_db_id models last_edited_at last_edited_by filter_items_in_personal_collection model_ancestors search_engine search_native_query verified ids calculate_available_models include_dashboard_questions] {q [:maybe ms/NonBlankString] context [:maybe :keyword] archived [:maybe :boolean] table_db_id [:maybe ms/PositiveInt] models [:maybe (ms/QueryVectorOf search/SearchableModel)] filter_items_in_personal_collection [:maybe [:enum "all" "only" "only-mine" "exclude" "exclude-others"]] created_at [:maybe ms/NonBlankString] created_by [:maybe (ms/QueryVectorOf ms/PositiveInt)] last_edited_at [:maybe ms/NonBlankString] last_edited_by [:maybe (ms/QueryVectorOf ms/PositiveInt)] model_ancestors [:maybe :boolean] search_engine [:maybe string?] search_native_query [:maybe true?] verified [:maybe true?] ids [:maybe (ms/QueryVectorOf ms/PositiveInt)] calculate_available_models [:maybe true?] include_dashboard_questions [:maybe :boolean]} (api/check-valid-page-params (request/limit) (request/offset)) (try (u/prog1 (search/search (search/search-context {:archived archived :context context :created-at created_at :created-by (set created_by) :current-user-id api/*current-user-id* :is-impersonated-user? (premium-features/impersonated-user?) :is-sandboxed-user? (premium-features/sandboxed-user?) :is-superuser? api/*is-superuser?* :current-user-perms @api/*current-user-permissions-set* :filter-items-in-personal-collection filter_items_in_personal_collection :last-edited-at last_edited_at :last-edited-by (set last_edited_by) :limit (request/limit) :model-ancestors? model_ancestors :models (not-empty (set models)) :offset (request/offset) :search-engine search_engine :search-native-query search_native_query :search-string q :table-db-id table_db_id :verified verified :ids (set ids) :calculate-available-models? calculate_available_models :include-dashboard-questions? include_dashboard_questions})) (prometheus/inc! :metabase-search/response-ok)) (catch Exception e (let [status-code (:status-code (ex-data e))] (when (or (not status-code) (= 5 (quot status-code 100))) (prometheus/inc! :metabase-search/response-error))) (throw e)))) |
(api/define-routes +engine-cookie) | |