Query Entity Relationships in Knowledge Graph

User Intent

"How do I query relationships between entities? Show me how to find all people at an organization, products by a company, or events at a location."

Operation

SDK Methods: queryObservables() with relationship filters, queryContents() with entity filters GraphQL Query: queryObservables, queryContents Entity: Observable relationships and co-occurrence patterns

Prerequisites

  • Graphlit project with extracted entities (knowledge graph built)

  • Understanding of Observable/Observation model

  • Content with entity extraction completed


Complete Code Example (TypeScript)

import { Graphlit } from 'graphlit-client';
import { ObservableTypes, EntityState } from 'graphlit-client/dist/generated/graphql-types';

const graphlit = new Graphlit();

console.log('=== Querying Entity Relationships ===\n');

// Example 1: Find all people at Graphlit
console.log('Example 1: Person → Organization\n');

// First, find the Graphlit organization entity
const graphlitOrg = await graphlit.queryObservables({
  search: "Graphlit",
  filter: {
    types: [ObservableTypes.Organization],
    states: [EntityState.Enabled]
  }
});

if (graphlitOrg.observables.results.length > 0) {
  const orgId = graphlitOrg.observables.results[0].observable.id;
  console.log(`Found organization: ${graphlitOrg.observables.results[0].observable.name}\n`);
  
  // Find all content mentioning Graphlit
  const graphlitContent = await graphlit.queryContents({
    filter: {
      observations: [{
        type: ObservableTypes.Organization,
        observable: { id: orgId }
      }]
    }
  });
  
  // Extract all people mentioned in that content
  const peopleAtGraphlit = new Map<string, { id: string; name: string }>();
  
  graphlitContent.contents.results.forEach(content => {
    content.observations
      ?.filter(obs => obs.type === ObservableTypes.Person)
      .forEach(obs => {
        peopleAtGraphlit.set(obs.observable.id, {
          id: obs.observable.id,
          name: obs.observable.name
        });
      });
  });
  
  console.log(`People at Graphlit: ${peopleAtGraphlit.size}`);
  Array.from(peopleAtGraphlit.values()).slice(0, 5).forEach(person => {
    console.log(`  - ${person.name}`);
  });
  if (peopleAtGraphlit.size > 5) {
    console.log(`  ... and ${peopleAtGraphlit.size - 5} more`);
  }
}

console.log('\n---\n');

// Example 2: Find all events at a location
console.log('Example 2: Event → Place\n');

// Find a place entity
const seattle = await graphlit.queryObservables({
  search: "Seattle",
  filter: {
    types: [ObservableTypes.Place],
    states: [EntityState.Enabled]
  }
});

if (seattle.observables.results.length > 0) {
  const placeId = seattle.observables.results[0].observable.id;
  console.log(`Found place: ${seattle.observables.results[0].observable.name}\n`);
  
  // Find content mentioning both events and this place
  const contentWithPlace = await graphlit.queryContents({
    filter: {
      observations: [{
        type: ObservableTypes.Place,
        observable: { id: placeId }
      }]
    }
  });
  
  // Extract events from that content
  const eventsAtPlace = new Set<string>();
  
  contentWithPlace.contents.results.forEach(content => {
    content.observations
      ?.filter(obs => obs.type === ObservableTypes.Event)
      .forEach(obs => {
        eventsAtPlace.add(obs.observable.name);
      });
  });
  
  console.log(`Events in Seattle: ${eventsAtPlace.size}`);
  Array.from(eventsAtPlace).slice(0, 5).forEach(event => {
    console.log(`  - ${event}`);
  });
}

console.log('\n---\n');

// Example 3: Find products by organization
console.log('Example 3: Product → Organization\n');

// Find Microsoft
const microsoft = await graphlit.queryObservables({
  search: "Microsoft",
  filter: {
    types: [ObservableTypes.Organization],
    states: [EntityState.Enabled]
  }
});

if (microsoft.observables.results.length > 0) {
  const msftId = microsoft.observables.results[0].observable.id;
  console.log(`Found organization: ${microsoft.observables.results[0].observable.name}\n`);
  
  // Find content mentioning Microsoft
  const msftContent = await graphlit.queryContents({
    filter: {
      observations: [{
        type: ObservableTypes.Organization,
        observable: { id: msftId }
      }]
    }
  });
  
  // Extract products
  const msftProducts = new Map<string, number>();
  
  msftContent.contents.results.forEach(content => {
    content.observations
      ?.filter(obs => obs.type === ObservableTypes.Product)
      .forEach(obs => {
        msftProducts.set(
          obs.observable.name,
          (msftProducts.get(obs.observable.name) || 0) + 1
        );
      });
  });
  
  console.log(`Products by Microsoft: ${msftProducts.size}`);
  Array.from(msftProducts.entries())
    .sort((a, b) => b[1] - a[1])
    .slice(0, 5)
    .forEach(([product, count]) => {
      console.log(`  - ${product} (mentioned ${count} times)`);
    });
}

