bloodhound 0.3 → 0.3.1
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/bloodhound.gemspec +2 -2
- data/lib/bloodhound.rb +83 -14
- data/spec/bloodhound_spec.rb +32 -1
- metadata +12 -5
data/bloodhound.gemspec
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
Gem::Specification.new do |s|
|
2
2
|
s.name = "bloodhound"
|
3
|
-
s.version = "0.3"
|
4
|
-
s.date = "2010-
|
3
|
+
s.version = "0.3.1"
|
4
|
+
s.date = "2010-03-01"
|
5
5
|
|
6
6
|
s.description = "Map strings like 'user:foca age:23' to ActiveRecord named_scopes"
|
7
7
|
s.summary = "Map strings like 'user:foca age:23' to ActiveRecord named_scopes"
|
data/lib/bloodhound.rb
CHANGED
@@ -2,7 +2,7 @@ require "chronic"
|
|
2
2
|
require "active_record"
|
3
3
|
|
4
4
|
class Bloodhound
|
5
|
-
VERSION = "0.3"
|
5
|
+
VERSION = "0.3.1"
|
6
6
|
ATTRIBUTE_RE = /\s*(?:(?:(\S+):)?(?:"([^"]*)"|'([^']*)'|(\S+)))\s*/.freeze
|
7
7
|
|
8
8
|
attr_reader :fields
|
@@ -14,17 +14,28 @@ class Bloodhound
|
|
14
14
|
|
15
15
|
def field(name, options={}, &mapping)
|
16
16
|
@fields[name.to_sym] = field = {
|
17
|
-
:attribute
|
18
|
-
:type
|
19
|
-
:
|
20
|
-
:
|
17
|
+
:attribute => options.fetch(:attribute, name).to_s,
|
18
|
+
:type => options.fetch(:type, :string).to_sym,
|
19
|
+
:case_sensitive => options.fetch(:case_sensitive, true),
|
20
|
+
:match_substring => options.fetch(:match_substring, false),
|
21
|
+
:options => options.except(:attribute, :type, :case_sensitive, :match_substring),
|
22
|
+
:mapping => mapping || default_mapping
|
21
23
|
}
|
22
24
|
|
25
|
+
define_field(name, field)
|
26
|
+
end
|
27
|
+
|
28
|
+
def define_field(name, field)
|
23
29
|
define_scope(name) do |value|
|
24
|
-
|
25
|
-
field[:options].merge(:conditions => { field[:attribute] => value })
|
30
|
+
field[:options].merge(:conditions => setup_conditions(field, value))
|
26
31
|
end
|
27
32
|
end
|
33
|
+
private :define_field
|
34
|
+
|
35
|
+
def alias_field(name, other_field)
|
36
|
+
@fields[name.to_sym] = @fields[other_field.to_sym]
|
37
|
+
define_field(name, @fields[name.to_sym])
|
38
|
+
end
|
28
39
|
|
29
40
|
def text_search(*fields)
|
30
41
|
options = fields.extract_options!
|
@@ -75,14 +86,10 @@ class Bloodhound
|
|
75
86
|
"false" => false,
|
76
87
|
"no" => false,
|
77
88
|
"n" => false }.fetch(value.downcase, true)
|
78
|
-
when :integer
|
79
|
-
|
80
|
-
# Integer(0.0) fails, when we just want it interpreted as a zero
|
81
|
-
Float(value).to_i
|
82
|
-
when :float, :decimal
|
83
|
-
Float(value)
|
89
|
+
when :float, :decimal, :integer
|
90
|
+
NumericExpression.new(value, type)
|
84
91
|
when :date
|
85
|
-
Date.parse(Chronic.parse(value).to_s)
|
92
|
+
Date.parse(Chronic.parse(value, :context => :past).to_s)
|
86
93
|
when :time, :datetime
|
87
94
|
Chronic.parse(value)
|
88
95
|
else
|
@@ -91,6 +98,38 @@ class Bloodhound
|
|
91
98
|
end
|
92
99
|
private :cast_value
|
93
100
|
|
101
|
+
def setup_conditions(field, value)
|
102
|
+
value = field[:mapping].call(cast_value(value, field[:type]))
|
103
|
+
case field[:type]
|
104
|
+
when :string
|
105
|
+
conditions_for_string_search(field, value)
|
106
|
+
when :float, :decimal, :integer
|
107
|
+
conditions_for_numeric(field, value)
|
108
|
+
else
|
109
|
+
{ field[:attribute] => value }
|
110
|
+
end
|
111
|
+
end
|
112
|
+
private :setup_conditions
|
113
|
+
|
114
|
+
def conditions_for_numeric(field, value)
|
115
|
+
[ "#{field[:attribute]} #{value.condition} ?", value.to_s ]
|
116
|
+
end
|
117
|
+
|
118
|
+
def conditions_for_string_search(field, value)
|
119
|
+
if field[:case_sensitive]
|
120
|
+
field_to_search = field[:attribute]
|
121
|
+
value_to_search = value
|
122
|
+
else
|
123
|
+
field_to_search = "lower(#{field[:attribute]})"
|
124
|
+
value_to_search = value.downcase
|
125
|
+
end
|
126
|
+
if field[:match_substring]
|
127
|
+
value_to_search = "%#{value_to_search}%"
|
128
|
+
end
|
129
|
+
[ "#{field_to_search} like ?", value_to_search ]
|
130
|
+
end
|
131
|
+
private :conditions_for_string_search
|
132
|
+
|
94
133
|
def self.tokenize(query)
|
95
134
|
query.scan(ATTRIBUTE_RE).map do |(key,*value)|
|
96
135
|
value = value.compact.first
|
@@ -108,4 +147,34 @@ class Bloodhound
|
|
108
147
|
bloodhound.search(query)
|
109
148
|
end
|
110
149
|
end
|
150
|
+
|
151
|
+
class NumericExpression
|
152
|
+
attr_reader :value, :condition
|
153
|
+
|
154
|
+
def initialize(value, type)
|
155
|
+
parts = value.scan(/(?:[=<>]+|(?:\d|\.)+)/)[0,2]
|
156
|
+
# Kernel#Float is a bit more lax about parsing numbers, like
|
157
|
+
# Integer(0.0) fails, when we just want it interpreted as a zero
|
158
|
+
@value = Float(parts.last)
|
159
|
+
@value = @value.to_i if type == :integer
|
160
|
+
@condition = sanitize_condition(parts.first)
|
161
|
+
end
|
162
|
+
|
163
|
+
def sanitize_condition(cond)
|
164
|
+
valid = %w(= == > < <= >= <>)
|
165
|
+
valid.include?(cond) ? cond : "="
|
166
|
+
end
|
167
|
+
|
168
|
+
def to_i
|
169
|
+
value.to_i
|
170
|
+
end
|
171
|
+
|
172
|
+
def to_f
|
173
|
+
value.to_f
|
174
|
+
end
|
175
|
+
|
176
|
+
def to_s
|
177
|
+
value.to_s
|
178
|
+
end
|
179
|
+
end
|
111
180
|
end
|
data/spec/bloodhound_spec.rb
CHANGED
@@ -7,7 +7,10 @@ class User < ActiveRecord::Base
|
|
7
7
|
|
8
8
|
bloodhound do |search|
|
9
9
|
search.field :first_name
|
10
|
+
search.alias_field :name, :first_name
|
10
11
|
search.field :last_name
|
12
|
+
search.field :insensitive_last_name, :attribute => "last_name", :case_sensitive => false
|
13
|
+
search.field :substringed_last_name, :attribute => "last_name", :match_substring => true
|
11
14
|
search.field :available_product, :type => :boolean,
|
12
15
|
:attribute => "products.available",
|
13
16
|
:include => :products
|
@@ -25,7 +28,7 @@ class Product < ActiveRecord::Base
|
|
25
28
|
search.text_search "products.name", "products.value"
|
26
29
|
|
27
30
|
search.field :name
|
28
|
-
search.field :value
|
31
|
+
search.field :value, :type => :decimal
|
29
32
|
search.field :available, :type => :boolean
|
30
33
|
|
31
34
|
search.keyword :sort do |value|
|
@@ -45,6 +48,10 @@ describe Bloodhound do
|
|
45
48
|
User.scopes_for_query("first_name:John").should include(@john)
|
46
49
|
end
|
47
50
|
|
51
|
+
it "finds a user by first_name, using an alias" do
|
52
|
+
User.scopes_for_query("name:John").should include(@john)
|
53
|
+
end
|
54
|
+
|
48
55
|
it "finds by text search" do
|
49
56
|
Product.scopes_for_query("duck rubber").should include(@ducky)
|
50
57
|
end
|
@@ -54,6 +61,16 @@ describe Bloodhound do
|
|
54
61
|
User.scopes_for_query("last_name:Doe").should have(1).element
|
55
62
|
end
|
56
63
|
|
64
|
+
it "can find using case insensitive search" do
|
65
|
+
User.scopes_for_query("insensitive_last_name:doe").should include(@john)
|
66
|
+
User.scopes_for_query("insensitive_last_name:doe").should have(1).element
|
67
|
+
end
|
68
|
+
|
69
|
+
it "can find matching substrings" do
|
70
|
+
User.scopes_for_query("substringed_last_name:oe").should include(@john)
|
71
|
+
User.scopes_for_query("substringed_last_name:oe").should have(1).element
|
72
|
+
end
|
73
|
+
|
57
74
|
it "can find using key:value pairs for attribuets that define a type" do
|
58
75
|
User.scopes_for_query("available_product:yes").should include(@john)
|
59
76
|
User.scopes_for_query("available_product:no").should_not include(@john)
|
@@ -62,4 +79,18 @@ describe Bloodhound do
|
|
62
79
|
it "allows defining arbitrary keywords to create scopes" do
|
63
80
|
@john.products.scopes_for_query("sort:name").all.should == [@tv, @ducky]
|
64
81
|
end
|
82
|
+
|
83
|
+
it "allows to search by numeric fields with greater than or lower than modifiers" do
|
84
|
+
@john.products.scopes_for_query("value:=10").should include(@ducky)
|
85
|
+
@john.products.scopes_for_query("value:=10").should have(1).element
|
86
|
+
@john.products.scopes_for_query("value:10").should include(@ducky)
|
87
|
+
@john.products.scopes_for_query("value:10").should have(1).element
|
88
|
+
@john.products.scopes_for_query("value:>100").should include(@tv)
|
89
|
+
@john.products.scopes_for_query("value:>100").should have(1).element
|
90
|
+
@john.products.scopes_for_query("value:<100").should include(@ducky)
|
91
|
+
@john.products.scopes_for_query("value:<100").should have(1).element
|
92
|
+
@john.products.scopes_for_query("value:>200").should have(0).elements
|
93
|
+
@john.products.scopes_for_query("value:>=200").should include(@tv)
|
94
|
+
@john.products.scopes_for_query("value:>=200").should have(1).element
|
95
|
+
end
|
65
96
|
end
|
metadata
CHANGED
@@ -1,7 +1,12 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: bloodhound
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 3
|
8
|
+
- 1
|
9
|
+
version: 0.3.1
|
5
10
|
platform: ruby
|
6
11
|
authors:
|
7
12
|
- "Nicol\xC3\xA1s Sanguinetti"
|
@@ -9,7 +14,7 @@ autorequire:
|
|
9
14
|
bindir: bin
|
10
15
|
cert_chain: []
|
11
16
|
|
12
|
-
date: 2010-
|
17
|
+
date: 2010-03-01 00:00:00 -02:00
|
13
18
|
default_executable:
|
14
19
|
dependencies: []
|
15
20
|
|
@@ -41,18 +46,20 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
41
46
|
requirements:
|
42
47
|
- - ">="
|
43
48
|
- !ruby/object:Gem::Version
|
49
|
+
segments:
|
50
|
+
- 0
|
44
51
|
version: "0"
|
45
|
-
version:
|
46
52
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
47
53
|
requirements:
|
48
54
|
- - ">="
|
49
55
|
- !ruby/object:Gem::Version
|
56
|
+
segments:
|
57
|
+
- 0
|
50
58
|
version: "0"
|
51
|
-
version:
|
52
59
|
requirements: []
|
53
60
|
|
54
61
|
rubyforge_project:
|
55
|
-
rubygems_version: 1.3.
|
62
|
+
rubygems_version: 1.3.6
|
56
63
|
signing_key:
|
57
64
|
specification_version: 3
|
58
65
|
summary: Map strings like 'user:foca age:23' to ActiveRecord named_scopes
|