refactor: blocks search returns all blocks including pages

Fuzzy search support both pages and objects (blocks have tags).
pull/11433/head
Tienson Qin 2024-07-20 20:10:36 +08:00
parent 36fed3e212
commit ed1ee94d88
8 changed files with 192 additions and 224 deletions

View File

@ -257,9 +257,7 @@
property i.e. what the user sees. For page types the content is the page name"
[ent]
(or (:block/title ent)
(if-some [content (:property.value/content ent)]
content
(:block/title ent))))
(:property.value/content ent)))
(defn ref->property-value-content
"Given a ref from a pulled query e.g. `{:db/id X}`, gets a readable name for

View File

@ -23,6 +23,7 @@
[goog.functions :as gfun]
[goog.object :as gobj]
[logseq.shui.ui :as shui]
[logseq.db :as ldb]
[promesa.core :as p]
[rum.core :as rum]
[frontend.mixins :as mixins]
@ -46,8 +47,7 @@
(def GROUP-LIMIT 5)
(def search-actions
[{:filter {:group :pages :mode "search"} :text "Search only pages" :info "Add filter to search" :icon-theme :gray :icon "page"}
{:filter {:group :current-page} :text "Search only current page" :info "Add filter to search" :icon-theme :gray :icon "page"}
[{:filter {:group :current-page} :text "Search only current page" :info "Add filter to search" :icon-theme :gray :icon "page"}
{:filter {:group :blocks} :text "Search only blocks" :info "Add filter to search" :icon-theme :gray :icon "block"}
{:filter {:group :commands} :text "Search only commands" :info "Add filter to search" :icon-theme :gray :icon "command"}
{:filter {:group :whiteboards} :text "Search only whiteboards" :info "Add filter to search" :icon-theme :gray :icon "whiteboard"}
@ -61,7 +61,6 @@
{:commands {:status :success :show :less :items nil}
:favorites {:status :success :show :less :items nil}
:current-page {:status :success :show :less :items nil}
:pages {:status :success :show :less :items nil}
:blocks {:status :success :show :less :items nil}
:files {:status :success :show :less :items nil}
:themes {:status :success :show :less :items nil}
@ -108,17 +107,13 @@
page-exists? (when-not (string/blank? input)
(db/get-page (string/trim input)))
include-slash? (or (string/includes? input "/")
(string/starts-with? input "/"))
(string/starts-with? input "/"))
order* (cond
(= search-mode :graph)
[["Pages" :pages (visible-items :pages)]]
[]
include-slash?
[
(when page-exists?
["Pages" :pages (visible-items :pages)])
(when-not page-exists?
[(when-not page-exists?
["Create" :create (create-items input)])
["Current page" :current-page (visible-items :current-page)]
@ -132,21 +127,19 @@
[(if (= filter-group :current-page) "Current page" (name filter-group))
filter-group
(visible-items filter-group)]
(when (= filter-group :pages)
(when-not page-exists?
["Create" :create (create-items input)]))]
(when-not page-exists?
["Create" :create (create-items input)])]
:else
(->>
[["Pages" :pages (visible-items :pages)]
(when-not page-exists?
["Create" :create (create-items input)])
["Commands" :commands (visible-items :commands)]
["Current page" :current-page (visible-items :current-page)]
["Blocks" :blocks (visible-items :blocks)]
["Files" :files (visible-items :files)]
["Filters" :filters (visible-items :filters)]]
(remove nil?)))
[(when-not page-exists?
["Create" :create (create-items input)])
["Current page" :current-page (visible-items :current-page)]
["Blocks" :blocks (visible-items :blocks)]
["Commands" :commands (visible-items :commands)]
["Files" :files (visible-items :files)]
["Filters" :filters (visible-items :filters)]]
(remove nil?)))
order (remove nil? order*)]
(for [[group-name group-key group-items] order]
[group-name
@ -207,27 +200,6 @@
(hash-map :status :success :items)
(swap! !results update group merge)))))
;; The pages search action uses an existing handler
(defmethod load-results :pages [group state]
(let [!input (::input state)
!results (::results state)
repo (state/get-current-repo)]
(swap! !results assoc-in [group :status] :loading)
(p/let [pages (search/page-search @!input)
items (->> pages
(map
(fn [page]
(let [entity (db/entity [:block/uuid (uuid (:id page))])
whiteboard? (contains? (:block/type entity) "whiteboard")
source-page (model/get-alias-source-page repo (:db/id entity))]
(hash-map :icon (if whiteboard? "whiteboard" "page")
:icon-theme :gray
:text (:title page)
:source-page (if source-page
(:block/title source-page)
(:title page)))))))]
(swap! !results update group merge {:status :success :items items}))))
(defmethod load-results :whiteboards [group state]
(let [!input (::input state)
!results (::results state)]
@ -258,14 +230,36 @@
hiccups-add [(when-not (string/blank? b-cut)
[:span b-cut])
(when-not (string/blank? hl-cut)
(let [hl-cut' (string/trimr hl-cut)]
[:mark.p-0.rounded-none hl-cut']))]
[:mark.p-0.rounded-none hl-cut])]
hiccups-add (remove nil? hiccups-add)
new-result (concat result hiccups-add)]
(if-not (string/blank? e-cut)
(recur e-cut new-result)
new-result)))]))
(defn- page-item
[repo page]
(let [entity (db/entity [:block/uuid (:block/uuid page)])
whiteboard? (contains? (:block/type entity) "whiteboard")
source-page (model/get-alias-source-page repo (:db/id entity))]
(hash-map :icon (if whiteboard? "whiteboard" "page")
:icon-theme :gray
:text (:block/title page)
:source-page (if source-page
(:block/title source-page)
(:block/title page)))))
(defn- block-item
[repo block current-page !input]
(let [id (:block/uuid block)]
{:icon "block"
:icon-theme :gray
:text (highlight-content-query (:block/title block) @!input)
:header (block/breadcrumb {:search? true} repo id {})
:current-page? (when-let [page-id (:block/page block)]
(= page-id (:block/uuid current-page)))
:source-block block}))
;; The blocks search action uses an existing handler
(defmethod load-results :blocks [group state]
(let [!input (::input state)
@ -278,15 +272,10 @@
(swap! !results assoc-in [:current-page :status] :loading)
(p/let [blocks (search/block-search repo @!input opts)
blocks (remove nil? blocks)
items (map (fn [block]
(let [id (:block/uuid block)]
{:icon "block"
:icon-theme :gray
:text (highlight-content-query (:block/title block) @!input)
:header (block/breadcrumb {:search? true} repo id {})
:current-page? (when-let [page-id (:block/page block)]
(= page-id (:block/uuid current-page)))
:source-block block})) blocks)
items (keep (fn [block]
(if (:page? block)
(page-item repo block)
(block-item repo block current-page !input))) blocks)
items-on-other-pages (remove :current-page? items)
items-on-current-page (filter :current-page? items)]
(swap! !results update group merge {:status :success :items items-on-other-pages})
@ -387,7 +376,6 @@
(do
(load-results :commands state)
(load-results :blocks state)
(load-results :pages state)
(load-results :filters state)
(load-results :files state)
;; (load-results :recents state)
@ -825,7 +813,7 @@
backspace? (= (util/ekey e) "Backspace")
filter-group (:group @(::filter state))
slash? (= (util/ekey e) "/")
namespace-pages (when (and slash? (contains? #{:pages :whiteboards} filter-group))
namespace-pages (when (and slash? (contains? #{:whiteboards} filter-group))
(search/page-search (str value "/")))
namespace-page-matched? (some #(string/includes? % "/") namespace-pages)]
(when (and filter-group
@ -1010,7 +998,7 @@
(or (= group-filter group-key)
(and (= group-filter :blocks)
(= group-key :current-page))
(and (contains? #{:pages :create} group-filter)
(and (contains? #{:create} group-filter)
(= group-key :create))))))
results-ordered)]
(if (seq items)

View File

@ -483,8 +483,9 @@
;; Search
(search-blocks
[this repo q option]
(p/let [db (get-search-db repo)
result (search/search-blocks db q (bean/->clj option))]
(p/let [search-db (get-search-db repo)
conn (worker-state/get-datascript-conn repo)
result (search/search-blocks repo conn search-db q (bean/->clj option))]
(bean/->js result)))
(search-upsert-blocks
@ -508,18 +509,11 @@
(search-build-blocks-indice
[this repo]
(when-let [conn (worker-state/get-datascript-conn repo)]
(search/build-blocks-indice @conn)))
(search/build-blocks-indice repo @conn)))
(search-build-pages-indice
[this repo]
(when-let [conn (worker-state/get-datascript-conn repo)]
(search/build-page-indice repo @conn)
nil))
(page-search
[this repo q options]
(when-let [conn (worker-state/get-datascript-conn repo)]
(search/page-search repo @conn q (bean/->clj options))))
nil)
(apply-outliner-ops
[this repo ops-str opts-str]

View File

@ -69,7 +69,7 @@
(defn search-handler
[q filters]
(let [{:keys [pages? blocks?]} (js->clj filters {:keywordize-keys true})
(let [{:keys [blocks?]} (js->clj filters {:keywordize-keys true})
repo (state/get-current-repo)
limit 100]
(p/let [blocks (when blocks? (search/block-search repo q {:limit limit}))
@ -77,9 +77,8 @@
(-> b
(update :block/uuid str)
(update :block/title #(->> (text-util/cut-by % "$pfts_2lqh>$" "$<pfts_2lqh$")
(apply str))))) blocks)
pages (when pages? (search/page-search q))]
(clj->js {:pages pages :blocks blocks}))))
(apply str))))) blocks)]
(clj->js {:blocks blocks}))))
(defn save-asset-handler
[file]

View File

@ -1654,14 +1654,6 @@
pages)
(map :title))))
(comment
(defn get-matched-classes
"Return matched class names"
[q]
(let [classes (->> (db-model/get-all-classes (state/get-current-repo))
(map first))]
(search/fuzzy-search classes q {:limit 100}))))
(defn get-matched-blocks
[q block-id]
;; remove current block

View File

@ -33,14 +33,12 @@
page-db-id)
opts (if page-db-id (assoc opts :page (str page-db-id)) opts)]
(p/let [blocks (search/block-search repo q opts)
pages (search/page-search q)
files (search/file-search q)]
(let [result (merge
{:blocks blocks
:has-more? (= limit (count blocks))}
(when-not page-db-id
{:pages pages
:files files}))
{:files files}))
search-key (if more? :search/more-result :search/result)]
(swap! state/state assoc search-key result)
result))))))

