ocean-brain 0.8.2 → 0.11.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 +103 -105
- package/package.json +3 -2
- package/server/client/dist/assets/{ArrowRight.es-g2YZTklZ.js → ArrowRight.es-BEhnlwfe.js} +1 -1
- package/server/client/dist/assets/AuxiliaryPanelHeader-B1k7pZpn.js +1 -0
- package/server/client/dist/assets/Calendar-D7QJsAlg.js +25 -0
- package/server/client/dist/assets/{Callout-B_IPSF_5.js → Callout-BTd1BYtV.js} +1 -1
- package/server/client/dist/assets/{Copy.es-CNxe4ujr.js → Copy.es-BoLjilxv.js} +1 -1
- package/server/client/dist/assets/File.es-Ct7fscBg.js +1 -0
- package/server/client/dist/assets/Graph-DYUb7gQb.js +46 -0
- package/server/client/dist/assets/{Graph-D9GeO71k.js → Graph-LY_IgMYy.js} +1 -1
- package/server/client/dist/assets/{Image.es-DnON8cv2.js → Image.es-OYyN3BYt.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-DsMhwrHw.js +25 -0
- package/server/client/dist/assets/NotePropertiesPanel-aCKXAe5F.js +2 -0
- package/server/client/dist/assets/PencilLine.es-0JpEgxFY.js +1 -0
- package/server/client/dist/assets/{Plus.es-CQ8wn2is.js → Plus.es-DRfLW_bI.js} +1 -1
- package/server/client/dist/assets/Reminders-COr1OHvF.js +1 -0
- package/server/client/dist/assets/Search-Ctkx0BQk.js +31 -0
- package/server/client/dist/assets/SurfaceCard-t7pdfhEe.js +1 -0
- package/server/client/dist/assets/Tag-BjxtrIQW.js +1 -0
- package/server/client/dist/assets/TagNotes-DV_fdRbg.js +1 -0
- package/server/client/dist/assets/{Trash.es-0aPpA-3S.js → Trash.es-Bo1VyK9Q.js} +1 -1
- package/server/client/dist/assets/ViewChip-DlCkL4z4.js +1 -0
- package/server/client/dist/assets/ViewNotes-wWWeFJyU.js +1 -0
- package/server/client/dist/assets/ViewSectionDialog-DhH8harC.js +102 -0
- package/server/client/dist/assets/Views-CWE99SnI.js +1 -0
- package/server/client/dist/assets/abnfDiagram-VRR7QNED-CWlV5dXw.js +1 -0
- package/server/client/dist/assets/app-B9WZRrAH.css +1 -0
- package/server/client/dist/assets/app-BpSMjzET.js +26 -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-CyQisInP.js +17 -0
- package/server/client/dist/assets/index-B-E1Ilh9.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-azmo5iVw.js +1 -0
- package/server/client/dist/assets/manage-image-detail-BF-uSqb_.js +1 -0
- package/server/client/dist/assets/map-Mm1V-_Na.js +1 -0
- package/server/client/dist/assets/{mcp-CT_9IV2c.js → mcp-CCaFd0zm.js} +4 -4
- package/server/client/dist/assets/{mcp-admin-adapter-xAuyZc_M.js → mcp-admin-adapter-PMmgPupc.js} +1 -1
- 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-CtijppHK.js} +1 -1
- package/server/client/dist/assets/{note.api-D_00ZzJC.js → note.api-D3oJmG3q.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-AusSo8p9.js} +4 -4
- package/server/client/dist/assets/properties-BrNJLkxR.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-D-xKTPdO.js +1 -0
- package/server/client/dist/assets/search-admin-adapter-CJnJmL6w.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-DNSQpqwk.js} +3 -3
- package/server/client/dist/assets/time-D5LRMumI.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-D4-9sv8q.js +1 -0
- package/server/client/dist/assets/url-9nrbQs7Z.js +1 -0
- package/server/client/dist/assets/{useReminderMutate-CLK1tHvT.js → useReminderMutate-BrGO9M0o.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.field.resolver.js +2 -0
- package/server/dist/features/note/graphql/note.mutation.resolver.js +54 -23
- package/server/dist/features/note/graphql/note.query.resolver.js +15 -5
- package/server/dist/features/note/graphql/note.type-defs.js +2 -1
- package/server/dist/features/note/http/mcp.js +1 -60
- package/server/dist/features/note/services/authoring.js +17 -80
- package/server/dist/features/note/services/markdown-patch.js +80 -8
- package/server/dist/features/note/services/note-reference-index.js +98 -0
- 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/search.js +5 -0
- package/server/dist/features/note/services/snapshot.js +82 -58
- package/server/dist/features/note/services/trash.js +18 -31
- package/server/dist/features/note/services/write.js +10 -3
- 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 +32 -0
- package/server/dist/features/search/graphql/type-defs.js +37 -0
- package/server/dist/features/search/http/handlers.js +108 -0
- package/server/dist/features/search/hybrid-ranking.js +57 -0
- package/server/dist/features/search/hybrid-search.js +176 -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/related-notes.js +186 -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 +12 -3
- 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/prisma/migrations/20260728090000_0019_note_reference_index/migration.sql +15 -0
- package/server/prisma/schema.prisma +16 -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/NotePropertiesPanel-2DgXEM4B.js +0 -2
- 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/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,176 @@
|
|
|
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 MAX_HYBRID_SEMANTIC_CANDIDATES = 30;
|
|
12
|
+
const unavailableSemanticAttempt = (error) => ({
|
|
13
|
+
available: false,
|
|
14
|
+
used: false,
|
|
15
|
+
matches: [],
|
|
16
|
+
error: error instanceof Error ? error.message : "Semantic search is unavailable."
|
|
17
|
+
});
|
|
18
|
+
const buildLexicalWhere = (query) => {
|
|
19
|
+
const parsedQuery = parseNoteSearchQuery(query);
|
|
20
|
+
if (!parsedQuery.hasFilters) {
|
|
21
|
+
return void 0;
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
AND: [
|
|
25
|
+
{ searchableTextVersion: NOTE_SEARCH_TEXT_SCHEMA_VERSION },
|
|
26
|
+
...parsedQuery.included.length > 0 ? [
|
|
27
|
+
{
|
|
28
|
+
OR: parsedQuery.included.map((term) => ({ searchableText: { contains: term } }))
|
|
29
|
+
}
|
|
30
|
+
] : [],
|
|
31
|
+
...parsedQuery.excluded.map((term) => ({ NOT: { searchableText: { contains: term } } }))
|
|
32
|
+
]
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
const matchesHybridLexicalQuery = (searchableText, query) => {
|
|
36
|
+
const parsedQuery = parseNoteSearchQuery(query);
|
|
37
|
+
const normalizedSearchableText = searchableText.toLowerCase();
|
|
38
|
+
const includesAnyTerm = parsedQuery.included.length === 0 || parsedQuery.included.some((term) => normalizedSearchableText.includes(term));
|
|
39
|
+
return includesAnyTerm && parsedQuery.excluded.every((term) => !normalizedSearchableText.includes(term));
|
|
40
|
+
};
|
|
41
|
+
const scoreLexicalCandidate = (candidate, query) => {
|
|
42
|
+
const parsedQuery = parseNoteSearchQuery(query);
|
|
43
|
+
const phrase = parsedQuery.included.join(" ");
|
|
44
|
+
const title = candidate.title.toLowerCase();
|
|
45
|
+
const searchableText = candidate.searchableText.toLowerCase();
|
|
46
|
+
const matchingTermCount = parsedQuery.included.filter((term) => searchableText.includes(term)).length;
|
|
47
|
+
let score = 0;
|
|
48
|
+
if (phrase && title === phrase) {
|
|
49
|
+
score += 100;
|
|
50
|
+
} else if (phrase && title.includes(phrase)) {
|
|
51
|
+
score += 40;
|
|
52
|
+
}
|
|
53
|
+
if (phrase && searchableText.includes(phrase)) {
|
|
54
|
+
score += 15;
|
|
55
|
+
}
|
|
56
|
+
if (parsedQuery.included.length > 1 && matchingTermCount === parsedQuery.included.length) {
|
|
57
|
+
score += 20;
|
|
58
|
+
}
|
|
59
|
+
for (const term of parsedQuery.included) {
|
|
60
|
+
if (title.includes(term)) {
|
|
61
|
+
score += 8;
|
|
62
|
+
}
|
|
63
|
+
if (searchableText.includes(term)) {
|
|
64
|
+
score += 1;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return score;
|
|
68
|
+
};
|
|
69
|
+
const rankLexicalCandidates = (candidates, query) => {
|
|
70
|
+
return [...candidates].sort((left, right) => {
|
|
71
|
+
const scoreDifference = scoreLexicalCandidate(right, query) - scoreLexicalCandidate(left, query);
|
|
72
|
+
if (scoreDifference !== 0) {
|
|
73
|
+
return scoreDifference;
|
|
74
|
+
}
|
|
75
|
+
const updatedAtDifference = right.updatedAt.getTime() - left.updatedAt.getTime();
|
|
76
|
+
return updatedAtDifference !== 0 ? updatedAtDifference : left.id - right.id;
|
|
77
|
+
});
|
|
78
|
+
};
|
|
79
|
+
const listDefaultLexicalNoteIds = async (query, limit) => {
|
|
80
|
+
const where = buildLexicalWhere(query);
|
|
81
|
+
if (!where) {
|
|
82
|
+
return [];
|
|
83
|
+
}
|
|
84
|
+
const [freshCandidates, staleCandidates] = await Promise.all([
|
|
85
|
+
models.note.findMany({
|
|
86
|
+
where,
|
|
87
|
+
select: {
|
|
88
|
+
id: true,
|
|
89
|
+
title: true,
|
|
90
|
+
searchableText: true,
|
|
91
|
+
updatedAt: true
|
|
92
|
+
}
|
|
93
|
+
}),
|
|
94
|
+
models.note.findMany({
|
|
95
|
+
where: { searchableTextVersion: { not: NOTE_SEARCH_TEXT_SCHEMA_VERSION } },
|
|
96
|
+
select: {
|
|
97
|
+
id: true,
|
|
98
|
+
title: true,
|
|
99
|
+
content: true,
|
|
100
|
+
searchableText: true,
|
|
101
|
+
updatedAt: true
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
]);
|
|
105
|
+
if (staleCandidates.length > 0) {
|
|
106
|
+
void runDataMaintenanceInBackground();
|
|
107
|
+
}
|
|
108
|
+
const matchingStaleCandidates = staleCandidates.map((candidate) => ({
|
|
109
|
+
...candidate,
|
|
110
|
+
searchableText: buildNoteSearchText(candidate)
|
|
111
|
+
})).filter((candidate) => matchesHybridLexicalQuery(candidate.searchableText, query));
|
|
112
|
+
return rankLexicalCandidates([...freshCandidates, ...matchingStaleCandidates], query).slice(0, limit).map((candidate) => candidate.id);
|
|
113
|
+
};
|
|
114
|
+
const defaultDependencies = {
|
|
115
|
+
listLexicalNoteIds: listDefaultLexicalNoteIds,
|
|
116
|
+
trySemanticSearch: (query, limit) => getDefaultSemanticSearchManager().trySearch(query, limit),
|
|
117
|
+
findNotesByIds: (ids) => models.note.findMany({ where: { id: { in: ids } } })
|
|
118
|
+
};
|
|
119
|
+
const createHybridNoteSearch = (dependencies = defaultDependencies) => {
|
|
120
|
+
return async ({
|
|
121
|
+
query,
|
|
122
|
+
limit,
|
|
123
|
+
offset,
|
|
124
|
+
mode = "hybrid"
|
|
125
|
+
}) => {
|
|
126
|
+
const normalizedQuery = query.trim();
|
|
127
|
+
if (!normalizedQuery || limit <= 0 || offset < 0) {
|
|
128
|
+
return {
|
|
129
|
+
totalCount: 0,
|
|
130
|
+
notes: [],
|
|
131
|
+
matches: [],
|
|
132
|
+
semanticAvailable: false,
|
|
133
|
+
semanticUsed: false,
|
|
134
|
+
semanticError: null
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
const candidateLimit = MAX_HYBRID_CANDIDATES;
|
|
138
|
+
const semanticCandidateLimit = mode === "hybrid" ? MAX_HYBRID_SEMANTIC_CANDIDATES : MAX_HYBRID_CANDIDATES;
|
|
139
|
+
const [lexicalNoteIds, semanticAttempt] = await Promise.all([
|
|
140
|
+
mode === "semantic" ? Promise.resolve([]) : dependencies.listLexicalNoteIds(normalizedQuery, candidateLimit),
|
|
141
|
+
mode === "lexical" ? Promise.resolve({
|
|
142
|
+
available: false,
|
|
143
|
+
used: false,
|
|
144
|
+
matches: [],
|
|
145
|
+
error: null
|
|
146
|
+
}) : dependencies.trySemanticSearch(normalizedQuery, semanticCandidateLimit).catch((error) => unavailableSemanticAttempt(error))
|
|
147
|
+
]);
|
|
148
|
+
const rankedCandidates = fuseHybridSearchRanks({
|
|
149
|
+
lexicalNoteIds,
|
|
150
|
+
semanticNoteIds: semanticAttempt.matches.slice(0, semanticCandidateLimit).map((match) => match.noteId)
|
|
151
|
+
});
|
|
152
|
+
const pageCandidates = rankedCandidates.slice(offset, offset + limit);
|
|
153
|
+
const notes = await dependencies.findNotesByIds(pageCandidates.map((candidate) => candidate.noteId));
|
|
154
|
+
const notesById = new Map(notes.map((note) => [note.id, note]));
|
|
155
|
+
const existingCandidates = pageCandidates.filter((candidate) => notesById.has(candidate.noteId));
|
|
156
|
+
return {
|
|
157
|
+
totalCount: rankedCandidates.length,
|
|
158
|
+
notes: existingCandidates.map((candidate) => notesById.get(candidate.noteId)),
|
|
159
|
+
matches: existingCandidates.map((candidate) => ({
|
|
160
|
+
noteId: candidate.noteId,
|
|
161
|
+
lexical: candidate.lexicalRank !== null,
|
|
162
|
+
semantic: candidate.semanticRank !== null
|
|
163
|
+
})),
|
|
164
|
+
semanticAvailable: semanticAttempt.available,
|
|
165
|
+
semanticUsed: semanticAttempt.used,
|
|
166
|
+
semanticError: semanticAttempt.error
|
|
167
|
+
};
|
|
168
|
+
};
|
|
169
|
+
};
|
|
170
|
+
const searchNotesHybrid = createHybridNoteSearch();
|
|
171
|
+
export {
|
|
172
|
+
createHybridNoteSearch,
|
|
173
|
+
matchesHybridLexicalQuery,
|
|
174
|
+
rankLexicalCandidates,
|
|
175
|
+
searchNotesHybrid
|
|
176
|
+
};
|
|
@@ -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,186 @@
|
|
|
1
|
+
import models, { Prisma } from "../../models.js";
|
|
2
|
+
const MAX_RELATED_NOTES = 5;
|
|
3
|
+
const normalizeLimit = (limit) => Math.min(MAX_RELATED_NOTES, Math.max(0, Math.trunc(limit)));
|
|
4
|
+
const toDate = (value) => value instanceof Date ? value : new Date(value);
|
|
5
|
+
const buildReasons = (candidate) => {
|
|
6
|
+
const reasons = [];
|
|
7
|
+
if (candidate.linked) {
|
|
8
|
+
reasons.push("Linked from this note");
|
|
9
|
+
}
|
|
10
|
+
if (candidate.backlink) {
|
|
11
|
+
reasons.push("Backlink to this note");
|
|
12
|
+
}
|
|
13
|
+
for (const tagName of candidate.sharedTagNames) {
|
|
14
|
+
reasons.push(`Shares ${tagName}`);
|
|
15
|
+
}
|
|
16
|
+
return reasons;
|
|
17
|
+
};
|
|
18
|
+
const compareCandidates = (left, right) => {
|
|
19
|
+
const leftScore = (left.linked ? 100 : 0) + (left.backlink ? 100 : 0) + left.sharedTagNames.length * 20;
|
|
20
|
+
const rightScore = (right.linked ? 100 : 0) + (right.backlink ? 100 : 0) + right.sharedTagNames.length * 20;
|
|
21
|
+
if (leftScore !== rightScore) {
|
|
22
|
+
return rightScore - leftScore;
|
|
23
|
+
}
|
|
24
|
+
const updatedAtDifference = right.updatedAt.getTime() - left.updatedAt.getTime();
|
|
25
|
+
return updatedAtDifference !== 0 ? updatedAtDifference : left.id - right.id;
|
|
26
|
+
};
|
|
27
|
+
const findCandidatesFromIndex = async ({
|
|
28
|
+
noteId,
|
|
29
|
+
tagIds,
|
|
30
|
+
limit
|
|
31
|
+
}) => {
|
|
32
|
+
const rows = tagIds.length === 0 ? await models.$queryRaw(Prisma.sql`
|
|
33
|
+
WITH candidate_scores AS (
|
|
34
|
+
SELECT
|
|
35
|
+
note."id" AS "id",
|
|
36
|
+
note."title" AS "title",
|
|
37
|
+
note."updatedAt" AS "updatedAt",
|
|
38
|
+
CASE WHEN EXISTS (
|
|
39
|
+
SELECT 1
|
|
40
|
+
FROM "NoteReference" AS outgoing
|
|
41
|
+
WHERE outgoing."sourceNoteId" = ${noteId}
|
|
42
|
+
AND outgoing."targetNoteId" = note."id"
|
|
43
|
+
) THEN 1 ELSE 0 END AS "linked",
|
|
44
|
+
CASE WHEN EXISTS (
|
|
45
|
+
SELECT 1
|
|
46
|
+
FROM "NoteReference" AS incoming
|
|
47
|
+
WHERE incoming."targetNoteId" = ${noteId}
|
|
48
|
+
AND incoming."sourceNoteId" = note."id"
|
|
49
|
+
) THEN 1 ELSE 0 END AS "backlink",
|
|
50
|
+
0 AS "sharedTagCount"
|
|
51
|
+
FROM "Note" AS note
|
|
52
|
+
WHERE note."id" <> ${noteId}
|
|
53
|
+
AND (
|
|
54
|
+
EXISTS (
|
|
55
|
+
SELECT 1
|
|
56
|
+
FROM "NoteReference" AS outgoing
|
|
57
|
+
WHERE outgoing."sourceNoteId" = ${noteId}
|
|
58
|
+
AND outgoing."targetNoteId" = note."id"
|
|
59
|
+
)
|
|
60
|
+
OR EXISTS (
|
|
61
|
+
SELECT 1
|
|
62
|
+
FROM "NoteReference" AS incoming
|
|
63
|
+
WHERE incoming."targetNoteId" = ${noteId}
|
|
64
|
+
AND incoming."sourceNoteId" = note."id"
|
|
65
|
+
)
|
|
66
|
+
)
|
|
67
|
+
)
|
|
68
|
+
SELECT *, ("linked" * 100 + "backlink" * 100 + "sharedTagCount" * 20) AS "score"
|
|
69
|
+
FROM candidate_scores
|
|
70
|
+
ORDER BY "score" DESC, "updatedAt" DESC, "id" ASC
|
|
71
|
+
LIMIT ${limit}
|
|
72
|
+
`) : await models.$queryRaw(Prisma.sql`
|
|
73
|
+
WITH candidate_scores AS (
|
|
74
|
+
SELECT
|
|
75
|
+
note."id" AS "id",
|
|
76
|
+
note."title" AS "title",
|
|
77
|
+
note."updatedAt" AS "updatedAt",
|
|
78
|
+
CASE WHEN EXISTS (
|
|
79
|
+
SELECT 1
|
|
80
|
+
FROM "NoteReference" AS outgoing
|
|
81
|
+
WHERE outgoing."sourceNoteId" = ${noteId}
|
|
82
|
+
AND outgoing."targetNoteId" = note."id"
|
|
83
|
+
) THEN 1 ELSE 0 END AS "linked",
|
|
84
|
+
CASE WHEN EXISTS (
|
|
85
|
+
SELECT 1
|
|
86
|
+
FROM "NoteReference" AS incoming
|
|
87
|
+
WHERE incoming."targetNoteId" = ${noteId}
|
|
88
|
+
AND incoming."sourceNoteId" = note."id"
|
|
89
|
+
) THEN 1 ELSE 0 END AS "backlink",
|
|
90
|
+
COUNT(DISTINCT noteTags."B") AS "sharedTagCount"
|
|
91
|
+
FROM "Note" AS note
|
|
92
|
+
LEFT JOIN "_NoteToTag" AS noteTags
|
|
93
|
+
ON noteTags."A" = note."id"
|
|
94
|
+
AND noteTags."B" IN (${Prisma.join(tagIds)})
|
|
95
|
+
WHERE note."id" <> ${noteId}
|
|
96
|
+
AND (
|
|
97
|
+
EXISTS (
|
|
98
|
+
SELECT 1
|
|
99
|
+
FROM "NoteReference" AS outgoing
|
|
100
|
+
WHERE outgoing."sourceNoteId" = ${noteId}
|
|
101
|
+
AND outgoing."targetNoteId" = note."id"
|
|
102
|
+
)
|
|
103
|
+
OR EXISTS (
|
|
104
|
+
SELECT 1
|
|
105
|
+
FROM "NoteReference" AS incoming
|
|
106
|
+
WHERE incoming."targetNoteId" = ${noteId}
|
|
107
|
+
AND incoming."sourceNoteId" = note."id"
|
|
108
|
+
)
|
|
109
|
+
OR noteTags."B" IS NOT NULL
|
|
110
|
+
)
|
|
111
|
+
GROUP BY note."id", note."title", note."updatedAt"
|
|
112
|
+
)
|
|
113
|
+
SELECT *, ("linked" * 100 + "backlink" * 100 + "sharedTagCount" * 20) AS "score"
|
|
114
|
+
FROM candidate_scores
|
|
115
|
+
ORDER BY "score" DESC, "updatedAt" DESC, "id" ASC
|
|
116
|
+
LIMIT ${limit}
|
|
117
|
+
`);
|
|
118
|
+
if (rows.length === 0) {
|
|
119
|
+
return [];
|
|
120
|
+
}
|
|
121
|
+
const candidateIds = rows.map((row) => row.id);
|
|
122
|
+
const candidateNotes = await models.note.findMany({
|
|
123
|
+
where: { id: { in: candidateIds } },
|
|
124
|
+
select: {
|
|
125
|
+
id: true,
|
|
126
|
+
tags: {
|
|
127
|
+
select: {
|
|
128
|
+
id: true,
|
|
129
|
+
name: true
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
const tagsByNoteId = new Map(candidateNotes.map((note) => [note.id, note.tags]));
|
|
135
|
+
const sourceTagIds = new Set(tagIds);
|
|
136
|
+
return rows.map((row) => ({
|
|
137
|
+
id: row.id,
|
|
138
|
+
title: row.title || "Untitled",
|
|
139
|
+
updatedAt: toDate(row.updatedAt),
|
|
140
|
+
linked: Number(row.linked) === 1,
|
|
141
|
+
backlink: Number(row.backlink) === 1,
|
|
142
|
+
sharedTagNames: (tagsByNoteId.get(row.id) ?? []).filter((tag) => sourceTagIds.has(tag.id)).map((tag) => tag.name).sort((left, right) => left.localeCompare(right))
|
|
143
|
+
}));
|
|
144
|
+
};
|
|
145
|
+
const defaultDependencies = {
|
|
146
|
+
findSourceNote: (noteId) => models.note.findUnique({
|
|
147
|
+
where: { id: noteId },
|
|
148
|
+
select: {
|
|
149
|
+
id: true,
|
|
150
|
+
tags: {
|
|
151
|
+
select: {
|
|
152
|
+
id: true,
|
|
153
|
+
name: true
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}),
|
|
158
|
+
findCandidates: findCandidatesFromIndex
|
|
159
|
+
};
|
|
160
|
+
const createSearchRelatedNotes = (dependencies = defaultDependencies) => {
|
|
161
|
+
return async (noteId, limit = MAX_RELATED_NOTES) => {
|
|
162
|
+
const normalizedLimit = normalizeLimit(limit);
|
|
163
|
+
if (!Number.isSafeInteger(noteId) || noteId <= 0 || normalizedLimit === 0) {
|
|
164
|
+
return [];
|
|
165
|
+
}
|
|
166
|
+
const source = await dependencies.findSourceNote(noteId);
|
|
167
|
+
if (!source) {
|
|
168
|
+
return [];
|
|
169
|
+
}
|
|
170
|
+
const candidates = await dependencies.findCandidates({
|
|
171
|
+
noteId,
|
|
172
|
+
tagIds: source.tags.map((tag) => tag.id),
|
|
173
|
+
limit: normalizedLimit
|
|
174
|
+
});
|
|
175
|
+
return candidates.filter((candidate) => candidate.linked || candidate.backlink || candidate.sharedTagNames.length > 0).sort(compareCandidates).slice(0, normalizedLimit).map((candidate) => ({
|
|
176
|
+
id: candidate.id,
|
|
177
|
+
title: candidate.title || "Untitled",
|
|
178
|
+
reasons: buildReasons(candidate)
|
|
179
|
+
}));
|
|
180
|
+
};
|
|
181
|
+
};
|
|
182
|
+
const searchRelatedNotes = createSearchRelatedNotes();
|
|
183
|
+
export {
|
|
184
|
+
createSearchRelatedNotes,
|
|
185
|
+
searchRelatedNotes
|
|
186
|
+
};
|
|
@@ -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
|
+
};
|