@lde/search-typesense 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +66 -7
- package/dist/blue-green-rebuild.d.ts +17 -5
- package/dist/blue-green-rebuild.d.ts.map +1 -1
- package/dist/blue-green-rebuild.js +19 -6
- package/dist/collection-definition.d.ts +9 -3
- package/dist/collection-definition.d.ts.map +1 -1
- package/dist/collection-definition.js +7 -2
- package/dist/collection-name.d.ts +24 -0
- package/dist/collection-name.d.ts.map +1 -0
- package/dist/collection-name.js +43 -0
- package/dist/in-place-rebuild.d.ts +10 -2
- package/dist/in-place-rebuild.d.ts.map +1 -1
- package/dist/in-place-rebuild.js +17 -6
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/rebuild-support.d.ts +15 -4
- package/dist/rebuild-support.d.ts.map +1 -1
- package/dist/rebuild-support.js +14 -4
- package/dist/search.d.ts +32 -11
- package/dist/search.d.ts.map +1 -1
- package/dist/search.js +58 -14
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -9,9 +9,66 @@ Typesense search params, runs it, reconstructs the engine-neutral `SearchResult`
|
|
|
9
9
|
and manages the search index lifecycle as transactional `@lde/pipeline`
|
|
10
10
|
writers (Blue/green Rebuild and In-place Rebuild).
|
|
11
11
|
|
|
12
|
+
## Collection naming
|
|
13
|
+
|
|
14
|
+
You pass `SearchType`s; this package names the collections. `deriveCollectionName`
|
|
15
|
+
turns a type’s logical `name` into a Typesense collection name by Typesense’s own
|
|
16
|
+
convention – snake_case, named after the plural of what the collection holds, as
|
|
17
|
+
its [collection guide](https://typesense.org/docs/guide/organizing-collections.html)
|
|
18
|
+
writes them (`people`, `companies`, `blog_articles`):
|
|
19
|
+
|
|
20
|
+
| `SearchType.name` | collection |
|
|
21
|
+
| ----------------- | ---------------- |
|
|
22
|
+
| `Dataset` | `datasets` |
|
|
23
|
+
| `CreativeWork` | `creative_works` |
|
|
24
|
+
| `Person` | `people` |
|
|
25
|
+
| `TVSeries` | `tv_series` |
|
|
26
|
+
| `DCATDataset` | `dcat_datasets` |
|
|
27
|
+
|
|
28
|
+
The writers and the engine all fall back to this one convention, so documents
|
|
29
|
+
cannot be written to `creative_works` and queried from `creativeWorks` – and a
|
|
30
|
+
reference’s `labelSource` resolves to its label type’s collection the same way.
|
|
31
|
+
Every naming input is therefore optional: `buildCollectionDefinition(type)`,
|
|
32
|
+
`new BlueGreenRebuild(client, type)`, `createTypesenseSearchEngine(client, schema)`.
|
|
33
|
+
|
|
34
|
+
Naming lives here rather than in `@lde/search` because it is engine-specific:
|
|
35
|
+
Elasticsearch/OpenSearch index names are kebab-case and
|
|
36
|
+
[constrained](https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-create-index.html)
|
|
37
|
+
(lowercase only, no `\ / * ? " < > | space , #`, no leading `-` `_` `+`, ≤ 255
|
|
38
|
+
bytes), so a future `@lde/search-elasticsearch` formats the same neutral tokens
|
|
39
|
+
(`@lde/search`’s `physicalNameTokens`) its own way. The core stays engine-neutral.
|
|
40
|
+
|
|
41
|
+
Pass an explicit name to **override** the convention – an env prefix, a
|
|
42
|
+
multi-tenant name, a collection that already exists, a migration:
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
const writer = new BlueGreenRebuild(client, CREATIVE_WORK, {
|
|
46
|
+
name: 'staging_creative_works',
|
|
47
|
+
});
|
|
48
|
+
const engine = createTypesenseSearchEngine(client, schema, {
|
|
49
|
+
collections: { CreativeWork: 'staging_creative_works' },
|
|
50
|
+
});
|
|
51
|
+
writer.collectionName; // 'staging_creative_works' – read-only, for logs/health checks
|
|
52
|
+
engine.collectionNameFor(CREATIVE_WORK); // the same name, resolved once at construction
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`collections` overrides only the types it names; the rest stay derived.
|
|
56
|
+
|
|
57
|
+
Everything the convention cannot do, it refuses to guess at, when the writer or
|
|
58
|
+
engine is constructed rather than on the first rebuild or search:
|
|
59
|
+
|
|
60
|
+
- a name it cannot spell (`Café` would quietly become `cafs`) or turn into a
|
|
61
|
+
legal collection name;
|
|
62
|
+
- two types whose names **derive to the same collection** – English collapses
|
|
63
|
+
`Medium` and `Media` onto `media`, `Person` and `People` onto `people` – which
|
|
64
|
+
would land both types’ documents in one collection, each search returning the
|
|
65
|
+
other’s. Name either one explicitly to resolve it. Types that share a
|
|
66
|
+
collection _deliberately_ (say, several label sources in one `labels`
|
|
67
|
+
collection) are fine: name both, and it is your call, not an accident.
|
|
68
|
+
|
|
12
69
|
## Collection schema and engine
|
|
13
70
|
|
|
14
|
-
`buildCollectionDefinition(searchType, { name
|
|
71
|
+
`buildCollectionDefinition(searchType, { name?, defaultSortingField, … })` derives a
|
|
15
72
|
Typesense collection from the unified `SearchField` model – the Typesense field
|
|
16
73
|
type comes from each field’s `kind`, and the physical fanout (per-locale
|
|
17
74
|
search/sort keys, plus one regex display field per output text field) matches
|
|
@@ -29,7 +86,7 @@ facet/reference and `*_sort_${locale}` companions are indexed. Keeping
|
|
|
29
86
|
retrieval-only fields un-indexed is the lever for holding a large index’s RAM
|
|
30
87
|
down.
|
|
31
88
|
|
|
32
|
-
`createTypesenseSearchEngine(client, schema, { collections })` is the
|
|
89
|
+
`createTypesenseSearchEngine(client, schema, { collections? })` is the
|
|
33
90
|
`SearchEngine` implementation. Each search:
|
|
34
91
|
|
|
35
92
|
- validates the query against the search type (the port contract – a
|
|
@@ -45,9 +102,9 @@ down.
|
|
|
45
102
|
maps, labelled references, labelled facet buckets.
|
|
46
103
|
|
|
47
104
|
A label source is just another `SearchType` in the schema (with an `output`,
|
|
48
|
-
`searchable` text field called `label`)
|
|
49
|
-
|
|
50
|
-
same kind of thing.
|
|
105
|
+
`searchable` text field called `label`), so its collection is named by the same
|
|
106
|
+
convention as any other type’s – a typed entity collection and a ‘labels
|
|
107
|
+
collection’ are the same kind of thing.
|
|
51
108
|
|
|
52
109
|
`searchFacets` – the port’s batch entry point – answers a whole batch of
|
|
53
110
|
facet-only queries (e.g. a faceted listing’s skip-own-filter variants) as a
|
|
@@ -63,7 +120,8 @@ direct use and testing.
|
|
|
63
120
|
Indexing runs through two transactional writers, one per update mode – the
|
|
64
121
|
[NDE Stack](https://docs.nde.nl/stack/patterns) patterns of the same names:
|
|
65
122
|
|
|
66
|
-
- [**Blue/green Rebuild**](#bluegreen-rebuild: build a fresh index, then swap to
|
|
123
|
+
- [**Blue/green Rebuild**](#bluegreen-rebuild): build a fresh index, then swap to
|
|
124
|
+
it atomically;
|
|
67
125
|
- [**In-place Rebuild**](#in-place-rebuild): update the live index directly by
|
|
68
126
|
upserting changed sources and sweeping the rest.
|
|
69
127
|
|
|
@@ -98,7 +156,8 @@ const client = new Client({
|
|
|
98
156
|
apiKey,
|
|
99
157
|
});
|
|
100
158
|
|
|
101
|
-
|
|
159
|
+
// The collection is named from the type: `Dataset` → `datasets`.
|
|
160
|
+
const writer = new BlueGreenRebuild(client, DATASET);
|
|
102
161
|
// Standalone use; under @lde/pipeline the Pipeline drives this lifecycle.
|
|
103
162
|
const run = await writer.openRun(context);
|
|
104
163
|
await run.write(dataset, documents);
|
|
@@ -3,7 +3,8 @@ import type { SearchType } from '@lde/search';
|
|
|
3
3
|
import type { RunContext, RunWriter, Writer } from '@lde/pipeline';
|
|
4
4
|
import { type RebuildOptions } from './rebuild-support.js';
|
|
5
5
|
/** {@link BlueGreenRebuild} options: the collection-definition options (`name` is
|
|
6
|
-
* the logical index name the alias is kept on
|
|
6
|
+
* the logical index name the alias is kept on – omit it to derive one from the
|
|
7
|
+
* {@link SearchType}) plus the rebuild tuning knobs. */
|
|
7
8
|
export type BlueGreenRebuildOptions = RebuildOptions;
|
|
8
9
|
/**
|
|
9
10
|
* Blue/green Rebuild (build a fresh index alongside the live one, then swap to
|
|
@@ -29,16 +30,27 @@ export type BlueGreenRebuildOptions = RebuildOptions;
|
|
|
29
30
|
* - `abort` drops the half-built collection and releases the lock; the live
|
|
30
31
|
* index is untouched.
|
|
31
32
|
*
|
|
32
|
-
* The caller passes
|
|
33
|
-
*
|
|
33
|
+
* The caller passes at most the logical index `name` – omitted, it is derived
|
|
34
|
+
* from the {@link SearchType} ({@link deriveCollectionName}), the same
|
|
35
|
+
* convention the engine reads by; the versioned collection name and the alias
|
|
36
|
+
* are managed here.
|
|
34
37
|
*/
|
|
35
38
|
export declare class BlueGreenRebuild<TDocument extends {
|
|
36
39
|
id: string;
|
|
37
40
|
}> implements Writer<TDocument> {
|
|
38
41
|
private readonly client;
|
|
39
42
|
private readonly searchType;
|
|
40
|
-
|
|
41
|
-
|
|
43
|
+
/**
|
|
44
|
+
* The live Typesense alias this writer keeps pointed at its newest build:
|
|
45
|
+
* the explicit `options.name`, or the name derived from the
|
|
46
|
+
* {@link SearchType}. Read-only and for observability (logging, health
|
|
47
|
+
* checks) – never an input, and the same name
|
|
48
|
+
* {@link createTypesenseSearchEngine} reads the type from. The versioned
|
|
49
|
+
* collections behind it (`${collectionName}_<timestamp>`) stay internal.
|
|
50
|
+
*/
|
|
51
|
+
readonly collectionName: string;
|
|
52
|
+
private readonly resolved;
|
|
53
|
+
constructor(client: Client, searchType: SearchType, options?: BlueGreenRebuildOptions);
|
|
42
54
|
openRun(context: RunContext): Promise<RunWriter<TDocument>>;
|
|
43
55
|
/** The collection an alias currently points at, or `undefined` if unset. */
|
|
44
56
|
private aliasTarget;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"blue-green-rebuild.d.ts","sourceRoot":"","sources":["../src/blue-green-rebuild.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AACxC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,KAAK,EAEV,UAAU,EACV,SAAS,EACT,MAAM,EACP,MAAM,eAAe,CAAC;AAMvB,OAAO,EAKL,KAAK,cAAc,
|
|
1
|
+
{"version":3,"file":"blue-green-rebuild.d.ts","sourceRoot":"","sources":["../src/blue-green-rebuild.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AACxC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,KAAK,EAEV,UAAU,EACV,SAAS,EACT,MAAM,EACP,MAAM,eAAe,CAAC;AAMvB,OAAO,EAKL,KAAK,cAAc,EAEpB,MAAM,sBAAsB,CAAC;AAE9B;;yDAEyD;AACzD,MAAM,MAAM,uBAAuB,GAAG,cAAc,CAAC;AAErD;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,qBAAa,gBAAgB,CAC3B,SAAS,SAAS;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,CAChC,YAAW,MAAM,CAAC,SAAS,CAAC;IAa1B,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,UAAU;IAb7B;;;;;;;OAOG;IACH,SAAgB,cAAc,EAAE,MAAM,CAAC;IACvC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAyB;gBAG/B,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,UAAU,EACvC,OAAO,GAAE,uBAA4B;IAQjC,OAAO,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC;IA6FjE,4EAA4E;YAC9D,WAAW;CAW1B"}
|
|
@@ -27,22 +27,35 @@ import { assertNoReservedFields, deleteByFilter, resolveRebuildOptions, stampDoc
|
|
|
27
27
|
* - `abort` drops the half-built collection and releases the lock; the live
|
|
28
28
|
* index is untouched.
|
|
29
29
|
*
|
|
30
|
-
* The caller passes
|
|
31
|
-
*
|
|
30
|
+
* The caller passes at most the logical index `name` – omitted, it is derived
|
|
31
|
+
* from the {@link SearchType} ({@link deriveCollectionName}), the same
|
|
32
|
+
* convention the engine reads by; the versioned collection name and the alias
|
|
33
|
+
* are managed here.
|
|
32
34
|
*/
|
|
33
35
|
export class BlueGreenRebuild {
|
|
34
36
|
client;
|
|
35
37
|
searchType;
|
|
36
|
-
|
|
37
|
-
|
|
38
|
+
/**
|
|
39
|
+
* The live Typesense alias this writer keeps pointed at its newest build:
|
|
40
|
+
* the explicit `options.name`, or the name derived from the
|
|
41
|
+
* {@link SearchType}. Read-only and for observability (logging, health
|
|
42
|
+
* checks) – never an input, and the same name
|
|
43
|
+
* {@link createTypesenseSearchEngine} reads the type from. The versioned
|
|
44
|
+
* collections behind it (`${collectionName}_<timestamp>`) stay internal.
|
|
45
|
+
*/
|
|
46
|
+
collectionName;
|
|
47
|
+
resolved;
|
|
48
|
+
constructor(client, searchType, options = {}) {
|
|
38
49
|
this.client = client;
|
|
39
50
|
this.searchType = searchType;
|
|
40
|
-
this.options = options;
|
|
41
51
|
// `source` is stamped on every document for per-dataset rollback.
|
|
42
52
|
assertNoReservedFields(searchType, [SOURCE_FIELD]);
|
|
53
|
+
this.resolved = resolveRebuildOptions(searchType, options);
|
|
54
|
+
this.collectionName = this.resolved.definitionOptions.name;
|
|
43
55
|
}
|
|
44
56
|
async openRun(context) {
|
|
45
|
-
const {
|
|
57
|
+
const { batchSize, lockTtlMs, definitionOptions } = this.resolved;
|
|
58
|
+
const name = this.collectionName;
|
|
46
59
|
return openLockedRun(this.client, name, lockTtlMs, async () => {
|
|
47
60
|
// Create the fresh (blue) collection up front, so a failure surfaces
|
|
48
61
|
// before any dataset is processed. startedAt orders the versioned names;
|
|
@@ -2,8 +2,10 @@ import type { CollectionCreateSchema } from 'typesense';
|
|
|
2
2
|
import { type SearchType } from '@lde/search';
|
|
3
3
|
/** Deployment-specific options the generic field model does not carry. */
|
|
4
4
|
export interface CollectionDefinitionOptions {
|
|
5
|
-
/** The Typesense collection (or alias) name.
|
|
6
|
-
|
|
5
|
+
/** The Typesense collection (or alias) name. Omit to derive it from the
|
|
6
|
+
* type’s `name` ({@link deriveCollectionName}); supply one to override that
|
|
7
|
+
* (an env prefix, a multi-tenant name, an existing collection). */
|
|
8
|
+
readonly name?: string;
|
|
7
9
|
/** Snowball stemming locale for non-localized searchable fields (e.g. `en`).
|
|
8
10
|
* Unset, those fields are not stemmed – folding still applies – so no
|
|
9
11
|
* language is ever assumed. Localized text search fields always stem in
|
|
@@ -26,6 +28,10 @@ export interface CollectionDefinitionOptions {
|
|
|
26
28
|
* stems in `defaultLocale` when one is set, and is left unstemmed (folded
|
|
27
29
|
* only) otherwise.
|
|
28
30
|
*
|
|
31
|
+
* The collection `name` is optional: omitted, it is derived from the type’s own
|
|
32
|
+
* `name` ({@link deriveCollectionName}), so a caller that has no naming opinion
|
|
33
|
+
* states none.
|
|
34
|
+
*
|
|
29
35
|
* Memory lever: Typesense holds the index in RAM (with a raw copy of each
|
|
30
36
|
* document on disk), so RAM tracks the *indexed* surface – roughly 2–3× the
|
|
31
37
|
* size of the fields you search, facet or sort on – not the whole document.
|
|
@@ -36,5 +42,5 @@ export interface CollectionDefinitionOptions {
|
|
|
36
42
|
* companions are indexed. Keeping retrieval-only fields un-indexed is the lever
|
|
37
43
|
* for holding a large index’s RAM down.
|
|
38
44
|
*/
|
|
39
|
-
export declare function buildCollectionDefinition(searchType: SearchType, options
|
|
45
|
+
export declare function buildCollectionDefinition(searchType: SearchType, options?: CollectionDefinitionOptions): CollectionCreateSchema;
|
|
40
46
|
//# sourceMappingURL=collection-definition.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"collection-definition.d.ts","sourceRoot":"","sources":["../src/collection-definition.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,WAAW,CAAC;AAExD,OAAO,EAAoB,KAAK,UAAU,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"collection-definition.d.ts","sourceRoot":"","sources":["../src/collection-definition.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,WAAW,CAAC;AAExD,OAAO,EAAoB,KAAK,UAAU,EAAE,MAAM,aAAa,CAAC;AAIhE,0EAA0E;AAC1E,MAAM,WAAW,2BAA2B;IAC1C;;wEAEoE;IACpE,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB;;;4BAGwB;IACxB,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,kEAAkE;IAClE,QAAQ,CAAC,mBAAmB,CAAC,EAAE,MAAM,CAAC;IACtC,kEAAkE;IAClE,QAAQ,CAAC,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC1C;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,yBAAyB,CACvC,UAAU,EAAE,UAAU,EACtB,OAAO,GAAE,2BAAgC,GACxC,sBAAsB,CAexB"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { displayFieldPattern, physicalFields } from '@lde/search/adapter';
|
|
2
|
+
import { deriveCollectionName } from './collection-name.js';
|
|
2
3
|
/**
|
|
3
4
|
* Build a Typesense collection definition from the unified {@link SearchType}, so
|
|
4
5
|
* the index and the projection are driven by one declarative source and cannot
|
|
@@ -11,6 +12,10 @@ import { displayFieldPattern, physicalFields } from '@lde/search/adapter';
|
|
|
11
12
|
* stems in `defaultLocale` when one is set, and is left unstemmed (folded
|
|
12
13
|
* only) otherwise.
|
|
13
14
|
*
|
|
15
|
+
* The collection `name` is optional: omitted, it is derived from the type’s own
|
|
16
|
+
* `name` ({@link deriveCollectionName}), so a caller that has no naming opinion
|
|
17
|
+
* states none.
|
|
18
|
+
*
|
|
14
19
|
* Memory lever: Typesense holds the index in RAM (with a raw copy of each
|
|
15
20
|
* document on disk), so RAM tracks the *indexed* surface – roughly 2–3× the
|
|
16
21
|
* size of the fields you search, facet or sort on – not the whole document.
|
|
@@ -21,10 +26,10 @@ import { displayFieldPattern, physicalFields } from '@lde/search/adapter';
|
|
|
21
26
|
* companions are indexed. Keeping retrieval-only fields un-indexed is the lever
|
|
22
27
|
* for holding a large index’s RAM down.
|
|
23
28
|
*/
|
|
24
|
-
export function buildCollectionDefinition(searchType, options) {
|
|
29
|
+
export function buildCollectionDefinition(searchType, options = {}) {
|
|
25
30
|
const { defaultLocale } = options;
|
|
26
31
|
const collection = {
|
|
27
|
-
name: options.name,
|
|
32
|
+
name: options.name ?? deriveCollectionName(searchType),
|
|
28
33
|
fields: searchType.fields.flatMap((field) => typesenseFields(field, defaultLocale, options.defaultSortingField)),
|
|
29
34
|
};
|
|
30
35
|
if (options.defaultSortingField !== undefined) {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { SearchType } from '@lde/search';
|
|
2
|
+
/**
|
|
3
|
+
* The Typesense collection name for a {@link SearchType}, derived from its
|
|
4
|
+
* logical `name` – the default every writer and the engine fall back to when a
|
|
5
|
+
* deployment supplies none, so the write side and the read side share one
|
|
6
|
+
* convention and cannot drift.
|
|
7
|
+
*
|
|
8
|
+
* The convention is Typesense’s own, as its docs write collection names:
|
|
9
|
+
* snake_case, named after the plural of what the collection holds – `people`,
|
|
10
|
+
* `companies`, `blog_articles`
|
|
11
|
+
* ({@link https://typesense.org/docs/guide/organizing-collections.html |
|
|
12
|
+
* Organizing collections}, which frames a collection as ‘similar to a table in
|
|
13
|
+
* a relational database’). So `CreativeWork` → `creative_works`, `Person` →
|
|
14
|
+
* `people`, `TVSeries` → `tv_series`. The engine-neutral half – the word split
|
|
15
|
+
* and the inflection – is {@link physicalNameTokens}; only the `_` join and the
|
|
16
|
+
* validation below are Typesense’s.
|
|
17
|
+
*
|
|
18
|
+
* A deployment that needs another name (an env prefix, a multi-tenant name, an
|
|
19
|
+
* index that already exists) passes it explicitly instead; the override is
|
|
20
|
+
* never validated here, since an existing collection’s name is the
|
|
21
|
+
* deployment’s business.
|
|
22
|
+
*/
|
|
23
|
+
export declare function deriveCollectionName(searchType: SearchType): string;
|
|
24
|
+
//# sourceMappingURL=collection-name.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"collection-name.d.ts","sourceRoot":"","sources":["../src/collection-name.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAG9C;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,UAAU,GAAG,MAAM,CAWnE"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { physicalNameTokens } from '@lde/search/adapter';
|
|
2
|
+
/**
|
|
3
|
+
* The Typesense collection name for a {@link SearchType}, derived from its
|
|
4
|
+
* logical `name` – the default every writer and the engine fall back to when a
|
|
5
|
+
* deployment supplies none, so the write side and the read side share one
|
|
6
|
+
* convention and cannot drift.
|
|
7
|
+
*
|
|
8
|
+
* The convention is Typesense’s own, as its docs write collection names:
|
|
9
|
+
* snake_case, named after the plural of what the collection holds – `people`,
|
|
10
|
+
* `companies`, `blog_articles`
|
|
11
|
+
* ({@link https://typesense.org/docs/guide/organizing-collections.html |
|
|
12
|
+
* Organizing collections}, which frames a collection as ‘similar to a table in
|
|
13
|
+
* a relational database’). So `CreativeWork` → `creative_works`, `Person` →
|
|
14
|
+
* `people`, `TVSeries` → `tv_series`. The engine-neutral half – the word split
|
|
15
|
+
* and the inflection – is {@link physicalNameTokens}; only the `_` join and the
|
|
16
|
+
* validation below are Typesense’s.
|
|
17
|
+
*
|
|
18
|
+
* A deployment that needs another name (an env prefix, a multi-tenant name, an
|
|
19
|
+
* index that already exists) passes it explicitly instead; the override is
|
|
20
|
+
* never validated here, since an existing collection’s name is the
|
|
21
|
+
* deployment’s business.
|
|
22
|
+
*/
|
|
23
|
+
export function deriveCollectionName(searchType) {
|
|
24
|
+
// A name the split cannot spell at all throws in physicalNameTokens, which
|
|
25
|
+
// owns that engine-neutral rule; what is left to check here is whether the
|
|
26
|
+
// formatted result is legal for THIS engine.
|
|
27
|
+
const name = physicalNameTokens(searchType).join('_');
|
|
28
|
+
if (!DERIVED_COLLECTION_NAME.test(name)) {
|
|
29
|
+
throw new Error(`Cannot derive a Typesense collection name from search type “${searchType.name}”: it yields “${name}”, which is not a legal collection name. Rename the type, or pass an explicit name.`);
|
|
30
|
+
}
|
|
31
|
+
return name;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* What a *derived* name must look like. Typesense documents no collection-name
|
|
35
|
+
* rules, but a name travels in the URL path of every collection call
|
|
36
|
+
* (`/collections/:name`), so a character with URL meaning breaks that call
|
|
37
|
+
* rather than being rejected at creation – `#` and `+` silently strand a
|
|
38
|
+
* collection ({@link https://github.com/typesense/typesense/issues/548}).
|
|
39
|
+
* {@link physicalNameTokens} already guarantees lowercase ASCII words, so what
|
|
40
|
+
* this still rules on is the leading character: a type named `123` derives
|
|
41
|
+
* `123s`, and a name should open with a letter rather than read as a number.
|
|
42
|
+
*/
|
|
43
|
+
const DERIVED_COLLECTION_NAME = /^[a-z][a-z0-9_]*$/;
|
|
@@ -47,8 +47,16 @@ export declare class InPlaceRebuild<TDocument extends {
|
|
|
47
47
|
}> implements Writer<TDocument> {
|
|
48
48
|
private readonly client;
|
|
49
49
|
private readonly searchType;
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
/**
|
|
51
|
+
* The Typesense collection this writer maintains: the explicit
|
|
52
|
+
* `options.name`, or the name derived from the {@link SearchType}. Read-only
|
|
53
|
+
* and for observability (logging, health checks) – never an input, and the
|
|
54
|
+
* same name {@link createTypesenseSearchEngine} reads the type from.
|
|
55
|
+
*/
|
|
56
|
+
readonly collectionName: string;
|
|
57
|
+
private readonly maxSweepableSources;
|
|
58
|
+
private readonly resolved;
|
|
59
|
+
constructor(client: Client, searchType: SearchType, options?: InPlaceRebuildOptions);
|
|
52
60
|
openRun(context: RunContext): Promise<RunWriter<TDocument>>;
|
|
53
61
|
/**
|
|
54
62
|
* The distinct sources present in the collection, via a single `source`
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"in-place-rebuild.d.ts","sourceRoot":"","sources":["../src/in-place-rebuild.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAExC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE9C,OAAO,KAAK,EAEV,UAAU,EACV,SAAS,EACT,MAAM,EACP,MAAM,eAAe,CAAC;AAYvB,OAAO,EAKL,KAAK,cAAc,
|
|
1
|
+
{"version":3,"file":"in-place-rebuild.d.ts","sourceRoot":"","sources":["../src/in-place-rebuild.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAExC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE9C,OAAO,KAAK,EAEV,UAAU,EACV,SAAS,EACT,MAAM,EACP,MAAM,eAAe,CAAC;AAYvB,OAAO,EAKL,KAAK,cAAc,EAEpB,MAAM,sBAAsB,CAAC;AAa9B;gCACgC;AAChC,MAAM,WAAW,qBAAsB,SAAQ,cAAc;IAC3D;sEACkE;IAClE,QAAQ,CAAC,mBAAmB,CAAC,EAAE,MAAM,CAAC;CACvC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,qBAAa,cAAc,CACzB,SAAS,SAAS;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,CAChC,YAAW,MAAM,CAAC,SAAS,CAAC;IAY1B,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,UAAU;IAZ7B;;;;;OAKG;IACH,SAAgB,cAAc,EAAE,MAAM,CAAC;IACvC,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAS;IAC7C,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAyB;gBAG/B,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,UAAU,EACvC,OAAO,GAAE,qBAA0B;IAY/B,OAAO,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC;IAsFjE;;;;;;OAMG;YACW,cAAc;CAsB7B"}
|
package/dist/in-place-rebuild.js
CHANGED
|
@@ -49,16 +49,27 @@ const DEFAULT_MAX_SWEEPABLE_SOURCES = 10_000;
|
|
|
49
49
|
export class InPlaceRebuild {
|
|
50
50
|
client;
|
|
51
51
|
searchType;
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
/**
|
|
53
|
+
* The Typesense collection this writer maintains: the explicit
|
|
54
|
+
* `options.name`, or the name derived from the {@link SearchType}. Read-only
|
|
55
|
+
* and for observability (logging, health checks) – never an input, and the
|
|
56
|
+
* same name {@link createTypesenseSearchEngine} reads the type from.
|
|
57
|
+
*/
|
|
58
|
+
collectionName;
|
|
59
|
+
maxSweepableSources;
|
|
60
|
+
resolved;
|
|
61
|
+
constructor(client, searchType, options = {}) {
|
|
54
62
|
this.client = client;
|
|
55
63
|
this.searchType = searchType;
|
|
56
|
-
this.options = options;
|
|
57
64
|
assertNoReservedFields(searchType, [SOURCE_FIELD, LAST_SEEN_FIELD]);
|
|
65
|
+
const { maxSweepableSources = DEFAULT_MAX_SWEEPABLE_SOURCES, ...rebuildOptions } = options;
|
|
66
|
+
this.maxSweepableSources = maxSweepableSources;
|
|
67
|
+
this.resolved = resolveRebuildOptions(searchType, rebuildOptions);
|
|
68
|
+
this.collectionName = this.resolved.definitionOptions.name;
|
|
58
69
|
}
|
|
59
70
|
async openRun(context) {
|
|
60
|
-
const {
|
|
61
|
-
const
|
|
71
|
+
const { batchSize, lockTtlMs, definitionOptions } = this.resolved;
|
|
72
|
+
const name = this.collectionName;
|
|
62
73
|
return openLockedRun(this.client, name, lockTtlMs, async () => {
|
|
63
74
|
// Create the collection on demand: SearchType schema + the bookkeeping
|
|
64
75
|
// fields, `source` faceted so the membership sweep can enumerate the
|
|
@@ -101,7 +112,7 @@ export class InPlaceRebuild {
|
|
|
101
112
|
},
|
|
102
113
|
commit: async () => {
|
|
103
114
|
await importer.flush();
|
|
104
|
-
const departed = departedSources(await this.indexedSources(name, maxSweepableSources), context.selectedSources());
|
|
115
|
+
const departed = departedSources(await this.indexedSources(name, this.maxSweepableSources), context.selectedSources());
|
|
105
116
|
for (const filter of membershipSweepFilters(departed)) {
|
|
106
117
|
await deleteByFilter(this.client, name, filter);
|
|
107
118
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -6,8 +6,9 @@ export { RebuildAlreadyRunning } from './lock.js';
|
|
|
6
6
|
export { departedSources, membershipSweepFilters, sourceDocumentsFilter, staleDocumentsFilter, } from './sweep.js';
|
|
7
7
|
export { buildCollectionDefinition } from './collection-definition.js';
|
|
8
8
|
export type { CollectionDefinitionOptions } from './collection-definition.js';
|
|
9
|
+
export { deriveCollectionName } from './collection-name.js';
|
|
9
10
|
export { buildSearchParams } from './query-compiler.js';
|
|
10
11
|
export type { BuildSearchParamsOptions } from './query-compiler.js';
|
|
11
12
|
export { createTypesenseSearchEngine, parseSearchResponse } from './search.js';
|
|
12
|
-
export type { TypesenseSearchEngineOptions, TypesenseSearchResponse, } from './search.js';
|
|
13
|
+
export type { TypesenseSearchEngine, TypesenseSearchEngineOptions, TypesenseSearchResponse, } from './search.js';
|
|
13
14
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAC3D,YAAY,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AACvE,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,YAAY,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AACnE,OAAO,EAAE,qBAAqB,EAAE,MAAM,WAAW,CAAC;AAClD,OAAO,EACL,eAAe,EACf,sBAAsB,EACtB,qBAAqB,EACrB,oBAAoB,GACrB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,yBAAyB,EAAE,MAAM,4BAA4B,CAAC;AACvE,YAAY,EAAE,2BAA2B,EAAE,MAAM,4BAA4B,CAAC;AAC9E,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACxD,YAAY,EAAE,wBAAwB,EAAE,MAAM,qBAAqB,CAAC;AACpE,OAAO,EAAE,2BAA2B,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAC/E,YAAY,EACV,4BAA4B,EAC5B,uBAAuB,GACxB,MAAM,aAAa,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAC3D,YAAY,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AACvE,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,YAAY,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AACnE,OAAO,EAAE,qBAAqB,EAAE,MAAM,WAAW,CAAC;AAClD,OAAO,EACL,eAAe,EACf,sBAAsB,EACtB,qBAAqB,EACrB,oBAAoB,GACrB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,yBAAyB,EAAE,MAAM,4BAA4B,CAAC;AACvE,YAAY,EAAE,2BAA2B,EAAE,MAAM,4BAA4B,CAAC;AAC9E,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAC5D,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACxD,YAAY,EAAE,wBAAwB,EAAE,MAAM,qBAAqB,CAAC;AACpE,OAAO,EAAE,2BAA2B,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAC/E,YAAY,EACV,qBAAqB,EACrB,4BAA4B,EAC5B,uBAAuB,GACxB,MAAM,aAAa,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -3,5 +3,6 @@ export { InPlaceRebuild } from './in-place-rebuild.js';
|
|
|
3
3
|
export { RebuildAlreadyRunning } from './lock.js';
|
|
4
4
|
export { departedSources, membershipSweepFilters, sourceDocumentsFilter, staleDocumentsFilter, } from './sweep.js';
|
|
5
5
|
export { buildCollectionDefinition } from './collection-definition.js';
|
|
6
|
+
export { deriveCollectionName } from './collection-name.js';
|
|
6
7
|
export { buildSearchParams } from './query-compiler.js';
|
|
7
8
|
export { createTypesenseSearchEngine, parseSearchResponse } from './search.js';
|
|
@@ -14,13 +14,24 @@ export interface RebuildOptions extends CollectionDefinitionOptions {
|
|
|
14
14
|
/** The shared options resolved to concrete values plus the residual
|
|
15
15
|
* collection-definition options a writer passes to {@link buildCollectionDefinition}. */
|
|
16
16
|
export interface ResolvedRebuildOptions {
|
|
17
|
-
readonly name: string;
|
|
18
17
|
readonly batchSize: number;
|
|
19
18
|
readonly lockTtlMs: number;
|
|
20
|
-
|
|
19
|
+
/** The collection-definition options with the `name` resolved – the one place
|
|
20
|
+
* the resolved name lives, so the collection a writer talks to and the
|
|
21
|
+
* definition it creates cannot say different things. */
|
|
22
|
+
readonly definitionOptions: CollectionDefinitionOptions & {
|
|
23
|
+
readonly name: string;
|
|
24
|
+
};
|
|
21
25
|
}
|
|
22
|
-
/**
|
|
23
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Apply the shared defaults, once, so neither writer restates them – including
|
|
28
|
+
* the collection name, derived from the type ({@link deriveCollectionName})
|
|
29
|
+
* when the deployment supplies none.
|
|
30
|
+
*
|
|
31
|
+
* Writers resolve at construction rather than per run, so an underivable name
|
|
32
|
+
* throws when the writer is built, not on the first rebuild.
|
|
33
|
+
*/
|
|
34
|
+
export declare function resolveRebuildOptions(searchType: SearchType, options: RebuildOptions): ResolvedRebuildOptions;
|
|
24
35
|
/**
|
|
25
36
|
* Reject a {@link SearchType} that declares any of the bookkeeping field names
|
|
26
37
|
* a writer stamps, so a domain field can never collide with `source` /
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rebuild-support.d.ts","sourceRoot":"","sources":["../src/rebuild-support.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AACxC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,4BAA4B,CAAC;
|
|
1
|
+
{"version":3,"file":"rebuild-support.d.ts","sourceRoot":"","sources":["../src/rebuild-support.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AACxC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,4BAA4B,CAAC;AAK9E;;;GAGG;AACH,MAAM,WAAW,cAAe,SAAQ,2BAA2B;IACjE,+DAA+D;IAC/D,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,0EAA0E;IAC1E,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED;0FAC0F;AAC1F,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B;;6DAEyD;IACzD,QAAQ,CAAC,iBAAiB,EAAE,2BAA2B,GAAG;QACxD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;KACvB,CAAC;CACH;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CACnC,UAAU,EAAE,UAAU,EACtB,OAAO,EAAE,cAAc,GACtB,sBAAsB,CAcxB;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CACpC,UAAU,EAAE,UAAU,EACtB,QAAQ,EAAE,SAAS,MAAM,EAAE,GAC1B,IAAI,CAWN;AAED,4EAA4E;AAC5E,wBAAuB,cAAc,CAAC,SAAS,EAC7C,SAAS,EAAE,aAAa,CAAC,SAAS,CAAC,EACnC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GACtC,aAAa,CAAC,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAInD;AAED,mEAAmE;AACnE,wBAAsB,cAAc,CAClC,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,IAAI,CAAC,CAKf"}
|
package/dist/rebuild-support.js
CHANGED
|
@@ -1,13 +1,23 @@
|
|
|
1
|
+
import { deriveCollectionName } from './collection-name.js';
|
|
1
2
|
import { DEFAULT_LOCK_TTL_MS } from './lock.js';
|
|
2
3
|
import { DEFAULT_BATCH_SIZE } from './import.js';
|
|
3
|
-
/**
|
|
4
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Apply the shared defaults, once, so neither writer restates them – including
|
|
6
|
+
* the collection name, derived from the type ({@link deriveCollectionName})
|
|
7
|
+
* when the deployment supplies none.
|
|
8
|
+
*
|
|
9
|
+
* Writers resolve at construction rather than per run, so an underivable name
|
|
10
|
+
* throws when the writer is built, not on the first rebuild.
|
|
11
|
+
*/
|
|
12
|
+
export function resolveRebuildOptions(searchType, options) {
|
|
5
13
|
const { batchSize = DEFAULT_BATCH_SIZE, lockTtlMs = DEFAULT_LOCK_TTL_MS, ...definitionOptions } = options;
|
|
6
14
|
return {
|
|
7
|
-
name: definitionOptions.name,
|
|
8
15
|
batchSize,
|
|
9
16
|
lockTtlMs,
|
|
10
|
-
definitionOptions
|
|
17
|
+
definitionOptions: {
|
|
18
|
+
...definitionOptions,
|
|
19
|
+
name: definitionOptions.name ?? deriveCollectionName(searchType),
|
|
20
|
+
},
|
|
11
21
|
};
|
|
12
22
|
}
|
|
13
23
|
/**
|
package/dist/search.d.ts
CHANGED
|
@@ -3,15 +3,20 @@ import { type LocalizedValue, type SearchEngine, type SearchResult, type SearchS
|
|
|
3
3
|
import { type BuildSearchParamsOptions } from './query-compiler.js';
|
|
4
4
|
/** Where the engine reads documents – plus every query-compiler knob
|
|
5
5
|
* ({@link BuildSearchParamsOptions}), declared once there and forwarded
|
|
6
|
-
* wholesale into each search.
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* the SearchType its `labelSource` names – a label source is just another
|
|
10
|
-
* entry in `collections`. */
|
|
6
|
+
* wholesale into each search. Reference labels resolve per field from the
|
|
7
|
+
* collection of the SearchType its `labelSource` names – a label source is
|
|
8
|
+
* just another type, so it is named the same way. */
|
|
11
9
|
export interface TypesenseSearchEngineOptions<TypeName extends string = string> extends BuildSearchParamsOptions {
|
|
12
|
-
/**
|
|
13
|
-
*
|
|
14
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Overrides the collection (or alias) a search type reads, keyed by the
|
|
12
|
+
* type’s `name`. Every type not named here reads the collection derived from
|
|
13
|
+
* its own name ({@link deriveCollectionName}) – the same convention the
|
|
14
|
+
* writers create, so the read side and the write side cannot drift. Supply an
|
|
15
|
+
* entry only where a deployment needs another name (an env prefix, a
|
|
16
|
+
* multi-tenant name, an existing collection); `TypeName` is the union of the
|
|
17
|
+
* schema’s type names, so a typo is still a compile error.
|
|
18
|
+
*/
|
|
19
|
+
readonly collections?: Partial<Readonly<Record<TypeName, string>>>;
|
|
15
20
|
/**
|
|
16
21
|
* Called when reference-label resolution fails; the search then degrades to
|
|
17
22
|
* id-only references rather than failing. Optional – omit to swallow silently.
|
|
@@ -27,6 +32,21 @@ export interface TypesenseSearchEngineOptions<TypeName extends string = string>
|
|
|
27
32
|
*/
|
|
28
33
|
readonly labelCacheTtlMs?: number;
|
|
29
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* A {@link SearchEngine} backed by Typesense: the port, plus the one engine
|
|
37
|
+
* specific worth exposing – which collection each type actually reads.
|
|
38
|
+
*/
|
|
39
|
+
export interface TypesenseSearchEngine<Types extends readonly SearchType[] = readonly SearchType[]> extends SearchEngine<Types> {
|
|
40
|
+
/**
|
|
41
|
+
* The collection `searchType` reads: its {@link
|
|
42
|
+
* TypesenseSearchEngineOptions.collections} override, or the name derived
|
|
43
|
+
* from the type. Resolved at construction and read-only – for observability
|
|
44
|
+
* (logging a search’s target, a health check asserting the collection
|
|
45
|
+
* exists), never an input. Throws for a type outside this engine’s schema,
|
|
46
|
+
* like every other entry point.
|
|
47
|
+
*/
|
|
48
|
+
collectionNameFor(searchType: Types[number]): string;
|
|
49
|
+
}
|
|
30
50
|
/**
|
|
31
51
|
* One reference field’s resolved label source: the collection it reads, the
|
|
32
52
|
* source type’s `label` declaration (for reconstructing localized labels) and
|
|
@@ -41,8 +61,9 @@ export interface LabelSource {
|
|
|
41
61
|
/**
|
|
42
62
|
* A Typesense-backed {@link SearchEngine}, bound to the whole
|
|
43
63
|
* {@link SearchSchema} at construction – like every other schema consumer.
|
|
44
|
-
* Each type’s collection
|
|
45
|
-
*
|
|
64
|
+
* Each type’s collection is derived from the type ({@link
|
|
65
|
+
* deriveCollectionName}) unless `options.collections` overrides it. `search`
|
|
66
|
+
* compiles the query ({@link buildSearchParams}), runs it against the type’s
|
|
46
67
|
* collection, resolves the reference labels for the page of hits – each
|
|
47
68
|
* reference field from its own label source’s collection, all sources bundled
|
|
48
69
|
* into one lookup – and reconstructs the engine-neutral
|
|
@@ -57,7 +78,7 @@ export interface LabelSource {
|
|
|
57
78
|
* `search()` accepts only the deployment’s own types and returns typo-safe
|
|
58
79
|
* facet/document keys per call – no caller-side generics.
|
|
59
80
|
*/
|
|
60
|
-
export declare function createTypesenseSearchEngine<const Types extends readonly SearchType[]>(client: Client, schema: SearchSchema<Types>, options
|
|
81
|
+
export declare function createTypesenseSearchEngine<const Types extends readonly SearchType[]>(client: Client, schema: SearchSchema<Types>, options?: TypesenseSearchEngineOptions<Types[number]['name']>): TypesenseSearchEngine<Types>;
|
|
61
82
|
/** One label lookup: a source and the distinct IRIs to resolve against it. */
|
|
62
83
|
export interface LabelLookupGroup {
|
|
63
84
|
readonly source: LabelSource;
|
package/dist/search.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../src/search.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AACxC,OAAO,EAGL,KAAK,cAAc,EAMnB,KAAK,YAAY,EAEjB,KAAK,YAAY,EACjB,KAAK,YAAY,EACjB,KAAK,UAAU,EAEf,KAAK,SAAS,EACf,MAAM,aAAa,CAAC;AAYrB,OAAO,EAGL,KAAK,wBAAwB,EAC9B,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../src/search.ts"],"names":[],"mappings":"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"}
|
package/dist/search.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { assertTypeInSchema, assertValidQuery, displayLangOf, fieldNamed, isRangeFacet, labelFieldOf, outputFields, physicalFields, referenceFields, } from '@lde/search/adapter';
|
|
2
2
|
import { buildSearchParams, escapeFilterValue, } from './query-compiler.js';
|
|
3
|
+
import { deriveCollectionName } from './collection-name.js';
|
|
3
4
|
/**
|
|
4
5
|
* A Typesense-backed {@link SearchEngine}, bound to the whole
|
|
5
6
|
* {@link SearchSchema} at construction – like every other schema consumer.
|
|
6
|
-
* Each type’s collection
|
|
7
|
-
*
|
|
7
|
+
* Each type’s collection is derived from the type ({@link
|
|
8
|
+
* deriveCollectionName}) unless `options.collections` overrides it. `search`
|
|
9
|
+
* compiles the query ({@link buildSearchParams}), runs it against the type’s
|
|
8
10
|
* collection, resolves the reference labels for the page of hits – each
|
|
9
11
|
* reference field from its own label source’s collection, all sources bundled
|
|
10
12
|
* into one lookup – and reconstructs the engine-neutral
|
|
@@ -19,20 +21,28 @@ import { buildSearchParams, escapeFilterValue, } from './query-compiler.js';
|
|
|
19
21
|
* `search()` accepts only the deployment’s own types and returns typo-safe
|
|
20
22
|
* facet/document keys per call – no caller-side generics.
|
|
21
23
|
*/
|
|
22
|
-
export function createTypesenseSearchEngine(client, schema, options) {
|
|
23
|
-
// Resolve every type's collection ONCE at construction
|
|
24
|
-
//
|
|
25
|
-
// happens to hit
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
24
|
+
export function createTypesenseSearchEngine(client, schema, options = {}) {
|
|
25
|
+
// Resolve every type's collection ONCE at construction – the override when
|
|
26
|
+
// the deployment named one, the derived name otherwise – so an underivable
|
|
27
|
+
// name fails at startup, never on the first search that happens to hit that
|
|
28
|
+
// type.
|
|
29
|
+
const resolved = [...schema.values()].map((searchType) => {
|
|
30
|
+
const override = options.collections?.[searchType.name];
|
|
31
|
+
return {
|
|
32
|
+
searchType,
|
|
33
|
+
collection: override ?? deriveCollectionName(searchType),
|
|
34
|
+
derived: override === undefined,
|
|
35
|
+
};
|
|
36
|
+
});
|
|
37
|
+
assertNoAccidentalSharing(resolved);
|
|
38
|
+
const collections = new Map(resolved.map(({ searchType, collection }) => [
|
|
39
|
+
searchType.class,
|
|
40
|
+
collection,
|
|
41
|
+
]));
|
|
33
42
|
// Resolve every reference field's label source ONCE at construction. The
|
|
34
43
|
// schema already guarantees the named type exists and serves labels
|
|
35
|
-
// (`searchSchema`/`labelFieldOf`), and
|
|
44
|
+
// (`searchSchema`/`labelFieldOf`), and every type resolved a collection
|
|
45
|
+
// above, so both lookups below always hit.
|
|
36
46
|
const typesByName = new Map([...schema.values()].map((searchType) => [searchType.name, searchType]));
|
|
37
47
|
const labelSources = new Map([...schema.values()].map((searchType) => [
|
|
38
48
|
searchType.class,
|
|
@@ -156,6 +166,10 @@ export function createTypesenseSearchEngine(client, schema, options) {
|
|
|
156
166
|
}
|
|
157
167
|
const engine = {
|
|
158
168
|
schema,
|
|
169
|
+
collectionNameFor(searchType) {
|
|
170
|
+
assertTypeInSchema(schema, searchType);
|
|
171
|
+
return collections.get(searchType.class);
|
|
172
|
+
},
|
|
159
173
|
async search(searchType, query) {
|
|
160
174
|
// The port contract: a type outside the bound schema and a structurally
|
|
161
175
|
// invalid query (unknown field, wrong operator, unknown facet) are both
|
|
@@ -208,6 +222,36 @@ export function createTypesenseSearchEngine(client, schema, options) {
|
|
|
208
222
|
// The runtime object is string-keyed; the literal-schema typing narrows it.
|
|
209
223
|
return engine;
|
|
210
224
|
}
|
|
225
|
+
/**
|
|
226
|
+
* Reject two types landing in one collection *by accident*.
|
|
227
|
+
*
|
|
228
|
+
* A schema keeps type names distinct, but distinct names can still derive the
|
|
229
|
+
* same collection – English collapses them (`Medium` and `Media` both give
|
|
230
|
+
* `media`, `Person` and `People` both give `people`). Nothing downstream would
|
|
231
|
+
* complain: both writers would build the one collection, each type’s search
|
|
232
|
+
* would return the other’s documents, and the membership sweep would delete
|
|
233
|
+
* across them. Exactly the drift deriving names is meant to end, so it fails at
|
|
234
|
+
* construction with the override that resolves it.
|
|
235
|
+
*
|
|
236
|
+
* Sharing a collection *deliberately* stays allowed – several label-source
|
|
237
|
+
* types served by one `labels` collection is a reasonable deployment – so this
|
|
238
|
+
* fires only when at least one side was derived, i.e. when nobody chose it.
|
|
239
|
+
*/
|
|
240
|
+
function assertNoAccidentalSharing(resolved) {
|
|
241
|
+
const byCollection = new Map();
|
|
242
|
+
for (const entry of resolved) {
|
|
243
|
+
byCollection.set(entry.collection, [
|
|
244
|
+
...(byCollection.get(entry.collection) ?? []),
|
|
245
|
+
entry,
|
|
246
|
+
]);
|
|
247
|
+
}
|
|
248
|
+
for (const [collection, entries] of byCollection) {
|
|
249
|
+
if (entries.length > 1 && entries.some((entry) => entry.derived)) {
|
|
250
|
+
const names = entries.map((entry) => `“${entry.searchType.name}”`);
|
|
251
|
+
throw new Error(`Search types ${names.join(' and ')} would share the Typesense collection “${collection}”, which no deployment asked for: their names derive to it. Give ${names.join(' or ')} an explicit collections entry.`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
211
255
|
/**
|
|
212
256
|
* Load a FULL label-source collection into a label map via the documents
|
|
213
257
|
* export endpoint, which streams every document as JSONL (one JSON object per
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lde/search-typesense",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Typesense implementation of the @lde/search SearchEngine port: collection-schema builder, query compiler, label-resolving result reconstruction, and blue/green index lifecycle. Engine-specific (Typesense) but domain-agnostic.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"url": "git+https://github.com/ldelements/lde.git",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"!**/*.tsbuildinfo"
|
|
26
26
|
],
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@lde/search": "^0.
|
|
28
|
+
"@lde/search": "^0.8.0",
|
|
29
29
|
"@lde/text-normalization": "^0.1.1",
|
|
30
30
|
"tslib": "^2.3.0",
|
|
31
31
|
"typesense": "^3.0.6"
|