activerecord-turbopuffer-adapter 0.1.2 → 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: 23fa3cc80f986e56ce18f779fb0d108fc441bc0eb38bc7f8b3eeb23d25ae1c7b
4
- data.tar.gz: 1f888b8b3bf5238a24f32d76bfcfa5fe71ef433a3b85c12db4b0f96d36c888f5
3
+ metadata.gz: cc9b47b80bd694b53052b54ca2647989e6bfd911c1a0f2e59056d61cfccf075e
4
+ data.tar.gz: a306ba71a2c62db295b438c6583f61a765d425a8f2888c328b831df31bf9840b
5
5
  SHA512:
6
- metadata.gz: c969a1e7195c56421d923e8d711f81c71028fe8b7ccbdb3b774ccb56671db31b75f5e9a9803bd28886ef503cf7c6477171f4175f799cbe025d65fba41e9a0476
7
- data.tar.gz: 96966fdeb4d6ebeebaa15d717404359947b5afee6c753b19e92e7fe9fe50ead8e708e7728b654e72ef7706042f7dca34bfbec7cdbf3f278e04fde7449b9b271d
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
- include_attributes: nil, upsert_rows: nil, aggregate_by: nil, group_by: nil)
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
 
@@ -120,10 +123,18 @@ module Arel::Visitors
120
123
  rank_by: o.orders.map { |ord| visit(ord) },
121
124
  include_attributes: attributes.flat_map { |p| visit(p) },
122
125
  group_by: core.groups.map { |g| visit(g) },
123
- aggregate_by: aggregate && visit(aggregate)
126
+ aggregate_by: aggregate && visit(aggregate),
127
+ consistency: consistency_hint(core.optimizer_hints)
124
128
  )
125
129
  end
126
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
+
127
138
  def visit_Arel_Nodes_InsertStatement(o)
128
139
  raise NotImplementedError, "INSERT ... SELECT" if o.select
129
140
 
@@ -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|
@@ -1,5 +1,5 @@
1
1
  module Turbopuffer
2
2
  module ActiveRecord
3
- VERSION = "0.1.2"
3
+ VERSION = "0.1.3"
4
4
  end
5
5
  end
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.2
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-20 00:00:00.000000000 Z
10
+ date: 2026-09-22 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: activerecord