Lucene.net站內搜索—5、搜索引擎第一版實現


目錄

Lucene.net站內搜索—1、SEO優化
Lucene.net站內搜索—2、Lucene.Net簡介和分詞
Lucene.net站內搜索—3、最簡單搜索引擎代碼
Lucene.net站內搜索—4、搜索引擎第一版技術儲備(簡單介紹Log4Net、生產者消費者模式)
Lucene.net站內搜索—5、搜索引擎第一版實現
Lucene.net站內搜索—6、站內搜索第二版

  •  站內搜索模塊:生產者、消費者,多線程。復習多線程,用多線程做一個winform的生產者、消費者的例子,有任務的時候(點按鈕給整數)就處理任務,沒任務的時候就每次掃描都說“還是沒任務,睡會再看”,用Sleep模擬耗時操作,線程中操作UI線程的代碼見備注。
  •  派一個人來管理索引庫,想向索引庫中寫數據的地方都向這個人來發出請求。
  •  由於索引庫同時只能有一個IndexWriter進行寫,所以有一個消費者線程一直保持對IndexWriter寫的狀態,有新任務進入的時候對IndexWriter寫入。如果IndexWriter一直保持打開狀態的話,新添加的文檔是不會被搜索到的,因此必須處理完隊列中的任務后關閉writer,然后下次while循環掃描的時候判斷如果隊列匯總沒有任務,則sleep5秒鍾后再判斷,防止不斷判斷給服務器cpu壓力
  •  IndexManager做成單例。維持一個任務的Queue,Thread thread = new Thread(ScanThread); thread.Start();啟動一個線程,在ScanThread方法中不斷遍歷Queue ,當有新任務加入的時候把新任務加入索引庫,當要刪除文章的時候也是加入一個jobType == JobType.Delete的內容。UpdateDocument
  • 文章的更新和刪除。

1、線程訪問UI線程:

 ParameterizedThreadStart threadStart = (obj) =>
                    {
                        txtLog.AppendText(obj + "\n");
                    };
                    txtLog.Invoke(threadStart, item);

詳細代碼如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Threading;
using log4net;
using System.Configuration;
using System.Web.Hosting;
using RuPeng.Utils;
using RuPengSite.DataTier.DataSetThreadTableAdapters;
using System.Text;
using Lucene.Net.Store;
using Lucene.Net.Index;
using System.IO;
using Lucene.Net.Analysis.PanGu;
using Lucene.Net.Documents;
namespace RuPengSite.Search
{
    public class IndexManager
    {
        public readonly static IndexManager Instance = new IndexManager();
        private HashSet<IndexJobItem> jobs = new HashSet<IndexJobItem>();//任務的集合
        private bool isStopped;//任務是否停止
        
