diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..7bf589d --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,48 @@ +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +name: Continuous integration + +env: + CARGO_TERM_COLOR: always + CARGO_INCREMENTAL: 0 + +jobs: + tests: + runs-on: ubuntu-latest + strategy: + matrix: + include: + - rust: stable + + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: ${{ matrix.rust }} + override: true + - name: Tests + run: | + cargo test --verbose + + bench: + runs-on: ubuntu-latest + strategy: + matrix: + include: + - rust: nightly + + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: ${{ matrix.rust }} + override: true + - name: Tests + run: | + cargo bench --verbose diff --git a/Cargo.lock b/Cargo.lock index 8f84829..24eb134 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -30,15 +30,15 @@ checksum = "1d49d90015b3c36167a20fe2810c5cd875ad504b39cff3d4eae7977e6b7c1cb2" [[package]] name = "autocfg" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "bitflags" -version = "1.2.1" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bstr" @@ -73,6 +73,12 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + [[package]] name = "clap" version = "2.33.3" @@ -158,8 +164,8 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "058ed274caafc1f60c4997b5fc07bf7dc7cca454af7c6e81edffe5f33f70dace" dependencies = [ - "autocfg 1.0.1", - "cfg-if", + "autocfg 1.1.0", + "cfg-if 0.1.10", "crossbeam-utils", "lazy_static", "maybe-uninit", @@ -173,11 +179,36 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8" dependencies = [ - "autocfg 1.0.1", - "cfg-if", + "autocfg 1.1.0", + "cfg-if 0.1.10", "lazy_static", ] +[[package]] +name = "crossterm" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67" +dependencies = [ + "bitflags", + "crossterm_winapi", + "libc", + "mio", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ae1b35a484aa10e07fe0638d02301c5ad24de82d310ccbd2f3693da5f09bf1c" +dependencies = [ + "winapi", +] + [[package]] name = "csv" version = "1.1.3" @@ -200,6 +231,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "dyn-clone" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9b0705efd4599c15a38151f4721f7bc388306f61084d3bfd50bd07fbca5cb60" + [[package]] name = "either" version = "1.6.1" @@ -231,6 +268,22 @@ dependencies = [ "libc", ] +[[package]] +name = "inquire" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a94f0659efe59329832ba0452d3ec753145fc1fb12a8e1d60de4ccf99f5364" +dependencies = [ + "bitflags", + "crossterm", + "dyn-clone", + "lazy_static", + "newline-converter", + "thiserror", + "unicode-segmentation", + "unicode-width", +] + [[package]] name = "itertools" version = "0.8.2" @@ -260,9 +313,19 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.77" +version = "0.2.139" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f96b10ec2560088a8e76961b00d47107b3a625fecb76dedb29ee7ccbf98235" +checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" + +[[package]] +name = "lock_api" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" +dependencies = [ + "autocfg 1.1.0", + "scopeguard", +] [[package]] name = "log" @@ -270,7 +333,7 @@ version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b" dependencies = [ - "cfg-if", + "cfg-if 0.1.10", ] [[package]] @@ -291,7 +354,28 @@ version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "043175f069eda7b85febe4a74abbaeff828d9f8b448515d3151a14a3542811aa" dependencies = [ - "autocfg 1.0.1", + "autocfg 1.1.0", +] + +[[package]] +name = "mio" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.42.0", +] + +[[package]] +name = "newline-converter" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f71d09d5c87634207f894c6b31b6a2b2c64ea3bdcf71bd5599fdbbe1600c00f" +dependencies = [ + "unicode-segmentation", ] [[package]] @@ -300,7 +384,7 @@ version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac267bcc07f48ee5f8935ab0d24f316fb722d7a1292e2913f0cc196b29ffd611" dependencies = [ - "autocfg 1.0.1", + "autocfg 1.1.0", ] [[package]] @@ -313,6 +397,29 @@ dependencies = [ "libc", ] +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "redox_syscall", + "smallvec", + "windows-sys 0.45.0", +] + [[package]] name = "proc-macro2" version = "0.4.30" @@ -494,7 +601,7 @@ version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcf6960dc9a5b4ee8d3e4c5787b4a112a8818e0290a42ff664ad60692fdf2032" dependencies = [ - "autocfg 1.0.1", + "autocfg 1.1.0", "crossbeam-deque", "either", "rayon-core", @@ -522,6 +629,15 @@ dependencies = [ "rand_core 0.3.1", ] +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags", +] + [[package]] name = "regex" version = "1.3.9" @@ -611,7 +727,7 @@ checksum = "f630a6370fd8e457873b4bd2ffdae75408bc291ba72be773772a4c2a065d9ae8" dependencies = [ "proc-macro2 1.0.23", "quote 1.0.7", - "syn 1.0.42", + "syn 1.0.67", ] [[package]] @@ -625,11 +741,42 @@ dependencies = [ "serde", ] +[[package]] +name = "signal-hook" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a253b5e89e2698464fc26b545c9edceb338e18a89effeeecfea192c3025be29d" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" +dependencies = [ + "libc", +] + [[package]] name = "simsearch" version = "0.2.3" dependencies = [ "criterion", + "inquire", "itertools", "json", "lazy_static", @@ -640,6 +787,12 @@ dependencies = [ "triple_accel", ] +[[package]] +name = "smallvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" + [[package]] name = "strsim" version = "0.10.0" @@ -659,9 +812,9 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.42" +version = "1.0.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c51d92969d209b54a98397e1b91c8ae82d8c87a7bb87df0b29aa2ad81454228" +checksum = "6498a9efc342871f91cc2d0d694c674368b4ceb40f62b65a7a08c3792935e702" dependencies = [ "proc-macro2 1.0.23", "quote 1.0.7", @@ -677,6 +830,26 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "thiserror" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f" +dependencies = [ + "proc-macro2 1.0.23", + "quote 1.0.7", + "syn 1.0.67", +] + [[package]] name = "thread_local" version = "1.0.1" @@ -702,6 +875,12 @@ version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "622b09ce2fe2df4618636fb92176d205662f59803f39e70d1c333393082de96c" +[[package]] +name = "unicode-segmentation" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" + [[package]] name = "unicode-width" version = "0.1.8" @@ -731,6 +910,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + [[package]] name = "winapi" version = "0.3.9" @@ -761,3 +946,84 @@ name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2522491fbfcd58cc84d47aeb2958948c4b8982e9a2d8a2a35bbaed431390e7" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" diff --git a/Cargo.toml b/Cargo.toml index faf0477..223e5cf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ optional = true default-features = false [dev-dependencies] +inquire = "0.5" json = "0.11.13" lazy_static = "1.3.0" quickcheck = "0.8" diff --git a/benches/lib.rs b/benches/lib.rs index 07435bf..9c56b66 100644 --- a/benches/lib.rs +++ b/benches/lib.rs @@ -2,8 +2,8 @@ use std::fs::File; use std::io::Read; use criterion::{criterion_group, criterion_main, BatchSize, Criterion}; -use json; -use simsearch::{SimSearch, SearchOptions}; + +use simsearch::{SearchOptions, SimSearch}; /// Loads content of a 'books.json' file into a JsonValue. fn load_content() -> json::JsonValue { @@ -25,7 +25,7 @@ fn bench_engine(c: &mut Criterion) { } bencher.iter_batched_ref( - || SimSearch::new(), + SimSearch::new, |engine| { for (title, terms) in &books { engine.insert(*title, *terms); diff --git a/examples/books.rs b/examples/books.rs index b5df859..4a5245a 100644 --- a/examples/books.rs +++ b/examples/books.rs @@ -1,37 +1,67 @@ use std::fs::File; -use std::io::{self, Read as _}; -use std::time::Instant; +use std::io::Read as _; + +use inquire::ui::{Color, RenderConfig, StyleSheet, Styled}; +use inquire::{ + autocompletion::{Autocomplete, Replacement}, + CustomUserError, Text, +}; -use json; use simsearch::SimSearch; -fn main() -> io::Result<()> { - let mut engine = SimSearch::new(); +fn main() { + inquire::set_global_render_config(get_render_config()); - let mut file = File::open("./books.json")?; - let mut content = String::new(); - file.read_to_string(&mut content)?; + Text::new("Search for a book:") + .with_autocomplete(BookSearcher::load()) + .with_help_message("Try typing 'old man'") + .with_page_size(15) + .prompt() + .unwrap(); +} - let j = json::parse(&content).unwrap(); +#[derive(Clone)] +pub struct BookSearcher { + engine: SimSearch, +} - for title in j.members() { - engine.insert(title.as_str().unwrap(), title.as_str().unwrap()); - } +impl BookSearcher { + pub fn load() -> Self { + let mut engine = SimSearch::new(); - println!("Please input a query string and hit enter (e.g 'old man'):",); + let mut file = File::open("./books.json").unwrap(); + let mut content = String::new(); + file.read_to_string(&mut content).unwrap(); + let j = json::parse(&content).unwrap(); + for title in j.members() { + engine.insert(title.as_str().unwrap().to_string(), title.as_str().unwrap()); + } - loop { - let mut pattern = String::new(); - io::stdin() - .read_line(&mut pattern) - .expect("failed to read from stdin"); + BookSearcher { engine } + } +} - let start = Instant::now(); - let res = engine.search(&pattern); - let end = Instant::now(); +impl Autocomplete for BookSearcher { + fn get_suggestions(&mut self, input: &str) -> Result, CustomUserError> { + Ok(self.engine.search(input)) + } - println!("pattern: {:?}", pattern.trim()); - println!("results: {:?}", res); - println!("time: {:?}", end - start); + fn get_completion( + &mut self, + _: &str, + _: Option, + ) -> Result { + Ok(None) } } + +fn get_render_config() -> RenderConfig { + let mut render_config = RenderConfig::default(); + + render_config.prompt_prefix = Styled::new(">").with_fg(Color::LightRed); + render_config.highlighted_option_prefix = Styled::new("*").with_fg(Color::LightYellow); + render_config.option = StyleSheet::new().with_fg(Color::DarkBlue); + render_config.help_message = StyleSheet::new().with_fg(Color::LightYellow); + + render_config +} diff --git a/src/lib.rs b/src/lib.rs index 11a0b9b..7b5ad42 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -27,7 +27,7 @@ //! let mut engine: SimSearch = SimSearch::new_with(options); //! ``` -use std::cmp::max; +use std::cmp::{max, Ordering}; use std::collections::HashMap; use std::f64; use std::hash::Hash; @@ -39,10 +39,11 @@ use triple_accel::levenshtein::levenshtein_simd_k; use serde::{Deserialize, Serialize}; /// The simple search engine. +#[derive(Debug, Clone)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct SimSearch where - Id: Eq + PartialEq + Clone + Hash, + Id: Eq + PartialEq + Clone + Hash + Ord, { option: SearchOptions, id_num_counter: usize, @@ -54,7 +55,7 @@ where impl SimSearch where - Id: Eq + PartialEq + Clone + Hash, + Id: Eq + PartialEq + Clone + Hash + Ord, { /// Creates search engine with default options. pub fn new() -> Self { @@ -247,34 +248,42 @@ where } } - let mut result_scores: Vec<(usize, f64)> = result_scores.drain().collect(); - result_scores.sort_by(|lhs, rhs| rhs.1.partial_cmp(&lhs.1).unwrap()); - - let result_ids: Vec = result_scores - .iter() - .map(|(id_num, _)| { - self.reverse_ids_map - .get(id_num) + let mut result_scores: Vec<(f64, Id)> = result_scores + .drain() + .map(|(id_num, score)| { + let id = self + .reverse_ids_map + .get(&id_num) // this can go wrong only if something (e.g. delete) leaves us in an // inconsistent state .expect("id at id_num should be there") - .to_owned() + .to_owned(); + (score, id) }) .collect(); + result_scores.sort_by(|(lhs_score, lhs_id), (rhs_score, rhs_id)| { + match rhs_score.partial_cmp(lhs_score).unwrap() { + Ordering::Equal => lhs_id.cmp(rhs_id), + ord => ord, + } + }); + + let result_ids: Vec = result_scores.into_iter().map(|(_, id)| id).collect(); + result_ids } /// Deletes entry by id. pub fn delete(&mut self, id: &Id) { if let Some(id_num) = self.ids_map.get(id) { - for token in &self.forward_map[&id_num] { + for token in &self.forward_map[id_num] { self.reverse_map .get_mut(token) .unwrap() .retain(|i| i != id_num); } - self.forward_map.remove(&id_num); + self.forward_map.remove(id_num); self.reverse_ids_map.remove(id_num); self.ids_map.remove(id); }; @@ -326,6 +335,7 @@ where /// let mut engine: SimSearch = SimSearch::new_with( /// SearchOptions::new().case_sensitive(true)); /// ``` +#[derive(Debug, Clone)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct SearchOptions { case_sensitive: bool, @@ -422,3 +432,18 @@ impl SearchOptions { } } } + +impl Default for SimSearch +where + Id: Eq + PartialEq + Clone + Hash + Ord, +{ + fn default() -> Self { + SimSearch::new() + } +} + +impl Default for SearchOptions { + fn default() -> Self { + SearchOptions::new() + } +} diff --git a/tests/lib.rs b/tests/lib.rs index fd0260e..7f47ae4 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -1,7 +1,6 @@ use std::fs::File; use std::io::Read; -use json; use simsearch::{SearchOptions, SimSearch}; #[macro_use(quickcheck)]