各位朋友,謝謝你們的支持,因爲文件過大,有考慮到版權的問題,故沒有提供下載,本人已創建一個搜索技術交流羣:77570783,源代碼已上傳至羣共享,須要的朋友,請自行下載!html
首先自問自答幾個問題,以讓各位看官瞭解寫此文的目的web
什麼是站內搜索?與通常搜索的區別?
不少網站都有搜索功能,不少都是用SQL語句的Like實現的,可是Like沒法作到模糊匹配(例如我搜索「.net學習」,若是有「.net的學習」,Like就沒法搜索到,這明顯不符合需求,可是站內搜索就能作到),另外Like會形成全盤掃描,會對數據庫形成很大壓力,爲何不用數據庫全文檢索,跟普通SQL同樣,很傻瓜,靈活性不行數據庫
爲何不用百度、google的站內搜索?
畢竟是別人的東西,用起來確定會受制於人(哪天你的網站火了,它看你不爽了,就可能被K),主要仍是索引的不夠及時,網站新的內容,須要必定時間才能被索引到,而且用戶的體驗也不太好函數
最近改造了《動力起航》的站內搜索的功能,它其實已經有站內搜索的功能,可是是用like來實現的,改造此功能是本着在儘量少的修改網站的源代碼的狀況下去改造此功能以及此站內搜索功能能夠很好的移植到其餘項目的原則來編寫!本文有借鑑其餘大神及園友的技術,在此謝謝!學習
站內搜索使用的技術
Log4Net 日誌記錄網站
lucene.Net 全文檢索開發包,只能檢索文本信息ui
分詞(lucene.Net提供StandardAnalyzer一元分詞,按照單個字進行分詞,一個漢字一個詞)google
盤古分詞 基於詞庫的分詞,能夠維護詞庫spa
首先咱們新增的SearchHelper類須要將其作成一個單例,使用單例是由於:有許多地方須要使用使用,但咱們同時又但願只有一個對象去操做,具體代碼以下:.net
#region 建立單例 // 定義一個靜態變量來保存類的實例 private static SearchHelper uniqueInstance; // 定義一個標識確保線程同步 private static readonly object locker = new object(); // 定義私有構造函數,使外界不能建立該類實例 private SearchHelper() { } /// <summary> /// 定義公有方法提供一個全局訪問點,同時你也能夠定義公有屬性來提供全局訪問點 /// </summary> /// <returns></returns> public static SearchHelper GetInstance() { // 當第一個線程運行到這裏時,此時會對locker對象 "加鎖", // 當第二個線程運行該方法時,首先檢測到locker對象爲"加鎖"狀態,該線程就會掛起等待第一個線程解鎖 // lock語句運行完以後(即線程運行完以後)會對該對象"解鎖" lock (locker) { // 若是類的實例不存在則建立,不然直接返回 if (uniqueInstance == null) { uniqueInstance = new SearchHelper(); } } return uniqueInstance; } #endregion
其次,使用Lucene.Net須要將被搜索的進行索引,而後保存到索引庫以便被搜索,咱們引入了「生產者,消費者模式」. 生產者就是當咱們新增,修改或刪除的時候咱們就須要將其在索引庫進行相應的操做,咱們將此操做交給另外一個線程去處理,這個線程就是咱們的消費者,使用「生產者,消費者模式」是由於:索引庫使用前需解鎖操做,使用完成以後必須解鎖,因此只能有一個對象對索引庫進行操做,避免數據混亂,因此要使用生產者,消費者模式
首先咱們來看生產者,代碼以下:
private Queue<IndexJob> jobs = new Queue<IndexJob>(); //任務隊列,保存生產出來的任務和消費者使用,不使用list避免移除時數據混亂問題 /// <summary> /// 任務類,包括任務的Id ,操做的類型 /// </summary> class IndexJob { public int Id { get; set; } public JobType JobType { get; set; } } /// <summary> /// 枚舉,操做類型是增長仍是刪除 /// </summary> enum JobType { Add, Remove } #region 任務添加 public void AddArticle(int artId) { IndexJob job = new IndexJob(); job.Id = artId; job.JobType = JobType.Add; logger.Debug(artId + "加入任務列表"); jobs.Enqueue(job);//把任務加入商品庫 } public void RemoveArticle(int artId) { IndexJob job = new IndexJob(); job.JobType = JobType.Remove; job.Id = artId; logger.Debug(artId + "加入刪除任務列表"); jobs.Enqueue(job);//把任務加入商品庫 } #endregion
下面是消費者,消費者咱們單獨一個線程來進行任務的處理:
/// <summary> /// 索引任務線程 /// </summary> private void IndexOn() { logger.Debug("索引任務線程啓動"); while (true) { if (jobs.Count <= 0) { Thread.Sleep(5 * 1000); continue; } //建立索引目錄 if (!System.IO.Directory.Exists(IndexDic)) { System.IO.Directory.CreateDirectory(IndexDic); } FSDirectory directory = FSDirectory.Open(new DirectoryInfo(IndexDic), new NativeFSLockFactory()); bool isUpdate = IndexReader.IndexExists(directory); logger.Debug("索引庫存在狀態" + isUpdate); if (isUpdate) { //若是索引目錄被鎖定(好比索引過程當中程序異常退出),則首先解鎖 if (IndexWriter.IsLocked(directory)) { logger.Debug("開始解鎖索引庫"); IndexWriter.Unlock(directory); logger.Debug("解鎖索引庫完成"); } } IndexWriter writer = new IndexWriter(directory, new PanGuAnalyzer(), !isUpdate, Lucene.Net.Index.IndexWriter.MaxFieldLength.UNLIMITED); ProcessJobs(writer); writer.Close(); directory.Close();//不要忘了Close,不然索引結果搜不到 logger.Debug("所有索引完畢"); } } private void ProcessJobs(IndexWriter writer) { while (jobs.Count != 0) { IndexJob job = jobs.Dequeue(); writer.DeleteDocuments(new Term("number", job.Id.ToString())); //若是「添加文章」任務再添加, if (job.JobType == JobType.Add) { BLL.article bll = new BLL.article(); Model.article art = bll.GetArticleModel(job.Id); if (art == null)//有可能剛添加就被刪除了 { continue; } string channel_id = art.channel_id.ToString(); string title = art.title; DateTime time = art.add_time; string content = Utils.DropHTML(art.content.ToString()); string Addtime = art.add_time.ToString("yyyy-MM-dd"); Document document = new Document(); //只有對須要全文檢索的字段才ANALYZED document.Add(new Field("number", job.Id.ToString(), Field.Store.YES, Field.Index.NOT_ANALYZED)); document.Add(new Field("title", title, Field.Store.YES, Field.Index.ANALYZED, Lucene.Net.Documents.Field.TermVector.WITH_POSITIONS_OFFSETS)); document.Add(new Field("channel_id", channel_id, Field.Store.YES, Field.Index.NOT_ANALYZED)); document.Add(new Field("Addtime", Addtime, Field.Store.YES, Field.Index.NOT_ANALYZED)); document.Add(new Field("content", content, Field.Store.YES, Field.Index.ANALYZED, Lucene.Net.Documents.Field.TermVector.WITH_POSITIONS_OFFSETS)); writer.AddDocument(document); logger.Debug("索引" + job.Id + "完畢"); } } } #endregion
以上咱們就把索引庫創建完畢了,接下來就是進行搜索了,搜索操做裏面包括對搜索關鍵詞進行分詞,其次是搜索內容搜索詞高亮顯示,下面就是搜索的代碼:
#region 從索引搜索結果 /// <summary> /// 從索引搜索結果 /// </summary> public List<Model.article> SearchIndex(string Words, int PageSize, int PageIndex, out int _totalcount) { _totalcount = 0; Dictionary<string, string> dic = new Dictionary<string, string>(); BooleanQuery bQuery = new BooleanQuery(); string title = string.Empty; string content = string.Empty; title = GetKeyWordsSplitBySpace(Words); QueryParser parse = new QueryParser(Lucene.Net.Util.Version.LUCENE_29, "title", new PanGuAnalyzer()); Query query = parse.Parse(title); parse.SetDefaultOperator(QueryParser.Operator.AND); bQuery.Add(query, BooleanClause.Occur.SHOULD); dic.Add("title", Words); content = GetKeyWordsSplitBySpace(Words); QueryParser parseC = new QueryParser(Lucene.Net.Util.Version.LUCENE_29, "content", new PanGuAnalyzer()); Query queryC = parseC.Parse(content); parseC.SetDefaultOperator(QueryParser.Operator.AND); bQuery.Add(queryC, BooleanClause.Occur.SHOULD); dic.Add("content", Words); if (bQuery != null && bQuery.GetClauses().Length > 0) { return GetSearchResult(bQuery, dic, PageSize, PageIndex, out _totalcount); } return null; } /// <summary> /// 獲取 /// </summary> /// <param name="bQuery"></param> private List<Model.article> GetSearchResult(BooleanQuery bQuery, Dictionary<string, string> dicKeywords, int PageSize, int PageIndex, out int totalCount) { List<Model.article> list = new List<Model.article>(); FSDirectory directory = FSDirectory.Open(new DirectoryInfo(IndexDic), new NoLockFactory()); IndexReader reader = IndexReader.Open(directory, true); IndexSearcher searcher = new IndexSearcher(reader); TopScoreDocCollector collector = TopScoreDocCollector.create(1000, true); Sort sort = new Sort(new SortField("Addtime", SortField.DOC, true)); searcher.Search(bQuery, null, collector); totalCount = collector.GetTotalHits();//返回總條數 TopDocs docs = searcher.Search(bQuery, (Filter)null, PageSize * PageIndex, sort); if (docs != null && docs.totalHits > 0) { for (int i = 0; i < docs.totalHits; i++) { if (i >= (PageIndex - 1) * PageSize && i < PageIndex * PageSize) { Document doc = searcher.Doc(docs.scoreDocs[i].doc); Model.article model = new Model.article() { id = int.Parse(doc.Get("number").ToString()), title = doc.Get("title").ToString(), content = doc.Get("content").ToString(), add_time = DateTime.Parse(doc.Get("Addtime").ToString()), channel_id = int.Parse(doc.Get("channel_id").ToString()) }; list.Add(SetHighlighter(dicKeywords, model)); } } } return list; } /// <summary> /// 設置關鍵字高亮 /// </summary> /// <param name="dicKeywords">關鍵字列表</param> /// <param name="model">返回的數據模型</param> /// <returns></returns> private Model.article SetHighlighter(Dictionary<string, string> dicKeywords, Model.article model) { SimpleHTMLFormatter simpleHTMLFormatter = new PanGu.HighLight.SimpleHTMLFormatter("<font color=\"red\">", "</font>"); Highlighter highlighter = new PanGu.HighLight.Highlighter(simpleHTMLFormatter, new Segment()); highlighter.FragmentSize = 250; string strTitle = string.Empty; string strContent = string.Empty; dicKeywords.TryGetValue("title", out strTitle); dicKeywords.TryGetValue("content", out strContent); if (!string.IsNullOrEmpty(strTitle)) { string title = model.title; model.title = highlighter.GetBestFragment(strTitle, model.title); if (string.IsNullOrEmpty(model.title)) { model.title = title; } } if (!string.IsNullOrEmpty(strContent)) { string content = model.content; model.content = highlighter.GetBestFragment(strContent, model.content); if (string.IsNullOrEmpty(model.content)) { model.content = content; } } return model; } /// <summary> /// 處理關鍵字爲索引格式 /// </summary> /// <param name="keywords"></param> /// <returns></returns> private string GetKeyWordsSplitBySpace(string keywords) { PanGuTokenizer ktTokenizer = new PanGuTokenizer(); StringBuilder result = new StringBuilder(); ICollection<WordInfo> words = ktTokenizer.SegmentToWordInfos(keywords); foreach (WordInfo word in words) { if (word == null) { continue; } result.AppendFormat("{0}^{1}.0 ", word.Word, (int)Math.Pow(3, word.Rank)); } return result.ToString().Trim(); } #endregion
以上咱們的站內搜索的SearchHelper類就創建好了,下面來說講如何使用,此類提供如下幾個方法對外使用:
在Global裏面啓動消費者線程:
protected void Application_Start(object sender, EventArgs e) { //啓動索引庫的掃描線程(生產者) SearchHelper.GetInstance().CustomerStart(); }
在需被搜索的新增或修改處添加下面方法:
SearchHelper.GetInstance().AddArticle(model.id);
在需被搜索的刪除處添加下面方法:
SearchHelper.GetInstance().RemoveArticle(model.id);
搜索的時候使用下面的方法便可:
public List<Model.article> SearchIndex(string Words, int PageSize, int PageIndex, out int _totalcount)
以上就是整個站內搜索的所有代碼,SearchHelper幫助類下載地址:http://files.cnblogs.com/beimeng/SearchHelper.rar
原本想直接提供改造了《動力起航》的源代碼,這樣就能夠直接看到效果了,一方面因爲文件過大,另外一方面不知道是否是會侵權,全部沒有提供下載.若是有須要的朋友能夠留下郵箱我將發給你,但僅供學習交流之用,誤用作商業用途,以上若是有侵權等問題還請及時告知我,以便我及時更正!
很榮幸此文能上最多推薦,多謝你們的支持,因爲索要改造了《動力起航》的源代碼的園友不少,一一發給你們有點麻煩,在考慮是否放到網盤提供你們下載是否是更方便一些,可是不知道這樣會不會有侵權之嫌啊,求各位給個建議,若是能夠我就上傳網盤了,不行的話就只能一個一個發給你們了!
最好若是以爲好的話!請給個推薦啊~~~~親!!!!