scentvec GitHub
Open-source embedding model · v1.0.0

A perfume, as 128 numbers.

scentvec reads a perfume's note pyramid (top, heart and base notes) and places it in a space where similar compositions sit close together. It comes with a search engine over 44,658 perfumes, a command line, a local web app and a JSON API, and it runs on a laptop CPU.

  • 44,658perfumes in the catalog
  • 1,888brands
  • 491,055parameters, 2.3 MB
  • MITlicensed code

Compose two perfumes. Watch the model read them.

This is the trained model itself, running in your browser with no server. Type notes into each pyramid: the model predicts the accords and the olfactive family of each formula, and tells you how close the two are.

–similarity, A to B
Loading the model…
Loading the model (0.65 MB)…

The demo knows the 1,053 notes that have their own entry in the model, which covers the common ones. The Python package goes further: it also embeds notes it has never seen, from their words and from an odor-chemistry profile. Accord scores are independent, so several can be high at once. The family guess is right about 64% of the time on perfumes held out from training.

What the full package does

Install it and the same model is wired to a catalog of 44,658 perfumes from 1,888 brands. Every command also answers in JSON, and the same functions are available from Python and over HTTP.

Find what is similar

Start from a perfume you know. Results list the notes and accords they share with it, and other editions of the same line are hidden unless you ask for them.

scentvec similar "creed aventus"

Search in plain words

Say what you want and what you do not. Notes, accords, "not too sweet", "no patchouli", plus filters for brand, year, gender and rating count.

scentvec search "fresh citrus for summer, no musk"

Blend, or bring your own

Ask for something between two perfumes, or embed a note list that is in no catalog: your own formula or brief.

scentvec blend "baccarat rouge 540" "tom ford tobacco vanille"

Real output

Catalog search runs on your machine, not on this page, because the perfume data belongs to third parties and is not redistributed. These are unedited top results from the released model.

scentvec similar "creed aventus" -k 5
  1. Supremacy (Silver) · Afnan Perfumes, 20130.913shares 11 of its 13 notes: apple, pineapple, birch, oakmoss, ambergris…
  2. Black Amber & Patchouli · Dermacol, 20150.875shares 9 of its 10 notes
  3. Man.Aubusson Sport Gray · Aubusson, 20150.872shares 10 of its 11 notes
  4. Invincible · Tonino Lamborghini, 20180.872shares 10 of its 11 notes
  5. Resurrection · Revolution, 20220.872shares 10 of its 11 notes
scentvec search "smoky vanilla and tobacco, not too sweet, no patchouli" -k 5

Understood as: notes vanilla, tobacco; accord Smoky; less Sweet (a penalty); never patchouli (a filter).

  1. Cuba Royal · Cuba, 20101.036incense, tobacco leaf, cedarwood, vanilla
  2. Anti-Blues · Pierre Guillaume, 20190.975vanilla, leather, vetiver, tobacco, incense, hay…
  3. Oud Tabac · Amouroud, 20180.941oud, incense, pipe tobacco, vanilla, labdanum…
  4. Broken Theories · Kerosene, 20150.937incense, tobacco, woody notes, spicy notes, vanilla
  5. Encens Précieux · Van Cleef & Arpels, 20240.929incense, leather, myrrh, amber, tobacco, vanilla…

The first score is cosine similarity between embeddings. The search score adds note matches and accord fit to it, so it can pass 1.

Measured, with baselines

The test: two independent perfume databases often list the same perfume with different notes. Given one listing, find its twin in the other database. Every test perfume was kept out of training. Scores are mean reciprocal rank, where 1.0 means the twin is always the first result.

TaskQueriesCandidatesscentvecBest baselineGain (95% interval)
Parfumo listing → its twin6912,5660.8720.849+0.023 (+0.009 to +0.036)
The reverse direction6992,6370.8560.826+0.030 (+0.015 to +0.046)
Parfumo listing → twin, whole database69126,3190.7790.739+0.039 (+0.024 to +0.055)
The reverse, whole database69926,6660.7580.716+0.042 (+0.024 to +0.059)

The best baseline is latent semantic analysis over bags of notes. The gain is real but modest, and comes from a single training run. On label prediction scentvec only ties plain logistic regression (accord mAP 0.781 against 0.782; family accuracy 0.639 against 0.640). Full tables, ablations and the bootstrap test are in the model card.

What it cannot do

  • It has never smelled anything. It sees the published note list only. Two perfumes that smell alike but list different notes will land far apart, and note lists are marketing as much as chemistry.
  • No concentrations, no materials. A pyramid says "rose", not how much or which one. The model cannot tell a whisper of oud from an overdose.
  • Text search is a parser, not a chatbot. It picks out notes, accords, negations and filters from English. It does not understand moods or occasions it has no words for.
  • The data is community-edited. Listings contain mistakes and duplicates. About 4,500 fabricated rows were removed from one source during cleaning, and a few hundred may remain.

Run it yourself

Python 3.9 or newer, git, and about 1.5 GB of disk, most of it PyTorch. The repository ships the code and the trained model. The perfume data is fetched from its original hosts on first set-up, then everything runs offline.

git clone https://github.com/juw12/scentvec.git
cd scentvec
python3 -m venv .venv
./.venv/bin/pip install --upgrade pip
./.venv/bin/pip install -e .
./.venv/bin/python scripts/fetch_raw_data.py     # 36 MB, checksummed
sh scripts/rebuild_data.sh                       # clean and link the data
./.venv/bin/scentvec build-index                 # embed 44,658 perfumes
./.venv/bin/scentvec serve                       # open http://127.0.0.1:8765/
# Python: vectors only, no catalog needed
from scentvec.encoder import NoteEncoder

encoder = NoteEncoder.load("artifacts/model")
vector = encoder.embed(
    top=["bergamot", "lemon"],
    middle=["rose"],
    base=["musk", "cedarwood"],
)   # numpy array, shape (128,)
# JavaScript: the port this page runs
<script src="scentvec.js"></script>

const model = await ScentVec.load("./");
const a = model.embed({
  top: ["bergamot", "lemon"],
  middle: ["rose"],
  base: ["musk", "cedarwood"],
});
model.describe(a.vector).accords;