---
topic: Playlist Search implementation
last_updated: 2026-03-16
---

# Playlist Search

Playlist Search lives in **graphql-knowledge only** — NOT in graphql-knowledge-search.

## Entry Points

- GraphQL resolver: `graphql-knowledge/src/resolvers/Query.js` → `playlistSearch()`
- Snowflake connector: `graphql-knowledge/src/connectors/snowflake/snowflake.ts` → `searchPlaylists()`
- Cortex API client: `graphql-knowledge/src/connectors/snowflake/snowflakeSearchApi.ts`
- Row utilities: `graphql-knowledge/src/connectors/snowflake/utils.ts`
- Row Zod schema: `graphql-knowledge/src/connectors/snowflake/types.ts` → `PlaylistMetadataRow`
- Constants: `graphql-knowledge/src/constants.js`
- Frontend query: `frontend-insights/src/apollo/queries/playlists/playlistSearch.gql`
- Frontend hook: `usePlaylistSearchQuery`
- Frontend selector: `selectPlaylistSearchResults` in `src/apollo/selectors/playlist.ts`
- Frontend component: `src/pages/searchES/pages/playlistResults/playlistResults.tsx`

## ID Detection (short-circuits Cortex Search)

`searchPlaylists()` detects known playlist ID formats before falling through to Cortex:

| Input | Function | Behaviour |
|-------|----------|-----------|
| `spotify:playlist:<id>` or `open.spotify.com/playlist/<id>` | `extractSpotifyPlaylistId()` | Direct `getPlaylistMetadataById`, immediate return |
| Exactly 22 alphanumeric chars | Regex | Optimistic direct lookup; falls back to Cortex if not found |
| `pl.[a-f0-9]{32}` or Apple Music URL | `extractAppleMusicPlaylistId()` | Direct lookup; prefers US storefront |
| Anything else | — | Full Cortex Search |

## Cortex Search

**IaC**: `terraform-infra/prod/snowflake/orchard/databases/facts/cortex_search_services.tf`
The services themselves ARE Terraformed (4 resources: V1 QA/PROD, V2 QA/PROD). The scoring profile is NOT.

| Service | Source query | When used |
|---------|-------------|-----------|
| `PLAYLIST_METADATA_SEARCH` | `PRIORITY_PLAYLIST_METADATA` only | `insights_playlist_page_hourly_playlists` off |
| `PLAYLIST_METADATA_SEARCH_V2` | `PRIORITY_PLAYLIST_METADATA UNION ALL HOURLY_PLAYLIST_METADATA` | `insights_playlist_page_hourly_playlists` on |

Both: `on = "PLAYLIST_NAME"`, `target_lag = "1 hour"`, warehouse `{env}_ETL_WAREHOUSE`.

**VARIANT columns gotcha**: `FRONTLINE_PERCENT`, `LOCAL_PERCENT`, `SMG_PERCENT`, `PLAYLIST_GENRES` are in the source SELECT but excluded from `attributes` — Cortex Search cannot index or return VARIANT types. These fields are NOT returned in Cortex Search results even though they appear in `PLAYLIST_SEARCH_COLUMNS` in graphql-knowledge constants.

Semantics of the three percent columns (all VARIANT, all are circular header metrics in the UI):
- `FRONTLINE_PERCENT` — share of tracklist tracks that are frontline releases
- `SMG_PERCENT` — share of tracklist tracks that are SMG
- `LOCAL_PERCENT` — share of tracks whose ISRC country prefix matches the user's current context market (e.g., `USXXX` is local to USA)

**Scoring**: `POPULARITY_BOOST` profile. **Must be re-applied manually with ACCOUNTADMIN after every deploy of the Cortex Search service** — recreating the service (Liquibase/Terraform) destroys the profile. Cannot be Terraformed. Observable symptom of a missing profile: search falls back silently (code retries without profile on "Scoring profile not found" error).

**Request payload**:
- `query`: search term
- `columns`: 20 columns from `V_PLAYLIST_METADATA` (name, id, store, artwork, genres, counts, type, uri, curator, dates, percentages, owner) — note VARIANT cols will be null in results
- `limit`: adaptive (base × 2 if AM disabled, × 1.5 if hourly enabled, max 50)
- `scoring_profile`: `'POPULARITY_BOOST'`
- `filter`: `{ '@eq': { STORE_ID: storeId } }` only when storeId provided

**HTTP**: POST to `{SNOWFLAKE_SEARCH_API_URL}/databases/{db}/schemas/{schema}/cortex-search-services/{service}:query`
Auth: JWT key-pair Bearer token. Timeout: `SNOWFLAKE_SEARCH_API_TIMEOUT_MS` (default 2000ms).
Error recovery: "Scoring profile not found" → retry without profile.

## Result Processing Pipeline

1. `sortRowsWithScore()` — sort by `@scores.cosine_similarity` (or `text_match`), descending
2. `dedupeRowsByPlaylistId()` — dedupe by `STORE_PLAYLIST_ID`; Apple Music: prefer US storefront
3. `mapRowToPlaylist()` — validate via `PlaylistMetadataRow` Zod schema; drop rows missing `PLAYLIST_NAME` or `STORE_PLAYLIST_ID` (silent, logged)
4. `computeAdjustedScore()` — clamp Cortex score to [0,1], round to 2dp; all scoring delegated to Snowflake (no client-side boost)

## Resolver Post-Processing

- If Apple Music disabled: filter out `storeId=1` results, slice to requested limit
- Hard cap: `PLAYLIST_SEARCH_MAX_FETCH_LIMIT = 50`
- Default limit: 10

## Validation

- Empty / whitespace-only term → error
- Term > 110 words → error

## Zod Schema Key Points (`PlaylistMetadataRow`)

- Only `STORE_PLAYLIST_ID` is required (string)
- `PLAYLIST_TRACK_COUNT` and `PLAYLIST_FOLLOWER_COUNT` auto-coerce string → number
- Uses `.catchall(z.any())` to pass through `@scores` and `@metadata` from Cortex response

## Feature Flags

| Flag | Effect |
|------|--------|
| `insights_playlist_page_hourly_playlists` | V2 search service + 1.5× fetch limit |
| `insights_playlist_page_apple_music_playlists` | If off: filters AM results, doubles fetch limit |

## Frontend

- `fetchPolicy: 'cache-first'`, skipped when term is empty
- No `storeId` passed from the search results page (searches all platforms)
- Displays in `PlaylistResultsGridTable` with pagination
