You're browsing a store. You find a jacket you like but it's out of stock, or it's too expensive, or you just want to see what else looks like it. You scroll down and there's a "Similar Products" section showing four jackets with a similar vibe.
That's the feature. And it's responsible for more revenue than most store owners realize.
Why This Feature Matters
Baymard Institute found that 75% of e-commerce sites have product recommendation features, but most of them are basic: "other customers also bought" or "from the same category." These rely on purchase history and manual categorization.
Visual similarity is different. It shows products that actually look alike. A customer viewing a blue denim jacket sees other blue denim jackets, not "other jackets" which could include anything from a puffer vest to a blazer.
The numbers back this up:
- Product recommendations drive 10-30% of e-commerce revenue
- Visually similar recommendations convert better than category-based ones because they match buyer intent more precisely
- Stores that show relevant alternatives reduce bounce rates on out-of-stock products by keeping shoppers engaged
The problem is that building this traditionally requires image tagging, ML models, or manual curation. Most stores skip it because it's too much work for a sidebar feature.
It doesn't have to be.
How Visual Similarity Works
When a shopper views a product, your store takes that product's image and searches your entire catalog for visually similar ones. The search runs on the visual content of the images, not tags, categories, or text descriptions.
A red leather handbag finds other red leather handbags. A mid-century modern desk finds other mid-century modern desks. Nobody had to tag any of these as "red," "leather," "mid-century," or "modern." The model understands what they look like.
This is the key difference from category-based recommendations. "Women's Bags" is a category. "Bags that look like this one" is visual similarity. One gives you 500 results. The other gives you 6 that actually match.
Building It
Here's the full implementation. We'll insert product images into Vecstore, then query for similar products whenever a shopper views a product page.
Step 1: Insert Your Product Catalog
Each product image gets inserted with metadata (product name, price, URL, category) so the search results have everything you need to display the recommendations.
import requests
API_KEY = "your_api_key"
DB_ID = "your_database_id"
BASE = "https://api.vecstore.app/api"
HEADERS = {"X-API-Key": API_KEY, "Content-Type": "application/json"}
def insert_product(image_url, product_id, name, price, category, product_url):
resp = requests.post(
f"{BASE}/databases/{DB_ID}/documents",
headers=HEADERS,
json={
"image_url": image_url,
"metadata": {
"product_id": product_id,
"name": name,
"price": price,
"category": category,
"url": product_url,
},
},
)
return resp.json()
# Insert your catalog
insert_product(
"https://yourstore.com/images/jacket-blue-denim.jpg",
"SKU-1234",
"Classic Blue Denim Jacket",
89.99,
"jackets",
"https://yourstore.com/products/jacket-blue-denim",
)
Step 2: Batch Insert Your Full Catalog
If you have thousands of products, inserting one at a time is slow. Use async requests to insert hundreds concurrently:
import asyncio
import aiohttp
async def insert_batch(products, concurrency=20):
sem = asyncio.Semaphore(concurrency)
url = f"{BASE}/databases/{DB_ID}/documents"
async def insert_one(session, product):
async with sem:
async with session.post(url, headers=HEADERS, json={
"image_url": product["image_url"],
"metadata": product["metadata"],
}) as resp:
return await resp.json()
async with aiohttp.ClientSession() as session:
tasks = [insert_one(session, p) for p in products]
return await asyncio.gather(*tasks)
# 10,000 products inserts in minutes, not hours
asyncio.run(insert_batch(your_products))
Step 3: Query for Similar Products
When a shopper views a product, take that product's image and search for similar ones. Exclude the current product from results.
def find_similar(image_url, current_product_id, count=6):
resp = requests.post(
f"{BASE}/databases/{DB_ID}/search",
headers=HEADERS,
json={
"image_url": image_url,
"top_k": count + 1, # fetch extra to filter self
},
)
results = resp.json().get("results", [])
# Remove the current product from results
return [
r for r in results
if r.get("metadata", {}).get("product_id") != current_product_id
][:count]
The response includes everything you need to render the recommendation cards:
// Each result looks like this
{
"vector_id": "abc123",
"score": 0.87,
"metadata": {
"product_id": "SKU-5678",
"name": "Vintage Wash Denim Jacket",
"price": 79.99,
"category": "jackets",
"url": "https://yourstore.com/products/vintage-wash-denim"
}
}
Product name, price, category, URL. No second database query. Render the cards directly from the response.
Step 4: Add It to Your Frontend
Here's a basic React component that shows similar products on a product page:
export default function SimilarProducts({ productImageUrl, productId }) {
const [similar, setSimilar] = useState([]);
useEffect(() => {
fetch('/api/similar', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
image_url: productImageUrl,
exclude_id: productId,
}),
})
.then(res => res.json())
.then(data => setSimilar(data.results || []));
}, [productImageUrl, productId]);
if (!similar.length) return null;
return (
<section>
<h3>Similar Products</h3>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{similar.map(item => (
<a key={item.vector_id} href={item.metadata.url}>
<img src={item.metadata.image_url} alt={item.metadata.name} />
<p>{item.metadata.name}</p>
<p>${item.metadata.price}</p>
</a>
))}
</div>
</section>
);
}
That's the entire feature. Insert your catalog, query on each product page, render the results.
Making It Work Well
Cache the Results
Similar products for a given item don't change unless your catalog changes. Cache the results for each product (Redis, in-memory, even a JSON file) so you're not hitting the API on every page view. Refresh the cache when products are added or removed.
Handle Product Variants
If you sell the same shirt in 5 colors, each color is a separate image. Without handling this, "similar products" for a blue shirt will just be the same shirt in red, green, white, and black.
Two ways to handle it:
- Only insert one image per product (the primary/default image). Variants won't pollute results.
- Group results by product ID after the search. If multiple variants of the same product appear, keep only the highest-scoring one.
Set a Similarity Threshold
Not every result is worth showing. A score of 0.85+ means the products look very similar. A score of 0.60 means they're loosely related. For "similar products," 0.70+ is usually the right cutoff.
# Only show results above a quality threshold
similar = [r for r in results if r["score"] >= 0.70]
Showing loosely related products is worse than showing nothing. If the matches aren't strong, hide the section.
Combine with Text Search
Visual similarity is great for "looks like this." But sometimes you want "looks like this AND costs less than $50." Since Vecstore stores metadata with each image, you can filter results on your end:
def find_similar_in_budget(image_url, current_id, max_price, count=6):
results = find_similar(image_url, current_id, count=20)
return [
r for r in results
if r["metadata"].get("price", 0) <= max_price
][:count]
Fetch more results than you need, filter by price, show the top matches. This gives you "visually similar AND affordable" which is exactly what shoppers on a budget want.
What About Shopify, WooCommerce, etc.?
If you're on a hosted platform, you can still use this. The API works the same way regardless of your stack. You just need somewhere to run the backend logic:
- Shopify: Use a custom app or a serverless function (Cloudflare Workers, Vercel) that sits between your storefront and the Vecstore API
- WooCommerce: A small WordPress plugin or an external microservice
- Custom store: Integrate directly into your backend
The heaviest part is the initial catalog insert. After that, each "find similar" query is a single API call that returns in ~300ms.
Out-of-Stock Products
This is one of the highest-value use cases. A shopper lands on a product page, the item is out of stock, and they bounce. Every store owner knows how much revenue this costs.
With visual similarity, you can show "Similar products that are in stock" right on the out-of-stock page. Filter results by an in_stock metadata field:
def find_in_stock_alternatives(image_url, current_id, count=6):
results = find_similar(image_url, current_id, count=20)
return [
r for r in results
if r["metadata"].get("in_stock", False)
][:count]
Instead of losing the customer, you redirect them to something they'll probably like just as much.
The Numbers
For a store with 10,000 products:
- Initial catalog insert: A few minutes with async batching
- Per-query latency: ~300ms
- Cost: At $1.60 per 1K credits, a store with 50,000 product page views per month (each triggering a similarity query) costs about $80/month. With caching, much less.
Compare that to building your own pipeline with CLIP, a vector database, and GPU servers. That's weeks of engineering and $740+/month in infrastructure.
Getting Started
Insert a few hundred products, add the similar products component to one product page, and see if the results look right. The free tier gives you 25 credits to test with, no credit card required.


