attr_searchable 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
[![Build Status](https://secure.travis-ci.org/mrkamel/attr_searchable.png?branch=master)](http://travis-ci.org/mrkamel/attr_searchable)
|
4
4
|
[![Code Climate](https://codeclimate.com/github/mrkamel/attr_searchable.png)](https://codeclimate.com/github/mrkamel/attr_searchable)
|
5
5
|
[![Dependency Status](https://gemnasium.com/mrkamel/attr_searchable.png?travis)](https://gemnasium.com/mrkamel/attr_searchable)
|
6
|
+
[![Gem Version](https://badge.fury.io/rb/attr_searchable.svg)](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
|
-
|