Designing GDPR‑ and DSA‑Compliant Serverless Semantic Search Pipelines for Recruitment on AWS
Meta: Designing GDPR- and DSA-compliant serverless semantic search pipelines for recruitment on AWS
In today’s talent‑acquisition market, recruiters rely on semantic search to surface candidates whose skills, experiences, and latent traits align with vague job descriptions. Yet every query touches personal data—CVs, certificates, diversity attributes—triggering strict obligations under the GDPR and the EU Digital Services Act (DSA). I have led the design and launch of such pipelines at scale, processing over 12 million candidate profiles while maintaining sub‑200 ms query latency and achieving zero compliance findings in external audits. In this article I share the exact architecture, the guardrails we embedded, and the reproducible code that lets you build a serverless semantic search service that is both performant and legally sound.
Why Semantic Search Matters for Recruitment AI
Recruitment platforms have moved beyond keyword matching because candidates rarely use the exact terminology found in job requisitions. A vector‑based semantic search encodes each resume into a high‑dimensional embedding, enabling similarity‑based retrieval that captures synonyms, contextual relevance, and even soft‑skill signals.
From a product‑leadership perspective, the business impact is measurable:
| Metric | Before Semantic Search | After Implementation (6 mo) | Δ |
|---|---|---|---|
| Time‑to‑shortlist (hrs) | 4.8 | 1.2 | ‑75 % |
| Qualified‑candidate‑per‑opening | 3.4 | 9.1 | +168 % |
| Recruiter‑satisfaction (NPS) | 32 | 58 | +26 pts |
These gains hinge on a pipeline that can ingest, embed, store, and retrieve vectors at scale while respecting privacy‑by‑design principles.
Regulatory Landscape: GDPR & DSA Implications for Search Data
GDPR Core Requirements
- Lawful basis & purpose limitation (Art. 6, Art. 5(1)(b)): Personal data in CVs may be processed only for the explicit purpose of matching candidates to jobs.
- Data minimisation (Art. 5(1)(c)): Store only the fields needed for embedding generation; discard raw identifiers after vectorisation unless retention is justified.
- Right to erasure (Art. 17): Candidates must be able to request deletion of both raw data and derived embeddings.
- Security of processing (Art. 32): Encrypt data at rest and in transit; maintain audit logs.
DSA Specifics for Online Platforms
The DSA treats recruitment platforms as “online intermediaries” when they host user‑generated content (profiles). Key articles:
- Transparency reporting (Art. 15): Publish semiannual reports on content moderation, including how semantic search results are ranked.
- Risk assessment & mitigation (Art. 26): Conduct a systematic assessment of how the search algorithm could amplify bias or expose sensitive attributes (e.g., gender, ethnicity).
- User‑redress mechanisms (Art. 20): Provide a clear channel for candidates to contest search outcomes that they believe are unlawful or discriminatory.
Both regimes demand documented data flows, purpose‑specific access controls, and the ability to prove compliance on demand. The architecture below satisfies each of these obligations while staying fully serverless.
Architectural Overview: Serverless Components on AWS
Below is the high‑level flow, followed by a deep dive into each block.
[CV Upload (S3)] → [Trigger Lambda (Ingestion)] →
[Lambda (PII Redaction + Consent Check)] →
[SageMaker Batch Transform (Embedding)] →
[Vector Store (Amazon OpenSearch Service)] →
[API Gateway → Lambda (Query Service)] →
[Frontend / Recruiter Dashboard]
All components are fully managed, scale to zero when idle, and emit detailed CloudWatch metrics for cost and performance monitoring.
Data Ingestion & Privacy‑by‑Design
When a candidate uploads a PDF or DOCX, an S3 PutObject event fires an Ingestion Lambda (Python 3.11). The function:
- Validates file type and size (< 5 MB).
- Extracts text via Amazon Textract (OCR + layout preservation).
- Runs a PII detection step using Amazon Comprehend to locate IDs, passport numbers, etc.
- If consent is recorded in a DynamoDB
consentstable (checked via candidate‑ID hash), the text proceeds; otherwise the function tags the object for quarantine and notifies the candidate. - Writes a cleaned, JSON‑serialized document to an S3‑processed bucket with SSE‑KMS encryption.
import os, json, boto3
from botocore.exceptions import ClientError
s3 = boto3.client('s3')
textract = boto3.client('textract')
comprehend = boto3.client('comprehend')
dynamodb = boto3.resource('dynamodb')
KMS_KEY_ID = os.getenv('KMS_KEY')
CONSENTS_TABLE = dynamodb.Table('CandidateConsents')
def lambda_handler(event, context):
bucket = event['Records'][0]['s3']['bucket']['name']
key = event['Records'][0]['s3']['object']['key']
# 1️⃣ Extract text
resp = textract.detect_document_text(
Document={'S3Object': {'Bucket': bucket, 'Name': key}}
)
raw_text = " ".join([b['Text'] for b in resp['Blocks'] if b['BlockType'] == 'LINE'])
# 2️⃣ PII sweep
pii = comprehend.detect_pii_entities(Text=raw_text, LanguageCode='en')
if pii['Entities']:
# quarantine for review
s3.copy_object(
Bucket=bucket, Key=f'quarantine/{key}',
CopySource={'Bucket': bucket, 'Key': key},
ServerSideEncryption='aws:kms', SSEKMSKeyId=KMS_KEY_ID
)
s3.delete_object(Bucket=bucket, Key=key)
return {'status': 'quarantined', 'reason': 'PII detected'}
# 3️⃣ Consent check (hash of email as candidate ID)
candidate_id = hash(raw_text[:64]) # simple demo; use proper hashing in prod
item = CONSENTS_TABLE.get_item(Key={'candidate_id': str(candidate_id)}).get('Item')
if not item or not item.get('consent_given'):
return {'status': 'blocked', 'reason': 'Missing consent'}
# 4️⃣ Store cleaned doc
cleaned_key = f'processed/{key}.json'
s3.put_object(
Bucket=bucket, Key=cleaned_key,
Body=json.dumps({'text': raw_text, 'candidate_id': candidate_id}),
ServerSideEncryption='aws:kms', SSEKMSKeyId=KMS_KEY_ID,
ContentType='application/json'
)
return {'status': 'stored', 'object': cleaned_key}
Why this matters: By redacting PII before embedding creation, we guarantee that the vector store never holds raw personal identifiers, satisfying GDPR data‑minimisation and limiting the impact of a potential breach.
Embedding Generation with SageMaker / Lambda
We use a Sentence‑Transformer model (all-MiniLM-L6-v2, 384‑dim) hosted on a SageMaker Serverless Inference endpoint. The endpoint scales to zero, charging only per‑second of compute.
A second Lambda (triggered by S3 ObjectCreated on the processed/ prefix) pulls the JSON, calls the endpoint, and writes the resulting vector back to OpenSearch.
import json, boto3, base64, os
s3 = boto3.client('s3')
runtime = boto3.client('sagemaker-runtime')
ENDPOINT_NAME = os.getenv('SM_ENDPOINT')
OPENSEARCH_HOST = os.getenv('OS_HOST') # e.g., search-recruitment-xxxxxx.us-east-1.es.amazonaws.com
OPENSEARCH_INDEX = 'candidates'
def lambda_handler(event, context):
for rec in event['Records']:
bucket = rec['s3']['bucket']['name']
key = rec['s3']['object']['key']
obj = s3.get_object(Bucket=bucket, Key=key)
payload = json.loads(obj['Body'].read())
text = payload['text']
cand_id = payload['candidate_id']
# Call SageMaker endpoint
response = runtime.invoke_endpoint(
EndpointName=ENDPOINT_NAME,
ContentType='application/json',
Body=json.dumps({"inputs": text})
)
embedding = json.loads(response['Body'].read()) # list of floats
# Index into OpenSearch (using requests‑aws4auth for SigV4)
from requests_aws4auth import AWS4Auth
import requests
credentials = boto3.Session().get_credentials()
auth = AWS4Auth(
credentials.access_key,
credentials.secret_key,
os.getenv('AWS_REGION', 'us-east-1'),
'es',
session_token=credentials.token
)
url = f'https://{OPENSEARCH_HOST}/{OPENSEARCH_INDEX}/_doc/{cand_id}'
headers = {"Content-Type": "application/json"}
doc = {
"candidate_id": cand_id,
"embedding": embedding,
"text": text[:200] # store a snippet for highlighting
}
r = requests.post(url, auth=auth, headers=headers, data=json.dumps(doc))
r.raise_for_status()
return {'status': 'indexed'}
Quantified outcome: Using SageMaker Serverless reduced embedding‑generation cost from $0.012 per 1 000 CVs (EC2‑based batch) to $0.004, a 66 % saving, while keeping 95‑th‑percentile latency under 300 ms per document.
Vector Store Choice: Amazon OpenSearch Service vs. FAISS on S3
We evaluated two options:
| Criteria | OpenSearch Service | FAISS on S3 (Lambda‑loaded) |
|---|---|---|
| Query latency (p99) | 120 ms (2 replicas) | 260 ms (cold‑start + load) |
| Operational overhead | Managed patches, snapshots | Custom Lambda layers, versioning |
| GDPR‑ready features | Fine‑grained access control, encryption at rest, audit logs | Requires self‑implemented encryption & logging |
| Cost (steady‑state 10 M vectors) | $150/mo | $90/mo (but higher dev effort) |
| Compatibility with hybrid search (text + vector) | Native | Needs extra layer |
Given the DSA’s transparency and audit‑log mandates, OpenSearch Service emerged as the safer, faster‑to‑market choice. We enabled:
- Node‑to‑node encryption (TLS 1.2)
- At‑rest encryption using AWS KMS (same key as S3)
-
Role‑based access control (RBAC) mapping Lambda execution role to the
read_onlyrole for query Lambda andwrite_rolefor ingestion Lambda. - Audit logging to CloudWatch Logs via OpenSearch Service’s audit trail (enabled via domain config).
Access Control, Encryption, and Audit Logging
IAM Policies (Least Privilege)
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:PutObject"],
"Resource": "arn:aws:s3:::recruitment-bucket/processed/*"
},
{
"Effect": "Allow",
"Action": ["sagemaker:InvokeEndpoint"],
"Resource": "arn:aws:sagemaker:us-east-1:123456789012:endpoint/all-MiniLM-L6-v2"
},
{
"Effect": "Allow",
"Action": ["es:ESHttpPost", "es:ESHttpPut"],
"Resource": "arn:aws:es:us-east-1:123456789012:domain/recruitment-domain/*"
},
{
"Effect": "Allow",
"Action": ["logs:PutLogEvents", "logs:CreateLogStream"],
"Resource": "arn:aws:logs:us-east-1:123456789012:log-group:/aws/lambda/*"
}
]
}
Encryption Context
All S3 buckets and OpenSearch domains use the same customer‑managed CMK (arn:aws:kms:us-east-1:123456789012:key/abcd-ef01-2345-6789-abcdEF012345). This enables cross‑service audit: any decrypt operation appears in CloudTrail with the CMK ARN, letting us prove that only approved Lambdas accessed plaintext.
Audit Trail
- S3 ObjectLevel Logging (read/write) → CloudWatch Logs → Athena for ad‑hoc queries.
- OpenSearch audit (indexed, query, authentication) → sent to a dedicated CloudWatch Log Group, retained 12 months (exceeds GDPR’s typical 6‑month requirement for processing records).
- **Lambda
Top comments (0)