Context
Current state on master: algolia.service.ts is the active search provider. The candidate search page uses angular-instantsearch widgets bound to Algolia.
What the meilisearch branch proved: A clean service abstraction works well — CandidateSearchService with BehaviorSubject state, infinite scroll pagination, and a backend proxy transport. That architecture should be brought forward and the transport swapped to Typesense.
Why Typesense over continuing with Algolia:
- At 100k users, Algolia replication/indexing operations are expensive
angular-instantsearch was deprecated by Algolia in Sept 2024 — it's a dead dependency
- Typesense is self-hostable via Docker, free at any scale (infra cost only)
- Typesense returns facet counts for all values regardless of active filters — fixing the UX problem we hit on the meilisearch branch
Depends on: #40 (Typesense backend infra — /search/key and /search/search endpoints must exist first)
Starting Point
Branch from master. Then cherry-pick the service architecture from the meilisearch branch:
# Cherry-pick the SearchState interface + CandidateSearchService
# Cherry-pick the updated candidate-search.page.ts
# Cherry-pick the search results visible commit
This gives you the scaffolding. Then rename/replace MeilisearchService → TypesenseService as below.
Scope
1. Create src/app/services/typesense.service.ts
Replace AlgoliaService (and any cherry-picked MeilisearchService) with TypesenseService:
export interface SearchRequest {
collectionName: string;
query?: string;
filters?: Record<string, string[]>;
sort?: string[];
page?: number;
hitsPerPage?: number;
facets?: string[];
}
export interface SearchResponse {
hits: any[];
pagination: { total: number; page: number; hitsPerPage: number; totalPages: number };
facets?: Record<string, Record<string, number>>;
processingTimeMs: number;
query: string;
}
@Injectable({ providedIn: 'root' })
export class TypesenseService {
private cachedKey: { apiKey: string; apiKeyValidUntil: number } | null = null;
private readonly _apiEndpoint = '/search';
async getKey(forceRefresh = false): Promise<string> { ... }
async search(request: SearchRequest): Promise<SearchResponse> { ... }
}
Backend endpoints from #40: /search/key (get scoped API key) and /search/search (proxy search). No filter syntax building in the frontend — the backend handles Record<string, string[]> → Typesense filter_by string translation.
2. Update CandidateSearchService
- Change import from
MeilisearchService → TypesenseService
- Change
environment.meilisearchCandidateIndex → environment.typesenseCandidateCollection
- No logic changes needed
3. Remove deprecated packages
npm uninstall angular-instantsearch algoliasearch
Delete algolia.service.ts and algolia.service.spec.ts. Remove any angular-instantsearch widget usage from templates.
4. Update environment.ts / environment.prod.ts
// Remove:
algoliaAppId: '...'
algoliaApiKey: '...'
// Add:
typesenseCandidateCollection: 'candidates'
5. Update AppModule
Remove NgAisModule (angular-instantsearch module) from AppModule imports. No replacement needed — TypesenseService is a plain injectable.
What Stays the Same (Do Not Change)
SearchState interface
- All methods on
CandidateSearchService: setQuery, setFilter, toggleFilter, clearFilters, removeFilter, setPage, getFacetCounts
CandidateSearchPage — zero changes to this file
- Infinite scroll and session-aware pagination logic
- 5-minute facet cache
Acceptance Criteria
Branch
Branch from master. Name: feature/typesense-service
Related
Context
Current state on
master:algolia.service.tsis the active search provider. The candidate search page usesangular-instantsearchwidgets bound to Algolia.What the
meilisearchbranch proved: A clean service abstraction works well —CandidateSearchServicewithBehaviorSubjectstate, infinite scroll pagination, and a backend proxy transport. That architecture should be brought forward and the transport swapped to Typesense.Why Typesense over continuing with Algolia:
angular-instantsearchwas deprecated by Algolia in Sept 2024 — it's a dead dependencyDepends on: #40 (Typesense backend infra —
/search/keyand/search/searchendpoints must exist first)Starting Point
Branch from
master. Then cherry-pick the service architecture from themeilisearchbranch:This gives you the scaffolding. Then rename/replace
MeilisearchService→TypesenseServiceas below.Scope
1. Create
src/app/services/typesense.service.tsReplace
AlgoliaService(and any cherry-pickedMeilisearchService) withTypesenseService:Backend endpoints from #40:
/search/key(get scoped API key) and/search/search(proxy search). No filter syntax building in the frontend — the backend handlesRecord<string, string[]>→ Typesensefilter_bystring translation.2. Update
CandidateSearchServiceMeilisearchService→TypesenseServiceenvironment.meilisearchCandidateIndex→environment.typesenseCandidateCollection3. Remove deprecated packages
Delete
algolia.service.tsandalgolia.service.spec.ts. Remove anyangular-instantsearchwidget usage from templates.4. Update
environment.ts/environment.prod.ts5. Update AppModule
Remove
NgAisModule(angular-instantsearch module) from AppModule imports. No replacement needed —TypesenseServiceis a plain injectable.What Stays the Same (Do Not Change)
SearchStateinterfaceCandidateSearchService:setQuery,setFilter,toggleFilter,clearFilters,removeFilter,setPage,getFacetCountsCandidateSearchPage— zero changes to this fileAcceptance Criteria
TypesenseServiceexists atsrc/app/services/typesense.service.tsCandidateSearchServiceimportsTypesenseServiceangular-instantsearchandalgoliasearchremoved frompackage.jsonalgolia.service.tsdeletedNgAisModuleremoved from AppModuleenvironment.tsupdated withtypesenseCandidateCollectionBranch
Branch from
master. Name:feature/typesense-serviceRelated