3.0 KiB
Performance notes
The ?seo= article filter
The SEO status of an article depends on its body, its fields, its authors, its site (default_description) and the site default language. It is computed by RefreshArticleSEOUseCase and stored on the article (seo_status, seo_suggestions). Every repository write to one of those inputs clears the stored status and raises seo_version in the same UPDATE (withStaleSEO); site-level changes (default description, default language) clear the whole site through article.SEOIndex.MarkSiteSEOStale. Before answering, GET /v1/article/ recomputes the cleared articles only, in batches of 500, reading their bodies once; StoreSEO writes a status only while seo_version is still the one it read, so a concurrent edit is never overwritten by an older assessment; the site and its languages are read after each batch of stale rows, never cached across batches, so a site-level change that commits during a refresh is either seen or makes the store miss. The server recomputes everything that is cleared at startup (the backfill of existing rows after the upgrade). ?seo= and counts.suggestions are then plain SQL conditions on seo_status, so the cost of a list no longer grows with the number of visible bodies.
TestSeoFilterPerformance in test/seo_filter_performance_test.go (skipped with -short) measures 5,000 articles of about 5 KB each in one site on a temporary SQLite file, every article with suggestions, slowest of 3 runs after the startup backfill:
| Request | Time |
|---|---|
| startup backfill of the 5,000 articles (once) | 270 ms |
?seo=suggestions&size=25 |
24 ms |
default page, no ?seo= |
23 ms |
The target is 300 ms for 5,000 articles; the test fails above 500 ms. Re-run it with go test -tags nofrontend ./test/... -run TestSeoFilterPerformance -v -count=1 after a change to the assessment rules or the article repository.
The ?seo= filter of the grouped list
GET /v1/article/?group=story filters on the stored status in SQL (q.seo_status = ? among the conditions of the qualifying versions) and counts suggestions with COUNT(DISTINCT story_id) over the readable versions, so no id list is built for the SEO filter. With q, the title search still runs once per request through article.StoryReader.FindTitleMatches (accent folding happens in Go) and its matches are handed to the repository as StoryFilters.VersionIDs. That set can be larger than any bind-parameter limit (SQLite accepts 32,766 parameters), so it never becomes an IN (?, …) list: the repository reads the qualifying (story_id, id) pairs and the ordered story rows with id-only projections and keeps the members of the set in memory before paging. ListByStories reads the versions of the page without their bodies and only those the caller may read; ListVersions reads the bodies of the page's representatives only. TestArticleStoryListSEOFilterAtScaleSQLite covers 40,001 versions: the backfill assesses each of them once, and the list requests that follow read no stale version and no page of bodies.