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 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
@@ -0,0 +1,11 @@
1
+ require "redis_search/base"
2
+ require "redis_search/search"
3
+ require "redis_search/config"
4
+
5
+ module RedisSearch
6
+ class << self
7
+ def configure
8
+ yield self.config ||= Config.new
9
+ end
10
+ end
11
+ end
@@ -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,14 @@
1
+ module RedisSearch
2
+ class << self
3
+ attr_accessor :config
4
+ end
5
+
6
+ class Config
7
+ attr_accessor :redis, :debug
8
+
9
+ def initialize
10
+ self.debug = false
11
+ self.redis = nil
12
+ end
13
+ end
14
+ 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: []