从这篇文章开始我会引入一些领域上的知识或者给出一些参考文章来供大家阅读,我们对这些领域上的知识没有必要深挖,只要大概了解一下就可以了,这对于我们分析BlogEngine.Net会有很大的帮助。在这篇文章中我主要向大家介绍一下BlogEngine.Net的搜索部分的实现以及支持的相应标准等。
先用自己的话解释一下什么是开放搜索
开放搜索应该也是一种标准了,现在它越来越流行起来,记得我第一次见到这个东东是在codeproject中。大家一定注意到了IE7右上角的那个小工具栏了,
附件: OpenSearch.jpg
我们可以在那里选择一个搜索提供程序例如:Google,当我们输入信息回车以后就会发现页面跳转到了Google的结果页面。那么这些提供程序信息是怎么获得的呢?IE又是怎么知道要去Google的页面呢?原来是通过一个标准的XML文件,这个文件的具体格式可以参照一下codeproject的开放搜索文件http://www.codeproject.com/info/OpenSearch.xml,当把
<link rel="search" type="application/opensearchdescription+xml" title="CodeProject" href="http://www.codeproject.com/info/OpenSearch.xml"></link>
这种代码加入到一个Html的Head中,在浏览器打开这个Html文当时就会发觉里面的开放搜索文件,然后在那个小工具栏中增加一个提供程序,当我们在工具栏中输入信息并回车以后就可以直接跳转到相应的结果页面,可以看出这个XML文件充当着一种中间桥梁的作用。这种功能不仅需要浏览器的支持,同时也需要网站本身提供相应的支持,这就是开放搜索。这个和IE7中的Rss源发现机制很类似。
那么BlogEngine.Net中对于开放搜索支持是如何实现的呢
进入BlogEngine.Net的首页查看生成的Html源代码我们会看到
<link type="application/opensearchdescription+xml" rel="search" title="Name of the blog" href="http://hostname/opensearch.axd" />,
这里请求了opensearch.axd,查看Web.config我们注意到opensearch.axd交给了BlogEngine.Core.Web.HttpHandlers.OpenSearchHandler处理,这个XML是通过一个HttpHandler来生成的。在Html源代码的Head中我们会发现很多*.axd的引用,实际上它们都是使用自定义的HttpHandler来处理的,对于这些HttpHandler我会在后续有关文章中进行说明。从OpenSearchHandler生成的XML我们可以看出,执行搜索的Url为(类似Google的处理方式)http://hostname/search.aspx?q={searchTerms},searchTerms就是输入的部分,具体的搜索由页面search.aspx调用核心层内部逻辑来完成。
内部逻辑的主要部分IPublishable与Search的实现分析
BlogEngine.Net的搜索目标不是像Google那种页面抓取,也不是像我们一般的企业应用直接查询数据库数据,而是一种内存中对象的搜索。当对象完成填充以后数据就驻留在内存中,所以BlogEngine.Net的搜索目标应该是对象实例。主要通过IPublishable与Search两个类完成。首先IPublishable定义了可被搜索对象的类的共有特征,以便统一搜索模型,凡是实现了这个接口的类的对象都有机会被搜索到,例如Comment,Page,Post。
- 1namespace BlogEngine.Core
- 2{
- 3 /**//// <summary>
- 4 /// An interface implemented by the classed that can be published.
- 5 /// <remarks>
- 6 /// To implemnet this interface means that the class can be searched
- 7 /// from the search page and that it can be syndicated in RSS and ATOM.
- 8 /// </remarks>
- 9 /// </summary>
- 10 public interface IPublishable
- 11 {
- 12 /**//// <summary>
- 13 /// Gets the title of the object
- 14 /// </summary>
- 15 String Title { get; }
- 16
- 17 /**//// <summary>
- 18 /// Gets the content.
- 19 /// </summary>
- 20 /// <value>The content.</value>
- 21 String Content { get;}
- 22
- 23 /**//// <summary>
- 24 /// Gets the date created.
- 25 /// </summary>
- 26 /// <value>The date created.</value>
- 27 DateTime DateCreated { get; }
- 28
- 29 /**//// <summary>
- 30 /// Gets the date modified.
- 31 /// </summary>
- 32 /// <value>The date modified.</value>
- 33 DateTime DateModified { get; }
- 34
- 35 /**//// <summary>
- 36 /// Gets the id.
- 37 /// </summary>
- 38 /// <value>The id.</value>
- 39 Guid Id { get; }
- 40
- 41 /**//// <summary>
- 42 /// Gets the relative link.
- 43 /// </summary>
- 44 /// <value>The relative link.</value>
- 45 String RelativeLink { get;}
- 46
- 47 /**//// <summary>
- 48 /// Gets the absolute link.
- 49 /// </summary>
- 50 /// <value>The absolute link.</value>
- 51 Uri AbsoluteLink { get;}
- 52
- 53 /**//// <summary>
- 54 /// Gets the description.
- 55 /// </summary>
- 56 /// <value>The description.</value>
- 57 String Description { get;}
- 58
- 59 /**//// <summary>
- 60 /// Gets the author.
- 61 /// </summary>
- 62 /// <value>The author.</value>
- 63 String Author { get;}
- 64
- 65 /**//// <summary>
- 66 /// Raises the <see cref="E:Serving"/> event.
- 67 /// </summary>
- 68 /// <param name="eventArgs">The <see cref="BlogEngine.Core.ServingEventArgs"/> instance containing the event data.</param>
- 69 void OnServing(ServingEventArgs eventArgs);
- 70
- 71 /**//// <summary>
- 72 /// Gets the categories.
- 73 /// </summary>
- 74 /// <value>The categories.</value>
- 75 StateList<Category> Categories { get;}
- 76
- 77 /**//// <summary>
- 78 /// Gets whether or not this item should be shown
- 79 /// </summary>
- 80 bool IsVisible { get;}
- 81 }
- 82}
复制代码
void OnServing(ServingEventArgs eventArgs);这个东西现在我也不是很确定是干嘛的?还没有看到使用到的相关代码,我猜好像可以做一些统计什么的,包括阅读量等,欢迎大家一起讨论。
Search类的实现代码看起来很繁杂,但是脉络是很清晰的。它只有两个方法对外公开,分别为:
- 1/**//// <summary>
- 2/// Searches all the posts and returns a ranked result set.
- 3/// </summary>
- 4/// <param name="searchTerm">The term to search for</param>
- 5/// <param name="includeComments">True to include a post's comments and their authors in search</param>
- 6public static List<IPublishable> Hits(string searchTerm, bool includeComments)
- 7{
- 8 lock (_SyncRoot)
- 9 {
- 10 List<Result> results = BuildResultSet(searchTerm, includeComments);
- 11 List<IPublishable> items = results.ConvertAll(new Converter<Result, IPublishable>(ResultToPost));
- 12 results.Clear();
- 13 OnSearcing(searchTerm);
- 14 return items;
- 15 }
- 16}
- 17
- 18public static List<IPublishable> ApmlMatches(XmlDocument apmlFile, int maxInterests)
- 19{
- 20 Dictionary<string, float> concepts = new Dictionary<string, float>();
- 21 XmlNodeList nodes = apmlFile.SelectNodes("//Concept");
- 22 foreach (XmlNode node in nodes)
- 23 {
- 24 string key = node.Attributes["key"].InnerText.ToLowerInvariant().Trim();
- 25 float value = float.Parse(node.Attributes["value"].InnerText, System.Globalization.CultureInfo.InvariantCulture);
- 26 if (!concepts.ContainsKey(key))
- 27 {
- 28 concepts.Add(key, value);
- 29 }
- 30 else if (concepts[key] < value)
- 31 {
- 32 concepts[key] = value;
- 33 }
- 34 }
- 35
- 36 concepts = SortDictionary(concepts);
- 37 int max = Math.Min(concepts.Count, maxInterests);
- 38 int counter = 0;
- 39 List<Result> resultSet = new List<Result>();
- 40 foreach (string key in concepts.Keys)
- 41 {
- 42 counter++;
- 43 List<Result> results = BuildResultSet(key, false);
- 44 //results = results.FindAll(delegate(Result r) { return r.Rank > 1; });
- 45 resultSet.AddRange(results);
- 46 if (counter == max)
- 47 break;
- 48 }
- 49
- 50 resultSet.Sort();
- 51 List<Result> aggregatedResults = new List<Result>();
- 52 foreach (Result r in resultSet)
- 53 {
- 54 if (!aggregatedResults.Contains(r))
- 55 {
- 56 aggregatedResults.Add(r);
- 57 }
- 58 else
- 59 {
- 60 Result existingResult = aggregatedResults.Find(delegate(Result res) { return res.GetHashCode() == r.GetHashCode(); });
- 61 existingResult.Rank += r.Rank;
- 62 }
- 63 }
- 64
- 65 aggregatedResults = aggregatedResults.FindAll(delegate(Result r) { return r.Rank > 1; });
- 66 List<IPublishable> items = aggregatedResults.ConvertAll(new Converter<Result, IPublishable>(ResultToPost));
- 67 List<IPublishable> uniqueItems = new List<IPublishable>();
- 68
- 69 foreach (IPublishable item in items)
- 70 {
- 71 if (!uniqueItems.Contains(item))
- 72 uniqueItems.Add(item);
- 73 }
- 74
- 75 return uniqueItems;
- 76}
复制代码
其中Hits是直接根据内容进行查找,ApmlMatches是为支持Apml而提供的(下文做了进一步的解释)。在Search类的静态构造函数中我们可以看到Search类监听了很多实现IPublishable接口的BusinessBase的事件以便来重新构造目录,这个目录也是在内存中已经存在的,每当有新的文章等可以被搜索到的对象(实现IPublishable接口)被保存时,它就会被重新建立,以Post为例:
- 1/**//// <summary>
- 2/// Adds a post to the catalog when it is added.
- 3/// </summary>
- 4private static void Post_Saved(object sender, SavedEventArgs e)
- 5{
- 6 lock (_SyncRoot)
- 7 {
- 8 if (e.Action == SaveAction.Insert)// 插入时进行添加
- 9 {
- 10 AddItem(sender as Post);
- 11 }
- 12 else
- 13 {
- 14 BuildCatalog();// 否则重建目录
- 15 }
- 16 }
- 17}
复制代码
这里又体现了BusinessBase提供的静态事件确实很灵活(请参照我的第二篇文章)。Hits方法的的主逻辑是执行BuildResultSet来构造结果集,BuildResultSet使用正则表达式来进行匹配,并按照一定的匹配度算法进行了结果排序:
- 1/**//// <summary>
- 2/// Builds the results set and ranks it.
- 3/// </summary>
- 4private static List<Result> BuildResultSet(string searchTerm, bool includeComments)
- 5{
- 6 List<Result> results = new List<Result>();
- 7 string term = CleanContent(searchTerm.ToLowerInvariant().Trim(), false);
- 8 string[] terms = term.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
- 9 string regex = string.Format(System.Globalization.CultureInfo.InvariantCulture, "({0})", string.Join("|", terms));
- 10
- 11 foreach (Entry entry in _Catalog)
- 12 {
- 13 Result result = new Result();
- 14 if (!(entry.Item is Comment))
- 15 {
- 16 int titleMatches = Regex.Matches(entry.Title, regex).Count;
- 17 result.Rank = titleMatches * 20;
- 18
- 19 int postMatches = Regex.Matches(entry.Content, regex).Count;
- 20 result.Rank += postMatches;
- 21
- 22 int descriptionMatches = Regex.Matches(entry.Item.Description, regex).Count;
- 23 result.Rank += descriptionMatches * 2;
- 24 }
- 25 else if (includeComments)
- 26 {
- 27 int commentMatches = Regex.Matches(entry.Content + entry.Title, regex).Count;
- 28 result.Rank += commentMatches;
- 29 }
- 30
- 31 if (result.Rank > 0)
- 32 {
- 33 result.Item = entry.Item;
- 34 results.Add(result);
- 35 }
- 36 }
- 37
- 38 results.Sort();
- 39 return results;
- 40}
复制代码
ApmlMatches的原理也差不多,这里就不再重复了。
在Search类中,请大家在阅读源代码时注意一下对于一些字符的过滤处理是如何实现的,还有Search中对外公布的几个事件。Entry和Result是为查询时数据的交换而定义的,而Result还实现了IComparable<Result>来对结果排序使用。Search这部分代码给我的感觉就是结构很清晰,但是处理的逻辑很复杂。
那么客户端如何使用Search中的方法?
对于Search方法的调用,一般有两种方式,一种形式如:
List<IPublishable> list = Search.Hits(term, includeComments);
term就是输入的关键词,includeComments为是否包含评论,list是已排序的搜索结果。另一种形式是:
list = Search.ApmlMatches(docs[key], 30);
这是根据一个Uri上的一个Apml文件来对查找结果进行输出,Apml也是一种标准文件,用来定义互联网上一些关键词的活跃程度(自己的话),在BlogEngine.Net中实际上也支持这种Apml,同样也是通过HttpHandler实现的。对于客户端的使用大家可以具体参照一下Web项目中Search.aspx文件中的CodeBehind,此外BlogEngine.Net在Web项目中的Wdiget中也有一个客户端查询,这个以后会在讲解Wdiget时做更多的说明。
总结
1.BlogEngine.Net的搜索还是很经典的,支持开放搜索和Apml等标准。
2.IPublishable的定义很有必要而且处理得很巧妙,解决了搜索对象的统一性问题,以便统一处理。
3.Search类的内核实现比较好,结构很清晰。
领域知识文章参考
1.使用 RSS 和 Atom 实现新闻联合
2.浅述RDF,畅想一下FOAF应用
3.OPML 详解
三人行,必有我师!