Query Performance Patterns

Content: Query Performance Patterns

User Intent

"How do I optimize content queries for production?"

Operation

  • SDK Method: queryContents()

  • GraphQL: queryContents query

  • Entity Type: Content

  • Common Use Cases: Performance optimization, pagination, large result sets, production patterns

Performance Overview

Query performance depends on query type, filters used, result size, and indexing. Understanding these patterns helps optimize for production.

TypeScript (Canonical)

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

const graphlit = new Graphlit();

// Efficient pagination
const page1 = await graphlit.queryContents({
  search: "query",
  limit: 50,
  offset: 0
});

const page2 = await graphlit.queryContents({
  search: "query",
  limit: 50,
  offset: 50
});

// Use indexed filters for speed
const fast = await graphlit.queryContents({
  search: "machine learning",
  filter: {
    types: [ContentTypes.File],           // Indexed
    creationDateRange: { from: '2024-01-01' },  // Indexed
    collections: [{ id: 'collection-id' }]      // Indexed
  },
  limit: 20  // Reasonable limit
});

// Narrow scope before querying
const scoped = await graphlit.queryContents({
  filter: {
    types: [ContentTypes.Email],
    feeds: [{ id: 'specific-feed' }],  // Narrow to one feed
    createdInLast: 'P7D'               // Recent only
  }
});

Query Performance Characteristics

Fast Queries (<50ms)

// 1. Keyword search with indexed filters
const fast1 = await graphlit.queryContents({
  search: "PROJ-1234",
  searchType: SearchTypes.Keyword,
  filter: {
    types: [ContentTypes.File],
    creationDateRange: { from: '2024-01-01' }
  },
  limit: 20
});

// 2. Metadata-only filter (no search)
const fast2 = await graphlit.queryContents({
  filter: {
    types: [ContentTypes.Email],
    feeds: [{ id: 'feed-id' }],
    createdInLast: 'P7D'
  }
});

// 3. Get by ID (single result)
const fast3 = await graphlit.getContent('content-id');

Medium Queries (50-200ms)

// 1. Vector search
const medium1 = await graphlit.queryContents({
  search: "machine learning applications",
  searchType: SearchTypes.Vector,
  limit: 20
});

// 2. Hybrid search (default)
const medium2 = await graphlit.queryContents({
  search: "AI research",
  searchType: SearchTypes.Hybrid,
  limit: 20
});

// 3. Large result sets
const medium3 = await graphlit.queryContents({
  filter: {
    types: [ContentTypes.File]
  },
  limit: 100
});

Slow Queries (200ms+)

// 1. Entity filters (graph queries)
const slow1 = await graphlit.queryContents({
  filter: {
    observations: [{
      type: ObservableTypes.Person,
      observable: { id: 'person-id' }
    }]
  }
});

// 2. No filters (full scan)
const slow2 = await graphlit.queryContents({
  limit: 1000  // Large limit
});

// 3. Complex multi-entity filters
const slow3 = await graphlit.queryContents({
  filter: {
    observations: [
      { type: ObservableTypes.Person, observable: { id: 'p1' } },
      { type: ObservableTypes.Person, observable: { id: 'p2' } }
    ]
  }
});

Pagination Best Practices

Offset-Based Pagination

// Page size
const pageSize = 50;

// Get page function
async function getPage(pageNumber: number) {
  return await graphlit.queryContents({
    search: "query",
    limit: pageSize,
    offset: pageNumber * pageSize
  });
}

// Usage
const page1 = await getPage(0);  // First page
const page2 = await getPage(1);  // Second page
const page3 = await getPage(2);  // Third page

Progressive Loading

// Load results progressively
async function loadAllResults(query: string) {
  const pageSize = 50;
  let offset = 0;
  let allResults: any[] = [];
  let hasMore = true;
  
  while (hasMore) {
    const page = await graphlit.queryContents({
      search: query,
      limit: pageSize,
      offset: offset
    });
    
    allResults = allResults.concat(page.contents.results);
    offset += pageSize;
    
    // Stop if we got fewer results than page size
    hasMore = page.contents.results.length === pageSize;
    
    console.log(`Loaded ${allResults.length} results so far...`);
  }
  
  return allResults;
}

Limit Best Practices

// ✓ Good limits
limit: 10   // UI: Quick preview
limit: 20   // UI: Search results page
limit: 50   // API: Reasonable batch
limit: 100  // API: Large batch (use with caution)

// ✗ Avoid
limit: 1000  // Too large, slow queries
limit: 10000 // Will timeout

Caching Strategies

Client-Side Result Caching

// Simple in-memory cache
const queryCache = new Map<string, any>();

async function cachedQuery(query: string, ttlMs: number = 60000) {
  const cacheKey = `query:${query}`;
  const cached = queryCache.get(cacheKey);
  
  if (cached && Date.now() - cached.timestamp < ttlMs) {
    console.log('Cache hit');
    return cached.results;
  }
  
  console.log('Cache miss');
  const results = await graphlit.queryContents({ search: query });
  
  queryCache.set(cacheKey, {
    results,
    timestamp: Date.now()
  });
  
  return results;
}

