redis-textsearch 0.1.2 → 0.1.3

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.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