Skip to content

Commit 29a774a

Browse files
committed
Add support for upserts and update
1 parent 3dcb991 commit 29a774a

6 files changed

Lines changed: 125 additions & 98 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ Tests are also a good place to know how the the library works internally: [spec]
3333

3434
| Typesense Server | typesense-ruby |
3535
|------------------|----------------|
36-
| \>= v0.15.1 | \>= v0.8.0 |
36+
| \>= v0.16.0 | \>= v0.8.0 |
3737
| \>= v0.15.0 | \>= v0.7.0 |
3838
| \>= v0.12.1 | \>= v0.5.0 |
3939
| \>= v0.12.0 | \>= v0.4.0 |

lib/typesense/api_call.rb

Lines changed: 26 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -24,45 +24,33 @@ def initialize(configuration)
2424
@current_node_index = -1
2525
end
2626

27-
def post(endpoint, parameters = {})
28-
headers, query, body = split_post_put_parameters(parameters)
29-
27+
def post(endpoint, body_parameters = {}, query_parameters = {})
3028
perform_request :post,
3129
endpoint,
32-
headers,
33-
params: query,
34-
body: body
30+
query_parameters: query_parameters,
31+
body_parameters: body_parameters
3532
end
3633

37-
def put(endpoint, parameters = {})
38-
headers, query, body = split_post_put_parameters(parameters)
39-
34+
def put(endpoint, body_parameters = {}, query_parameters = {})
4035
perform_request :put,
4136
endpoint,
42-
headers,
43-
params: query,
44-
body: body
37+
query_parameters: query_parameters,
38+
body_parameters: body_parameters
4539
end
4640

47-
def get(endpoint, parameters = {})
48-
headers, query = extract_headers_and_query_from(parameters)
49-
41+
def get(endpoint, query_parameters = {})
5042
perform_request :get,
5143
endpoint,
52-
headers,
53-
params: query
44+
query_parameters: query_parameters
5445
end
5546

56-
def delete(endpoint, parameters = {})
57-
headers, query = extract_headers_and_query_from(parameters)
58-
47+
def delete(endpoint, query_parameters = {})
5948
perform_request :delete,
6049
endpoint,
61-
headers,
62-
params: query
50+
query_parameters: query_parameters
6351
end
6452

65-
def perform_request(method, endpoint, headers = {}, options = {})
53+
def perform_request(method, endpoint, query_parameters: nil, body_parameters: nil, additional_headers: {})
6654
@configuration.validate!
6755
last_exception = nil
6856
@logger.debug "Performing #{method.to_s.upcase} request: #{endpoint}"
@@ -72,12 +60,20 @@ def perform_request(method, endpoint, headers = {}, options = {})
7260
@logger.debug "Attempting #{method.to_s.upcase} request Try ##{num_tries} to Node #{node[:index]}"
7361

7462
begin
75-
response = Typhoeus::Request.new(uri_for(endpoint, node),
76-
{
77-
method: method,
78-
headers: default_headers.merge(headers),
79-
timeout: @connection_timeout_seconds
80-
}.merge(options)).run
63+
request_options = {
64+
method: method,
65+
timeout: @connection_timeout_seconds,
66+
headers: default_headers.merge(additional_headers)
67+
}
68+
request_options.merge!(params: query_parameters) unless query_parameters.nil?
69+
70+
unless body_parameters.nil?
71+
body = body_parameters
72+
body = Oj.dump(body_parameters) if request_options[:headers]['Content-Type'] == 'application/json'
73+
request_options.merge!(body: body)
74+
end
75+
76+
response = Typhoeus::Request.new(uri_for(endpoint, node), request_options).run
8177
set_node_healthcheck(node, is_healthy: true) if response.code >= 1 && response.code <= 499
8278

8379
@logger.debug "Request to Node #{node[:index]} was successfully made (at the network layer). Response Code was #{response.code}."
@@ -112,43 +108,6 @@ def perform_request(method, endpoint, headers = {}, options = {})
112108

113109
private
114110

