Mert Özen Her Satırda Daha İleri
Yapay Zeka RAG — #06

EF Core ile Vector Search (1): SQL Server 2025 vector Tipi, VectorDistance ve EF Core 11 VectorSearch

Mert Özen 8 Kas 2026 10 dk 3 görüntülenme
EF Core ile Vector Search (1): SQL Server 2025 vector Tipi, VectorDistance ve EF Core 11 VectorSearch

EF Core 10'da SQL Server 2025 vector kolonu SqlVector<float> property ve HasColumnType("vector(768)") ile map edilir, arama EF.Functions.VectorDistance ile exact yapılır. Kodu derleyip EF Core'un ürettiği gerçek SQL'i inceliyoruz: projection'sız sorgunun vektörü de çektiğini, preview vector index ve VECTOR_SEARCH'ü ve EF Core 11'in getirdiklerini. SQL Server 2025 bu yazı için çalıştırılmadı.

Sürüm notu: Bu yazıdaki kodlar .NET 10 ve EF (Entity Framework) Core 10.0.14 (Microsoft.EntityFrameworkCore.SqlServer, Microsoft.Data.SqlClient 6.1.7) ile derlenmiştir. SQL (Structured Query Language) Server 2025'e bağlanan kısımlar bu yazı için çalıştırılmadı; gösterilen SQL'ler EF Core'un bağlantı açmadan ürettiği gerçek çıktılardır.

Önce Tahmin Et

Bookstore'un chunk'larını SQL Server 2025'te bir DocChunks tablosunda tutuyorsun; her satırda 768 boyutlu bir vector kolonu var. Bir kargo politikası değişti ve o dokümanın chunk'larını db.DocChunks.Where(c => c.DocId == "kargo-ve-teslimat") ile okuyup metinlerine bakmak istiyorsun. EF Core 10'un ürettiği SELECT'te Embedding kolonu var mı? Varsa her satır için kaç byte gereksiz veri taşınıyor?

Cevap yazının ortasında. Önce bu yazının sınırını söyleyeyim: SQL Server 2025 Docker image'ı lisans kabulü (ACCEPT_EULA) gerektirdiği için bu seride çalıştırmıyorum. Kodlar derleniyor ve EF Core'un SQL üretimi gerçek; ama veritabanına giden hiçbir sorgunun sonucunu göstermiyorum. Bilgilerin kaynağı Microsoft Learn'deki SQL Server ve EF Core dokümanları. Aynı işi çalışan bir veritabanıyla, PostgreSQL ve pgvector üzerinde bir sonraki yazıda yapıyoruz.

SQL Server 2025'in vector Tipi

SQL Server 2025, Azure SQL Database ve Azure SQL Managed Instance yerleşik bir vector tipi getiriyor. Değerler optimize edilmiş bir binary biçimde saklanıyor ama dışarıya JSON (JavaScript Object Notation) array olarak görünüyor: '[0.1, 2, 30]'. Varsayılan taban tip float32; bu tipte en fazla 1.998 boyut destekleniyor. Yarı hassasiyetli float16 3.996 boyuta kadar çıkıyor, ama SQL Server 2025'te hâlâ preview ve PREVIEW_FEATURES database scoped configuration'ını açmanı istiyor.

Birkaç kısıt tasarımı doğrudan etkiliyor. Vector kolonu primary key, foreign key ya da unique constraint olamaz; üzerinde B-tree veya columnstore index kurulamaz (yalnız "included column" olabilir). Aritmetik ve karşılaştırma operatörleri yok; iki vektörü yalnız fonksiyonlarla karşılaştırırsın. Boyut sayısı kolon tipiyle sabit: 768 boyutlu bir kolona 256 elemanlı bir array yazmaya çalışırsan dokümana göre 42204 numaralı hatayı alırsın.

İstemci tarafında Microsoft.Data.SqlClient 6.1.0 ile gelen SqlVector<float> tipi vektörü TDS (Tabular Data Stream) protokolünde binary olarak taşıyor; JSON'a çevirip parse etme maliyeti ve hassasiyet kaybı yok. EF Core 10 bu tipi doğrudan map ediyor. Eski sürücüler ise vektörü varchar(max) JSON olarak görmeye devam ediyor.

Entity ve Mapping

