Skip to main content

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

FileRole
docroot/plugins/finder/bcconfigbox/src/Extension/Bcconfigbox.phpThe Smart Search adapter (ours, tracked): builds the index entries, handles onFinderAfterSave / onFinderAfterDelete
data/customization/system_overrides/ObserverJoomlaIndexing.phpKenedo observer: turns admin-side save/copy/delete of products, clones, elements and option xrefs into finder events
data/customization/models/bcinstantresults.php + viewInstant 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

  1. Save/copy (admin): ObserverJoomlaIndexing dispatches onFinderAfterSave with context com_configbox.product; the adapter reindexes the product and all its clones. Covers products, clones, elements and element-option xrefs.
  2. Delete (admin): onAfterDeleteRecord dispatches onFinderAfterDelete with context com_configbox.product (removes the product's entry and all its clones' entries, by URL prefix) or com_configbox.productclone (removes the single #clone_id=N entry). Removal goes through the finder Indexer, which also cleans term mappings and taxonomy.
  3. 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. Only finder: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 the is_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 params is 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 nightly finder:index run (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 (empty firstPageId). 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.ts runs 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 are utf8mb4_unicode_ci — join them numerically (CAST(… AS UNSIGNED)), never on string expressions, or you get Illegal mix of collations.