Product search & Smart Search indexing
Audience: developers & AI agents · Scope: how ConfigBox products get into (and out of) the Joomla Smart Search index, and the search surfaces that read it · Last reviewed: 2026-08-19
TL;DR — Site search runs on Joomla Smart Search (com_finder) with a custom adapter that
writes one index entry per product plus one per product clone (same URL + #clone_id=N).
The index changes only when told to: admin saves/copies and deletes propagate through
ObserverJoomlaIndexing → finder events → the adapter, and the scheduled finder:index CLI
upserts but never deletes. Deletions have been propagated since migration 0.5.89
(2026-08); before that, deleted products/clones stayed in the index forever.
The pieces
| File | Role |
|---|---|
docroot/plugins/finder/bcconfigbox/src/Extension/Bcconfigbox.php | The Smart Search adapter (ours, tracked): builds the index entries, handles onFinderAfterSave / onFinderAfterDelete |
data/customization/system_overrides/ObserverJoomlaIndexing.php | Kenedo observer: turns admin-side save/copy/delete of products, clones, elements and option xrefs into finder events |
data/customization/models/bcinstantresults.php + view | Instant results dropdown: queries #__finder_links/#__finder_links_terms directly (prefix term match, APCu-cached 3 min per customer group) |
templates/betacalco2/html/com_finder/search/ | The /search results page (standard com_finder query path) |
What one index entry holds
Baked in at index time (stale until the product is reindexed): title/subtitle, intro text,
SKUs of luminaire-coded options, converted old codes, image URL, and params with
listing_title, is_discontinued, is_hidden, is_quick_ship and group_ids — the
customer-group filter both search surfaces apply at query time. bc_discontinued or
is_hidden products are indexed unpublished (invisible to search, entry still present).
URLs are the identity of an entry — the indexer upserts by exact URL match:
index.php?option=com_configbox&view=configuratorpage&prod_id=771&page_id=717 ← product
index.php?option=com_configbox&view=configuratorpage&prod_id=771&page_id=717#clone_id=435 ← clone
The adapter hardcodes https://betacalco.com/ as the image base, so locally indexed results
point their thumbnails at production.
The three maintenance paths — and the trap
- Save/copy (admin):
ObserverJoomlaIndexingdispatchesonFinderAfterSavewith contextcom_configbox.product; the adapter reindexes the product and all its clones. Covers products, clones, elements and element-option xrefs. - Delete (admin):
onAfterDeleteRecorddispatchesonFinderAfterDeletewith contextcom_configbox.product(removes the product's entry and all its clones' entries, by URL prefix) orcom_configbox.productclone(removes the single#clone_id=Nentry). Removal goes through the finderIndexer, which also cleans term mappings and taxonomy. - Cron:
php cli/joomla.php finder:index(schedule lives in the server crontab) walks the adapter's list query and upserts by URL — it never removes anything. Onlyfinder:index purge(full wipe + rebuild of all content types) removes entries.
The trap all three circle around: nothing reconciles the index against the database. An
entry disappears only via path 2, so anything deleted before that path existed stayed
published and searchable — migration 0.5.89 cleared that backlog (25 clone entries on the
2026-08 dataset). If stale entries ever reappear, check that deletions actually run through
the Kenedo models (raw SQL deletes bypass the observer), then rebuild with
finder:index purge as the blunt fix.
Quirks worth knowing
- The adapter is dispatched through Joomla's legacy plugin layer (it doesn't implement
SubscriberInterface), which unpacks event arguments positionally: handlers receive($context, $subject, …), not the event object. That's what theis_string($event)branch in each handler is for. Don't "fix" a handler to type-hint the event class without registering the plugin as a real subscriber. Adapter::remove()(core) is useless here — it matches links on the default URL format (…&view=product&id=N), which this adapter never writes. Deletions must (and do) look up links by the configuratorpage URLs instead.- Everything in
paramsis index-time state. Changing a product's customer groups, discontinued flag or image only reaches search after a reindex — the observer covers admin edits; imports that write products via raw SQL would not be picked up until the nightlyfinder:indexrun (and deletions that bypass the models are never picked up at all). - A product with no pages still gets indexed, with a broken
…&page_id=URL (emptyfirstPageId). Harmless for real products; test/archive products should be hidden or discontinued so they index unpublished. - E2E coverage:
tests/specs/backend/product-clone-search-indexing.spec.tsruns the full lifecycle through the real admin UI — clone created → searchable, clone deleted → gone (skipped on live; it mutates catalog data). - The core finder tables are
utf8mb4_0900_ai_ci(Joomla-created on MySQL 8) while CBX tables areutf8mb4_unicode_ci— join them numerically (CAST(… AS UNSIGNED)), never on string expressions, or you getIllegal mix of collations.