mongodb_meilisearch 1.3.0 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.org ADDED
@@ -0,0 +1,648 @@
1
+ #+OPTIONS: toc:2
2
+ * Table of Contents :TOC:
3
+ - [[#mongodbmeilisearch][MongodbMeilisearch]]
4
+ - [[#installation][Installation]]
5
+ - [[#usage][Usage]]
6
+ - [[#model-integration][Model Integration]]
7
+ - [[#indexes][Indexes]]
8
+ - [[#searching][Searching]]
9
+ - [[#upgrading][Upgrading]]
10
+ - [[#development][Development]]
11
+ - [[#license][License]]
12
+ - [[#code-of-conduct][Code of Conduct]]
13
+ - [[#footnotes][Footnotes]]
14
+
15
+ * MongodbMeilisearch
16
+ A simple gem for integrating
17
+ [[https://www.meilisearch.com][Meilisearch]] into Ruby applications
18
+ that are backed by [[https://www.mongodb.com/][MongoDB]].
19
+
20
+ ** Installation
21
+ Install the gem and add to the application's Gemfile by executing:
22
+
23
+ #+begin_example
24
+ $ bundle add mongodb_meilisearch
25
+ #+end_example
26
+
27
+ If bundler is not being used to manage dependencies, install the gem by
28
+ executing:
29
+
30
+ #+begin_example
31
+ $ gem install mongodb_meilisearch
32
+ #+end_example
33
+
34
+ ** Usage
35
+ A high level overview
36
+
37
+ *** Pre-Requisites
38
+ - [[https://www.meilisearch.com][Meilisearch]]
39
+ - [[https://www.mongodb.com/][MongoDB]]
40
+ - Some models that =include Mongoid::Document=
41
+
42
+ *** Configuration
43
+ **** Background
44
+ Meilisearch uses 3 different API keys.
45
+ - A Master API Key
46
+ - An Administrative API key
47
+ - A Search API key
48
+
49
+ The Master API key should be used when starting Meilisearch,[fn:macos] but should /never/ be used when searching or administering [[https://www.meilisearch.com/docs/learn/security/basic_security][for security reasons]].
50
+
51
+ When you start it with a master key, it will create default Search & Administrative API Keys.[fn:default_keys] These ~mongodb_meilisearch~ uses these for searching & administering respectively.
52
+
53
+ **** Details
54
+ =MEILI_MASTER_KEY= is the environment variable Meilisearch uses for setting its master key at startup if it isn't provided via command line arguments.
55
+
56
+ =MEILISEARCH_SEARCH_KEY= & =MEILISEARCH_ADMIN_KEY= are for Search & administration respectively. Administration is /everything/ that isn't search, including things like adding new records to the index.
57
+
58
+ I recommend you define the Search & Administration keys. If you provide both of those the =MEILI_MASTER_KEY= will /not/ be required, /or used/ if present. If you only know the master key, there are details below for how to obtain the others.
59
+
60
+ Define the following variables in your environment (or =.env= file if
61
+ you're using [[https://github.com/bkeepers/dotenv][dotenv]]). The url below is the default one Meilisearch
62
+ uses when run locally. Change it if you're running it on a different box or have otherwise changed the default location setup.
63
+
64
+ #+begin_src sh
65
+ SEARCH_ENABLED=true
66
+ MEILI_MASTER_KEY=<your search master key>
67
+ MEILISEARCH_SEARCH_KEY=<your search api key here>
68
+ MEILISEARCH_ADMIN_KEY=<your admin api key here>
69
+ MEILISEARCH_URL=http://127.0.0.1:7700
70
+ #+end_src
71
+
72
+ Optional configuration
73
+
74
+ #+begin_src sh
75
+ MEILISEARCH_TIMEOUT=10
76
+ MEILISEARCH_MAX_RETRIES=2
77
+ #+end_src
78
+
79
+
80
+ 👋 Environment variables starting with =MEILI_= are [[https://www.meilisearch.com/docs/learn/configuration/instance_options][ones defined by Meilisearch]]. The ones starting with =MEILISEARCH_= are specific to this library.
81
+
82
+ ***** Finding your Search & Admin keys
83
+ Meilisearch doesn't make it easy to find out what your default search & administration API keys are, and I haven't found a way to define them yourself.
84
+
85
+ Fortunately for you, this library has an easy way, and you've got this library. 😉
86
+
87
+ #+begin_src ruby
88
+ Search::Client.instance.get_default_keys("your master key here")
89
+ #+end_src
90
+
91
+ This will return a hash with the keys you'll need.
92
+
93
+ #+begin_src ruby
94
+ {:search=>"your search api key",
95
+ :admin=>"your admin api key"}
96
+ #+end_src
97
+
98
+ Use those keys in your environment. The next time the =Search::Client= is initialized it'll use them.
99
+
100
+ ***** Validating Search & Admin keys
101
+ When switching between Meilisearch instances the same master key doesn't result in the same default admin and search keys. Being able to easily validate this is useful in situations like testing code against Meilisearch instances that are inside a Docker container /and/ in the Host OS.
102
+
103
+ Practically speaking you may want to simply /not/ provide these keys during development, and only use the =MEILI_MASTER_KEY=. Just be sure to include them when talking to a production instance.
104
+
105
+ #+begin_src ruby
106
+ Search::Client.instance.validate_default_keys(ENV['MEILI_MASTER_KEY'])
107
+ #+end_src
108
+
109
+ This returns a hash like the following.
110
+
111
+ - =status= is always either ="provided"= or ="missing"= and indicates if the =MEILISEARCH_SEARCH_KEY= or =MEILISEARCH_ADMIN_KEY= were found in the environment.
112
+ - =matches= is always either =true= or =false=, and indicates if the provided Search & Admin keys (if any) match the defaults the current instance is returning.
113
+
114
+ #+begin_src ruby
115
+ {
116
+ :search_key=>{
117
+ :status=>"provided",
118
+ :matches=>true
119
+ },
120
+ :admin_key=>{
121
+ :status=>"provided",
122
+ :matches=>true
123
+ }
124
+ }
125
+ #+end_src
126
+
127
+ ** Model Integration
128
+ Add the following near the top of your model. Only the =extend= and
129
+ =include= lines are required. This assumes your model also includes
130
+ =Mongoid::Document=
131
+
132
+ #+begin_src ruby
133
+ include Search::InstanceMethods
134
+ extend Search::ClassMethods
135
+ #+end_src
136
+
137
+ If you want Rails to automatically add, update, and delete records from
138
+ the index, add the following to your model.
139
+
140
+ You can override these methods if needed, but you're unlikely to want
141
+ to.
142
+
143
+ #+begin_src ruby
144
+ # enabled?() is controlled by the SEARCH_ENABLED environment variable
145
+ if Search::Client.instance.enabled?
146
+ after_create :add_to_search
147
+ after_update :update_in_search
148
+ after_destroy :remove_from_search
149
+ end
150
+ #+end_src
151
+
152
+ Assuming you've done the above a new index will be created with a name
153
+ that corresponds to your model's name, only in snake case. All of your
154
+ models fields will be indexed and
155
+ [[https://www.meilisearch.com/docs/learn/fine_tuning_results/filtering][filterable]].
156
+
157
+ *** Example Rails Model
158
+ Here's what it looks like when you put it all together in a Rails model
159
+ with the default behavior.
160
+
161
+ #+begin_src ruby
162
+ class Person
163
+ include Mongoid::Document
164
+ extend Search::ClassMethods
165
+
166
+ if Search::Client.instance.enabled?
167
+ after_create :add_to_search
168
+ after_update :update_in_search
169
+ after_destroy :remove_from_search
170
+ end
171
+
172
+ # normal Mongoid attributes
173
+ field :name, type: String
174
+ field :description, type: String
175
+ field :age, type: Integer
176
+ end
177
+ #+end_src
178
+
179
+ Note that that /unless you configure it otherwise/ the ids of
180
+ =belongs_to= objects will not be searchable. This is because they're
181
+ random strings that no human's ever going to be searching for, and we
182
+ don't want to waste RAM or storage.
183
+
184
+ *** Going Beyond The Defaults
185
+ This module strives for sensible defaults, but you can override them
186
+ with the following optional constants:
187
+
188
+ - =PRIMARY_SEARCH_KEY= - a Symbol matching one of your model's
189
+ attributes that is guaranteed unique. This defaults to =_id=
190
+ - =SEARCH_INDEX_NAME= - a String - useful if you want to have records
191
+ from multiple classes come back in the same search results. This
192
+ defaults to the underscored form of the current class name.
193
+ - =SEARCH_OPTIONS= - a hash of key value pairs in JS style
194
+ - See the
195
+ [[https://www.meilisearch.com/docs/reference/api/search#search-parameters][meilisearch search parameter docs]] for details.
196
+ - example from
197
+ [[https://github.com/meilisearch/meilisearch-ruby/blob/main/spec/meilisearch/index/search/multi_params_spec.rb][meliesearch's multi_param_spec]]
198
+
199
+ #+begin_src ruby
200
+ {
201
+ attributesToCrop: ['title'],
202
+ cropLength: 2,
203
+ filter: 'genre = adventure',
204
+ attributesToHighlight: ['title'],
205
+ limit: 2
206
+ }
207
+ #+end_src
208
+
209
+ - =SEARCH_RANKING_RULES= - an array of strings that correspond to
210
+ meilisearch rules see
211
+ [[https://www.meilisearch.com/docs/learn/core_concepts/relevancy#ranking-rules][meilisearch ranking rules docs]] You probably don't want to change this.
212
+
213
+ ** Indexes
214
+ Searching is limited to records that have been added to a given index.
215
+ This means, if you want to perform one search and get back records from
216
+ multiple models you'll need to add them to the same index.
217
+
218
+ In order to do that add the =SEARCH_INDEX_NAME= constant to the model
219
+ whose search stuff you want to end up in the same index. You can name
220
+ this just about anything. The important thing is that all the models
221
+ that share this index have the same =SEARCH_INDEX_NAME= constant
222
+ defined. You may want to just add it to a module they all import.
223
+
224
+ #+begin_src ruby
225
+ SEARCH_INDEX_NAME='general_search'
226
+ #+end_src
227
+
228
+ If multiple models are using the same index, you should also add
229
+ =CLASS_PREFIXED_SEARCH_IDS=true=. This causes the =id= field to be
230
+ =<ClassName>_<_id>= For example, a =Note= record might have an index of
231
+ ="Note_64274543906b1d7d02c1fcc6"=. If undefined this will default to
232
+ =false=. This is not needed if you can absolutely guarantee that there
233
+ will be no overlap in ids amongst all the models using a shared index.
234
+
235
+ #+begin_src ruby
236
+ CLASS_PREFIXED_SEARCH_IDS=true
237
+ #+end_src
238
+
239
+ Setting =CLASS_PREFIXED_SEARCH_IDS= to =true= will also cause the
240
+ original Mongoid =_id= field to be indexed as =original_document_id=.
241
+ This is useful if you want to be able to retrieve the original record
242
+ from the database.
243
+
244
+ *** Searchable Data
245
+ You probably don't want to index /all/ the fields. For example, unless
246
+ you intend to allow users to sort by when a record was created, there's
247
+ no point in recording it's =created_at= in the search index. It'll just
248
+ waste bandwidth, memory, and disk space.
249
+
250
+ Define a =SEARCHABLE_ATTRIBUTES= constant with an array of strings to
251
+ limit things. These are the field names, and/or names of methods you
252
+ wish to have indexed.
253
+
254
+ By default these will /also/ be the fields you can filter on.
255
+
256
+ Note that Meilisearch requires there to be an =id= field and it must be
257
+ a string. If you don't define one it will use string version of the
258
+ =_id= your document's =BSON::ObjectId=.
259
+
260
+ #+begin_src ruby
261
+ # explicitly define the fields you want to be searchable
262
+ # this should be an array of symbols
263
+ SEARCHABLE_ATTRIBUTES = %w[title body]
264
+ # OR explicitly define the fields you DON'T want searchable
265
+ SEARCHABLE_ATTRIBUTES = searchable_attributes - [:created_at]
266
+ #+end_src
267
+
268
+ **** Including Foreign Key data
269
+ If, for example, your =Person= =belongs_to: group= and you wanted that
270
+ group's id to be searchable you would include =group_id= in the list.
271
+
272
+ If you don't specify any =SEARCHABLE_ATTRIBUTES=, the default list will
273
+ exclude any fields that are =Mongoid::Fields::ForeignKey= objects.
274
+
275
+ **** Getting Extra Specific
276
+ If your searchable data needs to by dynamically generated instead of
277
+ just taken directly from the =Mongoid::Document='s attributes or
278
+ existing methods you can define a =search_indexable_hash= method on your
279
+ class.
280
+
281
+ Before you do, please note that as of v1.1 your =SEARCHABLE_ATTRIBUTES=
282
+ constant can contain fields and method names in its array of values.
283
+ Making a method for each thing dynamically generated thing you want in
284
+ the search and then including it in SEARCHABLE_ATTRIBUTES is going to be
285
+ the easiest way of accomplishing this.
286
+
287
+ Your =search_indexable_hash= must return a hash, and that hash must
288
+ include the following keys: - ="id"= - a string that uniquely identifies
289
+ the record - ="object_class"= the name of the class that this record
290
+ corresponds to.
291
+
292
+ The value of ="object_class"= is usually just =self.class.name=. This is
293
+ something specific to this gem, and not Meilisearch itself.
294
+
295
+ See =InstanceMethods#search_indexable_hash= for an example.
296
+
297
+ **** Filterable Fields
298
+ If you'd like to only be able to filter on a subset of those then you
299
+ can define =FILTERABLE_ATTRIBUTE_NAMES= but it /must/ be a subset of
300
+ =SEARCHABLE_ATTRIBUTES=. This is enforced by the gem to guarantee no
301
+ complaints from Meilisearch. These must be symbols.
302
+
303
+ If you have no direct need for filterable results, set
304
+ =UNFILTERABLE_IN_SEARCH=true= in your model. This will save on index
305
+ size and speed up indexing, but you won't be able to filter search
306
+ results, and that's half of what makes Meilisearch so great. It should
307
+ be noted, that even if this /is/ set to =true= this gem will still add
308
+ ="object_class"= as a filterable attribute.
309
+
310
+ This is the magic that allows you to have an index shared by multiple
311
+ models and still be able to retrieve results specifically for one.
312
+
313
+ If you decide to re-enable filtering you can remove that constant, or
314
+ set it to false. Then call the following. If
315
+ =FILTERABLE_ATTRIBUTE_NAMES= is defined it will use that, otherwise it
316
+ will use whatever =.searchable_attributes= returns.
317
+
318
+ #+begin_src ruby
319
+ MyModel.set_filterable_attributes! # synchronous
320
+ MyModel.set_filterable_attributes # asynchronous
321
+ #+end_src
322
+
323
+ This will cause Meilisearch to reindex all the records for that index.
324
+ If you have a large number of records this could take a while. Consider
325
+ running it on a background thread. Note that filtering is managed at the
326
+ index level, not the individual record level. By setting filterable
327
+ attributes you're giving Meilisearch guidance on what to do when
328
+ indexing your data.
329
+
330
+ Note that you will encounter problems in a shared index if you try and
331
+ filter on a field that one of the contributing models doesn't have set
332
+ as a filterable field, or doesn't have at all.
333
+
334
+ *** Sortable Fields
335
+ Sortable fields work in essentially the same way as filterable fields.
336
+ By default it's the same as your =FILTERABLE_ATTRIBUTE_NAMES= which, in
337
+ turn, defaults to your =SEARCHABLE_ATTRIBUTES= You can override it by
338
+ setting =SORTABLE_ATTRIBUTE_NAMES=.
339
+
340
+ Note that you will encounter problems in a shared index if you try and
341
+ sort on a field that one of the contributing models doesn't have set as
342
+ a sortable field, or doesn't have at all.
343
+
344
+ #+begin_src ruby
345
+ MyModel.set_sortable_attributes! # synchronous
346
+ MyModel.set_sortable_attributes # asynchronous
347
+ #+end_src
348
+
349
+ *** Indexing things
350
+ *Important note*: By default anything you do that updates the search
351
+ index (adding, removing, or changing) happens asynchronously.
352
+
353
+ Sometimes, especially when debugging something on the console, you want
354
+ to update the index /synchronously/. The convention used in this
355
+ codebase - and in the meilisearch-ruby library we build on - is that the
356
+ synchronous methods are the ones with the bang. Similar to how mutating
357
+ state is potentially dangerous and noted with a bang, using synchronous
358
+ methods is potentially problematic for your users, and thus noted with a
359
+ bang.
360
+
361
+ For example:
362
+
363
+ #+begin_src ruby
364
+ MyModel.reindex # runs asyncronously
365
+ #+end_src
366
+
367
+ vs
368
+
369
+ #+begin_src ruby
370
+ MyModel.reindex! # runs synchronously
371
+ #+end_src
372
+
373
+ **** Reindexing, Adding, Updating, and Deleting
374
+ *Reindexing*
375
+ Calling =MyModel.reindex!= deletes all the existing records from the
376
+ current index, and then reindexes all the records for the current model.
377
+ It's safe to run this even if there aren't any records. In addition to
378
+ re-indexing your models, it will update/set the "sortable" and
379
+ "filterable" fields on the relevant indexes.
380
+
381
+ Note: reindexing behaves slightly differently than all the other
382
+ methods. It runs semi-asynchronously by default. The Asynchronous form
383
+ will first, attempt to /synchronously/ delete all the records from the
384
+ index. If that fails an exception will be raised. Otherwise you'd think
385
+ everything was fine when actually it had failed miserably. If you call
386
+ =.reindex!= it will be entirely synchronous.
387
+
388
+ Note: adding, updating, and deleting should happen automatically if
389
+ you've defined =after_create=, =after_update=, and =after_destroy= as
390
+ instructed above. You'll mostly only want to use these when manually
391
+ mucking with things in the console.
392
+
393
+ *Adding*
394
+ Be careful to not add documents that are already in the index.
395
+
396
+ - Add everything: =MyClass.add_all_to_search=
397
+ - Add a specific instance: =my_instance.add_to_search=
398
+ - Add a specific subset of documents:
399
+ =MyClass.add_documents(documents_hashes)= IMPORTANT:
400
+ =documents_hashes= must be an array of hashes that were each generated
401
+ via =search_indexable_hash=
402
+
403
+ *Updating*
404
+ - Update everything: call =reindex=
405
+ - Update a specific instance: =my_instance.update_in_search=
406
+ - Update a specific subset of documents: =MyClass.update_documents(documents_hashes)= IMPORTANT: =documents_hashes= must be an array of hashes that
407
+ were generated via =search_indexable_hash= The =PRIMARY_SEARCH_KEY=
408
+ (=_id= by default) will be used to find records in the index to update.
409
+
410
+ *Deleting*
411
+ - Delete everything: =MyClass.delete_all_documents!=
412
+ - Delete a specific record: =my_instance.remove_from_search=
413
+ - Delete the index: =MyClass.delete_index!=
414
+ WARNING: if you think you should use this, /you're probably mistaken/.
415
+
416
+ **** Indexes
417
+ By default every model gets its own search index. This means that
418
+ =Foo.search("some text")= will only search =Foo= objects. To have a
419
+ search cross objects you'll need to use a "Shared Index" (see below).
420
+
421
+ The name of the index isn't important when not using shared indexes. By
422
+ default a model's index is the snake cased form of the class name. For
423
+ example, data for =MyWidget= models will be stored in the =my_widget=
424
+ index.
425
+
426
+ **** Shared indexes
427
+ Imagine you have a =Note= and a =Comment= model, sharing an index so
428
+ that you can perform a single search and have search results for both
429
+ models that are ranked by relevance.
430
+
431
+ In this case both models would define a =SEARCH_INDEX_NAME= constant
432
+ with the same value. You might want to just put this, and the other
433
+ search stuff in a common module that they all =include=.
434
+
435
+ Then, when you search you can say =Note.search("search term")= and it
436
+ will /only/ bring back results for =Note= records. If you want to
437
+ include results that match =Comment= records too, you can set the
438
+ optional =filtered_by_class= parameter to =false=.
439
+
440
+ For example: =Note.search("search term", filtered_by_class: false)= will
441
+ return all matching =Note= results, as well as results for /all/ the
442
+ other models that share the same index as =Note=.
443
+
444
+ ⚠ Models sharing the same index must share the same primary key field as
445
+ well. This is a known limitation of the system.
446
+
447
+ ** Searching
448
+ To get a list of all the matching objects in the order returned by the
449
+ search engine run =MyModel.search("search term")= Note that this will
450
+ restrict the results to records generated by the model you're calling
451
+ this on. If you have an index that contains data from multiple models
452
+ and wish to include all of them in the results pass in the optional
453
+ =filtered_by_class= parameter with a =false= value. E.g.
454
+ =MyModel.search("search term", filtered_by_class: false)=
455
+
456
+ Searching returns a hash, with the class name of the results as the key
457
+ and an array of String ids, or =Mongoid::Document= objects as the value.
458
+ By default it assumes you want =Mongoid::Document= objects. The returned
459
+ hash /also/ includes a key of ="search_result_metadata"= which includes
460
+ the metadata provided by Meilisearch regarding your request. You'll need
461
+ this for pagination if you have lots of results. To /exclude/ the
462
+ metadata pass =include_metadata: false= as an option. E.g.
463
+ =MyModel.search("search term", include_metadata: false)=
464
+
465
+ *** Useful Keyword Parameters
466
+ - =ids_only=
467
+ - only return matching ids. These will be an array under the
468
+ ="matches"= key.
469
+ - defaults to =false=
470
+ - =filtered_by_class=
471
+ - limit results to the class you initiated the search from. E.g.
472
+ =Note.search("foo")= will only return results from the =Note= class
473
+ even if there are records from other classes in the same index.
474
+ - defaults to =true=
475
+ - =include_metadata=
476
+ - include the metadata about the search results provided by
477
+ Meilisearch. If true (default) there will be a
478
+ ="search_result_metadata"= key, with a hash of the Meilisearch
479
+ metadata.
480
+ - You'll likely need this in order to support pagination, however if
481
+ you just want to return a single page worth of data, you can set
482
+ this to =false= to discard it.
483
+ - defaults to =true=
484
+
485
+ *** Example Search Results
486
+ Search results, ids only, for a class where
487
+ =CLASS_PREFIXED_SEARCH_IDS=false=.
488
+
489
+ #+begin_src ruby
490
+ Note.search('foo', ids_only: true) # => returns
491
+ {
492
+ "matches" => [
493
+ "64274a5d906b1d7d02c1fcc7",
494
+ "643f5e1c906b1d60f9763071",
495
+ "64483e63906b1d84f149717a"
496
+ ],
497
+ "search_result_metadata" => {
498
+ "query"=>query_string,
499
+ "processingTimeMs"=>1,
500
+ "limit"=>50,
501
+ "offset"=>0,
502
+ "estimatedTotalHits"=>33,
503
+ "nbHits"=>33
504
+ }
505
+ }
506
+ #+end_src
507
+
508
+ If =CLASS_PREFIXED_SEARCH_IDS=true= the above would have ids like
509
+ ="Note_64274a5d906b1d7d02c1fcc7"=
510
+
511
+ Without =ids_only= you get full objects in a =matches= array.
512
+
513
+ #+begin_src ruby
514
+ Note.search('foo') # or Note.search('foo', ids_only: false) # => returns
515
+ {
516
+ "matches" => [
517
+ #<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">,
518
+ #<Note _id: 643f5e1c906b1d60f9763071, created_at: 2023-04-18 00:00:00 UTC, updated_at: 2023-04-19 03:21:00.41 UTC, title: "offline standup ", body: "onother body", type: "misc", context: "WORK">,
519
+ #<Note _id: 64483e63906b1d84f149717a, created_at: 2023-04-25 00:00:00 UTC, updated_at: 2023-04-26 11:23:38.125 UTC, title: "Standup Notes (for wed)", body: "very full bodied", type: "misc", context: "WORK">
520
+ ],
521
+ "search_result_metadata" => {
522
+ "query"=>query_string, "processingTimeMs"=>1, "limit"=>50,
523
+ "offset"=>0, "estimatedTotalHits"=>33, "nbHits"=>33
524
+ }
525
+ }
526
+ #+end_src
527
+
528
+ If =Note= records shared an index with =Task= and they both had
529
+ =CLASS_PREFIXED_SEARCH_ID=true= you'd get a result like this.
530
+
531
+ #+begin_src ruby
532
+ Note.search('foo') #=> returns
533
+ {
534
+ "matches" => [
535
+ #<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">,
536
+ #<Note _id: 643f5e1c906b1d60f9763071, created_at: 2023-04-18 00:00:00 UTC, updated_at: 2023-04-19 03:21:00.41 UTC, title: "offline standup ", body: "onother body", type: "misc", context: "WORK">,
537
+ #<Task _id: 64483e63906b1d84f149717a, created_at: 2023-04-25 00:00:00 UTC, updated_at: 2023-04-26 11:23:38.125 UTC, title: "Do the thing", body: "very full bodied", type: "misc", context: "WORK">
538
+ ],
539
+ "search_result_metadata" => {
540
+ "query"=>query_string, "processingTimeMs"=>1, "limit"=>50,
541
+ "offset"=>0, "estimatedTotalHits"=>33, "nbHits"=>33
542
+ }
543
+
544
+ }
545
+ #+end_src
546
+
547
+ *** Custom Search Options
548
+ To invoke any of Meilisearch's custom search options (see
549
+ [[https://www.meilisearch.com/docs/reference/api/search][their
550
+ documentation]]). You can pass them in via an options hash.
551
+
552
+ =MyModel.search("search term", options: <my custom options>)=
553
+
554
+ Currently the Meilisearch-ruby gem can convert keys from snake case to
555
+ camel case. For example =hits_per_page= will become =hitsPerPage=.
556
+ Meilisearch ultimately wants camel case (=camelCase=) parameter keys,
557
+ /but/ =meilisearch-ruby= wants snake case (=snake_case=).
558
+
559
+ Follow Meilisearch's documentation to see what's available and what type
560
+ of options to pass it, but convert them to snake case first. Note that
561
+ your options keys and values must all be simple JSON values.
562
+
563
+ If for some reason that still isn't enough, you can work with the
564
+ meilisearch-ruby index directly via
565
+ =Search::Client.instance.index(search_index_name)=
566
+
567
+ **** Pagination
568
+ This gem has no specific pagination handling, as there are multiple
569
+ libraries for handling pagination in Ruby. Here's an example of how to
570
+ get started with [[https://github.com/ddnexus/pagy][Pagy]].
571
+
572
+ #+begin_src ruby
573
+ current_page_number = 1
574
+ max_items_per_page = 10
575
+
576
+ search_results = Note.search('foo')
577
+
578
+ Pagy.new(
579
+ count: search_results["search_result_metadata"]["nbHits"],
580
+ page: current_page_number,
581
+ items: max_items_per_page
582
+ )
583
+ #+end_src
584
+
585
+ ** Upgrading
586
+ *** From v1.x
587
+ primary difference between v1.x and v2 is that =mongodb_meilisearch= no longer uses the Meilisearch master key [[https://www.meilisearch.com/docs/learn/security/basic_security][for security reasons]]. The /only/ thing it will use it for is to look up the default search API key and / or the default administrative API key.
588
+
589
+ It now initializes 2 =Meilisearch::Client= objects: one for searching, one for administration, and all of the internal methods that interact with an index have been rewritten to interact with it via the correct client.
590
+
591
+
592
+ Practically speaking, there are 3 significant differences from v1.x
593
+
594
+ 1. =MEILISEARCH_API_KEY= is no longer supported. It has been replaced with the official Meilisearch environment variable for the same purpose: =MEILI_MASTER_KEY=
595
+ 2. The master key is only used when an API key for search and / or administration is not provided. It is recommended that you provide =MEILISEARCH_SEARCH_KEY= & =MEILISEARCH_ADMIN_KEY= for those purposes. See "Finding your Search & Admin keys" above for more details.
596
+ 3. =Search::Client.instance.client= has been replaced with 2 clients with specific purposes
597
+ - =Search::Client::instance.search_client=
598
+ - =Search::Client::instance.admin_client=
599
+ 4. =ClassMethods.search_index= has been replaced with
600
+ - =ClassMethods.searchable_index=
601
+ - =ClassMethods.administratable_index=
602
+
603
+ ** Development
604
+ To contribute to this gem.
605
+
606
+ - Run =bundle install= to install all the dependencies.
607
+ - run =lefthook install= to set up
608
+ [[https://github.com/evilmartians/lefthook][lefthook]] This will do
609
+ things like make sure the tests still pass, and run rubocop before you
610
+ commit.
611
+ - Start hacking.
612
+ - Add RSpec tests.
613
+ - Add your name to CONTRIBUTORS.md
614
+ - Make PR.
615
+
616
+ NOTE: by contributing to this repository you are offering to transfer
617
+ copyright to the current maintainer of the repository.
618
+
619
+ To install this gem onto your local machine, run
620
+ =bundle exec rake install=. To release a new version, update the version
621
+ number in =version.rb=, and then run =bundle exec rake release=, which
622
+ will create a git tag for the version, push git commits and the created
623
+ tag, and push the =.gem= file to [[https://rubygems.org][rubygems.org]].
624
+
625
+ Bug reports and pull requests are welcome on GitHub at
626
+ https://github.com/masukomi/mongodb_meilisearch. This project is
627
+ intended to be a safe, welcoming space for collaboration, and
628
+ contributors are expected to adhere to the
629
+ [[https://github.com/masukomi/mongodb_meilisearch/blob/main/CODE_OF_CONDUCT.md][code of conduct]].
630
+
631
+ ** License
632
+ The gem is available as open source under the terms of the
633
+ [[https://github.com/masukomi/mongodb_meilisearch/blob/main/LICENSE.txt][Server Side Public License]]. For those unfamiliar, the short version is that
634
+ if you use it in a server side app you need to share all the code for
635
+ that app and its infrastructure. It's like AGPL on steroids. Commercial
636
+ licenses are available if you want to use this in a commercial setting
637
+ but not share all your source.
638
+
639
+ ** Code of Conduct
640
+ Everyone interacting in this project's codebases, issue trackers, chat
641
+ rooms and mailing lists is expected to follow the [[https://github.com/masukomi/mongodb_meilisearch/blob/main/CODE_OF_CONDUCT.md][code of conduct]].
642
+
643
+ It is furthermore /fully/ acknowledged that "master" is a problematic term that shouldn't be used. However, we're stuck with it until Meilisearch changes the name of that key.
644
+
645
+ * Footnotes
646
+ [fn:macos] MacOS users I have not found a way to successfully automatically Meilisearch with launchctl (brew services uses this) that allows it to see the ~MEILI_MASTER_KEY~ environment variable. As such, I can /not/ recommend using launchctl for running Meilisearch. Running it without a master key only works in development mode, and introduces complications.
647
+
648
+ [fn:default_keys] Using the master key you can access the [[https://www.meilisearch.com/docs/reference/api/keys][/keys]] endpoint in Meilisearch to retrieve the default Search & Administrative API keys
@@ -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.3.0"
8
+ VERSION = "2.1.0"
9
9
  end