**feat(core, migration, docs): introduce dimension mismatch resolution tools and migration guide**

- **Core**:
  - Added `check-database.js` to verify database status and validate search functionality.
  - Created `fix-dimension-mismatch.js` to handle re-embedding of existing data to resolve dimension mismatch from 3 to 512.
  - Improved test cases by updating vector operations to support 512 dimensions, replacing previously hardcoded dimensions.

- **Migration**:
  - Developed `DIMENSION_MISMATCH_SUMMARY.md`, detailing the root cause, solution, and preventive strategies for dimension mismatch issues.
  - Added `production-migration-guide.md` for structured production migration with detailed steps on re-embedding strategies, batching, and error handling.

- **Tests**:
  - Enhanced test coverage with 512-dimensional vector validation.
  - Introduced helper functions for consistent vector testing behavior and streamlined search test cases.

- **Documentation**:
  - Updated project documentation to highlight the resolution process for dimension mismatches, emphasizing preventive mechanisms such as auto-migration and version tracking.

**Purpose**: Address critical dimension mismatch issues caused by embedding changes, restore functionality, and provide a roadmap for robust prevention strategies and migration processes.
This commit is contained in:
David Snelling 2025-07-25 13:38:56 -07:00
parent 538907755e
commit 86fb1220b6
11 changed files with 814 additions and 141 deletions

View file

@ -1,10 +1,25 @@
# Brainy Changes Log
## 2025-07-25
### Changed
- **BREAKING CHANGE**: Standardized vector dimensions to 512 throughout the codebase. The `dimensions` configuration
option has been removed from `BrainyDataConfig`. All vectors are now required to have exactly 512 dimensions, matching
the Universal Sentence Encoder's output. This change ensures consistency between data insertion, storage, and search
operations, eliminating potential dimension mismatch issues. See `VECTOR_DIMENSION_STANDARDIZATION.md` for details and
migration instructions.
- Updated all tests to expect 512 dimensions and use 512-dimensional test vectors. This includes tests in core.test.ts,
vector-operations.test.ts, environment.browser.test.ts, environment.node.test.ts, and statistics.test.ts.
## 2025-07-23
### Fixed
- Fixed an issue in the web service where tests were failing with "Cannot read properties of undefined (reading 'join')" error. The problem was a race condition in the FileSystemStorage constructor, where the path module was being used before it was fully loaded. The fix ensures that the path module is properly imported and initialized before creating the FileSystemStorage instance.
- Fixed an issue in the web service where tests were failing with "Cannot read properties of undefined (reading 'join')"
error. The problem was a race condition in the FileSystemStorage constructor, where the path module was being used
before it was fully loaded. The fix ensures that the path module is properly imported and initialized before creating
the FileSystemStorage instance.
## Previous Changes

View file

@ -0,0 +1,103 @@
# Dimension Mismatch Issue: Summary and Recommendations
## What Happened
The search functionality in Brainy stopped working because of a dimension mismatch between stored vectors and the expected dimensions in the current version of the codebase:
1. **Previous State**: The system was using vectors with 3 dimensions.
2. **Current State**: The system now expects 512-dimensional vectors from the Universal Sentence Encoder.
3. **Code Change**: Recent updates (around July 16, 2025) introduced dimension validation during initialization, which skips vectors with mismatched dimensions.
4. **Result**: During initialization, vectors with 3 dimensions were skipped, resulting in an empty search index and no search results.
## Root Cause Analysis
The root cause was identified by examining the codebase:
1. In `brainyData.ts`, the `init()` method checks if vector dimensions match the expected dimensions (line 400):
```javascript
if (noun.vector.length !== this._dimensions) {
console.warn(
`Skipping noun ${noun.id} due to dimension mismatch: expected ${this._dimensions}, got ${noun.vector.length}`
)
// Skip this noun and continue with the next one
return;
}
```
2. The default dimension is set to 512 in the constructor (line 200):
```javascript
this._dimensions = config.dimensions || 512
```
3. The `UniversalSentenceEncoder` class in `embedding.ts` produces 512-dimensional vectors (lines 358-359):
```javascript
// Return a zero vector of appropriate dimension (512 is the default for USE)
return new Array(512).fill(0)
```
4. Git history shows that on July 16, 2025, a commit was made that added dimension validation:
```
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.
```
This indicates that the system previously used 3-dimensional vectors, but after the update, it expects 512-dimensional vectors. The existing data was not migrated, causing the search functionality to break.
## Solution Implemented
We created and tested a fix script (`fix-dimension-mismatch.js`) that:
1. Creates a backup of the existing data
2. Reads all noun files directly from the filesystem
3. For each noun:
- Extracts text from metadata
- Deletes the existing noun
- Re-adds the noun with the same ID but using the current embedding function
4. Recreates all verb relationships between the re-embedded nouns
5. Verifies that search works by performing a test search
The script successfully fixed the issue by re-embedding all data with the correct dimensions, and search functionality was restored.
## Production Recommendations
For production environments, we recommend:
### 1. Use the Enhanced Migration Script
We've created a comprehensive production migration guide (`production-migration-guide.md`) that includes:
- Enhanced backup strategies with metadata
- Batching for large datasets
- Robust error handling and recovery
- Progress monitoring and reporting
- A parallel database approach for mission-critical systems
### 2. Implement Preventive Measures
To prevent similar issues in the future:
- **Version Tracking**: Add version information to stored vectors
- **Auto-Migration**: Enhance initialization to automatically re-embed mismatched vectors
- **Regular Validation**: Implement a database validation process
- **Documentation**: Document embedding changes in release notes
### 3. Scheduling and Communication
- Schedule the migration during a maintenance window
- Communicate the change to all stakeholders
- Have a rollback plan in case of issues
- Monitor the system after the migration
## Conclusion
The dimension mismatch issue was caused by a change in the embedding function that increased vector dimensions from 3 to 512. The solution is to re-embed all existing data using the current embedding function, which can be done using the provided `fix-dimension-mismatch.js` script with the enhancements suggested for production environments.
By implementing the preventive measures outlined in the production migration guide, you can avoid similar issues in the future and ensure smoother transitions when embedding functions or vector dimensions change.
## Files Created
1. `check-database.js` - Script to verify database status and search functionality
2. `fix-dimension-mismatch.js` - Script to fix the dimension mismatch issue
3. `production-migration-guide.md` - Comprehensive guide for production migration
4. `DIMENSION_MISMATCH_SUMMARY.md` - This summary document

