typesense 0.2.0 → 0.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +2 -0
- data/.rubocop.yml +22 -0
- data/.rubocop_todo.yml +17 -41
- data/README.md +8 -0
- data/codecov.yml +10 -0
- data/examples/aliases.rb +10 -32
- data/examples/client_initialization.rb +67 -0
- data/examples/collections_and_documents.rb +36 -59
- data/examples/keys.rb +127 -0
- data/examples/overrides.rb +13 -34
- data/examples/search.rb +17 -44
- data/lib/typesense.rb +4 -0
- data/lib/typesense/alias.rb +5 -5
- data/lib/typesense/aliases.rb +5 -5
- data/lib/typesense/api_call.rb +184 -76
- data/lib/typesense/client.rb +10 -3
- data/lib/typesense/collection.rb +7 -7
- data/lib/typesense/collections.rb +6 -6
- data/lib/typesense/configuration.rb +35 -19
- data/lib/typesense/debug.rb +3 -3
- data/lib/typesense/document.rb +4 -4
- data/lib/typesense/documents.rb +11 -6
- data/lib/typesense/error.rb +6 -0
- data/lib/typesense/health.rb +15 -0
- data/lib/typesense/key.rb +24 -0
- data/lib/typesense/keys.rb +34 -0
- data/lib/typesense/metrics.rb +15 -0
- data/lib/typesense/override.rb +4 -4
- data/lib/typesense/overrides.rb +5 -5
- data/lib/typesense/version.rb +1 -1
- data/typesense.gemspec +7 -6
- metadata +35 -14
data/examples/keys.rb
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
##
|
|
4
|
+
# These examples walk you through operations to manage API Keys
|
|
5
|
+
|
|
6
|
+
require_relative './client_initialization'
|
|
7
|
+
|
|
8
|
+
# Let's setup some test data for this example
|
|
9
|
+
schema = {
|
|
10
|
+
'name' => 'users',
|
|
11
|
+
'fields' => [
|
|
12
|
+
{
|
|
13
|
+
'name' => 'company_id',
|
|
14
|
+
'type' => 'int32',
|
|
15
|
+
'facet' => false
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
'name' => 'user_name',
|
|
19
|
+
'type' => 'string',
|
|
20
|
+
'facet' => false
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
'name' => 'login_count',
|
|
24
|
+
'type' => 'int32',
|
|
25
|
+
'facet' => false
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
'name' => 'country',
|
|
29
|
+
'type' => 'string',
|
|
30
|
+
'facet' => true
|
|
31
|
+
}
|
|
32
|
+
],
|
|
33
|
+
'default_sorting_field' => 'company_id'
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
# We have four users, belonging to two companies: 124 and 126
|
|
37
|
+
documents = [
|
|
38
|
+
{
|
|
39
|
+
'company_id' => 124,
|
|
40
|
+
'user_name' => 'Hilary Bradford',
|
|
41
|
+
'login_count' => 10,
|
|
42
|
+
'country' => 'USA'
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
'company_id' => 124,
|
|
46
|
+
'user_name' => 'Nile Carty',
|
|
47
|
+
'login_count' => 100,
|
|
48
|
+
'country' => 'USA'
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
'company_id' => 126,
|
|
52
|
+
'user_name' => 'Tahlia Maxwell',
|
|
53
|
+
'login_count' => 1,
|
|
54
|
+
'country' => 'France'
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
'company_id' => 126,
|
|
58
|
+
'user_name' => 'Karl Roy',
|
|
59
|
+
'login_count' => 2,
|
|
60
|
+
'country' => 'Germany'
|
|
61
|
+
}
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
# Delete if the collection already exists from a previous example run
|
|
65
|
+
begin
|
|
66
|
+
@typesense.collections['users'].delete
|
|
67
|
+
rescue Typesense::Error::ObjectNotFound
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# create a collection
|
|
71
|
+
@typesense.collections.create(schema)
|
|
72
|
+
|
|
73
|
+
# Index documents
|
|
74
|
+
documents.each do |document|
|
|
75
|
+
@typesense.collections['users'].documents.create(document)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Generate an API key and restrict it to only allow searches
|
|
79
|
+
# You want to use this API Key in the browser instead of the master API Key
|
|
80
|
+
unscoped_search_only_api_key_response = @typesense.keys.create({
|
|
81
|
+
'description' => 'Search-only key.',
|
|
82
|
+
'actions' => ['documents:search'],
|
|
83
|
+
'collections' => ['*']
|
|
84
|
+
})
|
|
85
|
+
ap unscoped_search_only_api_key_response
|
|
86
|
+
|
|
87
|
+
# Save the key returned, since this will be the only time the full API Key is returned, for security purposes
|
|
88
|
+
unscoped_search_only_api_key = unscoped_search_only_api_key_response['value']
|
|
89
|
+
|
|
90
|
+
# Side note: you can also retrieve metadata of API keys using the ID returned in the above response
|
|
91
|
+
unscoped_search_only_api_key_response = @typesense.keys[unscoped_search_only_api_key_response['id']].retrieve
|
|
92
|
+
ap unscoped_search_only_api_key_response
|
|
93
|
+
|
|
94
|
+
# We'll now use this search-only API key to generate a scoped search API key that can only access documents that have company_id:124
|
|
95
|
+
# This is useful when you store multi-tenant data in a single Typesense server, but you only want
|
|
96
|
+
# a particular tenant to access their own data. You'd generate one scoped search key per tenant.
|
|
97
|
+
# IMPORTANT: scoped search keys should only be generated *server-side*, so as to not leak the unscoped main search key to clients
|
|
98
|
+
scoped_search_only_api_key = @typesense.keys.generate_scoped_search_key(unscoped_search_only_api_key, { 'filter_by': 'company_id:124' })
|
|
99
|
+
ap "scoped_search_only_api_key: #{scoped_search_only_api_key}"
|
|
100
|
+
|
|
101
|
+
# Now let's search the data using the scoped API Key for company_id:124
|
|
102
|
+
# You can do searches with this scoped_search_only_api_key from the server-side or client-side
|
|
103
|
+
scoped_typesense_client = Typesense::Client.new({
|
|
104
|
+
'nodes': [{
|
|
105
|
+
'host': 'localhost',
|
|
106
|
+
'port': '8108',
|
|
107
|
+
'protocol': 'http'
|
|
108
|
+
}],
|
|
109
|
+
'api_key': scoped_search_only_api_key
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
search_results = scoped_typesense_client.collections['users'].documents.search({
|
|
113
|
+
'q' => 'Hilary',
|
|
114
|
+
'query_by' => 'user_name'
|
|
115
|
+
})
|
|
116
|
+
ap search_results
|
|
117
|
+
|
|
118
|
+
# Search for a user that exists, but is outside the current key's scope
|
|
119
|
+
search_results = scoped_typesense_client.collections['users'].documents.search({
|
|
120
|
+
'q': 'Maxwell',
|
|
121
|
+
'query_by': 'user_name'
|
|
122
|
+
})
|
|
123
|
+
ap search_results # Will return empty result set
|
|
124
|
+
|
|
125
|
+
# Now let's delete the unscoped_search_only_api_key. You'd want to do this when you need to rotate keys for example.
|
|
126
|
+
results = @typesense.keys[unscoped_search_only_api_key_response['id']].delete
|
|
127
|
+
ap results
|
data/examples/overrides.rb
CHANGED
|
@@ -3,30 +3,9 @@
|
|
|
3
3
|
##
|
|
4
4
|
# These examples walk you through operations specifically related to overrides
|
|
5
5
|
# This is a Typesense Premium feature (see: https://typesense.org/premium)
|
|
6
|
+
# Be sure to add `--license-key=<>` as a parameter, when starting a Typesense Premium server
|
|
6
7
|
|
|
7
|
-
require_relative '
|
|
8
|
-
require 'awesome_print'
|
|
9
|
-
|
|
10
|
-
AwesomePrint.defaults = {
|
|
11
|
-
indent: -2
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
##
|
|
15
|
-
# Setup
|
|
16
|
-
#
|
|
17
|
-
# Start the master
|
|
18
|
-
# $ docker run -p 8108:8108 -it -v/tmp/typesense-data-master/:/data -it typesense/typesense:0.8.0-rc1 --data-dir /data --api-key=abcd --listen-port 8108 --license-key=<>
|
|
19
|
-
|
|
20
|
-
##
|
|
21
|
-
# Create a client
|
|
22
|
-
typesense = Typesense::Client.new(
|
|
23
|
-
master_node: {
|
|
24
|
-
host: 'localhost',
|
|
25
|
-
port: 8108,
|
|
26
|
-
protocol: 'http',
|
|
27
|
-
api_key: 'abcd'
|
|
28
|
-
}
|
|
29
|
-
)
|
|
8
|
+
require_relative './client_initialization'
|
|
30
9
|
|
|
31
10
|
##
|
|
32
11
|
# Create a collection
|
|
@@ -50,31 +29,31 @@ schema = {
|
|
|
50
29
|
'default_sorting_field' => 'num_employees'
|
|
51
30
|
}
|
|
52
31
|
|
|
53
|
-
typesense.collections.create(schema)
|
|
32
|
+
@typesense.collections.create(schema)
|
|
54
33
|
|
|
55
34
|
# Let's create a couple documents for us to use in our search examples
|
|
56
|
-
typesense.collections['companies'].documents.create(
|
|
35
|
+
@typesense.collections['companies'].documents.create(
|
|
57
36
|
'id' => '124',
|
|
58
37
|
'company_name' => 'Stark Industries',
|
|
59
38
|
'num_employees' => 5215,
|
|
60
39
|
'country' => 'USA'
|
|
61
40
|
)
|
|
62
41
|
|
|
63
|
-
typesense.collections['companies'].documents.create(
|
|
42
|
+
@typesense.collections['companies'].documents.create(
|
|
64
43
|
'id' => '127',
|
|
65
44
|
'company_name' => 'Stark Corp',
|
|
66
45
|
'num_employees' => 1031,
|
|
67
46
|
'country' => 'USA'
|
|
68
47
|
)
|
|
69
48
|
|
|
70
|
-
typesense.collections['companies'].documents.create(
|
|
49
|
+
@typesense.collections['companies'].documents.create(
|
|
71
50
|
'id' => '125',
|
|
72
51
|
'company_name' => 'Acme Corp',
|
|
73
52
|
'num_employees' => 1002,
|
|
74
53
|
'country' => 'France'
|
|
75
54
|
)
|
|
76
55
|
|
|
77
|
-
typesense.collections['companies'].documents.create(
|
|
56
|
+
@typesense.collections['companies'].documents.create(
|
|
78
57
|
'id' => '126',
|
|
79
58
|
'company_name' => 'Doofenshmirtz Inc',
|
|
80
59
|
'num_employees' => 2,
|
|
@@ -84,7 +63,7 @@ typesense.collections['companies'].documents.create(
|
|
|
84
63
|
##
|
|
85
64
|
# Create overrides
|
|
86
65
|
|
|
87
|
-
typesense.collections['companies'].overrides.create(
|
|
66
|
+
@typesense.collections['companies'].overrides.create(
|
|
88
67
|
"id": 'promote-doofenshmirtz',
|
|
89
68
|
"rule": {
|
|
90
69
|
"query": 'doofen',
|
|
@@ -92,7 +71,7 @@ typesense.collections['companies'].overrides.create(
|
|
|
92
71
|
},
|
|
93
72
|
"includes": [{ 'id' => '126', 'position' => 1 }]
|
|
94
73
|
)
|
|
95
|
-
typesense.collections['companies'].overrides.create(
|
|
74
|
+
@typesense.collections['companies'].overrides.create(
|
|
96
75
|
"id": 'promote-acme',
|
|
97
76
|
"rule": {
|
|
98
77
|
"query": 'stark',
|
|
@@ -103,19 +82,19 @@ typesense.collections['companies'].overrides.create(
|
|
|
103
82
|
|
|
104
83
|
##
|
|
105
84
|
# Search for documents
|
|
106
|
-
results = typesense.collections['companies'].documents.search(
|
|
85
|
+
results = @typesense.collections['companies'].documents.search(
|
|
107
86
|
'q' => 'doofen',
|
|
108
87
|
'query_by' => 'company_name'
|
|
109
88
|
)
|
|
110
89
|
ap results
|
|
111
90
|
|
|
112
|
-
results = typesense.collections['companies'].documents.search(
|
|
91
|
+
results = @typesense.collections['companies'].documents.search(
|
|
113
92
|
'q' => 'stark',
|
|
114
93
|
'query_by' => 'company_name'
|
|
115
94
|
)
|
|
116
95
|
ap results
|
|
117
96
|
|
|
118
|
-
results = typesense.collections['companies'].documents.search(
|
|
97
|
+
results = @typesense.collections['companies'].documents.search(
|
|
119
98
|
'q' => 'Inc',
|
|
120
99
|
'query_by' => 'company_name',
|
|
121
100
|
'filter_by' => 'num_employees:<100',
|
|
@@ -126,4 +105,4 @@ ap results
|
|
|
126
105
|
##
|
|
127
106
|
# Cleanup
|
|
128
107
|
# Drop the collection
|
|
129
|
-
typesense.collections['companies'].delete
|
|
108
|
+
@typesense.collections['companies'].delete
|
data/examples/search.rb
CHANGED
|
@@ -3,41 +3,7 @@
|
|
|
3
3
|
##
|
|
4
4
|
# These examples walk you through operations specifically related to search
|
|
5
5
|
|
|
6
|
-
require_relative '
|
|
7
|
-
require 'awesome_print'
|
|
8
|
-
|
|
9
|
-
AwesomePrint.defaults = {
|
|
10
|
-
indent: -2
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
##
|
|
14
|
-
# Setup
|
|
15
|
-
#
|
|
16
|
-
# Start the master
|
|
17
|
-
# $ docker run -p 8108:8108 -it -v/tmp/typesense-data-master/:/data -it typesense/typesense:0.8.0-rc1 --data-dir /data --api-key=abcd --listen-port 8108
|
|
18
|
-
#
|
|
19
|
-
# Start the read replica
|
|
20
|
-
# $ docker run -p 8109:8109 -it -v/tmp/typesense-data-read-replica-1/:/data -it typesense/typesense:0.8.0-rc1 --data-dir /data --api-key=wxyz --listen-port 8109 --master http://localhost:8108
|
|
21
|
-
|
|
22
|
-
##
|
|
23
|
-
# Create a client
|
|
24
|
-
typesense = Typesense::Client.new(
|
|
25
|
-
master_node: {
|
|
26
|
-
host: 'localhost',
|
|
27
|
-
port: 8108,
|
|
28
|
-
protocol: 'http',
|
|
29
|
-
api_key: 'abcd'
|
|
30
|
-
},
|
|
31
|
-
read_replica_nodes: [
|
|
32
|
-
{
|
|
33
|
-
host: 'localhost',
|
|
34
|
-
port: 8109,
|
|
35
|
-
protocol: 'http',
|
|
36
|
-
api_key: 'wxyz'
|
|
37
|
-
}
|
|
38
|
-
],
|
|
39
|
-
timeout_seconds: 10
|
|
40
|
-
)
|
|
6
|
+
require_relative './client_initialization'
|
|
41
7
|
|
|
42
8
|
##
|
|
43
9
|
# Create a collection
|
|
@@ -61,31 +27,38 @@ schema = {
|
|
|
61
27
|
'default_sorting_field' => 'num_employees'
|
|
62
28
|
}
|
|
63
29
|
|
|
64
|
-
|
|
30
|
+
# Delete the collection if it already exists
|
|
31
|
+
begin
|
|
32
|
+
@typesense.collections['companies'].delete
|
|
33
|
+
rescue Typesense::Error::ObjectNotFound
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Now create the collection
|
|
37
|
+
@typesense.collections.create(schema)
|
|
65
38
|
|
|
66
39
|
# Let's create a couple documents for us to use in our search examples
|
|
67
|
-
typesense.collections['companies'].documents.create(
|
|
40
|
+
@typesense.collections['companies'].documents.create(
|
|
68
41
|
'id' => '124',
|
|
69
42
|
'company_name' => 'Stark Industries',
|
|
70
43
|
'num_employees' => 5215,
|
|
71
44
|
'country' => 'USA'
|
|
72
45
|
)
|
|
73
46
|
|
|
74
|
-
typesense.collections['companies'].documents.create(
|
|
47
|
+
@typesense.collections['companies'].documents.create(
|
|
75
48
|
'id' => '127',
|
|
76
49
|
'company_name' => 'Stark Corp',
|
|
77
50
|
'num_employees' => 1031,
|
|
78
51
|
'country' => 'USA'
|
|
79
52
|
)
|
|
80
53
|
|
|
81
|
-
typesense.collections['companies'].documents.create(
|
|
54
|
+
@typesense.collections['companies'].documents.create(
|
|
82
55
|
'id' => '125',
|
|
83
56
|
'company_name' => 'Acme Corp',
|
|
84
57
|
'num_employees' => 1002,
|
|
85
58
|
'country' => 'France'
|
|
86
59
|
)
|
|
87
60
|
|
|
88
|
-
typesense.collections['companies'].documents.create(
|
|
61
|
+
@typesense.collections['companies'].documents.create(
|
|
89
62
|
'id' => '126',
|
|
90
63
|
'company_name' => 'Doofenshmirtz Inc',
|
|
91
64
|
'num_employees' => 2,
|
|
@@ -94,7 +67,7 @@ typesense.collections['companies'].documents.create(
|
|
|
94
67
|
|
|
95
68
|
##
|
|
96
69
|
# Search for documents
|
|
97
|
-
results = typesense.collections['companies'].documents.search(
|
|
70
|
+
results = @typesense.collections['companies'].documents.search(
|
|
98
71
|
'q' => 'Stark',
|
|
99
72
|
'query_by' => 'company_name'
|
|
100
73
|
)
|
|
@@ -133,7 +106,7 @@ ap results
|
|
|
133
106
|
|
|
134
107
|
##
|
|
135
108
|
# Search for more documents
|
|
136
|
-
results = typesense.collections['companies'].documents.search(
|
|
109
|
+
results = @typesense.collections['companies'].documents.search(
|
|
137
110
|
'q' => 'Inc',
|
|
138
111
|
'query_by' => 'company_name',
|
|
139
112
|
'filter_by' => 'num_employees:<100',
|
|
@@ -163,7 +136,7 @@ ap results
|
|
|
163
136
|
|
|
164
137
|
##
|
|
165
138
|
# Search for more documents
|
|
166
|
-
results = typesense.collections['companies'].documents.search(
|
|
139
|
+
results = @typesense.collections['companies'].documents.search(
|
|
167
140
|
'q' => 'Non-existent',
|
|
168
141
|
'query_by' => 'company_name'
|
|
169
142
|
)
|
|
@@ -179,4 +152,4 @@ ap results
|
|
|
179
152
|
##
|
|
180
153
|
# Cleanup
|
|
181
154
|
# Drop the collection
|
|
182
|
-
typesense.collections['companies'].delete
|
|
155
|
+
@typesense.collections['companies'].delete
|
data/lib/typesense.rb
CHANGED
|
@@ -15,5 +15,9 @@ require_relative 'typesense/overrides'
|
|
|
15
15
|
require_relative 'typesense/override'
|
|
16
16
|
require_relative 'typesense/aliases'
|
|
17
17
|
require_relative 'typesense/alias'
|
|
18
|
+
require_relative 'typesense/keys'
|
|
19
|
+
require_relative 'typesense/key'
|
|
18
20
|
require_relative 'typesense/debug'
|
|
21
|
+
require_relative 'typesense/health'
|
|
22
|
+
require_relative 'typesense/metrics'
|
|
19
23
|
require_relative 'typesense/error'
|
data/lib/typesense/alias.rb
CHANGED
|
@@ -2,17 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
module Typesense
|
|
4
4
|
class Alias
|
|
5
|
-
def initialize(
|
|
6
|
-
@
|
|
7
|
-
@
|
|
5
|
+
def initialize(name, api_call)
|
|
6
|
+
@name = name
|
|
7
|
+
@api_call = api_call
|
|
8
8
|
end
|
|
9
9
|
|
|
10
10
|
def retrieve
|
|
11
|
-
|
|
11
|
+
@api_call.get(endpoint_path)
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
def delete
|
|
15
|
-
|
|
15
|
+
@api_call.delete(endpoint_path)
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
private
|
data/lib/typesense/aliases.rb
CHANGED
|
@@ -4,21 +4,21 @@ module Typesense
|
|
|
4
4
|
class Aliases
|
|
5
5
|
RESOURCE_PATH = '/aliases'
|
|
6
6
|
|
|
7
|
-
def initialize(
|
|
8
|
-
@
|
|
7
|
+
def initialize(api_call)
|
|
8
|
+
@api_call = api_call
|
|
9
9
|
@aliases = {}
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
def upsert(alias_name, mapping)
|
|
13
|
-
|
|
13
|
+
@api_call.put(endpoint_path(alias_name), mapping)
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
def retrieve
|
|
17
|
-
|
|
17
|
+
@api_call.get(RESOURCE_PATH)
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
def [](alias_name)
|
|
21
|
-
@aliases[alias_name] ||= Alias.new(
|
|
21
|
+
@aliases[alias_name] ||= Alias.new(alias_name, @api_call)
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
private
|
data/lib/typesense/api_call.rb
CHANGED
|
@@ -10,116 +10,224 @@ module Typesense
|
|
|
10
10
|
|
|
11
11
|
def initialize(configuration)
|
|
12
12
|
@configuration = configuration
|
|
13
|
+
|
|
14
|
+
@api_key = @configuration.api_key
|
|
15
|
+
@nodes = @configuration.nodes.dup # Make a copy, since we'll be adding additional metadata to the nodes
|
|
16
|
+
@nearest_node = @configuration.nearest_node.dup
|
|
17
|
+
@connection_timeout_seconds = @configuration.connection_timeout_seconds
|
|
18
|
+
@healthcheck_interval_seconds = @configuration.healthcheck_interval_seconds
|
|
19
|
+
@num_retries_per_request = @configuration.num_retries
|
|
20
|
+
@retry_interval_seconds = @configuration.retry_interval_seconds
|
|
21
|
+
|
|
22
|
+
@logger = @configuration.logger
|
|
23
|
+
|
|
24
|
+
initialize_metadata_for_nodes
|
|
25
|
+
@current_node_index = -1
|
|
13
26
|
end
|
|
14
27
|
|
|
15
28
|
def post(endpoint, parameters = {})
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
end.parsed_response
|
|
29
|
+
headers, body = extract_headers_and_body_from(parameters)
|
|
30
|
+
|
|
31
|
+
perform_request :post,
|
|
32
|
+
endpoint,
|
|
33
|
+
body: body,
|
|
34
|
+
headers: default_headers.merge(headers)
|
|
23
35
|
end
|
|
24
36
|
|
|
25
37
|
def put(endpoint, parameters = {})
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
end.parsed_response
|
|
38
|
+
headers, body = extract_headers_and_body_from(parameters)
|
|
39
|
+
|
|
40
|
+
perform_request :put,
|
|
41
|
+
endpoint,
|
|
42
|
+
body: body,
|
|
43
|
+
headers: default_headers.merge(headers)
|
|
33
44
|
end
|
|
34
45
|
|
|
35
46
|
def get(endpoint, parameters = {})
|
|
36
|
-
|
|
37
|
-
end
|
|
47
|
+
headers, query = extract_headers_and_query_from(parameters)
|
|
38
48
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
query: parameters,
|
|
44
|
-
headers: default_headers
|
|
45
|
-
))
|
|
46
|
-
end
|
|
49
|
+
perform_request :get,
|
|
50
|
+
endpoint,
|
|
51
|
+
query: query,
|
|
52
|
+
headers: default_headers.merge(headers)
|
|
47
53
|
end
|
|
48
54
|
|
|
49
55
|
def delete(endpoint, parameters = {})
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
56
|
+
headers, query = extract_headers_and_query_from(parameters)
|
|
57
|
+
|
|
58
|
+
perform_request :delete,
|
|
59
|
+
endpoint,
|
|
60
|
+
query: query,
|
|
61
|
+
headers: default_headers.merge(headers)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def perform_request(method, endpoint, options = {})
|
|
65
|
+
@configuration.validate!
|
|
66
|
+
last_exception = nil
|
|
67
|
+
@logger.debug "Performing #{method.to_s.upcase} request: #{endpoint}"
|
|
68
|
+
(1..(@num_retries_per_request + 1)).each do |num_tries|
|
|
69
|
+
node = next_node
|
|
70
|
+
|
|
71
|
+
@logger.debug "Attempting #{method.to_s.upcase} request Try ##{num_tries} to Node #{node[:index]}"
|
|
72
|
+
|
|
73
|
+
begin
|
|
74
|
+
response_object = self.class.send(method,
|
|
75
|
+
uri_for(endpoint, node),
|
|
76
|
+
default_options.merge(options))
|
|
77
|
+
response_code = response_object.response.code.to_i
|
|
78
|
+
set_node_healthcheck(node, is_healthy: true) if response_code >= 1 && response_code <= 499
|
|
79
|
+
|
|
80
|
+
@logger.debug "Request to Node #{node[:index]} was successfully made (at the network layer). Response Code was #{response_code}."
|
|
81
|
+
|
|
82
|
+
# If response is 2xx return the object, else raise the response as an exception
|
|
83
|
+
return response_object.parsed_response if response_object.response.code_type <= Net::HTTPSuccess # 2xx
|
|
84
|
+
|
|
85
|
+
exception_message = (response_object.parsed_response && response_object.parsed_response['message']) || 'Error'
|
|
86
|
+
raise custom_exception_klass_for(response_object.response), exception_message
|
|
87
|
+
rescue Net::ReadTimeout, Net::OpenTimeout,
|
|
88
|
+
EOFError, Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, Net::ProtocolError,
|
|
89
|
+
Errno::EINVAL, Errno::ENETDOWN, Errno::ENETUNREACH, Errno::ENETRESET, Errno::ECONNABORTED, Errno::ECONNRESET,
|
|
90
|
+
Errno::ETIMEDOUT, Errno::ECONNREFUSED, Errno::EHOSTDOWN, Errno::EHOSTUNREACH,
|
|
91
|
+
Timeout::Error, HTTParty::ResponseError, Typesense::Error::ServerError, Typesense::Error::HTTPStatus0Error => e
|
|
92
|
+
# Rescue network layer exceptions and HTTP 5xx errors, so the loop can continue.
|
|
93
|
+
# Using loops for retries instead of rescue...retry to maintain consistency with client libraries in
|
|
94
|
+
# other languages that might not support the same construct.
|
|
95
|
+
set_node_healthcheck(node, is_healthy: false)
|
|
96
|
+
last_exception = e
|
|
97
|
+
@logger.warn "Request to Node #{node[:index]} failed due to \"#{e.class}: #{e.message}\""
|
|
98
|
+
@logger.warn "Sleeping for #{@retry_interval_seconds}s and then retrying request..."
|
|
99
|
+
sleep @retry_interval_seconds
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
@logger.debug "No retries left. Raising last error \"#{last_exception.class}: #{last_exception.message}\"..."
|
|
103
|
+
raise last_exception
|
|
57
104
|
end
|
|
58
105
|
|
|
59
106
|
private
|
|
60
107
|
|
|
61
|
-
def
|
|
62
|
-
if
|
|
63
|
-
|
|
108
|
+
def extract_headers_and_body_from(parameters)
|
|
109
|
+
if json_request?(parameters)
|
|
110
|
+
headers = { 'Content-Type' => 'application/json' }
|
|
111
|
+
body = sanitize_parameters(parameters).to_json
|
|
64
112
|
else
|
|
65
|
-
|
|
113
|
+
headers = {}
|
|
114
|
+
body = parameters[:body]
|
|
66
115
|
end
|
|
116
|
+
[headers, body]
|
|
67
117
|
end
|
|
68
118
|
|
|
69
|
-
def
|
|
70
|
-
|
|
119
|
+
def extract_headers_and_query_from(parameters)
|
|
120
|
+
if json_request?(parameters)
|
|
121
|
+
headers = { 'Content-Type' => 'application/json' }
|
|
122
|
+
query = sanitize_parameters(parameters)
|
|
123
|
+
else
|
|
124
|
+
headers = {}
|
|
125
|
+
query = parameters[:query]
|
|
126
|
+
end
|
|
127
|
+
[headers, query]
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def json_request?(parameters)
|
|
131
|
+
parameters[:as_json].nil? ? true : parameters[:as_json]
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def sanitize_parameters(parameters)
|
|
135
|
+
sanitized_parameters = parameters.dup
|
|
136
|
+
sanitized_parameters.delete(:as_json)
|
|
137
|
+
sanitized_parameters.delete(:body)
|
|
138
|
+
sanitized_parameters.delete(:query)
|
|
71
139
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if ([:use_read_replicas, true].include? use_read_replicas) &&
|
|
103
|
-
!@configuration.read_replica_nodes.nil?
|
|
104
|
-
node = :read_replica
|
|
105
|
-
node_index += 1
|
|
106
|
-
|
|
107
|
-
retry unless @configuration.read_replica_nodes[node_index].nil?
|
|
140
|
+
sanitized_parameters
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def uri_for(endpoint, node)
|
|
144
|
+
"#{node[:protocol]}://#{node[:host]}:#{node[:port]}#{endpoint}"
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
## Attempts to find the next healthy node, looping through the list of nodes once.
|
|
148
|
+
# But if no healthy nodes are found, it will just return the next node, even if it's unhealthy
|
|
149
|
+
# so we can try the request for good measure, in case that node has become healthy since
|
|
150
|
+
def next_node
|
|
151
|
+
# Check if nearest_node is set and is healthy, if so return it
|
|
152
|
+
unless @nearest_node.nil?
|
|
153
|
+
@logger.debug "Nodes health: Node #{@nearest_node[:index]} is #{@nearest_node[:is_healthy] == true ? 'Healthy' : 'Unhealthy'}"
|
|
154
|
+
if @nearest_node[:is_healthy] == true || node_due_for_healthcheck?(@nearest_node)
|
|
155
|
+
@logger.debug "Updated current node to Node #{@nearest_node[:index]}"
|
|
156
|
+
return @nearest_node
|
|
157
|
+
end
|
|
158
|
+
@logger.debug 'Falling back to individual nodes'
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Fallback to nodes as usual
|
|
162
|
+
@logger.debug "Nodes health: #{@nodes.each_with_index.map { |node, i| "Node #{i} is #{node[:is_healthy] == true ? 'Healthy' : 'Unhealthy'}" }.join(' || ')}"
|
|
163
|
+
candidate_node = nil
|
|
164
|
+
(0..@nodes.length).each do |_i|
|
|
165
|
+
@current_node_index = (@current_node_index + 1) % @nodes.length
|
|
166
|
+
candidate_node = @nodes[@current_node_index]
|
|
167
|
+
if candidate_node[:is_healthy] == true || node_due_for_healthcheck?(candidate_node)
|
|
168
|
+
@logger.debug "Updated current node to Node #{candidate_node[:index]}"
|
|
169
|
+
return candidate_node
|
|
108
170
|
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# None of the nodes are marked healthy, but some of them could have become healthy since last health check.
|
|
174
|
+
# So we will just return the next node.
|
|
175
|
+
@logger.debug "No healthy nodes were found. Returning the next node, Node #{candidate_node[:index]}"
|
|
176
|
+
candidate_node
|
|
177
|
+
end
|
|
109
178
|
|
|
110
|
-
|
|
179
|
+
def node_due_for_healthcheck?(node)
|
|
180
|
+
is_due_for_check = Time.now.to_i - node[:last_access_timestamp] > @healthcheck_interval_seconds
|
|
181
|
+
@logger.debug "Node #{node[:index]} has exceeded healthcheck_interval_seconds of #{@healthcheck_interval_seconds}. Adding it back into rotation." if is_due_for_check
|
|
182
|
+
is_due_for_check
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def initialize_metadata_for_nodes
|
|
186
|
+
unless @nearest_node.nil?
|
|
187
|
+
@nearest_node[:index] = 'nearest_node'
|
|
188
|
+
set_node_healthcheck(@nearest_node, is_healthy: true)
|
|
189
|
+
end
|
|
190
|
+
@nodes.each_with_index do |node, index|
|
|
191
|
+
node[:index] = index
|
|
192
|
+
set_node_healthcheck(node, is_healthy: true)
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def set_node_healthcheck(node, is_healthy:)
|
|
197
|
+
node[:is_healthy] = is_healthy
|
|
198
|
+
node[:last_access_timestamp] = Time.now.to_i
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def custom_exception_klass_for(response)
|
|
202
|
+
response_code_type = response.code_type
|
|
203
|
+
if response_code_type <= Net::HTTPBadRequest # 400
|
|
204
|
+
Typesense::Error::RequestMalformed
|
|
205
|
+
elsif response_code_type <= Net::HTTPUnauthorized # 401
|
|
206
|
+
Typesense::Error::RequestUnauthorized
|
|
207
|
+
elsif response_code_type <= Net::HTTPNotFound # 404
|
|
208
|
+
Typesense::Error::ObjectNotFound
|
|
209
|
+
elsif response_code_type <= Net::HTTPConflict # 409
|
|
210
|
+
Typesense::Error::ObjectAlreadyExists
|
|
211
|
+
elsif response_code_type <= Net::HTTPUnprocessableEntity # 422
|
|
212
|
+
Typesense::Error::ObjectUnprocessable
|
|
213
|
+
elsif response_code_type <= Net::HTTPServerError # 5xx
|
|
214
|
+
Typesense::Error::ServerError
|
|
215
|
+
elsif response.code.to_i.zero?
|
|
216
|
+
Typesense::Error::HTTPStatus0Error
|
|
217
|
+
else
|
|
218
|
+
Typesense::Error::HTTPError
|
|
111
219
|
end
|
|
112
220
|
end
|
|
113
221
|
|
|
114
222
|
def default_options
|
|
115
223
|
{
|
|
116
|
-
timeout: @
|
|
224
|
+
timeout: @connection_timeout_seconds
|
|
117
225
|
}
|
|
118
226
|
end
|
|
119
227
|
|
|
120
228
|
def default_headers
|
|
121
229
|
{
|
|
122
|
-
API_KEY_HEADER_NAME.to_s => @
|
|
230
|
+
API_KEY_HEADER_NAME.to_s => @api_key
|
|
123
231
|
}
|
|
124
232
|
end
|
|
125
233
|
end
|