voyageai-cli
Version:
CLI for Voyage AI embeddings, reranking, and MongoDB Atlas Vector Search
188 lines (165 loc) • 6.5 kB
JavaScript
;
const { getDefaultDimensions } = require('../lib/catalog');
const { getMongoCollection } = require('../lib/mongo');
const ui = require('../lib/ui');
/**
* Register the index command (with create, list, delete subcommands) on a Commander program.
* @param {import('commander').Command} program
*/
function registerIndex(program) {
const indexCmd = program
.command('index')
.description('Manage Atlas Vector Search indexes');
// ── index create ──
indexCmd
.command('create')
.description('Create a vector search index')
.requiredOption('--db <database>', 'Database name')
.requiredOption('--collection <name>', 'Collection name')
.requiredOption('--field <name>', 'Embedding field name')
.option('-d, --dimensions <n>', 'Vector dimensions', (v) => parseInt(v, 10), getDefaultDimensions())
.option('-s, --similarity <type>', 'Similarity function: cosine, dotProduct, euclidean', 'cosine')
.option('-n, --index-name <name>', 'Index name', 'vector_index')
.option('--json', 'Machine-readable JSON output')
.option('-q, --quiet', 'Suppress non-essential output')
.action(async (opts) => {
let client;
try {
const useColor = !opts.json;
const useSpinner = useColor && !opts.quiet;
let spin;
if (useSpinner) {
spin = ui.spinner('Creating vector search index...');
spin.start();
}
const { client: c, collection } = await getMongoCollection(opts.db, opts.collection);
client = c;
const indexDef = {
name: opts.indexName,
type: 'vectorSearch',
definition: {
fields: [
{
type: 'vector',
path: opts.field,
numDimensions: parseInt(opts.dimensions, 10) || getDefaultDimensions(),
similarity: opts.similarity,
},
],
},
};
const result = await collection.createSearchIndex(indexDef);
if (spin) spin.stop();
if (opts.json) {
console.log(JSON.stringify({ indexName: result, definition: indexDef }, null, 2));
} else if (!opts.quiet) {
console.log(ui.success(`Vector search index created: "${result}"`));
console.log(ui.label('Database', opts.db));
console.log(ui.label('Collection', opts.collection));
console.log(ui.label('Field', opts.field));
console.log(ui.label('Dimensions', String(opts.dimensions)));
console.log(ui.label('Similarity', opts.similarity));
console.log('');
console.log(ui.dim('Note: Index may take a few minutes to become ready.'));
}
} catch (err) {
if (err.message && err.message.includes('already exists')) {
console.error(ui.error(`Index "${opts.indexName}" already exists on ${opts.db}.${opts.collection}`));
console.error(ui.dim('Use a different --index-name or delete the existing index first.'));
} else {
console.error(ui.error(err.message));
}
process.exit(1);
} finally {
if (client) await client.close();
}
});
// ── index list ──
indexCmd
.command('list')
.description('List all search indexes on a collection')
.requiredOption('--db <database>', 'Database name')
.requiredOption('--collection <name>', 'Collection name')
.option('--json', 'Machine-readable JSON output')
.option('-q, --quiet', 'Suppress non-essential output')
.action(async (opts) => {
let client;
try {
const useColor = !opts.json;
const useSpinner = useColor && !opts.quiet;
let spin;
if (useSpinner) {
spin = ui.spinner('Listing indexes...');
spin.start();
}
const { client: c, collection } = await getMongoCollection(opts.db, opts.collection);
client = c;
const indexes = await collection.listSearchIndexes().toArray();
if (spin) spin.stop();
if (opts.json) {
console.log(JSON.stringify(indexes, null, 2));
return;
}
if (indexes.length === 0) {
console.log(`No search indexes found on ${opts.db}.${opts.collection}`);
return;
}
if (!opts.quiet) {
console.log(`Search indexes on ${ui.cyan(opts.db + '.' + opts.collection)}:`);
console.log('');
}
for (const idx of indexes) {
console.log(ui.label('Name', ui.bold(idx.name)));
console.log(ui.label('Type', idx.type || 'N/A'));
console.log(ui.label('Status', ui.status(idx.status || 'N/A')));
if (idx.latestDefinition) {
console.log(ui.label('Fields', JSON.stringify(idx.latestDefinition.fields || [])));
}
console.log('');
}
} catch (err) {
console.error(ui.error(err.message));
process.exit(1);
} finally {
if (client) await client.close();
}
});
// ── index delete ──
indexCmd
.command('delete')
.description('Drop a search index')
.requiredOption('--db <database>', 'Database name')
.requiredOption('--collection <name>', 'Collection name')
.requiredOption('-n, --index-name <name>', 'Index name to delete')
.option('--json', 'Machine-readable JSON output')
.option('-q, --quiet', 'Suppress non-essential output')
.action(async (opts) => {
let client;
try {
const useColor = !opts.json;
const useSpinner = useColor && !opts.quiet;
let spin;
if (useSpinner) {
spin = ui.spinner('Deleting index...');
spin.start();
}
const { client: c, collection } = await getMongoCollection(opts.db, opts.collection);
client = c;
await collection.dropSearchIndex(opts.indexName);
if (spin) spin.stop();
if (opts.json) {
console.log(JSON.stringify({ dropped: opts.indexName }, null, 2));
} else if (!opts.quiet) {
console.log(ui.success(`Dropped search index: "${opts.indexName}"`));
console.log(ui.label('Database', opts.db));
console.log(ui.label('Collection', opts.collection));
}
} catch (err) {
console.error(ui.error(err.message));
process.exit(1);
} finally {
if (client) await client.close();
}
});
}
module.exports = { registerIndex };