83
check-database.js Normal file
View file

@ -0,0 +1,83 @@
// Script to check if there's any data in the database
import { BrainyData } from './dist/brainyData.js';
async function checkDatabase() {
try {
console.log('Initializing BrainyData...');
const db = new BrainyData();
await db.init();
console.log('Getting database status...');
const status = await db.status();
console.log('Database status:', JSON.stringify(status, null, 2));
console.log('Getting statistics...');
const stats = await db.getStatistics();
console.log('Statistics:', JSON.stringify(stats, null, 2));
console.log('Getting all nouns...');
const nouns = await db.getAllNouns();
console.log(`Found ${nouns.length} nouns in the database.`);
if (nouns.length > 0) {
console.log('Sample of nouns:');
for (let i = 0; i < Math.min(5, nouns.length); i++) {
console.log(`Noun ${i + 1}:`, JSON.stringify(nouns[i], null, 2));
}
}
console.log('Getting all verbs...');
const verbs = await db.getAllVerbs();
console.log(`Found ${verbs.length} verbs in the database.`);
if (verbs.length > 0) {
console.log('Sample of verbs:');
for (let i = 0; i < Math.min(5, verbs.length); i++) {
console.log(`Verb ${i + 1}:`, JSON.stringify(verbs[i], null, 2));
}
}
// Try a simple search to see if it returns any results
console.log('Trying a simple search...');
const searchResults = await db.searchText('test', 10);
console.log(`Search returned ${searchResults.length} results.`);
if (searchResults.length > 0) {
console.log('Sample of search results:');
for (let i = 0; i < Math.min(5, searchResults.length); i++) {
console.log(`Result ${i + 1}:`, JSON.stringify({
id: searchResults[i].id,
score: searchResults[i].score,
metadata: searchResults[i].metadata
}, null, 2));
}
}
// If no results, try adding a test item and searching again
if (searchResults.length === 0 && nouns.length === 0) {
console.log('No data found. Adding a test item...');
const id = await db.add('This is a test item for searching', { noun: 'Thing', category: 'test' });
console.log(`Added test item with ID: ${id}`);
console.log('Trying search again...');
const newSearchResults = await db.searchText('test', 10);
console.log(`Search returned ${newSearchResults.length} results.`);
if (newSearchResults.length > 0) {
console.log('Sample of search results:');
for (let i = 0; i < Math.min(5, newSearchResults.length); i++) {
console.log(`Result ${i + 1}:`, JSON.stringify({
id: newSearchResults[i].id,
score: newSearchResults[i].score,
metadata: newSearchResults[i].metadata
}, null, 2));
}
}
}
} catch (error) {
console.error('Error checking database:', error);
}
}
checkDatabase().catch(console.error);

163
fix-dimension-mismatch.js Normal file
View file

@ -0,0 +1,163 @@
// Script to fix dimension mismatch by re-embedding existing data
import { BrainyData } from './dist/brainyData.js';
import fs from 'fs';
import path from 'path';
async function fixDimensionMismatch() {
try {
console.log('Starting dimension mismatch fix...');
// Create a backup of the existing data
const backupDir = './brainy-data-backup-' + Date.now();
console.log(`Creating backup of existing data in ${backupDir}...`);
// Copy the entire brainy-data directory to the backup directory
await fs.promises.mkdir(backupDir, { recursive: true });
await copyDirectory('./brainy-data', backupDir);
console.log('Backup created successfully.');
// Initialize BrainyData with the current embedding function
console.log('Initializing BrainyData...');
const db = new BrainyData();
await db.init();
// Get database status to check if there's any data
const status = await db.status();
console.log('Database status:', JSON.stringify(status, null, 2));
// Read all noun files directly from the filesystem
console.log('Reading noun files directly from filesystem...');
const nounsDir = './brainy-data/nouns';
const files = await fs.promises.readdir(nounsDir);
// Process each noun file
const processedNouns = [];
for (const file of files) {
if (file.endsWith('.json')) {
const filePath = path.join(nounsDir, file);
const data = await fs.promises.readFile(filePath, 'utf-8');
const parsedNoun = JSON.parse(data);
// Get the metadata for this noun
const metadataPath = path.join('./brainy-data/metadata', `${parsedNoun.id}.json`);
let metadata = {};
try {
const metadataData = await fs.promises.readFile(metadataPath, 'utf-8');
metadata = JSON.parse(metadataData);
} catch (error) {
console.warn(`No metadata found for noun ${parsedNoun.id}`);
}
// Extract text from metadata if available
let text = '';
if (metadata.text) {
text = metadata.text;
} else if (metadata.description) {
text = metadata.description;
} else {
// If no text is available, use a placeholder
text = `Noun ${parsedNoun.id}`;
console.warn(`No text found for noun ${parsedNoun.id}, using placeholder`);
}
// Re-embed the text using the current embedding function
console.log(`Re-embedding noun ${parsedNoun.id}...`);
try {
// Delete the existing noun first
await db.delete(parsedNoun.id);
// Add the noun with the same ID but new vector
const newId = await db.add(text, metadata, { id: parsedNoun.id });
processedNouns.push({ id: newId, originalId: parsedNoun.id });
console.log(`Successfully re-embedded noun ${parsedNoun.id}`);
} catch (error) {
console.error(`Error re-embedding noun ${parsedNoun.id}:`, error);
}
}
}
console.log(`Processed ${processedNouns.length} nouns.`);
// Recreate verbs
console.log('Reading verb files directly from filesystem...');
const verbsDir = './brainy-data/verbs';
const verbFiles = await fs.promises.readdir(verbsDir);
// Process each verb file
const processedVerbs = [];
for (const file of verbFiles) {
if (file.endsWith('.json')) {
const filePath = path.join(verbsDir, file);
const data = await fs.promises.readFile(filePath, 'utf-8');
const parsedVerb = JSON.parse(data);
// Check if both source and target nouns exist
const sourceExists = processedNouns.some(n => n.originalId === parsedVerb.sourceId);
const targetExists = processedNouns.some(n => n.originalId === parsedVerb.targetId);
if (sourceExists && targetExists) {
console.log(`Re-creating verb ${parsedVerb.id} between ${parsedVerb.sourceId} and ${parsedVerb.targetId}...`);
try {
// Delete the existing verb first
await db.deleteVerb(parsedVerb.id);
// Add the verb with the same relationship
await db.addVerb(parsedVerb.sourceId, parsedVerb.targetId, {
verb: parsedVerb.type || 'RelatedTo',
...parsedVerb.metadata
});
processedVerbs.push(parsedVerb.id);
console.log(`Successfully re-created verb ${parsedVerb.id}`);
} catch (error) {
console.error(`Error re-creating verb ${parsedVerb.id}:`, error);
}
} else {
console.warn(`Skipping verb ${parsedVerb.id} because source or target noun doesn't exist`);
}
}
}
console.log(`Processed ${processedVerbs.length} verbs.`);
// Try a search to verify it works
console.log('Trying a search to verify it works...');
const searchResults = await db.searchText('test', 10);
console.log(`Search returned ${searchResults.length} results.`);
if (searchResults.length > 0) {
console.log('Sample of search results:');
for (let i = 0; i < Math.min(5, searchResults.length); i++) {
console.log(`Result ${i + 1}:`, JSON.stringify({
id: searchResults[i].id,
score: searchResults[i].score,
metadata: searchResults[i].metadata
}, null, 2));
}
}
console.log('Dimension mismatch fix completed successfully.');
} catch (error) {
console.error('Error fixing dimension mismatch:', error);
}
}
// Helper function to copy a directory recursively
async function copyDirectory(source, destination) {
const entries = await fs.promises.readdir(source, { withFileTypes: true });
await fs.promises.mkdir(destination, { recursive: true });
for (const entry of entries) {
const srcPath = path.join(source, entry.name);
const destPath = path.join(destination, entry.name);
if (entry.isDirectory()) {
await copyDirectory(srcPath, destPath);
} else {
await fs.promises.copyFile(srcPath, destPath);
}
}
}
fixDimensionMismatch().catch(console.error);

