mongoid-haystack 1.1.0 → 1.2.0

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