anshdadhich's picture
initial commit
1c4658f
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;
}
}