【问题标题】:High-Performance Hierarchical Text Search高性能分层文本搜索
【发布时间】:2010-12-31 06:41:53
【问题描述】:

我现在正处于升级主要事务系统中的层次结构设计的最后阶段,并且我已经盯着这个 150 行查询(我将为您省去所有乏味的阅读)和认为必须有更好的方法。

问题的简要总结如下:

您将如何实现分层搜索,以匹配层次结构中不同级别的多个搜索词,并针对最快的搜索时间进行优化?


我找到了somewhat related question,但它实际上只占我实际需要的答案的 20% 左右。这是完整的场景/规范:

  • 最终目标是在层次结构中的任意位置找到一个或多个任意项。
  • 完整的层次结构大约有 80,000 个节点,预计在几年内增长到 1M。
  • 整个层次结构路径的全文是唯一且具有描述性的;但是,单个节点的文本可能不是。这是一个商业现实,而不是一个轻率的决定。
  • 示例:一个节点的名称可能类似于 “门”,它本身没有意义,但完整的上下文,“亚伦 > 房子 > 客厅 > 酒柜 > 门” ,意思很明确,它描述了特定位置的特定门。 (请注意,这只是一个示例,真正的设计远没有那么简单)
  • 为了找到这个特定的门,用户可能会键入“aaron Liquid door”,这可能只会出现一个结果。查询被翻译为一个序列:一个包含文本“door”的项目,在一个包含文本“liquor”的项目下,另一个包含文本“aaron”的项目下。
  • 或者,用户可能只需键入“house Liquid” 来列出人们家中的所有酒柜(这不是很好)。我明确提到这个例子是为了表明搜索不需要匹配任何特定的根或叶级别。该用户确切地知道他在寻找哪扇门,但不记得是谁拥有它,如果名字出现在他面前,他会记得。
  • 所有术语都必须按照指定的顺序匹配,但正如上面的示例所示,层次结构中的级别可以“跳过”。术语“aaron booze cabinet”与此节点匹配。
  • 平台是 SQL Server 2008,但我认为这是一个独立于平台的问题,并且不希望将答案限制在该平台上。
  • 层次结构本身基于hierarchyid(物化路径),索引为广度优先和深度优先。每个层次结构节点/记录都有一个要查询的Name 列。基于节点的层次结构查询非常快,所以不用担心。
  • 没有严格的层次结构 - 一个根可能根本不包含任何节点,也可能包含 30 个子树,这些子树散布到 10,000 个叶节点。
  • 最大嵌套是任意的,但实际上它往往不超过 4-8 层。
  • 层次结构可以而且确实会发生变化,尽管很少发生。任何节点都可以移动到任何其他节点,但有明显的例外(父节点不能移动到它自己的子节点等)
  • 如果这不是已经暗示的:我确实可以控制设计,并且可以添加索引、字段、表,以及获得最佳结果所需的任何内容。

我的“梦想”是向用户提供即时反馈,就像在渐进式搜索/过滤器中一样,但我知道这可能是不可能的或极其困难的。我会对当前方法的任何重大改进感到满意,这通常需要 0.5 秒到 1 秒,具体取决于结果的数量。

为了完整起见,现有查询(存储过程)首先收集包含最终术语的所有叶节点,然后向上连接并排除路径与早期术语不匹配的任何叶节点。如果这对任何人来说都是落后的,请放心,这比从根开始并散开要有效得多。那是“旧”方式,每次搜索很容易花费几秒钟。

所以我的问题又来了:有没有更好(更有效)的方法来执行此搜索?

我不一定要寻找代码,只是寻找方法。我考虑了几种可能性,但它们似乎都有一些问题:

  • 创建一个分隔的“路径文本”列并使用全文搜索对其进行索引。问题是在此列上的搜索也会返回所有子节点; “aaron house” 也匹配 “aaron house kitchen”“aaron house basement”
  • 创建了一个NamePath 列,它实际上是一个嵌套的字符串序列,使用CLR 类型,类似于hierarchyid 本身。问题是,我不知道微软如何能够将这种类型的查询“翻译”为索引操作,我什至不确定是否可以在 UDT 上进行。如果最终结果只是完整的索引扫描,那么我通过这种方法一无所获。

如果我不能做得比我已经拥有的更好,那并不是真正的世界末日;搜索“非常快”,没有人抱怨它。但我敢打赌,以前有人解决过这个问题并且有一些想法。请分享!

【问题讨论】:

  • +1 正确。当前的实现是做什么的?
  • 另外,关卡的最大数量是多少?关卡的数量是固定的吗?
  • 我确实试图解释当前的方法是什么。如果您可以更具体地说明不清楚的地方,我很乐意详细说明。
  • 您将系统描述为“事务性”。这是否意味着层次结构本身会发生变化——删除或修改以及添加节点?此外,它只是叶节点还是可以添加整个路径?叶节点可以从一个分支移动到另一个分支吗?
  • @APC:好问题,答案是肯定的。我在问题中添加了关于此的注释。

标签: database performance database-design search hierarchy


【解决方案1】:

看看 Apache Lucene。您可以使用 Lucene 实现非常灵活且高效的搜索。可能有用。

还请查看搜索模式 - 您所描述的内容可能适合分面搜索模式。

