activerecord-turbopuffer-adapter 0.1.3 → 0.1.5
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/README.md +18 -0
- data/lib/active_record/connection_adapters/turbopuffer_adapter.rb +19 -1
- data/lib/turbopuffer/active_record/schema/attribute.rb +1 -1
- data/lib/turbopuffer/active_record/schema/model.rb +1 -0
- data/lib/turbopuffer/active_record/type/bytes.rb +21 -0
- data/lib/turbopuffer/active_record/type/sparse_vector.rb +21 -0
- data/lib/turbopuffer/active_record/type/vector.rb +16 -2
- data/lib/turbopuffer/active_record/type.rb +2 -0
- data/lib/turbopuffer/active_record/version.rb +1 -1
- metadata +18 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8bb295502835167a50a215f96707b208574ac3e6641b3cd9d02c50646b2335b6
|
|
4
|
+
data.tar.gz: '09c4fcdaf392f394619a7a1b786a0be832e248edd7449a5d49e804908fd4d94d'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d822446b93294cbca0db8d9cb13a373588e160805d6dc1f1d8e9e88b3d6695ebff58e3cb94fae1715bf0e42850cafb6b88b807b369cc48d174dbeab893e7c35a
|
|
7
|
+
data.tar.gz: a22568481e63de3ca8a83a7367a59ac9b1e57853ed2315873c2948cd17a0bf4d4a348e77188e8d2f9deefcc25484b8e128e028948dbe11c4064adbc1909e1ffe
|
data/README.md
CHANGED
|
@@ -120,6 +120,24 @@ Document
|
|
|
120
120
|
|
|
121
121
|
> Eventual reads are cheaper but may not include writes from the last few seconds. Set a default with `turbopuffer_consistency "eventual"` in a model or `consistency: eventual` in `database.yml`. Strong is the default.
|
|
122
122
|
|
|
123
|
+
### Errors
|
|
124
|
+
|
|
125
|
+
Turbopuffer errors are raised as the ActiveRecord exceptions a Rails app already handles.
|
|
126
|
+
|
|
127
|
+
```ruby
|
|
128
|
+
begin
|
|
129
|
+
Document.where(published: true).to_a
|
|
130
|
+
rescue ActiveRecord::ConnectionFailed
|
|
131
|
+
# turbopuffer is unreachable
|
|
132
|
+
rescue ActiveRecord::StatementTimeout
|
|
133
|
+
# the request timed out
|
|
134
|
+
rescue ActiveRecord::DatabaseConnectionError
|
|
135
|
+
# the API key is invalid
|
|
136
|
+
rescue ActiveRecord::StatementInvalid => e
|
|
137
|
+
e.cause # => the underlying Turbopuffer::Errors::APIError, e.g. a bad request or rate limit
|
|
138
|
+
end
|
|
139
|
+
```
|
|
140
|
+
|
|
123
141
|
### Using alongside Postgres
|
|
124
142
|
|
|
125
143
|
While something of a lark, aspirationally the idea of this gem is to make turbopuffer conveniently usable as the primary db in a Rails app. In practice, however, using turbopuffer alongside a traditional primary db (such as postgres) is a supported, potentially more practical solution.
|
|
@@ -32,12 +32,17 @@ module ActiveRecord
|
|
|
32
32
|
m.register_type "float", Type::Float.new
|
|
33
33
|
m.register_type "bool", Type::Boolean.new
|
|
34
34
|
m.register_type "datetime", ::Turbopuffer::ActiveRecord::Type::DateTime.new
|
|
35
|
+
m.register_type "bytes", ::Turbopuffer::ActiveRecord::Type::Bytes.new
|
|
36
|
+
m.register_type "{}f16", ::Turbopuffer::ActiveRecord::Type::SparseVector.new
|
|
35
37
|
m.register_type(%r{\A\[\].+\z}) do |type|
|
|
36
38
|
::Turbopuffer::ActiveRecord::Type::Array.new(m.lookup(type.delete_prefix("[]")))
|
|
37
39
|
end
|
|
38
40
|
m.register_type(%r{\A\[\d+\]f(?:16|32)\z}) do |type|
|
|
39
41
|
::Turbopuffer::ActiveRecord::Type::Vector.new(type[/\d+/].to_i)
|
|
40
42
|
end
|
|
43
|
+
m.register_type(%r{\A\[\d+\]i8\z}) do |type|
|
|
44
|
+
::Turbopuffer::ActiveRecord::Type::Vector.new(type[/\d+/].to_i, integer: true)
|
|
45
|
+
end
|
|
41
46
|
end
|
|
42
47
|
end
|
|
43
48
|
|
|
@@ -96,7 +101,7 @@ module ActiveRecord
|
|
|
96
101
|
|
|
97
102
|
def type_cast(value)
|
|
98
103
|
case value
|
|
99
|
-
when Array
|
|
104
|
+
when Array, Hash
|
|
100
105
|
value
|
|
101
106
|
else
|
|
102
107
|
super
|
|
@@ -363,6 +368,19 @@ module ActiveRecord
|
|
|
363
368
|
def reconnect
|
|
364
369
|
connect
|
|
365
370
|
end
|
|
371
|
+
|
|
372
|
+
def translate_exception(exception, message:, sql:, binds:)
|
|
373
|
+
case exception
|
|
374
|
+
when Turbopuffer::Errors::APITimeoutError
|
|
375
|
+
ActiveRecord::StatementTimeout.new(message, sql: sql, binds: binds, connection_pool: @pool)
|
|
376
|
+
when Turbopuffer::Errors::APIConnectionError
|
|
377
|
+
ActiveRecord::ConnectionFailed.new(message, sql: sql, binds: binds, connection_pool: @pool)
|
|
378
|
+
when Turbopuffer::Errors::AuthenticationError, Turbopuffer::Errors::PermissionDeniedError
|
|
379
|
+
ActiveRecord::DatabaseConnectionError.new(message)
|
|
380
|
+
else
|
|
381
|
+
super
|
|
382
|
+
end
|
|
383
|
+
end
|
|
366
384
|
end
|
|
367
385
|
end
|
|
368
386
|
end
|
|
@@ -2,7 +2,7 @@ module Turbopuffer
|
|
|
2
2
|
module ActiveRecord
|
|
3
3
|
module Schema
|
|
4
4
|
class Attribute
|
|
5
|
-
TYPES = %r{\A(?:\[\])?(?:string|uuid|int|uint|float|bool|datetime)\z|\A\[\d+\]
|
|
5
|
+
TYPES = %r{\A(?:\[\])?(?:string|uuid|int|uint|float|bool|datetime)\z|\Abytes\z|\A\[\d+\](?:f16|f32|i8)\z|\A\{\}f16\z}
|
|
6
6
|
|
|
7
7
|
attr_reader :name, :type, :filterable, :full_text_search, :ann, :glob, :regex, :notnull
|
|
8
8
|
|
|
@@ -115,6 +115,7 @@ module Turbopuffer
|
|
|
115
115
|
attrs[:ann] = true if attribute.ann
|
|
116
116
|
attrs[:glob] = true if attribute.glob
|
|
117
117
|
attrs[:regex] = true if attribute.regex
|
|
118
|
+
attrs[:sparse_knn] = { distance_metric: "dot_product" } if attribute.type == "{}f16"
|
|
118
119
|
|
|
119
120
|
schema[attribute.name] = attrs
|
|
120
121
|
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
require "base64"
|
|
2
|
+
|
|
3
|
+
module Turbopuffer
|
|
4
|
+
module ActiveRecord
|
|
5
|
+
module Type
|
|
6
|
+
class Bytes < ::ActiveModel::Type::Value
|
|
7
|
+
def serialize(value)
|
|
8
|
+
return if value.nil?
|
|
9
|
+
|
|
10
|
+
Base64.strict_encode64(value)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def deserialize(value)
|
|
14
|
+
return if value.nil?
|
|
15
|
+
|
|
16
|
+
Base64.decode64(value)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module Turbopuffer
|
|
2
|
+
module ActiveRecord
|
|
3
|
+
module Type
|
|
4
|
+
class SparseVector < ::ActiveModel::Type::Value
|
|
5
|
+
include ::ActiveModel::Type::Helpers::Mutable
|
|
6
|
+
|
|
7
|
+
def serialize(value)
|
|
8
|
+
return if value.nil?
|
|
9
|
+
|
|
10
|
+
value.to_h { |key, weight| [ key.to_s, Float(weight) ] }
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def deserialize(value)
|
|
14
|
+
return if value.nil?
|
|
15
|
+
|
|
16
|
+
value.to_h.transform_keys(&:to_s)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -4,15 +4,16 @@ module Turbopuffer
|
|
|
4
4
|
class Vector < ::ActiveModel::Type::Value
|
|
5
5
|
include ::ActiveModel::Type::Helpers::Mutable
|
|
6
6
|
|
|
7
|
-
def initialize(dimensions)
|
|
7
|
+
def initialize(dimensions, integer: false)
|
|
8
8
|
@dimensions = dimensions
|
|
9
|
+
@integer = integer
|
|
9
10
|
super()
|
|
10
11
|
end
|
|
11
12
|
|
|
12
13
|
def serialize(value)
|
|
13
14
|
return if value.nil?
|
|
14
15
|
|
|
15
|
-
vector = ::Array.wrap(value).map { |component|
|
|
16
|
+
vector = ::Array.wrap(value).map { |component| component(component) }
|
|
16
17
|
|
|
17
18
|
unless vector.size == @dimensions
|
|
18
19
|
raise ArgumentError, "expected a vector of #{@dimensions} dimensions, got #{vector.size}"
|
|
@@ -24,6 +25,19 @@ module Turbopuffer
|
|
|
24
25
|
def deserialize(value)
|
|
25
26
|
value
|
|
26
27
|
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def component(value)
|
|
32
|
+
number = Float(value)
|
|
33
|
+
return number unless @integer
|
|
34
|
+
|
|
35
|
+
unless number == number.to_i
|
|
36
|
+
raise ArgumentError, "expected an integer vector component, got #{value.inspect}"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
number.to_i
|
|
40
|
+
end
|
|
27
41
|
end
|
|
28
42
|
end
|
|
29
43
|
end
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
require "turbopuffer/active_record/type/date_time"
|
|
2
2
|
require "turbopuffer/active_record/type/unsigned_integer"
|
|
3
|
+
require "turbopuffer/active_record/type/bytes"
|
|
3
4
|
require "turbopuffer/active_record/type/array"
|
|
4
5
|
require "turbopuffer/active_record/type/vector"
|
|
6
|
+
require "turbopuffer/active_record/type/sparse_vector"
|
|
5
7
|
|
|
6
8
|
module Turbopuffer
|
|
7
9
|
module ActiveRecord
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: activerecord-turbopuffer-adapter
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Richard Monette
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-09-
|
|
10
|
+
date: 2026-09-23 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: activerecord
|
|
@@ -23,6 +23,20 @@ dependencies:
|
|
|
23
23
|
- - "~>"
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
25
|
version: '8.1'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: base64
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0'
|
|
26
40
|
- !ruby/object:Gem::Dependency
|
|
27
41
|
name: railties
|
|
28
42
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -81,7 +95,9 @@ files:
|
|
|
81
95
|
- lib/turbopuffer/active_record/schema/model.rb
|
|
82
96
|
- lib/turbopuffer/active_record/type.rb
|
|
83
97
|
- lib/turbopuffer/active_record/type/array.rb
|
|
98
|
+
- lib/turbopuffer/active_record/type/bytes.rb
|
|
84
99
|
- lib/turbopuffer/active_record/type/date_time.rb
|
|
100
|
+
- lib/turbopuffer/active_record/type/sparse_vector.rb
|
|
85
101
|
- lib/turbopuffer/active_record/type/unsigned_integer.rb
|
|
86
102
|
- lib/turbopuffer/active_record/type/vector.rb
|
|
87
103
|
- lib/turbopuffer/active_record/version.rb
|