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:
- The article row — title, summary and a topical vector — goes through RRF.
- Every published section, projected into a search index, goes through RRF separately.
- 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.
- 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¶
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.