Skip to main content
doc0blade47/fumadocsdocumentation home
Sign in

Search Server Indexing

Last updated Oct 12, 2026
Relevant source files

The following files were used as context for generating this wiki page:

Overview

Search Server Indexing transforms raw MDX documentation and source collections into structured, searchable records and indexes for search backends like local databases, Orama Cloud, Algolia, and Mixedbread. It extracts document content, headings, descriptions, and breadcrumbs, supplying consistent search endpoints across localized page trees.

Sources: packages/core/src/search/server/build-index.ts:6-48, packages/core/src/search/orama/create-server.ts:78-134

Document extraction and structured indexing

SharedIndex and SharedDocument interfaces

The search indexing pipeline transforms page collections into search-ready payloads using two foundational data structures: SharedIndex and SharedDocument. A SharedIndex captures page-level metadata alongside preprocessed StructuredData, while SharedDocument decomposes that record into granular searchable entries categorized by type (page, heading, or text).

export interface SharedIndex {
  id: string;
  title: string;
  description?: string;
  breadcrumbs?: string[];
  tag?: string | string[];
  structuredData: StructuredData;
  url: string;
}
 
export interface SharedDocument {
  id: string;
  content: string;
  page_id: string;
  type: 'page' | 'heading' | 'text';
  breadcrumbs?: string[];
  tags: string[];
  url: string;
}

Sources: packages/core/src/search/server/build-index.ts:6-22, packages/core/src/search/server/build-doc.ts:3-11

Page index building and resolution

The buildIndexDefault function resolves structured data from a Page object by checking page.data.structuredData (evaluating it if defined as a function) or falling back to invoking page.data.load() if present. If neither source yields structured data, an error is thrown. Titles default to page.data.title or fall back to the filename derived via basename and extname.

export async function buildIndexDefault(page: Page): Promise<SharedIndex> {
  let structuredData: StructuredData | undefined;
 
  if (page.data.structuredData) {
    structuredData =
      typeof page.data.structuredData === 'function'
        ? await page.data.structuredData()
        : page.data.structuredData;
  } else if ('load' in page.data && typeof page.data.load === 'function') {
    structuredData = (await page.data.load()).structuredData;
  }
 
  if (!structuredData)
    throw new Error(
      'Cannot find structured data from page, please define the page to index function.',
    );
 
  return {
    title: page.data.title ?? basename(page.path, extname(page.path)),
    description: page.data.description,
    url: page.url,
    id: page.url,
    structuredData,
  };
}

Sources: packages/core/src/search/server/build-index.ts:24-48

Warning

Omitting structured data preprocessing on a page without a load method or static structuredData property causes buildIndexDefault to throw an explicit runtime error during index compilation.

Sources: packages/core/src/search/server/build-index.ts:36-39

The buildBreadcrumbs utility constructs hierarchical navigation paths for search records by querying the localized page tree obtained via source.getPageTree(page.locale). It uses findPath to locate the target node, pops the final page segment, and collects valid non-empty string names from parent nodes and the page tree root.

function isBreadcrumbItem(item: unknown): item is string {
  return typeof item === 'string' && item.length > 0;
}
 
export function buildBreadcrumbs<C extends LoaderConfig>(
  source: LoaderOutput<C>,
  page: C['page'],
): string[] | undefined {
  const pageTree = source.getPageTree(page.locale);
  const path = findPath(pageTree.children, (node) => node.type === 'page' && node.url === page.url);
 
  if (path) {
    const breadcrumbs: string[] = [];
    path.pop();
 
    if (isBreadcrumbItem(pageTree.name)) {
      breadcrumbs.push(pageTree.name);
    }
 
    for (const segment of path) {
      if (!isBreadcrumbItem(segment.name)) continue;
      breadcrumbs.push(segment.name);
    }
 
    return breadcrumbs;
  }
}

Sources: packages/core/src/search/server/build-index.ts:50-76

Document flattening and record generation

The buildDocuments function iterates over an array of SharedIndex objects to produce a flat list of SharedDocument records. Each page generates a primary document record (type: 'page'), an optional description record (type: 'text'), multiple heading records (type: 'heading'), and body text content chunks (type: 'text').

Document TypeID FormatURL FormatContent Source
pagepage.idpage.urlpage.title
text (description)${page.id}-${id++}page.urlpage.description
heading${page.id}-${id++}${page.url}#${heading.id}heading.content
text (content chunk)${page.id}-${id++}${page.url}#${content.heading} or page.urlcontent.content

