【问题标题】:How to optimize "text search" for inverted index and relational database? [closed]如何优化倒排索引和关系数据库的“文本搜索”? [关闭]
【发布时间】:2023-03-23 16:52:01
【问题描述】:

2020 年 2 月 18 日更新

我再次遇到了这个问题,虽然接受的答案保持不变,但我想分享我现在如何优化它(再次不使用第三方库或工具 - 即从头开始重新发明轮子,就像在原始问题)。

为了简化和优化这个系统,我会在域逻辑层上使用 Trie (Prefix tree) 而不是 "inverted index maps" 并完全丢弃“表查询” SQL 表的不良做法。我举个例子来说明:

  • 假设应用程序的一些用户已经将几个对象添加到数据库中:w、wo、woo、wood 和 woodx
  • 那些对象(字符串/标签)将由内存中的 Trie 表示,每个 Trie 节点将包含该对象在树中所在级别的数据库颁发的 ID(想想 关联数组)。
  • 当用户查询一个词时,我们在 Trie 中搜索该词并在途中累积所有相关 ID,从搜索到的词开始向下移动(从那里依次遍历 )。我们从这些 ID 中检索所有需要的对象数据(无论是来自缓存还是数据库)。

这里有一张图片来说明:

  • 接下来,如果用户向数据库中添加了一个新词,例如“woodxe”,则 Trie 会相应更新。
  • 当用户查询“woodx”时,会发生与之前相同的过程,并且会累积一个新 ID(“woodxe”的颁发 ID)
  • 英语词典中有一个以特定字母序列开头的有限单词列表,因此向下移动并获取所有子节点仍然是一个复杂度为 O(1) 的有限过程。例如,如果您在 Trie 上以“wood”开头,则在英语词典中以“wood”为前缀的子节点列表是一个有限常数。是否将所有这些子节点返回给用户、定义限制(延迟加载/分页)或仅显示前 10 个命中,都是个人架构偏好。

这是一张图片来说明(检查绿色添加的内容)

  • 当用户的查询是多字串时,例如:“木家具”,每个字都会被单独解析/添加到 Trie,每个字都会有一个相应的匹配 ID 列表。

*Trie* 如何改进之前的架构?

  • “表查询”很麻烦、不好的做法,而且开销与数据库成正比增长;现已移除。
  • 我们拥有的“倒排索引映射”产生了额外的内存开销,并且无法通过新词轻松扩展(如上面的“woodx”示例)。有人可能会争辩说,查询一个 Hashmap 是 O(1),但是在内存中有几个大的 hashmap 实际上会在一定程度上减慢速度,并且被认为是糟糕的工程设计。
  • trie 的搜索复杂度为 O(m),其中 m 是提供的字母表中的字符数。 由于用户查询的是纯英文字母的单词,因此最大的子树将等于可用的最大英文单词(常数,即 O(1))。此外,如前所述,英语词典中以定义的单词前缀开头的子节点的数量也是一个常数,因此遍历所有组合是 O(1)。所以总的来说这是一个 O(1) 操作。
    • 所以查询 Trie = Get key from Hashmap = O(1) 一样快。
    • 除此之外,在该系统中使用 trie 的好处是:
      • 与在内存中运行多个倒排索引哈希映射相比,内存开销更小
      • 集中查询树
      • 易于扩展,将新词添加到数据库中只需要在内存中的现有 Trie 中添加几个新节点。即,不再有数据库增长和搜索查询数量增加的问题(可扩展性噩梦)。

2015 年 10 月 15 日更新

早在 2012 年,我正在构建一个个人在线应用程序,实际上我想重新发明轮子,因为我的天性很好奇,用于学习目的并提高我的算法和架构技能。我本来可以使用 apache lucene 和其他的,但是正如我所提到的,我决定构建自己的迷你搜索引擎。

问题:那么,除了使用可用的服务(如 elasticsearch、lucene 等)之外,真的没有其他方法可以增强这种架构吗?


原始问题

我正在开发一个网络应用程序,用户在其中搜索特定的标题(例如:书 x、书 y 等),这些数据位于关系数据库 (MySQL) 中。

我遵循的原则是从数据库中获取的每条记录都缓存在内存中,这样应用程序对数据库的调用就更少了。

我开发了自己的迷你搜索引擎,架构如下:

