attr_searchable 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ gemfiles/*.lock
data/.travis.yml ADDED
@@ -0,0 +1,34 @@
1
+
2
+ addons:
3
+ postgresql: "9.3"
4
+
5
+ before_script:
6
+ - mysql -e 'create database attr_searchable;'
7
+ - psql -c 'create database attr_searchable;' -U postgres
8
+
9
+ rvm:
10
+ - 1.9.3
11
+ - 2.0.0
12
+ - 2.1.1
13
+ - rbx-2
14
+ - jruby
15
+ env:
16
+ - DATABASE=sqlite
17
+ - DATABASE=mysql
18
+ - DATABASE=postgres
19
+ matrix:
20
+ allow_failures:
21
+ - rvm: rbx-2
22
+ - rvm: jruby
23
+ fast_finish: true
24
+
25
+ gemfile:
26
+ - gemfiles/3.2.gemfile
27
+ - gemfiles/4.0.gemfile
28
+ - gemfiles/4.1.gemfile
29
+
30
+ install:
31
+ - "travis_retry bundle install"
32
+
33
+ script: "bundle exec rake test"
34
+
data/Appraisals ADDED
@@ -0,0 +1,14 @@
1
+ appraise "3.2" do
2
+ gem "activerecord", "~> 3.2.18"
3
+ gem "attr_searchable", :path => "../"
4
+ end
5
+
6
+ appraise "4.0" do
7
+ gem "activerecord", "~> 4.0.0"
8
+ gem "attr_searchable", :path => "../"
9
+ end
10
+
11
+ appraise "4.1" do
12
+ gem "activerecord", "~> 4.1.0.beta"
13
+ gem "attr_searchable", :path => "../"
14
+ end
data/Gemfile ADDED
@@ -0,0 +1,23 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in attr_searchable.gemspec
4
+ gemspec
5
+
6
+ platforms :jruby do
7
+ gem 'activerecord-jdbcmysql-adapter'
8
+ gem 'activerecord-jdbcsqlite3-adapter'
9
+ gem 'activerecord-jdbcpostgresql-adapter'
10
+ end
11
+
12
+ platforms :ruby do
13
+ gem 'sqlite3'
14
+ gem 'mysql2'
15
+ gem 'pg'
16
+ end
17
+
18
+ platforms :rbx do
19
+ gem 'racc'
20
+ gem 'rubysl', '~> 2.0'
21
+ gem 'psych'
22
+ end
23
+
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Benjamin Vetter
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,339 @@
1
+ # AttrSearchable
2
+
3
+ [![Build Status](https://secure.travis-ci.org/mrkamel/attr_searchable.png?branch=master)](http://travis-ci.org/mrkamel/attr_searchable)
4
+ [![Code Climate](https://codeclimate.com/github/mrkamel/attr_searchable.png)](https://codeclimate.com/github/mrkamel/attr_searchable)
5
+ [![Dependency Status](https://gemnasium.com/mrkamel/attr_searchable.png?travis)](https://gemnasium.com/mrkamel/attr_searchable)
6
+
7
+ AttrSearchable extends your ActiveRecord models to support fulltext search
8
+ engine like queries via simple query strings and hash-based queries. Assume you
9
+ have a `Book` model having various attributes like `title`, `author`, `stock`,
10
+ `price`, `available`. Using AttrSearchable you can perform:
11
+
12
+ ```ruby
13
+ Book.search("Joanne Rowling Harry Potter")
14
+ Book.search("author: Rowling title:'Harry Potter'")
15
+ Book.search("price > 10 AND price < 20 -stock:0 (Potter OR Rowling)")
16
+ # ...
17
+ ```
18
+
19
+ Thus, you can hand out a search query string to your models and you, your app's
20
+ admins and/or users will get powerful query features without the need for
21
+ integrating additional third party search servers, since AttrSearchable can use
22
+ fulltext index capabilities of your RDBMS in a database agnostic way (currently
23
+ MySQL and PostgreSQL fulltext indices are supported) and optimizes the queries
24
+ to make optimal use of them. Read more below.
25
+
26
+ Complex hash-based queries are supported as well:
27
+
28
+ ```ruby
29
+ Book.search(:author => "Rowling", :title => "Harry Potter")
30
+ Book.search(:or => [{:author => "Rowling"}, {:author => "Tolkien"}])
31
+ Book.search(:and => [{:price => {:gt => 10}}, {:not => {:stock => 0}}, :or => [{:title => "Potter"}, {:author => "Rowling"}]])
32
+ Book.search(:or => [{:query => "Rowling -Potter"}, {:query => "Tolkien -Rings"}])
33
+ # ...
34
+ ```
35
+
36
+ ## Installation
37
+
38
+ Add this line to your application's Gemfile:
39
+
40
+ gem 'attr_searchable'
41
+
42
+ And then execute:
43
+
44
+ $ bundle
45
+
46
+ Or install it yourself as:
47
+
48
+ $ gem install attr_searchable
49
+
50
+ ## Usage
51
+
52
+ To enable AttrSearchable for a model, `include AttrSearchable` and specify the
53
+ attributes you want to expose to search queries:
54
+
55
+ ```ruby
56
+ class Book < ActiveRecord::Base
57
+ include AttrSearchable
58
+
59
+ attr_searchable :title, :description, :stock, :price, :created_at, :available
60
+ attr_searchable :comment => ["comments.title", "comments.message"]
61
+ attr_searchable :author => "author.name"
62
+ # ...
63
+
64
+ has_many :comments
65
+ belongs_to :author
66
+ end
67
+ ```
68
+
69
+ ## How does it work
70
+
71
+ AttrSearchable parses the query and maps it to an SQL Query using Arel.
72
+ Thus, AttrSearchable is not bound to a specific RDBMS.
73
+
74
+ ```ruby
75
+ Book.search("stock > 0")
76
+ # ... WHERE books.stock > 0
77
+
78
+ Book.search("price > 10 stock > 0")
79
+ # ... WHERE books.price > 10 AND books.stock > 0
80
+
81
+ Book.search("Harry Potter")
82
+ # ... WHERE (books.title LIKE '%Harry%' OR books.description LIKE '%Harry%' OR ...) AND (books.title LIKE '%Potter%' OR books.description LIKE '%Potter%' ...)
83
+
84
+ Book.search("available:yes OR created_at:2014")
85
+ # ... WHERE books.available = 1 OR (books.created_at >= '2014-01-01 00:00:00' and books.created_at <= '2014-12-31 00:00:00')
86
+ ```
87
+
88
+ Of course, these `LIKE '%...%'` queries won't achieve optimal performance, but
89
+ check out the section below on AttrSearchable's fulltext capabilities to
90
+ understand how the resulting queries can be optimized.
91
+
92
+ As `Book.search(...)` returns an `ActiveRecord::Relation`, you are free to pre-
93
+ or post-process the search results in every possible way:
94
+
95
+ ```ruby
96
+ Book.where(:available => true).search("Harry Potter").order("books.id desc").paginate(:page => params[:page])
97
+ ```
98
+
99
+ ## Fulltext index capabilities
100
+
101
+ By default, i.e. if you don't tell AttrSearchable about your fulltext indices,
102
+ AttrSearchable will use `LIKE '%...%'` queries. Unfortunately, unless you
103
+ create a [trigram index](http://www.postgresql.org/docs/9.1/static/pgtrgm.html)
104
+ (postgres only), theses queries can not use SQL indices, such that every row
105
+ needs to be scanned by your RDBMS when you search for `Book.search("Harry
106
+ Potter")` or similar, which is btw. usually ok for small data sets and a small
107
+ amount of regular queries. Contrary, when you search for
108
+ `Book.search("title=Potter")` indices can and will be used. Moreover, other
109
+ indices (on price, stock, etc) will of course be used by your RDBMS when you
110
+ search for `Book.search("stock > 0")`, etc.
111
+
112
+ Regarding the `LIKE` penalty, the easiest way to make them use indices is
113
+ to remove the left wildcard. AttrSearchble supports this via:
114
+
115
+ ```ruby
116
+ class Book < ActiveRecord::Base
117
+ # ...
118
+
119
+ attr_searchable_options, :title, :left_wildcard => false
120
+
121
+ # ...
122
+ end
123
+ ```
124
+
125
+ However, this is often not desirable. Therefore, AttrSearchable can exploit the
126
+ fulltext index capabilities of MySQL and PostgreSQL. To use already existing
127
+ fulltext indices, simply tell AttrSearchable to use them via:
128
+
129
+ ```ruby
130
+ class Book < ActiveRecord::Base
131
+ # ...
132
+
133
+ attr_searchable_options :title, :type => :fulltext
134
+ attr_searchable_options :author, :type => :fulltext
135
+
136
+ # ...
137
+ end
138
+ ```
139
+
140
+ AttrSearchable will then transparently change its SQL queries for the
141
+ attributes having fulltext indices to:
142
+
143
+ ```ruby
144
+ Book.search("Harry Potter")
145
+ # MySQL: ... WHERE MATCH(books.title) AGAINST('+Harry +Potter' IN BOOLEAN MODE) OR MATCH(books.author) AGAINST('+Harry +Potter' IN BOOLEAN MODE)
146
+ # PostgreSQL: ... WHERE to_tsvector('simple', books.title) @@ to_tsquery('simple', 'Harry & Potter') OR to_tsvector('simple', books.author) @@ to_tsquery('simple', 'Harry & Potter')
147
+ ```
148
+
149
+ Obviously, theses queries won't always return the same results as wildcard
150
+ `LIKE` queries, because we search for words instead of substrings. However,
151
+ fulltext indices will usually of course provide better performance.
152
+
153
+ Moreover, the query above is not yet perfect. To improve it even more,
154
+ AttrSearchable tries to optimize the queries to make optimal use of fulltext
155
+ indices while still allowing to mix them with non-fulltext attributes. To
156
+ improve queries, the first thing you want to do is to specify a default field
157
+ to search in, such that AttrSearchable must no longer search within all fields:
158
+
159
+ ```ruby
160
+ attr_searchable :all => [:author, :title]
161
+ attr_searchable_options :all, :type => :fulltext, :default => true
162
+ ```
163
+
164
+ Now AttrSearchable can optimize the following, not yet optimal query:
165
+
166
+ ```ruby
167
+ BookSearch("Rowling OR Tolkien stock > 1")
168
+ # MySQL: ... WHERE (MATCH(books.author) AGAINST('+Rowling' IN BOOLEAN MODE) OR MATCH(books.title) AGAINST('+Tolkien' IN BOOLEAN MODE)) AND books.stock > 1
169
+ # PostgreSQL: ... WHERE (to_tsvector('simple', books.author) @@ to_tsquery('simple', 'Rowling') OR to_tsvector('simple', books.title) @@ to_tsquery('simple', 'Tolkien')) AND books.stock > 1
170
+ ```
171
+
172
+ to the following, more performant query:
173
+
174
+ ```ruby
175
+ BookSearch("Rowling OR Tolkien stock > 1")
176
+ # MySQL: ... WHERE MATCH(books.author, books.title) AGAINST('Rowling Tolkien' IN BOOLEAN MODE) AND books.stock > 1
177
+ # PostgreSQL: ... WHERE to_tsvector('simple', books.author || ' ' || books.title) @@ to_tsquery('simple', 'Rowling | Tokien') and books.stock > 1
178
+ ```
179
+
180
+ Other queries will be optimized in a similar way, such that AttrSearchable
181
+ tries to minimize the fultext constraints within a query, namely `MATCH()
182
+ AGAINST()` for MySQL and `to_tsvector() @@ to_tsquery()` for PostgreSQL.
183
+
184
+ ```ruby
185
+ BookSearch("(Rowling -Potter) OR Tolkien")
186
+ # MySQL: ... WHERE MATCH(books.author, books.title) AGAINST('(+Rowling -Potter) Tolkien' IN BOOLEAN MODE)
187
+ # PostgreSQL: ... WHERE to_tsvector('simple', books.author || ' ' || books.title) @@ to_tsquery('simple', '(Rowling & !Potter) | Tolkien')
188
+ ```
189
+
190
+ To create a fulltext index on `books.title` in MySQL, simply use:
191
+
192
+ ```ruby
193
+ add_index :books, :title, :type => :fulltext
194
+ ```
195
+
196
+ Regarding compound indices, which will e.g. be used for the default field `all`
197
+ we already specified above, use:
198
+
199
+ ```ruby
200
+ add_index :books, [:author, :title], :type => :fulltext
201
+ ```
202
+
203
+ Please note that MySQL supports fulltext indices for MyISAM and, as of MySQL
204
+ version 5.6+, for InnoDB as well. For more details about MySQL fulltext indices
205
+ visit
206
+ [http://dev.mysql.com/doc/refman/5.6/en/fulltext-search.html](http://dev.mysql.com/doc/refman/5.6/en/fulltext-search.html)
207
+
208
+ Regarding PostgreSQL there are more ways to create a fulltext index. However,
209
+ one of the easiest ways is:
210
+
211
+ ```ruby
212
+ ActiveRecord::Base.connection.execute "CREATE INDEX fulltext_index_books_on_title ON books USING GIN(to_tsvector('simple', title))"
213
+ ```
214
+
215
+ Regarding compound indices for PostgreSQL, use:
216
+
217
+ ```ruby
218
+ ActiveRecord::Base.connection.execute "CREATE INDEX fulltext_index_books_on_title ON books USING GIN(to_tsvector('simple', author || ' ' || title))"
219
+ ```
220
+
221
+ To use another PostgreSQL dictionary than `simple`, you have to create the
222
+ index accordingly and you need tell AttrSearchable about it, e.g.:
223
+
224
+ ```ruby
225
+ attr_searchable_options :title, :dictionary => "english"
226
+ ```
227
+
228
+ For more details about PostgreSQL fulltext indices visit
229
+ [http://www.postgresql.org/docs/9.3/static/textsearch.html](http://www.postgresql.org/docs/9.3/static/textsearch.html)
230
+
231
+ ## Associations
232
+
233
+ If you specify searchable attributes from another model, like
234
+
235
+ ```ruby
236
+ class Book < ActiveRecord::Base
237
+ # ...
238
+
239
+ attr_searchable :author => "author.name"
240
+
241
+ # ...
242
+ end
243
+ ```
244
+
245
+ AttrSearchable will by default `eager_load` these associations, when you
246
+ perform `Book.search(...)`. If you don't want that or need to perform special
247
+ operations, define a `search_scope` within your model:
248
+
249
+ ```ruby
250
+ class Book < ActiveRecord::Base
251
+ # ...
252
+
253
+ scope :search_scope, lambda { joins(:author).eager_load(:comments) } # etc.
254
+
255
+ # ...
256
+ end
257
+ ```
258
+
259
+ AttrSearchable will then skip any association auto loading and will use
260
+ the `search_scope` instead.
261
+
262
+ ## Supported operators
263
+
264
+ Query string queries support `AND/and`, `OR/or`, `:`, `=`, `!=`, `<`, `<=`,
265
+ `>`, `>=`, `NOT/not/-`, `()`, `"..."` and `'...'`. Default operators are `AND`
266
+ and `matches`, `OR` has precedence over `AND`. `NOT` can only be used as infix
267
+ operator regarding a single attribute.
268
+
269
+ Hash based queries support `:and => [...]` and `:or => [...]`, which take an array
270
+ of `:not => {...}`, `:matches => {...}`, `:eq => {...}`, `:not_eq => {...}`,
271
+ `:lt => {...}`, `:lteq => {...}`, `gt => {...}`, `:gteq => {...}` and `:query => "..."`
272
+ arguments. Moreover, `:query => "..."` makes it possible to create sub-queries.
273
+ The other rules for query string queries apply to hash based queries as well.
274
+
275
+ ## Mapping
276
+
277
+ When searching in boolean, datetime, timestamp, etc. fields, AttrSearchable
278
+ performs some mapping. The following queries are equivalent:
279
+
280
+ ```ruby
281
+ Book.search("available:true")
282
+ Book.search("available:1")
283
+ Book.search("available:yes")
284
+ ```
285
+
286
+ as well as
287
+
288
+ ```ruby
289
+ Book.search("available:false")
290
+ Book.search("available:0")
291
+ Book.search("available:no")
292
+ ```
293
+
294
+ For datetime and timestamp fields, AttrSearchable expands certain values to
295
+ ranges:
296
+
297
+ ```ruby
298
+ Book.search("created_at:2014")
299
+ # ... WHERE created_at >= '2014-01-01 00:00:00' AND created_at <= '2014-12-31 23:59:59'
300
+
301
+ Book.search("created_at:2014-06")
302
+ # ... WHERE created_at >= '2014-06-01 00:00:00' AND created_at <= '2014-06-30 23:59:59'
303
+
304
+ Book.search("created_at:2014-06-15")
305
+ # ... WHERE created_at >= '2014-06-15 00:00:00' AND created_at <= '2014-06-15 23:59:59'
306
+ ```
307
+
308
+ ## Chaining
309
+
310
+ Chaining of searches is possible. However, chaining does currently not allow
311
+ AttrSearchable to optimize the individual queries for fulltext indices.
312
+
313
+ ```ruby
314
+ Book.search("Harry").search("Potter")
315
+ ```
316
+
317
+ will generate
318
+
319
+ ```ruby
320
+ # MySQL: ... WHERE MATCH(...) AGAINST('+Harry' IN BOOLEAN MODE) AND MATCH(...) AGAINST('+Potter' IN BOOLEAN MODE)
321
+ # PostgreSQL: ... WHERE to_tsvector(...) @@ to_tsquery('simple', 'Harry') AND to_tsvector(...) @@ to_tsquery('simple', 'Potter')
322
+ ```
323
+
324
+ instead of
325
+
326
+ ```ruby
327
+ # MySQL: ... WHERE MATCH(...) AGAINST('+Harry +Potter' IN BOOLEAN MODE)
328
+ # PostgreSQL: ... WHERE to_tsvector(...) @@ to_tsquery('simple', 'Harry & Potter')
329
+ ```
330
+
331
+ Thus, if you use fulltext indices, you better avoid chaining.
332
+
333
+ ## Contributing
334
+
335
+ 1. Fork it
336
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
337
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
338
+ 4. Push to the branch (`git push origin my-new-feature`)
339
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,9 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "lib"
6
+ t.pattern = "test/**/*_test.rb"
7
+ t.verbose = true
8
+ end
9
+
@@ -0,0 +1,29 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'attr_searchable/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "attr_searchable"
8
+ spec.version = AttrSearchable::VERSION
9
+ spec.authors = ["Benjamin Vetter"]
10
+ spec.email = ["vetter@flakks.com"]
11
+ spec.description = %q{Complex search-engine like query support for activerecord}
12
+ spec.summary = %q{Easily perform complex search-engine like queries on your activerecord models}
13
+ spec.homepage = "https://github.com/mrkamel/attr_searchable"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency "treetop"
22
+
23
+ spec.add_development_dependency "bundler", "~> 1.3"
24
+ spec.add_development_dependency "rake"
25
+ spec.add_development_dependency "activerecord", ">= 3.0.0"
26
+ spec.add_development_dependency "factory_girl"
27
+ spec.add_development_dependency "appraisal"
28
+ spec.add_development_dependency "minitest"
29
+ end
@@ -0,0 +1,26 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "activerecord", "~> 3.2.18"
6
+ gem "attr_searchable", :path => "../"
7
+
8
+ platforms :jruby do
9
+ gem "activerecord-jdbcmysql-adapter"
10
+ gem "activerecord-jdbcsqlite3-adapter"
11
+ gem "activerecord-jdbcpostgresql-adapter"
12
+ end
13
+
14
+ platforms :ruby do
15
+ gem "sqlite3"
16
+ gem "mysql2"
17
+ gem "pg"
18
+ end
19
+
20
+ platforms :rbx do
21
+ gem "racc"
22
+ gem "rubysl", "~> 2.0"
23
+ gem "psych"
24
+ end
25
+
26
+ gemspec :path => "../"
@@ -0,0 +1,26 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "activerecord", "~> 4.0.0"
6
+ gem "attr_searchable", :path => "../"
7
+
8
+ platforms :jruby do
9
+ gem "activerecord-jdbcmysql-adapter"
10
+ gem "activerecord-jdbcsqlite3-adapter"
11
+ gem "activerecord-jdbcpostgresql-adapter"
12
+ end
13
+
14
+ platforms :ruby do
15
+ gem "sqlite3"
16
+ gem "mysql2"
17
+ gem "pg"
18
+ end
19
+
20
+ platforms :rbx do
21
+ gem "racc"
22
+ gem "rubysl", "~> 2.0"
23
+ gem "psych"
24
+ end
25
+
26
+ gemspec :path => "../"
@@ -0,0 +1,26 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "activerecord", "~> 4.1.0.beta"
6
+ gem "attr_searchable", :path => "../"
7
+
8
+ platforms :jruby do
9
+ gem "activerecord-jdbcmysql-adapter"
10
+ gem "activerecord-jdbcsqlite3-adapter"
11
+ gem "activerecord-jdbcpostgresql-adapter"
12
+ end
13
+
14
+ platforms :ruby do
15
+ gem "sqlite3"
16
+ gem "mysql2"
17
+ gem "pg"
18
+ end
19
+
20
+ platforms :rbx do
21
+ gem "racc"
22
+ gem "rubysl", "~> 2.0"
23
+ gem "psych"
24
+ end
25
+
26
+ gemspec :path => "../"