对于一个网站来说,无论是商城网站还是门户网站,搜索框都是有一个比较重要的地位,它的存在可以说是
为了让用户更快、更方便的去找到自己想要的东西。对于经常逛这个网站的用户,当然也会想知道在这里比较“火”
的东西是什么,这个时候我们搜索框上的热词就起作用了。其实我觉得这一块的完善会对这个网站带来许多益处。
可能现在比较普遍的做法是把这些相应的信息存到我们的关系型数据库中,如sql server 和 oracle。方便起见
的话,可能每搜索一次就往表里插一次数据,用的时候要先统计数据,统计完后再排序,最后才展示。这种情况下,
如果搜索量很大的话,表的膨胀速度就会非常快,如果sql没写好,查询的时候估计会。。相比Redis,同等条件下,
Redis的速率肯定是会较优,毕竟是从内存中拿出来的。
下面我们就用.NET Core和StackExchange.Redis来做一下这个简单的案例。
案例用到的一些相关技术和说明:
技术 | 说明 |
.NET Core | 网站嘛,你懂的。有事没事用Core写写Demo,免得跟不上发展的脚步。 |
Redis | 存储搜索词,用了主从的模式,主写从读 |
Jquery-ui | 主要是用了里面的autocomplete |
开始正题之前,我们要确定用Redis中的那种数据结构,五种之中比较合适的应该是SortedSet,我们可以用成员来
作为搜索词,成员分数来作为搜索词的搜索次数,这样就可以很方便的来操作相关的数据了。
下面开始正题:
我们在开始的时候需要初始化一下数据。这里就直接在第一次运行的时候初始化。用上流水线的技术,速度还是
很可观的。初始化了70个搜索关键词(NBA球星),然后用随机数作为关键字的下标,去随机给这个关键字加1分。这
个分数就是这个关键字被搜索的次数。下面来看看初始化的相关代码:
1 public IActionResult Index()
2 {
3 //keys
4 IList<string> keys = new List<string>()
5 {
6 "kobe","johnson","jabbar","west","o'neal","baylor","mccann","worthy","gasol","chamberlain",
7 "fisher","odom","bynum","horry","rambis","riley","clarkson","Williams","young","Russell",
8 "ingram","randle","nance","brown","deng","yi","ariza","artest","walton","vujacic",
9 "james","paul","curry","park","yao","kevin","wade","rose","popovich","leonard",
10 "aldridge","ginobili","duncan","lavine","rubio","garnett","wiggins","westbrook","durant","ibaka",
11 "nowitzki","pierce","crawford","love","smith","iguodala","barnes","green","thompson","harden",
12 "lillard","mccollum","lin","jackson","nash","stoudemire","whiteside","dragic","Howard","batum"
13 };
14
15 //init
16 Random random = new Random();
17 var tran = _redis.GetTransaction();
18 for (int i = 0; i < 1000000; i++)
19 {
20 tran.SortedSetIncrementAsync(_searchKey, keys[random.Next(0, 70)], 1);
21 }
22 tran.ExecuteAsync();
23
24 return View();
25 }
这里是在加载这个页面的时候就把这些热搜词存进Redis中,这样我们才能有数据来演示啊。这里还用到了一个
非事务型的流水线。就是把要操作的指令存放到一个队列中,最后把这个队列扔到服务端去执行,这样就有效的减少
了不必要的网络传输,同时也提高了执行速度。
好了,初始数据有了,下面要做的就是用户在搜索的时候,根据用户的输入去匹配搜索次数多的关键字,展示最
Hot的10个,当然这个展示的个数是随我们定的,最后可以考虑把这个放到我们的配置文件中去,甚至是放到数据库中,
为的是灵活和方便维护。下面是我们在后台的处理逻辑:
1 public IActionResult GetHotKey(string key="")
2 {
3 if (string.IsNullOrEmpty(key))
4 {//default
5 var res = _redis.ZRevRange(_searchKey, 0, 9);
6 var list = (from i in res select i.ToString());
7 return Json(list);
8 }
9 else
10 {//by user input
11 var res = _redis.ZRevRange(_searchKey, 0, -1);
12 var list = (from i in res select i.ToString()).Where(x => x.Contains(key)).Take(10).ToList();
13 return Json(list);
14 }
15 }
对于查询的处理是非常的简单的,用户不小心输入空格的时候就展示最热的10个关键词,如果用户有输入的话,就把
关键词中包含用户输入的展示出来。那么我们在页面上要做些什么呢?下面就是我们演示用的搜索框。
1 <div class="row">
2 <div class="col-md-6 col-md-offset-4" style="padding-top:50px;">
3 <input id="key" name="key" placeholder="search" class="form-control col-md-4">
4 <button class="btn btn-primary" type="button" id="searchSubmit">Search</button>
5 <div id="result"></div>
6 </div>
7 </div>
相应的js是写到 scripts 这个section中的,js的话是比较简单的就是用ajax去请求我们要展示的数据。更多的应该是
jquery-ui的api问题,大家也可以换用自己比较熟悉的组件,举一反三即可。下面是autocomplete的api ,如果有需要可
以去看一下。
1 @section scripts{
2 <script type="text/javascript">
3 $(function () {
4 //show hot keyword
5 $("#key").autocomplete({
6 source: function (request, response) {
7 $.ajax({
8 url: "@Url.Action("GetHotKey", "Auto")",
9 dataType: "json",
10 data: {
11 key: request.term
12 },
13 success: function (data) {
14 response(data);
15 }
16 });
17 },
18 });
19 </script>
20 }
到这里,用户搜索前的操作,我们是做好了,下面先来看一下效果。
那么用户点击了搜索之后我们要做些什么处理呢?无论是新的关键字还是已有的关键字,我们都是要做处理的,当然redis
中zincrby命令来处理这个是十分合适的,存在的就把分数加1,不存在就创建一个分数为1的成员。下面是搜索时的后台逻辑处理:
1 [HttpPost]
2 public IActionResult SetHotKey(string key)
3 {
4 if (!string.IsNullOrWhiteSpace(key))
5 {
6 _redis.ZIncrby(_searchKey,key);
7 //other
8 //...
9 return Json(new { code = "000", msg = "OK" });
10 }
11 else
12 {
13 return Json(new { code = "999", msg = "keyword can not be empty!" });
14 }
15 }
限制了用户不能搜索空关键字,在把这个关键字存储或者分数加一之后,就是展示我们的搜索的结果。这个搜索的结果一般
是从solr等全文检索的地方查出来的,不是我们讲的重点,所以就忽略了。然后我们还要加一段js去处理我们搜索的时候应该做的
操作。当然,都是些比较简单的操作。
1 //search
2 $("#searchSubmit").click(function () {
3 $.ajax({
4 url: "@Url.Action("SetHotKey", "Auto")",
5 dataType: "json",
6 type: "POST",
7 data: { key: $("#key").val() },
8 success: function (data) {
9 if (data.code == "000") {
10 $("<p>search successful!</p>").appendTo("#result");
11 } else {
12 $("<p>"+data.msg+"</p>").appendTo("#result");
13 }
14 }
15 });
16 });
在演示的时候,我们搜索了“我爱你”和“我不信”,在Redis的客户端我们找出搜索次数最少的6个,然后就可以看到我们那两
个关键字最的分数都是1。确定是刚插入的数据。
到这里,我们做的这个热搜词可以说是大功告成了。当然这可以说是最最最简单的一个雏形。我们还可以适当的添加一些
东西让这个功能变得更加完善。比如我可以在搜索展示的时候显示一下搜索的次数等。
最后是完整的控制器和页面代码:
1 using AutoCompleteDemo.Common;
2 using Microsoft.AspNetCore.Mvc;
3 using System;
4 using System.Collections.Generic;
5 using System.Linq;
6
7 namespace AutoCompleteDemo.Controllers
8 {
9 public class AutoController : Controller
10 {
11 private readonly IRedis _redis;
12 private readonly string _searchKey = "search";
13 public AutoController(IRedis redis)
14 {
15 _redis = redis;
16 }
17
18 public IActionResult Index()
19 {
20 //keys
21 IList<string> keys = new List<string>()
22 {
23 "kobe","johnson","jabbar","west","o'neal","baylor","mccann","worthy","gasol","chamberlain",
24 "fisher","odom","bynum","horry","rambis","riley","clarkson","Williams","young","Russell",
25 "ingram","randle","nance","brown","deng","yi","ariza","artest","walton","vujacic",
26 "james","paul","curry","park","yao","kevin","wade","rose","popovich","leonard",
27 "aldridge","ginobili","duncan","lavine","rubio","garnett","wiggins","westbrook","durant","ibaka",
28 "nowitzki","pierce","crawford","love","smith","iguodala","barnes","green","thompson","harden",
29 "lillard","mccollum","lin","jackson","nash","stoudemire","whiteside","dragic","Howard","batum"
30 };
31
32 //init
33 Random random = new Random();
34 var tran = _redis.GetTransaction();
35 for (int i = 0; i < 2000000; i++)
36 {
37 tran.SortedSetIncrementAsync(_searchKey, keys[random.Next(0, 70)], 1);
38 }
39 tran.ExecuteAsync();
40
41 return View();
42 }
43
44 public IActionResult GetHotKey(string key="")
45 {
46 if (string.IsNullOrEmpty(key))
47 {//default
48 var res = _redis.ZRevRange(_searchKey, 0, 9);
49 var list = (from i in res select i.ToString());
50 return Json(list);
51 }
52 else
53 {//by user input
54 var res = _redis.ZRevRange(_searchKey, 0, -1);
55 var list = (from i in res select i.ToString()).Where(x => x.Contains(key)).Take(10).ToList();
56 return Json(list);
57 }
58 }
59
60 [HttpPost]
61 public IActionResult SetHotKey(string key)
62 {
63 if (!string.IsNullOrWhiteSpace(key))
64 {
65 _redis.ZIncrby(_searchKey,key);
66 //other
67 //...
68 return Json(new { code = "000", msg = "OK" });
69 }
70 else
71 {
72 return Json(new { code = "999", msg = "keyword can not be empty!" });
73 }
74 }
75 }
76 }
AutoController
1 @{
2 ViewData["Title"] = "Auto Complete";
3 }
4 <div class="row">
5 <div class="col-md-6 col-md-offset-4" style="padding-top:50px;">
6 <input id="key" name="key" placeholder="search" class="form-control col-md-4">
7 <button class="btn btn-primary" type="button" id="searchSubmit">Search</button>
8 <div id="result"></div>
9 </div>
10 </div>
11 @section scripts{
12 <script type="text/javascript">
13 $(function () {
14 //show hot keyword
15 $("#key").autocomplete({
16 source: function (request, response) {
17 $.ajax({
18 url: "@Url.Action("GetHotKey", "Auto")",
19 dataType: "json",
20 data: {
21 key: request.term
22 },
23 success: function (data) {
24 response(data);
25 }
26 });
27 },
28 });
29
30 //search
31 $("#searchSubmit").click(function () {
32 $.ajax({
33 url: "@Url.Action("SetHotKey", "Auto")",
34 dataType: "json",
35 type: "POST",
36 data: { key: $("#key").val() },
37 success: function (data) {
38 if (data.code == "000") {
39 $("<p>search successful!</p>").appendTo("#result");
40 } else {
41 $("<p>"+data.msg+"</p>").appendTo("#result");
42 }
43 }
44 });
45 });
46 });
47 </script>
48 }
Index.cshtml