bloodhound 0.3 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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-02-07"
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"
@@ -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 => options.fetch(:attribute, name).to_s,
18
- :type => options.fetch(:type, :string).to_sym,
19
- :options => options.except(:attribute, :type),
20
- :mapping => mapping || default_mapping
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
- value = field[:mapping].call(cast_value(value, field[:type]))
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
- # Kernel#Float is a bit more lax about parsing numbers, like
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
@@ -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
- version: "0.3"
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-02-07 00:00:00 -02:00
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.5
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