Files
itworx-knowledge-hub-Public/tests/backend/ItWorx.KnowledgeHub.IntegrationTests/Knowledge/SearchScaleTests.cs
T

100 lines
4.0 KiB
C#

using System.Diagnostics;
using System.Net.Http.Json;
using ItWorx.KnowledgeHub.Contracts.Knowledge;
using ItWorx.KnowledgeHub.Domain.Knowledge;
using ItWorx.KnowledgeHub.Infrastructure.Persistence;
using ItWorx.KnowledgeHub.IntegrationTests.Infrastructure;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
namespace ItWorx.KnowledgeHub.IntegrationTests.Knowledge;
/// <summary>
/// M3-A07 proof at a reduced, locally-runnable scale. docs/10-search-and-indexing.md's
/// aspirational scale-verification dataset (100,000 current pages, 1,000,000 revisions)
/// requires dedicated benchmarking infrastructure and multi-minute seeding that does not
/// fit a routine local/CI test run; that full-scale verification is explicitly deferred
/// to M8's "Performance and scale evidence" gate (docs/adr/ADR-012-m3-discovery-revisions-search.md).
/// This test instead proves the search query plan and GIN indexes keep a bounded,
/// permission-filtered query fast and correct at a smaller but still non-trivial
/// synthetic scale — the same proof shape as M2-A07's 10,000-page tree scale test.
/// </summary>
[Collection(PostgresTestGroup.Name)]
public sealed class SearchScaleTests(PostgresContainerFixture postgres) : OwnerSetupTestBase(postgres)
{
private const int PageCount = 5_000;
[Fact]
public async Task Search_stays_fast_and_correct_against_5000_pages_with_one_needle()
{
using var client = Factory.CreateClient();
await SetupHelper.LoginAsync(client, OwnerEmail, OwnerPassword);
Guid needleId;
using (var scope = Factory.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<KnowledgeHubDbContext>();
var workspace = await db.Workspaces.AsNoTracking().FirstAsync();
var owner = await db.Users.AsNoTracking().FirstAsync();
var sortKey = SortKey.First();
var batch = new List<Page>(1000);
Page? needle = null;
for (var i = 0; i < PageCount; i++)
{
var isNeedle = i == PageCount / 2;
var page = Page.Create(
workspace.Id,
null,
sortKey,
PageTitle.Create(isNeedle ? "Uniek vindbaar trefwoord xyzzyplugh" : $"Vulpagina {i:D5}"),
DocumentType.WikiPage,
isNeedle ? "Deze pagina bevat het unieke trefwoord xyzzyplugh nergens anders." : "Gewone opvultekst zonder bijzondere termen.",
owner.Id,
DateTimeOffset.UtcNow);
if (isNeedle)
{
needle = page;
}
batch.Add(page);
sortKey = SortKey.Next(sortKey);
if (batch.Count == 1000)
{
db.Pages.AddRange(batch);
await db.SaveChangesAsync();
db.ChangeTracker.Clear();
batch.Clear();
}
}
if (batch.Count > 0)
{
db.Pages.AddRange(batch);
await db.SaveChangesAsync();
}
Assert.NotNull(needle);
needleId = needle.Id;
}
var stopwatch = Stopwatch.StartNew();
var response = await client.GetFromJsonAsync<SearchResponse>("/api/v1/search?q=xyzzyplugh");
stopwatch.Stop();
Assert.NotNull(response);
Assert.Contains(response.Items, item => item.Id == needleId);
// Generous bound for a CI/dev machine, proving the GIN-indexed query stays
// bounded rather than scanning ~5,000 rows' worth of text at request time — not
// a precise benchmark (that belongs to the M8 release scale suite at full
// documented scale).
Assert.True(
stopwatch.Elapsed < TimeSpan.FromSeconds(3),
$"Searching {PageCount} pages for one needle term took {stopwatch.Elapsed}, expected well under 3s.");
}
}