Model, yedinci yazıda pgvector'da kuracağımız doc_chunks tablosunun SQL Server karşılığı. Yetki filtresi için Acl, model değişimini yakalamak için EmbeddingModel kolonu da var (ikinci yazıda konuştuğumuz "vektörün kimliği"):

using Microsoft.Data.SqlTypes;
using Microsoft.EntityFrameworkCore;

namespace Bookstore.Rag.SqlServer;

public class DocChunk
{
    public required string Id { get; set; }            // "kargo-ve-teslimat#0"
    public required string DocId { get; set; }
    public required string Section { get; set; }
    public required string Content { get; set; }
    public required string Acl { get; set; }
    public DateOnly UpdatedAt { get; set; }
    public required string EmbeddingModel { get; set; }
    public SqlVector<float> Embedding { get; set; }
}

public class RagDbContext(DbContextOptions<RagDbContext> options) : DbContext(options)
{
    public DbSet<DocChunk> DocChunks => Set<DocChunk>();

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<DocChunk>(e =>
        {
            e.Property(c => c.Id).HasMaxLength(200);
            e.Property(c => c.DocId).HasMaxLength(100);
            e.Property(c => c.Acl).HasMaxLength(20);
            e.Property(c => c.EmbeddingModel).HasMaxLength(100);
            // Boyut embedding modelinden gelir: nomic-embed-text = 768
            e.Property(c => c.Embedding).HasColumnType("vector(768)");
            e.HasIndex(c => new { c.Acl, c.DocId });
        });
    }
}

Embedding'i float[] değil SqlVector<float> olarak tanımlamak önemli; EF Core'un vector tipine map ettiği tip bu. Kolon tipi HasColumnType("vector(768)") ile veriliyor; aynı şeyi [Column(TypeName = "vector(768)")] attribute'uyla da yapabilirsin. Bağlantı açmadan GenerateCreateScript() ile tablonun DDL'ini (Data Definition Language) aldım:

$ dotnet run -- schema
CREATE TABLE [DocChunks] (
    [Id] nvarchar(200) NOT NULL,
    [DocId] nvarchar(100) NOT NULL,
    [Section] nvarchar(max) NOT NULL,
    [Content] nvarchar(max) NOT NULL,
    [Acl] nvarchar(20) NOT NULL,
    [UpdatedAt] date NOT NULL,
    [EmbeddingModel] nvarchar(100) NOT NULL,
    [Embedding] vector(768) NOT NULL,
    CONSTRAINT [PK_DocChunks] PRIMARY KEY ([Id])
);
GO
CREATE INDEX [IX_DocChunks_Acl_DocId] ON [DocChunks] ([Acl], [DocId]);
GO

[Embedding] vector(768) NOT NULL beklediğimiz gibi. Acl ve DocId üzerindeki B-tree index, beşinci yazıdaki pre-filter planının SQL Server'daki karşılığı için var: seçici bir filtrede önce bu index'le satırları daraltmak, sonra yalnız onlar için distance hesaplamak.

Embedding Üretip Kaydetmek

Embedding üretimi veritabanının dışında, IEmbeddingGenerator ile oluyor. Aşağıdaki sınıf derleniyor ama bu yazı için çalıştırılmadı:

public sealed record ChunkInput(string Id, string DocId, string Section, string Content, string Acl, DateOnly UpdatedAt);

public sealed class SqlServerChunkIndexer(RagDbContext db, IEmbeddingGenerator<string, Embedding<float>> embedder)
{
    public async Task IndexAsync(IReadOnlyList<ChunkInput> chunks, CancellationToken ct = default)
    {
        // Tek istekte tüm chunk'lar: Ollama'ya 53 ayrı çağrı yerine bir batch
        GeneratedEmbeddings<Embedding<float>> embeddings =
            await embedder.GenerateAsync(chunks.Select(c => "search_document: " + c.Content), cancellationToken: ct);

        for (int i = 0; i < chunks.Count; i++)
        {
            ChunkInput c = chunks[i];
            Embedding<float> e = embeddings[i];
            if (e.Vector.Length != 768)
            {
                throw new InvalidOperationException($"{c.Id}: expected 768 dimensions, got {e.Vector.Length}");
            }

            db.DocChunks.Add(new DocChunk
            {
                Id = c.Id,
                DocId = c.DocId,
                Section = c.Section,
                Content = c.Content,
                Acl = c.Acl,
                UpdatedAt = c.UpdatedAt,
                EmbeddingModel = e.ModelId ?? "unknown",
                Embedding = new SqlVector<float>(e.Vector)
            });
        }
        await db.SaveChangesAsync(ct);
    }
}

