caoutsearch 0.0.5 → 0.0.6

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9a9f19fbbee99086c07a66881e1b3d5f5df90f4dc06d32ce4a4d4dfc323a85bb
4
- data.tar.gz: cf8736b55478e95f3e98a529d9e2f4102de8a59921108bf99703d9c6fe742a54
3
+ metadata.gz: 2d22b648c6800d29d075baa757ec80d6c1429752918db6fb899fb7484d3ff9f7
4
+ data.tar.gz: 03fdbf0e6cd2dcc966bf435e9ea7e48ede4e14bd0ae3ec50215cb6867b07d90a
5
5
  SHA512:
6
- metadata.gz: 3ee219e96b1b77bbdea6b20a91bb252915b2f557c82924892e67d9ca6d08caf7013f8a8ac05852375f9821cda4e7fb0ba23226ff04943e03a4a7f2d16dd80bf4
7
- data.tar.gz: d154e911d74f71f14540d551ead74a9452770660a8be4b991a73d2d2dbed3a206274b51a30b1c7090587e6bbdea389729104af293fd6f542211ae7fd952ec12a
6
+ metadata.gz: 129553e5adac2bf6e4c4158ce977812ee49e7c05381987ba8a45e4f2dc6e9cac9a1d9203ca8f36e8a72fa8e0e0bc40ad11141db2adc8e5858ae8694e3bed39f5
7
+ data.tar.gz: e2207e9d3d647a4f4465aaf11ec9f5a7cbde73b1db4867b45b56ad67ffcc5d86be639cb03ec965cdc25ea1c686c1e3fd1e9805035cb946fdf051af8306547063
data/README.md CHANGED
@@ -1,13 +1,13 @@
1
1
  # Caoutsearch [\ˈkawt͡ˈsɝtʃ\\](http://ipa-reader.xyz/?text=ˈkawt͡ˈsɝtʃ)
2
2
 
