How this site works: one embedding space, a latent map, and a retrieval-grounded assistant
Overview
This site has two machine-learning features. The band of points at the bottom of the hero is an embedding map of the site's content. The assistant in the corner answers questions about my work with retrieval-augmented generation over the same embeddings. This post documents how they are built: the embedding pipeline, the map's layout and axes, the retrieval corpus and its scoring, the serving path, and the failure modes the design works around.
One embedding space
Every content object on the site is embedded in a single batch with gemini-embedding-001: each project, publication, and job, and every blog post, including this one. The map renders those vectors directly. The assistant retrieves over a 256-dimensional Matryoshka slice of the same batch. Matryoshka representation learning trains embeddings so that a prefix of the vector is itself a usable lower-dimensional embedding, which means the index can be truncated to 256 dimensions without re-embedding; the sliced vectors only need renormalization.
The practical reason for the slice is payload size: the index is a committed source file that also ships to the browser to power a retrieval inspector, so full-width vectors would be dead weight. The architectural reason for the shared batch is that retrieval results and map points then live in the same geometry with the same ids. When an answer cites sources, the client can highlight the corresponding map points by id, with no extra lookup or service; the linkage is a property of the data layout rather than a feature that had to be built.
The latent map: layout, edges, axes
Layout is principal component analysis to two dimensions over the item vectors, normalized into a unit square that maps onto a band at the bottom of the hero. Edges are cosine nearest neighbors: each item links to its closest few neighbors, weighted by similarity. All of it is dependency-free linear algebra in a module of under a hundred lines; there is no plotting library and no physics simulation.
PCA alone produces coordinates without meaning, so the rendered axes are not the principal components. Each axis is a concept direction: two pole descriptions, for example academic research and hands-on systems engineering, are embedded, and the axis direction is normalize(embed(pos) - embed(neg)). Every item's coordinate along the axis is its projection onto that direction. This is the same construction as steering vectors in interpretability work. The pole texts are paragraph-length contrastive descriptions rather than single words.
Committed artifacts and staleness
Both artifacts, the map JSON and the retrieval index, are generated by a script and committed to the repository. Committing generated artifacts has a well-known failure mode: someone edits the source content, forgets to regenerate, and nothing fails. The guard here is a content hash. The generation script hashes the exact text it embedded and stores the hash in each artifact; in development, the client rebuilds the same hash input from the current content and compares, logging a warning on mismatch. The check runs independently for the map items and the retrieval chunks, since either can drift on its own.
The hash input is the concatenation of each entry's id and embedded text, so the warning fires on any change to what was actually embedded. Fields that do not affect the embedded text, like link targets, can change without a false alarm.
The retrieval corpus
The corpus is built in three groups, in stable order. First, one chunk per map item, sharing the item's id so a retrieval hit can reference its map point. Second, one chunk per section of every blog post, split at heading boundaries, with the heading and body joined as the chunk text; this post contributes its own sections the same way. Third, standalone fact chunks for material the items do not cover: identity and contact, skills, side projects, what I am looking for, and a note on this site itself.
Three of the fact chunks are rollups: category summaries for research, publications, and experience. They exist because of an embedding-geometry problem with generic questions. A query like 'what has he published?' embeds closer to a summary of the publication category than to any individual paper chunk, so a corpus of only instance chunks misses precisely the broad questions visitors ask most. The rollup gives those queries something to land on.
Scoring: dense with a lexical assist
At question time the query is embedded into the same 256-dimensional space and scored against every chunk. The score is dense cosine similarity plus a small lexical term: 0.15 times query coverage, where coverage is the fraction of the query's content tokens present in the chunk, matched on lowercase 5-character prefix stems so that publish, published, and publications collide. Stopwords are dropped.
The order of operations matters more than the formula. A dense floor of 0.35 is applied first: chunks below it are discarded before the lexical term is considered, so lexical overlap re-ranks genuine semantic candidates but can never promote an irrelevant chunk into the results. The top four survivors become the prompt context. If nothing clears the floor, the context block states that explicitly and the assistant is instructed to say it does not have that detail rather than improvise. On a corpus this small, the lexical channel mostly breaks ties among short generic questions, which embed close to everything.
The serving path
The route is an edge function that validates input with bounded message count and length, embeds the query, retrieves, and streams the answer from a flash-tier model with thinking disabled, parsing the upstream server-sent events and re-emitting plain text. Retrieved sources travel in a response header as compact JSON; the client renders them as source pills and can expand a per-answer inspector showing each chunk's text and score, loading the chunk texts lazily from the committed index.
Failure handling is layered. If the query-embedding call fails, the route falls back to a prompt containing the full knowledge base, so a retrieval problem degrades to prompt stuffing instead of an outage. If the generation call fails, the client gets a plain-text error, and rate-limited responses point visitors to direct email instead. Because the index is a committed file, there is no vector database to be down.
Limitations
- The corpus is a few dozen chunks, so exhaustive scoring per query is the right call. The same design at much larger scale would need approximate nearest-neighbor search and a reranking stage.
- The generation model is flash-tier with thinking disabled: a latency and cost tradeoff appropriate for a personal site, not a claim about reasoning depth.
- The retrieval floor and the lexical weight are hand-tuned constants, not learned parameters.
- This describes the system as of July 2026. The content and the numbers will drift, which is what the staleness checks are for.
The system is small, but the constraints are real ones: committed artifacts drift, generic queries embed close to everything, external calls fail. Most of the design above exists to make those failures either visible or survivable.