Skip to main content

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​

StatusMeaningClient action
200Search completedRender hits, facets, and pagination metadata
400Invalid or unsupported inputCorrect the mapped field, operator, or request shape
401Missing, invalid, or revoked keyReplace the configured public search key
429Rate limit reachedRespect 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.