全站搜索实战应用(Lucene.Net+盘古分词)
首先自问自答几个问题,以让各位看官了解写此文的目的
什么是站内搜索?与一般搜索的区别?
多网站都有搜索功能,很多都是用SQL语句的Like实现的,但是Like无法做到模糊匹配(例如我搜索“.net学习”,如果有“.net的学习”,Like就无法搜索到,这明显不符合需求,但是站内搜索就能做到),另外Like会造成全盘扫描,会对数据库造成很大压力,为什么不用数据库全文检索,跟普通SQL一样,很傻瓜,灵活性不行
本文有借鉴其他大神及园友的技术,在此谢谢!
站内搜索使用的技术
- 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 string Id { get; set; } public JobType JobType { get; set; } public HaiMi.Data.Article art { get; set; }//这里为项目中的文章实体类,根据需要自己定义 } /// <summary> /// 枚举,操作类型是增加还是删除 /// </summary> enum JobType { Add, Remove } #region 任务添加 public void AddArticle(HaiMi.Data.Article model) { IndexJob job = new IndexJob(); job.Id = model.Id; job.JobType = JobType.Add; job.art = model; logger.Debug(model.Id + "加入任务列表"); jobs.Enqueue(job);//把任务加入商品库 } public void RemoveArticle(HaiMi.Data.Article model) { IndexJob job = new IndexJob(); job.JobType = JobType.Remove; job.art = model; job.Id = model.Id; logger.Debug(model.Id + "加入删除任务列表"); jobs.Enqueue(job); } #endregion
下面是消费者,消费者我们单独一个线程来进行任务的处理:
#region 任务索引 /// <summary> /// 索引任务线程 /// </summary> private void IndexOn() { try { 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.Dispose(); directory.Dispose(); //writer.Close(); //directory.Close(); logger.Debug("全部索引完毕"); } } catch (Exception ex) { logger.Error(ex.Message,ex); } } 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) { HaiMi.Data.Article art = new HaiMi.Data.Article(); //这里的数据体通过任务线程传递 art = job.art; if (art == null) { continue; } string channel_id = art.Categoryid.ToString(); string title = art.Title; DateTime time = art.Createtime; string content = HtmlHelper.DropHTML(art.Content.ToString()); string Addtime = art.Createtime.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 从索引搜索结果 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.Query(title); Query query = parse.Parse(title); parse.DefaultOperator = QueryParser.Operator.AND; //parse.SetDefaultOperator(QueryParser.Operator.AND); bQuery.Add(query, 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.DefaultOperator = QueryParser.Operator.AND; bQuery.Add(queryC, 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> /// <returns></returns> 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.TotalHits; //返回总条数 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 = doc.Get("number").ToString(), title = doc.Get("title").ToString(), content = doc.Get("content").ToString(), add_time = DateTime.Parse(doc.Get("Addtime").ToString()), channel_id = 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里面启动消费者线程:
/// <summary>
/// 启动消费者线程
/// </summary>
public void CustomerStart()
{
log4net.Config.XmlConfigurator.Configure();
PanGu.Segment.Init(PanGuPath);
Thread threadIndex = new Thread(IndexOn);
threadIndex.IsBackground = true;
threadIndex.Start();
}
protected void Application_Start(object sender, EventArgs e) { //启动索引库的扫描线程(生产者) SearchHelper.GetInstance().CustomerStart(); }
在需被搜索的新增或修改处添加下面方法(model为你项目中的文章实体类):
SearchHelper.GetInstance().AddArticle(model);
在需被搜索的删除处添加下面方法:
SearchHelper.GetInstance().RemoveArticle(model);
搜索的时候使用下面的方法即可:
List<Model.article> list = new List<Model.article>();
try
{
int _totalcount = 0;
int PageSize = 10;
int PageIndex = 1;
list= SearchHelper.GetInstance().SearchIndex(words, PageSize, PageIndex, out _totalcount);
}
catch (Exception ex)
{
throw;
}
return View(list);
最后配置一下索引存放目录和分词文件目录
/// <summary> /// 索引存放目录 /// </summary> protected string IndexDic { get { //return Utils.GetXmlMapPath(DTKeys.FILE_INDEXDICPATH_XML_CONFING); return System.AppDomain.CurrentDomain.BaseDirectory + "\\lucenedir"; } } /// <summary> /// 盘古分词配置目录 /// </summary> protected string PanGuPath { get { //return Utils.GetXmlMapPath(DTKeys.FILE_PANGU_XML_CONFING); return System.AppDomain.CurrentDomain.BaseDirectory + "\\PanGu\\PanGu.xml"; } }
分词可以从网上下载,然后在PanGu.xml中配置DictionaryPath为你的分词存放路径
<DictionaryPath>..\Dictionaries</DictionaryPath>
以上就是整个站内搜索的全部代码,SearchHelper帮助类下载地址:https://files.cnblogs.com/beimeng/SearchHelper.rar
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构