search.cljs 14 KB

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