Skip to content

Commit be2eb22

Browse files
committed
fix: address review feedback
1 parent 4b2a77a commit be2eb22

8 files changed

Lines changed: 69 additions & 102 deletions

File tree

NUGET-README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# ParadeDB for Entity Framework Core
22

3-
The official [Entity Framework Core](https://learn.microsoft.com/en-us/ef/core/) 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#entity-framework-core) to begin.
3+
The official [Entity Framework Core](https://learn.microsoft.com/en-us/ef/core/) integration for [ParadeDB](https://paradedb.com), including first-class support for managing BM25 indexes and running queries using the full ParadeDB API. The integration covers both full-text and vector search — vector search indexes pgvector `vector` columns directly, with no separate pgvector ORM plugin required. Follow the [getting started guide](https://docs.paradedb.com/documentation/getting-started/environment#entity-framework-core) to begin.
44

55
## Requirements & Compatibility
66

@@ -10,6 +10,7 @@ The official [Entity Framework Core](https://learn.microsoft.com/en-us/ef/core/)
1010
| EF Core | 8.0+ |
1111
| ParadeDB | 0.23.0+ |
1212
| PostgreSQL | 15+ (with ParadeDB extension) |
13+
| pgvector | Required for vector search |
1314

1415
## Examples
1516

README.md

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

3737
## ParadeDB for Entity Framework Core
3838

39-
The official [Entity Framework Core](https://learn.microsoft.com/en-us/ef/core/) 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#entity-framework-core) to begin.
39+
The official [Entity Framework Core](https://learn.microsoft.com/en-us/ef/core/) 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. The integration covers both full-text and vector search — vector search indexes pgvector `vector` columns directly, with no separate pgvector ORM plugin required. Follow the [getting started guide](https://docs.paradedb.com/documentation/getting-started/environment#entity-framework-core) to begin.
4040

4141
## Requirements & Compatibility
4242

@@ -46,49 +46,7 @@ The official [Entity Framework Core](https://learn.microsoft.com/en-us/ef/core/)
4646
| EF Core | 8.0+ |
4747
| ParadeDB | 0.23.0+ |
4848
| PostgreSQL | 15+ (with ParadeDB extension) |
49-
50-
## Vector Search
51-
52-
ParadeDB indexes pgvector `vector` columns directly inside its BM25 index — no
53-
pgvector ORM plugin is required. Map a `float[]` property to a `vector(n)`
54-
column, pick a distance metric for the index, and order by the matching
55-
distance function:
56-
57-
```csharp
58-
public class Item
59-
{
60-
public int Id { get; set; }
61-
public float[]? Embedding { get; set; }
62-
}
63-
64-
// Model configuration
65-
modelBuilder.Entity<Item>(entity =>
66-
{
67-
entity.Property(x => x.Embedding).HasVectorType(384);
68-
69-
entity
70-
.HasParadeDbIndex("items_idx", x => x.Id) // also available as HasBm25Index
71-
.HasField(x => x.Embedding, VectorMetric.Cosine);
72-
});
73-
74-
// Top-K query: a `@@@` predicate (`EF.Functions.All` for match-all here) and
75-
// a LIMIT (`Take`) are required for the index to serve the query
76-
float[] queryEmbedding = GetQueryEmbedding();
77-
78-
var results = await db
79-
.Items.Where(x => EF.Functions.All(x.Id))
80-
.OrderBy(x => EF.Functions.CosineDistance(x.Embedding, queryEmbedding))
81-
.Take(10)
82-
.ToListAsync();
83-
```
84-
85-
`VectorMetric.L2` (`<->`, the default), `VectorMetric.Cosine` (`<=>`), and
86-
`VectorMetric.InnerProduct` (`<#>`) map to the `vector_l2_ops`,
87-
`vector_cosine_ops`, and `vector_ip_ops` operator classes. The distance
88-
function used in `OrderBy` must match the metric of the index opclass —
89-
`L2Distance` with `L2`, `CosineDistance` with `Cosine`, and `InnerProduct`
90-
with `InnerProduct` — otherwise the query still returns correct results but
91-
falls back to a sequential scan instead of Top-K index pushdown.
49+
| pgvector | Required for vector search |
9250

9351
## Examples
9452

examples/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,8 @@ dotnet run --project examples/Examples.csproj -p:Example=HybridRrf
4949

5050
## Vector Search (`examples/VectorSearch`)
5151

52-
Runs Top-K nearest-neighbor queries over a pgvector column with the native
53-
vector support, backed by a bm25 index when the server supports it.
52+
Runs Top-K nearest-neighbor queries over a pgvector column with ParadeDB
53+
vector support, backed by a paradedb index when the server supports it.
5454

5555
```bash
5656
dotnet run --project examples/Examples.csproj -p:Example=VectorSearch

examples/Shared/ExampleSetup.cs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@ await db.Database.ExecuteSqlRawAsync(
6161
"CALL paradedb.create_bm25_test_table(schema_name => 'public', table_name => 'mock_items')"
6262
);
6363
await db.Database.ExecuteSqlRawAsync("DROP INDEX IF EXISTS mock_items_bm25_idx");
64+
// The VectorSearch example may have left its own index behind, and newer
65+
// pg_search versions allow only one ParadeDB index per table
66+
await db.Database.ExecuteSqlRawAsync("DROP INDEX IF EXISTS mock_items_vector_idx");
6467
var accessMethod = await GetIndexAccessMethodAsync(db);
6568
await db.Database.ExecuteSqlRawAsync(
6669
"""
@@ -119,14 +122,18 @@ public static async Task SetupHybridAsync(DbContext db)
119122
{
120123
await SetupMockItemsAsync(db);
121124
await db.Database.ExecuteSqlRawAsync("CREATE EXTENSION IF NOT EXISTS vector");
125+
// Newer pg_search versions create mock_items with an embedding column of a
126+
// different dimension; the examples use 384-dimensional embeddings
122127
await db.Database.ExecuteSqlRawAsync(
123128
"""
124129
DO $$
125130
BEGIN
126-
IF NOT EXISTS (
127-
SELECT 1 FROM information_schema.columns
128-
WHERE table_name = 'mock_items' AND column_name = 'embedding'
129-
) THEN
131+
IF (
132+
SELECT atttypmod FROM pg_attribute
133+
WHERE attrelid = 'mock_items'::regclass
134+
AND attname = 'embedding' AND NOT attisdropped
135+
) IS DISTINCT FROM 384 THEN
136+
ALTER TABLE mock_items DROP COLUMN IF EXISTS embedding;
130137
ALTER TABLE mock_items ADD COLUMN embedding vector(384);
131138
END IF;
132139
END $$;

examples/VectorSearch/Program.cs

Lines changed: 26 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -19,38 +19,42 @@
1919
await ExampleSetup.SetupHybridAsync(dbContext);
2020
await EmbeddingLoader.LoadAsync(dbContext);
2121

22-
var supportsVectorIndex = await SupportsVectorInBm25Async(dbContext);
23-
if (supportsVectorIndex)
22+
var vectorAccessMethod = await GetVectorAccessMethodAsync(dbContext);
23+
if (vectorAccessMethod is not null)
2424
{
25+
// Newer pg_search versions allow only one ParadeDB index per table
26+
await dbContext.Database.ExecuteSqlRawAsync("DROP INDEX IF EXISTS mock_items_bm25_idx");
2527
await dbContext.Database.ExecuteSqlRawAsync("DROP INDEX IF EXISTS mock_items_vector_idx");
2628
await dbContext.Database.ExecuteSqlRawAsync(
2729
"""
2830
CREATE INDEX mock_items_vector_idx ON mock_items
29-
USING bm25 (id, embedding vector_cosine_ops)
31+
USING paradedb (id, embedding vector_cosine_ops)
3032
WITH (key_field='id');
31-
"""
33+
""".Replace("USING paradedb", $"USING {vectorAccessMethod}")
34+
);
35+
Console.WriteLine(
36+
$"Created a {vectorAccessMethod} index over the embedding column (cosine metric)"
3237
);
33-
Console.WriteLine("Created a bm25 index over the embedding column (cosine metric)");
3438
}
3539
else
3640
{
3741
Console.WriteLine(
38-
"This ParadeDB version does not support vector columns in bm25 indexes; "
39-
+ "queries fall back to a sequential scan."
42+
"This ParadeDB version does not support vector columns in ParadeDB indexes "
43+
+ "(requires pg_search 0.25.0+, unreleased); queries fall back to a sequential scan."
4044
);
4145
}
4246

43-
await Demo(dbContext, "running shoes", supportsVectorIndex);
44-
await Demo(dbContext, "wireless earbuds", supportsVectorIndex);
47+
await Demo(dbContext, "running shoes", vectorAccessMethod is not null);
48+
await Demo(dbContext, "wireless earbuds", vectorAccessMethod is not null);
4549
return;
4650

4751
static async Task Demo(AppDbContext db, string query, bool useIndex)
4852
{
4953
var queryEmbedding = QueryEmbeddings.Values[query];
5054

5155
// The @@@ predicate (EF.Functions.All) and the LIMIT are required for the
52-
// bm25 index to serve the query; the ORDER BY metric must match the index
53-
// opclass (cosine here)
56+
// ParadeDB index to serve the query; the ORDER BY metric must match the
57+
// index opclass (cosine here)
5458
var candidates = useIndex
5559
? db.MockItems.Where(x => EF.Functions.All(x.Id))
5660
: db.MockItems.Where(x => x.Embedding != null);
@@ -76,18 +80,22 @@ static async Task Demo(AppDbContext db, string query, bool useIndex)
7680
}
7781
}
7882

79-
static async Task<bool> SupportsVectorInBm25Async(AppDbContext db)
83+
// The vector operator classes may be declared for the `paradedb` access method, the `bm25`
84+
// access method, or neither, depending on the pg_search version
85+
static async Task<string?> GetVectorAccessMethodAsync(AppDbContext db)
8086
{
81-
var count = await db
82-
.Database.SqlQueryRaw<long>(
87+
var accessMethods = await db
88+
.Database.SqlQueryRaw<string>(
8389
"""
84-
SELECT count(*) AS "Value"
90+
SELECT a.amname AS "Value"
8591
FROM pg_opclass o
8692
JOIN pg_am a ON a.oid = o.opcmethod
87-
WHERE a.amname = 'bm25' AND o.opcname = 'vector_cosine_ops'
93+
WHERE a.amname IN ('paradedb', 'bm25') AND o.opcname = 'vector_cosine_ops'
8894
"""
8995
)
90-
.SingleAsync();
96+
.ToListAsync();
9197

92-
return count > 0;
98+
return accessMethods.Contains("paradedb") ? "paradedb"
99+
: accessMethods.Contains("bm25") ? "bm25"
100+
: null;
93101
}

src/Internal/ParadeDbOptionsExtension.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure;
1414
#endif
1515

16-
1716
namespace ParadeDB.EntityFrameworkCore.Internal;
1817

1918
internal sealed class ParadeDbOptionsExtension : IDbContextOptionsExtension

tests/IndexingTest.cs

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -537,17 +537,18 @@ public async Task ParadeDbIndex_WithVectorFields()
537537

538538
sql.ShouldBe(
539539
"""
540-
CREATE INDEX indexing_items_idx ON indexing_items USING bm25 (id, description, embedding_l2 vector_l2_ops, embedding_cosine vector_cosine_ops, (embedding_ip) vector_ip_ops) WITH (key_field = 'id');
540+
CREATE INDEX indexing_items_idx ON indexing_items USING paradedb (id, description, embedding_l2 vector_l2_ops, embedding_cosine vector_cosine_ops, (embedding_ip) vector_ip_ops) WITH (key_field = 'id');
541541
542542
"""
543543
);
544544

545545
await using var context = DbFixture.CreateContext();
546546
await context.Database.OpenConnectionAsync();
547547

548+
var vectorAccessMethod = await GetVectorAccessMethodAsync(context);
548549
Skip.When(
549-
!await SupportsVectorInBm25Async(context),
550-
"This ParadeDB version does not support vector columns in bm25 indexes"
550+
vectorAccessMethod is null,
551+
"This ParadeDB version does not support vector columns in ParadeDB indexes (requires pg_search 0.25.0+, unreleased)"
551552
);
552553

553554
await context.Database.ExecuteSqlRawAsync(
@@ -561,23 +562,31 @@ embedding_ip vector(3)
561562
);
562563
"""
563564
);
564-
await context.Database.ExecuteSqlRawAsync(sql);
565+
await context.Database.ExecuteSqlRawAsync(
566+
sql.Replace(" USING paradedb ", $" USING {vectorAccessMethod} ")
567+
);
565568
}
566569

567-
private static async Task<bool> SupportsVectorInBm25Async(Persistence.TestDbContext context)
570+
// The vector operator classes may be declared for the `paradedb` access method, the `bm25`
571+
// access method, or neither, depending on the pg_search version
572+
internal static async Task<string?> GetVectorAccessMethodAsync(
573+
Persistence.TestDbContext context
574+
)
568575
{
569-
var count = await context
570-
.Database.SqlQueryRaw<long>(
576+
var accessMethods = await context
577+
.Database.SqlQueryRaw<string>(
571578
"""
572-
SELECT count(*) AS "Value"
579+
SELECT a.amname AS "Value"
573580
FROM pg_opclass o
574581
JOIN pg_am a ON a.oid = o.opcmethod
575-
WHERE a.amname = 'bm25' AND o.opcname = 'vector_l2_ops'
582+
WHERE a.amname IN ('paradedb', 'bm25') AND o.opcname = 'vector_l2_ops'
576583
"""
577584
)
578-
.SingleAsync();
585+
.ToListAsync();
579586

580-
return count > 0;
587+
return accessMethods.Contains("paradedb") ? "paradedb"
588+
: accessMethods.Contains("bm25") ? "bm25"
589+
: null;
581590
}
582591

583592
private Task ExecuteCreateIndexSqlAsync(Persistence.TestDbContext context, string sql) =>

tests/VectorSearchTests.cs

Lines changed: 6 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -161,21 +161,22 @@ LIMIT @p
161161
}
162162

163163
[Test]
164-
public async Task VectorSearch_WithBm25Index()
164+
public async Task VectorSearch_WithParadeDbIndex()
165165
{
166166
await using var context = await CreateContextWithVectorItemsAsync(DbFixture);
167167

168+
var vectorAccessMethod = await IndexingTest.GetVectorAccessMethodAsync(context);
168169
Skip.When(
169-
!await SupportsVectorInBm25Async(context),
170-
"This ParadeDB version does not support vector columns in bm25 indexes"
170+
vectorAccessMethod is null,
171+
"This ParadeDB version does not support vector columns in ParadeDB indexes (requires pg_search 0.25.0+, unreleased)"
171172
);
172173

173174
await context.Database.ExecuteSqlRawAsync(
174175
"""
175176
CREATE INDEX vector_items_idx ON vector_items
176-
USING bm25 (id, embedding vector_l2_ops)
177+
USING paradedb (id, embedding vector_l2_ops)
177178
WITH (key_field = 'id');
178-
"""
179+
""".Replace("USING paradedb", $"USING {vectorAccessMethod}")
179180
);
180181

181182
float[] queryVector = [1, 0, 0];
@@ -189,20 +190,4 @@ USING bm25 (id, embedding vector_l2_ops)
189190

190191
results.ShouldBe([1, 2]);
191192
}
192-
193-
private static async Task<bool> SupportsVectorInBm25Async(TestDbContext context)
194-
{
195-
var count = await context
196-
.Database.SqlQueryRaw<long>(
197-
"""
198-
SELECT count(*) AS "Value"
199-
FROM pg_opclass o
200-
JOIN pg_am a ON a.oid = o.opcmethod
201-
WHERE a.amname = 'bm25' AND o.opcname = 'vector_l2_ops'
202-
"""
203-
)
204-
.SingleAsync();
205-
206-
return count > 0;
207-
}
208193
}

0 commit comments

Comments
 (0)