diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 13080f11d..3a6ba2df9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -39,11 +39,11 @@ jobs: - name: Check Formatting run: cargo +nightly fmt --all -- --check - + - name: Check Stable Compilation run: cargo build --all-features - + - name: Check Bench Compilation run: cargo +nightly bench --no-run --profile=dev --all-features @@ -59,10 +59,10 @@ jobs: strategy: matrix: - features: [ - { label: "all", flags: "mmap,stopwords,lz4-compression,zstd-compression,failpoints" }, - { label: "quickwit", flags: "mmap,quickwit,failpoints" } - ] + features: + - { label: "all", flags: "mmap,stopwords,lz4-compression,zstd-compression,failpoints,stemmer" } + - { label: "quickwit", flags: "mmap,quickwit,failpoints" } + - { label: "none", flags: "" } name: test-${{ matrix.features.label}} @@ -80,7 +80,21 @@ jobs: - uses: Swatinem/rust-cache@v2 - name: Run tests - run: cargo +stable nextest run --features ${{ matrix.features.flags }} --verbose --workspace + run: | + # if matrix.feature.flags is empty then run on --lib to avoid compiling examples + # (as most of them rely on mmap) otherwise run all + if [ -z "${{ matrix.features.flags }}" ]; then + cargo +stable nextest run --lib --no-default-features --verbose --workspace + else + cargo +stable nextest run --features ${{ matrix.features.flags }} --no-default-features --verbose --workspace + fi - name: Run doctests - run: cargo +stable test --doc --features ${{ matrix.features.flags }} --verbose --workspace + run: | + # if matrix.feature.flags is empty then run on --lib to avoid compiling examples + # (as most of them rely on mmap) otherwise run all + if [ -z "${{ matrix.features.flags }}" ]; then + echo "no doctest for no feature flag" + else + cargo +stable test --doc --features ${{ matrix.features.flags }} --verbose --workspace + fi diff --git a/Cargo.toml b/Cargo.toml index 10d1c8400..40eff7814 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,7 +37,7 @@ fs4 = { version = "0.13.1", optional = true } levenshtein_automata = "0.2.1" uuid = { version = "1.0.0", features = ["v4", "serde"] } crossbeam-channel = "0.5.4" -rust-stemmers = "1.2.0" +rust-stemmers = { version = "1.2.0", optional = true } downcast-rs = "2.0.1" bitpacking = { version = "0.9.2", default-features = false, features = [ "bitpacker4x", @@ -113,7 +113,8 @@ debug-assertions = true overflow-checks = true [features] -default = ["mmap", "stopwords", "lz4-compression", "columnar-zstd-compression"] +default = ["mmap", "stopwords", "lz4-compression", "columnar-zstd-compression", "stemmer"] +stemmer = ["rust-stemmers"] mmap = ["fs4", "tempfile", "memmap2"] stopwords = [] diff --git a/src/directory/file_watcher.rs b/src/directory/mmap_directory/file_watcher.rs similarity index 100% rename from src/directory/file_watcher.rs rename to src/directory/mmap_directory/file_watcher.rs diff --git a/src/directory/mmap_directory.rs b/src/directory/mmap_directory/mod.rs similarity index 99% rename from src/directory/mmap_directory.rs rename to src/directory/mmap_directory/mod.rs index f4785ef72..60ef82b30 100644 --- a/src/directory/mmap_directory.rs +++ b/src/directory/mmap_directory/mod.rs @@ -1,3 +1,5 @@ +mod file_watcher; + use std::collections::HashMap; use std::fmt; use std::fs::{self, File, OpenOptions}; @@ -7,6 +9,7 @@ use std::path::{Path, PathBuf}; use std::sync::{Arc, RwLock, Weak}; use common::StableDeref; +use file_watcher::FileWatcher; use fs4::fs_std::FileExt; #[cfg(all(feature = "mmap", unix))] pub use memmap2::Advice; @@ -18,7 +21,6 @@ use crate::core::META_FILEPATH; use crate::directory::error::{ DeleteError, LockError, OpenDirectoryError, OpenReadError, OpenWriteError, }; -use crate::directory::file_watcher::FileWatcher; use crate::directory::{ AntiCallToken, Directory, DirectoryLock, FileHandle, Lock, OwnedBytes, TerminatingWrite, WatchCallback, WatchHandle, WritePtr, diff --git a/src/directory/mod.rs b/src/directory/mod.rs index 7fab7e051..d4494d307 100644 --- a/src/directory/mod.rs +++ b/src/directory/mod.rs @@ -5,7 +5,6 @@ mod mmap_directory; mod directory; mod directory_lock; -mod file_watcher; pub mod footer; mod managed_directory; mod ram_directory; diff --git a/src/index/index_meta.rs b/src/index/index_meta.rs index d95ce6ff7..d06d706c4 100644 --- a/src/index/index_meta.rs +++ b/src/index/index_meta.rs @@ -404,7 +404,10 @@ mod tests { schema_builder.build() }; let index_metas = IndexMeta { - index_settings: IndexSettings::default(), + index_settings: IndexSettings { + docstore_compression: Compressor::None, + ..Default::default() + }, segments: Vec::new(), schema, opstamp: 0u64, @@ -413,7 +416,7 @@ mod tests { let json = serde_json::ser::to_string(&index_metas).expect("serialization failed"); assert_eq!( json, - r#"{"index_settings":{"docstore_compression":"lz4","docstore_blocksize":16384},"segments":[],"schema":[{"name":"text","type":"text","options":{"indexing":{"record":"position","fieldnorms":true,"tokenizer":"default"},"stored":false,"fast":false}}],"opstamp":0}"# + r#"{"index_settings":{"docstore_compression":"none","docstore_blocksize":16384},"segments":[],"schema":[{"name":"text","type":"text","options":{"indexing":{"record":"position","fieldnorms":true,"tokenizer":"default"},"stored":false,"fast":false}}],"opstamp":0}"# ); let deser_meta: UntrackedIndexMeta = serde_json::from_str(&json).unwrap(); @@ -494,6 +497,8 @@ mod tests { #[test] #[cfg(feature = "lz4-compression")] fn test_index_settings_default() { + use crate::store::Compressor; + let mut index_settings = IndexSettings::default(); assert_eq!( index_settings, diff --git a/src/indexer/segment_writer.rs b/src/indexer/segment_writer.rs index 72152cffa..94e3f0de2 100644 --- a/src/indexer/segment_writer.rs +++ b/src/indexer/segment_writer.rs @@ -421,10 +421,9 @@ fn remap_and_write( #[cfg(test)] mod tests { use std::collections::BTreeMap; - use std::path::{Path, PathBuf}; + use std::path::Path; use columnar::ColumnType; - use tempfile::TempDir; use crate::collector::{Count, TopDocs}; use crate::directory::RamDirectory; @@ -1067,10 +1066,7 @@ mod tests { let mut schema_builder = Schema::builder(); schema_builder.add_text_field("title", text_options); let schema = schema_builder.build(); - let tempdir = TempDir::new().unwrap(); - let tempdir_path = PathBuf::from(tempdir.path()); - Index::create_in_dir(&tempdir_path, schema).unwrap(); - let index = Index::open_in_dir(tempdir_path).unwrap(); + let index = Index::create_in_ram(schema); let schema = index.schema(); let mut index_writer = index.writer(50_000_000).unwrap(); let title = schema.get_field("title").unwrap(); diff --git a/src/lib.rs b/src/lib.rs index 7890d6188..f0b3120a5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -207,6 +207,7 @@ mod docset; mod reader; #[cfg(test)] +#[cfg(feature = "mmap")] mod compat_tests; pub use self::reader::{IndexReader, IndexReaderBuilder, ReloadPolicy, Warmer}; @@ -1174,12 +1175,11 @@ pub mod tests { #[test] fn test_validate_checksum() -> crate::Result<()> { - let index_path = tempfile::tempdir().expect("dir"); let mut builder = Schema::builder(); let body = builder.add_text_field("body", TEXT | STORED); let schema = builder.build(); - let index = Index::create_in_dir(&index_path, schema)?; - let mut writer: IndexWriter = index.writer(50_000_000)?; + let index = Index::create_in_ram(schema); + let mut writer: IndexWriter = index.writer_for_tests()?; writer.set_merge_policy(Box::new(NoMergePolicy)); for _ in 0..5000 { writer.add_document(doc!(body => "foo"))?; diff --git a/src/snippet/mod.rs b/src/snippet/mod.rs index 020e6b588..ee61b534a 100644 --- a/src/snippet/mod.rs +++ b/src/snippet/mod.rs @@ -483,7 +483,7 @@ mod tests { use super::{collapse_overlapped_ranges, search_fragments, select_best_fragment_combination}; use crate::query::QueryParser; - use crate::schema::{IndexRecordOption, Schema, TextFieldIndexing, TextOptions, TEXT}; + use crate::schema::{Schema, TEXT}; use crate::snippet::SnippetGenerator; use crate::tokenizer::{NgramTokenizer, SimpleTokenizer}; use crate::Index; @@ -727,8 +727,10 @@ Survey in 2016, 2017, and 2018."#; Ok(()) } + #[cfg(feature = "stemmer")] #[test] fn test_snippet_generator() -> crate::Result<()> { + use crate::schema::{IndexRecordOption, TextFieldIndexing, TextOptions}; let mut schema_builder = Schema::builder(); let text_options = TextOptions::default().set_indexing_options( TextFieldIndexing::default() diff --git a/src/store/mod.rs b/src/store/mod.rs index 582643515..cccf4d8f9 100644 --- a/src/store/mod.rs +++ b/src/store/mod.rs @@ -102,6 +102,7 @@ pub(crate) mod tests { } const NUM_DOCS: usize = 1_000; + #[test] fn test_doc_store_iter_with_delete_bug_1077() -> crate::Result<()> { // this will cover deletion of the first element in a checkpoint @@ -113,7 +114,7 @@ pub(crate) mod tests { let directory = RamDirectory::create(); let store_wrt = directory.open_write(path)?; let schema = - write_lorem_ipsum_store(store_wrt, NUM_DOCS, Compressor::Lz4, BLOCK_SIZE, true); + write_lorem_ipsum_store(store_wrt, NUM_DOCS, Compressor::default(), BLOCK_SIZE, true); let field_title = schema.get_field("title").unwrap(); let store_file = directory.open_read(path)?; let store = StoreReader::open(store_file, 10)?; diff --git a/src/store/reader.rs b/src/store/reader.rs index fb1533988..a4105abec 100644 --- a/src/store/reader.rs +++ b/src/store/reader.rs @@ -465,7 +465,7 @@ mod tests { let directory = RamDirectory::create(); let path = Path::new("store"); let writer = directory.open_write(path)?; - let schema = write_lorem_ipsum_store(writer, 500, Compressor::default(), BLOCK_SIZE, true); + let schema = write_lorem_ipsum_store(writer, 500, Compressor::None, BLOCK_SIZE, true); let title = schema.get_field("title").unwrap(); let store_file = directory.open_read(path)?; let store = StoreReader::open(store_file, DOCSTORE_CACHE_CAPACITY)?; @@ -499,7 +499,7 @@ mod tests { assert_eq!(store.cache_stats().cache_hits, 1); assert_eq!(store.cache_stats().cache_misses, 2); - assert_eq!(store.cache.peek_lru(), Some(11207)); + assert_eq!(store.cache.peek_lru(), Some(232206)); Ok(()) } diff --git a/src/tokenizer/mod.rs b/src/tokenizer/mod.rs index 5a5435562..31c518fd4 100644 --- a/src/tokenizer/mod.rs +++ b/src/tokenizer/mod.rs @@ -132,13 +132,14 @@ mod regex_tokenizer; mod remove_long; mod simple_tokenizer; mod split_compound_words; -mod stemmer; mod stop_word_filter; mod tokenized_string; mod tokenizer; mod tokenizer_manager; mod whitespace_tokenizer; +#[cfg(feature = "stemmer")] +mod stemmer; pub use tokenizer_api::{BoxTokenStream, Token, TokenFilter, TokenStream, Tokenizer}; pub use self::alphanum_only::AlphaNumOnlyFilter; @@ -151,6 +152,7 @@ pub use self::regex_tokenizer::RegexTokenizer; pub use self::remove_long::RemoveLongFilter; pub use self::simple_tokenizer::{SimpleTokenStream, SimpleTokenizer}; pub use self::split_compound_words::SplitCompoundWords; +#[cfg(feature = "stemmer")] pub use self::stemmer::{Language, Stemmer}; pub use self::stop_word_filter::StopWordFilter; pub use self::tokenized_string::{PreTokenizedStream, PreTokenizedString}; @@ -167,10 +169,7 @@ pub const MAX_TOKEN_LEN: usize = u16::MAX as usize - 5; #[cfg(test)] pub(crate) mod tests { - use super::{ - Language, LowerCaser, RemoveLongFilter, SimpleTokenizer, Stemmer, Token, TokenizerManager, - }; - use crate::tokenizer::TextAnalyzer; + use super::{Token, TokenizerManager}; /// This is a function that can be used in tests and doc tests /// to assert a token's correctness. @@ -205,59 +204,15 @@ pub(crate) mod tests { } #[test] - fn test_en_tokenizer() { + fn test_tokenizer_does_not_exist() { let tokenizer_manager = TokenizerManager::default(); assert!(tokenizer_manager.get("en_doesnotexist").is_none()); - let mut en_tokenizer = tokenizer_manager.get("en_stem").unwrap(); - let mut tokens: Vec = vec![]; - { - let mut add_token = |token: &Token| { - tokens.push(token.clone()); - }; - en_tokenizer - .token_stream("Hello, happy tax payer!") - .process(&mut add_token); - } - - assert_eq!(tokens.len(), 4); - assert_token(&tokens[0], 0, "hello", 0, 5); - assert_token(&tokens[1], 1, "happi", 7, 12); - assert_token(&tokens[2], 2, "tax", 13, 16); - assert_token(&tokens[3], 3, "payer", 17, 22); - } - - #[test] - fn test_non_en_tokenizer() { - let tokenizer_manager = TokenizerManager::default(); - tokenizer_manager.register( - "el_stem", - TextAnalyzer::builder(SimpleTokenizer::default()) - .filter(RemoveLongFilter::limit(40)) - .filter(LowerCaser) - .filter(Stemmer::new(Language::Greek)) - .build(), - ); - let mut en_tokenizer = tokenizer_manager.get("el_stem").unwrap(); - let mut tokens: Vec = vec![]; - { - let mut add_token = |token: &Token| { - tokens.push(token.clone()); - }; - en_tokenizer - .token_stream("Καλημέρα, χαρούμενε φορολογούμενε!") - .process(&mut add_token); - } - - assert_eq!(tokens.len(), 3); - assert_token(&tokens[0], 0, "καλημερ", 0, 16); - assert_token(&tokens[1], 1, "χαρουμεν", 18, 36); - assert_token(&tokens[2], 2, "φορολογουμεν", 37, 63); } #[test] fn test_tokenizer_empty() { let tokenizer_manager = TokenizerManager::default(); - let mut en_tokenizer = tokenizer_manager.get("en_stem").unwrap(); + let mut en_tokenizer = tokenizer_manager.get("default").unwrap(); { let mut tokens: Vec = vec![]; { diff --git a/src/tokenizer/stemmer.rs b/src/tokenizer/stemmer.rs index fc87440ce..764efc2ee 100644 --- a/src/tokenizer/stemmer.rs +++ b/src/tokenizer/stemmer.rs @@ -142,3 +142,60 @@ impl TokenStream for StemmerTokenStream { self.tail.token_mut() } } + +#[cfg(test)] +mod tests { + use tokenizer_api::Token; + + use super::*; + use crate::tokenizer::tests::assert_token; + use crate::tokenizer::{LowerCaser, SimpleTokenizer, TextAnalyzer, TokenizerManager}; + + #[test] + fn test_en_stem() { + let tokenizer_manager = TokenizerManager::default(); + let mut en_tokenizer = tokenizer_manager.get("en_stem").unwrap(); + let mut tokens: Vec = vec![]; + { + let mut add_token = |token: &Token| { + tokens.push(token.clone()); + }; + en_tokenizer + .token_stream("Dogs are the bests!") + .process(&mut add_token); + } + + assert_eq!(tokens.len(), 4); + assert_token(&tokens[0], 0, "dog", 0, 4); + assert_token(&tokens[1], 1, "are", 5, 8); + assert_token(&tokens[2], 2, "the", 9, 12); + assert_token(&tokens[3], 3, "best", 13, 18); + } + + #[test] + fn test_non_en_stem() { + let tokenizer_manager = TokenizerManager::default(); + tokenizer_manager.register( + "el_stem", + TextAnalyzer::builder(SimpleTokenizer::default()) + .filter(LowerCaser) + .filter(Stemmer::new(Language::Greek)) + .build(), + ); + let mut el_tokenizer = tokenizer_manager.get("el_stem").unwrap(); + let mut tokens: Vec = vec![]; + { + let mut add_token = |token: &Token| { + tokens.push(token.clone()); + }; + el_tokenizer + .token_stream("Καλημέρα, χαρούμενε φορολογούμενε!") + .process(&mut add_token); + } + + assert_eq!(tokens.len(), 3); + assert_token(&tokens[0], 0, "καλημερ", 0, 16); + assert_token(&tokens[1], 1, "χαρουμεν", 18, 36); + assert_token(&tokens[2], 2, "φορολογουμεν", 37, 63); + } +} diff --git a/src/tokenizer/tokenizer_manager.rs b/src/tokenizer/tokenizer_manager.rs index a0bdbcc0c..8bdbba7bd 100644 --- a/src/tokenizer/tokenizer_manager.rs +++ b/src/tokenizer/tokenizer_manager.rs @@ -1,10 +1,9 @@ use std::collections::HashMap; use std::sync::{Arc, RwLock}; -use crate::tokenizer::stemmer::Language; use crate::tokenizer::tokenizer::TextAnalyzer; use crate::tokenizer::{ - LowerCaser, RawTokenizer, RemoveLongFilter, SimpleTokenizer, Stemmer, WhitespaceTokenizer, + LowerCaser, RawTokenizer, RemoveLongFilter, SimpleTokenizer, WhitespaceTokenizer, }; /// The tokenizer manager serves as a store for @@ -64,14 +63,18 @@ impl Default for TokenizerManager { .filter(LowerCaser) .build(), ); - manager.register( - "en_stem", - TextAnalyzer::builder(SimpleTokenizer::default()) - .filter(RemoveLongFilter::limit(40)) - .filter(LowerCaser) - .filter(Stemmer::new(Language::English)) - .build(), - ); + #[cfg(feature = "stemmer")] + { + use crate::tokenizer::stemmer::{Language, Stemmer}; + manager.register( + "en_stem", + TextAnalyzer::builder(SimpleTokenizer::default()) + .filter(RemoveLongFilter::limit(40)) + .filter(LowerCaser) // The stemmer does not lowercase + .filter(Stemmer::new(Language::English)) + .build(), + ); + } manager.register("whitespace", WhitespaceTokenizer::default()); manager }