A semantic image search tool for macOS that uses CLIP to find images through natural language queries.
On-device processing · Spotlight-style interface · GPU accelerated
demo.1.mp4
Photo management on macOS sucks. Searchy indexes your images locally and lets you search them using descriptive phrases like "sunset over mountains", "person wearing red", or "cat sleeping on couch". Access it instantly from the menu bar with a global hotkey.
- Download
Searchy.dmgfrom Releases - Drag to Applications
- Right-click → Open (first time only, macOS security)
- Click Start Setup — Python & AI models install automatically (3-5 min)
That's it. No manual Python installation required.
git clone https://github.com/AusafMo/searchy.git
cd searchyOpen searchy.xcodeproj in Xcode and build (⌘R).
On first launch, Searchy will automatically:
- Install Python (if not found)
- Create an isolated virtual environment
- Download AI dependencies (~2GB)
- Download the CLIP model
⌘⇧Space Open Searchy
↑ ↓ Navigate results
Enter Copy and paste selected image
⌘Enter Reveal in Finder
⌘1-9 Copy and paste by position
Ctrl+1-9 Copy to clipboard only
Esc Close window
Access via the gear icon in the header.
Display — Grid columns (2-6), thumbnail size (100-400px), performance statistics toggle
Search — Maximum results (10/20/50/100), minimum similarity threshold (0-100%)
Indexing — Fast indexing mode, maximum image dimension (256-768px), batch size (32-256)
Watched Directories — Configure folders for automatic indexing with optional filename filters
Semantic Search
- Query images using natural language descriptions
- Real-time results with 400ms debounce
- Similarity scores displayed as color-coded percentages
- Adjustable threshold to filter weak matches
- Filter results by file type, date range, and size
Duplicate Detection
- Find visually similar images across your library
- Adjustable similarity threshold (85-99%)
- Auto-select smaller duplicates for cleanup
- Preview images before deleting
- Move or trash duplicates in bulk
Spotlight-Style Interface
- Global hotkey
⌘⇧Spacesummons a floating search window - Displays recent images on launch
- Fully keyboard-navigable
- Click images to preview full-size
Auto-Indexing
- Monitors configured directories automatically
- Supports custom directories with prefix, suffix, or regex filters
- Incremental indexing — only processes new files
- Search while indexing is in progress
- Cancel indexing anytime
Smart Filtering
- Automatically skips system directories (Library, node_modules, site-packages, etc.)
- Ignores hidden files and build artifacts
- Focuses only on your actual photos
Zero-Config Setup
- Automatic Python installation (via Homebrew or standalone)
- Isolated virtual environment in Application Support
- All dependencies installed on first launch
Privacy
- All processing runs locally on your machine
- No network requests after initial setup
- GPU acceleration via Metal (Apple Silicon)
searchy/
├── ContentView.swift # SwiftUI interface
├── searchyApp.swift # App lifecycle, setup manager, server management
├── server.py # FastAPI backend
├── generate_embeddings.py # CLIP model and embedding generation
├── image_watcher.py # File system monitor for auto-indexing
└── requirements.txt
Stack: SwiftUI + AppKit → FastAPI + Uvicorn → CLIP ViT-B/32 → NumPy embeddings
All data is stored in ~/Library/Application Support/searchy/:
searchy/
├── venv/ # Isolated Python environment
├── image_index.bin # Embeddings + paths (pickle)
├── watched_directories.json
└── settings files...
The index file (image_index.bin) is a pickled Python dictionary:
{
'embeddings': np.ndarray, # Shape: (num_images, 512) - normalized vectors
'image_paths': list[str] # Absolute paths to images
}Load it in Python:
import pickle
with open('image_index.bin', 'rb') as f:
data = pickle.load(f)
embeddings = data['embeddings'] # numpy array
paths = data['image_paths'] # list of stringsThe FastAPI server runs on localhost:7860 (or next available port).
Search images:
curl -X POST http://localhost:7860/search \
-H "Content-Type: application/json" \
-d '{"query": "sunset over mountains", "n_results": 10}'Response:
{
"results": [
{"path": "/path/to/image.jpg", "similarity": 0.342},
...
]
}Get recent images:
curl http://localhost:7860/recent?n=20Health check:
curl http://localhost:7860/healthEdit generate_embeddings.py line 37:
# Default: ViT-B/32 (512-dim embeddings, fastest)
model_name = "openai/clip-vit-base-patch32"
# Alternatives:
# "openai/clip-vit-base-patch16" # 512-dim, more accurate
# "openai/clip-vit-large-patch14" # 768-dim, most accurate, slowerNote: Changing models requires re-indexing all images. Delete
image_index.binbefore switching.
Want to replace CLIP with your own model or rewrite the backend entirely? Just respect these contracts:
The Swift app reads ~/Library/Application Support/searchy/image_index.bin. Your indexer must produce a pickle file with this exact structure:
import pickle
import numpy as np
data = {
'embeddings': np.ndarray, # Shape: (N, embedding_dim), float32, L2-normalized
'image_paths': list[str] # Length N, absolute paths
}
with open('image_index.bin', 'wb') as f:
pickle.dump(data, f)The app expects a FastAPI/HTTP server. Implement these endpoints:
| Endpoint | Method | Request | Response |
|---|---|---|---|
/health |
GET | — | {"status": "ok"} |
/search |
POST | {"query": str, "n_results": int, "threshold": float} |
{"results": [{"path": str, "similarity": float}, ...]} |
/recent |
GET | ?n=int |
{"results": [{"path": str, "similarity": float}, ...]} |
/index-count |
GET | — | {"count": int} |
/duplicates |
POST | {"threshold": float, "data_dir": str} |
{"groups": [...], "total_duplicates": int} |
The app calls your scripts via Process(). Replace these files in the app bundle's Resources:
generate_embeddings.py — Called for indexing:
python generate_embeddings.py /path/to/folder [options]
# Must accept:
--fast # Optional: fast mode flag
--max-dimension INT # Optional: resize dimension
--batch-size INT # Optional: batch size
--filter-type TYPE # Optional: all|starts-with|ends-with|contains|regex
--filter VALUE # Optional: filter value
# Must output to stdout (JSON, one per line):
{"type": "start", "total_images": N, "total_batches": N}
{"type": "progress", "batch": N, "total_batches": N, "images_processed": N, "total_images": N, "elapsed": float, "images_per_sec": float}
{"type": "complete", "total_images": N, "new_images": N, "total_time": float, "images_per_sec": float}server.py — Called to start the API server:
python server.py --port PORT
# Must start HTTP server on given portimage_watcher.py — Called for auto-indexing:
python image_watcher.py
# Watches directories, triggers incremental indexing# my_custom_indexer.py - Use any model you want
from sentence_transformers import SentenceTransformer
import pickle, numpy as np
model = SentenceTransformer('clip-ViT-L-14') # Or any model
def index_images(paths):
embeddings = model.encode([Image.open(p) for p in paths])
embeddings = embeddings / np.linalg.norm(embeddings, axis=1, keepdims=True)
with open('image_index.bin', 'wb') as f:
pickle.dump({'embeddings': embeddings, 'image_paths': paths}, f)As long as you output the right JSON progress messages and maintain the pickle format, the Swift UI will work with any backend.
Generate embeddings directly:
cd ~/Library/Application\ Support/searchy
source venv/bin/activate
python generate_embeddings.py /path/to/images --batch-size 64 --fastOptions:
--fast/--no-fast— Resize images before processing--max-dimension 384— Max image size (256, 384, 512, 768)--batch-size 64— Images per batch (32, 64, 128, 256)--filter-type starts-with— Filter filenames--filter "IMG_"— Filter value
- Spotlight-style floating widget
- Global hotkey (
⌘⇧Space) - Theme toggle (System/Light/Dark)
- Real-time search with debouncing
- Auto-indexing with file watchers
- Watched directories with filters
- Configurable settings panel
- Menu bar app (no Dock icon)
- GPU acceleration (Metal)
- Recent images display
- Fast indexing with image resizing
- Bundled .app distribution with auto-setup
- Duplicate image detection
- Date, size, and file type filters
- Alternative/smaller models
- macOS 13+
- Apple Silicon
- Internet connection (first-time setup only)
- ~2GB disk space for AI models
Supported formats: jpg, jpeg, png, gif, bmp, tiff, webp, heic
MIT License