@lde/search-typesense 0.8.0 → 0.9.1
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 +78 -17
- 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 +18 -6
- package/dist/collection-definition.d.ts +16 -9
- package/dist/collection-definition.d.ts.map +1 -1
- package/dist/collection-definition.js +32 -19
- package/dist/collection-name.d.ts +24 -0
- package/dist/collection-name.d.ts.map +1 -0
- package/dist/collection-name.js +55 -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 +16 -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 +11 -2
- package/dist/rebuild-support.d.ts.map +1 -1
- package/dist/rebuild-support.js +15 -4
- package/dist/search.d.ts +43 -21
- package/dist/search.d.ts.map +1 -1
- package/dist/search.js +87 -49
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -9,27 +9,87 @@ 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
|
|
15
|
-
Typesense collection from the unified `SearchField` model
|
|
71
|
+
`buildCollectionDefinition(searchType, { name?, defaultSortingField, … })` derives a
|
|
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
|
-
search/sort keys
|
|
18
|
-
`@lde/search`’s `physicalFields
|
|
74
|
+
search/sort keys, plus one regex display field per output text field) matches
|
|
75
|
+
what the projection writes, via `@lde/search`’s `physicalFields` and its display
|
|
76
|
+
helpers, so the index and the documents cannot drift.
|
|
19
77
|
|
|
20
78
|
**Memory lever.** Typesense keeps the index in RAM (with a raw copy of each
|
|
21
79
|
document on disk), so RAM tracks the _indexed_ surface – roughly 2–3× the size
|
|
22
80
|
of the fields you search, facet or sort on – not the full document.
|
|
23
|
-
`buildCollectionDefinition` keeps that surface minimal: the `output` display
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
81
|
+
`buildCollectionDefinition` keeps that surface minimal: the `output` display
|
|
82
|
+
labels land in one `index: false` regex field (`${name}_<lang>`, one value per
|
|
83
|
+
present language), stored on disk and fetched only for a hit, so they cost no RAM
|
|
84
|
+
and preserve every language; only the folded `*_search_${locale}`,
|
|
85
|
+
facet/reference and `*_sort_${locale}` companions are indexed. Keeping
|
|
86
|
+
retrieval-only fields un-indexed is the lever for holding a large index’s RAM
|
|
87
|
+
down.
|
|
88
|
+
|
|
89
|
+
`createTypesenseSearchEngine(client, schema, { collections? })` is the
|
|
30
90
|
`SearchEngine` implementation. Each search:
|
|
31
91
|
|
|
32
|
-
- validates the query against the search type (the port contract
|
|
92
|
+
- validates the query against the search type (the port contract – a
|
|
33
93
|
structurally invalid query is rejected, never sent);
|
|
34
94
|
- compiles it into Typesense search params (`buildSearchParams`);
|
|
35
95
|
- runs the search;
|
|
@@ -38,13 +98,13 @@ un-indexed is the lever for holding a large index’s RAM down.
|
|
|
38
98
|
sources bundled into a single lookup. A reference without a `labelSource`
|
|
39
99
|
stays id-only. With `labelCacheTtlMs` set, each label-source collection is
|
|
40
100
|
instead loaded once into an in-memory cache;
|
|
41
|
-
- reconstructs the logical `SearchResult` (`parseSearchResponse`)
|
|
101
|
+
- reconstructs the logical `SearchResult` (`parseSearchResponse`) – language
|
|
42
102
|
maps, labelled references, labelled facet buckets.
|
|
43
103
|
|
|
44
104
|
A label source is just another `SearchType` in the schema (with an `output`,
|
|
45
|
-
`searchable` text field called `label`)
|
|
46
|
-
|
|
47
|
-
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.
|
|
48
108
|
|
|
49
109
|
`searchFacets` – the port’s batch entry point – answers a whole batch of
|
|
50
110
|
facet-only queries (e.g. a faceted listing’s skip-own-filter variants) as a
|
|
@@ -95,7 +155,8 @@ const client = new Client({
|
|
|
95
155
|
apiKey,
|
|
96
156
|
});
|
|
97
157
|
|
|
98
|
-
|
|
158
|
+
// The collection is named from the type: `Dataset` → `datasets`.
|
|
159
|
+
const writer = new BlueGreenRebuild(client, DATASET);
|
|
99
160
|
// Standalone use; under @lde/pipeline the Pipeline drives this lifecycle.
|
|
100
161
|
const run = await writer.openRun(context);
|
|
101
162
|
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;IA4FjE,4EAA4E;YAC9D,WAAW;CAW1B"}
|
|
@@ -27,22 +27,34 @@ 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.name;
|
|
43
55
|
}
|
|
44
56
|
async openRun(context) {
|
|
45
|
-
const { name, batchSize, lockTtlMs, definitionOptions } =
|
|
57
|
+
const { name, batchSize, lockTtlMs, definitionOptions } = this.resolved;
|
|
46
58
|
return openLockedRun(this.client, name, lockTtlMs, async () => {
|
|
47
59
|
// Create the fresh (blue) collection up front, so a failure surfaces
|
|
48
60
|
// before any dataset is processed. startedAt orders the versioned names;
|
|
@@ -2,10 +2,12 @@ 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
|
-
* Unset, those fields are not stemmed
|
|
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
|
|
10
12
|
* their own locale. */
|
|
11
13
|
readonly defaultLocale?: string;
|
|
@@ -26,14 +28,19 @@ 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.
|
|
32
|
-
* This builder keeps that surface minimal: the `output` display labels
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* `*
|
|
36
|
-
* un-indexed is the lever
|
|
38
|
+
* This builder keeps that surface minimal: the `output` display labels land in
|
|
39
|
+
* a single `index: false` regex field (`${name}_<lang>`, one value per present
|
|
40
|
+
* language), kept on disk and read back only for a hit, so they cost no RAM;
|
|
41
|
+
* only the folded `*_search_${locale}`, facet/reference and `*_sort_${locale}`
|
|
42
|
+
* companions are indexed. Keeping retrieval-only fields un-indexed is the lever
|
|
43
|
+
* for holding a large index’s RAM down.
|
|
37
44
|
*/
|
|
38
|
-
export declare function buildCollectionDefinition(searchType: SearchType, options
|
|
45
|
+
export declare function buildCollectionDefinition(searchType: SearchType, options?: CollectionDefinitionOptions): CollectionCreateSchema;
|
|
39
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
|
-
import { physicalFields } from '@lde/search/adapter';
|
|
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,19 +12,24 @@ import { 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.
|
|
17
|
-
* This builder keeps that surface minimal: the `output` display labels
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* `*
|
|
21
|
-
* un-indexed is the lever
|
|
22
|
+
* This builder keeps that surface minimal: the `output` display labels land in
|
|
23
|
+
* a single `index: false` regex field (`${name}_<lang>`, one value per present
|
|
24
|
+
* language), kept on disk and read back only for a hit, so they cost no RAM;
|
|
25
|
+
* only the folded `*_search_${locale}`, facet/reference and `*_sort_${locale}`
|
|
26
|
+
* companions are indexed. Keeping retrieval-only fields un-indexed is the lever
|
|
27
|
+
* for holding a large index’s RAM down.
|
|
22
28
|
*/
|
|
23
|
-
export function buildCollectionDefinition(searchType, options) {
|
|
29
|
+
export function buildCollectionDefinition(searchType, options = {}) {
|
|
24
30
|
const { defaultLocale } = options;
|
|
25
31
|
const collection = {
|
|
26
|
-
name: options.name,
|
|
32
|
+
name: options.name ?? deriveCollectionName(searchType),
|
|
27
33
|
fields: searchType.fields.flatMap((field) => typesenseFields(field, defaultLocale, options.defaultSortingField)),
|
|
28
34
|
};
|
|
29
35
|
if (options.defaultSortingField !== undefined) {
|
|
@@ -39,17 +45,24 @@ function typesenseFields(field, defaultLocale, defaultSortingField) {
|
|
|
39
45
|
const names = physicalFields(field);
|
|
40
46
|
if (field.kind === 'text') {
|
|
41
47
|
const locales = field.locales;
|
|
48
|
+
const displayPattern = displayFieldPattern(field);
|
|
42
49
|
return [
|
|
43
|
-
// Display labels:
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
// Display labels: ONE regex field (`${name}_<lang>`) storing every
|
|
51
|
+
// present language’s value, NOT indexed (`index: false`) – search hits
|
|
52
|
+
// the folded `*_search` companions, so the display copies stay on disk and
|
|
53
|
+
// off RAM (fetched only for a hit), accents preserved, and a language
|
|
54
|
+
// outside `locales` still renders. This is the memory lever: RAM tracks
|
|
55
|
+
// the search surface, not the display text. Absent for a non-output field.
|
|
56
|
+
...(displayPattern !== undefined
|
|
57
|
+
? [
|
|
58
|
+
{
|
|
59
|
+
name: displayPattern,
|
|
60
|
+
type: 'string',
|
|
61
|
+
index: false,
|
|
62
|
+
optional: true,
|
|
63
|
+
},
|
|
64
|
+
]
|
|
65
|
+
: []),
|
|
53
66
|
// One folded search field per locale, each stemmed in its own
|
|
54
67
|
// language; the untagged `und` locale is folded but unstemmed unless
|
|
55
68
|
// the deployment opts in via `defaultLocale`.
|
|
@@ -83,7 +96,7 @@ function typesenseFields(field, defaultLocale, defaultSortingField) {
|
|
|
83
96
|
},
|
|
84
97
|
];
|
|
85
98
|
// `names.search` is non-empty exactly when the field projects a folded
|
|
86
|
-
// search companion
|
|
99
|
+
// search companion – physicalFields owns that rule.
|
|
87
100
|
for (const name of names.search) {
|
|
88
101
|
fields.push({
|
|
89
102
|
name,
|
|
@@ -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,CAanE"}
|
|
@@ -0,0 +1,55 @@
|
|
|
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
|
+
if (!NAMEABLE_TYPE_NAME.test(searchType.name)) {
|
|
25
|
+
throw new Error(`Cannot derive a Typesense collection name from search type “${searchType.name}”: it carries characters the convention would silently drop rather than name. Rename the type, or pass an explicit name.`);
|
|
26
|
+
}
|
|
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
|
+
* A type name this convention can name a collection after without silently
|
|
35
|
+
* losing part of it. {@link physicalNameTokens} matches ASCII words and drops
|
|
36
|
+
* everything else, which is what lets a name already written `creative_work` or
|
|
37
|
+
* `creative-work` tokenize like `CreativeWork` – but it would just as quietly
|
|
38
|
+
* turn `Café` into `cafs` and `Musée` into `mus_es`: legal names for the wrong
|
|
39
|
+
* collection, which is worse than no name at all. So a name is nameable only
|
|
40
|
+
* when it is ASCII words and word separators; anything else is rejected rather
|
|
41
|
+
* than mangled. An explicit name still overrides, since a deployment may
|
|
42
|
+
* legitimately index a type whose name this cannot spell.
|
|
43
|
+
*/
|
|
44
|
+
const NAMEABLE_TYPE_NAME = /^[A-Za-z0-9 _-]+$/;
|
|
45
|
+
/**
|
|
46
|
+
* What a *derived* name must look like. Typesense documents no collection-name
|
|
47
|
+
* rules, but a name travels in the URL path of every collection call
|
|
48
|
+
* (`/collections/:name`), so a character with URL meaning breaks that call
|
|
49
|
+
* rather than being rejected at creation – `#` and `+` silently strand a
|
|
50
|
+
* collection ({@link https://github.com/typesense/typesense/issues/548}). This
|
|
51
|
+
* charset is the safe subset {@link physicalNameTokens} can produce anyway, so
|
|
52
|
+
* the check is really a guard on the type name behind it: a `name` with no
|
|
53
|
+
* alphanumerics at all (tokens `[]`, name `''`) is the case that reaches it.
|
|
54
|
+
*/
|
|
55
|
+
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;IAqFjE;;;;;;OAMG;YACW,cAAc;CAsB7B"}
|
package/dist/in-place-rebuild.js
CHANGED
|
@@ -49,16 +49,26 @@ 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.name;
|
|
58
69
|
}
|
|
59
70
|
async openRun(context) {
|
|
60
|
-
const {
|
|
61
|
-
const { name, batchSize, lockTtlMs, definitionOptions } = resolveRebuildOptions(rebuildOptions);
|
|
71
|
+
const { name, batchSize, lockTtlMs, definitionOptions } = this.resolved;
|
|
62
72
|
return openLockedRun(this.client, name, lockTtlMs, async () => {
|
|
63
73
|
// Create the collection on demand: SearchType schema + the bookkeeping
|
|
64
74
|
// fields, `source` faceted so the membership sweep can enumerate the
|
|
@@ -101,7 +111,7 @@ export class InPlaceRebuild {
|
|
|
101
111
|
},
|
|
102
112
|
commit: async () => {
|
|
103
113
|
await importer.flush();
|
|
104
|
-
const departed = departedSources(await this.indexedSources(name, maxSweepableSources), context.selectedSources());
|
|
114
|
+
const departed = departedSources(await this.indexedSources(name, this.maxSweepableSources), context.selectedSources());
|
|
105
115
|
for (const filter of membershipSweepFilters(departed)) {
|
|
106
116
|
await deleteByFilter(this.client, name, filter);
|
|
107
117
|
}
|
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';
|
|
@@ -19,8 +19,17 @@ export interface ResolvedRebuildOptions {
|
|
|
19
19
|
readonly lockTtlMs: number;
|
|
20
20
|
readonly definitionOptions: CollectionDefinitionOptions;
|
|
21
21
|
}
|
|
22
|
-
/**
|
|
23
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Apply the shared defaults, once, so neither writer restates them – including
|
|
24
|
+
* the collection name, derived from the type ({@link deriveCollectionName})
|
|
25
|
+
* when the deployment supplies none. The resolved name is put back into
|
|
26
|
+
* `definitionOptions`, so the collection a writer talks to and the definition
|
|
27
|
+
* it creates are the same one name, resolved once.
|
|
28
|
+
*
|
|
29
|
+
* Writers resolve at construction rather than per run, so an underivable name
|
|
30
|
+
* throws when the writer is built, not on the first rebuild.
|
|
31
|
+
*/
|
|
32
|
+
export declare function resolveRebuildOptions(searchType: SearchType, options: RebuildOptions): ResolvedRebuildOptions;
|
|
24
33
|
/**
|
|
25
34
|
* Reject a {@link SearchType} that declares any of the bookkeeping field names
|
|
26
35
|
* 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,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,iBAAiB,EAAE,2BAA2B,CAAC;CACzD;AAED;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,CACnC,UAAU,EAAE,UAAU,EACtB,OAAO,EAAE,cAAc,GACtB,sBAAsB,CAaxB;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,24 @@
|
|
|
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. The resolved name is put back into
|
|
8
|
+
* `definitionOptions`, so the collection a writer talks to and the definition
|
|
9
|
+
* it creates are the same one name, resolved once.
|
|
10
|
+
*
|
|
11
|
+
* Writers resolve at construction rather than per run, so an underivable name
|
|
12
|
+
* throws when the writer is built, not on the first rebuild.
|
|
13
|
+
*/
|
|
14
|
+
export function resolveRebuildOptions(searchType, options) {
|
|
5
15
|
const { batchSize = DEFAULT_BATCH_SIZE, lockTtlMs = DEFAULT_LOCK_TTL_MS, ...definitionOptions } = options;
|
|
16
|
+
const name = definitionOptions.name ?? deriveCollectionName(searchType);
|
|
6
17
|
return {
|
|
7
|
-
name
|
|
18
|
+
name,
|
|
8
19
|
batchSize,
|
|
9
20
|
lockTtlMs,
|
|
10
|
-
definitionOptions,
|
|
21
|
+
definitionOptions: { ...definitionOptions, name },
|
|
11
22
|
};
|
|
12
23
|
}
|
|
13
24
|
/**
|
package/dist/search.d.ts
CHANGED
|
@@ -1,20 +1,25 @@
|
|
|
1
1
|
import type { Client } from 'typesense';
|
|
2
2
|
import { type LocalizedValue, type SearchEngine, type SearchResult, type SearchSchema, type SearchType, type TextField } from '@lde/search';
|
|
3
3
|
import { type BuildSearchParamsOptions } from './query-compiler.js';
|
|
4
|
-
/** Where the engine reads documents
|
|
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
|
-
* id-only references rather than failing. Optional
|
|
22
|
+
* id-only references rather than failing. Optional – omit to swallow silently.
|
|
18
23
|
*/
|
|
19
24
|
readonly onLabelError?: (error: unknown) => void;
|
|
20
25
|
/**
|
|
@@ -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
|
|
@@ -40,12 +60,13 @@ export interface LabelSource {
|
|
|
40
60
|
}
|
|
41
61
|
/**
|
|
42
62
|
* A Typesense-backed {@link SearchEngine}, bound to the whole
|
|
43
|
-
* {@link SearchSchema} at construction
|
|
44
|
-
* Each type’s collection
|
|
45
|
-
*
|
|
46
|
-
*
|
|
63
|
+
* {@link SearchSchema} at construction – like every other schema consumer.
|
|
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
|
|
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
|
-
* into one lookup
|
|
69
|
+
* into one lookup – and reconstructs the engine-neutral
|
|
49
70
|
* {@link SearchResult} ({@link parseSearchResponse}). `searchFacets` answers
|
|
50
71
|
* a whole facet batch (e.g. a faceted listing’s skip-own-filter query variants) as a
|
|
51
72
|
* single `multi_search` round-trip with one shared label lookup; a failed
|
|
@@ -55,22 +76,23 @@ export interface LabelSource {
|
|
|
55
76
|
*
|
|
56
77
|
* With a schema built by `searchSchema` over `defineSearchType` literals,
|
|
57
78
|
* `search()` accepts only the deployment’s own types and returns typo-safe
|
|
58
|
-
* facet/document keys per call
|
|
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;
|
|
64
85
|
readonly iris: readonly string[];
|
|
65
86
|
}
|
|
66
87
|
/**
|
|
67
|
-
* Resolve labels from each group’s label-source collection.
|
|
68
|
-
*
|
|
69
|
-
*
|
|
88
|
+
* Resolve labels from each group’s label-source collection. Labels are
|
|
89
|
+
* reconstructed from the source type’s `label` declaration ({@link
|
|
90
|
+
* localizedValue}), carrying every present language – including ones outside
|
|
91
|
+
* the declared `locales` and untagged (`und`) values.
|
|
70
92
|
*
|
|
71
93
|
* All groups travel as ONE `multi_search` (POST) call, each group’s id-list
|
|
72
94
|
* split over per-search batches: the id-list of a page or facet carrying many
|
|
73
|
-
* references
|
|
95
|
+
* references – e.g. a dataset with dozens of classes – would overflow
|
|
74
96
|
* Typesense’s GET query-string limit (4000 chars, and IRIs URL-encode to
|
|
75
97
|
* several times their length) if it travelled in the URL. POST puts it in the
|
|
76
98
|
* body; each batch stays under Typesense’s `per_page` cap, and bundling the
|
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;
|
|
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;AAG7B;;;;sDAIsD;AACtD,MAAM,WAAW,4BAA4B,CAC3C,QAAQ,SAAS,MAAM,GAAG,MAAM,CAChC,SAAQ,wBAAwB;IAChC;;;;;;;;OAQG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC;IACnE;;;OAGG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;IACjD;;;;;;;OAOG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;CACnC;AAED;;;GAGG;AACH,MAAM,WAAW,qBAAqB,CACpC,KAAK,SAAS,SAAS,UAAU,EAAE,GAAG,SAAS,UAAU,EAAE,CAC3D,SAAQ,YAAY,CAAC,KAAK,CAAC;IAC3B;;;;;;;OAOG;IACH,iBAAiB,CAAC,UAAU,EAAE,KAAK,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC;CACtD;AAED;;;;;GAKG;AACH,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,UAAU,EAAE,SAAS,CAAC;IAC/B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,2BAA2B,CACzC,KAAK,CAAC,KAAK,SAAS,SAAS,UAAU,EAAE,EAEzC,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,YAAY,CAAC,KAAK,CAAC,EAC3B,OAAO,GAAE,4BAA4B,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,CAAM,GAChE,qBAAqB,CAAC,KAAK,CAAC,CAgS9B;AAyCD,8EAA8E;AAC9E,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAC7B,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;CAClC;AA0FD;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,WAAW,CAC/B,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,aAAa,CAAC,EACnC,MAAM,EAAE,SAAS,gBAAgB,EAAE,EACnC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,GAC/B,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC,CAgDtC;AA6BD,oEAAoE;AACpE,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS;QAAE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,EAAE,CAAC;IAC1E,QAAQ,CAAC,YAAY,CAAC,EAAE,SAAS;QAC/B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;QAC5B,QAAQ,CAAC,MAAM,EAAE,SAAS;YACxB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;YACvB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;SACxB,EAAE,CAAC;KACL,EAAE,CAAC;CACL;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,uBAAuB,EACjC,UAAU,EAAE,UAAU,EACtB,MAAM,EAAE,WAAW,CAAC,MAAM,EAAE,cAAc,CAAC,GAC1C,YAAY,CAsCd"}
|
package/dist/search.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
import { assertTypeInSchema, assertValidQuery, fieldNamed, isRangeFacet, labelFieldOf, outputFields, physicalFields, referenceFields, } from '@lde/search/adapter';
|
|
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
|
-
* {@link SearchSchema} at construction
|
|
6
|
-
* Each type’s collection
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* {@link SearchSchema} at construction – like every other schema consumer.
|
|
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
|
|
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
|
-
* into one lookup
|
|
12
|
+
* into one lookup – and reconstructs the engine-neutral
|
|
11
13
|
* {@link SearchResult} ({@link parseSearchResponse}). `searchFacets` answers
|
|
12
14
|
* a whole facet batch (e.g. a faceted listing’s skip-own-filter query variants) as a
|
|
13
15
|
* single `multi_search` round-trip with one shared label lookup; a failed
|
|
@@ -17,22 +19,30 @@ import { buildSearchParams, escapeFilterValue, } from './query-compiler.js';
|
|
|
17
19
|
*
|
|
18
20
|
* With a schema built by `searchSchema` over `defineSearchType` literals,
|
|
19
21
|
* `search()` accepts only the deployment’s own types and returns typo-safe
|
|
20
|
-
* facet/document keys per call
|
|
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,10 +222,40 @@ 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
|
|
214
|
-
* line). Each line is reconstructed by {@link
|
|
258
|
+
* line). Each line is reconstructed by {@link localizedValue}, exactly as the
|
|
215
259
|
* per-search {@link fetchLabels} path does for its `multi_search` hits.
|
|
216
260
|
*/
|
|
217
261
|
async function loadAllLabels(client, source) {
|
|
@@ -225,7 +269,7 @@ async function loadAllLabels(client, source) {
|
|
|
225
269
|
continue;
|
|
226
270
|
}
|
|
227
271
|
const document = JSON.parse(line);
|
|
228
|
-
const label =
|
|
272
|
+
const label = localizedValue(document, source.labelField);
|
|
229
273
|
if (label !== undefined) {
|
|
230
274
|
labels.set(String(document.id), label);
|
|
231
275
|
}
|
|
@@ -286,13 +330,14 @@ function labelLookupGroups(responses, sources, outputSources) {
|
|
|
286
330
|
}));
|
|
287
331
|
}
|
|
288
332
|
/**
|
|
289
|
-
* Resolve labels from each group’s label-source collection.
|
|
290
|
-
*
|
|
291
|
-
*
|
|
333
|
+
* Resolve labels from each group’s label-source collection. Labels are
|
|
334
|
+
* reconstructed from the source type’s `label` declaration ({@link
|
|
335
|
+
* localizedValue}), carrying every present language – including ones outside
|
|
336
|
+
* the declared `locales` and untagged (`und`) values.
|
|
292
337
|
*
|
|
293
338
|
* All groups travel as ONE `multi_search` (POST) call, each group’s id-list
|
|
294
339
|
* split over per-search batches: the id-list of a page or facet carrying many
|
|
295
|
-
* references
|
|
340
|
+
* references – e.g. a dataset with dozens of classes – would overflow
|
|
296
341
|
* Typesense’s GET query-string limit (4000 chars, and IRIs URL-encode to
|
|
297
342
|
* several times their length) if it travelled in the URL. POST puts it in the
|
|
298
343
|
* body; each batch stays under Typesense’s `per_page` cap, and bundling the
|
|
@@ -331,7 +376,7 @@ export async function fetchLabels(client, groups, onError) {
|
|
|
331
376
|
return;
|
|
332
377
|
}
|
|
333
378
|
for (const hit of result.hits ?? []) {
|
|
334
|
-
const label =
|
|
379
|
+
const label = localizedValue(hit.document, groupPerSearch[index].source.labelField);
|
|
335
380
|
if (label !== undefined) {
|
|
336
381
|
labels.set(String(hit.document.id), label);
|
|
337
382
|
}
|
|
@@ -342,21 +387,6 @@ export async function fetchLabels(client, groups, onError) {
|
|
|
342
387
|
/** Typesense caps `per_page` at 250; the multi_search POST body holds the
|
|
343
388
|
* id-list comfortably, so resolve references in batches of this size. */
|
|
344
389
|
const LABEL_BATCH_SIZE = 200;
|
|
345
|
-
/**
|
|
346
|
-
* Reconstruct a label document into a language map via the source type’s
|
|
347
|
-
* `label` declaration (its per-locale display fields), with a bare untagged
|
|
348
|
-
* `label` value as the `und` fallback when no locale variant exists, or
|
|
349
|
-
* `undefined` when the document carries no usable label – so the reference
|
|
350
|
-
* stays id-only rather than gaining an empty label.
|
|
351
|
-
*/
|
|
352
|
-
function labelValue(document, labelField) {
|
|
353
|
-
const localized = localizedValue(document, labelField);
|
|
354
|
-
if (localized !== undefined) {
|
|
355
|
-
return localized;
|
|
356
|
-
}
|
|
357
|
-
const untagged = document[labelField.name];
|
|
358
|
-
return typeof untagged === 'string' ? { und: [untagged] } : undefined;
|
|
359
|
-
}
|
|
360
390
|
/** Merge per-collection label maps into one IRI-keyed map (URI identity). */
|
|
361
391
|
function mergeLabels(maps) {
|
|
362
392
|
if (maps.length === 1) {
|
|
@@ -383,7 +413,7 @@ export function parseSearchResponse(response, searchType, labels) {
|
|
|
383
413
|
document: reconstructDocument(hit.document, searchType, labels),
|
|
384
414
|
}));
|
|
385
415
|
// Reference facets are IRI-keyed; their buckets carry a resolved data label.
|
|
386
|
-
// Plain facets (tokens, free strings) carry no label
|
|
416
|
+
// Plain facets (tokens, free strings) carry no label – the consumer owns display.
|
|
387
417
|
// Only reference facets with a label source get bucket labels; an id-only
|
|
388
418
|
// reference facet stays id-only even when a cached full-collection map holds
|
|
389
419
|
// its IRIs.
|
|
@@ -447,16 +477,24 @@ function logicalValue(flat, field, labels) {
|
|
|
447
477
|
return flat[field.name] === true;
|
|
448
478
|
}
|
|
449
479
|
}
|
|
450
|
-
/**
|
|
480
|
+
/**
|
|
481
|
+
* Gather every present-language display field back into a language map. Display
|
|
482
|
+
* fields are pattern-based (`${name}_<lang>`, {@link displayLangOf}), so this
|
|
483
|
+
* recovers languages outside the declared `locales` too – a value tagged in an
|
|
484
|
+
* undeclared language, or untagged (`und`), still reconstructs rather than being
|
|
485
|
+
* dropped.
|
|
486
|
+
*/
|
|
451
487
|
function localizedValue(flat, field) {
|
|
452
488
|
const map = {};
|
|
453
|
-
const
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
if (typeof value === 'string') {
|
|
457
|
-
map[locale] = [value];
|
|
489
|
+
for (const [key, value] of Object.entries(flat)) {
|
|
490
|
+
if (typeof value !== 'string') {
|
|
491
|
+
continue;
|
|
458
492
|
}
|
|
459
|
-
|
|
493
|
+
const lang = displayLangOf(field, key);
|
|
494
|
+
if (lang !== undefined) {
|
|
495
|
+
map[lang] = [value];
|
|
496
|
+
}
|
|
497
|
+
}
|
|
460
498
|
return Object.keys(map).length > 0 ? map : undefined;
|
|
461
499
|
}
|
|
462
500
|
/** Map stored reference IRIs to labelled references; id-only when no label. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lde/search-typesense",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.1",
|
|
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.7.1",
|
|
29
29
|
"@lde/text-normalization": "^0.1.1",
|
|
30
30
|
"tslib": "^2.3.0",
|
|
31
31
|
"typesense": "^3.0.6"
|