package kiwix // End-to-end score: Russian question -> model rewrite -> Kiwix search -> did a // wanted article come back. Same 9 cases as the retrieval eval, so the two // numbers are directly comparable: retrieval with hand-written keywords is the // ceiling, this is what the model actually reaches. import ( "context" "encoding/json" "fmt" "strings" ) // RewriteOutcome — one case, end to end. type RewriteOutcome struct { Outcome ModelQuery string // what the model asked for ("" if it failed) RewriteErr error } // RunRewriteEval rewrites every question with the model, then searches. func RunRewriteEval(ctx context.Context, c *Client, rw *Rewriter, topN int) (RewriteReport, error) { var f fixture if err := json.Unmarshal(knowledgeFixtureJSON, &f); err != nil { return RewriteReport{}, err } rep := RewriteReport{Report: Report{Name: f.Name + "-rewrite", Book: f.Book, TopN: topN}} for _, cs := range f.Cases { out := RewriteOutcome{Outcome: Outcome{Case: cs}} q, err := rw.Rewrite(ctx, cs.Question) out.ModelQuery, out.RewriteErr = q, err if err == nil { res, serr := c.Search(ctx, q, f.Book, topN) out.Err = serr for i, hit := range res { out.Titles = append(out.Titles, hit.Title) if out.Rank == 0 && matches(cs.WantTitles, hit.Title) { out.Rank = i + 1 } } } if out.RewriteErr != nil || out.Err != nil { rep.Errors++ } if !cs.ExpectMiss { rep.Scored++ if out.Hit() { rep.Hits++ } } rep.Cases = append(rep.Cases, out) } return rep, nil } // RewriteReport — the score plus per-case detail. type RewriteReport struct { Report Cases []RewriteOutcome } // String — the headline number. func (r RewriteReport) String() string { return fmt.Sprintf("%s: %d/%d answerable questions retrieve a wanted article in top %d (%.1f%%), %d errors\n book: %s\n", r.Name, r.Hits, r.Scored, r.TopN, 100*r.Accuracy(), r.Errors, r.Book) } // Detail — per case: hand-written query next to the model's, and what came back. // The point is seeing WHERE the model's phrasing differs, not just the score. func (r RewriteReport) Detail() string { var b strings.Builder for _, o := range r.Cases { mark := "MISS" switch { case o.Case.ExpectMiss: mark = "n/a " case o.Hit(): mark = fmt.Sprintf("hit@%d", o.Rank) } fmt.Fprintf(&b, " %-6s %-20s\n", mark, o.Case.ID) fmt.Fprintf(&b, " asked: %s\n", o.Case.Question) fmt.Fprintf(&b, " hand: %q\n", o.Case.Query) fmt.Fprintf(&b, " model: %q\n", o.ModelQuery) if o.RewriteErr != nil { fmt.Fprintf(&b, " rewrite rejected: %v\n", o.RewriteErr) continue } if o.Err != nil { fmt.Fprintf(&b, " search error: %v\n", o.Err) continue } fmt.Fprintf(&b, " got: %s\n", strings.Join(o.Titles, " | ")) } return b.String() }