rails-paradedb 0.12.0 → 0.13.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6cec3ad6c144476e82c57a3f9cba2b9ff497a838c34c032919b1eb785d4da435
4
- data.tar.gz: 2af9e08e2bf7b9a12f9c005e99bc73f4a804f8a0c23f50c1fbc57637d6b87109
3
+ metadata.gz: 87745906c0e84ce264fcb243665c8edaf756218568745eecbe2781b0aa35f52f
4
+ data.tar.gz: 499f1837259b7a2c2a56ff6c55a98ff8dc6ef72e9de08517631215d9a7c091c2
5
5
  SHA512:
6
- metadata.gz: 1af9bfba690e6ba816fc5972f576885b72408594e68504e6d3bbf063944f6392debcf3cf7a028b5d08ba626d6acfaaeb7dbcb526db4adafa96cec987902fb6fa
7
- data.tar.gz: 384b58ba9857bc2edb9a984ab71bd60d455daf5add4b3fc83b0c53458892496d21959b84e71680daddf7acc48f7c2832dbf930185661fb3ff587a22399a4bd84
6
+ metadata.gz: ffcfe007ab852d8b36f69e9af1853dc1b0df485bfdbb0599749aa6ee27e1dd43707ea8958733c6b4c112fdd3d2ba6038d460dfb8c3062475e53826164d54a3f7
7
+ data.tar.gz: 236e0df31d2f705f3b7f015e5c051951e4aec16e3438961e5ff2a2757ab007f3a21565c312847aaa26fc844d3fd9c16e3c732634ba2d1d9b3c4a00f9a30263a9
data/CHANGELOG.md CHANGED
@@ -4,6 +4,21 @@ All notable changes to this project will be documented in this file. The format
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.13.0] - 2026-10-07
8
+
9
+ ### Added
10
+
11
+ - Index partitioning, target segment count, and vector quantization configuration.
12
+ - Vector storage, configuration, and estimator diagnostics, plus aggregation visibility modes.
13
+ - Snippet-position pagination with `limit` and `offset`.
14
+ - Index layer sizes, background layer sizes, and mutable segment row limits.
15
+ - Index search tokenizer configuration.
16
+
17
+ ### Changed
18
+
19
+ - **Breaking:** Remove the key-field setting from index definitions, migration helpers, and schema dumps. Query helpers use the model primary key. Require ParadeDB 0.26.0 or newer.
20
+ - **Breaking:** Update vector index build options to `training_sample_ratio` and `max_leaf_size` for ParadeDB 0.26.0 and remove the obsolete cluster replication option.
21
+
7
22
  ## [0.12.0] - 2026-08-13
8
23
 
9
24
  ### Changed
@@ -183,6 +198,7 @@ All notable changes to this project will be documented in this file. The format
183
198
  - Schema dump/load round-trip for tokenizer configuration and index options
184
199
  (including `target_segment_count`)
185
200
 
201
+ [0.13.0]: https://github.com/paradedb/rails-paradedb/releases/tag/v0.13.0
186
202
  [0.12.0]: https://github.com/paradedb/rails-paradedb/releases/tag/v0.12.0
187
203
  [0.11.0]: https://github.com/paradedb/rails-paradedb/releases/tag/v0.11.0
188
204
  [0.10.0]: https://github.com/paradedb/rails-paradedb/releases/tag/v0.10.0
data/README.md CHANGED
@@ -10,16 +10,16 @@
10
10
  </h1>
11
11
 
12
12
  <p align="center">
13
- <b>Search without a second system.</b><br/>
13
+ <b>Just use Postgres.</b><br/>
14
14
  One Postgres for your application data, full-text search, vector retrieval, and aggregations.
15
15
  </p>
16
16
 
17
17
  <h3 align="center">
18
18
  <a href="https://paradedb.com">Website</a> &bull;
19
- <a href="https://docs.paradedb.com">Docs</a> &bull;
19
+ <a href="https://www.paradedb.com/docs/start/introduction">Docs</a> &bull;
20
20
  <a href="https://paradedb.com/slack/">Community</a> &bull;
21
21
  <a href="https://paradedb.com/blog/">Blog</a> &bull;
22
- <a href="https://docs.paradedb.com/changelog/">Changelog</a>
22
+ <a href="https://www.paradedb.com/docs/project/changelog">Changelog</a>
23
23
  </h3>
24
24
 
25
25
  <p align="center">
@@ -36,29 +36,17 @@
36
36
 
37
37
  ## ParadeDB for Rails
38
38
 