115-
def split_post_put_parameters(parameters)
116-
if json_request?(parameters)
117-
headers = { 'Content-Type' => 'application/json' }
118-
query = parameters[:query]
119-
body = Oj.dump(sanitize_parameters(parameters))
120-
else
121-
headers = {}
122-
query = parameters[:query]
123-
body = parameters[:body]
124-
end
125-
[headers, query, body]
126-
end
127-
128-
def extract_headers_and_query_from(parameters)
129-
if json_request?(parameters)
130-
headers = { 'Content-Type' => 'application/json' }
131-
query = sanitize_parameters(parameters)
132-
else
133-
headers = {}
134-
query = parameters[:query]
135-
end
136-
[headers, query]
137-
end
138-
139-
def json_request?(parameters)
140-
parameters[:as_json].nil? ? true : parameters[:as_json]
141-
end
142-
143-
def sanitize_parameters(parameters)
144-
sanitized_parameters = parameters.dup
145-
sanitized_parameters.delete(:as_json)
146-
sanitized_parameters.delete(:body)
147-
sanitized_parameters.delete(:query)
148-
149-
sanitized_parameters
150-
end
151-
152111
def uri_for(endpoint, node)
153112
"#{node[:protocol]}://#{node[:host]}:#{node[:port]}#{endpoint}"
154113
end
@@ -231,6 +190,7 @@ def custom_exception_klass_for(response)
231190

232191
def default_headers
233192
{
193+
'Content-Type' => 'application/json',
234194
API_KEY_HEADER_NAME.to_s => @api_key,
235195
'User-Agent' => 'Typesense Ruby Client'
236196
}

lib/typesense/document.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ def delete
1616
@api_call.delete(endpoint_path)
1717
end
1818

19+
def update(partial_document)
20+
@api_call.put(endpoint_path, partial_document)
21+
end
22+
1923
private
2024

2125
def endpoint_path

lib/typesense/documents.rb

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,21 +12,24 @@ def initialize(collection_name, api_call)
1212
@documents = {}
1313
end
1414

15-
def create(document)
16-
@api_call.post(endpoint_path, document)
15+
def create(document, options = {})
16+
@api_call.post(endpoint_path, document, options)
1717
end
1818

19-
def create_many(documents)
19+
def create_many(documents, options = {})
2020
documents_in_jsonl_format = documents.map { |document| Oj.dump(document) }.join("\n")
21-
results_in_jsonl_format = import(documents_in_jsonl_format)
21+
results_in_jsonl_format = import(documents_in_jsonl_format, options)
2222
results_in_jsonl_format.split("\n").map { |r| Oj.load(r) }
2323
end
2424

25-
def import(documents_in_jsonl_format, query_parameters = {})
26-
@api_call.post(endpoint_path('import'),
27-
as_json: false,
28-
query: query_parameters,
29-
body: documents_in_jsonl_format)
25+
def import(documents_in_jsonl_format, options = {})
26+
@api_call.perform_request(
27+
'post',
28+
endpoint_path('import'),
29+
query_parameters: options,
30+
body_parameters: documents_in_jsonl_format,
31+
additional_headers: { 'Content-Type' => 'text/plain' }
32+
)
3033
end
3134

3235
def export

spec/typesense/document_spec.rb

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,25 @@
5757
end
5858
end
5959

60+
describe '#update' do
61+
it 'updates the specified document' do
62+
partial_document = {
63+
'id' => '124',
64+
'num_employees' => 5200
65+
}
66+
stub_request(:put, Typesense::ApiCall.new(typesense.configuration).send(:uri_for, '/collections/companies/documents/124', typesense.configuration.nodes[0]))
67+
.with(headers: {
68+
'Content-Type' => 'application/json',
69+
'X-Typesense-Api-Key' => typesense.configuration.api_key
70+
})
71+
.to_return(status: 200, body: JSON.dump(partial_document), headers: { 'Content-Type': 'application/json' })
72+
73+
result = document_124.update(partial_document)
74+
75+
expect(result).to eq(partial_document)
76+
end
77+
end
78+
6079
describe '#delete' do
6180
it 'deletes the specified document' do
6281
stub_request(:delete, Typesense::ApiCall.new(typesense.configuration).send(:uri_for, '/collections/companies/documents/124', typesense.configuration.nodes[0]))

spec/typesense/documents_spec.rb

Lines changed: 63 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -43,38 +43,79 @@
4343
end
4444

4545
describe '#create' do
46-
it 'creates creates/indexes a document and returns it' do
47-
stub_request(:post, Typesense::ApiCall.new(typesense.configuration).send(:uri_for, '/collections/companies/documents', typesense.configuration.nodes[0]))
48-
.with(body: document,
49-
headers: {
50-
'X-Typesense-Api-Key' => typesense.configuration.api_key,
51-
'Content-Type' => 'application/json'
52-
})
53-
.to_return(status: 200, body: JSON.dump(document), headers: { 'Content-Type': 'application/json' })
46+
context 'when no options are specified' do
47+
it 'creates creates/indexes a document and returns it' do
48+
stub_request(:post, Typesense::ApiCall.new(typesense.configuration).send(:uri_for, '/collections/companies/documents', typesense.configuration.nodes[0]))
49+
.with(body: document,
50+
headers: {
51+
'X-Typesense-Api-Key' => typesense.configuration.api_key,
52+
'Content-Type' => 'application/json'
53+
})
54+
.to_return(status: 200, body: JSON.dump(document), headers: { 'Content-Type': 'application/json' })
55+
56+
result = companies_documents.create(document)
57+
58+
expect(result).to eq(document)
59+
end
60+
end
5461

