@lde/search-indexer 0.27.1 → 0.28.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/dist/cli.js CHANGED
@@ -8,7 +8,7 @@ const program = new Command()
8
8
  .name('search-indexer')
9
9
  .description('Run the object-grain search indexer: select datasets, extract and project each root type per the mounted schema module, and rebuild the Typesense collections. Configuration comes from environment variables; see the README.')
10
10
  .version(version)
11
- .argument('[datasets...]', 'dataset IRIs to index (overrides the DATASETS environment variable)')
11
+ .argument('[datasets...]', 'dataset IRIs and file: URLs to index (overrides the DATASETS environment variable)')
12
12
  .option('--check', 'validate the configuration and schema module, then exit without indexing');
13
13
  program.parse();
14
14
  try {
@@ -3,7 +3,10 @@ import type { RootType, SearchDocument, SearchSchema } from '@lde/search';
3
3
  import { type QleverOptions } from '@lde/sparql-qlever';
4
4
  import type { Client } from 'typesense';
5
5
  import type { IndexerConfig } from './config.js';
6
- /** The registry-backed dataset selection the configuration describes. */
6
+ /**
7
+ * The dataset selection the configuration describes: what the registry selects,
8
+ * followed by the local files and directories it names.
9
+ */
7
10
  export declare function datasetSelectorFrom(config: IndexerConfig): DatasetSelector;
8
11
  /**
9
12
  * The engine-writer factory the configuration describes: an
@@ -1 +1 @@
1
- {"version":3,"file":"composition.d.ts","sourceRoot":"","sources":["../src/composition.ts"],"names":[],"mappings":"AAIA,OAAO,EAIL,KAAK,eAAe,EACpB,KAAK,oBAAoB,EACzB,KAAK,MAAM,EACZ,MAAM,eAAe,CAAC;AACvB,OAAO,KAAK,EACV,QAAQ,EACR,cAAc,EACd,YAAY,EAEb,MAAM,aAAa,CAAC;AAMrB,OAAO,EAAgB,KAAK,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACtE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AACxC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjD,yEAAyE;AACzE,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,aAAa,GAAG,eAAe,CAK1E;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,aAAa,GACpB,CAAC,UAAU,EAAE,QAAQ,EAAE,MAAM,EAAE,YAAY,KAAK,MAAM,CAAC,cAAc,CAAC,CAmBxE;AAED;;;;;;;;;GASG;AACH,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,aAAa,EACrB,oBAAoB,CAAC,EAAE,aAAa,CAAC,sBAAsB,CAAC,GAC3D,oBAAoB,GAAG,SAAS,CAmClC"}
1
+ {"version":3,"file":"composition.d.ts","sourceRoot":"","sources":["../src/composition.ts"],"names":[],"mappings":"AAKA,OAAO,EAOL,KAAK,eAAe,EACpB,KAAK,oBAAoB,EACzB,KAAK,MAAM,EACZ,MAAM,eAAe,CAAC;AACvB,OAAO,KAAK,EACV,QAAQ,EACR,cAAc,EACd,YAAY,EAEb,MAAM,aAAa,CAAC;AAMrB,OAAO,EAAgB,KAAK,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACtE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AACxC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjD;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,aAAa,GAAG,eAAe,CAiB1E;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,aAAa,GACpB,CAAC,UAAU,EAAE,QAAQ,EAAE,MAAM,EAAE,YAAY,KAAK,MAAM,CAAC,cAAc,CAAC,CAmBxE;AAED;;;;;;;;;GASG;AACH,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,aAAa,EACrB,oBAAoB,CAAC,EAAE,aAAa,CAAC,sBAAsB,CAAC,GAC3D,oBAAoB,GAAG,SAAS,CAmClC"}
@@ -1,16 +1,28 @@
1
1
  // The composition pieces of createSearchIndexer, each usable on its own so a
2
2
  // deployment that needs one bespoke stage keeps the rest of the wiring
3
3
  // (Pipeline keeps its own wiring private).
4
+ import { Dataset } from '@lde/dataset';
4
5
  import { Client as RegistryClient } from '@lde/dataset-registry-client';
5
- import { ImportResolver, RegistrySelector, SparqlDistributionResolver, } from '@lde/pipeline';
6
+ import { CombinedSelector, DirectorySelector, ImportResolver, ManualDatasetSelection, RegistrySelector, SparqlDistributionResolver, } from '@lde/pipeline';
6
7
  import { BlueGreenRebuild, deriveCollectionName, InPlaceRebuild, } from '@lde/search-typesense';
7
8
  import { createQlever } from '@lde/sparql-qlever';
8
- /** The registry-backed dataset selection the configuration describes. */
9
+ /**
10
+ * The dataset selection the configuration describes: what the registry selects,
11
+ * followed by the local files and directories it names.
12
+ */
9
13
  export function datasetSelectorFrom(config) {
10
- return new RegistrySelector({
11
- registry: new RegistryClient(config.registryEndpoint),
12
- criteria: config.datasetCriteria,
13
- });
14
+ const selectors = config.localSources.map((path) => path.endsWith('/')
15
+ ? new DirectorySelector(path)
16
+ : new ManualDatasetSelection([Dataset.localFile(path)]));
17
+ if (config.registryEndpoint) {
18
+ selectors.unshift(new RegistrySelector({
19
+ registry: new RegistryClient(config.registryEndpoint),
20
+ criteria: config.datasetCriteria,
21
+ }));
22
+ }
23
+ return selectors.length === 1
24
+ ? selectors[0]
25
+ : new CombinedSelector(selectors);
14
26
  }
15
27
  /**
16
28
  * The engine-writer factory the configuration describes: an
package/dist/config.d.ts CHANGED
@@ -9,8 +9,9 @@ export interface IndexerConfig {
9
9
  /** Path of the mounted schema-declaration module (`SCHEMA_MODULE`). */
10
10
  readonly schemaModulePath: string;
11
11
  /** SPARQL endpoint of the DCAT dataset registry the selection queries
12
- * (`REGISTRY_ENDPOINT`). */
13
- readonly registryEndpoint: URL;
12
+ * (`REGISTRY_ENDPOINT`). Undefined when `DATASETS` names only local data, so
13
+ * the run has no registry to read. */
14
+ readonly registryEndpoint?: URL;
14
15
  /**
15
16
  * Root types extracted from {@link registryEndpoint} instead of from each
16
17
  * dataset’s own distribution (`REGISTRY_ROOT_TYPES`, whitespace- or
@@ -24,6 +25,10 @@ export interface IndexerConfig {
24
25
  * search criteria (`DATASET_CRITERIA`), or every dataset when neither is
25
26
  * set. */
26
27
  readonly datasetCriteria: SearchCriteria;
28
+ /** Local data to index besides what the registry selects, from the `file:`
29
+ * URLs in `DATASETS`: paths of files, each one dataset, and of directories,
30
+ * which end in `/`, with one dataset per RDF file. */
31
+ readonly localSources: readonly string[];
27
32
  /** Where the Typesense engine writes (`TYPESENSE_*`). */
28
33
  readonly typesense: TypesenseConnection;
29
34
  /** How each type’s collection is rebuilt (`REBUILD_MODE`): update the live
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAEnE;;;;;GAKG;AACH,MAAM,WAAW,aAAa;IAC5B,uEAAuE;IACvE,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC;iCAC6B;IAC7B,QAAQ,CAAC,gBAAgB,EAAE,GAAG,CAAC;IAC/B;;;;;;;OAOG;IACH,QAAQ,CAAC,iBAAiB,EAAE,SAAS,MAAM,EAAE,CAAC;IAC9C;;eAEW;IACX,QAAQ,CAAC,eAAe,EAAE,cAAc,CAAC;IACzC,yDAAyD;IACzD,QAAQ,CAAC,SAAS,EAAE,mBAAmB,CAAC;IACxC;wEACoE;IACpE,QAAQ,CAAC,WAAW,EAAE,UAAU,GAAG,YAAY,CAAC;IAChD;wEACoE;IACpE,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IACnC;;;;;OAKG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC;oDACgD;IAChD,QAAQ,CAAC,UAAU,CAAC,EAAE,gBAAgB,CAAC;IACvC;;;iBAGa;IACb,QAAQ,CAAC,MAAM,CAAC,EAAE,YAAY,CAAC;CAChC;AAED,sEAAsE;AACtE,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;IACpC,uEAAuE;IACvE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED;sEACsE;AACtE,MAAM,WAAW,gBAAgB;IAC/B,sEAAsE;IACtE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,yEAAyE;IACzE,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;CAClC;AAED;;;GAGG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB,sEAAsE;IACtE,QAAQ,CAAC,QAAQ,EAAE,QAAQ,GAAG,0BAA0B,GAAG,QAAQ,CAAC;IACpE,8DAA8D;IAC9D,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B,GAAG,CACA;IACE,qEAAqE;IACrE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IACxB,mEAAmE;IACnE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB;;;iDAG6C;IAC7C,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B,GACD;IACE;8DAC0D;IAC1D,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;CACzB,CACJ,CAAC;AAUF;;;;GAIG;AACH,wBAAgB,qBAAqB,CACnC,WAAW,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,GACxD,aAAa,CAsFf"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAInE;;;;;GAKG;AACH,MAAM,WAAW,aAAa;IAC5B,uEAAuE;IACvE,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC;;2CAEuC;IACvC,QAAQ,CAAC,gBAAgB,CAAC,EAAE,GAAG,CAAC;IAChC;;;;;;;OAOG;IACH,QAAQ,CAAC,iBAAiB,EAAE,SAAS,MAAM,EAAE,CAAC;IAC9C;;eAEW;IACX,QAAQ,CAAC,eAAe,EAAE,cAAc,CAAC;IACzC;;2DAEuD;IACvD,QAAQ,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;IACzC,yDAAyD;IACzD,QAAQ,CAAC,SAAS,EAAE,mBAAmB,CAAC;IACxC;wEACoE;IACpE,QAAQ,CAAC,WAAW,EAAE,UAAU,GAAG,YAAY,CAAC;IAChD;wEACoE;IACpE,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IACnC;;;;;OAKG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC;oDACgD;IAChD,QAAQ,CAAC,UAAU,CAAC,EAAE,gBAAgB,CAAC;IACvC;;;iBAGa;IACb,QAAQ,CAAC,MAAM,CAAC,EAAE,YAAY,CAAC;CAChC;AAED,sEAAsE;AACtE,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;IACpC,uEAAuE;IACvE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED;sEACsE;AACtE,MAAM,WAAW,gBAAgB;IAC/B,sEAAsE;IACtE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,yEAAyE;IACzE,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;CAClC;AAED;;;GAGG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB,sEAAsE;IACtE,QAAQ,CAAC,QAAQ,EAAE,QAAQ,GAAG,0BAA0B,GAAG,QAAQ,CAAC;IACpE,8DAA8D;IAC9D,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B,GAAG,CACA;IACE,qEAAqE;IACrE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IACxB,mEAAmE;IACnE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB;;;iDAG6C;IAC7C,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B,GACD;IACE;8DAC0D;IAC1D,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;CACzB,CACJ,CAAC;AAUF;;;;GAIG;AACH,wBAAgB,qBAAqB,CACnC,WAAW,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,GACxD,aAAa,CAkGf"}
package/dist/config.js CHANGED
@@ -1,3 +1,6 @@
1
+ import { Dataset } from '@lde/dataset';
2
+ import { dirname } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
1
4
  const QLEVER_MODES = ['docker', 'remote'];
2
5
  const IMPORT_STRATEGIES = [
3
6
  'sparql',
@@ -43,7 +46,13 @@ export function configFromEnvironment(environment) {
43
46
  return new URL('http://invalid.invalid');
44
47
  }
45
48
  };
46
- const registryEndpoint = url('REGISTRY_ENDPOINT', required('REGISTRY_ENDPOINT'));
49
+ const datasets = names(environment['DATASETS']);
50
+ const registryEndpoint = datasets.length > 0 && datasets.every(isLocal)
51
+ ? undefined
52
+ : url('REGISTRY_ENDPOINT', required('REGISTRY_ENDPOINT'));
53
+ if (registryEndpoint?.protocol === 'file:') {
54
+ problems.push('REGISTRY_ENDPOINT must be a SPARQL endpoint: name local files and directories in DATASETS');
55
+ }
47
56
  const protocol = environment['TYPESENSE_PROTOCOL'] ?? 'http';
48
57
  if (protocol !== 'http' && protocol !== 'https') {
49
58
  problems.push(`TYPESENSE_PROTOCOL must be “http” or “https”, got “${protocol}”`);
@@ -52,14 +61,16 @@ export function configFromEnvironment(environment) {
52
61
  if (rebuildMode !== 'in-place' && rebuildMode !== 'blue-green') {
53
62
  problems.push(`REBUILD_MODE must be “in-place” or “blue-green”, got “${rebuildMode}”`);
54
63
  }
55
- const datasetCriteria = criteriaFromEnvironment(environment, problems);
64
+ const datasetCriteria = criteriaFromEnvironment(environment, datasets, problems);
56
65
  const provenance = provenanceFromEnvironment(environment, rebuildMode, problems);
57
66
  const qlever = qleverFromEnvironment(environment, problems);
67
+ const localSources = localSourcesFrom(datasets, qlever, problems);
58
68
  const config = {
59
69
  schemaModulePath: environment['SCHEMA_MODULE'] ?? '/config/search-schema.mjs',
60
70
  registryEndpoint,
61
71
  registryRootTypes: names(environment['REGISTRY_ROOT_TYPES']),
62
72
  datasetCriteria,
73
+ localSources,
63
74
  typesense: {
64
75
  host: required('TYPESENSE_HOST'),
65
76
  port: integer('TYPESENSE_PORT', 8108),
@@ -87,17 +98,53 @@ function names(value) {
87
98
  ? []
88
99
  : value.split(/[\s,]+/).filter((name) => name.length > 0);
89
100
  }
90
- /** `DATASETS` (whitespace-separated IRIs) or `DATASET_CRITERIA` (a JSON
91
- * object) – never both; neither selects the whole registry. */
92
- function criteriaFromEnvironment(environment, problems) {
93
- const datasets = environment['DATASETS'];
101
+ /** Whether a `DATASETS` entry names local data rather than a registered dataset.
102
+ * Parsed, because a URL scheme is case-insensitive. */
103
+ function isLocal(entry) {
104
+ return URL.parse(entry)?.protocol === 'file:';
105
+ }
106
+ /** The paths of the `file:` URLs in `DATASETS`. Only QLever can read a file, so
107
+ * they require the import path. */
108
+ function localSourcesFrom(datasets, qlever, problems) {
109
+ const entries = datasets.filter(isLocal);
110
+ if (entries.length > 0 && !qlever) {
111
+ problems.push('A file: URL in DATASETS requires QLEVER_IMAGE or QLEVER_MODE=remote: a file must be imported before it can be queried');
112
+ }
113
+ const paths = entries.flatMap((entry) => {
114
+ try {
115
+ const path = fileURLToPath(entry);
116
+ if (!path.endsWith('/')) {
117
+ Dataset.localFile(path);
118
+ }
119
+ return [path];
120
+ }
121
+ catch (error) {
122
+ problems.push(`DATASETS contains “${entry}”, which is not a local RDF file or a directory ending in /: ${error.message}`);
123
+ return [];
124
+ }
125
+ });
126
+ const directories = new Set(paths.filter((path) => path.endsWith('/')));
127
+ paths.forEach((path, index) => {
128
+ if (paths.indexOf(path) !== index) {
129
+ problems.push(`DATASETS lists “${path}” twice`);
130
+ }
131
+ else if (!path.endsWith('/') && directories.has(`${dirname(path)}/`)) {
132
+ problems.push(`DATASETS lists “${path}” and its directory, which already selects it`);
133
+ }
134
+ });
135
+ return paths;
136
+ }
137
+ /** `DATASETS` (whitespace-separated IRIs, of which `file:` URLs are local data
138
+ * rather than registered datasets) or `DATASET_CRITERIA` (a JSON object) –
139
+ * never both; neither selects the whole registry. */
140
+ function criteriaFromEnvironment(environment, datasets, problems) {
94
141
  const criteriaJson = environment['DATASET_CRITERIA'];
95
- if (datasets && criteriaJson) {
142
+ if (environment['DATASETS'] && criteriaJson) {
96
143
  problems.push('DATASETS and DATASET_CRITERIA are mutually exclusive; set at most one');
97
144
  return {};
98
145
  }
99
- if (datasets) {
100
- const iris = datasets.split(/[\s,]+/).filter((iri) => iri.length > 0);
146
+ if (environment['DATASETS']) {
147
+ const iris = datasets.filter((iri) => !isLocal(iri));
101
148
  for (const iri of iris) {
102
149
  try {
103
150
  new URL(iri);
package/dist/indexer.js CHANGED
@@ -34,7 +34,7 @@ export async function createSearchIndexer(config, extensions = {}) {
34
34
  datasets: datasetSelectorFrom(config),
35
35
  distributionResolver: distributionResolverFrom(config, extensions.replaceJsonLdContext),
36
36
  writerFor: writerFactoryFrom(client, config),
37
- registryTypes: config.registryRootTypes.length > 0
37
+ registryTypes: config.registryEndpoint && config.registryRootTypes.length > 0
38
38
  ? {
39
39
  endpoint: config.registryEndpoint,
40
40
  names: config.registryRootTypes,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lde/search-indexer",
3
- "version": "0.27.1",
3
+ "version": "0.28.0",
4
4
  "description": "The @lde/search-pipeline indexer as a bootable process and prebuilt Docker image: loads a mounted schema-declaration module, dataset selection, Typesense connection and rebuild mode from the environment, then runs the object-grain searchIndexerPipeline once to completion. The composition layer that binds the engine-agnostic @lde/search-pipeline stages to @lde/search-typesense – the write-path counterpart of @lde/search-api-server.",
5
5
  "repository": {
6
6
  "url": "git+https://github.com/ldelements/lde.git",
@@ -28,13 +28,14 @@
28
28
  "!**/*.tsbuildinfo"
29
29
  ],
30
30
  "dependencies": {
31
- "@lde/dataset-registry-client": "^0.11.0",
32
- "@lde/pipeline": "^0.40.1",
33
- "@lde/pipeline-console-reporter": "^0.31.1",
31
+ "@lde/dataset": "^0.10.0",
32
+ "@lde/dataset-registry-client": "^0.12.0",
33
+ "@lde/pipeline": "^0.41.0",
34
+ "@lde/pipeline-console-reporter": "^0.32.0",
34
35
  "@lde/search": "^0.34.0",
35
- "@lde/search-pipeline": "^0.37.1",
36
- "@lde/search-typesense": "^0.40.1",
37
- "@lde/sparql-qlever": "^0.19.0",
36
+ "@lde/search-pipeline": "^0.38.0",
37
+ "@lde/search-typesense": "^0.41.0",
38
+ "@lde/sparql-qlever": "^0.20.0",
38
39
  "commander": "^15.0.0",
39
40
  "tslib": "^2.3.0",
40
41
  "typesense": "^3.1.1"