mongodb_meilisearch 1.0.1 → 1.1.1

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: 1d42d4314105bccabf3b1cc8dc49c1e7c578cc854d213f8fb73cde81aba26c03
4
- data.tar.gz: 0dc187a25352ae68526a389d8e0deed4ba9772ff94a3a6fb2bf001e67f8ccbcb
3
+ metadata.gz: c7873b1a21e75522aa4fdaa063231997319faae90ddb9a4774cd814bf18a4afc
4
+ data.tar.gz: b4f241003a1ff709c1247f6411cec39831459795d8e17c429a9b7b187ff790cd
5
5
  SHA512:
6
- metadata.gz: 13f4ab53d0794010d49bcbdf325db757ceae92cbcce77c77c7dccc99fc4436312b9d786a79fe62609363a8f83372c025374f73cc967261cda231f9624b0e0672
7
- data.tar.gz: ef36cb85710a0bcbe4c47747686aa19d0a7d6748000e4f8ed73e1682a01ec7c4166cffc7e0984faeb7d3b25f9c64288943a828888bcb161d24d2d2268ddf9974
6
+ metadata.gz: 8ad38b1b7f7c14baaee4c12491affeb62c9b9a700ec41d3bddcdf3c095288c084c9bf8f6c9feb28300b441e5fd94380179c9f334c7d2df078d48410221fbc05f
7
+ data.tar.gz: 5792131d06698d2af16194078e1d5a8bfbf37b9084502f25183d5f3c347d712d2eeac7e604edf0298800a03795e573336adbdc9a5b1d47929691f9288e974027
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.1)
4
+ mongodb_meilisearch (1.1.1)
5
5
  meilisearch
6
6
  mongoid (~> 7.0)
7
7
  rails
data/README.md CHANGED
@@ -226,7 +226,8 @@ Then call the following. If `FILTERABLE_ATTRIBUTE_NAMES` is defined it will use
226
226
  otherwise it will use whatever `.searchable_attributes` returns.
227
227
 
228
228
  ```ruby
229
- MyModel.set_filterable_attributes!
229
+ MyModel.set_filterable_attributes! # synchronous
230
+ MyModel.set_filterable_attributes # asynchronous
230
231
  ```
231
232
 
232
233
  This will cause Meilisearch to reindex all the records for that index. If you
@@ -235,14 +236,31 @@ on a background thread. Note that filtering is managed at the index level, not t
235
236
  record level. By setting filterable attributes you're giving Meilisearch
236
237
  guidance on what to do when indexing your data.
237
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.
238
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`.
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
+ ```
239
257
 
240
258
  ### Indexing things
241
259
  **Important note**: By default anything you do that updates the search index (adding, removing, or changing) happens asynchronously.
242
260
 
243
261
  Sometimes, especially when debugging something on the console, you want to
244
- update the index _synchronously_. The convention used in this codebase is that
245
- 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
246
264
  state is potentially dangerous and noted with a bang, using synchronous methods
247
265
  is potentially problematic for your users, and thus noted with a bang.
248
266
 
@@ -264,7 +282,9 @@ MyModel.reindex! # runs synchronously
264
282
  **Reindexing**
265
283
  Calling `MyModel.reindex!` deletes all the existing records from the current index,
266
284
  and then reindexes all the records for the current model. It's safe to run this
267
- 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.
268
288
 
269
289
  Note: reindexing behaves slightly differently than all the other methods.
270
290
  It runs semi-asynchronously by default. The Asynchronous form will first,
@@ -303,6 +323,16 @@ Be careful to not add documents that are already in the index.
303
323
  WARNING: if you think you should use this, you're probably
304
324
  mistaken.
305
325
 
326
+ #### Indexes
327
+ By default every model gets its own search index. This means that
328
+ `Foo.search("some text")` will only search `Foo` objects. To have a
329
+ search cross objects you'll need to use a "Shared Index" (see below).
330
+
331
+
332
+ The name of the index isn't important when not using shared indexes.
333
+ By default a model's index is the snake cased form of the class name.
334
+ For example, data for `MyWidget` models will be stored in the `my_widget` index.
335
+
306
336
  #### Shared indexes
307
337
  Imagine you have a `Note` and a `Comment` model, sharing an index so that
308
338
  you can perform a single search and have search results for both models
@@ -422,10 +452,14 @@ To invoke any of Meilisearch's custom search options (see [their documentation](
422
452
 
423
453
  `MyModel.search("search term", options: <my custom options>)`
424
454
 
425
- The Meilisearch-ruby gem should be able to convert keys from snake case to
455
+ Currently the Meilisearch-ruby gem can convert keys from snake case to
426
456
  camel case. For example `hits_per_page` will become `hitsPerPage`.
427
- Meilisearch ultimately wants camel case. Follow their documentation
428
- to see what's available and what type of options to pass it. Note that your
457
+ Meilisearch ultimately wants camel case (`camelCase`) parameter keys,
458
+ _but_ `meilisearch-ruby` wants snake case (`snake_case`).
459
+
460
+ Follow Meilisearch's documentation
461
+ to see what's available and what type of options to pass it, but convert
462
+ them to snake case first. Note that your
429
463
  options keys and values must all be simple JSON values.
