Typesense-backed faceted search for the Africa Multiple DRE Omeka S instance (the one populated by MongoDB2OmekaS). It indexes content directly from the Omeka dashboard via MySQL — there is no external ingestion pipeline.
Twelve search corpora ship out of the box, each as its own page block:
- Research items search — the digitised research items (resource template 10).
- Research projects search — the cluster's research projects (template 5).
- Publications search — the cluster bibliography (journal articles, books, chapters, theses, … — one item set across several templates).
- Podcasts search — the cluster's podcast episodes (template 21, item set 39095), filterable by series, the people in each episode (hosts + guests), and language; sortable by episode number or date. Cards show the series logo as the thumbnail, the abstract, and a "Listen" link.
- YouTube videos search — the cluster's recorded talks, interviews, panels and screenings on YouTube (template 22, item set 39192), filterable by playlist, speaker, and language, with a year slider. Cards show the video thumbnail, the abstract, the speakers, a "Transcript" badge, and a "Watch" link; search covers the transcript.
- People search — researchers and contributors (template 4), filterable by affiliation and role, with per-person research-item and publication counts.
- Research sections search — the cluster's thematic sections (template 7), filterable by phase and associated person, with per-section project and member counts.
- Organisations search — institutions and groups (template 2, one item set), split by a Type facet and filterable by the role each plays, with per-organisation project, research-item, and affiliated-people counts.
- Genres search — the genre/form authority terms (item set 21), with a per-genre research-item count.
- Languages search — the language authority terms (item set 19), with per-language research-item and publication counts.
- Locations search — the place authority terms (item set 1851), split by a Type facet (Country / geographic location), with a per-place research-item count.
- Subjects & tags search — the subject authority terms (item set 1852), both LCSH headings and tags in one corpus split by a Type facet, with per-term research-item and publication counts.
All are search profiles: independent Typesense collections + facet/index
mappings, all config-driven. Adding a further corpus is a config block plus a
mapper — not a rewrite. The last four share the generic term kind (one
TermMapper + one TermCard): an authority item set whose substance is the
reverse count of the records that reference each term.
Typesense is optional. With no connection configured the module installs cleanly and the site runs normally; the search blocks just show a quiet "search unavailable" notice. This keeps the module reusable on installs that don't want a search backend.
- A Research items search page block: full-text search with autocomplete,
faceted filtering, sorting, pagination, and result cards (title, type,
project, origin year, place of origin, current location, thumbnail).
- Facets: a Year range slider, plus Type, Project, Place of origin, Country, Current location, Language, Subject, Tag, Target audience, Digitisation method. (Place of origin is the exact place recorded; Country is that rolled up to the country for broad browsing; Current location is where the item is held now — a specific place or repository institution.)
- A Research projects search page block: cards show the project name, year
range, research section(s), principal investigator(s), funding institution(s),
and the number of associated research items.
- Facets: a Year range slider, Institution, Research section, and Has research items (yes/no).
- On a card, the research-section, institution, and PI are clickable — each adds that value as a filter (a PI filters by Associated people, so you can pivot to every project that person is involved in).
- A Publications search page block: cards show a formatted bibliographic
reference — title, authors (linked to their person pages), venue (journal or
book + series), volume/issue, pages, publisher, year, abstract, and a DOI link.
- Facets: a Year range slider, Type, Author, Journal / Book, Publisher, Keyword, Language.
- A Podcasts search page block: cards show the podcast series logo as the
thumbnail (every episode of a series shares it), the episode number and date, the
title, a series chip, the hosts, guests and sound engineer (linked to their person
pages), the language, the abstract, a Transcript badge when the episode is
full-text searchable, and a Listen link to the audio / episode page.
- Facets: Series, People (a union of hosts and guests), and Language.
- Sort: Episode number (default — newest episode first) or Newest / Oldest by date, plus Relevance / Title.
- Search covers the title, abstract, transcript, and the people in each episode.
- A YouTube videos search page block: cards show the video's own poster-frame
thumbnail, the date, the title, a playlist chip, the speakers (linked to their
person pages), the language, the abstract, a Transcript badge when the video
is full-text searchable, and a Watch link to YouTube.
- Facets: a Year range slider, Playlist, Speaker, and Language.
- Sort: Newest / Oldest by date (newest first by default), plus Relevance / Title.
- Search covers the title, abstract, transcript, and the speakers in each video.
- A People search page block: cards show the person's name, affiliation(s),
role chips, and how many research items and publications they're associated
with (laid out two-up on wide screens, since the cards are compact).
- Facets: Affiliation and Role — Principal investigator, Project member,
Author, Editor, plus the specific contributor role each person plays on a
research item (Author, Photographer, Interviewee, Translator, Collector, … —
one per
marcrel:*relator actually in use).
- Facets: Affiliation and Role — Principal investigator, Project member,
Author, Editor, plus the specific contributor role each person plays on a
research item (Author, Photographer, Interviewee, Translator, Collector, … —
one per
- A Research sections search page block: cards show the section name, its
phase, the project count, the leaders (PIs or spokesperson), the member count,
and the abstract — including the External section. The phase and each
leader are clickable filters.
- Facets: Phase (1 / 2) and Associated person (PIs, spokesperson, or members).
- An Organisations search page block: one corpus for both institutions and
groups (bands, choirs, archives, …). Cards show the name, a Type chip
(Institution / Group), role chips, and how many projects, research items, and
affiliated people the organisation is associated with (laid out two-up on wide
screens, since the cards are compact). The Type and role chips are clickable
filters.
- Facets: Type (Institution / Group) and Role (Funder, Contributor, Host institution).
- Four authority-term page blocks — Genres, Languages, Locations,
and Subjects & tags — each a searchable, paginated list of the terms applied
across the collection, laid out two-up on wide screens. Cards show the term, an
optional Type chip, and how many records use it; the term links to its Omeka
page. They sort by Most research items by default (also Relevance / Title);
being date-less, they offer no Newest/Oldest.
- Genres: per-genre research-item count. No facets.
- Languages: per-language research-item and publication counts. No facets.
- Locations: a Type facet (Country / geographic location) and a
Relationship facet — Place of origin (
dcterms:spatial) vs Current location (dcterms:provenance); per-place research-item count. - Subjects & tags: a Type facet (LCSH subject / tag); per-term research-item and publication counts.
- A dashboard Reindex action per corpus (Admin → DRE Search) that rebuilds an index as a background job, reading the Omeka database and pushing to Typesense in batches.
Beyond the per-corpus blocks, the module exposes a site-wide search across all corpora at once, designed to replace a theme's header search:
- A header search bar (a
dreSearchBarview helper the theme drops into its header) with a federated autocomplete: one Typesensemulti_searchover every corpus, suggestions grouped by type (Research item / Person / Project / Location / …). Picking a suggestion jumps straight to that record's page; pressing Enter / "See all results" goes to the results page. - A federated results page at
/s/{site-slug}/dre-search(?q=…): an All tab uses Typesense 30's union search for one relevance-ranked stream, followed by one tab per corpus with its hit count. Selecting a tab reveals that corpus's own facets, cards, sorting and paging — it reuses the same per-corpus search UI as the blocks. The initial union scope is curated to content and named-entity corpora so generic authority terms do not swamp the ranking. Union responses do not carry facet counts; choose a corpus tab to filter them.
Three server endpoints back it (top-level, anonymous, is_public:=true enforced):
/dre-search/api/suggest-all (grouped autocomplete) and
/dre-search/api/search-all (per-corpus counts + the focused corpus's results),
plus /dre-search/api/union (the merged result stream). The Typesense key remains
server-side in every case.
Theme integration is one guarded helper call — the theme degrades to its own search form when the module is absent:
<?php // near the top of layout.phtml, before <head> is rendered: ?>
<?php if ($this->getHelperPluginManager()->has('dreSearchAssets')): ?>
<?php echo $this->dreSearchAssets(); // inject the bundle into <head> ?>
<?php endif; ?>
<?php // in the header markup: ?>
<?php if ($this->getHelperPluginManager()->has('dreSearchBar')): ?>
<?php echo $this->dreSearchBar('header-desktop', 'd-none d-xl-block'); // always-visible ?>
<?php echo $this->dreSearchBar('header-mobile', 'd-block d-xl-none', true); // collapsible ?>
<?php endif; ?>(The early dreSearchAssets call is needed because the header renders as layout
chrome — after <head> is already emitted — so the bundle must be injected
before then. Search blocks self-inject, so a page with a block needs only the
header calls; headScript/headLink dedupe by URL.)
- Omeka S
^4.2 - A Typesense server (optional)
- Node 20+ — only to rebuild the Svelte bundle during development (the compiled
bundle ships in
asset/dist/, so production needs no Node toolchain).
- Put the module at
modules/DRESearch. With the AMomeka-s-dockerstack it is already referenced in_docker/default-modules.txt(gh:AM-Digital-Research-Environment/DRESearch); otherwise usebash scripts/install-module.shor copy it in manually. - Install PHP dependencies (the Typesense client):
composer install --no-dev
- Activate DRE Search in Admin → Modules.
- Set a strong key in
.env:TYPESENSE_API_KEY=<a long random string> - Start the optional search service:
The
docker compose --profile search up -d
phpservice already forwardsTYPESENSE_HOST/PORT/PROTOCOL/API_KEYto the module, and Typesense stays on the internal network (never exposed to the host).
Admin → Modules → DRE Search → Configure: host, port, protocol, API key. Resolution order is settings → environment variables → config defaults, so the admin form overrides everything. (Collection aliases are per-profile config, not set here — see below.)
Admin → DRE Search → Reindex: one button per corpus. Progress is logged
to Admin → Jobs. Re-run after significant content changes (or wire the job to a
schedule via the Cron module). Each reindex builds a fresh, timestamped Typesense
collection and swaps that profile's alias (dre_research_current /
dre_projects_current / dre_publications_current / dre_podcasts_current /
dre_people_current / dre_sections_current / dre_organisations_current /
dre_genres_current / dre_languages_current / dre_locations_current /
dre_subjects_current) to it atomically, so live searches never hit a half-built
index.
Run Reindex all corpora after deploying this release. Every union profile needs its new source markers, Publications needs the full-text availability fields, and Locations needs its coordinates. The Locations result toolbar then offers an opt-in, lazy-loaded MapLibre map; list remains the default. Research items offer list and image-forward gallery views, with the chosen view stored in the shareable URL and as a local preference.
To collect popular and zero-result queries, start Typesense with
--enable-search-analytics=true and a persistent --analytics-dir. Then choose
Admin → DRE Search → Provision analytics. The maintenance page shows the top
signals per corpus. Provisioning is deliberately non-fatal: reindex and public
search continue when analytics is disabled.
Edit a site page → add the Research items search, Research projects search, Publications search, Podcasts search, People search, Research sections search, Organisations search, Genres search, Languages search, Locations search, or Subjects & tags search block. All share the same options:
- Filters to show — which facets appear in the sidebar, including whether the Year range slider shows.
- Default sort — the choices depend on the corpus: Relevance and Title always; Newest / Oldest only for corpora with a date; Most research items (count) for the authority-term corpora; Episode number for podcasts. (Date-less corpora — people, sections, organisations, and the four term corpora — no longer offer the meaningless Newest/Oldest.)
- Results per page.
- Locked filter — an optional Typesense
filter_bysaved with the block, e.g. pin items to one project (project_s) or projects to one research section (section_ss). The browser sends only the block ID; the server reloads the saved scope, so visitors cannot weaken or replace it.
Put each block on its own page and link them in the site navigation.
Facet behaviour is the same across every block: a facet with many values shows a type-to-filter box (scroll or type to find a value — no "show N more"), and on narrow screens the whole filter sidebar collapses behind a Filters toggle (with an active-filter count) that opens and closes it.
All instance-specific mapping lives in the dre_search.profiles block of
config/module.config.php and is overridable, key by
key, via Omeka's config/local.config.php (no module source edit). Each profile
sets its Typesense collection alias, source template/item set, query_by, date
mode, and facets. The Typesense field names (type_s, institution_ss,
year_start, …) are the stable interface; the Typesense schema, the SQL term
list, and each block's facet picker all derive from the profile config.
| Facet | Omeka property | Authority set |
|---|---|---|
| Type | dcterms:type |
1 |
| Project | dcterms:isPartOf |
20 |
| Place of origin | dcterms:spatial |
1851 (the linked place verbatim — city / region / country) |
| Country | dcterms:spatial |
1851 (rolled up to country, direct or via city isPartOf) |
| Current location | dcterms:provenance |
1851 places + 110 institutions (held-at; not rolled up) |
| Language | dcterms:language |
19 |
| Subject | dcterms:subject |
1852 (target type = lcsh) |
| Tag | dcterms:subject |
1852 (target type = tag) |
| Target audience | dcterms:audience |
3169 |
| Digitisation method | dcterms:format |
7438 (genres in set 21 excluded) |
| Year (range slider) | dcterms:issued |
single year (fallback created → date) |
| Field | Omeka property | Notes |
|---|---|---|
| Institution (facet) | frapo:isFundedBy |
linked institution titles (set 110) |
| Research section (facet) | dcterms:isPartOf |
linked research-section titles (set 17) |
| Year (range slider) | dcterms:temporal |
numeric:interval → year_start / year_end |
| Has research items (facet) | — derived — | from the count below |
| PI(s) (card) | dcterms:creator |
linked person titles; clickable → adds an Associated-people (people_ss) filter |
| Associated items (card) | — derived — | research items (template 10) linking back via dcterms:isPartOf, public only |
Publications span ~10 type-specific resource templates (article, book, chapter,
thesis, …) but share one item set, so this profile scopes by item_set_id
(29918) with template_id: null — the only profile with no template filter.
| Field | Omeka property | Notes |
|---|---|---|
| Type (facet) | dcterms:type |
linked publication-type title (set 30613) |
| Author (facet + card) | bibo:authorList |
linked person titles (set 18); author_ids link the card |
| Journal / Book (facet) | dcterms:isPartOf |
literal venue + series |
| Publisher (facet) | dcterms:publisher |
literal |
| Keyword (facet) | dcterms:subject |
linked subject titles (set 1852), literal fallback |
| Language (facet) | dcterms:language |
linked language (set 19), literal fallback |
| Year (range slider) | dcterms:date |
numeric:timestamp → single year |
| Reference bits (card) | bibo:editorList, bibo:volume, bibo:issue, pages* |
editors, volume/issue, recombined page string |
| DOI (card) | bibo:doi |
the URI value's @id (full https://doi.org/… link) |
| Abstract (card) | bibo:abstract |
publications use bibo:abstract, not dcterms:abstract |
* Pages come from bibo:pages / bibo:pageStart / bibo:pageEnd /
bibo:numPages (the pipeline splits them by publication kind) and are recombined
into one display string — 141–165, a lone start, or 121 pp..
The cluster's podcast episodes, hand-curated in Omeka (not from the
MongoDB2OmekaS pipeline), so they get a dedicated corpus rather than being folded
into publications — their links differ (marcrel:hst/spk contributors,
dcterms:abstract, a series link, an episode number, a transcript).
| Field | Omeka property | Notes |
|---|---|---|
| Series (facet) | dcterms:isPartOf |
linked series title (single-valued); series_id links the chip |
| People (facet) | — | derived union of hosts + guests (people_ss) |
| Language (facet) | dcterms:language |
linked language (set 19), literal fallback |
| Host (card) | marcrel:hst |
linked person titles (set 18); host_ids link the card |
| Guest (card) | marcrel:spk |
linked person titles (set 18); guest_ids link the card |
| Sound engineer (card) | marcrel:sde |
optional credit line; engineer_ids link the card (display-only) |
| Episode number (sort) | bibo:number |
numeric:integer → sortable episode (the default sort) |
| Date (sort + card) | dcterms:date |
date_s shown verbatim; the 4-digit year drives Newest/Oldest |
| Listen link (card) | fabio:hasURL |
the URI value's @id — opens the audio / episode page |
| Abstract (card) | dcterms:abstract |
podcasts use dcterms:abstract (unlike publications' bibo:abstract) |
| Transcript (search) | bibo:content |
full-text search payload (often empty today); search_only — indexed for query_by but excluded from result payloads, so a long transcript never bloats a hit (matches still surface as a highlighted snippet). has_transcript drives a Transcript badge on the card |
| Thumbnail (card) | dcterms:isPartOf |
the series item's logo, via thumbnail_property (see below) |
The episode's own media is the audio file, so the card thumbnail is resolved by
hopping dcterms:isPartOf to the series item and using its first thumbnailed
media (thumbnail_property → Reindexer::loadThumbnailsVia()). Sorts: Episode
number (default) is a config-defined numeric sort (sort_fields — the generic
form of the term corpora's count sort) over the sortable episode field; the date
sorts use year. There is no year slider (the corpus is small and ordered by
episode). The People facet unions hosts and guests, while the card keeps them
separate so each is labelled by role.
Data note: the template carries Host, Sound engineer, Language and Transcript, but most episodes today fill only guest, series, episode number, date and abstract — the facets and fields are built for the whole template and fill in as the corpus grows.
A person record carries only a name and affiliation; the roles and counts come
from the reverse direction — the records that point at the person. The
reindexer computes these per person from the profile's reverse_links config
(one grouped query per count bucket / role rule per page). This corpus has no
date — it sorts by name.
| Field | Source | Notes |
|---|---|---|
| Affiliation (facet) | dcterms:isPartOf on the person |
linked Institution titles (set 110), literal fallback |
| Role (facet) | — derived (reverse) — | see the role rules below |
| Research items (card) | research items (template 10) referencing the person, public only | item_count |
| Publications (card) | publications (item set 29918) referencing the person, public only | publication_count |
Role rules (a person earns a role if a public record references them so):
| Role | Source corpus | Property |
|---|---|---|
| Principal investigator | project (template 5) | dcterms:creator |
| Project member | project (template 5) | foaf:member |
| Author | publication (set 29918) | bibo:authorList |
| Editor | publication (set 29918) | bibo:editorList |
| the specific MARC roles | research item (tmpl 10) | each marcrel:* relator in use (per_property) |
The last rule uses per_property: rather than collapsing every research-item
credit into a single "Research contributor" value, it emits one role per
marcrel:* relator the person actually holds (Author, Photographer,
Interviewee, Translator, Collector, …), labelling each from that property's
template-10 alternate label. Only relators present in the data appear, so the
facet lists exactly the roles in use. Labels that coincide with a publication
role (Author, Editor) merge into one facet value.
The cluster's 13 thematic sections, including the synthetic External section (item 25135 — it exists in the data, so it appears as a card with its project count). Phase is not stored — it's derived from which leadership property the section carries. No date — sorts by name.
| Field | Source | Notes |
|---|---|---|
| Phase (facet) | — derived — | PIs present → Phase 1; spokesperson → Phase 2; External has neither |
| Associated person (facet) | — derived — | union of PIs + spokesperson + members |
| PIs (card) | dcterms:creator |
linked person titles (Phase 1 sections) |
| Spokesperson (card) | marcrel:spk |
linked person title (Phase 2 sections) |
| Members (card) | foaf:member |
counted → member_count |
| Projects (card) | — derived (item_link) — |
public projects (template 5) linking via dcterms:isPartOf |
| Abstract (card) | dcterms:abstract |
One corpus for both institutions and groups: the pipeline stores them as the
same Organisation item (template 2, item set 110) and tells them apart with
dcterms:type (→ "Institution" / "Group"), which becomes the Type facet. Like
people, an organisation's substance is in the reverse direction, so the same
reverse_links machinery counts the records pointing at it and derives the roles
it plays. No date — sorts by name.
| Field | Source | Notes |
|---|---|---|
| Type (facet) | dcterms:type on the organisation |
linked type item title — "Institution" / "Group" |
| Role (facet) | — derived (reverse) — | see the role rules below |
| Projects (card) | projects (template 5) funding it via frapo:isFundedBy, public |
project_count |
| Research items (card) | research items (template 10) crediting it (any role), public | item_count |
| People (card) | people (template 4) affiliated via dcterms:isPartOf, public |
people_count |
Role rules (an organisation earns a role if a public record references it so):
| Role | Source corpus | Property |
|---|---|---|
| Funder | project (template 5) | frapo:isFundedBy |
| Contributor | research item (tmpl 10) | any reference |
| Host institution | person (template 4) | dcterms:isPartOf |
Four corpora that index a curated authority item set each. Like people and
organisations, a term record carries only a name (and, for some, a dcterms:type
sub-kind); its substance is the reverse count of the public records that
reference it, computed by the reindexer from reverse_links. They are scoped by
item set alone (template_id: null), have no date, default to the Most
research items sort (count desc, name tie-break), and use the same linking
properties the working research-items / publications facets use.
| Corpus | Profile | Item set | Facets | Reverse counts |
|---|---|---|---|---|
| Genres | research_genres |
21 | — | research items (tmpl 10) via dcterms:format |
| Languages | research_languages |
19 | — | research items via dcterms:language + publications (set 29918) via same |
| Locations | research_locations |
1851 + tmpl 2† | Type, Relationship | research items via dcterms:spatial and dcterms:provenance |
| Subjects & tags | research_subjects |
1852 | Type | research items via dcterms:subject + publications (set 29918) via same |
Notes:
- The Type facet is the term's own
dcterms:typelinked-item title — the same discriminator items the research-items mapper uses to split Subject vs Tag (3167/22199) and Country vs geographic location (3168/22431). - The Locations Relationship facet is derived (reverse) from how a record
references the place: Place of origin (
dcterms:spatial, fromlocation.origin) vs Current location (dcterms:provenance, fromlocation.current). Itsitem_countcounts research items referencing the place either way (deduped). - †
dcterms:provenancealso targets repository institutions. The geocoded ones (resource template 2 in set 110 that carrygeo:lat/geo:long— the repositories holding research items) are folded into this corpus via the profile'sextra_sources, so they appear alongside places as a new Type "Institution" (their owndcterms:type) with a Current location relationship and a held-items count.extra_sourcesis the generic multi-source mechanism (an OR-group in the reindex source query); other corpora omit it. The inclusion filter is "has coordinates", so un-geocoded institutions stay out. - The Locations corpus indexes every place term directly and does not roll cities up to their country (unlike the research-items Country facet), so both "Nigeria" and "Lagos" appear with their own direct-mention counts.
- All counts are public records only; the term itself appears only if it is public.
Before the first production reindex, verify these IDs against your instance — the project IDs (template 5, item set 20, authority sets 17/110,
dcterms:typetarget 3346), the publication item set (29918) and type set (30613), the people template (4) / item set (18) and their reverse person-link properties (dcterms:creator,foaf:member,bibo:authorList,bibo:editorList), the research-sections template (7) / item set (17) and their leadership properties (dcterms:creator= PIs,marcrel:spk= spokesperson,foaf:member= members), the organisations template (2) / item set (110) and their reverse-link properties (frapo:isFundedBy,dcterms:isPartOf) plus thedcterms:typetargets that split Institution vs Group, the authority-term item sets (genres 21, languages 19, locations 1851, subjects 1852) and their linking properties (dcterms:format,dcterms:language,dcterms:spatial= place of origin +dcterms:provenance= current location,dcterms:subject), and the item authority sets /dcterms:typetargets. They come from the MongoDB2OmekaS config; on a different Omeka instance, override thedre_searchconfig inconfig/local.config.php.
npm install
npm run build # compile src/svelte → asset/dist (commit the result)
npm run check # svelte-check (types)
npm run lint # eslint + prettier
npm run lint:fix # auto-fixnpm run lint also runs scripts/check-profile-schema.php, which fails CI when
profile query/facet/search-only fields drift from the generated Typesense schema
or client card/i18n contracts. Run check + lint + build before committing
anything under src/svelte/.
Lint the PHP in your runtime image (php -l) since this repo ships no PHP
toolchain.
- Multi-corpus by config. A
ProfileRegistry(built fromdre_search.profiles) holds oneSearchProfileper corpus. The schema builder, paged reindexer, query builder, search proxy, and page blocks are all parameterised by a profile; a profile'skind(item|project|publication|podcast|person|section|organisation|term) selects its indexer mapper and its result card. - Search is server-side. The browser calls the module's own JSON endpoints
(
/dre-search/api/search,/dre-search/api/suggestper corpus;/dre-search/api/suggest-all,/dre-search/api/search-all,/dre-search/api/unionfederated) with aprofile; PHP forwards to Typesense with the server-held key and enforcesis_public:=true. The key never reaches the browser, so there are no scoped keys or nginx changes — and "optional" is trivial (no key → no search). The federated endpoints use Typesensemulti_search; the All tab opts into the Typesense 30 union mode for one merged ranking. - PHP:
src/Search(proxy, query builder, lazy client provider),src/Indexer(schema, authority resolver, mappers, analytics sync, paged reindexer and shared reindex orchestrator),src/Job(background operations),src/Site/BlockLayout(the blocks),src/Controller(public proxy + federated results page + admin maintenance),src/View/Helper(thedreSearchBar/dreFederatedSearch/dreSearchAssetssurfaces),src/Settings(SearchProfile,ProfileRegistry,SortOptions). - JS:
src/svelte— Svelte 5, one IIFE bundle that auto-mounts three surfaces (per-corpusApp, headerSearchBar,FederatedApp); the federated page reusesAppper type-tab. Styled with DRE-theme design tokens (so it inherits the theme's light/dark palette automatically). - Keyword search only (no embedding model) to keep Typesense lean on a modest host. The reindexer is paged + batched, so memory stays flat.
GPL-3.0-or-later.