From 11cc9bb050c8c6f02dbf3e79c8c8db7768d95cd8 Mon Sep 17 00:00:00 2001 From: Harry Vangberg Date: Fri, 24 Jan 2025 10:31:25 +0100 Subject: [PATCH 1/7] wip --- package-lock.json | 901 ++++++++++++++++++- package.json | 5 +- rfc/005-semantic-search.md | 57 ++ script/huggingfaceDownload.ts | 3 + src/skrift-electron/main/ipc.ts | 8 +- src/skrift/huggingface.ts | 14 + src/skrift/notes_db/computeEmbedding.test.ts | 14 + src/skrift/notes_db/delete.test.ts | 6 +- src/skrift/notes_db/get.test.ts | 18 +- src/skrift/notes_db/getNoteLinks.test.ts | 8 +- src/skrift/notes_db/index.ts | 75 +- src/skrift/notes_db/initialize.test.ts | 4 +- src/skrift/notes_db/save.test.ts | 18 +- src/skrift/notes_db/search.test.ts | 6 +- src/skrift/notes_db/semanticSearch.test.ts | 43 + 15 files changed, 1136 insertions(+), 44 deletions(-) create mode 100644 rfc/005-semantic-search.md create mode 100644 script/huggingfaceDownload.ts create mode 100644 src/skrift/huggingface.ts create mode 100644 src/skrift/notes_db/computeEmbedding.test.ts create mode 100644 src/skrift/notes_db/semanticSearch.test.ts diff --git a/package-lock.json b/package-lock.json index 4e0adc1..8ac1caa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.0.3", "@benrbray/prosemirror-math": "^1.0.0", "@handlewithcare/remark-prosemirror": "^0.1.5", + "@huggingface/transformers": "^3.3.1", "@nytimes/react-prosemirror": "^1.0.0", "@types/better-sqlite3": "^7.6.12", "@types/markdown-it": "^14.1.2", @@ -62,6 +63,7 @@ "remark-math": "^6.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", + "sqlite-vec": "^0.1.7-alpha.2", "style-loader": "^4.0.0", "tailwindcss": "^3.4.16", "tiny-invariant": "^1.3.3", @@ -523,6 +525,17 @@ "node": ">= 10.0.0" } }, + "node_modules/@emnapi/runtime": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.3.1.tgz", + "integrity": "sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.23.1", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz", @@ -1139,6 +1152,29 @@ "prosemirror-model": "^1.24.0" } }, + "node_modules/@huggingface/jinja": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@huggingface/jinja/-/jinja-0.3.2.tgz", + "integrity": "sha512-F2FvuIc+w1blGsaqJI/OErRbWH6bVJDCBI8Rm5D86yZ2wlwrGERsfIaru7XUv9eYC3DMP3ixDRRtF0h6d8AZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@huggingface/transformers": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@huggingface/transformers/-/transformers-3.3.2.tgz", + "integrity": "sha512-KewnlOEeB3LcgvS416rTsLiah98V7sP1STmE584wA2qlymHLjp0QXihAKNA37XQ8y19thK7VjUqHYWSAUg9isg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@huggingface/jinja": "^0.3.2", + "onnxruntime-node": "1.20.1", + "onnxruntime-web": "1.21.0-dev.20250114-228dd16893", + "sharp": "^0.33.5" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -1205,6 +1241,386 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", + "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", + "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", + "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", + "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", + "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", + "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", + "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", + "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", + "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", + "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", + "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.0.5" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", + "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", + "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", + "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", + "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", + "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", + "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.2.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", + "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", + "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -1302,10 +1718,33 @@ "strip-ansi": "^7.0.1" }, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@isaacs/fs-minipass/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" } }, "node_modules/@jridgewell/gen-mapping": { @@ -1609,6 +2048,80 @@ "node": ">=14" } }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.31.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.31.0.tgz", @@ -4723,6 +5236,20 @@ "node": ">=6" } }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -4743,6 +5270,17 @@ "dev": true, "license": "MIT" }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, "node_modules/color-support": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", @@ -7182,6 +7720,13 @@ "node": ">=16" } }, + "node_modules/flatbuffers": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/flatbuffers/-/flatbuffers-1.12.0.tgz", + "integrity": "sha512-c7CZADjRcl6j0PlvFy0ZqXQ67qSEZfrVPynmnL+2zPc+NtMvrF8Y0QceMo7QqnSPc7+uWjUIAbvCQ5WIKlMVdQ==", + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt" + }, "node_modules/flatted": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz", @@ -7716,6 +8261,13 @@ "dev": true, "license": "MIT" }, + "node_modules/guid-typescript": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/guid-typescript/-/guid-typescript-1.0.9.tgz", + "integrity": "sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ==", + "dev": true, + "license": "ISC" + }, "node_modules/handle-thing": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", @@ -9483,6 +10035,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/long": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.4.tgz", + "integrity": "sha512-qtzLbJE8hq7VabR3mISmVGtoXP8KGc2Z/AT8OuqlYD7JTR3oqrgwdjnk07wpj1twXxYmgDXgoKVWUG/fReSzHg==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/longest-streak": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", @@ -10981,6 +11540,167 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/onnxruntime-common": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.20.1.tgz", + "integrity": "sha512-YiU0s0IzYYC+gWvqD1HzLc46Du1sXpSiwzKb63PACIJr6LfL27VsXSXQvt68EzD3V0D5Bc0vyJTjmMxp0ylQiw==", + "dev": true, + "license": "MIT" + }, + "node_modules/onnxruntime-node": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/onnxruntime-node/-/onnxruntime-node-1.20.1.tgz", + "integrity": "sha512-di/I4HDXRw+FLgq+TyHmQEDd3cEp9iFFZm0r4uJ1Wd7b/WE1VXtKWo8yemex347c6GNF/3Pv86ZfPhIWxORr0w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "os": [ + "win32", + "darwin", + "linux" + ], + "dependencies": { + "onnxruntime-common": "1.20.1", + "tar": "^7.0.1" + } + }, + "node_modules/onnxruntime-node/node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/onnxruntime-node/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/onnxruntime-node/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/onnxruntime-node/node_modules/minizlib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.1.tgz", + "integrity": "sha512-umcy022ILvb5/3Djuu8LWeqUa8D68JaBzlttKeMWen48SjabqS3iY5w/vzeMzMUNhLDifyhbOwKDSznB1vvrwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.0.4", + "rimraf": "^5.0.5" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/onnxruntime-node/node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/onnxruntime-node/node_modules/rimraf": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", + "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/onnxruntime-node/node_modules/tar": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", + "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/onnxruntime-node/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/onnxruntime-web": { + "version": "1.21.0-dev.20250114-228dd16893", + "resolved": "https://registry.npmjs.org/onnxruntime-web/-/onnxruntime-web-1.21.0-dev.20250114-228dd16893.tgz", + "integrity": "sha512-fUnedxS63NYwNkQJlvdD55jVcOtyM+Qzw1SGt9Pj3jZVaIwR4mltx/5C0yvwdue44BTSV7M5Q0qnhL6/30ewqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatbuffers": "^1.12.0", + "guid-typescript": "^1.0.9", + "long": "^5.2.3", + "onnxruntime-common": "1.21.0-dev.20241212-1f88284f96", + "platform": "^1.3.6", + "protobufjs": "^7.2.4" + } + }, + "node_modules/onnxruntime-web/node_modules/onnxruntime-common": { + "version": "1.21.0-dev.20241212-1f88284f96", + "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.21.0-dev.20241212-1f88284f96.tgz", + "integrity": "sha512-zD6mQJfgeezbNKV2fiN/ZqB+LKdixJ7sKc5vu6PdqMU+bZk581g5XqrhoYNwe/RDJdFGQSMKK9+gUg4Mep+jKw==", + "dev": true, + "license": "MIT" + }, "node_modules/open": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/open/-/open-10.1.0.tgz", @@ -11462,6 +12182,13 @@ "node": ">=8" } }, + "node_modules/platform": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz", + "integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==", + "dev": true, + "license": "MIT" + }, "node_modules/plist": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", @@ -12015,6 +12742,31 @@ "prosemirror-transform": "^1.1.0" } }, + "node_modules/protobufjs": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz", + "integrity": "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==", + "dev": true, + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -13156,6 +13908,46 @@ "node": ">=8" } }, + "node_modules/sharp": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", + "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.3", + "semver": "^7.6.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.33.5", + "@img/sharp-darwin-x64": "0.33.5", + "@img/sharp-libvips-darwin-arm64": "1.0.4", + "@img/sharp-libvips-darwin-x64": "1.0.4", + "@img/sharp-libvips-linux-arm": "1.0.5", + "@img/sharp-libvips-linux-arm64": "1.0.4", + "@img/sharp-libvips-linux-s390x": "1.0.4", + "@img/sharp-libvips-linux-x64": "1.0.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", + "@img/sharp-libvips-linuxmusl-x64": "1.0.4", + "@img/sharp-linux-arm": "0.33.5", + "@img/sharp-linux-arm64": "0.33.5", + "@img/sharp-linux-s390x": "0.33.5", + "@img/sharp-linux-x64": "0.33.5", + "@img/sharp-linuxmusl-arm64": "0.33.5", + "@img/sharp-linuxmusl-x64": "0.33.5", + "@img/sharp-wasm32": "0.33.5", + "@img/sharp-win32-ia32": "0.33.5", + "@img/sharp-win32-x64": "0.33.5" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -13327,6 +14119,23 @@ "simple-concat": "^1.0.0" } }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/simple-swizzle/node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "dev": true, + "license": "MIT" + }, "node_modules/simple-update-notifier": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", @@ -13503,6 +14312,90 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/sqlite-vec": { + "version": "0.1.7-alpha.2", + "resolved": "https://registry.npmjs.org/sqlite-vec/-/sqlite-vec-0.1.7-alpha.2.tgz", + "integrity": "sha512-rNgRCv+4V4Ed3yc33Qr+nNmjhtrMnnHzXfLVPeGb28Dx5mmDL3Ngw/Wk8vhCGjj76+oC6gnkmMG8y73BZWGBwQ==", + "dev": true, + "license": "MIT OR Apache", + "optionalDependencies": { + "sqlite-vec-darwin-arm64": "0.1.7-alpha.2", + "sqlite-vec-darwin-x64": "0.1.7-alpha.2", + "sqlite-vec-linux-arm64": "0.1.7-alpha.2", + "sqlite-vec-linux-x64": "0.1.7-alpha.2", + "sqlite-vec-windows-x64": "0.1.7-alpha.2" + } + }, + "node_modules/sqlite-vec-darwin-arm64": { + "version": "0.1.7-alpha.2", + "resolved": "https://registry.npmjs.org/sqlite-vec-darwin-arm64/-/sqlite-vec-darwin-arm64-0.1.7-alpha.2.tgz", + "integrity": "sha512-raIATOqFYkeCHhb/t3r7W7Cf2lVYdf4J3ogJ6GFc8PQEgHCPEsi+bYnm2JT84MzLfTlSTIdxr4/NKv+zF7oLPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/sqlite-vec-darwin-x64": { + "version": "0.1.7-alpha.2", + "resolved": "https://registry.npmjs.org/sqlite-vec-darwin-x64/-/sqlite-vec-darwin-x64-0.1.7-alpha.2.tgz", + "integrity": "sha512-jeZEELsQjjRsVojsvU5iKxOvkaVuE+JYC8Y4Ma8U45aAERrDYmqZoHvgSG7cg1PXL3bMlumFTAmHynf1y4pOzA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/sqlite-vec-linux-arm64": { + "version": "0.1.7-alpha.2", + "resolved": "https://registry.npmjs.org/sqlite-vec-linux-arm64/-/sqlite-vec-linux-arm64-0.1.7-alpha.2.tgz", + "integrity": "sha512-6Spj4Nfi7tG13jsUG+W7jnT0bCTWbyPImu2M8nWp20fNrd1SZ4g3CSlDAK8GBdavX7wRlbBHCZ+BDa++rbDewA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/sqlite-vec-linux-x64": { + "version": "0.1.7-alpha.2", + "resolved": "https://registry.npmjs.org/sqlite-vec-linux-x64/-/sqlite-vec-linux-x64-0.1.7-alpha.2.tgz", + "integrity": "sha512-IcgrbHaDccTVhXDf8Orwdc2+hgDLAFORl6OBUhcvlmwswwBP1hqBTSEhovClG4NItwTOBNgpwOoQ7Qp3VDPWLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/sqlite-vec-windows-x64": { + "version": "0.1.7-alpha.2", + "resolved": "https://registry.npmjs.org/sqlite-vec-windows-x64/-/sqlite-vec-windows-x64-0.1.7-alpha.2.tgz", + "integrity": "sha512-TRP6hTjAcwvQ6xpCZvjP00pdlda8J38ArFy1lMYhtQWXiIBmWnhMaMbq4kaeCYwvTTddfidatRS+TJrwIKB/oQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/ssri": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/ssri/-/ssri-9.0.1.tgz", diff --git a/package.json b/package.json index 752fdaf..5eb7dc2 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,8 @@ "test": "ELECTRON_RUN_AS_NODE=1 electron --experimental-vm-modules ./node_modules/.bin/vitest run", "test-watch": "ELECTRON_RUN_AS_NODE=1 electron --experimental-vm-modules ./node_modules/.bin/vitest", "postinstall": "electron-builder install-app-deps", - "notarize": "./notarize.sh" + "notarize": "./notarize.sh", + "huggingface:download": "tsx script/huggingfaceDownload.ts" }, "build": { "appId": "com.halestreet.Skrift", @@ -76,6 +77,7 @@ "@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.0.3", "@benrbray/prosemirror-math": "^1.0.0", "@handlewithcare/remark-prosemirror": "^0.1.5", + "@huggingface/transformers": "^3.3.1", "@nytimes/react-prosemirror": "^1.0.0", "@types/better-sqlite3": "^7.6.12", "@types/markdown-it": "^14.1.2", @@ -121,6 +123,7 @@ "remark-math": "^6.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", + "sqlite-vec": "^0.1.7-alpha.2", "style-loader": "^4.0.0", "tailwindcss": "^3.4.16", "tiny-invariant": "^1.3.3", diff --git a/rfc/005-semantic-search.md b/rfc/005-semantic-search.md new file mode 100644 index 0000000..0e395fe --- /dev/null +++ b/rfc/005-semantic-search.md @@ -0,0 +1,57 @@ +# Semantic Search + +Semantic search lets the user search by content, matching meaning rather than keywords. +We will add semantic search to complement the existing full-text search. + +## Compute embeddings + +Add a function `NotesDB.computeEmbedding` that computes the embedding for a note. +Use the package `@huggingface/transformers` to compute the embedding. Use the +`all-MiniLM-L6-v2` model. + +## Store embeddings + +Use `sqlite-vec` to store the embeddings in a table like this: + +```sql +CREATE VIRTUAL TABLE IF NOT EXISTS notes_embeddings USING vec0( + embedding float[384] +) +``` + +When a note is saved with `NotesDB.save`, compute the embedding and store it in the +`notes_embeddings` table. + +## Search + +Performing a semantic is done with `NotesDB.searchSemantic: (query: string) => Promise`. +It will perform a similarity search on the `notes_embeddings` table. The distance search could +be inspired by this Python code: + +``` +def search_semantic(conn, term, top_k=10): + query = embedding_model().encode(term) + + df = pd.read_sql_query( + """ + SELECT + f.filename, + f.title, + e.distance AS score, + ROW_NUMBER() OVER (ORDER BY e.distance ASC) AS rank + FROM files f + INNER JOIN ( + SELECT rowid, distance + FROM files_embeddings + WHERE embedding MATCH ? + ORDER BY distance ASC + LIMIT ? + ) e ON f.id = e.rowid + ORDER BY e.distance ASC + """, + conn, + params=(serialize_f32(query), top_k), + ) + + return df +``` \ No newline at end of file diff --git a/script/huggingfaceDownload.ts b/script/huggingfaceDownload.ts new file mode 100644 index 0000000..45c73ed --- /dev/null +++ b/script/huggingfaceDownload.ts @@ -0,0 +1,3 @@ +import { HuggingFace } from "../src/skrift/huggingface.js"; + +HuggingFace.preload(); \ No newline at end of file diff --git a/src/skrift-electron/main/ipc.ts b/src/skrift-electron/main/ipc.ts index 8384374..4a6c705 100644 --- a/src/skrift-electron/main/ipc.ts +++ b/src/skrift-electron/main/ipc.ts @@ -81,14 +81,14 @@ const handleDeleteNote = ( reply(event, { type: "event/DELETED_NOTE", id }); }; -const handleAddNote = ( +const handleAddNote = async ( event: Electron.IpcMainEvent, cmd: IpcAddNoteCommand ) => { const { id, markdown } = cmd; const db = getDB(); - NotesDB.save(db, id, markdown); + await NotesDB.save(db, id, markdown); NotesFS.save(_path, id, markdown); @@ -97,7 +97,7 @@ const handleAddNote = ( reply(event, { type: "event/SET_NOTE", note }); }; -const handleSetNote = ( +const handleSetNote = async ( event: Electron.IpcMainEvent, cmd: IpcSetNoteCommand ) => { @@ -106,7 +106,7 @@ const handleSetNote = ( const noteBefore = NotesDB.get(db, id); - NotesDB.save(db, id, markdown); + await NotesDB.save(db, id, markdown); NotesFS.save(_path, id, markdown); diff --git a/src/skrift/huggingface.ts b/src/skrift/huggingface.ts new file mode 100644 index 0000000..66061b9 --- /dev/null +++ b/src/skrift/huggingface.ts @@ -0,0 +1,14 @@ +import { AutoModel } from "@huggingface/transformers"; + +export const HuggingFace = { + preload: async () => { + const models = Object.values(HuggingFace.models); + for (const model of models) { + AutoModel.from_pretrained(model); + } + }, + + models: { + sentenceTransformer: "Xenova/all-MiniLM-L6-v2" + } +} \ No newline at end of file diff --git a/src/skrift/notes_db/computeEmbedding.test.ts b/src/skrift/notes_db/computeEmbedding.test.ts new file mode 100644 index 0000000..988b62e --- /dev/null +++ b/src/skrift/notes_db/computeEmbedding.test.ts @@ -0,0 +1,14 @@ +import { describe, expect, it } from "vitest"; + +import { NotesDB } from "./index.js"; + +describe("NotesDB.computeEmbedding", () => { + it("should compute embeddings with expected dimensions", async () => { + const markdown = "This is a test note for embedding generation"; + const embedding = await NotesDB.computeEmbedding(markdown); + + // The model Xenova/all-MiniLM-L6-v2 produces 384-dimensional embeddings + expect(embedding).toBeInstanceOf(Float32Array); + expect(embedding.length).toBe(384); + }); +}); \ No newline at end of file diff --git a/src/skrift/notes_db/delete.test.ts b/src/skrift/notes_db/delete.test.ts index 5605df7..a2cafbd 100644 --- a/src/skrift/notes_db/delete.test.ts +++ b/src/skrift/notes_db/delete.test.ts @@ -5,13 +5,13 @@ import { NotesDB, NoteNotFoundError } from "./index.js"; describe("NotesDB.delete()", () => { let db: BetterSqlite3.Database; - beforeAll(() => { + beforeEach(() => { db = NotesDB.memory(); NotesDB.initialize(db); }); - test("deletes a note", () => { - NotesDB.save( + test("deletes a note", async () => { + await NotesDB.save( db, "a.md", "# Added note\n\nLinks: [#](b.md), [#](c.md)", diff --git a/src/skrift/notes_db/get.test.ts b/src/skrift/notes_db/get.test.ts index 258f486..7ed4156 100644 --- a/src/skrift/notes_db/get.test.ts +++ b/src/skrift/notes_db/get.test.ts @@ -5,14 +5,14 @@ import { NotesDB, NoteNotFoundError } from "./index.js"; describe("NotesDB.get()", () => { let db: BetterSqlite3.Database; - beforeAll(() => { + beforeEach(() => { db = NotesDB.memory(); NotesDB.initialize(db); }); - test("gets a note", () => { + test("gets a note", async () => { const date = new Date(); - NotesDB.save( + await NotesDB.save( db, "a.md", "# Added note\n\nLinks: [#](b.md), [#](c.md)", @@ -26,10 +26,10 @@ describe("NotesDB.get()", () => { expect(result.modifiedAt).toEqual(date); }); - test("gets a note with links", () => { - NotesDB.save(db, "a.md", "Alpha\n\n[#](b.md) [#](c.md)"); - NotesDB.save(db, "b.md", "Beta\n\n[#](a.md)"); - NotesDB.save(db, "c.md", "Charlie"); + test("gets a note with links", async () => { + await NotesDB.save(db, "a.md", "Alpha\n\n[#](b.md) [#](c.md)"); + await NotesDB.save(db, "b.md", "Beta\n\n[#](a.md)"); + await NotesDB.save(db, "c.md", "Charlie"); const result = NotesDB.getWithLinks(db, "a.md"); expect(result.links.sort()).toEqual( @@ -43,8 +43,8 @@ describe("NotesDB.get()", () => { test("gets backlinks", async () => { const date = new Date(); - NotesDB.save(db, "a.md", "[#](b.md)", date); - NotesDB.save(db, "b.md", "# B", date); + await NotesDB.save(db, "a.md", "[#](b.md)", date); + await NotesDB.save(db, "b.md", "# B", date); const result = NotesDB.get(db, "b.md"); expect(result.backlinkIds).toEqual(new Set(["a.md"])); diff --git a/src/skrift/notes_db/getNoteLinks.test.ts b/src/skrift/notes_db/getNoteLinks.test.ts index cc9bd32..7c0a197 100644 --- a/src/skrift/notes_db/getNoteLinks.test.ts +++ b/src/skrift/notes_db/getNoteLinks.test.ts @@ -5,15 +5,15 @@ import { NotesDB } from "./index.js"; describe("NotesDB.getNoteLinks()", () => { let db: BetterSqlite3.Database; - beforeAll(() => { + beforeEach(() => { db = NotesDB.memory(); NotesDB.initialize(db); }); - test("gets note links in specified order", () => { + test("gets note links in specified order", async () => { const date = new Date(); - NotesDB.save(db, "a.md", "A", date); - NotesDB.save(db, "b.md", "B", date); + await NotesDB.save(db, "a.md", "A", date); + await NotesDB.save(db, "b.md", "B", date); const result = NotesDB.getNoteLinks(db, ["b.md", "a.md"]); diff --git a/src/skrift/notes_db/index.ts b/src/skrift/notes_db/index.ts index 49a9ff5..f7dff62 100644 --- a/src/skrift/notes_db/index.ts +++ b/src/skrift/notes_db/index.ts @@ -1,9 +1,11 @@ import BetterSqlite3 from "better-sqlite3"; +import * as sqliteVec from "sqlite-vec"; import { Note, NoteID, NoteLink, NoteWithLinks } from "../note/index.js"; import path from "path"; import { Fts } from "./fts.js"; import { ParsedNote } from "../note/fromMarkdown.js"; import { createHash } from "crypto"; +import { pipeline } from "@huggingface/transformers"; export interface NoteRow { id: string; @@ -25,6 +27,11 @@ export interface SearchRow { modifiedAt: string; } +export interface EmbeddingRow { + id: string; + embedding: Float32Array; +} + export interface LinkRow { fromId: string; toId: string; @@ -48,6 +55,8 @@ export const NotesDB = { }, initialize(db: BetterSqlite3.Database): void { + sqliteVec.load(db) + db.prepare( `CREATE VIRTUAL TABLE IF NOT EXISTS notes USING fts5( id UNINDEXED, @@ -55,6 +64,13 @@ export const NotesDB = { markdown, modifiedAt UNINDEXED, checksum UNINDEXED + ); + ` + ).run(); + + db.prepare( + `CREATE VIRTUAL TABLE IF NOT EXISTS notes_embeddings USING vec0( + embedding float[384] )` ).run(); @@ -67,21 +83,39 @@ export const NotesDB = { ).run(); }, - save( + dropTables(db: BetterSqlite3.Database): void { + db.prepare("DROP TABLE IF EXISTS notes").run(); + db.prepare("DROP TABLE IF EXISTS notes_embeddings").run(); + db.prepare("DROP TABLE IF EXISTS links").run(); + }, + + getRowid(db: BetterSqlite3.Database, id: NoteID): bigint { + const result = db.prepare("SELECT rowid FROM notes WHERE id = ?").run(id); + return BigInt(result.lastInsertRowid); + }, + + async save( db: BetterSqlite3.Database, id: NoteID, markdown: string, modifiedAt?: Date - ): void { + ): Promise { const note = Note.fromMarkdown(markdown); const checksum = NotesDB.computeChecksum(markdown); + const embedding = await NotesDB.computeEmbedding(markdown); return db.transaction((note: ParsedNote) => { db.prepare("DELETE FROM notes WHERE id = ?").run(id); db.prepare("DELETE FROM links WHERE fromId = ?").run(id); - db.prepare( + db.prepare("DELETE FROM notes_embeddings WHERE rowid = ?").run(NotesDB.getRowid(db, id)); + + const result = db.prepare( `INSERT INTO notes (id, title, markdown, modifiedAt, checksum) VALUES (?, ?, ?, ?, ?)` ).run([id, note.title, markdown, (modifiedAt || new Date()).getTime(), checksum]); + + const rowid = result.lastInsertRowid; + db.prepare("INSERT INTO notes_embeddings (rowid, embedding) VALUES (?, ?)").run([BigInt(rowid), embedding]); + note.linkIds.forEach((link) => db.prepare("INSERT INTO links (fromId, toId) VALUES (?, ?)").run([id, link]) ); @@ -144,8 +178,11 @@ export const NotesDB = { }, delete(db: BetterSqlite3.Database, id: NoteID): void { + const rowid = NotesDB.getRowid(db, id); + db.prepare(`DELETE FROM notes WHERE id = ?`).run(id); db.prepare(`DELETE FROM links WHERE toId = ? OR fromId = ?`).run(id, id); + db.prepare(`DELETE FROM notes_embeddings WHERE rowid = ?`).run(rowid); }, search(db: BetterSqlite3.Database, query: string): NoteID[] { @@ -166,6 +203,27 @@ export const NotesDB = { return rows.map((row) => row.id); }, + async searchSemantic(db: BetterSqlite3.Database, query: string, topK: number = 10): Promise { + const embedding = await NotesDB.computeEmbedding(query); + + const rows = db.prepare(` + SELECT + id, + ROW_NUMBER() OVER (ORDER BY e.distance ASC) AS rank + FROM notes n + INNER JOIN ( + SELECT rowid, distance + FROM notes_embeddings + WHERE embedding MATCH ? + ORDER BY distance ASC + LIMIT ? + ) e ON n.id = e.rowid + ORDER BY e.distance ASC + `).all(embedding); + + return rows.map((row) => row.id as NoteID); + }, + recent(db: BetterSqlite3.Database): NoteID[] { const rows = db .prepare<[], SearchRow>( @@ -180,6 +238,13 @@ export const NotesDB = { return createHash("md5").update(markdown).digest("hex"); }, + // Does this really belong in `NotesDB`? + async computeEmbedding(markdown: string): Promise { + const model = await pipeline("feature-extraction", "Xenova/all-MiniLM-L6-v2", { dtype: "fp16" }); + const output = await model(markdown, { pooling: "mean", normalize: true }); + return new Float32Array(output.data); + }, + async *loadDir(db: BetterSqlite3.Database, notes: AsyncIterable): AsyncGenerator { const existingNotes = new Set( db.prepare<[], { id: string }>("SELECT id FROM notes").all().map(row => row.id) @@ -193,12 +258,12 @@ export const NotesDB = { if (!existing) { // New note - NotesDB.save(db, note.id, note.markdown, note.modifiedAt); + await NotesDB.save(db, note.id, note.markdown, note.modifiedAt); loaded++; batchSize++; } else if (existing.checksum !== checksum) { // Modified note - NotesDB.save(db, note.id, note.markdown, note.modifiedAt); + await NotesDB.save(db, note.id, note.markdown, note.modifiedAt); loaded++; batchSize++; } diff --git a/src/skrift/notes_db/initialize.test.ts b/src/skrift/notes_db/initialize.test.ts index a010e2b..21d9b1b 100644 --- a/src/skrift/notes_db/initialize.test.ts +++ b/src/skrift/notes_db/initialize.test.ts @@ -1,11 +1,11 @@ -import { describe, beforeAll, expect, test } from 'vitest'; +import { describe, beforeAll, expect, test, beforeEach } from 'vitest'; import BetterSqlite3 from "better-sqlite3"; import { NotesDB } from "./index.js"; describe("NotesDB.initialize()", () => { let db: BetterSqlite3.Database; - beforeAll(() => { + beforeEach(() => { db = NotesDB.memory(); NotesDB.initialize(db); }); diff --git a/src/skrift/notes_db/save.test.ts b/src/skrift/notes_db/save.test.ts index c4c37ae..2af8e3f 100644 --- a/src/skrift/notes_db/save.test.ts +++ b/src/skrift/notes_db/save.test.ts @@ -10,8 +10,8 @@ describe("NotesDB.save()", () => { NotesDB.initialize(db); }); - test("inserts a new note", () => { - NotesDB.save(db, "a.md", "# Added note"); + test("inserts a new note", async () => { + await NotesDB.save(db, "a.md", "# Added note"); const rows = db.prepare( `SELECT * FROM notes WHERE title = ?` @@ -19,9 +19,9 @@ describe("NotesDB.save()", () => { expect(rows.length).toEqual(1); }); - test("updates an existing note", () => { - NotesDB.save(db, "a.md", "New note"); - NotesDB.save(db, "a.md", "Updated note"); + test("updates an existing note", async () => { + await NotesDB.save(db, "a.md", "New note"); + await NotesDB.save(db, "a.md", "Updated note"); const rows = db.prepare( `SELECT * FROM notes WHERE id = ?` @@ -30,8 +30,8 @@ describe("NotesDB.save()", () => { expect(rows[0].title).toEqual("Updated note"); }); - test("inserts new links", () => { - NotesDB.save(db, "a.md", "[#](b.md) [#](c.md)"); + test("inserts new links", async () => { + await NotesDB.save(db, "a.md", "[#](b.md) [#](c.md)"); const rows = db.prepare( `SELECT * FROM links WHERE fromId = ?` @@ -41,8 +41,8 @@ describe("NotesDB.save()", () => { }); test("deletes and updates existing links", async () => { - NotesDB.save(db, "a.md", "[#](b.md) [#](c.md)"); - NotesDB.save(db, "a.md", "[#](b.md) [#](d.md)"); + await NotesDB.save(db, "a.md", "[#](b.md) [#](c.md)"); + await NotesDB.save(db, "a.md", "[#](b.md) [#](d.md)"); const rows = db.prepare( `SELECT * FROM links WHERE fromId = ?` diff --git a/src/skrift/notes_db/search.test.ts b/src/skrift/notes_db/search.test.ts index 4af46d9..baf146d 100644 --- a/src/skrift/notes_db/search.test.ts +++ b/src/skrift/notes_db/search.test.ts @@ -5,11 +5,11 @@ import { NotesDB } from "./index.js"; describe("NotesDB.search()", () => { let db: BetterSqlite3.Database; - beforeEach(() => { + beforeEach(async () => { db = NotesDB.memory(); NotesDB.initialize(db); - NotesDB.save(db, "a.md", "Monkey"); - NotesDB.save(db, "b.md", "Tiger"); + await NotesDB.save(db, "a.md", "Monkey"); + await NotesDB.save(db, "b.md", "Tiger"); }); test("returns search results", () => { diff --git a/src/skrift/notes_db/semanticSearch.test.ts b/src/skrift/notes_db/semanticSearch.test.ts new file mode 100644 index 0000000..e3745a1 --- /dev/null +++ b/src/skrift/notes_db/semanticSearch.test.ts @@ -0,0 +1,43 @@ +import { describe, expect, it, beforeEach } from "vitest"; + +import BetterSqlite3 from "better-sqlite3"; +import { NotesDB } from "./index.js"; + +describe("NotesDB.semanticSearch()", () => { + let db: BetterSqlite3.Database; + + beforeEach(async () => { + db = NotesDB.memory(); + NotesDB.initialize(db); + await NotesDB.save(db, "a.md", "Monkey eating bananas in the jungle"); + await NotesDB.save(db, "b.md", "Tiger hunting in the jungle"); + await NotesDB.save(db, "c.md", "Programming in Python and JavaScript"); + }); + + it("returns semantically similar results", async () => { + const results = await NotesDB.searchSemantic(db, "wild animals in nature"); + expect(results).toContain("a.md"); + expect(results).toContain("b.md"); + expect(results).not.toContain("c.md"); + }); + + it("returns results ordered by relevance", async () => { + const results = await NotesDB.searchSemantic(db, "dangerous predator"); + expect(results[0]).toBe("b.md"); // Tiger should be most relevant + }); + + it("respects the topK parameter", async () => { + const results = await NotesDB.searchSemantic(db, "animals", 1); + expect(results).toHaveLength(1); + }); + + it("handles empty query", async () => { + const results = await NotesDB.searchSemantic(db, ""); + expect(results).toHaveLength(0); + }); + + it("handles query with no semantic matches", async () => { + const results = await NotesDB.searchSemantic(db, "xyzabc123"); + expect(results).toHaveLength(0); + }); +}); From 75094fd4894d059c73cd3937a09c26f0c76c34c8 Mon Sep 17 00:00:00 2001 From: Harry Vangberg Date: Fri, 24 Jan 2025 10:56:09 +0100 Subject: [PATCH 2/7] search --- src/skrift/notes_db/index.ts | 30 +++++++++++++++------- src/skrift/notes_db/semanticSearch.test.ts | 14 ++-------- 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/src/skrift/notes_db/index.ts b/src/skrift/notes_db/index.ts index f7dff62..570e2de 100644 --- a/src/skrift/notes_db/index.ts +++ b/src/skrift/notes_db/index.ts @@ -7,6 +7,10 @@ import { ParsedNote } from "../note/fromMarkdown.js"; import { createHash } from "crypto"; import { pipeline } from "@huggingface/transformers"; +export interface RowIdRow { + rowid: number; +} + export interface NoteRow { id: string; title: string; @@ -89,9 +93,12 @@ export const NotesDB = { db.prepare("DROP TABLE IF EXISTS links").run(); }, - getRowid(db: BetterSqlite3.Database, id: NoteID): bigint { - const result = db.prepare("SELECT rowid FROM notes WHERE id = ?").run(id); - return BigInt(result.lastInsertRowid); + getRowid(db: BetterSqlite3.Database, id: NoteID): bigint | null { + const result = db.prepare("SELECT rowid FROM notes WHERE id = ?").get(id); + if (!result) { + return null; + } + return BigInt(result.rowid); }, async save( @@ -105,9 +112,13 @@ export const NotesDB = { const embedding = await NotesDB.computeEmbedding(markdown); return db.transaction((note: ParsedNote) => { + const deleteRowId = NotesDB.getRowid(db, id); + if (deleteRowId) { + db.prepare("DELETE FROM notes_embeddings WHERE rowid = ?").run(deleteRowId); + } + db.prepare("DELETE FROM notes WHERE id = ?").run(id); db.prepare("DELETE FROM links WHERE fromId = ?").run(id); - db.prepare("DELETE FROM notes_embeddings WHERE rowid = ?").run(NotesDB.getRowid(db, id)); const result = db.prepare( `INSERT INTO notes (id, title, markdown, modifiedAt, checksum) VALUES (?, ?, ?, ?, ?)` @@ -206,10 +217,11 @@ export const NotesDB = { async searchSemantic(db: BetterSqlite3.Database, query: string, topK: number = 10): Promise { const embedding = await NotesDB.computeEmbedding(query); - const rows = db.prepare(` + const rows = db.prepare<[Float32Array, number], EmbeddingRow>(` SELECT id, - ROW_NUMBER() OVER (ORDER BY e.distance ASC) AS rank + ROW_NUMBER() OVER (ORDER BY e.distance ASC) AS rank, + e.distance AS score FROM notes n INNER JOIN ( SELECT rowid, distance @@ -217,9 +229,9 @@ export const NotesDB = { WHERE embedding MATCH ? ORDER BY distance ASC LIMIT ? - ) e ON n.id = e.rowid + ) e ON n.rowid = e.rowid ORDER BY e.distance ASC - `).all(embedding); + `).all(embedding, topK); return rows.map((row) => row.id as NoteID); }, @@ -240,7 +252,7 @@ export const NotesDB = { // Does this really belong in `NotesDB`? async computeEmbedding(markdown: string): Promise { - const model = await pipeline("feature-extraction", "Xenova/all-MiniLM-L6-v2", { dtype: "fp16" }); + const model = await pipeline("feature-extraction", "Xenova/all-MiniLM-L6-v2", { dtype: "fp32" }); const output = await model(markdown, { pooling: "mean", normalize: true }); return new Float32Array(output.data); }, diff --git a/src/skrift/notes_db/semanticSearch.test.ts b/src/skrift/notes_db/semanticSearch.test.ts index e3745a1..e700db7 100644 --- a/src/skrift/notes_db/semanticSearch.test.ts +++ b/src/skrift/notes_db/semanticSearch.test.ts @@ -14,11 +14,11 @@ describe("NotesDB.semanticSearch()", () => { await NotesDB.save(db, "c.md", "Programming in Python and JavaScript"); }); - it("returns semantically similar results", async () => { + it("returns semantically similar results first", async () => { const results = await NotesDB.searchSemantic(db, "wild animals in nature"); expect(results).toContain("a.md"); expect(results).toContain("b.md"); - expect(results).not.toContain("c.md"); + expect(results.at(-1)).toBe("c.md"); }); it("returns results ordered by relevance", async () => { @@ -30,14 +30,4 @@ describe("NotesDB.semanticSearch()", () => { const results = await NotesDB.searchSemantic(db, "animals", 1); expect(results).toHaveLength(1); }); - - it("handles empty query", async () => { - const results = await NotesDB.searchSemantic(db, ""); - expect(results).toHaveLength(0); - }); - - it("handles query with no semantic matches", async () => { - const results = await NotesDB.searchSemantic(db, "xyzabc123"); - expect(results).toHaveLength(0); - }); }); From aa0e5e13ac81d72adf96d3b3e13720161941a8ca Mon Sep 17 00:00:00 2001 From: Harry Vangberg Date: Fri, 24 Jan 2025 14:35:00 +0100 Subject: [PATCH 3/7] reciprocal rank fusion --- rfc/006-search-reciprocal-rank-fusion.md | 14 +++++ script/huggingfaceDownload.ts | 4 +- src/skrift-electron/main/ipc.ts | 8 +-- src/skrift/huggingface.ts | 22 ++++--- src/skrift/notes_db/index.ts | 39 ++++++++++-- src/skrift/notes_db/search.test.ts | 72 +++++++++++++---------- src/skrift/notes_db/searchKeyword.test.ts | 32 ++++++++++ 7 files changed, 141 insertions(+), 50 deletions(-) create mode 100644 rfc/006-search-reciprocal-rank-fusion.md create mode 100644 src/skrift/notes_db/searchKeyword.test.ts diff --git a/rfc/006-search-reciprocal-rank-fusion.md b/rfc/006-search-reciprocal-rank-fusion.md new file mode 100644 index 0000000..f03b5e7 --- /dev/null +++ b/rfc/006-search-reciprocal-rank-fusion.md @@ -0,0 +1,14 @@ +# Search: Reciprocal Rank Fusion + +We have two search methods: + +- `NotesDB.searchKeyword`: Search by keyword. +- `NotesDB.searchSemantic`: Search by semantic similarity. + +We can combine the results of these two searches using Reciprocal Rank Fusion (RRF)[^rrf]. + +The idea is to first search by keyword, then search by semantic similarity, and then combine the results. + +`NotesDB.search` will perform an RRF search. + +[^rrf]: https://learn.microsoft.com/en-us/azure/search/hybrid-search-ranking \ No newline at end of file diff --git a/script/huggingfaceDownload.ts b/script/huggingfaceDownload.ts index 45c73ed..d87bb7d 100644 --- a/script/huggingfaceDownload.ts +++ b/script/huggingfaceDownload.ts @@ -1,3 +1,3 @@ -import { HuggingFace } from "../src/skrift/huggingface.js"; +import { Pipelines } from "../src/skrift/huggingface.js"; -HuggingFace.preload(); \ No newline at end of file +Pipelines.preload(); diff --git a/src/skrift-electron/main/ipc.ts b/src/skrift-electron/main/ipc.ts index 4a6c705..0b10345 100644 --- a/src/skrift-electron/main/ipc.ts +++ b/src/skrift-electron/main/ipc.ts @@ -132,14 +132,14 @@ const handleSetNote = async ( }); }; -const handleSearch = ( +const handleSearch = async ( event: Electron.IpcMainInvokeEvent, query: string -): NoteLink[] => { +): Promise => { const db = getDB(); - const ids = NotesDB.search(db, query); - const links = NotesDB.getNoteLinks(db, ids); + const ids = await NotesDB.search(db, query); + const links = await NotesDB.getNoteLinks(db, ids); return links; }; diff --git a/src/skrift/huggingface.ts b/src/skrift/huggingface.ts index 66061b9..342c1d9 100644 --- a/src/skrift/huggingface.ts +++ b/src/skrift/huggingface.ts @@ -1,14 +1,20 @@ -import { AutoModel } from "@huggingface/transformers"; +import { pipeline, FeatureExtractionPipeline } from "@huggingface/transformers"; -export const HuggingFace = { +export const Pipelines = { preload: async () => { - const models = Object.values(HuggingFace.models); - for (const model of models) { - AutoModel.from_pretrained(model); - } + await getSentenceTransformerPipeline(); }, +} - models: { - sentenceTransformer: "Xenova/all-MiniLM-L6-v2" +let sentenceTransformerPipeline: FeatureExtractionPipeline | null = null; + +export const getSentenceTransformerPipeline = async () => { + if (sentenceTransformerPipeline === null) { + sentenceTransformerPipeline = await pipeline( + "feature-extraction", + "Xenova/all-MiniLM-L6-v2", + { dtype: "fp32" } + ); } + return sentenceTransformerPipeline; } \ No newline at end of file diff --git a/src/skrift/notes_db/index.ts b/src/skrift/notes_db/index.ts index 570e2de..2cdc800 100644 --- a/src/skrift/notes_db/index.ts +++ b/src/skrift/notes_db/index.ts @@ -6,6 +6,7 @@ import { Fts } from "./fts.js"; import { ParsedNote } from "../note/fromMarkdown.js"; import { createHash } from "crypto"; import { pipeline } from "@huggingface/transformers"; +import { getSentenceTransformerPipeline } from "../huggingface.js"; export interface RowIdRow { rowid: number; @@ -196,7 +197,32 @@ export const NotesDB = { db.prepare(`DELETE FROM notes_embeddings WHERE rowid = ?`).run(rowid); }, - search(db: BetterSqlite3.Database, query: string): NoteID[] { + async search(db: BetterSqlite3.Database, query: string): Promise { + const k = 60; // Constant for RRF calculation + const keywordResults = await NotesDB.searchKeyword(db, query); + const semanticResults = await NotesDB.searchSemantic(db, query); + + // Create a map to store combined scores + const scores = new Map(); + + // Calculate RRF scores for keyword results + keywordResults.forEach((id, rank) => { + scores.set(id, 1 / (k + rank + 1)); + }); + + // Add RRF scores for semantic results + semanticResults.forEach((id, rank) => { + const existingScore = scores.get(id) || 0; + scores.set(id, existingScore + 1 / (k + rank + 1)); + }); + + // Sort by score descending + return Array.from(scores.entries()) + .sort(([, a], [, b]) => b - a) + .map(([id]) => id); + }, + + async searchKeyword(db: BetterSqlite3.Database, query: string, limit: number = 50): Promise { if (query === "*") { return NotesDB.recent(db); } @@ -208,13 +234,13 @@ export const NotesDB = { } const rows = db - .prepare(`SELECT * FROM notes WHERE notes MATCH ? LIMIT 50`) - .all(Fts.toMatch(tokens)); + .prepare<[string, number], SearchRow>(`SELECT * FROM notes WHERE notes MATCH ? LIMIT ?`) + .all(Fts.toMatch(tokens), limit); return rows.map((row) => row.id); }, - async searchSemantic(db: BetterSqlite3.Database, query: string, topK: number = 10): Promise { + async searchSemantic(db: BetterSqlite3.Database, query: string, limit: number = 50): Promise { const embedding = await NotesDB.computeEmbedding(query); const rows = db.prepare<[Float32Array, number], EmbeddingRow>(` @@ -231,7 +257,7 @@ export const NotesDB = { LIMIT ? ) e ON n.rowid = e.rowid ORDER BY e.distance ASC - `).all(embedding, topK); + `).all(embedding, limit); return rows.map((row) => row.id as NoteID); }, @@ -252,8 +278,9 @@ export const NotesDB = { // Does this really belong in `NotesDB`? async computeEmbedding(markdown: string): Promise { - const model = await pipeline("feature-extraction", "Xenova/all-MiniLM-L6-v2", { dtype: "fp32" }); + const model = await getSentenceTransformerPipeline(); const output = await model(markdown, { pooling: "mean", normalize: true }); + return new Float32Array(output.data); }, diff --git a/src/skrift/notes_db/search.test.ts b/src/skrift/notes_db/search.test.ts index baf146d..dba1490 100644 --- a/src/skrift/notes_db/search.test.ts +++ b/src/skrift/notes_db/search.test.ts @@ -1,32 +1,44 @@ -import { describe, expect, test, beforeAll, beforeEach, afterEach } from 'vitest'; -import BetterSqlite3 from "better-sqlite3"; import { NotesDB } from "./index.js"; +import { Note } from "../note/index.js"; +import { describe, it, expect, beforeEach } from "vitest"; +import BetterSqlite3 from "better-sqlite3"; -describe("NotesDB.search()", () => { - let db: BetterSqlite3.Database; - - beforeEach(async () => { - db = NotesDB.memory(); - NotesDB.initialize(db); - await NotesDB.save(db, "a.md", "Monkey"); - await NotesDB.save(db, "b.md", "Tiger"); - }); - - test("returns search results", () => { - const notes = NotesDB.search(db, "Tiger"); - - expect(notes).toEqual(["b.md"]); - }); - - test("returns all results", () => { - const notes = NotesDB.search(db, "*"); - - expect(notes.sort()).toEqual(["a.md", "b.md"].sort()); - }); - - test("ignores special characters", () => { - const notes = NotesDB.search(db, "Tiger #"); - - expect(notes).toEqual(["b.md"]); - }); -}); +describe("NotesDB.search", () => { + let db: BetterSqlite3.Database; + + beforeEach(() => { + db = NotesDB.memory(); + NotesDB.initialize(db); + }); + + it("combines keyword and semantic search results", async () => { + // Create test notes with different content + await NotesDB.save(db, "1.md", "# Python Programming\nLearn about Python programming language basics."); + await NotesDB.save(db, "2.md", "# JavaScript Guide\nA comprehensive guide to JavaScript programming."); + await NotesDB.save(db, "3.md", "# Programming Languages\nCompare different programming languages."); + await NotesDB.save(db, "4.md", "# Cooking Recipe\nHow to make a delicious pasta."); + + // Search for programming related content + const results = await NotesDB.search(db, "programming language guide"); + + // Verify that programming related notes are ranked higher + expect(results).toContain("3.md"); // Contains both "programming" and "languages" + expect(results).toContain("1.md"); // Contains "programming language" + expect(results).toContain("2.md"); // Contains "programming" + + // Cooking recipe should be ranked last or not included + const cookingIndex = results.indexOf("4.md"); + const programmingIndex = results.indexOf("3.md"); + expect(cookingIndex).toBeGreaterThan(programmingIndex); + }); + + it("returns empty array for empty query", async () => { + const results = await NotesDB.search(db, ""); + expect(results).toEqual([]); + }); + + it("handles non-existing content gracefully", async () => { + const results = await NotesDB.search(db, "xyznonexistent"); + expect(results).toEqual([]); + }); +}); \ No newline at end of file diff --git a/src/skrift/notes_db/searchKeyword.test.ts b/src/skrift/notes_db/searchKeyword.test.ts new file mode 100644 index 0000000..69d1fe7 --- /dev/null +++ b/src/skrift/notes_db/searchKeyword.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, test, beforeAll, beforeEach, afterEach } from 'vitest'; +import BetterSqlite3 from "better-sqlite3"; +import { NotesDB } from "./index.js"; + +describe("NotesDB.searchKeyword()", () => { + let db: BetterSqlite3.Database; + + beforeEach(async () => { + db = NotesDB.memory(); + NotesDB.initialize(db); + await NotesDB.save(db, "a.md", "Monkey"); + await NotesDB.save(db, "b.md", "Tiger"); + }); + + test("returns search results", async () => { + const notes = await NotesDB.searchKeyword(db, "Tiger"); + + expect(notes).toEqual(["b.md"]); + }); + + test("returns all results", async () => { + const notes = await NotesDB.searchKeyword(db, "*"); + + expect(notes.sort()).toEqual(["a.md", "b.md"].sort()); + }); + + test("ignores special characters", async () => { + const notes = await NotesDB.searchKeyword(db, "Tiger #"); + + expect(notes).toEqual(["b.md"]); + }); +}); From 8359fefa97dfdc8022aed9cac7cf23b2346f2aab Mon Sep 17 00:00:00 2001 From: Harry Vangberg Date: Fri, 24 Jan 2025 14:40:18 +0100 Subject: [PATCH 4/7] node loeader --- package-lock.json | 56 ++++++++++++++++++++++++++++++++++++++++++ package.json | 3 ++- webpack.main.config.js | 4 +++ 3 files changed, 62 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 8ac1caa..fc95f62 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42,6 +42,7 @@ "html-webpack-plugin": "^5.3.1", "immer": "^10.1.1", "markdown-it": "^14.1.0", + "node-loader": "^2.1.0", "parsimmon": "^1.17.0", "postcss": "^8.2.15", "postcss-loader": "^8.1.1", @@ -4391,6 +4392,16 @@ "prebuild-install": "^7.1.1" } }, + "node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -6665,6 +6676,16 @@ "dev": true, "license": "MIT" }, + "node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -9927,6 +9948,21 @@ "node": ">=6.11.5" } }, + "node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -11282,6 +11318,26 @@ "node": "^12.13 || ^14.13 || >=16" } }, + "node_modules/node-loader": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/node-loader/-/node-loader-2.1.0.tgz", + "integrity": "sha512-OwjPkyh8+7jW8DMd/iq71uU1Sspufr/C2+c3t0p08J3CrM9ApZ4U53xuisNrDXOHyGi5OYHgtfmmh+aK9zJA6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "loader-utils": "^2.0.3" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, "node_modules/node-releases": { "version": "2.0.19", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", diff --git a/package.json b/package.json index 5eb7dc2..021c653 100644 --- a/package.json +++ b/package.json @@ -102,6 +102,7 @@ "html-webpack-plugin": "^5.3.1", "immer": "^10.1.1", "markdown-it": "^14.1.0", + "node-loader": "^2.1.0", "parsimmon": "^1.17.0", "postcss": "^8.2.15", "postcss-loader": "^8.1.1", @@ -140,4 +141,4 @@ "dependencies": { "better-sqlite3": "^11.7.0" } -} \ No newline at end of file +} diff --git a/webpack.main.config.js b/webpack.main.config.js index 41fc82b..e9770f8 100644 --- a/webpack.main.config.js +++ b/webpack.main.config.js @@ -18,6 +18,10 @@ export default [ include: /src/, use: [{ loader: "ts-loader" }], }, + { + test: /\.node$/, + loader: "node-loader", + }, ], }, output: { From 2df4b75337275a634ce1fe9455737bda9d9e26dd Mon Sep 17 00:00:00 2001 From: Harry Vangberg Date: Fri, 24 Jan 2025 15:03:36 +0100 Subject: [PATCH 5/7] it works --- .gitignore | 3 ++- package-lock.json | 3 ++- package.json | 3 ++- webpack.main.config.js | 6 ++---- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/.gitignore b/.gitignore index c7caf17..d7d240a 100644 --- a/.gitignore +++ b/.gitignore @@ -22,4 +22,5 @@ npm-debug.log* yarn-debug.log* yarn-error.log* dist -build \ No newline at end of file +build +.cache diff --git a/package-lock.json b/package-lock.json index fc95f62..59becf7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ "@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.0.3", "@benrbray/prosemirror-math": "^1.0.0", "@handlewithcare/remark-prosemirror": "^0.1.5", - "@huggingface/transformers": "^3.3.1", + "@huggingface/transformers": "^3.3.2", "@nytimes/react-prosemirror": "^1.0.0", "@types/better-sqlite3": "^7.6.12", "@types/markdown-it": "^14.1.2", @@ -64,6 +64,7 @@ "remark-math": "^6.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", + "sharp": "^0.33.5", "sqlite-vec": "^0.1.7-alpha.2", "style-loader": "^4.0.0", "tailwindcss": "^3.4.16", diff --git a/package.json b/package.json index 021c653..c2975a3 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,7 @@ "@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.0.3", "@benrbray/prosemirror-math": "^1.0.0", "@handlewithcare/remark-prosemirror": "^0.1.5", - "@huggingface/transformers": "^3.3.1", + "@huggingface/transformers": "^3.3.2", "@nytimes/react-prosemirror": "^1.0.0", "@types/better-sqlite3": "^7.6.12", "@types/markdown-it": "^14.1.2", @@ -124,6 +124,7 @@ "remark-math": "^6.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", + "sharp": "^0.33.5", "sqlite-vec": "^0.1.7-alpha.2", "style-loader": "^4.0.0", "tailwindcss": "^3.4.16", diff --git a/webpack.main.config.js b/webpack.main.config.js index e9770f8..d0e29e9 100644 --- a/webpack.main.config.js +++ b/webpack.main.config.js @@ -18,10 +18,6 @@ export default [ include: /src/, use: [{ loader: "ts-loader" }], }, - { - test: /\.node$/, - loader: "node-loader", - }, ], }, output: { @@ -34,6 +30,8 @@ export default [ }, externals: { "better-sqlite3": "better-sqlite3", + "sharp": "sharp", + "onnxruntime-node": "onnxruntime-node" }, }, ]; From 6f27381e9b9652dfdfabd0cf6d9cac65fdc0eadd Mon Sep 17 00:00:00 2001 From: Harry Vangberg Date: Mon, 27 Jan 2025 21:39:56 +0100 Subject: [PATCH 6/7] comment --- src/skrift/notes_db/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/skrift/notes_db/index.ts b/src/skrift/notes_db/index.ts index 2cdc800..52b1447 100644 --- a/src/skrift/notes_db/index.ts +++ b/src/skrift/notes_db/index.ts @@ -198,6 +198,7 @@ export const NotesDB = { }, async search(db: BetterSqlite3.Database, query: string): Promise { + // https://learn.microsoft.com/en-us/azure/search/hybrid-search-ranking const k = 60; // Constant for RRF calculation const keywordResults = await NotesDB.searchKeyword(db, query); const semanticResults = await NotesDB.searchSemantic(db, query); From a68598cc1d17c5857cb1107e6ebd6b9c9ae7233b Mon Sep 17 00:00:00 2001 From: Harry Vangberg Date: Wed, 29 Jan 2025 11:50:23 +0100 Subject: [PATCH 7/7] split search results in keyword/semantic --- src/skrift-electron/main/ipc.ts | 23 +++++++++------ .../renderer/components/SearchCard.tsx | 14 +++++++--- .../containers/SearchCardContainer.tsx | 28 ++++++++++++++----- src/skrift-electron/renderer/ipc.ts | 5 ---- src/skrift-electron/shared/types.ts | 10 ++++--- 5 files changed, 52 insertions(+), 28 deletions(-) diff --git a/src/skrift-electron/main/ipc.ts b/src/skrift-electron/main/ipc.ts index 0b10345..0800f3f 100644 --- a/src/skrift-electron/main/ipc.ts +++ b/src/skrift-electron/main/ipc.ts @@ -6,6 +6,7 @@ import { IpcAddNoteCommand, IpcDeleteNoteCommand, IpcSetNoteCommand, + IpcSearchCommand, } from "../shared/types.js"; import BetterSqlite3 from "better-sqlite3"; import path from "path"; @@ -133,15 +134,20 @@ const handleSetNote = async ( }; const handleSearch = async ( - event: Electron.IpcMainInvokeEvent, - query: string -): Promise => { + event: Electron.IpcMainEvent, + cmd: IpcSearchCommand +) => { + const { query } = cmd; const db = getDB(); - const ids = await NotesDB.search(db, query); - const links = await NotesDB.getNoteLinks(db, ids); + const [keywordIds, semanticIds] = await Promise.all([ + NotesDB.searchKeyword(db, query), + NotesDB.searchSemantic(db, query), + ]); + const keywordLinks = NotesDB.getNoteLinks(db, keywordIds); + const semanticLinks = NotesDB.getNoteLinks(db, semanticIds); - return links; + reply(event, { type: "event/SEARCH_RESULT", query, keyword: keywordLinks, semantic: semanticLinks }); }; const handleWriteHTMLToClipboard = ( @@ -169,11 +175,12 @@ export const setupIpc = () => { case "command/DELETE_NOTE": handleDeleteNote(event, command); break; + case "command/SEARCH": + handleSearch(event, command); + break; } }); - ipcMain.handle("search", handleSearch); - ipcMain.handle('show-message-box', async (event, options) => { return await dialog.showMessageBox(options); }); diff --git a/src/skrift-electron/renderer/components/SearchCard.tsx b/src/skrift-electron/renderer/components/SearchCard.tsx index 9a6020d..3ac4d84 100644 --- a/src/skrift-electron/renderer/components/SearchCard.tsx +++ b/src/skrift-electron/renderer/components/SearchCard.tsx @@ -1,6 +1,6 @@ import React from "react"; import { SearchCardInput } from "./SearchCardInput.js"; -import { Note } from "../../../skrift/note/index.js"; +import { NoteLink } from "../../../skrift/note/index.js"; import { CardToolbar } from "./CardToolbar.js"; import { Card } from "./Card.js"; import { CardBody } from "./CardBody.js"; @@ -10,7 +10,8 @@ import { OpenCardMode } from "../interfaces/state/index.js"; type Props = { query: string; - results: Note[]; + keywordResults: NoteLink[]; + semanticResults: NoteLink[]; onOpen: (id: string, mode: OpenCardMode) => void; onClose: () => void; onAdd: (title: string, mode: OpenCardMode) => void; @@ -19,7 +20,8 @@ type Props = { export const SearchCard: React.FC = ({ query, - results, + keywordResults, + semanticResults, onOpen, onClose, onAdd, @@ -37,7 +39,11 @@ export const SearchCard: React.FC = ({ onAdd={onAdd} onSearch={onSearch} /> - +

