keep the evidence.
A music dialogue recommender where the difficult part was keeping the right track through two candidate-compression stages.
archive date · Date on the supplied solution slides; this retrospective was prepared in October 2026. · text updated 2026-10-13
A music request can change direction in one sentence. Keep the artist, change the album. Find something with a particular sound. Or choose a record for its abstract cover art.
In RecSys Challenge 2026, our system had to follow that dialogue across a catalogue of 47,071 tracks, return 20 ranked IDs, and write an explanation. The most interesting problem was inside the shortlist: we often found the right track, then lost it before the final ranking.
Our entry, listed as vkost, finished 7th of 20 in the industry track. The final Blind-B table reports a composite score of 0.5098 and nDCG@20 of 0.4729. Select Industry on the results page to see the track ranking.
The solution kept the retrieval sources separate long enough for a learned ranker to decide which evidence mattered for each candidate. Two compression stages had to do their jobs before a language model could explain anything.
Finding it is only the first step
The retrieval stack used seven families of signals. BM25 helped with exact terms. Dense text retrieval worked with enriched catalogue descriptions. Audio and image embeddings handled requests about sound and artwork. Continuity sources followed previously accepted artists and albums.
Wikidata facts and generated descriptors made the catalogue easier to search. A title and an artist do not necessarily describe mood, instruments, or the scene a listener has in mind. Those descriptors also needed to be frozen: a fresh API call or LLM sample could change the input to a later experiment.
The sources produced 24 retrieval lists, each with up to 500 track IDs. Their deduplicated union averaged about 5,599 candidates on dev. It contained the accepted track in 7,259 of 8,000 groups.
But finding the track somewhere in that variable-size union did not guarantee a useful shortlist. A naive ordering of its top 1,000 kept the track in only 5,863 groups. We were throwing away candidates that retrieval had already found.
| Dev candidate surface | Accepted track retained / all 8,000 groups |
|---|---|
| Raw union, variable size | 7,259 / 8,000 |
| Naive top-1,000 | 5,863 / 8,000 |
| Protected top-1,000 recipe | 6,356 / 8,000 |
| Stage1 TargetAttention top-1,000 | 6,512 / 8,000 |
The final Stage1 still missed 1,488 groups. In 747, the track existed in the raw union but was lost during compression. In the other 741, retrieval had never supplied it. Improving the compressor can help the first group. It cannot recover a track it never receives.
Keep the source disagreement
A single blended score hides the reason a candidate arrived. A track might rank high in audio retrieval and poorly in lexical search. Another might match an exact album request while receiving little support from broader semantic retrieval.
We retained per-source evidence: whether the source found the track, its best rank and reciprocal rank, how many turns supported it, and where that support appeared in the dialogue. Variant-level tokens and family aggregates gave 35 source tokens, with eight fields each.
TargetAttention then asked which sources mattered for this candidate. A projection of candidate-level numeric features supplied the attention query. Projected source tokens supplied keys and values.

candidate = project(candidate_features)
evidence = project(source_tokens) + source_position
attended = attention(query=candidate, keys=evidence, values=evidence)
combined = concat(candidate, attended, target_turn_embedding)
score = MLP(DCNv2(combined))
Each candidate attended to its own source evidence. The forward pass received engineered signals and catalogue features; track identity and raw dialogue text were kept out of the ranker. The dialogue had already shaped the retrieval lists.
The resulting vector had 192 dimensions. A low-rank DCNv2 layer modeled multiplicative interactions before the MLP produced a scalar score. That matters when evidence changes meaning in context: strong artist continuity can make a source rank more useful, while agreement between sources can matter differently on an early or late turn.
Two compressors, two jobs
The selected pipeline used the same architecture twice:
retrieval union
→ wide frame: up to 4,000 candidates
→ Stage1: top 1,000
→ Stage2: ordered top 300
→ first 20 IDs
→ response about the selected track
Protected source heads helped construct the wide frame. After that, Stage1 used learned scores to select the top 1,000. Its checkpoint metric was recall@1,000: preserve useful candidates for the next stage. Stage2 optimized the narrower pool and was selected by recall@300.
Both used masked BCE plus 0.2 × ListNet. If the accepted track was absent from the input pool, we disabled the listwise term for that group: the model had no positive candidate to move up. The group still counted as a miss in system recall. That kept retrieval failures in the denominator rather than making them disappear from the metric.
Stage2 also had to train on the kind of pool it would receive later. Stage1 fitted on fold0; its frozen predictions formed fold1 top-1,000 pools, without inserting the gold track. A fold1 subset had helped select Stage1’s checkpoint. Stage2’s checkpoint selection used fold2.
Let the language model explain
Qwen reranking was tempting. The experiments included listwise and pairwise objectives, pointwise scoring, and small gated changes to the ranking.
One variant raised proxy nDCG@20 from about 0.362 to 0.380. That was encouraging until we looked at the hidden result: its submission package scored 0.50, while the selected package scored 0.51. Qwen reranking stayed out of the final ranking path.
The proxy used 280 causal prefixes derived from already visible accepted events in Blind-B dialogues. It was a transductive diagnostic, useful for inspecting rankings rather than predicting performance on independent held-out data.
Qwen’s final job was to write a grounded response about an already selected track. It could use the visible dialogue and catalogue description while the top-20 IDs remained fixed. The response graft checks in the slides report no ranking mismatches across 80 rows.
The slides round the final result to 0.51. The official table gives 0.5098 composite: a weighted combination of ranking, catalogue diversity, lexical diversity, and response evaluation. Its 0.4729 nDCG@20 is the ranking component. These are different from the local proxy above. The earlier 0.43 submission package also differed in more than its ranker, so that jump cannot be attributed entirely to TargetAttention.
The useful question after a bad recommendation is: where did the right track disappear? Missing from the union sends us back to retrieval. Present there but absent from the shortlist points to compression. Surviving the shortlist but buried in the final list points to ordering. A wrong explanation is another failure, with the IDs already fixed.
The 747 tracks found and then lost gave us a specific problem to work on. That is the part of this solution I like most: the aggregate score tells you something went wrong; keeping the evidence tells you where to look.
The pipeline details come from our July solution slides, which link to the solution repository. The official final results provide the track placement and Blind-B metrics.