caoutsearch 0.0.4 → 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: 3c909350638d5b778a223df94d8b5692984c1060aa1d3a8ade199d9b92a7e278
4
- data.tar.gz: 760e99bffe678f12997a49855b860bd8dd8f85e871e67e01603fa6a1507b58b2
3
+ metadata.gz: 2d22b648c6800d29d075baa757ec80d6c1429752918db6fb899fb7484d3ff9f7
4
+ data.tar.gz: 03fdbf0e6cd2dcc966bf435e9ea7e48ede4e14bd0ae3ec50215cb6867b07d90a
5
5
  SHA512:
6
- metadata.gz: 7f2ed2ab8c08ba6d7a2ac9077a8c59e5bb92b9606ab3fd825b8b10fdea039a2c2a3aefa7b2705c98601e150de5b48becb0cea7dbfa4eb15256e5b650f0251523
7
- data.tar.gz: e33c6d5ed88ecd9d3fbd03aad636bd3c636098775c8a35295df8aa3ec8518cccd1ea125689052a25fab3270af22739d9977b3bda2e6511ac9d427dd0fb94b401
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,814 +17,59 @@ 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
45
 
106
- Caoutsearch different types of filters to handle different types of data or ways to search them:
107
-
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 :
142
-
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
54
+ filter :tags
185
55
 
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
- has_aggregation :popular_tags,
56
+ has_aggregation :popular_tags, {
192
57
  filter: { term: { published: true } },
193
58
  aggs: {
194
59
  published: {
195
60
  terms: { field: :tags, size: 10 }
196
61
  }
197
62
  }
63
+ }
198
64
  end