430
464
 
431
465
  If for some reason that still isn't enough, you can work with the
@@ -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.1"
8
+ VERSION = "1.1.1"
9
9
  end
@@ -109,6 +109,7 @@ module Search
109
109
  # @param filtered_by_class [Boolean] - defaults to filtering results by the class
110
110
  # your searching from. Ex. Foo.search("something") it will
111
111
  # have Meilisearch filter on records where `object_class` == `"Foo"`
112
+ # This simplifies working with shared indexes.
112
113
  # @return [Hash[String, [Array[String | Document]] a hash with keys corresponding
113
114
  # to the classes of objects returned, and a value of an array of ids or
114
115
  # Mongoid::Document objects sorted by match strength. It will ALSO have a key named
@@ -135,13 +136,13 @@ module Search
135
136
 
136
137
  # TODO: break this if/else out into a separate function
137
138
  if ids_only
138
- options.merge!({attributesToRetrieve: [pk.to_s]})
139
+ options.merge!({attributes_to_retrieve: [pk.to_s]})
139
140
  else
140
141
  # Don't care what you add, but we need the primary key and object_class
141
- options[:attributesToRetrieve] = [] unless options.has_key?(:attributesToRetrieve)
142
+ options[:attributes_to_retrieve] = [] unless options.has_key?(:attributes_to_retrieve)
142
143
  [pk.to_s, "object_class"].each do |attr|
143
- unless options[:attributesToRetrieve].include?(attr)
144
- options[:attributesToRetrieve] << attr
144
+ unless options[:attributes_to_retrieve].include?(attr)
145
+ options[:attributes_to_retrieve] << attr
145
146
  end
146
147
  end
147
148
  end
@@ -260,6 +261,7 @@ module Search
260
261
  # - each document hash is presumed to have been created by way of
261
262
  # search_indexable_hash
262
263
  def add_documents(new_documents, async: true)
264
+ configure_attributes_and_index_if_needed!
263
265
  if async
264
266
  search_index.add_documents(new_documents, primary_search_key)
265
267
  else
@@ -378,28 +380,113 @@ module Search
378
380
  # @return [Array[Symbol]] an array of symbols corresponding to
379
381
  # filterable attribute names.
380
382
  def filterable_attributes
381
- attributes = []
382
- if constants.include?(:FILTERABLE_ATTRIBUTE_NAMES)
383
- # the union operator is to guarantee no-one tries to create
384
- # invalid filterable attributes
385
- attributes = const_get(:FILTERABLE_ATTRIBUTE_NAMES).map(&:to_sym) & searchable_attributes
386
- elsif !unfilterable?
387
- attributes = searchable_attributes
383
+ @_filterable_attributes ||= sort_or_filter_attributes(:filterable)
384
+ end
385
+
386
+ def sortable_attributes
387
+ @_sortable_attributes ||= sort_or_filter_attributes(:sortable)
388
+ end
389
+
390
+ # For more details on sortable attributes see the official
391
+ # Meilisearch docs
392
+ # https://www.meilisearch.com/docs/reference/api/settings#sortable-attributes
393
+ #
394
+ #
395
+ # @return [Array] - an array of attributes configured as sortable
396
+ # in the index.
397
+ def meilisearch_sortable_attributes
398
+ @_meilisearch_sortable_attributes ||= search_index.get_sortable_attributes
399
+ end
400
+
401
+ def meilisearch_filterable_attributes
402
+ @_meilisearch_filterable_attributes ||= search_index.get_filterable_attributes
403
+ end
404
+
405
+ def reset_cached_data!
406
+ @_meilisearch_filterable_attributes = nil
407
+ @_filterable_attributes = nil
408
+ @_meilisearch_sortable_attributes = nil
409
+ @_sortable_attributes = nil
410
+ end
411
+
412
+ # Guarantees that the filterable attributes have been configured
413
+ # This should be called before
414
+ def configure_attributes_and_index_if_needed!
415
+ return if unfilterable?
416
+ indexes_filterable_attributes = []
417
+
418
+ begin
419
+ indexes_filterable_attributes = meilisearch_filterable_attributes
420
+ rescue ::MeiliSearch::ApiError => e
421
+ # this is expected to happen the first time an instance
422
+ # of a new model is saved.
423
+ raise unless e.message.match?(/Index `\S+` not found\./)
424
+ Search::Client.instance.create_index(search_index_name)
388
425
  end
389
- attributes << "object_class" unless attributes.include? "object_class"
390
- attributes
426
+
427
+ return if indexes_filterable_attributes.include?("object_class")
428
+ set_filterable_attributes
429
+ set_sortable_attributes
391
430
  end
392
431
 
393
432
  # Updates the filterable attributes in the search index.
394
433
  # Note that this forces Meilisearch to rebuild your index,
395
434
  # which may take time. Best to run this in a background job
396
435
  # for large datasets.
