CommunityServer的搜索功能是通过定时对cs_Post表中的新增文章进行增量分词索引(由communityserver.config的Jobs配置节中ForumIndexing, GalleryIndexing, WeblogIndexing和FilesIndexing完成), 将分词索引结果hash后记录到cs_SearchBarrel表中, 搜索时再根据cs_SearchBarrel同其他表进行关联查询来实现的. 从CS诞生至今, 它的搜索功能一直对中文支持得很不好, 究其原因, 通过查看cs_SearchBarrel表中Word字段的值就可以发现, 主要是由于它对中文分词的实现很糟造成的. 所以一种解决方案是引入一个新的效果更好的中文分词组件, 而我采用的实现方案是使用SQL Server内置的Full-text index服务.
CS中广泛采用了Provider模式来实现各种功能, 搜索模块也不例外, 我们主要关注的有两个类, 一个是SearchTerms类, 一个是继承自SearchProvider类的SearchBarrelProvider类, SQL全文检索采用的是原文, 所以实现步骤如下:
1. 首先将SearchTerms类中对搜索关键字进行hash的代码去掉, 一是在对TokenizeKeywords方法进行调用时将第二个参数赋为false, 二是修改GetAndOrKeywords方法去掉对关键字hash的代码.
2. 其次我们要修改SearchBarrelProvider中的SQL查询字符串, 用全文检索的CONTAINS/FREETEXT谓词取代对cs_SearchBarrel表的关联查询. 同时数据库中的cs_SearchBarrel_Search存储过程也需要修改以去除对cs_SearchBarrel的查询操作.
3. 最后为cs_Posts表的Body字段创建Full-text Index, 设置一个schedule每隔一段时间增量索引一次, 修改communityserver,config文件注释掉那几个CS内置的定时索引Jobs, 重新编译源码, CS就开始采用SQL内置的全文检索功能了, 从实际使用的角度看, 效果还是蛮不错的. (具体修改细节请参考文后的附上的代码下载)
Google.cn主页的搜索框在用户键入关键字时会自动出现一个相关关键字的下拉列表供用户选择, 我们采用AJAX Controls Toolkit中的AutoComplete控件也可以为CS的搜索输入框实现同样的效果. 下面是实现步骤:
1. 首先创建一个名为cs_QueryHistory表, 它具有三个字段:QueryWord(搜索词), ResultCount(搜索结果数), QueryCount(同一搜索词被采用的次数), 在每次用户进行搜索之后, 如果此次搜索词已经在cs_QueryHistory表中存在, 则更新对应的QueryCount和ResultCount, 否则插入一条新的数据.
2. 然后建立一个名为QueryAutoComplete.asmx的web service提供给AutoComplete控件, web service提供一个声明为public string[] GetCompletionList(string prefixText, int count)的web method, 被调用时会根据prefixText查询cs_QueryHistory表, 返回由prefixText作为前缀以及数量为count的string数组. 为了提高效率, 我们还可以将由a-z开头的所有关键词预先取出置入缓存中以加快响应速度.
在调试过程中还发现了两个问题, 一是CS中的输入框控件DefaultButtonTextBox在回车时会触发设置到它的button属性上的按钮的postback行为, 而在AutoComplete控件的下拉框中选取好某个备选条目时, 也是用回车键进行确认的, 这就造成了事件处理先后问题, 如果AutoComplete控件先响应了回车的keydown事件, 则一切正常. 否则, DefaultButtonTextBox先行响应了回车的keydown事件, 会使得AutoComplete控件改变搜索框中值的行为发生在postback之后. 也就意味着submit到服务器的表单中不是我们选中的条目, 而是在搜索框中未补全的关键词. 第二个问题是, 当返回的string数组不足count条目时, AutoComplete控件的下拉框中会用多条名为”null”的条目替代, 用户体验很不好. 还好AJAX Controls Toolkit是开源的, 我们下载源码修改AutoComplete控件客户端脚本中的_onKeyDown响应方法, 在用户选取条目的同时就同时更新文本框的值, 而不必用回车确认. 同时修改呈现下拉框的_update方法, 在增加新条目的循环中加一条判断语句, 一旦遇到null值或者string长度为空则break跳出. 重新编译AJAX Control Toolkit程序集并在CS项目中刷新引用. 这样就实现了关键字自动补完功能.
后记: 通过对CS搜索模块的改进, 发现里面有一些不甚必要代码, 一个简单的过程偏偏要弄得非常复杂, 一个模块的调用要经历大量的继承和多态绑定, 很多人反映CS整体效率不高也就不足为奇了. 当然ASP.NET2.0往往也就陪上成了替罪羊...我在调试过程中, 还发现, 默认的搜索模块拼接出来的查询串相当恐怖, 当搜索"过程 and 计划 or CommunityServer"时, 拼接出来的查询串居然是: SELECT DISTINCT P.PostID, P.SectionID, Weight = (B0.Weight + B1.Weight), P.PostDate FROM cs_SearchBarrel B0, cs_SearchBarrel B1, cs_Posts P, cs_Sections F WHERE (B0.WordHash = -2605057 OR B0.WordHash = -272220937) AND B1.WordHash = -405848999 AND B0.PostID = P.PostID AND B1.PostID = P.PostID AND (P.SectionID in (3)) AND F.SectionID = P.SectionID AND P.SettingsID = 1000 AND F.IsSearchable = 1 AND P.IsApproved = 1 ORDER BY Weight DESC, PostDate DESC, 我们可以看到, 有n个查询关键词, 那么cs_SearchBarrel表就要被嵌套进行自然连接n-1次, 查询复杂度会随着关键字数量增多呈指数级(X^n, n增长)增大, 当索引表在一段时间不断膨胀后, 针对相同的query, 查询的复杂度又会依据幂级数(X^n, X增长)增大!
我是陈远(Vincent Chen), 正在上研, 对Web开发和数据挖掘技术很感兴趣, 我的邮件是NickLedson[at]gmail[dot]com, 欢迎交流:-)
打印 | 张贴于 2007-04-09 21:32:00 | Tag:WEB开发
留言反馈
比较紧急,望不吝赐教!
CS的写cache策略是透写, 读cache策略是LRU(latest recently used), 这种策略对内存的要求很高, 因为它基本思想就是, 存储是廉价的, 用它来换性能划得来. 具体代码分析博客园曾经有人写了一个系列文章, 还有宝玉的博客上也有一部分, 多搜一搜吧.
能详细说么?我这两天看cs的代码。不知道有没有人系统分析过。或者统一整理过。