Indexer & Auto-Sync

The Grid Panda indexer pre-builds a lookup table of facet value → post ID relationships. This avoids running complex WP_Query aggregations on every filter request and makes facet count calculations fast regardless of post volume.

Why the Index Exists

Without an index, calculating facet counts requires joining wp_posts, wp_postmeta, and term tables for every unique facet value on every request — even for facets not being filtered. For a shop with 5,000 products and 15 facets, that becomes hundreds of queries per page load.

The index table denormalizes this into a flat structure: one row per post × facet × value combination. Count queries become simple SELECT COUNT(DISTINCT post_id) scans on an indexed table — sub-millisecond even for large sites.

Index Table Schema: wp_gridpanda_index

ColumnTypeDescription
idbigint unsignedAuto-increment primary key
facet_idbigint unsignedForeign key to wp_gridpanda_facets.id
post_idbigint unsignedWordPress post ID that has this value
facet_valuevarchar(500)Raw filterable value — term slug, meta value, author ID, etc.
facet_displayvarchar(500)Human-readable label — term name, author display name, ACF field label
facet_orderbigintSort order (term_order for taxonomies, 0 for meta)
depthintTree depth for hierarchy facets. 0 = root term. Used by HierarchyType.
parent_idbigint unsignedWP term_id of the parent term (hierarchy). 0 = no parent.
languagevarchar(10)Language code for multilingual indexing (WPML/Polylang). Empty = all languages.

Indexes

PRIMARY KEY (id)
KEY facet_post   (facet_id, post_id)         -- main lookup
KEY facet_value  (facet_id, facet_value(191))-- value filtering
KEY post_facet   (post_id, facet_id)         -- deindex by post
KEY facet_order  (facet_id, facet_order)     -- sorted results
KEY facet_depth  (facet_id, depth, parent_id)-- hierarchy queries
KEY facet_lang   (facet_id, language)        -- WPML/Polylang

Queue Table Schema: wp_gridpanda_queue

Large reindex operations run asynchronously through the job queue. The queue also handles incremental indexing when the async threshold is exceeded:

ColumnTypeDescription
idbigint unsignedAuto-increment primary key
typevarchar(100)Job type: index_post, reindex_facet, reindex_all
payloadlongtext (JSON)Job parameters, e.g. {post_id: 123} or {facet_id: 5}
statusvarchar(20)Job state: pending, processing, completed, failed
prioritytinyint unsignedJob priority 0–255 (lower number = higher priority)
attemptstinyint unsignedNumber of times the job has been attempted
max_attemptstinyint unsignedMaximum retries before marking as failed (default: 3)
fingerprintvarchar(32)MD5 hash for deduplication — prevents queuing the same job twice
error_messagetextError details from the last failed attempt
available_atdatetimeEarliest time the job can be picked up (supports delayed execution)
started_atdatetimeTimestamp when processing began
completed_atdatetimeTimestamp when processing finished
locked_byvarchar(100)Worker ID holding the lock (prevents concurrent processing)
locked_atdatetimeWhen the lock was acquired

Data Resolvers

The indexer uses a resolver pattern. Each resolver handles one source type and returns an array of{ value, display, order, depth, parent_id } objects for a given post:

TaxonomyResolvertaxonomy:{slug}

Calls wp_get_object_terms() for the post. For hierarchical taxonomies, calculates depth via get_ancestors() and captures parent term_id. Stores term slug as value, term name as display.

Output format: value=slug, display=name, order=term_order, depth=ancestor_count, parent_id=parent_term_id

PostMetaResolverpost_meta:{key}

Reads meta values from wp_postmeta. Handles serialized PHP arrays by iterating each element. For ACF fields, reads the field label as display value. Supports multiple values per key.

Output format: value=meta_value, display=ACF_label_or_value, order=0, depth=0, parent_id=null

PostFieldResolverpost_field:{field}

Reads standard WP_Post object fields. post_author → user ID as value, display_name as display. post_date → YYYY-MM-DD as value, formatted date as display.

Output format: value=field_value, display=human_readable, order=0, depth=0, parent_id=null

WooCommerceResolverwc:{field}

Handles WooCommerce-specific fields: stock_status, on_sale, product attributes (pa_*), price, rating, weight. Reads from WC's internal meta keys or product methods.

Output format: value=wc_field_value, display=formatted_label, order=0, depth=0, parent_id=null

Incremental Indexing (Auto-Sync)

The IncrementalIndexer registers WordPress hooks that trigger re-indexing when post data changes. Posts are queued within the request and processed at shutdown:

WordPress HookWhen It FiresGrid Panda Action
wp_after_insert_post (WP 5.6+)After any post is saved or updatedIndex the post if it's published; skip revisions and auto-drafts
delete_postBefore a post is permanently deletedRemove all index rows for this post_id immediately
post_updatedWhen post status changesIndex if transitioning to 'publish'; deindex if transitioning away
set_object_termsWhen terms are assigned to a postQueue the post for reindexing
updated_post_metaWhen a post meta value is updatedQueue the post for reindexing
added_post_metaWhen a new post meta key is addedQueue the post for reindexing
shutdownAt the end of the PHP requestProcess all queued posts synchronously (up to async threshold)
Async threshold: When more than 50 posts are queued in a single request (e.g. bulk-editing in the admin), the indexer switches to async queue jobs instead of processing inline at shutdown. This prevents request timeouts during large bulk operations.

Full Reindex & Per-Facet Reindex

A full reindex clears the entire index table and re-runs all resolvers for every published post across all facets. A per-facet reindex only re-indexes posts for a single facet (useful after adding a new facet to an existing site).

From the admin (Grid Panda → Index Status)

Click Reindex All to queue a full reindex, or click Reindex beside a specific facet for a per-facet reindex. The Index Status page shows queue depth and estimated completion time.

Via REST API (admin only)

POST /wp-json/gridpanda/v1/index/reindex-all queues a full reindex. POST /wp-json/gridpanda/v1/index/reindex/{facet_id} queues a per-facet reindex.

Via WordPress action

do_action('gridpanda/indexer/reindex_all') or do_action('gridpanda/indexer/reindex_facet', $facet_id) from custom code.

Indexer REST API

GET
/wp-json/gridpanda/v1/index/status

Get index statistics: per-facet row counts, queue depth, timestamps

POST
/wp-json/gridpanda/v1/index/reindex-all

Queue a full site reindex (admin)

POST
/wp-json/gridpanda/v1/index/reindex/{facet_id}

Queue a per-facet reindex (admin)

DELETE
/wp-json/gridpanda/v1/index/purge

Delete all rows from the index table (admin)

POST
/wp-json/gridpanda/v1/index/cancel

Cancel all pending queue jobs (admin)

Multilingual Indexing

When WPML or Polylang is active, Grid Panda indexes each post in every active language. The language column in the index table stores the language code. At query time, Grid Panda automatically filters by the current language so facet counts are language-aware.

WPML indexing setup fires the gridpanda/wpml/indexing_setup action. Polylang fires gridpanda/polylang/indexing_setup with the active languages array. String translations for facet names are registered via icl_register_string().