mongodb_meilisearch 1.3.0 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/README.org +621 -0
- data/lib/mongodb_meilisearch/version.rb +1 -1
- data/lib/search/class_methods.rb +25 -19
- data/lib/search/client.rb +148 -20
- data/lib/search/errors.rb +7 -0
- data/lib/search/instance_methods.rb +11 -10
- metadata +4 -3
- data/README.md +0 -531
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0c07c25d10efdfee292401ba54dacb34aee8abd153226a3618a2c88de96661dc
|
4
|
+
data.tar.gz: 40ec770c86ab522f3cf49ff950230a8f82f6527d4a3b33842f8d56c12f47067a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: db89325982a30521bec7fcaee27f0da61a177b7738591dbf0b95c36666f3a02e94a85e3403a7c2b81a92d0396839b1344dc7c63a4a4ae937793c743bcbb26e4f
|
7
|
+
data.tar.gz: 05415b71991b53ff28ab4c10e5c3b9ea27ef7399f1c91d1e0444974469b9649d24191ec0438a91045438c95207fa4dcf6f4c4ea545008e5d30b52574daf5752b
|
data/Gemfile.lock
CHANGED
data/README.org
ADDED
@@ -0,0 +1,621 @@
|
|
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
|
+
** Model Integration
|
101
|
+
Add the following near the top of your model. Only the =extend= and
|
102
|
+
=include= lines are required. This assumes your model also includes
|
103
|
+
=Mongoid::Document=
|
104
|
+
|
105
|
+
#+begin_src ruby
|
106
|
+
include Search::InstanceMethods
|
107
|
+
extend Search::ClassMethods
|
108
|
+
#+end_src
|
109
|
+
|
110
|
+
If you want Rails to automatically add, update, and delete records from
|
111
|
+
the index, add the following to your model.
|
112
|
+
|
113
|
+
You can override these methods if needed, but you're unlikely to want
|
114
|
+
to.
|
115
|
+
|
116
|
+
#+begin_src ruby
|
117
|
+
# enabled?() is controlled by the SEARCH_ENABLED environment variable
|
118
|
+
if Search::Client.instance.enabled?
|
119
|
+
after_create :add_to_search
|
120
|
+
after_update :update_in_search
|
121
|
+
after_destroy :remove_from_search
|
122
|
+
end
|
123
|
+
#+end_src
|
124
|
+
|
125
|
+
Assuming you've done the above a new index will be created with a name
|
126
|
+
that corresponds to your model's name, only in snake case. All of your
|
127
|
+
models fields will be indexed and
|
128
|
+
[[https://www.meilisearch.com/docs/learn/fine_tuning_results/filtering][filterable]].
|
129
|
+
|
130
|
+
*** Example Rails Model
|
131
|
+
Here's what it looks like when you put it all together in a Rails model
|
132
|
+
with the default behavior.
|
133
|
+
|
134
|
+
#+begin_src ruby
|
135
|
+
class Person
|
136
|
+
include Mongoid::Document
|
137
|
+
extend Search::ClassMethods
|
138
|
+
|
139
|
+
if Search::Client.instance.enabled?
|
140
|
+
after_create :add_to_search
|
141
|
+
after_update :update_in_search
|
142
|
+
after_destroy :remove_from_search
|
143
|
+
end
|
144
|
+
|
145
|
+
# normal Mongoid attributes
|
146
|
+
field :name, type: String
|
147
|
+
field :description, type: String
|
148
|
+
field :age, type: Integer
|
149
|
+
end
|
150
|
+
#+end_src
|
151
|
+
|
152
|
+
Note that that /unless you configure it otherwise/ the ids of
|
153
|
+
=belongs_to= objects will not be searchable. This is because they're
|
154
|
+
random strings that no human's ever going to be searching for, and we
|
155
|
+
don't want to waste RAM or storage.
|
156
|
+
|
157
|
+
*** Going Beyond The Defaults
|
158
|
+
This module strives for sensible defaults, but you can override them
|
159
|
+
with the following optional constants:
|
160
|
+
|
161
|
+
- =PRIMARY_SEARCH_KEY= - a Symbol matching one of your model's
|
162
|
+
attributes that is guaranteed unique. This defaults to =_id=
|
163
|
+
- =SEARCH_INDEX_NAME= - a String - useful if you want to have records
|
164
|
+
from multiple classes come back in the same search results. This
|
165
|
+
defaults to the underscored form of the current class name.
|
166
|
+
- =SEARCH_OPTIONS= - a hash of key value pairs in JS style
|
167
|
+
- See the
|
168
|
+
[[https://www.meilisearch.com/docs/reference/api/search#search-parameters][meilisearch search parameter docs]] for details.
|
169
|
+
- example from
|
170
|
+
[[https://github.com/meilisearch/meilisearch-ruby/blob/main/spec/meilisearch/index/search/multi_params_spec.rb][meliesearch's multi_param_spec]]
|
171
|
+
|
172
|
+
#+begin_src ruby
|
173
|
+
{
|
174
|
+
attributesToCrop: ['title'],
|
175
|
+
cropLength: 2,
|
176
|
+
filter: 'genre = adventure',
|
177
|
+
attributesToHighlight: ['title'],
|
178
|
+
limit: 2
|
179
|
+
}
|
180
|
+
#+end_src
|
181
|
+
|
182
|
+
- =SEARCH_RANKING_RULES= - an array of strings that correspond to
|
183
|
+
meilisearch rules see
|
184
|
+
[[https://www.meilisearch.com/docs/learn/core_concepts/relevancy#ranking-rules][meilisearch ranking rules docs]] You probably don't want to change this.
|
185
|
+
|
186
|
+
** Indexes
|
187
|
+
Searching is limited to records that have been added to a given index.
|
188
|
+
This means, if you want to perform one search and get back records from
|
189
|
+
multiple models you'll need to add them to the same index.
|
190
|
+
|
191
|
+
In order to do that add the =SEARCH_INDEX_NAME= constant to the model
|
192
|
+
whose search stuff you want to end up in the same index. You can name
|
193
|
+
this just about anything. The important thing is that all the models
|
194
|
+
that share this index have the same =SEARCH_INDEX_NAME= constant
|
195
|
+
defined. You may want to just add it to a module they all import.
|
196
|
+
|
197
|
+
#+begin_src ruby
|
198
|
+
SEARCH_INDEX_NAME='general_search'
|
199
|
+
#+end_src
|
200
|
+
|
201
|
+
If multiple models are using the same index, you should also add
|
202
|
+
=CLASS_PREFIXED_SEARCH_IDS=true=. This causes the =id= field to be
|
203
|
+
=<ClassName>_<_id>= For example, a =Note= record might have an index of
|
204
|
+
="Note_64274543906b1d7d02c1fcc6"=. If undefined this will default to
|
205
|
+
=false=. This is not needed if you can absolutely guarantee that there
|
206
|
+
will be no overlap in ids amongst all the models using a shared index.
|
207
|
+
|
208
|
+
#+begin_src ruby
|
209
|
+
CLASS_PREFIXED_SEARCH_IDS=true
|
210
|
+
#+end_src
|
211
|
+
|
212
|
+
Setting =CLASS_PREFIXED_SEARCH_IDS= to =true= will also cause the
|
213
|
+
original Mongoid =_id= field to be indexed as =original_document_id=.
|
214
|
+
This is useful if you want to be able to retrieve the original record
|
215
|
+
from the database.
|
216
|
+
|
217
|
+
*** Searchable Data
|
218
|
+
You probably don't want to index /all/ the fields. For example, unless
|
219
|
+
you intend to allow users to sort by when a record was created, there's
|
220
|
+
no point in recording it's =created_at= in the search index. It'll just
|
221
|
+
waste bandwidth, memory, and disk space.
|
222
|
+
|
223
|
+
Define a =SEARCHABLE_ATTRIBUTES= constant with an array of strings to
|
224
|
+
limit things. These are the field names, and/or names of methods you
|
225
|
+
wish to have indexed.
|
226
|
+
|
227
|
+
By default these will /also/ be the fields you can filter on.
|
228
|
+
|
229
|
+
Note that Meilisearch requires there to be an =id= field and it must be
|
230
|
+
a string. If you don't define one it will use string version of the
|
231
|
+
=_id= your document's =BSON::ObjectId=.
|
232
|
+
|
233
|
+
#+begin_src ruby
|
234
|
+
# explicitly define the fields you want to be searchable
|
235
|
+
# this should be an array of symbols
|
236
|
+
SEARCHABLE_ATTRIBUTES = %w[title body]
|
237
|
+
# OR explicitly define the fields you DON'T want searchable
|
238
|
+
SEARCHABLE_ATTRIBUTES = searchable_attributes - [:created_at]
|
239
|
+
#+end_src
|
240
|
+
|
241
|
+
**** Including Foreign Key data
|
242
|
+
If, for example, your =Person= =belongs_to: group= and you wanted that
|
243
|
+
group's id to be searchable you would include =group_id= in the list.
|
244
|
+
|
245
|
+
If you don't specify any =SEARCHABLE_ATTRIBUTES=, the default list will
|
246
|
+
exclude any fields that are =Mongoid::Fields::ForeignKey= objects.
|
247
|
+
|
248
|
+
**** Getting Extra Specific
|
249
|
+
If your searchable data needs to by dynamically generated instead of
|
250
|
+
just taken directly from the =Mongoid::Document='s attributes or
|
251
|
+
existing methods you can define a =search_indexable_hash= method on your
|
252
|
+
class.
|
253
|
+
|
254
|
+
Before you do, please note that as of v1.1 your =SEARCHABLE_ATTRIBUTES=
|
255
|
+
constant can contain fields and method names in its array of values.
|
256
|
+
Making a method for each thing dynamically generated thing you want in
|
257
|
+
the search and then including it in SEARCHABLE_ATTRIBUTES is going to be
|
258
|
+
the easiest way of accomplishing this.
|
259
|
+
|
260
|
+
Your =search_indexable_hash= must return a hash, and that hash must
|
261
|
+
include the following keys: - ="id"= - a string that uniquely identifies
|
262
|
+
the record - ="object_class"= the name of the class that this record
|
263
|
+
corresponds to.
|
264
|
+
|
265
|
+
The value of ="object_class"= is usually just =self.class.name=. This is
|
266
|
+
something specific to this gem, and not Meilisearch itself.
|
267
|
+
|
268
|
+
See =InstanceMethods#search_indexable_hash= for an example.
|
269
|
+
|
270
|
+
**** Filterable Fields
|
271
|
+
If you'd like to only be able to filter on a subset of those then you
|
272
|
+
can define =FILTERABLE_ATTRIBUTE_NAMES= but it /must/ be a subset of
|
273
|
+
=SEARCHABLE_ATTRIBUTES=. This is enforced by the gem to guarantee no
|
274
|
+
complaints from Meilisearch. These must be symbols.
|
275
|
+
|
276
|
+
If you have no direct need for filterable results, set
|
277
|
+
=UNFILTERABLE_IN_SEARCH=true= in your model. This will save on index
|
278
|
+
size and speed up indexing, but you won't be able to filter search
|
279
|
+
results, and that's half of what makes Meilisearch so great. It should
|
280
|
+
be noted, that even if this /is/ set to =true= this gem will still add
|
281
|
+
="object_class"= as a filterable attribute.
|
282
|
+
|
283
|
+
This is the magic that allows you to have an index shared by multiple
|
284
|
+
models and still be able to retrieve results specifically for one.
|
285
|
+
|
286
|
+
If you decide to re-enable filtering you can remove that constant, or
|
287
|
+
set it to false. Then call the following. If
|
288
|
+
=FILTERABLE_ATTRIBUTE_NAMES= is defined it will use that, otherwise it
|
289
|
+
will use whatever =.searchable_attributes= returns.
|
290
|
+
|
291
|
+
#+begin_src ruby
|
292
|
+
MyModel.set_filterable_attributes! # synchronous
|
293
|
+
MyModel.set_filterable_attributes # asynchronous
|
294
|
+
#+end_src
|
295
|
+
|
296
|
+
This will cause Meilisearch to reindex all the records for that index.
|
297
|
+
If you have a large number of records this could take a while. Consider
|
298
|
+
running it on a background thread. Note that filtering is managed at the
|
299
|
+
index level, not the individual record level. By setting filterable
|
300
|
+
attributes you're giving Meilisearch guidance on what to do when
|
301
|
+
indexing your data.
|
302
|
+
|
303
|
+
Note that you will encounter problems in a shared index if you try and
|
304
|
+
filter on a field that one of the contributing models doesn't have set
|
305
|
+
as a filterable field, or doesn't have at all.
|
306
|
+
|
307
|
+
*** Sortable Fields
|
308
|
+
Sortable fields work in essentially the same way as filterable fields.
|
309
|
+
By default it's the same as your =FILTERABLE_ATTRIBUTE_NAMES= which, in
|
310
|
+
turn, defaults to your =SEARCHABLE_ATTRIBUTES= You can override it by
|
311
|
+
setting =SORTABLE_ATTRIBUTE_NAMES=.
|
312
|
+
|
313
|
+
Note that you will encounter problems in a shared index if you try and
|
314
|
+
sort on a field that one of the contributing models doesn't have set as
|
315
|
+
a sortable field, or doesn't have at all.
|
316
|
+
|
317
|
+
#+begin_src ruby
|
318
|
+
MyModel.set_sortable_attributes! # synchronous
|
319
|
+
MyModel.set_sortable_attributes # asynchronous
|
320
|
+
#+end_src
|
321
|
+
|
322
|
+
*** Indexing things
|
323
|
+
*Important note*: By default anything you do that updates the search
|
324
|
+
index (adding, removing, or changing) happens asynchronously.
|
325
|
+
|
326
|
+
Sometimes, especially when debugging something on the console, you want
|
327
|
+
to update the index /synchronously/. The convention used in this
|
328
|
+
codebase - and in the meilisearch-ruby library we build on - is that the
|
329
|
+
synchronous methods are the ones with the bang. Similar to how mutating
|
330
|
+
state is potentially dangerous and noted with a bang, using synchronous
|
331
|
+
methods is potentially problematic for your users, and thus noted with a
|
332
|
+
bang.
|
333
|
+
|
334
|
+
For example:
|
335
|
+
|
336
|
+
#+begin_src ruby
|
337
|
+
MyModel.reindex # runs asyncronously
|
338
|
+
#+end_src
|
339
|
+
|
340
|
+
vs
|
341
|
+
|
342
|
+
#+begin_src ruby
|
343
|
+
MyModel.reindex! # runs synchronously
|
344
|
+
#+end_src
|
345
|
+
|
346
|
+
**** Reindexing, Adding, Updating, and Deleting
|
347
|
+
*Reindexing*
|
348
|
+
Calling =MyModel.reindex!= deletes all the existing records from the
|
349
|
+
current index, and then reindexes all the records for the current model.
|
350
|
+
It's safe to run this even if there aren't any records. In addition to
|
351
|
+
re-indexing your models, it will update/set the "sortable" and
|
352
|
+
"filterable" fields on the relevant indexes.
|
353
|
+
|
354
|
+
Note: reindexing behaves slightly differently than all the other
|
355
|
+
methods. It runs semi-asynchronously by default. The Asynchronous form
|
356
|
+
will first, attempt to /synchronously/ delete all the records from the
|
357
|
+
index. If that fails an exception will be raised. Otherwise you'd think
|
358
|
+
everything was fine when actually it had failed miserably. If you call
|
359
|
+
=.reindex!= it will be entirely synchronous.
|
360
|
+
|
361
|
+
Note: adding, updating, and deleting should happen automatically if
|
362
|
+
you've defined =after_create=, =after_update=, and =after_destroy= as
|
363
|
+
instructed above. You'll mostly only want to use these when manually
|
364
|
+
mucking with things in the console.
|
365
|
+
|
366
|
+
*Adding*
|
367
|
+
Be careful to not add documents that are already in the index.
|
368
|
+
|
369
|
+
- Add everything: =MyClass.add_all_to_search=
|
370
|
+
- Add a specific instance: =my_instance.add_to_search=
|
371
|
+
- Add a specific subset of documents:
|
372
|
+
=MyClass.add_documents(documents_hashes)= IMPORTANT:
|
373
|
+
=documents_hashes= must be an array of hashes that were each generated
|
374
|
+
via =search_indexable_hash=
|
375
|
+
|
376
|
+
*Updating*
|
377
|
+
- Update everything: call =reindex=
|
378
|
+
- Update a specific instance: =my_instance.update_in_search=
|
379
|
+
- Update a specific subset of documents: =MyClass.update_documents(documents_hashes)= IMPORTANT: =documents_hashes= must be an array of hashes that
|
380
|
+
were generated via =search_indexable_hash= The =PRIMARY_SEARCH_KEY=
|
381
|
+
(=_id= by default) will be used to find records in the index to update.
|
382
|
+
|
383
|
+
*Deleting*
|
384
|
+
- Delete everything: =MyClass.delete_all_documents!=
|
385
|
+
- Delete a specific record: =my_instance.remove_from_search=
|
386
|
+
- Delete the index: =MyClass.delete_index!=
|
387
|
+
WARNING: if you think you should use this, /you're probably mistaken/.
|
388
|
+
|
389
|
+
**** Indexes
|
390
|
+
By default every model gets its own search index. This means that
|
391
|
+
=Foo.search("some text")= will only search =Foo= objects. To have a
|
392
|
+
search cross objects you'll need to use a "Shared Index" (see below).
|
393
|
+
|
394
|
+
The name of the index isn't important when not using shared indexes. By
|
395
|
+
default a model's index is the snake cased form of the class name. For
|
396
|
+
example, data for =MyWidget= models will be stored in the =my_widget=
|
397
|
+
index.
|
398
|
+
|
399
|
+
**** Shared indexes
|
400
|
+
Imagine you have a =Note= and a =Comment= model, sharing an index so
|
401
|
+
that you can perform a single search and have search results for both
|
402
|
+
models that are ranked by relevance.
|
403
|
+
|
404
|
+
In this case both models would define a =SEARCH_INDEX_NAME= constant
|
405
|
+
with the same value. You might want to just put this, and the other
|
406
|
+
search stuff in a common module that they all =include=.
|
407
|
+
|
408
|
+
Then, when you search you can say =Note.search("search term")= and it
|
409
|
+
will /only/ bring back results for =Note= records. If you want to
|
410
|
+
include results that match =Comment= records too, you can set the
|
411
|
+
optional =filtered_by_class= parameter to =false=.
|
412
|
+
|
413
|
+
For example: =Note.search("search term", filtered_by_class: false)= will
|
414
|
+
return all matching =Note= results, as well as results for /all/ the
|
415
|
+
other models that share the same index as =Note=.
|
416
|
+
|
417
|
+
⚠ Models sharing the same index must share the same primary key field as
|
418
|
+
well. This is a known limitation of the system.
|
419
|
+
|
420
|
+
** Searching
|
421
|
+
To get a list of all the matching objects in the order returned by the
|
422
|
+
search engine run =MyModel.search("search term")= Note that this will
|
423
|
+
restrict the results to records generated by the model you're calling
|
424
|
+
this on. If you have an index that contains data from multiple models
|
425
|
+
and wish to include all of them in the results pass in the optional
|
426
|
+
=filtered_by_class= parameter with a =false= value. E.g.
|
427
|
+
=MyModel.search("search term", filtered_by_class: false)=
|
428
|
+
|
429
|
+
Searching returns a hash, with the class name of the results as the key
|
430
|
+
and an array of String ids, or =Mongoid::Document= objects as the value.
|
431
|
+
By default it assumes you want =Mongoid::Document= objects. The returned
|
432
|
+
hash /also/ includes a key of ="search_result_metadata"= which includes
|
433
|
+
the metadata provided by Meilisearch regarding your request. You'll need
|
434
|
+
this for pagination if you have lots of results. To /exclude/ the
|
435
|
+
metadata pass =include_metadata: false= as an option. E.g.
|
436
|
+
=MyModel.search("search term", include_metadata: false)=
|
437
|
+
|
438
|
+
*** Useful Keyword Parameters
|
439
|
+
- =ids_only=
|
440
|
+
- only return matching ids. These will be an array under the
|
441
|
+
="matches"= key.
|
442
|
+
- defaults to =false=
|
443
|
+
- =filtered_by_class=
|
444
|
+
- limit results to the class you initiated the search from. E.g.
|
445
|
+
=Note.search("foo")= will only return results from the =Note= class
|
446
|
+
even if there are records from other classes in the same index.
|
447
|
+
- defaults to =true=
|
448
|
+
- =include_metadata=
|
449
|
+
- include the metadata about the search results provided by
|
450
|
+
Meilisearch. If true (default) there will be a
|
451
|
+
="search_result_metadata"= key, with a hash of the Meilisearch
|
452
|
+
metadata.
|
453
|
+
- You'll likely need this in order to support pagination, however if
|
454
|
+
you just want to return a single page worth of data, you can set
|
455
|
+
this to =false= to discard it.
|
456
|
+
- defaults to =true=
|
457
|
+
|
458
|
+
*** Example Search Results
|
459
|
+
Search results, ids only, for a class where
|
460
|
+
=CLASS_PREFIXED_SEARCH_IDS=false=.
|
461
|
+
|
462
|
+
#+begin_src ruby
|
463
|
+
Note.search('foo', ids_only: true) # => returns
|
464
|
+
{
|
465
|
+
"matches" => [
|
466
|
+
"64274a5d906b1d7d02c1fcc7",
|
467
|
+
"643f5e1c906b1d60f9763071",
|
468
|
+
"64483e63906b1d84f149717a"
|
469
|
+
],
|
470
|
+
"search_result_metadata" => {
|
471
|
+
"query"=>query_string,
|
472
|
+
"processingTimeMs"=>1,
|
473
|
+
"limit"=>50,
|
474
|
+
"offset"=>0,
|
475
|
+
"estimatedTotalHits"=>33,
|
476
|
+
"nbHits"=>33
|
477
|
+
}
|
478
|
+
}
|
479
|
+
#+end_src
|
480
|
+
|
481
|
+
If =CLASS_PREFIXED_SEARCH_IDS=true= the above would have ids like
|
482
|
+
="Note_64274a5d906b1d7d02c1fcc7"=
|
483
|
+
|
484
|
+
Without =ids_only= you get full objects in a =matches= array.
|
485
|
+
|
486
|
+
#+begin_src ruby
|
487
|
+
Note.search('foo') # or Note.search('foo', ids_only: false) # => returns
|
488
|
+
{
|
489
|
+
"matches" => [
|
490
|
+
#<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">,
|
491
|
+
#<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">,
|
492
|
+
#<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">
|
493
|
+
],
|
494
|
+
"search_result_metadata" => {
|
495
|
+
"query"=>query_string, "processingTimeMs"=>1, "limit"=>50,
|
496
|
+
"offset"=>0, "estimatedTotalHits"=>33, "nbHits"=>33
|
497
|
+
}
|
498
|
+
}
|
499
|
+
#+end_src
|
500
|
+
|
501
|
+
If =Note= records shared an index with =Task= and they both had
|
502
|
+
=CLASS_PREFIXED_SEARCH_ID=true= you'd get a result like this.
|
503
|
+
|
504
|
+
#+begin_src ruby
|
505
|
+
Note.search('foo') #=> returns
|
506
|
+
{
|
507
|
+
"matches" => [
|
508
|
+
#<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">,
|
509
|
+
#<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">,
|
510
|
+
#<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">
|
511
|
+
],
|
512
|
+
"search_result_metadata" => {
|
513
|
+
"query"=>query_string, "processingTimeMs"=>1, "limit"=>50,
|
514
|
+
"offset"=>0, "estimatedTotalHits"=>33, "nbHits"=>33
|
515
|
+
}
|
516
|
+
|
517
|
+
}
|
518
|
+
#+end_src
|
519
|
+
|
520
|
+
*** Custom Search Options
|
521
|
+
To invoke any of Meilisearch's custom search options (see
|
522
|
+
[[https://www.meilisearch.com/docs/reference/api/search][their
|
523
|
+
documentation]]). You can pass them in via an options hash.
|
524
|
+
|
525
|
+
=MyModel.search("search term", options: <my custom options>)=
|
526
|
+
|
527
|
+
Currently the Meilisearch-ruby gem can convert keys from snake case to
|
528
|
+
camel case. For example =hits_per_page= will become =hitsPerPage=.
|
529
|
+
Meilisearch ultimately wants camel case (=camelCase=) parameter keys,
|
530
|
+
/but/ =meilisearch-ruby= wants snake case (=snake_case=).
|
531
|
+
|
532
|
+
Follow Meilisearch's documentation to see what's available and what type
|
533
|
+
of options to pass it, but convert them to snake case first. Note that
|
534
|
+
your options keys and values must all be simple JSON values.
|
535
|
+
|
536
|
+
If for some reason that still isn't enough, you can work with the
|
537
|
+
meilisearch-ruby index directly via
|
538
|
+
=Search::Client.instance.index(search_index_name)=
|
539
|
+
|
540
|
+
**** Pagination
|
541
|
+
This gem has no specific pagination handling, as there are multiple
|
542
|
+
libraries for handling pagination in Ruby. Here's an example of how to
|
543
|
+
get started with [[https://github.com/ddnexus/pagy][Pagy]].
|
544
|
+
|
545
|
+
#+begin_src ruby
|
546
|
+
current_page_number = 1
|
547
|
+
max_items_per_page = 10
|
548
|
+
|
549
|
+
search_results = Note.search('foo')
|
550
|
+
|
551
|
+
Pagy.new(
|
552
|
+
count: search_results["search_result_metadata"]["nbHits"],
|
553
|
+
page: current_page_number,
|
554
|
+
items: max_items_per_page
|
555
|
+
)
|
556
|
+
#+end_src
|
557
|
+
|
558
|
+
** Upgrading
|
559
|
+
*** From v1.x
|
560
|
+
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.
|
561
|
+
|
562
|
+
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.
|
563
|
+
|
564
|
+
|
565
|
+
Practically speaking, there are 3 significant differences from v1.x
|
566
|
+
|
567
|
+
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=
|
568
|
+
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.
|
569
|
+
3. =Search::Client.instance.client= has been replaced with 2 clients with specific purposes
|
570
|
+
- =Search::Client::instance.search_client=
|
571
|
+
- =Search::Client::instance.admin_client=
|
572
|
+
4. =ClassMethods.search_index= has been replaced with
|
573
|
+
- =ClassMethods.searchable_index=
|
574
|
+
- =ClassMethods.administratable_index=
|
575
|
+
|
576
|
+
** Development
|
577
|
+
To contribute to this gem.
|
578
|
+
|
579
|
+
- Run =bundle install= to install all the dependencies.
|
580
|
+
- run =lefthook install= to set up
|
581
|
+
[[https://github.com/evilmartians/lefthook][lefthook]] This will do
|
582
|
+
things like make sure the tests still pass, and run rubocop before you
|
583
|
+
commit.
|
584
|
+
- Start hacking.
|
585
|
+
- Add RSpec tests.
|
586
|
+
- Add your name to CONTRIBUTORS.md
|
587
|
+
- Make PR.
|
588
|
+
|
589
|
+
NOTE: by contributing to this repository you are offering to transfer
|
590
|
+
copyright to the current maintainer of the repository.
|
591
|
+
|
592
|
+
To install this gem onto your local machine, run
|
593
|
+
=bundle exec rake install=. To release a new version, update the version
|
594
|
+
number in =version.rb=, and then run =bundle exec rake release=, which
|
595
|
+
will create a git tag for the version, push git commits and the created
|
596
|
+
tag, and push the =.gem= file to [[https://rubygems.org][rubygems.org]].
|
597
|
+
|
598
|
+
Bug reports and pull requests are welcome on GitHub at
|
599
|
+
https://github.com/masukomi/mongodb_meilisearch. This project is
|
600
|
+
intended to be a safe, welcoming space for collaboration, and
|
601
|
+
contributors are expected to adhere to the
|
602
|
+
[[https://github.com/masukomi/mongodb_meilisearch/blob/main/CODE_OF_CONDUCT.md][code of conduct]].
|
603
|
+
|
604
|
+
** License
|
605
|
+
The gem is available as open source under the terms of the
|
606
|
+
[[https://github.com/masukomi/mongodb_meilisearch/blob/main/LICENSE.txt][Server Side Public License]]. For those unfamiliar, the short version is that
|
607
|
+
if you use it in a server side app you need to share all the code for
|
608
|
+
that app and its infrastructure. It's like AGPL on steroids. Commercial
|
609
|
+
licenses are available if you want to use this in a commercial setting
|
610
|
+
but not share all your source.
|
611
|
+
|
612
|
+
** Code of Conduct
|
613
|
+
Everyone interacting in this project's codebases, issue trackers, chat
|
614
|
+
rooms and mailing lists is expected to follow the
|
615
|
+
[[https://github.com/masukomi/mongodb_meilisearch/blob/main/CODE_OF_CONDUCT.md][code
|
616
|
+
of conduct]].
|
617
|
+
|
618
|
+
* Footnotes
|
619
|
+
[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.
|
620
|
+
|
621
|
+
[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
|