Skip to content

Commit 4997b28

Browse files
committed
Initial insert and search optimizations
1 parent 7567b5e commit 4997b28

4 files changed

Lines changed: 96 additions & 57 deletions

File tree

src/extractors/character_ngrams.rs

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
use crate::FeatureExtractor;
22
use lasso::{Rodeo, Spur};
3+
use rustc_hash::FxHashMap;
4+
use std::fmt::Write;
35

46
#[derive(Clone)]
57
pub struct CharacterNgrams {
@@ -38,26 +40,39 @@ impl FeatureExtractor for CharacterNgrams {
3840
}
3941

4042
let expected_ngrams = total_len - self.n + 1;
41-
let mut ngrams = Vec::with_capacity(expected_ngrams);
42-
4343
let padding = self.endmarker.repeat(padding_len);
4444

45-
// collect chars once, then slice
45+
// Collect chars once, then slice
4646
let mut all_chars = Vec::with_capacity(total_len);
4747
all_chars.extend(padding.chars());
4848
all_chars.extend(text.chars());
4949
all_chars.extend(padding.chars());
5050

51-
// Generate n-grams using efficient windowing
51+
// Inline counting + interning in one pass (no intermediate Vec<String>)
52+
let mut counter: FxHashMap<String, usize> = FxHashMap::default();
53+
let mut result = Vec::with_capacity(expected_ngrams);
54+
let mut ngram_buffer = String::with_capacity(self.n * 4);
55+
let mut counted_buffer = String::with_capacity(self.n * 4 + 8);
56+
5257
for window in all_chars.windows(self.n) {
53-
// Pre-allocate string with known capacity
54-
let mut ngram = String::with_capacity(self.n * 4); // Assume max 4 bytes per char
58+
// Build n-gram in reusable buffer
59+
ngram_buffer.clear();
5560
for &ch in window {
56-
ngram.push(ch);
61+
ngram_buffer.push(ch);
5762
}
58-
ngrams.push(ngram);
63+
64+
// Count occurrence
65+
let count = counter.entry(ngram_buffer.clone()).or_insert(0);
66+
*count += 1;
67+
68+
// Build counted string and intern
69+
counted_buffer.clear();
70+
counted_buffer.push_str(&ngram_buffer);
71+
write!(&mut counted_buffer, "{count}").unwrap();
72+
result.push(interner.get_or_intern(&counted_buffer));
5973
}
6074

61-
super::append_feature_counts(interner, ngrams)
75+
result.sort_unstable();
76+
result
6277
}
6378
}

src/extractors/mod.rs

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,29 +2,6 @@ mod character_ngrams;
22
mod word_ngrams;
33

44
use lasso::{Rodeo, Spur};
5-
use rustc_hash::FxHashMap;
6-
use std::fmt::Write;
7-
8-
/// Takes a list of features and makes each one unique by appending its occurrence count,
9-
/// then interns the result and returns them sorted.
10-
fn append_feature_counts(interner: &mut Rodeo, features: Vec<String>) -> Vec<Spur> {
11-
let mut counter: FxHashMap<String, usize> = FxHashMap::default();
12-
let mut unique_features = Vec::with_capacity(features.len());
13-
14-
for val in features {
15-
let count = counter.entry(val.clone()).or_insert(0);
16-
*count += 1;
17-
18-
let mut unique_string = String::with_capacity(val.len() + 8); // Extra space for count
19-
unique_string.push_str(&val);
20-
write!(&mut unique_string, "{count}",).unwrap();
21-
22-
unique_features.push(interner.get_or_intern(unique_string));
23-
}
24-
25-
unique_features.sort_unstable();
26-
unique_features
27-
}
285

