Repair search result routing and advisory query ranking
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user