- **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.
294 lines
No EOL
10 KiB
Markdown
294 lines
No EOL
10 KiB
Markdown
# 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. |