redis-search 0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.textile +55 -0
- data/lib/redis_search.rb +11 -0
- data/lib/redis_search/base.rb +53 -0
- data/lib/redis_search/config.rb +14 -0
- data/lib/redis_search/search.rb +173 -0
- metadata +74 -0
data/README.textile
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
h1. RedisSearch
|
2
|
+
|
3
|
+
High performance real-time search (Support Chinese), index in Redis for Rails application
|
4
|
+
|
5
|
+
h2. Features
|
6
|
+
|
7
|
+
* Real-time search
|
8
|
+
* High performance
|
9
|
+
|
10
|
+
h2. Requirements
|
11
|
+
|
12
|
+
* Redis 2.2
|
13
|
+
* Libmmseg
|
14
|
+
|
15
|
+
h2. Install
|
16
|
+
|
17
|
+
* gem install redis-search
|
18
|
+
|
19
|
+
h2. Configure
|
20
|
+
|
21
|
+
# config/initializers/redis_search.rb
|
22
|
+
require "redis_search"
|
23
|
+
redis = Redis.new(:host => redis_config[:host],:port => redis_config[:port])
|
24
|
+
redis.select("app_name.search")
|
25
|
+
RedisSearch.configure do |config|
|
26
|
+
config.redis = redis
|
27
|
+
end
|
28
|
+
|
29
|
+
h2. Usage
|
30
|
+
|
31
|
+
class Post
|
32
|
+
include Mongoid::Document
|
33
|
+
include RedisSearch
|
34
|
+
|
35
|
+
field :title
|
36
|
+
field :body
|
37
|
+
|
38
|
+
belongs_to :user
|
39
|
+
belongs_to :category
|
40
|
+
|
41
|
+
# bind RedisSearch callback event, it will to rebuild search indexes when data create or update.
|
42
|
+
redis_search_index(:title_field => :title,
|
43
|
+
:ext_fields => [:category_name])
|
44
|
+
|
45
|
+
def category_name
|
46
|
+
self.category.name
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# GET /searchs?q=title
|
51
|
+
class SearchController < ApplicationController
|
52
|
+
def index
|
53
|
+
RedisSearch::Search.query(params[:q], :type => "Post")
|
54
|
+
end
|
55
|
+
end
|
data/lib/redis_search.rb
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
module RedisSearch
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
module ClassMethods
|
5
|
+
def redis_search_index(options = {})
|
6
|
+
title_field = options[:title_field] || :title
|
7
|
+
ext_fields = options[:ext_fields] || []
|
8
|
+
class_eval %(
|
9
|
+
def redis_search_ext_fields(ext_fields)
|
10
|
+
exts = {}
|
11
|
+
ext_fields.each do |f|
|
12
|
+
exts[f] = instance_eval(f.to_s)
|
13
|
+
end
|
14
|
+
exts
|
15
|
+
end
|
16
|
+
|
17
|
+
after_create :redis_search_index_create
|
18
|
+
def redis_search_index_create
|
19
|
+
s = Search.new(:title => self.#{title_field}, :id => self.id,
|
20
|
+
:exts => self.redis_search_ext_fields(#{ext_fields}),
|
21
|
+
:type => self.class.to_s)
|
22
|
+
s.save
|
23
|
+
end
|
24
|
+
|
25
|
+
before_destroy :redis_search_index_remove
|
26
|
+
def redis_search_index_remove
|
27
|
+
Search.remove(:id => self.id, :title => self.#{title_field}, :type => self.class.to_s)
|
28
|
+
end
|
29
|
+
|
30
|
+
before_update :redis_search_index_update
|
31
|
+
def redis_search_index_update
|
32
|
+
index_fields_changed = false
|
33
|
+
#{ext_fields}.each do |f|
|
34
|
+
next if f.to_s == "id"
|
35
|
+
if instance_eval(f.to_s + "_changed?")
|
36
|
+
index_fields_changed = true
|
37
|
+
end
|
38
|
+
end
|
39
|
+
begin
|
40
|
+
if(self.#{title_field}_changed?)
|
41
|
+
index_fields_changed = true
|
42
|
+
end
|
43
|
+
rescue
|
44
|
+
end
|
45
|
+
if index_fields_changed
|
46
|
+
Search.remove(:id => self.id, :title => self.#{title_field}_was, :type => self.class.to_s)
|
47
|
+
self.redis_search_index_create
|
48
|
+
end
|
49
|
+
end
|
50
|
+
)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,173 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
require "mmseg"
|
3
|
+
module RedisSearch
|
4
|
+
class Search
|
5
|
+
attr_accessor :type, :title, :id, :exts
|
6
|
+
def initialize(options = {})
|
7
|
+
self.exts = []
|
8
|
+
options.keys.each do |k|
|
9
|
+
eval("self.#{k} = options[k]")
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.split(text)
|
14
|
+
algor = RMMSeg::Algorithm.new(text)
|
15
|
+
words = []
|
16
|
+
loop do
|
17
|
+
tok = algor.next_token
|
18
|
+
break if tok.nil?
|
19
|
+
words << tok.text
|
20
|
+
end
|
21
|
+
words
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.warn(msg)
|
25
|
+
puts "[RedisSearch][warn]: #{msg}"
|
26
|
+
end
|
27
|
+
|
28
|
+
# 生成 uuid,用于作为 hashes 的 field, sets 关键词的值
|
29
|
+
def self.mk_sets_key(type, key)
|
30
|
+
"#{type}:#{key.downcase}"
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.mk_complete_key(type)
|
34
|
+
"Compl#{type}"
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.word?(word)
|
38
|
+
return !/^[\w\u4e00-\u9fa5]+$/i.match(word.force_encoding("UTF-8")).blank?
|
39
|
+
end
|
40
|
+
|
41
|
+
def save
|
42
|
+
return if self.title.blank?
|
43
|
+
data = {:title => self.title, :id => self.id, :type => self.type}
|
44
|
+
self.exts.each do |f|
|
45
|
+
data[f[0]] = f[1]
|
46
|
+
end
|
47
|
+
|
48
|
+
# 将原始数据存入 hashes
|
49
|
+
res = RedisSearch.config.redis.hset(self.type, self.id, data.to_json)
|
50
|
+
# 保存 sets 索引,以分词的单词为key,用于后面搜索,里面存储 ids
|
51
|
+
words = Search.split(self.title)
|
52
|
+
return if words.blank?
|
53
|
+
words.each do |word|
|
54
|
+
next if not Search.word?(word)
|
55
|
+
save_zindex(word)
|
56
|
+
key = Search.mk_sets_key(self.type,word)
|
57
|
+
RedisSearch.config.redis.sadd(key, self.id)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def save_zindex(word)
|
62
|
+
return if not Search.word?(word)
|
63
|
+
word = word.downcase
|
64
|
+
key = Search.mk_complete_key(self.type)
|
65
|
+
(1..(word.length)).each do |l|
|
66
|
+
prefix = word[0...l]
|
67
|
+
RedisSearch.config.redis.zadd(key, 0, prefix)
|
68
|
+
end
|
69
|
+
RedisSearch.config.redis.zadd(key, 0, word + "*")
|
70
|
+
end
|
71
|
+
|
72
|
+
def self.remove(options = {})
|
73
|
+
puts options.inspect
|
74
|
+
type = options[:type]
|
75
|
+
RedisSearch.config.redis.hdel(type,options[:id])
|
76
|
+
words = Search.split(options[:title])
|
77
|
+
words.each do |word|
|
78
|
+
next if not Search.word?(word)
|
79
|
+
key = Search.mk_sets_key(type,word)
|
80
|
+
RedisSearch.config.redis.srem(key, options[:id])
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# Use for short title search, this method is search by chars, for example Tag, User, Category ...
|
85
|
+
#
|
86
|
+
# h3. params:
|
87
|
+
# type model name
|
88
|
+
# w search char
|
89
|
+
# :limit result limit
|
90
|
+
# h3. usage:
|
91
|
+
# * RedisSearch::Search.complete("Tag","r") => ["Ruby","Rails", "REST", "Redis", "Redmine"]
|
92
|
+
# * RedisSearch::Search.complete("Tag","re") => ["Redis", "Redmine"]
|
93
|
+
# * RedisSearch::Search.complete("Tag","red") => ["Redis", "Redmine"]
|
94
|
+
# * RedisSearch::Search.complete("Tag","redi") => ["Redis"]
|
95
|
+
def self.complete(type, w, options = {})
|
96
|
+
limit = options[:limit] || 10
|
97
|
+
|
98
|
+
prefix_matchs = []
|
99
|
+
rangelen = 100 # This is not random, try to get replies < MTU size
|
100
|
+
prefix = w.downcase
|
101
|
+
key = Search.mk_complete_key(type)
|
102
|
+
start = RedisSearch.config.redis.zrank(key,prefix)
|
103
|
+
|
104
|
+
return [] if !start
|
105
|
+
count = limit
|
106
|
+
while prefix_matchs.length <= count
|
107
|
+
range = RedisSearch.config.redis.zrange(key,start,start+rangelen-1)
|
108
|
+
start += rangelen
|
109
|
+
break if !range or range.length == 0
|
110
|
+
range.each {|entry|
|
111
|
+
minlen = [entry.length,prefix.length].min
|
112
|
+
if entry[0...minlen] != prefix[0...minlen]
|
113
|
+
count = prefix_matchs.count
|
114
|
+
break
|
115
|
+
end
|
116
|
+
if entry[-1..-1] == "*" and prefix_matchs.length != count
|
117
|
+
prefix_matchs << entry[0...-1]
|
118
|
+
end
|
119
|
+
}
|
120
|
+
end
|
121
|
+
words = []
|
122
|
+
words = prefix_matchs.uniq.collect { |w| Search.mk_sets_key(type,w) }
|
123
|
+
ids = RedisSearch.config.redis.sunion(*words)
|
124
|
+
return [] if ids.blank?
|
125
|
+
hmget(type,ids, :limit => limit)
|
126
|
+
end
|
127
|
+
|
128
|
+
# Search items, this will split words by Libmmseg
|
129
|
+
#
|
130
|
+
# h3. params:
|
131
|
+
# type model name
|
132
|
+
# text search text
|
133
|
+
# :limit result limit
|
134
|
+
# h3. usage:
|
135
|
+
# * RedisSearch::Search.query("Tag","Ruby vs Python")
|
136
|
+
def self.query(type, text,options = {})
|
137
|
+
result = []
|
138
|
+
return result if text.strip.blank?
|
139
|
+
|
140
|
+
words = Search.split(text)
|
141
|
+
limit = options[:limit] || 10
|
142
|
+
sort_field = options[:sort_field] || "id"
|
143
|
+
words = words.collect { |w| Search.mk_sets_key(type,w) }
|
144
|
+
return result if words.blank?
|
145
|
+
ids = RedisSearch.config.redis.sinter(*words)
|
146
|
+
hmget(type,ids, :limit => limit, :sort_field => sort_field)
|
147
|
+
end
|
148
|
+
|
149
|
+
private
|
150
|
+
def self.hmget(type, ids, options = {})
|
151
|
+
result = []
|
152
|
+
limit = options[:limit] || 10
|
153
|
+
sort_field = options[:sort_field] || "id"
|
154
|
+
return result if ids.blank?
|
155
|
+
RedisSearch.config.redis.hmget(type,*ids).each do |r|
|
156
|
+
begin
|
157
|
+
result << JSON.parse(r)
|
158
|
+
rescue => e
|
159
|
+
Search.warn("Search.query failed: #{e}")
|
160
|
+
end
|
161
|
+
end
|
162
|
+
items = sort_result(result, type, sort_field)
|
163
|
+
items = items[0..limit-1] if items.length > limit
|
164
|
+
items
|
165
|
+
end
|
166
|
+
|
167
|
+
def self.sort_result(items, type, sort_field)
|
168
|
+
return items if items.blank?
|
169
|
+
items = items.sort { |x,y| y[sort_field] <=> x[sort_field] }
|
170
|
+
items
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
metadata
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: redis-search
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: '0.1'
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Jason Lee
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2011-08-16 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rmmseg-cpp-huacnlee
|
16
|
+
requirement: &2159834180 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 0.2.8
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *2159834180
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: redis
|
27
|
+
requirement: &2159833700 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ~>
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 2.1.1
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *2159833700
|
36
|
+
description: High performance real-time search (Support Chinese), index in Redis for
|
37
|
+
Rails application.
|
38
|
+
email:
|
39
|
+
- huacnlee@gmail.com
|
40
|
+
executables: []
|
41
|
+
extensions: []
|
42
|
+
extra_rdoc_files: []
|
43
|
+
files:
|
44
|
+
- lib/redis_search/base.rb
|
45
|
+
- lib/redis_search/config.rb
|
46
|
+
- lib/redis_search/search.rb
|
47
|
+
- lib/redis_search.rb
|
48
|
+
- README.textile
|
49
|
+
homepage: http://github.com/huacnlee/redis-search
|
50
|
+
licenses: []
|
51
|
+
post_install_message:
|
52
|
+
rdoc_options: []
|
53
|
+
require_paths:
|
54
|
+
- lib
|
55
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
56
|
+
none: false
|
57
|
+
requirements:
|
58
|
+
- - ! '>='
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '0'
|
61
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
62
|
+
none: false
|
63
|
+
requirements:
|
64
|
+
- - ! '>='
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
version: 1.3.6
|
67
|
+
requirements: []
|
68
|
+
rubyforge_project:
|
69
|
+
rubygems_version: 1.8.6
|
70
|
+
signing_key:
|
71
|
+
specification_version: 3
|
72
|
+
summary: High performance real-time search (Support Chinese), index in Redis for Rails
|
73
|
+
application.
|
74
|
+
test_files: []
|