search_magic 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,34 +1,31 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- search_magic (0.2.0)
4
+ search_magic (0.3.0)
5
5
  chronic
6
- mongoid (>= 2.0.0)
6
+ mongoid (>= 2.4.3)
7
7
 
8
8
  GEM
9
9
  remote: http://rubygems.org/
10
10
  specs:
11
- activemodel (3.1.3)
12
- activesupport (= 3.1.3)
11
+ activemodel (3.2.1)
12
+ activesupport (= 3.2.1)
13
13
  builder (~> 3.0.0)
14
+ activesupport (3.2.1)
14
15
  i18n (~> 0.6)
15
- activesupport (3.1.3)
16
16
  multi_json (~> 1.0)
17
- archive-tar-minitar (0.5.2)
18
- bson (1.5.1)
19
- bson_ext (1.5.1)
17
+ bson (1.5.2)
18
+ bson_ext (1.5.2)
19
+ bson (= 1.5.2)
20
20
  builder (3.0.0)
21
21
  chronic (0.6.6)
22
- columnize (0.3.5)
23
22
  database_cleaner (0.7.0)
24
23
  diff-lcs (1.1.3)
25
24
  fabrication (1.2.0)
26
25
  i18n (0.6.0)
27
- linecache19 (0.5.12)
28
- ruby_core_source (>= 0.1.4)
29
- mongo (1.5.1)
30
- bson (= 1.5.1)
31
- mongoid (2.3.4)
26
+ mongo (1.5.2)
27
+ bson (= 1.5.2)
28
+ mongoid (2.4.3)
32
29
  activemodel (~> 3.1)
33
30
  mongo (~> 1.3)
34
31
  tzinfo (~> 0.3.22)
@@ -41,16 +38,6 @@ GEM
41
38
  rspec-expectations (2.7.0)
42
39
  diff-lcs (~> 1.1.2)
43
40
  rspec-mocks (2.7.0)
44
- ruby-debug-base19 (0.11.25)
45
- columnize (>= 0.3.1)
46
- linecache19 (>= 0.5.11)
47
- ruby_core_source (>= 0.1.4)
48
- ruby-debug19 (0.11.6)
49
- columnize (>= 0.3.1)
50
- linecache19 (>= 0.5.11)
51
- ruby-debug-base19 (>= 0.11.19)
52
- ruby_core_source (0.1.5)
53
- archive-tar-minitar (>= 0.5.2)
54
41
  tzinfo (0.3.31)
55
42
 
56
43
  PLATFORMS
@@ -61,5 +48,4 @@ DEPENDENCIES
61
48
  database_cleaner
62
49
  fabrication
63
50
  rspec
64
- ruby-debug19
65
51
  search_magic!
