mongodb_meilisearch 1.0.0 → 1.1.0

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: 90bfdb172f8ff15ffe0d490cbfc03939de3f3b05881b830f0515807f35882e96
4
- data.tar.gz: 1c680831f8b35d5ee540fab9e7ec47b3b093f02a5be054a1b0c393597930e556
3
+ metadata.gz: f57c18174244b2885536a990a2d602ce833e949dcf205112c78f30567a6eee6a
4
+ data.tar.gz: bb5bdd6c8cb90a6e1334c4606063a56b71515510f0f45c3e1ae2d817b4497163
5
5
  SHA512:
6
- metadata.gz: f10fe61be5b67e4e5264abed8adabb41ecdb6a3e2cbdca2ae23f5979d552498a018bf327c682cc28e54b37ce16b9c50f74dca294eb55a0acbff5277a5f0389f0
7
- data.tar.gz: 5d39d79ddf1bc213760959fdd4ebee1451cc2b2c1a6a48b1a24e6e74bd3dff688121e12d2dad8fc103c38db14d7766fdc2c6117b592cf2445eebfd2aaa6773b9
6
+ metadata.gz: cb3e4f69dad6365682470020e3acc864492c45d79de54fb65fbab6fb9eaacb0811d8af02414f4df324a9d0cf64278646e0eac3062e7aff1a9873aa2f264d0d64
7
+ data.tar.gz: 54aa4eea1cfe0b378e9ad93c719eb31a5c35c96c08ef842d975efb8391cf1b55b0ca53e9ca3d8daf98626a02c30ea817e9623c78de0b6ce14ade726473767304
@@ -0,0 +1 @@
1
+ {"type":"Documented","tickets":[],"description":"README: improved example code.","tags":[]}
@@ -0,0 +1 @@
1
+ {"git_add":true}
data/.rubocop.yml CHANGED
@@ -1116,12 +1116,6 @@ Style/ComparableClamp:
1116
1116
  Style/ConcatArrayLiterals:
1117
1117
  Enabled: false
1118
1118
 
1119
- Style/ConditionalAssignment:
1120
- Enabled: true
1121
- EnforcedStyle: assign_to_condition
1122
- SingleLineConditionsOnly: true
1123
- IncludeTernaryExpressions: true
1124
-
1125
1119
  Style/ConstantVisibility:
1126
1120
  Enabled: false
1127
1121
 
@@ -1857,3 +1851,11 @@ Style/YodaExpression:
1857
1851
  Style/ZeroLengthPredicate:
1858
1852
  Enabled: false
1859
1853
 
1854
+ # this is horrible for readability
1855
+ # and maintainability
1856
+ Style/ConditionalAssignment:
1857
+ Enabled: false
1858
+
1859
+ # I'll nest how I wanna nest! :p
1860
+ RSpec/NestedGroups:
1861
+ Enabled: false
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- mongodb_meilisearch (1.0.0)
4
+ mongodb_meilisearch (1.1.0)
5
5
  meilisearch
6
6
  mongoid (~> 7.0)
7
7
  rails
@@ -131,6 +131,8 @@ GEM
131
131
  nio4r (2.5.9)
132
132
  nokogiri (1.15.3-arm64-darwin)
133
133
  racc (~> 1.4)
134
+ nokogiri (1.15.3-x86_64-linux)
135
+ racc (~> 1.4)
134
136
  parallel (1.23.0)
135
137
  parser (3.2.2.3)
136
138
  ast (~> 2.4.1)
@@ -220,6 +222,7 @@ GEM
220
222
 
221
223
  PLATFORMS
222
224
  arm64-darwin-22
225
+ x86_64-linux
223
226
 
224
227
  DEPENDENCIES
225
228
  debug
