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,20 @@
|
|
|
1
|
+
import { searchNotesHybrid } from "../hybrid-search.js";
|
|
2
|
+
const createSearchNotesResolver = (search = searchNotesHybrid) => {
|
|
3
|
+
return async (_, { query, pagination, mode = "HYBRID" }) => {
|
|
4
|
+
return search({
|
|
5
|
+
query,
|
|
6
|
+
limit: Math.min(50, Math.max(0, pagination.limit)),
|
|
7
|
+
offset: Math.max(0, pagination.offset),
|
|
8
|
+
mode: mode.toLowerCase()
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
const searchResolvers = {
|
|
13
|
+
Query: {
|
|
14
|
+
searchNotes: createSearchNotesResolver()
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
export {
|
|
18
|
+
createSearchNotesResolver,
|
|
19
|
+
searchResolvers
|
|
20
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { gql } from "../../../modules/graphql.js";
|
|
2
|
+
const searchTypeDefs = gql`
|
|
3
|
+
enum SearchMode {
|
|
4
|
+
HYBRID
|
|
5
|
+
LEXICAL
|
|
6
|
+
SEMANTIC
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
type SearchNoteMatch {
|
|
10
|
+
noteId: ID!
|
|
11
|
+
lexical: Boolean!
|
|
12
|
+
semantic: Boolean!
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
type SearchNotesResult {
|
|
16
|
+
totalCount: Int!
|
|
17
|
+
notes: [Note!]!
|
|
18
|
+
matches: [SearchNoteMatch!]!
|
|
19
|
+
semanticAvailable: Boolean!
|
|
20
|
+
semanticUsed: Boolean!
|
|
21
|
+
semanticError: String
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
extend type Query {
|
|
25
|
+
searchNotes(query: String!, pagination: PaginationInput!, mode: SearchMode = HYBRID): SearchNotesResult!
|
|
26
|
+
}
|
|
27
|
+
`;
|
|
28
|
+
export {
|
|
29
|
+
searchTypeDefs
|
|
30
|
+
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { createAppError } from "../../../modules/error-handler.js";
|
|
2
|
+
import { normalizeEmbeddingApiKey } from "../embedding-api-key-store.js";
|
|
3
|
+
import { normalizeEmbeddingModelsUrl } from "../embedding-client.js";
|
|
4
|
+
import { normalizeSemanticSearchConfig } from "../search-config.js";
|
|
5
|
+
import {
|
|
6
|
+
getDefaultSemanticSearchManager
|
|
7
|
+
} from "../search-manager.js";
|
|
8
|
+
import { stripTrailingSlashes } from "../url-normalization.js";
|
|
9
|
+
const parseApiKeyInput = (value) => {
|
|
10
|
+
if (!value || typeof value !== "object" || !Object.hasOwn(value, "apiKey")) {
|
|
11
|
+
return { provided: false };
|
|
12
|
+
}
|
|
13
|
+
const rawApiKey = value.apiKey;
|
|
14
|
+
if (rawApiKey !== null && typeof rawApiKey !== "string") {
|
|
15
|
+
throw createAppError(400, "INVALID_EMBEDDING_API_KEY", "Embedding API key must be a string or null.");
|
|
16
|
+
}
|
|
17
|
+
try {
|
|
18
|
+
return {
|
|
19
|
+
provided: true,
|
|
20
|
+
apiKey: rawApiKey === null ? void 0 : normalizeEmbeddingApiKey(rawApiKey)
|
|
21
|
+
};
|
|
22
|
+
} catch (error) {
|
|
23
|
+
throw createAppError(
|
|
24
|
+
400,
|
|
25
|
+
"INVALID_EMBEDDING_API_KEY",
|
|
26
|
+
error instanceof Error ? error.message : "Embedding API key is invalid."
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
const parseConfig = (value) => {
|
|
31
|
+
if (!value || typeof value !== "object") {
|
|
32
|
+
throw createAppError(400, "INVALID_SEARCH_CONFIG", "Semantic search settings must be an object.");
|
|
33
|
+
}
|
|
34
|
+
const config = value;
|
|
35
|
+
if (typeof config.enabled !== "boolean" || typeof config.baseUrl !== "string" || typeof config.model !== "string" || typeof config.queryInstruction !== "string") {
|
|
36
|
+
throw createAppError(
|
|
37
|
+
400,
|
|
38
|
+
"INVALID_SEARCH_CONFIG",
|
|
39
|
+
"enabled, baseUrl, model, and queryInstruction are required."
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
return normalizeSemanticSearchConfig(config);
|
|
44
|
+
} catch (error) {
|
|
45
|
+
throw createAppError(
|
|
46
|
+
400,
|
|
47
|
+
"INVALID_SEARCH_CONFIG",
|
|
48
|
+
error instanceof Error ? error.message : "Semantic search settings are invalid."
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
const parseBaseUrl = (value) => {
|
|
53
|
+
if (!value || typeof value !== "object" || typeof value.baseUrl !== "string") {
|
|
54
|
+
throw createAppError(400, "INVALID_EMBEDDING_API_URL", "Embedding API URL is required.");
|
|
55
|
+
}
|
|
56
|
+
const inputBaseUrl = value.baseUrl;
|
|
57
|
+
if (inputBaseUrl.length > 2048) {
|
|
58
|
+
throw createAppError(400, "INVALID_EMBEDDING_API_URL", "Embedding API URL is too long.");
|
|
59
|
+
}
|
|
60
|
+
const baseUrl = stripTrailingSlashes(inputBaseUrl.trim());
|
|
61
|
+
try {
|
|
62
|
+
normalizeEmbeddingModelsUrl(baseUrl);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
throw createAppError(
|
|
65
|
+
400,
|
|
66
|
+
"INVALID_EMBEDDING_API_URL",
|
|
67
|
+
error instanceof Error ? error.message : "Embedding API URL is invalid."
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
return baseUrl;
|
|
71
|
+
};
|
|
72
|
+
const createSearchAdminStatusHandler = (manager = getDefaultSemanticSearchManager()) => {
|
|
73
|
+
return async (_req, res) => {
|
|
74
|
+
res.status(200).json(await manager.getStatus()).end();
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
const createSearchAdminSaveConfigHandler = (manager = getDefaultSemanticSearchManager()) => {
|
|
78
|
+
return async (req, res) => {
|
|
79
|
+
const status = await manager.saveConfig(parseConfig(req.body), parseApiKeyInput(req.body));
|
|
80
|
+
res.status(200).json(status).end();
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
const createSearchAdminTestConnectionHandler = (manager = getDefaultSemanticSearchManager()) => {
|
|
84
|
+
return async (req, res) => {
|
|
85
|
+
const config = parseConfig({ ...req.body, enabled: true });
|
|
86
|
+
const result = await manager.testConnection(config, parseApiKeyInput(req.body));
|
|
87
|
+
res.status(200).json(result).end();
|
|
88
|
+
};
|
|
89
|
+
};
|
|
90
|
+
const createSearchAdminListModelsHandler = (listModels = (baseUrl, apiKeyInput) => getDefaultSemanticSearchManager().listModels(baseUrl, apiKeyInput)) => {
|
|
91
|
+
return async (req, res) => {
|
|
92
|
+
const models = await listModels(parseBaseUrl(req.body), parseApiKeyInput(req.body));
|
|
93
|
+
res.status(200).json({ models }).end();
|
|
94
|
+
};
|
|
95
|
+
};
|
|
96
|
+
const createSearchAdminReindexHandler = (manager = getDefaultSemanticSearchManager()) => {
|
|
97
|
+
return async (_req, res) => {
|
|
98
|
+
const result = await manager.startReindex();
|
|
99
|
+
res.status(result.started ? 202 : 200).json(result).end();
|
|
100
|
+
};
|
|
101
|
+
};
|
|
102
|
+
export {
|
|
103
|
+
createSearchAdminListModelsHandler,
|
|
104
|
+
createSearchAdminReindexHandler,
|
|
105
|
+
createSearchAdminSaveConfigHandler,
|
|
106
|
+
createSearchAdminStatusHandler,
|
|
107
|
+
createSearchAdminTestConnectionHandler
|
|
108
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
const DEFAULT_RANK_CONSTANT = 60;
|
|
2
|
+
const uniqueNoteIds = (noteIds) => {
|
|
3
|
+
return [...new Set(noteIds.filter((noteId) => Number.isInteger(noteId) && noteId > 0))];
|
|
4
|
+
};
|
|
5
|
+
const fuseHybridSearchRanks = ({
|
|
6
|
+
lexicalNoteIds,
|
|
7
|
+
semanticNoteIds,
|
|
8
|
+
lexicalWeight = 1,
|
|
9
|
+
semanticWeight = 1,
|
|
10
|
+
rankConstant = DEFAULT_RANK_CONSTANT
|
|
11
|
+
}) => {
|
|
12
|
+
const lexicalIds = uniqueNoteIds(lexicalNoteIds);
|
|
13
|
+
const semanticIds = uniqueNoteIds(semanticNoteIds);
|
|
14
|
+
const candidates = /* @__PURE__ */ new Map();
|
|
15
|
+
const addRankedIds = (noteIds, source, weight) => {
|
|
16
|
+
noteIds.forEach((noteId, index) => {
|
|
17
|
+
const rank = index + 1;
|
|
18
|
+
const candidate = candidates.get(noteId) ?? {
|
|
19
|
+
noteId,
|
|
20
|
+
lexicalRank: null,
|
|
21
|
+
semanticRank: null,
|
|
22
|
+
score: 0
|
|
23
|
+
};
|
|
24
|
+
candidate.score += weight / (rankConstant + rank);
|
|
25
|
+
if (source === "lexical") {
|
|
26
|
+
candidate.lexicalRank = rank;
|
|
27
|
+
} else {
|
|
28
|
+
candidate.semanticRank = rank;
|
|
29
|
+
}
|
|
30
|
+
candidates.set(noteId, candidate);
|
|
31
|
+
});
|
|
32
|
+
};
|
|
33
|
+
addRankedIds(lexicalIds, "lexical", lexicalWeight);
|
|
34
|
+
addRankedIds(semanticIds, "semantic", semanticWeight);
|
|
35
|
+
return [...candidates.values()].sort((left, right) => {
|
|
36
|
+
if (left.score !== right.score) {
|
|
37
|
+
return right.score - left.score;
|
|
38
|
+
}
|
|
39
|
+
const leftBestRank = Math.min(left.lexicalRank ?? Number.POSITIVE_INFINITY, left.semanticRank ?? Infinity);
|
|
40
|
+
const rightBestRank = Math.min(right.lexicalRank ?? Number.POSITIVE_INFINITY, right.semanticRank ?? Infinity);
|
|
41
|
+
if (leftBestRank !== rightBestRank) {
|
|
42
|
+
return leftBestRank - rightBestRank;
|
|
43
|
+
}
|
|
44
|
+
if (left.lexicalRank !== null && right.lexicalRank === null) {
|
|
45
|
+
return -1;
|
|
46
|
+
}
|
|
47
|
+
if (left.lexicalRank === null && right.lexicalRank !== null) {
|
|
48
|
+
return 1;
|
|
49
|
+
}
|
|
50
|
+
return left.noteId - right.noteId;
|
|
51
|
+
});
|
|
52
|
+
};
|
|
53
|
+
export {
|
|
54
|
+
fuseHybridSearchRanks
|
|
55
|
+
};
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildNoteSearchText,
|
|
3
|
+
NOTE_SEARCH_TEXT_SCHEMA_VERSION,
|
|
4
|
+
parseNoteSearchQuery
|
|
5
|
+
} from "../../features/note/services/search.js";
|
|
6
|
+
import models from "../../models.js";
|
|
7
|
+
import { runDataMaintenanceInBackground } from "../../modules/data-maintenance.js";
|
|
8
|
+
import { fuseHybridSearchRanks } from "./hybrid-ranking.js";
|
|
9
|
+
import { getDefaultSemanticSearchManager } from "./search-manager.js";
|
|
10
|
+
const MAX_HYBRID_CANDIDATES = 80;
|
|
11
|
+
const unavailableSemanticAttempt = (error) => ({
|
|
12
|
+
available: false,
|
|
13
|
+
used: false,
|
|
14
|
+
matches: [],
|
|
15
|
+
error: error instanceof Error ? error.message : "Semantic search is unavailable."
|
|
16
|
+
});
|
|
17
|
+
const buildLexicalWhere = (query) => {
|
|
18
|
+
const parsedQuery = parseNoteSearchQuery(query);
|
|
19
|
+
if (!parsedQuery.hasFilters) {
|
|
20
|
+
return void 0;
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
AND: [
|
|
24
|
+
{ searchableTextVersion: NOTE_SEARCH_TEXT_SCHEMA_VERSION },
|
|
25
|
+
...parsedQuery.included.length > 0 ? [
|
|
26
|
+
{
|
|
27
|
+
OR: parsedQuery.included.map((term) => ({ searchableText: { contains: term } }))
|
|
28
|
+
}
|
|
29
|
+
] : [],
|
|
30
|
+
...parsedQuery.excluded.map((term) => ({ NOT: { searchableText: { contains: term } } }))
|
|
31
|
+
]
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
const matchesHybridLexicalQuery = (searchableText, query) => {
|
|
35
|
+
const parsedQuery = parseNoteSearchQuery(query);
|
|
36
|
+
const normalizedSearchableText = searchableText.toLowerCase();
|
|
37
|
+
const includesAnyTerm = parsedQuery.included.length === 0 || parsedQuery.included.some((term) => normalizedSearchableText.includes(term));
|
|
38
|
+
return includesAnyTerm && parsedQuery.excluded.every((term) => !normalizedSearchableText.includes(term));
|
|
39
|
+
};
|
|
40
|
+
const scoreLexicalCandidate = (candidate, query) => {
|
|
41
|
+
const parsedQuery = parseNoteSearchQuery(query);
|
|
42
|
+
const phrase = parsedQuery.included.join(" ");
|
|
43
|
+
const title = candidate.title.toLowerCase();
|
|
44
|
+
const searchableText = candidate.searchableText.toLowerCase();
|
|
45
|
+
const matchingTermCount = parsedQuery.included.filter((term) => searchableText.includes(term)).length;
|
|
46
|
+
let score = 0;
|
|
47
|
+
if (phrase && title === phrase) {
|
|
48
|
+
score += 100;
|
|
49
|
+
} else if (phrase && title.includes(phrase)) {
|
|
50
|
+
score += 40;
|
|
51
|
+
}
|
|
52
|
+
if (phrase && searchableText.includes(phrase)) {
|
|
53
|
+
score += 15;
|
|
54
|
+
}
|
|
55
|
+
if (parsedQuery.included.length > 1 && matchingTermCount === parsedQuery.included.length) {
|
|
56
|
+
score += 20;
|
|
57
|
+
}
|
|
58
|
+
for (const term of parsedQuery.included) {
|
|
59
|
+
if (title.includes(term)) {
|
|
60
|
+
score += 8;
|
|
61
|
+
}
|
|
62
|
+
if (searchableText.includes(term)) {
|
|
63
|
+
score += 1;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return score;
|
|
67
|
+
};
|
|
68
|
+
const rankLexicalCandidates = (candidates, query) => {
|
|
69
|
+
return [...candidates].sort((left, right) => {
|
|
70
|
+
const scoreDifference = scoreLexicalCandidate(right, query) - scoreLexicalCandidate(left, query);
|
|
71
|
+
if (scoreDifference !== 0) {
|
|
72
|
+
return scoreDifference;
|
|
73
|
+
}
|
|
74
|
+
const updatedAtDifference = right.updatedAt.getTime() - left.updatedAt.getTime();
|
|
75
|
+
return updatedAtDifference !== 0 ? updatedAtDifference : left.id - right.id;
|
|
76
|
+
});
|
|
77
|
+
};
|
|
78
|
+
const listDefaultLexicalNoteIds = async (query, limit) => {
|
|
79
|
+
const where = buildLexicalWhere(query);
|
|
80
|
+
if (!where) {
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
const [freshCandidates, staleCandidates] = await Promise.all([
|
|
84
|
+
models.note.findMany({
|
|
85
|
+
where,
|
|
86
|
+
select: {
|
|
87
|
+
id: true,
|
|
88
|
+
title: true,
|
|
89
|
+
searchableText: true,
|
|
90
|
+
updatedAt: true
|
|
91
|
+
}
|
|
92
|
+
}),
|
|
93
|
+
models.note.findMany({
|
|
94
|
+
where: { searchableTextVersion: { not: NOTE_SEARCH_TEXT_SCHEMA_VERSION } },
|
|
95
|
+
select: {
|
|
96
|
+
id: true,
|
|
97
|
+
title: true,
|
|
98
|
+
content: true,
|
|
99
|
+
searchableText: true,
|
|
100
|
+
updatedAt: true
|
|
101
|
+
}
|
|
102
|
+
})
|
|
103
|
+
]);
|
|
104
|
+
if (staleCandidates.length > 0) {
|
|
105
|
+
void runDataMaintenanceInBackground();
|
|
106
|
+
}
|
|
107
|
+
const matchingStaleCandidates = staleCandidates.map((candidate) => ({
|
|
108
|
+
...candidate,
|
|
109
|
+
searchableText: buildNoteSearchText(candidate)
|
|
110
|
+
})).filter((candidate) => matchesHybridLexicalQuery(candidate.searchableText, query));
|
|
111
|
+
return rankLexicalCandidates([...freshCandidates, ...matchingStaleCandidates], query).slice(0, limit).map((candidate) => candidate.id);
|
|
112
|
+
};
|
|
113
|
+
const defaultDependencies = {
|
|
114
|
+
listLexicalNoteIds: listDefaultLexicalNoteIds,
|
|
115
|
+
trySemanticSearch: (query, limit) => getDefaultSemanticSearchManager().trySearch(query, limit),
|
|
116
|
+
findNotesByIds: (ids) => models.note.findMany({ where: { id: { in: ids } } })
|
|
117
|
+
};
|
|
118
|
+
const createHybridNoteSearch = (dependencies = defaultDependencies) => {
|
|
119
|
+
return async ({
|
|
120
|
+
query,
|
|
121
|
+
limit,
|
|
122
|
+
offset,
|
|
123
|
+
mode = "hybrid"
|
|
124
|
+
}) => {
|
|
125
|
+
const normalizedQuery = query.trim();
|
|
126
|
+
if (!normalizedQuery || limit <= 0 || offset < 0) {
|
|
127
|
+
return {
|
|
128
|
+
totalCount: 0,
|
|
129
|
+
notes: [],
|
|
130
|
+
matches: [],
|
|
131
|
+
semanticAvailable: false,
|
|
132
|
+
semanticUsed: false,
|
|
133
|
+
semanticError: null
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
const candidateLimit = MAX_HYBRID_CANDIDATES;
|
|
137
|
+
const [lexicalNoteIds, semanticAttempt] = await Promise.all([
|
|
138
|
+
mode === "semantic" ? Promise.resolve([]) : dependencies.listLexicalNoteIds(normalizedQuery, candidateLimit),
|
|
139
|
+
mode === "lexical" ? Promise.resolve({
|
|
140
|
+
available: false,
|
|
141
|
+
used: false,
|
|
142
|
+
matches: [],
|
|
143
|
+
error: null
|
|
144
|
+
}) : dependencies.trySemanticSearch(normalizedQuery, candidateLimit).catch((error) => unavailableSemanticAttempt(error))
|
|
145
|
+
]);
|
|
146
|
+
const rankedCandidates = fuseHybridSearchRanks({
|
|
147
|
+
lexicalNoteIds,
|
|
148
|
+
semanticNoteIds: semanticAttempt.matches.map((match) => match.noteId)
|
|
149
|
+
});
|
|
150
|
+
const pageCandidates = rankedCandidates.slice(offset, offset + limit);
|
|
151
|
+
const notes = await dependencies.findNotesByIds(pageCandidates.map((candidate) => candidate.noteId));
|
|
152
|
+
const notesById = new Map(notes.map((note) => [note.id, note]));
|
|
153
|
+
const existingCandidates = pageCandidates.filter((candidate) => notesById.has(candidate.noteId));
|
|
154
|
+
return {
|
|
155
|
+
totalCount: rankedCandidates.length,
|
|
156
|
+
notes: existingCandidates.map((candidate) => notesById.get(candidate.noteId)),
|
|
157
|
+
matches: existingCandidates.map((candidate) => ({
|
|
158
|
+
noteId: candidate.noteId,
|
|
159
|
+
lexical: candidate.lexicalRank !== null,
|
|
160
|
+
semantic: candidate.semanticRank !== null
|
|
161
|
+
})),
|
|
162
|
+
semanticAvailable: semanticAttempt.available,
|
|
163
|
+
semanticUsed: semanticAttempt.used,
|
|
164
|
+
semanticError: semanticAttempt.error
|
|
165
|
+
};
|
|
166
|
+
};
|
|
167
|
+
};
|
|
168
|
+
const searchNotesHybrid = createHybridNoteSearch();
|
|
169
|
+
export {
|
|
170
|
+
createHybridNoteSearch,
|
|
171
|
+
matchesHybridLexicalQuery,
|
|
172
|
+
rankLexicalCandidates,
|
|
173
|
+
searchNotesHybrid
|
|
174
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
const NOTE_CHANGE_CHANNEL = "semantic-search-note-change";
|
|
3
|
+
const noteChangeEmitter = new EventEmitter();
|
|
4
|
+
const notifySemanticSearchNoteChanged = (noteId) => {
|
|
5
|
+
if (Number.isInteger(noteId) && noteId > 0) {
|
|
6
|
+
noteChangeEmitter.emit(NOTE_CHANGE_CHANNEL, noteId);
|
|
7
|
+
}
|
|
8
|
+
};
|
|
9
|
+
const subscribeSemanticSearchNoteChanges = (listener) => {
|
|
10
|
+
noteChangeEmitter.on(NOTE_CHANGE_CHANNEL, listener);
|
|
11
|
+
return () => noteChangeEmitter.off(NOTE_CHANGE_CHANNEL, listener);
|
|
12
|
+
};
|
|
13
|
+
export {
|
|
14
|
+
notifySemanticSearchNoteChanged,
|
|
15
|
+
subscribeSemanticSearchNoteChanges
|
|
16
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { extractVisibleSearchTextFromContent } from "../../features/note/services/search.js";
|
|
3
|
+
const NOTE_EMBEDDING_TEXT_SCHEMA_VERSION = 1;
|
|
4
|
+
const DEFAULT_NOTE_CHUNK_MAX_LENGTH = 900;
|
|
5
|
+
const DEFAULT_NOTE_CHUNK_OVERLAP = 120;
|
|
6
|
+
const normalizeText = (value) => value.replace(/\s+/g, " ").trim();
|
|
7
|
+
const findWindowEnd = (text, start, maxLength) => {
|
|
8
|
+
const desiredEnd = Math.min(text.length, start + maxLength);
|
|
9
|
+
if (desiredEnd === text.length) {
|
|
10
|
+
return desiredEnd;
|
|
11
|
+
}
|
|
12
|
+
const minimumEnd = start + Math.floor(maxLength * 0.7);
|
|
13
|
+
const breakAt = text.lastIndexOf(" ", desiredEnd);
|
|
14
|
+
return breakAt >= minimumEnd ? breakAt : desiredEnd;
|
|
15
|
+
};
|
|
16
|
+
const findNextWindowStart = (text, start, end, overlap) => {
|
|
17
|
+
const desiredStart = Math.max(start + 1, end - overlap);
|
|
18
|
+
const nextSpace = text.indexOf(" ", desiredStart);
|
|
19
|
+
if (nextSpace === -1 || nextSpace >= end) {
|
|
20
|
+
return desiredStart;
|
|
21
|
+
}
|
|
22
|
+
return nextSpace + 1;
|
|
23
|
+
};
|
|
24
|
+
const splitSearchText = (text, options = {}) => {
|
|
25
|
+
const normalizedText = normalizeText(text);
|
|
26
|
+
if (!normalizedText) {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
const maxLength = options.maxLength ?? DEFAULT_NOTE_CHUNK_MAX_LENGTH;
|
|
30
|
+
const overlap = options.overlap ?? DEFAULT_NOTE_CHUNK_OVERLAP;
|
|
31
|
+
if (!Number.isInteger(maxLength) || maxLength < 100) {
|
|
32
|
+
throw new Error("Note chunk maxLength must be an integer of at least 100.");
|
|
33
|
+
}
|
|
34
|
+
if (!Number.isInteger(overlap) || overlap < 0 || overlap >= maxLength) {
|
|
35
|
+
throw new Error("Note chunk overlap must be a non-negative integer smaller than maxLength.");
|
|
36
|
+
}
|
|
37
|
+
const chunks = [];
|
|
38
|
+
let start = 0;
|
|
39
|
+
while (start < normalizedText.length) {
|
|
40
|
+
const end = findWindowEnd(normalizedText, start, maxLength);
|
|
41
|
+
const chunk = normalizedText.slice(start, end).trim();
|
|
42
|
+
if (chunk) {
|
|
43
|
+
chunks.push(chunk);
|
|
44
|
+
}
|
|
45
|
+
if (end >= normalizedText.length) {
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
start = findNextWindowStart(normalizedText, start, end, overlap);
|
|
49
|
+
}
|
|
50
|
+
return chunks;
|
|
51
|
+
};
|
|
52
|
+
const buildSourceHash = (title, body) => {
|
|
53
|
+
return createHash("sha256").update(`${NOTE_EMBEDDING_TEXT_SCHEMA_VERSION}\0${title}\0${body}`).digest("hex");
|
|
54
|
+
};
|
|
55
|
+
const buildNoteEmbeddingChunks = (note, options = {}) => {
|
|
56
|
+
const title = normalizeText(note.title);
|
|
57
|
+
const body = normalizeText(extractVisibleSearchTextFromContent(note.content));
|
|
58
|
+
const bodyChunks = splitSearchText(body, options);
|
|
59
|
+
const searchableChunks = bodyChunks.length > 0 ? bodyChunks : title ? [""] : [];
|
|
60
|
+
const sourceHash = buildSourceHash(title, body);
|
|
61
|
+
return searchableChunks.map((chunk, chunkIndex) => ({
|
|
62
|
+
noteId: note.id,
|
|
63
|
+
chunkIndex,
|
|
64
|
+
sourceHash,
|
|
65
|
+
text: title ? `Title: ${title}
|
|
66
|
+
Content: ${chunk}`.trimEnd() : chunk
|
|
67
|
+
}));
|
|
68
|
+
};
|
|
69
|
+
export {
|
|
70
|
+
DEFAULT_NOTE_CHUNK_MAX_LENGTH,
|
|
71
|
+
DEFAULT_NOTE_CHUNK_OVERLAP,
|
|
72
|
+
NOTE_EMBEDDING_TEXT_SCHEMA_VERSION,
|
|
73
|
+
buildNoteEmbeddingChunks,
|
|
74
|
+
splitSearchText
|
|
75
|
+
};
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { normalizeEmbeddingApiUrl } from "./embedding-client.js";
|
|
2
|
+
import { stripTrailingSlashes } from "./url-normalization.js";
|
|
3
|
+
const SEMANTIC_SEARCH_CONFIG_CACHE_KEY = "SEMANTIC_SEARCH_CONFIG_V1";
|
|
4
|
+
const SEMANTIC_SEARCH_VALIDATED_CONNECTION_CACHE_KEY = "SEMANTIC_SEARCH_VALIDATED_CONNECTION_V1";
|
|
5
|
+
const LEGACY_KOREAN_QUERY_INSTRUCTION = "Given a vague Korean memory query, retrieve relevant passages from personal notes.";
|
|
6
|
+
const DEFAULT_SEMANTIC_SEARCH_CONFIG = {
|
|
7
|
+
enabled: false,
|
|
8
|
+
baseUrl: "",
|
|
9
|
+
model: "",
|
|
10
|
+
queryInstruction: ""
|
|
11
|
+
};
|
|
12
|
+
const parseStoredConfig = (value) => {
|
|
13
|
+
try {
|
|
14
|
+
const parsed = JSON.parse(value);
|
|
15
|
+
if (typeof parsed.enabled !== "boolean" || typeof parsed.baseUrl !== "string" || typeof parsed.model !== "string" || typeof parsed.queryInstruction !== "string") {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
enabled: parsed.enabled,
|
|
20
|
+
baseUrl: parsed.baseUrl,
|
|
21
|
+
model: parsed.model,
|
|
22
|
+
queryInstruction: parsed.queryInstruction
|
|
23
|
+
};
|
|
24
|
+
} catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
const readStoredAuthFingerprint = ({
|
|
29
|
+
authFingerprint,
|
|
30
|
+
usesBearerAuth
|
|
31
|
+
}) => {
|
|
32
|
+
if (typeof authFingerprint === "string") {
|
|
33
|
+
return authFingerprint;
|
|
34
|
+
}
|
|
35
|
+
if (usesBearerAuth === false) {
|
|
36
|
+
return "";
|
|
37
|
+
}
|
|
38
|
+
return "legacy-bearer-auth";
|
|
39
|
+
};
|
|
40
|
+
const parseValidatedConnection = (value) => {
|
|
41
|
+
try {
|
|
42
|
+
const parsed = JSON.parse(value);
|
|
43
|
+
if (typeof parsed.baseUrl !== "string" || typeof parsed.model !== "string" || typeof parsed.validatedAt !== "string") {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
baseUrl: parsed.baseUrl,
|
|
48
|
+
model: parsed.model,
|
|
49
|
+
validatedAt: parsed.validatedAt,
|
|
50
|
+
authFingerprint: readStoredAuthFingerprint(parsed)
|
|
51
|
+
};
|
|
52
|
+
} catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
const normalizeSemanticSearchConfig = (input) => {
|
|
57
|
+
if (input.baseUrl.length > 2048) {
|
|
58
|
+
throw new Error("Embedding API URL is too long.");
|
|
59
|
+
}
|
|
60
|
+
const config = {
|
|
61
|
+
enabled: input.enabled,
|
|
62
|
+
baseUrl: stripTrailingSlashes(input.baseUrl.trim()),
|
|
63
|
+
model: input.model.trim(),
|
|
64
|
+
queryInstruction: input.queryInstruction.trim()
|
|
65
|
+
};
|
|
66
|
+
if (config.baseUrl) {
|
|
67
|
+
normalizeEmbeddingApiUrl(config.baseUrl);
|
|
68
|
+
}
|
|
69
|
+
if (config.enabled && (!config.baseUrl || !config.model)) {
|
|
70
|
+
throw new Error("Embedding API URL and model are required when semantic search is enabled.");
|
|
71
|
+
}
|
|
72
|
+
if (config.model.length > 200) {
|
|
73
|
+
throw new Error("Embedding model name is too long.");
|
|
74
|
+
}
|
|
75
|
+
if (config.queryInstruction.length > 1e3) {
|
|
76
|
+
throw new Error("Embedding query instruction is too long.");
|
|
77
|
+
}
|
|
78
|
+
return config;
|
|
79
|
+
};
|
|
80
|
+
class SemanticSearchConfigStore {
|
|
81
|
+
constructor(cache) {
|
|
82
|
+
this.cache = cache;
|
|
83
|
+
}
|
|
84
|
+
cache;
|
|
85
|
+
async get() {
|
|
86
|
+
const row = await this.cache.findUnique({ where: { key: SEMANTIC_SEARCH_CONFIG_CACHE_KEY } });
|
|
87
|
+
const config = row ? parseStoredConfig(row.value) : null;
|
|
88
|
+
if (!config) {
|
|
89
|
+
return DEFAULT_SEMANTIC_SEARCH_CONFIG;
|
|
90
|
+
}
|
|
91
|
+
if (config.queryInstruction.trim() === LEGACY_KOREAN_QUERY_INSTRUCTION) {
|
|
92
|
+
return this.set({ ...config, queryInstruction: "" });
|
|
93
|
+
}
|
|
94
|
+
return config;
|
|
95
|
+
}
|
|
96
|
+
async set(input) {
|
|
97
|
+
const config = normalizeSemanticSearchConfig(input);
|
|
98
|
+
const value = JSON.stringify(config);
|
|
99
|
+
await this.cache.upsert({
|
|
100
|
+
where: { key: SEMANTIC_SEARCH_CONFIG_CACHE_KEY },
|
|
101
|
+
create: { key: SEMANTIC_SEARCH_CONFIG_CACHE_KEY, value },
|
|
102
|
+
update: { value }
|
|
103
|
+
});
|
|
104
|
+
return config;
|
|
105
|
+
}
|
|
106
|
+
async getValidatedConnection() {
|
|
107
|
+
const row = await this.cache.findUnique({
|
|
108
|
+
where: { key: SEMANTIC_SEARCH_VALIDATED_CONNECTION_CACHE_KEY }
|
|
109
|
+
});
|
|
110
|
+
return row ? parseValidatedConnection(row.value) : null;
|
|
111
|
+
}
|
|
112
|
+
async isConnectionValidated(input, authFingerprint = "") {
|
|
113
|
+
const config = normalizeSemanticSearchConfig({
|
|
114
|
+
...DEFAULT_SEMANTIC_SEARCH_CONFIG,
|
|
115
|
+
baseUrl: input.baseUrl,
|
|
116
|
+
model: input.model
|
|
117
|
+
});
|
|
118
|
+
const validated = await this.getValidatedConnection();
|
|
119
|
+
return Boolean(
|
|
120
|
+
validated && validated.baseUrl === config.baseUrl && validated.model === config.model && validated.authFingerprint === authFingerprint
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
async markConnectionValidated(input, authFingerprint = "") {
|
|
124
|
+
const config = normalizeSemanticSearchConfig({
|
|
125
|
+
...DEFAULT_SEMANTIC_SEARCH_CONFIG,
|
|
126
|
+
baseUrl: input.baseUrl,
|
|
127
|
+
model: input.model
|
|
128
|
+
});
|
|
129
|
+
const validated = {
|
|
130
|
+
baseUrl: config.baseUrl,
|
|
131
|
+
model: config.model,
|
|
132
|
+
validatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
133
|
+
authFingerprint
|
|
134
|
+
};
|
|
135
|
+
const value = JSON.stringify(validated);
|
|
136
|
+
await this.cache.upsert({
|
|
137
|
+
where: { key: SEMANTIC_SEARCH_VALIDATED_CONNECTION_CACHE_KEY },
|
|
138
|
+
create: { key: SEMANTIC_SEARCH_VALIDATED_CONNECTION_CACHE_KEY, value },
|
|
139
|
+
update: { value }
|
|
140
|
+
});
|
|
141
|
+
return validated;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
export {
|
|
145
|
+
DEFAULT_SEMANTIC_SEARCH_CONFIG,
|
|
146
|
+
SEMANTIC_SEARCH_CONFIG_CACHE_KEY,
|
|
147
|
+
SEMANTIC_SEARCH_VALIDATED_CONNECTION_CACHE_KEY,
|
|
148
|
+
SemanticSearchConfigStore,
|
|
149
|
+
normalizeSemanticSearchConfig
|
|
150
|
+
};
|