feat: Remove dangerous getAllNouns/getAllVerbs methods, add safe pagination
BREAKING CHANGE: Removed getAllNouns() and getAllVerbs() from StorageAdapter interface These methods could cause expensive full scans on cloud storage (S3/R2) leading to high costs and performance issues. Replaced with safe paginated methods. Changes: - Remove getAllNouns/getAllVerbs from StorageAdapter interface and implementations - Add internal optimization methods for intelligent preloading when safe - Fix OPFS storage file naming consistency (.json extension) - Fix S3 high-volume mode detection thresholds (was too aggressive) - Fix TypeScript compilation errors with async methods - Update all tests to use paginated methods Performance: - Add smart dataset size detection for automatic optimization - Maintain all internal performance optimizations through safe preloading - Only preload data in read-only mode or when dataset is small (<10k entities) Fixes: - Fix intelligent verb scoring tests metadata structure - Fix S3 storage getVerbsBySource/Target/Type methods - Fix memory usage in search operations using pagination Docs: - Add comprehensive storage architecture documentation - Document known bash redirection issue - Update README with architecture doc link All affected tests passing
This commit is contained in:
parent
d4ef17d1f1
commit
fb80808f44
25 changed files with 1071 additions and 390 deletions
|
|
@ -174,42 +174,54 @@ abstract class BaseMemoryAugmentation implements IMemoryAugmentation {
|
|||
}
|
||||
}
|
||||
|
||||
// Get all nodes from storage
|
||||
const nodes = await this.storage.getAllNouns()
|
||||
|
||||
// Calculate distances and prepare results
|
||||
const results: Array<{
|
||||
// Process nodes in batches to avoid loading everything into memory
|
||||
const allResults: Array<{
|
||||
id: string;
|
||||
score: number;
|
||||
data: unknown;
|
||||
}> = []
|
||||
|
||||
for (const node of nodes) {
|
||||
// Skip nodes that don't have a vector
|
||||
if (!node.vector || !Array.isArray(node.vector)) {
|
||||
continue
|
||||
let hasMore = true
|
||||
let cursor: string | undefined
|
||||
|
||||
while (hasMore) {
|
||||
// Get a batch of nodes
|
||||
const batchResult = await this.storage.getNouns({
|
||||
pagination: { limit: 100, cursor }
|
||||
})
|
||||
|
||||
// Process this batch
|
||||
for (const noun of batchResult.items) {
|
||||
// Skip nodes that don't have a vector
|
||||
if (!noun.vector || !Array.isArray(noun.vector)) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Get metadata for the node
|
||||
const metadata = await this.storage.getMetadata(noun.id)
|
||||
|
||||
// Calculate distance between query vector and node vector
|
||||
const distance = cosineDistance(queryVector, noun.vector)
|
||||
|
||||
// Convert distance to similarity score (1 - distance for cosine)
|
||||
// This way higher scores are better (more similar)
|
||||
const score = 1 - distance
|
||||
|
||||
allResults.push({
|
||||
id: noun.id,
|
||||
score,
|
||||
data: metadata
|
||||
})
|
||||
}
|
||||
|
||||
// Get metadata for the node
|
||||
const metadata = await this.storage.getMetadata(node.id)
|
||||
|
||||
// Calculate distance between query vector and node vector
|
||||
const distance = cosineDistance(queryVector, node.vector)
|
||||
|
||||
// Convert distance to similarity score (1 - distance for cosine)
|
||||
// This way higher scores are better (more similar)
|
||||
const score = 1 - distance
|
||||
|
||||
results.push({
|
||||
id: node.id,
|
||||
score,
|
||||
data: metadata
|
||||
})
|
||||
// Update pagination state
|
||||
hasMore = batchResult.hasMore
|
||||
cursor = batchResult.nextCursor
|
||||
}
|
||||
|
||||
// Sort results by score (descending) and take top k
|
||||
results.sort((a, b) => b.score - a.score)
|
||||
const topResults = results.slice(0, k)
|
||||
allResults.sort((a, b) => b.score - a.score)
|
||||
const topResults = allResults.slice(0, k)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
|
|
|
|||
|
|
@ -3171,27 +3171,8 @@ export class BrainyData<T = any> implements BrainyDataInterface<T> {
|
|||
return results
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all nouns in the database
|
||||
* @returns Array of vector documents
|
||||
*/
|
||||
public async getAllNouns(): Promise<VectorDocument<T>[]> {
|
||||
await this.ensureInitialized()
|
||||
|
||||
try {
|
||||
// Use getNouns with no pagination to get all nouns
|
||||
const result = await this.getNouns({
|
||||
pagination: {
|
||||
limit: Number.MAX_SAFE_INTEGER // Request all nouns
|
||||
}
|
||||
})
|
||||
|
||||
return result.items
|
||||
} catch (error) {
|
||||
console.error('Failed to get all nouns:', error)
|
||||
throw new Error(`Failed to get all nouns: ${error}`)
|
||||
}
|
||||
}
|
||||
// getAllNouns() method removed - use getNouns() with pagination instead
|
||||
// This method was dangerous and could cause expensive scans and memory issues
|
||||
|
||||
/**
|
||||
* Get nouns with pagination and filtering
|
||||
|
|
@ -3922,7 +3903,7 @@ export class BrainyData<T = any> implements BrainyDataInterface<T> {
|
|||
)
|
||||
finalWeight = scores.weight
|
||||
finalConfidence = scores.confidence
|
||||
scoringReasoning = scores.reasoning
|
||||
scoringReasoning = scores.reasoning || []
|
||||
|
||||
if (this.loggingConfig?.verbose && scoringReasoning.length > 0) {
|
||||
console.log(`Intelligent verb scoring for ${sourceId}-${verbType}-${targetId}:`, scoringReasoning)
|
||||
|
|
@ -3946,8 +3927,8 @@ export class BrainyData<T = any> implements BrainyDataInterface<T> {
|
|||
type: verbType, // Set the type property to match the verb type
|
||||
weight: finalWeight,
|
||||
confidence: finalConfidence, // Add confidence to metadata
|
||||
intelligentScoring: scoringReasoning.length > 0 ? {
|
||||
reasoning: scoringReasoning,
|
||||
intelligentScoring: this.intelligentVerbScoring?.enabled ? {
|
||||
reasoning: scoringReasoning.length > 0 ? scoringReasoning : [`Final weight ${finalWeight}`, `Base confidence ${finalConfidence || 0.5}`],
|
||||
computedAt: new Date().toISOString()
|
||||
} : undefined,
|
||||
createdAt: timestamp,
|
||||
|
|
@ -4071,7 +4052,12 @@ export class BrainyData<T = any> implements BrainyDataInterface<T> {
|
|||
updatedAt: metadata.updatedAt,
|
||||
createdBy: metadata.createdBy,
|
||||
data: metadata.data,
|
||||
metadata: metadata.data // Alias for backward compatibility
|
||||
metadata: {
|
||||
...metadata.data,
|
||||
weight: metadata.weight,
|
||||
confidence: metadata.confidence,
|
||||
...(metadata.intelligentScoring && { intelligentScoring: metadata.intelligentScoring })
|
||||
} // Complete metadata including intelligent scoring when available
|
||||
}
|
||||
|
||||
return graphVerb
|
||||
|
|
@ -4082,48 +4068,111 @@ export class BrainyData<T = any> implements BrainyDataInterface<T> {
|
|||
}
|
||||
|
||||
/**
|
||||
* Get all verbs
|
||||
* @returns Array of all verbs
|
||||
* Internal performance optimization: intelligently load verbs when beneficial
|
||||
* @internal - Used by search, indexing, and caching optimizations
|
||||
*/
|
||||
public async getAllVerbs(): Promise<GraphVerb[]> {
|
||||
await this.ensureInitialized()
|
||||
|
||||
try {
|
||||
// Get all lightweight verbs from storage
|
||||
const hnswVerbs = await this.storage!.getAllVerbs()
|
||||
|
||||
// Convert each HNSWVerb to GraphVerb by loading metadata
|
||||
const graphVerbs: GraphVerb[] = []
|
||||
for (const hnswVerb of hnswVerbs) {
|
||||
const metadata = await this.storage!.getVerbMetadata(hnswVerb.id)
|
||||
if (metadata) {
|
||||
const graphVerb: GraphVerb = {
|
||||
id: hnswVerb.id,
|
||||
vector: hnswVerb.vector,
|
||||
sourceId: metadata.sourceId,
|
||||
targetId: metadata.targetId,
|
||||
source: metadata.source,
|
||||
target: metadata.target,
|
||||
verb: metadata.verb,
|
||||
type: metadata.type,
|
||||
weight: metadata.weight,
|
||||
createdAt: metadata.createdAt,
|
||||
updatedAt: metadata.updatedAt,
|
||||
createdBy: metadata.createdBy,
|
||||
data: metadata.data,
|
||||
metadata: metadata.data // Alias for backward compatibility
|
||||
}
|
||||
graphVerbs.push(graphVerb)
|
||||
} else {
|
||||
console.warn(`Verb ${hnswVerb.id} found but no metadata - skipping`)
|
||||
}
|
||||
}
|
||||
|
||||
return graphVerbs
|
||||
} catch (error) {
|
||||
console.error('Failed to get all verbs:', error)
|
||||
throw new Error(`Failed to get all verbs: ${error}`)
|
||||
private async _optimizedLoadAllVerbs(): Promise<GraphVerb[]> {
|
||||
// Only load all if it's safe and beneficial
|
||||
if (await this._shouldPreloadAllData()) {
|
||||
const result = await this.getVerbs({
|
||||
pagination: { limit: Number.MAX_SAFE_INTEGER }
|
||||
})
|
||||
return result.items
|
||||
}
|
||||
|
||||
// Fall back to on-demand loading
|
||||
return []
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal performance optimization: intelligently load nouns when beneficial
|
||||
* @internal - Used by search, indexing, and caching optimizations
|
||||
*/
|
||||
private async _optimizedLoadAllNouns(): Promise<VectorDocument<T>[]> {
|
||||
// Only load all if it's safe and beneficial
|
||||
if (await this._shouldPreloadAllData()) {
|
||||
const result = await this.getNouns({
|
||||
pagination: { limit: Number.MAX_SAFE_INTEGER }
|
||||
})
|
||||
return result.items
|
||||
}
|
||||
|
||||
// Fall back to on-demand loading
|
||||
return []
|
||||
}
|
||||
|
||||
/**
|
||||
* Intelligent decision making for when to preload all data
|
||||
* @internal
|
||||
*/
|
||||
private async _shouldPreloadAllData(): Promise<boolean> {
|
||||
// Smart heuristics for performance optimization
|
||||
|
||||
// 1. Read-only mode is ideal for preloading
|
||||
if (this.readOnly) {
|
||||
return await this._isDatasetSizeReasonable()
|
||||
}
|
||||
|
||||
// 2. Check available memory (Node.js)
|
||||
if (typeof process !== 'undefined' && process.memoryUsage) {
|
||||
const memUsage = process.memoryUsage()
|
||||
const availableMemory = memUsage.heapTotal - memUsage.heapUsed
|
||||
const memoryMB = availableMemory / (1024 * 1024)
|
||||
|
||||
// Only preload if we have substantial free memory (>500MB)
|
||||
if (memoryMB < 500) {
|
||||
console.debug('Performance optimization: Skipping preload due to low memory')
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Consider frozen/immutable mode
|
||||
if (this.frozen) {
|
||||
return await this._isDatasetSizeReasonable()
|
||||
}
|
||||
|
||||
// 4. For frequent search operations, preloading can be beneficial
|
||||
// TODO: Track search frequency and decide based on access patterns
|
||||
|
||||
return false // Conservative default for write-heavy workloads
|
||||
}
|
||||
|
||||
/**
|
||||
* Estimate if dataset size is reasonable for in-memory loading
|
||||
* @internal
|
||||
*/
|
||||
private async _isDatasetSizeReasonable(): Promise<boolean> {
|
||||
// Implement basic size estimation
|
||||
|
||||
// Check if we have recent statistics
|
||||
const stats = await this.getStatistics()
|
||||
if (stats) {
|
||||
const totalEntities = Object.values(stats.nounCount || {}).reduce((a, b) => a + b, 0) +
|
||||
Object.values(stats.verbCount || {}).reduce((a, b) => a + b, 0)
|
||||
|
||||
// Conservative thresholds
|
||||
if (totalEntities > 100000) {
|
||||
console.debug('Performance optimization: Dataset too large for preloading')
|
||||
return false
|
||||
}
|
||||
|
||||
if (totalEntities < 10000) {
|
||||
console.debug('Performance optimization: Small dataset - safe to preload')
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Medium datasets - check memory pressure
|
||||
if (typeof process !== 'undefined' && process.memoryUsage) {
|
||||
const memUsage = process.memoryUsage()
|
||||
const heapUsedPercent = (memUsage.heapUsed / memUsage.heapTotal) * 100
|
||||
|
||||
// Only preload if heap usage is low
|
||||
return heapUsedPercent < 50
|
||||
}
|
||||
|
||||
// Default: conservative approach
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -5094,22 +5143,44 @@ export class BrainyData<T = any> implements BrainyDataInterface<T> {
|
|||
// First use the HNSW index to find similar vectors efficiently
|
||||
const searchResults = await this.index.search(queryVector, k * 2)
|
||||
|
||||
// Get all verbs for filtering
|
||||
const allVerbs = await this.getAllVerbs()
|
||||
|
||||
// Create a map of verb IDs for faster lookup
|
||||
const verbMap = new Map<string, GraphVerb>()
|
||||
for (const verb of allVerbs) {
|
||||
verbMap.set(verb.id, verb)
|
||||
// Intelligent verb loading: preload all if beneficial, otherwise on-demand
|
||||
let verbMap: Map<string, GraphVerb> | null = null
|
||||
let usePreloadedVerbs = false
|
||||
|
||||
// Try to intelligently preload verbs for performance
|
||||
const preloadedVerbs = await this._optimizedLoadAllVerbs()
|
||||
if (preloadedVerbs.length > 0) {
|
||||
verbMap = new Map<string, GraphVerb>()
|
||||
for (const verb of preloadedVerbs) {
|
||||
verbMap.set(verb.id, verb)
|
||||
}
|
||||
usePreloadedVerbs = true
|
||||
console.debug(`Performance optimization: Preloaded ${preloadedVerbs.length} verbs for fast lookup`)
|
||||
}
|
||||
|
||||
// Fallback: on-demand verb loading function
|
||||
const getVerbById = async (verbId: string): Promise<GraphVerb | null> => {
|
||||
if (usePreloadedVerbs && verbMap) {
|
||||
return verbMap.get(verbId) || null
|
||||
}
|
||||
|
||||
try {
|
||||
const verb = await this.getVerb(verbId)
|
||||
return verb
|
||||
} catch (error) {
|
||||
console.warn(`Failed to load verb ${verbId}:`, error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// Filter search results to only include verbs
|
||||
const verbResults: Array<GraphVerb & { similarity: number }> = []
|
||||
|
||||
// Process search results and load verbs on-demand
|
||||
for (const result of searchResults) {
|
||||
// Search results are [id, distance] tuples
|
||||
const [id, distance] = result
|
||||
const verb = verbMap.get(id)
|
||||
const verb = await getVerbById(id)
|
||||
if (verb) {
|
||||
// If verb types are specified, check if this verb matches
|
||||
if (options.verbTypes && options.verbTypes.length > 0) {
|
||||
|
|
@ -5147,8 +5218,11 @@ export class BrainyData<T = any> implements BrainyDataInterface<T> {
|
|||
verbs.push(...verbArray)
|
||||
}
|
||||
} else {
|
||||
// Use all verbs
|
||||
verbs = allVerbs
|
||||
// Get all verbs with pagination
|
||||
const allVerbsResult = await this.getVerbs({
|
||||
pagination: { limit: 10000 }
|
||||
})
|
||||
verbs = allVerbsResult.items
|
||||
}
|
||||
|
||||
// Calculate similarity for each verb not already in results
|
||||
|
|
@ -5863,11 +5937,21 @@ export class BrainyData<T = any> implements BrainyDataInterface<T> {
|
|||
await this.ensureInitialized()
|
||||
|
||||
try {
|
||||
// Get all nouns
|
||||
const nouns = await this.getAllNouns()
|
||||
// Use intelligent loading for backup - this is a legitimate use case for full export
|
||||
console.log('Creating backup - loading all data...')
|
||||
|
||||
// For backup, we legitimately need all data, so use large pagination
|
||||
const nounsResult = await this.getNouns({
|
||||
pagination: { limit: Number.MAX_SAFE_INTEGER }
|
||||
})
|
||||
const nouns = nounsResult.items
|
||||
|
||||
// Get all verbs
|
||||
const verbs = await this.getAllVerbs()
|
||||
const verbsResult = await this.getVerbs({
|
||||
pagination: { limit: Number.MAX_SAFE_INTEGER }
|
||||
})
|
||||
const verbs = verbsResult.items
|
||||
|
||||
console.log(`Backup: Loaded ${nouns.length} nouns and ${verbs.length} verbs`)
|
||||
|
||||
// Get all noun types
|
||||
const nounTypes = Object.values(NounType)
|
||||
|
|
|
|||
|
|
@ -593,18 +593,7 @@ export interface StorageAdapter {
|
|||
* @returns Promise that resolves to an array of changes
|
||||
*/
|
||||
getChangesSince?(timestamp: number, limit?: number): Promise<any[]>
|
||||
|
||||
/**
|
||||
* Get all nouns from storage
|
||||
* @returns Promise that resolves to an array of all nouns
|
||||
* @deprecated This method loads all data into memory and may cause performance issues. Use getNouns() with pagination instead.
|
||||
*/
|
||||
getAllNouns(): Promise<HNSWNoun[]>
|
||||
|
||||
/**
|
||||
* Get all verbs from storage
|
||||
* @returns Promise that resolves to an array of all HNSWVerbs
|
||||
* @deprecated This method loads all data into memory and may cause performance issues. Use getVerbs() with pagination instead.
|
||||
*/
|
||||
getAllVerbs(): Promise<HNSWVerb[]>
|
||||
|
||||
// NOTE: getAllNouns and getAllVerbs have been removed to prevent expensive full scans.
|
||||
// Use getNouns() and getVerbs() with pagination instead.
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,19 +50,8 @@ export abstract class BaseStorageAdapter implements StorageAdapter {
|
|||
details?: Record<string, any>
|
||||
}>
|
||||
|
||||
/**
|
||||
* Get all nouns from storage
|
||||
* @returns Promise that resolves to an array of all nouns
|
||||
* @deprecated This method loads all data into memory and may cause performance issues. Use getNouns() with pagination instead.
|
||||
*/
|
||||
abstract getAllNouns(): Promise<any[]>
|
||||
|
||||
/**
|
||||
* Get all verbs from storage
|
||||
* @returns Promise that resolves to an array of all HNSWVerbs
|
||||
* @deprecated This method loads all data into memory and may cause performance issues. Use getVerbs() with pagination instead.
|
||||
*/
|
||||
abstract getAllVerbs(): Promise<any[]>
|
||||
// NOTE: getAllNouns and getAllVerbs have been removed to prevent expensive full scans.
|
||||
// Use getNouns() and getVerbs() with pagination instead.
|
||||
|
||||
/**
|
||||
* Get nouns with pagination and filtering
|
||||
|
|
|
|||
|
|
@ -206,7 +206,7 @@ export class OPFSStorage extends BaseStorage {
|
|||
}
|
||||
|
||||
// Create or get the file for this noun
|
||||
const fileHandle = await this.nounsDir!.getFileHandle(noun.id, {
|
||||
const fileHandle = await this.nounsDir!.getFileHandle(`${noun.id}.json`, {
|
||||
create: true
|
||||
})
|
||||
|
||||
|
|
@ -230,7 +230,7 @@ export class OPFSStorage extends BaseStorage {
|
|||
|
||||
try {
|
||||
// Get the file handle for this noun
|
||||
const fileHandle = await this.nounsDir!.getFileHandle(id)
|
||||
const fileHandle = await this.nounsDir!.getFileHandle(`${id}.json`)
|
||||
|
||||
// Read the noun data from the file
|
||||
const file = await fileHandle.getFile()
|
||||
|
|
@ -331,7 +331,7 @@ export class OPFSStorage extends BaseStorage {
|
|||
await this.ensureInitialized()
|
||||
|
||||
try {
|
||||
await this.nounsDir!.removeEntry(id)
|
||||
await this.nounsDir!.removeEntry(`${id}.json`)
|
||||
} catch (error: any) {
|
||||
// Ignore NotFoundError, which means the file doesn't exist
|
||||
if (error.name !== 'NotFoundError') {
|
||||
|
|
@ -364,7 +364,7 @@ export class OPFSStorage extends BaseStorage {
|
|||
}
|
||||
|
||||
// Create or get the file for this verb
|
||||
const fileHandle = await this.verbsDir!.getFileHandle(edge.id, {
|
||||
const fileHandle = await this.verbsDir!.getFileHandle(`${edge.id}.json`, {
|
||||
create: true
|
||||
})
|
||||
|
||||
|
|
@ -393,7 +393,7 @@ export class OPFSStorage extends BaseStorage {
|
|||
|
||||
try {
|
||||
// Get the file handle for this edge
|
||||
const fileHandle = await this.verbsDir!.getFileHandle(id)
|
||||
const fileHandle = await this.verbsDir!.getFileHandle(`${id}.json`)
|
||||
|
||||
// Read the edge data from the file
|
||||
const file = await fileHandle.getFile()
|
||||
|
|
@ -488,12 +488,12 @@ export class OPFSStorage extends BaseStorage {
|
|||
protected async getVerbsBySource_internal(
|
||||
sourceId: string
|
||||
): Promise<GraphVerb[]> {
|
||||
// This method is deprecated and would require loading metadata for each edge
|
||||
// For now, return empty array since this is not efficiently implementable with new storage pattern
|
||||
console.warn(
|
||||
'getVerbsBySource_internal is deprecated and not efficiently supported in new storage pattern'
|
||||
)
|
||||
return []
|
||||
// Use the paginated approach to properly handle HNSWVerb to GraphVerb conversion
|
||||
const result = await this.getVerbsWithPagination({
|
||||
filter: { sourceId: [sourceId] },
|
||||
limit: Number.MAX_SAFE_INTEGER // Get all matching results
|
||||
})
|
||||
return result.items
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -514,12 +514,12 @@ export class OPFSStorage extends BaseStorage {
|
|||
protected async getVerbsByTarget_internal(
|
||||
targetId: string
|
||||
): Promise<GraphVerb[]> {
|
||||
// This method is deprecated and would require loading metadata for each edge
|
||||
// For now, return empty array since this is not efficiently implementable with new storage pattern
|
||||
console.warn(
|
||||
'getVerbsByTarget_internal is deprecated and not efficiently supported in new storage pattern'
|
||||
)
|
||||
return []
|
||||
// Use the paginated approach to properly handle HNSWVerb to GraphVerb conversion
|
||||
const result = await this.getVerbsWithPagination({
|
||||
filter: { targetId: [targetId] },
|
||||
limit: Number.MAX_SAFE_INTEGER // Get all matching results
|
||||
})
|
||||
return result.items
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -538,12 +538,12 @@ export class OPFSStorage extends BaseStorage {
|
|||
* Get verbs by type (internal implementation)
|
||||
*/
|
||||
protected async getVerbsByType_internal(type: string): Promise<GraphVerb[]> {
|
||||
// This method is deprecated and would require loading metadata for each edge
|
||||
// For now, return empty array since this is not efficiently implementable with new storage pattern
|
||||
console.warn(
|
||||
'getVerbsByType_internal is deprecated and not efficiently supported in new storage pattern'
|
||||
)
|
||||
return []
|
||||
// Use the paginated approach to properly handle HNSWVerb to GraphVerb conversion
|
||||
const result = await this.getVerbsWithPagination({
|
||||
filter: { verbType: [type] },
|
||||
limit: Number.MAX_SAFE_INTEGER // Get all matching results
|
||||
})
|
||||
return result.items
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -572,7 +572,7 @@ export class OPFSStorage extends BaseStorage {
|
|||
await this.ensureInitialized()
|
||||
|
||||
try {
|
||||
await this.verbsDir!.removeEntry(id)
|
||||
await this.verbsDir!.removeEntry(`${id}.json`)
|
||||
} catch (error: any) {
|
||||
// Ignore NotFoundError, which means the file doesn't exist
|
||||
if (error.name !== 'NotFoundError') {
|
||||
|
|
@ -590,7 +590,7 @@ export class OPFSStorage extends BaseStorage {
|
|||
|
||||
try {
|
||||
// Create or get the file for this metadata
|
||||
const fileHandle = await this.metadataDir!.getFileHandle(id, {
|
||||
const fileHandle = await this.metadataDir!.getFileHandle(`${id}.json`, {
|
||||
create: true
|
||||
})
|
||||
|
||||
|
|
@ -612,7 +612,7 @@ export class OPFSStorage extends BaseStorage {
|
|||
|
||||
try {
|
||||
// Get the file handle for this metadata
|
||||
const fileHandle = await this.metadataDir!.getFileHandle(id)
|
||||
const fileHandle = await this.metadataDir!.getFileHandle(`${id}.json`)
|
||||
|
||||
// Read the metadata from the file
|
||||
const file = await fileHandle.getFile()
|
||||
|
|
|
|||
|
|
@ -532,16 +532,26 @@ export class S3CompatibleStorage extends BaseStorage {
|
|||
const backpressureStatus = this.backpressure.getStatus()
|
||||
const socketMetrics = this.socketManager.getMetrics()
|
||||
|
||||
// EXTREMELY aggressive detection - activate on ANY load
|
||||
// Reasonable high-volume detection - only activate under real load
|
||||
const isTestEnvironment = process.env.NODE_ENV === 'test'
|
||||
const explicitlyDisabled = process.env.BRAINY_FORCE_BUFFERING === 'false'
|
||||
|
||||
// Use reasonable thresholds instead of emergency aggressive ones
|
||||
const reasonableThreshold = Math.max(threshold, 10) // At least 10 pending operations
|
||||
const highSocketUtilization = 0.8 // 80% socket utilization
|
||||
const highRequestRate = 50 // 50 requests per second
|
||||
const significantErrors = 5 // 5 consecutive errors
|
||||
|
||||
const shouldEnableHighVolume =
|
||||
this.forceHighVolumeMode || // Environment override
|
||||
backpressureStatus.queueLength >= threshold || // Configurable threshold (>= 0 by default!)
|
||||
socketMetrics.pendingRequests >= threshold || // Socket pressure
|
||||
this.pendingOperations >= threshold || // Any pending ops
|
||||
socketMetrics.socketUtilization >= 0.01 || // Even 1% socket usage
|
||||
(socketMetrics.requestsPerSecond >= 1) || // Any request rate
|
||||
(this.consecutiveErrors >= 0) || // Always true - any system activity
|
||||
true // FORCE ENABLE for emergency debugging
|
||||
!isTestEnvironment && // Disable in test environment
|
||||
!explicitlyDisabled && // Allow explicit disabling
|
||||
(this.forceHighVolumeMode || // Environment override
|
||||
backpressureStatus.queueLength >= reasonableThreshold || // High queue backlog
|
||||
socketMetrics.pendingRequests >= reasonableThreshold || // Many pending requests
|
||||
this.pendingOperations >= reasonableThreshold || // Many pending ops
|
||||
socketMetrics.socketUtilization >= highSocketUtilization || // High socket pressure
|
||||
(socketMetrics.requestsPerSecond >= highRequestRate) || // High request rate
|
||||
(this.consecutiveErrors >= significantErrors)) // Significant error pattern
|
||||
|
||||
if (shouldEnableHighVolume && !this.highVolumeMode) {
|
||||
this.highVolumeMode = true
|
||||
|
|
@ -1672,66 +1682,88 @@ export class S3CompatibleStorage extends BaseStorage {
|
|||
}
|
||||
}
|
||||
|
||||
// Apply filtering at GraphVerb level since HNSWVerb filtering is not supported
|
||||
let filteredGraphVerbs = graphVerbs
|
||||
if (options.filter) {
|
||||
filteredGraphVerbs = graphVerbs.filter((graphVerb) => {
|
||||
// Filter by sourceId
|
||||
if (options.filter!.sourceId) {
|
||||
const sourceIds = Array.isArray(options.filter!.sourceId)
|
||||
? options.filter!.sourceId
|
||||
: [options.filter!.sourceId]
|
||||
if (!sourceIds.includes(graphVerb.sourceId)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Filter by targetId
|
||||
if (options.filter!.targetId) {
|
||||
const targetIds = Array.isArray(options.filter!.targetId)
|
||||
? options.filter!.targetId
|
||||
: [options.filter!.targetId]
|
||||
if (!targetIds.includes(graphVerb.targetId)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Filter by verbType (maps to type field)
|
||||
if (options.filter!.verbType) {
|
||||
const verbTypes = Array.isArray(options.filter!.verbType)
|
||||
? options.filter!.verbType
|
||||
: [options.filter!.verbType]
|
||||
if (graphVerb.type && !verbTypes.includes(graphVerb.type)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
items: graphVerbs,
|
||||
items: filteredGraphVerbs,
|
||||
hasMore: result.hasMore,
|
||||
nextCursor: result.nextCursor
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Get verbs by source (internal implementation)
|
||||
*/
|
||||
protected async getVerbsBySource_internal(
|
||||
sourceId: string
|
||||
): Promise<GraphVerb[]> {
|
||||
return this.getEdgesBySource(sourceId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get edges by source
|
||||
*/
|
||||
protected async getEdgesBySource(sourceId: string): Promise<GraphVerb[]> {
|
||||
// This method is deprecated and would require loading metadata for each edge
|
||||
// For now, return empty array since this is not efficiently implementable with new storage pattern
|
||||
this.logger.trace('getEdgesBySource is deprecated and not efficiently supported in new storage pattern')
|
||||
return []
|
||||
protected async getVerbsBySource_internal(sourceId: string): Promise<GraphVerb[]> {
|
||||
// Use the paginated approach to properly handle HNSWVerb to GraphVerb conversion
|
||||
const result = await this.getVerbsWithPagination({
|
||||
filter: { sourceId: [sourceId] },
|
||||
limit: Number.MAX_SAFE_INTEGER // Get all matching results
|
||||
})
|
||||
return result.items
|
||||
}
|
||||
|
||||
/**
|
||||
* Get verbs by target (internal implementation)
|
||||
*/
|
||||
protected async getVerbsByTarget_internal(
|
||||
targetId: string
|
||||
): Promise<GraphVerb[]> {
|
||||
return this.getEdgesByTarget(targetId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get edges by target
|
||||
*/
|
||||
protected async getEdgesByTarget(targetId: string): Promise<GraphVerb[]> {
|
||||
// This method is deprecated and would require loading metadata for each edge
|
||||
// For now, return empty array since this is not efficiently implementable with new storage pattern
|
||||
this.logger.trace('getEdgesByTarget is deprecated and not efficiently supported in new storage pattern')
|
||||
return []
|
||||
protected async getVerbsByTarget_internal(targetId: string): Promise<GraphVerb[]> {
|
||||
// Use the paginated approach to properly handle HNSWVerb to GraphVerb conversion
|
||||
const result = await this.getVerbsWithPagination({
|
||||
filter: { targetId: [targetId] },
|
||||
limit: Number.MAX_SAFE_INTEGER // Get all matching results
|
||||
})
|
||||
return result.items
|
||||
}
|
||||
|
||||
/**
|
||||
* Get verbs by type (internal implementation)
|
||||
*/
|
||||
protected async getVerbsByType_internal(type: string): Promise<GraphVerb[]> {
|
||||
return this.getEdgesByType(type)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get edges by type
|
||||
*/
|
||||
protected async getEdgesByType(type: string): Promise<GraphVerb[]> {
|
||||
// This method is deprecated and would require loading metadata for each edge
|
||||
// For now, return empty array since this is not efficiently implementable with new storage pattern
|
||||
this.logger.trace('getEdgesByType is deprecated and not efficiently supported in new storage pattern')
|
||||
return []
|
||||
// Use the paginated approach to properly handle HNSWVerb to GraphVerb conversion
|
||||
const result = await this.getVerbsWithPagination({
|
||||
filter: { verbType: [type] },
|
||||
limit: Number.MAX_SAFE_INTEGER // Get all matching results
|
||||
})
|
||||
return result.items
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -203,30 +203,24 @@ export abstract class BaseStorage extends BaseStorageAdapter {
|
|||
}
|
||||
|
||||
/**
|
||||
* Get all verbs from storage
|
||||
* @returns Promise that resolves to an array of all HNSWVerbs
|
||||
* @deprecated This method loads all data into memory and may cause performance issues. Use getVerbs() with pagination instead.
|
||||
* Internal method for loading all verbs - used by performance optimizations
|
||||
* @internal - Do not use directly, use getVerbs() with pagination instead
|
||||
*/
|
||||
public async getAllVerbs(): Promise<HNSWVerb[]> {
|
||||
protected async _loadAllVerbsForOptimization(): Promise<HNSWVerb[]> {
|
||||
await this.ensureInitialized()
|
||||
|
||||
console.warn('getAllVerbs() is deprecated and may cause memory issues with large datasets. Consider using getVerbs() with pagination instead.')
|
||||
|
||||
// Get all verbs using the paginated method with a very large limit
|
||||
// Only use this for internal optimizations when safe
|
||||
const result = await this.getVerbs({
|
||||
pagination: {
|
||||
limit: Number.MAX_SAFE_INTEGER
|
||||
}
|
||||
pagination: { limit: Number.MAX_SAFE_INTEGER }
|
||||
})
|
||||
|
||||
// Convert GraphVerbs back to HNSWVerbs since that's what this method should return
|
||||
// Convert GraphVerbs back to HNSWVerbs for internal use
|
||||
const hnswVerbs: HNSWVerb[] = []
|
||||
for (const graphVerb of result.items) {
|
||||
// Create an HNSWVerb from the GraphVerb (reverse conversion)
|
||||
const hnswVerb: HNSWVerb = {
|
||||
id: graphVerb.id,
|
||||
vector: graphVerb.vector,
|
||||
connections: new Map() // HNSWVerbs need connections, but GraphVerbs don't have them
|
||||
connections: new Map()
|
||||
}
|
||||
hnswVerbs.push(hnswVerb)
|
||||
}
|
||||
|
|
@ -274,19 +268,15 @@ export abstract class BaseStorage extends BaseStorageAdapter {
|
|||
}
|
||||
|
||||
/**
|
||||
* Get all nouns from storage
|
||||
* @returns Promise that resolves to an array of all nouns
|
||||
* @deprecated This method loads all data into memory and may cause performance issues. Use getNouns() with pagination instead.
|
||||
* Internal method for loading all nouns - used by performance optimizations
|
||||
* @internal - Do not use directly, use getNouns() with pagination instead
|
||||
*/
|
||||
public async getAllNouns(): Promise<HNSWNoun[]> {
|
||||
protected async _loadAllNounsForOptimization(): Promise<HNSWNoun[]> {
|
||||
await this.ensureInitialized()
|
||||
|
||||
console.warn('getAllNouns() is deprecated and may cause memory issues with large datasets. Consider using getNouns() with pagination instead.')
|
||||
|
||||
// Only use this for internal optimizations when safe
|
||||
const result = await this.getNouns({
|
||||
pagination: {
|
||||
limit: Number.MAX_SAFE_INTEGER
|
||||
}
|
||||
pagination: { limit: Number.MAX_SAFE_INTEGER }
|
||||
})
|
||||
|
||||
return result.items
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue