mongoid-haystack 1.1.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -9,6 +9,30 @@ DESCRIPTION
9
9
  mongoid-haystack provides a zero-config, POLS, pure mongo, fulltext search
10
10
  solution for your mongoid models.
11
11
 
12
+ INSTALL
13
+ -------
14
+
15
+ rubygems: gem intstall 'mongoid-haystack'
16
+
17
+ Gemfile: gem 'mongoid-haystack'
18
+
19
+ rake db:mongoid:create_indexes # IMPORTANT
20
+
21
+ ````ruby
22
+
23
+ # you might want this in lib/tasks/db.rake ...
24
+ #
25
+
26
+ namespace :db do
27
+ namespace :mongoid do
28
+ task :create_indexes do
29
+ Mongoid::Haystack.create_indexes
30
+ end
31
+ end
32
+ end
33
+
34
+ ````
35
+
12
36
  SYNOPSIS
13
37
  --------
14
38
 
@@ -29,6 +53,32 @@ SYNOPSIS
29
53
 
30
54
  article = results.first.model
31
55
 
56
+ # by default 'search' returns a Mongoid::Criteria object. the result set will
57
+ # be full of objects that refer to a model in your app via a polymorphic
58
+ # relation out. aka
59
+ #
60
+ # Article.search('foobar').first.class #=> Mongoid::Haystack::Index
61
+ # Article.search('foobar').first.model.class #=> Article
62
+ #
63
+ # in an index view you are not going to want to expand the search index
64
+ # objects into full blown models one at the time (N+1) so you can use the
65
+ # 'models' method on the collection to effciently expand the collection into
66
+ # your application models with the fewest possible queries. note that
67
+ # 'models' is a terminal operator. that is to say it returns an array and,
68
+ # afterwards, no more fancy query language is gonna work.
69
+ #
70
+ @results =
71
+ Mongoid::Haystack.search('needle').models
72
+
73
+ # pagination is supported *out of the box*. note that you should chain it
74
+ # *b4* any call to 'models' as 'models' is a terminal operator: it returns
75
+ # an array and *not* a Mongoid::Criteria object
76
+ #
77
+ @models =
78
+ Mongoid::Haystack.search('needle').
79
+ paginate(:page => 3, :size => 42).
80
+ models
81
+
32
82
 
33
83
  # haystack stems the search terms and does score based sorting all using a
34
84
  # fast b-tree
@@ -44,10 +94,11 @@ SYNOPSIS
44
94
  results == [a] #=> true
45
95
 
46
96
 
47
- # cross models searching is supported out of the box, and models can
48
- # customise how they are indexed:
97
+ # cross model searching (site search)is supported out of the box, and models
98
+ # can customise how they are indexed:
49
99
  #
50
100
  # - a global score lets some models appear hight in the global results
101
+ #
51
102
  # - keywords count more than fulltext
52
103
  #
53
104
  class Article
@@ -89,19 +140,8 @@ SYNOPSIS
89
140
  models == [a1, a2] #=> true. because keywords score highter than general fulltext
90
141
 
91
142
 
92
- # by default searching returns Mongoid::Haystack::Index objects. you'll want
93
- # to expand these results to the models they reference in your views, but
94
- # avoid doing an N+1 query. to do this simply call #models on the result set
95
- # and the models will be eager loaded using only as many queries as their are
96
- # model types in your result set
97
- #
98
-
99
- @results = Mongoid::Haystack.search('needle').page(params[:page]).per(10)
100
- @models = @results.models
101
-
102
-
103
143
  # you can decorate your search items with arbirtrary meta data and filter
104
- # searches by it later. this too uses a b-tree index.
144
+ # searches by it later. this too uses a speedy b-tree index.
105
145
  #
106
146
  class Article
107
147
  include Mongoid::Document
@@ -128,7 +168,8 @@ SYNOPSIS
128
168
  :content => 'seen the needles and the damage done...'
129
169
  )
130
170
 
131
- author_articles = Article.search('needle', :facets => {:author_id => author.id})
171
+ articles_for_teh_author =
172
+ Article.search('needle', :facets => {:author_id => author.id})
132
173
 
133
174
 
134
175
  ````
@@ -136,14 +177,16 @@ SYNOPSIS
136
177
  DESCRIPTION
137
178
  -----------
138
179
 
139
- there two main pathways to understand in the code. shit going into the
140
- index, and shit coming out.
180
+ there two main pathways to understand in the code.
181
+
182
+ 1) shit going into the into the index.
183
+ 2) shit coming out of the index.
141
184
 
142
185
  shit going in entails:
143
186
 
144
- - stem and stopword the search terms.
187
+ - stem and stopword the search terms
145
188
  - create or update a new token for each
146
- - create an index item reference all the tokens with precomputed scores
189
+ - create an index item referening all the tokens with precomputed scores
147
190
 
148
191
  for example the terms 'dog dogs cat' might result in these tokens
149
192
 
@@ -166,7 +209,7 @@ for example the terms 'dog dogs cat' might result in these tokens
166
209
 
167
210
  ````
168
211
 
169
- and this index item
212
+ being created|updated and this index item
170
213
 
171
214
 
172
215
  ````javascript
@@ -195,25 +238,34 @@ for example the terms 'dog dogs cat' might result in these tokens
195
238
 
