first commit
This commit is contained in:
137
docs/api-stack/api/api-plugin-search/src/service/service.md
Normal file
137
docs/api-stack/api/api-plugin-search/src/service/service.md
Normal file
@@ -0,0 +1,137 @@
|
||||
# service/service
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
The service module is the in-memory search engine that powers
|
||||
`@suaveplan/api-plugin-search`. It owns index creation and deletion,
|
||||
per-document analysis and storage, TF-weighted scoring, scalar/array
|
||||
filtering, multi-key sorting, facet aggregation, optional highlighting,
|
||||
and the prefix-based suggest pass. Every route handler in the package
|
||||
delegates to the functions here, every code path that fails throws a
|
||||
typed `SearchError(SEARCH.*)`, and every consumer-visible operation
|
||||
emits to the OTel meter bundle so dashboards reflect actual hot-path
|
||||
behaviour.
|
||||
|
||||
## Features
|
||||
|
||||
- Synchronous, dependency-free engine — `Map<string, SearchIndex>` is
|
||||
the entire backing store.
|
||||
- TF × field-weight scoring loop with deterministic ordering across
|
||||
identical scores (relies on `Map` insertion order).
|
||||
- Filter pass supporting scalar equality (`actual === expected`) and
|
||||
array membership (`expected.includes(actual)`) so a single Zod
|
||||
schema accepts both shapes.
|
||||
- Five-way sort (`relevance`, `date_asc`, `date_desc`, `alpha_asc`,
|
||||
`alpha_desc`) using `localeCompare` for stable cross-locale ordering.
|
||||
- Facet aggregation restricted to fields with `facet: true`; counts are
|
||||
built over the filtered (not raw) document set.
|
||||
- Optional highlighting wraps matching whole words in the
|
||||
caller-supplied tag (`<mark>` by default).
|
||||
- Prefix-based suggest derives candidates from the last analysed token
|
||||
and ranks by occurrence count.
|
||||
- Three hard caps (`maxIndexes`, `maxDocumentsPerIndex`) enforced with
|
||||
typed errors that map to HTTP 400 via `context.statusCode`.
|
||||
- Five OTel instruments wired into every call: `queriesTotal`,
|
||||
`queryDurationMs`, `documentsIndexedTotal`, `indexesTotal`,
|
||||
`suggestRequestsTotal`.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import {
|
||||
createSearchIndex,
|
||||
indexDocument,
|
||||
search,
|
||||
} from "@suaveplan/api-plugin-search";
|
||||
|
||||
const indexes = new Map();
|
||||
createSearchIndex(
|
||||
{ name: "posts", fields: [{ name: "title", weight: 2, facet: false }] },
|
||||
indexes
|
||||
);
|
||||
indexDocument(
|
||||
"posts",
|
||||
{ id: "1", fields: { title: "Hello world" } },
|
||||
indexes
|
||||
);
|
||||
const result = search(
|
||||
{
|
||||
query: "hello",
|
||||
index: "posts",
|
||||
sort: "relevance",
|
||||
limit: 20,
|
||||
offset: 0,
|
||||
highlight: false,
|
||||
highlightTag: "mark",
|
||||
},
|
||||
indexes
|
||||
);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Bulk-index 500 documents, search with a facet + filter, and observe
|
||||
the emitted OTel metrics:
|
||||
|
||||
```ts
|
||||
import {
|
||||
bulkIndexDocuments,
|
||||
search,
|
||||
} from "@suaveplan/api-plugin-search";
|
||||
|
||||
bulkIndexDocuments(
|
||||
"posts",
|
||||
docs,
|
||||
indexes,
|
||||
{ maxIndexes: 100, maxDocumentsPerIndex: 1_000, defaultAnalyzer: "standard" }
|
||||
);
|
||||
|
||||
const result = search(
|
||||
{
|
||||
query: "search",
|
||||
index: "posts",
|
||||
filters: { tag: ["blog", "release"] },
|
||||
facets: ["tag"],
|
||||
sort: "relevance",
|
||||
limit: 20,
|
||||
offset: 0,
|
||||
highlight: true,
|
||||
highlightTag: "em",
|
||||
},
|
||||
indexes
|
||||
);
|
||||
// result.facets[0].field === "tag", result.hits[0].highlights.title contains <em>…</em>
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Function | Throws |
|
||||
|---|---|
|
||||
| `createSearchIndex(config, indexes, serviceConfig?)` | `INDEX_ALREADY_EXISTS`, `INDEX_LIMIT_REACHED` |
|
||||
| `deleteSearchIndex(name, indexes)` | — |
|
||||
| `getSearchIndex(name, indexes)` | — |
|
||||
| `listSearchIndexes(indexes, query?)` | — |
|
||||
| `indexDocument(name, doc, indexes, serviceConfig?)` | `INDEX_NOT_FOUND`, `DOCUMENT_LIMIT_REACHED` |
|
||||
| `bulkIndexDocuments(name, docs, indexes, serviceConfig?)` | propagates from `indexDocument` |
|
||||
| `removeDocument(name, docId, indexes)` | `INDEX_NOT_FOUND` |
|
||||
| `search(query, indexes)` | `INDEX_NOT_FOUND`, `UNKNOWN_ANALYZER` |
|
||||
| `suggest(query, indexes)` | `INDEX_NOT_FOUND`, `UNKNOWN_ANALYZER` |
|
||||
|
||||
`SearchServiceConfig` carries the three hard caps; `DEFAULT_SEARCH_CONFIG` provides the defaults
|
||||
(`maxIndexes: 100`, `maxDocumentsPerIndex: 100_000`, `defaultAnalyzer: "standard"`).
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`scoreDocuments` filters out documents with `score === 0` so they
|
||||
never appear in result sets — this matches upstream behaviour. The
|
||||
`sortResults` switch is closed by the `SearchSortOrder` Zod enum so
|
||||
the `default` arm is provably unreachable; it is preserved as a
|
||||
forward-defensive carve-out. `buildFacets` walks `facetFields` in
|
||||
caller order so consumers can rely on response shape. `suggest` reads
|
||||
`tokens.at(-1)` rather than indexing `[len - 1]` so the empty-array
|
||||
case is statically impossible — even though the caller already short-
|
||||
circuits on zero tokens, the `at` call is the documented narrow form.
|
||||
Reference in New Issue
Block a user