前言
各位朋友,謝謝大家的支持,由於文件過大,有考慮到版權的問題,故沒有提供下載,本人已建立一個搜索技術交流群:77570783,源代碼已上傳至群共享,需要的朋友,請自行下載!
首先自問自答幾個問題,以讓各位看官了解寫此文的目的
什么是站內搜索?與一般搜索的區別?
很多網站都有搜索功能,很多都是用SQL語句的Like實現的,但是Like無法做到模糊匹配(例如我搜索“.net學習”,如果有“.net的學習”,Like就無法搜索到,這明顯不符合需求,但是站內搜索就能做到),另外Like會造成全盤掃描,會對數據庫造成很大壓力,為什么不用數據庫全文檢索,跟普通SQL一樣,很傻瓜,靈活性不行
為什么不用百度、google的站內搜索?
畢竟是別人的東西,用起來肯定會受制於人(哪天你的網站火了,它看你不爽了,就可能被K),主要還是索引的不夠及時,網站新的內容,需要一定時間才能被索引到,並且用戶的體驗也不太好
最近改造了《動力起航》的站內搜索的功能,它其實已經有站內搜索的功能,但是是用like來實現的,改造此功能是本着在盡可能少的修改網站的源代碼的情況下去改造此功能以及此站內搜索功能可以很好的移植到其他項目的原則來編寫!本文有借鑒其他大神及園友的技術,在此謝謝!
功能簡介
站內搜索使用的技術
Log4Net 日志記錄
lucene.Net 全文檢索開發包,只能檢索文本信息
分詞(lucene.Net提供StandardAnalyzer一元分詞,按照單個字進行分詞,一個漢字一個詞)
盤古分詞 基於詞庫的分詞,可以維護詞庫
具體詳解
首先我們新增的SearchHelper類需要將其做成一個單例,使用單例是因為:有許多地方需要使用使用,但我們同時又希望只有一個對象去操作,具體代碼如下:
#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
本來想直接提供改造了《動力起航》的源代碼,這樣就可以直接看到效果了,一方面由於文件過大,另一方面不知道是不是會侵權,所有沒有提供下載.如果有需要的朋友可以留下郵箱我將發給你,但僅供學習交流之用,誤用做商業用途,以上如果有侵權等問題還請及時告知我,以便我及時更正!
很榮幸此文能上最多推薦,多謝大家的支持,由於索要改造了《動力起航》的源代碼的園友很多,一一發給大家有點麻煩,在考慮是否放到網盤提供大家下載是不是更方便一些,但是不知道這樣會不會有侵權之嫌啊,求各位給個建議,如果可以我就上傳網盤了,不行的話就只能一個一個發給大家了!
最好如果覺得好的話!請給個推薦啊~~~~親!!!!