        private static ILog log = LogManager.GetLogger(typeof(IndexManager));
        private IndexManager()
        {            
        }
        //啟動任務
        public void Start()
        {
            isStopped = false;
            Thread thread = new Thread(ScanThread);
            thread.Start();            
        }
        //停止任務
        public void Stop()
        {
            isStopped = true;
        }
        /// <summary>
        /// 掃描線程
        /// </summary>
        private void ScanThread()
        {
            //如果停止,則不再無限循環
            while (!isStopped)
            {
                Thread.Sleep(5000);//休息5秒鍾,盡可能多的累積任務
                if (jobs.Count <= 0)
                {
                    continue;//如果沒任務繼續睡
                }
                log.Debug("開始索引預處理");
                string indexPath = SearchHelper.GetSearchIndexFullPath();
                log.Debug("索引路徑是:" + indexPath);
                FSDirectory directory = FSDirectory.Open(new DirectoryInfo(indexPath), new NativeFSLockFactory());
                //判斷索引目錄是否已經存在
                bool isUpdate = IndexReader.IndexExists(directory);
                log.Debug("索引路徑存在狀態是" + isUpdate);
                if (isUpdate)
                {
                    //如果索引目錄被鎖定(比如索引過程中程序異常退出),則首先解鎖
                    if (IndexWriter.IsLocked(directory))
                    {
                        log.Debug("開始解鎖索引路徑");
                        IndexWriter.Unlock(directory);
                    }
                }
                IndexWriter writer = new IndexWriter(directory, new PanGuAnalyzer(), !isUpdate, 
                    Lucene.Net.Index.IndexWriter.MaxFieldLength.UNLIMITED);
                try
                {
                    ProcessJobItems(directory, writer);
                }
                finally
                {
                    log.Debug("開始關閉reader、writer");
                    writer.Close();
                    directory.Close();
                    log.Debug("完成關閉reader、writer");
                }                       
            }
        }
        /// <summary>
        /// 處理隊列中的任務
        /// </summary>
        /// <param name="directory"></param>
        /// <param name="writer"></param>
        private void ProcessJobItems(FSDirectory directory, IndexWriter writer)
        {
            log.Debug("開始處理隊列中的"+jobs.Count+"個任務");
            foreach (var jobItem in jobs.ToArray())//轉換為數組,避免讀的時候不能修改的問題
            {
                try
                {
                    ProcessJobItem(writer, jobItem);
                    jobs.Remove(jobItem);//將處理完成的任務移除
                }
                catch (Exception ex)
                {
                    log.Error("對任務進行處理失敗" + jobItem, ex);
                }                
            }
            log.Debug("隊列中的任務處理完畢");
        }
        private static void ProcessJobItem(IndexWriter writer, IndexJobItem jobItem)
        {
            long threadId = jobItem.ThreadId;
            JobType jobType = jobItem.ItemType;
            string url = SearchHelper.GetThreadUrl(threadId);
            if (jobType == JobType.Delete)//判斷任務的類型
            {
                log.Debug("將帖子從索引中移除,threadId=" + threadId);
                writer.DeleteDocuments(new Term(SearchHelper.URL, url));//刪除舊的收錄
            }
            else if (jobType == JobType.Add)
            {
                writer.DeleteDocuments(new Term(SearchHelper.URL, url));//刪除舊的收錄
                var threads = new rp_threadsTableAdapter().GetDataById(threadId);
                if (threads.Count <= 0)
                {
                    log.Debug("id為"+threadId+"的帖子不存在!");
                    return;
                }
                string body = SearchHelper.GetThreadContent(threadId);//帖子內容
                string title = threads.Single().Subject;//主題
                Document document = new Document();
                document.Add(new Field(SearchHelper.URL, url, Field.Store.YES, Field.Index.NOT_ANALYZED));
                document.Add(new Field(SearchHelper.TITLE, title, Field.Store.YES, Field.Index.NOT_ANALYZED));
                document.Add(new Field(SearchHelper.BODY, body, Field.Store.YES, Field.Index.ANALYZED,
Lucene.Net.Documents.Field.TermVector.WITH_POSITIONS_OFFSETS)); writer.AddDocument(document); log.Debug(
"索引帖子" + threadId+"完成"); } else { throw new Exception("錯誤的jobType:" + jobType); } } /// <summary> /// 添加帖子的索引任務 /// </summary> /// <param name="threadId"></param> public void AddThread(long threadId) { IndexJobItem jobItem = new IndexJobItem() { ItemType=JobType.Add,ThreadId=threadId}; jobs.Add(jobItem); } /// <summary> /// 移除帖子的索引任務 /// </summary> /// <param name="threadId"></param> public void DeleteThread(long threadId) { IndexJobItem jobItem = new IndexJobItem() { ItemType = JobType.Delete, ThreadId = threadId }; jobs.Add(jobItem); } class IndexJobItem { public JobType ItemType { get; set; } public long ThreadId { get; set; } public override bool Equals(object obj) { IndexJobItem item = obj as IndexJobItem; if (item == null) { return false; } return this.ItemType==item.ItemType&&this.ThreadId==item.ThreadId; } public override int GetHashCode() { return ToString().GetHashCode(); } public override string ToString() { return ItemType+":"+ThreadId; } } enum JobType {Delete,Add }//任務類型 } }

多條件查詢

我看了下淘寶,淘寶的站內搜索只實現了且條件:

或條件查詢可以來看看百度,當然,百度是同時采用了且條件和或條件查詢的:

在標題和正文中查找

PhraseQuery queryMsg = new PhraseQuery();

foreach (string word in CommonHelper.SplitWords(txtKW.Text))
            {
                queryMsg.Add(new Term("msg", word));
            }

queryMsg.SetSlop(100);
PhraseQuery queryTitle = new PhraseQuery();

foreach (string word in CommonHelper.SplitWords(txtKW.Text))
            {
                queryTitle.Add(new Term("title", word));
            }

queryTitle.SetSlop(100);
BooleanQuery query = new BooleanQuery();
query.Add(queryMsg, BooleanClause.Occur.SHOULD); query.Add(queryTitle, BooleanClause.Occur.SHOULD);

BooleanQuery相當於盛放其他查詢條件的容器,類似於div。第二個參數:Must為必須有,Must_Not為必須沒有,Should為可以有

高亮顯示

