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)
Step 2: Query Pattern 1 - Find Related Entities
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:
Entities from different content sources
Entities never co-occur
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?