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.
What gets searched
Section titled “What gets searched”SearchService merges two sources:
- 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.
- 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.
Making an entity searchable
Section titled “Making an entity searchable”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.UrlTemplatebuilds the link the result navigates to;Filterscopes 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.
Providers
Section titled “Providers”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_scopeand 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.
How documents get there
Section titled “How documents get there”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=5Each 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.