196
239
  being built
197
240
 
198
- in addition, some other information is tracked such and the total number of
199
- search tokens every discovered in the corpus
241
+
242
+ some other information is tracked, but the two normal mongoid models
243
+
244
+ - Mongoid::Haystack::Token
245
+ - Mongoid::Haystack::Index
246
+
247
+ are simple to look at and compromise 80% of the library functionality.
200
248
 
201
249
 
202
250
 
203
251
  a few things to notice:
204
252
 
205
- - the tokens are counted and auto-id'd using hex notation and a sequence
206
- generator. the reason for this is so that their ids are legit hash keys
207
- in the keyword and fulltext score hashes.
253
+ - tokens are counted and auto-id'd using hex notation and a sequence
254
+ generator. the reason for this is so that their ids are legit hash keys in
255
+ the keyword and fulltext score hashes (they are also smaller than 12 byte
256
+ object_ids or the words themselves). aka this sort can be contructed:
257
+
258
+ ````ruby
259
+ order_by('keyword_scores.0x1' => :desc, 'keyword_scores.0x.1' => :desc)
260
+ ````
208
261
 
209
262
  - the data structure above allows both filtering for index items that have
210
- certain tokens, but also ordering them based on global, keyword, and
211
- fulltext score without resorting to map-reduce: a b-tree index can be
212
- used.
263
+ certain tokens, but also ordering them based on global, keyword, and fulltext
264
+ score without resorting to map-reduce: a b-tree index can be used.
213
265
 
214
266
  - all tokens have their text/stem stored exactly once. aka: we do not store
215
- 'hugewords' all over the place but store it once and count occurances of
216
- it to keep the total index much smaller
267
+ 'hugewords' all over the place but store it once and count occurances of it to
268
+ keep the total index much smaller
217
269
 
218
270
 
219
271
 
@@ -27,9 +27,14 @@ module Mongoid
27
27
 
28
28
  class << Index
29
29
  def add(*args)
30
+ # we all one or more models to the index..
31
+ #
30
32
  models_for(*args) do |model|
31
33
  config = nil
32
34
 
35
+ # ask the model how it wants to be indexed. if it does not know,
36
+ # guess.
37
+ #
33
38
  if model.respond_to?(:to_haystack)
34
39
  config = Map.for(model.to_haystack)
35
40
  else
@@ -56,41 +61,64 @@ module Mongoid
56
61
  )
57
62
  end
58
63
 
64
+ # blow up if no sane config was produced
65
+ #
66
+ unless %w( keywords fulltext facets score ).detect{|key| config.has_key?(key)}
67
+ raise ArgumentError, "you need to defined #{ model }#to_haystack"
68
+ end
69
+
70
+ # parse the config
71
+ #
59
72
  keywords = Array(config[:keywords]).join(' ')
60
73
  fulltext = Array(config[:fulltext]).join(' ')
61
74
  facets = Map.for(config[:facets] || {})
62
75
  score = config[:score]
63
76
 
77
+ # find or create an index item for this model
78
+ #
64
79
  index =
65
80
  Haystack.find_or_create(
66
81
  ->{ where(:model => model).first },
67
82
  ->{ new(:model => model) },
68
83
  )
69
84
 
85
+ # if we are updating an index we need to decrement old token counts
86
+ # before updating it
87
+ #
70
88
  if index.persisted?
71
89
  Index.subtract(index)
72
90
  end
73
91
 
92
+ # add tokens for both keywords and fulltext. increment counts for
93
+ # both.
94
+ #
74
95
  keyword_scores = Hash.new{|h,k| h[k] = 0}
75
96
  fulltext_scores = Hash.new{|h,k| h[k] = 0}
76
97
  token_ids = []
77
98
 
78
- Token.values_for(keywords).each do |value|
79
- token = Token.add(value)
99
+ values = Token.values_for(keywords)
100
+ tokens = Token.add(values)
101
+ token_index = tokens.inject({}){|hash, token| hash[token.value] = token; hash}
102
+ values.each do |value|
103
+ token = token_index.fetch(value)
80
104
  id = token.id
81
-
82
105
  token_ids.push(id)
83
106
  keyword_scores[id] += 1
84
107
  end
85
108
 
86
- Token.values_for(fulltext).each do |value|
87
- token = Token.add(value)
109
+ values = Token.values_for(fulltext)
110
+ tokens = Token.add(values)
111
+ token_index = tokens.inject({}){|hash, token| hash[token.value] = token; hash}
112
+ values.each do |value|
113
+ token = token_index.fetch(value)
88
114
  id = token.id
89
-
90
115
  token_ids.push(id)
91
116
  fulltext_scores[id] += 1
92
117
  end
93
118
 
119
+ # our index item is complete with list of tokens, counts of each
120
+ # one, and a facet hash for this model
121
+ #
94
122
  index.keyword_scores = keyword_scores
95
123
  index.fulltext_scores = fulltext_scores
96
124
 
@@ -113,19 +141,23 @@ module Mongoid
113
141
  def subtract(index)
114
142
  tokens = index.tokens
115
143
 
116
- n = 0
144
+ counts = {}
117
145
 
118
146
  tokens.each do |token|
119
147
  keyword_score = index.keyword_scores[token.id].to_i
