Browse Source

Merge pull request #12009 from logseq/feat/hnswlib+transformer-js

feat: semantic search
Tienson Qin 2 tháng trước cách đây
mục cha
commit
a2eee34d80
100 tập tin đã thay đổi với 2002 bổ sung571 xóa
  1. 1 0
      .cljfmt.edn
  2. 1 1
      .github/workflows/clj-e2e.yml
  3. 1 1
      .github/workflows/clj-rtc-e2e.yml
  4. 1 1
      .github/workflows/deploy-db-test-pages.yml
  5. 3 3
      clj-e2e/src/logseq/e2e/page.clj
  6. 6 6
      clj-e2e/test/logseq/e2e/plugins_basic_test.clj
  7. 3 6
      deps.edn
  8. 1 1
      deps/common/src/logseq/common/path.cljs
  9. 1 1
      deps/db/src/logseq/db/common/delete_blocks.cljs
  10. 2 2
      deps/db/src/logseq/db/common/entity_plus.cljc
  11. 1 0
      deps/db/src/logseq/db/common/initial_data.cljs
  12. 2 2
      deps/db/src/logseq/db/file_based/schema.cljs
  13. 13 13
      deps/db/src/logseq/db/frontend/db_ident.cljc
  14. 6 1
      deps/db/src/logseq/db/frontend/kv_entity.cljs
  15. 9 2
      deps/db/src/logseq/db/frontend/property.cljs
  16. 1 1
      deps/db/src/logseq/db/frontend/schema.cljs
  17. 1 1
      deps/db/test/logseq/db/common/initial_data_test.cljs
  18. 2 2
      deps/db/test/logseq/db/sqlite/build_test.cljs
  19. 1 1
      deps/outliner/src/logseq/outliner/core.cljs
  20. 8 5
      deps/outliner/src/logseq/outliner/pipeline.cljs
  21. 3 3
      deps/publishing/src/logseq/publishing/export.cljs
  22. 1 1
      deps/publishing/src/logseq/publishing/html.cljs
  23. 32 32
      deps/shui/src/logseq/shui/util.cljs
  24. 86 0
      externs/app.txt
  25. 87 0
      externs/mobile.txt
  26. 2 2
      gulpfile.js
  27. 39 28
      package.json
  28. 69 0
      public/index.html
  29. 1 1
      resources/index.html
  30. 1 1
      resources/mobile/index.html
  31. 5 5
      scripts/src/logseq/tasks/db_graph/create_graph_with_schema_org.cljs
  32. 11 11
      scripts/src/logseq/tasks/dev.clj
  33. 4 1
      scripts/src/logseq/tasks/dev/db_and_file_graphs.clj
  34. 9 9
      scripts/src/logseq/tasks/dev/lint.clj
  35. 77 52
      shadow-cljs.edn
  36. 0 18
      src/dev-cljs/shadow/hooks.clj
  37. 3 3
      src/dev-cljs/shadow/user.clj
  38. 4 4
      src/electron/electron/fs_watcher.cljs
  39. 2 2
      src/electron/electron/utils.cljs
  40. 33 33
      src/electron/electron/window.cljs
  41. 10 10
      src/main/frontend/common/file/util.cljs
  42. 21 22
      src/main/frontend/common/search_fuzzy.cljs
  43. 4 2
      src/main/frontend/common/thread_api.cljc
  44. 5 5
      src/main/frontend/components/block/macros.cljs
  45. 2 4
      src/main/frontend/components/cmdk/core.cljs
  46. 1 1
      src/main/frontend/components/content.cljs
  47. 1 2
      src/main/frontend/components/file.cljs
  48. 14 14
      src/main/frontend/components/file_based/block.cljs
  49. 10 10
      src/main/frontend/components/file_based/query.cljs
  50. 33 4
      src/main/frontend/components/header.cljs
  51. 11 0
      src/main/frontend/components/right_sidebar.cljs
  52. 92 1
      src/main/frontend/components/settings.cljs
  53. 1 1
      src/main/frontend/components/settings.css
  54. 4 4
      src/main/frontend/components/shortcut.cljs
  55. 119 0
      src/main/frontend/components/vector_search/sidebar.cljs
  56. 1 0
      src/main/frontend/core.cljs
  57. 1 1
      src/main/frontend/db/file_based/model.cljs
  58. 27 27
      src/main/frontend/db/react.cljs
  59. 2 2
      src/main/frontend/db/utils.cljs
  60. 12 12
      src/main/frontend/extensions/latex.cljs
  61. 2 4
      src/main/frontend/fs/diff_merge.cljs
  62. 1 1
      src/main/frontend/fs/watcher_handler.cljs
  63. 9 1
      src/main/frontend/handler.cljs
  64. 15 15
      src/main/frontend/handler/db_based/property/util.cljs
  65. 28 0
      src/main/frontend/handler/db_based/vector_search_background_tasks.cljs
  66. 22 0
      src/main/frontend/handler/db_based/vector_search_flows.cljs
  67. 6 0
      src/main/frontend/handler/events.cljs
  68. 13 13
      src/main/frontend/handler/export/opml.cljs
  69. 19 19
      src/main/frontend/handler/export/text.cljs
  70. 2 2
      src/main/frontend/handler/file_based/page_property.cljs
  71. 1 1
      src/main/frontend/handler/plugin.cljs
  72. 7 1
      src/main/frontend/handler/worker.cljs
  73. 72 0
      src/main/frontend/inference_worker/inference_worker.cljs
  74. 10 0
      src/main/frontend/inference_worker/state.cljs
  75. 237 0
      src/main/frontend/inference_worker/text_embedding.cljs
  76. 2 1
      src/main/frontend/mixins.cljs
  77. 1 1
      src/main/frontend/mobile/intent.cljs
  78. 2 1
      src/main/frontend/modules/outliner/pipeline.cljs
  79. 4 4
      src/main/frontend/modules/shortcut/core.cljs
  80. 34 2
      src/main/frontend/persist_db/browser.cljs
  81. 6 1
      src/main/frontend/state.cljs
  82. 1 1
      src/main/frontend/template.cljs
  83. 10 8
      src/main/frontend/ui.cljs
  84. 1 1
      src/main/frontend/util.cljc
  85. 2 2
      src/main/frontend/utils.js
  86. 3 2
      src/main/frontend/worker/db/migrate.cljs
  87. 71 35
      src/main/frontend/worker/db_worker.cljs
  88. 295 0
      src/main/frontend/worker/embedding.cljs
  89. 1 1
      src/main/frontend/worker/file.cljs
  90. 1 1
      src/main/frontend/worker/file/reset.cljs
  91. 1 1
      src/main/frontend/worker/pipeline.cljs
  92. 4 3
      src/main/frontend/worker/rtc/core.cljs
  93. 1 1
      src/main/frontend/worker/rtc/full_upload_download_graph.cljs
  94. 1 1
      src/main/frontend/worker/rtc/remote_update.cljs
  95. 1 1
      src/main/frontend/worker/rtc/ws_util.cljs
  96. 146 66
      src/main/frontend/worker/search.cljs
  97. 1 0
      src/main/frontend/worker/state.cljs
  98. 2 2
      src/main/frontend/worker_common/util.cljc
  99. 2 0
      src/resources/dicts/en.edn
  100. 61 0
      webpack.config.js

+ 1 - 0
.cljfmt.edn

@@ -1,4 +1,5 @@
 {:extra-indents {missionary.core/sp [[:block 0]]
                  missionary.core/ap [[:block 0]]
+                 frontend.util/profile [[:inner 0]]
                  frontend.common.missionary/run-task [[:inner 0]]}
  :sort-ns-references? true}

+ 1 - 1
.github/workflows/clj-e2e.yml

@@ -78,7 +78,7 @@ jobs:
       # NOTE: require the app to be build with DEV-RELEASE flag
       - name: Prepare E2E test build
         run: |
-          yarn gulp:build && clojure -M:cljs release app --config-merge "{:closure-defines {frontend.config/DEV-RELEASE true}}" --debug
+          yarn gulp:build && clojure -M:cljs release app workers --config-merge "{:closure-defines {frontend.config/DEV-RELEASE true}}" --debug && yarn webpack-app-build
 
       - name: Run e2e tests
         run: cd clj-e2e && timeout 30m bb dev

+ 1 - 1
.github/workflows/clj-rtc-e2e.yml

@@ -79,7 +79,7 @@ jobs:
       # NOTE: require the app to be build with DEV-RELEASE flag
       - name: Prepare E2E test build
         run: |
-          yarn gulp:build && clojure -M:cljs release app --config-merge "{:closure-defines {frontend.config/DEV-RELEASE true}}" --debug
+          yarn gulp:build && clojure -M:cljs release app workers --config-merge "{:closure-defines {frontend.config/DEV-RELEASE true}}" --debug && yarn webpack-app-build
           rsync -avz --exclude node_modules --exclude android --exclude ios ./static/ ./public/
           ls -lR ./public
 

+ 1 - 1
.github/workflows/deploy-db-test-pages.yml

@@ -42,7 +42,7 @@ jobs:
 
       - name: Build Released-Web
         run: |
-          yarn gulp:build && clojure -M:cljs release app  --config-merge '{:compiler-options {:source-map true :source-map-include-sources-content true :source-map-detail-level :symbols}}'
+          yarn gulp:build && clojure -M:cljs release app workers  --config-merge '{:compiler-options {:source-map true :source-map-include-sources-content true :source-map-detail-level :symbols}}' && yarn webpack-app-build
           rsync -avz --exclude node_modules --exclude android --exclude ios ./static/ ./public/
           ls -lR ./public && mkdir r2 && mv ./public/js/main.js.map ./r2/db-test.main.js.map
           sed -i 's/=main.js.map/=https:\/\/assets.logseq.io\/db-test.main.js.map/g' ./public/js/main.js

+ 3 - 3
clj-e2e/src/logseq/e2e/page.clj

@@ -1,9 +1,9 @@
 (ns logseq.e2e.page
-  (:require [logseq.e2e.keyboard :as k]
+  (:require [logseq.e2e.block :as b]
+            [logseq.e2e.keyboard :as k]
             [logseq.e2e.util :as util]
             [wally.main :as w]
-            [wally.selectors :as ws]
-            [logseq.e2e.block :as b])
+            [wally.selectors :as ws])
   (:import (com.microsoft.playwright TimeoutError)))
 
 (defn goto-page

+ 6 - 6
clj-e2e/test/logseq/e2e/plugins_basic_test.clj

@@ -25,14 +25,14 @@
   (when (string? s)
     (-> s
       ;; Normalize input: replace hyphens/spaces with underscores, collapse multiple underscores
-      (clojure.string/replace #"[-\s]+" "_")
+        (clojure.string/replace #"[-\s]+" "_")
       ;; Split on uppercase letters (except at start) and join with underscore
-      (clojure.string/replace #"(?<!^)([A-Z])" "_$1")
+        (clojure.string/replace #"(?<!^)([A-Z])" "_$1")
       ;; Remove redundant underscores and trim
-      (clojure.string/replace #"_+" "_")
-      (clojure.string/trim)
+        (clojure.string/replace #"_+" "_")
+        (clojure.string/trim)
       ;; Convert to lowercase
-      (clojure.string/lower-case))))
+        (clojure.string/lower-case))))
 
 (defn- ls-api-call!
   [tag & args]
@@ -63,7 +63,7 @@
     (let [ret (ls-api-call! :editor.appendBlockInPage "test-block-apis" "append-block-in-page-0")
           uuid' (assert-api-ls-block! ret)]
       (-> (ls-api-call! :editor.insertBlock uuid' "insert-0")
-        (assert-api-ls-block!))
+          (assert-api-ls-block!))
       (ls-api-call! :editor.updateBlock uuid' "append-but-updated-0")
       (k/esc)
       (w/wait-for ".block-title-wrap:text('append-but-updated-0')")

+ 3 - 6
deps.edn

@@ -53,16 +53,14 @@
                                           :git/sha "d61ce7e29186de021a2a453a8cee68efb5a88440"}}
 
  :aliases {:cljs {:extra-paths ["src/dev-cljs/" "src/test/" "src/electron/"]
-                  :extra-deps  {org.clojure/clojurescript        {:mvn/version "1.11.132"}
-                                org.clojure/tools.namespace      {:mvn/version "0.2.11"}
+                  :extra-deps  {org.clojure/tools.namespace      {:mvn/version "0.2.11"}
                                 cider/cider-nrepl                {:mvn/version "0.55.1"}
                                 org.clojars.knubie/cljs-run-test {:mvn/version "1.0.1"}
                                 tortue/spy                       {:mvn/version "2.14.0"}}
                   :main-opts   ["-m" "shadow.cljs.devtools.cli"]}
 
            :test {:extra-paths ["src/test/"]
-                  :extra-deps  {org.clojure/clojurescript        {:mvn/version "1.11.132"}
-                                org.clojure/test.check           {:mvn/version "1.1.1"}
+                  :extra-deps  {org.clojure/test.check           {:mvn/version "1.1.1"}
                                 pjstadig/humane-test-output      {:mvn/version "0.11.0"}
                                 org.clojars.knubie/cljs-run-test {:mvn/version "1.0.1"}
                                 tortue/spy                       {:mvn/version "2.14.0"}
@@ -70,8 +68,7 @@
                   :main-opts   ["-m" "shadow.cljs.devtools.cli"]}
 
            :rtc-e2e-test {:extra-paths ["src/rtc_e2e_test"]
-                          :extra-deps {org.clojure/clojurescript        {:mvn/version "1.11.132"}
-                                       cider/cider-nrepl                {:mvn/version "0.50.2"}}
+                          :extra-deps {cider/cider-nrepl                {:mvn/version "0.50.2"}}
                           :main-opts ["-m" "shadow.cljs.devtools.cli"]}
 
            :bench {:extra-paths ["src/bench/"]

+ 1 - 1
deps/common/src/logseq/common/path.cljs

@@ -148,7 +148,7 @@
         scheme (.-protocol url)
         path (.-pathname url)
         domain (or (not-empty (.-host url))
-                 (if (string/starts-with? path "/") "" "/"))
+                   (if (string/starts-with? path "/") "" "/"))
         encoded-new-path (apply uri-path-join-internal path segments)]
     (str scheme "//" domain encoded-new-path)))
 

+ 1 - 1
deps/db/src/logseq/db/common/delete_blocks.cljs

@@ -5,8 +5,8 @@
             [logseq.common.util :as common-util]
             [logseq.common.util.block-ref :as block-ref]
             [logseq.common.util.page-ref :as page-ref]
-            [logseq.db.common.entity-util :as common-entity-util]
             [logseq.db.common.entity-plus :as entity-plus]
+            [logseq.db.common.entity-util :as common-entity-util]
             [logseq.db.frontend.entity-util :as entity-util]))
 
 (defn- replace-ref-with-deleted-block-title

+ 2 - 2
deps/db/src/logseq/db/common/entity_plus.cljc

@@ -95,8 +95,8 @@
        (get (.-kv e) k)
        (if db-based?
          (let [result (lookup-entity e k default-value)
-             ;; Replace title for pages only, otherwise it'll recursively
-             ;; replace block id refs if there're cycle references of blocks
+               ;; Replace title for pages only, otherwise it'll recursively
+               ;; replace block id refs if there're cycle references of blocks
                refs (:block/refs e)
                result' (if (and (string? result) refs)
                          (db-content/id-ref->title-ref result refs

+ 1 - 0
deps/db/src/logseq/db/common/initial_data.cljs

@@ -344,6 +344,7 @@
                         :logseq.kv/graph-uuid
                         :logseq.kv/latest-code-lang
                         :logseq.kv/graph-backup-folder
+                        :logseq.kv/graph-text-embedding-model-name
                         :logseq.property/empty-placeholder])
         favorites (when db-graph? (get-favorites db))
         views (when db-graph? (get-views-data db))

+ 2 - 2
deps/db/src/logseq/db/file_based/schema.cljs

@@ -32,8 +32,8 @@
    :block/path-refs {:db/valueType   :db.type/ref
                      :db/cardinality :db.cardinality/many}
 
-  :block/tags {:db/valueType :db.type/ref
-               :db/cardinality :db.cardinality/many}
+   :block/tags {:db/valueType :db.type/ref
+                :db/cardinality :db.cardinality/many}
 
    ;; which block this block links to, used for tag, embeds
    :block/link {:db/valueType :db.type/ref

+ 13 - 13
deps/db/src/logseq/db/frontend/db_ident.cljc

@@ -68,24 +68,24 @@
   ([user-namespace name-string random-suffix?]
    {:pre [(or (keyword? user-namespace) (string? user-namespace)) (string? name-string) (boolean? random-suffix?)]}
    (assert (not (re-find #"^(logseq|block)(\.|$)" (name user-namespace)))
-     "New ident is not allowed to use an internal namespace")
+           "New ident is not allowed to use an internal namespace")
    (if #?(:org.babashka/nbb true
           :cljs             (or (false? random-suffix?)
-                              (and (exists? js/process)
-                                (or js/process.env.REPEATABLE_IDENTS js/process.env.DB_GRAPH)))
+                                (and (exists? js/process)
+                                     (or js/process.env.REPEATABLE_IDENTS js/process.env.DB_GRAPH)))
           :default          false)
      ;; Used for contexts where we want repeatable idents e.g. tests and CLIs
      (keyword user-namespace
-       (->> (string/replace-first name-string #"^(\d)" "NUM-$1")
+              (->> (string/replace-first name-string #"^(\d)" "NUM-$1")
          ;; '-' must go last in char class
-         (filter #(re-find #"[0-9a-zA-Z*+!_'?<>=-]{1}" %))
-         (apply str)))
+                   (filter #(re-find #"[0-9a-zA-Z*+!_'?<>=-]{1}" %))
+                   (apply str)))
      (keyword user-namespace
-       (str
-         (->> (string/replace-first name-string #"^(\d)" "NUM-$1")
+              (str
+               (->> (string/replace-first name-string #"^(\d)" "NUM-$1")
            ;; '-' must go last in char class
-           (filter #(re-find #"[0-9a-zA-Z*+!_'?<>=-]{1}" %))
-           (apply str))
-         "-"
-         (rand-nth non-int-char-range)
-         (nano-id 7))))))
+                    (filter #(re-find #"[0-9a-zA-Z*+!_'?<>=-]{1}" %))
+                    (apply str))
+               "-"
+               (rand-nth non-int-char-range)
+               (nano-id 7))))))

+ 6 - 1
deps/db/src/logseq/db/frontend/kv_entity.cljs

@@ -26,6 +26,11 @@ RTC won't start when major-schema-versions don't match"
                                            :rtc {:rtc/ignore-entity-when-init-upload true
                                                  :rtc/ignore-entity-when-init-download true}}
   :logseq.kv/graph-initial-schema-version {:doc "Graph's schema version when created"}
+
   :logseq.kv/graph-last-gc-at             {:doc "Last time graph gc at"
                                            :rtc {:rtc/ignore-entity-when-init-upload true
-                                                 :rtc/ignore-entity-when-init-download true}})
+                                                 :rtc/ignore-entity-when-init-download true}}
+
+  :logseq.kv/graph-text-embedding-model-name   {:doc "Graph's text-embedding model name"
+                                                :rtc {:rtc/ignore-entity-when-init-upload true
+                                                      :rtc/ignore-entity-when-init-download true}})

+ 9 - 2
deps/db/src/logseq/db/frontend/property.cljs

@@ -559,7 +559,14 @@
                                            :schema {:type :class
                                                     :cardinality :many
                                                     :public? true}
-                                           :queryable? true})))
+                                           :queryable? true}
+     :logseq.property.embedding/hnsw-label-updated-at {:title "HNSW label updated-at"
+                                                       :schema {:type :datetime
+                                                                :public? false
+                                                                :hide? true}
+                                                       :rtc {:rtc/ignore-attr-when-init-upload true
+                                                             :rtc/ignore-attr-when-init-download true
+                                                             :rtc/ignore-attr-when-syncing true}})))
 
 (def db-attribute-properties
   "Internal properties that are also db schema attributes"
@@ -611,7 +618,7 @@
     "logseq.property.linked-references" "logseq.property.asset" "logseq.property.table" "logseq.property.node"
     "logseq.property.code" "logseq.property.repeat"
     "logseq.property.journal" "logseq.property.class" "logseq.property.view"
-    "logseq.property.user" "logseq.property.history"})
+    "logseq.property.user" "logseq.property.history" "logseq.property.embedding"})
 
 (defn logseq-property?
   "Determines if keyword is a logseq property"

+ 1 - 1
deps/db/src/logseq/db/frontend/schema.cljs

@@ -37,7 +37,7 @@
          (map (juxt :major :minor)
               [(parse-schema-version x) (parse-schema-version y)])))
 
-(def version (parse-schema-version "65.8"))
+(def version (parse-schema-version "65.9"))
 
 (defn major-version
   "Return a number.

+ 1 - 1
deps/db/test/logseq/db/common/initial_data_test.cljs

@@ -6,8 +6,8 @@
             [cljs.test :refer [deftest async use-fixtures is testing]]
             [datascript.core :as d]
             [logseq.db.common.initial-data :as common-initial-data]
-            [logseq.db.sqlite.build :as sqlite-build]
             [logseq.db.common.sqlite-cli :as sqlite-cli]
+            [logseq.db.sqlite.build :as sqlite-build]
             [logseq.db.sqlite.create-graph :as sqlite-create-graph]
             [logseq.db.test.helper :as db-test]))
 

+ 2 - 2
deps/db/test/logseq/db/sqlite/build_test.cljs

@@ -3,10 +3,10 @@
             [datascript.core :as d]
             [logseq.common.util.page-ref :as page-ref]
             [logseq.db :as ldb]
+            [logseq.db.frontend.entity-util :as entity-util]
             [logseq.db.frontend.property :as db-property]
             [logseq.db.sqlite.build :as sqlite-build]
-            [logseq.db.test.helper :as db-test]
-            [logseq.db.frontend.entity-util :as entity-util]))
+            [logseq.db.test.helper :as db-test]))
 
 (deftest build-tags
   (let [conn (db-test/create-conn)

+ 1 - 1
deps/outliner/src/logseq/outliner/core.cljs

@@ -251,7 +251,7 @@
           collapse-or-expand? (= outliner-op :collapse-expand-blocks)
           m* (cond->
               (-> data'
-                  (dissoc :block/children :block/meta :block/unordered
+                  (dissoc :block/children :block/meta :block/unordered :block/path-refs
                           :block.temp/ast-title :block.temp/ast-body :block/level :block.temp/load-status
                           :block.temp/has-children?)
                   common-util/remove-nils

+ 8 - 5
deps/outliner/src/logseq/outliner/pipeline.cljs

@@ -50,7 +50,7 @@
                                    (recur (into parent-refs (:block-ref-ids parent))
                                           (:parent-id parent))
                                    ;; exits when top-level parent is reached
-                                   parent-refs)))})
+                                   (remove nil? parent-refs))))})
                            children-maps)]
     children-refs))
 
