Repair search result routing and advisory query ranking

This commit is contained in:
master
2026-03-12 11:57:40 +02:00
parent 6964a046a5
commit 29b68f5bee
17 changed files with 945 additions and 20 deletions

View File

@@ -40,8 +40,10 @@ internal sealed class DomainWeightCalculator
var hasCve = entities.Any(static e =>
e.EntityType.Equals("cve", StringComparison.OrdinalIgnoreCase) ||
e.EntityType.Equals("ghsa", StringComparison.OrdinalIgnoreCase));
var mentionsSecurityIdFamily = query.Contains("cve", StringComparison.OrdinalIgnoreCase) ||
query.Contains("ghsa", StringComparison.OrdinalIgnoreCase);
if (hasCve)
if (hasCve || mentionsSecurityIdFamily)
{
weights["findings"] += tuning.CveBoostFindings;
weights["vex"] += tuning.CveBoostVex;

View File

@@ -1718,19 +1718,18 @@ internal sealed class UnifiedSearchService : IUnifiedSearchService
{
var method = GetMetadataString(metadata, "method") ?? "GET";
var path = GetMetadataString(metadata, "path") ?? "/";
var service = GetMetadataString(metadata, "service") ?? "unknown";
var operationId = GetMetadataString(metadata, "operationId") ?? row.Title;
actions.Add(new EntityCardAction(
"Open",
"navigate",
$"/ops/integrations?q={Uri.EscapeDataString(operationId)}",
null,
true));
actions.Add(new EntityCardAction(
"Curl",
"Copy Curl",
"copy",
null,
$"curl -X {method.ToUpperInvariant()} \"$STELLAOPS_API_BASE{path}\"",
true));
actions.Add(new EntityCardAction(
"Copy Operation ID",
"copy",
null,
operationId,
false));
break;
}

View File

@@ -7,6 +7,7 @@ internal static class WeightedRrfFusion
private const int ReciprocalRankConstant = 60;
private const double EntityProximityBoost = 0.8;
private const double MaxFreshnessBoost = 0.05;
private const double ApiOperationAdvisoryPenalty = -0.08;
private const int FreshnessDaysCap = 365;
public static IReadOnlyList<(KnowledgeChunkRow Row, double Score, IReadOnlyDictionary<string, string> Debug)> Fuse(
@@ -67,12 +68,14 @@ internal static class WeightedRrfFusion
? ComputeFreshnessBoost(item.Row, referenceTime ?? DateTimeOffset.UnixEpoch)
: 0d;
var popBoost = ComputePopularityBoost(item.Row, popularityMap, popularityBoostWeight);
item.Score += entityBoost + contextBoost + gravityBoost + freshnessBoost + popBoost;
var resultTypeAdjustment = ComputeResultTypeAdjustment(item.Row, query, detectedEntities);
item.Score += entityBoost + contextBoost + gravityBoost + freshnessBoost + popBoost + resultTypeAdjustment;
item.Debug["entityBoost"] = entityBoost.ToString("F6", System.Globalization.CultureInfo.InvariantCulture);
item.Debug["contextBoost"] = contextBoost.ToString("F6", System.Globalization.CultureInfo.InvariantCulture);
item.Debug["gravityBoost"] = gravityBoost.ToString("F6", System.Globalization.CultureInfo.InvariantCulture);
item.Debug["freshnessBoost"] = freshnessBoost.ToString("F6", System.Globalization.CultureInfo.InvariantCulture);
item.Debug["popularityBoost"] = popBoost.ToString("F6", System.Globalization.CultureInfo.InvariantCulture);
item.Debug["resultTypeAdjustment"] = resultTypeAdjustment.ToString("F6", System.Globalization.CultureInfo.InvariantCulture);
item.Debug["chunkId"] = item.Row.ChunkId;
return item;
})
@@ -298,4 +301,66 @@ internal static class WeightedRrfFusion
var entityKey = entityKeyProp.GetString();
return string.IsNullOrWhiteSpace(entityKey) ? null : entityKey.Trim();
}
private static double ComputeResultTypeAdjustment(
KnowledgeChunkRow row,
string query,
IReadOnlyList<EntityMention>? detectedEntities)
{
if (!string.Equals(row.Kind, "api_operation", StringComparison.OrdinalIgnoreCase))
{
return 0d;
}
if (!IsAdvisoryIntentQuery(query, detectedEntities) || IsExplicitApiIntentQuery(query))
{
return 0d;
}
return ApiOperationAdvisoryPenalty;
}
private static bool IsAdvisoryIntentQuery(
string query,
IReadOnlyList<EntityMention>? detectedEntities)
{
if (detectedEntities?.Any(static entity =>
string.Equals(entity.EntityType, "cve", StringComparison.OrdinalIgnoreCase) ||
string.Equals(entity.EntityType, "ghsa", StringComparison.OrdinalIgnoreCase)) == true)
{
return true;
}
if (string.IsNullOrWhiteSpace(query))
{
return false;
}
return query.Contains("cve", StringComparison.OrdinalIgnoreCase)
|| query.Contains("ghsa", StringComparison.OrdinalIgnoreCase)
|| query.Contains("vulnerability", StringComparison.OrdinalIgnoreCase)
|| query.Contains("advisory", StringComparison.OrdinalIgnoreCase)
|| query.Contains("cvss", StringComparison.OrdinalIgnoreCase)
|| query.Contains("epss", StringComparison.OrdinalIgnoreCase)
|| query.Contains("kev", StringComparison.OrdinalIgnoreCase);
}
private static bool IsExplicitApiIntentQuery(string query)
{
if (string.IsNullOrWhiteSpace(query))
{
return false;
}
return query.Contains("/api/", StringComparison.OrdinalIgnoreCase)
|| query.Contains("curl", StringComparison.OrdinalIgnoreCase)
|| query.Contains("endpoint", StringComparison.OrdinalIgnoreCase)
|| query.Contains("operation", StringComparison.OrdinalIgnoreCase)
|| query.Contains("route", StringComparison.OrdinalIgnoreCase)
|| query.Contains("get ", StringComparison.OrdinalIgnoreCase)
|| query.Contains("post ", StringComparison.OrdinalIgnoreCase)
|| query.Contains("put ", StringComparison.OrdinalIgnoreCase)
|| query.Contains("patch ", StringComparison.OrdinalIgnoreCase)
|| query.Contains("delete ", StringComparison.OrdinalIgnoreCase);
}
}

