search.cljs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370
  1. (ns frontend.search
  2. "Provides search functionality for a number of features including Cmd-K
  3. search. Most of these fns depend on the search protocol"
  4. (:require [cljs-bean.core :as bean]
  5. [clojure.string :as string]
  6. [logseq.graph-parser.config :as gp-config]
  7. [frontend.db :as db]
  8. [frontend.db.model :as db-model]
  9. [frontend.regex :as regex]
  10. [frontend.search.agency :as search-agency]
  11. [frontend.search.db :as search-db :refer [indices]]
  12. [frontend.search.protocol :as protocol]
  13. [frontend.state :as state]
  14. [frontend.util :as util]
  15. [goog.object :as gobj]
  16. [promesa.core :as p]
  17. [clojure.set :as set]
  18. [datascript.core :as d]
  19. [frontend.handler.file-based.property.util :as property-util]
  20. [frontend.config :as config]
  21. [logseq.db.property :as db-property]))
  22. (defn get-engine
  23. [repo]
  24. (search-agency/->Agency repo))
  25. ;; Copied from https://gist.github.com/vaughnd/5099299
  26. (defn str-len-distance
  27. ;; normalized multiplier 0-1
  28. ;; measures length distance between strings.
  29. ;; 1 = same length
  30. [s1 s2]
  31. (let [c1 (count s1)
  32. c2 (count s2)
  33. maxed (max c1 c2)
  34. mined (min c1 c2)]
  35. (double (- 1
  36. (/ (- maxed mined)
  37. maxed)))))
  38. (def MAX-STRING-LENGTH 1000.0)
  39. (defn clean-str
  40. [s]
  41. (string/replace (string/lower-case s) #"[\[ \\/_\]\(\)]+" ""))
  42. (def escape-str regex/escape)
  43. (defn char-array
  44. [s]
  45. (bean/->js (seq s)))
  46. (defn score
  47. [oquery ostr]
  48. (let [query (clean-str oquery)
  49. str (clean-str ostr)]
  50. (loop [q (seq (char-array query))
  51. s (seq (char-array str))
  52. mult 1
  53. idx MAX-STRING-LENGTH
  54. score 0]
  55. (cond
  56. ;; add str-len-distance to score, so strings with matches in same position get sorted by length
  57. ;; boost score if we have an exact match including punctuation
  58. (empty? q) (+ score
  59. (str-len-distance query str)
  60. (if (<= 0 (.indexOf ostr oquery)) MAX-STRING-LENGTH 0))
  61. (empty? s) 0
  62. :else (if (= (first q) (first s))
  63. (recur (rest q)
  64. (rest s)
  65. (inc mult) ;; increase the multiplier as more query chars are matched
  66. (dec idx) ;; decrease idx so score gets lowered the further into the string we match
  67. (+ mult score)) ;; score for this match is current multiplier * idx
  68. (recur q
  69. (rest s)
  70. 1 ;; when there is no match, reset multiplier to one
  71. (dec idx)
  72. score))))))
  73. (defn fuzzy-search
  74. [data query & {:keys [limit extract-fn]
  75. :or {limit 20}}]
  76. (let [query (util/search-normalize query (state/enable-search-remove-accents?))]
  77. (->> (take limit
  78. (sort-by :score (comp - compare)
  79. (filter #(< 0 (:score %))
  80. (for [item data]
  81. (let [s (str (if extract-fn (extract-fn item) item))]
  82. {:data item
  83. :score (score query (util/search-normalize s (state/enable-search-remove-accents?)))})))))
  84. (map :data))))
  85. (defn block-search
  86. [repo q option]
  87. (when-let [engine (get-engine repo)]
  88. (let [q (util/search-normalize q (state/enable-search-remove-accents?))
  89. q (if (util/electron?) q (escape-str q))]
  90. (when-not (string/blank? q)
  91. (protocol/query engine q option)))))
  92. (defn page-content-search
  93. [repo q option]
  94. (when-let [engine (get-engine repo)]
  95. (let [q (util/search-normalize q (state/enable-search-remove-accents?))
  96. q (if (util/electron?) q (escape-str q))]
  97. (when-not (string/blank? q)
  98. (protocol/query-page engine q option)))))
  99. (defn- transact-blocks!
  100. [repo data]
  101. (when-let [engine (get-engine repo)]
  102. (protocol/transact-blocks! engine data)))
  103. (defn- transact-pages!
  104. "Transact pages to search engine
  105. :pages-to-remove-set the set of pages to remove (not include those to update)
  106. :pages-to-add the page entities to add"
  107. [repo data]
  108. (when-let [engine (get-engine repo)]
  109. (protocol/transact-pages! engine data)))
  110. (defn exact-matched?
  111. "Check if two strings points toward same search result"
  112. [q match]
  113. (when (and (string? q) (string? match))
  114. (boolean
  115. (reduce
  116. (fn [coll char]
  117. (let [coll' (drop-while #(not= char %) coll)]
  118. (if (seq coll')
  119. (rest coll')
  120. (reduced false))))
  121. (seq (util/search-normalize match (state/enable-search-remove-accents?)))
  122. (seq (util/search-normalize q (state/enable-search-remove-accents?)))))))
  123. (defn page-search
  124. "Return a list of page names that match the query"
  125. ([q]
  126. (page-search q 10))
  127. ([q limit]
  128. (when-let [repo (state/get-current-repo)]
  129. (let [q (util/search-normalize q (state/enable-search-remove-accents?))
  130. q (clean-str q)]
  131. (when-not (string/blank? q)
  132. (let [indice (or (get-in @indices [repo :pages])
  133. (search-db/make-pages-title-indice!))
  134. result (->> (.search indice q (clj->js {:limit limit}))
  135. (bean/->clj))]
  136. ;; TODO: add indexes for highlights
  137. (->> (map
  138. (fn [{:keys [item]}]
  139. (:original-name item))
  140. result)
  141. (remove nil?)
  142. (map string/trim)
  143. (distinct)
  144. (filter (fn [original-name]
  145. (exact-matched? q original-name))))))))))
  146. (defn file-search
  147. ([q]
  148. (file-search q 3))
  149. ([q limit]
  150. (let [q (clean-str q)]
  151. (when-not (string/blank? q)
  152. (let [mldoc-exts (set (map name gp-config/mldoc-support-formats))
  153. files (->> (db/get-files (state/get-current-repo))
  154. (map first)
  155. (remove (fn [file]
  156. (mldoc-exts (util/get-file-ext file)))))]
  157. (when (seq files)
  158. (fuzzy-search files q :limit limit)))))))
  159. (defn template-search
  160. ([q]
  161. (template-search q 100))
  162. ([q limit]
  163. (when q
  164. (let [q (clean-str q)
  165. templates (db/get-all-templates)]
  166. (when (seq templates)
  167. (let [result (fuzzy-search (keys templates) q :limit limit)]
  168. (vec (select-keys templates result))))))))
  169. (defn get-all-properties
  170. []
  171. (let [hidden-props (if (config/db-based-graph? (state/get-current-repo))
  172. (set (map name db-property/hidden-built-in-properties))
  173. (set (map name (property-util/hidden-properties))))]
  174. (remove hidden-props (db-model/get-all-properties))))
  175. (defn property-search
  176. ([q]
  177. (property-search q 100))
  178. ([q limit]
  179. (when q
  180. (let [q (clean-str q)
  181. properties (get-all-properties)]
  182. (when (seq properties)
  183. (if (string/blank? q)
  184. properties
  185. (let [result (fuzzy-search properties q :limit limit)]
  186. (vec result))))))))
  187. (defn property-value-search
  188. ([property q]
  189. (property-value-search property q 100))
  190. ([property q limit]
  191. (when q
  192. (let [q (clean-str q)
  193. result (db-model/get-property-values (keyword property))]
  194. (when (seq result)
  195. (if (string/blank? q)
  196. result
  197. (let [result (fuzzy-search result q :limit limit)]
  198. (vec result))))))))
  199. (defn- get-pages-from-datoms-impl
  200. [pages]
  201. (let [pages-result (db/pull-many '[:db/id :block/name :block/original-name] (set (map :e pages)))
  202. pages-to-add-set (->> (filter :added pages)
  203. (map :e)
  204. (set))
  205. pages-to-add (->> (filter (fn [page]
  206. (contains? pages-to-add-set (:db/id page))) pages-result)
  207. (map (fn [p] (or (:block/original-name p)
  208. (:block/name p))))
  209. (remove string/blank?)
  210. (map search-db/original-page-name->index))
  211. pages-to-remove-set (->> (remove :added pages)
  212. (map :v)
  213. set)
  214. pages-to-remove-id-set (->> (remove :added pages)
  215. (map :e)
  216. set)]
  217. {:pages-to-add pages-to-add
  218. :pages-to-remove-set pages-to-remove-set
  219. :pages-to-add-id-set pages-to-add-set
  220. :pages-to-remove-id-set pages-to-remove-id-set}))
  221. (defn- get-blocks-from-datoms-impl
  222. [blocks]
  223. (when (seq blocks)
  224. (let [blocks-result (->> (db/pull-many '[:db/id :block/uuid :block/format :block/content :block/page] (set (map :e blocks)))
  225. (map (fn [b] (assoc b :block/page (get-in b [:block/page :db/id])))))
  226. blocks-to-add-set (->> (filter :added blocks)
  227. (map :e)
  228. (set))
  229. blocks-to-add (->> (filter (fn [block]
  230. (contains? blocks-to-add-set (:db/id block)))
  231. blocks-result)
  232. (map search-db/block->index)
  233. (remove nil?))
  234. added (set (map :id blocks-to-add))
  235. blocks-to-remove-set (->> (remove :added blocks)
  236. (map :e)
  237. (remove added)
  238. (set))]
  239. {:blocks-to-remove-set blocks-to-remove-set
  240. :blocks-to-add blocks-to-add})))
  241. (defn- get-direct-blocks-and-pages
  242. [tx-report]
  243. (let [data (:tx-data tx-report)
  244. datoms (filter
  245. (fn [datom]
  246. ;; Capture any direct change on page display title, page ref or block content
  247. (contains? #{:block/name :block/original-name :block/content} (:a datom)))
  248. data)]
  249. (when (seq datoms)
  250. (let [datoms (group-by :a datoms)
  251. blocks (:block/content datoms)
  252. pages (concat ;; Duplicated eids are handled in `get-pages-from-datoms-impl`
  253. (:block/name datoms)
  254. (:block/original-name datoms))]
  255. (merge (get-blocks-from-datoms-impl blocks)
  256. (get-pages-from-datoms-impl pages))))))
  257. (defn- get-indirect-pages
  258. "Return the set of pages that will have content updated"
  259. [tx-report]
  260. (let [data (:tx-data tx-report)
  261. datoms (filter
  262. (fn [datom]
  263. (and (:added datom)
  264. (contains? #{:file/content} (:a datom))))
  265. data)]
  266. (when (seq datoms)
  267. (->> datoms
  268. (mapv (fn [datom]
  269. (let [tar-db (:db-after tx-report)]
  270. ;; Reverse query the corresponding page id of the modified `:file/content`)
  271. (when-let [page-id (->> (:e datom)
  272. (d/pull tar-db '[:block/_file])
  273. (:block/_file)
  274. (first)
  275. (:db/id))]
  276. ;; Fetch page entity according to what page->index requested
  277. (d/pull tar-db '[:db/id :block/uuid
  278. :block/original-name
  279. {:block/file [:file/content]}]
  280. page-id)))))
  281. (remove nil?)))))
  282. ;; TODO merge with logic in `invoke-hooks` when feature and test is sufficient
  283. (defn sync-search-indice!
  284. [repo tx-report]
  285. (let [{:keys [pages-to-add pages-to-remove-set pages-to-remove-id-set
  286. blocks-to-add blocks-to-remove-set]} (get-direct-blocks-and-pages tx-report) ;; directly modified block & pages
  287. updated-pages (get-indirect-pages tx-report)]
  288. ;; update page title indice
  289. (when (or (seq pages-to-add) (seq pages-to-remove-set))
  290. (swap! search-db/indices update-in [repo :pages]
  291. (fn [indice]
  292. (when indice
  293. (doseq [page-name pages-to-remove-set]
  294. (.remove indice
  295. (fn [page]
  296. (= (util/safe-page-name-sanity-lc page-name)
  297. (util/safe-page-name-sanity-lc (gobj/get page "original-name"))))))
  298. (when (seq pages-to-add)
  299. (doseq [page pages-to-add]
  300. (.add indice (bean/->js page)))))
  301. indice)))
  302. ;; update block indice
  303. (when (or (seq blocks-to-add) (seq blocks-to-remove-set))
  304. (transact-blocks! repo
  305. {:blocks-to-remove-set blocks-to-remove-set
  306. :blocks-to-add blocks-to-add}))
  307. ;; update page indice
  308. (when (or (seq pages-to-remove-id-set) (seq updated-pages)) ;; when move op happens, no :block/content provided
  309. (let [indice-pages (map search-db/page->index updated-pages)
  310. invalid-set (->> (map (fn [updated indiced] ;; get id of pages without valid page index
  311. (if indiced nil (:db/id updated)))
  312. updated-pages indice-pages)
  313. (remove nil?)
  314. set)
  315. pages-to-add (->> indice-pages
  316. (remove nil?)
  317. set)
  318. pages-to-remove-set (set/union pages-to-remove-id-set invalid-set)]
  319. (transact-pages! repo {:pages-to-remove-set pages-to-remove-set
  320. :pages-to-add pages-to-add})))))
  321. (defn rebuild-indices!
  322. ([]
  323. (rebuild-indices! (state/get-current-repo)))
  324. ([repo]
  325. (when repo
  326. (when-let [engine (get-engine repo)]
  327. (let [page-titles (search-db/make-pages-title-indice!)]
  328. (p/let [blocks (protocol/rebuild-blocks-indice! engine)]
  329. (let [result {:pages page-titles ;; TODO: rename key to :page-titles
  330. :blocks blocks}]
  331. (swap! indices assoc repo result)
  332. indices)))))))
  333. (defn reset-indice!
  334. [repo]
  335. (when-let [engine (get-engine repo)]
  336. (protocol/truncate-blocks! engine))
  337. (swap! indices assoc-in [repo :pages] nil))
  338. (defn remove-db!
  339. [repo]
  340. (when-let [engine (get-engine repo)]
  341. (protocol/remove-db! engine)))