Sources: packages/core/src/search/server/build-doc.ts:13-68

Local Orama database instantiation

Database Schemas and Document Types

The Orama search engine supports two distinct schema configurations depending on whether simple or advanced search servers are initialized. The simple schema defines fields for url, title, breadcrumbs, description, content, and keywords. The advanced schema incorporates granular fields including content, page_id, type, breadcrumbs, an enum[] for tags, url, and a 512-dimensional vector field embeddings.

export type SimpleDocument = TypedDocument<Orama<typeof simpleSchema>>;
export const simpleSchema = {
  url: 'string',
  title: 'string',
  breadcrumbs: 'string[]',
  description: 'string',
  content: 'string',
  keywords: 'string',
} as const;
 
export type AdvancedDocument = TypedDocument<Orama<typeof advancedSchema>>;
export const advancedSchema = {
  content: 'string',
  page_id: 'string',
  type: 'string',
  breadcrumbs: 'string[]',
  tags: 'enum[]',
  url: 'string',
  embeddings: 'vector[512]',
} as const;

Sources: packages/core/src/search/orama/create-db.ts:11-30

Note

Advanced search vector queries require installing @orama/plugin-embeddings separately; invoking vector mode without this plugin throws an explicit runtime configuration error.

Sources: packages/core/src/search/orama/create-server.ts:121-128

Database Creation and Index Insertion

The createDB and createDBSimple functions instantiate in-memory Orama databases and populate them with records. If indexes is supplied as a dynamic function, it is awaited before index processing. Custom tokenizers take precedence when passed in configuration options, falling back to existing component tokenizers.

export async function createDB({
  indexes,
  tokenizer,
  search: _,
  ...rest
}: AdvancedOptions): Promise<Orama<typeof advancedSchema>> {
  const items = typeof indexes === 'function' ? await indexes() : indexes;
 
  const db = create({
    schema: advancedSchema,
    ...rest,
    components: {
      ...rest.components,
      tokenizer: tokenizer ?? rest.components?.tokenizer,
    },
  }) as Orama<typeof advancedSchema>;
 
  const mapTo: PartialSchemaDeep<AdvancedDocument>[] = buildDocuments(items);
  await insertMultiple(db, mapTo);
  return db;
}

Sources: packages/core/src/search/orama/create-db.ts:32-52

export async function createDBSimple({
  indexes,
  tokenizer,
  ...rest
}: SimpleOptions): Promise<Orama<typeof simpleSchema>> {
  const items = typeof indexes === 'function' ? await indexes() : indexes;
  const db = create({
    schema: simpleSchema,
    ...rest,
    components: {
      ...rest.components,
      tokenizer: tokenizer ?? rest.components?.tokenizer,
    },
  }) as Orama<typeof simpleSchema>;
 
  await insertMultiple(
    db,
    items.map((page) => ({
      title: page.title,
      description: page.description,
      breadcrumbs: page.breadcrumbs,
      url: page.url,
      content: page.content,
      keywords: page.keywords,
    })),
  );
 
  return db;
}

Sources: packages/core/src/search/orama/create-db.ts:54-82

Stemmers and Internationalized Tokenizer Resolution

Internationalized search servers resolve language stemmers using a predefined mapping of full language names to language codes. The STEMMERS dictionary defines 32 supported language mappings for tokenization and stemming.

Language NameCodeLanguage NameCodeLanguage NameCode
arabicaririshieslovenianru
armenianamitalianitspanishes
bulgarianbglithuanianltswedishse
czechcznepalinptamilta
danishdknorwegiannoturkishtr
dutchnlportugueseptukrainianuk
englishenromanianrovietnamesevi
finnishfirussianrusanskritsk
frenchfrserbianrs
germande
greekgr
hungarianhu
indianin
indonesianid

Sources: packages/core/src/search/orama/_stemmers.ts:2-33

The getTokenizer helper checks if a given locale string matches a registered stemmer key, returning the corresponding language code or falling back directly to the input locale string.

function getTokenizer(locale: string): { language: string } | { tokenizer: Tokenizer } {
  return {
    language: Object.keys(STEMMERS).find((lang) => STEMMERS[lang] === locale) ?? locale,
  };
}

Sources: packages/core/src/search/orama/create-server.ts:172-176

Flexsearch index generation and routing

Initialization and Execution Walkthrough

