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 +4 -4
- data/CHANGELOG.md +16 -0
- data/README.md +12 -32
- data/lib/generators/parade_db/index/templates/index.rb.tt +0 -1
- data/lib/parade_db/diagnostics.rb +19 -0
- data/lib/parade_db/index.rb +23 -32
- data/lib/parade_db/migration_helpers.rb +25 -29
- data/lib/parade_db/model.rb +0 -6
- data/lib/parade_db/query_builder.rb +10 -3
- data/lib/parade_db/search_methods.rb +48 -49
- data/lib/parade_db/tokenizer.rb +6 -0
- data/lib/parade_db/version.rb +1 -1
- metadata +5 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 87745906c0e84ce264fcb243665c8edaf756218568745eecbe2781b0aa35f52f
|
|
4
|
+
data.tar.gz: 499f1837259b7a2c2a56ff6c55a98ff8dc6ef72e9de08517631215d9a7c091c2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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>
|
|
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> •
|
|
19
|
-
<a href="https://
|
|
19
|
+
<a href="https://www.paradedb.com/docs/start/introduction">Docs</a> •
|
|
20
20
|
<a href="https://paradedb.com/slack/">Community</a> •
|
|
21
21
|
<a href="https://paradedb.com/blog/">Blog</a> •
|
|
22
|
-
<a href="https://
|
|
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
|
|
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
|
|
44
|
-
|
|
|
45
|
-
| Ruby
|
|
46
|
-
| Rails
|
|
47
|
-
|
|
|
48
|
-
|
|
|
49
|
-
|
|
|
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
|
|
|
@@ -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,
|
data/lib/parade_db/index.rb
CHANGED
|
@@ -6,16 +6,12 @@ require_relative "vector"
|
|
|
6
6
|
module ParadeDB
|
|
7
7
|
class Index
|
|
8
8
|
class << self
|
|
9
|
-
attr_writer :table_name, :
|
|
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
|
|
87
|
-
POSITIVE_INTEGER_INDEX_OPTION_KEYS = %i[target_segment_count
|
|
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, :
|
|
86
|
+
attr_reader :table_name, :index_name, :entries, :index_options, :field_options, :where
|
|
91
87
|
|
|
92
|
-
def initialize(table_name:,
|
|
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.
|
|
259
|
-
|
|
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[:
|
|
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:,
|
|
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, :
|
|
102
|
+
when :target_segment_count, :max_leaf_size, :mutable_segment_rows
|
|
106
103
|
options << "#{name}=#{Integer(value)}"
|
|
107
|
-
when :
|
|
108
|
-
options << "
|
|
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
|
|
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
|
-
|
|
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", "
|
|
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 "
|
|
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[:
|
|
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:,
|
|
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,
|
data/lib/parade_db/model.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
250
|
-
key_value = more_like_this_key_value(key,
|
|
251
|
-
pk_node = builder[
|
|
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(
|
|
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:
|
|
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 `
|
|
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(
|
|
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,
|
|
487
|
-
return key.public_send(
|
|
488
|
-
return key.id if
|
|
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 #{
|
|
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:
|
|
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")
|
data/lib/parade_db/tokenizer.rb
CHANGED
|
@@ -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
|
data/lib/parade_db/version.rb
CHANGED
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.
|
|
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:
|
|
87
|
-
|
|
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://
|
|
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:
|
|
140
|
+
summary: The official ActiveRecord integration for ParadeDB.
|
|
141
141
|
test_files: []
|