【发布时间】: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 添加到倒排索引映射中。
- “查询”表(一种历史记录表)中已经存在任何查询,因此可以根据 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