120
148
  fulltext_score = index.fulltext_scores[token.id].to_i
121
149
 
122
- i = keyword_score + fulltext_score
123
- token.inc(:count, -i)
150
+ count = keyword_score + fulltext_score
124
151
 
125
- n += i
152
+ counts[count] ||= []
153
+ counts[count].push(token.id)
126
154
  end
127
155
 
128
- Count[:tokens].inc(-n)
156
+ counts.each do |count, token_ids|
157
+ Token.where(:id.in => token_ids).inc(:count, -count)
158
+ end
159
+
160
+ tokens
129
161
  end
130
162
 
131
163
  def models_for(*args, &block)
@@ -147,13 +179,13 @@ module Mongoid
147
179
  belongs_to(:model, :polymorphic => true)
148
180
 
149
181
  has_and_belongs_to_many(:tokens, :class_name => '::Mongoid::Haystack::Token', :inverse_of => nil)
182
+
150
183
  field(:score, :type => Integer, :default => 0)
151
184
  field(:keyword_scores, :type => Hash, :default => proc{ Hash.new{|h,k| h[k] = 0} })
152
185
  field(:fulltext_scores, :type => Hash, :default => proc{ Hash.new{|h,k| h[k] = 0} })
153
186
  field(:facets, :type => Hash, :default => {})
154
187
 
155
- index({:model_type => 1})
156
- index({:model_id => 1})
188
+ index({:model_type => 1, :model_id => 1}, :unique => true)
157
189
 
158
190
  index({:token_ids => 1})
159
191
  index({:score => 1})
@@ -1,5 +1,62 @@
1
1
  module Mongoid
2
2
  module Haystack
3
+ module Search
4
+ ClassMethods = proc do
5
+ def search(*args, &block)
6
+ options = Map.options_for!(args)
7
+ options[:types] = Array(options[:types]).flatten.compact
8
+ options[:types].push(self)
9
+ args.push(options)
10
+ results = Haystack.search(*args, &block)
11
+ end
12
+
13
+ def search_index_all!
14
+ all.each do |doc|
15
+ Mongoid::Haystack::Index.remove(doc)
16
+ Mongoid::Haystack::Index.add(doc)
17
+ end
18
+ end
19
+
20
+ after_save do |doc|
21
+ begin
22
+ doc.search_index! if doc.persisted?
23
+ rescue Object
24
+ nil
25
+ end
26
+ end
27
+
28
+ after_destroy do |doc|
29
+ begin
30
+ doc.search_unindex! if doc.destroyed?
31
+ rescue Object
32
+ nil
33
+ end
34
+ end
35
+
36
+ has_one(:haystack_index, :as => :model, :class_name => '::Mongoid::Haystack::Index')
37
+ end
38
+
39
+ InstanceMethods = proc do
40
+ def search_index!
41
+ doc = self
42
+ Mongoid::Haystack::Index.remove(doc)
43
+ Mongoid::Haystack::Index.add(doc)
44
+ end
45
+
46
+ def search_unindex!
47
+ doc = self
48
+ Mongoid::Haystack::Index.remove(doc)
49
+ end
50
+ end
51
+
52
+ def Search.included(other)
53
+ super
54
+ ensure
55
+ other.instance_eval(&ClassMethods)
56
+ other.class_eval(&InstanceMethods)
57
+ end
58
+ end
59
+
3
60
  def search(*args, &block)
4
61
  #
5
62
  options = Map.options_for!(args)
@@ -14,15 +71,15 @@ module Mongoid
14
71
  case
15
72
  when options[:all]
16
73
  op = :token_ids.all
17
- search += Coerce.string(options[:all])
74
+ search += Coerce.string(options[:all])
18
75
 
19
76
  when options[:any]
20
77
  op = :token_ids.in
21
- search += Coerce.string(options[:any])
78
+ search += Coerce.string(options[:any])
22
79
 
23
80
  when options[:in]
24
81
  op = :token_ids.in
25
- search += Coerce.string(options[:in])
82
+ search += Coerce.string(options[:in])
26
83
  end
27
84
 
28
85
  #
@@ -55,114 +112,154 @@ module Mongoid
55
112
  end
56
113
 
57
114
  #
58
- Index.where(conditions).order_by(order).tap do |results|
59
- results.extend(Denormalize)
60
- end
61
- end
115
+ query =
116
+ Index.where(conditions)
117
+ .order_by(order)
118
+ .only(:_id, :model_type, :model_id)
62
119
 
63
- def search_tokens_for(search)
64
- values = Token.values_for(search.to_s)
65
- tokens = Token.where(:value.in => values).to_a
66
-
67
- positions = {}
68
- tokens.each_with_index{|token, index| positions[token] = index + 1}
120
+ query.extend(Pagination)
69
121
 
70
- t = Count[:tokens].value.to_f
122
+ query.extend(Denormalization)
71
123
 
72
- tokens.sort! do |a,b|
73
- [b.rarity_bin(t), positions[b]] <=> [a.rarity_bin(t), positions[a]]
74
- end
75
-
76
- tokens
124
+ query
77
125
  end
78
126
 