// Usage
const results = await cachedQuery("machine learning", 60000);  // 1 minute TTL

Query Fingerprinting

// Cache by query fingerprint
function getQueryFingerprint(params: any): string {
  return JSON.stringify({
    search: params.search,
    filter: params.filter,
    searchType: params.searchType,
    limit: params.limit,
    offset: params.offset
  });
}

const cache = new Map<string, { results: any; timestamp: number }>();

async function cachedQueryByFingerprint(params: any, ttlMs: number = 60000) {
  const fingerprint = getQueryFingerprint(params);
  const cached = cache.get(fingerprint);
  
  if (cached && Date.now() - cached.timestamp < ttlMs) {
    return cached.results;
  }
  
  const results = await graphlit.queryContents(params);
  cache.set(fingerprint, { results, timestamp: Date.now() });
  
  return results;
}

Indexed vs Non-Indexed Fields

Indexed (Fast)

// These filters are indexed and fast:
const fast = await graphlit.queryContents({
  filter: {
    types: [...],                    // ✓ Indexed
    fileTypes: [...],                // ✓ Indexed
    creationDateRange: {...},        // ✓ Indexed
    dateRange: {...},                // ✓ Indexed
    feeds: [...],                    // ✓ Indexed
    collections: [...],              // ✓ Indexed
    workflows: [...],                // ✓ Indexed
    states: [...],                   // ✓ Indexed
    fileExtensions: [...],           // ✓ Indexed
    formats: [...],                  // ✓ Indexed
    fileSizeRange: {...}             // ✓ Indexed
  }
});

Non-Indexed (Slower)

// Entity filters require graph database lookup:
const slower = await graphlit.queryContents({
  filter: {
    observations: [...]              // ✗ Graph query (slower)
  }
});

// Custom metadata not indexed:
// Can't filter directly on custom metadata fields
// Must query all and filter client-side

Optimization Patterns

// ✓ Good: Narrow scope with filters first
const optimized = await graphlit.queryContents({
  search: "query",
  filter: {
    collections: [{ id: 'small-collection' }],  // Reduce search space
    createdInLast: 'P30D'                       // Recent only
  }
});

// ✗ Less optimal: Search everything
const slow = await graphlit.queryContents({
  search: "query"
  // No filters = searches all content
});

Pattern 2: Use Appropriate Search Type

// For exact matching: use keyword (fastest)
const exact = await graphlit.queryContents({
  search: "PROJ-1234",
  searchType: SearchTypes.Keyword  // Fast
});

// For concepts: use vector
const semantic = await graphlit.queryContents({
  search: "machine learning applications",
  searchType: SearchTypes.Vector
});

// For general queries: use hybrid (default, balanced)
const balanced = await graphlit.queryContents({
  search: "AI research papers"
  // Hybrid is default
});

Pattern 3: Batch Operations

// Get multiple content items efficiently
async function batchGetContent(ids: string[]) {
  // Parallel requests
  const results = await Promise.all(
    ids.map(id => graphlit.getContent(id))
  );
  return results;
}

// Better than sequential
// for (const id of ids) {
//   await graphlit.getContent(id);  // Slow!
// }

Pattern 4: Count vs Results

// If you only need count, don't fetch full results
const results = await graphlit.queryContents({
  filter: { types: [ContentTypes.Email] },
  limit: 1  // Minimal fetch
});

const count = results.contents.results.length;
// Use count for display, not actual results

Pagination

page1 = await graphlit.queryContents( search="query", limit=50, offset=0 )

page2 = await graphlit.queryContents( search="query", limit=50, offset=50 )

Optimized query

fast = await graphlit.queryContents( search="machine learning", filter=ContentFilterInput( types=[ContentTypes.File], creation_date_range=DateRangeInput( from_='2024-01-01' ) ), limit=20 )


