前言
這次開發的博客主要功能或特點:
第一:可以兼容各終端,特別是手機端。
第二:到時會用到大量html5,炫啊。
第三:導入博客園的精華文章,並做分類。(不要封我)
第四:做個插件,任何網站上的技術文章都可以轉發收藏 到本博客。
所以打算寫個系類:《一步步搭建自己的博客》
- 一步步開發自己的博客 .NET版(1、頁面布局、blog遷移、數據加載)
- 一步步開發自己的博客 .NET版(2、評論功能)
- 一步步開發自己的博客 .NET版(3、注冊登錄功能)
- 一步步開發自己的博客 .NET版(4、文章發布功能)
- 一步步開發自己的博客 .NET版(5、搜索功能)
- 一步步開發自己的博客 .NET版(6、手機端的兼容)
演示地址:http://haojima.net/ 群內共享源碼:469075305

今天來分析下 嗨-博客 中的搜索功能。搜索功能在個人網站里面要有這么個東西,但又不是特別重要。所以我們需要有,可以不用太深入了解,畢竟我們不是專門做搜索這塊的。
所以,我打算把搜索分兩塊。一塊是,用Lucene.Net實現站內搜索。一塊是利用第三方搜索引擎來 實現站內搜索。
Lucene.Net簡介
Lucene.net是Lucene的.net移植版本,是一個開源的全文檢索引擎開發包,即它不是一個完整的全文檢索引擎,而是一個全文檢索引擎的架構,提供了完整的查詢引擎和索引引擎。開發人員可以基於Lucene.net實現全文檢索的功能。Lucene.net是Apache軟件基金會贊助的開源項目,基於Apache License協議。Lucene.net並不是一個爬行搜索引擎,也不會自動地索引內容。我們得先將要索引的文檔中的文本抽取出來,然后再將其加到Lucene.net索引中。標准的步驟是先初始化一個Analyzer、打開一個IndexWriter、然后再將文檔一個接一個地加進去。一旦完成這些步驟,索引就可以在關閉前得到優化,同時所做的改變也會生效。這個過程可能比開發者習慣的方式更加手工化一些,但卻在數據的索引上給予你更多的靈活性,而且其效率也很高。(來源百度百科)
Lucene幫助類
其實 在之前 我也是接觸到過Lucene.net,那也是自己 做的個小玩意(博客備份小工具3) 瞎折騰的。但是 這次打算遷移到這個系統中,不知怎么的 報錯了。可能是這次用的是 .net 4.5。Lucene這東西太高深,我也沒打算深究。於是 在網上收索了一把,資料還挺多的。《lucene.net 3.0.3、結合盤古分詞進行搜索的小例子(分頁功能)》 我隨意看了下,這里有個 幫助類 挺不錯的,也還符合 我這樣想要的效果。這里來分析下這個幫助類。
1.首先創建索引。
IndexWriter writer = new IndexWriter(directory_luce, analyzer, false, IndexWriter.MaxFieldLength.LIMITED); Document doc = new Document(); doc.Add(new Field(name, value, Field.Store.YES, Field.Index.NOT_ANALYZED)); writer.AddDocument(doc);
這里的
directory_luce 是索引創建路徑
analyzer 分析器
value 是對應 存入索引額名字和值
2.從索引里面搜索
string[] fileds = { "title", "content" };//查詢字段 QueryParser parser = null; parser = new MultiFieldQueryParser(version, fileds, analyzer);//多個字段查詢 Query query = parser.Parse(keyword); int n = 1000; IndexSearcher searcher = new IndexSearcher(directory_luce, true);//true-表示只讀 TopDocs docs = searcher.Search(query, (Filter)null, n); if (docs == null || docs.TotalHits == 0) { return null; } else { List<SearchResult> list = new List<SearchResult>(); int counter = 1; foreach (ScoreDoc sd in docs.ScoreDocs)//遍歷搜索到的結果 { try { Document doc = searcher.Doc(sd.Doc); int id = int.Parse(doc.Get("id")); string title = doc.Get("title"); string content = doc.Get("content"); string blogTag = doc.Get("blogTag"); string url = doc.Get("url"); int flag = int.Parse(doc.Get("flag")); int clickQuantity = int.Parse(doc.Get("clickQuantity")); string createdate = doc.Get("createdate"); PanGu.HighLight.SimpleHTMLFormatter simpleHTMLFormatter = new PanGu.HighLight.SimpleHTMLFormatter("<font color=\"red\">", "</font>"); PanGu.HighLight.Highlighter highlighter = new PanGu.HighLight.Highlighter(simpleHTMLFormatter, new PanGu.Segment()); highlighter.FragmentSize = 50; content = highlighter.GetBestFragment(keyword, content); string titlehighlight = highlighter.GetBestFragment(keyword, title); if (titlehighlight != "") title = titlehighlight; list.Add(new SearchResult(title, content, url, blogTag, id, clickQuantity, flag)); } catch (Exception ex) { Console.WriteLine(ex.Message); } counter++; } return list;
3.完整代碼

