Skip to content

Commit f8b45e6

Browse files
fix(pii): derive spaCy-NER set from registry + skip fast path when score_threshold set
1 parent e2f5e48 commit f8b45e6

1 file changed

Lines changed: 29 additions & 11 deletions

File tree

apps/pii/server.py

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
PlPeselRecognizer,
4141
SgFinRecognizer,
4242
SgUenRecognizer,
43+
SpacyRecognizer,
4344
UkNinoRecognizer,
4445
)
4546
from presidio_anonymizer import AnonymizerEngine
@@ -174,10 +175,18 @@ def build_analyzer() -> AnalyzerEngine:
174175
batch_analyzer = BatchAnalyzerEngine(analyzer_engine=analyzer)
175176
anonymizer = AnonymizerEngine()
176177

177-
# The 4 spaCy-NER entity types. A request that names entities but none of these
178-
# needs no NLP pass — the regex/checksum recognizers match on the raw text. The
179-
# block-output redaction stage is restricted to exactly this case (regex-only).
180-
NER_ENTITIES = frozenset({"PERSON", "LOCATION", "NRP", "DATE_TIME"})
178+
# Every entity the spaCy NER recognizers can produce. A request touching any of
179+
# these must run spaCy; a request naming only non-NER (regex/checksum) entities can
180+
# skip it. Derived from the live registry so it stays authoritative if Presidio's
181+
# default entity set changes (e.g. ORGANIZATION), unioned with a known floor so an
182+
# unexpectedly empty derivation can never let an NER request skip the NLP pass.
183+
_SPACY_NER_FLOOR = frozenset({"PERSON", "LOCATION", "NRP", "DATE_TIME", "ORGANIZATION"})
184+
NER_ENTITIES = _SPACY_NER_FLOOR | frozenset(
185+
entity
186+
for recognizer in analyzer.registry.recognizers
187+
if isinstance(recognizer, SpacyRecognizer)
188+
for entity in recognizer.supported_entities
189+
)
181190

182191
# One blank NlpArtifacts per language, built once at startup. Passing these to
183192
# analyze() skips nlp_engine.process_text (the spaCy tok2vec+ner pass) entirely:
@@ -192,10 +201,17 @@ def build_analyzer() -> AnalyzerEngine:
192201
}
193202

194203

195-
def _regex_only(entities: list[str] | None) -> bool:
196-
"""True when a request names entities and none are spaCy-NER, so the spaCy NLP
197-
pass can be skipped."""
198-
return bool(entities) and NER_ENTITIES.isdisjoint(entities)
204+
def _regex_only(entities: list[str] | None, score_threshold: float | None) -> bool:
205+
"""True when the spaCy NLP pass can be skipped: the request names entities, none
206+
require spaCy NER, and no positive score_threshold is set. The blank-artifacts
207+
fast path drops context-based score boosting, which can only change what is
208+
returned when a threshold gates a match between its base and context-boosted
209+
score — so fall back to the full path whenever a threshold is in play."""
210+
return (
211+
bool(entities)
212+
and NER_ENTITIES.isdisjoint(entities)
213+
and (score_threshold is None or score_threshold <= 0)
214+
)
199215

200216

201217
def _analyze_one(
@@ -207,7 +223,9 @@ def _analyze_one(
207223
):
208224
# Regex-only requests reuse a blank NlpArtifacts to skip the spaCy NLP pass;
209225
# otherwise analyze() computes artifacts (runs spaCy) as usual.
210-
nlp_artifacts = _BLANK_ARTIFACTS.get(language) if _regex_only(entities) else None
226+
nlp_artifacts = (
227+
_BLANK_ARTIFACTS.get(language) if _regex_only(entities, score_threshold) else None
228+
)
211229
return analyzer.analyze(
212230
text=text,
213231
language=language,
@@ -225,7 +243,7 @@ def _analyze_many(
225243
score_threshold: float | None,
226244
):
227245
"""Analyze many texts, skipping the spaCy pass for regex-only requests."""
228-
if _regex_only(entities):
246+
if _regex_only(entities, score_threshold):
229247
blank = _BLANK_ARTIFACTS.get(language)
230248
return [
231249
analyzer.analyze(
@@ -467,7 +485,7 @@ def redact_batch(req: RedactBatchRequest) -> dict[str, list[str]]:
467485
req.language,
468486
len(req.texts),
469487
len(req.entities) if req.entities else "all",
470-
"skip" if _regex_only(req.entities) else "full",
488+
"skip" if _regex_only(req.entities, req.score_threshold) else "full",
471489
total_spans,
472490
(time.perf_counter() - started) * 1000,
473491
)

0 commit comments

Comments
 (0)