397
- def set_filterable_attributes!(new_attributes = filterable_attributes)
436
+ def set_filterable_attributes(new_attributes = filterable_attributes)
398
437
  search_index.update_filterable_attributes(new_attributes)
399
438
  end
400
439
 
440
+ def set_filterable_attributes!(new_attributes = filterable_attributes)
441
+ # meilisearch-ruby doesn't provide a synchronous version of this
442
+ task = set_filterable_attributes(new_attributes)
443
+ search_index.wait_for_task(task["taskUid"])
444
+ end
445
+
446
+ # Updates the sortable attributes in the search index.
447
+ # Note that this forces Meilisearch to rebuild your index,
448
+ # which may take time. Best to run this in a background job
449
+ # for large datasets.
450
+ def set_sortable_attributes(new_attributes = sortable_attributes)
451
+ search_index.update_sortable_attributes(new_attributes)
452
+ end
453
+
454
+ def set_sortable_attributes!(new_attributes = sortable_attributes)
455
+ # meilisearch-ruby doesn't provide a synchronous version of this
456
+ task = set_sortable_attributes(new_attributes)
457
+ search_index.wait_for_task(task["taskUid"])
458
+ end
459
+
401
460
  private
402
461
 
462
+ # @param [Symbol] which - either :sortable or :filterable
463
+ def sort_or_filter_attributes(which)
464
+ constant_symbol = (which == :sortable) ? :SORTABLE_ATTRIBUTE_NAMES : :FILTERABLE_ATTRIBUTE_NAMES
465
+
466
+ if which == :filterable && unfilterable? && constants.include?(constant_symbol)
467
+ raise "You can't define FILTERABLE_ATTRIBUTE_NAMES & UNFILTERABLE_IN_SEARCH on #{self.class.name}"
468
+ end
469
+ # with that out of the way...
470
+
471
+ attributes = []
472
+ # sortable == defined or filterable
473
+ # filterable == defined or search
474
+ if constants.include?(constant_symbol)
475
+ # the union operator is to guarantee no-one tries to create
476
+ # invalid filterable attributes
477
+ attributes = const_get(constant_symbol).map(&:to_sym) & searchable_attributes
478
+ elsif which == :filterable && !unfilterable?
479
+ attributes = searchable_attributes
480
+ elsif which == :sortable
481
+ # yes this is recursive...
482
+ attributes = filterable_attributes
483
+ end
484
+ # we need object_class even if you're unfilterable, because
485
+ # we need to be able to filter by object_class in shared indexes.
486
+ attributes << :object_class unless attributes.include? :object_class
487
+ attributes
488
+ end
489
+
403
490
  def lookup_matches_by_class!(results, primary_key)
404
491
  # matches_by_class contains a hash of
405
492
  # "FooModel" => [<array of ids>]
@@ -515,7 +602,7 @@ module Search
515
602
  def reindex_core(async: true)
516
603
  # no point in continuing if this fails...
517
604
  delete_all_documents!
518
-
605
+ reset_cached_data!
519
606
  # this conveniently lines up with the batch size of 100
520
607
  # that Mongoid gives us
521
608
  documents = []
@@ -528,7 +615,13 @@ module Search
528
615
  end
529
616
  add_documents(documents, async: async) if documents.size != 0
530
617
 
531
- set_filterable_attributes!
618
+ if async
619
+ set_filterable_attributes
620
+ set_sortable_attributes
621
+ else
622
+ set_filterable_attributes!
623
+ set_sortable_attributes!
624
+ end
532
625
  end
533
626
  end
534
627
  end
data/lib/search/client.rb CHANGED
@@ -30,7 +30,7 @@ module Search
30
30
  if @client.respond_to? m.to_sym
31
31
  @client.send(m, *args, &block)
32
32
  else
33
- raise ArgumentError.new("Method `#{m}` doesn't exist.")
33
+ raise ArgumentError.new("Method `#{m}` doesn't exist in #{@client.inspect}.")
34
34
  end
35
35
  end
36
36
 
@@ -2,6 +2,7 @@ module Search
2
2
  module InstanceMethods
3
3
  # Adds this record to the search index asynchronously
4
4
  def add_to_search
5
+ self.class.configure_attributes_and_index_if_needed!
5
6
  search_index.add_documents(
6
7
  [search_indexable_hash],
7
8
  primary_search_key.to_s
@@ -10,6 +11,7 @@ module Search
10
11
 
11
12
  # Adds this record to the search index synchronously
12
13
  def add_to_search!
14
+ self.class.configure_attributes_and_index_if_needed!
13
15
  index = search_index
14
16
  documents = [search_indexable_hash]
15
17
  pk = primary_search_key.to_s
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.1
4
+ version: 1.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - masukomi
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-09-02 00:00:00.000000000 Z
11
+ date: 2023-11-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -187,7 +187,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
187
187
  - !ruby/object:Gem::Version
188
188
  version: '0'
189
189
  requirements: []
190
- rubygems_version: 3.4.17
190
+ rubygems_version: 3.4.20
191
191
  signing_key:
192
192
  specification_version: 4
193
193
  summary: MeiliSearch integration for MongoDB