@lde/search 0.7.0 → 0.8.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 -6
- package/dist/adapter.d.ts +1 -0
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js +1 -0
- package/dist/physical-name.d.ts +40 -0
- package/dist/physical-name.d.ts.map +1 -0
- package/dist/physical-name.js +72 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -309,18 +309,20 @@ unrepresentable; the port enforces them for everyone else (deployment
|
|
|
309
309
|
An engine is **bound to the whole `SearchSchema` at construction** – like
|
|
310
310
|
every other schema consumer (`projectGraph(quads, schema)`,
|
|
311
311
|
`buildGraphQLSchema(schema)`): the adapter factory takes the deployment’s
|
|
312
|
-
declaration
|
|
313
|
-
|
|
314
|
-
|
|
312
|
+
declaration, so a query can never meet the wrong index, and deployment-level
|
|
313
|
+
concerns (the label cache, cross-type search, facet batching) have one home.
|
|
314
|
+
Where each type physically lives is the **adapter’s** to decide – it derives a
|
|
315
|
+
collection/index name from the type, by its own engine’s naming conventions,
|
|
316
|
+
and a deployment only overrides that where it must. A search names its type per
|
|
315
317
|
call. Because `searchSchema()` captures the declared types as a literal
|
|
316
318
|
tuple, `search()` accepts **only the deployment’s own types** (a foreign type
|
|
317
319
|
is a compile error) and returns facet/document keys typed by the type passed
|
|
318
320
|
– no caller-side generics:
|
|
319
321
|
|
|
320
322
|
```ts
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
323
|
+
// No `collections`: each type reads the collection the adapter names it,
|
|
324
|
+
// which is the one its writer builds. Pass `collections` only to override.
|
|
325
|
+
const engine = createTypesenseSearchEngine(client, schema);
|
|
324
326
|
|
|
325
327
|
const result = await engine.search(DATASET, query);
|
|
326
328
|
result.facets.publisher; // typed: only DATASET’s facetable fields are keys
|
package/dist/adapter.d.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
export { assertTypeInSchema, physicalFields, displayFieldName, displayFieldPattern, displayLangOf, searchableFields, facetableFields, filterableFields, sortableFields, outputFields, referenceFields, fieldNamed, labelFieldOf, isRangeFacet, isoToUnixSeconds, unixSecondsToIso, } from './schema.js';
|
|
10
10
|
export type { PhysicalFields } from './schema.js';
|
|
11
|
+
export { physicalNameTokens } from './physical-name.js';
|
|
11
12
|
export { filterOperatorFor, filterOperator, validateQuery, assertValidQuery, pageForOffset, } from './query.js';
|
|
12
13
|
export type { FilterOperator, QueryIssue } from './query.js';
|
|
13
14
|
//# sourceMappingURL=adapter.d.ts.map
|
package/dist/adapter.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EACL,kBAAkB,EAClB,cAAc,EACd,gBAAgB,EAChB,mBAAmB,EACnB,aAAa,EACb,gBAAgB,EAChB,eAAe,EACf,gBAAgB,EAChB,cAAc,EACd,YAAY,EACZ,eAAe,EACf,UAAU,EACV,YAAY,EACZ,YAAY,EACZ,gBAAgB,EAChB,gBAAgB,GACjB,MAAM,aAAa,CAAC;AACrB,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,EACL,iBAAiB,EACjB,cAAc,EACd,aAAa,EACb,gBAAgB,EAChB,aAAa,GACd,MAAM,YAAY,CAAC;AACpB,YAAY,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC"}
|
|
1
|
+
{"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EACL,kBAAkB,EAClB,cAAc,EACd,gBAAgB,EAChB,mBAAmB,EACnB,aAAa,EACb,gBAAgB,EAChB,eAAe,EACf,gBAAgB,EAChB,cAAc,EACd,YAAY,EACZ,eAAe,EACf,UAAU,EACV,YAAY,EACZ,YAAY,EACZ,gBAAgB,EAChB,gBAAgB,GACjB,MAAM,aAAa,CAAC;AACrB,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,EACL,iBAAiB,EACjB,cAAc,EACd,aAAa,EACb,gBAAgB,EAChB,aAAa,GACd,MAAM,YAAY,CAAC;AACpB,YAAY,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC"}
|
package/dist/adapter.js
CHANGED
|
@@ -7,4 +7,5 @@
|
|
|
7
7
|
* this; the main entry point carries that (small) authoring surface.
|
|
8
8
|
*/
|
|
9
9
|
export { assertTypeInSchema, physicalFields, displayFieldName, displayFieldPattern, displayLangOf, searchableFields, facetableFields, filterableFields, sortableFields, outputFields, referenceFields, fieldNamed, labelFieldOf, isRangeFacet, isoToUnixSeconds, unixSecondsToIso, } from './schema.js';
|
|
10
|
+
export { physicalNameTokens } from './physical-name.js';
|
|
10
11
|
export { filterOperatorFor, filterOperator, validateQuery, assertValidQuery, pageForOffset, } from './query.js';
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { SearchType } from './schema.js';
|
|
2
|
+
/**
|
|
3
|
+
* The engine-neutral word tokens an adapter formats into the physical name of a
|
|
4
|
+
* {@link SearchType}’s container – a Typesense collection, an Elasticsearch or
|
|
5
|
+
* OpenSearch index: the type’s `name` split into words, lowercased, with the
|
|
6
|
+
* last word pluralized (`CreativeWork` → `['creative', 'works']`).
|
|
7
|
+
*
|
|
8
|
+
* Both engines document the same convention – name a container after the plural
|
|
9
|
+
* of what it holds – and differ only in how they join the words: Typesense
|
|
10
|
+
* snake_case (`blog_articles`), Elasticsearch/OpenSearch kebab-case
|
|
11
|
+
* (`blog-articles`). So the split and the inflection are shared here, and only
|
|
12
|
+
* the join and the engine’s own legality rules live in the adapter – which is
|
|
13
|
+
* why this returns tokens rather than a formatted name, and why it is spelled
|
|
14
|
+
* neutrally (‘container’, not ‘collection’ or ‘index’).
|
|
15
|
+
*
|
|
16
|
+
* Acronyms stay whole (`DCATDataset` → `['dcat', 'datasets']`, `TVSeries` →
|
|
17
|
+
* `['tv', 'series']`), which is why the split is hand-rolled rather than
|
|
18
|
+
* delegated to an inflector’s `underscore`/`tableize` – those split on every
|
|
19
|
+
* capital (`t_v_series`).
|
|
20
|
+
*
|
|
21
|
+
* Pluralization is a real inflector, so irregular and invariant nouns land the
|
|
22
|
+
* way the engines’ own docs write them (`Person` → `people`, `TVSeries` →
|
|
23
|
+
* `tv_series`, `Analysis` → `analyses`) rather than as the non-words regular
|
|
24
|
+
* rules produce (`serieses`, `analysises`). An already-plural name is
|
|
25
|
+
* idempotent (`People` → `people`).
|
|
26
|
+
*
|
|
27
|
+
* **Throws rather than name the wrong container.** Dropping what it cannot
|
|
28
|
+
* match is what lets `creative_work` tokenize like `CreativeWork`, but it would
|
|
29
|
+
* just as quietly turn `Café` into `['cafs']` and `Musée` into `['mus', 'es']`
|
|
30
|
+
* – a perfectly legal name for a container nobody meant. So a name is spellable
|
|
31
|
+
* only when it is ASCII words and word separators, and only when it leaves at
|
|
32
|
+
* least one word to name anything after; anything else throws here. This guard
|
|
33
|
+
* lives with the split it protects, not in each adapter: an adapter cannot
|
|
34
|
+
* derive a name without going through this function, so it cannot forget to
|
|
35
|
+
* check – and `SearchType.name` is otherwise unvalidated
|
|
36
|
+
* ({@link validateSearchType} rules on field names only). A deployment whose
|
|
37
|
+
* type name this cannot spell passes its adapter an explicit name instead.
|
|
38
|
+
*/
|
|
39
|
+
export declare function physicalNameTokens(searchType: SearchType): readonly string[];
|
|
40
|
+
//# sourceMappingURL=physical-name.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"physical-name.d.ts","sourceRoot":"","sources":["../src/physical-name.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE9C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,UAAU,GAAG,SAAS,MAAM,EAAE,CAiB5E"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { pluralize } from 'inflection';
|
|
2
|
+
/**
|
|
3
|
+
* The engine-neutral word tokens an adapter formats into the physical name of a
|
|
4
|
+
* {@link SearchType}’s container – a Typesense collection, an Elasticsearch or
|
|
5
|
+
* OpenSearch index: the type’s `name` split into words, lowercased, with the
|
|
6
|
+
* last word pluralized (`CreativeWork` → `['creative', 'works']`).
|
|
7
|
+
*
|
|
8
|
+
* Both engines document the same convention – name a container after the plural
|
|
9
|
+
* of what it holds – and differ only in how they join the words: Typesense
|
|
10
|
+
* snake_case (`blog_articles`), Elasticsearch/OpenSearch kebab-case
|
|
11
|
+
* (`blog-articles`). So the split and the inflection are shared here, and only
|
|
12
|
+
* the join and the engine’s own legality rules live in the adapter – which is
|
|
13
|
+
* why this returns tokens rather than a formatted name, and why it is spelled
|
|
14
|
+
* neutrally (‘container’, not ‘collection’ or ‘index’).
|
|
15
|
+
*
|
|
16
|
+
* Acronyms stay whole (`DCATDataset` → `['dcat', 'datasets']`, `TVSeries` →
|
|
17
|
+
* `['tv', 'series']`), which is why the split is hand-rolled rather than
|
|
18
|
+
* delegated to an inflector’s `underscore`/`tableize` – those split on every
|
|
19
|
+
* capital (`t_v_series`).
|
|
20
|
+
*
|
|
21
|
+
* Pluralization is a real inflector, so irregular and invariant nouns land the
|
|
22
|
+
* way the engines’ own docs write them (`Person` → `people`, `TVSeries` →
|
|
23
|
+
* `tv_series`, `Analysis` → `analyses`) rather than as the non-words regular
|
|
24
|
+
* rules produce (`serieses`, `analysises`). An already-plural name is
|
|
25
|
+
* idempotent (`People` → `people`).
|
|
26
|
+
*
|
|
27
|
+
* **Throws rather than name the wrong container.** Dropping what it cannot
|
|
28
|
+
* match is what lets `creative_work` tokenize like `CreativeWork`, but it would
|
|
29
|
+
* just as quietly turn `Café` into `['cafs']` and `Musée` into `['mus', 'es']`
|
|
30
|
+
* – a perfectly legal name for a container nobody meant. So a name is spellable
|
|
31
|
+
* only when it is ASCII words and word separators, and only when it leaves at
|
|
32
|
+
* least one word to name anything after; anything else throws here. This guard
|
|
33
|
+
* lives with the split it protects, not in each adapter: an adapter cannot
|
|
34
|
+
* derive a name without going through this function, so it cannot forget to
|
|
35
|
+
* check – and `SearchType.name` is otherwise unvalidated
|
|
36
|
+
* ({@link validateSearchType} rules on field names only). A deployment whose
|
|
37
|
+
* type name this cannot spell passes its adapter an explicit name instead.
|
|
38
|
+
*/
|
|
39
|
+
export function physicalNameTokens(searchType) {
|
|
40
|
+
const { name } = searchType;
|
|
41
|
+
if (!SPELLABLE_NAME.test(name)) {
|
|
42
|
+
throw new Error(`Cannot name search type “${name}”: a physical name is derived from the type’s name, and this one carries characters outside ASCII words and word separators – deriving would silently drop them and name the wrong container. Rename the type, or give the adapter an explicit name.`);
|
|
43
|
+
}
|
|
44
|
+
const tokens = (name.match(NAME_WORD_PATTERN) ?? []).map((word) => word.toLowerCase());
|
|
45
|
+
const last = tokens.at(-1);
|
|
46
|
+
if (last === undefined) {
|
|
47
|
+
throw new Error(`Cannot name search type “${name}”: its name carries no word to name a container after. Rename the type, or give the adapter an explicit name.`);
|
|
48
|
+
}
|
|
49
|
+
return [...tokens.slice(0, -1), pluralize(last)];
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* A name the split can spell without losing part of it: ASCII letters and
|
|
53
|
+
* digits, plus the separators a name may already be written with (`creative
|
|
54
|
+
* work`, `creative_work`, `creative-work` all tokenize like `CreativeWork`).
|
|
55
|
+
* Deliberately narrower than what {@link NAME_WORD_PATTERN} would tolerate –
|
|
56
|
+
* that one skips anything it does not match, which is precisely the silent
|
|
57
|
+
* mangling this rejects.
|
|
58
|
+
*
|
|
59
|
+
* Matches the empty name too, on purpose: nothing about it is *unspellable*,
|
|
60
|
+
* there is simply nothing to spell, which the no-word check below reports far
|
|
61
|
+
* better than a complaint about characters it does not carry.
|
|
62
|
+
*/
|
|
63
|
+
const SPELLABLE_NAME = /^[A-Za-z0-9 _-]*$/;
|
|
64
|
+
/**
|
|
65
|
+
* One word of a PascalCase/camelCase name, in precedence order: a run of
|
|
66
|
+
* capitals not followed by a lowercase letter (an acronym – `DCAT` in
|
|
67
|
+
* `DCATDataset`, `TV` in `TVSeries`); a capital starting an ordinary word; or a
|
|
68
|
+
* lowercase run. Anything else (a separator, punctuation) matches nothing and
|
|
69
|
+
* is dropped, so a name already written as `creative_work` or `creative-work`
|
|
70
|
+
* tokenizes the same way.
|
|
71
|
+
*/
|
|
72
|
+
const NAME_WORD_PATTERN = /[A-Z]+(?![a-z])|[A-Z][a-z0-9]*|[a-z0-9]+/g;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lde/search",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.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",
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
"@lde/text-normalization": "^0.1.1",
|
|
41
41
|
"@rdfjs/types": "^2.0.1",
|
|
42
42
|
"@tpluscode/rdf-ns-builders": "^5.0.0",
|
|
43
|
+
"inflection": "^3.0.2",
|
|
43
44
|
"jsonld": "^9.0.0",
|
|
44
45
|
"tslib": "^2.3.0"
|
|
45
46
|
},
|