typesense 0.1.0

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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 7938337742ce5b45e214069abd8f70bcabd0a247ad47dec68f5f48b43a1841c7
4
+ data.tar.gz: b71fd0346976e30ce637a42e16496b650bdfba80ba7e012f5d712c4a169decc2
5
+ SHA512:
6
+ metadata.gz: ee805aefbcea10f4275b5aac9efeace0e5bf422bd4d30911ea67223ce8e334264bb4731927a98aca7990fbe2890d7a18357e0c530d85f46d30ae8c5435766a7e
7
+ data.tar.gz: 546ba5bd5f554bad1c16e97e5cb3c37cf0945324f269b9b0273fb3e85ffc5b79c672bea80779b8347da2660b7db78f76d761fc90211c05470f581e0060a21bae
@@ -0,0 +1,67 @@
1
+ # Ruby CircleCI 2.0 configuration file
2
+ #
3
+ # Check https://circleci.com/docs/2.0/language-ruby/ for more details
4
+ #
5
+ version: 2
6
+ jobs:
7
+ build:
8
+ docker:
9
+ # specify the version you desire here
10
+ - image: circleci/ruby:2.4.1-node-browsers
11
+
12
+ # Specify service dependencies here if necessary
13
+ # CircleCI maintains a library of pre-built images
14
+ # documented at https://circleci.com/docs/2.0/circleci-images/
15
+ # - image: circleci/postgres:9.4
16
+
17
+ working_directory: ~/repo
18
+
19
+ steps:
20
+ - checkout
21
+
22
+ # # Download and cache dependencies
23
+ # - restore_cache:
24
+ # keys:
25
+ # - v1-dependencies-{{ checksum "Gemfile.lock" }}
26
+ # # fallback to using the latest cache if no exact match is found
27
+ # - v1-dependencies-
28
+
29
+ - run:
30
+ name: update bundler
31
+ command: |
32
+ gem install bundler
33
+
34
+ - run:
35
+ name: install dependencies
36
+ command: |
37
+ bundle install --jobs=4 --retry=3 --path vendor/bundle
38
+
39
+ # - save_cache:
40
+ # paths:
41
+ # - ./vendor/bundle
42
+ # key: v1-dependencies-{{ checksum "Gemfile.lock" }}
43
+
44
+ - run:
45
+ name: lint
46
+ command: |
47
+ rubocop
48
+
49
+ # run tests!
50
+ - run:
51
+ name: run tests
52
+ command: |
53
+ mkdir /tmp/test-results
54
+
55
+ bundle exec rspec --format documentation \
56
+ --format RspecJunitFormatter \
57
+ --out /tmp/test-results/rspec.xml
58
+
59
+ # collect reports
60
+ - store_test_results:
61
+ path: /tmp/test-results
62
+ - store_artifacts:
63
+ path: /tmp/test-results
64
+ destination: test-results
65
+ - store_artifacts:
66
+ path: coverage
67
+ destination: coverage
@@ -0,0 +1,15 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ Gemfile.lock
11
+ .ruby-version
12
+ .ruby-gemset
13
+
14
+ # rspec failure tracking
15
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,4 @@
1
+ --format documentation
2
+ --color
3
+ --order rand
4
+ --require spec_helper
@@ -0,0 +1,12 @@
1
+ inherit_from: .rubocop_todo.yml
2
+
3
+ require: rubocop-rspec
4
+
5
+ AllCops:
6
+ TargetRubyVersion: "2.4"
7
+
8
+ Metrics/BlockLength:
9
+ Exclude:
10
+ - 'spec/**/*'
11
+ - 'Gemfile'
12
+ - 'typesense.gemspec'
@@ -0,0 +1,66 @@
1
+ # This configuration was generated by
2
+ # `rubocop --auto-gen-config`
3
+ # on 2018-03-17 15:11:06 -0700 using RuboCop version 0.53.0.
4
+ # The point is for the user to remove these configuration records
5
+ # one by one as the offenses are removed from the code base.
6
+ # Note that changes in the inspected code, or installation of new
7
+ # versions of RuboCop, may require this file to be generated again.
8
+
9
+ # Offense count: 1
10
+ Lint/ShadowedException:
11
+ Exclude:
12
+ - 'lib/typesense/api_call.rb'
13
+
14
+ # Offense count: 3
15
+ Metrics/AbcSize:
16
+ Max: 37
17
+
18
+ # Offense count: 1
19
+ Metrics/CyclomaticComplexity:
20
+ Max: 13
21
+
22
+ # Offense count: 1
23
+ # Configuration parameters: CountComments.
24
+ Metrics/MethodLength:
25
+ Max: 35
26
+
27
+ # Offense count: 1
28
+ Metrics/PerceivedComplexity:
29
+ Max: 14
30
+
31
+ # Offense count: 13
32
+ # Configuration parameters: Max.
33
+ RSpec/ExampleLength:
34
+ Exclude:
35
+ - 'spec/typesense/api_call_spec.rb'
36
+ - 'spec/typesense/collection_spec.rb'
37
+ - 'spec/typesense/collections_spec.rb'
38
+ - 'spec/typesense/debug_spec.rb'
39
+ - 'spec/typesense/document_spec.rb'
40
+ - 'spec/typesense/documents_spec.rb'
41
+
42
+ # Offense count: 2
43
+ # Configuration parameters: AggregateFailuresByDefault.
44
+ RSpec/MultipleExpectations:
45
+ Max: 2
46
+
47
+ # Offense count: 9
48
+ Style/Documentation:
49
+ Exclude:
50
+ - 'spec/**/*'
51
+ - 'test/**/*'
52
+ - 'lib/typesense.rb'
53
+ - 'lib/typesense/api_call.rb'
54
+ - 'lib/typesense/client.rb'
55
+ - 'lib/typesense/collection.rb'
56
+ - 'lib/typesense/collections.rb'
57
+ - 'lib/typesense/configuration.rb'
58
+ - 'lib/typesense/debug.rb'
59
+ - 'lib/typesense/document.rb'
60
+ - 'lib/typesense/documents.rb'
61
+
62
+ # Offense count: 92
63
+ # Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns.
64
+ # URISchemes: http, https
65
+ Metrics/LineLength:
66
+ Max: 245
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
6
+
7
+ # Specify your gem's dependencies in typesense.gemspec
8
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,13 @@
1
+ Copyright 2018 Wreally Studios Inc. <contact@wreally.com> http://wreally.com
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this software except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS,
11
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ See the License for the specific language governing permissions and
13
+ limitations under the License.
@@ -0,0 +1,40 @@
1
+ # Typesense Ruby Library [![CircleCI](https://circleci.com/gh/typesense/typesense-ruby.svg?style=shield&circle-token=063f2179925b0b37d540126f6c96f6e1fe23f1b9)](https://circleci.com/gh/typesense/typesense-ruby)
2
+
3
+
4
+ Ruby client library for accessing the [Typesense HTTP API](https://github.com/typesense/typesense).
5
+
6
+ Follows the API spec [here](https://github.com/typesense/typesense-api-spec).
7
+
8
+ ## Installation
9
+
10
+ Add this line to your application's Gemfile:
11
+
12
+ ```ruby
13
+ gem 'typesense'
14
+ ```
15
+
16
+ And then execute:
17
+
18
+ $ bundle
19
+
20
+ Or install it yourself as:
21
+
22
+ $ gem install typesense
23
+
24
+ ## Usage
25
+
26
+ You'll find detailed documentation here: [https://typesense.org/api/](https://typesense.org/api/)
27
+
28
+ Here are some examples that show you how the Ruby client works: [examples](examples)
29
+
30
+ Tests are also a good place to know how the the library works internally: [spec](spec)
31
+
32
+ ## Development
33
+
34
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
35
+
36
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
37
+
38
+ ## Contributing
39
+
40
+ Bug reports and pull requests are welcome on GitHub at [typesense/typesense-ruby](https://github.com/typesense/typesense-ruby).
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'typesense'
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require 'irb'
15
+ IRB.start(__FILE__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,251 @@
1
+ # frozen_string_literal: true
2
+
3
+ ##
4
+ # These examples walk you through all the operations you can do on a collection and a document
5
+ # Search is specifically covered in another file in the examples folder
6
+
7
+ require_relative '../lib/typesense'
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
19
+ #
20
+ # Start the read replica
21
+ # $ 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
22
+
23
+ ##
24
+ # Create a client
25
+ typesense = Typesense::Client.new(
26
+ master_node: {
27
+ host: 'localhost',
28
+ port: 8108,
29
+ protocol: 'http',
30
+ api_key: 'abcd'
31
+ },
32
+ read_replica_nodes: [
33
+ {
34
+ host: 'localhost',
35
+ port: 8109,
36
+ protocol: 'http',
37
+ api_key: 'wxyz'
38
+ }
39
+ ],
40
+ timeout_seconds: 10
41
+ )
42
+
43
+ ##
44
+ # Create a collection
45
+ schema = {
46
+ 'name' => 'companies',
47
+ 'fields' => [
48
+ {
49
+ 'name' => 'company_name',
50
+ 'type' => 'string'
51
+ },
52
+ {
53
+ 'name' => 'num_employees',
54
+ 'type' => 'int32'
55
+ },
56
+ {
57
+ 'name' => 'country',
58
+ 'type' => 'string',
59
+ 'facet' => true
60
+ }
61
+ ],
62
+ 'default_sorting_field' => 'num_employees'
63
+ }
64
+
65
+ collection = typesense.collections.create(schema)
66
+ ap collection
67
+
68
+ # {
69
+ # "name" => "companies",
70
+ # "fields" => [
71
+ # [0] {
72
+ # "name" => "company_name",
73
+ # "type" => "string"
74
+ # },
75
+ # [1] {
76
+ # "name" => "num_employees",
77
+ # "type" => "int32"
78
+ # },
79
+ # [2] {
80
+ # "name" => "country",
81
+ # "type" => "string",
82
+ # "facet" => true
83
+ # }
84
+ # ],
85
+ # "default_sorting_field" => "num_employees"
86
+ # }
87
+
88
+ ##
89
+ # Retrieve a collection
90
+ collection = typesense.collections['companies'].retrieve
91
+ ap collection
92
+
93
+ # {
94
+ # "default_sorting_field" => "num_employees",
95
+ # "fields" => [
96
+ # [0] {
97
+ # "facet" => false,
98
+ # "name" => "company_name",
99
+ # "type" => "string"
100
+ # },
101
+ # [1] {
102
+ # "facet" => false,
103
+ # "name" => "num_employees",
104
+ # "type" => "int32"
105
+ # },
106
+ # [2] {
107
+ # "facet" => true,
108
+ # "name" => "country",
109
+ # "type" => "string"
110
+ # }
111
+ # ],
112
+ # "name" => "companies",
113
+ # "num_documents" => 0
114
+ # }
115
+
116
+ ##
117
+ # Retrieve all collections
118
+ collections = typesense.collections.retrieve
119
+ ap collections
120
+
121
+ # [
122
+ # [0] {
123
+ # "default_sorting_field" => "num_employees",
124
+ # "fields" => [
125
+ # [0] {
126
+ # "facet" => false,
127
+ # "name" => "company_name",
128
+ # "type" => "string"
129
+ # },
130
+ # [1] {
131
+ # "facet" => false,
132
+ # "name" => "num_employees",
133
+ # "type" => "int32"
134
+ # },
135
+ # [2] {
136
+ # "facet" => true,
137
+ # "name" => "country",
138
+ # "type" => "string"
139
+ # }
140
+ # ],
141
+ # "name" => "companies",
142
+ # "num_documents" => 0
143
+ # }
144
+ # ]
145
+
146
+ ##
147
+ # Delete a collection
148
+ # Deletion returns the schema of the collection after deletion
149
+ collection = typesense.collections['companies'].delete
150
+ ap collection
151
+
152
+ # {
153
+ # "default_sorting_field" => "num_employees",
154
+ # "fields" => [
155
+ # [0] {
156
+ # "facet" => false,
157
+ # "name" => "company_name",
158
+ # "type" => "string"
159
+ # },
160
+ # [1] {
161
+ # "facet" => false,
162
+ # "name" => "num_employees",
163
+ # "type" => "int32"
164
+ # },
165
+ # [2] {
166
+ # "facet" => true,
167
+ # "name" => "country",
168
+ # "type" => "string"
169
+ # }
170
+ # ],
171
+ # "name" => "companies",
172
+ # "num_documents" => 0
173
+ # }
174
+
175
+ # Let's create the collection again for use in our remaining examples
176
+ typesense.collections.create(schema)
177
+
178
+ ##
179
+ # Create (index) a document
180
+ document = {
181
+ 'id' => '124',
182
+ 'company_name' => 'Stark Industries',
183
+ 'num_employees' => 5215,
184
+ 'country' => 'USA'
185
+ }
186
+
187
+ document = typesense.collections['companies'].documents.create(document)
188
+ ap document
189
+
190
+ # {
191
+ # "company_name" => "Stark Industries",
192
+ # "country" => "USA",
193
+ # "id" => "124",
194
+ # "num_employees" => 5215
195
+ # }
196
+
197
+ ##
198
+ # Retrieve a document
199
+ document = typesense.collections['companies'].documents['124'].retrieve
200
+ ap document
201
+
202
+ # {
203
+ # "company_name" => "Stark Industries",
204
+ # "country" => "USA",
205
+ # "id" => "124",
206
+ # "num_employees" => 5215
207
+ # }
208
+
209
+ ##
210
+ # Delete a document
211
+ # Deleting a document, returns the document after deletion
212
+ document = typesense.collections['companies'].documents['124'].delete
213
+ ap document
214
+
215
+ # {
216
+ # "company_name" => "Stark Industries",
217
+ # "country" => "USA",
218
+ # "id" => "124",
219
+ # "num_employees" => 5215
220
+ # }
221
+
222
+ # Let's create two documents again for use in our remaining examples
223
+ typesense.collections['companies'].documents.create(
224
+ 'id' => '124',
225
+ 'company_name' => 'Stark Industries',
226
+ 'num_employees' => 5215,
227
+ 'country' => 'USA'
228
+ )
229
+
230
+ typesense.collections['companies'].documents.create(
231
+ 'id' => '125',
232
+ 'company_name' => 'Acme Corp',
233
+ 'num_employees' => 1002,
234
+ 'country' => 'France'
235
+ )
236
+
237
+ ##
238
+ # Export all documents in a collection in JSON Lines format
239
+ # We use JSON Lines format for performance reasons. You can choose to parse selected lines (elements in the array) as needed.
240
+ array_of_json_strings = typesense.collections['companies'].documents.export
241
+ ap array_of_json_strings
242
+
243
+ # [
244
+ # [0] "{\"company_name\":\"Stark Industries\",\"country\":\"USA\",\"id\":\"124\",\"num_employees\":5215}",
245
+ # [1] "{\"company_name\":\"Acme Corp\",\"country\":\"France\",\"id\":\"125\",\"num_employees\":1002}"
246
+ # ]
247
+
248
+ ##
249
+ # Cleanup
250
+ # Drop the collection
251
+ typesense.collections['companies'].delete
@@ -0,0 +1,182 @@
1
+ # frozen_string_literal: true
2
+
3
+ ##
4
+ # These examples walk you through operations specifically related to search
5
+
6
+ require_relative '../lib/typesense'
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
+ )
41
+
42
+ ##
43
+ # Create a collection
44
+ schema = {
45
+ 'name' => 'companies',
46
+ 'fields' => [
47
+ {
48
+ 'name' => 'company_name',
49
+ 'type' => 'string'
50
+ },
51
+ {
52
+ 'name' => 'num_employees',
53
+ 'type' => 'int32'
54
+ },
55
+ {
56
+ 'name' => 'country',
57
+ 'type' => 'string',
58
+ 'facet' => true
59
+ }
60
+ ],
61
+ 'default_sorting_field' => 'num_employees'
62
+ }
63
+
64
+ typesense.collections.create(schema)
65
+
66
+ # Let's create a couple documents for us to use in our search examples
67
+ typesense.collections['companies'].documents.create(
68
+ 'id' => '124',
69
+ 'company_name' => 'Stark Industries',
70
+ 'num_employees' => 5215,
71
+ 'country' => 'USA'
72
+ )
73
+
74
+ typesense.collections['companies'].documents.create(
75
+ 'id' => '127',
76
+ 'company_name' => 'Stark Corp',
77
+ 'num_employees' => 1031,
78
+ 'country' => 'USA'
79
+ )
80
+
81
+ typesense.collections['companies'].documents.create(
82
+ 'id' => '125',
83
+ 'company_name' => 'Acme Corp',
84
+ 'num_employees' => 1002,
85
+ 'country' => 'France'
86
+ )
87
+
88
+ typesense.collections['companies'].documents.create(
89
+ 'id' => '126',
90
+ 'company_name' => 'Doofenshmirtz Inc',
91
+ 'num_employees' => 2,
92
+ 'country' => 'Tri-State Area'
93
+ )
94
+
95
+ ##
96
+ # Search for documents
97
+ results = typesense.collections['companies'].documents.search(
98
+ 'q' => 'Stark',
99
+ 'query_by' => 'company_name'
100
+ )
101
+ ap results
102
+
103
+ # {
104
+ # "facet_counts" => [],
105
+ # "found" => 2,
106
+ # "hits" => [
107
+ # [0] {
108
+ # "document" => {
109
+ # "company_name" => "Stark Industries",
110
+ # "country" => "USA",
111
+ # "id" => "124",
112
+ # "num_employees" => 5215
113
+ # },
114
+ # "highlight" => {
115
+ # "company_name" => "<mark>Stark</mark> Industries"
116
+ # }
117
+ # },
118
+ # [1] {
119
+ # "document" => {
120
+ # "company_name" => "Stark Corp",
121
+ # "country" => "USA",
122
+ # "id" => "127",
123
+ # "num_employees" => 1031
124
+ # },
125
+ # "highlight" => {
126
+ # "company_name" => "<mark>Stark</mark> Corp"
127
+ # }
128
+ # }
129
+ # ],
130
+ # "page" => 1,
131
+ # "search_time_ms" => 0
132
+ # }
133
+
134
+ ##
135
+ # Search for more documents
136
+ results = typesense.collections['companies'].documents.search(
137
+ 'q' => 'Inc',
138
+ 'query_by' => 'company_name',
139
+ 'filter_by' => 'num_employees:<100',
140
+ 'sort_by' => 'num_employees:desc'
141
+ )
142
+ ap results
143
+
144
+ # {
145
+ # "facet_counts" => [],
146
+ # "found" => 1,
147
+ # "hits" => [
148
+ # [0] {
149
+ # "document" => {
150
+ # "company_name" => "Doofenshmirtz Inc",
151
+ # "country" => "Tri-State Area",
152
+ # "id" => "126",
153
+ # "num_employees" => 2
154
+ # },
155
+ # "highlight" => {
156
+ # "company_name" => "Doofenshmirtz <mark>Inc</mark>"
157
+ # }
158
+ # }
159
+ # ],
160
+ # "page" => 1,
161
+ # "search_time_ms" => 0
162
+ # }
163
+
164
+ ##
165
+ # Search for more documents
166
+ results = typesense.collections['companies'].documents.search(
167
+ 'q' => 'Non-existent',
168
+ 'query_by' => 'company_name'
169
+ )
170
+ ap results
171
+
172
+ # {
173
+ # "found" => 0,
174
+ # "hits" => [],
175
+ # "page" => 1,
176
+ # "search_time_ms" => 0
177
+ # }
178
+
179
+ ##
180
+ # Cleanup
181
+ # Drop the collection
182
+ typesense.collections['companies'].delete
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Typesense
4
+ end
5
+
6
+ require_relative 'typesense/version'
7
+ require_relative 'typesense/configuration'
8
+ require_relative 'typesense/client'
9
+ require_relative 'typesense/api_call'
10
+ require_relative 'typesense/collections'
11
+ require_relative 'typesense/collection'
12
+ require_relative 'typesense/documents'
13
+ require_relative 'typesense/document'
14
+ require_relative 'typesense/debug'
15
+ require_relative 'typesense/error'
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'httparty'
4
+
5
+ module Typesense
6
+ class ApiCall
7
+ include HTTParty
8
+
9
+ API_KEY_HEADER_NAME = 'X-TYPESENSE-API-KEY'
10
+
11
+ def initialize(configuration)
12
+ @configuration = configuration
13
+ end
14
+
15
+ def post(endpoint, parameters = {})
16
+ perform_with_error_handling(:do_not_use_read_replicas) do
17
+ self.class.post(uri_for(endpoint),
18
+ default_options.merge(
19
+ body: parameters.to_json,
20
+ headers: default_headers.merge('Content-Type' => 'application/json')
21
+ ))
22
+ end.parsed_response
23
+ end
24
+
25
+ def get(endpoint, parameters = {})
26
+ get_unparsed_response(endpoint, parameters).parsed_response
27
+ end
28
+
29
+ def get_unparsed_response(endpoint, parameters = {})
30
+ perform_with_error_handling(:use_read_replicas) do |node, node_index|
31
+ self.class.get(uri_for(endpoint, node, node_index),
32
+ default_options.merge(
33
+ query: parameters,
34
+ headers: default_headers
35
+ ))
36
+ end
37
+ end
38
+
39
+ def delete(endpoint, parameters = {})
40
+ perform_with_error_handling(:do_not_use_read_replicas) do
41
+ self.class.delete(uri_for(endpoint),
42
+ default_options.merge(
43
+ query: parameters,
44
+ headers: default_headers
45
+ ))
46
+ end.parsed_response
47
+ end
48
+
49
+ private
50
+
51
+ def uri_for(endpoint, node = :master, node_index = 0)
52
+ if node == :read_replica
53
+ "#{@configuration.read_replica_nodes[node_index][:protocol]}://#{@configuration.read_replica_nodes[node_index][:host]}:#{@configuration.read_replica_nodes[node_index][:port]}#{endpoint}"
54
+ else
55
+ "#{@configuration.master_node[:protocol]}://#{@configuration.master_node[:host]}:#{@configuration.master_node[:port]}#{endpoint}"
56
+ end
57
+ end
58
+
59
+ def perform_with_error_handling(use_read_replicas = :do_not_use_read_replicas)
60
+ @configuration.validate!
61
+
62
+ node = :master
63
+ node_index = -1
64
+
65
+ begin
66
+ response_object = yield node, node_index
67
+
68
+ return response_object if response_object.response.code_type <= Net::HTTPSuccess # 2xx
69
+
70
+ error_klass = if response_object.response.code_type <= Net::HTTPBadRequest # 400
71
+ Error::RequestMalformed
72
+ elsif response_object.response.code_type <= Net::HTTPUnauthorized # 401
73
+ Error::RequestUnauthorized
74
+ elsif response_object.response.code_type <= Net::HTTPNotFound # 404
75
+ Error::ObjectNotFound
76
+ elsif response_object.response.code_type <= Net::HTTPConflict # 409
77
+ Error::ObjectAlreadyExists
78
+ elsif response_object.response.code_type <= Net::HTTPUnprocessableEntity # 422
79
+ Error::ObjectUnprocessable
80
+ elsif response_object.response.code_type <= Net::HTTPServerError # 5xx
81
+ Error::ServerError
82
+ else
83
+ Error
84
+ end
85
+
86
+ raise error_klass, response_object.parsed_response['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, Error::ServerError, HTTParty::ResponseError
92
+ if (use_read_replicas == :use_read_replicas || use_read_replicas == true) &&
93
+ !@configuration.read_replica_nodes.nil?
94
+ node = :read_replica
95
+ node_index += 1
96
+
97
+ retry unless @configuration.read_replica_nodes[node_index].nil?
98
+ end
99
+
100
+ raise
101
+ end
102
+ end
103
+
104
+ def default_options
105
+ {
106
+ timeout: @configuration.timeout_seconds
107
+ }
108
+ end
109
+
110
+ def default_headers
111
+ {
112
+ API_KEY_HEADER_NAME.to_s => @configuration.master_node[:api_key]
113
+ }
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Typesense
4
+ class Client
5
+ attr_reader :configuration
6
+ attr_reader :collections
7
+ attr_reader :debug
8
+
9
+ def initialize(options = {})
10
+ @configuration ||= Configuration.new(options)
11
+ @collections = Collections.new(@configuration)
12
+ @debug = Debug.new(@configuration)
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Typesense
4
+ class Collection
5
+ attr_reader :documents
6
+
7
+ def initialize(configuration, name)
8
+ @configuration = configuration
9
+ @name = name
10
+ @documents = Documents.new(@configuration, @name)
11
+ end
12
+
13
+ def retrieve
14
+ ApiCall.new(@configuration).get(endpoint_path)
15
+ end
16
+
17
+ def delete
18
+ ApiCall.new(@configuration).delete(endpoint_path)
19
+ end
20
+
21
+ private
22
+
23
+ def endpoint_path
24
+ "#{Collections::RESOURCE_PATH}/#{@name}"
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Typesense
4
+ class Collections
5
+ RESOURCE_PATH = '/collections'
6
+
7
+ def initialize(configuration)
8
+ @configuration = configuration
9
+ @collections = {}
10
+ end
11
+
12
+ def create(schema)
13
+ ApiCall.new(@configuration).post(RESOURCE_PATH, schema)
14
+ end
15
+
16
+ def retrieve
17
+ ApiCall.new(@configuration).get(RESOURCE_PATH)
18
+ end
19
+
20
+ def [](collection_name)
21
+ @collections[collection_name] ||= Collection.new(@configuration, collection_name)
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Typesense
4
+ class Configuration
5
+ attr_accessor :master_node
6
+ attr_accessor :read_replica_nodes
7
+ attr_accessor :timeout_seconds
8
+
9
+ def initialize(options = {})
10
+ @master_node = options[:master_node] || {
11
+ host: 'localhost',
12
+ port: '8108',
13
+ protocol: 'http'
14
+ }
15
+
16
+ @read_replica_nodes = options[:read_replica_nodes] || []
17
+ @timeout_seconds = options[:timeout_seconds] || 10
18
+ end
19
+
20
+ def validate!
21
+ if @master_node.nil? ||
22
+ node_missing_parameters?(@master_node)
23
+ raise Error::MissingConfiguration, 'Missing required configuration. Ensure that master_node[:protocol], master_node[:host], master_node[:port] and master_node[:api_key] are set.'
24
+ end
25
+
26
+ if !@read_replica_nodes.nil? &&
27
+ @read_replica_nodes.any? { |node| node_missing_parameters?(node) }
28
+ raise Error::MissingConfiguration, 'Missing required configuration for read_replica_nodes. Ensure that read_replica_nodes[][:protocol], read_replica_nodes[][:host], read_replica_nodes[][:port] and read_replica_nodes[][:api_key] are set.'
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def node_missing_parameters?(node)
35
+ %i[protocol host port api_key].any? { |attr| node.send(:[], attr).nil? }
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Typesense
4
+ class Debug
5
+ RESOURCE_PATH = '/debug'
6
+
7
+ def initialize(configuration)
8
+ @configuration = configuration
9
+ end
10
+
11
+ def retrieve
12
+ ApiCall.new(@configuration).get(RESOURCE_PATH)
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Typesense
4
+ class Document
5
+ def initialize(configuration, collection_name, document_id)
6
+ @configuration = configuration
7
+ @collection_name = collection_name
8
+ @document_id = document_id
9
+ end
10
+
11
+ def retrieve
12
+ ApiCall.new(@configuration).get(endpoint_path)
13
+ end
14
+
15
+ def delete
16
+ ApiCall.new(@configuration).delete(endpoint_path)
17
+ end
18
+
19
+ private
20
+
21
+ def endpoint_path
22
+ "#{Collections::RESOURCE_PATH}/#{@collection_name}#{Documents::RESOURCE_PATH}/#{@document_id}"
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Typesense
4
+ class Documents
5
+ RESOURCE_PATH = '/documents'
6
+
7
+ def initialize(configuration, collection_name)
8
+ @configuration = configuration
9
+ @collection_name = collection_name
10
+ @documents = {}
11
+ end
12
+
13
+ def create(document)
14
+ ApiCall.new(@configuration).post(endpoint_path, document)
15
+ end
16
+
17
+ def export
18
+ ApiCall.new(@configuration).get_unparsed_response(endpoint_path('export')).split("\n")
19
+ end
20
+
21
+ def search(search_parameters)
22
+ ApiCall.new(@configuration).get(endpoint_path('search'), search_parameters)
23
+ end
24
+
25
+ def [](document_id)
26
+ @documents[document_id] ||= Document.new(@configuration, @collection_name, document_id)
27
+ end
28
+
29
+ private
30
+
31
+ def endpoint_path(operation = nil)
32
+ "#{Collections::RESOURCE_PATH}/#{@collection_name}#{Documents::RESOURCE_PATH}#{operation.nil? ? '' : '/' + operation}"
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Typesense
4
+ class Error < StandardError
5
+ class MissingConfiguration < Error
6
+ end
7
+
8
+ class ObjectAlreadyExists < Error
9
+ end
10
+
11
+ class ObjectNotFound < Error
12
+ end
13
+
14
+ class ObjectUnprocessable < Error
15
+ end
16
+
17
+ class RequestMalformed < Error
18
+ end
19
+
20
+ class RequestUnauthorized < Error
21
+ end
22
+
23
+ class ServerError < Error
24
+ end
25
+
26
+ class NoMethodError < ::NoMethodError
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Typesense
4
+ VERSION = '0.1.0'
5
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'typesense/version'
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = 'typesense'
9
+ spec.version = Typesense::VERSION
10
+ spec.authors = ['Wreally Studios']
11
+ spec.email = ['contact@typesense.org']
12
+
13
+ spec.summary = 'Ruby Library for Typesense'
14
+ spec.description = 'Typesense is an open source search engine for building a delightful search experience.'
15
+ spec.homepage = 'https://github.com/typesense/typesense-ruby'
16
+ spec.license = 'Apache-2.0'
17
+
18
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
19
+ f.match(%r{^(test|spec|features)/})
20
+ end
21
+ spec.bindir = 'exe'
22
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
23
+ spec.require_paths = ['lib']
24
+
25
+ spec.add_development_dependency 'awesome_print', '~> 1.8'
26
+ spec.add_development_dependency 'bundler', '~> 1.16'
27
+ spec.add_development_dependency 'pry-byebug', '~> 3.5'
28
+ spec.add_development_dependency 'rake', '~> 10.0'
29
+ spec.add_development_dependency 'rspec', '~> 3.0'
30
+ spec.add_development_dependency 'rspec_junit_formatter', '~> 0.3'
31
+ spec.add_development_dependency 'rubocop', '~> 0.53.0'
32
+ spec.add_development_dependency 'rubocop-rspec', '~> 1.24'
33
+ spec.add_development_dependency 'simplecov', '~> 0.15'
34
+ spec.add_development_dependency 'webmock', '~> 3.2'
35
+
36
+ spec.add_dependency 'httparty', '~> 0.15'
37
+ end
metadata ADDED
@@ -0,0 +1,224 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: typesense
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Wreally Studios
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2018-04-04 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: awesome_print
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.8'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.8'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.16'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.16'
41
+ - !ruby/object:Gem::Dependency
42
+ name: pry-byebug
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.5'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.5'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '10.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '10.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '3.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '3.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rspec_junit_formatter
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '0.3'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '0.3'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rubocop
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: 0.53.0
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: 0.53.0
111
+ - !ruby/object:Gem::Dependency
112
+ name: rubocop-rspec
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '1.24'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '1.24'
125
+ - !ruby/object:Gem::Dependency
126
+ name: simplecov
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '0.15'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '0.15'
139
+ - !ruby/object:Gem::Dependency
140
+ name: webmock
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - "~>"
144
+ - !ruby/object:Gem::Version
145
+ version: '3.2'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - "~>"
151
+ - !ruby/object:Gem::Version
152
+ version: '3.2'
153
+ - !ruby/object:Gem::Dependency
154
+ name: httparty
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - "~>"
158
+ - !ruby/object:Gem::Version
159
+ version: '0.15'
160
+ type: :runtime
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - "~>"
165
+ - !ruby/object:Gem::Version
166
+ version: '0.15'
167
+ description: Typesense is an open source search engine for building a delightful search
168
+ experience.
169
+ email:
170
+ - contact@typesense.org
171
+ executables: []
172
+ extensions: []
173
+ extra_rdoc_files: []
174
+ files:
175
+ - ".circleci/config.yml"
176
+ - ".gitignore"
177
+ - ".rspec"
178
+ - ".rubocop.yml"
179
+ - ".rubocop_todo.yml"
180
+ - Gemfile
181
+ - LICENSE
182
+ - README.md
183
+ - Rakefile
184
+ - bin/console
185
+ - bin/setup
186
+ - examples/collections_and_documents.rb
187
+ - examples/search.rb
188
+ - lib/typesense.rb
189
+ - lib/typesense/api_call.rb
190
+ - lib/typesense/client.rb
191
+ - lib/typesense/collection.rb
192
+ - lib/typesense/collections.rb
193
+ - lib/typesense/configuration.rb
194
+ - lib/typesense/debug.rb
195
+ - lib/typesense/document.rb
196
+ - lib/typesense/documents.rb
197
+ - lib/typesense/error.rb
198
+ - lib/typesense/version.rb
199
+ - typesense.gemspec
200
+ homepage: https://github.com/typesense/typesense-ruby
201
+ licenses:
202
+ - Apache-2.0
203
+ metadata: {}
204
+ post_install_message:
205
+ rdoc_options: []
206
+ require_paths:
207
+ - lib
208
+ required_ruby_version: !ruby/object:Gem::Requirement
209
+ requirements:
210
+ - - ">="
211
+ - !ruby/object:Gem::Version
212
+ version: '0'
213
+ required_rubygems_version: !ruby/object:Gem::Requirement
214
+ requirements:
215
+ - - ">="
216
+ - !ruby/object:Gem::Version
217
+ version: '0'
218
+ requirements: []
219
+ rubyforge_project:
220
+ rubygems_version: 2.7.6
221
+ signing_key:
222
+ specification_version: 4
223
+ summary: Ruby Library for Typesense
224
+ test_files: []