rails-paradedb 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e28ebb1ec7734474a1ed321766b2e652f4aa1d56aadcc16d48667ac4ddbfc5bc
4
- data.tar.gz: 51c25b12045eed272ad0e71475c22f9a670c4da6ff46fcad6d99ce32e3bc1f6e
3
+ metadata.gz: 25216744f6402f3ffdc06d8381f8ed801acec379dc15c71b92187b9e28a61b6d
4
+ data.tar.gz: 64dfcdcefa05ff52d0cd6b4b0292c005a0f4c2cf3a193ee07ac27bdae175ddd0
5
5
  SHA512:
6
- metadata.gz: 4fced3ee50bef2ef4c59567246e9c5382a7e2e3f60293a785d33fe4e9e857abea685740fcf2c2da38c98dce4371d13010f9eacbbbe1c580df760208a20542f3d
7
- data.tar.gz: 2e3efbf965bb99866dffe45de2963528ccf9ebfaa4a947abe5b72211306409d5b006eb1e90621cc6591e72f473d91593348b0d1bcaaa1e96cbc08cbe252e8615
6
+ metadata.gz: f3fb68ca97ffc5a30f6d503f15e97b02bc8db1de5aaeab020991848f777bc401fc62d06049490790ef75416b80c6a226edeb8cfe91e2bd8d8c3d76d12e95accb
7
+ data.tar.gz: 6c1e58314734c06d138a32f8a6928d13c7178e7db44e8460a45efbfb128e168d81f22dac53f74bc57ccd19f88f4ad0fdf60c3590dfcfd8d04e39b80d39faf531
data/CHANGELOG.md CHANGED
@@ -4,6 +4,28 @@ All notable changes to this project will be documented in this file. The format
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ [0.9.0] - 2026-07-14
8
+
9
+ ### Added
10
+
11
+ - Allow SQL/Arel function nodes as `match_all` and `match_any` query arguments.
12
+
13
+ ### Changed
14
+
15
+ - **BREAKING**: Restrict match APIs to a single query argument.
16
+ - **BREAKING**: Rename relation query methods `matching_all` and `matching_any`
17
+ to `match_all` and `match_any`.
18
+
19
+ ### Removed
20
+
21
+ - **BREAKING**: Removed max_term_freq from the more_like_this API.
22
+
23
+ ## [0.8.0] - 2026-06-15
24
+
25
+ ### Changed
26
+
27
+ - **BREAKING**: The `Tokenizer` class is now namespaced as `ParadeDB::Tokenizer`. Update references from `Tokenizer.simple(...)` to `ParadeDB::Tokenizer.simple(...)`. Schema dumps (`schema.rb`) now emit the fully-qualified constant.
28
+
7
29
  ## [0.7.0] - 2026-04-21
8
30
 
9
31
  ### Changed
@@ -132,7 +154,9 @@ All notable changes to this project will be documented in this file. The format
132
154
  - Schema dump/load round-trip for tokenizer configuration and index options
133
155
  (including `target_segment_count`)
134
156
 
135
- [Unreleased]: https://github.com/paradedb/rails-paradedb/compare/v0.7.0...HEAD
157
+ [Unreleased]: https://github.com/paradedb/rails-paradedb/compare/v0.9.0...HEAD
158
+ [0.9.0]: https://github.com/paradedb/rails-paradedb/releases/tag/v0.9.0
159
+ [0.8.0]: https://github.com/paradedb/rails-paradedb/releases/tag/v0.8.0
136
160
  [0.7.0]: https://github.com/paradedb/rails-paradedb/releases/tag/v0.7.0
137
161
  [0.6.0]: https://github.com/paradedb/rails-paradedb/releases/tag/v0.6.0
138
162
  [0.5.0]: https://github.com/paradedb/rails-paradedb/releases/tag/v0.5.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"><img src="https://github.com/paradedb/paradedb/raw/main/docs/logo/readme.svg" alt="ParadeDB"></a>
4
- <br>
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>Simple, Elastic-quality search for Postgres</b><br/>
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,7 +36,7 @@
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 BM25 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.
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 BM25 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
 
@@ -21,7 +21,7 @@ module ParadeDB
21
21
 
22
22
  def match(
23
23
  column,
24
- *terms,
24
+ term = nil,
25
25
  tokenizer: nil,
26
26
  distance: nil,
27
27
  prefix: nil,
@@ -35,7 +35,7 @@ module ParadeDB
35
35
  prefix: prefix,
36
36
  transposition_cost_one: transposition_cost_one
37
37
  )
38
- rhs = quoted_value(join_terms(terms))
38
+ rhs = term_query_node(term)
39
39
  rhs = apply_fuzzy(
40
40
  rhs,
41
41
  distance: distance,
@@ -50,7 +50,7 @@ module ParadeDB
50
50
 
51
51
  def match_any(
52
52
  column,
53
- *terms,
53
+ term = nil,
54
54
  tokenizer: nil,
55
55
  distance: nil,
56
56
  prefix: nil,
@@ -64,7 +64,7 @@ module ParadeDB
64
64
  prefix: prefix,
65
65
  transposition_cost_one: transposition_cost_one
66
66
  )
67
- rhs = quoted_value(join_terms(terms))
67
+ rhs = term_query_node(term)
68
68
  rhs = apply_fuzzy(
69
69
  rhs,
70
70
  distance: distance,
@@ -302,7 +302,7 @@ module ParadeDB
302
302
  def apply_tokenizer(node, tokenizer)
303
303
  return node if tokenizer.nil?
304
304
 
305
- unless tokenizer.is_a?(Tokenizer)
305
+ unless tokenizer.is_a?(ParadeDB::Tokenizer)
306
306
  raise ArgumentError, "tokenizer must be a Tokenizer"
307
307
  end
308
308
 
@@ -538,12 +538,24 @@ module ParadeDB
538
538
  )
539
539
  end
540
540
 
541
- def join_terms(terms)
542
- joined = terms.flatten.compact.map(&:to_s).join(" ")
541
+ def join_term(term)
542
+ joined = term.to_s
543
543
  raise ArgumentError, "at least one search term is required" if joined.strip.empty?
544
544
  joined
545
545
  end
546
546
 
547
+ def term_query_node(term)
548
+ return term if arel_expression?(term)
549
+
550
+ quoted_value(join_term(term))
551
+ end
552
+
553
+ def arel_expression?(value)
554
+ value.is_a?(::Arel::Nodes::Node) ||
555
+ value.is_a?(::Arel::Attributes::Attribute) ||
556
+ value.is_a?(::Arel::Nodes::SqlLiteral)
557
+ end
558
+
547
559
  def normalize_term_set_terms(terms)
548
560
  values = Array(terms).flatten.compact
549
561
  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(*terms, tokenizer: nil, distance: nil, prefix: nil, transposition_cost_one: nil, boost: nil)
11
- rhs = pdb_quoted(pdb_join_terms(terms))
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(*terms, tokenizer: nil, distance: nil, prefix: nil, transposition_cost_one: nil, boost: nil)
19
- rhs = pdb_quoted(pdb_join_terms(terms))
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)
@@ -193,11 +193,19 @@ module ParadeDB
193
193
  ::Arel::Nodes.build_quoted(value)
194
194
  end
195
195
 
196
- def pdb_join_terms(terms)
197
- joined = terms.flatten.compact.map(&:to_s).join(" ")
196
+ def pdb_term_query_node(term)
197
+ return term if pdb_arel_expression?(term)
198
+
199
+ joined = term.to_s
198
200
  raise ArgumentError, "at least one search term is required" if joined.strip.empty?
199
201
 
200
- joined
202
+ pdb_quoted(joined)
203
+ end
204
+
205
+ def pdb_arel_expression?(value)
206
+ value.is_a?(::Arel::Nodes::Node) ||
207
+ value.is_a?(::Arel::Attributes::Attribute) ||
208
+ value.is_a?(::Arel::Nodes::SqlLiteral)
201
209
  end
202
210
 
203
211
  def pdb_validate_numeric!(value, name)
@@ -53,7 +53,7 @@ module ParadeDB
53
53
 
54
54
  class << self
55
55
  def parse(source_name, tokenizer, context:)
