@lde/search-typesense 0.2.0 → 0.3.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 +6 -0
- package/dist/search.d.ts +6 -2
- package/dist/search.d.ts.map +1 -1
- package/dist/search.js +79 -25
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -28,6 +28,12 @@ search/sort keys) matches what the projection writes, via
|
|
|
28
28
|
- reconstructs the logical `SearchResult` (`parseSearchResponse`) — language
|
|
29
29
|
maps, labelled references, labelled facet buckets.
|
|
30
30
|
|
|
31
|
+
`searchFacets` – the port’s batch entry point – answers a whole batch of
|
|
32
|
+
facet-only queries (e.g. a faceted sidebar’s skip-own-filter variants) as a
|
|
33
|
+
**single `multi_search` round-trip**, with one bundled label lookup shared by
|
|
34
|
+
every facet result in the batch. A failed entry is reported in place as a
|
|
35
|
+
per-query outcome, so its siblings’ facets survive.
|
|
36
|
+
|
|
31
37
|
The pure halves `buildSearchParams` and `parseSearchResponse` are exported for
|
|
32
38
|
direct use and testing.
|
|
33
39
|
|
package/dist/search.d.ts
CHANGED
|
@@ -35,8 +35,12 @@ export interface TypesenseSearchEngineOptions<TypeName extends string = string>
|
|
|
35
35
|
* ({@link buildSearchParams}), runs it against the type’s collection,
|
|
36
36
|
* resolves the reference labels for the page of hits from the sidecar
|
|
37
37
|
* `labels` collection in one lookup, and reconstructs the engine-neutral
|
|
38
|
-
* {@link SearchResult} ({@link parseSearchResponse}).
|
|
39
|
-
*
|
|
38
|
+
* {@link SearchResult} ({@link parseSearchResponse}). `searchFacets` answers
|
|
39
|
+
* a whole facet batch (e.g. a sidebar’s skip-own-filter query variants) as a
|
|
40
|
+
* single `multi_search` round-trip with one shared label lookup; a failed
|
|
41
|
+
* entry is reported in place as a per-query outcome, so it never discards
|
|
42
|
+
* its siblings’ facets. Every engine specific stays here; consumers see only
|
|
43
|
+
* logical documents.
|
|
40
44
|
*
|
|
41
45
|
* With a schema built by `searchSchema` over `defineSearchType` literals,
|
|
42
46
|
* `search()` accepts only the deployment’s own types and returns typo-safe
|
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,
|
|
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,EAGhB,MAAM,aAAa,CAAC;AAUrB,OAAO,EAGL,KAAK,wBAAwB,EAC9B,MAAM,qBAAqB,CAAC;AAE7B;;;;kCAIkC;AAClC,MAAM,WAAW,4BAA4B,CAC3C,QAAQ,SAAS,MAAM,GAAG,MAAM,CAChC,SAAQ,wBAAwB;IAChC;qEACiE;IACjE,QAAQ,CAAC,WAAW,EAAE,QAAQ,CAAC,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC;IACzD,kFAAkF;IAClF,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IACnC;;;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;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,2BAA2B,CACzC,KAAK,CAAC,KAAK,SAAS,SAAS,UAAU,EAAE,EAEzC,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,YAAY,CAAC,KAAK,CAAC,EAC3B,OAAO,EAAE,4BAA4B,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,CAAC,GAC3D,YAAY,CAAC,KAAK,CAAC,CAwKrB;AA+DD;;;;;;;;;;;;GAYG;AACH,wBAAsB,WAAW,CAC/B,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,aAAa,CAAC,EACnC,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,SAAS,MAAM,EAAE,GACtB,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC,CAmCtC;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,CAiCd"}
|
package/dist/search.js
CHANGED
|
@@ -8,8 +8,12 @@ import { buildSearchParams, escapeFilterValue, } from './query-compiler.js';
|
|
|
8
8
|
* ({@link buildSearchParams}), runs it against the type’s collection,
|
|
9
9
|
* resolves the reference labels for the page of hits from the sidecar
|
|
10
10
|
* `labels` collection in one lookup, and reconstructs the engine-neutral
|
|
11
|
-
* {@link SearchResult} ({@link parseSearchResponse}).
|
|
12
|
-
*
|
|
11
|
+
* {@link SearchResult} ({@link parseSearchResponse}). `searchFacets` answers
|
|
12
|
+
* a whole facet batch (e.g. a sidebar’s skip-own-filter query variants) as a
|
|
13
|
+
* single `multi_search` round-trip with one shared label lookup; a failed
|
|
14
|
+
* entry is reported in place as a per-query outcome, so it never discards
|
|
15
|
+
* its siblings’ facets. Every engine specific stays here; consumers see only
|
|
16
|
+
* logical documents.
|
|
13
17
|
*
|
|
14
18
|
* With a schema built by `searchSchema` over `defineSearchType` literals,
|
|
15
19
|
* `search()` accepts only the deployment’s own types and returns typo-safe
|
|
@@ -54,6 +58,37 @@ export function createTypesenseSearchEngine(client, schema, options) {
|
|
|
54
58
|
});
|
|
55
59
|
return inFlightLoad;
|
|
56
60
|
}
|
|
61
|
+
// Cached path: the once-loaded full collection serves labels by in-memory
|
|
62
|
+
// lookup (no per-search round-trip). The load does not depend on the
|
|
63
|
+
// response, so it is started BEFORE awaiting the search and runs alongside
|
|
64
|
+
// it; it never rejects (a failed load degrades to an empty map), so it
|
|
65
|
+
// cannot leave an unhandled rejection behind if the search itself fails.
|
|
66
|
+
// `undefined` when the cache (or the sidecar) is not configured.
|
|
67
|
+
function startCachedLabels() {
|
|
68
|
+
return options.labelsCollection !== undefined &&
|
|
69
|
+
options.labelCacheTtlMs !== undefined
|
|
70
|
+
? cachedAllLabels(options.labelsCollection, options.labelCacheTtlMs)
|
|
71
|
+
: undefined;
|
|
72
|
+
}
|
|
73
|
+
// Labels are supplementary: a failed lookup (e.g. the sidecar collection
|
|
74
|
+
// mid-rebuild) degrades to id-only references rather than failing the whole
|
|
75
|
+
// search, so the listing still renders with bare IRIs. `iris` is a thunk so
|
|
76
|
+
// the cached and no-sidecar paths never pay for collecting them.
|
|
77
|
+
async function resolveLabels(cachedLabelsPromise, iris) {
|
|
78
|
+
if (cachedLabelsPromise !== undefined) {
|
|
79
|
+
return cachedLabelsPromise;
|
|
80
|
+
}
|
|
81
|
+
if (options.labelsCollection === undefined) {
|
|
82
|
+
return new Map();
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
return await fetchLabels(client, options.labelsCollection, iris());
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
options.onLabelError?.(error);
|
|
89
|
+
return new Map();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
57
92
|
const engine = {
|
|
58
93
|
schema,
|
|
59
94
|
async search(searchType, query) {
|
|
@@ -63,35 +98,48 @@ export function createTypesenseSearchEngine(client, schema, options) {
|
|
|
63
98
|
assertTypeInSchema(schema, searchType);
|
|
64
99
|
assertValidQuery(query, searchType);
|
|
65
100
|
const params = buildSearchParams(query, searchType, options);
|
|
66
|
-
|
|
67
|
-
// lookup (no per-search round-trip). The load does not depend on the
|
|
68
|
-
// response, so it runs alongside the search; it never rejects (a failed
|
|
69
|
-
// load degrades to an empty map), so it cannot leave an unhandled
|
|
70
|
-
// rejection behind if the search itself fails.
|
|
71
|
-
const cachedLabelsPromise = options.labelsCollection !== undefined &&
|
|
72
|
-
options.labelCacheTtlMs !== undefined
|
|
73
|
-
? cachedAllLabels(options.labelsCollection, options.labelCacheTtlMs)
|
|
74
|
-
: undefined;
|
|
101
|
+
const cachedLabelsPromise = startCachedLabels();
|
|
75
102
|
const response = (await client
|
|
76
103
|
.collections(collections.get(searchType.type))
|
|
77
104
|
.documents()
|
|
78
105
|
.search(params));
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
106
|
+
const labels = await resolveLabels(cachedLabelsPromise, () => referenceIris(response, searchType));
|
|
107
|
+
return parseSearchResponse(response, searchType, labels);
|
|
108
|
+
},
|
|
109
|
+
async searchFacets(searchType, queries) {
|
|
110
|
+
assertTypeInSchema(schema, searchType);
|
|
111
|
+
for (const query of queries) {
|
|
112
|
+
assertValidQuery(query, searchType);
|
|
85
113
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
labels = await fetchLabels(client, options.labelsCollection, referenceIris(response, searchType));
|
|
89
|
-
}
|
|
90
|
-
catch (error) {
|
|
91
|
-
options.onLabelError?.(error);
|
|
92
|
-
}
|
|
114
|
+
if (queries.length === 0) {
|
|
115
|
+
return [];
|
|
93
116
|
}
|
|
94
|
-
|
|
117
|
+
const collection = collections.get(searchType.type);
|
|
118
|
+
const cachedLabelsPromise = startCachedLabels();
|
|
119
|
+
// The whole batch travels as ONE multi_search round-trip. Each query
|
|
120
|
+
// compiles as facet-only regardless of what it carries: no hits
|
|
121
|
+
// (per_page 0) and no ordering – nothing is transferred or sorted that
|
|
122
|
+
// this method cannot return.
|
|
123
|
+
const { results } = (await client.multiSearch.perform({
|
|
124
|
+
searches: queries.map((query) => ({
|
|
125
|
+
collection,
|
|
126
|
+
...buildSearchParams({ ...query, orderBy: [], limit: 0, offset: 0 }, searchType, options),
|
|
127
|
+
})),
|
|
128
|
+
}));
|
|
129
|
+
// One label lookup serves every successful facet result in the batch.
|
|
130
|
+
const responses = results.filter((result) => !('error' in result));
|
|
131
|
+
const labels = await resolveLabels(cachedLabelsPromise, () => [
|
|
132
|
+
...new Set(responses.flatMap((response) => referenceIris(response, searchType))),
|
|
133
|
+
]);
|
|
134
|
+
// multi_search reports a failed entry inline instead of rejecting the
|
|
135
|
+
// call; pass that through as a per-query outcome – naming the facets,
|
|
136
|
+
// not the position, since the batch order is the caller's internal –
|
|
137
|
+
// so one failed query never discards its siblings' facets.
|
|
138
|
+
return results.map((result, index) => 'error' in result
|
|
139
|
+
? {
|
|
140
|
+
error: new Error(`Typesense facet search for “${queries[index].facets.join('”, “')}” failed${result.code !== undefined ? ` (${result.code})` : ''}: ${result.error}`),
|
|
141
|
+
}
|
|
142
|
+
: { facets: parseSearchResponse(result, searchType, labels).facets });
|
|
95
143
|
},
|
|
96
144
|
};
|
|
97
145
|
// The runtime object is string-keyed; the literal-schema typing narrows it.
|
|
@@ -180,6 +228,12 @@ export async function fetchLabels(client, collection, iris) {
|
|
|
180
228
|
}
|
|
181
229
|
const { results } = (await client.multiSearch.perform({ searches }));
|
|
182
230
|
for (const result of results) {
|
|
231
|
+
// multi_search reports a failed entry inline instead of rejecting; throw
|
|
232
|
+
// so the caller's degradation path (onLabelError, id-only references)
|
|
233
|
+
// engages rather than mistaking the failure for absent labels.
|
|
234
|
+
if ('error' in result) {
|
|
235
|
+
throw new Error(`Typesense label lookup failed${result.code !== undefined ? ` (${result.code})` : ''}: ${result.error}`);
|
|
236
|
+
}
|
|
183
237
|
for (const hit of result.hits ?? []) {
|
|
184
238
|
labels.set(String(hit.document.id), labelToLocalizedValue(hit.document));
|
|
185
239
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lde/search-typesense",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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.3.0",
|
|
29
29
|
"@lde/text-normalization": "^0.1.1",
|
|
30
30
|
"tslib": "^2.3.0",
|
|
31
31
|
"typesense": "^3.0.6"
|