dusen 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -0
- data/README.md +161 -3
- data/lib/dusen/active_record_ext.rb +7 -3
- data/lib/dusen/parser.rb +6 -6
- data/lib/dusen/query.rb +4 -4
- data/lib/dusen/syntax.rb +3 -3
- data/lib/dusen/{atom.rb → token.rb} +1 -1
- data/lib/dusen/version.rb +2 -2
- data/lib/dusen.rb +1 -1
- data/spec/shared/dusen/active_record_spec.rb +9 -1
- metadata +5 -6
- data/spec/shared/app_root/db/gem_test.db +0 -0
data/.gitignore
CHANGED
data/README.md
CHANGED
@@ -1,4 +1,162 @@
|
|
1
|
-
Dusen - Maps Google-like queries to ActiveRecord
|
2
|
-
|
1
|
+
Dusen - Maps Google-like queries to ActiveRecord scopes
|
2
|
+
=======================================================
|
3
3
|
|
4
|
-
|
4
|
+
|
5
|
+
Dusen gives your ActiveRecord models a DSL to process Google-like queries like:
|
6
|
+
|
7
|
+
some words
|
8
|
+
"a phrase of words"
|
9
|
+
filetype:pdf
|
10
|
+
a mix of words "and phrases" and qualified:fields
|
11
|
+
|
12
|
+
Dusen tokenizes these queries for you and feeds them through simple mappers that
|
13
|
+
convert a token to an ActiveRecord scope chain.
|
14
|
+
This process is packaged in a class method `.search`:
|
15
|
+
|
16
|
+
Contact.search('makandra software "Ruby on Rails" city:augsburg')
|
17
|
+
|
18
|
+
|
19
|
+
Installation
|
20
|
+
------------
|
21
|
+
|
22
|
+
In your `Gemfile` say:
|
23
|
+
|
24
|
+
gem 'dusen'
|
25
|
+
|
26
|
+
Now run `bundle install` and restart your server.
|
27
|
+
|
28
|
+
|
29
|
+
|
30
|
+
Processing text queries
|
31
|
+
-----------------------
|
32
|
+
|
33
|
+
This describes how to define a search syntax that processes queries
|
34
|
+
of words and phrases:
|
35
|
+
|
36
|
+
coworking fooville "market ave"
|
37
|
+
|
38
|
+
|
39
|
+
Our example will be a simple address book:
|
40
|
+
|
41
|
+
class Contact < ActiveRecord::Base
|
42
|
+
|
43
|
+
validates_presence_of :name, :street, :city, :name
|
44
|
+
|
45
|
+
end
|
46
|
+
|
47
|
+
|
48
|
+
We will now teach `Contact` to process a text query like this:
|
49
|
+
|
50
|
+
class Contact < ActiveRecord::Base
|
51
|
+
|
52
|
+
...
|
53
|
+
|
54
|
+
search_syntax do
|
55
|
+
|
56
|
+
search_by :text do |scope, phrase|
|
57
|
+
columns = [:name, :street, :city, :email]
|
58
|
+
scope.where_like(columns => phrase)
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
|
65
|
+
|
66
|
+
Note how you will only ever need to deal with a single token (word or phrase) and return a scope that matches the token.
|
67
|
+
Dusen will take care how these scopes will be chained together.
|
68
|
+
|
69
|
+
If we now call `Contact.search('coworking fooville "market ave"')`
|
70
|
+
the block supplied to `search_by` is called once per token:
|
71
|
+
|
72
|
+
1. `|Contact, 'coworking'|`
|
73
|
+
2. `|Contact.where_like(columns => 'coworking'), 'fooville'|`
|
74
|
+
3. `|Contact.where_like(columns => 'coworking').where_like(columns => 'fooville'), 'market ave'|`
|
75
|
+
|
76
|
+
|
77
|
+
The resulting scope chain is your `Contact` model filtered by
|
78
|
+
the given query:
|
79
|
+
|
80
|
+
> Contact.search('coworking fooville "market ave"')
|
81
|
+
=> Contact.where_like(columns => 'coworking').where_like(columns => 'fooville').where_like(columns => 'market ave')
|
82
|
+
|
83
|
+
|
84
|
+
Note that `where_like` is an utility method that comes with the Dusen gem.
|
85
|
+
It takes one or more column names and a phrase and generates an SQL fragment
|
86
|
+
like this:
|
87
|
+
|
88
|
+
contacts.name LIKE "%coworking%" OR contacts.street LIKE "%coworking%" OR contacts.email LIKE "%coworking%" OR contacts.email LIKE "%coworking%"
|
89
|
+
|
90
|
+
|
91
|
+
Processing queries for qualified fields
|
92
|
+
---------------------------------------
|
93
|
+
|
94
|
+
Let's give `Contact` a way to explictely search for a contact's email address, without
|
95
|
+
going through a full text search. We do this by adding additional `search_by` instructions
|
96
|
+
to our model:
|
97
|
+
|
98
|
+
search_syntax do
|
99
|
+
|
100
|
+
search_by :text do |scope, phrase|
|
101
|
+
...
|
102
|
+
end
|
103
|
+
|
104
|
+
search_by :email do |scope, email|
|
105
|
+
scope.where(:email => email)
|
106
|
+
end
|
107
|
+
|
108
|
+
end
|
109
|
+
|
110
|
+
|
111
|
+
The result is this:
|
112
|
+
|
113
|
+
> Contact.search('email:foo@bar.com')
|
114
|
+
=> Contact.where(:email => 'foo@bar.com')
|
115
|
+
|
116
|
+
|
117
|
+
Feel free to combine text tokens and field tokens:
|
118
|
+
|
119
|
+
> Contact.search('fooville email:foo@bar.com')
|
120
|
+
=> Contact.where_like(columns => 'fooville').where(:email => 'foo@bar.com')
|
121
|
+
|
122
|
+
|
123
|
+
|
124
|
+
Programmatic access without DSL
|
125
|
+
-------------------------------
|
126
|
+
|
127
|
+
You can use Dusen's functionality without using the ActiveRecord DSL or the search scope. Here are some method calls to get you started:
|
128
|
+
|
129
|
+
Contact.search_syntax # => #<Dusen::Syntax>
|
130
|
+
|
131
|
+
syntax = Dusen::Syntax.new
|
132
|
+
syntax.learn_field :email do |scope, email|
|
133
|
+
scope.where(:email => email)
|
134
|
+
end
|
135
|
+
|
136
|
+
query = Dusen::Parser.parse('fooville email:foo@bar.com') # => #<Dusen::Query>
|
137
|
+
query.tokens # => [#<Dusen::Token field: 'text', value: 'fooville'>, #<Dusen::Token field: 'email', value: 'foo@bar.com'>]
|
138
|
+
query.to_s # => "fooville + foo@bar.com"
|
139
|
+
|
140
|
+
syntax.search(Contact, query) # => #<ActiveRecord::Relation>
|
141
|
+
|
142
|
+
|
143
|
+
Development
|
144
|
+
-----------
|
145
|
+
|
146
|
+
Test applications for various Rails versions lives in `spec`. You can run specs from the project root by saying:
|
147
|
+
|
148
|
+
bundle exec rake all:spec
|
149
|
+
|
150
|
+
If you would like to contribute:
|
151
|
+
|
152
|
+
- Fork the repository.
|
153
|
+
- Push your changes **with specs**.
|
154
|
+
- Send me a pull request.
|
155
|
+
|
156
|
+
I'm very eager to keep this gem leightweight and on topic. If you're unsure whether a change would make it into the gem, [talk to me beforehand](mailto:henning.koch@makandra.de).
|
157
|
+
|
158
|
+
|
159
|
+
Credits
|
160
|
+
-------
|
161
|
+
|
162
|
+
Henning Koch from [makandra](http://makandra.com/)
|
@@ -2,9 +2,13 @@ module Dusen
|
|
2
2
|
module ActiveRecord
|
3
3
|
|
4
4
|
def search_syntax(&dsl)
|
5
|
-
|
6
|
-
|
7
|
-
|
5
|
+
if dsl
|
6
|
+
@search_syntax = Dusen::Description.read_syntax(&dsl)
|
7
|
+
singleton_class.send(:define_method, :search) do |query_string|
|
8
|
+
@search_syntax.search(self, query_string)
|
9
|
+
end
|
10
|
+
else
|
11
|
+
@search_syntax
|
8
12
|
end
|
9
13
|
end
|
10
14
|
|
data/lib/dusen/parser.rb
CHANGED
@@ -8,23 +8,23 @@ module Dusen
|
|
8
8
|
def self.parse(query_string)
|
9
9
|
query_string = query_string.dup # we are going to delete substrings in-place
|
10
10
|
query = Query.new
|
11
|
-
|
12
|
-
|
11
|
+
extract_field_query_tokens(query_string, query)
|
12
|
+
extract_text_query_tokens(query_string, query)
|
13
13
|
query
|
14
14
|
end
|
15
15
|
|
16
|
-
def self.
|
16
|
+
def self.extract_text_query_tokens(query_string, query)
|
17
17
|
while query_string.sub!(TEXT_QUERY, '')
|
18
18
|
value = "#{$1}#{$2}"
|
19
|
-
query <<
|
19
|
+
query << Token.new(value)
|
20
20
|
end
|
21
21
|
end
|
22
22
|
|
23
|
-
def self.
|
23
|
+
def self.extract_field_query_tokens(query_string, query)
|
24
24
|
while query_string.sub!(FIELD_QUERY, '')
|
25
25
|
field = $1
|
26
26
|
value = "#{$2}#{$3}"
|
27
|
-
query <<
|
27
|
+
query << Token.new(field, value)
|
28
28
|
end
|
29
29
|
end
|
30
30
|
|
data/lib/dusen/query.rb
CHANGED
@@ -4,11 +4,11 @@ module Dusen
|
|
4
4
|
include Enumerable
|
5
5
|
|
6
6
|
def initialize
|
7
|
-
@
|
7
|
+
@tokens = []
|
8
8
|
end
|
9
9
|
|
10
|
-
def <<(
|
11
|
-
@
|
10
|
+
def <<(token)
|
11
|
+
@tokens << token
|
12
12
|
end
|
13
13
|
|
14
14
|
def to_s
|
@@ -16,7 +16,7 @@ module Dusen
|
|
16
16
|
end
|
17
17
|
|
18
18
|
def each(&block)
|
19
|
-
@
|
19
|
+
@tokens.each(&block)
|
20
20
|
end
|
21
21
|
|
22
22
|
end
|
data/lib/dusen/syntax.rb
CHANGED
@@ -17,9 +17,9 @@ module Dusen
|
|
17
17
|
def search(root_scope, query)
|
18
18
|
scope = root_scope
|
19
19
|
query = parse(query) if query.is_a?(String)
|
20
|
-
query.each do |
|
21
|
-
scoper = @scopers[
|
22
|
-
scope = scoper.call(scope,
|
20
|
+
query.each do |token|
|
21
|
+
scoper = @scopers[token.field] || unknown_scoper
|
22
|
+
scope = scoper.call(scope, token.value)
|
23
23
|
end
|
24
24
|
scope
|
25
25
|
end
|
data/lib/dusen/version.rb
CHANGED
@@ -1,3 +1,3 @@
|
|
1
1
|
module Dusen
|
2
|
-
VERSION = '0.
|
3
|
-
end
|
2
|
+
VERSION = '0.2.0'
|
3
|
+
end
|
data/lib/dusen.rb
CHANGED
@@ -34,7 +34,7 @@ describe Dusen::ActiveRecord do
|
|
34
34
|
User.search('city:"Foo Bar"').to_a.should == [match]
|
35
35
|
end
|
36
36
|
|
37
|
-
it 'should allow to mix multiple types of
|
37
|
+
it 'should allow to mix multiple types of tokens in a single query' do
|
38
38
|
match = User.create!(:name => 'Foo', :city => 'Foohausen')
|
39
39
|
no_match = User.create!(:name => 'Foo', :city => 'Barhausen')
|
40
40
|
User.search('Foo city:Foohausen').to_a.should == [match]
|
@@ -42,4 +42,12 @@ describe Dusen::ActiveRecord do
|
|
42
42
|
|
43
43
|
end
|
44
44
|
|
45
|
+
describe '.search_syntax' do
|
46
|
+
|
47
|
+
it "should return the model's syntax definition when called without a block" do
|
48
|
+
User.search_syntax.should be_a(Dusen::Syntax)
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
|
45
53
|
end
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dusen
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 23
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
|
-
-
|
8
|
+
- 2
|
9
9
|
- 0
|
10
|
-
version: 0.
|
10
|
+
version: 0.2.0
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Henning Koch
|
@@ -15,7 +15,7 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2012-11-
|
18
|
+
date: 2012-11-23 00:00:00 +01:00
|
19
19
|
default_executable:
|
20
20
|
dependencies:
|
21
21
|
- !ruby/object:Gem::Dependency
|
@@ -47,11 +47,11 @@ files:
|
|
47
47
|
- dusen.gemspec
|
48
48
|
- lib/dusen.rb
|
49
49
|
- lib/dusen/active_record_ext.rb
|
50
|
-
- lib/dusen/atom.rb
|
51
50
|
- lib/dusen/description.rb
|
52
51
|
- lib/dusen/parser.rb
|
53
52
|
- lib/dusen/query.rb
|
54
53
|
- lib/dusen/syntax.rb
|
54
|
+
- lib/dusen/token.rb
|
55
55
|
- lib/dusen/util.rb
|
56
56
|
- lib/dusen/version.rb
|
57
57
|
- spec/rails-2.3/Gemfile
|
@@ -110,7 +110,6 @@ files:
|
|
110
110
|
- spec/rails-3.2/spec_helper.rb
|
111
111
|
- spec/shared/app_root/app/controllers/application_controller.rb
|
112
112
|
- spec/shared/app_root/app/models/user.rb
|
113
|
-
- spec/shared/app_root/db/gem_test.db
|
114
113
|
- spec/shared/app_root/db/migrate/001_create_users.rb
|
115
114
|
- spec/shared/dusen/active_record_spec.rb
|
116
115
|
has_rdoc: true
|
Binary file
|