79
- module Search
80
- ClassMethods = proc do
81
- def search(*args, &block)
82
- options = Map.options_for!(args)
83
- options[:types] = Array(options[:types]).flatten.compact
84
- options[:types].push(self)
85
- args.push(options)
86
- results = Haystack.search(*args, &block)
87
- end
127
+ module Pagination
128
+ def paginate(*args, &block)
129
+ list = self
130
+ options = Map.options_for!(args)
88
131
 
89
- after_save do |doc|
90
- begin
91
- Mongoid::Haystack::Index.add(doc) if doc.persisted?
92
- rescue Object
93
- nil
132
+ page = Integer(args.shift || options[:page] || 1)
133
+ size = Integer(args.shift || options[:size] || 42)
134
+
135
+ count =
136
+ if list.is_a?(Array)
137
+ list.size
138
+ else
139
+ list.count
94
140
  end
95
- end
96
141
 
97
- after_destroy do |doc|
98
- begin
99
- Mongoid::Haystack::Index.remove(doc)
100
- rescue Object
101
- nil
142
+ limit = size
143
+ skip = (page - 1 ) * size
144
+
145
+ result =
146
+ if list.is_a?(Array)
147
+ list.slice(skip, limit)
148
+ else
149
+ list.skip(skip).limit(limit)
102
150
  end
103
- end
104
151
 
105
- has_one(:haystack_index, :as => :model, :class_name => '::Mongoid::Haystack::Index')
152
+ result._paginated.update(
153
+ :total_pages => (count / size.to_f).ceil,
154
+ :num_pages => (count / size.to_f).ceil,
155
+ :current_page => page
156
+ )
157
+
158
+ result
106
159
  end
107
160
 
108
- InstanceMethods = proc do
161
+ def _paginated
162
+ @_paginated ||= Map.new
109
163
  end
110
164
 
111
- def Search.included(other)
112
- super
113
- ensure
114
- other.instance_eval(&ClassMethods)
115
- other.class_eval(&InstanceMethods)
165
+ def method_missing(method, *args, &block)
166
+ if respond_to?(:_paginated) and _paginated.has_key?(method) and args.empty? and block.nil?
167
+ _paginated[method]
168
+ else
169
+ super
170
+ end
116
171
  end
117
172
  end
118
173
 
119
- module Denormalize
120
- def denormalize
121
- ::Mongoid::Haystack.denormalize(self)
122
- self
174
+ module Denormalization
175
+ def models
176
+ Results.for(query = self)
123
177
  end
124
178
 
125
- def models
126
- denormalize
127
- map(&:model)
179
+ def _denormalized
180
+ @_denormalized ||= (is_a?(Mongoid::Criteria) ? ::Mongoid::Haystack.denormalize(self) : self)
128
181
  end
182
+
183
+ class Results < ::Array
184
+ include ::Mongoid::Haystack::Pagination
185
+
186
+ attr_accessor :query
187
+
188
+ def Results.for(query)
189
+ Results.new.tap do |results|
190
+ results.query = query
191
+ results.replace(query._denormalized)
192
+ results._paginated.replace(query._paginated) rescue nil
193
+ end
194
+ end
195
+
196
+ def models
197
+ self
198
+ end
199
+ end
200
+ end
201
+
202
+ def search_tokens_for(search)
203
+ values = Token.values_for(search.to_s)
204
+ tokens = Token.where(:value.in => values).to_a
205
+
206
+ positions = {}
207
+ tokens.each_with_index{|token, index| positions[token] = index + 1}
208
+
209
+ total = Token.total.to_f
210
+
211
+ tokens.sort! do |a,b|
212
+ [b.rarity_bin(total), positions[b]] <=> [a.rarity_bin(total), positions[a]]
213
+ end
214
+
215
+ tokens
129
216
  end
130
217
 
131
218
  def Haystack.denormalize(results)
132
219
  queries = Hash.new{|h,k| h[k] = []}
133
220
 
134
- results = results.to_a.flatten.compact
135
-
136
221
  results.each do |result|
137
222
  model_type = result[:model_type]
138
223
  model_id = result[:model_id]
139
- model_class = model_type.constantize
224
+ model_class = eval(model_type) rescue next
140
225
  queries[model_class].push(model_id)
141
226
  end
142
227
 
228
+ =begin
143
229
  index = Hash.new{|h,k| h[k] = {}}
144
-
145
- queries.each do |model_class, model_ids|
146
- models =
147
- begin
148
- model_class.find(model_ids)
149
- rescue Mongoid::Errors::DocumentNotFound
150
- model_ids.map do |model_id|
151
- begin
152
- model_class.find(model_id)
153
- rescue Mongoid::Errors::DocumentNotFound
154
- nil
230
+ =end
231
+
232
+ models =
233
+ queries.map do |model_class, model_ids|
234
+ model_class_models =
235
+ begin
236
+ model_class.find(model_ids)
237
+ rescue Mongoid::Errors::DocumentNotFound
238
+ model_ids.map do |model_id|
239
+ begin
240
+ model_class.find(model_id)
241
+ rescue Mongoid::Errors::DocumentNotFound
242
+ nil
243
+ end
155
244
  end
156
245
  end
246
+
247
+ =begin
248
+ model_class_models.each do |model|
249
+ index[model.class.name] ||= Hash.new
250
+ next unless model
251
+ index[model.class.name][model.id.to_s] = model
157
252
  end
253
+ =end
158
254
 