View File

@@ -133,6 +133,21 @@ public sealed class QueryUnderstandingTests
weights["vex"].Should().BeGreaterThan(weights["knowledge"]);
}
[Fact]
public void DomainWeightCalculator_boosts_generic_cve_keyword_queries()
{
var extractor = new EntityExtractor();
var classifier = new IntentClassifier();
var calculator = new DomainWeightCalculator(extractor, classifier, Options.Create(new KnowledgeSearchOptions()));
var entities = extractor.Extract("cve");
var weights = calculator.ComputeWeights("cve", entities, null);
weights["findings"].Should().BeGreaterThan(weights["knowledge"]);
weights["vex"].Should().BeGreaterThan(weights["knowledge"]);
weights["graph"].Should().BeGreaterThan(weights["knowledge"]);
}
[Fact]
public void DomainWeightCalculator_boosts_policy_for_policy_query()
{

View File

@@ -1383,6 +1383,13 @@ public sealed class UnifiedSearchServiceTests
card.Preview.StructuredFields.Should().Contain(f => f.Label == "Summary" && f.Value == "Start a new scan");
card.Preview.Content.Should().Contain("curl");
card.Preview.Content.Should().Contain("POST");
card.Actions.Should().HaveCountGreaterThanOrEqualTo(2);
card.Actions[0].Label.Should().Be("Copy Curl");
card.Actions[0].ActionType.Should().Be("copy");
card.Actions[0].Route.Should().BeNull();
card.Actions[0].Command.Should().Contain("/api/v1/scanner/scans");
card.Actions[1].Label.Should().Be("Copy Operation ID");
card.Actions[1].Command.Should().Be("createScan");
}
[Fact]

View File

@@ -227,6 +227,72 @@ public sealed class WeightedRrfFusionTests
"when popularity boost is disabled, ranking should match the baseline order");
}
[Fact]
public void Fuse_demotes_api_operations_for_advisory_intent_queries()
{
var weights = new Dictionary<string, double>
{
["knowledge"] = 1.0,
["findings"] = 1.0
};
using var apiMetadata = JsonDocument.Parse("""{"domain":"knowledge","entity_key":"api:search"}""");
using var findingMetadata = JsonDocument.Parse("""{"domain":"findings","cveId":"CVE-2026-1234"}""");
var apiRow = MakeRow("chunk-api", "api_operation", "GET /api/v1/search/query", apiMetadata);
var findingRow = MakeRow("chunk-finding", "finding", "CVE-2026-1234 finding", findingMetadata);
var lexical = new Dictionary<string, (string ChunkId, int Rank, KnowledgeChunkRow Row)>(StringComparer.Ordinal)
{
["chunk-api"] = ("chunk-api", 1, apiRow),
["chunk-finding"] = ("chunk-finding", 2, findingRow)
};
var result = WeightedRrfFusion.Fuse(
weights,
lexical,
[],
"cve",
null);
result.Should().HaveCount(2);
result[0].Row.ChunkId.Should().Be("chunk-finding");
result.Single(item => item.Row.ChunkId == "chunk-api").Debug["resultTypeAdjustment"].Should().NotBe("0.000000");
}
[Fact]
public void Fuse_keeps_api_operations_ranked_for_explicit_api_queries()
{
var weights = new Dictionary<string, double>
{
["knowledge"] = 1.0,
["findings"] = 1.0
};
using var apiMetadata = JsonDocument.Parse("""{"domain":"knowledge","entity_key":"api:search"}""");
using var findingMetadata = JsonDocument.Parse("""{"domain":"findings","cveId":"CVE-2026-1234"}""");
var apiRow = MakeRow("chunk-api", "api_operation", "GET /api/v1/search/query", apiMetadata);
var findingRow = MakeRow("chunk-finding", "finding", "CVE-2026-1234 finding", findingMetadata);
var lexical = new Dictionary<string, (string ChunkId, int Rank, KnowledgeChunkRow Row)>(StringComparer.Ordinal)
{
["chunk-api"] = ("chunk-api", 1, apiRow),
["chunk-finding"] = ("chunk-finding", 2, findingRow)
};
var result = WeightedRrfFusion.Fuse(
weights,
lexical,
[],
"cve api endpoint",
null);
result.Should().HaveCount(2);
result[0].Row.ChunkId.Should().Be("chunk-api");
result.Single(item => item.Row.ChunkId == "chunk-api").Debug["resultTypeAdjustment"].Should().Be("0.000000");
}
private static KnowledgeChunkRow MakeRow(
string chunkId,
string kind,