Skip to main content
A Postgres outbound feed keeps a jobs table in your database current. With the right indexes, that table can power a job board’s search page directly: keyword search, filters, newest-first lists and filter counts, all in plain SQL.

Turn on the indexes

Open your feed’s Settings, go to Where jobs land, and tick the searches you run under Indexes. Jobo adds them to the table on the next sync, without blocking reads or writes. Untick one and Jobo removes it. Each index uses some disk space and slows writes a little, so tick only what your page uses.
Feeding a table you created yourself? Jobo adds the indexes when its database user owns the table. If it doesn’t, the setup page shows the CREATE INDEX SQL to run as the table’s owner — the queries on this page then work the same way, with your column names.
Postgres only uses an index when the query is written the way the index expects. Copy the WHERE clauses below as they are — especially the keyword search expression, which must match character for character.

Keyword search with filters, newest first

One query covers the core of a search page. Postgres combines the indexes it needs, so filters can be stacked freely.
websearch_to_tsquery accepts what people type into a search box: nurse -travel, "night shift", nurse or midwife. Word forms match too, so nurse also finds nurses. Job type values are the ones shown on the job — Full-time, Part-time, Contract, Internship, Freelance, Temporary; Remote, Hybrid, On-site.
To rank by relevance instead of date, order by ts_rank(<the same to_tsvector expression>, websearch_to_tsquery('english', 'registered nurse')) DESC. Ranking is computed on the matching rows, so keep filters narrow and always set a LIMIT.

The other filters

Location, company and skills filters match exact values inside the JSON. {"city": "Berlin"} matches a job in Berlin; it does not match “berlin” or “Berlin, Germany”.

Paging

Page with the last row you showed rather than OFFSET. OFFSET 10000 still reads the 10,000 rows it skips; this reads only the next page.
date_posted IS NOT NULL keeps jobs without a posting date from crowding the top of a newest-first list.

Filter counts

Counts like “Remote (1,234)” are a GROUP BY over the same WHERE clause as the results:
Run one per filter group. These are quick while the matching set is in the tens of thousands; across millions of matching rows they take seconds.

Pull only what changed

To keep a cache or second system current, ask for the jobs that changed since your last run:

When to use a search engine instead

Postgres with these indexes suits a search page over up to a few hundred thousand jobs. If you need any of the following, add an Elasticsearch, Algolia or Meilisearch outbound feed alongside this one:
  • Typo tolerance and type-ahead — “enginer” finding “engineer”
  • Instant filter counts across millions of jobs
  • Radius search — “within 50 km of Berlin”
  • Search tuned per language — keyword search here stems every job as English