如何使用Lucene.NET来帮助实现类似Stack Overflow网站的搜索功能?

62
我在Meta Stack Overflow上提出了类似的问题,但那个问题专门涉及到Stack Overflow是否使用Lucene.NET
这里的问题更多是假设性的,即如果将Lucene.NET作为站内搜索和其他因素的基础(如Stack Overflow [SO]),则应该采取哪些方法。
根据Stack Overflow博客上的“SQL 2008 Full-Text Search Problems”文章,有强烈的迹象表明曾经考虑过使用Lucene.NET,但根据Geoff Dalgas在2010年2月19日的评论,情况显然不是这样的:

Stack Overflow没有使用Lucene.NET——我们使用SQL Server全文索引。搜索是我们继续进行微小调整的领域。

所以我的问题是,如何将Lucene.NET应用于具有与Stack Overflow相同语义的站点?
以下是一些背景信息和我迄今为止所做/思考的内容(是的,我已经实现了大部分内容,搜索是我要完成的最后一个方面): 技术:

当然,主角是Lucene.NET。

我们的意图也是尽快转移到.NET/C# 4.0。虽然我认为这不会改变游戏规则,但值得注意。

在深入了解Lucene.NET的方面之前,重要的是指出它与SQL Server 2008以及涉及的模型有关。

模型

与Stack Overflow相比,该系统具有多个主要模型类型。其中一些模型的示例包括:

  • 问题:这些是人们可以提出的问题。人们可以回答问题,就像在Stack Overflow上一样。
  • 注释:这些是单向投影,因此与问题不同,您正在对内容进行陈述。人们无法发布回复。
  • 事件:这是有关实时事件的数据。它具有位置信息、日期/时间信息。

需要注意这些模型的重要事项:

  • 所有的模型都有一个名称/标题(文本)属性和一个正文(HTML)属性(格式不重要,因为内容将被适当解析以进行分析)。
  • 每个模型实例在网站上都有一个唯一的URL。

然后,Stack Overflow提供了一些在我看来是模型的装饰器。这些装饰器可以具有不同的基数,可以是一对一或一对多:

  • 投票:以用户为键
  • 回复:可选的,例如,请参见上面的注释案例
  • 被收藏:该模型是否列为用户的收藏?
  • 评论:(可选)
  • 标签关联:标签位于单独的表中,因此不会为每个模型复制标签。模型与标签关联表之间存在链接,然后从标签关联表到标签表。

还有支持计数,它们本身是与它们相同方式键入的模型的一对一装饰器:

  • 投票计数:总正面、负面投票,威尔逊得分区间(这很重要,它将根据条目的投票确定置信水平,在很大程度上,假设威尔逊区间的下限)。

回复(答案)是大多数模型具有的大部分装饰器,它们只是没有标题或网址,以及一个模型是否有回复是可选的。如果允许回复,那当然是一对多的关系。

SQL Server 2008

表格基本上遵循上面模型的布局,具有单独的装饰器表格,以及一些支持表格和视图、存储过程等。

应该注意的是,不使用全文搜索的决定主要基于它不能像Lucene.NET那样规范化得分。我愿意听取如何利用基于文本的搜索的建议,但我将不得不在多个模型类型之间执行搜索,因此请记住我需要以某种方式规范化得分。

Lucene.NET

这里是一个大问号。以下是我对Stack Overflow功能的想法,以及我已经做了什么。
索引
我认为每个模型都应该有自己的索引,包含唯一的ID,以便根据该ID的Term实例(索引而非分析)快速查找它。
在这个领域,我考虑让Lucene.NET分别分析每个问题/模型和每个回答。因此,如果有一个问题和五个答案,那么问题和每个答案将分别作为一个单元进行索引。
这里的想法是Lucene.NET返回的相关性得分更容易比较不同方式投影的模型之间的得分(例如,没有回复的内容)。
例如,一个问题设置主题,然后答案详细阐述主题。
对于笔记,它处理呈现主题然后详细阐述主题的问题,而没有回复。
我相信这将有助于使相关性得分更加相关。
标签
最初,我认为这些应该保留在单独的索引中,具有将IDs放入适当模型索引的多个字段。或者,如果太大,就有一个只包含标签的索引和另一个索引来维护标签索引与它们应用于的问题之间的关系。这样,当您单击标签(或使用URL结构)时,可以很容易地逐步查看只有在成功时才需要“购买”的内容:
如果标签存在
与标签相关的问题
问题本身

