Skip to content

Global Search

Integration

Services integrate with Global Search through the GlobalSearchClientModule.

A service should implement this integration when it owns entities that need to be searchable from the shared Global Search feature.

The integration model is based on document maps:

  • the source service chooses which entities participate in Global Search
  • a document map defines which fields are projected into the search document
  • the document map defines which event triggers reindexing for that entity
  • the source service registers that map in its module configuration through the client module API

Source service responsibilities

The source service is responsible for:

  • deciding which entities should be searchable
  • choosing which events should trigger reindexing
  • projecting a search-oriented document from its entity
  • including any related data needed to build that document

Global Search responsibilities

Global Search is responsible for:

  • interpreting mapped fields by convention
  • creating and evolving the underlying search index schema
  • managing internal search fields
  • applying search behavior uniformly across entity types
  • exposing search and autocomplete capabilities for consumers

Producer services only decide which content belongs in the indexed document.

What the service must provide

To integrate an entity with Global Search, the service must provide:

  1. The source entity
    The domain or read-model entity that will be indexed.

  2. The triggering event
    The event that indicates the searchable representation should be created or updated.

  3. A document map
    A class that defines:

  4. how to extract the event identifier used to locate the entity
  5. optionally, how to override the document identifier
  6. which related data must be eagerly loaded before building the document
  7. which fields are projected into the indexed document through Map(...)

  8. Module registration
    The module must register the document map through GlobalSearchClientModuleOptions.

Design guidance

The document map should project a search document, not a raw persistence model.

That means it should contain the fields that are useful for search scenarios, such as:

  • identifiers
  • names, codes, and descriptions
  • lightweight related data needed for matching or display
  • computed or flattened values from child collections when searching at the aggregate-root level

Avoid sending data that is:

  • large but not useful for discovery
  • sensitive and not intended for search
  • shaped only for persistence concerns

Mapping model

The mapping API is content-only.

A document map should declare which values are part of the search document, for example:

C#
1
2
3
4
5
6
builder
    .Map(x => x.Code)
    .Map(x => x.Description)
    .Map("customerName", x => x.Customer.Name)
    .Map("linesCount", x => x.Lines.Count)
    .Map("productCodes", x => x.Lines.Select(line => line.ProductCode));

The producer does not choose field roles such as SearchText, Filter, or Sortable. Those concerns are owned by Global Search conventions.

Well-known fields and conventions

Some fields are engine-owned and are added by convention.

id

The id field is always included automatically by the base SearchDocumentMap<TEntity, TEvent> implementation. By default, it is derived from entity.CorrelationId, although maps may override GetId(...) when needed.

sharingLevelId

If an entity implements IShareableEntity, Global Search adds sharingLevelId automatically through an internal convention.

C#
1
2
3
4
public interface IShareableEntity
{
    Guid SharingLevelId { get; set; }
}

This means:

  • producer maps should not map sharingLevelId manually
  • the field name is canonical and reserved
  • entities that implement IShareableEntity receive that field automatically in the indexed document

Integration example

The example below shows how a service can make WorkOrder searchable.

1. Source entity and event

C#
public class WorkOrder : ICorrelatedEntity, IShareableEntity
{
    public Guid CorrelationId { get; set; }

    public Guid SharingLevelId { get; set; }

    public string Code { get; set; } = string.Empty;

    public string Description { get; set; } = string.Empty;

    public Customer Customer { get; set; } = null!;

    public ICollection<OrderLine> Lines { get; set; } = new List<OrderLine>();
}

public class OrderLine : ICorrelatedEntity
{
    public Guid CorrelationId { get; set; }

    public string ProductCode { get; set; } = string.Empty;

    public decimal TotalPrice { get; set; }
}

public record WorkOrderUpdated
{
    public Guid CorrelationId { get; init; }
}

In this case:

  • WorkOrder is the entity that should appear in Global Search
  • WorkOrderUpdated is the event that triggers reindexing
  • because WorkOrder implements IShareableEntity, sharingLevelId is added automatically by convention

2. Document map

C#
public class WorkOrderDocumentMap : SearchDocumentMap<WorkOrder, WorkOrderUpdated>
{
    public override Guid GetEventIdField(WorkOrderUpdated @event)
    {
        return @event.CorrelationId;
    }

    public override IncludeSpecification<WorkOrder>[] UseIncludes()
    {
        return
        [
            new WorkOrderWithCustomerIncludeSpecification(),
            new WorkOrderWithLinesIncludeSpecification(),
        ];
    }

    protected override void Configure(SearchEntityDocumentBuilder<WorkOrder> builder)
    {
        builder
            .Map(x => x.Code)
            .Map(x => x.Description)
            .Map("customerName", x => x.Customer.Name)
            .Map("linesCount", x => x.Lines.Count)
            .Map("productCodes", x => x.Lines.Select(line => line.ProductCode).ToArray());
    }
}

This map defines the producer-side contract for WorkOrder:

  • GetEventIdField(...) extracts the entity identifier from the triggering event
  • UseIncludes() ensures related data is loaded before the document is built
  • Configure(...) declares which values are projected into the search document
  • id is added automatically by the base class
  • sharingLevelId is added automatically because WorkOrder implements IShareableEntity

3. Module registration

C#
public class WorkOrdersModule : SuiteModule
{
    public override void SetupModule(IModuleBuilder builder)
    {
        base.SetupModule(builder);

        // other module dependencies

        builder.DependsOn<GlobalSearchClientModule, GlobalSearchClientModuleOptions>(opts =>
        {
            opts.AddDocumentMap<WorkOrder, WorkOrderUpdated, WorkOrderDocumentMap>();
        });
    }
}

How to read this example

Using the previous example, the indexing flow is:

  1. A WorkOrder is updated in the owning service.
  2. The service publishes WorkOrderUpdated.
  3. The Global Search client integration handles that event.
  4. The configured WorkOrderDocumentMap is used to:
  5. locate the affected WorkOrder
  6. load the required related data
  7. build the indexed document from the configured mapped fields
  8. The resulting search document is sent to Global Search and upserted into the appropriate index.

This keeps the integration declarative: the service registers the map, and the client module uses that configuration to drive indexing.

Schema and index evolution

Global Search uses a stable alias derived from the entity well-known name and a versioned physical index name derived from the effective schema version.

This allows:

  • consumers to keep querying the same logical entity target
  • Global Search to create a new physical index when the schema changes
  • alias cutover to happen independently from the UI or API contract

The effective schema may change when:

  • the mapped fields in Configure(...) change
  • internal conventions add or remove well-known fields
  • Global Search indexing conventions evolve

When schema evolution requires rebuilding an index, Global Search should create the new physical index, reingest data, and switch the alias only after the new index is ready.

Practical notes

Keep documents search-oriented

Avoid treating the document map as a raw entity dump. A better document usually contains only the fields needed for:

  • matching
  • ranking
  • rendering the search result

If the search document depends on navigation properties or related entities, those dependencies should be declared through UseIncludes() so the map can build a complete and consistent document.

Flatten aggregate data when needed

When searching aggregate roots, it is often better to index one document per root and flatten child data into that document through computed mappings.

Examples include:

  • child descriptions
  • child product codes
  • line counts
  • totals
  • booleans derived from child state

Do not map well-known convention fields manually

Fields such as id and sharingLevelId are engine-owned. Producer maps should not attempt to define them explicitly.