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 +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
|