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 +1 -1
- data/README.textile +17 -1
- data/lib/search_magic/full_text_search.rb +14 -31
- data/lib/search_magic/metadata.rb +31 -0
- data/lib/search_magic/version.rb +1 -1
- data/lib/search_magic.rb +1 -0
- data/spec/models/developer.rb +10 -0
- data/spec/models/game.rb +15 -0
- data/spec/models/player.rb +9 -0
- data/spec/unit/search_magic/arrangements_spec.rb +86 -0
- data/spec/unit/search_magic/associations_spec.rb +12 -0
- data/spec/unit/search_magic/fields_spec.rb +1 -1
- metadata +11 -2
data/Gemfile.lock
CHANGED
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
|
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
|
43
|
-
associated.
|
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
|
66
|
-
|
67
|
-
|
68
|
-
|
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
|
data/lib/search_magic/version.rb
CHANGED
data/lib/search_magic.rb
CHANGED
data/spec/models/game.rb
ADDED
@@ -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,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 :
|
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
|
-
-
|
9
|
-
version: 0.0.
|
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
|