Search & pagination
This recipe shows how to discover the query web service name from the app configuration, run a search with fetchQuery(), and page through the results.
1. Discover the query name with fetchApp()β
fetchQuery needs a name (the query web service configured on the app). Don't hard-code it β read it from the app config.
fetchApp()β aCCAppobject.CCApp.queriesis aRecord<string, CCQuery>(key = query name).CCApp.defaultQueryNamegives the default query.
import { fetchApp } from '@sinequa/atomic';
export async function resolveQueryName() {
const app = await fetchApp();
const names = Object.keys(app.queries ?? {});
return app.defaultQueryName || names[0] || '';
}
2. Run a search with fetchQuery()β
fetchQuery(query) posts to /api/v1/query and returns a Result:
result.recordsβ the documents (Article[])result.page,result.pageSize,result.rowCountβ the pagination state
import { fetchQuery } from '@sinequa/atomic';
export async function search(name, text, page = 1, pageSize = 10) {
const result = await fetchQuery({ name, text, page, pageSize });
console.log(`${result.rowCount} hits β showing ${result.records.length}`);
return result;
}
The server may clamp the requested pageSize. Always compute the page math from result.page / result.pageSize / result.rowCount (the values the server actually used), so the UI stays consistent.
Paging helpersβ
Results are enriched with nextPage / previousPage (the page number, or undefined at the bounds):
const result = await fetchQuery({ name, text, page: 2, pageSize: 10 });
result.previousPage; // 1
result.nextPage; // 3 (or undefined on the last page)
const totalPages = Math.max(1, Math.ceil(result.rowCount / result.pageSize));
3. Bulk queriesβ
fetchBulkQuery() runs several queries in parallel β handy for prefetching adjacent pages or running the same text against different query services.
import { fetchBulkQuery } from '@sinequa/atomic';
const base = { name: '_query', text: 'tesla', pageSize: 10 };
const [page1, page2] = await fetchBulkQuery([
{ ...base, page: 1 },
{ ...base, page: 2 },
]);
React: a useSearch hook + results componentβ
The hook owns the request state and exposes a single run(text, page, size) entry point. Keeping the searched text separate from the input text means paging doesn't break when the user edits the box.
import { useCallback, useEffect, useState } from 'react';
import { fetchApp, fetchQuery } from '@sinequa/atomic';
export function useSearch() {
const [queryName, setQueryName] = useState('');
const [result, setResult] = useState(null);
const [searchedText, setSearchedText] = useState('');
const [pageSize, setPageSize] = useState(10);
const [busy, setBusy] = useState(false);
const [error, setError] = useState(null);
// Resolve the query web service name once.
useEffect(() => {
let cancelled = false;
fetchApp()
.then((app) => {
if (cancelled) return;
const names = Object.keys(app.queries ?? {});
setQueryName(app.defaultQueryName || names[0] || '');
})
.catch(() => !cancelled && setError("Couldn't load the app configuration."));
return () => {
cancelled = true;
};
}, []);
const run = useCallback(
async (text, page = 1, size = pageSize) => {
if (!text.trim() || !queryName) return;
setBusy(true);
setError(null);
try {
const res = await fetchQuery({ name: queryName, text, page, pageSize: size });
setResult(res);
setSearchedText(text);
setPageSize(size);
} catch (err) {
setError(err instanceof Error ? err.message : 'Search failed.');
setResult(null);
} finally {
setBusy(false);
}
},
[queryName, pageSize],
);
return { queryName, result, searchedText, pageSize, busy, error, run, setPageSize };
}
import { useState } from 'react';
import { useSearch } from './use-search';
export function SearchPage() {
const { result, searchedText, pageSize, busy, error, run } = useSearch();
const [text, setText] = useState('');
const totalPages = result
? Math.max(1, Math.ceil(result.rowCount / result.pageSize))
: 1;
return (
<div>
<form onSubmit={(e) => { e.preventDefault(); run(text, 1); }}>
<input value={text} onChange={(e) => setText(e.target.value)} placeholder="Searchβ¦" />
<button type="submit" disabled={busy}>{busy ? 'Searchingβ¦' : 'Search'}</button>
<select
value={pageSize}
onChange={(e) => run(searchedText || text, 1, Number(e.target.value))}
>
{[10, 20, 50, 100].map((n) => <option key={n} value={n}>{n} / page</option>)}
</select>
</form>
{error && <p role="alert">{error}</p>}
{result && (
<>
<p>{result.rowCount} result(s)</p>
<ul>
{result.records.map((record) => (
<li key={record.id}>
<a href={record.originalUrl} target="_blank" rel="noreferrer">{record.title}</a>
{record.relevantExtracts && (
// The backend returns <b>β¦</b> highlight markup. Sanitize if your sources aren't trusted.
<p dangerouslySetInnerHTML={{ __html: record.relevantExtracts }} />
)}
</li>
))}
</ul>
{result.rowCount > result.pageSize && (
<div>
<button disabled={busy || result.page <= 1} onClick={() => run(searchedText, result.page - 1)}>
β Previous
</button>
<span>Page {result.page} / {totalPages}</span>
<button disabled={busy || result.page >= totalPages} onClick={() => run(searchedText, result.page + 1)}>
Next β
</button>
</div>
)}
</>
)}
</div>
);
}
See alsoβ
- Facets & filters β refine the same query with aggregations, tabs, sort and scope.
- Document preview β open a result in a preview panel.
- Session & token management β wrap these calls so an expired session falls back to login.