search_magic 0.0.3 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
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