anshdadhich commited on
Commit
7914da1
·
verified ·
1 Parent(s): 776e190

SearchEngine: Exact match to Rust — use IndexStore.Name()/NameLower()/BuildPath() arena accessors, remove parallel/Spar optimization that diverged from Rust. Single-threaded matching identical to Rust behavior.

Browse files
Files changed (1) hide show
  1. FastSeekWpf/Core/SearchEngine.cs +60 -137
FastSeekWpf/Core/SearchEngine.cs CHANGED
@@ -22,21 +22,6 @@ public class SearchResult
22
 
23
  public static class SearchEngine
24
  {
25
- [ThreadStatic]
26
- private static StringBuilder? _pathBuilder;
27
-
28
- private static StringBuilder GetPathBuilder()
29
- {
30
- var sb = _pathBuilder;
31
- if (sb == null)
32
- {
33
- sb = new StringBuilder(512);
34
- _pathBuilder = sb;
35
- }
36
- sb.Clear();
37
- return sb;
38
- }
39
-
40
  private static readonly string[] AppExtensions = { "exe", "lnk", "msi", "appx", "msix" };
41
  private static readonly string[] AppPathMarkers = {
42
  "\\program files\\", "\\program files (x86)\\",
@@ -88,20 +73,7 @@ public static class SearchEngine
88
  return false;
89
  }
90
 
91
- private static bool SpanContains(ReadOnlySpan<char> haystack, ReadOnlySpan<char> needle)
92
- {
93
- if (needle.IsEmpty) return true;
94
- if (needle.Length > haystack.Length) return false;
95
-
96
- int limit = haystack.Length - needle.Length + 1;
97
- for (int i = 0; i < limit; i++)
98
- {
99
- if (haystack.Slice(i, needle.Length).SequenceEqual(needle))
100
- return true;
101
- }
102
- return false;
103
- }
104
-
105
  public static List<SearchResult> Search(
106
  IndexStore store,
107
  string query,
@@ -109,72 +81,60 @@ public static class SearchEngine
109
  bool caseSensitive,
110
  List<string> excludedDirs)
111
  {
112
- if (string.IsNullOrWhiteSpace(query))
113
  return new List<SearchResult>();
114
 
115
- var searchSw = System.Diagnostics.Stopwatch.StartNew();
116
  string q = caseSensitive ? query : query.ToLowerInvariant();
117
- ReadOnlySpan<char> qSpan = q.AsSpan();
118
 
119
- Span<(int idx, byte rank)> candidates = stackalloc (int, byte)[10000];
120
- int candidateCount = 0;
121
 
122
- lock (store)
123
  {
124
- int count = store.Entries.Count;
125
- for (int i = 0; i < count; i++)
126
- {
127
- string name = caseSensitive ? store.NameAt(i) : store.NameLowerAt(i);
128
- ReadOnlySpan<char> nameSpan = name.AsSpan();
129
 
130
- byte rank;
131
- if (nameSpan.SequenceEqual(qSpan)) rank = 1;
132
- else if (nameSpan.StartsWith(qSpan, StringComparison.Ordinal)) rank = 2;
133
- else if (SpanContains(nameSpan, qSpan)) rank = 3;
134
- else continue;
135
 
136
- if (candidateCount < candidates.Length)
137
- candidates[candidateCount++] = (i, rank);
138
- }
139
  }
140
 
141
- candidates[..candidateCount].Sort((a, b) => a.rank.CompareTo(b.rank));
142
-
143
  int overshoot = Math.Max(limit * 5, 1000);
144
- if (candidateCount > overshoot)
145
- candidateCount = overshoot;
146
 
147
- var results = new List<SearchResult>(Math.Min(limit, candidateCount));
148
- var pathBuilder = GetPathBuilder();
149
 
150
- foreach (var (idx, baseRank) in candidates[..candidateCount])
151
  {
152
  var entry = store.Entries[idx];
153
- string fullPath = BuildPathFast(store, entry.FileRef, pathBuilder, entry.DriveIdx);
154
- string fullPathLower = fullPath.ToLowerInvariant();
155
 
156
  if (excludedDirs.Count > 0)
157
  {
158
- bool excluded = false;
159
- foreach (var ex in excludedDirs)
160
- {
161
- if (fullPathLower.StartsWith(ex, StringComparison.Ordinal))
162
- { excluded = true; break; }
163
- }
164
- if (excluded) continue;
165
  }
166
 
167
- string name = store.NameAt(idx);
168
- string nameLower = store.NameLowerAt(idx);
169
-
170
  byte rank = baseRank;
171
  if (baseRank <= 2)
172
  {
173
- int dotIdx = nameLower.LastIndexOf('.');
174
- if (dotIdx >= 0)
175
  {
176
- string ext = nameLower[(dotIdx + 1)..];
177
- if (Array.IndexOf(AppExtensions, ext) >= 0 && ContainsAny(fullPathLower, AppPathMarkers))
 
 
178
  rank = 0;
179
  }
180
  }
@@ -182,6 +142,7 @@ public static class SearchEngine
182
  string? fileExt = null;
183
  if (!entry.IsDir)
184
  {
 
185
  int dot = name.LastIndexOf('.');
186
  if (dot >= 0) fileExt = name[(dot + 1)..].ToLowerInvariant();
187
  }
@@ -189,10 +150,10 @@ public static class SearchEngine
189
  results.Add(new SearchResult
190
  {
191
  FullPath = fullPath,
192
- Name = name,
193
  Rank = rank,
194
  IsDir = entry.IsDir,
195
- Kind = GetKind(fileExt, entry.IsDir, fullPathLower)
196
  });
197
  }
198
 
@@ -200,12 +161,10 @@ public static class SearchEngine
200
  if (results.Count > limit)
201
  results.RemoveRange(limit, results.Count - limit);
202
 
203
- searchSw.Stop();
204
- Logger.Log($"Search '{query}': {candidateCount} candidates, {results.Count} results, {searchSw.ElapsedMilliseconds}ms (index={store.Count:N0})");
205
-
206
  return results;
207
  }
208
 
 
209
  public static List<SearchResult> SearchByExtension(
210
  IndexStore store,
211
  string ext,
@@ -213,76 +172,40 @@ public static class SearchEngine
213
  {
214
  string dotExt = "." + ext.ToLowerInvariant();
215
  var results = new List<SearchResult>();
216
- var pathBuilder = GetPathBuilder();
217
 
218
- lock (store)
219
  {
220
- for (int i = 0; i < store.Entries.Count; i++)
221
- {
222
- string name = store.NameLowerAt(i);
223
- if (!name.EndsWith(dotExt)) continue;
224
-
225
- var entry = store.Entries[i];
226
- string fullPath = BuildPathFast(store, entry.FileRef, pathBuilder, entry.DriveIdx);
227
- string fullPathLower = fullPath.ToLowerInvariant();
228
 
229
- bool excluded = false;
230
- foreach (var ex in excludedDirs)
231
- {
232
- if (fullPathLower.StartsWith(ex, StringComparison.Ordinal))
233
- { excluded = true; break; }
234
- }
235
- if (excluded) continue;
236
-
237
- string? fileExt = null;
238
- int dot = name.LastIndexOf('.');
239
- if (dot >= 0) fileExt = name[(dot + 1)..];
240
-
241
- results.Add(new SearchResult
242
- {
243
- FullPath = fullPath,
244
- Name = store.NameAt(i),
245
- Rank = 0,
246
- IsDir = entry.IsDir,
247
- Kind = GetKind(fileExt, entry.IsDir, fullPathLower)
248
- });
249
 
250
- if (results.Count >= 50) break;
 
 
 
 
251
  }
252
- }
253
-
254
- return results;
255
- }
256
 
257
- private static string BuildPathFast(IndexStore store, ulong fileRef, StringBuilder sb, byte startDriveIdx)
258
- {
259
- sb.Clear();
260
- string[] components = new string[64];
261
- int compCount = 0;
262
- ulong current = fileRef;
263
- byte driveIdx = startDriveIdx;
264
 
265
- for (int i = 0; i < 64; i++)
266
- {
267
- var idx = store.LookupIdx(current);
268
- if (idx == null) break;
 
 
 
 
269
 
270
- int eIdx = (int)idx;
271
- components[compCount++] = store.NameAt(eIdx);
272
- var entry = store.Entries[eIdx];
273
- driveIdx = entry.DriveIdx;
274
- if (entry.ParentRef == current) break;
275
- current = entry.ParentRef;
276
  }
277
 
278
- string driveRoot = driveIdx < store.DriveRoots.Count ? store.DriveRoots[driveIdx] : "C:\\";
279
- sb.Append(driveRoot);
280
- for (int i = compCount - 1; i >= 0; i--)
281
- {
282
- if (sb.Length > 0 && sb[sb.Length - 1] != '\\' && sb[sb.Length - 1] != '/')
283
- sb.Append('\\');
284
- sb.Append(components[i]);
285
- }
286
- return sb.ToString();
287
  }
288
  }
 
22
 
23
  public static class SearchEngine
24
  {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  private static readonly string[] AppExtensions = { "exe", "lnk", "msi", "appx", "msix" };
26
  private static readonly string[] AppPathMarkers = {
27
  "\\program files\\", "\\program files (x86)\\",
 
73
  return false;
74
  }
75
 
76
+ // Search matches Rust search() exactly
 
 
 
 
 
 
 
 
 
 
 
 
 
77
  public static List<SearchResult> Search(
78
  IndexStore store,
79
  string query,
 
81
  bool caseSensitive,
82
  List<string> excludedDirs)
83
  {
84
+ if (string.IsNullOrEmpty(query))
85
  return new List<SearchResult>();
86
 
 
87
  string q = caseSensitive ? query : query.ToLowerInvariant();
 
88
 
89
+ // Phase 1: lightweight name-only matching
90
+ var candidates = new List<(int idx, byte rank)>();
91
 
92
+ for (int i = 0; i < store.Entries.Count; i++)
93
  {
94
+ var entry = store.Entries[i];
95
+ string nameCmp = caseSensitive ? store.Name(entry) : store.NameLower(entry);
 
 
 
96
 
97
+ byte rank;
98
+ if (nameCmp == q) rank = 1;
99
+ else if (nameCmp.StartsWith(q, StringComparison.Ordinal)) rank = 2;
100
+ else if (nameCmp.Contains(q, StringComparison.Ordinal)) rank = 3;
101
+ else continue;
102
 
103
+ candidates.Add((i, rank));
 
 
104
  }
105
 
106
+ // Phase 2: sort by rank, keep overshoot buffer
107
+ candidates.Sort((a, b) => a.rank.CompareTo(b.rank));
108
  int overshoot = Math.Max(limit * 5, 1000);
109
+ if (candidates.Count > overshoot)
110
+ candidates.RemoveRange(overshoot, candidates.Count - overshoot);
111
 
112
+ // Phase 3: build paths + exclusions + app promotion
113
+ var results = new List<SearchResult>(limit);
114
 
115
+ foreach (var (idx, baseRank) in candidates)
116
  {
117
  var entry = store.Entries[idx];
118
+ string fullPath = store.BuildPath(entry.FileRef);
 
119
 
120
  if (excludedDirs.Count > 0)
121
  {
122
+ string pathLower = fullPath.ToLowerInvariant();
123
+ if (excludedDirs.Any(ex => pathLower.StartsWith(ex, StringComparison.Ordinal)))
124
+ continue;
 
 
 
 
125
  }
126
 
127
+ string nameLower = store.NameLower(entry);
 
 
128
  byte rank = baseRank;
129
  if (baseRank <= 2)
130
  {
131
+ string? ext = nameLower.Contains('.') ? nameLower[(nameLower.LastIndexOf('.') + 1)..] : null;
132
+ if (ext != null && Array.IndexOf(AppExtensions, ext) >= 0)
133
  {
134
+ string pathLower = fullPath.ToLowerInvariant();
135
+ if (!ContainsAny(pathLower, AppPathMarkers))
136
+ rank = baseRank;
137
+ else
138
  rank = 0;
139
  }
140
  }
 
142
  string? fileExt = null;
143
  if (!entry.IsDir)
144
  {
145
+ string name = store.Name(entry);
146
  int dot = name.LastIndexOf('.');
147
  if (dot >= 0) fileExt = name[(dot + 1)..].ToLowerInvariant();
148
  }
 
150
  results.Add(new SearchResult
151
  {
152
  FullPath = fullPath,
153
+ Name = store.Name(entry),
154
  Rank = rank,
155
  IsDir = entry.IsDir,
156
+ Kind = GetKind(fileExt, entry.IsDir, fullPath.ToLowerInvariant())
157
  });
158
  }
159
 
 
161
  if (results.Count > limit)
162
  results.RemoveRange(limit, results.Count - limit);
163
 
 
 
 
164
  return results;
165
  }
166
 
167
+ // Extension search — matches Rust extension filtering in main.rs
168
  public static List<SearchResult> SearchByExtension(
169
  IndexStore store,
170
  string ext,
 
172
  {
173
  string dotExt = "." + ext.ToLowerInvariant();
174
  var results = new List<SearchResult>();
 
175
 
176
+ for (int i = 0; i < store.Entries.Count; i++)
177
  {
178
+ var entry = store.Entries[i];
179
+ string name = store.NameLower(entry);
180
+ if (!name.EndsWith(dotExt)) continue;
 
 
 
 
 
181
 
182
+ string fullPath = store.BuildPath(entry.FileRef);
183
+ string fullPathLower = fullPath.ToLowerInvariant();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
184
 
185
+ bool excluded = false;
186
+ foreach (var ex in excludedDirs)
187
+ {
188
+ if (fullPathLower.StartsWith(ex, StringComparison.Ordinal))
189
+ { excluded = true; break; }
190
  }
191
+ if (excluded) continue;
 
 
 
192
 
193
+ string? fileExt = null;
194
+ int dot = name.LastIndexOf('.');
195
+ if (dot >= 0) fileExt = name[(dot + 1)..];
 
 
 
 
196
 
197
+ results.Add(new SearchResult
198
+ {
199
+ FullPath = fullPath,
200
+ Name = store.Name(entry),
201
+ Rank = 0,
202
+ IsDir = entry.IsDir,
203
+ Kind = GetKind(fileExt, entry.IsDir, fullPathLower)
204
+ });
205
 
206
+ if (results.Count >= 50) break;
 
 
 
 
 
207
  }
208
 
209
+ return results;
 
 
 
 
 
 
 
 
210
  }
211
  }