TypeScript, React, and PHP clients
These examples call the same constrained endpoint used by Indexlume's tested clients:
POST /api/v1/search/v1/:projectPublicId/:indexSlug
Create a key under Settings → API keys and copy it when shown. Browser-side search keys are revocable public credentials: keep their scope read-only, configure a rate limit, and never substitute a Typesense administrative key.
TypeScript client
The Indexlume source tree includes the typed @osc/ts-client workspace package. Build or consume that package from the release source before importing it; version 0.1.0 is not documented here as a public npm-registry package.
import {SearchClient, SearchClientError} from '@osc/ts-client';
type Product = {
id: string;
name: string;
description: string;
price_cents: number;
category_name: string;
stock_quantity: number;
};
const client = new SearchClient({
baseUrl: 'https://search.example.com',
projectPublicId: '<public-project-id>',
searchKey: '<search-key>',
});
try {
const result = await client.search<Product>('products', {
q: 'keyboard',
filter: [{field: 'stock_quantity', operator: 'gt', value: 0}],
sort: [{field: 'price_cents', direction: 'asc'}],
facets: ['category_name'],
page: 1,
perPage: 10,
});
console.log(result.found, result.hits);
} catch (error) {
if (error instanceof SearchClientError) {
console.error(error.status, error.message, error.requestId);
}
}
The client only targets Indexlume's public proxy and supports an AbortSignal as the third search argument.
React search component
This example uses the browser fetch API, so it does not assume the workspace client has been published to a package registry.
import {FormEvent, useState} from 'react';
type Product = {
id: string;
name: string;
price_cents: number;
category_name: string;
};
type SearchResponse = {
hits: Array<{id: string; document: Product; highlights: unknown}>;
found: number;
page: number;
perPage: number;
requestMs: number;
};
export function ProductSearch() {
const [query, setQuery] = useState('');
const [result, setResult] = useState<SearchResponse | null>(null);
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
async function search(event?: FormEvent) {
event?.preventDefault();
setLoading(true);
setError('');
try {
const response = await fetch(
`${import.meta.env.VITE_INDEXLUME_URL}/api/v1/search/v1/` +
`${import.meta.env.VITE_INDEXLUME_PROJECT_ID}/products`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${import.meta.env.VITE_INDEXLUME_SEARCH_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({q: query, page: 1, perPage: 10}),
},
);
const payload = await response.json();
if (!response.ok) {
throw new Error(payload.error?.message ?? `Search failed (${response.status})`);
}
setResult(payload);
} catch (caught) {
setError(caught instanceof Error ? caught.message : 'Search failed');
} finally {
setLoading(false);
}
}
return (
<section aria-labelledby="product-search-title">
<h2 id="product-search-title">Search products</h2>
<form onSubmit={search}>
<label htmlFor="product-query">Product name</label>
<input
id="product-query"
value={query}
onChange={(event) => setQuery(event.target.value)}
/>
<button disabled={loading}>{loading ? 'Searching…' : 'Search'}</button>
</form>
{error && <p role="alert">{error}</p>}
{result && <p aria-live="polite">{result.found} products found</p>}
<ul>
{result?.hits.map(({id, document}) => (
<li key={id}>
{document.name} — ${(document.price_cents / 100).toFixed(2)}
</li>
))}
</ul>
</section>
);
}
VITE_ values are bundled into browser code. The search key is therefore visible to visitors by design; it must remain read-only, rate-limited, and revocable. Keep administrative credentials server-side.
Dependency-free PHP
The release includes examples/php-public-search/index.php. Run it with environment variables rather than embedding credentials:
OSC_BASE_URL=https://search.example.com \
OSC_PROJECT_PUBLIC_ID=<public-project-id> \
OSC_SEARCH_KEY=<search-key> \
php examples/php-public-search/index.php products keyboard
The script URL-encodes the project and index identifiers, sends JSON through cURL, enforces a ten-second timeout, rejects non-2xx responses, and prints the decoded result as formatted JSON.
Handle expected responses
| Status | Meaning | Client action |
|---|---|---|
200 | Search completed | Render hits, facets, and pagination metadata |
400 | Invalid or unsupported input | Correct the mapped field, operator, or request shape |
401 | Missing, invalid, or revoked key | Replace the configured public search key |
429 | Rate limit reached | Respect Retry-After; do not retry in a tight loop |
Keep perPage at or below 100 and the result window at or below 10,000 records. Filter, sort, and facet fields must have the matching capability in the saved collection mapping.