Skip to content

Commit 8830e40

Browse files
committed
fix: address review feedback
1 parent dfb3550 commit 8830e40

7 files changed

Lines changed: 48 additions & 37 deletions

File tree

README.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636

3737
## ParadeDB for Rails
3838

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.
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. The integration covers both full-text (BM25) and vector search — vector search indexes pgvector `vector(n)` columns directly. Follow the [getting started guide](https://docs.paradedb.com/documentation/getting-started/environment#rails) to begin.
4040

4141
## Requirements & Compatibility
4242

@@ -46,17 +46,18 @@ The official ActiveRecord integration for [ParadeDB](https://paradedb.com) (powe
4646
| Rails | 7.2+ |
4747
| ParadeDB | 0.22.0+ |
4848
| PostgreSQL | 15+ (PostgreSQL adapter with ParadeDB extension) |
49+
| pgvector | Required for vector search (ships with ParadeDB) |
4950

5051
## Vector Search
5152

52-
rails-paradedb natively supports pgvector `vector(n)` columns — no `neighbor` or other pgvector gem required. Declare the column, list it in the BM25 index with a distance metric, and run Top-K queries:
53+
rails-paradedb natively supports pgvector `vector(n)` columns — no `neighbor` or other pgvector gem required. Declare the column, list it in the ParadeDB index with a distance metric, and run Top-K queries:
5354

5455
```ruby
5556
class AddVectorSearchToProducts < ActiveRecord::Migration[8.1]
5657
def change
5758
add_column :products, :embedding, :vector, limit: 384
5859

59-
add_bm25_index :products,
60+
add_paradedb_index :products,
6061
fields: { id: {}, description: {}, embedding: { metric: :l2 } },
6162
key_field: :id
6263
end
@@ -73,7 +74,7 @@ Product.nearest(:embedding, query_embedding).limit(10)
7374
Product.search(:description).match_all("shoes").nearest(:embedding, query_embedding).limit(10)
7475
```
7576

76-
Top-K index pushdown requires a `LIMIT`, and the query metric must match the index opclass metric — a mismatch still returns correct results but silently falls back to a plain sort. Vector columns inside BM25 indexes require a ParadeDB build with bm25 vector opclasses (not yet in a released ParadeDB version); the plain `vector(n)` column type and distance expressions work on any ParadeDB or Postgres with pgvector.
77+
Top-K index pushdown requires a `LIMIT`, and the query metric must match the index opclass metric — a mismatch still returns correct results but silently falls back to a plain sort. Vector columns inside ParadeDB indexes require pg_search 0.25.0+ (unreleased); the plain `vector(n)` column type and distance expressions work on any ParadeDB or Postgres with pgvector.
7778

7879
## Examples
7980

examples/README.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -121,10 +121,9 @@ Structure:
121121

122122
1. Vector Search (`vector_search/`)
123123

124-
Top-K vector search inside the BM25 index: `vector(n)` columns, metric
125-
opclasses, and `nearest` queries. Requires a ParadeDB build with bm25 vector
126-
opclasses (not yet in a released ParadeDB version); the setup script aborts
127-
with a clear message otherwise.
124+
Top-K vector search inside the ParadeDB index: `vector(n)` columns, metric
125+
opclasses, and `nearest` queries. Requires pg_search 0.25.0+ (unreleased);
126+
the setup script aborts with a clear message otherwise.
128127

129128
```bash
130129
BUNDLE_GEMFILE=examples/Gemfile bundle exec ruby examples/vector_search/setup.rb

examples/hybrid_rrf/hybrid_rrf.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ def display_results(query, rows)
134134
puts "=" * 80
135135
puts "Hybrid Search with Reciprocal Rank Fusion (single SQL query)"
136136
puts "=" * 80
137-
puts "\nCombining ParadeDB BM25 + native vector distance in one CTE-based query"
137+
puts "\nCombining ParadeDB BM25 + vector distance in one CTE-based query"
138138

139139
HybridRrfSetup.setup!
140140
MockItem.reset_column_information

examples/vector_search/setup.rb

100644100755
Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -50,14 +50,17 @@ def connect!
5050
ActiveRecord::Base.logger = nil
5151
end
5252

53-
def vector_search_supported?
54-
ActiveRecord::Base.connection.select_value(<<~SQL)
55-
SELECT 1
53+
# Prefer the paradedb access method when its vector opclasses exist,
54+
# otherwise fall back to bm25; nil when the server has no vector opclasses.
55+
def vector_index_am
56+
ams = ActiveRecord::Base.connection.select_values(<<~SQL)
57+
SELECT am.amname
5658
FROM pg_opclass oc
5759
JOIN pg_am am ON am.oid = oc.opcmethod
58-
WHERE am.amname = 'bm25' AND oc.opcname = 'vector_l2_ops'
59-
LIMIT 1
60+
WHERE am.amname IN ('paradedb', 'bm25') AND oc.opcname = 'vector_l2_ops'
6061
SQL
62+
63+
ams.include?("paradedb") ? "paradedb" : ams.first
6164
end
6265

6366
def setup!
@@ -67,10 +70,12 @@ def setup!
6770
conn.execute("CREATE EXTENSION IF NOT EXISTS pg_search;")
6871
conn.execute("CREATE EXTENSION IF NOT EXISTS vector;")
6972

70-
unless vector_search_supported?
71-
abort "This example requires a ParadeDB build with bm25 vector opclasses " \
72-
"(unreleased; see paradedb/paradedb branch mvp/vector-search)."
73+
am = vector_index_am
74+
unless am
75+
abort "This example requires vector opclasses for the paradedb/bm25 access methods " \
76+
"(pg_search 0.25.0+, unreleased)."
7377
end
78+
VectorItemIndex.am = am
7479

7580
conn.execute("DROP TABLE IF EXISTS vector_items CASCADE;")
7681
ActiveRecord::Schema.define do

examples/vector_search/vector_search.rb

100644100755
File mode changed.

spec/vector_search_integration_spec.rb

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -58,11 +58,12 @@ class VectorItemIndex < ParadeDB::Index
5858
end
5959
end
6060

61-
it "runs Top-K vector search through the bm25 index" do
62-
skip_unless_bm25_vector_support!
61+
it "runs Top-K vector search through the ParadeDB index" do
62+
skip_unless_vector_index_support!
6363

6464
conn = ActiveRecord::Base.connection
65-
conn.remove_bm25_index(:vector_items, if_exists: true)
65+
conn.remove_paradedb_index(:vector_items, if_exists: true)
66+
VectorItemIndex.am = vector_index_am
6667
conn.create_paradedb_index(VectorItemIndex)
6768

6869
labels = VectorItem.nearest(:embedding, [1, 0, 0]).limit(2).pluck(:label)
@@ -75,26 +76,28 @@ class VectorItemIndex < ParadeDB::Index
7576
.pluck(:label)
7677
assert_equal ["near x"], filtered
7778
ensure
78-
ActiveRecord::Base.connection.remove_bm25_index(:vector_items, if_exists: true)
79+
ActiveRecord::Base.connection.remove_paradedb_index(:vector_items, if_exists: true)
7980
end
8081

8182
it "round-trips vector opclasses through the schema dumper" do
82-
skip_unless_bm25_vector_support!
83+
skip_unless_vector_index_support!
8384

8485
conn = ActiveRecord::Base.connection
85-
conn.remove_bm25_index(:vector_items, if_exists: true)
86+
conn.remove_paradedb_index(:vector_items, if_exists: true)
8687

88+
am = vector_index_am
8789
cosine_index = Class.new(ParadeDB::Index) do
8890
self.table_name = :vector_items
8991
self.key_field = :id
92+
self.am = am
9093
self.fields = { id: {}, label: {}, embedding: { metric: :cosine } }
9194
end
9295
conn.create_paradedb_index(cosine_index)
9396

9497
output = dump_schema
95-
assert_match(/add_bm25_index :vector_items,.*embedding: \{ metric: :cosine \}/, output)
98+
assert_match(/add_paradedb_index :vector_items,.*embedding: \{ metric: :cosine \}/, output)
9699
ensure
97-
ActiveRecord::Base.connection.remove_bm25_index(:vector_items, if_exists: true)
100+
ActiveRecord::Base.connection.remove_paradedb_index(:vector_items, if_exists: true)
98101
end
99102

100103
private
@@ -109,24 +112,27 @@ def pgvector_available?
109112
)
110113
end
111114

112-
def bm25_vector_search_supported?
113-
ActiveRecord::Base.connection.select_value(<<~SQL)
114-
SELECT 1
115+
def vector_opclass_ams
116+
@vector_opclass_ams ||= ActiveRecord::Base.connection.select_values(<<~SQL)
117+
SELECT am.amname
115118
FROM pg_opclass oc
116119
JOIN pg_am am ON am.oid = oc.opcmethod
117-
WHERE am.amname = 'bm25' AND oc.opcname = 'vector_l2_ops'
118-
LIMIT 1
120+
WHERE am.amname IN ('paradedb', 'bm25') AND oc.opcname = 'vector_l2_ops'
119121
SQL
120122
end
121123

122-
def skip_unless_bm25_vector_support!
123-
return if bm25_vector_search_supported?
124+
def vector_index_am
125+
vector_opclass_ams.include?(paradedb_test_am) ? paradedb_test_am : vector_opclass_ams.first
126+
end
127+
128+
def skip_unless_vector_index_support!
129+
return if vector_index_am
124130

125131
version = ActiveRecord::Base.connection.select_value(
126132
"SELECT extversion FROM pg_extension WHERE extname = 'pg_search'"
127133
)
128-
skip "pg_search #{version} has no bm25 vector opclasses; vector-in-bm25 support " \
129-
"is unreleased (paradedb/paradedb branch mvp/vector-search)"
134+
skip "pg_search #{version} has no vector opclasses for the paradedb/bm25 access methods; " \
135+
"vector-in-index support requires pg_search 0.25.0+ (unreleased)"
130136
end
131137

132138
def ensure_vector_schema!

spec/vector_search_unit_spec.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -140,14 +140,14 @@ def sql(node)
140140
assert_equal "embedding", entry.query_key
141141
end
142142

143-
it "emits opclasses in bm25 index DDL" do
143+
it "emits opclasses in index DDL" do
144144
sql = ActiveRecord::Base.connection.send(
145145
:build_create_sql, VectorSearchProductIndex.compiled_definition, if_not_exists: false
146146
)
147147

148148
assert_sql_equal <<~SQL, sql
149149
CREATE INDEX vector_search_products_bm25_idx ON products
150-
USING bm25 (id, description, embedding vector_cosine_ops)
150+
USING paradedb (id, description, embedding vector_cosine_ops)
151151
WITH (key_field='id')
152152
SQL
153153
end
@@ -207,7 +207,7 @@ def sql(node)
207207
recorder = Class.new do
208208
attr_reader :captured
209209

210-
def add_bm25_index(table, fields:, key_field:, name:, **)
210+
def add_paradedb_index(table, fields:, key_field:, name:, **)
211211
@captured = { table: table, fields: fields, key_field: key_field, name: name }
212212
end
213213
end.new

0 commit comments

Comments
 (0)