feat: add verb and noun metadata handling in storage adapters

- Implement saveVerbMetadata and getVerbMetadata methods for managing verb metadata.
- Implement saveNounMetadata and getNounMetadata methods for managing noun metadata.
- Update storage adapters to use HNSWVerb instead of GraphVerb for improved performance.
- Deprecate methods that require loading metadata for edges, returning empty arrays instead.
This commit is contained in:
David Snelling 2025-08-03 10:47:55 -07:00
parent 9905a5dc35
commit 1040f1ce34
4 changed files with 386 additions and 137 deletions

View file

@ -4,7 +4,7 @@
* including Amazon S3, Cloudflare R2, and Google Cloud Storage
*/
import { GraphVerb, HNSWNoun, StatisticsData } from '../../coreTypes.js'
import { GraphVerb, HNSWNoun, HNSWVerb, StatisticsData } from '../../coreTypes.js'
import {
BaseStorage,
NOUNS_DIR,
@ -22,7 +22,7 @@ import { CacheManager } from '../cacheManager.js'
// Type aliases for better readability
type HNSWNode = HNSWNoun
type Edge = GraphVerb
type Edge = HNSWVerb
// Change log entry interface for tracking data modifications
interface ChangeLogEntry {
@ -712,7 +712,7 @@ export class S3CompatibleStorage extends BaseStorage {
/**
* Save a verb to storage (internal implementation)
*/
protected async saveVerb_internal(verb: GraphVerb): Promise<void> {
protected async saveVerb_internal(verb: HNSWVerb): Promise<void> {
return this.saveEdge(verb)
}
@ -751,11 +751,7 @@ export class S3CompatibleStorage extends BaseStorage {
entityType: 'verb',
entityId: edge.id,
data: {
sourceId: edge.sourceId || edge.source,
targetId: edge.targetId || edge.target,
type: edge.type || edge.verb,
vector: edge.vector,
metadata: edge.metadata
vector: edge.vector
}
})
} catch (error) {
@ -767,7 +763,7 @@ export class S3CompatibleStorage extends BaseStorage {
/**
* Get a verb from storage (internal implementation)
*/
protected async getVerb_internal(id: string): Promise<GraphVerb | null> {
protected async getVerb_internal(id: string): Promise<HNSWVerb | null> {
return this.getEdge(id)
}
@ -815,10 +811,7 @@ export class S3CompatibleStorage extends BaseStorage {
!parsedEdge ||
!parsedEdge.id ||
!parsedEdge.vector ||
!parsedEdge.connections ||
!(parsedEdge.sourceId || parsedEdge.source) ||
!(parsedEdge.targetId || parsedEdge.target) ||
!(parsedEdge.type || parsedEdge.verb)
!parsedEdge.connections
) {
console.error(`Invalid edge data for ${id}:`, parsedEdge)
return null
@ -830,33 +823,10 @@ export class S3CompatibleStorage extends BaseStorage {
connections.set(Number(level), new Set(nodeIds as string[]))
}
// Create default timestamp if not present
const defaultTimestamp = {
seconds: Math.floor(Date.now() / 1000),
nanoseconds: (Date.now() % 1000) * 1000000
}
// Create default createdBy if not present
const defaultCreatedBy = {
augmentation: 'unknown',
version: '1.0'
}
const edge = {
id: parsedEdge.id,
vector: parsedEdge.vector,
connections,
sourceId: parsedEdge.sourceId || parsedEdge.source,
targetId: parsedEdge.targetId || parsedEdge.target,
source: parsedEdge.sourceId || parsedEdge.source,
target: parsedEdge.targetId || parsedEdge.target,
verb: parsedEdge.type || parsedEdge.verb,
type: parsedEdge.type || parsedEdge.verb,
weight: parsedEdge.weight || 1.0, // Default weight if not provided
metadata: parsedEdge.metadata || {},
createdAt: parsedEdge.createdAt || defaultTimestamp,
updatedAt: parsedEdge.updatedAt || defaultTimestamp,
createdBy: parsedEdge.createdBy || defaultCreatedBy
connections
}
console.log(`Successfully retrieved edge ${id}:`, edge)
@ -877,7 +847,7 @@ export class S3CompatibleStorage extends BaseStorage {
* @deprecated This method is deprecated and will be removed in a future version.
* It can cause memory issues with large datasets. Use getVerbsWithPagination() instead.
*/
protected async getAllVerbs_internal(): Promise<GraphVerb[]> {
protected async getAllVerbs_internal(): Promise<HNSWVerb[]> {
console.warn('WARNING: getAllVerbs_internal() is deprecated and will be removed in a future version. Use getVerbsWithPagination() instead.')
return this.getAllEdges()
}
@ -1053,27 +1023,10 @@ export class S3CompatibleStorage extends BaseStorage {
targetId?: string
type?: string
}): boolean {
// If no filter, include all edges
if (!filter.sourceId && !filter.targetId && !filter.type) {
return true
}
// Filter by source ID
if (filter.sourceId && edge.sourceId !== filter.sourceId) {
return false
}
// Filter by target ID
if (filter.targetId && edge.targetId !== filter.targetId) {
return false
}
// Filter by type
if (filter.type && edge.type !== filter.type) {
return false
}
return true
// HNSWVerb filtering is not supported since metadata is stored separately
// This method is deprecated and should not be used with the new storage pattern
console.warn('Edge filtering is deprecated and not supported with the new storage pattern')
return true // Return all edges since filtering requires metadata
}
/**
@ -1137,9 +1090,17 @@ export class S3CompatibleStorage extends BaseStorage {
filter: edgeFilter
})
// Convert edges to verbs (they're the same in this implementation)
// Convert HNSWVerbs to GraphVerbs by combining with metadata
const graphVerbs: GraphVerb[] = []
for (const hnswVerb of result.edges) {
const graphVerb = await this.convertHNSWVerbToGraphVerb(hnswVerb)
if (graphVerb) {
graphVerbs.push(graphVerb)
}
}
return {
items: result.edges,
items: graphVerbs,
hasMore: result.hasMore,
nextCursor: result.nextCursor
}
@ -1157,9 +1118,11 @@ export class S3CompatibleStorage extends BaseStorage {
/**
* Get edges by source
*/
protected async getEdgesBySource(sourceId: string): Promise<Edge[]> {
const edges = await this.getAllEdges()
return edges.filter((edge) => (edge.sourceId || edge.source) === sourceId)
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
console.warn('getEdgesBySource is deprecated and not efficiently supported in new storage pattern')
return []
}
/**
@ -1174,9 +1137,11 @@ export class S3CompatibleStorage extends BaseStorage {
/**
* Get edges by target
*/
protected async getEdgesByTarget(targetId: string): Promise<Edge[]> {
const edges = await this.getAllEdges()
return edges.filter((edge) => (edge.targetId || edge.target) === targetId)
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
console.warn('getEdgesByTarget is deprecated and not efficiently supported in new storage pattern')
return []
}
/**
@ -1189,9 +1154,11 @@ export class S3CompatibleStorage extends BaseStorage {
/**
* Get edges by type
*/
protected async getEdgesByType(type: string): Promise<Edge[]> {
const edges = await this.getAllEdges()
return edges.filter((edge) => (edge.type || edge.verb) === 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
console.warn('getEdgesByType is deprecated and not efficiently supported in new storage pattern')
return []
}
/**
@ -1303,6 +1270,200 @@ export class S3CompatibleStorage extends BaseStorage {
}
}
/**
* Save verb metadata to storage
*/
public async saveVerbMetadata(id: string, metadata: any): Promise<void> {
await this.ensureInitialized()
try {
console.log(`Saving verb metadata for ${id} to bucket ${this.bucketName}`)
// Import the PutObjectCommand only when needed
const { PutObjectCommand } = await import('@aws-sdk/client-s3')
const key = `verb-metadata/${id}.json`
const body = JSON.stringify(metadata, null, 2)
console.log(`Saving verb metadata to key: ${key}`)
console.log(`Verb Metadata: ${body}`)
// Save the verb metadata to S3-compatible storage
const result = await this.s3Client!.send(
new PutObjectCommand({
Bucket: this.bucketName,
Key: key,
Body: body,
ContentType: 'application/json'
})
)
console.log(`Verb metadata for ${id} saved successfully:`, result)
} catch (error) {
console.error(`Failed to save verb metadata for ${id}:`, error)
throw new Error(`Failed to save verb metadata for ${id}: ${error}`)
}
}
/**
* Get verb metadata from storage
*/
public async getVerbMetadata(id: string): Promise<any | null> {
await this.ensureInitialized()
try {
// Import the GetObjectCommand only when needed
const { GetObjectCommand } = await import('@aws-sdk/client-s3')
console.log(`Getting verb metadata for ${id} from bucket ${this.bucketName}`)
const key = `verb-metadata/${id}.json`
console.log(`Looking for verb metadata at key: ${key}`)
// Try to get the verb metadata
const response = await this.s3Client!.send(
new GetObjectCommand({
Bucket: this.bucketName,
Key: key
})
)
// Check if response is null or undefined
if (!response || !response.Body) {
console.log(`No verb metadata found for ${id}`)
return null
}
// Convert the response body to a string
const bodyContents = await response.Body.transformToString()
console.log(`Retrieved verb metadata body: ${bodyContents}`)
// Parse the JSON string
try {
const parsedMetadata = JSON.parse(bodyContents)
console.log(
`Successfully retrieved verb metadata for ${id}:`,
parsedMetadata
)
return parsedMetadata
} catch (parseError) {
console.error(`Failed to parse verb metadata for ${id}:`, parseError)
return null
}
} catch (error: any) {
// Check if this is a "NoSuchKey" error (object doesn't exist)
if (
error.name === 'NoSuchKey' ||
(error.message &&
(error.message.includes('NoSuchKey') ||
error.message.includes('not found') ||
error.message.includes('does not exist')))
) {
console.log(`Verb metadata not found for ${id}`)
return null
}
// For other types of errors, convert to BrainyError for better classification
throw BrainyError.fromError(error, `getVerbMetadata(${id})`)
}
}
/**
* Save noun metadata to storage
*/
public async saveNounMetadata(id: string, metadata: any): Promise<void> {
await this.ensureInitialized()
try {
console.log(`Saving noun metadata for ${id} to bucket ${this.bucketName}`)
// Import the PutObjectCommand only when needed
const { PutObjectCommand } = await import('@aws-sdk/client-s3')
const key = `noun-metadata/${id}.json`
const body = JSON.stringify(metadata, null, 2)
console.log(`Saving noun metadata to key: ${key}`)
console.log(`Noun Metadata: ${body}`)
// Save the noun metadata to S3-compatible storage
const result = await this.s3Client!.send(
new PutObjectCommand({
Bucket: this.bucketName,
Key: key,
Body: body,
ContentType: 'application/json'
})
)
console.log(`Noun metadata for ${id} saved successfully:`, result)
} catch (error) {
console.error(`Failed to save noun metadata for ${id}:`, error)
throw new Error(`Failed to save noun metadata for ${id}: ${error}`)
}
}
/**
* Get noun metadata from storage
*/
public async getNounMetadata(id: string): Promise<any | null> {
await this.ensureInitialized()
try {
// Import the GetObjectCommand only when needed
const { GetObjectCommand } = await import('@aws-sdk/client-s3')
console.log(`Getting noun metadata for ${id} from bucket ${this.bucketName}`)
const key = `noun-metadata/${id}.json`
console.log(`Looking for noun metadata at key: ${key}`)
// Try to get the noun metadata
const response = await this.s3Client!.send(
new GetObjectCommand({
Bucket: this.bucketName,
Key: key
})
)
// Check if response is null or undefined
if (!response || !response.Body) {
console.log(`No noun metadata found for ${id}`)
return null
}
// Convert the response body to a string
const bodyContents = await response.Body.transformToString()
console.log(`Retrieved noun metadata body: ${bodyContents}`)
// Parse the JSON string
try {
const parsedMetadata = JSON.parse(bodyContents)
console.log(
`Successfully retrieved noun metadata for ${id}:`,
parsedMetadata
)
return parsedMetadata
} catch (parseError) {
console.error(`Failed to parse noun metadata for ${id}:`, parseError)
return null
}
} catch (error: any) {
// Check if this is a "NoSuchKey" error (object doesn't exist)
if (
error.name === 'NoSuchKey' ||
(error.message &&
(error.message.includes('NoSuchKey') ||
error.message.includes('not found') ||
error.message.includes('does not exist')))
) {
console.log(`Noun metadata not found for ${id}`)
return null
}
// For other types of errors, convert to BrainyError for better classification
throw BrainyError.fromError(error, `getNounMetadata(${id})`)
}
}
/**
* Get metadata from storage
*/