199
- ````
200
-
201
- Then you can request one or more aggregations at the same time or chain the `aggregate` method.
202
- The `aggregations` method will trigger a request and returns a [Response::Aggregations](#responses).
203
-
204
- ````ruby
205
- ArticleSearch.aggregate(:view_count).aggregations
206
- # ArticleSearch Search { "body": { "aggs": { "view_count": { "sum": { "field": "view_count" }}}}}
207
- # ArticleSearch Search (10ms / took 5ms)
208
- => #<Caoutsearch::Response::Aggregations view_count=#<Caoutsearch::Response::Response value=119652>>
209
-
210
- ArticleSearch.aggregate(:view_count, :popular_tags).aggregations
211
- # ArticleSearch Search { "body": { "aggs": { "view_count": {…}, "popular_tags": {…}}}}
212
- # ArticleSearch Search (10ms / took 5ms)
213
- => #<Caoutsearch::Response::Aggregations view_count=#<Caoutsearch::Response::Response value=119652> popular_tags=#<Caoutsearch::Response::Response buckets=…>>
214
-
215
- ArticleSearch.aggregate(:view_count).aggregate(:popular_tags).aggregations
216
- # ArticleSearch Search { "body": { "aggs": { "view_count": {…}, "popular_tags": {…}}}}
217
- # ArticleSearch Search (10ms / took 5ms)
218
- => #<Caoutsearch::Response::Aggregations view_count=#<Caoutsearch::Response::Response value=119652> popular_tags=#<Caoutsearch::Response::Response buckets=…>>
219
- ````
220
-
221
- You can create powerful aggregations using blocks and pass arguments to them.
222
-
223
- ````ruby
224
- class ArticleSearch < Caoutsearch::Search::Base
225
- has_aggregation :popular_tags_since do |date|
226
- raise TypeError unless date.is_a?(Date)
227
-
228
- query.aggregations[:popular_tags_since] = {
229
- filter: { range: { publication_date: { gte: date.to_s } } },
230
- aggs: {
231
- published: {
232
- terms: { field: :tags, size: 20 }
233
- }
234
- }
235
- }
236
- end
237
- end
238
-
239
- ArticleSearch.aggregate(popular_tags_since: 1.day.ago).aggregations
240
- # ArticleSearch Search { "body": { "aggs": { "popular_tags_since": {…}}}}
241
- # ArticleSearch Search (10ms / took 5ms)
242
- => #<Caoutsearch::Response::Aggregations popular_tags_since=#<Caoutsearch::Response::Response …
243
- ````
244
-
245
- Only one argument can be passed to an aggregation block.
246
- Use an Array or a Hash if you need to pass multiple options.
247
-
248
- ````ruby
249
- class ArticleSearch < Caoutsearch::Search::Base
250
- has_aggregation :popular_tags_since do |options|
251
- # …
252
- end
253
-
254
- has_aggregation :popular_tags_between do |(first_date, end_date)|
255
- # …
256
- end
257
- end
258
-
259
- ArticleSearch.aggregate(popular_tags_since: { date: 1.day.ago, size: 20 })
260
- ArticleSearch.aggregate(popular_tags_between: [date1, date2])
261
- ````
262
-
263
- Finally, you can create a "catch-all" aggregation to handle cumbersome behaviors:
264
-
265
- ````ruby
266
- class ArticleSearch < Caoutsearch::Search::Base
267
- has_aggregation do |name, options = {}|
268
- raise "unxpected_error" unless name.match?(/^view_count_(?<year>\d{4})$/)
269
-
270
- query.aggregations[name] = {
271
- filter: { term: { year: $LAST_LATCH_INFO[:year] } },
272
- aggs: {
273
- filtered: {
274
- sum: { field: :view_count }
275
- }
276
- }
277
- }
278
- end
279
- end
280
-
281
- ArticleSearch.aggregate(:view_count_2020, :view_count_2019).aggregations
282
- # ArticleSearch Search { "body": { "aggs": { "view_count_2020": {…}, "view_count_2019": {…}}}}
283
- # ArticleSearch Search (10ms / took 5ms)
284
- => #<Caoutsearch::Response::Aggregations view_count_2020=#<Caoutsearch::Response::Response …
285
- ````
286
-
287
- #### Transform aggregations
288
-
289
- 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.
290
-
291
- ````ruby
292
- ArticleSearch.aggregate(popular_tags_since: 1.month.ago).aggregations.popular_tags_since.published.buckets.pluck(:key)
293
- => ["Blog", "Tech", …]
294
- ````
295
-
296
- Instead, you can define transformations to provide simpler access to aggregated data:
297
-
298
- ````ruby
299
- class ArticleSearch < Caoutsearch::Search::Base
300
- has_aggregation :popular_tags_since do |since|
301
- # …
302
- end
303
-
304
- transform_aggregation :popular_tags_since do |aggs|
305
- aggs.dig(:popular_tags_since, :published, :buckets).pluck(:key)
306
- end
307
- end
308
-
309
- ArticleSearch.aggregate(popular_tags_since: 1.month.ago).aggregations.popular_tags_since
310
- => ["Blog", "Tech", …]
311
- ````
312
-
313
- You can also use transformations to combine multiple aggregations:
314
-
315
- ````ruby
316
- class ArticleSearch < Caoutsearch::Search::Base
317
- has_aggregation :blog_count, filter: { term: { category: "blog" } }
318
- has_aggregation :archives_count, filter: { term: { archived: true } }
319
-
320
- transform_aggregation :stats, from: %i[blog_count archives_count] do |aggs|
321
- {
322
- blog_count: aggs.dig(:blog_count, :doc_count),
323
- archives_count: aggs.dig(:archives, :doc_count)
324
- }
325
- end
326
- end
327
-
328
- ArticleSearch.aggregate(:stats).aggregations.stats
329
- # ArticleSearch Search { "body": { "aggs": { "blog_count": {…}, "archives_count": {…}}}}
330
- # ArticleSearch Search (10ms / took 5ms)
331
- => { blog_count: 124, archives_count: 2452 }
332
- ````
333
-
334
- This is also usefull to unify the API between different search engines:
335
-
336
- ````ruby
337
- class ArticleSearch < Caoutsearch::Search::Base
338
- has_aggregation :popular_tags,
339
- filter: { term: { published: true } },
340
- aggs: { published: { terms: { field: :tags, size: 10 } } }
341
-
342
- transform_aggregation :popular_tags do |aggs|
343
- aggs.dig(:popular_tags, :published, :buckets).pluck(:key)
344
- end
345
- end
346
-
347
- class TagSearch < Caoutsearch::Search::Base
348
- has_aggregation :popular_tags,
349
- terms: { field: "label", size: 20, order: { used_count: "desc" } }
350
-
351
- transform_aggregation :popular_tags do |aggs|
352
- aggs.dig(:popular_tags, :buckets).pluck(:key)
353
- end
354
- end
355
-
356
- ArticleSearch.aggregate(:popular_tags).aggregations.popular_tags
357
- => ["Blog", "Tech", …]
358
-
359
- TagSearch.aggregate(:popular_tags).aggregations.popular_tags
360
- => ["Tech", "Blog", …]
361
- ````
362
-
363
- Transformations are performed on demand and result is memorized. That means:
364
- - the result of transformation is not visible in the [Response::Aggregations](#responses) output.
365
- - the block is called only once for the same search instance.
366
-
367
- ````ruby
368
- class ArticleSearch < Caoutsearch::Search::Base
369
- has_aggregation :popular_tags, …
370
-
371
- transform_aggregation :popular_tags do |aggs|
372
- tags = aggs.dig(:popular_tags, :published, :buckets).pluck(:key)
373
- authorized = Tag.where(title: tags, authorize: true).pluck(:title)
374
- tags & authorized
375
- end
376
- end
377
-
378
- article_search = ArticleSearch.aggregate(:popular_tags)
379
- => #<ArticleSearch current_aggregations: [:popular_tags]>
380
-
381
- article_search.aggregations
382
- # ArticleSearch Search (10ms / took 5ms)
383
- => #<Caoutsearch::Response::Aggregations popular_tags=#<Caoutsearch::Response::Response doc_count=100 …
384
-
385
- article_search.aggregations.popular_tags
386
- # (10.2ms) SELECT "tags"."title" FROM "tags" WHERE "tags"."title" IN …
387
- => ["Blog", "Tech", …]
388
-
389
- article_search.aggregations.popular_tags
390
- => ["Blog", "Tech", …]
391
-
392
- article_search.search("Tech").aggregations.popular_tags
393
- # ArticleSearch Search (10ms / took 5ms)
394
- # (10.2ms) SELECT "tags"."title" FROM "tags" WHERE "tags"."title" IN …
395
- => ["Blog", "Tech", …]
396
- ````
397
-
398
- Be careful to avoid using `aggregations.<aggregation_name>` inside a transformation block: it can lead to an infinite recursion.
399
-
400
- ````ruby
401
- class ArticleSearch < Caoutsearch::Search::Base
402
- transform_aggregation :popular_tags do
403
- aggregations.popular_tags.buckets.pluck("key")
404
- end
405
- end
406
-
407
- ArticleSearch.aggregate(:popular_tags).aggregations.popular_tags
408
- Traceback (most recent call last):
409
- 4: from app/searches/article_search.rb:3:in `block in <class:ArticleSearch>'
410
- 3: from app/searches/article_search.rb:3:in `block in <class:ArticleSearch>'
411
- 2: from app/searches/article_search.rb:3:in `block in <class:ArticleSearch>'
412
- 1: from app/searches/article_search.rb:3:in `block in <class:ArticleSearch>'
413
- SystemStackError (stack level too deep)
414
- ````
415
-
416
- 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).
417
-
418
- ````ruby
419
- class ArticleSearch < Caoutsearch::Search::Base
420
- transform_aggregation :popular_tags do |aggs|
421
- aggs.popular_tags.buckets.pluck("key")
422
- end
423
- end
424
-
425
- ArticleSearch.aggregate(:popular_tags).aggregations.popular_tags
426
- => ["Blog", "Tech", …]
427
- ````
428
-
429
- 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.
430
- Take a look at [Total count](#total-count) to understand why a second request could be performed.
431
-
432
- ````ruby
433
- class ArticleSearch < Caoutsearch::Search::Base
434
- aggregation :tagged, filter: { exist: "tag" }
435
-
436
- transform_aggregation :tagged_rate, from: :tagged, track_total_hits: true do |aggs|
437
- count = aggs.dig(:tagged, :doc_count)
438
- count.to_f / total_count
439
- end
440
-
441
- transform_aggregation :tagged_rate_without_track_total_hits, from: :tagged do |aggs|
442
- count = aggs.dig(:tagged, :doc_count)
443
- count.to_f / total_count
444
- end
445
- end
446
-
447
- ArticleSearch.aggregate(:tagged_rate).aggregations.tagged_rate
448
- # ArticleSearch Search { "body": { "track_total_hits": true, "aggs": { "blog_count": {…}, "archives_count": {…}}}}
449
- # ArticleSearch Search (10ms / took 5ms)
450
- => 0.95
451
-
452
- ArticleSearch.aggregate(:tagged_rate_without_track_total_hits).aggregations.tagged_rate
453
- # ArticleSearch Search { "body": { "aggs": { "blog_count": {…}, "archives_count": {…}}}}
454
- # ArticleSearch Search (10ms / took 5ms)
455
- # ArticleSearch Search { "body": { "track_total_hits": true, "aggs": { "blog_count": {…}, "archives_count":
456
- # ArticleSearch Search (10ms / took 5ms)
457
- => 0.95
458
- ````
459
-
460
- #### Responses
461
-
462
- 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).
463
-
464
- Aggregations and suggestions are wrapped in their own respective subclass of `Response::Response`
465
-
466
- ````ruby
467
- results.response
468
- => #<Caoutsearch::Response::Response _shards=#<Caoutsearch::Response::Response failed=0 skipped=0 successful=5 total=5> hits=…
469
-
470
- search.hits
471
- => #<Hashie::Array [#<Caoutsearch::Response::Response _id="2"…
472
-
473
- search.aggregations
474
- => #<Caoutsearch::Response::Aggregations view_count=#<Caoutsearch::Response::Response…
475
-
476
- search.suggestions
477
- => #<Caoutsearch::Response::Suggestions tags=#<Caoutsearch::Response::Response…
478
- ````
479
-
480
- ##### Loading records
481
-
482
- When calling `records`, the search engine will try to load records from a model using the same class name without `Search` the suffix:
483
- * `ArticleSearch` > `Article`
484
- * `Blog::ArticleSearch` > `Blog::Article`
485
-
486
- ````ruby
487
- ArticleSearch.new.records.first
488
- # ArticleSearch Search (10ms / took 5ms)
489
- # Article Load (9.6ms) SELECT "articles".* FROM "articles" WHERE "articles"."id" IN (1, …
490
- => #<Article id: 1, …>
491
- ````
492
-
493
- 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).
494
-
495
- ````ruby
496
- ArticleSearch.new.records(use: BlogArticle).first
497
- # ArticleSearch Search (10ms / took 5ms)
498
- # BlogArticle Load (9.6ms) SELECT "articles".* FROM "articles" WHERE "articles"."id" IN (1, …
499
- => #<BlogArticle id: 1, …>
500
- ````
501
-
502
- You can also define an alternative model at class level:
503
-
504
- ````ruby
505
- class BlogArticleSearch < Caoutsearch::Search::Base
506
- self.model_name = "Article"
507
-
508
- default do
509
- query.filters << { term: { category: "blog" } }
510
- end
511
- end
512
-
513
- BlogArticleSearch.new.records.first
514
- # BlogArticleSearch Search (10ms / took 5ms)
515
- # Article Load (9.6ms) SELECT "articles".* FROM "articles" WHERE "articles"."id" IN (1, …
516
- => #<Article id: 1, …>
517
- ````
518
-
519
- ### Model integration
520
-
521
- #### Add Caoutsearch to your models
522
-
523
- The simplest solution is to add `Caoutsearch::Model` to your model and the link the appropriate `Index` and/or `Search` engines:
524
-
525
- ```ruby
526
- class Article < ActiveRecord::Base
527
- include Caoutsearch::Model
528
-
529
- index_with ArticleIndex
530
- search_with ArticleSearch
531
- end
532
- ```
533
-
534
- If you don't need your models to be `Indexable` and `Searchable`, you can include only one of the following two modules:
535
-
536
- ````ruby
537
- class Article < ActiveRecord::Base
538
- include Caoutsearch::Model::Indexable
539
-
540
- index_with ArticleIndex
541
- end
542
- ````
543
- or
544
- ````ruby
545
- class Article < ActiveRecord::Base
546
- include Caoutsearch::Model::Searchable
547
-
548
- search_with ArticleSearch
549
- end
550
- ````
551
-
552
- The modules can be safely included in the meta model `ApplicationRecord`.
553
- Indexing & searching features are not available until you call `index_with` or `search_with`:
554
-
555
- ````ruby
556
- class ApplicationRecord < ActiveRecord::Base
557
- include Caoutsearch::Model
558
- end
559
- ````
560
-
561
- #### Index records
562
-
563
- ##### Index multiple records
564
-
565
- Import all your records or a restricted scope of records to Elastcisearch.
566
-
567
- ````ruby
568
- Article.reindex
569
- Article.where(published: true).reindex
570
- ````
571
-
572
- You can update one or more properties. (see [Indexation Engines](#indexation-engines) to read more about properties):
573
-
574
- ````ruby
575
- Article.reindex(:category)
576
- Article.reindex(%i[category published_on])
577
- ````
578
-
579
- When `reindex` is called without properties, it'll import the full document to ES.
580
- On the contrary, when properties are passed, it'll only update existing documents.
581
- You can control this behavior with the `method` argument.
582
-
583
- ````ruby
584
- Article.where(id: 123).reindex(:category)
585
- # ArticleIndex Reindex {"index":"articles","body":[{"update":{"_id":123}},{"doc":{"category":"blog"}}]}
586
- # [Error] {"update"=>{"_index"=>"articles", "_id"=>"123", "status"=>404, "error"=>{"type"=>"document_missing_exception", …}}
587
-
588
- Article.where(id: 123).reindex(:category, method: :index)
589
- # ArticleIndex Reindex {"index":"articles","body":[{"index":{"_id":123}},{"category":"blog"}]}
590
-
591
- Article.where(id: 123).reindex(method: :update)
592
- # ArticleIndex Reindex {"index":"articles","body":[{"update":{"_id":123}},{"doc":{…}}]}
593
- ````
594
-
595
- ##### Index single records
596
-
597
- Import a single record.
598
-
599
- ````ruby
600
- Article.find(123).update_index
601
- ````
602
-
603
- You can update one or more properties. (see [Indexation Engines](#indexation-engines) to read more about properties):
604
-
605
- ````ruby
606
- Article.find(123).update_index(:category)
607
- Article.find(123).update_index(%i[category published_on])
608
- ````
609
-
610
- You can verify if and how documents are indexed.
611
- If the document is missing in ES, it'll raise a `Elastic::Transport::Transport::Errors::NotFound`.
612
-
613
- ````ruby
614
- Article.find(123).indexed_document
615
- # Traceback (most recent call last):
616
- # 1: from (irb):1
617
- # Elastic::Transport::Transport::Errors::NotFound ([404] {"_index":"articles","_id":"123","found":false})
618
-
619
- Article.find(123).update_index
620
- Article.find(123).indexed_document
621
- => {"_index"=>"articles", "_id"=>"123", "_version"=>1"found"=>true, "_source"=>{…}}
622
- ````
623
-
624
- ##### Delete documents
625
-
626
- You can delete one or more documents.
627
- **Note**: it won't delete records from database, only from the ES indice.
628
-
629
- ````ruby
630
- Article.delete_indexes
631
- Article.where(id: 123).delete_indexed_documents
632
- Article.find(123).delete_index
633
- ````
634
-
635
- If a record is already deleted from the database, you can still delete its document.
636
-
637
- ````ruby
638
- Article.delete_index(123)
639
- ````
640
-
641
- ##### Automatic Callbacks
642
-
643
- Callbacks are not provided by Caoutsearch but they are very easy to add:
644
-
645
- ````ruby
646
- class Article < ApplicationRecord
647
- index_with ArticleIndex
648
-
649
- after_commit :update_index, on: %i[create update]
650
- after_commit :delete_index, on: %i[destroy]
651
- end
652
- ````
653
-
654
- ##### Asynchronous methods
655
-
656
- TODO
657
-
658
- #### Search for records
659
-
660
- ##### Search API
661
- Searching is pretty simple.
662
-
663
- ````ruby
664
- Article.search("Quick brown fox")
665
- => #<ArticleSearch current_criteria: ["Quick brown fox"]>
666
- ````
667
-
668
- You can chain criteria and many other parameters:
669
- ````ruby
670
- Article.search("Quick brown fox").search(published: true)
671
- => #<ArticleSearch current_criteria: ["Quick brown fox", {"published"=>true}]>
672
-
673
- Article.search("Quick brown fox").order(:publication_date)
674
- => #<ArticleSearch current_criteria: ["Quick brown fox"], current_order: :publication_date>
675
-
676
- Article.search("Quick brown fox").limit(100).offset(100)
677
- => #<ArticleSearch current_criteria: ["Quick brown fox"], current_limit: 100, current_offset: 100>
678
-
679
- Article.search("Quick brown fox").page(1).per(100)
680
- => #<ArticleSearch current_criteria: ["Quick brown fox"], current_page: 1, current_limit: 100>
681
-
682
- Article.search("Quick brown fox").aggregate(:tags).aggregate(:dates)
683
- => #<ArticleSearch current_criteria: ["Quick brown fox"], current_aggregations: [:tags, :dates]>>
684
- ````
685
-
686
- ##### Pagination
687
-
688
- Search results can be paginated.
689
- ````ruby
690
- search = Article.search("Quick brown fox").page(1).per(100)
691
- search.current_page
692
- => 1
693
-
694
- search.total_pages
695
- => 2546
696
-
697
- > search.total_count
698
- => 254514
699
- ````
700
-
701
- ##### Total count
702
-
703
- 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.
704
- To avoid a second roundtrip, use `track_total_hits`:
705
-
706
- ````ruby
707
- search = Article.search("Quick brown fox")
708
- search.hits
709
- # ArticleSearch Search {…}
710
- # ArticleSearch Search (81.8ms / took 16ms)
711
- => […]
712
-
713
- search.total_count
714
- # ArticleSearch Search {…, track_total_hits: true }
715
- # ArticleSearch Search (135.3ms / took 76ms)
716
- => 276
717
-
718
- search = Article.search("Quick brown fox").track_total_hits
719
- search.hits
720
- # ArticleSearch Search {…, track_total_hits: true }
721
- # ArticleSearch Search (120.2ms / took 56ms)
722
- => […]
723
-
724
- search.total_count
725
- => 276
726
- ````
727
-
728
- ##### Iterating results
729
-
730
- Several methods are provided to loop through a collection or hits or records.
731
- 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).
732
-
733
- * `find_each_hit` to yield each hit returned by Elasticsearch.
734
- * `find_each_record` to yield each record from your database.
735
- * `find_hits_in_batches` to yield each batch of hits as returned by Elasticsearch.
736
- * `find_records_in_batches` to yield each batch of records from the database.
737
-
738
- Example:
739
-
740
- ```ruby
741
- Article.search(published: true).find_each_record do |record|
742
- record.inspect
743
- end
744
- ```
745
-
746
- The `keep_alive` parameter tells Elasticsearch how long it should keep the point in time alive. Defaults to 1 minute.
747
-
748
- ```ruby
749
- Article.search(published: true).find_each_record(keep_alive: "2h")
750
- ```
751
-
752
- To specifies the size of the batch, use `per` chainable method or `batch_size` parameter. Defaults to 1000.
753
-
754
- ```ruby
755
- Article.search(published: true).find_records_in_batches(batch_size: 500)
756
- Article.search(published: true).per(500).find_records_in_batches
757
- ```
758
-
759
- ## Testing with Caoutsearch
760
-
761
- Caoutsearch offers few methods to stub Elasticsearch requests.
762
- You first need to add [webmock](https://github.com/bblimke/webmock) to your Gemfile.
763
-
764
- ```bash
765
- bundle add webmock
766
- ```
767
-
768
- Then, add `Caoutsearch::Testing::MockRequests` to your test suite.
769
- The examples below uses RSpec, but it should be compatible with other test framework.
770
-
771
- ```ruby
772
- # spec/spec_helper.rb
773
-
774
- require "caoutsearch/testing"
775
-
776
- RSpec.configure do |config|
777
- config.include Caoutsearch::Testing::MockRequests
778
- end
779
- ```
780
-
781
- You can then call the following methods:
782
-
783
- ```ruby
784
- RSpec.describe SomeClass do
785
- before do
786
- stub_elasticsearch_request(:head, "articles").to_return(status: 200)
787
-
788
- stub_elasticsearch_request(:get, "_cat/indices?format=json&h=index").to_return_json, [
789
- { index: "ca_locals_v14" }
790
- ])
791
-
792
- stub_elasticsearch_reindex_request("articles")
793
- stub_elasticsearch_search_request("articles", [
794
- {"_id" => "135", "_source" => {"name" => "Hello World"}},
795
- {"_id" => "137", "_source" => {"name" => "Hello World"}}
796
- ])
797
- end
798
-
799
- # ... do your tests...
800
- end
801
- ```
802
-
803
- `stub_elasticsearch_search_request` accepts an array or records:
804
-
805
- ```ruby
806
- RSpec.describe SomeClass do
807
- let(:articles) { create_list(:article, 5) }
808
-
809
- before do
810
- stub_elasticsearch_search_request("articles", articles)
811
- end
812
65
 
813
- # ... do your tests...
814
- end
66
+ ArticleSearch.search(published_on: [["now-1y", nil]]).aggregate(:popular_tags)
815
67
  ```
816
68
 
817
- It allows to shim the total number of hits returned.
818
69
 
819
- ```ruby
820
- RSpec.describe SomeClass do
821
- before do
822
- stub_elasticsearch_search_request("articles", [], total: 250)
823
- end
70
+ ## Documentation
824
71
 
825
- # ... do your tests...
826
- end
827
- ```
72
+ Visit our [offical documentation](https://mon-territoire.github.io/caoutsearch) to understand how to use Caoutsearch.
828
73
 
829
74
  ## Contributing
830
75
 
@@ -27,8 +27,6 @@ module Caoutsearch
27
27
  build_range_query(input)
28
28
  when ::Hash
29
29
  case input
30
- in { operator:, value:, **other}
31
- build_legacy_range_query_from_hash(input)
32
30
  in { between: dates }
33
31
  build_range_query(dates)
34
32
  else
@@ -61,23 +59,6 @@ module Caoutsearch
61
59
  query
62
60
  end
63
61
 
64
- def build_legacy_range_query_from_hash(input)
65
- ActiveSupport::Deprecation.warn("This form of hash to search for dates will be removed")
66
- operator, value, unit = input.values_at(:operator, :value, :unit)
67
-
68
- case operator
69
- when "less_than"
70
- {range: {key => {gte: cast_date(value, unit)}}}
71
- when "greater_than"
72
- {range: {key => {lt: cast_date(value, unit)}}}
73
- when "between"
74
- dates = value.map { |v| cast_date(v, unit) }.sort
75
- {range: {key => {gte: dates[0], lt: dates[1]}}}
76
- else
77
- raise ArgumentError, "unknown operator #{operator.inspect} in #{value.inspect}"
78
- end
79
- end
80
-
81
62
  RANGE_OPERATORS = {
82
63
  "less_than" => "lt",
83
64
  "less_than_or_equal" => "lte",
@@ -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,11 +68,16 @@ module Caoutsearch
68
68
  sort(new_name) { |direction| sort_by(old_name, direction) }
69
69
  end
70
70
 
71
- def has_aggregation(name, **options, &block)
72
- raise ArgumentError, "has_aggregation accepts options or block but not both" if block && options.any?
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
+
76
+ def has_aggregation(name, definition = {}, &block)
77
+ raise ArgumentError, "has_aggregation accepts Hash definition or block but not both" if block && definition.any?
73
78
 
74
79
  self.aggregations = aggregations.dup
75
- aggregations[name.to_s] = Caoutsearch::Search::DSL::Item.new(name, options, &block)
80
+ aggregations[name.to_s] = Caoutsearch::Search::DSL::Item.new(name, definition, &block)
76
81
  end
77
82
 
78
83
  def alias_aggregation(new_name, old_name)
@@ -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.4"
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.4
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-04 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.1.6
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