Add self-serve search telemetry gap surfacing

This commit is contained in:
master
2026-03-07 17:15:38 +02:00
parent 5fac47f99f
commit 14d7612cc2
13 changed files with 677 additions and 28 deletions

View File

@@ -17,7 +17,10 @@ public static class SearchAnalyticsEndpoints
"query",
"click",
"zero_result",
"synthesis"
"synthesis",
"answer_frame",
"reformulation",
"rescue_action"
};
public static RouteGroupBuilder MapSearchAnalyticsEndpoints(this IEndpointRouteBuilder builder)
@@ -30,10 +33,11 @@ public static class SearchAnalyticsEndpoints
group.MapPost("/analytics", RecordAnalyticsAsync)
.WithName("SearchAnalyticsRecord")
.WithSummary("Records batch search analytics events (query, click, zero_result, synthesis).")
.WithSummary("Records batch search analytics events (query, click, zero_result, synthesis, answer_frame, reformulation, rescue_action).")
.WithDescription(
"Accepts a batch of search analytics events for tracking query frequency, click-through rates, " +
"zero-result queries, and synthesis usage. Queries and user identifiers are pseudonymized before persistence. " +
"zero-result queries, self-serve answer states, reformulations, rescue-action usage, and synthesis usage. " +
"Queries and user/session identifiers are pseudonymized before persistence. " +
"Fire-and-forget from the client; failures do not affect search functionality.")
.RequireAuthorization(AdvisoryAIPolicies.OperatePolicy)
.Produces(StatusCodes.Status204NoContent)
@@ -113,11 +117,14 @@ public static class SearchAnalyticsEndpoints
EventType: apiEvent.EventType.Trim().ToLowerInvariant(),
Query: apiEvent.Query.Trim(),
UserId: userId,
SessionId: string.IsNullOrWhiteSpace(apiEvent.SessionId) ? null : apiEvent.SessionId.Trim(),
EntityKey: string.IsNullOrWhiteSpace(apiEvent.EntityKey) ? null : apiEvent.EntityKey.Trim(),
Domain: string.IsNullOrWhiteSpace(apiEvent.Domain) ? null : apiEvent.Domain.Trim(),
ResultCount: apiEvent.ResultCount,
Position: apiEvent.Position,
DurationMs: apiEvent.DurationMs));
DurationMs: apiEvent.DurationMs,
AnswerStatus: string.IsNullOrWhiteSpace(apiEvent.AnswerStatus) ? null : apiEvent.AnswerStatus.Trim(),
AnswerCode: string.IsNullOrWhiteSpace(apiEvent.AnswerCode) ? null : apiEvent.AnswerCode.Trim()));
}
if (events.Count > 0)
@@ -301,6 +308,8 @@ public sealed record SearchAnalyticsApiEvent
public string Query { get; init; } = string.Empty;
public string? SessionId { get; init; }
public string? EntityKey { get; init; }
public string? Domain { get; init; }
@@ -310,6 +319,10 @@ public sealed record SearchAnalyticsApiEvent
public int? Position { get; init; }
public int? DurationMs { get; init; }
public string? AnswerStatus { get; init; }
public string? AnswerCode { get; init; }
}
public sealed record SearchHistoryApiResponse

View File

@@ -39,11 +39,11 @@ public static class SearchFeedbackEndpoints
// G10-002: List quality alerts (admin only)
group.MapGet("/quality/alerts", GetAlertsAsync)
.WithName("SearchQualityAlertsList")
.WithSummary("Lists open search quality alerts (zero-result queries, high negative feedback).")
.WithSummary("Lists open search quality alerts (zero-result, high negative feedback, fallback loops, abandoned fallback).")
.WithDescription(
"Returns search quality alerts ordered by occurrence count. " +
"Filterable by status (open, acknowledged, resolved) and alert type " +
"(zero_result, low_feedback, high_negative_feedback). Requires admin scope.")
"(zero_result, high_negative_feedback, fallback_loop, abandoned_fallback). Requires admin scope.")
.RequireAuthorization(AdvisoryAIPolicies.AdminPolicy)
.Produces<IReadOnlyList<SearchQualityAlertDto>>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status403Forbidden);
@@ -198,6 +198,12 @@ public static class SearchFeedbackEndpoints
ZeroResultRate = metrics.ZeroResultRate,
AvgResultCount = metrics.AvgResultCount,
FeedbackScore = metrics.FeedbackScore,
FallbackAnswerRate = metrics.FallbackAnswerRate,
ClarifyRate = metrics.ClarifyRate,
InsufficientRate = metrics.InsufficientRate,
ReformulationCount = metrics.ReformulationCount,
RescueActionCount = metrics.RescueActionCount,
AbandonedFallbackCount = metrics.AbandonedFallbackCount,
Period = metrics.Period,
LowQualityResults = metrics.LowQualityResults
.Select(row => new SearchLowQualityResultDto
@@ -308,6 +314,12 @@ public sealed record SearchQualityMetricsDto
public double ZeroResultRate { get; init; }
public double AvgResultCount { get; init; }
public double FeedbackScore { get; init; }
public double FallbackAnswerRate { get; init; }
public double ClarifyRate { get; init; }
public double InsufficientRate { get; init; }
public int ReformulationCount { get; init; }
public int RescueActionCount { get; init; }
public int AbandonedFallbackCount { get; init; }
public string Period { get; init; } = "7d";
public IReadOnlyList<SearchLowQualityResultDto> LowQualityResults { get; init; } = [];
public IReadOnlyList<SearchTopQueryDto> TopQueries { get; init; } = [];