Guides
Selfie photo search for a 3,000-photo wedding: the pipeline, the cost, and what goes wrong
The code is short. The decisions around it are what make guests trust the results.
TL;DR
Index every gallery photo into one collection per event, keyed by your own photo id. When a guest uploads a selfie, search that collection and show the returned photo ids, strongest match first. For a 3,000-photo wedding with 120 guest searches, indexing costs about $1.86, searches about $0.11 and a month of storage about $0.05. Most of the work is elsewhere: rejecting bad selfies at upload, choosing a threshold, a "not me" button, and deleting the collection when the gallery closes.
A photographer delivers 3,000 photos from a wedding. Within a day, 120 guests have asked the same question: which ones am I in? Scrolling a public gallery link is how most of them give up, and it also shows every guest's photos to everyone with the link. Selfie search fixes both. The guest takes one selfie and sees only the photos they appear in.
We run this feature on real event galleries, so this guide is the version with the edge cases in it. The code uses our API, SightRadar, but the structure is the same on any face search API that has collections.
The shape of it
- Index once. Every gallery photo goes into a collection named for the event. Each face found becomes a stored faceprint tagged with your photo id, so a group shot of eight people produces eight faceprints that all point at one photo.
- Search on demand. A guest's selfie is searched against that one collection. Each match carries the photo id and a similarity score. The photo ids are the answer.
- Delete when done. When the gallery expires, delete the collection. That one call is the whole retention story for the event's biometric data.
Step 1: index the gallery in one batch
Photos are already in your storage, so send URLs, not bytes. A batch takes up to 1,000 URLs per call and reports each photo through a webhook, which is the right tool for a gallery upload. A 3,000-photo wedding is three calls.
import os, requests
API = "https://api.sightradar.com"
H = {"Authorization": f"Bearer {os.environ['SIGHTRADAR_API_KEY']}"}
EVENT = "wedding-2026-11-14-mehta" # one collection per event
def index_gallery(photo_keys: list[str]) -> list[str]:
batch_ids = []
for i in range(0, len(photo_keys), 1000):
chunk = photo_keys[i : i + 1000]
r = requests.post(f"{API}/v1/batches", headers=H, timeout=30, json={
"collection_id": EVENT, # created on first write
"photos": [{"url": f"https://cdn.example.com/{k}", "external_id": k} for k in chunk],
})
r.raise_for_status()
batch_ids.append(r.json()["batch_id"])
return batch_idsexternal_id is echoed back on every webhook and every search match, so use the key you already store the photo under. A photo that succeeds with face_count: 0 is a valid result (a table setting, a venue shot), not an error.
Step 2: reject bad selfies at upload
On our benchmarks page, 96% of 618 real guest selfie searches returned at least one photo. Most of the other 4% were bad selfies: a photo of a screen, heavy coloured party lighting, a face that is barely visible. The fix belongs in the upload screen, not in the search. Run detection on the selfie first, and ask for a retake unless exactly one face passes quality.
def selfie_ok(selfie: bytes) -> tuple[bool, str]:
r = requests.post(f"{API}/v1/detect", headers=H, files={"file": selfie}, timeout=30)
r.raise_for_status()
good = [f for f in r.json()["faces"] if f["quality_passed"]]
if not good:
return False, "We couldn't see your face clearly. Try a closer photo in normal light."
if len(good) > 1:
return False, "We found more than one face. Take a selfie with just you in it."
return True, ""Tip: Tell guests what a good selfie looks like before they take it: face the camera, normal light, no sunglasses. One line of copy above the camera button saves more searches than any threshold tuning.
Step 3: search and rank
def my_photos(selfie: bytes, threshold: float = 0.42) -> list[str]:
r = requests.post(
f"{API}/v1/collections/{EVENT}/search",
headers=H,
files={"file": selfie},
data={"threshold": str(threshold), "limit": "500"},
timeout=30,
)
r.raise_for_status()
body = r.json()
if not body["matches"]:
return [] # body.get("reason") says why: no_face, low_quality_selfie
# Several faces in one photo can match (mirrors, a framed portrait).
best: dict[str, float] = {}
for m in body["matches"]:
best[m["photo_id"]] = max(m["score"], best.get(m["photo_id"], 0))
return sorted(best, key=best.get, reverse=True)If you pass limit, set it well above the number of photos one person is likely to be in. The bride appears in hundreds of photos, and a small value like the 10 in our quickstart would cut her results off without any error.
Choosing the threshold
Scores are cosine similarity from 0 to 1. Our default is 0.42, chosen to keep false matches low for event photography. In our 50-gallery study (about 4.06 million faces, 22,070 real selfies), the false-match rate between selfies registered by different accounts was at most 0.13% at a threshold of 0.38, so 0.42 can only be stricter. For a wedding, a wrong photo shown to the wrong guest is worse than a missed one, so start at the default and only lower it if guests report missing photos.
If you want more recall, use two thresholds instead of one: show strong matches straight away, and put weaker ones behind a "Are these you too?" step. Choosing a face-match threshold covers how to sweep for both numbers on your own photos.
What goes wrong in real galleries
- Small faces in wide shots. The back rows of a 200-person group photo are a few dozen pixels each. Faces under the quality minimum are skipped at index time, so those guests won't find that photo. That's expected, and it's better than a wrong match.
- Children. Children's faces match less reliably than adults', even with good selfies. Our benchmarks page lists this as a known limit. If kids matter to your users, let a parent register a child's selfie separately rather than expecting their own selfie to find the child.
- Look-alike relatives. Siblings and parent-child pairs score closer than strangers. A "not me" button on each result lets guests clean up their own view, and it gives you a signal for tuning.
- Dark dance-floor photos. Coloured lights and motion blur lower both detection and match scores. Expect fewer matches from the reception than the ceremony.
- Mirror and screen shots. A guest photographing their face on another phone's screen is the most common bad selfie we see. The detect pre-check catches many of these, not all.
Consent and deletion
A faceprint is biometric data in most privacy laws (GDPR, Illinois BIPA and others). Get an explicit opt-in before a guest's selfie is processed, say how long it's kept, and delete it when the gallery closes. Because each event has its own collection, deletion is one call. Erasure is asynchronous, though, so don't record the event as erased until the deletion status confirms it.
import time
# When the gallery expires. ?compliance=true skips the grace window and
# records an audited, final erasure.
requests.delete(f"{API}/v1/collections/{EVENT}?compliance=true", headers=H, timeout=30).raise_for_status()
# Erasure runs asynchronously. Only mark the event erased once it is verified.
for _ in range(60):
r = requests.get(f"{API}/v1/collections/{EVENT}/deletion", headers=H, timeout=30)
r.raise_for_status()
st = r.json()
if st["phase"] == "completed" and st["verified_zero"]:
break # safe to record as erased in your own database
if st["phase"] == "blocked":
raise RuntimeError(f"erasure blocked: {st.get('last_error')}")
time.sleep(30)
else:
raise RuntimeError("erasure not verified yet; keep polling or wait for the webhook")What it costs
| Item | Volume | Rate | Cost |
|---|---|---|---|
| Index the gallery (batch) | 3,000 photos | $0.00062 per photo | $1.86 |
| Guest selfie searches | 120 searches | $0.00093 per search | $0.11 |
| Storage | ~9,000 faces for a month | $6 per million faces per month | $0.05 |
| Total for one wedding | $2.03 |
The full API flow, including selfie registration so a returning guest doesn't need to upload again, is in build face search with a selfie. For per-tenant layout when you run hundreds of events, see multi-tenant face search.
Try a search on sample photos first, no sign-up needed. New accounts get free credits, no card.
Open the face compare toolFrequently asked questions
How do guests find their wedding photos with a selfie?
The gallery's photos are indexed into a face search collection ahead of time. When a guest uploads a selfie, the selfie is searched against that collection, and the app shows only the photos that contain a matching face.
How much does selfie photo search cost for one event?
On SightRadar, a 3,000-photo wedding with 120 guest searches costs about $2.03: $1.86 to index the photos at the batch rate, $0.11 for the searches, and $0.05 for a month of storage.
Why can't a guest find photos where they are in the background?
Faces that are very small in the frame fall below the detector's quality minimum and are skipped at indexing, so they cannot be matched later. This affects the back rows of large group photos and distant candid shots.
Do I need consent to run face search on event photos?
In most jurisdictions a faceprint is biometric data. Get an explicit opt-in from each guest before processing their selfie, state how long data is kept, and delete the event's collection when the gallery closes.