Building Search Queries
SearchJet doesn't have a query-builder library. Instead, you build a search query from URL parameters on GET /api/v1/search, and you write filters in Meilisearch filter syntax. This page shows how to combine those parameters into precise queries, and how to wrap them in a small helper so your own code reads like a builder.
The parameters
| Parameter | What it does | Example |
|---|---|---|
q |
Search text (required) | q=api integration |
filters |
Meilisearch filter expression, at most 1,000 characters | filters=type = "post" AND price < 50 |
sort |
Up to 5 comma-separated attr:asc or attr:desc pairs |
sort=date:desc |
limit |
Results per request, 1 to 100 (default 20) | limit=10 |
page / offset |
Which slice of results to return. offset wins if you send both. |
page=2 |
attributes |
Fields to return, within your plan's field list | attributes=id,title,url |
attributesToHighlight |
Highlight matches in _formatted (Pro and above) |
attributesToHighlight=title,excerpt |
semantic, semanticRatio |
Hybrid semantic search (Pro and above) | semantic=true&semanticRatio=0.7 |
Send each request to https://app.searchjetengine.com/api/v1/search with Authorization: Bearer and Accept: application/json. See the REST API Reference for the full list.
What you can filter and sort on
- Free: filter on
id,title,type,price,currencyandsku. Sort on a single field:id,titleorprice. - Pro, Growth and Enterprise: filter and sort on any attribute you've marked filterable or sortable in Index Settings – General Attributes.
Anything else returns 400 INVALID_FILTER or 400 INVALID_SORT. For example:
{
"error": "Filter references an attribute that is not filterable. Add it to filterableAttributes in your index settings.",
"code": "INVALID_FILTER"
}
Filter syntax
Put string values in double quotes. Parentheses and quotes must be balanced, or the API returns INVALID_FILTER.
# Equality
type = "post"
# Numeric comparison
price < 50
price >= 10 AND price <= 100
# Either value
type = "post" OR type = "page"
# Grouping (paid plans: any filterable attributes)
type = "post" AND (tags = "wordpress" OR tags = "search")
# Exclude
NOT type = "archive"
Combining filters, sorting and limits
This query searches posts for "api integration", sorts them newest first, and returns 10 results. It sorts on date, which needs a paid plan with date marked sortable.
curl -G "https://app.searchjetengine.com/api/v1/search" \
-H "Authorization: Bearer $SEARCHJET_PUBLIC_KEY" \
-H "Accept: application/json" \
--data-urlencode "q=api integration" \
--data-urlencode 'filters=type = "post"' \
--data-urlencode "sort=date:desc" \
--data-urlencode "limit=10"
const params = new URLSearchParams({
q: 'api integration',
filters: 'type = "post"',
sort: 'date:desc',
limit: '10',
});
const res = await fetch(`https://app.searchjetengine.com/api/v1/search?${params}`, {
headers: { Authorization: 'Bearer YOUR_PUBLIC_KEY', Accept: 'application/json' },
});
const { results, total } = await res.json();
use Illuminate\Support\Facades\Http;
$hits = Http::withToken(config('services.searchjet.public_key'))
->acceptJson()
->get('https://app.searchjetengine.com/api/v1/search', [
'q' => 'api integration',
'filters' => 'type = "post"',
'sort' => 'date:desc',
'limit' => 10,
])
->throw()
->json('results');
Writing your own small query helper
If you build queries in many places, a few lines of your own code keep things tidy. You own this code, so change it however you need.
function buildSearchParams({ q, where = {}, sort = [], limit = 10, page = 1 }) {
const quote = (v) => (typeof v === 'number' ? v : `"${String(v).replace(/"/g, '\\"')}"`);
const clauses = Object.entries(where).map(([attr, value]) =>
Array.isArray(value)
? `(${value.map((v) => `${attr} = ${quote(v)}`).join(' OR ')})`
: `${attr} = ${quote(value)}`
);
const params = new URLSearchParams({ q, limit: String(limit), page: String(page) });
if (clauses.length) params.set('filters', clauses.join(' AND '));
if (sort.length) params.set('sort', sort.join(','));
return params;
}
// type is "post" OR "page"
const params = buildSearchParams({ q: 'search plugin', where: { type: ['post', 'page'] } });
function searchjetFilters(array $where): string
{
$quote = fn ($v) => is_numeric($v) ? $v : '"' . str_replace('"', '\"', $v) . '"';
return collect($where)->map(function ($value, $attr) use ($quote) {
if (is_array($value)) {
return '(' . collect($value)->map(fn ($v) => "$attr = " . $quote($v))->implode(' OR ') . ')';
}
return "$attr = " . $quote($value);
})->implode(' AND ');
}
$hits = Http::withToken(config('services.searchjet.public_key'))
->acceptJson()
->get('https://app.searchjetengine.com/api/v1/search', [
'q' => 'search plugin',
'filters' => searchjetFilters(['type' => ['post', 'page']]),
])
->throw()
->json('results');
Highlighting matches
On Pro and above, ask the API to highlight matches. Each hit then gets a _formatted object:
attributesToHighlight=title,excerpt&highlightPreTag=__sj__&highlightPostTag=__/sj__
Read hit._formatted.title instead of hit.title. The rest of the text in _formatted isn't HTML-escaped, so it's safest to use plain-text marker tags like the ones above. Escape the value, then replace the markers with and .
On Free, highlighting parameters are ignored and hits contain no _formatted. Highlight on the client instead: escape the text first, then wrap the matches:
function highlight(text, query) {
const escaped = String(text ?? '').replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]));
const terms = query.split(/\s+/).filter(Boolean).map((t) => t.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
if (!terms.length) return escaped;
return escaped.replace(new RegExp(`(${terms.join('|')})`, 'gi'), '$1');
}
Related pages
- Basic Usage: simple search, filters and pagination
- Search API Deep Dive: semantic search and AI answers
- Error Handling