search_magic 0.0.3 → 0.0.4

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/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- search_magic (0.0.2)
4
+ search_magic (0.0.3)
5
5
  mongoid (>= 2.0.0.rc.7)
6
6
 
7
7
  GEM
data/README.textile CHANGED
@@ -29,10 +29,14 @@ bc.. class Address
29
29
  search_on :post_code
30
30
  end
31
31
 
32
- p. At this point, *Address* can be searched by calling its @:search@ method:
32
+ p. At this point, *Address* can be searched by calling its _*:search*_ method:
33
33
 
34
34
  bc. Address.search("state:ca")
35
35
 
36
+ It is also possible to sort models on fields which have been marked as searchable through the _*:arrange*_ method:
37
+
38
+ bc. Address.arrange(:state, :asc)
39
+
36
40
  h3. :search_on
37
41
 
38
42
  Fields that are made searchable by :search_on have their values cached in an embedded array within each document. This array, *:searchable_values*, should contain entries of the form *field_name:value*. The selector, *field_name*, represents a filter which can be used when searching to narrow the search space; it can be manually renamed by passing the *:as* option to :search_on:
@@ -124,6 +128,18 @@ bc. Part.search("table") # full text search on "table"
124
128
  Part.search("category_name:table") # restricts the search for "table" to "category_name"
125
129
  Part.search("bike serial:b1234") # full text search on "bike", with an extra requirement that the serial be "b1234"
126
130
 
131
+ h3. :arrange
132
+
133
+ SearchMagic also provides a utility scope for arranging the model by the searchables defined within it. This method, _*:arrange*_, has one required parameter specifying the searchable to sort on and one optional parameter specifying the sort direction. (If the second parameter is omitted, it will default to ascending.)
134
+
135
+ bc. Part.arrange(:serial)
136
+ Part.arrange(:serial, :asc) # same as last example
137
+ Part.arrange(:category_name, :desc) # arrange the parts in descending order by :category_name
138
+
139
+ As mentioned, _*:arrange*_ is a scope, so it can be chained with other scopes on a given model:
140
+
141
+ bc. Part.search("category_name:table").arrange(:serial, :asc)
142
+
127
143
  h2. Problems? Comments?
128
144
 
129
145
  Feel free to add an "issue on GitHub":search_magic/issues or fork the project and send a pull request. I'm always looking for new ways of bending hardware to my will, so suggestions are welcome.
@@ -5,7 +5,9 @@ module SearchMagic
5
5
  receiver.send :class_attribute, :searchable_fields, :instance_writer => false
6
6
  receiver.send :searchable_fields=, {}
7
7
  receiver.send :field, :searchable_values, :type => Array, :default => []
8
+ receiver.send :field, :arrangeable_values, :type => Hash, :default => {}
8
9
  receiver.send :before_save, :update_searchable_values
10
+ receiver.send :before_save, :update_arrangeable_values
9
11
  end
10
12
 
11
13
  def search_on(field_name, options = {})
@@ -31,6 +33,10 @@ module SearchMagic
31
33
  end
32
34
  end
33
35
 
36
+ def arrange(arrangeable, direction = :asc)
37
+ arrangeable.blank? || !searchables.keys.include?(arrangeable) ? criteria : order_by(["arrangeable_values.#{arrangeable}", direction])
38
+ end
39
+
34
40
  private
35
41
 
36
42
  def create_searchables
@@ -38,9 +44,10 @@ module SearchMagic
38
44
  if association = reflect_on_association(field_name)
39
45
  options[:as] ||= nil
40
46
  only = [options[:only]].flatten.compact