@@ -87,10 +87,13 @@
                        old-refs (if db-before
                                   (set (map :db/id (:block/path-refs (d/entity db-before (:db/id block)))))
                                   #{})
-                       new-refs (set (concat
-                                      (some-> (:db/id (:block/page block)) vector)
-                                      (map :db/id (:block/refs block))
-                                      parents-refs))
+                       new-refs (->>
+                                 (concat
+                                  (some-> (:db/id (:block/page block)) vector)
+                                  (map :db/id (:block/refs block))
+                                  parents-refs)
+                                 (remove nil?)
+                                 set)
                        refs-changed? (not= old-refs new-refs)
                        children (when refs-changed?
                                   (when-not page?

+ 3 - 3
deps/publishing/src/logseq/publishing/export.cljs

@@ -1,14 +1,14 @@
 (ns ^:node-only logseq.publishing.export
   "This electron only ns (for the main process) exports files from multiple
   locations to provide a complete publishing app"
-  (:require ["fs-extra" :as fse]
+  (:require ["fs" :as fs]
+            ["fs-extra" :as fse]
             ["path" :as node-path]
-            ["fs" :as fs]
             [promesa.core :as p]))
 
 (def ^:api js-files
   "js files from publishing release build"
-  (->> ["shared.js" "main.js" "code-editor.js" "excalidraw.js" "tldraw.js" "db-worker.js"]
+  (->> ["main.js" "code-editor.js" "excalidraw.js" "tldraw.js"]
        ;; Add source maps for all js files as it doesn't affect initial load time
        (mapcat #(vector % (str % ".map")))
        vec))

+ 1 - 1
deps/publishing/src/logseq/publishing/html.cljs

@@ -124,7 +124,7 @@ necessary db filtering"
             [:script {:src "static/js/react.production.min.js"}]
             [:script {:src "static/js/react-dom.production.min.js"}]
             [:script {:src "static/js/ui.js"}]
-            [:script {:src "static/js/shared.js"}]
+            [:script {:src "static/js/main-bundle.js"}]
             [:script {:src "static/js/main.js"}]
             ;; Deferring scripts above results in errors
             [:script {:defer true :src "static/js/interact.min.js"}]

+ 32 - 32
deps/shui/src/logseq/shui/util.cljs

@@ -1,13 +1,13 @@
 (ns logseq.shui.util
   (:require
-   [clojure.string :as s]
-   [rum.core :refer [use-state use-effect!] :as rum]
-   [logseq.shui.rum :as shui-rum]
-   [goog.object :refer [getValueByKeys] :as gobj]
+   [cljs-bean.core :as bean]
    [clojure.set :refer [rename-keys]]
+   [clojure.string :as s]
    [clojure.walk :as w]
-   [cljs-bean.core :as bean]
-   [goog.dom :as gdom]))
+   [goog.dom :as gdom]
+   [goog.object :refer [getValueByKeys] :as gobj]
+   [logseq.shui.rum :as shui-rum]
+   [rum.core :refer [use-state use-effect!] :as rum]))
 
 (goog-define NODETEST false)
 
@@ -16,7 +16,7 @@
   [input]
   (let [words (s/split input #"-")
         capitalize (->> (rest words)
-                     (map #(apply str (s/upper-case (first %)) (rest %))))]
+                        (map #(apply str (s/upper-case (first %)) (rest %))))]
     (apply str (first words) capitalize)))
 
 (defn map-keys->camel-case
@@ -28,7 +28,7 @@
   (let [convert-to-camel (fn [[key value]]
                            (let [k (name key)]
                              [(if-not (or (s/starts-with? k "data-")
-                                        (s/starts-with? k "aria-"))
+                                          (s/starts-with? k "aria-"))
                                 (kebab-case->camel-case k) k) value]))]
     (w/postwalk (fn [x]
                   (if (map? x)
@@ -37,7 +37,7 @@
                                     x)]
                       (into {} (map convert-to-camel new-map)))
                     x))
-      data)))
+                data)))
 
 (defn $LSUtils [] (aget js/window "LSUtils"))
 (def dev? (some-> ($LSUtils) (aget "isDev")))
@@ -45,11 +45,11 @@
 (defn uuid-color
   [uuid-str]
   (some-> ($LSUtils) (aget "uniqolor")
-    (apply [uuid-str
-            #js {:saturation #js [55, 70],
-                 :lightness 70,
-                 :differencePoint 60}])
-    (aget "color")))
+          (apply [uuid-str
+                  #js {:saturation #js [55, 70],
+                       :lightness 70,
+                       :differencePoint 60}])
+          (aget "color")))
 
 (defn get-path
   "Returns the component path."
@@ -67,10 +67,10 @@
         ;; we have to make sure to check if the children is sequential
         ;; as a list can be returned, eg: from a (for)
         new-children (if (and (not (nil? children#))
-                           (not (empty? children))
-                           (or (not (array? children#))
+                              (not (empty? children))
+                              (or (not (array? children#))
                              ;; maybe list children
-                             (not (vector? type#))))
+                                  (not (vector? type#))))
                        [children#] children#)
 
         ;; convert any options key value to a React element, if
@@ -83,22 +83,22 @@
         react-class (if dev? (react-class) react-class)]
     (apply js/React.createElement react-class
       ;; sablono html-to-dom-attrs does not work for nested hash-maps
-      (bean/->js (map-keys->camel-case new-options :html-props true))
-      new-children)))
+           (bean/->js (map-keys->camel-case new-options :html-props true))
+           new-children)))
 
 (defn use-atom-fn
   [a getter-fn setter-fn]
   (let [[val set-val] (use-state (getter-fn @a))]
     (use-effect!
-      (fn []
-        (let [id (str (random-uuid))]
-          (add-watch a id (fn [_ _ prev-state next-state]
-                            (let [prev-value (getter-fn prev-state)
-                                  next-value (getter-fn next-state)]
-                              (when-not (= prev-value next-value)
-                                (set-val next-value)))))
-          #(remove-watch a id)))
-      [])
+     (fn []
+       (let [id (str (random-uuid))]
+         (add-watch a id (fn [_ _ prev-state next-state]
+                           (let [prev-value (getter-fn prev-state)
+                                 next-value (getter-fn next-state)]
+                             (when-not (= prev-value next-value)
+                               (set-val next-value)))))
+         #(remove-watch a id)))
+     [])
     [val #(swap! a setter-fn %)]))
 
 (defn use-atom
@@ -110,10 +110,10 @@
   []
   (let [*mounted (rum/use-ref false)]
     (use-effect!
-      (fn []
-        (rum/set-ref! *mounted true)
-        #(rum/set-ref! *mounted false))
-      [])
+     (fn []
+       (rum/set-ref! *mounted true)
+       #(rum/set-ref! *mounted false))
+     [])
     #(rum/deref *mounted)))
 
 (defn react->rum [c static?]

+ 86 - 0
externs/app.txt

@@ -0,0 +1,86 @@
+require
+DEBUG
+convertFileSrc
+getPlatform
+handleError
+isLoggingEnabled
+isNative
+isNativePlatform
+isPluginAvailable
+platform
+pluginMethodNoop
+registerPlugin
+registerHelper
+Pass
+showHint
+createRef
+call
+setState
+forceUpdate
+memo
+createElement
+render
+unmount
+createContext
+createPortal
+createRef
+createElement
+ref
+path
+current
+useState
+useReducer
+useEffect
+useLayoutEffect
+useCallback
+useMemo
+useRef
+renderToString
+idb-keyval
+newStore
+isError
+value
+CodeMirror
+over
+forEachNode
+on
+getTextArea
+setUser
+nativeEvent
+which
+multipliedBy
+minus
+plus
+isInteger
+isPositive
+isLessThan
+toNumber
+modulo
+exponentiatedBy
+dividedBy
+multipliedBy
+negated
+isZero
+isGreaterThanOrEqualTo
+isLessThan
+shiftedBy
+s
+dividedToIntegerBy
+abs
+toFraction
+e
+getCursor
+getLineTokens
+getTokenAt
+getValue
+completionActive
+state
+mime
+getWrapperElement
+doc
+lastLine
+getLine
+setOption
+string
+dispose
+path

+ 87 - 0
externs/mobile.txt

@@ -0,0 +1,87 @@
+require
+DEBUG
+convertFileSrc
+getPlatform
+handleError
+isLoggingEnabled
+isNative
+isNativePlatform
+isPluginAvailable
+platform
+pluginMethodNoop
+registerPlugin
+registerHelper
+Pass
+showHint
+createRef
+call
+setState
+forceUpdate
+memo
+createElement
+render
+unmount
+createContext
+createPortal
+createRef
+createElement
+ref
+path
+current
+useState
+useReducer
+useEffect
+useLayoutEffect
+useCallback
+useMemo
+useRef
+renderToString
+idb-keyval
+newStore
+isError
+value
+CodeMirror
+over
+forEachNode
+on
+getTextArea
+setUser
+nativeEvent
+which
+multipliedBy
+minus
+plus
+isInteger
+isPositive
+isLessThan
+toNumber
+modulo
+exponentiatedBy
+dividedBy
+multipliedBy
+negated
+isZero
+isGreaterThanOrEqualTo
+isLessThan
+shiftedBy
+s
+dividedToIntegerBy
+abs
+toFraction
+e
+getCursor
+getLineTokens
+getTokenAt
+getValue
+completionActive
+state
+mime
+getWrapperElement
+doc
+lastLine
+getLine
+setOption
+string
+dispose
+path
+base64String

+ 2 - 2
gulpfile.js

@@ -78,7 +78,7 @@ const common = {
         'node_modules/@isomorphic-git/lightning-fs/dist/lightning-fs.min.js',
         'packages/amplify/dist/amplify.js',
         'packages/ui/dist/ui/ui.js',
-        'node_modules/@logseq/sqlite-wasm/sqlite-wasm/jswasm/sqlite3.wasm',
+        'node_modules/@sqlite.org/sqlite-wasm/sqlite-wasm/jswasm/sqlite3.wasm',
         'node_modules/react/umd/react.production.min.js',
         'node_modules/react/umd/react.development.js',
         'node_modules/react-dom/umd/react-dom.production.min.js',
@@ -129,7 +129,7 @@ const common = {
         'node_modules/photoswipe/dist/umd/*.js',
         'packages/amplify/dist/amplify.js',
         'packages/ui/dist/ui/ui.js',
-        'node_modules/@logseq/sqlite-wasm/sqlite-wasm/jswasm/sqlite3.wasm',
+        'node_modules/@sqlite.org/sqlite-wasm/sqlite-wasm/jswasm/sqlite3.wasm',
       ]).pipe(gulp.dest(path.join(outputPath, 'mobile', 'js'))),
       () => gulp.src([
         'packages/ui/dist/ionic/*.js',

+ 39 - 28
package.json

@@ -36,27 +36,36 @@
         "postcss-import": "15.0.0",
         "postcss-import-ext-glob": "2.0.1",
         "postcss-nested": "6.0.0",
+        "process": "^0.11.10",
         "purgecss": "4.0.2",
         "semver": "7.5.2",
-        "shadow-cljs": "2.26.0",
+        "shadow-cljs": "2.28.23",
         "stylelint": "^13.8.0",
         "stylelint-config-standard": "^20.0.0",
         "tailwindcss": "3.3.5",
         "tailwindcss-animate": "^1.0.7",
-        "typescript": "^4.4.3"
+        "typescript": "^4.4.3",
+        "webpack": "^5.98.0",
+        "webpack-cli": "^6.0.1"
     },
     "scripts": {
-        "watch": "run-p gulp:watch cljs:watch",
-        "electron-watch": "run-p gulp:watch cljs:electron-watch",
-        "app-watch": "run-p gulp:watch cljs:app-watch",
-        "dev": "run-p gulp:watch gulp:mobile-watch cljs:dev-watch",
-        "release": "run-s gulp:build cljs:release",
-        "release-app": "run-s gulp:build cljs:release-app",
-        "release-mobile": "run-s gulp:buildMobile cljs:release-mobile",
-        "dev-release-app": "run-s gulp:build cljs:dev-release-app",
+        "watch": "run-p gulp:watch cljs:watch webpack-app-watch",
+        "electron-watch": "run-p gulp:watch cljs:electron-watch webpack-app-watch",
+        "app-watch": "run-p gulp:watch cljs:app-watch webpack-app-watch",
+        "mobile-watch": "run-p gulp:mobile-watch cljs:mobile-watch webpack-mobile-watch",
+        "dev": "run-p gulp:watch gulp:mobile-watch cljs:dev-watch webpack",
+        "release": "run-s gulp:build cljs:release webpack-app-build",
+        "release-app": "run-s gulp:build cljs:release-app webpack-app-build",
+        "release-mobile": "run-s gulp:buildMobile cljs:release-mobile webpack-mobile-build",
+        "dev-release-app": "run-s gulp:build cljs:dev-release-app webpack-app-build",
         "dev-electron-app": "gulp electron",
-        "release-electron": "run-s gulp:build && gulp electronMaker",
+        "release-electron": "run-s gulp:build && gulp electronMaker webpack-app-build",
         "debug-electron": "cd static/ && yarn electron:debug",
+        "webpack": "webpack --watch",
+        "webpack-app-watch": "npx webpack --watch --config-name app",
+        "webpack-mobile-watch": "npx webpack --watch --config-name mobile",
+        "webpack-app-build": "npx webpack build --config-name app",
+        "webpack-mobile-build": "npx webpack build --config-name mobile",
         "sync-android-release": "yarn clean && yarn release-mobile && rm -rf ./static/mobile/**/*.map && npx cap sync android",
         "sync-ios-release": "yarn clean && yarn release-mobile && rm -rf ./static/mobile/**/*.map && npx cap sync ios",
         "clean": "gulp clean",
@@ -68,27 +77,27 @@
         "gulp:buildMobile": "cross-env NODE_ENV=production gulp buildMobile",
         "css:build": "postcss tailwind.all.css -o static/css/style.css --verbose --env production",
         "css:watch": "cross-env TAILWIND_MODE=watch postcss tailwind.all.css -o static/css/style.css --verbose --watch",
-        "cljs:watch": "clojure -M:cljs watch app electron",
+        "cljs:watch": "clojure -M:cljs watch app workers electron",
         "cljs:storybook-watch": "clojure -M:cljs watch stories-dev",
         "gulp:mobile-watch": "gulp watchMobile",
         "css:mobile-build": "postcss tailwind.mobile.css -o static/mobile/style.css --verbose --env production",
         "css:mobile-watch": "cross-env TAILWIND_MODE=watch postcss tailwind.mobile.css -o static/mobile/style.css --verbose --watch",
-        "cljs:mobile-watch": "clojure -M:cljs watch mobile",
-        "cljs:release-mobile": "clojure -M:cljs release mobile",
-        "cljs:dev-watch": "clojure -M:cljs watch app electron mobile",
-        "cljs:app-watch": "clojure -M:cljs watch app",
-        "cljs:electron-watch": "clojure -M:cljs watch app electron --config-merge \"{:asset-path \\\"./js\\\"}\"",
-        "cljs:release": "clojure -M:cljs release app publishing electron",
-        "cljs:release-electron": "clojure -M:cljs release app electron --debug && clojure -M:cljs release publishing",
-        "cljs:release-app": "clojure -M:cljs release app",
-        "cljs:release-publishing": "clojure -M:cljs release publishing",
+        "cljs:mobile-watch": "clojure -M:cljs watch mobile workers --config-merge \"{:output-dir \\\"./static/mobile/js\\\" :asset-path \\\"/static/mobile/js\\\" :release {:asset-path \\\"http://localhost\\\"}}\"",
+        "cljs:release-mobile": "clojure -M:cljs release mobile workers --config-merge \"{:output-dir \\\"./static/mobile/js\\\" :asset-path \\\"/static/mobile/js\\\" :release {:asset-path \\\"http://localhost\\\"}}\"",
+        "cljs:dev-watch": "clojure -M:cljs watch app workers electron mobile",
+        "cljs:app-watch": "clojure -M:cljs watch app workers",
+        "cljs:electron-watch": "clojure -M:cljs watch app workers electron --config-merge \"{:asset-path \\\"./js\\\"}\"",
+        "cljs:release": "clojure -M:cljs release app workers publishing electron",
+        "cljs:release-electron": "clojure -M:cljs release app workers electron --debug && clojure -M:cljs release publishing",
+        "cljs:release-app": "clojure -M:cljs release app workers",
+        "cljs:release-publishing": "clojure -M:cljs release app publishing",
         "cljs:test": "clojure -M:test compile test",
         "cljs:run-test": "node static/tests.js",
-        "cljs:dev-release-app": "clojure -M:cljs release app --config-merge \"{:closure-defines {frontend.config/DEV-RELEASE true}}\"",
-        "cljs:dev-release-electron": "clojure -M:cljs release app electron --debug --config-merge \"{:closure-defines {frontend.config/DEV-RELEASE true}}\" && clojure -M:cljs release publishing",
-        "cljs:debug": "clojure -M:cljs release app --debug",
-        "cljs:report": "clojure -M:cljs run shadow.cljs.build-report app report.html",
-        "cljs:build-electron": "clojure -A:cljs compile app electron",
+        "cljs:dev-release-app": "clojure -M:cljs release app workers --config-merge \"{:closure-defines {frontend.config/DEV-RELEASE true}}\"",
+        "cljs:dev-release-electron": "clojure -M:cljs release app workers electron --debug --config-merge \"{:closure-defines {frontend.config/DEV-RELEASE true}}\" && clojure -M:cljs release publishing",
+        "cljs:debug": "clojure -M:cljs release app workers --debug",
+        "cljs:report": "clojure -M:cljs run shadow.cljs.build-report app workers report.html",
+        "cljs:build-electron": "clojure -A:cljs compile app workers electron",
         "cljs:lint": "clojure -M:clj-kondo --parallel --lint src --cache false",
         "ios:dev": "cross-env PLATFORM=ios gulp cap",
         "android:dev": "cross-env PLATFORM=android gulp cap",
@@ -121,6 +130,7 @@
         "@excalidraw/excalidraw": "0.16.1",
         "@glidejs/glide": "^3.6.0",
         "@highlightjs/cdn-assets": "10.4.1",
+        "@huggingface/transformers": "^3.6.3",
         "@ionic/core": "^8.5.4",
         "@ionic/react": "^8.5.4",
         "@isomorphic-git/lightning-fs": "^4.6.0",
@@ -129,10 +139,10 @@
         "@js-joda/timezone": "2.5.0",
         "@logseq/diff-merge": "^0.2.2",
         "@logseq/react-tweet-embed": "1.3.1-1",
-        "@logseq/sqlite-wasm": "=0.1.0",
         "@radix-ui/colors": "^0.1.8",
         "@sentry/react": "^6.18.2",
         "@sentry/tracing": "^6.18.2",
+        "@sqlite.org/sqlite-wasm": "^3.50.3-build1",
         "@tabler/icons-react": "^2.47.0",
         "@tabler/icons-webfont": "^2.47.0",
         "@tippyjs/react": "4.2.5",
@@ -154,6 +164,7 @@
         "fuse.js": "6.4.6",
         "grapheme-splitter": "1.0.4",
         "graphology": "0.20.0",
+        "hnswlib-wasm": "^0.8.2",
         "html2canvas": "^1.4.1",
         "ignore": "5.1.8",
         "inter-ui": "^3.19.3",
@@ -174,7 +185,6 @@
         "react-dom": "18.3.1",
         "react-grid-layout": "0.16.6",
         "react-intersection-observer": "^9.3.5",
-        "react-resize-context": "3.0.0",
         "react-textarea-autosize": "8.3.3",
         "react-transition-group": "4.3.0",
         "react-virtuoso": "4.12.5",
@@ -185,6 +195,7 @@
         "tailwind-capitalize-first-letter": "^1.0.4",
         "threads": "1.6.5",
         "url": "^0.11.0",
+        "util": "^0.12.5",
         "yargs-parser": "20.2.4"
     },
     "resolutions": {

+ 69 - 0
public/index.html

@@ -0,0 +1,69 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <meta charset="utf-8">
+  <!-- Handle iOS indexedDB preloading. Ref: https://developer.apple.com/forums/thread/681201 -->
+  <script type="text/javascript">
+    window.indexedDB;
+  </script>
+  <meta content="minimum-scale=1, initial-scale=1, maximum-scale=1, width=device-width, shrink-to-fit=no" name="viewport">
+  <link href="/static/css/style.css" rel="stylesheet" type="text/css">
+  <link href="/static/img/logo.png" rel="shortcut icon" type="image/png">
+  <link href="/static/img/logo.png" rel="shortcut icon" sizes="192x192">
+  <link href="/static/img/logo.png" rel="apple-touch-icon">
+  <meta content="Logseq" name="apple-mobile-web-app-title">
+  <meta content="yes" name="apple-mobile-web-app-capable">
+  <meta content="yes" name="apple-touch-fullscreen">
+  <meta content="black-translucent" name="apple-mobile-web-app-status-bar-style">
+  <meta content="yes" name="mobile-web-app-capable">
+  <meta content="summary" name="twitter:card">
+  <meta content="A privacy-first, open-source platform for knowledge management and collaboration." name="twitter:description">
+  <meta content="@logseq" name="twitter:site">
+  <meta content="A privacy-first, open-source platform for knowledge management and collaboration." name="twitter:title">
+  <meta content="https://asset.logseq.com/static/img/logo.png" name="twitter:image:src">
+  <meta content="A privacy-first, open-source platform for knowledge management and collaboration." name="twitter:image:alt">
+  <meta content="A privacy-first, open-source platform for knowledge management and collaboration." property="og:title">
+  <meta content="site" property="og:type">
+  <meta content="https://logseq.com" property="og:url">
+  <meta content="https://asset.logseq.com/static/img/logo.png" property="og:image">
+  <meta content="A privacy-first, open-source platform for knowledge management and collaboration." property="og:description">
+  <title>Logseq: "A privacy-first platform for knowledge management and collaboration."</title>
+  <meta content="logseq" property="og:site_name">
+  <meta content="A privacy-first, open-source platform for knowledge management and collaboration." name="description">
+</head>
+<body>
+<div id="root"></div>
+<script>window.user = null</script>
+<script>window.__LSP__HOST__ = true</script>
+<script src="/static/js/magic_portal.js"></script>
+<script>let worker = new Worker('/static/js/worker.js')
+  const portal = new MagicPortal(worker);
+  (async () => {
+    const fs = await portal.get('fs')
+    window.fs = fs
+    const pfs = await portal.get('pfs')
+    window.pfs = pfs
+    const workerThread = await portal.get('workerThread')
+    window.workerThread = workerThread
+  })()
+</script>
+<script defer src="/static/js/highlight.min.js"></script>
+<script defer src="/static/js/interact.min.js"></script>
+<script defer src="/static/js/marked.min.js"></script>
+<script defer src="/static/js/eventemitter3.umd.min.js"></script>
+<script defer src="/static/js/html2canvas.min.js"></script>
+<script defer src="/static/js/lsplugin.core.js"></script>
+<script defer src="/static/js/react.production.min.js"></script>
+<script defer src="/static/js/react-dom.production.min.js"></script>
+<script defer src="/static/js/main-bundle.js"></script>
+<script defer src="/static/js/ui.js"></script>
+<script defer src="/static/js/main.js"></script>
+<script defer src="/static/js/amplify.js"></script>
+<script defer src="/static/js/prop-types.min.js"></script>
+<script defer src="/static/js/tabler-icons-react.min.js"></script>
+<script defer src="/static/js/tabler.ext.js"></script>
+<script defer src="/static/js/code-editor.js"></script>
+<script defer src="/static/js/tldraw.js"></script>
+<script defer src="/static/js/excalidraw.js"></script>
+</body>
+</html>

+ 1 - 1
resources/index.html

@@ -58,8 +58,8 @@ const portal = new MagicPortal(worker);
 <script defer src="./js/lsplugin.core.js"></script>
 <script defer src="./js/react.production.min.js"></script>
 <script defer src="./js/react-dom.production.min.js"></script>
+<script defer src="./js/main-bundle.js"></script>
 <script defer src="./js/ui.js"></script>
-<script defer src="./js/shared.js"></script>
 <script defer src="./js/main.js"></script>
 <script defer src="./js/amplify.js"></script>
 <script defer src="./js/prop-types.min.js"></script>

+ 1 - 1
resources/mobile/index.html

@@ -36,7 +36,7 @@ const portal = new MagicPortal(worker);
 <script defer src="./js/ui.js"></script>
 <script defer src="./js/amplify.js"></script>
 <script defer src="./ionic.js"></script>
-<script defer src="./js/shared.js"></script>
+<script defer src="./js/main-bundle.js"></script>
 <script defer src="./js/main.js"></script>
 <script defer src="./js/code-editor.js"></script>
 <script>

+ 5 - 5
scripts/src/logseq/tasks/db_graph/create_graph_with_schema_org.cljs

@@ -195,11 +195,11 @@
                              (map #(vector % (str % "_Class")))
                              (into {}))]
     (when (seq renamed-classes)
-     (if verbose
-       (println "Renaming the following classes because they have names that conflict with Logseq's case insensitive :block/name:"
-                (keys renamed-classes) "\n")
-       (when (pos? (count renamed-classes))
-         (println "Renaming" (count renamed-classes) "classes due to page name conflicts"))))
+      (if verbose
+        (println "Renaming the following classes because they have names that conflict with Logseq's case insensitive :block/name:"
+                 (keys renamed-classes) "\n")
+        (when (pos? (count renamed-classes))
+          (println "Renaming" (count renamed-classes) "classes due to page name conflicts"))))
     renamed-classes))
 
 (defn- get-all-properties [schema-data {:keys [verbose]}]

+ 11 - 11
scripts/src/logseq/tasks/dev.clj

@@ -1,17 +1,17 @@
 (ns logseq.tasks.dev
   "Tasks for general development. For desktop or mobile development see their
   namespaces"
-  (:require [babashka.process :refer [shell]]
+  (:require [babashka.cli :as cli]
             [babashka.fs :as fs]
-            [babashka.cli :as cli]
-            [logseq.tasks.util :as task-util]
-            [logseq.tasks.dev.lint :as dev-lint]
+            [babashka.process :refer [shell]]
+            [clojure.core.async :as async]
+            [clojure.data :as data]
+            [clojure.edn :as edn]
             [clojure.java.io :as io]
             [clojure.pprint :as pp]
-            [clojure.edn :as edn]
-            [clojure.data :as data]
-            [clojure.core.async :as async]
-            [clojure.string :as string]))
+            [clojure.string :as string]
+            [logseq.tasks.dev.lint :as dev-lint]
+            [logseq.tasks.util :as task-util]))
 
 (defn test
   "Run tests. Pass args through to cmd 'yarn cljs:run-test'"
@@ -75,7 +75,7 @@
                                                     (fs/glob "." "{src/main,deps/graph-parser/src}/**")))))]
     (do
       (println "Building publishing js asset...")
-      (shell "clojure -M:cljs release publishing"))
+      (shell "clojure -M:cljs release publishing workers"))
     (println "Publishing js asset is up to date")))
 
 (defn publishing-backend
@@ -87,7 +87,7 @@
 
 (defn watch-publishing-frontend
   [& _args]
-  (shell "clojure -M:cljs watch publishing"))
+  (shell "npx shadow-cljs watch publishing"))
 
 (defn watch-publishing-backend
   "Builds publishing backend once watch-publishing-frontend has built initial frontend"
@@ -110,4 +110,4 @@
       (let [db-graph (fs/path parent-graph-dir (fs/file-name file-graph))]
         (println "Importing" (str db-graph) "...")
         (apply shell "bb" "dev:db-import" file-graph db-graph import-options)
-        (shell "bb" "dev:validate-db" db-graph "-gHc")))))
+        (shell "bb" "dev:validate-db" db-graph "-gHc")))))

+ 4 - 1
scripts/src/logseq/tasks/dev/db_and_file_graphs.clj

@@ -18,7 +18,8 @@
          "electron.db"
          "frontend.handler.db-based."
          "frontend.worker.handler.page.db-based"
-         "frontend.components.property" "frontend.components.class" "frontend.components.quick-add"
+         "frontend.inference-worker"
+         "frontend.components.property" "frontend.components.class" "frontend.components.quick-add" "frontend.components.vector-search"
          "frontend.components.db-based" "frontend.components.objects" "frontend.components.query.view"
          "mobile.core" "mobile.events" "mobile.externals" "mobile.init" "mobile.ionic" "mobile.state"
          "mobile.components"]))
@@ -58,8 +59,10 @@
          "src/main/frontend/components/property"
          "src/main/frontend/components/objects.cljs"
          "src/main/frontend/components/quick_add.cljs"
+         "src/main/frontend/components/vector_search"
          "src/main/frontend/components/db_based"
          "src/main/frontend/components/query/view.cljs"
+         "src/main/frontend/inference_worker"
          "src/electron/electron/db.cljs"
          "src/main/mobile"]))
 

