# Phase 4 — Search pipeline, ranking, dedupe, validation **Status:** Implemented **Date:** 2026-10-12 **OpenAI model:** `gpt-6-luna` (cheap, accurate; `$0.10` / `$0.50` per 1M tokens) ## Pipeline ``` NL query → OpenAI interpretation (gpt-6-luna, reasoning_effort=none) → merge with user filter edits → Indeed fetch (paginated, max 4 pages) → local JobListingFilter → dedupe by indeedJobKey → availability validation (source signals) → optional OpenAI / heuristic ranking → in-memory cache (15m TTL) → UI results ``` ## What shipped | Component | Path | |-----------|------| | Orchestrator | `Services/Search/JobSearchPipeline.swift` | | Local filter | `Services/Search/JobListingFilter.swift` | | Dedupe | `Services/Search/JobListingDeduper.swift` | | Criteria merge | `Services/Search/SearchCriteriaMerge.swift` | | Ranking | `Services/OpenAI/OpenAIRankingService.swift` | | Validation | `Services/Validation/ListingValidationServices.swift` | | Cache | `Services/Indeed/InMemoryIndeedListingCache.swift` | | Model default | `Services/OpenAI/OpenAIModels.swift` → `gpt-6-luna` | ## Indeed source Live DI uses **`MockJobDataSource`** (via `JobDataSourceIndeedAdapter`) with openable Indeed **browse/search** URLs until the licensed Option C vendor is ready. Fixtures remain for mapper/contract tests. **Why links used to look “expired”:** interim keys like `fixture-ios-…` / `ios001` are not real Indeed `jk` values. Opening `/viewjob?jk=…` with those keys shows Indeed’s expired/not-found page. The app now opens `/jobs?q=…&l=…` (live Indeed search for that role) when the key is not a real 16-char hex `jk`. ## Open on Indeed `JobListingOpener` + `OpenIndeedLinkButton` are available on **AI Job Search** rows (and Saved Jobs). Xcode console logs (`JobOpen` / `SearchPipeline`) explain fallbacks and unavailable listings. ## Cache / TTL | Policy | Value | Use | |--------|-------|-----| | Listing metadata cache | **15 minutes** | Search result pages in `InMemoryIndeedListingCache` | | Saved job revalidation | **24 hours** | Reserved for Phase 5 persistence | ## Feature flags ```swift FeatureFlags.isAIInterpretationEnabled // NL → criteria FeatureFlags.isAIRankingEnabled // optional batch rank (degrades to heuristic) FeatureFlags.isListingValidationEnabled // source availability pass ``` HTTP HEAD validation exists (`HTTPJobValidationService`) but is **not wired** in live DI yet — fixtures/source signals only, to avoid unsolicited Indeed probing before vendor expired-signal policy. ## Acceptance checklist - [x] Duplicate listings collapsed by `indeedJobKey` - [x] Shared local filter for Indeed-oriented fields (incl. freshness / exclude / exact phrase best-effort) - [x] Open Indeed URLs remain regional Indeed hosts from fixtures/mapper - [x] OpenAI uses `gpt-6-luna` for interpretation + ranking - [x] Pipeline wired into `JobSearchViewModel` - [x] Unit tests for filter, dedupe, merge, ranking order, pipeline, cache