Search Index Lifecycle

Since v3.4.0, collection accessors and Models can create, inspect, update, drop, and wait for MongoDB Search indexes. One API covers full-text search indexes and vectorSearch indexes; use vectorSearch() to query a Vector Search index.

Requirements

  • MongoDB Atlas, MongoDB Community or Enterprise 8.2+ with mongot, or the local mongodb/mongodb-atlas-local image.
  • Plain mongod deployments (including mongodb-memory-server) do not support Search index commands; calls reject with SEARCH_INDEX_UNSUPPORTED.
  • Index builds are asynchronous on the server. Create, update, and drop return once the server accepts the command; use waitForSearchIndex() to wait for the outcome.

API

collection.createSearchIndex(spec: SearchIndexSpec, options?: { ifExists?: 'error' | 'skip' | 'update' })
  : Promise<{ name: string; action: 'created' | 'skipped' | 'updated'; created: boolean }>;
collection.createSearchIndexes(specs: SearchIndexSpec[]): Promise<string[]>;
collection.listSearchIndexes(options?: { type?: 'search' | 'vectorSearch' }): Promise<SearchIndexInfo[]>;
collection.getSearchIndex(name: string): Promise<SearchIndexInfo>;
collection.updateSearchIndex(name: string, definition: Record<string, unknown>): Promise<void>;
collection.dropSearchIndex(name: string, options?: { ignoreMissing?: boolean }): Promise<void>;
collection.waitForSearchIndex(name: string, options?: {
  until?: 'queryable' | 'ready' | 'dropped'; // default 'queryable'
  timeoutMs?: number;                        // default 300000
  intervalMs?: number;                       // default 2000, minimum 100
  signal?: AbortSignal;
}): Promise<SearchIndexInfo>;

Models expose the same seven methods.

interface SearchIndexSpec {
  name: string;
  type: 'search' | 'vectorSearch';
  definition: Record<string, unknown>;
}

interface SearchIndexInfo {
  name: string;
  status: 'PENDING' | 'BUILDING' | 'READY' | 'FAILED' | 'STALE' | 'DELETING' | 'DOES_NOT_EXIST';
  queryable: boolean;
  type?: 'search' | 'vectorSearch';
  typeSource: 'server' | 'inferred' | 'unknown';
  id?: string;
  latestDefinition?: Record<string, unknown>;
  latestDefinitionVersion?: { version: number; createdAt: Date };
  statusDetail?: Array<Record<string, unknown>>;
  raw: Record<string, unknown>;
}

Behavior

MethodBehavior
createSearchIndexValidates locally, then checks for an index with the same name. With the default ifExists: 'error' an existing index rejects with SEARCH_INDEX_EXISTS. 'skip' leaves it unchanged (action: 'skipped'). 'update' replaces the definition (action: 'updated'), unless the type differs, which rejects with INVALID_SEARCH_INDEX_DEFINITION (drop the index first).
createSearchIndexesValidates every spec before sending one command. It has no ifExists option; duplicate names reject with SEARCH_INDEX_EXISTS.
listSearchIndexesReturns normalized entries. type filters by the resolved type.
getSearchIndexA missing index resolves to status: 'DOES_NOT_EXIST'; it does not throw.
updateSearchIndexReplaces the definition and the server rebuilds the index. While it rebuilds, the previous version can keep serving queries. The reported queryable flag varies: Community 8.2.12 kept it true, while atlas-local 8.3.13 reported false during a search rebuild even though $search still returned results (both measured). A missing index rejects with SEARCH_INDEX_NOT_FOUND.
dropSearchIndexA missing index rejects with SEARCH_INDEX_NOT_FOUND unless ignoreMissing: true.
waitForSearchIndexPolls once per intervalMs. See the table below.

Updating a vectorSearch index depends on the deployment.

DeploymentIn-place updateSearchIndex on vectorSearch
MongoDB AtlasSupported according to the MongoDB documentation; the old index keeps serving queries while the new one builds. Not tested by monSQLize.
mongodb/mongodb-atlas-local (mongod 8.3.13)Rejected by the server: BadValue (2) "mappings" is required. Measured.
Community 8.2.12 + mongodb/mongodb-community-searchAccepted. During the rebuild (about 30 s for 20,000 documents × 64 dimensions) $vectorSearch kept returning results. Measured.

