Skip to content

Commit 20ec6bd

Browse files
committed
feat #2664: Geocoding DB should be completely isolated from vexl db
1 parent 85cf62c commit 20ec6bd

50 files changed

Lines changed: 991 additions & 394 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ When changing any UI / UX in the mobile app or in the ui package you must read @
2828
- packages/cryptography - shared cryptography utilities for both backend and mobile app
2929
- packages/generic-utils - shared generic utilities for both backend and mobile app - should not depend on any of the other vexl packages
3030
- packages/localization - shared localization utilities, types, and similar for both backend and mobile app
31+
- packages/geocoding-db - standalone geocoding database used by location-service: schema, queries, and the OSM ingest pipeline. Lives on its own Postgres instance (`GEOCODING_DB_*` env, never the shared `DB_URL`) so geocoding never shares resources with vexl user data. The dataset refresh (`pnpm refresh:geocoding`) is run manually from an operator's machine — never add server-side cron/updater infrastructure. Details in packages/geocoding-db/README.md.
3132
- packages/resources-utils - shared utilities for handling offers, chat, and other "resources" operations
3233
- packages/rest-api - effect-ts based rest api definitions and client for both backend and mobile app
3334
- tooling/* - shared tooling for the repo (esling, prettier, etc...)

apps/location-service/.env.example

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
SIGNATURE_PUBLIC_KEY=
55

6-
# Postgres with the places dataset (see scripts/ingestPlaces.ts)
7-
DB_URL=postgresql://localhost:5432/location
8-
DB_USER=postgres
9-
DB_PASSWORD=root
6+
# Standalone geocoding Postgres (see packages/geocoding-db)
7+
GEOCODING_DB_URL=postgresql://localhost:5433/geocoding
8+
GEOCODING_DB_USER=postgres
9+
GEOCODING_DB_PASSWORD=root

apps/location-service/.gitignore

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,2 @@
1-
21
# Sentry Config File
32
.env.sentry-build-plugin
4-
5-
# OSM/Natural Earth datasets for the places DB (fetched by scripts/refresh-places.sh)
6-
data/

apps/location-service/README.md

Lines changed: 24 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,24 @@
11
# location-service
22

33
Serves location autocomplete (`GET /suggest`) and reverse geocoding
4-
(`GET /geocode`) for the mobile app — backed by an **in-house places database**
5-
built from OpenStreetMap. No third-party geocoding API is involved, so the
6-
coordinates users pick for offers never leave Vexl infrastructure.
4+
(`GET /geocode`) for the mobile app — backed by the **standalone geocoding
5+
database** built from OpenStreetMap (see `packages/geocoding-db`). No
6+
third-party geocoding API is involved, so the coordinates users pick for
7+
offers never leave Vexl infrastructure.
8+
9+
The geocoding DB runs on its own Postgres instance, fully separated from the
10+
vexl service databases — geocoding traffic and the monthly OSM refresh never
11+
compete with chats/offers/connections for database resources. The service
12+
connects to it via `GEOCODING_DB_URL` / `GEOCODING_DB_USER` /
13+
`GEOCODING_DB_PASSWORD` and runs the schema migrations from
14+
`@vexl-next/geocoding-db` on startup. The only other component that connects
15+
to that database is the dataset refresh pipeline
16+
(`packages/geocoding-db/scripts/refresh.sh`).
717

818
## Architecture
919

10-
- **Data**, three tiers built from OSM, all enriched with the translations the
11-
app ships languages for (`name:xx` tags) and a country code stamped via
12-
Natural Earth boundaries:
13-
- settlements (place=\* nodes: city…city_block) — searchable + reverse
14-
geocoding,
15-
- POIs (cafés, restaurants, pubs, bars, fast food, parks, gardens,
16-
attractions, museums) — searchable only,
17-
- streets (named street-like highway ways, deduplicated to one entry per
18-
street name per ~10 km grid cell; **no house numbers by design**) —
19-
searchable only.
20+
- **Data + schema + ingest**: `packages/geocoding-db` — see its README for
21+
the data tiers, the refresh pipeline, and dev seeding.
2022
- **Search** (`/suggest`): prefix matching on normalized names cascading from
2123
cheap to expensive — important places first (partial covering index), then
2224
all names incl. streets/POIs, then typo-tolerant trigram matching (important
@@ -31,71 +33,19 @@ coordinates users pick for offers never leave Vexl infrastructure.
3133
- Localization is resolved per request `lang`: place names from the stored
3234
translations, country names via `Intl.DisplayNames` (CLDR, built into Node).
3335

34-
## Refreshing the dataset
36+
## Deployment
3537

36-
`scripts/refresh-places.sh` is the whole pipeline, meant to run weekly (cron)
37-
with no arguments — it fetches raw Geofabrik extracts (needs curl + osmium),
38-
filters them into the three tiers, and ingests into Postgres:
39-
40-
```sh
41-
DB_URL=... DB_USER=... DB_PASSWORD=... ./scripts/refresh-places.sh
42-
```
43-
44-
Raw and filtered files are kept under `data/` (`raw/`, `filtered/`), so stages
45-
can be re-run independently — e.g. after tweaking the filters, re-run just
46-
`refresh-places.sh extract ingest` without re-downloading ~85 GB. Regions are
47-
selectable with `-r` (any Geofabrik path): `-r europe`, `-r europe/slovakia`.
48-
49-
Set `SLACK_ALERT_WEBHOOK_URL` (a Slack incoming-webhook URL) on the cron host
50-
to get a Slack message whenever any step fails — download, extraction, or
51-
ingest (including the sanity-gate abort). Leave it unset for dev runs.
52-
53-
The ingest loads into staging tables and swaps them in a single transaction —
54-
the service keeps serving the old dataset until the new one is complete, and a
55-
sanity gate refuses to swap in a dataset >30 % smaller than the previous one.
38+
The service requires `GEOCODING_DB_URL` / `GEOCODING_DB_USER` /
39+
`GEOCODING_DB_PASSWORD` at startup — there is no fallback to the old `DB_URL`
40+
vars. Rolling out an image built from this change therefore requires the
41+
dedicated geocoding Postgres and the new env vars to be provisioned first;
42+
without them the service crash-loops at startup.
5643

5744
## Local development
5845

59-
`pnpm dev:backend` seeds the places DB automatically: before the services
60-
start it runs `scripts/seedDevPlaces.ts`, which creates the `location`
61-
database and schema if missing and — only when the places table is empty —
62-
loads data. Because the Postgres volume persists between runs, this is a
63-
fast no-op on every run except the first one (and after `--fresh-db`).
64-
65-
What an empty table gets seeded with:
66-
67-
- **`--seed-places auto`** (the default): a full OSM ingest of Slovakia +
68-
Czechia via `refresh-places.sh` when `osmium` is installed
69-
(`brew install osmium-tool`), topped up with a fixture of the European
70-
capitals so search and reverse geocoding work across Europe. Downloads
71-
(~1 GB) are cached in `~/.cache/vexl/osm`, so re-seeding after
72-
`--fresh-db` skips the download and only re-runs the ~minutes-long ingest.
73-
- **`--seed-places fixture`** (also the automatic fallback when osmium is
74-
missing): just the committed fixture — major SK/CZ cities + European
75-
capitals (`scripts/devSeedData.ts`). Instant, offline, no tools needed.
76-
- **`--seed-places off`**: no data — the database and schema are still
77-
created if missing (the service crash-loops without them), but nothing is
78-
seeded.
79-
80-
The seeder can also run standalone (env from `.env`):
81-
82-
```sh
83-
pnpm --filter @vexl-next/location-service seed:dev-places
84-
```
85-
86-
A non-empty table is never touched. To switch an existing DB from fixture to
87-
real data, truncate first:
88-
89-
```sh
90-
docker exec vexl-postgres psql -U postgres -d location -c 'TRUNCATE places CASCADE'
91-
```
92-
93-
For other regions, run the pipeline directly, e.g.:
94-
95-
```sh
96-
DB_URL=postgresql://localhost:5432/location DB_USER=postgres DB_PASSWORD=root \
97-
./scripts/refresh-places.sh -r europe/austria
98-
```
46+
`pnpm dev:backend` starts the dedicated geocoding Postgres container and
47+
seeds it automatically via `packages/geocoding-db` — see that package's
48+
README for seeding modes (`--seed-places auto|fixture|off`) and re-seeding.
9949

10050
## Tests
10151

apps/location-service/package.json

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,26 +12,23 @@
1212
"format:fix": "prettier --write \"**/*.{js,mjs,cjs,jsx,ts,tsx,md,json}\"",
1313
"lint": "eslint src",
1414
"dev": "tsx -r dotenv/config src/index.ts",
15-
"start": "node --enable-source-maps dist/index.js",
16-
"ingest:places": "tsx -r dotenv/config scripts/ingestPlaces.ts",
17-
"seed:dev-places": "tsx -r dotenv/config scripts/seedDevPlaces.ts"
15+
"start": "node --enable-source-maps dist/index.js"
1816
},
1917
"dependencies": {
2018
"@effect/platform": "^0.93.6",
2119
"@effect/platform-node": "^0.103.0",
22-
"@effect/sql": "^0.48.6",
23-
"@effect/sql-pg": "^0.49.7",
2420
"@vexl-next/domain": "0.0.0",
21+
"@vexl-next/geocoding-db": "0.0.0",
2522
"@vexl-next/localization": "0.0.0",
2623
"@vexl-next/rest-api": "0.0.0",
2724
"@vexl-next/server-utils": "0.0.0",
2825
"dotenv": "^16.4.5",
29-
"effect": "^3.20.0",
30-
"pg": "^8.12.0"
26+
"effect": "^3.20.0"
3127
},
3228
"devDependencies": {
29+
"@effect/sql": "^0.48.6",
30+
"@effect/sql-pg": "^0.49.7",
3331
"@types/jest": "^29.4.0",
34-
"@types/pg": "^8",
3532
"@vexl-next/esbuild": "0.0.0",
3633
"@vexl-next/eslint-config": "0.0.0",
3734
"@vexl-next/prettier-config": "0.0.0",

apps/location-service/src/__tests__/handlers/geocode.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import {Latitude, Longitude} from '@vexl-next/domain/src/utility/geoCoordinates'
2+
import {seedPlacesInDb} from '@vexl-next/geocoding-db/src/tests/seedPlaces'
23
import {LocationNotFoundError} from '@vexl-next/rest-api/src/services/location/contracts'
34
import {expectErrorResponse} from '@vexl-next/server-utils/src/tests/expectErrorResponse'
45
import {setDummyAuthHeaders} from '@vexl-next/server-utils/src/tests/nodeTestingApp'
56
import {Effect, Schema} from 'effect'
67
import {NodeTestingApp} from '../utils/NodeTestingApp'
78
import {runPromiseInMockedEnvironment} from '../utils/runPromiseInMockedEnvironment'
8-
import {seedPlacesInDb} from '../utils/seedPlaces'
99

1010
const coordinates = (
1111
latitude: number,

apps/location-service/src/__tests__/handlers/suggest.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1+
import {seedPlacesInDb} from '@vexl-next/geocoding-db/src/tests/seedPlaces'
12
import {setDummyAuthHeaders} from '@vexl-next/server-utils/src/tests/nodeTestingApp'
23
import {Effect} from 'effect'
34
import {NodeTestingApp} from '../utils/NodeTestingApp'
45
import {runPromiseInMockedEnvironment} from '../utils/runPromiseInMockedEnvironment'
5-
import {seedPlacesInDb} from '../utils/seedPlaces'
66

77
// The runtime itself is started/disposed globally in jest.afterenv.ts
88
beforeAll(async () => {

apps/location-service/src/__tests__/ingest/ingestPipeline.test.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/**
2-
* End-to-end test of the places ingest pipeline (scripts/ingestPlaces.ts):
2+
* End-to-end test of the places ingest pipeline
3+
* (packages/geocoding-db/scripts/ingest.ts):
34
* geojsonseq parsing → transform → staging inserts → street merging →
45
* importance boosting → index build → sanity gate → atomic table swap — and
56
* finally the live API serving the ingested dataset.
@@ -11,18 +12,23 @@
1112
*/
1213
import {PgClient} from '@effect/sql-pg'
1314
import {Latitude, Longitude} from '@vexl-next/domain/src/utility/geoCoordinates'
15+
import {computeImportance} from '@vexl-next/geocoding-db/src/common'
1416
import {setDummyAuthHeaders} from '@vexl-next/server-utils/src/tests/nodeTestingApp'
1517
import {Effect, Schema} from 'effect'
1618
import {execFile} from 'node:child_process'
1719
import {chmodSync, mkdirSync, mkdtempSync, rmSync, writeFileSync} from 'node:fs'
1820
import {tmpdir} from 'node:os'
1921
import path from 'node:path'
20-
import {computeImportance} from '../../places/common'
2122
import {NodeTestingApp} from '../utils/NodeTestingApp'
2223
import {runPromiseInMockedEnvironment} from '../utils/runPromiseInMockedEnvironment'
2324

2425
const RS = '\u001e'
25-
const SERVICE_ROOT = path.resolve(__dirname, '../../..')
26+
// The ingest pipeline lives in the geocoding-db package — resolve it through
27+
// the workspace symlink so the test does not depend on the repo layout.
28+
const GEOCODING_DB_ROOT = path.resolve(
29+
__dirname,
30+
'../../../node_modules/@vexl-next/geocoding-db'
31+
)
2632

2733
// ---------------------------------------------------------------------------
2834
// Fixture builders
@@ -256,13 +262,13 @@ const runIngest = async (
256262
[
257263
'exec',
258264
'tsx',
259-
'scripts/ingestPlaces.ts',
265+
'scripts/ingest.ts',
260266
'--countries',
261267
path.join(workDir, 'ne_countries.geojson'),
262268
...pbfPaths,
263269
],
264270
{
265-
cwd: SERVICE_ROOT,
271+
cwd: GEOCODING_DB_ROOT,
266272
maxBuffer: 32 * 1024 * 1024,
267273
env: {
268274
...process.env,

apps/location-service/src/__tests__/utils/runPromiseInMockedEnvironment.ts

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,26 @@ import * as NodeHttpServer from '@effect/platform-node/NodeHttpServer'
33
import {type HttpClient} from '@effect/platform/HttpClient'
44
import {HttpApiBuilder} from '@effect/platform/index'
55
import {type SqlClient} from '@effect/sql/SqlClient'
6+
import {GeocodingDbService} from '@vexl-next/geocoding-db/src/GeocodingDbService'
7+
import {GeocodingDbLayer} from '@vexl-next/geocoding-db/src/layer'
8+
import {
9+
disposeGeocodingTestDatabase,
10+
setupGeocodingTestDatabase,
11+
} from '@vexl-next/geocoding-db/src/tests/testGeocodingDb'
612
import {type RateLimitingService} from '@vexl-next/server-utils/src/RateLimiting'
713
import {ServerCrypto} from '@vexl-next/server-utils/src/ServerCrypto'
814
import {mockedRateLimitingLayer} from '@vexl-next/server-utils/src/tests/mockedRateLimitingLayer'
915
import {TestRequestHeaders} from '@vexl-next/server-utils/src/tests/nodeTestingApp'
10-
import {
11-
disposeTestDatabase,
12-
setupTestDatabase,
13-
} from '@vexl-next/server-utils/src/tests/testDb'
1416
import {Console, Effect, Layer, ManagedRuntime, type Scope} from 'effect'
1517
import {cryptoConfig} from '../../configs'
16-
import DbLayer from '../../db/layer'
17-
import {PlacesDbService} from '../../db/PlacesDbService'
18+
import {GeocodingService} from '../../geocoding'
1819
import {LocationApiLive} from '../../httpServer'
19-
import {PlacesService} from '../../places'
2020

2121
export type MockedContexts =
2222
| ServerCrypto
2323
| SqlClient
24-
| PlacesDbService
25-
| PlacesService
24+
| GeocodingDbService
25+
| GeocodingService
2626
| HttpClient
2727
| TestRequestHeaders
2828
| RateLimitingService
@@ -36,9 +36,9 @@ const context = Layer.empty.pipe(
3636
Layer.provideMerge(TestServerLive),
3737
Layer.provideMerge(TestRequestHeaders.Live),
3838
Layer.provideMerge(mockedRateLimitingLayer),
39-
Layer.provideMerge(PlacesService.Live),
40-
Layer.provideMerge(PlacesDbService.Live),
41-
Layer.provideMerge(DbLayer),
39+
Layer.provideMerge(GeocodingService.Live),
40+
Layer.provideMerge(GeocodingDbService.Live),
41+
Layer.provideMerge(GeocodingDbLayer),
4242
Layer.provideMerge(ServerCrypto.layer(cryptoConfig)),
4343
Layer.provideMerge(NodeContext.layer)
4444
)
@@ -47,7 +47,7 @@ const runtime = ManagedRuntime.make(context)
4747
let runtimeReady = false
4848

4949
export const startRuntime = async (): Promise<void> => {
50-
await Effect.runPromise(setupTestDatabase)
50+
await Effect.runPromise(setupGeocodingTestDatabase)
5151
await runtime.runPromise(Console.log('Initialized the test environment'))
5252
runtimeReady = true
5353
}
@@ -58,7 +58,7 @@ export const disposeRuntime = async (): Promise<void> => {
5858
Console.log('Disposed test environment')
5959
)
6060
)
61-
await Effect.runPromise(disposeTestDatabase)
61+
await Effect.runPromise(disposeGeocodingTestDatabase)
6262
runtimeReady = false
6363
}
6464

apps/location-service/src/configs.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
export {
22
cryptoConfig,
3-
databaseConfig,
43
healthServerPortConfig,
54
isRunningInTestConfig,
65
nodeEnvConfig,

0 commit comments

Comments
 (0)