Skip to content

Commerce recommendations

An internal, multi-property item-to-item recommendation service. It streams catalog, view, online-purchase, and optionally in-store/offline-purchase rows directly from configured relational databases, trains eleven batch strategies, atomically publishes immutable snapshots, and serves pre-ranked results. A default-off additive capability can rerank bounded candidates for an authorized Shopper without changing the underlying snapshot.

Implemented strategies:

  • Frequently Bought Together: Order-level co-purchase with regularized cosine, followed by complementary category and purchase-popularity fallbacks. Metadata substitutes are excluded.
  • Also Viewed: Session-level co-view with regularized cosine, followed by metadata similarity, category views, and property views.
  • Similar Items: sparse metadata TF-IDF/cosine.
  • Best Sellers, Most Viewed, and Trending: time-decayed global rankings.
  • Five category-constrained anchored strategies: popular viewed/bought in the same category, frequent bought in a different category, and frequent viewed in the same/different category.

Configured Swimlanes accumulate strategy/geographic variations in order, then use authorized recent view history to fill remaining slots. Separately enabled personalization can rerank candidates; opt-in embedded ANN can retrieve snapshot-local Similar Items and For You candidates using metadata SVD or an aggregate-trained two-tower representation. These paths still never query merchant data while serving.

The approved product specification is specs/commerce-recommendation-service.md; the implementation design is design/commerce-recommendation-service.md. The implementation-as-built reference, including UML sequence diagrams, class diagrams, durable and ephemeral ERDs, algorithms, lifecycle, failure handling, and operations, is docs/technical-implementation.md. Use docs/index.md to navigate the complete documentation set and docs/command-reference.md for the commands needed to build, test, simulate, generate data, benchmark, and deploy documentation. Use AGENTS.md for the repository change and verification contract. For a start-to-finish explanation of the repository, including code excerpts and the rationale behind its boundaries and integrations, read the Commerce Recommendations repository guide. For an experienced engineer's explanation of the current choices and their costs, start with engineering rationale, then the implementation workflow. These distinguish recorded decisions, code-derived explanations, and implementation limitations.

A reusable request-rate and concurrency admission-control foundation is described in design/serving-admission-control.md. It is optional and disabled in the recommendation API unless a policy controller and trusted context factory are provided; its in-memory adapter is not a distributed-capacity claim.

Runtime shape

Run the API and worker as separate processes against one service-owned PostgreSQL database. The API creates/inspects jobs, serves snapshots, and, when explicitly enabled, accepts ordered Shopper interactions and performs bounded personalization. The worker streams each configured source inside a consistent relational transaction and also performs bounded personalization expiry and deletion reconciliation. Raw source interaction rows are never exported or staged; bounded group state is reduced into ephemeral derived aggregates using DuckDB (default) or Polars.

Select the complete aggregation engine in the deployment JSON:

{
  "pipeline": {"aggregation_backend": "polars"}
}

Use "duckdb" or omit aggregation_backend to retain the default. Polars computes co-occurrence, popularity, behavioral scoring, and evaluation aggregates over temporary derived-only Parquet; source adapters, metadata similarity, publication, and serving keep their existing contracts. See training backend operations for the different thread and memory controls.

Python 3.12–3.14 is supported. The supported local bootstrap uses uv and expects the sibling ../telemetry checkout pinned by the repository workflow.

make setup
make doctor
export RECOMMENDATIONS_CONTROL_DATABASE_URL='postgresql+psycopg://...'
export RECOMMENDATIONS_DATA_SOURCES_FILE="$PWD/config/data_sources.json"
export MERCHANT_PLATFORM_DATABASE_URL='postgresql+psycopg://...'
uv run --frozen alembic upgrade head
uv run --frozen recommendations-api

In another process:

uv run --frozen recommendations-worker

Observability runtime

Application observability is disabled by default and the base installation does not import or require OpenTelemetry. The reusable runtime and Collector assets live in the sibling telemetry repository; this project retains the recommendation-owned metric/event schema and domain adapter. The backend-neutral dashboards, alerts, fixtures, and response procedures are defined in operations/recommendations.toml and docs/recommendations-operations.md. Install .[observability,postgresql] to enable the process-owned runtime, then configure the strict observability group shown in config/data_sources.example.json. API and worker processes send enabled traces, metrics, and structured logs to one OTLP Collector endpoint; vendor/backend selection and credentials remain Collector-only concerns.

