Text and paging
The search box and the pager — with the pagination fields the library computes for you, and the arithmetic not to write.
A search box and a pager. The only thing that needs care is reading pagination back, because two of the useful fields are computed by the library rather than sent by the server.
A search box with a pager
import { createAtomicClient } from "@sinequa/atomic";
import type { Query, Result, ResultPages } from "@sinequa/atomic";
const client = createAtomicClient({
app: "<your-app>",
backendUrl: "https://<your-sinequa-server>",
});
await client.initializeConfig();
await client.auth.login();
const state = { text: "", page: 1 };
export async function search(text: string) {
// A new search always starts at page 1: keeping the page would show an empty
// page 7 of a result set that now has two.
state.text = text;
state.page = 1;
return run();
}
export async function goToPage(page: number) {
state.page = page;
return run();
}
async function run() {
const query: Query = {
name: "<your-query>",
text: state.text,
page: state.page,
pageSize: 10,
};
// nextPage and previousPage are computed by the library and declared in the
// return type, so an annotation is enough.
const result: Result & ResultPages = await client.api.query.search(query);
if (result.$error) throw result.$error;
return result;
}import { Injectable, computed, inject, resource, signal } from "@angular/core";
import { SINEQUA } from "./sinequa";
@Injectable({ providedIn: "root" })
export class SearchService {
private readonly client = inject(SINEQUA);
readonly text = signal("");
readonly page = signal(1);
readonly results = resource({
params: () => ({ text: this.text(), page: this.page() }),
loader: ({ params }) =>
this.client.api.query.search({
name: "<your-query>",
text: params.text,
page: params.page,
pageSize: 10,
}),
});
readonly total = computed(() => this.results.value()?.rowCount ?? 0);
/** A new search resets the page: page 7 of a two-page result set is empty. */
searchFor(text: string) {
this.page.set(1);
this.text.set(text);
}
}import { ChangeDetectionStrategy, Component, inject } from "@angular/core";
import { SearchService } from "./search.service";
@Component({
selector: "app-search",
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<input
[value]="search.text()"
(change)="search.searchFor(value($event))"
placeholder="Search…"
aria-label="Search"
/>
@if (search.results.isLoading()) {
<p>Searching…</p>
} @else if (search.results.value(); as result) {
<p>{{ search.total() }} document(s)</p>
<ol [attr.start]="(result.page - 1) * result.pageSize + 1">
@for (record of result.records; track record.id) {
<li>{{ record.title }}</li>
}
</ol>
<!-- undefined at the ends, so a falsy check is the whole guard -->
<button type="button" [disabled]="!result.previousPage" (click)="go(result.previousPage)">
Previous
</button>
<button type="button" [disabled]="!result.nextPage" (click)="go(result.nextPage)">Next</button>
}
`,
})
export class SearchComponent {
readonly search = inject(SearchService);
go(page: number | undefined) {
if (page) this.search.page.set(page);
}
value(event: Event) {
return (event.target as HTMLInputElement).value;
}
}import type { Result, ResultPages } from "@sinequa/atomic";
import { useEffect, useState } from "react";
import { ready, sinequa } from "./sinequa";
export function SearchUI() {
const [text, setText] = useState("");
const [page, setPage] = useState(1);
const [result, setResult] = useState<Result & ResultPages>();
useEffect(() => {
let active = true;
ready
.then(
() =>
sinequa.api.query.search({
name: "<your-query>",
text,
page,
pageSize: 10,
}),
)
.then((next) => active && setResult(next));
return () => {
active = false;
};
}, [text, page]);
/** A new search resets the page: page 7 of a two-page result set is empty. */
function searchFor(next: string) {
setPage(1);
setText(next);
}
if (!result) return <p>Searching…</p>;
return (
<>
<input
defaultValue={text}
onBlur={(event) => searchFor(event.target.value)}
placeholder="Search…"
aria-label="Search"
/>
<p>{result.rowCount} document(s)</p>
<ol start={(result.page - 1) * result.pageSize + 1}>
{result.records.map((record) => (
<li key={record.id}>{record.title}</li>
))}
</ol>
{/* undefined at the ends, so a falsy check is the whole guard */}
<button type="button" disabled={!result.previousPage} onClick={() => setPage(result.previousPage!)}>
Previous
</button>
<button type="button" disabled={!result.nextPage} onClick={() => setPage(result.nextPage!)}>
Next
</button>
</>
);
}The pagination fields
Prop
Type
nextPage and previousPage come from resultPages(), which the library applies to every search response. It
is exported, so you can apply it yourself to a result you obtained another way:
import { resultPages } from "@sinequa/atomic";
const { nextPage, previousPage } = resultPages(result);Why this is not left to the caller
Math.ceil(undefined / undefined) is NaN, and every comparison against NaN is false — so a response with
no pagination read exactly like one on its last page. Both are now reported as undefined explicitly,
which a falsy check handles correctly.
Accumulating pages instead of replacing them
For an infinite scroll, keep the pages you already have. Each record carries $page, so you can tell where it
came from:
const next = await client.api.query.search({ ...base, page });
// $page is set by the library on every record.
records.push(...next.records);