56
- unless tokenizer.is_a?(Tokenizer)
56
+ unless tokenizer.is_a?(ParadeDB::Tokenizer)
57
57
  raise InvalidIndexDefinition, "#{context} for #{source_name.inspect} must be a Tokenizer"
58
58
  end
59
59
 
@@ -422,7 +422,7 @@ module ParadeDB
422
422
 
423
423
  with_sql = indexdef[start..pos - 2]
424
424
  trailing_sql = indexdef[pos..]&.strip
425
- trailing_sql = nil if trailing_sql&.empty?
425
+ trailing_sql = nil if trailing_sql == ""
426
426
 
427
427
  [with_sql, trailing_sql]
428
428
  end
@@ -695,13 +695,13 @@ module ParadeDB
695
695
  end
696
696
 
697
697
  def bm25_tokenizer_ruby(name, positional_args, options)
698
- if name.match?(/\A[a-z_][a-z0-9_]*\z/) && Tokenizer.respond_to?(name)
698
+ if name.match?(/\A[a-z_][a-z0-9_]*\z/) && ParadeDB::Tokenizer.respond_to?(name)
699
699
  args = positional_args.map { |arg| ruby_literal(arg) }
700
700
  args << "options: #{ruby_hash_literal(options)}" unless options.empty?
701
- return "Tokenizer.#{name}(#{args.join(', ')})"
701
+ return "ParadeDB::Tokenizer.#{name}(#{args.join(', ')})"
702
702
  end
703
703
 
704
- "Tokenizer.new(#{name.inspect}, #{ruby_literal(positional_args.empty? ? nil : positional_args)}, #{ruby_literal(options.empty? ? nil : options)})"
704
+ "ParadeDB::Tokenizer.new(#{name.inspect}, #{ruby_literal(positional_args.empty? ? nil : positional_args)}, #{ruby_literal(options.empty? ? nil : options)})"
705
705
  end
706
706
 
707
707
  def split_sql_arguments(args_sql)