39
- The official [ActiveRecord](https://guides.rubyonrails.org/active_record_basics.html) integration for [ParadeDB](https://paradedb.com) (powered by the [`pg_search`](https://github.com/paradedb/paradedb) Postgres extension), including first-class support for managing ParadeDB indexes and running queries using the full ParadeDB API. The integration covers both [full-text search](https://docs.paradedb.com/documentation/full-text/overview) and [vector search](https://docs.paradedb.com/documentation/vector/overview) over pgvector `vector` types. Follow the [getting started guide](https://docs.paradedb.com/documentation/getting-started/environment#rails) to begin.
39
+ The official [ActiveRecord](https://guides.rubyonrails.org/active_record_basics.html) integration for ParadeDB. Get started with the [setup guide](https://www.paradedb.com/docs/start/connect-your-app#rails).
40
40
 
41
41
  ## Requirements & Compatibility
42
42
 
43
- | Component | Supported |
44
- | ---------- | ------------------------------------------------------------------ |
45
- | Ruby | 3.2+ |
46
- | Rails | 7.2+ |
47
- | ParadeDB | 0.25.0+ |
48
- | PostgreSQL | 15+ (with the ParadeDB pg_search extension) |
49
- | pgvector | Required for vector search (included in the ParadeDB Docker image) |
50
-
51
- ## Examples
52
-
53
- - [Quickstart](examples/quickstart/quickstart.rb)
54
- - [Vector Search](examples/vector_search/vector_search.rb)
55
- - [Faceted Search](examples/faceted_search/faceted_search.rb)
56
- - [Hybrid Search (RRF)](examples/hybrid_rrf/hybrid_rrf.rb)
57
- - [Retrieval-Augmented Generation (RAG)](examples/rag/rag.rb)
58
- - [Autocomplete](examples/autocomplete/autocomplete.rb)
59
- - [More Like This](examples/more_like_this/more_like_this.rb)
60
-
61
- See [examples/README.md](examples/README.md) for setup instructions and a description of each example.
43
+ | Component | Supported |
44
+ | -------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
45
+ | Ruby | 3.2+ |
46
+ | Rails | 7.2+ |
47
+ | PostgreSQL | 15+ |
48
+ | pgvector | 0.7.0+ (provides vector types for ParadeDB’s [native vector search](https://www.paradedb.com/docs/reference/vector/overview)) |
49
+ | ParadeDB / pg_search | 0.26.0+ |
62
50
 
63
51
  ## Contributing
64
52
 
@@ -66,15 +54,7 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, running tests, lin
66
54
 
67
55
  ## Support
68
56
 
69
- If you're missing a feature or have found a bug, please open a
70
- [GitHub Issue](https://github.com/paradedb/rails-paradedb/issues/new/choose).
71
-
72
- To get community support, you can:
73
-
74
- - Post a question in the [ParadeDB Slack Community](https://paradedb.com/slack)
75
- - Ask for help on our [GitHub Discussions](https://github.com/paradedb/paradedb/discussions)
76
-
77
- If you need commercial support, please [contact the ParadeDB team](mailto:sales@paradedb.com).
57
+ If you're missing a feature or have found a bug, please open a [GitHub Issue](https://github.com/paradedb/rails-paradedb/issues/new/choose). For community support, join the [ParadeDB Slack Community](https://paradedb.com/slack).
78
58
 
79
59
  ## Acknowledgments
80
60
 
@@ -2,7 +2,6 @@
2
2
 
3
3
  class <%= class_name %>Index < ParadeDB::Index
4
4
  self.table_name = :<%= table_name %>
5
- self.key_field = :id
6
5
  self.fields = {
7
6
  id: {},
8
7
  <% fields.each do |field| -%>
@@ -13,6 +13,25 @@ module ParadeDB
13
13
  execute_table_function(connection, sql)
14
14
  end
15
15
 
16
+ def vector_info(index, field, connection: ActiveRecord::Base.connection)
17
+ args = "#{connection.quote(index.to_s)}::regclass, #{connection.quote(field.to_s)}::text"
18
+ execute_table_function(connection, "SELECT * FROM paradedb.vector_info(#{args})")
19
+ end
20
+
21
+ def vector_config(index, field, connection: ActiveRecord::Base.connection)
22
+ args = "#{connection.quote(index.to_s)}::regclass, #{connection.quote(field.to_s)}::text"
23
+ execute_table_function(connection, "SELECT * FROM paradedb.vector_config(#{args})")
24
+ end
25
+
26
+ def vector_estimator_info(index, field, queries: nil, connection: ActiveRecord::Base.connection)
27
+ args = "#{connection.quote(index.to_s)}::regclass, #{connection.quote(field.to_s)}::text"
28
+ unless queries.nil?
29
+ vectors = queries.map { |query| connection.quote("[#{query.map { |value| Float(value) }.join(',')}]") }
30
+ args += ", ARRAY[#{vectors.join(', ')}]::vector[]"
31
+ end
32
+ execute_table_function(connection, "SELECT * FROM paradedb.vector_estimator_info(#{args})")
33
+ end
34
+
16
35
  def verify_index(
17
36
  index,
18
37
  heapallindexed: false,
@@ -6,16 +6,12 @@ require_relative "vector"
6
6
  module ParadeDB
7
7
  class Index
8
8
  class << self
9
- attr_writer :table_name, :key_field, :index_name, :fields, :index_options, :where
9
+ attr_writer :table_name, :index_name, :fields, :index_options, :where
10
10
 
11
11
  def table_name
12
12
  @table_name
13
13
  end
14
14
 
15
- def key_field
16
- @key_field
17
- end
18
-
19
15
  def index_name
20
16
  @index_name || default_index_name
21
17
  end
@@ -83,15 +79,14 @@ module ParadeDB
83
79
  # Consumed by migration helpers; validates and normalizes the DSL class
84
80
  class DefinitionCompiler
85
81
  FIELD_OPTION_KEYS = %i[fast record normalizer expand_dots].freeze
86
- INDEX_OPTION_KEYS = %i[target_segment_count centroid_ratio training_samples_per_centroid cluster_replication].freeze
87
- POSITIVE_INTEGER_INDEX_OPTION_KEYS = %i[target_segment_count training_samples_per_centroid cluster_replication].freeze
82
+ INDEX_OPTION_KEYS = %i[target_segment_count training_sample_ratio max_leaf_size partition_by vector_fields search_tokenizer layer_sizes background_layer_sizes mutable_segment_rows].freeze
83
+ POSITIVE_INTEGER_INDEX_OPTION_KEYS = %i[target_segment_count max_leaf_size].freeze
88
84
 
89
85
  class Compiled
90
- attr_reader :table_name, :key_field, :index_name, :entries, :index_options, :field_options, :where
86
+ attr_reader :table_name, :index_name, :entries, :index_options, :field_options, :where
91
87
 
92
- def initialize(table_name:, key_field:, index_name:, entries:, index_options:, field_options:, where:)
88
+ def initialize(table_name:, index_name:, entries:, index_options:, field_options:, where:)
93
89
  @table_name = table_name
94
- @key_field = key_field
95
90
  @index_name = index_name
96
91
  @entries = entries
97
92
  @index_options = index_options
@@ -104,7 +99,6 @@ module ParadeDB
104
99
  class << self
105
100
  def compile!(klass)
106
101
  table_name = require_symbol!(klass.table_name, "table_name")
107
- key_field = require_symbol!(klass.key_field, "key_field")
108
102
  index_name = klass.index_name.to_s
109
103
  raise InvalidIndexDefinition, "index_name must be present" if index_name.strip.empty?
110
104
 
@@ -116,12 +110,10 @@ module ParadeDB
116
110
 
117
111
  index_options = normalize_index_options(klass.index_options)
118
112
 
119
- validate_key_field_shape!(key_field.to_s, entries)
120
113
  validate_query_key_collisions!(entries)
121
114
 
122
115
  Compiled.new(
123
116
  table_name: table_name,
124
- key_field: key_field,
125
117
  index_name: index_name,
126
118
  entries: entries,
127
119
  index_options: index_options,
@@ -255,14 +247,29 @@ module ParadeDB
255
247
  end
256
248
  end
257
249
 
258
- if normalized.key?(:centroid_ratio)
259
- ratio = normalized[:centroid_ratio]
250
+ if normalized[:search_tokenizer].is_a?(ParadeDB::Tokenizer)
251
+ normalized[:search_tokenizer] = normalized[:search_tokenizer].render_search
252
+ end
253
+ %i[search_tokenizer layer_sizes background_layer_sizes].each do |key|
254
+ if normalized.key?(key) && (!normalized[key].is_a?(String) || normalized[key].strip.empty?)
255
+ raise InvalidIndexDefinition, "index_options[#{key.inspect}] must be a non-empty string"
256
+ end
257
+ end
258
+ if normalized.key?(:mutable_segment_rows) && (!normalized[:mutable_segment_rows].is_a?(Integer) || !(0..10000).cover?(normalized[:mutable_segment_rows]))
259
+ raise InvalidIndexDefinition, "mutable_segment_rows must be an integer between 0 and 10000"
260
+ end
261
+ if normalized.key?(:training_sample_ratio)
262
+ ratio = normalized[:training_sample_ratio]
260
263
  unless ratio.is_a?(Numeric) && ratio >= 0.000001 && ratio <= 1.0
261
264
  raise InvalidIndexDefinition,
262
- "index_options[:centroid_ratio] must be a Numeric between 0.000001 and 1.0"
265
+ "index_options[:training_sample_ratio] must be a Numeric between 0.000001 and 1.0"
263
266
  end
264
267
  end
265
268
 
269
+ if normalized.key?(:vector_fields) && !normalized[:vector_fields].is_a?(Hash)
270
+ raise InvalidIndexDefinition, "index_options[:vector_fields] must be a Hash"
271
+ end
272
+
266
273
  normalized
267
274
  end
268
275
 
@@ -288,23 +295,7 @@ module ParadeDB
288
295
  end
289
296
  end
290
297
 
291
- def validate_key_field_shape!(key_field_name, entries)
292
- unless entries.any? { |entry| entry.source == key_field_name }
293
- raise InvalidIndexDefinition,
294
- "key_field #{key_field_name.inspect} must be present in fields."
295
- end
296
298
 
297
- first_entry = entries.first
298
- unless first_entry.source == key_field_name
299
- raise InvalidIndexDefinition,
300
- "key_field #{key_field_name.inspect} must be first in fields."
301
- end
302
-
303
- return if first_entry.tokenizer.nil?
304
-
305
- raise InvalidIndexDefinition,
306
- "key_field #{key_field_name.inspect} must not be tokenized."
307
- end
308
299
  end
309
300
  end
310
301
  end
@@ -25,11 +25,10 @@ module ParadeDB
25
25
  remember_schema_index_reference(resolved)
26
26
  end
27
27
 
28
- def add_paradedb_index(table, fields:, key_field:, name: nil, index_options: nil, where: nil, if_not_exists: false, concurrently: false)
28
+ def add_paradedb_index(table, fields:, name: nil, index_options: nil, where: nil, if_not_exists: false, concurrently: false)
29
29
  ensure_postgresql_adapter!
30
30
  anonymous = Class.new(ParadeDB::Index)
31
31
  anonymous.table_name = table
32
- anonymous.key_field = key_field
33
32
  anonymous.index_name = name unless name.nil?
34
33
  anonymous.fields = fields
35
34
  anonymous.index_options = index_options unless index_options.nil?
@@ -86,26 +85,30 @@ module ParadeDB
86
85
  prefix = if_not_exists ? "IF NOT EXISTS " : ""
87
86
  fields_sql = compiled.entries.map { |entry| paradedb_entry_sql(entry) }.join(", ")
88
87
  with_options_sql = paradedb_with_options_sql(compiled)
88
+ with_clause = with_options_sql.empty? ? "" : "\nWITH (#{with_options_sql})"
89
89
  where_sql = compiled.where ? "\nWHERE #{compiled.where}" : ""
90
90
 
91
91
  <<~SQL.strip.gsub(/\s+/, " ")
92
92
  CREATE INDEX#{modifier} #{prefix}#{quote_table_name(compiled.index_name)} ON #{quote_table_name(compiled.table_name)}
93
- USING paradedb (#{fields_sql})
94
- WITH (#{with_options_sql})#{where_sql}
93
+ USING paradedb (#{fields_sql})#{with_clause}#{where_sql}
95
94
  SQL
96
95
  end
97
96
 
98
97
  def paradedb_with_options_sql(compiled)
99
98
  options = []
100
- options << "key_field=#{quote(compiled.key_field.to_s)}"
101
-
102
99
  compiled.index_options.each do |key, value|
103
100
  name = key.to_sym
104
101
  case name
105
- when :target_segment_count, :training_samples_per_centroid, :cluster_replication
102
+ when :target_segment_count, :max_leaf_size, :mutable_segment_rows
106
103
  options << "#{name}=#{Integer(value)}"
107
- when :centroid_ratio
108
- options << "centroid_ratio=#{Float(value)}"
104
+ when :partition_by
105
+ options << "#{name}=#{quote(value.join(","))}"
106
+ when :search_tokenizer, :layer_sizes, :background_layer_sizes
107
+ options << "#{name}=#{quote(value.to_s)}"
108
+ when :vector_fields
109
+ options << "vector_fields=#{quote(JSON.generate(value))}"
110
+ when :training_sample_ratio
111
+ options << "training_sample_ratio=#{Float(value)}"
109
112
  else
110
113
  raise ParadeDB::InvalidIndexDefinition, "unsupported index option #{key.inspect}"
111
114
  end
@@ -322,12 +325,11 @@ module ParadeDB
322
325
  table = row["table_name"]
323
326
  name = row["index_name"]
324
327
 
325
- key_field = extract_paradedb_key_field(indexdef)
326
328
  index_options = extract_paradedb_index_options(row["reloptions"])
327
329
  fields_sql = extract_paradedb_fields_sql(indexdef)
328
330
  where = normalize_paradedb_where_clause(row["where_clause"])
329
331
 
330
- if key_field && fields_sql
332
+ if fields_sql
331
333
  field_sqls = split_paradedb_top_level(fields_sql).map(&:strip)
332
334
  parsed = field_sqls.map { |f| paradedb_parse_column_entry(f) }
333
335
 
@@ -350,9 +352,8 @@ module ParadeDB
350
352
  end
351
353
 
352
354
  statement = "add_paradedb_index #{table.to_sym.inspect}, " \
353
- "fields: { #{fields_pairs.join(', ')} }, " \
354
- "key_field: #{key_field.to_sym.inspect}, " \
355
- "name: #{name.inspect}"
355
+ "fields: { #{fields_pairs.join(', ')} }, "
356
+ statement += "name: #{name.inspect}"
356
357
  unless index_options.empty?
357
358
  statement += ", index_options: #{ruby_hash_literal(index_options)}"
358
359
  end
@@ -363,28 +364,24 @@ module ParadeDB
363
364
  end
364
365
  end
365
366
 
366
- def extract_paradedb_key_field(indexdef)
367
- quoted = indexdef.match(/WITH\s*\([^)]*key_field\s*=\s*'((?:[^']|'')*)'/i)
368
- return quoted[1].gsub("''", "'") if quoted
369
-
370
- unquoted = indexdef.match(/WITH\s*\([^)]*key_field\s*=\s*([a-zA-Z_][a-zA-Z0-9_]*)/i)
371
- return unquoted[1] if unquoted
372
-
373
- nil
374
- end
375
-
376
367
  def extract_paradedb_index_options(reloptions)
377
368
  paradedb_reloption_entries(reloptions).each_with_object({}) do |entry, options|
378
369
  key, separator, value = entry.to_s.partition("=")
379
370
  next if separator.empty?
380
371
 
381
372
  case key
382
- when "target_segment_count", "training_samples_per_centroid", "cluster_replication"
373
+ when "target_segment_count", "max_leaf_size", "mutable_segment_rows"
383
374
  parsed = Integer(value, 10, exception: false)
384
375
  options[key.to_sym] = parsed if parsed
385
- when "centroid_ratio"
376
+ when "partition_by"
377
+ options[key.to_sym] = value.split(",")
378
+ when "search_tokenizer", "layer_sizes", "background_layer_sizes"
379
+ options[key.to_sym] = value
380
+ when "vector_fields"
381
+ options[:vector_fields] = JSON.parse(value)
382
+ when "training_sample_ratio"
386
383
  parsed = Float(value, exception: false)
387
- options[:centroid_ratio] = parsed if parsed
384
+ options[:training_sample_ratio] = parsed if parsed
388
385
  end
389
386
  end
390
387
  end
@@ -868,11 +865,10 @@ if defined?(ActiveRecord::Migration)
868
865
  connection.replace_paradedb_index(index_klass)
869
866
  end
870
867
 
871
- def add_paradedb_index(table, fields:, key_field:, name: nil, index_options: nil, where: nil, if_not_exists: false, concurrently: false)
868
+ def add_paradedb_index(table, fields:, name: nil, index_options: nil, where: nil, if_not_exists: false, concurrently: false)
872
869
  connection.add_paradedb_index(
873
870
  table,
874
871
  fields: fields,
875
- key_field: key_field,
876
872
  name: name,
877
873
  index_options: index_options,
878
874
  where: where,
@@ -23,7 +23,6 @@ module ParadeDB
23
23
  :paradedb_index_class,
24
24
  :paradedb_index_classes,
25
25
  :paradedb_indexed_fields,
26
- :paradedb_key_field,
27
26
  :paradedb_index_name,
28
27
  :paradedb_validate_index!
29
28
  ].freeze
@@ -155,11 +154,6 @@ module ParadeDB
155
154
  definition.entries.map(&:query_key).uniq
156
155
  end
157
156
 
158
- def paradedb_key_field
159
- definition = paradedb_index_definition
160
- definition&.key_field
161
- end
162
-
163
157
  def paradedb_index_name
164
158
  definition = paradedb_index_definition
165
159
  definition&.index_name
@@ -159,14 +159,21 @@ module ParadeDB
159
159
  ::Arel::Nodes::NamedFunction.new("pdb.snippets", args)
160
160
  end
161
161
 
162
- def snippet_positions(column)
163
- ::Arel::Nodes::NamedFunction.new("pdb.snippet_positions", [column_node(column)])
162
+ def snippet_positions(column, limit: nil, offset: nil)
163
+ args = [column_node(column)]
164
+ args << keyword_arg_node("limit", limit, quoted_name: true) unless limit.nil?
165
+ args << keyword_arg_node("offset", offset, quoted_name: true) unless offset.nil?
166
+ ::Arel::Nodes::NamedFunction.new("pdb.snippet_positions", args)
164
167
  end
165
168
 
166
- def agg(json, exact: nil)
169
+ def agg(json, exact: nil, visibility: nil)
167
170
  raise ArgumentError, "exact must be true, false, or nil" unless exact.nil? || exact == true || exact == false
168
171
 
169
172
  args = [quoted_value(json)]
173
+ unless visibility.nil?
174
+ raise ArgumentError, "Specify visibility or exact, not both" unless exact.nil?
175
+ args << quoted_value(visibility.to_s)
176
+ end
170
177
  args << quoted_value(false) if exact == false
171
178
  ::Arel::Nodes::NamedFunction.new("pdb.agg", args)
172
179
  end
@@ -246,9 +246,9 @@ module ParadeDB
246
246
 
247
247
  def more_like_this(key, fields: nil, **options)
248
248
  ensure_paradedb_runtime!
249
- runtime_key_field = paradedb_runtime_key_field
250
- key_value = more_like_this_key_value(key, runtime_key_field)
251
- pk_node = builder[runtime_key_field]
249
+ row_identifier = primary_key
250
+ key_value = more_like_this_key_value(key, row_identifier)
251
+ pk_node = builder[row_identifier]
252
252
  mlt_options = normalize_more_like_this_options(options)
253
253
  node = builder.more_like_this(pk_node, key_value, fields: fields, options: mlt_options)
254
254
  where(grouped(node))
@@ -257,7 +257,7 @@ module ParadeDB
257
257
  # ---- Decorators ----
258
258
 
259
259
  def with_score
260
- with_projection(builder.score(paradedb_runtime_key_field).as("search_score"))
260
+ with_projection(builder.score(primary_key).as("search_score"))
261
261
  end
262
262
 
263
263
  def with_snippet(column, start_tag: nil, end_tag: nil, max_chars: nil)
@@ -299,14 +299,14 @@ module ParadeDB
299
299
  with_projection(snippets.as(normalize_projection_alias(as, "#{column}_snippets")))
300
300
  end
301
301
 
302
- def with_snippet_positions(column, as: nil)
303
- positions = builder.snippet_positions(column)
302
+ def with_snippet_positions(column, as: nil, limit: nil, offset: nil)
303
+ positions = builder.snippet_positions(column, limit: normalize_integer_option!(limit, "limit"), offset: normalize_integer_option!(offset, "offset"))
304
304
  with_projection(positions.as(normalize_projection_alias(as, "#{column}_snippet_positions")))
305
305
  end
306
306
 
307
307
  # ---- Facets ----
308
308
 
309
- def facets(*fields, size: 10, order: :count_desc, missing: nil, agg: nil, exact: nil)
309
+ def facets(*fields, size: 10, order: :count_desc, missing: nil, agg: nil, exact: nil, visibility: nil)
310
310
  ensure_paradedb_runtime!
311
311
  validate_exact_option!(exact)
312
312
  if exact == false
@@ -319,23 +319,24 @@ module ParadeDB
319
319
  order: order,
320
320
  missing: missing,
321
321
  agg: agg,
322
- exact: exact
322
+ exact: exact,
323
+ visibility: visibility
323
324
  ).execute
324
325
  end
325
326
 
326
- def facets_agg(exact: nil, **named_aggregations)
327
+ def facets_agg(exact: nil, visibility: nil, **named_aggregations)
327
328
  validate_exact_option!(exact)
328
329
  agg_specs = normalize_named_aggregation_specs(named_aggregations)
329
- build_aggregation_query(agg_specs, exact: exact).execute
330
+ build_aggregation_query(agg_specs, exact: exact, visibility: visibility).execute
330
331
  end
331
332
 
332
333
  # Internal method to build facet query (for testing)
333
- def build_facet_query(fields:, size: 10, order: :count_desc, missing: nil, agg: nil, exact: nil)
334
+ def build_facet_query(fields:, size: 10, order: :count_desc, missing: nil, agg: nil, exact: nil, visibility: nil)
334
335
  ensure_paradedb_runtime!
335
336
  facet_args = normalize_facet_inputs(fields: fields, size: size, order: order, missing: missing, agg: agg)
336
337
  FacetQuery.build(
337
338
  relation: self,
338
- primary_key: paradedb_runtime_key_field,
339
+ primary_key: primary_key,
339
340
  builder: builder,
340
341
  fields: facet_args[:fields],
341
342
  size: facet_args[:size],
@@ -343,11 +344,12 @@ module ParadeDB
343
344
  missing: facet_args[:missing],
344
345
  agg: facet_args[:agg],
345
346
  exact: exact,
347
+ visibility: visibility,
346
348
  connection: connection
347
349
  )
348
350
  end
349
351
 
350
- def with_facets(*fields, size: 10, order: :count_desc, missing: nil, agg: nil, exact: nil)
352
+ def with_facets(*fields, size: 10, order: :count_desc, missing: nil, agg: nil, exact: nil, visibility: nil)
351
353
  ensure_paradedb_runtime!
352
354
  validate_exact_option!(exact)
353
355
  facet_args = normalize_facet_inputs(fields: fields, size: size, order: order, missing: missing, agg: agg)
@@ -370,14 +372,14 @@ module ParadeDB
370
372
  # Add window aggregates to SELECT using native Arel nodes.
371
373
  facet_selects = facet_fields.map do |field|
372
374
  json = facet_args[:agg] || facet_json(field, opts)
373
- builder.agg(json, exact: exact).over.as("_#{field}_facet")
375
+ builder.agg(json, exact: exact, visibility: visibility).over.as("_#{field}_facet")
374
376
  end
375
377
 
376
378
  rel = rel.select(klass.arel_table[::Arel.star]) if rel.select_values.empty?
377
379
  rel.select(*facet_selects)
378
380
  end
379
381
 
380
- def with_agg(exact: nil, **named_aggregations)
382
+ def with_agg(exact: nil, visibility: nil, **named_aggregations)
381
383
  ensure_paradedb_runtime!
382
384
  validate_exact_option!(exact)
383
385
  agg_specs = normalize_named_aggregation_specs(named_aggregations)
@@ -389,7 +391,7 @@ module ParadeDB
389
391
  end
390
392
 
391
393
  facet_selects = agg_specs.map do |alias_name, agg_spec|
392
- render_aggregation_node(agg_spec, exact: exact).over.as("_#{alias_name}_facet")
394
+ render_aggregation_node(agg_spec, exact: exact, visibility: visibility).over.as("_#{alias_name}_facet")
393
395
  end
394
396
 
395
397
  rel = rel.select(klass.arel_table[::Arel.star]) if rel.select_values.empty?
@@ -398,7 +400,7 @@ module ParadeDB
398
400
 
399
401
  # Grouped ParadeDB aggregations:
400
402
  # Product.search(:id).match_all.aggregate_by(:rating, agg: ParadeDB::Aggregations.value_count(:id))
401
- def aggregate_by(*group_fields, exact: nil, **named_aggregations)
403
+ def aggregate_by(*group_fields, exact: nil, visibility: nil, **named_aggregations)
402
404
  ensure_paradedb_runtime!
403
405
  validate_exact_option!(exact)
404
406
  normalized_group_fields = normalize_group_fields(group_fields)
@@ -409,14 +411,14 @@ module ParadeDB
409
411
 
410
412
  group_nodes = normalized_group_fields.map { |field| resolve_group_field_node(field) }
411
413
  aggregate_nodes = agg_specs.map do |alias_name, agg_spec|
412
- render_aggregation_node(agg_spec, exact: exact).as(alias_name.to_s)
414
+ render_aggregation_node(agg_spec, exact: exact, visibility: visibility).as(alias_name.to_s)
413
415
  end
414
416
 
415
417
  rel.except(:select, :group).select(*group_nodes, *aggregate_nodes).group(*group_nodes)
416
418
  end
417
419
 
418
420
  # Orders by vector distance for Top-K pushdown inside the ParadeDB index.
419
- # Adds `key_field @@@ pdb.all()` when the relation has no ParadeDB predicate,
421
+ # Adds `primary_key @@@ pdb.all()` when the relation has no ParadeDB predicate,
420
422
  # since vector ordering requires a @@@ predicate to activate the index scan.
421
423
  # Callers must add `.limit(k)`; the metric defaults to the index opclass metric.
422
424
  def nearest(column, vector, metric: nil)
@@ -434,7 +436,7 @@ module ParadeDB
434
436
 
435
437
  def ensure_paradedb_predicate
436
438
  # Add pdb.all() sentinel to force aggregate pushdown
437
- where(grouped(builder.match_all(paradedb_runtime_key_field)))
439
+ where(grouped(builder.match_all(primary_key)))
438
440
  end
439
441
 
440
442
  private
@@ -445,15 +447,6 @@ module ParadeDB
445
447
  klass.send(:paradedb_index_entry, column)&.metric
446
448
  end
447
449
 
448
- def paradedb_runtime_key_field
449
- return primary_key unless klass.respond_to?(:paradedb_key_field)
450
-
451
- key_field = klass.paradedb_key_field
452
- return primary_key if key_field.nil? || key_field.to_s.empty?
453
-
454
- key_field
455
- end
456
-
457
450
  def default_range_type_for_field(field)
458
451
  column = klass.columns_hash[field.to_s]
459
452
  return nil unless column
@@ -483,13 +476,13 @@ module ParadeDB
483
476
  sql_type if QueryBuilder::RANGE_TYPES.include?(sql_type)
484
477
  end
485
478
 
486
- def more_like_this_key_value(key, runtime_key_field)
487
- return key.public_send(runtime_key_field) if key.respond_to?(runtime_key_field)
488
- return key.id if runtime_key_field.to_s == "id" && key.respond_to?(:id)
479
+ def more_like_this_key_value(key, row_identifier)
480
+ return key.public_send(row_identifier) if key.respond_to?(row_identifier)
481
+ return key.id if row_identifier.to_s == "id" && key.respond_to?(:id)
489
482
  return key if scalar_more_like_this_key?(key)
490
483
 
491
484
  raise ArgumentError,
492
- "more_like_this key object must respond to #{runtime_key_field.inspect} or be a scalar id/document value"
485
+ "more_like_this key object must respond to #{row_identifier.inspect} or be a scalar id/document value"
493
486
  end
494
487
 
495
488
  def scalar_more_like_this_key?(key)
@@ -591,8 +584,8 @@ module ParadeDB
591
584
  end
592
585
  end
593
586
 
594
- def render_aggregation_node(agg_spec, exact:, builder_override: builder)
595
- agg_node = builder_override.agg(agg_spec[:json], exact: exact)
587
+ def render_aggregation_node(agg_spec, exact:, visibility: nil, builder_override: builder)
588
+ agg_node = builder_override.agg(agg_spec[:json], exact: exact, visibility: visibility)
596
589
  filter = resolve_agg_filter_node(agg_spec[:filter], builder_override)
597
590
  return agg_node if filter.nil?
598
591
 
@@ -632,13 +625,14 @@ module ParadeDB
632
625
  raise ArgumentError, "exact must be true, false, or nil"
633
626
  end
634
627
 
635
- def build_aggregation_query(agg_specs, exact: nil)
628
+ def build_aggregation_query(agg_specs, exact: nil, visibility: nil)
636
629
  AggregationQuery.build(
637
630
  relation: self,
638
- primary_key: paradedb_runtime_key_field,
631
+ primary_key: primary_key,
639
632
  builder: builder,
640
633
  agg_specs: agg_specs,
641
634
  exact: exact,
635
+ visibility: visibility,
642
636
  connection: connection
643
637
  )
644
638
  end
@@ -845,7 +839,7 @@ module ParadeDB
845
839
  class FacetQuery
846
840
  attr_reader :relation, :connection
847
841
 
848
- def self.build(relation:, primary_key:, builder:, fields:, size:, order:, missing:, agg:, exact:, connection:)
842
+ def self.build(relation:, primary_key:, builder:, fields:, size:, order:, missing:, agg:, exact:, visibility: nil, connection:)
849
843
  new(
850
844
  relation: relation,
851
845
  primary_key: primary_key,
@@ -856,11 +850,12 @@ module ParadeDB
856
850
  missing: missing,
857
851
  agg: agg,
858
852
  exact: exact,
853
+ visibility: visibility,
859
854
  connection: connection
860
855
  )
861
856
  end
862
857
 
863
- def initialize(relation:, primary_key:, builder:, fields:, size:, order:, missing:, agg:, exact:, connection:)
858
+ def initialize(relation:, primary_key:, builder:, fields:, size:, order:, missing:, agg:, exact:, visibility: nil, connection:)
864
859
  @connection = connection
865
860
  @relation = build_relation(
866
861
  relation: relation,
@@ -871,7 +866,8 @@ module ParadeDB
871
866
  order: order,
872
867
  missing: missing,
873
868
  agg: agg,
874
- exact: exact
869
+ exact: exact,
870
+ visibility: visibility
875
871
  )
876
872
  end
877
873
 
@@ -885,7 +881,7 @@ module ParadeDB
885
881
 
886
882
  private
887
883
 
888
- def build_relation(relation:, primary_key:, builder:, fields:, size:, order:, missing:, agg:, exact:)
884
+ def build_relation(relation:, primary_key:, builder:, fields:, size:, order:, missing:, agg:, exact:, visibility: nil)
889
885
  predicate_scope = relation.except(:select, :order, :limit, :offset, :group, :having, :distinct)
890
886
  predicate_scope = predicate_scope.select(relation.klass.arel_table[::Arel.star]) if predicate_scope.select_values.empty?
891
887
 
@@ -895,18 +891,18 @@ module ParadeDB
895
891
 
896
892
  source_alias = "paradedb_facet_source"
897
893
  source = predicate_scope.arel.as(source_alias)
898
- projections = build_facet_projections(relation, builder, fields, size, order, missing, agg, exact)
894
+ projections = build_facet_projections(relation, builder, fields, size, order, missing, agg, exact, visibility)
899
895
 
900
896
  relation.klass.unscoped.from(source).select(*projections)
901
897
  end
902
898
 
903
- def build_facet_projections(relation, builder, fields, size, order, missing, agg, exact)
904
- return [builder.agg(relation.send(:normalize_agg_json, agg), exact: exact).as("agg_facet")] if agg
899
+ def build_facet_projections(relation, builder, fields, size, order, missing, agg, exact, visibility)
900
+ return [builder.agg(relation.send(:normalize_agg_json, agg), exact: exact, visibility: visibility).as("agg_facet")] if agg
905
901
 
906
902
  fields.map do |field|
907
903
  opts = { size: size, order: order, missing: missing }
908
904
  json = relation.send(:facet_json, field, opts)
909
- builder.agg(json, exact: exact).as("#{field}_facet")
905
+ builder.agg(json, exact: exact, visibility: visibility).as("#{field}_facet")
910
906
  end
911
907
  end
912
908
 
@@ -939,18 +935,19 @@ module ParadeDB
939
935
  class AggregationQuery
940
936
  attr_reader :relation, :connection
941
937
 
942
- def self.build(relation:, primary_key:, builder:, agg_specs:, exact:, connection:)
938
+ def self.build(relation:, primary_key:, builder:, agg_specs:, exact:, visibility: nil, connection:)
943
939
  new(
944
940
  relation: relation,
945
941
  primary_key: primary_key,
946
942
  builder: builder,
947
943
  agg_specs: agg_specs,
948
944
  exact: exact,
945
+ visibility: visibility,
949
946
  connection: connection
950
947
  )
951
948
  end
952
949
 
953
- def initialize(relation:, primary_key:, builder:, agg_specs:, exact:, connection:)
950
+ def initialize(relation:, primary_key:, builder:, agg_specs:, exact:, visibility: nil, connection:)
954
951
  @connection = connection
955
952
  @agg_specs = agg_specs
956
953
  @relation = build_relation(
@@ -958,7 +955,8 @@ module ParadeDB
958
955
  primary_key: primary_key,
959
956
  builder: builder,
960
957
  agg_specs: agg_specs,
961
- exact: exact
958
+ exact: exact,
959
+ visibility: visibility
962
960
  )
963
961
  end
964
962
 
@@ -975,7 +973,7 @@ module ParadeDB
975
973
 
976
974
  attr_reader :agg_specs
977
975
 
978
- def build_relation(relation:, primary_key:, builder:, agg_specs:, exact:)
976
+ def build_relation(relation:, primary_key:, builder:, agg_specs:, exact:, visibility: nil)
979
977
  predicate_scope = relation.except(:select, :order, :limit, :offset, :group, :having, :distinct)
980
978
  predicate_scope = predicate_scope.select(relation.klass.arel_table[::Arel.star]) if predicate_scope.select_values.empty?
981
979
 
@@ -991,6 +989,7 @@ module ParadeDB
991
989
  :render_aggregation_node,
992
990
  agg_spec,
993
991
  exact: exact,
992
+ visibility: visibility,
994
993
  builder_override: projection_builder
995
994
  )
996
995
  agg_node.as("#{alias_name}_facet")
@@ -24,6 +24,12 @@ module ParadeDB
24
24
  return "pdb.#{name}(#{args.join(",")})"
25
25
  end
26
26
 
27
+ def render_search
28
+ args = Array(positional_args).map(&:to_s)
29
+ args.concat((options || {}).map { |key, value| "#{key}=#{value}" })
30
+ name + (args.empty? ? "" : "(#{args.join(',')})")
31
+ end
32
+
27
33
  def self.whitespace(options: nil)
28
34
  new("whitespace", nil, options)
29
35
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ParadeDB
4
- VERSION = "0.12.0"
4
+ VERSION = "0.13.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails-paradedb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.12.0
4
+ version: 0.13.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - ParadeDB
@@ -83,8 +83,8 @@ dependencies:
83
83
  - - "<"
84
84
  - !ruby/object:Gem::Version
85
85
  version: '9'
86
- description: Elastic-quality full-text search, vector retrieval, and aggregations
87
- in Postgres via ParadeDB and ActiveRecord.
86
+ description: One Postgres for your application data, full-text search, vector retrieval,
87
+ and aggregations.
88
88
  email:
89
89
  - support@paradedb.com
90
90
  executables: []
@@ -118,7 +118,7 @@ licenses:
118
118
  metadata:
119
119
  homepage_uri: https://github.com/paradedb/rails-paradedb
120
120
  source_code_uri: https://github.com/paradedb/rails-paradedb
121
- documentation_uri: https://docs.paradedb.com
121
+ documentation_uri: https://paradedb.com/docs
122
122
  changelog_uri: https://github.com/paradedb/rails-paradedb/blob/main/CHANGELOG.md
123
123
  bug_tracker_uri: https://github.com/paradedb/rails-paradedb/issues
124
124
  rdoc_options: []
@@ -137,5 +137,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
137
137
  requirements: []
138
138
  rubygems_version: 4.0.3
139
139
  specification_version: 4
140
- summary: Official ParadeDB integration for ActiveRecord
140
+ summary: The official ActiveRecord integration for ParadeDB.
141
141
  test_files: []