10.5 搜索的优化版

简介:

目录:ASP.NET MVC企业级实战目录

像www.verycd.com、博客园、淘宝、京东都有实现站内搜索功能,站内搜索无论在性能和用户体验上都非常不错,本节,通过使用Lucene.Net来实现站内搜索。

演示效果预览如下图10-22~10-24所示。

图10-22

 

图10-23

 

图10-24

在10.4节,已经完成了搜索的第一个版本,但是还有许多地方需要优化。比如说,我要统计关键词搜索的频率高的词,也即热词,以及像百度搜索那样,在输入关键字后,会自动把搜索相关的热词自动以下拉列表的形式带出来。还有诸如搜索结果分页,查看文章明细等。

10.5.1 热词统计

思路:

1、  首先,我们脑海里要明确一点:搜索关键字的统计,实时性是不高的。也就是说我们可以定期的去进行统计。

2、  客户的每一次搜索记录,我们都需要存起来,这样才能够统计得到。

从第1点,我们脑海中就会呈现一张汇总统计表,从第2点中,我们会想到使用一张搜索记录明细表。那方案就很明了了,只需要定期的从明细表中Group by查询,然后把查询结构放到汇总表中。怎么放到汇总表中?是直接Update更新吗?其实我们可以有更快捷的方式,那就是对汇总表先进行truncate,然后再进行insert操作。

表10-1 搜索汇总统计表SearchTotals

字段名称

字段类型

说明

Id

char(36)

主键,采用Guid方式存储

KeyWords

nvarchar(50)

搜索关键字

SearchCounts

int

搜索次数

表10-2 搜索明细表SearchDetails

字段名称

字段类型

说明

Id

char(36)

主键,采用Guid方式存储

KeyWords

nvarchar(50)

搜索关键字

SearchDateTime

datetime

搜索时间

操作步骤:

(1)在Models文件夹中,新建两个类SearchTotal、SearchDetail。

SearchTotal.cs代码:

复制代码
using System;
using System.ComponentModel.DataAnnotations;

namespace SearchDemo.Models
{
    public class SearchTotal
    {
        public Guid Id { get; set; }
        [StringLength(50)]
        public string KeyWords { get; set; }
        public int SearchCounts { get; set; }
    }
}
复制代码

SearchDetail.cs代码:

复制代码
using System;
using System.ComponentModel.DataAnnotations;

namespace SearchDemo.Models
{
    public class SearchDetail
    {
        public Guid Id { get; set; }
        [StringLength(50)]
        public string KeyWords { get; set; }
        public Nullable<DateTime> SearchDateTime { get; set; }
    }
}
复制代码

(2)修改SearchDemoContext类,新增了属性SearchTotal、SearchDetail。

复制代码
using System.Data.Entity;

namespace SearchDemo.Models
{
    public class SearchDemoContext : DbContext
    {
        public SearchDemoContext() : base("name=SearchDemoContext") { }
        public DbSet<Article> Article { get; set; }
        //下面两个属性是新增加的
        public DbSet<SearchTotal> SearchTotal { get; set; }
        public DbSet<SearchDetail> SearchDetail { get; set; }
    }
}
复制代码

3)更新数据库

由于修改了EF上下文,新增了两个模型类,所以需要进行迁移更新数据库操作。

将应用程序重新编译,然后选择工具->库程序包管理器->程序包管理控制台。

打开控制台,输入enable-migrations -force ,然后回车。回车后会在项目项目资源管理器中会出现Migrations文件夹,打开Configuration.cs 文件,将AutomaticMigrationsEnabled 值改为 true,然后在控制台中输入 update-database 运行。操作完成之后,会在数据库SearchDemo中多新建两张表SearchTotals、SearchDetails,而原来的Articles表保持不变。如图10-20所示。

 

图10-20

(4)保存搜索记录

