feat: Optimize image hash checks with bulk queries and add database indexing for performance

This commit is contained in:
Ross
2026-05-11 12:07:55 +01:00
parent e55a3bf36a
commit 2b9f3e3b68
6 changed files with 85 additions and 28 deletions
+41 -25
View File
@@ -583,41 +583,57 @@ def check_image_hash(request, hash: str):
@router.post("/check_image_hashes/", auth=TokenAuth())
def check_images_hashes(request, hashes: List[str]):
"""Checks a list of image hashes and returns the series id / url if found
"""Checks a list of image hashes and returns the series id / url if found.
Uses two bulk queries (one for SeriesImage, one for UncategorisedDicom) to
handle large batches efficiently regardless of batch size.
Return format
{ "hash_id": {"id": "series_id|false", "url": "series_url|false"}, ...}
"""
try:
hash_status = {}
hash_status: dict = {h: {"id": False, "url": False} for h in hashes}
for hash in hashes:
# Prefer filter().first() to avoid MultipleObjectsReturned
series_image = SeriesImage.objects.filter(image_blake3_hash=hash).select_related("series").first()
if series_image:
try:
url = series_image.series.get_absolute_url()
except Exception:
url = None
data = {"id": series_image.pk, "url": url, "type": "series"}
hash_status[hash] = data
continue
# Single bulk query for all SeriesImage matches
series_images = (
SeriesImage.objects.filter(image_blake3_hash__in=hashes)
.select_related("series")
.only("pk", "image_blake3_hash", "series__slug", "series__pk")
)
matched_hashes: set = set()
for si in series_images:
if si.image_blake3_hash in matched_hashes:
continue # keep first match per hash
try:
url = si.series.get_absolute_url()
except Exception:
url = None
hash_status[si.image_blake3_hash] = {"id": si.pk, "url": url, "type": "series"}
matched_hashes.add(si.image_blake3_hash)
uncategorised = UncategorisedDicom.objects.filter(image_blake3_hash=hash).first()
if uncategorised:
try:
uploads_url = reverse("atlas:user_uploads")
except Exception:
uploads_url = None
data = {"id": uncategorised.pk, "url": uploads_url, "type": "uncategorised"}
hash_status[hash] = data
continue
hash_status[hash] = {"id": False, "url": False}
# Single bulk query for remaining hashes in UncategorisedDicom
remaining = [h for h in hashes if h not in matched_hashes]
if remaining:
try:
uploads_url = reverse("atlas:user_uploads")
except Exception:
uploads_url = None
uncategorised_qs = (
UncategorisedDicom.objects.filter(image_blake3_hash__in=remaining)
.only("pk", "image_blake3_hash")
)
for uc in uncategorised_qs:
if uc.image_blake3_hash not in matched_hashes:
hash_status[uc.image_blake3_hash] = {
"id": uc.pk,
"url": uploads_url,
"type": "uncategorised",
}
matched_hashes.add(uc.image_blake3_hash)
return hash_status
except Exception as e:
except Exception:
logger.exception("check_images_hashes failed")
return JsonResponse({"detail": "Internal server error"}, status=500)
@@ -0,0 +1,23 @@
# Generated by Django 5.2.7 on 2026-05-11 11:06
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('atlas', '0101_series_study_instance_uid'),
]
operations = [
migrations.AlterField(
model_name='seriesimage',
name='image_blake3_hash',
field=models.CharField(blank=True, db_index=True, max_length=64, null=True),
),
migrations.AlterField(
model_name='uncategoriseddicom',
name='image_blake3_hash',
field=models.CharField(blank=True, db_index=True, max_length=64, null=True),
),
]
+1 -1
View File
@@ -2173,7 +2173,7 @@ class UncategorisedDicom(models.Model):
series_instance_uid = models.CharField(max_length=255, blank=True, null=True)
image_blake3_hash = models.CharField(max_length=64, null=True, blank=True)
image_blake3_hash = models.CharField(max_length=64, null=True, blank=True, db_index=True)
created_date = models.DateTimeField(default=timezone.now)
+1 -1
View File
@@ -184,7 +184,7 @@
const SITE_PREFIXES = ["ref", "rk9", "ra9", "rh8", "rbz", "rba"];
const OCR_MAX_SERIES_SAMPLES = 40;
const OCR_CANVAS_MAX = 768;
const DUPLICATE_BATCH_SIZE = 50;
const DUPLICATE_BATCH_SIZE = 500;
function setProgress(text) {
document.getElementById("progress-text").textContent = text;
+1 -1
View File
@@ -298,7 +298,7 @@ class SeriesImageBase(models.Model):
# The second is a blake3 hash of the direct pixel data
# This is much faster when using pydicom
image_blake3_hash = models.CharField(max_length=64, null=True, blank=True)
image_blake3_hash = models.CharField(max_length=64, null=True, blank=True, db_index=True)
is_dicom = models.BooleanField(default=False)
@@ -0,0 +1,18 @@
# Generated by Django 5.2.7 on 2026-05-11 11:06
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('longs', '0034_exam_created_exam_updated'),
]
operations = [
migrations.AlterField(
model_name='longseriesimage',
name='image_blake3_hash',
field=models.CharField(blank=True, db_index=True, max_length=64, null=True),
),
]