- Added SearchCursor and PaginatedSearchResult interfaces for cursor-based pagination support. - Introduced SearchCache class to cache search results, improving performance. - Implemented tests for automatic cache configuration and performance improvements. - Enhanced existing tests to validate pagination and caching behavior.
257 lines
No EOL
7.9 KiB
TypeScript
257 lines
No EOL
7.9 KiB
TypeScript
/**
|
|
* Tests for performance improvements: caching and cursor-based pagination
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
|
import { BrainyData } from '../src/brainyData.js'
|
|
import { cleanupWorkerPools } from '../src/utils/index.js'
|
|
|
|
describe('Performance Improvements', () => {
|
|
let db: BrainyData
|
|
|
|
beforeEach(async () => {
|
|
// Initialize BrainyData with caching enabled
|
|
db = new BrainyData({
|
|
storage: {
|
|
forceMemoryStorage: true
|
|
},
|
|
searchCache: {
|
|
enabled: true,
|
|
maxSize: 50,
|
|
maxAge: 60000 // 1 minute
|
|
},
|
|
logging: {
|
|
verbose: false
|
|
}
|
|
})
|
|
await db.init()
|
|
})
|
|
|
|
afterEach(async () => {
|
|
await db.clear()
|
|
await cleanupWorkerPools()
|
|
})
|
|
|
|
describe('Search Result Caching', () => {
|
|
it('should cache search results transparently', async () => {
|
|
// Add test data
|
|
for (let i = 0; i < 10; i++) {
|
|
await db.add({
|
|
id: `item-${i}`,
|
|
text: `test document ${i}`,
|
|
value: i
|
|
})
|
|
}
|
|
|
|
// Clear cache to start fresh
|
|
db.clearCache()
|
|
let stats = db.getCacheStats()
|
|
expect(stats.search.hits).toBe(0)
|
|
expect(stats.search.misses).toBe(0)
|
|
|
|
// First search - should be cache miss
|
|
const query = 'test document'
|
|
const results1 = await db.search(query, 5)
|
|
expect(results1.length).toBe(5)
|
|
|
|
stats = db.getCacheStats()
|
|
expect(stats.search.misses).toBe(1) // First search is a miss
|
|
expect(stats.search.hits).toBe(0)
|
|
|
|
// Second identical search - should be cache hit
|
|
const results2 = await db.search(query, 5)
|
|
expect(results2.length).toBe(5)
|
|
expect(results2).toEqual(results1) // Same results
|
|
|
|
stats = db.getCacheStats()
|
|
expect(stats.search.misses).toBe(1) // Still only one miss
|
|
expect(stats.search.hits).toBe(1) // Now we have a hit
|
|
expect(stats.search.hitRate).toBe(0.5) // 50% hit rate
|
|
})
|
|
|
|
it('should invalidate cache when data changes', async () => {
|
|
// Add test data
|
|
for (let i = 0; i < 5; i++) {
|
|
await db.add({
|
|
id: `item-${i}`,
|
|
text: `test document ${i}`
|
|
})
|
|
}
|
|
|
|
// Search and cache results
|
|
const results1 = await db.search('test document', 3)
|
|
expect(results1.length).toBe(3)
|
|
|
|
let stats = db.getCacheStats()
|
|
const initialMisses = stats.search.misses
|
|
|
|
// Same search should hit cache
|
|
await db.search('test document', 3)
|
|
stats = db.getCacheStats()
|
|
expect(stats.search.hits).toBeGreaterThan(0)
|
|
|
|
// Add new data - should invalidate cache
|
|
await db.add({
|
|
id: 'new-item',
|
|
text: 'new test document'
|
|
})
|
|
|
|
// Same search should miss cache (due to invalidation)
|
|
await db.search('test document', 3)
|
|
stats = db.getCacheStats()
|
|
// Cache was cleared, so we should have fewer misses recorded than expected
|
|
// The key point is that cache size should be 0 or low, indicating invalidation worked
|
|
expect(stats.search.size).toBeLessThanOrEqual(1) // Cache should have been cleared
|
|
})
|
|
|
|
it('should handle cache with different search parameters', async () => {
|
|
// Add test data
|
|
for (let i = 0; i < 10; i++) {
|
|
await db.add({
|
|
id: `item-${i}`,
|
|
text: `test document ${i}`
|
|
})
|
|
}
|
|
|
|
db.clearCache()
|
|
|
|
// Different k values should create different cache entries
|
|
await db.search('test document', 3)
|
|
await db.search('test document', 5) // Different k
|
|
await db.search('test document', 3) // Should hit cache
|
|
|
|
const stats = db.getCacheStats()
|
|
expect(stats.search.hits).toBe(1)
|
|
expect(stats.search.misses).toBe(2)
|
|
})
|
|
})
|
|
|
|
describe('Cursor-based Pagination', () => {
|
|
beforeEach(async () => {
|
|
// Add more test data for pagination
|
|
for (let i = 0; i < 50; i++) {
|
|
await db.add({
|
|
id: `doc-${i.toString().padStart(2, '0')}`,
|
|
text: `document content for testing pagination ${i}`,
|
|
index: i
|
|
})
|
|
}
|
|
})
|
|
|
|
it('should return cursor for pagination', async () => {
|
|
const page1 = await db.searchWithCursor('document content for testing', 10)
|
|
|
|
expect(page1.results.length).toBe(10)
|
|
expect(page1.hasMore).toBe(true)
|
|
expect(page1.cursor).toBeDefined()
|
|
expect(page1.cursor!.lastId).toBeDefined()
|
|
expect(page1.cursor!.lastScore).toBeDefined()
|
|
})
|
|
|
|
it('should paginate consistently with cursor', async () => {
|
|
const page1 = await db.searchWithCursor('document content for testing', 5)
|
|
expect(page1.results.length).toBe(5)
|
|
expect(page1.hasMore).toBe(true)
|
|
|
|
// Get next page using cursor
|
|
const page2 = await db.searchWithCursor('document content for testing', 5, {
|
|
cursor: page1.cursor
|
|
})
|
|
expect(page2.results.length).toBe(5)
|
|
|
|
// Ensure no overlap between pages
|
|
const page1Ids = page1.results.map(r => r.id)
|
|
const page2Ids = page2.results.map(r => r.id)
|
|
const overlap = page1Ids.filter(id => page2Ids.includes(id))
|
|
expect(overlap.length).toBe(0)
|
|
})
|
|
|
|
it('should handle last page correctly', async () => {
|
|
// Get small pages to reach the end
|
|
let currentCursor: any = undefined
|
|
let allResults: any[] = []
|
|
let pageCount = 0
|
|
const maxPages = 10 // Safety limit
|
|
|
|
while (pageCount < maxPages) {
|
|
const page = await db.searchWithCursor('document content', 5, {
|
|
cursor: currentCursor
|
|
})
|
|
|
|
allResults.push(...page.results)
|
|
pageCount++
|
|
|
|
if (!page.hasMore) {
|
|
expect(page.cursor).toBeUndefined()
|
|
break
|
|
}
|
|
|
|
currentCursor = page.cursor
|
|
}
|
|
|
|
expect(pageCount).toBeLessThan(maxPages) // Should have finished before limit
|
|
expect(allResults.length).toBeGreaterThan(0)
|
|
})
|
|
|
|
it('should provide total estimate when possible', async () => {
|
|
const page = await db.searchWithCursor('document content', 50) // Request more than we have
|
|
|
|
// Should get all results in one page
|
|
expect(page.results.length).toBeGreaterThan(0)
|
|
expect(page.hasMore).toBe(false)
|
|
expect(page.totalEstimate).toBeDefined()
|
|
expect(page.totalEstimate).toBe(page.results.length)
|
|
})
|
|
})
|
|
|
|
describe('Performance Characteristics', () => {
|
|
it('should show performance improvement with caching', async () => {
|
|
// Add substantial test data
|
|
for (let i = 0; i < 50; i++) {
|
|
await db.add({
|
|
id: `perf-test-${i}`,
|
|
text: `performance test document ${i} with some content`
|
|
})
|
|
}
|
|
|
|
// Clear cache and perform first search
|
|
db.clearCache()
|
|
const results1 = await db.search('performance test document', 10)
|
|
|
|
let stats = db.getCacheStats()
|
|
expect(stats.search.misses).toBe(1) // First search is a miss
|
|
expect(stats.search.hits).toBe(0)
|
|
|
|
// Perform second identical search (should hit cache)
|
|
const results2 = await db.search('performance test document', 10)
|
|
|
|
expect(results1).toEqual(results2) // Same results
|
|
|
|
stats = db.getCacheStats()
|
|
expect(stats.search.hits).toBe(1) // Second search is a hit
|
|
expect(stats.search.hitRate).toBe(0.5) // 50% hit rate (1 hit, 1 miss)
|
|
})
|
|
|
|
it('should provide cache memory usage information', async () => {
|
|
// Add some data and search to populate cache
|
|
for (let i = 0; i < 10; i++) {
|
|
await db.add({
|
|
id: `mem-test-${i}`,
|
|
text: `memory test ${i}`
|
|
})
|
|
}
|
|
|
|
db.clearCache()
|
|
|
|
// Perform several searches to populate cache
|
|
await db.search('memory test', 5)
|
|
await db.search('memory test', 3)
|
|
await db.search('test', 5)
|
|
|
|
const stats = db.getCacheStats()
|
|
expect(stats.searchMemoryUsage).toBeGreaterThan(0)
|
|
expect(stats.search.size).toBeGreaterThan(0)
|
|
expect(stats.search.size).toBeLessThanOrEqual(stats.search.maxSize)
|
|
})
|
|
})
|
|
}) |