Merge

Solution · Healthcare

Entity Resolution for Pharmaceutical Data: Building a Drug Knowledge Graph

Pharmaceutical companies manage data from dozens of sources — FDA registries, clinical trial databases, pharmacy systems, supplier records, SEC filings. The same drug, manufacturer, or condition appears under different names, abbreviations, and formats in each system. "GlaxoSmithKline" in one database is "GSK" in another and "Glaxo SmithKline" in a third. "Johnson & Johnson" becomes "Johnson and Johnson" or "J&J" depending on who entered the data.

Without entity resolution, you end up with fragmented records that make it impossible to answer basic questions: Which manufacturer produces which drugs? What conditions does a drug treat? Which clinical trials study a given compound?

In this post, we build a pharmaceutical knowledge graph using Merge — connecting drugs, manufacturers, medical conditions, and clinical trials into a unified, deduplicated graph with automatic entity resolution.

Schemas configured in Merge


The Challenge: Fragmented Pharma Data

A typical pharmaceutical organization ingests data from:

  • FDA registries — official drug names and classifications
  • Clinical trial databases — study metadata with sponsor information
  • Pharmacy systems — brand names, generics, dosage forms
  • SEC filings — corporate legal names
  • Supplier databases — informal abbreviations and regional variants

The same entity appears differently across these systems:

Entity Source A Source B Source C
GlaxoSmithKline "GlaxoSmithKline" "GSK" "Glaxo SmithKline"
Pfizer "Pfizer" "Pfizer Inc"
Johnson & Johnson "Johnson & Johnson" "Johnson and Johnson" "J&J"
Humira "Humira (Adalimumab)" "Humira (Adalimumab)"
Tylenol "Tylenol" "Acetaminophen"

Step 1: Define the Schema

We start by defining four entity types that form our knowledge graph:

# Drug schema
curl -X POST https://merge-ai.app/v1/schemas \
  -H "X-API-Key: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "entity_type": "Drug",
    "attributes": [
      {"name": "name", "type": "string", "required": true},
      {"name": "generic_name", "type": "string"},
      {"name": "drug_class", "type": "string"},
      {"name": "form", "type": "string"}
    ]
  }'
# Manufacturer schema — stock_ticker is a deterministic identity field
curl -X POST https://merge-ai.app/v1/schemas \
  -H "X-API-Key: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "entity_type": "Manufacturer",
    "attributes": [
      {"name": "name", "type": "string", "required": true},
      {"name": "headquarters", "type": "string"},
      {"name": "stock_ticker", "type": "string", "identity": true}
    ]
  }'
# Condition schema — icd_code as identity
curl -X POST https://merge-ai.app/v1/schemas \
  -H "X-API-Key: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "entity_type": "Condition",
    "attributes": [
      {"name": "name", "type": "string", "required": true},
      {"name": "icd_code", "type": "string", "identity": true},
      {"name": "body_system", "type": "string"}
    ]
  }'
# ClinicalTrial schema
curl -X POST https://merge-ai.app/v1/schemas \
  -H "X-API-Key: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "entity_type": "ClinicalTrial",
    "attributes": [
      {"name": "title", "type": "string", "required": true},
      {"name": "phase", "type": "string"},
      {"name": "status", "type": "string"},
      {"name": "start_year", "type": "string"}
    ]
  }'

Key design decisions:

  • stock_ticker on Manufacturer is marked identity: true — if two records share the same ticker symbol, they are the same entity regardless of name variations.
  • icd_code on Condition uses the same pattern — ICD codes are authoritative identifiers.
  • Drug matching relies on fuzzy name comparison plus supporting attributes like generic_name and drug_class.

Step 2: Define Relationships

Relationships connect our entity types into a traversable graph:

# Drug → Manufacturer
curl -X POST https://merge-ai.app/v1/relationships \
  -H "X-API-Key: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "relationship_type": "manufactured_by",
    "from_entity_type": "Drug",
    "to_entity_type": "Manufacturer"
  }'

# Drug → Condition
curl -X POST https://merge-ai.app/v1/relationships \
  -H "X-API-Key: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "relationship_type": "treats",
    "from_entity_type": "Drug",
    "to_entity_type": "Condition"
  }'

# ClinicalTrial → Manufacturer
curl -X POST https://merge-ai.app/v1/relationships \
  -H "X-API-Key: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "relationship_type": "conducted_by",
    "from_entity_type": "ClinicalTrial",
    "to_entity_type": "Manufacturer"
  }'

# ClinicalTrial → Drug
curl -X POST https://merge-ai.app/v1/relationships \
  -H "X-API-Key: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "relationship_type": "studies",
    "from_entity_type": "ClinicalTrial",
    "to_entity_type": "Drug"
  }'

This creates a rich graph structure: a Drug is manufactured_by a Manufacturer, treats a Condition, and is studied in ClinicalTrials.


