search_cop 1.0.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 +18 -0
- data/.travis.yml +33 -0
- data/Appraisals +14 -0
- data/Gemfile +23 -0
- data/LICENSE.txt +22 -0
- data/MIGRATION.md +66 -0
- data/README.md +530 -0
- data/Rakefile +9 -0
- data/gemfiles/3.2.gemfile +26 -0
- data/gemfiles/4.0.gemfile +26 -0
- data/gemfiles/4.1.gemfile +26 -0
- data/lib/search_cop/arel/visitors.rb +223 -0
- data/lib/search_cop/arel.rb +4 -0
- data/lib/search_cop/grammar_parser.rb +22 -0
- data/lib/search_cop/hash_parser.rb +42 -0
- data/lib/search_cop/query_builder.rb +26 -0
- data/lib/search_cop/query_info.rb +13 -0
- data/lib/search_cop/search_scope.rb +62 -0
- data/lib/search_cop/version.rb +3 -0
- data/lib/search_cop.rb +71 -0
- data/lib/search_cop_grammar/attributes.rb +229 -0
- data/lib/search_cop_grammar/nodes.rb +183 -0
- data/lib/search_cop_grammar.rb +133 -0
- data/lib/search_cop_grammar.treetop +55 -0
- data/search_cop.gemspec +29 -0
- data/test/and_test.rb +27 -0
- data/test/boolean_test.rb +53 -0
- data/test/database.yml +17 -0
- data/test/date_test.rb +75 -0
- data/test/datetime_test.rb +76 -0
- data/test/error_test.rb +17 -0
- data/test/float_test.rb +67 -0
- data/test/fulltext_test.rb +27 -0
- data/test/hash_test.rb +97 -0
- data/test/integer_test.rb +67 -0
- data/test/not_test.rb +27 -0
- data/test/or_test.rb +29 -0
- data/test/scope_test.rb +35 -0
- data/test/search_cop_test.rb +131 -0
- data/test/string_test.rb +84 -0
- data/test/test_helper.rb +150 -0
- metadata +216 -0
data/README.md
ADDED
@@ -0,0 +1,530 @@
|
|
1
|
+
# SearchCop
|
2
|
+
|
3
|
+
[](http://travis-ci.org/mrkamel/search_cop)
|
4
|
+
[](https://codeclimate.com/github/mrkamel/search_cop)
|
5
|
+
[](https://gemnasium.com/mrkamel/search_cop)
|
6
|
+
[](http://badge.fury.io/rb/search_cop)
|
7
|
+
|
8
|
+

|
9
|
+
|
10
|
+
SearchCop extends your ActiveRecord models to support fulltext search
|
11
|
+
engine like queries via simple query strings and hash-based queries. Assume you
|
12
|
+
have a `Book` model having various attributes like `title`, `author`, `stock`,
|
13
|
+
`price`, `available`. Using SearchCop you can perform:
|
14
|
+
|
15
|
+
```ruby
|
16
|
+
Book.search("Joanne Rowling Harry Potter")
|
17
|
+
Book.search("author: Rowling title:'Harry Potter'")
|
18
|
+
Book.search("price > 10 AND price < 20 -stock:0 (Potter OR Rowling)")
|
19
|
+
# ...
|
20
|
+
```
|
21
|
+
|
22
|
+
Thus, you can hand out a search query string to your models and you, your app's
|
23
|
+
admins and/or users will get powerful query features without the need for
|
24
|
+
integrating additional third party search servers, since SearchCop can use
|
25
|
+
fulltext index capabilities of your RDBMS in a database agnostic way (currently
|
26
|
+
MySQL and PostgreSQL fulltext indices are supported) and optimizes the queries
|
27
|
+
to make optimal use of them. Read more below.
|
28
|
+
|
29
|
+
Complex hash-based queries are supported as well:
|
30
|
+
|
31
|
+
```ruby
|
32
|
+
Book.search(:author => "Rowling", :title => "Harry Potter")
|
33
|
+
Book.search(:or => [{:author => "Rowling"}, {:author => "Tolkien"}])
|
34
|
+
Book.search(:and => [{:price => {:gt => 10}}, {:not => {:stock => 0}}, :or => [{:title => "Potter"}, {:author => "Rowling"}]])
|
35
|
+
Book.search(:or => [{:query => "Rowling -Potter"}, {:query => "Tolkien -Rings"}])
|
36
|
+
# ...
|
37
|
+
```
|
38
|
+
|
39
|
+
## AttrSearchable is now SearchCop
|
40
|
+
|
41
|
+
As the set of features of AttrSearchable grew and grew, it has been neccessary
|
42
|
+
to change its DSL and name, as no `attr_searchable` method is present anymore.
|
43
|
+
The new DSL is cleaner and more concise. Morever, the migration process is
|
44
|
+
simple. Please take a look into the migration guide
|
45
|
+
[MIGRATION.md](https://github.com/mrkamel/search_cop/MIGRATION.md)
|
46
|
+
|
47
|
+
## Installation
|
48
|
+
|
49
|
+
For Rails/ActiveRecord 3 (or 4), add this line to your application's Gemfile:
|
50
|
+
|
51
|
+
gem 'search_cop'
|
52
|
+
|
53
|
+
And then execute:
|
54
|
+
|
55
|
+
$ bundle
|
56
|
+
|
57
|
+
Or install it yourself as:
|
58
|
+
|
59
|
+
$ gem install search_cop
|
60
|
+
|
61
|
+
## Usage
|
62
|
+
|
63
|
+
To enable SearchCop for a model, `include SearchCop` and specify the
|
64
|
+
attributes you want to expose to search queries within a `search_scope`:
|
65
|
+
|
66
|
+
```ruby
|
67
|
+
class Book < ActiveRecord::Base
|
68
|
+
include SearchCop
|
69
|
+
|
70
|
+
search_scope :search do
|
71
|
+
attributes :title, :description, :stock, :price, :created_at, :available
|
72
|
+
attributes :comment => ["comments.title", "comments.message"]
|
73
|
+
attributes :author => "author.name"
|
74
|
+
# ...
|
75
|
+
end
|
76
|
+
|
77
|
+
has_many :comments
|
78
|
+
belongs_to :author
|
79
|
+
end
|
80
|
+
```
|
81
|
+
|
82
|
+
## How does it work
|
83
|
+
|
84
|
+
SearchCop parses the query and maps it to an SQL Query using Arel.
|
85
|
+
Thus, SearchCop is not bound to a specific RDBMS.
|
86
|
+
|
87
|
+
```ruby
|
88
|
+
Book.search("stock > 0")
|
89
|
+
# ... WHERE books.stock > 0
|
90
|
+
|
91
|
+
Book.search("price > 10 stock > 0")
|
92
|
+
# ... WHERE books.price > 10 AND books.stock > 0
|
93
|
+
|
94
|
+
Book.search("Harry Potter")
|
95
|
+
# ... WHERE (books.title LIKE '%Harry%' OR books.description LIKE '%Harry%' OR ...) AND (books.title LIKE '%Potter%' OR books.description LIKE '%Potter%' ...)
|
96
|
+
|
97
|
+
Book.search("available:yes OR created_at:2014")
|
98
|
+
# ... WHERE books.available = 1 OR (books.created_at >= '2014-01-01 00:00:00' and books.created_at <= '2014-12-31 00:00:00')
|
99
|
+
```
|
100
|
+
|
101
|
+
Of course, these `LIKE '%...%'` queries won't achieve optimal performance, but
|
102
|
+
check out the section below on SearchCop's fulltext capabilities to
|
103
|
+
understand how the resulting queries can be optimized.
|
104
|
+
|
105
|
+
As `Book.search(...)` returns an `ActiveRecord::Relation`, you are free to pre-
|
106
|
+
or post-process the search results in every possible way:
|
107
|
+
|
108
|
+
```ruby
|
109
|
+
Book.where(:available => true).search("Harry Potter").order("books.id desc").paginate(:page => params[:page])
|
110
|
+
```
|
111
|
+
|
112
|
+
## Fulltext index capabilities
|
113
|
+
|
114
|
+
By default, i.e. if you don't tell SearchCop about your fulltext indices,
|
115
|
+
SearchCop will use `LIKE '%...%'` queries. Unfortunately, unless you
|
116
|
+
create a [trigram index](http://www.postgresql.org/docs/9.1/static/pgtrgm.html)
|
117
|
+
(postgres only), theses queries can not use SQL indices, such that every row
|
118
|
+
needs to be scanned by your RDBMS when you search for `Book.search("Harry
|
119
|
+
Potter")` or similar. To avoid the penalty of `LIKE` queries, SearchCop
|
120
|
+
can exploit the fulltext index capabilities of MySQL and PostgreSQL. To use
|
121
|
+
already existing fulltext indices, simply tell SearchCop to use them via:
|
122
|
+
|
123
|
+
```ruby
|
124
|
+
class Book < ActiveRecord::Base
|
125
|
+
# ...
|
126
|
+
|
127
|
+
search_scope :search do
|
128
|
+
attributes :title, :author
|
129
|
+
|
130
|
+
options :title, :type => :fulltext
|
131
|
+
options :author, :type => :fulltext
|
132
|
+
end
|
133
|
+
|
134
|
+
# ...
|
135
|
+
end
|
136
|
+
```
|
137
|
+
|
138
|
+
SearchCop will then transparently change its SQL queries for the
|
139
|
+
attributes having fulltext indices to:
|
140
|
+
|
141
|
+
```ruby
|
142
|
+
Book.search("Harry Potter")
|
143
|
+
# MySQL: ... WHERE (MATCH(books.title) AGAINST('+Harry' IN BOOLEAN MODE) OR MATCH(books.author) AGAINST('+Harry' IN BOOLEAN MODE)) AND (MATCH(books.title) AGAINST ('+Potter' IN BOOLEAN MODE) OR MATCH(books.author) AGAINST('+Potter' IN BOOLEAN MODE))
|
144
|
+
# PostgreSQL: ... WHERE (to_tsvector('simple', books.title) @@ to_tsquery('simple', 'Harry') OR to_tsvector('simple', books.author) @@ to_tsquery('simple', 'Harry')) AND (to_tsvector('simple', books.title) @@ to_tsquery('simple', 'Potter') OR to_tsvector('simple', books.author) @@ to_tsquery('simple', 'Potter'))
|
145
|
+
```
|
146
|
+
|
147
|
+
Obviously, theses queries won't always return the same results as wildcard
|
148
|
+
`LIKE` queries, because we search for words instead of sub-strings. However,
|
149
|
+
fulltext indices will usually of course provide better performance.
|
150
|
+
|
151
|
+
Moreover, the query above is not yet perfect. To improve it even more,
|
152
|
+
SearchCop tries to optimize the queries to make optimal use of fulltext
|
153
|
+
indices while still allowing to mix them with non-fulltext attributes. To
|
154
|
+
improve queries even more, you can group attributes via aliases and specify a
|
155
|
+
default field to search in, such that SearchCop must no longer search
|
156
|
+
within all fields:
|
157
|
+
|
158
|
+
```ruby
|
159
|
+
search_scope :search do
|
160
|
+
attributes :all => [:author, :title]
|
161
|
+
|
162
|
+
options :all, :type => :fulltext, :default => true
|
163
|
+
|
164
|
+
# Use :default => true to explicitly enable fields as default fields (whitelist approach)
|
165
|
+
# Use :default => false to explicitly disable fields as default fields (blacklist approach)
|
166
|
+
end
|
167
|
+
```
|
168
|
+
|
169
|
+
Now SearchCop can optimize the following, not yet optimal query:
|
170
|
+
|
171
|
+
```ruby
|
172
|
+
Book.search("Rowling OR Tolkien stock > 1")
|
173
|
+
# MySQL: ... WHERE ((MATCH(books.author) AGAINST('+Rowling' IN BOOLEAN MODE) OR MATCH(books.title) AGAINST('+Rowling' IN BOOLEAN MODE)) OR (MATCH(books.author) AGAINST('+Tolkien' IN BOOLEAN MODE) OR MATCH(books.title) AGAINST('+Tolkien' IN BOOLEAN MODE))) AND books.stock > 1
|
174
|
+
# PostgreSQL: ... WHERE ((to_tsvector('simple', books.author) @@ to_tsquery('simple', 'Rowling') OR to_tsvector('simple', books.title) @@ to_tsquery('simple', 'Rowling')) OR (to_tsvector('simple', books.author) @@ to_tsquery('simple', 'Tolkien') OR to_tsvector('simple', books.title) @@ to_tsquery('simple', 'Tolkien'))) AND books.stock > 1
|
175
|
+
```
|
176
|
+
|
177
|
+
to the following, more performant query:
|
178
|
+
|
179
|
+
|
180
|
+
```ruby
|
181
|
+
Book.search("Rowling OR Tolkien stock > 1")
|
182
|
+
# MySQL: ... WHERE MATCH(books.author, books.title) AGAINST('Rowling Tolkien' IN BOOLEAN MODE) AND books.stock > 1
|
183
|
+
# PostgreSQL: ... WHERE to_tsvector('simple', books.author || ' ' || books.title) @@ to_tsquery('simple', 'Rowling | Tokien') and books.stock > 1
|
184
|
+
```
|
185
|
+
|
186
|
+
What is happening here? Well, we specified `all` as an alias that consists of
|
187
|
+
`author` and `title`. As we, in addition, specified `all` to be a fulltext
|
188
|
+
attribute, SearchCop assumes there is a compound fulltext index present on
|
189
|
+
`author` and `title`, such that the query is optimized accordingly. Finally, we
|
190
|
+
specified `all` to be the default attribute to search in, such that
|
191
|
+
SearchCop can ignore other attributes, like e.g. `stock`, as long as they
|
192
|
+
are not specified within queries directly (like for `stock > 0`).
|
193
|
+
|
194
|
+
Other queries will be optimized in a similar way, such that SearchCop
|
195
|
+
tries to minimize the fultext constraints within a query, namely `MATCH()
|
196
|
+
AGAINST()` for MySQL and `to_tsvector() @@ to_tsquery()` for PostgreSQL.
|
197
|
+
|
198
|
+
```ruby
|
199
|
+
Book.search("(Rowling -Potter) OR Tolkien")
|
200
|
+
# MySQL: ... WHERE MATCH(books.author, books.title) AGAINST('(+Rowling -Potter) Tolkien' IN BOOLEAN MODE)
|
201
|
+
# PostgreSQL: ... WHERE to_tsvector('simple', books.author || ' ' || books.title) @@ to_tsquery('simple', '(Rowling & !Potter) | Tolkien')
|
202
|
+
```
|
203
|
+
|
204
|
+
To create a fulltext index on `books.title` in MySQL, simply use:
|
205
|
+
|
206
|
+
```ruby
|
207
|
+
add_index :books, :title, :type => :fulltext
|
208
|
+
```
|
209
|
+
|
210
|
+
Regarding compound indices, which will e.g. be used for the default field `all`
|
211
|
+
we already specified above, use:
|
212
|
+
|
213
|
+
```ruby
|
214
|
+
add_index :books, [:author, :title], :type => :fulltext
|
215
|
+
```
|
216
|
+
|
217
|
+
Please note that MySQL supports fulltext indices for MyISAM and, as of MySQL
|
218
|
+
version 5.6+, for InnoDB as well. For more details about MySQL fulltext indices
|
219
|
+
visit
|
220
|
+
[http://dev.mysql.com/doc/refman/5.6/en/fulltext-search.html](http://dev.mysql.com/doc/refman/5.6/en/fulltext-search.html)
|
221
|
+
|
222
|
+
Regarding PostgreSQL there are more ways to create a fulltext index. However,
|
223
|
+
one of the easiest ways is:
|
224
|
+
|
225
|
+
```ruby
|
226
|
+
ActiveRecord::Base.connection.execute "CREATE INDEX fulltext_index_books_on_title ON books USING GIN(to_tsvector('simple', title))"
|
227
|
+
```
|
228
|
+
|
229
|
+
Regarding compound indices for PostgreSQL, use:
|
230
|
+
|
231
|
+
```ruby
|
232
|
+
ActiveRecord::Base.connection.execute "CREATE INDEX fulltext_index_books_on_title ON books USING GIN(to_tsvector('simple', author || ' ' || title))"
|
233
|
+
```
|
234
|
+
|
235
|
+
To use another PostgreSQL dictionary than `simple`, you have to create the
|
236
|
+
index accordingly and you need tell SearchCop about it, e.g.:
|
237
|
+
|
238
|
+
```ruby
|
239
|
+
search_scope :search do
|
240
|
+
attributes :title
|
241
|
+
|
242
|
+
options :title, :type => :fulltext, :dictionary => "english"
|
243
|
+
end
|
244
|
+
```
|
245
|
+
|
246
|
+
For more details about PostgreSQL fulltext indices visit
|
247
|
+
[http://www.postgresql.org/docs/9.3/static/textsearch.html](http://www.postgresql.org/docs/9.3/static/textsearch.html)
|
248
|
+
|
249
|
+
## Other indices
|
250
|
+
|
251
|
+
In case you expose non-fulltext attributes to search queries (price, stock,
|
252
|
+
etc.), the respective queries, like `Book.search("stock > 0")`, will profit
|
253
|
+
from from the usual non-fulltext indices. Thus, you should add a usual index on
|
254
|
+
every column you expose to search queries plus a fulltext index for every
|
255
|
+
fulltext attribute.
|
256
|
+
|
257
|
+
In case you can't use fulltext indices, because you're e.g. still on MySQL 5.5
|
258
|
+
while using InnoDB or another RDBMS without fulltext support, you can make your
|
259
|
+
RDBMS use usual non-fulltext indices for string columns if you don't need the
|
260
|
+
left wildcard within `LIKE` queries. Simply supply the following option:
|
261
|
+
|
262
|
+
```ruby
|
263
|
+
class User < ActiveRecord::Base
|
264
|
+
include SearchCop
|
265
|
+
|
266
|
+
search_scope :search do
|
267
|
+
attributes :username
|
268
|
+
|
269
|
+
options :username, :left_wildcard => false
|
270
|
+
end
|
271
|
+
|
272
|
+
# ...
|
273
|
+
```
|
274
|
+
|
275
|
+
such that SearchCop will omit the left most wildcard.
|
276
|
+
|
277
|
+
```ruby
|
278
|
+
User.search("admin")
|
279
|
+
# ... WHERE users.username LIKE 'admin%'
|
280
|
+
```
|
281
|
+
|
282
|
+
## Associations
|
283
|
+
|
284
|
+
If you specify searchable attributes from another model, like
|
285
|
+
|
286
|
+
```ruby
|
287
|
+
class Book < ActiveRecord::Base
|
288
|
+
# ...
|
289
|
+
|
290
|
+
belongs_to :author
|
291
|
+
|
292
|
+
search_scope :search do
|
293
|
+
attributes :author => "author.name"
|
294
|
+
end
|
295
|
+
|
296
|
+
# ...
|
297
|
+
end
|
298
|
+
```
|
299
|
+
|
300
|
+
SearchCop will by default `eager_load` the referenced associations, when
|
301
|
+
you perform `Book.search(...)`. If you don't want the automatic `eager_load`
|
302
|
+
or need to perform special operations, specify a `scope`:
|
303
|
+
|
304
|
+
```ruby
|
305
|
+
class Book < ActiveRecord::Base
|
306
|
+
# ...
|
307
|
+
|
308
|
+
search_scope :search do
|
309
|
+
# ...
|
310
|
+
|
311
|
+
scope { joins(:author).eager_load(:comments) } # etc.
|
312
|
+
end
|
313
|
+
|
314
|
+
# ...
|
315
|
+
end
|
316
|
+
```
|
317
|
+
|
318
|
+
SearchCop will then skip any association auto loading and will use the
|
319
|
+
scope instead. Assocations of associations can as well be referenced
|
320
|
+
and used:
|
321
|
+
|
322
|
+
```ruby
|
323
|
+
class Book < ActiveRecord::Base
|
324
|
+
# ...
|
325
|
+
|
326
|
+
has_many :comments
|
327
|
+
has_many :users, :through => :comments
|
328
|
+
|
329
|
+
search_scope :search do
|
330
|
+
attributes :user => "users.username"
|
331
|
+
end
|
332
|
+
|
333
|
+
# ...
|
334
|
+
end
|
335
|
+
```
|
336
|
+
|
337
|
+
## Custom table names and associations
|
338
|
+
|
339
|
+
SearchCop tries to infer a model's class name and SQL alias from the
|
340
|
+
specified attributes to autodetect datatype definitions, etc. This usually
|
341
|
+
works quite fine. In case you're using custom table names via `self.table_name
|
342
|
+
= ...` or if a model is associated multiple times, SearchCop however can't
|
343
|
+
infer the class and alias names, e.g.
|
344
|
+
|
345
|
+
```ruby
|
346
|
+
class Book < ActiveRecord::Base
|
347
|
+
# ...
|
348
|
+
|
349
|
+
has_many :users, :through => :comments
|
350
|
+
belongs_to :user
|
351
|
+
|
352
|
+
search_scope :search do
|
353
|
+
attributes :user => ["users.username", "users_books.username"]
|
354
|
+
end
|
355
|
+
|
356
|
+
# ...
|
357
|
+
end
|
358
|
+
```
|
359
|
+
|
360
|
+
Here, for queries to work you have to use `users_books.username`, because
|
361
|
+
ActiveRecord assigns a different SQL alias for users within its SQL queries,
|
362
|
+
because the user model is associated multiple times. However, as SearchCop
|
363
|
+
now can't infer the `User` model from `users_books`, you have to add:
|
364
|
+
|
365
|
+
```ruby
|
366
|
+
class Book < ActiveRecord::Base
|
367
|
+
# ...
|
368
|
+
|
369
|
+
search_scope :search do
|
370
|
+
# ...
|
371
|
+
|
372
|
+
aliases :users_books => :user
|
373
|
+
end
|
374
|
+
|
375
|
+
# ...
|
376
|
+
end
|
377
|
+
```
|
378
|
+
|
379
|
+
to tell SearchCop about the custom SQL alias and mapping.
|
380
|
+
|
381
|
+
## Supported operators
|
382
|
+
|
383
|
+
Query string queries support `AND/and`, `OR/or`, `:`, `=`, `!=`, `<`, `<=`,
|
384
|
+
`>`, `>=`, `NOT/not/-`, `()`, `"..."` and `'...'`. Default operators are `AND`
|
385
|
+
and `matches`, `OR` has precedence over `AND`. `NOT` can only be used as infix
|
386
|
+
operator regarding a single attribute.
|
387
|
+
|
388
|
+
Hash based queries support `:and => [...]` and `:or => [...]`, which take an array
|
389
|
+
of `:not => {...}`, `:matches => {...}`, `:eq => {...}`, `:not_eq => {...}`,
|
390
|
+
`:lt => {...}`, `:lteq => {...}`, `gt => {...}`, `:gteq => {...}` and `:query => "..."`
|
391
|
+
arguments. Moreover, `:query => "..."` makes it possible to create sub-queries.
|
392
|
+
The other rules for query string queries apply to hash based queries as well.
|
393
|
+
|
394
|
+
## Mapping
|
395
|
+
|
396
|
+
When searching in boolean, datetime, timestamp, etc. fields, SearchCop
|
397
|
+
performs some mapping. The following queries are equivalent:
|
398
|
+
|
399
|
+
```ruby
|
400
|
+
Book.search("available:true")
|
401
|
+
Book.search("available:1")
|
402
|
+
Book.search("available:yes")
|
403
|
+
```
|
404
|
+
|
405
|
+
as well as
|
406
|
+
|
407
|
+
```ruby
|
408
|
+
Book.search("available:false")
|
409
|
+
Book.search("available:0")
|
410
|
+
Book.search("available:no")
|
411
|
+
```
|
412
|
+
|
413
|
+
For datetime and timestamp fields, SearchCop expands certain values to
|
414
|
+
ranges:
|
415
|
+
|
416
|
+
```ruby
|
417
|
+
Book.search("created_at:2014")
|
418
|
+
# ... WHERE created_at >= '2014-01-01 00:00:00' AND created_at <= '2014-12-31 23:59:59'
|
419
|
+
|
420
|
+
Book.search("created_at:2014-06")
|
421
|
+
# ... WHERE created_at >= '2014-06-01 00:00:00' AND created_at <= '2014-06-30 23:59:59'
|
422
|
+
|
423
|
+
Book.search("created_at:2014-06-15")
|
424
|
+
# ... WHERE created_at >= '2014-06-15 00:00:00' AND created_at <= '2014-06-15 23:59:59'
|
425
|
+
```
|
426
|
+
|
427
|
+
## Chaining
|
428
|
+
|
429
|
+
Chaining of searches is possible. However, chaining does currently not allow
|
430
|
+
SearchCop to optimize the individual queries for fulltext indices.
|
431
|
+
|
432
|
+
```ruby
|
433
|
+
Book.search("Harry").search("Potter")
|
434
|
+
```
|
435
|
+
|
436
|
+
will generate
|
437
|
+
|
438
|
+
```ruby
|
439
|
+
# MySQL: ... WHERE MATCH(...) AGAINST('+Harry' IN BOOLEAN MODE) AND MATCH(...) AGAINST('+Potter' IN BOOLEAN MODE)
|
440
|
+
# PostgreSQL: ... WHERE to_tsvector(...) @@ to_tsquery('simple', 'Harry') AND to_tsvector(...) @@ to_tsquery('simple', 'Potter')
|
441
|
+
```
|
442
|
+
|
443
|
+
instead of
|
444
|
+
|
445
|
+
```ruby
|
446
|
+
# MySQL: ... WHERE MATCH(...) AGAINST('+Harry +Potter' IN BOOLEAN MODE)
|
447
|
+
# PostgreSQL: ... WHERE to_tsvector(...) @@ to_tsquery('simple', 'Harry & Potter')
|
448
|
+
```
|
449
|
+
|
450
|
+
Thus, if you use fulltext indices, you better avoid chaining.
|
451
|
+
|
452
|
+
## Debugging
|
453
|
+
|
454
|
+
When using `Model#search`, SearchCop conveniently prevents certain
|
455
|
+
exceptions from being raised in case the query string passed to it is invalid
|
456
|
+
(parse errors, incompatible datatype errors, etc). Instead, `Model#search`
|
457
|
+
returns an empty relation. However, if you need to debug certain cases, use
|
458
|
+
`Model#unsafe_search`, which will raise them.
|
459
|
+
|
460
|
+
```ruby
|
461
|
+
Book.unsafe_search("stock: None") # => raise SearchCop::IncompatibleDatatype
|
462
|
+
```
|
463
|
+
|
464
|
+
## Reflection
|
465
|
+
|
466
|
+
SearchCop provides reflective methods, namely `#attributes`,
|
467
|
+
`#default_attributes`, `#options` and `#aliases`. You can use these methods to
|
468
|
+
e.g. provide an individual search help widget for your models, that lists the
|
469
|
+
attributes to search in as well as the default ones, etc.
|
470
|
+
|
471
|
+
```ruby
|
472
|
+
class Product < ActiveRecord::Base
|
473
|
+
include SearchCop
|
474
|
+
|
475
|
+
search_scope :search do
|
476
|
+
attributes :title, :description
|
477
|
+
|
478
|
+
options :title, :default => true
|
479
|
+
end
|
480
|
+
end
|
481
|
+
|
482
|
+
Product.search_reflection(:search).attributes
|
483
|
+
# {"title" => ["products.title"], "description" => ["products.description"]}
|
484
|
+
|
485
|
+
Product.search_reflection(:search).default_attributes
|
486
|
+
# {"title" => ["products.title"]}
|
487
|
+
|
488
|
+
# ...
|
489
|
+
```
|
490
|
+
|
491
|
+
## Contributing
|
492
|
+
|
493
|
+
1. Fork it
|
494
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
495
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
496
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
497
|
+
5. Create new Pull Request
|
498
|
+
|
499
|
+
## Changelog
|
500
|
+
|
501
|
+
Version 1.0.0:
|
502
|
+
|
503
|
+
* Project name changed to SearchCop
|
504
|
+
* Scope support added
|
505
|
+
* Multiple DSL changes
|
506
|
+
|
507
|
+
Version 0.0.5:
|
508
|
+
|
509
|
+
* Supporting :default => false
|
510
|
+
* Datetime/Date greater operator fix
|
511
|
+
* Use reflection to find associated models
|
512
|
+
* Providing reflection
|
513
|
+
|
514
|
+
Version 0.0.4:
|
515
|
+
|
516
|
+
* Fixed date attributes
|
517
|
+
* Fail softly for mixed datatype attributes
|
518
|
+
* Support custom table, class and alias names via attr_searchable_alias
|
519
|
+
|
520
|
+
Version 0.0.3:
|
521
|
+
|
522
|
+
* belongs_to association fixes
|
523
|
+
|
524
|
+
Version 0.0.2:
|
525
|
+
|
526
|
+
* Arel abstraction layer added
|
527
|
+
* count() queries resulting in "Cannot visit AttrSearchableGrammar::Nodes..." fixed
|
528
|
+
* Better error messages
|
529
|
+
* Model#unsafe_search added
|
530
|
+
|
data/Rakefile
ADDED
@@ -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 "search_cop", :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 "search_cop", :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 "search_cop", :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 => "../"
|