redis-textsearch 0.1.2 → 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc CHANGED
@@ -17,13 +17,11 @@ that just returns a string, or an MD5 of a filename, or something else unique.
17
17
 
18
18
  == Installation
19
19
 
20
- gem install gemcutter
21
- gem tumble
22
20
  gem install redis-textsearch
23
21
 
24
22
  == Initialization
25
23
 
26
- If on Rails, config/initializers/redis.rb is a good place for this:
24
+ If you're using Rails, config/initializers/redis.rb is a good place for this:
27
25
 
28
26
  require 'redis'
29
27
  require 'redis/text_search'
@@ -89,5 +87,5 @@ hash must be in brackets:
89
87
 
90
88
  == Author
91
89
 
92
- Copyright (c) 2009 {Nate Wiger}[http://nate.wiger.org]. All Rights Reserved.
90
+ Copyright (c) 2009-2010 {Nate Wiger}[http://nate.wiger.org]. All Rights Reserved.
93
91
  Released under the {Artistic License}[http://www.opensource.org/licenses/artistic-license-2.0.php].
@@ -118,6 +118,19 @@ module Redis::TextSearch
118
118
  @total_entries = number.to_i
119
119
  @total_pages = (@total_entries / per_page.to_f).ceil
120
120
  end
121
+
122
+ # returns the current start ordinal of the paginated collection
123
+ def row_start
124
+ @total_entries > 0 ? offset + 1 : 0
125
+ end
126
+
127
+ # returns the current end ordinal of the paginated collection
128
+ def row_end
129
+ end_ordinal = offset + per_page
130
+ # bounds check row_end
131
+ end_ordinal = @total_entries if end_ordinal > @total_entries
132
+ end_ordinal
133
+ end
121
134
 
122
135
  # This is a magic wrapper for the original Array#replace method. It serves
123
136
  # for populating the paginated collection after initialization.
@@ -7,6 +7,7 @@ class Redis
7
7
  module TextSearch
8
8
  class NoFinderMethod < StandardError; end
9
9
  class BadTextIndex < StandardError; end
10
+ class BadConditions < StandardError; end
10
11
 
11
12
  DEFAULT_EXCLUDE_LIST = %w(a an and as at but by for in into of on onto to the)
12
13
 
@@ -26,7 +27,7 @@ class Redis
26
27
  klass.instance_variable_set('@text_index_exclude_list', DEFAULT_EXCLUDE_LIST)
27
28
  klass.send :include, InstanceMethods
28
29
  klass.extend ClassMethods
29
- klass.guess_text_search_find
30
+ klass.extend WpHelpers unless respond_to?(:wp_count)
30
31
  class << klass
31
32
  define_method(:per_page) { 30 } unless respond_to?(:per_page)
32
33
  end
@@ -61,31 +62,40 @@ class Redis
61
62
  # This is called when the class is imported, and uses reflection to guess
62
63
  # how to retrieve records. You can override it by explicitly defining a
63
64
  # +text_search_find+ class method that takes an array of IDs as an argument.
64
- def guess_text_search_find
65
- if defined?(ActiveRecord::Base) and ancestors.include?(ActiveRecord::Base)
66
- instance_eval <<-EndMethod
67
- def text_search_find(ids, options)
68
- all(options.merge(:conditions => {:#{primary_key} => ids}))
69
- end
70
- EndMethod
71
- elsif defined?(MongoRecord::Base) and ancestors.include?(MongoRecord::Base)
72
- instance_eval <<-EndMethod
73
- def text_search_find(ids, options)
74
- all(options.merge(:conditions => {:#{primary_key} => ids}))
75
- end
76
- EndMethod
65
+ def text_search_find(ids, options)
66
+ if defined?(ActiveModel)
67
+ # guess that we're on Rails 3
68
+ raise "text_search_find not implemented for Rails 3 (yet) - patches welcome"
69
+ elsif defined?(ActiveRecord::Base) and ancestors.include?(ActiveRecord::Base)
70
+ merge_text_search_conditions!(ids, options)
71
+ all(options)
77
72
  elsif defined?(Sequel::Model) and ancestors.include?(Sequel::Model)
78
- instance_eval <<-EndMethod
79
- def text_search_find(ids, options)
80
- all(options.merge(:conditions => {:#{primary_key} => ids}))
81
- end
82
- EndMethod
73
+ self[primary_key.to_sym => ids].filter(options)
83
74
  elsif defined?(DataMapper::Resource) and included_modules.include?(DataMapper::Resource)
84
- instance_eval <<-EndMethod
85
- def text_search_find(ids, options)
86
- get(ids, options)
87
- end
88
- EndMethod
75
+ get(options.merge(primary_key.to_sym => ids))
76
+ end
77
+ end
78
+
79
+ def merge_text_search_conditions!(ids, options)
80
+ pk = "#{table_name}.#{primary_key}"
81
+ case options[:conditions]
82
+ when Array
83
+ if options[:conditions][1].is_a?(Hash)
84
+ options[:conditions][0] = "(#{options[:conditions][0]}) AND #{pk} IN (:text_search_ids)"
85
+ options[:conditions][1][:text_search_ids] = ids
86
+ else
87
+ options[:conditions][0] = "(#{options[:conditions][0]}) AND #{pk} IN (?)"
88
+ options[:conditions] << ids
89
+ end
90
+ when Hash
91
+ if options[:conditions].has_key?(primary_key.to_sym)
92
+ raise BadConditions, "Cannot specify primary key (#{pk}) in :conditions to #{self.name}.text_search"
93
+ end
94
+ options[:conditions][primary_key.to_sym] = ids
95
+ when String
96
+ options[:conditions] = ["(#{options[:conditions]}) AND #{pk} IN (?)", ids]
97
+ else
98
+ options.merge!(:conditions => {primary_key => ids})
89
99
  end
90
100
  end
91
101
 
@@ -93,7 +103,7 @@ class Redis
93
103
  # update_text_indexes after record save or at the appropriate point.
94
104
  def text_index(*args)
95
105
  options = args.last.is_a?(Hash) ? args.pop : {}
96
- options[:minlength] ||= 1
106
+ options[:minlength] ||= 2
97
107
  options[:split] ||= /\s+/
98
108
  raise ArgumentError, "Must specify fields to index to #{self.name}.text_index" unless args.length > 0
99
109
  args.each do |name|
@@ -141,21 +151,29 @@ class Redis
141
151
  end
142
152
  end
143
153
 
154
+ # Assemble our options for our finder conditions (destructive for speed)
155
+ recalculate_count = options.has_key?(:conditions)
156
+
144
157
  # Calculate pagination if applicable. Presence of :page indicates we want pagination.
145
158
  # Adapted from will_paginate/finder.rb
146
159
  if options.has_key?(:page)
147
160
  page = options.delete(:page) || 1
148
161
  per_page = options.delete(:per_page) || self.per_page
149
- total = ids.length
150
162
 
151
- Redis::TextSearch::Collection.create(page, per_page, total) do |pager|
163
+ Redis::TextSearch::Collection.create(page, per_page, nil) do |pager|
152
164
  # Convert page/per_page to limit/offset
153
165
  options.merge!(:offset => pager.offset, :limit => pager.per_page)
154
- pager.replace(send(finder, ids, options){ |*a| yield(*a) if block_given? })
166
+ if ids.empty?
167
+ pager.replace([])
168
+ pager.total_entries = 0
169
+ else
170
+ pager.replace(send(finder, ids, options){ |*a| yield(*a) if block_given? })
171
+ pager.total_entries = recalculate_count ? wp_count(options, [], finder.to_s) : ids.length # hacked into will_paginate for compat
172
+ end
155
173
  end
156
174
  else
157
175
  # Execute finder directly
158
- send(finder, ids, options)
176
+ ids.empty? ? [] : send(finder, ids, options)
159
177
  end
160
178
  end
161
179
 
@@ -221,7 +239,7 @@ class Redis
221
239
  fields = self.class.text_indexes.keys if fields.empty?
222
240
  fields.each do |field|
223
241
  options = self.class.text_indexes[field]
224
- value = self.send(field)
242
+ value = self.send(field).to_s
225
243
  return false if value.length < options[:minlength] # too short to index
226
244
  indexes = []
227
245
 
@@ -238,13 +256,24 @@ class Redis
238
256
  indexes << "#{options[:key]}:#{str}"
239
257
  else
240
258
  len = options[:minlength]
241
- while len < val.length
242
- str = val[0..len].gsub(/\s+/, '.') # can't have " " in Redis cmd string
259
+ while len <= val.length
260
+ str = val[0,len].gsub(/\s+/, '.') # can't have " " in Redis cmd string
243
261
  indexes << "#{options[:key]}:#{str}"
244
262
  len += 1
245
263
  end
246
264
  end
247
265
  end
266
+
267
+ # Also left-anchor the cropped string if "full" is specified
268
+ if options[:full]
269
+ val = values.join('.')
270
+ len = options[:minlength]
271
+ while len <= val.length
272
+ str = val[0,len].gsub(/\s+/, '.') # can't have " " in Redis cmd string
273
+ indexes << "#{options[:key]}:#{str}"
274
+ len += 1
275
+ end
276
+ end
248
277
 
249
278
  # Determine what, if anything, needs to be done. If the indexes are unchanged,
250
279
  # don't make any trips to Redis. Saves tons of useless network calls.
@@ -288,7 +317,67 @@ class Redis
288
317
  end
289
318
  end
290
319
  end
291
-
292
- end
320
+
321
+ end # InstanceMethods
322
+
323
+ module WpHelpers
324
+ # Does the not-so-trivial job of finding out the total number of entries
325
+ # in the database. It relies on the ActiveRecord +count+ method.
326
+ def wp_count(options, args, finder)
327
+ excludees = [:count, :order, :limit, :offset, :readonly]
328
+ if defined?(ActiveRecord::Calculations)
329
+ excludees << :from unless ActiveRecord::Calculations::CALCULATIONS_OPTIONS.include?(:from)
330
+ end
331
+
332
+ # we may be in a model or an association proxy
333
+ klass = (@owner and @reflection) ? @reflection.klass : self
334
+
335
+ # Use :select from scope if it isn't already present.
336
+ options[:select] = scope(:find, :select) unless options[:select]
337
+
338
+ if options[:select] and options[:select] =~ /^\s*DISTINCT\b/i
339
+ # Remove quoting and check for table_name.*-like statement.
340
+ if options[:select].gsub('`', '') =~ /\w+\.\*/
341
+ options[:select] = "DISTINCT #{klass.table_name}.#{klass.primary_key}"
342
+ end
343
+ else
344
+ excludees << :select # only exclude the select param if it doesn't begin with DISTINCT
345
+ end
346
+
347
+ # count expects (almost) the same options as find
348
+ count_options = options.except *excludees
349
+
350
+ # merge the hash found in :count
351
+ # this allows you to specify :select, :order, or anything else just for the count query
352
+ count_options.update options[:count] if options[:count]
353
+
354
+ # forget about includes if they are irrelevant (Rails 2.1)
355
+ # if count_options[:include] and
356
+ # klass.private_methods.include_method?(:references_eager_loaded_tables?) and
357
+ # !klass.send(:references_eager_loaded_tables?, count_options)
358
+ # count_options.delete :include
359
+ # end
360
+
361
+ # we may have to scope ...
362
+ counter = Proc.new { count(count_options) }
363
+
364
+ count = if finder.index('find_') == 0 and klass.respond_to?(scoper = finder.sub('find', 'with'))
365
+ # scope_out adds a 'with_finder' method which acts like with_scope, if it's present
366
+ # then execute the count with the scoping provided by the with_finder
367
+ send(scoper, &counter)
368
+ elsif finder =~ /^find_(all_by|by)_([_a-zA-Z]\w*)$/
369
+ # extract conditions from calls like "paginate_by_foo_and_bar"
370
+ attribute_names = $2.split('_and_')
371
+ conditions = construct_attributes_from_arguments(attribute_names, args)
372
+ with_scope(:find => { :conditions => conditions }, &counter)
373
+ else
374
+ counter.call
375
+ end
376
+
377
+ count.respond_to?(:length) ? count.length : count
378
+ end
379
+
380
+ end # WpHelpers
381
+
293
382
  end
294
383
  end
@@ -1,8 +1,13 @@
1
1
 
2
2
  require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
3
3
 
4
+ DBFILE = File.dirname(__FILE__) + '/test.db'
5
+
6
+ require 'logger'
4
7
  require 'active_record'
5
- ActiveRecord::Base.establish_connection(:adapter => 'sqlite3', :database => 'test.db')
8
+ require 'fileutils'
9
+
10
+ ActiveRecord::Base.establish_connection(:adapter => 'sqlite3', :database => DBFILE)
6
11
  ActiveRecord::Base.logger = Logger.new(STDOUT)
7
12
 
8
13
  def check_result_ids(results, ids, sort=true)
@@ -20,9 +25,14 @@ end
20
25
  class Post < ActiveRecord::Base
21
26
  include Redis::TextSearch
22
27
  include Marshal
28
+
29
+ BLOG = 101
30
+ INFO = 102
31
+ CAL = 103
23
32
 
24
33
  text_index :title
25
34
  text_index :tags, :exact => true
35
+ text_index :type_id, :exact => true
26
36
  end
27
37
 
28
38
  class CreatePosts < ActiveRecord::Migration
@@ -30,6 +40,7 @@ class CreatePosts < ActiveRecord::Migration
30
40
  create_table :posts do |t|
31
41
  t.string :title
32
42
  t.string :tags
43
+ t.integer :type_id
33
44
  t.timestamps
34
45
  end
35
46
  end
@@ -52,20 +63,25 @@ TAGS = [
52
63
  ['gaming','technical']
53
64
  ]
54
65
 
66
+ TYPES = [
67
+ Post::BLOG,
68
+ Post::BLOG,
69
+ Post::INFO
70
+ ]
55
71
 
56
72
  describe Redis::TextSearch do
57
73
  before :all do
58
74
  CreatePosts.up
59
75
 
60
- @post = Post.new(:title => TITLES[0], :tags => TAGS[0] * ' ')
76
+ @post = Post.new(:title => TITLES[0], :tags => TAGS[0] * ' ', :type_id => TYPES[0])
61
77
  # @post.id = 1
62
78
  @post.save!
63
79
  # sleep 1 # sqlite timestamps
64
- @post2 = Post.new(:title => TITLES[1], :tags => TAGS[1] * ' ')
80
+ @post2 = Post.new(:title => TITLES[1], :tags => TAGS[1] * ' ', :type_id => TYPES[1])
65
81
  # @post2.id = 2
66
82
  @post2.save!
67
83
  # sleep 1 # sqlite timestamps
68
- @post3 = Post.new(:title => TITLES[2], :tags => TAGS[2] * ' ')
84
+ @post3 = Post.new(:title => TITLES[2], :tags => TAGS[2] * ' ', :type_id => TYPES[2])
69
85
  # @post3.id = 3
70
86
  @post3.save!
71
87
  # sleep 1 # sqlite timestamps
@@ -77,6 +93,7 @@ describe Redis::TextSearch do
77
93
 
78
94
  after :all do
79
95
  CreatePosts.down
96
+ FileUtils.rm_f DBFILE
80
97
  end
81
98
 
82
99
  it "should define text indexes in the class" do
@@ -153,6 +170,7 @@ describe Redis::TextSearch do
153
170
  res.first.tags
154
171
  rescue => error
155
172
  end
173
+ error.should_not be_nil
156
174
  error.should be_kind_of ActiveRecord::MissingAttributeError
157
175
 
158
176
  error = nil
@@ -160,6 +178,7 @@ describe Redis::TextSearch do
160
178
  res.first.updated_at
161
179
  rescue => error
162
180
  end
181
+ error.should_not be_nil
163
182
  error.should be_kind_of ActiveRecord::MissingAttributeError
164
183
 
165
184
  error = nil
@@ -167,7 +186,25 @@ describe Redis::TextSearch do
167
186
  res.first.created_at
168
187
  rescue => error
169
188
  end
189
+ error.should_not be_nil
170
190
  error.should be_kind_of ActiveRecord::MissingAttributeError
191
+
192
+ # Merging conditions for SQL
193
+ check_result_ids Post.text_search('some', :conditions => 'id < 2'), [1]
194
+ check_result_ids Post.text_search('some', :conditions => ['id < ?', 2]), [1]
195
+ check_result_ids Post.text_search('some', :conditions => ['id = ?', 1]), [1]
196
+ check_result_ids Post.text_search('some', :conditions => ['id = ? and title = ?', 1, TITLES[0]]), [1]
197
+ check_result_ids Post.text_search('some', :conditions => ['title = :title and id = :id', {:id => 1, :title => TITLES[0]}]), [1]
198
+ check_result_ids Post.text_search('some', :conditions => {:title => TITLES[0]}), [1]
199
+ check_result_ids Post.text_search('some', :conditions => {:title => TITLES[0], :tags => TAGS[0] * ' '}), [1]
200
+
201
+ error = nil
202
+ begin
203
+ check_result_ids Post.text_search('some', :conditions => {:id => 1}), [1]
204
+ rescue => error
205
+ end
206
+ error.should_not be_nil
207
+ error.should be_kind_of Redis::TextSearch::BadConditions
171
208
  end
172
209
 
173
210
  it "should handle pagination" do
@@ -191,6 +228,21 @@ describe Redis::TextSearch do
191
228
  res.total_pages.should == 1
192
229
  res.per_page.should == 5
193
230
  res.current_page.should == 2
231
+
232
+ res = Post.text_search('some', :page => 1, :per_page => 1, :conditions => ['id > ?', 2], :order => 'id')
233
+ check_result_ids res, [3]
234
+ res.total_entries.should == 1
235
+ res.total_pages.should == 1
236
+ res.per_page.should == 1
237
+ res.current_page.should == 1
238
+
239
+ res = Post.text_search('some', :page => 1, :per_page => 1, :order => 'id',
240
+ :conditions => ['title = :title', {:title => TITLES[0]}])
241
+ check_result_ids res, [1]
242
+ res.total_entries.should == 1
243
+ res.total_pages.should == 1
244
+ res.per_page.should == 1
245
+ res.current_page.should == 1
194
246
  end
195
247
 
196
248
  it "should support a hash to the text_search method" do
@@ -6,6 +6,10 @@ class Post
6
6
 
7
7
  text_index :title
8
8
  text_index :tags, :exact => true
9
+ text_index :description, :full => true
10
+
11
+ def self.table_name; 'post'; end
12
+ def self.primary_key; 'id'; end
9
13
 
10
14
  def self.text_search_find(ids, options)
11
15
  options.empty? ? ids : [ids, options]
@@ -21,7 +25,7 @@ class Post
21
25
  end
22
26
  def id; @id; end
23
27
  def method_missing(name, *args)
24
- @attrib[name] || super
28
+ @attrib.has_key?(name) ? @attrib[name] : super
25
29
  end
26
30
  end
27
31
 
@@ -35,15 +39,15 @@ TITLES = [
35
39
  TAGS = [
36
40
  ['personal', 'nontechnical'],
37
41
  ['mysql', 'technical'],
38
- ['gaming','technical']
42
+ ['gaming','technical'],
43
+ ['character', 'halloween']
39
44
  ]
40
45
 
41
-
42
46
  describe Redis::TextSearch do
43
47
  before :all do
44
- @post = Post.new(:title => TITLES[0], :tags => TAGS[0] * ' ', :id => 1)
45
- @post2 = Post.new(:title => TITLES[1], :tags => TAGS[1], :id => 2)
46
- @post3 = Post.new(:title => TITLES[2], :tags => TAGS[2] * ' ', :id => 3)
48
+ @post = Post.new(:title => TITLES[0], :tags => TAGS[0] * ' ', :id => 1, :description => nil)
49
+ @post2 = Post.new(:title => TITLES[1], :tags => TAGS[1], :id => 2, :description => nil)
50
+ @post3 = Post.new(:title => TITLES[2], :tags => TAGS[2] * ' ', :id => 3, :description => nil)
47
51
 
48
52
  @post.delete_text_indexes
49
53
  @post2.delete_text_indexes
@@ -59,6 +63,7 @@ describe Redis::TextSearch do
59
63
  @post.update_text_indexes
60
64
  @post2.update_text_indexes
61
65
 
66
+ Post.redis.set_members('post:text_index:title:s').should == []
62
67
  Post.redis.set_members('post:text_index:title:so').should == ['1']
63
68
  Post.redis.set_members('post:text_index:title:som').should == ['1']
64
69
  Post.redis.set_members('post:text_index:title:some').should == ['1']
@@ -66,6 +71,7 @@ describe Redis::TextSearch do
66
71
  Post.redis.set_members('post:text_index:title:pla').sort.should == ['1','2']
67
72
  Post.redis.set_members('post:text_index:title:plai').sort.should == ['1','2']
68
73
  Post.redis.set_members('post:text_index:title:plain').sort.should == ['1','2']
74
+ Post.redis.set_members('post:text_index:title:t').should == []
69
75
  Post.redis.set_members('post:text_index:title:te').sort.should == ['1','2']
70
76
  Post.redis.set_members('post:text_index:title:tex').sort.should == ['1','2']
71
77
  Post.redis.set_members('post:text_index:title:text').sort.should == ['1','2']
@@ -75,6 +81,7 @@ describe Redis::TextSearch do
75
81
  Post.redis.set_members('post:text_index:title:textstri').should == ['2']
76
82
  Post.redis.set_members('post:text_index:title:textstrin').should == ['2']
77
83
  Post.redis.set_members('post:text_index:title:textstring').should == ['2']
84
+ Post.redis.set_members('post:text_index:tags:p').should == []
78
85
  Post.redis.set_members('post:text_index:tags:pe').should == []
79
86
  Post.redis.set_members('post:text_index:tags:per').should == []
80
87
  Post.redis.set_members('post:text_index:tags:pers').should == []
@@ -82,6 +89,7 @@ describe Redis::TextSearch do
82
89
  Post.redis.set_members('post:text_index:tags:person').should == []
83
90
  Post.redis.set_members('post:text_index:tags:persona').should == []
84
91
  Post.redis.set_members('post:text_index:tags:personal').should == ['1']
92
+ Post.redis.set_members('post:text_index:tags:n').should == []
85
93
  Post.redis.set_members('post:text_index:tags:no').should == []
86
94
  Post.redis.set_members('post:text_index:tags:non').should == []
87
95
  Post.redis.set_members('post:text_index:tags:nont').should == []
@@ -130,6 +138,39 @@ describe Redis::TextSearch do
130
138
  Post.text_search(:tags => ['technical','MYsql'], :title => 'some').should == []
131
139
  Post.text_search(:tags => 'technical', :title => 'comments').sort.should == ['2','3']
132
140
  end
141
+
142
+ it "should support full-phrase and sub-phrase simultaneously" do
143
+ @post4 = Post.new(:title => 'Dude', :description => 'Red flame dude', :tags => TAGS[3], :id => 4)
144
+ @post4.delete_text_indexes
145
+ @post4.update_text_indexes
146
+ Post.text_search(:description => 'Red flame dude').should == ['4']
147
+ Post.text_search(:description => 'Red flame dude', :tags => 'character').should == ['4']
148
+ Post.text_search(:description => 'Red').should == ['4']
149
+ Post.text_search(:description => 'Red', :tags => 'character').should == ['4']
150
+ Post.text_search(:description => 'Red', :tags => 'luigi').should == []
151
+ Post.text_search(:description => 'Red flame').should == ['4']
152
+ Post.text_search(:description => 'Red flame', :tags => 'halloween').should == ['4']
153
+ Post.text_search(:description => 'Red flame', :tags => 'hallowee').should == []
154
+ Post.text_search(:description => 'red FLame').should == ['4']
155
+ Post.text_search(:description => 'Red fla').should == ['4']
156
+ Post.text_search(:description => 'flame').should == ['4']
157
+ Post.text_search(:description => 'flame dude').should == [] # NOT SUPPORTED (must left-anchor)
158
+
159
+ Post.redis.set_members('post:text_index:description:red.flame.dude').should == ['4']
160
+ Post.redis.set_members('post:text_index:description:red.flame.dud').should == ['4']
161
+ Post.redis.set_members('post:text_index:description:red.flame.du').should == ['4']
162
+ Post.redis.set_members('post:text_index:description:red.flame.d').should == ['4']
163
+ Post.redis.set_members('post:text_index:description:red.flame.').should == ['4']
164
+ Post.redis.set_members('post:text_index:description:red.flame').should == ['4']
165
+ Post.redis.set_members('post:text_index:description:red.flam').should == ['4']
166
+ Post.redis.set_members('post:text_index:description:red.fla').should == ['4']
167
+ Post.redis.set_members('post:text_index:description:red.fl').should == ['4']
168
+ Post.redis.set_members('post:text_index:description:red.f').should == ['4']
169
+ Post.redis.set_members('post:text_index:description:red.').should == ['4']
170
+ Post.redis.set_members('post:text_index:description:red').should == ['4']
171
+ Post.redis.set_members('post:text_index:description:re').should == ['4']
172
+ Post.redis.set_members('post:text_index:description:r').should == []
173
+ end
133
174
 
134
175
  # MUST BE LAST!!!!!!
135
176
  it "should delete text indexes" do
@@ -139,5 +180,6 @@ describe Redis::TextSearch do
139
180
  @post.text_indexes.should == []
140
181
  @post2.text_indexes.should == []
141
182
  @post3.text_indexes.should == []
183
+ Post.delete_text_indexes(4)
142
184
  end
143
185
  end
@@ -0,0 +1,266 @@
1
+
2
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
3
+
4
+ DBFILE = File.dirname(__FILE__) + '/test.db'
5
+
6
+ require 'logger'
7
+ require 'sequel'
8
+ require 'fileutils'
9
+
10
+ Sequel.extension :migration
11
+ DB = Sequel.connect(:adapter => 'sqlite', :database => DBFILE, :loggers => [Logger.new(STDOUT)])
12
+
13
+ def check_result_ids(results, ids, sort=true)
14
+ results.length.should == ids.length
15
+ if results.length > 0
16
+ results.first.should be_kind_of(Sequel::Model)
17
+ end
18
+ if sort
19
+ results.collect{|m| m.id}.sort.should == ids.sort
20
+ else
21
+ results.collect{|m| m.id}.should == ids
22
+ end
23
+ end
24
+
25
+ class Post < Sequel::Model
26
+ include Redis::TextSearch
27
+ include Marshal
28
+
29
+ BLOG = 101
30
+ INFO = 102
31
+ CAL = 103
32
+
33
+ text_index :title
34
+ text_index :tags, :exact => true
35
+ text_index :type_id, :exact => true
36
+ end
37
+
38
+ class CreatePosts < Sequel::Migration
39
+ def up
40
+ create_table :posts do
41
+ primary_key :id
42
+ String :title
43
+ String :tags
44
+ Integer :type_id
45
+ end
46
+ end
47
+
48
+ def down
49
+ drop_table :posts
50
+ end
51
+ end
52
+
53
+ TITLES = [
54
+ 'Some plain text',
55
+ 'More plain textstring comments',
56
+ 'Come get somebody personal comments',
57
+ '*Welcome to Nate\'s new BLOG!!',
58
+ ]
59
+
60
+ TAGS = [
61
+ ['personal', 'nontechnical'],
62
+ ['mysql', 'technical'],
63
+ ['gaming','technical']
64
+ ]
65
+
66
+ TYPES = [
67
+ Post::BLOG,
68
+ Post::BLOG,
69
+ Post::INFO
70
+ ]
71
+
72
+ describe Redis::TextSearch do
73
+ before :all do
74
+ CreatePosts.apply(DB, :up)
75
+
76
+ @post = Post.new(:title => TITLES[0], :tags => TAGS[0] * ' ', :type_id => TYPES[0])
77
+ # @post.id = 1
78
+ @post.save!
79
+ # sleep 1 # sqlite timestamps
80
+ @post2 = Post.new(:title => TITLES[1], :tags => TAGS[1] * ' ', :type_id => TYPES[1])
81
+ # @post2.id = 2
82
+ @post2.save!
83
+ # sleep 1 # sqlite timestamps
84
+ @post3 = Post.new(:title => TITLES[2], :tags => TAGS[2] * ' ', :type_id => TYPES[2])
85
+ # @post3.id = 3
86
+ @post3.save!
87
+ # sleep 1 # sqlite timestamps
88
+
89
+ @post.delete_text_indexes
90
+ @post2.delete_text_indexes
91
+ Post.delete_text_indexes(3)
92
+ end
93
+
94
+ after :all do
95
+ CreatePosts.apply(DB, :down)
96
+ FileUtils.rm_f DBFILE
97
+ end
98
+
99
+ it "should define text indexes in the class" do
100
+ Post.text_indexes[:title][:key].should == 'post:text_index:title'
101
+ Post.text_indexes[:tags][:key].should == 'post:text_index:tags'
102
+ end
103
+
104
+ it "should update text indexes correctly" do
105
+ @post.update_text_indexes
106
+ @post2.update_text_indexes
107
+
108
+ Post.redis.set_members('post:text_index:title:so').should == ['1']
109
+ Post.redis.set_members('post:text_index:title:som').should == ['1']
110
+ Post.redis.set_members('post:text_index:title:some').should == ['1']
111
+ Post.redis.set_members('post:text_index:title:pl').sort.should == ['1','2']
112
+ Post.redis.set_members('post:text_index:title:pla').sort.should == ['1','2']
113
+ Post.redis.set_members('post:text_index:title:plai').sort.should == ['1','2']
114
+ Post.redis.set_members('post:text_index:title:plain').sort.should == ['1','2']
115
+ Post.redis.set_members('post:text_index:title:te').sort.should == ['1','2']
116
+ Post.redis.set_members('post:text_index:title:tex').sort.should == ['1','2']
117
+ Post.redis.set_members('post:text_index:title:text').sort.should == ['1','2']
118
+ Post.redis.set_members('post:text_index:title:texts').should == ['2']
119
+ Post.redis.set_members('post:text_index:title:textst').should == ['2']
120
+ Post.redis.set_members('post:text_index:title:textstr').should == ['2']
121
+ Post.redis.set_members('post:text_index:title:textstri').should == ['2']
122
+ Post.redis.set_members('post:text_index:title:textstrin').should == ['2']
123
+ Post.redis.set_members('post:text_index:title:textstring').should == ['2']
124
+ Post.redis.set_members('post:text_index:tags:pe').should == []
125
+ Post.redis.set_members('post:text_index:tags:per').should == []
126
+ Post.redis.set_members('post:text_index:tags:pers').should == []
127
+ Post.redis.set_members('post:text_index:tags:perso').should == []
128
+ Post.redis.set_members('post:text_index:tags:person').should == []
129
+ Post.redis.set_members('post:text_index:tags:persona').should == []
130
+ Post.redis.set_members('post:text_index:tags:personal').should == ['1']
131
+ Post.redis.set_members('post:text_index:tags:no').should == []
132
+ Post.redis.set_members('post:text_index:tags:non').should == []
133
+ Post.redis.set_members('post:text_index:tags:nont').should == []
134
+ Post.redis.set_members('post:text_index:tags:nonte').should == []
135
+ Post.redis.set_members('post:text_index:tags:nontec').should == []
136
+ Post.redis.set_members('post:text_index:tags:nontech').should == []
137
+ Post.redis.set_members('post:text_index:tags:nontechn').should == []
138
+ Post.redis.set_members('post:text_index:tags:nontechni').should == []
139
+ Post.redis.set_members('post:text_index:tags:nontechnic').should == []
140
+ Post.redis.set_members('post:text_index:tags:nontechnica').should == []
141
+ Post.redis.set_members('post:text_index:tags:nontechnical').should == ['1']
142
+ end
143
+
144
+ it "should search text indexes and return records" do
145
+ check_result_ids Post.text_search('some'), [1]
146
+ @post3.update_text_indexes
147
+ check_result_ids Post.text_search('some'), [1,3]
148
+
149
+ check_result_ids Post.text_search('plain'), [1,2]
150
+ check_result_ids Post.text_search('plain','text'), [1,2]
151
+ check_result_ids Post.text_search('plain','textstr'), [2]
152
+ check_result_ids Post.text_search('some','TExt'), [1]
153
+ check_result_ids Post.text_search('techNIcal'), [2,3]
154
+ check_result_ids Post.text_search('nontechnical'), [1]
155
+ check_result_ids Post.text_search('personal'), [1,3]
156
+ check_result_ids Post.text_search('personAL', :fields => :tags), [1]
157
+ check_result_ids Post.text_search('PERsonal', :fields => [:tags]), [1]
158
+ check_result_ids Post.text_search('nontechnical', :fields => [:title]), []
159
+ end
160
+
161
+ it "should pass options thru to find" do
162
+ check_result_ids Post.text_search('some', :order => 'id desc'), [3,1], false
163
+ res = Post.text_search('some', :select => 'id,title', :order => 'tags desc')
164
+ check_result_ids res, [1,3]
165
+ res.first.title.should == TITLES[0]
166
+ res.last.title.should == TITLES[2]
167
+
168
+ error = nil
169
+ begin
170
+ res.first.tags
171
+ rescue => error
172
+ end
173
+ error.should_not be_nil
174
+ error.should be_kind_of ActiveRecord::MissingAttributeError
175
+
176
+ error = nil
177
+ begin
178
+ res.first.updated_at
179
+ rescue => error
180
+ end
181
+ error.should_not be_nil
182
+ error.should be_kind_of ActiveRecord::MissingAttributeError
183
+
184
+ error = nil
185
+ begin
186
+ res.first.created_at
187
+ rescue => error
188
+ end
189
+ error.should_not be_nil
190
+ error.should be_kind_of ActiveRecord::MissingAttributeError
191
+
192
+ # Merging conditions for SQL
193
+ check_result_ids Post.text_search('some', :conditions => 'id < 2'), [1]
194
+ check_result_ids Post.text_search('some', :conditions => ['id < ?', 2]), [1]
195
+ check_result_ids Post.text_search('some', :conditions => ['id = ?', 1]), [1]
196
+ check_result_ids Post.text_search('some', :conditions => ['id = ? and title = ?', 1, TITLES[0]]), [1]
197
+ check_result_ids Post.text_search('some', :conditions => ['title = :title and id = :id', {:id => 1, :title => TITLES[0]}]), [1]
198
+ check_result_ids Post.text_search('some', :conditions => {:title => TITLES[0]}), [1]
199
+ check_result_ids Post.text_search('some', :conditions => {:title => TITLES[0], :tags => TAGS[0] * ' '}), [1]
200
+
201
+ error = nil
202
+ begin
203
+ check_result_ids Post.text_search('some', :conditions => {:id => 1}), [1]
204
+ rescue => error
205
+ end
206
+ error.should_not be_nil
207
+ error.should be_kind_of Redis::TextSearch::BadConditions
208
+ end
209
+
210
+ it "should handle pagination" do
211
+ res = Post.text_search('some', :page => 1, :per_page => 1, :order => 'id desc')
212
+ check_result_ids res, [3]
213
+ res.total_entries.should == 2
214
+ res.total_pages.should == 2
215
+ res.per_page.should == 1
216
+ res.current_page.should == 1
217
+
218
+ res = Post.text_search('some', :page => 2, :per_page => 1, :order => 'id desc')
219
+ check_result_ids res, [1]
220
+ res.total_entries.should == 2
221
+ res.total_pages.should == 2
222
+ res.per_page.should == 1
223
+ res.current_page.should == 2
224
+
225
+ res = Post.text_search('some', :page => 2, :per_page => 5)
226
+ check_result_ids res, []
227
+ res.total_entries.should == 2
228
+ res.total_pages.should == 1
229
+ res.per_page.should == 5
230
+ res.current_page.should == 2
231
+
232
+ res = Post.text_search('some', :page => 1, :per_page => 1, :conditions => ['id > ?', 2], :order => 'id')
233
+ check_result_ids res, [3]
234
+ res.total_entries.should == 1
235
+ res.total_pages.should == 1
236
+ res.per_page.should == 1
237
+ res.current_page.should == 1
238
+
239
+ res = Post.text_search('some', :page => 1, :per_page => 1, :order => 'id',
240
+ :conditions => ['title = :title', {:title => TITLES[0]}])
241
+ check_result_ids res, [1]
242
+ res.total_entries.should == 1
243
+ res.total_pages.should == 1
244
+ res.per_page.should == 1
245
+ res.current_page.should == 1
246
+ end
247
+
248
+ it "should support a hash to the text_search method" do
249
+ check_result_ids Post.text_search(:tags => 'technical'), [2,3]
250
+ check_result_ids Post.text_search(:tags => 'nontechnical'), [1]
251
+ check_result_ids Post.text_search(:tags => 'technical', :title => 'plain'), [2]
252
+ check_result_ids Post.text_search(:tags => ['technical','MYsql'], :title => 'Mo'), [2]
253
+ check_result_ids Post.text_search(:tags => ['technical','MYsql'], :title => 'some'), []
254
+ check_result_ids Post.text_search(:tags => 'technical', :title => 'comments'), [2,3]
255
+ end
256
+
257
+ # MUST BE LAST!!!!!!
258
+ it "should delete text indexes" do
259
+ @post.delete_text_indexes
260
+ @post2.delete_text_indexes
261
+ Post.delete_text_indexes(3)
262
+ @post.text_indexes.should == []
263
+ @post2.text_indexes.should == []
264
+ @post3.text_indexes.should == []
265
+ end
266
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: redis-textsearch
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nate Wiger
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-12-04 00:00:00 -08:00
12
+ date: 2010-03-05 00:00:00 -08:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
@@ -33,8 +33,9 @@ extra_rdoc_files:
33
33
  files:
34
34
  - lib/redis/text_search/collection.rb
35
35
  - lib/redis/text_search.rb
36
+ - spec/redis_text_search_activerecord_spec.rb
36
37
  - spec/redis_text_search_core_spec.rb
37
- - spec/redis_text_search_sqlite_spec.rb
38
+ - spec/redis_text_search_sequel_spec.rb
38
39
  - spec/spec_helper.rb
39
40
  - README.rdoc
40
41
  has_rdoc: true