Skip to content

Context Service

What is a context?

Every call to Filter requires a context name — an arbitrary string that groups related filtering operations together. The same context name should be used for all documents or text fragments that belong to the same logical session, user, or case.

result, err := svc.Filter(pol, "patient-record-42", input)

The context name is stored in the Context field of each returned Span and in FilterResult.Context, so you can trace back which context produced a given span.


Referential integrity

Referential integrity means that the same PII token always receives the same replacement value within the same context. Without it, a value like 123-45-6789 could be redacted as {{{REDACTED-ssn}}} in one sentence but as a different token in another, making it impossible to correlate occurrences across a document or session.

The ContextService achieves referential integrity by acting as a store keyed by:

context name → token → replacement

When a token is seen for the first time in a context it is stored together with its generated replacement. On subsequent encounters the stored replacement is returned instead of generating a new one.


ContextService interface

type ContextService interface {
    // Get returns the replacement for the given token in the given context.
    // The second return value is false when no entry exists.
    Get(context, token string) (string, bool)

    // Put stores a token → replacement mapping under the given context.
    Put(context, token, replacement string)
}

Default: InMemoryContextService

InMemoryContextService is the built-in implementation. It stores all data in a map[string]map[string]string protected by a sync.RWMutex, making it safe for concurrent use.

It is used automatically when you create a FilterService with NewFilterService:

svc := services.NewFilterService(pol)
// uses InMemoryContextService internally

Custom ContextService

To supply your own implementation — for example, a Redis-backed store for distributed or multi-process deployments — implement the ContextService interface and pass it to NewFilterServiceWithContext:

package main

import (
    "github.com/philterd/go-phileas/pkg/policy"
    "github.com/philterd/go-phileas/pkg/services"
)

// RedisContextService is an example custom implementation.
type RedisContextService struct {
    // ... Redis client fields ...
}

func (r *RedisContextService) Get(context, token string) (string, bool) {
    // retrieve from Redis
    return "", false
}

func (r *RedisContextService) Put(context, token, replacement string) {
    // store in Redis
}

func main() {
    pol := &policy.Policy{
        Identifiers: policy.Identifiers{
            SSN: &policy.SSNFilter{},
        },
    }

    redisSvc := &RedisContextService{ /* ... */ }
    svc := services.NewFilterServiceWithContext(pol, redisSvc)

    result, err := svc.Filter(pol, "session-abc", "My SSN is 123-45-6789.")
    if err != nil {
        panic(err)
    }

    _ = result
}

Multiple contexts

Different context names are fully isolated. The same token can have a different replacement in each context:

svc := services.NewFilterService(pol)

// Context A
r1, _ := svc.Filter(pol, "ctx-a", "SSN: 123-45-6789")

// Context B — independent store; replacement may differ
r2, _ := svc.Filter(pol, "ctx-b", "SSN: 123-45-6789")

_ = r1
_ = r2

Contexts are never automatically cleared from InMemoryContextService. If long-running processes accumulate many contexts, consider supplying a custom implementation with eviction logic (e.g., LRU or TTL).