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.
- checksums.yaml +4 -4
- data/Gemfile.lock +5 -4
- data/README.org +648 -0
- data/lib/mongodb_meilisearch/version.rb +1 -1
- data/lib/search/class_methods.rb +25 -19
- data/lib/search/client.rb +178 -21
- data/lib/search/errors.rb +7 -0
- data/lib/search/instance_methods.rb +11 -10
- metadata +5 -4
- data/README.md +0 -531
data/lib/search/class_methods.rb
CHANGED
@@ -69,9 +69,15 @@ module Search
|
|
69
69
|
class_index_name
|
70
70
|
end
|
71
71
|
|
72
|
+
# @return [MeiliSearch::Index] the search-only search index for this class
|
73
|
+
def searchable_index
|
74
|
+
Search::Client.instance.search_client.index(search_index_name)
|
75
|
+
end
|
76
|
+
|
72
77
|
# @return [MeiliSearch::Index] the search index for this class
|
73
|
-
|
74
|
-
|
78
|
+
# in a form that can modify the index
|
79
|
+
def administratable_index
|
80
|
+
Search::Client.instance.admin_client.index(search_index_name)
|
75
81
|
end
|
76
82
|
|
77
83
|
# MeiliSearch allows you to define the ranking of search results. Alas, this is not based
|
@@ -96,7 +102,7 @@ module Search
|
|
96
102
|
# @return [Hash] raw results directly from meilisearch-ruby gem
|
97
103
|
# This is a hash with paging information and more.
|
98
104
|
def raw_search(search_string, options = search_options)
|
99
|
-
index =
|
105
|
+
index = searchable_index
|
100
106
|
index.search(search_string, options)
|
101
107
|
end
|
102
108
|
|
@@ -251,9 +257,9 @@ module Search
|
|
251
257
|
# search_indexable_hash
|
252
258
|
def update_documents(updated_documents, async: true)
|
253
259
|
if async
|
254
|
-
|
260
|
+
administratable_index.update_documents(updated_documents, primary_search_key)
|
255
261
|
else
|
256
|
-
|
262
|
+
administratable_index.update_documents!(updated_documents, primary_search_key)
|
257
263
|
end
|
258
264
|
end
|
259
265
|
|
@@ -263,9 +269,9 @@ module Search
|
|
263
269
|
def add_documents(new_documents, async: true)
|
264
270
|
configure_attributes_and_index_if_needed!
|
265
271
|
if async
|
266
|
-
|
272
|
+
administratable_index.add_documents(new_documents, primary_search_key)
|
267
273
|
else
|
268
|
-
|
274
|
+
administratable_index.add_documents!(new_documents, primary_search_key)
|
269
275
|
end
|
270
276
|
end
|
271
277
|
|
@@ -286,14 +292,14 @@ module Search
|
|
286
292
|
# A convenience method that wraps MeiliSearch::Index#stats
|
287
293
|
# See https://www.meilisearch.com/docs/reference/api/stats for more info
|
288
294
|
def search_stats
|
289
|
-
|
295
|
+
administratable_index.stats
|
290
296
|
end
|
291
297
|
|
292
298
|
# @return [Integer] the number of documents in the search index
|
293
299
|
# as reported via stats.
|
294
300
|
# See https://www.meilisearch.com/docs/reference/api/stats for more info
|
295
301
|
def searchable_documents
|
296
|
-
|
302
|
+
administratable_index.number_of_documents
|
297
303
|
end
|
298
304
|
|
299
305
|
# @return [Boolean] indicating if search ids should be prefixed with the class name
|
@@ -309,19 +315,19 @@ module Search
|
|
309
315
|
# you should use this, you're probably mistaken.
|
310
316
|
# @warning this will delete the index and all documents in it
|
311
317
|
def delete_index!
|
312
|
-
|
318
|
+
administratable_index.delete_index
|
313
319
|
end
|
314
320
|
|
315
321
|
# Asynchronously deletes all documents from the search index
|
316
322
|
# regardless of what model they're associated with.
|
317
323
|
def delete_all_documents
|
318
|
-
|
324
|
+
administratable_index.delete_all_documents
|
319
325
|
end
|
320
326
|
|
321
327
|
# Synchronously deletes all documents from the search index
|
322
328
|
# regardless of what model they're associated with.
|
323
329
|
def delete_all_documents!
|
324
|
-
|
330
|
+
administratable_index.delete_all_documents!
|
325
331
|
end
|
326
332
|
|
327
333
|
# Asynchronously delete & reindex all instances of this class.
|
@@ -395,11 +401,11 @@ module Search
|
|
395
401
|
# @return [Array] - an array of attributes configured as sortable
|
396
402
|
# in the index.
|
397
403
|
def meilisearch_sortable_attributes
|
398
|
-
@_meilisearch_sortable_attributes ||=
|
404
|
+
@_meilisearch_sortable_attributes ||= administratable_index.get_sortable_attributes
|
399
405
|
end
|
400
406
|
|
401
407
|
def meilisearch_filterable_attributes
|
402
|
-
@_meilisearch_filterable_attributes ||=
|
408
|
+
@_meilisearch_filterable_attributes ||= administratable_index.get_filterable_attributes
|
403
409
|
end
|
404
410
|
|
405
411
|
def reset_cached_data!
|
@@ -421,7 +427,7 @@ module Search
|
|
421
427
|
# this is expected to happen the first time an instance
|
422
428
|
# of a new model is saved.
|
423
429
|
raise unless e.message.match?(/Index `\S+` not found\./)
|
424
|
-
Search::Client.instance.create_index(search_index_name)
|
430
|
+
Search::Client.instance.admin_client.create_index(search_index_name)
|
425
431
|
end
|
426
432
|
|
427
433
|
return if indexes_filterable_attributes.include?("object_class")
|
@@ -434,13 +440,13 @@ module Search
|
|
434
440
|
# which may take time. Best to run this in a background job
|
435
441
|
# for large datasets.
|
436
442
|
def set_filterable_attributes(new_attributes = filterable_attributes)
|
437
|
-
|
443
|
+
administratable_index.update_filterable_attributes(new_attributes)
|
438
444
|
end
|
439
445
|
|
440
446
|
def set_filterable_attributes!(new_attributes = filterable_attributes)
|
441
447
|
# meilisearch-ruby doesn't provide a synchronous version of this
|
442
448
|
task = set_filterable_attributes(new_attributes)
|
443
|
-
|
449
|
+
administratable_index.wait_for_task(task["taskUid"])
|
444
450
|
end
|
445
451
|
|
446
452
|
# Updates the sortable attributes in the search index.
|
@@ -448,13 +454,13 @@ module Search
|
|
448
454
|
# which may take time. Best to run this in a background job
|
449
455
|
# for large datasets.
|
450
456
|
def set_sortable_attributes(new_attributes = sortable_attributes)
|
451
|
-
|
457
|
+
administratable_index.update_sortable_attributes(new_attributes)
|
452
458
|
end
|
453
459
|
|
454
460
|
def set_sortable_attributes!(new_attributes = sortable_attributes)
|
455
461
|
# meilisearch-ruby doesn't provide a synchronous version of this
|
456
462
|
task = set_sortable_attributes(new_attributes)
|
457
|
-
|
463
|
+
administratable_index.wait_for_task(task["taskUid"])
|
458
464
|
end
|
459
465
|
|
460
466
|
private
|
data/lib/search/client.rb
CHANGED
@@ -1,42 +1,199 @@
|
|
1
1
|
require "singleton"
|
2
|
+
require "logger"
|
2
3
|
|
3
4
|
module Search
|
4
5
|
class Client
|
5
6
|
include Singleton
|
6
|
-
attr_reader :
|
7
|
+
attr_reader :admin_client, :search_client
|
8
|
+
attr_accessor :logger
|
9
|
+
MASTER_KEY = "MEILI_MASTER_KEY".freeze
|
10
|
+
SEARCH_KEY = "MEILISEARCH_SEARCH_KEY".freeze
|
11
|
+
ADMIN_KEY = "MEILISEARCH_ADMIN_KEY".freeze
|
12
|
+
URL_KEY = "MEILISEARCH_URL".freeze
|
13
|
+
TIMEOUT_KEY = "MEILISEARCH_TIMEOUT".freeze
|
14
|
+
MAX_RETRIES_KEY = "MEILISEARCH_MAX_RETRIES".freeze
|
15
|
+
SEARCH_ENABLED_KEY = "SEARCH_ENABLED".freeze
|
7
16
|
|
8
17
|
def initialize
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
else
|
20
|
-
Rails.logger.warn("UNABLE TO CONFIGURE SEARCH. Check env vars.")
|
21
|
-
@client = nil
|
18
|
+
@logger = default_logger
|
19
|
+
if ENV.fetch(SEARCH_ENABLED_KEY, "true") == "true"
|
20
|
+
initialize_clients
|
21
|
+
# WARNING: ⚠ clients MAY be nil depending on what api keys were provided
|
22
|
+
# In this case rails logger warnings and/or errors will have already
|
23
|
+
# been created.
|
24
|
+
unless admin_client || search_client
|
25
|
+
raise Search::Errors::ConfigurationError.new(
|
26
|
+
"Unable to configure any MeliSearch clients. Check env vars."
|
27
|
+
)
|
22
28
|
end
|
29
|
+
else
|
30
|
+
@logger.info("#{SEARCH_ENABLED_KEY} is not \"true\" - mongodb_meilisearch NOT initialized")
|
23
31
|
end
|
24
32
|
end
|
25
33
|
|
34
|
+
# Indicates if there is a client available to
|
35
|
+
# administration OR searches.
|
26
36
|
def enabled?
|
27
|
-
!@
|
37
|
+
!@admin_client.nil? || !@search_client.nil?
|
38
|
+
end
|
39
|
+
|
40
|
+
# Indicates if there is a client available
|
41
|
+
# that has been configured with an admin key.
|
42
|
+
def admin_enabled?
|
43
|
+
!@admin_client.nil?
|
44
|
+
end
|
45
|
+
|
46
|
+
# @deprecated use search_client for searches
|
47
|
+
# & admin_client for everything else
|
48
|
+
def client
|
49
|
+
@logger.info("Search::Client.instance.client is a deprecated method")
|
50
|
+
@admin_client || @search_client
|
28
51
|
end
|
29
52
|
|
30
|
-
def
|
31
|
-
|
32
|
-
|
53
|
+
def initialize_clients
|
54
|
+
# see what env vars they've configured
|
55
|
+
search_api_key = ENV.fetch(SEARCH_KEY, nil)
|
56
|
+
admin_api_key = ENV.fetch(ADMIN_KEY, nil)
|
57
|
+
search_api_key = nil if search_api_key == ""
|
58
|
+
admin_api_key = nil if admin_api_key == ""
|
59
|
+
|
60
|
+
# if there is a master key (and it's valid) we're guaranteed to have
|
61
|
+
# default api keys we can use for search & admin
|
62
|
+
if search_api_key.nil? || admin_api_key.nil?
|
63
|
+
m_c = master_client
|
64
|
+
if m_c
|
65
|
+
default_keys = get_default_keys(m_c)
|
66
|
+
search_api_key ||= default_keys[:search]
|
67
|
+
admin_api_key ||= default_keys[:admin]
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
if !admin_api_key.nil?
|
72
|
+
@admin_client = initialize_new_client(
|
73
|
+
url: url,
|
74
|
+
api_key: admin_api_key,
|
75
|
+
timeout: timeout,
|
76
|
+
max_retries: max_retries
|
77
|
+
)
|
78
|
+
@logger.debug("initialized admin client with admin key: #{admin_api_key[0..5]}…")
|
79
|
+
else
|
80
|
+
@logger.error("UNABLE TO CONFIGURE MEILISEARCH ADMINISTRATION CLIENT. Check env vars.")
|
81
|
+
@admin_client = nil
|
82
|
+
end
|
83
|
+
|
84
|
+
if !search_api_key.nil?
|
85
|
+
@search_client = initialize_new_client(
|
86
|
+
url: url,
|
87
|
+
api_key: search_api_key,
|
88
|
+
timeout: timeout,
|
89
|
+
max_retries: max_retries
|
90
|
+
)
|
91
|
+
@logger.debug("initialized search client with search key: #{search_api_key[0..5]}…")
|
33
92
|
else
|
34
|
-
|
93
|
+
@logger.error("UNABLE TO CONFIGURE GENERAL MEILISEARCH SEARCH CLIENT. Check env vars.")
|
94
|
+
@search_client = nil
|
95
|
+
end
|
96
|
+
rescue MeiliSearch::ApiError => e
|
97
|
+
@logger.error("MeiliSearch Api Error when attempting to list keys: #{e}")
|
98
|
+
end
|
99
|
+
|
100
|
+
def initialize_new_client(api_key:, url:, timeout:, max_retries:)
|
101
|
+
MeiliSearch::Client.new(url, api_key,
|
102
|
+
timeout: timeout,
|
103
|
+
max_retries: max_retries)
|
104
|
+
end
|
105
|
+
|
106
|
+
def master_client(master_api_key = nil)
|
107
|
+
master_api_key = nil if master_api_key == ""
|
108
|
+
master_api_key ||= ENV.fetch(MASTER_KEY, nil)
|
109
|
+
if !url || !master_api_key
|
110
|
+
|
111
|
+
unless master_api_key
|
112
|
+
@logger.error(
|
113
|
+
"#{MASTER_KEY} is not set. Cannot create master client."
|
114
|
+
)
|
115
|
+
end
|
116
|
+
|
117
|
+
return nil
|
35
118
|
end
|
119
|
+
|
120
|
+
initialize_new_client(
|
121
|
+
url: url,
|
122
|
+
api_key: ENV.fetch(MASTER_KEY),
|
123
|
+
timeout: timeout,
|
124
|
+
max_retries: max_retries
|
125
|
+
)
|
126
|
+
end
|
127
|
+
|
128
|
+
def url
|
129
|
+
maybe_url = ENV.fetch(URL_KEY, nil)
|
130
|
+
unless maybe_url
|
131
|
+
@logger.error(
|
132
|
+
"#{MASTER_KEY} is not set. Cannot create master client."
|
133
|
+
)
|
134
|
+
end
|
135
|
+
maybe_url
|
136
|
+
end
|
137
|
+
|
138
|
+
def timeout
|
139
|
+
ENV.fetch(TIMEOUT_KEY, 10).to_i
|
140
|
+
end
|
141
|
+
|
142
|
+
def max_retries
|
143
|
+
ENV.fetch(MAX_RETRIES_KEY, 2).to_i
|
144
|
+
end
|
145
|
+
|
146
|
+
def get_default_keys(m_c)
|
147
|
+
# NOTE: master_client /can/ return nil
|
148
|
+
if m_c.nil?
|
149
|
+
m_c = master_client
|
150
|
+
elsif m_c.is_a?(String)
|
151
|
+
m_c = master_client(m_c)
|
152
|
+
end
|
153
|
+
|
154
|
+
unless m_c
|
155
|
+
raise Search::Errors::ConfigurationError.new(
|
156
|
+
"Can't retrieve default keys without Master API Key & URL configured."
|
157
|
+
)
|
158
|
+
end
|
159
|
+
keys = m_c.keys
|
160
|
+
response = {search: nil, admin: nil}
|
161
|
+
|
162
|
+
keys&.[]("results")&.each do |hash|
|
163
|
+
if hash["name"] == "Default Search API Key"
|
164
|
+
response[:search] = hash["key"]
|
165
|
+
elsif hash["name"] == "Default Admin API Key"
|
166
|
+
response[:admin] = hash["key"]
|
167
|
+
end
|
168
|
+
end
|
169
|
+
response
|
170
|
+
end
|
171
|
+
|
172
|
+
# Validates that the keys keys provided for search & admin
|
173
|
+
# correspond to the default keys Meilisearch returns
|
174
|
+
# @param [String] m_c - the Meilisearch Master Key to use.
|
175
|
+
# This is most likely found in ENV['MEILI_MASTER_KEY']
|
176
|
+
# @return [Hash[Hash]]
|
177
|
+
def validate_default_keys(m_c)
|
178
|
+
default_keys = get_default_keys(m_c)
|
179
|
+
search_key = ENV.fetch(SEARCH_KEY, nil)
|
180
|
+
admin_key = ENV.fetch(ADMIN_KEY, nil)
|
181
|
+
|
182
|
+
{
|
183
|
+
search_key: {
|
184
|
+
status: search_key.nil? ? "missing" : "provided",
|
185
|
+
matches: default_keys[:search] == search_key
|
186
|
+
},
|
187
|
+
admin_key: {
|
188
|
+
status: admin_key.nil? ? "missing" : "provided",
|
189
|
+
matches: default_keys[:admin] == admin_key
|
190
|
+
}
|
191
|
+
}
|
36
192
|
end
|
37
193
|
|
38
|
-
def
|
39
|
-
|
194
|
+
def default_logger
|
195
|
+
in_rails = Module.constants.include?(:Rails)
|
196
|
+
in_rails ? Rails.logger : Logger.new($stdout)
|
40
197
|
end
|
41
198
|
end
|
42
199
|
end
|
@@ -3,7 +3,7 @@ module Search
|
|
3
3
|
# Adds this record to the search index asynchronously
|
4
4
|
def add_to_search
|
5
5
|
self.class.configure_attributes_and_index_if_needed!
|
6
|
-
|
6
|
+
administratable_index.add_documents(
|
7
7
|
[search_indexable_hash],
|
8
8
|
primary_search_key.to_s
|
9
9
|
)
|
@@ -12,7 +12,7 @@ module Search
|
|
12
12
|
# Adds this record to the search index synchronously
|
13
13
|
def add_to_search!
|
14
14
|
self.class.configure_attributes_and_index_if_needed!
|
15
|
-
index =
|
15
|
+
index = administratable_index
|
16
16
|
documents = [search_indexable_hash]
|
17
17
|
pk = primary_search_key.to_s
|
18
18
|
index.add_documents!(documents, pk)
|
@@ -20,7 +20,7 @@ module Search
|
|
20
20
|
|
21
21
|
# Updates this record in the search index asynchronously
|
22
22
|
def update_in_search
|
23
|
-
|
23
|
+
administratable_index.update_documents(
|
24
24
|
[search_indexable_hash],
|
25
25
|
primary_search_key
|
26
26
|
)
|
@@ -28,7 +28,7 @@ module Search
|
|
28
28
|
|
29
29
|
# Updates this record in the search index synchronously
|
30
30
|
def update_in_search!
|
31
|
-
|
31
|
+
administratable_index.update_documents!(
|
32
32
|
[search_indexable_hash],
|
33
33
|
primary_search_key
|
34
34
|
)
|
@@ -36,12 +36,12 @@ module Search
|
|
36
36
|
|
37
37
|
# Removes this record from the search asynchronously
|
38
38
|
def remove_from_search
|
39
|
-
|
39
|
+
administratable_index.delete_document(send(primary_search_key).to_s)
|
40
40
|
end
|
41
41
|
|
42
42
|
# Removes this record from the search synchronously
|
43
43
|
def remove_from_search!
|
44
|
-
|
44
|
+
administratable_index.delete_document!(send(primary_search_key).to_s)
|
45
45
|
end
|
46
46
|
|
47
47
|
def searchable_attributes
|
@@ -60,6 +60,7 @@ module Search
|
|
60
60
|
# _unless_ one is already defined. This gem relies on "object_class" being present
|
61
61
|
# in returned results
|
62
62
|
def search_indexable_hash
|
63
|
+
return @_search_indexable_hash if defined?(@_search_indexable_hash)
|
63
64
|
klass = self.class
|
64
65
|
# the to_s & to_sym is just safety in case someone
|
65
66
|
# defined searchable_attributes as an array of strings
|
@@ -83,13 +84,13 @@ module Search
|
|
83
84
|
|
84
85
|
hash["object_class"] = klass.name unless hash.has_key?("object_class")
|
85
86
|
hash["original_document_id"] = _id.to_s if klass.has_class_prefixed_search_ids?
|
86
|
-
hash
|
87
|
+
@_search_indexable_hash = hash
|
87
88
|
end
|
88
89
|
|
89
|
-
# A convenience method to ease accessing the
|
90
|
+
# A convenience method to ease accessing the administratable index
|
90
91
|
# from the ClassMethods
|
91
|
-
def
|
92
|
-
self.class.
|
92
|
+
def administratable_index
|
93
|
+
self.class.administratable_index
|
93
94
|
end
|
94
95
|
|
95
96
|
# A convenience method to ease accessing the primary search key
|
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.
|
4
|
+
version: 2.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: 2024-
|
11
|
+
date: 2024-08-04 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -156,13 +156,14 @@ files:
|
|
156
156
|
- Gemfile
|
157
157
|
- Gemfile.lock
|
158
158
|
- LICENSE.txt
|
159
|
-
- README.
|
159
|
+
- README.org
|
160
160
|
- Rakefile
|
161
161
|
- lefthook.yml
|
162
162
|
- lib/mongodb_meilisearch.rb
|
163
163
|
- lib/mongodb_meilisearch/version.rb
|
164
164
|
- lib/search/class_methods.rb
|
165
165
|
- lib/search/client.rb
|
166
|
+
- lib/search/errors.rb
|
166
167
|
- lib/search/instance_methods.rb
|
167
168
|
- sig/mongodb_meilisearch.rbs
|
168
169
|
homepage: https://github.com/masukomi/mongodb_meilisearch
|
@@ -187,7 +188,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
187
188
|
- !ruby/object:Gem::Version
|
188
189
|
version: '0'
|
189
190
|
requirements: []
|
190
|
-
rubygems_version: 3.
|
191
|
+
rubygems_version: 3.5.11
|
191
192
|
signing_key:
|
192
193
|
specification_version: 4
|
193
194
|
summary: MeiliSearch integration for MongoDB
|