Mettez vos bases vectorielles .NET sous contrat en CI : vérifiez CRUD, recherche, filtres et types avant de promouvoir une application RAG en production.

Tester un fournisseur vectoriel .NET en CI
Une application RAG peut passer tous ses tests unitaires et pourtant échouer dès que le fournisseur de stockage vectoriel change : un filtre est traduit différemment, une clé Guid est sérialisée de manière inattendue, ou l’index n’est pas prêt au moment où la recherche démarre. Ces défauts n’appartiennent ni au modèle de langage ni au prompt. Ils se situent dans le contrat entre le code .NET et la base qui stocke les embeddings.
L’écosystème .NET propose désormais Microsoft.Extensions.VectorData pour séparer l’application des SDK de bases vectorielles. En 2026, les abstractions et le package Microsoft.Extensions.VectorData.ConformanceTests ont rejoint le dépôt dotnet/extensions. Le package de conformité est particulièrement intéressant pour le DevOps : il rend vérifiables, en CI, des comportements qui doivent rester identiques lorsque l’on ajoute ou met à niveau un backend.
Ce tutoriel construit deux niveaux de protection : des tests de contrat applicatifs courts, qui reflètent vos invariants métier, et la suite de conformité .NET pour un fournisseur que vous développez ou intégrez étroitement. Le résultat ne mesure pas la pertinence d’une réponse IA — cet aspect appartient à un corpus d’évaluation — mais empêche une régression de persistance ou de récupération d’atteindre l’environnement de production.
À retenir. Une base en mémoire est pratique pour développer, mais Microsoft déconseille de l’utiliser comme unique test d’un backend de production : elle ne reproduit ni son index, ni ses filtres, ni ses délais de disponibilité. Faites tourner le même contrat contre le vrai moteur dans un environnement éphémère ou de staging.
Ce que le contrat protège — et ce qu’il ne protège pas
Un contrat de stockage vectoriel répond à des questions très concrètes : peut-on créer, écrire, relire et supprimer une collection ? Une recherche renvoie-t-elle le bon enregistrement avec le filtre demandé ? Les clés, champs scalaires et vecteurs ont-ils la forme attendue ? Les résultats sont-ils disponibles avant la promotion ?
Il ne prouve pas qu’un chunk est pertinent, qu’un modèle produit une réponse fidèle ou qu’un seuil de score est bon pour vos utilisateurs. Conservez ces responsabilités séparées : cela évite de rendre une pull request dépendante d’un appel LLM coûteux et variable.
| Niveau | Exemple de contrôle | Exécution recommandée |
|---|---|---|
| Unitaire | transformation d’un document en KnowledgeChunk | chaque pull request, sans réseau |
| Contrat applicatif | CRUD, recherche et filtre contre le backend réel | chaque pull request avec service éphémère |
| Conformité du fournisseur | types, mapping dynamique et comportements communs .NET | changement de provider ou mise à jour majeure |
| Évaluation RAG | documents récupérés et qualité de réponse | environnement contrôlé, budget explicite |

Prérequis et règle de versionnement
Il faut le SDK .NET 10, Docker Desktop ou un environnement de test équivalent pour la base visée, et PowerShell. Le laboratoire utilise xUnit. Épinglez les versions des packages de stockage et des abstractions ensemble : la version 10.9.0 de Microsoft.Extensions.VectorData.ConformanceTests a été publiée en août 2026, en même temps que les abstractions correspondantes.
Créez une solution de tests :
dotnet new sln --name VectorContracts
dotnet new xunit --framework net10.0 --name VectorContracts.Tests
dotnet sln VectorContracts.sln add VectorContracts.Tests
Set-Location VectorContracts.Tests
dotnet add package Microsoft.Extensions.VectorData.Abstractions --version 10.9.0
dotnet add package Microsoft.Extensions.VectorData.ConformanceTests --version 10.9.0
dotnet add package CommunityToolkit.VectorData.InMemory
dotnet restore --use-lock-file
Ajoutez aussi le package officiel du fournisseur réellement déployé. Par exemple, un fournisseur Azure AI Search, PostgreSQL, Qdrant, Redis, SQL Server ou Cosmos DB expose son propre constructeur VectorStore et ses options d’index. Le package de conformité apporte des tests, pas un connecteur de base de données.
Conservez packages.lock.json dans Git, puis utilisez --locked-mode en CI. Les abstractions vectorielles évoluent rapidement ; une restauration non verrouillée peut changer le mapping des propriétés ou une dépendance de test sans changement de code dans la pull request.
1. Définir un petit contrat métier stable
Commencez par le modèle que votre application écrit réellement. Une représentation explicite évite de cacher la dimension et l’algorithme de distance dans une configuration distante.
Créez KnowledgeChunk.cs :
using Microsoft.Extensions.VectorData;
namespace VectorContracts.Tests;
public sealed class KnowledgeChunk
{
[VectorStoreKey]
public Guid Id { get; init; }
[VectorStoreData(IsIndexed = true)]
public string TenantId { get; init; } = "";
[VectorStoreData]
public string Source { get; init; } = "";
[VectorStoreVector(
dimensions: 3,
DistanceFunction = DistanceFunction.CosineSimilarity)]
public ReadOnlyMemory<float> Embedding { get; init; }
}
La dimension 3 rend l’exemple lisible et déterministe. En production, elle doit être celle du modèle d’embeddings sélectionné. Un changement de modèle ou de dimension impose normalement une nouvelle collection ou un nouvel index : mélanger des vecteurs incompatibles n’est pas une migration sûre.
Ne générez pas de vrais embeddings dans ce test. Les vecteurs [1, 0, 0] et [0.99f, 0.01f, 0] suffisent à exprimer un ordre de similarité. Cela rend le signal de CI reproductible, gratuit et indépendant des quotas d’un fournisseur IA.
2. Isoler le bootstrap propre au fournisseur
Les opérations sont portables grâce à VectorStoreCollection<TKey, TRecord> ; le raccordement au moteur, lui, ne l’est pas. Encapsulez ce raccordement derrière une fabrique de test. Le seul fichier à adapter lorsque vous changez de backend est alors celui-ci.
using Microsoft.Extensions.VectorData;
namespace VectorContracts.Tests;
public interface IVectorCollectionFactory : IAsyncDisposable
{
ValueTask<VectorStoreCollection<Guid, KnowledgeChunk>> CreateAsync(
string collectionName,
CancellationToken cancellationToken = default);
}
Pour vérifier localement la mécanique du contrat, voici une implémentation exécutable avec le fournisseur en mémoire. Elle valide le code de test, mais ne constitue pas la validation d’un backend de production : remplacez-la dans la CI par une fabrique qui construit le VectorStore du SDK réellement déployé.
using CommunityToolkit.VectorData.InMemory;
using Microsoft.Extensions.VectorData;
namespace VectorContracts.Tests;
public sealed class InMemoryCollectionFactory : IVectorCollectionFactory
{
private readonly InMemoryVectorStore _store = new();
public ValueTask<VectorStoreCollection<Guid, KnowledgeChunk>> CreateAsync(
string collectionName,
CancellationToken cancellationToken = default)
=> ValueTask.FromResult(_store.GetCollection<Guid, KnowledgeChunk>(collectionName));
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
}
Votre implémentation de CI crée le VectorStore du SDK choisi puis renvoie une collection nommée à partir de collectionName. Transmettez-lui l’URL du conteneur éphémère ou du service de staging par variable d’environnement. Ne codez ni mot de passe, ni chaîne de connexion, ni nom d’index de production dans les tests.
Le nom doit être unique par exécution. Dans GitHub Actions, GITHUB_RUN_ID convient ; localement, un GUID suffit :
string collectionName = $"contract-{Environment.GetEnvironmentVariable("GITHUB_RUN_ID")
?? Guid.NewGuid().ToString("N")}";
Cette isolation évite que deux jobs parallèles effacent mutuellement leurs données. Elle rend aussi la suppression finale non négociable : une suite de tests ne doit jamais posséder le droit de supprimer une collection durable.
3. Écrire des tests de contrat applicatifs exécutables
Le test suivant est un vrai test xUnit. Il ne dépend d’aucun modèle, mais il détecte les régressions les plus fréquentes : cycle de vie de collection, récupération de vecteur et filtrage d’un tenant. Injectez dans VectorStoreContractTests l’implémentation de fabrique de votre fournisseur.
using Microsoft.Extensions.VectorData;
namespace VectorContracts.Tests;
public sealed class VectorStoreContractTests(VectorStoreFixture fixture)
: IAsyncLifetime, IClassFixture<VectorStoreFixture>
{
private VectorStoreCollection<Guid, KnowledgeChunk> _collection = null!;
public async ValueTask InitializeAsync()
{
_collection = await fixture.Factory.CreateAsync(
$"contract-{Guid.NewGuid():N}");
await _collection.EnsureCollectionExistsAsync();
}
public async ValueTask DisposeAsync()
{
await _collection.EnsureCollectionDeletedAsync();
await fixture.Factory.DisposeAsync();
}
[Fact]
public async Task Upsert_get_and_delete_preserve_the_record()
{
var chunk = new KnowledgeChunk
{
Id = Guid.Parse("603840bf-cf91-4521-8b8e-8b6a2e75910a"),
TenantId = "tenant-a",
Source = "runbook.md",
Embedding = new float[] { 1, 0, 0 }
};
await _collection.UpsertAsync(chunk);
KnowledgeChunk? loaded = await _collection.GetAsync(
chunk.Id,
new VectorStoreRecordGetOptions { IncludeVectors = true });
Assert.NotNull(loaded);
Assert.Equal(chunk.TenantId, loaded.TenantId);
Assert.Equal(chunk.Source, loaded.Source);
Assert.Equal(chunk.Embedding.ToArray(), loaded.Embedding.ToArray());
await _collection.DeleteAsync(chunk.Id);
Assert.Null(await _collection.GetAsync(chunk.Id));
}
[Fact]
public async Task Search_never_crosses_the_tenant_filter()
{
await _collection.UpsertAsync(
[
new()
{
Id = Guid.NewGuid(), TenantId = "tenant-a", Source = "a.md",
Embedding = new float[] { 1, 0, 0 }
},
new()
{
Id = Guid.NewGuid(), TenantId = "tenant-b", Source = "b.md",
Embedding = new float[] { 0.99f, 0.01f, 0 }
}
]);
var options = new VectorSearchOptions<KnowledgeChunk>
{
Filter = record => record.TenantId == "tenant-a"
};
var results = new List<VectorSearchResult<KnowledgeChunk>>();
await foreach (VectorSearchResult<KnowledgeChunk> result in _collection.SearchAsync(
new float[] { 1, 0, 0 }, top: 5, options))
{
results.Add(result);
}
Assert.NotEmpty(results);
Assert.All(results, result => Assert.Equal("tenant-a", result.Record.TenantId));
}
}
public sealed class VectorStoreFixture : IAsyncLifetime
{
public IVectorCollectionFactory Factory { get; } =
new InMemoryCollectionFactory();
public ValueTask InitializeAsync() => ValueTask.CompletedTask;
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
}
Selon le moteur, l’index peut être asynchrone. Une lecture immédiatement après UpsertAsync peut donc être légitimement vide pendant quelques secondes. N’ajoutez pas un Thread.Sleep arbitraire : faites porter à IVectorCollectionFactory une attente bornée et observable, par exemple WaitForIndexAsync, qui interroge l’état du service ou relance la lecture jusqu’à un délai explicite. En cas de dépassement, le log doit indiquer la collection, le backend et le délai, sans afficher le contenu sensible des chunks.
La filtration par tenant est volontairement traitée comme un contrat de sécurité, pas comme une optimisation de recherche. Testez aussi vos ACL, votre région ou votre classification de document si ces champs participent à la requête. Un score voisin ne doit jamais permettre à un résultat non autorisé d’entrer dans le contexte du modèle.
4. Ajouter la suite de conformité de Microsoft
Les tests ci-dessus protègent votre application. Microsoft.Extensions.VectorData.ConformanceTests s’adresse surtout au mainteneur d’un fournisseur ou à une équipe qui possède l’intégration : la suite vérifie les comportements communs des abstractions .NET, notamment les clés, les types d’embeddings, le mapping typé et dynamique, les opérations de collection et la recherche.
Le principe est d’hériter des classes de test fournies et de brancher une fixture qui sait créer votre TestStore. La structure minimale ressemble à ceci :
using VectorData.ConformanceTests.Support;
using VectorData.ConformanceTests.TypeTests;
namespace MyVectorProvider.Tests;
public sealed class ProviderKeyTypeTests(ProviderFixture fixture)
: KeyTypeTests(fixture), IClassFixture<ProviderFixture>
{
}
public sealed class ProviderFixture : KeyTypeTests.Fixture
{
public override TestStore TestStore { get; } = new ProviderTestStore();
}
ProviderTestStore est le point d’adaptation du fournisseur : il démarre ou référence le service de test, construit son VectorStore, choisit le type de clé et expose les capacités réellement disponibles. Ne forcez pas artificiellement une capacité. Si un backend ne peut pas comparer exactement les vecteurs, ou nécessite une attente d’indexation, exprimez ce comportement dans le TestStore prévu par la suite ; c’est précisément ce qui rend les différences visibles et revues.
La suite officielle contient des fixtures pour les collections, des tests de types et des tests de clés. Elle attend par exemple qu’une clé Guid soit prise en charge, y compris lorsque le fournisseur la stocke sous forme de chaîne. Depuis la mise à jour 10.8.2, elle s’appuie sur xUnit 3 ; laissez donc le package piloter la version transitive de xUnit ou alignez explicitement vos dépendances avant de mélanger une suite existante en xUnit 2.
Important. Ne copiez pas le code source de la suite dans votre dépôt. Référencez le package, dérivez les tests publics et conservez votre adaptation très mince. Vous profiterez ainsi des nouvelles vérifications lors des mises à jour contrôlées.
5. Exécuter le contrat dans GitHub Actions
Une pull request peut démarrer un conteneur du moteur de stockage puis exécuter les tests contre lui. Remplacez your-vector-image et les variables par les valeurs officielles de votre fournisseur. La commande de santé doit vérifier le service, pas seulement le processus Docker.
Créez .github/workflows/vector-contracts.yml :
name: Vector store contracts
on:
pull_request:
paths:
- "src/**"
- "tests/VectorContracts.Tests/**"
- ".github/workflows/vector-contracts.yml"
workflow_dispatch:
permissions:
contents: read
jobs:
contract:
runs-on: ubuntu-latest
timeout-minutes: 12
services:
vector-store:
image: your-vector-image:your-pinned-version
ports:
- 6333:6333
options: >-
--health-cmd "your-health-command"
--health-interval 5s
--health-timeout 3s
--health-retries 20
env:
VECTOR_STORE_ENDPOINT: http://localhost:6333
VECTOR_STORE_DATABASE: contract
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: 10.0.x
- name: Restore locked dependencies
run: dotnet restore VectorContracts.sln --locked-mode
- name: Run vector-store contract tests
run: >-
dotnet test VectorContracts.sln --no-restore --configuration Release
--logger "trx;LogFileName=vector-contracts.trx"
- name: Upload test report
if: always()
uses: actions/upload-artifact@v4
with:
name: vector-contracts-${{ github.run_id }}
path: "**/TestResults/vector-contracts.trx"
if-no-files-found: error
Épinglez l’image avec un tag immuable ou un digest après l’avoir validée. Une image latest rend impossible de distinguer un défaut applicatif d’un changement de serveur. Limitez aussi les permissions : cette tâche n’a besoin ni de id-token: write, ni d’accès à Azure, ni de secrets de production. Si un backend managé est indispensable, utilisez un projet cloud de test dédié, des droits minimaux et une collection à durée de vie limitée.
Une promotion par paliers
Le test de pull request doit rester rapide et déterministe. Ajoutez ensuite une validation de compatibilité plus large lors d’une mise à jour de fournisseur ou avant une promotion de production : même version de schéma, volume raisonnable de données synthétiques, recherche avec filtres représentatifs et métriques de latence.
pull request
-> tests .NET sans modèle ni secret de production
-> base vectorielle éphémère et contrat CRUD/recherche/ACL
-> artefact TRX
-> validation staging avec la version d’index prévue
-> approbation humaine et promotion
L’environnement de staging ne doit pas être un prétexte pour injecter des données de production. Utilisez des documents fictifs mais structurés comme les vrais, avec plusieurs tenants, des sources retirées et des vecteurs qui produisent des voisins volontairement proches. Les cas « presque bons » détectent mieux un filtre oublié qu’un jeu composé d’un seul document.
Points de vigilance
- Consistance éventuelle : configurez une attente bornée après les écritures et exportez sa durée comme métrique. Une boucle infinie masque une indisponibilité.
- Dimensions et distance : validez la dimension attendue, la distance choisie et les scores seulement lorsque le moteur documente leur échelle. Comparez le rang et l’autorisation avant de fixer un seuil numérique.
- Nettoyage : la fixture doit supprimer uniquement les collections préfixées par le run de test. N’utilisez jamais une variable qui peut désigner une collection de production.
- Données et journaux : les textes de chunks, embeddings, URL signées et chaînes de connexion ne doivent pas être joints aux rapports TRX ou aux artefacts.
- Mises à jour : faites évoluer
Microsoft.Extensions.VectorData.Abstractions, le fournisseur et les tests de conformité dans une branche dédiée. Consultez les breaking changes, notamment les paramètres nommés des attributs, puis exécutez le contrat avant le merge. - Portabilité réelle : une API commune ne signifie pas que tous les index, filtres, types et performances sont interchangeables. Documentez ce que votre application exige plutôt que de supposer un plus petit dénominateur commun.
Le bon garde-fou pour une application RAG
Les abstractions .NET réduisent le couplage au stockage vectoriel ; les tests de contrat empêchent que cette souplesse devienne une promesse non vérifiée. Gardez quelques scénarios métier très lisibles près de l’application, complétez-les par la suite de conformité lorsque vous maintenez l’intégration, puis exécutez le tout contre un vrai moteur isolé dans la CI.
Ce dispositif ne remplace ni l’évaluation de la récupération ni la gouvernance des données. Il établit toutefois une base indispensable : avant de demander à un modèle de raisonner sur un contexte, assurez-vous que votre plateforme stocke, retrouve et isole ce contexte comme vous l’avez décidé.
Sources
- Bases vectorielles pour applications IA .NET — Microsoft Learn
- Utiliser des magasins vectoriels dans une application IA .NET — Microsoft Learn
- Intégrations de magasins vectoriels du Microsoft Agent Framework — Microsoft Learn
- Microsoft.Extensions.VectorData.ConformanceTests 10.9.0 — NuGet
- Notes de version
dotnet/extensions10.9.0 — GitHub - Fixtures de la suite de conformité — source .NET
