attr_searchable 0.0.1 → 0.0.2
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/.travis.yml +0 -1
- data/README.md +16 -5
- data/attr_searchable.gemspec +2 -2
- data/lib/attr_searchable.rb +17 -10
- data/lib/attr_searchable/arel/visitors.rb +68 -4
- data/lib/attr_searchable/hash_parser.rb +1 -1
- data/lib/attr_searchable/version.rb +1 -1
- data/lib/attr_searchable_grammar.rb +24 -24
- data/lib/attr_searchable_grammar/attributes.rb +13 -9
- data/lib/attr_searchable_grammar/nodes.rb +34 -14
- data/test/and_test.rb +4 -4
- data/test/attr_searchable_test.rb +15 -9
- data/test/boolean_test.rb +12 -6
- data/test/datetime_test.rb +15 -9
- data/test/error_test.rb +17 -0
- data/test/float_test.rb +14 -8
- data/test/fulltext_test.rb +5 -5
- data/test/hash_test.rb +97 -0
- data/test/integer_test.rb +14 -8
- data/test/not_test.rb +4 -4
- data/test/or_test.rb +6 -6
- data/test/string_test.rb +12 -12
- data/test/test_helper.rb +2 -0
- metadata +9 -6
- data/test/sub_query_test.rb +0 -17
data/.travis.yml
CHANGED
data/README.md
CHANGED
@@ -3,6 +3,7 @@
|
|
3
3
|
[](http://travis-ci.org/mrkamel/attr_searchable)
|
4
4
|
[](https://codeclimate.com/github/mrkamel/attr_searchable)
|
5
5
|
[](https://gemnasium.com/mrkamel/attr_searchable)
|
6
|
+
[](http://badge.fury.io/rb/attr_searchable)
|
6
7
|
|
7
8
|
AttrSearchable extends your ActiveRecord models to support fulltext search
|
8
9
|
engine like queries via simple query strings and hash-based queries. Assume you
|
@@ -35,7 +36,7 @@ Book.search(:or => [{:query => "Rowling -Potter"}, {:query => "Tolkien -Rings"}]
|
|
35
36
|
|
36
37
|
## Installation
|
37
38
|
|
38
|
-
|
39
|
+
For Rails/ActiveRecord 3 (or 4), add this line to your application's Gemfile:
|
39
40
|
|
40
41
|
gem 'attr_searchable'
|
41
42
|
|
@@ -142,8 +143,8 @@ attributes having fulltext indices to:
|
|
142
143
|
|
143
144
|
```ruby
|
144
145
|
Book.search("Harry Potter")
|
145
|
-
# MySQL: ... WHERE MATCH(books.title) AGAINST('+Harry
|
146
|
-
# PostgreSQL: ... WHERE to_tsvector('simple', books.title) @@ to_tsquery('simple', 'Harry
|
146
|
+
# MySQL: ... WHERE (MATCH(books.title) AGAINST('+Harry' IN BOOLEAN MODE) OR MATCH(books.author) AGAINST('+Harry' IN BOOLEAN MODE)) AND (MATCH(books.title) AGAINST ('+Potter' IN BOOLEAN MODE) OR MATCH(books.author) AGAINST('+Potter' IN BOOLEAN MODE))
|
147
|
+
# PostgreSQL: ... WHERE (to_tsvector('simple', books.title) @@ to_tsquery('simple', 'Harry') OR to_tsvector('simple', books.author) @@ to_tsquery('simple', 'Harry')) AND (to_tsvector('simple', books.title) @@ to_tsquery('simple', 'Potter') OR to_tsvector('simple', books.author) @@ to_tsquery('simple', 'Potter'))
|
147
148
|
```
|
148
149
|
|
149
150
|
Obviously, theses queries won't always return the same results as wildcard
|
@@ -165,8 +166,8 @@ Now AttrSearchable can optimize the following, not yet optimal query:
|
|
165
166
|
|
166
167
|
```ruby
|
167
168
|
BookSearch("Rowling OR Tolkien stock > 1")
|
168
|
-
# MySQL: ... WHERE (MATCH(books.author) AGAINST('+Rowling' IN BOOLEAN MODE) OR MATCH(books.title) AGAINST('+Tolkien' IN BOOLEAN MODE)) AND books.stock > 1
|
169
|
-
# PostgreSQL: ... WHERE (to_tsvector('simple', books.author) @@ to_tsquery('simple', 'Rowling') OR to_tsvector('simple', books.title) @@ to_tsquery('simple', 'Tolkien')) AND books.stock > 1
|
169
|
+
# 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
|
170
|
+
# 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
|
170
171
|
```
|
171
172
|
|
172
173
|
to the following, more performant query:
|
@@ -330,6 +331,16 @@ instead of
|
|
330
331
|
|
331
332
|
Thus, if you use fulltext indices, you better avoid chaining.
|
332
333
|
|
334
|
+
## Debugging
|
335
|
+
|
336
|
+
AttrSearchable conveniently hides certain errors, like parse errors, and
|
337
|
+
instead returns an empty relation. However, if you need to debug certain
|
338
|
+
cases, use `Model#unsafe_search`, which will raise them.
|
339
|
+
|
340
|
+
```ruby
|
341
|
+
Book.unsafe_search("stock: None") # => raise AttrSearchable::IncompatibleDatatype
|
342
|
+
```
|
343
|
+
|
333
344
|
## Contributing
|
334
345
|
|
335
346
|
1. Fork it
|
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{
|
12
|
-
spec.summary = %q{Easily perform complex search-engine like queries on your
|
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
@@ -8,11 +8,14 @@ require "treetop"
|
|
8
8
|
Treetop.load File.expand_path("../attr_searchable_grammar.treetop", __FILE__)
|
9
9
|
|
10
10
|
module AttrSearchable
|
11
|
-
class
|
12
|
-
class
|
13
|
-
|
14
|
-
class
|
15
|
-
class
|
11
|
+
class SpecificationError < StandardError; end
|
12
|
+
class UnknownAttribute < SpecificationError; end
|
13
|
+
|
14
|
+
class RuntimeError < StandardError; end
|
15
|
+
class UnknownColumn < RuntimeError; end
|
16
|
+
class NoSearchableAttributes < RuntimeError; end
|
17
|
+
class IncompatibleDatatype < RuntimeError; end
|
18
|
+
class ParseError < RuntimeError; end
|
16
19
|
|
17
20
|
module Parser
|
18
21
|
def self.parse(arg, model)
|
@@ -26,7 +29,7 @@ module AttrSearchable
|
|
26
29
|
def self.parse_string(string, model)
|
27
30
|
node = AttrSearchableGrammarParser.new.parse(string) || raise(ParseError)
|
28
31
|
node.model = model
|
29
|
-
node.
|
32
|
+
node.evaluate
|
30
33
|
end
|
31
34
|
end
|
32
35
|
|
@@ -61,15 +64,19 @@ module AttrSearchable
|
|
61
64
|
self.searchable_attribute_options[key.to_s] = (self.searchable_attribute_options[key.to_s] || {}).merge(options)
|
62
65
|
end
|
63
66
|
|
64
|
-
def search(
|
67
|
+
def search(*args)
|
68
|
+
unsafe_search *args
|
69
|
+
rescue AttrSearchable::RuntimeError
|
70
|
+
respond_to?(:none) ? none : where("1 = 0")
|
71
|
+
end
|
72
|
+
|
73
|
+
def unsafe_search(arg)
|
65
74
|
return respond_to?(:scoped) ? scoped : all if arg.blank?
|
66
75
|
|
67
76
|
scope = respond_to?(:search_scope) ? search_scope : nil
|
68
77
|
scope ||= eager_load(searchable_attributes.values.flatten.uniq.collect { |column| column.split(".").first.to_sym } - [name.tableize.to_sym])
|
69
78
|
|
70
|
-
scope.where AttrSearchable::Parser.parse(arg, self).optimize
|
71
|
-
rescue AttrSearchable::Error
|
72
|
-
respond_to?(:none) ? none : where("1 = 0")
|
79
|
+
scope.where AttrSearchable::Parser.parse(arg, self).optimize!.to_sql(self)
|
73
80
|
end
|
74
81
|
end
|
75
82
|
end
|
@@ -5,19 +5,83 @@ module AttrSearchable
|
|
5
5
|
module ToSql
|
6
6
|
if ::Arel::VERSION >= "4.0.1"
|
7
7
|
def visit_AttrSearchableGrammar_Nodes_And(o, a)
|
8
|
-
visit ::Arel::Nodes::Grouping.new(o.
|
8
|
+
visit ::Arel::Nodes::Grouping.new(o.nodes.inject { |res, cur| ::Arel::Nodes::And.new [res, cur] }), a
|
9
9
|
end
|
10
10
|
|
11
11
|
def visit_AttrSearchableGrammar_Nodes_Or(o, a)
|
12
|
-
visit ::Arel::Nodes::Grouping.new(o.
|
12
|
+
visit ::Arel::Nodes::Grouping.new(o.nodes.inject { |res, cur| ::Arel::Nodes::Or.new res, cur }), a
|
13
|
+
end
|
14
|
+
|
15
|
+
def visit_AttrSearchableGrammar_Nodes_Equality(o, a)
|
16
|
+
visit ::Arel::Nodes::Equality.new(o.left, o.right), a
|
17
|
+
end
|
18
|
+
|
19
|
+
def visit_AttrSearchableGrammar_Nodes_NotEqual(o, a)
|
20
|
+
visit ::Arel::Nodes::NotEqual.new(o.left, o.right), a
|
21
|
+
end
|
22
|
+
|
23
|
+
def visit_AttrSearchableGrammar_Nodes_LessThan(o, a)
|
24
|
+
visit ::Arel::Nodes::LessThan.new(o.left, o.right), a
|
25
|
+
end
|
26
|
+
|
27
|
+
def visit_AttrSearchableGrammar_Nodes_LessThanOrEqual(o, a)
|
28
|
+
visit ::Arel::Nodes::LessThanOrEqual.new(o.left, o.right), a
|
29
|
+
end
|
30
|
+
|
31
|
+
def visit_AttrSearchableGrammar_Nodes_GreaterThan(o, a)
|
32
|
+
visit ::Arel::Nodes::GreaterThan.new(o.left, o.right), a
|
33
|
+
end
|
34
|
+
|
35
|
+
def visit_AttrSearchableGrammar_Nodes_GreaterThanOrEqual(o, a)
|
36
|
+
visit ::Arel::Nodes::GreaterThanOrEqual.new(o.left, o.right), a
|
37
|
+
end
|
38
|
+
|
39
|
+
def visit_AttrSearchableGrammar_Nodes_Not(o, a)
|
40
|
+
visit ::Arel::Nodes::Not.new(o.object), a
|
41
|
+
end
|
42
|
+
|
43
|
+
def visit_AttrSearchableGrammar_Nodes_Matches(o, a)
|
44
|
+
visit ::Arel::Nodes::Matches.new(o.left, o.right), a
|
13
45
|
end
|
14
46
|
else
|
15
47
|
def visit_AttrSearchableGrammar_Nodes_And(o)
|
16
|
-
visit ::Arel::Nodes::Grouping.new(o.
|
48
|
+
visit ::Arel::Nodes::Grouping.new(o.nodes.inject { |res, cur| ::Arel::Nodes::And.new [res, cur] })
|
17
49
|
end
|
18
50
|
|
19
51
|
def visit_AttrSearchableGrammar_Nodes_Or(o)
|
20
|
-
visit ::Arel::Nodes::Grouping.new(o.
|
52
|
+
visit ::Arel::Nodes::Grouping.new(o.nodes.inject { |res, cur| ::Arel::Nodes::Or.new res, cur })
|
53
|
+
end
|
54
|
+
|
55
|
+
def visit_AttrSearchableGrammar_Nodes_Equality(o)
|
56
|
+
visit ::Arel::Nodes::Equality.new(o.left, o.right)
|
57
|
+
end
|
58
|
+
|
59
|
+
def visit_AttrSearchableGrammar_Nodes_NotEqual(o)
|
60
|
+
visit ::Arel::Nodes::NotEqual.new(o.left, o.right)
|
61
|
+
end
|
62
|
+
|
63
|
+
def visit_AttrSearchableGrammar_Nodes_LessThan(o)
|
64
|
+
visit ::Arel::Nodes::LessThan.new(o.left, o.right)
|
65
|
+
end
|
66
|
+
|
67
|
+
def visit_AttrSearchableGrammar_Nodes_LessThanOrEqual(o)
|
68
|
+
visit ::Arel::Nodes::LessThanOrEqual.new(o.left, o.right)
|
69
|
+
end
|
70
|
+
|
71
|
+
def visit_AttrSearchableGrammar_Nodes_GreaterThan(o)
|
72
|
+
visit ::Arel::Nodes::GreaterThan.new(o.left, o.right)
|
73
|
+
end
|
74
|
+
|
75
|
+
def visit_AttrSearchableGrammar_Nodes_GreaterThanOrEqual(o)
|
76
|
+
visit ::Arel::Nodes::GreaterThanOrEqual.new(o.left, o.right)
|
77
|
+
end
|
78
|
+
|
79
|
+
def visit_AttrSearchableGrammar_Nodes_Not(o)
|
80
|
+
visit ::Arel::Nodes::Not.new(o.object)
|
81
|
+
end
|
82
|
+
|
83
|
+
def visit_AttrSearchableGrammar_Nodes_Matches(o)
|
84
|
+
visit ::Arel::Nodes::Matches.new(o.left, o.right)
|
21
85
|
end
|
22
86
|
end
|
23
87
|
end
|
@@ -29,7 +29,7 @@ class AttrSearchable::HashParser
|
|
29
29
|
collection = AttrSearchableGrammar::Attributes::Collection.new(@model, key.to_s)
|
30
30
|
|
31
31
|
if value.is_a?(Hash)
|
32
|
-
raise
|
32
|
+
raise(AttrSearchable::ParseError, "Unknown operator #{value.keys.first}") unless [:matches, :eq, :not_eq, :gt, :gteq, :lt, :lteq].include?(value.keys.first)
|
33
33
|
|
34
34
|
collection.send value.keys.first, value.values.first.to_s
|
35
35
|
else
|
@@ -10,23 +10,23 @@ module AttrSearchableGrammar
|
|
10
10
|
@model || parent.model
|
11
11
|
end
|
12
12
|
|
13
|
-
def
|
14
|
-
elements.collect(&:
|
13
|
+
def evaluate
|
14
|
+
elements.collect(&:evaluate).inject(:and)
|
15
15
|
end
|
16
16
|
|
17
17
|
def elements
|
18
18
|
super.select { |element| element.class != Treetop::Runtime::SyntaxNode }
|
19
19
|
end
|
20
20
|
|
21
|
-
def
|
22
|
-
raise
|
21
|
+
def collection_for(key)
|
22
|
+
raise(AttrSearchable::UnknownColumn, "Unknown column #{key}") if model.searchable_attributes[key].nil?
|
23
23
|
|
24
24
|
Attributes::Collection.new model, key
|
25
25
|
end
|
26
26
|
end
|
27
27
|
|
28
28
|
class OperatorNode < Treetop::Runtime::SyntaxNode
|
29
|
-
def
|
29
|
+
def evaluate
|
30
30
|
text_value
|
31
31
|
end
|
32
32
|
end
|
@@ -35,59 +35,59 @@ module AttrSearchableGrammar
|
|
35
35
|
class ParenthesesExpression < BaseNode; end
|
36
36
|
|
37
37
|
class ComparativeExpression < BaseNode
|
38
|
-
def
|
39
|
-
elements[0].
|
38
|
+
def evaluate
|
39
|
+
elements[0].collection.send elements[1].method_name, elements[2].text_value
|
40
40
|
end
|
41
41
|
end
|
42
42
|
|
43
43
|
class IncludesOperator < OperatorNode
|
44
|
-
def
|
44
|
+
def method_name
|
45
45
|
:matches
|
46
46
|
end
|
47
47
|
end
|
48
48
|
|
49
49
|
class EqualOperator < OperatorNode
|
50
|
-
def
|
50
|
+
def method_name
|
51
51
|
:eq
|
52
52
|
end
|
53
53
|
end
|
54
54
|
|
55
55
|
class UnequalOperator < OperatorNode
|
56
|
-
def
|
56
|
+
def method_name
|
57
57
|
:not_eq
|
58
58
|
end
|
59
59
|
end
|
60
60
|
|
61
61
|
class GreaterEqualOperator < OperatorNode
|
62
|
-
def
|
62
|
+
def method_name
|
63
63
|
:gteq
|
64
64
|
end
|
65
65
|
end
|
66
66
|
|
67
67
|
class GreaterOperator < OperatorNode
|
68
|
-
def
|
68
|
+
def method_name
|
69
69
|
:gt
|
70
70
|
end
|
71
71
|
end
|
72
72
|
|
73
73
|
class LessEqualOperator < OperatorNode
|
74
|
-
def
|
74
|
+
def method_name
|
75
75
|
:lteq
|
76
76
|
end
|
77
77
|
end
|
78
78
|
|
79
79
|
class LessOperator < OperatorNode
|
80
|
-
def
|
80
|
+
def method_name
|
81
81
|
:lt
|
82
82
|
end
|
83
83
|
end
|
84
84
|
|
85
85
|
class AnywhereExpression < BaseNode
|
86
|
-
def
|
86
|
+
def evaluate
|
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|
|
90
|
+
queries = keys.collect { |key| collection_for key }.select { |attribute| attribute.compatible? text_value }.collect { |attribute| attribute.matches text_value }
|
91
91
|
|
92
92
|
raise AttrSearchable::NoSearchableAttributes unless model.searchable_attributes
|
93
93
|
|
@@ -96,26 +96,26 @@ module AttrSearchableGrammar
|
|
96
96
|
end
|
97
97
|
|
98
98
|
class AndExpression < BaseNode
|
99
|
-
def
|
100
|
-
[elements.first.
|
99
|
+
def evaluate
|
100
|
+
[elements.first.evaluate, elements.last.evaluate].inject(:and)
|
101
101
|
end
|
102
102
|
end
|
103
103
|
|
104
104
|
class OrExpression < BaseNode
|
105
|
-
def
|
106
|
-
[elements.first.
|
105
|
+
def evaluate
|
106
|
+
[elements.first.evaluate, elements.last.evaluate].inject(:or)
|
107
107
|
end
|
108
108
|
end
|
109
109
|
|
110
110
|
class NotExpression < BaseNode
|
111
|
-
def
|
112
|
-
elements.first.
|
111
|
+
def evaluate
|
112
|
+
elements.first.evaluate.not
|
113
113
|
end
|
114
114
|
end
|
115
115
|
|
116
116
|
class Column < BaseNode
|
117
|
-
def
|
118
|
-
|
117
|
+
def collection
|
118
|
+
collection_for text_value
|
119
119
|
end
|
120
120
|
end
|
121
121
|
|
@@ -7,7 +7,7 @@ module AttrSearchableGrammar
|
|
7
7
|
attr_reader :model, :key
|
8
8
|
|
9
9
|
def initialize(model, key)
|
10
|
-
raise
|
10
|
+
raise(AttrSearchable::UnknownColumn, "Unknown column #{key}") unless model.searchable_attributes[key]
|
11
11
|
|
12
12
|
@model = model
|
13
13
|
@key = key
|
@@ -52,12 +52,16 @@ module AttrSearchableGrammar
|
|
52
52
|
end
|
53
53
|
|
54
54
|
def attributes
|
55
|
-
@attributes ||= model.searchable_attributes[key].collect
|
56
|
-
|
57
|
-
klass = table.classify.constantize
|
55
|
+
@attributes ||= model.searchable_attributes[key].collect { |attribute_definition| attribute_for attribute_definition }
|
56
|
+
end
|
58
57
|
|
59
|
-
|
60
|
-
|
58
|
+
def attribute_for(attribute_definition)
|
59
|
+
table, column = attribute_definition.split(".")
|
60
|
+
klass = table.classify.constantize
|
61
|
+
|
62
|
+
raise(AttrSearchable::UnknownAttribute, "Unknown attribute #{attribute_definition}") unless klass.columns_hash[column]
|
63
|
+
|
64
|
+
Attributes.const_get(klass.columns_hash[column].type.to_s.classify).new(klass.arel_table.alias(klass.table_name)[column], klass, options)
|
61
65
|
end
|
62
66
|
end
|
63
67
|
|
@@ -88,7 +92,7 @@ module AttrSearchableGrammar
|
|
88
92
|
|
89
93
|
{ :eq => "Equality", :not_eq => "NotEqual", :lt => "LessThan", :lteq => "LessThanOrEqual", :gt => "GreaterThan", :gteq => "GreaterThanOrEqual", :matches => "Matches" }.each do |method, class_name|
|
90
94
|
define_method method do |value|
|
91
|
-
raise
|
95
|
+
raise(AttrSearchable::IncompatibleDatatype, "Incompatible datatype for #{value}") unless compatible?(value)
|
92
96
|
|
93
97
|
AttrSearchableGrammar::Nodes.const_get(class_name).new(@attribute, map(value))
|
94
98
|
end
|
@@ -152,7 +156,7 @@ module AttrSearchableGrammar
|
|
152
156
|
time .. time
|
153
157
|
end
|
154
158
|
rescue ArgumentError
|
155
|
-
raise AttrSearchable::IncompatibleDatatype
|
159
|
+
raise AttrSearchable::IncompatibleDatatype, "Incompatible datatype for #{value}"
|
156
160
|
end
|
157
161
|
|
158
162
|
def map(value)
|
@@ -188,7 +192,7 @@ module AttrSearchableGrammar
|
|
188
192
|
return true if value.to_s =~ /^(1|true|yes)$/i
|
189
193
|
return false if value.to_s =~ /^(0|false|no)$/i
|
190
194
|
|
191
|
-
raise AttrSearchable::IncompatibleDatatype
|
195
|
+
raise AttrSearchable::IncompatibleDatatype, "Incompatible datatype for #{value}"
|
192
196
|
end
|
193
197
|
end
|
194
198
|
end
|
@@ -46,6 +46,10 @@ module AttrSearchableGrammar
|
|
46
46
|
finalize!
|
47
47
|
end
|
48
48
|
|
49
|
+
def to_sql(model)
|
50
|
+
model.connection.visitor.accept self
|
51
|
+
end
|
52
|
+
|
49
53
|
def finalize!
|
50
54
|
self
|
51
55
|
end
|
@@ -55,12 +59,36 @@ module AttrSearchableGrammar
|
|
55
59
|
end
|
56
60
|
end
|
57
61
|
|
58
|
-
|
59
|
-
|
60
|
-
|
62
|
+
class Binary
|
63
|
+
include Base
|
64
|
+
|
65
|
+
attr_accessor :left, :right
|
66
|
+
|
67
|
+
def initialize(left, right)
|
68
|
+
@left = left
|
69
|
+
@right = right
|
70
|
+
end
|
61
71
|
end
|
62
72
|
|
63
|
-
class
|
73
|
+
class Equality < Binary; end
|
74
|
+
class NotEqual < Binary; end
|
75
|
+
class GreaterThan < Binary; end
|
76
|
+
class GreaterThanOrEqual < Binary; end
|
77
|
+
class LessThan < Binary; end
|
78
|
+
class LessThanOrEqual < Binary; end
|
79
|
+
class Matches < Binary; end
|
80
|
+
|
81
|
+
class Not
|
82
|
+
include Base
|
83
|
+
|
84
|
+
attr_accessor :object
|
85
|
+
|
86
|
+
def initialize(object)
|
87
|
+
@object = object
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
class MatchesFulltext < Binary
|
64
92
|
include Base
|
65
93
|
|
66
94
|
def not
|
@@ -82,7 +110,7 @@ module AttrSearchableGrammar
|
|
82
110
|
|
83
111
|
class MatchesFulltextNot < MatchesFulltext; end
|
84
112
|
|
85
|
-
class FulltextExpression
|
113
|
+
class FulltextExpression
|
86
114
|
include Base
|
87
115
|
|
88
116
|
attr_reader :collection, :node
|
@@ -93,7 +121,7 @@ module AttrSearchableGrammar
|
|
93
121
|
end
|
94
122
|
end
|
95
123
|
|
96
|
-
class Collection
|
124
|
+
class Collection
|
97
125
|
include Base
|
98
126
|
|
99
127
|
attr_reader :nodes
|
@@ -149,18 +177,10 @@ module AttrSearchableGrammar
|
|
149
177
|
|
150
178
|
class And < Collection
|
151
179
|
class Fulltext < FulltextCollection; end
|
152
|
-
|
153
|
-
def to_arel
|
154
|
-
nodes.inject { |res, cur| Arel::Nodes::And.new [res, cur] }
|
155
|
-
end
|
156
180
|
end
|
157
181
|
|
158
182
|
class Or < Collection
|
159
183
|
class Fulltext < FulltextCollection; end
|
160
|
-
|
161
|
-
def to_arel
|
162
|
-
nodes.inject { |res, cur| Arel::Nodes::Or.new res, cur }
|
163
|
-
end
|
164
184
|
end
|
165
185
|
end
|
166
186
|
end
|
data/test/and_test.rb
CHANGED
@@ -3,8 +3,8 @@ require File.expand_path("../test_helper", __FILE__)
|
|
3
3
|
|
4
4
|
class AndTest < AttrSearchable::TestCase
|
5
5
|
def test_and_string
|
6
|
-
expected =
|
7
|
-
rejected =
|
6
|
+
expected = create(:product, :title => "Expected title", :description => "Description")
|
7
|
+
rejected = create(:product, :title => "Rejected title", :description => "Description")
|
8
8
|
|
9
9
|
results = Product.search("title: 'Expected title' description: Description")
|
10
10
|
|
@@ -15,8 +15,8 @@ class AndTest < AttrSearchable::TestCase
|
|
15
15
|
end
|
16
16
|
|
17
17
|
def test_and_hash
|
18
|
-
expected =
|
19
|
-
rejected =
|
18
|
+
expected = create(:product, :title => "Expected title", :description => "Description")
|
19
|
+
rejected = create(:product, :title => "Rejected title", :description => "Description")
|
20
20
|
|
21
21
|
results = Product.search(:and => [{:title => "Expected title"}, {:description => "Description"}])
|
22
22
|
|
@@ -3,9 +3,9 @@ require File.expand_path("../test_helper", __FILE__)
|
|
3
3
|
|
4
4
|
class AttrSearchableTest < AttrSearchable::TestCase
|
5
5
|
def test_associations
|
6
|
-
product =
|
7
|
-
|
8
|
-
|
6
|
+
product = create(:product, :comments => [
|
7
|
+
create(:comment, :title => "Title1", :message => "Message1"),
|
8
|
+
create(:comment, :title => "Title2", :message => "Message2")
|
9
9
|
])
|
10
10
|
|
11
11
|
assert_includes Product.search("comment: Title1 comment: Message1"), product
|
@@ -13,15 +13,15 @@ class AttrSearchableTest < AttrSearchable::TestCase
|
|
13
13
|
end
|
14
14
|
|
15
15
|
def test_multiple
|
16
|
-
product =
|
16
|
+
product = create(:product, :comments => [create(:comment, :title => "Title", :message => "Message")])
|
17
17
|
|
18
18
|
assert_includes Product.search("comment: Title"), product
|
19
19
|
assert_includes Product.search("comment: Message"), product
|
20
20
|
end
|
21
21
|
|
22
22
|
def test_default
|
23
|
-
product1 =
|
24
|
-
product2 =
|
23
|
+
product1 = create(:product, :title => "Expected")
|
24
|
+
product2 = create(:product, :description => "Expected")
|
25
25
|
|
26
26
|
results = Product.search("Expected")
|
27
27
|
|
@@ -30,9 +30,9 @@ class AttrSearchableTest < AttrSearchable::TestCase
|
|
30
30
|
end
|
31
31
|
|
32
32
|
def test_custom_default
|
33
|
-
product1 =
|
34
|
-
product2 =
|
35
|
-
product3 =
|
33
|
+
product1 = create(:product, :title => "Expected")
|
34
|
+
product2 = create(:product, :description => "Expected")
|
35
|
+
product3 = create(:product, :brand => "Expected")
|
36
36
|
|
37
37
|
results = with_attr_searchable_options(Product, :primary, :default => true) { Product.search "Expected" }
|
38
38
|
|
@@ -40,5 +40,11 @@ class AttrSearchableTest < AttrSearchable::TestCase
|
|
40
40
|
assert_includes results, product2
|
41
41
|
refute_includes results, product3
|
42
42
|
end
|
43
|
+
|
44
|
+
def test_count
|
45
|
+
create_list :product, 2, :title => "Expected"
|
46
|
+
|
47
|
+
assert_equal 2, Product.search("Expected").count
|
48
|
+
end
|
43
49
|
end
|
44
50
|
|
data/test/boolean_test.rb
CHANGED
@@ -3,13 +3,13 @@ require File.expand_path("../test_helper", __FILE__)
|
|
3
3
|
|
4
4
|
class BooleanTest < AttrSearchable::TestCase
|
5
5
|
def test_mapping
|
6
|
-
product =
|
6
|
+
product = create(:product, :available => true)
|
7
7
|
|
8
8
|
assert_includes Product.search("available: 1"), product
|
9
9
|
assert_includes Product.search("available: true"), product
|
10
10
|
assert_includes Product.search("available: yes"), product
|
11
11
|
|
12
|
-
product =
|
12
|
+
product = create(:product, :available => false)
|
13
13
|
|
14
14
|
assert_includes Product.search("available: 0"), product
|
15
15
|
assert_includes Product.search("available: false"), product
|
@@ -17,31 +17,37 @@ class BooleanTest < AttrSearchable::TestCase
|
|
17
17
|
end
|
18
18
|
|
19
19
|
def test_anywhere
|
20
|
-
product =
|
20
|
+
product = create(:product, :available => true)
|
21
21
|
|
22
22
|
assert_includes Product.search("true"), product
|
23
23
|
refute_includes Product.search("false"), product
|
24
24
|
end
|
25
25
|
|
26
26
|
def test_includes
|
27
|
-
product =
|
27
|
+
product = create(:product, :available => true)
|
28
28
|
|
29
29
|
assert_includes Product.search("available: true"), product
|
30
30
|
refute_includes Product.search("available: false"), product
|
31
31
|
end
|
32
32
|
|
33
33
|
def test_equals
|
34
|
-
product =
|
34
|
+
product = create(:product, :available => true)
|
35
35
|
|
36
36
|
assert_includes Product.search("available = true"), product
|
37
37
|
refute_includes Product.search("available = false"), product
|
38
38
|
end
|
39
39
|
|
40
40
|
def test_equals_not
|
41
|
-
product =
|
41
|
+
product = create(:product, :available => false)
|
42
42
|
|
43
43
|
assert_includes Product.search("available != true"), product
|
44
44
|
refute_includes Product.search("available != false"), product
|
45
45
|
end
|
46
|
+
|
47
|
+
def test_incompatible_datatype
|
48
|
+
assert_raises AttrSearchable::IncompatibleDatatype do
|
49
|
+
Product.unsafe_search "available: Value"
|
50
|
+
end
|
51
|
+
end
|
46
52
|
end
|
47
53
|
|
data/test/datetime_test.rb
CHANGED
@@ -3,7 +3,7 @@ require File.expand_path("../test_helper", __FILE__)
|
|
3
3
|
|
4
4
|
class DatetimeTest < AttrSearchable::TestCase
|
5
5
|
def test_mapping
|
6
|
-
product =
|
6
|
+
product = create(:product, :created_at => Time.parse("2014-05-01 12:30:30"))
|
7
7
|
|
8
8
|
assert_includes Product.search("created_at: 2014"), product
|
9
9
|
assert_includes Product.search("created_at: 2014-05"), product
|
@@ -12,59 +12,65 @@ class DatetimeTest < AttrSearchable::TestCase
|
|
12
12
|
end
|
13
13
|
|
14
14
|
def test_anywhere
|
15
|
-
product =
|
15
|
+
product = create(:product, :created_at => Time.parse("2014-05-01"))
|
16
16
|
|
17
17
|
assert_includes Product.search("2014-05-01"), product
|
18
18
|
refute_includes Product.search("2014-05-02"), product
|
19
19
|
end
|
20
20
|
|
21
21
|
def test_includes
|
22
|
-
product =
|
22
|
+
product = create(:product, :created_at => Time.parse("2014-05-01"))
|
23
23
|
|
24
24
|
assert_includes Product.search("created_at: 2014-05-01"), product
|
25
25
|
refute_includes Product.search("created_at: 2014-05-02"), product
|
26
26
|
end
|
27
27
|
|
28
28
|
def test_equals
|
29
|
-
product =
|
29
|
+
product = create(:product, :created_at => Time.parse("2014-05-01"))
|
30
30
|
|
31
31
|
assert_includes Product.search("created_at = 2014-05-01"), product
|
32
32
|
refute_includes Product.search("created_at = 2014-05-02"), product
|
33
33
|
end
|
34
34
|
|
35
35
|
def test_equals_not
|
36
|
-
product =
|
36
|
+
product = create(:product, :created_at => Time.parse("2014-05-01"))
|
37
37
|
|
38
38
|
assert_includes Product.search("created_at != 2014-05-02"), product
|
39
39
|
refute_includes Product.search("created_at != 2014-05-01"), product
|
40
40
|
end
|
41
41
|
|
42
42
|
def test_greater
|
43
|
-
product =
|
43
|
+
product = create(:product, :created_at => Time.parse("2014-05-01"))
|
44
44
|
|
45
45
|
assert_includes Product.search("created_at < 2014-05-02"), product
|
46
46
|
refute_includes Product.search("created_at < 2014-05-01"), product
|
47
47
|
end
|
48
48
|
|
49
49
|
def test_greater_equals
|
50
|
-
product =
|
50
|
+
product = create(:product, :created_at => Time.parse("2014-05-01"))
|
51
51
|
|
52
52
|
assert_includes Product.search("created_at >= 2014-05-01"), product
|
53
53
|
refute_includes Product.search("created_at >= 2014-05-02"), product
|
54
54
|
end
|
55
55
|
|
56
56
|
def test_less
|
57
|
-
product =
|
57
|
+
product = create(:product, :created_at => Time.parse("2014-05-01"))
|
58
58
|
|
59
59
|
assert_includes Product.search("created_at < 2014-05-02"), product
|
60
60
|
refute_includes Product.search("created_at < 2014-05-01"), product
|
61
61
|
end
|
62
62
|
|
63
63
|
def test_less_equals
|
64
|
-
product =
|
64
|
+
product = create(:product, :created_at => Time.parse("2014-05-02"))
|
65
65
|
|
66
66
|
assert_includes Product.search("created_at <= 2014-05-02"), product
|
67
67
|
refute_includes Product.search("created_at <= 2014-05-01"), product
|
68
68
|
end
|
69
|
+
|
70
|
+
def test_incompatible_datatype
|
71
|
+
assert_raises AttrSearchable::IncompatibleDatatype do
|
72
|
+
Product.unsafe_search "created_at: Value"
|
73
|
+
end
|
74
|
+
end
|
69
75
|
end
|
70
76
|
|
data/test/error_test.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
|
2
|
+
require File.expand_path("../test_helper", __FILE__)
|
3
|
+
|
4
|
+
class ErrorTest < AttrSearchable::TestCase
|
5
|
+
def test_parse_error
|
6
|
+
assert_raises AttrSearchable::ParseError do
|
7
|
+
Product.unsafe_search :title => { :unknown_operator => "Value" }
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def test_unknown_column
|
12
|
+
assert_raises AttrSearchable::UnknownColumn do
|
13
|
+
Product.unsafe_search "Unknown: Column"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
data/test/float_test.rb
CHANGED
@@ -3,59 +3,65 @@ require File.expand_path("../test_helper", __FILE__)
|
|
3
3
|
|
4
4
|
class FloatTest < AttrSearchable::TestCase
|
5
5
|
def test_anywhere
|
6
|
-
product =
|
6
|
+
product = create(:product, :price => 10.5, :created_at => Time.now - 1.day)
|
7
7
|
|
8
8
|
assert_includes Product.search("10.5"), product
|
9
9
|
refute_includes Product.search("11.5"), product
|
10
10
|
end
|
11
11
|
|
12
12
|
def test_includes
|
13
|
-
product =
|
13
|
+
product = create(:product, :price => 10.5)
|
14
14
|
|
15
15
|
assert_includes Product.search("price: 10.5"), product
|
16
16
|
refute_includes Product.search("price: 11.5"), product
|
17
17
|
end
|
18
18
|
|
19
19
|
def test_equals
|
20
|
-
product =
|
20
|
+
product = create(:product, :price => 10.5)
|
21
21
|
|
22
22
|
assert_includes Product.search("price = 10.5"), product
|
23
23
|
refute_includes Product.search("price = 11.5"), product
|
24
24
|
end
|
25
25
|
|
26
26
|
def test_equals_not
|
27
|
-
product =
|
27
|
+
product = create(:product, :price => 10.5)
|
28
28
|
|
29
29
|
assert_includes Product.search("price != 11.5"), product
|
30
30
|
refute_includes Product.search("price != 10.5"), product
|
31
31
|
end
|
32
32
|
|
33
33
|
def test_greater
|
34
|
-
product =
|
34
|
+
product = create(:product, :price => 10.5)
|
35
35
|
|
36
36
|
assert_includes Product.search("price > 10.4"), product
|
37
37
|
refute_includes Product.search("price < 10.5"), product
|
38
38
|
end
|
39
39
|
|
40
40
|
def test_greater_equals
|
41
|
-
product =
|
41
|
+
product = create(:product, :price => 10.5)
|
42
42
|
|
43
43
|
assert_includes Product.search("price >= 10.5"), product
|
44
44
|
refute_includes Product.search("price >= 10.6"), product
|
45
45
|
end
|
46
46
|
|
47
47
|
def test_less
|
48
|
-
product =
|
48
|
+
product = create(:product, :price => 10.5)
|
49
49
|
|
50
50
|
assert_includes Product.search("price < 10.6"), product
|
51
51
|
refute_includes Product.search("price < 10.5"), product
|
52
52
|
end
|
53
53
|
|
54
54
|
def test_less_equals
|
55
|
-
product =
|
55
|
+
product = create(:product, :price => 10.5)
|
56
56
|
|
57
57
|
assert_includes Product.search("price <= 10.5"), product
|
58
58
|
refute_includes Product.search("price <= 10.4"), product
|
59
59
|
end
|
60
|
+
|
61
|
+
def test_incompatible_datatype
|
62
|
+
assert_raises AttrSearchable::IncompatibleDatatype do
|
63
|
+
Product.unsafe_search "price: Value"
|
64
|
+
end
|
65
|
+
end
|
60
66
|
end
|
61
67
|
|
data/test/fulltext_test.rb
CHANGED
@@ -3,9 +3,9 @@ require File.expand_path("../test_helper", __FILE__)
|
|
3
3
|
|
4
4
|
class FulltextTest < AttrSearchable::TestCase
|
5
5
|
def test_complex
|
6
|
-
product1 =
|
7
|
-
product2 =
|
8
|
-
product3 =
|
6
|
+
product1 = create(:product, :title => "word1")
|
7
|
+
product2 = create(:product, :title => "word2 word3")
|
8
|
+
product3 = create(:product, :title => "word2")
|
9
9
|
|
10
10
|
results = Product.search("title:word1 OR (title:word2 -title:word3)")
|
11
11
|
|
@@ -15,8 +15,8 @@ class FulltextTest < AttrSearchable::TestCase
|
|
15
15
|
end
|
16
16
|
|
17
17
|
def test_mixed
|
18
|
-
expected =
|
19
|
-
rejected =
|
18
|
+
expected = create(:product, :title => "Expected title", :stock => 1)
|
19
|
+
rejected = create(:product, :title => "Expected title", :stock => 0)
|
20
20
|
|
21
21
|
results = Product.search("title:Expected title:Title stock > 0")
|
22
22
|
|
data/test/hash_test.rb
ADDED
@@ -0,0 +1,97 @@
|
|
1
|
+
|
2
|
+
require File.expand_path("../test_helper", __FILE__)
|
3
|
+
|
4
|
+
class HashTest < AttrSearchable::TestCase
|
5
|
+
def test_subquery
|
6
|
+
product1 = create(:product, :title => "Title1", :description => "Description")
|
7
|
+
product2 = create(:product, :title => "Title2", :description => "Description")
|
8
|
+
product3 = create(:product, :title => "TItle3", :description => "Description")
|
9
|
+
|
10
|
+
results = Product.search(:or => [{ :query => "Description Title1" }, { :query => "Description Title2" }])
|
11
|
+
|
12
|
+
assert_includes results, product1
|
13
|
+
assert_includes results, product2
|
14
|
+
refute_includes results, product3
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_matches
|
18
|
+
expected = create(:product, :title => "Expected")
|
19
|
+
rejected = create(:product, :title => "Rejected")
|
20
|
+
|
21
|
+
results = Product.search(:title => { :matches => "Expected" })
|
22
|
+
|
23
|
+
assert_includes results, expected
|
24
|
+
refute_includes results, rejected
|
25
|
+
end
|
26
|
+
|
27
|
+
def test_matches_default
|
28
|
+
expected = create(:product, :title => "Expected")
|
29
|
+
rejected = create(:product, :title => "Rejected")
|
30
|
+
|
31
|
+
results = Product.search(:title => "Expected")
|
32
|
+
|
33
|
+
assert_includes results, expected
|
34
|
+
refute_includes results, rejected
|
35
|
+
end
|
36
|
+
|
37
|
+
def test_eq
|
38
|
+
expected = create(:product, :title => "Expected")
|
39
|
+
rejected = create(:product, :title => "Rejected")
|
40
|
+
|
41
|
+
results = Product.search(:title => { :eq => "Expected" })
|
42
|
+
|
43
|
+
assert_includes results, expected
|
44
|
+
refute_includes results, rejected
|
45
|
+
end
|
46
|
+
|
47
|
+
def test_not_eq
|
48
|
+
expected = create(:product, :title => "Expected")
|
49
|
+
rejected = create(:product, :title => "Rejected")
|
50
|
+
|
51
|
+
results = Product.search(:title => { :not_eq => "Rejected" })
|
52
|
+
|
53
|
+
assert_includes results, expected
|
54
|
+
refute_includes results, rejected
|
55
|
+
end
|
56
|
+
|
57
|
+
def test_gt
|
58
|
+
expected = create(:product, :stock => 1)
|
59
|
+
rejected = create(:product, :stock => 0)
|
60
|
+
|
61
|
+
results = Product.search(:stock => { :gt => 0 })
|
62
|
+
|
63
|
+
assert_includes results, expected
|
64
|
+
refute_includes results, rejected
|
65
|
+
end
|
66
|
+
|
67
|
+
def test_gteq
|
68
|
+
expected = create(:product, :stock => 1)
|
69
|
+
rejected = create(:product, :stock => 0)
|
70
|
+
|
71
|
+
results = Product.search(:stock => { :gteq => 1 })
|
72
|
+
|
73
|
+
assert_includes results, expected
|
74
|
+
refute_includes results, rejected
|
75
|
+
end
|
76
|
+
|
77
|
+
def test_lt
|
78
|
+
expected = create(:product, :stock => 0)
|
79
|
+
rejected = create(:product, :stock => 1)
|
80
|
+
|
81
|
+
results = Product.search(:stock => { :lt => 1 })
|
82
|
+
|
83
|
+
assert_includes results, expected
|
84
|
+
refute_includes results, rejected
|
85
|
+
end
|
86
|
+
|
87
|
+
def test_lteq
|
88
|
+
expected = create(:product, :stock => 0)
|
89
|
+
rejected = create(:product, :stock => 1)
|
90
|
+
|
91
|
+
results = Product.search(:stock => { :lteq => 0 })
|
92
|
+
|
93
|
+
assert_includes results, expected
|
94
|
+
refute_includes results, rejected
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
data/test/integer_test.rb
CHANGED
@@ -3,59 +3,65 @@ require File.expand_path("../test_helper", __FILE__)
|
|
3
3
|
|
4
4
|
class IntegerTest < AttrSearchable::TestCase
|
5
5
|
def test_anywhere
|
6
|
-
product =
|
6
|
+
product = create(:product, :stock => 1)
|
7
7
|
|
8
8
|
assert_includes Product.search("1"), product
|
9
9
|
refute_includes Product.search("0"), product
|
10
10
|
end
|
11
11
|
|
12
12
|
def test_includes
|
13
|
-
product =
|
13
|
+
product = create(:product, :stock => 1)
|
14
14
|
|
15
15
|
assert_includes Product.search("stock: 1"), product
|
16
16
|
refute_includes Product.search("stock: 10"), product
|
17
17
|
end
|
18
18
|
|
19
19
|
def test_equals
|
20
|
-
product =
|
20
|
+
product = create(:product, :stock => 1)
|
21
21
|
|
22
22
|
assert_includes Product.search("stock = 1"), product
|
23
23
|
refute_includes Product.search("stock = 0"), product
|
24
24
|
end
|
25
25
|
|
26
26
|
def test_equals_not
|
27
|
-
product =
|
27
|
+
product = create(:product, :stock => 1)
|
28
28
|
|
29
29
|
assert_includes Product.search("stock != 0"), product
|
30
30
|
refute_includes Product.search("stock != 1"), product
|
31
31
|
end
|
32
32
|
|
33
33
|
def test_greater
|
34
|
-
product =
|
34
|
+
product = create(:product, :stock => 1)
|
35
35
|
|
36
36
|
assert_includes Product.search("stock > 0"), product
|
37
37
|
refute_includes Product.search("stock < 1"), product
|
38
38
|
end
|
39
39
|
|
40
40
|
def test_greater_equals
|
41
|
-
product =
|
41
|
+
product = create(:product, :stock => 1)
|
42
42
|
|
43
43
|
assert_includes Product.search("stock >= 1"), product
|
44
44
|
refute_includes Product.search("stock >= 2"), product
|
45
45
|
end
|
46
46
|
|
47
47
|
def test_less
|
48
|
-
product =
|
48
|
+
product = create(:product, :stock => 1)
|
49
49
|
|
50
50
|
assert_includes Product.search("stock < 2"), product
|
51
51
|
refute_includes Product.search("stock < 1"), product
|
52
52
|
end
|
53
53
|
|
54
54
|
def test_less_equals
|
55
|
-
product =
|
55
|
+
product = create(:product, :stock => 1)
|
56
56
|
|
57
57
|
assert_includes Product.search("stock <= 1"), product
|
58
58
|
refute_includes Product.search("stock <= 0"), product
|
59
59
|
end
|
60
|
+
|
61
|
+
def test_incompatible_datatype
|
62
|
+
assert_raises AttrSearchable::IncompatibleDatatype do
|
63
|
+
Product.unsafe_search "stock: Value"
|
64
|
+
end
|
65
|
+
end
|
60
66
|
end
|
61
67
|
|
data/test/not_test.rb
CHANGED
@@ -3,8 +3,8 @@ require File.expand_path("../test_helper", __FILE__)
|
|
3
3
|
|
4
4
|
class NotTest < AttrSearchable::TestCase
|
5
5
|
def test_not_string
|
6
|
-
expected =
|
7
|
-
rejected =
|
6
|
+
expected = create(:product, :title => "Expected title")
|
7
|
+
rejected = create(:product, :title => "Rejected title")
|
8
8
|
|
9
9
|
results = Product.search("title: Title NOT title: Rejected")
|
10
10
|
|
@@ -15,8 +15,8 @@ class NotTest < AttrSearchable::TestCase
|
|
15
15
|
end
|
16
16
|
|
17
17
|
def test_not_hash
|
18
|
-
expected =
|
19
|
-
rejected =
|
18
|
+
expected = create(:product, :title => "Expected title")
|
19
|
+
rejected = create(:product, :title => "Rejected title")
|
20
20
|
|
21
21
|
results = Product.search(:and => [{:title => "Title"}, {:not => {:title => "Rejected"}}])
|
22
22
|
|
data/test/or_test.rb
CHANGED
@@ -3,9 +3,9 @@ require File.expand_path("../test_helper", __FILE__)
|
|
3
3
|
|
4
4
|
class OrTest < AttrSearchable::TestCase
|
5
5
|
def test_or_string
|
6
|
-
product1 =
|
7
|
-
product2 =
|
8
|
-
product3 =
|
6
|
+
product1 = create(:product, :title => "Title1")
|
7
|
+
product2 = create(:product, :title => "Title2")
|
8
|
+
product3 = create(:product, :title => "Title3")
|
9
9
|
|
10
10
|
results = Product.search("title: Title1 OR title: Title2")
|
11
11
|
|
@@ -15,9 +15,9 @@ class OrTest < AttrSearchable::TestCase
|
|
15
15
|
end
|
16
16
|
|
17
17
|
def test_or_hash
|
18
|
-
product1 =
|
19
|
-
product2 =
|
20
|
-
product3 =
|
18
|
+
product1 = create(:product, :title => "Title1")
|
19
|
+
product2 = create(:product, :title => "Title2")
|
20
|
+
product3 = create(:product, :title => "Title3")
|
21
21
|
|
22
22
|
results = Product.search(:or => [{:title => "Title1"}, {:title => "Title2"}])
|
23
23
|
|
data/test/string_test.rb
CHANGED
@@ -3,35 +3,35 @@ require File.expand_path("../test_helper", __FILE__)
|
|
3
3
|
|
4
4
|
class StringTest < AttrSearchable::TestCase
|
5
5
|
def test_anywhere
|
6
|
-
product =
|
6
|
+
product = create(:product, :title => "Expected title")
|
7
7
|
|
8
8
|
assert_includes Product.search("Expected"), product
|
9
9
|
refute_includes Product.search("Rejected"), product
|
10
10
|
end
|
11
11
|
|
12
12
|
def test_multiple
|
13
|
-
product =
|
13
|
+
product = create(:product, :comments => [create(:comment, :title => "Expected title", :message => "Expected message")])
|
14
14
|
|
15
15
|
assert_includes Product.search("Expected"), product
|
16
16
|
refute_includes Product.search("Rejected"), product
|
17
17
|
end
|
18
18
|
|
19
19
|
def test_includes
|
20
|
-
product =
|
20
|
+
product = create(:product, :title => "Expected")
|
21
21
|
|
22
22
|
assert_includes Product.search("title: Expected"), product
|
23
23
|
refute_includes Product.search("title: Rejected"), product
|
24
24
|
end
|
25
25
|
|
26
26
|
def test_includes_with_left_wildcard
|
27
|
-
product =
|
27
|
+
product = create(:product, :title => "Some title")
|
28
28
|
|
29
29
|
assert_includes Product.search("title: Title"), product
|
30
30
|
end
|
31
31
|
|
32
32
|
def test_includes_without_left_wildcard
|
33
|
-
expected =
|
34
|
-
rejected =
|
33
|
+
expected = create(:product, :brand => "Brand")
|
34
|
+
rejected = create(:product, :brand => "Rejected brand")
|
35
35
|
|
36
36
|
results = with_attr_searchable_options(Product, :brand, :left_wildcard => false) { Product.search "brand: Brand" }
|
37
37
|
|
@@ -40,42 +40,42 @@ class StringTest < AttrSearchable::TestCase
|
|
40
40
|
end
|
41
41
|
|
42
42
|
def test_equals
|
43
|
-
product =
|
43
|
+
product = create(:product, :title => "Expected title")
|
44
44
|
|
45
45
|
assert_includes Product.search("title = 'Expected title'"), product
|
46
46
|
refute_includes Product.search("title = Expected"), product
|
47
47
|
end
|
48
48
|
|
49
49
|
def test_equals_not
|
50
|
-
product =
|
50
|
+
product = create(:product, :title => "Expected")
|
51
51
|
|
52
52
|
assert_includes Product.search("title != Rejected"), product
|
53
53
|
refute_includes Product.search("title != Expected"), product
|
54
54
|
end
|
55
55
|
|
56
56
|
def test_greater
|
57
|
-
product =
|
57
|
+
product = create(:product, :title => "Title B")
|
58
58
|
|
59
59
|
assert_includes Product.search("title > 'Title A'"), product
|
60
60
|
refute_includes Product.search("title > 'Title B'"), product
|
61
61
|
end
|
62
62
|
|
63
63
|
def test_greater_equals
|
64
|
-
product =
|
64
|
+
product = create(:product, :title => "Title A")
|
65
65
|
|
66
66
|
assert_includes Product.search("title >= 'Title A'"), product
|
67
67
|
refute_includes Product.search("title >= 'Title B'"), product
|
68
68
|
end
|
69
69
|
|
70
70
|
def test_less
|
71
|
-
product =
|
71
|
+
product = create(:product, :title => "Title A")
|
72
72
|
|
73
73
|
assert_includes Product.search("title < 'Title B'"), product
|
74
74
|
refute_includes Product.search("title < 'Title A'"), product
|
75
75
|
end
|
76
76
|
|
77
77
|
def test_less_or_greater
|
78
|
-
product =
|
78
|
+
product = create(:product, :title => "Title B")
|
79
79
|
|
80
80
|
assert_includes Product.search("title <= 'Title B'"), product
|
81
81
|
refute_includes Product.search("title <= 'Title A'"), product
|
data/test/test_helper.rb
CHANGED
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.2
|
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-06-
|
12
|
+
date: 2014-06-21 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:
|
126
|
+
description: Search-engine like fulltext query support for ActiveRecord
|
127
127
|
email:
|
128
128
|
- vetter@flakks.com
|
129
129
|
executables: []
|
@@ -155,13 +155,14 @@ files:
|
|
155
155
|
- test/boolean_test.rb
|
156
156
|
- test/database.yml
|
157
157
|
- test/datetime_test.rb
|
158
|
+
- test/error_test.rb
|
158
159
|
- test/float_test.rb
|
159
160
|
- test/fulltext_test.rb
|
161
|
+
- test/hash_test.rb
|
160
162
|
- test/integer_test.rb
|
161
163
|
- test/not_test.rb
|
162
164
|
- test/or_test.rb
|
163
165
|
- test/string_test.rb
|
164
|
-
- test/sub_query_test.rb
|
165
166
|
- test/test_helper.rb
|
166
167
|
homepage: https://github.com/mrkamel/attr_searchable
|
167
168
|
licenses:
|
@@ -187,18 +188,20 @@ rubyforge_project:
|
|
187
188
|
rubygems_version: 1.8.23
|
188
189
|
signing_key:
|
189
190
|
specification_version: 3
|
190
|
-
summary: Easily perform complex search-engine like queries on your
|
191
|
+
summary: Easily perform complex search-engine like fulltext queries on your ActiveRecord
|
192
|
+
models
|
191
193
|
test_files:
|
192
194
|
- test/and_test.rb
|
193
195
|
- test/attr_searchable_test.rb
|
194
196
|
- test/boolean_test.rb
|
195
197
|
- test/database.yml
|
196
198
|
- test/datetime_test.rb
|
199
|
+
- test/error_test.rb
|
197
200
|
- test/float_test.rb
|
198
201
|
- test/fulltext_test.rb
|
202
|
+
- test/hash_test.rb
|
199
203
|
- test/integer_test.rb
|
200
204
|
- test/not_test.rb
|
201
205
|
- test/or_test.rb
|
202
206
|
- test/string_test.rb
|
203
|
-
- test/sub_query_test.rb
|
204
207
|
- test/test_helper.rb
|
data/test/sub_query_test.rb
DELETED
@@ -1,17 +0,0 @@
|
|
1
|
-
|
2
|
-
require File.expand_path("../test_helper", __FILE__)
|
3
|
-
|
4
|
-
class SubQueryTest < AttrSearchable::TestCase
|
5
|
-
def test_subquery
|
6
|
-
product1 = FactoryGirl.create(:product, :title => "Title1")
|
7
|
-
product2 = FactoryGirl.create(:product, :title => "Title2")
|
8
|
-
product3 = FactoryGirl.create(:product, :title => "TItle3")
|
9
|
-
|
10
|
-
results = Product.search(:or => [{:query => "Title1"}, {:query => "Title2"}])
|
11
|
-
|
12
|
-
assert_includes results, product1
|
13
|
-
assert_includes results, product2
|
14
|
-
refute_includes results, product3
|
15
|
-
end
|
16
|
-
end
|
17
|
-
|