console.log('\n---\n');

// Example 4: Multi-hop relationship (Person → Organization → Event)
console.log('Example 4: Multi-hop Relationship (Person → Org → Event)\n');

// Find a person
const person = await graphlit.queryObservables({
  search: "Kirk Marple",
  filter: {
    types: [ObservableTypes.Person],
    states: [EntityState.Enabled]
  }
});

if (person.observables.results.length > 0) {
  const personId = person.observables.results[0].observable.id;
  console.log(`Person: ${person.observables.results[0].observable.name}\n`);
  
  // Step 1: Find organizations this person is associated with
  const personContent = await graphlit.queryContents({
    filter: {
      observations: [{
        type: ObservableTypes.Person,
        observable: { id: personId }
      }]
    }
  });
  
  const relatedOrgs = new Set<string>();
  personContent.contents.results.forEach(content => {
    content.observations
      ?.filter(obs => obs.type === ObservableTypes.Organization)
      .forEach(obs => {
        relatedOrgs.add(obs.observable.id);
      });
  });
  
  console.log(`Associated organizations: ${relatedOrgs.size}`);
  
  // Step 2: For each organization, find events
  const allEvents = new Set<string>();
  
  for (const orgId of Array.from(relatedOrgs).slice(0, 3)) {  // Limit for demo
    const orgEvents = await graphlit.queryContents({
      filter: {
        observations: [{
          type: ObservableTypes.Organization,
          observable: { id: orgId }
        }]
      }
    });
    
    orgEvents.contents.results.forEach(content => {
      content.observations
        ?.filter(obs => obs.type === ObservableTypes.Event)
        .forEach(obs => {
          allEvents.add(obs.observable.name);
        });
    });
  }
  
  console.log(`Events related to person's organizations: ${allEvents.size}`);
  Array.from(allEvents).slice(0, 5).forEach(event => {
    console.log(`  - ${event}`);
  });
}

console.log('\n✓ Relationship queries complete!');

Step-by-Step Explanation

Step 1: Understanding Relationship Types

Direct Relationships (inferred from co-occurrence):

  • Person → Organization: People work at organizations

  • Event → Place: Events happen at locations

  • Product → Organization: Companies make products

  • Person → Event: People attend/organize events

  • Software → Organization: Companies develop software

Implicit Relationships (from content context):

  • Entities mentioned together in same document

  • Entities on same page (PDF documents)

  • Entities in same conversation thread

  • Entities in same time window (audio/video)

Pattern: Entity A → Entity B

// 1. Find entity A
const entityA = await graphlit.queryObservables({
  search: "Entity A Name",
  filter: { types: [ObservableTypeA] }
});

const entityAId = entityA.observables.results[0].observable.id;

// 2. Find content mentioning entity A
const content = await graphlit.queryContents({
  filter: {
    observations: [{
      type: ObservableTypeA,
      observable: { id: entityAId }
    }]
  }
});

// 3. Extract entity B from that content
const relatedEntitiesB = new Set();
content.contents.results.forEach(item => {
  item.observations
    ?.filter(obs => obs.type === ObservableTypeB)
    .forEach(obs => {
      relatedEntitiesB.add(obs.observable);
    });
});

Step 3: Query Pattern 2 - Co-Occurrence Filtering

Pattern: Find content with BOTH entities

// Find content mentioning both Person X and Organization Y
const cooccurrence = await graphlit.queryContents({
  filter: {
    observations: [
      {
        type: ObservableTypes.Person,
        observable: { id: personId }
      },
      {
        type: ObservableTypes.Organization,
        observable: { id: orgId }
      }
    ]
  }
});

// This returns only content where BOTH entities appear
console.log(`Co-occurrence count: ${cooccurrence.contents.results.length}`);

Step 4: Query Pattern 3 - Multi-Hop Relationships

Pattern: Entity A → Entity B → Entity C

// Example: Person → Organization → Event
// "Find events at companies where Person X works"

