dusen 0.1.0 → 0.2.0
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/.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
|