+ 9 - 9
scripts/src/logseq/tasks/dev/lint.clj

@@ -43,13 +43,14 @@
           (when (pos? (:exit res)) (System/exit (:exit res)))))
       (println "No clj* files have changed to lint."))))
 
-(defn- validate-frontend-not-in-worker
+(defn- validate-frontend-not-in-workers
   []
   (let [res (shell {:out :string}
-                   "git grep -h" "\\[frontend.*:as" "src/main/frontend/worker")
+                   "git grep -h" "\\[frontend.*:as"
+                   "src/main/frontend/worker" "src/main/frontend/worker_common" "src/main/frontend/inference_worker")
         req-lines (->> (:out res)
                        string/split-lines
-                       (remove #(re-find #"frontend\.worker|frontend\.common" %)))]
+                       (remove #(re-find #"frontend\.worker|frontend\.common|frontend\.inference-worker" %)))]
 
     (if (seq req-lines)
       (do
@@ -58,10 +59,10 @@
         (System/exit 1))
       (println "Valid worker namespaces!"))))
 
-(defn- validate-worker-not-in-frontend
+(defn- validate-workers-not-in-frontend
   []
   (let [res (shell {:out :string :continue true}
-                   "grep -r --exclude-dir=worker" "\\[frontend.worker.*:" "src/main/frontend")
+                   "grep -r --exclude-dir=worker --exclude-dir=inference_worker" "\\[frontend.worker.*:" "src/main/frontend")
         ;; allow reset-file b/c it's only affects tests
         allowed-exceptions #{"src/main/frontend/handler/file_based/file.cljs:            [frontend.worker.file.reset :as file-reset]"}
         invalid-lines (when (= 0 (:exit res))
@@ -75,8 +76,7 @@
       (println "Valid frontend namespaces!"))))
 
 (defn worker-and-frontend-separate
-  "Ensures worker is independent of frontend"
+  "Ensures workers are independent of frontend"
   []
-  (validate-frontend-not-in-worker)
-  (validate-worker-not-in-frontend))
-
+  (validate-frontend-not-in-workers)
+  (validate-workers-not-in-frontend))

+ 77 - 52
shadow-cljs.edn

@@ -14,16 +14,18 @@
  :builds
  {:app {:target        :browser
         :module-loader true
-        :js-options    {:ignore-asset-requires true
+        :js-options    {:js-provider    :external
+                        :external-index "target/main.js"
+                        :external-index-format :esm
+                        :entry-keys ["module" "browser" "main"]
+                        :export-conditions ["module" "import", "browser" "require" "default"]
+                        :ignore-asset-requires true
                         :resolve {"react" {:target :global
                                            :global "React"}
                                   "react-dom" {:target :global
-                                               :global "ReactDOM"}}} ;; handle `require(xxx.css)`
-        :modules       {:shared
-                        {:entries []}
-                        :main
-                        {:init-fn frontend.core/init
-                         :depends-on #{:shared}}
+                                               :global "ReactDOM"}}}
+        :modules       {:main
+                        {:init-fn frontend.core/init}
                         :code-editor
                         {:entries [frontend.extensions.code]
                          :depends-on #{:main}}
@@ -32,11 +34,7 @@
                          :depends-on #{:main}}
                         :tldraw
                         {:entries [frontend.extensions.tldraw]
-                         :depends-on #{:main}}
-                        :db-worker
-                        {:init-fn frontend.worker.db-worker/init
-                         :depends-on #{:shared}
-                         :web-worker true}}
+                         :depends-on #{:main}}}
 
         :output-dir       "./static/js"
         :asset-path       "/static/js"
@@ -46,8 +44,7 @@
                            :externs ["datascript/externs.js"
                                      "externs.js"]
                            :warnings {:fn-deprecated false
-                                      :redef false}
-                           :cross-chunk-method-motion false}
+                                      :redef false}}
         :build-hooks [(shadow.hooks/git-revision-hook "--long --always --dirty")]
         :closure-defines {lambdaisland.glogi.console/colorize "false"
                           goog.debug.LOGGING_ENABLED true
@@ -58,32 +55,68 @@
                           frontend.config/ENABLE-FILE-SYNC-PRODUCTION #shadow/env ["ENABLE_FILE_SYNC_PRODUCTION" :as :bool :default true]
                           frontend.config/ENABLE-RTC-SYNC-PRODUCTION #shadow/env ["ENABLE_RTC_SYNC_PRODUCTION" :as :bool :default true]
                           frontend.config/REVISION #shadow/env ["LOGSEQ_REVISION" :default "dev"]} ;; set by git-revision-hook
-
+        
         :devtools {:before-load frontend.core/stop          ;; before live-reloading any code call this function
                    :after-load frontend.core/start          ;; after live-reloading finishes call this function
                    :watch-path "/static"
                    :preloads [devtools.preload
-                              shadow.remote.runtime.cljs.browser]}}
-
+                              shadow.remote.runtime.cljs.browser]
+                   ;; :ignore-warnings true
+                   }}
+
+  :workers {:target        :browser
+            :module-loader true
+            :js-options    {:js-provider    :external
+                            :external-index "target/workers.js"
+                            :external-index-format :esm
+                            :entry-keys ["module" "browser" "main"]
+                            :export-conditions ["module" "import", "browser" "require" "default"]}
+            :modules       {:workers-shared {:entries []}
+                            :db-worker
+                            {:init-fn frontend.worker.db-worker/init
+                             :web-worker true
+                             :prepend "importScripts('workers-bundle.js');\n"
+                             :depends-on #{:workers-shared}}
+                            :inference-worker
+                            {:init-fn frontend.inference-worker.inference-worker/init
+                             :web-worker true
+                             :prepend "importScripts('workers-bundle.js');\n"
+                             :depends-on #{:workers-shared}}}
+
+            :output-dir       "./static/js"
+            :asset-path       "/static/js"
+            :release          {:asset-path "https://asset.logseq.com/static/js"}
+            :compiler-options {:infer-externs      :auto
+                               :output-feature-set :es-next-in
+                               :source-map true
+                               :externs ["datascript/externs.js"
+                                         "externs.js"]
+                               :warnings {:fn-deprecated false
+                                          :redef false}}
+            :closure-defines {goog.debug.LOGGING_ENABLED true}
+            :devtools {:before-load frontend.core/stop          ;; before live-reloading any code call this function
+                       :after-load frontend.core/start          ;; after live-reloading finishes call this function
+                       :watch-path "/static"
+                       :preloads [devtools.preload
+                                  shadow.remote.runtime.cljs.browser]}}
   :mobile {:target        :browser
            :module-loader true
-           :js-options    {:ignore-asset-requires true
+           :js-options    {:js-provider    :external
+                           :external-index "target/mobile.js"
+                           :external-index-format :esm
+                           :entry-keys ["module" "browser" "main"]
+                           :export-conditions ["module" "import", "browser" "require" "default"]
+                           :ignore-asset-requires true
                            :resolve {"react" {:target :global
                                               :global "React"}
                                      "react-dom" {:target :global
-                                                  :global "ReactDOM"}}} ;; handle `require(xxx.css)`
-           :modules       {:shared
-                           {:entries []}
-                           :main
-                           {:init-fn mobile.core/init
-                            :depends-on #{:shared}}
+                                                  :global "ReactDOM"}}}
+           ;; handle `require(xxx.css)`
+           :modules       {:main
+                           {:init-fn mobile.core/init}
                            :code-editor
                            {:entries [frontend.extensions.code]
-                            :depends-on #{:main}}
-                           :db-worker
-                           {:init-fn frontend.worker.db-worker/init
-                            :depends-on #{:shared}
-                            :web-worker true}}
+                            :depends-on #{:main}}}
 
            :output-dir       "./static/mobile/js"
            :asset-path       "./js"
@@ -94,24 +127,22 @@
                               :externs ["datascript/externs.js"
                                         "externs.js"]
                               :warnings {:fn-deprecated false
-                                         :redef false}
-                              :cross-chunk-method-motion false}
+                                         :redef false}}
            :build-hooks [(shadow.hooks/git-revision-hook "--long --always --dirty")]
            :closure-defines
            {;; Set to switch file sync server to dev, set this to false in `yarn watch`
             frontend.config/ENABLE-FILE-SYNC-PRODUCTION #shadow/env ["ENABLE_FILE_SYNC_PRODUCTION" :as :bool :default true]
             frontend.config/ENABLE-RTC-SYNC-PRODUCTION #shadow/env ["ENABLE_RTC_SYNC_PRODUCTION" :as :bool :default true]}
 
-;; NOTE: electron, browser/mobile-app use different asset-paths.
-                  ;;   For browser/mobile-app devs, assets are located in /static/js(via HTTP root).
-                  ;;   For electron devs, assets are located in ./js(via relative path).
-                  ;; :dev      {:asset-path "./js"}
+           ;; NOTE: electron, browser/mobile-app use different asset-paths.
+           ;;   For browser/mobile-app devs, assets are located in /static/js(via HTTP root).
+           ;;   For electron devs, assets are located in ./js(via relative path).
+           ;; :dev      {:asset-path "./js"}
            :devtools {:before-load mobile.core/stop!          ;; before live-reloading any code call this function
                       :after-load mobile.core/render!          ;; after live-reloading finishes call this function
                       :watch-path "/static/mobile"
                       :preloads [devtools.preload
                                  shadow.remote.runtime.cljs.browser]}}
-
   :electron {:target :node-script
              :output-to "static/electron.js"
              :main electron.core/main
@@ -153,17 +184,18 @@
 
   :publishing {:target :browser
                :module-loader true
-               :js-options {;; handle `require(xxx.css)`
+               :js-options {:js-provider    :external
+                            :external-index "target/main.js"
+                            :external-index-format :esm
+                            :entry-keys ["module" "browser" "main"]
+                            :export-conditions ["module" "import", "browser" "require" "default"]
                             :ignore-asset-requires true
                             :resolve {"react" {:target :global
                                                :global "React"}
                                       "react-dom" {:target :global
                                                    :global "ReactDOM"}}}
-               :modules {:shared
-                         {:entries []}
-                         :main
-                         {:init-fn frontend.publishing/init
-                          :depends-on #{:shared}}
+               :modules {:main
+                         {:init-fn frontend.publishing/init}
                          :code-editor
                          {:entries [frontend.extensions.code]
                           :depends-on #{:main}}
@@ -172,26 +204,19 @@
                           :depends-on #{:main}}
                          :tldraw
                          {:entries [frontend.extensions.tldraw]
-                          :depends-on #{:main}}
-                         :db-worker
-                         {:init-fn frontend.worker.db-worker/init
-                          :depends-on #{:shared}
-                          :web-worker true}}
+                          :depends-on #{:main}}}
 
                :output-dir "./static/js/publishing"
                :asset-path "static/js"
                :closure-defines {logseq.common.config/PUBLISHING true
                                  goog.debug.LOGGING_ENABLED true}
                :compiler-options {:infer-externs :auto
-                                  :output-feature-set :es-next
+                                  :output-feature-set :es-next-in
                                   :source-map true
                                   :externs ["datascript/externs.js"
                                             "externs.js"]
                                   :warnings {:fn-deprecated false
-                                             :redef false}
-                                  ;; https://github.com/thheller/shadow-cljs/issues/611#issuecomment-620845276
-                                  ;; fixes cljs.spec bug with code splitting
-                                  :cross-chunk-method-motion false}
+                                             :redef false} }
                :devtools {:before-load frontend.core/stop
                           :after-load frontend.core/start
                           :preloads [devtools.preload]}}

+ 0 - 18
src/dev-cljs/shadow/hooks.clj

@@ -12,24 +12,6 @@
         (string/trim out))
       (println err))))
 
-(defn purge-css
-  {:shadow.build/stage :flush}
-  [state {:keys [css-source
-                 js-globs
-                 public-dir]}]
-  (case (:shadow.build/mode state)
-    :release
-    (exec "purgecss --css " css-source
-          (for [content (if (string? js-globs) [js-globs] js-globs)]
-            (str "--content " content))
-          "-o" public-dir)
-
-    :dev
-    (do
-      (exec "mkdir -p" public-dir)
-      (exec "cp" css-source (str public-dir "/" (last (string/split css-source #"/"))))))
-  state)
-
 (defn git-revision-hook
   {:shadow.build/stage :configure}
   [build-state & args]

+ 3 - 3
src/dev-cljs/shadow/user.clj

@@ -19,7 +19,7 @@
 ;; Get the runtime id from http://localhost:9630/runtimes, pick the one which shows `browser-worker`
 (defn worker-repl
   ([]
-   (when-let [runtime-id (->> (api/repl-runtimes :app)
+   (when-let [runtime-id (->> (api/repl-runtimes :workers)
                               (filter (fn [runtime] (= :browser-worker (:host runtime))))
                               (map :client-id)
                               (apply max))]
@@ -29,8 +29,8 @@
    (if
     (number? runtime-id-or-which)
      (do (prn :worker-runtime-id runtime-id-or-which)
-         (api/repl :app {:runtime-id runtime-id-or-which}))
-     (let [runtime-ids (->> (api/repl-runtimes :app)
+         (api/repl :workers {:runtime-id runtime-id-or-which}))
+     (let [runtime-ids (->> (api/repl-runtimes :workers)
                             (filter (fn [runtime] (= :browser-worker (:host runtime))))
                             (map :client-id))
            runtime-id (apply (if (= :old runtime-id-or-which) min max) runtime-ids)]

+ 4 - 4
src/electron/electron/fs_watcher.cljs

@@ -2,12 +2,12 @@
   "This ns is a wrapper around the chokidar file watcher,
   https://www.npmjs.com/package/chokidar. File watcher events are sent to the
   `file-watcher` ipc channel"
-  (:require [cljs-bean.core :as bean]
+  (:require ["chokidar" :as watcher]
+            ["electron" :refer [app]]
             ["fs" :as fs]
-            ["chokidar" :as watcher]
-            [electron.utils :as utils]
+            [cljs-bean.core :as bean]
             [electron.logger :as logger]
-            ["electron" :refer [app]]
+            [electron.utils :as utils]
             [electron.window :as window]
             [logseq.common.graph :as common-graph]))
 

+ 2 - 2
src/electron/electron/utils.cljs

@@ -3,12 +3,12 @@
             ["electron" :refer [app BrowserWindow]]
             ["fs-extra" :as fs]
             ["path" :as node-path]
+            [cljs-bean.core :as bean]
             [clojure.string :as string]
             [electron.configs :as cfgs]
+            [electron.db :as db]
             [electron.logger :as logger]
             [logseq.db.sqlite.util :as sqlite-util]
-            [cljs-bean.core :as bean]
-            [electron.db :as db]
             [promesa.core :as p]))
 
 (defonce *win (atom nil)) ;; The main window

+ 33 - 33
src/electron/electron/window.cljs

@@ -1,15 +1,15 @@
 (ns electron.window
-  (:require ["electron-window-state" :as windowStateKeeper]
-            [electron.utils :refer [mac? win32? linux? dev? open] :as utils]
+  (:require ["electron" :refer [BrowserWindow app session shell dialog] :as electron]
+            ["electron-window-state" :as windowStateKeeper]
+            ["path" :as node-path]
+            ["url" :as URL]
+            [cljs-bean.core :as bean]
+            [clojure.string :as string]
             [electron.configs :as cfgs]
             [electron.context-menu :as context-menu]
             [electron.logger :as logger]
-            ["electron" :refer [BrowserWindow app session shell dialog] :as electron]
-            ["path" :as node-path]
-            ["url" :as URL]
             [electron.state :as state]
-            [cljs-bean.core :as bean]
-            [clojure.string :as string]))
+            [electron.utils :refer [mac? win32? linux? dev? open] :as utils]))
 
 (defonce *quitting? (atom false))
 
@@ -28,27 +28,27 @@
          native-titlebar? (cfgs/get-item :window/native-titlebar?)
          url (if graph (str url "#/?graph=" graph) url)
          win-opts  (cond->
-                     {:backgroundColor      "#fff" ; SEE https://www.electronjs.org/docs/latest/faq#the-font-looks-blurry-what-is-this-and-what-can-i-do
-                      :width                (.-width win-state)
-                      :height               (.-height win-state)
-                      :frame                (or mac? native-titlebar?)
-                      :titleBarStyle        "hiddenInset"
-                      :trafficLightPosition {:x 16 :y 16}
-                      :autoHideMenuBar      (not mac?)
-                      :show                 false
-                      :webPreferences
-                      {:plugins                 true        ; pdf
-                       :nodeIntegration         false
-                       :nodeIntegrationInWorker false
-                       :nativeWindowOpen        true
-                       :sandbox                 false
-                       :webSecurity             (not dev?)
-                       :contextIsolation        true
-                       :spellcheck              ((fnil identity true) (cfgs/get-item :spell-check))
+                    {:backgroundColor      "#fff" ; SEE https://www.electronjs.org/docs/latest/faq#the-font-looks-blurry-what-is-this-and-what-can-i-do
+                     :width                (.-width win-state)
+                     :height               (.-height win-state)
+                     :frame                (or mac? native-titlebar?)
+                     :titleBarStyle        "hiddenInset"
+                     :trafficLightPosition {:x 16 :y 16}
+                     :autoHideMenuBar      (not mac?)
+                     :show                 false
+                     :webPreferences
+                     {:plugins                 true        ; pdf
+                      :nodeIntegration         false
+                      :nodeIntegrationInWorker false
+                      :nativeWindowOpen        true
+                      :sandbox                 false
+                      :webSecurity             (not dev?)
+                      :contextIsolation        true
+                      :spellcheck              ((fnil identity true) (cfgs/get-item :spell-check))
                        ;; Remove OverlayScrollbars and transition `.scrollbar-spacing`
                        ;; to use `scollbar-gutter` after the feature is implemented in browsers.
-                       :enableBlinkFeatures     'OverlayScrollbars'
-                       :preload                 (node-path/join js/__dirname "js/preload.js")}}
+                      :enableBlinkFeatures     'OverlayScrollbars'
+                      :preload                 (node-path/join js/__dirname "js/preload.js")}}
 
                      (seq opts)
                      (merge opts)
@@ -129,12 +129,12 @@
       (if (contains? #{"https:" "http:" "mailto:"} (.-protocol parsed-url))
         (.openExternal shell url)
         (when-let [^js res (and (fn? default-open)
-                             (.showMessageBoxSync dialog
-                               #js {:type "warning"
-                                    :message (str "Are you sure you want to open this link? \n\n" url)
-                                    :defaultId 1
-                                    :cancelId 0
-                                    :buttons #js ["Cancel" "OK"]}))]
+                                (.showMessageBoxSync dialog
+                                                     #js {:type "warning"
+                                                          :message (str "Are you sure you want to open this link? \n\n" url)
+                                                          :defaultId 1
+                                                          :cancelId 0
+                                                          :buttons #js ["Cancel" "OK"]}))]
           (when (= res 1)
             (default-open url)))))))
 
@@ -187,7 +187,7 @@
                               :webSecurity (not dev?)
                               :preload (node-path/join js/__dirname "js/preload.js")
                               :nativeWindowOpen true}}}
-                      features)
+                           features)
                     (do (open-external! url) {:action "deny"}))
                   (bean/->js))))]
 

+ 10 - 10
src/main/frontend/common/file/util.cljs

@@ -76,13 +76,13 @@
 ;; A fast pprint alternative.
 (defn print-prefix-map* [prefix m print-one writer opts]
   (pr-sequential-writer
-    writer
-    (fn [e w opts]
-      (print-one (key e) w opts)
-      (-write w \space)
-      (print-one (val e) w opts))
-    (str prefix "\n{") \newline "}"
-    opts (seq m)))
+   writer
+   (fn [e w opts]
+     (print-one (key e) w opts)
+     (-write w \space)
+     (print-one (val e) w opts))
+   (str prefix "\n{") \newline "}"
+   opts (seq m)))
 
 (defn ugly-pr-str
   "Ugly printing fast, with newlines so that git diffs are smaller"
@@ -91,6 +91,6 @@
     (pr-str x)))
 
 (defn post-message
-  [type data]
-  (when (exists? js/self)
-    (.postMessage js/self (ldb/write-transit-str [type data]))))
+  [type data & {:keys [port]}]
+  (when-let [worker (or port js/self)]
+    (.postMessage worker (ldb/write-transit-str [type data]))))

+ 21 - 22
src/main/frontend/common/search_fuzzy.cljs

@@ -1,8 +1,8 @@
 (ns frontend.common.search-fuzzy
   "fuzzy search. Used by frontend and worker namespaces"
-  (:require [clojure.string :as string]
+  (:require ["remove-accents" :as removeAccents]
             [cljs-bean.core :as bean]
-            ["remove-accents" :as removeAccents]))
+            [clojure.string :as string]))
 
 (def MAX-STRING-LENGTH 1000.0)
 
@@ -28,10 +28,19 @@
                (/ (- maxed mined)
                   maxed)))))
 
+(defn search-normalize
+  "Normalize string for searching (loose)"
+  [s remove-accents?]
+  (when s
+    (let [normalize-str (.normalize (string/lower-case s) "NFKC")]
+      (if remove-accents?
+        (removeAccents normalize-str)
+        normalize-str))))
+
 (defn score
   [oquery ostr]
-  (let [query (clean-str oquery)
-        original-s (clean-str ostr)]
+  (let [query (-> (clean-str oquery) (search-normalize true))
+        original-s (-> (clean-str ostr) (search-normalize true))]
     (loop [q (seq (char-array query))
            s (seq (char-array original-s))
            mult 1
@@ -68,24 +77,14 @@
                        (dec idx)
                        (- score' 0.1)))))))
 
-(defn search-normalize
-  "Normalize string for searching (loose)"
-  [s remove-accents?]
-  (when s
-    (let [normalize-str (.normalize (string/lower-case s) "NFKC")]
-      (if remove-accents?
-        (removeAccents normalize-str)
-        normalize-str))))
-
 (defn fuzzy-search
   [data query & {:keys [limit extract-fn]
                  :or {limit 20}}]
-  (let [query (search-normalize query true)]
-    (->> (take limit
-               (sort-by :score (comp - compare)
-                        (filter #(< 0 (:score %))
-                                (for [item data]
-                                  (let [s (str (if extract-fn (extract-fn item) item))]
-                                    {:data item
-                                     :score (score query (search-normalize s true))})))))
-         (map :data))))
+  (->> (take limit
+             (sort-by :score (comp - compare)
+                      (filter #(< 0 (:score %))
+                              (for [item data]
+                                (let [s (str (if extract-fn (extract-fn item) item))]
+                                  {:data item
+                                   :score (score query s)})))))
+       (map :data)))

+ 4 - 2
src/main/frontend/common/thread_api.cljc

@@ -29,8 +29,10 @@
      (let [qkw (keyword qualified-kw-str)]
        (vswap! *profile update qkw inc)
        (if-let [f (@*thread-apis qkw)]
-         (let [result (apply f (cond-> args-transit-str-or-args-array
-                                 (not direct-pass?) ldb/read-transit-str))
+         (let [result (if (= qkw :thread-api/set-infer-worker-proxy)
+                        (f args-transit-str-or-args-array)
+                        (apply f (cond-> args-transit-str-or-args-array
+                                   (not direct-pass?) ldb/read-transit-str)))
                result-promise
                (if (fn? result) ;; missionary task is a fn
                  (js/Promise. result)

+ 5 - 5
src/main/frontend/components/block/macros.cljs

@@ -1,15 +1,15 @@
 (ns frontend.components.block.macros
   "Logseq macros that render and evaluate in blocks"
   (:require [clojure.walk :as walk]
+            [datascript.core :as d]
+            [frontend.config :as config]
+            [frontend.db.conn :as db-conn]
             [frontend.extensions.sci :as sci]
             [frontend.handler.common :as common-handler]
+            [frontend.state :as state]
             [goog.string :as gstring]
             [goog.string.format]
-            [frontend.state :as state]
-            [frontend.config :as config]
-            [datascript.core :as d]
-            [logseq.db.frontend.property :as db-property]
-            [frontend.db.conn :as db-conn]))
+            [logseq.db.frontend.property :as db-property]))
 
 (defn- properties-by-name
   "Given a block from a query result, returns a map of its properties indexed by

+ 2 - 4
src/main/frontend/components/cmdk/core.cljs

@@ -71,7 +71,7 @@
    :commands       {:status :success :show :less :items nil}
    :favorites      {:status :success :show :less :items nil}
    :current-page   {:status :success :show :less :items nil}
-   :nodes         {:status :success :show :less :items nil}
+   :nodes          {:status :success :show :less :items nil}
    :files          {:status :success :show :less :items nil}
    :themes         {:status :success :show :less :items nil}
    :filters        {:status :success :show :less :items nil}})
@@ -630,7 +630,6 @@
   [state' state title group visible-items first-item sidebar?]
   (let [{:keys [show items]} (some-> state ::results deref group)
         highlighted-item (or @(::highlighted-item state) first-item)
-        highlighted-group @(::highlighted-group state)
         *mouse-active? (::mouse-active? state')
         filter' @(::filter state)
         can-show-less? (< (get-group-limit group) (count visible-items))
@@ -660,8 +659,7 @@
 
          [:div {:class "flex-1"}]
 
-         (when (and (= group highlighted-group)
-                    (or can-show-more? can-show-less?)
+         (when (and (or can-show-more? can-show-less?)
                     (empty? filter')
                     (not sidebar?))
            [:a.text-link.select-node.opacity-50.hover:opacity-90

+ 1 - 1
src/main/frontend/components/content.cljs

@@ -19,10 +19,10 @@
             [frontend.handler.property :as property-handler]
             [frontend.handler.property.util :as pu]
             [frontend.modules.shortcut.core :as shortcut]
-            [frontend.util.ref :as ref]
             [frontend.state :as state]
             [frontend.ui :as ui]
             [frontend.util :as util]
+            [frontend.util.ref :as ref]
             [frontend.util.url :as url-util]
             [goog.dom :as gdom]
             [goog.object :as gobj]

+ 1 - 2
src/main/frontend/components/file.cljs

@@ -69,8 +69,7 @@
   [:div.flex-1.overflow-hidden
    [:h1.title
     (t :all-files)]
-   (files-all)
-   ])
+   (files-all)])
 
 ;; FIXME: misuse of rpath and fpath
 (rum/defcs file-inner < rum/reactive

+ 14 - 14
src/main/frontend/components/file_based/block.cljs

@@ -82,8 +82,8 @@
   [{:block/keys [pre-block? priority] :as block}]
   (when (and (not pre-block?) priority)
     (ui/tooltip
-      (priority-text priority)
-      (set-priority block priority))))
+     (priority-text priority)
+     (set-priority block priority))))
 
 (defn clock-summary-cp
   [block body]
@@ -96,19 +96,19 @@
                  (not (string/blank? summary)))
         [:div {:style {:max-width 100}}
          (ui/tooltip
-           [:div.text-sm.time-spent.ml-1 {:style {:padding-top 3}}
-            [:a.fade-link
-             summary]]
+          [:div.text-sm.time-spent.ml-1 {:style {:padding-top 3}}
+           [:a.fade-link
+            summary]]
 
-           (when-let [logbook (drawer/get-logbook body)]
-             (let [clocks (->> (last logbook)
-                            (filter #(string/starts-with? % "CLOCK:"))
-                            (remove string/blank?))]
-               [:div.p-4
-                [:div.font-bold.mb-2 "LOGBOOK:"]
-                [:ul
-                 (for [clock (take 10 (reverse clocks))]
-                   [:li clock])]])))]))))
+          (when-let [logbook (drawer/get-logbook body)]
+            (let [clocks (->> (last logbook)
+                              (filter #(string/starts-with? % "CLOCK:"))
+                              (remove string/blank?))]
+              [:div.p-4
+               [:div.font-bold.mb-2 "LOGBOOK:"]
+               [:ul
+                (for [clock (take 10 (reverse clocks))]
+                  [:li clock])]])))]))))
 
 (rum/defc timestamp-editor
   [ast *show-datapicker?]

+ 10 - 10
src/main/frontend/components/file_based/query.cljs

@@ -11,16 +11,16 @@
 (rum/defc query-refresh-button
   [query-time {:keys [on-pointer-down full-text-search?]}]
   (ui/tooltip
-    [:a.fade-link.flex
-     {:on-pointer-down on-pointer-down}
-     (ui/icon "refresh" {:style {:font-size 20}})]
-    [:div
-     [:p
-      (if full-text-search?
-        [:span "Full-text search results will not be refreshed automatically."]
-        [:span (str "This query takes " (int query-time) "ms to finish, it's a bit slow so that auto refresh is disabled.")])]
-     [:p
-      "Click the refresh button instead if you want to see the latest result."]]))
+   [:a.fade-link.flex
+    {:on-pointer-down on-pointer-down}
+    (ui/icon "refresh" {:style {:font-size 20}})]
+   [:div
+    [:p
+     (if full-text-search?
+       [:span "Full-text search results will not be refreshed automatically."]
+       [:span (str "This query takes " (int query-time) "ms to finish, it's a bit slow so that auto refresh is disabled.")])]
+    [:p
+     "Click the refresh button instead if you want to see the latest result."]]))
 
 ;; Custom query header only used by file graphs
 (rum/defc custom-query-header

+ 33 - 4
src/main/frontend/components/header.cljs

@@ -20,6 +20,7 @@
             [frontend.db :as db]
             [frontend.handler :as handler]
             [frontend.handler.db-based.rtc-flows :as rtc-flows]
+            [frontend.handler.db-based.vector-search-flows :as vector-search-flows]
             [frontend.handler.page :as page-handler]
             [frontend.handler.plugin :as plugin-handler]
             [frontend.handler.route :as route-handler]
@@ -342,6 +343,30 @@
                                     (:block/uuid page)
                                     {:header? true})]])))
 