它是这样工作的:

  • a) 用户搜索记录名称
  • b) 系统检查查询以什么字符开头,检查是否有查询:获取记录。如果不存在,则添加它并使用两种方式从数据库中获取所有匹配记录:
    • “查询”表(一种历史记录表)中已经存在任何查询,因此可以根据 ID 获取记录(快速性能)
    • 或者,否则使用 Mysql LIKE %% 语句 来获取记录/ID(然后将用户使用过的查询连同它映射到的 ID 一起保存在历史表查询中)。
      -->然后它将记录及其 id 添加到 缓存 并且仅将 id 添加到倒排索引映射中。
  • c) 结果返回到 UI

系统运行良好,但是我有 两个 主要问题,我找不到一个好的解决方案(过去一个月一直在尝试):

第一期:
如果您检查点 (b) ,没有找到查询“历史”并且它必须使用 Like %% 语句的情况:这个过程在查询时变得 time匹配数据库中的大量记录(而不是一两条):

  • 从 Mysql 获取记录需要一些时间(这就是我在特定列上使用 INDEXES 的原因)
  • 那么是时候保存查询历史了
  • 然后是时候将记录/ID 添加到缓存和倒排索引映射中了

第二期:
该应用程序允许用户自己添加新记录,这些新记录可以立即被登录到应用程序的其他用户使用。
然而,为了实现这一点,必须更新倒排索引映射和表“查询”,以便在任何旧查询与新词匹配的情况下。例如,如果添加了 new 记录“woodX”,旧查询“wood”仍然会映射到它。因此,为了将查询“wood”重新挂钩到这个新记录,这就是我现在正在做的事情:

  • 新记录“woodX”被添加到“记录”表中
  • 然后我运行 Like %% 语句来查看表“查询”中的哪个 已存在 查询确实映射到该记录(例如“木头”),然后将此查询与新记录 ID 添加为新行:[wood, new id]。
  • 然后在内存中,更新倒排索引 Map 的“wood”键的值(即列表),将新的记录 Id 添加到此列表中

--> 因此,现在如果远程用户搜索“wood”,它将从 内存 中获取:wood 和 woodX

这里的问题也是时间消耗。将所有查询历史(在表查询中)与新添加的单词匹配需要很长时间(匹配的查询越多,时间越长)。那么内存中的更新也需要很多时间。

解决这个问题的方法是先将所需的结果返回给用户 ,然后让应用程序发布一个 ajax 调用所需的数据来完成所有这些 UPDATE 任务。但我不确定这是一种不好的做法还是一种不专业的做事方式?
所以在过去的一个月里(再多一点),我试图为这个架构考虑最好的优化/修改/更新,但我不是文档检索领域的专家(实际上它是我构建的第一个迷你搜索引擎)。

我将不胜感激任何关于我应该做些什么才能实现这种架构的反馈或指导。
提前致谢。

PS:

  • 它是一个使用 servlet 的 j2ee 应用程序。
  • 我正在使用 MySQL innodb(因此我无法使用全文搜索选项)

【问题讨论】:

  • 搜索历史(基本上是您缓存的所有内容)是由用户限定的,还是整个应用程序都一样? IE。用户 2 是否能够找到缓存对象,因为用户 1 已经在寻找相同的键?
  • @theDmi 是的,它是一个共享缓存(如单例)

标签: algorithm architecture full-text-search search-engine inverted-index


【解决方案1】:

我强烈推荐 Sphinx 搜索服务器,它在全文搜索中得到了最佳优化。访问http://sphinxsearch.com/

它旨在与 MySQL 一起使用,因此它是您当前工作空间的补充。

【讨论】:

【解决方案2】:

我不假装有解决方案,但这是我的想法。 首先,我喜欢你的耗时查询 LIKE%% :我会在 MySQL 中执行一个仅限于几个答案的查询,比如十几个,然后将其返回给用户,然后等待用户是否想要更多匹配的记录,或者启动在后台进行完整查询,具体取决于您将来搜索的索引需求。

更一般地说,我认为将所有内容存储在内存中可能会在某一天导致过多的内存消耗。尽管搜索引擎在将所有内容保存在内存中时会变得越来越快,但您必须在添加或更新数据时使所有这些缓存保持最新,这肯定会花费越来越多的时间。

这就是为什么我认为我一天在“开源论坛软件”(我不记得它的名字)中看到的解决方案对于帖子中的文本搜索来说还不错:每次插入数据时,都会出现一个表格名为“Words”的单词跟踪每个现有单词,另一个表(比如说“WordsLinks”)记录每个单词与其出现的帖子之间的链接。 这种解决方案有一些缺点:

  • 数据库中的每次插入、删除、更新都会慢很多
  • 必须预见到搜索引擎的数据选择:如果您选择保留从未保留的两个字母单词,那么对于已记录的数据来说已经太迟了,除非您启动完整的数据重新处理。
  • 您必须注意 DELETE 以及 UPDATE 和 INSERT