用户在每次搜索的时候,要把搜索记录存入SearchDetails表中。为了方便,这里我是在用户每次点击搜索之后就立即往SearchDetails表中插入记录了,也就是同步操作,而实际上,如果为了提升搜索的效率,我们可以采用异步操作,即把搜索记录的数据先写入redis队列中,后台再开辟一个线程来监听redis队列,然后把队列中的搜索记录数据写入到数据表中。因为在每次点击搜索的时候,我们把记录往redis写和把记录直接往关系型数据库中写的效率是相差很大的。

 //先将搜索的词插入到明细表。
            SearchDetail _SearchDetail = new SearchDetail { Id = Guid.NewGuid(), KeyWords = kw, SearchDateTime = DateTime.Now };
            db.SearchDetail.Add(_SearchDetail);
            int r = db.SaveChanges();

(5)定时更新SearchTotals表记录

看到这种定时任务操作,这里可以采用Quartz.Net框架,为了方便,我把Quartz.Net的Job寄宿在控制台程序中,而实际工作中,我则更倾向于将其寄宿在Windows服务中。如果有必要,可以把这个定时更新SearchTotals表记录的程序部署到独立的服务器,这样可以减轻Web服务器的压力。

  1. 新建控制台程序QuartzNet,添加Quartz.dll和Common.Logging.dll的程序集引用,这里采用Database First的方式,添加ADO.NET实体数据模型,把表SearchTotals、SearchDetails添加进来。

2.添加KeyWordsTotalService.cs类,里面封装两个方法,清空SearchTotals表,然后把SearchDetails表的分组查询结构插入到SearchTotals表,这里我只统计近30天内的搜索明细。

复制代码
namespace QuartzNet
{
    public class KeyWordsTotalService
    {
        private SearchDemoEntities db = new SearchDemoEntities();
        /// <summary>
        /// 将统计的明细表的数据插入。
        /// </summary>
        /// <returns></returns>
        public bool InsertKeyWordsRank()
        {
            string sql = "insert into SearchTotals(Id,KeyWords,SearchCounts) select newid(),KeyWords,count(*)  from SearchDetails where DateDiff(day,SearchDetails.SearchDateTime,
getdate())<=30 group by SearchDetails.KeyWords
"; return this.db.Database.ExecuteSqlCommand(sql) > 0; } /// <summary> /// 删除汇总中的数据。 /// </summary> /// <returns></returns> public bool DeleteAllKeyWordsRank() { string sql = "truncate table SearchTotals"; return this.db.Database.ExecuteSqlCommand(sql) > 0; } } }
复制代码

3. 添加TotalJob.cs类,继承Ijob接口,并实现Execute方法。

复制代码
namespace QuartzNet
{
    public class TotalJob : IJob
    {
        /// <summary>
        /// 将明细表中的数据插入到汇总表中。
        /// </summary>
        /// <param name="context"></param>
        public void Execute(JobExecutionContext context)
        {
            KeyWordsTotalService bll = new KeyWordsTotalService();
            bll.DeleteAllKeyWordsRank();
            bll.InsertKeyWordsRank();
        }
    }
}
复制代码

4.修改Program.cs类

复制代码
using Quartz;
using Quartz.Impl;
using System;

namespace QuartzNet
{
    class Program
    {
        static void Main(string[] args)
        {
            IScheduler sched;
            ISchedulerFactory sf = new StdSchedulerFactory();
            sched = sf.GetScheduler();
            JobDetail job = new JobDetail("job1", "group1", typeof(TotalJob));//IndexJob为实现了IJob接口的类
            DateTime ts = TriggerUtils.GetNextGivenSecondDate(null, 5);//5秒后开始第一次运行
            TimeSpan interval = TimeSpan.FromSeconds(50);//每隔50秒执行一次
            Trigger trigger = new SimpleTrigger("trigger1", "group1", "job1", "group1", ts, null,
                                                    SimpleTrigger.RepeatIndefinitely, interval);//每若干时间运行一次,时间间隔可以放到配置文件中指定

            sched.AddJob(job, true);
            sched.ScheduleJob(trigger);
            sched.Start();
            Console.ReadKey();
        }
    }
}
复制代码

这里我是直接把Job和计划都直接写到代码中了,理由还是因为方便。而实际工作中,我们应当把这些信息尽量写到配置文件中,这样后面改动起来方便,不需要修改代码,只需要修改配置文件。

为了尽快看到效果,我这里是每隔50秒就进行了一次统计操作,而在实际应用中,我们的时间间隔可能是几个小时甚至一天,因为像这样的大数据统计,对实时性的要求不高,我们可以尽量减少对数据库的IO读写次数。

保持运行控制台程序QuartzNet,然后我们去进行搜索操作,这样后台就定期的生成了搜索统计记录。

10.5.2 热门搜索

10.5.2.1 展示热门搜索

其实就是从表SearchTotals中按照搜索次数进行降序排列,然后取出数条记录而已。

LastSearch控制器中的Index方法中添加如下代码:

var keyWords = db.SearchTotal.OrderByDescending(a => a.SearchCounts).Select(x => x.KeyWords).Skip(0).Take(6).ToList();
            ViewBag.KeyWords = keyWords;

View视图中

<div id="divKeyWords"><span>热门搜索:</span>@if (ViewBag.KeyWords != null) {
             foreach (string v in ViewBag.KeyWords) { 
              <a href="#">@v</a>
             }
         }</div>

接下来,我想要实现如下图10-21所示的效果:

 

图10-21

当我点击一个热词的时候,自动加载到文本框,并点击“搜索”按钮。

在View中添加代码:

复制代码
<script type="text/javascript">
    $(function () {
        $("#divKeyWords a").click(function () {
            $("#txtSearch").val($(this).html());
            $("#btnSearch").click();
        });
});
</script>
复制代码
10.5.2.2 搜索下拉框

这里我引入一个第三方js框架Autocomplete,它能在文本框中输入文字的时候,自动从后台抓去数据下拉列表。

云盘中我提供了Autocomplete.rar,将其解压,然后拷贝到SearchDemo项目中的lib目录下。

在SearchDemo项目中的KeyWordsTotalService.cs类中添加方法

复制代码
using System;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.Linq;

namespace SearchDemo.Common
{
    public class KeyWordsTotalService
    {
        private SearchDemoContext db = new SearchDemoContext();

