A local multimodal image search engine powered by CLIP embeddings and Qdrant vector database. Search your personal photo library using natural language queries or by uploading a reference image — all running entirely on your machine, with no data sent to the cloud.
- Text-to-image search — describe what you're looking for in plain language ("sunset over the ocean", "birthday party with cake")
- Image-to-image search — upload a photo to find visually similar images in your library
- Local & offline — CLIP model runs locally via PyTorch with Apple Silicon GPU acceleration (MPS), no internet required after setup
- Fast vector search — Qdrant stores and retrieves embeddings efficiently, even across large collections
- Web UI — clean, dark-mode interface accessible from any browser
- Drag & drop — drop images directly onto the browser window to start a search
- Metadata extraction — indexes EXIF data including timestamps, camera model, and GPS coordinates
- Thumbnail generation — 300×300px JPEG thumbnails for fast grid previews
- Ingest — the system scans a directory, extracts metadata, generates thumbnails, and encodes each image into a 768-dimensional CLIP embedding
- Store — embeddings and metadata are stored in a local Qdrant vector database (running in Docker)
- Search — at query time, your text or image is encoded with the same CLIP model and the nearest embeddings are retrieved via cosine similarity
- Python 3.10+
- Docker (for Qdrant)
- Node.js (optional, not required at runtime)
- Apple Silicon Mac recommended (MPS acceleration); CUDA GPU or CPU also supported
git clone <repo-url>
cd image-retrieval-systempython -m venv .venv
source .venv/bin/activatepip install -r requirements.txtThe model is stored locally so it can run offline:
python download_model.pyThis downloads clip-vit-large-patch14 (~900MB) to data/models/clip-vit-large-patch14/.
docker-compose up -dQdrant will be available at http://localhost:6333.
./start.shOr manually:
source .venv/bin/activate
python app.pyThen open http://localhost:8000 in your browser.
The API documentation is available at http://localhost:8000/docs.
- Click Manage in the top-right corner
- Enter the full path to your image directory (e.g.
/Users/yourname/Pictures) - Click Ingest New to index only new images, or Re-index Directory to reprocess everything
Alternatively, use the API directly:
# Index new images only
curl -X POST http://localhost:8000/ingest/new \
-H "Content-Type: application/json" \
-d '{"path": "/Users/yourname/Pictures"}'
# Re-index all images in a directory
curl -X POST http://localhost:8000/ingest \
-H "Content-Type: application/json" \
-d '{"path": "/Users/yourname/Pictures"}'Text search — type a description in the search box and press Enter or click the search button.
Image search — switch to Image Search mode, then either click to select an image or drag and drop one anywhere on the page.
Results are ranked by cosine similarity. Each result card shows a thumbnail, similarity score, file path, and buttons to open the file in Finder or copy its path.
.
├── app.py # FastAPI backend
├── download_model.py # One-time model download script
├── start.sh # Startup script
├── docker-compose.yml # Qdrant service
├── requirements.txt
├── frontend/
│ ├── index.html
│ └── static/
│ ├── app.js
│ └── styles.css
├── src/
│ ├── search.py # Text and image search logic
│ ├── ingest.py # Directory ingestion and re-ingestion
│ ├── database/
│ │ └── qdrant_db.py # Qdrant client wrapper
│ ├── models/
│ │ └── clip_model.py # CLIP model wrapper (encode text & images)
│ └── utils/
│ ├── metadata.py # EXIF metadata extraction
│ ├── scanner.py # Recursive directory scanner
│ └── thumbnails.py # Thumbnail generation
└── data/
├── models/ # Local CLIP model (created by download_model.py)
├── qdrant_data/ # Qdrant persistent storage (created by Docker)
└── thumbnails/ # Generated thumbnails
| Method | Endpoint | Description |
|---|---|---|
GET |
/ |
Serves the web UI |
GET |
/thumbnail?path=... |
Serves an image or thumbnail |
POST |
/search_text |
Search by text query |
POST |
/search_image |
Search by uploaded image |
POST |
/ingest/new |
Index new images in a directory |
POST |
/ingest |
Re-index all images in a directory |
POST |
/open |
Reveal a file in Finder (macOS) |
GET |
/health |
Health check |
{ "query": "a dog on the beach" }[
{
"id": 123456789,
"score": 0.312,
"metadata": {
"path": "/Users/yourname/Pictures/holiday.jpg",
"thumbnail_path": "data/thumbnails/abc123.jpg",
"width": 4032,
"height": 3024,
"camera_make": "Apple",
"camera_model": "iPhone 14 Pro",
"timestamp": "2023-08-15T14:22:01",
"gps_coordinates": [44.4, 8.9]
},
"path_exists": true
}
]Result limit — search results are capped at 10% of the total indexed collection (minimum 5). This scales automatically as your library grows.
Score filtering — results below the mean similarity score are filtered out by default, keeping only the most relevant matches.
Supported image formats — .jpg, .jpeg, .png, .heic, .heif, .webp, .bmp, .tiff
Thumbnail size — 300×300px JPEG at quality 85, stored in data/thumbnails/
"Search failed. Is the backend running?" — make sure python app.py is running and Qdrant is up (docker ps | grep qdrant).
"Model directory not found" — run python download_model.py to download the CLIP model before starting the app.
Slow first search — the CLIP model is loaded into memory on the first query. Subsequent searches reuse the cached model and are much faster.
Images show as "Missing" — the file was indexed but has since been moved or deleted. Use Re-index Directory to refresh the database, or run db.cleanup_missing_images() from Python.
Qdrant not starting — ensure Docker Desktop is running and port 6333 is not in use by another service.
This project is for personal, local use. CLIP model weights are subject to OpenAI's model license. Qdrant is Apache 2.0 licensed.