Upload src/index/search.rs
Browse files- src/index/search.rs +129 -0
src/index/search.rs
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
use rayon::prelude::*;
|
| 2 |
+
use crate::index::store::IndexStore;
|
| 3 |
+
|
| 4 |
+
const APP_EXTENSIONS: &[&str] = &["exe", "lnk", "msi", "appx", "msix"];
|
| 5 |
+
const APP_PATH_MARKERS: &[&str] = &[
|
| 6 |
+
"\\program files\\", "\\program files (x86)\\",
|
| 7 |
+
"\\start menu\\", "\\desktop\\", "\\appdata\\",
|
| 8 |
+
];
|
| 9 |
+
|
| 10 |
+
#[derive(Debug, Clone)]
|
| 11 |
+
pub struct SearchResult {
|
| 12 |
+
pub full_path: std::path::PathBuf,
|
| 13 |
+
pub name: String,
|
| 14 |
+
pub rank: u8,
|
| 15 |
+
pub is_dir: bool,
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
pub fn search(
|
| 19 |
+
store: &IndexStore,
|
| 20 |
+
query: &str,
|
| 21 |
+
limit: usize,
|
| 22 |
+
case_sensitive: bool,
|
| 23 |
+
excluded_dirs: &[String],
|
| 24 |
+
) -> Vec<SearchResult> {
|
| 25 |
+
if query.is_empty() {
|
| 26 |
+
return Vec::new();
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
let q = if case_sensitive { query.to_string() } else { query.to_lowercase() };
|
| 30 |
+
|
| 31 |
+
// ββ Phase 1: lightweight name-only matching ββββββββββββββββββββββββββ
|
| 32 |
+
let entries = &store.entries;
|
| 33 |
+
let name_lower_arena = &store.name_lower_arena;
|
| 34 |
+
let name_arena = &store.name_arena;
|
| 35 |
+
|
| 36 |
+
let mut candidates: Vec<(u32, u8)> = entries
|
| 37 |
+
.par_iter()
|
| 38 |
+
.enumerate()
|
| 39 |
+
.filter_map(|(idx, entry)| {
|
| 40 |
+
let name_cmp = if case_sensitive {
|
| 41 |
+
unsafe { std::str::from_utf8_unchecked(&name_arena[entry.name_off as usize..(entry.name_off as usize + entry.name_len as usize)]) }
|
| 42 |
+
} else {
|
| 43 |
+
unsafe { std::str::from_utf8_unchecked(&name_lower_arena[entry.name_lower_off as usize..(entry.name_lower_off as usize + entry.name_lower_len as usize)]) }
|
| 44 |
+
};
|
| 45 |
+
|
| 46 |
+
let rank = if name_cmp == q { 1u8 }
|
| 47 |
+
else if name_cmp.starts_with(&q) { 2 }
|
| 48 |
+
else if name_cmp.contains(q.as_str()) { 3 }
|
| 49 |
+
else { return None; };
|
| 50 |
+
|
| 51 |
+
Some((idx as u32, rank))
|
| 52 |
+
})
|
| 53 |
+
.collect();
|
| 54 |
+
|
| 55 |
+
// ββ Phase 2: sort by rank, keep overshoot buffer βββββββββββββββββ
|
| 56 |
+
candidates.sort_unstable_by_key(|&(_, rank)| rank);
|
| 57 |
+
let overshoot = (limit * 5).max(1000);
|
| 58 |
+
candidates.truncate(overshoot);
|
| 59 |
+
|
| 60 |
+
// ββ Phase 3: build paths + exclusions + app promotion ββββββββββββ
|
| 61 |
+
let mut results: Vec<SearchResult> = Vec::with_capacity(limit);
|
| 62 |
+
|
| 63 |
+
for &(idx, base_rank) in &candidates {
|
| 64 |
+
let entry = &entries[idx as usize];
|
| 65 |
+
let full_path = build_path(entry.file_ref, store);
|
| 66 |
+
|
| 67 |
+
if !excluded_dirs.is_empty() {
|
| 68 |
+
let path_lower = full_path.to_string_lossy().to_lowercase();
|
| 69 |
+
if excluded_dirs.iter().any(|ex| path_lower.starts_with(ex.as_str())) {
|
| 70 |
+
continue;
|
| 71 |
+
}
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
let name_lower = store.name_lower(entry);
|
| 75 |
+
let rank = if base_rank <= 2 {
|
| 76 |
+
let ext_is_app = name_lower
|
| 77 |
+
.rsplit('.')
|
| 78 |
+
.next()
|
| 79 |
+
.map(|e| APP_EXTENSIONS.contains(&e))
|
| 80 |
+
.unwrap_or(false);
|
| 81 |
+
if ext_is_app {
|
| 82 |
+
let path_lower = full_path.to_string_lossy().to_lowercase();
|
| 83 |
+
if APP_PATH_MARKERS.iter().any(|m| path_lower.contains(m)) { 0 } else { base_rank }
|
| 84 |
+
} else {
|
| 85 |
+
base_rank
|
| 86 |
+
}
|
| 87 |
+
} else {
|
| 88 |
+
base_rank
|
| 89 |
+
};
|
| 90 |
+
|
| 91 |
+
results.push(SearchResult {
|
| 92 |
+
full_path,
|
| 93 |
+
name: store.name(entry).to_string(),
|
| 94 |
+
rank,
|
| 95 |
+
is_dir: entry.is_dir(),
|
| 96 |
+
});
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
results.sort_unstable_by_key(|r| r.rank);
|
| 100 |
+
results.truncate(limit);
|
| 101 |
+
results
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
/// Iterative path builder β walks parent chain via sorted ref_lookup.
|
| 105 |
+
pub fn build_path(file_ref: u64, store: &IndexStore) -> std::path::PathBuf {
|
| 106 |
+
let mut components: Vec<&str> = Vec::with_capacity(16);
|
| 107 |
+
let mut current = file_ref;
|
| 108 |
+
|
| 109 |
+
for _ in 0..64 {
|
| 110 |
+
match store.lookup_idx(current) {
|
| 111 |
+
Some(idx) => {
|
| 112 |
+
let entry = &store.entries[idx as usize];
|
| 113 |
+
components.push(store.name(entry));
|
| 114 |
+
if entry.parent_ref == current {
|
| 115 |
+
break;
|
| 116 |
+
}
|
| 117 |
+
current = entry.parent_ref;
|
| 118 |
+
}
|
| 119 |
+
None => break,
|
| 120 |
+
}
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
components.reverse();
|
| 124 |
+
let mut path = std::path::PathBuf::from(&store.drive_root);
|
| 125 |
+
for comp in components {
|
| 126 |
+
path.push(comp);
|
| 127 |
+
}
|
| 128 |
+
path
|
| 129 |
+
}
|