Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
21 changes: 16 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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<Item>().Property(x => x.Embedding).Metadata.IsAutoLoaded = true;
```

This also applies to `HalfVector` and `SparseVector`, but not `BitArray` properties.

Insert a vector

```csharp
Expand All @@ -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);
}
```

Expand Down
4 changes: 2 additions & 2 deletions global.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"sdk": {
"version": "10.0.100",
"version": "11.0.100-rc.2.26504.105",
"rollForward": "latestMajor",
"allowPrerelease": "false"
"allowPrerelease": true
}
}
11 changes: 11 additions & 0 deletions src/Pgvector.EntityFrameworkCore/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@

<PropertyGroup>
<PackageId>Pgvector.EntityFrameworkCore</PackageId>
<Version>0.3.0</Version>
<Version>11.0.0</Version>
<Authors>ankane</Authors>
<Description>pgvector support for Entity Framework Core</Description>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageProjectUrl>https://github.com/pgvector/pgvector-dotnet</PackageProjectUrl>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageIcon>icon.png</PackageIcon>

<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net11.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
Expand All @@ -22,7 +22,7 @@
<None Include="..\..\icon.png" Pack="true" PackagePath="\" />
<None Include="build\**\*" Pack="true" PackagePath="build" />
<ProjectReference Include="..\Pgvector\Pgvector.csproj" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.1" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="11.0.0-rc.1.1" />
</ItemGroup>

</Project>
31 changes: 31 additions & 0 deletions src/Pgvector.EntityFrameworkCore/VectorConventionSetPlugin.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -16,6 +17,7 @@ public void ApplyServices(IServiceCollection services)
{
new EntityFrameworkNpgsqlServicesBuilder(services)
.TryAdd<INpgsqlDataSourceConfigurationPlugin, VectorDataSourceConfigurationPlugin>()
.TryAdd<IConventionSetPlugin, VectorConventionSetPlugin>()
.TryAdd<IMethodCallTranslatorPlugin, VectorDbFunctionsTranslatorPlugin>();

services.AddSingleton<IRelationalTypeMappingSourcePlugin, VectorTypeMappingSourcePlugin>();
Expand Down
78 changes: 77 additions & 1 deletion tests/Pgvector.CSharp.Tests/EntityFrameworkCoreTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Item>().Property(x => x.Embedding).Metadata.IsAutoLoaded = true;
modelBuilder.Entity<Item>().Property(x => x.HalfEmbedding).Metadata.IsAutoLoaded = true;
modelBuilder.Entity<Item>().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<IRelationalDatabaseCreator>();
Expand All @@ -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();
Expand Down Expand Up @@ -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<IRelationalDatabaseCreator>().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)")]
Expand Down
2 changes: 1 addition & 1 deletion tests/Pgvector.CSharp.Tests/Pgvector.CSharp.Tests.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<TargetFramework>net11.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>

Expand Down