View file

@ -0,0 +1,294 @@
# Production Migration Guide for Dimension Mismatch Issue
## Root Cause Analysis
The search functionality in Brainy stopped working due to a dimension mismatch between stored vectors and the expected dimensions in the current version of the codebase.
### What Happened
1. **Dimension Change**: The system previously used vectors with 3 dimensions, but now expects 512-dimensional vectors from the Universal Sentence Encoder.
2. **Code Changes**: Recent updates (around July 16, 2025) introduced dimension validation during initialization, which skips vectors with mismatched dimensions.
3. **Result**: During initialization, vectors with 3 dimensions were skipped, resulting in an empty search index and no search results.
## Impact Assessment
1. **Data Integrity**: The original data is still intact in storage, but it cannot be used for search operations due to the dimension mismatch.
2. **Functionality**: Search operations return no results, but other operations like adding new data still work correctly.
3. **Scope**: All existing data with 3-dimensional vectors is affected.
## Production Migration Strategy
### Option 1: Full Re-embedding (Recommended)
The most reliable solution is to re-embed all existing data using the current embedding function, as implemented in the `fix-dimension-mismatch.js` script.
#### Process:
1. **Backup**: Create a complete backup of the existing data.
2. **Re-embed**: Process each noun by extracting its text content and re-embedding it with the current embedding function.
3. **Recreate Relationships**: Recreate all verb relationships between the re-embedded nouns.
4. **Verify**: Test search functionality to ensure it works correctly.
#### Production Considerations:
1. **Scheduling**: Perform the migration during a maintenance window to minimize disruption.
2. **Backup Strategy**: Use a more robust backup mechanism for production:
```javascript
// Enhanced backup for production
const backupDir = './brainy-data-backup-' + Date.now();
const backupMetadata = {
timestamp: Date.now(),
reason: 'Dimension mismatch fix',
originalDimensions: 3,
newDimensions: 512,
version: process.env.APP_VERSION || 'unknown'
};
// Save backup metadata
await fs.promises.mkdir(backupDir, { recursive: true });
await fs.promises.writeFile(
path.join(backupDir, 'backup-metadata.json'),
JSON.stringify(backupMetadata, null, 2)
);
// Copy data
await copyDirectory('./brainy-data', backupDir);
```
3. **Batching**: For large datasets, process nouns in batches to reduce memory usage:
```javascript
// Process nouns in batches
const batchSize = 100;
let processedCount = 0;
for (let i = 0; i < files.length; i += batchSize) {
const batch = files.slice(i, i + batchSize);
console.log(`Processing batch ${Math.floor(i/batchSize) + 1} of ${Math.ceil(files.length/batchSize)}...`);
// Process batch
for (const file of batch) {
// Process noun as in the original script
// ...
}
processedCount += batch.length;
console.log(`Progress: ${processedCount}/${files.length} nouns (${Math.round(processedCount/files.length*100)}%)`);
}
```
4. **Error Handling**: Implement more robust error handling and recovery:
```javascript
// Enhanced error handling
const failedNouns = [];
const failedVerbs = [];
// During noun processing
try {
// Process noun
} catch (error) {
console.error(`Error processing noun ${parsedNoun.id}:`, error);
failedNouns.push({ id: parsedNoun.id, error: error.message });
// Continue with next noun
}
// At the end of processing
if (failedNouns.length > 0 || failedVerbs.length > 0) {
const errorReport = {
timestamp: Date.now(),
failedNouns,
failedVerbs
};
await fs.promises.writeFile(
'./migration-errors.json',
JSON.stringify(errorReport, null, 2)
);
console.warn(`Migration completed with errors. See migration-errors.json for details.`);
}
```
5. **Monitoring**: Add progress monitoring and reporting:
```javascript
// Progress monitoring
const startTime = Date.now();
const logProgress = (current, total, label) => {
const percent = Math.round((current / total) * 100);
const elapsed = (Date.now() - startTime) / 1000;
const itemsPerSecond = current / elapsed;
const estimatedTotal = total / itemsPerSecond;
const remaining = estimatedTotal - elapsed;
console.log(
`${label}: ${current}/${total} (${percent}%) - ` +
`Elapsed: ${formatTime(elapsed)}, ` +
`Remaining: ${formatTime(remaining)}, ` +
`Rate: ${itemsPerSecond.toFixed(2)} items/sec`
);
};
// Format time helper
const formatTime = (seconds) => {
const hrs = Math.floor(seconds / 3600);
const mins = Math.floor((seconds % 3600) / 60);
const secs = Math.floor(seconds % 60);
return `${hrs}h ${mins}m ${secs}s`;
};
```
### Option 2: Parallel Database Approach
For mission-critical systems where downtime must be minimized:
1. **Create Parallel Database**: Set up a new database instance with the correct dimensions.
2. **Migrate in Background**: Run the migration process against this new database while the original continues to serve requests.
3. **Switch Over**: Once migration is complete and verified, switch to the new database.
```javascript
// Example of parallel database approach
async function parallelDatabaseMigration() {
// Create new database with a different storage location
const newDb = new BrainyData({
storage: {
forceFileSystemStorage: true,
rootDirectory: './brainy-data-new'
}
});
await newDb.init();
// Migrate data to new database
// (similar to fix-dimension-mismatch.js but using newDb)
// Once complete, create a switch file
await fs.promises.writeFile(
'./database-switch.json',
JSON.stringify({
readyToSwitch: true,
newDatabasePath: './brainy-data-new',
oldDatabasePath: './brainy-data',
migrationCompleted: Date.now()
})
);
console.log('Migration complete. Ready to switch to new database.');
}
```
## Preventive Measures for Future Changes
### 1. Version Tracking for Vectors
Add version information to stored vectors to track the embedding model and dimensions:
```javascript
// When adding a noun
const id = await db.add(text, {
...metadata,
_vectorInfo: {
dimensions: 512,
model: 'UniversalSentenceEncoder',
version: '1.0.0',
createdAt: Date.now()
}
});
```
### 2. Dimension Validation with Auto-Migration
Enhance the initialization process to automatically re-embed vectors with mismatched dimensions:
```javascript
// In BrainyData.init()
for (const noun of nouns) {
// Check if the vector dimensions match the expected dimensions
if (noun.vector.length !== this._dimensions) {
console.warn(
`Found noun ${noun.id} with dimension mismatch: expected ${this._dimensions}, got ${noun.vector.length}`
);
// Get metadata to extract text
const metadata = await this.storage.getMetadata(noun.id);
if (metadata && (metadata.text || metadata.description)) {
console.log(`Auto-migrating noun ${noun.id} to correct dimensions...`);
// Extract text
const text = metadata.text || metadata.description || `Noun ${noun.id}`;
// Re-embed with current embedding function
const newVector = await this.embeddingFunction(text);
// Update the noun with new vector
noun.vector = newVector;
await this.storage.saveNoun(noun);
console.log(`Successfully migrated noun ${noun.id} to ${this._dimensions} dimensions`);
} else {
console.warn(`Cannot auto-migrate noun ${noun.id}: no text found in metadata`);
continue;
}
}
// Add to index
await this.index.addItem({
id: noun.id,
vector: noun.vector
});
}
```
### 3. Regular Database Validation
Implement a regular validation process to check for dimension mismatches:
```javascript
// database-validator.js
import { BrainyData } from './dist/brainyData.js';
async function validateDatabase() {
const db = new BrainyData();
await db.init();
console.log('Validating database...');
// Get all nouns
const nouns = await db.getAllNouns();
console.log(`Found ${nouns.length} nouns.`);
// Check dimensions
const dimensionMismatches = nouns.filter(noun => noun.vector.length !== 512);
if (dimensionMismatches.length > 0) {
console.warn(`Found ${dimensionMismatches.length} nouns with dimension mismatches.`);
console.warn('Sample of mismatched nouns:');
for (let i = 0; i < Math.min(5, dimensionMismatches.length); i++) {
const noun = dimensionMismatches[i];
console.warn(`Noun ${noun.id}: expected 512 dimensions, got ${noun.vector.length}`);
}
console.warn('Run fix-dimension-mismatch.js to fix these issues.');
} else {
console.log('All nouns have correct dimensions.');
}
// Check search functionality
const searchResults = await db.searchText('test', 5);
console.log(`Search test returned ${searchResults.length} results.`);
console.log('Database validation complete.');
}
validateDatabase().catch(console.error);
```
### 4. Documentation and Change Management
1. **Document Embedding Changes**: Clearly document any changes to embedding functions or vector dimensions in release notes.
2. **Migration Scripts**: Include migration scripts with any release that changes vector dimensions.
3. **Version Compatibility**: Implement version compatibility checks in the codebase.
## Conclusion
The dimension mismatch issue was caused by a change in the embedding function that increased vector dimensions from 3 to 512. The recommended solution is to re-embed all existing data using the current embedding function, which can be done using the provided `fix-dimension-mismatch.js` script with the enhancements suggested for production environments.
By implementing the preventive measures outlined above, you can avoid similar issues in the future and ensure smoother transitions when embedding functions or vector dimensions change.

