├── .github └── workflows │ └── build.yml ├── .gitignore ├── LICENSE.txt ├── Pgvector.sln ├── README.md ├── examples ├── Citus │ ├── Citus.csproj │ └── Program.cs ├── Cohere │ ├── Cohere.csproj │ └── Program.cs ├── Disco │ ├── Disco.csproj │ └── Program.cs ├── Hybrid │ ├── Hybrid.csproj │ └── Program.cs ├── Loading │ ├── Loading.csproj │ └── Program.cs ├── OpenAI │ ├── OpenAI.csproj │ └── Program.cs ├── Sparse │ ├── Program.cs │ └── Sparse.csproj └── TopicModeling │ ├── Program.cs │ └── TopicModeling.csproj ├── global.json ├── icon.png ├── src ├── Pgvector.Dapper │ ├── CHANGELOG.md │ ├── HalfvecTypeHandler.cs │ ├── Pgvector.Dapper.csproj │ ├── SparsevecTypeHandler.cs │ └── VectorTypeHandler.cs ├── Pgvector.EntityFrameworkCore │ ├── CHANGELOG.md │ ├── Pgvector.EntityFrameworkCore.csproj │ ├── VectorCodeGeneratorPlugin.cs │ ├── VectorDbContextOptionsBuilderExtensions.cs │ ├── VectorDbContextOptionsExtension.cs │ ├── VectorDbFunctionsExtensions.cs │ ├── VectorDbFunctionsTranslatorPlugin.cs │ ├── VectorDesignTimeServices.cs │ ├── VectorTypeMapping.cs │ ├── VectorTypeMappingSourcePlugin.cs │ └── build │ │ └── net8.0 │ │ └── Pgvector.EntityFrameworkCore.targets └── Pgvector │ ├── CHANGELOG.md │ ├── HalfVector.cs │ ├── Npgsql │ ├── HalfvecConverter.cs │ ├── SparsevecConverter.cs │ ├── VectorConverter.cs │ ├── VectorExtensions.cs │ └── VectorTypeInfoResolverFactory.cs │ ├── Pgvector.csproj │ ├── SparseVector.cs │ └── Vector.cs └── tests ├── Pgvector.CSharp.Tests ├── DapperTests.cs ├── EntityFrameworkCoreTests.cs ├── HalfVectorTests.cs ├── NpgsqlTests.cs ├── Pgvector.CSharp.Tests.csproj ├── SparseVectorTests.cs └── VectorTests.cs ├── Pgvector.FSharp.Tests ├── NpgsqlFSharpTests.fs ├── Pgvector.FSharp.Tests.fsproj └── Program.fs └── Pgvector.VisualBasic.Tests ├── NpgsqlTests.vb └── Pgvector.VisualBasic.Tests.vbproj /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: [push, pull_request] 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v4 8 | - uses: actions/setup-dotnet@v4 9 | - uses: ankane/setup-postgres@v1 10 | with: 11 | database: pgvector_dotnet_test 12 | dev-files: true 13 | - run: | 14 | cd /tmp 15 | git clone --branch v0.8.0 https://github.com/pgvector/pgvector.git 16 | cd pgvector 17 | make 18 | sudo make install 19 | - run: psql -d pgvector_dotnet_test -c "CREATE EXTENSION vector" 20 | - run: dotnet test 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | obj/ 3 | TestResults/ 4 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2023-2025 Andrew Kane 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Pgvector.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.31903.59 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{B76AB67C-094F-433B-AF25-807B50D2888E}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Pgvector", "src\Pgvector\Pgvector.csproj", "{3C580CE2-B935-4656-BCC9-4FB4035089E4}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{4C5546A5-6119-4D42-A65B-2327280E1881}" 11 | EndProject 12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Pgvector.CSharp.Tests", "tests\Pgvector.CSharp.Tests\Pgvector.CSharp.Tests.csproj", "{E9267E87-A783-414F-8916-49D5A5F55634}" 13 | EndProject 14 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Pgvector.Dapper", "src\Pgvector.Dapper\Pgvector.Dapper.csproj", "{FF6D3EDD-C96A-4AD5-A298-0A7E53BB3A07}" 15 | EndProject 16 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Pgvector.EntityFrameworkCore", "src\Pgvector.EntityFrameworkCore\Pgvector.EntityFrameworkCore.csproj", "{4493B7D8-112B-4867-9373-C08B6F996875}" 17 | EndProject 18 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Pgvector.FSharp.Tests", "tests\Pgvector.FSharp.Tests\Pgvector.FSharp.Tests.fsproj", "{2D51D6AE-2355-4BAE-8FDC-7001EBAE78BD}" 19 | EndProject 20 | Project("{F184B08F-C81C-45F6-A57F-5ABD9991F28F}") = "Pgvector.VisualBasic.Tests", "tests\Pgvector.VisualBasic.Tests\Pgvector.VisualBasic.Tests.vbproj", "{1C52EAE5-7888-48E9-86E2-FADA289CE658}" 21 | EndProject 22 | Global 23 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 24 | Debug|Any CPU = Debug|Any CPU 25 | Release|Any CPU = Release|Any CPU 26 | EndGlobalSection 27 | GlobalSection(SolutionProperties) = preSolution 28 | HideSolutionNode = FALSE 29 | EndGlobalSection 30 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 31 | {3C580CE2-B935-4656-BCC9-4FB4035089E4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 32 | {3C580CE2-B935-4656-BCC9-4FB4035089E4}.Debug|Any CPU.Build.0 = Debug|Any CPU 33 | {3C580CE2-B935-4656-BCC9-4FB4035089E4}.Release|Any CPU.ActiveCfg = Release|Any CPU 34 | {3C580CE2-B935-4656-BCC9-4FB4035089E4}.Release|Any CPU.Build.0 = Release|Any CPU 35 | {E9267E87-A783-414F-8916-49D5A5F55634}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 36 | {E9267E87-A783-414F-8916-49D5A5F55634}.Debug|Any CPU.Build.0 = Debug|Any CPU 37 | {E9267E87-A783-414F-8916-49D5A5F55634}.Release|Any CPU.ActiveCfg = Release|Any CPU 38 | {E9267E87-A783-414F-8916-49D5A5F55634}.Release|Any CPU.Build.0 = Release|Any CPU 39 | {FF6D3EDD-C96A-4AD5-A298-0A7E53BB3A07}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 40 | {FF6D3EDD-C96A-4AD5-A298-0A7E53BB3A07}.Debug|Any CPU.Build.0 = Debug|Any CPU 41 | {FF6D3EDD-C96A-4AD5-A298-0A7E53BB3A07}.Release|Any CPU.ActiveCfg = Release|Any CPU 42 | {FF6D3EDD-C96A-4AD5-A298-0A7E53BB3A07}.Release|Any CPU.Build.0 = Release|Any CPU 43 | {4493B7D8-112B-4867-9373-C08B6F996875}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 44 | {4493B7D8-112B-4867-9373-C08B6F996875}.Debug|Any CPU.Build.0 = Debug|Any CPU 45 | {4493B7D8-112B-4867-9373-C08B6F996875}.Release|Any CPU.ActiveCfg = Release|Any CPU 46 | {4493B7D8-112B-4867-9373-C08B6F996875}.Release|Any CPU.Build.0 = Release|Any CPU 47 | {2D51D6AE-2355-4BAE-8FDC-7001EBAE78BD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 48 | {2D51D6AE-2355-4BAE-8FDC-7001EBAE78BD}.Debug|Any CPU.Build.0 = Debug|Any CPU 49 | {2D51D6AE-2355-4BAE-8FDC-7001EBAE78BD}.Release|Any CPU.ActiveCfg = Release|Any CPU 50 | {2D51D6AE-2355-4BAE-8FDC-7001EBAE78BD}.Release|Any CPU.Build.0 = Release|Any CPU 51 | {1C52EAE5-7888-48E9-86E2-FADA289CE658}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 52 | {1C52EAE5-7888-48E9-86E2-FADA289CE658}.Debug|Any CPU.Build.0 = Debug|Any CPU 53 | {1C52EAE5-7888-48E9-86E2-FADA289CE658}.Release|Any CPU.ActiveCfg = Release|Any CPU 54 | {1C52EAE5-7888-48E9-86E2-FADA289CE658}.Release|Any CPU.Build.0 = Release|Any CPU 55 | EndGlobalSection 56 | GlobalSection(NestedProjects) = preSolution 57 | {3C580CE2-B935-4656-BCC9-4FB4035089E4} = {B76AB67C-094F-433B-AF25-807B50D2888E} 58 | {E9267E87-A783-414F-8916-49D5A5F55634} = {4C5546A5-6119-4D42-A65B-2327280E1881} 59 | {FF6D3EDD-C96A-4AD5-A298-0A7E53BB3A07} = {B76AB67C-094F-433B-AF25-807B50D2888E} 60 | {4493B7D8-112B-4867-9373-C08B6F996875} = {B76AB67C-094F-433B-AF25-807B50D2888E} 61 | {2D51D6AE-2355-4BAE-8FDC-7001EBAE78BD} = {4C5546A5-6119-4D42-A65B-2327280E1881} 62 | {1C52EAE5-7888-48E9-86E2-FADA289CE658} = {4C5546A5-6119-4D42-A65B-2327280E1881} 63 | EndGlobalSection 64 | EndGlobal 65 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pgvector-dotnet 2 | 3 | [pgvector](https://github.com/pgvector/pgvector) support for .NET (C#, F#, and Visual Basic) 4 | 5 | Supports [Npgsql](https://github.com/npgsql/npgsql), [Dapper](https://github.com/DapperLib/Dapper), [Entity Framework Core](https://github.com/dotnet/efcore), and [Npgsql.FSharp](https://github.com/Zaid-Ajaj/Npgsql.FSharp) 6 | 7 | [![Build Status](https://github.com/pgvector/pgvector-dotnet/actions/workflows/build.yml/badge.svg)](https://github.com/pgvector/pgvector-dotnet/actions) 8 | 9 | ## Getting Started 10 | 11 | Follow the instructions for your database library: 12 | 13 | - C# - [Npgsql](#npgsql-c), [Dapper](#dapper), [Entity Framework Core](#entity-framework-core) 14 | - F# - [Npgsql.FSharp](#npgsqlfsharp) 15 | - Visual Basic - [Npgsql](#npgsql-visual-basic) 16 | 17 | Or check out some examples: 18 | 19 | - [Embeddings](https://github.com/pgvector/pgvector-dotnet/blob/master/examples/OpenAI/Program.cs) with OpenAI 20 | - [Binary embeddings](https://github.com/pgvector/pgvector-dotnet/blob/master/examples/Cohere/Program.cs) with Cohere 21 | - [Hybrid search](https://github.com/pgvector/pgvector-dotnet/blob/master/examples/Hybrid/Program.cs) with Ollama (Reciprocal Rank Fusion) 22 | - [Sparse search](https://github.com/pgvector/pgvector-dotnet/blob/master/examples/Sparse/Program.cs) with Text Embeddings Inference 23 | - [Recommendations](https://github.com/pgvector/pgvector-dotnet/blob/master/examples/Disco/Program.cs) with Disco 24 | - [Topic modeling](https://github.com/pgvector/pgvector-dotnet/blob/master/examples/TopicModeling/Program.cs) with ML.NET 25 | - [Horizontal scaling](https://github.com/pgvector/pgvector-dotnet/blob/master/examples/Citus/Program.cs) with Citus 26 | - [Bulk loading](https://github.com/pgvector/pgvector-dotnet/blob/master/examples/Loading/Program.cs) with `COPY` 27 | 28 | ## Npgsql (C#) 29 | 30 | Run 31 | 32 | ```sh 33 | dotnet add package Pgvector 34 | ``` 35 | 36 | Create a connection 37 | 38 | ```csharp 39 | var dataSourceBuilder = new NpgsqlDataSourceBuilder(connString); 40 | dataSourceBuilder.UseVector(); 41 | await using var dataSource = dataSourceBuilder.Build(); 42 | 43 | var conn = dataSource.OpenConnection(); 44 | ``` 45 | 46 | Enable the extension 47 | 48 | ```csharp 49 | await using (var cmd = new NpgsqlCommand("CREATE EXTENSION IF NOT EXISTS vector", conn)) 50 | { 51 | await cmd.ExecuteNonQueryAsync(); 52 | } 53 | 54 | conn.ReloadTypes(); 55 | ``` 56 | 57 | Create a table 58 | 59 | ```csharp 60 | await using (var cmd = new NpgsqlCommand("CREATE TABLE items (id serial PRIMARY KEY, embedding vector(3))", conn)) 61 | { 62 | await cmd.ExecuteNonQueryAsync(); 63 | } 64 | ``` 65 | 66 | Insert a vector 67 | 68 | ```csharp 69 | await using (var cmd = new NpgsqlCommand("INSERT INTO items (embedding) VALUES ($1)", conn)) 70 | { 71 | var embedding = new Vector(new float[] { 1, 1, 1 }); 72 | cmd.Parameters.AddWithValue(embedding); 73 | await cmd.ExecuteNonQueryAsync(); 74 | } 75 | ``` 76 | 77 | Get the nearest neighbors 78 | 79 | ```csharp 80 | await using (var cmd = new NpgsqlCommand("SELECT * FROM items ORDER BY embedding <-> $1 LIMIT 5", conn)) 81 | { 82 | var embedding = new Vector(new float[] { 1, 1, 1 }); 83 | cmd.Parameters.AddWithValue(embedding); 84 | 85 | await using (var reader = await cmd.ExecuteReaderAsync()) 86 | { 87 | while (await reader.ReadAsync()) 88 | { 89 | Console.WriteLine(reader.GetValue(0)); 90 | } 91 | } 92 | } 93 | ``` 94 | 95 | Add an approximate index 96 | 97 | ```csharp 98 | await using (var cmd = new NpgsqlCommand("CREATE INDEX ON items USING hnsw (embedding vector_l2_ops)", conn)) 99 | { 100 | await cmd.ExecuteNonQueryAsync(); 101 | } 102 | ``` 103 | 104 | Use `vector_ip_ops` for inner product and `vector_cosine_ops` for cosine distance 105 | 106 | See a [full example](https://github.com/pgvector/pgvector-dotnet/blob/master/tests/Pgvector.CSharp.Tests/NpgsqlTests.cs) 107 | 108 | ## Dapper 109 | 110 | Run 111 | 112 | ```sh 113 | dotnet add package Pgvector.Dapper 114 | ``` 115 | 116 | Import the library 117 | 118 | ```csharp 119 | using Pgvector.Dapper; 120 | ``` 121 | 122 | Create a connection 123 | 124 | ```csharp 125 | SqlMapper.AddTypeHandler(new VectorTypeHandler()); 126 | 127 | var dataSourceBuilder = new NpgsqlDataSourceBuilder(connString); 128 | dataSourceBuilder.UseVector(); 129 | await using var dataSource = dataSourceBuilder.Build(); 130 | 131 | var conn = dataSource.OpenConnection(); 132 | ``` 133 | 134 | Enable the extension 135 | 136 | ```csharp 137 | conn.Execute("CREATE EXTENSION IF NOT EXISTS vector"); 138 | conn.ReloadTypes(); 139 | ``` 140 | 141 | Define a class 142 | 143 | ```csharp 144 | public class Item 145 | { 146 | public int Id { get; set; } 147 | public Vector? Embedding { get; set; } 148 | } 149 | ``` 150 | 151 | Also supports `HalfVector` and `SparseVector` 152 | 153 | Create a table 154 | 155 | ```csharp 156 | conn.Execute("CREATE TABLE items (id serial PRIMARY KEY, embedding vector(3))"); 157 | ``` 158 | 159 | Insert a vector 160 | 161 | ```csharp 162 | var embedding = new Vector(new float[] { 1, 1, 1 }); 163 | conn.Execute(@"INSERT INTO items (embedding) VALUES (@embedding)", new { embedding }); 164 | ``` 165 | 166 | Get the nearest neighbors 167 | 168 | ```csharp 169 | var embedding = new Vector(new float[] { 1, 1, 1 }); 170 | var items = conn.Query("SELECT * FROM items ORDER BY embedding <-> @embedding LIMIT 5", new { embedding }); 171 | foreach (Item item in items) 172 | { 173 | Console.WriteLine(item.Embedding); 174 | } 175 | ``` 176 | 177 | Add an approximate index 178 | 179 | ```csharp 180 | conn.Execute("CREATE INDEX ON items USING hnsw (embedding vector_l2_ops)"); 181 | // or 182 | conn.Execute("CREATE INDEX ON items USING ivfflat (embedding vector_l2_ops) WITH (lists = 100)"); 183 | ``` 184 | 185 | Use `vector_ip_ops` for inner product and `vector_cosine_ops` for cosine distance 186 | 187 | See a [full example](https://github.com/pgvector/pgvector-dotnet/blob/master/tests/Pgvector.CSharp.Tests/DapperTests.cs) 188 | 189 | ## Entity Framework Core 190 | 191 | Run 192 | 193 | ```sh 194 | dotnet add package Pgvector.EntityFrameworkCore 195 | ``` 196 | 197 | The latest version works with .NET 8 and 9. For .NET 6 and 7, use version 0.1.2 and [this readme](https://github.com/pgvector/pgvector-dotnet/blob/efcore-v0.1.2/README.md#entity-framework-core). 198 | 199 | Import the library 200 | 201 | ```csharp 202 | using Pgvector.EntityFrameworkCore; 203 | ``` 204 | 205 | Enable the extension 206 | 207 | ```csharp 208 | protected override void OnModelCreating(ModelBuilder modelBuilder) 209 | { 210 | modelBuilder.HasPostgresExtension("vector"); 211 | } 212 | ``` 213 | 214 | Configure the connection 215 | 216 | ```csharp 217 | protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) 218 | { 219 | optionsBuilder.UseNpgsql("connString", o => o.UseVector()); 220 | } 221 | ``` 222 | 223 | Define a model 224 | 225 | ```csharp 226 | public class Item 227 | { 228 | public int Id { get; set; } 229 | 230 | [Column(TypeName = "vector(3)")] 231 | public Vector? Embedding { get; set; } 232 | } 233 | ``` 234 | 235 | Also supports `HalfVector` and `SparseVector` 236 | 237 | Insert a vector 238 | 239 | ```csharp 240 | ctx.Items.Add(new Item { Embedding = new Vector(new float[] { 1, 1, 1 }) }); 241 | ctx.SaveChanges(); 242 | ``` 243 | 244 | Get the nearest neighbors 245 | 246 | ```csharp 247 | var embedding = new Vector(new float[] { 1, 1, 1 }); 248 | var items = await ctx.Items 249 | .OrderBy(x => x.Embedding!.L2Distance(embedding)) 250 | .Take(5) 251 | .ToListAsync(); 252 | 253 | foreach (Item item in items) 254 | { 255 | if (item.Embedding != null) 256 | { 257 | Console.WriteLine(item.Embedding); 258 | } 259 | } 260 | ``` 261 | 262 | Also supports `MaxInnerProduct`, `CosineDistance`, `L1Distance`, `HammingDistance`, and `JaccardDistance` 263 | 264 | Get the distance 265 | 266 | ```csharp 267 | var items = await ctx.Items 268 | .Select(x => new { Entity = x, Distance = x.Embedding!.L2Distance(embedding) }) 269 | .ToListAsync(); 270 | ``` 271 | 272 | Get items within a certain distance 273 | 274 | ```csharp 275 | var items = await ctx.Items 276 | .Where(x => x.Embedding!.L2Distance(embedding) < 5) 277 | .ToListAsync(); 278 | ``` 279 | 280 | Add an approximate index 281 | 282 | ```csharp 283 | protected override void OnModelCreating(ModelBuilder modelBuilder) 284 | { 285 | modelBuilder.Entity() 286 | .HasIndex(i => i.Embedding) 287 | .HasMethod("hnsw") 288 | .HasOperators("vector_l2_ops") 289 | .HasStorageParameter("m", 16) 290 | .HasStorageParameter("ef_construction", 64); 291 | // or 292 | modelBuilder.Entity() 293 | .HasIndex(i => i.Embedding) 294 | .HasMethod("ivfflat") 295 | .HasOperators("vector_l2_ops") 296 | .HasStorageParameter("lists", 100); 297 | } 298 | ``` 299 | 300 | Use `vector_ip_ops` for inner product and `vector_cosine_ops` for cosine distance 301 | 302 | See a [full example](https://github.com/pgvector/pgvector-dotnet/blob/master/tests/Pgvector.CSharp.Tests/EntityFrameworkCoreTests.cs) 303 | 304 | ## Npgsql.FSharp 305 | 306 | Run 307 | 308 | ```sh 309 | dotnet add package Pgvector 310 | ``` 311 | 312 | Import the library 313 | 314 | ```fsharp 315 | open Pgvector 316 | ``` 317 | 318 | Create a connection 319 | 320 | ```fsharp 321 | let dataSourceBuilder = new NpgsqlDataSourceBuilder(connString) 322 | dataSourceBuilder.UseVector() 323 | use dataSource = dataSourceBuilder.Build() 324 | ``` 325 | 326 | Enable the extension 327 | 328 | ```fsharp 329 | dataSource 330 | |> Sql.fromDataSource 331 | |> Sql.query "CREATE EXTENSION IF NOT EXISTS vector" 332 | |> Sql.executeNonQuery 333 | ``` 334 | 335 | Create a table 336 | 337 | ```fsharp 338 | dataSource 339 | |> Sql.fromDataSource 340 | |> Sql.query "CREATE TABLE items (id serial PRIMARY KEY, embedding vector(3))" 341 | |> Sql.executeNonQuery 342 | ``` 343 | 344 | Insert a vector 345 | 346 | ```fsharp 347 | let embedding = new Vector([| 1f; 1f; 1f |]) 348 | let parameter = new NpgsqlParameter("", embedding) 349 | 350 | dataSource 351 | |> Sql.fromDataSource 352 | |> Sql.query "INSERT INTO items (embedding) VALUES (@embedding)" 353 | |> Sql.parameters [ "embedding", Sql.parameter parameter ] 354 | |> Sql.executeNonQuery 355 | ``` 356 | 357 | Get the nearest neighbors 358 | 359 | ```fsharp 360 | type Item = { 361 | Id: int 362 | Embedding: Vector 363 | } 364 | 365 | dataSource 366 | |> Sql.fromDataSource 367 | |> Sql.query "SELECT * FROM items ORDER BY embedding <-> @embedding LIMIT 5" 368 | |> Sql.parameters [ "embedding", Sql.parameter parameter ] 369 | |> Sql.execute (fun read -> 370 | { 371 | Id = read.int "id" 372 | Embedding = read.fieldValue "embedding" 373 | }) 374 | |> printfn "%A" 375 | ``` 376 | 377 | Add an approximate index 378 | 379 | ```fsharp 380 | dataSource 381 | |> Sql.fromDataSource 382 | |> Sql.query "CREATE INDEX ON items USING hnsw (embedding vector_l2_ops)" 383 | |> Sql.executeNonQuery 384 | ``` 385 | 386 | Use `vector_ip_ops` for inner product and `vector_cosine_ops` for cosine distance 387 | 388 | See a [full example](https://github.com/pgvector/pgvector-dotnet/blob/master/tests/Pgvector.FSharp.Tests/NpgsqlFSharpTests.fs) 389 | 390 | ## Npgsql (Visual Basic) 391 | 392 | Run 393 | 394 | ```sh 395 | dotnet add package Pgvector 396 | ``` 397 | 398 | Create a connection 399 | 400 | ```vb 401 | Dim dataSourceBuilder As New NpgsqlDataSourceBuilder(connString) 402 | dataSourceBuilder.UseVector() 403 | Dim dataSource = dataSourceBuilder.Build() 404 | 405 | Dim conn = dataSource.OpenConnection() 406 | ``` 407 | 408 | Enable the extension 409 | 410 | ```vb 411 | Using cmd As New NpgsqlCommand("CREATE EXTENSION IF NOT EXISTS vector", conn) 412 | cmd.ExecuteNonQuery() 413 | End Using 414 | 415 | conn.ReloadTypes() 416 | ``` 417 | 418 | Create a table 419 | 420 | ```vb 421 | Using cmd As New NpgsqlCommand("CREATE TABLE items (id serial PRIMARY KEY, embedding vector(3))", conn) 422 | cmd.ExecuteNonQuery() 423 | End Using 424 | ``` 425 | 426 | Insert a vector 427 | 428 | ```vb 429 | Using cmd As New NpgsqlCommand("INSERT INTO items (embedding) VALUES ($1)", conn) 430 | Dim embedding As New Vector(New Single() {1, 1, 1}) 431 | cmd.Parameters.AddWithValue(embedding) 432 | cmd.ExecuteNonQuery() 433 | End Using 434 | ``` 435 | 436 | Get the nearest neighbors 437 | 438 | ```vb 439 | Using cmd As New NpgsqlCommand("SELECT * FROM items ORDER BY embedding <-> $1 LIMIT 5", conn) 440 | Dim embedding As New Vector(New Single() {1, 1, 1}) 441 | cmd.Parameters.AddWithValue(embedding) 442 | 443 | Using reader As NpgsqlDataReader = cmd.ExecuteReader() 444 | While reader.Read() 445 | Console.WriteLine(reader.GetValue(0)) 446 | End While 447 | End Using 448 | End Using 449 | ``` 450 | 451 | Add an approximate index 452 | 453 | ```vb 454 | Using cmd As New NpgsqlCommand("CREATE INDEX ON items USING hnsw (embedding vector_l2_ops)", conn) 455 | cmd.ExecuteNonQuery() 456 | End Using 457 | ``` 458 | 459 | Use `vector_ip_ops` for inner product and `vector_cosine_ops` for cosine distance 460 | 461 | See a [full example](https://github.com/pgvector/pgvector-dotnet/blob/master/tests/Pgvector.VisualBasic.Tests/NpgsqlTests.vb) 462 | 463 | ## Reference 464 | 465 | ### Vectors 466 | 467 | Create a vector from an array 468 | 469 | ```csharp 470 | var vec = new Vector(new float[] { 1, 2, 3 }); 471 | ``` 472 | 473 | Get an array 474 | 475 | ```csharp 476 | var arr = vec.ToArray(); 477 | ``` 478 | 479 | ### Half Vectors 480 | 481 | Create a half vector from an array 482 | 483 | ```csharp 484 | var vec = new HalfVector(new Half[] { (Half)1, (Half)2, (Half)3 }); 485 | ``` 486 | 487 | Get an array 488 | 489 | ```csharp 490 | var arr = vec.ToArray(); 491 | ``` 492 | 493 | ### Sparse Vectors 494 | 495 | Create a sparse vector from an array 496 | 497 | ```csharp 498 | var vec = new SparseVector(new float[] { 1, 0, 2, 0, 3, 0 }); 499 | ``` 500 | 501 | Or a dictionary of non-zero elements 502 | 503 | ```csharp 504 | var dictionary = new Dictionary(); 505 | dictionary.Add(0, 1); 506 | dictionary.Add(2, 2); 507 | dictionary.Add(4, 3); 508 | var vec = new SparseVector(dictionary, 6); 509 | ``` 510 | 511 | Note: Indices start at 0 512 | 513 | Get the number of dimensions 514 | 515 | ```csharp 516 | var dim = vec.Dimensions; 517 | ``` 518 | 519 | Get the indices of non-zero elements 520 | 521 | ```csharp 522 | var indices = vec.Indices; 523 | ``` 524 | 525 | Get the values of non-zero elements 526 | 527 | ```csharp 528 | var values = vec.Values; 529 | ``` 530 | 531 | Get an array 532 | 533 | ```csharp 534 | var arr = vec.ToArray(); 535 | ``` 536 | 537 | ## History 538 | 539 | - [Pgvector](https://github.com/pgvector/pgvector-dotnet/blob/master/src/Pgvector/CHANGELOG.md) 540 | - [Pgvector.Dapper](https://github.com/pgvector/pgvector-dotnet/blob/master/src/Pgvector.Dapper/CHANGELOG.md) 541 | - [Pgvector.EntityFrameworkCore](https://github.com/pgvector/pgvector-dotnet/blob/master/src/Pgvector.EntityFrameworkCore/CHANGELOG.md) 542 | 543 | ## Contributing 544 | 545 | Everyone is encouraged to help improve this project. Here are a few ways you can help: 546 | 547 | - [Report bugs](https://github.com/pgvector/pgvector-dotnet/issues) 548 | - Fix bugs and [submit pull requests](https://github.com/pgvector/pgvector-dotnet/pulls) 549 | - Write, clarify, or fix documentation 550 | - Suggest or add new features 551 | 552 | To get started with development: 553 | 554 | ```sh 555 | git clone https://github.com/pgvector/pgvector-dotnet.git 556 | cd pgvector-dotnet 557 | createdb pgvector_dotnet_test 558 | dotnet test 559 | ``` 560 | 561 | To run an example: 562 | 563 | ```sh 564 | cd examples/Loading 565 | createdb pgvector_example 566 | dotnet run 567 | ``` 568 | -------------------------------------------------------------------------------- /examples/Citus/Citus.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net9.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /examples/Citus/Program.cs: -------------------------------------------------------------------------------- 1 | class Program 2 | { 3 | static async Task Main() 4 | { 5 | // generate random data 6 | var rows = 1000000; 7 | var dimensions = 128; 8 | var rand = new Random(); 9 | var embeddings = Enumerable.Range(0, rows).Select((r) => Enumerable.Range(0, dimensions).Select((d) => (float)rand.NextDouble()).ToArray()); 10 | var categories = Enumerable.Range(0, rows).Select((r) => (long)rand.Next(100)).ToArray(); 11 | var queries = Enumerable.Range(0, 10).Select((r) => Enumerable.Range(0, dimensions).Select((d) => (float)rand.NextDouble()).ToArray()); 12 | 13 | // enable extensions 14 | var connString = "Host=localhost;Database=pgvector_citus"; 15 | var dataSourceBuilder = new NpgsqlDataSourceBuilder(connString); 16 | dataSourceBuilder.UseVector(); 17 | await using var dataSource = dataSourceBuilder.Build(); 18 | var conn = dataSource.OpenConnection(); 19 | await using (var cmd = new NpgsqlCommand("CREATE EXTENSION IF NOT EXISTS citus", conn)) 20 | { 21 | await cmd.ExecuteNonQueryAsync(); 22 | } 23 | await using (var cmd = new NpgsqlCommand("CREATE EXTENSION IF NOT EXISTS vector", conn)) 24 | { 25 | await cmd.ExecuteNonQueryAsync(); 26 | } 27 | 28 | // GUC variables set on the session do not propagate to Citus workers 29 | // https://github.com/citusdata/citus/issues/462 30 | // you can either: 31 | // 1. set them on the system, user, or database and reconnect 32 | // 2. set them for a transaction with SET LOCAL 33 | await using (var cmd = new NpgsqlCommand("ALTER DATABASE pgvector_citus SET maintenance_work_mem = '512MB'", conn)) 34 | { 35 | await cmd.ExecuteNonQueryAsync(); 36 | } 37 | await using (var cmd = new NpgsqlCommand("ALTER DATABASE pgvector_citus SET hnsw.ef_search = 20", conn)) 38 | { 39 | await cmd.ExecuteNonQueryAsync(); 40 | } 41 | conn.Close(); 42 | 43 | // reconnect for updated GUC variables to take effect 44 | conn = dataSource.OpenConnection(); 45 | 46 | Console.WriteLine("Creating distributed table"); 47 | await using (var cmd = new NpgsqlCommand("DROP TABLE IF EXISTS items", conn)) 48 | { 49 | await cmd.ExecuteNonQueryAsync(); 50 | } 51 | await using (var cmd = new NpgsqlCommand($"CREATE TABLE items (id bigserial, embedding vector({dimensions}), category_id bigint, PRIMARY KEY (id, category_id))", conn)) 52 | { 53 | await cmd.ExecuteNonQueryAsync(); 54 | } 55 | await using (var cmd = new NpgsqlCommand("SET citus.shard_count = 4", conn)) 56 | { 57 | await cmd.ExecuteNonQueryAsync(); 58 | } 59 | await using (var cmd = new NpgsqlCommand("SELECT create_distributed_table('items', 'category_id')", conn)) 60 | { 61 | await cmd.ExecuteNonQueryAsync(); 62 | } 63 | 64 | Console.WriteLine("Loading data in parallel"); 65 | await using (var writer = conn.BeginBinaryImport("COPY items (embedding, category_id) FROM STDIN WITH (FORMAT BINARY)")) 66 | { 67 | foreach (var (embedding, category) in embeddings.Zip(categories)) 68 | { 69 | writer.StartRow(); 70 | writer.Write(new Vector(embedding)); 71 | writer.Write(category); 72 | } 73 | writer.Complete(); 74 | } 75 | 76 | Console.WriteLine("Creating index in parallel"); 77 | await using (var cmd = new NpgsqlCommand("CREATE INDEX ON items USING hnsw (embedding vector_l2_ops)", conn)) 78 | { 79 | cmd.CommandTimeout = 300; 80 | await cmd.ExecuteNonQueryAsync(); 81 | } 82 | 83 | Console.WriteLine("Running distributed queries"); 84 | foreach (var query in queries) 85 | { 86 | await using (var cmd = new NpgsqlCommand("SELECT id FROM items ORDER BY embedding <-> $1 LIMIT 5", conn)) 87 | { 88 | cmd.Parameters.AddWithValue(new Vector(query)); 89 | 90 | var ids = new List(); 91 | await using (var reader = await cmd.ExecuteReaderAsync()) 92 | { 93 | while (await reader.ReadAsync()) 94 | { 95 | ids.Add((long)reader.GetValue(0)); 96 | } 97 | } 98 | Console.WriteLine(String.Join(", ", ids)); 99 | } 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /examples/Cohere/Cohere.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net9.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /examples/Cohere/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Net.Http.Json; 3 | using System.Text; 4 | using System.Text.Json; 5 | 6 | class EmbedResponse 7 | { 8 | public required EmbeddingsObject embeddings { get; set; } 9 | } 10 | 11 | class EmbeddingsObject 12 | { 13 | public required int[][] ubinary { get; set; } 14 | } 15 | 16 | class Program 17 | { 18 | static async Task Main() 19 | { 20 | var apiKey = Environment.GetEnvironmentVariable("CO_API_KEY"); 21 | if (apiKey is null) 22 | throw new Exception("Set CO_API_KEY"); 23 | 24 | var connString = "Host=localhost;Database=pgvector_example"; 25 | 26 | var dataSourceBuilder = new NpgsqlDataSourceBuilder(connString); 27 | dataSourceBuilder.UseVector(); 28 | await using var dataSource = dataSourceBuilder.Build(); 29 | 30 | var conn = dataSource.OpenConnection(); 31 | 32 | await using (var cmd = new NpgsqlCommand("CREATE EXTENSION IF NOT EXISTS vector", conn)) 33 | { 34 | await cmd.ExecuteNonQueryAsync(); 35 | } 36 | 37 | conn.ReloadTypes(); 38 | 39 | await using (var cmd = new NpgsqlCommand("DROP TABLE IF EXISTS documents", conn)) 40 | { 41 | await cmd.ExecuteNonQueryAsync(); 42 | } 43 | 44 | await using (var cmd = new NpgsqlCommand("CREATE TABLE documents (id bigserial PRIMARY KEY, content text, embedding bit(1536))", conn)) 45 | { 46 | await cmd.ExecuteNonQueryAsync(); 47 | } 48 | 49 | string[] input = { 50 | "The dog is barking", 51 | "The cat is purring", 52 | "The bear is growling" 53 | }; 54 | var embeddings = await Embed(input, "search_document", apiKey); 55 | 56 | for (int i = 0; i < input.Length; i++) 57 | { 58 | await using (var cmd = new NpgsqlCommand("INSERT INTO documents (content, embedding) VALUES ($1, $2)", conn)) 59 | { 60 | cmd.Parameters.AddWithValue(input[i]); 61 | cmd.Parameters.AddWithValue(new BitArray(embeddings[i])); 62 | await cmd.ExecuteNonQueryAsync(); 63 | } 64 | } 65 | 66 | var query = "forest"; 67 | var queryEmbedding = (await Embed(new string[] { query }, "search_query", apiKey))[0]; 68 | 69 | await using (var cmd = new NpgsqlCommand("SELECT content FROM documents ORDER BY embedding <~> $1 LIMIT 5", conn)) 70 | { 71 | cmd.Parameters.AddWithValue(new BitArray(queryEmbedding)); 72 | 73 | await using (var reader = await cmd.ExecuteReaderAsync()) 74 | { 75 | while (await reader.ReadAsync()) 76 | { 77 | Console.WriteLine((string)reader.GetValue(0)); 78 | } 79 | } 80 | } 81 | } 82 | 83 | private static async Task Embed(string[] texts, string inputType, string apiKey) 84 | { 85 | var url = "https://api.cohere.com/v2/embed"; 86 | var data = new 87 | { 88 | texts = texts, 89 | model = "embed-v4.0", 90 | input_type = inputType, 91 | embedding_types = new string[] { "ubinary" } 92 | }; 93 | var client = new HttpClient(); 94 | client.DefaultRequestHeaders.Add("Authorization", "Bearer " + apiKey); 95 | using HttpResponseMessage response = await client.PostAsJsonAsync(url, data); 96 | response.EnsureSuccessStatusCode(); 97 | var apiResponse = await response.Content.ReadFromJsonAsync(); 98 | return apiResponse!.embeddings.ubinary.Select(e => e.Select(v => (byte)v).ToArray()).ToArray(); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /examples/Disco/Disco.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net9.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /examples/Disco/Program.cs: -------------------------------------------------------------------------------- 1 | using DiscoRec; 2 | 3 | class Program 4 | { 5 | static async Task Main() 6 | { 7 | var connString = "Host=localhost;Database=pgvector_example"; 8 | 9 | var dataSourceBuilder = new NpgsqlDataSourceBuilder(connString); 10 | dataSourceBuilder.UseVector(); 11 | await using var dataSource = dataSourceBuilder.Build(); 12 | 13 | var conn = dataSource.OpenConnection(); 14 | 15 | await using (var cmd = new NpgsqlCommand("CREATE EXTENSION IF NOT EXISTS vector", conn)) 16 | { 17 | await cmd.ExecuteNonQueryAsync(); 18 | } 19 | 20 | conn.ReloadTypes(); 21 | 22 | await using (var cmd = new NpgsqlCommand("DROP TABLE IF EXISTS users", conn)) 23 | { 24 | await cmd.ExecuteNonQueryAsync(); 25 | } 26 | 27 | await using (var cmd = new NpgsqlCommand("DROP TABLE IF EXISTS movies", conn)) 28 | { 29 | await cmd.ExecuteNonQueryAsync(); 30 | } 31 | 32 | await using (var cmd = new NpgsqlCommand("CREATE TABLE users (id integer PRIMARY KEY, factors vector(20))", conn)) 33 | { 34 | await cmd.ExecuteNonQueryAsync(); 35 | } 36 | 37 | await using (var cmd = new NpgsqlCommand("CREATE TABLE movies (name text PRIMARY KEY, factors vector(20))", conn)) 38 | { 39 | await cmd.ExecuteNonQueryAsync(); 40 | } 41 | 42 | var data = await Data.LoadMovieLens(); 43 | var recommender = Recommender.FitExplicit(data, new RecommenderOptions { Factors = 20 }); 44 | 45 | foreach (var id in recommender.UserIds()) 46 | { 47 | await using (var cmd = new NpgsqlCommand("INSERT INTO users (id, factors) VALUES ($1, $2)", conn)) 48 | { 49 | cmd.Parameters.AddWithValue(id); 50 | cmd.Parameters.AddWithValue(new Vector(recommender.UserFactors(id))); 51 | 52 | await cmd.ExecuteNonQueryAsync(); 53 | } 54 | } 55 | 56 | foreach (var id in recommender.ItemIds()) 57 | { 58 | await using (var cmd = new NpgsqlCommand("INSERT INTO movies (name, factors) VALUES ($1, $2)", conn)) 59 | { 60 | cmd.Parameters.AddWithValue(id); 61 | cmd.Parameters.AddWithValue(new Vector(recommender.ItemFactors(id))); 62 | 63 | await cmd.ExecuteNonQueryAsync(); 64 | } 65 | } 66 | 67 | var movie = "Star Wars (1977)"; 68 | Console.WriteLine("Item-based recommendations for {0}", movie); 69 | await using (var cmd = new NpgsqlCommand("SELECT name FROM movies WHERE name != $1 ORDER BY factors <=> (SELECT factors FROM movies WHERE name = $1) LIMIT 5", conn)) 70 | { 71 | cmd.Parameters.AddWithValue(movie); 72 | 73 | await using (var reader = await cmd.ExecuteReaderAsync()) 74 | { 75 | while (await reader.ReadAsync()) 76 | { 77 | Console.WriteLine("- {0}", (string)reader.GetValue(0)); 78 | } 79 | } 80 | } 81 | 82 | var userId = 123; 83 | Console.WriteLine("\nUser-based recommendations for user {0}", userId); 84 | await using (var cmd = new NpgsqlCommand("SELECT name FROM movies ORDER BY factors <#> (SELECT factors FROM users WHERE id = $1) LIMIT 5", conn)) 85 | { 86 | cmd.Parameters.AddWithValue(userId); 87 | 88 | await using (var reader = await cmd.ExecuteReaderAsync()) 89 | { 90 | while (await reader.ReadAsync()) 91 | { 92 | Console.WriteLine("- {0}", (string)reader.GetValue(0)); 93 | } 94 | } 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /examples/Hybrid/Hybrid.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net9.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /examples/Hybrid/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http.Json; 2 | using System.Text; 3 | using System.Text.Json; 4 | 5 | class ApiResponse 6 | { 7 | public required float[][] embeddings { get; set; } 8 | } 9 | 10 | class Program 11 | { 12 | static async Task Main() 13 | { 14 | var connString = "Host=localhost;Database=pgvector_example"; 15 | 16 | var dataSourceBuilder = new NpgsqlDataSourceBuilder(connString); 17 | dataSourceBuilder.UseVector(); 18 | await using var dataSource = dataSourceBuilder.Build(); 19 | 20 | var conn = dataSource.OpenConnection(); 21 | 22 | await using (var cmd = new NpgsqlCommand("CREATE EXTENSION IF NOT EXISTS vector", conn)) 23 | { 24 | await cmd.ExecuteNonQueryAsync(); 25 | } 26 | 27 | conn.ReloadTypes(); 28 | 29 | await using (var cmd = new NpgsqlCommand("DROP TABLE IF EXISTS documents", conn)) 30 | { 31 | await cmd.ExecuteNonQueryAsync(); 32 | } 33 | 34 | await using (var cmd = new NpgsqlCommand("CREATE TABLE documents (id bigserial PRIMARY KEY, content text, embedding vector(768))", conn)) 35 | { 36 | await cmd.ExecuteNonQueryAsync(); 37 | } 38 | 39 | await using (var cmd = new NpgsqlCommand("CREATE INDEX ON documents USING GIN (to_tsvector('english', content))", conn)) 40 | { 41 | await cmd.ExecuteNonQueryAsync(); 42 | } 43 | 44 | string[] input = { 45 | "The dog is barking", 46 | "The cat is purring", 47 | "The bear is growling" 48 | }; 49 | var embeddings = await Embed(input, "search_document"); 50 | 51 | for (int i = 0; i < input.Length; i++) 52 | { 53 | await using (var cmd = new NpgsqlCommand("INSERT INTO documents (content, embedding) VALUES ($1, $2)", conn)) 54 | { 55 | cmd.Parameters.AddWithValue(input[i]); 56 | cmd.Parameters.AddWithValue(new Vector(embeddings[i])); 57 | await cmd.ExecuteNonQueryAsync(); 58 | } 59 | } 60 | 61 | var sql = @" 62 | WITH semantic_search AS ( 63 | SELECT id, RANK () OVER (ORDER BY embedding <=> $2) AS rank 64 | FROM documents 65 | ORDER BY embedding <=> $2 66 | LIMIT 20 67 | ), 68 | keyword_search AS ( 69 | SELECT id, RANK () OVER (ORDER BY ts_rank_cd(to_tsvector('english', content), query) DESC) 70 | FROM documents, plainto_tsquery('english', $1) query 71 | WHERE to_tsvector('english', content) @@ query 72 | ORDER BY ts_rank_cd(to_tsvector('english', content), query) DESC 73 | LIMIT 20 74 | ) 75 | SELECT 76 | COALESCE(semantic_search.id, keyword_search.id) AS id, 77 | COALESCE(1.0 / ($3 + semantic_search.rank), 0.0) + 78 | COALESCE(1.0 / ($3 + keyword_search.rank), 0.0) AS score 79 | FROM semantic_search 80 | FULL OUTER JOIN keyword_search ON semantic_search.id = keyword_search.id 81 | ORDER BY score DESC 82 | LIMIT 5 83 | "; 84 | var query = "growling bear"; 85 | var queryEmbedding = (await Embed(new string[] { query }, "search_query"))[0]; 86 | var k = 60; 87 | await using (var cmd = new NpgsqlCommand(sql, conn)) 88 | { 89 | cmd.Parameters.AddWithValue(query); 90 | cmd.Parameters.AddWithValue(new Vector(queryEmbedding)); 91 | cmd.Parameters.AddWithValue(k); 92 | 93 | await using (var reader = await cmd.ExecuteReaderAsync()) 94 | { 95 | while (await reader.ReadAsync()) 96 | { 97 | Console.WriteLine("document: {0}, RRF score: {1}", (long)reader.GetValue(0), (decimal)reader.GetValue(1)); 98 | } 99 | } 100 | } 101 | } 102 | 103 | private static async Task Embed(string[] input, string taskType) 104 | { 105 | // nomic-embed-text uses a task prefix 106 | // https://huggingface.co/nomic-ai/nomic-embed-text-v1.5 107 | input = input.Select(v => taskType + ": " + v).ToArray(); 108 | 109 | var url = "http://localhost:11434/api/embed"; 110 | var data = new 111 | { 112 | input = input, 113 | model = "nomic-embed-text" 114 | }; 115 | var client = new HttpClient(); 116 | using HttpResponseMessage response = await client.PostAsJsonAsync(url, data); 117 | response.EnsureSuccessStatusCode(); 118 | var apiResponse = await response.Content.ReadFromJsonAsync(); 119 | return apiResponse!.embeddings.ToArray(); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /examples/Loading/Loading.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net9.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /examples/Loading/Program.cs: -------------------------------------------------------------------------------- 1 | class Program 2 | { 3 | static async Task Main() 4 | { 5 | // generate random data 6 | var rows = 1000000; 7 | var dimensions = 128; 8 | var rand = new Random(); 9 | var embeddings = Enumerable.Range(0, rows).Select((r) => Enumerable.Range(0, dimensions).Select((d) => (float)rand.NextDouble()).ToArray()); 10 | 11 | // connect 12 | var connString = "Host=localhost;Database=pgvector_example"; 13 | var dataSourceBuilder = new NpgsqlDataSourceBuilder(connString); 14 | dataSourceBuilder.UseVector(); 15 | await using var dataSource = dataSourceBuilder.Build(); 16 | var conn = dataSource.OpenConnection(); 17 | 18 | // enable extension 19 | await using (var cmd = new NpgsqlCommand("CREATE EXTENSION IF NOT EXISTS vector", conn)) 20 | { 21 | await cmd.ExecuteNonQueryAsync(); 22 | } 23 | conn.ReloadTypes(); 24 | 25 | // create table 26 | await using (var cmd = new NpgsqlCommand("DROP TABLE IF EXISTS items", conn)) 27 | { 28 | await cmd.ExecuteNonQueryAsync(); 29 | } 30 | await using (var cmd = new NpgsqlCommand($"CREATE TABLE items (id bigserial, embedding vector({dimensions}))", conn)) 31 | { 32 | await cmd.ExecuteNonQueryAsync(); 33 | } 34 | 35 | // load data 36 | Console.WriteLine($"Loading {rows} rows"); 37 | await using (var writer = conn.BeginBinaryImport("COPY items (embedding) FROM STDIN WITH (FORMAT BINARY)")) 38 | { 39 | var i = 0; 40 | foreach (var embedding in embeddings) 41 | { 42 | writer.StartRow(); 43 | writer.Write(new Vector(embedding)); 44 | 45 | // show progress 46 | if (i++ % 10000 == 0) 47 | Console.Write("."); 48 | } 49 | 50 | writer.Complete(); 51 | } 52 | Console.WriteLine("\nSuccess!"); 53 | 54 | // create any indexes *after* loading initial data 55 | if (Environment.GetEnvironmentVariable("TEST_LOADING") == "index") 56 | { 57 | Console.WriteLine("Creating index"); 58 | await using (var cmd = new NpgsqlCommand("SET maintenance_work_mem = '8GB'", conn)) 59 | { 60 | await cmd.ExecuteNonQueryAsync(); 61 | } 62 | await using (var cmd = new NpgsqlCommand("SET max_parallel_maintenance_workers = 7", conn)) 63 | { 64 | await cmd.ExecuteNonQueryAsync(); 65 | } 66 | await using (var cmd = new NpgsqlCommand("CREATE INDEX ON items USING hnsw (embedding vector_cosine_ops)", conn)) 67 | { 68 | cmd.CommandTimeout = 300; 69 | await cmd.ExecuteNonQueryAsync(); 70 | } 71 | } 72 | 73 | // update planner statistics for good measure 74 | await using (var cmd = new NpgsqlCommand("ANALYZE items", conn)) 75 | { 76 | await cmd.ExecuteNonQueryAsync(); 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /examples/OpenAI/OpenAI.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net9.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /examples/OpenAI/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http.Json; 2 | using System.Text; 3 | using System.Text.Json; 4 | 5 | class ApiResponse 6 | { 7 | public required ApiObject[] data { get; set; } 8 | } 9 | 10 | class ApiObject 11 | { 12 | public required float[] embedding { get; set; } 13 | } 14 | 15 | class Program 16 | { 17 | static async Task Main() 18 | { 19 | var apiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY"); 20 | if (apiKey is null) 21 | throw new Exception("Set OPENAI_API_KEY"); 22 | 23 | var connString = "Host=localhost;Database=pgvector_example"; 24 | 25 | var dataSourceBuilder = new NpgsqlDataSourceBuilder(connString); 26 | dataSourceBuilder.UseVector(); 27 | await using var dataSource = dataSourceBuilder.Build(); 28 | 29 | var conn = dataSource.OpenConnection(); 30 | 31 | await using (var cmd = new NpgsqlCommand("CREATE EXTENSION IF NOT EXISTS vector", conn)) 32 | { 33 | await cmd.ExecuteNonQueryAsync(); 34 | } 35 | 36 | conn.ReloadTypes(); 37 | 38 | await using (var cmd = new NpgsqlCommand("DROP TABLE IF EXISTS documents", conn)) 39 | { 40 | await cmd.ExecuteNonQueryAsync(); 41 | } 42 | 43 | await using (var cmd = new NpgsqlCommand("CREATE TABLE documents (id bigserial PRIMARY KEY, content text, embedding vector(1536))", conn)) 44 | { 45 | await cmd.ExecuteNonQueryAsync(); 46 | } 47 | 48 | string[] input = { 49 | "The dog is barking", 50 | "The cat is purring", 51 | "The bear is growling" 52 | }; 53 | var embeddings = await Embed(input, apiKey); 54 | 55 | for (int i = 0; i < input.Length; i++) 56 | { 57 | await using (var cmd = new NpgsqlCommand("INSERT INTO documents (content, embedding) VALUES ($1, $2)", conn)) 58 | { 59 | cmd.Parameters.AddWithValue(input[i]); 60 | cmd.Parameters.AddWithValue(new Vector(embeddings[i])); 61 | await cmd.ExecuteNonQueryAsync(); 62 | } 63 | } 64 | 65 | var query = "forest"; 66 | var queryEmbedding = (await Embed(new string[] { query }, apiKey))[0]; 67 | 68 | await using (var cmd = new NpgsqlCommand("SELECT content FROM documents ORDER BY embedding <=> $1 LIMIT 5", conn)) 69 | { 70 | cmd.Parameters.AddWithValue(new Vector(queryEmbedding)); 71 | 72 | await using (var reader = await cmd.ExecuteReaderAsync()) 73 | { 74 | while (await reader.ReadAsync()) 75 | { 76 | Console.WriteLine((string)reader.GetValue(0)); 77 | } 78 | } 79 | } 80 | } 81 | 82 | private static async Task Embed(string[] input, string apiKey) 83 | { 84 | var url = "https://api.openai.com/v1/embeddings"; 85 | var data = new 86 | { 87 | input = input, 88 | model = "text-embedding-3-small" 89 | }; 90 | var client = new HttpClient(); 91 | client.DefaultRequestHeaders.Add("Authorization", "Bearer " + apiKey); 92 | using HttpResponseMessage response = await client.PostAsJsonAsync(url, data); 93 | response.EnsureSuccessStatusCode(); 94 | var apiResponse = await response.Content.ReadFromJsonAsync(); 95 | return apiResponse!.data.Select(e => e.embedding).ToArray(); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /examples/Sparse/Program.cs: -------------------------------------------------------------------------------- 1 | // good resources 2 | // https://opensearch.org/blog/improving-document-retrieval-with-sparse-semantic-encoders/ 3 | // https://huggingface.co/opensearch-project/opensearch-neural-sparse-encoding-v1 4 | // 5 | // run with 6 | // text-embeddings-router --model-id opensearch-project/opensearch-neural-sparse-encoding-v1 --pooling splade 7 | 8 | using System.Net.Http.Json; 9 | using System.Text; 10 | using System.Text.Json; 11 | 12 | class ApiElement 13 | { 14 | public required int index { get; set; } 15 | public required float value { get; set; } 16 | } 17 | 18 | class Program 19 | { 20 | static async Task Main() 21 | { 22 | var connString = "Host=localhost;Database=pgvector_example"; 23 | 24 | var dataSourceBuilder = new NpgsqlDataSourceBuilder(connString); 25 | dataSourceBuilder.UseVector(); 26 | await using var dataSource = dataSourceBuilder.Build(); 27 | 28 | var conn = dataSource.OpenConnection(); 29 | 30 | await using (var cmd = new NpgsqlCommand("CREATE EXTENSION IF NOT EXISTS vector", conn)) 31 | { 32 | await cmd.ExecuteNonQueryAsync(); 33 | } 34 | 35 | conn.ReloadTypes(); 36 | 37 | await using (var cmd = new NpgsqlCommand("DROP TABLE IF EXISTS documents", conn)) 38 | { 39 | await cmd.ExecuteNonQueryAsync(); 40 | } 41 | 42 | await using (var cmd = new NpgsqlCommand("CREATE TABLE documents (id bigserial PRIMARY KEY, content text, embedding sparsevec(30522))", conn)) 43 | { 44 | await cmd.ExecuteNonQueryAsync(); 45 | } 46 | 47 | string[] input = { 48 | "The dog is barking", 49 | "The cat is purring", 50 | "The bear is growling" 51 | }; 52 | var embeddings = await Embed(input); 53 | 54 | for (int i = 0; i < input.Length; i++) 55 | { 56 | await using (var cmd = new NpgsqlCommand("INSERT INTO documents (content, embedding) VALUES ($1, $2)", conn)) 57 | { 58 | cmd.Parameters.AddWithValue(input[i]); 59 | cmd.Parameters.AddWithValue(new SparseVector(embeddings[i], 30522)); 60 | await cmd.ExecuteNonQueryAsync(); 61 | } 62 | } 63 | 64 | var query = "forest"; 65 | var queryEmbedding = (await Embed(new string[] { query }))[0]; 66 | await using (var cmd = new NpgsqlCommand("SELECT content FROM documents ORDER BY embedding <#> $1 LIMIT 5", conn)) 67 | { 68 | cmd.Parameters.AddWithValue(new SparseVector(queryEmbedding, 30522)); 69 | 70 | await using (var reader = await cmd.ExecuteReaderAsync()) 71 | { 72 | while (await reader.ReadAsync()) 73 | { 74 | Console.WriteLine((string)reader.GetValue(0)); 75 | } 76 | } 77 | } 78 | } 79 | 80 | private static async Task[]> Embed(string[] inputs) 81 | { 82 | var url = "http://localhost:3000/embed_sparse"; 83 | var data = new 84 | { 85 | inputs = inputs 86 | }; 87 | var client = new HttpClient(); 88 | using HttpResponseMessage response = await client.PostAsJsonAsync(url, data); 89 | response.EnsureSuccessStatusCode(); 90 | var apiResponse = await response.Content.ReadFromJsonAsync(); 91 | return apiResponse!.Select(v => v.ToDictionary(e => e.index, e => e.value)).ToArray(); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /examples/Sparse/Sparse.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net9.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /examples/TopicModeling/Program.cs: -------------------------------------------------------------------------------- 1 | // https://learn.microsoft.com/en-us/dotnet/api/microsoft.ml.textcatalog.latentdirichletallocation?view=ml-dotnet 2 | 3 | using Microsoft.ML; 4 | 5 | class LdaInput 6 | { 7 | public string Text { get; set; } 8 | } 9 | 10 | class LdaOutput 11 | { 12 | public float[] Features { get; set; } 13 | } 14 | 15 | class Program 16 | { 17 | static async Task Main() 18 | { 19 | var connString = "Host=localhost;Database=pgvector_example"; 20 | 21 | var dataSourceBuilder = new NpgsqlDataSourceBuilder(connString); 22 | dataSourceBuilder.UseVector(); 23 | await using var dataSource = dataSourceBuilder.Build(); 24 | 25 | var conn = dataSource.OpenConnection(); 26 | 27 | await using (var cmd = new NpgsqlCommand("CREATE EXTENSION IF NOT EXISTS vector", conn)) 28 | { 29 | await cmd.ExecuteNonQueryAsync(); 30 | } 31 | 32 | conn.ReloadTypes(); 33 | 34 | await using (var cmd = new NpgsqlCommand("DROP TABLE IF EXISTS documents", conn)) 35 | { 36 | await cmd.ExecuteNonQueryAsync(); 37 | } 38 | 39 | await using (var cmd = new NpgsqlCommand("CREATE TABLE documents (id bigserial PRIMARY KEY, content text, embedding vector(20))", conn)) 40 | { 41 | await cmd.ExecuteNonQueryAsync(); 42 | } 43 | 44 | string[] input = { 45 | "The dog is barking", 46 | "The cat is purring", 47 | "The bear is growling" 48 | }; 49 | var embeddings = GenerateEmbeddings(input); 50 | 51 | for (int i = 0; i < input.Length; i++) 52 | { 53 | await using (var cmd = new NpgsqlCommand("INSERT INTO documents (content, embedding) VALUES ($1, $2)", conn)) 54 | { 55 | cmd.Parameters.AddWithValue(input[i]); 56 | cmd.Parameters.AddWithValue(new Vector(embeddings[i])); 57 | await cmd.ExecuteNonQueryAsync(); 58 | } 59 | } 60 | 61 | var documentId = 1; 62 | await using (var cmd = new NpgsqlCommand("SELECT * FROM documents WHERE id != $1 ORDER BY embedding <=> (SELECT embedding FROM documents WHERE id = $1) LIMIT 5", conn)) 63 | { 64 | cmd.Parameters.AddWithValue(documentId); 65 | 66 | await using (var reader = await cmd.ExecuteReaderAsync()) 67 | { 68 | while (await reader.ReadAsync()) 69 | { 70 | Console.WriteLine((string)reader.GetValue(1)); 71 | } 72 | } 73 | } 74 | } 75 | 76 | private static float[][] GenerateEmbeddings(string[] texts) 77 | { 78 | var mlContext = new MLContext(); 79 | var input = texts.Select((v) => new LdaInput { Text = v }); 80 | var dataView = mlContext.Data.LoadFromEnumerable(input); 81 | var pipeline = mlContext.Transforms.Text.NormalizeText("NormalizedText", "Text") 82 | .Append(mlContext.Transforms.Text.TokenizeIntoWords("Tokens", "NormalizedText")) 83 | .Append(mlContext.Transforms.Text.RemoveDefaultStopWords("Tokens")) 84 | .Append(mlContext.Transforms.Conversion.MapValueToKey("Tokens")) 85 | .Append(mlContext.Transforms.Text.ProduceNgrams("Tokens")) 86 | .Append(mlContext.Transforms.Text.LatentDirichletAllocation("Features", "Tokens", numberOfTopics: 20)); 87 | var model = pipeline.Fit(dataView); 88 | var engine = mlContext.Model.CreatePredictionEngine(model); 89 | return input.Select((v) => engine.Predict(v).Features).ToArray(); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /examples/TopicModeling/TopicModeling.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net9.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "9.0.100", 4 | "rollForward": "latestMajor", 5 | "allowPrerelease": "false" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pgvector/pgvector-dotnet/9b91baad1b7b94f131e6bbd7aa7754893820ae28/icon.png -------------------------------------------------------------------------------- /src/Pgvector.Dapper/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.3.1 (2025-03-21) 2 | 3 | - Restored support for .NET Standard 2.0 4 | 5 | ## 0.3.0 (2024-06-25) 6 | 7 | - Added support for `halfvec` and `sparsevec` types 8 | - Dropped support for .NET Standard 9 | 10 | ## 0.2.0 (2024-04-17) 11 | 12 | - Added support for Npgsql 8 13 | - Dropped support for Npgsql < 8 14 | 15 | ## 0.1.1 (2023-04-25) 16 | 17 | - Added support for .NET 6 and .NET Standard 2.0 18 | 19 | ## 0.1.0 (2023-03-28) 20 | 21 | - First release 22 | -------------------------------------------------------------------------------- /src/Pgvector.Dapper/HalfvecTypeHandler.cs: -------------------------------------------------------------------------------- 1 | using Dapper; 2 | using Pgvector; 3 | using System; 4 | using System.Data; 5 | using System.Data.SqlClient; 6 | 7 | namespace Pgvector.Dapper; 8 | 9 | #if NET5_0_OR_GREATER 10 | 11 | public class HalfvecTypeHandler : SqlMapper.TypeHandler 12 | { 13 | public override HalfVector? Parse(object value) 14 | => value switch 15 | { 16 | null or DBNull => null, 17 | HalfVector vec => vec, 18 | _ => value.ToString() is string s ? new HalfVector(s) : null 19 | }; 20 | 21 | public override void SetValue(IDbDataParameter parameter, HalfVector? value) 22 | { 23 | parameter.Value = value is null ? DBNull.Value : value; 24 | 25 | if (parameter is SqlParameter sqlParameter) 26 | { 27 | sqlParameter.UdtTypeName = "halfvec"; 28 | } 29 | } 30 | } 31 | 32 | #endif 33 | -------------------------------------------------------------------------------- /src/Pgvector.Dapper/Pgvector.Dapper.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Pgvector.Dapper 5 | 0.3.1 6 | ankane 7 | pgvector support for Dapper 8 | MIT 9 | https://github.com/pgvector/pgvector-dotnet 10 | README.md 11 | icon.png 12 | 13 | net6.0;netstandard2.0;net462 14 | enable 15 | latest 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/Pgvector.Dapper/SparsevecTypeHandler.cs: -------------------------------------------------------------------------------- 1 | using Dapper; 2 | using Pgvector; 3 | using System; 4 | using System.Data; 5 | using System.Data.SqlClient; 6 | 7 | namespace Pgvector.Dapper; 8 | 9 | public class SparsevecTypeHandler : SqlMapper.TypeHandler 10 | { 11 | public override SparseVector? Parse(object value) 12 | => value switch 13 | { 14 | null or DBNull => null, 15 | SparseVector vec => vec, 16 | _ => value.ToString() is string s ? new SparseVector(s) : null 17 | }; 18 | 19 | public override void SetValue(IDbDataParameter parameter, SparseVector? value) 20 | { 21 | parameter.Value = value is null ? DBNull.Value : value; 22 | 23 | if (parameter is SqlParameter sqlParameter) 24 | { 25 | sqlParameter.UdtTypeName = "sparsevec"; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Pgvector.Dapper/VectorTypeHandler.cs: -------------------------------------------------------------------------------- 1 | using Dapper; 2 | using Pgvector; 3 | using System; 4 | using System.Data; 5 | using System.Data.SqlClient; 6 | 7 | namespace Pgvector.Dapper; 8 | 9 | public class VectorTypeHandler : SqlMapper.TypeHandler 10 | { 11 | public override Vector? Parse(object value) 12 | => value switch 13 | { 14 | null or DBNull => null, 15 | Vector vec => vec, 16 | _ => value.ToString() is string s ? new Vector(s) : null 17 | }; 18 | 19 | public override void SetValue(IDbDataParameter parameter, Vector? value) 20 | { 21 | parameter.Value = value is null ? DBNull.Value : value; 22 | 23 | if (parameter is SqlParameter sqlParameter) 24 | { 25 | sqlParameter.UdtTypeName = "vector"; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Pgvector.EntityFrameworkCore/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.2.2 (2025-03-26) 2 | 3 | - Added support for scaffolding 4 | - Added support for `MaxLength` 5 | 6 | ## 0.2.1 (2024-06-25) 7 | 8 | - Added support for `halfvec` and `sparsevec` types 9 | - Added support for compiled models 10 | - Added `L1Distance`, `HammingDistance`, and `JaccardDistance` functions 11 | 12 | ## 0.2.0 (2023-11-24) 13 | 14 | - Added support for Npgsql.EntityFrameworkCore.PostgreSQL 8 15 | - Dropped support for Npgsql.EntityFrameworkCore.PostgreSQL < 8 16 | 17 | ## 0.1.2 (2023-09-25) 18 | 19 | - Added distance functions 20 | 21 | ## 0.1.1 (2023-04-25) 22 | 23 | - Added support for .NET 6 24 | 25 | ## 0.1.0 (2023-03-29) 26 | 27 | - First release 28 | -------------------------------------------------------------------------------- /src/Pgvector.EntityFrameworkCore/Pgvector.EntityFrameworkCore.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Pgvector.EntityFrameworkCore 5 | 0.2.2 6 | ankane 7 | pgvector support for Entity Framework Core 8 | MIT 9 | https://github.com/pgvector/pgvector-dotnet 10 | README.md 11 | icon.png 12 | 13 | net8.0 14 | enable 15 | enable 16 | latest 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/Pgvector.EntityFrameworkCore/VectorCodeGeneratorPlugin.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.EntityFrameworkCore.Design; 3 | using Microsoft.EntityFrameworkCore.Scaffolding; 4 | using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; 5 | using System.Reflection; 6 | 7 | namespace Pgvector.EntityFrameworkCore; 8 | 9 | public class VectorCodeGeneratorPlugin : ProviderCodeGeneratorPlugin 10 | { 11 | private static readonly MethodInfo _useVectorMethodInfo 12 | = typeof(VectorDbContextOptionsBuilderExtensions).GetMethod( 13 | nameof(VectorDbContextOptionsBuilderExtensions.UseVector), 14 | [typeof(NpgsqlDbContextOptionsBuilder)])!; 15 | 16 | public override MethodCallCodeFragment GenerateProviderOptions() 17 | => new(_useVectorMethodInfo); 18 | } 19 | -------------------------------------------------------------------------------- /src/Pgvector.EntityFrameworkCore/VectorDbContextOptionsBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Infrastructure; 2 | using Npgsql; 3 | using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; 4 | using Pgvector.EntityFrameworkCore; 5 | using Pgvector.Npgsql; 6 | 7 | namespace Microsoft.EntityFrameworkCore; 8 | 9 | public static class VectorDbContextOptionsBuilderExtensions 10 | { 11 | public static NpgsqlDbContextOptionsBuilder UseVector(this NpgsqlDbContextOptionsBuilder optionsBuilder) 12 | { 13 | // not ideal, but how Npgsql.EntityFrameworkCore.PostgreSQL does it 14 | #pragma warning disable CS0618 15 | NpgsqlConnection.GlobalTypeMapper.UseVector(); 16 | #pragma warning restore CS0618 17 | 18 | var coreOptionsBuilder = ((IRelationalDbContextOptionsBuilderInfrastructure)optionsBuilder).OptionsBuilder; 19 | 20 | var extension = coreOptionsBuilder.Options.FindExtension() 21 | ?? new VectorDbContextOptionsExtension(); 22 | 23 | ((IDbContextOptionsBuilderInfrastructure)coreOptionsBuilder).AddOrUpdateExtension(extension); 24 | 25 | return optionsBuilder; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Pgvector.EntityFrameworkCore/VectorDbContextOptionsExtension.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Infrastructure; 2 | using Microsoft.EntityFrameworkCore.Query; 3 | using Microsoft.EntityFrameworkCore.Storage; 4 | using Microsoft.Extensions.DependencyInjection; 5 | 6 | namespace Pgvector.EntityFrameworkCore; 7 | 8 | public class VectorDbContextOptionsExtension : IDbContextOptionsExtension 9 | { 10 | private DbContextOptionsExtensionInfo? _info; 11 | 12 | public virtual DbContextOptionsExtensionInfo Info => _info ??= new ExtensionInfo(this); 13 | 14 | public void ApplyServices(IServiceCollection services) 15 | { 16 | new EntityFrameworkRelationalServicesBuilder(services) 17 | .TryAdd(); 18 | 19 | services.AddSingleton(); 20 | } 21 | 22 | public void Validate(IDbContextOptions options) { } 23 | 24 | private sealed class ExtensionInfo : DbContextOptionsExtensionInfo 25 | { 26 | public ExtensionInfo(IDbContextOptionsExtension extension) : base(extension) { } 27 | 28 | private new VectorDbContextOptionsExtension Extension 29 | => (VectorDbContextOptionsExtension)base.Extension; 30 | 31 | public override bool IsDatabaseProvider => false; 32 | 33 | public override string LogFragment => "using vector "; 34 | 35 | public override int GetServiceProviderHashCode() 36 | => 0; 37 | 38 | public override void PopulateDebugInfo(IDictionary debugInfo) 39 | { 40 | debugInfo["Pgvector.EntityFrameworkCore:UseVector"] = "1"; 41 | } 42 | 43 | public override bool ShouldUseSameServiceProvider(DbContextOptionsExtensionInfo other) 44 | => true; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Pgvector.EntityFrameworkCore/VectorDbFunctionsExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Diagnostics; 2 | 3 | namespace Pgvector.EntityFrameworkCore; 4 | 5 | public static class VectorDbFunctionsExtensions 6 | { 7 | public static double L2Distance(this object a, object b) 8 | => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(L2Distance))); 9 | 10 | public static double MaxInnerProduct(this object a, object b) 11 | => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(MaxInnerProduct))); 12 | 13 | public static double CosineDistance(this object a, object b) 14 | => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(CosineDistance))); 15 | 16 | public static double L1Distance(this object a, object b) 17 | => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(L1Distance))); 18 | 19 | public static double HammingDistance(this object a, object b) 20 | => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(HammingDistance))); 21 | 22 | public static double JaccardDistance(this object a, object b) 23 | => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(JaccardDistance))); 24 | } 25 | -------------------------------------------------------------------------------- /src/Pgvector.EntityFrameworkCore/VectorDbFunctionsTranslatorPlugin.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.EntityFrameworkCore.Diagnostics; 3 | using Microsoft.EntityFrameworkCore.Query; 4 | using Microsoft.EntityFrameworkCore.Query.SqlExpressions; 5 | using Microsoft.EntityFrameworkCore.Storage; 6 | using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal; 7 | using System.Reflection; 8 | 9 | namespace Pgvector.EntityFrameworkCore; 10 | 11 | public class VectorDbFunctionsTranslatorPlugin : IMethodCallTranslatorPlugin 12 | { 13 | public VectorDbFunctionsTranslatorPlugin( 14 | ISqlExpressionFactory sqlExpressionFactory, 15 | IRelationalTypeMappingSource typeMappingSource 16 | ) 17 | { 18 | Translators = new[] 19 | { 20 | new VectorDbFunctionsTranslator(sqlExpressionFactory, typeMappingSource), 21 | }; 22 | } 23 | 24 | public virtual IEnumerable Translators { get; } 25 | 26 | private class VectorDbFunctionsTranslator : IMethodCallTranslator 27 | { 28 | private readonly ISqlExpressionFactory _sqlExpressionFactory; 29 | private readonly IRelationalTypeMappingSource _typeMappingSource; 30 | 31 | private static readonly MethodInfo _methodL2Distance = typeof(VectorDbFunctionsExtensions) 32 | .GetRuntimeMethod(nameof(VectorDbFunctionsExtensions.L2Distance), new[] 33 | { 34 | typeof(object), 35 | typeof(object), 36 | })!; 37 | 38 | private static readonly MethodInfo _methodMaxInnerProduct = typeof(VectorDbFunctionsExtensions) 39 | .GetRuntimeMethod(nameof(VectorDbFunctionsExtensions.MaxInnerProduct), new[] 40 | { 41 | typeof(object), 42 | typeof(object), 43 | })!; 44 | 45 | private static readonly MethodInfo _methodCosineDistance = typeof(VectorDbFunctionsExtensions) 46 | .GetRuntimeMethod(nameof(VectorDbFunctionsExtensions.CosineDistance), new[] 47 | { 48 | typeof(object), 49 | typeof(object), 50 | })!; 51 | 52 | private static readonly MethodInfo _methodL1Distance = typeof(VectorDbFunctionsExtensions) 53 | .GetRuntimeMethod(nameof(VectorDbFunctionsExtensions.L1Distance), new[] 54 | { 55 | typeof(object), 56 | typeof(object), 57 | })!; 58 | 59 | private static readonly MethodInfo _methodHammingDistance = typeof(VectorDbFunctionsExtensions) 60 | .GetRuntimeMethod(nameof(VectorDbFunctionsExtensions.HammingDistance), new[] 61 | { 62 | typeof(object), 63 | typeof(object), 64 | })!; 65 | 66 | private static readonly MethodInfo _methodJaccardDistance = typeof(VectorDbFunctionsExtensions) 67 | .GetRuntimeMethod(nameof(VectorDbFunctionsExtensions.JaccardDistance), new[] 68 | { 69 | typeof(object), 70 | typeof(object), 71 | })!; 72 | 73 | public VectorDbFunctionsTranslator( 74 | ISqlExpressionFactory sqlExpressionFactory, 75 | IRelationalTypeMappingSource typeMappingSource 76 | ) 77 | { 78 | _sqlExpressionFactory = sqlExpressionFactory; 79 | _typeMappingSource = typeMappingSource; 80 | } 81 | 82 | #pragma warning disable EF1001 83 | public SqlExpression? Translate( 84 | SqlExpression? instance, 85 | MethodInfo method, 86 | IReadOnlyList arguments, 87 | IDiagnosticsLogger logger 88 | ) 89 | { 90 | var vectorOperator = method switch 91 | { 92 | _ when ReferenceEquals(method, _methodL2Distance) => "<->", 93 | _ when ReferenceEquals(method, _methodMaxInnerProduct) => "<#>", 94 | _ when ReferenceEquals(method, _methodCosineDistance) => "<=>", 95 | _ when ReferenceEquals(method, _methodL1Distance) => "<+>", 96 | _ when ReferenceEquals(method, _methodHammingDistance) => "<~>", 97 | _ when ReferenceEquals(method, _methodJaccardDistance) => "<%>", 98 | _ => null 99 | }; 100 | 101 | if (vectorOperator != null) 102 | { 103 | var resultTypeMapping = _typeMappingSource.FindMapping(method.ReturnType)!; 104 | 105 | return new PgUnknownBinaryExpression( 106 | left: _sqlExpressionFactory.ApplyDefaultTypeMapping(arguments[0]), 107 | right: _sqlExpressionFactory.ApplyDefaultTypeMapping(arguments[1]), 108 | binaryOperator: vectorOperator, 109 | type: resultTypeMapping.ClrType, 110 | typeMapping: resultTypeMapping 111 | ); 112 | } 113 | 114 | return null; 115 | } 116 | #pragma warning restore EF1001 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/Pgvector.EntityFrameworkCore/VectorDesignTimeServices.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Design; 2 | using Microsoft.EntityFrameworkCore.Scaffolding; 3 | using Microsoft.EntityFrameworkCore.Storage; 4 | using Microsoft.Extensions.DependencyInjection; 5 | 6 | namespace Pgvector.EntityFrameworkCore; 7 | 8 | public class VectorDesignTimeServices : IDesignTimeServices 9 | { 10 | public virtual void ConfigureDesignTimeServices(IServiceCollection serviceCollection) 11 | => serviceCollection 12 | .AddSingleton() 13 | .AddSingleton(); 14 | } 15 | -------------------------------------------------------------------------------- /src/Pgvector.EntityFrameworkCore/VectorTypeMapping.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Storage; 2 | using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; 3 | using NpgsqlTypes; 4 | 5 | namespace Pgvector.EntityFrameworkCore; 6 | 7 | public class VectorTypeMapping : RelationalTypeMapping 8 | { 9 | public static VectorTypeMapping Default { get; } = new("vector", typeof(Vector)); 10 | 11 | public VectorTypeMapping(string storeType, Type clrType, int? size = null) 12 | : this( 13 | new RelationalTypeMappingParameters( 14 | new CoreTypeMappingParameters(clrType), 15 | storeType, 16 | StoreTypePostfix.Size, 17 | size: size, 18 | fixedLength: size is not null)) 19 | { 20 | } 21 | 22 | protected VectorTypeMapping(RelationalTypeMappingParameters parameters) : base(parameters) { } 23 | 24 | protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) 25 | => new VectorTypeMapping(parameters); 26 | } 27 | -------------------------------------------------------------------------------- /src/Pgvector.EntityFrameworkCore/VectorTypeMappingSourcePlugin.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Storage; 2 | 3 | namespace Pgvector.EntityFrameworkCore; 4 | 5 | public class VectorTypeMappingSourcePlugin : IRelationalTypeMappingSourcePlugin 6 | { 7 | public RelationalTypeMapping? FindMapping(in RelationalTypeMappingInfo mappingInfo) 8 | { 9 | // for scaffolding 10 | if (mappingInfo.ClrType == null) 11 | { 12 | return (mappingInfo.StoreTypeNameBase ?? mappingInfo.StoreTypeName) switch 13 | { 14 | "vector" => new VectorTypeMapping(mappingInfo.StoreTypeName ?? "vector", typeof(Vector), mappingInfo.Size), 15 | "halfvec" => new VectorTypeMapping(mappingInfo.StoreTypeName ?? "halfvec", typeof(HalfVector), mappingInfo.Size), 16 | "sparsevec" => new VectorTypeMapping(mappingInfo.StoreTypeName ?? "sparsevec", typeof(SparseVector), mappingInfo.Size), 17 | _ => null, 18 | }; 19 | } 20 | 21 | // for entities 22 | return mappingInfo.ClrType switch 23 | { 24 | var t when t == typeof(Vector) => new VectorTypeMapping(mappingInfo.StoreTypeName ?? "vector", typeof(Vector), mappingInfo.Size), 25 | var t when t == typeof(HalfVector) => new VectorTypeMapping(mappingInfo.StoreTypeName ?? "halfvec", typeof(HalfVector), mappingInfo.Size), 26 | var t when t == typeof(SparseVector) => new VectorTypeMapping(mappingInfo.StoreTypeName ?? "sparsevec", typeof(SparseVector), mappingInfo.Size), 27 | _ => null, 28 | }; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Pgvector.EntityFrameworkCore/build/net8.0/Pgvector.EntityFrameworkCore.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | $(MSBuildAllProjects);$(MSBuildThisFileFullPath) 4 | $(IntermediateOutputPath)EFCoreNpgsqlPgvector$(DefaultLanguageSourceExtension) 5 | 6 | 7 | 8 | 9 | 10 | 11 | CompileBefore 12 | 13 | 14 | 15 | 16 | CompileAfter 17 | 18 | 19 | 20 | 21 | 22 | 23 | Compile 24 | 25 | 26 | 27 | 33 | 34 | 35 | <_Parameter1>Pgvector.EntityFrameworkCore.VectorDesignTimeServices, Pgvector.EntityFrameworkCore 36 | <_Parameter2>Npgsql.EntityFrameworkCore.PostgreSQL 37 | 38 | 39 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /src/Pgvector/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.3.2 (2025-05-20) 2 | 3 | - Reduced allocations 4 | 5 | ## 0.3.1 (2025-03-21) 6 | 7 | - Restored support for .NET Standard 2.0 8 | 9 | ## 0.3.0 (2024-06-25) 10 | 11 | - Added support for `halfvec` and `sparsevec` types 12 | - Dropped support for .NET Standard 13 | 14 | ## 0.2.0 (2023-11-24) 15 | 16 | - Added support for Npgsql 8 17 | - Dropped support for Npgsql < 8 18 | 19 | ## 0.1.4 (2023-09-25) 20 | 21 | - Fixed error with large vectors 22 | 23 | ## 0.1.3 (2023-05-20) 24 | 25 | - Updated text representation to be culture-invariant 26 | 27 | ## 0.1.2 (2023-04-25) 28 | 29 | - Added support for .NET 6 and .NET Standard 2.0 30 | 31 | ## 0.1.1 (2023-03-12) 32 | 33 | - Added type mapping 34 | 35 | ## 0.1.0 (2023-03-09) 36 | 37 | - First release 38 | -------------------------------------------------------------------------------- /src/Pgvector/HalfVector.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using System.Linq; 4 | 5 | namespace Pgvector; 6 | 7 | #if NET5_0_OR_GREATER 8 | 9 | public class HalfVector : IEquatable 10 | { 11 | public ReadOnlyMemory Memory { get; } 12 | 13 | public HalfVector(ReadOnlyMemory v) 14 | => Memory = v; 15 | 16 | public HalfVector(string s) 17 | => Memory = Array.ConvertAll(s.Substring(1, s.Length - 2).Split(','), v => Half.Parse(v, CultureInfo.InvariantCulture)); 18 | 19 | public override string ToString() 20 | => string.Concat("[", string.Join(",", Memory.ToArray().Select(v => v.ToString(CultureInfo.InvariantCulture))), "]"); 21 | 22 | public Half[] ToArray() 23 | => Memory.ToArray(); 24 | 25 | public bool Equals(HalfVector? other) 26 | => other is not null && Memory.Span.SequenceEqual(other.Memory.Span); 27 | 28 | public override bool Equals(object? obj) 29 | => obj is HalfVector vector && Equals(vector); 30 | 31 | public static bool operator ==(HalfVector? x, HalfVector? y) 32 | => (x is null && y is null) || (x is not null && x.Equals(y)); 33 | 34 | public static bool operator !=(HalfVector? x, HalfVector? y) => !(x == y); 35 | 36 | public override int GetHashCode() 37 | { 38 | var hashCode = new HashCode(); 39 | var span = Memory.Span; 40 | 41 | for (var i = 0; i < span.Length; i++) 42 | hashCode.Add(span[i]); 43 | 44 | return hashCode.ToHashCode(); 45 | } 46 | } 47 | 48 | #endif 49 | -------------------------------------------------------------------------------- /src/Pgvector/Npgsql/HalfvecConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Npgsql.Internal; 5 | 6 | namespace Pgvector.Npgsql; 7 | 8 | #if NET5_0_OR_GREATER 9 | 10 | public class HalfvecConverter : PgStreamingConverter 11 | { 12 | public override HalfVector Read(PgReader reader) 13 | { 14 | if (reader.ShouldBuffer(2 * sizeof(ushort))) 15 | reader.Buffer(2 * sizeof(ushort)); 16 | 17 | var dim = reader.ReadUInt16(); 18 | var unused = reader.ReadUInt16(); 19 | if (unused != 0) 20 | throw new InvalidCastException("expected unused to be 0"); 21 | 22 | var vec = new Half[dim]; 23 | for (var i = 0; i < dim; i++) 24 | { 25 | if (reader.ShouldBuffer(sizeof(ushort))) 26 | reader.Buffer(sizeof(ushort)); 27 | vec[i] = BitConverter.UInt16BitsToHalf(reader.ReadUInt16()); 28 | } 29 | 30 | return new HalfVector(vec); 31 | } 32 | 33 | public override async ValueTask ReadAsync(PgReader reader, CancellationToken cancellationToken = default) 34 | { 35 | if (reader.ShouldBuffer(2 * sizeof(ushort))) 36 | await reader.BufferAsync(2 * sizeof(ushort), cancellationToken).ConfigureAwait(false); 37 | 38 | var dim = reader.ReadUInt16(); 39 | var unused = reader.ReadUInt16(); 40 | if (unused != 0) 41 | throw new InvalidCastException("expected unused to be 0"); 42 | 43 | var vec = new Half[dim]; 44 | for (var i = 0; i < dim; i++) 45 | { 46 | if (reader.ShouldBuffer(sizeof(ushort))) 47 | await reader.BufferAsync(sizeof(ushort), cancellationToken).ConfigureAwait(false); 48 | vec[i] = BitConverter.UInt16BitsToHalf(reader.ReadUInt16()); 49 | } 50 | 51 | return new HalfVector(vec); 52 | } 53 | 54 | public override Size GetSize(SizeContext context, HalfVector value, ref object? writeState) 55 | => sizeof(ushort) * 2 + sizeof(ushort) * value.Memory.Length; 56 | 57 | public override void Write(PgWriter writer, HalfVector value) 58 | { 59 | if (writer.ShouldFlush(sizeof(ushort) * 2)) 60 | writer.Flush(); 61 | 62 | var span = value.Memory.Span; 63 | var dim = span.Length; 64 | writer.WriteUInt16(Convert.ToUInt16(dim)); 65 | writer.WriteUInt16(0); 66 | 67 | for (int i = 0; i < dim; i++) 68 | { 69 | if (writer.ShouldFlush(sizeof(ushort))) 70 | writer.Flush(); 71 | writer.WriteUInt16(BitConverter.HalfToUInt16Bits(span[i])); 72 | } 73 | } 74 | 75 | public override async ValueTask WriteAsync( 76 | PgWriter writer, HalfVector value, CancellationToken cancellationToken = default) 77 | { 78 | if (writer.ShouldFlush(sizeof(ushort) * 2)) 79 | await writer.FlushAsync(cancellationToken); 80 | 81 | var memory = value.Memory; 82 | var dim = memory.Length; 83 | writer.WriteUInt16(Convert.ToUInt16(dim)); 84 | writer.WriteUInt16(0); 85 | 86 | for (int i = 0; i < dim; i++) 87 | { 88 | if (writer.ShouldFlush(sizeof(ushort))) 89 | await writer.FlushAsync(cancellationToken); 90 | writer.WriteUInt16(BitConverter.HalfToUInt16Bits(memory.Span[i])); 91 | } 92 | } 93 | } 94 | 95 | #endif 96 | -------------------------------------------------------------------------------- /src/Pgvector/Npgsql/SparsevecConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Npgsql.Internal; 5 | 6 | namespace Pgvector.Npgsql; 7 | 8 | public class SparsevecConverter : PgStreamingConverter 9 | { 10 | public override SparseVector Read(PgReader reader) 11 | { 12 | if (reader.ShouldBuffer(3 * sizeof(int))) 13 | reader.Buffer(3 * sizeof(int)); 14 | 15 | var dim = reader.ReadInt32(); 16 | var nnz = reader.ReadInt32(); 17 | var unused = reader.ReadInt32(); 18 | if (unused != 0) 19 | throw new InvalidCastException("expected unused to be 0"); 20 | 21 | var indices = new int[nnz]; 22 | for (var i = 0; i < nnz; i++) 23 | { 24 | if (reader.ShouldBuffer(sizeof(int))) 25 | reader.Buffer(sizeof(int)); 26 | indices[i] = reader.ReadInt32(); 27 | } 28 | 29 | var values = new float[nnz]; 30 | for (var i = 0; i < nnz; i++) 31 | { 32 | if (reader.ShouldBuffer(sizeof(float))) 33 | reader.Buffer(sizeof(float)); 34 | values[i] = reader.ReadFloat(); 35 | } 36 | 37 | return new SparseVector(dim, indices, values); 38 | } 39 | 40 | public override async ValueTask ReadAsync(PgReader reader, CancellationToken cancellationToken = default) 41 | { 42 | if (reader.ShouldBuffer(3 * sizeof(int))) 43 | await reader.BufferAsync(3 * sizeof(int), cancellationToken).ConfigureAwait(false); 44 | 45 | var dim = reader.ReadInt16(); 46 | var nnz = reader.ReadInt16(); 47 | var unused = reader.ReadInt16(); 48 | if (unused != 0) 49 | throw new InvalidCastException("expected unused to be 0"); 50 | 51 | var indices = new int[nnz]; 52 | for (var i = 0; i < nnz; i++) 53 | { 54 | if (reader.ShouldBuffer(sizeof(int))) 55 | await reader.BufferAsync(sizeof(int), cancellationToken).ConfigureAwait(false); ; 56 | indices[i] = reader.ReadInt32(); 57 | } 58 | 59 | var values = new float[nnz]; 60 | for (var i = 0; i < nnz; i++) 61 | { 62 | if (reader.ShouldBuffer(sizeof(float))) 63 | await reader.BufferAsync(sizeof(float), cancellationToken).ConfigureAwait(false); 64 | values[i] = reader.ReadFloat(); 65 | } 66 | 67 | return new SparseVector(dim, indices, values); 68 | } 69 | 70 | public override Size GetSize(SizeContext context, SparseVector value, ref object? writeState) 71 | => sizeof(int) * 3 + sizeof(int) * value.Indices.Length + sizeof(float) * value.Values.Length; 72 | 73 | public override void Write(PgWriter writer, SparseVector value) 74 | { 75 | if (writer.ShouldFlush(sizeof(int) * 3)) 76 | writer.Flush(); 77 | 78 | var indicesSpan = value.Indices.Span; 79 | var valuesSpan = value.Values.Span; 80 | writer.WriteInt32(value.Dimensions); 81 | writer.WriteInt32(indicesSpan.Length); 82 | writer.WriteInt32(0); 83 | 84 | for (int i = 0; i < indicesSpan.Length; i++) 85 | { 86 | if (writer.ShouldFlush(sizeof(int))) 87 | writer.Flush(); 88 | writer.WriteInt32(indicesSpan[i]); 89 | } 90 | 91 | for (int i = 0; i < valuesSpan.Length; i++) 92 | { 93 | if (writer.ShouldFlush(sizeof(float))) 94 | writer.Flush(); 95 | writer.WriteFloat(valuesSpan[i]); 96 | } 97 | } 98 | 99 | public override async ValueTask WriteAsync( 100 | PgWriter writer, SparseVector value, CancellationToken cancellationToken = default) 101 | { 102 | if (writer.ShouldFlush(sizeof(int) * 3)) 103 | await writer.FlushAsync(cancellationToken); 104 | 105 | var indices = value.Indices; 106 | var values = value.Values; 107 | writer.WriteInt32(value.Dimensions); 108 | writer.WriteInt32(indices.Length); 109 | writer.WriteInt32(0); 110 | 111 | for (int i = 0; i < indices.Length; i++) 112 | { 113 | if (writer.ShouldFlush(sizeof(int))) 114 | await writer.FlushAsync(cancellationToken); 115 | writer.WriteInt32(indices.Span[i]); 116 | } 117 | 118 | for (int i = 0; i < values.Length; i++) 119 | { 120 | if (writer.ShouldFlush(sizeof(float))) 121 | await writer.FlushAsync(cancellationToken); 122 | writer.WriteFloat(values.Span[i]); 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/Pgvector/Npgsql/VectorConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Npgsql.Internal; 5 | 6 | namespace Pgvector.Npgsql; 7 | 8 | public class VectorConverter : PgStreamingConverter 9 | { 10 | public override Vector Read(PgReader reader) 11 | { 12 | if (reader.ShouldBuffer(2 * sizeof(ushort))) 13 | reader.Buffer(2 * sizeof(ushort)); 14 | 15 | var dim = reader.ReadUInt16(); 16 | var unused = reader.ReadUInt16(); 17 | if (unused != 0) 18 | throw new InvalidCastException("expected unused to be 0"); 19 | 20 | var vec = new float[dim]; 21 | for (var i = 0; i < dim; i++) 22 | { 23 | if (reader.ShouldBuffer(sizeof(float))) 24 | reader.Buffer(sizeof(float)); 25 | vec[i] = reader.ReadFloat(); 26 | } 27 | 28 | return new Vector(vec); 29 | } 30 | 31 | public override async ValueTask ReadAsync(PgReader reader, CancellationToken cancellationToken = default) 32 | { 33 | if (reader.ShouldBuffer(2 * sizeof(ushort))) 34 | await reader.BufferAsync(2 * sizeof(ushort), cancellationToken).ConfigureAwait(false); 35 | 36 | var dim = reader.ReadUInt16(); 37 | var unused = reader.ReadUInt16(); 38 | if (unused != 0) 39 | throw new InvalidCastException("expected unused to be 0"); 40 | 41 | var vec = new float[dim]; 42 | for (var i = 0; i < dim; i++) 43 | { 44 | if (reader.ShouldBuffer(sizeof(float))) 45 | await reader.BufferAsync(sizeof(float), cancellationToken).ConfigureAwait(false); 46 | vec[i] = reader.ReadFloat(); 47 | } 48 | 49 | return new Vector(vec); 50 | } 51 | 52 | public override Size GetSize(SizeContext context, Vector value, ref object? writeState) 53 | => sizeof(ushort) * 2 + sizeof(float) * value.Memory.Length; 54 | 55 | public override void Write(PgWriter writer, Vector value) 56 | { 57 | if (writer.ShouldFlush(sizeof(ushort) * 2)) 58 | writer.Flush(); 59 | 60 | var span = value.Memory.Span; 61 | var dim = span.Length; 62 | writer.WriteUInt16(Convert.ToUInt16(dim)); 63 | writer.WriteUInt16(0); 64 | 65 | for (int i = 0; i < dim; i++) 66 | { 67 | if (writer.ShouldFlush(sizeof(float))) 68 | writer.Flush(); 69 | writer.WriteFloat(span[i]); 70 | } 71 | } 72 | 73 | public override async ValueTask WriteAsync( 74 | PgWriter writer, Vector value, CancellationToken cancellationToken = default) 75 | { 76 | if (writer.ShouldFlush(sizeof(ushort) * 2)) 77 | await writer.FlushAsync(cancellationToken); 78 | 79 | var memory = value.Memory; 80 | var dim = memory.Length; 81 | writer.WriteUInt16(Convert.ToUInt16(dim)); 82 | writer.WriteUInt16(0); 83 | 84 | for (int i = 0; i < dim; i++) 85 | { 86 | if (writer.ShouldFlush(sizeof(float))) 87 | await writer.FlushAsync(cancellationToken); 88 | writer.WriteFloat(memory.Span[i]); 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/Pgvector/Npgsql/VectorExtensions.cs: -------------------------------------------------------------------------------- 1 | using Npgsql.TypeMapping; 2 | using Pgvector.Npgsql; 3 | 4 | namespace Npgsql; 5 | 6 | public static class VectorExtensions 7 | { 8 | public static INpgsqlTypeMapper UseVector(this INpgsqlTypeMapper mapper) 9 | { 10 | mapper.AddTypeInfoResolverFactory(new VectorTypeInfoResolverFactory()); 11 | return mapper; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Pgvector/Npgsql/VectorTypeInfoResolverFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Npgsql.Internal; 3 | using Npgsql.Internal.Postgres; 4 | 5 | namespace Pgvector.Npgsql; 6 | 7 | public class VectorTypeInfoResolverFactory : PgTypeInfoResolverFactory 8 | { 9 | public override IPgTypeInfoResolver CreateResolver() => new Resolver(); 10 | public override IPgTypeInfoResolver CreateArrayResolver() => new ArrayResolver(); 11 | 12 | class Resolver : IPgTypeInfoResolver 13 | { 14 | TypeInfoMappingCollection? _mappings; 15 | protected TypeInfoMappingCollection Mappings => _mappings ??= AddMappings(new()); 16 | 17 | public PgTypeInfo? GetTypeInfo(Type? type, DataTypeName? dataTypeName, PgSerializerOptions options) 18 | => Mappings.Find(type, dataTypeName, options); 19 | 20 | static TypeInfoMappingCollection AddMappings(TypeInfoMappingCollection mappings) 21 | { 22 | mappings.AddType("vector", 23 | static (options, mapping, _) => mapping.CreateInfo(options, new VectorConverter()), isDefault: true); 24 | #if NET5_0_OR_GREATER 25 | mappings.AddType("halfvec", 26 | static (options, mapping, _) => mapping.CreateInfo(options, new HalfvecConverter()), isDefault: true); 27 | #endif 28 | mappings.AddType("sparsevec", 29 | static (options, mapping, _) => mapping.CreateInfo(options, new SparsevecConverter()), isDefault: true); 30 | return mappings; 31 | } 32 | } 33 | 34 | sealed class ArrayResolver : Resolver, IPgTypeInfoResolver 35 | { 36 | TypeInfoMappingCollection? _mappings; 37 | new TypeInfoMappingCollection Mappings => _mappings ??= AddMappings(new(base.Mappings)); 38 | 39 | public new PgTypeInfo? GetTypeInfo(Type? type, DataTypeName? dataTypeName, PgSerializerOptions options) 40 | => Mappings.Find(type, dataTypeName, options); 41 | 42 | static TypeInfoMappingCollection AddMappings(TypeInfoMappingCollection mappings) 43 | { 44 | mappings.AddArrayType("vector"); 45 | #if NET5_0_OR_GREATER 46 | mappings.AddArrayType("halfvec"); 47 | #endif 48 | mappings.AddArrayType("sparsevec"); 49 | return mappings; 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Pgvector/Pgvector.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Pgvector 5 | 0.3.2 6 | ankane 7 | pgvector support for Npgsql 8 | MIT 9 | https://github.com/pgvector/pgvector-dotnet 10 | README.md 11 | icon.png 12 | 13 | net6.0;netstandard2.0;net462 14 | enable 15 | latest 16 | NPG9001 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/Pgvector/SparseVector.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.Linq; 5 | 6 | namespace Pgvector; 7 | 8 | public class SparseVector 9 | { 10 | public int Dimensions { get; } 11 | public ReadOnlyMemory Indices { get; } 12 | public ReadOnlyMemory Values { get; } 13 | 14 | // caller must ensure: 15 | // 1. indices are sorted, unique, >= 0, and < dimensions 16 | // 2. values does not contain zeros 17 | public SparseVector(int dimensions, ReadOnlyMemory indices, ReadOnlyMemory values) 18 | { 19 | if (indices.Length != values.Length) 20 | { 21 | throw new ArgumentException("indices and values must be the same length"); 22 | } 23 | 24 | Dimensions = dimensions; 25 | Indices = indices; 26 | Values = values; 27 | } 28 | 29 | public SparseVector(ReadOnlyMemory v) 30 | { 31 | var dense = v.Span; 32 | var count = 0; 33 | var capacity = 0; 34 | 35 | for (var i = 0; i < dense.Length; i++) 36 | capacity += Convert.ToInt32(dense[i] != 0); 37 | 38 | var indices = new int[capacity]; 39 | var values = new float[capacity]; 40 | 41 | for (var i = 0; i < dense.Length; i++) 42 | { 43 | if (dense[i] != 0) 44 | { 45 | indices[count] = i; 46 | values[count] = dense[i]; 47 | count++; 48 | } 49 | } 50 | 51 | Dimensions = v.Length; 52 | Indices = indices; 53 | Values = values; 54 | } 55 | 56 | public SparseVector(IDictionary dictionary, int dimensions) 57 | { 58 | var count = 0; 59 | var capacity = dictionary.Count; 60 | var indices = new int[capacity]; 61 | var values = new float[capacity]; 62 | 63 | foreach (var e in dictionary) 64 | { 65 | if (e.Value != 0) 66 | { 67 | indices[count] = e.Key; 68 | values[count] = e.Value; 69 | count++; 70 | } 71 | } 72 | 73 | Array.Sort(indices, values, 0, count); 74 | 75 | Dimensions = dimensions; 76 | Indices = new ReadOnlyMemory(indices, 0, count); 77 | Values = new ReadOnlyMemory(values, 0, count); 78 | } 79 | 80 | public SparseVector(string s) 81 | { 82 | var parts = s.Split(['/'], 2); 83 | var elements = parts[0].Substring(1, parts[0].Length - 2).Split(','); 84 | var nnz = elements.Length; 85 | var indices = new int[nnz]; 86 | var values = new float[nnz]; 87 | 88 | for (int i = 0; i < nnz; i++) 89 | { 90 | var ep = elements[i].Split([':'], 2); 91 | indices[i] = Int32.Parse(ep[0], CultureInfo.InvariantCulture) - 1; 92 | values[i] = float.Parse(ep[1], CultureInfo.InvariantCulture); 93 | } 94 | 95 | Dimensions = Int32.Parse(parts[1], CultureInfo.InvariantCulture); 96 | Indices = indices; 97 | Values = values; 98 | } 99 | 100 | public override string ToString() 101 | { 102 | var elements = Indices.ToArray().Zip(Values.ToArray(), (i, v) => string.Concat((i + 1).ToString(CultureInfo.InvariantCulture), ":", v.ToString(CultureInfo.InvariantCulture))); 103 | return string.Concat("{", string.Join(",", elements), "}/", Dimensions); 104 | } 105 | 106 | public float[] ToArray() 107 | { 108 | var result = new float[Dimensions]; 109 | var indices = Indices.Span; 110 | var values = Values.Span; 111 | 112 | for (var i = 0; i < indices.Length; i++) 113 | result[indices[i]] = values[i]; 114 | 115 | return result; 116 | } 117 | 118 | public bool Equals(SparseVector? other) 119 | => other is not null && Dimensions == other.Dimensions && Indices.Span.SequenceEqual(other.Indices.Span) && Values.Span.SequenceEqual(other.Values.Span); 120 | 121 | public override bool Equals(object? obj) 122 | => obj is SparseVector vector && Equals(vector); 123 | 124 | public static bool operator ==(SparseVector? x, SparseVector? y) 125 | => (x is null && y is null) || (x is not null && x.Equals(y)); 126 | 127 | public static bool operator !=(SparseVector? x, SparseVector? y) => !(x == y); 128 | 129 | public override int GetHashCode() 130 | { 131 | var hashCode = new HashCode(); 132 | 133 | hashCode.Add(Dimensions); 134 | 135 | var indices = Indices.Span; 136 | for (var i = 0; i < indices.Length; i++) 137 | hashCode.Add(indices[i]); 138 | 139 | var values = Values.Span; 140 | for (var i = 0; i < values.Length; i++) 141 | hashCode.Add(values[i]); 142 | 143 | return hashCode.ToHashCode(); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/Pgvector/Vector.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using System.Linq; 4 | 5 | namespace Pgvector; 6 | 7 | public class Vector : IEquatable 8 | { 9 | public ReadOnlyMemory Memory { get; } 10 | 11 | public Vector(ReadOnlyMemory v) 12 | => Memory = v; 13 | 14 | public Vector(string s) 15 | => Memory = Array.ConvertAll(s.Substring(1, s.Length - 2).Split(','), v => float.Parse(v, CultureInfo.InvariantCulture)); 16 | 17 | public override string ToString() 18 | => string.Concat("[", string.Join(",", Memory.ToArray().Select(v => v.ToString(CultureInfo.InvariantCulture))), "]"); 19 | 20 | public float[] ToArray() 21 | => Memory.ToArray(); 22 | 23 | public bool Equals(Vector? other) 24 | => other is not null && Memory.Span.SequenceEqual(other.Memory.Span); 25 | 26 | public override bool Equals(object? obj) 27 | => obj is Vector vector && Equals(vector); 28 | 29 | public static bool operator ==(Vector? x, Vector? y) 30 | => (x is null && y is null) || (x is not null && x.Equals(y)); 31 | 32 | public static bool operator !=(Vector? x, Vector? y) => !(x == y); 33 | 34 | public override int GetHashCode() 35 | { 36 | var hashCode = new HashCode(); 37 | var span = Memory.Span; 38 | 39 | for (var i = 0; i < span.Length; i++) 40 | hashCode.Add(span[i]); 41 | 42 | return hashCode.ToHashCode(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tests/Pgvector.CSharp.Tests/DapperTests.cs: -------------------------------------------------------------------------------- 1 | using Dapper; 2 | using Pgvector.Dapper; 3 | using System.Collections; 4 | 5 | namespace Pgvector.Tests; 6 | 7 | public class DapperItem 8 | { 9 | public int Id { get; set; } 10 | public Vector? Embedding { get; set; } 11 | public HalfVector? HalfEmbedding { get; set; } 12 | public BitArray? BinaryEmbedding { get; set; } 13 | public SparseVector? SparseEmbedding { get; set; } 14 | } 15 | 16 | public class DapperTests 17 | { 18 | [Fact] 19 | public async Task Main() 20 | { 21 | SqlMapper.AddTypeHandler(new VectorTypeHandler()); 22 | SqlMapper.AddTypeHandler(new HalfvecTypeHandler()); 23 | SqlMapper.AddTypeHandler(new SparsevecTypeHandler()); 24 | 25 | var connString = "Host=localhost;Database=pgvector_dotnet_test"; 26 | 27 | var dataSourceBuilder = new NpgsqlDataSourceBuilder(connString); 28 | dataSourceBuilder.UseVector(); 29 | await using var dataSource = dataSourceBuilder.Build(); 30 | 31 | var conn = dataSource.OpenConnection(); 32 | 33 | conn.Execute("CREATE EXTENSION IF NOT EXISTS vector"); 34 | conn.ReloadTypes(); 35 | 36 | conn.Execute("DROP TABLE IF EXISTS dapper_items"); 37 | conn.Execute("CREATE TABLE dapper_items (id serial PRIMARY KEY, embedding vector(3), halfembedding halfvec(3), binaryembedding bit(3), sparseembedding sparsevec(3))"); 38 | 39 | var embedding1 = new Vector(new float[] { 1, 1, 1 }); 40 | var embedding2 = new Vector(new float[] { 2, 2, 2 }); 41 | var embedding3 = new Vector(new float[] { 1, 1, 2 }); 42 | var halfEmbedding1 = new HalfVector(new Half[] { (Half)1, (Half)1, (Half)1 }); 43 | var halfEmbedding2 = new HalfVector(new Half[] { (Half)2, (Half)2, (Half)2 }); 44 | var halfEmbedding3 = new HalfVector(new Half[] { (Half)1, (Half)1, (Half)2 }); 45 | var binaryEmbedding1 = new BitArray(new bool[] { false, false, false }); 46 | var binaryEmbedding2 = new BitArray(new bool[] { true, false, true }); 47 | var binaryEmbedding3 = new BitArray(new bool[] { true, true, true }); 48 | var sparseEmbedding1 = new SparseVector(new float[] { 1, 1, 1 }); 49 | var sparseEmbedding2 = new SparseVector(new float[] { 2, 2, 2 }); 50 | var sparseEmbedding3 = new SparseVector(new float[] { 1, 1, 2 }); 51 | conn.Execute(@"INSERT INTO dapper_items (embedding, halfembedding, binaryembedding, sparseembedding) VALUES (@embedding1, @halfEmbedding1, @binaryEmbedding1, @sparseEmbedding1), (@embedding2, @halfEmbedding2, @binaryEmbedding2, @sparseEmbedding2), (@embedding3, @halfEmbedding3, @binaryEmbedding3, @sparseEmbedding3)", new { embedding1, halfEmbedding1, binaryEmbedding1, sparseEmbedding1, embedding2, halfEmbedding2, binaryEmbedding2, sparseEmbedding2, embedding3, halfEmbedding3, binaryEmbedding3, sparseEmbedding3 }); 52 | 53 | var embedding = new Vector(new float[] { 1, 1, 1 }); 54 | var items = conn.Query("SELECT * FROM dapper_items ORDER BY embedding <-> @embedding LIMIT 5", new { embedding }).AsList(); 55 | Assert.Equal(new int[] { 1, 3, 2 }, items.Select(v => v.Id).ToArray()); 56 | Assert.Equal(new float[] { 1, 1, 1 }, items[0].Embedding!.ToArray()); 57 | Assert.Equal(new Half[] { (Half)1, (Half)1, (Half)1 }, items[0].HalfEmbedding!.ToArray()); 58 | Assert.Equal(new BitArray(new bool[] { false, false, false }), items[0].BinaryEmbedding!); 59 | Assert.Equal(new float[] { 1, 1, 1 }, items[0].SparseEmbedding!.ToArray()); 60 | 61 | conn.Execute("CREATE INDEX ON dapper_items USING ivfflat (embedding vector_l2_ops) WITH (lists = 100)"); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /tests/Pgvector.CSharp.Tests/EntityFrameworkCoreTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.EntityFrameworkCore.Infrastructure; 3 | using Microsoft.EntityFrameworkCore.Storage; 4 | using Pgvector.EntityFrameworkCore; 5 | using System.Collections; 6 | using System.ComponentModel.DataAnnotations.Schema; 7 | 8 | namespace Pgvector.Tests; 9 | 10 | public class ItemContext : DbContext 11 | { 12 | public DbSet Items { get; set; } 13 | 14 | protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) 15 | { 16 | var connString = "Host=localhost;Database=pgvector_dotnet_test"; 17 | optionsBuilder.UseNpgsql(connString, o => o.UseVector()); 18 | } 19 | 20 | protected override void OnModelCreating(ModelBuilder modelBuilder) 21 | { 22 | modelBuilder.HasPostgresExtension("vector"); 23 | 24 | modelBuilder.Entity() 25 | .HasIndex(i => i.Embedding) 26 | .HasMethod("hnsw") 27 | .HasOperators("vector_l2_ops") 28 | .HasStorageParameter("m", 16) 29 | .HasStorageParameter("ef_construction", 64); 30 | } 31 | } 32 | 33 | [Table("efcore_items")] 34 | public class Item 35 | { 36 | public int Id { get; set; } 37 | 38 | [Column("embedding", TypeName = "vector(3)")] 39 | public Vector? Embedding { get; set; } 40 | 41 | [Column("half_embedding", TypeName = "halfvec(3)")] 42 | public HalfVector? HalfEmbedding { get; set; } 43 | 44 | [Column("binary_embedding", TypeName = "bit(3)")] 45 | public BitArray? BinaryEmbedding { get; set; } 46 | 47 | [Column("sparse_embedding", TypeName = "sparsevec(3)")] 48 | public SparseVector? SparseEmbedding { get; set; } 49 | } 50 | 51 | public class EntityFrameworkCoreTests 52 | { 53 | [Fact] 54 | public async Task Main() 55 | { 56 | await using var ctx = new ItemContext(); 57 | 58 | ctx.Database.ExecuteSql($"DROP TABLE IF EXISTS efcore_items"); 59 | var databaseCreator = ctx.GetService(); 60 | databaseCreator.CreateTables(); 61 | 62 | ctx.Items.Add(new Item { Embedding = new Vector(new float[] { 1, 1, 1 }), HalfEmbedding = new HalfVector(new Half[] { (Half)1, (Half)1, (Half)1 }), BinaryEmbedding = new BitArray(new bool[] { false, false, false }), SparseEmbedding = new SparseVector(new float[] { 1, 1, 1 }) }); 63 | 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 }) }); 64 | 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 }) }); 65 | ctx.SaveChanges(); 66 | 67 | var embedding = new Vector(new float[] { 1, 1, 1 }); 68 | var items = await ctx.Items.FromSql($"SELECT * FROM efcore_items ORDER BY embedding <-> {embedding} LIMIT 5").ToListAsync(); 69 | Assert.Equal(new int[] { 1, 3, 2 }, items.Select(v => v.Id).ToArray()); 70 | Assert.Equal(new float[] { 1, 1, 1 }, items[0].Embedding!.ToArray()); 71 | Assert.Equal(new Half[] { (Half)1, (Half)1, (Half)1 }, items[0].HalfEmbedding!.ToArray()); 72 | Assert.Equal(new BitArray(new bool[] { false, false, false }), items[0].BinaryEmbedding!); 73 | Assert.Equal(new float[] { 1, 1, 1 }, items[0].SparseEmbedding!.ToArray()); 74 | 75 | // vector distance functions 76 | 77 | items = await ctx.Items.OrderBy(x => x.Embedding!.L2Distance(embedding)).Take(5).ToListAsync(); 78 | Assert.Equal(new int[] { 1, 3, 2 }, items.Select(v => v.Id).ToArray()); 79 | Assert.Equal(new float[] { 1, 1, 1 }, items[0].Embedding!.ToArray()); 80 | 81 | items = await ctx.Items.OrderBy(x => x.Embedding!.MaxInnerProduct(embedding)).Take(5).ToListAsync(); 82 | Assert.Equal(new int[] { 2, 3, 1 }, items.Select(v => v.Id).ToArray()); 83 | 84 | items = await ctx.Items.OrderBy(x => x.Embedding!.CosineDistance(embedding)).Take(5).ToListAsync(); 85 | Assert.Equal(3, items[2].Id); 86 | 87 | items = await ctx.Items.OrderBy(x => x.Embedding!.L1Distance(embedding)).Take(5).ToListAsync(); 88 | Assert.Equal(new int[] { 1, 3, 2 }, items.Select(v => v.Id).ToArray()); 89 | 90 | // halfvec distance functions 91 | 92 | var halfEmbedding = new HalfVector(new Half[] { (Half)1, (Half)1, (Half)1 }); 93 | items = await ctx.Items.OrderBy(x => x.HalfEmbedding!.L2Distance(halfEmbedding)).Take(5).ToListAsync(); 94 | Assert.Equal(new int[] { 1, 3, 2 }, items.Select(v => v.Id).ToArray()); 95 | 96 | items = await ctx.Items.OrderBy(x => x.HalfEmbedding!.MaxInnerProduct(halfEmbedding)).Take(5).ToListAsync(); 97 | Assert.Equal(new int[] { 2, 3, 1 }, items.Select(v => v.Id).ToArray()); 98 | 99 | items = await ctx.Items.OrderBy(x => x.HalfEmbedding!.CosineDistance(halfEmbedding)).Take(5).ToListAsync(); 100 | Assert.Equal(3, items[2].Id); 101 | 102 | items = await ctx.Items.OrderBy(x => x.HalfEmbedding!.L1Distance(halfEmbedding)).Take(5).ToListAsync(); 103 | Assert.Equal(new int[] { 1, 3, 2 }, items.Select(v => v.Id).ToArray()); 104 | 105 | // sparsevec distance functions 106 | 107 | var sparseEmbedding = new SparseVector(new float[] { 1, 1, 1 }); 108 | items = await ctx.Items.OrderBy(x => x.SparseEmbedding!.L2Distance(sparseEmbedding)).Take(5).ToListAsync(); 109 | Assert.Equal(new int[] { 1, 3, 2 }, items.Select(v => v.Id).ToArray()); 110 | 111 | items = await ctx.Items.OrderBy(x => x.SparseEmbedding!.MaxInnerProduct(sparseEmbedding)).Take(5).ToListAsync(); 112 | Assert.Equal(new int[] { 2, 3, 1 }, items.Select(v => v.Id).ToArray()); 113 | 114 | items = await ctx.Items.OrderBy(x => x.SparseEmbedding!.CosineDistance(sparseEmbedding)).Take(5).ToListAsync(); 115 | Assert.Equal(3, items[2].Id); 116 | 117 | items = await ctx.Items.OrderBy(x => x.SparseEmbedding!.L1Distance(sparseEmbedding)).Take(5).ToListAsync(); 118 | Assert.Equal(new int[] { 1, 3, 2 }, items.Select(v => v.Id).ToArray()); 119 | 120 | // bit distance functions 121 | 122 | var binaryEmbedding = new BitArray(new bool[] { true, false, true }); 123 | items = await ctx.Items.OrderBy(x => x.BinaryEmbedding!.HammingDistance(binaryEmbedding)).Take(5).ToListAsync(); 124 | Assert.Equal(new int[] { 2, 3, 1 }, items.Select(v => v.Id).ToArray()); 125 | 126 | items = await ctx.Items.OrderBy(x => x.BinaryEmbedding!.JaccardDistance(binaryEmbedding)).Take(5).ToListAsync(); 127 | Assert.Equal(new int[] { 2, 3, 1 }, items.Select(v => v.Id).ToArray()); 128 | 129 | // additional 130 | 131 | items = await ctx.Items 132 | .OrderBy(x => x.Id) 133 | .Where(x => x.Embedding!.L2Distance(embedding) < 1.5) 134 | .ToListAsync(); 135 | Assert.Equal(new int[] { 1, 3 }, items.Select(v => v.Id).ToArray()); 136 | 137 | var neighbors = await ctx.Items 138 | .OrderBy(x => x.Embedding!.L2Distance(embedding)) 139 | .Select(x => new { Entity = x, Distance = x.Embedding!.L2Distance(embedding) }) 140 | .ToListAsync(); 141 | Assert.Equal(new int[] { 1, 3, 2 }, neighbors.Select(v => v.Entity.Id).ToArray()); 142 | Assert.Equal(new double[] { 0, 1, Math.Sqrt(3) }, neighbors.Select(v => v.Distance).ToArray()); 143 | } 144 | 145 | [Theory] 146 | [InlineData(typeof(Vector), null, "vector")] 147 | [InlineData(typeof(Vector), 3, "vector(3)")] 148 | [InlineData(typeof(HalfVector), null, "halfvec")] 149 | [InlineData(typeof(HalfVector), 3, "halfvec(3)")] 150 | [InlineData(typeof(BitArray), null, "bit varying")] 151 | [InlineData(typeof(BitArray), 3, "bit varying(3)")] 152 | [InlineData(typeof(SparseVector), null, "sparsevec")] 153 | [InlineData(typeof(SparseVector), 3, "sparsevec(3)")] 154 | public void ByStoreType(Type type, int? size, string expectedStoreType) 155 | { 156 | using var ctx = new ItemContext(); 157 | var typeMappingSource = ctx.GetService(); 158 | 159 | var typeMapping = typeMappingSource.FindMapping(type, storeTypeName: null, size: size)!; 160 | Assert.Equal(expectedStoreType, typeMapping.StoreType); 161 | Assert.Same(type, typeMapping.ClrType); 162 | Assert.Equal(size, typeMapping.Size); 163 | } 164 | 165 | [Theory] 166 | [InlineData("vector", typeof(Vector), null)] 167 | [InlineData("vector(3)", typeof(Vector), 3)] 168 | [InlineData("halfvec", typeof(HalfVector), null)] 169 | [InlineData("halfvec(3)", typeof(HalfVector), 3)] 170 | [InlineData("bit varying", typeof(BitArray), null)] 171 | [InlineData("bit(3)", typeof(BitArray), 3)] 172 | [InlineData("sparsevec", typeof(SparseVector), null)] 173 | [InlineData("sparsevec(3)", typeof(SparseVector), 3)] 174 | public void ByClrType(string storeType, Type expectedType, int? expectedSize) 175 | { 176 | using var ctx = new ItemContext(); 177 | var typeMappingSource = ctx.GetService(); 178 | 179 | var typeMapping = typeMappingSource.FindMapping(storeType)!; 180 | Assert.Equal(storeType, typeMapping.StoreType); 181 | Assert.Same(expectedType, typeMapping.ClrType); 182 | Assert.Equal(expectedSize, typeMapping.Size); 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /tests/Pgvector.CSharp.Tests/HalfVectorTests.cs: -------------------------------------------------------------------------------- 1 | using Pgvector; 2 | 3 | namespace Pgvector.Tests; 4 | 5 | public class HalfVectorTests 6 | { 7 | [Fact] 8 | public void StringConstructor() 9 | { 10 | var v = new HalfVector("[1,2,3]"); 11 | Assert.Equal("[1,2,3]", v.ToString()); 12 | } 13 | 14 | [Fact] 15 | public void ArrayConstructor() 16 | { 17 | var v = new HalfVector(new Half[] { (Half)1, (Half)2, (Half)3 }); 18 | Assert.Equal(new Half[] { (Half)1, (Half)2, (Half)3 }, v.ToArray()); 19 | } 20 | 21 | [Fact] 22 | public void Equal() 23 | { 24 | var a = new HalfVector(new Half[] { (Half)1, (Half)1, (Half)1 }); 25 | var b = new HalfVector(new Half[] { (Half)1, (Half)1, (Half)1 }); 26 | var c = new HalfVector(new Half[] { (Half)1, (Half)2, (Half)3 }); 27 | 28 | Assert.Equal(a, b); 29 | Assert.NotEqual(a, c); 30 | 31 | Assert.True(a == b); 32 | Assert.False(a == c); 33 | 34 | Assert.False(a != b); 35 | Assert.True(a != c); 36 | 37 | Assert.False(a == null); 38 | Assert.False(null == a); 39 | Assert.True((HalfVector?)null == null); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /tests/Pgvector.CSharp.Tests/NpgsqlTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | 3 | namespace Pgvector.Tests; 4 | 5 | public class NpgsqlTests 6 | { 7 | [Fact] 8 | public async Task Main() 9 | { 10 | var connString = "Host=localhost;Database=pgvector_dotnet_test"; 11 | 12 | var dataSourceBuilder = new NpgsqlDataSourceBuilder(connString); 13 | dataSourceBuilder.UseVector(); 14 | await using var dataSource = dataSourceBuilder.Build(); 15 | 16 | var conn = dataSource.OpenConnection(); 17 | 18 | await using (var cmd = new NpgsqlCommand("CREATE EXTENSION IF NOT EXISTS vector", conn)) 19 | { 20 | await cmd.ExecuteNonQueryAsync(); 21 | } 22 | 23 | conn.ReloadTypes(); 24 | 25 | await using (var cmd = new NpgsqlCommand("DROP TABLE IF EXISTS items", conn)) 26 | { 27 | await cmd.ExecuteNonQueryAsync(); 28 | } 29 | 30 | await using (var cmd = new NpgsqlCommand("CREATE TABLE items (id serial PRIMARY KEY, embedding vector(3), half_embedding halfvec(3), binary_embedding bit(3), sparse_embedding sparsevec(3))", conn)) 31 | { 32 | await cmd.ExecuteNonQueryAsync(); 33 | } 34 | 35 | await using (var cmd = new NpgsqlCommand("INSERT INTO items (embedding, half_embedding, binary_embedding, sparse_embedding) VALUES ($1, $2, $3, $4), ($5, $6, $7, $8), ($9, $10, $11, $12)", conn)) 36 | { 37 | var embedding1 = new Vector(new float[] { 1, 1, 1 }); 38 | var embedding2 = new Vector(new float[] { 2, 2, 2 }); 39 | var embedding3 = new Vector(new float[] { 1, 1, 2 }); 40 | var halfEmbedding1 = new HalfVector(new Half[] { (Half)1, (Half)1, (Half)1 }); 41 | var halfEmbedding2 = new HalfVector(new Half[] { (Half)2, (Half)2, (Half)2 }); 42 | var halfEmbedding3 = new HalfVector(new Half[] { (Half)1, (Half)1, (Half)2 }); 43 | var binaryEmbedding1 = new BitArray(new bool[] { false, false, false }); 44 | var binaryEmbedding2 = new BitArray(new bool[] { true, false, true }); 45 | var binaryEmbedding3 = new BitArray(new bool[] { true, true, true }); 46 | var sparseEmbedding1 = new SparseVector(new float[] { 1, 1, 1 }); 47 | var sparseEmbedding2 = new SparseVector(new float[] { 2, 2, 2 }); 48 | var sparseEmbedding3 = new SparseVector(new float[] { 1, 1, 2 }); 49 | cmd.Parameters.AddWithValue(embedding1); 50 | cmd.Parameters.AddWithValue(halfEmbedding1); 51 | cmd.Parameters.AddWithValue(binaryEmbedding1); 52 | cmd.Parameters.AddWithValue(sparseEmbedding1); 53 | cmd.Parameters.AddWithValue(embedding2); 54 | cmd.Parameters.AddWithValue(halfEmbedding2); 55 | cmd.Parameters.AddWithValue(binaryEmbedding2); 56 | cmd.Parameters.AddWithValue(sparseEmbedding2); 57 | cmd.Parameters.AddWithValue(embedding3); 58 | cmd.Parameters.AddWithValue(halfEmbedding3); 59 | cmd.Parameters.AddWithValue(binaryEmbedding3); 60 | cmd.Parameters.AddWithValue(sparseEmbedding3); 61 | await cmd.ExecuteNonQueryAsync(); 62 | } 63 | 64 | await using (var cmd = new NpgsqlCommand("SELECT * FROM items ORDER BY embedding <-> $1 LIMIT 5", conn)) 65 | { 66 | var embedding = new Vector(new float[] { 1, 1, 1 }); 67 | cmd.Parameters.AddWithValue(embedding); 68 | 69 | await using (var reader = await cmd.ExecuteReaderAsync()) 70 | { 71 | var ids = new List(); 72 | var embeddings = new List(); 73 | var halfEmbeddings = new List(); 74 | var binaryEmbeddings = new List(); 75 | var sparseEmbeddings = new List(); 76 | 77 | while (await reader.ReadAsync()) 78 | { 79 | ids.Add((int)reader.GetValue(0)); 80 | embeddings.Add((Vector)reader.GetValue(1)); 81 | halfEmbeddings.Add((HalfVector)reader.GetValue(2)); 82 | binaryEmbeddings.Add((BitArray)reader.GetValue(3)); 83 | sparseEmbeddings.Add((SparseVector)reader.GetValue(4)); 84 | } 85 | 86 | Assert.Equal(new int[] { 1, 3, 2 }, ids.ToArray()); 87 | Assert.Equal(new float[] { 1, 1, 1 }, embeddings[0].ToArray()); 88 | Assert.Equal(new float[] { 1, 1, 2 }, embeddings[1].ToArray()); 89 | Assert.Equal(new float[] { 2, 2, 2 }, embeddings[2].ToArray()); 90 | Assert.Equal(new Half[] { (Half)1, (Half)1, (Half)1 }, halfEmbeddings[0].ToArray()); 91 | Assert.Equal(new Half[] { (Half)1, (Half)1, (Half)2 }, halfEmbeddings[1].ToArray()); 92 | Assert.Equal(new Half[] { (Half)2, (Half)2, (Half)2 }, halfEmbeddings[2].ToArray()); 93 | Assert.Equal(new BitArray(new bool[] { false, false, false }), binaryEmbeddings[0]); 94 | Assert.Equal(new BitArray(new bool[] { true, true, true }), binaryEmbeddings[1]); 95 | Assert.Equal(new BitArray(new bool[] { true, false, true }), binaryEmbeddings[2]); 96 | Assert.Equal(new float[] { 1, 1, 1 }, sparseEmbeddings[0].ToArray()); 97 | Assert.Equal(new float[] { 1, 1, 2 }, sparseEmbeddings[1].ToArray()); 98 | Assert.Equal(new float[] { 2, 2, 2 }, sparseEmbeddings[2].ToArray()); 99 | } 100 | } 101 | 102 | await using (var cmd = new NpgsqlCommand("CREATE INDEX ON items USING ivfflat (embedding vector_l2_ops) WITH (lists = 100)", conn)) 103 | { 104 | await cmd.ExecuteNonQueryAsync(); 105 | } 106 | 107 | await using (var writer = conn.BeginBinaryImport("COPY items (embedding) FROM STDIN WITH (FORMAT BINARY)")) 108 | { 109 | writer.StartRow(); 110 | writer.Write(new Vector(new float[] { 1, 1, 1 })); 111 | 112 | writer.StartRow(); 113 | writer.Write(new Vector(new float[] { 2, 2, 2 })); 114 | 115 | writer.StartRow(); 116 | writer.Write(new Vector(new float[] { 1, 1, 2 })); 117 | 118 | writer.Complete(); 119 | } 120 | 121 | await using (var cmd = new NpgsqlCommand("SELECT $1", conn)) 122 | { 123 | var embedding = new Vector(new float[16000]); 124 | cmd.Parameters.AddWithValue(embedding); 125 | 126 | await using (var reader = await cmd.ExecuteReaderAsync()) 127 | { 128 | await reader.ReadAsync(); 129 | Assert.Equal(embedding, ((Vector)reader.GetValue(0))); 130 | } 131 | } 132 | 133 | await using (var cmd = new NpgsqlCommand("SELECT $1", conn)) 134 | { 135 | var embedding = new Vector(new float[65536]); 136 | cmd.Parameters.AddWithValue(embedding); 137 | var exception = await Assert.ThrowsAsync(() => cmd.ExecuteReaderAsync()); 138 | Assert.Equal("Value was either too large or too small for a UInt16.", exception.Message); 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /tests/Pgvector.CSharp.Tests/Pgvector.CSharp.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | enable 6 | enable 7 | 8 | false 9 | 10 | 11 | 12 | 13 | 14 | 15 | runtime; build; native; contentfiles; analyzers; buildtransitive 16 | all 17 | 18 | 19 | runtime; build; native; contentfiles; analyzers; buildtransitive 20 | all 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /tests/Pgvector.CSharp.Tests/SparseVectorTests.cs: -------------------------------------------------------------------------------- 1 | using Pgvector; 2 | 3 | namespace Pgvector.Tests; 4 | 5 | public class SparseVectorTests 6 | { 7 | [Fact] 8 | public void StringConstructor() 9 | { 10 | var v = new SparseVector("{1:1,3:2,5:3}/6"); 11 | Assert.Equal("{1:1,3:2,5:3}/6", v.ToString()); 12 | Assert.Equal(new float[] { 1, 0, 2, 0, 3, 0 }, v.ToArray()); 13 | Assert.Equal(6, v.Dimensions); 14 | Assert.Equal(new int[] { 0, 2, 4 }, v.Indices.ToArray()); 15 | Assert.Equal(new float[] { 1, 2, 3 }, v.Values.ToArray()); 16 | } 17 | 18 | [Fact] 19 | public void ArrayConstructor() 20 | { 21 | var v = new SparseVector(new float[] { 1, 0, 2, 0, 3, 0 }); 22 | Assert.Equal(new float[] { 1, 0, 2, 0, 3, 0 }, v.ToArray()); 23 | Assert.Equal(6, v.Dimensions); 24 | Assert.Equal(new int[] { 0, 2, 4 }, v.Indices.ToArray()); 25 | Assert.Equal(new float[] { 1, 2, 3 }, v.Values.ToArray()); 26 | } 27 | 28 | [Fact] 29 | public void DictionaryConstructor() 30 | { 31 | var dictionary = new Dictionary(); 32 | dictionary.Add(2, 2); 33 | dictionary.Add(4, 3); 34 | dictionary.Add(0, 1); 35 | dictionary.Add(3, 0); 36 | var v = new SparseVector(dictionary, 6); 37 | Assert.Equal(new float[] { 1, 0, 2, 0, 3, 0 }, v.ToArray()); 38 | Assert.Equal(6, v.Dimensions); 39 | Assert.Equal(new int[] { 0, 2, 4 }, v.Indices.ToArray()); 40 | Assert.Equal(new float[] { 1, 2, 3 }, v.Values.ToArray()); 41 | } 42 | 43 | [Fact] 44 | public void Equal() 45 | { 46 | var a = new SparseVector(new float[] { 1, 1, 1 }); 47 | var b = new SparseVector(new float[] { 1, 1, 1 }); 48 | var c = new SparseVector(new float[] { 1, 2, 3 }); 49 | 50 | Assert.Equal(a, b); 51 | Assert.NotEqual(a, c); 52 | 53 | Assert.True(a == b); 54 | Assert.False(a == c); 55 | 56 | Assert.False(a != b); 57 | Assert.True(a != c); 58 | 59 | Assert.False(a == null); 60 | Assert.False(null == a); 61 | Assert.True((Vector?)null == null); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /tests/Pgvector.CSharp.Tests/VectorTests.cs: -------------------------------------------------------------------------------- 1 | using Pgvector; 2 | 3 | namespace Pgvector.Tests; 4 | 5 | public class VectorTests 6 | { 7 | [Fact] 8 | public void StringConstructor() 9 | { 10 | var v = new Vector("[1,2,3]"); 11 | Assert.Equal("[1,2,3]", v.ToString()); 12 | } 13 | 14 | [Fact] 15 | public void ArrayConstructor() 16 | { 17 | var v = new Vector(new float[] { 1, 2, 3 }); 18 | Assert.Equal(new float[] { 1, 2, 3 }, v.ToArray()); 19 | } 20 | 21 | [Fact] 22 | public void Equal() 23 | { 24 | var a = new Vector(new float[] { 1, 1, 1 }); 25 | var b = new Vector(new float[] { 1, 1, 1 }); 26 | var c = new Vector(new float[] { 1, 2, 3 }); 27 | 28 | Assert.Equal(a, b); 29 | Assert.NotEqual(a, c); 30 | 31 | Assert.True(a == b); 32 | Assert.False(a == c); 33 | 34 | Assert.False(a != b); 35 | Assert.True(a != c); 36 | 37 | Assert.False(a == null); 38 | Assert.False(null == a); 39 | Assert.True((Vector?)null == null); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /tests/Pgvector.FSharp.Tests/NpgsqlFSharpTests.fs: -------------------------------------------------------------------------------- 1 | module Pgvector.FSharp.Tests 2 | 3 | open Npgsql 4 | open Npgsql.FSharp 5 | open NUnit.Framework 6 | open Pgvector 7 | 8 | type Item = { 9 | Id: int 10 | Embedding: Vector 11 | } 12 | 13 | [] 14 | let Main () = 15 | let dataSourceBuilder = new NpgsqlDataSourceBuilder("Host=localhost;Database=pgvector_dotnet_test") 16 | dataSourceBuilder.UseVector() |> ignore 17 | use dataSource = dataSourceBuilder.Build() 18 | 19 | dataSource 20 | |> Sql.fromDataSource 21 | |> Sql.query "CREATE EXTENSION IF NOT EXISTS vector" 22 | |> Sql.executeNonQuery 23 | |> ignore 24 | 25 | dataSource 26 | |> Sql.fromDataSource 27 | |> Sql.query "DROP TABLE IF EXISTS fsharp_items" 28 | |> Sql.executeNonQuery 29 | |> ignore 30 | 31 | dataSource 32 | |> Sql.fromDataSource 33 | |> Sql.query "CREATE TABLE fsharp_items (id serial PRIMARY KEY, embedding vector(3))" 34 | |> Sql.executeNonQuery 35 | |> ignore 36 | 37 | let embedding1 = new Vector([| 1f; 1f; 1f |]) 38 | let embedding2 = new Vector([| 2f; 2f; 2f |]) 39 | let embedding3 = new Vector([| 1f; 1f; 2f |]) 40 | 41 | let parameter1 = new NpgsqlParameter("", embedding1) 42 | let parameter2 = new NpgsqlParameter("", embedding2) 43 | let parameter3 = new NpgsqlParameter("", embedding3) 44 | 45 | dataSource 46 | |> Sql.fromDataSource 47 | |> Sql.query "INSERT INTO fsharp_items (embedding) VALUES (@embedding1), (@embedding2), (@embedding3)" 48 | |> Sql.parameters [ "embedding1", Sql.parameter parameter1; "embedding2", Sql.parameter parameter2; "embedding3", Sql.parameter parameter3 ] 49 | |> Sql.executeNonQuery 50 | |> ignore 51 | 52 | let embedding = new Vector([| 1f; 1f; 1f |]) 53 | let parameter = new NpgsqlParameter("", embedding) 54 | 55 | let items = 56 | dataSource 57 | |> Sql.fromDataSource 58 | |> Sql.query "SELECT * FROM fsharp_items ORDER BY embedding <-> @embedding LIMIT 5" 59 | |> Sql.parameters [ "embedding", Sql.parameter parameter ] 60 | |> Sql.execute (fun read -> 61 | { 62 | Id = read.int "id" 63 | Embedding = read.fieldValue "embedding" 64 | }) 65 | 66 | Assert.That([| for i in items -> i.Id |], Is.EquivalentTo([| 1; 3; 2 |])) 67 | Assert.That(items[0].Embedding.ToArray(), Is.EquivalentTo([| 1f; 1f; 1f |])) 68 | Assert.That(items[1].Embedding.ToArray(), Is.EquivalentTo([| 1f; 1f; 2f |])) 69 | Assert.That(items[2].Embedding.ToArray(), Is.EquivalentTo([| 2f; 2f; 2f |])) 70 | 71 | dataSource 72 | |> Sql.fromDataSource 73 | |> Sql.query "CREATE INDEX ON fsharp_items USING hnsw (embedding vector_l2_ops)" 74 | |> Sql.executeNonQuery 75 | |> ignore 76 | -------------------------------------------------------------------------------- /tests/Pgvector.FSharp.Tests/Pgvector.FSharp.Tests.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | 6 | false 7 | false 8 | true 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /tests/Pgvector.FSharp.Tests/Program.fs: -------------------------------------------------------------------------------- 1 | module Program = 2 | 3 | [] 4 | let main _ = 0 5 | -------------------------------------------------------------------------------- /tests/Pgvector.VisualBasic.Tests/NpgsqlTests.vb: -------------------------------------------------------------------------------- 1 | Imports Microsoft.VisualStudio.TestTools.UnitTesting 2 | Imports Microsoft.VisualStudio.TestTools.UnitTesting.CollectionAssert 3 | Imports Npgsql 4 | 5 | Namespace Pgvector.VisualBasic.Tests 6 | 7 | Public Class NpgsqlTests 8 | 9 | Sub TestSub() 10 | Dim connString = "Host=localhost;Database=pgvector_dotnet_test" 11 | 12 | Dim dataSourceBuilder As New NpgsqlDataSourceBuilder(connString) 13 | dataSourceBuilder.UseVector() 14 | Dim dataSource = dataSourceBuilder.Build() 15 | 16 | Dim conn = dataSource.OpenConnection() 17 | 18 | Using cmd As New NpgsqlCommand("CREATE EXTENSION IF NOT EXISTS vector", conn) 19 | cmd.ExecuteNonQuery() 20 | End Using 21 | 22 | conn.ReloadTypes() 23 | 24 | Using cmd As New NpgsqlCommand("DROP TABLE IF EXISTS vb_items", conn) 25 | cmd.ExecuteNonQuery() 26 | End Using 27 | 28 | Using cmd As New NpgsqlCommand("CREATE TABLE vb_items (id serial PRIMARY KEY, embedding vector(3))", conn) 29 | cmd.ExecuteNonQuery() 30 | End Using 31 | 32 | Using cmd As New NpgsqlCommand("INSERT INTO vb_items (embedding) VALUES ($1), ($2), ($3)", conn) 33 | Dim embedding1 As New Vector(New Single() {1, 1, 1}) 34 | Dim embedding2 As New Vector(New Single() {2, 2, 2}) 35 | Dim embedding3 As New Vector(New Single() {1, 1, 2}) 36 | cmd.Parameters.AddWithValue(embedding1) 37 | cmd.Parameters.AddWithValue(embedding2) 38 | cmd.Parameters.AddWithValue(embedding3) 39 | cmd.ExecuteNonQuery() 40 | End Using 41 | 42 | Using cmd As New NpgsqlCommand("SELECT * FROM vb_items ORDER BY embedding <-> $1 LIMIT 5", conn) 43 | Dim embedding As New Vector(New Single() {1, 1, 1}) 44 | cmd.Parameters.AddWithValue(embedding) 45 | 46 | Using reader As NpgsqlDataReader = cmd.ExecuteReader() 47 | Dim ids As New List(Of Integer) 48 | Dim embeddings As New List(Of Vector) 49 | 50 | While reader.Read() 51 | ids.Add(reader.GetValue(0)) 52 | embeddings.Add(reader.GetValue(1)) 53 | End While 54 | 55 | CollectionAssert.AreEqual(new Integer() {1, 3, 2}, ids.ToArray()) 56 | CollectionAssert.AreEqual(new Single() {1, 1, 1}, embeddings(0).ToArray()) 57 | CollectionAssert.AreEqual(new Single() {1, 1, 2}, embeddings(1).ToArray()) 58 | CollectionAssert.AreEqual(new Single() {2, 2, 2}, embeddings(2).ToArray()) 59 | End Using 60 | End Using 61 | 62 | Using cmd As New NpgsqlCommand("CREATE INDEX ON vb_items USING hnsw (embedding vector_l2_ops)", conn) 63 | cmd.ExecuteNonQuery() 64 | End Using 65 | End Sub 66 | End Class 67 | End Namespace 68 | -------------------------------------------------------------------------------- /tests/Pgvector.VisualBasic.Tests/Pgvector.VisualBasic.Tests.vbproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Pgvector.VisualBasic.Tests 5 | net9.0 6 | 7 | false 8 | true 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | --------------------------------------------------------------------------------