  • 高亮顯示,只顯示包含關鍵詞的部分。參考盤古分詞的文檔。
  • 從網上、文檔找來的代碼不用細讀每行代碼,先把它拿過來運行通過再說。
  • 不用每次改代碼都重啟,在項目的屬性頁面的Web中選中“啟用編輯並繼續(Enable Edit and Continue)”
  • 把font控制顏色改成通過style控制顏色,原則“不要在正文中控制格式”。不推薦使用font、b、i、br等。
private static String highLight(string keyword,String content)
        {
            PanGu.HighLight.SimpleHTMLFormatter formatter = new PanGu.HighLight.SimpleHTMLFormatter("<font color='red'>", "</font>");
            PanGu.HighLight.Highlighter highlighter = new PanGu.HighLight.Highlighter(formatter, new Segment());
            highlighter.FragmentSize = 500;
            string msg = highlighter.GetBestFragment(keyword,content);
if (string.IsNullOrEmpty(msg)) { return content; } else { return msg; } } String hightlightTitle = highLight(keyword, title); String hightlightBody = HttpUtility.HtmlEncode(body);//防止XSS攻擊 hightlightBody = highLight(keyword, hightlightBody);

路徑可配置化

  • 連接配置信息放到Web.Config的ConnectionStrings段中,而普通的自定義配置則可以寫到AppSettings段中,哪些需要配置:索引的路徑,被索引的網站url,索引的時間間隔。
  • 讀取string indexPath = ConfigurationManager.AppSettings["IndexPath"],使用ConfigurationManager添加引用System.Configuration
  • 使用request.MapPath或者Server.MapPath把相對於網站根路徑的路徑轉換為絕對路徑(不是轉換為http://www.baidu.com/a.aspx,轉換為c:/baidu_com/a.aspx)。在定時任務等不在Http線程中取HttpContext.Current得到的是null,因此在定時任務中不能用HttpContext.Current.Server.MapPath方法來轉換,要用HostingEnvironment.MapPath,因此可以在其他地方也用HostingEnvironment.MapPath。
  • 修改Web.config會造成IIS重啟,這樣會立即加載新的任務

解決:地址無法發給好友

我們先看下淘寶的站內搜索:

細心看,我們會發現url是一連串的字符串,可以肯定這是采用了get請求的方式。

  • 用戶沒法把搜索結果頁面發給好友,要用Get提交,這樣才能得到搜索頁面地址。如果采用Get方式的話,要刪掉form的runat=server,變成HTML的form、method改為get,所有控件都要用HTML控件。因為只有去掉runat=server的form,才會完全去掉ViewState
  • 注意input不能只指定id,而應該指定name,否則不會出現在querystring中。Id是供Javascript用的,name是供querystring/Request用的。對於type=submit的input來說,只有被點擊的input的name、value才會被提交給服務器。
  • method改為get
  • 1、要刪掉form的runat=server。(唯一去掉viewstate的方法)
  • 2、所有除了DataBound控件(比如GridView、Repeater等)都要用HTML控件。Repeater、ObjectDataSource之類控件不需要runat=server的form也可以,但是VS總是提示,去源代碼視圖拖放、讓他生成再手動刪掉。
  • 3、控件注意要給表單name屬性賦值。
  • 4、在后台Page_Load代碼中進行響應
  • 5、IsPostBack不再有用,只能通過判斷參數是否為空來判斷是否是提交的頁面。
  • 點搜索按鈕以后如何顯示搜索關鍵字:在aspx.cs中定義一個GetKeyWord方法,<input type="text" id="kw1" name="kw" value='<%=Request["kw"] %>'/>

只要有runat=server的form就會產生__VIEWSTATE等,所以去掉form的runat=server,這樣除了Repeater等少數控件之外服務端控件都沒法使用,只能使用html標簽。這是為什么說“要求高的互聯網項目不用服務端控件”。面試時候說:我在有的項目中沒有用服務端控件的例子。

為了能讓查詢參數顯示在地址欄中,方便傳播地址,把form的method改為get;因為ViewState太長,所以影響美觀,因此禁用ViewState;但是發現哪怕禁用ViewState,ViewState也沒有完全消失;研究發現,只有去掉form的runat=server后才能完全干掉ViewState;但是,一旦去掉form的runat=server后幾乎所有的WebForm控件都用不了(除了Repeater等少數幾個和input無關的之外),只能用html控件,然后在Page_Load中進行響應。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM