bloodhound 0.2.1 → 0.3
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 +4 -4
- data/lib/bloodhound.rb +39 -68
- data/spec/bloodhound_spec.rb +18 -39
- data/spec/spec_helper.rb +0 -15
- metadata +4 -4
data/bloodhound.gemspec
CHANGED
@@ -1,10 +1,10 @@
|
|
1
1
|
Gem::Specification.new do |s|
|
2
2
|
s.name = "bloodhound"
|
3
|
-
s.version = "0.
|
4
|
-
s.date = "2010-02-
|
3
|
+
s.version = "0.3"
|
4
|
+
s.date = "2010-02-07"
|
5
5
|
|
6
|
-
s.description = "
|
7
|
-
s.summary = "
|
6
|
+
s.description = "Map strings like 'user:foca age:23' to ActiveRecord named_scopes"
|
7
|
+
s.summary = "Map strings like 'user:foca age:23' to ActiveRecord named_scopes"
|
8
8
|
s.homepage = "http://github.com/foca/bloodhound"
|
9
9
|
|
10
10
|
s.authors = ["Nicolás Sanguinetti"]
|
data/lib/bloodhound.rb
CHANGED
@@ -1,82 +1,65 @@
|
|
1
1
|
require "chronic"
|
2
|
+
require "active_record"
|
2
3
|
|
3
4
|
class Bloodhound
|
4
|
-
VERSION = "0.
|
5
|
+
VERSION = "0.3"
|
5
6
|
ATTRIBUTE_RE = /\s*(?:(?:(\S+):)?(?:"([^"]*)"|'([^']*)'|(\S+)))\s*/.freeze
|
6
7
|
|
7
|
-
|
8
|
-
@search_fields = {}
|
9
|
-
@keywords = {}
|
10
|
-
end
|
8
|
+
attr_reader :fields
|
11
9
|
|
12
|
-
def
|
13
|
-
@
|
10
|
+
def initialize(model)
|
11
|
+
@model = model
|
12
|
+
@fields = {}
|
14
13
|
end
|
15
14
|
|
16
15
|
def field(name, options={}, &mapping)
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
fields[name.to_sym] = {
|
22
|
-
:attribute => attribute.to_s,
|
23
|
-
:type => type,
|
24
|
-
:fuzzy => fuzzy.nil? ? true : fuzzy,
|
25
|
-
:options => options,
|
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),
|
26
20
|
:mapping => mapping || default_mapping
|
27
21
|
}
|
28
|
-
end
|
29
22
|
|
30
|
-
|
31
|
-
|
23
|
+
define_scope(name) do |value|
|
24
|
+
value = field[:mapping].call(cast_value(value, field[:type]))
|
25
|
+
field[:options].merge(:conditions => { field[:attribute] => value })
|
26
|
+
end
|
32
27
|
end
|
33
28
|
|
34
|
-
def
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
else
|
41
|
-
attribute_search_for(model, key, value)
|
42
|
-
end
|
29
|
+
def text_search(*fields)
|
30
|
+
options = fields.extract_options!
|
31
|
+
fields = fields.map {|f| "LOWER(#{f}) LIKE :value" }.join(" OR ")
|
32
|
+
|
33
|
+
define_scope "text_search" do |value|
|
34
|
+
options.merge(:conditions => [fields, { :value => "%#{value.downcase}%" }])
|
43
35
|
end
|
44
36
|
end
|
45
37
|
|
46
|
-
def
|
47
|
-
|
38
|
+
def keyword(name, &block)
|
39
|
+
define_scope(name, &block)
|
40
|
+
end
|
48
41
|
|
49
|
-
|
42
|
+
def search(query)
|
43
|
+
self.class.tokenize(query).inject(@model) do |model, (key,value)|
|
44
|
+
key, value = "text_search", key if value.nil?
|
50
45
|
|
51
|
-
|
52
|
-
|
53
|
-
conditions[0] << "#{properties[:attribute]} LIKE :#{name}"
|
54
|
-
conditions[1].update(name => properties[:mapping].call("%#{value}%"))
|
55
|
-
conditions
|
46
|
+
if model.respond_to?(scope_name_for(key))
|
47
|
+
model.send(scope_name_for(key), value)
|
56
48
|
else
|
57
|
-
|
49
|
+
model
|
58
50
|
end
|
59
51
|
end
|
60
|
-
|
61
|
-
model = model.scoped(:conditions => [text_search_conditions[0].join(" OR "),
|
62
|
-
text_search_conditions[1]])
|
63
52
|
end
|
64
|
-
private :text_search_for
|
65
|
-
|
66
|
-
def attribute_search_for(model, name, value)
|
67
|
-
properties = fields.fetch(name.to_sym, {})
|
68
53
|
|
69
|
-
|
70
|
-
|
71
|
-
value = properties[:mapping].call(cast_value(value, properties[:type]))
|
72
|
-
model.scoped(properties[:options].merge(:conditions => { properties[:attribute] => value }))
|
54
|
+
def define_scope(name, &scope)
|
55
|
+
@model.send(:named_scope, scope_name_for(name), scope)
|
73
56
|
end
|
74
|
-
private :
|
57
|
+
private :define_scope
|
75
58
|
|
76
|
-
def
|
77
|
-
|
59
|
+
def scope_name_for(key)
|
60
|
+
"bloodhound_for_#{key}"
|
78
61
|
end
|
79
|
-
private :
|
62
|
+
private :scope_name_for
|
80
63
|
|
81
64
|
def default_mapping
|
82
65
|
lambda {|value| value }
|
@@ -108,33 +91,21 @@ class Bloodhound
|
|
108
91
|
end
|
109
92
|
private :cast_value
|
110
93
|
|
111
|
-
def
|
112
|
-
fields.inject(model) do |model, (name,properties)|
|
113
|
-
properties[:fuzzy] ? model.scoped(properties[:options]) : model
|
114
|
-
end
|
115
|
-
end
|
116
|
-
private :scoped_by_text_search_options
|
117
|
-
|
118
|
-
def tokenize(query)
|
94
|
+
def self.tokenize(query)
|
119
95
|
query.scan(ATTRIBUTE_RE).map do |(key,*value)|
|
120
96
|
value = value.compact.first
|
121
97
|
[key || value, key && value]
|
122
98
|
end
|
123
99
|
end
|
124
|
-
private :tokenize
|
125
|
-
|
126
|
-
def has_keyword?(name)
|
127
|
-
@keywords.has_key?(name.to_sym)
|
128
|
-
end
|
129
100
|
|
130
101
|
module Searchable
|
131
102
|
def bloodhound(&block)
|
132
|
-
@bloodhound ||= Bloodhound.new
|
103
|
+
@bloodhound ||= Bloodhound.new(self)
|
133
104
|
block ? @bloodhound.tap(&block) : @bloodhound
|
134
105
|
end
|
135
106
|
|
136
|
-
def
|
137
|
-
bloodhound.
|
107
|
+
def scopes_for_query(query)
|
108
|
+
bloodhound.search(query)
|
138
109
|
end
|
139
110
|
end
|
140
111
|
end
|
data/spec/bloodhound_spec.rb
CHANGED
@@ -6,14 +6,13 @@ class User < ActiveRecord::Base
|
|
6
6
|
has_many :products
|
7
7
|
|
8
8
|
bloodhound do |search|
|
9
|
-
search.field
|
9
|
+
search.field :first_name
|
10
10
|
search.field :last_name
|
11
|
-
search.field :product, :attribute => "products.name", :joins => :products, :group => column_names.join(",")
|
12
11
|
search.field :available_product, :type => :boolean,
|
13
12
|
:attribute => "products.available",
|
14
|
-
:
|
15
|
-
|
16
|
-
|
13
|
+
:include => :products
|
14
|
+
|
15
|
+
search.text_search "first_name", "last_name", "products.name", :include => :products
|
17
16
|
end
|
18
17
|
end
|
19
18
|
|
@@ -23,9 +22,11 @@ class Product < ActiveRecord::Base
|
|
23
22
|
belongs_to :user
|
24
23
|
|
25
24
|
bloodhound do |search|
|
25
|
+
search.text_search "products.name", "products.value"
|
26
|
+
|
26
27
|
search.field :name
|
27
28
|
search.field :value
|
28
|
-
search.field :available, :type => :boolean
|
29
|
+
search.field :available, :type => :boolean
|
29
30
|
|
30
31
|
search.keyword :sort do |value|
|
31
32
|
{ :order => "#{search.fields[value.to_sym][:attribute]} DESC" }
|
@@ -36,51 +37,29 @@ end
|
|
36
37
|
describe Bloodhound do
|
37
38
|
before :all do
|
38
39
|
@john = User.create(:first_name => "John", :last_name => "Doe")
|
39
|
-
@
|
40
|
-
@
|
40
|
+
@ducky = @john.products.create(:name => "Rubber Duck", :value => 10)
|
41
|
+
@tv = @john.products.create(:name => "TV", :value => 200)
|
41
42
|
end
|
42
43
|
|
43
|
-
it "finds a user by first_name
|
44
|
-
User.
|
45
|
-
User.scoped_search("joHN").should include(@john)
|
44
|
+
it "finds a user by first_name" do
|
45
|
+
User.scopes_for_query("first_name:John").should include(@john)
|
46
46
|
end
|
47
47
|
|
48
|
-
it "
|
49
|
-
|
48
|
+
it "finds by text search" do
|
49
|
+
Product.scopes_for_query("duck rubber").should include(@ducky)
|
50
50
|
end
|
51
51
|
|
52
52
|
it "finds a user by last_name, but only in a case sensitive manner" do
|
53
|
-
User.
|
54
|
-
User.
|
55
|
-
User.scoped_search("Smith").should_not include(@john)
|
53
|
+
User.scopes_for_query("last_name:Doe").should include(@john)
|
54
|
+
User.scopes_for_query("last_name:Doe").should have(1).element
|
56
55
|
end
|
57
56
|
|
58
57
|
it "can find using key:value pairs for attribuets that define a type" do
|
59
|
-
User.
|
60
|
-
User.
|
58
|
+
User.scopes_for_query("available_product:yes").should include(@john)
|
59
|
+
User.scopes_for_query("available_product:no").should_not include(@john)
|
61
60
|
end
|
62
61
|
|
63
62
|
it "allows defining arbitrary keywords to create scopes" do
|
64
|
-
@john.products.
|
65
|
-
end
|
66
|
-
|
67
|
-
it "exposes the fields via bloodhound#attributes" do
|
68
|
-
last_name = User.bloodhound.fields[:last_name]
|
69
|
-
last_name[:attribute].should == "last_name"
|
70
|
-
last_name[:type].should be_nil
|
71
|
-
last_name[:fuzzy].should be_true
|
72
|
-
last_name[:options].should == {}
|
73
|
-
|
74
|
-
first_name = User.bloodhound.fields[:first_name]
|
75
|
-
first_name[:attribute].should == "lower(users.first_name)"
|
76
|
-
first_name[:type].should be_nil
|
77
|
-
first_name[:fuzzy].should be_true
|
78
|
-
first_name[:options].should == {}
|
79
|
-
|
80
|
-
availability = Product.bloodhound.fields[:available]
|
81
|
-
availability[:attribute].should == "available"
|
82
|
-
availability[:type].should == :boolean
|
83
|
-
availability[:fuzzy].should be_false
|
84
|
-
availability[:options].should == {}
|
63
|
+
@john.products.scopes_for_query("sort:name").all.should == [@tv, @ducky]
|
85
64
|
end
|
86
65
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -27,18 +27,3 @@ ActiveRecord::Schema.define(:version => 1) do
|
|
27
27
|
t.belongs_to :user
|
28
28
|
end
|
29
29
|
end
|
30
|
-
|
31
|
-
Spec::Runner.configure do |config|
|
32
|
-
module CustomMatchers
|
33
|
-
def be_included_in_extracted_attributes_from(attribute_string)
|
34
|
-
simple_matcher :other_set_of_attributes do |given|
|
35
|
-
expected = subject.tokenize(attribute_string)
|
36
|
-
given.each do |key, value|
|
37
|
-
expected.fetch(key).should == value
|
38
|
-
end
|
39
|
-
end
|
40
|
-
end
|
41
|
-
end
|
42
|
-
|
43
|
-
config.include CustomMatchers
|
44
|
-
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: bloodhound
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: "0.3"
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- "Nicol\xC3\xA1s Sanguinetti"
|
@@ -9,11 +9,11 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2010-02-
|
12
|
+
date: 2010-02-07 00:00:00 -02:00
|
13
13
|
default_executable:
|
14
14
|
dependencies: []
|
15
15
|
|
16
|
-
description:
|
16
|
+
description: Map strings like 'user:foca age:23' to ActiveRecord named_scopes
|
17
17
|
email: contacto@nicolassanguinetti.info
|
18
18
|
executables: []
|
19
19
|
|
@@ -55,6 +55,6 @@ rubyforge_project:
|
|
55
55
|
rubygems_version: 1.3.5
|
56
56
|
signing_key:
|
57
57
|
specification_version: 3
|
58
|
-
summary:
|
58
|
+
summary: Map strings like 'user:foca age:23' to ActiveRecord named_scopes
|
59
59
|
test_files: []
|
60
60
|
|