minidusen 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +7 -0
- data/.rspec +2 -0
- data/.ruby-version +2 -0
- data/.travis.yml +30 -0
- data/LICENSE +22 -0
- data/README.md +209 -0
- data/Rakefile +42 -0
- data/doc/filtered_index_view.cropped.png +0 -0
- data/gemfiles/Gemfile.3.2.mysql2 +10 -0
- data/gemfiles/Gemfile.3.2.mysql2.lock +59 -0
- data/gemfiles/Gemfile.4.2.mysql2 +9 -0
- data/gemfiles/Gemfile.4.2.mysql2.lock +63 -0
- data/gemfiles/Gemfile.4.2.pg +9 -0
- data/gemfiles/Gemfile.4.2.pg.lock +63 -0
- data/gemfiles/Gemfile.5.0.mysql2 +9 -0
- data/gemfiles/Gemfile.5.0.mysql2.lock +60 -0
- data/gemfiles/Gemfile.5.0.pg +9 -0
- data/gemfiles/Gemfile.5.0.pg.lock +60 -0
- data/lib/minidusen.rb +12 -0
- data/lib/minidusen/active_record_ext.rb +39 -0
- data/lib/minidusen/filter.rb +31 -0
- data/lib/minidusen/parser.rb +35 -0
- data/lib/minidusen/query.rb +54 -0
- data/lib/minidusen/syntax.rb +54 -0
- data/lib/minidusen/token.rb +25 -0
- data/lib/minidusen/util.rb +55 -0
- data/lib/minidusen/version.rb +3 -0
- data/minidusen.gemspec +23 -0
- data/spec/minidusen/active_record_ext_spec.rb +45 -0
- data/spec/minidusen/filter_spec.rb +196 -0
- data/spec/minidusen/parser_spec.rb +22 -0
- data/spec/minidusen/query_spec.rb +18 -0
- data/spec/minidusen/util_spec.rb +3 -0
- data/spec/spec_helper.rb +29 -0
- data/spec/support/database.rb +50 -0
- data/spec/support/database.sample.yml +10 -0
- data/spec/support/database.travis.yml +9 -0
- data/spec/support/models.rb +81 -0
- metadata +123 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 21b9abdca5644bbe2c9d7266988f8a0a22927435
|
4
|
+
data.tar.gz: 8fde451ba5ee361aa2c7ee3377ac195edd641d0e
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: fbd3c16faed08b0ec651898c609f78da008688701513dbfa372f495327540d14d283f031d9723d75a8425069499aab945129ee794b0bd1aa85699bc9bcd36e6c
|
7
|
+
data.tar.gz: fc869ed7bd8b21f1babd9c433bf6c5c31383188a6d9672e3a43680591bde2989dbd61ce4678d9ff525a1ef2d266935fd3da245a288eb3b110ba0c00846d01812
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.ruby-version
ADDED
data/.travis.yml
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
language: ruby
|
2
|
+
rvm:
|
3
|
+
- "2.1.8"
|
4
|
+
- "2.3.1"
|
5
|
+
gemfile:
|
6
|
+
- gemfiles/Gemfile.3.2.mysql2
|
7
|
+
- gemfiles/Gemfile.4.2.mysql2
|
8
|
+
- gemfiles/Gemfile.4.2.pg
|
9
|
+
- gemfiles/Gemfile.5.0.mysql2
|
10
|
+
- gemfiles/Gemfile.5.0.pg
|
11
|
+
before_script:
|
12
|
+
- psql -c 'create database minidusen_test;' -U postgres
|
13
|
+
- mysql -e 'create database IF NOT EXISTS minidusen_test;'
|
14
|
+
script: bundle exec rspec spec
|
15
|
+
sudo: false
|
16
|
+
cache: bundler
|
17
|
+
notifications:
|
18
|
+
email:
|
19
|
+
- fail@makandra.de
|
20
|
+
branches:
|
21
|
+
only:
|
22
|
+
- master
|
23
|
+
matrix:
|
24
|
+
exclude:
|
25
|
+
- rvm: "2.3.1"
|
26
|
+
gemfile: gemfiles/Gemfile.3.2.mysql2
|
27
|
+
- rvm: "2.1.8"
|
28
|
+
gemfile: gemfiles/Gemfile.5.0.mysql2
|
29
|
+
- rvm: "2.1.8"
|
30
|
+
gemfile: gemfiles/Gemfile.5.0.pg
|
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Henning Koch
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,209 @@
|
|
1
|
+
Minidusen [![Build Status](https://secure.travis-ci.org/makandra/minidusen.png?branch=master)](https://travis-ci.org/makandra/minidusen)
|
2
|
+
=========
|
3
|
+
|
4
|
+
Low-tech search solution for ActiveRecord with MySQL or PostgreSQL
|
5
|
+
------------------------------------------------------------------
|
6
|
+
|
7
|
+
Minidusen lets you filter ActiveRecord models with a single query string.
|
8
|
+
It works with your existing MySQL or PostgreSQL schema by mostly relying on simple `LIKE` queries. No additional indexes, tables or indexing databases are required.
|
9
|
+
|
10
|
+
Minidusen accepts a single, Google-like query string and converts it into `WHERE` conditions for [an ActiveRecord scope](http://guides.rubyonrails.org/active_record_querying.html#conditions).
|
11
|
+
|
12
|
+
The following type of queries are supported:
|
13
|
+
|
14
|
+
- `foo` (case-insensitive search for `foo` in developer-defined columns)
|
15
|
+
- `foo bar` (rows must include both `foo` and `bar`)
|
16
|
+
- `"foo bar"` (rows must include the phrase `"foo bar"`)
|
17
|
+
- `-bar` (rows must not include the word `bar`)
|
18
|
+
- `filetype:pdf` (developer-defined filter for file type)
|
19
|
+
- `some words 'a phrase' filetype:pdf -excluded -'excluded phrase' -filetype:pdf` (combination of the above)
|
20
|
+
|
21
|
+
Minidusen is a quick way to implement find-as-you-type filters for index views:
|
22
|
+
|
23
|
+
![A list of records filtered by a query](https://raw.githubusercontent.com/makandra/minidusen/master/doc/filtered_index_view.cropped.png)
|
24
|
+
|
25
|
+
We have found it to scale well for many thousand records. It's probably not a good idea to use Minidusen for hundreds of thousands of records or very long text columns. For this we recommend to use PostgreSQL with [pg_search](https://github.com/Casecommons/pg_search) or full-text databases like [Solr](https://github.com/sunspot/sunspot).
|
26
|
+
|
27
|
+
|
28
|
+
Installation
|
29
|
+
------------
|
30
|
+
|
31
|
+
In your `Gemfile` say:
|
32
|
+
|
33
|
+
```ruby
|
34
|
+
gem 'minidusen'
|
35
|
+
```
|
36
|
+
|
37
|
+
Now run `bundle install` and restart your server.
|
38
|
+
|
39
|
+
|
40
|
+
Basic Usage
|
41
|
+
-----------
|
42
|
+
|
43
|
+
Our example will be a simple address book:
|
44
|
+
|
45
|
+
```ruby
|
46
|
+
class Contact < ActiveRecord::Base
|
47
|
+
validates_presence_of :name, :street, :city, :email
|
48
|
+
end
|
49
|
+
```
|
50
|
+
|
51
|
+
We create a new class `ContactFilter` that will describe the searchable columns:
|
52
|
+
|
53
|
+
```ruby
|
54
|
+
class ContactFilter
|
55
|
+
include Minidusen::Filter
|
56
|
+
|
57
|
+
filter :text do |scope, phrases|
|
58
|
+
columns = [:name, :email]
|
59
|
+
scope.where_like(columns => phrases)
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
```
|
64
|
+
|
65
|
+
We can now use `ContactFilter` to filter a scope of `Contact` records:
|
66
|
+
|
67
|
+
```ruby
|
68
|
+
# We start by building a scope of all contacts.
|
69
|
+
# No SQL query is made.
|
70
|
+
all_contacts = Contact.all
|
71
|
+
# => ActiveRecord::Relation
|
72
|
+
|
73
|
+
# Now we filter the scope to only contain contacts with "gmail" in either :name or :email column.
|
74
|
+
# Again, no SQL query is made.
|
75
|
+
gmail_contacts = ContactFilter.new.filter(all_contacts, 'gmail')
|
76
|
+
# => ActiveRecord::Relation
|
77
|
+
|
78
|
+
# Inspect the filtered scope.
|
79
|
+
gmail_contacts.to_sql
|
80
|
+
# => "SELECT * FROM contacts WHERE name LIKE '%gmail%' OR email LIKE '%gmail%'"
|
81
|
+
|
82
|
+
# Finally we load the scope to produce an array of Contact records.
|
83
|
+
gmail_contacts.to_a
|
84
|
+
# => Array
|
85
|
+
```
|
86
|
+
|
87
|
+
### Filtering scopes with existing conditions
|
88
|
+
|
89
|
+
Note that you can also pass a scope with existing conditions to `ContactFilter#filter`. The returned scope will contain both the existing conditions and the conditions from the filter:
|
90
|
+
|
91
|
+
```ruby
|
92
|
+
published_contacts = Contact.where(published: true)
|
93
|
+
# => ActiveRecord::Relation
|
94
|
+
|
95
|
+
published_contacts.to_sql
|
96
|
+
# => "SELECT * FROM contacts WHERE (published = 1)"
|
97
|
+
|
98
|
+
gmail_contacts = ContactFilter.new.filter(published_contacts, 'gmail')
|
99
|
+
# => ActiveRecord::Relation
|
100
|
+
|
101
|
+
gmail_contacts.to_sql
|
102
|
+
# => "SELECT * FROM contacts WHERE (published = 1) AND (name LIKE '%gmail%' OR email LIKE '%gmail%')"
|
103
|
+
```
|
104
|
+
|
105
|
+
### How `where_like` works
|
106
|
+
|
107
|
+
The example above uses `where_like`. You can call `where_like` on any scope to produce a new scope where the given array of column names must contain all of the given phrases.
|
108
|
+
|
109
|
+
Let's say we call `ContactFilter.new.filter(Contact.published, 'foo "bar baz" bam')`. This will call the block `filter :text do |scope, phrases|` with the following arguments:
|
110
|
+
|
111
|
+
```ruby
|
112
|
+
scope == Contact.published
|
113
|
+
phrases == ['foo', 'bar baz', 'bam']
|
114
|
+
```
|
115
|
+
|
116
|
+
The scope `scope.where_like(columns => phrases)` will now represent the following SQL query:
|
117
|
+
|
118
|
+
```ruby
|
119
|
+
SELECT * FROM contacts
|
120
|
+
WHERE (name LIKE "%foo%" OR email LIKE "%foo") AND (email LIKE "%foo%" OR email LIKE "%foo")
|
121
|
+
```
|
122
|
+
|
123
|
+
You can also use `where_like` to find all the records *not* matching some phrases, using the `:negate` option:
|
124
|
+
|
125
|
+
```ruby
|
126
|
+
Contact.where_like(name: 'foo', negate: true)
|
127
|
+
```
|
128
|
+
|
129
|
+
Processing queries for qualified fields
|
130
|
+
---------------------------------------
|
131
|
+
|
132
|
+
Google supports queries like `filetype:pdf` that filters records by some criteria without performing a full text search. Minidusen gives you a simple way to support such search syntax.
|
133
|
+
|
134
|
+
Let's support a query like `email:foo@bar.com` to explictly search for a contact's email address, without filtering against other columns.
|
135
|
+
|
136
|
+
We can learn this syntax by adding a `filter:email` instruction
|
137
|
+
to our `ContactFilter` class`:
|
138
|
+
|
139
|
+
```ruby
|
140
|
+
class ContactFilter
|
141
|
+
include Minidusen::Filter
|
142
|
+
|
143
|
+
search_by :email do |scope, email|
|
144
|
+
scope.where(emai: email)
|
145
|
+
end
|
146
|
+
|
147
|
+
search_by :text do |scope, phrases|
|
148
|
+
columns = [:name, :email]
|
149
|
+
scope.where_like(columns => phrases)
|
150
|
+
end
|
151
|
+
|
152
|
+
end
|
153
|
+
```
|
154
|
+
|
155
|
+
We can now explicitly search for a user's e-mail address:
|
156
|
+
|
157
|
+
```ruby
|
158
|
+
ContactFilter.new.filter(Contact, 'email:foo@bar.com').to_sql
|
159
|
+
# => "SELECT * FROM contacts WHERE email='foo@bar.com'"
|
160
|
+
```
|
161
|
+
|
162
|
+
### Caveat
|
163
|
+
|
164
|
+
If you search for a phrase containing a colon (e.g. `deploy:rollback`), Minidusen will mistake the first part as a – nonexistent – qualifier and return an empty set.
|
165
|
+
|
166
|
+
To prevent that, search for a phrase:
|
167
|
+
|
168
|
+
"deploy:rollback"
|
169
|
+
|
170
|
+
|
171
|
+
Supported Rails versions
|
172
|
+
------------------------
|
173
|
+
|
174
|
+
Minidusen is tested on:
|
175
|
+
|
176
|
+
- Rails 3.2
|
177
|
+
- Rails 4.2
|
178
|
+
- Rails 5.0
|
179
|
+
- MySQL 5.6
|
180
|
+
- PostgreSQL
|
181
|
+
|
182
|
+
If you need support for platforms not listed above, please submit a PR!
|
183
|
+
|
184
|
+
|
185
|
+
Development
|
186
|
+
-----------
|
187
|
+
|
188
|
+
- There are tests in `spec`. We only accept PRs with tests.
|
189
|
+
- We currently develop using Ruby 2.2.4 (see `.ruby-version`) since that version works for all versions of ActiveRecord that we support.
|
190
|
+
- TravisCI will test additional Ruby versions (2.1.8 and 2.3.1)
|
191
|
+
- Put your database credentials into `spec/support/database.yml`. There's a `database.sample.yml` you can use as a template.
|
192
|
+
- Create a database `minidusen_test` in both MySQL and PostgreSQL.
|
193
|
+
- There are gem bundles in `gemfiles` for each combination of ActiveRecord version and database type that we support.
|
194
|
+
- You can bundle all test applications by saying `bundle exec rake all:install`
|
195
|
+
- You can run specs from the project root by saying `bundle exec rake all:spec`.
|
196
|
+
|
197
|
+
If you would like to contribute:
|
198
|
+
|
199
|
+
- Fork the repository.
|
200
|
+
- Push your changes **with passing specs**.
|
201
|
+
- Send me a pull request.
|
202
|
+
|
203
|
+
Note that we're very eager to keep this gem lightweight. If you're unsure whether a change would make it into the gem, [open an issue](https://github.com/makandra/minidusen/issues/new).
|
204
|
+
|
205
|
+
|
206
|
+
Credits
|
207
|
+
-------
|
208
|
+
|
209
|
+
Henning Koch from [makandra](http://makandra.com/)
|
data/Rakefile
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'bundler/gem_tasks'
|
3
|
+
|
4
|
+
desc 'Default: Run all specs.'
|
5
|
+
task :default => 'all:spec'
|
6
|
+
|
7
|
+
namespace :all do
|
8
|
+
|
9
|
+
desc "Run specs on all versions"
|
10
|
+
task :spec do
|
11
|
+
success = true
|
12
|
+
for_each_gemfile do
|
13
|
+
success &= system("bundle exec rspec spec")
|
14
|
+
end
|
15
|
+
fail "Tests failed" unless success
|
16
|
+
end
|
17
|
+
|
18
|
+
desc "Bundle all versions"
|
19
|
+
task :install do
|
20
|
+
for_each_gemfile do
|
21
|
+
system('bundle install')
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
desc "Update all versions"
|
26
|
+
task :update do
|
27
|
+
for_each_gemfile do
|
28
|
+
system('bundle update')
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
def for_each_gemfile
|
35
|
+
version = ENV['VERSION'] || '*'
|
36
|
+
Dir["gemfiles/Gemfile.#{version}"].sort.each do |gemfile|
|
37
|
+
next if gemfile =~ /.lock/
|
38
|
+
puts '', "\033[44m#{gemfile}\033[0m", ''
|
39
|
+
ENV['BUNDLE_GEMFILE'] = gemfile
|
40
|
+
yield
|
41
|
+
end
|
42
|
+
end
|
Binary file
|
@@ -0,0 +1,59 @@
|
|
1
|
+
PATH
|
2
|
+
remote: ..
|
3
|
+
specs:
|
4
|
+
minidusen (0.7.0)
|
5
|
+
activerecord (>= 3.2)
|
6
|
+
activesupport (>= 3.2)
|
7
|
+
edge_rider (>= 0.2.5)
|
8
|
+
|
9
|
+
GEM
|
10
|
+
remote: https://rubygems.org/
|
11
|
+
specs:
|
12
|
+
activemodel (3.2.22)
|
13
|
+
activesupport (= 3.2.22)
|
14
|
+
builder (~> 3.0.0)
|
15
|
+
activerecord (3.2.22)
|
16
|
+
activemodel (= 3.2.22)
|
17
|
+
activesupport (= 3.2.22)
|
18
|
+
arel (~> 3.0.2)
|
19
|
+
tzinfo (~> 0.3.29)
|
20
|
+
activesupport (3.2.22)
|
21
|
+
i18n (~> 0.6, >= 0.6.4)
|
22
|
+
multi_json (~> 1.0)
|
23
|
+
arel (3.0.3)
|
24
|
+
builder (3.0.4)
|
25
|
+
byebug (8.2.0)
|
26
|
+
diff-lcs (1.2.5)
|
27
|
+
edge_rider (0.3.1)
|
28
|
+
activerecord
|
29
|
+
i18n (0.6.11)
|
30
|
+
multi_json (1.11.2)
|
31
|
+
mysql2 (0.3.17)
|
32
|
+
rspec (3.4.0)
|
33
|
+
rspec-core (~> 3.4.0)
|
34
|
+
rspec-expectations (~> 3.4.0)
|
35
|
+
rspec-mocks (~> 3.4.0)
|
36
|
+
rspec-core (3.4.1)
|
37
|
+
rspec-support (~> 3.4.0)
|
38
|
+
rspec-expectations (3.4.0)
|
39
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
40
|
+
rspec-support (~> 3.4.0)
|
41
|
+
rspec-mocks (3.4.1)
|
42
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
43
|
+
rspec-support (~> 3.4.0)
|
44
|
+
rspec-support (3.4.1)
|
45
|
+
tzinfo (0.3.46)
|
46
|
+
|
47
|
+
PLATFORMS
|
48
|
+
ruby
|
49
|
+
|
50
|
+
DEPENDENCIES
|
51
|
+
activerecord (= 3.2.22)
|
52
|
+
byebug
|
53
|
+
i18n (= 0.6.11)
|
54
|
+
minidusen!
|
55
|
+
mysql2 (= 0.3.17)
|
56
|
+
rspec (~> 3.4)
|
57
|
+
|
58
|
+
BUNDLED WITH
|
59
|
+
1.12.5
|
@@ -0,0 +1,63 @@
|
|
1
|
+
PATH
|
2
|
+
remote: ..
|
3
|
+
specs:
|
4
|
+
minidusen (0.7.0)
|
5
|
+
activerecord (>= 3.2)
|
6
|
+
activesupport (>= 3.2)
|
7
|
+
edge_rider (>= 0.2.5)
|
8
|
+
|
9
|
+
GEM
|
10
|
+
remote: https://rubygems.org/
|
11
|
+
specs:
|
12
|
+
activemodel (4.2.5)
|
13
|
+
activesupport (= 4.2.5)
|
14
|
+
builder (~> 3.1)
|
15
|
+
activerecord (4.2.5)
|
16
|
+
activemodel (= 4.2.5)
|
17
|
+
activesupport (= 4.2.5)
|
18
|
+
arel (~> 6.0)
|
19
|
+
activesupport (4.2.5)
|
20
|
+
i18n (~> 0.7)
|
21
|
+
json (~> 1.7, >= 1.7.7)
|
22
|
+
minitest (~> 5.1)
|
23
|
+
thread_safe (~> 0.3, >= 0.3.4)
|
24
|
+
tzinfo (~> 1.1)
|
25
|
+
arel (6.0.3)
|
26
|
+
builder (3.2.2)
|
27
|
+
byebug (8.2.0)
|
28
|
+
diff-lcs (1.2.5)
|
29
|
+
edge_rider (0.3.1)
|
30
|
+
activerecord
|
31
|
+
i18n (0.7.0)
|
32
|
+
json (1.8.3)
|
33
|
+
minitest (5.8.3)
|
34
|
+
mysql2 (0.3.20)
|
35
|
+
rspec (3.4.0)
|
36
|
+
rspec-core (~> 3.4.0)
|
37
|
+
rspec-expectations (~> 3.4.0)
|
38
|
+
rspec-mocks (~> 3.4.0)
|
39
|
+
rspec-core (3.4.1)
|
40
|
+
rspec-support (~> 3.4.0)
|
41
|
+
rspec-expectations (3.4.0)
|
42
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
43
|
+
rspec-support (~> 3.4.0)
|
44
|
+
rspec-mocks (3.4.1)
|
45
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
46
|
+
rspec-support (~> 3.4.0)
|
47
|
+
rspec-support (3.4.1)
|
48
|
+
thread_safe (0.3.5)
|
49
|
+
tzinfo (1.2.2)
|
50
|
+
thread_safe (~> 0.1)
|
51
|
+
|
52
|
+
PLATFORMS
|
53
|
+
ruby
|
54
|
+
|
55
|
+
DEPENDENCIES
|
56
|
+
activerecord (~> 4.2.1)
|
57
|
+
byebug
|
58
|
+
minidusen!
|
59
|
+
mysql2 (~> 0.3.17)
|
60
|
+
rspec (~> 3.4)
|
61
|
+
|
62
|
+
BUNDLED WITH
|
63
|
+
1.12.5
|