Üç ayrıntı var. Chunk'ların hepsi tek bir GenerateAsync çağrısıyla embed ediliyor; ingestion'da her chunk için ayrı istek atmak gereksiz gecikme demek. Boyut kontrolü kayıttan önce yapılıyor: yanlış boyutlu bir vektörü SQL Server zaten reddeder, ama hatayı hangi chunk'ta olduğunu bilerek kendi kodunda almak daha iyi. Ve EmbeddingModel kolonu modelin döndürdüğü ModelId'den doluyor; kimse elle yazmıyor.

VectorDistance ile Exact Arama

EF Core 10'da arama EF.Functions.VectorDistance ile yapılıyor; SQL Server'ın VECTOR_DISTANCE fonksiyonuna çevriliyor. Metric olarak "cosine", "euclidean" ve "dot" var:

float[] embedding = args.Length > 1
    ? (await new OllamaApiClient(new Uri("http://localhost:11434"), "nomic-embed-text")
        .GenerateVectorAsync("search_query: " + args[1])).ToArray()
    : new float[768];
SqlVector<float> queryVector = new(embedding);

var query = db.DocChunks
    .Where(c => c.Acl == "public" && c.EmbeddingModel == "nomic-embed-text")
    .OrderBy(c => EF.Functions.VectorDistance("cosine", c.Embedding, queryVector))
    .Select(c => new
    {
        c.Id,
        c.Content,
        Distance = EF.Functions.VectorDistance("cosine", c.Embedding, queryVector)
    })
    .Take(5);
