100 lines
4.0 KiB
C#
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.");
|
|
}
|
|
}
|