3
- [![Gem Version](https://badge.fury.io/rb/caoutsearch.svg)](https://rubygems.org/gems/caoutsearch)
4
- [![CI Status](https://github.com/mon-territoire/caoutsearch/actions/workflows/ci.yml/badge.svg)](https://github.com/mon-territoire/caoutsearch/actions/workflows/ci.yml)
5
- [![Ruby Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://github.com/testdouble/standard)
6
- [![Maintainability](https://api.codeclimate.com/v1/badges/fbe73db3fd8be9a10e12/maintainability)](https://codeclimate.com/github/mon-territoire/caoutsearch/maintainability)
7
- [![Test Coverage](https://api.codeclimate.com/v1/badges/fbe73db3fd8be9a10e12/test_coverage)](https://codeclimate.com/github/mon-territoire/caoutsearch/test_coverage)
3
+ <span>[![Gem Version](https://badge.fury.io/rb/caoutsearch.svg)](https://rubygems.org/gems/caoutsearch)</span> <span>
4
+ [![CI Status](https://github.com/mon-territoire/caoutsearch/actions/workflows/ci.yml/badge.svg)](https://github.com/mon-territoire/caoutsearch/actions/workflows/ci.yml)</span> <span>
5
+ [![Ruby Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://github.com/testdouble/standard)</span> <span>
6
+ [![Maintainability](https://api.codeclimate.com/v1/badges/fbe73db3fd8be9a10e12/maintainability)](https://codeclimate.com/github/mon-territoire/caoutsearch/maintainability)</span> <span>
7
+ [![Test Coverage](https://api.codeclimate.com/v1/badges/fbe73db3fd8be9a10e12/test_coverage)](https://codeclimate.com/github/mon-territoire/caoutsearch/test_coverage)</span>
8
8
 
9
- [![JRuby](https://github.com/mon-territoire/caoutsearch/actions/workflows/jruby.yml/badge.svg)](https://github.com/mon-territoire/caoutsearch/actions/workflows/jruby.yml)
10
- [![Truffle Ruby](https://github.com/mon-territoire/caoutsearch/actions/workflows/truffle_ruby.yml/badge.svg)](https://github.com/mon-territoire/caoutsearch/actions/workflows/truffle_ruby.yml)
9
+ <span>[![JRuby](https://github.com/mon-territoire/caoutsearch/actions/workflows/jruby.yml/badge.svg)](https://github.com/mon-territoire/caoutsearch/actions/workflows/jruby.yml)</span> <span>
10
+ [![Truffle Ruby](https://github.com/mon-territoire/caoutsearch/actions/workflows/truffle_ruby.yml/badge.svg)](https://github.com/mon-territoire/caoutsearch/actions/workflows/truffle_ruby.yml)</span>
11
11
 
12
12
  **!! Gem under development before public release !!**
13
13
 
@@ -17,177 +17,42 @@ It provides a simple but powerful DSL to perform complex indexing and searching,
17
17
  Caoutsearch only supports Elasticsearch 8.x right now.
18
18
  It is used in production in a robust application, updated and maintained for several years at [Mon Territoire](https://mon-territoire.fr).
19
19
 
20
- Caoutsearch was inspired by awesome gems such as [elasticsearch-rails](https://github.com/elastic/elasticsearch-rails) or [search_flip](https://github.com/mrkamel/search_flip).
21
- If you don't have scenarios as complex as those described in this documentation, they should better suite your needs.
22
-
23
- ## Table of Contents
24
-
25
- - [Installation](#installation)
26
- - [Configuration](#configuration)
27
- - Instrumentation
28
- - [Usage](#usage)
29
- - [Indice Configuration](#indice-configuration)
30
- - Mapping & settings
31
- - Text analysis
32
- - Versionning
33
- - [Index Engine](#index-engine)
34
- - Properties
35
- - Partial updates
36
- - Eager loading
37
- - Interdependencies
38
- - [Search Engine](#search-engine)
39
- - Queries
40
- - [Filters](#filters)
41
- - Full-text query
42
- - Custom filters
43
- - Orders
44
- - [Aggregations](#aggregations)
45
- - [Transform aggregations](#transform-aggregations)
46
- - [Responses](#responses)
47
- - [Loading records](#loading-records)
48
- - [Model integration](#model-integration)
49
- - [Add Caoutsearch to your models](#add-caoutsearch-to-your-models)
50
- - [Index records](#index-records)
51
- - [Index multiple records](#index-multiple-records)
52
- - [Index single records](#index-single-records)
53
- - [Delete documents](#delete-documents)
54
- - [Automatic Callbacks](#automatic-callbacks)
55
- - Asynchronous methods
56
- - [Search for records](#search-for-records)
57
- - [Search API](#search-api)
58
- - [Pagination](#pagination)
59
- - [Total count](#total-count)
60
- - [Iterating results](#iterating-results)
61
- - [Testing with Caoutsearch](#testing-with-Caoutsearch)
20
+ Caoutsearch was inspired by awesome gems such as [elasticsearch-rails](https://github.com/elastic/elasticsearch-rails) or [search_flip](https://github.com/mrkamel/search_flip).
21
+ Depending on your search scenarios, they may better suite your needs.
62
22
 
63
23
  ## Installation
64
24
 
25
+ Add the gem in your Gemfile:
26
+
65
27
  ```bash
66
28
  bundle add caoutsearch
67
29
  ```
68
30
 
69
- ## Configuration
70
-
71
- TODO
72
-
73
- ## Usage
74
-
75
- ### Indice Configuration
76
-
77
- TODO
78
-
79
- ### Index Engine
80
-
81
- TODO
31
+ ## Overview
82
32
 
83
- ### Search Engine
33
+ Caoutsearch let you create `Index` and `Search` classes to manipulate your data :
84
34
 
85
- #### Filters
86
- Filters declared in the search engine will define how Caoutsearch will build the queries
87
-
88
- The main use of filters is to expose a field for search, but they can also be used to build more complex queries:
89
35
  ```ruby
90
- class ArticleSearch < Caoutsearch::Search::Base
91
- # Build a filter on the author field
92
- filter :author
36
+ class ArticleIndex < Caoutsearch::Index::Base
37
+ property :title
38
+ property :published_on
39
+ property :tags
93
40
 
94
- # Build a Match filter on multiple fields
95
- filter :content, indexes: %i[title.words content], as: :match
96
-
97
- # Build a more complex filter by using other filters
98
- filter :public, as: :boolean
99
- filter :published_on, as: :date
100
- filter :active do |value|
101
- search_by(published: value, published_on: value)
41
+ def tags
42
+ records.tags.public.map(&:to_s)
102
43
  end
103
44
  end
104
- ```
105
-
106
- Caoutsearch different types of filters to handle different types of data or ways to search them:
107
45
 
108
- ##### Default filter
109
-
110
- ##### Boolean filter
46
+ ArticleIndex.reindex(:tags)
47
+ ```
111
48
 
112
- ##### Date filter
113
49
 
114
- For a date filter defined like this:
115
50
  ```ruby
116
51
  class ArticleSearch < Caoutsearch::Search::Base
117
- ...
118
-
52
+ filter :title, as: :match
119
53
  filter :published_on, as: :date
120
- end
121
- ```
122
-
123
- You can now search the matching index with the `published_on` criterion:
124
- ```ruby
125
- Article.search(published_on: Date.today)
126
- ```
127
-
128
- and the following query will be generated to send to elasticsearch:
129
- ```json
130
- {
131
- "query": {
132
- "bool": {
133
- "filter": [
134
- { "range": { "published_on": { "gte": "2022-23-11", "lte": "2022-23-11"}}}
135
- ]
136
- }
137
- }
138
- }
139
- ```
140
-
141
- The date filter accepts multiple types of arguments :
54
+ filter :tags
142
55
 
143
- ```ruby
144
- # Search for articles published on a date:
145
- Article.search(published_on: Date.today)
146
-
147
- # Search for articles published before a date:
148
- Article.search(published_on: { less_than: "2022-12-25" })
149
- Article.search(published_on: { less_than_or_equal: "2022-12-25" })
150
- Article.search(published_on: ..Date.new(2022, 12, 25))
151
- Article.search(published_on: [[nil, "now-2w/d"]])
152
-
153
- # Search for articles published after a date:
154
- Article.search(published_on: { greater_than: "2022-12-25" })
155
- Article.search(published_on: { greater_than_or_equal: "2022-12-25" })
156
- Article.search(published_on: Date.new(2022, 12, 25)..)
157
- Article.search(published_on: [["now-1w/d", nil]])
158
-
159
- # Search for articles published between two dates:
160
- Article.search(published_on: { greater_than: "2022-12-25", less_than: "2023-12-25" })
161
- Article.search(published_on: Date.new(2022, 12, 25)..Date.new(2023, 12, 25))
162
- Article.search(published_on: [["now-1w/d", "now/d"]])
163
- ```
164
-
165
- Dates of various formats are handled:
166
- ```ruby
167
- "2022-10-11"
168
- Date.today
169
- Time.zone.now
170
- ```
171
-
172
- We also support elasticsearch's date math
173
- ```ruby
174
- "now-1h"
175
- "now+2w/d"
176
- ```
177
-
178
- ##### GeoPoint filter
179
-
180
- ##### Match filter
181
-
182
- ##### Range filter
183
-
184
- #### Aggregations
185
-
186
- You can define simple to complex aggregations.
187
-
188
- ````ruby
189
- class ArticleSearch < Caoutsearch::Search::Base
190
- has_aggregation :view_count, { sum: { field: :view_count } }
191
56
  has_aggregation :popular_tags, {
192
57
  filter: { term: { published: true } },
193
58
  aggs: {
@@ -197,637 +62,14 @@ class ArticleSearch < Caoutsearch::Search::Base
197
62
  }
198
63
  }
199
64
  end
200
- ````
201
-
202
- Then you can request one or more aggregations at the same time or chain the `aggregate` method.
203
- The `aggregations` method will trigger a request and returns a [Response::Aggregations](#responses).
204
-
205
- ````ruby
206
- ArticleSearch.aggregate(:view_count).aggregations
207
- # ArticleSearch Search { "body": { "aggs": { "view_count": { "sum": { "field": "view_count" }}}}}
208
- # ArticleSearch Search (10ms / took 5ms)
209
- => #<Caoutsearch::Response::Aggregations view_count=#<Caoutsearch::Response::Response value=119652>>
210
-
211
- ArticleSearch.aggregate(:view_count, :popular_tags).aggregations
212
- # ArticleSearch Search { "body": { "aggs": { "view_count": {…}, "popular_tags": {…}}}}
213
- # ArticleSearch Search (10ms / took 5ms)
214
- => #<Caoutsearch::Response::Aggregations view_count=#<Caoutsearch::Response::Response value=119652> popular_tags=#<Caoutsearch::Response::Response buckets=…>>
215
-
216
- ArticleSearch.aggregate(:view_count).aggregate(:popular_tags).aggregations
217
- # ArticleSearch Search { "body": { "aggs": { "view_count": {…}, "popular_tags": {…}}}}
218
- # ArticleSearch Search (10ms / took 5ms)
219
- => #<Caoutsearch::Response::Aggregations view_count=#<Caoutsearch::Response::Response value=119652> popular_tags=#<Caoutsearch::Response::Response buckets=…>>
220
- ````
221
-
222
- You can create powerful aggregations using blocks and pass arguments to them.
223
-
224
- ````ruby
225
- class ArticleSearch < Caoutsearch::Search::Base
226
- has_aggregation :popular_tags_since do |date|
227
- raise TypeError unless date.is_a?(Date)
228
-
229
- query.aggregations[:popular_tags_since] = {
230
- filter: { range: { publication_date: { gte: date.to_s } } },
231
- aggs: {
232
- published: {
233
- terms: { field: :tags, size: 20 }
234
- }
235
- }
236
- }
237
- end
238
- end
239
-
240
- ArticleSearch.aggregate(popular_tags_since: 1.day.ago).aggregations
241
- # ArticleSearch Search { "body": { "aggs": { "popular_tags_since": {…}}}}
242
- # ArticleSearch Search (10ms / took 5ms)
243
- => #<Caoutsearch::Response::Aggregations popular_tags_since=#<Caoutsearch::Response::Response …
244
- ````
245
-
246
- Only one argument can be passed to an aggregation block.
247
- Use an Array or a Hash if you need to pass multiple options.
248
-
249
- ````ruby
250
- class ArticleSearch < Caoutsearch::Search::Base
251
- has_aggregation :popular_tags_since do |options|
252
- # …
253
- end
254
-
255
- has_aggregation :popular_tags_between do |(first_date, end_date)|
256
- # …
257
- end
258
- end
259
-
260
- ArticleSearch.aggregate(popular_tags_since: { date: 1.day.ago, size: 20 })
261
- ArticleSearch.aggregate(popular_tags_between: [date1, date2])
262
- ````
263
-
264
- Finally, you can create a "catch-all" aggregation to handle cumbersome behaviors:
265
-
266
- ````ruby
267
- class ArticleSearch < Caoutsearch::Search::Base
268
- has_aggregation do |name, options = {}|
269
- raise "unxpected_error" unless name.match?(/^view_count_(?<year>\d{4})$/)
270
-
271
- query.aggregations[name] = {
272
- filter: { term: { year: $LAST_LATCH_INFO[:year] } },
273
- aggs: {
274
- filtered: {
275
- sum: { field: :view_count }
276
- }
277
- }
278
- }
279
- end
280
- end
281
-
282
- ArticleSearch.aggregate(:view_count_2020, :view_count_2019).aggregations
283
- # ArticleSearch Search { "body": { "aggs": { "view_count_2020": {…}, "view_count_2019": {…}}}}
284
- # ArticleSearch Search (10ms / took 5ms)
285
- => #<Caoutsearch::Response::Aggregations view_count_2020=#<Caoutsearch::Response::Response …
286
- ````
287
-
288
- #### Transform aggregations
289
-
290
- When using [buckets aggregation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket.html) and/or [pipeline aggregation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline.html), the path to the expected values can get complicated and become subject to unexpected changes for a public API.
291
-
292
- ````ruby
293
- ArticleSearch.aggregate(popular_tags_since: 1.month.ago).aggregations.popular_tags_since.published.buckets.pluck(:key)
294
- => ["Blog", "Tech", …]
295
- ````
296
-
297
- Instead, you can define transformations to provide simpler access to aggregated data:
298
-
299
- ````ruby
300
- class ArticleSearch < Caoutsearch::Search::Base
301
- has_aggregation :popular_tags_since do |since|
302
- # …
303
- end
304
-
305
- transform_aggregation :popular_tags_since do |aggs|
306
- aggs.dig(:popular_tags_since, :published, :buckets).pluck(:key)
307
- end
308
- end
309
-
310
- ArticleSearch.aggregate(popular_tags_since: 1.month.ago).aggregations.popular_tags_since
311
- => ["Blog", "Tech", …]
312
- ````
313
-
314
- You can also use transformations to combine multiple aggregations:
315
-
316
- ````ruby
317
- class ArticleSearch < Caoutsearch::Search::Base
318
- has_aggregation :blog_count, { filter: { term: { category: "blog" } } }
319
- has_aggregation :archives_count, { filter: { term: { archived: true } } }
320
-
321
- transform_aggregation :stats, from: %i[blog_count archives_count] do |aggs|
322
- {
323
- blog_count: aggs.dig(:blog_count, :doc_count),
324
- archives_count: aggs.dig(:archives, :doc_count)
325
- }
326
- end
327
- end
328
-
329
- ArticleSearch.aggregate(:stats).aggregations.stats
330
- # ArticleSearch Search { "body": { "aggs": { "blog_count": {…}, "archives_count": {…}}}}
331
- # ArticleSearch Search (10ms / took 5ms)
332
- => { blog_count: 124, archives_count: 2452 }
333
- ````
334
-
335
- This is also usefull to unify the API between different search engines:
336
-
337
- ````ruby
338
- class ArticleSearch < Caoutsearch::Search::Base
339
- has_aggregation :popular_tags, {
340
- filter: { term: { published: true } },
341
- aggs: { published: { terms: { field: :tags, size: 10 } } }
342
- }
343
-
344
- transform_aggregation :popular_tags do |aggs|
345
- aggs.dig(:popular_tags, :published, :buckets).pluck(:key)
346
- end
347
- end
348
-
349
- class TagSearch < Caoutsearch::Search::Base
350
- has_aggregation :popular_tags, {
351
- terms: { field: "label", size: 20, order: { used_count: "desc" } }
352
- }
353
-
354
- transform_aggregation :popular_tags do |aggs|
355
- aggs.dig(:popular_tags, :buckets).pluck(:key)
356
- end
357
- end
358
-
359
- ArticleSearch.aggregate(:popular_tags).aggregations.popular_tags
360
- => ["Blog", "Tech", …]
361
-
362
- TagSearch.aggregate(:popular_tags).aggregations.popular_tags
363
- => ["Tech", "Blog", …]
364
- ````
365
-
366
- Transformations are performed on demand and result is memorized. That means:
367
- - the result of transformation is not visible in the [Response::Aggregations](#responses) output.
368
- - the block is called only once for the same search instance.
369
-
370
- ````ruby
371
- class ArticleSearch < Caoutsearch::Search::Base
372
- has_aggregation :popular_tags, {…}
373
-
374
- transform_aggregation :popular_tags do |aggs|
375
- tags = aggs.dig(:popular_tags, :published, :buckets).pluck(:key)
376
- authorized = Tag.where(title: tags, authorize: true).pluck(:title)
377
- tags & authorized
378
- end
379
- end
380
-
381
- article_search = ArticleSearch.aggregate(:popular_tags)
382
- => #<ArticleSearch current_aggregations: [:popular_tags]>
383
-
384
- article_search.aggregations
385
- # ArticleSearch Search (10ms / took 5ms)
386
- => #<Caoutsearch::Response::Aggregations popular_tags=#<Caoutsearch::Response::Response doc_count=100 …
387
-
388
- article_search.aggregations.popular_tags
389
- # (10.2ms) SELECT "tags"."title" FROM "tags" WHERE "tags"."title" IN …
390
- => ["Blog", "Tech", …]
391
-
392
- article_search.aggregations.popular_tags
393
- => ["Blog", "Tech", …]
394
-
395
- article_search.search("Tech").aggregations.popular_tags
396
- # ArticleSearch Search (10ms / took 5ms)
397
- # (10.2ms) SELECT "tags"."title" FROM "tags" WHERE "tags"."title" IN …
398
- => ["Blog", "Tech", …]
399
- ````
400
-
401
- Be careful to avoid using `aggregations.<aggregation_name>` inside a transformation block: it can lead to an infinite recursion.
402
-
403
- ````ruby
404
- class ArticleSearch < Caoutsearch::Search::Base
405
- transform_aggregation :popular_tags do
406
- aggregations.popular_tags.buckets.pluck("key")
407
- end
408
- end
409
-
410
- ArticleSearch.aggregate(:popular_tags).aggregations.popular_tags
411
- Traceback (most recent call last):
412
- 4: from app/searches/article_search.rb:3:in `block in <class:ArticleSearch>'
413
- 3: from app/searches/article_search.rb:3:in `block in <class:ArticleSearch>'
414
- 2: from app/searches/article_search.rb:3:in `block in <class:ArticleSearch>'
415
- 1: from app/searches/article_search.rb:3:in `block in <class:ArticleSearch>'
416
- SystemStackError (stack level too deep)
417
- ````
418
-
419
- Instead, use the argument passed to the block: it's is a shortcut for `response.aggregations` which is a [Response::Reponse](#responses) and not a [Response::Aggregations](#responses).
420
-
421
- ````ruby
422
- class ArticleSearch < Caoutsearch::Search::Base
423
- transform_aggregation :popular_tags do |aggs|
424
- aggs.popular_tags.buckets.pluck("key")
425
- end
426
- end
427
-
428
- ArticleSearch.aggregate(:popular_tags).aggregations.popular_tags
429
- => ["Blog", "Tech", …]
430
- ````
431
-
432
- One last helpful argument is `track_total_hits` which allows to perform calculations over aggregations using the `total_count` method without sending a second request.
433
- Take a look at [Total count](#total-count) to understand why a second request could be performed.
434
-
435
- ````ruby
436
- class ArticleSearch < Caoutsearch::Search::Base
437
- aggregation :tagged, filter: { exist: "tag" }
438
65
 
439
- transform_aggregation :tagged_rate, from: :tagged, track_total_hits: true do |aggs|
440
- count = aggs.dig(:tagged, :doc_count)
441
- count.to_f / total_count
442
- end
443
-
444
- transform_aggregation :tagged_rate_without_track_total_hits, from: :tagged do |aggs|
445
- count = aggs.dig(:tagged, :doc_count)
446
- count.to_f / total_count
447
- end
448
- end
449
-
450
- ArticleSearch.aggregate(:tagged_rate).aggregations.tagged_rate
451
- # ArticleSearch Search { "body": { "track_total_hits": true, "aggs": { "blog_count": {…}, "archives_count": {…}}}}
452
- # ArticleSearch Search (10ms / took 5ms)
453
- => 0.95
454
-
455
- ArticleSearch.aggregate(:tagged_rate_without_track_total_hits).aggregations.tagged_rate
456
- # ArticleSearch Search { "body": { "aggs": { "blog_count": {…}, "archives_count": {…}}}}
457
- # ArticleSearch Search (10ms / took 5ms)
458
- # ArticleSearch Search { "body": { "track_total_hits": true, "aggs": { "blog_count": {…}, "archives_count":
459
- # ArticleSearch Search (10ms / took 5ms)
460
- => 0.95
461
- ````
462
-
463
- #### Responses
464
-
465
- After the request has been sent by calling a method such as `load`, `response` or `hits`, the results is wrapped in a `Response::Response` class which provides method access to its properties via [Hashie::Mash](http://github.com/intridea/hashie).
466
-
467
- Aggregations and suggestions are wrapped in their own respective subclass of `Response::Response`
468
-
469
- ````ruby
470
- results.response
471
- => #<Caoutsearch::Response::Response _shards=#<Caoutsearch::Response::Response failed=0 skipped=0 successful=5 total=5> hits=…
472
-
473
- search.hits
474
- => #<Hashie::Array [#<Caoutsearch::Response::Response _id="2"…
475
-
476
- search.aggregations
477
- => #<Caoutsearch::Response::Aggregations view_count=#<Caoutsearch::Response::Response…
478
-
479
- search.suggestions
480
- => #<Caoutsearch::Response::Suggestions tags=#<Caoutsearch::Response::Response…
481
- ````
482
-
483
- ##### Loading records
484
-
485
- When calling `records`, the search engine will try to load records from a model using the same class name without `Search` the suffix:
486
- * `ArticleSearch` > `Article`
487
- * `Blog::ArticleSearch` > `Blog::Article`
488
-
489
- ````ruby
490
- ArticleSearch.new.records.first
491
- # ArticleSearch Search (10ms / took 5ms)
492
- # Article Load (9.6ms) SELECT "articles".* FROM "articles" WHERE "articles"."id" IN (1, …
493
- => #<Article id: 1, …>
494
- ````
495
-
496
- However, you can define an alternative model to load records. This might be helpful when using [single table inheritance](https://api.rubyonrails.org/classes/ActiveRecord/Inheritance.html).
497
-
498
- ````ruby
499
- ArticleSearch.new.records(use: BlogArticle).first
500
- # ArticleSearch Search (10ms / took 5ms)
501
- # BlogArticle Load (9.6ms) SELECT "articles".* FROM "articles" WHERE "articles"."id" IN (1, …
502
- => #<BlogArticle id: 1, …>
503
- ````
504
-
505
- You can also define an alternative model at class level:
506
-
507
- ````ruby
508
- class BlogArticleSearch < Caoutsearch::Search::Base
509
- self.model_name = "Article"
510
-
511
- default do
512
- query.filters << { term: { category: "blog" } }
513
- end
514
- end
515
-
516
- BlogArticleSearch.new.records.first
517
- # BlogArticleSearch Search (10ms / took 5ms)
518
- # Article Load (9.6ms) SELECT "articles".* FROM "articles" WHERE "articles"."id" IN (1, …
519
- => #<Article id: 1, …>
520
- ````
521
-
522
- ### Model integration
523
-
524
- #### Add Caoutsearch to your models
525
-
526
- The simplest solution is to add `Caoutsearch::Model` to your model and the link the appropriate `Index` and/or `Search` engines:
527
-
528
- ```ruby
529
- class Article < ActiveRecord::Base
530
- include Caoutsearch::Model
531
-
532
- index_with ArticleIndex
533
- search_with ArticleSearch
534
- end
535
- ```
536
-
537
- If you don't need your models to be `Indexable` and `Searchable`, you can include only one of the following two modules:
538
-
539
- ````ruby
540
- class Article < ActiveRecord::Base
541
- include Caoutsearch::Model::Indexable
542
-
543
- index_with ArticleIndex
544
- end
545
- ````
546
- or
547
- ````ruby
548
- class Article < ActiveRecord::Base
549
- include Caoutsearch::Model::Searchable
550
-
551
- search_with ArticleSearch
552
- end
553
- ````
554
-
555
- The modules can be safely included in the meta model `ApplicationRecord`.
556
- Indexing & searching features are not available until you call `index_with` or `search_with`:
557
-
558
- ````ruby
559
- class ApplicationRecord < ActiveRecord::Base
560
- include Caoutsearch::Model
561
- end
562
- ````
563
-
564
- #### Index records
565
-
566
- ##### Index multiple records
567
-
568
- Import all your records or a restricted scope of records to Elastcisearch.
569
-
570
- ````ruby
571
- Article.reindex
572
- Article.where(published: true).reindex
573
- ````
574
-
575
- You can update one or more properties. (see [Indexation Engines](#indexation-engines) to read more about properties):
576
-
577
- ````ruby
578
- Article.reindex(:category)
579
- Article.reindex(%i[category published_on])
580
- ````
581
-
582
- When `reindex` is called without properties, it'll import the full document to ES.
583
- On the contrary, when properties are passed, it'll only update existing documents.
584
- You can control this behavior with the `method` argument.
585
-
586
- ````ruby
587
- Article.where(id: 123).reindex(:category)
588
- # ArticleIndex Reindex {"index":"articles","body":[{"update":{"_id":123}},{"doc":{"category":"blog"}}]}
589
- # [Error] {"update"=>{"_index"=>"articles", "_id"=>"123", "status"=>404, "error"=>{"type"=>"document_missing_exception", …}}
590
-
591
- Article.where(id: 123).reindex(:category, method: :index)
592
- # ArticleIndex Reindex {"index":"articles","body":[{"index":{"_id":123}},{"category":"blog"}]}
593
-
594
- Article.where(id: 123).reindex(method: :update)
595
- # ArticleIndex Reindex {"index":"articles","body":[{"update":{"_id":123}},{"doc":{…}}]}
596
- ````
597
-
598
- ##### Index single records
599
-
600
- Import a single record.
601
-
602
- ````ruby
603
- Article.find(123).update_index
604
- ````
605
-
606
- You can update one or more properties. (see [Indexation Engines](#indexation-engines) to read more about properties):
607
-
608
- ````ruby
609
- Article.find(123).update_index(:category)
610
- Article.find(123).update_index(%i[category published_on])
611
- ````
612
-
613
- You can verify if and how documents are indexed.
614
- If the document is missing in ES, it'll raise a `Elastic::Transport::Transport::Errors::NotFound`.
615
-
616
- ````ruby
617
- Article.find(123).indexed_document
618
- # Traceback (most recent call last):
619
- # 1: from (irb):1
620
- # Elastic::Transport::Transport::Errors::NotFound ([404] {"_index":"articles","_id":"123","found":false})
621
-
622
- Article.find(123).update_index
623
- Article.find(123).indexed_document
624
- => {"_index"=>"articles", "_id"=>"123", "_version"=>1"found"=>true, "_source"=>{…}}
625
- ````
626
-
627
- ##### Delete documents
628
-
629
- You can delete one or more documents.
630
- **Note**: it won't delete records from database, only from the ES indice.
631
-
632
- ````ruby
633
- Article.delete_indexes
634
- Article.where(id: 123).delete_indexed_documents
635
- Article.find(123).delete_index
636
- ````
637
-
638
- If a record is already deleted from the database, you can still delete its document.
639
-
640
- ````ruby
641
- Article.delete_index(123)
642
- ````
643
-
644
- ##### Automatic Callbacks
645
-
646
- Callbacks are not provided by Caoutsearch but they are very easy to add:
647
-
648
- ````ruby
649
- class Article < ApplicationRecord
650
- index_with ArticleIndex
651
-
652
- after_commit :update_index, on: %i[create update]
653
- after_commit :delete_index, on: %i[destroy]
654
- end
655
- ````
656
-
657
- ##### Asynchronous methods
658
-
659
- TODO
660
-
661
- #### Search for records
662
-
663
- ##### Search API
664
- Searching is pretty simple.
665
-
666
- ````ruby
667
- Article.search("Quick brown fox")
668
- => #<ArticleSearch current_criteria: ["Quick brown fox"]>
669
- ````
670
-
671
- You can chain criteria and many other parameters:
672
- ````ruby
673
- Article.search("Quick brown fox").search(published: true)
674
- => #<ArticleSearch current_criteria: ["Quick brown fox", {"published"=>true}]>
675
-
676
- Article.search("Quick brown fox").order(:publication_date)
677
- => #<ArticleSearch current_criteria: ["Quick brown fox"], current_order: :publication_date>
678
-
679
- Article.search("Quick brown fox").limit(100).offset(100)
680
- => #<ArticleSearch current_criteria: ["Quick brown fox"], current_limit: 100, current_offset: 100>
681
-
682
- Article.search("Quick brown fox").page(1).per(100)
683
- => #<ArticleSearch current_criteria: ["Quick brown fox"], current_page: 1, current_limit: 100>
684
-
685
- Article.search("Quick brown fox").aggregate(:tags).aggregate(:dates)
686
- => #<ArticleSearch current_criteria: ["Quick brown fox"], current_aggregations: [:tags, :dates]>>
687
- ````
688
-
689
- ##### Pagination
690
-
691
- Search results can be paginated.
692
- ````ruby
693
- search = Article.search("Quick brown fox").page(1).per(100)
694
- search.current_page
695
- => 1
696
-
697
- search.total_pages
698
- => 2546
699
-
700
- > search.total_count
701
- => 254514
702
- ````
703
-
704
- ##### Total count
705
-
706
- By default [ES doesn't return the total number of hits](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-your-data.html#track-total-hits). So, when calling `total_count` or `total_pages` a second request might be sent to ES.
707
- To avoid a second roundtrip, use `track_total_hits`:
708
-
709
- ````ruby
710
- search = Article.search("Quick brown fox")
711
- search.hits
712
- # ArticleSearch Search {…}
713
- # ArticleSearch Search (81.8ms / took 16ms)
714
- => […]
715
-
716
- search.total_count
717
- # ArticleSearch Search {…, track_total_hits: true }
718
- # ArticleSearch Search (135.3ms / took 76ms)
719
- => 276
720
-
721
- search = Article.search("Quick brown fox").track_total_hits
722
- search.hits
723
- # ArticleSearch Search {…, track_total_hits: true }
724
- # ArticleSearch Search (120.2ms / took 56ms)
725
- => […]
726
-
727
- search.total_count
728
- => 276
729
- ````
730
-
731
- ##### Iterating results
732
-
733
- Several methods are provided to loop through a collection or hits or records.
734
- These methods are processing batches in the most efficient way: [PIT search_after](https://www.elastic.co/guide/en/elasticsearch/reference/current/paginate-search-results.html#search-after).
735
-
736
- * `find_each_hit` to yield each hit returned by Elasticsearch.
737
- * `find_each_record` to yield each record from your database.
738
- * `find_hits_in_batches` to yield each batch of hits as returned by Elasticsearch.
739
- * `find_records_in_batches` to yield each batch of records from the database.
740
-
741
- Example:
742
-
743
- ```ruby
744
- Article.search(published: true).find_each_record do |record|
745
- record.inspect
746
- end
747
- ```
748
-
749
- The `keep_alive` parameter tells Elasticsearch how long it should keep the point in time alive. Defaults to 1 minute.
750
-
751
- ```ruby
752
- Article.search(published: true).find_each_record(keep_alive: "2h")
753
- ```
754
-
755
- To specifies the size of the batch, use `per` chainable method or `batch_size` parameter. Defaults to 1000.
756
-
757
- ```ruby
758
- Article.search(published: true).find_records_in_batches(batch_size: 500)
759
- Article.search(published: true).per(500).find_records_in_batches
760
- ```
761
-
762
- ## Testing with Caoutsearch
763
-
764
- Caoutsearch offers few methods to stub Elasticsearch requests.
765
- You first need to add [webmock](https://github.com/bblimke/webmock) to your Gemfile.
766
-
767
- ```bash
768
- bundle add webmock
769
- ```
770
-
771
- Then, add `Caoutsearch::Testing::MockRequests` to your test suite.
772
- The examples below uses RSpec, but it should be compatible with other test framework.
773
-
774
- ```ruby
775
- # spec/spec_helper.rb
776
-
777
- require "caoutsearch/testing"
778
-
779
- RSpec.configure do |config|
780
- config.include Caoutsearch::Testing::MockRequests
781
- end
782
- ```
783
-
784
- You can then call the following methods:
785
-
786
- ```ruby
787
- RSpec.describe SomeClass do
788
- before do
789
- stub_elasticsearch_request(:head, "articles").to_return(status: 200)
790
-
791
- stub_elasticsearch_request(:get, "_cat/indices?format=json&h=index").to_return_json, [
792
- { index: "ca_locals_v14" }
793
- ])
794
-
795
- stub_elasticsearch_reindex_request("articles")
796
- stub_elasticsearch_search_request("articles", [
797
- {"_id" => "135", "_source" => {"name" => "Hello World"}},
798
- {"_id" => "137", "_source" => {"name" => "Hello World"}}
799
- ])
800
- end
801
-
802
- # ... do your tests...
803
- end
66
+ ArticleSearch.search(published_on: [["now-1y", nil]]).aggregate(:popular_tags)
804
67
  ```
805
68
 
806
- `stub_elasticsearch_search_request` accepts an array or records:
807
69
 
808
- ```ruby
809
- RSpec.describe SomeClass do
810
- let(:articles) { create_list(:article, 5) }
811
-
812
- before do
813
- stub_elasticsearch_search_request("articles", articles)
814
- end
70
+ ## Documentation
815
71
 
816
- # ... do your tests...
817
- end
818
- ```
819
-
820
- It allows to shim the total number of hits returned.
821
-
822
- ```ruby
823
- RSpec.describe SomeClass do
824
- before do
825
- stub_elasticsearch_search_request("articles", [], total: 250)
826
- end
827
-
828
- # ... do your tests...
829
- end
830
- ```
72
+ Visit our [offical documentation](https://mon-territoire.github.io/caoutsearch) to understand how to use Caoutsearch.
831
73
 
832
74
  ## Contributing
833
75
 
@@ -5,7 +5,7 @@ module Caoutsearch
5
5
  module Inspect
6
6
  PROPERTIES_TO_INSPECT = %i[
7
7
  search_criteria
8
- current_context
8
+ current_contexts
9
9
  current_order
10
10
  current_page
11
11
  current_limit
@@ -15,13 +15,13 @@ module Caoutsearch
15
15
  # self.config[:filter][key] = ...
16
16
  #
17
17
  class_attribute :config, default: {
18
- contexts: ActiveSupport::HashWithIndifferentAccess.new,
19
18
  filters: ActiveSupport::HashWithIndifferentAccess.new,
20
19
  defaults: ActiveSupport::HashWithIndifferentAccess.new,
21
20
  suggestions: ActiveSupport::HashWithIndifferentAccess.new,
22
21
  sorts: ActiveSupport::HashWithIndifferentAccess.new
23
22
  }
24
23
 
24
+ class_attribute :contexts, instance_accessor: false, default: {}
25
25
  class_attribute :aggregations, instance_accessor: false, default: {}
26
26
  class_attribute :transformations, instance_accessor: false, default: {}
27
27
  end
@@ -32,7 +32,7 @@ module Caoutsearch
32
32
  config[:match_all] = block
33
33
  end
34
34
 
35
- %w[context default].each do |method|
35
+ %w[default].each do |method|
36
36
  config_attribute = method.pluralize.to_sym
37
37
 
38
38
  define_method method do |name = nil, &block|
@@ -68,6 +68,11 @@ module Caoutsearch
68
68
  sort(new_name) { |direction| sort_by(old_name, direction) }
69
69
  end
70
70
 
71
+ def has_context(name, &block)
72
+ self.contexts = contexts.dup
73
+ contexts[name.to_s] = Caoutsearch::Search::DSL::Item.new(name, &block)
74
+ end
75
+
71
76
  def has_aggregation(name, definition = {}, &block)
72
77
  raise ArgumentError, "has_aggregation accepts Hash definition or block but not both" if block && definition.any?
73
78
 
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Caoutsearch
4
+ module Search
5
+ module QueryBuilder
6
+ module Contexts
7
+ private
8
+
9
+ def build_contexts
10
+ call_contexts(*current_contexts) if current_contexts
11
+ end
12
+
13
+ def call_contexts(*args)
14
+ args.each do |arg|
15
+ call_context(arg)
16
+ end
17
+ end
18
+
19
+ def call_context(name)
20
+ name = name.to_s
21
+
22
+ if self.class.contexts.include?(name)
23
+ item = self.class.contexts[name]
24
+ call_context_item(item)
25
+ end
26
+ end
27
+
28
+ def call_context_item(item)
29
+ instance_exec(&item.block)
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -5,6 +5,7 @@ module Caoutsearch
5
5
  module QueryBuilder
6
6
  extend ActiveSupport::Concern
7
7
  include QueryBuilder::Aggregations
8
+ include QueryBuilder::Contexts
8
9
 
9
10
  def build
10
11
  reset_variable(:@elasticsearch_query)
@@ -13,7 +14,7 @@ module Caoutsearch
13
14
  run_callbacks :build do
14
15
  build_prepend_hash
15
16
  build_search_criteria
16
- build_context
17
+ build_contexts
17
18
  build_defaults
18
19
  build_limits
19
20
  build_orders
@@ -35,13 +36,6 @@ module Caoutsearch
35
36
  search_by(search_criteria)
36
37
  end
37
38
 
38
- def build_context
39
- return unless current_context
40
-
41
- item = config[:contexts][current_context.to_s]
42
- instance_exec(&item.block) if item
43
- end
44
-
45
39
  def build_defaults
46
40
  keys = search_criteria_keys.map(&:to_s)
47
41
 
@@ -5,7 +5,7 @@ module Caoutsearch
5
5
  module SearchMethods
6
6
  extend ActiveSupport::Concern
7
7
 
8
- attr_reader :current_context, :current_order, :current_aggregations,
8
+ attr_reader :current_contexts, :current_order, :current_aggregations,
9
9
  :current_suggestions, :current_fields, :current_source
10
10
 
11
11
  # Public API
@@ -97,8 +97,9 @@ module Caoutsearch
97
97
  self
98
98
  end
99
99
 
100
- def context!(value)
101
- @current_context = value
100
+ def context!(*values)
101
+ @current_contexts ||= []
102
+ @current_contexts += values.flatten
102
103
  self
103
104
  end
104
105
 
@@ -173,7 +174,7 @@ module Caoutsearch
173
174
  "aggregations" => :@current_aggregations,
174
175
  "suggest" => :@current_suggestions,
175
176
  "suggestions" => :@current_suggestions,
176
- "context" => :@current_context,
177
+ "context" => :@current_contexts,
177
178
  "order" => :@current_order,
178
179
  "page" => :@current_page,
179
180
  "offset" => :@current_offset,
@@ -189,7 +190,7 @@ module Caoutsearch
189
190
  self
190
191
  end
191
192
 
192
- # Getters
193
+ # Getters and predicates
193
194
  # ------------------------------------------------------------------------
194
195
  def search_criteria
195
196
  @search_criteria ||= []
@@ -213,6 +214,10 @@ module Caoutsearch
213
214
  end
214
215
  end
215
216
 
217
+ def current_context?(name)
218
+ @current_contexts&.map(&:to_s)&.include?(name.to_s)
219
+ end
220
+
216
221
  # Criteria handlers
217
222
  # ------------------------------------------------------------------------
218
223
  def find_criterion(key)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Caoutsearch
4
- VERSION = "0.0.5"
4
+ VERSION = "0.0.6"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: caoutsearch
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.5
4
+ version: 0.0.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Savater Sebastien
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2023-01-30 00:00:00.000000000 Z
12
+ date: 2023-03-21 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activesupport
@@ -147,6 +147,7 @@ files:
147
147
  - lib/caoutsearch/search/query/setters.rb
148
148
  - lib/caoutsearch/search/query_builder.rb
149
149
  - lib/caoutsearch/search/query_builder/aggregations.rb
150
+ - lib/caoutsearch/search/query_builder/contexts.rb
150
151
  - lib/caoutsearch/search/query_methods.rb
151
152
  - lib/caoutsearch/search/records.rb
152
153
  - lib/caoutsearch/search/resettable.rb
@@ -179,7 +180,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
179
180
  - !ruby/object:Gem::Version
180
181
  version: '0'
181
182
  requirements: []
182
- rubygems_version: 3.3.7
183
+ rubygems_version: 3.4.8
183
184
  signing_key:
184
185
  specification_version: 4
185
186
  summary: An alternative approach to index & search with Elasticsearch & Ruby on Rails