Full-Text Search
Benevia uses PostgreSQL full-text search to power the OData $search query parameter. Mark properties with [Searchable] to include them in search indexes.
[Searchable]
Marks a property for full-text search with weighted ranking.
[Searchable(rank, Dictionary = "dictionary")]
Rank (Weight)
PostgreSQL supports 4 weight classes. Higher-ranked properties score higher in search results:
| Rank | PostgreSQL Weight | Use For |
|---|---|---|
| 1 | A (highest) | Primary identifiers — IDs, SKUs, codes |
| 2 | B | Important text — names, descriptions |
| 3 | C | Secondary text |
| 4 | D (lowest) | Minor text (default if no rank specified) |
Dictionary
Controls how text is tokenized and matched:
| Dictionary | Behavior | Use For |
|---|---|---|
"english" |
Stemming, stop words removed. "running" matches "run" | Natural language text — descriptions, notes |
"simple" |
Exact token matching, no stemming | Identifiers — IDs, SKUs, codes, names |
Default is "english" if not specified.
Examples
// SKU: highest priority, exact matching
[Searchable(1, Dictionary = "simple")]
[Property<DataTypes.IdText>("SKU")]
public partial string Sku { get; set; }
// Description: high priority, natural language matching
[Searchable(2)]
[Property<DataTypes.MultilineText>("Description")]
public partial string SalesDescription { get; set; }
// Customer ID: highest priority, exact matching
[Searchable(1, Dictionary = "simple")]
[Property<DataTypes.IdText>("Id")]
public partial string Id { get; set; }
// Customer name: also high priority, exact matching for names
[Searchable(2, Dictionary = "simple")]
[Property<DataTypes.ProperNoun>("Full name")]
public partial string FullName { get; }
[SearchableNavigation]
Marks a reference property so the target entity's [Searchable] properties are included in this entity's search. Searching for a sales order will also match on the customer's name.
[SearchableNavigation]
[ReferenceProperty("Customer", DeleteAction.Restrict)]
public virtual partial Customer? SellToCustomer { get; set; }
When searching through a navigation, the rank is reduced (rank value increases by 2, capped at 4). This means a customer name at rank 2 becomes rank 4 when searching from the sales order.
MaxDepth
Control how many levels of related entities are traversed:
[SearchableNavigation(MaxDepth = 0)] // Only direct properties, no further navigation
[ReferenceProperty("Contact", DeleteAction.Restrict)]
public virtual partial Contact? PrimaryContact { get; set; }
[SearchableOppositeSideCollection]
Makes the generated opposite-side collection searchable. Apply this alongside [OppositeSideCollection]:
[ReferenceProperty("Customer", DeleteAction.Restrict)]
[OppositeSideCollection("Contacts", "Contacts", CollectionLoadMode.LoadAll)]
[SearchableOppositeSideCollection]
public virtual partial Customer? Customer { get; set; }
This enables searching the parent entity through its children. For collections, the search generates .Any() predicates to match across collection items.
How It Works
At the API level, clients use OData $search:
GET /odata/Product?$search=WIDGET
GET /odata/SalesOrder?$search=ACME
The system:
- Collects all
[Searchable]properties on the entity - Follows
[SearchableNavigation]references to include related entities' searchable properties - Builds a PostgreSQL full-text search query with weighted ranking
- Returns results ordered by relevance score