redis实现自动输入完成(八)
1. 介绍
当我们在京东商城的搜索框,输入想要搜索的内容,比如你想要搜索"热水瓶",刚输入一个"热"字,就会出现一个下拉框,列出了很多以"热"字开头的可供选择的条目,比如"热水器"、"热水袋"、”热水瓶"等,如下图所示:
这种技术就叫做自动输入完成,当输入想要搜索的首字符或其中被包含的字符时,就会出现可供选择的条目,用户可以选择其中的条目来完成此次搜索,避免了用户输入全部的字符,改善了用户体验。这种技术很常用,比如在一个社交网站添加联系人时,就是可以通过输入来联想匹配到联系人的。
2. 功能分析
要实现这样的功能,需要前端和后端一起配合,前端部分需要监听搜入框内容的改变,当内容改变时把内容作为参数传递给服务器,服务器再请求后端的数据库,再把数据返回给客户端,前端只要把结果渲染出来即可。
这个数据库我们选择的是内存数据库redis,而不用存储在磁盘的关系型数据库,毕竟这个功能对实时性要求比较高,频繁地请求数据库肯定是用性能高和速度快的redis好。
我们有一个实例网站,是存放文章的,每篇文章都有自己的标签(tag),整个网站是具有全文检索功能的,也就是说,可以根据文章的标题、内容、标签来搜索文章。现在需要在输入框加一个自动完成的功能,当用户在输入框输入标签的头一个或几个字符的时候,会自动联想到标签。比如,有"ruby","postgresql"两个标签,当用户输入"ru"的时候,会自动出现"ruby"作为可选的项。
首先,被检索的标签的名称事先存放到redis中,每个标签都是唯一的,所以可选择redis的集合作为数据库,但是,有可以出现很多个以"ru"开头的标签,所以需要一个权重算法,那就是排序,标签被使用得频繁的越排在前面,所以最终是用排序集合(sortedset)来存储标签数据。
# rails console
> ActsAsTaggableOn::Tag.all
[
[ 0] 消息队列 {
:id => 50,
:name => "消息队列",
:taggings_count => 5
},
[ 1] ruby on rails {
:id => 8,
:name => "ruby on rails",
:taggings_count => 17
},
[ 2] websocket {
:id => 55,
:name => "websocket",
:taggings_count => 1
},
[ 3] database {
:id => 25,
:name => "database",
:taggings_count => 1
}
]
taggings_count
就是标签被使用的次数,只要把tag
的name
按照taggings_count
作为排序标准存到redis的sort set
中,就可以了。下面我们会说如何去存储这些数据。
3. soulmate使用
soulmate是一个结合redis实现自动输入完成的功能强大的gem。
先安装这个gem。
$ gem install soulmate
soulmate要求你的数据存成一种特定格式的json,再把json导出到redis中,格式是类似这样的。
{
"id": 5,
"term": "devise",
"score": 3,
"data": {
}
}
id
是唯一的标识,term
就是搜索的条目,score
就是排序的项,这三项是必须要带上的,而data
是可选的,里面可以存自己想要的数据,比如可把tag的描述信息加上。
很简单,在我们的案例中,把上面的tag的name
换成term
,taggings_count
换成score
即可。
3.1 导入tags数据
我写了一个方法可以将所有的tag导出到一个json文件。
File.open("tags.json","w+") do |f|
ActsAsTaggableOn::Tag.find_each do |tag|
tag_json = {
id: tag.id,
term: tag.name,
score: tag.taggings_count
}
f.write("#{tag_json.to_json}\n")
end
end
输出的tags.json类似下面这样:
{"id":5,"term":"devise","score":3}
{"id":6,"term":"登录","score":3}
{"id":7,"term":"认证","score":3}
{"id":8,"term":"ruby on rails","score":17}
{"id":9,"term":"rails","score":4}
{"id":10,"term":"ruby","score":4}
现在可以先这个tags.json导入到redis数据库中,soulmate这个gem也提供了相关的命令行工具。
$ soulmate load tag --redis=redis://localhost:6379/0 < tags.json
你会发现redis中增加了很多的数据,我们先不管,等下再来分析那些数据。
3.2 添加单个tag到redis
每次都用这种导入的方式来在redis增加数据很不方便的,毕竟以后我们随时要增加tag。你总不可能为了增加一个tag再导一次json吧,这不太科学。所以我们需要在增加或删除tag的时候自动把数据添加到redis中。
通过查看soulmate的源码,发现它是提供了相应的方法的。
# https://github.com/seatgeek/soulmate/blob/master/lib/soulmate/loader.rb#L29
def add(item, opts = {})
opts = { :skip_duplicate_check => false }.merge(opts)
raise ArgumentError, "Items must specify both an id and a term" unless item["id"] && item["term"]
# kill any old items with this id
remove("id" => item["id"]) unless opts[:skip_duplicate_check]
Soulmate.redis.pipelined do
# store the raw data in a separate key to reduce memory usage
Soulmate.redis.hset(database, item["id"], MultiJson.encode(item))
phrase = ([item["term"]] + (item["aliases"] || [])).join(' ')
prefixes_for_phrase(phrase).each do |p|
Soulmate.redis.sadd(base, p) # remember this prefix in a master set
Soulmate.redis.zadd("#{base}:#{p}", item["score"], item["id"]) # store the id of this term in the index
end
end
end
由于add
方法要接一个hash作为参数,所以需要对tag这个model作一些加工。
# config/initializers/tag.rb
module ActsAsTaggableOn
class Tag < ::ActiveRecord::Base
after_create :create_soulmate
def to_hash
tag_json = {
id: self.id,
term: self.name,
score: self.taggings_count
}
JSON.parse(tag_json.to_json )
end
private
def create_soulmate
Soulmate::Loader.new("tag").add self.to_hash
end
end
end
我们拿其中一个tag试一下,看看它究竟生成了怎样的redis数据(如果在开发环境,做这个之前可以用redis的flushdb指令先清除数据)。
$ rails console
> tag = ActsAsTaggableOn::Tag.first.to_hash
{
"id" => 5,
"term" => "devise",
"score" => 3
}
> Soulmate::Loader.new("tag").add tag
在redis生成了如下的数据:
$ redis-cli
> keys *
1) "soulmate-index:tag:devise"
2) "soulmate-index:tag"
3) "soulmate-index:tag:devis"
4) "soulmate-index:tag:dev"
5) "soulmate-index:tag:de"
6) "soulmate-index:tag:devi"
7) "soulmate-data:tag"
所有的key分为三大类,分别是"soulmate-index:tag"
、"soulmate-data:tag"
,剩下的以tag的name为devise的前缀开头的key。结合add方法的源码,也可以知道这三种key的类型分别为set, hash, sortedset,可以自己用redis的type命令打印出来。现在打印出这些key的值。
$ redis-cli
> smembers soulmate-index:tag
1) "devis"
2) "devi"
3) "dev"
4) "de"
5) "devise"
> hgetall soulmate-data:tag
1) "5"
2) "{\"id\":5,\"term\":\"devise\",\"score\":3}"
> zrange "soulmate-index:tag:de" 0 -1 WITHSCORES
1) "5"
2) "3"
> zrange "soulmate-index:tag:dev" 0 -1 WITHSCORES
1) "5"
2) "3"
soulmate-index:tag
存的是集合(set),将"devise"分成一个个前缀,以后按照这些前缀就能搜出"devise"这个tag。soulmate-data:tag
存的是一个哈希(hash),健为标签(tag)的id,值为tag的所有内容,就是那个标签(ActsAsTaggableOn::Tag) model中to_hash
方法的内容,如果有存data,它的数据也是会在这里出现。soulmate-index:tag:de
等存的是排序后的集合,排序的健是score
,也就是标签(tag)的taggings_count
,值为标签(tag)的id
。
3.3 测试效果
现在来实现服务端的逻辑,我们不用自己写controller端的代码,soulmate为我们提供好了这一切。
先安装这个gem。
gem install rack-contrib
然后执行以下这个命令。
$ soulmate-web --foreground --no-launch --redis=redis://localhost:6379/0
这会开启一个服务并且监听在5678端口。
现在我们可以用curl工具或chrome浏览器插件postman来测试这个服务。下面是curl工具的使用例子。
$ curl -X GET http://localhost:5678/search --data "types[]=tag&term=de"
{"term":"de","results":{"tag":[{"id":5,"term":"devise","score":3}]}}
$ curl -X GET http://localhost:5678/search --data "types[]=tag&term=devis"
{"term":"devis","results":{"tag":[{"id":5,"term":"devise","score":3}]}}
使用postman请求http://localhost:5678/search?types[]=tag&term=de
的例子如下:
现在再来查看redis的数据,会发现多了两个key。
$ redis-cli
> zrange "soulmate-cache:tag:de" 0 -1 WITHSCORES
1) "5"
2) "3"
> zrange "soulmate-cache:tag:devis" 0 -1 WITHSCORES
1) "5"
2) "3"
4. 页面上的实现
我们要用soulmate配合前端在rails项目里实现自动输入完成的功能。
先把soulmate安装进rails项目里。
4.1 挂载soulmate服务
把下面的代码添加到Gemfile文件,然后执行bundle
命令。
gem 'rack-contrib'
gem 'soulmate', :require => 'soulmate/server'
在config/routes.rb
文件中添加一行路由。
mount Soulmate::Server, :at => "/sm"
在config/initializers
目录下添加soulmate.rb文件。
Soulmate.redis = 'redis://127.0.0.1:6379/0'
# or you can asign an existing instance of Redis, Redis::Namespace, etc.
# Soulmate.redis = $redis
重启服务器。现在可以这样来测试。
$ curl -X GET http://localhost:3000/sm/search --data "types[]=tag&term=dev"
{"term":"dev","results":{"tag":[{"id":5,"term":"devise","score":3}]}}
4.2 soulmate.js
至于前端部分,我们使用官方推荐的soulmate.js这个库。
相关的代码是这样的。
= form_tag "/articles", method: "get", class: "navbar-form navbar-left", role: "search"
.form-group
input.form-control id="search" placeholder="Search" type="text" name="term" value="#{params[:search].presence}" autocomplete="off"/
javascript:
// Make the input field autosuggest-y.
$(document).ready(function() {
render = function(term, data, type){ return term; }
select = function(term, data, type){ console.log("Selected " + term); }
$('#search').soulmate({
url: '/sm/search',
types: ['tag'],
renderCallback: render,
selectCallback: select,
minQueryLength: 2,
maxResults: 5
});
});
具体的css和js效果可以自己处理。
5. 源码解析
现在有个美中不足的地方,就是必须要输入两个字符以上才能开始自动输入完成。而readme文档没有相关的解决方法,我们只能从源码入手。
上面的例子中,"devise"这个单词会被分解成一个个前缀,比如"de","dev”等。我们来看下中文词组是如何被分解的。
上面有提到那个add
方法的源码,其中调用了prefixes_for_phrase
这个方法。
# https://github.com/seatgeek/soulmate/blob/ead5d6c2b6d698a5c49294a73a4f7536a5013f01/lib/soulmate/helpers.rb#L3
module Soulmate
module Helpers
def prefixes_for_phrase(phrase)
words = normalize(phrase).split(' ').reject do |w|
Soulmate.stop_words.include?(w)
end
words.map do |w|
(Soulmate.min_complete-1..(w.length-1)).map{ |l| w[0..l] }
end.flatten.uniq
end
end
end
执行rails console
进入终端来看一下这个方法是如何分解中文词组的。
$ rails console
> include Soulmate::Helpers
Object < BasicObject
> prefixes_for_phrase("前端构建与部署工具")
[
[0] "前端",
[1] "前端构",
[2] "前端构建",
[3] "前端构建与",
[4] "前端构建与部",
[5] "前端构建与部署",
[6] "前端构建与部署工",
[7] "前端构建与部署工具"
]
中文词组还是能够正常处理,可是至少要输入两个字符。通过分析prefixes_for_phrase
方法,可以发现关键在于Soulmate.min_complete
这个变量。
通过搜索源码,发现了定义Soulmate.min_complete
变量的地方。
# https://github.com/seatgeek/soulmate/blob/ead5d6c2b6d698a5c49294a73a4f7536a5013f01/lib/soulmate/config.rb#L11
require 'uri'
require 'redis'
module Soulmate
module Config
attr_writer :min_complete
def min_complete
@min_complete ||= DEFAULT_MIN_COMPLETE
end
def redis=(server)
...
end
end
end
还记得上文"挂载soulmate服务"部分提到config/initializers/soulmate.rb
这个文件吗,里面就是定义了Soulmate.redis
这个变量,那同样的道理,也把min_complete
定义在里面。
# config/initializers/soulmate.rb
Soulmate.redis = 'redis://127.0.0.1:6379/0'
Soulmate.min_complete = 1
再来看结果。
$ redis-cli
> include Soulmate::Helpers
Object < BasicObject
> prefixes_for_phrase("前端构建与部署工具")
[
[0] "前",
[1] "前端",
[2] "前端构",
[3] "前端构建",
[4] "前端构建与",
[5] "前端构建与部",
[6] "前端构建与部署",
[7] "前端构建与部署工",
[8] "前端构建与部署工具"
]
完结。
redis实现自动输入完成(八)的更多相关文章
- 通过Keepalived实现Redis Failover自动故障切换功能
通过Keepalived实现Redis Failover自动故障切换功能[实践分享] 参考资料: http://patrick-tang.blogspot.com/2012/06/redis-keep ...
- Redis主从自动failover
Redis主从架构持久化存在一个问题,即前次测试的结论,持久化需要配置在主实例上才能跨越实例保证数据不丢失,这样以来主实例在持久化数据到硬 盘的过程中,势必会造成磁盘的I/O等待,经过实际测试,这个持 ...
- 通过JavaScript脚本实现验证码自动输入
很多网站在用户进行某次点击,比如在线购物确认购买时,会要求用户输入验证码,这在一般情况下也没啥问题,但在用户需要频繁购买或是抢购时就很讨厌了.其实网站的验证码一般是由JS脚本生成的,因此也可以通过编写 ...
- 使用Python完成表格自动输入
看了看<Python编程快速上手>,写了个小脚本完成12306登录数据的自动输入.如下: 1 import webbrowser 2 import pyautogui 3 import t ...
- 在VC中使用SendInput函数实现中文的自动输入
很早以前写了一个刷卡程序,功能是定时监控读卡器,当发现有IC卡放到读卡器上后,自动识别出卡号,然后带着这个卡号搜索一个英文用户名和卡号的对照表,最后把英文用户名直接自动输入到当前光标所在的位置.本来程 ...
- pl/sql developer 自动输入替换 光标自动定位
pl/sql developer 自动输入替换 工具->首选项->用户界面->编辑器->自动替换,自己定义一些规则,然后输入key,点击tab或者空格,就可以进行替换了: SL ...
- C# WPF MVVM QQ密码管家项目(8,完结篇:自动输入QQ号、密码)
原文:C# WPF MVVM QQ密码管家项目(8,完结篇:自动输入QQ号.密码) 目录: 1,界面设计 2,数据模型的建立与数据绑定 3,添加QQ数据 4,修改QQ数据 5,删除QQ数据 6,密码选 ...
- Redis进阶实践之十八 使用管道模式提高Redis查询的速度
原文:Redis进阶实践之十八 使用管道模式提高Redis查询的速度 一.引言 学习redis 也有一段时间了,该接触的也差不多了.后来有一天,以为同事问我,如何向redis中 ...
- Python自动输入【新手必学】
前言 本文的文字及图片来源于网络,仅供学习.交流使用,不具有任何商业用途,版权归原作者所有,如有问题请及时联系我们以作处理.作者:哈喽哈嘿哈 这篇文章是我的第一篇文章,写的不好的地方,请大家多多指教哈 ...
随机推荐
- 大数据(10) - HBase的安装与使用
HBaes介绍 HBase是什么? 数据库 非关系型数据库(Not-Only-SQL) NoSQL 强依赖于HDFS(基于HDFS) 按照BigTable论文思想开发而来 面向列来存储 可以用来存储: ...
- ComBoFuzzySearch.js
/** * combobox和combotree模糊查询 */(function () { //combobox可编辑,自定义模糊查询 $.fn.combobox.defaults.editable ...
- C++ 类的继承四(类继承中的重名成员)
//类继承中的重名成员 #include<iostream> using namespace std; /* 自己猜想: 对于子类中的与父类重名的成员,c++编译器会单独为子类的这个成员变 ...
- 电脑端与iPad 端如何共享ChemDraw结构
在日常生活中,我们使用的电脑会有好几种系统,很多的软件不能做好各个系统的兼容.但是ChemDraw软件很好的解决了这个问题,可以应用于Mac.Windows两个电脑客户端以及Chem3D for iP ...
- CPictureEx类
CPictueEx类不仅可以显示GIF(包括GIF动画),还可以显示JPEG.BMP.WMF.ICO.CUR等. 参考:https://www.codeproject.com/Articles/142 ...
- 编程之美 海量数据寻找 K 大数
1. 使用最小堆, 设置最小堆的大小为K, 仅需遍历一遍即可 2. 寻找最大的 K 个数实质上是寻找第 K 大的数. 通过二分法在区间内不断校正 mid 的值来找到 pivot, 时间复杂度为 o(N ...
- linux命令之rpm
1.查询一个包是否被安装的命令rpm -q < rpm package name> 2.列出所有被安装的rpm package 命令rpm -qa
- 88、android 插件开发教程(转载)
http://blog.csdn.net/qq435757399/article/details/46521085 http://blog.csdn.net/t12x3456/article/deta ...
- 模拟window桌面实现
正在开发中的游戏有个全屏功能--可以在window桌面背景上运行,就像一些视频播放器在桌面背景上播放一样的,花了个上午整了个Demo放出来留个纪念. 实现功能:显示图标,双击图标执行相应的程序,右击图 ...
- C++编译遇到参数错误(cannot convert parameter * from 'const char [**]' to 'LPCWSTR')
转:http://blog.sina.com.cn/s/blog_9ffcd5dc01014nw9.html 前面的几天一直都在复习着被实习落下的C++基础知识.今天在复习着上次创建的窗口程序时,出现 ...