Building Blomma
2025-06-10
Blomma is a plant curation platform with about 250 plants, and the interesting part isn't the catalogue — it's the search. When someone is looking for a plant, they often can't describe what they want in words. They know the vibe: trailing, dark leaves, architectural. Or they have a photo of something they saw at a friend's house. The search needs to handle both.
I built a multimodal embedding pipeline using Voyage AI. Every plant gets two embeddings: one from a text description (care requirements, visual characteristics, growth habit) and one from the primary image. These end up in the same vector space, which means a text query and an image query can return the same results. Upload a photo of a monstera and you'll find monsteras. Type "large dramatic leaves" and you'll also find monsteras. Same embedding space, different entry points.
The search blend is 70% text, 30% image, tuned by experimentation. Pure text search misses the visual similarity that makes plant discovery feel right. Pure image search is too literal — a photo of a pothos returns pothos variants but misses other trailing plants that share the same aesthetic. The 70/30 split captures both meaning and appearance.
Image generation is another layer. I use fal.ai to generate styled plant photographs for the catalogue. Real plant photography is expensive and inconsistent — different lighting, different pots, different backgrounds. Generated images give every plant the same treatment, which makes the browsing experience feel cohesive. The prompt engineering for botanical accuracy is its own rabbit hole. Getting AI to produce a convincing Philodendron gloriosum without hallucinating extra leaves took more iterations than I expected.
The catalogue itself uses a tag system for filtering — light requirements, water needs, pet safety, size category — but the semantic search often outperforms the filters. Someone searching for "low maintenance desk plant" gets better results from the vector search than from manually filtering by light:low + size:small + water:low. The embeddings understand intent in a way that faceted filters can't.
Storage is pgvector on Neon, with HNSW indexes for fast approximate nearest-neighbour lookup. The catalogue is small enough that exact search would work fine, but I built it with HNSW from the start because I use the same pattern across every project and wanted consistent infrastructure.