Skip to content

feat(search-indexer): ship the search indexer as a CLI and Docker image#655

Merged
ddeboer merged 3 commits into
feat/search-graphql-dockerfrom
feat/search-indexer
Jul 24, 2026
Merged

feat(search-indexer): ship the search indexer as a CLI and Docker image#655
ddeboer merged 3 commits into
feat/search-graphql-dockerfrom
feat/search-indexer

Conversation

@ddeboer

@ddeboer ddeboer commented Jul 24, 2026

Copy link
Copy Markdown
Member

Fix #652.

Stacked on #646 (feat/search-graphql-docker): it reuses that PR’s whole delivery pattern (ADR 15 image staging, docker:smoke CI gate, docker.yml) and its schema-module contract, so it targets that branch – three commits on top of it. Retarget to main once #646 lands.

What

@lde/search-indexer: the write-path counterpart of @lde/search-api-server – the object-grain search indexer as a Commander CLI, npm package and prebuilt Docker image (ghcr.io/ldelements/search-indexer), configured entirely from environment variables. Mount the same schema-declaration module into this image and the API-server image, point both at the same TYPESENSE_* coordinates, and the write and read sides cannot disagree about the schema.

  • Shared loader (feat(search)): the schema-module load-and-validate logic moves from search-api-server into a new @lde/search/module entry point; both composition packages boot from it. The server keeps validating its read-side optional exports (schemaOptions, engineOptions), with a new test for the non-object export failure mode.
  • Config, boot-or-fail (all problems in one error, mirroring ADR 15): registry endpoint + dataset IRIs (DATASETS) or registry criteria (DATASET_CRITERIA, JSON) → RegistrySelector; TYPESENSE_* (admin key); REBUILD_MODE in-place (default) / blue-green with optional COLLECTION_PREFIX; PROVENANCE_FILE + PIPELINE_VERSIONFileProvenanceStore; ConsoleReporter by default. Two combinations are rejected at boot: provenance with only one of its halves, and provenance with blue-green rebuilds (a skipped dataset would be missing from the fresh collection the swap makes live).
  • The QLever import path ships behind a flag (ADR 16): QLEVER_IMAGE enables ImportResolver + DockerTaskRunner – the indexer spawns and fully controls sibling QLever containers over the mounted Docker socket (dockerode, no docker CLI in the image). Default stays endpoint-only, which runs anywhere. Native mode in-image is the documented Kubernetes follow-up; the statically-declared-sidecar option is rejected in the ADR (no control plane for the per-dataset index/restart loop).
  • --check validates configuration and schema module then exits – used by tools/docker-smoke-indexer.sh to boot the built image in CI, so pruned-image module-resolution failures (ERR_MODULE_NOT_FOUND) fail the PR. The image needs npm ci --legacy-peer-deps for the same reason as the server image: the docker:lockfile repair step installs with that flag, so peers it leaves out (typesense’s @babel/runtime) would otherwise fail npm ci.
  • docker.yml generalized: the workflow now derives package name and version from the release tag (keeping the env-based, injection-safe ref handling) and serves both @lde/search-api-server@* and @lde/search-indexer@*.