// Step 1: Person → Organizations
const personContent = await graphlit.queryContents({
  filter: {
    observations: [{ type: ObservableTypes.Person, observable: { id: personId } }]
  }
});

const orgs = new Set<string>();
personContent.contents.results.forEach(content => {
  content.observations
    ?.filter(obs => obs.type === ObservableTypes.Organization)
    .forEach(obs => orgs.add(obs.observable.id));
});

// Step 2: Organizations → Events
const allEvents = new Set<string>();
for (const orgId of orgs) {
  const orgContent = await graphlit.queryContents({
    filter: {
      observations: [{ type: ObservableTypes.Organization, observable: { id: orgId } }]
    }
  });
  
  orgContent.contents.results.forEach(content => {
    content.observations
      ?.filter(obs => obs.type === ObservableTypes.Event)
      .forEach(obs => allEvents.add(obs.observable.name));
  });
}

Step 5: Relationship Strength (Frequency)

Pattern: Count co-occurrences to measure relationship strength

interface Relationship {
  entityA: string;
  entityB: string;
  strength: number;  // Number of co-occurrences
}

function calculateRelationshipStrength(
  entityAId: string,
  entityBId: string
): number {
  const cooccurrence = await graphlit.queryContents({
    filter: {
      observations: [
        { type: ObservableTypes.Person, observable: { id: entityAId } },
        { type: ObservableTypes.Organization, observable: { id: entityBId } }
      ]
    }
  });
  
  return cooccurrence.contents.results.length;
}

// Find strongest Person-Organization relationships
const relationships: Relationship[] = [];

for (const person of people) {
  for (const org of organizations) {
    const strength = await calculateRelationshipStrength(person.id, org.id);
    if (strength > 0) {
      relationships.push({
        entityA: person.name,
        entityB: org.name,
        strength
      });
    }
  }
}

relationships.sort((a, b) => b.strength - a.strength);

Configuration Options

Filtering by Content Type

// Only find relationships in emails
const emailRelationships = await graphlit.queryContents({
  filter: {
    types: [ContentTypes.Email],
    observations: [{
      type: ObservableTypes.Person,
      observable: { id: personId }
    }]
  }
});

// Only in Slack messages
const slackRelationships = await graphlit.queryContents({
  filter: {
    types: [ContentTypes.Message],
    observations: [{
      type: ObservableTypes.Person,
      observable: { id: personId }
    }]
  }
});

Time-Based Relationship Queries

// Find relationships in last 30 days
const recentRelationships = await graphlit.queryContents({
  filter: {
    creationDateRange: {
      from: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString()
    },
    observations: [{
      type: ObservableTypes.Person,
      observable: { id: personId }
    }]
  }
});

Confidence-Based Filtering

// Only high-confidence relationships
const content = await graphlit.queryContents({
  filter: {
    observations: [{
      type: ObservableTypes.Person,
      observable: { id: personId }
    }]
  }
});

// Filter by confidence client-side
const highConfidence = content.contents.results.filter(item =>
  item.observations?.some(obs =>
    obs.observable.id === orgId &&
    obs.occurrences?.some(occ => occ.confidence >= 0.8)
  )
);

Variations

Variation 1: Build Network Graph

Create network visualization data:

interface NetworkNode {
  id: string;
  name: string;
  type: string;
}

interface NetworkEdge {
  source: string;
  target: string;
  weight: number;
}

async function buildNetworkGraph(
  startEntityId: string,
  startEntityType: ObservableTypes,
  depth: number = 2
): Promise<{ nodes: NetworkNode[]; edges: NetworkEdge[] }> {
  const nodes = new Map<string, NetworkNode>();
  const edges: NetworkEdge[] = [];
  const visited = new Set<string>();
  
  async function traverse(entityId: string, entityType: ObservableTypes, currentDepth: number) {
    if (currentDepth > depth || visited.has(entityId)) return;
    visited.add(entityId);
    
    // Get content mentioning this entity
    const content = await graphlit.queryContents({
      filter: {
        observations: [{
          type: entityType,
          observable: { id: entityId }
        }]
      }
    });
    
    // Extract all other entities from that content
    content.contents.results.forEach(item => {
      item.observations?.forEach(obs => {
        // Add node
        if (!nodes.has(obs.observable.id)) {
          nodes.set(obs.observable.id, {
            id: obs.observable.id,
            name: obs.observable.name,
            type: obs.type
          });
        }
        
        // Add edge
        if (obs.observable.id !== entityId) {
          edges.push({
            source: entityId,
            target: obs.observable.id,
            weight: 1
          });
        }
        
        // Recursively traverse
        if (currentDepth < depth) {
          traverse(obs.observable.id, obs.type, currentDepth + 1);
        }
      });
    });
  }
  
  await traverse(startEntityId, startEntityType, 0);
  
  return {
    nodes: Array.from(nodes.values()),
    edges
  };
}