然而,在实践中,基于标签进行所有项目的查询(例如在Stack Overflow中点击标签)使用SQL Server 2008非常容易。根据上述模型,它只需要一个查询,如下:

select
     m.Name, m.Body
from
    Models as m
        left outer join TagAssociations as ta on
            ta.ModelTypeId = <fixed model type id> and
            ta.ModelId = m.Id
        left outer join Tags as t on t.Id = ta.TagId
where
    t.Name = <tag>

由于某些属性在所有模型之间共享,因此很容易在不同的模型类型/表之间执行UNION并生成一致的结果集。

这类似于Lucene.NET中的TermQuery(我引用Java文档,因为它很全面,而且Lucene.NET是Lucene的逐行翻译,所以所有文档都是相同的)。

在使用Lucene.NET时出现的问题是排序顺序。对于标签的TermQuery的相关性得分无关紧要。它要么是1,要么是0(它要么有它,要么没有它)。

此时,置信度得分(Wilson得分区间)就会影响结果的排序。

这个得分可以存储在Lucene.NET中,但是为了按此字段对结果进行排序,它将依赖于值存储在字段缓存中,这是我真正想避免的事情。对于大量文档,字段缓存可能会变得非常大(Wilson得分是一个double,您需要为每个文档一个double,这可能是一个大数组)。

鉴于我可以更改SQL语句以按Wilson得分区间排序,如下:

select
     m.Name, m.Body
from
    Models as m
        left outer join TagAssociations as ta on
            ta.ModelTypeId = <fixed model type id> and
            ta.ModelId = m.Id
        left outer join Tags as t on t.Id = ta.TagId
        left outer join VoteTallyStatistics as s on
            s.ModelTypeId = ta.ModelTypeId and
            s.ModelId = ta.ModelId
where
    t.Name = <tag>
order by
    --- Use Id to break ties.
    s.WilsonIntervalLowerBound desc, m.Id

看起来使用这个处理“获取所有标记为<tag>的项目”的Stack Overflow功能似乎是一个简单的选择。

回复

最初,我认为这是一个独立的索引,其中包含指向问题索引的关键字。

我认为应该结合每个模型和每个回复(如果有的话),以便在比较不同模型的相关性分数时更加“平等”。

当然,这会使索引变得臃肿。 我现在对此感到有些舒适。

或者,是否有一种方法可以将模型和回复作为Lucene.NET中的单独文档存储,然后将两者合并,并能够将两个文档视为一个来获取查询的相关性分数? 如果是这样,那么这将是理想的

当然,还有一个问题,即存储哪些字段,索引哪些字段,分析哪些字段(所有操作都可以是单独的操作,也可以混合匹配)? 需要建立多少索引?

使用特殊的词干提取器/波特算法来纠正拼写错误(使用Metaphone)以及同义词(社区中有术语,我将为其提供服务,其中某些术语对于某些事物具有自己的俚语/术语,这些术语有多个表示形式)。

提升

这当然与索引有关,但我认为它值得单独成一节。

您是否正在提升字段和/或文档? 如果是这样,您如何提升它们? 对于某些字段,提升是否保持不变? 还是重新计算适用于投票/查看/收藏/外部数据的字段的提升。