Out of scope (per #652)

Bespoke root selectors and per-stage tuning (library path), SHACL as schema source (#495), engines other than Typesense, native-mode QLever (follow-up).

After merge

First publish needs the manual Trusted-Publishing bootstrap, like every new @lde/* package.

ddeboer added 3 commits July 24, 2026 14:30
- loadSchemaModule() loads and validates a mounted schema-declaration
  module (an .mjs default-exporting SearchType declarations as plain
  data) and returns the validated schema plus the raw exports, so each
  consumer validates its own optional exports
- Node-only, so it lives in a separate entry point like ./adapter and
  ./testing, keeping the main entry point runtime-agnostic
- The one loader both the indexer image and the served-API image boot
  from, so the write and the read side cannot disagree about the schema
…odule

- The generic load-and-validate logic moves to the shared loader; what
  stays here is validating the read side's optional exports
  (schemaOptions, engineOptions)
- Add a test for the non-object optional export failure mode
- New @lde/search-indexer package: a Commander CLI wrapping
  searchIndexerPipeline, configured entirely from environment variables
  (boot-or-fail, all problems reported in one error)
- Mounts the same schema-declaration module as the search-api-server
  image, loaded with the shared @lde/search/module loader
- Dataset selection against a DCAT registry: explicit IRIs (DATASETS),
  search criteria (DATASET_CRITERIA), or the whole registry
- Typesense rebuild writers per root type: in-place (default) or
  blue-green, with an optional derived-collection-name prefix
- Optional file provenance (PROVENANCE_FILE + PIPELINE_VERSION);
  rejected with blue-green rebuilds, which would drop skipped datasets
- Optional QLever import path behind QLEVER_IMAGE: data dumps are
  imported into pipeline-controlled sibling containers over the Docker
  socket (see ADR 16 for why a static QLever service cannot work)
- --check validates configuration and schema module, then exits; the
  Docker smoke test boots the image with it to catch pruned-image
  module-resolution failures in CI
- The docker.yml release workflow now derives the package from the tag,
  serving both search-api-server and search-indexer images
@ddeboer
ddeboer force-pushed the feat/search-indexer branch from 6cdfec3 to ac70566 Compare July 24, 2026 12:35
@ddeboer
ddeboer merged commit 7b779ae into feat/search-graphql-docker Jul 24, 2026
2 checks passed
@ddeboer
ddeboer deleted the feat/search-indexer branch July 24, 2026 13:08
ddeboer added a commit that referenced this pull request Jul 24, 2026
…nd Docker image (#646)

* feat(search): reject an unknown field kind at declaration time

- validateSearchType silently accepted a kind outside the FieldKind union:
  every kind-dependent rule passed vacuously, deferring the failure to
  whatever consumer read the declaration first
- guards the plain-JS declaration path (a mounted schema module, a SHACL
  generator) that the runtime validation exists for
- adds the 'unknown-kind' issue reason

* feat(search-api-server): serve the search API as a bootable process and Docker image

- new composition package binding the engine-agnostic search-api-graphql
  handler to a Typesense engine: environment config, a mounted plain-data
  schema-declaration module (validated at boot), and a node:http server
  answering /graphql, /health and a root redirect
- the search-api-server bin boots from environment variables alone and
  reports every configuration problem in one error
- the image is built from the workspace's own outputs, never from npm:
  docker:build (inferred by @nx/docker) stages the compiled package, the
  same-commit builds of its @lde/* dependencies (copy-workspace-modules)
  and a pruned lockfile (prune-lockfile plus an npm --package-lock-only
  repair step for the Nx pruning fallback); the Dockerfile is npm ci + COPY
- docker:smoke boots each affected image in CI with a fixture schema and
  probes /health and the SDL over HTTP, so runtime-only failures (the
  ERR_MODULE_NOT_FOUND class) cannot pass a green build
- each release tag rebuilds, smoke-tests and pushes
  ghcr.io/ldelements/search-api-server:<version>, independent of the npm
  publish
- the read-only image omits the write-side peers (@lde/pipeline,
  @lde/dataset): every runtime import is 'import type'
- recorded as ADR 15

* feat(search-indexer): ship the search indexer as a CLI and Docker image (#655)

* feat(search): add the schema-module loader as @lde/search/module

- loadSchemaModule() loads and validates a mounted schema-declaration
  module (an .mjs default-exporting SearchType declarations as plain
  data) and returns the validated schema plus the raw exports, so each
  consumer validates its own optional exports
- Node-only, so it lives in a separate entry point like ./adapter and
  ./testing, keeping the main entry point runtime-agnostic
- The one loader both the indexer image and the served-API image boot
  from, so the write and the read side cannot disagree about the schema

* refactor(search-api-server): load the schema module via @lde/search/module

- The generic load-and-validate logic moves to the shared loader; what
  stays here is validating the read side's optional exports
  (schemaOptions, engineOptions)
- Add a test for the non-object optional export failure mode

* feat(search-indexer): ship the search indexer as a CLI and Docker image

- New @lde/search-indexer package: a Commander CLI wrapping
  searchIndexerPipeline, configured entirely from environment variables
  (boot-or-fail, all problems reported in one error)
- Mounts the same schema-declaration module as the search-api-server
  image, loaded with the shared @lde/search/module loader
- Dataset selection against a DCAT registry: explicit IRIs (DATASETS),
  search criteria (DATASET_CRITERIA), or the whole registry
- Typesense rebuild writers per root type: in-place (default) or
  blue-green, with an optional derived-collection-name prefix
- Optional file provenance (PROVENANCE_FILE + PIPELINE_VERSION);
  rejected with blue-green rebuilds, which would drop skipped datasets
- Optional QLever import path behind QLEVER_IMAGE: data dumps are
  imported into pipeline-controlled sibling containers over the Docker
  socket (see ADR 16 for why a static QLever service cannot work)
- --check validates configuration and schema module, then exits; the
  Docker smoke test boots the image with it to catch pruned-image
  module-resolution failures in CI
- The docker.yml release workflow now derives the package from the tag,
  serving both search-api-server and search-indexer images
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant