Situs data alternative ingestion #147
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Right now we have a simple system for ingesting situs data in nidus sync. Essentially we provide a URL pattern and it does a web scrape against the website using the external ID (APN). This works against a vendor, mptsweb, which so far has been the vendor for the situs data of the only county we've cared about (Tulare).
We've now interested in getting situs data for a new customer - Kings County, CA. They actually provide all of the situs data that we need through ArcGIS features. This is great - it's way better and faster that scraping mptsweb data. However, we need to design a way to scrape this data.
I'd like you to design changes to our database schema to support our broadening situs sources. We'll need some way of identifying mptsweb scrapers as one way of getting situs data, and arcgis as another. For ArcGIS we'll need to identy a feature server, layer index, and mappings for the various fields we care about. Right now from our latest situs data scrapes we have the following:
It would be best to move this schema out of a single JSONB field and into something far more native schema with proper data types. We'll want to continue to keep track of unstructured data so that we can pull everything the county assesor makes available, but steadily as we go we can normalize the data and standardize how we keep it to make queries easier.
Anyway, take a look at our existing patterns, propose a new database schema and a migration path. The goal ultimately will be to tie together parcels, addresses, and situs information. As we get more customers, we may also need to develop more ways of ingesting the situs data they keep.
Did a thorough walk of the existing codebase — here's my assessment and proposal.
Current State Assessment
What exists today
Two separate source types, two separate tables, two different ingestion flows:
municipal.source_parcelmunicipal.source+municipal.source_situssource_parcel(service_feature_item_id, layer_index, field mappings){external_id}in URL pattern, scrapes HTMLmunicipal.parcel(well-typed columns + geometry)municipal.situs(a few typed columns +raw_dataJSONB)Key observations:
municipal.sourceis already a generic parent table with common metadata (created, url, region, county, etc.) — good foundationmunicipal.source_situscurrently only tracks the MPTSWeb scraping config (url_pattern, parcel_source_id)municipal.source_parcel(ArcGIS) has the arcgis reference columns but they're not onmunicipal.sourceitselfmunicipal.situstable has a mix of typed columns + raw_data JSONB — the typed columns have been added incrementally in migrations 00177 and 00179raw_dataholds 35+ fields from Tulare County's assessor that haven't been normalized yetProposed Schema Design
Goal: Single situs source abstraction supporting both MPTSWeb and ArcGIS
Step 1: Add a
source_typetomunicipal.source_situsRationale for field mappings on source_situs vs. a separate mapping table: The situs fields are well-known from the data Eli showed (36 fields). A dedicated field-per-column approach is type-safe, queryable, and follows the existing pattern from
municipal.source_parcel(which usesarcgis_layer_field_mapping_external_id_name, etc.). If the field set grows substantially we could revisit a mapping table, but right now this is cleaner.Step 2: Normalize
municipal.situscolumnsThe existing
raw_dataJSONB has 36 fields. We should add typed columns for the ones most commonly queried, while keepingraw_datafor everything else:Step 3: Link to parcels properly
municipal.situsalready hasexternal_id(which maps to APN) andparcel_id(document number). Theexternal_idshould become a proper FK tomunicipal.parcel.external_id(or to the unified concept). We need a FK relationship:But this assumes parcel sync has already run. The situs sync currently walks parcels by looking for situs records that don't exist yet — we'd need to change that to a join-based approach.
Migration Strategy
Phase 1: Schema expansion (no breaking changes)
source_typeand arcgis columns tomunicipal.source_situs— existing MPTSWeb sources get'mptsweb'with no arcgis fieldsmunicipal.situs— all nullable, existing data unaffectedUPDATE municipal.situs SET year_built = (raw_data->>'Year Built')::integer, ...for the common fieldsPhase 2: ArcGIS situs ingestion
SitusArcGISSync()— parallelsMunicipalJobSourceSync()but writes tomunicipal.situsinstead ofmunicipal.parcelmunicipal-situs-syncwith a dispatch that checkssource_type)Phase 3: Address linking (future work)
The
SitusAddrfield (present in ~80% of records) should eventually link to the public.address table. But we need a geocoding step or address normalization before that's reliable. For now, storing it as a typed column is a big improvement over JSONB.ArcGIS Situs Ingestion Flow (proposed)
How this scales for more customers
With
source_typeonmunicipal.source_situs, adding a third ingestion method is just:'csv_upload','api')source_situsNo schema redesign needed each time.
I'll wait for feedback before starting implementation. The biggest open questions before I code:
ArcGIS field mapping granularity — should I go with per-column mappings on
source_situs(as proposed), or a separatemunicipal.situs_field_mappingtable with a destination enum? The former is simpler for the known 14-20 common fields; the latter is more extensible.Parcel linking — should
municipal.situs.external_idbecome a proper FK tomunicipal.parcel.external_id, or should situs remain somewhat independent (since not all situs records may have a matching parcel)?Kings County specifics — do we know the ArcGIS Feature Service URL and layer for Kings County situs data yet? That'll inform the field mapping design.
Thanks, solid report and proposal.
I don't love that the
municipal.source_situstable is going to end up with a bunch of fields related to arcgis when, for mptsweb they'll all be null.What if we were to create two separate tables,
municipal.source_situs_mptswebandmunicipal.source_situs_arcgis. Then we could have schema that is specific to each use case. Can you write up what it would look like with two separate tables, and how migration would be handled?Response to feedback: Two-table design
Good call, neighborino. A single table with half the columns always null is not diddly-doodley clean. Let me lay out the two-table approach.
The Two-Table Design
municipal.source_situs_mptsweb— for MPTSWeb HTML scraping sourcesThis is essentially the current
source_situstable minus the PK rename. Nothing changes for the existing Tulare County workflow — every existing row insource_situsmigrates straight in.municipal.source_situs_arcgis— for ArcGIS Feature Service sourcesWhy so many mapping columns? Because situs has ~36 distinct attributes and we want type-safe, SQL-queryable access to each. This follows the same principle as
source_parcel.arcgis_layer_field_mapping_external_id_name. If the count gets unwieldy, we can refactor to amunicipal.situs_field_mappingtable later.Dispatching: how does the sync job know which table to use?
Since both tables use
source_idas their PK, a givenmunicipal.sourceID will have a row in exactly one of the two child tables. The dispatch logic checks both:This keeps the existing
municipal-situs-syncJobType andSitusSyncStartentry point unchanged.The situs data table stays shared
municipal.situsdoesn't split. It holds the actual assessment records regardless of how they were collected. The normalized columns I proposed before still make sense — they're just facts about a property, not tied to any ingestion method:The
situs.external_id→parcel.external_idlink works the same way whether the data came from a scrape or a feature query.Migration path
Phase 1 — Create the new tables, migrate existing data
Zero data loss: the current
source_situshas only MPTSWeb entries. Every row goes straight intosource_situs_mptswebwith no transformation needed.Phase 2 — Normalize situs data columns
Add typed columns to
municipal.situs, backfill fromraw_data, same as my previous proposal. This is independent of Phase 1.Phase 3 — ArcGIS situs ingestion code
db/query/municipal/:source_situs_arcgis.go(insert, list, fromID)db/query/municipal/source_situs.go→source_situs_mptsweb.goSitusJobSync()dispatch (shown above)situsArcgisSync()inplatform/municipal.gofollowing the same pagination pattern asMunicipalJobSourceSync()How this scales
Adding a third ingestion method (CSV upload, direct API, etc.) means creating a third child table and adding a branch in the dispatch. No schema changes to existing source configs, no nullable columns polluting unrelated records.
Want me to turn this into a concrete PR — migrations, query files, and dispatch refactor — or would you like to bikeshed the arcgis field mapping list first?
I agree with you on the field mapping - we can go to
municipal.situs_field_mappingif we get any more columns, but that may be a while.Go ahead and put together a PR, then I'll review and see where we land.
PR is ready for your review: #148
The migration adds both subtype tables, migrates existing data, then drops the old unified source_situs. The situs data table gets 25+ normalized columns alongside raw_data. Backward-compat wrappers keep existing callers compiling without changes.
I held back on writing the actual ArcGIS scraper and the raw_data backfill SQL — want to get the schema nailed down first, then we can iterate on the ingestion plumbing.