One search box over everything: listings, companies, contacts, notes and the text of what was sent #29

Closed
opened 2026-09-05 14:04:08 +00:00 by tiagoagueda · 0 comments
Owner

Why

Search exists in exactly two places, each over its own list: the applications table (q across role, company and location, ApplicationFilterMixin) and the companies table (q over the company). Everything else that a person writes or keeps is findable only by knowing where it is.

The text worth searching is already stored, model by model: Company.notes, Contact.notes, JobPosting.description, ApplicationEvent.summary and body, Reminder.summary, CoverLetter.body, CV.summary, UploadedDocument.notes, the career record's summaries — and RenderedDocument.source_text, "text as sent", kept precisely so that "what did I claim?" can be answered without opening a PDF (documents/models.py, line 285). Nothing reads it back today.

Shape

  • A search box in the header, small, expanding on focus, with / as the keyboard shortcut (a delegated handler in app.js, no inline script). Submitting goes to /search/?q=…, a page with results grouped by kind — listings, applications, companies, contacts, notes and events, letters, CVs, sent documents — each group capped with a more link, each hit showing the matching excerpt with the term highlighted.
  • Portable first. Postulo runs on SQLite by default and PostgreSQL optionally, so the first implementation is one search(user, query) function per model using case-insensitive containment on the fields above, unioned and lightly ranked (title hits before body hits). Everything through for_user(); a result is never shown that its list would not show.
  • The seam for later. The per-model function is the place where SQLite FTS5 or PostgreSQL SearchVector can be plugged in when the containment version gets slow, without touching the page. Not before: a personal instance has thousands of rows, not millions.
  • Sent text is the special case: a hit in source_text links to the application it was sent with and says "in the CV you sent on 12 May", which is the question people actually have.
  • API and agents. GET /api/v1/search under #12's read scope, and one MCP tool in #19 that wraps it.

Classification

Enhancement. Not breaking: a new page, a new box, no schema.

Open questions

  1. Live results while typing (htmx, the same pattern as #20) or a results page only? Proposal: the page first; live results are an afternoon on top once it exists.
  2. Should search cover discarded listings (#25) and closed applications by default, or behind a switch? Proposal: everything, always. The person is looking for something they remember.
## Why Search exists in exactly two places, each over its own list: the applications table (`q` across role, company and location, `ApplicationFilterMixin`) and the companies table (`q` over the company). Everything else that a person writes or keeps is findable only by knowing where it is. The text worth searching is already stored, model by model: `Company.notes`, `Contact.notes`, `JobPosting.description`, `ApplicationEvent.summary` and `body`, `Reminder.summary`, `CoverLetter.body`, `CV.summary`, `UploadedDocument.notes`, the career record's summaries — and `RenderedDocument.source_text`, "text as sent", kept precisely so that "what did I claim?" can be answered without opening a PDF (`documents/models.py`, line 285). Nothing reads it back today. ## Shape - **A search box in the header**, small, expanding on focus, with `/` as the keyboard shortcut (a delegated handler in `app.js`, no inline script). Submitting goes to `/search/?q=…`, a page with results **grouped by kind** — listings, applications, companies, contacts, notes and events, letters, CVs, sent documents — each group capped with a *more* link, each hit showing the matching excerpt with the term highlighted. - **Portable first.** Postulo runs on SQLite by default and PostgreSQL optionally, so the first implementation is one `search(user, query)` function per model using case-insensitive containment on the fields above, unioned and lightly ranked (title hits before body hits). Everything through `for_user()`; a result is never shown that its list would not show. - **The seam for later.** The per-model function is the place where SQLite FTS5 or PostgreSQL `SearchVector` can be plugged in when the containment version gets slow, without touching the page. Not before: a personal instance has thousands of rows, not millions. - **Sent text** is the special case: a hit in `source_text` links to the application it was sent with and says "in the CV you sent on 12 May", which is the question people actually have. - **API and agents.** `GET /api/v1/search` under #12's `read` scope, and one MCP tool in #19 that wraps it. ## Classification Enhancement. Not breaking: a new page, a new box, no schema. ## Open questions 1. Live results while typing (htmx, the same pattern as #20) or a results page only? Proposal: the page first; live results are an afternoon on top once it exists. 2. Should search cover discarded listings (#25) and closed applications by default, or behind a switch? Proposal: everything, always. The person is looking for something they remember.
tiagoagueda added this to the 0.2.0 milestone 2026-09-05 14:04:08 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Postulo/postulo#29
No description provided.