Skip to content

Finding things — search

Retrieval is always hybrid

Two rankers, always both, fused:

Ranker Is
Lexical PostgreSQL full text — ts_rank_cd over websearch_to_tsquery('english', …)
Semantic pgvector cosine similarity, with a floor of 0.45

Fused by Reciprocal Rank Fusion with k = 60, the constant from the original paper: each ranker contributes 1 / (60 + rank + 1) and the scores are summed. It is a union, not an intersection — a row need only be found by one of them.

A quoted phrase query makes the semantic ranker sit out. If you asked for "absence seizure" in quotes, you meant those words.

There is deliberately no OpenSearch or Elasticsearch here. A second datastore to keep in sync is a second thing that can be stale.

Articles are searched twice

An article is searched as itself and as its sections, and the two rankings are fused again:

  1. The article row — title, summary and a topical vector — goes through RRF.
  2. Every published section, projected into a search index, goes through RRF separately.
  3. Only the section ranking is reranked by the cross-encoder. Handing a cross-encoder the article row means handing it a stub; its best section stands for it instead.
  4. The whole-article score and the best-matching-section score are fused by the same RRF formula into the final order.

Where the reranker is and is not

Applied Not applied
Article section hits (and so article search) The typeahead
The builder's "describe a test" path Flashcard and media search
The AI Mode shortlist The AI Mode closeness thresholds

The reranker is a permutation and nothing else. An unset, unreachable, slow or malformed reranker returns the ids untouched — never fewer, never reordered wrongly. At most 50 candidates are sent; past that the round trip costs more than it is worth. Results are cached in Redis for a day, keyed on model, query and document text.

Default model cohere-rerank-v4.0-pro, overridable at settings:rerank_model in Redis from the admin settings page. Blank is a valid answer and means "keep the fused order".

Full note: Reranking.

The global search endpoint

GET /api/v1/search?q=&kinds=article,flashcard,media&limit=10

q must be at least 2 characters. limit defaults to 10 and caps at 50.

Kind Returns
article id, slug, title, snippet, summary, status, section counts, and the top 6 matching sections with their variants
flashcard Decks, never individual cards. Listing cards turns the page into a dump of fronts and backs and gives the answers away. Each row carries the matching-card count, the deck size, and whether the learner has started it.
media Figures, and only figures hanging on a published article. Question stem and explanation figures are excluded from search for everybody, moderators included.

The response also carries topic — a side panel built from the single best-matching article's summary plus up to 6 of its figures, which is the definition card you see beside the results.

Questions are not searchable

There is no question kind. ?kinds=question yields nothing rather than an error.

Meeting a question outside a sitting spends it — you cannot un-see a stem — and there is no route from a search result into a session anyway. The place to find questions is the session builder or the question bank browser.

The typeahead

GET /search/suggest is lexical only — a prefix match against article titles. No embedding, no reranker. A semantic neighbour of half a word is noise.

Visibility

Search never reimplements permission. It reuses the same draft rule as the article routes, the same deck sharing rule, and the same media library grants.