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 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 Search(IndexStore store, string query, int limit, bool caseSensitive, IReadOnlyList 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(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(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; } }