PHP Search API
CelerSearch exposes one public helper, celersearch(), with one method: search(). It runs
the same search the plugin's own surfaces run — index selection, pinned (curated) results
for both exact and contains matches, visibility filtering by context, sorting, pagination,
facet counts and query analytics — and hands you back one page of results as a plain array.
That means a custom search you build yourself behaves like the built-in one: the merchandising your store manager set up still applies, drafts still stay hidden, and the search still shows up in your reports.
$results = celersearch()->search( [ 'query' => 'winter jacket' ] );
Call it from anywhere that runs at or after plugins_loaded: REST handlers, cron and Action
Scheduler jobs, WP-CLI commands, shortcodes, blocks and theme templates.
When to use it
| You want to… | Use |
|---|---|
| Search from your own PHP — a custom endpoint, shortcode, block, template, CLI command, scheduled job | This API |
| Add a search box to a page without writing code | Views and the [celersearch] shortcode |
| Replace an existing WordPress search screen | Search Areas |
| Faceted filtering on a WooCommerce shop page | Shop Filters or a shop View |
The plugin's own REST endpoints (autocomplete, View search, shop filter) exist to serve the plugin's own front-end widgets, so their parameters and payloads are shaped around those widgets and are free to change with them. When you're writing PHP, call the PHP API instead — it's the surface that carries a stability promise, and if you need HTTP you can wrap it in a REST route of your own, in exactly the shape your front end wants.
Getting started
celersearch() only exists when the plugin is active, so guard for it — your integration
should keep working (with whatever fallback you choose) when someone deactivates CelerSearch.
if ( ! function_exists( 'celersearch' ) ) {
return; // CelerSearch is not active — fall back to WP_Query, or bail.
}
try {
$results = celersearch()->search( [
'query' => 'winter jacket',
'index' => 'products',
'per_page' => 20,
'format' => 'view',
'facets' => [ 'taxonomies.product_cat' ],
'filters' => [
[ 'field' => 'in_stock', 'operator' => '=', 'value' => true ],
],
] );
} catch ( \CelerSearch\Exceptions\SearchApiException $e ) {
$results = [ 'hits' => [], 'total' => 0 ];
}
foreach ( $results['hits'] as $hit ) {
echo esc_html( $hit['title'] );
}
Everything is optional
Every argument has a default and unknown keys are ignored — passing an argument a future
version adds is always safe, and celersearch()->search() with no arguments at all is a valid
"browse the default index" call.
Arguments
| Key | Type | Default | Meaning |
|---|---|---|---|
query | string | '' | The search term. '' is a valid "browse" search that matches everything; pinned searches never match an empty query. |
index_id | int | 0 | Index record id. 0 falls back to the default index configured in Settings. |
index | string | '' | Index slug, an alternative to index_id. Only consulted when index_id is 0 or absent. A slug that matches nothing throws — it never silently falls back to the default. |
page | int | 1 | 1-based page number. Clamped to >= 1. |
per_page | int | 10 | Page size. Clamped to 1–100. |
context | string | 'frontend' | 'frontend' or 'admin'. Selects the index's status filter — 'admin' typically widens results to drafts, private and pending content. The caller owns the capability check (see the warning below). |
sort | string | '' | Sort key resolved by the index ('date', 'title-desc', index-specific keys such as 'price' on products). An unknown key is ignored and results stay in relevance order. |
filters | array | [] | Normalized filter conditions, applied on top of the context's own visibility filter. |
facets | string | [] | Attributes to compute facet counts for. Each must be filterable on the index. |
format | string | 'raw' | Hit shape: 'raw', 'autocomplete' or 'view'. |
highlight | null | bool | array | null | null uses the format's default, false disables it, an array overrides it. |
pinning | bool | true | false skips the curated phases and goes straight to the engine. |
analytics | bool | false | Record this search in Query Analytics. |
analytics_area | string | 'api' | The area label written to the analytics row. |
`context => 'admin'` does not check permissions
The API performs no current_user_can() test. Passing 'admin' widens results to
unpublished content, so an unauthenticated request that reaches it will leak drafts and
private posts. Gate it yourself in your endpoint's permission_callback (or wherever the
call lives) before you pass it in.
Filters
Filter conditions use CelerSearch's normalized, engine-independent format: a field, an
operator and a value.
'filters' => [
[ 'field' => 'in_stock', 'operator' => '=', 'value' => true ],
[ 'field' => 'taxonomies.product_cat', 'operator' => 'IN', 'value' => [ 'jackets', 'coats' ] ],
[ 'field' => 'price', 'operator' => '<=', 'value' => 200 ],
],
Operators: =, !=, IN, NOT IN, >, >=, <, <=.
Group conditions with relation + conditions, nesting as deep as you need:
'filters' => [
[
'relation' => 'OR',
'conditions' => [
[ 'field' => 'taxonomies.pa_color', 'operator' => '=', 'value' => 'blue' ],
[ 'field' => 'taxonomies.pa_color', 'operator' => '=', 'value' => 'red' ],
],
],
[ 'field' => 'in_stock', 'operator' => '=', 'value' => true ],
],
Conditions are passed to the engine untouched, so a field that isn't filterable on the index
surfaces as a SearchEngineException rather than being silently dropped.
Hit formats
format | Hits look like | Highlighting by default |
|---|---|---|
'raw' | The engine's hit arrays, untouched | Off |
'autocomplete' | The index's autocomplete shape, also passed through the celersearch_autocomplete_hit filter | title, content |
'view' | The index's view / result-item shape | The index's view highlight fields |
Hits the index declines to format — a record whose underlying post has since been deleted, for example — are dropped from the returned array.
Highlighting wraps matches in <em class="celersearch-highlight"> … </em> by default, the
same markup the built-in surfaces emit, so bundled CSS keeps working. Override any part of it:
'highlight' => [
'fields' => [ 'title' ],
'pre_tag' => '<mark>',
'post_tag' => '</mark>',
],
Pass false to turn it off, or true for "on, with the format's defaults".
Return value
search() always returns an array with all nine keys present:
[
'hits' => array, // shaped by `format`
'total' => int, // matches across all pages
'page' => int, // the page actually served
'per_page' => int, // after clamping
'total_pages' => int, // never below 1
'pinned' => bool, // a curated pin took part
'source' => string, // 'pinned' | 'boost' | 'organic'
'facets' => array, // attribute => [ value => count ]
'facet_stats' => array, // attribute => [ 'min' => …, 'max' => … ]
]
source tells you where the page came from: 'pinned' when an exact
Pinned Search replaced the
results outright, 'boost' when curated documents were pinned on top of engine results,
'organic' for a plain engine page.
facets and facet_stats are empty whenever no engine response backs the page — an exact pin
replaces the engine results, so there is nothing to count.
Error handling
Failures throw. There is no partial or empty-on-error payload to misread as "no results",
which means your fallback logic is explicit. Every failure extends
CelerSearch\Exceptions\SearchApiException, so one catch handles them all:
use CelerSearch\Exceptions\SearchApiException;
try {
$results = celersearch()->search( [ 'query' => $term ] );
} catch ( SearchApiException $e ) {
return my_native_search_fallback( $term );
}
Catch the subclasses when the reason changes what you do:
| Exception | Meaning |
|---|---|
SearchDisabledException | CelerSearch is switched off in Settings. Checked before any database or engine work happens — treat it as "do what you'd do without CelerSearch". |
SearchIndexNotFoundException | No index was given and no default is configured, the slug matched nothing, or the record could not be built (deleted, or its service is gone). The original failure is chained as $previous. |
SearchEngineException | The search itself failed — engine unreachable, a rejected filter, a sort on a non-sortable attribute. Configuration is fine, the request wasn't answered: retry or degrade. |
\InvalidArgumentException | An invalid context or format. Thrown before any work happens — this one is a bug in your code, not a runtime condition. |
Customising the engine parameters
celersearch_api_search_params fires for every engine call search() makes, letting you
adjust the parameters after your arguments were assembled.
add_filter( 'celersearch_api_search_params', function ( $params, $args, $index ) {
// Only touch calls from your own integration.
if ( 'headless-storefront' !== ( $args['analytics_area'] ?? '' ) ) {
return $params;
}
// Never surface clearance stock in the storefront's suggestions.
$params['filters'][] = [
'field' => 'taxonomies.product_cat',
'operator' => 'NOT IN',
'value' => [ 'clearance' ],
];
return $params;
}, 10, 3 );
| Parameter | Type | Description |
|---|---|---|
$params | array | The parameters for this engine call, in CelerSearch's normalized shape (filters, facets, sort, highlighting) — the engine syntax is generated afterwards. |
$args | array | Your arguments after defaults and clamping, with index_id resolved. |
$index | BaseIndex | The index being searched. |
One page can produce more than one engine call: the organic query, plus one call per organic
window when curated documents are boosted onto the page. The organic query uses hitsPerPage
page; boost windows uselimit+offset. Check those keys if a hook needs to treat the two differently.
Complete example: a custom autocomplete endpoint
The use case this API was built for — a bespoke storefront that renders its own suggestions and wants CelerSearch's results without adopting CelerSearch's front end.
<?php
use CelerSearch\Exceptions\SearchApiException;
use CelerSearch\Exceptions\SearchDisabledException;
add_action( 'rest_api_init', function () {
register_rest_route( 'my-storefront/v1', '/suggest', [
'methods' => 'GET',
'permission_callback' => '__return_true', // Public, frontend context only.
'args' => [
'q' => [
'type' => 'string',
'required' => true,
'sanitize_callback' => 'sanitize_text_field',
],
],
'callback' => 'my_storefront_suggest',
] );
} );
function my_storefront_suggest( WP_REST_Request $request ) {
if ( ! function_exists( 'celersearch' ) ) {
return new WP_REST_Response( [ 'suggestions' => [] ] );
}
try {
$results = celersearch()->search( [
'query' => $request->get_param( 'q' ),
'index' => 'products',
'per_page' => 6,
'format' => 'autocomplete',
'context' => 'frontend', // Never widen this on a public route.
'filters' => [
[ 'field' => 'in_stock', 'operator' => '=', 'value' => true ],
],
'analytics' => true,
'analytics_area' => 'headless-storefront', // Its own row in reports.
] );
} catch ( SearchDisabledException $e ) {
return new WP_REST_Response( [ 'suggestions' => [] ] );
} catch ( SearchApiException $e ) {
return new WP_Error( 'search_failed', 'Search is unavailable.', [ 'status' => 503 ] );
}
$suggestions = array_map( function ( $hit ) {
return [
'id' => $hit['id'],
'title' => $hit['title'],
'url' => $hit['url'],
'image' => $hit['thumbnail'],
];
}, $results['hits'] );
return new WP_REST_Response( [
'suggestions' => $suggestions,
'total' => $results['total'],
'curated' => $results['pinned'],
] );
}
Because it goes through the API, this endpoint inherits the merchandising rules configured in wp-admin, hides unpublished products, respects the index's own settings, and reports its searches under its own label — none of which it had to implement.
Give your integration its own analytics area
Set analytics_area to something recognisable instead of leaving it at api. Query Analytics
then separates your integration's searches from the rest of the site's, which is what makes
the report actionable. Analytics still honour the site's own opt-in and minimum-length
settings, and only page 1 of a non-empty query is ever recorded.
Stability
The celersearch() helper, the search() signature, the argument names, the returned keys
and the exception hierarchy are covered by semantic versioning: they will not change within a
major version. Everything else in the plugin — the classes this API is built on included — is
internal and may change in any release, so build against this surface rather than reaching
past it.
Next Steps
- Custom indices — make your own content searchable, then query it here.
- Search hooks — hook the plugin's own search paths.
- Autocomplete hooks — including
celersearch_autocomplete_hit, which theautocompleteformat applies. - Dynamic Search Rules — engine-side merchandising, and how it compares to the Pinned Searches this API applies for you.