caoutsearch 0.0.5 → 0.0.6

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.
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