Mood-Based Discovery
A discovery tool that surfaces archive content by emotional resonance, not keyword match.
SAR has thousands of pieces in the archive, and most as relevant today as the day they ran. Readers find new work through the homepage and social channels, but older work goes unnoticed. WordPress search isn’t built for subjective content, tagging doesn’t scale, and manual curation is a time investment that a small organization doesn’t have. I built a discovery tool that takes emotional resonance as the search primitive with humans as the judgment layer.
When content resists categories, keyword search and manual curation both fail.
Content can be melancholic and hopeful and political at once. Readers searching the archive aren’t typing keywords; they’re typing feelings, often complicated ones. “My dog died last week. I’m heartbroken but relieved he’s no longer suffering.” That isn’t “grief” the category. That is an emotional state that resists categorization. Indexing harder doesn’t solve a vocabulary problem.
The conventional answers — tag everything, add more category pages, curate manually — fail at the levels they have to succeed at. Tags are inconsistent across years of contributors and content managers. Categories flatten what’s interesting about the content into bins that it doesn’t fit. Manual curation works but doesn’t scale.
An archive becomes invisible the moment its discoverability depends on someone remembering it’s there. For content managers, the archive is the publication’s largest underused asset. For readers, it’s a body of work they’ll never find unless we surface it for them. The tool had to do both jobs.
Embeddings of classification, not text, using content managers as the classification authority.
Two architectural decisions did most of the work. First: every piece is classified offline by an LLM and then reviewed by an editor. The LLM suggests primary, secondary, and optional tertiary moods plus a central theme, while the content manager confirms or changes each one. Second, the embedding used in the vector index isn’t generated from the piece’s raw text. It’s generated from a structured representation — summary, moods, themes — so the human-reviewed classifications are baked directly into the semantic fingerprint. A search for “sad but relieved about losing a pet” finds pieces whose classification matches that emotional shape, not pieces that happen to share surface vocabulary.
At runtime, the user types a natural-language query, the query passes a two-layer safety check, gets embedded, and queries the Pinecone index. The top three matches plus the user’s query go to an LLM running with the content explorer system prompt — a warm but grounded character whose only job is to connect users to pieces and explain why they might resonate. No advice, no chatting, no therapy. When the safety layer flags self-harm or violent intent, the system returns crisis resources and recommends nothing.
The places where this tool could have crossed into human judgment, therapy, or surveillance, but didn’t.
The decisions below are all places where automation could have done more work, but would have made the system worse. Each one defends a relationship: content manager authority, user safety, or user privacy.
A human-curated classification system, a structured embedding pipeline, a conversational interface with a defined character.
Each piece is classified with a primary mood, a secondary mood, an optional tertiary mood, and a theme (its central idea). The vocabulary is deliberately broad — narrow categories like “sad” and “happy” can’t carry what readers actually search for, which is closer to “conflicted relief” or “ambivalent grief.” The vocabulary itself was bootstrapped from an LLM analysis of a corpus subset, then reviewed, refined, and finalized by human judgment. It continues to evolve as the archive grows and the system is tested against real queries.
Claude handles the initial pass at assigning moods and themes. It’s faster and more consistent at this kind of grunt work than a human starting from scratch every time. The content manager reviews every assignment before it goes into the index. When the LLM misclassifies, and it does (e.g. a piece about losing a dog was tagged as relevant to losing a human best friend, which is a different emotional shape), the human catches it on review, or, sometimes, only when the misclassification shows up in a search result. Both kinds of error feed back into refinement.
The embedding text for each piece isn’t the piece’s raw text. It’s a structured representation: summary, moods, themes. That string gets embedded by OpenAI’s embeddings API and upserted to Pinecone with the piece’s metadata (title, author, URL). At under five thousand vectors, Pinecone’s free tier handles the index. The structured-embedding choice means a search for emotional shape matches pieces classified for that shape.
The interface is conversational because discovery is. Readers describe emotional states in their own words, and the system finds pieces and explains why each one might resonate. The character is defined by what it isn’t as much as by what it is. It isn’t a corporate proxy, a customer-service bot, a friend or a therapist. Its only job is to connect users to pieces and provide emotionally resonant descriptions. No selling, no answering account questions, no chatting. When users ask for things outside that scope, it acknowledges the constraint and pivots back to the mission.
Discovery in a corpus that includes pieces about grief, violence, illness, and despair means the safety layer has to be unusually careful. Exploring a dark theme in a piece is different from expressing harmful intent in a query, and the system has to distinguish between them. Two layers: keyword detection (fast, catches the obvious), then OpenAI’s moderation endpoint (broader, catches what the keyword pass misses). When either fires, the assistant does not recommend pieces; it returns a gentle redirect to crisis resources and the 988 line. The backend enforces that behavior, and the LLM cannot override it.
A Python API hosted on Render exposes a single /chat endpoint: it takes a user query, runs the safety check, embeds the query, queries Pinecone for the top three pieces, sends those pieces plus the user message to OpenAI chat with the Poetry Explorer system prompt, and returns the response with titles, authors, and URLs to the frontend. The frontend is a static page hosted on Netlify, embedded on SAR. Free tier on Pinecone, low-cost on Render and OpenAI.
Conversations are not stored in a database or persisted long-term. Readers describing grief, anxiety, or vulnerability to the tool are not generating a permanent record of that state.
An archive that’s discoverable by feeling and a content pipeline that scales.
Preserved
- Human judgment determines what every piece is about. The index is built on human review, not LLM output.
- The publication’s voice and posture. The character is a guide on behalf of the publication, not a generic assistant.
- User safety and privacy. Distress gets crisis resources and conversations don’t persist.
- The tool is a force multiplier for people, not a replacement for their work.
Now possible
- Content managers compose Collections by searching for emotional shape — “ambivalent grief,” “quiet defiance,” “hope without resolution” — instead of remembering which piece felt like a particular thing.
- Themed packages come together in minutes rather than hours of browsing the archive.
- Older work stays in rotation and keeps finding new readers.
- Content managers recover hours per week of curation time, freeing them for contributor outreach and developmental work.
- The pattern ports to any archive of subjective content where category vocabulary doesn’t fit the search vocabulary, like academic papers organized by argument shape, music libraries by feel, regulated content libraries where context matters more than keywords, and any catalog whose users search for resonance rather than name.
Misty Cripps · Vaquita Design