159
- models.each do |model|
160
- index[model.class.name] ||= Hash.new
161
- next unless model
162
- index[model.class.name][model.id.to_s] = model
255
+ model_class_models
163
256
  end
164
- end
165
257
 
258
+ models.flatten!
259
+ models.compact!
260
+ models
261
+
262
+ =begin
166
263
  to_ignore = []
167
264
 
168
265
  results.each_with_index do |result, i|
@@ -175,13 +272,24 @@ module Mongoid
175
272
  result.model = model
176
273
  end
177
274
 
178
- result.model.freeze
179
- result.freeze
275
+ result.model
276
+ result
277
+ end
278
+
279
+ to_ignore.reverse.each do |index|
280
+ models.delete_at(index)
180
281
  end
282
+ =end
181
283
 
182
- to_ignore.reverse.each{|i| results.delete_at(i)}
284
+ models
285
+ end
286
+
287
+ def Haystack.expand(*args, &block)
288
+ Haystack.denormalize(*args, &block)
289
+ end
183
290
 
184
- results.to_a
291
+ def Haystack.models_for(*args, &block)
292
+ Haystack.denormalize(*args, &block)
185
293
  end
186
294
  end
187
295
  end
@@ -9,17 +9,54 @@ module Mongoid
9
9
  end
10
10
 
11
11
  def add(value)
12
- token =
13
- Haystack.find_or_create(
14
- ->{ where(:value => value).first },
15
- ->{ create!(:value => value) }
16
- )
12
+ # handle a value or array of values - which may contain dups
13
+ #
14
+ values = Array(value)
15
+ values.flatten!
16
+ values.compact!
17
17
 
18
- token.inc(:count, 1)
18
+ # ensure that a token exists for each value seen
19
+ #
20
+ existing = where(:value.in => values)
21
+ missing = values - existing.map(&:value)
19
22
 
20
- Count[:tokens].inc(1)
23
+ docs = missing.map{|value| {:_id => Token.next_hex_id, :value => value}}
24
+ unless docs.empty?
25
+ collection = mongo_session.with(:safe => false)[collection_name]
26
+ collection.insert(docs, [:continue_on_error])
27
+ end
21
28
 
22
- token
29
+ # new we should have one token per uniq value
30
+ #
31
+ tokens = where(:value.in => values)
32
+
33
+ # batch update the counts on the tokens by the number of times each
34
+ # value was seen in the list
35
+ #
36
+ # 'dog dog' #=> increment the 'dog' token's count by 2
37
+ #
38
+ counts = {}
39
+ token_index = tokens.inject({}){|hash, token| hash[token.value] = token; hash}
40
+ value_index = values.inject({}){|hash, value| hash[value] ||= []; hash[value].push(value); hash}
41
+
42
+ values.each do |value|
43
+ token = token_index[value]
44
+ count = value_index[value].size
45
+ counts[count] ||= []
46
+ counts[count].push(token.id)
47
+ end
48
+
49
+ counts.each do |count, token_ids|
50
+ Token.where(:id.in => token_ids).inc(:count, count)
51
+ end
52
+
53
+ # return an array or single token depending on whether a list or
54
+ # single value was added
55
+ #
56
+ value.is_a?(Array) ? tokens : tokens.first
57
+ end
58
+
59
+ def subtract(tokens)
23
60
  end
24
61
 
25
62
  def sequence
@@ -29,6 +66,10 @@ module Mongoid
29
66
  def next_hex_id
30
67
  "0x#{ hex = sequence.next.to_s(16) }"
31
68
  end
69
+
70
+ def total
71
+ sum(:count)
72
+ end
32
73
  end
33
74
 
34
75
  field(:_id, :type => String, :default => proc{ Token.next_hex_id })
@@ -38,19 +79,19 @@ module Mongoid
38
79
  index({:value => 1}, {:unique => true})
39
80
  index({:count => 1})
40
81
 
41
- def frequency(n_tokens = Count[:tokens].value.to_f)
82
+ def frequency(n_tokens = Token.total.value.to_f)
42
83
  (count / n_tokens).round(2)
43
84
  end
44
85
 
45
- def frequency_bin(n_tokens = Count[:tokens].value.to_f)
86
+ def frequency_bin(n_tokens = Token.total.value.to_f)
46
87
  (frequency(n_tokens) * 10).truncate
47
88
  end
48
89
 
49
- def rarity(n_tokens = Count[:tokens].value.to_f)
90
+ def rarity(n_tokens = Token.total.value.to_f)
50
91
  ((n_tokens - count) / n_tokens).round(2)
51
92
  end
52
93
 
53
- def rarity_bin(n_tokens = Count[:tokens].value.to_f)
94
+ def rarity_bin(n_tokens = Token.total.value.to_f)
54
95
  (rarity(n_tokens) * 10).truncate
55
96
  end
56
97
  end
@@ -5,7 +5,6 @@ module Mongoid
5
5
  [
6
6
  Mongoid::Haystack::Token,
7
7
  Mongoid::Haystack::Index,
8
- Mongoid::Haystack::Count,
9
8
  Mongoid::Haystack::Sequence
10
9
  ]
11
10
  end
@@ -29,6 +28,10 @@ module Mongoid
29
28
  end
30
29
  end
31
30
 