View File

@ -34,10 +34,8 @@
(defn page-search
([q]
(page-search q {}))
([q options]
(when-let [^js sqlite @search-browser/*sqlite]
(p/let [result (.page-search sqlite (state/get-current-repo) q (clj->js (merge {:limit 100} options)))]
(bean/->clj result)))))
([q option]
(when-not (string/blank? q) (block-search (state/get-current-repo) q option))))
(defn file-search
([q]

View File

@ -10,11 +10,11 @@
[frontend.worker.util :as worker-util]
[logseq.db.sqlite.util :as sqlite-util]
[logseq.common.util :as common-util]
[logseq.db :as ldb]
[logseq.db.frontend.property :as db-property]))
[logseq.db :as ldb]))
;; TODO: use sqlite for fuzzy search
(defonce indices (atom nil))
;; maybe https://github.com/nalgeon/sqlean/blob/main/docs/fuzzy.md?
(defonce fuzzy-search-indices (atom {}))
(defn- add-blocks-fts-triggers!
"Table bindings of blocks tables and the blocks FTS virtual tables"
@ -125,7 +125,11 @@
:rowMode "array"}))
blocks (bean/->clj result)]
(map (fn [block]
(update block 3 get-snippet-result)) blocks))
(let [[id page title snippet] (update block 3 get-snippet-result)]
{:id id
:page page
:title title
:snippet snippet})) blocks))
(catch :default e
(prn :debug "Search blocks failed: ")
(js/console.error e))))
@ -142,9 +146,116 @@
(string/replace match-input "," "")
(str "\"" match-input "\"*"))))
(defn exact-matched?
"Check if two strings points toward same search result"
[q match]
(when (and (string? q) (string? match))
(boolean
(reduce
(fn [coll char]
(let [coll' (drop-while #(not= char %) coll)]
(if (seq coll')
(rest coll')
(reduced false))))
(seq (worker-util/search-normalize match true))
(seq (worker-util/search-normalize q true))))))
(defn- hidden-page?
[page]
(when page
(if (string? page)
(string/starts-with? page "$$$")
(contains? (set (:block/type page)) "hidden"))))
(defn- page-or-object?
[entity]
(and (or (ldb/page? entity) (seq (:block/tags entity)))
(not (hidden-page? entity))))
(defn get-all-fuzzy-supported-blocks
"Only pages and objects are supported now."
[db]
(let [page-ids (->> (d/datoms db :avet :block/name)
(map :e))
object-ids (->> (d/datoms db :avet :block/tags)
(map :e))
blocks (->> (distinct (concat page-ids object-ids))
(map #(d/entity db %)))]
(remove hidden-page? blocks)))
(defn- sanitize
[content]
(some-> content
(worker-util/search-normalize true)))
(defn block->index
"Convert a block to the index for searching"
[{:block/keys [uuid page title format] :as block}]
(when-not (or
(ldb/closed-value? block)
(and (string? title) (> (count title) 10000))
(string/blank? title)) ; empty page or block
;; Should properties be included in the search indice?
;; It could slow down the search indexing, also it can be confusing
;; if the showing properties are not useful to users.
;; (let [content (if (and db-based? (seq (:block/properties block)))
;; (str content (when (not= content "") "\n") (get-db-properties-str db properties))
;; content)])
(when uuid
{:id (str uuid)
:page (str (or (:block/uuid page) uuid))
:title (sanitize title)
:built-in? (ldb/built-in? block)
:format format})))
(defn build-fuzzy-search-indice
"Build a block title indice from scratch.
Incremental page title indice is implemented in frontend.search.sync-search-indice!"
[repo db]
(let [blocks (->> (get-all-fuzzy-supported-blocks db)
(map block->index)
(bean/->js))
indice (fuse. blocks
(clj->js {:keys ["title"]
:shouldSort true
:tokenize true
:distance 1024
:threshold 0.5 ;; search for 50% match from the start
:minMatchCharLength 1}))]
(swap! fuzzy-search-indices assoc-in repo indice)
indice))
(defn fuzzy-search
"Return a list of blocks (pages && tagged blocks) that match the query. Takes the following
options:
* :limit - Number of result to limit search results. Defaults to 100
* :built-in? - Whether to return built-in pages for db graphs. Defaults to true"
[repo db q {:keys [limit built-in?]
:or {limit 100
built-in? true}}]
(when repo
(let [q (worker-util/search-normalize q true)
q (fuzzy/clean-str q)
q (if (= \# (first q)) (subs q 1) q)]
(when-not (string/blank? q)
(let [indice (or (get @fuzzy-search-indices repo)
(build-fuzzy-search-indice repo db))
result (cond->>
(->> (.search indice q (clj->js {:limit limit}))
(bean/->clj))
(and (sqlite-util/db-based-graph? repo) (= false built-in?))
(remove #(get-in % [:item :built-in?])))]
(->> (map :item result)
(filter (fn [{:keys [title]}]
(exact-matched? q title)))))))))
(defn search-blocks
":page - the page to specifically search on"
[db q {:keys [limit page]}]
"Options:
* :page - the page to specifically search on
* :limit - Number of result to limit search results. Defaults to 100
* :built-in? - Whether to return built-in pages for db graphs. Defaults to true"
[repo conn search-db q {:keys [limit page] :as option}]
(when-not (string/blank? q)
(p/let [match-input (get-match-input q)
limit (or limit 100)
@ -152,33 +263,27 @@
;; the 2nd column in blocks_fts (content)
;; pfts_2lqh is a key for retrieval
;; highlight and snippet only works for some matching with high rank
snippet-aux "snippet(blocks_fts, 1, ' $pfts_2lqh>$ ', ' $<pfts_2lqh$ ', '...', 32)"
snippet-aux "snippet(blocks_fts, 1, '$pfts_2lqh>$', '$<pfts_2lqh$', '...', 32)"
select (str "select id, page, title, " snippet-aux " from blocks_fts where ")
pg-sql (if page "page = ? and" "")
match-sql (str select
pg-sql
" title match ? order by rank limit ?")
matched-result (search-blocks-aux db match-sql match-input page limit)
all-result (->> matched-result
matched-result (search-blocks-aux search-db match-sql match-input page limit)
fuzzy-result (when-not page (fuzzy-search repo @conn q option))
all-result (->> (concat fuzzy-result matched-result)
(map (fn [result]
(let [[id page _title snippet] result]
(let [{:keys [id page title snippet]} result]
{:uuid id
:title snippet
:title (or snippet title)
:page page}))))]
(->>
all-result
(common-util/distinct-by :uuid)))))
(common-util/distinct-by :uuid all-result))))
(defn truncate-table!
[db]
(.exec db "delete from blocks")
(.exec db "delete from blocks_fts"))
(defn- sanitize
[content]
(some-> content
(worker-util/search-normalize true)))
(comment
(defn- property-value-when-closed
"Returns property value if the given entity is type 'closed value' or nil"
@ -217,26 +322,6 @@
(string/join "; " values))))))
(string/join ", "))))
(defn block->index
"Convert a block to the index for searching"
[{:block/keys [uuid page title format] :as block}]
(when-not (or
(ldb/closed-value? block)
(and (string? title) (> (count title) 10000))
(string/blank? title)) ; empty page or block
;; Should properties be included in the search indice?
;; It could slow down the search indexing, also it can be confusing
;; if the showing properties are not useful to users.
;; (let [content (if (and db-based? (seq (:block/properties block)))
;; (str content (when (not= content "") "\n") (get-db-properties-str db properties))
;; content)])
(when uuid
{:id (str uuid)
:page (str (or (:block/uuid page) uuid))
:title (sanitize title)
:format format})))
(defn get-all-block-contents
[db]
(when db
@ -245,51 +330,12 @@
(keep #(d/entity db [:block/uuid %])))))
(defn build-blocks-indice
[db]
[repo db]
(build-fuzzy-search-indice repo db)
(->> (get-all-block-contents db)
(keep block->index)
(bean/->js)))
(defn original-page-name->index
[p]
(when p
{:id (str (:block/uuid p))
:name (:block/name p)
:built-in? (boolean (db-property/property-value-content (:logseq.property/built-in? p)))
:title (:block/title p)}))
(defn- hidden-page?
[page]
(when page
(if (string? page)
(string/starts-with? page "$$$")
(contains? (set (:block/type page)) "hidden"))))
(defn get-all-pages
[db]
(let [page-datoms (d/datoms db :avet :block/name)
pages (map (fn [d] (d/entity db (:e d))) page-datoms)]
(remove (fn [p] (hidden-page? (:block/name p))) pages)))
(defn build-page-indice
"Build a page title indice from scratch.
Incremental page title indice is implemented in frontend.search.sync-search-indice!
Rename from the page indice since 10.25.2022, since this is only used for page title search.
From now on, page indice is talking about page content search."
[repo db]
(let [pages (->> (get-all-pages db)
(map original-page-name->index)
(bean/->js))
indice (fuse. pages
(clj->js {:keys ["title"]
:shouldSort true
:tokenize true
:distance 1024
:threshold 0.5 ;; search for 50% match from the start
:minMatchCharLength 1}))]
(swap! indices assoc-in [repo :pages] indice)
indice))
(defn- get-blocks-from-datoms-impl
[repo {:keys [db-after db-before]} datoms]
(when (seq datoms)
@ -313,7 +359,7 @@
(keep #(d/entity db-after %) blocks-to-add-set')
(remove hidden-page?))})))
(defn- get-direct-blocks-and-pages
(defn- get-affected-blocks
[repo tx-report]
(let [data (:tx-data tx-report)
datoms (filter
@ -326,19 +372,19 @@
(defn sync-search-indice
[repo tx-report]
(let [{:keys [blocks-to-add blocks-to-remove]} (get-direct-blocks-and-pages repo tx-report)]
(let [{:keys [blocks-to-add blocks-to-remove]} (get-affected-blocks repo tx-report)]
;; update page title indice
(let [pages-to-add (filter :block/name blocks-to-add)
pages-to-remove (filter :block/name blocks-to-remove)]
(when (or (seq pages-to-add) (seq pages-to-remove))
(swap! indices update-in [repo :pages]
(let [fuzzy-blocks-to-add (filter page-or-object? blocks-to-add)
fuzzy-blocks-to-remove (filter page-or-object? blocks-to-remove)]
(when (or (seq fuzzy-blocks-to-add) (seq fuzzy-blocks-to-remove))
(swap! fuzzy-search-indices update-in repo
(fn [indice]
(when indice
(doseq [page-entity pages-to-remove]
(doseq [page-entity fuzzy-blocks-to-remove]
(.remove indice (fn [page] (= (str (:block/uuid page-entity)) (gobj/get page "id")))))
(doseq [page pages-to-add]
(doseq [page fuzzy-blocks-to-add]
(.remove indice (fn [p] (= (str (:block/uuid page)) (gobj/get p "id"))))
(.add indice (bean/->js (original-page-name->index page))))
(.add indice (bean/->js (block->index page))))
indice)))))
;; update block indice
@ -347,48 +393,3 @@
blocks-to-remove (set (map (comp str :block/uuid) blocks-to-remove))]
{:blocks-to-remove-set blocks-to-remove
:blocks-to-add blocks-to-add}))))
(defn exact-matched?
"Check if two strings points toward same search result"
[q match]
(when (and (string? q) (string? match))
(boolean
(reduce
(fn [coll char]
(let [coll' (drop-while #(not= char %) coll)]
(if (seq coll')
(rest coll')
(reduced false))))
(seq (worker-util/search-normalize match true))
(seq (worker-util/search-normalize q true))))))
(defn page-search
"Return a list of page names that match the query. Takes the following
options:
* :limit - Number of pages to limit search results. Defaults to 100
* :built-in? - Whether to return built-in pages for db graphs. Defaults to true"
[repo db q {:keys [limit built-in?]
:or {limit 100
built-in? true}}]
(when repo
(let [q (worker-util/search-normalize q true)
q (fuzzy/clean-str q)
q (if (= \# (first q)) (subs q 1) q)]
(when-not (string/blank? q)
(let [indice (or (get-in @indices [repo :pages])
(build-page-indice repo db))
result (cond->>
(->> (.search indice q (clj->js {:limit limit}))
(bean/->clj))
(and (sqlite-util/db-based-graph? repo) (= false built-in?))
(remove #(get-in % [:item :built-in?])))]
(->> result
(keep
(fn [{:keys [item]}]
{:id (:id item)
:title (:title item)}))
(distinct)
(filter (fn [{:keys [title]}]
(exact-matched? q title)))
bean/->js))))))