**C#**:
```csharp
using Graphlit;

var client = new Graphlit();

// Pagination
var page1 = await graphlit.QueryContents(new ContentFilter
{
    Search = "query",
    Limit = 50,
    Offset = 0
});

var page2 = await graphlit.QueryContents(new ContentFilter
{
    Search = "query",
    Limit = 50,
    Offset = 50
});

// Optimized query
var fast = await graphlit.QueryContents(new ContentFilter
{
    Search = "machine learning",
    Filter = new ContentCriteria
    {
        Types = new[] { ContentTypes.File },
        CreationDateRange = new DateRange { From = "2024-01-01" }
    },
    Limit = 20
});

Developer Hints

Measure Query Performance

async function measureQuery(queryFn: () => Promise<any>) {
  const start = Date.now();
  const results = await queryFn();
  const elapsed = Date.now() - start;
  
  console.log(`Query time: ${elapsed}ms`);
  console.log(`Results: ${results.contents.results.length}`);
  console.log(`Ms per result: ${(elapsed / results.contents.results.length).toFixed(2)}`);
  
  return results;
}

// Usage
await measureQuery(() => graphlit.queryContents({ search: "query" }));

Parallelize Independent Queries

// ✓ Parallel (fast)
const [emails, messages, files] = await Promise.all([
  graphlit.queryContents({ filter: { types: [ContentTypes.Email] } }),
  graphlit.queryContents({ filter: { types: [ContentTypes.Message] } }),
  graphlit.queryContents({ filter: { types: [ContentTypes.File] } })
]);

// ✗ Sequential (slow)
const emails = await graphlit.queryContents({ filter: { types: [ContentTypes.Email] } });
const messages = await graphlit.queryContents({ filter: { types: [ContentTypes.Message] } });
const files = await graphlit.queryContents({ filter: { types: [ContentTypes.File] } });

Use Reasonable Limits

// ✓ Good: Reasonable page size
const results = await graphlit.queryContents({
  search: "query",
  limit: 50  // Good balance
});

// ✗ Bad: Too large
const huge = await graphlit.queryContents({
  search: "query",
  limit: 1000  // Will be slow
});

Common Issues & Solutions

Issue: Queries timing out Solution: Reduce scope with filters and limits

// ✗ Too broad
await graphlit.queryContents({ limit: 1000 });

// ✓ Narrowed
await graphlit.queryContents({
  filter: {
    collections: [{ id: 'collection-id' }],
    createdInLast: 'P7D'
  },
  limit: 50
});

Issue: Slow entity-based queries Solution: Expected behavior, optimize where possible

// Entity queries are slower (graph lookup)
// Optimize by combining with other filters
const optimized = await graphlit.queryContents({
  filter: {
    types: [ContentTypes.Email],      // Narrow first
    createdInLast: 'P30D',                  // Then narrow more
    observations: [{                         // Then entity filter
      type: ObservableTypes.Person,
      observable: { id: 'person-id' }
    }]
  }
});

Issue: Need all results but hitting limits Solution: Use pagination

async function getAllResults(filter: any) {
  const pageSize = 50;
  let allResults: any[] = [];
  let offset = 0;
  let hasMore = true;
  
  while (hasMore) {
    const page = await graphlit.queryContents({
      filter,
      limit: pageSize,
      offset
    });
    
    allResults.push(...page.contents.results);
    hasMore = page.contents.results.length === pageSize;
    offset += pageSize;
  }
  
  return allResults;
}

Production Example

class ContentQueryService {
  private cache = new Map<string, { data: any; timestamp: number }>();
  
  constructor(private client: Graphlit) {}
  
  // Optimized query with caching
  async query(params: {
    search?: string;
    filter?: any;
    limit?: number;
    offset?: number;
    useCache?: boolean;
    cacheTTL?: number;
  }) {
    const {
      search,
      filter,
      limit = 20,
      offset = 0,
      useCache = true,
      cacheTTL = 60000
    } = params;
    
    // Generate cache key
    const cacheKey = JSON.stringify({ search, filter, limit, offset });
    
    // Check cache
    if (useCache) {
      const cached = this.cache.get(cacheKey);
      if (cached && Date.now() - cached.timestamp < cacheTTL) {
        console.log('✓ Cache hit');
        return cached.data;
      }
    }
    
    // Measure performance
    const start = Date.now();
    
    // Execute query
    const results = await this.graphlit.queryContents({
      search,
      filter,
      limit,
      offset
    });
    
    const elapsed = Date.now() - start;
    console.log(`Query: ${elapsed}ms, Results: ${results.contents.results.length}`);
    
    // Cache results
    if (useCache) {
      this.cache.set(cacheKey, {
        data: results,
        timestamp: Date.now()
      });
    }
    
    return results;
  }
  
  // Paginated query
  async queryPaginated(params: {
    search?: string;
    filter?: any;
    pageSize?: number;
  }) {
    const { search, filter, pageSize = 50 } = params;
    
    let page = 0;
    
    return {
      async next() {
        const results = await this.query({
          search,
          filter,
          limit: pageSize,
          offset: page * pageSize
        });
        
        page++;
        
        return {
          results: results.contents.results,
          hasMore: results.contents.results.length === pageSize
        };
      }
    };
  }
  
  // Clear cache
  clearCache() {
    this.cache.clear();
  }
}

// Usage
const service = new ContentQueryService(client);

// Cached query
const results = await service.query({
  search: "machine learning",
  filter: { types: [ContentTypes.File] },
  useCache: true,
  cacheTTL: 300000  // 5 minutes
});

// Paginated query
const paginator = await service.queryPaginated({
  search: "AI research",
  pageSize: 50
});

let page1 = await paginator.next();
let page2 = await paginator.next();

Last updated

Was this helpful?