Search matching
Overview
Section titled “Overview”Every list page has a search box that does more than a simple “find rows whose name contains this string” — it expands abbreviations, treats numbers and their spelled-out forms as equivalent, supports phrase quoting, and recognises a handful of explicit operators for filtering by tag, note, field, or boolean. This page explains exactly what gets matched and how.
Basic matching
Section titled “Basic matching”When you type a single word, the system:
- Tokenises it (splits on whitespace, lowercases).
- Expands it through the synonym table and the number-to-word table.
- Appends a wildcard so partial words match (
Boland*matchesBoland,Boland's,Bolands). - Builds a SQL Server CONTAINS query that ORs the expansions together.
Multi-word queries get ANDed at the group level (every word must match somewhere), but within each word’s group the synonyms / number forms are ORed (any one of them is good enough).
So typing Acme Ltd matches “Acme Limited”, “Acme Ltd.”, and anything else where both Acme and Ltd-or-Limited appear in the indexed text.
Synonyms
Section titled “Synonyms”The synonym table is bidirectional — every entry is a two-way mapping. Typing the short form matches the long form, and vice versa. A representative sample (the full set is around 60 pairs):
| If you type | The system also matches |
|---|---|
st | street |
street | st |
ave, av | avenue |
avenue | ave, av |
blvd | boulevard |
rd | road |
sq | square |
ln | lane |
pl | place |
pk | park |
ct | court |
ind | industrial |
ie | industrial estate |
te | trading estate |
bldg | building |
hse | house |
bpk | business park |
ctre | centre |
hq | headquarters |
ltd | limited |
grp | group |
off | office |
bus | business |
terr | terrace |
The mappings cover three broad categories — street types (st/street, ave/avenue, rd/road, etc.), building types (hse/house, ctre/centre, bldg/building), and company suffixes (ltd/limited, grp/group). They’re hand-maintained — additions go through engineering.
For the complete list at any point in time, see src/services/api/app/app.api/Services/Search/SearchTokenizer.cs.
Number-word equivalence
Section titled “Number-word equivalence”Numbers and their spelled-out forms match interchangeably for digits 0-19, decades from twenty to ninety, plus hundred and thousand.
Examples:
| Typing | Also matches |
|---|---|
3 | three |
three | 3 |
10 | ten |
100 | hundred |
1000 | thousand |
This is useful for things like “3 Cromwell Place” matching “Three Cromwell Place” without forcing one form or the other on data entry.
The list covers digits 0-9, tens 10-19, the tens family (20, 30, 40… 90), 100, and 1000. Compound numbers (e.g. “thirty-two”) aren’t expanded — those are matched character-by-character.
Wildcard suffix
Section titled “Wildcard suffix”Every word in your search gets a * suffix appended so partial words still match. Typing Boland matches Boland’s, Bolands, Boland Industrial Estate, etc. The wildcard runs against the end of words, not the start — searching land will not match Roland.
The suffix is applied after synonym expansion, so st* also gets the street* form added.
Phrase matching
Section titled “Phrase matching”Wrap a phrase in double quotes to require the words appear together in that order:
"cromwell road"— matches records containing the phrase “Cromwell Road” but not records where “Cromwell” and “Road” appear in different places.
Phrases don’t get wildcard-expanded or synonym-expanded — they’re matched literally (apart from the system normalising case and stripping extra whitespace).
Operators
Section titled “Operators”The search box supports five explicit operators in addition to free-text matching:
#tag — filter by tag
Section titled “#tag — filter by tag”Prefix a word with # to filter to records carrying that tag:
#confidential— show only records tagged confidential.
Tag names are case-insensitive. Multiple # tokens AND together: #confidential #high-priority requires both tags.
@note — filter to records with notes containing a word
Section titled “@note — filter to records with notes containing a word”Prefix a word with @ to filter to records whose notes contain that text:
@deadline— show only records that have at least one note mentioning deadline.
Note that @ matches against the notes, not the parent record’s free text. A record whose name is “Deadline Holdings” won’t match @deadline unless one of its notes also mentions it.
field:value — filter by a specific field
Section titled “field:value — filter by a specific field”Use field:value to filter on a named field rather than the general index:
companynumber:12345— match records whose CompanyNumber is exactly 12345 (or starts with it, given the wildcard suffix).
The available fields differ by entity. On Addresses you can use postalcode:, country:, locality:. On Companies, companynumber:, url:. Field names are matched case-insensitively. If you use a field name that doesn’t exist on the entity you’re searching, the filter is ignored (rather than blowing up the whole search).
+bool and -bool — boolean field filtering
Section titled “+bool and -bool — boolean field filtering”Prefix a boolean field name with + to require it true, or - to require it false:
+verified— only verified records.-retired— exclude closed records.
The bool name maps to the entity’s flag field — +verified → IsVerified = true, -retired → IsRetired = false (which is the default). Combine freely with other operators.
Combining operators
Section titled “Combining operators”You can mix any of the above in a single query. The general pattern:
general words "phrase" #tag1 #tag2 field:value +bool1 -bool2 @note
Order doesn’t matter — operators are recognised by their prefix character, not their position. All operators AND together; within a single field:value you can have OR semantics by repeating: country:gb country:us means “country is GB or US”.
What’s not searchable
Section titled “What’s not searchable”A few things to know about the limits of the search:
- The
@noteoperator searches against notes for entities of the current list. Cross-entity note search (find any record whose notes mention X) isn’t a single-query feature. - Tag content lives in a separate table; the
#tagoperator hits that table directly, not the general text index. A tag named “confidential” will only be found via#confidential, not by typingconfidentialinto a free-text search. - The synonym list is hand-maintained — only entries in the table count as synonyms. Typing incorp won’t match incorporated; engineering would need to add that pair.
- Excerpts from a record’s full free text aren’t returned with results — the search yields full records, not snippets. To see why a record matched, examine the record.
Where this lives
Section titled “Where this lives”- Tokeniser + synonym table + number-to-word table:
src/services/api/app/app.api/Services/Search/SearchTokenizer.cs - Query parser (extracts operators from your input):
src/services/api/app/app.api/Services/Search/SearchQueryParser.cs - CONTAINS pattern builder:
src/services/api/app/app.api/Services/Search/FullTextSearchHelper.cs - Per-entity search services:
AddressSearchService.cs,CompanySearchService.cs,SchemeSearchService.cs, etc., undersrc/services/api/app/app.api/Services/Search/ - Backing indexes: SQL Server full-text indexes on the
query.*Listviews (seesrc/data/app/query/Tables/)