Multi-locale Flexsearch indices are constructed and served by navigating through a strict call chain. The process executes through the following steps: flexsearchI18n calls serverI18n, which invokes initSearchServers to group indexes by locale and initialize per-locale servers via server. Each server call invokes initIndex, which builds documents via buildDocuments and generates unique identifiers using nextId.

Sources: packages/core/src/search/flexsearch.ts:41-137, packages/core/src/search/server/build-doc.ts:13-68

Note

When flexsearchFromSource evaluates a loader function, it caches the resulting promise in a WeakMap mapped to the specific loader output instance. Re-indexing occurs automatically once a distinct loader instance is returned.

Sources: packages/core/src/search/flexsearch.ts:149-186

Document Construction and Search Options

The Flexsearch document schema configures tokenization, indexed fields, and storage options. By default, createDocument sets tokenize to 'full', indexes the content field, tags records using tags, and enables store.

export function createDocument(options?: DocumentOptions<Doc>) {
  return new Search.Document<Doc>({
    tokenize: 'full',
    ...options,
    document: {
      id: 'id',
      index: ['content'],
      tag: ['tags'],
      store: true,
      ...options?.document,
    },
  });

Sources: packages/core/src/search/flexsearch/utils.ts:70-81

Parameter / PropertyTypeDefault ValuePurpose
tokenizestring'full'Sets the string tokenization strategy for indexing.
document.idstring'id'Specifies the unique document identifier field.
document.indexstring[]['content']Defines the document fields included in the search index.
document.tagstring[]['tags']Defines the fields used for tag-based filtering.
document.storebooleantrueEnables field storage within the index for retrieval.

Sources: packages/core/src/search/flexsearch/utils.ts:70-81

Warning

Setting a localeMap entry to the literal string 'cjk' automatically forces the document encoder configuration to Search.Charset.CJK. Any custom document options passed alongside are overridden or merged depending on the locale map specification.

Sources: packages/core/src/search/flexsearch.ts:31-39, packages/core/src/search/flexsearch.ts:96-100

Algolia search index synchronization

Synchronization Flow and Settings Configuration

Synchronization is executed by the sync function, which calls setIndexSettings followed by updateDocuments. The indexName defaults to 'document' when omitted in SyncOptions.

export async function sync(client: Algoliasearch, options: SyncOptions): Promise<void> {
  const { indexName = 'document', documents } = options;
  await setIndexSettings(client, indexName);
  await updateDocuments(client, indexName, documents);
}

Sources: packages/core/src/search/algolia.ts:49-53

The index settings are configured via client.setSettings with specific attributes for retrieval, faceting, searchability, and distinct filtering.

export async function setIndexSettings(client: Algoliasearch, indexName: string): Promise<void> {
  await client.setSettings({
    indexName,
    indexSettings: {
      attributeForDistinct: 'page_id',
      attributesToRetrieve: ['title', 'section', 'content', 'url', 'section_id', 'breadcrumbs'],
      searchableAttributes: ['title', 'section', 'content'],
      attributesToSnippet: [],
      attributesForFaceting: ['tag'],
    },
  });
}

Sources: packages/core/src/search/algolia.ts:55-66

Setting FieldTarget Value / TypePurpose
attributeForDistinct'page_id'Groups search results by page identifier using distinct records.
attributesToRetrieve['title', 'section', 'content', 'url', 'section_id', 'breadcrumbs']Specifies the record attributes returned in search queries.
searchableAttributes['title', 'section', 'content']Defines attributes included in search ranking and matching.
attributesToSnippet[]Configures snippet generation for retrieved records.
attributesForFaceting['tag']Enables tag-based faceting and filtering on records.

Sources: packages/core/src/search/algolia.ts:58-64

Note

The sync function replaces all objects in the target index rather than performing incremental updates, ensuring stale records are purged during synchronization.

Sources: packages/core/src/search/algolia.ts:49-53, packages/core/src/search/algolia.ts:119-122

Record Batch Transformation

The toIndex function processes each DocumentRecord to generate an array of BaseIndex objects. If a page contains a description, an initial record is created with undefined for section and section_id.

function toIndex(page: DocumentRecord): BaseIndex[] {
  let id = 0;
  const indexes: BaseIndex[] = [];
  const scannedHeadings = new Set<string>();
 
  function createIndex(
    section: string | undefined,
    sectionId: string | undefined,
    content: string,
  ): BaseIndex {
    return {
      objectID: `${page._id}-${(id++).toString()}`,
      breadcrumbs: page.breadcrumbs,
      title: page.title,
      url: page.url,
      page_id: page._id,
      tag: page.tag,
      section,
      section_id: sectionId,
      content,
      ...page.extra_data,
    };
  }
 
  if (page.description) indexes.push(createIndex(undefined, undefined, page.description));

Sources: packages/core/src/search/algolia.ts:68-92

For each content item in page.structured, matching headings are resolved. When a heading is encountered for the first time, a dedicated heading index record is created and added before the content record.

const { headings, contents } = page.structured;
 
for (const p of contents) {
  const heading = p.heading ? headings.find((h) => p.heading === h.id) : null;
 
  const index = createIndex(heading?.content, heading?.id, p.content);
 
  if (heading && !scannedHeadings.has(heading.id)) {
    scannedHeadings.add(heading.id);
 
    indexes.push(createIndex(heading.content, heading.id, heading.content));
  }
 
  indexes.push(index);
}

Sources: packages/core/src/search/algolia.ts:93-107

 
  return indexes;
}

Sources: packages/core/src/search/algolia.ts:108-110

Warning

Object identifiers are constructed by appending an incrementing index counter to page._id separated by a hyphen (${page._id}-${(id++).toString()}). Collisions will occur if multiple documents share the same _id.

Sources: packages/core/src/search/algolia.ts:78-90

Orama Cloud integration and export

Structured Document Export and Index Transformation

The exportSearchIndexes function extracts pages from the source collection and maps each entry into an OramaDocument containing its unique identifier, URL, title, description, and structured data.

export async function exportSearchIndexes() {
  return source.getPages().map((page) => {
    return {
      id: page.url,
      structured: page.data.structuredData,
      url: page.url,
      title: page.data.title,
      description: page.data.description,
    } satisfies OramaDocument;
  });
}

Sources: packages/create-app/template/+orama-cloud/@app/lib/export-static-indexes.ts:4-14

The toIndex function converts each OramaDocument into an array of OramaIndex records by iterating over structured contents and matching headings. If a page description exists, an initial record is created with undefined section identifiers.

function toIndex(page: OramaDocument): OramaIndex[] {
  let id = 0;
  const indexes: OramaIndex[] = [];
  const scannedHeadings = new Set<string>();
 
  function createIndex(
    section: string | undefined,
    sectionId: string | undefined,
    content: string,
  ): OramaIndex {
    return {
      id: `${page.id}-${(id++).toString()}`,
      title: page.title,
      url: page.url,
      page_id: page.id,
      tag: page.tag,
      section,
      section_id: sectionId,
      content,
      breadcrumbs: page.breadcrumbs,
      ...page.extra_data,
    };
  }
 
  if (page.description) indexes.push(createIndex(undefined, undefined, page.description));
 

Sources: packages/core/src/search/orama-cloud.ts:118-143

page.structured.contents.forEach((p) => {
  const heading = p.heading ? page.structured.headings.find((h) => p.heading === h.id) : null;
 
  const index = createIndex(heading?.content, heading?.id, p.content);
 
  if (heading && !scannedHeadings.has(heading.id)) {
    scannedHeadings.add(heading.id);
 
    indexes.push(createIndex(heading.content, heading.id, heading.content));
  }
 
  indexes.push(index);
});
 
return indexes;

Sources: packages/core/src/search/orama-cloud.ts:144-158

Warning

Unique index identifiers are generated by combining the document ID with an incrementing counter separated by a hyphen (${'{'}page.id{'}'}-${'{'}(id++).toString(){'}'}).

Sources: packages/core/src/search/orama-cloud.ts:123-140

Remote Synchronization and Framework Endpoints

The sync function opens a transaction on the specified Orama Cloud index, inserts the flattened document records, and commits the transaction if autoDeploy is enabled.

export async function sync(orama: OramaCloud, options: SyncOptions): Promise<void> {
  const { autoDeploy = true } = options;
  const index = orama.index.set(options.index);
  await index.transaction.open();
 
  await index.transaction.insertDocuments(
    options.documents.flatMap(toIndex) as unknown as AnyObject[],
  );
 
  if (autoDeploy) await index.transaction.commit();
}

Sources: packages/core/src/search/orama-cloud.ts:89-99

Internationalized synchronization is handled by syncI18n, which maps document locales to corresponding index identifiers and executes concurrent synchronization tasks for each locale.

export async function syncI18n(orama: OramaCloud, options: I18nSyncOptions): Promise<void> {
  const { autoDeploy = true, indexes } = options;
 
  const tasks = options.documents.map(async (document) => {
    const index = orama.index.set(indexes[document.locale]);
    await index.transaction.open();
 
    await index.transaction.insertDocuments(
      document.items.flatMap(toIndex) as unknown as AnyObject[],
    );
 
    if (autoDeploy) await index.transaction.commit();
  });
 
  await Promise.all(tasks);
}

Sources: packages/core/src/search/orama-cloud.ts:101-116

Framework routing plugins configure static JSON export endpoints across different application routers.

Framework TargetRoute PathImplementation Handler
Next.js App Router/static.jsonResponse.json(await exportSearchIndexes())
React Router / SPAstatic.jsonloader returning Response.json(await exportSearchIndexes())
Tanstack Start/static.jsonServer file route with GET handler returning Response.json(await exportSearchIndexes())
Waku/static.jsonGET handler with static configuration

Sources: packages/create-app/src/plugins/orama-cloud.ts:37-65, packages/create-app/src/plugins/orama-cloud.ts:109-140

Mixedbread vector and semantic indexing

Semantic Chunk Transformation and Endpoint Configuration

The createMixedbreadSearchAPI factory constructs a search endpoint that queries Mixedbread vector stores using @mixedbread/sdk clients. Options allow configuring client instances, store identifiers, top-k limits, and semantic options like re-ranking and query rewriting.

export interface MixedbreadSearchOptions {
  /**
   * The Mixedbread SDK client instance
   */
  client: Mixedbread;
 
  /**
   * The identifier of the store to search in
   */
  storeIdentifier: string;
 
  /**
   * Maximum number of results to return
   *
   * @defaultValue 10
   */
  topK?: number;
 
  /**
   * Re-rank search results for improved relevance (this adds latency to the search)
   */
  rerank?: boolean;
 
  /**
   * Rewrite the query for better search results (this adds latency to the search)
   */
  rewriteQuery?: boolean;
 
  /**
   * Minimum score threshold for results
   */
  scoreThreshold?: number;
 
  /**
   * Custom transform function for search results
   */
  transform?: (results: StoreSearchResult[], query: string) => SortedResult[];
}

Sources: packages/core/src/search/mixedbread.ts:20-57

export function createMixedbreadSearchAPI(options: MixedbreadSearchOptions): SearchAPI {
  const {
    client,
    storeIdentifier,
    topK = 10,
    rerank,
    rewriteQuery,
    scoreThreshold,
    transform,
  } = options;
 

Sources: packages/core/src/search/mixedbread.ts:113-123

Caution

Exporting search indexes is explicitly unsupported for Mixedbread endpoints; attempting to invoke export() throws an error instructing administrators to use the Mixedbread dashboard for store management.

Sources: packages/core/src/search/mixedbread.ts:167-172

Result Formatting and Filtering

The default transformation engine processes raw store search results by extracting document metadata such as URLs and titles, producing structured page and heading entries. Headings are cleaned using remove-markdown and converted into URL anchor links via github-slugger.

function defaultTransform(results: StoreSearchResult[]): SortedResult[] {
  return results.flatMap((item) => {
    const metadata = item.generated_metadata;
 
    const url = metadata.url || '#';
    const title = metadata.title || 'Untitled';
 
    const chunkResults: SortedResult[] = [
      {
        id: `${item.file_id}-${item.chunk_index}-page`,
        type: 'page',
        content: title,
        url,
      },
    ];
 
    const headingTitle = item.type === 'text' && item.text ? extractHeadingTitle(item.text) : '';
 
    if (headingTitle) {
      slugger.reset();
 
      chunkResults.push({
        id: `${item.file_id}-${item.chunk_index}-heading`,
        type: 'heading',
        content: headingTitle,
        url: `${url}#${slugger.slug(headingTitle)}`,
      });
    }
 
    return chunkResults;
  });
}

Sources: packages/core/src/search/mixedbread.ts:80-111

Search requests support optional tag filters applied directly to generated metadata fields within store queries.

const { tag, limit } = searchOptions;
let filters: StoreSearchParams['filters'] | undefined;
if (Array.isArray(tag) && tag.length > 0) {
  filters = {
    key: 'generated_metadata.tag',
    operator: 'in',
    value: tag,
  };
} else if (typeof tag === 'string') {
  filters = {
    key: 'generated_metadata.tag',
    operator: 'eq',
    value: tag,
  };
}

Sources: packages/core/src/search/mixedbread.ts:130-144

Was this page helpful?