diff --git a/Cargo.lock b/Cargo.lock index 0ccc2fc00f..54db828618 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -29,6 +29,17 @@ dependencies = [ "cpufeatures", ] +[[package]] +name = "ahash" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" +dependencies = [ + "cfg-if 1.0.0", + "once_cell", + "version_check", +] + [[package]] name = "aho-corasick" version = "1.0.2" @@ -59,6 +70,12 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" +[[package]] +name = "arc-swap" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6" + [[package]] name = "ascii_utils" version = "0.9.3" @@ -234,6 +251,15 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitpacking" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c7d2ac73c167c06af4a5f37e6e59d84148d57ccbe4480b76f0273eefea82d7" +dependencies = [ + "crunchy", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -306,6 +332,12 @@ dependencies = [ "jobserver", ] +[[package]] +name = "census" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fafee10a5dd1cffcb5cc560e0d0df8803d7355a2b12272e3557dee57314cb6e" + [[package]] name = "cfg-if" version = "0.1.10" @@ -392,6 +424,15 @@ dependencies = [ "os_str_bytes", ] +[[package]] +name = "combine" +version = "4.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4" +dependencies = [ + "memchr", +] + [[package]] name = "console_error_panic_hook" version = "0.1.7" @@ -521,6 +562,12 @@ dependencies = [ "cfg-if 1.0.0", ] +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + [[package]] name = "crypto-common" version = "0.1.6" @@ -694,6 +741,12 @@ version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" +[[package]] +name = "downcast-rs" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" + [[package]] name = "dynamic-graphql" version = "0.7.3" @@ -807,6 +860,17 @@ dependencies = [ "serde", ] +[[package]] +name = "fail" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5e43d0f78a42ad591453aedb1d7ae631ce7ee445c7643691055a9ed8d3b01c" +dependencies = [ + "log", + "once_cell", + "rand 0.8.5", +] + [[package]] name = "fast_chemail" version = "0.9.6" @@ -816,6 +880,12 @@ dependencies = [ "ascii_utils", ] +[[package]] +name = "fastdivide" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25c7df09945d65ea8d70b3321547ed414bbc540aad5bac6883d021b970f35b04" + [[package]] name = "fastrand" version = "1.9.0" @@ -878,6 +948,16 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs4" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7672706608ecb74ab2e055c68327ffc25ae4cac1e12349204fd5fb0f3487cce2" +dependencies = [ + "rustix", + "windows-sys 0.48.0", +] + [[package]] name = "fuchsia-cprng" version = "0.1.1" @@ -1004,6 +1084,19 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "generator" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc16584ff22b460a382b7feec54b23d2908d858152e5739a120b949293bd74e" +dependencies = [ + "cc", + "libc", + "log", + "rustversion", + "windows", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -1094,6 +1187,15 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "hashbrown" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" +dependencies = [ + "ahash", +] + [[package]] name = "hashbrown" version = "0.14.0" @@ -1158,6 +1260,12 @@ dependencies = [ "digest", ] +[[package]] +name = "htmlescape" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9025058dae765dee5070ec375f591e2ba14638c63feff74f13805a72e523163" + [[package]] name = "http" version = "0.2.9" @@ -1311,6 +1419,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" dependencies = [ "cfg-if 1.0.0", + "js-sys", + "wasm-bindgen", + "web-sys", ] [[package]] @@ -1397,6 +1508,12 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +[[package]] +name = "levenshtein_automata" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c2cdeb66e45e9f36bfad5bbdb4d2384e70936afbee843c6f6543f0c551ebb25" + [[package]] name = "libc" version = "0.2.147" @@ -1432,6 +1549,35 @@ version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" +[[package]] +name = "loom" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff50ecb28bb86013e935fb6683ab1f6d3a20016f123c76fd4c27470076ac30f5" +dependencies = [ + "cfg-if 1.0.0", + "generator", + "pin-utils", + "scoped-tls", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "lru" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "718e8fae447df0c7e1ba7f5189829e63fd536945c8988d61444c19039f16b670" +dependencies = [ + "hashbrown 0.13.2", +] + +[[package]] +name = "lz4_flex" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b8c72594ac26bfd34f2d99dfced2edfaddfe8a476e3ff2ca0eb293d925c4f83" + [[package]] name = "matchers" version = "0.1.0" @@ -1441,12 +1587,31 @@ dependencies = [ "regex-automata", ] +[[package]] +name = "measure_time" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56220900f1a0923789ecd6bf25fbae8af3b2f1ff3e9e297fc9b6b8674dd4d852" +dependencies = [ + "instant", + "log", +] + [[package]] name = "memchr" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +[[package]] +name = "memmap2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d28bba84adfe6646737845bc5ebbfa2c08424eb1c37e94a1fd2a82adb56a872" +dependencies = [ + "libc", +] + [[package]] name = "memoffset" version = "0.9.0" @@ -1506,6 +1671,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "murmurhash32" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9380db4c04d219ac5c51d14996bbf2c2e9a15229771b53f8671eb6c83cf44df" + [[package]] name = "nanorand" version = "0.7.0" @@ -1667,6 +1838,15 @@ version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +[[package]] +name = "oneshot" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc22d22931513428ea6cc089e942d38600e3d00976eef8c86de6b8a3aadec6eb" +dependencies = [ + "loom", +] + [[package]] name = "oorandom" version = "11.1.3" @@ -1831,6 +2011,15 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +[[package]] +name = "ownedbytes" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c718e498b20704d5fb5d51d07f414a22f61c19254c1708e117b93fd76860739c" +dependencies = [ + "stable_deref_trait", +] + [[package]] name = "parking_lot" version = "0.12.1" @@ -2357,6 +2546,7 @@ dependencies = [ "serde_json", "serde_with", "sorted_vector_map", + "tantivy", "tempdir", "thiserror", "tokio", @@ -2577,6 +2767,16 @@ dependencies = [ "serde", ] +[[package]] +name = "rust-stemmers" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e46a2036019fdb888131db7a4c847a1063a7493f971ed94ea82c67eada63ca54" +dependencies = [ + "serde", + "serde_derive", +] + [[package]] name = "rustc-hash" version = "1.1.0" @@ -2619,6 +2819,12 @@ dependencies = [ "untrusted", ] +[[package]] +name = "rustversion" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f3208ce4d8448b3f3e7d168a73f5e0c43a61e32930de3bceeccedb388b6bf06" + [[package]] name = "ryu" version = "1.0.13" @@ -2793,6 +2999,15 @@ dependencies = [ "libc", ] +[[package]] +name = "sketches-ddsketch" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68a406c1882ed7f29cd5e248c9848a80e7cb6ae0fea82346d2746f2f941c07e1" +dependencies = [ + "serde", +] + [[package]] name = "slab" version = "0.4.8" @@ -2842,6 +3057,12 @@ dependencies = [ "lock_api", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + [[package]] name = "static_assertions" version = "1.1.0" @@ -2893,6 +3114,149 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "tantivy" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aec540e9cebc88f523f67f596dee213e491f0c55961de013566f267a0c31f5e9" +dependencies = [ + "aho-corasick", + "arc-swap", + "async-trait", + "base64 0.21.2", + "bitpacking", + "byteorder", + "census", + "crc32fast", + "crossbeam-channel", + "downcast-rs", + "fail", + "fastdivide", + "fs4", + "htmlescape", + "itertools", + "levenshtein_automata", + "log", + "lru", + "lz4_flex", + "measure_time", + "memmap2", + "murmurhash32", + "num_cpus", + "once_cell", + "oneshot", + "rayon", + "regex", + "rust-stemmers", + "rustc-hash", + "serde", + "serde_json", + "sketches-ddsketch", + "smallvec", + "tantivy-bitpacker", + "tantivy-columnar", + "tantivy-common", + "tantivy-fst", + "tantivy-query-grammar", + "tantivy-stacker", + "tantivy-tokenizer-api", + "tempfile", + "thiserror", + "time 0.3.22", + "uuid", + "winapi", +] + +[[package]] +name = "tantivy-bitpacker" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16099e96f0ede682084469b80d6909dc170aa2b11d2a45538b5b36b2a90090b9" +dependencies = [ + "bitpacking", +] + +[[package]] +name = "tantivy-columnar" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e32b024b26eab93eb8648faf08004356bf9d47376557ee4409f4b210163656" +dependencies = [ + "fastdivide", + "fnv", + "itertools", + "serde", + "tantivy-bitpacker", + "tantivy-common", + "tantivy-sstable", + "tantivy-stacker", +] + +[[package]] +name = "tantivy-common" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7d12fdd6ec0f7e0962f129c03c696a85ec567734950cbb2b89af4a293ce342f" +dependencies = [ + "async-trait", + "byteorder", + "ownedbytes", + "serde", + "time 0.3.22", +] + +[[package]] +name = "tantivy-fst" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc3c506b1a8443a3a65352df6382a1fb6a7afe1a02e871cee0d25e2c3d5f3944" +dependencies = [ + "byteorder", + "regex-syntax 0.6.29", + "utf8-ranges", +] + +[[package]] +name = "tantivy-query-grammar" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "106d8f78ad1da4f0fdd526a0760c326c0573510d4dedabeb1962d35a35879797" +dependencies = [ + "combine", + "once_cell", + "regex", +] + +[[package]] +name = "tantivy-sstable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eda34243d3ee64bd8f9ba74a3b0d05f4d07beff7767a727212e9b5a19c13dde7" +dependencies = [ + "tantivy-common", + "tantivy-fst", + "zstd 0.12.3+zstd.1.5.2", +] + +[[package]] +name = "tantivy-stacker" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b9e9470301b026ad3b95f79a791a2a3ee81f3ab16fbe412a9dd81ff834acf5" +dependencies = [ + "murmurhash32", + "tantivy-common", +] + +[[package]] +name = "tantivy-tokenizer-api" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64186801b6e06b3a1c4275e23b517835ff4ecbb707318b838dc9de457c062200" +dependencies = [ + "serde", +] + [[package]] name = "target-lexicon" version = "0.12.8" @@ -2998,8 +3362,10 @@ version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea9e1b3cf1243ae005d9e74085d4d542f3125458f3a81af210d901dcd7411efd" dependencies = [ + "itoa", "serde", "time-core", + "time-macros", ] [[package]] @@ -3008,6 +3374,15 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" +[[package]] +name = "time-macros" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "372950940a5f07bf38dbe211d7283c9e6d7327df53794992d293e534c733d09b" +dependencies = [ + "time-core", +] + [[package]] name = "tinytemplate" version = "1.2.1" @@ -3327,6 +3702,12 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "utf8-ranges" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcfc827f90e53a02eaef5e535ee14266c1d569214c6aa70133a624d8a3164ba" + [[package]] name = "uuid" version = "1.4.0" @@ -3334,6 +3715,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d023da39d1fde5a8a3fe1f3e01ca9632ada0a63e9797de55a879d6e2236277be" dependencies = [ "getrandom 0.2.10", + "serde", ] [[package]] @@ -3716,7 +4098,7 @@ dependencies = [ "pbkdf2", "sha1", "time 0.3.22", - "zstd", + "zstd 0.11.2+zstd.1.5.2", ] [[package]] @@ -3725,7 +4107,16 @@ version = "0.11.2+zstd.1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4" dependencies = [ - "zstd-safe", + "zstd-safe 5.0.2+zstd.1.5.2", +] + +[[package]] +name = "zstd" +version = "0.12.3+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76eea132fb024e0e13fd9c2f5d5d595d8a967aa72382ac2f9d39fcc95afd0806" +dependencies = [ + "zstd-safe 6.0.5+zstd.1.5.4", ] [[package]] @@ -3738,6 +4129,16 @@ dependencies = [ "zstd-sys", ] +[[package]] +name = "zstd-safe" +version = "6.0.5+zstd.1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56d9e60b4b1758206c238a10165fbcae3ca37b01744e394c463463f6529d23b" +dependencies = [ + "libc", + "zstd-sys", +] + [[package]] name = "zstd-sys" version = "2.0.8+zstd.1.5.5" diff --git a/raphtory-graphql/src/data.rs b/raphtory-graphql/src/data.rs index bd9c34e7d2..32663a6327 100644 --- a/raphtory-graphql/src/data.rs +++ b/raphtory-graphql/src/data.rs @@ -1,4 +1,4 @@ -use raphtory::prelude::{Graph, GraphViewOps}; +use raphtory::{prelude::{Graph, GraphViewOps}, search::IndexedGraph}; use std::{ collections::{HashMap, HashSet}, path::Path, @@ -6,7 +6,7 @@ use std::{ use walkdir::WalkDir; pub(crate) struct Data { - pub(crate) graphs: HashMap, + pub(crate) graphs: HashMap>, } impl Data { @@ -36,7 +36,7 @@ impl Data { } }; - let graphs: HashMap = valid_paths + let graphs: HashMap> = valid_paths .into_iter() .map(|path| { println!("loading graph from {path}"); @@ -54,7 +54,7 @@ impl Data { (graph_name.to_string(), graph) } }; - }) + }).map(|(name, g)| (name, IndexedGraph::from_graph(&g).expect("Unable to index graph"))) .collect(); Self { graphs } diff --git a/raphtory-graphql/src/lib.rs b/raphtory-graphql/src/lib.rs index 7a2222eb32..bd814c7dd7 100644 --- a/raphtory-graphql/src/lib.rs +++ b/raphtory-graphql/src/lib.rs @@ -15,12 +15,58 @@ mod graphql_test { use std::collections::HashMap; #[tokio::test] - async fn basic_query() { + async fn search_for_gandalf_query() { let graph = Graph::new(); - if let Err(err) = graph.add_vertex(0, 11, []) { - panic!("Could not add vertex! {:?}", err); + graph + .add_vertex(0, "Gandalf", [("kind".to_string(), Prop::str("wizard"))]) + .expect("Could not add vertex!"); + graph + .add_vertex(0, "Frodo", [("kind".to_string(), Prop::str("Hobbit"))]) + .expect("Could not add vertex!"); + + let graphs = HashMap::from([("lotr".to_string(), graph.into())]); + let data = data::Data { graphs }; + + #[derive(App)] + struct App(model::QueryRoot); + let schema = App::create_schema().data(data).finish().unwrap(); + + let query = r#" + { + graph(name: "lotr") { + search(query: "kind:wizard", limit: 10, offset: 0) { + name + } + } } - let graphs = HashMap::from([("lotr".to_string(), graph)]); + "#; + + let root = model::QueryRoot; + let req = dynamic_graphql::Request::new(query).root_value(FieldValue::owned_any(root)); + + let res = schema.execute(req).await; + let data = res.data.into_json().unwrap(); + + assert_eq!( + data, + serde_json::json!({ + "graph": { + "search": [ + { + "name": "Gandalf" + } + ] + } + }), + ); + } + + #[tokio::test] + async fn basic_query() { + let graph = Graph::new(); + graph.add_vertex(0, 11, []).expect("Could not add vertex!"); + + let graphs = HashMap::from([("lotr".to_string(), graph.into())]); let data = data::Data { graphs }; #[derive(App)] @@ -70,7 +116,7 @@ mod graphql_test { panic!("Could not add vertex! {:?}", err); } - let graphs = HashMap::from([("lotr".to_string(), graph)]); + let graphs = HashMap::from([("lotr".to_string(), graph.into())]); let data = data::Data { graphs }; #[derive(App)] @@ -158,7 +204,7 @@ mod graphql_test { panic!("Could not add vertex! {:?}", err); } - let graphs = HashMap::from([("lotr".to_string(), graph)]); + let graphs = HashMap::from([("lotr".to_string(), graph.into())]); let data = data::Data { graphs }; #[derive(App)] diff --git a/raphtory-graphql/src/model/graph/graph.rs b/raphtory-graphql/src/model/graph/graph.rs index 8767409656..e62802142a 100644 --- a/raphtory-graphql/src/model/graph/graph.rs +++ b/raphtory-graphql/src/model/graph/graph.rs @@ -1,3 +1,5 @@ +use std::ops::Deref; + use crate::model::{ algorithm::Algorithms, filters::{edgefilter::EdgeFilter, nodefilter::NodeFilter}, @@ -5,10 +7,10 @@ use crate::model::{ }; use dynamic_graphql::{ResolvedObject, ResolvedObjectFields}; use itertools::Itertools; -use raphtory::db::api::view::{ +use raphtory::{db::api::view::{ internal::{DynamicGraph, IntoDynamic}, GraphViewOps, TimeOps, VertexViewOps, -}; +}, search::IndexedGraph}; #[derive(ResolvedObject)] pub(crate) struct GraphMeta { @@ -47,13 +49,13 @@ impl GraphMeta { #[derive(ResolvedObject)] pub(crate) struct GqlGraph { - graph: DynamicGraph, + graph: IndexedGraph, } impl From for GqlGraph { fn from(value: G) -> Self { Self { - graph: value.into_dynamic(), + graph: value.into_dynamic().into(), } } } @@ -86,6 +88,15 @@ impl GqlGraph { } } + async fn search(&self, query: String, limit: usize, offset: usize) -> Vec { + self.graph + .search(&query, limit, offset) + .into_iter() + .flat_map(|vv| vv) + .map(|vv| vv.into()) + .collect() + } + async fn edges<'a>(&self, filter: Option) -> Vec { match filter { Some(filter) => self @@ -116,6 +127,6 @@ impl GqlGraph { } async fn algorithms(&self) -> Algorithms { - self.graph.clone().into() + self.graph.deref().clone().into() } } diff --git a/raphtory-graphql/src/model/mod.rs b/raphtory-graphql/src/model/mod.rs index 3100b4f159..2806f995a9 100644 --- a/raphtory-graphql/src/model/mod.rs +++ b/raphtory-graphql/src/model/mod.rs @@ -1,3 +1,5 @@ +use std::ops::Deref; + use crate::{ data::Data, model::graph::graph::{GqlGraph, GraphMeta}, @@ -25,14 +27,15 @@ impl QueryRoot { async fn graph<'a>(ctx: &Context<'a>, name: &str) -> Option { let data = ctx.data_unchecked::(); let g = data.graphs.get(name)?; - Some(g.clone().into()) + let graph = g.deref(); + Some(graph.clone().into()) } async fn graphs<'a>(ctx: &Context<'a>) -> Vec { let data = ctx.data_unchecked::(); data.graphs .iter() - .map(|(name, g)| GraphMeta::new(name.clone(), g.clone().into_dynamic())) + .map(|(name, g)| GraphMeta::new(name.clone(), g.deref().clone().into_dynamic())) .collect_vec() } } diff --git a/raphtory/Cargo.toml b/raphtory/Cargo.toml index 7831c248dd..0b75064ece 100644 --- a/raphtory/Cargo.toml +++ b/raphtory/Cargo.toml @@ -55,6 +55,9 @@ serde_json = {version="1", optional=true} reqwest = { version = "0.11.14", features = ["blocking"], optional=true} tokio = { version = "1.27.0", features = ["full"], optional=true} +# search optional dependencies +tantivy = {version="0.20", optional=true} + # python binding optional dependencies pyo3 = {version= "0.19.0", features=["multiple-pymethods", "chrono"], optional=true} num = {version="0.4.0", optional=true} @@ -69,8 +72,10 @@ quickcheck_macros = "1" [features] +default = ["search"] # Enables the graph loader io module io = ["dep:zip","dep:neo4rs", "dep:bzip2", "dep:flate2", "dep:csv", "dep:serde_json", "dep:reqwest", "dep:tokio"] # Enables generating the pyo3 python bindings python = ["io", "dep:pyo3", "dep:num", "dep:display-error-chain"] - +# search +search = ["dep:tantivy"] diff --git a/raphtory/src/core/entities/graph/tgraph.rs b/raphtory/src/core/entities/graph/tgraph.rs index 8a50323e7b..3bd23c0950 100644 --- a/raphtory/src/core/entities/graph/tgraph.rs +++ b/raphtory/src/core/entities/graph/tgraph.rs @@ -95,6 +95,10 @@ impl Default for InnerTemporalGraph { } impl InnerTemporalGraph { + pub(crate) fn get_all_vertex_property_names(&self, is_static: bool) -> Vec { + self.vertex_meta.get_all_property_names(is_static) + } + pub(crate) fn get_all_layers(&self) -> Vec { self.edge_meta.get_all_layers() } @@ -216,7 +220,7 @@ impl InnerTemporalGraph { v: u64, name: Option<&str>, props: Vec<(String, Prop)>, - ) -> Result<(), GraphError> { + ) -> Result { let t = time.try_into_time()?; self.update_time(t); @@ -255,7 +259,7 @@ impl InnerTemporalGraph { node.add_static_prop(*prop_id, name, prop.clone())?; } - Ok(()) + Ok(v_id.into()) } pub(crate) fn add_vertex_no_props(&self, t: i64, v: u64) -> Result { diff --git a/raphtory/src/core/entities/properties/props.rs b/raphtory/src/core/entities/properties/props.rs index cfdb2e1c48..63740406a3 100644 --- a/raphtory/src/core/entities/properties/props.rs +++ b/raphtory/src/core/entities/properties/props.rs @@ -167,6 +167,14 @@ impl Meta { .collect() } + pub fn get_all_property_names(&self, is_static: bool) -> Vec { + if is_static { + self.meta_prop_static.map.iter().map(|entry| entry.key().clone()).collect() + } else { + self.meta_prop_temporal.map.iter().map(|entry| entry.key().clone()).collect() + } + } + pub fn reverse_prop_id(&self, prop_id: usize, is_static: bool) -> Option { if is_static { self.meta_prop_static.reverse_lookup(&prop_id) @@ -174,6 +182,7 @@ impl Meta { self.meta_prop_temporal.reverse_lookup(&prop_id) } } + } #[derive(Serialize, Deserialize, Default, Debug)] diff --git a/raphtory/src/core/mod.rs b/raphtory/src/core/mod.rs index 087a5d7a17..e6a45374f2 100644 --- a/raphtory/src/core/mod.rs +++ b/raphtory/src/core/mod.rs @@ -60,6 +60,12 @@ pub enum Prop { Graph(Graph), } +impl Prop{ + pub fn str(s: &str) -> Prop { + Prop::Str(s.to_string()) + } +} + pub trait PropUnwrap: Sized { fn into_str(self) -> Option; fn unwrap_str(self) -> String { diff --git a/raphtory/src/core/utils/errors.rs b/raphtory/src/core/utils/errors.rs index 214707123d..65d691546b 100644 --- a/raphtory/src/core/utils/errors.rs +++ b/raphtory/src/core/utils/errors.rs @@ -1,5 +1,9 @@ use crate::core::{storage::lazy_vec::IllegalSet, utils::time::error::ParseTimeError, Prop}; +#[cfg(feature = "search")] +use tantivy; +use tantivy::query::QueryParserError; + #[derive(thiserror::Error, Debug)] pub enum GraphError { #[error("Immutable graph reference already exists. You can access mutable graph apis only exclusively.")] @@ -24,6 +28,13 @@ pub enum GraphError { BinCodeError { source: Box }, #[error("IO operation failed")] IOError { source: std::io::Error }, + #[cfg(feature = "search")] + #[error("Index operation failed")] + IndexError { source: tantivy::TantivyError }, + + #[cfg(feature = "search")] + #[error("Index operation failed")] + QueryError { source: QueryParserError }, } impl From for GraphError { @@ -44,6 +55,19 @@ impl From for GraphError { } } +#[cfg(feature = "search")] +impl From for GraphError { + fn from(source: tantivy::TantivyError) -> Self { + GraphError::IndexError { source } + } +} +#[cfg(feature = "search")] +impl From for GraphError { + fn from(source: QueryParserError) -> Self { + GraphError::QueryError { source } + } +} + #[derive(thiserror::Error, Debug, PartialEq)] pub enum MutateGraphError { #[error("Create vertex '{vertex_id}' first before adding static properties to it")] diff --git a/raphtory/src/db/api/mutation/addition_ops.rs b/raphtory/src/db/api/mutation/addition_ops.rs index 114600dc5c..d69621451d 100644 --- a/raphtory/src/db/api/mutation/addition_ops.rs +++ b/raphtory/src/db/api/mutation/addition_ops.rs @@ -104,7 +104,8 @@ impl AdditionOps for G { v.id(), v.id_str(), props.collect_properties(), - ) + )?; + Ok(()) } fn add_edge( diff --git a/raphtory/src/db/api/mutation/internal/internal_addition_ops.rs b/raphtory/src/db/api/mutation/internal/internal_addition_ops.rs index 573516374f..4a410b0a4d 100644 --- a/raphtory/src/db/api/mutation/internal/internal_addition_ops.rs +++ b/raphtory/src/db/api/mutation/internal/internal_addition_ops.rs @@ -1,5 +1,5 @@ use crate::{ - core::{utils::errors::GraphError, Prop}, + core::{utils::errors::GraphError, Prop, entities::vertices::vertex_ref::VertexRef}, db::api::view::internal::Base, }; @@ -10,7 +10,7 @@ pub trait InternalAdditionOps { v: u64, name: Option<&str>, props: Vec<(String, Prop)>, - ) -> Result<(), GraphError>; + ) -> Result; fn internal_add_edge( &self, @@ -48,7 +48,7 @@ impl InternalAdditionOps for G { v: u64, name: Option<&str>, props: Vec<(String, Prop)>, - ) -> Result<(), GraphError> { + ) -> Result { self.graph().internal_add_vertex(t, v, name, props) } diff --git a/raphtory/src/db/api/view/internal/core_ops.rs b/raphtory/src/db/api/view/internal/core_ops.rs index 6151b100c9..5ea9269e2d 100644 --- a/raphtory/src/db/api/view/internal/core_ops.rs +++ b/raphtory/src/db/api/view/internal/core_ops.rs @@ -114,6 +114,13 @@ pub trait CoreGraphOps { /// A vector of strings representing the names of the temporal properties fn temporal_vertex_prop_names(&self, v: VID) -> Vec; + /// Returns a vector of all names of temporal properties that exist on at least one vertex + /// + /// # Returns + /// + /// A vector of strings representing the names of the temporal properties + fn all_vertex_prop_names(&self, is_static: bool) -> Vec; + /// Returns the static edge property with the given name for the /// given edge reference. /// @@ -239,6 +246,10 @@ impl CoreGraphOps for G { self.graph().temporal_vertex_prop_names(v) } + fn all_vertex_prop_names(&self, is_static: bool) -> Vec { + self.graph().all_vertex_prop_names(is_static) + } + fn static_edge_prop(&self, e: EdgeRef, name: &str) -> Option { self.graph().static_edge_prop(e, name) } diff --git a/raphtory/src/db/internal/addition.rs b/raphtory/src/db/internal/addition.rs index 4060bcc4a3..764ed2f45b 100644 --- a/raphtory/src/db/internal/addition.rs +++ b/raphtory/src/db/internal/addition.rs @@ -1,5 +1,5 @@ use crate::{ - core::{entities::graph::tgraph::InnerTemporalGraph, utils::errors::GraphError}, + core::{entities::{graph::tgraph::InnerTemporalGraph, vertices::vertex_ref::VertexRef}, utils::errors::GraphError}, db::api::mutation::internal::InternalAdditionOps, prelude::Prop, }; @@ -11,8 +11,10 @@ impl InternalAdditionOps for InnerTemporalGraph { v: u64, name: Option<&str>, props: Vec<(String, Prop)>, - ) -> Result<(), GraphError> { - self.add_vertex_internal(t, v, name, props) + ) -> Result { + let v_id = self.add_vertex_internal(t, v, name, props)?; + + Ok(VertexRef::Local(v_id)) } fn internal_add_edge( diff --git a/raphtory/src/db/internal/core_ops.rs b/raphtory/src/db/internal/core_ops.rs index 9f8831041e..a9b9d658cf 100644 --- a/raphtory/src/db/internal/core_ops.rs +++ b/raphtory/src/db/internal/core_ops.rs @@ -89,6 +89,10 @@ impl CoreGraphOps for InnerTemporalGraph { .collect() } + fn all_vertex_prop_names(&self, is_static: bool) -> Vec { + self.get_all_vertex_property_names(is_static) + } + fn static_edge_prop(&self, e: EdgeRef, name: &str) -> Option { let entry = self.edge_entry(e.pid()); let edge = entry.value()?; diff --git a/raphtory/src/lib.rs b/raphtory/src/lib.rs index cbe7fa45e4..292b7fc484 100644 --- a/raphtory/src/lib.rs +++ b/raphtory/src/lib.rs @@ -96,6 +96,9 @@ pub mod python; #[cfg(feature = "io")] pub mod graph_loader; +#[cfg(feature = "search")] +pub mod search; + pub mod prelude { pub use crate::{ core::{Prop, PropUnwrap}, diff --git a/raphtory/src/search/mod.rs b/raphtory/src/search/mod.rs new file mode 100644 index 0000000000..dec7de7ac8 --- /dev/null +++ b/raphtory/src/search/mod.rs @@ -0,0 +1,629 @@ +// search goes here + +use std::{collections::HashSet, ops::Deref, sync::Arc}; + +use rayon::{prelude::ParallelIterator, slice::ParallelSlice}; +use tantivy::{ + collector::TopDocs, + schema::{Field, Schema, SchemaBuilder, FAST, INDEXED, STORED, TEXT}, + DateOptions, DocAddress, Document, Index, IndexReader, IndexSortByField, TantivyError, +}; + +use crate::{ + core::{ + entities::vertices::vertex_ref::VertexRef, + utils::{errors::GraphError, time::TryIntoTime}, + }, + db::{api::mutation::internal::InternalAdditionOps, graph::vertex::VertexView}, + prelude::*, +}; + +use self::fields::{NAME, TIME, VERTEX_ID, VERTEX_ID_REV}; + +#[derive(Clone)] +pub struct IndexedGraph { + graph: G, + index: Arc, + reader: tantivy::IndexReader, +} + +impl Deref for IndexedGraph { + type Target = G; + + fn deref(&self) -> &Self::Target { + &self.graph + } +} + +pub(in crate::search) mod fields { + pub const TIME: &str = "time"; + pub const VERTEX_ID: &str = "vertex_id"; + pub const VERTEX_ID_REV: &str = "vertex_id_rev"; + pub const NAME: &str = "name"; +} + +const EMPTY: [(&str, Prop); 0] = []; + +impl From for IndexedGraph { + fn from(graph: G) -> Self { + Self::from_graph(&graph).expect("failed to generate index from graph") + } +} + +impl IndexedGraph { + fn new_schema_builder() -> SchemaBuilder { + let mut schema = Schema::builder(); + // we first add GID time, ID and ID_REV + // ensure time is part of the index + schema.add_i64_field(fields::TIME, INDEXED | STORED); + // ensure we add vertex_id as stored to get back the vertex id after the search + schema.add_u64_field(fields::VERTEX_ID, FAST | STORED); + // reverse to sort by it + schema.add_u64_field(fields::VERTEX_ID_REV, FAST | STORED); + // add name + schema.add_text_field(fields::NAME, TEXT); + schema + } + + fn schema_from_props, I: IntoIterator>(props: I) -> Schema { + let mut schema = Self::new_schema_builder(); + + for (prop_name, prop) in props.into_iter() { + match prop { + Prop::Str(_) => { + schema.add_text_field(prop_name.as_ref(), TEXT); + } + Prop::DTime(_) => { + schema.add_date_field(prop_name.as_ref(), INDEXED); + } + _ => todo!(), + } + } + + schema.build() + } + + // we need to check every vertex for the properties and add them + // to the schem depending on the type of the property + // + fn schema(g: &G) -> Schema { + let mut schema = Self::new_schema_builder(); + + // TODO: load all these from the graph at some point in the future + let mut prop_names_set = g + .all_vertex_prop_names(false) + .into_iter() + .chain(g.all_vertex_prop_names(true).into_iter()) + .collect::>(); + + for vertex in g.vertices() { + if prop_names_set.is_empty() { + break; + } + let mut found_props = HashSet::from(["name".to_string()]); + + for prop in prop_names_set.iter() { + // load temporal props + for (_, prop_value) in vertex.property_history(prop.to_string()) { + if found_props.contains(prop) { + continue; + } + match prop_value { + Prop::Str(_) => { + schema.add_text_field(prop, TEXT); + } + Prop::DTime(_) => { + schema.add_date_field(prop, INDEXED); + } + x => todo!("prop value {:?} not supported yet", x), + } + + found_props.insert(prop.to_string()); + } + // load static props + if let Some(prop_value) = vertex.static_property(prop.to_string()) { + match prop_value { + Prop::Str(_) => { + let name = if prop == "_id" { NAME } else { prop }; + if !found_props.contains(name) { + println!("found_props {:?}", found_props); + println!("adding text field {:?}", name); + schema.add_text_field(name, TEXT); + found_props.insert(prop.to_string()); + } + } + _ => todo!(), + } + } + } + + for found_prop in found_props { + prop_names_set.remove(&found_prop); + } + } + + schema.build() + } + + pub fn from_graph(g: &G) -> tantivy::Result { + let schema = Self::schema(g); + let (index, reader) = Self::new_index(schema.clone()); + + let time_field = schema.get_field(fields::TIME)?; + let vertex_id_field = schema.get_field(fields::VERTEX_ID)?; + let vertex_id_rev_field = schema.get_field(fields::VERTEX_ID_REV)?; + + let writer = Arc::new(parking_lot::RwLock::new(index.writer(100_000_000)?)); + + let v_ids = (0..g.num_vertices()).collect::>(); + + v_ids.par_chunks(128).try_for_each(|v_ids| { + let writer_lock = writer.clone(); + { + let writer_guard = writer_lock.read(); + for v_id in v_ids { + if let Some(vertex) = g.vertex(VertexRef::new_local((*v_id).into())) { + let vertex_id: u64 = Into::::into(vertex.vertex) as u64; + let temp_prop_names = vertex.property_names(false); + + for temp_prop_name in temp_prop_names { + let prop_field = schema.get_field(&temp_prop_name)?; + for (time, prop_value) in vertex.property_history(temp_prop_name) { + if let Prop::Str(prop_text) = prop_value { + let mut document = Document::new(); + // add time to the document + document.add_i64(time_field, time); + // add the property to the document + document.add_text(prop_field, prop_text); + // add the vertex_id + document.add_u64(vertex_id_field, vertex_id); + document.add_u64(vertex_id_rev_field, u64::MAX - vertex_id); + + writer_guard.add_document(document)?; + } + } + } + + let prop_names = vertex.property_names(true); + for prop_name in prop_names { + let field_name = if prop_name == "_id" { + "name" + } else { + &prop_name + }; + + let prop_field = schema.get_field(field_name)?; + if let Some(prop_value) = vertex.static_property(prop_name.to_string()) { + if let Prop::Str(prop_text) = prop_value { + // what now? + let mut document = Document::new(); + // add the property to the document + document.add_text(prop_field, prop_text); + // add the vertex_id + document.add_u64(vertex_id_field, vertex_id); + document.add_u64(vertex_id_rev_field, u64::MAX - vertex_id); + + document.add_i64(time_field, i64::MAX); + writer_guard.add_document(document)?; + } + } + } + } + } + } + + let mut writer_guard = writer_lock.write(); + writer_guard.commit()?; + Ok::<(), TantivyError>(()) + })?; + + reader.reload()?; + Ok(IndexedGraph { + graph: g.clone(), + index: Arc::new(index), + reader, + }) + } + + fn new_index(schema: Schema) -> (Index, IndexReader) { + let index_settings = tantivy::IndexSettings { + sort_by_field: Some(IndexSortByField { + field: VERTEX_ID.to_string(), + order: tantivy::Order::Asc, + }), + ..tantivy::IndexSettings::default() + }; + + let index = Index::builder() + .settings(index_settings) + .schema(schema) + .create_in_ram() + .expect("failed to create index"); + + let reader = index + .reader_builder() + .reload_policy(tantivy::ReloadPolicy::OnCommit) + .try_into() + .unwrap(); + (index, reader) + } + + pub fn new, I: IntoIterator>(graph: G, props: I) -> Self { + let schema = Self::schema_from_props(props); + + let (index, reader) = Self::new_index(schema); + + IndexedGraph { + graph, + index: Arc::new(index), + reader, + } + } + + pub fn reload(&self) -> Result<(), GraphError> { + self.reader.reload()?; + Ok(()) + } + + fn resolve_vertex_from_search_result( + &self, + vertex_id: Field, + doc: Document, + ) -> Option> { + let vertex_id: usize = doc + .get_first(vertex_id) + .and_then(|value| value.as_u64())? + .try_into() + .ok()?; + let vertex_id = VertexRef::Local(vertex_id.into()); + self.graph.vertex(vertex_id) + } + + pub fn search( + &self, + q: &str, + limit: usize, + offset: usize, + ) -> Result>, GraphError> { + let searcher = self.reader.searcher(); + let query_parser = tantivy::query::QueryParser::for_index(&self.index, vec![]); + let query = query_parser.parse_query(q)?; + + let ranking = TopDocs::with_limit(limit) + .and_offset(offset) + .order_by_u64_field(VERTEX_ID_REV.to_string()); + + let top_docs: Vec<(u64, DocAddress)> = searcher.search(&query, &ranking)?; + // let top_docs = searcher.search(&query, &ranking)?; + + let vertex_id = self.index.schema().get_field("vertex_id")?; + + let results = top_docs + .into_iter() + .map(|(_, doc_address)| searcher.doc(doc_address)) + .filter_map(Result::ok) + .filter_map(|doc| self.resolve_vertex_from_search_result(vertex_id, doc)) + .collect::>(); + + Ok(results) + } +} + +impl InternalAdditionOps for IndexedGraph { + fn internal_add_vertex( + &self, + t: i64, + v: u64, + name: Option<&str>, + props: Vec<(String, Prop)>, + ) -> Result { + let t: i64 = t.try_into_time()?; + let mut document = Document::new(); + // add time to the document + let time = self.index.schema().get_field(TIME)?; + document.add_i64(time, t); + // add name to the document + + if let Some(vertex_name) = name { + let name = self.index.schema().get_field(NAME)?; + document.add_text(name, vertex_name); + } + + // index all props that are declared in the schema + for (prop_name, prop) in props.iter() { + if let Ok(field) = self.index.schema().get_field(prop_name) { + match prop { + Prop::Str(s) => document.add_text(field, s), + _ => {} + } + } + } + // add the vertex id to the document + let v_ref = self.graph.internal_add_vertex(t, v, name, props)?; + let v_id = self.graph.local_vertex_ref(v_ref).unwrap(); + // get the field from the index + let vertex_id = self.index.schema().get_field(VERTEX_ID)?; + let vertex_id_rev = self.index.schema().get_field(VERTEX_ID_REV)?; + let index_v_id: u64 = Into::::into(v_id) as u64; + + document.add_u64(vertex_id, index_v_id); + document.add_u64(vertex_id_rev, u64::MAX - index_v_id); + + let mut writer = self.index.writer(50_000_000)?; + + writer.add_document(document)?; + + writer.commit()?; + + Ok(v_ref) + } + + fn internal_add_edge( + &self, + _t: i64, + _src: u64, + _dst: u64, + _props: Vec<(String, Prop)>, + _layer: Option<&str>, + ) -> Result<(), GraphError> { + todo!() + } +} + +#[cfg(test)] +mod test { + use std::time::SystemTime; + + use tantivy::{doc, DocAddress}; + + use super::*; + + #[test] + #[ignore = "this test is for experiments with the jira graph"] + fn load_jira_graph() -> Result<(), GraphError> { + let graph = Graph::load_from_file("/tmp/graphs/jira").expect("failed to load graph"); + assert!(graph.num_vertices() > 0); + + let now = SystemTime::now(); + + let index_graph: IndexedGraph = graph.into(); + let elapsed = now.elapsed().unwrap().as_secs(); + println!("indexing took: {:?}", elapsed); + + let issues = index_graph.search("name:'DEV-1690'", 5, 0)?; + + assert!(issues.len() >= 1); + + println!("issues len: {:?}", issues.len()); + + Ok(()) + } + + #[test] + fn create_indexed_graph_from_existing_graph() { + let graph = Graph::new(); + + graph + .add_vertex(1, "Gandalf", [("kind".to_string(), Prop::str("Wizard"))]) + .expect("add vertex failed"); + + graph + .add_vertex( + 2, + "Frodo", + [ + ("kind".to_string(), Prop::str("Hobbit")), + ("has_ring".to_string(), Prop::str("yes")), + ], + ) + .expect("add vertex failed"); + + graph + .add_vertex(2, "Merry", [("kind".to_string(), Prop::str("Hobbit"))]) + .expect("add vertex failed"); + + graph + .add_vertex(4, "Gollum", [("kind".to_string(), Prop::str("Creature"))]) + .expect("add vertex failed"); + + graph + .add_vertex(9, "Gollum", [("has_ring".to_string(), Prop::str("yes"))]) + .expect("add vertex failed"); + + graph + .add_vertex(9, "Frodo", [("has_ring".to_string(), Prop::str("no"))]) + .expect("add vertex failed"); + + graph + .add_vertex(10, "Frodo", [("has_ring".to_string(), Prop::str("yes"))]) + .expect("add vertex failed"); + + graph + .add_vertex(10, "Gollum", [("has_ring".to_string(), Prop::str("no"))]) + .expect("add vertex failed"); + + let indexed_graph: IndexedGraph = + IndexedGraph::from_graph(&graph).expect("failed to generate index from graph"); + indexed_graph.reload().expect("failed to reload index"); + + let results = indexed_graph + .search("kind:hobbit", 10, 0) + .expect("search failed"); + let actual = results.into_iter().map(|v| v.name()).collect::>(); + let expected = vec!["Frodo", "Merry"]; + assert_eq!(actual, expected); + + let results = indexed_graph + .search("kind:wizard", 10, 0) + .expect("search failed"); + let actual = results.into_iter().map(|v| v.name()).collect::>(); + let expected = vec!["Gandalf"]; + assert_eq!(actual, expected); + + let results = indexed_graph + .search("kind:creature", 10, 0) + .expect("search failed"); + let actual = results.into_iter().map(|v| v.name()).collect::>(); + let expected = vec!["Gollum"]; + assert_eq!(actual, expected); + + // search by name + let results = indexed_graph + .search("name:gollum", 10, 0) + .expect("search failed"); + let actual = results.into_iter().map(|v| v.name()).collect::>(); + let expected = vec!["Gollum"]; + assert_eq!(actual, expected); + } + + #[test] + fn add_vertex_search_by_name() { + let graph = IndexedGraph::new(Graph::new(), EMPTY); + + graph + .add_vertex(1, "Gandalf", []) + .expect("add vertex failed"); + + graph.reload().expect("reload failed"); + + let vertices = graph + .search(r#"name:gandalf"#, 10, 0) + .expect("search failed"); + + let actual = vertices.into_iter().map(|v| v.name()).collect::>(); + let expected = vec!["Gandalf"]; + + assert_eq!(actual, expected); + } + + #[test] + fn add_vertex_search_by_description() { + let graph = IndexedGraph::new(Graph::new(), [("description", Prop::str(""))]); + + graph + .add_vertex( + 1, + "Bilbo", + [("description".to_string(), Prop::str("A hobbit"))], + ) + .expect("add vertex failed"); + + graph + .add_vertex( + 2, + "Gandalf", + [("description".to_string(), Prop::str("A wizard"))], + ) + .expect("add vertex failed"); + + graph.reload().expect("reload failed"); + // Find the Wizard + let vertices = graph + .search(r#"description:wizard"#, 10, 0) + .expect("search failed"); + let actual = vertices.into_iter().map(|v| v.name()).collect::>(); + let expected = vec!["Gandalf"]; + assert_eq!(actual, expected); + // Find the Hobbit + let vertices = graph + .search(r#"description:'hobbit'"#, 10, 0) + .expect("search failed"); + let actual = vertices.into_iter().map(|v| v.name()).collect::>(); + let expected = vec!["Bilbo"]; + assert_eq!(actual, expected); + } + + #[test] + fn add_vertex_search_by_description_and_time() { + let graph = IndexedGraph::new(Graph::new(), [("description", Prop::str(""))]); + + graph + .add_vertex( + 1, + "Gandalf", + [("description".to_string(), Prop::str("The wizard"))], + ) + .expect("add vertex failed"); + + graph + .add_vertex( + 2, + "Saruman", + [("description".to_string(), Prop::str("Another wizard"))], + ) + .expect("add vertex failed"); + + graph.reload().expect("reload failed"); + // Find Saruman + let vertices = graph + .search(r#"description:wizard AND time:[2 TO 5]"#, 10, 0) + .expect("search failed"); + let actual = vertices.into_iter().map(|v| v.name()).collect::>(); + let expected = vec!["Saruman"]; + assert_eq!(actual, expected); + // Find Gandalf + let vertices = graph + .search(r#"description:'wizard' AND time:[1 TO 2}"#, 10, 0) + .expect("search failed"); + let actual = vertices.into_iter().map(|v| v.name()).collect::>(); + let expected = vec!["Gandalf"]; + assert_eq!(actual, expected); + // Find both wizards + let vertices = graph + .search(r#"description:'wizard' AND time:[1 TO 100]"#, 10, 0) + .expect("search failed"); + let actual = vertices.into_iter().map(|v| v.name()).collect::>(); + let expected = vec!["Gandalf", "Saruman"]; + assert_eq!(actual, expected); + } + + #[test] + fn tantivy_101() { + let vertex_index_props = vec!["name"]; + + let mut schema = Schema::builder(); + + for prop in vertex_index_props { + schema.add_text_field(prop.as_ref(), TEXT); + } + + // ensure time is part of the index + schema.add_u64_field("time", INDEXED | STORED); + // ensure we add vertex_id as stored to get back the vertex id after the search + schema.add_text_field("vertex_id", FAST | STORED); + + let index = Index::create_in_ram(schema.build()); + + let reader = index + .reader_builder() + .reload_policy(tantivy::ReloadPolicy::OnCommit) + .try_into() + .unwrap(); + + { + let mut writer = index.writer(50_000_000).unwrap(); + + let name = index.schema().get_field("name").unwrap(); + let time = index.schema().get_field("time").unwrap(); + let vertex_id = index.schema().get_field("vertex_id").unwrap(); + + writer + .add_document(doc!(name => "Gandalf", time => 1u64, vertex_id => 0u64)) + .expect("add document failed"); + + writer.commit().expect("commit failed"); + } + + reader.reload().unwrap(); + + let searcher = reader.searcher(); + + let query_parser = tantivy::query::QueryParser::for_index(&index, vec![]); + let query = query_parser.parse_query(r#"name:"gandalf""#).unwrap(); + + let ranking = TopDocs::with_limit(10).order_by_u64_field(VERTEX_ID.to_string()); + let top_docs: Vec<(u64, DocAddress)> = searcher.search(&query, &ranking).unwrap(); + + assert!(top_docs.len() > 0); + } +}