Non-secret precedence is code defaults, deployment JSON, then the allowlisted RECOMMENDATIONS_OBSERVABILITY_ENABLED, RECOMMENDATIONS_DEPLOYMENT_ENVIRONMENT, OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_PROTOCOL, and OTEL_EXPORTER_OTLP_CERTIFICATE overrides. OTLP header and client-key values must use environment or file references in JSON. Secret values are resolved only during runtime construction and are excluded from configuration fingerprints, representations, errors, logs, and metrics. Insecure transport is accepted only for local or test; enabled production-like environments require TLS. Configuration changes take effect on process restart, not hot reload.

For local development, uv resolves telemetry from ../telemetry through the editable source in pyproject.toml. CI checks out a pinned telemetry commit at that sibling path so locked resolution is reproducible. A published distribution declares the compatible telemetry version range and must resolve an independently published telemetry wheel rather than the local source override.

Commerce Simulation Harness with Docker Compose

Build the shared image and start the complete local simulation stack—separate control and simulation PostgreSQL databases, migrations and seed initialization, API, worker, and browsable storefront—with:

docker compose up --build

The image consumes the sibling telemetry checkout as a named build context. A direct build uses the equivalent command docker build --build-context telemetry=../telemetry ..

The API is available at http://127.0.0.1:8000 and the Simulation Storefront at http://127.0.0.1:8080. Compose mounts config/simulation-data-source.json read-only, initializes a fixed local/test-only Commerce Scope, and uses named volumes for both databases, worker scratch, and authoritative evidence reports. The complete workflow and safety boundary are documented in docs/commerce-simulation-harness.md.

Run the default deterministic independent-behavior journey through the same storefront HTTP actions available to a human browser with:

docker compose --profile runner run --rm simulation-runner

The resulting evidence describes only the synthetic scenario. It is not evidence of real business outcomes, causal uplift, production capacity, or production scalability.

To use Compose with a separate merchant-like source instead of the simulation defaults, copy the example source configuration, set RECOMMENDATIONS_DATA_SOURCES_FILE_HOST, and expose the URL named by that file's connection_url_env setting. For example:

Before submitting a Training Run, point the example source configuration at a relational source that contains the configured tables:

export RECOMMENDATIONS_DATA_SOURCES_FILE_HOST=./config/data_sources.json
export MERCHANT_PLATFORM_DATABASE_URL='postgresql+psycopg://user:password@host/source'
docker compose up --build

Override POSTGRES_DB, POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_PORT, SIMULATION_POSTGRES_PORT, RECOMMENDATIONS_API_PORT, or SIMULATION_STOREFRONT_PORT as needed. The Compose defaults are intentionally restricted to local/test environments and are not production credentials or orchestration.

Copy config/data_sources.example.json and adapt its three required ordered canonical queries. Each source must also list its allowed tracking_id and catalog_id pairs under scopes; both training and serving reject any other boundary. Catalog queries must return and filter both identifiers. purchases_query supplies online purchases; offline_purchases_query may be omitted when the merchandiser does not provide in-store purchase data. compatibility_query is also optional and streams configured product-to-product complement rules ordered by anchor and candidate. Root-level freshness settings support per-property overrides, while retention controls snapshot, run, and failed-diagnostic lifetimes. The optional root-level quality_drift group is disabled by default; when enabled, it controls the bounded history window, sample minimums, material-change floors, and robust-deviation thresholds. A source dialect is supported only after its driver and the source contract suite pass. Install .[snowflake] for Snowflake; any SQLAlchemy dialect can use the same contract. SQLite is intended for local/focused execution, not the production concurrency claim.

Generated Relational Data Source

To learn how the generator constructs Catalogs, shopping contexts, and reproducible evidence, read the synthetic data generation walkthrough, including Mermaid diagrams and a hands-on smoke-profile exercise.

Deterministic synthetic source generation is documented in docs/generated-relational-data-source.md. Generate a verified SQLite smoke source and its lineage artifacts with:

recommendations-generate-source --profile smoke --output-dir artifacts/generated-smoke

The source uses the same canonical adapter boundary as a merchant source. Its generation and materialization manifests contain logical digests and bounded oracle observations; raw generated interaction rows are not part of the evidence documents.

To stream the generated source directly into a PostgreSQL database, install the postgresql extra, place the SQLAlchemy connection URL in an environment variable, and name that variable without exposing the credential in process arguments:

recommendations-generate-source \
  --profile development \
  --database-url-env GENERATED_SOURCE_DATABASE_URL \
  --workers 4 \
  --output-dir artifacts/generated-development

