search.cljs 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. (ns frontend.search
  2. (:require [cljs-bean.core :as bean]
  3. [clojure.string :as string]
  4. [frontend.config :as config]
  5. [frontend.db :as db]
  6. [frontend.regex :as regex]
  7. [frontend.search.browser :as search-browser]
  8. [frontend.search.db :as search-db :refer [indices]]
  9. [frontend.search.node :as search-node]
  10. [frontend.search.protocol :as protocol]
  11. [frontend.state :as state]
  12. [frontend.util :as util]
  13. [goog.object :as gobj]
  14. [promesa.core :as p]))
  15. (defn get-engine
  16. [repo]
  17. (if (util/electron?)
  18. (search-node/->Node repo)
  19. (search-browser/->Browser repo)))
  20. ;; Copied from https://gist.github.com/vaughnd/5099299
  21. (defn str-len-distance
  22. ;; normalized multiplier 0-1
  23. ;; measures length distance between strings.
  24. ;; 1 = same length
  25. [s1 s2]
  26. (let [c1 (count s1)
  27. c2 (count s2)
  28. maxed (max c1 c2)
  29. mined (min c1 c2)]
  30. (double (- 1
  31. (/ (- maxed mined)
  32. maxed)))))
  33. (def MAX-STRING-LENGTH 1000.0)
  34. (defn clean-str
  35. [s]
  36. (string/replace (string/lower-case s) #"[\[ \\/_\]\(\)]+" ""))
  37. (def escape-str regex/escape)
  38. (defn char-array
  39. [s]
  40. (bean/->js (seq s)))
  41. (defn score
  42. [oquery ostr]
  43. (let [query (clean-str oquery)
  44. str (clean-str ostr)]
  45. (loop [q (seq (char-array query))
  46. s (seq (char-array str))
  47. mult 1
  48. idx MAX-STRING-LENGTH
  49. score 0]
  50. (cond
  51. ;; add str-len-distance to score, so strings with matches in same position get sorted by length
  52. ;; boost score if we have an exact match including punctuation
  53. (empty? q) (+ score
  54. (str-len-distance query str)
  55. (if (<= 0 (.indexOf ostr oquery)) MAX-STRING-LENGTH 0))
  56. (empty? s) 0
  57. :default (if (= (first q) (first s))
  58. (recur (rest q)
  59. (rest s)
  60. (inc mult) ;; increase the multiplier as more query chars are matched
  61. (dec idx) ;; decrease idx so score gets lowered the further into the string we match
  62. (+ mult score)) ;; score for this match is current multiplier * idx
  63. (recur q
  64. (rest s)
  65. 1 ;; when there is no match, reset multiplier to one
  66. (dec idx)
  67. score))))))
  68. (defn fuzzy-search
  69. [data query & {:keys [limit extract-fn]
  70. :or {limit 20}}]
  71. (let [query (util/search-normalize query)]
  72. (->> (take limit
  73. (sort-by :score (comp - compare)
  74. (filter #(< 0 (:score %))
  75. (for [item data]
  76. (let [s (str (if extract-fn (extract-fn item) item))]
  77. {:data item
  78. :score (score query (util/search-normalize s))})))))
  79. (map :data))))
  80. (defn block-search
  81. [repo q option]
  82. (when-let [engine (get-engine repo)]
  83. (let [q (util/search-normalize q)
  84. q (if (util/electron?) q (escape-str q))]
  85. (when-not (string/blank? q)
  86. (protocol/query engine q option)))))
  87. (defn transact-blocks!
  88. [repo data]
  89. (when-let [engine (get-engine repo)]
  90. (protocol/transact-blocks! engine data)))
  91. (defn exact-matched?
  92. "Check if two strings the same thing"
  93. [q match]
  94. (when (and (string? q) (string? match))
  95. (boolean
  96. (reduce
  97. (fn [coll char]
  98. (let [coll' (drop-while #(not= char %) coll)]
  99. (if (seq coll')
  100. (rest coll')
  101. (reduced false))))
  102. (seq (util/search-normalize match))
  103. (seq (util/search-normalize q))))))
  104. (defn page-search
  105. "Return a list of page names that match the query"
  106. ([q]
  107. (page-search q 10))
  108. ([q limit]
  109. (when-let [repo (state/get-current-repo)]
  110. (let [q (util/search-normalize q)
  111. q (clean-str q)]
  112. (when-not (string/blank? q)
  113. (let [indice (or (get-in @indices [repo :pages])
  114. (search-db/make-pages-indice!))
  115. result (->> (.search indice q (clj->js {:limit limit}))
  116. (bean/->clj))]
  117. ;; TODO: add indexes for highlights
  118. (->> (map
  119. (fn [{:keys [item]}]
  120. (:name item))
  121. result)
  122. (remove nil?)
  123. (map string/trim)
  124. (distinct)
  125. (filter (fn [name]
  126. (exact-matched? q name))))))))))
  127. (defn file-search
  128. ([q]
  129. (file-search q 3))
  130. ([q limit]
  131. (let [q (clean-str q)]
  132. (when-not (string/blank? q)
  133. (let [mldoc-exts (set (map name config/mldoc-support-formats))
  134. files (->> (db/get-files (state/get-current-repo))
  135. (map first)
  136. (remove (fn [file]
  137. (mldoc-exts (util/get-file-ext file)))))]
  138. (when (seq files)
  139. (fuzzy-search files q :limit limit)))))))
  140. (defn template-search
  141. ([q]
  142. (template-search q 10))
  143. ([q limit]
  144. (let [q (clean-str q)]
  145. (let [templates (db/get-all-templates)]
  146. (when (seq templates)
  147. (let [result (fuzzy-search (keys templates) q :limit limit)]
  148. (vec (select-keys templates result))))))))
  149. (defn sync-search-indice!
  150. [datoms]
  151. (when (seq datoms)
  152. (when-let [repo (state/get-current-repo)]
  153. (let [datoms (group-by :a datoms)
  154. pages (:block/name datoms)
  155. blocks (:block/content datoms)]
  156. (when (seq pages)
  157. (let [pages-result (db/pull-many '[:db/id :block/name :block/original-name] (set (map :e pages)))
  158. pages-to-add-set (->> (filter :added pages)
  159. (map :e)
  160. (set))
  161. pages-to-add (->> (filter (fn [page]
  162. (contains? pages-to-add-set (:db/id page))) pages-result)
  163. (map (fn [p] {:name (or (:block/original-name p)
  164. (:block/name p))})))
  165. pages-to-remove-set (->> (remove :added pages)
  166. (map :v))]
  167. (swap! search-db/indices update-in [repo :pages]
  168. (fn [indice]
  169. (when indice
  170. (doseq [page-name pages-to-remove-set]
  171. (.remove indice
  172. (fn [page]
  173. (= (util/safe-lower-case page-name)
  174. (util/safe-lower-case (gobj/get page "name"))))))
  175. (when (seq pages-to-add)
  176. (doseq [page pages-to-add]
  177. (.add indice (bean/->js page)))))
  178. indice))))
  179. (when (seq blocks)
  180. (let [blocks-result (->> (db/pull-many '[:db/id :block/uuid :block/format :block/content :block/page] (set (map :e blocks)))
  181. (map (fn [b] (assoc b :block/page (get-in b [:block/page :db/id])))))
  182. blocks-to-add-set (->> (filter :added blocks)
  183. (map :e)
  184. (set))
  185. blocks-to-add (->> (filter (fn [block]
  186. (contains? blocks-to-add-set (:db/id block)))
  187. blocks-result)
  188. (map search-db/block->index))
  189. blocks-to-remove-set (->> (remove :added blocks)
  190. (map :e)
  191. (set))]
  192. (transact-blocks! repo
  193. {:blocks-to-remove-set blocks-to-remove-set
  194. :blocks-to-add blocks-to-add})))))))
  195. (defn rebuild-indices!
  196. ([]
  197. (rebuild-indices! (state/get-current-repo)))
  198. ([repo]
  199. (when repo
  200. (when-let [engine (get-engine repo)]
  201. (let [pages (search-db/make-pages-indice!)]
  202. (p/let [blocks (protocol/rebuild-blocks-indice! engine)]
  203. (let [result {:pages pages
  204. :blocks blocks}]
  205. (swap! indices assoc repo result)
  206. indices)))))))
  207. (defn reset-indice!
  208. [repo]
  209. (when-let [engine (get-engine repo)]
  210. (protocol/truncate-blocks! engine))
  211. (swap! indices assoc-in [repo :pages] nil))
  212. (defn remove-db!
  213. [repo]
  214. (when-let [engine (get-engine repo)]
  215. (protocol/remove-db! engine)))