diff --git a/Cargo.lock b/Cargo.lock index 27a243c..a1165b9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1232,12 +1232,19 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + [[package]] name = "prefix_index" version = "0.4.0" dependencies = [ "hdk", "holochain_integrity_types", + "rand", "serde", ] @@ -1303,6 +1310,36 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + [[package]] name = "rayon" version = "1.7.0" diff --git a/dnas/demo/zomes/coordinator/demo/src/demo_prefix_index.rs b/dnas/demo/zomes/coordinator/demo/src/demo_prefix_index.rs index e7dd575..9f5f940 100644 --- a/dnas/demo/zomes/coordinator/demo/src/demo_prefix_index.rs +++ b/dnas/demo/zomes/coordinator/demo/src/demo_prefix_index.rs @@ -47,6 +47,18 @@ pub fn search_index_a(input: SearchIndexInput) -> ExternResult> { index.get_results(input.query, input.limit) } +#[hdk_extern] +pub fn get_random_results_index_a(limit: usize) -> ExternResult> { + let index = PrefixIndex::new( + PREFIX_INDEX_A_NAME.into(), + LinkTypes::PrefixIndexA, + PREFIX_INDEX_A_WIDTH, + PREFIX_INDEX_A_DEPTH, + )?; + + index.get_random_results(limit) +} + #[hdk_extern] pub fn add_to_index_b(text: String) -> ExternResult<()> { let index = PrefixIndex::new( diff --git a/flake.lock b/flake.lock index 4d440e1..062a4b8 100644 --- a/flake.lock +++ b/flake.lock @@ -418,11 +418,11 @@ }, "locked": { "dir": "versions/0_1", - "lastModified": 1683815730, - "narHash": "sha256-AdKLUdb59DVhQg0quBWoR+01p2X+DHt9WzaYnranqso=", + "lastModified": 1684218489, + "narHash": "sha256-k6FKy1k+/8qhnXWwWZcAR5F28Ip3CV+/ERoJ1xCSsCA=", "owner": "holochain", "repo": "holochain", - "rev": "300bf2c59443d24d6b4d4a917815d1c87792615a", + "rev": "e6d3e965814d0bf8f4c77a2f7c4116a27446ab4a", "type": "github" }, "original": { diff --git a/lib/prefix_index/Cargo.toml b/lib/prefix_index/Cargo.toml index b508971..9072b10 100644 --- a/lib/prefix_index/Cargo.toml +++ b/lib/prefix_index/Cargo.toml @@ -11,3 +11,4 @@ name = "prefix_index" hdk = { workspace = true } holochain_integrity_types = { workspace = true } serde = { workspace = true } +rand = "0.8.5" \ No newline at end of file diff --git a/lib/prefix_index/src/prefix_index.rs b/lib/prefix_index/src/prefix_index.rs index 06d89a4..d4d0fd2 100644 --- a/lib/prefix_index/src/prefix_index.rs +++ b/lib/prefix_index/src/prefix_index.rs @@ -1,6 +1,7 @@ use hdk::{hash_path::path::Component, prelude::*}; use crate::utils::*; use crate::validate::*; +use rand::prelude::*; #[derive(Clone, Debug, PartialEq, Deserialize, Serialize, SerializedBytes)] pub struct PrefixIndex { @@ -129,20 +130,19 @@ impl PrefixIndex { path_to_string(path.clone()) ); - let results = self.get_results_from_path(path, limit)?; - - let leafs: Vec = results - .into_iter() - .filter(|r| r.leaf().is_some()) - .map(|p| p.leaf().unwrap().clone()) - .collect(); + self.inner_get_results(path, limit, false) + } - let strings = leafs - .into_iter() - .filter_map(|c| String::try_from(&c).ok()) - .collect(); + pub fn get_random_results(&self, limit: usize) -> ExternResult> { + if limit == 0 { + return Err(wasm_error!(WasmErrorInner::Guest( + "limit must be > 0".into() + ))); + } - Ok(strings) + let base_path = Path::from(self.index_name.clone()).typed(self.link_type)?; + + self.inner_get_results(base_path, limit, true) } /// Make a Path to the result following the ShardStrategy specified by PrefixIndex width + depth @@ -184,20 +184,43 @@ impl PrefixIndex { } /// Gets the deepest-most Paths that descend from `path`, or it's parents, up to limit - fn get_results_from_path(&self, path: TypedPath, limit: usize) -> ExternResult> { - self.inner_get_results_from_path(path, limit, vec![], vec![]) + fn get_results_from_path(&self, path: TypedPath, limit: usize, shuffle: bool) -> ExternResult> { + self.inner_get_results_from_path(path, limit, shuffle, vec![], vec![]) + } + + fn inner_get_results( + &self, + path: TypedPath, + limit: usize, + shuffle: bool, + ) -> ExternResult> { + let results = self.get_results_from_path(path, limit, shuffle)?; + + let leafs: Vec = results + .into_iter() + .filter(|r| r.leaf().is_some()) + .map(|p| p.leaf().unwrap().clone()) + .collect(); + + let strings = leafs + .into_iter() + .filter_map(|c| String::try_from(&c).ok()) + .collect(); + + Ok(strings) } fn inner_get_results_from_path( &self, path: TypedPath, limit: usize, + shuffle: bool, mut visited: Vec, mut results: Vec, ) -> ExternResult> { visited.push(path.clone()); - let children = get_children_paths(path.clone())?; + let mut children = get_children_paths(path.clone())?; match children.len() == 0 { true => { if path.exists()? && !results.contains(&path) && results.len() < limit { @@ -208,7 +231,7 @@ impl PrefixIndex { Some(parent) => { if !visited.contains(&parent) && !parent.is_root() { return self - .inner_get_results_from_path(parent, limit, visited, results); + .inner_get_results_from_path(parent, limit, shuffle, visited, results); } Ok(results) @@ -217,11 +240,18 @@ impl PrefixIndex { } } false => { + if shuffle { + let mut rng = rand::thread_rng(); + let y: f64 = rng.gen(); + children.shuffle(&mut rng) + } + for child in children.into_iter() { let grandchildren = self .inner_get_results_from_path( child.clone(), limit, + shuffle, visited.clone(), results.clone(), ) @@ -241,7 +271,7 @@ impl PrefixIndex { Some(parent) => { if !visited.contains(&parent) && !parent.is_root() { return self - .inner_get_results_from_path(parent, limit, visited, results); + .inner_get_results_from_path(parent, limit, shuffle, visited, results); } Ok(results) diff --git a/tests/src/demo/demo/prefix-index.test.ts b/tests/src/demo/demo/prefix-index.test.ts index 80a2c0f..a42a72e 100644 --- a/tests/src/demo/demo/prefix-index.test.ts +++ b/tests/src/demo/demo/prefix-index.test.ts @@ -742,9 +742,6 @@ test("add results with labels", async () => { ); }); - - - test("preserve letter case in result, but ignore letter case in indexing", async () => { await runScenario( async (scenario) => { @@ -793,4 +790,87 @@ test("preserve letter case in result, but ignore letter case in indexing", async ]); } ) +}); + +test("get_random_results returns random results from prefix index", async () => { + await runScenario( + async (scenario) => { + // Set up the app to be installed + const appSource = { appBundleSource: { path: "../workdir/prefix-index.happ"}}; + + // Add 2 players with the test app to the Scenario. The returned players + // can be destructured. + const [alice] = await scenario.addPlayersWithApps([appSource]); + + // Shortcut peer discovery through gossip and register all agents in every + // conductor of the scenario. + await scenario.shareAllAgents(); + + await alice.cells[0].callZome({ + zome_name: "demo", + fn_name: "add_hashtag_to_index_a", + payload: "#HOLOCHAIN", + }); + await alice.cells[0].callZome({ + zome_name: "demo", + fn_name: "add_hashtag_to_index_a", + payload: "#holosapian", + }); + await alice.cells[0].callZome({ + zome_name: "demo", + fn_name: "add_cashtag_to_index_a", + payload: "$HOLY", + }); + await alice.cells[0].callZome({ + zome_name: "demo", + fn_name: "add_cashtag_to_index_a", + payload: "$CAT", + }); + await alice.cells[0].callZome({ + zome_name: "demo", + fn_name: "add_cashtag_to_index_a", + payload: "$DOGGO", + }); + await alice.cells[0].callZome({ + zome_name: "demo", + fn_name: "add_hashtag_to_index_a", + payload: "#monkeys", + }); + + await pause(1000); + + const [ result1 ]: string[] = await alice.cells[0].callZome({ + zome_name: "demo", + fn_name: "get_random_results_index_a", + payload: 1 + }); + + const [ result2 ]: string[] = await alice.cells[0].callZome({ + zome_name: "demo", + fn_name: "get_random_results_index_a", + payload: 1 + }); + + const [ result3 ]: string[] = await alice.cells[0].callZome({ + zome_name: "demo", + fn_name: "get_random_results_index_a", + payload: 1 + }); + + const [ result4 ]: string[] = await alice.cells[0].callZome({ + zome_name: "demo", + fn_name: "get_random_results_index_a", + payload: 1 + }); + + const [ result5 ]: string[] = await alice.cells[0].callZome({ + zome_name: "demo", + fn_name: "get_random_results_index_a", + payload: 1 + }); + + // Assert we did not get the exact same result 5 times + assert.ok(new Set([result1, result2, result3, result4, result5]).size > 1); + } + ) }); \ No newline at end of file