ocean-brain 0.8.2 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/mcp.js +47 -75
- package/package.json +3 -2
- package/server/client/dist/assets/{ArrowRight.es-g2YZTklZ.js → ArrowRight.es-Bm-ZXpEC.js} +1 -1
- package/server/client/dist/assets/Calendar-BsNs3NzO.js +25 -0
- package/server/client/dist/assets/{Callout-B_IPSF_5.js → Callout-CviyPHKO.js} +1 -1
- package/server/client/dist/assets/{Copy.es-CNxe4ujr.js → Copy.es-BPreRnP9.js} +1 -1
- package/server/client/dist/assets/{Graph-D9GeO71k.js → Graph-vXQQ0a-Q.js} +1 -1
- package/server/client/dist/assets/Graph-yPE3ljW0.js +46 -0
- package/server/client/dist/assets/{Image.es-DnON8cv2.js → Image.es-DnR3qc_c.js} +1 -1
- package/server/client/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/server/client/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/server/client/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/server/client/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/server/client/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/server/client/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/server/client/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/server/client/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/server/client/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/server/client/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/server/client/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/server/client/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/server/client/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/server/client/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/server/client/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/server/client/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/server/client/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/server/client/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/server/client/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/server/client/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/server/client/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/server/client/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/server/client/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/server/client/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/server/client/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/server/client/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/server/client/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/server/client/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/server/client/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/server/client/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/server/client/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/server/client/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/server/client/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/server/client/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/server/client/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/server/client/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/server/client/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/server/client/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/server/client/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/server/client/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/server/client/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/server/client/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/server/client/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/server/client/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/server/client/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/server/client/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/server/client/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/server/client/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/server/client/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/server/client/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/server/client/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/server/client/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/server/client/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/server/client/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/server/client/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/server/client/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/server/client/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/server/client/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/server/client/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/server/client/dist/assets/Note-CmreVzvQ.js +25 -0
- package/server/client/dist/assets/{NotePropertiesPanel-2DgXEM4B.js → NotePropertiesPanel-DXYgE8h2.js} +2 -2
- package/server/client/dist/assets/PencilLine.es-B9V6u64t.js +1 -0
- package/server/client/dist/assets/{Plus.es-CQ8wn2is.js → Plus.es-CRRp2u5J.js} +1 -1
- package/server/client/dist/assets/Reminders-BOtfNeYD.js +1 -0
- package/server/client/dist/assets/Search-C-FB3pKf.js +1 -0
- package/server/client/dist/assets/SurfaceCard-GMt5xM7v.js +1 -0
- package/server/client/dist/assets/Tag-lBxDRE77.js +1 -0
- package/server/client/dist/assets/TagNotes-Bgorp_rC.js +1 -0
- package/server/client/dist/assets/{Trash.es-0aPpA-3S.js → Trash.es-svHOb5LN.js} +1 -1
- package/server/client/dist/assets/ViewNotes-C7d10VFf.js +1 -0
- package/server/client/dist/assets/ViewSectionDialog-Bhw-ILop.js +102 -0
- package/server/client/dist/assets/Views-C7V3saUo.js +1 -0
- package/server/client/dist/assets/abnfDiagram-VRR7QNED-CWlV5dXw.js +1 -0
- package/server/client/dist/assets/app-DQBV36r_.js +50 -0
- package/server/client/dist/assets/app-ZgwR5t_I.css +1 -0
- package/server/client/dist/assets/arc-BV-n6RUX.js +1 -0
- package/server/client/dist/assets/architectureDiagram-ZJ3FMSHR-CekzMXdu.js +36 -0
- package/server/client/dist/assets/blockDiagram-677ZJIJ3-B49yHoxq.js +132 -0
- package/server/client/dist/assets/c4Diagram-LMCZKHZV-0AfB9NBC.js +10 -0
- package/server/client/dist/assets/channel-CigOulR-.js +1 -0
- package/server/client/dist/assets/chunk-2Q5K7J3B-DaPQAU1q.js +1 -0
- package/server/client/dist/assets/chunk-32BRIVSS-CfL3kZAg.js +1 -0
- package/server/client/dist/assets/chunk-5VM5RSS4-DeOwWPE_.js +15 -0
- package/server/client/dist/assets/chunk-EX3LRPZG-C3W1oc9y.js +231 -0
- package/server/client/dist/assets/chunk-JWPE2WC7-_Oqh79OY.js +1 -0
- package/server/client/dist/assets/chunk-MOJQB5TN-DDKNNzI6.js +88 -0
- package/server/client/dist/assets/chunk-RYQCIY6F-DCHah7CC.js +1 -0
- package/server/client/dist/assets/chunk-V7JOEXUC-uUOIzgxj.js +206 -0
- package/server/client/dist/assets/chunk-VR4S4FIN-DDVw2WPs.js +1 -0
- package/server/client/dist/assets/chunk-XXDRQBXY-Coo4UFm2.js +1 -0
- package/server/client/dist/assets/classDiagram-OUVF2IWQ-D13ISIOW.js +1 -0
- package/server/client/dist/assets/classDiagram-v2-EOCWNBFH-D13ISIOW.js +1 -0
- package/server/client/dist/assets/colors-Cc3OSVma.js +1 -0
- package/server/client/dist/assets/cose-bilkent-JH36ORCC-C3pHmHpD.js +1 -0
- package/server/client/dist/assets/cynefin-VYW2F7L2-DU_G4Atc.js +166 -0
- package/server/client/dist/assets/cynefinDiagram-TSTJHNR4-DZ6iiJFo.js +62 -0
- package/server/client/dist/assets/cytoscape.esm-D3_iZ_3b.js +321 -0
- package/server/client/dist/assets/dagre-VKFMJZFB-DNELbhwC.js +4 -0
- package/server/client/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/server/client/dist/assets/diagram-FQU43EPY-CMEvZJ61.js +3 -0
- package/server/client/dist/assets/diagram-G47NLZAW-BYDtf6j9.js +24 -0
- package/server/client/dist/assets/diagram-NH7WQ7WH-CmXfcCQA.js +24 -0
- package/server/client/dist/assets/diagram-OA4YK3LP-BflTKJxU.js +30 -0
- package/server/client/dist/assets/diagram-WEI45ONY-KW3fKqna.js +41 -0
- package/server/client/dist/assets/ebnfDiagram-CCIWWBDH-CbyhY5Nj.js +1 -0
- package/server/client/dist/assets/erDiagram-Q63AITRT-CC2eZV8g.js +85 -0
- package/server/client/dist/assets/{floating-ui.react-dom-wWLZ1CdM.js → floating-ui.react-dom-BWTnA00G.js} +1 -1
- package/server/client/dist/assets/flowDiagram-23GEKE2U-unplo7Qm.js +156 -0
- package/server/client/dist/assets/ganttDiagram-NO4QXBWP-BcBK5HcZ.js +292 -0
- package/server/client/dist/assets/gitGraphDiagram-IHSO6WYX-DgVbbb5k.js +106 -0
- package/server/client/dist/assets/graph-C5DFNLrC.js +1 -0
- package/server/client/dist/assets/image-delete-confirmation-D7xrODQV.js +1 -0
- package/server/client/dist/assets/image.api-QmHYjp3z.js +17 -0
- package/server/client/dist/assets/index-CaF9d-zG.js +1 -0
- package/server/client/dist/assets/infoDiagram-FWYZ7A6U-N_D9RdfG.js +2 -0
- package/server/client/dist/assets/init-Gi6I4Gst.js +1 -0
- package/server/client/dist/assets/isObject-U9U2HXiq.js +1 -0
- package/server/client/dist/assets/ishikawaDiagram-FXEZZL3T-BBKrzmB2.js +70 -0
- package/server/client/dist/assets/journeyDiagram-5HDEW3XC-NdCxLmQB.js +139 -0
- package/server/client/dist/assets/kanban-definition-HUTT4EX6-Ck7t4-tt.js +89 -0
- package/server/client/dist/assets/katex-DywYRkvg.css +1 -0
- package/server/client/dist/assets/katex-HP8lGamR.js +257 -0
- package/server/client/dist/assets/katex-x288kqHd.js +257 -0
- package/server/client/dist/assets/layout-Cz0MDlGi.js +1 -0
- package/server/client/dist/assets/linear-D6yRgDLq.js +1 -0
- package/server/client/dist/assets/manage-image-DCDF8AM9.js +1 -0
- package/server/client/dist/assets/manage-image-detail-e0ViRAj-.js +1 -0
- package/server/client/dist/assets/map-Mm1V-_Na.js +1 -0
- package/server/client/dist/assets/mcp-admin-adapter-D39G3udJ.js +1 -0
- package/server/client/dist/assets/{mcp-CT_9IV2c.js → mcp-uFZkdhCi.js} +4 -4
- package/server/client/dist/assets/mermaid.core-B14ZAmWA.js +308 -0
- package/server/client/dist/assets/min-tv6RD-6E.js +1 -0
- package/server/client/dist/assets/mindmap-definition-LN4V7U3C-BTUqSdcP.js +96 -0
- package/server/client/dist/assets/{note-core-Buwzyxi7.js → note-core-DUTqUh5N.js} +2 -2
- package/server/client/dist/assets/{note-runtime-CPDVOFEo.js → note-runtime-zN5ddTba.js} +1 -1
- package/server/client/dist/assets/{note-ui-CHI-tzj-.js → note-ui-CxK8ymUc.js} +1 -1
- package/server/client/dist/assets/{note.api-D_00ZzJC.js → note.api-BQovzEzd.js} +6 -6
- package/server/client/dist/assets/ordinal-Cboi1Yqb.js +1 -0
- package/server/client/dist/assets/pegDiagram-2B236MQR-Cjcu3mVz.js +1 -0
- package/server/client/dist/assets/pieDiagram-ENE6RG2P-BxI_c22Z.js +39 -0
- package/server/client/dist/assets/{placeholder-DbdGiYGL.js → placeholder-D3b8lZGE.js} +4 -4
- package/server/client/dist/assets/properties-M-yWq1D7.js +1 -0
- package/server/client/dist/assets/quadrantDiagram-ABIIQ3AL-DGm0a-yR.js +7 -0
- package/server/client/dist/assets/railroadDiagram-RFXS5EU6-DmCDu89O.js +1 -0
- package/server/client/dist/assets/requirementDiagram-TGXJPOKE-CBpUt7Ig.js +84 -0
- package/server/client/dist/assets/route-preload.js +2 -2
- package/server/client/dist/assets/sankeyDiagram-HTMAVEWB-BjGnDZlp.js +40 -0
- package/server/client/dist/assets/search-DFnzZKLd.js +1 -0
- package/server/client/dist/assets/sequenceDiagram-DBY2YBRQ-B7rpCfS6.js +162 -0
- package/server/client/dist/assets/sizeCapture-X5ZJPWSS-CUJhD0uW.js +1 -0
- package/server/client/dist/assets/stateDiagram-2N3HPSRC-Y9-3XqRX.js +1 -0
- package/server/client/dist/assets/stateDiagram-v2-6OUMAXLB-DbHXhT02.js +1 -0
- package/server/client/dist/assets/swimlanes-5IMT3BWC-Cs1NO9ng.js +2 -0
- package/server/client/dist/assets/swimlanesDiagram-G3AALYLV-DQnrq2ZC.js +8 -0
- package/server/client/dist/assets/{tag.api-CX0SE06J.js → tag.api-DUfjr8bg.js} +3 -3
- package/server/client/dist/assets/time-CfAYZipR.js +9 -0
- package/server/client/dist/assets/timeline-definition-FHXFAJF6-8fqD4WNL.js +120 -0
- package/server/client/dist/assets/toNumber-DE3J3kMD.js +1 -0
- package/server/client/dist/assets/transform-BUB79_za.js +1 -0
- package/server/client/dist/assets/trash-CClKfP1x.js +1 -0
- package/server/client/dist/assets/url-9nrbQs7Z.js +1 -0
- package/server/client/dist/assets/{useReminderMutate-CLK1tHvT.js → useReminderMutate--omDLmBV.js} +4 -4
- package/server/client/dist/assets/vennDiagram-L72KCM5P-BPqKkd5a.js +34 -0
- package/server/client/dist/assets/wardleyDiagram-EHGQE667-Cq11kU2M.js +78 -0
- package/server/client/dist/assets/xychartDiagram-FW5EYKEG-DJ6UzFj-.js +7 -0
- package/server/client/dist/index.html +7 -7
- package/server/dist/features/image/services/delete.js +25 -1
- package/server/dist/features/note/graphql/note.mutation.resolver.js +38 -12
- package/server/dist/features/note/graphql/note.type-defs.js +1 -1
- package/server/dist/features/note/http/mcp.js +1 -60
- package/server/dist/features/note/services/authoring.js +2 -70
- package/server/dist/features/note/services/markdown-patch.js +80 -8
- package/server/dist/features/note/services/properties.js +21 -1
- package/server/dist/features/note/services/property-restore.js +26 -0
- package/server/dist/features/note/services/snapshot.js +79 -59
- package/server/dist/features/note/services/trash.js +16 -31
- package/server/dist/features/search/embedding-api-key-store.js +57 -0
- package/server/dist/features/search/embedding-client.js +179 -0
- package/server/dist/features/search/graphql/index.js +6 -0
- package/server/dist/features/search/graphql/resolver.js +20 -0
- package/server/dist/features/search/graphql/type-defs.js +30 -0
- package/server/dist/features/search/http/handlers.js +108 -0
- package/server/dist/features/search/hybrid-ranking.js +55 -0
- package/server/dist/features/search/hybrid-search.js +174 -0
- package/server/dist/features/search/note-change.js +16 -0
- package/server/dist/features/search/note-chunking.js +75 -0
- package/server/dist/features/search/search-config.js +150 -0
- package/server/dist/features/search/search-manager.js +431 -0
- package/server/dist/features/search/semantic-indexer.js +138 -0
- package/server/dist/features/search/sqlite-vector-index.js +636 -0
- package/server/dist/features/search/url-normalization.js +10 -0
- package/server/dist/features/tag/services/organization.js +3 -0
- package/server/dist/features/view/services/workspace.js +1 -1
- package/server/dist/main.js +2 -0
- package/server/dist/modules/blocknote.js +4 -5
- package/server/dist/paths.js +7 -2
- package/server/dist/routes/api.js +37 -0
- package/server/dist/routes/mcp.js +0 -5
- package/server/dist/schema/index.js +3 -0
- package/server/client/dist/assets/Calendar-BP6HTuSn.js +0 -25
- package/server/client/dist/assets/Graph-Xha3fwhp.js +0 -46
- package/server/client/dist/assets/Image-BLEjLUqd.js +0 -1
- package/server/client/dist/assets/Note-DjrCYtat.js +0 -24
- package/server/client/dist/assets/Reminders-B4l3KdlK.js +0 -1
- package/server/client/dist/assets/Search-BN5sf7bE.js +0 -1
- package/server/client/dist/assets/SurfaceCard-BZm73nyS.js +0 -1
- package/server/client/dist/assets/Tag-oSnmViVp.js +0 -1
- package/server/client/dist/assets/TagNotes-BDTCcJcL.js +0 -1
- package/server/client/dist/assets/ViewNotes-CiZtGJNE.js +0 -1
- package/server/client/dist/assets/ViewSectionDialog-Ctl_694Y.js +0 -102
- package/server/client/dist/assets/Views-D-zlatYD.js +0 -1
- package/server/client/dist/assets/app-DmqIOMWF.css +0 -1
- package/server/client/dist/assets/app-cTRcKMGc.js +0 -26
- package/server/client/dist/assets/image.api-CVuuvTBk.js +0 -17
- package/server/client/dist/assets/index-DKJChPr4.js +0 -1
- package/server/client/dist/assets/manage-image-BZjaxp-f.js +0 -1
- package/server/client/dist/assets/manage-image-detail-DPv0FQ2m.js +0 -1
- package/server/client/dist/assets/mcp-admin-adapter-xAuyZc_M.js +0 -1
- package/server/client/dist/assets/properties-D447wsWk.js +0 -1
- package/server/client/dist/assets/time-B3pt8vt4.js +0 -9
- package/server/client/dist/assets/trash-8sH7EZgc.js +0 -1
- package/server/client/dist/assets/url-BSnWpmJx.js +0 -1
|
@@ -0,0 +1,636 @@
|
|
|
1
|
+
import { mkdir } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { getLoadablePath } from "sqlite-vec";
|
|
4
|
+
import sqlite3 from "sqlite3";
|
|
5
|
+
const SEMANTIC_INDEX_SCHEMA_VERSION = 2;
|
|
6
|
+
const SEARCH_DATABASE_SCHEMA_VERSION = 1;
|
|
7
|
+
const SEARCH_DATABASE_SCHEMA_SQL = `
|
|
8
|
+
CREATE TABLE IF NOT EXISTS search_index_state (
|
|
9
|
+
id INTEGER NOT NULL PRIMARY KEY CHECK (id = 1),
|
|
10
|
+
schema_version INTEGER NOT NULL,
|
|
11
|
+
profile_json TEXT NOT NULL,
|
|
12
|
+
note_count INTEGER NOT NULL,
|
|
13
|
+
chunk_count INTEGER NOT NULL,
|
|
14
|
+
indexed_at TEXT NOT NULL
|
|
15
|
+
);
|
|
16
|
+
CREATE TABLE IF NOT EXISTS search_chunks (
|
|
17
|
+
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
|
18
|
+
note_id INTEGER NOT NULL,
|
|
19
|
+
chunk_index INTEGER NOT NULL,
|
|
20
|
+
source_hash TEXT NOT NULL,
|
|
21
|
+
text TEXT NOT NULL,
|
|
22
|
+
embedding BLOB NOT NULL,
|
|
23
|
+
UNIQUE (note_id, chunk_index)
|
|
24
|
+
);
|
|
25
|
+
CREATE INDEX IF NOT EXISTS search_chunks_note_id_idx ON search_chunks(note_id);
|
|
26
|
+
CREATE TABLE IF NOT EXISTS search_note_sync_queue (
|
|
27
|
+
note_id INTEGER NOT NULL PRIMARY KEY,
|
|
28
|
+
version INTEGER NOT NULL,
|
|
29
|
+
first_queued_at INTEGER NOT NULL,
|
|
30
|
+
last_queued_at INTEGER NOT NULL,
|
|
31
|
+
attempt_count INTEGER NOT NULL,
|
|
32
|
+
next_attempt_at INTEGER NOT NULL,
|
|
33
|
+
last_error TEXT
|
|
34
|
+
);
|
|
35
|
+
CREATE INDEX IF NOT EXISTS search_note_sync_queue_ready_idx
|
|
36
|
+
ON search_note_sync_queue(next_attempt_at, first_queued_at);
|
|
37
|
+
CREATE TABLE IF NOT EXISTS search_note_sync_state (
|
|
38
|
+
id INTEGER NOT NULL PRIMARY KEY CHECK (id = 1),
|
|
39
|
+
last_synced_at TEXT,
|
|
40
|
+
last_reconciled_at TEXT
|
|
41
|
+
);
|
|
42
|
+
`;
|
|
43
|
+
const RESET_SEARCH_DATABASE_SCHEMA_SQL = `
|
|
44
|
+
DROP TABLE IF EXISTS search_note_sync_queue;
|
|
45
|
+
DROP TABLE IF EXISTS search_note_sync_state;
|
|
46
|
+
DROP TABLE IF EXISTS search_chunks;
|
|
47
|
+
DROP TABLE IF EXISTS search_index_state;
|
|
48
|
+
`;
|
|
49
|
+
const openDatabase = async (filePath) => {
|
|
50
|
+
if (filePath !== ":memory:") {
|
|
51
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
52
|
+
}
|
|
53
|
+
const database = await new Promise((resolve, reject) => {
|
|
54
|
+
const db = new sqlite3.Database(filePath, (error) => {
|
|
55
|
+
if (error) {
|
|
56
|
+
reject(error);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
resolve(db);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
await new Promise((resolve, reject) => {
|
|
63
|
+
database.loadExtension(getLoadablePath(), (error) => {
|
|
64
|
+
if (error) {
|
|
65
|
+
reject(error);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
resolve();
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
return database;
|
|
72
|
+
};
|
|
73
|
+
const run = (database, sql, parameters = []) => {
|
|
74
|
+
return new Promise((resolve, reject) => {
|
|
75
|
+
database.run(sql, parameters, function onRun(error) {
|
|
76
|
+
if (error) {
|
|
77
|
+
reject(error);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
resolve(this);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
};
|
|
84
|
+
const get = (database, sql, parameters = []) => {
|
|
85
|
+
return new Promise((resolve, reject) => {
|
|
86
|
+
database.get(sql, parameters, (error, row) => {
|
|
87
|
+
if (error) {
|
|
88
|
+
reject(error);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
resolve(row);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
};
|
|
95
|
+
const all = (database, sql, parameters = []) => {
|
|
96
|
+
return new Promise((resolve, reject) => {
|
|
97
|
+
database.all(sql, parameters, (error, rows) => {
|
|
98
|
+
if (error) {
|
|
99
|
+
reject(error);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
resolve(rows);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
};
|
|
106
|
+
const exec = (database, sql) => {
|
|
107
|
+
return new Promise((resolve, reject) => {
|
|
108
|
+
database.exec(sql, (error) => {
|
|
109
|
+
if (error) {
|
|
110
|
+
reject(error);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
resolve();
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
};
|
|
117
|
+
const initializeSearchDatabase = async (database) => {
|
|
118
|
+
await exec(
|
|
119
|
+
database,
|
|
120
|
+
`
|
|
121
|
+
PRAGMA journal_mode = WAL;
|
|
122
|
+
PRAGMA synchronous = NORMAL;
|
|
123
|
+
`
|
|
124
|
+
);
|
|
125
|
+
const schemaVersion = await get(
|
|
126
|
+
database,
|
|
127
|
+
"SELECT user_version AS userVersion FROM pragma_user_version"
|
|
128
|
+
);
|
|
129
|
+
const storedVersion = schemaVersion?.userVersion ?? 0;
|
|
130
|
+
if (storedVersion !== 0 && storedVersion !== SEARCH_DATABASE_SCHEMA_VERSION) {
|
|
131
|
+
await exec(database, RESET_SEARCH_DATABASE_SCHEMA_SQL);
|
|
132
|
+
}
|
|
133
|
+
await exec(database, SEARCH_DATABASE_SCHEMA_SQL);
|
|
134
|
+
await exec(database, `PRAGMA user_version = ${SEARCH_DATABASE_SCHEMA_VERSION};`);
|
|
135
|
+
};
|
|
136
|
+
const close = (database) => {
|
|
137
|
+
return new Promise((resolve, reject) => {
|
|
138
|
+
database.close((error) => {
|
|
139
|
+
if (error) {
|
|
140
|
+
reject(error);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
resolve();
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
};
|
|
147
|
+
const serializeEmbedding = (embedding) => {
|
|
148
|
+
return Buffer.from(Float32Array.from(embedding).buffer);
|
|
149
|
+
};
|
|
150
|
+
const isSemanticIndexProfile = (value) => {
|
|
151
|
+
if (!value || typeof value !== "object") {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
const profile = value;
|
|
155
|
+
return typeof profile.model === "string" && typeof profile.baseUrl === "string" && Number.isInteger(profile.dimensions) && Number(profile.dimensions) > 0 && typeof profile.queryInstruction === "string" && Number.isInteger(profile.textSchemaVersion);
|
|
156
|
+
};
|
|
157
|
+
const parseProfile = (profileJson) => {
|
|
158
|
+
try {
|
|
159
|
+
const profile = JSON.parse(profileJson);
|
|
160
|
+
return isSemanticIndexProfile(profile) ? profile : null;
|
|
161
|
+
} catch {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
const validateIndexInput = (profile, chunks) => {
|
|
166
|
+
if (!profile.model.trim()) {
|
|
167
|
+
throw new Error("Semantic index profile model is required.");
|
|
168
|
+
}
|
|
169
|
+
if (!profile.baseUrl.trim()) {
|
|
170
|
+
throw new Error("Semantic index profile API URL is required.");
|
|
171
|
+
}
|
|
172
|
+
if (!Number.isInteger(profile.dimensions) || profile.dimensions <= 0) {
|
|
173
|
+
throw new Error("Semantic index dimensions must be a positive integer.");
|
|
174
|
+
}
|
|
175
|
+
for (const chunk of chunks) {
|
|
176
|
+
if (chunk.embedding.length !== profile.dimensions) {
|
|
177
|
+
throw new Error(
|
|
178
|
+
`Embedding dimensions for note ${chunk.noteId} chunk ${chunk.chunkIndex} do not match the index profile.`
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
if (chunk.embedding.some((value) => !Number.isFinite(value))) {
|
|
182
|
+
throw new Error(`Embedding for note ${chunk.noteId} chunk ${chunk.chunkIndex} contains an invalid value.`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
class SqliteSemanticVectorIndex {
|
|
187
|
+
constructor(filePath) {
|
|
188
|
+
this.filePath = filePath;
|
|
189
|
+
}
|
|
190
|
+
filePath;
|
|
191
|
+
databasePromise = null;
|
|
192
|
+
operationTail = Promise.resolve();
|
|
193
|
+
getDatabase() {
|
|
194
|
+
if (!this.databasePromise) {
|
|
195
|
+
this.databasePromise = openDatabase(this.filePath).then(async (database) => {
|
|
196
|
+
await initializeSearchDatabase(database);
|
|
197
|
+
return database;
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
return this.databasePromise;
|
|
201
|
+
}
|
|
202
|
+
runExclusive(operation) {
|
|
203
|
+
const result = this.operationTail.then(operation, operation);
|
|
204
|
+
this.operationTail = result.then(
|
|
205
|
+
() => void 0,
|
|
206
|
+
() => void 0
|
|
207
|
+
);
|
|
208
|
+
return result;
|
|
209
|
+
}
|
|
210
|
+
async replaceAll(profile, chunks) {
|
|
211
|
+
validateIndexInput(profile, chunks);
|
|
212
|
+
return this.runExclusive(async () => {
|
|
213
|
+
const database = await this.getDatabase();
|
|
214
|
+
const indexedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
215
|
+
const noteCount = new Set(chunks.map((chunk) => chunk.noteId)).size;
|
|
216
|
+
await exec(database, "BEGIN IMMEDIATE;");
|
|
217
|
+
try {
|
|
218
|
+
await run(database, "DELETE FROM search_chunks");
|
|
219
|
+
await run(database, "DELETE FROM search_index_state");
|
|
220
|
+
for (const chunk of chunks) {
|
|
221
|
+
await run(
|
|
222
|
+
database,
|
|
223
|
+
`
|
|
224
|
+
INSERT INTO search_chunks (
|
|
225
|
+
note_id,
|
|
226
|
+
chunk_index,
|
|
227
|
+
source_hash,
|
|
228
|
+
text,
|
|
229
|
+
embedding
|
|
230
|
+
) VALUES (?, ?, ?, ?, ?)
|
|
231
|
+
`,
|
|
232
|
+
[
|
|
233
|
+
chunk.noteId,
|
|
234
|
+
chunk.chunkIndex,
|
|
235
|
+
chunk.sourceHash,
|
|
236
|
+
chunk.text,
|
|
237
|
+
serializeEmbedding(chunk.embedding)
|
|
238
|
+
]
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
await run(
|
|
242
|
+
database,
|
|
243
|
+
`
|
|
244
|
+
INSERT INTO search_index_state (
|
|
245
|
+
id,
|
|
246
|
+
schema_version,
|
|
247
|
+
profile_json,
|
|
248
|
+
note_count,
|
|
249
|
+
chunk_count,
|
|
250
|
+
indexed_at
|
|
251
|
+
) VALUES (1, ?, ?, ?, ?, ?)
|
|
252
|
+
`,
|
|
253
|
+
[SEMANTIC_INDEX_SCHEMA_VERSION, JSON.stringify(profile), noteCount, chunks.length, indexedAt]
|
|
254
|
+
);
|
|
255
|
+
await exec(database, "COMMIT;");
|
|
256
|
+
} catch (error) {
|
|
257
|
+
await exec(database, "ROLLBACK;");
|
|
258
|
+
throw error;
|
|
259
|
+
}
|
|
260
|
+
return this.getStatusFromDatabase(database);
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
async getStatusFromDatabase(database) {
|
|
264
|
+
const state = await get(
|
|
265
|
+
database,
|
|
266
|
+
`
|
|
267
|
+
SELECT
|
|
268
|
+
profile_json AS profileJson,
|
|
269
|
+
note_count AS noteCount,
|
|
270
|
+
chunk_count AS chunkCount,
|
|
271
|
+
indexed_at AS indexedAt
|
|
272
|
+
FROM search_index_state
|
|
273
|
+
WHERE id = 1 AND schema_version = ?
|
|
274
|
+
`,
|
|
275
|
+
[SEMANTIC_INDEX_SCHEMA_VERSION]
|
|
276
|
+
);
|
|
277
|
+
const profile = state ? parseProfile(state.profileJson) : null;
|
|
278
|
+
return {
|
|
279
|
+
ready: Boolean(state && profile && state.chunkCount > 0),
|
|
280
|
+
profile,
|
|
281
|
+
noteCount: state?.noteCount ?? 0,
|
|
282
|
+
chunkCount: state?.chunkCount ?? 0,
|
|
283
|
+
indexedAt: state?.indexedAt ?? null
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
async refreshIndexCounts(database) {
|
|
287
|
+
const counts = await get(
|
|
288
|
+
database,
|
|
289
|
+
`
|
|
290
|
+
SELECT
|
|
291
|
+
COUNT(DISTINCT note_id) AS noteCount,
|
|
292
|
+
COUNT(*) AS chunkCount
|
|
293
|
+
FROM search_chunks
|
|
294
|
+
`
|
|
295
|
+
);
|
|
296
|
+
await run(
|
|
297
|
+
database,
|
|
298
|
+
`
|
|
299
|
+
UPDATE search_index_state
|
|
300
|
+
SET note_count = ?, chunk_count = ?, indexed_at = ?
|
|
301
|
+
WHERE id = 1 AND schema_version = ?
|
|
302
|
+
`,
|
|
303
|
+
[counts?.noteCount ?? 0, counts?.chunkCount ?? 0, (/* @__PURE__ */ new Date()).toISOString(), SEMANTIC_INDEX_SCHEMA_VERSION]
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
async getStatus() {
|
|
307
|
+
return this.runExclusive(async () => this.getStatusFromDatabase(await this.getDatabase()));
|
|
308
|
+
}
|
|
309
|
+
async getNoteSourceHash(noteId) {
|
|
310
|
+
return this.runExclusive(async () => {
|
|
311
|
+
const row = await get(
|
|
312
|
+
await this.getDatabase(),
|
|
313
|
+
"SELECT source_hash AS sourceHash FROM search_chunks WHERE note_id = ? LIMIT 1",
|
|
314
|
+
[noteId]
|
|
315
|
+
);
|
|
316
|
+
return row?.sourceHash ?? null;
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
async getAllNoteSourceHashes() {
|
|
320
|
+
return this.runExclusive(async () => {
|
|
321
|
+
const rows = await all(
|
|
322
|
+
await this.getDatabase(),
|
|
323
|
+
`
|
|
324
|
+
SELECT note_id AS noteId, source_hash AS sourceHash
|
|
325
|
+
FROM search_chunks
|
|
326
|
+
GROUP BY note_id, source_hash
|
|
327
|
+
ORDER BY note_id ASC
|
|
328
|
+
`
|
|
329
|
+
);
|
|
330
|
+
return new Map(rows.map((row) => [row.noteId, row.sourceHash]));
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
async enqueueNoteSync(noteId, queuedAt) {
|
|
334
|
+
if (!Number.isInteger(noteId) || noteId <= 0) {
|
|
335
|
+
throw new Error("A positive note ID is required to queue semantic search synchronization.");
|
|
336
|
+
}
|
|
337
|
+
if (!Number.isFinite(queuedAt) || queuedAt < 0) {
|
|
338
|
+
throw new Error("A valid queue timestamp is required for semantic search synchronization.");
|
|
339
|
+
}
|
|
340
|
+
return this.runExclusive(async () => {
|
|
341
|
+
await run(
|
|
342
|
+
await this.getDatabase(),
|
|
343
|
+
`
|
|
344
|
+
INSERT INTO search_note_sync_queue (
|
|
345
|
+
note_id,
|
|
346
|
+
version,
|
|
347
|
+
first_queued_at,
|
|
348
|
+
last_queued_at,
|
|
349
|
+
attempt_count,
|
|
350
|
+
next_attempt_at,
|
|
351
|
+
last_error
|
|
352
|
+
) VALUES (?, 1, ?, ?, 0, ?, NULL)
|
|
353
|
+
ON CONFLICT(note_id) DO UPDATE SET
|
|
354
|
+
version = search_note_sync_queue.version + 1,
|
|
355
|
+
last_queued_at = excluded.last_queued_at,
|
|
356
|
+
attempt_count = 0,
|
|
357
|
+
next_attempt_at = excluded.next_attempt_at,
|
|
358
|
+
last_error = NULL
|
|
359
|
+
`,
|
|
360
|
+
[noteId, queuedAt, queuedAt, queuedAt]
|
|
361
|
+
);
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
async listPendingNoteSyncs({
|
|
365
|
+
now,
|
|
366
|
+
quietPeriodMs,
|
|
367
|
+
maxWaitMs,
|
|
368
|
+
limit,
|
|
369
|
+
force = false
|
|
370
|
+
}) {
|
|
371
|
+
if (![now, quietPeriodMs, maxWaitMs].every((value) => Number.isFinite(value) && value >= 0)) {
|
|
372
|
+
throw new Error("Semantic search synchronization timing values must be non-negative numbers.");
|
|
373
|
+
}
|
|
374
|
+
if (!Number.isInteger(limit) || limit <= 0) {
|
|
375
|
+
throw new Error("Semantic search synchronization batch limit must be a positive integer.");
|
|
376
|
+
}
|
|
377
|
+
return this.runExclusive(async () => {
|
|
378
|
+
return all(
|
|
379
|
+
await this.getDatabase(),
|
|
380
|
+
`
|
|
381
|
+
SELECT
|
|
382
|
+
note_id AS noteId,
|
|
383
|
+
version,
|
|
384
|
+
first_queued_at AS firstQueuedAt,
|
|
385
|
+
last_queued_at AS lastQueuedAt,
|
|
386
|
+
attempt_count AS attemptCount
|
|
387
|
+
FROM search_note_sync_queue
|
|
388
|
+
WHERE
|
|
389
|
+
? = 1
|
|
390
|
+
OR (
|
|
391
|
+
next_attempt_at <= ?
|
|
392
|
+
AND (
|
|
393
|
+
last_queued_at <= ?
|
|
394
|
+
OR first_queued_at <= ?
|
|
395
|
+
)
|
|
396
|
+
)
|
|
397
|
+
ORDER BY first_queued_at ASC, note_id ASC
|
|
398
|
+
LIMIT ?
|
|
399
|
+
`,
|
|
400
|
+
[force ? 1 : 0, now, now - quietPeriodMs, now - maxWaitMs, limit]
|
|
401
|
+
);
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
async completeNoteSyncs(entries, completedAt) {
|
|
405
|
+
return this.runExclusive(async () => {
|
|
406
|
+
const database = await this.getDatabase();
|
|
407
|
+
await exec(database, "BEGIN IMMEDIATE;");
|
|
408
|
+
try {
|
|
409
|
+
for (const entry of entries) {
|
|
410
|
+
await run(database, "DELETE FROM search_note_sync_queue WHERE note_id = ? AND version = ?", [
|
|
411
|
+
entry.noteId,
|
|
412
|
+
entry.version
|
|
413
|
+
]);
|
|
414
|
+
}
|
|
415
|
+
await run(
|
|
416
|
+
database,
|
|
417
|
+
`
|
|
418
|
+
INSERT INTO search_note_sync_state (id, last_synced_at, last_reconciled_at)
|
|
419
|
+
VALUES (1, ?, NULL)
|
|
420
|
+
ON CONFLICT(id) DO UPDATE SET last_synced_at = excluded.last_synced_at
|
|
421
|
+
`,
|
|
422
|
+
[new Date(completedAt).toISOString()]
|
|
423
|
+
);
|
|
424
|
+
await exec(database, "COMMIT;");
|
|
425
|
+
} catch (error) {
|
|
426
|
+
await exec(database, "ROLLBACK;");
|
|
427
|
+
throw error;
|
|
428
|
+
}
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
async failNoteSyncs(entries, error, retryAt) {
|
|
432
|
+
return this.runExclusive(async () => {
|
|
433
|
+
const database = await this.getDatabase();
|
|
434
|
+
await exec(database, "BEGIN IMMEDIATE;");
|
|
435
|
+
try {
|
|
436
|
+
for (const entry of entries) {
|
|
437
|
+
await run(
|
|
438
|
+
database,
|
|
439
|
+
`
|
|
440
|
+
UPDATE search_note_sync_queue
|
|
441
|
+
SET attempt_count = attempt_count + 1,
|
|
442
|
+
next_attempt_at = ?,
|
|
443
|
+
last_error = ?
|
|
444
|
+
WHERE note_id = ? AND version = ?
|
|
445
|
+
`,
|
|
446
|
+
[retryAt, error, entry.noteId, entry.version]
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
await exec(database, "COMMIT;");
|
|
450
|
+
} catch (transactionError) {
|
|
451
|
+
await exec(database, "ROLLBACK;");
|
|
452
|
+
throw transactionError;
|
|
453
|
+
}
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
async recordNoteSyncSuccess(completedAt) {
|
|
457
|
+
return this.completeNoteSyncs([], completedAt);
|
|
458
|
+
}
|
|
459
|
+
async recordNoteSyncReconciliation(reconciledAt) {
|
|
460
|
+
return this.runExclusive(async () => {
|
|
461
|
+
await run(
|
|
462
|
+
await this.getDatabase(),
|
|
463
|
+
`
|
|
464
|
+
INSERT INTO search_note_sync_state (id, last_synced_at, last_reconciled_at)
|
|
465
|
+
VALUES (1, NULL, ?)
|
|
466
|
+
ON CONFLICT(id) DO UPDATE SET last_reconciled_at = excluded.last_reconciled_at
|
|
467
|
+
`,
|
|
468
|
+
[new Date(reconciledAt).toISOString()]
|
|
469
|
+
);
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
async getNoteSyncQueueStatus() {
|
|
473
|
+
return this.runExclusive(async () => {
|
|
474
|
+
const database = await this.getDatabase();
|
|
475
|
+
const [queue, state, latestError] = await Promise.all([
|
|
476
|
+
get(
|
|
477
|
+
database,
|
|
478
|
+
`
|
|
479
|
+
SELECT
|
|
480
|
+
COUNT(*) AS pendingNoteCount,
|
|
481
|
+
MIN(first_queued_at) AS oldestQueuedAt
|
|
482
|
+
FROM search_note_sync_queue
|
|
483
|
+
`
|
|
484
|
+
),
|
|
485
|
+
get(
|
|
486
|
+
database,
|
|
487
|
+
`
|
|
488
|
+
SELECT
|
|
489
|
+
last_synced_at AS lastSyncedAt,
|
|
490
|
+
last_reconciled_at AS lastReconciledAt
|
|
491
|
+
FROM search_note_sync_state
|
|
492
|
+
WHERE id = 1
|
|
493
|
+
`
|
|
494
|
+
),
|
|
495
|
+
get(
|
|
496
|
+
database,
|
|
497
|
+
`
|
|
498
|
+
SELECT last_error AS error
|
|
499
|
+
FROM search_note_sync_queue
|
|
500
|
+
WHERE last_error IS NOT NULL
|
|
501
|
+
ORDER BY next_attempt_at DESC, note_id ASC
|
|
502
|
+
LIMIT 1
|
|
503
|
+
`
|
|
504
|
+
)
|
|
505
|
+
]);
|
|
506
|
+
return {
|
|
507
|
+
pendingNoteCount: queue?.pendingNoteCount ?? 0,
|
|
508
|
+
oldestQueuedAt: queue?.oldestQueuedAt === null || queue?.oldestQueuedAt === void 0 ? null : new Date(queue.oldestQueuedAt).toISOString(),
|
|
509
|
+
lastSyncedAt: state?.lastSyncedAt ?? null,
|
|
510
|
+
lastReconciledAt: state?.lastReconciledAt ?? null,
|
|
511
|
+
error: latestError?.error ?? null
|
|
512
|
+
};
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
async replaceNote(noteId, chunks) {
|
|
516
|
+
return this.runExclusive(async () => {
|
|
517
|
+
const database = await this.getDatabase();
|
|
518
|
+
const status = await this.getStatusFromDatabase(database);
|
|
519
|
+
if (!status.ready || !status.profile) {
|
|
520
|
+
throw new Error("Build the semantic index before updating an individual note.");
|
|
521
|
+
}
|
|
522
|
+
if (chunks.some((chunk) => chunk.noteId !== noteId)) {
|
|
523
|
+
throw new Error("Every replacement chunk must belong to the requested note.");
|
|
524
|
+
}
|
|
525
|
+
validateIndexInput(status.profile, chunks);
|
|
526
|
+
await exec(database, "BEGIN IMMEDIATE;");
|
|
527
|
+
try {
|
|
528
|
+
await run(database, "DELETE FROM search_chunks WHERE note_id = ?", [noteId]);
|
|
529
|
+
for (const chunk of chunks) {
|
|
530
|
+
await run(
|
|
531
|
+
database,
|
|
532
|
+
`
|
|
533
|
+
INSERT INTO search_chunks (
|
|
534
|
+
note_id,
|
|
535
|
+
chunk_index,
|
|
536
|
+
source_hash,
|
|
537
|
+
text,
|
|
538
|
+
embedding
|
|
539
|
+
) VALUES (?, ?, ?, ?, ?)
|
|
540
|
+
`,
|
|
541
|
+
[
|
|
542
|
+
chunk.noteId,
|
|
543
|
+
chunk.chunkIndex,
|
|
544
|
+
chunk.sourceHash,
|
|
545
|
+
chunk.text,
|
|
546
|
+
serializeEmbedding(chunk.embedding)
|
|
547
|
+
]
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
await this.refreshIndexCounts(database);
|
|
551
|
+
await exec(database, "COMMIT;");
|
|
552
|
+
} catch (error) {
|
|
553
|
+
await exec(database, "ROLLBACK;");
|
|
554
|
+
throw error;
|
|
555
|
+
}
|
|
556
|
+
return this.getStatusFromDatabase(database);
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
async removeNote(noteId) {
|
|
560
|
+
return this.runExclusive(async () => {
|
|
561
|
+
const database = await this.getDatabase();
|
|
562
|
+
await exec(database, "BEGIN IMMEDIATE;");
|
|
563
|
+
try {
|
|
564
|
+
await run(database, "DELETE FROM search_chunks WHERE note_id = ?", [noteId]);
|
|
565
|
+
await this.refreshIndexCounts(database);
|
|
566
|
+
await exec(database, "COMMIT;");
|
|
567
|
+
} catch (error) {
|
|
568
|
+
await exec(database, "ROLLBACK;");
|
|
569
|
+
throw error;
|
|
570
|
+
}
|
|
571
|
+
return this.getStatusFromDatabase(database);
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
async search(queryEmbedding, limit) {
|
|
575
|
+
if (!Number.isInteger(limit) || limit <= 0) {
|
|
576
|
+
return [];
|
|
577
|
+
}
|
|
578
|
+
return this.runExclusive(async () => {
|
|
579
|
+
const database = await this.getDatabase();
|
|
580
|
+
const status = await this.getStatusFromDatabase(database);
|
|
581
|
+
if (!status.ready || !status.profile) {
|
|
582
|
+
return [];
|
|
583
|
+
}
|
|
584
|
+
if (queryEmbedding.length !== status.profile.dimensions) {
|
|
585
|
+
throw new Error(
|
|
586
|
+
`Query embedding has ${queryEmbedding.length} dimensions but the index requires ${status.profile.dimensions}.`
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
if (queryEmbedding.some((value) => !Number.isFinite(value))) {
|
|
590
|
+
throw new Error("Query embedding contains an invalid value.");
|
|
591
|
+
}
|
|
592
|
+
const rows = await all(
|
|
593
|
+
database,
|
|
594
|
+
`
|
|
595
|
+
SELECT
|
|
596
|
+
note_id AS noteId,
|
|
597
|
+
MIN(vec_distance_cosine(embedding, ?)) AS distance
|
|
598
|
+
FROM search_chunks
|
|
599
|
+
GROUP BY note_id
|
|
600
|
+
ORDER BY distance ASC, note_id ASC
|
|
601
|
+
LIMIT ?
|
|
602
|
+
`,
|
|
603
|
+
[serializeEmbedding(queryEmbedding), limit]
|
|
604
|
+
);
|
|
605
|
+
return rows.map((row) => ({ noteId: row.noteId, distance: row.distance }));
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
async clear() {
|
|
609
|
+
return this.runExclusive(async () => {
|
|
610
|
+
const database = await this.getDatabase();
|
|
611
|
+
await exec(database, "BEGIN IMMEDIATE;");
|
|
612
|
+
try {
|
|
613
|
+
await run(database, "DELETE FROM search_chunks");
|
|
614
|
+
await run(database, "DELETE FROM search_index_state");
|
|
615
|
+
await exec(database, "COMMIT;");
|
|
616
|
+
} catch (error) {
|
|
617
|
+
await exec(database, "ROLLBACK;");
|
|
618
|
+
throw error;
|
|
619
|
+
}
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
async close() {
|
|
623
|
+
await this.operationTail;
|
|
624
|
+
if (!this.databasePromise) {
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
const database = await this.databasePromise;
|
|
628
|
+
this.databasePromise = null;
|
|
629
|
+
await close(database);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
export {
|
|
633
|
+
SEARCH_DATABASE_SCHEMA_VERSION,
|
|
634
|
+
SEMANTIC_INDEX_SCHEMA_VERSION,
|
|
635
|
+
SqliteSemanticVectorIndex
|
|
636
|
+
};
|
|
@@ -10,6 +10,9 @@ const normalizeTagName = (name) => {
|
|
|
10
10
|
if (!trimmedName) {
|
|
11
11
|
throw new InvalidTagNameError("A tag name is required.");
|
|
12
12
|
}
|
|
13
|
+
if (trimmedName.startsWith("#")) {
|
|
14
|
+
throw new InvalidTagNameError("Tag names must use @, not #. Example: @project.");
|
|
15
|
+
}
|
|
13
16
|
const normalizedName = trimmedName.startsWith("@") ? trimmedName : `@${trimmedName}`;
|
|
14
17
|
if (normalizedName === "@" || /\s/.test(normalizedName.slice(1))) {
|
|
15
18
|
throw new InvalidTagNameError("Tag names must be a single token like @project.");
|
package/server/dist/main.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createApp } from "./app.js";
|
|
2
|
+
import { getDefaultSemanticSearchManager } from "./features/search/search-manager.js";
|
|
2
3
|
import { logAuthConfig, resolveAuthConfig } from "./modules/auth-mode.js";
|
|
3
4
|
import { startDataMaintenanceScheduler } from "./modules/data-maintenance.js";
|
|
4
5
|
const PORT = Number(process.env.PORT || 6683);
|
|
@@ -7,6 +8,7 @@ try {
|
|
|
7
8
|
const authConfig = resolveAuthConfig(process.env);
|
|
8
9
|
logAuthConfig(authConfig);
|
|
9
10
|
const app = createApp(authConfig);
|
|
11
|
+
getDefaultSemanticSearchManager();
|
|
10
12
|
app.listen(PORT, HOST, () => {
|
|
11
13
|
process.stdout.write(`http server listen on ${HOST}:${PORT} (auth: ${authConfig.mode})
|
|
12
14
|
`);
|