Mood-Based Discovery

A discovery tool that surfaces archive content by emotional resonance, not keyword match.

Status In Production · 2024
Role Design + EngineeringSolo · designed and built end-to-end
Context San Antonio ReviewThe tool content managers use to surface pieces for Collections and themed packages
Stack
Python Pinecone OpenAI Embeddings + Moderation Claude (classification) Render Netlify WordPress

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.

The Design Problem

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.

The conventional approach
“Tag consistently. Rely on the search bar. Curate themed packages. The archive’s discoverability is the content manager’s work, so we just need to do more of it.”
What that misses
“Readers don’t search keywords; they search feelings. Tags can’t capture overlapping moods. Manual curation requires resources the team doesn’t have. The problem isn’t volume, it’s vocabulary.”
What I Designed

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.

OFFLINE INDEXING · EVERY CLASSIFICATION EDITOR-REVIEWED Pieces ~800 in archive LLM suggests mood · theme summary Editor reviews every assignment Structured embed summary + moods + themes (OpenAI) Pinecone index vectors + metadata queries the index at runtime RUNTIME QUERY User query natural language Safety check 2-layer: keyword + moderation Embed query OpenAI Vector search Pinecone top 3 results LLM response Poetry Explorer system prompt with safety constraints User result pieces + reasoning if flagged → crisis resources no recommendations HUMAN STEP LLM / AUTOMATED INDEX / RESULT SAFETY EXIT
Two phases. Humans are the classification authority offline, while safety is the gate at runtime. The index sits between them, built with human judgment, queried in milliseconds.
What I Deliberately Did Not Build

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.

Fully automated classification
LLMs are good at the grunt work of suggesting moods and themes, but they are not reliable judges of what a piece is actually about. Every primary mood, secondary mood, optional tertiary mood, and theme assignment passes through a human before it reaches the index. The tool’s discovery quality is downstream of that decision being made by humans.
Therapy mode or emotional advice
The character is a guide to the archive, not a chatbot or a therapist. When the safety layer detects expressed self-harm or violent intent, the system stops recommending pieces and returns crisis resources (988 in the US). It does not provide comfort, offer coping strategies, or attempt to interpret what the user is going through. The boundary is explicit in the system prompt and enforced at the backend, so the assistant can’t override it.
Embedding the raw text of pieces
If embeddings came from raw text, semantic similarity would reflect surface word patterns. Embedding from a structured representation (summary + moods + themes) means similarity reflects human-reviewed classification. A query about ambivalent grief surfaces pieces classified as ambivalent grief, not pieces that incidentally mention the word “grief.”
Long-term conversation storage
Discovery happens at moments of real emotional vulnerability, where people describe grief, anxiety, longing, or conflicted feelings about people they love. Storing those conversations long-term would treat that vulnerability as data. Conversations are ephemeral. Nothing about a user’s emotional state persists in a database.
How It Works

A human-curated classification system, a structured embedding pipeline, a conversational interface with a defined character.

Classification system

Controlled vocabulary
What each piece is “about”

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.

Human-reviewed classification pipeline

Claude · Python
AI suggests, editor decides

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.

Structured embeddings

OpenAI Embeddings · Pinecone
Classification baked into similarity

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.

Conversational interface with a defined character

OpenAI chat · system prompt
“Warm but grounded · guide, not chatbot or therapist”

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.

Two-layer safety

Python · OpenAI Moderation
Pre-recommendation gate

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.

# Two-layer safety check before any piece recommendations. # Layer 1: keyword detection (soft check) # Layer 2: OpenAI moderation classification def is_safe_query(query): if has_self_harm_keywords(query) or has_violence_keywords(query): return False moderation = openai.Moderations.create(input=query) if moderation.results[0].flagged: return False return True # When flagged, the backend returns crisis resources directly. # The LLM never sees the query; it cannot improvise around the boundary. if not is_safe_query(user_query): return crisis_redirect_response()

Backend stack and deployment

Python · Render · Netlify · WordPress embed
Production-grade on a small budget

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.

Privacy posture

No long-term storage
Ephemeral by design

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.

Result

An archive that’s discoverable by feeling and a content pipeline that scales.

~800
Pieces in the indexed archive
0
Classifications without editor review
2
Safety layers before any recommendation
0
Conversations stored long-term

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.