使用具有字符串属性的对象更快地搜索集合

时间:2015-11-21 05:03:38

标签: c# performance linq

我有以下

public class SearchResult
{
    public string Description { get; set; }
    public int Year{ get; set; }
    public int Type { get; set; }
}

我创建了这些List的列表并缓存它然后我尝试使用以下

搜索此集合(1.2M)记录
var found = myList.Where(x => x.Description.StartsWith(query)).Take(10).ToList();

这比我想要的要慢,是否有更好的方法来存储对象列表并且能够搜索对象的字符串属性?

我应该在缓存之前对集合进行排序吗? 我希望能够在描述属性上执行.StartsWith和.Contains,并获得前10名匹配的最快路径。

如果我更快地访问数据库(我已经在文本字段上添加了一个索引),我希望通过获取结果一次,将它们粘贴在内存中然后针对缓存完成所有搜索来提高我的性能在内存中每次都要进入数据库。但事实证明,使用SQL LIKE' {query}%'声明

3 个答案:

答案 0 :(得分:1)

字符串比较本质上很慢,另外你必须整个迭代整个列表以查看是否匹配。这永远不会表现良好,事实上随着时间的推移,随着新记录被添加到源中,这种情况很可能会变得更糟。

Here 是一篇很好的文章,关于字符串搜索与速度有关的人。

我建议您按照提到的方式进行操作,将搜索移动到数据库并限制返回的行数。虽然这仍然是I / O,但数据库已针对处理此类事情进行了优化。其他一些优点是你最终没有让你的应用程序崩溃和丢失缓存搜索的陷阱,同样你可以利用async/await这将使你的应用程序更具响应性。

如果您决定继续将所有内容都拉入内存然后查询对象,那么祝你好运。我唯一的另一个建议就是考虑搜索缓存,这样如果有人在最​​近搜索相同的东西 - 你可以缓存这些结果并立即返回它们。

来自同一作者,这是另一个阅读来源 - 在这里他比较了集合字符串查找速度。

http://cc.davelozinski.com/c-sharp/fastest-collection-for-string-lookups

答案 1 :(得分:1)

首先 - 它是一个information retrieval task,似乎没有有效的方法只使用LINQ。

你需要的是某种reverse index。在您的情况下,非常简单的反向索引实现是Dictionary<string, List<SearchResult>>。对于电影标题中的每个单词,此Dictionary包含所有电影,哪个标题包含该单词。要构建这个简单的反向索引,您可以使用以下代码:

        reverseIndex = new Dictionary<string, List<SearchResult>> ();
        for (int i = 0; i < searchResults.Count; i++) {
            var res = searchResults[i];
            string[] words = res.Description.Split (new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
            foreach (var word in words) {
                if (!reverseIndex.ContainsKey (word))
                    reverseIndex [word] = new List<SearchResult> () { res };
                else if (!reverseIndex[word].Contains(res))
                    reverseIndex [word].Add (res);              
            }
        }

现在,而不是慢:

searchResults.Where(x => x.Description.Contains(query));

你可以使用简单的:

reverseIndex[query];

它的工作速度非常快。而不是

searchResults.Where(x => x.Description.StartsWith(query));

你可以使用:

reverseIndex[query].Where(s => s.Description.StartsWith(query));

如果您的查询包含多个单词,则可以将其拆分为单词,然后为每个单词提取List<SearchResult>,然后交叉列表。

通过反向索引的这种简单实现,您的查询只能包含整个单词。如果您想通过部分字词进行搜索,则需要使用permuterm index。关于C#的一种可能的实现,你可以找到here。注意,permuterm索引需要大量额外的内存。

答案 2 :(得分:1)

快速字符串前缀搜索最好使用trie数据结构。关于trie的酷炫之处在于,任何给定节点的所有后代都具有与该节点关联的字符串的公共前缀。它也可以压缩成radix tree,实现起来可能稍微复杂一些。

现在您正在使用Linq-to-objects为每次搜索遍历整个列表(并且每个StartsWith方法都是O(m)m是{{1}的长度1}}字符串)。如果您使用Linq-to-SQL,它将被转换为SQL查询,该查询将使用索引来执行更有效的查找。

This link使用trie实现了自动完成功能的示例。

(适用更新)

正如@David在评论中提到的,如果你已经将这些数据加载到一个列表中(也就是说,如果你需要将它保存在这种形式中,你可能会这样做),那么trie可能是一种过度杀伤力。在这种情况下,query查询的更好选择是对列表进行排序。这样您就可以使用二进制搜索在StartsWith中获得结果。

根据数据是否经常更改,您还可以使用平衡二叉树来存储数据,以便快速插入/删除(这基本上是SortedDictionary给您的)。

但最终,如果您还需要O(m log n)个查询,那么您需要在索引上节省更多内存(如@Igor in his answer所描述的那样),或者只是让您的DMBS执行此操作(建议使用@David)。

另一方面,您可能希望尝试使用SQL Server的全文搜索。或者,如果您愿意在编写SQL之外,Lucene.Net使用内存中缓存应该为您提供even better performance