activerecord-turbopuffer-adapter 0.1.1 → 0.1.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
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cc9b47b80bd694b53052b54ca2647989e6bfd911c1a0f2e59056d61cfccf075e
|
|
4
|
+
data.tar.gz: a306ba71a2c62db295b438c6583f61a765d425a8f2888c328b831df31bf9840b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f73376471edf444dfc0f2c5dd702d2727a280c307f440da3a367d7d594074a9654e8b20fa58c19c2c6e57a4ab393dbacf17bb8044be82867e9fe7528d5b78874
|
|
7
|
+
data.tar.gz: 8988590d661ac5c872df12f3e467f939bdb71f291d9485eab31227d3ea3afa3773f8ad8d885f2a91e3b8d3856e92835fc1a2381ceee79978185206b6f52e5479
|
data/README.md
CHANGED
|
@@ -105,6 +105,8 @@ Document.find("018f...")
|
|
|
105
105
|
Document.rank_by("vector", "ANN", query_vector).limit(10)
|
|
106
106
|
Document.rank_by("text", "BM25", "quick walrus").limit(10)
|
|
107
107
|
|
|
108
|
+
Document.consistency(:eventual).where(published: true)
|
|
109
|
+
|
|
108
110
|
Document
|
|
109
111
|
.where(published: true)
|
|
110
112
|
.rank_by(["Sum", [
|
|
@@ -116,6 +118,8 @@ Document
|
|
|
116
118
|
|
|
117
119
|
> Note that turbopuffer has a limit on the maximum number of documents returned (https://turbopuffer.com/docs/query#param-limit), so Document.all.to_a etc. will only return at most 10,000 items
|
|
118
120
|
|
|
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
|
+
|
|
119
123
|
### Using alongside Postgres
|
|
120
124
|
|
|
121
125
|
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.
|
|
@@ -225,6 +225,14 @@ module ActiveRecord
|
|
|
225
225
|
0
|
|
226
226
|
end
|
|
227
227
|
|
|
228
|
+
def consistency_for(query)
|
|
229
|
+
model = ::Turbopuffer::ActiveRecord::Schema::Model.for_table(query.namespace)
|
|
230
|
+
level = query.consistency || model.turbopuffer_consistency || @config[:consistency]
|
|
231
|
+
return if level.nil?
|
|
232
|
+
|
|
233
|
+
{ level: ::Turbopuffer::ActiveRecord::Schema::Model.validate_consistency(level).to_sym }
|
|
234
|
+
end
|
|
235
|
+
|
|
228
236
|
def turbopuffer_aggregate(namespace, query)
|
|
229
237
|
aggregate_alias, aggregate = query.aggregate_by
|
|
230
238
|
|
|
@@ -233,6 +241,8 @@ module ActiveRecord
|
|
|
233
241
|
}
|
|
234
242
|
|
|
235
243
|
tpuf_query_args[:filters] = query.filters if query.filters
|
|
244
|
+
consistency = consistency_for(query)
|
|
245
|
+
tpuf_query_args[:consistency] = consistency if consistency
|
|
236
246
|
|
|
237
247
|
if query.group_by.present?
|
|
238
248
|
tpuf_query_args[:group_by] = query.group_by
|
|
@@ -269,6 +279,8 @@ module ActiveRecord
|
|
|
269
279
|
|
|
270
280
|
tpuf_query_args[:rank_by] = query.rank_by.first if query.rank_by.present?
|
|
271
281
|
tpuf_query_args[:filters] = query.filters if query.filters
|
|
282
|
+
consistency = consistency_for(query)
|
|
283
|
+
tpuf_query_args[:consistency] = consistency if consistency
|
|
272
284
|
|
|
273
285
|
tpuf_result = namespace.query(tpuf_query_args)
|
|
274
286
|
|
|
@@ -10,7 +10,8 @@ module Arel::Visitors
|
|
|
10
10
|
rank_by: @rank_by,
|
|
11
11
|
include_attributes: @include_attributes,
|
|
12
12
|
aggregate_by: @aggregate_by,
|
|
13
|
-
upsert_rows: @upsert_rows
|
|
13
|
+
upsert_rows: @upsert_rows,
|
|
14
|
+
consistency: @consistency
|
|
14
15
|
}
|
|
15
16
|
end
|
|
16
17
|
def to_s = JSON.generate(to_h)
|
|
@@ -19,10 +20,11 @@ module Arel::Visitors
|
|
|
19
20
|
def eql?(other) = other.is_a?(self.class) && to_h == other.to_h
|
|
20
21
|
alias == eql?
|
|
21
22
|
|
|
22
|
-
attr_reader :op, :namespace, :filters, :top_k, :rank_by, :include_attributes, :upsert_rows, :aggregate_by, :group_by
|
|
23
|
+
attr_reader :op, :namespace, :filters, :top_k, :rank_by, :include_attributes, :upsert_rows, :aggregate_by, :group_by,
|
|
24
|
+
:consistency
|
|
23
25
|
|
|
24
|
-
def initialize(op:, namespace:, filters: nil, top_k: nil, rank_by: nil,
|
|
25
|
-
|
|
26
|
+
def initialize(op:, namespace:, filters: nil, top_k: nil, rank_by: nil, include_attributes: nil,
|
|
27
|
+
upsert_rows: nil, aggregate_by: nil, group_by: nil, consistency: nil)
|
|
26
28
|
@op = op
|
|
27
29
|
@namespace = namespace
|
|
28
30
|
@filters = filters
|
|
@@ -32,6 +34,7 @@ module Arel::Visitors
|
|
|
32
34
|
@aggregate_by = aggregate_by
|
|
33
35
|
@group_by = group_by
|
|
34
36
|
@upsert_rows = upsert_rows
|
|
37
|
+
@consistency = consistency
|
|
35
38
|
end
|
|
36
39
|
end
|
|
37
40
|
|
|
@@ -59,10 +62,14 @@ module Arel::Visitors
|
|
|
59
62
|
case filters.size
|
|
60
63
|
when 0 then nil
|
|
61
64
|
when 1 then filters.first
|
|
62
|
-
else
|
|
65
|
+
else conjunction(filters)
|
|
63
66
|
end
|
|
64
67
|
end
|
|
65
68
|
|
|
69
|
+
def conjunction(filters)
|
|
70
|
+
[ "And", filters.flat_map { |filter| (filter in [ "And", children ]) ? children : [ filter ] } ]
|
|
71
|
+
end
|
|
72
|
+
|
|
66
73
|
def aggregate_node(projection)
|
|
67
74
|
projection.is_a?(Arel::Nodes::As) ? projection.left : projection
|
|
68
75
|
end
|
|
@@ -116,10 +123,18 @@ module Arel::Visitors
|
|
|
116
123
|
rank_by: o.orders.map { |ord| visit(ord) },
|
|
117
124
|
include_attributes: attributes.flat_map { |p| visit(p) },
|
|
118
125
|
group_by: core.groups.map { |g| visit(g) },
|
|
119
|
-
aggregate_by: aggregate && visit(aggregate)
|
|
126
|
+
aggregate_by: aggregate && visit(aggregate),
|
|
127
|
+
consistency: consistency_hint(core.optimizer_hints)
|
|
120
128
|
)
|
|
121
129
|
end
|
|
122
130
|
|
|
131
|
+
def consistency_hint(hints)
|
|
132
|
+
return unless hints
|
|
133
|
+
|
|
134
|
+
hint = hints.expr.reverse.find { |h| h.start_with?("consistency=") }
|
|
135
|
+
hint&.delete_prefix("consistency=")
|
|
136
|
+
end
|
|
137
|
+
|
|
123
138
|
def visit_Arel_Nodes_InsertStatement(o)
|
|
124
139
|
raise NotImplementedError, "INSERT ... SELECT" if o.select
|
|
125
140
|
|
|
@@ -182,7 +197,7 @@ module Arel::Visitors
|
|
|
182
197
|
def visit_Arel_Table(o) = o.name
|
|
183
198
|
|
|
184
199
|
# filters
|
|
185
|
-
def visit_Arel_Nodes_And(o) =
|
|
200
|
+
def visit_Arel_Nodes_And(o) = conjunction(o.children.map { |c| visit(c) })
|
|
186
201
|
def visit_Arel_Nodes_Or(o) = [ "Or", [ visit(o.left), visit(o.right) ] ]
|
|
187
202
|
def visit_Arel_Nodes_Grouping(o) = visit(o.expr)
|
|
188
203
|
|
|
@@ -200,10 +215,18 @@ module Arel::Visitors
|
|
|
200
215
|
"Lt" => "AnyLt", "Lte" => "AnyLte", "Gt" => "AnyGt", "Gte" => "AnyGte"
|
|
201
216
|
}.freeze
|
|
202
217
|
|
|
218
|
+
MATCHES_MISSING = [ "Lt", "Lte" ].freeze
|
|
219
|
+
|
|
203
220
|
def comparison(node, operator, value = visit(node.right))
|
|
221
|
+
attribute = visit(node.left)
|
|
204
222
|
operator = ARRAY_OPERATORS.fetch(operator) if array_attribute?(node.left) && !value.nil?
|
|
223
|
+
filter = [ attribute, operator, value ]
|
|
205
224
|
|
|
206
|
-
|
|
225
|
+
if MATCHES_MISSING.include?(operator) && !value.nil?
|
|
226
|
+
[ "And", [ filter, [ attribute, "NotEq", nil ] ] ]
|
|
227
|
+
else
|
|
228
|
+
filter
|
|
229
|
+
end
|
|
207
230
|
end
|
|
208
231
|
|
|
209
232
|
def array_attribute?(node)
|
|
@@ -227,7 +250,7 @@ module Arel::Visitors
|
|
|
227
250
|
def visit_Arel_Nodes_Between(o)
|
|
228
251
|
low, high = o.right.children.map { |bound| visit(bound) }
|
|
229
252
|
|
|
230
|
-
[
|
|
253
|
+
conjunction([ comparison(o, "Gte", low), comparison(o, "Lte", high) ])
|
|
231
254
|
end
|
|
232
255
|
|
|
233
256
|
def visit_Arel_Nodes_Not(o) = [ "Not", visit(o.expr) ]
|
|
@@ -5,10 +5,12 @@ module Turbopuffer
|
|
|
5
5
|
extend ActiveSupport::Concern
|
|
6
6
|
|
|
7
7
|
DISTANCE_METRICS = [ "cosine_distance", "euclidean_squared" ].freeze
|
|
8
|
+
CONSISTENCY_LEVELS = [ "strong", "eventual" ].freeze
|
|
8
9
|
|
|
9
10
|
included do
|
|
10
11
|
class_attribute :turbopuffer_attributes, instance_accessor: false, default: [].freeze
|
|
11
12
|
class_attribute :_turbopuffer_distance_metric, instance_accessor: false, default: "cosine_distance"
|
|
13
|
+
class_attribute :_turbopuffer_consistency, instance_accessor: false, default: nil
|
|
12
14
|
end
|
|
13
15
|
|
|
14
16
|
class << self
|
|
@@ -32,6 +34,17 @@ module Turbopuffer
|
|
|
32
34
|
|
|
33
35
|
model
|
|
34
36
|
end
|
|
37
|
+
|
|
38
|
+
def validate_consistency(level)
|
|
39
|
+
level = level.to_s
|
|
40
|
+
|
|
41
|
+
unless CONSISTENCY_LEVELS.include?(level)
|
|
42
|
+
raise ArgumentError,
|
|
43
|
+
"consistency must be one of #{CONSISTENCY_LEVELS.join(", ")}, got #{level.inspect}"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
level
|
|
47
|
+
end
|
|
35
48
|
end
|
|
36
49
|
|
|
37
50
|
class_methods do
|
|
@@ -50,6 +63,8 @@ module Turbopuffer
|
|
|
50
63
|
def turbopuffer_distance_metric(value = nil)
|
|
51
64
|
return _turbopuffer_distance_metric if value.nil?
|
|
52
65
|
|
|
66
|
+
value = value.to_s
|
|
67
|
+
|
|
53
68
|
unless DISTANCE_METRICS.include?(value)
|
|
54
69
|
raise ArgumentError,
|
|
55
70
|
"distance metric must be one of #{DISTANCE_METRICS.join(", ")}, got #{value.inspect}"
|
|
@@ -67,6 +82,16 @@ module Turbopuffer
|
|
|
67
82
|
::Turbopuffer::ActiveRecord::Glob.new(pattern, case_sensitive:)
|
|
68
83
|
end
|
|
69
84
|
|
|
85
|
+
def consistency(level)
|
|
86
|
+
optimizer_hints("consistency=#{Model.validate_consistency(level)}")
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def turbopuffer_consistency(value = nil)
|
|
90
|
+
return _turbopuffer_consistency if value.nil?
|
|
91
|
+
|
|
92
|
+
self._turbopuffer_consistency = Model.validate_consistency(value)
|
|
93
|
+
end
|
|
94
|
+
|
|
70
95
|
def predicate_builder
|
|
71
96
|
@predicate_builder ||= super.tap do |builder|
|
|
72
97
|
builder.register_handler(::Regexp, lambda { |attribute, regexp|
|
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.3
|
|
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-22 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: activerecord
|