**feat(core): enhance vector handling, model loading, and compatibility**

- **Vector Handling Updates**:
  - Added a `dimensions` property to `BrainyDataConfig` for specifying vector dimensions.
  - Introduced validation for vector dimensions during database creation and insertion to ensure consistency.
  - Enhanced error handling and logging for dimension mismatches.

- **Model Loading Improvements**:
  - Implemented retry logic for Universal Sentence Encoder model loading to handle network instability and JSON parsing errors gracefully.
  - Improved logging and debugging support for failures during model initialization and embedding operations.

- **Compatibility Enhancements**:
  - Updated polyfills to support TensorFlow.js compatibility across diverse server environments (Node.js, serverless, etc.).
  - Introduced and refactored global `TextEncoder`/`TextDecoder` definitions for seamless operation in non-browser environments.
  - Simplified TensorFlow.js backend setup with streamlined imports and logging for GPU/WebGL fallback.

- **Purpose**:
  - These updates improve BrainyData's robustness, enforce correct vector usage, and extend compatibility with varied runtime environments. The changes enhance the usability, reliability, and cross-platform readiness of core functionalities.
This commit is contained in:
David Snelling 2025-07-16 13:51:00 -07:00
parent ad4af27385
commit 3ec183dab6
9 changed files with 828 additions and 365 deletions

View file

@ -37,6 +37,11 @@ import { WebSocketConnection } from './types/augmentations.js'
import { BrainyDataInterface } from './types/brainyDataInterface.js'
export interface BrainyDataConfig {
/**
* Vector dimensions (required if not using an embedding function that auto-detects dimensions)
*/
dimensions?: number
/**
* HNSW index configuration
*/
@ -136,16 +141,48 @@ export class BrainyData<T = any> implements BrainyDataInterface<T> {
private readOnly: boolean
private storageConfig: BrainyDataConfig['storage'] = {}
private useOptimizedIndex: boolean = false
private _dimensions: number
// Remote server properties
private remoteServerConfig: BrainyDataConfig['remoteServer'] | null = null
private serverSearchConduit: ServerSearchConduitAugmentation | null = null
private serverConnection: WebSocketConnection | null = null
/**
* Get the vector dimensions
*/
public get dimensions(): number {
return this._dimensions
}
/**
* Get the maximum connections parameter from HNSW configuration
*/
public get maxConnections(): number {
const config = this.index.getConfig()
return config.M || 16
}
/**
* Get the efConstruction parameter from HNSW configuration
*/
public get efConstruction(): number {
const config = this.index.getConfig()
return config.efConstruction || 200
}
/**
* Create a new vector database
*/
constructor(config: BrainyDataConfig = {}) {
// Validate dimensions
if (config.dimensions !== undefined && config.dimensions <= 0) {
throw new Error('Dimensions must be a positive number')
}
// Set dimensions (default to 512 for embedding functions, or require explicit config)
this._dimensions = config.dimensions || 512
// Set distance function
this.distanceFunction = config.distanceFunction || cosineDistance
@ -284,6 +321,16 @@ export class BrainyData<T = any> implements BrainyDataInterface<T> {
// Clear the index and add all nouns
this.index.clear()
for (const noun of nouns) {
// Check if the vector dimensions match the expected dimensions
if (noun.vector.length !== this._dimensions) {
console.warn(
`Skipping noun ${noun.id} due to dimension mismatch: expected ${this._dimensions}, got ${noun.vector.length}`
)
// Optionally, you could delete the mismatched noun from storage
// await this.storage!.deleteNoun(noun.id)
continue
}
// Add to index
await this.index.addItem({
id: noun.id,
@ -393,6 +440,11 @@ export class BrainyData<T = any> implements BrainyDataInterface<T> {
throw new Error('Vector is undefined or null')
}
// Validate vector dimensions
if (vector.length !== this._dimensions) {
throw new Error(`Vector dimension mismatch: expected ${this._dimensions}, got ${vector.length}`)
}
// Use ID from options if it exists, otherwise from metadata, otherwise generate a new UUID
const id =
options.id ||
@ -431,7 +483,13 @@ export class BrainyData<T = any> implements BrainyDataInterface<T> {
}
}
await this.storage!.saveMetadata(id, metadata)
// Ensure metadata has the correct id field
let metadataToSave = metadata
if (metadata && typeof metadata === 'object') {
metadataToSave = { ...metadata, id }
}
await this.storage!.saveMetadata(id, metadataToSave)
}
// If addToRemote is true and we're connected to a remote server, add to remote as well
@ -452,6 +510,26 @@ export class BrainyData<T = any> implements BrainyDataInterface<T> {
}
}
/**
* Add a text item to the database with automatic embedding
* This is a convenience method for adding text data with metadata
* @param text Text data to add
* @param metadata Metadata to associate with the text
* @param options Additional options
* @returns The ID of the added item
*/
public async addItem(
text: string,
metadata?: T,
options: {
addToRemote?: boolean // Whether to also add to the remote server if connected
id?: string // Optional ID to use instead of generating a new one
} = {}
): Promise<string> {
// Use the existing add method with forceEmbed to ensure text is embedded
return this.add(text, metadata, { ...options, forceEmbed: true })
}
/**
* Add data to both local and remote Brainy instances
* @param vectorOrData Vector or data to add
@ -685,7 +763,9 @@ export class BrainyData<T = any> implements BrainyDataInterface<T> {
forceEmbed?: boolean // Force using the embedding function even if input is a vector
} = {}
): Promise<SearchResult<T>[]> {
await this.ensureInitialized()
if (!this.isInitialized) {
throw new Error('BrainyData must be initialized before searching. Call init() first.')
}
try {
let queryVector: Vector
@ -814,6 +894,9 @@ export class BrainyData<T = any> implements BrainyDataInterface<T> {
verbDirection?: 'outgoing' | 'incoming' | 'both' // Direction of verbs to consider when searching connected nouns
} = {}
): Promise<SearchResult<T>[]> {
if (!this.isInitialized) {
throw new Error('BrainyData must be initialized before searching. Call init() first.')
}
// If searching for verbs directly
if (options.searchVerbs) {
const verbResults = await this.searchVerbs(queryVectorOrData, k, {
@ -873,6 +956,9 @@ export class BrainyData<T = any> implements BrainyDataInterface<T> {
includeVerbs?: boolean // Whether to include associated GraphVerbs in the results
} = {}
): Promise<SearchResult<T>[]> {
if (!this.isInitialized) {
throw new Error('BrainyData must be initialized before searching. Call init() first.')
}
// If input is a string and not a vector, automatically vectorize it
let queryToUse = queryVectorOrData
if (typeof queryVectorOrData === 'string' && !options.forceEmbed) {