A unified system for multi-modal (Text/Image) search with geographic filtering, powered by CLIP, Qdrant, MinIO, and FastAPI.
This project consists of a Python backend and a React-based frontend. It allows users to search for products or images using natural language queries and filter results based on their location on a map.
- FastAPI (
app.py): The main web server that provides the search API and serves the compiled frontend. - CLIP & Qdrant (
main.py):- CLIP (OpenAI): Encodes both images and text into the same 512-dimensional vector space.
- Qdrant: A vector database used to store embeddings and perform efficient similarity searches with geo-spatial filtering.
- MinIO: Object storage used for hosting images. During ingestion, images can be uploaded to MinIO to generate publicly accessible URLs stored in Qdrant payloads.
- React Frontend (
map_search): An interactive map interface using Leaflet. Users can drop a pin, define a search radius, and see matching products as markers on the map.
- Python 3.12+
- Node.js & npm (for frontend)
- Docker (to run Qdrant and MinIO)
-
Clone the repository and navigate to the search API folder:
cd search_api -
Configure environment variables:
cp .env.example .env # Edit .env if you need to change ports or credentials -
Install Python dependencies using
uv:uv sync
-
Install Frontend dependencies:
cd map_search npm install cd ..
The ingestion process is handled by search_api/main.py. It uses CLIPQdrantManager to process images.
- Connect to Qdrant: Initializes the client and creates a collection with 512-dimensional vectors (Cosine similarity).
- Load Images: Loads raw images (e.g., from a dataset or local folder).
- Embed & Upload:
- Encodes images into vectors using the
sentence-transformers/clip-ViT-B-32model. - (Optional) Uploads images to MinIO and retrieves their permanent URLs.
- (Optional) Attaches random or specific geo-coordinates to the metadata.
- Encodes images into vectors using the
- Upsert to Qdrant: Stores the vectors and metadata (image URL, location, source) in the Qdrant collection.
To run the demo ingestion:
uv run python search_api/main.pyIn production, FastAPI serves both the API and the React frontend from the dist folder.
- Build the frontend:
cd search_api/map_search npm run build - Start the FastAPI server:
Access the app at
cd search_api uv run uvicorn app:app --host 0.0.0.0 --port 8100http://localhost:8100/.
- Start the Backend:
cd search_api uv run uvicorn app:app --reload --port 8100 - Start the Frontend (Vite):
Access the frontend at
cd search_api/map_search npm run devhttp://localhost:5173/(Vite proxies API requests to port 8100).
You can also run the entire stack using Docker Compose.
- Build the frontend:
cd search_api/map_search npm install npm run build cd ../..
- Start the stack:
Access the app at
docker compose up --build
http://localhost:8100/.
To run the ingestion script (main.py) inside the running container:
docker compose exec api uv run python main.pySearch for products using text and a geo-radius.
Request Body:
{
"query_text": "blue summer dress",
"lat": 40.7128,
"lon": -74.0060,
"radius_km": 50,
"limit": 20
}Response:
Returns a list of matching products including image_url, lat, lon, and score.
Environment variables can be set in a .env file in the search_api directory. Copy .env.example to get started:
cp .env.example .env| Variable | Description | Default |
|---|---|---|
QDRANT_URL |
Qdrant server address | localhost:6333 |
QDRANT_API_KEY |
Optional API key for Qdrant | - |
MINIO_ENDPOINT |
MinIO server address | localhost:9000 |
MINIO_ACCESS_KEY |
MinIO access key | minio |
MINIO_SECRET_KEY |
MinIO secret key | minio_secret_change_me |
MINIO_BUCKET |
Bucket name for images | pickpic |
MINIO_PUBLIC_URL |
Publicly accessible URL for MinIO | http://localhost:9000 |
SEARCH_COLLECTION |
Qdrant collection name | hm_ecommerce_clip |
SEARCH_WARMUP |
Preload CLIP model on startup | true |
MINIO_UPLOAD |
Upload images to MinIO during ingestion | true |
EMBED_MIN_SHORT_EDGE |
Minimum pixels for upscaling small images | 512 |
EMBED_MAX_SHORT_EDGE |
Maximum pixels for capping huge images | 2048 |