但我认为有一些很大的优势:

  • 计算时间可能与“内存解决方案”(最终)相同,但它是在每个数据库的 Create/Update/Delete 中划分的,而不是在查询时。
  • 查找整个单词或“以”开头的单词是瞬时的:索引时,在“单词”表中搜索是二分法的。并且“WordLinks”表查询速度非常快,无论是使用索引。
  • 同时查找多个单词可能很简单:为每个找到的单词收集一组“WordLinks”,并对它们执行交集以仅保留所有这些组共有的“数据库 ID”。例如单词“tree”和“leaf”,第一个可以给出表格记录{1,4,6},第二个可以给出{1,3,6,9}。因此,对于交集,只保留公共部分很简单:{1, 6}。
  • 单列表中的“Like %%”可能比不同表的不同字段中的许多“Like %%”要快。每个数据库引擎都会处理一些缓存:“Words”表可能足够小,可以保存在内存中
  • 我认为,如果数据变得庞大,性能和内存问题的风险很小。
  • 由于每次搜索都很快,您甚至可以查找同义词。例如,如果用户没有找到任何与“以太网”相关的内容,则搜索“网络”。
  • 您可以应用规则,例如拆分驼峰式单词以从“woodX”生成例如 3 个单词“wood”、“X”、“woodX”。每个“单词”都非常便于存储和查找,因此您可以做很多事情。

我认为您需要的解决方案可能是多种方法的混合:例如,您可以保持轻量级的 UPDATE、INSERT、DELETE,并从 TRIGGER 启动“Words”和“WordsLinks”馈送。

只是为了轶事,我看到我的公司开发了一个软件,它决定将“一切”(!)保留在内存中。它使我们向客户推荐购买具有 64GB RAM 的服务器。有点贵。它解释了为什么当我看到最终可能导致内存填充的解决方案时,我会非常谨慎。

【讨论】:

    【解决方案3】:

    我不得不说,我认为你的设计不太适合这个问题。你现在看到的问题就是这样的后果。除此之外,您当前的解决方案无法扩展。

    这是一个可能的解决方案:

    1. 重新设计您的数据库,使其仅包含权威数据,但不包含派生数据。所以所有缓存条目都必须从 MySQL 中消失。

    2. 仅在应用程序内存中的请求期间保留数据。这使您的应用程序设计更加简单(想想竞争条件),并使您能够扩展到合理数量的客户端。

    3. 引入缓存层。我强烈建议使用已建立的产品,而不是自己构建。这可以让您摆脱应用程序中所有自定义构建的缓存逻辑,甚至可以更好地完成这项工作。

    您可以查看 Redis 或 Memcached 的缓存层。我认为 LRU 策略应该适合这里。根据您的查询变得多么复杂,像 Lucene 这样的专用索引搜索机制也可能有意义。

    【讨论】:

    • for 1-当我重新启动服务器以重新获取已经存在的查询条件时,MySQL 中的缓存条目就在那里。 2- 重点是在内存中的共享查询数据可供应用程序的所有用户实时使用。 3- 正确,但是这是在 2012 年完成的,对于一个我实际上想重新发明轮子来学习的个人项目(应该提到这一点),但你是 100% 正确的。
    【解决方案4】:

    我确信这可以在 MySQL 中实现,但仅使用现有的面向搜索的数据库(例如 Elasticsearch)会少很多努力。它使用 Lucene 库来实现倒排索引,拥有丰富的文档,支持水平缩放,相当简单的查询语言等等。我想要做到这一点已经做了很多工作,处理缓存、竞争条件、错误、性能问题等将是更多的工作,以使解决方案成为“生产级”。

    【讨论】:

    • 我本可以在 2012 年使用 apache lucene,但是出于好奇和学习的目的,我想为个人在线项目构建自己的引擎。这如何在 MySQL 中实现?
    • 重新发明的东西总是很有教育意义:)我很难理解这个问题的细节,但是通过适当的标记化,我想你只需要 WHERE pattern LIKE "quer%' 类型的查询(前缀匹配)比任意的WHERE pattern LIKE "%quer%' 查询更有效(假设健全的 MySQL 索引,应该检查文档)。我认为内存方面在这里并不那么重要,重点是 MySQL(或任何一个 db)有一个合适的索引来满足您的查询。在数据库中缓存查询和结果似乎太复杂了。
    猜你喜欢
    • 1970-01-01
    • 2013-02-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-04-23
    • 2012-05-12
    • 2010-09-17
    相关资源
    最近更新 更多