+(rum/defc semantic-search-progressing
+  [repo]
+  (let [[vec-search-state set-vec-search-state] (hooks/use-state nil)
+        {:keys [indexing?]} (get-in vec-search-state [:repo->index-info repo])]
+    (hooks/use-effect!
+     (fn []
+       (c.m/run-task
+         ::update-vec-search-state
+         (m/reduce
+          (fn [_ v]
+            (set-vec-search-state v))
+          (m/ap
+            (m/?> vector-search-flows/infer-worker-ready-flow)
+            (c.m/<? (state/<invoke-db-worker :thread-api/vec-search-update-index-info repo))
+            (m/?> vector-search-flows/vector-search-state-flow)))
+         :succ (constantly nil)))
+     [])
+    (when indexing?
+      (shui/button
+       {:class   "opacity-50"
+        :variant :ghost
+        :size    :sm}
+       "Embedding..."))))
+
 (rum/defc ^:large-vars/cleanup-todo header-aux < rum/reactive
   [{:keys [current-repo default-home new-block-mode]}]
   (let [electron-mac? (and util/mac? (util/electron?))
@@ -349,7 +374,8 @@
                                                  (state/set-left-sidebar-open!
                                                   (not (:ui/left-sidebar-open? @state/state))))})
         custom-home-page? (and (state/custom-home-page?)
-                               (= (state/sub-default-home-page) (state/get-current-page)))]
+                               (= (state/sub-default-home-page) (state/get-current-page)))
+        db-based? (config/db-based-graph? current-repo)]
     [:div.cp__header.drag-region#head
      {:class           (util/classnames [{:electron-mac   electron-mac?
                                           :native-ios     (mobile-util/native-ios?)
@@ -387,11 +413,11 @@
      [:div.r.flex.drag-region.justify-between.items-center.gap-2.overflow-x-hidden.w-full
       [:div.flex.flex-1
        (block-breadcrumb (state/get-current-page))]
-      [:div.flex
+      [:div.flex.items-center
        (when (and current-repo
                   (ldb/get-graph-rtc-uuid (db/get-db))
                   (user-handler/logged-in?)
-                  (config/db-based-graph? current-repo)
+                  db-based?
                   (user-handler/rtc-group?))
          [:<>
           (recent-slider)
@@ -404,9 +430,12 @@
        (when (user-handler/logged-in?)
          (rtc-indicator/uploading-detail))
 
+       (when db-based?
+         (semantic-search-progressing current-repo))
+
        (when (and current-repo
                   (not (config/demo-graph? current-repo))
-                  (not (config/db-based-graph? current-repo))
+                  (not db-based?)
                   (user-handler/alpha-or-beta-user?))
          (fs-sync/indicator))
 

+ 11 - 0
src/main/frontend/components/right_sidebar.cljs

@@ -8,6 +8,7 @@
             [frontend.components.page :as page]
             [frontend.components.profiler :as profiler]
             [frontend.components.shortcut-help :as shortcut-help]
+            [frontend.components.vector-search.sidebar :as vector-search]
             [frontend.config :as config]
             [frontend.context.i18n :refer [t]]
             [frontend.date :as date]
@@ -150,6 +151,10 @@
         [[:.flex.items-center (ui/icon "cloud" {:class "text-md mr-2"}) "(Dev) Profiler"]
          (profiler/profiler)]
 
+        :vector-search
+        [[:.flex.items-center (ui/icon "file-search" {:class "text-md mr-2"}) "(Dev) VectorSearch"]
+         (vector-search/vector-search-sidebar)]
+
         ["" [:span]])))
    (p/catch (fn [error]
               (js/console.error error)))))
@@ -443,6 +448,12 @@
            [:button.button.cp__right-sidebar-settings-btn {:on-click (fn [_e]
                                                                        (state/sidebar-add-block! repo "rtc" :rtc))}
             "(Dev) RTC"]])
+        (when (state/sub [:ui/developer-mode?])
+          [:div.text-sm
+           [:button.button.cp__right-sidebar-settings-btn
+            {:on-click (fn [_e]
+                         (state/sidebar-add-block! repo "vector-search" :vector-search))}
+            "(Dev) vector-search"]])
         (when (state/sub [:ui/developer-mode?])
           [:div.text-sm
            [:button.button.cp__right-sidebar-settings-btn {:on-click (fn [_e]

+ 92 - 1
src/main/frontend/components/settings.cljs

@@ -1,7 +1,9 @@
 (ns frontend.components.settings
   (:require [clojure.string :as string]
+            [clojure.walk :as walk]
             [electron.ipc :as ipc]
             [frontend.colors :as colors]
+            [frontend.common.missionary :as c.m]
             [frontend.components.assets :as assets]
             [frontend.components.file-sync :as fs]
             [frontend.components.shortcut :as shortcut]
@@ -13,6 +15,7 @@
             [frontend.dicts :as dicts]
             [frontend.handler.config :as config-handler]
             [frontend.handler.db-based.rtc :as rtc-handler]
+            [frontend.handler.db-based.vector-search-flows :as vector-search-flows]
             [frontend.handler.file-sync :as file-sync-handler]
             [frontend.handler.global-config :as global-config-handler]
             [frontend.handler.notification :as notification]
@@ -35,6 +38,7 @@
             [logseq.db :as ldb]
             [logseq.shui.hooks :as hooks]
             [logseq.shui.ui :as shui]
+            [missionary.core :as m]
             [promesa.core :as p]
             [reitit.frontend.easy :as rfe]
             [rum.core :as rum]))
@@ -1189,6 +1193,87 @@
                            (rtc-handler/<rtc-invite-email graph-uuid user-email)))))}
         "Invite")]]]))
 
+(rum/defc settings-ai
+  []
+  (let [[model-info set-model-info] (hooks/use-state nil)
+        [load-model-progress set-load-model-progress] (hooks/use-state nil)
+        {:keys [status]} load-model-progress
+        repo (state/get-current-repo)
+        current-model (:graph-text-embedding-model-name model-info)]
+    (hooks/use-effect!
+     (fn []
+       (c.m/run-task
+         ::fetch-model-info
+         (m/reduce
+          (constantly nil)
+          (m/ap
+            (m/?> vector-search-flows/infer-worker-ready-flow)
+            (let [model-info (c.m/<? (state/<invoke-db-worker :thread-api/vec-search-embedding-model-info repo))]
+              (set-model-info model-info))))
+         :succ (constantly nil)))
+     [])
+    (hooks/use-effect!
+     (fn []
+       (c.m/run-task
+         ::update-load-model-progress
+         (m/reduce
+          (fn [_ v] (set-load-model-progress (walk/keywordize-keys v)))
+          vector-search-flows/load-model-progress-flow)
+         :succ (constantly nil)))
+     [])
+    [:div.panel-wrap
+     [:div.flex.flex-col.gap-2.mt-4
+      [:div.font-medium.text-muted-foreground.text-sm "Semantic search:"]
+
+      [:div.flex.flex-col.gap-2
+       [:div.it.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-start
+        [:label.block.text-sm.font-medium.leading-8.opacity-70
+         {:for "local-embedding-model"}
+         "Local embedding model"]
+        [:div.rounded-md.sm:max-w-tss.sm:col-span-2
+         [:div.flex.flex-col.gap-2
+          (shui/select
+           (cond->
+            {:on-value-change (fn [model-name]
+                                (c.m/run-task
+                                  ::load-model
+                                  (m/sp
+                                    (c.m/<?
+                                     (state/<invoke-db-worker :thread-api/vec-search-load-model repo model-name))
+                                    (set-model-info (assoc model-info :graph-text-embedding-model-name model-name))
+                                    (c.m/<?
+                                     (state/<invoke-db-worker :thread-api/vec-search-cancel-indexing repo))
+                                    (c.m/<?
+                                     (state/<invoke-db-worker :thread-api/vec-search-re-embedding-graph-data repo)))
+                                  :succ (constantly nil)))}
+             current-model
+             (assoc :value current-model))
+           (shui/select-trigger
+            {:class "h-8"}
+            (shui/select-value
+             {:placeholder "Select a model"}))
+
+           (shui/select-content
+            (shui/select-group
+             (for [model-name (:available-model-names model-info)]
+               (shui/select-item {:value model-name} model-name)))))
+
+          (when (and status current-model)
+            [:div.text-muted-foreground.text-sm
+             (let [{:keys [file progress loaded total]} load-model-progress]
+               (case status
+                 ("progress" "download" "initiate")
+                 (str "Downloading " file
+                      (when progress
+                        (util/format " %d/%dm"
+                                     (int (/ loaded 1024 1024))
+                                     (int (/ total 1024 1024)))))
+                 "done"
+                 (str "Downloaded " file)
+                 "ready"
+                 "Model is ready  🚀"
+                 nil))])]]]]]]))
+
 (rum/defcs ^:large-vars/cleanup-todo settings
   < (rum/local DEFAULT-ACTIVE-TAB-STATE ::active)
   {:will-mount
@@ -1212,7 +1297,8 @@
         _installed-plugins (state/sub :plugin/installed-plugins)
         plugins-of-settings (and config/lsp-enabled? (seq (plugin-handler/get-enabled-plugins-if-setting-schema)))
         *active (::active state)
-        logged-in? (user-handler/logged-in?)]
+        logged-in? (user-handler/logged-in?)
+        db-based? (config/db-based-graph?)]
 
     [:div#settings.cp__settings-main
      (settings-effect @*active)
@@ -1228,6 +1314,8 @@
                [:editor "editor" (t :settings-page/tab-editor) (ui/icon "writing")]
                [:keymap "keymap" (t :settings-page/tab-keymap) (ui/icon "keyboard")]
 
+               (when db-based?
+                 [:ai (t :settings-page/tab-ai) (t :settings-page/ai) (ui/icon "wand")])
                (when (util/electron?)
                  [:version-control "git" (t :settings-page/tab-version-control) (ui/icon "history")])
 
@@ -1293,4 +1381,7 @@
          :collaboration
          (settings-collaboration)
 
+         :ai
+         (settings-ai)
+
          nil)]]]))

