Skip to content

Search

Genie ships a global search that spans two things at once: the user’s navigation (jump to a page) and the indexable entities (find a record). Results come back grouped and ranked by object type. All scoping is resolved server-side from the authenticated session — a user only ever sees their own tenant’s rows and the pages they may open.

SearchService merges two sources:

  1. Navigation — the current user’s permission-filtered navbar (see Navigation); each navigable leaf is scored against the term so a page match ranks slightly above data matches on a tie.
  2. Searchable entities — every entity that declares an enabled <Search> block, queried through the active provider.

A term shorter than 2 characters returns nothing. Results are limited overall and per group, with a Truncated flag when trimmed.

Add a <Search> block to the entity model and mark the columns to match with Searchable="true". See Entities → Search for the full contract.

<Entity Name="Customer" SchemaName="Sales" PluralName="Customers">
<Fields>
<String Name="Name" Searchable="true" />
<String Name="Email" Searchable="true" />
</Fields>
<Search UrlTemplate="/object/Customer?id={Id}"
Filter="IsActive = 1 AND CompanyId = @SessionCompanyId">
<Context Field="Name" />
</Search>
</Entity>
  • Searchable="true" columns are the ones matched against the term (falling back to the <Context> fields, then all fields, if none are marked).
  • <Context> fields are returned with each hit so the UI can render a meaningful result row.
  • UrlTemplate builds the link the result navigates to; Filter scopes which rows are eligible and can use session parameters (@SessionCompanyId, @SessionUserId, …).
  • ObjectType (optional) labels the result group; it defaults to the entity name.

SearchableEntityRegistry reads these declarations from the entity store at query time and projects them into ready-to-query descriptors.

Search resolves through one provider, chosen per deployment (ISearchProvider):

  • SqlSearchProvider (default) — generates a dialect-appropriate case-insensitive substring query (LIKE/ILIKE) over the searchable columns at runtime; the term is always a bound parameter. Results are scoped to the caller’s company via [Identity].fn_user_company_scope and exclude soft-deleted rows. All datasets are batched into a single round-trip.
  • MeilisearchSearchProvider — used when a Meilisearch warehousing target is configured; the SQL provider is the fallback otherwise.

Meilisearch is configured as a warehousing target

Section titled “Meilisearch is configured as a warehousing target”

There is no Genie:Meilisearch section. Meilisearch is one entry in Genie:Warehousing:Targets, and that single entry configures both halves of the integration: the write path that indexes documents and the read path that queries them.

"Warehousing": {
"Enabled": true,
"Targets": {
"search": { "Provider": "Meilisearch", "Url": "http://localhost:7700", "Key": "masterKey", "Entities": [ "*" ] }
}
}

An index nothing replicates into is empty, so splitting the two across separate sections would only create a way for them to disagree. With no Meilisearch target, search falls back to the SQL provider.

Declaring <Search> gives the entity a NOT NULL IsSyncable flag plus an insert/update trigger (trg_{table}_Syncable) that sets it, marking the row pending. The warehousing sync worker periodically pushes flagged rows into the entity’s index — each document carries CompanyId for tenant isolation — and clears the flag once every configured target has accepted the batch, not just this one.

That flag write is wrapped in the framework-suppression sentinel, so it never trips the concurrency or audit triggers — the audit change-logging trigger also explicitly ignores IsSyncable, so the triggers coexist without producing phantom changes or spurious RowVersion bumps.

Soft-deleted records are removed from the index rather than left behind. See Warehousing → Deletes for the hard-delete caveat.

Method Route Returns
GET /api/v1/genie/search?term=&limit=&perGroupLimit= SuccessDataResult<SearchResponse> — ranked groups
GET /api/v1/genie/search?term=acme&perGroupLimit=5

Each SearchResponse carries the Term, a Truncated flag, and Groups; each group has an ObjectType, its Items (with Url, RecordKey, Context, Score) and a TotalMatches count.