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:

  1. Collects all [Searchable] properties on the entity
  2. Follows [SearchableNavigation] references to include related entities' searchable properties
  3. Builds a PostgreSQL full-text search query with weighted ranking
  4. Returns results ordered by relevance score