search_flip 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +18 -0
- data/.travis.yml +34 -0
- data/Gemfile +7 -0
- data/LICENSE.txt +22 -0
- data/README.md +606 -0
- data/Rakefile +9 -0
- data/irb.rb +7 -0
- data/lib/search_flip/aggregatable.rb +69 -0
- data/lib/search_flip/aggregation.rb +57 -0
- data/lib/search_flip/bulk.rb +152 -0
- data/lib/search_flip/config.rb +21 -0
- data/lib/search_flip/criteria.rb +737 -0
- data/lib/search_flip/filterable.rb +240 -0
- data/lib/search_flip/http_client.rb +49 -0
- data/lib/search_flip/index.rb +545 -0
- data/lib/search_flip/json.rb +18 -0
- data/lib/search_flip/model.rb +21 -0
- data/lib/search_flip/post_filterable.rb +252 -0
- data/lib/search_flip/response.rb +319 -0
- data/lib/search_flip/result.rb +12 -0
- data/lib/search_flip/to_json.rb +31 -0
- data/lib/search_flip/version.rb +5 -0
- data/lib/search_flip.rb +82 -0
- data/search_flip.gemspec +35 -0
- data/test/database.yml +4 -0
- data/test/search_flip/aggregation_test.rb +212 -0
- data/test/search_flip/bulk_test.rb +55 -0
- data/test/search_flip/criteria_test.rb +825 -0
- data/test/search_flip/http_client_test.rb +35 -0
- data/test/search_flip/index_test.rb +350 -0
- data/test/search_flip/model_test.rb +39 -0
- data/test/search_flip/response_test.rb +136 -0
- data/test/search_flip/to_json_test.rb +30 -0
- data/test/search_flip_test.rb +26 -0
- data/test/test_helper.rb +243 -0
- metadata +258 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 140f180d3c3e60a2ff681c46294726278de9c105
|
4
|
+
data.tar.gz: 2a39b8a053056167b3bb08a5e74a8ea868516a1a
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: b287350dce24ff80a57d831bc70e58be732793315dd9c0ae5ecebc720ea57b9c91bd1240401b9ec9714eee84536169fc7492eecc1068d70fac58b388ad79dc42
|
7
|
+
data.tar.gz: 685148c002d82f19047a90e80085e0d104ae6e3c0b4b6d81dffc30c221d3a97a611f580b5a6185c97d6c9bd1d6f3e0e65aa7fe53e2ec19a499f653061258952d
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
|
2
|
+
rvm:
|
3
|
+
- 2.1.10
|
4
|
+
- 2.2.5
|
5
|
+
- 2.3.1
|
6
|
+
|
7
|
+
dist: trusty
|
8
|
+
|
9
|
+
jdk:
|
10
|
+
- openjdk8
|
11
|
+
|
12
|
+
env:
|
13
|
+
- ES_VERSION=1
|
14
|
+
- ES_VERSION=2
|
15
|
+
- ES_VERSION=5
|
16
|
+
|
17
|
+
install:
|
18
|
+
- travis_retry bundle install
|
19
|
+
- sh -c "if [ '$ES_VERSION' = '5' ]; then (curl -s https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-5.4.0.tar.gz | tar xz -C /tmp); fi"
|
20
|
+
- sh -c "if [ '$ES_VERSION' = '5' ]; then /tmp/elasticsearch-5.4.0/bin/elasticsearch -d; fi"
|
21
|
+
- sh -c "if [ '$ES_VERSION' = '2' ]; then (curl -s https://download.elastic.co/elasticsearch/release/org/elasticsearch/distribution/tar/elasticsearch/2.4.1/elasticsearch-2.4.1.tar.gz | tar xz -C /tmp); fi"
|
22
|
+
- sh -c "if [ '$ES_VERSION' = '2' ]; then /tmp/elasticsearch-2.4.1/bin/plugin install delete-by-query; fi"
|
23
|
+
- sh -c "if [ '$ES_VERSION' = '2' ]; then /tmp/elasticsearch-2.4.1/bin/elasticsearch -d; fi"
|
24
|
+
- sh -c "if [ '$ES_VERSION' = '1' ]; then (curl -s https://download.elastic.co/elasticsearch/elasticsearch/elasticsearch-1.7.4.tar.gz | tar xz -C /tmp); fi"
|
25
|
+
- sh -c "if [ '$ES_VERSION' = '1' ]; then /tmp/elasticsearch-1.7.4/bin/elasticsearch -d; fi"
|
26
|
+
|
27
|
+
before_script:
|
28
|
+
- sleep 30
|
29
|
+
|
30
|
+
script:
|
31
|
+
- bundle exec rake test
|
32
|
+
|
33
|
+
sudo: false
|
34
|
+
|
data/Gemfile
ADDED
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,606 @@
|
|
1
|
+
|
2
|
+
# SearchFlip
|
3
|
+
|
4
|
+
[![Build Status](https://secure.travis-ci.org/mrkamel/search_flip.png?branch=master)](http://travis-ci.org/mrkamel/search_flip)
|
5
|
+
|
6
|
+
Using SearchFlip it is dead-simple to create index classes that correspond to
|
7
|
+
[ElasticSearch](https://www.elastic.co/) indices and to manipulate, query and
|
8
|
+
aggregate these indices using a chainable, concise, yet powerful DSL. Finally,
|
9
|
+
SearchFlip supports ElasticSearch 1.x, 2.x, 5.x, 6.x. Check section
|
10
|
+
[Feature Support](#feature-support) for version dependent features.
|
11
|
+
|
12
|
+
```ruby
|
13
|
+
CommentIndex.search("hello world", default_field: "title").where(visible: true).aggregate(:user_id).sort(id: "desc")
|
14
|
+
|
15
|
+
CommentIndex.aggregate(:user_id) do |aggregation|
|
16
|
+
aggregation.aggregate(histogram: { date_histogram: { field: "created_at", interval: "month" }})
|
17
|
+
end
|
18
|
+
|
19
|
+
CommentIndex.range(:created_at, gt: Date.today - 1.week, lt: Date.today).where(state: ["approved", "pending"])
|
20
|
+
```
|
21
|
+
|
22
|
+
## Comparison with other gems
|
23
|
+
|
24
|
+
There are great ruby gems to work with Elasticsearch like e.g. searchkick and
|
25
|
+
elasticsearch-ruby already. However, they don't have a chainable API. Compare
|
26
|
+
yourself.
|
27
|
+
|
28
|
+
```ruby
|
29
|
+
# elasticsearch-ruby
|
30
|
+
Comment.search(
|
31
|
+
query: {
|
32
|
+
query_string: {
|
33
|
+
query: "hello world",
|
34
|
+
default_operator: "AND"
|
35
|
+
}
|
36
|
+
}
|
37
|
+
)
|
38
|
+
|
39
|
+
# searchkick
|
40
|
+
Comment.search("hello world", where: { available: true }, order: { id: "desc" }, aggs: [:username])
|
41
|
+
|
42
|
+
# search_flip
|
43
|
+
CommentIndex.where(available: true).search("hello world").sort(id: "desc").aggregate(:username)
|
44
|
+
|
45
|
+
```
|
46
|
+
|
47
|
+
## Reference Docs
|
48
|
+
|
49
|
+
SearchFlip has a great documentation.
|
50
|
+
Check youself at [http://www.rubydoc.info/github/mrkamel/search_flip](http://www.rubydoc.info/github/mrkamel/search_flip)
|
51
|
+
|
52
|
+
## Install
|
53
|
+
|
54
|
+
Add this line to your application's Gemfile:
|
55
|
+
|
56
|
+
```ruby
|
57
|
+
gem 'search_flip'
|
58
|
+
```
|
59
|
+
|
60
|
+
and then execute
|
61
|
+
|
62
|
+
```
|
63
|
+
$ bundle
|
64
|
+
```
|
65
|
+
|
66
|
+
or install it via
|
67
|
+
|
68
|
+
```
|
69
|
+
$ gem install search_flip
|
70
|
+
```
|
71
|
+
|
72
|
+
## Config
|
73
|
+
|
74
|
+
You can change global config options like:
|
75
|
+
|
76
|
+
```ruby
|
77
|
+
SearchFlip::Config[:environment] = "development"
|
78
|
+
SearchFlip::Config[:base_url] = "http://127.0.0.1:9200"
|
79
|
+
```
|
80
|
+
|
81
|
+
Available config options are:
|
82
|
+
|
83
|
+
* `index_prefix` to have a prefix added to your index names automatically. This
|
84
|
+
can be useful to separate the indices of e.g. testing and development environments.
|
85
|
+
* `base_url` to tell search_flip how to connect to your cluster
|
86
|
+
* `bulk_limit` a global limit for bulk requests
|
87
|
+
* `auto_refresh` tells search_flip to automatically refresh an index after
|
88
|
+
import, index, delete, etc operations. This is e.g. usuful for testing, etc.
|
89
|
+
Defaults to false.
|
90
|
+
|
91
|
+
## Usage
|
92
|
+
|
93
|
+
First, create a separate class for your index and include `SearchFlip::Index`.
|
94
|
+
|
95
|
+
```ruby
|
96
|
+
class CommentIndex
|
97
|
+
include SearchFlip::Index
|
98
|
+
end
|
99
|
+
```
|
100
|
+
|
101
|
+
Then tell the Index about the type name, the correspoding model and how to
|
102
|
+
serialize the model for indexing.
|
103
|
+
|
104
|
+
```ruby
|
105
|
+
class CommentIndex
|
106
|
+
include SearchFlip::Index
|
107
|
+
|
108
|
+
def self.type_name
|
109
|
+
"comments"
|
110
|
+
end
|
111
|
+
|
112
|
+
def self.model
|
113
|
+
Comment
|
114
|
+
end
|
115
|
+
|
116
|
+
def self.serialize(comment)
|
117
|
+
{
|
118
|
+
id: comment.id,
|
119
|
+
username: comment.username,
|
120
|
+
title: comment.title,
|
121
|
+
message: comment.message
|
122
|
+
}
|
123
|
+
end
|
124
|
+
end
|
125
|
+
```
|
126
|
+
|
127
|
+
You can additionally specify an `index_scope` which will automatically be
|
128
|
+
applied to scopes, eg. ActiveRecord::Relation objects, passed to `#import`,
|
129
|
+
`#index`, etc. This can be used to preload associations that are used when
|
130
|
+
serializing records or to restrict the records you want to index.
|
131
|
+
|
132
|
+
```ruby
|
133
|
+
class CommentIndex
|
134
|
+
# ...
|
135
|
+
|
136
|
+
def self.index_scope(scope)
|
137
|
+
scope.preload(:user)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
CommentIndex.import(Comment.all) # => CommentIndex.import(Comment.all.preload(:user))
|
142
|
+
```
|
143
|
+
|
144
|
+
Please note, ElasticSearch allows to have multiple types per index. However,
|
145
|
+
this forces to have the same mapping for fields having the same name even
|
146
|
+
though the fields live in different types of the same index. Thus, this gem is
|
147
|
+
using a different index for each type by default, but you can change that.
|
148
|
+
Simply supply a custom `index_name`.
|
149
|
+
|
150
|
+
```ruby
|
151
|
+
class CommentIndex
|
152
|
+
# ...
|
153
|
+
|
154
|
+
def self.index_name
|
155
|
+
"custom_index_name"
|
156
|
+
end
|
157
|
+
|
158
|
+
# ...
|
159
|
+
end
|
160
|
+
```
|
161
|
+
|
162
|
+
Optionally, specify a custom mapping:
|
163
|
+
|
164
|
+
```ruby
|
165
|
+
class CommentIndex
|
166
|
+
# ...
|
167
|
+
|
168
|
+
def self.mapping
|
169
|
+
{
|
170
|
+
comments: {
|
171
|
+
properties: {
|
172
|
+
# ...
|
173
|
+
}
|
174
|
+
}
|
175
|
+
}
|
176
|
+
end
|
177
|
+
|
178
|
+
# ...
|
179
|
+
end
|
180
|
+
```
|
181
|
+
|
182
|
+
or index settings:
|
183
|
+
|
184
|
+
```ruby
|
185
|
+
def self.index_settings
|
186
|
+
{
|
187
|
+
settings: {
|
188
|
+
number_of_shards: 10,
|
189
|
+
number_of_replicas: 2
|
190
|
+
}
|
191
|
+
}
|
192
|
+
end
|
193
|
+
```
|
194
|
+
|
195
|
+
Then you can interact with the index:
|
196
|
+
|
197
|
+
```ruby
|
198
|
+
CommentIndex.create_index
|
199
|
+
CommentIndex.index_exists?
|
200
|
+
CommentIndex.delete_index
|
201
|
+
CommentIndex.update_mapping
|
202
|
+
```
|
203
|
+
|
204
|
+
index records (automatically uses the bulk API):
|
205
|
+
|
206
|
+
```ruby
|
207
|
+
CommentIndex.import(Comment.all)
|
208
|
+
CommentIndex.import(Comment.first)
|
209
|
+
CommentIndex.import([Comment.find(1), Comment.find(2)])
|
210
|
+
CommentIndex.import(Comment.where("created_at > ?", Time.now - 7.days))
|
211
|
+
```
|
212
|
+
|
213
|
+
query records:
|
214
|
+
|
215
|
+
```ruby
|
216
|
+
CommentIndex.total_entries
|
217
|
+
# => 2838
|
218
|
+
|
219
|
+
CommentIndex.search("title:hello").records
|
220
|
+
# => [#<Comment ...>, #<Comment ...>, ...]
|
221
|
+
|
222
|
+
CommentIndex.where(username: "mrkamel").total_entries
|
223
|
+
# => 13
|
224
|
+
|
225
|
+
CommentIndex.aggregate(:username).aggregations(:username)
|
226
|
+
# => {1=>#<SearchFlip::Result doc_count=37 ...>, 2=>... }
|
227
|
+
...
|
228
|
+
|
229
|
+
CommentIndex.search("hello world").sort(id: "desc").aggregate(:username).request
|
230
|
+
# => {:query=>{:bool=>{:must=>[{:query_string=>{:query=>"hello world", :default_operator=>:AND}}]}}, ...}
|
231
|
+
```
|
232
|
+
|
233
|
+
delete records:
|
234
|
+
|
235
|
+
```ruby
|
236
|
+
# for ElasticSearch >= 2.x and < 5.x, the delete-by-query plugin is required
|
237
|
+
# for the following query:
|
238
|
+
|
239
|
+
CommentIndex.match_all.delete
|
240
|
+
|
241
|
+
# or delete manually via the bulk API:
|
242
|
+
|
243
|
+
CommentIndex.match_all.find_each do |record|
|
244
|
+
CommentIndex.bulk do |indexer|
|
245
|
+
indexer.delete record.id
|
246
|
+
end
|
247
|
+
end
|
248
|
+
```
|
249
|
+
|
250
|
+
## Advanced Usage
|
251
|
+
|
252
|
+
SearchFlip supports even more advanced usages, like e.g. post filters, filtered
|
253
|
+
aggregations or nested aggregations via simple to use API methods.
|
254
|
+
|
255
|
+
### Post filters
|
256
|
+
|
257
|
+
All criteria methods (`#where`, `#where_not`, `#range`, etc.) are available
|
258
|
+
in post filter mode as well, ie. filters/queries applied after aggregations
|
259
|
+
are calculated. Checkout the ElasticSearch docs for further info.
|
260
|
+
|
261
|
+
```ruby
|
262
|
+
query = CommentIndex.aggregate(:user_id)
|
263
|
+
query = query.post_where(reviewed: true)
|
264
|
+
query = query.post_search("username:a*")
|
265
|
+
```
|
266
|
+
|
267
|
+
Checkout [PostFilterable](http://www.rubydoc.info/github/mrkamel/search_flip/SearchFlip/PostFilterable)
|
268
|
+
for a complete API reference.
|
269
|
+
|
270
|
+
### Aggregations
|
271
|
+
|
272
|
+
SearchFlip allows to elegantly specify nested aggregations, no matter how deeply
|
273
|
+
nested:
|
274
|
+
|
275
|
+
```ruby
|
276
|
+
query = OrderIndex.aggregate(:username, order: { revenue: "desc" }) do |aggregation|
|
277
|
+
aggregation.aggregate(revenue: { sum: { field: "price" }})
|
278
|
+
end
|
279
|
+
```
|
280
|
+
|
281
|
+
Generally, aggregation results returned by ElasticSearch are wrapped in a
|
282
|
+
`SearchFlip::Result`, which wraps a `Hashie::Mash`such that you can access them
|
283
|
+
via:
|
284
|
+
|
285
|
+
```ruby
|
286
|
+
query.aggregations(:username)["mrkamel"].revenue.value
|
287
|
+
```
|
288
|
+
|
289
|
+
Still, if you want to get the raw aggregations returned by ElasticSearch,
|
290
|
+
access them without supplying any aggregation name to `#aggregations`:
|
291
|
+
|
292
|
+
```ruby
|
293
|
+
query.aggregations # => returns the raw aggregation section
|
294
|
+
|
295
|
+
query.aggregations["username"]["buckets"].detect { |bucket| bucket["key"] == "mrkamel" }["revenue"]["value"] # => 238.50
|
296
|
+
```
|
297
|
+
|
298
|
+
Once again, the criteria methods (`#where`, `#range`, etc.) are available in
|
299
|
+
aggregations as well:
|
300
|
+
|
301
|
+
```ruby
|
302
|
+
query = OrderIndex.aggregate(average_price: {}) do |aggregation|
|
303
|
+
aggregation = aggregation.match_all
|
304
|
+
aggregation = aggregation.where(user_id: current_user.id) if current_user
|
305
|
+
|
306
|
+
aggregation.aggregate(average_price: { avg: { field: "price" }})
|
307
|
+
end
|
308
|
+
|
309
|
+
query.aggregations(:average_price).average_price.value
|
310
|
+
```
|
311
|
+
|
312
|
+
Checkout [Aggregatable](http://www.rubydoc.info/github/mrkamel/search_flip/SearchFlip/Aggregatable)
|
313
|
+
as well as [Aggregation](http://www.rubydoc.info/github/mrkamel/search_flip/SearchFlip/Aggregation)
|
314
|
+
for a complete API reference.
|
315
|
+
|
316
|
+
### Suggestions
|
317
|
+
|
318
|
+
```ruby
|
319
|
+
query = CommentIndex.suggest(:suggestion, text: "helo", term: { field: "message" })
|
320
|
+
query.suggestions(:suggestion).first["text"] # => "hello"
|
321
|
+
```
|
322
|
+
|
323
|
+
### Highlighting
|
324
|
+
|
325
|
+
```ruby
|
326
|
+
CommentIndex.highlight([:title, :message])
|
327
|
+
CommentIndex.highlight(:title).highlight(:description)
|
328
|
+
CommentIndex.highlight(:title, require_field_match: false)
|
329
|
+
CommentIndex.highlight(title: { type: "fvh" })
|
330
|
+
```
|
331
|
+
|
332
|
+
```ruby
|
333
|
+
query = CommentIndex.highlight(:title).search("hello")
|
334
|
+
query.results[0].highlight.title # => "<em>hello</em> world"
|
335
|
+
```
|
336
|
+
|
337
|
+
### Advanced Criteria Methods
|
338
|
+
|
339
|
+
There are even more methods to make your life easier, namely `source`,
|
340
|
+
`scroll`, `profile`, `includes`, `preload`, `find_in_batches`, `find_each`,
|
341
|
+
`failsafe` and `unscope` to name just a few:
|
342
|
+
|
343
|
+
* `source`
|
344
|
+
|
345
|
+
In case you want to restrict the returned fields, simply specify
|
346
|
+
the fields via `#source`:
|
347
|
+
|
348
|
+
```ruby
|
349
|
+
CommentIndex.source([:id, :message]).search("hello world")
|
350
|
+
```
|
351
|
+
|
352
|
+
* `paginate`, `page`, `per`
|
353
|
+
|
354
|
+
SearchFlip supports
|
355
|
+
[will_paginate](https://github.com/mislav/will_paginate) and
|
356
|
+
[kaminari](https://github.com/kaminari/kaminari) compatible pagination. Thus,
|
357
|
+
you can either use `#paginate` or `#page` in combination with `#per`:
|
358
|
+
|
359
|
+
```ruby
|
360
|
+
CommentIndex.paginate(page: 3, per_page: 50)
|
361
|
+
CommentIndex.page(3).per(50)
|
362
|
+
```
|
363
|
+
|
364
|
+
* `scroll`
|
365
|
+
|
366
|
+
You can as well use the underlying scroll API directly, ie. without using higher
|
367
|
+
level pagination:
|
368
|
+
|
369
|
+
```ruby
|
370
|
+
query = CommentIndex.scroll(timeout: "5m")
|
371
|
+
|
372
|
+
until query.records.empty?
|
373
|
+
# ...
|
374
|
+
|
375
|
+
query = query.scroll(id: query.scroll_id, timeout: "5m")
|
376
|
+
end
|
377
|
+
```
|
378
|
+
|
379
|
+
* `profile`
|
380
|
+
|
381
|
+
Use `#profile` To enable query profiling:
|
382
|
+
|
383
|
+
```ruby
|
384
|
+
query = CommentIndex.profile(true)
|
385
|
+
query.raw_response["profile"] # => { "shards" => ... }
|
386
|
+
```
|
387
|
+
|
388
|
+
* `preload`, `eager_load` and `includes`
|
389
|
+
|
390
|
+
Uses the well known methods from ActiveRecord to load
|
391
|
+
associated database records when fetching the respective
|
392
|
+
records themselves. Works with other ORMs as well, if
|
393
|
+
supported.
|
394
|
+
|
395
|
+
Using `#preload`:
|
396
|
+
|
397
|
+
```ruby
|
398
|
+
CommentIndex.preload(:user, :post).records
|
399
|
+
PostIndex.includes(comments: :user).records
|
400
|
+
```
|
401
|
+
|
402
|
+
or `#eager_load`
|
403
|
+
|
404
|
+
```ruby
|
405
|
+
CommentIndex.eager_load(:user, :post).records
|
406
|
+
PostIndex.eager_load(comments: :user).records
|
407
|
+
```
|
408
|
+
|
409
|
+
or `#includes`
|
410
|
+
|
411
|
+
```ruby
|
412
|
+
CommentIndex.includes(:user, :post).records
|
413
|
+
PostIndex.includes(comments: :user).records
|
414
|
+
```
|
415
|
+
|
416
|
+
* `find_in_batches`
|
417
|
+
|
418
|
+
Used to fetch and yield records in batches using the ElasicSearch scroll API.
|
419
|
+
The batch size and scroll API timeout can be specified.
|
420
|
+
|
421
|
+
```ruby
|
422
|
+
CommentIndex.search("hello world").find_in_batches(batch_size: 100) do |batch|
|
423
|
+
# ...
|
424
|
+
end
|
425
|
+
```
|
426
|
+
|
427
|
+
* `find_each`
|
428
|
+
|
429
|
+
Like `#find_in_batches`, use `#find_each` to fetch records in batches, but yields
|
430
|
+
one record at a time.
|
431
|
+
|
432
|
+
```ruby
|
433
|
+
CommentIndex.search("hello world").find_each(batch_size: 100) do |record|
|
434
|
+
# ...
|
435
|
+
end
|
436
|
+
```
|
437
|
+
|
438
|
+
* `failsafe`
|
439
|
+
|
440
|
+
Use `#failsafe` to prevent any exceptions from being raised for query string
|
441
|
+
syntax errors or ElasticSearch being unavailable, etc.
|
442
|
+
|
443
|
+
```ruby
|
444
|
+
CommentIndex.search("invalid/request").execute
|
445
|
+
# raises SearchFlip::ResponseError
|
446
|
+
|
447
|
+
# ...
|
448
|
+
|
449
|
+
CommentIndex.search("invalid/request").failsafe(true).execute
|
450
|
+
# => #<SearchFlip::Response ...>
|
451
|
+
```
|
452
|
+
|
453
|
+
* `merge`
|
454
|
+
|
455
|
+
You can merge criterias, ie. combine the attributes (constraints, settings,
|
456
|
+
etc) of two individual criterias:
|
457
|
+
|
458
|
+
```ruby
|
459
|
+
CommentIndex.where(approved: true).merge(CommentIndex.search("hello"))
|
460
|
+
# equivalent to: CommentIndex.where(approved: true).search("hello")
|
461
|
+
```
|
462
|
+
|
463
|
+
* `unscope`
|
464
|
+
|
465
|
+
You can even remove certain already added scopes via `#unscope`:
|
466
|
+
|
467
|
+
```ruby
|
468
|
+
CommentIndex.aggregate(:username).search("hello world").unscope(:search, :aggregate)
|
469
|
+
```
|
470
|
+
|
471
|
+
* `timeout`
|
472
|
+
|
473
|
+
Specify a timeout to limit query processing time:
|
474
|
+
|
475
|
+
```ruby
|
476
|
+
CommentIndex.timeout("3s").execute
|
477
|
+
```
|
478
|
+
|
479
|
+
* `terminate_after`
|
480
|
+
|
481
|
+
Activate early query termination to stop query processing after the specified
|
482
|
+
number of records has been found:
|
483
|
+
|
484
|
+
```ruby
|
485
|
+
CommentIndex.terminate_after(10).execute
|
486
|
+
```
|
487
|
+
|
488
|
+
For further details and a full list of methods, check out the reference docs.
|
489
|
+
|
490
|
+
## Non-ActiveRecord models
|
491
|
+
|
492
|
+
SearchFlip ships with built-in support for ActiveRecord models, but using
|
493
|
+
non-ActiveRecord models is very easy. The model must implement a `find_each`
|
494
|
+
class method and the Index class needs to implement `Index.record_id` and
|
495
|
+
`Index.fetch_records`. The default implementations for the index class are as
|
496
|
+
follows:
|
497
|
+
|
498
|
+
```ruby
|
499
|
+
class MyIndex
|
500
|
+
include SearchFlip::Index
|
501
|
+
|
502
|
+
def self.record_id(object)
|
503
|
+
object.id
|
504
|
+
end
|
505
|
+
|
506
|
+
def self.fetch_records(ids)
|
507
|
+
model.where(id: ids)
|
508
|
+
end
|
509
|
+
end
|
510
|
+
```
|
511
|
+
|
512
|
+
Thus, simply add your custom implementation of those methods that work with
|
513
|
+
whatever ORM you use.
|
514
|
+
|
515
|
+
## Date and Timestamps in JSON
|
516
|
+
|
517
|
+
ElasticSearch requires dates and timestamps to have one of the formats listed
|
518
|
+
here: [https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-date-format.html#strict-date-time](https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-date-format.html#strict-date-time).
|
519
|
+
|
520
|
+
However, `JSON.generate` in ruby by default outputs something like:
|
521
|
+
|
522
|
+
```ruby
|
523
|
+
JSON.generate(time: Time.now.utc)
|
524
|
+
# => "{\"time\":\"2018-02-22 18:19:33 UTC\"}"
|
525
|
+
```
|
526
|
+
|
527
|
+
This format is not compatible with ElasticSearch by default. If you're on
|
528
|
+
Rails, ActiveSupport adds its own `#to_json` methods to `Time`, `Date`, etc.
|
529
|
+
However, ActiveSupport checks whether they are used in combination with
|
530
|
+
`JSON.generate` or not and adapt:
|
531
|
+
|
532
|
+
```ruby
|
533
|
+
Time.now.utc.to_json
|
534
|
+
=> "\"2018-02-22T18:18:22.088Z\""
|
535
|
+
|
536
|
+
JSON.generate(time: Time.now.utc)
|
537
|
+
=> "{\"time\":\"2018-02-22 18:18:59 UTC\"}"
|
538
|
+
```
|
539
|
+
|
540
|
+
SearchFlip is using the [Oj gem](https://github.com/ohler55/oj) to generate
|
541
|
+
JSON. More concretely, SearchFlip is using:
|
542
|
+
|
543
|
+
```ruby
|
544
|
+
Oj.dump({ key: "value" }, mode: :custom, use_to_json: true)
|
545
|
+
```
|
546
|
+
|
547
|
+
This mitigates the issues if you're on Rails:
|
548
|
+
|
549
|
+
```ruby
|
550
|
+
Oj.dump(Time.now, mode: :custom, use_to_json: true)
|
551
|
+
# => "\"2018-02-22T18:21:21.064Z\""
|
552
|
+
```
|
553
|
+
|
554
|
+
However, if you're not on Rails, you need to add `#to_json` methods to `Time`,
|
555
|
+
`Date` and `DateTime` to get proper serialization. You can either add them on
|
556
|
+
your own, via other libraries or by simply using:
|
557
|
+
|
558
|
+
```ruby
|
559
|
+
require "search_flip/to_json"
|
560
|
+
```
|
561
|
+
|
562
|
+
## Feature Support
|
563
|
+
|
564
|
+
* `#post_search` and `#profile` are only supported from up to ElasticSearch
|
565
|
+
version >= 2.
|
566
|
+
* for ElasticSearch 2.x, the delete-by-query plugin is required to delete
|
567
|
+
records via queries
|
568
|
+
|
569
|
+
## Keeping your Models and Indices in Sync
|
570
|
+
|
571
|
+
Besides the most basic approach to get you started, SarchFlip currently doesn't
|
572
|
+
ship with any means to automatically keep your models and indices in sync,
|
573
|
+
because every method is very much bound to the concrete environment and depends
|
574
|
+
on your concrete requirements. In addition, the methods to achieve model/index
|
575
|
+
consistency can get arbitrarily complex and we want to keep this bloat out of
|
576
|
+
the SearchFlip codebase.
|
577
|
+
|
578
|
+
```ruby
|
579
|
+
class Comment < ActiveRecord::Base
|
580
|
+
include SearchFlip::Model
|
581
|
+
|
582
|
+
notifies_index(CommentIndex)
|
583
|
+
end
|
584
|
+
```
|
585
|
+
|
586
|
+
It uses `after_commit` (if applicable, `after_save`, `after_destroy` and
|
587
|
+
`after_touch` otherwise) hooks to synchronously update the index when your
|
588
|
+
model changes.
|
589
|
+
|
590
|
+
## Links
|
591
|
+
|
592
|
+
* ElasticSearch: [https://www.elastic.co/](https://www.elastic.co/)
|
593
|
+
* Reference Docs: [http://www.rubydoc.info/github/mrkamel/search_flip](http://www.rubydoc.info/github/mrkamel/search_flip)
|
594
|
+
* Travis: [http://travis-ci.org/mrkamel/search_flip](http://travis-ci.org/mrkamel/search_flip)
|
595
|
+
* will_paginate: [https://github.com/mislav/will_paginate](https://github.com/mislav/will_paginate)
|
596
|
+
* kaminari: [https://github.com/kaminari/kaminari](https://github.com/kaminari/kaminari)
|
597
|
+
* Oj: [https://github.com/ohler55/oj](https://github.com/ohler55/oj)
|
598
|
+
|
599
|
+
## Contributing
|
600
|
+
|
601
|
+
1. Fork it
|
602
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
603
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
604
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
605
|
+
5. Create new Pull Request
|
606
|
+
|