Telecom: from raw CDRs to a co-location network
The problem
Call detail records are the highest-volume, lowest-tooled workload in telecom fraud and law-enforcement investigation. A single case can mean tens of millions of rows, and the actual analytical question — "who has this number been in contact with, and who do those contacts also talk to?" — usually ends up answered in a spreadsheet or a $100k/seat proprietary tool, because turning flat CDR rows into a navigable graph is genuinely hard: it needs time-series scan, graph traversal, and identity resolution (a number can be reassigned, a SIM can be swapped) all in the same query.
The scenario
A fraud team has a suspect MSISDN linked to a SIM-swap fraud ring. They need the immediate contact network, ranked by call volume, and they need to resolve any contact number to a known identity — without exporting CSVs between three tools.
Ingest, then query the graph directly
CDR ingest and analysis are native — CdrRecord is a first-class governed type, not a staging table you build yourself.
# Ingest a CSV export (columns: caller, callee, duration_secs, timestamp_utc)
relata cdr ingest calls.csv --purpose law_enforcementPURPOSE 'law_enforcement'
-- Common-contact analysis: who has this number called or been called by,
-- ranked by call volume
SELECT callee, COUNT(*) AS call_count, SUM(duration_secs) AS total_secs
FROM CdrRecord
WHERE caller = '+919876543210' OR callee = '+919876543210'
GROUP BY callee
ORDER BY call_count DESC
LIMIT 20;
-- Resolve a contact number to a known identity, if one exists
SELECT * FROM RESOLVE_IDENTITY('+447700900123');Typed SDK snippet
from relata import RelataClient
with RelataClient(
"http://localhost:9090",
bearer_token="relata-dev",
purpose="law_enforcement",
) as client:
# Common-contact / hand-off analysis for the suspect number
contacts = client.query(
"SELECT callee, COUNT(*) AS call_count, SUM(duration_secs) AS total_secs "
"FROM CdrRecord "
"WHERE caller = '+919876543210' OR callee = '+919876543210' "
"GROUP BY callee ORDER BY call_count DESC LIMIT 20"
)
# Build the co-location network: for each frequent contact, find
# their own frequent contacts, and rank the resulting network by degree
network = {}
for row in contacts:
second_hop = client.query(
f"SELECT callee, COUNT(*) AS call_count FROM CdrRecord "
f"WHERE caller = '{row['callee']}' "
f"GROUP BY callee ORDER BY call_count DESC LIMIT 10"
)
network[row["callee"]] = [r["callee"] for r in second_hop]
# Resolve any number in the network to a known identity
for number in network:
identity = client.query(f"SELECT * FROM RESOLVE_IDENTITY('{number}')")
if identity:
print(f"{number} -> {identity[0]['linked_entity_ids']}")CLI shorthand for the same two-step flow, if you're working interactively:
relata cdr analyze +919876543210
relata cdr timeline +919876543210Why this replaces the bespoke stack
CdrRecordis bi-temporal, not a flat import. Number portability and SIM reassignment don't corrupt history — a query against last month's data resolves identity as it stood then, viaAS OF. See Bi-Temporal Model.- Identity resolution is a query, not a separate lookup service.
RESOLVE_IDENTITYruns against the sameIdentityIndexthat SmartIngest built at ingest time — no batch reconciliation job between the CDR table and a subscriber master. See Identity Resolution. - The graph traversal and the row scan are the same engine.
DEGREE()andPATHS_BETWEENoperate on the CSR graph layer derived fromCdrRecordat ingest — building a co-location network is a query, not an ETL job into a separate graph database. - Governed by default.
PURPOSE 'law_enforcement'scopes and audits the query the same way it would for any other protected dataset — there's no separate ACL model for CDR data.
See also
- Query Cookbook — CDR analysis — the full
CdrRecordschema, CLI shorthand, and verified query patterns - Identity Resolution —
RESOLVE_IDENTITYand the canonicalMsisdn/Imeitypes - LEA: court-admissible investigation graph — the same
PATHS_BETWEENtraversal applied to a full case file - Ingestion & SmartIngest — how identifiers are auto-detected on write