On every measured deployment the server rejects a type field in the updateSearchIndex command (IDLUnknownField), so sending one does not help. monSQLize passes a rejection through as MONGODB_ERROR with serverCode and never falls back on its own.

Where an in-place update is not available, change the definition without downtime like this: create an index under a new name, waitForSearchIndex(newName, { until: 'queryable' }), switch your queries to the new name, then drop the old index. Both indexes exist for a while, which uses extra resources and counts against index limits (Atlas Free tier: 3 Search indexes, Flex: 10). If a short outage is acceptable, you can instead drop the index, wait for dropped, and create it again. Searches fail until it is queryable again, and that gap grows with data size (measured on atlas-local: about 1–2 s for 50,000 documents × 64 dimensions).

status passes the server value through unchanged. Use queryable to decide whether searches can run and status to show progress.

Index type

The type comes from the server's top-level type field (typeSource: 'server'). Self-managed mongot does not return that field (MongoDB limitations). In that case the type is inferred from latestDefinition (typeSource: 'inferred'): a fields array with a type: 'vector' entry means vectorSearch, and a mappings key means search. Otherwise type is omitted and typeSource is 'unknown'; such entries never match a listSearchIndexes({ type }) filter.

waitForSearchIndex

untilResolves whenFAILEDMissing index
queryablequeryable === trueRejects with SEARCH_INDEX_BUILD_FAILED only when not queryableKeeps waiting for up to 30 seconds, then rejects with SEARCH_INDEX_NOT_FOUND
readystatus === 'READY'Rejects with SEARCH_INDEX_BUILD_FAILEDSame as above
droppedstatus === 'DOES_NOT_EXIST'Keeps waitingResolves

Timeouts reject with SEARCH_INDEX_WAIT_TIMEOUT, and details[0].last holds the last observed status. Aborting signal rejects with signal.reason unchanged.

writePathPolicy

createSearchIndex, createSearchIndexes, updateSearchIndex, and dropSearchIndex are management operations, the same as createIndex(). Under model-only with management: 'inherit', Model calls are allowed and direct collection calls reject with INVALID_OPERATION. Read methods (listSearchIndexes, getSearchIndex, waitForSearchIndex) are not restricted.

Errors

CodeWhen
INVALID_SEARCH_INDEX_DEFINITIONLocal validation failed (name, type, definition, options), or ifExists: 'update' targets an index of another type
SEARCH_INDEX_EXISTSSame-name index exists with ifExists: 'error', or duplicate names in createSearchIndexes
SEARCH_INDEX_NOT_FOUNDupdateSearchIndex / dropSearchIndex target is missing; waitForSearchIndex saw no index after the grace period
SEARCH_INDEX_UNSUPPORTEDThe deployment does not support Search index commands
SEARCH_INDEX_BUILD_FAILEDwaitForSearchIndex observed FAILED
SEARCH_INDEX_WAIT_TIMEOUTwaitForSearchIndex exceeded timeoutMs

Every error raised from a server response keeps the driver error as cause. Server errors without a dedicated code reject with MONGODB_ERROR; details[0] carries serverCode, serverCodeName, and serverMessage, and error.monsqlize = { operation, namespace, indexName } is attached. See Error codes.

Examples

const docs = msq.collection('docs');

await docs.createSearchIndex({
  name: 'embedding_idx',
  type: 'vectorSearch',
  definition: {
    fields: [
      { type: 'vector', path: 'embedding', numDimensions: 1536, similarity: 'cosine' },
      { type: 'filter', path: 'tenantId' },
    ],
  },
}, { ifExists: 'skip' });
await docs.waitForSearchIndex('embedding_idx', { until: 'queryable', timeoutMs: 600_000 });

await docs.createSearchIndex({ name: 'text_idx', type: 'search', definition: { mappings: { dynamic: true } } });
const info = await docs.getSearchIndex('text_idx');
console.log(info.status, info.queryable);

await docs.dropSearchIndex('text_idx', { ignoreMissing: true });
await docs.waitForSearchIndex('text_idx', { until: 'dropped' });