Tabs, scopes and sorting
The three selectors the server configures for you — what each one restricts, and why their options should never be hardcoded.
Three properties of a query, three server-configured lists of options, and one rule: read the options from the configuration rather than writing them in your code. An administrator changes them, and an interface that hardcoded a list quietly stops matching the deployment.
| Property | Restricts | Options come from |
|---|---|---|
tab | A top-level slice of the corpus, with its own count | CCQuery.tabSearch.tabs |
scope | A server-defined subset the user switches between | CCQuery.scopes |
sort | The ordering — by name, not by clause | CCQuery.sortingChoices |
Tabs
Concept — a tab
A tab is a distribution presented as navigation: "All (1 240)", "Documents (830)", "People (78)". The server counts each one, so the numbers come back with the results — you do not run a query per tab.
result.tabs carries each tab with its count; result.tab is the one that was applied.
import { createAtomicClient, sysLang } from "@sinequa/atomic";
import type { Result } from "@sinequa/atomic";
const client = createAtomicClient({
app: "<your-app>",
backendUrl: "https://<your-sinequa-server>",
});
await client.initializeConfig();
await client.auth.login();
const state = { text: "report", tab: undefined as string | undefined, page: 1 };
export async function search(): Promise<Result> {
return client.api.query.search({
name: "<your-query>",
text: state.text,
tab: state.tab,
page: state.page,
pageSize: 10,
});
}
export async function selectTab(value: string) {
state.tab = value;
// A different tab is a different result set: page 7 of it may not exist.
state.page = 1;
return search();
}
const result = await search();
for (const tab of result.tabs ?? []) {
const selected = tab.value === result.tab;
// Labels are multilingual strings — decode before displaying.
console.log(`${selected ? ">" : " "} ${sysLang(tab.display, "en-US")} (${tab.count})`);
}import { ChangeDetectionStrategy, Component, inject, resource, signal } from "@angular/core";
import { sysLang } from "@sinequa/atomic";
import { SINEQUA } from "./sinequa";
@Component({
selector: "app-tabs",
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
@if (results.value(); as result) {
<nav>
@for (tab of result.tabs ?? []; track tab.value) {
<button
type="button"
[attr.aria-current]="tab.value === result.tab ? 'page' : null"
(click)="select(tab.value)"
>
{{ label(tab.display) }} ({{ tab.count }})
</button>
}
</nav>
<p>{{ result.rowCount }} document(s)</p>
}
`,
})
export class TabsComponent {
private readonly client = inject(SINEQUA);
readonly text = signal("report");
readonly tab = signal<string | undefined>(undefined);
readonly page = signal(1);
readonly results = resource({
params: () => ({ text: this.text(), tab: this.tab(), page: this.page() }),
loader: ({ params }) =>
this.client.api.query.search({
name: "<your-query>",
text: params.text,
tab: params.tab,
page: params.page,
pageSize: 10,
}),
});
/** A different tab is a different result set: reset the page. */
select(value: string) {
this.page.set(1);
this.tab.set(value);
}
label(display: string) {
return sysLang(display, navigator.language);
}
}import { sysLang } from "@sinequa/atomic";
import type { Result } from "@sinequa/atomic";
import { useEffect, useState } from "react";
import { ready, sinequa } from "./sinequa";
export function Tabs() {
const [tab, setTab] = useState<string>();
const [page, setPage] = useState(1);
const [result, setResult] = useState<Result>();
useEffect(() => {
let active = true;
ready
.then(() =>
sinequa.api.query.search({
name: "<your-query>",
text: "report",
tab,
page,
pageSize: 10,
}),
)
.then((next) => active && setResult(next));
return () => {
active = false;
};
}, [tab, page]);
/** A different tab is a different result set: reset the page. */
function select(value: string) {
setPage(1);
setTab(value);
}
if (!result) return <p>Searching…</p>;
return (
<>
<nav>
{(result.tabs ?? []).map((entry) => (
<button
key={entry.value}
type="button"
aria-current={entry.value === result.tab ? "page" : undefined}
onClick={() => select(entry.value)}
>
{sysLang(entry.display, navigator.language)} ({entry.count})
</button>
))}
</nav>
<p>{result.rowCount} document(s)</p>
</>
);
}What the tab configuration decides
CCQuery.tabSearch holds more than the list, and two of its keys change what you receive:
Prop
Type
A CCTab also carries excludedAggregations and excludedIndices: selecting that tab drops those facets from
the response and those indexes from the search. So a facet disappearing when a tab is selected is
configuration, not a bug.
Scopes
A scope is a restriction the administrator defined and named — a subset of the corpus the user can switch
between, without expressing it as a filter. Options come from CCQuery.scopes, each with name, an optional
display, and isDefault.
const result = await client.api.query.search({
name: "<your-query>",
text: "report",
scope: "internal-only", // a CCScope name
});result.scope reports the one applied. Use scopes for restrictions the server owns, and
filters for everything the user builds.
Sorting
sort takes the name of a sorting choice, not an ordering clause:
const app = await client.api.app.get();
const choices = app.queries["<your-query>"].sortingChoices ?? [];
// ["relevance", "date", "title"] — whatever the administrator configured
console.log(choices.map((c) => c.name));
const result = await client.api.query.search({
name: "<your-query>",
text: "report",
sort: "date",
});Which choice is the default depends on the query
Two flags on CCSortingChoice, and the one that applies is decided by whether the query had text to rank by:
| Flag | Applies when |
|---|---|
isDefaultWithRelevance | The query had search text — result.hasRelevance is true |
isDefaultNoRelevance | It did not: a corpus listing has no relevance to sort on |
So a "sort by relevance" option must be hidden, or disabled, when hasRelevance is false — there is nothing
to sort by:
const available = choices.filter(
(choice) => result.hasRelevance || choice.name !== "relevance",
);
const currentDefault = choices.find((choice) =>
result.hasRelevance ? choice.isDefaultWithRelevance : choice.isDefaultNoRelevance,
);