例如,在文档中,标题是否比正文更有优势?如果是的话,您认为哪些提升因素效果好?标签呢?
这里的思路与Stack Overflow类似。文档中的术语具有相关性,但如果文档标记了该术语或者它在标题中,则应进行提升。 Shashikant Kore建议使用以下文档结构:
- 标题 - 问题 - 已接受答案(如果没有已接受答案,则为高票答案) - 所有答案合并
然后使用提升,但不基于原始投票值。我相信用Wilson Score区间可以解决这个问题。
问题是,应该将提升应用于整个文档吗?我倾向于不这样做,因为这意味着每次用户对模型进行投票时,我都必须重新索引文档。
搜索标记项
最初,我认为查询标记(通过特定点击标记或使用URL结构查找标记内容)时,只需针对标记索引执行简单的TermQuery,然后在关联索引中(如果需要)再回到问题,Lucene.NET可以快速处理此操作。
但是,考虑到上面关于如何在SQL Server中轻松执行此操作的说明,当涉及搜索标记项时,我选择了该路线。
一般搜索

现在,最重要的问题是在对内容进行常规短语或术语搜索时,如何整合其他信息(例如投票),以便按正确顺序确定结果?例如,在对ASP.NET MVC on Stack Overflow进行此搜索时,这是使用相关性选项卡的前五个结果的计数:

    q votes answers accepted answer votes asp.net highlights mvc highlights
    ------- ------- --------------------- ------------------ --------------
         21      26                    51                  2              2
         58      23                    70                  2              5
         29      24                    40                  3              4
         37      15                    25                  1              2
         59      23                    47                  2              2

请注意,结果页面上的标题和摘要中只有高亮显示,它们只是对文档、标题、标签、回复的真实词频的轻微指示(无论它们如何应用,这是另一个好问题)。
所有这些是如何结合起来的?
目前,我知道Lucene.NET将返回一个归一化的相关度分数,而投票数据将给出我可以使用它来确定置信度分数的威尔逊得分区间。
我该如何将这两个分数相结合以指示基于相关性和置信度的结果集的排序顺序?
对我来说,很明显它们之间应该有某种关系,但目前我还不清楚这种关系应该是什么。我知道随着时间的推移,我必须加以完善,但我在这一部分真的很迷茫。
我的初步想法是,如果相关度分数介于0和1之间,置信度分数介于0和1之间,则我可以这样做:
1 / ((e ^ cs) * (e ^ rs))

这样,我们可以得到一个归一化的值,该值越接近于0,结果就越相关和可信,并且可以按此进行排序。
主要问题在于,如果在标签和/或标题字段上执行了提升操作,则相关度评分将超出0到1的范围(上限变得无限,我不知道如何处理)。
此外,我认为我必须调整置信度评分以考虑完全为负的投票总数。由于完全为负的投票总数会导致威尔逊得分区间的下限为0,因此具有-500票的内容与具有-1票或0票的内容具有相同的置信度评分。
幸运的是,随着负面投票总数的增加,上限从1降至0。我可以将置信度评分更改为-1到1的范围,如下所示:
confidence score = votetally < 0 ? 
    -(1 - wilson score interval upper bound) :
    wilson score interval lower bound

这样做的问题是将0插入方程式中,将使所有得票为0的项目排在那些得票为负数的项目之下。
为此,我认为如果置信度分数将用于像上面那样的倒数方程式中(我显然担心溢出),则需要重新设计以始终为正。一种实现这一点的方法是:
confidence score = 0.5 + 
    (votetally < 0 ? 
        -(1 - wilson score interval upper bound) :
        wilson score interval lower bound) / 2

我的其他问题是如何使用Lucene.NET和SQL Server进行实际计算。 我不愿将置信度得分放在Lucene索引中,因为它需要使用字段缓存,这可能会对内存消耗产生巨大影响(如前所述)。
我想到的一个主意是从Lucene.NET获取相关性得分,然后使用表值参数将该分数流式传输到SQL Server(以及要选择的项的ID),此时我将使用置信度得分执行计算,然后正确地返回数据排序。
如前所述,我还有很多其他问题,答案已经开始构建框架,并将继续扩展问题和答案。
5个回答