        public List<string> GetSearchMsg(string term)
        {
            try
            {
                //存在SQL注入的安全隐患
                //string sql = "select KeyWords from SearchTotals where KeyWords like '"+term.Trim()+"%'";
                //return db.Database.SqlQuery<string>(sql).ToList();
                string sql = "select KeyWords from SearchTotals where KeyWords like @term";
                return db.Database.SqlQuery<string>(sql, new SqlParameter("@term", term+"%")).ToList();
            }
            catch (Exception ex)
            {
                throw new Exception(ex.Message);
            }
        }
    }
}
复制代码

然后在LastSearch控制器中添加方法:

复制代码
     /// <summary>
        /// 获取客户列表 模糊查询
        /// </summary>
        /// <param name="term"></param>
        /// <returns></returns>
        public string GetKeyWordsList(string term)
        {
            if (string.IsNullOrWhiteSpace(term))
                return null;

            var list = new KeyWordsTotalService().GetSearchMsg(term);
            //序列化对象
            //尽量不要用JavaScriptSerializer,为什么?性能差,完全可用Newtonsoft.Json来代替
            //System.Web.Script.Serialization.JavaScriptSerializer js = new System.Web.Script.Serialization.JavaScriptSerializer();
            //return js.Serialize(list.ToArray());
            return JsonConvert.SerializeObject(list.ToArray());
        }
复制代码

我们来看View:

复制代码
<link href="~/lib/Autocomplete/css/ui-lightness/jquery-ui-1.8.17.custom.css" rel="stylesheet" />
<script src="~/lib/Autocomplete/js/jquery-ui-1.8.17.custom.min.js"></script>
<script type="text/javascript">
    $(function () {
        $("#divKeyWords a").click(function () {
            $("#txtSearch").val($(this).html());
            $("#btnSearch").click();
        });
        getKeyWordsList("txtSearch");
    });
    //自动加载搜索列表
    function getKeyWordsList(txt) {
        if (txt == undefined || txt == "")
            return;
        $("#" + txt).autocomplete({
            source: "/LastSearch/GetKeyWordsList",
            minLength: 1
        });
    }
</script>
复制代码

10.5.3 标题和内容都支持搜索并高亮展示

在10.4中,只支持在内容中对关键词进行搜索,而实际上,我们可能既要支持在标题中搜索,也要在内容中搜索。

这里引入了BooleanQuery,我们的查询条件也添加了一个titleQuery。

搜索方法中,如下代码有修改:

复制代码
 PhraseQuery query = new PhraseQuery();//查询条件
            PhraseQuery titleQuery = new PhraseQuery();//标题查询条件
            List<string> lstkw = LuceneHelper.PanGuSplitWord(kw);//对用户输入的搜索条件进行拆分。