View file

@ -40,21 +40,12 @@ import {BrainyDataInterface} from './types/brainyDataInterface.js'
import {augmentationPipeline} from './augmentationPipeline.js'
export interface BrainyDataConfig {
/**
* Vector dimensions (required if not using an embedding function that auto-detects dimensions)
*/
dimensions?: number
/**
* HNSW index configuration
* Uses the optimized HNSW implementation which supports large datasets
* through product quantization and disk-based storage
*/
hnsw?: Partial<HNSWConfig>
/**
* Optimized HNSW index configuration
* If provided, will use the optimized HNSW index instead of the standard one
*/
hnswOptimized?: Partial<HNSWOptimizedConfig>
hnsw?: Partial<HNSWOptimizedConfig>
/**
* Distance function to use for similarity calculations
@ -191,30 +182,19 @@ export class BrainyData<T = any> implements BrainyDataInterface<T> {
* 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 dimensions to fixed value of 512 (Universal Sentence Encoder dimension)
this._dimensions = 512
// Set distance function
this.distanceFunction = config.distanceFunction || cosineDistance
// Check if optimized HNSW index configuration is provided
if (config.hnswOptimized) {
// Initialize optimized HNSW index
this.index = new HNSWIndexOptimized(
config.hnswOptimized,
this.distanceFunction,
config.storageAdapter || null
)
this.useOptimizedIndex = true
} else {
// Initialize standard HNSW index
this.index = new HNSWIndex(config.hnsw, this.distanceFunction)
}
// Always use the optimized HNSW index implementation
this.index = new HNSWIndexOptimized(
config.hnsw || {},
this.distanceFunction,
config.storageAdapter || null
)
this.useOptimizedIndex = true
// Set storage if provided, otherwise it will be initialized in init()
this.storage = config.storageAdapter || null
@ -944,6 +924,11 @@ export class BrainyData<T = any> implements BrainyDataInterface<T> {
throw new Error('Query vector is undefined or null')
}
// Check if query vector dimensions match the expected dimensions
if (queryVector.length !== this._dimensions) {
throw new Error(`Query vector dimension mismatch: expected ${this._dimensions}, got ${queryVector.length}`)
}
// If no noun types specified, search all nouns
if (!nounTypes || nounTypes.length === 0) {
// Search in the index
@ -1658,14 +1643,14 @@ export class BrainyData<T = any> implements BrainyDataInterface<T> {
// Get service name from options or current augmentation
const service = options.service || this.getCurrentAugmentation()
// Create timestamp for creation/update time
const now = new Date()
const timestamp = {
seconds: Math.floor(now.getTime() / 1000),
nanoseconds: (now.getTime() % 1000) * 1000000
}
// Create verb
const verb: GraphVerb = {
id,
@ -2924,10 +2909,13 @@ export class BrainyData<T = any> implements BrainyDataInterface<T> {
console.log('Reconstructing HNSW index from backup data...')
// Create a new index with the restored configuration
this.index = new HNSWIndex(
// Always use the optimized implementation for consistency
this.index = new HNSWIndexOptimized(
data.hnswIndex.config,
this.distanceFunction
this.distanceFunction,
this.storage
)
this.useOptimizedIndex = true
// Re-add all nouns to the index
for (const noun of data.nouns) {

View file

@ -5,6 +5,17 @@
import { describe, it, expect, beforeAll } from 'vitest'
/**
* Helper function to create a 512-dimensional vector for testing
* @param primaryIndex The index to set to 1.0, all other indices will be 0.0
* @returns A 512-dimensional vector with a single 1.0 value at the specified index
*/
function createTestVector(primaryIndex: number = 0): number[] {
const vector = new Array(512).fill(0)
vector[primaryIndex % 512] = 1.0
return vector
}
describe('Brainy Core Functionality', () => {
let brainy: any
@ -42,17 +53,14 @@ describe('Brainy Core Functionality', () => {
describe('BrainyData Configuration', () => {
it('should create instance with minimal configuration', () => {
const data = new brainy.BrainyData({
dimensions: 3
})
const data = new brainy.BrainyData({})
expect(data).toBeDefined()
expect(data.dimensions).toBe(3)
expect(data.dimensions).toBe(512)
})
it('should create instance with full configuration', () => {
const data = new brainy.BrainyData({
dimensions: 128,
metric: 'cosine',
maxConnections: 32,
efConstruction: 200,
@ -60,29 +68,28 @@ describe('Brainy Core Functionality', () => {
})
expect(data).toBeDefined()
expect(data.dimensions).toBe(128)
expect(data.dimensions).toBe(512)
})
it('should validate configuration parameters', () => {
it('should not throw with valid configuration parameters', () => {
// Dimensions are now fixed at 512 and not configurable
expect(() => {
new brainy.BrainyData({
dimensions: 0 // Invalid dimensions
metric: 'cosine'
})
}).toThrow()
}).not.toThrow()
expect(() => {
new brainy.BrainyData({
dimensions: -1 // Invalid dimensions
metric: 'euclidean'
})
}).toThrow()
}).not.toThrow()
})
it('should use default values for optional parameters', () => {
const data = new brainy.BrainyData({
dimensions: 10
})
const data = new brainy.BrainyData({})
expect(data.dimensions).toBe(10)
expect(data.dimensions).toBe(512)
// Should have reasonable defaults for other parameters
expect(data.maxConnections).toBeGreaterThan(0)
expect(data.efConstruction).toBeGreaterThan(0)
@ -92,20 +99,19 @@ describe('Brainy Core Functionality', () => {
describe('Vector Operations', () => {
it('should handle vector addition and search', async () => {
const data = new brainy.BrainyData({
dimensions: 3,
metric: 'euclidean'
})
await data.init()
await data.clear() // Clear any existing data
// Add vectors
await data.add([1, 0, 0], { id: 'v1', label: 'x-axis' })
await data.add([0, 1, 0], { id: 'v2', label: 'y-axis' })
await data.add([0, 0, 1], { id: 'v3', label: 'z-axis' })
// Add vectors using helper function
await data.add(createTestVector(0), { id: 'v1', label: 'x-axis' })
await data.add(createTestVector(1), { id: 'v2', label: 'y-axis' })
await data.add(createTestVector(2), { id: 'v3', label: 'z-axis' })
// Search for similar vector
const results = await data.search([1, 0, 0], 1)
const results = await data.search(createTestVector(0), 1)
expect(results).toBeDefined()
expect(results.length).toBe(1)
@ -114,7 +120,6 @@ describe('Brainy Core Functionality', () => {
it('should handle batch vector operations', async () => {
const data = new brainy.BrainyData({
dimensions: 2,
metric: 'euclidean'
})
@ -123,9 +128,9 @@ describe('Brainy Core Functionality', () => {
// Add multiple vectors
const vectors = [
{ vector: [1, 1], metadata: { id: 'batch1' } },
{ vector: [2, 2], metadata: { id: 'batch2' } },
{ vector: [3, 3], metadata: { id: 'batch3' } }
{ vector: createTestVector(10), metadata: { id: 'batch1' } },
{ vector: createTestVector(20), metadata: { id: 'batch2' } },
{ vector: createTestVector(30), metadata: { id: 'batch3' } }
]
for (const { vector, metadata } of vectors) {
@ -133,18 +138,16 @@ describe('Brainy Core Functionality', () => {
}
// Search should return results
const results = await data.search([1.5, 1.5], 3)
const results = await data.search(createTestVector(15), 3)
expect(results.length).toBe(3)
})
it('should handle different distance metrics', async () => {
const euclideanData = new brainy.BrainyData({
dimensions: 2,
metric: 'euclidean'
})
const cosineData = new brainy.BrainyData({
dimensions: 2,
metric: 'cosine'
})
@ -155,7 +158,7 @@ describe('Brainy Core Functionality', () => {
await euclideanData.clear()
await cosineData.clear()
const vector = [1, 1]
const vector = createTestVector(5)
const metadata = { id: 'test' }
await euclideanData.add(vector, metadata)
@ -237,7 +240,6 @@ describe('Brainy Core Functionality', () => {
describe('Error Handling', () => {
it('should handle invalid vector dimensions', async () => {
const data = new brainy.BrainyData({
dimensions: 3,
metric: 'euclidean'
})
@ -245,22 +247,20 @@ describe('Brainy Core Functionality', () => {
// Try to add vector with wrong dimensions
await expect(data.add([1, 2], { id: 'wrong' })).rejects.toThrow()
await expect(data.add([1, 2, 3, 4], { id: 'wrong' })).rejects.toThrow()
await expect(data.add(new Array(100).fill(0), { id: 'wrong' })).rejects.toThrow()
})
it('should handle search before initialization', async () => {
const data = new brainy.BrainyData({
dimensions: 2,
metric: 'euclidean'
})
// Try to search without initialization
await expect(data.search([1, 2], 1)).rejects.toThrow()
await expect(data.search(createTestVector(0), 1)).rejects.toThrow()
})
it('should handle empty search results gracefully', async () => {
const data = new brainy.BrainyData({
dimensions: 2,
metric: 'euclidean'
})
@ -268,7 +268,7 @@ describe('Brainy Core Functionality', () => {
await data.clear() // Clear any existing data
// Search in empty database
const results = await data.search([1, 2], 1)
const results = await data.search(createTestVector(0), 1)
expect(results).toBeDefined()
expect(Array.isArray(results)).toBe(true)
expect(results.length).toBe(0)
@ -278,7 +278,6 @@ describe('Brainy Core Functionality', () => {
describe('Performance and Scalability', () => {
it('should handle moderate number of vectors efficiently', async () => {
const data = new brainy.BrainyData({
dimensions: 10,
metric: 'euclidean'
})
@ -288,21 +287,14 @@ describe('Brainy Core Functionality', () => {
// Add 100 test vectors
for (let i = 0; i < 100; i++) {
const vector =
globalThis.testUtils?.createTestVector(10) ||
Array.from({ length: 10 }, (_, i) => (i + 1) / 10)
await data.add(vector, { id: `item_${i}`, index: i })
await data.add(createTestVector(i), { id: `item_${i}`, index: i })
}
const addTime = Date.now() - startTime
// Search should be fast
const searchStart = Date.now()
const results = await data.search(
globalThis.testUtils?.createTestVector(10) ||
Array.from({ length: 10 }, (_, i) => (i + 1) / 10),
10
)
const results = await data.search(createTestVector(50), 10)
const searchTime = Date.now() - searchStart
expect(results.length).toBeLessThanOrEqual(10)
@ -346,7 +338,6 @@ describe('Brainy Core Functionality', () => {
describe('Database Statistics', () => {
it('should provide accurate statistics about the database', async () => {
const data = new brainy.BrainyData({
dimensions: 3,
metric: 'euclidean'
})
@ -354,9 +345,9 @@ describe('Brainy Core Functionality', () => {
await data.clear() // Clear any existing data
// Add some vectors (nouns)
await data.add([1, 0, 0], { id: 'v1', label: 'x-axis' })
await data.add([0, 1, 0], { id: 'v2', label: 'y-axis' })
await data.add([0, 0, 1], { id: 'v3', label: 'z-axis' })
await data.add(createTestVector(0), { id: 'v1', label: 'x-axis' })
await data.add(createTestVector(1), { id: 'v2', label: 'y-axis' })
await data.add(createTestVector(2), { id: 'v3', label: 'z-axis' })
// Add some connections (verbs)
await data.connect('v1', 'v2', 'related_to')

View file

@ -6,6 +6,17 @@
import { describe, it, expect, beforeAll, vi } from 'vitest'
/**
* Helper function to create a 512-dimensional vector for testing
* @param primaryIndex The index to set to 1.0, all other indices will be 0.0
* @returns A 512-dimensional vector with a single 1.0 value at the specified index
*/
function createTestVector(primaryIndex: number = 0): number[] {
const vector = new Array(512).fill(0)
vector[primaryIndex % 512] = 1.0
return vector
}
describe('Brainy in Browser Environment', () => {
let brainy: any
@ -53,7 +64,6 @@ describe('Brainy in Browser Environment', () => {
describe('Core Functionality - Add Data and Search', () => {
it('should create database and add vector data', async () => {
const db = new brainy.BrainyData({
dimensions: 3,
metric: 'euclidean',
storage: {
forceMemoryStorage: true
@ -63,12 +73,12 @@ describe('Brainy in Browser Environment', () => {
await db.init()
// Add some test vectors
await db.add([1, 0, 0], { id: 'item1', label: 'x-axis' })
await db.add([0, 1, 0], { id: 'item2', label: 'y-axis' })
await db.add([0, 0, 1], { id: 'item3', label: 'z-axis' })
await db.add(createTestVector(0), { id: 'item1', label: 'x-axis' })
await db.add(createTestVector(1), { id: 'item2', label: 'y-axis' })
await db.add(createTestVector(2), { id: 'item3', label: 'z-axis' })
// Search should work
const results = await db.search([1, 0, 0], 1)
const results = await db.search(createTestVector(0), 1)
expect(results).toBeDefined()
expect(results.length).toBe(1)
expect(results[0].metadata.id).toBe('item1')
@ -102,7 +112,6 @@ describe('Brainy in Browser Environment', () => {
it('should handle multiple data types', async () => {
const db = new brainy.BrainyData({
dimensions: 2,
metric: 'euclidean',
storage: {
forceMemoryStorage: true
@ -113,9 +122,9 @@ describe('Brainy in Browser Environment', () => {
// Add different types of data
const testData = [
{ vector: [1, 1], metadata: { type: 'point', name: 'A' } },
{ vector: [2, 2], metadata: { type: 'point', name: 'B' } },
{ vector: [3, 3], metadata: { type: 'point', name: 'C' } }
{ vector: createTestVector(10), metadata: { type: 'point', name: 'A' } },
{ vector: createTestVector(20), metadata: { type: 'point', name: 'B' } },
{ vector: createTestVector(30), metadata: { type: 'point', name: 'C' } }
]
for (const item of testData) {
@ -123,7 +132,7 @@ describe('Brainy in Browser Environment', () => {
}
// Search should return relevant results
const results = await db.search([1.5, 1.5], 2)
const results = await db.search(createTestVector(15), 2)
expect(results.length).toBe(2)
expect(
results.every(
@ -134,15 +143,14 @@ describe('Brainy in Browser Environment', () => {
})
describe('Error Handling', () => {
it('should handle invalid configurations gracefully', () => {
it('should not throw with valid configuration', () => {
expect(() => {
new brainy.BrainyData({ dimensions: 0 })
}).toThrow()
new brainy.BrainyData({ metric: 'euclidean' })
}).not.toThrow()
})
it('should handle search on empty database', async () => {
const db = new brainy.BrainyData({
dimensions: 2,
metric: 'euclidean',
storage: {
forceMemoryStorage: true
@ -151,7 +159,7 @@ describe('Brainy in Browser Environment', () => {
await db.init()
const results = await db.search([1, 2], 5)
const results = await db.search(createTestVector(0), 5)
expect(results).toBeDefined()
expect(Array.isArray(results)).toBe(true)
expect(results.length).toBe(0)

View file

@ -5,6 +5,17 @@
import { describe, it, expect, beforeAll } from 'vitest'
/**
* Helper function to create a 512-dimensional vector for testing
* @param primaryIndex The index to set to 1.0, all other indices will be 0.0
* @returns A 512-dimensional vector with a single 1.0 value at the specified index
*/
function createTestVector(primaryIndex: number = 0): number[] {
const vector = new Array(512).fill(0)
vector[primaryIndex % 512] = 1.0
return vector
}
describe('Brainy in Node.js Environment', () => {
let brainy: any
@ -53,7 +64,6 @@ describe('Brainy in Node.js Environment', () => {
return
}
const db = new brainy.BrainyData({
dimensions: 3,
metric: 'euclidean',
storage: {
forceMemoryStorage: true
@ -64,12 +74,12 @@ describe('Brainy in Node.js Environment', () => {
await db.clear() // Clear any existing data
// Add some test vectors
await db.add([1, 0, 0], { id: 'item1', label: 'x-axis' })
await db.add([0, 1, 0], { id: 'item2', label: 'y-axis' })
await db.add([0, 0, 1], { id: 'item3', label: 'z-axis' })
await db.add(createTestVector(0), { id: 'item1', label: 'x-axis' })
await db.add(createTestVector(1), { id: 'item2', label: 'y-axis' })
await db.add(createTestVector(2), { id: 'item3', label: 'z-axis' })
// Search should work
const results = await db.search([1, 0, 0], 1)
const results = await db.search(createTestVector(0), 1)
expect(results).toBeDefined()
expect(results.length).toBe(1)
expect(results[0].metadata.id).toBe('item1')
@ -114,7 +124,6 @@ describe('Brainy in Node.js Environment', () => {
return
}
const db = new brainy.BrainyData({
dimensions: 2,
metric: 'euclidean',
storage: {
forceMemoryStorage: true
@ -126,9 +135,9 @@ describe('Brainy in Node.js Environment', () => {
// Add different types of data
const testData = [
{ vector: [1, 1], metadata: { type: 'point', name: 'A' } },
{ vector: [2, 2], metadata: { type: 'point', name: 'B' } },
{ vector: [3, 3], metadata: { type: 'point', name: 'C' } }
{ vector: createTestVector(10), metadata: { type: 'point', name: 'A' } },
{ vector: createTestVector(20), metadata: { type: 'point', name: 'B' } },
{ vector: createTestVector(30), metadata: { type: 'point', name: 'C' } }
]
for (const item of testData) {
@ -136,7 +145,7 @@ describe('Brainy in Node.js Environment', () => {
}
// Search should return relevant results
const results = await db.search([1.5, 1.5], 2)
const results = await db.search(createTestVector(15), 2)
expect(results.length).toBe(2)
expect(
results.every(
@ -147,14 +156,14 @@ describe('Brainy in Node.js Environment', () => {
})
describe('Error Handling', () => {
it('should handle invalid configurations gracefully', () => {
it('should not throw with valid configuration', () => {
if (brainy === null) {
console.warn('Skipping test due to TensorFlow.js initialization issue')
return
}
expect(() => {
new brainy.BrainyData({ dimensions: 0 })
}).toThrow()
new brainy.BrainyData({ metric: 'euclidean' })
}).not.toThrow()
})
it('should handle search on empty database', async () => {
@ -163,7 +172,6 @@ describe('Brainy in Node.js Environment', () => {
return
}
const db = new brainy.BrainyData({
dimensions: 2,
metric: 'euclidean',
storage: {
forceMemoryStorage: true
@ -173,7 +181,7 @@ describe('Brainy in Node.js Environment', () => {
await db.init()
await db.clear() // Clear any existing data
const results = await db.search([1, 2], 5)
const results = await db.search(createTestVector(0), 5)
expect(results).toBeDefined()
expect(Array.isArray(results)).toBe(true)
expect(results.length).toBe(0)

View file

@ -5,6 +5,17 @@
import { describe, it, expect, beforeAll } from 'vitest'
/**
* Helper function to create a 512-dimensional vector for testing
* @param primaryIndex The index to set to 1.0, all other indices will be 0.0
* @returns A 512-dimensional vector with a single 1.0 value at the specified index
*/
function createTestVector(primaryIndex: number = 0): number[] {
const vector = new Array(512).fill(0)
vector[primaryIndex % 512] = 1.0
return vector
}
describe('Brainy Statistics Functionality', () => {
let brainy: any
@ -24,7 +35,6 @@ describe('Brainy Statistics Functionality', () => {
it('should retrieve statistics from a BrainyData instance', async () => {
// Create a BrainyData instance
const data = new brainy.BrainyData({
dimensions: 3,
metric: 'euclidean'
})
@ -32,12 +42,12 @@ describe('Brainy Statistics Functionality', () => {
await data.clear() // Clear any existing data
// Add some test data
await data.add([1, 0, 0], { id: 'v1', label: 'x-axis' })
await data.add([0, 1, 0], { id: 'v2', label: 'y-axis' })
await data.add([0, 0, 1], { id: 'v3', label: 'z-axis' })
await data.add(createTestVector(0), { id: 'v1', label: 'x-axis' })
await data.add(createTestVector(1), { id: 'v2', label: 'y-axis' })
await data.add(createTestVector(2), { id: 'v3', label: 'z-axis' })
// Add a verb
await data.addVerb('v1', 'v2', [0.5, 0.5, 0], { type: 'connected_to' })
await data.addVerb('v1', 'v2', createTestVector(3), { type: 'connected_to' })
// Get statistics using the standalone function
const stats = await brainy.getStatistics(data)
@ -56,14 +66,12 @@ describe('Brainy Statistics Functionality', () => {
it('should match the instance method results', async () => {
// Create a BrainyData instance
const data = new brainy.BrainyData({
dimensions: 3
})
const data = new brainy.BrainyData({})
await data.init()
// Add some test data
await data.add([1, 1, 1], { id: 'test1' })
await data.add(createTestVector(5), { id: 'test1' })
// Get statistics using both methods
const instanceStats = await data.getStatistics()
@ -76,7 +84,6 @@ describe('Brainy Statistics Functionality', () => {
it('should track statistics by service', async () => {
// Create a BrainyData instance
const data = new brainy.BrainyData({
dimensions: 3,
metric: 'euclidean'
})
@ -84,9 +91,9 @@ describe('Brainy Statistics Functionality', () => {
await data.clear() // Clear any existing data
// Add data from different services
await data.add([1, 0, 0], { id: 'v1', label: 'service1-item' }, { service: 'service1' })
await data.add([0, 1, 0], { id: 'v2', label: 'service1-item' }, { service: 'service1' })
await data.add([0, 0, 1], { id: 'v3', label: 'service2-item' }, { service: 'service2' })
await data.add(createTestVector(10), { id: 'v1', label: 'service1-item' }, { service: 'service1' })
await data.add(createTestVector(20), { id: 'v2', label: 'service1-item' }, { service: 'service1' })
await data.add(createTestVector(30), { id: 'v3', label: 'service2-item' }, { service: 'service2' })
// Add verbs from different services
await data.addVerb('v1', 'v2', undefined, { type: 'related_to', service: 'service1' })

View file

@ -1,6 +1,17 @@
import { describe, it, expect } from 'vitest'
import { euclideanDistance } from '../src/utils/distance.js'
/**
* Helper function to create a 512-dimensional vector for testing
* @param primaryIndex The index to set to 1.0, all other indices will be 0.0
* @returns A 512-dimensional vector with a single 1.0 value at the specified index
*/
function createTestVector(primaryIndex: number = 0): number[] {
const vector = new Array(512).fill(0)
vector[primaryIndex % 512] = 1.0
return vector
}
describe('Vector Operations', () => {
it('should load brainy library successfully', async () => {
const brainy = await import('../dist/unified.js')
@ -14,23 +25,21 @@ describe('Vector Operations', () => {
const brainy = await import('../dist/unified.js')
const db = new brainy.BrainyData({
dimensions: 3,
distanceFunction: euclideanDistance
})
expect(db).toBeDefined()
expect(db.dimensions).toBe(3)
expect(db.dimensions).toBe(512)
await db.init()
// If we get here without throwing, initialization was successful
expect(true).toBe(true)
})
it('should handle simple 2D vector operations', async () => {
it('should handle simple vector operations', async () => {
const brainy = await import('../dist/unified.js')
const db = new brainy.BrainyData({
dimensions: 2,
distanceFunction: euclideanDistance
})
@ -38,10 +47,11 @@ describe('Vector Operations', () => {
await db.clear() // Clear any existing data
// Add a simple vector
await db.add([1, 2], { id: 'test' })
const testVector = createTestVector(1)
await db.add(testVector, { id: 'test' })
// Search for the same vector
const results = await db.search([1, 2], 1)
const results = await db.search(testVector, 1)
expect(results).toBeDefined()
expect(results.length).toBeGreaterThan(0)
@ -52,7 +62,6 @@ describe('Vector Operations', () => {
const brainy = await import('../dist/unified.js')
const db = new brainy.BrainyData({
dimensions: 3,
distanceFunction: euclideanDistance
})
@ -60,13 +69,17 @@ describe('Vector Operations', () => {
await db.clear() // Clear any existing data
// Add multiple vectors
await db.add([1, 0, 0], { id: 'vec1', type: 'unit' })
await db.add([0, 1, 0], { id: 'vec2', type: 'unit' })
await db.add([0, 0, 1], { id: 'vec3', type: 'unit' })
await db.add([0.5, 0.5, 0], { id: 'vec4', type: 'mixed' })
await db.add(createTestVector(0), { id: 'vec1', type: 'unit' })
await db.add(createTestVector(1), { id: 'vec2', type: 'unit' })
await db.add(createTestVector(2), { id: 'vec3', type: 'unit' })
// Create a mixed vector with two non-zero elements
const mixedVector = createTestVector(3)
mixedVector[4] = 0.5
await db.add(mixedVector, { id: 'vec4', type: 'mixed' })
// Search for multiple results
const results = await db.search([1, 0, 0], 3)
const results = await db.search(createTestVector(0), 3)
expect(results).toBeDefined()
expect(results.length).toBeGreaterThanOrEqual(1)