General info

Project name: Notemae

Elevator pitch: Notemae understands your fragrance preferences and turns your personality, climate, occasion, and budget into personalized scent recommendations.

Project Story — About the project

Paste everything from "Inspiration" down to "What's next for Notemae" into the About the project field. It is plain Markdown and renders as-is on Devpost.

Inspiration

Buying a fragrance online is strangely difficult. People describe what they want in human terms: "something safe for my first interview," "not too sweet in Jakarta heat," "smells like Layton but doesn't cost three million rupiah." Catalogs, on the other hand, speak in notes, families, longevity scores, and price tiers. A generic chatbot can bridge that language gap, but it will also cheerfully invent products, prices, and performance claims that do not exist.

We wanted the opposite: a guide that listens like a friend who is deep into fragrance, but only ever answers from a real catalog it can show you. That idea became Notemae, a fragrance dupe guide and personal scent consultant. You tell it who you are, where you live, what the occasion is, and what you can spend, and it answers with products, similarity scores, reasons, and cautions you can actually inspect.

What it does

Notemae is an editorial, bilingual (English and Indonesian) web app with four connected experiences:

  • A fragrance catalog you can browse and filter, with notes, families, gender profiles, ratings, and prices for every entry.
  • A dupe guide built around three steps: pick the original perfume, read a similarity score computed from shared catalog notes rather than ad claims, then compare candidates side by side to see shared notes, differences, and the price gap.
  • An AI consultant that accepts a request in everyday language, such as "Saya pria, kerja kantoran di Jakarta, budget 1,2 juta, suka citrus dan cedar, tidak suka manis" and turns it into a typed preference profile: budget, occasion, climate, gender, loved and avoided notes, and reference perfumes. Recommendations stream in live over server-sent events, each with reasons, cautions, and alternatives.
  • Grounded answers everywhere. The language model is never allowed to invent a product. It can only reorder and explain candidates that already exist in PostgreSQL, and if the model provider is down, deterministic catalog scoring takes over so the product keeps working.

How we built it

Notemae is a small fleet of services, each owning one concern, all shipped with Docker Compose:

Browser -> Caddy (HTTPS) -> Frontend (React 19 + TypeScript, Express SSR)
  -> Backend /v1 (Go, clean architecture) -> PostgreSQL 16 + pgvector
                                          -> Agent (Python + FastAPI) -> Qwen + Redis
Ingestion (approved sources only) -> Backend /internal (service-key auth)
  • Frontend. A React 19 + TypeScript SPA served by Express with server-side rendering on Node 22, built with Vite. The editorial feel comes from GSAP and Lenis motion plus a glass perfume bottle rendered live with Three.js, and the whole interface is internationalized in English and Indonesian.
  • Backend. A Go service using only the standard library HTTP stack and pgx, structured in three clean architecture layers (presentation, domain, data). It owns the public /v1 API, hybrid SQL plus vector retrieval, and the SSE recommendation stream. It began life as FastAPI; we rewrote it in Go and verified the read endpoints byte-for-byte against the Python service before cutting over.
  • Recommendation agent. A Python + FastAPI service that extracts typed preferences, applies deterministic constraint filters and scoring informed by the fragrance taxonomy, then lets Qwen (qwen3.7-plus through DashScope's OpenAI-compatible API) do a strictly bounded listwise rerank. Multiple rerank votes are merged with a Borda count, maximal marginal relevance keeps the top results from being near duplicates of each other, and the final explanation is generated only from the selected catalog record. Redis handles caching and rate limits.
  • Retrieval. PostgreSQL 16 with pgvector stores 1024-dimension Qwen text-embedding-v4 vectors. When users name perfumes they love or hate, Rocchio relevance feedback pulls the query vector toward the references they like and away from the ones they dislike.
  • Ingestion. A scheduled worker that only accepts reviewed public datasets, official APIs, and licensed feeds, currently including nightly Open Beauty Facts deltas under ODbL. It normalizes records, runs a Qwen enrichment pass, and writes to an internal endpoint protected by a shared service key. Provenance is preserved on every record.

The full system diagram is in the image gallery.

Challenges we ran into

  • Bounding the model without neutering it. Budget, avoided notes, and gender compatibility are enforced deterministically before any model call, so a persuasive LLM answer can never override catalog constraints. The reranker may only return slugs it was given. Designing that boundary so the model still adds real value took several iterations.
  • Rewriting the backend without breaking anyone. Moving from FastAPI to Go sounds mechanical until you discover Python renders a NUMERIC rating as 4.0 while Go prints 4, and a float32 confidence widens to 0.8999999761581421. We reproduced Python's float rendering in Go and ran live byte-for-byte parity tests on every read endpoint before deleting the old service.
  • SSR without a server build step. Node 22 strips TypeScript types natively, so server.ts runs directly, but it does not transform JSX, so the React tree is bundled separately by Vite. We also learned the hard way that a plain node --watch restarts forever because Vite writes its cache inside node_modules.
  • Evaluating a stochastic reranker honestly. Repeated model calls are not independent users. We built an evaluation harness with repeated runs, bootstrap confidence intervals, fixture hashes, and a frozen holdout so we could tell real ranking improvements from noise.
  • Knowing when the algorithm is not the problem. Some low scores turned out to be catalog gaps: an outdoor worker on a tight budget who needs extreme longevity may genuinely have no good match yet. Surfacing that honestly beats making the model sound confident.

Accomplishments that we're proud of

  • A complete grounded flow that works end to end: a plain request in, then a typed profile, vector retrieval, deterministic scoring, bounded rerank, diversity control, and a streamed, inspectable explanation out.
  • Measured ranking quality on 30 Indonesian personas that we labeled by hand: the recorded A/B run lifted nDCG@3 from 0.778 to 0.852 and hit@1 from 0.833 to 0.933 (p = 0.0137 on a paired sign flip test), with no violations of budget or avoided notes on that fixture. We present this as offline evidence, not a production guarantee.
  • A backend rewrite to Go, cut over with zero API breakage: on the read endpoints, the new service answers with responses identical to the old one down to the last byte.
  • A renewable ingestion pipeline that preserves the provenance of every record, instead of opaque scraping.
  • An editorial, bilingual interface with SSR, WebGL, and motion design that feels closer to a fragrance magazine than a database frontend.

What we learned

The useful architecture is not "LLM versus rules." Code should protect facts and constraints; the model should resolve contextual tradeoffs inside a bounded candidate set. LLMs shine when intent is thin and contextual, while deterministic signals win when constraints are explicit.

We also learned that honest evaluation changes the roadmap. Some of our worst scores pointed at missing catalog segments, not ranking bugs, and that insight is worth more than a demo that merely sounds better. And on the engineering side: parity tests make bold rewrites boring, and boring is exactly what you want in the middle of a hackathon.

What's next for Notemae

  • A licensed retailer feed for authoritative fragrance notes, performance data, and real Indonesian prices.
  • A fine-tuned model for preference extraction, replacing the general-purpose parsing step.
  • A richer dupe graph with similarity signals validated by the community.
  • A public beta behind the existing HTTPS deployment.

Built With

Share this project:

Updates