10

你所需要的答案并不能仅仅通过使用Lucene找到。你需要排序和分组算法来过滤和理解数据以及它们之间的关系。Lucene可以帮助你获得规范化的数据,但之后你还需要正确的算法。

我建议你查阅以下一本或多本书籍,这些书将帮助你了解相关数学知识并指引你朝着正确的方向前进:

智能web算法

群体智慧在行动

编程之美


Mike Glenn:你说得很有道理。我会在今天晚些时候更新问题以反映这一事实,所以欢迎您对更新后的问题提出意见。此外,我已经阅读了《编程集体智慧》(并在撰写本文之前进行了咨询),发现它对于这种情况(即具有某种相关性得分与相关物品排名不同的情况)没有太多帮助,但我可能会再仔细看一下。 - casperOne
《智能Web算法》是一本你真的会想要查看的书。至于问题,这里是我使用lucene的起点。问题文本将被放置在一个分词字段中,回复、评论和标签也将在文档的各自字段中进行分词处理。默认情况下,lucene会对具有较少术语的字段上的匹配进行高权重,而不是对具有许多术语的字段上的匹配进行高权重。也就是说,在标签字段上的匹配比在问题或回复字段上的匹配更具相关性。 - Mike Glenn
现在引入投票和可能的用户声望分数可能会使您的搜索更准确。但是,您需要设置一个随机测试,并确定一种测量两种方法在提供用户正在搜索的结果方面有效性的方法。 - Mike Glenn
我更新了答案,更深入地介绍了使用投票数据来计算相关得分和置信度得分。我再次查阅了《编程之美》,但它所说的大多是“如果您想使用置信度数据来增强相关数据,您必须使用前面提到的技术之一”。它没有详细说明。我将查阅其他书籍,但如果您能提供与书中相关部分有关的具体参考资料,我会非常感激,并且当然也欢迎对上述更新问题的任何反馈。给予书籍参考+1。 - casperOne

4
  • 标题
  • 问题
  • 已接受的答案(如果没有被接受的答案,则为高投票答案)
  • 所有答案的组合

所有这些字段都是经过分析的。关闭长度规范化以更好地控制评分。

上述字段的顺序也反映了它们的重要性,按降序排列。也就是说,如果标题中的查询匹配比接受的答案中的匹配更重要,其他所有内容保持不变。

点赞数量是针对问题的,可以通过提高这些字段来捕获顶部答案。但是,原始点赞计数不能用作提升值,因为它可能会严重扭曲结果。(一个得到4个赞的问题将得到2个赞的问题的两倍得分。)在使用它们作为提升因子之前,这些值需要被强烈抑制。使用自然对数(对于upvotes > 3)看起来不错。

标题可以通过比问题稍高的值进行提升。

尽管问题的内部链接并不常见,但为问题设置类似pagerank的基本权重可能会产生一些有趣的结果。

我认为问题的标签并不是非常有价值的搜索信息。当您只想浏览问题时,标签很好用。大多数时候,标签是文本的一部分,因此搜索标签会导致匹配问题。这是可以讨论的。

典型的搜索查询将在所有四个字段上执行。

+(title:query question:query accepted_answer:query all_combined:query)

这是一个宏观的概述,需要进行大量调整才能得出正确的提升值和查询权重(如果需要)。实验将展示质量两个维度(相关性和重要性)的正确权重。您可以通过引入时间作为排名参数来使事情变得复杂。这里的想法是,如果问题发生在产品的特定版本中,并在后续修订中得到解决,则新的问题可能对用户更有用。
搜索中可以添加一些有趣的转折。如果只找到了“少量”匹配结果,则某种基本同义词搜索可能会有所帮助。例如,“减少Java堆大小”与“缩小Java堆大小”相同。但是,这也意味着“map reduce”将开始匹配“map decrease”。(拼写检查器显而易见,但我认为程序员会正确拼写他们的查询。)