public class PanGuLuceneHelper { private PanGuLuceneHelper() { } #region 單一實例 private static PanGuLuceneHelper _instance = null; /// <summary> /// 單一實例 /// </summary> public static PanGuLuceneHelper instance { get { if (_instance == null) _instance = new PanGuLuceneHelper(); return _instance; } } #endregion #region 00一些屬性和參數 #region Lucene.Net的目錄-參數 private Lucene.Net.Store.Directory _directory_luce = null; /// <summary> /// Lucene.Net的目錄-參數 /// </summary> public Lucene.Net.Store.Directory directory_luce { get { if (_directory_luce == null) _directory_luce = Lucene.Net.Store.FSDirectory.Open(directory); return _directory_luce; } } #endregion #region 索引在硬盤上的目錄 private System.IO.DirectoryInfo _directory = null; /// <summary> /// 索引在硬盤上的目錄 /// </summary> public System.IO.DirectoryInfo directory { get { if (_directory == null) { string dirPath = AppDomain.CurrentDomain.BaseDirectory + "SearchIndex"; if (System.IO.Directory.Exists(dirPath) == false) _directory = System.IO.Directory.CreateDirectory(dirPath); else _directory = new System.IO.DirectoryInfo(dirPath); } return _directory; } } #endregion #region 分析器 private Analyzer _analyzer = null; /// <summary> /// 分析器 /// </summary> public Analyzer analyzer { get { { _analyzer = new Lucene.Net.Analysis.PanGu.PanGuAnalyzer();// } return _analyzer; } } #endregion #region 版本號枚舉類 private static Lucene.Net.Util.Version _version = Lucene.Net.Util.Version.LUCENE_30; /// <summary> /// 版本號枚舉類 /// </summary> public Lucene.Net.Util.Version version { get { return _version; } } #endregion #endregion #region 01創建索引 /// <summary> /// 創建索引(先刪 后更新) /// </summary> /// <param name="datalist"></param> /// <returns></returns> public bool CreateIndex(List<SearchResult> datalist) { IndexWriter writer = null; try { writer = new IndexWriter(directory_luce, analyzer, false, IndexWriter.MaxFieldLength.LIMITED);//false表示追加(true表示刪除之前的重新寫入) } catch { writer = new IndexWriter(directory_luce, analyzer, true, IndexWriter.MaxFieldLength.LIMITED);//false表示追加(true表示刪除之前的重新寫入) } foreach (SearchResult data in datalist) { writer.DeleteDocuments(new Term("id", data.id.ToString()));//新增前 刪除 不然會有重復數據 CreateIndex(writer, data); } writer.Optimize(); writer.Dispose(); return true; } public bool CreateIndex(SearchResult data) { List<SearchResult> datalist = new List<SearchResult>(); datalist.Add(data); return CreateIndex(datalist); } public bool CreateIndex(IndexWriter writer, SearchResult data) { try { if (data == null) return false; Document doc = new Document(); Type type = data.GetType();//assembly.GetType("Reflect_test.PurchaseOrderHeadManageModel", true, true); //命名空間名稱 + 類名 //創建類的實例 //object obj = Activator.CreateInstance(type, true); //獲取公共屬性 PropertyInfo[] Propertys = type.GetProperties(); for (int i = 0; i < Propertys.Length; i++) { //Propertys[i].SetValue(Propertys[i], i, null); //設置值 PropertyInfo pi = Propertys[i]; string name = pi.Name; object objval = pi.GetValue(data, null); string value = objval == null ? "" : objval.ToString(); //值 if (name == "id" || name == "flag")//id在寫入索引時必是不分詞,否則是模糊搜索和刪除,會出現混亂 { doc.Add(new Field(name, value, Field.Store.YES, Field.Index.NOT_ANALYZED));//id不分詞 } else { doc.Add(new Field(name, value, Field.Store.YES, Field.Index.ANALYZED)); } } writer.AddDocument(doc); } catch (System.IO.FileNotFoundException fnfe) { throw fnfe; } return true; } #endregion #region 02在title和content字段中查詢數據 /// <summary> /// 在title和content字段中查詢數據 /// </summary> /// <param name="keyword"></param> /// <returns></returns> public List<SearchResult> Search(string keyword) { string[] fileds = { "title", "content" };//查詢字段 QueryParser parser = null; parser = new MultiFieldQueryParser(version, fileds, analyzer);//多個字段查詢 Query query = parser.Parse(keyword); int n = 1000; IndexSearcher searcher = new IndexSearcher(directory_luce, true);//true-表示只讀 TopDocs docs = searcher.Search(query, (Filter)null, n); if (docs == null || docs.TotalHits == 0) { return null; } else { List<SearchResult> list = new List<SearchResult>(); int counter = 1; foreach (ScoreDoc sd in docs.ScoreDocs)//遍歷搜索到的結果 { try { Document doc = searcher.Doc(sd.Doc); int id = int.Parse(doc.Get("id")); string title = doc.Get("title"); string content = doc.Get("content"); string blogTag = doc.Get("blogTag"); string url = doc.Get("url"); int flag = int.Parse(doc.Get("flag")); int clickQuantity = int.Parse(doc.Get("clickQuantity")); string createdate = doc.Get("createdate"); PanGu.HighLight.SimpleHTMLFormatter simpleHTMLFormatter = new PanGu.HighLight.SimpleHTMLFormatter("<font color=\"red\">", "</font>"); PanGu.HighLight.Highlighter highlighter = new PanGu.HighLight.Highlighter(simpleHTMLFormatter, new PanGu.Segment()); highlighter.FragmentSize = 50; content = highlighter.GetBestFragment(keyword, content); string titlehighlight = highlighter.GetBestFragment(keyword, title); if (titlehighlight != "") title = titlehighlight; list.Add(new SearchResult(title, content, url, blogTag, id, clickQuantity, flag)); } catch (Exception ex) { Console.WriteLine(ex.Message); } counter++; } return list; } //st.Stop(); //Response.Write("查詢時間:" + st.ElapsedMilliseconds + " 毫秒<br/>"); } #endregion #region 03在不同的分類下再根據title和content字段中查詢數據(分頁) /// <summary> /// 在不同的類型下再根據title和content字段中查詢數據(分頁) /// </summary> /// <param name="_flag">分類,傳空值查詢全部</param> /// <param name="keyword"></param> /// <param name="PageIndex"></param> /// <param name="PageSize"></param> /// <param name="TotalCount"></param> /// <returns></returns> public List<SearchResult> Search(string _flag, string keyword, int PageIndex, int PageSize) { if (PageIndex < 1) PageIndex = 1; Stopwatch st = Stopwatch.StartNew(); st.Start(); BooleanQuery bq = new BooleanQuery(); if (_flag != "") { QueryParser qpflag = new QueryParser(version, "flag", analyzer); Query qflag = qpflag.Parse(_flag); bq.Add(qflag, Occur.MUST);//與運算 } if (keyword != "") { string[] fileds = { "blogTag", "title", "content" };//查詢字段 QueryParser parser = null;// new QueryParser(version, field, analyzer);//一個字段查詢 parser = new MultiFieldQueryParser(version, fileds, analyzer);//多個字段查詢 Query queryKeyword = parser.Parse(keyword); bq.Add(queryKeyword, Occur.MUST);//與運算 } TopScoreDocCollector collector = TopScoreDocCollector.Create(PageIndex * PageSize, false); IndexSearcher searcher = new IndexSearcher(directory_luce, true);//true-表示只讀 searcher.Search(bq, collector); if (collector == null || collector.TotalHits == 0) { //TotalCount = 0; return null; } else { int start = PageSize * (PageIndex - 1); //結束數 int limit = PageSize; ScoreDoc[] hits = collector.TopDocs(start, limit).ScoreDocs; List<SearchResult> list = new List<SearchResult>(); int counter = 1; //TotalCount = collector.TotalHits; st.Stop(); //st.ElapsedMilliseconds;//毫秒 foreach (ScoreDoc sd in hits)//遍歷搜索到的結果 { try { Document doc = searcher.Doc(sd.Doc); int id = int.Parse(doc.Get("id")); string title = doc.Get("title"); string content = doc.Get("content"); string blogTag = doc.Get("blogTag"); string url = doc.Get("url"); int flag = int.Parse(doc.Get("flag")); int clickQuantity = int.Parse(doc.Get("clickQuantity")); content = Highlight(keyword, content); //string titlehighlight = Highlight(keyword, title); //if (titlehighlight != "") title = titlehighlight; list.Add(new SearchResult(title, content, url, blogTag, id, clickQuantity, flag)); } catch (Exception ex) { Console.WriteLine(ex.Message); } counter++; } return list; } } #endregion #region 把content按照keywords進行高亮 /// <summary> /// 把content按照keywords進行高亮 /// </summary> /// <param name="keywords"></param> /// <param name="content"></param> /// <returns></returns> private static string Highlight(string keywords, string content) { SimpleHTMLFormatter simpleHTMLFormatter = new PanGu.HighLight.SimpleHTMLFormatter("<strong>", "</strong>"); Highlighter highlighter = new PanGu.HighLight.Highlighter(simpleHTMLFormatter, new Segment()); highlighter.FragmentSize = 200; return highlighter.GetBestFragment(keywords, content); } #endregion #region 04刪除索引 #region 刪除索引數據(根據id) /// <summary> /// 刪除索引數據(根據id) /// </summary> /// <param name="id"></param> /// <returns></returns> public bool Delete(string id) { bool IsSuccess = false; Term term = new Term("id", id); IndexWriter writer = new IndexWriter(directory_luce, analyzer, false, IndexWriter.MaxFieldLength.LIMITED); writer.DeleteDocuments(term); // writer.DeleteDocuments(term)或者writer.DeleteDocuments(query); writer.Commit(); IsSuccess = writer.HasDeletions(); writer.Dispose(); return IsSuccess; } #endregion #region 刪除全部索引數據 /// <summary> /// 刪除全部索引數據 /// </summary> /// <returns></returns> public bool DeleteAll() { bool IsSuccess = true; try { IndexWriter writer = new IndexWriter(directory_luce, analyzer, false, IndexWriter.MaxFieldLength.LIMITED); writer.DeleteAll(); writer.Commit(); IsSuccess = writer.HasDeletions(); writer.Dispose(); } catch { IsSuccess = false; } return IsSuccess; } #endregion #endregion #region 分詞測試 /// <summary> /// 分詞測試 /// </summary> /// <param name="keyword"></param> /// <returns></returns> public string Token(string keyword) { string ret = ""; System.IO.StringReader reader = new System.IO.StringReader(keyword); Lucene.Net.Analysis.TokenStream ts = analyzer.TokenStream(keyword, reader); bool hasNext = ts.IncrementToken(); Lucene.Net.Analysis.Tokenattributes.ITermAttribute ita; while (hasNext) { ita = ts.GetAttribute<Lucene.Net.Analysis.Tokenattributes.ITermAttribute>(); ret += ita.Term + "|"; hasNext = ts.IncrementToken(); } ts.CloneAttributes(); reader.Close(); analyzer.Close(); return ret; } #endregion }

public class SearchResult { public SearchResult() { } public SearchResult(string title, string content, string url, string blogTag, int id, int clickQuantity, int flag) { this.blogTag = blogTag; this.clickQuantity = clickQuantity; this.content = content; this.id = id; this.url = url; this.title = title; this.flag = flag; } /// <summary> /// 標題 /// </summary> public string title { get; set; } /// <summary> /// 正文內容 /// </summary> public string content { get; set; } /// <summary> /// url地址 /// </summary> public string url { get; set; } /// <summary> /// tag標簽 /// </summary> public string blogTag { get; set; } /// <summary> /// 唯一id /// </summary> public int id { get; set; } /// <summary> /// 點擊量 /// </summary> public int clickQuantity { get; set; } /// <summary> /// 標記(用戶) /// </summary> public int flag { get; set; } }
必應站內搜索
1.為什么要用必應搜索?
因為我們現在做的主要功能是博客系統,搜索只是其中的一小塊環節。而 我對這搜索並不了解,所以就用第三方搜索,省事嘛。
2.為什么不用別的三方收索呢?
百度?不用說了,咱們程序員都懂的。谷歌?我倒是想用,可生在天朝,也是沒得辦法。選來選去 還是選了必應。
3.怎么來使用第三方的站內搜索?
格式如下:http://cn.bing.com/search?q=關鍵字+site:網站地址
例如:http://cn.bing.com/search?q=博客+site:blog.haojima.net
效果圖:
嘿嘿,如此之簡單。既然都已經看到效果了,那么 我們可以干些什么呢? 我打算 直接把結果 顯示在我的 站內搜索結果。為什么 不直接跳轉到這個頁面顯示 搜索結果?因為 這個頁面有廣告什么的,不能按照我自己的方式顯示。我直接把結果放我的搜索頁面 可以和 我上面用Lucene.net的搜索結果一起顯示,這樣豈不是 顯得更專業。
,不知道的 還以為 是我自己怎么弄出來的。那么 我們怎么解析 搜到的結果呢?我這里推薦下 Jumony 之前我一直是用 HtmlAgilityPack ,現在為什么不用了,因為有了更好的。HtmlAgilityPack 缺點是 要去xpath,然 如果頁面存在js動態改變文檔結構的話,我們直接F12 復制出來的 xpath是不准的。那么有人 會說 HtmlAgilityPack 我已經用習慣了,不想 重新學習Jumony 。這里我告訴你錯了,根本就需要重新學習,如果你會jquery 的話。常用功能語法基本一樣,還支持拉姆達表達式,爽的一逼。
我們來看看 怎么使用Jumony 解析 解鎖結果吧。
var document = jumony.LoadDocument(url); var list = document.Find("#b_results .b_algo").ToList().Select(t => t.ToString()).ToList();
兩行代碼搞定,還直接轉成了list集合。在頁面循環加載就ok了。
站內下的某個用戶內搜索
我個人覺得 這是個蠻實用的功能,我們有時候 寫了博客(很多時候我們寫博客就是把自己怕會忘記的知識點 整理記錄),而后期找不到。那么通過這個功能 可以很好的解決我們的問題。我們不想全站搜索,只搜索自己的內容就可以了。
頁面還是用全站的搜索頁面,我們直接在搜索關鍵詞上做手腳就可以了。比如,我們想搜索 zhaopei 用戶下的內容,那么我們可以要搜索的關鍵字前面加上 blog:zhaopei 那么完整的搜索關鍵字就成了 blog:zhaopei 關鍵字
那么 我們要做的就是 在用戶頁面 搜索 就在關鍵字 前面加上 blog:用戶名 我們在搜索 頁面解析的時候 需要做的就是 分解關鍵字 blog:用戶名 關鍵字 先用空格 分割 然后如果中間有 空格的話 ,然后判斷 前面五個字符是不是 blog: 然后截取 到用戶名和 關鍵字。
我們下面具體看看 在Lucene.net 和 必應搜索里面是怎么做的。
1.Lucene.net
#region 加載 Lucene.net 的搜索結果 /// <summary> /// 加載 Lucene.net 的搜索結果 /// </summary> /// <returns></returns> public ActionResult ShowLuceneResult() { if (!Request.QueryString.AllKeys.Contains("key")) return null; string key = Request.QueryString["key"]; var zhankey = key.Split(' ');//分割關鍵字 var blogName = string.Empty; if (zhankey.Length >= 2) { var str = zhankey[0].Trim(); if (str.Length > 6 && str.Substring(0, 5) == "blog:") blogName = str.Substring(5);//取得用戶名 } string userid = Request.QueryString.AllKeys.Contains("userid") ? Request.QueryString["userid"] : ""; //這里判斷是否 用戶名不為空 然后取得用戶對應的 用戶ID (因為 我在做Lucene 是用用戶id 來標記的) if (!string.IsNullOrEmpty(blogName)) { key = key.Substring(key.IndexOf(' ')); var userinfo = CacheData.GetAllUserInfo().Where(t => t.UserName == blogName).FirstOrDefault(); if (null != userinfo) userid = userinfo.Id.ToString(); } string pIndex = Request.QueryString.AllKeys.Contains("p") ? Request.QueryString["p"] : ""; int PageIndex = 1; int.TryParse(pIndex, out PageIndex); int PageSize = 10; var searchlist = PanGuLuceneHelper.instance.Search(userid, key, PageIndex, PageSize); return PartialView(searchlist); } #endregion
2. 必應搜索
#region 加載 bing 的搜索結果 /// <summary> /// 加載 bing 的搜索結果 /// </summary> /// <returns></returns> public ActionResult ShowBingResult() { if (!Request.QueryString.AllKeys.Contains("key")) return null; string key = Request.QueryString["key"];//搜索關鍵字 JumonyParser jumony = new JumonyParser(); //http://cn.bing.com/search?q=AJAX+site%3ablog.haojima.net&first=11&FORM=PERE string pIndex = Request.QueryString.AllKeys.Contains("p") ? Request.QueryString["p"] : ""; int PageIndex = 1; int.TryParse(pIndex, out PageIndex); PageIndex--; //如:blog:JeffreyZhao 博客 var zhankey = key.Split(' ');//先用空格分割 var blogName = string.Empty; if (zhankey.Length >= 2) { var str = zhankey[0].Trim(); if (str.Length > 6 && str.Substring(0, 5) == "blog:") blogName = "/" + str.Substring(5);//這里取得 用戶名 } if (!string.IsNullOrEmpty(blogName)) key = key.Substring(key.IndexOf(' ')); //如: var url = "http://cn.bing.com/search?q=" + key + "+site:" + siteUrl + blogName + "&first=" + PageIndex + "1&FORM=PERE"; var document = jumony.LoadDocument(url); var list = document.Find("#b_results .b_algo").ToList().Select(t => t.ToString()).ToList(); var listli = document.Find("li.b_pag nav ul li"); if (PageIndex > 0 && listli.Count() == 0) return null; if (listli.Count() > 1) { var text = document.Find("li.b_pag nav ul li").Last().InnerText(); int npage = -1; if (text == "下一頁") { if (listli.Count() > 1) { var num = listli.ToList()[listli.Count() - 2].InnerText(); int.TryParse(num, out npage); } } else int.TryParse(text, out npage); if (npage <= PageIndex) list = null; } return PartialView(list); } #endregion
看看 我們的搜索結果的效果圖吧。

總結
首先 搜索是必不可少的功能,但又不是主要功能。那么我們可以直接用lucene.net 來做搜索,然后用必應搜索做備用。但是 用必應 有個弊端。就是 如果我們 的文章頁面 被用戶自己刪除了,而 必應已經收錄了,那么 我們在搜索結果頁面 點擊 可能就是404 或 500 了。當然 我們自己的 lucene.net 也會有這個 問題,我們可以在用戶刪除 文章的時候 也刪除 對應的那天搜索索引就好了。
演示地址:http://blog.haojima.net/Search/Index?key=blog:zhaopei 博客&p=1
如果您對本篇文章感興趣,那就麻煩您點個贊,您的鼓勵將是我的動力。 當然您還可以加入QQ群:討論。
如果您有更好的處理方式,希望不要吝嗇賜教。
一步步開發自己的博客 .NET版系列:http://www.cnblogs.com/zhaopei/tag/Hi-Blogs/
本文鏈接:http://www.cnblogs.com/zhaopei/p/4783986.html