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
| Column | Type | Description |
|---|---|---|
| id | bigint unsigned | Auto-increment primary key |
| facet_id | bigint unsigned | Foreign key to wp_gridpanda_facets.id |
| post_id | bigint unsigned | WordPress post ID that has this value |
| facet_value | varchar(500) | Raw filterable value — term slug, meta value, author ID, etc. |
| facet_display | varchar(500) | Human-readable label — term name, author display name, ACF field label |
| facet_order | bigint | Sort order (term_order for taxonomies, 0 for meta) |
| depth | int | Tree depth for hierarchy facets. 0 = root term. Used by HierarchyType. |
| parent_id | bigint unsigned | WP term_id of the parent term (hierarchy). 0 = no parent. |
| language | varchar(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:
| Column | Type | Description |
|---|---|---|
| id | bigint unsigned | Auto-increment primary key |
| type | varchar(100) | Job type: index_post, reindex_facet, reindex_all |
| payload | longtext (JSON) | Job parameters, e.g. {post_id: 123} or {facet_id: 5} |
| status | varchar(20) | Job state: pending, processing, completed, failed |
| priority | tinyint unsigned | Job priority 0–255 (lower number = higher priority) |
| attempts | tinyint unsigned | Number of times the job has been attempted |
| max_attempts | tinyint unsigned | Maximum retries before marking as failed (default: 3) |
| fingerprint | varchar(32) | MD5 hash for deduplication — prevents queuing the same job twice |
| error_message | text | Error details from the last failed attempt |
| available_at | datetime | Earliest time the job can be picked up (supports delayed execution) |
| started_at | datetime | Timestamp when processing began |
| completed_at | datetime | Timestamp when processing finished |
| locked_by | varchar(100) | Worker ID holding the lock (prevents concurrent processing) |
| locked_at | datetime | When 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:
taxonomy:{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
post_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
post_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
wc:{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 Hook | When It Fires | Grid Panda Action |
|---|---|---|
| wp_after_insert_post (WP 5.6+) | After any post is saved or updated | Index the post if it's published; skip revisions and auto-drafts |
| delete_post | Before a post is permanently deleted | Remove all index rows for this post_id immediately |
| post_updated | When post status changes | Index if transitioning to 'publish'; deindex if transitioning away |
| set_object_terms | When terms are assigned to a post | Queue the post for reindexing |
| updated_post_meta | When a post meta value is updated | Queue the post for reindexing |
| added_post_meta | When a new post meta key is added | Queue the post for reindexing |
| shutdown | At the end of the PHP request | Process all queued posts synchronously (up to async threshold) |
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
/wp-json/gridpanda/v1/index/statusGet index statistics: per-facet row counts, queue depth, timestamps
/wp-json/gridpanda/v1/index/reindex-allQueue a full site reindex (admin)
/wp-json/gridpanda/v1/index/reindex/{facet_id}Queue a per-facet reindex (admin)
/wp-json/gridpanda/v1/index/purgeDelete all rows from the index table (admin)
/wp-json/gridpanda/v1/index/cancelCancel 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.
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().