attr_searchable 0.0.3 → 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +68 -0
- data/lib/attr_searchable.rb +20 -9
- data/lib/attr_searchable/version.rb +1 -1
- data/lib/attr_searchable_grammar.rb +2 -2
- data/lib/attr_searchable_grammar/attributes.rb +31 -8
- data/test/date_test.rb +75 -0
- data/test/test_helper.rb +8 -3
- metadata +4 -2
data/README.md
CHANGED
@@ -5,6 +5,8 @@
|
|
5
5
|
[![Dependency Status](https://gemnasium.com/mrkamel/attr_searchable.png?travis)](https://gemnasium.com/mrkamel/attr_searchable)
|
6
6
|
[![Gem Version](https://badge.fury.io/rb/attr_searchable.svg)](http://badge.fury.io/rb/attr_searchable)
|
7
7
|
|
8
|
+
![attr_searchable](https://raw.githubusercontent.com/mrkamel/attr_searchable_logo/master/attr_searchable.png)
|
9
|
+
|
8
10
|
AttrSearchable extends your ActiveRecord models to support fulltext search
|
9
11
|
engine like queries via simple query strings and hash-based queries. Assume you
|
10
12
|
have a `Book` model having various attributes like `title`, `author`, `stock`,
|
@@ -218,6 +220,28 @@ from from the usual non-fulltext indices. Thus, you should add a usual index on
|
|
218
220
|
every column you expose to search queries plus a fulltext index for every
|
219
221
|
fulltext attribute.
|
220
222
|
|
223
|
+
In case you can't use fulltext indices, because you're e.g. still on MySQL 5.5
|
224
|
+
while using InnoDB or another RDBMS without fulltext support, you can make your
|
225
|
+
RDBMS use usual non-fulltext indices for string columns if you don't need the
|
226
|
+
left wildcard within `LIKE` queries. Simply supply the following option:
|
227
|
+
|
228
|
+
```ruby
|
229
|
+
class User < ActiveRecord::Base
|
230
|
+
include AttrSearchable
|
231
|
+
|
232
|
+
attr_searchable :username
|
233
|
+
attr_searchable_options :username, :left_wildcard => false
|
234
|
+
|
235
|
+
# ...
|
236
|
+
```
|
237
|
+
|
238
|
+
such that AttrSearchable will omit the left most wildcard.
|
239
|
+
|
240
|
+
```ruby
|
241
|
+
User.search("admin")
|
242
|
+
# ... WHERE users.username LIKE 'admin%'
|
243
|
+
```
|
244
|
+
|
221
245
|
## Associations
|
222
246
|
|
223
247
|
If you specify searchable attributes from another model, like
|
@@ -267,6 +291,44 @@ end
|
|
267
291
|
AttrSearchable will then skip any association auto loading and will use
|
268
292
|
the `search_scope` instead.
|
269
293
|
|
294
|
+
## Custom table names and associations
|
295
|
+
|
296
|
+
AttrSearchable tries to infer a model's class name and SQL alias from the
|
297
|
+
specified attributes to autodetect datatype definitions, etc. This usually
|
298
|
+
works quite fine. In case you're using custom table names via `self.table_name
|
299
|
+
= ...` or if a model is associated multiple times, AttrSearchable however can't
|
300
|
+
infer the class and alias names, e.g.
|
301
|
+
|
302
|
+
```ruby
|
303
|
+
class Book < ActiveRecord::Base
|
304
|
+
# ...
|
305
|
+
|
306
|
+
has_many :users, :through => :comments
|
307
|
+
belongs_to :user
|
308
|
+
|
309
|
+
attr_searchable :user => ["users.username", "users_books.username"]
|
310
|
+
|
311
|
+
# ...
|
312
|
+
end
|
313
|
+
```
|
314
|
+
|
315
|
+
Here, for queries to work you have to use `users_books.username`, because
|
316
|
+
ActiveRecord assigns a different SQL alias for users within its SQL queries,
|
317
|
+
because the user model is associated multiple times. However, as AttrSearchable
|
318
|
+
now can't infer the `User` model from `users_books`, you have to add:
|
319
|
+
|
320
|
+
```ruby
|
321
|
+
class Book < ActiveRecord::Base
|
322
|
+
# ...
|
323
|
+
|
324
|
+
attr_searchable_alias :users_books => :user
|
325
|
+
|
326
|
+
# ...
|
327
|
+
end
|
328
|
+
```
|
329
|
+
|
330
|
+
to tell AttrSearchable about the custom SQL alias and mapping.
|
331
|
+
|
270
332
|
## Supported operators
|
271
333
|
|
272
334
|
Query string queries support `AND/and`, `OR/or`, `:`, `=`, `!=`, `<`, `<=`,
|
@@ -360,6 +422,12 @@ Book.unsafe_search("stock: None") # => raise AttrSearchable::IncompatibleDatatyp
|
|
360
422
|
|
361
423
|
## Changelog
|
362
424
|
|
425
|
+
Version 0.0.4:
|
426
|
+
|
427
|
+
* Fixed date attributes
|
428
|
+
* Fail softly for mixed datatype attributes
|
429
|
+
* Support custom table, class and alias names via attr_searchable_alias
|
430
|
+
|
363
431
|
Version 0.0.3:
|
364
432
|
|
365
433
|
* belongs_to association fixes
|
data/lib/attr_searchable.rb
CHANGED
@@ -18,8 +18,8 @@ module AttrSearchable
|
|
18
18
|
class ParseError < RuntimeError; end
|
19
19
|
|
20
20
|
module Parser
|
21
|
-
def self.parse(
|
22
|
-
|
21
|
+
def self.parse(query, model)
|
22
|
+
query.is_a?(Hash) ? parse_hash(query, model) : parse_string(query, model)
|
23
23
|
end
|
24
24
|
|
25
25
|
def self.parse_hash(hash, model)
|
@@ -40,6 +40,9 @@ module AttrSearchable
|
|
40
40
|
base.class_attribute :searchable_attribute_options
|
41
41
|
base.searchable_attribute_options = {}
|
42
42
|
|
43
|
+
base.class_attribute :searchable_attribute_aliases
|
44
|
+
base.searchable_attribute_aliases = {}
|
45
|
+
|
43
46
|
base.extend ClassMethods
|
44
47
|
end
|
45
48
|
|
@@ -61,22 +64,30 @@ module AttrSearchable
|
|
61
64
|
end
|
62
65
|
|
63
66
|
def attr_searchable_options(key, options = {})
|
64
|
-
self.searchable_attribute_options[key.to_s] = (
|
67
|
+
self.searchable_attribute_options[key.to_s] = (searchable_attribute_options[key.to_s] || {}).merge(options)
|
68
|
+
end
|
69
|
+
|
70
|
+
def attr_searchable_alias(hash)
|
71
|
+
hash.each do |key, value|
|
72
|
+
self.searchable_attribute_aliases[key.to_s] = value.respond_to?(:table_name) ? value.table_name : value.to_s
|
73
|
+
end
|
65
74
|
end
|
66
75
|
|
67
|
-
def search(
|
68
|
-
unsafe_search
|
76
|
+
def search(query)
|
77
|
+
unsafe_search query
|
69
78
|
rescue AttrSearchable::RuntimeError
|
70
79
|
respond_to?(:none) ? none : where("1 = 0")
|
71
80
|
end
|
72
81
|
|
73
|
-
def unsafe_search(
|
74
|
-
return respond_to?(:scoped) ? scoped : all if
|
82
|
+
def unsafe_search(query)
|
83
|
+
return respond_to?(:scoped) ? scoped : all if query.blank?
|
84
|
+
|
85
|
+
associations = searchable_attributes.values.flatten.uniq.collect { |column| column.split(".").first }.collect { |column| searchable_attribute_aliases[column] || column.to_sym }
|
75
86
|
|
76
87
|
scope = respond_to?(:search_scope) ? search_scope : nil
|
77
|
-
scope ||= eager_load(
|
88
|
+
scope ||= eager_load(associations - [name.tableize.to_sym])
|
78
89
|
|
79
|
-
scope.where AttrSearchable::Parser.parse(
|
90
|
+
scope.where AttrSearchable::Parser.parse(query, self).optimize!.to_sql(self)
|
80
91
|
end
|
81
92
|
end
|
82
93
|
end
|
@@ -87,9 +87,9 @@ module AttrSearchableGrammar
|
|
87
87
|
keys = model.searchable_attribute_options.select { |key, value| value[:default] == true }.keys
|
88
88
|
keys = model.searchable_attributes.keys if keys.empty?
|
89
89
|
|
90
|
-
queries = keys.collect { |key| collection_for key }.select { |
|
90
|
+
queries = keys.collect { |key| collection_for key }.select { |collection| collection.compatible? text_value }.collect { |collection| collection.matches text_value }
|
91
91
|
|
92
|
-
raise AttrSearchable::NoSearchableAttributes
|
92
|
+
raise AttrSearchable::NoSearchableAttributes if queries.empty?
|
93
93
|
|
94
94
|
queries.flatten.inject(:or)
|
95
95
|
end
|
@@ -55,13 +55,24 @@ module AttrSearchableGrammar
|
|
55
55
|
@attributes ||= model.searchable_attributes[key].collect { |attribute_definition| attribute_for attribute_definition }
|
56
56
|
end
|
57
57
|
|
58
|
+
def klass_for(table)
|
59
|
+
klass = model.searchable_attribute_aliases[table]
|
60
|
+
klass ||= table
|
61
|
+
|
62
|
+
klass.classify.constantize
|
63
|
+
end
|
64
|
+
|
65
|
+
def alias_for(table)
|
66
|
+
(model.searchable_attribute_aliases[table] && table) || klass_for(table).table_name
|
67
|
+
end
|
68
|
+
|
58
69
|
def attribute_for(attribute_definition)
|
59
70
|
table, column = attribute_definition.split(".")
|
60
|
-
klass = table
|
71
|
+
klass = klass_for(table)
|
61
72
|
|
62
73
|
raise(AttrSearchable::UnknownAttribute, "Unknown attribute #{attribute_definition}") unless klass.columns_hash[column]
|
63
74
|
|
64
|
-
Attributes.const_get(klass.columns_hash[column].type.to_s.classify).new(klass.arel_table.alias(
|
75
|
+
Attributes.const_get(klass.columns_hash[column].type.to_s.classify).new(klass.arel_table.alias(alias_for(table))[column], klass, options)
|
65
76
|
end
|
66
77
|
end
|
67
78
|
|
@@ -148,16 +159,16 @@ module AttrSearchableGrammar
|
|
148
159
|
::Time.new($1, $3, 15).beginning_of_month .. ::Time.new($1, $3, 15).end_of_month
|
149
160
|
elsif value =~ /^([0-9]{1,2})(\.|-|\/)([0-9]{4,})$/
|
150
161
|
::Time.new($3, $1, 15).beginning_of_month .. ::Time.new($3, $1, 15).end_of_month
|
151
|
-
elsif value !~ /:/
|
162
|
+
elsif value !~ /:/
|
152
163
|
time = ::Time.parse(value)
|
153
164
|
time.beginning_of_day .. time.end_of_day
|
154
165
|
else
|
155
166
|
time = ::Time.parse(value)
|
156
167
|
time .. time
|
157
|
-
end
|
168
|
+
end
|
158
169
|
rescue ArgumentError
|
159
170
|
raise AttrSearchable::IncompatibleDatatype, "Incompatible datatype for #{value}"
|
160
|
-
end
|
171
|
+
end
|
161
172
|
|
162
173
|
def map(value)
|
163
174
|
parse(value).first
|
@@ -180,8 +191,20 @@ module AttrSearchableGrammar
|
|
180
191
|
|
181
192
|
class Date < Datetime
|
182
193
|
def parse(value)
|
183
|
-
|
184
|
-
|
194
|
+
return value .. value unless value.is_a?(::String)
|
195
|
+
|
196
|
+
if value =~ /^[0-9]{4,}$/
|
197
|
+
::Date.new(value.to_i).beginning_of_year .. ::Date.new(value.to_i).end_of_year
|
198
|
+
elsif value =~ /^([0-9]{4,})(\.|-|\/)([0-9]{1,2})$/
|
199
|
+
::Date.new($1.to_i, $3.to_i, 15).beginning_of_month .. ::Date.new($1.to_i, $3.to_i, 15).end_of_month
|
200
|
+
elsif value =~ /^([0-9]{1,2})(\.|-|\/)([0-9]{4,})$/
|
201
|
+
::Date.new($3.to_i, $1.to_i, 15).beginning_of_month .. ::Date.new($3.to_i, $1.to_i, 15).end_of_month
|
202
|
+
else
|
203
|
+
date = ::Date.parse(value)
|
204
|
+
date .. date
|
205
|
+
end
|
206
|
+
rescue ArgumentError
|
207
|
+
raise AttrSearchable::IncompatibleDatatype, "Incompatible datatype for #{value}"
|
185
208
|
end
|
186
209
|
end
|
187
210
|
|
@@ -193,7 +216,7 @@ module AttrSearchableGrammar
|
|
193
216
|
return false if value.to_s =~ /^(0|false|no)$/i
|
194
217
|
|
195
218
|
raise AttrSearchable::IncompatibleDatatype, "Incompatible datatype for #{value}"
|
196
|
-
end
|
219
|
+
end
|
197
220
|
end
|
198
221
|
end
|
199
222
|
end
|
data/test/date_test.rb
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
|
2
|
+
require File.expand_path("../test_helper", __FILE__)
|
3
|
+
|
4
|
+
class DateTest < AttrSearchable::TestCase
|
5
|
+
def test_mapping
|
6
|
+
product = create(:product, :created_on => Date.parse("2014-05-01"))
|
7
|
+
|
8
|
+
assert_includes Product.search("created_on: 2014"), product
|
9
|
+
assert_includes Product.search("created_on: 2014-05"), product
|
10
|
+
assert_includes Product.search("created_on: 2014-05-01"), product
|
11
|
+
end
|
12
|
+
|
13
|
+
def test_anywhere
|
14
|
+
product = create(:product, :created_on => Date.parse("2014-05-01"))
|
15
|
+
|
16
|
+
assert_includes Product.search("2014-05-01"), product
|
17
|
+
refute_includes Product.search("2014-05-02"), product
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_includes
|
21
|
+
product = create(:product, :created_on => Date.parse("2014-05-01"))
|
22
|
+
|
23
|
+
assert_includes Product.search("created_on: 2014-05-01"), product
|
24
|
+
refute_includes Product.search("created_on: 2014-05-02"), product
|
25
|
+
end
|
26
|
+
|
27
|
+
def test_equals
|
28
|
+
product = create(:product, :created_on => Date.parse("2014-05-01"))
|
29
|
+
|
30
|
+
assert_includes Product.search("created_on = 2014-05-01"), product
|
31
|
+
refute_includes Product.search("created_on = 2014-05-02"), product
|
32
|
+
end
|
33
|
+
|
34
|
+
def test_equals_not
|
35
|
+
product = create(:product, :created_on => Date.parse("2014-05-01"))
|
36
|
+
|
37
|
+
assert_includes Product.search("created_on != 2014-05-02"), product
|
38
|
+
refute_includes Product.search("created_on != 2014-05-01"), product
|
39
|
+
end
|
40
|
+
|
41
|
+
def test_greater
|
42
|
+
product = create(:product, :created_on => Date.parse("2014-05-01"))
|
43
|
+
|
44
|
+
assert_includes Product.search("created_on < 2014-05-02"), product
|
45
|
+
refute_includes Product.search("created_on < 2014-05-01"), product
|
46
|
+
end
|
47
|
+
|
48
|
+
def test_greater_equals
|
49
|
+
product = create(:product, :created_on => Date.parse("2014-05-01"))
|
50
|
+
|
51
|
+
assert_includes Product.search("created_on >= 2014-05-01"), product
|
52
|
+
refute_includes Product.search("created_on >= 2014-05-02"), product
|
53
|
+
end
|
54
|
+
|
55
|
+
def test_less
|
56
|
+
product = create(:product, :created_on => Date.parse("2014-05-01"))
|
57
|
+
|
58
|
+
assert_includes Product.search("created_on < 2014-05-02"), product
|
59
|
+
refute_includes Product.search("created_on < 2014-05-01"), product
|
60
|
+
end
|
61
|
+
|
62
|
+
def test_less_equals
|
63
|
+
product = create(:product, :created_on => Date.parse("2014-05-02"))
|
64
|
+
|
65
|
+
assert_includes Product.search("created_on <= 2014-05-02"), product
|
66
|
+
refute_includes Product.search("created_on <= 2014-05-01"), product
|
67
|
+
end
|
68
|
+
|
69
|
+
def test_incompatible_datatype
|
70
|
+
assert_raises AttrSearchable::IncompatibleDatatype do
|
71
|
+
Product.unsafe_search "created_on: Value"
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
data/test/test_helper.rb
CHANGED
@@ -34,11 +34,12 @@ end
|
|
34
34
|
class Product < ActiveRecord::Base
|
35
35
|
include AttrSearchable
|
36
36
|
|
37
|
-
attr_searchable :title, :description, :brand, :stock, :price, :created_at, :available
|
38
|
-
attr_searchable :comment => ["comments.title", "comments.message"], :user => "users.username"
|
39
|
-
|
37
|
+
attr_searchable :title, :description, :brand, :stock, :price, :created_at, :created_on, :available
|
38
|
+
attr_searchable :comment => ["comments.title", "comments.message"], :user => ["users.username", "users_products.username"]
|
40
39
|
attr_searchable :primary => [:title, :description]
|
41
40
|
|
41
|
+
attr_searchable_alias :users_products => :user
|
42
|
+
|
42
43
|
if DATABASE != "sqlite"
|
43
44
|
attr_searchable_options :title, :type => :fulltext
|
44
45
|
attr_searchable_options :description, :type => :fulltext
|
@@ -51,6 +52,8 @@ class Product < ActiveRecord::Base
|
|
51
52
|
|
52
53
|
has_many :comments
|
53
54
|
has_many :users, :through => :comments
|
55
|
+
|
56
|
+
belongs_to :user
|
54
57
|
end
|
55
58
|
|
56
59
|
FactoryGirl.define do
|
@@ -69,11 +72,13 @@ ActiveRecord::Base.connection.execute "DROP TABLE IF EXISTS comments"
|
|
69
72
|
ActiveRecord::Base.connection.execute "DROP TABLE IF EXISTS users"
|
70
73
|
|
71
74
|
ActiveRecord::Base.connection.create_table :products do |t|
|
75
|
+
t.references :user
|
72
76
|
t.string :title
|
73
77
|
t.text :description
|
74
78
|
t.integer :stock
|
75
79
|
t.float :price
|
76
80
|
t.datetime :created_at
|
81
|
+
t.date :created_on
|
77
82
|
t.boolean :available
|
78
83
|
t.string :brand
|
79
84
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: attr_searchable
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.4
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2014-
|
12
|
+
date: 2014-07-05 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: treetop
|
@@ -154,6 +154,7 @@ files:
|
|
154
154
|
- test/attr_searchable_test.rb
|
155
155
|
- test/boolean_test.rb
|
156
156
|
- test/database.yml
|
157
|
+
- test/date_test.rb
|
157
158
|
- test/datetime_test.rb
|
158
159
|
- test/error_test.rb
|
159
160
|
- test/float_test.rb
|
@@ -195,6 +196,7 @@ test_files:
|
|
195
196
|
- test/attr_searchable_test.rb
|
196
197
|
- test/boolean_test.rb
|
197
198
|
- test/database.yml
|
199
|
+
- test/date_test.rb
|
198
200
|
- test/datetime_test.rb
|
199
201
|
- test/error_test.rb
|
200
202
|
- test/float_test.rb
|