redis-search 0.6.3 → 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.markdown +14 -3
- data/lib/redis/search/base.rb +11 -7
- data/lib/redis/search/finder.rb +74 -32
- data/lib/redis/search/index.rb +12 -2
- metadata +10 -10
data/README.markdown
CHANGED
@@ -14,6 +14,7 @@ High performance real-time search (Support Chinese), index in Redis for Rails ap
|
|
14
14
|
* Support ActiveRecord and Mongoid
|
15
15
|
* Sort results by one field
|
16
16
|
* Homophone search, pinyin search
|
17
|
+
* Conditions support
|
17
18
|
|
18
19
|
## Requirements
|
19
20
|
|
@@ -26,7 +27,8 @@ in Rails application Gemfile
|
|
26
27
|
gem 'redis','>= 2.1.1'
|
27
28
|
gem 'chinese_pinyin', '0.4.1'
|
28
29
|
gem 'rmmseg-cpp-huacnlee', '0.2.9'
|
29
|
-
gem 'redis-
|
30
|
+
gem 'redis-namespace','~> 1.1.0'
|
31
|
+
gem 'redis-search', '0.6.3'
|
30
32
|
|
31
33
|
install bundlers
|
32
34
|
|
@@ -36,9 +38,14 @@ install bundlers
|
|
36
38
|
|
37
39
|
create file in: config/initializers/redis_search.rb
|
38
40
|
|
41
|
+
require "redis"
|
42
|
+
require "redis-namespace"
|
39
43
|
require "redis-search"
|
40
44
|
# don't forget change namespace
|
41
45
|
redis = Redis.new(:host => "127.0.0.1",:port => "6379")
|
46
|
+
# We suggest you use a special db in Redis, when you need to clear all data, you can use flushdb command to clear them.
|
47
|
+
redis.select(3)
|
48
|
+
# Give a special namespace as prefix for Redis key, when your have more than one project used redis-search, this config will make them work fine.
|
42
49
|
redis = Redis::Namespace.new("your_app_name:redis_search", :redis => redis)
|
43
50
|
Redis::Search.configure do |config|
|
44
51
|
config.redis = redis
|
@@ -63,6 +70,7 @@ bind Redis::Search callback event, it will to rebuild search indexes when data c
|
|
63
70
|
|
64
71
|
redis_search_index(:title_field => :title,
|
65
72
|
:score_field => :hits,
|
73
|
+
:condition_fields => [:user_id, :category_id],
|
66
74
|
:ext_fields => [:category_name])
|
67
75
|
|
68
76
|
def category_name
|
@@ -88,12 +96,12 @@ bind Redis::Search callback event, it will to rebuild search indexes when data c
|
|
88
96
|
class SearchController < ApplicationController
|
89
97
|
# GET /searchs?q=title
|
90
98
|
def index
|
91
|
-
Redis::Search.query("Post", params[:q])
|
99
|
+
Redis::Search.query("Post", params[:q], :conditions => {:user_id => 12})
|
92
100
|
end
|
93
101
|
|
94
102
|
# GET /search_users?q=j
|
95
103
|
def search_users
|
96
|
-
Redis::Search.complete("Post", params[:q])
|
104
|
+
Redis::Search.complete("Post", params[:q], :conditions => {:user_id => 12, :category_id => 4})
|
97
105
|
end
|
98
106
|
end
|
99
107
|
|
@@ -109,6 +117,9 @@ see [Rdoc.info redis-search](http://rubydoc.info/gems/redis-search)
|
|
109
117
|
|
110
118
|
## Benchmark test
|
111
119
|
|
120
|
+
You can run the rake command (see Rakefile) to make test.
|
121
|
+
There is my performance test result.
|
122
|
+
|
112
123
|
* [https://gist.github.com/1150933](https://gist.github.com/1150933)
|
113
124
|
|
114
125
|
## Demo
|
data/lib/redis/search/base.rb
CHANGED
@@ -15,15 +15,18 @@ class Redis
|
|
15
15
|
prefix_index_enable = options[:prefix_index_enable] || false
|
16
16
|
ext_fields = options[:ext_fields] || []
|
17
17
|
score_field = options[:score_field] || :created_at
|
18
|
+
condition_fields = options[:condition_fields] || []
|
18
19
|
# Add score field to ext_fields
|
19
|
-
ext_fields
|
20
|
+
ext_fields |= [score_field]
|
21
|
+
# Add condition fields to ext_fields
|
22
|
+
ext_fields |= condition_fields
|
20
23
|
|
21
24
|
# store Model name to indexed_models for Rake tasks
|
22
25
|
Search.indexed_models = [] if Search.indexed_models == nil
|
23
26
|
Search.indexed_models << self
|
24
27
|
# bind instance methods and callback events
|
25
28
|
class_eval %(
|
26
|
-
def
|
29
|
+
def redis_search_fields_to_hash(ext_fields)
|
27
30
|
exts = {}
|
28
31
|
ext_fields.each do |f|
|
29
32
|
exts[f] = instance_eval(f.to_s)
|
@@ -34,11 +37,12 @@ class Redis
|
|
34
37
|
# after_create :redis_search_index_create
|
35
38
|
def redis_search_index_create
|
36
39
|
s = Search::Index.new(:title => self.#{title_field},
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
40
|
+
:id => self.id,
|
41
|
+
:exts => self.redis_search_fields_to_hash(#{ext_fields.inspect}),
|
42
|
+
:type => self.class.to_s,
|
43
|
+
:condition_fields => #{condition_fields},
|
44
|
+
:score => self.#{score_field}.to_i,
|
45
|
+
:prefix_index_enable => #{prefix_index_enable})
|
42
46
|
s.save
|
43
47
|
# release s
|
44
48
|
s = nil
|
data/lib/redis/search/finder.rb
CHANGED
@@ -20,39 +20,54 @@ class Redis
|
|
20
20
|
# * Redis::Search.complete("Tag","red") => ["Redis", "Redmine"]
|
21
21
|
# * Redis::Search.complete("Tag","redi") => ["Redis"]
|
22
22
|
def self.complete(type, w, options = {})
|
23
|
-
return [] if w.blank? or type.blank?
|
24
23
|
limit = options[:limit] || 10
|
25
|
-
|
24
|
+
conditions = options[:conditions] || []
|
25
|
+
return [] if (w.blank? and conditions.blank?) or type.blank?
|
26
|
+
|
26
27
|
prefix_matchs = []
|
27
28
|
# This is not random, try to get replies < MTU size
|
28
29
|
rangelen = Redis::Search.config.complete_max_length
|
29
30
|
prefix = w.downcase
|
30
31
|
key = Search.mk_complete_key(type)
|
31
32
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
33
|
+
|
34
|
+
if start = Redis::Search.config.redis.zrank(key,prefix)
|
35
|
+
count = limit
|
36
|
+
max_range = start+(rangelen*limit)-1
|
37
|
+
range = Redis::Search.config.redis.zrange(key,start,max_range)
|
38
|
+
while prefix_matchs.length <= count
|
39
|
+
start += rangelen
|
40
|
+
break if !range or range.length == 0
|
41
|
+
range.each {|entry|
|
42
|
+
minlen = [entry.length,prefix.length].min
|
43
|
+
if entry[0...minlen] != prefix[0...minlen]
|
44
|
+
count = prefix_matchs.count
|
45
|
+
break
|
46
|
+
end
|
47
|
+
if entry[-1..-1] == "*" and prefix_matchs.length != count
|
48
|
+
prefix_matchs << entry[0...-1]
|
49
|
+
end
|
50
|
+
}
|
51
|
+
range = range[start..max_range]
|
52
|
+
end
|
51
53
|
end
|
54
|
+
|
55
|
+
# 组合 words 的特别 key 名
|
52
56
|
words = []
|
53
57
|
words = prefix_matchs.uniq.collect { |w| Search.mk_sets_key(type,w) }
|
58
|
+
|
59
|
+
# 组合特别 key ,但这里不会像 query 那样放入 words, 因为在 complete 里面 words 是用 union 取的,condition_keys 和 words 应该取交集
|
60
|
+
condition_keys = []
|
61
|
+
if !conditions.blank?
|
62
|
+
conditions = conditions[0] if conditions.is_a?(Array)
|
63
|
+
conditions.keys.each do |c|
|
64
|
+
condition_keys << Search.mk_condition_key(type,c,conditions[c])
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# 按词语搜索
|
69
|
+
temp_store_key = "tmpsunionstore:#{words.join("+")}"
|
54
70
|
if words.length > 1
|
55
|
-
temp_store_key = "tmpsunionstore:#{words.join("+")}"
|
56
71
|
if !Redis::Search.config.redis.exists(temp_store_key)
|
57
72
|
# 将多个词语组合对比,得到并集,并存入临时区域
|
58
73
|
Redis::Search.config.redis.sunionstore(temp_store_key,*words)
|
@@ -60,16 +75,24 @@ class Redis
|
|
60
75
|
Redis::Search.config.redis.expire(temp_store_key,86400)
|
61
76
|
end
|
62
77
|
# 根据需要的数量取出 ids
|
63
|
-
ids = Redis::Search.config.redis.sort(temp_store_key,
|
64
|
-
:limit => [0,limit],
|
65
|
-
:by => Search.mk_score_key(type,"*"),
|
66
|
-
:order => "desc")
|
67
78
|
else
|
68
|
-
|
69
|
-
:limit => [0,limit],
|
70
|
-
:by => Search.mk_score_key(type,"*"),
|
71
|
-
:order => "desc")
|
79
|
+
temp_store_key = words.first
|
72
80
|
end
|
81
|
+
|
82
|
+
# 如果有条件,这里再次组合一下
|
83
|
+
if !condition_keys.blank?
|
84
|
+
condition_keys << temp_store_key if !words. blank?
|
85
|
+
temp_store_key = "tmpsinterstore:#{condition_keys.join('+')}"
|
86
|
+
if !Redis::Search.config.redis.exists(temp_store_key)
|
87
|
+
Redis::Search.config.redis.sinterstore(temp_store_key,*condition_keys)
|
88
|
+
Redis::Search.config.redis.expire(temp_store_key,86400)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
ids = Redis::Search.config.redis.sort(temp_store_key,
|
93
|
+
:limit => [0,limit],
|
94
|
+
:by => Search.mk_score_key(type,"*"),
|
95
|
+
:order => "desc")
|
73
96
|
return [] if ids.blank?
|
74
97
|
hmget(type,ids)
|
75
98
|
end
|
@@ -85,14 +108,28 @@ class Redis
|
|
85
108
|
def self.query(type, text,options = {})
|
86
109
|
tm = Time.now
|
87
110
|
result = []
|
88
|
-
|
89
|
-
|
111
|
+
|
90
112
|
limit = options[:limit] || 10
|
91
113
|
sort_field = options[:sort_field] || "id"
|
114
|
+
conditions = options[:conditions] || []
|
115
|
+
|
116
|
+
# 如果搜索文本和查询条件均没有,那就直接返回 []
|
117
|
+
return result if text.strip.blank? and conditions.blank?
|
92
118
|
|
93
119
|
words = Search.split(text)
|
94
120
|
words = words.collect { |w| Search.mk_sets_key(type,w) }
|
95
121
|
|
122
|
+
|
123
|
+
condition_keys = []
|
124
|
+
if !conditions.blank?
|
125
|
+
conditions = conditions[0] if conditions.is_a?(Array)
|
126
|
+
conditions.keys.each do |c|
|
127
|
+
condition_keys << Search.mk_condition_key(type,c,conditions[c])
|
128
|
+
end
|
129
|
+
# 将条件的 key 放入关键词搜索集合内,用于 sinterstore 搜索
|
130
|
+
words += condition_keys
|
131
|
+
end
|
132
|
+
|
96
133
|
return result if words.blank?
|
97
134
|
|
98
135
|
temp_store_key = "tmpinterstore:#{words.join("+")}"
|
@@ -108,6 +145,7 @@ class Redis
|
|
108
145
|
if Search.config.pinyin_match
|
109
146
|
pinyin_words = Search.split_pinyin(text)
|
110
147
|
pinyin_words = pinyin_words.collect { |w| Search.mk_sets_key(type,w) }
|
148
|
+
pinyin_words += condition_keys
|
111
149
|
temp_sunion_key = "tmpsunionstore:#{words.join("+")}"
|
112
150
|
if Search.config.pinyin_match
|
113
151
|
temp_pinyin_store_key = "tmpinterstore:#{pinyin_words.join("+")}"
|
@@ -182,6 +220,10 @@ class Redis
|
|
182
220
|
def self.mk_score_key(type, id)
|
183
221
|
"#{type}:_score_:#{id}"
|
184
222
|
end
|
223
|
+
|
224
|
+
def self.mk_condition_key(type, field, id)
|
225
|
+
"#{type}:_by:_#{field}:#{id}"
|
226
|
+
end
|
185
227
|
|
186
228
|
def self.mk_complete_key(type)
|
187
229
|
"Compl#{type}"
|
data/lib/redis/search/index.rb
CHANGED
@@ -1,9 +1,14 @@
|
|
1
1
|
class Redis
|
2
2
|
module Search
|
3
3
|
class Index
|
4
|
-
attr_accessor :type, :title, :id,:score, :exts, :prefix_index_enable
|
4
|
+
attr_accessor :type, :title, :id,:score, :exts, :condition_fields, :prefix_index_enable
|
5
5
|
def initialize(options = {})
|
6
|
+
# default data
|
7
|
+
self.condition_fields = []
|
6
8
|
self.exts = []
|
9
|
+
self.prefix_index_enable = false
|
10
|
+
|
11
|
+
# set attributes value from params
|
7
12
|
options.keys.each do |k|
|
8
13
|
eval("self.#{k} = options[k]")
|
9
14
|
end
|
@@ -28,11 +33,16 @@ class Redis
|
|
28
33
|
# score for search sort
|
29
34
|
Redis::Search.config.redis.set(Search.mk_score_key(self.type,self.id),self.score)
|
30
35
|
end
|
36
|
+
|
37
|
+
# 将目前的编号保存到条件(conditions)字段所创立的索引上面
|
38
|
+
self.condition_fields.each do |field|
|
39
|
+
Redis::Search.config.redis.sadd(Search.mk_condition_key(self.type,field,data[field.to_sym]), self.id)
|
40
|
+
end
|
31
41
|
|
32
42
|
# 建立前最索引
|
33
43
|
if prefix_index_enable
|
34
44
|
save_prefix_index
|
35
|
-
end
|
45
|
+
end
|
36
46
|
end
|
37
47
|
|
38
48
|
def self.remove(options = {})
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: redis-search
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.7.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,11 +9,11 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2011-10-
|
12
|
+
date: 2011-10-17 00:00:00.000000000Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: chinese_pinyin
|
16
|
-
requirement: &
|
16
|
+
requirement: &2156513140 !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ! '>='
|
@@ -21,10 +21,10 @@ dependencies:
|
|
21
21
|
version: 0.3.0
|
22
22
|
type: :runtime
|
23
23
|
prerelease: false
|
24
|
-
version_requirements: *
|
24
|
+
version_requirements: *2156513140
|
25
25
|
- !ruby/object:Gem::Dependency
|
26
26
|
name: rmmseg-cpp-huacnlee
|
27
|
-
requirement: &
|
27
|
+
requirement: &2156512660 !ruby/object:Gem::Requirement
|
28
28
|
none: false
|
29
29
|
requirements:
|
30
30
|
- - ! '>='
|
@@ -32,10 +32,10 @@ dependencies:
|
|
32
32
|
version: 0.2.8
|
33
33
|
type: :runtime
|
34
34
|
prerelease: false
|
35
|
-
version_requirements: *
|
35
|
+
version_requirements: *2156512660
|
36
36
|
- !ruby/object:Gem::Dependency
|
37
37
|
name: redis-namespace
|
38
|
-
requirement: &
|
38
|
+
requirement: &2156512200 !ruby/object:Gem::Requirement
|
39
39
|
none: false
|
40
40
|
requirements:
|
41
41
|
- - ~>
|
@@ -43,10 +43,10 @@ dependencies:
|
|
43
43
|
version: 1.0.2
|
44
44
|
type: :runtime
|
45
45
|
prerelease: false
|
46
|
-
version_requirements: *
|
46
|
+
version_requirements: *2156512200
|
47
47
|
- !ruby/object:Gem::Dependency
|
48
48
|
name: redis
|
49
|
-
requirement: &
|
49
|
+
requirement: &2156511720 !ruby/object:Gem::Requirement
|
50
50
|
none: false
|
51
51
|
requirements:
|
52
52
|
- - ! '>='
|
@@ -54,7 +54,7 @@ dependencies:
|
|
54
54
|
version: 2.1.1
|
55
55
|
type: :runtime
|
56
56
|
prerelease: false
|
57
|
-
version_requirements: *
|
57
|
+
version_requirements: *2156511720
|
58
58
|
description: High performance real-time search (Support Chinese), index in Redis for
|
59
59
|
Rails application.
|
60
60
|
email:
|