// Build 2-hop network from person
const network = await buildNetworkGraph(personId, ObservableTypes.Person, 2);
console.log(`Network: ${network.nodes.length} nodes, ${network.edges.length} edges`);

// Export for visualization (D3.js, Cytoscape, etc.)
fs.writeFileSync('network.json', JSON.stringify(network, null, 2));

Variation 2: Relationship Timeline

Track when relationships formed:

interface RelationshipTimeline {
  entityA: string;
  entityB: string;
  firstMention: Date;
  lastMention: Date;
  mentions: Array<{ date: Date; contentId: string }>;
}

async function buildRelationshipTimeline(
  entityAId: string,
  entityBId: string
): Promise<RelationshipTimeline> {
  const content = await graphlit.queryContents({
    filter: {
      observations: [
        { observable: { id: entityAId } },
        { observable: { id: entityBId } }
      ]
    },
    orderBy: { creationDate: 'ASCENDING' }
  });
  
  const mentions = content.contents.results.map(item => ({
    date: new Date(item.creationDate),
    contentId: item.id
  }));
  
  return {
    entityA: entityAId,
    entityB: entityBId,
    firstMention: mentions[0]?.date,
    lastMention: mentions[mentions.length - 1]?.date,
    mentions
  };
}

const timeline = await buildRelationshipTimeline(personId, orgId);
console.log(`First mentioned together: ${timeline.firstMention.toLocaleDateString()}`);
console.log(`Last mentioned together: ${timeline.lastMention.toLocaleDateString()}`);
console.log(`Total mentions: ${timeline.mentions.length}`);

Variation 3: Entity Influence Score

Rank entities by relationship count:

interface InfluenceScore {
  entityId: string;
  entityName: string;
  relationshipCount: number;
  uniqueConnections: number;
}

async function calculateInfluence(
  entityType: ObservableTypes
): Promise<InfluenceScore[]> {
  // Get all entities of type
  const entities = await graphlit.queryObservables({
    filter: { types: [entityType] }
  });
  
  const scores: InfluenceScore[] = [];
  
  for (const entity of entities.observables.results) {
    // Find all content mentioning this entity
    const content = await graphlit.queryContents({
      filter: {
        observations: [{
          type: entityType,
          observable: { id: entity.observable.id }
        }]
      }
    });
    
    // Count unique connected entities
    const connections = new Set<string>();
    content.contents.results.forEach(item => {
      item.observations?.forEach(obs => {
        if (obs.observable.id !== entity.observable.id) {
          connections.add(obs.observable.id);
        }
      });
    });
    
    scores.push({
      entityId: entity.observable.id,
      entityName: entity.observable.name,
      relationshipCount: content.contents.results.length,
      uniqueConnections: connections.size
    });
  }
  
  return scores.sort((a, b) => b.uniqueConnections - a.uniqueConnections);
}

// Find most connected people
const topPeople = await calculateInfluence(ObservableTypes.Person);
console.log('Most connected people:');
topPeople.slice(0, 10).forEach((score, i) => {
  console.log(`${i + 1}. ${score.entityName}: ${score.uniqueConnections} connections`);
});

Variation 4: Relationship Path Finding

Find shortest path between two entities:

async function findPath(
  startId: string,
  endId: string,
  maxDepth: number = 5
): Promise<string[] | null> {
  const queue: Array<{ id: string; path: string[] }> = [
    { id: startId, path: [startId] }
  ];
  const visited = new Set<string>();
  
  while (queue.length > 0) {
    const { id, path } = queue.shift()!;
    
    if (id === endId) {
      return path;  // Found!
    }
    
    if (path.length > maxDepth || visited.has(id)) {
      continue;
    }
    visited.add(id);
    
    // Find connected entities
    const content = await graphlit.queryContents({
      filter: {
        observations: [{ observable: { id } }]
      }
    });
    
    const connected = new Set<string>();
    content.contents.results.forEach(item => {
      item.observations?.forEach(obs => {
        if (obs.observable.id !== id && !visited.has(obs.observable.id)) {
          connected.add(obs.observable.id);
        }
      });
    });
    
    // Add to queue
    for (const connectedId of connected) {
      queue.push({
        id: connectedId,
        path: [...path, connectedId]
      });
    }
  }
  
  return null;  // No path found
}

