Publish curated Knowledge Hub source from 2ed833f290b439a7bc91e5fd19b042c269e3bee9
This commit is contained in:
1 parent
10f2edcd29
commit
59f15b2d08
218 files changed
+23995
-1849
No files matched your search
+107
@@ -0,0 +1,107 @@
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text;
|
||||
using ItWorx.KnowledgeHub.Contracts.UnifiedSearch;
|
||||
using ItWorx.KnowledgeHub.Domain.Knowledge;
|
||||
using ItWorx.KnowledgeHub.Domain.Projects;
|
||||
using ItWorx.KnowledgeHub.Infrastructure.Persistence;
|
||||
using ItWorx.KnowledgeHub.Infrastructure.UnifiedSearch;
|
||||
using ItWorx.KnowledgeHub.IntegrationTests.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using SortKey = ItWorx.KnowledgeHub.Domain.Knowledge.SortKey;
|
||||
|
||||
namespace ItWorx.KnowledgeHub.IntegrationTests.UnifiedSearch;
|
||||
|
||||
[Collection(PostgresTestGroup.Name)]
|
||||
[Trait("Category", "ReleaseScale")]
|
||||
public sealed class UnifiedSearchScaleTests(PostgresContainerFixture postgres) : OwnerSetupTestBase(postgres)
|
||||
{
|
||||
[Fact]
|
||||
public async Task Unified_search_counts_and_cursor_pages_stay_bounded_with_10000_pages_and_2000_projects()
|
||||
{
|
||||
using var client = Factory.CreateClient();
|
||||
await SetupHelper.LoginAsync(client, OwnerEmail, OwnerPassword);
|
||||
using var scope = Factory.Services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<KnowledgeHubDbContext>();
|
||||
var workspaceId = await db.Workspaces.Select(item => item.Id).SingleAsync();
|
||||
var actorId = await db.Users.Select(item => item.Id).SingleAsync();
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var sortKey = SortKey.First();
|
||||
|
||||
for (var offset = 0; offset < 10_000; offset += 500)
|
||||
{
|
||||
for (var index = offset; index < offset + 500; index++)
|
||||
{
|
||||
var title = index == 0 ? "Measure uniqueneedle page" : $"Measure page {index:D5}";
|
||||
var page = Page.Create(workspaceId, null, sortKey, PageTitle.Create(title), DocumentType.WikiPage,
|
||||
"Measure synthetic documentation with a bounded result snippet.", actorId, now);
|
||||
page.ReplaceTags(["platform-scale"]);
|
||||
db.Pages.Add(page);
|
||||
sortKey = SortKey.Next(sortKey);
|
||||
}
|
||||
await db.SaveChangesAsync();
|
||||
db.ChangeTracker.Clear();
|
||||
}
|
||||
|
||||
for (var offset = 0; offset < 2_000; offset += 500)
|
||||
{
|
||||
for (var index = offset; index < offset + 500; index++)
|
||||
{
|
||||
var title = index == 0 ? "Measure uniqueneedle project" : $"Measure project {index:D5}";
|
||||
db.Projects.Add(Project.Create(workspaceId, title, "Measure synthetic project summary.",
|
||||
ProjectPriority.Medium, "Measure project state.", actorId, actorId, now));
|
||||
}
|
||||
await db.SaveChangesAsync();
|
||||
db.ChangeTracker.Clear();
|
||||
}
|
||||
await db.Database.ExecuteSqlRawAsync("ANALYZE pages; ANALYZE projects;");
|
||||
|
||||
var repository = new UnifiedSearchRepository(db);
|
||||
var first = await repository.SearchAsync(workspaceId, "measure", null, null, null, 50, CancellationToken.None);
|
||||
Assert.Equal(10_000, first.Counts.Knowledge);
|
||||
Assert.Equal(2_000, first.Counts.Projects);
|
||||
Assert.Equal(50, first.Items.Count);
|
||||
Assert.NotNull(first.NextCursor);
|
||||
var next = await repository.SearchAsync(workspaceId, "measure", null, null, first.NextCursor, 50, CancellationToken.None);
|
||||
Assert.Equal(first.Counts, next.Counts);
|
||||
Assert.Equal(50, next.Items.Count);
|
||||
Assert.Empty(first.Items.Select(item => item.Id).Intersect(next.Items.Select(item => item.Id)));
|
||||
|
||||
var report = new StringBuilder();
|
||||
report.AppendLine(CultureInfo.InvariantCulture, $"Unified search scale — {DateTimeOffset.UtcNow:O}; 10000 pages, 2000 projects; 30 warm samples per query.");
|
||||
foreach (var query in new[] { "measure", "uniqueneedle" })
|
||||
{
|
||||
var samples = new List<double>();
|
||||
for (var sample = 0; sample < 30; sample++)
|
||||
{
|
||||
var watch = Stopwatch.StartNew();
|
||||
var result = await repository.SearchAsync(workspaceId, query, null, null, null, 50, CancellationToken.None);
|
||||
watch.Stop();
|
||||
Assert.Equal(query == "measure" ? 10_000 : 1, result.Counts.Knowledge);
|
||||
Assert.Equal(query == "measure" ? 2_000 : 1, result.Counts.Projects);
|
||||
Assert.InRange(result.Items.Count, 1, 50);
|
||||
samples.Add(watch.Elapsed.TotalMilliseconds);
|
||||
}
|
||||
samples.Sort();
|
||||
var p95 = samples[28];
|
||||
report.AppendLine(CultureInfo.InvariantCulture, $"{query}: p50={samples[14]:F2}ms p95={p95:F2}ms max={samples[^1]:F2}ms");
|
||||
// Same broad correctness/latency budget as existing scale gates; this
|
||||
// exercises the new grouped-count query rather than the legacy search.
|
||||
Assert.True(p95 < 3_000, $"Unified search p95 for '{query}' was {p95:F2}ms (budget 3000ms).");
|
||||
}
|
||||
|
||||
using var response = await client.GetAsync("/api/v1/unified-search?q=measure&limit=50");
|
||||
response.EnsureSuccessStatusCode();
|
||||
var bytes = await response.Content.ReadAsByteArrayAsync();
|
||||
var body = await response.Content.ReadFromJsonAsync<UnifiedSearchResponse>();
|
||||
Assert.NotNull(body);
|
||||
Assert.Equal(50, body.Items.Count);
|
||||
Assert.Equal(12_000, body.Counts.Total);
|
||||
Assert.True(bytes.Length < 100_000, $"One 50-item search page contained {bytes.Length} bytes.");
|
||||
report.AppendLine(CultureInfo.InvariantCulture, $"HTTP 50-item response: {bytes.Length} bytes; total=12000; no duplicate IDs across cursor pages.");
|
||||
Directory.CreateDirectory("artifacts/performance");
|
||||
await File.WriteAllTextAsync("artifacts/performance/platform-unified-search-scale.txt", report.ToString());
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user