这里的问题更多是假设性的,即如果将Lucene.NET作为站内搜索和其他因素的基础(如Stack Overflow [SO]),则应该采取哪些方法。
根据Stack Overflow博客上的“SQL 2008 Full-Text Search Problems”文章,有强烈的迹象表明曾经考虑过使用Lucene.NET,但根据Geoff Dalgas在2010年2月19日的评论,情况显然不是这样的:
所以我的问题是,如何将Lucene.NET应用于具有与Stack Overflow相同语义的站点?Stack Overflow没有使用Lucene.NET——我们使用SQL Server全文索引。搜索是我们继续进行微小调整的领域。
以下是一些背景信息和我迄今为止所做/思考的内容(是的,我已经实现了大部分内容,搜索是我要完成的最后一个方面): 技术:
- ASP.NET MVC(一种Web应用程序框架)
- SQL Server 2008(一种关系型数据库管理系统)
- .NET 3.5(一种开发框架)
- C# 3.0(一种编程语言)
当然,主角是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),此时我将使用置信度得分执行计算,然后正确地返回数据排序。
如前所述,我还有很多其他问题,答案已经开始构建框架,并将继续扩展问题和答案。