Recognizers return %Obscura.Analyzer.Result{} values with byte offsets.
Built-ins stay dependency-light, while controlled extension points support
project-specific identifiers.
Built-In Recognizers
The registry supports:
:email:phone:credit_card:us_ssn:iban:ip_address:url:domain
Built-ins remain regex and validator based. Optional NER uses explicit recognizer configuration and is not enabled by default.
The :deterministic_plus evaluation profile adds conservative recognizers for generated-template quality coverage:
- Presidio-style date formats, including ISO timestamps, timezone timestamps, slash/dash/dot dates, and abbreviated month dates.
- Explicit address contexts such as contact blocks, return-address requests, restaurant-address sentences, and moved/lived-at address prompts.
- High-confidence person and location contexts where the surrounding words make boundaries clear.
- URL/domain duplicate handling for generated "posted photo" text, while still preserving explicit URL detection when callers request
entities: [:url].
These additions are intentionally narrow. They improve supported Presidio-Research compatibility without making broad arbitrary name, organization, location, or free-form address regexes the default strategy.
Optional Phone Parser
The default phone recognizer remains dependency-light. For stronger international validation, callers can opt into ex_phone_number through the parser adapter:
Obscura.analyze(text,
entities: [:phone],
phone_parser: Obscura.Recognizer.Phone.ExPhoneNumberValidator
)The dependency is optional:
{:ex_phone_number, "~> 0.4.11", optional: true}Parser-backed candidate scanning is enabled only when :phone_parser or :phone_validator is configured. Without that option, the existing deterministic phone recognizer path is unchanged.
Promoted heldout evidence shows the tradeoff:
- phone recall improves from deterministic
0.6239to filtered parser0.8624 - phone F1 improves from
0.7684to0.8744 - phone false positives increase from
0to12 - overall deterministic-plus F1 improves from
0.6631to0.6721 - compared with the earlier parser policy, phone false positives drop from
39to12
This is why the parser remains opt-in. It is useful for recall-focused or international-phone workflows, but the conservative default avoids the extra false positives.
Benchmark CLI:
mix obscura.eval --compatibility --dataset generated_large \
--profile deterministic_plus \
--full \
--template-split template_heldout \
--phone-parser ex_phone_number
Optional parser settings:
:phone_regions: region list for national numbers, defaulting toUS,GB,DE,FR,IL,IN,CA,BR,JP, andCN.- Plus-prefixed numbers are parsed without a default region.
- Parser candidates are post-filtered with Presidio-like evidence rules: plus-prefixed numbers, extension-bearing numbers, or national numbers with phone context are accepted; date-like, repeated digit, sequential digit, too-short, and junk candidates are rejected.
- Parser metadata records
:validation,:phone_region,:phone_e164, and:phone_number_type.
NER Recognizer
Obscura.Recognizer.NER supports open-class entities with caller-provided serving:
serving =
Obscura.Recognizer.NER.FakeServing.new(%{
"Alice works at Acme." => [
%{label: "PER", start: 0, end: 5, score: 0.94},
%{label: "ORG", start: 15, end: 19, score: 0.91}
]
})
Obscura.analyze("Alice works at Acme.",
entities: [:person, :organization],
recognizers: [{Obscura.Recognizer.NER, serving: serving}]
)See docs/model-backed-recognition.md for label mapping, fake serving, optional analyzer-level NLP engines, Bumblebee/Nx serving hooks, and batch analysis.
Custom Modules
A custom recognizer implements Obscura.Recognizer. The analyzer passes NLP artifacts in opts[:nlp_artifacts], so custom recognizers can use token offsets, normalized tokens, and optional engine-populated model outputs without rebuilding tokenization or invoking model serving independently:
defmodule TicketRecognizer do
@behaviour Obscura.Recognizer
alias Obscura.Analyzer.Result
def name, do: :ticket
def supported_entities, do: [:ticket]
def analyze(text, opts) do
artifacts = Keyword.fetch!(opts, :nlp_artifacts)
for [{start, length}] <- Regex.scan(~r/TKT-\d{4}/, text, return: :index) do
value = binary_part(text, start, length)
%Result{
entity: :ticket,
start: start,
end: start + length,
byte_start: start,
byte_end: start + length,
score: 0.8,
text: value,
source_entity: "TICKET",
recognizer: :ticket,
metadata: %{token_count: length(artifacts.tokens)}
}
end
end
endUse it with analyzer options:
Obscura.analyze("Ticket TKT-1234",
entities: [:ticket],
recognizers: [TicketRecognizer]
)Inline Patterns
Use Obscura.Recognizer.PatternDefinition for project-local regexes without creating a module:
employee_id =
Obscura.Recognizer.PatternDefinition.new!(
name: :employee_id,
entity: :employee_id,
patterns: [%{name: :employee_id_v1, regex: ~r/EMP-\d{6}/, score: 0.65}],
context: ["employee"]
)
Obscura.analyze("employee EMP-123456",
entities: [:employee_id],
recognizers: [employee_id],
context: ["employee"],
explain: true
)Pattern definitions must use compiled regexes and explicit atom entities. Do not create atoms from external user data.
Pattern Validation And Invalidation
Obscura.Recognizer.PatternDefinition supports Presidio-inspired validation hooks:
validatecan accept a match withtrue,:ok,:valid,{:ok, metadata},{:score, score}, or{:ok, score, metadata}.validatecan reject a match withfalse,:invalid, or unsupported return values.invalidatecan reject a match withtrue,:invalid,{:invalid, metadata}, or{:error, reason}.- A pattern can set
invalid_scoreto keep an invalid low-confidence result instead of dropping it.
Pattern maps may also include:
requires_context: truecontext_min_score: 0.55weak: true
Context-required results are emitted as candidates but dropped after context enhancement unless a nearby context word matched. This supports weak patterns such as ZIP-like numbers without treating every five-digit number as PII.
Conflict Resolution
Analyzer conflict resolution is now closer to Presidio's duplicate/contained-span behavior:
- exact duplicate spans are removed
- contained lower-score same-entity spans are removed
- nested different-entity spans are preserved by default
:aggressiveor:prefer_longerkeeps the earlier behavior of dropping any overlap:prefer_higher_confidencekeeps the highest-confidence non-overlapping candidates
The anonymizer still defaults to aggressive overlap removal because replacement operations cannot safely apply overlapping spans without corrupting output offsets.
Deny Lists
Deny lists turn configured values into recognizer results:
Obscura.analyze("Project ORCHID",
entities: [:project_codename],
deny_lists: [
%{entity: :project_codename, values: ["orchid"], case_sensitive: false}
]
)Deny lists are intended for known secrets, project codenames, tenant-specific identifiers, or values supplied by application configuration.
Allow Lists
Allow lists remove known-safe values after recognition:
Obscura.analyze("support@example.com jane@example.com",
entities: [:email],
allow_list: [%{entity: :email, values: ["support@example.com"]}]
)Allow list entries may contain literal values or patterns, scoped by entity when needed.