31
+ def create_indexes
32
+ models.each{|model| model.create_indexes}
33
+ end
34
+
32
35
  def destroy_all
33
36
  models.map{|model| model.destroy_all}
34
37
  end
@@ -2,7 +2,7 @@
2
2
  #
3
3
  module Mongoid
4
4
  module Haystack
5
- const_set :Version, '1.1.0'
5
+ const_set :Version, '1.2.0'
6
6
 
7
7
  class << Haystack
8
8
  def version
@@ -11,11 +11,13 @@
11
11
 
12
12
  def dependencies
13
13
  {
14
- 'mongoid' => [ 'mongoid' , '~> 3.0' ] ,
15
- 'map' => [ 'map' , '~> 6.2' ] ,
16
- 'fattr' => [ 'fattr' , '~> 2.2' ] ,
17
- 'coerce' => [ 'coerce' , '~> 0.0.3' ] ,
18
- 'unicode_utils' => [ 'unicode_utils' , '~> 1.4.0' ] ,
14
+ 'mongoid' => [ 'mongoid' , '~> 3.0.14' ] ,
15
+ 'moped' => [ 'moped' , '~> 1.3.1' ] ,
16
+ 'origin' => [ 'origin' , '~> 1.0.11' ] ,
17
+ 'map' => [ 'map' , '~> 6.2' ] ,
18
+ 'fattr' => [ 'fattr' , '~> 2.2' ] ,
19
+ 'coerce' => [ 'coerce' , '~> 0.0.3' ] ,
20
+ 'unicode_utils' => [ 'unicode_utils' , '~> 1.4.0' ] ,
19
21
  }
20
22
  end
21
23
 
@@ -73,7 +75,6 @@
73
75
 
74
76
  load Haystack.libdir('stemming.rb')
75
77
  load Haystack.libdir('util.rb')
76
- load Haystack.libdir('count.rb')
77
78
  load Haystack.libdir('sequence.rb')
78
79
  load Haystack.libdir('token.rb')
79
80
  load Haystack.libdir('index.rb')
@@ -86,3 +87,11 @@
86
87
  extend Haystack
87
88
  end
88
89
  end
90
+
91
+ ##
92
+ #
93
+ if defined?(Rails)
94
+ class Mongoid::Haystack::Engine < Rails::Engine
95
+ paths['app/models'] = File.dirname(__FILE__)
96
+ end
97
+ end
@@ -3,7 +3,7 @@
3
3
 
4
4
  Gem::Specification::new do |spec|
5
5
  spec.name = "mongoid-haystack"
6
- spec.version = "1.1.0"
6
+ spec.version = "1.2.0"
7
7
  spec.platform = Gem::Platform::RUBY
8
8
  spec.summary = "mongoid-haystack"
9
9
  spec.description = "a mongoid 3 zero-config, zero-integration, POLS pure mongo fulltext solution"
@@ -16,13 +16,11 @@ Gem::Specification::new do |spec|
16
16
  "lib/app/models",
17
17
  "lib/app/models/mongoid",
18
18
  "lib/app/models/mongoid/haystack",
19
- "lib/app/models/mongoid/haystack/count.rb",
20
19
  "lib/app/models/mongoid/haystack/index.rb",
21
20
  "lib/app/models/mongoid/haystack/sequence.rb",
22
21
  "lib/app/models/mongoid/haystack/token.rb",
23
22
  "lib/mongoid-haystack",
24
23
  "lib/mongoid-haystack.rb",
25
- "lib/mongoid-haystack/count.rb",
26
24
  "lib/mongoid-haystack/index.rb",
27
25
  "lib/mongoid-haystack/search.rb",
28
26
  "lib/mongoid-haystack/sequence.rb",
@@ -58,7 +56,11 @@ Gem::Specification::new do |spec|
58
56
  spec.test_files = nil
59
57
 
60
58
 
61
- spec.add_dependency(*["mongoid", "~> 3.0"])
59
+ spec.add_dependency(*["mongoid", "~> 3.0.14"])
60
+
61
+ spec.add_dependency(*["moped", "~> 1.3.1"])
62
+
63
+ spec.add_dependency(*["origin", "~> 1.0.11"])
62
64
 
63
65
  spec.add_dependency(*["map", "~> 6.2"])
64
66
 
@@ -14,6 +14,17 @@ Testing Mongoid::Haystack do
14
14
  assert{ Mongoid::Haystack.search('cat').map(&:model) == [b] }
15
15
  end
16
16
 
17
+ ##
18
+ #
19
+ testing 'that results are returned as chainable Mongoid::Criteria' do
20
+ k = new_klass
21
+
22
+ 3.times{ k.create! :content => 'cats' }
23
+
24
+ results = assert{ Mongoid::Haystack.search('cat') }
25
+ assert{ results.is_a?(Mongoid::Criteria) }
26
+ end
27
+
17
28
  ##
18
29
  #
19
30
  testing 'that word occurance affects the sort' do
@@ -67,7 +78,7 @@ Testing Mongoid::Haystack do
67
78
 
68
79
  assert{ Mongoid::Haystack::Token.count == 2 }
69
80
  assert{ Mongoid::Haystack::Token.all.map(&:value).sort == %w( cat dog ) }
70
- assert{ Mongoid::Haystack::Count[:tokens].value == 3 }
81
+ assert{ Mongoid::Haystack::Token.total == 3 }
71
82
  end