data/README.md CHANGED
@@ -1,3 +1,14 @@
1
+ # <!-- :TOC: -->
2
+ - [MongodbMeilisearch](#mongodbmeilisearch)
3
+ - [Installation](#installation)
4
+ - [Usage](#usage)
5
+ - [Model Integration](#model-integration)
6
+ - [Indexes](#indexes)
7
+ - [Searching](#searching)
8
+ - [Development](#development)
9
+ - [License](#license)
10
+ - [Code of Conduct](#code-of-conduct)
11
+
1
12
  # MongodbMeilisearch
2
13
 
3
14
  A simple gem for integrating [Meilisearch](https://www.meilisearch.com) into Ruby† applications that are backed by [MongoDB](https://www.mongodb.com/).
@@ -35,7 +46,10 @@ SEARCH_ENABLED=true
35
46
  MEILISEARCH_API_KEY=<your api key here>
36
47
  MEILISEARCH_URL=http://127.0.0.1:7700
37
48
 
38
- # optional configuration
49
+ ```
50
+
51
+ Optional configuration
52
+ ```bash
39
53
  MEILISEARCH_TIMEOUT=10
40
54
  MEILISEARCH_MAX_RETRIES=2
41
55
  ```
@@ -46,8 +60,8 @@ Add the following near the top of your model. Only the `extend` and `include` li
46
60
  This assumes your model also includes `Mongoid::Document`
47
61
 
48
62
  ```ruby
49
- extend Search::ClassMethods
50
63
  include Search::InstanceMethods
64
+ extend Search::ClassMethods
51
65
  ```
52
66
 
53
67
  If you want Rails to automatically add, update, and delete records from the index, add the following to your model.
@@ -65,19 +79,46 @@ You can override these methods if needed, but you're unlikely to want to.
65
79
 
66
80
  Assuming you've done the above a new index will be created with a name that
67
81
  corresponds to your model's name, only in snake case. All of your models
68
- attributes will be indexed and [filterable](https://www.meilisearch.com/docs/learn/fine_tuning_results/filtering).
82
+ fields will be indexed and [filterable](https://www.meilisearch.com/docs/learn/fine_tuning_results/filtering).
83
+
84
+
85
+ ### Example Rails Model
86
+
87
+ Here's what it looks like when you put it all
88
+ together in a Rails model with the default behavior.
69
89
 
90
+ ```ruby
91
+ class Person
92
+ include Mongoid::Document
93
+ extend Search::ClassMethods
94
+
95
+ if Search::Client.instance.enabled?
96
+ after_create :add_to_search
97
+ after_update :update_in_search
98
+ after_destroy :remove_from_search
99
+ end
100
+
101
+ # normal Mongoid attributes
102
+ field :name, type: String
103
+ field :description, type: String
104
+ field :age, type: Integer
105
+ end
106
+ ```
107
+
108
+ Note that that _unless you configure it otherwise_ the ids of `belongs_to` objects
109
+ will not be searchable. This is because they're random strings that no human's ever
110
+ going to be searching for, and we don't want to waste RAM or storage.
70
111
 
71
112
  ### Going Beyond The Defaults
72
113
  This module strives for sensible defaults, but you can override them with the
73
114
  following optional constants:
74
115
 
75
- * `PRIMARY_SEARCH_KEY` - a Symbol matching one of your model's attributes
116
+ - `PRIMARY_SEARCH_KEY` - a Symbol matching one of your model's attributes
76
117
  that is guaranteed unique. This defaults to `_id`
77
- * `SEARCH_INDEX_NAME` - a String - useful if you want to have records from
118
+ - `SEARCH_INDEX_NAME` - a String - useful if you want to have records from
78
119
  multiple classes come back in the same search results. This defaults to the
79
120
  underscored form of the current class name.
80
- * `SEARCH_OPTIONS` - a hash of key value pairs in JS style
121
+ - `SEARCH_OPTIONS` - a hash of key value pairs in JS style
81
122
  - See the [meilisearch search parameter docs](https://www.meilisearch.com/docs/reference/api/search#search-parameters) for details.
82
123
  - example from [meliesearch's `multi_param_spec`](https://github.com/meilisearch/meilisearch-ruby/blob/main/spec/meilisearch/index/search/multi_params_spec.rb)
83
124
  ```ruby
@@ -89,7 +130,7 @@ following optional constants:
89
130
  limit: 2
90
131
  }
91
132
  ```
92
- * `SEARCH_RANKING_RULES` - an array of strings that correspond to meilisearch rules
133
+ - `SEARCH_RANKING_RULES` - an array of strings that correspond to meilisearch rules
93
134
  see [meilisearch ranking rules docs](https://www.meilisearch.com/docs/learn/core_concepts/relevancy#ranking-rules)
94
135
  You probably don't want to change this.
95
136
 
@@ -126,7 +167,7 @@ as `original_document_id`. This is useful if you want to be able to retrieve the
126
167
  You probably don't want to index _all_ the fields. For example,
127
168
  unless you intend to allow users to sort by when a record was created,
128
169
  there's no point in recording it's `created_at` in the search index.
129
- It'll just waste bandwidth, memory, and disk space.
170
+ It'll just waste bandwidth, memory, and disk space.
130
171
 
131
172
  Define a `SEARCHABLE_ATTRIBUTES` constant with an array of strings to limit things.
132
173
  By default these will _also_ be the fields you can filter on. Note that
@@ -142,6 +183,15 @@ document's `BSON::ObjectId`.
142
183
  SEARCHABLE_ATTRIBUTES = searchable_attributes - [:created_at]
143
184
  ```
144
185
 
186
+ #### Including Foreign Key data
187
+ If, for example, your `Person` `belongs_to: group`
188
+ and you wanted that group's id to be searchable you would include `group_id`
189
+ in the list.
190
+
191
+ If you don't specify any `SEARCHABLE_ATTRIBUTES`, the default list will
192
+ exclude any fields that are `Mongoid::Fields::ForeignKey` objects.
193
+
194
+
145
195
  #### Getting Extra Specific
146
196
  If your searchable data needs to by dynamically generated instead of
147
197
  just taken directly from the `Mongoid::Document`'s attributes you can
@@ -150,8 +200,8 @@ must return a hash, and that hash must include the following keys:
150
200
  - `"id"` - a string that uniquely identifies the record
151
201
  - `"object_class"` the name of the class that this record corresponds to.
152
202
 
153
- The value of `"object_class"` is usually just `self.class.name`. Additionally,
154
- this is something specific to this gem, and not Meilisearch itself.
203
+ The value of `"object_class"` is usually just `self.class.name`.
204
+ This is something specific to this gem, and not Meilisearch itself.
155
205
 
156
206
  See `InstanceMethods#search_indexable_hash` for an example.
157
207
 
@@ -176,7 +226,8 @@ Then call the following. If `FILTERABLE_ATTRIBUTE_NAMES` is defined it will use
176
226
  otherwise it will use whatever `.searchable_attributes` returns.
177
227
 
178
228
  ```ruby
179
- MyModel.set_filterable_attributes!
229
+ MyModel.set_filterable_attributes! # synchronous
230
+ MyModel.set_filterable_attributes # asynchronous
180
231
  ```
181
232
 
182
233
  This will cause Meilisearch to reindex all the records for that index. If you
@@ -185,14 +236,31 @@ on a background thread. Note that filtering is managed at the index level, not t
185
236
  record level. By setting filterable attributes you're giving Meilisearch
186
237
  guidance on what to do when indexing your data.
187
238
 
239
+ Note that you will encounter problems in a shared index if you try and
240
+ filter on a field that one of the contributing models doesn't have set
241
+ as a filterable field, or doesn't have at all.
242
+
243
+ ### Sortable Fields
244
+
245
+ Sortable fields work in essentially the same way as filterable fields.
246
+ By default it's the same as your `FILTERABLE_ATTRIBUTE_NAMES` which, in turn, defaults to your `SEARCHABLE_ATTRIBUTES` You can
247
+ override it by setting `SORTABLE_ATTRIBUTE_NAMES`.
188
248
 
249
+ Note that you will encounter problems in a shared index if you try and
250
+ sort on a field that one of the contributing models doesn't have set
251
+ as a sortable field, or doesn't have at all.
252
+
253
+ ```ruby
254
+ MyModel.set_sortable_attributes! # synchronous
255
+ MyModel.set_sortable_attributes # asynchronous
256
+ ```
189
257
 
190
258
  ### Indexing things
191
259
  **Important note**: By default anything you do that updates the search index (adding, removing, or changing) happens asynchronously.
192
260
 
193
261
  Sometimes, especially when debugging something on the console, you want to
194
- update the index _synchronously_. The convention used in this codebase is that
195
- the synchronous methods are the ones with the bang. Similar to how mutating
262
+ update the index _synchronously_. The convention used in this codebase - and in the meilisearch-ruby library we build on - is that
263
+ the synchronous methods are the ones with the bang. Similar to how mutating
196
264
  state is potentially dangerous and noted with a bang, using synchronous methods
197
265
  is potentially problematic for your users, and thus noted with a bang.
198
266
 
@@ -200,7 +268,12 @@ is potentially problematic for your users, and thus noted with a bang.
200
268
  For example:
201
269
  ```ruby
202
270
  MyModel.reindex # runs asyncronously
203
- # vs
271
+
272
+ ```
273
+
274
+ vs
275
+
276
+ ```ruby
204
277
  MyModel.reindex! # runs synchronously
205
278
  ```
206
279
 
@@ -209,7 +282,9 @@ MyModel.reindex! # runs synchronously
209
282
  **Reindexing**
210
283
  Calling `MyModel.reindex!` deletes all the existing records from the current index,
211
284
  and then reindexes all the records for the current model. It's safe to run this
212
- even if there aren't any records.
285
+ even if there aren't any records. In addition to re-indexing your models,
286
+ it will update/set the "sortable" and "filterable" fields on the
287
+ relevant indexes.
213
288
 
214
289
  Note: reindexing behaves slightly differently than all the other methods.
215
290
  It runs semi-asynchronously by default. The Asynchronous form will first,
@@ -303,8 +378,7 @@ E.g. `MyModel.search("search term", include_metadata: false)`
303
378
  Search results, ids only, for a class where `CLASS_PREFIXED_SEARCH_IDS=false`.
304
379
 
305
380
  ```ruby
306
- Note.search('foo', ids_only: true)
307
- # returns
381
+ Note.search('foo', ids_only: true) # => returns
308
382
  {
309
383
  "matches" => [
310
384
  "64274a5d906b1d7d02c1fcc7",
@@ -328,8 +402,7 @@ Without `ids_only` you get full objects in a `matches` array.
328
402
 
329
403
 
330
404
  ```ruby
331
- Note.search('foo') # or Note.search('foo', ids_only: false)
332
- # returns
405
+ Note.search('foo') # or Note.search('foo', ids_only: false) # => returns
333
406
  {
334
407
  "matches" => [
335
408
  #<Note _id: 64274a5d906b1d7d02c1fcc7, created_at: 2023-03-15 00:00:00 UTC, updated_at: 2023-03-31 21:02:21.108 UTC, title: "A note from the past", body: "a body", type: "misc", context: "dachary">,
@@ -348,8 +421,7 @@ Note.search('foo') # or Note.search('foo', ids_only: false)
348
421
  If `Note` records shared an index with `Task` and they both had `CLASS_PREFIXED_SEARCH_ID=true` you'd get a result like this.
349
422
 
350
423
  ```ruby
351
- Note.search('foo')
352
- # returns
424
+ Note.search('foo') #=> returns
353
425
  {
354
426
  "matches" => [
355
427
  #<Note _id: 64274a5d906b1d7d02c1fcc7, created_at: 2023-03-15 00:00:00 UTC, updated_at: 2023-03-31 21:02:21.108 UTC, title: "A note from the past", body: "a body", type: "misc", context: "dachary">,
@@ -5,5 +5,5 @@ module MongodbMeilisearch
5
5
  # @note This library will adhere to strict semantic versioning.
6
6
  # See https://semver.org/
7
7
  #
8
- VERSION = "1.0.0"
8
+ VERSION = "1.1.0"
9
9
  end
@@ -339,7 +339,12 @@ module Search
339
339
  # Please don't override this. Define SEARCHABLE_ATTRIBUTES instead.
340
340
  # @return [Array[Symbol]] an array of attribute names as symbols
341
341
  def default_searchable_attributes
342
- attribute_names.map { |n| n.to_sym }
342
+ good_fields = []
343
+ fields.each { |name, obj|
344
+ next if name == "_type"
345
+ good_fields.push(name.to_sym) unless obj.is_a? Mongoid::Fields::ForeignKey
346
+ }
347
+ good_fields
343
348
  end
344
349
 
345
350
  # This returns the list from SEARCHABLE_ATTRIBUTES if defined,
@@ -373,28 +378,92 @@ module Search
373
378
  # @return [Array[Symbol]] an array of symbols corresponding to
374
379
  # filterable attribute names.
375
380
  def filterable_attributes
376
- attributes = []
377
- if constants.include?(:FILTERABLE_ATTRIBUTE_NAMES)
378
- # the union operator is to guarantee no-one tries to create
379
- # invalid filterable attributes
380
- attributes = const_get(:FILTERABLE_ATTRIBUTE_NAMES).map(&:to_sym) & searchable_attributes
381
- elsif !unfilterable?
382
- attributes = searchable_attributes
383
- end
384
- attributes << "object_class" unless attributes.include? "object_class"
385
- attributes
381
+ @_filterable_attributes ||= sort_or_filter_attributes(:filterable)
382
+ end
383
+
384
+ def sortable_attributes
385
+ @_sortable_attributes ||= sort_or_filter_attributes(:sortable)
386
+ end
387
+
388
+ # For more details on sortable attributes see the official
389
+ # Meilisearch docs
390
+ # https://www.meilisearch.com/docs/reference/api/settings#sortable-attributes
391
+ #
392
+ #
393
+ # @return [Array] - an array of attributes configured as sortable
394
+ # in the index.
395
+ def meilisearch_sortable_attributes
396
+ # Search::Client.instance.http_get(
397
+ search_index.http_get(
398
+ "/indexes/#{search_index}/settings/sortable-attributes"
399
+ )
400
+ end
401
+
402
+ def meilisearch_filterable_attributes
403
+ # Search::Client.instance.http_get(
404
+ search_index.http_get(
405
+ "/indexes/#{search_index}/settings/filterable-attributes"
406
+ )
386
407
  end
387
408
 
388
409
  # Updates the filterable attributes in the search index.
389
410
  # Note that this forces Meilisearch to rebuild your index,
390
411
  # which may take time. Best to run this in a background job
391
412
  # for large datasets.
392
- def set_filterable_attributes!(new_attributes = filterable_attributes)
413
+ def set_filterable_attributes(new_attributes = filterable_attributes)
393
414
  search_index.update_filterable_attributes(new_attributes)
394
415
  end
395
416
 
417
+ def set_filterable_attributes!(new_attributes = filterable_attributes)
418
+ # meilisearch-ruby doesn't provide a synchronous version of this
419
+ task = set_filterable_attributes(new_attributes)
420
+ search_index.wait_for_task(task["taskUid"])
421
+ end
422
+
423
+ # Updates the sortable attributes in the search index.
424
+ # Note that this forces Meilisearch to rebuild your index,
425
+ # which may take time. Best to run this in a background job
426
+ # for large datasets.
427
+ def set_sortable_attributes(new_attributes = sortable_attributes)
428
+ search_index.update_sortable_attributes(new_attributes)
429
+ end
430
+
431
+ def set_sortable_attributes!(new_attributes = sortable_attributes)
432
+ # meilisearch-ruby doesn't provide a synchronous version of this
433
+ task = set_sortable_attributes(new_attributes)
434
+ search_index.wait_for_task(task["taskUid"])
435
+ end
436
+
396
437
  private
397
438
 
439
+ # @param [Symbol] which - either :sortable or :filterable
440
+ def sort_or_filter_attributes(which)
441
+ constant_symbol = (which == :sortable) ? :SORTABLE_ATTRIBUTE_NAMES : :FILTERABLE_ATTRIBUTE_NAMES
442
+
443
+ if which == :filterable && unfilterable? && constants.include?(constant_symbol)
444
+ raise "You can't define FILTERABLE_ATTRIBUTE_NAMES & UNFILTERABLE_IN_SEARCH on #{self.class.name}"
445
+ end
446
+ # with that out of the way...
447
+
448
+ attributes = []
449
+ # sortable == defined or filterable
450
+ # filterable == defined or search
451
+ if constants.include?(constant_symbol)
452
+ # the union operator is to guarantee no-one tries to create
453
+ # invalid filterable attributes
454
+ attributes = const_get(constant_symbol).map(&:to_sym) & searchable_attributes
455
+ elsif which == :filterable && !unfilterable?
456
+ attributes = searchable_attributes
457
+ elsif which == :sortable
458
+ # yes this is recursive...
459
+ attributes = filterable_attributes
460
+ end
461
+ # we need object_class even if you're unfilterable, because
462
+ # we need to be able to filter by object_class in shared indexes.
463
+ attributes << :object_class unless attributes.include? :object_class
464
+ attributes
465
+ end
466
+
398
467
  def lookup_matches_by_class!(results, primary_key)
399
468
  # matches_by_class contains a hash of
400
469
  # "FooModel" => [<array of ids>]
@@ -523,7 +592,13 @@ module Search
523
592
  end
524
593
  add_documents(documents, async: async) if documents.size != 0
525
594
 
526
- set_filterable_attributes!
595
+ if async
596
+ set_filterable_attributes
597
+ set_sortable_attributes
598
+ else
599
+ set_filterable_attributes!
600
+ set_sortable_attributes!
601
+ end
527
602
  end
528
603
  end
529
604
  end
data/lib/search/client.rb CHANGED
@@ -13,8 +13,8 @@ module Search
13
13
  max_retries = ENV.fetch("MEILISEARCH_MAX_RETRIES", 2).to_i
14
14
  if url.present? && api_key.present?
15
15
  @client = MeiliSearch::Client.new(url, api_key,
16
- timeout: timeout,
17
- max_retries: max_retries)
16
+ timeout: timeout,
17
+ max_retries: max_retries)
18
18
  else
19
19
  Rails.logger.warn("UNABLE TO CONFIGURE SEARCH. Check env vars.")
20
20
  @client = nil
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mongodb_meilisearch
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - masukomi
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-07-22 00:00:00.000000000 Z
11
+ date: 2023-09-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -143,6 +143,8 @@ executables: []
143
143
  extensions: []
144
144
  extra_rdoc_files: []
145
145
  files:
146
+ - ".changelog_entries/1d952700c037533909615648b5f10582.json"
147
+ - ".changelog_entries/config.json"
146
148
  - ".env"
147
149
  - ".idea/.gitignore"
148
150
  - ".idea/modules.xml"