实现一个简单的“Aaron House Living Door”算法非常容易,但不确定基于常规 SVM/分类/熵的算法能否扩展到大型数据集。您可能还想查看 Motwani 和 Raghavan 的“近似搜索”概念。

如果可能的话,请发回你找到的东西:-)

【讨论】:

  • 我查找了“近似搜索”,第一个结果包含文本“NP-hard”——太可怕了! Lucene 看起来很有前途;我需要一段时间才能完成,我仍然更喜欢一个独立的解决方案,但这会带来一些可能性,所以也为你的答案 +1。
【解决方案2】:

嗨亚伦,我有以下想法:
根据您的描述,我脑海中出现了以下图像:

          Aaron
         /     \
        /       \
       /         \
  House           Cars
    |            /    \
Living Room   Ferrari  Mercedes
    |
Liquor Cabinet
    /    |    \
Table   Door   Window

这就是您的搜索树的样子。现在我将对每个级别的节点进行排序:

               Aaron
              /     \
             /       \
            /         \
         Cars         House
         /   \       /
        /     \     /
       /       \   /
      /         \ /
     /           X
    /           / \
   /           /   \
  /           /     \
 /           /       \
|           /         \
|          /           \ 
Ferrari   Living Room   Mercedes
                        |
                  Liquor Cabinet
                   /    |    \
               Door   Table   Window

现在处理查询应该很容易和快速:

  1. 从查询中的最后一个单词和最低节点级别(叶子)开始
  2. 由于所有节点都在一个级别内排序,因此您可以使用二进制搜索,因此在 O(log N) 时间内找到匹配项,其中 N 是节点数。
  3. 为每个级别执行此操作。树中有 O(log N) 个级别。
  4. 找到匹配项后,处理所有父节点以查看路径是否与您的查询匹配。路径的长度为 O(log N)。如果匹配,则将其存储在结果中,该结果应显示给用户。

设 M 为总匹配数(匹配查询中最后一个单词的节点数)。那么我们的处理时间是:O( (log N)^2 + M * (log N) ):
二进制搜索每个级别需要 O(log N) 时间,并且有 O(log N) 个级别,因此我们必须花费至少 O( (log N)^2 ) 时间。现在,对于每个匹配,我们必须测试从匹配节点到根的完整路径是否与完整查询匹配。路径的长度为 O(log N)。因此,给定 M 个整体匹配,我们又花费了 M * O(log N) 时间,因此执行时间为 O( (log N)^2 + M * (log N) )。

当您的匹配项很少时,处理时间接近 O( (log N)^2 ),这非常好。相反,如果发生最坏的情况(每条路径都匹配查询(M = N)),处理时间接近 O(N log N),虽然不太好,但也不太可能。

实施: 你说,你只想要一个想法。另外我对数据库的了解也很有限,这里就不多写了,简单说一下思路。
节点表可能如下所示:

  • ID : 整数
  • 文本:字符串
  • 父级:int -> 节点 ID
  • Level : int //我不希望这个变化太频繁,所以你可以保存并更新它,随着数据库的变化。

此表必须按“文本”列排序。使用上述算法,循环内的 sql 查询可能如下所示:
SELECT ID FROM node WHERE Level = $i AND Text LIKE $text
希望有人能明白我的意思。

不仅可以通过“文本”列对表格进行排序,还可以通过“文本”和“级别”列的组合来对表格进行排序,也就是说,Level=20 内的所有条目都已排序,所有条目在 Level=19 内排序等(但没有必要对整个表格进行整体排序)。但是,PER LEVEL 的节点数为 O(N),因此没有渐近运行时改进,但考虑到您在现实中得到的较低常量,我认为值得尝试。

编辑:改进

我刚刚注意到,迭代算法是完全没有必要的(因此可以放弃关卡信息)。完全足够:

  1. 存储按文本值排序的所有节点
  2. 使用对所有节点的二分搜索一次查找查询最后一个单词的所有匹配项。
  3. 从每个匹配项开始,跟踪到根的路径并检查该路径是否与整个查询匹配。

这将运行时间提高到 O(log N + M * (log N))。

【讨论】:

  • 我一定会对此进行调查,并且 +1 以获得精心编写和深思熟虑的答案。我不确定它在实践中的效果如何,因为叶节点的选择性最低(可能有 50,000 个“门”),这必须与子树一一处理,而不是逐步过滤放。不过还是个好主意!
  • 感谢亚伦的反馈。我已经编辑了这篇文章,并认为你应该看看它。
  • @Dave - 改进版本与问题中描述的现有方法有什么区别?尽管无论哪种方式,Big-O 分析都是有价值的见解。
  • 在第二个版本中,您只对完整的节点集进行 1 次二分搜索,而不是在树中的每一层进行一次搜索。从我的角度来看,这使得编码算法变得更加容易,并且提高了运行时间。
  • + 当我读到您的问题时,这两种方法似乎都非常相似。我一开始并没有想到这一点,因为您没有就您的解决方案提供太多细节。好吧,如果它们真的相等(您可以更好地判断),那么我认为没有比您现在更好的解决方案了:-P
猜你喜欢
  • 1970-01-01
  • 2013-09-01
  • 1970-01-01
  • 2012-01-23
  • 2015-03-29
  • 1970-01-01
  • 2021-11-22
  • 2017-07-16
  • 1970-01-01
相关资源
最近更新 更多