+32
-8
src/search.rs
+32
-8
src/search.rs
···
61
61
/// family-friendly mode: filters out inappropriate content (default true)
62
62
#[serde(default = "default_family_friendly")]
63
63
pub family_friendly: bool,
64
-
/// comma-separated glob patterns to exclude from results (e.g., "*party*,*sad*")
64
+
/// comma-separated regex patterns to exclude from results (e.g., "excited,party")
65
65
#[serde(default)]
66
66
pub exclude: Option<String>,
67
+
/// comma-separated regex patterns to include (overrides exclude)
68
+
#[serde(default)]
69
+
pub include: Option<String>,
67
70
}
68
71
69
72
fn default_top_k() -> usize {
···
102
105
}
103
106
104
107
/// generate etag for caching based on query parameters
105
-
fn generate_etag(query: &str, top_k: usize, alpha: f32, family_friendly: bool, exclude: &Option<String>) -> String {
108
+
fn generate_etag(query: &str, top_k: usize, alpha: f32, family_friendly: bool, exclude: &Option<String>, include: &Option<String>) -> String {
106
109
let mut hasher = DefaultHasher::new();
107
110
query.hash(&mut hasher);
108
111
top_k.hash(&mut hasher);
···
110
113
alpha.to_bits().hash(&mut hasher);
111
114
family_friendly.hash(&mut hasher);
112
115
exclude.hash(&mut hasher);
116
+
include.hash(&mut hasher);
113
117
format!("\"{}\"", hasher.finish())
114
118
}
115
119
···
120
124
alpha: f32,
121
125
family_friendly: bool,
122
126
exclude: Option<String>,
127
+
include: Option<String>,
123
128
config: &Config,
124
129
) -> ActixResult<SearchResponse> {
125
130
// parse and compile exclusion regex patterns from comma-separated string
···
130
135
.map(|p| p.trim())
131
136
.filter(|p| !p.is_empty())
132
137
.filter_map(|p| Regex::new(p).ok()) // silently skip invalid patterns
138
+
.collect()
139
+
})
140
+
.unwrap_or_default();
141
+
142
+
// parse and compile inclusion regex patterns (these override exclusions)
143
+
let include_patterns: Vec<Regex> = include
144
+
.as_ref()
145
+
.map(|s| {
146
+
s.split(',')
147
+
.map(|p| p.trim())
148
+
.filter(|p| !p.is_empty())
149
+
.filter_map(|p| Regex::new(p).ok())
133
150
.collect()
134
151
})
135
152
.unwrap_or_default();
···
365
382
return false;
366
383
}
367
384
368
-
// filter out results matching any exclude regex pattern
369
-
for pattern in &exclude_patterns {
370
-
if pattern.is_match(&result.name) {
371
-
return false;
372
-
}
385
+
// check if result matches any include pattern (allowlist override)
386
+
let matches_include = include_patterns.iter().any(|p| p.is_match(&result.name));
387
+
388
+
// check if result matches any exclude pattern
389
+
let matches_exclude = exclude_patterns.iter().any(|p| p.is_match(&result.name));
390
+
391
+
// keep if: matches include OR doesn't match exclude
392
+
// i.e., include overrides exclude
393
+
if matches_exclude && !matches_include {
394
+
return false;
373
395
}
374
396
375
397
true
···
409
431
query.alpha,
410
432
query.family_friendly,
411
433
query.exclude.clone(),
434
+
query.include.clone(),
412
435
&config
413
436
).await?;
414
437
Ok(HttpResponse::Ok().json(response))
···
421
444
req: HttpRequest,
422
445
) -> ActixResult<HttpResponse> {
423
446
// generate etag for caching
424
-
let etag = generate_etag(&query.query, query.top_k, query.alpha, query.family_friendly, &query.exclude);
447
+
let etag = generate_etag(&query.query, query.top_k, query.alpha, query.family_friendly, &query.exclude, &query.include);
425
448
426
449
// check if client has cached version
427
450
if let Some(if_none_match) = req.headers().get("if-none-match") {
···
438
461
query.alpha,
439
462
query.family_friendly,
440
463
query.exclude.clone(),
464
+
query.include.clone(),
441
465
&config
442
466
).await?;
443
467