The destination must not already contain the five generated-source tables unless --replace is explicitly supplied. PostgreSQL uses bounded, concurrent COPY workers plus parallel index and receipt verification. They write uniquely named staging tables; one final transaction publishes the fully verified tables, so failure cannot replace the last good source. --workers defaults to 4 and accepts 1 through 16.

For a dedicated local PostgreSQL container and one-shot generator, use compose.generated-source.yaml. The setup commands and local password configuration are in the generated source guide.

To retain generated history and add activity through a newer cutoff, use PostgreSQL --append with the existing output directory. It inherits the source identity and adds rows at the base profile's daily rate, with atomic publication and duplicate-safe exact-cutoff retries. See the append commands and limits.

API

Submit asynchronous training:

curl -X POST http://127.0.0.1:8000/v1/training-runs \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: daily-2026-08-05' \
  -d '{"data_source_id":"merchant-platform","tracking_id":"store-42","catalog_id":"main"}'

Inspect the returned run ID:

curl http://127.0.0.1:8000/v1/training-runs/RUN_ID

Training status includes configuration/model versions and the ordered pending, running, and terminal state transitions with UTC timestamps. The configuration version is a canonical SHA3-512 identity of source mappings and training policy; scoring grids, trend weights, and freshness policy therefore remain reproducible across workers.

Serve an anchored lane:

curl 'http://127.0.0.1:8000/v1/catalogs/main/items/SKU-1/recommendations/frequently-bought-together?data_source_id=merchant-platform&tracking_id=store-42&limit=20'

Global lanes use /v1/catalogs/{catalog_id}/recommendations/{best-sellers|most-viewed|trending}. Serving never queries a merchant database and continues to return an older snapshot with is_stale: true when a newer run fails.

Shopper personalization

Shopper personalization is additive and disabled by default. Enabled Commerce Properties can append ordered, idempotent interactions through POST /v1/personalization/interactions, request in-lane reranking with personalize=true, or request the global for-you strategy. Authentication is derived from the bearer principal; signed Personalization-Context and causal tokens bind every operation to one data source, property, Catalog, and opaque Shopper ID. Personalized serving reads only the current Recommendation Snapshot and its feature companion—it never queries the merchant source. See docs/shopper-personalization.md for the complete contract, privacy lifecycle, evaluation boundary, and rollout gates.

Verification

make setup
make doctor
make test
make smoke

Use make test-focused TEST=tests/path.py::test_name for the shortest feedback loop. The complete command ladder, PostgreSQL boundary, generated-artifact rules, and Apple Silicon macOS CI contract are documented in docs/testing.md.

Migrations can be round-tripped on a disposable database with alembic upgrade head and alembic downgrade base. Export the HTTP schema with:

uv run --frozen python scripts/export_openapi.py artifacts/openapi.json

The 200,000-product / 100M-view / 100M-purchase and latency objectives require a preloaded relational qualification source and the approved reference worker; they cannot be established by the focused local suite. Run the retained driver against that environment:

uv run --frozen python scripts/qualify.py \
  --base-url http://service:8000 \
  --data-source-id qualification \
  --tracking-id qualification-store \
  --catalog-id qualification-catalog \
  --anchor-product-id SKU-000001 \
  --artifact artifacts/qualification.json

The command fails unless the recorded source counts, 12-hour batch objective, 500 ms submission objective, and 90/150 ms serving percentiles all pass. Monthly availability remains an operations measurement: use successful eligible requests divided by all eligible requests, excluding planned maintenance only if the service-level agreement explicitly permits that exclusion.

Operational behavior

  • One active run is database-enforced per (data_source_id, tracking_id, catalog_id).
  • Workers renew leases while training. A lost run is reclaimed once; a second loss is terminal.
  • Snapshot rows are loaded in bounded chunks while invisible. Publication verifies the complete strategy manifest, swaps the serving head atomically, then applies retention.
  • Five successful snapshots are retained per scope by default. Training metadata is retained for 90 days and bounded failure diagnostics for 30 days.
  • Browsing Sessions and Orders above their configured distinct-product limits are excluded whole from co-occurrence; the limits default to 100 and 200 respectively, and excluded rows still contribute to popularity and exclusion metrics.
  • Successful Training Runs persist versioned aggregate evidence. Quality/drift assessment compares only the latest same-scope runs with matching model, configuration, and Training Evidence Schema versions; it uses median/MAD baselines and never changes snapshot publication or serving state.
  • The Training Evidence Schema version is independent of the OpenTelemetry signal schema. Missing additive evidence is reported as unavailable, and crossing a drift threshold is diagnostic—not a claim about clicks, conversion, revenue, retention, attribution, or causal performance.