@Shashikant Kore:在查询级别还是文档/字段级别上提升会更好呢?我认为在字段或查询级别上可能会更好,因为我想根据我所做的问题更改以不同的方式加权相关性得分。 - casperOne
是的,我更喜欢字段级别的加权,就像上面建议的那样。由于我们在索引时已经有了投票信息,所以在那个时候使用它是一个好主意。 - Shashikant Kore

2
您可能比大多数人更深入地思考了这个问题(这也是为什么我是您的第一个回复者的原因之一,我想)。 我只打算尝试解决您的最后三个问题,因为其中有很多内容我没有时间详细讨论,而且我认为这三个问题最有趣(物理实现问题可能会最终变成“选择某些东西,然后随着学习的深入进行微调”)。 投票数据 实话说,我不确定投票是否会使某个帖子在搜索中更相关,它只会使其更受欢迎。 如果有意义的话,我想说,无论给定的帖子是否与您的问题相关,它们是否与其他人相关大多是独立的。 也就是说,可能存在有趣的问题和人们想要找到的问题之间至少有一种弱相关性。 投票数据在纯粹基于数据的搜索中可能最有用,例如“最受欢迎”的类型搜索。 在通用的基于文本的搜索中,我可能一开始不会为投票提供任何权重,但会考虑开发一种算法,可能为排序提供轻微的权重(因此,不是返回结果,而是对它们的排序提供小幅度的提升)。 回复 我同意您在这里的做法,需要进行一些测试;请记住,这将是一个基于用户反馈的迭代过程(因此,您需要收集有关搜索结果是否成功的度量标准)。 其他 不要忘记用户的得分。 因此,用户在SO上也会得到积分,并影响他们在回答每个问题时的默认排名(看起来主要用于对于具有相同数量的提升的回复进行打破平局的情况)。

@Paul:我已更新问题,以反映投票数据(即置信度分数)与相关度分数的关系,以及对回复的想法。我不认为我会使用用户声望来加权搜索排序结果,但在确定投票方面,可以在SQL Server中轻松完成。 - casperOne

2
确定相关性总是很棘手的。您需要弄清楚自己想要实现什么目标。您的搜索是试图为某个人可能遇到的问题提供精确匹配,还是试图提供有关某个主题的最新项目列表?
一旦您确定了要返回的内容,就可以查看每个正在索引的特征的相对影响。这将启动一个粗略的搜索。从那里,您可以根据用户反馈进行微调(我建议使用隐式反馈,而不是显式反馈,否则会使用户感到恼火)。
至于索引,您应该尝试将数据放入其中,以便每个项目都具有排名所需的所有信息。这意味着您需要从多个位置获取数据来构建它。一些索引系统具有向现有项目添加值的功能,这将使在后续答案到达时将分数添加到问题变得容易。简单的方法只需要定期重建问题即可。

2
我认为Lucene不适合这项工作。 你需要一些真正快速、高可用性的东西...比如SQL 但你想要开源吗?
我建议你使用Sphinx - http://www.sphinxsearch.com/ 它更好,我有经验在说,我都用过。
Sphinx真是太棒了。

@Yuki:需要注意的是,我在后端使用的是 SQL Server 2008,所以 Sphinx 并没有帮助。 - casperOne
你好,再次提醒,你错了!我使用Sphinx和Sql Server 2008(甚至是2005)!当你配置它时,可以在索引ini文件中指定连接和选择语句... Sphinx不介意使用哪个数据库来获取数据。在谷歌上搜索一下,你就可以找到一些例子。 - Yuki
抱歉,你是对的,它可以在SQL Server 2008上运行,但我也在共享托管环境中,所以我无法运行服务(我认为这是必需的)。 - casperOne

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接