rails-paradedb 0.9.0 → 0.10.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 +18 -1
- data/README.md +13 -7
- data/lib/generators/parade_db/index/index_generator.rb +4 -4
- data/lib/generators/parade_db/index/templates/migration.rb.tt +1 -1
- data/lib/parade_db/arel/builder.rb +25 -0
- data/lib/parade_db/arel/predications.rb +16 -0
- data/lib/parade_db/index.rb +25 -4
- data/lib/parade_db/migration_helpers.rb +88 -72
- data/lib/parade_db/model.rb +9 -2
- data/lib/parade_db/search_methods.rb +19 -0
- data/lib/parade_db/vector.rb +119 -0
- data/lib/parade_db/version.rb +1 -1
- data/lib/parade_db.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 296e696bfa9711537e984df359c43be2481b53a91f75c880dc33ac36aacf4b1c
|
|
4
|
+
data.tar.gz: 78781d5fc912c5b57053818d7f3e76e2d01251fb7d31a586edd5d28b88426e69
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5b4fecd2d8620a91e782ea62df980ab6638fec2906ad5a380664e97b77a8d0e1b4b656aa330cc4b9e80a99e18f2223cd2d50da6cea98245c63c1c4fe88ca7450
|
|
7
|
+
data.tar.gz: a6c42abf6b75aad3ee2d3b58651cfde545ba86deef7e33df0593cafc94da529818dbe41f290286e0acc3eb9bc62071d4d2b06d7976cb8c44d5a94be7c9a37722
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,22 @@ All notable changes to this project will be documented in this file. The format
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
[0.10.0] - 2026-08-04
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Native pgvector `vector(n)` column support: ActiveRecord attribute type, `t.vector` / `add_column :table, :col, :vector, limit: n` migration DSL, and `schema.rb` dumping — no `neighbor` gem required.
|
|
12
|
+
- Vector fields in ParadeDB indexes via `embedding: { metric: :l2 | :cosine | :ip }` in `ParadeDB::Index` fields and `add_paradedb_index`, emitted as `vector_l2_ops` / `vector_cosine_ops` / `vector_ip_ops` opclasses and round-tripped through the schema dumper.
|
|
13
|
+
- `Model.nearest(column, vector, metric: nil)` for Top-K vector search. Orders by the pgvector distance operator (`<->`, `<=>`, `<#>`), defaults the metric from the index definition, and adds `key_field @@@ pdb.all()` when the relation has no ParadeDB predicate.
|
|
14
|
+
- `l2_distance` / `cosine_distance` / `inner_product` / `vector_distance` Arel builder methods and `pdb_l2_distance` / `pdb_cosine_distance` / `pdb_inner_product` / `pdb_vector_distance` attribute predications.
|
|
15
|
+
- `examples/vector_search` example. `examples/hybrid_rrf` now uses the native vector support instead of `neighbor`, and `examples/rag` retrieval combines full-text search with `nearest`.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- **BREAKING**: Renamed the `bm25`-named migration helpers to `paradedb`: `add_bm25_index` is now `add_paradedb_index`, `remove_bm25_index` is now `remove_paradedb_index`, and `reindex_bm25` is now `reindex_paradedb_index`. The old names were removed; update migrations and `schema.rb` files to the new names.
|
|
20
|
+
- **BREAKING**: Index creation always emits `USING paradedb`, which requires pg_search 0.25.0+. There is no option to select the legacy `bm25` access method.
|
|
21
|
+
- **BREAKING**: The default index name is now `<table>_search_idx` (previously `<table>_bm25_idx`), and the index generator emits `Create<Model>SearchIndex` migrations named `create_<table>_search_index.rb`.
|
|
22
|
+
|
|
7
23
|
[0.9.0] - 2026-07-14
|
|
8
24
|
|
|
9
25
|
### Added
|
|
@@ -154,7 +170,8 @@ All notable changes to this project will be documented in this file. The format
|
|
|
154
170
|
- Schema dump/load round-trip for tokenizer configuration and index options
|
|
155
171
|
(including `target_segment_count`)
|
|
156
172
|
|
|
157
|
-
[Unreleased]: https://github.com/paradedb/rails-paradedb/compare/v0.
|
|
173
|
+
[Unreleased]: https://github.com/paradedb/rails-paradedb/compare/v0.10.0...HEAD
|
|
174
|
+
[0.10.0]: https://github.com/paradedb/rails-paradedb/releases/tag/v0.10.0
|
|
158
175
|
[0.9.0]: https://github.com/paradedb/rails-paradedb/releases/tag/v0.9.0
|
|
159
176
|
[0.8.0]: https://github.com/paradedb/rails-paradedb/releases/tag/v0.8.0
|
|
160
177
|
[0.7.0]: https://github.com/paradedb/rails-paradedb/releases/tag/v0.7.0
|
data/README.md
CHANGED
|
@@ -36,20 +36,26 @@
|
|
|
36
36
|
|
|
37
37
|
## ParadeDB for Rails
|
|
38
38
|
|
|
39
|
-
The official ActiveRecord integration for [ParadeDB](https://paradedb.com) (powered by the [`pg_search`](https://github.com/paradedb/paradedb) Postgres extension), including first-class support for managing
|
|
39
|
+
The official ActiveRecord 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. Follow the [getting started guide](https://docs.paradedb.com/documentation/getting-started/environment#rails) to begin.
|
|
40
40
|
|
|
41
41
|
## Requirements & Compatibility
|
|
42
42
|
|
|
43
|
-
| Component | Supported
|
|
44
|
-
| ---------- |
|
|
45
|
-
| Ruby | 3.2+
|
|
46
|
-
| Rails | 7.2+
|
|
47
|
-
| ParadeDB | 0.
|
|
48
|
-
| PostgreSQL | 15+ (PostgreSQL adapter with ParadeDB extension)
|
|
43
|
+
| Component | Supported |
|
|
44
|
+
| ---------- | ----------------------------------------------------------------- |
|
|
45
|
+
| Ruby | 3.2+ |
|
|
46
|
+
| Rails | 7.2+ |
|
|
47
|
+
| ParadeDB | 0.25.0+ |
|
|
48
|
+
| PostgreSQL | 15+ (PostgreSQL adapter with ParadeDB extension) |
|
|
49
|
+
| pgvector | Required for vector search; included in the ParadeDB Docker image |
|
|
50
|
+
|
|
51
|
+
## Vector Search
|
|
52
|
+
|
|
53
|
+
rails-paradedb supports full-text search and vector search over pgvector `vector(n)` columns. See the [vector search documentation](https://docs.paradedb.com/documentation/vector/overview) for details.
|
|
49
54
|
|
|
50
55
|
## Examples
|
|
51
56
|
|
|
52
57
|
- [Quickstart](examples/quickstart/quickstart.rb)
|
|
58
|
+
- [Vector Search](examples/vector_search/vector_search.rb)
|
|
53
59
|
- [Faceted Search](examples/faceted_search/faceted_search.rb)
|
|
54
60
|
- [Autocomplete](examples/autocomplete/autocomplete.rb)
|
|
55
61
|
- [More Like This](examples/more_like_this/more_like_this.rb)
|
|
@@ -15,7 +15,7 @@ module ParadeDB
|
|
|
15
15
|
class_option :concurrent, type: :boolean, default: false,
|
|
16
16
|
desc: "Add disable_ddl_transaction! to the migration (required for concurrent index creation)"
|
|
17
17
|
|
|
18
|
-
desc "Creates a ParadeDB::Index class and a
|
|
18
|
+
desc "Creates a ParadeDB::Index class and a ParadeDB index migration for MODEL."
|
|
19
19
|
|
|
20
20
|
def self.next_migration_number(dirname)
|
|
21
21
|
ActiveRecord::Generators::Base.next_migration_number(dirname)
|
|
@@ -26,17 +26,17 @@ module ParadeDB
|
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
def create_migration_file
|
|
29
|
-
migration_template "migration.rb.tt", "db/migrate/create_#{table_name}
|
|
29
|
+
migration_template "migration.rb.tt", "db/migrate/create_#{table_name}_search_index.rb"
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
private
|
|
33
33
|
|
|
34
34
|
def index_name
|
|
35
|
-
"#{table_name}
|
|
35
|
+
"#{table_name}_search_idx"
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
def migration_class_name
|
|
39
|
-
"Create#{class_name}
|
|
39
|
+
"Create#{class_name}SearchIndex"
|
|
40
40
|
end
|
|
41
41
|
end
|
|
42
42
|
end
|
|
@@ -10,6 +10,6 @@ class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Mi
|
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
def down
|
|
13
|
-
|
|
13
|
+
remove_paradedb_index :<%= table_name %>, name: :<%= index_name %>, if_exists: true
|
|
14
14
|
end
|
|
15
15
|
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
require "date"
|
|
3
3
|
require_relative "../tokenizer_sql"
|
|
4
|
+
require_relative "../vector"
|
|
4
5
|
|
|
5
6
|
module ParadeDB
|
|
6
7
|
module Arel
|
|
@@ -220,6 +221,23 @@ module ParadeDB
|
|
|
220
221
|
infix("@@@", column_node(column), rhs)
|
|
221
222
|
end
|
|
222
223
|
|
|
224
|
+
def l2_distance(column, vector)
|
|
225
|
+
vector_distance(column, vector, metric: :l2)
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def cosine_distance(column, vector)
|
|
229
|
+
vector_distance(column, vector, metric: :cosine)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def inner_product(column, vector)
|
|
233
|
+
vector_distance(column, vector, metric: :ip)
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def vector_distance(column, vector, metric: ParadeDB::Vector::DEFAULT_METRIC)
|
|
237
|
+
operator = ParadeDB::Vector::DISTANCE_OPERATORS.fetch(ParadeDB::Vector.normalize_metric(metric))
|
|
238
|
+
infix(operator, column_node(column), vector_operand(vector))
|
|
239
|
+
end
|
|
240
|
+
|
|
223
241
|
def score(key)
|
|
224
242
|
::Arel::Nodes::NamedFunction.new("pdb.score", [column_node(key)])
|
|
225
243
|
end
|
|
@@ -338,6 +356,13 @@ module ParadeDB
|
|
|
338
356
|
::Arel::Nodes.build_quoted(value)
|
|
339
357
|
end
|
|
340
358
|
|
|
359
|
+
def vector_operand(vector)
|
|
360
|
+
return vector if arel_expression?(vector)
|
|
361
|
+
|
|
362
|
+
literal = vector.is_a?(String) ? vector : ParadeDB::Vector.literal(vector)
|
|
363
|
+
Nodes::TypeCast.new(quoted_value(literal), "vector")
|
|
364
|
+
end
|
|
365
|
+
|
|
341
366
|
def proximity_query_node(proximity, boost: nil, const: nil)
|
|
342
367
|
unless proximity.is_a?(ParadeDB::Proximity::Clause)
|
|
343
368
|
raise ArgumentError, "near requires a ParadeDB.proximity(...) clause"
|
|
@@ -120,6 +120,22 @@ module ParadeDB
|
|
|
120
120
|
::Arel::Nodes::InfixOperation.new("@@@", self, rhs)
|
|
121
121
|
end
|
|
122
122
|
|
|
123
|
+
def pdb_l2_distance(vector)
|
|
124
|
+
BUILDER.l2_distance(self, vector)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def pdb_cosine_distance(vector)
|
|
128
|
+
BUILDER.cosine_distance(self, vector)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def pdb_inner_product(vector)
|
|
132
|
+
BUILDER.inner_product(self, vector)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def pdb_vector_distance(vector, metric: ParadeDB::Vector::DEFAULT_METRIC)
|
|
136
|
+
BUILDER.vector_distance(self, vector, metric: metric)
|
|
137
|
+
end
|
|
138
|
+
|
|
123
139
|
def pdb_score
|
|
124
140
|
::Arel::Nodes::NamedFunction.new("pdb.score", [self])
|
|
125
141
|
end
|
data/lib/parade_db/index.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "tokenizer"
|
|
4
|
+
require_relative "vector"
|
|
4
5
|
|
|
5
6
|
module ParadeDB
|
|
6
7
|
class Index
|
|
@@ -34,7 +35,7 @@ module ParadeDB
|
|
|
34
35
|
def default_index_name
|
|
35
36
|
return nil if table_name.nil?
|
|
36
37
|
|
|
37
|
-
"#{table_name}
|
|
38
|
+
"#{table_name}_search_idx"
|
|
38
39
|
end
|
|
39
40
|
|
|
40
41
|
def compiled_definition
|
|
@@ -96,7 +97,7 @@ module ParadeDB
|
|
|
96
97
|
@where = where
|
|
97
98
|
end
|
|
98
99
|
end
|
|
99
|
-
Entry = Struct.new(:source, :expression, :tokenizer, :options, :query_key, keyword_init: true)
|
|
100
|
+
Entry = Struct.new(:source, :expression, :tokenizer, :options, :query_key, :metric, keyword_init: true)
|
|
100
101
|
|
|
101
102
|
class << self
|
|
102
103
|
def compile!(klass)
|
|
@@ -153,7 +154,7 @@ module ParadeDB
|
|
|
153
154
|
end
|
|
154
155
|
normalized = (config || {}).each_with_object({}) { |(k, v), memo| memo[k.to_sym] = v }
|
|
155
156
|
|
|
156
|
-
unknown_keys = normalized.keys - (TokenizerParser::TOKENIZER_SINGLE_KEYS + [
|
|
157
|
+
unknown_keys = normalized.keys - (TokenizerParser::TOKENIZER_SINGLE_KEYS + %i[tokenizers metric] + FIELD_OPTION_KEYS)
|
|
157
158
|
unless unknown_keys.empty?
|
|
158
159
|
raise InvalidIndexDefinition,
|
|
159
160
|
"unknown field config keys for #{source_name.inspect}: #{unknown_keys.map(&:inspect).join(', ')}"
|
|
@@ -163,7 +164,27 @@ module ParadeDB
|
|
|
163
164
|
single_tokenizer_keys_present = TokenizerParser::TOKENIZER_SINGLE_KEYS.any? { |key| normalized.key?(key) }
|
|
164
165
|
|
|
165
166
|
is_alias = normalized[:alias] && normalized.length == 1
|
|
166
|
-
if
|
|
167
|
+
if normalized.key?(:metric)
|
|
168
|
+
unless normalized.length == 1
|
|
169
|
+
raise InvalidIndexDefinition,
|
|
170
|
+
"field #{source_name.inspect} cannot mix :metric with other field config keys"
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
begin
|
|
174
|
+
metric = ParadeDB::Vector.normalize_metric(normalized[:metric])
|
|
175
|
+
rescue ArgumentError => e
|
|
176
|
+
raise InvalidIndexDefinition, "field #{source_name.inspect}: #{e.message}"
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
entries << Entry.new(
|
|
180
|
+
source: source_name,
|
|
181
|
+
expression: expression?(source_name),
|
|
182
|
+
tokenizer: nil,
|
|
183
|
+
options: {},
|
|
184
|
+
query_key: source_name,
|
|
185
|
+
metric: metric
|
|
186
|
+
)
|
|
187
|
+
elsif is_alias
|
|
167
188
|
entries << Entry.new(
|
|
168
189
|
source: source_name,
|
|
169
190
|
expression: expression?(source_name),
|
|
@@ -20,12 +20,12 @@ module ParadeDB
|
|
|
20
20
|
ensure_postgresql_adapter!
|
|
21
21
|
resolved = resolve_index_klass(index_klass)
|
|
22
22
|
compiled = resolved.compiled_definition
|
|
23
|
-
|
|
23
|
+
remove_paradedb_index(compiled.table_name, name: compiled.index_name, if_exists: true)
|
|
24
24
|
execute(build_create_sql(compiled, if_not_exists: false))
|
|
25
25
|
remember_schema_index_reference(resolved)
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
-
def
|
|
28
|
+
def add_paradedb_index(table, fields:, key_field:, 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
|
|
@@ -38,31 +38,31 @@ module ParadeDB
|
|
|
38
38
|
create_paradedb_index(anonymous, if_not_exists: if_not_exists, concurrently: concurrently)
|
|
39
39
|
end
|
|
40
40
|
|
|
41
|
-
def
|
|
41
|
+
def remove_paradedb_index(table, name: nil, if_exists: false)
|
|
42
42
|
ensure_postgresql_adapter!
|
|
43
|
-
index_name = (name || "#{table}
|
|
43
|
+
index_name = (name || "#{table}_search_idx").to_s
|
|
44
44
|
prefix = if_exists ? "IF EXISTS " : ""
|
|
45
45
|
execute("DROP INDEX #{prefix}#{quote_table_name(index_name)}")
|
|
46
46
|
end
|
|
47
47
|
|
|
48
|
-
def
|
|
48
|
+
def reindex_paradedb_index(table, name: nil, concurrently: false)
|
|
49
49
|
ensure_postgresql_adapter!
|
|
50
50
|
if concurrently && transaction_open_for_paradedb?
|
|
51
|
-
raise ArgumentError, "
|
|
51
|
+
raise ArgumentError, "reindex_paradedb_index concurrently: true cannot run inside a transaction"
|
|
52
52
|
end
|
|
53
53
|
|
|
54
|
-
index_name = (name || "#{table}
|
|
54
|
+
index_name = (name || "#{table}_search_idx").to_s
|
|
55
55
|
modifier = concurrently ? " CONCURRENTLY" : ""
|
|
56
56
|
execute("REINDEX INDEX#{modifier} #{quote_table_name(index_name)}")
|
|
57
57
|
end
|
|
58
58
|
|
|
59
59
|
def dump_paradedb_indexes(stream)
|
|
60
|
-
rows =
|
|
60
|
+
rows = paradedb_index_rows
|
|
61
61
|
return if rows.empty?
|
|
62
62
|
|
|
63
63
|
stream.puts
|
|
64
64
|
rows.each do |row|
|
|
65
|
-
ruby_stmt =
|
|
65
|
+
ruby_stmt = paradedb_index_to_ruby(row)
|
|
66
66
|
stream.puts " #{ruby_stmt}"
|
|
67
67
|
end
|
|
68
68
|
end
|
|
@@ -71,8 +71,8 @@ module ParadeDB
|
|
|
71
71
|
(@paradedb_schema_index_references || []).uniq.sort
|
|
72
72
|
end
|
|
73
73
|
|
|
74
|
-
def
|
|
75
|
-
|
|
74
|
+
def paradedb_index_names
|
|
75
|
+
paradedb_index_rows.map { |r| r["index_name"] }
|
|
76
76
|
end
|
|
77
77
|
|
|
78
78
|
private
|
|
@@ -84,18 +84,18 @@ module ParadeDB
|
|
|
84
84
|
def build_create_sql(compiled, if_not_exists:, concurrently: false)
|
|
85
85
|
modifier = concurrently ? " CONCURRENTLY" : ""
|
|
86
86
|
prefix = if_not_exists ? "IF NOT EXISTS " : ""
|
|
87
|
-
fields_sql = compiled.entries.map { |entry|
|
|
88
|
-
with_options_sql =
|
|
87
|
+
fields_sql = compiled.entries.map { |entry| paradedb_entry_sql(entry) }.join(", ")
|
|
88
|
+
with_options_sql = paradedb_with_options_sql(compiled)
|
|
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
|
|
93
|
+
USING paradedb (#{fields_sql})
|
|
94
94
|
WITH (#{with_options_sql})#{where_sql}
|
|
95
95
|
SQL
|
|
96
96
|
end
|
|
97
97
|
|
|
98
|
-
def
|
|
98
|
+
def paradedb_with_options_sql(compiled)
|
|
99
99
|
options = []
|
|
100
100
|
options << "key_field=#{quote(compiled.key_field.to_s)}"
|
|
101
101
|
|
|
@@ -108,14 +108,14 @@ module ParadeDB
|
|
|
108
108
|
end
|
|
109
109
|
end
|
|
110
110
|
|
|
111
|
-
|
|
111
|
+
paradedb_field_option_groups(compiled).each do |param_name, value_hash|
|
|
112
112
|
options << "#{param_name}=#{quote(JSON.generate(value_hash))}"
|
|
113
113
|
end
|
|
114
114
|
|
|
115
115
|
options.join(", ")
|
|
116
116
|
end
|
|
117
117
|
|
|
118
|
-
def
|
|
118
|
+
def paradedb_field_option_groups(compiled)
|
|
119
119
|
field_options = compiled.field_options || {}
|
|
120
120
|
return {} if field_options.empty?
|
|
121
121
|
|
|
@@ -128,7 +128,7 @@ module ParadeDB
|
|
|
128
128
|
column = columns_by_name[source.to_s]
|
|
129
129
|
next unless column
|
|
130
130
|
|
|
131
|
-
param_name =
|
|
131
|
+
param_name = paradedb_field_option_param_for_column(column)
|
|
132
132
|
next if param_name.nil?
|
|
133
133
|
|
|
134
134
|
normalized = normalize_field_options_for_param(opts, param_name)
|
|
@@ -141,7 +141,7 @@ module ParadeDB
|
|
|
141
141
|
grouped
|
|
142
142
|
end
|
|
143
143
|
|
|
144
|
-
def
|
|
144
|
+
def paradedb_field_option_param_for_column(column)
|
|
145
145
|
sql_type = column.sql_type.to_s.downcase
|
|
146
146
|
return "range_fields" if sql_type.include?("range")
|
|
147
147
|
|
|
@@ -182,8 +182,10 @@ module ParadeDB
|
|
|
182
182
|
end
|
|
183
183
|
end
|
|
184
184
|
|
|
185
|
-
def
|
|
186
|
-
source_sql =
|
|
185
|
+
def paradedb_entry_sql(entry)
|
|
186
|
+
source_sql = paradedb_source_sql(entry)
|
|
187
|
+
|
|
188
|
+
return "#{source_sql} #{ParadeDB::Vector::OPCLASSES.fetch(entry.metric)}" if entry.metric
|
|
187
189
|
|
|
188
190
|
if entry.tokenizer.nil? && entry.query_key != entry.source
|
|
189
191
|
return "(#{source_sql}::pdb.alias(#{quote(entry.query_key)}))"
|
|
@@ -194,7 +196,7 @@ module ParadeDB
|
|
|
194
196
|
"(#{source_sql}::#{tokenizer_sql(entry.tokenizer, entry.options)})"
|
|
195
197
|
end
|
|
196
198
|
|
|
197
|
-
def
|
|
199
|
+
def paradedb_source_sql(entry)
|
|
198
200
|
if entry.expression
|
|
199
201
|
"(#{entry.source})"
|
|
200
202
|
else
|
|
@@ -289,7 +291,7 @@ module ParadeDB
|
|
|
289
291
|
false
|
|
290
292
|
end
|
|
291
293
|
|
|
292
|
-
def
|
|
294
|
+
def paradedb_index_rows
|
|
293
295
|
sql = <<~SQL
|
|
294
296
|
SELECT
|
|
295
297
|
c.relname AS index_name,
|
|
@@ -302,28 +304,28 @@ module ParadeDB
|
|
|
302
304
|
JOIN pg_class t ON t.oid = i.indrelid
|
|
303
305
|
JOIN pg_am am ON am.oid = c.relam
|
|
304
306
|
WHERE n.nspname = current_schema()
|
|
305
|
-
AND am.amname
|
|
307
|
+
AND am.amname IN ('paradedb', 'bm25')
|
|
306
308
|
ORDER BY t.relname, c.relname
|
|
307
309
|
SQL
|
|
308
310
|
select_all(sql).to_a
|
|
309
311
|
rescue => e
|
|
310
|
-
Kernel.warn("ParadeDB: unable to query
|
|
312
|
+
Kernel.warn("ParadeDB: unable to query paradedb indexes from catalog: #{e.message}")
|
|
311
313
|
[]
|
|
312
314
|
end
|
|
313
315
|
|
|
314
|
-
def
|
|
316
|
+
def paradedb_index_to_ruby(row)
|
|
315
317
|
indexdef = row["indexdef"]
|
|
316
318
|
table = row["table_name"]
|
|
317
319
|
name = row["index_name"]
|
|
318
320
|
|
|
319
|
-
key_field =
|
|
320
|
-
index_options =
|
|
321
|
-
fields_sql =
|
|
322
|
-
where =
|
|
321
|
+
key_field = extract_paradedb_key_field(indexdef)
|
|
322
|
+
index_options = extract_paradedb_index_options(indexdef)
|
|
323
|
+
fields_sql = extract_paradedb_fields_sql(indexdef)
|
|
324
|
+
where = normalize_paradedb_where_clause(row["where_clause"])
|
|
323
325
|
|
|
324
326
|
if key_field && fields_sql
|
|
325
|
-
field_sqls =
|
|
326
|
-
parsed = field_sqls.map { |f|
|
|
327
|
+
field_sqls = split_paradedb_top_level(fields_sql).map(&:strip)
|
|
328
|
+
parsed = field_sqls.map { |f| paradedb_parse_column_entry(f) }
|
|
327
329
|
|
|
328
330
|
grouped = {}
|
|
329
331
|
parsed.each { |e| (grouped[e[:source]] ||= []) << e }
|
|
@@ -331,17 +333,19 @@ module ParadeDB
|
|
|
331
333
|
fields_pairs = grouped.map do |source, entries|
|
|
332
334
|
source_ruby = source.match?(/[^a-zA-Z0-9_]/) ? "#{source.inspect} =>" : "#{source}:"
|
|
333
335
|
|
|
334
|
-
if entries.
|
|
336
|
+
if entries.length == 1 && entries.first[:metric]
|
|
337
|
+
"#{source_ruby} { metric: #{entries.first[:metric].inspect} }"
|
|
338
|
+
elsif entries.all? { |e| e[:tokenizer].nil? }
|
|
335
339
|
"#{source_ruby} {}"
|
|
336
340
|
elsif entries.length == 1
|
|
337
|
-
"#{source_ruby} #{
|
|
341
|
+
"#{source_ruby} #{paradedb_tokenizer_config_ruby(entries.first)}"
|
|
338
342
|
else
|
|
339
|
-
configs = entries.map { |e|
|
|
343
|
+
configs = entries.map { |e| paradedb_tokenizer_ruby_from_entry(e) }
|
|
340
344
|
"#{source_ruby} { tokenizers: [#{configs.join(', ')}] }"
|
|
341
345
|
end
|
|
342
346
|
end
|
|
343
347
|
|
|
344
|
-
statement = "
|
|
348
|
+
statement = "add_paradedb_index #{table.to_sym.inspect}, " \
|
|
345
349
|
"fields: { #{fields_pairs.join(', ')} }, " \
|
|
346
350
|
"key_field: #{key_field.to_sym.inspect}, " \
|
|
347
351
|
"name: #{name.inspect}"
|
|
@@ -355,7 +359,7 @@ module ParadeDB
|
|
|
355
359
|
end
|
|
356
360
|
end
|
|
357
361
|
|
|
358
|
-
def
|
|
362
|
+
def extract_paradedb_key_field(indexdef)
|
|
359
363
|
quoted = indexdef.match(/WITH\s*\([^)]*key_field\s*=\s*'((?:[^']|'')*)'/i)
|
|
360
364
|
return quoted[1].gsub("''", "'") if quoted
|
|
361
365
|
|
|
@@ -365,8 +369,8 @@ module ParadeDB
|
|
|
365
369
|
nil
|
|
366
370
|
end
|
|
367
371
|
|
|
368
|
-
def
|
|
369
|
-
with_sql, =
|
|
372
|
+
def extract_paradedb_index_options(indexdef)
|
|
373
|
+
with_sql, = extract_paradedb_with_components(indexdef)
|
|
370
374
|
options = {}
|
|
371
375
|
split_sql_arguments(with_sql).each do |argument|
|
|
372
376
|
key, value_sql = split_assignment(argument)
|
|
@@ -388,8 +392,8 @@ module ParadeDB
|
|
|
388
392
|
{}
|
|
389
393
|
end
|
|
390
394
|
|
|
391
|
-
def
|
|
392
|
-
match = indexdef.match(/USING\s+bm25\s*\(/im)
|
|
395
|
+
def extract_paradedb_fields_sql(indexdef)
|
|
396
|
+
match = indexdef.match(/USING\s+(?:paradedb|bm25)\s*\(/im)
|
|
393
397
|
|
|
394
398
|
start = match.end(0)
|
|
395
399
|
depth = 1
|
|
@@ -406,7 +410,7 @@ module ParadeDB
|
|
|
406
410
|
indexdef[start..pos - 2]
|
|
407
411
|
end
|
|
408
412
|
|
|
409
|
-
def
|
|
413
|
+
def extract_paradedb_with_components(indexdef)
|
|
410
414
|
match = indexdef.match(/WITH\s*\(/im)
|
|
411
415
|
start = match.end(0)
|
|
412
416
|
depth = 1
|
|
@@ -427,20 +431,20 @@ module ParadeDB
|
|
|
427
431
|
[with_sql, trailing_sql]
|
|
428
432
|
end
|
|
429
433
|
|
|
430
|
-
def
|
|
434
|
+
def normalize_paradedb_where_clause(where)
|
|
431
435
|
return nil if where.nil?
|
|
432
436
|
|
|
433
437
|
normalized = where.to_s.strip
|
|
434
438
|
return nil if normalized.empty?
|
|
435
439
|
|
|
436
|
-
while
|
|
440
|
+
while paradedb_wrapped_in_parentheses?(normalized)
|
|
437
441
|
normalized = normalized[1...-1].strip
|
|
438
442
|
end
|
|
439
443
|
|
|
440
444
|
normalized.empty? ? nil : normalized
|
|
441
445
|
end
|
|
442
446
|
|
|
443
|
-
def
|
|
447
|
+
def paradedb_wrapped_in_parentheses?(sql)
|
|
444
448
|
return false unless sql.start_with?("(") && sql.end_with?(")")
|
|
445
449
|
|
|
446
450
|
depth = 0
|
|
@@ -456,7 +460,7 @@ module ParadeDB
|
|
|
456
460
|
depth.zero?
|
|
457
461
|
end
|
|
458
462
|
|
|
459
|
-
def
|
|
463
|
+
def split_paradedb_top_level(str)
|
|
460
464
|
parts = []
|
|
461
465
|
current = +""
|
|
462
466
|
depth = 0
|
|
@@ -479,16 +483,28 @@ module ParadeDB
|
|
|
479
483
|
parts
|
|
480
484
|
end
|
|
481
485
|
|
|
482
|
-
def
|
|
486
|
+
def paradedb_parse_column_entry(field_sql)
|
|
483
487
|
stripped = field_sql.strip
|
|
484
488
|
|
|
489
|
+
opclass_match = stripped.match(/\A(.+?)\s+(vector_(?:l2|cosine|ip)_ops)\z/m)
|
|
490
|
+
if opclass_match
|
|
491
|
+
source_sql = opclass_match[1].strip
|
|
492
|
+
source_name = identifier_sql?(source_sql) ? unquote_identifier(source_sql) : source_sql
|
|
493
|
+
return {
|
|
494
|
+
source: source_name,
|
|
495
|
+
tokenizer: nil,
|
|
496
|
+
options: {},
|
|
497
|
+
metric: ParadeDB::Vector::METRICS_BY_OPCLASS.fetch(opclass_match[2])
|
|
498
|
+
}
|
|
499
|
+
end
|
|
500
|
+
|
|
485
501
|
if stripped.start_with?("(") && stripped.end_with?(")")
|
|
486
502
|
inner = stripped[1..-2].strip
|
|
487
503
|
|
|
488
504
|
if (source_sql, tok_sql = split_tokenized_cast(inner))
|
|
489
505
|
normalized = unwrap_surrounding_groupings(source_sql)
|
|
490
506
|
source_name = identifier_sql?(normalized) ? unquote_identifier(normalized) : normalized
|
|
491
|
-
|
|
507
|
+
paradedb_parse_tokenizer(tok_sql).merge(source: source_name)
|
|
492
508
|
else
|
|
493
509
|
{ source: inner, tokenizer: nil, options: {} }
|
|
494
510
|
end
|
|
@@ -638,7 +654,7 @@ module ParadeDB
|
|
|
638
654
|
sql.match?(/\A[a-zA-Z_][a-zA-Z0-9_]*\z/) || sql.match?(/\A"(?:[^"]|"")+"\z/)
|
|
639
655
|
end
|
|
640
656
|
|
|
641
|
-
def
|
|
657
|
+
def paradedb_parse_tokenizer(tokenizer_sql_str)
|
|
642
658
|
match = tokenizer_sql_str.strip.match(/\A([a-zA-Z_][a-zA-Z0-9_]*(?:(?:::|\.)[a-zA-Z_][a-zA-Z0-9_]*)*)(?:\((.*)\))?\z/m)
|
|
643
659
|
return { tokenizer: tokenizer_sql_str, options: {} } unless match
|
|
644
660
|
|
|
@@ -677,11 +693,11 @@ module ParadeDB
|
|
|
677
693
|
{ tokenizer: normalized_name, options: options }
|
|
678
694
|
end
|
|
679
695
|
|
|
680
|
-
def
|
|
681
|
-
"{ tokenizer: #{
|
|
696
|
+
def paradedb_tokenizer_config_ruby(entry)
|
|
697
|
+
"{ tokenizer: #{paradedb_tokenizer_ruby_from_entry(entry)} }"
|
|
682
698
|
end
|
|
683
699
|
|
|
684
|
-
def
|
|
700
|
+
def paradedb_tokenizer_ruby_from_entry(entry)
|
|
685
701
|
opts = entry[:options].dup
|
|
686
702
|
positional_args = Array(opts.delete(:__positional))
|
|
687
703
|
alias_val = opts.delete(:alias)
|
|
@@ -691,10 +707,10 @@ module ParadeDB
|
|
|
691
707
|
|
|
692
708
|
opts[:alias] = alias_val if alias_val
|
|
693
709
|
|
|
694
|
-
|
|
710
|
+
paradedb_tokenizer_ruby(entry[:tokenizer], positional_args, opts)
|
|
695
711
|
end
|
|
696
712
|
|
|
697
|
-
def
|
|
713
|
+
def paradedb_tokenizer_ruby(name, positional_args, options)
|
|
698
714
|
if name.match?(/\A[a-z_][a-z0-9_]*\z/) && ParadeDB::Tokenizer.respond_to?(name)
|
|
699
715
|
args = positional_args.map { |arg| ruby_literal(arg) }
|
|
700
716
|
args << "options: #{ruby_hash_literal(options)}" unless options.empty?
|
|
@@ -866,8 +882,8 @@ if defined?(ActiveRecord::Migration)
|
|
|
866
882
|
connection.replace_paradedb_index(index_klass)
|
|
867
883
|
end
|
|
868
884
|
|
|
869
|
-
def
|
|
870
|
-
connection.
|
|
885
|
+
def add_paradedb_index(table, fields:, key_field:, name: nil, index_options: nil, where: nil, if_not_exists: false, concurrently: false)
|
|
886
|
+
connection.add_paradedb_index(
|
|
871
887
|
table,
|
|
872
888
|
fields: fields,
|
|
873
889
|
key_field: key_field,
|
|
@@ -879,12 +895,12 @@ if defined?(ActiveRecord::Migration)
|
|
|
879
895
|
)
|
|
880
896
|
end
|
|
881
897
|
|
|
882
|
-
def
|
|
883
|
-
connection.
|
|
898
|
+
def remove_paradedb_index(table, name: nil, if_exists: false)
|
|
899
|
+
connection.remove_paradedb_index(table, name: name, if_exists: if_exists)
|
|
884
900
|
end
|
|
885
901
|
|
|
886
|
-
def
|
|
887
|
-
connection.
|
|
902
|
+
def reindex_paradedb_index(table, name: nil, concurrently: false)
|
|
903
|
+
connection.reindex_paradedb_index(table, name: name, concurrently: concurrently)
|
|
888
904
|
end
|
|
889
905
|
end
|
|
890
906
|
end
|
|
@@ -905,11 +921,11 @@ if defined?(ActiveRecord::SchemaDumper)
|
|
|
905
921
|
def indexes_in_create(table, stream)
|
|
906
922
|
conn = paradedb_connection
|
|
907
923
|
if conn
|
|
908
|
-
|
|
924
|
+
paradedb_names = conn.paradedb_index_names
|
|
909
925
|
original_indexes = conn.method(:indexes)
|
|
910
926
|
|
|
911
927
|
conn.define_singleton_method(:indexes) do |tbl|
|
|
912
|
-
original_indexes.call(tbl).reject { |idx|
|
|
928
|
+
original_indexes.call(tbl).reject { |idx| paradedb_names.include?(idx.name) }
|
|
913
929
|
end
|
|
914
930
|
|
|
915
931
|
begin
|
|
@@ -940,10 +956,10 @@ if defined?(ActiveRecord::Migration::CommandRecorder)
|
|
|
940
956
|
module CommandRecorderPatch
|
|
941
957
|
%i[
|
|
942
958
|
create_paradedb_index
|
|
943
|
-
|
|
944
|
-
|
|
959
|
+
add_paradedb_index
|
|
960
|
+
remove_paradedb_index
|
|
945
961
|
replace_paradedb_index
|
|
946
|
-
|
|
962
|
+
reindex_paradedb_index
|
|
947
963
|
].each do |method_name|
|
|
948
964
|
define_method(method_name) do |*args, &block|
|
|
949
965
|
record(method_name, args, &block)
|
|
@@ -958,10 +974,10 @@ if defined?(ActiveRecord::Migration::CommandRecorder)
|
|
|
958
974
|
compiled = resolve_paradedb_index_klass(index_klass).compiled_definition
|
|
959
975
|
remove_options = Hash.ruby2_keywords_hash(name: compiled.index_name, if_exists: true)
|
|
960
976
|
|
|
961
|
-
[:
|
|
977
|
+
[:remove_paradedb_index, [compiled.table_name, remove_options]]
|
|
962
978
|
end
|
|
963
979
|
|
|
964
|
-
def
|
|
980
|
+
def invert_add_paradedb_index(args)
|
|
965
981
|
table, options = args
|
|
966
982
|
options = symbolize_options_hash(options)
|
|
967
983
|
|
|
@@ -969,12 +985,12 @@ if defined?(ActiveRecord::Migration::CommandRecorder)
|
|
|
969
985
|
remove_options[:name] = options[:name] if options[:name]
|
|
970
986
|
remove_options = Hash.ruby2_keywords_hash(remove_options)
|
|
971
987
|
|
|
972
|
-
[:
|
|
988
|
+
[:remove_paradedb_index, [table, remove_options]]
|
|
973
989
|
end
|
|
974
990
|
|
|
975
|
-
def
|
|
991
|
+
def invert_remove_paradedb_index(_args)
|
|
976
992
|
raise ActiveRecord::IrreversibleMigration,
|
|
977
|
-
"
|
|
993
|
+
"remove_paradedb_index is not automatically reversible. Use #up/#down or #reversible."
|
|
978
994
|
end
|
|
979
995
|
|
|
980
996
|
def invert_replace_paradedb_index(_args)
|
|
@@ -982,9 +998,9 @@ if defined?(ActiveRecord::Migration::CommandRecorder)
|
|
|
982
998
|
"replace_paradedb_index is not automatically reversible. Use #up/#down or #reversible."
|
|
983
999
|
end
|
|
984
1000
|
|
|
985
|
-
def
|
|
1001
|
+
def invert_reindex_paradedb_index(_args)
|
|
986
1002
|
raise ActiveRecord::IrreversibleMigration,
|
|
987
|
-
"
|
|
1003
|
+
"reindex_paradedb_index is not automatically reversible. Use #up/#down or #reversible."
|
|
988
1004
|
end
|
|
989
1005
|
|
|
990
1006
|
def resolve_paradedb_index_klass(index_klass)
|
data/lib/parade_db/model.rb
CHANGED
|
@@ -10,6 +10,7 @@ module ParadeDB
|
|
|
10
10
|
INJECTED_CLASS_METHODS = [
|
|
11
11
|
:paradedb_search,
|
|
12
12
|
:more_like_this,
|
|
13
|
+
:nearest,
|
|
13
14
|
:with_facets,
|
|
14
15
|
:facets,
|
|
15
16
|
:with_agg,
|
|
@@ -67,6 +68,12 @@ module ParadeDB
|
|
|
67
68
|
all.extending(SearchMethods).more_like_this(key, fields: fields, **options)
|
|
68
69
|
end
|
|
69
70
|
|
|
71
|
+
def nearest(column, vector, metric: nil)
|
|
72
|
+
ensure_postgres!
|
|
73
|
+
paradedb_validate_index!
|
|
74
|
+
all.extending(SearchMethods).nearest(column, vector, metric: metric)
|
|
75
|
+
end
|
|
76
|
+
|
|
70
77
|
def with_facets(*fields, **opts)
|
|
71
78
|
ensure_postgres!
|
|
72
79
|
paradedb_validate_index!
|
|
@@ -158,7 +165,7 @@ module ParadeDB
|
|
|
158
165
|
next if paradedb_catalog_index_valid?(definition)
|
|
159
166
|
|
|
160
167
|
all_valid = false
|
|
161
|
-
message = "ParadeDB index drift detected for #{name}: expected #{definition.index_name} on #{definition.table_name} with
|
|
168
|
+
message = "ParadeDB index drift detected for #{name}: expected #{definition.index_name} on #{definition.table_name} with a ParadeDB access method."
|
|
162
169
|
case ParadeDB.index_validation_mode
|
|
163
170
|
when :warn
|
|
164
171
|
paradedb_log_warn(message)
|
|
@@ -263,7 +270,7 @@ module ParadeDB
|
|
|
263
270
|
WHERE c.relname = #{connection.quote(definition.index_name.to_s)}
|
|
264
271
|
AND t.relname = #{connection.quote(definition.table_name.to_s)}
|
|
265
272
|
AND n.nspname = current_schema()
|
|
266
|
-
AND am.amname
|
|
273
|
+
AND am.amname IN ('paradedb', 'bm25')
|
|
267
274
|
LIMIT 1
|
|
268
275
|
SQL
|
|
269
276
|
|
|
@@ -479,6 +479,19 @@ module ParadeDB
|
|
|
479
479
|
rel.except(:select, :group).select(*group_nodes, *aggregate_nodes).group(*group_nodes)
|
|
480
480
|
end
|
|
481
481
|
|
|
482
|
+
# Orders by vector distance for Top-K pushdown inside the ParadeDB index.
|
|
483
|
+
# Adds `key_field @@@ pdb.all()` when the relation has no ParadeDB predicate,
|
|
484
|
+
# since vector ordering requires a @@@ predicate to activate the index scan.
|
|
485
|
+
# Callers must add `.limit(k)`; the metric defaults to the index opclass metric.
|
|
486
|
+
def nearest(column, vector, metric: nil)
|
|
487
|
+
ensure_paradedb_runtime!
|
|
488
|
+
resolved_metric = metric || index_vector_metric(column) || ParadeDB::Vector::DEFAULT_METRIC
|
|
489
|
+
node = builder.vector_distance(column, vector, metric: resolved_metric)
|
|
490
|
+
|
|
491
|
+
rel = has_paradedb_predicate? ? self : ensure_paradedb_predicate
|
|
492
|
+
rel.order(node.asc)
|
|
493
|
+
end
|
|
494
|
+
|
|
482
495
|
def has_paradedb_predicate?
|
|
483
496
|
PredicateInspector.relation_has_paradedb_predicate?(self)
|
|
484
497
|
end
|
|
@@ -490,6 +503,12 @@ module ParadeDB
|
|
|
490
503
|
|
|
491
504
|
private
|
|
492
505
|
|
|
506
|
+
def index_vector_metric(column)
|
|
507
|
+
return nil unless klass.respond_to?(:paradedb_index_entry, true)
|
|
508
|
+
|
|
509
|
+
klass.send(:paradedb_index_entry, column)&.metric
|
|
510
|
+
end
|
|
511
|
+
|
|
493
512
|
def paradedb_runtime_key_field
|
|
494
513
|
return primary_key unless klass.respond_to?(:paradedb_key_field)
|
|
495
514
|
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_model"
|
|
4
|
+
|
|
5
|
+
module ParadeDB
|
|
6
|
+
class Vector < ActiveModel::Type::Value
|
|
7
|
+
METRICS = %i[l2 cosine ip].freeze
|
|
8
|
+
METRIC_ALIASES = { inner_product: :ip }.freeze
|
|
9
|
+
OPCLASSES = { l2: "vector_l2_ops", cosine: "vector_cosine_ops", ip: "vector_ip_ops" }.freeze
|
|
10
|
+
METRICS_BY_OPCLASS = OPCLASSES.invert.freeze
|
|
11
|
+
DISTANCE_OPERATORS = { l2: "<->", cosine: "<=>", ip: "<#>" }.freeze
|
|
12
|
+
DEFAULT_METRIC = :l2
|
|
13
|
+
|
|
14
|
+
def self.normalize_metric(metric)
|
|
15
|
+
normalized = metric.to_sym
|
|
16
|
+
normalized = METRIC_ALIASES.fetch(normalized, normalized)
|
|
17
|
+
return normalized if METRICS.include?(normalized)
|
|
18
|
+
|
|
19
|
+
raise ArgumentError,
|
|
20
|
+
"unknown vector metric #{metric.inspect}. Valid metrics: #{(METRICS + METRIC_ALIASES.keys).map(&:inspect).join(', ')}"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.literal(value)
|
|
24
|
+
"[#{Array(value).map { |v| Float(v) }.join(',')}]"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
attr_reader :limit
|
|
28
|
+
|
|
29
|
+
def initialize(limit: nil)
|
|
30
|
+
super()
|
|
31
|
+
@limit = limit
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def type
|
|
35
|
+
:vector
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def cast(value)
|
|
39
|
+
case value
|
|
40
|
+
when nil
|
|
41
|
+
nil
|
|
42
|
+
when Array
|
|
43
|
+
validate_dimensions!(value.map { |v| Float(v) })
|
|
44
|
+
when String
|
|
45
|
+
cast_string(value)
|
|
46
|
+
else
|
|
47
|
+
raise ArgumentError, "cannot cast #{value.class} to vector"
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def serialize(value)
|
|
52
|
+
return nil if value.nil?
|
|
53
|
+
|
|
54
|
+
self.class.literal(cast(value))
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def deserialize(value)
|
|
58
|
+
cast(value)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def changed_in_place?(raw_old_value, new_value)
|
|
62
|
+
deserialize(raw_old_value) != new_value
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def self.register_with_type_map(m)
|
|
66
|
+
m.register_type "vector" do |_, _, sql_type|
|
|
67
|
+
limit = sql_type.to_s[/\((\d+)\)/, 1]
|
|
68
|
+
ParadeDB::Vector.new(limit: limit&.to_i)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
module PostgreSQLAdapterPatch
|
|
73
|
+
def initialize_type_map(m)
|
|
74
|
+
super
|
|
75
|
+
ParadeDB::Vector.register_with_type_map(m)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def self.install_postgresql_adapter!(adapter_class)
|
|
80
|
+
adapter_class::NATIVE_DATABASE_TYPES[:vector] = { name: "vector" }
|
|
81
|
+
|
|
82
|
+
unless adapter_class.singleton_class.ancestors.include?(PostgreSQLAdapterPatch)
|
|
83
|
+
adapter_class.singleton_class.prepend(PostgreSQLAdapterPatch)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
register_with_type_map(adapter_class::TYPE_MAP) if adapter_class.const_defined?(:TYPE_MAP)
|
|
87
|
+
|
|
88
|
+
table_definition = ActiveRecord::ConnectionAdapters::PostgreSQL::TableDefinition
|
|
89
|
+
unless table_definition.method_defined?(:vector)
|
|
90
|
+
table_definition.send(:define_column_methods, :vector)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
def cast_string(value)
|
|
97
|
+
stripped = value.strip
|
|
98
|
+
unless stripped.start_with?("[") && stripped.end_with?("]")
|
|
99
|
+
raise ArgumentError, "malformed vector literal: #{value.inspect}"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
inner = stripped[1..-2].strip
|
|
103
|
+
values = inner.empty? ? [] : inner.split(",").map { |v| Float(v) }
|
|
104
|
+
validate_dimensions!(values)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def validate_dimensions!(values)
|
|
108
|
+
if limit && values.length != limit
|
|
109
|
+
raise ArgumentError, "expected #{limit} dimensions, got #{values.length}"
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
values
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
ActiveSupport.on_load(:active_record_postgresqladapter) do
|
|
118
|
+
ParadeDB::Vector.install_postgresql_adapter!(self)
|
|
119
|
+
end
|
data/lib/parade_db/version.rb
CHANGED
data/lib/parade_db.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require_relative "parade_db/version"
|
|
4
4
|
require_relative "parade_db/errors"
|
|
5
5
|
require_relative "parade_db/arel"
|
|
6
|
+
require_relative "parade_db/vector"
|
|
6
7
|
require_relative "parade_db/index"
|
|
7
8
|
require_relative "parade_db/aggregations"
|
|
8
9
|
require_relative "parade_db/proximity"
|
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.10.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- ParadeDB
|
|
@@ -115,6 +115,7 @@ files:
|
|
|
115
115
|
- lib/parade_db/search_methods.rb
|
|
116
116
|
- lib/parade_db/tokenizer.rb
|
|
117
117
|
- lib/parade_db/tokenizer_sql.rb
|
|
118
|
+
- lib/parade_db/vector.rb
|
|
118
119
|
- lib/parade_db/version.rb
|
|
119
120
|
homepage: https://github.com/paradedb/rails-paradedb
|
|
120
121
|
licenses:
|