# 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)

```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({
    
      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({
    
      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({
    
      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({
    
      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({
      
        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)

### Step 2: Query Pattern 1 - Find Related Entities

**Pattern**: Entity A → Entity B

```typescript
// 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({
  
    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

```typescript
// Find content mentioning both Person X and Organization Y
const cooccurrence = await graphlit.queryContents({
  
    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

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

// Step 1: Person → Organizations
const personContent = await graphlit.queryContents({
  
    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({
    
      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

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

function calculateRelationshipStrength(
  entityAId: string,
  entityBId: string
): number {
  const cooccurrence = await graphlit.queryContents({
    
      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

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

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

### Time-Based Relationship Queries

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

### Confidence-Based Filtering

```typescript
// Only high-confidence relationships
const content = await graphlit.queryContents({
  
    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:

```typescript
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({
      
        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:

```typescript
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({
    
      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:

```typescript
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({
      
        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:

```typescript
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({
      
        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:

```typescript
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({
      
        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:

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

async function getCachedContent(entityId: string) {
  if (!contentCache.has(entityId)) {
    const content = await graphlit.queryContents({
      
        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:

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

### Issue: Duplicate Relationships

**Problem**: Same relationship counted multiple times.

**Solution**: Deduplicate by entity ID:

```typescript
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:

```typescript
// 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

***


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.graphlit.dev/api-guides/use-cases/knowledge-graph/observable-relationship-queries.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