Step 3: Ingest Data from Multiple Sources

We load manufacturers from different source systems using batch ingest:

curl -X POST https://merge-ai.app/v1/entities/batch \
  -H "X-API-Key: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "entity_type": "Manufacturer",
    "records": [
      {"name": "Pfizer", "headquarters": "New York, USA", "stock_ticker": "PFE"},
      {"name": "AbbVie", "headquarters": "Chicago, USA", "stock_ticker": "ABBV"},
      {"name": "Novo Nordisk", "headquarters": "Copenhagen, Denmark", "stock_ticker": "NVO"},
      {"name": "Johnson & Johnson", "headquarters": "New Brunswick, USA", "stock_ticker": "JNJ"},
      {"name": "GlaxoSmithKline", "headquarters": "London, UK", "stock_ticker": "GSK"},
      {"name": "Eli Lilly", "headquarters": "Indianapolis, USA", "stock_ticker": "LLY"},
      {"name": "Novartis", "headquarters": "Basel, Switzerland", "stock_ticker": "NVS"},
      {"name": "Roche", "headquarters": "Basel, Switzerland", "stock_ticker": "RHHBY"}
    ]
  }'

Then drugs with their relationships:

curl -X POST https://merge-ai.app/v1/entities \
  -H "X-API-Key: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "entity_type": "Drug",
    "attributes": {
      "name": "Lipitor",
      "generic_name": "Atorvastatin",
      "drug_class": "Statin",
      "form": "Tablet"
    },
    "relationships": [
      {"relationship_type": "manufactured_by", "to_entity_id": "ent_pfizer_id"},
      {"relationship_type": "treats", "to_entity_id": "ent_high_cholesterol_id"}
    ]
  }'

Entities in the system

We ingested 8 manufacturers, 6 conditions, 8 drugs, and 5 clinical trials — all interconnected via relationships.


Step 4: Entity Resolution in Action

Now we simulate real-world data quality issues by ingesting duplicate and variant records from different source systems.

Test: Manufacturer Name Variations

# From FDA registry
curl -X POST https://merge-ai.app/v1/entities \
  -H "X-API-Key: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "entity_type": "Manufacturer",
    "attributes": {"name": "GSK", "headquarters": "London, UK", "stock_ticker": "GSK"},
    "source_system": "fda_registry"
  }'

# From clinical trial data
curl -X POST https://merge-ai.app/v1/entities \
  -H "X-API-Key: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "entity_type": "Manufacturer",
    "attributes": {"name": "Glaxo SmithKline", "headquarters": "London, United Kingdom", "stock_ticker": "GSK"},
    "source_system": "clinical_data"
  }'

# From SEC filings
curl -X POST https://merge-ai.app/v1/entities \
  -H "X-API-Key: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "entity_type": "Manufacturer",
    "attributes": {"name": "Pfizer Inc", "headquarters": "New York, USA", "stock_ticker": "PFE"},
    "source_system": "sec_filings"
  }'

Test: Exact Drug Duplicate

# Same drug from a different source system
curl -X POST https://merge-ai.app/v1/entities \
  -H "X-API-Key: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "entity_type": "Drug",
    "attributes": {"name": "Humira", "generic_name": "Adalimumab", "drug_class": "Biologic DMARD", "form": "Injection"},
    "source_system": "pharmacy_system"
  }'

Resolution Results

Merge automatically resolved the duplicates. Here is what happened:

Input Records Resolved Entity Sources Merged Resolution Method
"GlaxoSmithKline" + "GSK" + "Glaxo SmithKline" GlaxoSmithKline 3 Identity match (stock_ticker: GSK)
"Pfizer" + "Pfizer Inc" Pfizer Inc 2 Identity match (stock_ticker: PFE)
"Johnson & Johnson" + "Johnson and Johnson" Johnson and Johnson 2 Identity match (stock_ticker: JNJ)
"Humira" + "Humira" (pharmacy_system) Humira 2 Exact match (confidence: 0.95)
"Tylenol" vs "Acetaminophen" Separate entities 1 each Below threshold (brand vs generic)

GSK entity with 3 merged sources

Why the Identity Field Matters

The stock_ticker identity field is what made manufacturer resolution so reliable. Even though "GSK" looks nothing like "GlaxoSmithKline" from a fuzzy string matching perspective, the shared ticker symbol GSK provides a deterministic link. This is the power of combining fuzzy matching with deterministic identity fields.

Why Tylenol and Acetaminophen Stayed Separate

"Tylenol" (brand name) and "Acetaminophen" (generic name) correctly remained as separate entities. The fuzzy name match scored too low because the strings are completely different. The generic_name field matched ("Acetaminophen"), but without the same brand name, the overall confidence stayed below the merge threshold. This is the correct behavior — they represent different concepts in a pharmaceutical knowledge graph (the branded product vs. the active ingredient).


