Support batch processing over the REST API. (#1806)

* Support batch processing over the REST API.

* Partially fix e2e tests

* Fix e2e tests

* ruff

* ruff

* consistent use of strings

* Update API docs

* Support batch processing over the REST API.

* Partially fix e2e tests

* Fix e2e tests

* ruff

* ruff

* consistent use of strings

* Update API docs
This commit is contained in:
Thomas E Lackey
2026-02-04 08:44:39 -06:00
committed by GitHub
parent eaf7a36c3d
commit 662cf2d70e
3 changed files with 100 additions and 15 deletions

View File

@@ -40,13 +40,20 @@ paths:
content:
application/json:
schema:
description: "A list analysis results"
type: array
items:
$ref: "#/components/schemas/RecognizerResultWithAnalysisExplanation"
oneOf:
- description: "A list of analysis results (when input text is a single string)"
type: array
items:
$ref: "#/components/schemas/RecognizerResultWithAnalysisExplanation"
- description: "A list of analysis result lists (when input text is an array of strings)"
type: array
items:
type: array
items:
$ref: "#/components/schemas/RecognizerResultWithAnalysisExplanation"
examples:
Enhanced response:
Enhanced response (single text):
value:
[
{ "entity_type": "PERSON", "start": 0, "end": 10, "score": 0.85,
@@ -66,7 +73,7 @@ paths:
}
}
]
Lean response:
Lean response (single text):
value:
[
{
@@ -77,6 +84,17 @@ paths:
"start": 30
}
]
Batch response (array of texts):
value:
[
[
{ "entity_type": "PERSON", "start": 0, "end": 10, "score": 0.85, "analysis_explanation": null },
{ "entity_type": "US_DRIVER_LICENSE", "start": 30, "end": 38, "score": 0.6499999999999999, "analysis_explanation": null }
],
[
{ "entity_type": "EMAIL_ADDRESS", "start": 15, "end": 35, "score": 0.95, "analysis_explanation": null }
]
]
/recognizers:
get:
@@ -272,13 +290,19 @@ components:
schema:
$ref: "#/components/schemas/AnalyzeRequest"
examples:
Minimal Request:
Minimal Request (single text):
value:
{
"text": "John Smith drivers license is AC432223",
"language": "en"
}
Enhanced Request :
Minimal Request (batch request):
value:
{
"text": ["John Smith drivers license is AC432223", "John Smith drivers license is AC432223 and the zip code is 12345"],
"language": "en"
}
Enhanced Request (single text):
value:
{
"text": "John Smith drivers license is AC432223 and the zip code is 12345",
@@ -370,8 +394,12 @@ components:
- language
properties:
text:
type: string
description: "The text to analyze"
oneOf:
- type: string
- type: array
items:
type: string
description: "The text to analyze. Can be a single string or an array of strings."
example: "hello world, my name is Jane Doe. My number is: 034453334"
language:
type: string

View File

@@ -572,3 +572,34 @@ def test_given_regex_flags_and_normal_entities_are_returned():
assert equal_json_strings(
expected_response, response_content
)
@pytest.mark.api
def test_given_a_correct_analyze_input_then_return_full_response_batched():
request_body = """
{
"text": [
"John Smith drivers license is AC432223",
"John Smith drivers license is AC432223"
],
"language": "en"
}
"""
response_status, response_content = analyze(request_body)
expected_response = """
[
[
{"entity_type": "PERSON", "start": 0, "end": 10, "score": 0.85, "analysis_explanation":null},
{"entity_type": "US_DRIVER_LICENSE", "start": 30, "end": 38, "score": 0.6499999999999999, "analysis_explanation":null}
],
[
{"entity_type": "PERSON", "start": 0, "end": 10, "score": 0.85, "analysis_explanation":null},
{"entity_type": "US_DRIVER_LICENSE", "start": 30, "end": 38, "score": 0.6499999999999999, "analysis_explanation":null}
]
]
"""
assert response_status == 200
assert equal_json_strings(
expected_response, response_content
)

View File

@@ -8,10 +8,17 @@ from pathlib import Path
from typing import Tuple
from flask import Flask, Response, jsonify, request
from presidio_analyzer import AnalyzerEngine, AnalyzerEngineProvider, AnalyzerRequest
from presidio_analyzer import (
AnalyzerEngine,
AnalyzerEngineProvider,
AnalyzerRequest,
BatchAnalyzerEngine,
)
from werkzeug.exceptions import HTTPException
DEFAULT_PORT = "3000"
DEFAULT_BATCH_SIZE = "500"
DEFAULT_N_PROCESS = "1"
LOGGING_CONF_FILE = "logging.ini"
@@ -46,6 +53,8 @@ class Server:
nlp_engine_conf_file=nlp_engine_conf_file,
recognizer_registry_conf_file=recognizer_registry_conf_file,
).create_engine()
self.batch_engine = BatchAnalyzerEngine(self.engine)
self.logger.info(WELCOME_MESSAGE)
@self.app.route("/health")
@@ -62,11 +71,21 @@ class Server:
if not req_data.text:
raise Exception("No text provided")
batch_request = isinstance(req_data.text, list)
batch = req_data.text if batch_request else [req_data.text]
if not req_data.language:
raise Exception("No language provided")
else:
# Make sure the language is supported by the engine.
self.engine.get_supported_entities(req_data.language)
recognizer_result_list = self.engine.analyze(
text=req_data.text,
iterator = self.batch_engine.analyze_iterator(
texts=batch,
batch_size=min(
len(batch),
int(os.environ.get("BATCH_SIZE", DEFAULT_BATCH_SIZE))
),
language=req_data.language,
correlation_id=req_data.correlation_id,
score_threshold=req_data.score_threshold,
@@ -77,12 +96,19 @@ class Server:
allow_list=req_data.allow_list,
allow_list_match=req_data.allow_list_match,
regex_flags=req_data.regex_flags,
n_process=min(
len(batch),
int(os.environ.get("N_PROCESS", DEFAULT_N_PROCESS))
)
)
_exclude_attributes_from_dto(recognizer_result_list)
results = []
for recognizer_result_list in iterator:
_exclude_attributes_from_dto(recognizer_result_list)
results.append(recognizer_result_list)
return Response(
json.dumps(
recognizer_result_list,
results if batch_request else results[0],
default=lambda o: o.to_dict(),
sort_keys=True,
),