72
83
 
73
84
  testing 'that removing a model from the index decrements counts appropriately' do
@@ -81,27 +92,27 @@ Testing Mongoid::Haystack do
81
92
 
82
93
  assert{ Mongoid::Haystack::Token.where(:value => 'cat').first.count == 2 }
83
94
  assert{ Mongoid::Haystack::Token.where(:value => 'dog').first.count == 2 }
84
- assert{ Mongoid::Haystack::Count[:tokens].value == 4 }
95
+ assert{ Mongoid::Haystack::Token.total == 4 }
85
96
  assert{ Mongoid::Haystack::Token.all.map(&:value).sort == %w( cat dog ) }
86
97
  assert{ Mongoid::Haystack.unindex(c) }
87
98
  assert{ Mongoid::Haystack::Token.all.map(&:value).sort == %w( cat dog ) }
88
- assert{ Mongoid::Haystack::Count[:tokens].value == 2 }
99
+ assert{ Mongoid::Haystack::Token.total == 2 }
89
100
  assert{ Mongoid::Haystack::Token.where(:value => 'cat').first.count == 1 }
90
101
  assert{ Mongoid::Haystack::Token.where(:value => 'dog').first.count == 1 }
91
102
 
92
- assert{ Mongoid::Haystack::Count[:tokens].value == 2 }
103
+ assert{ Mongoid::Haystack::Token.total == 2 }
93
104
  assert{ Mongoid::Haystack::Token.all.map(&:value).sort == %w( cat dog ) }
94
105
  assert{ Mongoid::Haystack.unindex(b) }
95
106
  assert{ Mongoid::Haystack::Token.all.map(&:value).sort == %w( cat dog ) }
96
- assert{ Mongoid::Haystack::Count[:tokens].value == 1 }
107
+ assert{ Mongoid::Haystack::Token.total == 1 }
97
108
  assert{ Mongoid::Haystack::Token.where(:value => 'cat').first.count == 0 }
98
109
  assert{ Mongoid::Haystack::Token.where(:value => 'dog').first.count == 1 }
99
110
 
100
- assert{ Mongoid::Haystack::Count[:tokens].value == 1 }
111
+ assert{ Mongoid::Haystack::Token.total == 1 }
101
112
  assert{ Mongoid::Haystack::Token.all.map(&:value).sort == %w( cat dog ) }
102
113
  assert{ Mongoid::Haystack.unindex(a) }
103
114
  assert{ Mongoid::Haystack::Token.all.map(&:value).sort == %w( cat dog ) }
104
- assert{ Mongoid::Haystack::Count[:tokens].value == 0 }
115
+ assert{ Mongoid::Haystack::Token.total == 0 }
105
116
  assert{ Mongoid::Haystack::Token.where(:value => 'cat').first.count == 0 }
106
117
  assert{ Mongoid::Haystack::Token.where(:value => 'dog').first.count == 0 }
107
118
  end
@@ -235,20 +246,119 @@ Testing Mongoid::Haystack do
235
246
  assert{ Mongoid::Haystack.search('dog').first.model == b }
236
247
  end
237
248
 
249
+ ##
250
+ #
251
+ testing 'that re-indexing a class is idempotent' do
252
+ k = new_klass do
253
+ field(:title)
254
+ field(:body)
255
+
256
+ def to_haystack
257
+ { :keywords => title, :fulltext => body }
258
+ end
259
+ end
260
+
261
+ n = 10
262
+
263
+ n.times do
264
+ k.create!(:title => 'the cats and dogs', :body => 'now now is is the the time time for for all all good good men women')
265
+ end
266
+
267
+ n.times do
268
+ k.create!(:title => 'a b c abc xyz abc xyz b', :body => 'pdq pdq pdq xyz teh ngr am')
269
+ end
270
+
271
+ assert{ Mongoid::Haystack.search('cat').count == n }
272
+ assert{ Mongoid::Haystack.search('pdq').count == n }
273
+
274
+ ca = Mongoid::Haystack::Token.all.inject({}){|hash, token| hash.update token.id => token.value}
275
+
276
+ assert{ k.search_index_all! }
277
+
278
+ cb = Mongoid::Haystack::Token.all.inject({}){|hash, token| hash.update token.id => token.value}
279
+
280
+ assert{ ca.size == Mongoid::Haystack::Token.count }
281
+ assert{ cb.size == Mongoid::Haystack::Token.count }
282
+ assert{ ca == cb }
283
+ end
284
+
285
+ ##
286
+ #
287
+ testing 'that not just any model can be indexed' do
288
+ o = new_klass.create!
289
+ assert{ begin; Mongoid::Haystack::Index.add(o); rescue Object => e; e.is_a?(ArgumentError); end }
290
+ end
291
+
292
+ ##
293
+ #
294
+ testing 'that results can be expanded efficiently if need be' do
295
+ k = new_klass
296
+ 3.times{ k.create! :content => 'cats' }
297
+
298
+ results = assert{ Mongoid::Haystack.search('cat') }
299
+ assert{ Mongoid::Haystack.models_for(results).map{|model| model.class} == [k, k, k] }
300
+ end
301
+
302
+ ##
303
+ #
304
+ testing 'basic pagination' do
305
+ k = new_klass
306
+ 11.times{|i| k.create! :content => "cats #{ i }" }
307
+
308
+ assert{ k.search('cat').paginate(:page => 1, :size => 2).to_a.size == 2 }
309
+ assert{ k.search('cat').paginate(:page => 2, :size => 5).to_a.size == 5 }
310
+
311
+ accum = []
312
+
313
+ n = 6
314
+ size = 2
315
+ (1..n).each do |page|
316
+ list = assert{ k.search('cat').paginate(:page => page, :size => size) }
317
+ accum.push(*list)
318
+ assert{ list.num_pages == n }
319
+ assert{ list.total_pages == n }
320
+ assert{ list.current_page == page }
321
+ end
322
+
323
+ a = accum.map{|i| i.model}.sort_by{|m| m.content}
324
+ b = k.all.sort_by{|m| m.content}
325
+
326
+ assert{ a == b }
327
+ end
328
+
329
+ ##
330
+ #
331
+ testing 'that pagination preserves the #model terminator' do
332
+ k = new_klass
333
+ 11.times{|i| k.create! :content => "cats #{ i }" }
334
+
335
+ list = assert{ k.search('cat').paginate(:page => 1, :size => 2) }
336
+ assert{ list.is_a?(Mongoid::Criteria) }
337
+
338
+ models = assert{ list.models }
339
+ assert{ models.is_a?(Array) }
340
+ end
341
+
238
342
  protected
