2020-02-18 更新
我再次遇到了这个问题,虽然接受的答案保持不变,但我想分享我现在如何优化它(再次不使用第三方库或工具 - 即从头开始重新发明轮子,就像原始问题中提到的那样) .
为了简化和优化这个系统,我将在域逻辑层上使用Trie(前缀树)而不是“倒排索引映射”,并完全摒弃“表查询” SQL 表的不良做法。我将举例说明:
- 假设该应用程序的一些用户已经在数据库中添加了几个对象:w、wo、woo、wood 和 woodx。
- 这些对象(字符串/标签)将由内存中的 Trie 表示,每个 Trie 节点将包含该对象在树中所在级别的数据库发布 ID(Think Associative Array)。
- 当用户查询一个词时,我们在 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 中添加几个新节点。即,不再有数据库增长和搜索查询数量增加的问题(可扩展性噩梦)。
2015-10-15 更新
早在 2012 年,我正在构建一个个人在线应用程序,实际上我想重新发明轮子,因为我天生好奇,为了学习目的并提高我的算法和架构技能。我本可以使用 apache lucene 和其他的,但是正如我所提到的,我决定构建自己的迷你搜索引擎。
问题:那么,除了使用可用的服务(如 elasticsearch、lucene 等)之外,真的没有办法增强这种架构吗?
原始问题
我正在开发一个 Web 应用程序,用户在其中搜索特定的标题(例如:book x、book y 等),这些数据位于关系数据库 (MySQL) 中。
我遵循的原则是,从数据库中获取的每条记录都缓存在内存中,这样应用程序对数据库的调用就更少了。
我开发了自己的迷你搜索引擎,具有以下架构:
这是它的工作原理:
- a) 用户搜索记录名称
- b)系统检查查询以什么字符开头,检查是否有查询:获取记录。如果不存在,则添加它并使用两种方式从数据库中获取所有匹配记录:
- 表“查询”(这是一种历史表)中已经存在的任一查询,因此根据 ID 获取记录(快速性能)
- 或者,否则使用Mysql LIKE %% 语句来获取记录/ID(然后将用户使用的查询连同它映射到的 ID 一起保存在历史表查询中)。
-->然后它将记录及其ID添加到 缓存中,并且仅将ID添加到倒排索引映射中。
- 表“查询”(这是一种历史表)中已经存在的任一查询,因此根据 ID 获取记录(快速性能)
- c) 结果返回到 UI
该系统工作正常,但是我有两个主要问题,我找不到一个好的解决方案(过去一个月一直在尝试):
第一个问题:
如果您检查点 (b) ,没有找到查询“历史”并且它必须使用Like %%语句的情况:当查询匹配数据库中的大量记录(而不是一个或二):
- 从 Mysql 获取记录需要一些时间(这就是我在特定列上使用 INDEXES 的原因)
- 然后是时候保存查询历史了
- 然后是时候向缓存和倒排索引图添加记录/ID
第二个问题:
应用程序允许用户自己添加新记录,其他登录到应用程序的用户可以立即使用这些记录。
然而,为了实现这一点,必须更新倒排索引映射和表“查询”,以便在任何旧查询与新词匹配的情况下。例如,如果要添加新记录“woodX”,旧查询“wood”仍然会映射到它。因此,为了将查询“wood”重新连接到这个新记录,这就是我现在正在做的事情:
- 新记录“woodX”被添加到“记录”表中
- 然后我运行一个Like %%语句来查看表“查询”中哪个已经存在的查询映射到该记录(例如“wood”),然后将此查询与新记录 id 添加为新行: [ wood, new ID]。
- 然后在内存中,更新倒排索引 Map 的“wood”键的值(即列表),将新的记录 ID 添加到这个列表中
--> 因此,现在如果远程用户搜索“wood”,它将从内存中获取:wood 和 woodX
这里的问题也是时间消耗。将所有查询历史(在表查询中)与新添加的单词匹配需要很长时间(匹配的查询越多,时间越长)。那么内存中的更新也需要很多时间。
我正在考虑解决这个时间问题,首先将所需的结果返回给用户,然后让应用程序使用所需的数据发布一个ajax调用以实现所有这些更新任务。但我不确定这是一种不好的做法还是一种不专业的做事方式?
所以在过去的一个月里(再多一点),我试图为这个架构考虑最好的优化/修改/更新,但我不是文档检索领域的专家(实际上它是我构建的第一个迷你搜索引擎)。
对于我应该做些什么来实现这种架构,我将不胜感激任何反馈或指导。
提前致谢。
PS:
- 它是一个使用 servlet 的 j2ee 应用程序。
- 我正在使用 MySQL innodb(因此我不能使用全文搜索选项)