@lde/search 0.7.0 → 0.7.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 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 together with each type’s physical location, so a query can never
313
- meet the wrong index, and deployment-level concerns (the label cache,
314
- cross-type search, facet batching) have one home. A search names its type per
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
- const engine = createTypesenseSearchEngine(client, schema, {
322
- collections: { Dataset: 'datasets', Person: 'people' }, // omitting one = type error
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
@@ -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,31 @@
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
+ * Returns an empty array when `name` carries no alphanumerics at all; the
28
+ * adapter decides what that means, since only it knows its engine’s rules.
29
+ */
30
+ export declare function physicalNameTokens(searchType: SearchType): readonly string[];
31
+ //# 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;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,UAAU,GAAG,SAAS,MAAM,EAAE,CAK5E"}
@@ -0,0 +1,44 @@
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
+ * Returns an empty array when `name` carries no alphanumerics at all; the
28
+ * adapter decides what that means, since only it knows its engine’s rules.
29
+ */
30
+ export function physicalNameTokens(searchType) {
31
+ const words = searchType.name.match(NAME_WORD_PATTERN) ?? [];
32
+ const tokens = words.map((word) => word.toLowerCase());
33
+ const last = tokens.at(-1);
34
+ return last === undefined ? [] : [...tokens.slice(0, -1), pluralize(last)];
35
+ }
36
+ /**
37
+ * One word of a PascalCase/camelCase name, in precedence order: a run of
38
+ * capitals not followed by a lowercase letter (an acronym – `DCAT` in
39
+ * `DCATDataset`, `TV` in `TVSeries`); a capital starting an ordinary word; or a
40
+ * lowercase run. Anything else (a separator, punctuation) matches nothing and
41
+ * is dropped, so a name already written as `creative_work` or `creative-work`
42
+ * tokenizes the same way.
43
+ */
44
+ 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.7.0",
3
+ "version": "0.7.1",
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
  },