41
- except = [options[:except]].flatten.compact
42
- associated = association.class_name.constantize.searchables.reject {|key, value| except.include?(key) }.select {|key, value| only.blank? ? true : only.include?(key) }
43
- associated.map do |name, metadata|
47
+ except = [:_D_E_A_D_B_3_3_F_, options[:except]].flatten.compact
48
+ associated = association.class_name.constantize.searchables
49
+ wanted = associated.keys.grep(/^(?!.*?(#{except.join("|")})).*/).grep(/^#{only.join("|")}/)
50
+ associated.select {|key, value| wanted.include?(key)}.map do |name, metadata|
44
51
  Metadata.new(:type => self, :through => lambda do |obj|
45
52
  value = obj.send(field_name)
46
53
  value.is_a?(Array) ? value.map {|item| metadata.through.call(item)} : metadata.through.call(value)
@@ -62,36 +69,12 @@ module SearchMagic
62
69
  self.searchable_values = self.class.searchables.values.map {|metadata| metadata.searchable_value_for(self)}.flatten
63
70
  end
64
71
 
65
- def find_searchable_value(name)
66
- matches = self.searchable_values.grep(/^#{name}:(.*)/){$1}
67
- matches.count == 1 ? matches.first : matches
68
- end
69
- end
70
-
71
- class Metadata
72
- attr_accessor :type, :through, :prefix, :field_name, :options
73
-
74
- def initialize(attributes = {})
75
- attributes.each do |key, value|
76
- send(:"#{key}=", value)
72
+ def update_arrangeable_values
73
+ self.arrangeable_values = {}
74
+ self.class.searchables.values.each do |metadata|
75
+ self.arrangeable_values[metadata.name] = metadata.arrangeable_value_for(self)
77
76
  end
78
77
  end
79
-
80
- def name
81
- @name ||= [options[:skip_prefix].presence ? nil : (prefix.present? ? options[:as] || prefix : nil),
82
- prefix.present? ? field_name : (options[:as] || field_name)].compact.join("_").to_sym
83
- end
84
-
85
- def value_for(obj)
86
- v = self.through.call(obj)
87
- v = v.is_a?(Array) ? v.join(" ") : v.to_s
88
- v = v.gsub(/[[:punct:]]/, ' ') unless options[:keep_punctuation]
89
- v
90
- end
91
-
92
- def searchable_value_for(obj)
93
- value_for(obj).downcase.split.map {|word| [name, word].join(":")}
94
- end
95
78
  end
96
79
 
97
80
  def self.included(receiver)
@@ -0,0 +1,31 @@
1
+ module SearchMagic
2
+ class Metadata
3
+ attr_accessor :type, :through, :prefix, :field_name, :options
4
+
5
+ def initialize(attributes = {})
6
+ attributes.each do |key, value|
7
+ send(:"#{key}=", value)
8
+ end
9
+ end
10
+
11
+ def name
12
+ @name ||= [options[:skip_prefix].presence ? nil : (prefix.present? ? options[:as] || prefix : nil),
13
+ prefix.present? ? field_name : (options[:as] || field_name)].compact.join("_").to_sym
14
+ end
15
+
16
+ def value_for(obj, keep_punctuation)
17
+ v = self.through.call(obj)
18
+ v = v.is_a?(Array) ? v.join(" ") : v.to_s
19
+ v = v.gsub(/[[:punct:]]/, ' ') unless keep_punctuation
20
+ v
21
+ end
22
+
23
+ def arrangeable_value_for(obj)
24
+ self.through.call(obj)
25
+ end
26
+
27
+ def searchable_value_for(obj)
28
+ value_for(obj, options[:keep_punctuation]).downcase.split.map {|word| [name, word].join(":")}
29
+ end
30
+ end
31
+ end
@@ -1,3 +1,3 @@
1
1
  module SearchMagic
2
- VERSION = "0.0.3"
2
+ VERSION = "0.0.4"
3
3
  end
data/lib/search_magic.rb CHANGED
@@ -1,3 +1,4 @@
1
1
  module SearchMagic
2
+ require 'search_magic/metadata'
2
3
  require 'search_magic/full_text_search'
3
4
  end
@@ -0,0 +1,10 @@
1
+ class Developer
2
+ include Mongoid::Document
3
+ include SearchMagic::FullTextSearch
4
+ field :name
5
+ field :opened_on, :type => Date
6
+ references_many :games
7
+
8
+ search_on :name
9
+ search_on :opened_on
10
+ end
@@ -0,0 +1,15 @@
1
+ class Game
2
+ include Mongoid::Document
3
+ include SearchMagic::FullTextSearch
4
+ field :title
5
+ field :price, :type => Float
6
+ field :high_score, :type => Integer
7
+ field :released_on, :type => Date
8
+ references_and_referenced_in_many :players
9
+ referenced_in :developer
10
+
11
+ search_on :title
12
+ search_on :price
13
+ search_on :high_score
14
+ search_on :developer, :except => :opened_on
15
+ end
@@ -0,0 +1,9 @@
1
+ class Player
2
+ include Mongoid::Document
3
+ include SearchMagic::FullTextSearch
4
+ field :name
5
+ references_and_referenced_in_many :games
6
+
7
+ search_on :name
8
+ search_on :games, :only => [:title, :developer]
9
+ end
@@ -0,0 +1,86 @@
1
+ require 'spec_helper'
2
+
3
+ describe SearchMagic::FullTextSearch do
4
+ context "when included in a model without :searchables" do
5
+ subject { NoSearchables }
6
+ its("fields.keys") { should include("arrangeable_values") }
7
+ describe "arrangeable_values" do
8
+ subject { NoSearchables.fields["arrangeable_values"] }
9
+ its(:type) { should == Hash }
10
+ its(:default) { should == {} }
11
+ end
12
+
13
+ it { should respond_to(:arrange).with(2).argument }
14
+ end
15
+
16
+ describe "saving a model should run the :update_arrangeable_values callback" do
17
+ subject { Asset.new }
18
+ after(:each) { subject.save }
19
+ it { subject.should_receive :update_arrangeable_values }
20
+ end
21
+
22
+ context "when a model is saved, its :arrangeable_values update" do
23
+ subject { Asset.new(:title => "Foo Bar: The Bazzening", :description => "Sequel to last years hit summer blockbuster.", :tags => ["movies", "foo.bar", "the-bazzening"], :uuid => "ae9d14ee-be93-11df-9fec-78ca39fffe11")}
24
+ before(:each) { subject.save }
25
+ its(:arrangeable_values) { should_not be_empty }
26
+ its(:arrangeable_values) { should include(:title => "Foo Bar: The Bazzening") }
27
+ its(:arrangeable_values) { should include(:description => "Sequel to last years hit summer blockbuster.", ) }
28
+ its(:arrangeable_values) { should include(:tag => ["movies", "foo.bar", "the-bazzening"]) }
29
+ its("arrangeable_values.keys") { should_not include(:uuid) }
30
+ end
31
+
32
+ context "when :arrange is performed on a model with :searchables" do
33
+ before(:each) do
34
+ Asset.create(:title => "Foo Bar: The Bazzening", :description => "Sequel to last years hit summer blockbuster.", :tags => ["movies", "suspense", "foo.bar", "the-bazzening"])
35
+ Asset.create(:title => "Undercover Foo", :description => "When a foo goes undercover, how far will he go to protect those he loves?", :tags => ["movies", "action", "undercover.foo"])
36
+ Asset.create(:title => "Cheese of the Damned", :description => "This is not your father's munster.", :tags => ["movies", "horror", "cheese", "munster"])
37
+ end
38
+
39
+ context "when arranging a model by nil" do
40
+ subject { Asset.arrange(nil) }
41
+ it { should be_a(Mongoid::Criteria) }
42
+ its(:options) { should be_empty }
43
+ its("options.keys") { should_not include(:sort) }
44
+ end
45
+
46
+ context "when arranging a model by ''" do
47
+ subject { Asset.arrange("") }
48
+ it { should be_a(Mongoid::Criteria) }
49
+ its(:options) { should be_empty }
50
+ its("options.keys") { should_not include(:sort) }
51
+ end
52
+
53
+ context "when arranging a model by a non searchable" do
54
+ subject { Asset.arrange(:is_not_searchable) }
55
+ it { should be_a(Mongoid::Criteria) }
56
+ its(:options) { should be_empty }
57
+ its("options.keys") { should_not include(:sort) }
58
+ end
59
+
60
+ context "when arranging a model by a searchable" do
61
+ subject { Asset.arrange(:title) }
62
+ it { should be_a(Mongoid::Criteria) }
63
+ its(:options) { should_not be_empty }
64
+ its(:options) { should include(:sort => ["arrangeable_values.title", :asc]) }
65
+ end
66
+
67
+ shared_examples_for "arranged assets" do |arrangeable, direction, expected_order|
68
+ context "when arranging a model by '#{arrangeable}' => '#{direction || 'nil'}'" do
69
+ subject { (direction.present? ? Asset.arrange(arrangeable, direction) : Asset.arrange(arrangeable)).map(&:title) }
70
+ it { should == expected_order }
71
+ end
72
+ end
73
+
74
+ it_should_behave_like "arranged assets", :title, nil, ["Cheese of the Damned", "Foo Bar: The Bazzening", "Undercover Foo"]
75
+ it_should_behave_like "arranged assets", :description, nil, ["Foo Bar: The Bazzening", "Cheese of the Damned", "Undercover Foo"]
76
+ it_should_behave_like "arranged assets", :tag, nil, ["Undercover Foo", "Cheese of the Damned", "Foo Bar: The Bazzening"]
77
+
78
+ it_should_behave_like "arranged assets", :title, :asc, ["Cheese of the Damned", "Foo Bar: The Bazzening", "Undercover Foo"]
79
+ it_should_behave_like "arranged assets", :description, :asc, ["Foo Bar: The Bazzening", "Cheese of the Damned", "Undercover Foo"]
80
+ it_should_behave_like "arranged assets", :tag, :asc, ["Undercover Foo", "Cheese of the Damned", "Foo Bar: The Bazzening"]
81
+
82
+ it_should_behave_like "arranged assets", :title, :desc, ["Undercover Foo", "Foo Bar: The Bazzening", "Cheese of the Damned"]
83
+ it_should_behave_like "arranged assets", :description, :desc, ["Undercover Foo", "Cheese of the Damned", "Foo Bar: The Bazzening"]
84
+ it_should_behave_like "arranged assets", :tag, :desc, ["Foo Bar: The Bazzening", "Cheese of the Damned", "Undercover Foo"]
85
+ end
86
+ end
@@ -16,6 +16,18 @@ describe SearchMagic::FullTextSearch do
16
16
  its("searchables.keys") { should include(:part_number, :category_name) }
17
17
  end
18
18
 
19
+ context "when a model excludes an associated documents fields" do
20
+ subject { Game }
21
+ its("searchables.keys") { should include(:title, :price, :high_score, :developer_name) }
22
+ its("searchables.keys") { should_not include(:developer_opened_on) }
23
+ end
24
+
25
+ context "when a model only includes certain fields from an associated document" do
26
+ subject { Player }
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)}
29
+ end
30
+
19
31
  context "when a model embeds one other document" do
20
32
  before(:each) do
21
33
  Person.create(:name => "Joshua", :address => {:street => "123 Example St.", :city => "Nowhereland", :state => "CA", :post_code => 12345}, :phones => [{:country_code => 1, :number => "555-1234"}, {:country_code => 2, :number => "333-7890"}])
@@ -35,7 +35,7 @@ describe SearchMagic::FullTextSearch do
35
35
  it { subject.should_receive :update_searchable_values }
36
36
  end
37
37
 
38
- context "when a model is saved, its :_searchable_values update" do
38
+ context "when a model is saved, its :searchable_values update" do
39
39
  subject { Asset.new(:title => "Foo Bar: The Bazzening", :description => "Sequel to last years hit summer blockbuster.", :tags => ["movies", "foo.bar", "the-bazzening"], :uuid => "ae9d14ee-be93-11df-9fec-78ca39fffe11")}
40
40
  before(:each) { subject.save }
41
41
  its(:searchable_values) { should_not be_empty }
metadata CHANGED
@@ -5,8 +5,8 @@ version: !ruby/object:Gem::Version
5
5
  segments:
6
6
  - 0
7
7
  - 0
8
- - 3
9
- version: 0.0.3
8
+ - 4
9
+ version: 0.0.4
10
10
  platform: ruby
11
11
  authors:
12
12
  - Joshua Bowers
@@ -90,17 +90,22 @@ files:
90
90
  - Rakefile
91
91
  - lib/search_magic.rb
92
92
  - lib/search_magic/full_text_search.rb
93
+ - lib/search_magic/metadata.rb
93
94
  - lib/search_magic/version.rb
94
95
  - search_magic.gemspec
95
96
  - spec/models/address.rb
96
97
  - spec/models/asset.rb
98
+ - spec/models/developer.rb
99
+ - spec/models/game.rb
97
100
  - spec/models/no_searchables.rb
98
101
  - spec/models/part.rb
99
102
  - spec/models/part_category.rb
100
103
  - spec/models/part_number.rb
101
104
  - spec/models/person.rb
102
105
  - spec/models/phone.rb
106
+ - spec/models/player.rb
103
107
  - spec/spec_helper.rb
108
+ - spec/unit/search_magic/arrangements_spec.rb
104
109
  - spec/unit/search_magic/associations_spec.rb
105
110
  - spec/unit/search_magic/fields_spec.rb
106
111
  has_rdoc: true
@@ -138,12 +143,16 @@ summary: SearchMagic provides scoped full text search and sort capabilities to M
138
143
  test_files:
139
144
  - spec/models/address.rb
140
145
  - spec/models/asset.rb
146
+ - spec/models/developer.rb
147
+ - spec/models/game.rb
141
148
  - spec/models/no_searchables.rb
142
149
  - spec/models/part.rb
143
150
  - spec/models/part_category.rb
144
151
  - spec/models/part_number.rb
145
152
  - spec/models/person.rb
146
153
  - spec/models/phone.rb
154
+ - spec/models/player.rb
147
155
  - spec/spec_helper.rb
156
+ - spec/unit/search_magic/arrangements_spec.rb
148
157
  - spec/unit/search_magic/associations_spec.rb
149
158
  - spec/unit/search_magic/fields_spec.rb