239
343
 
240
344
  def new_klass(&block)
241
- Object.send(:remove_const, :K) if Object.send(:const_defined?, :K)
345
+ if Object.send(:const_defined?, :K)
346
+ Object.const_get(:K).destroy_all
347
+ Object.send(:remove_const, :K)
348
+ end
242
349
 
243
350
  k = Class.new(A) do
244
351
  self.default_collection_name = :ks
245
352
  def self.name() 'K' end
246
- include ::Mongoid::Haystack::Search
247
- class_eval(&block) if block
248
353
  end
249
354
 
250
355
  Object.const_set(:K, k)
251
356
 
357
+ k.class_eval do
358
+ include ::Mongoid::Haystack::Search
359
+ class_eval(&block) if block
360
+ end
361
+
252
362
  k
253
363
  end
254
364
 
@@ -260,4 +370,6 @@ protected
260
370
  [A, B, C].map{|m| m.destroy_all}
261
371
  Mongoid::Haystack.destroy_all
262
372
  end
373
+
374
+ at_exit{ K.destroy_all if defined?(K) }
263
375
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mongoid-haystack
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.2.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-12-06 00:00:00.000000000 Z
12
+ date: 2012-12-11 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: mongoid
@@ -18,7 +18,7 @@ dependencies:
18
18
  requirements:
19
19
  - - ~>
20
20
  - !ruby/object:Gem::Version
21
- version: '3.0'
21
+ version: 3.0.14
22
22
  type: :runtime
23
23
  prerelease: false
24
24
  version_requirements: !ruby/object:Gem::Requirement
@@ -26,7 +26,39 @@ dependencies:
26
26
  requirements:
27
27
  - - ~>
28
28
  - !ruby/object:Gem::Version
29
- version: '3.0'
29
+ version: 3.0.14
30
+ - !ruby/object:Gem::Dependency
31
+ name: moped
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ~>
36
+ - !ruby/object:Gem::Version
37
+ version: 1.3.1
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ version: 1.3.1
46
+ - !ruby/object:Gem::Dependency
47
+ name: origin
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ~>
52
+ - !ruby/object:Gem::Version
53
+ version: 1.0.11
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: 1.0.11
30
62
  - !ruby/object:Gem::Dependency
31
63
  name: map
32
64
  requirement: !ruby/object:Gem::Requirement
@@ -99,12 +131,10 @@ extra_rdoc_files: []
99
131
  files:
100
132
  - README.md
101
133
  - Rakefile
102
- - lib/app/models/mongoid/haystack/count.rb
103
134
  - lib/app/models/mongoid/haystack/index.rb
104
135
  - lib/app/models/mongoid/haystack/sequence.rb
105
136
  - lib/app/models/mongoid/haystack/token.rb
106
137
  - lib/mongoid-haystack.rb
107
- - lib/mongoid-haystack/count.rb
108
138
  - lib/mongoid-haystack/index.rb
109
139
  - lib/mongoid-haystack/search.rb
110
140
  - lib/mongoid-haystack/sequence.rb
@@ -1 +0,0 @@
1
- Mongoid::Haystack::Count
@@ -1,28 +0,0 @@
1
- module Mongoid
2
- module Haystack
3
- class Count
4
- include Mongoid::Document
5
-
6
- field(:name, :type => String)
7
- field(:value, :type => Integer, :default => 0)
8
-
9
- index({:name => 1}, {:unique => true})
10
- index({:value => 1})
11
-
12
- def Count.for(name)
13
- Haystack.find_or_create(
14
- ->{ where(:name => name.to_s).first },
15
- ->{ create!(:name => name.to_s) }
16
- )
17
- end
18
-
19
- def Count.[](name)
20
- Count.for(name)
21
- end
22
-
23
- def inc(n = 1)
24
- super(:value, n)
25
- end
26
- end
27
- end
28
- end