Search Service
8.10 SearchService — Rank a list against a query
Runs in: both worker and view.
Permission required: None. You supply your own already-known items and get back an ordering — no host data is read, nothing is persisted, no cross-extension exposure.
SearchService ranks an arbitrary list against a query using the same tiered fuzzy ranker the launcher's own search uses (Rust search_engine::ranker, skim fuzzy matching). Use it for in-view lists instead of shipping your own fuzzy-match library.
export interface RankableItem {
id: string;
title: string;
subtitle?: string;
keywords?: string[];
}
export interface ISearchService {
/**
* Rank `items` against `query`. Returns matching ids, best match first.
* Items with no match in any field are omitted. An empty/whitespace query
* returns every id in input order.
*/
rank(query: string, items: RankableItem[]): Promise<string[]>;
}
Usage:
import type { ISearchService, RankableItem } from 'asyar-sdk/contracts';
const search = context.getService<ISearchService>('search');
interface Todo {
id: string;
text: string;
notes?: string;
tags?: string[];
}
function rankTodos(query: string, todos: Todo[]): Promise<Todo[]> {
const items: RankableItem[] = todos.map((t) => ({
id: t.id,
title: t.text,
subtitle: t.notes,
keywords: t.tags,
}));
return search.rank(query, items).then((orderedIds) => {
const byId = new Map(todos.map((t) => [t.id, t]));
return orderedIds.map((id) => byId.get(id)!);
});
}
How ranking works:
- Exact title match — the query equals the title (case-insensitive).
- Title prefix — the title starts with the query.
- Title fuzzy — skim fuzzy match against the title (typo-tolerant subsequence matching, e.g.
"qrtly"matches"Quarterly Report"). - Subtitle or keyword — fuzzy match against
subtitle+keywordsjoined, when the title doesn't match at all. - Items matching nowhere are omitted from the result —
rank()does not return non-matches.
Within each tier, results are ordered by fuzzy score, then alphabetically by title as a tiebreaker.
id is opaque to the host: it's returned verbatim, best match first, so you map ordered ids back to your own objects — the host never sees or needs your full item shape, only title/subtitle/keywords.
Empty query: an empty or whitespace-only query returns every id in input order — no ranking pass runs, so it's safe to call on every keystroke including when the search box is cleared.
Scale limits. rank() is stateless — there is no persistent index, and your full items array is sent over IPC on every call. Fine for keystroke-by-keystroke filtering of up to a few hundred items. Not a fit for thousands+.
SearchService.rank() |
Indexed search (e.g. clipboard history) | |
|---|---|---|
| State | Stateless — no index | Persistent index, built once |
| Per-call cost | Full item list sent every call | Query only; index already in Rust |
| Good for | A few hundred items (your own snippets, an extension catalog) | Thousands+ items |
| Available to extensions | Yes — this service | No — internal to the clipboard feature, not exposed via the SDK |
If your list can grow into the thousands, don't pass a larger payload to
rank()— that doesn't scale. Build a dedicated indexed-search command instead, the same way clipboard history's full-text search works internally.