rails-paradedb 0.8.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 +35 -1
- data/README.md +23 -11
- 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 +43 -6
- data/lib/parade_db/arel/predications.rb +31 -7
- data/lib/parade_db/index.rb +25 -4
- data/lib/parade_db/migration_helpers.rb +89 -73
- data/lib/parade_db/model.rb +9 -2
- data/lib/parade_db/search_methods.rb +41 -30
- data/lib/parade_db/vector.rb +119 -0
- data/lib/parade_db/version.rb +1 -1
- data/lib/parade_db.rb +1 -0
- metadata +4 -3
- data/lib/parade_db/arel/README.md +0 -76
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,38 @@ 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
|
+
|
|
23
|
+
[0.9.0] - 2026-07-14
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
|
|
27
|
+
- Allow SQL/Arel function nodes as `match_all` and `match_any` query arguments.
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
|
|
31
|
+
- **BREAKING**: Restrict match APIs to a single query argument.
|
|
32
|
+
- **BREAKING**: Rename relation query methods `matching_all` and `matching_any`
|
|
33
|
+
to `match_all` and `match_any`.
|
|
34
|
+
|
|
35
|
+
### Removed
|
|
36
|
+
|
|
37
|
+
- **BREAKING**: Removed max_term_freq from the more_like_this API.
|
|
38
|
+
|
|
7
39
|
## [0.8.0] - 2026-06-15
|
|
8
40
|
|
|
9
41
|
### Changed
|
|
@@ -138,7 +170,9 @@ All notable changes to this project will be documented in this file. The format
|
|
|
138
170
|
- Schema dump/load round-trip for tokenizer configuration and index options
|
|
139
171
|
(including `target_segment_count`)
|
|
140
172
|
|
|
141
|
-
[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
|
|
175
|
+
[0.9.0]: https://github.com/paradedb/rails-paradedb/releases/tag/v0.9.0
|
|
142
176
|
[0.8.0]: https://github.com/paradedb/rails-paradedb/releases/tag/v0.8.0
|
|
143
177
|
[0.7.0]: https://github.com/paradedb/rails-paradedb/releases/tag/v0.7.0
|
|
144
178
|
[0.6.0]: https://github.com/paradedb/rails-paradedb/releases/tag/v0.6.0
|
data/README.md
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
|
-
<!-- ParadeDB: Postgres for Search and Analytics -->
|
|
2
1
|
<h1 align="center">
|
|
3
|
-
<a href="https://paradedb.com"
|
|
4
|
-
<
|
|
2
|
+
<a href="https://paradedb.com">
|
|
3
|
+
<picture align=center>
|
|
4
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/paradedb/paradedb/raw/main/docs/logo/paradedb-logo-dark-large.svg">
|
|
5
|
+
<source media="(prefers-color-scheme: light)" srcset="https://github.com/paradedb/paradedb/raw/main/docs/logo/paradedb-logo-light-large.svg">
|
|
6
|
+
<img alt="The ParadeDB logo." src="https://github.com/paradedb/paradedb/raw/main/docs/logo/paradedb-logo-light-large.svg">
|
|
7
|
+
</picture>
|
|
8
|
+
</a>
|
|
9
|
+
<br>
|
|
5
10
|
</h1>
|
|
6
11
|
|
|
7
12
|
<p align="center">
|
|
8
|
-
<b>
|
|
13
|
+
<b>Search without a second system.</b><br/>
|
|
14
|
+
One Postgres for your application data, full-text search, vector retrieval, and aggregations.
|
|
9
15
|
</p>
|
|
10
16
|
|
|
11
17
|
<h3 align="center">
|
|
@@ -30,20 +36,26 @@
|
|
|
30
36
|
|
|
31
37
|
## ParadeDB for Rails
|
|
32
38
|
|
|
33
|
-
The official ActiveRecord integration for [ParadeDB](https://paradedb.com), 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.
|
|
34
40
|
|
|
35
41
|
## Requirements & Compatibility
|
|
36
42
|
|
|
37
|
-
| Component | Supported
|
|
38
|
-
| ---------- |
|
|
39
|
-
| Ruby | 3.2+
|
|
40
|
-
| Rails | 7.2+
|
|
41
|
-
| ParadeDB | 0.
|
|
42
|
-
| 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.
|
|
43
54
|
|
|
44
55
|
## Examples
|
|
45
56
|
|
|
46
57
|
- [Quickstart](examples/quickstart/quickstart.rb)
|
|
58
|
+
- [Vector Search](examples/vector_search/vector_search.rb)
|
|
47
59
|
- [Faceted Search](examples/faceted_search/faceted_search.rb)
|
|
48
60
|
- [Autocomplete](examples/autocomplete/autocomplete.rb)
|
|
49
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
|
|
@@ -21,7 +22,7 @@ module ParadeDB
|
|
|
21
22
|
|
|
22
23
|
def match(
|
|
23
24
|
column,
|
|
24
|
-
|
|
25
|
+
term = nil,
|
|
25
26
|
tokenizer: nil,
|
|
26
27
|
distance: nil,
|
|
27
28
|
prefix: nil,
|
|
@@ -35,7 +36,7 @@ module ParadeDB
|
|
|
35
36
|
prefix: prefix,
|
|
36
37
|
transposition_cost_one: transposition_cost_one
|
|
37
38
|
)
|
|
38
|
-
rhs =
|
|
39
|
+
rhs = term_query_node(term)
|
|
39
40
|
rhs = apply_fuzzy(
|
|
40
41
|
rhs,
|
|
41
42
|
distance: distance,
|
|
@@ -50,7 +51,7 @@ module ParadeDB
|
|
|
50
51
|
|
|
51
52
|
def match_any(
|
|
52
53
|
column,
|
|
53
|
-
|
|
54
|
+
term = nil,
|
|
54
55
|
tokenizer: nil,
|
|
55
56
|
distance: nil,
|
|
56
57
|
prefix: nil,
|
|
@@ -64,7 +65,7 @@ module ParadeDB
|
|
|
64
65
|
prefix: prefix,
|
|
65
66
|
transposition_cost_one: transposition_cost_one
|
|
66
67
|
)
|
|
67
|
-
rhs =
|
|
68
|
+
rhs = term_query_node(term)
|
|
68
69
|
rhs = apply_fuzzy(
|
|
69
70
|
rhs,
|
|
70
71
|
distance: distance,
|
|
@@ -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"
|
|
@@ -538,12 +563,24 @@ module ParadeDB
|
|
|
538
563
|
)
|
|
539
564
|
end
|
|
540
565
|
|
|
541
|
-
def
|
|
542
|
-
joined =
|
|
566
|
+
def join_term(term)
|
|
567
|
+
joined = term.to_s
|
|
543
568
|
raise ArgumentError, "at least one search term is required" if joined.strip.empty?
|
|
544
569
|
joined
|
|
545
570
|
end
|
|
546
571
|
|
|
572
|
+
def term_query_node(term)
|
|
573
|
+
return term if arel_expression?(term)
|
|
574
|
+
|
|
575
|
+
quoted_value(join_term(term))
|
|
576
|
+
end
|
|
577
|
+
|
|
578
|
+
def arel_expression?(value)
|
|
579
|
+
value.is_a?(::Arel::Nodes::Node) ||
|
|
580
|
+
value.is_a?(::Arel::Attributes::Attribute) ||
|
|
581
|
+
value.is_a?(::Arel::Nodes::SqlLiteral)
|
|
582
|
+
end
|
|
583
|
+
|
|
547
584
|
def normalize_term_set_terms(terms)
|
|
548
585
|
values = Array(terms).flatten.compact
|
|
549
586
|
raise ArgumentError, "term_set requires at least one value" if values.empty?
|
|
@@ -7,16 +7,16 @@ module ParadeDB
|
|
|
7
7
|
TOKENIZER_EXPRESSION = /\A[a-zA-Z_][a-zA-Z0-9_]*(?:(?:::|\.)[a-zA-Z_][a-zA-Z0-9_]*)*(?:\(\s*[a-zA-Z0-9_'".,=\s:-]*\s*\))?\z/.freeze
|
|
8
8
|
BUILDER = Builder.new.freeze
|
|
9
9
|
|
|
10
|
-
def pdb_match(
|
|
11
|
-
rhs =
|
|
10
|
+
def pdb_match(term = nil, tokenizer: nil, distance: nil, prefix: nil, transposition_cost_one: nil, boost: nil)
|
|
11
|
+
rhs = pdb_term_query_node(term)
|
|
12
12
|
rhs = pdb_apply_fuzzy(rhs, distance: distance, prefix: prefix, transposition_cost_one: transposition_cost_one)
|
|
13
13
|
rhs = pdb_apply_tokenizer(rhs, tokenizer)
|
|
14
14
|
rhs = pdb_apply_boost(rhs, boost)
|
|
15
15
|
::Arel::Nodes::InfixOperation.new("&&&", self, rhs)
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
-
def pdb_match_any(
|
|
19
|
-
rhs =
|
|
18
|
+
def pdb_match_any(term = nil, tokenizer: nil, distance: nil, prefix: nil, transposition_cost_one: nil, boost: nil)
|
|
19
|
+
rhs = pdb_term_query_node(term)
|
|
20
20
|
rhs = pdb_apply_fuzzy(rhs, distance: distance, prefix: prefix, transposition_cost_one: transposition_cost_one)
|
|
21
21
|
rhs = pdb_apply_tokenizer(rhs, tokenizer)
|
|
22
22
|
rhs = pdb_apply_boost(rhs, boost)
|
|
@@ -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
|
|
@@ -193,11 +209,19 @@ module ParadeDB
|
|
|
193
209
|
::Arel::Nodes.build_quoted(value)
|
|
194
210
|
end
|
|
195
211
|
|
|
196
|
-
def
|
|
197
|
-
|
|
212
|
+
def pdb_term_query_node(term)
|
|
213
|
+
return term if pdb_arel_expression?(term)
|
|
214
|
+
|
|
215
|
+
joined = term.to_s
|
|
198
216
|
raise ArgumentError, "at least one search term is required" if joined.strip.empty?
|
|
199
217
|
|
|
200
|
-
joined
|
|
218
|
+
pdb_quoted(joined)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def pdb_arel_expression?(value)
|
|
222
|
+
value.is_a?(::Arel::Nodes::Node) ||
|
|
223
|
+
value.is_a?(::Arel::Attributes::Attribute) ||
|
|
224
|
+
value.is_a?(::Arel::Nodes::SqlLiteral)
|
|
201
225
|
end
|
|
202
226
|
|
|
203
227
|
def pdb_validate_numeric!(value, name)
|
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
|
|
@@ -422,25 +426,25 @@ module ParadeDB
|
|
|
422
426
|
|
|
423
427
|
with_sql = indexdef[start..pos - 2]
|
|
424
428
|
trailing_sql = indexdef[pos..]&.strip
|
|
425
|
-
trailing_sql = nil if trailing_sql
|
|
429
|
+
trailing_sql = nil if trailing_sql == ""
|
|
426
430
|
|
|
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
|
|
|
@@ -13,8 +13,6 @@ module ParadeDB
|
|
|
13
13
|
max_query_terms: :max_query_terms,
|
|
14
14
|
min_doc_freq: :min_doc_frequency,
|
|
15
15
|
min_doc_frequency: :min_doc_frequency,
|
|
16
|
-
max_term_freq: :max_term_frequency,
|
|
17
|
-
max_term_frequency: :max_term_frequency,
|
|
18
16
|
max_doc_freq: :max_doc_frequency,
|
|
19
17
|
max_doc_frequency: :max_doc_frequency,
|
|
20
18
|
min_word_length: :min_word_length,
|
|
@@ -25,7 +23,6 @@ module ParadeDB
|
|
|
25
23
|
min_term_frequency
|
|
26
24
|
max_query_terms
|
|
27
25
|
min_doc_frequency
|
|
28
|
-
max_term_frequency
|
|
29
26
|
max_doc_frequency
|
|
30
27
|
min_word_length
|
|
31
28
|
max_word_length
|
|
@@ -34,13 +31,11 @@ module ParadeDB
|
|
|
34
31
|
min_term_frequency
|
|
35
32
|
max_query_terms
|
|
36
33
|
min_doc_frequency
|
|
37
|
-
max_term_frequency
|
|
38
34
|
max_doc_frequency
|
|
39
35
|
min_word_length
|
|
40
36
|
max_word_length
|
|
41
37
|
stopwords
|
|
42
38
|
].freeze
|
|
43
|
-
|
|
44
39
|
# Internal state tracking
|
|
45
40
|
attr_accessor :_paradedb_current_field
|
|
46
41
|
attr_accessor :_paradedb_facet_fields
|
|
@@ -120,8 +115,8 @@ module ParadeDB
|
|
|
120
115
|
extending(SearchMethods).tap { |rel| rel._paradedb_current_field = search_column }
|
|
121
116
|
end
|
|
122
117
|
|
|
123
|
-
def
|
|
124
|
-
|
|
118
|
+
def match_all(
|
|
119
|
+
term = nil,
|
|
125
120
|
tokenizer: nil,
|
|
126
121
|
distance: nil,
|
|
127
122
|
prefix: nil,
|
|
@@ -131,21 +126,26 @@ module ParadeDB
|
|
|
131
126
|
)
|
|
132
127
|
require_search_field!
|
|
133
128
|
|
|
134
|
-
node =
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
129
|
+
node =
|
|
130
|
+
if term.nil? && tokenizer.nil? && distance.nil? && prefix.nil? && transposition_cost_one.nil?
|
|
131
|
+
builder.match_all(_paradedb_current_field, boost: boost, constant_score: constant_score)
|
|
132
|
+
else
|
|
133
|
+
builder.match(
|
|
134
|
+
_paradedb_current_field,
|
|
135
|
+
term,
|
|
136
|
+
tokenizer: tokenizer,
|
|
137
|
+
distance: distance,
|
|
138
|
+
prefix: prefix,
|
|
139
|
+
transposition_cost_one: transposition_cost_one,
|
|
140
|
+
boost: boost,
|
|
141
|
+
constant_score: constant_score
|
|
142
|
+
)
|
|
143
|
+
end
|
|
144
144
|
where(grouped(node))
|
|
145
145
|
end
|
|
146
146
|
|
|
147
|
-
def
|
|
148
|
-
|
|
147
|
+
def match_any(
|
|
148
|
+
term,
|
|
149
149
|
tokenizer: nil,
|
|
150
150
|
distance: nil,
|
|
151
151
|
prefix: nil,
|
|
@@ -157,7 +157,7 @@ module ParadeDB
|
|
|
157
157
|
|
|
158
158
|
node = builder.match_any(
|
|
159
159
|
_paradedb_current_field,
|
|
160
|
-
|
|
160
|
+
term,
|
|
161
161
|
tokenizer: tokenizer,
|
|
162
162
|
distance: distance,
|
|
163
163
|
prefix: prefix,
|
|
@@ -168,10 +168,10 @@ module ParadeDB
|
|
|
168
168
|
where(grouped(node))
|
|
169
169
|
end
|
|
170
170
|
|
|
171
|
-
def excluding(
|
|
171
|
+
def excluding(term)
|
|
172
172
|
require_search_field!
|
|
173
173
|
|
|
174
|
-
neg = builder.match(_paradedb_current_field,
|
|
174
|
+
neg = builder.match(_paradedb_current_field, term)
|
|
175
175
|
where(grouped(neg.not))
|
|
176
176
|
end
|
|
177
177
|
|
|
@@ -273,14 +273,6 @@ module ParadeDB
|
|
|
273
273
|
where(grouped(node))
|
|
274
274
|
end
|
|
275
275
|
|
|
276
|
-
# Match-all wrapper for APIs that need an explicit ParadeDB predicate.
|
|
277
|
-
# Use with `.search(:id)` (or any indexed field): `Product.search(:id).match_all`.
|
|
278
|
-
def match_all(boost: nil, constant_score: nil)
|
|
279
|
-
require_search_field!
|
|
280
|
-
|
|
281
|
-
where(grouped(builder.match_all(_paradedb_current_field, boost: boost, constant_score: constant_score)))
|
|
282
|
-
end
|
|
283
|
-
|
|
284
276
|
# Exists wrapper to match rows where the indexed field has a value.
|
|
285
277
|
# Use with `.search(:id)` (or another exists-compatible indexed field).
|
|
286
278
|
def exists(boost: nil, constant_score: nil)
|
|
@@ -487,6 +479,19 @@ module ParadeDB
|
|
|
487
479
|
rel.except(:select, :group).select(*group_nodes, *aggregate_nodes).group(*group_nodes)
|
|
488
480
|
end
|
|
489
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
|
+
|
|
490
495
|
def has_paradedb_predicate?
|
|
491
496
|
PredicateInspector.relation_has_paradedb_predicate?(self)
|
|
492
497
|
end
|
|
@@ -498,6 +503,12 @@ module ParadeDB
|
|
|
498
503
|
|
|
499
504
|
private
|
|
500
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
|
+
|
|
501
512
|
def paradedb_runtime_key_field
|
|
502
513
|
return primary_key unless klass.respond_to?(:paradedb_key_field)
|
|
503
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
|
|
@@ -83,7 +83,8 @@ dependencies:
|
|
|
83
83
|
- - "<"
|
|
84
84
|
- !ruby/object:Gem::Version
|
|
85
85
|
version: '9'
|
|
86
|
-
description:
|
|
86
|
+
description: Elastic-quality full-text search, vector retrieval, and aggregations
|
|
87
|
+
in Postgres via ParadeDB and ActiveRecord.
|
|
87
88
|
email:
|
|
88
89
|
- support@paradedb.com
|
|
89
90
|
executables: []
|
|
@@ -99,7 +100,6 @@ files:
|
|
|
99
100
|
- lib/parade_db.rb
|
|
100
101
|
- lib/parade_db/aggregations.rb
|
|
101
102
|
- lib/parade_db/arel.rb
|
|
102
|
-
- lib/parade_db/arel/README.md
|
|
103
103
|
- lib/parade_db/arel/builder.rb
|
|
104
104
|
- lib/parade_db/arel/nodes.rb
|
|
105
105
|
- lib/parade_db/arel/predications.rb
|
|
@@ -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:
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
# ParadeDB Arel Guide
|
|
2
|
-
|
|
3
|
-
This guide covers the low-level Arel API used by `rails-paradedb`.
|
|
4
|
-
Use it when you need explicit AST composition or direct SQL rendering.
|
|
5
|
-
|
|
6
|
-
## Quickstart
|
|
7
|
-
|
|
8
|
-
```ruby
|
|
9
|
-
require "parade_db/arel"
|
|
10
|
-
|
|
11
|
-
arel = ParadeDB::Arel::Builder.new(:products)
|
|
12
|
-
|
|
13
|
-
predicate = arel.match(:description, "running", "shoes")
|
|
14
|
-
.and(arel.regex(:description, "run.*"))
|
|
15
|
-
.and(arel.term(:in_stock, true))
|
|
16
|
-
|
|
17
|
-
sql = ParadeDB::Arel.to_sql(predicate, Product.connection)
|
|
18
|
-
# => ("products"."description" &&& 'running shoes' AND "products"."description" @@@ pdb.regex('run.*') AND "products"."in_stock" === TRUE)
|
|
19
|
-
|
|
20
|
-
Product.where(Arel.sql(sql))
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
Render any node with `ParadeDB::Arel.to_sql(node)`. All nodes respond to
|
|
24
|
-
`.and`, `.or`, and `.not`.
|
|
25
|
-
|
|
26
|
-
## Builder Methods
|
|
27
|
-
|
|
28
|
-
| Method | ParadeDB SQL |
|
|
29
|
-
| ------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
|
|
30
|
-
| `match(column, *terms, tokenizer: nil, distance:, prefix:, transposition_cost_one:, boost: nil, constant_score: nil)` | `column &&& 'a b'::pdb.whitespace::pdb.fuzzy(...)::pdb.boost(N)` |
|
|
31
|
-
| `match_any(column, *terms, tokenizer: nil, distance:, prefix:, transposition_cost_one:, boost: nil, constant_score: nil)` | `column \|\|\| 'a b'::pdb.whitespace::pdb.fuzzy(...)::pdb.boost(N)` |
|
|
32
|
-
| `phrase(column, text_or_terms, slop: n, tokenizer: nil, boost: nil, constant_score: nil)` | `column ### 'text'::pdb.slop(n)::pdb.whitespace` / `### ARRAY['a', 'b']::pdb.slop(n)` |
|
|
33
|
-
| `term(column, term, distance:, prefix:, transposition_cost_one:, boost: nil, constant_score: nil)` | `column === 'term'::pdb.fuzzy(...)::pdb.boost(N)` |
|
|
34
|
-
| `term_set(column, *terms, boost: nil, constant_score: nil)` | `column @@@ pdb.term_set(ARRAY[...])` |
|
|
35
|
-
| `regex(column, pattern, boost: nil, constant_score: nil)` | `column @@@ pdb.regex('pattern')` |
|
|
36
|
-
| `regex_phrase(column, *patterns, slop: nil, max_expansions: nil, boost: nil, constant_score: nil)` | `column @@@ pdb.regex_phrase(ARRAY['a', 'b'], slop => 2)` |
|
|
37
|
-
| `near(column, ParadeDB.proximity('a').within(d, 'b'))` | `column @@@ ('a' ## d ## 'b')` |
|
|
38
|
-
| `near(column, ParadeDB.proximity('a', 'b').within(d, ParadeDB.regex_term('c')).within(e, 'd'))` | `column @@@ ((pdb.prox_array('a', 'b') ## d ## pdb.prox_regex('c')) ## e ## 'd')` |
|
|
39
|
-
| `near(column, ParadeDB.proximity('a').within(d, 'b'), boost: 2.0)` | `column @@@ ('a' ## d ## 'b')::pdb.boost(2.0)` |
|
|
40
|
-
| `near(column, ParadeDB.proximity('a').within(d, 'b'), const: 1.0)` | `column @@@ ('a' ## d ## 'b')::pdb.const(1.0)` |
|
|
41
|
-
| `phrase_prefix(column, *terms, max_expansion: nil, boost: nil, constant_score: nil)` | `column @@@ pdb.phrase_prefix(ARRAY['a','b'][, 100])` |
|
|
42
|
-
| `parse(column, query, lenient: nil, conjunction_mode: nil, boost: nil, constant_score: nil)` | `column @@@ pdb.parse('q', lenient => true, conjunction_mode => true)` |
|
|
43
|
-
| `full_text(column, expr)` | `column @@@ expr` (raw right-hand value) |
|
|
44
|
-
| `match_all(column, boost: nil, constant_score: nil)` | `column @@@ pdb.all()` |
|
|
45
|
-
| `exists(column, boost: nil, constant_score: nil)` | `column @@@ pdb.exists()` |
|
|
46
|
-
| `range(column, value = nil, gte:, gt:, lte:, lt:, type:, boost: nil, constant_score: nil)` | `column @@@ pdb.range(int8range(3, 5, '[)'))` |
|
|
47
|
-
| `range_term(column, value, relation: nil, range_type: nil, boost: nil, constant_score: nil)` | `column @@@ pdb.range_term(1)` / `pdb.range_term('(1,2]'::int4range, 'Intersects')` |
|
|
48
|
-
| `more_like_this(column, key, fields: [:f1, :f2], options: {}, boost: nil, constant_score: nil)` | `column @@@ pdb.more_like_this(key, ARRAY['f1','f2'])` |
|
|
49
|
-
| `score(key_field)` | `pdb.score(key_field)` |
|
|
50
|
-
| `snippet(column, start, finish, max)` | `pdb.snippet(column, start, finish, max)` |
|
|
51
|
-
| `snippets(column, start_tag:, end_tag:, max_num_chars:, limit:, offset:, sort_by:)` | `pdb.snippets(column, ...)` |
|
|
52
|
-
| `snippet_positions(column)` | `pdb.snippet_positions(column)` |
|
|
53
|
-
| `agg(json, exact: nil)` | `pdb.agg(json[, false])` |
|
|
54
|
-
|
|
55
|
-
`Builder#[]` returns a column node for manual composition: `arel[:description]`.
|
|
56
|
-
|
|
57
|
-
> **Note:** `Builder` has no access to ActiveRecord model metadata.
|
|
58
|
-
> When calling `range_term` with a `relation:`, you must pass `range_type:` explicitly.
|
|
59
|
-
> The `SearchMethods` layer (`.search(:col).range_term(...)`) auto-infers `range_type` from the column's SQL type.
|
|
60
|
-
> Tokenizer overrides on `match`/`match_any` are mutually exclusive with fuzzy options.
|
|
61
|
-
|
|
62
|
-
## Composition
|
|
63
|
-
|
|
64
|
-
Boolean composition uses the standard helpers:
|
|
65
|
-
|
|
66
|
-
```ruby
|
|
67
|
-
fast = arel.match(:description, "running").and(arel.term(:rating, 4))
|
|
68
|
-
cheap = arel.match(:description, "budget")
|
|
69
|
-
predicate = fast.or(cheap.not)
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
`predicate` renders to:
|
|
73
|
-
|
|
74
|
-
```sql
|
|
75
|
-
(("products"."description" &&& 'running' AND "products"."rating" === 4) OR NOT ("products"."description" &&& 'budget'))
|
|
76
|
-
```
|