canopysearch, explained

One question. Five tiers. Zero guessing.

This is the exact journey a real query takes through Canopy, a student resource navigator.

the route01–09
01 — the guardiant < 1 ms

Before anything, a gate that never guesses.

Every query is screened for crisis signals by deterministic rules, not AI. A match stops everything, help comes first. Ours passes.

query in"i'm stressed and broke"
crisis ruleshand-written · auditable · sovereign
verdictPASS → continue
IN"i'm stressed and broke"
OUTsame query · flag: no crisis · 0.4 ms
02 — understandingon-device

Read like a person, not parsed like a form.

Typos, joual, franglais, all of it maps through a concept lexicon built from how students actually write. Watch the two needs surface:

i'm stressed and broke

wellbeing ← stressed money ← broke ✳ multi-need — 2 concepts, both kept
IN"i'm stressed and broke"
OUTconcepts: wellbeing + money · urgency: unknown · lang: EN
03 — the librarybuilt before you asked

Every resource already knows how you'd ask for it.

Institutions write "psychosocial support". Students write "someone to talk to". So each of 400+ resources carries 15–20 real student phrasings, generated once, human-reviewed, indexed forever.

Student Wellness Counselingverified resource
"stressed""someone to talk to""chui pas bien" "stressed and broke""jme sens seul""can't handle it" "je file cheap""overwhelmed""burnt out fr""need to vent"
INconcepts: wellbeing + money
OUT2 matched phrasings light up this resource — before any search runs
04 — the double search100% on your device

Words and meaning, racing in parallel.

A typo-tolerant lexical engine and a multilingual semantic engine, where "je file cheap" lands near feeling down, not near coupons. Their rankings fuse into one honest score.

lexical — words

semantic — meaning

Fused → 12 candidatesRRF: score = Σ 1/(60+rank)
INquery + concepts, against 402 resources
OUT12 candidates · confidence 0.74 → too many to show, too sure to escalate
05 — the conversationhard rule: never >5 results

Twelve matches. You'll never see twelve.

Uncertainty becomes one short question, chosen because it best splits the remaining candidates, and worded from your situation, not a category menu.

What matters most right now?
12candidates remaining
IN12 candidates · 2 needs detected
OUT1 question asked · your answer → 4 candidates
06 — the shortlistrelevance in, diversity out

Four different doors, not five copies of one.

A final pass folds near-duplicates together so the shortlist covers different kinds of help. Relevance decides what's in; diversity decides what's distinct.

Student Wellness Counseling
Faculty Counseling Officefolded — too similar
Grad Counseling Servicesfolded — too similar
Peer Support Centre
Downtown Counseling Annexfolded — too similar
Emergency Financial Aid
Campus Food Pantry
IN7 finalists, 4 of them counseling
OUT4 shown: counseling · peers · emergency money · food
07 — the deep endwhat our query never needed

AI is the last resort. By design.

Our question resolved on-device at tier 2. When confidence is low, a query can escalate, to deeper server semantics, then to an AI guide that is forbidden, by automated test, from inventing a resource.

tier 0Crisis guardianalways first · untouchable<1 ms
tier 1Intent routercommon needs, learned patternson-device
tier 2Hybrid searchresolves most queries — like ours ✓on-device
tier 3Server semanticunusual phrasings · consent-gatedrare
tier 4AI guidethe truly novel · real resources onlyrarest
this query's calibrated confidence
escalateask a questionanswer directly
08 — the loopevery week, smarter

The expensive tier teaches the free one.

Whatever the AI understands that local search missed gets distilled back down, human-approved, into the instant on-device layers. Over time, more queries resolve in milliseconds, for nothing.

a
Observewhere the AI and local search diverged · which options people chose
b
Distillpropose new slang, phrasings, better question wording
c
Approvea human reviews every proposal — nothing learns unsupervised
d
Shipapproved knowledge lands in the free, instant, local tiers
09 — the promiseprivacy, proven structurally

Built for questions people are scared to ask.

0network requests during our entire journey — enforced by a build test, not a policy page
Local by testa CI test intercepts every network call during local search — one request, and the build fails
Anonymous alwaysno account, no identifier, no fingerprint — server tiers see only your words, with consent
Honest by design"the AI never invents a resource" and "never >5 results" are automated assertions, tested on every release