            foreach (string word in lstkw)            {
                query.Add(new Term("Content", word));//contains("Content",word)
                titleQuery.Add(new Term("Title", word));
            }
            query.SetSlop(100);//两个词的距离大于100(经验值)就不放入搜索结果,因为距离太远相关度就不高了

            BooleanQuery bq = new BooleanQuery();
            //Occur.Should 表示 Or , Must 表示 and 运算
            bq.Add(query, BooleanClause.Occur.SHOULD);
            bq.Add(titleQuery, BooleanClause.Occur.SHOULD);

            TopScoreDocCollector collector = TopScoreDocCollector.create(1000, true);//盛放查询结果的容器
            searcher.Search(bq, null, collector);//使用query这个查询条件进行搜索,搜索结果放入collector
复制代码

10.5.4 与查询、或查询、分页

前面我们在搜索的时候,其实采用的都是与查询,也就是说,我输入“诸葛亮周瑜”,则只会查找出,既存在诸葛亮,又存在周瑜的记录。那么有时候,我们是想查询存在诸葛亮或者周瑜的记录的,这也就是所谓的或查询。

我在界面添加一个复选框“或查询”,来让用户决定采用何种方式进行查询。

至于分页,这里采用MvcPager,关于MvcPager的使用方法请参见4.6.3。

View完整代码预览:

  View Code

然后,各位看官请再看LastSearch控制器中的方法:

  View Code

至此,站内搜索的基本功能均已完成。


本文转自邹琼俊博客园博客,原文链接:http://www.cnblogs.com/jiekzou/p/5661752.html,如需转载请自行联系原作者

相关文章
|
7月前
|
数据采集 存储 API
手动给docusaurus添加一个搜索
如果algolia不能自动配置的话,我教你手动给docusaurus添加一个搜索
手动给docusaurus添加一个搜索
|
移动开发 算法
秒懂算法 | A*搜索
本篇内容包括了A*搜索算法的原理精解以及2个例题。
575 1
秒懂算法 | A*搜索
|
小程序 数据库
小程序搜索功能,云开发搜索,小程序云开发模糊搜索,同时搜索多个字段
小程序搜索功能,云开发搜索,小程序云开发模糊搜索,同时搜索多个字段
287 0
|
存储 并行计算 算法
秒懂算法 | 搜索基础
本篇介绍了BFS和DFS的概念、性质、模板代码。
171 0
秒懂算法 | 搜索基础
|
数据采集 搜索推荐 前端开发
11、搜索服务
根据分类、关键字匹配课程名称,课程内容、难度等级搜索,搜索方式为全文搜索,搜索节点分页显示。
105 0
|
搜索推荐 安全 Java
搜索
搜索
125 0
html+css实战182-搜索
html+css实战182-搜索
126 0
html+css实战182-搜索
|
机器学习/深度学习 算法 搜索推荐
DARTS+:DARTS 搜索为何需要早停?
近日,华为诺亚 方舟实验室的作者们提出一种可微分的神经网络架构搜索算法 DARTS+,将早停机制(early stopping)引入到原始的 DARTS[1] 算法中,不仅减小了 DARTS 搜索的时间,而且极大地提升了 DARTS 的性能。相关论文《DARTS+: Improved Differentiable Architecture Search with Early Stopping》已经公开(相关代码稍后也会开源)。
235 0
DARTS+:DARTS 搜索为何需要早停?