Exact Matches

+ + +

Semantic Matches

+ ); diff --git a/src/skrift-electron/renderer/containers/SearchCardContainer.tsx b/src/skrift-electron/renderer/containers/SearchCardContainer.tsx index 9f331ae..fa014cb 100644 --- a/src/skrift-electron/renderer/containers/SearchCardContainer.tsx +++ b/src/skrift-electron/renderer/containers/SearchCardContainer.tsx @@ -1,6 +1,6 @@ import React, { useCallback, useEffect } from "react"; import { SearchCard } from "../components/SearchCard.js"; -import { Note } from "../../../skrift/note/index.js"; +import { Note, NoteLink } from "../../../skrift/note/index.js"; import { useCache } from "../hooks/useCache.js"; import { Ipc } from "../ipc.js"; import { Path } from "../interfaces/path/index.js"; @@ -20,18 +20,31 @@ export const SearchCardContainer: React.FC = ({ path, card }) => { const { onOpenNote, onClose, onUpdate } = useCardActions(card, path); - const [results, setResults] = useCache( - `card/${card.meta.key}/results`, + const [keywordResults, setKeywordResults] = useCache( + `card/${card.meta.key}/keywordResults`, + [] + ); + const [semanticResults, setSemanticResults] = useCache( + `card/${card.meta.key}/semanticResults`, [] ); useEffect(() => { if (query !== "*" && query.length <= 1) { - return setResults([]); + return setKeywordResults([]); } - Ipc.search(query).then((results) => setResults(results)); - }, [query, setResults]); + Ipc.send({ type: "command/SEARCH", query }); + + const deregister = Ipc.on((event) => { + if (event.type === "event/SEARCH_RESULT" && event.query === query) { + setKeywordResults(event.keyword); + setSemanticResults(event.semantic); + } + }); + + return deregister; + }, [query, setKeywordResults, setSemanticResults]); const handleSearch = useCallback( (query: string) => { @@ -57,7 +70,8 @@ export const SearchCardContainer: React.FC = ({ path, card }) => { onClose={onClose} onSearch={handleSearch} query={query} - results={results} + keywordResults={keywordResults} + semanticResults={semanticResults} /> ); }; diff --git a/src/skrift-electron/renderer/ipc.ts b/src/skrift-electron/renderer/ipc.ts index f9d038d..77da712 100644 --- a/src/skrift-electron/renderer/ipc.ts +++ b/src/skrift-electron/renderer/ipc.ts @@ -1,6 +1,5 @@ import { ipcRenderer, IpcRendererEvent } from "electron"; import { IpcReply, IpcCommand } from "../shared/types.js"; -import { Note } from "../../skrift/note/index.js"; export const Ipc = { on(callback: (reply: IpcReply) => void) { @@ -20,10 +19,6 @@ export const Ipc = { ipcRenderer.send("skrift", command); }, - search(query: String): Promise { - return ipcRenderer.invoke("search", query); - }, - showMessageBox(options: Electron.MessageBoxOptions): Promise { return ipcRenderer.invoke('show-message-box', options); }, diff --git a/src/skrift-electron/shared/types.ts b/src/skrift-electron/shared/types.ts index c9254f7..3581624 100644 --- a/src/skrift-electron/shared/types.ts +++ b/src/skrift-electron/shared/types.ts @@ -1,4 +1,4 @@ -import { NoteID, NoteWithLinks } from "../../skrift/note/index.js"; +import { NoteID, NoteLink, NoteWithLinks } from "../../skrift/note/index.js"; export type IpcAddNoteCommand = { type: "command/ADD_NOTE"; @@ -12,13 +12,14 @@ export type IpcSetNoteCommand = { markdown: string; }; export type IpcDeleteNoteCommand = { type: "command/DELETE_NOTE"; id: NoteID }; - +export type IpcSearchCommand = { type: "command/SEARCH"; query: string }; export type IpcCommand = | { type: "command/LOAD_DIR" } | IpcAddNoteCommand | IpcLoadNoteCommand | IpcSetNoteCommand - | IpcDeleteNoteCommand; + | IpcDeleteNoteCommand + | IpcSearchCommand; export type IpcSetNoteEvent = { type: "event/SET_NOTE"; note: NoteWithLinks }; @@ -27,4 +28,5 @@ export type IpcReply = | { type: "event/LOADED_DIR"; initialNoteID: NoteID | null } | IpcSetNoteEvent | { type: "event/DELETED_NOTE"; id: NoteID } - | { type: "event/ADDED_LINK"; from: NoteID; to: NoteID }; + | { type: "event/ADDED_LINK"; from: NoteID; to: NoteID } + | { type: "event/SEARCH_RESULT"; query: string; keyword: NoteLink[]; semantic: NoteLink[] };