diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b9b6a88..9e9f0b5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,6 +6,9 @@ jobs: steps: - uses: actions/checkout@v7 - uses: actions/setup-dotnet@v5 + with: + dotnet-version: 11.0.x + dotnet-quality: preview - uses: ankane/setup-postgres@v1 with: database: pgvector_dotnet_test diff --git a/README.md b/README.md index 0f3b7b2..8821ed7 100644 --- a/README.md +++ b/README.md @@ -194,7 +194,7 @@ Run dotnet add package Pgvector.EntityFrameworkCore ``` -The latest version works with Entity Framework Core 9 and 10. For Entity Framework Core 8, use version 0.2.2 and [this readme](https://github.com/pgvector/pgvector-dotnet/blob/efcore-v0.2.2/README.md#entity-framework-core). +The latest version requires .NET 11 and works with Entity Framework Core 11 (currently Npgsql.EntityFrameworkCore.PostgreSQL 11.0.0-rc.1.1). For Entity Framework Core 10, use version 10.0.0. For Entity Framework Core 9, use version 0.3.0. For Entity Framework Core 8, use version 0.2.2 and [this readme](https://github.com/pgvector/pgvector-dotnet/blob/efcore-v0.2.2/README.md#entity-framework-core). Import the library @@ -247,6 +247,20 @@ public class Item Also supports `HalfVector` and `SparseVector` +Vector properties are not automatically loaded when querying entities. They remain available for filtering, ordering, and explicit projections: + +```csharp +var embeddings = await ctx.Items.Select(x => x.Embedding).ToListAsync(); +``` + +To automatically load a vector property, configure it explicitly in `OnModelCreating`: + +```csharp +modelBuilder.Entity().Property(x => x.Embedding).Metadata.IsAutoLoaded = true; +``` + +This also applies to `HalfVector` and `SparseVector`, but not `BitArray` properties. + Insert a vector ```csharp @@ -265,10 +279,7 @@ var items = await ctx.Items foreach (Item item in items) { - if (item.Embedding != null) - { - Console.WriteLine(item.Embedding); - } + Console.WriteLine(item.Id); } ``` diff --git a/global.json b/global.json index 0b2a45c..b40b33e 100644 --- a/global.json +++ b/global.json @@ -1,7 +1,7 @@ { "sdk": { - "version": "10.0.100", + "version": "11.0.100-rc.2.26504.105", "rollForward": "latestMajor", - "allowPrerelease": "false" + "allowPrerelease": true } } diff --git a/src/Pgvector.EntityFrameworkCore/CHANGELOG.md b/src/Pgvector.EntityFrameworkCore/CHANGELOG.md index c08d9f7..f07385b 100644 --- a/src/Pgvector.EntityFrameworkCore/CHANGELOG.md +++ b/src/Pgvector.EntityFrameworkCore/CHANGELOG.md @@ -1,3 +1,14 @@ +## 11.0.0 (unreleased) + +- Updated Npgsql.EntityFrameworkCore.PostgreSQL to 11.0.0-rc.1.1 +- Dropped support for .NET < 11 +- Disabled automatic loading of `Vector`, `HalfVector`, and `SparseVector` properties by default + +## 10.0.0 (unreleased) + +- Updated Npgsql.EntityFrameworkCore.PostgreSQL to 10 +- Dropped support for .NET < 10 + ## 0.3.0 (2025-12-20) - Improved type registration diff --git a/src/Pgvector.EntityFrameworkCore/Pgvector.EntityFrameworkCore.csproj b/src/Pgvector.EntityFrameworkCore/Pgvector.EntityFrameworkCore.csproj index 34a1ec5..2bbca86 100644 --- a/src/Pgvector.EntityFrameworkCore/Pgvector.EntityFrameworkCore.csproj +++ b/src/Pgvector.EntityFrameworkCore/Pgvector.EntityFrameworkCore.csproj @@ -2,7 +2,7 @@ Pgvector.EntityFrameworkCore - 0.3.0 + 11.0.0 ankane pgvector support for Entity Framework Core MIT @@ -10,7 +10,7 @@ README.md icon.png - net8.0 + net11.0 enable enable latest @@ -22,7 +22,7 @@ - + diff --git a/src/Pgvector.EntityFrameworkCore/VectorConventionSetPlugin.cs b/src/Pgvector.EntityFrameworkCore/VectorConventionSetPlugin.cs new file mode 100644 index 0000000..4da8f78 --- /dev/null +++ b/src/Pgvector.EntityFrameworkCore/VectorConventionSetPlugin.cs @@ -0,0 +1,31 @@ +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Metadata.Conventions; +using Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure; + +namespace Pgvector.EntityFrameworkCore; + +public class VectorConventionSetPlugin : IConventionSetPlugin +{ + private readonly ProviderConventionSetBuilderDependencies _dependencies; + + public VectorConventionSetPlugin(ProviderConventionSetBuilderDependencies dependencies) + { + _dependencies = dependencies; + } + + public virtual ConventionSet ModifyConventions(ConventionSet conventionSet) + { + conventionSet.Add(new VectorAutoLoadConvention(_dependencies)); + return conventionSet; + } + + private class VectorAutoLoadConvention : AutoLoadConvention + { + public VectorAutoLoadConvention(ProviderConventionSetBuilderDependencies dependencies) : base(dependencies) { } + + protected override bool ShouldBeAutoLoaded(IConventionProperty property) + => property.ClrType != typeof(Vector) + && property.ClrType != typeof(HalfVector) + && property.ClrType != typeof(SparseVector); + } +} diff --git a/src/Pgvector.EntityFrameworkCore/VectorDbContextOptionsExtension.cs b/src/Pgvector.EntityFrameworkCore/VectorDbContextOptionsExtension.cs index 5da3dca..5a72904 100644 --- a/src/Pgvector.EntityFrameworkCore/VectorDbContextOptionsExtension.cs +++ b/src/Pgvector.EntityFrameworkCore/VectorDbContextOptionsExtension.cs @@ -1,4 +1,5 @@ using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure; using Microsoft.EntityFrameworkCore.Query; using Microsoft.EntityFrameworkCore.Storage; using Microsoft.Extensions.DependencyInjection; @@ -16,6 +17,7 @@ public void ApplyServices(IServiceCollection services) { new EntityFrameworkNpgsqlServicesBuilder(services) .TryAdd() + .TryAdd() .TryAdd(); services.AddSingleton(); diff --git a/src/Pgvector.EntityFrameworkCore/build/net8.0/Pgvector.EntityFrameworkCore.targets b/src/Pgvector.EntityFrameworkCore/build/net11.0/Pgvector.EntityFrameworkCore.targets similarity index 100% rename from src/Pgvector.EntityFrameworkCore/build/net8.0/Pgvector.EntityFrameworkCore.targets rename to src/Pgvector.EntityFrameworkCore/build/net11.0/Pgvector.EntityFrameworkCore.targets diff --git a/tests/Pgvector.CSharp.Tests/EntityFrameworkCoreTests.cs b/tests/Pgvector.CSharp.Tests/EntityFrameworkCoreTests.cs index a896f6b..96b16f9 100644 --- a/tests/Pgvector.CSharp.Tests/EntityFrameworkCoreTests.cs +++ b/tests/Pgvector.CSharp.Tests/EntityFrameworkCoreTests.cs @@ -50,10 +50,21 @@ public class Item public class EntityFrameworkCoreTests { + private class AutoLoadedItemContext : ItemContext + { + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity().Property(x => x.Embedding).Metadata.IsAutoLoaded = true; + modelBuilder.Entity().Property(x => x.HalfEmbedding).Metadata.IsAutoLoaded = true; + modelBuilder.Entity().Property(x => x.SparseEmbedding).Metadata.IsAutoLoaded = true; + } + } + [Fact] public async Task Main() { - await using var ctx = new ItemContext(); + await using var ctx = new AutoLoadedItemContext(); ctx.Database.ExecuteSql($"DROP TABLE IF EXISTS efcore_items"); var databaseCreator = ctx.GetService(); @@ -63,6 +74,7 @@ public async Task Main() ctx.Items.Add(new Item { Embedding = new Vector(new float[] { 2, 2, 2 }), HalfEmbedding = new HalfVector(new Half[] { (Half)2, (Half)2, (Half)2 }), BinaryEmbedding = new BitArray(new bool[] { true, false, true }), SparseEmbedding = new SparseVector(new float[] { 2, 2, 2 }) }); ctx.Items.Add(new Item { Embedding = new Vector(new float[] { 1, 1, 2 }), HalfEmbedding = new HalfVector(new Half[] { (Half)1, (Half)1, (Half)2 }), BinaryEmbedding = new BitArray(new bool[] { true, true, true }), SparseEmbedding = new SparseVector(new float[] { 1, 1, 2 }) }); ctx.SaveChanges(); + ctx.ChangeTracker.Clear(); var embedding = new Vector(new float[] { 1, 1, 1 }); var items = await ctx.Items.FromSql($"SELECT * FROM efcore_items ORDER BY embedding <-> {embedding} LIMIT 5").ToListAsync(); @@ -142,6 +154,70 @@ public async Task Main() Assert.Equal(new double[] { 0, 1, Math.Sqrt(3) }, neighbors.Select(v => v.Distance).ToArray()); } + [Fact] + public async Task VectorPropertiesAreNotAutoLoaded() + { + await using var ctx = new ItemContext(); + var entityType = ctx.Model.FindEntityType(typeof(Item))!; + Assert.False(entityType.FindProperty(nameof(Item.Embedding))!.IsAutoLoaded); + Assert.False(entityType.FindProperty(nameof(Item.HalfEmbedding))!.IsAutoLoaded); + Assert.False(entityType.FindProperty(nameof(Item.SparseEmbedding))!.IsAutoLoaded); + Assert.True(entityType.FindProperty(nameof(Item.Id))!.IsAutoLoaded); + Assert.True(entityType.FindProperty(nameof(Item.BinaryEmbedding))!.IsAutoLoaded); + + await ctx.Database.ExecuteSqlAsync($"DROP TABLE IF EXISTS efcore_items"); + ctx.GetService().CreateTables(); + + var embedding = new Vector(new float[] { 1, 2, 3 }); + var halfEmbedding = new HalfVector(new Half[] { (Half)1, (Half)2, (Half)3 }); + var sparseEmbedding = new SparseVector(new float[] { 1, 2, 3 }); + var binaryEmbedding = new BitArray(new bool[] { true, false, true }); + ctx.Items.Add(new Item + { + Embedding = embedding, + HalfEmbedding = halfEmbedding, + SparseEmbedding = sparseEmbedding, + BinaryEmbedding = binaryEmbedding + }); + await ctx.SaveChangesAsync(); + ctx.ChangeTracker.Clear(); + + var sql = ctx.Items.ToQueryString(); + Assert.DoesNotContain(".embedding", sql); + Assert.DoesNotContain(".half_embedding", sql); + Assert.DoesNotContain(".sparse_embedding", sql); + Assert.Contains(".binary_embedding", sql); + + var untracked = await ctx.Items.AsNoTracking().SingleAsync(); + Assert.Null(untracked.Embedding); + Assert.Null(untracked.HalfEmbedding); + Assert.Null(untracked.SparseEmbedding); + Assert.Equal(binaryEmbedding, untracked.BinaryEmbedding); + + var neighbor = await ctx.Items + .Where(x => x.Embedding!.L2Distance(embedding) < 1) + .OrderBy(x => x.Embedding!.L2Distance(embedding)) + .Select(x => new { Entity = x, Distance = x.Embedding!.L2Distance(embedding) }) + .SingleAsync(); + Assert.Equal(untracked.Id, neighbor.Entity.Id); + Assert.Equal(0, neighbor.Distance); + Assert.Null(neighbor.Entity.Embedding); + Assert.Null(neighbor.Entity.HalfEmbedding); + Assert.Null(neighbor.Entity.SparseEmbedding); + Assert.Equal(binaryEmbedding, neighbor.Entity.BinaryEmbedding); + + neighbor.Entity.BinaryEmbedding = new BitArray(new bool[] { false, true, false }); + await ctx.SaveChangesAsync(); + + var projected = await ctx.Items + .Select(x => new { x.Embedding, x.HalfEmbedding, x.SparseEmbedding, x.BinaryEmbedding }) + .SingleAsync(); + Assert.Equal(embedding, projected.Embedding); + Assert.Equal(halfEmbedding, projected.HalfEmbedding); + Assert.Equal(sparseEmbedding, projected.SparseEmbedding); + Assert.Equal(neighbor.Entity.BinaryEmbedding, projected.BinaryEmbedding); + } + [Theory] [InlineData(typeof(Vector), null, "vector")] [InlineData(typeof(Vector), 3, "vector(3)")] diff --git a/tests/Pgvector.CSharp.Tests/Pgvector.CSharp.Tests.csproj b/tests/Pgvector.CSharp.Tests/Pgvector.CSharp.Tests.csproj index f25be28..522ee1a 100644 --- a/tests/Pgvector.CSharp.Tests/Pgvector.CSharp.Tests.csproj +++ b/tests/Pgvector.CSharp.Tests/Pgvector.CSharp.Tests.csproj @@ -1,7 +1,7 @@ - net10.0 + net11.0 enable enable