我正在创建一个具有搜索功能的(C#)工具。搜索类似于“去任何地方”搜索(如ReSharper或VS2013)。
搜索上下文是一个字符串数组,包含前面的所有项目:
private string[] context; // contains thousands of elements
搜索是增量搜索,与用户提供的每个新输入(字符)一起发生。
我使用LINQ Where扩展方法实现了搜索:
// User searched for "c"
var input = "c";
var results = context.Where(s => s.Contains(input));
当用户搜索“ca”时,我试图将之前的结果用作搜索上下文,但是这会导致(我认为?)嵌套的Where迭代,并且运行得不好。想想这样的代码:
// Cache these results.
var results = var results = context.Where(s => s.Contains(input));
// Next search uses the previous search results
var newResults = results.Where(s => s.Contains(input));
有没有办法优化这种情况?
每次搜索都会将IEnumerable转换为数组,导致内存分配过高,运行效果不佳。
答案 0 :(得分:1)
向用户呈现数千个搜索结果是非常没用的。在将结果呈现给用户之前,您应该在查询中添加“top”(linq Take
)语句。
var results = context.Where(s => s.Contains(input)).Take(100);
如果您想向用户展示接下来的100个结果:
var results = context.Where(s => s.Contains(input)).Skip(100).Take(100);
同样只使用原始数组进行所有搜索,不使用嵌套Where
,因为除非您实现查询,否则它没有任何好处。
答案 1 :(得分:1)
我有几个有用的要点要添加,太多的评论。
首先,我同意您应该从.take(100)
开始的其他评论,减少加载时间。更好的是,当时添加一个结果:
var results = context.Where(s => s.Contains(input));
var resultEnumerator = result.GetEnumerator()
遍历resultEnumerator以显示一个结果,当屏幕已满或启动新搜索时停止。
其次,限制你的输入。如果用户撰写Hello
,则您不想针对H
,He
,Hel
,Hell
和Hello
进行5次搜索,你想只搜索Hello
。当用户稍后添加world
时,可能值得采用旧结果并将Hello world
添加到where子句中。
results = results.Where(s => s.Contains(input));
resultEnumerator = result.GetEnumerator()
当然,当用户添加新文本时,取消当前正在进行的结果。
使用Rx,节流部分很简单,你可以得到这样的结果:
var result = context.AsEnumerable();
var oldStr = "";
var resultEnumerator = result.GetEnumerator();
Observable.FromEventPattern(h => txt.TextChanged += h, h => txt.TextChanged -= h)
.Select(s => txt.Text)
.DistinctUntilChanged().Throttle(TimeSpan.FromMilliseconds(300))
.Subscribe(s =>
{
if (s.Contains(oldStr))
result = result.Where(t => t.Contains(s));
else
result = context.Where(t => t.Contains(s));
resultEnumerator = result.GetEnumerator();
oldStr = s;
// and probably start iterating resultEnumerator again,
// but perhaps not on this thread.
});
答案 2 :(得分:0)
如果您关注的是alloc并且您不想编写trie实现或使用第三方代码,那么您应该先连续分区您的上下文数组,以便在前面聚集匹配的条目。不是LINQ-ish,但速度快,内存成本为零。
分区扩展方法,基于C ++' std::partition
/// <summary>
/// All elements for which predicate is true are moved to the front of the array.
/// </summary>
/// <param name="start">Index to start with</param>
/// <param name="end">Index to end with</param>
/// <param name="predicate"></param>
/// <returns>Index of the first element for which predicate returns false</returns>
static int Partition<T>(this T[] array, int start, int end, Predicate<T> predicate)
{
while (start != end)
{
// move start to the first not-matching element
while ( predicate(array[start]) )
{
if ( ++start == end )
{
return start;
}
}
// move end to the last matching element
do
{
if (--end == start)
{
return start;
}
}
while (!predicate(array[end]));
// swap the two
var temp = array[start];
array[start] = array[end];
array[end] = temp;
++start;
}
return start;
}
所以现在你需要存储最后一个分区索引,它应该用context
长度初始化:
private int resultsCount = context.Length;
然后,对于输入的每个更改,您可以运行增量:
resultsCount = context.Partition(0, resultsCount, s => s.Contains(input));
每次这只会检查以前没有被过滤掉的元素,这正是你所追求的。
对于每次非增量更改,您需要将resultsCount
重置为原始值。
您可以使用方便的调试器和LINQ友好方式公开结果:
public IEnumerable<string> Matches
{
get { return context.Take(resultsCount); }
}