Console.WriteLine(query.ToQueryString());
$ dotnet run -- query
DECLARE @p int = 5;
DECLARE @queryVector sql_variant = Microsoft.Data.SqlTypes.SqlVector`1[System.Single];
SELECT TOP(@p) [d].[Id], [d].[Content], VECTOR_DISTANCE('cosine', [d].[Embedding], @queryVector) AS [Distance]
FROM [DocChunks] AS [d]
WHERE [d].[Acl] = N'public' AND [d].[EmbeddingModel] = N'nomic-embed-text'
ORDER BY VECTOR_DISTANCE('cosine', [d].[Embedding], @queryVector)

SQL beklediğimiz gibi: filtre WHERE'de, sıralama VECTOR_DISTANCE ile, TOP(@p) ile 5 satır. Projection sayesinde Embedding kolonu SELECT'e girmiyor; distance ise ayrıca hesaplanıp dönüyor. ToQueryString()'in parametre satırının sql_variant göstermesine aldanma; EF Core serisinin "EF Core 10 Yenilikleri" yazısında da not ettiğimiz gibi bu yalnız ToQueryString()'in gösterimi. SQL Server dokümanı vector tipinin sql_variant'a atanamayacağını açıkça söylüyor; gerçek parametre SqlClient tarafından vector tipiyle gönderiliyor.

Bu bir exact kNN araması: SQL Server filtreden geçen her satır için distance hesaplıyor. Filtre seçiciyse (tek bir tenant'ın birkaç bin chunk'ı) bu tamamen makul; beşinci yazıda pgvector'da gördüğümüz pre-filter planının aynısı. Yüz binlerce satırda ise approximate aramaya, yani vector index'e ihtiyacın var.

Vector Kolonunu Gereksiz Yere Çekmek

Açılıştaki soruya dönelim. Projection yazmadan entity'leri okuyan sorgu:

$ dotnet run -- load-all
SELECT [d].[Id], [d].[Acl], [d].[Content], [d].[DocId], [d].[Embedding], [d].[EmbeddingModel], [d].[Section], [d].[UpdatedAt]
FROM [DocChunks] AS [d]
WHERE [d].[DocId] = N'kargo-ve-teslimat'

Evet, [d].[Embedding] SELECT'in içinde. 768 boyutlu float32 bir vektör 3.072 byte ham veri; metnine bakmak için okuduğun her chunk bunu da beraberinde getiriyor ve EF Core onu change tracker'da tutuyor. 100 chunk'lık bir yönetim ekranı için 300 KB, bir milyon chunk üzerinde çalışan bir ingestion job'ı için 3 GB gereksiz trafik. Çözüm basit: listeleme ve yönetim sorgularında projection kullan, vektöre yalnız gerçekten ihtiyaç duyduğun yerde dokun. EF Core 11 bu davranışı değiştiriyor; bir sonraki bölümde.

VECTOR_SEARCH, Vector Index ve EF Core 11

SQL Server'da approximate arama CREATE VECTOR INDEX ve VECTOR_SEARCH table-valued function'ıyla yapılıyor. Index türü DiskANN; metric olarak cosine, euclidean ve dot destekleniyor. Azure SQL Database'de bu özellikler genel kullanıma açık, SQL Server 2025'te ise preview ve PREVIEW_FEATURES gerektiriyor. Microsoft Learn'deki syntax'a göre bizim tablomuz için T-SQL (Transact-SQL) şöyle olurdu; bu blok çalıştırılmadı:

ALTER DATABASE SCOPED CONFIGURATION SET PREVIEW_FEATURES = ON;

CREATE VECTOR INDEX IX_DocChunks_Embedding
    ON DocChunks (Embedding)
    WITH (METRIC = 'cosine', TYPE = 'diskann');

SELECT TOP (5) WITH APPROXIMATE d.Id, d.Content, s.distance
FROM VECTOR_SEARCH(
        TABLE = DocChunks AS d,
        COLUMN = Embedding,
        SIMILAR_TO = @queryVector,
        METRIC = 'cosine'
     ) AS s
WHERE d.Acl = N'public'
ORDER BY s.distance;

Learn dokümanındaki üç not tasarımını etkiler. Birincisi, vector index en az 100 satırda vektör olan bir tabloda kurulabiliyor; daha azında 42266 hatası dönüyor. İkincisi, index'in güncel sürümü filtreyi aramanın içinde uyguluyor (iterative filtering) ve tabloyu DML'e (Data Manipulation Language) açık bırakıyor; önceki sürümde filtre aramadan sonra uygulanıyordu ve index'li tablo salt okunurdu. Bu yüzden sürüm notlarını ve sys.vector_indexes'teki index sürümünü takip etmen gerekiyor. Üçüncüsü, embedding'lerin neredeyse tamamını yeni bir modelle yeniden ürettiğinde (ikinci yazıdaki model değişimi) index'i silip yeniden kurman öneriliyor; aksi halde arama çalışır ama recall düşebilir.

EF Core tarafında bu sorgunun karşılığı EF Core 11 ile geliyor. Microsoft Learn'deki EF Core vector search sayfasına göre HasVectorIndex(b => b.Embedding, "cosine") model'de vector index tanımlıyor, VectorSearch(...) extension method'u VECTOR_SEARCH'e çevriliyor ve VectorSearchResult<T> ile entity'yi ve Distance'ı birlikte döndürüyor; Take(k)'den sonra çağrılan WithApproximate() da TOP'a WITH APPROXIMATE ekliyor. Aynı sayfa, EF Core 11'den itibaren vector property'lerinin entity sorgularında varsayılan olarak yüklenmediğini söylüyor; yukarıdaki load-all sorgusu EF Core 11'de Embedding'i getirmeyecek. Dokümana göre bu API'ler (Application Programming Interface) ve SQL Server tarafındaki karşılıkları experimental ve değişebilir. EF Core 11, .NET 11 ile 10 Kasım 2026'da çıkıyor; bu yazıyı yazarken NuGet'teki son sürüm 11.0.0-rc.3'tü ve yalnız net11.0'ı hedeflediği için .NET 10 SDK'sı (Software Development Kit) ile derlenemiyor. Bu yüzden bu API'lerin kodunu burada vermiyorum.

Üretimde Dikkat

Bağımlılıklarını tara. Bu projeyi ilk derlediğimde NuGet şu uyarıyı verdi:

$ dotnet list package --vulnerable --include-transitive
Project `EfSqlLab` has the following vulnerable packages
   [net10.0]:
   Transitive Package                       Resolved   Severity   Advisory URL
   > System.Security.Cryptography.Pkcs      9.0.11     High       https://github.com/advisories/GHSA-846f-f764-76mx
                                                       Moderate   https://github.com/advisories/GHSA-2fmv-rv2c-59x7

Paket SqlClient'ın bağımlılık zincirinden geliyor. System.Security.Cryptography.Pkcs'e 10.0.14 sürümünü doğrudan referans verince uyarı kayboldu ve aynı komut "has no vulnerable packages" dedi. EF Core'u güncellemek her zaman transitive bağımlılıkları da güncellemez; dotnet list package --vulnerable --include-transitive'i CI'da (Continuous Integration) çalıştır.

Preview özelliklere production'da bağlanmadan önce tabloyu iki parçaya ayırarak düşün: vector tipi ve VECTOR_DISTANCE SQL Server 2025'te genel kullanımda, vector index ve VECTOR_SEARCH preview. Exact arama ve seçici bir B-tree filtresi orta ölçekli bir RAG (Retrieval-Augmented Generation) için çoğu zaman yeterli; approximate aramaya ihtiyacın olduğunu ölçerek göster, sonra preview'a geç.

Daha önce EFCore.SqlServer.VectorSearch paketini kullandıysan EF Core 10'a geçerken onu projeden kaldır; Learn'e göre EF Core 10'daki yerleşik destek bu extension'ın yerini alıyor. Vektörleri eskiden varbinary ya da JSON string olarak sakladıysan, vector kolonuna taşımak bir veri migration'ı demek; ikinci yazıdaki "yeni kolon, backfill, geçiş" sırası burada da geçerli.

Sık Düşülen Tuzaklar

Birincisi, SQL Server 2022'de denemek. vector tipi ve VECTOR_DISTANCE SQL Server 2025 ile geliyor; EF Core 10'un dokümanı da vector desteğinin yalnız SQL Server 2025 ve üstünde (ve Azure SQL'de) çalıştığını söylüyor. Eski bir sunucuda hata migration anında ya da ilk sorguda çıkar.

İkincisi, dimension'ı modelden farklı tanımlamak. vector(1536) kopyalanmış bir örnekle 768 boyutlu nomic-embed-text vektörü yazmaya çalışırsan her insert hata verir. Boyutu embedding modelinin çıktısından al ve kodda kontrol et.

Üçüncüsü, büyük modellerde float32 sınırını unutmak. 3.072 boyut üreten bir model (ör. text-embedding-3-large) float32'de 1.998 sınırını aşar; ya float16'ya (SQL Server 2025'te preview) ya da modelin destekliyorsa daha kısa bir boyuta geçmen gerekir.

Dördüncüsü, her sorguda tüm vektörü çekmek. EF Core 10'da projection'sız her entity sorgusu Embedding'i de getirir. Listeleme ekranlarında ve ingestion'daki "değişti mi?" kontrollerinde projection kullan.

Beşincisi, ToQueryString()'e güvenip parametre tipini sorun sanmak. sql_variant satırı yalnız gösterim; gerçek davranışı SQL Profiler ya da Extended Events ile doğrula.

Küçük Bir Deneme

★☆☆ load-all sorgusunu bir projection'la (Id, Section, Content) yeniden yaz ve ToQueryString() çıktısında Embedding'in kaybolduğunu doğrula.

★★☆ DocChunk'a Tenant property'si ekle, (Tenant, Acl) üzerinde index tanımla ve GenerateCreateScript() çıktısını incele. Sorgudaki filtreyi tenant'a göre genişlet ve üretilen SQL'e bak.

★★★ SQL Server 2025'i lisans koşullarını kabul edebileceğin bir ortamda çalıştırabiliyorsan, SqlServerChunkIndexer ile 53 chunk'ı yükle, 100 satır sınırını aşmak için sentetik satır ekle ve yukarıdaki T-SQL ile vector index'i kur. Exact VECTOR_DISTANCE ve VECTOR_SEARCH sonuçlarını aynı sorgu vektörüyle karşılaştır.

SQL Server tarafında modeli ve sorguyu kurduk ama çalıştıramadık. Bir sonraki yazıda aynı modeli PostgreSQL ve pgvector üzerinde gerçekten çalıştırıyoruz: Pgvector.EntityFrameworkCore ile mapping, HNSW (Hierarchical Navigable Small World) index, EXPLAIN ile index kullanımı, halfvec ve aynı sorgunun Dapper ile yazılmış hali.