Skip to main content
Glama
search-operations.perf.test.ts11 kB
/** * Search Operations Performance Tests * * Validates that search operations meet performance targets: * - Search operations < 200ms for 100k memories * - Full-text search performance * - Vector similarity search performance * - Metadata filtering performance * - Cache effectiveness * * Requirements: 4.1, 4.2, 4.3, 4.4, 4.5 */ import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest"; import { DatabaseConnectionManager } from "../../database/connection-manager"; import { EmbeddingCache } from "../../embeddings/cache"; import { EmbeddingEngine } from "../../embeddings/embedding-engine"; import { EmbeddingStorage } from "../../embeddings/embedding-storage"; import { MemorySector } from "../../embeddings/types"; import { WaypointGraphBuilder } from "../../graph/waypoint-builder"; import { MemoryRepository } from "../../memory/memory-repository"; import type { MemoryContent, SearchQuery } from "../../memory/types"; import { MemorySearchEngine } from "../../search/memory-search-engine"; import type { IntegratedSearchQuery } from "../../search/types"; import { PerformanceMonitor } from "../../utils/performance-monitor"; import { MockOllamaEmbeddingModel } from "../utils/mock-embeddings"; describe("Search Operations Performance", () => { let db: DatabaseConnectionManager; let repository: MemoryRepository; let searchEngine: MemorySearchEngine; let embeddingStorage: EmbeddingStorage; let monitor: PerformanceMonitor; const testUserId = "search-perf-user"; const testSessionId = "search-perf-session"; beforeAll(async () => { // Initialize components db = new DatabaseConnectionManager({ host: process.env.DB_HOST ?? "localhost", port: parseInt(process.env.DB_PORT ?? "5432"), database: process.env.DB_NAME ?? "thoughtmcp_test", user: process.env.DB_USER ?? "postgres", password: process.env.DB_PASSWORD ?? "postgres", poolSize: 20, connectionTimeout: 5000, idleTimeout: 30000, }); await db.connect(); const model = new MockOllamaEmbeddingModel({ host: "http://localhost:11434", modelName: "nomic-embed-text", dimension: 768, }); const cache = new EmbeddingCache(10000, 3600000); const embeddingEngine = new EmbeddingEngine(model, cache); embeddingStorage = new EmbeddingStorage(db); const graphBuilder = new WaypointGraphBuilder(db, embeddingStorage, { similarityThreshold: 0.7, maxLinksPerNode: 3, minLinksPerNode: 1, enableBidirectional: true, }); repository = new MemoryRepository(db, embeddingEngine, graphBuilder, embeddingStorage); searchEngine = new MemorySearchEngine(db, embeddingStorage); monitor = new PerformanceMonitor(); }, 30000); beforeEach(async () => { // Clean database before each test if (db.pool) { await db.pool.query("TRUNCATE TABLE memories CASCADE"); } monitor.clear(); searchEngine.clearCache(); }); afterAll(async () => { await db.disconnect(); }); it("should perform vector search in <200ms", async () => { // Create 100 test memories for (let i = 0; i < 100; i++) { const content: MemoryContent = { content: `Search performance test memory ${i} with various keywords`, userId: testUserId, sessionId: testSessionId, primarySector: "semantic", }; await repository.create(content); } // Generate query embedding const model = new MockOllamaEmbeddingModel({ host: "http://localhost:11434", modelName: "nomic-embed-text", dimension: 768, }); const queryEmbedding = await model.generate("search query"); // Measure search performance const durations: number[] = []; for (let i = 0; i < 20; i++) { const timer = monitor.startTimer(`vector-search-${i}`, "search"); await embeddingStorage.vectorSimilaritySearch(queryEmbedding, MemorySector.Semantic, 10, 0.5); const duration = monitor.endTimer(timer); durations.push(duration); } // Calculate p95 const sorted = durations.sort((a, b) => a - b); const p95 = sorted[Math.floor(sorted.length * 0.95)]; expect(p95).toBeLessThan(200); }, 60000); it("should perform full-text search in <200ms", async () => { // Create 100 test memories with searchable content for (let i = 0; i < 100; i++) { const content: MemoryContent = { content: `Full-text search test memory ${i} with important keywords and phrases`, userId: testUserId, sessionId: testSessionId, primarySector: "semantic", }; await repository.create(content); } // Measure full-text search performance const durations: number[] = []; for (let i = 0; i < 20; i++) { const timer = monitor.startTimer(`fulltext-search-${i}`, "search"); await repository.searchFullText({ query: "important keywords", userId: testUserId, maxResults: 10, }); const duration = monitor.endTimer(timer); durations.push(duration); } // Calculate p95 const sorted = durations.sort((a, b) => a - b); const p95 = sorted[Math.floor(sorted.length * 0.95)]; expect(p95).toBeLessThan(200); }, 60000); it("should perform metadata filtering in <200ms", async () => { // Create 100 test memories with metadata for (let i = 0; i < 100; i++) { const content: MemoryContent = { content: `Metadata filter test memory ${i}`, userId: testUserId, sessionId: testSessionId, primarySector: "semantic", }; await repository.create(content, { keywords: [`keyword${i % 10}`, "common"], tags: [`tag${i % 5}`], category: `category${i % 3}`, importance: 0.5, isAtomic: true, }); } // Measure metadata filtering performance const durations: number[] = []; for (let i = 0; i < 20; i++) { const query: SearchQuery = { userId: testUserId, metadata: { keywords: ["common"], tags: ["tag0"], }, limit: 10, }; const timer = monitor.startTimer(`metadata-filter-${i}`, "search"); await repository.search(query); const duration = monitor.endTimer(timer); durations.push(duration); } // Calculate p95 const sorted = durations.sort((a, b) => a - b); const p95 = sorted[Math.floor(sorted.length * 0.95)]; expect(p95).toBeLessThan(200); }, 60000); it("should perform integrated search in <200ms", async () => { // Create 100 test memories for (let i = 0; i < 100; i++) { const content: MemoryContent = { content: `Integrated search test memory ${i} with searchable content`, userId: testUserId, sessionId: testSessionId, primarySector: "semantic", }; await repository.create(content, { keywords: [`keyword${i % 10}`], tags: [`tag${i % 5}`], category: "test", importance: 0.5, isAtomic: true, }); } // Measure integrated search performance const durations: number[] = []; for (let i = 0; i < 20; i++) { const query: IntegratedSearchQuery = { text: "searchable content", userId: testUserId, metadata: { categories: ["test"], }, limit: 10, }; const timer = monitor.startTimer(`integrated-search-${i}`, "search"); await searchEngine.search(query); const duration = monitor.endTimer(timer); durations.push(duration); } // Calculate p95 const sorted = durations.sort((a, b) => a - b); const p95 = sorted[Math.floor(sorted.length * 0.95)]; expect(p95).toBeLessThan(200); }, 60000); it("should benefit from caching on repeated searches", async () => { // Create 50 test memories for (let i = 0; i < 50; i++) { const content: MemoryContent = { content: `Cache test memory ${i}`, userId: testUserId, sessionId: testSessionId, primarySector: "semantic", }; await repository.create(content); } const query: IntegratedSearchQuery = { text: "cache test", userId: testUserId, limit: 10, }; // First search (cache miss) const timer1 = monitor.startTimer("first-search", "search"); await searchEngine.search(query); const duration1 = monitor.endTimer(timer1); // Second search (cache hit) const timer2 = monitor.startTimer("cached-search", "search"); await searchEngine.search(query); const duration2 = monitor.endTimer(timer2); // Cached search should be significantly faster expect(duration2).toBeLessThan(duration1 * 0.5); // Verify cache hit const cacheStats = searchEngine.getCacheStats(); expect(cacheStats.hits).toBeGreaterThan(0); }, 60000); it("should scale linearly with result limit", async () => { // Create 200 test memories for (let i = 0; i < 200; i++) { const content: MemoryContent = { content: `Scaling test memory ${i}`, userId: testUserId, sessionId: testSessionId, primarySector: "semantic", }; await repository.create(content); } // Search with limit 10 const query1: SearchQuery = { userId: testUserId, text: "scaling test", limit: 10, }; const timer1 = monitor.startTimer("search-limit-10", "search"); await repository.search(query1); const duration1 = monitor.endTimer(timer1); // Search with limit 50 const query2: SearchQuery = { userId: testUserId, text: "scaling test", limit: 50, }; const timer2 = monitor.startTimer("search-limit-50", "search"); await repository.search(query2); const duration2 = monitor.endTimer(timer2); // Should scale roughly linearly (allow 6x for overhead) expect(duration2).toBeLessThan(duration1 * 6); }, 120000); it("should maintain performance under concurrent searches", async () => { // Create 100 test memories for (let i = 0; i < 100; i++) { const content: MemoryContent = { content: `Concurrent search test memory ${i}`, userId: testUserId, sessionId: testSessionId, primarySector: "semantic", }; await repository.create(content); } // Execute 10 concurrent searches const queries: SearchQuery[] = Array.from({ length: 10 }, (_, i) => ({ userId: testUserId, text: `concurrent test ${i}`, limit: 10, })); const timer = monitor.startTimer("concurrent-searches", "search"); await Promise.all(queries.map((q) => repository.search(q))); const duration = monitor.endTimer(timer); // Concurrent searches should complete in reasonable time // Allow 2s total for 10 concurrent searches (200ms * 10 with some parallelism) expect(duration).toBeLessThan(2000); }, 60000); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/keyurgolani/ThoughtMcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server