4040 PlPeselRecognizer ,
4141 SgFinRecognizer ,
4242 SgUenRecognizer ,
43+ SpacyRecognizer ,
4344 UkNinoRecognizer ,
4445)
4546from presidio_anonymizer import AnonymizerEngine
@@ -174,10 +175,18 @@ def build_analyzer() -> AnalyzerEngine:
174175batch_analyzer = BatchAnalyzerEngine (analyzer_engine = analyzer )
175176anonymizer = 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
201217def _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