finder-wpf / FastSeekWpf /Core /SearchEngine.cs
anshdadhich's picture
Upload FastSeekWpf/Core/SearchEngine.cs
8f85f2c verified
raw
history blame
10.5 kB
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
namespace FastSeekWpf.Core;
public enum ResultKind
{
App, Document, Image, Video, Audio, Archive, Folder, Other
}
public class SearchResult
{
public string FullPath { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public byte Rank { get; set; }
public bool IsDir { get; set; }
public ResultKind Kind { get; set; }
}
public static class SearchEngine
{
[ThreadStatic]
private static StringBuilder? _pathBuilder;
private static StringBuilder GetPathBuilder()
{
var sb = _pathBuilder;
if (sb == null)
{
sb = new StringBuilder(512);
_pathBuilder = sb;
}
sb.Clear();
return sb;
}
private static readonly string[] AppExtensions = { "exe", "lnk", "msi", "appx", "msix" };
private static readonly string[] AppPathMarkers = {
"\\program files\\", "\\program files (x86)\\",
"\\start menu\\", "\\desktop\\", "\\appdata\\"
};
private static readonly Dictionary<string, ResultKind> ExtensionMap = new(StringComparer.OrdinalIgnoreCase)
{
["exe"] = ResultKind.App, ["lnk"] = ResultKind.App, ["msi"] = ResultKind.App,
["doc"] = ResultKind.Document, ["docx"] = ResultKind.Document, ["pdf"] = ResultKind.Document,
["txt"] = ResultKind.Document, ["xlsx"] = ResultKind.Document, ["xls"] = ResultKind.Document,
["pptx"] = ResultKind.Document, ["ppt"] = ResultKind.Document, ["odt"] = ResultKind.Document,
["ods"] = ResultKind.Document, ["odp"] = ResultKind.Document, ["rtf"] = ResultKind.Document,
["md"] = ResultKind.Document, ["csv"] = ResultKind.Document, ["json"] = ResultKind.Document,
["xml"] = ResultKind.Document, ["yaml"] = ResultKind.Document, ["toml"] = ResultKind.Document,
["ini"] = ResultKind.Document, ["log"] = ResultKind.Document,
["png"] = ResultKind.Image, ["jpg"] = ResultKind.Image, ["jpeg"] = ResultKind.Image,
["gif"] = ResultKind.Image, ["bmp"] = ResultKind.Image, ["webp"] = ResultKind.Image,
["svg"] = ResultKind.Image, ["ico"] = ResultKind.Image, ["tiff"] = ResultKind.Image,
["heic"] = ResultKind.Image, ["raw"] = ResultKind.Image, ["psd"] = ResultKind.Image,
["mp4"] = ResultKind.Video, ["mkv"] = ResultKind.Video, ["avi"] = ResultKind.Video,
["mov"] = ResultKind.Video, ["wmv"] = ResultKind.Video, ["flv"] = ResultKind.Video,
["webm"] = ResultKind.Video, ["m4v"] = ResultKind.Video,
["mp3"] = ResultKind.Audio, ["flac"] = ResultKind.Audio, ["wav"] = ResultKind.Audio,
["aac"] = ResultKind.Audio, ["ogg"] = ResultKind.Audio, ["m4a"] = ResultKind.Audio,
["wma"] = ResultKind.Audio, ["opus"] = ResultKind.Audio,
["zip"] = ResultKind.Archive, ["rar"] = ResultKind.Archive, ["7z"] = ResultKind.Archive,
["tar"] = ResultKind.Archive, ["gz"] = ResultKind.Archive, ["bz2"] = ResultKind.Archive,
["xz"] = ResultKind.Archive, ["zst"] = ResultKind.Archive,
};
public static ResultKind GetKind(string? ext, bool isDir, string fullPathLower)
{
if (isDir) return ResultKind.Folder;
if (ext == null) return ResultKind.Other;
if (ExtensionMap.TryGetValue(ext, out var kind))
{
if (kind == ResultKind.App && ContainsAny(fullPathLower, AppPathMarkers))
return ResultKind.App;
return kind;
}
return ResultKind.Other;
}
private static bool ContainsAny(string s, string[] markers)
{
foreach (var m in markers)
if (s.Contains(m, StringComparison.Ordinal)) return true;
return false;
}
private static bool SpanContains(ReadOnlySpan<char> haystack, ReadOnlySpan<char> needle)
{
if (needle.IsEmpty) return true;
if (needle.Length > haystack.Length) return false;
int limit = haystack.Length - needle.Length + 1;
for (int i = 0; i < limit; i++)
{
if (haystack.Slice(i, needle.Length).SequenceEqual(needle))
return true;
}
return false;
}
public static List<SearchResult> Search(
IndexStore store,
string query,
int limit,
bool caseSensitive,
List<string> excludedDirs)
{
if (string.IsNullOrWhiteSpace(query))
return new List<SearchResult>();
var searchSw = System.Diagnostics.Stopwatch.StartNew();
string q = caseSensitive ? query : query.ToLowerInvariant();
ReadOnlySpan<char> qSpan = q.AsSpan();
Span<(int idx, byte rank)> candidates = stackalloc (int, byte)[10000];
int candidateCount = 0;
lock (store)
{
int count = store.Entries.Count;
for (int i = 0; i < count; i++)
{
string name = caseSensitive ? store.NameAt(i) : store.NameLowerAt(i);
ReadOnlySpan<char> nameSpan = name.AsSpan();
byte rank;
if (nameSpan.SequenceEqual(qSpan)) rank = 1;
else if (nameSpan.StartsWith(qSpan, StringComparison.Ordinal)) rank = 2;
else if (SpanContains(nameSpan, qSpan)) rank = 3;
else continue;
if (candidateCount < candidates.Length)
candidates[candidateCount++] = (i, rank);
}
}
candidates[..candidateCount].Sort((a, b) => a.rank.CompareTo(b.rank));
int overshoot = Math.Max(limit * 5, 1000);
if (candidateCount > overshoot)
candidateCount = overshoot;
var results = new List<SearchResult>(Math.Min(limit, candidateCount));
var pathBuilder = GetPathBuilder();
foreach (var (idx, baseRank) in candidates[..candidateCount])
{
var entry = store.Entries[idx];
string fullPath = BuildPathFast(store, entry.FileRef, pathBuilder, entry.DriveIdx);
string fullPathLower = fullPath.ToLowerInvariant();
if (excludedDirs.Count > 0)
{
bool excluded = false;
foreach (var ex in excludedDirs)
{
if (fullPathLower.StartsWith(ex, StringComparison.Ordinal))
{ excluded = true; break; }
}
if (excluded) continue;
}
string name = store.NameAt(idx);
string nameLower = store.NameLowerAt(idx);
byte rank = baseRank;
if (baseRank <= 2)
{
int dotIdx = nameLower.LastIndexOf('.');
if (dotIdx >= 0)
{
string ext = nameLower[(dotIdx + 1)..];
if (Array.IndexOf(AppExtensions, ext) >= 0 && ContainsAny(fullPathLower, AppPathMarkers))
rank = 0;
}
}
string? fileExt = null;
if (!entry.IsDir)
{
int dot = name.LastIndexOf('.');
if (dot >= 0) fileExt = name[(dot + 1)..].ToLowerInvariant();
}
results.Add(new SearchResult
{
FullPath = fullPath,
Name = name,
Rank = rank,
IsDir = entry.IsDir,
Kind = GetKind(fileExt, entry.IsDir, fullPathLower)
});
}
results.Sort((a, b) => a.Rank.CompareTo(b.Rank));
if (results.Count > limit)
results.RemoveRange(limit, results.Count - limit);
searchSw.Stop();
Logger.Log($"Search '{query}': {candidateCount} candidates, {results.Count} results, {searchSw.ElapsedMilliseconds}ms (index={store.Count:N0})");
return results;
}
public static List<SearchResult> SearchByExtension(
IndexStore store,
string ext,
List<string> excludedDirs)
{
string dotExt = "." + ext.ToLowerInvariant();
var results = new List<SearchResult>();
var pathBuilder = GetPathBuilder();
lock (store)
{
for (int i = 0; i < store.Entries.Count; i++)
{
string name = store.NameLowerAt(i);
if (!name.EndsWith(dotExt)) continue;
var entry = store.Entries[i];
string fullPath = BuildPathFast(store, entry.FileRef, pathBuilder, entry.DriveIdx);
string fullPathLower = fullPath.ToLowerInvariant();
bool excluded = false;
foreach (var ex in excludedDirs)
{
if (fullPathLower.StartsWith(ex, StringComparison.Ordinal))
{ excluded = true; break; }
}
if (excluded) continue;
string? fileExt = null;
int dot = name.LastIndexOf('.');
if (dot >= 0) fileExt = name[(dot + 1)..];
results.Add(new SearchResult
{
FullPath = fullPath,
Name = store.NameAt(i),
Rank = 0,
IsDir = entry.IsDir,
Kind = GetKind(fileExt, entry.IsDir, fullPathLower)
});
if (results.Count >= 50) break;
}
}
return results;
}
private static string BuildPathFast(IndexStore store, ulong fileRef, StringBuilder sb, byte startDriveIdx)
{
sb.Clear();
string[] components = new string[64];
int compCount = 0;
ulong current = fileRef;
byte driveIdx = startDriveIdx;
for (int i = 0; i < 64; i++)
{
var idx = store.LookupIdx(current);
if (idx == null) break;
int eIdx = (int)idx;
components[compCount++] = store.NameAt(eIdx);
var entry = store.Entries[eIdx];
driveIdx = entry.DriveIdx;
if (entry.ParentRef == current) break;
current = entry.ParentRef;
}
string driveRoot = driveIdx < store.DriveRoots.Count ? store.DriveRoots[driveIdx] : "C:\\";
sb.Append(driveRoot);
for (int i = compCount - 1; i >= 0; i--)
{
if (sb.Length > 0 && sb[sb.Length - 1] != '\\' && sb[sb.Length - 1] != '/')
sb.Append('\\');
sb.Append(components[i]);
}
return sb.ToString();
}
}