55-
result = companies_documents.create(document)
62+
context 'when an option is specified' do
63+
it 'creates creates/upserts a document and returns it' do
64+
stub_request(:post, Typesense::ApiCall.new(typesense.configuration).send(:uri_for, '/collections/companies/documents', typesense.configuration.nodes[0]))
65+
.with(body: document,
66+
headers: {
67+
'X-Typesense-Api-Key' => typesense.configuration.api_key,
68+
'Content-Type' => 'application/json'
69+
},
70+
query: {
71+
'upsert' => 'true'
72+
})
73+
.to_return(status: 200, body: JSON.dump(document), headers: { 'Content-Type': 'application/json' })
5674

57-
expect(result).to eq(document)
75+
result = companies_documents.create(document, upsert: true)
76+
77+
expect(result).to eq(document)
78+
end
5879
end
5980
end
6081

6182
describe '#create_many' do
62-
it 'creates creates/indexes documents in bulk' do
63-
stub_request(:post, Typesense::ApiCall.new(typesense.configuration).send(:uri_for, '/collections/companies/documents/import', typesense.configuration.nodes[0]))
64-
.with(body: "#{JSON.dump(document)}\n#{JSON.dump(document)}",
65-
headers: {
66-
'X-Typesense-Api-Key' => typesense.configuration.api_key
67-
})
68-
.to_return(status: 200, body: JSON.dump({ 'success' => true }), headers: { 'Content-Type': 'text/plain' })
83+
context 'when no options are specified' do
84+
it 'creates creates/indexes documents in bulk' do
85+
stub_request(:post, Typesense::ApiCall.new(typesense.configuration).send(:uri_for, '/collections/companies/documents/import', typesense.configuration.nodes[0]))
86+
.with(body: "#{JSON.dump(document)}\n#{JSON.dump(document)}",
87+
headers: {
88+
'X-Typesense-Api-Key' => typesense.configuration.api_key
89+
})
90+
.to_return(status: 200, body: JSON.dump({ 'success' => true }), headers: { 'Content-Type': 'text/plain' })
91+
92+
result = companies_documents.create_many([document, document])
93+
94+
expect(result).to eq([{ 'success' => true }])
95+
end
96+
end
97+
98+
context 'when an option is specified' do
99+
it 'creates creates/indexes documents in bulk, with the option' do
100+
stub_request(:post, Typesense::ApiCall.new(typesense.configuration).send(:uri_for, '/collections/companies/documents/import', typesense.configuration.nodes[0]))
101+
.with(body: "#{JSON.dump(document)}\n#{JSON.dump(document)}",
102+
headers: {
103+
'X-Typesense-Api-Key' => typesense.configuration.api_key
104+
},
105+
query: {
106+
'upsert' => 'true'
107+
})
108+
.to_return(status: 200, body: JSON.dump({ 'success' => true }), headers: { 'Content-Type': 'text/plain' })
69109

70-
result = companies_documents.create_many([document, document])
110+
result = companies_documents.create_many([document, document], upsert: true)
71111

72-
expect(result).to eq([{ 'success' => true }])
112+
expect(result).to eq([{ 'success' => true }])
113+
end
73114
end
74115
end
75116

76117
describe '#import' do
77-
context 'when upsert is not specified' do
118+
context 'when no options are specified' do
78119
it 'imports documents in JSONL format' do
79120
stub_request(:post, Typesense::ApiCall.new(typesense.configuration).send(:uri_for, '/collections/companies/documents/import', typesense.configuration.nodes[0]))
80121
.with(body: "#{JSON.dump(document)}\n#{JSON.dump(document)}",
@@ -89,8 +130,8 @@
89130
end
90131
end
91132

92-
context 'when upsert is true' do
93-
it 'imports documents in JSONL format, with upsert query parameter' do
133+
context 'when an option is specified' do
134+
it 'imports documents in JSONL format, with the option' do
94135
stub_request(:post, Typesense::ApiCall.new(typesense.configuration).send(:uri_for, '/collections/companies/documents/import', typesense.configuration.nodes[0]))
95136
.with(body: "#{JSON.dump(document)}\n#{JSON.dump(document)}",
96137
headers: {

0 commit comments

Comments
 (0)