+ 1 - 1
src/main/frontend/components/settings.css

@@ -56,7 +56,7 @@
     }
 
     h1.cp__settings-category-title {
-      @apply text-xl lowercase;
+      @apply text-xl;
     }
 
     h1.cp__settings-modal-title:first-letter,

+ 4 - 4
src/main/frontend/components/shortcut.cljs

@@ -52,8 +52,8 @@
        (let [key-handler (KeyHandler. js/document)]
           ;; setup
          (util/profile
-          "[shortcuts] unlisten*"
-          (shortcut/unlisten-all! true))
+           "[shortcuts] unlisten*"
+           (shortcut/unlisten-all! true))
          (events/listen key-handler "key"
                         (fn [^js e]
                           (.preventDefault e)
@@ -62,8 +62,8 @@
           ;; teardown
          #(do
             (util/profile
-             "[shortcuts] listen*"
-             (shortcut/listen-all!))
+              "[shortcuts] listen*"
+              (shortcut/listen-all!))
             (.dispose key-handler))))
      [])
 

+ 119 - 0
src/main/frontend/components/vector_search/sidebar.cljs

@@ -0,0 +1,119 @@
+(ns frontend.components.vector-search.sidebar
+  (:require [clojure.string :as string]
+            [fipp.edn :as fipp]
+            [frontend.common.missionary :as c.m]
+            [frontend.handler.db-based.vector-search-flows :as vector-search-flows]
+            [frontend.state :as state]
+            [frontend.util :as util]
+            [logseq.shui.hooks :as hooks]
+            [logseq.shui.ui :as shui]
+            [missionary.core :as m]
+            [rum.core :as rum]))
+
+(rum/defc ^:large-vars/cleanup-todo vector-search-sidebar
+  []
+  (let [repo (state/get-current-repo)
+        [model-info set-model-info] (hooks/use-state nil)
+        [vec-search-state set-vec-search-state] (hooks/use-state nil)
+        [load-model-progress set-load-model-progress] (hooks/use-state nil)
+        [query-string set-query-string] (hooks/use-state nil)
+        [result set-result] (hooks/use-state nil)]
+    (hooks/use-effect!
+     (fn []
+       (c.m/run-task
+         ::update-vec-search-state
+         (m/reduce
+          (fn [_ v]
+            (set-vec-search-state v))
+          (m/ap
+            (m/?> vector-search-flows/infer-worker-ready-flow)
+            (c.m/<? (state/<invoke-db-worker :thread-api/vec-search-update-index-info repo))
+            (m/?> vector-search-flows/vector-search-state-flow)))
+         :succ (constantly nil)))
+     [])
+    (hooks/use-effect!
+     (fn []
+       (c.m/run-task
+         ::update-load-model-progress
+         (m/reduce
+          (fn [_ v] (set-load-model-progress v))
+          vector-search-flows/load-model-progress-flow)
+         :succ (constantly nil)))
+     [])
+    (hooks/use-effect!
+     (fn []
+       (c.m/run-task
+         ::fetch-model-info
+         (m/reduce
+          (constantly nil)
+          (m/ap
+            (m/?> vector-search-flows/infer-worker-ready-flow)
+            (let [model-info (c.m/<? (state/<invoke-db-worker :thread-api/vec-search-embedding-model-info repo))]
+              (set-model-info model-info))))
+         :succ (constantly nil)))
+     [])
+    (hooks/use-effect!
+     (fn []
+       (when-not (string/blank? query-string)
+         (c.m/run-task
+           :update-search-result
+           (m/sp
+             (-> (c.m/<? (state/<invoke-db-worker :thread-api/vec-search-search repo query-string 10))
+                 set-result))
+           :succ (constantly nil))))
+     [(hooks/use-debounced-value query-string 200)])
+    [:div
+     [:b "State"]
+     (let [state-map (assoc (get-in vec-search-state [:repo->index-info repo])
+                            :load-model-progress load-model-progress)]
+       [:pre.select-text
+        (with-out-str
+          (fipp/pprint state-map {:width 10}))])
+     [:hr]
+     [:b "Actions"]
+     [:div
+      (shui/button
+       {:size :sm
+        :class "mx-2"
+        :on-click (fn [_]
+                    (state/<invoke-db-worker :thread-api/vec-search-embedding-stale-blocks repo))}
+       "embedding-stale-blocks")
+      (shui/button
+       {:size :sm
+        :class "mx-2"
+        :on-click (fn [_]
+                    (state/<invoke-db-worker :thread-api/vec-search-re-embedding-graph-data repo))}
+       "force-embedding-all-graph-blocks")
+      (when (get-in vec-search-state [:repo->index-info repo :indexing?])
+        (shui/button
+         {:size :sm
+          :class "mx-2"
+          :on-click (fn [_]
+                      (state/<invoke-db-worker :thread-api/vec-search-cancel-indexing repo))}
+         "cancel-current-indexing"))]
+     [:hr]
+     [:b "Settings"]
+     (shui/select
+      {:on-value-change (fn [model-name]
+                          (c.m/run-task
+                            ::load-model
+                            (m/sp
+                              (c.m/<?
+                               (state/<invoke-db-worker :thread-api/vec-search-load-model repo model-name)))
+                            :succ (constantly nil)))}
+      (shui/select-trigger
+       (shui/select-value
+        {:placeholder "Select a model(need force-embedding-all-graph-blocks again)"}))
+      (shui/select-content
+       (shui/select-group
+        (let [graph-text-embedding-model-name (:graph-text-embedding-model-name model-info)]
+          (for [model-name (:available-model-names model-info)]
+            (shui/select-item {:value model-name :disabled? (= graph-text-embedding-model-name model-name)} model-name))))))
+     [:hr]
+     [:b "Search"]
+     [:input.form-input.my-2.py-1
+      {:on-change (fn [e] (set-query-string (util/evalue e)))}]
+     [:b "Search Result:"]
+     [:pre.select-text
+      (with-out-str
+        (fipp/pprint result {:width 10}))]]))

+ 1 - 0
src/main/frontend/core.cljs

@@ -9,6 +9,7 @@
             [frontend.fs.sync :as sync]
             [frontend.handler :as handler]
             [frontend.handler.db-based.rtc-background-tasks]
+            [frontend.handler.db-based.vector-search-background-tasks]
             [frontend.handler.plugin :as plugin-handler]
             [frontend.handler.route :as route-handler]
             [frontend.log]

+ 1 - 1
src/main/frontend/db/file_based/model.cljs

