s',
44 | host: ENV.fetch('MEILISEARCH_URL', 'localhost'), port: ENV.fetch('MEILISEARCH_PORT', '7700'))
45 | MASTER_KEY = 'masterKey'
46 | DEFAULT_SEARCH_RESPONSE_KEYS = [
47 | 'hits',
48 | 'offset',
49 | 'limit',
50 | 'estimatedTotalHits',
51 | 'processingTimeMs',
52 | 'query',
53 | 'nbHits'
54 | ].freeze
55 |
56 | FINITE_PAGINATED_SEARCH_RESPONSE_KEYS = [
57 | 'hits',
58 | 'query',
59 | 'processingTimeMs',
60 | 'hitsPerPage',
61 | 'page',
62 | 'totalPages',
63 | 'totalHits'
64 | ].freeze
65 |
66 | Dir["#{Dir.pwd}/spec/support/**/*.rb"].each { |file| require file }
67 |
68 | # Reduce interval during testing to improve the test speeds
69 | # There are no long-running meilisearch operations during testing so this should be fine
70 | Meilisearch::Models::Task.default_interval_ms = 5
71 |
72 | RSpec.configure do |config|
73 | config.filter_run_when_matching :focus
74 | config.example_status_persistence_file_path = 'spec/examples.txt'
75 | config.order = :random
76 |
77 | config.include_context 'test defaults'
78 |
79 | config.include IndexesHelpers
80 | config.include ExceptionsHelpers
81 | config.include ExperimentalFeatureHelpers
82 | config.include KeysHelpers
83 |
84 | # New RSpec 4 defaults, remove when updated to RSpec 4
85 | config.shared_context_metadata_behavior = :apply_to_host_groups
86 |
87 | config.expect_with :rspec do |expectations|
88 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true
89 | end
90 |
91 | config.mock_with :rspec do |mocks|
92 | mocks.verify_partial_doubles = true
93 | end
94 | end
95 |
--------------------------------------------------------------------------------
/lib/meilisearch/utils.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'logger'
4 |
5 | module Meilisearch
6 | module Utils
7 | SNAKE_CASE = /[^a-zA-Z0-9]+(.)/
8 |
9 | class << self
10 | attr_writer :logger
11 |
12 | def logger
13 | @logger ||= Logger.new($stdout)
14 | end
15 |
16 | def soft_deprecate(subject, replacement)
17 | logger.warn("[meilisearch-ruby] #{subject} is DEPRECATED, please use #{replacement} instead.")
18 | end
19 |
20 | def warn_on_unfinished_task(task_uid)
21 | message = <<~UNFINISHED_TASK_WARNING
22 | [meilisearch-ruby] Task #{task_uid}'s finished state (succeeded?/failed?/cancelled?) is being checked before finishing.
23 | [meilisearch-ruby] Tasks in meilisearch are processed in the background asynchronously.
24 | [meilisearch-ruby] Please use the #finished? method to check if the task is finished or the #await method to wait for the task to finish.
25 | UNFINISHED_TASK_WARNING
26 |
27 | message.lines.each do |line|
28 | logger.warn(line)
29 | end
30 | end
31 |
32 | def transform_attributes(body)
33 | case body
34 | when Array
35 | body.map { |item| transform_attributes(item) }
36 | when Hash
37 | warn_on_non_conforming_attribute_names(body)
38 | parse(body)
39 | else
40 | body
41 | end
42 | end
43 |
44 | def filter(original_options, allowed_params = [])
45 | original_options.transform_keys(&:to_sym).slice(*allowed_params)
46 | end
47 |
48 | def parse_query(original_options, allowed_params = [])
49 | only_allowed_params = filter(original_options, allowed_params)
50 |
51 | Utils.transform_attributes(only_allowed_params).then do |body|
52 | body.transform_values do |v|
53 | v.respond_to?(:join) ? v.join(',') : v.to_s
54 | end
55 | end
56 | end
57 |
58 | def version_error_handler(method_name)
59 | yield if block_given?
60 | rescue Meilisearch::ApiError => e
61 | message = message_builder(e.http_message, method_name)
62 |
63 | raise Meilisearch::ApiError.new(e.http_code, message, e.http_body)
64 | rescue StandardError => e
65 | raise e.class, message_builder(e.message, method_name)
66 | end
67 |
68 | def warn_on_non_conforming_attribute_names(body)
69 | return if body.nil?
70 |
71 | non_snake_case = body.keys.grep_v(/^[a-z0-9_]+$/)
72 | return if non_snake_case.empty?
73 |
74 | message = <<~MSG
75 | [meilisearch-ruby] Attributes will be expected to be snake_case in future versions.
76 | [meilisearch-ruby] Non-conforming attributes: #{non_snake_case.join(', ')}
77 | MSG
78 |
79 | logger.warn(message)
80 | end
81 |
82 | private
83 |
84 | def parse(body)
85 | body
86 | .transform_keys(&:to_s)
87 | .transform_keys do |key|
88 | key.include?('_') ? key.downcase.gsub(SNAKE_CASE, &:upcase).gsub('_', '') : key
89 | end
90 | end
91 |
92 | def message_builder(current_message, method_name)
93 | "#{current_message}\nHint: It might not be working because maybe you're not up " \
94 | "to date with the Meilisearch version that `#{method_name}` call requires."
95 | end
96 | end
97 | end
98 | end
99 |
--------------------------------------------------------------------------------
/spec/meilisearch/index/search/facet_search_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe 'Meilisearch::Index - Facet search' do
4 | include_context 'search books with author, genre, year'
5 |
6 | before do
7 | response = index.update_filterable_attributes(['genre', 'year', 'author'])
8 | index.wait_for_task(response['taskUid'])
9 | end
10 |
11 | it 'requires facet name parameter' do
12 | expect { index.facet_search }.to raise_error ArgumentError
13 | end
14 |
15 | context 'without query parameter' do
16 | let(:results) { index.facet_search 'genre' }
17 |
18 | it 'returns all genres' do
19 | expect(results).to include(
20 | 'facetHits' => a_collection_including(
21 | a_hash_including('value' => 'fantasy'),
22 | a_hash_including('value' => 'adventure'),
23 | a_hash_including('value' => 'romance')
24 | )
25 | )
26 | end
27 |
28 | it 'returns all genre counts' do
29 | expect(results).to include(
30 | 'facetHits' => a_collection_including(
31 | a_hash_including('count' => 3),
32 | a_hash_including('count' => 3),
33 | a_hash_including('count' => 2)
34 | )
35 | )
36 | end
37 |
38 | it 'filters correctly' do
39 | results = index.facet_search 'genre', filter: 'year < 1940'
40 |
41 | expect(results['facetHits']).to contain_exactly(
42 | {
43 | 'value' => 'adventure',
44 | 'count' => 2
45 | },
46 | {
47 | 'value' => 'romance',
48 | 'count' => 2
49 | }
50 | )
51 | end
52 | end
53 |
54 | context 'with facet_query argument' do
55 | let(:results) { index.facet_search 'genre', 'fan' }
56 |
57 | it 'returns only matching genres' do
58 | expect(results).to include(
59 | 'facetHits' => a_collection_containing_exactly(
60 | 'value' => 'fantasy',
61 | 'count' => 3
62 | )
63 | )
64 | end
65 |
66 | it 'filters correctly' do
67 | results = index.facet_search 'genre', 'fantasy', filter: 'year < 2006'
68 |
69 | expect(results['facetHits']).to contain_exactly(
70 | 'value' => 'fantasy',
71 | 'count' => 2
72 | )
73 | end
74 | end
75 |
76 | context 'with q parameter' do
77 | it 'applies matching_strategy "all"' do
78 | results = index.facet_search 'author', 'J. K. Rowling', q: 'Potter Stories', matching_strategy: 'all'
79 |
80 | expect(results['facetHits']).to be_empty
81 | end
82 |
83 | it 'applies matching_strategy "last"' do
84 | results = index.facet_search 'author', 'J. K. Rowling', q: 'Potter Stories', matching_strategy: 'last'
85 |
86 | expect(results).to include(
87 | 'facetHits' => a_collection_containing_exactly(
88 | 'value' => 'J. K. Rowling',
89 | 'count' => 2
90 | )
91 | )
92 | end
93 |
94 | it 'applies filter parameter' do
95 | results = index.facet_search 'author', 'J. K. Rowling', q: 'Potter', filter: 'year < 2007'
96 |
97 | expect(results).to include(
98 | 'facetHits' => a_collection_containing_exactly(
99 | 'value' => 'J. K. Rowling',
100 | 'count' => 1
101 | )
102 | )
103 | end
104 |
105 | it 'applies attributes_to_search_on parameter' do
106 | results = index.facet_search 'author', 'J. K. Rowling', q: 'Potter', attributes_to_search_on: ['year']
107 |
108 | expect(results['facetHits']).to be_empty
109 | end
110 | end
111 | end
112 |
--------------------------------------------------------------------------------
/spec/meilisearch/index/search/multi_params_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe 'Meilisearch::Index - Multi-paramaters search' do
4 | include_context 'search books with genre'
5 |
6 | before { index.update_filterable_attributes(['genre']).await }
7 |
8 | it 'does a custom search with attributes to crop, filter and attributes to highlight' do
9 | response = index.search('prince',
10 | {
11 | attributes_to_crop: ['title'],
12 | crop_length: 2,
13 | filter: 'genre = adventure',
14 | attributes_to_highlight: ['title']
15 | })
16 | expect(response['hits'].count).to be(1)
17 | expect(response['hits'].first).to have_key('_formatted')
18 | expect(response['hits'].first['_formatted']['title']).to eq('…Petit Prince')
19 | end
20 |
21 | it 'does a custom search with attributes_to_retrieve and a limit' do
22 | response = index.search('the', attributes_to_retrieve: ['title', 'genre'], limit: 2)
23 | expect(response).to be_a(Hash)
24 | expect(response.keys).to include(*DEFAULT_SEARCH_RESPONSE_KEYS)
25 | expect(response['hits'].count).to eq(2)
26 | expect(response['hits'].first).to have_key('title')
27 | expect(response['hits'].first).not_to have_key('objectId')
28 | expect(response['hits'].first).to have_key('genre')
29 | end
30 |
31 | it 'does a placeholder search with filter and offset' do
32 | response = index.search('', { filter: 'genre = adventure', offset: 2 })
33 | expect(response['hits'].count).to eq(1)
34 | end
35 |
36 | it 'does a custom search with limit and attributes to highlight' do
37 | response = index.search('the', { limit: 1, attributes_to_highlight: ['*'] })
38 | expect(response).to be_a(Hash)
39 | expect(response.keys).to include(*DEFAULT_SEARCH_RESPONSE_KEYS)
40 | expect(response['hits'].count).to eq(1)
41 | expect(response['hits'].first).to have_key('_formatted')
42 | end
43 |
44 | it 'does a custom search with filter, attributes_to_retrieve and attributes_to_highlight' do
45 | index.update_filterable_attributes(['genre']).await
46 | response = index.search('prinec',
47 | {
48 | filter: ['genre = fantasy'],
49 | attributes_to_retrieve: ['title'],
50 | attributes_to_highlight: ['*']
51 | })
52 | expect(response.keys).to include(*DEFAULT_SEARCH_RESPONSE_KEYS)
53 | expect(response['estimatedTotalHits']).to eq(1)
54 | expect(response['hits'].first).to have_key('_formatted')
55 | expect(response['hits'].first).not_to have_key('objectId')
56 | expect(response['hits'].first).not_to have_key('genre')
57 | expect(response['hits'].first).to have_key('title')
58 | expect(response['hits'].first['_formatted']['title']).to eq('Harry Potter and the Half-Blood Prince')
59 | end
60 |
61 | it 'does a custom search with facets and limit' do
62 | index.update_filterable_attributes(['genre']).await
63 | response = index.search('prinec', facets: ['genre'], limit: 1)
64 |
65 | expect(response.keys).to include(
66 | *DEFAULT_SEARCH_RESPONSE_KEYS,
67 | 'facetDistribution',
68 | 'facetStats'
69 | )
70 | expect(response['estimatedTotalHits']).to eq(2)
71 | expect(response['hits'].count).to eq(1)
72 | expect(response['facetDistribution'].keys).to contain_exactly('genre')
73 | expect(response['facetDistribution']['genre'].keys).to contain_exactly('adventure', 'fantasy')
74 | expect(response['facetDistribution']['genre']['adventure']).to eq(1)
75 | expect(response['facetDistribution']['genre']['fantasy']).to eq(1)
76 | end
77 | end
78 |
--------------------------------------------------------------------------------
/spec/meilisearch/client/keys_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe 'Meilisearch::Client - Keys' do
4 | context 'When managing keys' do
5 | let(:uuid_v4) { 'c483e150-cff1-4a45-ac26-bb8eb8e01d36' }
6 | let(:delete_docs_key_options) do
7 | {
8 | description: 'A new key to delete docs',
9 | actions: ['documents.delete'],
10 | indexes: ['*'],
11 | expires_at: nil
12 | }
13 | end
14 | let(:add_docs_key_options) do
15 | {
16 | description: 'A new key to add docs',
17 | actions: ['documents.add'],
18 | indexes: ['*'],
19 | expires_at: nil
20 | }
21 | end
22 |
23 | it 'creates a key' do
24 | new_key = client.create_key(add_docs_key_options)
25 |
26 | expect(new_key['expiresAt']).to be_nil
27 | expect(new_key['key']).to be_a(String)
28 | expect(new_key['createdAt']).to be_a(String)
29 | expect(new_key['updatedAt']).to be_a(String)
30 | expect(new_key['indexes']).to eq(['*'])
31 | expect(new_key['description']).to eq('A new key to add docs')
32 | end
33 |
34 | it 'creates a key with wildcarded action' do
35 | new_key = client.create_key(add_docs_key_options.merge(actions: ['documents.*']))
36 |
37 | expect(new_key['actions']).to eq(['documents.*'])
38 | end
39 |
40 | it 'creates a key with setting uid' do
41 | new_key = client.create_key(add_docs_key_options.merge(uid: uuid_v4))
42 |
43 | expect(new_key['expiresAt']).to be_nil
44 | expect(new_key['name']).to be_nil
45 | expect(new_key['uid']).to eq(uuid_v4)
46 | expect(new_key['key']).to be_a(String)
47 | expect(new_key['createdAt']).to be_a(String)
48 | expect(new_key['updatedAt']).to be_a(String)
49 | expect(new_key['indexes']).to eq(['*'])
50 | expect(new_key['description']).to eq('A new key to add docs')
51 | end
52 |
53 | it 'gets a key with their key data' do
54 | new_key = client.create_key(delete_docs_key_options)
55 |
56 | expect(client.key(new_key['key'])['description']).to eq('A new key to delete docs')
57 |
58 | key = client.key(new_key['key'])
59 |
60 | expect(key['expiresAt']).to be_nil
61 | expect(key['key']).to be_a(String)
62 | expect(key['createdAt']).to be_a(String)
63 | expect(key['updatedAt']).to be_a(String)
64 | expect(key['indexes']).to eq(['*'])
65 | expect(key['description']).to eq('A new key to delete docs')
66 | end
67 |
68 | it 'retrieves a list of keys' do
69 | new_key = client.create_key(add_docs_key_options)
70 |
71 | list = client.keys
72 |
73 | expect(list.keys).to contain_exactly('limit', 'offset', 'results', 'total')
74 | expect(list['results']).to eq([new_key])
75 | expect(list['total']).to eq(1)
76 | end
77 |
78 | it 'paginates keys list with limit/offset' do
79 | client.create_key(add_docs_key_options)
80 |
81 | expect(client.keys(limit: 0, offset: 20)['results']).to be_empty
82 | expect(client.keys(limit: 5, offset: 199)['results']).to be_empty
83 | end
84 |
85 | it 'gets a key with their uid' do
86 | new_key = client.create_key(delete_docs_key_options.merge(uid: uuid_v4))
87 |
88 | key = client.key(uuid_v4)
89 |
90 | expect(key).to eq(new_key)
91 | end
92 |
93 | it 'updates a key with their key data' do
94 | new_key = client.create_key(delete_docs_key_options)
95 | new_updated_key = client.update_key(new_key['key'], indexes: ['coco'], description: 'no coco')
96 |
97 | expect(new_updated_key['key']).to eq(new_key['key'])
98 | expect(new_updated_key['description']).to eq('no coco')
99 | # remain untouched since v0.28.0 Meilisearch just support updating name/description.
100 | expect(new_updated_key['indexes']).to eq(['*'])
101 | end
102 |
103 | it 'updates a key with their uid data' do
104 | client.create_key(delete_docs_key_options.merge(uid: uuid_v4))
105 | new_updated_key = client.update_key(uuid_v4, name: 'coco')
106 |
107 | expect(new_updated_key['name']).to eq('coco')
108 | end
109 |
110 | it 'deletes a key' do
111 | new_key = client.create_key(add_docs_key_options)
112 | client.delete_key(new_key['key'])
113 |
114 | expect do
115 | client.key(new_key['key'])
116 | end.to raise_error(Meilisearch::ApiError)
117 | end
118 | end
119 | end
120 |
--------------------------------------------------------------------------------
/lib/meilisearch/models/task.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'forwardable'
4 |
5 | module Meilisearch
6 | module Models
7 | class Task
8 | DEFAULT_TIMEOUT_MS = 5000
9 | DEFAULT_INTERVAL_MS = 50
10 |
11 | class << self
12 | attr_writer :default_timeout_ms, :default_interval_ms
13 |
14 | def default_timeout_ms
15 | @default_timeout_ms || DEFAULT_TIMEOUT_MS
16 | end
17 |
18 | def default_interval_ms
19 | @default_interval_ms || DEFAULT_INTERVAL_MS
20 | end
21 | end
22 |
23 | extend Forwardable
24 |
25 | # Maintain backwards compatibility with task hash return type
26 | def_delegators :metadata, :[], :dig, :keys, :key?, :has_key?
27 |
28 | attr_reader :metadata
29 |
30 | def initialize(metadata_hash, task_endpoint)
31 | self.metadata = metadata_hash
32 | validate_required_fields! metadata
33 |
34 | @task_endpoint = task_endpoint
35 | end
36 |
37 | def uid
38 | @metadata['taskUid']
39 | end
40 |
41 | def type
42 | @metadata['type']
43 | end
44 |
45 | def status
46 | @metadata['status']
47 | end
48 |
49 | def enqueued?
50 | refresh if status_enqueued?
51 |
52 | status_enqueued?
53 | end
54 |
55 | def processing?
56 | refresh if status_processing? || status_enqueued?
57 |
58 | status_processing?
59 | end
60 |
61 | def unfinished?
62 | refresh if status_processing? || status_enqueued?
63 |
64 | status_processing? || status_enqueued?
65 | end
66 | alias waiting? unfinished?
67 |
68 | def finished?
69 | !unfinished?
70 | end
71 |
72 | def succeeded?
73 | Utils.warn_on_unfinished_task(self) if unfinished?
74 |
75 | status == 'succeeded'
76 | end
77 | alias has_succeeded? succeeded?
78 |
79 | def failed?
80 | Utils.warn_on_unfinished_task(self) if unfinished?
81 |
82 | status == 'failed'
83 | end
84 | alias has_failed? failed?
85 |
86 | def cancelled?
87 | Utils.warn_on_unfinished_task(self) if unfinished?
88 |
89 | status_cancelled?
90 | end
91 |
92 | def deleted?
93 | refresh unless @deleted
94 |
95 | !!@deleted
96 | end
97 |
98 | def error
99 | @metadata['error']
100 | end
101 |
102 | def refresh(with: nil)
103 | self.metadata = with || @task_endpoint.task(uid)
104 |
105 | self
106 | rescue Meilisearch::ApiError => e
107 | raise e unless e.http_code == 404
108 |
109 | @deleted = true
110 |
111 | self
112 | end
113 |
114 | def await(
115 | timeout_in_ms = self.class.default_timeout_ms,
116 | interval_in_ms = self.class.default_interval_ms
117 | )
118 | refresh with: @task_endpoint.wait_for_task(uid, timeout_in_ms, interval_in_ms) unless finished?
119 |
120 | self
121 | end
122 |
123 | def cancel
124 | return true if status_cancelled?
125 | return false if status_finished?
126 |
127 | @task_endpoint.cancel_tasks(uids: [uid]).await
128 |
129 | cancelled?
130 | end
131 |
132 | def delete
133 | return false unless status_finished?
134 |
135 | @task_endpoint.delete_tasks(uids: [uid]).await
136 |
137 | deleted?
138 | end
139 |
140 | def to_h
141 | @metadata
142 | end
143 | alias to_hash to_h
144 |
145 | private
146 |
147 | def validate_required_fields!(task_hash)
148 | raise ArgumentError, 'Cannot instantiate a task without an ID' unless task_hash['taskUid']
149 | raise ArgumentError, 'Cannot instantiate a task without a type' unless task_hash['type']
150 | raise ArgumentError, 'Cannot instantiate a task without a status' unless task_hash['status']
151 | end
152 |
153 | def status_enqueued?
154 | status == 'enqueued'
155 | end
156 |
157 | def status_processing?
158 | status == 'processing'
159 | end
160 |
161 | def status_finished?
162 | ['succeeded', 'failed', 'cancelled'].include? status
163 | end
164 |
165 | def status_cancelled?
166 | status == 'cancelled'
167 | end
168 |
169 | def metadata=(metadata)
170 | @metadata = metadata
171 |
172 | uid = @metadata['taskUid'] || @metadata['uid']
173 | @metadata['uid'] = uid
174 | @metadata['taskUid'] = uid
175 | end
176 | end
177 | end
178 | end
179 |
--------------------------------------------------------------------------------
/spec/support/books_contexts.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.shared_context 'search books with genre' do
4 | let(:index) { client.index('books') }
5 | let(:documents) do
6 | [
7 | { objectId: 123, title: 'Pride and Prejudice', genre: 'romance' },
8 | { objectId: 456, title: 'Le Petit Prince', genre: 'adventure' },
9 | { objectId: 1, title: 'Alice In Wonderland', genre: 'adventure' },
10 | { objectId: 2, title: 'Le Rouge et le Noir', genre: 'romance' },
11 | { objectId: 1344, title: 'The Hobbit', genre: 'adventure' },
12 | { objectId: 4, title: 'Harry Potter and the Half-Blood Prince', genre: 'fantasy' },
13 | { objectId: 7, title: 'Harry Potter and the Chamber of Secrets', genre: 'fantasy' },
14 | { objectId: 42, title: 'The Hitchhiker\'s Guide to the Galaxy' }
15 | ]
16 | end
17 |
18 | before { index.add_documents(documents).await }
19 | end
20 |
21 | RSpec.shared_context 'search books with author, genre, year' do
22 | let(:index) { client.index('books') }
23 | let(:documents) do
24 | [
25 | {
26 | objectId: 123,
27 | title: 'Pride and Prejudice',
28 | year: 1813,
29 | author: 'Jane Austen',
30 | genre: 'romance'
31 | },
32 | {
33 | objectId: 456,
34 | title: 'Le Petit Prince',
35 | year: 1943,
36 | author: 'Antoine de Saint-Exupéry',
37 | genre: 'adventure'
38 | },
39 | {
40 | objectId: 1,
41 | title: 'Alice In Wonderland',
42 | year: 1865,
43 | author: 'Lewis Carroll',
44 | genre: 'adventure'
45 | },
46 | {
47 | objectId: 2,
48 | title: 'Le Rouge et le Noir',
49 | year: 1830,
50 | author: 'Stendhal',
51 | genre: 'romance'
52 | },
53 | {
54 | objectId: 1344,
55 | title: 'The Hobbit',
56 | year: 1937,
57 | author: 'J. R. R. Tolkien',
58 | genre: 'adventure'
59 | },
60 | {
61 | objectId: 4,
62 | title: 'Harry Potter and the Half-Blood Prince',
63 | year: 2005,
64 | author: 'J. K. Rowling',
65 | genre: 'fantasy'
66 | },
67 | {
68 | objectId: 2056,
69 | title: 'Harry Potter and the Deathly Hallows',
70 | year: 2007,
71 | author: 'J. K. Rowling',
72 | genre: 'fantasy'
73 | },
74 | {
75 | objectId: 42,
76 | title: 'The Hitchhiker\'s Guide to the Galaxy',
77 | year: 1978,
78 | author: 'Douglas Adams'
79 | },
80 | {
81 | objectId: 190,
82 | title: 'A Game of Thrones',
83 | year: 1996,
84 | author: 'George R. R. Martin',
85 | genre: 'fantasy'
86 | }
87 | ]
88 | end
89 |
90 | before { index.add_documents(documents).await }
91 | end
92 |
93 | RSpec.shared_context 'search books with nested fields' do
94 | let(:index) { client.index('books') }
95 | let(:documents) do
96 | [
97 | {
98 | id: 1,
99 | title: 'Pride and Prejudice',
100 | info: {
101 | comment: 'A great book',
102 | reviewNb: 50
103 | }
104 | },
105 | {
106 | id: 2,
107 | title: 'Le Petit Prince',
108 | info: {
109 | comment: 'A french book',
110 | reviewNb: 600
111 | }
112 | },
113 | {
114 | id: 3,
115 | title: 'Le Rouge et le Noir',
116 | info: {
117 | comment: 'Another french book',
118 | reviewNb: 700
119 | }
120 | },
121 | {
122 | id: 4,
123 | title: 'Alice In Wonderland',
124 | info: {
125 | comment: 'A weird book',
126 | reviewNb: 800
127 | }
128 | },
129 | {
130 | id: 5,
131 | title: 'The Hobbit',
132 | info: {
133 | comment: 'An awesome book',
134 | reviewNb: 900
135 | }
136 | },
137 | {
138 | id: 6,
139 | title: 'Harry Potter and the Half-Blood Prince',
140 | info: {
141 | comment: 'The best book',
142 | reviewNb: 1000
143 | }
144 | },
145 | {
146 | id: 7,
147 | title: 'The Hitchhiker\'s Guide to the Galaxy'
148 | },
149 | {
150 | id: 8,
151 | title: 'Harry Potter and the Deathly Hallows',
152 | info: {
153 | comment: 'The best book again',
154 | reviewNb: 1000
155 | }
156 | }
157 | ]
158 | end
159 |
160 | before { index.add_documents(documents).await }
161 | end
162 |
--------------------------------------------------------------------------------
/spec/meilisearch/index/search/q_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe 'Meilisearch::Index - Basic search' do
4 | include_context 'search books with genre'
5 |
6 | it 'does a basic search in index' do
7 | response = index.search('prince')
8 | expect(response).to be_a(Hash)
9 | expect(response.keys).to include(*DEFAULT_SEARCH_RESPONSE_KEYS)
10 | expect(response['hits']).not_to be_empty
11 | expect(response['hits'].first).not_to have_key('_formatted')
12 | end
13 |
14 | it 'does a basic search with an empty query' do
15 | response = index.search('')
16 | expect(response).to be_a(Hash)
17 | expect(response.keys).to include(*DEFAULT_SEARCH_RESPONSE_KEYS)
18 | expect(response['hits'].count).to eq(documents.count)
19 | end
20 |
21 | it 'does a basic search with a nil query' do
22 | response = index.search(nil)
23 | expect(response).to be_a(Hash)
24 | expect(response.keys).to include(*DEFAULT_SEARCH_RESPONSE_KEYS)
25 | expect(response['hits'].count).to eq(documents.count)
26 | expect(response['hits'].first).not_to have_key('_formatted')
27 | end
28 |
29 | it 'has nbHits to maintain backward compatibility' do
30 | response = index.search('')
31 |
32 | expect(response).to be_a(Hash)
33 | expect(response).to have_key('nbHits')
34 | expect(response['nbHits']).to eq(response['estimatedTotalHits'])
35 | expect(response.keys).to include(*DEFAULT_SEARCH_RESPONSE_KEYS)
36 | expect(response['hits'].count).to eq(documents.count)
37 | expect(response['hits'].first).not_to have_key('_formatted')
38 | end
39 |
40 | it 'does a basic search with an empty query and a custom ranking rule' do
41 | index.update_ranking_rules([
42 | 'words',
43 | 'typo',
44 | 'sort',
45 | 'proximity',
46 | 'attribute',
47 | 'exactness',
48 | 'objectId:asc'
49 | ]).await
50 | response = index.search('')
51 | expect(response['estimatedTotalHits']).to eq(documents.count)
52 | expect(response['hits'].first['objectId']).to eq(1)
53 | end
54 |
55 | it 'does a basic search with an integer query' do
56 | response = index.search(1)
57 | expect(response).to be_a(Hash)
58 | expect(response.keys).to include(*DEFAULT_SEARCH_RESPONSE_KEYS)
59 | expect(response['hits'].count).to eq(3)
60 | expect(response['hits'].first).not_to have_key('_formatted')
61 | end
62 |
63 | it 'does a phrase search' do
64 | response = index.search('coco "harry"')
65 | expect(response).to be_a(Hash)
66 | expect(response.keys).to include(*DEFAULT_SEARCH_RESPONSE_KEYS)
67 | expect(response['hits'].count).to eq(2)
68 | expect(response['hits'].first['objectId']).to eq(4)
69 | expect(response['hits'].first).not_to have_key('_formatted')
70 | end
71 |
72 | context 'with finite pagination params' do
73 | it 'responds with specialized fields' do
74 | response = index.search('coco', { page: 2, hits_per_page: 2 })
75 | expect(response.keys).to include(*FINITE_PAGINATED_SEARCH_RESPONSE_KEYS)
76 |
77 | response = index.search('coco', { page: 2, hitsPerPage: 2 })
78 | expect(response.keys).to include(*FINITE_PAGINATED_SEARCH_RESPONSE_KEYS)
79 | end
80 | end
81 |
82 | context 'with attributes_to_search_on params' do
83 | it 'responds with empty attributes_to_search_on' do
84 | response = index.search('prince', { attributes_to_search_on: [] })
85 | expect(response).to be_a(Hash)
86 | expect(response.keys).to include(*DEFAULT_SEARCH_RESPONSE_KEYS)
87 | expect(response['hits']).to be_empty
88 | end
89 |
90 | it 'responds with nil attributes_to_search_on' do
91 | response = index.search('prince', { attributes_to_search_on: nil })
92 | expect(response).to be_a(Hash)
93 | expect(response.keys).to include(*DEFAULT_SEARCH_RESPONSE_KEYS)
94 | expect(response['hits']).not_to be_empty
95 | end
96 |
97 | it 'responds with title attributes_to_search_on' do
98 | response = index.search('prince', { attributes_to_search_on: ['title'] })
99 | expect(response).to be_a(Hash)
100 | expect(response.keys).to include(*DEFAULT_SEARCH_RESPONSE_KEYS)
101 | expect(response['hits']).not_to be_empty
102 | end
103 |
104 | it 'responds with genre attributes_to_search_on' do
105 | response = index.search('prince', { attributes_to_search_on: ['genry'] })
106 | expect(response).to be_a(Hash)
107 | expect(response.keys).to include(*DEFAULT_SEARCH_RESPONSE_KEYS)
108 | expect(response['hits']).to be_empty
109 | end
110 |
111 | it 'responds with nil attributes_to_search_on and empty query' do
112 | response = index.search('', { attributes_to_search_on: nil })
113 | expect(response).to be_a(Hash)
114 | expect(response.keys).to include(*DEFAULT_SEARCH_RESPONSE_KEYS)
115 | expect(response['hits'].count).to eq(documents.count)
116 | end
117 | end
118 | end
119 |
--------------------------------------------------------------------------------
/lib/meilisearch/http_request.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'httparty'
4 | require 'meilisearch/error'
5 |
6 | module Meilisearch
7 | class HTTPRequest
8 | include HTTParty
9 |
10 | attr_reader :options, :headers
11 |
12 | DEFAULT_OPTIONS = {
13 | timeout: 10,
14 | max_retries: 2,
15 | retry_multiplier: 1.2,
16 | convert_body?: true
17 | }.freeze
18 |
19 | def initialize(url, api_key = nil, options = {})
20 | @base_url = url
21 | @api_key = api_key
22 | @options = DEFAULT_OPTIONS.merge(options)
23 | @headers = build_default_options_headers
24 | end
25 |
26 | def http_get(relative_path = '', query_params = {}, options = {})
27 | send_request(
28 | proc { |path, config| self.class.get(path, config) },
29 | relative_path,
30 | config: {
31 | query_params: query_params,
32 | headers: remove_headers(@headers.dup.merge(options[:headers] || {}), 'Content-Type'),
33 | options: @options.merge(options),
34 | method_type: :get
35 | }
36 | )
37 | end
38 |
39 | def http_post(relative_path = '', body = nil, query_params = nil, options = {})
40 | send_request(
41 | proc { |path, config| self.class.post(path, config) },
42 | relative_path,
43 | config: {
44 | query_params: query_params,
45 | body: body,
46 | headers: @headers.dup.merge(options[:headers] || {}),
47 | options: @options.merge(options),
48 | method_type: :post
49 | }
50 | )
51 | end
52 |
53 | def http_put(relative_path = '', body = nil, query_params = nil, options = {})
54 | send_request(
55 | proc { |path, config| self.class.put(path, config) },
56 | relative_path,
57 | config: {
58 | query_params: query_params,
59 | body: body,
60 | headers: @headers.dup.merge(options[:headers] || {}),
61 | options: @options.merge(options),
62 | method_type: :put
63 | }
64 | )
65 | end
66 |
67 | def http_patch(relative_path = '', body = nil, query_params = nil, options = {})
68 | send_request(
69 | proc { |path, config| self.class.patch(path, config) },
70 | relative_path,
71 | config: {
72 | query_params: query_params,
73 | body: body,
74 | headers: @headers.dup.merge(options[:headers] || {}),
75 | options: @options.merge(options),
76 | method_type: :patch
77 | }
78 | )
79 | end
80 |
81 | def http_delete(relative_path = '', query_params = nil, options = {})
82 | send_request(
83 | proc { |path, config| self.class.delete(path, config) },
84 | relative_path,
85 | config: {
86 | query_params: query_params,
87 | headers: remove_headers(@headers.dup.merge(options[:headers] || {}), 'Content-Type'),
88 | options: @options.merge(options),
89 | method_type: :delete
90 | }
91 | )
92 | end
93 |
94 | private
95 |
96 | def build_default_options_headers
97 | {
98 | 'Content-Type' => 'application/json',
99 | 'Authorization' => ("Bearer #{@api_key}" unless @api_key.nil?),
100 | 'User-Agent' => [
101 | @options.fetch(:client_agents, []),
102 | Meilisearch.qualified_version
103 | ].flatten.join(';')
104 | }.compact
105 | end
106 |
107 | def remove_headers(data, *keys)
108 | data.delete_if { |k| keys.include?(k) }
109 | end
110 |
111 | def send_request(http_method, relative_path, config:)
112 | attempts = 0
113 | retry_multiplier = config.dig(:options, :retry_multiplier)
114 | max_retries = config.dig(:options, :max_retries)
115 | request_config = http_config(config[:query_params], config[:body], config[:options], config[:headers])
116 |
117 | begin
118 | response = http_method.call(@base_url + relative_path, request_config)
119 | rescue Errno::ECONNREFUSED, Errno::EPIPE => e
120 | raise CommunicationError, e.message
121 | rescue URI::InvalidURIError => e
122 | raise CommunicationError, "Client URL missing scheme/protocol. Did you mean https://#{@base_url}" unless @base_url =~ %r{^\w+://}
123 |
124 | raise CommunicationError, e
125 | rescue Net::OpenTimeout, Net::ReadTimeout => e
126 | attempts += 1
127 | raise TimeoutError, e.message unless attempts <= max_retries && safe_to_retry?(config[:method_type], e)
128 |
129 | sleep(retry_multiplier**attempts)
130 |
131 | retry
132 | end
133 |
134 | validate(response)
135 | end
136 |
137 | def http_config(query_params, body, options, headers)
138 | body = body.to_json if options[:convert_body?] == true
139 | {
140 | headers: headers,
141 | query: query_params,
142 | body: body,
143 | timeout: options[:timeout],
144 | max_retries: options[:max_retries]
145 | }.compact
146 | end
147 |
148 | def validate(response)
149 | raise ApiError.new(response.code, response.message, response.body) unless response.success?
150 |
151 | response.parsed_response
152 | end
153 |
154 | # Ensures the only retryable error is a timeout didn't reached the server
155 | def safe_to_retry?(method_type, error)
156 | method_type == :get || ([:post, :put, :patch, :delete].include?(method_type) && error.is_a?(Net::OpenTimeout))
157 | end
158 | end
159 | end
160 |
--------------------------------------------------------------------------------
/spec/meilisearch/client/token_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'jwt'
4 |
5 | VERIFY_OPTIONS = {
6 | required_claims: ['exp', 'apiKeyUid', 'searchRules'],
7 | algorithm: 'HS256'
8 | }.freeze
9 |
10 | RSpec.describe Meilisearch::TenantToken do
11 | let(:instance) { dummy_class.new(client_key) }
12 | let(:dummy_class) do
13 | Class.new do
14 | include Meilisearch::TenantToken
15 |
16 | def initialize(api_key)
17 | @api_key = api_key
18 | end
19 | end
20 | end
21 |
22 | let(:search_rules) { {} }
23 | let(:api_key) { SecureRandom.hex(24) }
24 | let(:client_key) { SecureRandom.hex(24) }
25 | let(:expires_at) { Time.now.utc + 10_000 }
26 |
27 | it 'responds to #generate_tenant_token' do
28 | expect(instance).to respond_to(:generate_tenant_token)
29 | end
30 |
31 | describe '#generate_tenant_token' do
32 | subject(:token) do
33 | instance.generate_tenant_token('uid', search_rules, api_key: api_key, expires_at: expires_at)
34 | end
35 |
36 | context 'with api_key param' do
37 | it 'decodes successfully using api_key from param' do
38 | expect do
39 | JWT.decode token, api_key, true, VERIFY_OPTIONS
40 | end.to_not raise_error
41 | end
42 |
43 | it 'tries to decode without the right signature raises a error' do
44 | expect do
45 | JWT.decode token, client_key, true, VERIFY_OPTIONS
46 | end.to raise_error(JWT::DecodeError)
47 | end
48 | end
49 |
50 | context 'without api_key param' do
51 | let(:api_key) { nil }
52 |
53 | it 'decodes successfully using @api_key from instance' do
54 | expect do
55 | JWT.decode token, client_key, true, VERIFY_OPTIONS
56 | end.not_to raise_error
57 | end
58 |
59 | it 'tries to decode without the right signature raises a error' do
60 | expect do
61 | JWT.decode token, api_key, true, VERIFY_OPTIONS
62 | end.to raise_error(JWT::DecodeError)
63 | end
64 |
65 | it 'raises error when both api_key are nil' do
66 | client = dummy_class.new(nil)
67 |
68 | expect do
69 | client.generate_tenant_token('uid', search_rules)
70 | end.to raise_error(described_class::InvalidApiKey)
71 | end
72 |
73 | it 'raises error when both api_key are empty' do
74 | client = dummy_class.new('')
75 |
76 | expect do
77 | client.generate_tenant_token('uid', search_rules, api_key: '')
78 | end.to raise_error(described_class::InvalidApiKey)
79 | end
80 | end
81 |
82 | context 'with expires_at' do
83 | it 'raises error when expires_at is in the past' do
84 | expect do
85 | instance.generate_tenant_token('uid', search_rules, expires_at: Time.now.utc - 10)
86 | end.to raise_error(described_class::ExpireOrInvalidSignature)
87 | end
88 |
89 | it 'allows generate token with a nil expires_at' do
90 | expect do
91 | instance.generate_tenant_token('uid', search_rules, expires_at: nil)
92 | end.not_to raise_error
93 | end
94 |
95 | it 'decodes successfully the expires_at param' do
96 | decoded = JWT.decode token, api_key, false
97 |
98 | expect(decoded.dig(0, 'exp')).to eq(expires_at.to_i)
99 | end
100 |
101 | it 'raises error when expires_at has a invalid type' do
102 | ['2042-01-01', 78_126_717_684, []].each do |exp|
103 | expect do
104 | instance.generate_tenant_token('uid', search_rules, expires_at: exp)
105 | end.to raise_error(described_class::ExpireOrInvalidSignature)
106 | end
107 | end
108 |
109 | it 'raises error when expires_at is not a UTC' do
110 | expect do
111 | instance.generate_tenant_token('uid', search_rules, expires_at: Time.now + 10)
112 | end.to raise_error(described_class::ExpireOrInvalidSignature)
113 | end
114 | end
115 |
116 | context 'without expires_at param' do
117 | it 'allows generate token without expires_at' do
118 | expect do
119 | instance.generate_tenant_token('uid', search_rules)
120 | end.not_to raise_error
121 | end
122 | end
123 |
124 | context 'with search_rules definitions' do
125 | include_context 'search books with genre'
126 |
127 | before { index.update_filterable_attributes(['genre', 'objectId']).await }
128 |
129 | let(:adm_client) { Meilisearch::Client.new(URL, adm_key['key']) }
130 | let(:adm_key) do
131 | client.create_key(
132 | description: 'tenants test',
133 | actions: ['*'],
134 | indexes: ['*'],
135 | expires_at: '2042-04-02T00:42:42Z'
136 | )
137 | end
138 | let(:rules) do
139 | [
140 | { '*': {} },
141 | { '*': nil },
142 | ['*'],
143 | { '*': { filter: 'genre = comedy' } },
144 | { books: {} },
145 | { books: nil },
146 | ['books'],
147 | { books: { filter: 'genre = comedy AND objectId = 1' } }
148 | ]
149 | end
150 |
151 | it 'accepts the token in the search request' do
152 | rules.each do |data|
153 | token = adm_client.generate_tenant_token(adm_key['uid'], data)
154 | custom = Meilisearch::Client.new(URL, token)
155 |
156 | expect(custom.index('books').search('')).to have_key('hits')
157 | end
158 | end
159 |
160 | it 'requires a non-nil payload in the search_rules' do
161 | expect do
162 | client.generate_tenant_token('uid', nil)
163 | end.to raise_error(described_class::InvalidSearchRules)
164 | end
165 | end
166 |
167 | it 'has apiKeyUid with the uid of the key' do
168 | decoded = JWT.decode(token, api_key, true, VERIFY_OPTIONS).dig(0, 'apiKeyUid')
169 |
170 | expect(decoded).to eq('uid')
171 | end
172 | end
173 | end
174 |
--------------------------------------------------------------------------------
/spec/meilisearch/utils_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe Meilisearch::Utils do
4 | let(:logger) { instance_double(Logger, warn: nil) }
5 |
6 | describe '.soft_deprecate' do
7 | before { described_class.logger = logger }
8 | after { described_class.logger = nil }
9 |
10 | it 'outputs a warning' do
11 | described_class.soft_deprecate('footballs', 'snowballs')
12 | expect(logger).to have_received(:warn)
13 | end
14 |
15 | it 'does not throw an error' do
16 | expect do
17 | described_class.soft_deprecate('footballs', 'snowballs')
18 | end.not_to raise_error
19 | end
20 |
21 | it 'includes relevant information' do
22 | described_class.soft_deprecate('footballs', 'snowballs')
23 | expect(logger).to have_received(:warn).with(a_string_including('footballs', 'snowballs'))
24 | end
25 | end
26 |
27 | describe '.parse_query' do
28 | it 'transforms arrays into strings' do
29 | data = described_class.parse_query({ array: [1, 2, 3], other: 'string' }, [:array, :other])
30 |
31 | expect(data).to eq({ 'array' => '1,2,3', 'other' => 'string' })
32 | end
33 |
34 | it 'cleans list based on another list' do
35 | data = described_class.parse_query({ array: [1, 2, 3], ignore: 'string' }, [:array])
36 |
37 | expect(data).to eq({ 'array' => '1,2,3' })
38 | end
39 |
40 | it 'transforms dates into strings' do
41 | data = described_class.parse_query({ date: DateTime.new(2012, 12, 21, 19, 5) }, [:date])
42 |
43 | expect(data).to eq({ 'date' => '2012-12-21T19:05:00+00:00' })
44 | end
45 | end
46 |
47 | describe '.transform_attributes' do
48 | before { described_class.logger = logger }
49 | after { described_class.logger = nil }
50 |
51 | it 'transforms snake_case into camelCased keys' do
52 | data = described_class.transform_attributes({
53 | index_name: 'books',
54 | my_UID: '123'
55 | })
56 |
57 | expect(data).to eq({ 'indexName' => 'books', 'myUid' => '123' })
58 | end
59 |
60 | it 'transforms snake_case into camel cased keys from array' do
61 | data = described_class
62 | .transform_attributes([
63 | { index_uid: 'books', q: 'prince' },
64 | { index_uid: 'movies', q: 'prince' }
65 | ])
66 |
67 | expect(data).to eq(
68 | [
69 | { 'indexUid' => 'books', 'q' => 'prince' },
70 | { 'indexUid' => 'movies', 'q' => 'prince' }
71 | ]
72 | )
73 | end
74 |
75 | it 'warns when using camelCase' do
76 | attrs = { distinctAttribute: 'title' }
77 |
78 | described_class.transform_attributes(attrs)
79 |
80 | expect(logger).to have_received(:warn)
81 | .with(a_string_including('Attributes will be expected to be snake_case', 'distinctAttribute'))
82 | end
83 |
84 | it 'warns when using camelCase in an array' do
85 | attrs = [
86 | { 'index_uid' => 'movies', 'q' => 'prince' },
87 | { 'indexUid' => 'books', 'q' => 'prince' }
88 | ]
89 |
90 | described_class.transform_attributes(attrs)
91 |
92 | expect(logger).to have_received(:warn)
93 | .with(a_string_including('Attributes will be expected to be snake_case', 'indexUid'))
94 | end
95 | end
96 |
97 | describe '.version_error_handler' do
98 | let(:http_body) do
99 | { 'message' => 'Was expecting an operation',
100 | 'code' => 'invalid_document_filter',
101 | 'type' => 'invalid_request',
102 | 'link' => 'https://docs.meilisearch.com/errors#invalid_document_filter' }
103 | end
104 |
105 | it 'spawns same error message' do
106 | expect do
107 | described_class.version_error_handler(:my_method) do
108 | raise Meilisearch::ApiError.new(405, 'I came from Meili server', http_body)
109 | end
110 | end.to raise_error(Meilisearch::ApiError, /I came from Meili server/)
111 | end
112 |
113 | it 'spawns same error message with html body' do
114 | expect do
115 | described_class.version_error_handler(:my_method) do
116 | raise Meilisearch::ApiError.new(405, 'I came from Meili server', '405 Error
')
117 | end
118 | end.to raise_error(Meilisearch::ApiError, /I came from Meili server/)
119 | end
120 |
121 | it 'spawns same error message with no body' do
122 | expect do
123 | described_class.version_error_handler(:my_method) do
124 | raise Meilisearch::ApiError.new(405, 'I came from Meili server', nil)
125 | end
126 | end.to raise_error(Meilisearch::ApiError, /I came from Meili server/)
127 | end
128 |
129 | it 'spawns message with version hint' do
130 | expect do
131 | described_class.version_error_handler(:my_method) do
132 | raise Meilisearch::ApiError.new(405, 'I came from Meili server', http_body)
133 | end
134 | end.to raise_error(Meilisearch::ApiError, /that `my_method` call requires/)
135 | end
136 |
137 | it 'adds hints to all error types' do
138 | expect do
139 | described_class.version_error_handler(:my_method) do
140 | raise Meilisearch::CommunicationError, 'I am an error'
141 | end
142 | end.to raise_error(Meilisearch::CommunicationError, /that `my_method` call requires/)
143 | end
144 |
145 | describe '.warn_on_non_conforming_attribute_names' do
146 | before { described_class.logger = logger }
147 | after { described_class.logger = nil }
148 |
149 | it 'warns when using camelCase attributes' do
150 | attrs = { attributesToHighlight: ['field'] }
151 | described_class.warn_on_non_conforming_attribute_names(attrs)
152 |
153 | expect(logger).to have_received(:warn)
154 | .with(a_string_including('Attributes will be expected to be snake_case', 'attributesToHighlight'))
155 | end
156 |
157 | it 'warns when using a mixed case' do
158 | attrs = { distinct_ATTribute: 'title' }
159 | described_class.warn_on_non_conforming_attribute_names(attrs)
160 |
161 | expect(logger).to have_received(:warn)
162 | .with(a_string_including('Attributes will be expected to be snake_case', 'distinct_ATTribute'))
163 | end
164 |
165 | it 'does not warn when using snake_case' do
166 | attrs = { q: 'query', attributes_to_highlight: ['field'] }
167 | described_class.warn_on_non_conforming_attribute_names(attrs)
168 |
169 | expect(logger).not_to have_received(:warn)
170 | end
171 | end
172 | end
173 | end
174 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | First of all, thank you for contributing to Meilisearch! The goal of this document is to provide everything you need to know in order to contribute to Meilisearch and its different integrations.
4 |
5 |
6 |
7 | - [Assumptions](#assumptions)
8 | - [How to Contribute](#how-to-contribute)
9 | - [Development Workflow](#development-workflow)
10 | - [Git Guidelines](#git-guidelines)
11 | - [Release Process (for internal team only)](#release-process-for-internal-team-only)
12 |
13 |
14 |
15 | ## Assumptions
16 |
17 | 1. **You're familiar with [GitHub](https://github.com) and the [Pull Request](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests)(PR) workflow.**
18 | 2. **You've read the Meilisearch [documentation](https://docs.meilisearch.com) and the [README](/README.md).**
19 | 3. **You know about the [Meilisearch community](https://discord.meilisearch.com). Please use this for help.**
20 |
21 | ## How to Contribute
22 |
23 | 1. Make sure that the contribution you want to make is explained or detailed in a GitHub issue! Find an [existing issue](https://github.com/meilisearch/meilisearch-ruby/issues/) or [open a new one](https://github.com/meilisearch/meilisearch-ruby/issues/new).
24 | 2. Once done, [fork the meilisearch-ruby repository](https://help.github.com/en/github/getting-started-with-github/fork-a-repo) in your own GitHub account. Ask a maintainer if you want your issue to be checked before making a PR.
25 | 3. [Create a new Git branch](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-and-deleting-branches-within-your-repository).
26 | 4. Review the [Development Workflow](#development-workflow) section that describes the steps to maintain the repository.
27 | 5. Make the changes on your branch.
28 | 6. [Submit the branch as a PR](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request-from-a-fork) pointing to the `main` branch of the main meilisearch-ruby repository. A maintainer should comment and/or review your Pull Request within a few days. Although depending on the circumstances, it may take longer.
29 | We do not enforce a naming convention for the PRs, but **please use something descriptive of your changes**, having in mind that the title of your PR will be automatically added to the next [release changelog](https://github.com/meilisearch/meilisearch-ruby/releases/).
30 |
31 | ## Development Workflow
32 |
33 | ### Setup
34 |
35 | You can set up your local environment natively or using `docker`, check out the [`docker-compose.yml`](/docker-compose.yml).
36 |
37 | Example of running all the checks with docker:
38 | ```bash
39 | docker-compose run --rm package bash -c "bundle install && bundle exec rspec && bundle exec rubocop"
40 | ```
41 |
42 | To install dependencies:
43 | ```bash
44 | bundle install
45 | ```
46 |
47 | ### Tests
48 |
49 | Each PR should pass the tests to be accepted.
50 |
51 | ```bash
52 | # Tests
53 | curl -L https://install.meilisearch.com | sh # download Meilisearch
54 | ./meilisearch --master-key=masterKey --no-analytics # run Meilisearch
55 | bundle exec rspec
56 | ```
57 |
58 | To launch a specific folder or file:
59 |
60 | ```bash
61 | bundle exec rspec spec/meilisearch/index/base_spec.rb
62 | ```
63 |
64 | To launch a single test in a specific file:
65 |
66 | ```bash
67 | bundle exec rspec spec/meilisearch/index/search_spec.rb -e 'does a basic search in index'
68 | ```
69 |
70 | ### Linter
71 |
72 | Each PR should pass the linter to be accepted.
73 |
74 | ```bash
75 | # Check the linter errors
76 | bundle exec rubocop lib/ spec/
77 | # Auto-correct the linter errors
78 | bundle exec rubocop -a lib/ spec/
79 | ```
80 |
81 | If you think the remaining linter errors are acceptable, do not add any `rubocop` in-line comments in the code.
82 | This project uses a `rubocop_todo.yml` file that is generated. Do not modify this file manually.
83 | To update it, run the following command:
84 |
85 | ```bash
86 | bundle exec rubocop --auto-gen-config
87 | ```
88 |
89 | ### Want to debug?
90 |
91 | You can use the [`byebug` gem](https://github.com/deivid-rodriguez/byebug).
92 |
93 | To create a breakpoint, just add this line in you code:
94 |
95 | ```ruby
96 | ...
97 | byebug
98 | ...
99 | ```
100 |
101 | The `byebug` gem is already imported in all the spec files.
102 | But if you want to use it in the source files you need to add this line at the top of the file:
103 |
104 | ```ruby
105 | require 'byebug'
106 | ```
107 |
108 | ## Git Guidelines
109 |
110 | ### Git Branches
111 |
112 | All changes must be made in a branch and submitted as PR.
113 | We do not enforce any branch naming style, but please use something descriptive of your changes.
114 |
115 | ### Git Commits
116 |
117 | As minimal requirements, your commit message should:
118 | - be capitalized
119 | - not finish by a dot or any other punctuation character (!,?)
120 | - start with a verb so that we can read your commit message this way: "This commit will ...", where "..." is the commit message.
121 | e.g.: "Fix the home page button" or "Add more tests for create_index method"
122 |
123 | We don't follow any other convention, but if you want to use one, we recommend [this one](https://chris.beams.io/posts/git-commit/).
124 |
125 | ### GitHub Pull Requests
126 |
127 | Some notes on GitHub PRs:
128 |
129 | - [Convert your PR as a draft](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/changing-the-stage-of-a-pull-request) if your changes are a work in progress: no one will review it until you pass your PR as ready for review.
130 | The draft PR can be very useful if you want to show that you are working on something and make your work visible.
131 | - All PRs must be reviewed and approved by at least one maintainer.
132 | - The PR title should be accurate and descriptive of the changes. The title of the PR will be indeed automatically added to the next [release changelogs](https://github.com/meilisearch/meilisearch-ruby/releases/).
133 |
134 | ## Release Process (for the internal team only)
135 |
136 | Meilisearch tools follow the [Semantic Versioning Convention](https://semver.org/).
137 |
138 | ### Automated Changelogs
139 |
140 | This project integrates a tool to create automated changelogs.
141 | _[Read more about this](https://github.com/meilisearch/integration-guides/blob/main/resources/release-drafter.md)._
142 |
143 | ### How to Publish the Release
144 |
145 | ⚠️ Before doing anything, make sure you got through the guide about [Releasing an Integration](https://github.com/meilisearch/integration-guides/blob/main/resources/integration-release.md).
146 |
147 | Make a PR modifying the file [`lib/meilisearch/version.rb`](/lib/meilisearch/version.rb) with the right version.
148 |
149 | ```ruby
150 | VERSION = 'X.X.X'
151 | ```
152 |
153 | Once the changes are merged on `main`, you can publish the current draft release via the [GitHub interface](https://github.com/meilisearch/meilisearch-ruby/releases): on this page, click on `Edit` (related to the draft release) > update the description (be sure you apply [these recommendations](https://github.com/meilisearch/integration-guides/blob/main/resources/integration-release.md#writting-the-release-description)) > when you are ready, click on `Publish release`.
154 |
155 | GitHub Actions will be triggered and push the package to [RubyGems](https://rubygems.org/gems/meilisearch).
156 |
157 |
158 |
159 | Thank you again for reading this through. We can not wait to begin to work with you if you make your way through this contributing guide ❤️
160 |
--------------------------------------------------------------------------------
/spec/meilisearch/index/base_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe Meilisearch::Index do
4 | it 'fetch the info of the index' do
5 | client.create_index('books').await
6 |
7 | index = client.fetch_index('books')
8 | expect(index).to be_a(described_class)
9 | expect(index.uid).to eq('books')
10 | expect(index.created_at).to be_a(Time)
11 | expect(index.created_at).to be_within(60).of(Time.now)
12 | expect(index.updated_at).to be_a(Time)
13 | expect(index.created_at).to be_within(60).of(Time.now)
14 | expect(index.primary_key).to be_nil
15 | end
16 |
17 | it 'fetch the raw Hash info of the index' do
18 | client.create_index('books', primary_key: 'reference_number').await
19 |
20 | raw_index = client.fetch_raw_index('books')
21 |
22 | expect(raw_index).to be_a(Hash)
23 | expect(raw_index['uid']).to eq('books')
24 | expect(raw_index['primaryKey']).to eq('reference_number')
25 | expect(Time.parse(raw_index['createdAt'])).to be_a(Time)
26 | expect(Time.parse(raw_index['createdAt'])).to be_within(60).of(Time.now)
27 | expect(Time.parse(raw_index['updatedAt'])).to be_a(Time)
28 | expect(Time.parse(raw_index['updatedAt'])).to be_within(60).of(Time.now)
29 | end
30 |
31 | it 'get primary-key of index if null' do
32 | client.create_index('index_without_primary_key').await
33 |
34 | index = client.fetch_index('index_without_primary_key')
35 | expect(index.primary_key).to be_nil
36 | expect(index.fetch_primary_key).to be_nil
37 | end
38 |
39 | it 'get primary-key of index if it exists' do
40 | client.create_index('index_with_prirmary_key', primary_key: 'primary_key').await
41 |
42 | index = client.fetch_index('index_with_prirmary_key')
43 | expect(index.primary_key).to eq('primary_key')
44 | expect(index.fetch_primary_key).to eq('primary_key')
45 | end
46 |
47 | it 'get uid of index' do
48 | client.create_index('uid').await
49 |
50 | index = client.fetch_index('uid')
51 | expect(index.uid).to eq('uid')
52 | end
53 |
54 | it 'updates primary-key of index if not defined before' do
55 | client.create_index('uid').await
56 |
57 | task = client.index('uid').update(primary_key: 'new_primary_key')
58 | expect(task.type).to eq('indexUpdate')
59 | task.await
60 |
61 | index = client.fetch_index('uid')
62 | expect(index).to be_a(described_class)
63 | expect(index.uid).to eq('uid')
64 | expect(index.primary_key).to eq('new_primary_key')
65 | expect(index.fetch_primary_key).to eq('new_primary_key')
66 | expect(index.created_at).to be_a(Time)
67 | expect(index.created_at).to be_within(60).of(Time.now)
68 | expect(index.updated_at).to be_a(Time)
69 | expect(index.updated_at).to be_within(60).of(Time.now)
70 | end
71 |
72 | it 'updates primary-key of index if has been defined before but there is not docs' do
73 | client.create_index('books', primary_key: 'reference_number').await
74 |
75 | task = client.index('books').update(primary_key: 'international_standard_book_number')
76 | expect(task.type).to eq('indexUpdate')
77 | task.await
78 |
79 | index = client.fetch_index('books')
80 | expect(index).to be_a(described_class)
81 | expect(index.uid).to eq('books')
82 | expect(index.primary_key).to eq('international_standard_book_number')
83 | expect(index.fetch_primary_key).to eq('international_standard_book_number')
84 | expect(index.created_at).to be_a(Time)
85 | expect(index.created_at).to be_within(60).of(Time.now)
86 | expect(index.updated_at).to be_a(Time)
87 | expect(index.updated_at).to be_within(60).of(Time.now)
88 | end
89 |
90 | it 'returns a failing task if primary-key is already defined' do
91 | index = client.index('uid')
92 | index.add_documents({ id: 1, title: 'My Title' }).await
93 |
94 | task = index.update(primary_key: 'new_primary_key')
95 | expect(task.type).to eq('indexUpdate')
96 |
97 | task.await
98 | expect(task).to be_failed
99 | expect(task.error['code']).to eq('index_primary_key_already_exists')
100 | end
101 |
102 | it 'supports options' do
103 | options = { timeout: 2, retry_multiplier: 1.2, max_retries: 1 }
104 | expected_headers = {
105 | 'Authorization' => "Bearer #{MASTER_KEY}",
106 | 'User-Agent' => Meilisearch.qualified_version
107 | }
108 |
109 | new_client = Meilisearch::Client.new(URL, MASTER_KEY, options)
110 | new_client.create_index('books').await
111 | index = new_client.fetch_index('books')
112 | expect(index.options).to eq({ max_retries: 1, retry_multiplier: 1.2, timeout: 2, convert_body?: true })
113 |
114 | expect(described_class).to receive(:get).with(
115 | "#{URL}/indexes/books",
116 | {
117 | headers: expected_headers,
118 | body: 'null',
119 | query: {},
120 | max_retries: 1,
121 | timeout: 2
122 | }
123 | ).and_return(double(success?: true,
124 | parsed_response: { 'createdAt' => '2021-10-16T14:57:35Z',
125 | 'updatedAt' => '2021-10-16T14:57:35Z' }))
126 | index.fetch_info
127 | end
128 |
129 | it 'supports client_agents' do
130 | custom_agent = 'Meilisearch Rails (v0.0.1)'
131 | options = { timeout: 2, retry_multiplier: 1.2, max_retries: 1, client_agents: [custom_agent] }
132 | expected_headers = {
133 | 'Authorization' => "Bearer #{MASTER_KEY}",
134 | 'User-Agent' => "#{custom_agent};#{Meilisearch.qualified_version}"
135 | }
136 |
137 | new_client = Meilisearch::Client.new(URL, MASTER_KEY, options)
138 | new_client.create_index('books').await
139 | index = new_client.fetch_index('books')
140 | expect(index.options).to eq(options.merge({ convert_body?: true }))
141 |
142 | expect(described_class).to receive(:get).with(
143 | "#{URL}/indexes/books",
144 | {
145 | headers: expected_headers,
146 | body: 'null',
147 | query: {},
148 | max_retries: 1,
149 | timeout: 2
150 | }
151 | ).and_return(double(success?: true,
152 | parsed_response: { 'createdAt' => '2021-10-16T14:57:35Z',
153 | 'updatedAt' => '2021-10-16T14:57:35Z' }))
154 | index.fetch_info
155 | end
156 |
157 | it 'deletes index' do
158 | client.create_index('uid').await
159 |
160 | task = client.index('uid').delete
161 | expect(task.type).to eq('indexDeletion')
162 | task.await
163 | expect(task).to be_succeeded
164 | expect { client.fetch_index('uid') }.to raise_index_not_found_meilisearch_api_error
165 | end
166 |
167 | it 'fails to manipulate index object after deletion' do
168 | client.create_index('uid').await
169 |
170 | task = client.index('uid').delete
171 | expect(task.type).to eq('indexDeletion')
172 | task.await
173 |
174 | index = client.index('uid')
175 | expect { index.fetch_primary_key }.to raise_index_not_found_meilisearch_api_error
176 | expect { index.fetch_info }.to raise_index_not_found_meilisearch_api_error
177 | end
178 |
179 | it 'works with method aliases' do
180 | client.create_index('uid', primary_key: 'primary_key').await
181 |
182 | index = client.fetch_index('uid')
183 | expect(index.method(:fetch_primary_key) == index.method(:get_primary_key)).to be_truthy
184 | expect(index.method(:update) == index.method(:update_index)).to be_truthy
185 | expect(index.method(:delete) == index.method(:delete_index)).to be_truthy
186 | end
187 |
188 | context 'with snake_case options' do
189 | it 'does the request with camelCase attributes' do
190 | client.create_index('uid').await
191 |
192 | task = client.index('uid').update(primary_key: 'new_primary_key')
193 | expect(task.type).to eq('indexUpdate')
194 | task.await
195 |
196 | index = client.fetch_index('uid')
197 | expect(index).to be_a(described_class)
198 | expect(index.uid).to eq('uid')
199 | expect(index.primary_key).to eq('new_primary_key')
200 | expect(index.fetch_primary_key).to eq('new_primary_key')
201 | end
202 | end
203 | end
204 |
--------------------------------------------------------------------------------
/spec/meilisearch/client/tasks_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe 'Meilisearch::Tasks' do
4 | include_context 'search books with genre'
5 |
6 | let(:enqueued_task_keys) { ['uid', 'indexUid', 'status', 'type', 'enqueuedAt'] }
7 | let(:succeeded_task_keys) { [*enqueued_task_keys, 'duration', 'startedAt', 'finishedAt'] }
8 | let!(:doc_addition_task) { index.add_documents(documents).await }
9 | let(:task_uid) { doc_addition_task['uid'] }
10 |
11 | it 'gets a task of an index' do
12 | task = index.task(task_uid)
13 |
14 | expect(task.keys).to include(*succeeded_task_keys)
15 | end
16 |
17 | it 'gets all the tasks of an index' do
18 | tasks = index.tasks
19 |
20 | expect(tasks['results']).to be_a(Array)
21 | expect(tasks['total']).to be > 0
22 |
23 | last_task = tasks['results'].first
24 |
25 | expect(last_task.keys).to include(*succeeded_task_keys)
26 | end
27 |
28 | it 'allows for returning tasks in reverse' do
29 | tasks = client.tasks
30 | rev_tasks = client.tasks(reverse: true)
31 |
32 | expect(tasks['results']).not_to eq(rev_tasks['results'])
33 | end
34 |
35 | it 'gets a task of the Meilisearch instance' do
36 | task = client.task(0)
37 |
38 | expect(task).to be_a(Hash)
39 | expect(task['uid']).to eq(0)
40 | expect(task.keys).to include(*succeeded_task_keys)
41 | end
42 |
43 | it 'gets tasks of the Meilisearch instance' do
44 | tasks = client.tasks
45 |
46 | expect(tasks['results']).to be_a(Array)
47 | expect(tasks['total']).to be > 0
48 |
49 | last_task = tasks['results'].first
50 |
51 | expect(last_task.keys).to include(*succeeded_task_keys)
52 | end
53 |
54 | it 'paginates tasks with limit/from/next' do
55 | tasks = client.tasks(limit: 2)
56 |
57 | expect(tasks['results'].count).to be <= 2
58 | expect(tasks['from']).to be_a(Integer)
59 | expect(tasks['next']).to be_a(Integer)
60 | expect(tasks['total']).to be > 0
61 | end
62 |
63 | it 'filters tasks with index_uids/types/statuses' do
64 | tasks = client.tasks(index_uids: ['a-cool-index-name'])
65 |
66 | expect(tasks['results'].count).to eq(0)
67 | expect(tasks['total']).to eq(0)
68 |
69 | tasks = client.tasks(index_uids: ['books'], types: ['documentAdditionOrUpdate'], statuses: ['succeeded'])
70 |
71 | expect(tasks['results'].count).to be > 1
72 | expect(tasks['total']).to be > 1
73 | end
74 |
75 | it 'ensures supports to all available filters' do
76 | allow(Meilisearch::Utils).to receive(:transform_attributes).and_call_original
77 |
78 | client.tasks(
79 | canceled_by: [1, 2], uids: [2], foo: 'bar',
80 | before_enqueued_at: '2022-01-20', after_enqueued_at: '2022-01-20',
81 | before_started_at: '2022-01-20', after_started_at: '2022-01-20',
82 | before_finished_at: '2022-01-20', after_finished_at: '2022-01-20'
83 | )
84 |
85 | expect(Meilisearch::Utils).to have_received(:transform_attributes)
86 | .with(
87 | canceled_by: [1, 2], uids: [2],
88 | before_enqueued_at: '2022-01-20', after_enqueued_at: '2022-01-20',
89 | before_started_at: '2022-01-20', after_started_at: '2022-01-20',
90 | before_finished_at: '2022-01-20', after_finished_at: '2022-01-20'
91 | )
92 | end
93 |
94 | describe '#index.wait_for_task' do
95 | it 'waits for task with default values' do
96 | task = index.add_documents(documents)
97 | task = index.wait_for_task(task['taskUid'])
98 |
99 | expect(task).to be_a(Hash)
100 | expect(task['status']).not_to eq('enqueued')
101 | end
102 |
103 | it 'waits for task with default values after several updates' do
104 | 5.times { index.add_documents(documents) }
105 | task = index.add_documents(documents)
106 | status = index.wait_for_task(task['taskUid'])
107 |
108 | expect(status).to be_a(Hash)
109 | expect(status['status']).not_to eq('enqueued')
110 | end
111 |
112 | it 'waits for task with custom timeout_in_ms and raises MeilisearchTimeoutError' do
113 | index.add_documents(documents)
114 | task = index.add_documents(documents)
115 | expect do
116 | index.wait_for_task(task['taskUid'], 1)
117 | end.to raise_error(Meilisearch::TimeoutError)
118 | end
119 |
120 | it 'waits for task with custom interval_in_ms and raises Timeout::Error' do
121 | index.add_documents(documents)
122 | task = index.add_documents(documents)
123 | expect do
124 | Timeout.timeout(0.1) do
125 | index.wait_for_task(task['taskUid'], 5000, 200)
126 | end
127 | end.to raise_error(Timeout::Error)
128 | end
129 | end
130 |
131 | describe '#client.wait_for_task' do
132 | it 'waits for task with default values' do
133 | task = index.add_documents(documents).await
134 | task = client.wait_for_task(task['taskUid'])
135 |
136 | expect(task).to be_a(Hash)
137 | expect(task['status']).not_to eq('enqueued')
138 | end
139 |
140 | it 'waits for task with default values after several updates' do
141 | 5.times { index.add_documents(documents) }
142 | task = index.add_documents(documents)
143 | status = client.wait_for_task(task['taskUid'])
144 |
145 | expect(status).to be_a(Hash)
146 | expect(status['status']).not_to eq('enqueued')
147 | end
148 |
149 | it 'waits for task with custom timeout_in_ms and raises MeilisearchTimeoutError' do
150 | index.add_documents(documents)
151 | task = index.add_documents(documents)
152 | expect do
153 | client.wait_for_task(task['taskUid'], 1)
154 | end.to raise_error(Meilisearch::TimeoutError)
155 | end
156 |
157 | it 'waits for task with custom interval_in_ms and raises Timeout::Error' do
158 | index.add_documents(documents)
159 | task = index.add_documents(documents)
160 | expect do
161 | Timeout.timeout(0.1) do
162 | client.wait_for_task(task['taskUid'], 5000, 200)
163 | end
164 | end.to raise_error(Timeout::Error)
165 | end
166 | end
167 |
168 | describe '#client.cancel_tasks' do
169 | it 'ensures supports to all available filters' do
170 | allow(Meilisearch::Utils).to receive(:transform_attributes).and_call_original
171 |
172 | client.cancel_tasks(
173 | canceled_by: [1, 2], uids: [2], foo: 'bar',
174 | before_enqueued_at: '2022-01-20', after_enqueued_at: '2022-01-20',
175 | before_started_at: '2022-01-20', after_started_at: '2022-01-20',
176 | before_finished_at: '2022-01-20', after_finished_at: '2022-01-20'
177 | )
178 |
179 | expect(Meilisearch::Utils).to have_received(:transform_attributes)
180 | .with(
181 | canceled_by: [1, 2], uids: [2],
182 | before_enqueued_at: '2022-01-20', after_enqueued_at: '2022-01-20',
183 | before_started_at: '2022-01-20', after_started_at: '2022-01-20',
184 | before_finished_at: '2022-01-20', after_finished_at: '2022-01-20'
185 | )
186 | end
187 |
188 | it 'has fields in the details field' do
189 | task = client.cancel_tasks(uids: [1, 2])
190 | task = client.wait_for_task(task['taskUid'])
191 |
192 | expect(task['details']['originalFilter']).to eq('?uids=1%2C2')
193 | expect(task['details']['matchedTasks']).to be_a(Integer)
194 | expect(task['details']['canceledTasks']).to be_a(Integer)
195 | end
196 | end
197 |
198 | describe '#client.delete_tasks' do
199 | it 'ensures supports to all available filters' do
200 | date = DateTime.new(2022, 1, 20)
201 |
202 | allow(Meilisearch::Utils).to receive(:transform_attributes).and_call_original
203 |
204 | client.delete_tasks(
205 | canceled_by: [1, 2], uids: [2], foo: 'bar',
206 | before_enqueued_at: date, after_enqueued_at: date,
207 | before_started_at: date, after_started_at: date,
208 | before_finished_at: date, after_finished_at: date
209 | )
210 |
211 | expect(Meilisearch::Utils).to have_received(:transform_attributes)
212 | .with(
213 | canceled_by: [1, 2], uids: [2],
214 | before_enqueued_at: date, after_enqueued_at: date,
215 | before_started_at: date, after_started_at: date,
216 | before_finished_at: date, after_finished_at: date
217 | )
218 | end
219 |
220 | it 'has fields in the details field' do
221 | task = client.delete_tasks(uids: [1, 2])
222 | task = client.wait_for_task(task['taskUid'])
223 |
224 | expect(task['details']['originalFilter']).to eq('?uids=1%2C2')
225 | expect(task['details']['matchedTasks']).to be_a(Integer)
226 | expect(task['details']['deletedTasks']).to be_a(Integer)
227 | end
228 | end
229 | end
230 |
--------------------------------------------------------------------------------
/spec/meilisearch/client/indexes_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe 'Meilisearch::Client - Indexes' do
4 | describe '#create_index' do
5 | context 'without a primary key' do
6 | it 'creates an index' do
7 | task = client.create_index('books')
8 | expect(task.type).to eq('indexCreation')
9 | task.await
10 |
11 | index = client.fetch_index('books')
12 | expect(index).to be_a(Meilisearch::Index)
13 | expect(index.uid).to eq('books')
14 | expect(index.primary_key).to be_nil
15 | end
16 |
17 | context 'synchronously' do
18 | context 'using ! method' do
19 | before { allow(Meilisearch::Utils).to receive(:soft_deprecate).and_return(nil) }
20 |
21 | it 'creates an index' do
22 | task = client.create_index!('books')
23 |
24 | expect(task.type).to eq('indexCreation')
25 | expect(task).to be_succeeded
26 |
27 | index = client.fetch_index('books')
28 |
29 | expect(index).to be_a(Meilisearch::Index)
30 | expect(index.uid).to eq('books')
31 | expect(index.primary_key).to be_nil
32 | end
33 |
34 | it 'warns about deprecation' do
35 | client.create_index!('books')
36 | expect(Meilisearch::Utils)
37 | .to have_received(:soft_deprecate)
38 | .with('Client#create_index!', a_string_including('books'))
39 | end
40 | end
41 |
42 | context 'using await syntax' do
43 | it 'creates an index' do
44 | task = client.create_index('books').await
45 |
46 | expect(task['type']).to eq('indexCreation')
47 | expect(task['status']).to eq('succeeded')
48 |
49 | index = client.fetch_index('books')
50 |
51 | expect(index).to be_a(Meilisearch::Index)
52 | expect(index.uid).to eq('books')
53 | expect(index.primary_key).to be_nil
54 | end
55 | end
56 | end
57 | end
58 |
59 | context 'with a primary key' do
60 | it 'creates an index' do
61 | task = client.create_index('books', primary_key: 'reference_code')
62 |
63 | expect(task.type).to eq('indexCreation')
64 | task.await
65 |
66 | index = client.fetch_index('books')
67 | expect(index).to be_a(Meilisearch::Index)
68 | expect(index.uid).to eq('books')
69 | expect(index.primary_key).to eq('reference_code')
70 | expect(index.fetch_primary_key).to eq('reference_code')
71 | end
72 |
73 | it 'creates an index synchronously' do
74 | task = client.create_index('books', primary_key: 'reference_code').await
75 |
76 | expect(task['type']).to eq('indexCreation')
77 | expect(task['status']).to eq('succeeded')
78 |
79 | index = client.fetch_index('books')
80 |
81 | expect(index).to be_a(Meilisearch::Index)
82 | expect(index.uid).to eq('books')
83 | expect(index.primary_key).to eq('reference_code')
84 | expect(index.fetch_primary_key).to eq('reference_code')
85 | end
86 |
87 | context 'when primary key option in snake_case' do
88 | it 'creates an index' do
89 | task = client.create_index('books', primary_key: 'reference_code')
90 | expect(task.type).to eq('indexCreation')
91 | task.await
92 |
93 | index = client.fetch_index('books')
94 | expect(index).to be_a(Meilisearch::Index)
95 | expect(index.uid).to eq('books')
96 | expect(index.primary_key).to eq('reference_code')
97 | expect(index.fetch_primary_key).to eq('reference_code')
98 | end
99 | end
100 |
101 | context 'when uid is provided as an option' do
102 | it 'ignores the uid option' do
103 | task = client.create_index(
104 | 'books',
105 | primary_key: 'reference_code',
106 | uid: 'publications'
107 | )
108 |
109 | expect(task.type).to eq('indexCreation')
110 | task.await
111 |
112 | index = client.fetch_index('books')
113 | expect(index).to be_a(Meilisearch::Index)
114 | expect(index.uid).to eq('books')
115 | expect(index.primary_key).to eq('reference_code')
116 | expect(index.fetch_primary_key).to eq('reference_code')
117 | end
118 | end
119 | end
120 |
121 | context 'when an index with a given uid already exists' do
122 | it 'returns a failing task' do
123 | initial_task = client.create_index('books').await
124 | last_task = client.create_index('books').await
125 |
126 | expect(initial_task['type']).to eq('indexCreation')
127 | expect(last_task['type']).to eq('indexCreation')
128 | expect(initial_task['status']).to eq('succeeded')
129 | expect(last_task['status']).to eq('failed')
130 | expect(last_task['error']['code']).to eq('index_already_exists')
131 | end
132 | end
133 |
134 | context 'when the uid format is invalid' do
135 | it 'raises an error' do
136 | expect do
137 | client.create_index('ancient books')
138 | end.to raise_meilisearch_api_error_with(400, 'invalid_index_uid', 'invalid_request')
139 | end
140 | end
141 | end
142 |
143 | describe '#indexes' do
144 | it 'returns Meilisearch::Index objects' do
145 | client.create_index('books').await
146 |
147 | index = client.indexes['results'].first
148 |
149 | expect(index).to be_a(Meilisearch::Index)
150 | end
151 |
152 | it 'gets a list of indexes' do
153 | ['books', 'colors', 'artists'].each { |name| client.create_index(name).await }
154 |
155 | indexes = client.indexes['results']
156 |
157 | expect(indexes).to be_a(Array)
158 | expect(indexes.length).to eq(3)
159 | uids = indexes.map(&:uid)
160 | expect(uids).to contain_exactly('books', 'colors', 'artists')
161 | end
162 |
163 | it 'paginates indexes list with limit and offset' do
164 | ['books', 'colors', 'artists'].each { |name| client.create_index(name).await }
165 |
166 | indexes = client.indexes(limit: 1, offset: 2)
167 |
168 | expect(indexes['results']).to be_a(Array)
169 | expect(indexes['total']).to eq(3)
170 | expect(indexes['limit']).to eq(1)
171 | expect(indexes['offset']).to eq(2)
172 | expect(indexes['results'].map(&:uid)).to eq(['colors'])
173 | end
174 | end
175 |
176 | describe '#raw_indexes' do
177 | it 'returns raw indexes' do
178 | client.create_index('index').await
179 |
180 | response = client.raw_indexes['results'].first
181 |
182 | expect(response).to be_a(Hash)
183 | expect(response['uid']).to eq('index')
184 | end
185 |
186 | it 'gets a list of raw indexes' do
187 | ['books', 'colors', 'artists'].each { |name| client.create_index(name).await }
188 |
189 | indexes = client.raw_indexes['results']
190 |
191 | expect(indexes).to be_a(Array)
192 | expect(indexes.length).to eq(3)
193 | uids = indexes.map { |elem| elem['uid'] }
194 | expect(uids).to contain_exactly('books', 'colors', 'artists')
195 | end
196 | end
197 |
198 | describe '#fetch_index' do
199 | it 'fetches index by uid' do
200 | client.create_index('books', primary_key: 'reference_code').await
201 |
202 | fetched_index = client.fetch_index('books')
203 |
204 | expect(fetched_index).to be_a(Meilisearch::Index)
205 | expect(fetched_index.uid).to eq('books')
206 | expect(fetched_index.primary_key).to eq('reference_code')
207 | expect(fetched_index.fetch_primary_key).to eq('reference_code')
208 | end
209 | end
210 |
211 | describe '#fetch_raw_index' do
212 | it 'fetch a specific index raw Hash response based on uid' do
213 | client.create_index('books', primary_key: 'reference_code').await
214 | index = client.fetch_index('books')
215 | raw_response = index.fetch_raw_info
216 |
217 | expect(raw_response).to be_a(Hash)
218 | expect(raw_response['uid']).to eq('books')
219 | expect(raw_response['primaryKey']).to eq('reference_code')
220 | expect(Time.parse(raw_response['createdAt'])).to be_a(Time)
221 | expect(Time.parse(raw_response['createdAt'])).to be_within(60).of(Time.now)
222 | expect(Time.parse(raw_response['updatedAt'])).to be_a(Time)
223 | expect(Time.parse(raw_response['updatedAt'])).to be_within(60).of(Time.now)
224 | end
225 | end
226 |
227 | describe '#index' do
228 | it 'returns an index object with the provided uid' do
229 | client.create_index('books', primary_key: 'reference_code').await
230 | # this index is in memory, without metadata from server
231 | index = client.index('books')
232 |
233 | expect(index).to be_a(Meilisearch::Index)
234 | expect(index.uid).to eq('books')
235 | expect(index.primary_key).to be_nil
236 |
237 | # fetch primary key metadata from server
238 | expect(index.fetch_primary_key).to eq('reference_code')
239 | expect(index.primary_key).to eq('reference_code')
240 | end
241 | end
242 |
243 | describe '#delete_index' do
244 | context 'when the index exists' do
245 | it 'deletes the index' do
246 | client.create_index('books').await
247 | task = client.delete_index('books')
248 |
249 | expect(task['type']).to eq('indexDeletion')
250 |
251 | task.await
252 |
253 | expect(task).to be_succeeded
254 | expect { client.fetch_index('books') }.to raise_index_not_found_meilisearch_api_error
255 | end
256 | end
257 |
258 | context 'when the index does not exist' do
259 | it 'raises an index not found error' do
260 | expect { client.fetch_index('bookss') }.to raise_index_not_found_meilisearch_api_error
261 | end
262 | end
263 | end
264 |
265 | describe '#swap_indexes' do
266 | it 'swaps two indexes' do
267 | task = client.swap_indexes(['indexA', 'indexB'], ['indexC', 'indexD'])
268 |
269 | expect(task.type).to eq('indexSwap')
270 | task.await
271 | expect(task['details']['swaps']).to include(
272 | include('indexes' => ['indexA', 'indexB']),
273 | include('indexes' => ['indexC', 'indexD'])
274 | )
275 | end
276 | end
277 | end
278 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Meilisearch Ruby
6 |
7 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | ⚡ The Meilisearch API client written for Ruby 💎
27 |
28 | **Meilisearch Ruby** is the Meilisearch API client for Ruby developers.
29 |
30 | **Meilisearch** is an open-source search engine. [Learn more about Meilisearch.](https://github.com/meilisearch/meilisearch)
31 |
32 | ## Table of Contents
33 |
34 | - [📖 Documentation](#-documentation)
35 | - [🔧 Installation](#-installation)
36 | - [🚀 Getting started](#-getting-started)
37 | - [🤖 Compatibility with Meilisearch](#-compatibility-with-meilisearch)
38 | - [💡 Learn more](#-learn-more)
39 | - [⚙️ Contributing](#️-contributing)
40 |
41 | ## 📖 Documentation
42 |
43 | This readme contains all the documentation you need to start using this Meilisearch SDK.
44 |
45 | For general information on how to use Meilisearch—such as our API reference, tutorials, guides, and in-depth articles—refer to our [main documentation website](https://www.meilisearch.com/docs/).
46 |
47 |
48 | ## 🔧 Installation
49 |
50 | We officially support any version of Ruby that is still receiving at least [security maintenance](https://www.ruby-lang.org/en/downloads/branches/). You may, however, be fine with any Ruby version above 3.0.
51 | However, we cannot guarantee support if your version is no longer being maintained.
52 |
53 | With `gem` in command line:
54 | ```bash
55 | gem install meilisearch
56 | ```
57 |
58 | In your `Gemfile` with [bundler](https://bundler.io/):
59 | ```ruby
60 | source 'https://rubygems.org'
61 |
62 | gem 'meilisearch'
63 | ```
64 |
65 | ### Run Meilisearch
66 |
67 | ⚡️ **Launch, scale, and streamline in minutes with Meilisearch Cloud**—no maintenance, no commitment, cancel anytime. [Try it free now](https://cloud.meilisearch.com/login?utm_campaign=oss&utm_source=github&utm_medium=meilisearch-ruby).
68 |
69 | 🪨 Prefer to self-host? [Download and deploy](https://www.meilisearch.com/docs/learn/self_hosted/getting_started_with_self_hosted_meilisearch?utm_campaign=oss&utm_source=github&utm_medium=meilisearch-ruby) our fast, open-source search engine on your own infrastructure.
70 |
71 | ## 🚀 Getting started
72 |
73 | #### Add documents
74 |
75 | ```ruby
76 | require 'meilisearch'
77 |
78 | client = Meilisearch::Client.new('http://127.0.0.1:7700', 'masterKey')
79 |
80 | # An index is where the documents are stored.
81 | index = client.index('movies')
82 |
83 | documents = [
84 | { id: 1, title: 'Carol', genres: ['Romance', 'Drama'] },
85 | { id: 2, title: 'Wonder Woman', genres: ['Action', 'Adventure'] },
86 | { id: 3, title: 'Life of Pi', genres: ['Adventure', 'Drama'] },
87 | { id: 4, title: 'Mad Max: Fury Road', genres: ['Adventure', 'Science Fiction'] },
88 | { id: 5, title: 'Moana', genres: ['Fantasy', 'Action']},
89 | { id: 6, title: 'Philadelphia', genres: ['Drama'] },
90 | ]
91 | # If the index 'movies' does not exist, Meilisearch creates it when you first add the documents.
92 | index.add_documents(documents) # => { "uid": 0 }
93 | ```
94 |
95 | With the `uid`, you can check the status (`enqueued`, `canceled`, `processing`, `succeeded` or `failed`) of your documents addition using the [task](https://www.meilisearch.com/docs/reference/api/tasks#get-tasks).
96 |
97 | 💡 To customize the `Client`, for example, increasing the default timeout, please check out [this section](https://github.com/meilisearch/meilisearch-ruby/wiki/Client-Options) of the Wiki.
98 |
99 | #### Basic Search
100 |
101 | ``` ruby
102 | # Meilisearch is typo-tolerant:
103 | puts index.search('carlo')
104 | ```
105 | Output:
106 |
107 | ```ruby
108 | {
109 | "hits" => [{
110 | "id" => 1,
111 | "title" => "Carol"
112 | }],
113 | "offset" => 0,
114 | "limit" => 20,
115 | "processingTimeMs" => 1,
116 | "query" => "carlo"
117 | }
118 | ```
119 |
120 | #### Custom search
121 |
122 | All the supported options are described in the [search parameters](https://www.meilisearch.com/docs/reference/api/search#search-parameters) section of the documentation.
123 |
124 | ```ruby
125 | index.search(
126 | 'wonder',
127 | attributes_to_highlight: ['*']
128 | )
129 | ```
130 |
131 | JSON output:
132 |
133 | ```json
134 | {
135 | "hits": [
136 | {
137 | "id": 2,
138 | "title": "Wonder Woman",
139 | "_formatted": {
140 | "id": 2,
141 | "title": "Wonder Woman"
142 | }
143 | }
144 | ],
145 | "offset": 0,
146 | "limit": 20,
147 | "processingTimeMs": 0,
148 | "query": "wonder"
149 | }
150 | ```
151 |
152 | #### Custom Search With Filters
153 |
154 | If you want to enable filtering, you must add your attributes to the `filterableAttributes` index setting.
155 |
156 | ```ruby
157 | index.update_filterable_attributes([
158 | 'id',
159 | 'genres'
160 | ])
161 | ```
162 |
163 | You only need to perform this operation once.
164 |
165 | Note that Meilisearch will rebuild your index whenever you update `filterableAttributes`. Depending on the size of your dataset, this might take time. You can track the process using the [tasks](https://www.meilisearch.com/docs/reference/api/tasks#get-tasks)).
166 |
167 | Then, you can perform the search:
168 |
169 | ```ruby
170 | index.search('wonder', { filter: ['id > 1 AND genres = Action'] })
171 | ```
172 |
173 | JSON output:
174 |
175 | ```json
176 | {
177 | "hits": [
178 | {
179 | "id": 2,
180 | "title": "Wonder Woman",
181 | "genres": [
182 | "Action",
183 | "Adventure"
184 | ]
185 | }
186 | ],
187 | "estimatedTotalHits": 1,
188 | "query": "wonder",
189 | "limit": 20,
190 | "offset": 0,
191 | "processingTimeMs": 0
192 | }
193 | ```
194 |
195 | #### Display ranking details at search
196 |
197 | JSON output:
198 |
199 | ```json
200 | {
201 | "hits": [
202 | {
203 | "id": 15359,
204 | "title": "Wonder Woman",
205 | "_rankingScoreDetails": {
206 | "words": {
207 | "order": 0,
208 | "matchingWords": 2,
209 | "maxMatchingWords": 2,
210 | "score": 1.0
211 | },
212 | "typo": {
213 | "order": 1,
214 | "typoCount": 0,
215 | "maxTypoCount": 2,
216 | "score": 1.0
217 | },
218 | "proximity": {
219 | "order": 2,
220 | "score": 1.0
221 | },
222 | "attribute": {
223 | "order": 3,
224 | "attributeRankingOrderScore": 0.8181818181818182,
225 | "queryWordDistanceScore": 1.0,
226 | "score": 0.8181818181818182
227 | },
228 | "exactness": {
229 | "order": 4,
230 | "matchType": "exactMatch",
231 | "score": 1.0
232 | }
233 | }
234 | }
235 | ]
236 | }
237 | ```
238 |
239 | This feature is only available with Meilisearch v1.3 and newer (optional).
240 |
241 | #### Custom Search With attributes on at search time
242 |
243 | [Customize attributes to search on at search time](https://www.meilisearch.com/docs/reference/api/search#customize-attributes-to-search-on-at-search-time).
244 |
245 | you can perform the search :
246 |
247 | ```ruby
248 | index.search('wonder', { attributes_to_search_on: ['genres'] })
249 | ```
250 |
251 |
252 | JSON output:
253 |
254 | ```json
255 | {
256 | "hits":[],
257 | "query":"wonder",
258 | "processingTimeMs":0,
259 | "limit":20,
260 | "offset":0,
261 | "estimatedTotalHits":0,
262 | "nbHits":0
263 | }
264 | ```
265 |
266 | This feature is only available with Meilisearch v1.3 and newer (optional).
267 |
268 |
269 | ## 🤖 Compatibility with Meilisearch
270 |
271 | This package guarantees compatibility with [version v1.x of Meilisearch](https://github.com/meilisearch/meilisearch/releases/latest), but some features may not be present. Please check the [issues](https://github.com/meilisearch/meilisearch-ruby/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22+label%3Aenhancement) for more info.
272 |
273 | ## 💡 Learn more
274 |
275 | The following sections in our main documentation website may interest you:
276 |
277 | - **Manipulate documents**: see the [API references](https://www.meilisearch.com/docs/reference/api/documents) or read more about [documents](https://www.meilisearch.com/docs/learn/core_concepts/documents).
278 | - **Search**: see the [API references](https://www.meilisearch.com/docs/reference/api/search) or follow our guide on [search parameters](https://www.meilisearch.com/docs/reference/api/search#search-parameters).
279 | - **Manage the indexes**: see the [API references](https://www.meilisearch.com/docs/reference/api/indexes) or read more about [indexes](https://www.meilisearch.com/docs/learn/core_concepts/indexes).
280 | - **Configure the index settings**: see the [API references](https://www.meilisearch.com/docs/reference/api/settings) or follow our guide on [settings parameters](https://www.meilisearch.com/docs/reference/api/settings).
281 |
282 | 📖 Also, check out the [Wiki](https://github.com/meilisearch/meilisearch-ruby/wiki) of this repository to know what this SDK provides!
283 |
284 | ## ⚙️ Contributing
285 |
286 | Any new contribution is more than welcome in this project!
287 |
288 | If you want to know more about the development workflow or want to contribute, please visit our [contributing guidelines](/CONTRIBUTING.md) for detailed instructions!
289 |
290 |
291 |
292 | **Meilisearch** provides and maintains many **SDKs and Integration tools** like this one. We want to provide everyone with an **amazing search experience for any kind of project**. If you want to contribute, make suggestions, or just know what's going on right now, visit us in the [integration-guides](https://github.com/meilisearch/integration-guides) repository.
293 |
--------------------------------------------------------------------------------
/spec/meilisearch/models/task_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | describe Meilisearch::Models::Task do
4 | subject { described_class.new task_hash, endpoint }
5 |
6 | let(:new_index_uid) { random_uid }
7 | let(:task_hash) { client.http_post '/indexes', { 'uid' => new_index_uid } }
8 | let(:endpoint) { Meilisearch::Task.new(URL, MASTER_KEY, client.options) }
9 |
10 | let(:enqueued_endpoint) { instance_double(Meilisearch::Task, task: task_hash) }
11 | let(:enqueued_task) { described_class.new task_hash, enqueued_endpoint }
12 |
13 | let(:processing_endpoint) { instance_double(Meilisearch::Task, task: task_hash.update('status' => 'processing')) }
14 | let(:processing_task) { described_class.new task_hash, processing_endpoint }
15 |
16 | let(:logger) { instance_double(Logger, warn: nil) }
17 |
18 | before { Meilisearch::Utils.logger = logger }
19 | after { Meilisearch::Utils.logger = nil }
20 |
21 | describe '.initialize' do
22 | it 'requires a uid in the task hash' do
23 | task_hash.delete 'taskUid'
24 |
25 | expect { subject }.to raise_error(ArgumentError)
26 | end
27 |
28 | it 'requires a type in the task hash' do
29 | task_hash.delete 'type'
30 |
31 | expect { subject }.to raise_error(ArgumentError)
32 | end
33 |
34 | it 'requires a status in the task hash' do
35 | task_hash.delete 'status'
36 |
37 | expect { subject }.to raise_error(ArgumentError)
38 | end
39 |
40 | it 'sets "taskUid" key when given a "uid"' do
41 | expect(subject).to have_key('uid')
42 | end
43 |
44 | it 'sets "uid" key when given a "taskUid"' do
45 | task_hash['uid'] = task_hash.delete 'taskUid'
46 |
47 | expect(subject).to have_key('taskUid')
48 | end
49 | end
50 |
51 | describe 'forwarding' do
52 | it 'allows accessing values in the internal task hash' do
53 | subject
54 |
55 | task_hash.each do |key, value|
56 | expect(subject[key]).to eq(value)
57 | end
58 | end
59 | end
60 |
61 | describe '#enqueued?' do
62 | context 'when the task is processing' do
63 | before { task_hash['status'] = 'processing' }
64 |
65 | it { is_expected.not_to be_enqueued }
66 |
67 | it 'does not refresh the task' do
68 | allow(subject).to receive(:refresh)
69 | subject.enqueued?
70 | expect(subject).not_to have_received(:refresh)
71 | end
72 | end
73 |
74 | context 'when the task has succeeded' do
75 | before { task_hash['status'] = 'succeeded' }
76 |
77 | it { is_expected.not_to be_enqueued }
78 |
79 | it 'does not refresh the task' do
80 | allow(subject).to receive(:refresh)
81 | subject.enqueued?
82 | expect(subject).not_to have_received(:refresh)
83 | end
84 | end
85 |
86 | context 'when the task has failed' do
87 | before { task_hash['status'] = 'failed' }
88 |
89 | it { is_expected.not_to be_enqueued }
90 |
91 | it 'does not refresh the task' do
92 | allow(subject).to receive(:refresh)
93 | subject.enqueued?
94 | expect(subject).not_to have_received(:refresh)
95 | end
96 | end
97 |
98 | it 'returns true when the task is enqueued' do
99 | expect(enqueued_task).to be_enqueued
100 | end
101 |
102 | context 'when the task has succeeded but not refreshed' do
103 | let(:successful_task_hash) { task_hash.merge('status' => 'succeeded') }
104 | let(:endpoint) { instance_double(Meilisearch::Task, task: successful_task_hash) }
105 |
106 | it { is_expected.not_to be_enqueued }
107 | end
108 | end
109 |
110 | describe '#processing?' do
111 | context 'when the task has succeeded' do
112 | before { task_hash['status'] = 'succeeded' }
113 |
114 | it { is_expected.not_to be_processing }
115 |
116 | it 'does not refresh the task' do
117 | allow(subject).to receive(:refresh)
118 | subject.processing?
119 | expect(subject).not_to have_received(:refresh)
120 | end
121 | end
122 |
123 | context 'when the task has failed' do
124 | before { task_hash['status'] = 'failed' }
125 |
126 | it { is_expected.not_to be_processing }
127 |
128 | it 'does not refresh the task' do
129 | allow(subject).to receive(:refresh)
130 | subject.processing?
131 | expect(subject).not_to have_received(:refresh)
132 | end
133 | end
134 |
135 | it 'returns false when the task has not begun to process' do
136 | expect(enqueued_task).not_to be_processing
137 | end
138 |
139 | it 'returns true when the task is processing' do
140 | expect(processing_task).to be_processing
141 | end
142 |
143 | context 'when the task has begun processing but has not refreshed' do
144 | let(:endpoint) { instance_double(Meilisearch::Task, task: task_hash.merge('status' => 'processing')) }
145 |
146 | it { is_expected.to be_processing }
147 | end
148 |
149 | context 'when the task has succeeded but not refreshed' do
150 | let(:successful_task_hash) { task_hash.merge('status' => 'succeeded') }
151 | let(:endpoint) { instance_double(Meilisearch::Task, task: successful_task_hash) }
152 |
153 | it 'refreshes and returns false' do
154 | expect(subject).not_to be_enqueued
155 | end
156 | end
157 | end
158 |
159 | describe '#unfinished?' do
160 | it 'returns false if the task has succeeded' do
161 | task_hash['status'] = 'succeeded'
162 | expect(subject).not_to be_unfinished
163 | end
164 |
165 | it 'returns false when the task has failed' do
166 | task_hash['status'] = 'failed'
167 | expect(subject).not_to be_unfinished
168 | end
169 |
170 | it 'returns true when the task is enqueued' do
171 | expect(enqueued_task).to be_unfinished
172 | end
173 |
174 | it 'returns true when the task is processing' do
175 | expect(processing_task).to be_unfinished
176 | end
177 |
178 | context 'when the task has succeeded but not refreshed' do
179 | let(:successful_task_hash) { task_hash.merge('status' => 'succeeded') }
180 | let(:endpoint) { instance_double(Meilisearch::Task, task: successful_task_hash) }
181 |
182 | it { is_expected.not_to be_unfinished }
183 | end
184 | end
185 |
186 | describe '#finished?' do
187 | it 'returns true when the task has succeeded' do
188 | task_hash['status'] = 'succeeded'
189 | expect(subject).to be_finished
190 | end
191 |
192 | it 'returns true when the task has failed' do
193 | task_hash['status'] = 'failed'
194 | expect(subject).to be_finished
195 | end
196 |
197 | it 'returns false when the task is enqueued' do
198 | expect(enqueued_task).not_to be_finished
199 | end
200 |
201 | it 'returns false when the task is processing' do
202 | expect(processing_task).not_to be_finished
203 | end
204 |
205 | context 'when the task has succeeded but not refreshed' do
206 | let(:successful_task_hash) { task_hash.merge('status' => 'succeeded') }
207 | let(:endpoint) { instance_double(Meilisearch::Task, task: successful_task_hash) }
208 |
209 | it { is_expected.to be_finished }
210 | end
211 | end
212 |
213 | describe '#failed?' do
214 | it 'returns false if the task has succeeded or been cancelled' do
215 | task_hash['status'] = 'succeeded'
216 | expect(subject).not_to be_failed
217 | task_hash['status'] = 'cancelled'
218 | expect(subject).not_to be_failed
219 | end
220 |
221 | it 'returns true if the task has failed' do
222 | task_hash['status'] = 'failed'
223 | expect(subject).to be_failed
224 | end
225 |
226 | context 'when the task is not finished' do
227 | let(:endpoint) { instance_double(Meilisearch::Task, task: task_hash) }
228 |
229 | it { is_expected.not_to be_failed }
230 |
231 | it 'warns that the task is not finished' do
232 | subject.failed?
233 |
234 | expect(logger).to have_received(:warn).with(a_string_including('checked before finishing'))
235 | end
236 | end
237 |
238 | context 'when the task has failed but not refreshed' do
239 | let(:failed_task_hash) { task_hash.merge('status' => 'failed') }
240 | let(:endpoint) { instance_double(Meilisearch::Task, task: failed_task_hash) }
241 |
242 | it { is_expected.to be_failed }
243 | end
244 | end
245 |
246 | describe '#succeeded?' do
247 | it 'returns true if the task has succeeded' do
248 | task_hash['status'] = 'succeeded'
249 | expect(subject).to be_succeeded
250 | end
251 |
252 | it 'returns false if the task has failed or been cancelled' do
253 | task_hash['status'] = 'failed'
254 | expect(subject).not_to be_succeeded
255 | task_hash['status'] = 'cancelled'
256 | expect(subject).not_to be_succeeded
257 | end
258 |
259 | context 'when the task is not finished' do
260 | let(:endpoint) { instance_double(Meilisearch::Task, task: task_hash) }
261 |
262 | it { is_expected.not_to be_succeeded }
263 |
264 | it 'warns that the task is not finished' do
265 | subject.succeeded?
266 |
267 | expect(logger).to have_received(:warn).with(a_string_including('checked before finishing'))
268 | end
269 | end
270 |
271 | context 'when the task has succeeded but not refreshed' do
272 | let(:successful_task_hash) { task_hash.merge('status' => 'succeeded') }
273 | let(:endpoint) { instance_double(Meilisearch::Task, task: successful_task_hash) }
274 |
275 | it { is_expected.to be_succeeded }
276 | end
277 | end
278 |
279 | describe '#cancelled?' do
280 | it 'returns false if the task has succeeded or failed' do
281 | task_hash['status'] = 'succeeded'
282 | expect(subject).not_to be_cancelled
283 | task_hash['status'] = 'failed'
284 | expect(subject).not_to be_cancelled
285 | end
286 |
287 | it 'returns true if the task has been cancelled' do
288 | task_hash['status'] = 'cancelled'
289 | expect(subject).to be_cancelled
290 | end
291 |
292 | context 'when the task is not finished' do
293 | let(:endpoint) { instance_double(Meilisearch::Task, task: task_hash) }
294 |
295 | it { is_expected.not_to be_cancelled }
296 |
297 | it 'warns that the task is not finished' do
298 | subject.cancelled?
299 |
300 | expect(logger).to have_received(:warn).with(a_string_including('checked before finishing'))
301 | end
302 | end
303 |
304 | context 'when the task has been cancelled but not refreshed' do
305 | let(:cancelled_task_hash) { task_hash.merge('status' => 'cancelled') }
306 | let(:endpoint) { instance_double(Meilisearch::Task, task: cancelled_task_hash) }
307 |
308 | it { is_expected.to be_cancelled }
309 | end
310 | end
311 |
312 | describe '#deleted?' do
313 | let(:not_found_error) { Meilisearch::ApiError.new(404, '', '') }
314 | let(:endpoint) { instance_double(Meilisearch::Task, task: task_hash) }
315 |
316 | it 'returns false when the task can be found' do
317 | expect(subject.deleted?).to be(false) # don't just return nil
318 | expect(subject).not_to be_deleted
319 | end
320 |
321 | context 'when it was deleted prior' do
322 | let(:endpoint) { instance_double(Meilisearch::Task) }
323 |
324 | before do
325 | allow(endpoint).to receive(:task) { raise not_found_error }
326 | subject.refresh
327 | end
328 |
329 | it 'does not check again' do
330 | subject.deleted?
331 | expect(endpoint).to have_received(:task).once
332 | end
333 |
334 | it { is_expected.to be_deleted }
335 | end
336 |
337 | it 'refreshes and returns true when it is no longer in instance' do
338 | allow(endpoint).to receive(:task) { raise not_found_error }
339 | expect(subject).to be_deleted
340 | end
341 | end
342 |
343 | describe '#cancel' do
344 | context 'when the task is still not finished' do
345 | let(:cancellation_task) { instance_double(described_class, await: nil) }
346 | let(:endpoint) { instance_double(Meilisearch::Task, task: task_hash, cancel_tasks: cancellation_task) }
347 |
348 | it 'sends a request to cancel itself' do
349 | subject.cancel
350 | expect(endpoint).to have_received(:cancel_tasks)
351 | end
352 |
353 | it 'returns true when the cancellation succeeds' do
354 | task_hash['status'] = 'cancelled'
355 | expect(subject.cancel).to be(true)
356 | end
357 |
358 | it 'returns false when the cancellation fails' do
359 | task_hash['status'] = 'succeeded'
360 | expect(subject.cancel).to be(false)
361 | end
362 | end
363 |
364 | context 'when the task is already finished' do
365 | let(:endpoint) { instance_double(Meilisearch::Task, task: task_hash, cancel_tasks: nil) }
366 |
367 | before { task_hash['status'] = 'succeeded' }
368 |
369 | it 'sends no request' do
370 | subject.cancel
371 | expect(endpoint).not_to have_received(:cancel_tasks)
372 | end
373 |
374 | it { is_expected.not_to be_cancelled }
375 | end
376 |
377 | context 'when the task is already cancelled' do
378 | let(:endpoint) { instance_double(Meilisearch::Task, task: task_hash, cancel_tasks: nil) }
379 |
380 | before { task_hash['status'] = 'cancelled' }
381 |
382 | it 'sends no request' do
383 | subject.cancel
384 | expect(endpoint).not_to have_received(:cancel_tasks)
385 | end
386 |
387 | it { is_expected.to be_cancelled }
388 | end
389 | end
390 |
391 | describe '#delete' do
392 | let(:deletion_task) { instance_double(described_class, await: nil) }
393 | let(:endpoint) { instance_double(Meilisearch::Task, delete_tasks: deletion_task) }
394 |
395 | context 'when the task is unfinished' do
396 | it 'makes no request' do
397 | subject.delete
398 | expect(endpoint).not_to have_received(:delete_tasks)
399 | end
400 |
401 | it 'returns false' do
402 | expect(subject.delete).to be(false)
403 | end
404 | end
405 |
406 | context 'when the task is finished' do
407 | before do
408 | task_hash['status'] = 'failed'
409 | not_found_error = Meilisearch::ApiError.new(404, '', '')
410 | allow(endpoint).to receive(:task) { raise not_found_error }
411 | end
412 |
413 | it 'makes a deletion request' do
414 | subject.delete
415 | expect(endpoint).to have_received(:delete_tasks)
416 | end
417 |
418 | it 'returns true' do
419 | expect(subject.delete).to be(true)
420 | end
421 | end
422 | end
423 |
424 | describe '#refresh' do
425 | let(:changed_task) { task_hash.merge('status' => 'succeeded', 'error' => 'Done too well') }
426 | let(:endpoint) { instance_double(Meilisearch::Task, task: changed_task) }
427 |
428 | it 'calls endpoint to update task' do
429 | expect { subject.refresh }.to change { subject['status'] }.from('enqueued').to('succeeded')
430 | .and(change { subject['error'] }.from(nil).to('Done too well'))
431 | end
432 | end
433 |
434 | describe '#await' do
435 | let(:changed_task) { task_hash.merge('status' => 'succeeded', 'error' => 'Done too well') }
436 | let(:endpoint) { instance_double(Meilisearch::Task, task: task_hash, wait_for_task: changed_task) }
437 |
438 | context 'when the task is not yet completed' do
439 | let(:endpoint) { instance_double(Meilisearch::Task, task: task_hash, wait_for_task: changed_task) }
440 |
441 | it 'waits for the task to complete' do
442 | expect { subject.await }.to change { subject['status'] }.from('enqueued').to('succeeded')
443 | .and(change { subject['error'] }.from(nil).to('Done too well'))
444 | end
445 |
446 | it 'returns itself for method chaining' do
447 | expect(subject.await).to be(subject)
448 | end
449 | end
450 |
451 | context 'when the task is already completed' do
452 | let(:endpoint) { instance_double(Meilisearch::Task, task: changed_task, wait_for_task: changed_task) }
453 |
454 | it 'does not contact the instance' do
455 | subject.refresh
456 | subject.await
457 |
458 | expect(endpoint).to have_received(:task).once
459 | expect(endpoint).not_to have_received(:wait_for_task)
460 | end
461 | end
462 | end
463 |
464 | describe '#error' do
465 | let(:error) do
466 | { 'message' => "Index `#{new_index_uid}` already exists.",
467 | 'code' => 'index_already_exists',
468 | 'type' => 'invalid_request',
469 | 'link' => 'https://docs.meilisearch.com/errors#index_already_exists' }
470 | end
471 |
472 | before { task_hash.merge!('error' => error, 'status' => 'failed') }
473 |
474 | it 'returns errors' do
475 | expect(subject.error).to match(error)
476 | end
477 | end
478 |
479 | describe '#to_h' do
480 | it 'returns the underlying task hash' do
481 | expect(subject.to_h).to be(task_hash)
482 | end
483 |
484 | it 'is aliased as #to_hash' do
485 | expect(subject.to_hash).to be(subject.to_h)
486 | end
487 | end
488 | end
489 |
--------------------------------------------------------------------------------
/lib/meilisearch/client.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Meilisearch
4 | # Manages a connection to a Meilisearch server.
5 | # client = Meilisearch::Client.new(MEILISEARCH_URL, MASTER_KEY, options)
6 | #
7 | # @see #indexes Managing search indexes
8 | # @see #keys Managing API keys
9 | # @see #stats View usage statistics
10 | # @see #tasks Managing ongoing tasks
11 | # @see #health Health checking
12 | # @see #create_dump
13 | # @see #create_snapshot
14 | class Client < HTTPRequest
15 | include Meilisearch::TenantToken
16 | include Meilisearch::MultiSearch
17 |
18 | ### INDEXES
19 |
20 | # Fetch indexes in instance, returning the raw server response.
21 | #
22 | # Unless you have a good reason to, {#indexes} should be used instead.
23 | #
24 | # @see #indexes
25 | # @see https://www.meilisearch.com/docs/reference/api/indexes#list-all-indexes Meilisearch API reference
26 | # @param options [Hash{Symbol => Object}] limit and offset options
27 | # @return [Hash{String => Object}]
28 | # {index response object}[https://www.meilisearch.com/docs/reference/api/indexes#response]
29 | def raw_indexes(options = {})
30 | body = Utils.transform_attributes(options.transform_keys(&:to_sym).slice(:limit, :offset))
31 |
32 | http_get('/indexes', body)
33 | end
34 |
35 | # Swap two indexes.
36 | #
37 | # Can be used as a convenient way to rebuild an index while keeping it operational.
38 | # client.index('a_swap').add_documents({})
39 | # client.swap_indexes(['a', 'a_swap'])
40 | #
41 | # Multiple swaps may be done with one request:
42 | # client.swap_indexes(['a', 'a_swap'], ['b', 'b_swap'])
43 | #
44 | # @see https://www.meilisearch.com/docs/reference/api/indexes#swap-indexes Meilisearch API reference
45 | #
46 | # @param options [Array] the indexes to swap
47 | # @return [Models::Task] the async task that swaps the indexes
48 | # @raise [ApiError]
49 | def swap_indexes(*options)
50 | mapped_array = options.map { |arr| { indexes: arr } }
51 |
52 | response = http_post '/swap-indexes', mapped_array
53 | Models::Task.new(response, task_endpoint)
54 | end
55 |
56 | # Fetch indexes in instance.
57 | #
58 | # @see https://www.meilisearch.com/docs/reference/api/indexes#list-all-indexes Meilisearch API reference
59 | # @param options [Hash{Symbol => Object}] limit and offset options
60 | # @return [Hash{String => Object}]
61 | # {index response object}[https://www.meilisearch.com/docs/reference/api/indexes#response]
62 | # with results mapped to instances of {Index}
63 | def indexes(options = {})
64 | response = raw_indexes(options)
65 |
66 | response['results'].map! do |index_hash|
67 | index_object(index_hash['uid'], index_hash['primaryKey'])
68 | end
69 |
70 | response
71 | end
72 |
73 | # Create a new empty index.
74 | #
75 | # client.create_index('indexUID')
76 | # client.create_index('indexUID', primary_key: 'id')
77 | #
78 | # Indexes are also created when accessed:
79 | #
80 | # client.index('new_index').add_documents({})
81 | #
82 | # @see #index
83 | # @see https://www.meilisearch.com/docs/reference/api/indexes#create-an-index Meilisearch API reference
84 | #
85 | # @param index_uid [String] the uid of the new index
86 | # @param options [Hash{Symbol => Object}, nil] snake_cased options of {the endpoint}[https://www.meilisearch.com/docs/reference/api/indexes#create-an-index]
87 | #
88 | # @raise [ApiError]
89 | # @return [Models::Task] the async task that creates the index
90 | def create_index(index_uid, options = {})
91 | body = Utils.transform_attributes(options.merge(uid: index_uid))
92 |
93 | response = http_post '/indexes', body
94 |
95 | Models::Task.new(response, task_endpoint)
96 | end
97 |
98 | # Synchronous version of {#create_index}.
99 | #
100 | # @deprecated
101 | # use {Models::Task#await} on task returned from {#create_index}
102 | #
103 | # client.create_index('foo').await
104 | #
105 | # Waits for the task to be achieved with a busy loop, be careful when using it.
106 | def create_index!(index_uid, options = {})
107 | Utils.soft_deprecate(
108 | 'Client#create_index!',
109 | "client.create_index('#{index_uid}').await"
110 | )
111 |
112 | create_index(index_uid, options).await
113 | end
114 |
115 | # Delete an index.
116 | #
117 | # @param index_uid [String] the uid of the index to be deleted
118 | # @return [Models::Task] the async task deleting the index
119 | def delete_index(index_uid)
120 | index_object(index_uid).delete
121 | end
122 |
123 | # Get index with given uid.
124 | #
125 | # Indexes that don't exist are lazily created by Meilisearch.
126 | # index = client.index('index_uid')
127 | # index.add_documents({}) # index is created here if it did not exist
128 | #
129 | # @see Index
130 | # @param index_uid [String] the uid of the index to get
131 | # @return [Index]
132 | def index(index_uid)
133 | index_object(index_uid)
134 | end
135 |
136 | # Shorthand for
137 | # client.index(index_uid).fetch_info
138 | #
139 | # @see Index#fetch_info
140 | # @param index_uid [String] uid of the index
141 | def fetch_index(index_uid)
142 | index_object(index_uid).fetch_info
143 | end
144 |
145 | # Shorthand for
146 | # client.index(index_uid).fetch_raw_info
147 | #
148 | # @see Index#fetch_raw_info
149 | # @param index_uid [String] uid of the index
150 | def fetch_raw_index(index_uid)
151 | index_object(index_uid).fetch_raw_info
152 | end
153 |
154 | ### KEYS
155 |
156 | # Get all API keys
157 | #
158 | # This and other key methods require that the Meilisearch instance have a
159 | # {master key}[https://www.meilisearch.com/docs/learn/security/differences_master_api_keys#master-key]
160 | # set.
161 | #
162 | # @see #create_key #create_key to create keys and set their scope
163 | # @see #key #key to fetch one key
164 | # @see https://www.meilisearch.com/docs/reference/api/keys#get-all-keys Meilisearch API reference
165 | # @param limit [String, Integer, nil] limit the number of returned keys
166 | # @param offset [String, Integer, nil] skip the first +offset+ keys,
167 | # useful for paging.
168 | #
169 | # @return [Hash{String => Object}] a {keys response}[https://www.meilisearch.com/docs/reference/api/keys#response]
170 | def keys(limit: nil, offset: nil)
171 | body = { limit: limit, offset: offset }.compact
172 |
173 | http_get '/keys', body
174 | end
175 |
176 | # Get a specific API key.
177 | #
178 | # # obviously this example uid will not correspond to a key on your server
179 | # # please replace it with your own key's uid
180 | # uid = '6062abda-a5aa-4414-ac91-ecd7944c0f8d'
181 | # client.key(uid)
182 | #
183 | # This and other key methods require that the Meilisearch instance have a
184 | # {master key}[https://www.meilisearch.com/docs/learn/security/differences_master_api_keys#master-key]
185 | # set.
186 | #
187 | # @see #keys #keys to get all keys in the instance
188 | # @see #create_key #create_key to create keys and set their scope
189 | # @see https://www.meilisearch.com/docs/reference/api/keys#get-one-key Meilisearch API reference
190 | # @param uid_or_key [String] either the uuidv4 that is the key's
191 | # {uid}[https://www.meilisearch.com/docs/reference/api/keys#uid] or
192 | # a hash of the uid and the master key that is the key's
193 | # {key}[https://www.meilisearch.com/docs/reference/api/keys#key] field
194 | #
195 | # @return [Hash{String => Object}] a {key object}[https://www.meilisearch.com/docs/reference/api/keys#key-object]
196 | def key(uid_or_key)
197 | http_get "/keys/#{uid_or_key}"
198 | end
199 |
200 | # Create a new API key.
201 | #
202 | # require 'date_core'
203 | # ten_days_later = (DateTime.now + 10).rfc3339
204 | # client.create_key(actions: ['*'], indexes: ['*'], expires_at: ten_days_later)
205 | #
206 | # This and other key methods require that the Meilisearch instance have a
207 | # {master key}[https://www.meilisearch.com/docs/learn/security/differences_master_api_keys#master-key]
208 | # set.
209 | #
210 | # @see #update_key #update_key to edit an existing key
211 | # @see #keys #keys to get all keys in the instance
212 | # @see #key #key to fetch one key
213 | # @see https://www.meilisearch.com/docs/reference/api/keys#create-a-key Meilisearch API reference
214 | # @param key_options [Hash{Symbol => Object}] the key options of which the required are
215 | # - +:actions+ +Array+ of API actions allowed for key, +["*"]+ for all
216 | # - +:indexes+ +Array+ of indexes key can act on, +["*"]+ for all
217 | # - +:expires_at+ expiration datetime in
218 | # {RFC 3339}[https://www.ietf.org/rfc/rfc3339.txt] format, nil if the key never expires
219 | #
220 | # @return [Hash{String => Object}] a {key object}[https://www.meilisearch.com/docs/reference/api/keys#key-object]
221 | def create_key(key_options)
222 | body = Utils.transform_attributes(key_options)
223 |
224 | http_post '/keys', body
225 | end
226 |
227 | # Update an existing API key.
228 | #
229 | # This and other key methods require that the Meilisearch instance have a
230 | # {master key}[https://www.meilisearch.com/docs/learn/security/differences_master_api_keys#master-key]
231 | # set.
232 | #
233 | # @see #create_key #create_key to create a new key
234 | # @see #keys #keys to get all keys in the instance
235 | # @see #key #key to fetch one key
236 | # @see https://www.meilisearch.com/docs/reference/api/keys#update-a-key Meilisearch API reference
237 | # @param key_options [Hash{Symbol => Object}] see {#create_key}
238 | #
239 | # @return [Hash{String => Object}] a {key object}[https://www.meilisearch.com/docs/reference/api/keys#key-object]
240 | def update_key(uid_or_key, key_options)
241 | body = Utils.transform_attributes(key_options)
242 | body = body.slice('description', 'name')
243 |
244 | http_patch "/keys/#{uid_or_key}", body
245 | end
246 |
247 | # Delete an API key.
248 | #
249 | # # obviously this example uid will not correspond to a key on your server
250 | # # please replace it with your own key's uid
251 | # uid = '6062abda-a5aa-4414-ac91-ecd7944c0f8d'
252 | # client.delete_key(uid)
253 | #
254 | # This and other key methods require that the Meilisearch instance have a
255 | # {master key}[https://www.meilisearch.com/docs/learn/security/differences_master_api_keys#master-key]
256 | # set.
257 | #
258 | # @see #keys #keys to get all keys in the instance
259 | # @see #create_key #create_key to create keys and set their scope
260 | # @see https://www.meilisearch.com/docs/reference/api/keys#delete-a-key Meilisearch API reference
261 | # @param uid_or_key [String] either the uuidv4 that is the key's
262 | # {uid}[https://www.meilisearch.com/docs/reference/api/keys#uid] or
263 | # a hash of the uid and the master key that is the key's
264 | # {key}[https://www.meilisearch.com/docs/reference/api/keys#key] field
265 | def delete_key(uid_or_key)
266 | http_delete "/keys/#{uid_or_key}"
267 | end
268 |
269 | ### HEALTH
270 |
271 | # Check if Meilisearch instance is healthy.
272 | #
273 | # @see #health
274 | # @return [bool] whether or not the +/health+ endpoint raises any errors
275 | def healthy?
276 | http_get '/health'
277 | true
278 | rescue StandardError
279 | false
280 | end
281 |
282 | # Check health of Meilisearch instance.
283 | #
284 | # @see https://www.meilisearch.com/docs/reference/api/health#get-health Meilisearch API reference
285 | # @return [Hash{String => Object}] the health report from the Meilisearch instance
286 | def health
287 | http_get '/health'
288 | end
289 |
290 | ### STATS
291 |
292 | # Check version of Meilisearch server
293 | #
294 | # @see https://www.meilisearch.com/docs/reference/api/version#get-version-of-meilisearch Meilisearch API reference
295 | # @return [Hash{String => String}] package version and last commit of Meilisearch server, see
296 | # {version object}[https://www.meilisearch.com/docs/reference/api/version#version-object]
297 | def version
298 | http_get '/version'
299 | end
300 |
301 | # Get stats of all indexes in instance.
302 | #
303 | # @see Index#stats
304 | # @see https://www.meilisearch.com/docs/reference/api/stats#get-stats-of-all-indexes Meilisearch API reference
305 | # @return [Hash{String => Object}] see {stats object}[https://www.meilisearch.com/docs/reference/api/stats#stats-object]
306 | def stats
307 | http_get '/stats'
308 | end
309 |
310 | ### DUMPS
311 |
312 | # Create a database dump.
313 | #
314 | # Dumps are "blueprints" which can be used to restore your database. Restoring
315 | # a dump requires reindexing all documents and is therefore inefficient.
316 | #
317 | # Dumps are created by the Meilisearch server in the directory where the server is started
318 | # under +dumps/+ by default.
319 | #
320 | # @see https://www.meilisearch.com/docs/learn/advanced/snapshots_vs_dumps
321 | # The difference between snapshots and dumps
322 | # @see https://www.meilisearch.com/docs/learn/advanced/dumps
323 | # Meilisearch documentation on how to use dumps
324 | # @see https://www.meilisearch.com/docs/reference/api/dump#create-a-dump
325 | # Meilisearch API reference
326 | # @return [Models::Task] the async task that is creating the dump
327 | def create_dump
328 | response = http_post '/dumps'
329 | Models::Task.new(response, task_endpoint)
330 | end
331 |
332 | ### SNAPSHOTS
333 |
334 | # Create a database snapshot.
335 | #
336 | # Snapshots are exact copies of the Meilisearch database. As such they are pre-indexed
337 | # and restoring one is a very efficient operation.
338 | #
339 | # Snapshots are not compatible between Meilisearch versions. Snapshot creation takes priority
340 | # over other tasks.
341 | #
342 | # Snapshots are created by the Meilisearch server in the directory where the server is started
343 | # under +snapshots/+ by default.
344 | #
345 | # @see https://www.meilisearch.com/docs/learn/advanced/snapshots_vs_dumps
346 | # The difference between snapshots and dumps
347 | # @see https://www.meilisearch.com/docs/learn/advanced/snapshots
348 | # Meilisearch documentation on how to use snapshots
349 | # @see https://www.meilisearch.com/docs/reference/api/snapshots#create-a-snapshot
350 | # Meilisearch API reference
351 | # @return [Models::Task] the async task that is creating the snapshot
352 | def create_snapshot
353 | http_post '/snapshots'
354 | end
355 |
356 | ### TASKS
357 |
358 | # Cancel tasks matching the filter.
359 | #
360 | # This route is meant to be used with options, please see the API reference.
361 | #
362 | # Operations in Meilisearch are done asynchronously using "tasks".
363 | # Tasks report their progress and status.
364 | #
365 | # Warning: This does not return instances of {Models::Task}. This is a raw
366 | # call to the Meilisearch API and the return is not modified.
367 | #
368 | # @see https://www.meilisearch.com/docs/reference/api/tasks#task-object The Task Object
369 | # @see https://www.meilisearch.com/docs/reference/api/tasks#cancel-tasks Meilisearch API reference
370 | # @param options [Hash{Symbol => Object}] task search options as snake cased symbols, see the API reference
371 | # @return [Hash{String => Object}] a Meilisearch task that is canceling other tasks
372 | def cancel_tasks(options = {})
373 | task_endpoint.cancel_tasks(options)
374 | end
375 |
376 | # Cancel tasks matching the filter.
377 | #
378 | # This route is meant to be used with options, please see the API reference.
379 | #
380 | # Operations in Meilisearch are done asynchronously using "tasks".
381 | # Tasks report their progress and status.
382 | #
383 | # Warning: This does not return instances of {Models::Task}. This is a raw
384 | # call to the Meilisearch API and the return is not modified.
385 | #
386 | # Tasks are run in batches, see {#batches}.
387 | #
388 | # @see https://www.meilisearch.com/docs/reference/api/tasks#task-object The Task Object
389 | # @see https://www.meilisearch.com/docs/reference/api/tasks#cancel-tasks Meilisearch API reference
390 | # @param options [Hash{Symbol => Object}] task search options as snake cased symbols, see the API reference
391 | # @return [Hash{String => Object}] a Meilisearch task that is canceling other tasks
392 | def delete_tasks(options = {})
393 | task_endpoint.delete_tasks(options)
394 | end
395 |
396 | # Get Meilisearch tasks matching the filters.
397 | #
398 | # Operations in Meilisearch are done asynchronously using "tasks".
399 | # Tasks report their progress and status.
400 | #
401 | # Warning: This does not return instances of {Models::Task}. This is a raw
402 | # call to the Meilisearch API and the return is not modified.
403 | #
404 | # @see https://www.meilisearch.com/docs/reference/api/tasks#task-object The Task Object
405 | # @see https://www.meilisearch.com/docs/reference/api/tasks#get-tasks Meilisearch API reference
406 | # @param options [Hash{Symbol => Object}] task search options as snake cased symbols, see the API reference
407 | # @return [Hash{String => Object}] results of the task search, see API reference
408 | def tasks(options = {})
409 | task_endpoint.task_list(options)
410 | end
411 |
412 | # Get one task.
413 | #
414 | # Operations in Meilisearch are done asynchronously using "tasks".
415 | # Tasks report their progress and status.
416 | #
417 | # Warning: This does not return instances of {Models::Task}. This is a raw
418 | # call to the Meilisearch API and the return is not modified.
419 | #
420 | # @see https://www.meilisearch.com/docs/reference/api/tasks#task-object The Task Object
421 | # @see https://www.meilisearch.com/docs/reference/api/tasks#get-one-task Meilisearch API reference
422 | # @param task_uid [String] uid of the requested task
423 | # @return [Hash{String => Object}] a Meilisearch task object (see above)
424 | def task(task_uid)
425 | task_endpoint.task(task_uid)
426 | end
427 |
428 | # Wait for a task in a busy loop.
429 | #
430 | # Try to avoid using it. Wrapper around {Task#wait_for_task}.
431 | # @see Task#wait_for_task
432 | def wait_for_task(
433 | task_uid,
434 | timeout_in_ms = Models::Task.default_timeout_ms,
435 | interval_in_ms = Models::Task.default_interval_ms
436 | )
437 | task_endpoint.wait_for_task(task_uid, timeout_in_ms, interval_in_ms)
438 | end
439 |
440 | ### BATCHES
441 |
442 | # Get Meilisearch task batches matching the filters.
443 | #
444 | # Operations in Meilisearch are done asynchronously using "tasks".
445 | # Tasks are run in batches.
446 | #
447 | # @see https://www.meilisearch.com/docs/reference/api/batches#batch-object The Batch Object
448 | # @see https://www.meilisearch.com/docs/reference/api/batches#get-batches Meilisearch API reference
449 | # @param options [Hash{Symbol => Object}] task search options as snake cased symbols, see the API reference
450 | # @return [Hash{String => Object}] results of the batches search, see API reference
451 | def batches(options = {})
452 | http_get '/batches', options
453 | end
454 |
455 | # Get a single Meilisearch task batch matching +batch_uid+.
456 | #
457 | # Operations in Meilisearch are done asynchronously using "tasks".
458 | # Tasks are run in batches.
459 | #
460 | # @see https://www.meilisearch.com/docs/reference/api/batches#batch-object The Batch Object
461 | # @see https://www.meilisearch.com/docs/reference/api/batches#get-one-batch Meilisearch API reference
462 | # @param batch_uid [String] the uid of the request batch
463 | # @return [Hash{String => Object}] a batch object, see above
464 | def batch(batch_uid)
465 | http_get "/batches/#{batch_uid}"
466 | end
467 |
468 | ### EXPERIMENTAL FEATURES
469 |
470 | def experimental_features
471 | http_get '/experimental-features'
472 | end
473 |
474 | def update_experimental_features(expe_feat_changes)
475 | expe_feat_changes = Utils.transform_attributes(expe_feat_changes)
476 | http_patch '/experimental-features', expe_feat_changes
477 | end
478 |
479 | private
480 |
481 | def index_object(uid, primary_key = nil)
482 | Index.new(uid, @base_url, @api_key, primary_key, @options)
483 | end
484 |
485 | def task_endpoint
486 | @task_endpoint ||= Task.new(@base_url, @api_key, @options)
487 | end
488 | end
489 | end
490 |
--------------------------------------------------------------------------------