@lde/search 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 +8 -0
- package/dist/engine.d.ts +30 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/testing.d.ts +2 -2
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +56 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -285,6 +285,14 @@ result.hits[0].document.title; // typed: only DATASET’s output fields are keys
|
|
|
285
285
|
await engine.search(OTHER_TYPE, query); // compile error: not in this schema
|
|
286
286
|
```
|
|
287
287
|
|
|
288
|
+
`searchFacets(type, queries)` is the port’s **batch entry point**: several
|
|
289
|
+
facet-only queries – e.g. a faceted sidebar’s skip-own-filter variants –
|
|
290
|
+
answered in one engine round-trip (Typesense: a single `multi_search`), one
|
|
291
|
+
outcome per query, positionally aligned – its facet map, or an in-place error,
|
|
292
|
+
so one failed query never discards its siblings’ facets. The same schema
|
|
293
|
+
binding, per-query validation and typed facet keys apply to every query in
|
|
294
|
+
the batch.
|
|
295
|
+
|
|
288
296
|
This only works when the types were declared with `defineSearchType` (or
|
|
289
297
|
captured `as const satisfies SearchType`) and composed with `searchSchema()`;
|
|
290
298
|
a plain `: SearchSchema` annotation widens gracefully to string keys.
|
package/dist/engine.d.ts
CHANGED
|
@@ -35,7 +35,37 @@ export interface SearchEngine<Types extends readonly SearchType[] = readonly Sea
|
|
|
35
35
|
* a caller can enumerate the searchable types. */
|
|
36
36
|
readonly schema: SearchSchema<Types>;
|
|
37
37
|
search<T extends Types[number]>(searchType: T, query: SearchQuery): Promise<SearchResult<FacetFieldsOf<T>, OutputFieldsOf<T>>>;
|
|
38
|
+
/**
|
|
39
|
+
* The batch entry point: answer several facet-only queries against one type
|
|
40
|
+
* in a single engine round-trip (where the engine supports one, e.g.
|
|
41
|
+
* Typesense `multi_search`), returning one {@link FacetsOutcome} per query,
|
|
42
|
+
* positionally aligned with `queries`. This is what keeps a faceted
|
|
43
|
+
* sidebar – one skip-own-filter query variant per filtered facet – from
|
|
44
|
+
* fanning out into per-facet engine calls. Only facet buckets come back: an
|
|
45
|
+
* engine answers every query in the batch facet-only (as if `limit: 0` and
|
|
46
|
+
* without ordering, whatever the query carries), so hits are never
|
|
47
|
+
* transferred. Reference-facet buckets carry resolved labels exactly as in
|
|
48
|
+
* {@link search}. A failure of one query is reported in place
|
|
49
|
+
* (`{ error }`) rather than by rejecting, so it never discards its
|
|
50
|
+
* siblings’ facets; the returned promise rejects only for batch-level
|
|
51
|
+
* failures (a foreign `searchType`, a structurally invalid query, the
|
|
52
|
+
* transport itself). The port contract holds for every query in the batch;
|
|
53
|
+
* an empty `queries` resolves to `[]` without touching the engine.
|
|
54
|
+
*/
|
|
55
|
+
searchFacets<T extends Types[number]>(searchType: T, queries: readonly SearchQuery[]): Promise<readonly FacetsOutcome<FacetFieldsOf<T>>[]>;
|
|
38
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* One query’s outcome in a {@link SearchEngine.searchFacets} batch – its
|
|
59
|
+
* facet buckets, or the error that failed it. The `allSettled`-style shape
|
|
60
|
+
* keeps one failed query (e.g. one failed `multi_search` entry) from
|
|
61
|
+
* discarding its siblings’ results: a caller degrades exactly the facets of
|
|
62
|
+
* the failed query. Discriminate with `'error' in outcome`.
|
|
63
|
+
*/
|
|
64
|
+
export type FacetsOutcome<FacetField extends string = string> = {
|
|
65
|
+
readonly facets: FacetMap<FacetField>;
|
|
66
|
+
} | {
|
|
67
|
+
readonly error: unknown;
|
|
68
|
+
};
|
|
39
69
|
/** What an engine returns: logical hits, a total, and the requested facets. */
|
|
40
70
|
export interface SearchResult<FacetField extends string = string, OutputField extends string = string> {
|
|
41
71
|
readonly hits: readonly SearchHit<OutputField>[];
|
package/dist/engine.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAC9C,OAAO,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE5D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,MAAM,WAAW,YAAY,CAC3B,KAAK,SAAS,SAAS,UAAU,EAAE,GAAG,SAAS,UAAU,EAAE;IAE3D;uDACmD;IACnD,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC,KAAK,CAAC,CAAC;IACrC,MAAM,CAAC,CAAC,SAAS,KAAK,CAAC,MAAM,CAAC,EAC5B,UAAU,EAAE,CAAC,EACb,KAAK,EAAE,WAAW,GACjB,OAAO,CAAC,YAAY,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -3,6 +3,6 @@ export type { SearchDocument } from './project.js';
|
|
|
3
3
|
export { defineSearchType, searchSchema, validateSearchType, assertValidSearchType, } from './schema.js';
|
|
4
4
|
export type { FieldKind, SearchField, SearchFieldBase, TextField, KeywordField, ReferenceField, NumericField, BooleanField, SearchType, SearchTypeIssue, SearchSchema, FacetRange, } from './schema.js';
|
|
5
5
|
export type { SearchQuery, Filter, Sort } from './query.js';
|
|
6
|
-
export type { SearchEngine, SearchResult, SearchHit, ResultDocument, SearchValue, LocalizedValue, Reference, FacetBucket, FacetMap, FacetFieldsOf, OutputFieldsOf, } from './engine.js';
|
|
6
|
+
export type { SearchEngine, SearchResult, SearchHit, ResultDocument, SearchValue, LocalizedValue, Reference, FacetBucket, FacetMap, FacetsOutcome, FacetFieldsOf, OutputFieldsOf, } from './engine.js';
|
|
7
7
|
export type { FramedNode } from './frame-by-type.js';
|
|
8
8
|
//# 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":"AASA,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAChF,YAAY,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAKnD,OAAO,EACL,gBAAgB,EAChB,YAAY,EACZ,kBAAkB,EAClB,qBAAqB,GACtB,MAAM,aAAa,CAAC;AACrB,YAAY,EACV,SAAS,EACT,WAAW,EACX,eAAe,EACf,SAAS,EACT,YAAY,EACZ,cAAc,EACd,YAAY,EACZ,YAAY,EACZ,UAAU,EACV,eAAe,EACf,YAAY,EACZ,UAAU,GACX,MAAM,aAAa,CAAC;AAIrB,YAAY,EAAE,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAI5D,YAAY,EACV,YAAY,EACZ,YAAY,EACZ,SAAS,EACT,cAAc,EACd,WAAW,EACX,cAAc,EACd,SAAS,EACT,WAAW,EACX,QAAQ,EACR,aAAa,EACb,cAAc,GACf,MAAM,aAAa,CAAC;AAErB,YAAY,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAChF,YAAY,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAKnD,OAAO,EACL,gBAAgB,EAChB,YAAY,EACZ,kBAAkB,EAClB,qBAAqB,GACtB,MAAM,aAAa,CAAC;AACrB,YAAY,EACV,SAAS,EACT,WAAW,EACX,eAAe,EACf,SAAS,EACT,YAAY,EACZ,cAAc,EACd,YAAY,EACZ,YAAY,EACZ,UAAU,EACV,eAAe,EACf,YAAY,EACZ,UAAU,GACX,MAAM,aAAa,CAAC;AAIrB,YAAY,EAAE,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAI5D,YAAY,EACV,YAAY,EACZ,YAAY,EACZ,SAAS,EACT,cAAc,EACd,WAAW,EACX,cAAc,EACd,SAAS,EACT,WAAW,EACX,QAAQ,EACR,aAAa,EACb,aAAa,EACb,cAAc,GACf,MAAM,aAAa,CAAC;AAErB,YAAY,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC"}
|
package/dist/testing.d.ts
CHANGED
|
@@ -5,8 +5,8 @@ import type { SearchEngine } from './engine.js';
|
|
|
5
5
|
* live instance of itself, so the port rules hold by test rather than by
|
|
6
6
|
* prose. Covers schema binding (a type outside the bound schema is rejected),
|
|
7
7
|
* the always-on query validation (a structurally invalid query is rejected
|
|
8
|
-
* before it reaches the engine) and the result shape of a browse query
|
|
9
|
-
* every type in the schema.
|
|
8
|
+
* before it reaches the engine) and the result shape of a browse query and a
|
|
9
|
+
* `searchFacets` batch – for every type in the schema.
|
|
10
10
|
*
|
|
11
11
|
* ```ts
|
|
12
12
|
* describeSearchEngineContract('TypesenseSearchEngine', () => engine);
|
package/dist/testing.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAahD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,4BAA4B,CAC1C,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,YAAY,GACzB,IAAI,CAsJN"}
|
package/dist/testing.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
import { filterOperatorFor, } from './query.js';
|
|
3
|
-
import { filterableFields } from './schema.js';
|
|
3
|
+
import { facetableFields, filterableFields, } from './schema.js';
|
|
4
4
|
/**
|
|
5
5
|
* The executable {@link SearchEngine} port contract (import from
|
|
6
6
|
* `@lde/search/testing`): a vitest suite every engine adapter runs against a
|
|
7
7
|
* live instance of itself, so the port rules hold by test rather than by
|
|
8
8
|
* prose. Covers schema binding (a type outside the bound schema is rejected),
|
|
9
9
|
* the always-on query validation (a structurally invalid query is rejected
|
|
10
|
-
* before it reaches the engine) and the result shape of a browse query
|
|
11
|
-
* every type in the schema.
|
|
10
|
+
* before it reaches the engine) and the result shape of a browse query and a
|
|
11
|
+
* `searchFacets` batch – for every type in the schema.
|
|
12
12
|
*
|
|
13
13
|
* ```ts
|
|
14
14
|
* describeSearchEngineContract('TypesenseSearchEngine', () => engine);
|
|
@@ -75,6 +75,59 @@ export function describeSearchEngineContract(name, engine) {
|
|
|
75
75
|
await expect(engine().search(searchType, query)).rejects.toThrow(/operator-mismatch/);
|
|
76
76
|
}
|
|
77
77
|
});
|
|
78
|
+
it('rejects a searchFacets batch for a type outside its schema', async () => {
|
|
79
|
+
const foreign = {
|
|
80
|
+
name: 'NotInSchema',
|
|
81
|
+
type: 'urn:test:not-in-schema',
|
|
82
|
+
fields: [],
|
|
83
|
+
};
|
|
84
|
+
await expect(engine().searchFacets(foreign, [browse(foreign)])).rejects.toThrow(/not in this engine/);
|
|
85
|
+
});
|
|
86
|
+
it('rejects a structurally invalid query anywhere in a searchFacets batch', async () => {
|
|
87
|
+
for (const searchType of types()) {
|
|
88
|
+
const queries = [
|
|
89
|
+
{ ...browse(searchType), limit: 0 },
|
|
90
|
+
{
|
|
91
|
+
...browse(searchType),
|
|
92
|
+
limit: 0,
|
|
93
|
+
facets: ['fieldThatDoesNotExist'],
|
|
94
|
+
},
|
|
95
|
+
];
|
|
96
|
+
await expect(engine().searchFacets(searchType, queries)).rejects.toThrow(/unknown-field/);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
it('answers a searchFacets batch with one facets outcome per query, positionally', async () => {
|
|
100
|
+
for (const searchType of types()) {
|
|
101
|
+
const facets = facetableFields(searchType).map((field) => field.name);
|
|
102
|
+
const queries = [
|
|
103
|
+
{ ...browse(searchType), limit: 0, facets },
|
|
104
|
+
// Facet-only regardless of the limit the query carries: this one
|
|
105
|
+
// keeps its non-zero browse limit and must be answered the same.
|
|
106
|
+
{ ...browse(searchType), facets: [] },
|
|
107
|
+
];
|
|
108
|
+
const outcomes = await engine().searchFacets(searchType, queries);
|
|
109
|
+
expect(outcomes).toHaveLength(queries.length);
|
|
110
|
+
for (const outcome of outcomes) {
|
|
111
|
+
// A valid query in a healthy engine yields facets, not an error.
|
|
112
|
+
expect('error' in outcome ? outcome.error : undefined).toBe(undefined);
|
|
113
|
+
if ('error' in outcome) {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
expect(outcome.facets).toBeTypeOf('object');
|
|
117
|
+
for (const buckets of Object.values(outcome.facets)) {
|
|
118
|
+
for (const bucket of buckets ?? []) {
|
|
119
|
+
expect(typeof bucket.value).toBe('string');
|
|
120
|
+
expect(typeof bucket.count).toBe('number');
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
it('resolves an empty searchFacets batch to an empty list', async () => {
|
|
127
|
+
for (const searchType of types()) {
|
|
128
|
+
await expect(engine().searchFacets(searchType, [])).resolves.toEqual([]);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
78
131
|
it('answers a browse query with hits, a total and facets', async () => {
|
|
79
132
|
for (const searchType of types()) {
|
|
80
133
|
const result = await engine().search(searchType, browse(searchType));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lde/search",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Engine- and domain-agnostic core for RDF-backed search: a unified declarative field model (SearchField/SearchType/SearchSchema), a neutral query IR, the SearchEngine port with logical result types, and a streaming CONSTRUCT-to-document projection. Bakes in no engine, protocol, or domain.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"url": "git+https://github.com/ldelements/lde.git",
|