// Find path between two people
const path = await findPath(personAId, personBId, 5);
if (path) {
  console.log(`Path found (${path.length - 1} hops):`);
  console.log(path.join(' → '));
} else {
  console.log('No path found');
}

Variation 5: Relationship Export for Analysis

Export relationship data for external analysis:

interface RelationshipExport {
  nodes: Array<{ id: string; name: string; type: string; properties: any }>;
  edges: Array<{ source: string; target: string; type: string; weight: number }>;
}

async function exportRelationships(): Promise<RelationshipExport> {
  const nodes: RelationshipExport['nodes'] = [];
  const edges: RelationshipExport['edges'] = [];
  
  // Get all observables
  const allObservables = await graphlit.queryObservables({});
  
  // Add nodes
  allObservables.observables.results.forEach(obs => {
    nodes.push({
      id: obs.observable.id,
      name: obs.observable.name,
      type: obs.type,
      properties: obs.observable.properties || {}
    });
  });
  
  // Find edges (co-occurrences)
  const processed = new Set<string>();
  
  for (const obsA of allObservables.observables.results) {
    const content = await graphlit.queryContents({
      filter: {
        observations: [{ observable: { id: obsA.observable.id } }]
      }
    });
    
    content.contents.results.forEach(item => {
      item.observations?.forEach(obsB => {
        const key = [obsA.observable.id, obsB.observable.id].sort().join('-');
        if (obsA.observable.id !== obsB.observable.id && !processed.has(key)) {
          processed.add(key);
          edges.push({
            source: obsA.observable.id,
            target: obsB.observable.id,
            type: 'CO_OCCURS',
            weight: 1
          });
        }
      });
    });
  }
  
  return { nodes, edges };
}

// Export to file
const export_data = await exportRelationships();
fs.writeFileSync('relationships.json', JSON.stringify(export_data, null, 2));
console.log(`Exported ${export_data.nodes.length} nodes and ${export_data.edges.length} edges`);

Common Issues & Solutions

Issue: Too Many API Calls for Large Graphs

Problem: Multi-hop queries make hundreds of API calls.

Solution: Batch queries and cache results:

// Cache entity content
const contentCache = new Map<string, typeof queryContentsResult>();

async function getCachedContent(entityId: string) {
  if (!contentCache.has(entityId)) {
    const content = await graphlit.queryContents({
      filter: {
        observations: [{ observable: { id: entityId } }]
      }
    });
    contentCache.set(entityId, content);
  }
  return contentCache.get(entityId)!;
}

Issue: No Relationships Found

Problem: Query returns no related entities.

Causes:

  1. Entities from different content sources

  2. Entities never co-occur

  3. Filters too restrictive

Solution: Broaden query:

// Remove date/type filters
const content = await graphlit.queryContents({
  filter: {
    observations: [{ observable: { id: entityId } }]
    // No other filters
  }
});

Issue: Duplicate Relationships

Problem: Same relationship counted multiple times.

Solution: Deduplicate by entity ID:

const unique = new Map<string, Entity>();
entities.forEach(entity => {
  unique.set(entity.id, entity);
});
const deduplicated = Array.from(unique.values());

Issue: Slow Multi-Hop Queries

Problem: Deep relationship traversal very slow.

Solution: Limit depth and parallelize:

// Limit to 2-3 hops max
const maxDepth = 2;

// Parallelize queries
const results = await Promise.all(
  entityIds.map(id => queryRelationships(id))
);

Developer Hints

Relationship Query Performance

  • Direct relationships (1-hop): Fast (<1s)

  • 2-hop relationships: Moderate (1-5s)

  • 3+ hop relationships: Slow (5-30s)

  • Cache aggressively for large graphs

Relationship Types by Content

  • Emails: Strong Person-Person, Person-Organization

  • Slack: Person-Person, Organization-Product

  • Documents: All types, especially Product-Organization

  • GitHub: Person-Repo, Software-Organization

Co-Occurrence Reliability

  • Same page (PDF): Very strong relationship indicator

  • Same document: Strong relationship

  • Same thread (Slack/email): Strong relationship

  • Different documents: Weaker relationship

Graph Traversal Strategies

  • Breadth-first: Find shortest paths

  • Depth-first: Explore deep relationships

  • Limited depth: Prevent explosion (max 3 hops)

  • Type-specific: Only traverse certain entity types


Last updated

Was this helpful?