texts fixes, search bar fixes, global menu fixes.
This commit is contained in:
@@ -55,13 +55,13 @@ public sealed class KnowledgeSearchOptions
|
||||
public List<string> OpenApiRoots { get; set; } = ["src", "devops/compose"];
|
||||
|
||||
public string UnifiedFindingsSnapshotPath { get; set; } =
|
||||
"src/AdvisoryAI/StellaOps.AdvisoryAI/UnifiedSearch/Snapshots/findings.snapshot.json";
|
||||
"UnifiedSearch/Snapshots/findings.snapshot.json";
|
||||
|
||||
public string UnifiedVexSnapshotPath { get; set; } =
|
||||
"src/AdvisoryAI/StellaOps.AdvisoryAI/UnifiedSearch/Snapshots/vex.snapshot.json";
|
||||
"UnifiedSearch/Snapshots/vex.snapshot.json";
|
||||
|
||||
public string UnifiedPolicySnapshotPath { get; set; } =
|
||||
"src/AdvisoryAI/StellaOps.AdvisoryAI/UnifiedSearch/Snapshots/policy.snapshot.json";
|
||||
"UnifiedSearch/Snapshots/policy.snapshot.json";
|
||||
|
||||
public bool UnifiedAutoIndexEnabled { get; set; }
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<InternalsVisibleTo Include="StellaOps.AdvisoryAI.WebService" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Storage\Migrations\**\*.sql" LogicalName="%(RecursiveDir)%(Filename)%(Extension)" />
|
||||
<EmbeddedResource Include="Storage\Migrations\**\*.sql" />
|
||||
<EmbeddedResource Include="UnifiedSearch\Synthesis\synthesis-system-prompt.txt" LogicalName="synthesis-system-prompt.txt" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
@@ -34,6 +34,18 @@
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
<TargetPath>models/all-MiniLM-L6-v2.onnx</TargetPath>
|
||||
</None>
|
||||
<None Update="UnifiedSearch/Snapshots/findings.snapshot.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
<TargetPath>UnifiedSearch/Snapshots/findings.snapshot.json</TargetPath>
|
||||
</None>
|
||||
<None Update="UnifiedSearch/Snapshots/vex.snapshot.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
<TargetPath>UnifiedSearch/Snapshots/vex.snapshot.json</TargetPath>
|
||||
</None>
|
||||
<None Update="UnifiedSearch/Snapshots/policy.snapshot.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
<TargetPath>UnifiedSearch/Snapshots/policy.snapshot.json</TargetPath>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" />
|
||||
|
||||
@@ -16,7 +16,7 @@ internal sealed class FindingsSearchAdapter : ISearchIngestionAdapter
|
||||
{
|
||||
private const string TenantHeader = "X-StellaOps-Tenant";
|
||||
private const string HttpClientName = "scanner-internal";
|
||||
private const string FindingsEndpoint = "/api/v1/scanner/security/findings";
|
||||
private const string FindingsEndpoint = "/api/v1/security/findings";
|
||||
private const int MaxPages = 20;
|
||||
private const int PageSize = 100;
|
||||
|
||||
|
||||
@@ -12,16 +12,19 @@ namespace StellaOps.AdvisoryAI.UnifiedSearch;
|
||||
internal sealed class UnifiedSearchIndexer : IUnifiedSearchIndexer
|
||||
{
|
||||
private readonly KnowledgeSearchOptions _options;
|
||||
private readonly IKnowledgeSearchStore _store;
|
||||
private readonly IEnumerable<ISearchIngestionAdapter> _adapters;
|
||||
private readonly ILogger<UnifiedSearchIndexer> _logger;
|
||||
|
||||
public UnifiedSearchIndexer(
|
||||
IOptions<KnowledgeSearchOptions> options,
|
||||
IKnowledgeSearchStore store,
|
||||
IEnumerable<ISearchIngestionAdapter> adapters,
|
||||
ILogger<UnifiedSearchIndexer> logger)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(options);
|
||||
_options = options.Value ?? new KnowledgeSearchOptions();
|
||||
_store = store ?? throw new ArgumentNullException(nameof(store));
|
||||
_adapters = adapters ?? throw new ArgumentNullException(nameof(adapters));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
@@ -39,6 +42,8 @@ internal sealed class UnifiedSearchIndexer : IUnifiedSearchIndexer
|
||||
return new UnifiedSearchIndexSummary(0, 0, 0);
|
||||
}
|
||||
|
||||
await _store.EnsureSchemaAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
var domains = 0;
|
||||
var chunks = 0;
|
||||
@@ -131,6 +136,8 @@ internal sealed class UnifiedSearchIndexer : IUnifiedSearchIndexer
|
||||
return new UnifiedSearchIndexSummary(0, 0, 0);
|
||||
}
|
||||
|
||||
await _store.EnsureSchemaAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
var domains = 0;
|
||||
var chunks = 0;
|
||||
@@ -348,11 +355,17 @@ internal sealed class UnifiedSearchIndexer : IUnifiedSearchIndexer
|
||||
UnifiedChunk chunk,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var sourceRef = ResolveSourceRef(chunk);
|
||||
var sourcePath = ResolveSourcePath(chunk);
|
||||
|
||||
const string sql = """
|
||||
INSERT INTO advisoryai.kb_doc
|
||||
(doc_id, doc_type, product, version, source_ref, path, title, content_hash, metadata, indexed_at)
|
||||
VALUES (@doc_id, @doc_type, @product, @version, @source_ref, @path, @title, @content_hash, '{}'::jsonb, NOW())
|
||||
ON CONFLICT (doc_id) DO NOTHING;
|
||||
ON CONFLICT (doc_id) DO UPDATE SET
|
||||
title = EXCLUDED.title,
|
||||
content_hash = EXCLUDED.content_hash,
|
||||
indexed_at = NOW();
|
||||
""";
|
||||
|
||||
await using var command = connection.CreateCommand();
|
||||
@@ -362,14 +375,34 @@ internal sealed class UnifiedSearchIndexer : IUnifiedSearchIndexer
|
||||
command.Parameters.AddWithValue("doc_type", chunk.Domain);
|
||||
command.Parameters.AddWithValue("product", "stella-ops");
|
||||
command.Parameters.AddWithValue("version", "local");
|
||||
command.Parameters.AddWithValue("source_ref", chunk.Domain);
|
||||
command.Parameters.AddWithValue("path", chunk.Kind);
|
||||
command.Parameters.AddWithValue("source_ref", sourceRef);
|
||||
command.Parameters.AddWithValue("path", sourcePath);
|
||||
command.Parameters.AddWithValue("title", chunk.Title);
|
||||
command.Parameters.AddWithValue("content_hash", KnowledgeSearchText.StableId(chunk.Body));
|
||||
|
||||
await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static string ResolveSourceRef(UnifiedChunk chunk)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(chunk.EntityKey))
|
||||
{
|
||||
return chunk.EntityKey.Trim();
|
||||
}
|
||||
|
||||
return chunk.DocId;
|
||||
}
|
||||
|
||||
private static string ResolveSourcePath(UnifiedChunk chunk)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(chunk.DocId))
|
||||
{
|
||||
return chunk.DocId;
|
||||
}
|
||||
|
||||
return $"{chunk.Domain}/{chunk.Kind}";
|
||||
}
|
||||
|
||||
private static IReadOnlyList<UnifiedChunk> DeduplicateChunks(IEnumerable<UnifiedChunk> chunks)
|
||||
{
|
||||
var byChunkId = new SortedDictionary<string, UnifiedChunk>(StringComparer.Ordinal);
|
||||
|
||||
@@ -62,7 +62,7 @@ public sealed class UnifiedSearchLiveAdapterIntegrationTests
|
||||
|
||||
handler.Requests.Should().ContainSingle();
|
||||
handler.Requests[0].Tenant.Should().Be("global");
|
||||
handler.Requests[0].Uri.Should().Contain("/api/v1/scanner/security/findings");
|
||||
handler.Requests[0].Uri.Should().Contain("/api/v1/security/findings");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -326,6 +326,7 @@ public sealed class UnifiedSearchLiveAdapterIntegrationTests
|
||||
|
||||
var indexer = new UnifiedSearchIndexer(
|
||||
options,
|
||||
store,
|
||||
[
|
||||
new FindingsSearchAdapter(
|
||||
new SingleClientFactory(findingsHandler, "http://scanner.local"),
|
||||
@@ -357,6 +358,35 @@ public sealed class UnifiedSearchLiveAdapterIntegrationTests
|
||||
(await CountDomainChunksAsync(connection, "policy")).Should().Be(4);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UnifiedSearchIndexer_RebuildAllAsync_EnsuresSchema_WhenTablesAreMissing()
|
||||
{
|
||||
await using var fixture = await StartPostgresOrSkipAsync();
|
||||
var options = Options.Create(new KnowledgeSearchOptions
|
||||
{
|
||||
Enabled = true,
|
||||
ConnectionString = fixture.ConnectionString,
|
||||
FtsLanguageConfig = "simple"
|
||||
});
|
||||
|
||||
await using var store = new PostgresKnowledgeSearchStore(options, NullLogger<PostgresKnowledgeSearchStore>.Instance);
|
||||
var adapter = new MutableAdapter("findings", [BuildFindingChunk("finding-seed", "CVE-2026-3000", "Schema bootstrap finding.")]);
|
||||
var indexer = new UnifiedSearchIndexer(
|
||||
options,
|
||||
store,
|
||||
[adapter],
|
||||
NullLogger<UnifiedSearchIndexer>.Instance);
|
||||
|
||||
var summary = await indexer.RebuildAllAsync(CancellationToken.None);
|
||||
|
||||
summary.DomainCount.Should().Be(1);
|
||||
summary.ChunkCount.Should().Be(1);
|
||||
|
||||
await using var connection = new NpgsqlConnection(fixture.ConnectionString);
|
||||
await connection.OpenAsync();
|
||||
(await CountDomainChunksAsync(connection, "findings")).Should().Be(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UnifiedSearchIndexer_IndexAll_UpsertsOnlyChangedChunks_AndFindsNewFinding()
|
||||
{
|
||||
@@ -378,6 +408,7 @@ public sealed class UnifiedSearchLiveAdapterIntegrationTests
|
||||
var adapter = new MutableAdapter("findings", [unchangedChunk]);
|
||||
var indexer = new UnifiedSearchIndexer(
|
||||
options,
|
||||
store,
|
||||
[adapter],
|
||||
NullLogger<UnifiedSearchIndexer>.Instance);
|
||||
|
||||
@@ -502,6 +533,7 @@ public sealed class UnifiedSearchLiveAdapterIntegrationTests
|
||||
var adapter = new MutableAdapter("findings", [chunkTenantA]);
|
||||
var indexer = new UnifiedSearchIndexer(
|
||||
options,
|
||||
store,
|
||||
[adapter],
|
||||
NullLogger<UnifiedSearchIndexer>.Instance);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user