KamoCRM

Don't load content_json to build the public tree index, and stop loading it twice

PerformanceKBService
Shipped
September 23, 2026 at 2:17 AM UTC
Author
Kamo
Commit
155e7de

buildPublicTreeIndex builds a small structural index (parent/child, slug path, ancestors) from every public article in the org, but it was loading full KbArticle rows to do it — content_json included, measured at ~6.6 KB per row against a few hundred bytes for the rest of a row combined. The tree index and everything downstream of it (the excerpt on every list/tree node, path and ancestor resolution) never reads content_json; only a single-article read serializes it, and that one row already comes from its own separate lookup. A 300-article organization's tree/list/single response was pulling ~2 MB of body text out of the database to immediately discard it for all but one row. **************** selects everything the index actually reads by name (a JPQL projection query, not a repository method — KbArticleRepository lives in kamo-shared-library, out of scope here) and builds real KbArticle instances from the rows via the entity's own builder, never persisted or attached to the session. Every existing reader of PublicTreeIndex keeps working unchanged; getContentJson() on one of these is simply null, which only matters to code that serializes it, and nothing outside a single-article read does. Separately, /tree-by-parent-slug called both **************** (which loaded its own copy of every public article to rebuild the same children map buildPublicTreeIndex already builds) and buildPublicTreeIndex — one request, the full org-wide article list loaded twice. The former now takes the index the caller already built as a parameter instead of rebuilding it. Also, translateRefs (breadcrumbs and "see also" links, non-English) resolved each ref's translation by re-looking the ref's guid back up to a KbArticle and then querying its translation — two queries per ref. A ref already carries the uid of the article it names now (KbArticleRefDTO.uid, internal only — @JsonIgnore, never in the public response), so **************** resolves every ref in a list in one batched IN query instead. Tests: KbArticleServiceTreeIndexTest asserts the projection carries every field a full entity load would have (contentJson excepted, asserted null) and that the tree/path/ancestor computation built from it is the same three-level shape a full-entity load would have produced — the fixture-based old-vs-new check. **************** exercises /by-slug end to end in French and proves the batched lookup replaces the guid round-trip **************** is never called for a ref).

All changes

Like what you see shipping?

All of it arrives in your workspace on its own. Start on the free plan and read this page again in a month.

Start Free ForeverView Pricing