@@ -3,8 +3,8 @@
   (:require [clojure.string :as string]
             [datascript.core :as d]
             [frontend.common.file-based.db :as common-file-db]
-            [frontend.db.utils :as db-utils]
             [frontend.db.conn :as conn]
+            [frontend.db.utils :as db-utils]
             [frontend.state :as state]
             [frontend.util :as util]
             [logseq.db :as ldb]

+ 27 - 27
src/main/frontend/db/react.cljs

@@ -172,34 +172,34 @@
   [repo-url affected-keys & {:keys [skip-kv-custom-keys?]
                              :or {skip-kv-custom-keys? false}}]
   (util/profile
-   "refresh!"
-   (let [db (conn/get-db repo-url)
-         affected-keys-set (set affected-keys)
-         state (->> (keep (fn [[k cache]]
-                            (let [k' (vec (rest k))]
-                              (when (and (= (first k) repo-url)
-                                         (or (contains? affected-keys-set k')
-                                             (contains? #{:custom :kv} (first k'))))
-                                [k' cache]))) @*query-state)
-                    (into {}))
-         all-keys (concat (distinct affected-keys)
-                          (when-not skip-kv-custom-keys?
-                            (filter #(contains? #{:custom :kv} (first %)) (keys state))))]
-     (doseq [k all-keys]
-       (when-let [cache (get state k)]
-         (let [{:keys [query query-fn]} cache
-               custom? (= :custom (first k))]
-           (when (or query query-fn)
-             (try
-               (let [f #(execute-query! repo-url db (vec (cons repo-url k)) cache)]
-                 (if custom?
+    "refresh!"
+    (let [db (conn/get-db repo-url)
+          affected-keys-set (set affected-keys)
+          state (->> (keep (fn [[k cache]]
+                             (let [k' (vec (rest k))]
+                               (when (and (= (first k) repo-url)
+                                          (or (contains? affected-keys-set k')
+                                              (contains? #{:custom :kv} (first k'))))
+                                 [k' cache]))) @*query-state)
+                     (into {}))
+          all-keys (concat (distinct affected-keys)
+                           (when-not skip-kv-custom-keys?
+                             (filter #(contains? #{:custom :kv} (first %)) (keys state))))]
+      (doseq [k all-keys]
+        (when-let [cache (get state k)]
+          (let [{:keys [query query-fn]} cache
+                custom? (= :custom (first k))]
+            (when (or query query-fn)
+              (try
+                (let [f #(execute-query! repo-url db (vec (cons repo-url k)) cache)]
+                  (if custom?
                    ;; perf: don't execute custom queries if they were collapsed
-                   (when-not (query-collapsed? k)
-                     (async/put! (state/get-reactive-custom-queries-chan) [f query]))
-                   (f)))
-               (catch :default e
-                 (js/console.error e)
-                 nil)))))))))
+                    (when-not (query-collapsed? k)
+                      (async/put! (state/get-reactive-custom-queries-chan) [f query]))
+                    (f)))
+                (catch :default e
+                  (js/console.error e)
+                  nil)))))))))
 
 (defn refresh!
   "Re-compute corresponding queries (from tx) and refresh the related react components."

+ 2 - 2
src/main/frontend/db/utils.cljs

@@ -3,8 +3,8 @@
   (:require [datascript.core :as d]
             [frontend.db.conn :as conn]
             [frontend.state :as state]
-            [logseq.db.frontend.content :as db-content]
-            [logseq.db.common.entity-plus :as entity-plus]))
+            [logseq.db.common.entity-plus :as entity-plus]
+            [logseq.db.frontend.content :as db-content]))
 
 ;; transit serialization
 

+ 12 - 12
src/main/frontend/extensions/latex.cljs

@@ -37,18 +37,18 @@
     (when-not @*loading?
       (reset! *loading? true)
       (loader/load "/js/katex.min.js"
-        (fn []
-          (loader/load "/js/mhchem.min.js"
-            (fn []
-              (-> (when-let [enhancers (and config/lsp-enabled?
-                                         (seq (hook-extensions-enhancers-by-key :katex)))]
-                    (for [{f :enhancer} enhancers]
-                      (when (fn? f) (f js/window.katex))))
-                (p/all)
-                (p/finally (fn []
-                             (reset! *loading? false)
-                             (render! state)))))))
-        state))))
+                   (fn []
+                     (loader/load "/js/mhchem.min.js"
+                                  (fn []
+                                    (-> (when-let [enhancers (and config/lsp-enabled?
+                                                                  (seq (hook-extensions-enhancers-by-key :katex)))]
+                                          (for [{f :enhancer} enhancers]
+                                            (when (fn? f) (f js/window.katex))))
+                                        (p/all)
+                                        (p/finally (fn []
+                                                     (reset! *loading? false)
+                                                     (render! state)))))))
+                   state))))
 
 (defn- state-&-load-and-render!
   [state]

+ 2 - 4
src/main/frontend/fs/diff_merge.cljs

@@ -2,14 +2,13 @@
   "Implementation of text (file) based content diff & merge for conflict resolution"
   (:require ["@logseq/diff-merge" :refer [attach_uuids Differ Merger]]
             [cljs-bean.core :as bean]
+            [clojure.string :as string]
             [frontend.db.file-based.model :as file-model]
             [frontend.db.utils :as db-utils]
             [logseq.graph-parser.block :as gp-block]
             [logseq.graph-parser.mldoc :as gp-mldoc]
             [logseq.graph-parser.property :as gp-property]
-            [logseq.graph-parser.utf8 :as utf8]
-            [clojure.string :as string]))
-
+            [logseq.graph-parser.utf8 :as utf8]))
 
 (defn diff
   "2-ways diff
@@ -91,7 +90,6 @@
                    :uuid uuid}
                   (reverse headings))))))))
 
-
 (defn- get-sub-content-from-pos-meta
   "Replace gp-block/get-block-content, return bare content, without any trim"
   [raw-content pos-meta]

+ 1 - 1
src/main/frontend/fs/watcher_handler.cljs

@@ -5,8 +5,8 @@
             [frontend.config :as config]
             [frontend.db :as db]
             [frontend.db.async :as db-async]
-            [frontend.db.model :as model]
             [frontend.db.file-based.model :as file-model]
+            [frontend.db.model :as model]
             [frontend.fs :as fs]
             [frontend.handler.file-based.file :as file-handler]
             [frontend.handler.file-based.property :as file-property-handler]

+ 9 - 1
src/main/frontend/handler.cljs

@@ -16,6 +16,7 @@
             [frontend.db.restore :as db-restore]
             [frontend.error :as error]
             [frontend.handler.command-palette :as command-palette]
+            [frontend.handler.db-based.vector-search-flows :as vector-search-flows]
             [frontend.handler.events :as events]
             [frontend.handler.events.ui]
             [frontend.handler.file-based.events]
@@ -167,8 +168,15 @@
                repo (or (state/get-current-repo) (:url (first repos)))
                _ (if (empty? repos)
                    (repo-handler/new-db! config/demo-repo)
-                   (restore-and-setup! repo))]
+                   (restore-and-setup! repo))
+               webgpu-available? (db-browser/<check-webgpu-available?)]
+         (log/info :webgpu-available? webgpu-available?)
+         (when webgpu-available?
+           (p/do! (db-browser/start-inference-worker!)
+                  (db-browser/<connect-db-worker-and-infer-worker!)
+                  (reset! vector-search-flows/*infer-worker-ready true)))
          (set-network-watcher!)
+
          (when (util/electron?)
            (persist-db/run-export-periodically!))
          (when (mobile-util/native-platform?)

+ 15 - 15
src/main/frontend/handler/db_based/property/util.cljs

@@ -1,7 +1,7 @@
 (ns frontend.handler.db-based.property.util
   "DB-graph only utility fns for properties"
-  (:require [frontend.db.utils :as db-utils]
-            [frontend.db.conn :as conn]
+  (:require [frontend.db.conn :as conn]
+            [frontend.db.utils :as db-utils]
             [frontend.state :as state]
             [logseq.db.frontend.property :as db-property]))
 
@@ -20,22 +20,22 @@
   ([properties {:keys [original-key? key-fn]
                 :or {key-fn identity}}]
    (->> properties
-     (map (fn [[k v]]
-            (let [prop-ent (db-utils/entity k)]
-              [(key-fn (if original-key? k (-> prop-ent :block/title keyword)))
-               (cond
-                 (set? v)
-                 (set (map db-property/property-value-content v))
+        (map (fn [[k v]]
+               (let [prop-ent (db-utils/entity k)]
+                 [(key-fn (if original-key? k (-> prop-ent :block/title keyword)))
+                  (cond
+                    (set? v)
+                    (set (map db-property/property-value-content v))
 
-                 (sequential? v)
-                 (map #(get-property-value (or (:db/id %) %)) v)
+                    (sequential? v)
+                    (map #(get-property-value (or (:db/id %) %)) v)
 
-                 (:db/id v)
-                 (get-property-value (or (:db/id v) v))
+                    (:db/id v)
+                    (get-property-value (or (:db/id v) v))
 
-                 :else
-                 v)])))
-     (into {}))))
+                    :else
+                    v)])))
+        (into {}))))
 
 (defn get-closed-property-values
   [property-id]

+ 28 - 0
src/main/frontend/handler/db_based/vector_search_background_tasks.cljs

@@ -0,0 +1,28 @@
+(ns frontend.handler.db-based.vector-search-background-tasks
+  "Background tasks for vector-search"
+  (:require [frontend.common.missionary :as c.m]
+            [frontend.config :as config]
+            [frontend.flows :as flows]
+            [frontend.handler.db-based.vector-search-flows :as vector-search-flows]
+            [frontend.state :as state]
+            [missionary.core :as m])
+  (:import [missionary Cancelled]))
+
+(defn- run-background-task-when-not-publishing
+  [key' task]
+  (when-not config/publishing?
+    (c.m/run-background-task key' task)))
+
+(run-background-task-when-not-publishing
+ ::init-load-model-when-switch-graph
+ (m/reduce
+  (constantly nil)
+  (m/ap
+    (m/?< vector-search-flows/infer-worker-ready-flow)
+    (when-let [repo (m/?< flows/current-repo-flow)]
+      (try
+        (c.m/<? (state/<invoke-db-worker :thread-api/vec-search-init-embedding-model repo))
+        (m/?< (c.m/clock (* 30 1000)))
+        (c.m/<? (state/<invoke-db-worker :thread-api/vec-search-embedding-graph repo))
+        (catch Cancelled _
+          (m/amb)))))))

+ 22 - 0
src/main/frontend/handler/db_based/vector_search_flows.cljs

@@ -0,0 +1,22 @@
+(ns frontend.handler.db-based.vector-search-flows
+  "Flows for vector-search state"
+  (:require [frontend.state :as state]
+            [missionary.core :as m]))
+
+;; input atoms
+(def *infer-worker-ready (atom nil))
+
+(def infer-worker-ready-flow
+  (m/eduction
+   (filter some?)
+   (take 1)
+   (m/watch *infer-worker-ready)))
+
+(def vector-search-state-flow
+  (m/watch (:vector-search/state @state/state)))
+
+(def load-model-progress-flow
+  (m/watch (:vector-search/load-model-progress @state/state)))
+
+(comment
+  ((m/reduce (fn [_ x] (prn :xx x)) vector-search-state-flow) prn js/console.log))

+ 6 - 0
src/main/frontend/handler/events.cljs

@@ -371,6 +371,9 @@
                       (db/entity [:block/uuid (:block/uuid block)])))]
        (js/setTimeout #(editor-handler/edit-block! block :max) 100)))))
 
+(defmethod handle :vector-search/sync-state [[_ state]]
+  (state/set-state! :vector-search/state state))
+
 (defmethod handle :rtc/sync-state [[_ state]]
   (state/update-state! :rtc/state (fn [old] (merge old state))))
 
@@ -433,6 +436,9 @@
                   (db-async/<get-block (state/get-current-repo) id
                                        {:skip-refresh? false})) ids))))
 
+(defmethod handle :vector-search/load-model-progress [[_ data]]
+  (state/set-state! :vector-search/load-model-progress data))
+
 (defn run!
   []
   (let [chan (state/get-events-chan)]

+ 13 - 13
src/main/frontend/handler/export/opml.cljs

@@ -435,19 +435,19 @@
   {:pre [(or (coll? root-block-uuids-or-page-uuid)
              (uuid? root-block-uuids-or-page-uuid))]}
   (util/profile
-   :export-blocks-as-opml
-   (let [content
-         (if (uuid? root-block-uuids-or-page-uuid)
+    :export-blocks-as-opml
+    (let [content
+          (if (uuid? root-block-uuids-or-page-uuid)
            ;; page
-           (common/get-page-content root-block-uuids-or-page-uuid)
-           (common/root-block-uuids->content repo root-block-uuids-or-page-uuid))
-         title (if (uuid? root-block-uuids-or-page-uuid)
-                 (:block/title (db/entity [:block/uuid root-block-uuids-or-page-uuid]))
-                 "untitled")
-         first-block (and (coll? root-block-uuids-or-page-uuid)
-                          (db/entity [:block/uuid (first root-block-uuids-or-page-uuid)]))
-         format (get first-block :block/format :markdown)]
-     (export-helper content format options :title title))))
+            (common/get-page-content root-block-uuids-or-page-uuid)
+            (common/root-block-uuids->content repo root-block-uuids-or-page-uuid))
+          title (if (uuid? root-block-uuids-or-page-uuid)
+                  (:block/title (db/entity [:block/uuid root-block-uuids-or-page-uuid]))
+                  "untitled")
+          first-block (and (coll? root-block-uuids-or-page-uuid)
+                           (db/entity [:block/uuid (first root-block-uuids-or-page-uuid)]))
+          format (get first-block :block/format :markdown)]
+      (export-helper content format options :title title))))
 
 (defn- export-files-as-opml
   "options see also `export-blocks-as-opml`"
@@ -456,7 +456,7 @@
    (fn [{:keys [path content title format]}]
      (when (and title (not (string/blank? content)))
        (util/profile (print-str :export-files-as-opml path)
-                     [path (export-helper content format options :title title)])))
+         [path (export-helper content format options :title title)])))
    files))
 
 (defn export-repo-as-opml!

+ 19 - 19
src/main/frontend/handler/export/text.cljs

@@ -512,25 +512,25 @@
   {:pre [(or (coll? root-block-uuids-or-page-uuid)
              (uuid? root-block-uuids-or-page-uuid))]}
   (util/profile
-   :export-blocks-as-markdown
-   (try
-     (let [content
-           (cond
+    :export-blocks-as-markdown
+    (try
+      (let [content
+            (cond
              ;; page
-             (and (= 1 (count root-block-uuids-or-page-uuid))
-                  (ldb/page? (db/entity [:block/uuid (first root-block-uuids-or-page-uuid)])))
-             (common/get-page-content (first root-block-uuids-or-page-uuid))
-             (and (coll? root-block-uuids-or-page-uuid) (every? #(ldb/page? (db/entity [:block/uuid %])) root-block-uuids-or-page-uuid))
-             (->> (mapv (fn [id] (:block/title (db/entity [:block/uuid id]))) root-block-uuids-or-page-uuid)
-                  (string/join "\n"))
-             :else
-             (common/root-block-uuids->content repo root-block-uuids-or-page-uuid))
-           first-block (and (coll? root-block-uuids-or-page-uuid)
-                            (db/entity [:block/uuid (first root-block-uuids-or-page-uuid)]))
-           format (get first-block :block/format :markdown)]
-       (export-helper content format options))
-     (catch :default e
-       (js/console.error e)))))
+              (and (= 1 (count root-block-uuids-or-page-uuid))
+                   (ldb/page? (db/entity [:block/uuid (first root-block-uuids-or-page-uuid)])))
+              (common/get-page-content (first root-block-uuids-or-page-uuid))
+              (and (coll? root-block-uuids-or-page-uuid) (every? #(ldb/page? (db/entity [:block/uuid %])) root-block-uuids-or-page-uuid))
+              (->> (mapv (fn [id] (:block/title (db/entity [:block/uuid id]))) root-block-uuids-or-page-uuid)
+                   (string/join "\n"))
+              :else
+              (common/root-block-uuids->content repo root-block-uuids-or-page-uuid))
+            first-block (and (coll? root-block-uuids-or-page-uuid)
+                             (db/entity [:block/uuid (first root-block-uuids-or-page-uuid)]))
+            format (get first-block :block/format :markdown)]
+        (export-helper content format options))
+      (catch :default e
+        (js/console.error e)))))
 
 (defn export-files-as-markdown
   "options see also `export-blocks-as-markdown`"
@@ -538,7 +538,7 @@
   (mapv
    (fn [{:keys [path title content]}]
      (util/profile (print-str :export-files-as-markdown title)
-                   [(or path title) (export-helper content :markdown options)]))
+       [(or path title) (export-helper content :markdown options)]))
    files))
 
 (defn export-repo-as-markdown!

+ 2 - 2
src/main/frontend/handler/file_based/page_property.cljs

@@ -75,8 +75,8 @@
                    :block/parent page-id
                    :block/page page-id
                    :block/title (if org?
-                                    (str "#+" (string/upper-case (name key)) ": " value)
-                                    (str (name key) ":: " value))
+                                  (str "#+" (string/upper-case (name key)) ": " value)
+                                  (str (name key) ":: " value))
                    :block/format format
                    :block/properties {key value}
                    :block/pre-block? true}

+ 1 - 1
src/main/frontend/handler/plugin.cljs

@@ -891,7 +891,7 @@
 
                   (.on "themes-changed" (fn [^js themes]
                                           (swap! state/state assoc :plugin/installed-themes
-                                            (vec (mapcat (fn [[pid vs]] (mapv #(assoc % :pid pid) (bean/->clj vs))) (bean/->clj themes))))))
+                                                 (vec (mapcat (fn [[pid vs]] (mapv #(assoc % :pid pid) (bean/->clj vs))) (bean/->clj themes))))))
 
                   (.on "theme-selected" (fn [^js theme]
                                           (let [theme (bean/->clj theme)

+ 7 - 1
src/main/frontend/handler/worker.cljs

@@ -1,5 +1,5 @@
 (ns frontend.handler.worker
-  "Handle messages received from the db worker"
+  "Handle messages received from the webworkers"
   (:require [cljs-bean.core :as bean]
             [frontend.handler.file-based.file :as file-handler]
             [frontend.handler.notification :as notification]
@@ -37,6 +37,9 @@
   (let [state data]
     (state/pub-event! [:rtc/sync-state state])))
 
+(defmethod handle :vector-search-sync-state [_ _worker data]
+  (state/pub-event! [:vector-search/sync-state data]))
+
 (defmethod handle :sync-db-changes [_ _worker data]
   (state/pub-event! [:db/sync-changes data]))
 
@@ -49,6 +52,9 @@
 (defmethod handle :capture-error [_ _worker data]
   (state/pub-event! [:capture-error data]))
 
+(defmethod handle :vector-search/load-model-progress [_ _ data]
+  (state/pub-event! [:vector-search/load-model-progress data]))
+
 (defmethod handle :backup-file [_ _worker data]
   (state/pub-event! [:graph/backup-file data]))
 

+ 72 - 0
src/main/frontend/inference_worker/inference_worker.cljs

@@ -0,0 +1,72 @@
+(ns frontend.inference-worker.inference-worker
+  "Worker used for text embedding and vector-db"
+  (:require ["comlink" :as Comlink]
+            [frontend.inference-worker.text-embedding :as infer-worker.text-embedding]
+            [lambdaisland.glogi.console :as glogi-console]
+            [promesa.core :as p]
+            [shadow.cljs.modern :refer [defclass]]))
+
+#_{:clj-kondo/ignore [:unresolved-symbol]}
+(defclass InferenceWorker
+  (extends js/Object)
+  (constructor
+   [this]
+   (super))
+
+  Object
+  (init
+   [_this model-name]
+   (infer-worker.text-embedding/<init model-name))
+
+  (load-model
+   [_this model-name]
+   (infer-worker.text-embedding/<load-model model-name))
+
+  (available-embedding-models
+   [_]
+   (clj->js (keys infer-worker.text-embedding/available-embedding-models)))
+
+  (text-embedding
+   [_this text-coll]
+   (p/chain
+    (infer-worker.text-embedding/<text-embedding text-coll)
+    clj->js))
+
+  (text-embedding+store!
+   ;; return labels(js array)
+   [_this repo text-array labels replace-deleted?]
+   (p/chain
+    (js/Promise. (infer-worker.text-embedding/task--text-embedding&store! repo text-array labels replace-deleted?))
+    clj->js))
+
+  (delete-labels
+   [_this repo labels]
+   (infer-worker.text-embedding/delete-items repo labels))
+
+  (force-reset-index!
+   [_this repo]
+   (js/Promise. (infer-worker.text-embedding/task--force-reset-index! repo)))
+
+  (write-index!
+   [_this repo]
+   (js/Promise. (infer-worker.text-embedding/task--write-index! repo)))
+
+  (search
+   [_this repo query-string nums-neighbors]
+   (infer-worker.text-embedding/<search-knn repo query-string nums-neighbors))
+
+  (index-info
+   [_this repo]
+   (clj->js (infer-worker.text-embedding/index-info repo))))
+
+(defn init
+  [])
+
+(.addEventListener js/self "connect"
+                   (fn [^js e]
+                     (glogi-console/install!)
+                     (let [port (first (.-ports e))
+                           ^js obj #_{:clj-kondo/ignore [:unresolved-symbol]} (InferenceWorker.)]
+                       (reset! infer-worker.text-embedding/*port port)
+                       (.start port)
+                       (Comlink/expose obj port))))

+ 10 - 0
src/main/frontend/inference_worker/state.cljs

@@ -0,0 +1,10 @@
+(ns frontend.inference-worker.state
+  "State hub for inference-worker")
+
+(defonce *hnswlib (atom nil))
+
+;;repo -> index
+(defonce *hnsw-index (atom {}))
+
+(defonce *extractor (atom nil))
+(defonce *model-name+config (atom nil))

+ 237 - 0
src/main/frontend/inference_worker/text_embedding.cljs

@@ -0,0 +1,237 @@
+(ns frontend.inference-worker.text-embedding
+  "text embedding fns"
+  (:require ["@huggingface/transformers" :refer [pipeline]]
+            ["hnswlib-wasm" :refer [loadHnswlib]]
+            [clojure.data :as data]
+            [frontend.common.missionary :as c.m]
+            [frontend.inference-worker.state :as infer-worker.state]
+            [frontend.worker-common.util :as worker-util]
+            [lambdaisland.glogi :as log]
+            [logseq.common.config :as common-config]
+            [missionary.core :as m]
+            [promesa.core :as p]))
+
+(add-watch infer-worker.state/*hnsw-index :delete-obj-when-dissoc
+           (fn [_ _ o n]
+             (let [[old-only] (data/diff o n)]
+               (doseq [[repo ^js hnsw-index] old-only]
+                 (when hnsw-index
+                   (log/info :delete-hnsw-index repo)
+                   (.delete hnsw-index))))))
+
+(defonce *port (atom nil))
+
+(def ^:private embedding-opts #js{"pooling" "mean" "normalize" true})
+
+(def ^:private init-max-elems 100)
+
+(defn- split-into-chunks
+  [js-array chunk-size]
+  (let [length (alength js-array)
+        result (array)]
+    (loop [i 0]
+      (when (< i length)
+        (.push result (.slice js-array i (+ i chunk-size)))
+        (recur (+ i chunk-size))))
+    result))
+
+(defn- init-index!
+  [^js hnsw]
+  (.initIndex hnsw init-max-elems 16 200 100)
+  (.setEfSearch hnsw 32))
+
+(defn- ^js get-hnsw-index
+  [repo]
+  (or (@infer-worker.state/*hnsw-index repo)
+      (let [hnsw-ctor (.-HierarchicalNSW ^js @infer-worker.state/*hnswlib)
+            hnsw (new hnsw-ctor "cosine" (or (:dims (:hnsw-config (second @infer-worker.state/*model-name+config))) 384) "")
+            file-exists? (.checkFileExists (.-EmscriptenFileSystemManager ^js @infer-worker.state/*hnswlib) repo)]
+        (when file-exists?
+          (.readIndex hnsw repo init-max-elems)
+          (swap! infer-worker.state/*hnsw-index assoc repo hnsw)
+          hnsw))))
+
+(defn- ^js new-hnsw-index!
+  [repo]
+  (when (get-hnsw-index repo)
+    (swap! infer-worker.state/*hnsw-index dissoc repo))
+  (let [hnsw-ctor (.-HierarchicalNSW ^js @infer-worker.state/*hnswlib)
+        hnsw (new hnsw-ctor "cosine" (or (:dims (:hnsw-config (second @infer-worker.state/*model-name+config))) 384) "")]
+    (init-index! hnsw)
+    (swap! infer-worker.state/*hnsw-index assoc repo hnsw)
+    hnsw))
+
+(defn- model-loaded?
+  []
+  (and @infer-worker.state/*extractor
+       @infer-worker.state/*model-name+config))
+
+(defn <text-embedding
+  [text-array]
+  (assert (and (array? text-array) (every? string? text-array)))
+  (p/let [^js r (._call ^js @infer-worker.state/*extractor text-array embedding-opts)]
+    {:data (.-data r)
+     :type (.-type r)
+     :dims (.-dims r)
+     :size (.-size r)}))
+
+(defn- add-items
+  [^js hnsw data-coll labels replace-deleted?]
+  (let [max-elems (.getMaxElements hnsw)
+        current-count (.getCurrentCount hnsw)
+        add-count (count data-coll)]
+    (when (>= (+ add-count current-count) max-elems)
+      (let [new-size (+ current-count (max (* 2 add-count) current-count))]
+        (log/info :hnsw-resize {:from current-count :to new-size})
+        (.resizeIndex hnsw new-size)))
+    ;; (.addItems hnsw data-coll labels replace-deleted?)
+    (dorun
+     (mapcat
+      (fn [embedding label]
+        (assert (and embedding label) {:embedding embedding
+                                       :label label})
+        (.addPoint hnsw embedding label replace-deleted?))
+      data-coll
+      labels))))
+
+(defn delete-items
+  [repo labels]
+  (when-let [hnsw (get-hnsw-index repo)]
+    (.markDeleteItems hnsw (into-array labels))))
+
+(defn task--text-embedding&store!
+  "return labels(js-array)"
+  [repo text-array labels replace-deleted?]
+  (m/sp
+    (when (model-loaded?)
+      (let [hnsw (or (get-hnsw-index repo) (new-hnsw-index! repo))
+            {:keys [data _type dims _size]} (worker-util/profile :<text-embedding
+                                                                 (c.m/<? (<text-embedding text-array)))
+            data-coll (split-into-chunks data (last dims))
+            _ (assert (= (count text-array) (count data-coll)))]
+        (worker-util/profile (keyword "add-items" (str (alength data-coll)))
+                             (add-items hnsw data-coll labels replace-deleted?))))))
+
+(def ^:private write-index-wait-delays-flow
+  (m/ap
+    (loop [[delay-ms & others]
+           ;; 50ms + 100 * 100ms = ~10s
+           (cons 50 (repeat 100 100))]
+      (if delay-ms
+        (m/amb
+         (m/? (m/sleep delay-ms))
+         (recur others))
+        (m/amb :timeout)))))
+
+(defn- task--write-index!*
+  "NOTE: writeIndex return nil, but it should be a promise.
+  so we loop check fs-synced here, until synced=true"
+  [repo ^js hnsw]
+  (m/sp
+    (.writeIndex hnsw repo)
+    (let [^js fs (.-EmscriptenFileSystemManager ^js @infer-worker.state/*hnswlib)]
+      (m/?
+       (m/reduce
+        (fn [_ x]
+          (if-not (keyword-identical? :timeout x)
+            (when (.isSynced fs)
+              (reduced true))
+            (reduced false)))
+        write-index-wait-delays-flow)))))
+
+(defn task--force-reset-index!
+  "Remove all data in hnsw-index.
+  Return synced? (bool)"
+  [repo]
+  (m/sp
+    (when-let [hnsw (new-hnsw-index! repo)]
+      (when-not (zero? (.getCurrentCount hnsw))
+        (init-index! hnsw)
+        (m/? (task--write-index!* repo hnsw))))))
+
+(defn task--write-index!
+  [repo]
+  (m/sp
+    (when-let [hnsw (get-hnsw-index repo)]
+      (m/? (task--write-index!* repo hnsw)))))
+
+(defn- search-knn
+  [repo query-point num-neighbors]
+  (when-let [hnsw (get-hnsw-index repo)]
+    (.searchKnn hnsw query-point num-neighbors nil)))
+
+(defn <search-knn
+  "return labels"
+  [repo query-string num-neighbors]
+  (when (model-loaded?)
+    (p/let [query-embedding (<text-embedding #js[query-string])
+            query-point (:data query-embedding)]
+      (search-knn repo query-point num-neighbors))))
+
+(defn index-info
+  [repo]
+  (when-let [hnsw (get-hnsw-index repo)]
+    {:current-count (.getCurrentCount hnsw)
+     :max-elements (.getMaxElements hnsw)
+     :ef-search (.getEfSearch hnsw)
+     :num-dims (.getNumDimensions hnsw)}))
+
+(def available-embedding-models
+  {"Xenova/all-MiniLM-L6-v2" {:tf-config {:dtype "fp32"}
+                              :hnsw-config {:dims 384}}
+   ;; "onnx-community/Qwen3-Embedding-0.6B-ONNX" {:tf-config {:dtype "fp16"}
+   ;;                                             :hnsw-config {:dims 1024}}
+   })
+
+(defonce ^:private *load-model-progress (atom nil))
+
+(defn <load-model
+  [model-name]
+  (if (= model-name (first @infer-worker.state/*model-name+config))
+    true
+    (when-let [config (get available-embedding-models model-name)]
+      (p/let [extractor (pipeline "feature-extraction" model-name
+                                  (clj->js
+                                   (-> (:tf-config config)
+                                       (assoc "device" "webgpu")
+                                       (assoc "progress_callback" #(reset! *load-model-progress %)))))]
+        (reset! infer-worker.state/*extractor extractor)
+        (reset! infer-worker.state/*model-name+config [model-name config])
+        true))))
+
+(defn <init
+  [model-name]
+  (p/let [hnswlib (loadHnswlib)]
+    (reset! infer-worker.state/*hnswlib hnswlib)
+    (.setDebugLogs (.-EmscriptenFileSystemManager ^js @infer-worker.state/*hnswlib) true)
+    (log/info :loaded :hnswlib)
+    (when model-name
+      (<load-model model-name))))
+
+(when-not common-config/PUBLISHING
+  (c.m/run-background-task
+   ::push-load-model-progress
+   (m/reduce
+    (fn [_ v]
+      (when-let [port @*port]
+        (worker-util/post-message :vector-search/load-model-progress v {:port port})))
+    (c.m/throttle 500 (m/watch *load-model-progress)))))
+
+(comment
+  (def repo "repo-1")
+  (def hnsw (ensure-hnsw-index! repo))
+  (def text-coll-100
+    (apply concat (repeatedly
+                   10
+                   (fn []
+                     ["The universe is constantly expanding, revealing new mysteries every day."
+                      "She decided to take a walk in the park to clear her mind."
+                      "Black holes are among the most fascinating and enigmatic objects in the cosmos."
+                      "The cat curled up on the windowsill, basking in the afternoon sun."
+                      "Scientists believe dark matter makes up a significant portion of the universe."
+                      "He practiced the piano diligently, hoping to master the piece by next week."
+                      "The stars twinkled brightly in the clear night sky, each one a distant sun."
+                      "They laughed together, sharing stories from their childhood."
+                      "The Milky Way is just one of billions of galaxies in the vast universe."
+                      "She opened the book and began reading, losing herself in the stor"]))))
+  (<text-embedding&store! repo text-coll-100 nil))

+ 2 - 1
src/main/frontend/mixins.cljs

@@ -9,7 +9,8 @@
 (defn detach
   "Detach all event listeners."
   [state]
-  (some-> state ::event-handler .removeAll))
+  (when-let [^EventHandler handler (some-> state ::event-handler)]
+    (.removeAll handler)))
 
 (defn listen
   "Register an event `handler` for events of `type` on `target`."

+ 1 - 1
src/main/frontend/mobile/intent.cljs

@@ -14,10 +14,10 @@
             [frontend.handler.editor :as editor-handler]
             [frontend.handler.notification :as notification]
             [frontend.mobile.util :as mobile-util]
-            [frontend.util.ref :as ref]
             [frontend.state :as state]
             [frontend.util :as util]
             [frontend.util.fs :as fs-util]
+            [frontend.util.ref :as ref]
             [goog.string :as gstring]
             [lambdaisland.glogi :as log]
             [logseq.common.config :as common-config]

+ 2 - 1
src/main/frontend/modules/outliner/pipeline.cljs

@@ -86,7 +86,8 @@
                 (state/set-state! :editor/edit-block-fn nil)
                 (when delete-blocks?
                   (util/mobile-keep-keyboard-open))
-                (react/refresh! repo affected-keys)
+                (when-not (:skip-refresh? tx-meta)
+                  (react/refresh! repo affected-keys))
                 (when edit-block-f
                   (util/schedule edit-block-f)))
 

+ 4 - 4
src/main/frontend/modules/shortcut/core.cljs

@@ -184,9 +184,9 @@
       :will-remount
       (fn [old-state new-state]
         (util/profile "[shortcuts] reinstalled:"
-                      (uninstall-shortcut-handler! (::install-id old-state))
-                      (when-let [install-id (install-shortcut-handler! handler-id {:state new-state})]
-                        (assoc new-state ::install-id install-id))))))))
+          (uninstall-shortcut-handler! (::install-id old-state))
+          (when-let [install-id (install-shortcut-handler! handler-id {:state new-state})]
+            (assoc new-state ::install-id install-id))))))))
 
 (defn mixin*
   "This is an optimized version compared to (mixin).
@@ -219,7 +219,7 @@
    (doseq [{:keys [handler group dispatch-fn]} (vals @*installed-handlers)
            :when (not= group :shortcut.handler/misc)]
      (if dispose?
-       (.dispose handler)
+       (.dispose ^js handler)
        (events/unlisten handler EventType/SHORTCUT_TRIGGERED dispatch-fn)))))
 
 (defn listen-all! []

+ 34 - 2
src/main/frontend/persist_db/browser.cljs

@@ -1,5 +1,5 @@
 (ns frontend.persist-db.browser
-  "Browser db persist support, using @logseq/sqlite-wasm.
+  "Browser db persist support, using sqlite-wasm.
 
    This interface uses clj data format as input."
   (:require ["comlink" :as Comlink]
@@ -8,6 +8,7 @@
             [frontend.common.thread-api :as thread-api]
             [frontend.config :as config]
             [frontend.date :as date]
+            [frontend.db :as db]
             [frontend.db.transact :as db-transact]
             [frontend.handler.notification :as notification]
             [frontend.handler.worker :as worker-handler]
@@ -15,6 +16,7 @@
             [frontend.state :as state]
             [frontend.undo-redo :as undo-redo]
             [frontend.util :as util]
+            [lambdaisland.glogi :as log]
             [logseq.db :as ldb]
             [missionary.core :as m]
             [promesa.core :as p]))
@@ -92,8 +94,12 @@
                            (p/let [result (.remoteInvoke ^js wrapped-worker*
                                                          (str (namespace qkw) "/" (name qkw))
                                                          direct-pass?
-                                                         (if direct-pass?
+                                                         (cond
+                                                           (= qkw :thread-api/set-infer-worker-proxy)
+                                                           (first args)
+                                                           direct-pass?
                                                            (into-array args)
+                                                           :else
                                                            (ldb/write-transit-str args)))]
                              (if direct-pass?
                                result
@@ -120,6 +126,32 @@
                      (prn :debug "Can't init SQLite wasm")
                      (js/console.error error)))))))
 
+(defn <check-webgpu-available?
+  []
+  (if (some? js/navigator.gpu)
+    (p/chain (js/navigator.gpu.requestAdapter) some?)
+    (p/promise false)))
+
+(defn start-inference-worker!
+  []
+  (when-not util/node-test?
+    (let [worker-url "js/inference-worker.js"
+          ^js worker (js/SharedWorker. (str worker-url "?electron=" (util/electron?) "&publishing=" config/publishing?))
+          ^js port (.-port worker)
+          wrapped-worker (Comlink/wrap port)
+          t1 (util/time-ms)]
+      (worker-handler/handle-message! port wrapped-worker)
+      (reset! state/*infer-worker wrapped-worker)
+      (p/do!
+       (let [embedding-model-name (ldb/get-key-value (db/get-db) :logseq.kv/graph-text-embedding-model-name)]
+         (.init wrapped-worker embedding-model-name))
+       (log/info "init infer-worker spent:" (str  (- (util/time-ms) t1) "ms"))))))
+
+(defn <connect-db-worker-and-infer-worker!
+  []
+  (assert (and @state/*infer-worker @state/*db-worker))
+  (state/<invoke-db-worker-direct-pass :thread-api/set-infer-worker-proxy (Comlink/proxy @state/*infer-worker)))
+
 (defn <export-db!
   [repo data]
   (when (util/electron?)

+ 6 - 1
src/main/frontend/state.cljs

@@ -60,6 +60,8 @@
   [qkw & args]
   (<invoke-db-worker* qkw true args))
 
+(defonce *infer-worker (atom nil))
+
 ;; Stores main application state
 (defonce ^:large-vars/data-var state
   (let [document-mode? (or (storage/get :document/mode?) false)
@@ -346,7 +348,10 @@
                                                        3))
       :favorites/updated?                    (atom 0)
       :db/async-queries                      (atom {})
-      :db/latest-transacted-entity-uuids     (atom {})})))
+      :db/latest-transacted-entity-uuids     (atom {})
+
+      :vector-search/state                   (atom {})
+      :vector-search/load-model-progress     (atom nil)})))
 
 ;; User configuration getters under :config (and sometimes :me)
 ;; ========================================

+ 1 - 1
src/main/frontend/template.cljs

@@ -4,8 +4,8 @@
             [frontend.date :as date]
             [frontend.db.conn :as conn]
             [frontend.db.utils :as db-utils]
-            [frontend.util.ref :as ref]
             [frontend.state :as state]
+            [frontend.util.ref :as ref]
             [logseq.db :as ldb]))
 
 (defn- variable-rules

+ 10 - 8
src/main/frontend/ui.cljs

@@ -47,7 +47,7 @@
 (defonce virtualized-list (r/adapt-class Virtuoso))
 (defonce virtualized-grid (r/adapt-class VirtuosoGrid))
 
-(def ReactTweetEmbed (r/adapt-class react-tweet-embed))
+(def ReactTweetEmbed (r/adapt-class (gobj/get react-tweet-embed "default")))
 (def useInView (gobj/get react-intersection-observer "useInView"))
 (defonce _emoji-init-data ((gobj/get emoji-mart "init") #js {:data emoji-data}))
 ;; (def EmojiPicker (r/adapt-class (gobj/get Picker "default")))
@@ -810,15 +810,17 @@
       :on-pointer-up #(let [value (util/evalue %)]
                         (on-change value))}]))
 
-(rum/defcs tweet-embed < (rum/local true :loading?)
+(rum/defcs tweet-embed < rum/reactive
+  (rum/local true :loading?)
   [state id]
   (let [*loading? (:loading? state)]
-    [:div [(when @*loading? [:span.flex.items-center [svg/loading " ... loading"]])
-           (ReactTweetEmbed
-            {:id                    id
-             :class                 "contents"
-             :options               {:theme (when (= (state/sub :ui/theme) "dark") "dark")}
-             :on-tweet-load-success #(reset! *loading? false)})]]))
+    [:div
+     (when @*loading? [:span.flex.items-center [svg/loading " loading"]])
+     (ReactTweetEmbed
+      {:id                    id
+       :class                 "contents"
+       :options               {:theme (when (= (state/sub :ui/theme) "dark") "dark")}
+       :on-tweet-load-success #(reset! *loading? false)})]))
 
 (def icon shui.icon.v2/root)
 

+ 1 - 1
src/main/frontend/util.cljc

@@ -1100,7 +1100,7 @@
 
 (defn keyname [key] (str (namespace key) "/" (name key)))
 
-;; FIXME: drain-chan was copied from frontend.worker.util due to shadow-cljs compile bug
+;; FIXME: drain-chan was copied from frontend.worker-common.util due to shadow-cljs compile bug
 #?(:cljs
    (defn drain-chan
      "drop all stuffs in CH, and return all of them"

+ 2 - 2
src/main/frontend/utils.js

@@ -1,4 +1,4 @@
-import path from 'path/path.js'
+import path from 'path'
 
 // TODO split the capacitor abilities to a separate file for capacitor APIs
 import { Capacitor } from '@capacitor/core'
@@ -481,4 +481,4 @@ export function base64ToUint8Array (base64String) {
     console.error('Invalid Base64 string:', e)
     return null
   }
-}
+}

+ 3 - 2
src/main/frontend/worker/db/migrate.cljs

@@ -4,7 +4,7 @@
             [clojure.walk :as walk]
             [datascript.core :as d]
             [datascript.impl.entity :as de]
-            [frontend.worker.util :as worker-util]
+            [frontend.worker-common.util :as worker-util]
             [logseq.common.config :as common-config]
             [logseq.common.util :as common-util]
             [logseq.db :as ldb]
@@ -366,7 +366,8 @@
    ["65.5" {:fix remove-block-order-for-tags}]
    ["65.6" {:fix update-extends-to-cardinality-many}]
    ["65.7" {:fix add-quick-add-page}]
-   ["65.8" {:fix add-missing-page-name}]])
+   ["65.8" {:fix add-missing-page-name}]
+   ["65.9" {:properties [:logseq.property.embedding/hnsw-label-updated-at]}]])
 
 (let [[major minor] (last (sort (map (comp (juxt :major :minor) db-schema/parse-schema-version first)
                                      schema-version->updates)))]

+ 71 - 35
src/main/frontend/worker/db_worker.cljs

@@ -1,6 +1,6 @@
 (ns frontend.worker.db-worker
   "Worker used for browser DB implementation"
-  (:require ["@logseq/sqlite-wasm" :default sqlite3InitModule]
+  (:require ["@sqlite.org/sqlite-wasm" :default sqlite3InitModule]
             ["comlink" :as Comlink]
             [cljs-bean.core :as bean]
             [cljs.cache :as cache]
@@ -13,11 +13,13 @@
             [frontend.common.graph-view :as graph-view]
             [frontend.common.missionary :as c.m]
             [frontend.common.thread-api :as thread-api :refer [def-thread-api]]
+            [frontend.worker-common.util :as worker-util]
             [frontend.worker.db-listener :as db-listener]
             [frontend.worker.db-metadata :as worker-db-metadata]
             [frontend.worker.db.fix :as db-fix]
             [frontend.worker.db.migrate :as db-migrate]
             [frontend.worker.db.validate :as worker-db-validate]
+            [frontend.worker.embedding :as embedding]
             [frontend.worker.export :as worker-export]
             [frontend.worker.file :as file]
             [frontend.worker.file.reset :as file-reset]
@@ -31,7 +33,6 @@
             [frontend.worker.shared-service :as shared-service]
             [frontend.worker.state :as worker-state]
             [frontend.worker.thread-atom]
-            [frontend.worker.util :as worker-util]
             [goog.object :as gobj]
             [lambdaisland.glogi :as log]
             [lambdaisland.glogi.console :as glogi-console]
@@ -69,8 +70,8 @@
   [graph]
   (when-not @*publishing?
     (or (worker-state/get-opfs-pool graph)
-        (p/let [^js pool (.installOpfsSAHPoolVfs @*sqlite #js {:name (worker-util/get-pool-name graph)
-                                                               :initialCapacity 20})]
+        (p/let [^js pool (.installOpfsSAHPoolVfs ^js @*sqlite #js {:name (worker-util/get-pool-name graph)
+                                                                   :initialCapacity 20})]
           (swap! *opfs-pools assoc graph pool)
           pool))))
 
@@ -78,17 +79,10 @@
   []
   (when-not @*sqlite
     (p/let [href (.. js/location -href)
-            electron? (string/includes? href "electron=true")
             publishing? (string/includes? href "publishing=true")
-
-            _ (reset! *publishing? publishing?)
-            base-url (str js/self.location.protocol "//" js/self.location.host)
-            sqlite-wasm-url (if electron?
-                              (js/URL. "sqlite3.wasm" (.. js/location -href))
-                              (str base-url (string/replace js/self.location.pathname "db-worker.js" "")))
-            sqlite (sqlite3InitModule (clj->js {:url sqlite-wasm-url
-                                                :print js/console.log
+            sqlite (sqlite3InitModule (clj->js {:print js/console.log
                                                 :printErr js/console.error}))]
+      (reset! *publishing? publishing?)
       (reset! *sqlite sqlite)
       nil)))
 
@@ -196,7 +190,7 @@
   (when search (.close search))
   (when client-ops (.close client-ops))
   (when-let [^js pool (worker-state/get-opfs-pool repo)]
-    (.releaseAccessHandles pool))
+    (.pauseVfs pool))
   (swap! *opfs-pools dissoc repo))
 
 (defn- close-other-dbs!
@@ -230,7 +224,7 @@
     (p/let [^js pool (<get-opfs-pool repo)
             capacity (.getCapacity pool)
             _ (when (zero? capacity)   ; file handle already releases since pool will be initialized only once
-                (.acquireAccessHandles pool))
+                (.unpauseVfs pool))
             db (new (.-OpfsSAHPoolDb pool) repo-path)
             search-db (new (.-OpfsSAHPoolDb pool) (str "search" repo-path))
             client-ops-db (new (.-OpfsSAHPoolDb pool) (str "client-ops-" repo-path))]
@@ -340,9 +334,7 @@
             db-dirs (filter (fn [file]
                               (string/starts-with? (.-name file) db-dir-prefix))
                             current-dir-dirs)]
-      (prn :debug
-           :db-dirs (map #(.-name %) db-dirs)
-           :all-dirs (map #(.-name %) current-dir-dirs))
+      (log/info :db-dirs (map #(.-name %) db-dirs) :all-dirs (map #(.-name %) current-dir-dirs))
       (p/all (map (fn [dir]
                     (p/let [graph-name (-> (.-name dir)
                                            (string/replace-first ".logseq-pool-" "")
@@ -388,8 +380,14 @@
   (reset! worker-state/*rtc-ws-url rtc-ws-url)
   (init-sqlite-module!))
 
+(def-thread-api :thread-api/set-infer-worker-proxy
+  [infer-worker-proxy]
+  (reset! worker-state/*infer-worker infer-worker-proxy)
+  nil)
+
 ;; [graph service]
 (defonce *service (atom []))
+
 (defonce fns {"remoteInvoke" thread-api/remote-function})
 
 (defn- start-db!
@@ -464,24 +462,25 @@
 
 (defn- search-blocks
   [repo q option]
-  (p/let [search-db (get-search-db repo)
-          conn (worker-state/get-datascript-conn repo)]
+  (let [search-db (get-search-db repo)
+        conn (worker-state/get-datascript-conn repo)]
     (search/search-blocks repo conn search-db q option)))
 
 (def-thread-api :thread-api/block-refs-check
   [repo id {:keys [unlinked?]}]
-  (when-let [conn (worker-state/get-datascript-conn repo)]
-    (let [db @conn
-          block (d/entity db id)]
-      (if unlinked?
-        (p/let [title (string/lower-case (:block/title block))
-                result (search-blocks repo title {:limit 100})]
-          (boolean (some (fn [b]
-                           (let [block (d/entity db (:db/id b))]
-                             (and (not= id (:db/id block))
-                                  (not ((set (map :db/id (:block/refs block))) id))
-                                  (string/includes? (string/lower-case (:block/title block)) title)))) result)))
-        (some? (first (common-initial-data/get-block-refs db (:db/id block))))))))
+  (m/sp
+    (when-let [conn (worker-state/get-datascript-conn repo)]
+      (let [db @conn
+            block (d/entity db id)]
+        (if unlinked?
+          (let [title (string/lower-case (:block/title block))
+                result (m/? (search-blocks repo title {:limit 100}))]
+            (boolean (some (fn [b]
+                             (let [block (d/entity db (:db/id b))]
+                               (and (not= id (:db/id block))
+                                    (not ((set (map :db/id (:block/refs block))) id))
+                                    (string/includes? (string/lower-case (:block/title block)) title)))) result)))
+          (some? (first (common-initial-data/get-block-refs db (:db/id block)))))))))
 
 (def-thread-api :thread-api/get-block-parents
   [repo id depth]
@@ -551,7 +550,7 @@
 (def-thread-api :thread-api/release-access-handles
   [repo]
   (when-let [^js pool (worker-state/get-opfs-pool repo)]
-    (.releaseAccessHandles pool)
+    (.pauseVfs pool)
     nil))
 
 (def-thread-api :thread-api/db-exists
@@ -730,6 +729,42 @@
       (gc-sqlite-dbs! db client-ops conn {:full-gc? true})
       nil)))
 
+(def-thread-api :thread-api/vec-search-embedding-model-info
+  [repo]
+  (embedding/task--embedding-model-info repo))
+
+(def-thread-api :thread-api/vec-search-init-embedding-model
+  [repo]
+  (js/Promise. (embedding/task--init-embedding-model repo)))
+
+(def-thread-api :thread-api/vec-search-load-model
+  [repo model-name]
+  (js/Promise. (embedding/task--load-model repo model-name)))
+
+(def-thread-api :thread-api/vec-search-embedding-stale-blocks
+  [repo]
+  (embedding/embedding-stale-blocks! repo))
+
+(def-thread-api :thread-api/vec-search-re-embedding-graph-data
+  [repo]
+  (embedding/re-embedding-graph-data! repo))
+
+(def-thread-api :thread-api/vec-search-embedding-graph
+  [repo]
+  (embedding/embedding-graph! repo))
+
+(def-thread-api :thread-api/vec-search-search
+  [repo query-string nums-neighbors]
+  (embedding/task--search repo query-string nums-neighbors))
+
+(def-thread-api :thread-api/vec-search-cancel-indexing
+  [repo]
+  (embedding/cancel-indexing repo))
+
+(def-thread-api :thread-api/vec-search-update-index-info
+  [repo]
+  (js/Promise. (embedding/task--update-index-info! repo)))
+
 (comment
   (def-thread-api :general/dangerousRemoveAllDbs
     []
@@ -836,8 +871,9 @@
                                     ;; wait for service ready
                                     (js-invoke (:proxy service) k args)))
 
-                                (or (contains? #{:thread-api/sync-app-state} method-k)
-                                    (nil? service))
+                                (or
+                                 (contains? #{:thread-api/set-infer-worker-proxy :thread-api/sync-app-state} method-k)
+                                 (nil? service))
                                 ;; only proceed down this branch before shared-service is initialized
                                 (apply f args)
 

+ 295 - 0
src/main/frontend/worker/embedding.cljs

@@ -0,0 +1,295 @@
+(ns frontend.worker.embedding
+  "Fns about text-embedding, add/delete/search items in hnsw"
+  (:require [clojure.string :as string]
+            [datascript.core :as d]
+            [frontend.common.missionary :as c.m]
+            [frontend.worker-common.util :as worker-util]
+            [frontend.worker.state :as worker-state]
+            [lambdaisland.glogi :as log]
+            [logseq.common.config :as common-config]
+            [logseq.db :as ldb]
+            [logseq.db.frontend.content :as db-content]
+            [missionary.core :as m]))
+
+;;; TODOs:
+;;; - [x] add :logseq.property/description into text-to-embedding
+;;; - [x] add tags to text-to-embedding
+;;; - [x] check webgpu available, transformers.js is slow without webgpu(the difference is ~70 times)
+;;; - [x] expose index-state to ui
+
+(def ^:private empty-vector-search-state
+  {:repo->index-info {} ;; repo->index-info
+   :repo->canceler {}   ;; repo->canceler
+   })
+
+(def ^:private vector-search-state-keys (set (keys empty-vector-search-state)))
+
+(def ^:private *vector-search-state (atom empty-vector-search-state
+                                          :validator
+                                          (fn [v] (= vector-search-state-keys (set (keys v))))))
+
+(defn- reset-*vector-search-state!
+  [repo & {:keys [index-info canceler]}]
+  (reset! *vector-search-state
+          (cond-> @*vector-search-state
+            index-info (assoc :repo->index-info {repo index-info})
+            canceler   (assoc-in [:repo->canceler repo] canceler)))
+  nil)
+
+(defn cancel-indexing
+  [repo]
+  (when-let [canceler (get-in @*vector-search-state [:repo->canceler repo])]
+    (canceler)
+    (swap! *vector-search-state assoc-in [:repo->canceler repo] nil)
+    (swap! *vector-search-state assoc-in [:repo->index-info repo :indexing?] false)
+    nil))
+
+(defn- indexing?
+  [repo]
+  (get-in @*vector-search-state [:repo->index-info repo :indexing?]))
+
+(defn- stale-block-filter-preds
+  "When `reset?`, ignore :logseq.property.embedding/hnsw-label-updated-at in block"
+  [reset?]
+  (let [preds (cond->> (list (fn [b]
+                               (let [db-ident (:db/ident b)
+                                     title (:block/title b)]
+                                 (and (or (nil? db-ident)
+                                          (not (string/starts-with? (namespace db-ident) "logseq.")))
+                                      (not (string/blank? title))
+                                      (not (ldb/hidden? title))
+                                      (nil? (:logseq.property/view-for b))
+                                      (not (keyword-identical?
+                                            :logseq.property/description
+                                            (:db/ident (:logseq.property/created-from-property b))))))))
+
+                (not reset?)
+                (cons (fn [b]
+                        (let [block-updated-at (:block/updated-at b)
+                              hnsw-label-updated-at (:logseq.property.embedding/hnsw-label-updated-at b)]
+                          (or (nil? hnsw-label-updated-at)
+                              (> block-updated-at hnsw-label-updated-at))))))]
+    (apply every-pred preds)))
+
+(defn- stale-block-lazy-seq
+  [db reset?]
+  (->> (rseq (d/index-range db :block/updated-at nil nil))
+       (sequence
+        (comp (map #(d/entity db (:e %)))
+              (filter (stale-block-filter-preds reset?))
+              (map (fn [b]
+                     (assoc b :block.temp/text-to-embedding
+                            (db-content/recur-replace-uuid-in-block-title b)
+                            ;; FIXME: tags and properties can affect sorting
+                            ;; (str (db-content/recur-replace-uuid-in-block-title b)
+                            ;;      (let [tags (->> (:block/tags b)
+                            ;;                      (map :block/title))]
+                            ;;        (when (seq tags)
+                            ;;          (str " " (string/join ", " (map (fn [t] (str "#" t)) tags)))))
+                            ;;      (when-let [desc (:block/title (:logseq.property/description b))]
+                            ;;        (str "\nDescription: " desc)))
+                            )))))))
+(defn- partition-by-text-size
+  [text-size]
+  (let [*current-size (volatile! 0)
+        *partition-index (volatile! 0)]
+    (partition-by
+     (fn [block]
+       (let [block-text-size (count (:block.temp/text-to-embedding block))]
+         (vswap! *current-size + block-text-size)
+         (if (>= text-size @*current-size)
+           @*partition-index
+           (do (vreset! *current-size block-text-size)
+               (vswap! *partition-index inc))))))))
+
+(defn- labels-update-tx-data
+  [db e+updated-at-coll]
+  (let [es (map first e+updated-at-coll)
+        exist-es (set (keep
+                       (fn [b] (when (:block/uuid b) (:db/id b)))
+                       (d/pull-many db [:block/uuid :db/id] es)))]
+    (keep
+     (fn [[e updated-at]]
+       (when (contains? exist-es e)
+         [:db/add e :logseq.property.embedding/hnsw-label-updated-at updated-at]))
+     e+updated-at-coll)))
+
+(defn- task--update-index-info!*
+  ([repo ^js infer-worker]
+   (m/sp
+     (reset-*vector-search-state! repo :index-info
+                                  (merge (:index-info @*vector-search-state)
+                                         (js->clj (c.m/<? (.index-info infer-worker repo))
+                                                  :keywordize-keys true)))))
+  ([repo ^js infer-worker indexing?*]
+   (m/sp
+     (reset-*vector-search-state! repo :index-info
+                                  (assoc (js->clj (c.m/<? (.index-info infer-worker repo))
+                                                  :keywordize-keys true)
+                                         :indexing? indexing?*)))))
+
+(defn task--update-index-info!
+  [repo]
+  (m/sp
+    (when-let [^js infer-worker @worker-state/*infer-worker]
+      (m/? (task--update-index-info!* repo infer-worker)))))
+
+(defn- get-partition-size
+  [_repo]
+  500
+  ;; (let [conn (worker-state/get-datascript-conn repo)
+  ;;       embedding-model-name (ldb/get-key-value @conn :logseq.kv/graph-text-embedding-model-name)]
+  ;;   (case embedding-model-name
+  ;;     "onnx-community/Qwen3-Embedding-0.6B-ONNX"
+  ;;     100
+  ;;     500))
+  )
+
+(defn- task--embedding-stale-blocks!
+  "embedding outdated block-data
+  outdate rule: block/updated-at > :logseq.property.embedding/hnsw-label-updated-at"
+  [repo]
+  (m/sp
+    (when-let [^js infer-worker @worker-state/*infer-worker]
+      (when-let [conn (worker-state/get-datascript-conn repo)]
+        (let [stale-blocks (stale-block-lazy-seq @conn false)]
+          (when (seq stale-blocks)
+            (m/? (task--update-index-info!* repo infer-worker true))
+            (doseq [stale-block-chunk (sequence (partition-by-text-size (get-partition-size repo)) stale-blocks)]
+              (let [e+updated-at-coll (map (juxt :db/id :block/updated-at) stale-block-chunk)
+                    _ (when (some (fn [id] (> id 2147483647)) (map :db/id stale-block-chunk))
+                        (throw (ex-info "Wrong db/id" {:data (filter (fn [item] (> (:db/id item) 2147483647)) stale-block-chunk)})))
+                    _ (c.m/<?
+                       (.text-embedding+store!
+                        infer-worker
+                        repo
+                        (into-array (map :block.temp/text-to-embedding stale-block-chunk))
+                        (into-array (map :db/id stale-block-chunk))
+                        false))
+                    tx-data (labels-update-tx-data @conn e+updated-at-coll)]
+                (d/transact! conn tx-data {:skip-refresh? true})
+                (m/? (task--update-index-info!* repo infer-worker true))))
+            (c.m/<? (.write-index! infer-worker repo))
+            (m/? (task--update-index-info!* repo infer-worker false))))))))
+
+(defn- task--re-embedding-graph-data!
+  "force re-embedding all block-data in graph"
+  [repo]
+  (m/sp
+    (when-let [^js infer-worker @worker-state/*infer-worker]
+      (when-let [conn (worker-state/get-datascript-conn repo)]
+        (m/? (task--update-index-info!* repo infer-worker true))
+        (c.m/<? (.force-reset-index! infer-worker repo))
+        (let [all-blocks (stale-block-lazy-seq @conn true)]
+          (doseq [block-chunk (sequence (partition-by-text-size (get-partition-size repo)) all-blocks)]
+            (let [e+updated-at-coll (map (juxt :db/id :block/updated-at) block-chunk)
+                  _ (when (some (fn [id] (> id 2147483647)) (map :db/id block-chunk))
+                      (throw (ex-info "Wrong db/id" {:data (filter (fn [item] (> (:db/id item) 2147483647)) block-chunk)})))
+                  _ (c.m/<?
+                     (.text-embedding+store!
+                      infer-worker repo
+                      (into-array (map :block.temp/text-to-embedding block-chunk))
+                      (into-array (map :db/id block-chunk))
+                      false))
+                  tx-data (labels-update-tx-data @conn e+updated-at-coll)]
+              (d/transact! conn tx-data {:skip-refresh? true})
+              (m/? (task--update-index-info!* repo infer-worker true)))))
+        (c.m/<? (.write-index! infer-worker repo))
+        (m/? (task--update-index-info!* repo infer-worker false))))))
+
+(defn embedding-stale-blocks!
+  [repo]
+  (when-not (indexing? repo)
+    (let [canceler (c.m/run-task
+                     :embedding-stale-blocks!
+                     (task--embedding-stale-blocks! repo)
+                     :succ (constantly nil))]
+      (reset-*vector-search-state! repo :canceler canceler))))
+
+(defn re-embedding-graph-data!
+  [repo]
+  (when-not (indexing? repo)
+    (let [canceler (c.m/run-task
+                     :re-embedding-graph-data!
+                     (task--re-embedding-graph-data! repo)
+                     :succ (constantly nil))]
+      (reset-*vector-search-state! repo :canceler canceler))))
+
+(defn embedding-graph!
+  [repo]
+  (when-not (indexing? repo)
+    (when-let [conn (worker-state/get-datascript-conn repo)]
+      (when (ldb/get-key-value @conn :logseq.kv/graph-text-embedding-model-name)
+        (if (first (d/datoms @conn :avet :logseq.property.embedding/hnsw-label-updated-at)) ; embedding exists
+          (embedding-stale-blocks! repo)
+          (re-embedding-graph-data! repo))))))
+
+(defn task--embedding-model-info
+  [repo]
+  (m/sp
+    (when-let [^js infer-worker @worker-state/*infer-worker]
+      (let [available-model-names (c.m/<? (.available-embedding-models infer-worker))
+            conn (worker-state/get-datascript-conn repo)
+            embedding-model-name (ldb/get-key-value @conn :logseq.kv/graph-text-embedding-model-name)]
+        {:available-model-names available-model-names
+         :graph-text-embedding-model-name embedding-model-name}))))
+
+(defn task--init-embedding-model
+  [repo]
+  (m/sp
+    (when-let [^js infer-worker @worker-state/*infer-worker]
+      (let [conn (worker-state/get-datascript-conn repo)]
+        (if-let [embedding-model-name (ldb/get-key-value @conn :logseq.kv/graph-text-embedding-model-name)]
+          (c.m/<? (.load-model infer-worker embedding-model-name))
+          (log/info :init-load-model "model-name has not been set yet, skip"))))))
+
+(defn task--load-model
+  [repo model-name]
+  (m/sp
+    (when-let [^js infer-worker @worker-state/*infer-worker]
+      (let [conn (worker-state/get-datascript-conn repo)]
+        (when (c.m/<? (.load-model infer-worker model-name))
+          (d/transact! conn [(ldb/kv :logseq.kv/graph-text-embedding-model-name model-name)])
+          (log/info :loaded-model model-name))))))
+
+(defn task--search
+  [repo query-string nums-neighbors]
+  (m/sp
+    (when-not (indexing? repo)
+      (when-let [^js infer-worker @worker-state/*infer-worker]
+        (when-let [conn (worker-state/get-datascript-conn repo)]
+          (let [{:keys [distances neighbors]}
+                (worker-util/profile (str "search: '" query-string "'")
+                                     (js->clj (c.m/<? (.search infer-worker repo query-string nums-neighbors)) :keywordize-keys true))]
+            (->> (map vector distances neighbors)
+                 (keep (fn [[distance label]]
+                         ;; (prn :debug :semantic-search-result
+                         ;;      :block (:block/title (d/entity @conn label))
+                         ;;      :distance distance)
+                         (when-not (or (js/isNaN distance) (>= distance 0.6)
+                                       (> label 2147483647))
+                           (when-let [block (d/entity @conn label)]
+                             (when (:block/title block)
+                               {:block block
+                                :distance distance}))))))))))))
+
+(def ^:private vector-search-state-flow
+  (m/eduction
+   (map (fn [m] (dissoc m :repo->canceler)))
+   (c.m/throttle 300 (m/watch *vector-search-state))))
+
+(when-not common-config/PUBLISHING ; NOTE: we may support vector-search in publishing mode later
+  (c.m/run-background-task
+   ::subscribe-state
+   (m/reduce
+    (fn [_ m] (worker-util/post-message :vector-search-sync-state m))
+    vector-search-state-flow)))
+
+(comment
+  (def repo (frontend.worker.state/get-current-repo))
+  (def conn (frontend.worker.state/get-datascript-conn (frontend.worker.state/get-current-repo)))
+  (.force-reset-index! @worker-state/*infer-worker repo)
+  ((task--embedding-stale-blocks! repo) prn js/console.log)
+  ((task--re-embedding-graph-data! repo) prn js/console.log)
+
+  ((task--search repo "perf performance datomic stat" 10) prn js/console.log))

+ 1 - 1
src/main/frontend/worker/file.cljs

@@ -9,8 +9,8 @@
             [frontend.common.async-util :as async-util]
             [frontend.common.file.core :as common-file]
             [frontend.common.file.util :as wfu]
+            [frontend.worker-common.util :as worker-util]
             [frontend.worker.state :as worker-state]
-            [frontend.worker.util :as worker-util]
             [goog.object :as gobj]
             [lambdaisland.glogi :as log]
             [logseq.common.date :as common-date]

+ 1 - 1
src/main/frontend/worker/file/reset.cljs

@@ -2,8 +2,8 @@
   "Fns for resetting a db file with parsed file content"
   (:require [clojure.string :as string]
             [datascript.core :as d]
+            [frontend.worker-common.util :as worker-util]
             [frontend.worker.state :as worker-state]
-            [frontend.worker.util :as worker-util]
             [logseq.common.config :as common-config]
             [logseq.common.util :as common-util]
             [logseq.db :as ldb]

+ 1 - 1
src/main/frontend/worker/pipeline.cljs

@@ -2,12 +2,12 @@
   "Pipeline work after transaction"
   (:require [clojure.string :as string]
             [datascript.core :as d]
+            [frontend.worker-common.util :as worker-util]
             [frontend.worker.commands :as commands]
             [frontend.worker.file :as file]
             [frontend.worker.react :as worker-react]
             [frontend.worker.shared-service :as shared-service]
             [frontend.worker.state :as worker-state]
-            [frontend.worker.util :as worker-util]
             [logseq.common.defkeywords :refer [defkeywords]]
             [logseq.common.util :as common-util]
             [logseq.common.uuid :as common-uuid]

+ 4 - 3
src/main/frontend/worker/rtc/core.cljs

@@ -4,6 +4,7 @@
             [datascript.core :as d]
             [frontend.common.missionary :as c.m]
             [frontend.common.thread-api :refer [def-thread-api]]
+            [frontend.worker-common.util :as worker-util]
             [frontend.worker.device :as worker-device]
             [frontend.worker.rtc.asset :as r.asset]
             [frontend.worker.rtc.branch-graph :as r.branch-graph]
@@ -20,7 +21,6 @@
             [frontend.worker.rtc.ws-util :as ws-util :refer [gen-get-ws-create-map--memoized]]
             [frontend.worker.shared-service :as shared-service]
             [frontend.worker.state :as worker-state]
-            [frontend.worker.util :as worker-util]
             [lambdaisland.glogi :as log]
             [logseq.common.config :as common-config]
             [logseq.db :as ldb]
@@ -304,10 +304,11 @@
    :canceler nil
    :*last-stop-exception nil})
 
+(def ^:private rtc-loop-metadata-keys (set (keys empty-rtc-loop-metadata)))
+
 (defonce ^:private *rtc-loop-metadata (atom empty-rtc-loop-metadata
                                             :validator
-                                            (fn [v] (= (set (keys empty-rtc-loop-metadata))
-                                                       (set (keys v))))))
+                                            (fn [v] (= rtc-loop-metadata-keys (set (keys v))))))
 
 (defn- validate-rtc-start-conditions
   "Return exception if validation failed"

+ 1 - 1
src/main/frontend/worker/rtc/full_upload_download_graph.cljs

@@ -6,6 +6,7 @@
             [datascript.core :as d]
             [frontend.common.missionary :as c.m]
             [frontend.common.thread-api :as thread-api]
+            [frontend.worker-common.util :as worker-util]
             [frontend.worker.crypt :as crypt]
             [frontend.worker.db-listener :as db-listener]
             [frontend.worker.db-metadata :as worker-db-metadata]
@@ -16,7 +17,6 @@
             [frontend.worker.rtc.ws-util :as ws-util]
             [frontend.worker.shared-service :as shared-service]
             [frontend.worker.state :as worker-state]
-            [frontend.worker.util :as worker-util]
             [logseq.db :as ldb]
             [logseq.db.frontend.malli-schema :as db-malli-schema]
             [logseq.db.frontend.schema :as db-schema]

+ 1 - 1
src/main/frontend/worker/rtc/remote_update.cljs

@@ -4,6 +4,7 @@
             [clojure.set :as set]
             [clojure.string :as string]
             [datascript.core :as d]
+            [frontend.worker-common.util :as worker-util]
             [frontend.worker.handler.page :as worker-page]
             [frontend.worker.rtc.asset :as r.asset]
             [frontend.worker.rtc.client-op :as client-op]
@@ -11,7 +12,6 @@
             [frontend.worker.rtc.log-and-state :as rtc-log-and-state]
             [frontend.worker.rtc.malli-schema :as rtc-schema]
             [frontend.worker.state :as worker-state]
-            [frontend.worker.util :as worker-util]
             [lambdaisland.glogi :as log]
             [logseq.clj-fractional-indexing :as index]
             [logseq.common.defkeywords :refer [defkeywords]]

+ 1 - 1
src/main/frontend/worker/rtc/ws_util.cljs

@@ -1,12 +1,12 @@
 (ns frontend.worker.rtc.ws-util
   "Add RTC related logic to the function based on ws."
   (:require [cljs-http-missionary.client :as http]
+            [frontend.worker-common.util :as worker-util]
             [frontend.worker.rtc.db :as rtc-db]
             [frontend.worker.rtc.exception :as r.ex]
             [frontend.worker.rtc.malli-schema :as rtc-schema]
             [frontend.worker.rtc.ws :as ws]
             [frontend.worker.state :as worker-state]
-            [frontend.worker.util :as worker-util]
             [goog.string :as gstring]
             [logseq.graph-parser.utf8 :as utf8]
             [missionary.core :as m]))

+ 146 - 66
src/main/frontend/worker/search.cljs

@@ -1,11 +1,12 @@
 (ns frontend.worker.search
   "Full-text and fuzzy search"
-  (:require ["fuse.js" :as fuse]
+  (:require ["fuse.js" :as Fuse]
             [cljs-bean.core :as bean]
             [clojure.set :as set]
             [clojure.string :as string]
             [datascript.core :as d]
             [frontend.common.search-fuzzy :as fuzzy]
+            [frontend.worker.embedding :as embedding]
             [goog.object :as gobj]
             [logseq.common.config :as common-config]
             [logseq.common.util :as common-util]
@@ -13,12 +14,34 @@
             [logseq.db :as ldb]
             [logseq.db.frontend.content :as db-content]
             [logseq.db.sqlite.util :as sqlite-util]
-            [logseq.graph-parser.text :as text]))
+            [logseq.graph-parser.text :as text]
+            [missionary.core :as m]))
+
+(def fuse (aget Fuse "default"))
 
 ;; TODO: use sqlite for fuzzy search
 ;; maybe https://github.com/nalgeon/sqlean/blob/main/docs/fuzzy.md?
 (defonce fuzzy-search-indices (atom {}))
 
+;; Configuration for re-ranking
+(def config
+  {:keyword-weight 0.9
+   :semantic-weight 0.1})
+
+(defn- log-score
+  [score]
+  (if (> score 2)
+    (js/Math.log score)
+    score))
+
+;; Normalize scores to [0, 1] range using min-max normalization
+(defn normalize-score [score min-score max-score]
+  (if (= min-score max-score)
+    0.0
+    (let [normalized (/ (log-score (- score min-score))
+                        (log-score (- max-score min-score)))]
+      (max 0.0 (min 1.0 normalized)))))
+
 (defn- add-blocks-fts-triggers!
   "Table bindings of blocks tables and the blocks FTS virtual tables"
   [db]
@@ -168,14 +191,16 @@ DROP TRIGGER IF EXISTS blocks_au;
                              :bind bind
                              :rowMode "array"}))
           blocks (bean/->clj result)]
-      (map (fn [block]
-             (let [[id page title snippet] (if enable-snippet?
-                                             (update block 3 get-snippet-result)
-                                             block)]
-               {:id id
-                :page page
-                :title title
-                :snippet snippet})) blocks))
+      (keep (fn [block]
+              (let [[id page title rank snippet] (if enable-snippet?
+                                                   (update block 4 get-snippet-result)
+                                                   block)]
+                (when title
+                  {:id id
+                   :keyword-score (+ (fuzzy/score q title) (js/Math.abs rank))
+                   :page page
+                   :title title
+                   :snippet snippet}))) blocks))
     (catch :default e
       (prn :debug "Search blocks failed: ")
       (js/console.error e))))
@@ -281,6 +306,41 @@ DROP TRIGGER IF EXISTS blocks_au;
                (filter (fn [{:keys [title]}]
                          (exact-matched? q title)))))))))
 
+;; Combine and re-rank results
+(defn combine-results
+  [db keyword-results semantic-results]
+  (let [;; Extract score ranges for normalization
+        keyword-scores (map :keyword-score keyword-results)
+        k-min (if (seq keyword-scores) (apply min keyword-scores) 0.0)
+        k-max (if (seq keyword-scores) (apply max keyword-scores) 1.0)
+        all-ids (set/union (set (map :id keyword-results))
+                           (set (map :id semantic-results)))
+        merged (map (fn [id]
+                      (let [block (when id (d/entity db [:block/uuid (uuid id)]))
+                            k-result (first (filter #(= (:id %) id) keyword-results))
+                            s-result (first (filter #(= (:id %) id) semantic-results))
+                            result (merge s-result k-result)
+                            k-score (or (:keyword-score k-result) 0.0)
+                            s-score (or (:semantic-score s-result) 0.0)
+                            norm-k-score (normalize-score k-score k-min k-max)
+                            ;; Weighted combination
+                            combined-score (+ (* (:keyword-weight config) norm-k-score)
+                                              (* (:semantic-weight config) s-score)
+                                              (cond
+                                                (ldb/page? block)
+                                                0.02
+                                                (:block/tags block)
+                                                0.01
+                                                :else
+                                                0))]
+                        (merge result
+                               {:combined-score combined-score
+                                :keyword-score k-score
+                                :semantic-score s-score})))
+                    all-ids)
+        sorted-result (sort-by :combined-score #(compare %2 %1) merged)]
+    sorted-result))
+
 (defn search-blocks
   "Options:
    * :page - the page to specifically search on
@@ -290,66 +350,86 @@ DROP TRIGGER IF EXISTS blocks_au;
   [repo conn search-db q {:keys [limit page enable-snippet? built-in? dev? page-only? library-page-search?]
                           :as option
                           :or {enable-snippet? true}}]
-  (when-not (string/blank? q)
-    (let [match-input (get-match-input q)
-          page-count (count (d/datoms @conn :avet :block/name))
-          large-graph? (> page-count 2500)
-          non-match-input (when (<= (count q) 2)
-                            (str "%" (string/replace q #"\s+" "%") "%"))
-          limit  (or limit 100)
+  (m/sp
+    (when-not (string/blank? q)
+      (let [match-input (get-match-input q)
+            page-count (count (d/datoms @conn :avet :block/name))
+            large-graph? (> page-count 2500)
+            non-match-input (when (<= (count q) 2)
+                              (str "%" (string/replace q #"\s+" "%") "%"))
+            limit  (or limit 100)
             ;; https://www.sqlite.org/fts5.html#the_highlight_function
             ;; 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$', '...', 256)"
-          select (if enable-snippet?
-                   (str "select id, page, title, " snippet-aux " from blocks_fts where ")
-                   "select id, page, title from blocks_fts where ")
-          pg-sql (if page "page = ? and" "")
-          match-sql (if (ns-util/namespace-page? q)
-                      (str select pg-sql " title match ? or title match ? order by rank limit ?")
-                      (str select pg-sql " title match ? order by rank limit ?"))
-          non-match-sql (str select pg-sql " title like ? limit ?")
-          matched-result (when-not page-only?
-                           (search-blocks-aux search-db match-sql q match-input page limit enable-snippet?))
-          non-match-result (when (and (not page-only?) non-match-input)
-                             (search-blocks-aux search-db non-match-sql q non-match-input page limit enable-snippet?))
-           ;; fuzzy is too slow for large graphs
-          fuzzy-result (when-not (or page large-graph?) (fuzzy-search repo @conn q option))
-          result (->> (concat fuzzy-result matched-result non-match-result)
-                      (common-util/distinct-by :id)
-                      (keep (fn [result]
-                              (let [{:keys [id page title snippet]} result
-                                    block-id (uuid id)]
-                                (when-let [block (d/entity @conn [:block/uuid block-id])]
-                                  (when-not (and library-page-search?
-                                                 (or (:block/parent block)
-                                                     (not (ldb/internal-page? block)))) ; remove pages that already have parents
-                                    (when (if dev?
-                                            true
-                                            (if built-in?
-                                              (or (not (ldb/built-in? block))
-                                                  (not (ldb/private-built-in-page? block))
-                                                  (ldb/class? block))
-                                              (or (not (ldb/built-in? block))
-                                                  (ldb/class? block))))
-                                      {:db/id (:db/id block)
-                                       :block/uuid block-id
-                                       :block/title (if (ldb/page? block)
-                                                      (ldb/get-title-with-parents block)
-                                                      (or snippet title))
-                                       :block/page (if (common-util/uuid-string? page)
-                                                     (uuid page)
-                                                     nil)
-                                       :block/tags (seq (map :db/id (:block/tags block)))
-                                       :page? (ldb/page? block)
-                                       :alias (some-> (first (:block/_alias block))
-                                                      (select-keys [:block/uuid :block/title]))})))))))
-          page-or-object-result (filter (fn [b] (or (:page? b) (:block/tags result))) result)]
-      (->>
-       (concat page-or-object-result
-               (remove (fn [b] (or (:page? b) (:block/tags result))) result))
-       (common-util/distinct-by :block/uuid)))))
+            snippet-aux "snippet(blocks_fts, 1, '$pfts_2lqh>$', '$<pfts_2lqh$', '...', 256)"
+            select (if enable-snippet?
+                     (str "select id, page, title, rank, " snippet-aux " from blocks_fts where ")
+                     "select id, page, title, rank from blocks_fts where ")
+            pg-sql (if page "page = ? and" "")
+            match-sql (if (ns-util/namespace-page? q)
+                        (str select pg-sql " title match ? or title match ? order by rank limit ?")
+                        (str select pg-sql " title match ? order by rank limit ?"))
+            non-match-sql (str select pg-sql " title like ? limit ?")
+            matched-result (when-not page-only?
+                             (search-blocks-aux search-db match-sql q match-input page limit enable-snippet?))
+            non-match-result (when (and (not page-only?) non-match-input)
+                               (->> (search-blocks-aux search-db non-match-sql q non-match-input page limit enable-snippet?)
+                                    (map (fn [result]
+                                           (assoc result :keyword-score (fuzzy/score q (:title result)))))))
+            ;; fuzzy is too slow for large graphs
+            fuzzy-result (when-not (or page large-graph?)
+                           (->> (fuzzy-search repo @conn q option)
+                                (map (fn [result]
+                                       (assoc result :keyword-score (fuzzy/score q (:title result)))))))
+            semantic-search-result* (m/? (embedding/task--search repo q 10))
+            semantic-search-result (->> semantic-search-result*
+                                        (map (fn [{:keys [block distance]}]
+                                               (let [page-id (when-let [id (:block/uuid (:block/page block))] (str id))]
+                                                 (cond->
+                                                  {:id (str (:block/uuid block))
+                                                   :title (:block/title block)
+                                                   :semantic-score (/ 1.0 (+ 1.0 distance))}
+                                                   page-id
+                                                   (assoc :page page-id))))))
+            ;; _ (doseq [item (concat fuzzy-result matched-result)]
+            ;;     (prn :debug :keyword-search-result item))
+            ;; _ (doseq [item semantic-search-result]
+            ;;     (prn :debug :semantic-search-item item))
+            combined-result (combine-results @conn (concat fuzzy-result matched-result non-match-result) semantic-search-result)
+            result (->> combined-result
+                        (common-util/distinct-by :id)
+                        (keep (fn [result]
+                                (let [{:keys [id page title snippet]} result
+                                      block-id (uuid id)]
+                                  (when-let [block (d/entity @conn [:block/uuid block-id])]
+                                    (when-not (and library-page-search?
+                                                   (or (:block/parent block)
+                                                       (not (ldb/internal-page? block)))) ; remove pages that already have parents
+                                      (when (if dev?
+                                              true
+                                              (if built-in?
+                                                (or (not (ldb/built-in? block))
+                                                    (not (ldb/private-built-in-page? block))
+                                                    (ldb/class? block))
+                                                (or (not (ldb/built-in? block))
+                                                    (ldb/class? block))))
+                                        {:db/id (:db/id block)
+                                         :block/uuid (:block/uuid block)
+                                         :block/title (if (ldb/page? block)
+                                                        (ldb/get-title-with-parents block)
+                                                        (or snippet title))
+                                         :block/page (or
+                                                      (:block/uuid (:block/page block))
+                                                      (when page
+                                                        (if (common-util/uuid-string? page)
+                                                          (uuid page)
+                                                          nil)))
+                                         :block/tags (seq (map :db/id (:block/tags block)))
+                                         :page? (ldb/page? block)
+                                         :alias (some-> (first (:block/_alias block))
+                                                        (select-keys [:block/uuid :block/title]))})))))))]
+        (common-util/distinct-by :block/uuid result)))))
 
 (defn truncate-table!
   [db]

+ 1 - 0
src/main/frontend/worker/state.cljs

@@ -4,6 +4,7 @@
             [logseq.common.util :as common-util]))
 
 (defonce *main-thread (atom nil))
+(defonce *infer-worker (atom nil))
 
 (defn- <invoke-main-thread*
   [qkw direct-pass? args-list]

+ 2 - 2
src/main/frontend/worker/util.cljc → src/main/frontend/worker_common/util.cljc

@@ -1,6 +1,6 @@
-(ns frontend.worker.util
+(ns frontend.worker-common.util
   "Worker utils"
-  #?(:cljs (:require-macros [frontend.worker.util]))
+  #?(:cljs (:require-macros [frontend.worker-common.util]))
   #?(:cljs (:refer-clojure :exclude [format]))
   #?(:cljs (:require [clojure.string :as string]
                      [goog.crypt :as crypt]

+ 2 - 0
src/resources/dicts/en.edn

@@ -213,6 +213,8 @@
  :context-menu/input-template-name "What's the template's name?"
  :context-menu/template-include-parent-block "Including the parent block in the template?"
  :context-menu/template-exists-warning "Template already exists!"
+ :settings-page/ai "AI"
+ :settings-page/tab-ai "AI"
  :settings-page/git-tip "If you have Logseq Sync enabled, you can view a page's edit history directly. This section is for tech-savvy only."
  :settings-page/git-desc-1 "To view page's edit history, click the three horizontal dots in the top-right corner and select \"View page history\"."
  :settings-page/git-desc-2 "For professional users, Logseq also supports using "

+ 61 - 0
webpack.config.js

@@ -0,0 +1,61 @@
+const path = require('path');
+const webpack = require('webpack');
+
+var config = {
+  mode: "development",
+  externals: {
+    'react': 'React',
+    'react-dom': 'ReactDOM',
+  },
+  module: {
+    rules: [
+      {
+        // docs: https://webpack.js.org/configuration/module/#resolvefullyspecified
+        test: /\.m?js/,
+        resolve: {
+          fullySpecified: false,
+        }
+      }
+    ]
+  },
+  plugins: [
+    // fix "process is not defined" error:
+    new webpack.ProvidePlugin({
+      process: 'process/browser',
+    }),
+  ],
+};
+
+var AppConfig = Object.assign({}, config, {
+  name: "app",
+  entry: {
+    main : "./target/main.js",
+    workers : "./target/workers.js",
+  },
+
+  output: {
+    path: path.resolve(__dirname, 'static/js'),
+    filename: '[name]-bundle.js',
+    clean: false,
+    chunkLoading: false,
+  },
+});
+
+var MobileConfig = Object.assign({}, config, {
+  name: "mobile",
+  entry: {
+    main : "./target/mobile.js",
+    workers : "./target/workers.js",
+  },
+
+  output: {
+    path: path.resolve(__dirname, 'static/mobile/js'),
+    filename: '[name]-bundle.js',
+    clean: false,
+    chunkLoading: false,
+  },
+});
+
+module.exports = [
+  AppConfig, MobileConfig,
+];

Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác