Skip to content

Blind Index Recompute ​

When you rotate an HMAC key for a blind index, all existing companion column values become stale - queries against the new key won't match rows written with the old key. The recompute services in Tayra.EFCore and Tayra.Marten rebuild these companion columns in batches.

When to Recompute ​

Recomputation is required after:

  • HMAC key rotation - the existing companion values were computed with the old key
  • Transform changes - you added, removed, or reordered transforms on a [BlindIndex] attribute
  • Initial adoption - you added blind indexes to an entity type that already has data

EF Core ​

Service Registration ​

cs
services.AddTayra(opts => opts.LicenseKey = licenseKey)
    .UsePostgreSqlKeyStore(connectionString);

services.AddTayraEFCore();

// Registers IBlindIndexRecomputeService (and the data migration service)
services.AddTayraEFCoreMigrations();
anchor

Running a Recompute ​

cs
var recompute = sp.GetRequiredService<IBlindIndexRecomputeService>();

var result = await recompute.RecomputeAsync<Customer>(
    dbContext,
    batchSize: 500);

Console.WriteLine($"Scanned: {result.TotalScanned}");
Console.WriteLine($"Recomputed: {result.Recomputed}");
Console.WriteLine($"Skipped: {result.Skipped}");
Console.WriteLine($"Errors: {result.Errors}");
Console.WriteLine($"Duration: {result.Duration}");
anchor

The service:

  1. Loads entities in batches of batchSize (default 100)
  2. For each entity, decrypts PII fields (via EF Core interceptors)
  3. Calls IBlindIndexer.ComputeIndexesAsync with the current HMAC key
  4. Saves the updated companion columns

Example: Full Rotation Workflow ​

cs
// 1. Generate new HMAC key for the scope (replaces bi:customer_email in the key store).
//    Pass the scope name: the provider adds the "bi:" prefix itself.
await blindIndexKeyProvider.RotateKeyAsync("customer_email");

// 2. Recompute all Customer blind indexes with the new key
var result = await recompute.RecomputeAsync<Customer>(dbContext, batchSize: 500);

// 3. Verify
Console.WriteLine($"Recomputed {result.Recomputed} of {result.TotalScanned} customers");
anchor

Example: Per-Tenant Keys ​

When blind index keys are partitioned by tenant, rotate one tenant's key and recompute; the recompute takes each row's tenant from its subject id, so it can run over the whole table:

cs
await blindIndexKeyProvider.RotateKeyForTenantAsync("customer_email", "orgA");
var result = await recompute.RecomputeAsync<Customer>(dbContext, batchSize: 500);
anchor

The same recompute is how existing data moves to per-tenant keys when you first turn on PartitionBlindIndexKeys, or away from a workaround that put the tenant into the indexed value.

Marten ​

Service Registration ​

cs
services.AddMarten(opts =>
{
    opts.Connection(connectionString);
});
services.UseTayra();

// Registers ITayraMartenBlindIndexRecomputeService (and the migration services)
services.AddTayraMartenMigrations();
anchor

Running a Recompute ​

cs
var recompute = sp.GetRequiredService<ITayraMartenBlindIndexRecomputeService>();

var result = await recompute.RecomputeAsync<Customer>(
    documentStore,
    batchSize: 500,
    tenantId: null);   // or specify tenant for multi-tenant stores

Console.WriteLine($"Recomputed: {result.Recomputed}/{result.TotalScanned}");
anchor

The Marten service follows the same pattern as EF Core but operates on Marten document sessions.

Array Companions ​

[ArrayBlindIndex] companions are recomputed automatically by both the EF Core and Marten services - there is no array-specific call. For each entity the service reads the source collection and re-hashes every element under the new key, preserving the collection shape and any null slots. A single RecomputeAsync<T> call updates scalar, compound, and array companions on the type in one pass.

cs
// User has both [BlindIndex] Email and [ArrayBlindIndex] Emails;
// one recompute call rebuilds every companion under the rotated key.
var result = await recompute.RecomputeAsync<User>(dbContext, batchSize: 500);
anchor

Automatic recompute is available for EF Core and Marten. MongoDB does not yet have a recompute service - re-hash and re-save affected documents yourself after rotating a MongoDB-backed HMAC key.

Result ​

Both services return a BlindIndexRecomputeResult:

PropertyTypeDescription
TotalScannedintTotal entities/documents loaded
RecomputedintEntities with updated blind indexes
SkippedintEntities with no blind indexes or null values
ErrorsintEntities that failed during recomputation
DurationTimeSpanTotal elapsed time

Operational Guidance ​

Batch Size ​

Choose a batch size based on your data volume and database capacity:

Data volumeRecommended batch size
< 10,000 rows500-1,000
10,000-100,000 rows100-500
> 100,000 rows50-100

Larger batches reduce round-trips but increase memory usage and transaction duration.

Downtime Strategy ​

Between rotating the key and completing the recompute, blind index queries will not find rows with stale companion values. Two strategies:

Maintenance Window (simplest)

  1. Take the application offline or disable search
  2. Rotate the HMAC key
  3. Run the recompute
  4. Bring the application back online

Dual-Read (zero downtime)

  1. Store the old HMAC key under a backup name (e.g., bi:customer_email:v1)
  2. Rotate the HMAC key to a new value
  3. Start the recompute in the background
  4. During the recompute, query with both old and new keys and merge results
  5. After the recompute completes, remove the dual-read path and delete the old key

CLI Integration ​

The dotnet tayra CLI does not yet include a built-in recompute command. Run recomputes programmatically via the services above, or wrap them in a hosted service:

cs
public class BlindIndexRecomputeJob(
    IBlindIndexRecomputeService recompute,
    IServiceScopeFactory scopeFactory) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        // DbContext is scoped, so resolve it from a scope rather than
        // injecting it into this singleton hosted service.
        await using var scope = scopeFactory.CreateAsyncScope();
        var db = scope.ServiceProvider.GetRequiredService<MyDbContext>();

        var result = await recompute.RecomputeAsync<Customer>(db, batchSize: 200, stoppingToken);
        // Log result, send notification, etc.
    }
}
anchor

Monitoring ​

The tayra_blind_index_recomputed_total OpenTelemetry metric tracks recompute operations. The pre-built Grafana dashboard includes a panel for this metric.

See Also ​