search-aggregation-service — LOCAL_DEV_SETUP
Companion: SERVICE_OVERVIEW · DATA_MODEL · TESTING_STRATEGY · DEPLOYMENT_TOPOLOGY · ../../docs/standards/SERVICE_TEMPLATE.md
This guide gets a developer from a fresh checkout to a fully running search-aggregation-service with seeded data, an OpenSearch index, a Pub/Sub emulator wired to mock upstream services, and an end-to-end search query in under 15 minutes.
1. Prerequisites
| Tool | Version | Why |
|---|---|---|
| Node.js | 20.x LTS | Runtime |
| pnpm | 9.x | Workspace package manager |
| Docker Desktop | latest | Local containers |
| docker compose v2 | latest | pnpm dev:up orchestration |
gcloud CLI | latest | only for prod-like auth tests |
openssl | any | dev cert generation |
psql (client only) | 15+ | optional debugging |
httpie or curl | any | smoke testing |
pnpm install from the repo root installs everything else.
2. Repo layout (this service)
services/search-aggregation-service/
├── src/
│ ├── domain/ # pure TS, no I/O
│ ├── application/ # commands, queries, consumers, ports
│ ├── infrastructure/
│ │ ├── postgres/
│ │ ├── opensearch/
│ │ ├── redis/
│ │ ├── pubsub/
│ │ └── ai-orchestrator/
│ └── presentation/
│ ├── http/ # NestJS controllers
│ └── internal/
├── contracts/
│ ├── openapi.yaml
│ ├── asyncapi.yaml
│ └── opensearch/
│ └── hotel-index.template.json
├── migrations/ # numbered SQL files
├── seeds/
│ ├── province_centers.sql
│ ├── golden-hotels.json # 30 demo properties across AF/TJ/IR
│ └── golden-events.json # property/pricing/inventory events
├── ops/
│ ├── compose.dev.yaml
│ ├── runbooks/
│ └── dashboards/
├── test/
│ ├── builders/
│ ├── unit/
│ ├── application/
│ ├── integration/
│ └── contract/
├── package.json
├── tsconfig.json
└── README.md # short pointer to this file
3. One-command bootstrap
pnpm install
pnpm --filter @melmastoon/search-aggregation-service dev:up
dev:up runs docker compose -f ops/compose.dev.yaml up -d and then pnpm dev:setup which:
- Waits for Postgres + OpenSearch + Pub/Sub emulator + Redis to be healthy.
- Runs
pnpm migrate:up(numbered SQL vianode-pg-migrate). - Loads
seeds/province_centers.sql. - Applies
contracts/opensearch/hotel-index.template.json. - Creates Pub/Sub topics and subscriptions for this service (per AsyncAPI).
- Replays
seeds/golden-events.jsonthrough the emulator → consumer pipeline → produces 30 indexed hotels. - Verifies
GET /healthzandGET /readyzreturn 200.
4. compose.dev.yaml (excerpt)
version: "3.9"
services:
postgres:
image: postgis/postgis:15-3.4
ports: ["5432:5432"]
environment:
POSTGRES_DB: search
POSTGRES_USER: search
POSTGRES_PASSWORD: search
healthcheck:
test: ["CMD", "pg_isready", "-U", "search"]
interval: 5s
opensearch:
image: opensearchproject/opensearch:2.13.0
environment:
- discovery.type=single-node
- DISABLE_SECURITY_PLUGIN=true
- OPENSEARCH_JAVA_OPTS=-Xms1g -Xmx1g
ports: ["9200:9200"]
redis:
image: redis:7-alpine
ports: ["6379:6379"]
pubsub:
image: gcr.io/google.com/cloudsdktool/cloud-sdk:emulators
command: >
gcloud beta emulators pubsub start
--project=melmastoon-dev --host-port=0.0.0.0:8085
ports: ["8085:8085"]
ai-orchestrator-stub:
image: ghasi/ai-orchestrator-stub:latest # in-house stub from /tools
ports: ["7070:7070"]
environment:
STUB_MODE: deterministic
(All ports are also exposed via service names on the shared melmastoon-dev network used by other services for cross-service local dev.)
5. Environment variables
pnpm dev loads .env.development (committed) merged with .env.local (git-ignored) for personal overrides.
NODE_ENV=development
SERVICE_NAME=search-aggregation-service
SERVICE_VERSION=dev
LOG_LEVEL=debug
# Postgres
PG_HOST=localhost
PG_PORT=5432
PG_DB=search
PG_USER=search
PG_PASSWORD=search
PG_POOL_MAX=10
PG_TENANT_SENTINEL=__cross_tenant__
# OpenSearch
OPENSEARCH_URL=http://localhost:9200
OPENSEARCH_INDEX_ALIAS=melmastoon-search-current
OPENSEARCH_TEMPLATE_NAME=melmastoon-search-template
# Redis
REDIS_URL=redis://localhost:6379
CACHE_TTL_QUERY_SECONDS=60
CACHE_TTL_DETAIL_SECONDS=300
CACHE_TTL_FX_SECONDS=3600
# Pub/Sub emulator
PUBSUB_EMULATOR_HOST=localhost:8085
PUBSUB_PROJECT_ID=melmastoon-dev
# AI orchestrator stub
AI_ORCHESTRATOR_BASE_URL=http://localhost:7070
AI_ORCHESTRATOR_TIMEOUT_MS=800
AI_ORCHESTRATOR_AUDIENCE=http://localhost:7070
SEARCH_AI_INTENT_CACHE_TTL_SEC=2592000
# JWT (operator routes)
IAM_JWKS_URL=http://localhost:7080/.well-known/jwks.json # iam-service stub
JWT_AUDIENCE=search-aggregation-service
JWT_ISSUER=https://iam.dev.melmastoon.local
# Feature flags (defaults for dev)
SEARCH_DEGRADE_ON_OPENSEARCH_ERROR=true
SEARCH_REGION_PINNING_STRICT=true
SEARCH_SEMANTIC_RERANK_ENABLED=false
# Cursor / idempotency signing keys (dev only — random per machine)
CURSOR_SIGNING_KEY=dev-cursor-signing-key-please-rotate
IDEMPOTENCY_SIGNING_KEY=dev-idem-key
.env.local overrides — typical use: point at a colleague's hosted OpenSearch, lower PG_POOL_MAX for low-RAM laptops, set LOG_LEVEL=trace for deep debugging.
6. Useful scripts
| Command | Effect |
|---|---|
pnpm dev | Hot-reload server on :3000 (NestJS via tsx watch) + in-process subscribers |
pnpm dev:up | Boot containers + bootstrap |
pnpm dev:down | Tear down (preserves volumes) |
pnpm dev:reset | Tear down and prune volumes (clean slate) |
pnpm dev:seed | Re-replay golden events without dropping data |
pnpm migrate:up / pnpm migrate:down | Apply / revert migrations |
pnpm test | Unit + application |
pnpm test:integration | Full slice with the same containers |
pnpm test:contract | OpenAPI + AsyncAPI + OpenSearch template validation |
pnpm lint / pnpm typecheck | Static checks |
pnpm openapi:gen | Regenerate contracts/openapi.yaml from controllers |
pnpm asyncapi:validate | Validate published events against contracts/asyncapi.yaml |
pnpm dev:smoke | Run the smoke search query (§ 8) against the running dev server |
7. Bootstrapping data manually
# Apply OpenSearch template (idempotent)
curl -sS -X PUT "http://localhost:9200/_index_template/melmastoon-search-template" \
-H 'Content-Type: application/json' \
--data-binary @contracts/opensearch/hotel-index.template.json
# Create the per-region indexes for AF/TJ/IR and the writer alias
for r in af tj ir; do
curl -sS -X PUT "http://localhost:9200/melmastoon-search-v1-$r"
done
curl -sS -X POST "http://localhost:9200/_aliases" -H 'Content-Type: application/json' -d '{
"actions": [
{"add": {"index": "melmastoon-search-v1-af", "alias": "melmastoon-search-current"}},
{"add": {"index": "melmastoon-search-v1-tj", "alias": "melmastoon-search-current"}},
{"add": {"index": "melmastoon-search-v1-ir", "alias": "melmastoon-search-current"}}
]
}'
# Seed golden hotels through the consumer
pnpm dev:seed
8. Smoke test
http POST :3000/api/v1/search/queries \
Accept-Language:en \
X-Currency:USD \
X-Region:AF \
text='hotel kabul wifi' \
filter:='{ "starRating": { "min": 3 } }' \
page:='{ "size": 5 }'
Expected 200 with at least one of the seeded Kabul hotels (Hotel Pamir, Kabul Serena Demo).
http GET ":3000/api/v1/search/hotels/ppt_01H8R2YQ7B2KS6RXQ7E4VV4QPM" X-Currency:USD
Expected 200 with the hotel detail aggregate including the 30-day rate snapshot range.
9. Running with another service locally
The repo's root compose file (infra/compose.dev.yaml) brings up the federated dev environment:
pnpm -w dev:platform up
This boots iam-service, tenant-service, property-service, pricing-service, inventory-service, bff-consumer-service, ai-orchestrator (or its stub), and search-aggregation-service together. Each service shares the Pub/Sub emulator and the iam-service stub. To exercise a real cross-tenant publish/index/search flow:
- In
property-service:pnpm dev:demo:create-property(creates a property, publishes events). - In
search-aggregation-service: tail logs (pnpm dev:logs) — within ~1 s you should see the upsert. - Run the smoke query (§ 8) — the new property appears.
10. Faking AI behaviour
ai-orchestrator-stub supports three modes via STUB_MODE:
deterministic(default): returns fixedintent.parse-searchresponse pertext.slow: 1 200 ms delay (exercises the timeout / fallback path).error: returns 500 (exercises the fallback path).
Switch modes without restarting the search service via curl :7070/admin/mode.
11. Faking upstream events
pnpm dev:replay <fixture.json> reads a JSON list of events shaped like AsyncAPI envelopes and publishes them to the right topic in the emulator with the correct ordering keys. Use it to reproduce edge cases (out-of-order, vector-clock conflicts, tenant purge).
12. Debugging
- DB introspection:
psql postgresql://search:search@localhost:5432/search -c '\dt search.*'. Note:SET LOCAL app.tenant_id = '__cross_tenant__'is required to read any row outside the auto-set sentinel; the dev seed sets the role default. - OpenSearch:
http://localhost:9200/_cat/indices?vandhttp://localhost:9200/melmastoon-search-current/_search?pretty. - Redis:
redis-cli -p 6379 KEYS 'srh:*' | head. - Pub/Sub emulator:
gcloud --project=melmastoon-dev pubsub topics list(setPUBSUB_EMULATOR_HOSTfirst). - Trace UI: spans are exported to a local Jaeger container if
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318is set; bring up Jaeger withdocker compose --profile observability up.
13. IDE setup
- VS Code: workspace settings include the
search-aggregation-serviceESLint config and a recommended Vitest extension. .editorconfigenforces 2-space indent, LF endings, UTF-8.pnpm prepareinstalls git hooks viahusky: pre-commit runslint-staged(eslint + prettier on changed files); pre-push runspnpm typecheck && pnpm test.
14. Common pitfalls
| Symptom | Cause | Fix |
|---|---|---|
| 401 on operator routes locally | JWKS stub not running | pnpm -w dev:platform up iam-service |
| Events published but index empty | OpenSearch template missing | re-run § 7 template apply step |
| Pub/Sub not delivering | PUBSUB_EMULATOR_HOST not set in your shell | export it or run via pnpm dev (loads .env.development) |
MELMASTOON.SEARCH.GEO_OUT_OF_BOUNDS on bbox | bbox area > 250 000 km² | shrink bbox |
MELMASTOON.SEARCH.PAGE_OUT_OF_RANGE | from + size > 10 000 | use cursor pagination or narrow filter |
| Slow integration tests | Aiven OpenSearch image needs ≥ 2 GiB RAM | raise Docker Desktop limit |
15. Cleanup
pnpm dev:down # stop containers, keep data
pnpm dev:reset # nuke volumes, fresh start
pnpm dev:platform down # stop all federated services