DailyMedToday is a daily-meditation application: a new faith-based meditation every day, an archive that grows with each one, and readers who arrive not with keywords but with situations. “I’m anxious about a decision.” “I can’t let go of a grudge.” “I feel far from God this week.”
The entire product question is whether the archive can meet a person where they actually are — and that’s a semantic search problem wearing the clothing of a devotional.
The stack
DailyMedToday is deliberately boring: a Laravel application backed by PostgreSQL, with Redis and queue workers, deployed on Kubernetes.1A future roadmap item also builds a React Native mobile application atop the same API. The AI layer is the architecture from part 2 of this series — embeddings stored in pgvector alongside the content rows, HNSW-indexed, queried by cosine distance.
Each meditation gets embedded by a queued job at publish time; queries get embedded at search time; Postgres ranks by distance.
There is no separate vector database, no ML service tier, no team of specialists — the meaning layer lives inside the same boring, reliable stack that serves the pages.
Search by feeling, not by keyword
The feature this enables is the same that makes the product work: readers search by state of mind.
A query like “overwhelmed at work and can’t rest” surfaces meditations about Sabbath, about laying down burdens, about being still — content that shares almost no vocabulary with the query but shares all of its meaning. Keyword search structurally cannot do this; the reader’s language and the writer’s language barely overlap, and the connection between them is conceptual.
The same embeddings drive a second feature for free: related meditations. Each meditation’s nearest neighbors in vector space become its “continue reading” suggestions — no editor curating links, no tag taxonomy to maintain, and the suggestions stay current automatically as the archive grows.
One embedding column, two user-facing features.
Production lessons
- Embed on a queue, always. Embedding belongs nowhere near the publish request path. A failed embedding job retries invisibly; content is never blocked on the AI layer.
- Version your embedding model like a schema. Vectors from different models are incompatible (recall part 1’s lesson). The model identifier lives next to the vectors, and changing models means a re-embed migration.
- Hybrid search earns its complexity. Scripture references are exact identifiers; “Psalm 23” must match Psalm 23. Full-text and vector search run side by side. The blend of approaches outperforms either in isolation.
- Fixed costs are real but small. The archive is thousands of items rather than millions — at this scale pgvector adds no operational burden whatsoever, and search latency is dominated by generating the query embedding.
Where the local stack slots in
DailyMedToday launched on cloud embedding APIs — they were the pragmatic choice at build time, and the Embedder implementation from part 8 is precisely how the dependency stays shallow. The migration path the rest of this series built — ext-infer generating embeddings in the same worker process that runs the queue jobs — turns every search and every publish into a zero-network, zero-marginal-cost operation.
Same interface, different binding; the application code doesn’t change.
The beauty nobody designed
Months of daily meditations, each written on its own — no content calendar enforcing themes, no taxonomy, no tags. But then I exported every embedding, flattened the space with UMAP, and looked at the archive as a map for the first time.
The map had structure. Distinct islands of content. Tight groupings that nobody — not the me nor the platform — had ever defined.
What those islands turned out to be is where we’ll go next.
- 1A future roadmap item also builds a React Native mobile application atop the same API. ↩︎
![Cloud Vortices off Isla Socorro [Detail]](https://i0.wp.com/eric.mann.blog/wp-content/uploads/2026/11/4642068978_906acebb9d_b.jpg?resize=1024%2C900&ssl=1)
Discussion