How Immich Detects Duplicate Assets: Algorithms and Techniques in duplicate.service.ts
Immich detects duplicate assets using CLIP embeddings and pgvector's approximate nearest-neighbor search, filtering results by a configurable distance threshold after pre-filtering assets for eligibility.
Immich's duplicate asset detection pipeline combines computer vision embeddings with scalable vector database queries to identify near-identical media files. The implementation spans two core files in the immich-app/immich repository: server/src/services/duplicate.service.ts handles job orchestration and filtering, while server/src/repositories/duplicate.repository.ts executes the vector similarity search. This architecture enables efficient background processing of large media libraries using modern approximate nearest-neighbor (ANN) algorithms.
Pre-Filtering and Job Orchestration in duplicate.service.ts
The service layer manages the duplicate detection lifecycle, ensuring only eligible assets enter the computationally expensive vector comparison phase.
Configuration Checks and Early Exits
Before processing begins, the job handler validates that duplicate detection is enabled in the machine learning configuration. If isDuplicateDetectionEnabled returns false, the job exits immediately without database queries, as implemented at lines 34-37 of server/src/services/duplicate.service.ts.
Asset Eligibility Filtering
For each asset ID received from the job queue, the service loads the full asset record (lines 65-68) and applies strict exclusion criteria. Assets are discarded if they belong to an existing stack, are marked as hidden or locked, or lack a computed CLIP embedding. These guards prevent false positives and reduce unnecessary vector computations. The embedding validation specifically checks for the existence of smart search data at lines 86-89, while visibility and stack checks occur at lines 71-84.
Batch Processing Strategy
To prevent memory pressure and database timeouts, the service processes assets in paginated batches controlled by JOBS_ASSET_PAGINATION_SIZE. This queue-driven approach, implemented at lines 39-53, allows the system to handle libraries containing millions of assets without blocking the main application thread.
Vector Similarity Search with CLIP Embeddings
The heavy mathematical lifting occurs in DuplicateRepository.search, which queries pre-computed CLIP embeddings stored in PostgreSQL via the pgvector extension.
Database Configuration for ANN Search
Before executing the similarity query, the repository optimizes the search performance by setting a session-local probe count. The expression SET LOCAL vchordrq.probes = ${probes[VectorIndex.Clip]} at line 19 tunes the approximate nearest-neighbor algorithm to match the CLIP index's optimal configuration, significantly accelerating query execution on large datasets.
Building the Candidate Set
The SQL query constructed at lines 24-35 joins the asset table with the smart_search table to retrieve embeddings. The candidate set is restricted to assets sharing the same owner and media type as the query asset, while excluding deleted files, stacked assets, and the source asset itself. This pre-filtering ensures comparisons occur only between relevant, active media files.
Distance Calculation and Threshold Filtering
The core duplicate detection algorithm relies on vector distance computation using pgvector's <=> operator at line 28:
smart_search.embedding <=> ${embedding}
This expression calculates the Euclidean (L2) distance between the query asset's CLIP embedding and candidate embeddings stored in the database. The database orders results by this distance metric and limits the initial candidate pool to the 64 nearest neighbors (lines 36-37).
After retrieving the nearest neighbors, the outer query applies a strict distance threshold filter. Only assets with a distance less than or equal to machineLearning.duplicateDetection.maxDistance (defaulting to 0.6) are retained as duplicates. This thresholding occurs at lines 40-42 of server/src/repositories/duplicate.repository.ts.
Post-Search Duplicate Group Management
Once the repository returns matching candidates, duplicate.service.ts manages the logical grouping of duplicates. If no neighbors satisfy the distance threshold, the service clears any existing duplicateId from the asset (lines 100-108). When matches exist, the updateDuplicates method (lines 119-138) extracts existing duplicate group IDs from candidates, selects a target group (prioritizing existing IDs, then the first candidate, or generating a new UUID), and merges all assets into this group via DuplicateRepository.merge. Finally, the service records the detection timestamp in duplicatesDetectedAt and returns JobStatus.Success (lines 110-113).
Summary
- CLIP Embeddings: Immich represents each image and video as a high-dimensional vector using OpenAI's CLIP model, stored in the
smart_searchtable. - Approximate Nearest-Neighbor Search: The system uses pgvector's
<=>operator with tunedvchordrq.probessettings for fast similarity queries on large datasets. - Configurable Distance Threshold: The
maxDistanceparameter (default 0.6) defines the similarity radius that determines whether two assets qualify as duplicates. - Multi-Layer Filtering: Assets are filtered for visibility, stack membership, and embedding availability before vector comparison, optimizing computational resources.
- Asynchronous Batch Processing: The NestJS job queue processes assets in configurable batches, enabling scalable background duplicate detection.
- Dynamic Group Merging: Detected duplicates are coalesced under shared
duplicateIdvalues using a merge strategy that preserves existing group relationships.
Frequently Asked Questions
How does Immich determine if two assets are duplicates?
Immich calculates the Euclidean distance between CLIP embeddings using pgvector's vector comparison operators. If the distance between two assets' embeddings is less than or equal to the configured maxDistance threshold (typically 0.6), the system classifies them as duplicates and assigns them to the same duplicate group.
What is a CLIP embedding and why does Immich use it for duplicate detection?
CLIP (Contrastive Language-Image Pre-training) embeddings are high-dimensional vectors generated by a neural network trained to understand visual content. Immich uses these embeddings because they capture semantic and visual similarity more effectively than perceptual hashing or checksums, enabling detection of near-duplicates with varying compression levels or minor edits.
Why does duplicate.service.ts exclude stacked or hidden assets from detection?
The service filters out stacked and hidden assets at lines 71-84 to prevent false positives and preserve user organization. Assets already manually grouped in stacks or marked as hidden are typically intentionally curated, and including them in automatic duplicate detection would undermine the user's explicit organization choices.
Can the duplicate detection sensitivity be adjusted in Immich?
Yes, administrators can modify the machineLearning.duplicateDetection.maxDistance configuration value. Lowering this value makes the detection stricter (requiring closer vector similarity), while increasing it captures more potential duplicates at the risk of false positives. The default value of 0.6 provides a balanced starting point for most libraries.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →