FastAPI is the go-to Python framework for building APIs right now, and for good reason. It auto-generates OpenAPI docs, the type hints actually do something useful, and you get request validation for free through Pydantic. If your backend is FastAPI and you want to add image search, this tutorial gets you there.
We'll build a small API that lets users search images by text description, upload a photo to find visually similar ones, and insert new images into the database. Everything runs through Vecstore's search API so you don't have to deal with embedding models or vector storage yourself.
What We're Building
A FastAPI service with three endpoints:
- Text search - user sends a query like "wooden desk lamp" and gets matching images
- Image search - user uploads a photo and gets visually similar images back
- Insert - add new images to the database by URL
Plus a batch insert script for populating your database quickly.
Prerequisites
- Python 3.10+
- A Vecstore account (free tier works)
- An image database created in the Vecstore dashboard
- Your API key and database ID
Step 1: Install Dependencies
pip install fastapi uvicorn python-dotenv requests python-multipart
python-multipart is needed for file uploads in FastAPI.
Create a .env file:
VECSTORE_API_KEY=your_api_key_here
VECSTORE_DB_ID=your_database_id_here
Step 2: Build the Vecstore Client
Create vecstore_client.py. This handles all the API communication.
import os
import base64
import requests
from dotenv import load_dotenv
load_dotenv()
API_KEY = os.getenv("VECSTORE_API_KEY")
DB_ID = os.getenv("VECSTORE_DB_ID")
BASE_URL = "https://api.vecstore.app/api"
HEADERS = {
"X-API-Key": API_KEY,
"Content-Type": "application/json",
}
def search_by_text(query: str, top_k: int = 12) -> dict:
resp = requests.post(
f"{BASE_URL}/databases/{DB_ID}/search",
headers=HEADERS,
json={"query": query, "top_k": top_k},
)
return resp.json()
def search_by_image(image_bytes: bytes, top_k: int = 12) -> dict:
b64 = base64.b64encode(image_bytes).decode()
resp = requests.post(
f"{BASE_URL}/databases/{DB_ID}/search",
headers=HEADERS,
json={"image": b64, "top_k": top_k},
)
return resp.json()
def insert_image_url(url: str, metadata: dict | None = None) -> dict:
body = {"image_url": url}
if metadata:
body["metadata"] = metadata
resp = requests.post(
f"{BASE_URL}/databases/{DB_ID}/documents",
headers=HEADERS,
json=body,
)
return resp.json()
def delete_image(document_id: str) -> dict:
resp = requests.delete(
f"{BASE_URL}/databases/{DB_ID}/documents/{document_id}",
headers=HEADERS,
)
return resp.json()
Four functions. That's the whole integration layer.
Step 3: Build the FastAPI App
Create main.py:
from fastapi import FastAPI, UploadFile, File
from pydantic import BaseModel
import vecstore_client
app = FastAPI(title="Image Search API")
class TextSearchRequest(BaseModel):
query: str
top_k: int = 12
class InsertRequest(BaseModel):
image_url: str
metadata: dict | None = None
@app.post("/search/text")
def text_search(req: TextSearchRequest):
results = vecstore_client.search_by_text(req.query, req.top_k)
return results
@app.post("/search/image")
async def image_search(image: UploadFile = File(...)):
contents = await image.read()
results = vecstore_client.search_by_image(contents)
return results
@app.post("/images")
def insert_image(req: InsertRequest):
result = vecstore_client.insert_image_url(
req.image_url, req.metadata
)
return result
@app.delete("/images/{document_id}")
def remove_image(document_id: str):
result = vecstore_client.delete_image(document_id)
return result
Run it:
uvicorn main:app --reload
That's it. Go to http://localhost:8000/docs and you get a full interactive API docs page for free. FastAPI generates it from your type hints and Pydantic models. You can test every endpoint right from the browser.
Step 4: Populate Your Database
Before search works, you need images in your database. You can do this from the Vecstore dashboard, or with a quick script. Create seed.py:
from vecstore_client import insert_image_url
images = [
"https://example.com/products/sneaker-red.jpg",
"https://example.com/products/sneaker-blue.jpg",
"https://example.com/products/boot-leather.jpg",
# ... your image URLs
]
for url in images:
insert_image_url(url)
print(f"Inserted: {url}")
Each image gets embedded automatically. No tagging, no preprocessing.
Step 5: Test It
With the server running, try each endpoint:
# text search
curl -X POST http://localhost:8000/search/text \
-H "Content-Type: application/json" \
-d '{"query": "red leather handbag"}'
# image search
curl -X POST http://localhost:8000/search/image \
-F "image=@photo.jpg"
# insert an image
curl -X POST http://localhost:8000/images \
-H "Content-Type: application/json" \
-d '{"image_url": "https://example.com/photo.jpg", "metadata": {"name": "Red bag"}}'
# delete an image
curl -X DELETE http://localhost:8000/images/your_document_id
Or just use the interactive docs at /docs. Easier for testing.
Things to Keep in Mind
File upload size. FastAPI doesn't impose a strict body limit by default, but you'll want to cap it to avoid someone uploading a 500MB file. You can set this in your deployment config (Uvicorn, Nginx, etc.) or check the file size in your endpoint before reading the full contents.
Image size for search quality. Sending huge images doesn't improve search quality. The embedding model doesn't benefit from anything beyond about 1000px on the longest side. If you're accepting uploads from users, consider resizing before sending to Vecstore to keep things fast.
Error handling. The code above doesn't handle Vecstore API errors for clarity. In production, check the response status and return proper HTTP errors to your clients:
@app.post("/search/text")
def text_search(req: TextSearchRequest):
results = vecstore_client.search_by_text(req.query, req.top_k)
if "error" in results:
raise HTTPException(status_code=502, detail=results["error"])
return results
Metadata. When inserting images, attach any metadata you'll need later. Product name, category, price, whatever. It comes back with search results so you don't have to do a separate database lookup:
curl -X POST http://localhost:8000/images \
-H "Content-Type: application/json" \
-d '{
"image_url": "https://example.com/products/shoe.jpg",
"metadata": {
"name": "Red Running Shoe",
"price": 89.99,
"category": "footwear"
}
}'
CORS. If you're calling this from a frontend, add CORS middleware:
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:3000"],
allow_methods=["*"],
allow_headers=["*"],
)
What Else You Can Do
The same Vecstore database supports more search types without extra setup:
- Face search - upload a photo of someone and find every image of that person in your database
- OCR search - find images by the text inside them (signs, screenshots, documents)
- NSFW detection - check uploaded images for content safety before storing or displaying them
All through the same API key and database.
Wrapping Up
The full setup: one client module, one FastAPI app with four endpoints, and a seed script. No embedding models, no vector database, no GPU servers.
FastAPI gives you automatic OpenAPI docs and request validation out of the box. Combined with Vecstore handling the search infrastructure, you get a working image search API with very little code.
For production you'd add authentication, rate limiting, and proper error handling. But the search itself works as shown.
Get started with Vecstore - free tier includes enough credits to build and test.