@@ -4,7 +4,7 @@ SearchMagic provides full-text search capabilities to [mongoid](http://github.co
4
4
 
5
5
  ## Installation
6
6
 
7
- SearchMagic is built on top of mongoid; in all likelihood, it will only work with versions greater than or equal to *2.0.0*. The project can be installed as a gem on a target system:
7
+ SearchMagic is built on top of mongoid; in all likelihood, it will only work with versions greater than or equal to *2.0.0*. However, see Upgrading for information related to bugs which might be fixed due to newer versions of mongoid. The project can be installed as a gem on a target system:
8
8
 
9
9
  ```
10
10
  gem install search_magic
@@ -138,6 +138,89 @@ Mongoid already comes with a mechanism for [ordering documents](http://mongoid.o
138
138
 
139
139
  We'll revisit this topic in more detail after looking at how associations work.
140
140
 
141
+ ### Instance values matching a search pattern
142
+
143
+ _(As of version 0.3.0.)_
144
+
145
+ Searchable documents gain an instance method, **values_matching**, which takes one parameter, a search **pattern**, returning the entries in **searchable_values** which match **pattern**. This method behaves similarly to **search_for**; notably, it will take any input accepted by **search_for**.
146
+
147
+ ```ruby
148
+ pattern = "los angeles state:ca"
149
+ address = Address.search_for(pattern).first
150
+ address.values_matching(pattern) # ["city:los", "city:angeles", "state:ca"]
151
+ ```
152
+
153
+ As can be inferred from the previous example, this can be useful for discerning which selectors of a given document are matching specific text. It can also be useful for determining which data is present in a given document, when altering the search mode.
154
+
155
+ ### Altering the search mode
156
+
157
+ _(As of version 0.3.0.)_
158
+
159
+ The search mode establishes how each successive query fragment winnows the result set of the search. Two different search modes are supported by SearchMagic:
160
+
161
+ 1. **all**: in this mode, a matching document must have data that satisfies each query fragment of the search pattern;
162
+ 2. **any**: in this mode, a matching document must have data that satisfies at least one query fragment of the search pattern.
163
+
164
+ The mode is alterable on a per-query basis through the **mode** selector. Should **mode** be absent from a query, the query defaults to the **all** behavior. Despite the examples, please note that **mode** need not be specified first within a pattern; its presence anywhere within the pattern is sufficient. However, also note that successive instances within a query will overwrite previous instances.
165
+
166
+ The following two searches are equivalent: they both return all addresses which represent a location within Los Angeles, CA.
167
+
168
+ ```ruby
169
+ Address.search_for("mode:all city:'los angeles' state:ca")
170
+ Address.search_for("city:'los angeles' state:ca")
171
+ ```
172
+
173
+ However, the following search is more expansive: it matches all addresses which represent a location either within Los Angeles, or within the state of California.
174
+
175
+ ```ruby
176
+ Address.search_for("mode:any city:'los angeles' state:ca")
177
+ ```
178
+
179
+ ### Searching within an embedded hash
180
+
181
+ _(As of version 0.3.0.)_
182
+
183
+ Embedded hashes are handled slightly differently from normal searchable fields: like normal fields, the hash selector follows normal naming rules, but keys are specified with an extra **":"** separator.
184
+
185
+ ```ruby
186
+ class Video
187
+ include Mongoid::Document
188
+ include SearchMagic
189
+ field :data, type: Hash, default: {}
190
+ search_on :data
191
+ end
192
+ ```
193
+
194
+ Given the preceding class definition, which defines a searchable hash field called **data**, a search may be performed on arbitrary keys within **data** in the following manner:
195
+
196
+ ```ruby
197
+ Video.search_for("data:resolution:1080p")
198
+ Video.search_for("data:director:'Tim Burton'")
199
+ Video.search_for("data:duration:#{60.minutes}")
200
+ ```
201
+
202
+ Which is to say that searching a hash takes the form of specifying the hash field selector, followed by a colon separator and the specific key, followed by a colon separator and the specific value for the key. Values are handled according to other rules specified within this document. In this format, the hash field selector and the specified key act as the full selector for the query fragment.
203
+
204
+ ### "I sense something; a presence I've not felt since..."
205
+
206
+ _(As of version 0.3.0.)_
207
+
208
+ Most of the useful functionality provided by SearchMagic is to be found in searching the document search graph for specific values. However, it can also be useful to simply determine whether a given selector has any value, rather than a particular one. This can be done through a simple presence detection test, which matches a given document if the document has a value set on the (searchable) field.
209
+
210
+ For example, to search for all addresses which have a **postal_code** set, the following query might be used:
211
+
212
+ ```ruby
213
+ Address.search_for("postal_code?") # i.e. all addresses for which address.postal_code.present? would return true.
214
+ ```
215
+
216
+ Which is to say that a presence detection query fragment is formed by specifying a selector followed by a question mark (**?**). This can be especially useful for determining if an embedded hash has a value present for a given key:
217
+
218
+ ```ruby
219
+ Video.search_for("data:duration?") # i.e. all videos for which video.data[duration].present? would return true.
220
+ ```
221
+
222
+ Note that Boolean searchable fields will automatically transcribe the boolean value into either true or false; presence detection does not currently take this into consideration, so while a presence detection fragment will match a selector which evaluates to true, it will also match a selector which evaluates to false.
223
+
141
224
  ### Querying docs about their searchables
142
225
 
143
226
  SearchMagic provides some utility methods which can be used to find out information about a document's searchable fields:
@@ -160,7 +243,7 @@ Similar caveats exist for **arrangeable_values**. Its main purpose is to enable
160
243
 
161
244
  ## Global Configuration
162
245
 
163
- For the most part, configuration options are local to the documents and fields they are defined within. However, there are a few global options which are used across models, which can be altered through global configuration. These options (okay, for right now, "option", as there is only one) can be accessed through SearchMagic's **config** hash:
246
+ For the most part, configuration options are local to the documents and fields they are defined within. However, there are a few global options which are used across models, which can be altered through global configuration. These options can be accessed through SearchMagic's **config** hash:
164
247
 
165
248
  ```ruby
166
249
  SearchMagic.config # for global options!
@@ -188,6 +271,18 @@ Note that **search_for** will immediately use the new separator value after a ch
188
271
 
189
272
  Setting **selector_value_separator** to **nil** results in the same behavior as setting it to ':'.
190
273
 
274
+ ### :presence_detector
275
+
276
+ _(As of 0.3.0)_
277
+
278
+ Defaults to '?'. This is the token matched by **search_for** to signal that a simple presence detection query fragment is being requested.
279
+
280
+ ### :default_search_mode
281
+
282
+ _(As of 0.3.0)_
283
+
284
+ Defaults to 'all'. This is the search mode that will be used by default unless overridden by a **mode** query fragment. While this can be set to anything, the only values which alter behavior are **nil**, **:all/"all"**, and **:any/"any"**. Setting this to nil causes the default behavior.
285
+
191
286
  ## A little more depth...
192
287
 
193
288
  ### Search Graph
@@ -234,7 +329,7 @@ Game.searchables.keys # [:title, :price, :high_score, :developer_name, :develope
234
329
 
235
330
  See how the searchables from Developer were automatically added to Game? Only those fields which are within Developer's search graph will be subsumed into Game. Notice how there is a field and an association within Document which are not being searched on? Those are not added to Game's searchables.
236
331
 
237
- Take another look at Game's searchables. Notice the way that fields coming from an association are handled? They are receive a prefix equivalent to the name of the association. This allows SearchMagic to build rather complex search graphs without having to necessarily worry about weird aliasing issues. The values coming from these fields will be stored within each Game instance's **searchable_values** with this prefixed name, and **search_for** will likewise be expecting the use of the prefixed names.
332
+ Take another look at Game's searchables. Notice the way that fields coming from an association are handled? They all receive a prefix equivalent to the name of the association. This allows SearchMagic to build rather complex search graphs without having to necessarily worry about weird aliasing issues. The values coming from these fields will be stored within each Game instance's **searchable_values** with this prefixed name, and **search_for** will likewise be expecting the use of the prefixed names.
238
333
 
239
334
  ```ruby
240
335
  game = Game.search_for("developer_name:bethesda title:skyrim").first
@@ -267,7 +362,7 @@ There are three important things to note about this:
267
362
  2. All of the searchables coming from **games** are prefixed with the singular form of the association name.
268
363
  3. This includes even those searchables which are, themselves, representative of an association.
269
364
 
270
- SearchMagic should be able to handle as complex of a document graph as you care to throw at it. (*SearchMagic and its developer are not liable for computer's exploding while attempting to process crazy large document graphs.*) While the running example is fairly linear, you are not limited to simple paths like this: you can have search graphs which are as broad and deep as you like. Which leads us to the next topic.
365
+ SearchMagic should be able to handle as complex of a document graph as you care to throw at it. (*SearchMagic and its developer are not liable for computers exploding while attempting to process crazy large document graphs.*) While the running example is fairly linear, you are not limited to simple paths like this: you can have search graphs which are as broad and deep as you like. Which leads us to the next topic.
271
366
 
272
367
  #### Cyclic Searches
273
368
 
@@ -299,7 +394,7 @@ As you can see, Game remains the same, while Developer gains a few extra searcha
299
394
 
300
395
  ### arrange
301
396
 
302
- Now that we've explored the search graph, its time to revisit arranging data. Any values coming from a document's searchables are replicated in a hash within that document: its **arrangeable_values**. Will slightly costly, when it comes to replicating data across a document hierarchy, it provides a very clever trick: it allows a document to be sorted by any of its searchables, regardless of where they come from. It matters not whether the searchable is a field on the defining document or on an association. It matters not whether a field is coming from a referenced or an embedded document. All are welcome, and all are handled exactly the same.
397
+ Now that we've explored the search graph, its time to revisit arranging data. Any values coming from a document's searchables are replicated in a hash within that document: its **arrangeable_values**. While slightly costly, when it comes to replicating data across a document hierarchy, it provides a very clever trick: it allows a document to be sorted by any of its searchables, regardless of where they come from. It matters not whether the searchable is a field on the defining document or on an association. It matters not whether a field is coming from a referenced or an embedded document. All are welcome, and all are handled exactly the same.
303
398
 
304
399
  Using the running examples from the last sections, let's explore some example usage:
305
400
 
@@ -391,6 +486,10 @@ Developer.search_for("opened_on:'January 1986'")
391
486
 
392
487
  Note, in order for the natural language processing to be invoked properly, the *value* part of a search must be wrapped in quotes; multiple occurrences of a datable searchable will be processed separately, rather than as a unit.
393
488
 
489
+ ## Upgrading
490
+
491
+ * **0.3.0**: Mongoid 2.4.3 or greater is now required, to facilitate the use of the **search_for** scope on an embedded document from within a non-searchable parent.
492
+
394
493
  ## Problems? Comments?
395
494
 
396
495
  Feel free to add an [issue on GitHub](search_magic/issues) or fork the project and send a pull request.
@@ -4,6 +4,7 @@ module SearchMagic
4
4
  require 'search_magic/stack_frame'
5
5
  require 'search_magic/metadata'
6
6
  require 'search_magic/full_text_search'
7
+ require 'search_magic/railtie' if defined?(Rails)
7
8
  include ActiveSupport::Configurable
8
9
  extend ActiveSupport::Concern
9
10
 
@@ -28,34 +28,84 @@ module SearchMagic
28
28
  map(&:first).map(&:name)
29
29
  end
30
30
 
31
+ # To support range searches, this will need to become more complicated. Specifically, it will need to be able
32
+ # to remove any term which contains a [:below, :before, :above, :after] selector, retreive the base selector and
33
+ # the target valuee, and match the latter against the former within the :arrangeable_values array as part of
34
+ # the returned criteria. How this then translates to :values_matching is currently unknown.
31
35
  def search_for(pattern)
32
- separator = Regexp.escape(SearchMagic.config.selector_value_separator || ':')
36
+ options, pattern = strip_option_terms_from(pattern)
37
+ terms = terms_for(pattern)
38
+ unless terms.blank?
39
+ send( :"#{options[:mode] || default_search_mode}_in", :searchable_values => terms)
40
+ else
41
+ criteria
42
+ end
43
+ end
44
+ alias :and_for :search_for
45
+
46
+ def arrange(arrangeable, direction = :asc)
47
+ arrangeable.blank? || !searchables.keys.include?(arrangeable.to_sym) ? criteria : order_by([["arrangeable_values.#{arrangeable}", direction]])
48
+ end
49
+
50
+ def strip_option_terms_from(pattern)
51
+ unless pattern.blank?
52
+ [Hash[*(pattern.scan(option_terms).flatten)].symbolize_keys, pattern.gsub(option_terms, '').strip]
53
+ else
54
+ [{}, pattern]
55
+ end
56
+ end
57
+
58
+ def terms_for(pattern)
33
59
  rval = /("[^"]+"|'[^']+'|\S+)/
34
- rsearch = /(?:(#{searchables.keys.join('|')})#{separator}#{rval})|#{rval}/i
60
+ rnot_separator = "[^#{separator}]+"
61
+ rsearch = /(?:(#{searchable_names})(?:#{separator}#{rval}|(#{presence_detector_escaped})))|#{rval}/i
35
62
  unless pattern.blank?
36
63
  terms = pattern.scan(rsearch).map(&:compact).map do |term|
37
- selector = term.length > 1 ? Regexp.escape(term.first) : "[^#{separator}]+"
38
- metadata = searchables[term.first.to_sym] if term.length > 1
64
+ selector = term.length > 1 ? Regexp.escape(term.first) : rnot_separator
65
+ metadata = searchables[term.first.match(/^[^:]+/)[0].to_sym] if term.length > 1
39
66
  parsed_date = Chronic.parse(term.last) if metadata && metadata.datable?
40
67
  prefix = "#{selector}#{separator}"
41
68
  prefix = "(#{prefix})?" if term.length == 1
42
- fragment = /#{selector}#{separator}#{parsed_date}/i if parsed_date
69
+ fragment = /^#{selector}#{separator}#{parsed_date}/i if parsed_date
70
+ fragment = /^#{prefix}[^#{separator}\s]+/i if term.last == presence_detector
43
71
  fragment || term.last.scan(/\b(\S+)\b/).flatten.map do |word|
44
- /#{prefix}.*#{Regexp.escape(word)}/i
72
+ /^#{prefix}.*#{Regexp.escape(word)}/i
45
73
  end
46
74
  end.flatten
47
- all_in(:searchable_values => terms)
48
75
  else
49
- criteria
76
+ []
50
77
  end
51
78
  end
52
- alias :and_for :search_for
53
79
 
54
- def arrange(arrangeable, direction = :asc)
55
- arrangeable.blank? || !searchables.keys.include?(arrangeable.to_sym) ? criteria : order_by([["arrangeable_values.#{arrangeable}", direction]])
80
+ private
81
+
82
+ def searchable_names
83
+ @searchable_names ||= searchables.values.map {|metadata| metadata.search_regex_fragment}.join("|")
56
84
  end
57
85
 
58
- private
86
+ def option_terms
87
+ @option_terms ||= Regexp.union(
88
+ *{
89
+ :mode => [:all, :any]
90
+ }.map {|key, value| /(#{key})#{separator}(#{value.join('|')})/i}
91
+ )
92
+ end
93
+
94
+ def presence_detector_escaped
95
+ @presence_detector_escaped ||= Regexp.escape(presence_detector)
96
+ end
97
+
98
+ def presence_detector
99
+ @presence_detector ||= (SearchMagic.config.presence_detector || '?')
100
+ end
101
+
102
+ def separator
103
+ @separator ||= Regexp.escape(SearchMagic.config.selector_value_separator || ':')
104
+ end
105
+
106
+ def default_search_mode
107
+ Regexp.escape(([SearchMagic.config.default_search_mode.to_s] & %w{all any}).first || 'all')
108
+ end
59
109
 
60
110
  def create_searchables
61
111
  stack, visited, fields = [StackFrame.new(self)], {}, []
@@ -78,6 +128,13 @@ module SearchMagic
78
128
  fields.index_by(&:name)
79
129
  end
80
130
  end
131
+
132
+ def values_matching(pattern)
133
+ options, pattern = self.class.strip_option_terms_from(pattern)
134
+ terms = self.class.terms_for(pattern)
135
+ r = Regexp.union(*terms)
136
+ searchable_values.grep(r)
137
+ end
81
138
 
82
139
  private
83
140
 
@@ -12,6 +12,10 @@ module SearchMagic
12
12
  @name ||= through.map(&:term).compact.join("_").to_sym
13
13
  end
14
14
 
15
+ def search_regex_fragment
16
+ self.hashable? ? "#{name}#{separator}[^#{separator}\\s]+" : name.to_s
17
+ end
18
+
15
19
  def comparable?
16
20
  @comparable ||= self.type.public_instance_methods.include? :<
17
21
  end
@@ -23,30 +27,45 @@ module SearchMagic
23
27
  def unnameable?
24
28
  @unnameable ||= self.name == :""
25
29
  end
26
-
27
- def value_for(obj, keep_punctuation)
28
- v = get_value(obj)
29
- v = v.is_a?(Array) ? v.join(" ") : v.to_s
30
- v = v.gsub(/[[:punct:]]/, ' ') unless keep_punctuation
31
- v
32
- end
33
30
 
31
+ def hashable?
32
+ @hashable ||= self.type <= Hash
33
+ end
34
+
34
35
  def arrangeable_value_for(obj)
35
36
  post_process(get_value(obj))
36
37
  end
37
38
 
38
39
  def searchable_value_for(obj)
39
- value_for(obj, options[:keep_punctuation]).downcase.split.map {|word| [name.blank? ? nil : name, word].compact.join(SearchMagic.config.selector_value_separator || ':')}
40
+ value_for(get_value(obj)).downcase.split.map {|word| [name.blank? ? nil : name, word].compact.join(separator)}
40
41
  end
41
42
 
42
43
  private
43
44
 
45
+ def value_for(obj)
46
+ v = obj
47
+ v = v.map {|key, value| value_for(value).split.map {|word| "#{key}#{separator}#{word}"} }.flatten if obj.is_a?(Hash)
48
+ v = v.is_a?(Array) ? v.join(" ") : v.to_s
49
+ options[:keep_punctuation] || obj.is_a?(Hash) ? v : v.to_s.gsub(/[[:punct:]]/, ' ')
50
+ end
51
+
52
+ def separator
53
+ SearchMagic.config.selector_value_separator || ':'
54
+ end
55
+
44
56
  def get_value(obj)
45
- self.through.map(&:field_name).inject(obj) {|memo, method| memo.is_a?(Array) ? memo.map{|o| o.send(method)} : memo.send(method)}
57
+ self.through.map(&:field_name).inject(obj) {|memo, method| memo.present? ? (memo.is_a?(Array) ? memo.map{|o| o.send(method)} : memo.send(method)) : nil}
46
58
  end
47
59
 
48
60
  def post_process(value)
49
- value.is_a?(Array) ? value.map {|obj| convert_date_to_time(obj)} : convert_date_to_time(value)
61
+ case(value.class)
62
+ when Array
63
+ value.map {|obj| convert_date_to_time(obj)}
64
+ when Hash
65
+ Hash[*value.map {|key, value| [key, convert_date_to_time(value)]}]
66
+ else
67
+ convert_date_to_time(value)
68
+ end
50
69
  end
51
70
 
52
71
  def convert_date_to_time(value)
@@ -0,0 +1,7 @@
1
+ module SearchMagic
2
+ class Railtie < Rails::Railtie
3
+ rake_tasks do
4
+ Dir["tasks/**/*.rake"].each { |ext| load ext }
5
+ end
6
+ end
7
+ end
@@ -1,3 +1,3 @@
1
1
  module SearchMagic
2
- VERSION = "0.2.0"
2
+ VERSION = "0.3.0"
3
3
  end
@@ -0,0 +1,8 @@
1
+ namespace :search_magic do
2
+ desc "Updates each SearchMagic document's :searchable_values array"
3
+ task :rebuild => :environment do |t, args|
4
+ Module.constants.map {|c| Module.const_get(c)}.select {|c| c.class == Class && c < SearchMagic}.each do |document|
5
+ document.all.each {|d| d.save!}
6
+ end
7
+ end
8
+ end
@@ -12,13 +12,13 @@ Gem::Specification.new do |s|
12
12
  s.summary = %q{SearchMagic provides scoped full text search and sort capabilities to Mongoid documents}
13
13
  s.description = %q{Adds scopes to a Mongoid document providing search and sort capabilities on arbitrary fields and associations.}
14
14
 
15
- s.add_dependency("mongoid", ">= 2.0.0")
15
+ s.add_dependency("mongoid", ">= 2.4.3")
16
16
  s.add_dependency("chronic")
17
17
  s.add_development_dependency("rspec")
18
18
  s.add_development_dependency("database_cleaner")
19
19
  s.add_development_dependency("bson_ext")
20
20
  s.add_development_dependency("fabrication")
21
- s.add_development_dependency('ruby-debug19')
21
+ # s.add_development_dependency('ruby-debug19')
22
22
 
23
23
  s.rubyforge_project = "search_magic"
24
24
 
@@ -0,0 +1,6 @@
1
+ Fabricator(:game) do
2
+ title { Fabricate.sequence(:game) {|i| "Game Title #{i}"} }
3
+ price { [19.95, 45.50, 59.99, 79.99, 99.99].sample }
4
+ high_score { 500 + rand(1000) }
5
+ released_on { [1.month.ago, 6.months.ago, 1.year.ago, 10.years.ago].sample.to_date }
6
+ end
@@ -0,0 +1,4 @@
1
+ Fabricator(:user) do
2
+ name { Fabricate.sequence(:user_name) {|i| "user-name-#{i}"} }
3
+ watchlists(:count => 5) {|user, watchlist| Fabricate(:watchlist, :user => user)}
4
+ end
@@ -0,0 +1,9 @@
1
+ Fabricator(:video) do
2
+ title { Fabricate.sequence(:video_title) {|i| "video-title-#{i}"} }
3
+ extra {
4
+ {
5
+ "resolution" => %w[480i 480p 720p 1080p 1080i 1080p].sample,
6
+ "duration" => 180 * rand
7
+ }
8
+ }
9
+ end
@@ -0,0 +1,3 @@
1
+ Fabricator(:watchlist) do
2
+ description { Fabricate.sequence(:watchlist_description) {|i| "watchlist-#{i}"} }
3
+ end
@@ -9,7 +9,8 @@ class Game
9
9
  referenced_in :developer
10
10
 
11
11
  search_on :title
12
- search_on :price
12
+ search_on :price, :keep_punctuation => true
13
13
  search_on :high_score
14
+ search_on :released_on, :keep_punctuation => true
14
15
  search_on :developer, :except => :opened_on
15
16
  end
@@ -7,6 +7,7 @@ class ModelWithFieldTypes
7
7
  field :date_field, :type => Date
8
8
  field :float_field, :type => Float
9
9
  field :bool_field, :type => Boolean
10
+ field :hash_field, :type => Hash
10
11
 
11
12
  def some_value
12
13
  true
@@ -18,5 +19,6 @@ class ModelWithFieldTypes
18
19
  search_on :date_field
19
20
  search_on :float_field
20
21
  search_on :bool_field
22
+ search_on :hash_field
21
23
  search_on :some_value
22
24
  end
@@ -0,0 +1,6 @@
1
+ class User
2
+ include Mongoid::Document
3
+ field :name
4
+
5
+ embeds_many :watchlists
6
+ end
@@ -0,0 +1,8 @@
1
+ class Video
2
+ include Mongoid::Document
3
+ include SearchMagic
4
+ field :title, :type => String
5
+ field :extra, :type => Hash, :default => {}
6
+ search_on :title
7
+ search_on :extra
8
+ end
@@ -0,0 +1,8 @@
1
+ class Watchlist
2
+ include Mongoid::Document
3
+ include SearchMagic
4
+ field :description
5
+
6
+ embedded_in :user
7
+ search_on :description
8
+ end
@@ -18,14 +18,14 @@ describe SearchMagic::FullTextSearch do
18
18
 
19
19
  context "when a model excludes an associated documents fields" do
20
20
  subject { Game }
21
- its("searchables.keys") { should include(:title, :price, :high_score, :developer_name) }
21
+ its("searchables.keys") { should include(:title, :price, :high_score, :released_on, :developer_name) }
22
22
  its("searchables.keys") { should_not include(:developer_opened_on) }
23
23
  end
24
24
 
25
25
  context "when a model only includes certain fields from an associated document" do
26
26
  subject { Player }
27
27
  its("searchables.keys") { should include(:name, :game_title, :game_developer_name) }
28
- its("searchables.keys") { should_not include(:game_price, :game_high_score, :game_developer_opened_on)}
28
+ its("searchables.keys") { should_not include(:game_price, :game_high_score, :game_released_on, :game_developer_opened_on)}
29
29
  end
30
30
 
31
31
  context "when a model embeds one other document" do
@@ -1,28 +1,56 @@
1
1
  describe SearchMagic do
2
2
  it { should respond_to(:config) }
3
3
 
4
- shared_examples_for "selector_value_separator" do |separator|
5
- context "with a configuration for :selector_value_separator of '#{separator}'" do
6
- let(:tags) { %w{foo b.a.r b-az q:ux z!png p'zow} }
7
- before(:each) { SearchMagic.config.selector_value_separator = separator }
8
- before(:each) { Fabricate(:asset, :tags => tags) }
9
- after(:each) { SearchMagic.config.selector_value_separator = nil }
10
- let(:asset) { Asset.first }
11
- describe do
12
- subject { asset }
13
- its(:searchable_values) { should include( *tags.map {|tag| "tag#{separator || ':'}#{tag}"} ) }
14
- end
15
- describe "searching for models should use '#{separator || ':'}'" do
16
- subject { Asset.search_for("tag#{separator || ':'}foo") }
17
- its(:count) { should == 1 }
18
- its(:first) { should == asset }
4
+ context "global option: selector_value_separator" do
5
+ shared_examples_for "selector_value_separator" do |separator|
6
+ context "with a configuration for :selector_value_separator of #{separator.inspect}" do
7
+ let(:tags) { %w{foo b.a.r b-az q:ux z!png p'zow} }
8
+ before(:each) { SearchMagic.config.selector_value_separator = separator }
9
+ before(:each) { Fabricate(:asset, :tags => tags) }
10
+ after(:each) { SearchMagic.config.selector_value_separator = nil }
11
+ let(:asset) { Asset.first }
12
+ describe do
13
+ subject { asset }
14
+ its(:searchable_values) { should include( *tags.map {|tag| "tag#{separator || ':'}#{tag}"} ) }
15
+ end
16
+ describe "searching for models should use '#{separator || ':'}'" do
17
+ subject { Asset.search_for("tag#{separator || ':'}foo") }
18
+ its(:count) { should == 1 }
19
+ its(:first) { should == asset }
20
+ end
19
21
  end
20
22
  end
23
+
24
+ it_should_behave_like "selector_value_separator", nil
25
+ it_should_behave_like "selector_value_separator", ':'
26
+ it_should_behave_like "selector_value_separator", '/'
27
+ it_should_behave_like "selector_value_separator", '-'
28
+ it_should_behave_like "selector_value_separator", '!'
21
29
  end
22
30
 
23
- it_should_behave_like "selector_value_separator", nil
24
- it_should_behave_like "selector_value_separator", ':'
25
- it_should_behave_like "selector_value_separator", '/'
26
- it_should_behave_like "selector_value_separator", '-'
27
- it_should_behave_like "selector_value_separator", '!'
31
+ context "global option: default_search_mode" do
32
+ before(:each) do
33
+ Fabricate(:asset, :tags => %w[b.a.r p'zow z!png dirigible])
34
+ Fabricate(:asset, :tags => %w[foo b-az q:ux dirigible])
35
+ end
36
+
37
+ shared_examples_for "default_search_mode" do |default_search_mode, expected_count, should_fail|
38
+ context "with a configuration for :default_search_mode of #{default_search_mode.inspect}" do
39
+ let(:pattern) { "dirigible z!png" }
40
+ before(:each) { SearchMagic.config.default_search_mode = default_search_mode }
41
+ after(:each) { SearchMagic.config.default_search_mode = nil }
42
+ describe "searching without an explicit mode" do
43
+ subject { Asset.search_for(pattern) }
44
+ its(:count) { should == expected_count }
45
+ end
46
+ end
47
+ end
48
+
49
+ it_should_behave_like "default_search_mode", nil, 1
50
+ it_should_behave_like "default_search_mode", "all", 1
51
+ it_should_behave_like "default_search_mode", "any", 2
52
+ it_should_behave_like "default_search_mode", :all, 1
53
+ it_should_behave_like "default_search_mode", :any, 2
54
+ it_should_behave_like "default_search_mode", "unsupported", 1
55
+ end
28
56
  end
@@ -0,0 +1,59 @@
1
+ require 'spec_helper'
2
+
3
+ describe SearchMagic do
4
+ context "when a model includes an embedded hash" do
5
+ subject { Video }
6
+ its("searchables.keys") { should == [:title, :extra] }
7
+ its(:searchable_names) { should == "title|extra:[^:\\s]+"}
8
+ end
9
+
10
+ context "a model with an embedded hash" do
11
+ subject { Video.searchables[:extra] }
12
+ it { should be_hashable }
13
+ end
14
+
15
+ context "searching for a model with an embedded hash" do
16
+ before(:each) do
17
+ Fabricate(:video, :extra => {"resolution" => "1080p"})
18
+ Fabricate(:video, :extra => {"resolution" => "1080i"})
19
+ Fabricate(:video, :extra => {"resolution" => "720p"})
20
+ Fabricate(:video, :extra => {"director" => "Alan Smithee"})
21
+ end
22
+
23
+ shared_examples_for "an embedded hash" do |key, expected_value, expected_count|
24
+ if expected_value.present?
25
+ context "the criteria \"extra:#{key}:#{expected_value}\"" do
26
+ subject { Video.search_for("extra:#{key}:#{expected_value}") }
27
+ its(:selector) { should == {:searchable_values => {"$all" => expected_value.gsub(/^'([^']+)'$/, '\1').split.map {|word| /^extra:#{key}:.*#{word}/i }}}}
28
+ its(:count) { should == 1 }
29
+ end
30
+ context "the instance" do
31
+ Video.search_for("extra:#{key}:#{expected_value}").each do |video|
32
+ context video do
33
+ its(:extra) { should_not be_blank }
34
+ it { video.extra[key].should =~ /#{expected_value}/i }
35
+ end
36
+ end
37
+ end
38
+ end
39
+ context "the criteria \"extra:#{key}?\"" do
40
+ subject { Video.search_for("extra:#{key}?") }
41
+ its(:selector) { should == {:searchable_values => {"$all" => [/^extra:#{key}:[^:\s]+/i]} }}
42
+ its(:count) { should == expected_count }
43
+ end
44
+ end
45
+
46
+ it_should_behave_like "an embedded hash", "resolution", "1080p", 3
47
+ it_should_behave_like "an embedded hash", "resolution", "1080i", 3
48
+ it_should_behave_like "an embedded hash", "resolution", "720p", 3
49
+ it_should_behave_like "an embedded hash", "director", "smithee", 1
50
+ it_should_behave_like "an embedded hash", "director", "alan", 1
51
+ it_should_behave_like "an embedded hash", "director", "'alan smithee'", 1
52
+ it_should_behave_like "an embedded hash", "widget", nil, 0
53
+
54
+ context "an instance's values_matching" do
55
+ subject { Video.search_for("extra:resolution?").first.values_matching("extra:resolution?") }
56
+ it { should include("extra:resolution:1080p") }
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,35 @@
1
+ describe SearchMagic do
2
+ context "when searching for documents through an attribute of a non-searchable class" do
3
+ before(:each) do
4
+ Fabricate(:user)
5
+ end
6
+
7
+ context "an embedded documents searchable_values should not be empty" do
8
+ subject { User.first.watchlists.first }
9
+ its(:searchable_values) { should == %w[description:watchlist description:0] }
10
+ end
11
+
12
+ context "directly searching an embedded document" do
13
+ subject { User.first.watchlists.where(:description => /^([^:]+:)?.*watchlist/i) }
14
+ its(:count) { should == 5 }
15
+ end
16
+
17
+ context "the terms for a given watchlist" do
18
+ subject { User.first.watchlists.first }
19
+ it { subject.values_matching("watchlist").should == ["description:watchlist"] }
20
+ end
21
+
22
+ context "directly searching the searchable_values of an embedded document" do
23
+ subject { User.first.watchlists.all_in(:searchable_values => [/^([^:]+:)?.*watchlist/i]) }
24
+ its(:selector) { should == {:searchable_values => {"$all" => [/^([^:]+:)?.*watchlist/i]}} }
25
+ its(:count) { should == 5 }
26
+ end
27
+
28
+ context "the search query should be" do
29
+ subject { User.first.watchlists.search_for("watchlist") }
30
+ its(:selector) { should == {:searchable_values => {"$all" => [/^([^:]+:)?.*watchlist/i]}} }
31
+ it { expect { subject.count }.to_not raise_error }
32
+ its(:count) { should == 5 }
33
+ end
34
+ end
35
+ end
@@ -12,7 +12,7 @@ describe SearchMagic::Metadata do
12
12
  end
13
13
 
14
14
  context "when dealing with breadcrumbs with :skip_prefix" do
15
- subject { SearchMagic::Metadata.new(through: [SearchMagic::Breadcrumb.new(:foo, {skip_prefix: true}), SearchMagic::Breadcrumb.new(:bar, {})]) }
15
+ subject { SearchMagic::Metadata.new(:through => [SearchMagic::Breadcrumb.new(:foo, {:skip_prefix => true}), SearchMagic::Breadcrumb.new(:bar, {})]) }
16
16
  its(:name) { should == :bar }
17
17
  end
18
18
 
@@ -20,4 +20,11 @@ describe SearchMagic::Metadata do
20
20
  subject { SearchMagic::Metadata.new(:through => [SearchMagic::Breadcrumb.new(:foo, {}), SearchMagic::Breadcrumb.new(:bar, {:skip_prefix => true}), SearchMagic::Breadcrumb.new(:baz, {:as => :qux})]) }
21
21
  its(:name) { should == :foo_qux }
22
22
  end
23
+
24
+ context "when dealing with a nil instance for a given breadcrump" do
25
+ let(:metadata) { SearchMagic::Metadata.new(:through => [SearchMagic::Breadcrumb.new(:foo, {}), SearchMagic::Breadcrumb.new(:bar, {})], :options => {}) }
26
+ let(:instance) { Struct.new(:foo).new(nil) }
27
+ it { expect { metadata.searchable_value_for(instance) }.to_not raise_error }
28
+ it { metadata.searchable_value_for(instance).should be_blank }
29
+ end
23
30
  end
@@ -1,19 +1,19 @@
1
1
  describe SearchMagic do
2
+ shared_examples_for "metadata" do |field, type, comparable, search_regex_fragment|
3
+ subject { ModelWithFieldTypes.searchables[field] }
4
+ its(:type) { should == type }
5
+ its(:comparable?) { should == comparable }
6
+ its(:search_regex_fragment) { should == search_regex_fragment }
7
+ end
8
+
2
9
  context "when included in a model which defines field types" do
3
- subject { ModelWithFieldTypes.searchables }
4
- it { subject[:generic_field].type.should == Mongoid::Fields::Serializable::Object }
5
- it { subject[:generic_field].comparable?.should be_false }
6
- it { subject[:string_field].type.should == String }
7
- it { subject[:string_field].comparable?.should be_true }
8
- it { subject[:float_field].type.should == Float }
9
- it { subject[:float_field].comparable?.should be_true }
10
- it { subject[:int_field].type.should == Integer }
11
- it { subject[:int_field].comparable?.should be_true }
12
- it { subject[:date_field].type.should == Date }
13
- it { subject[:date_field].comparable?.should be_true }
14
- it { subject[:bool_field].type.should == Boolean }
15
- it { subject[:bool_field].comparable?.should be_false }
16
- it { subject[:some_value].type.should == Object }
17
- it { subject[:some_value].comparable?.should be_false }
10
+ it_behaves_like "metadata", :generic_field, Object, false, "generic_field"
11
+ it_behaves_like "metadata", :string_field, String, true, "string_field"
12
+ it_behaves_like "metadata", :float_field, Float, true, "float_field"
13
+ it_behaves_like "metadata", :int_field, Integer, true, "int_field"
14
+ it_behaves_like "metadata", :date_field, Date, true, "date_field"
15
+ it_behaves_like "metadata", :bool_field, Boolean, false, "bool_field"
16
+ it_behaves_like "metadata", :some_value, Object, false, "some_value"
17
+ it_behaves_like "metadata", :hash_field, Hash, false, "hash_field:[^:\\s]+"
18
18
  end
19
19
  end
@@ -0,0 +1,57 @@
1
+ require 'spec_helper'
2
+
3
+ describe SearchMagic do
4
+ context "when searching a model" do
5
+ before(:each) do
6
+ Fabricate(:game, :high_score => 250, :price => 19.95, :released_on => 2.weeks.ago)
7
+ Fabricate(:game, :high_score => 500, :price => 49.95, :released_on => 6.weeks.ago)
8
+ Fabricate(:game, :high_score => 750, :price => 59.95, :released_on => 4.months.ago)
9
+ Fabricate(:game, :high_score => 1000, :price => 79.95, :released_on => 8.months.ago)
10
+ Fabricate(:game, :high_score => 1250, :price => 99.95, :released_on => 4.years.ago)
11
+ Fabricate(:game, :high_score => 1500, :price => 149.95, :released_on => 15.years.ago)
12
+ end
13
+
14
+ shared_examples_for "a simple range search" do |field, selector, value, expected_count|
15
+ context "the criteria" do
16
+ subject { Game.search_for("#{field}:#{selector}:#{value}") }
17
+ its(:count) { should == expected_count }
18
+ end
19
+ context "the instances" do
20
+ Game.search_for("#{field}:#{selector}:#{value}").each do |game|
21
+ context game do
22
+ case selector
23
+ when :below, :before
24
+ its(field) { should be < value }
25
+ when :above, :after
26
+ its(field) { should be > value }
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+
33
+ context "with a :below selector" do
34
+ it_should_behave_like "a simple range search", :high_score, :below, 1000, 3
35
+ it_should_behave_like "a simple range search", :price, :below, 50.00, 2
36
+ it_should_behave_like "a simple range search", :released_on, :below, 1.year.ago, 2
37
+ end
38
+
39
+ context "with a :before selector" do
40
+ it_should_behave_like "a simple range search", :high_score, :before, 1000, 3
41
+ it_should_behave_like "a simple range search", :price, :before, 50.00, 2
42
+ it_should_behave_like "a simple range search", :released_on, :before, 1.year.ago, 2
43
+ end
44
+
45
+ context "with an :above selector" do
46
+ it_should_behave_like "a simple range search", :high_score, :above, 1000, 2
47
+ it_should_behave_like "a simple range search", :price, :above, 50.00, 4
48
+ it_should_behave_like "a simple range search", :released_on, :above, 1.year.ago, 4
49
+ end
50
+
51
+ context "with an :after selector" do
52
+ it_should_behave_like "a simple range search", :high_score, :after, 1000, 2
53
+ it_should_behave_like "a simple range search", :price, :after, 50.00, 4
54
+ it_should_behave_like "a simple range search", :released_on, :after, 1.year.ago, 4
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,49 @@
1
+ require 'spec_helper'
2
+
3
+ describe SearchMagic do
4
+ context "the model class" do
5
+ subject { Asset }
6
+ it { should respond_to(:strip_option_terms_from).with(1).argument }
7
+ it { subject.strip_option_terms_from(nil).should == [{}, nil] }
8
+ it { subject.strip_option_terms_from("tag:foo").should == [{}, "tag:foo"] }
9
+ it { subject.strip_option_terms_from("mode:any tag:foo").should == [{:mode => "any"}, "tag:foo"] }
10
+ it { subject.strip_option_terms_from("mode:all tag:foo").should == [{:mode => "all"}, "tag:foo"] }
11
+ it { subject.strip_option_terms_from("mode:all mode:any").should == [{:mode => "any"}, ""] }
12
+ it { subject.strip_option_terms_from("mode:any mode:all").should == [{:mode => "all"}, ""] }
13
+ it { subject.strip_option_terms_from("mode:all mode:any mode:all").should == [{:mode => "all"}, ""] }
14
+ end
15
+ context "when searching" do
16
+ before(:each) do
17
+ 5.times { Fabricate(:asset, :tags => %w[b.a.r p'zow z!png dirigible]) }
18
+ 5.times { Fabricate(:asset, :tags => %w[foo b-az q:ux dirigible]) }
19
+ end
20
+
21
+ shared_examples_for "search" do |pattern, expected_count|
22
+ subject { Asset.search_for(pattern) }
23
+ its(:count) { should == expected_count }
24
+ end
25
+
26
+ context "without a search mode" do
27
+ it_should_behave_like "search", "tag:foo tag:b.a.r", 0
28
+ it_should_behave_like "search", "tag:foo tag:b-az", 5
29
+ it_should_behave_like "search", "tag:dirigible", 10
30
+ it_should_behave_like "search", "tag:foo tag:dirigible", 5
31
+ end
32
+
33
+ context "with a search mode" do
34
+ context "of all" do
35
+ it_should_behave_like "search", "mode:all tag:foo tag:b.a.r", 0
36
+ it_should_behave_like "search", "mode:all tag:foo tag:b-az", 5
37
+ it_should_behave_like "search", "mode:all tag:dirigible", 10
38
+ it_should_behave_like "search", "mode:all tag:foo tag:dirigible", 5
39
+ end
40
+
41
+ context "of any" do
42
+ it_should_behave_like "search", "mode:any tag:foo tag:b.a.r", 10
43
+ it_should_behave_like "search", "mode:any tag:foo tag:b-az", 5
44
+ it_should_behave_like "search", "mode:any tag:dirigible", 10
45
+ it_should_behave_like "search", "mode:any tag:foo tag:dirigible", 10
46
+ end
47
+ end
48
+ end
49
+ end
@@ -2,18 +2,18 @@ describe SearchMagic::FullTextSearch do
2
2
  context "when searching for \"title:'foo bar'\"" do
3
3
  subject { Asset.search_for("title:'foo bar'") }
4
4
  its("selector.keys") { should include(:searchable_values) }
5
- it { subject.selector[:searchable_values]["$all"].should include(/title:.*foo/i, /title:.*bar/i)}
5
+ it { subject.selector[:searchable_values]["$all"].should include(/^title:.*foo/i, /^title:.*bar/i)}
6
6
  end
7
7
 
8
8
  context "when searching for 'title:\"foo bar\"'" do
9
9
  subject { Asset.search_for('title:"foo bar"') }
10
10
  its("selector.keys") { should include(:searchable_values) }
11
- it { subject.selector[:searchable_values]["$all"].should include(/title:.*foo/i, /title:.*bar/i)}
11
+ it { subject.selector[:searchable_values]["$all"].should include(/^title:.*foo/i, /^title:.*bar/i)}
12
12
  end
13
13
 
14
14
  context "when searching for 'title:foo title:bar'" do
15
15
  subject { Asset.search_for('title:foo title:bar') }
16
16
  its("selector.keys") { should include(:searchable_values) }
17
- it { subject.selector[:searchable_values]["$all"].should include(/title:.*foo/i, /title:.*bar/i)}
17
+ it { subject.selector[:searchable_values]["$all"].should include(/^title:.*foo/i, /^title:.*bar/i)}
18
18
  end
19
19
  end
@@ -0,0 +1,55 @@
1
+ require 'spec_helper'
2
+
3
+ describe SearchMagic do
4
+ context "documents should have a method for finding values matching a pattern" do
5
+ let(:asset) { Fabricate(:asset) }
6
+ subject { asset }
7
+ it { should respond_to(:values_matching).with(1).argument }
8
+ end
9
+
10
+ context "given a pattern, :values_matching should return an array" do
11
+ let(:asset) { Fabricate(:asset) }
12
+ subject { asset.values_matching("some pattern") }
13
+ it { should be_an(Array) }
14
+ end
15
+
16
+ context "given a pattern" do
17
+ before(:each) { Fabricate(:asset, :tags => %w{b.a.r p'zow}) }
18
+ let(:asset) { Asset.first }
19
+ let(:pattern) { "asset generic tag:p'zow" }
20
+ context "which matches the document" do
21
+ subject { Asset.search_for(pattern) }
22
+ its(:count) { should == 1 }
23
+ its(:first) { should == asset }
24
+ end
25
+ context ":values_matching" do
26
+ subject { asset.values_matching(pattern) }
27
+ its(:length) { should == 3 }
28
+ it { should include("title:asset", "description:generic", "tag:p'zow") }
29
+ it { should_not include("title:title", "description:description", "tag:b.a.r") }
30
+ end
31
+ end
32
+
33
+ shared_examples_for "a pattern" do |mode, pattern, expected_values|
34
+ before(:each) { Fabricate(:asset, tags: %w{b.a.r p'zow}) }
35
+ let(:asset) { Asset.first }
36
+ context "which matches the document" do
37
+ subject { Asset.search_for(pattern) }
38
+ its(:count) { should == 1 }
39
+ its(:first) { should == asset }
40
+ end
41
+ context ":values_matching" do
42
+ subject { asset.values_matching("mode:#{mode || :all} #{pattern}") }
43
+ its(:length) { should == expected_values.length }
44
+ it { should =~ expected_values }
45
+ end
46
+ end
47
+
48
+ # The following specs are for a potential change to values_matching, which would allow it to take search mode
49
+ # into consideration. Needs more thought.
50
+ # context "given a pattern with a search mode" do
51
+ # it_should_behave_like "a pattern", :all, "asset generic tag:p'zow", %w{title:asset description:generic tag:p'zow}
52
+ # it_should_behave_like "a pattern", :all, "dirigible tag:p'zow", %w{}
53
+ # it_should_behave_like "a pattern", :any, "dirigible b.a.r", %w{tag:b.a.r}
54
+ # end
55
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: search_magic
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,22 +9,22 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2011-12-03 00:00:00.000000000Z
12
+ date: 2012-01-30 00:00:00.000000000Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: mongoid
16
- requirement: &70196635537280 !ruby/object:Gem::Requirement
16
+ requirement: &70118998615140 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ! '>='
20
20
  - !ruby/object:Gem::Version
21
- version: 2.0.0
21
+ version: 2.4.3
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *70196635537280
24
+ version_requirements: *70118998615140
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: chronic
27
- requirement: &70196635536820 !ruby/object:Gem::Requirement
27
+ requirement: &70118998612180 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
30
  - - ! '>='
@@ -32,10 +32,10 @@ dependencies:
32
32
  version: '0'
33
33
  type: :runtime
34
34
  prerelease: false
35
- version_requirements: *70196635536820
35
+ version_requirements: *70118998612180
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: rspec
38
- requirement: &70196635536300 !ruby/object:Gem::Requirement
38
+ requirement: &70118998611160 !ruby/object:Gem::Requirement
39
39
  none: false
40
40
  requirements:
41
41
  - - ! '>='
@@ -43,10 +43,10 @@ dependencies:
43
43
  version: '0'
44
44
  type: :development
45
45
  prerelease: false
46
- version_requirements: *70196635536300
46
+ version_requirements: *70118998611160
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: database_cleaner
49
- requirement: &70196635535760 !ruby/object:Gem::Requirement
49
+ requirement: &70118998609200 !ruby/object:Gem::Requirement
50
50
  none: false
51
51
  requirements:
52
52
  - - ! '>='
@@ -54,10 +54,10 @@ dependencies:
54
54
  version: '0'
55
55
  type: :development
56
56
  prerelease: false
57
- version_requirements: *70196635535760
57
+ version_requirements: *70118998609200
58
58
  - !ruby/object:Gem::Dependency
59
59
  name: bson_ext
60
- requirement: &70196635535140 !ruby/object:Gem::Requirement
60
+ requirement: &70118998606780 !ruby/object:Gem::Requirement
61
61
  none: false
62
62
  requirements:
63
63
  - - ! '>='
@@ -65,10 +65,10 @@ dependencies:
65
65
  version: '0'
66
66
  type: :development
67
67
  prerelease: false
68
- version_requirements: *70196635535140
68
+ version_requirements: *70118998606780
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: fabrication
71
- requirement: &70196635534340 !ruby/object:Gem::Requirement
71
+ requirement: &70118998604200 !ruby/object:Gem::Requirement
72
72
  none: false
73
73
  requirements:
74
74
  - - ! '>='
@@ -76,18 +76,7 @@ dependencies:
76
76
  version: '0'
77
77
  type: :development
78
78
  prerelease: false
79
- version_requirements: *70196635534340
80
- - !ruby/object:Gem::Dependency
81
- name: ruby-debug19
82
- requirement: &70196635533880 !ruby/object:Gem::Requirement
83
- none: false
84
- requirements:
85
- - - ! '>='
86
- - !ruby/object:Gem::Version
87
- version: '0'
88
- type: :development
89
- prerelease: false
90
- version_requirements: *70196635533880
79
+ version_requirements: *70118998604200
91
80
  description: Adds scopes to a Mongoid document providing search and sort capabilities
92
81
  on arbitrary fields and associations.
93
82
  email:
@@ -105,13 +94,19 @@ files:
105
94
  - lib/search_magic/breadcrumb.rb
106
95
  - lib/search_magic/full_text_search.rb
107
96
  - lib/search_magic/metadata.rb
97
+ - lib/search_magic/railtie.rb
108
98
  - lib/search_magic/stack_frame.rb
109
99
  - lib/search_magic/version.rb
100
+ - lib/tasks/rebuild_searchable_values.rake
110
101
  - search_magic.gemspec
111
102
  - spec/fabricators/asset_fabricator.rb
112
103
  - spec/fabricators/developer_fabricator.rb
113
104
  - spec/fabricators/field_skip_prefix_fabricator.rb
105
+ - spec/fabricators/game_fabricator.rb
114
106
  - spec/fabricators/simple_inclusion_model_fabricator.rb
107
+ - spec/fabricators/user_fabricator.rb
108
+ - spec/fabricators/video_fabricator.rb
109
+ - spec/fabricators/watchlist_fabricator.rb
115
110
  - spec/models/absolutely_not_searchable.rb
116
111
  - spec/models/address.rb
117
112
  - spec/models/asset.rb
@@ -131,20 +126,28 @@ files:
131
126
  - spec/models/phone.rb
132
127
  - spec/models/player.rb
133
128
  - spec/models/simple_inclusion_model.rb
129
+ - spec/models/user.rb
130
+ - spec/models/video.rb
131
+ - spec/models/watchlist.rb
134
132
  - spec/spec_helper.rb
135
133
  - spec/unit/search_magic/arrangements_spec.rb
136
134
  - spec/unit/search_magic/associations_spec.rb
137
135
  - spec/unit/search_magic/breadcrumb_spec.rb
138
136
  - spec/unit/search_magic/configuration_spec.rb
139
137
  - spec/unit/search_magic/date_parsing_spec.rb
138
+ - spec/unit/search_magic/embedded_hash_spec.rb
139
+ - spec/unit/search_magic/embedded_in_non_searchable_class_spec.rb
140
140
  - spec/unit/search_magic/fields_spec.rb
141
141
  - spec/unit/search_magic/graph_searches_spec.rb
142
142
  - spec/unit/search_magic/metadata_spec.rb
143
143
  - spec/unit/search_magic/metadata_type_spec.rb
144
144
  - spec/unit/search_magic/model_updates_spec.rb
145
+ - spec/unit/search_magic/range_searches_spec.rb
146
+ - spec/unit/search_magic/search_modes_spec.rb
145
147
  - spec/unit/search_magic/search_pattern_syntax_spec.rb
146
148
  - spec/unit/search_magic/simple_inclusion_model_spec.rb
147
149
  - spec/unit/search_magic/stack_frame_spec.rb
150
+ - spec/unit/search_magic/values_matching_spec.rb
148
151
  homepage: http://github.com/joshuabowers/search_magic
149
152
  licenses: []
150
153
  post_install_message:
@@ -174,7 +177,11 @@ test_files:
174
177
  - spec/fabricators/asset_fabricator.rb
175
178
  - spec/fabricators/developer_fabricator.rb
176
179
  - spec/fabricators/field_skip_prefix_fabricator.rb
180
+ - spec/fabricators/game_fabricator.rb
177
181
  - spec/fabricators/simple_inclusion_model_fabricator.rb
182
+ - spec/fabricators/user_fabricator.rb
183
+ - spec/fabricators/video_fabricator.rb
184
+ - spec/fabricators/watchlist_fabricator.rb
178
185
  - spec/models/absolutely_not_searchable.rb
179
186
  - spec/models/address.rb
180
187
  - spec/models/asset.rb
@@ -194,17 +201,25 @@ test_files:
194
201
  - spec/models/phone.rb
195
202
  - spec/models/player.rb
196
203
  - spec/models/simple_inclusion_model.rb
204
+ - spec/models/user.rb
205
+ - spec/models/video.rb
206
+ - spec/models/watchlist.rb
197
207
  - spec/spec_helper.rb
198
208
  - spec/unit/search_magic/arrangements_spec.rb
199
209
  - spec/unit/search_magic/associations_spec.rb
200
210
  - spec/unit/search_magic/breadcrumb_spec.rb
201
211
  - spec/unit/search_magic/configuration_spec.rb
202
212
  - spec/unit/search_magic/date_parsing_spec.rb
213
+ - spec/unit/search_magic/embedded_hash_spec.rb
214
+ - spec/unit/search_magic/embedded_in_non_searchable_class_spec.rb
203
215
  - spec/unit/search_magic/fields_spec.rb
204
216
  - spec/unit/search_magic/graph_searches_spec.rb
205
217
  - spec/unit/search_magic/metadata_spec.rb
206
218
  - spec/unit/search_magic/metadata_type_spec.rb
207
219
  - spec/unit/search_magic/model_updates_spec.rb
220
+ - spec/unit/search_magic/range_searches_spec.rb
221
+ - spec/unit/search_magic/search_modes_spec.rb
208
222
  - spec/unit/search_magic/search_pattern_syntax_spec.rb
209
223
  - spec/unit/search_magic/simple_inclusion_model_spec.rb
210
224
  - spec/unit/search_magic/stack_frame_spec.rb
225
+ - spec/unit/search_magic/values_matching_spec.rb