@@ -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 matching_all(
124
- *terms,
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 = builder.match(
135
- _paradedb_current_field,
136
- *terms,
137
- tokenizer: tokenizer,
138
- distance: distance,
139
- prefix: prefix,
140
- transposition_cost_one: transposition_cost_one,
141
- boost: boost,
142
- constant_score: constant_score
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 matching_any(
148
- *terms,
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
- *terms,
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(*terms)
171
+ def excluding(term)
172
172
  require_search_field!
173
173
 
174
- neg = builder.match(_paradedb_current_field, *terms)
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)
@@ -1,95 +1,97 @@
1
- class Tokenizer
2
- attr_reader :name, :positional_args, :options
1
+ module ParadeDB
2
+ class Tokenizer
3
+ attr_reader :name, :positional_args, :options
4
+
5
+ def initialize(name, positional_args, options)
6
+ @name = name
7
+ @positional_args = positional_args
8
+ @options = options
9
+ end
3
10
 
4
- def initialize(name, positional_args, options)
5
- @name = name
6
- @positional_args = positional_args
7
- @options = options
8
- end
11
+ def render()
12
+ if options.nil? && positional_args.nil?
13
+ return "pdb.#{name}"
14
+ end
9
15
 
10
- def render()
11
- if options.nil? && positional_args.nil?
12
- return "pdb.#{name}"
13
- end
16
+ args = []
17
+ if !positional_args.nil?
18
+ args.concat(positional_args.map { |x| render_positional_arg(x) })
19
+ end
20
+ if !options.nil?
21
+ args.concat(options.map {|k, v| quote_term("#{k}=#{v}")})
22
+ end
14
23
 
15
- args = []
16
- if !positional_args.nil?
17
- args.concat(positional_args.map { |x| render_positional_arg(x) })
18
- end
19
- if !options.nil?
20
- args.concat(options.map {|k, v| quote_term("#{k}=#{v}")})
24
+ return "pdb.#{name}(#{args.join(",")})"
21
25
  end
22
26
 
23
- return "pdb.#{name}(#{args.join(",")})"
24
- end
25
-
26
- def self.whitespace(options: nil)
27
- new("whitespace", nil, options)
28
- end
27
+ def self.whitespace(options: nil)
28
+ new("whitespace", nil, options)
29
+ end
29
30
 
30
- def self.unicode_words(options: nil)
31
- new("unicode_words", nil, options)
32
- end
31
+ def self.unicode_words(options: nil)
32
+ new("unicode_words", nil, options)
33
+ end
33
34
 
34
- def self.ngram(min_gram, max_gram, options: nil)
35
- new("ngram", [min_gram, max_gram], options)
36
- end
35
+ def self.ngram(min_gram, max_gram, options: nil)
36
+ new("ngram", [min_gram, max_gram], options)
37
+ end
37
38
 
38
- def self.simple(options: nil)
39
- new("simple", nil, options)
40
- end
39
+ def self.simple(options: nil)
40
+ new("simple", nil, options)
41
+ end
41
42
 
42
- def self.literal(options: nil)
43
- new("literal", nil, options)
44
- end
43
+ def self.literal(options: nil)
44
+ new("literal", nil, options)
45
+ end
45
46
 
46
- def self.literal_normalized(options: nil)
47
- new("literal_normalized", nil, options)
48
- end
47
+ def self.literal_normalized(options: nil)
48
+ new("literal_normalized", nil, options)
49
+ end
49
50
 
50
- def self.edge_ngram(min_gram, max_gram, options: nil)
51
- new("edge_ngram", [min_gram, max_gram], options)
52
- end
51
+ def self.edge_ngram(min_gram, max_gram, options: nil)
52
+ new("edge_ngram", [min_gram, max_gram], options)
53
+ end
53
54
 
54
- def self.regex_pattern(pattern, options: nil)
55
- new("regex_pattern", [pattern], options)
56
- end
55
+ def self.regex_pattern(pattern, options: nil)
56
+ new("regex_pattern", [pattern], options)
57
+ end
57
58
 
58
- def self.chinese_compatible(options: nil)
59
- new("chinese_compatible", nil, options)
60
- end
59
+ def self.chinese_compatible(options: nil)
60
+ new("chinese_compatible", nil, options)
61
+ end
61
62
 
62
- def self.lindera(dictionary, options: nil)
63
- new("lindera", [dictionary], options)
64
- end
63
+ def self.lindera(dictionary, options: nil)
64
+ new("lindera", [dictionary], options)
65
+ end
65
66
 
66
- def self.icu(options: nil)
67
- new("icu", nil, options)
68
- end
67
+ def self.icu(options: nil)
68
+ new("icu", nil, options)
69
+ end
69
70
 
70
- def self.jieba(options: nil)
71
- new("jieba", nil, options)
72
- end
71
+ def self.jieba(options: nil)
72
+ new("jieba", nil, options)
73
+ end
73
74
 
74
- def self.source_code(options: nil)
75
- new("source_code", nil, options)
76
- end
75
+ def self.source_code(options: nil)
76
+ new("source_code", nil, options)
77
+ end
77
78
 
78
- private
79
+ private
79
80
 
80
- def quote_term(value)
81
- escaped = value.gsub("'", "''")
82
- "'#{escaped}'"
83
- end
81
+ def quote_term(value)
82
+ escaped = value.gsub("'", "''")
83
+ "'#{escaped}'"
84
+ end
84
85
 
85
- def render_positional_arg(value)
86
- case value
87
- when true, false, Numeric
88
- value.to_s
89
- when String
90
- quote_term(value)
91
- else
92
- raise InvalidArgumentError, "Unsupported tokenizer arg type: #{value.class}"
86
+ def render_positional_arg(value)
87
+ case value
88
+ when true, false, Numeric
89
+ value.to_s
90
+ when String
91
+ quote_term(value)
92
+ else
93
+ raise ArgumentError, "Unsupported tokenizer arg type: #{value.class}"
94
+ end
93
95
  end
94
96
  end
95
97
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ParadeDB
4
- VERSION = "0.7.0"
4
+ VERSION = "0.9.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails-paradedb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.0
4
+ version: 0.9.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: Simple, Elastic-quality search for Postgres via ParadeDB and ActiveRecord.
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
@@ -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
- ```