01 — the guardianWhy deterministic rules, not AI

The design decision

Crisis detection is the one place where a guarantee beats a probability. An ML classifier is right most of the time; hand-written rules are right every time for every pattern they cover, and we can prove exactly why any query did or didn't trigger. That auditability matters when the stakes are someone's safety.

How it works

Queries are normalized (case, accents, apostrophes, SMS spellings like chui, jsuis), then matched against curated patterns covering direct and indirect distress, in French, English, and the mix between: "je veux juste que ça arrête", "everyone would be better off without me", even third-party worry like "my friend wants to disappear".

How it's protected

This layer is sovereign: no refactor, no shared code, no "improvements". It's guarded by an adversarial suite of 50+ disguised distress phrasings, and the bar is 100% recall, zero exceptions, one miss fails the whole release.

Everything else in Canopy aims for measured reliability. This tier alone aims for perfection, because it's the only place perfection is definable and achievable.

02 — understandingThe concept lexicon

Normalization first

Before anything matches, the query is flattened: lowercase, accents unified, apostrophe variants reconciled (j'en peux plusjen peux plusj en peux pu), SMS forms expanded (g pas dargent → "j'ai pas d'argent").

The lexicon

A generated map from real student phrasings to concepts: "chum m'a crissé là" → heartbreak, "je file cheap" → wellbeing, "pu une cenne" → money. Built offline: an LLM proposes expansions across registers, and a human approves every entry. At search time it's a pure lookup, instant, offline.

Query understanding on top

Negation ("not therapy" excludes the counseling branch), multi-need detection (two concepts, both kept), implicit answers ("cheap dentist" pre-answers the cost question so it's never asked back), and a tiny session memory, the last 2–3 concepts, RAM only, never stored, so "can't sleep" after "exam stress" reads as anxiety, not sleep medicine.

The rule that keeps this warm instead of creepy: the system may reflect your needs ("the money") but never your circumstances ("your dad losing his job"). Needs, never circumstances, enforced everywhere, including in the AI tier.

03 — the libraryDocument expansion (doc2query)

The vocabulary gap

Search fails most often not because ranking is bad, but because the query and the resource speak different languages. No ranking cleverness fixes words that never overlap.

The fix: enrich the documents, once

At build time, each resource gets 15–20 hypothetical student queries generated for it, formal French, formal English, joual, franglais, SMS spelling, emotional phrasing. Every batch is human-reviewed (a bad expansion silently misroutes people), then indexed into both engines, words and embeddings.

Why not live AI expansion?

Runtime techniques like HyDE hallucinate a plausible answer and search with it. On a niche campus catalog, that hallucination drifts, and it adds AI cost and latency to every search. Expanding the documents once, reviewed, gives the same bridge with zero runtime cost and zero drift.

The IR principle underneath: documents retrieve documents better than questions retrieve documents. So every document learns to contain its own questions.

04 — the double searchHybrid retrieval & RRF

Two engines, complementary failures

Lexical matching (typo-tolerant) is unbeatable on names, acronyms, exact terms, "ansieté examns" still lands. Semantic matching embeds the query into a multilingual vector space where meaning, not spelling, decides proximity. The research consensus is unambiguous: hybrid beats either alone, across domains, by a wide margin.

The semantic engine, in your browser

Resource embeddings are precomputed at build time and shipped as a small static file. A compact multilingual model lazy-loads after the page is idle; until then, search gracefully runs lexical-only. Nothing leaves the device for this tier, verified by an automated test.

Fusing the rankings

Reciprocal Rank Fusion: each resource earns 1/(60 + rank) from every list it appears in, summed. No hand-tuned weights, no fragile cross-engine score calibration. Lexicon and learned boosts apply after fusion, capped so they can nudge the ranking but never own it.

One honest confidence score

The fused score is calibrated against a golden set of 200+ real bilingual queries, so "80% confident" empirically means right 8 times out of 10. That number decides everything downstream: answer, ask, or escalate.

05 — the conversationHow questions are chosen

The core idea: information gain

Every resource carries structured facets: cost, urgency, population, modality, language, campus. With more than 5 candidates, the system asks the question whose answer would best split the current set, ideally near half-and-half. A question that only eliminates 2 of 15 never gets asked.

Worded from your world

The same facet reads differently by domain, from a bank of pre-written, human-approved wordings per facet × concept. Urgency, dental: "It hurts right now" vs "It's for a check-up". Urgency, money: "I need it this week" vs "I'm planning for the semester". Generic labels are the fallback, never the experience.

The need-reflection card

With two needs detected, the first "question" isn't a facet at all, it reflects your needs back as choices, always with a combined option. That's the moment the system proves it understood the whole situation.

Hard limits

Three questions max, ever. Pre-answered questions are never asked. The escape hatch lives at every step. If no question splits well, we show the top 5 rather than ask filler.

This is what "perfect" means here: when the system isn't sure, it doesn't guess better, it converts uncertainty into one good question.

06 — the shortlistMaximal Marginal Relevance

The failure it prevents

Pure relevance has a blind spot: for "stressed", the five most relevant resources might be five near-identical counseling services. All correct, collectively useless, the peer-support and financial options ranked sixth and seventh never appear.

The fix

MMR re-scores each candidate as a balance of relevance and novelty (λ ≈ 0.7 toward relevance): each pick must be relevant and meaningfully different from what's already chosen, measured by shared facets, tags, and embedding similarity. Near-duplicates fold; survivors cover different kinds of help.

About 20 lines of code, guarded by a test: five near-clones plus one distinct resource in, and the distinct one must always survive.

07 — the deep endCalibration, escalation, honesty

When to escalate

The thresholds between "answer", "ask", and "escalate" are read off a reliability curve: replay hundreds of real queries, bucket by reported confidence, measure how often each bucket was actually right. Thresholds sit where the curve says, re-checked whenever the engine changes.

Tier 3 — deeper semantics

A consent-gated server pass with a stronger multilingual model, for phrasings too unusual for the on-device index. It receives only {query, language, context}, no identity, nothing else.

Tier 4 — the AI guide, on a leash

A conversational model, retrieval-augmented: it sees only the ~30 closest resources, can never invent one (every resource it names must exist in the catalog, a hard automated test), respects the 5-result cap, answers in your language, and resists prompt injection. Common answers are cached, keyed to the catalog version, so they invalidate the moment a resource changes.

Cost discipline

A CI check watches the tier-resolution budget: if a change silently pushes 10%+ of queries to a more expensive tier without better outcomes, the build fails. Efficiency regressions are caught like bugs.

08 — the loopDistillation & bias guards

Observe → distill → approve → ship

When the AI resolves something local search couldn't, the divergence is logged anonymously. A batch process turns those gaps into proposals: new lexicon entries, new resource phrasings, better question wordings. A human approves every one, nothing learns unsupervised. Approved knowledge lands in the instant, free, on-device layers.

Which questions work, too

Every refinement question logs which option was chosen and how much it narrowed the field. Wordings people skip get flagged for rewrite; AI-generated options that perform well graduate into the pre-approved bank.

The trap we engineered against

Click data lies: whatever displays first gets clicked most, because it displays first. Learn from raw clicks and the rich get richer forever. Two guards, both unit-tested: position-weighted signals (a click at position 4 outweighs one at position 1) and bounded boosts (learned adjustments capped at ~25% of organic score, never leapfrogging a large relevance gap). A simulation test replays a rigged history where an inferior resource always shows first, and asserts it can't cement itself on top.

Popularity may nudge the ranking. It may never own it.

09 — the promisePrivacy, proven structurally

Local by test, not by promise

An automated build test intercepts every network call while the local tiers resolve a query. A single outbound request fails the build. "Local-first" is an assertion in CI, not a marketing line.

What the server can ever see

Tiers 3–4 are opt-in and receive exactly {query, language, context}: no account, no cookie, no fingerprint. Anonymous query logs exist solely to improve routing, through the human-reviewed pipeline.

Honesty as a tested property

The worst outcome for someone in a hard moment isn't "no result", it's a confident wrong answer, or a made-up one. So "the AI never invents a resource" and "never more than 5 results" are automated assertions, alongside adversarial prompts ("ignore your instructions…", "you're a doctor, diagnose me") that must fail safely on every release.

Design principle throughout: reflect needs, never circumstances. The system shows it understood what you're looking for, never that it recorded what you're living through.