A (somewhat) better, lightweight way to browse, search, potentially manage images on mac.
4
stars
168
commits
Swift
primary language
Sep 2, 2026
updated
A hybrid image search tool for macOS that uses CLIP + OCR to find images through natural language queries and text content.
On-device processing · Spotlight-style interface · GPU accelerated · Face recognition
Current App
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 text visible in images like "invoice 2024". Find photos by face, detect duplicates, and access it instantly from the menu bar with a global hotkey.
brew install --cask ausafmo/searchy/searchy
brew uninstall --cask searchy
# Remove app data (~2GB: venv, model weights, indexes)
rm -rf ~/Library/Application\ Support/searchy
Searchy-v4.0.dmg from Releasesgit clone https://github.com/AusafMo/searchy.git
cd searchy
Open searchy.xcodeproj in Xcode and build (⌘R).
On first launch, Searchy will automatically:
⌘⇧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
| Feature | Description |
|---|---|
| Hybrid Search | Natural language + OCR text search with adjustable weighting |
| Face Recognition | Auto-detect, cluster, name, pin, hide, merge, group faces |
| Duplicate Detection | Find visually similar images, bulk cleanup |
| Similar Image Search | Find images similar to any selected photo |
| Auto-Indexing | Watch directories with filters, incremental indexing |
| External Volumes | Index and search images on external drives |
| Model Selection | Multiple CLIP models, switch without re-indexing |
| Model TTL | Auto-unload CLIP model after idle period to free RAM/GPU, reloads from disk cache on next search |
| Update Checker | Notifies when a new version is available via Homebrew |
| Privacy | Fully local, no cloud, no telemetry, GPU accelerated via Metal |
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
├── face_recognition_service.py # Face detection & clustering (DeepFace)
├── image_watcher.py # File system monitor for auto-indexing
└── requirements.txt
Stack: SwiftUI + AppKit → FastAPI + Uvicorn → CLIP ViT-B/32 + DeepFace (ArcFace) → NumPy embeddings
All data stored in ~/Library/Application Support/searchy/:
image_index.bin # Embeddings + paths (pickle)
face_index.pkl # Face embeddings + clusters
venv/ # Isolated Python environment
# image_index.bin structure
{
'embeddings': np.ndarray, # Shape: (N, 512), float32, L2-normalized
'image_paths': list[str] # Absolute paths
}
The FastAPI server runs on localhost:7860 (or next available port).
| Endpoint | Method | Description |
|---|---|---|
/health | GET | Health check |
/search | POST | Semantic + OCR hybrid search |
/text-search | POST | Pure OCR text search |
/similar | POST | Find similar images |
/duplicates | POST | Find duplicate images |
/recent | GET | Recent images |
/face-scan | POST | Start face detection |
/face-clusters | GET | Get face groups |
/status | GET | Server + model loading state |
/model/ttl | GET/POST | Get/set model TTL |
Want to replace CLIP with your own model or rewrite the backend? Just respect these contracts:
1. Embedding Index File — ~/Library/Application Support/searchy/image_index.bin
import pickle, 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)
2. Server API Contract — FastAPI/HTTP server implementing the endpoints above.
3. Script Interface — The app calls scripts via Process():
# generate_embeddings.py — indexing
python generate_embeddings.py /path/to/folder [--fast] [--max-dimension INT] [--batch-size INT]
# Must output JSON to stdout:
{"type": "start", "total_images": N, "total_batches": N}
{"type": "progress", "batch": N, "total_batches": N, "images_processed": N, ...}
{"type": "complete", "total_images": N, "new_images": N, "total_time": float, ...}
# server.py — API server
python server.py --port PORT
# image_watcher.py — file system monitor
python image_watcher.py
4. Example: Custom Model
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.
cd ~/Library/Application\ Support/searchy
source venv/bin/activate
python generate_embeddings.py /path/to/images --batch-size 64 --fast
⌘⇧Space)macOS photo management is surprisingly lacking. Apple Photos is bloated and locks you into iCloud. Lightroom is a subscription editor. There's no lightweight, native gallery app for Mac.
Searchy aims to become the native macOS media manager that doesn't exist — with semantic search superpowers.
Fully local. No cloud, no telemetry, no external requests after initial model download.
Supported formats: jpg, jpeg, png, gif, bmp, tiff, webp, heic
MIT License
168 commits
Swift
78.9%
Python
19.0%
HTML
1.2%
A (somewhat) better, lightweight way to browse, search, potentially manage images on mac.
4
stars
168
commits
Swift
primary language
Sep 2, 2026
updated
A hybrid image search tool for macOS that uses CLIP + OCR to find images through natural language queries and text content.
On-device processing · Spotlight-style interface · GPU accelerated · Face recognition
Current App
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 text visible in images like "invoice 2024". Find photos by face, detect duplicates, and access it instantly from the menu bar with a global hotkey.
brew install --cask ausafmo/searchy/searchy
brew uninstall --cask searchy
# Remove app data (~2GB: venv, model weights, indexes)
rm -rf ~/Library/Application\ Support/searchy
Searchy-v4.0.dmg from Releasesgit clone https://github.com/AusafMo/searchy.git
cd searchy
Open searchy.xcodeproj in Xcode and build (⌘R).
On first launch, Searchy will automatically:
⌘⇧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
| Feature | Description |
|---|---|
| Hybrid Search | Natural language + OCR text search with adjustable weighting |
| Face Recognition | Auto-detect, cluster, name, pin, hide, merge, group faces |
| Duplicate Detection | Find visually similar images, bulk cleanup |
| Similar Image Search | Find images similar to any selected photo |
| Auto-Indexing | Watch directories with filters, incremental indexing |
| External Volumes | Index and search images on external drives |
| Model Selection | Multiple CLIP models, switch without re-indexing |
| Model TTL | Auto-unload CLIP model after idle period to free RAM/GPU, reloads from disk cache on next search |
| Update Checker | Notifies when a new version is available via Homebrew |
| Privacy | Fully local, no cloud, no telemetry, GPU accelerated via Metal |
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
├── face_recognition_service.py # Face detection & clustering (DeepFace)
├── image_watcher.py # File system monitor for auto-indexing
└── requirements.txt
Stack: SwiftUI + AppKit → FastAPI + Uvicorn → CLIP ViT-B/32 + DeepFace (ArcFace) → NumPy embeddings
All data stored in ~/Library/Application Support/searchy/:
image_index.bin # Embeddings + paths (pickle)
face_index.pkl # Face embeddings + clusters
venv/ # Isolated Python environment
# image_index.bin structure
{
'embeddings': np.ndarray, # Shape: (N, 512), float32, L2-normalized
'image_paths': list[str] # Absolute paths
}
The FastAPI server runs on localhost:7860 (or next available port).
| Endpoint | Method | Description |
|---|---|---|
/health | GET | Health check |
/search | POST | Semantic + OCR hybrid search |
/text-search | POST | Pure OCR text search |
/similar | POST | Find similar images |
/duplicates | POST | Find duplicate images |
/recent | GET | Recent images |
/face-scan | POST | Start face detection |
/face-clusters | GET | Get face groups |
/status | GET | Server + model loading state |
/model/ttl | GET/POST | Get/set model TTL |
Want to replace CLIP with your own model or rewrite the backend? Just respect these contracts:
1. Embedding Index File — ~/Library/Application Support/searchy/image_index.bin
import pickle, 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)
2. Server API Contract — FastAPI/HTTP server implementing the endpoints above.
3. Script Interface — The app calls scripts via Process():
# generate_embeddings.py — indexing
python generate_embeddings.py /path/to/folder [--fast] [--max-dimension INT] [--batch-size INT]
# Must output JSON to stdout:
{"type": "start", "total_images": N, "total_batches": N}
{"type": "progress", "batch": N, "total_batches": N, "images_processed": N, ...}
{"type": "complete", "total_images": N, "new_images": N, "total_time": float, ...}
# server.py — API server
python server.py --port PORT
# image_watcher.py — file system monitor
python image_watcher.py
4. Example: Custom Model
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.
cd ~/Library/Application\ Support/searchy
source venv/bin/activate
python generate_embeddings.py /path/to/images --batch-size 64 --fast
⌘⇧Space)macOS photo management is surprisingly lacking. Apple Photos is bloated and locks you into iCloud. Lightroom is a subscription editor. There's no lightweight, native gallery app for Mac.
Searchy aims to become the native macOS media manager that doesn't exist — with semantic search superpowers.
Fully local. No cloud, no telemetry, no external requests after initial model download.
Supported formats: jpg, jpeg, png, gif, bmp, tiff, webp, heic
MIT License
168 commits
Swift
78.9%
Python
19.0%
HTML
1.2%