| using FastSeek.Core.Index; |
|
|
| namespace FastSeek.Core.Search; |
|
|
| public sealed class SearchResult |
| { |
| public required string FullPath { get; init; } |
| public required string Name { get; init; } |
| public byte Rank { get; init; } |
| public bool IsDir { get; init; } |
| } |
|
|
| public static class SearchEngine |
| { |
| private static readonly HashSet<string> AppExtensions = new(StringComparer.OrdinalIgnoreCase) { "exe", "lnk", "msi", "appx", "msix" }; |
| private static readonly string[] AppPathMarkers = ["\\program files\\", "\\program files (x86)\\", "\\start menu\\", "\\desktop\\", "\\appdata\\"]; |
|
|
| public static List<SearchResult> Search(IndexStore store, string query, int limit, bool caseSensitive, IReadOnlyList<string> excludedDirs) |
| { |
| if (string.IsNullOrEmpty(query)) return []; |
| var q = caseSensitive ? query : query.ToLowerInvariant(); |
|
|
| var bag = new System.Collections.Concurrent.ConcurrentBag<(int idx, byte rank)>(); |
| Parallel.For(0, store.Entries.Count, i => |
| { |
| var e = store.Entries[i]; |
| var nameCmp = caseSensitive ? e.Name : e.NameLower; |
| byte rank; |
| if (nameCmp == q) rank = 1; |
| else if (nameCmp.StartsWith(q, StringComparison.Ordinal)) rank = 2; |
| else if (nameCmp.Contains(q, StringComparison.Ordinal)) rank = 3; |
| else return; |
| bag.Add((i, rank)); |
| }); |
|
|
| var candidates = bag.ToList(); |
| candidates.Sort(static (a, b) => a.rank.CompareTo(b.rank)); |
| if (candidates.Count > Math.Max(limit * 5, 1000)) candidates.RemoveRange(Math.Max(limit * 5, 1000), candidates.Count - Math.Max(limit * 5, 1000)); |
|
|
| var results = new List<SearchResult>(limit); |
| foreach (var (idx, baseRank) in candidates) |
| { |
| var entry = store.Entries[idx]; |
| var path = BuildPath(entry.FileRef, store); |
| if (excludedDirs.Count > 0) |
| { |
| var lowerPath = path.ToLowerInvariant(); |
| var excluded = false; |
| for (var i = 0; i < excludedDirs.Count; i++) |
| { |
| if (lowerPath.StartsWith(excludedDirs[i], StringComparison.Ordinal)) { excluded = true; break; } |
| } |
| if (excluded) continue; |
| } |
|
|
| var rank = baseRank; |
| if (baseRank <= 2) |
| { |
| var extIdx = entry.NameLower.LastIndexOf('.'); |
| if (extIdx >= 0 && extIdx + 1 < entry.NameLower.Length) |
| { |
| var ext = entry.NameLower[(extIdx + 1)..]; |
| if (AppExtensions.Contains(ext)) |
| { |
| var pathLower = path.ToLowerInvariant(); |
| for (var i = 0; i < AppPathMarkers.Length; i++) |
| { |
| if (pathLower.Contains(AppPathMarkers[i], StringComparison.Ordinal)) { rank = 0; break; } |
| } |
| } |
| } |
| } |
|
|
| results.Add(new SearchResult { FullPath = path, Name = entry.Name, Rank = rank, IsDir = entry.IsDir }); |
| } |
|
|
| results.Sort(static (a, b) => a.Rank.CompareTo(b.Rank)); |
| if (results.Count > limit) results.RemoveRange(limit, results.Count - limit); |
| return results; |
| } |
|
|
| public static string BuildPath(ulong fileRef, IndexStore store) |
| { |
| var components = new List<string>(16); |
| var current = fileRef; |
| for (var i = 0; i < 64; i++) |
| { |
| var idx = store.LookupIdx(current); |
| if (idx is null) break; |
| var e = store.Entries[idx.Value]; |
| components.Add(e.Name); |
| if (e.ParentRef == current) break; |
| current = e.ParentRef; |
| } |
|
|
| components.Reverse(); |
| if (components.Count == 0) return store.DriveRoot; |
|
|
| var path = store.DriveRoot; |
| for (var i = 0; i < components.Count; i++) path = Path.Combine(path, components[i]); |
| return path; |
| } |
| } |
|
|