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 +82 -30
- data/lib/mongoid-haystack/index.rb +45 -13
- data/lib/mongoid-haystack/search.rb +185 -77
- data/lib/mongoid-haystack/token.rb +53 -12
- data/lib/mongoid-haystack/util.rb +4 -1
- data/lib/mongoid-haystack.rb +16 -7
- data/mongoid-haystack.gemspec +6 -4
- data/test/mongoid-haystack_test.rb +122 -10
- metadata +36 -6
- data/lib/app/models/mongoid/haystack/count.rb +0 -1
- data/lib/mongoid-haystack/count.rb +0 -28
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
|
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
|
-
|
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.
|
140
|
-
|
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
|
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
|
-
|
199
|
-
|
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
|
-
-
|
206
|
-
|
207
|
-
|
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
|
-
|
211
|
-
|
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
|
-
|
216
|
-
|
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)
|
79
|
-
|
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)
|
87
|
-
|
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
|
-
|
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
|
-
|
123
|
-
token.inc(:count, -i)
|
150
|
+
count = keyword_score + fulltext_score
|
124
151
|
|
125
|
-
|
152
|
+
counts[count] ||= []
|
153
|
+
counts[count].push(token.id)
|
126
154
|
end
|
127
155
|
|
128
|
-
|
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
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
115
|
+
query =
|
116
|
+
Index.where(conditions)
|
117
|
+
.order_by(order)
|
118
|
+
.only(:_id, :model_type, :model_id)
|
62
119
|
|
63
|
-
|
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
|
-
|
122
|
+
query.extend(Denormalization)
|
71
123
|
|
72
|
-
|
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
|
80
|
-
|
81
|
-
|
82
|
-
|
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
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
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
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
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
|
-
|
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
|
-
|
161
|
+
def _paginated
|
162
|
+
@_paginated ||= Map.new
|
109
163
|
end
|
110
164
|
|
111
|
-
def
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
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
|
120
|
-
def
|
121
|
-
|
122
|
-
self
|
174
|
+
module Denormalization
|
175
|
+
def models
|
176
|
+
Results.for(query = self)
|
123
177
|
end
|
124
178
|
|
125
|
-
def
|
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
|
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
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
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
|
-
|
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
|
179
|
-
result
|
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
|
-
|
284
|
+
models
|
285
|
+
end
|
286
|
+
|
287
|
+
def Haystack.expand(*args, &block)
|
288
|
+
Haystack.denormalize(*args, &block)
|
289
|
+
end
|
183
290
|
|
184
|
-
|
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
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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 =
|
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 =
|
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 =
|
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 =
|
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
|
data/lib/mongoid-haystack.rb
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
#
|
3
3
|
module Mongoid
|
4
4
|
module Haystack
|
5
|
-
const_set :Version, '1.
|
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
|
-
'
|
16
|
-
'
|
17
|
-
'
|
18
|
-
'
|
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
|
data/mongoid-haystack.gemspec
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
|
4
4
|
Gem::Specification::new do |spec|
|
5
5
|
spec.name = "mongoid-haystack"
|
6
|
-
spec.version = "1.
|
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::
|
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::
|
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::
|
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::
|
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::
|
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::
|
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::
|
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
|
-
|
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.
|
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-
|
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:
|
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:
|
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
|