Your first search
From an empty file to rendered results with a working facet, in JavaScript, Angular or React.
A search interface needs four things: a client, a session, a query, and something to render. This page builds them in that order, then shows the finished file in your language.
Before you start
You need a reachable server, an app name, and a query web service — verified in Prerequisites. If step 1 below fails, that page tells you which piece is missing.
Build it up
Create the client
The client owns the configuration, the session, and the typed API. Create one and keep it — do not build a new one per request, or each gets its own session.
import { createAtomicClient } from "@sinequa/atomic";
const client = createAtomicClient({
app: "<your-app>",
backendUrl: "https://<your-sinequa-server>",
});Nothing has happened on the network yet. Creating a client is inert.
Sign in
Two calls, in this order. The first reads the server's configuration and settles how to authenticate; the second does it.
await client.initializeConfig();
await client.auth.login();You do not choose the method: login() branches on what the server announced — a form, Windows SSO, a provider
redirect, or a configured token. In a credentials deployment it returns false and you show a form; see
Authentication.
Run a search
A query is a plain object. Only name is required — the query web service to run.
const result = await client.api.query.search({
name: "<your-query>",
text: "quarterly report",
pageSize: 10,
});The response carries the documents in records and the facet counts in aggregations. You do not fetch
facets separately.
Render, and check for failure first
records is an array of documents. Read $error before concluding anything from an empty one: a failed
request and a corpus that matched nothing both arrive with no records.
if (result.$error) throw result.$error;
for (const record of result.records) {
console.log(record.title, record.modified);
}title and modified are always present. Anything the administrator mapped into a custom column is not on the
type — read those with getMetadata, which normalises the several shapes such a value can take.
Add a facet
The counts are already in the response. Turning a click into a filter is the filter builder — and re-running the search with it is the whole interaction.
import { filter } from "@sinequa/atomic";
const formats = result.aggregations.find((a) => a.column === "docformat");
const refined = await client.api.query.search({
name: "<your-query>",
text: "quarterly report",
pageSize: 10,
filters: filter.eq("docformat", "pdf"),
});The whole thing
Complete and runnable. Substitute your app name and server URL.
import { createAtomicClient, filter, getMetadata } from "@sinequa/atomic";
import type { Result } from "@sinequa/atomic";
const client = createAtomicClient({
app: "<your-app>",
backendUrl: "https://<your-sinequa-server>",
});
async function main() {
await client.initializeConfig();
if (!(await client.auth.login())) {
// A credentials deployment: pass { username, password } instead.
throw new Error("Not signed in — see the Authentication guide.");
}
// First search: no filter, so the facets describe the whole result set.
const all = await search();
report(all);
// Then narrow it, reusing the same query with one filter added.
const pdfOnly = await search(filter.eq("docformat", "pdf"));
report(pdfOnly);
}
function search(filters?: ReturnType<typeof filter.eq>) {
return client.api.query.search({
name: "<your-query>",
text: "quarterly report",
pageSize: 10,
filters,
});
}
function report(result: Result) {
if (result.$error) throw result.$error;
console.log(`\n${result.rowCount} document(s)`);
for (const record of result.records) {
// Custom metadata is not on the Article type; getMetadata normalises it.
const authors = getMetadata(record, "authors");
console.log(`- ${record.title}${authors.length ? ` — ${authors.join(", ")}` : ""}`);
}
const formats = result.aggregations.find((a) => a.column === "docformat");
for (const item of formats?.items ?? []) {
console.log(` ${item.display ?? item.value}: ${item.count}`);
}
}
main().catch((error) => console.error(error));import { InjectionToken, inject, provideAppInitializer } from "@angular/core";
import { type AtomicClient, createAtomicClient } from "@sinequa/atomic";
export const SINEQUA = new InjectionToken<AtomicClient>("Sinequa client", {
factory: () =>
createAtomicClient({
app: "<your-app>",
backendUrl: "https://<your-sinequa-server>",
}),
});
/** Configuration and session resolved before the first view renders. */
export function provideSinequa() {
return provideAppInitializer(async () => {
const client = inject(SINEQUA);
await client.initializeConfig();
await client.auth.login();
});
}import type { ApplicationConfig } from "@angular/core";
import { provideSinequa } from "./sinequa";
export const appConfig: ApplicationConfig = {
providers: [provideSinequa()],
};import { ChangeDetectionStrategy, Component, inject, resource, signal } from "@angular/core";
import { type AggregationItem, type FilterValue, filter, getMetadata } from "@sinequa/atomic";
import { SINEQUA } from "./sinequa";
@Component({
selector: "app-search",
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<input [value]="text()" (input)="text.set($any($event.target).value)" placeholder="Search…" />
@if (results.isLoading()) {
<p>Searching…</p>
} @else if (results.error()) {
<p>Search failed: {{ results.error()?.message }}</p>
} @else if (results.value(); as result) {
<p>{{ result.rowCount }} document(s)</p>
<!-- Facet counts arrive with the results; clicking one re-runs the search. -->
<ul>
@for (item of formats(); track item.value) {
<li>
<button type="button" (click)="toggleFormat(item)">
{{ item.display ?? item.value }} ({{ item.count }})
</button>
</li>
}
</ul>
<ol>
@for (record of result.records; track record.id) {
<li>{{ record.title }} — {{ authorsOf(record).join(', ') }}</li>
}
</ol>
}
`,
})
export class SearchComponent {
private readonly client = inject(SINEQUA);
readonly text = signal("quarterly report");
/**
* `FilterValue`, not `string`: a facet value can be a number or a boolean —
* a size bucket, a flag — and typing it as a string quietly excludes those.
*/
readonly format = signal<FilterValue | undefined>(undefined);
readonly results = resource({
params: () => ({ text: this.text(), format: this.format() }),
loader: ({ params }) =>
this.client.api.query.search({
name: "<your-query>",
text: params.text,
pageSize: 10,
// Test against `undefined`, not truthiness: `0`, `false` and `""` are
// valid facet values, and `params.format && …` would drop all three.
filters: filter.and(
params.format === undefined ? undefined : filter.eq("docformat", params.format),
),
}),
});
readonly formats = () =>
this.results.value()?.aggregations.find((a) => a.column === "docformat")?.items ?? [];
/** An item's value can be null; the selection uses undefined for "none". */
toggleFormat(item: AggregationItem) {
const value = item.value ?? undefined;
this.format.set(value === this.format() ? undefined : value);
}
authorsOf(record: unknown) {
return getMetadata(record as Parameters<typeof getMetadata>[0], "authors");
}
}import { createAtomicClient } from "@sinequa/atomic";
export const sinequa = createAtomicClient({
app: "<your-app>",
backendUrl: "https://<your-sinequa-server>",
});
/** One sign-in shared by every component that queries. */
export const ready = (async () => {
await sinequa.initializeConfig();
await sinequa.auth.login();
})();import { filter, getMetadata } from "@sinequa/atomic";
import type { AggregationItem, FilterValue, Result } from "@sinequa/atomic";
import { useEffect, useState } from "react";
import { ready, sinequa } from "./sinequa";
export function Search() {
const [text, setText] = useState("quarterly report");
// FilterValue, not string: a facet value can be a number or a boolean.
const [format, setFormat] = useState<FilterValue>();
const [result, setResult] = useState<Result>();
const [error, setError] = useState<Error>();
useEffect(() => {
let active = true;
setError(undefined);
ready
.then(() =>
sinequa.api.query.search({
name: "<your-query>",
text,
pageSize: 10,
// Test against `undefined`, not truthiness: `0`, `false` and `""` are
// valid facet values, and `format && …` would drop all three.
filters: filter.and(
format === undefined ? undefined : filter.eq("docformat", format),
),
}),
)
.then((next) => active && setResult(next))
.catch((cause: unknown) => active && setError(cause as Error));
// The query can change before the previous one answers; ignore the stale reply.
return () => {
active = false;
};
}, [text, format]);
/** An item's value can be null; the selection uses undefined for "none". */
const toggleFormat = (item: AggregationItem) => {
const value = item.value ?? undefined;
setFormat(value === format ? undefined : value);
};
if (error) return <p>Search failed: {error.message}</p>;
if (!result) return <p>Searching…</p>;
const formats = result.aggregations.find((a) => a.column === "docformat");
return (
<>
<input value={text} onChange={(event) => setText(event.target.value)} placeholder="Search…" />
<p>{result.rowCount} document(s)</p>
{/* Facet counts arrive with the results; clicking one re-runs the search. */}
<ul>
{formats?.items.map((item) => (
<li key={String(item.value)}>
<button
type="button"
/* An item's value can be null; the selection uses undefined for "none". */
onClick={() => toggleFormat(item)}
>
{item.display ?? String(item.value)} ({item.count})
</button>
</li>
))}
</ul>
<ol>
{result.records.map((record) => (
<li key={record.id}>
{record.title} — {getMetadata(record, "authors").join(", ")}
</li>
))}
</ol>
</>
);
}Pitfalls
What's next
Text and paging
Page through the results, and read pagination back correctly.
Filtering results
Turn facet clicks into filters, including the date and size buckets.
Aggregations
Tree facets, loading more items, and what a distribution really is.
Authentication
Every mode, and which one your deployment uses.