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.

PHP
$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 jobThis API
Add a search box to a page without writing codeViews and the [celersearch] shortcode
Replace an existing WordPress search screenSearch Areas
Faceted filtering on a WooCommerce shop pageShop 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.

PHP
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

KeyTypeDefaultMeaning
querystring''The search term. '' is a valid "browse" search that matches everything; pinned searches never match an empty query.
index_idint0Index record id. 0 falls back to the default index configured in Settings.
indexstring''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.
pageint11-based page number. Clamped to >= 1.
per_pageint10Page size. Clamped to 1–100.
contextstring'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).
sortstring''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.
filtersarray[]Normalized filter conditions, applied on top of the context's own visibility filter.
facetsstring[]Attributes to compute facet counts for. Each must be filterable on the index.
formatstring'raw'Hit shape: 'raw', 'autocomplete' or 'view'.
highlightnull | bool | arraynullnull uses the format's default, false disables it, an array overrides it.
pinningbooltruefalse skips the curated phases and goes straight to the engine.
analyticsboolfalseRecord this search in Query Analytics.
analytics_areastring'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.

PHP
'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:

PHP
'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

formatHits look likeHighlighting by default
'raw'The engine's hit arrays, untouchedOff
'autocomplete'The index's autocomplete shape, also passed through the celersearch_autocomplete_hit filtertitle, content
'view'The index's view / result-item shapeThe 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:

PHP
'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:

PHP
[
    '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:

functions.php
PHP
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:

ExceptionMeaning
SearchDisabledExceptionCelerSearch is switched off in Settings. Checked before any database or engine work happens — treat it as "do what you'd do without CelerSearch".
SearchIndexNotFoundExceptionNo 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.
SearchEngineExceptionThe 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.
\InvalidArgumentExceptionAn 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.

PHP
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 );
ParameterTypeDescription
$paramsarrayThe parameters for this engine call, in CelerSearch's normalized shape (filters, facets, sort, highlighting) — the engine syntax is generated afterwards.
$argsarrayYour arguments after defaults and clamping, with index_id resolved.
$indexBaseIndexThe 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 use limit + 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.

class-storefront-search-endpoint.php
PHP
<?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 the autocomplete format applies.
  • Dynamic Search Rules — engine-side merchandising, and how it compares to the Pinned Searches this API applies for you.