Step 5: Navigating the Knowledge Graph

With entities resolved and relationships established, we can traverse the graph:

# Get 1-hop graph for Lipitor
curl https://merge-ai.app/v1/entities/ent_602ccde6-0ab3-4687-8412-687db7525dbf/graph \
  -H "X-API-Key: $API_KEY"

Response:

{
  "entity_id": "ent_602ccde6-0ab3-4687-8412-687db7525dbf",
  "hops": 1,
  "nodes": [
    {"entity_id": "ent_602c...", "name": "Lipitor"},
    {"entity_id": "ent_213c...", "name": "Pfizer Inc"},
    {"entity_id": "ent_5cd9...", "name": "High Cholesterol"},
    {"entity_id": "ent_1d08...", "name": "ASCOT-LLA: Atorvastatin in Hypertensive Patients"}
  ],
  "edges": [
    {"from": "ent_602c...", "to": "ent_213c...", "type": "manufactured_by"},
    {"from": "ent_602c...", "to": "ent_5cd9...", "type": "treats"},
    {"from": "ent_602c...", "to": "ent_1d08...", "type": "studies"}
  ]
}

From a single entity, we can see that Lipitor:

  • Is manufactured by Pfizer Inc
  • Treats High Cholesterol
  • Is studied in the ASCOT-LLA clinical trial

Lipitor entity detail with relationships

Knowledge graph visualization


Step 6: Human-in-the-Loop Review

Not every match is clear-cut. When Merge encounters records that score between the auto-merge threshold and the review threshold, they go to human review.

In our dataset, two clinical trials triggered review:

  • "SURPASS-1: Tirzepatide Monotherapy for Type 2 Diabetes" was flagged as a potential match for "SUSTAIN-6: Semaglutide Cardiovascular Outcomes" (both Phase 3, Completed)
  • "ARMADA: Adalimumab in Rheumatoid Arthritis" was also flagged against the same candidate

The system correctly identified the attribute overlap (same phase, same status) but the title similarity was low enough (0.71 and 0.61) to route them for human review rather than auto-merging.

Pending reviews before resolution

We rejected both — these are distinct clinical trials that share metadata patterns but study different drugs. After rejection, Merge created new entities for each:

# Reject a false positive
curl -X POST https://merge-ai.app/v1/reviews/{review_id}/reject \
  -H "X-API-Key: $API_KEY"

The system learns from these decisions, improving future matching accuracy.


Step 7: Search and Discovery

Merge provides both fuzzy and semantic search across the resolved entity graph:

# Fuzzy search
curl "https://merge-ai.app/v1/entities/search?q=Pfizer&entity_type=Manufacturer" \
  -H "X-API-Key: $API_KEY"

Search results for Pfizer


Final Analytics

After all ingestion and resolution:

Metric Value
Total records ingested 27
Automatic merges 2
Reviews generated 2
Reviews rejected (false positives) 2
Final golden entities 25

Analytics overview


Tips for Pharmaceutical Entity Resolution

  1. Use deterministic identity fields — Stock tickers, NDC codes, ICD codes, and CAS numbers provide authoritative links that bypass fuzzy matching entirely. Mark these as identity: true in your schema.

  2. Separate brand from generic — Brand names (Tylenol) and generic names (Acetaminophen) are distinct concepts. Design your schema so they resolve correctly as separate entities unless you explicitly want them merged.

  3. Source system tagging — Always include source_system when ingesting. This makes provenance tracking possible and helps during review.

  4. Leverage relationships for context — A drug entity becomes far more useful when connected to its manufacturer, conditions, and clinical trials. Define relationship types upfront.

  5. Tune thresholds for your domain — Clinical trial titles are long and varied, so they need lower review thresholds. Manufacturer names with standard abbreviations benefit from identity fields over fuzzy matching.

  6. Human review for edge cases — Route uncertain matches to domain experts rather than auto-merging. In pharma, a false merge (combining two different drugs) is far more dangerous than a false split.

  7. Batch ingest for initial loads — Use the batch endpoint for historical data migration. Use single-record ingest for real-time feeds from pharmacy systems and registries.


Conclusion

Entity resolution in pharmaceutical data is not optional — it is a prerequisite for building reliable drug knowledge graphs. Without it, you cannot confidently answer questions like "What clinical trials has this manufacturer conducted?" or "Which drugs treat this condition?"

Merge handles the complexity of multi-source pharmaceutical data by combining:

  • Deterministic matching via identity fields (tickers, ICD codes)
  • Fuzzy matching for name variations and abbreviations
  • Human-in-the-loop review for ambiguous cases
  • Graph relationships that connect the resolved entities into a navigable knowledge graph

The result is a single source of truth for your pharmaceutical data — regardless of how many systems contribute records or how inconsistently they format entity names.

Get started at merge-ai.app.

Start free All solutions