attr_searchable 0.0.4 → 0.0.5
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/README.md +42 -6
- data/attr_searchable.gemspec +2 -2
- data/lib/attr_searchable.rb +8 -0
- data/lib/attr_searchable/version.rb +1 -1
- data/lib/attr_searchable_grammar.rb +1 -4
- data/lib/attr_searchable_grammar/attributes.rb +5 -1
- data/test/attr_searchable_test.rb +27 -1
- data/test/date_test.rb +2 -2
- data/test/datetime_test.rb +2 -2
- data/test/test_helper.rb +2 -1
- metadata +4 -4
data/README.md
CHANGED
@@ -106,9 +106,9 @@ AttrSearchable will use `LIKE '%...%'` queries. Unfortunately, unless you
|
|
106
106
|
create a [trigram index](http://www.postgresql.org/docs/9.1/static/pgtrgm.html)
|
107
107
|
(postgres only), theses queries can not use SQL indices, such that every row
|
108
108
|
needs to be scanned by your RDBMS when you search for `Book.search("Harry
|
109
|
-
Potter")` or similar.
|
110
|
-
capabilities of MySQL and PostgreSQL. To use
|
111
|
-
simply tell AttrSearchable to use them via:
|
109
|
+
Potter")` or similar. To avoid the penalty of `LIKE` queries, AttrSearchable
|
110
|
+
can exploit the fulltext index capabilities of MySQL and PostgreSQL. To use
|
111
|
+
already existing fulltext indices, simply tell AttrSearchable to use them via:
|
112
112
|
|
113
113
|
```ruby
|
114
114
|
class Book < ActiveRecord::Base
|
@@ -142,13 +142,17 @@ to search in, such that AttrSearchable must no longer search within all fields:
|
|
142
142
|
|
143
143
|
```ruby
|
144
144
|
attr_searchable :all => [:author, :title]
|
145
|
+
|
145
146
|
attr_searchable_options :all, :type => :fulltext, :default => true
|
147
|
+
|
148
|
+
# Use :default => true to explicitly enable fields as default fields (whitelist approach)
|
149
|
+
# Use :default => false to explicitly disable fields as default fields (blacklist approach)
|
146
150
|
```
|
147
151
|
|
148
152
|
Now AttrSearchable can optimize the following, not yet optimal query:
|
149
153
|
|
150
154
|
```ruby
|
151
|
-
|
155
|
+
Book.search("Rowling OR Tolkien stock > 1")
|
152
156
|
# MySQL: ... WHERE ((MATCH(books.author) AGAINST('+Rowling' IN BOOLEAN MODE) OR MATCH(books.title) AGAINST('+Rowling' IN BOOLEAN MODE)) OR (MATCH(books.author) AGAINST('+Tolkien' IN BOOLEAN MODE) OR MATCH(books.title) AGAINST('+Tolkien' IN BOOLEAN MODE))) AND books.stock > 1
|
153
157
|
# PostgreSQL: ... WHERE ((to_tsvector('simple', books.author) @@ to_tsquery('simple', 'Rowling') OR to_tsvector('simple', books.title) @@ to_tsquery('simple', 'Rowling')) OR (to_tsvector('simple', books.author) @@ to_tsquery('simple', 'Tolkien') OR to_tsvector('simple', books.title) @@ to_tsquery('simple', 'Tolkien'))) AND books.stock > 1
|
154
158
|
```
|
@@ -156,7 +160,7 @@ BookSearch("Rowling OR Tolkien stock > 1")
|
|
156
160
|
to the following, more performant query:
|
157
161
|
|
158
162
|
```ruby
|
159
|
-
|
163
|
+
Book.search("Rowling OR Tolkien stock > 1")
|
160
164
|
# MySQL: ... WHERE MATCH(books.author, books.title) AGAINST('Rowling Tolkien' IN BOOLEAN MODE) AND books.stock > 1
|
161
165
|
# PostgreSQL: ... WHERE to_tsvector('simple', books.author || ' ' || books.title) @@ to_tsquery('simple', 'Rowling | Tokien') and books.stock > 1
|
162
166
|
```
|
@@ -166,7 +170,7 @@ tries to minimize the fultext constraints within a query, namely `MATCH()
|
|
166
170
|
AGAINST()` for MySQL and `to_tsvector() @@ to_tsquery()` for PostgreSQL.
|
167
171
|
|
168
172
|
```ruby
|
169
|
-
|
173
|
+
Book.search("(Rowling -Potter) OR Tolkien")
|
170
174
|
# MySQL: ... WHERE MATCH(books.author, books.title) AGAINST('(+Rowling -Potter) Tolkien' IN BOOLEAN MODE)
|
171
175
|
# PostgreSQL: ... WHERE to_tsvector('simple', books.author || ' ' || books.title) @@ to_tsquery('simple', '(Rowling & !Potter) | Tolkien')
|
172
176
|
```
|
@@ -412,6 +416,31 @@ returns an empty relation. However, if you need to debug certain cases, use
|
|
412
416
|
Book.unsafe_search("stock: None") # => raise AttrSearchable::IncompatibleDatatype
|
413
417
|
```
|
414
418
|
|
419
|
+
## Reflection
|
420
|
+
|
421
|
+
AttrSearchable provides reflective methods, namely `#searchable_attributes`,
|
422
|
+
`#default_searchable_attributes`, `#searchable_attribute_options` and
|
423
|
+
`#searchable_attribute_aliases`. You can use these methods to e.g. provide an
|
424
|
+
individual search help widget for your models, that lists the attributes to
|
425
|
+
search in as well as the default ones, etc.
|
426
|
+
|
427
|
+
```ruby
|
428
|
+
class Product < ActiveRecord::Base
|
429
|
+
include AttrSearchable
|
430
|
+
|
431
|
+
attr_searchable :title, :description
|
432
|
+
attr_searchable_options :title, :default => true
|
433
|
+
end
|
434
|
+
|
435
|
+
Product.searchable_attributes
|
436
|
+
# {"title" => ["products.title"], "description" => ["products.description"]}
|
437
|
+
|
438
|
+
Product.default_searchable_attributes
|
439
|
+
# {"title" => ["products.title"]}
|
440
|
+
|
441
|
+
# ...
|
442
|
+
```
|
443
|
+
|
415
444
|
## Contributing
|
416
445
|
|
417
446
|
1. Fork it
|
@@ -422,6 +451,13 @@ Book.unsafe_search("stock: None") # => raise AttrSearchable::IncompatibleDatatyp
|
|
422
451
|
|
423
452
|
## Changelog
|
424
453
|
|
454
|
+
Version 0.0.5:
|
455
|
+
|
456
|
+
* Supporting :default => false
|
457
|
+
* Datetime/Date greater operator fix
|
458
|
+
* Use reflection to find associated models
|
459
|
+
* Providing reflection
|
460
|
+
|
425
461
|
Version 0.0.4:
|
426
462
|
|
427
463
|
* Fixed date attributes
|
data/attr_searchable.gemspec
CHANGED
@@ -8,8 +8,8 @@ Gem::Specification.new do |spec|
|
|
8
8
|
spec.version = AttrSearchable::VERSION
|
9
9
|
spec.authors = ["Benjamin Vetter"]
|
10
10
|
spec.email = ["vetter@flakks.com"]
|
11
|
-
spec.description = %q{Search
|
12
|
-
spec.summary = %q{Easily perform complex search
|
11
|
+
spec.description = %q{Search engine like fulltext query support for ActiveRecord}
|
12
|
+
spec.summary = %q{Easily perform complex search engine like fulltext queries on your ActiveRecord models}
|
13
13
|
spec.homepage = "https://github.com/mrkamel/attr_searchable"
|
14
14
|
spec.license = "MIT"
|
15
15
|
|
data/lib/attr_searchable.rb
CHANGED
@@ -73,6 +73,14 @@ module AttrSearchable
|
|
73
73
|
end
|
74
74
|
end
|
75
75
|
|
76
|
+
def default_searchable_attributes
|
77
|
+
keys = searchable_attribute_options.select { |key, value| value[:default] == true }.keys
|
78
|
+
keys = searchable_attributes.keys.reject { |key| searchable_attribute_options[key] && searchable_attribute_options[key][:default] == false } if keys.empty?
|
79
|
+
keys = keys.to_set
|
80
|
+
|
81
|
+
searchable_attributes.select { |key, value| keys.include? key }
|
82
|
+
end
|
83
|
+
|
76
84
|
def search(query)
|
77
85
|
unsafe_search query
|
78
86
|
rescue AttrSearchable::RuntimeError
|
@@ -84,10 +84,7 @@ module AttrSearchableGrammar
|
|
84
84
|
|
85
85
|
class AnywhereExpression < BaseNode
|
86
86
|
def evaluate
|
87
|
-
|
88
|
-
keys = model.searchable_attributes.keys if keys.empty?
|
89
|
-
|
90
|
-
queries = keys.collect { |key| collection_for key }.select { |collection| collection.compatible? text_value }.collect { |collection| collection.matches text_value }
|
87
|
+
queries = model.default_searchable_attributes.keys.collect { |key| collection_for key }.select { |collection| collection.compatible? text_value }.collect { |collection| collection.matches text_value }
|
91
88
|
|
92
89
|
raise AttrSearchable::NoSearchableAttributes if queries.empty?
|
93
90
|
|
@@ -59,7 +59,7 @@ module AttrSearchableGrammar
|
|
59
59
|
klass = model.searchable_attribute_aliases[table]
|
60
60
|
klass ||= table
|
61
61
|
|
62
|
-
klass.classify.constantize
|
62
|
+
model.reflections[klass.to_sym] ? model.reflections[klass.to_sym].klass : klass.classify.constantize
|
63
63
|
end
|
64
64
|
|
65
65
|
def alias_for(table)
|
@@ -182,6 +182,10 @@ module AttrSearchableGrammar
|
|
182
182
|
between(parse(value)).not
|
183
183
|
end
|
184
184
|
|
185
|
+
def gt(value)
|
186
|
+
super parse(value).last
|
187
|
+
end
|
188
|
+
|
185
189
|
def between(range)
|
186
190
|
gteq(range.first).and(lteq(range.last))
|
187
191
|
end
|
@@ -49,7 +49,7 @@ class AttrSearchableTest < AttrSearchable::TestCase
|
|
49
49
|
assert_includes results, product2
|
50
50
|
end
|
51
51
|
|
52
|
-
def
|
52
|
+
def test_custom_default_enabled
|
53
53
|
product1 = create(:product, :title => "Expected")
|
54
54
|
product2 = create(:product, :description => "Expected")
|
55
55
|
product3 = create(:product, :brand => "Expected")
|
@@ -61,10 +61,36 @@ class AttrSearchableTest < AttrSearchable::TestCase
|
|
61
61
|
refute_includes results, product3
|
62
62
|
end
|
63
63
|
|
64
|
+
def test_custom_default_disabled
|
65
|
+
product1 = create(:product, :brand => "Expected")
|
66
|
+
product2 = create(:product, :notice => "Expected")
|
67
|
+
|
68
|
+
results = with_attr_searchable_options(Product, :notice, :default => false) { Product.search "Expected" }
|
69
|
+
|
70
|
+
assert_includes results, product1
|
71
|
+
refute_includes results, product2
|
72
|
+
end
|
73
|
+
|
64
74
|
def test_count
|
65
75
|
create_list :product, 2, :title => "Expected"
|
66
76
|
|
67
77
|
assert_equal 2, Product.search("Expected").count
|
68
78
|
end
|
79
|
+
|
80
|
+
def test_default_searchable_attributes_true
|
81
|
+
with_attr_searchable_options(Product, :title, :default => true) do
|
82
|
+
with_attr_searchable_options(Product, :description, :default => true) do
|
83
|
+
assert_equal ["title", "description"], Product.default_searchable_attributes.keys
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def test_default_searchable_attributes_false
|
89
|
+
with_attr_searchable_options(Product, :title, :default => false) do
|
90
|
+
with_attr_searchable_options(Product, :description, :default => false) do
|
91
|
+
assert_equal Product.searchable_attributes.keys - ["title", "description"], Product.default_searchable_attributes.keys
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
69
95
|
end
|
70
96
|
|
data/test/date_test.rb
CHANGED
@@ -41,8 +41,8 @@ class DateTest < AttrSearchable::TestCase
|
|
41
41
|
def test_greater
|
42
42
|
product = create(:product, :created_on => Date.parse("2014-05-01"))
|
43
43
|
|
44
|
-
assert_includes Product.search("created_on
|
45
|
-
refute_includes Product.search("created_on
|
44
|
+
assert_includes Product.search("created_on > 2014-04-01"), product
|
45
|
+
refute_includes Product.search("created_on > 2014-05-01"), product
|
46
46
|
end
|
47
47
|
|
48
48
|
def test_greater_equals
|
data/test/datetime_test.rb
CHANGED
@@ -42,8 +42,8 @@ class DatetimeTest < AttrSearchable::TestCase
|
|
42
42
|
def test_greater
|
43
43
|
product = create(:product, :created_at => Time.parse("2014-05-01"))
|
44
44
|
|
45
|
-
assert_includes Product.search("created_at
|
46
|
-
refute_includes Product.search("created_at
|
45
|
+
assert_includes Product.search("created_at > 2014-04-01"), product
|
46
|
+
refute_includes Product.search("created_at > 2014-05-01"), product
|
47
47
|
end
|
48
48
|
|
49
49
|
def test_greater_equals
|
data/test/test_helper.rb
CHANGED
@@ -34,7 +34,7 @@ end
|
|
34
34
|
class Product < ActiveRecord::Base
|
35
35
|
include AttrSearchable
|
36
36
|
|
37
|
-
attr_searchable :title, :description, :brand, :stock, :price, :created_at, :created_on, :available
|
37
|
+
attr_searchable :title, :description, :brand, :notice, :stock, :price, :created_at, :created_on, :available
|
38
38
|
attr_searchable :comment => ["comments.title", "comments.message"], :user => ["users.username", "users_products.username"]
|
39
39
|
attr_searchable :primary => [:title, :description]
|
40
40
|
|
@@ -81,6 +81,7 @@ ActiveRecord::Base.connection.create_table :products do |t|
|
|
81
81
|
t.date :created_on
|
82
82
|
t.boolean :available
|
83
83
|
t.string :brand
|
84
|
+
t.string :notice
|
84
85
|
end
|
85
86
|
|
86
87
|
ActiveRecord::Base.connection.create_table :comments do |t|
|
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.5
|
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-07-
|
12
|
+
date: 2014-07-17 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: treetop
|
@@ -123,7 +123,7 @@ dependencies:
|
|
123
123
|
- - ! '>='
|
124
124
|
- !ruby/object:Gem::Version
|
125
125
|
version: '0'
|
126
|
-
description: Search
|
126
|
+
description: Search engine like fulltext query support for ActiveRecord
|
127
127
|
email:
|
128
128
|
- vetter@flakks.com
|
129
129
|
executables: []
|
@@ -189,7 +189,7 @@ rubyforge_project:
|
|
189
189
|
rubygems_version: 1.8.23
|
190
190
|
signing_key:
|
191
191
|
specification_version: 3
|
192
|
-
summary: Easily perform complex search
|
192
|
+
summary: Easily perform complex search engine like fulltext queries on your ActiveRecord
|
193
193
|
models
|
194
194
|
test_files:
|
195
195
|
- test/and_test.rb
|