296
pub trait FeatureExtractor: Send + Sync {
307
/// Extracts features from text, interning them and returning their IDs.

src/extractors/word_ngrams.rs

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
use crate::FeatureExtractor;
22
use lasso::{Rodeo, Spur};
3+
use rustc_hash::FxHashMap;
4+
use std::fmt::Write;
35

46
#[derive(Clone)]
57
pub struct WordNgrams {
@@ -30,25 +32,42 @@ impl FeatureExtractor for WordNgrams {
3032
return vec![];
3133
}
3234

33-
let tokens = text.split(&self.splitter).filter(|s| !s.is_empty());
35+
let tokens: Vec<&str> = text
36+
.split(&self.splitter)
37+
.filter(|s| !s.is_empty())
38+
.collect();
3439

35-
// an iterator that includes padding
36-
let padded_tokens_iter = std::iter::once(self.padder.as_str())
37-
.chain(tokens)
38-
.chain(std::iter::once(self.padder.as_str()));
40+
// Padded tokens iterator
41+
let padded_tokens: Vec<&str> = std::iter::once(self.padder.as_str())
42+
.chain(tokens.into_iter())
43+
.chain(std::iter::once(self.padder.as_str()))
44+
.collect();
3945

40-
// Use a buffer to collect tokens for each n-gram
41-
let mut buffer: Vec<&str> = Vec::with_capacity(self.n);
42-
let mut ngrams = Vec::new();
46+
if padded_tokens.len() < self.n {
47+
return vec![];
48+
}
49+
50+
// Inline counting + interning in one pass
51+
let mut counter: FxHashMap<String, usize> = FxHashMap::default();
52+
let expected_ngrams = padded_tokens.len() - self.n + 1;
53+
let mut result = Vec::with_capacity(expected_ngrams);
54+
let mut counted_buffer = String::with_capacity(64);
55+
56+
for window in padded_tokens.windows(self.n) {
57+
let ngram = window.join(" ");
58+
59+
// Count occurrence
60+
let count = counter.entry(ngram.clone()).or_insert(0);
61+
*count += 1;
4362

44-
for token in padded_tokens_iter {
45-
buffer.push(token);
46-
if buffer.len() == self.n {
47-
ngrams.push(buffer.join(" "));
48-
buffer.remove(0);
49-
}
63+
// Build counted string and intern
64+
counted_buffer.clear();
65+
counted_buffer.push_str(&ngram);
66+
write!(&mut counted_buffer, "{count}").unwrap();
67+
result.push(interner.get_or_intern(&counted_buffer));
5068
}
5169

52-
super::append_feature_counts(interner, ngrams)
70+
result.sort_unstable();
71+
result
5372
}
5473
}

src/search.rs

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -103,26 +103,46 @@ impl<'db, M: Measure> Searcher<'db, M> {
103103

104104
let min_feat_size = self.measure.min_feature_size(query_size, alpha);
105105
let max_feat_size = self.measure.max_feature_size(query_size, alpha, self.db);
106+
let range_size = max_feat_size.saturating_sub(min_feat_size) + 1;
106107

107-
(min_feat_size..=max_feat_size)
108+
// For small ranges, sequential is faster than Rayon's thread pool overhead
109+
if range_size <= 4 {
110+
let mut all_candidates: Vec<StringId> = Vec::new();
111+
for candidate_size in min_feat_size..=max_feat_size {
112+
let tau =
113+
self.measure
114+
.minimum_common_feature_count(query_size, candidate_size, alpha);
115+
116+
if tau == 0 || tau > query_size {
117+
continue;
118+
}
119+
120+
all_candidates.extend(self.overlap_join(query_features, tau, candidate_size));
121+
}
122+
// Deduplicate at the end (faster than HashSet for small collections)
123+
all_candidates.sort_unstable();
124+
all_candidates.dedup();
125+
return all_candidates.into_iter().collect();
126+
}
127+
128+
// For larger ranges, use parallel iteration but collect into Vec first
129+
let all_candidates: Vec<StringId> = (min_feat_size..=max_feat_size)
108130
.into_par_iter()
109-
.map(|candidate_size| {
131+
.flat_map(|candidate_size| {
110132
let tau =
111133
self.measure
112134
.minimum_common_feature_count(query_size, candidate_size, alpha);
113135

114136
if tau == 0 || tau > query_size {
115-
return FxHashSet::default();
137+
return Vec::new();
116138
}
117139

118140
self.overlap_join(query_features, tau, candidate_size)
119-
.into_iter()
120-
.collect::<FxHashSet<StringId>>()
121-
})
122-
.reduce(FxHashSet::default, |mut acc, set| {
123-
acc.extend(set);
124-
acc
125141
})
142+
.collect();
143+
144+
// Deduplicate - HashSet is efficient for large collections
145+
all_candidates.into_iter().collect()
126146
}
127147

128148
fn overlap_join(
@@ -149,7 +169,15 @@ impl<'db, M: Measure> Searcher<'db, M> {
149169
let mut feature_indices: Vec<usize> = (0..query_features.len()).collect();
150170
feature_indices.sort_unstable_by_key(|&i| feature_sets[i].map_or(usize::MAX, |s| s.len()));
151171

152-
let mut candidate_counts: FxHashMap<StringId, usize> = FxHashMap::default();
172+
// Pre-size HashMap based on smallest feature set to reduce rehashing
173+
let estimated_candidates = feature_sets
174+
.iter()
175+
.filter_map(|s| s.as_ref())
176+
.map(|s| s.len())
177+
.min()
178+
.unwrap_or(16);
179+
let mut candidate_counts: FxHashMap<StringId, usize> =
180+
FxHashMap::with_capacity_and_hasher(estimated_candidates, Default::default());
153181
let mut results = Vec::new();
154182
let q_len = query_features.len();
155183

0 commit comments

Comments
 (0)