【问题标题】:Live search on millions of records实时搜索数百万条记录
【发布时间】:2021-03-15 08:01:54
【问题描述】:

我需要什么:

考虑 700 万 个此类对象的列表:

public class Item
{
    [Key]
    public int Id { get; set; }

    public List<Person> People { get; set; }

    public string Name { get; set; }
    public string Description { get; set; }
    public string Tags { get; set; }
    public List<Category> Categories { get; set; }
    public float Rating { get; set; }
    public int Yar { get; set; }
    public long Visits { get; set; }
}

public class Person
{
    [Key]
    public int Id { get; set; }
    public string Name { get; set; }
}

public class Category
{
    [Key]
    public int Id { get; set; }
    public string Name { get; set; }
}

我希望能够对所有记录进行实时搜索(“StartsWith”或“Contains”)。通过“实时搜索”,我的意思是当用户键入结果时应该建议(如谷歌)。我希望能够在多个字段或它们的组合上运行。

到目前为止我尝试了什么:

我尝试使用 Linq 直接查询数据库。像这样:

await db.Items.Include(i => i.People).Where(i => i.Name.Contains(query) || i.People.Any(p => p.Name.Contains(query))).ToListAsync();

但是太慢了。然后我尝试将它们缓存在内存中。它大约有 8GB 的​​ RAM,但除了网站启动时非常缓慢的缓存过程和硬维护之外,查询仍然很慢。

缓存是指在网站开始时将这些表上的所有内容加载到内存中,并使用内存数据库上的 Linq 进行查询。

我认为需要的是某种基于树的文档索引。但是,我不知道有什么好的库/方法可以做到这一点。

我应该使用全文搜索引擎吗?我看到网站一直在这样做。

PS:

我使用 MongoDb 来存储数据。以前我有 SQL Server 数据库上的数据,但项目过于复杂,使用非关系数据库大大提高了性能。

【问题讨论】:

标签: c# asp.net linq search full-text-search


【解决方案1】:

您需要某种索引才能使其正常工作。

如果.StartsWith 足够,那么如果有索引,性能应该是可以接受的。我假设.StartsWith 应该被翻译成WHERE Column1 LIKE 'abc%',这应该使用索引,因此可能足够快。至少在 SQL 数据库上。我不确定 MongoDb 如何索引它的数据。

据我所知,.Contains 将无法使用索引,因此需要进行全表扫描,因此性能很差。

替代方法是使用 ElasticSearch 之类的内容或编写自己的搜索结构。

编辑: 另一种选择可能是Lucene.net

【讨论】:

  • ElasticSearch 是否适用于“包含”?如果没有,我可能需要放弃它以支持性能。
  • @alireza noon 我没有使用弹性搜索,但如果不支持“包含”,我会感到惊讶。
【解决方案2】:

所以你有一个字符串,你称之为查询,并且这个字符串随着操作员的打字速度而变化:每当操作员从查询中添加/删除/更改至少一个字符时,你想向这个操作员提供视觉反馈.

这种视觉反馈不必是完整的“项目及其所有人”,尤其是当操作员开始键入时,查询会产生数百个项目。

您唯一需要显示的是操作员决定接下来要输入什么的文本。

只有当操作员提示输入完毕时,才需要执行完整的查询,才能显示完整的结果。

改进输入时的查询

在键入时仅获取操作员需要知道接下来要键入什么的属性。
操作员在打字时是否需要知道 Yar、Rating、Visits 等?还是他只需要知道物品的名称?或者可能是项目的名称 + 所有人的姓名?

在键入时,操作员是否需要查看更多(例如 20 个)结果?
他会在打字时检查这些结果,还是会继续打字直到只有几个结果?

使用实体框架时,Select 通常比获取完整对象更快。
如果您还拥有外键引用的主键,请不要传输您不会使用的项目,例如外键。此外:Select 不会检查数据是否在 DbContext.ChangeTracker 中,也不会放在 ChangeTracter 中的查询之后。

牢记这三个建议,考虑在键入时更改查询: (省略 async-await 部分,不是问题的一部分)

string operatorText = ...
var namesToShow = db.Items.Where(item => item.Name.Contains(operatorText)
                    .Select(item => item.Name)
                    .Take(20)
                    .ToList();
this.ShowVisualFeedBack(namesToShow);

仅当操作员按下回车键或确定按钮时:

string operatorText = ...
var itemsToShow = db.Items
    .Where(item => item.Name.Contains(operatorText
                || item.People.Any(person => ...)
                    .Select(item => item.Name)
                    .ToList();

键入时,“每页”获取数据

如果您认为操作员只想键入几个字符,然后滚动浏览包含数百个结果的列表,请考虑“每页”获取数据,例如每页 20 个项目。

当操作员键入时,您只获取第一页

string operatorText = ...
IQueryable<Items> itemsWithPeople = ... // see improved query above
                  .Take(20).
                  .ToList();

输入一些字符后,操作员决定开始滚动浏览结果。 获取页面[i]:

IQueryable<TSource>FetchPage<TSource>(this IQueryable<TSource> source,
    int pageNr, int pageSize)
{
    return source.Skip(pageNr * pageSize).Take(pageSize);
}

某些数据库管理系统不允许跳过未排序的序列。在这种情况下,在您跳过并采取之前按 ID 或名称排序。

为名称添加额外索引

我对 Mongo 不是很熟悉。您可以添加额外的索引吗? 如果是这样,请考虑为名称编制索引。这将使名称搜索更快。 如果您可以添加单独的表,请创建一个单独的 ItemName 表,并与具有此名称的项目建立一对多关系。

类似于 peopleNames 表。

预取数据

您建议预取一些数据。考虑只预取 Id 和 Names,并将它们放在 LookupTable 中,Names 作为键,Ids 作为值。如果这导致键太多,因为您有很多项目,请考虑仅将名称的前几个字符作为键。

在这种情况下:例如在输入前五个字符时,使用查找表显示大约 20 个符合条件的项目。

【讨论】:

  • 感谢您的详尽回答。是的,MongoDB 有额外的索引。我会调查你的观点。另外,我认为 Jonas 指出的 ElasticSearch 与您的答案相结合可能是一个不错的选择。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-08-18
  • 2019-09-16
  • 1970-01-01
  • 2023-01-31
  • 2011-11-29
  • 2016-03-06
  • 1970-01-01
相关资源
最近更新 更多