diff --git a/.env b/.env index af3955f..252fd54 100644 --- a/.env +++ b/.env @@ -5,3 +5,6 @@ DATABASE_PATH2=Enter-path-2-here DATABASE_NAME2=Enter-name-2-here DATABASE_PATH3=Enter-path-3-here DATABASE_NAME3=Enter-name-3-here + +TEXT_EMBEDDING_URL=http://localhost:8080/vectors +IMAGE_EMBEDDING_URL=http://localhost:8080/vectors_img \ No newline at end of file diff --git a/.gitignore b/.gitignore index eede8e6..192444e 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,6 @@ /db .DS_Store /.idea -/testdb \ No newline at end of file +/testdb +.TODO +/databases \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 447db65..e0dc22f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 4 [[package]] name = "addr2line" -version = "0.24.2" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" dependencies = [ "gimli", ] @@ -43,19 +43,27 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.98" +version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "api" version = "0.1.0" dependencies = [ - "core", + "defs", "index", "storage", + "tempfile", + "uuid", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" version = "1.5.0" @@ -64,9 +72,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "backtrace" -version = "0.3.75" +version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" dependencies = [ "addr2line", "cfg-if", @@ -74,9 +82,15 @@ dependencies = [ "miniz_oxide", "object", "rustc-demangle", - "windows-targets 0.52.6", + "windows-link 0.2.1", ] +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "bincode" version = "1.3.3" @@ -109,11 +123,11 @@ dependencies = [ [[package]] name = "bindgen" -version = "0.71.1" +version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "cexpr", "clang-sys", "itertools 0.13.0", @@ -133,9 +147,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.1" +version = "2.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" [[package]] name = "bumpalo" @@ -176,10 +190,11 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.27" +version = "1.2.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d487aa071b5f64da6f19a3e848e3578944b726ee5a4854b82172f02aa876bfdc" +checksum = "e1d05d92f4b1fd76aad469d46cdd858ca761576082cd37df81416691e50199fb" dependencies = [ + "find-msvc-tools", "jobserver", "libc", "shlex", @@ -196,9 +211,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.1" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" [[package]] name = "chrono" @@ -211,7 +226,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-link", + "windows-link 0.2.1", ] [[package]] @@ -266,11 +281,13 @@ dependencies = [ ] [[package]] -name = "core" -version = "0.1.0" +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" dependencies = [ - "bincode", - "serde", + "core-foundation-sys", + "libc", ] [[package]] @@ -285,7 +302,7 @@ version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "crossterm_winapi", "libc", "mio 0.8.11", @@ -304,18 +321,63 @@ dependencies = [ "winapi", ] +[[package]] +name = "defs" +version = "0.1.0" +dependencies = [ + "bincode", + "serde", + "uuid", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dotenv" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" + [[package]] name = "either" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "equivalent" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + [[package]] name = "eyre" version = "0.6.12" @@ -326,12 +388,115 @@ dependencies = [ "once_cell", ] +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "find-msvc-tools" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0399f9d26e5191ce32c498bebd31e7a3ceabc2745f0ac54af3f335126c3f24b3" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "foldhash" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", +] + [[package]] name = "getrandom" version = "0.3.3" @@ -341,20 +506,39 @@ dependencies = [ "cfg-if", "libc", "r-efi", - "wasi 0.14.2+wasi-0.2.4", + "wasi 0.14.7+wasi-0.2.4", ] [[package]] name = "gimli" -version = "0.31.1" +version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" [[package]] name = "glob" -version = "0.3.2" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "h2" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] [[package]] name = "hashbrown" @@ -373,6 +557,126 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + [[package]] name = "iana-time-zone" version = "0.1.64" @@ -397,6 +701,113 @@ dependencies = [ "cc", ] +[[package]] +name = "icu_collections" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" + +[[package]] +name = "icu_properties" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "potential_utf", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" + +[[package]] +name = "icu_provider" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +dependencies = [ + "displaydoc", + "icu_locale_core", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + [[package]] name = "indenter" version = "0.3.4" @@ -407,20 +818,47 @@ checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" name = "index" version = "0.1.0" dependencies = [ - "core", + "defs", + "uuid", +] + +[[package]] +name = "indexmap" +version = "2.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" +dependencies = [ + "equivalent", + "hashbrown", ] [[package]] name = "io-uring" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4" +checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "cfg-if", "libc", ] +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "itertools" version = "0.12.1" @@ -447,19 +885,19 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jobserver" -version = "0.1.33" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" dependencies = [ - "getrandom", + "getrandom 0.3.3", "libc", ] [[package]] name = "js-sys" -version = "0.3.80" +version = "0.3.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "852f13bec5eba4ba9afbeb93fd7c13fe56147f055939ae21c43a29a0ecb2702e" +checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305" dependencies = [ "once_cell", "wasm-bindgen", @@ -479,18 +917,18 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.174" +version = "0.2.176" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" +checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174" [[package]] name = "libloading" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" dependencies = [ "cfg-if", - "windows-targets 0.53.2", + "windows-link 0.2.1", ] [[package]] @@ -520,21 +958,32 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "litemap" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" + [[package]] name = "lock_api" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "autocfg", "scopeguard", ] [[package]] name = "log" -version = "0.4.27" +version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" [[package]] name = "lru" @@ -557,9 +1006,25 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.5" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] [[package]] name = "minimal-lexical" @@ -600,8 +1065,25 @@ dependencies = [ ] [[package]] -name = "nom" -version = "7.1.3" +name = "native-tls" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "nom" +version = "7.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" dependencies = [ @@ -620,9 +1102,9 @@ dependencies = [ [[package]] name = "object" -version = "0.36.7" +version = "0.37.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" dependencies = [ "memchr", ] @@ -633,17 +1115,61 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "openssl" +version = "0.10.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" +dependencies = [ + "bitflags 2.9.4", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-sys" +version = "0.9.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "owo-colors" -version = "4.2.2" +version = "4.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48dd4f4a2c8405440fd0462561f0e5806bd0f77e86f51c761481bdd4018b545e" +checksum = "9c6901729fa79e91a0913333229e9ca5dc725089d1c363b2f4b4760709dc4a52" [[package]] name = "parking_lot" -version = "0.12.4" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", "parking_lot_core", @@ -651,15 +1177,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.11" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-targets 0.52.6", + "windows-link 0.2.1", ] [[package]] @@ -674,23 +1200,44 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + [[package]] name = "pin-project-lite" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + [[package]] name = "pkg-config" version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "potential_utf" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" +dependencies = [ + "zerovec", +] + [[package]] name = "prettyplease" -version = "0.2.34" +version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6837b9e10d61f45f987d50808f83d1ee3d206c66acf650c3e4ae2e1f6ddedf55" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", "syn", @@ -698,18 +1245,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.95" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.40" +version = "1.0.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" dependencies = [ "proc-macro2", ] @@ -726,7 +1273,7 @@ version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f44c9e68fd46eda15c646fbb85e1040b657a58cdc8c98db1d97a55930d991eef" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "cassowary", "compact_str", "crossterm", @@ -742,18 +1289,18 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.17" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", ] [[package]] name = "regex" -version = "1.11.1" +version = "1.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +checksum = "8b5288124840bee7b386bc413c487869b360b2b4ec421ea56425128692f2a82c" dependencies = [ "aho-corasick", "memchr", @@ -763,9 +1310,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.9" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +checksum = "833eb9ce86d40ef33cb1306d8accf7bc8ec2bfea4355cbdebb3df68b40925cad" dependencies = [ "aho-corasick", "memchr", @@ -774,9 +1321,66 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" + +[[package]] +name = "reqwest" +version = "0.12.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "mime", + "mime_guess", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] [[package]] name = "rocksdb" @@ -806,6 +1410,52 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +[[package]] +name = "rustix" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +dependencies = [ + "bitflags 2.9.4", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd3c25631629d034ce7cd9940adc9d45762d46de2b0f57193c4443b92c6d4d40" +dependencies = [ + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10b3f4191e8a80e6b43eebabfac91e5dcecebb27a71f04e820c47ec41d314bf" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.22" @@ -818,38 +1468,105 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +[[package]] +name = "schannel" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.9.4", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "serde" -version = "1.0.219" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", "syn", ] +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "server" version = "0.1.0" dependencies = [ "api", - "core", + "defs", "index", ] @@ -930,6 +1647,12 @@ dependencies = [ "syn", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + [[package]] name = "static_assertions" version = "1.1.0" @@ -941,9 +1664,11 @@ name = "storage" version = "0.1.0" dependencies = [ "bincode", - "core", + "defs", "index", "rocksdb", + "tempfile", + "uuid", ] [[package]] @@ -968,17 +1693,77 @@ dependencies = [ "syn", ] +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" -version = "2.0.103" +version = "2.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4307e30089d6fd6aff212f2da3a1f9e32f3223b1f010fb09b7c95f90f3ca1e8" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.9.4", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tempfile" +version = "3.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +dependencies = [ + "fastrand", + "getrandom 0.3.3", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + [[package]] name = "thread_local" version = "1.1.9" @@ -988,6 +1773,16 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "tinystr" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +dependencies = [ + "displaydoc", + "zerovec", +] + [[package]] name = "tokio" version = "1.47.1" @@ -1019,6 +1814,84 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +dependencies = [ + "bitflags 2.9.4", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + [[package]] name = "tracing" version = "0.1.41" @@ -1051,36 +1924,53 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.19" +version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" dependencies = [ "sharded-slab", "thread_local", "tracing-core", ] +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + [[package]] name = "tui" version = "0.1.0" dependencies = [ "anyhow", + "api", "chrono", "color-eyre", - "core", "crossterm", + "defs", + "dotenv", "index", "ratatui", + "reqwest", "serde", + "serde_json", "storage", "tokio", + "uuid", ] +[[package]] +name = "unicase" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" + [[package]] name = "unicode-ident" -version = "1.0.18" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" [[package]] name = "unicode-segmentation" @@ -1105,6 +1995,42 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" +dependencies = [ + "getrandom 0.3.3", + "js-sys", + "serde", + "wasm-bindgen", +] + [[package]] name = "valuable" version = "0.1.1" @@ -1117,6 +2043,15 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -1125,18 +2060,27 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasi" -version = "0.14.2+wasi-0.2.4" +version = "0.14.7+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" dependencies = [ - "wit-bindgen-rt", + "wasip2", +] + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.103" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab10a69fbd0a177f5f649ad4d8d3305499c42bab9aef2f7ff592d0ec8f833819" +checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d" dependencies = [ "cfg-if", "once_cell", @@ -1147,9 +2091,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.103" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bb702423545a6007bbc368fde243ba47ca275e549c8a28617f56f6ba53b1d1c" +checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19" dependencies = [ "bumpalo", "log", @@ -1159,11 +2103,24 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e038d41e478cc73bae0ff9b36c60cff1c98b8f38f8d7e8061e79ee63608ac5c" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" -version = "0.2.103" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc65f4f411d91494355917b605e1480033152658d71f722a90647f56a70c88a0" +checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1171,9 +2128,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.103" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffc003a991398a8ee604a401e194b6b3a39677b3173d6e74495eb51b82e99a32" +checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" dependencies = [ "proc-macro2", "quote", @@ -1184,13 +2141,23 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.103" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "293c37f4efa430ca14db3721dfbe48d8c33308096bd44d80ebaa775ab71ba1cf" +checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1" dependencies = [ "unicode-ident", ] +[[package]] +name = "web-sys" +version = "0.3.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9367c417a924a74cae129e6a2ae3b47fabb1f8995595ab474029da749a8be120" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "winapi" version = "0.3.9" @@ -1215,22 +2182,22 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-core" -version = "0.62.0" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57fe7168f7de578d2d8a05b07fd61870d2e73b4020e9f49aa00da8471723497c" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ "windows-implement", "windows-interface", - "windows-link", - "windows-result", - "windows-strings", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", ] [[package]] name = "windows-implement" -version = "0.60.0" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", @@ -1239,9 +2206,9 @@ dependencies = [ [[package]] name = "windows-interface" -version = "0.59.1" +version = "0.59.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", @@ -1250,26 +2217,61 @@ dependencies = [ [[package]] name = "windows-link" -version = "0.2.0" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-registry" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" +dependencies = [ + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] [[package]] name = "windows-result" -version = "0.4.0" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7084dcc306f89883455a206237404d3eaf961e5bd7e0f312f7c91f57eb44167f" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ - "windows-link", + "windows-link 0.2.1", ] [[package]] name = "windows-strings" -version = "0.5.0" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7218c655a553b0bed4426cf54b20d7ba363ef543b52d515b3e48d7fd55318dda" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ - "windows-link", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", ] [[package]] @@ -1281,6 +2283,15 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.59.0" @@ -1290,6 +2301,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -1314,29 +2334,13 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", + "windows_i686_gnullvm", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] -[[package]] -name = "windows-targets" -version = "0.53.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef" -dependencies = [ - "windows_aarch64_gnullvm 0.53.0", - "windows_aarch64_msvc 0.53.0", - "windows_i686_gnu 0.53.0", - "windows_i686_gnullvm 0.53.0", - "windows_i686_msvc 0.53.0", - "windows_x86_64_gnu 0.53.0", - "windows_x86_64_gnullvm 0.53.0", - "windows_x86_64_msvc 0.53.0", -] - [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -1349,12 +2353,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" - [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -1367,12 +2365,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" - [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -1385,24 +2377,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" -[[package]] -name = "windows_i686_gnu" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" - [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" - [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -1415,12 +2395,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" -[[package]] -name = "windows_i686_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" - [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -1433,12 +2407,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" - [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -1451,12 +2419,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" - [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -1470,27 +2432,108 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] -name = "windows_x86_64_msvc" -version = "0.53.0" +name = "wit-bindgen" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "writeable" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" + +[[package]] +name = "yoke" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] [[package]] -name = "wit-bindgen-rt" -version = "0.39.0" +name = "zerofrom-derive" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ - "bitflags 2.9.1", + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] name = "zstd-sys" -version = "2.0.15+zstd.1.5.7" +version = "2.0.16+zstd.1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" dependencies = [ - "bindgen 0.71.1", + "bindgen 0.72.1", "cc", "pkg-config", ] diff --git a/Cargo.toml b/Cargo.toml index c2276b3..3245256 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [workspace] resolver = "2" # Use the modern feature resolver members = [ - "crates/core", # Corresponds to your 'db-core' in earlier discussion + "crates/defs", "crates/storage", "crates/index", "crates/server", @@ -21,8 +21,9 @@ anyhow = "1.0" # axum = { version = "0.7", features = ["macros"] } # tonic = "0.11" # prost = "0.12" +uuid = { version = "1.18.1", features = ["v4", "serde"] } # # crates/some-crate/Cargo.toml # [dependencies] # tokio.workspace = true -# serde.workspace = true \ No newline at end of file +# serde.workspace = true diff --git a/crates/api/Cargo.toml b/crates/api/Cargo.toml index ef365d5..adcf536 100644 --- a/crates/api/Cargo.toml +++ b/crates/api/Cargo.toml @@ -5,6 +5,8 @@ edition = "2021" license = "MIT" [dependencies] -core = { path = "../core" } +defs = { path = "../defs" } index = { path = "../index" } -storage = { path = "../storage" } \ No newline at end of file +storage = { path = "../storage" } +tempfile = "3.20.0" +uuid.workspace = true \ No newline at end of file diff --git a/crates/api/src/lib.rs b/crates/api/src/lib.rs index 36ef19d..3657f58 100644 --- a/crates/api/src/lib.rs +++ b/crates/api/src/lib.rs @@ -1,54 +1,339 @@ -use core::DbError; +use defs::{DbError, IndexedVector, Similarity}; -// use core::{DenseVector, Payload, Point, PointId}; -// use std::{fmt::Error, path::PathBuf, sync::Arc}; +use defs::{DenseVector, Payload, Point, PointId}; +use std::path::PathBuf; +// use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::{Arc, RwLock}; -// use index::{IndexType, VectorIndex}; -// use storage::{StorageEngine, StorageType}; +use index::flat::FlatIndex; +use index::{IndexType, VectorIndex}; +use storage::rocks_db::RocksDbStorage; +use storage::{StorageEngine, StorageType}; -// pub struct VectorDb { -// storage: Arc, -// index: Arc, -// } +use uuid::Uuid; -// impl VectorDb { -// pub fn new(storage: Arc, index: Arc) -> Self { -// Self { storage, index } -// } - -// pub fn insert(&self, vector: DenseVector, payload: Payload) -> Result { -// // Add to storage and index -// Ok(0) -// } - -// pub fn delete(&self, id: PointId) -> Result<(), Error> { -// // Remove from storage -// // Remove from index -// Ok(()) -// } - -// pub fn get(&self, id: PointId) -> Result, Error> { -// // Search for the Point with given id in storage -// Ok(None) -// } - -// pub fn search(&self, query: DenseVector, limit: usize) -> Result, Error> { -// // Use vector index to find similar vectors -// // Return vector ids with similarity scores -// Ok(vec![]) -// } -// } +// static NEXT_ID: AtomicU64 = AtomicU64::new(1); -// pub struct DbConfig { -// pub storage_type: StorageType, -// pub index_type: IndexType, -// pub data_path: PathBuf, -// pub dimension: usize, +// fn generate_point_id() -> u64 { +// NEXT_ID.fetch_add(1, Ordering::Relaxed) // } -pub fn init_api_server() -> Result<(), DbError> { +fn generate_point_id() -> PointId { + Uuid::new_v4() +} + +pub struct VectorDb { + storage: Arc, + index: Arc>, // Using a RwLock instead of Mutex to improve concurrency +} + +impl VectorDb { + fn _new(storage: Arc, index: Arc>) -> Self { + Self { storage, index } + } + + //TODO: Make this an atomic operation + pub fn insert(&self, vector: DenseVector, payload: Payload) -> Result { + // Generate a new point id + let point_id = generate_point_id(); + self.storage + .insert_point(point_id, Some(vector.clone()), Some(payload))?; + + // Get write lock on the index + let mut index = self.index.write().map_err(|_| DbError::LockError)?; + index.insert(IndexedVector { + vector, + id: point_id, + })?; + + Ok(point_id) + } + + //TODO: Make this an atomic operation + pub fn delete(&self, id: PointId) -> Result<(), DbError> { + // Remove from storage + self.storage.delete_point(id)?; + // Remove from index + let mut index = self.index.write().map_err(|_| DbError::LockError)?; + index.delete(id)?; + Ok(()) + } + + pub fn get(&self, id: PointId) -> Result, DbError> { + // Search for the Point with given id in storage + let payload = self.storage.get_payload(id)?; + let vector = self.storage.get_vector(id)?; + if payload.is_some() || vector.is_some() { + Ok(Some(Point { + id, + payload, + vector, + })) + } else { + Ok(None) + } + } + + pub fn search( + &self, + query: DenseVector, + similarity: Similarity, + limit: usize, + ) -> Result, DbError> { + // Use vector index to find similar vectors + let index = self.index.read().map_err(|_| DbError::LockError)?; + + //TODO: Add feat of returning similarity scores in the search + let vectors = index.search(query, similarity, limit)?; + + Ok(vectors) + } + + pub fn list( + &self, + offset: PointId, + limit: usize, + ) -> Result, PointId)>, DbError> { + self.storage.list_vectors(offset, limit) + } + + // populates the current index with vectors from the storage + pub fn build_index(&self) -> Result { + // start from the minimal UUID and fetch in bounded batches and insert + let mut offset = Uuid::nil(); + let page_size: usize = 1000; + let mut inserted: usize = 0; + + let mut index = self.index.write().map_err(|_| DbError::LockError)?; + + while let Some((batch, next_offset)) = self.storage.list_vectors(offset, page_size)? { + if batch.is_empty() || next_offset == offset { + break; + } + + for (id, vector) in batch { + index.insert(IndexedVector { id, vector })?; + inserted += 1; + } + + offset = next_offset; + } + + Ok(inserted) + } +} + +pub struct DbConfig { + pub storage_type: StorageType, + pub index_type: IndexType, + pub data_path: PathBuf, + pub dimension: usize, +} + +pub fn init_api(config: DbConfig) -> Result { // Initialize the storage engine + let storage = match config.storage_type { + StorageType::RocksDb => Arc::new(RocksDbStorage::new(config.data_path)?), + _ => Arc::new(RocksDbStorage::new(config.data_path)?), + }; + // Initialize the vector index - // Start server - Ok(()) + let index: Arc> = match config.index_type { + IndexType::Flat => Arc::new(RwLock::new(FlatIndex::new())), + _ => Arc::new(RwLock::new(FlatIndex::new())), + }; + + // Init the db + let db = VectorDb::_new(storage, index); + + // populate the current index with vectors from the storage + db.build_index()?; + + Ok(db) +} + +#[cfg(test)] +mod tests { + + // TODO: Add more exhaustive tests + + use super::*; + use defs::ContentType; + use tempfile::tempdir; + + // Helper function to create a test database + fn create_test_db() -> VectorDb { + let temp_dir = tempdir().unwrap(); + let config = DbConfig { + storage_type: StorageType::RocksDb, + index_type: IndexType::Flat, + data_path: temp_dir.path().to_path_buf(), + dimension: 3, + }; + init_api(config).unwrap() + } + + #[test] + fn test_insert_and_get() { + let db = create_test_db(); + let vector = vec![1.0, 2.0, 3.0]; + let payload = Payload { + content_type: ContentType::Text, + content: "Test content".to_string(), + }; + + // Test insert + let id = db.insert(vector.clone(), payload.clone()).unwrap(); + assert!(id != Uuid::nil()); + + // Test get + let point = db.get(id).unwrap().unwrap(); + assert_eq!(point.id, id); + assert_eq!(point.vector.as_ref().unwrap(), &vector); + assert_eq!(point.payload.as_ref().unwrap(), &payload); + assert_eq!( + point.payload.as_ref().unwrap().content_type, + ContentType::Text + ); + assert_eq!(point.payload.as_ref().unwrap().content, "Test content"); + } + + #[test] + fn test_delete() { + let db = create_test_db(); + let vector = vec![1.0, 2.0, 3.0]; + let payload = Payload { + content_type: ContentType::Text, + content: "Test content".to_string(), + }; + + // Insert a point + let id = db.insert(vector, payload).unwrap(); + + assert!(db.get(id).unwrap().is_some()); + db.delete(id).unwrap(); + assert!(db.get(id).unwrap().is_none()); + } + + #[test] + fn test_search() { + let db = create_test_db(); + + // Insert some points + let vectors = vec![ + vec![1.0, 0.0, 0.0], + vec![0.0, 1.0, 0.0], + vec![0.0, 0.0, 1.0], + ]; + + let mut ids = Vec::new(); + for vector in vectors { + let payload = Payload { + content_type: ContentType::Text, + content: format!("Test content {vector:?}"), + }; + let id = db.insert(vector, payload).unwrap(); + ids.push(id); + } + + // Search for the closest vector to [1.0, 0.1, 0.1] + let query = vec![1.0, 0.1, 0.1]; + let results = db.search(query, Similarity::Cosine, 1).unwrap(); + + assert_eq!(results.len(), 1); + assert_eq!(results[0], ids[0]); // The first vector should be closest + } + + #[test] + fn test_search_limit() { + let db = create_test_db(); + + // Insert 5 points + let mut ids = Vec::new(); + for i in 0..5 { + let vector = vec![i as f32, 0.0, 0.0]; + let id = db + .insert( + vector, + Payload { + content_type: ContentType::Text, + content: format!("Test content {i}"), + }, + ) + .unwrap(); + ids.push(id); + } + + // Search with limit 3 + let query = vec![0.0, 0.0, 0.0]; + let results = db.search(query, Similarity::Euclidean, 3).unwrap(); + + assert_eq!(results.len(), 3); + } + + #[test] + fn test_empty_database() { + let db = create_test_db(); + + // Get non-existent point + assert!(db.get(Uuid::new_v4()).unwrap().is_none()); + + let query = vec![1.0, 2.0, 3.0]; + let results = db.search(query, Similarity::Cosine, 10).unwrap(); + assert_eq!(results.len(), 0); + } + + #[test] + fn test_list_vectors() { + let db = create_test_db(); + // insert some points + let mut ids = Vec::new(); + for i in 0..10 { + let i = i as f32; + let vector = vec![i, i + 1.0, i + 2.0]; + let id = db + .insert( + vector, + Payload { + content_type: ContentType::Text, + content: format!("Test content {i}"), + }, + ) + .unwrap(); + ids.push(id); + } + + // list vectors with limit 5 + // list the values as well as their length + let (vectors, next_offset) = db.list(Uuid::nil(), 5).unwrap().unwrap(); + assert_eq!(vectors.len(), 5); + + // list next set of vectors + // list the values as well as their length + let (next_vectors, _) = db.list(next_offset, 5).unwrap().unwrap(); + assert_eq!(next_vectors.len(), 5); + } + + #[test] + fn test_build_index() { + let db = create_test_db(); + + // insert some points + for i in 0..10 { + let i = i as f32; + let vector = vec![i, i + 1.0, i + 2.0]; + db.insert( + vector, + Payload { + content_type: ContentType::Text, + content: format!("Test content {i}"), + }, + ) + .unwrap(); + } + + // rebuild the index + let inserted = db.build_index().unwrap(); + assert_eq!(inserted, 10); + } } diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml deleted file mode 100644 index c293076..0000000 --- a/crates/core/Cargo.toml +++ /dev/null @@ -1,11 +0,0 @@ -# crates/core/Cargo.toml - -[package] -name="core" -version = "0.1.0" -edition = "2021" -license = "MIT" - -[dependencies] -bincode = "1.3.3" -serde = { version = "1.0", features = ["derive"] } \ No newline at end of file diff --git a/crates/defs/Cargo.toml b/crates/defs/Cargo.toml new file mode 100644 index 0000000..eb58c78 --- /dev/null +++ b/crates/defs/Cargo.toml @@ -0,0 +1,12 @@ +# crates/defs/Cargo.toml + +[package] +name="defs" +version = "0.1.0" +edition = "2021" +license = "MIT" + +[dependencies] +bincode = "1.3.3" +serde = { version = "1.0", features = ["derive"] } +uuid.workspace = true diff --git a/crates/core/src/error.rs b/crates/defs/src/error.rs similarity index 91% rename from crates/core/src/error.rs rename to crates/defs/src/error.rs index a762944..1079c5e 100644 --- a/crates/core/src/error.rs +++ b/crates/defs/src/error.rs @@ -5,4 +5,5 @@ pub enum DbError { SerializationError(String), DeserializationError, IndexError(String), + LockError, } diff --git a/crates/core/src/lib.rs b/crates/defs/src/lib.rs similarity index 54% rename from crates/core/src/lib.rs rename to crates/defs/src/lib.rs index 3728e9b..c2a79bf 100644 --- a/crates/core/src/lib.rs +++ b/crates/defs/src/lib.rs @@ -1,6 +1,6 @@ pub mod error; pub mod types; -// Without re-exports, users would need to write core::types::SomeType instead of just core::SomeType. Re-exports simplify the API by flattening the module hierarchy. The * means "everything public" from that module. +// Without re-exports, users would need to write defs::types::SomeType instead of just defs::SomeType. Re-exports simplify the API by flattening the module hierarchy. The * means "everything public" from that module. pub use error::*; pub use types::*; diff --git a/crates/core/src/types.rs b/crates/defs/src/types.rs similarity index 58% rename from crates/core/src/types.rs rename to crates/defs/src/types.rs index 7230b58..d7a9504 100644 --- a/crates/core/src/types.rs +++ b/crates/defs/src/types.rs @@ -1,6 +1,8 @@ use serde::{Deserialize, Serialize}; +use std::cmp::Ordering; +use uuid::Uuid; -pub type PointId = u64; +pub type PointId = Uuid; /// Type of vector element. pub type Element = f32; @@ -16,8 +18,15 @@ pub enum StoredVector { } #[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq)] +pub enum ContentType { + Text, + Image, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] pub struct Payload { - // Define here how payload is managed + pub content_type: ContentType, + pub content: String, } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] @@ -42,6 +51,35 @@ pub enum Similarity { Cosine, } +// Struct which stores the distance between a vector and query vector and implements ordering traits +#[derive(Copy, Clone)] +pub struct DistanceOrderedVector<'q> { + // 'q : lifetime of query vector + pub distance: f32, + pub query_vector: &'q DenseVector, + pub point_id: Option, +} + +impl<'q> Ord for DistanceOrderedVector<'q> { + fn cmp(&self, other: &Self) -> Ordering { + self.distance.total_cmp(&other.distance) + } +} + +impl<'q> PartialOrd for DistanceOrderedVector<'q> { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl<'q> PartialEq for DistanceOrderedVector<'q> { + fn eq(&self, other: &Self) -> bool { + self.distance == other.distance + } +} + +impl<'q> Eq for DistanceOrderedVector<'q> {} + // Query Vector. Basically the type of query results that can be generated. Not implementing this but referencing here for furture reference // #[derive(Debug, Clone)] // pub enum QueryVector { diff --git a/crates/index/Cargo.toml b/crates/index/Cargo.toml index 501a5f8..7643ba3 100644 --- a/crates/index/Cargo.toml +++ b/crates/index/Cargo.toml @@ -7,5 +7,5 @@ edition = "2021" [dependencies] -core = { path = "../core" } - +defs = { path = "../defs" } +uuid.workspace = true \ No newline at end of file diff --git a/crates/index/src/flat.rs b/crates/index/src/flat.rs index 1788b45..3ede81b 100644 --- a/crates/index/src/flat.rs +++ b/crates/index/src/flat.rs @@ -1,4 +1,4 @@ -use core::{DbError, DenseVector, IndexedVector, PointId, Similarity}; +use defs::{DbError, DenseVector, DistanceOrderedVector, IndexedVector, PointId, Similarity}; use crate::{distance, VectorIndex}; @@ -43,38 +43,38 @@ impl VectorIndex for FlatIndex { similarity: Similarity, k: usize, ) -> Result, DbError> { - let mut scores = self + let scores = self .index .iter() - .map(|point| { - ( - point.id, - distance(point.vector.clone(), query_vector.clone(), similarity), - ) + .map(|point| DistanceOrderedVector { + distance: distance(point.vector.clone(), query_vector.clone(), similarity), + query_vector: &query_vector, + point_id: Some(point.id), }) .collect::>(); - // Sorting logic according to type of metric used - match similarity { - Similarity::Euclidean | Similarity::Manhattan | Similarity::Hamming => { - scores.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap()); - } - Similarity::Cosine => { - scores.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap()); + // select k smallest elements in scores using a max heap + let mut heap = std::collections::BinaryHeap::::new(); + for score in scores { + if heap.len() < k { + heap.push(score); + } else if score < *heap.peek().unwrap() { + heap.pop(); + heap.push(score); } } - - Ok(scores + Ok(heap + .into_sorted_vec() .into_iter() - .take(k) - .map(|(id, _)| id) - .collect::>()) + .map(|v| v.point_id.unwrap()) + .collect()) } } #[cfg(test)] mod tests { use super::*; + use uuid::Uuid; #[test] fn test_flat_index_new() { @@ -86,11 +86,11 @@ mod tests { fn test_flat_index_build() { let vectors = vec![ IndexedVector { - id: 1, + id: Uuid::new_v4(), vector: vec![1.0, 2.0, 3.0], }, IndexedVector { - id: 2, + id: Uuid::new_v4(), vector: vec![4.0, 5.0, 6.0], }, ]; @@ -102,7 +102,7 @@ mod tests { fn test_insert() { let mut index = FlatIndex::new(); let vector = IndexedVector { - id: 1, + id: Uuid::new_v4(), vector: vec![1.0, 2.0, 3.0], }; @@ -114,13 +114,14 @@ mod tests { #[test] fn test_delete_existing() { let mut index = FlatIndex::new(); + let existing_id = Uuid::new_v4(); let vector = IndexedVector { - id: 1, + id: existing_id, vector: vec![1.0, 2.0, 3.0], }; index.insert(vector).unwrap(); - let result = index.delete(1).unwrap(); + let result = index.delete(existing_id).unwrap(); assert!(result); assert_eq!(index.index.len(), 0); } @@ -129,12 +130,12 @@ mod tests { fn test_delete_non_existing() { let mut index = FlatIndex::new(); let vector = IndexedVector { - id: 1, + id: Uuid::new_v4(), vector: vec![1.0, 2.0, 3.0], }; index.insert(vector).unwrap(); - let result = index.delete(999).unwrap(); + let result = index.delete(Uuid::new_v4()).unwrap(); assert!(!result); assert_eq!(index.index.len(), 1); } @@ -142,21 +143,24 @@ mod tests { #[test] fn test_search_euclidean() { let mut index = FlatIndex::new(); + let id1 = Uuid::new_v4(); + let id2 = Uuid::new_v4(); + let id3 = Uuid::new_v4(); index .insert(IndexedVector { - id: 1, + id: id1, vector: vec![1.0, 1.0], }) .unwrap(); index .insert(IndexedVector { - id: 2, + id: id2, vector: vec![2.0, 2.0], }) .unwrap(); index .insert(IndexedVector { - id: 3, + id: id3, vector: vec![10.0, 10.0], }) .unwrap(); @@ -164,53 +168,59 @@ mod tests { let results = index .search(vec![0.0, 0.0], Similarity::Euclidean, 2) .unwrap(); - assert_eq!(results, vec![1, 2]); + assert_eq!(results, vec![id1, id2]); } #[test] fn test_search_cosine() { let mut index = FlatIndex::new(); + let id1 = Uuid::new_v4(); + let id2 = Uuid::new_v4(); + let id3 = Uuid::new_v4(); index .insert(IndexedVector { - id: 1, + id: id1, vector: vec![1.0, 0.0], }) .unwrap(); index .insert(IndexedVector { - id: 2, + id: id2, vector: vec![0.5, 0.5], }) .unwrap(); index .insert(IndexedVector { - id: 3, + id: id3, vector: vec![0.0, 1.0], }) .unwrap(); let results = index.search(vec![1.0, 1.0], Similarity::Cosine, 2).unwrap(); - assert_eq!(results, vec![2, 1]); + assert_eq!(results, vec![id2, id1]); } #[test] fn test_search_manhattan() { let mut index = FlatIndex::new(); + let id1 = Uuid::new_v4(); + let id2 = Uuid::new_v4(); + let id3 = Uuid::new_v4(); index .insert(IndexedVector { - id: 1, + id: id1, vector: vec![1.0, 1.0], }) .unwrap(); index .insert(IndexedVector { - id: 2, + id: id2, vector: vec![2.0, 2.0], }) .unwrap(); index .insert(IndexedVector { - id: 3, + id: id3, vector: vec![5.0, 5.0], }) .unwrap(); @@ -218,27 +228,30 @@ mod tests { let results = index .search(vec![0.0, 0.0], Similarity::Manhattan, 2) .unwrap(); - assert_eq!(results, vec![1, 2]); + assert_eq!(results, vec![id1, id2]); } #[test] fn test_search_hamming() { let mut index = FlatIndex::new(); + let id1 = Uuid::new_v4(); + let id2 = Uuid::new_v4(); + let id3 = Uuid::new_v4(); index .insert(IndexedVector { - id: 1, + id: id1, vector: vec![1.0, 0.0, 1.0, 0.0], }) .unwrap(); index .insert(IndexedVector { - id: 2, + id: id2, vector: vec![1.0, 0.0, 0.0, 0.0], }) .unwrap(); index .insert(IndexedVector { - id: 3, + id: id3, vector: vec![0.0, 0.0, 0.0, 0.0], }) .unwrap(); @@ -246,7 +259,7 @@ mod tests { let results = index .search(vec![1.0, 0.0, 0.0, 0.0], Similarity::Hamming, 2) .unwrap(); - assert_eq!(results, vec![2, 3]); + assert_eq!(results, vec![id2, id3]); } #[test] diff --git a/crates/index/src/lib.rs b/crates/index/src/lib.rs index 50fad64..ef93755 100644 --- a/crates/index/src/lib.rs +++ b/crates/index/src/lib.rs @@ -1,4 +1,4 @@ -use core::{DbError, DenseVector, IndexedVector, PointId, Similarity}; +use defs::{DbError, DenseVector, IndexedVector, PointId, Similarity}; pub mod flat; @@ -42,7 +42,7 @@ pub fn distance(a: DenseVector, b: DenseVector, dist_type: Similarity) -> f32 { let score: Vec = a .iter() .zip(b.iter()) - .map(|(&x, &y)| (if (x - y) > 1e-8 { 1f32 } else { 0f32 })) + .map(|(&x, &y)| if (x - y).abs() > 1e-8 { 1f32 } else { 0f32 }) .collect(); score.iter().sum::() } @@ -53,7 +53,7 @@ pub fn distance(a: DenseVector, b: DenseVector, dist_type: Similarity) -> f32 { let q = q_score.iter().sum::().sqrt(); let r_score: Vec = b.iter().map(|&n| n * n).collect(); let r = r_score.iter().sum::().sqrt(); - p / (q * r) + 1.0 - (p / (q * r)) } } } diff --git a/crates/server/Cargo.toml b/crates/server/Cargo.toml index 6e2a72f..22fe704 100644 --- a/crates/server/Cargo.toml +++ b/crates/server/Cargo.toml @@ -7,6 +7,6 @@ edition = "2021" # tokio.workspace = true api = { path = "../api" } -core = { path = "../core" } +defs = { path = "../defs" } index = { path = "../index" } -# storage = { path = "../storage" } \ No newline at end of file +# storage = { path = "../storage" } diff --git a/crates/server/src/main.rs b/crates/server/src/main.rs index 9ab5dea..10f5b73 100644 --- a/crates/server/src/main.rs +++ b/crates/server/src/main.rs @@ -1,7 +1,7 @@ use std::fmt::Error; // Import from other crates -// use core; // Import the entire crate -use api::init_api_server; +// use defs; // Import the entire crate +// use api::init_api_server; // use index::some_module; // Import specific module // use storage::{Type1, Type2}; // Import specific types // use api::prelude::*; // Import everything from prelude @@ -10,6 +10,6 @@ fn main() -> Result<(), Error> { // Start tracing // Load configs for DB // Start API and/or gRPC server - let _ = init_api_server(); + // let _ = init_api_server(); Ok(()) } diff --git a/crates/storage/Cargo.toml b/crates/storage/Cargo.toml index 73f2436..70005cb 100644 --- a/crates/storage/Cargo.toml +++ b/crates/storage/Cargo.toml @@ -6,6 +6,8 @@ edition = "2021" [dependencies] bincode = "1.3.3" rocksdb = "0.21.0" +uuid.workspace = true -core = { path = "../core" } -index = { path = "../index" } \ No newline at end of file +defs = { path = "../defs" } +index = { path = "../index" } +tempfile = "3.23.0" diff --git a/crates/storage/src/in_memory.rs b/crates/storage/src/in_memory.rs index ea078ab..cc6a716 100644 --- a/crates/storage/src/in_memory.rs +++ b/crates/storage/src/in_memory.rs @@ -1,5 +1,5 @@ use crate::StorageEngine; -use core::{DbError, DenseVector, Payload, PointId}; +use defs::{DbError, DenseVector, Payload, PointId}; pub struct MemoryStorage { // define here how MemoryStorage will be defined @@ -38,4 +38,11 @@ impl StorageEngine for MemoryStorage { fn get_vector(&self, _id: PointId) -> Result, DbError> { Ok(None) } + fn list_vectors( + &self, + _offset: PointId, + _limit: usize, + ) -> Result, PointId)>, DbError> { + Ok(None) + } } diff --git a/crates/storage/src/lib.rs b/crates/storage/src/lib.rs index 07c1107..db49257 100644 --- a/crates/storage/src/lib.rs +++ b/crates/storage/src/lib.rs @@ -1,4 +1,4 @@ -use core::{DbError, DenseVector, Payload, PointId}; +use defs::{DbError, DenseVector, Payload, PointId}; use std::path::PathBuf; use std::sync::Arc; @@ -15,6 +15,11 @@ pub trait StorageEngine { fn get_payload(&self, id: PointId) -> Result, DbError>; fn delete_point(&self, id: PointId) -> Result<(), DbError>; fn contains_point(&self, id: PointId) -> Result; + fn list_vectors( + &self, + offset: PointId, + limit: usize, + ) -> Result, PointId)>, DbError>; } pub mod in_memory; diff --git a/crates/storage/src/rocks_db.rs b/crates/storage/src/rocks_db.rs index 9a4ec7f..a86ad79 100644 --- a/crates/storage/src/rocks_db.rs +++ b/crates/storage/src/rocks_db.rs @@ -2,7 +2,7 @@ use crate::StorageEngine; use bincode::{deserialize, serialize}; -use core::{DbError, DenseVector, Payload, Point, PointId}; +use defs::{DbError, DenseVector, Payload, Point, PointId}; use rocksdb::{Error, Options, DB}; use std::path::PathBuf; @@ -60,7 +60,7 @@ impl StorageEngine for RocksDbStorage { payload, }; let value = serialize(&point).map_err(|e| DbError::SerializationError(e.to_string()))?; - match self.db.put(key, value.as_ref() as &[u8]) { + match self.db.put(key.as_bytes(), value.as_slice()) { Ok(_) => Ok(()), Err(e) => Err(DbError::StorageError(e.into_string())), } @@ -121,16 +121,57 @@ impl StorageEngine for RocksDbStorage { Ok(value.vector) } + + fn list_vectors( + &self, + offset: PointId, + limit: usize, + ) -> Result, PointId)>, DbError> { + if limit < 1 { + return Ok(None); + } + + let mut result = Vec::with_capacity(limit); + let iter = self.db.iterator(rocksdb::IteratorMode::From( + offset.to_string().as_bytes(), + rocksdb::Direction::Forward, + )); + let mut last_id = offset; + + for item in iter { + let (_, v) = item.map_err(|e| DbError::StorageError(e.into_string()))?; + let point: Point = deserialize(&v).map_err(|_| DbError::DeserializationError)?; + + if point.id <= offset { + continue; + } + + if let Some(vec) = point.vector { + last_id = point.id; + result.push((point.id, vec)); + if result.len() == limit { + break; + } + } + } + Ok(Some((result, last_id))) + } } #[cfg(test)] mod tests { use super::*; + use defs::ContentType; + use uuid::Uuid; + + use tempfile::tempdir; fn create_test_db() -> (RocksDbStorage, String) { - let dir_path = String::from("/home/hawkeye/works/vector-db/testdb"); - let db = RocksDbStorage::new(dir_path.clone()).expect("Failed to create RocksDB"); - (db, dir_path) + let temp_dir = tempdir().unwrap(); + let temp_dir_path = temp_dir.path().to_str().unwrap().to_string(); + + let db = RocksDbStorage::new(temp_dir_path.clone()).expect("Failed to create RocksDB"); + (db, temp_dir_path) } #[test] @@ -143,9 +184,12 @@ mod tests { #[test] fn test_insert_and_get_vector() { let (db, path) = create_test_db(); - let id = 1; + let id = Uuid::new_v4(); let vector = Some(vec![0.1, 0.2, 0.3]); - let payload = None; + let payload = Some(Payload { + content_type: ContentType::Text, + content: "Test".to_string(), + }); assert!(db.insert_point(id, vector.clone(), payload).is_ok()); let result = db.get_vector(id).unwrap(); @@ -157,13 +201,21 @@ mod tests { #[test] fn test_insert_and_get_payload() { let (db, path) = create_test_db(); - let id = 2; - let payload = Some(Payload {}); + let id = Uuid::new_v4(); + let payload = Some(Payload { + content_type: ContentType::Text, + content: "Test".to_string(), + }); let vector = None; + // Move payload into insert_point and recreate expected for comparison assert!(db.insert_point(id, vector, payload).is_ok()); let result = db.get_payload(id).unwrap(); - assert_eq!(result, payload); + let expected = Some(Payload { + content_type: ContentType::Text, + content: "Test".to_string(), + }); + assert_eq!(result, expected); std::fs::remove_dir_all(path).unwrap_or_default(); } @@ -171,12 +223,16 @@ mod tests { #[test] fn test_contains_point() { let (db, path) = create_test_db(); - let id = 3; + let id = Uuid::new_v4(); + let payload = Some(Payload { + content_type: ContentType::Text, + content: "Test".to_string(), + }); assert!(!db.contains_point(id).unwrap()); let vector = Some(vec![0.4, 0.5, 0.6]); - db.insert_point(id, vector, None).unwrap(); + db.insert_point(id, vector, payload).unwrap(); assert!(db.contains_point(id).unwrap()); @@ -186,12 +242,15 @@ mod tests { #[test] fn test_delete_point() { let (db, path) = create_test_db(); - let id = 4; + let id = Uuid::new_v4(); + let payload = Some(Payload { + content_type: ContentType::Text, + content: "Test".to_string(), + }); let vector = vec![0.7, 0.8, 0.9]; - let payload = Payload {}; - db.insert_point(id, Some(vector), Some(payload)).unwrap(); + db.insert_point(id, Some(vector), payload).unwrap(); assert!(db.contains_point(id).unwrap()); @@ -207,7 +266,7 @@ mod tests { #[test] fn test_get_nonexistent_vector() { let (db, path) = create_test_db(); - let id = 999; + let id = Uuid::new_v4(); assert_eq!(db.get_vector(id).unwrap(), None); @@ -217,7 +276,7 @@ mod tests { #[test] fn test_get_nonexistent_payload() { let (db, path) = create_test_db(); - let id = 999; + let id = Uuid::new_v4(); assert_eq!(db.get_payload(id).unwrap(), None); diff --git a/crates/tui/Cargo.toml b/crates/tui/Cargo.toml index 8c319f4..b745781 100644 --- a/crates/tui/Cargo.toml +++ b/crates/tui/Cargo.toml @@ -10,7 +10,12 @@ crossterm = "0.27" tokio.workspace = true serde.workspace = true anyhow.workspace = true +uuid.workspace = true chrono = { version = "0.4", features = ["serde"] } -core = { path = "../core" } +defs = { path = "../defs" } storage = { path = "../storage" } index = { path = "../index" } +api = { path = "../api" } +serde_json.workspace = true +reqwest = { version = "0.12", features = ["json", "blocking", "multipart"] } +dotenv = "0.15" \ No newline at end of file diff --git a/crates/tui/src/app/database.rs b/crates/tui/src/app/database.rs index 0efc7e9..849bca7 100644 --- a/crates/tui/src/app/database.rs +++ b/crates/tui/src/app/database.rs @@ -1,11 +1,14 @@ +use api::{init_api, DbConfig, VectorDb}; +use index::IndexType; use std::io; use std::path::PathBuf; use std::sync::Arc; -use storage::{create_storage_engine, StorageEngine, StorageType}; +use storage::{StorageEngine, StorageType}; pub struct DatabaseManager { pub current_db_path: Option, pub storage_engine: Option>, + pub api_db: Option, pub available_databases: Vec<(String, PathBuf)>, pub selected_database: Option<(String, PathBuf)>, } @@ -15,15 +18,40 @@ impl DatabaseManager { Self { current_db_path: None, storage_engine: None, + api_db: None, available_databases: Vec::new(), selected_database: None, } } + fn close_current_database(&mut self) { + self.storage_engine = None; + self.current_db_path = None; + self.selected_database = None; + self.api_db = None; + } + pub fn create_new_database(&mut self, name: String, path: PathBuf) -> io::Result<()> { - match create_storage_engine(StorageType::RocksDb, &path) { - Ok(storage) => { - self.storage_engine = Some(storage); + // Drop any database currently open before creating a new one + self.close_current_database(); + // Check if database already exists to avoid name collisions + if path.exists() { + return Err(io::Error::new( + io::ErrorKind::AlreadyExists, + format!("Database '{name}' already exists!"), + )); + } + + let cfg = DbConfig { + storage_type: StorageType::RocksDb, + index_type: IndexType::Flat, + data_path: path.clone(), + dimension: 512, + }; + + match init_api(cfg) { + Ok(db) => { + self.api_db = Some(db); self.current_db_path = Some(path.clone()); self.available_databases.push((name.clone(), path.clone())); self.selected_database = Some((name, path)); @@ -37,6 +65,8 @@ impl DatabaseManager { } pub fn select_database(&mut self, name: String, path: PathBuf) -> io::Result<()> { + self.close_current_database(); + if !path.exists() { return Err(io::Error::new( io::ErrorKind::NotFound, @@ -44,8 +74,25 @@ impl DatabaseManager { )); } - self.selected_database = Some((name, path)); - Ok(()) + let cfg = DbConfig { + storage_type: StorageType::RocksDb, + index_type: IndexType::Flat, + data_path: path.clone(), + dimension: 512, + }; + + match init_api(cfg) { + Ok(db) => { + self.api_db = Some(db); + self.current_db_path = Some(path.clone()); + self.selected_database = Some((name, path)); + Ok(()) + } + Err(e) => Err(io::Error::new( + io::ErrorKind::Other, + format!("Failed to open database: {:?}", e), + )), + } } pub fn delete_database(&mut self, path: &PathBuf) -> io::Result<()> { @@ -54,6 +101,7 @@ impl DatabaseManager { if current_path == path { self.storage_engine = None; self.current_db_path = None; + self.api_db = None; } } diff --git a/crates/tui/src/app/embeddings.rs b/crates/tui/src/app/embeddings.rs new file mode 100644 index 0000000..9e9e59d --- /dev/null +++ b/crates/tui/src/app/embeddings.rs @@ -0,0 +1,128 @@ +use reqwest::blocking::{multipart, Client, Response}; +use reqwest::StatusCode; +use serde::de::DeserializeOwned; +use serde::Deserialize; +use std::env; +use std::path::Path; +use std::time::Duration; + +#[derive(Debug)] +pub struct EmbeddingClient { + client: Client, + text_url: String, + image_url: String, +} + +#[derive(Debug)] +pub enum EmbeddingError { + Io(std::io::Error), + Http(reqwest::Error), + UnexpectedStatus(StatusCode, String), +} + +impl std::fmt::Display for EmbeddingError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + EmbeddingError::Io(err) => write!(f, "I/O error: {err}"), + EmbeddingError::Http(err) => write!(f, "HTTP error: {err}"), + EmbeddingError::UnexpectedStatus(status, body) => { + write!(f, "Server returned {status}: {body}") + } + } + } +} + +impl std::error::Error for EmbeddingError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + EmbeddingError::Io(err) => Some(err), + EmbeddingError::Http(err) => Some(err), + EmbeddingError::UnexpectedStatus(_, _) => None, + } + } +} + +impl From for EmbeddingError { + fn from(value: std::io::Error) -> Self { + Self::Io(value) + } +} + +impl From for EmbeddingError { + fn from(value: reqwest::Error) -> Self { + Self::Http(value) + } +} + +impl EmbeddingClient { + pub fn new() -> Self { + dotenv::dotenv().ok(); + let client = Client::builder() + .timeout(Duration::from_secs(15)) + .build() + .unwrap_or_else(|_| Client::new()); + let text_url = env::var("TEXT_EMBEDDING_URL").expect("TEXT_EMBEDDING_URL must be set"); + let image_url = env::var("IMAGE_EMBEDDING_URL").expect("IMAGE_EMBEDDING_URL must be set"); + + Self { + client, + text_url, + image_url, + } + } + + pub fn text_embeddings(&self, text: &str) -> Result, EmbeddingError> { + let payload = serde_json::json!({ "text": text }); + let response = self.client.post(&self.text_url).json(&payload).send()?; + + Self::parse_response::(response).map(|body| body.result) + } + + pub fn image_embeddings(&self, path: &Path) -> Result, EmbeddingError> { + let file = std::fs::File::open(path)?; + let file_name = path + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or("image"); + + let part = multipart::Part::reader(file) + .file_name(file_name.to_string()) + .mime_str("application/octet-stream")?; + + let form = multipart::Form::new().part("file", part); + + let response = self.client.post(&self.image_url).multipart(form).send()?; + + Self::parse_response::(response).map(|body| body.result) + } + + fn parse_response(response: Response) -> Result + where + T: DeserializeOwned, + { + if response.status().is_success() { + let parsed = response.json::()?; + Ok(parsed) + } else { + let status = response.status(); + let body = response.text().unwrap_or_else(|_| "".to_string()); + Err(EmbeddingError::UnexpectedStatus(status, body)) + } + } +} + +impl Default for EmbeddingClient { + fn default() -> Self { + Self::new() + } +} + +#[derive(Debug, Deserialize)] +struct TextEmbeddingResponse { + result: Vec, +} + +#[derive(Debug, Deserialize)] +struct ImageEmbeddingResponse { + result: Vec, +} diff --git a/crates/tui/src/app/events.rs b/crates/tui/src/app/events.rs index 4e50682..97b1122 100644 --- a/crates/tui/src/app/events.rs +++ b/crates/tui/src/app/events.rs @@ -1,7 +1,12 @@ -use super::{App, AppState, ModalType}; +use super::{App, AppState, ModalType, VectorListItem}; use crossterm::event::{Event, KeyCode, KeyEvent}; +use defs::{ContentType, Payload, Similarity}; use std::io; use std::path::PathBuf; +use uuid::Uuid; + +// Set how many vectors to fetch per function call in list_vectors +const VECTOR_LIST_LIMIT: usize = 50; pub fn handle_event(app: &mut App, event: Event) -> io::Result<()> { if let Event::Key(key) = event { @@ -32,13 +37,28 @@ fn handle_key_event(app: &mut App, key: KeyEvent) -> io::Result<()> { fn handle_modal_input(app: &mut App, key: KeyEvent) -> io::Result<()> { match key.code { - KeyCode::Enter => { - execute_modal_action(app)?; - app.modal.close(); - } + KeyCode::Enter => match execute_modal_action(app) { + Ok(()) => { + // keep result modals open until user dismisses them; close others + match app.modal.modal_type() { + Some(ModalType::Success) + | Some(ModalType::Failure) + | Some(ModalType::Error) + | Some(ModalType::VectorDetails) + | Some(ModalType::ListVectors) => { + // leave open + } + _ => app.modal.close(), + } + } + Err(err) => app.modal.show_error(err.to_string()), + }, KeyCode::Esc => { app.modal.close(); } + KeyCode::Tab => { + app.switch_field(); + } KeyCode::Char(c) => { app.modal.add_char(c); } @@ -52,33 +72,103 @@ fn handle_modal_input(app: &mut App, key: KeyEvent) -> io::Result<()> { fn handle_modal_navigation(app: &mut App, key: KeyEvent) -> io::Result<()> { match key.code { - KeyCode::Enter => { - if matches!( - app.modal.modal_type(), - Some(ModalType::DatabaseList) | Some(ModalType::DeleteDatabase) - ) { - execute_selected_db_operation(app)?; - } else { - app.modal.enable_input_mode(); + KeyCode::Enter => match app.modal.modal_type() { + Some(ModalType::DatabaseList) | Some(ModalType::DeleteDatabase) => { + if let Err(err) = execute_selected_db_operation(app) { + app.modal.show_error(err.to_string()); + } } - } - KeyCode::Esc => { - app.modal.close(); - } - KeyCode::Up => { + Some(ModalType::ConfirmDeleteDatabase) => { + if let Err(err) = execute_selected_db_operation(app) { + app.modal.show_error(err.to_string()); + } + } + Some(ModalType::ListVectors) => { + open_selected_vector_from_list(app)?; + } + Some(ModalType::Error) => app.modal.close(), + Some(ModalType::VectorDetails) => { + close_vector_detail_modal(app); + } + _ => app.modal.enable_input_mode(), + }, + KeyCode::Esc => match app.modal.modal_type() { + Some(ModalType::Success) + | Some(ModalType::Failure) + | Some(ModalType::VectorDetails) + if app.vector_list_post_restore && !app.vector_list_items.is_empty() => + { + reopen_vector_list_modal(app); + } + Some(ModalType::ConfirmDeleteDatabase) => { + app.vector_list_post_restore = false; + restore_delete_database_modal(app); + } + Some(ModalType::ListVectors) => { + app.vector_list_post_restore = false; + app.vector_list_next_offset = None; + app.modal.close(); + } + Some(ModalType::VectorDetails) => { + close_vector_detail_modal(app); + } + _ => { + app.vector_list_post_restore = false; + app.modal.close(); + } + }, + KeyCode::Up => match app.modal.modal_type() { + Some(ModalType::DatabaseList) | Some(ModalType::DeleteDatabase) => { + app.select_previous(); + } + Some(ModalType::ListVectors) => { + if app.modal.selected_index() > 0 { + app.modal.select_previous(); + app.vector_list_selected_index = app.modal.selected_index(); + } + } + Some(ModalType::ConfirmDeleteDatabase) => { + app.modal.set_selected_index(0, 2); + } + _ => {} + }, + KeyCode::Down => match app.modal.modal_type() { + Some(ModalType::DatabaseList) | Some(ModalType::DeleteDatabase) => { + app.select_next(); + } + Some(ModalType::ListVectors) => { + let current_len = app.vector_list_items.len(); + if current_len == 0 { + return Ok(()); + } + + let mut max_items = current_len; + if app.modal.selected_index() + 1 >= current_len && fetch_next_vector_page(app)? { + max_items = app.vector_list_items.len(); + } + + app.modal.select_next(max_items); + app.vector_list_selected_index = app.modal.selected_index(); + } + Some(ModalType::ConfirmDeleteDatabase) => { + app.modal.set_selected_index(1, 2); + } + _ => {} + }, + KeyCode::Left => { if matches!( app.modal.modal_type(), - Some(ModalType::DatabaseList) | Some(ModalType::DeleteDatabase) + Some(ModalType::ConfirmDeleteDatabase) ) { - app.select_previous(); + app.modal.set_selected_index(0, 2); } } - KeyCode::Down => { + KeyCode::Right => { if matches!( app.modal.modal_type(), - Some(ModalType::DatabaseList) | Some(ModalType::DeleteDatabase) + Some(ModalType::ConfirmDeleteDatabase) ) { - app.select_next(); + app.modal.set_selected_index(1, 2); } } _ => {} @@ -88,7 +178,11 @@ fn handle_modal_navigation(app: &mut App, key: KeyEvent) -> io::Result<()> { fn handle_database_keys(app: &mut App, key: KeyEvent) -> io::Result<()> { match key.code { - KeyCode::Enter => open_database_modal(app)?, + KeyCode::Enter => { + if let Err(err) = open_database_modal(app) { + app.modal.show_error(err.to_string()); + } + } KeyCode::Up => app.select_previous(), KeyCode::Down => app.select_next(), KeyCode::Left => app.previous_page(), @@ -102,7 +196,22 @@ fn handle_database_keys(app: &mut App, key: KeyEvent) -> io::Result<()> { fn handle_general_keys(app: &mut App, key: KeyEvent) { match key.code { KeyCode::Char('q') | KeyCode::Esc => app.quit(), - KeyCode::Right | KeyCode::Enter => app.next_page(), + KeyCode::Right => app.next_page(), + KeyCode::Enter => match app.state { + AppState::VectorOperations => match app.vector_selected { + 0 => { + if let Err(err) = initialize_vector_listing(app) { + app.modal.show_error(err.to_string()); + } + } + 1 => app.modal.show_delete_vector(), + 2 => app.modal.show_search_similar_vectors(), + 3 => app.modal.show_text_embedding(), + 4 => app.modal.show_image_embedding(), + _ => {} + }, + _ => app.next_page(), + }, KeyCode::Left => app.previous_page(), KeyCode::Up => app.select_previous(), KeyCode::Down => app.select_next(), @@ -132,8 +241,9 @@ fn execute_modal_action(app: &mut App) -> io::Result<()> { let input = app.modal.get_input_value(); if !input.is_empty() { let name = input; - let path = PathBuf::from(format!("./databases/{}", name)); - app.database.create_new_database(name, path)?; + let path = PathBuf::from(format!("./databases/{name}")); + app.database.create_new_database(name.clone(), path)?; + app.modal.show_success(format!("Created database '{name}'")); } } Some(ModalType::DatabaseList) => { @@ -144,9 +254,185 @@ fn execute_modal_action(app: &mut App) -> io::Result<()> { } Some(ModalType::DeleteDatabase) => { let selected_index = app.modal.selected_index(); - if let Some((_, path)) = app.database.available_databases.get(selected_index) { - let path = path.clone(); - app.database.delete_database(&path)?; + if let Some((name, path)) = app.database.available_databases.get(selected_index) { + app.pending_delete_database = Some((name.clone(), path.clone())); + app.pending_delete_database_index = Some(selected_index); + app.modal.show_confirm_delete_database(name.clone()); + } + } + + Some(ModalType::DeleteVector) => { + let input = app.modal.get_input_value(); + let id = Uuid::parse_str(input.trim()).map_err(|_| { + io::Error::new(io::ErrorKind::InvalidInput, "ID should be a valid UUID v4!") + })?; + if let Some(db) = &app.database.api_db { + match db.get(id).map_err(to_io)? { + Some(_) => match db.delete(id).map_err(to_io) { + Ok(()) => app.modal.show_success(format!("Deleted vector id={id}")), + Err(e) => app.modal.show_failure(format!("DB error: {e}")), + }, + None => app + .modal + .show_failure(format!("Vector with id={id} not found")), + } + } else { + app.modal.show_error("No database selected!"); + } + } + Some(ModalType::SearchSimilarVectors) => { + let k_text = app.modal.get_input_value(); + let text_raw = app.modal.secondary_input().to_string(); + + let k = k_text.parse::().ok().filter(|value| *value > 0); + if k.is_none() { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "k should be a positive integer!", + )); + } + let k = k.unwrap(); + + let trimmed_text = text_raw.trim(); + if trimmed_text.is_empty() { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Text cannot be empty!", + )); + } + + let Some(db) = &app.database.api_db else { + app.modal.show_error("No database selected!"); + return Ok(()); + }; + + let query = match app.embeddings.text_embeddings(trimmed_text) { + Ok(v) => v, + Err(err) => { + app.modal + .show_failure(format!("Failed to generate embedding: {err}")); + return Ok(()); + } + }; + + let ids = db.search(query, Similarity::Cosine, k).map_err(to_io)?; + + app.vector_list_items.clear(); + app.vector_detail = None; + app.vector_list_next_offset = None; + app.vector_list_post_restore = false; + app.vector_list_selected_index = 0; + + for id in ids.into_iter() { + if let Some(point) = db.get(id).map_err(to_io)? { + if let Some(vector) = point.vector { + app.vector_list_items.push(VectorListItem { + id: point.id, + vector, + payload: point.payload, + }); + } + } + } + + if app.vector_list_items.is_empty() { + app.modal + .show_failure("Unable to display results. Try a different query vector."); + return Ok(()); + } + + app.modal.show_vector_list(); + let len = app.vector_list_items.len(); + app.modal.set_selected_index(0, len); + } + Some(ModalType::TextEmbedding) => { + let text_raw = app.modal.secondary_input().to_string(); + + let trimmed_text = text_raw.trim(); + + if trimmed_text.is_empty() { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Text cannot be empty!", + )); + } + + let Some(db) = &app.database.api_db else { + app.modal.show_error("No database selected!"); + return Ok(()); + }; + + match app.embeddings.text_embeddings(trimmed_text) { + Ok(vector) => { + let dims = vector.len(); + let payload = Payload { + content_type: ContentType::Text, + content: trimmed_text.to_string(), + }; + + match db.insert(vector, payload).map_err(to_io) { + Ok(new_id) => app.modal.show_success(format!( + "Text embedding inserted (id={new_id}, {dims} dims)." + )), + Err(err) => app + .modal + .show_failure(format!("Failed to insert embedding: {err}")), + } + } + Err(err) => app + .modal + .show_failure(format!("Failed to generate embedding: {err}")), + } + } + Some(ModalType::ImageEmbedding) => { + let path_raw = app.modal.secondary_input().to_string(); + + let trimmed_path = path_raw.trim(); + + if trimmed_path.is_empty() { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Image path cannot be empty!", + )); + } + + let Some(db) = &app.database.api_db else { + app.modal.show_error("No database selected!"); + return Ok(()); + }; + + let path = PathBuf::from(trimmed_path); + if !path.exists() { + app.modal + .show_failure(format!("Image not found at path '{trimmed_path}'")); + return Ok(()); + } + if !path.is_file() { + app.modal + .show_failure(format!("Path '{trimmed_path}' is not a file")); + return Ok(()); + } + + match app.embeddings.image_embeddings(&path) { + Ok(vector) => { + let dims = vector.len(); + let payload = Payload { + content_type: ContentType::Image, + content: trimmed_path.to_string(), + }; + + match db.insert(vector, payload).map_err(to_io) { + Ok(new_id) => app.modal.show_success(format!( + "Image embedding inserted (id={new_id}, {dims} dims)." + )), + Err(err) => app + .modal + .show_failure(format!("Failed to insert embedding: {err}")), + } + } + Err(err) => app + .modal + .show_failure(format!("Failed to generate embedding: {err}")), } } _ => {} @@ -154,6 +440,23 @@ fn execute_modal_action(app: &mut App) -> io::Result<()> { Ok(()) } +fn restore_delete_database_modal(app: &mut App) { + let index = app + .pending_delete_database_index + .unwrap_or_else(|| app.modal.selected_index()); + app.modal.show_delete_database(); + let max_items = app.database.available_databases.len(); + app.modal + .set_selected_index(index.min(max_items.saturating_sub(1)), max_items); + app.pending_delete_database = None; + app.pending_delete_database_index = None; + app.vector_list_post_restore = false; +} + +fn to_io(e: E) -> io::Error { + io::Error::other(format!("{e:?}")) +} + fn execute_selected_db_operation(app: &mut App) -> io::Result<()> { match app.modal.modal_type() { Some(ModalType::DatabaseList) => { @@ -165,13 +468,159 @@ fn execute_selected_db_operation(app: &mut App) -> io::Result<()> { } Some(ModalType::DeleteDatabase) => { let selected_index = app.modal.selected_index(); - if let Some((_, path)) = app.database.available_databases.get(selected_index) { - let path = path.clone(); - app.database.delete_database(&path)?; - app.modal.close(); + if let Some((name, path)) = app.database.available_databases.get(selected_index) { + app.pending_delete_database = Some((name.clone(), path.clone())); + app.pending_delete_database_index = Some(selected_index); + app.modal.show_confirm_delete_database(name.clone()); + } + } + Some(ModalType::ConfirmDeleteDatabase) => { + let confirm = app.modal.selected_index() == 0; + if confirm { + if let Some((name, path)) = app.pending_delete_database.take() { + app.database.delete_database(&path)?; + app.pending_delete_database_index = None; + app.modal.show_success(format!("Deleted database '{name}'")); + } + } else { + restore_delete_database_modal(app); } } _ => {} } Ok(()) } + +fn initialize_vector_listing(app: &mut App) -> io::Result<()> { + if app.database.api_db.is_none() { + app.modal.show_error("No database selected!"); + return Ok(()); + } + + app.vector_list_items.clear(); + app.vector_list_next_offset = Some(Uuid::nil()); + app.vector_list_post_restore = false; + app.vector_list_selected_index = 0; + app.vector_detail = None; + + if fetch_next_vector_page(app)? { + app.modal.show_vector_list(); + let len = app.vector_list_items.len(); + app.modal.set_selected_index(0, len); + } else { + app.modal + .show_failure("No vectors found for the current query."); + } + + Ok(()) +} + +fn fetch_next_vector_page(app: &mut App) -> io::Result { + let Some(db) = &app.database.api_db else { + app.modal.show_error("No database selected!"); + return Ok(false); + }; + + let Some(offset) = app.vector_list_next_offset else { + return Ok(false); + }; + + let response = db.list(offset, VECTOR_LIST_LIMIT).map_err(to_io)?; + + let Some((vectors, next_offset)) = response else { + app.vector_list_next_offset = None; + return Ok(false); + }; + + if vectors.is_empty() { + app.vector_list_next_offset = None; + return Ok(false); + } + + let vector_count = vectors.len(); + + for (id, vector) in vectors.into_iter() { + let payload = db.get(id).map_err(to_io)?.and_then(|p| p.payload); + app.vector_list_items.push(VectorListItem { + id, + vector, + payload, + }); + } + + if vector_count == VECTOR_LIST_LIMIT { + app.vector_list_next_offset = Some(next_offset); + } else { + app.vector_list_next_offset = None; + } + + Ok(true) +} + +fn open_selected_vector_from_list(app: &mut App) -> io::Result<()> { + let selected_index = app.modal.selected_index(); + if let Some(item) = app.vector_list_items.get(selected_index) { + show_vector_info(app, item.id)?; + app.vector_list_selected_index = selected_index; + app.vector_list_post_restore = + matches!(app.modal.modal_type(), Some(ModalType::VectorDetails)); + } + Ok(()) +} + +fn reopen_vector_list_modal(app: &mut App) { + let len = app.vector_list_items.len(); + if len == 0 { + app.vector_list_post_restore = false; + app.vector_detail = None; + app.modal.close(); + return; + } + + let desired_index = app.vector_list_selected_index.min(len.saturating_sub(1)); + + app.modal.show_vector_list(); + app.modal.set_selected_index(desired_index, len); + app.vector_list_selected_index = desired_index; + app.vector_list_post_restore = false; + app.vector_detail = None; +} + +fn close_vector_detail_modal(app: &mut App) { + if app.vector_list_post_restore && !app.vector_list_items.is_empty() { + reopen_vector_list_modal(app); + } else { + app.vector_detail = None; + app.vector_list_post_restore = false; + app.modal.close(); + } +} + +fn show_vector_info(app: &mut App, id: Uuid) -> io::Result<()> { + let Some(db) = &app.database.api_db else { + app.modal.show_error("No database selected!"); + return Ok(()); + }; + + app.vector_list_post_restore = false; + + let point_opt = db.get(id).map_err(to_io)?; + if point_opt.is_none() { + app.vector_detail = None; + app.modal + .show_failure(format!("Vector with id={id} not found!")); + return Ok(()); + } + + let point = point_opt.unwrap(); + let vector = point.vector.unwrap_or_default(); + let payload = point.payload; + + app.vector_detail = Some(VectorListItem { + id, + vector, + payload, + }); + app.modal.show_vector_details(); + Ok(()) +} diff --git a/crates/tui/src/app/mod.rs b/crates/tui/src/app/mod.rs index b189529..de62c78 100644 --- a/crates/tui/src/app/mod.rs +++ b/crates/tui/src/app/mod.rs @@ -1,4 +1,5 @@ mod database; +mod embeddings; mod events; mod modal; mod state; @@ -9,6 +10,7 @@ use crate::ui::{db, vector_operations}; use crossterm::event::Event; use std::io; use std::path::PathBuf; +use uuid::Uuid; pub struct App { pub should_quit: bool, @@ -17,6 +19,15 @@ pub struct App { pub vector_selected: usize, pub database: database::DatabaseManager, pub modal: modal::ModalManager, + pub embeddings: embeddings::EmbeddingClient, + + pub vector_list_items: Vec, + pub vector_list_next_offset: Option, + pub vector_list_post_restore: bool, + pub vector_list_selected_index: usize, + pub vector_detail: Option, + pub pending_delete_database: Option<(String, PathBuf)>, + pub pending_delete_database_index: Option, } impl Default for App { @@ -34,6 +45,15 @@ impl App { vector_selected: 0, database: database::DatabaseManager::new(), modal: modal::ModalManager::new(), + embeddings: embeddings::EmbeddingClient::default(), + + vector_list_items: Vec::new(), + vector_list_next_offset: None, + vector_list_post_restore: false, + vector_list_selected_index: 0, + vector_detail: None, + pending_delete_database: None, + pending_delete_database_index: None, } } @@ -89,12 +109,16 @@ impl App { self.modal.select_next(max_items); } else { let max_items = db::get_db_operations_count(); - self.db_selected = (self.db_selected + 1).min(max_items - 1); + if max_items > 0 { + self.db_selected = (self.db_selected + 1).min(max_items - 1); + } } } AppState::VectorOperations => { let max_items = vector_operations::get_vector_operations_count(); - self.vector_selected = (self.vector_selected + 1).min(max_items - 1); + if max_items > 0 { + self.vector_selected = (self.vector_selected + 1).min(max_items - 1); + } } } } @@ -115,7 +139,27 @@ impl App { self.modal.input_mode() } + pub fn secondary_input(&self) -> &str { + self.modal.secondary_input() + } + + pub fn active_field(&self) -> usize { + self.modal.active_field() + } + + pub fn switch_field(&mut self) { + self.modal.switch_field(); + } + pub fn available_databases(&self) -> &[(String, PathBuf)] { &self.database.available_databases } + + pub fn error_message(&self) -> Option<&str> { + self.modal.error_message() + } + + pub fn modal_footer_items(&self) -> Vec<(String, ratatui::style::Color)> { + self.modal.footer_items() + } } diff --git a/crates/tui/src/app/modal.rs b/crates/tui/src/app/modal.rs index 9807c0d..6ce0bc5 100644 --- a/crates/tui/src/app/modal.rs +++ b/crates/tui/src/app/modal.rs @@ -1,11 +1,15 @@ use super::state::ModalType; +use ratatui::style::Color; pub struct ModalManager { show_modal: bool, modal_type: Option, input_buffer: String, + secondary_input: String, + active_field: usize, input_mode: bool, selected_index: usize, + error_message: Option, } impl ModalManager { @@ -14,8 +18,11 @@ impl ModalManager { show_modal: false, modal_type: None, input_buffer: String::new(), + secondary_input: String::new(), + active_field: 0, input_mode: false, selected_index: 0, + error_message: None, } } @@ -31,6 +38,14 @@ impl ModalManager { &self.input_buffer } + pub fn secondary_input(&self) -> &str { + &self.secondary_input + } + + pub fn active_field(&self) -> usize { + self.active_field + } + pub fn input_mode(&self) -> bool { self.input_mode } @@ -45,6 +60,83 @@ impl ModalManager { self.input_buffer.clear(); self.input_mode = true; self.selected_index = 0; + self.error_message = None; + } + + pub fn show_search_similar_vectors(&mut self) { + self.show_modal = true; + self.modal_type = Some(ModalType::SearchSimilarVectors); + self.input_buffer.clear(); + self.secondary_input.clear(); + self.active_field = 0; + self.input_mode = true; + self.selected_index = 0; + self.error_message = None; + } + + pub fn show_delete_vector(&mut self) { + self.show_modal = true; + self.modal_type = Some(ModalType::DeleteVector); + self.input_buffer.clear(); + self.secondary_input.clear(); + self.active_field = 0; + self.input_mode = true; + self.selected_index = 0; + self.error_message = None; + } + + pub fn show_text_embedding(&mut self) { + self.show_modal = true; + self.modal_type = Some(ModalType::TextEmbedding); + self.input_buffer.clear(); + self.secondary_input.clear(); + self.active_field = 1; + self.input_mode = true; + self.selected_index = 0; + self.error_message = None; + } + + pub fn show_image_embedding(&mut self) { + self.show_modal = true; + self.modal_type = Some(ModalType::ImageEmbedding); + self.input_buffer.clear(); + self.secondary_input.clear(); + self.active_field = 1; + self.input_mode = true; + self.selected_index = 0; + self.error_message = None; + } + + pub fn show_vector_list(&mut self) { + self.show_modal = true; + self.modal_type = Some(ModalType::ListVectors); + self.input_mode = false; + self.error_message = None; + self.selected_index = 0; + } + pub fn show_vector_details(&mut self) { + self.show_modal = true; + self.modal_type = Some(ModalType::VectorDetails); + self.input_mode = false; + self.selected_index = 0; + self.error_message = None; + } + pub fn show_success>(&mut self, message: S) { + self.show_modal = true; + self.modal_type = Some(ModalType::Success); + self.input_mode = false; + self.input_buffer.clear(); + self.secondary_input.clear(); + self.error_message = Some(message.into()); + } + + pub fn show_failure>(&mut self, message: S) { + self.show_modal = true; + self.modal_type = Some(ModalType::Failure); + self.input_mode = false; + self.input_buffer.clear(); + self.secondary_input.clear(); + self.error_message = Some(message.into()); } pub fn show_database_list(&mut self) { @@ -52,6 +144,7 @@ impl ModalManager { self.modal_type = Some(ModalType::DatabaseList); self.input_mode = false; self.selected_index = 0; + self.error_message = None; } pub fn show_delete_database(&mut self) { @@ -59,14 +152,37 @@ impl ModalManager { self.modal_type = Some(ModalType::DeleteDatabase); self.input_mode = false; self.selected_index = 0; + self.error_message = None; + } + + pub fn show_confirm_delete_database(&mut self, name: String) { + self.show_modal = true; + self.modal_type = Some(ModalType::ConfirmDeleteDatabase); + self.input_mode = false; + self.selected_index = 0; + self.error_message = Some(format!("Delete database '{name}'?")); + } + + pub fn show_error>(&mut self, message: S) { + self.show_modal = true; + self.modal_type = Some(ModalType::Error); + self.input_mode = false; + self.input_buffer.clear(); + self.secondary_input.clear(); + self.active_field = 0; + self.selected_index = 0; + self.error_message = Some(message.into()); } pub fn close(&mut self) { self.show_modal = false; self.modal_type = None; self.input_buffer.clear(); + self.secondary_input.clear(); + self.active_field = 0; self.input_mode = false; self.selected_index = 0; + self.error_message = None; } pub fn enable_input_mode(&mut self) { @@ -74,11 +190,29 @@ impl ModalManager { } pub fn add_char(&mut self, c: char) { - self.input_buffer.push(c); + match self.active_field { + 0 => self.input_buffer.push(c), + 1 => self.secondary_input.push(c), + _ => {} + } } pub fn remove_char(&mut self) { - self.input_buffer.pop(); + match self.active_field { + 0 => { + self.input_buffer.pop(); + } + 1 => { + self.secondary_input.pop(); + } + _ => {} + } + } + + pub fn switch_field(&mut self) { + if matches!(self.modal_type, Some(ModalType::SearchSimilarVectors)) { + self.active_field = (self.active_field + 1) % 2; + } } pub fn select_previous(&mut self) { @@ -91,7 +225,77 @@ impl ModalManager { } } + pub fn set_selected_index(&mut self, index: usize, max_items: usize) { + if max_items == 0 { + self.selected_index = 0; + } else { + self.selected_index = index.min(max_items - 1); + } + } + pub fn get_input_value(&self) -> String { self.input_buffer.clone() } + + pub fn error_message(&self) -> Option<&str> { + self.error_message.as_deref() + } + + pub fn footer_items(&self) -> Vec<(String, Color)> { + use ModalType::*; + + match self.modal_type.as_ref() { + Some(CreateDatabase) => vec![ + ("Enter Create".into(), Color::Green), + ("Esc Cancel".into(), Color::Red), + ], + Some(DatabaseList) => vec![ + ("↑ Navigate".into(), Color::Gray), + ("↓ Navigate".into(), Color::Gray), + ("Enter Select".into(), Color::Green), + ("Esc Cancel".into(), Color::Red), + ], + Some(DeleteDatabase) => vec![ + ("↑ Navigate".into(), Color::Gray), + ("↓ Navigate".into(), Color::Gray), + ("Enter Continue".into(), Color::Green), + ("Esc Cancel".into(), Color::Red), + ], + Some(ConfirmDeleteDatabase) => vec![ + ("←/→ Toggle".into(), Color::Gray), + ("Enter Confirm".into(), Color::Green), + ("Esc Back".into(), Color::Red), + ], + + Some(SearchSimilarVectors) => vec![ + ("Tab Next".into(), Color::Gray), + ("Enter Search".into(), Color::Green), + ("Esc Cancel".into(), Color::Red), + ], + Some(DeleteVector) => vec![ + ("Enter Delete".into(), Color::Green), + ("Esc Cancel".into(), Color::Red), + ], + Some(TextEmbedding) => vec![ + ("Enter Insert".into(), Color::Green), + ("Esc Cancel".into(), Color::Red), + ], + Some(ImageEmbedding) => vec![ + ("Enter Insert".into(), Color::Green), + ("Esc Cancel".into(), Color::Red), + ], + Some(ListVectors) => vec![ + ("↑ Scroll".into(), Color::Gray), + ("↓ Scroll".into(), Color::Gray), + ("Enter Details".into(), Color::Green), + ("Esc Close".into(), Color::Red), + ], + Some(VectorDetails) => vec![ + ("Enter Close".into(), Color::Green), + ("Esc Back".into(), Color::Red), + ], + Some(Success) | Some(Failure) | Some(Error) => vec![("Esc Dismiss".into(), Color::Red)], + _ => vec![("Esc Cancel".into(), Color::Red)], + } + } } diff --git a/crates/tui/src/app/state.rs b/crates/tui/src/app/state.rs index f7711fb..12668f7 100644 --- a/crates/tui/src/app/state.rs +++ b/crates/tui/src/app/state.rs @@ -1,3 +1,6 @@ +use defs::{DenseVector, Payload}; +use uuid::Uuid; + #[derive(Debug, Default, Clone, PartialEq)] pub enum AppState { #[default] @@ -10,5 +13,23 @@ pub enum AppState { pub enum ModalType { CreateDatabase, DeleteDatabase, + ConfirmDeleteDatabase, DatabaseList, + ListVectors, + VectorDetails, + Error, + Success, + Failure, + + DeleteVector, + SearchSimilarVectors, + TextEmbedding, + ImageEmbedding, +} + +#[derive(Debug, Clone)] +pub struct VectorListItem { + pub id: Uuid, + pub vector: DenseVector, + pub payload: Option, } diff --git a/crates/tui/src/ui/db.rs b/crates/tui/src/ui/db.rs index 616e7a0..77d0198 100644 --- a/crates/tui/src/ui/db.rs +++ b/crates/tui/src/ui/db.rs @@ -36,8 +36,13 @@ pub fn render_database(f: &mut Frame, app: &App) { selected: app.db_selected, }; - let mut instructions = common_instructions(); - instructions.insert(4, ("→ Next".to_string(), Color::Gray)); + let instructions = if app.show_modal() { + app.modal_footer_items() + } else { + let mut base = common_instructions(); + base.insert(4, ("→ Next".to_string(), Color::Gray)); + base + }; f.render_widget(title.render(), chunks[0]); operations_list.render(f, chunks[1]); diff --git a/crates/tui/src/ui/modal.rs b/crates/tui/src/ui/modal.rs index ba51a85..7343975 100644 --- a/crates/tui/src/ui/modal.rs +++ b/crates/tui/src/ui/modal.rs @@ -1,7 +1,7 @@ use crate::app::{App, ModalType}; use ratatui::{ prelude::*, - widgets::{Block, Borders, Clear, List, ListItem, Paragraph}, + widgets::{Block, Borders, Cell, Clear, List, ListItem, Paragraph, Row, Table, TableState}, }; pub fn render_modal(f: &mut Frame, app: &App) { @@ -10,7 +10,12 @@ pub fn render_modal(f: &mut Frame, app: &App) { } let size = f.size(); - let popup_area = centered_rect(60, 60, size); + let (modal_width, modal_height) = match app.modal_type() { + Some(ModalType::ListVectors) => (85, 70), + Some(ModalType::VectorDetails) => (80, 60), + _ => (60, 60), + }; + let popup_area = centered_rect(modal_width, modal_height, size); // Clear the area f.render_widget(Clear, popup_area); @@ -19,6 +24,20 @@ pub fn render_modal(f: &mut Frame, app: &App) { Some(ModalType::CreateDatabase) => render_create_database_modal(f, app, popup_area), Some(ModalType::DatabaseList) => render_database_list_modal(f, app, popup_area), Some(ModalType::DeleteDatabase) => render_delete_database_modal(f, app, popup_area), + Some(ModalType::ConfirmDeleteDatabase) => { + render_confirm_delete_database_modal(f, app, popup_area) + } + Some(ModalType::Error) => render_error_modal(f, app, popup_area), + Some(ModalType::SearchSimilarVectors) => { + render_search_similar_vectors_modal(f, app, popup_area) + } + Some(ModalType::DeleteVector) => render_delete_vector_modal(f, app, popup_area), + Some(ModalType::TextEmbedding) => render_text_embedding_modal(f, app, popup_area), + Some(ModalType::ImageEmbedding) => render_image_embedding_modal(f, app, popup_area), + Some(ModalType::ListVectors) => render_vector_list_modal(f, app, popup_area), + Some(ModalType::VectorDetails) => render_vector_details_modal(f, app, popup_area), + Some(ModalType::Success) => render_success_modal(f, app, popup_area), + Some(ModalType::Failure) => render_failure_modal(f, app, popup_area), _ => {} } } @@ -28,14 +47,108 @@ fn render_create_database_modal(f: &mut Frame, app: &App, area: Rect) { .title("Create New Database") .borders(Borders::ALL); - let input = Paragraph::new(app.input_buffer()) + let lines = vec![ + Line::from(Span::raw("Enter name of new database:")), + Line::from(Span::raw(app.input_buffer().to_string())), + ]; + + let input = Paragraph::new(lines) + .block(block) + .wrap(ratatui::widgets::Wrap { trim: true }); + + f.render_widget(input, area); + + if app.input_mode() { + // place cursor on the second line where the input buffer is shown + f.set_cursor(area.x + 1 + app.input_buffer().len() as u16, area.y + 2); + } +} + +fn render_search_similar_vectors_modal(f: &mut Frame, app: &App, area: Rect) { + let block = Block::default() + .title("Search Similar Vectors") + .borders(Borders::ALL); + + let lines = vec![ + Line::from(Span::raw("Top-k (int):")), + Line::from(Span::raw(app.input_buffer().to_string())), + Line::from(Span::raw("")), + Line::from(Span::raw("Text:")), + Line::from(Span::raw(app.secondary_input().to_string())), + ]; + + let input = Paragraph::new(lines) .block(block) .wrap(ratatui::widgets::Wrap { trim: true }); + f.render_widget(input, area); + + if app.input_mode() { + if app.active_field() == 0 { + f.set_cursor(area.x + 1 + app.input_buffer().len() as u16, area.y + 2); + } else { + f.set_cursor(area.x + 1 + app.secondary_input().len() as u16, area.y + 5); + } + } +} + +fn render_delete_vector_modal(f: &mut Frame, app: &App, area: Rect) { + let block = Block::default() + .title("Delete Vector by ID") + .borders(Borders::ALL); + + let lines = vec![ + Line::from(Span::raw("ID (uuid):")), + Line::from(Span::raw(app.input_buffer().to_string())), + Line::from(""), + ]; + let input = Paragraph::new(lines) + .block(block) + .wrap(ratatui::widgets::Wrap { trim: true }); f.render_widget(input, area); if app.input_mode() { - f.set_cursor(area.x + app.input_buffer().len() as u16 + 1, area.y + 1); + f.set_cursor(area.x + 1 + app.input_buffer().len() as u16, area.y + 2); + } +} + +fn render_text_embedding_modal(f: &mut Frame, app: &App, area: Rect) { + let block = Block::default() + .title("Insert Text Embedding") + .borders(Borders::ALL); + + let lines = vec![ + Line::from(Span::raw("Text:")), + Line::from(Span::raw(app.secondary_input().to_string())), + ]; + + let input = Paragraph::new(lines) + .block(block) + .wrap(ratatui::widgets::Wrap { trim: true }); + f.render_widget(input, area); + + if app.input_mode() { + f.set_cursor(area.x + 1 + app.secondary_input().len() as u16, area.y + 2); + } +} + +fn render_image_embedding_modal(f: &mut Frame, app: &App, area: Rect) { + let block = Block::default() + .title("Insert Image Embedding") + .borders(Borders::ALL); + + let lines = vec![ + Line::from(Span::raw("Image Path:")), + Line::from(Span::raw(app.secondary_input().to_string())), + ]; + + let input = Paragraph::new(lines) + .block(block) + .wrap(ratatui::widgets::Wrap { trim: true }); + f.render_widget(input, area); + + if app.input_mode() { + f.set_cursor(area.x + 1 + app.secondary_input().len() as u16, area.y + 2); } } @@ -91,6 +204,223 @@ fn render_delete_database_modal(f: &mut Frame, app: &App, area: Rect) { f.render_widget(list, area); } +fn render_confirm_delete_database_modal(f: &mut Frame, app: &App, area: Rect) { + let prompt = app.error_message().unwrap_or("Are you sure?"); + let options = ["Yes", "No"]; + let selected = app + .modal + .selected_index() + .min(options.len().saturating_sub(1)); + + let option_line = options + .iter() + .enumerate() + .map(|(i, option)| { + if i == selected { + Span::styled( + format!("[ {option} ]"), + Style::default() + .fg(Color::Black) + .bg(Color::Yellow) + .add_modifier(Modifier::BOLD), + ) + } else { + Span::styled(format!(" {option} "), Style::default().fg(Color::Gray)) + } + }) + .collect::>(); + + let block = Block::default() + .title("Confirm Deletion") + .borders(Borders::ALL) + .border_style(Style::default().fg(Color::Red)); + + let content = Paragraph::new(vec![ + Line::from(prompt.to_string()), + Line::from(""), + Line::from(option_line), + ]) + .block(block) + .alignment(Alignment::Center) + .wrap(ratatui::widgets::Wrap { trim: true }); + + f.render_widget(content, area); +} + +fn render_vector_list_modal(f: &mut Frame, app: &App, area: Rect) { + let has_more = app.vector_list_next_offset.is_some(); + let title = if has_more { + "Vectors (scroll for more)" + } else { + "Vectors" + }; + + let header_cells = ["ID", "Payload Type", "Payload Content"] + .into_iter() + .map(|h| { + Cell::from(h).style( + Style::default() + .fg(Color::Magenta) + .add_modifier(Modifier::BOLD), + ) + }); + let header = Row::new(header_cells).height(1); + + let rows: Vec = app + .vector_list_items + .iter() + .map(|item| { + let id_str = item.id.to_string(); + let (ptype, pcontent) = match &item.payload { + Some(p) => (format!("{:?}", p.content_type), p.content.clone()), + None => ("None".to_string(), "".to_string()), + }; + Row::new(vec![ + Cell::from(id_str), + Cell::from(ptype), + Cell::from(pcontent), + ]) + }) + .collect(); + + let mut state = TableState::default(); + if !app.vector_list_items.is_empty() { + let selected = app + .modal + .selected_index() + .min(app.vector_list_items.len().saturating_sub(1)); + state.select(Some(selected)); + } + + let widths = [ + Constraint::Length(38), + Constraint::Length(14), + Constraint::Percentage(60), + ]; + + let table = Table::new(rows, widths) + .header(header) + .block( + Block::default() + .title(title) + .borders(Borders::ALL) + .border_style(Style::default().fg(Color::Magenta)), + ) + .column_spacing(2) + .highlight_symbol("▶ ") + .highlight_style(Style::default().bg(Color::Yellow).fg(Color::Black)); + + f.render_stateful_widget(table, area, &mut state); +} + +fn render_vector_details_modal(f: &mut Frame, app: &App, area: Rect) { + let block = Block::default() + .title("Vector Details") + .borders(Borders::ALL) + .border_style(Style::default().fg(Color::Magenta)); + + if let Some(item) = &app.vector_detail { + let header_cells = ["ID", "Payload Type", "Payload Content"] + .into_iter() + .map(|h| { + Cell::from(h).style( + Style::default() + .fg(Color::Magenta) + .add_modifier(Modifier::BOLD), + ) + }); + let header = Row::new(header_cells).height(1); + + let id_str = item.id.to_string(); + let (ptype, pcontent) = match &item.payload { + Some(p) => (format!("{:?}", p.content_type), p.content.clone()), + None => ("None".to_string(), "".to_string()), + }; + + let row = Row::new(vec![ + Cell::from(id_str), + Cell::from(ptype), + Cell::from(pcontent), + ]) + .height(2); + let rows = vec![row]; + + let widths = [ + Constraint::Length(38), + Constraint::Length(14), + Constraint::Percentage(60), + ]; + + let table = Table::new(rows, widths) + .header(header) + .block(block) + .column_spacing(2); + + f.render_widget(table, area); + } else { + let placeholder = Paragraph::new("No vector data available.") + .block(block) + .alignment(Alignment::Center) + .wrap(ratatui::widgets::Wrap { trim: true }); + f.render_widget(placeholder, area); + } +} + +fn render_error_modal(f: &mut Frame, app: &App, area: Rect) { + let message = app.error_message().unwrap_or("An unknown error occurred!"); + let content = Paragraph::new(vec![Line::from(message.to_string())]) + .block( + Block::default() + .title(Span::styled( + "Error", + Style::default().fg(Color::Red).add_modifier(Modifier::BOLD), + )) + .borders(Borders::ALL) + .border_style(Style::default().fg(Color::Red)), + ) + .wrap(ratatui::widgets::Wrap { trim: true }); + + f.render_widget(content, area); +} + +fn render_success_modal(f: &mut Frame, app: &App, area: Rect) { + let message = app.error_message().unwrap_or("Success"); + let content = Paragraph::new(vec![Line::from(message.to_string())]) + .block( + Block::default() + .title(Span::styled( + "Success", + Style::default() + .fg(Color::Green) + .add_modifier(Modifier::BOLD), + )) + .borders(Borders::ALL) + .border_style(Style::default().fg(Color::Green)), + ) + .wrap(ratatui::widgets::Wrap { trim: true }); + + f.render_widget(content, area); +} + +fn render_failure_modal(f: &mut Frame, app: &App, area: Rect) { + let message = app + .error_message() + .unwrap_or("Operation failed. Please try again."); + let content = Paragraph::new(vec![Line::from(message.to_string())]) + .block( + Block::default() + .title(Span::styled( + "Failure", + Style::default().fg(Color::Red).add_modifier(Modifier::BOLD), + )) + .borders(Borders::ALL) + .border_style(Style::default().fg(Color::Red)), + ) + .wrap(ratatui::widgets::Wrap { trim: true }); + + f.render_widget(content, area); +} + fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect { let popup_layout = Layout::default() .direction(Direction::Vertical) diff --git a/crates/tui/src/ui/vector_operations.rs b/crates/tui/src/ui/vector_operations.rs index 28dc052..ad9ab08 100644 --- a/crates/tui/src/ui/vector_operations.rs +++ b/crates/tui/src/ui/vector_operations.rs @@ -11,10 +11,10 @@ use crate::app::App; const VECTOR_OPERATIONS: &[&str] = &[ "List All Vectors", - "Get Vector", - "Insert Vector", "Delete Vector", "Search Similar Vectors", + "Insert Text Embedding", + "Insert Image Embedding", ]; fn get_vector_items() -> Vec> { @@ -77,7 +77,14 @@ pub fn render_vector_operations(f: &mut Frame, app: &App) { f.render_widget(title.render(), chunks[0]); f.render_widget(db_info, chunks[1]); operations_list.render(f, chunks[2]); - f.render_widget(create_instructions(common_instructions()), chunks[3]); + + let instructions = if app.show_modal() { + app.modal_footer_items() + } else { + common_instructions() + }; + + f.render_widget(create_instructions(instructions), chunks[3]); } pub fn get_vector_operations_count() -> usize {