Skip to content

[SEARCH] Replace AlgoliaService with TypesenseService (frontend) #41

Description

@BAWES

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 MeilisearchServiceTypesenseService 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 MeilisearchServiceTypesenseService
  • Change environment.meilisearchCandidateIndexenvironment.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

  • TypesenseService exists at src/app/services/typesense.service.ts
  • CandidateSearchService imports TypesenseService
  • angular-instantsearch and algoliasearch removed from package.json
  • algolia.service.ts deleted
  • NgAisModule removed from AppModule
  • Search returns results on candidate search page
  • Pagination (infinite scroll) works — new search replaces results, next page appends
  • environment.ts updated with typesenseCandidateCollection
  • No TypeScript compile errors

Branch

Branch from master. Name: feature/typesense-service

Related

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions