Skip to content

Commit 2c061e7

Browse files
committed
(#1340) Add Incremental Prefix Filtering
1 parent cc2755e commit 2c061e7

File tree

15 files changed

+425
-84
lines changed

15 files changed

+425
-84
lines changed

src/Infra/Lanceur.Infra/Services/SearchService.cs

Lines changed: 56 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,12 @@ public sealed class SearchService : ISearchService
1010
{
1111
#region Fields
1212

13-
private Cmdline? _lastQuery;
13+
private Cmdline? _previousQuery;
1414

1515
private readonly ILogger<SearchService> _logger;
1616
private readonly IMacroAliasExpanderService _macroAliasExpanderService;
1717
private readonly ISearchServiceOrchestrator _orchestrator;
18-
private readonly IEnumerable<IStoreService> _storeServices;
18+
private readonly IReadOnlyCollection<IStoreService> _storeServices;
1919

2020
#endregion
2121

@@ -31,7 +31,7 @@ ISearchServiceOrchestrator orchestrator
3131
ArgumentNullException.ThrowIfNull(storeServices);
3232

3333
_storeServices = storeServices.ToList();
34-
if (!_storeServices.Any())
34+
if (_storeServices.Count == 0)
3535
{
3636
throw new ArgumentException("There are no store activated for the search service");
3737
}
@@ -45,46 +45,55 @@ ISearchServiceOrchestrator orchestrator
4545

4646
#region Methods
4747

48-
private async Task DispatchSearchAsync(IList<QueryResult> destination, Cmdline query, bool doesReturnAllIfEmpty)
48+
private bool CanPrune(Cmdline? previousQuery, Cmdline currentQuery)
49+
{
50+
if (previousQuery is null) { return false; }
51+
52+
if (previousQuery.IsEmpty()) { return false; }
53+
54+
return GetAliveStores(currentQuery)
55+
.All(store => store.CanPruneResult(previousQuery, currentQuery));
56+
}
57+
58+
private async Task DispatchSearchAsync(IList<QueryResult> destination, Cmdline currentQuery, bool doesReturnAllIfEmpty)
4959
{
5060
using var measurement = _logger.WarnIfSlow(this);
51-
if (doesReturnAllIfEmpty && query.IsEmpty())
61+
if (doesReturnAllIfEmpty && currentQuery.IsEmpty())
5262
{
5363
destination.ReplaceWith(
5464
await GetAllAsync()
5565
);
5666
return;
5767
}
5868

59-
if (query.IsEmpty())
69+
if (currentQuery.IsEmpty())
6070
{
6171
destination.Clear();
6272
return;
6373
}
64-
65-
if (_lastQuery is not null
66-
&& !_lastQuery.IsEmpty()
67-
&& query.ToString().StartsWith(_lastQuery.ToString(), StringComparison.CurrentCultureIgnoreCase))
74+
75+
if (CanPrune(_previousQuery, currentQuery))
6876
{
6977
_logger.LogTrace(
7078
"Query {NewQuery} refines {OldQuery}; pruning existing results instead of searching stores",
71-
query.Parameters,
72-
_lastQuery.Parameters
79+
currentQuery.ToString(),
80+
_previousQuery?.ToString() ?? "<EMPTY>"
7381
);
74-
PruneResults(destination, query);
82+
83+
//CanPrune guarantees the _previousQuery is not null
84+
PruneResults(destination, _previousQuery!, currentQuery);
7585
return;
7686
}
7787

7888
_logger.LogTrace(
7989
"Execute a full search for query {Query}",
80-
query
90+
currentQuery
8191
);
82-
await SearchInStoreAsync(destination, query);
92+
await SearchInStoreAsync(destination, currentQuery);
8393
}
8494

8595
private IEnumerable<QueryResult> FormatForDisplay(QueryResult[] collection)
8696
{
87-
collection ??= [];
8897
// Upgrade alias to executable macros (if any)
8998
var macros = collection.Length != 0
9099
? _macroAliasExpanderService.Expand(collection).ToList()
@@ -101,36 +110,43 @@ private IEnumerable<QueryResult> FormatForDisplay(QueryResult[] collection)
101110
: DisplayQueryResult.NoResultFound;
102111
}
103112

104-
private void PruneResults(IList<QueryResult> destination, Cmdline query)
113+
private IStoreService[] GetAliveStores(Cmdline query)
114+
=> _storeServices.Where(service => _orchestrator.IsAlive(service, query))
115+
.ToArray();
116+
117+
private void PruneResults(IList<QueryResult> destination, Cmdline previousQuery, Cmdline currentQuery)
105118
{
106-
var toDelete = destination.Where(item =>
107-
!item.Name.StartsWith(query.Parameters, StringComparison.InvariantCultureIgnoreCase)
108-
).ToArray();
119+
var stores = GetAliveStores(currentQuery);
120+
121+
var idle = stores.FirstOrDefault(s => s.StoreOrchestration.IdleOthers);
122+
if (idle is not null)
123+
{
124+
stores = [idle];
125+
}
126+
127+
var deletedCount = stores.Sum(store => store.PruneResult(destination, previousQuery, currentQuery));
109128

110129
_logger.LogTrace(
111-
"Prune {ItemCount} result(s) for {Query}",
112-
toDelete.Length,
113-
query
130+
"Pruned {ItemCount} result(s) for {Query}",
131+
deletedCount,
132+
currentQuery
114133
);
115-
116-
destination.RemoveMultiple(toDelete);
117134
}
118135

119136
private async Task SearchInStoreAsync(IList<QueryResult> destination, Cmdline query)
120137
{
121138
//Get the alive stores
122-
var aliveStores = _storeServices.Where(service => _orchestrator.IsAlive(service, query))
123-
.ToArray();
139+
var aliveStores = GetAliveStores(query);
124140

125-
// I've got a service that stunts all the others, then
141+
// I've got a service that idles all the others, then
126142
// I execute the search for this one only
127143
var tasks = new List<Task<IEnumerable<QueryResult>>>();
128144
if (aliveStores.Any(x => x.StoreOrchestration.IdleOthers))
129145
{
130146
var store = aliveStores.First(x => x.StoreOrchestration.IdleOthers);
131147
tasks.Add(Task.Run(() => store.Search(query)));
132148
}
133-
else // No store that stunt all the other stores, execute aggregated search
149+
else // No store that idles all the other stores, execute aggregated search
134150
{
135151
tasks = aliveStores.Select(store => Task.Run(() => store.Search(query)))
136152
.ToList();
@@ -139,7 +155,7 @@ private async Task SearchInStoreAsync(IList<QueryResult> destination, Cmdline qu
139155
_logger.LogTrace(
140156
"For the query {Query}, {IdleCount} store(s) IDLE and {ActiveCount} store(s) ALIVE",
141157
query,
142-
_storeServices.Count() - tasks.Count,
158+
_storeServices.Count - tasks.Count,
143159
tasks.Count
144160
);
145161

@@ -155,16 +171,17 @@ private async Task SearchInStoreAsync(IList<QueryResult> destination, Cmdline qu
155171

156172
// If there's an exact match, promote
157173
// it to the top of the list.
158-
var match = orderedResults.FirstOrDefault(r => r.Name == query.Name);
174+
var match = orderedResults.FirstOrDefault(r =>
175+
r.Name.Equals(query.Name, StringComparison.InvariantCultureIgnoreCase)
176+
);
177+
159178
if (match is not null) { orderedResults.Move(match, 0); }
160179

161-
if (orderedResults.Count == 0)
162-
{
163-
destination.ReplaceWith(
164-
DisplayQueryResult.SingleFromResult("No result found", iconKind: "AlertCircleOutline")
165-
);
166-
}
167-
else { destination.ReplaceWith(orderedResults); }
180+
destination.ReplaceWith(
181+
orderedResults.Count == 0
182+
? DisplayQueryResult.SingleFromResult("No result found", iconKind: "AlertCircleOutline")
183+
: orderedResults
184+
);
168185
}
169186

170187
/// <inheritdoc />
@@ -184,7 +201,7 @@ public async Task<IEnumerable<QueryResult>> GetAllAsync()
184201
public async Task SearchAsync(IList<QueryResult> destination, Cmdline query, bool doesReturnAllIfEmpty = false)
185202
{
186203
await DispatchSearchAsync(destination, query, doesReturnAllIfEmpty);
187-
_lastQuery = query;
204+
_previousQuery = query;
188205
}
189206

190207
#endregion

src/Infra/Lanceur.Infra/Stores/AdditionalParametersStore.cs

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
using System.Text.RegularExpressions;
2+
using Humanizer;
13
using Lanceur.Core.Configuration;
2-
using Lanceur.Core.Configuration.Sections;
34
using Lanceur.Core.Configuration.Sections.Application;
45
using Lanceur.Core.Constants;
56
using Lanceur.Core.Managers;
@@ -12,7 +13,7 @@
1213

1314
namespace Lanceur.Infra.Stores;
1415

15-
[Store]
16+
[Store("(.*):(.*)")]
1617
public sealed class AdditionalParametersStore : StoreBase, IStoreService
1718
{
1819
#region Fields
@@ -36,28 +37,73 @@ ISection<StoreSection> storeSettings
3637
_aliasService = aliasService;
3738
_logger = logger;
3839
_featureFlags = featureFlags;
40+
41+
ShortcutRegex = new Lazy<Regex>(() => new Regex(Shortcut, RegexOptions.Compiled, 250.Milliseconds()));
3942
}
4043

4144
#endregion
4245

4346
#region Properties
4447

45-
/// <inheritdoc />
46-
public bool IsOverridable => false;
48+
private Lazy<Regex> ShortcutRegex { get; }
49+
50+
/// <inheritdoc cref="IStoreService.IsOverridable" />
51+
public override bool IsOverridable => false;
4752

4853
/// <inheritdoc />
4954
public StoreOrchestration StoreOrchestration
5055
=> _featureFlags.IsEnabled(Features.AdditionalParameterAlwaysActive)
5156
? StoreOrchestrationFactory.SharedAlwaysActive()
52-
: StoreOrchestrationFactory.Shared(".*:.*");
57+
: StoreOrchestrationFactory.Shared(Shortcut);
5358

5459
#endregion
5560

5661
#region Methods
5762

63+
private bool IsRefinementOf(string value, string check)
64+
=> !IsUnfiltered(check)
65+
&& check.StartsWith(value, StringComparison.InvariantCultureIgnoreCase);
66+
67+
private bool IsRefinementOf(QueryResult query, string value)
68+
=> IsRefinementOf(value, query.Name);
69+
70+
71+
private bool IsUnfiltered(Cmdline previous)
72+
{
73+
var splits = ShortcutRegex.Value.Match(previous);
74+
75+
if (!splits.Success) { return false; }
76+
77+
return splits.Groups[2].Value.Length == 0;
78+
}
79+
80+
private static string SelectProperty(Cmdline cmdline) => cmdline.Name;
81+
82+
/// <inheritdoc cref="CanPruneResult" />
83+
public override bool CanPruneResult(Cmdline previous, Cmdline current)
84+
{
85+
if (SelectProperty(current).Length == 0) { return false; }
86+
87+
return OverrideCanPruneResult(
88+
previous,
89+
current,
90+
SelectProperty,
91+
IsRefinementOf
92+
);
93+
}
94+
5895
/// <inheritdoc cref="IStoreService.GetAll" />
5996
public override IEnumerable<QueryResult> GetAll() => _aliasService.GetAllAliasWithAdditionalParameters();
6097

98+
public override int PruneResult(IList<QueryResult> destination, Cmdline previous, Cmdline current)
99+
=> OverridePruneResult(
100+
destination,
101+
previous,
102+
current,
103+
SelectProperty,
104+
IsRefinementOf
105+
);
106+
61107
/// <inheritdoc />
62108
public IEnumerable<QueryResult> Search(Cmdline cmdline)
63109
{

src/Infra/Lanceur.Infra/Stores/AliasStore.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
using Lanceur.Core.Configuration;
2-
using Lanceur.Core.Configuration.Sections;
32
using Lanceur.Core.Configuration.Sections.Application;
43
using Lanceur.Core.Managers;
54
using Lanceur.Core.Models;
@@ -38,8 +37,8 @@ ISection<StoreSection> storeSettings
3837

3938
#region Properties
4039

41-
/// <inheritdoc />
42-
public bool IsOverridable => false;
40+
/// <inheritdoc cref="IStoreService.IsOverridable"/>
41+
public override bool IsOverridable => false;
4342

4443
/// <inheritdoc />
4544
public StoreOrchestration StoreOrchestration => StoreOrchestrationFactory.SharedAlwaysActive();

src/Infra/Lanceur.Infra/Stores/BookmarksStore.cs

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
using System.Web.Bookmarks;
22
using Lanceur.Core.Configuration;
3-
using Lanceur.Core.Configuration.Sections;
43
using Lanceur.Core.Configuration.Sections.Application;
54
using Lanceur.Core.Managers;
65
using Lanceur.Core.Models;
@@ -26,41 +25,72 @@ public BookmarksStore(
2625
IStoreOrchestrationFactory orchestrationFactory,
2726
IBookmarkRepositoryFactory bookmarkRepositoryFactory,
2827
ISection<StoreSection> storeSettings
29-
) : base(orchestrationFactory, storeSettings)
30-
{
28+
) : base(orchestrationFactory, storeSettings) =>
3129
_bookmarkRepositoryFactory = bookmarkRepositoryFactory;
32-
}
3330

3431
#endregion
3532

3633
#region Properties
3734

38-
/// <inheritdoc />
39-
public bool IsOverridable => true;
35+
/// <inheritdoc cref="IStoreService.IsOverridable" />
36+
public override bool IsOverridable => true;
4037

41-
public StoreOrchestration StoreOrchestration => StoreOrchestrationFactory.Exclusive(DefaultShortcut);
38+
public StoreOrchestration StoreOrchestration => StoreOrchestrationFactory.Exclusive(Shortcut);
4239

4340
#endregion
4441

4542
#region Methods
4643

44+
private static bool IsRefinementOf(string value, string check)
45+
=> check.Contains(value, StringComparison.InvariantCultureIgnoreCase);
46+
47+
private static bool IsRefinementOf(QueryResult query, string value)
48+
=> IsRefinementOf(value, query.Name);
49+
50+
private static string SelectProperty(Cmdline cmdline) => cmdline.Parameters;
51+
52+
/// <inheritdoc cref="CanPruneResult" />
53+
public override bool CanPruneResult(Cmdline previous, Cmdline current)
54+
{
55+
if (SelectProperty(previous).Length == 0) { return false; }
56+
57+
return OverrideCanPruneResult(
58+
previous,
59+
current,
60+
SelectProperty,
61+
IsRefinementOf
62+
);
63+
}
64+
65+
/// <inheritdoc cref="PruneResult" />
66+
public override int PruneResult(IList<QueryResult> destination, Cmdline previous, Cmdline current)
67+
=> OverridePruneResult(
68+
destination,
69+
previous,
70+
current,
71+
SelectProperty,
72+
IsRefinementOf
73+
);
74+
4775
/// <inheritdoc />
4876
public IEnumerable<QueryResult> Search(Cmdline cmdline)
4977
{
5078
var bookmarkSourceBrowser = StoreSettings.Value.BookmarkSourceBrowser;
5179
var repository = _bookmarkRepositoryFactory.BuildBookmarkRepository(bookmarkSourceBrowser);
5280

81+
var query = SelectProperty(cmdline);
82+
5383
if (!repository.IsBookmarkSourceAvailable())
5484
{
5585
return DisplayQueryResult.SingleFromResult("The bookmark source is not available!");
5686
}
5787

58-
if (cmdline.Parameters.IsNullOrWhiteSpace())
88+
if (query.IsNullOrWhiteSpace())
5989
{
6090
return DisplayQueryResult.SingleFromResult("Enter text to search in your browser's bookmarks...");
6191
}
6292

63-
return repository.GetBookmarks(cmdline.Parameters)
93+
return repository.GetBookmarks(query)
6494
.Select(e => e.ToAliasQueryResult())
6595
.ToList();
6696
}

0 commit comments

Comments
 (0)