Story-3169 · functions lane
Find a matter by the name of a party in it
Counsel identifies a matter by who is in it — “the one where the freight company is fighting that consignee.” Until now that phrase resolved to nothing. The matter search vector covered title, case reference, dispute text, outcome sought and frame text; no party-bearing table appeared in any search vector or trigram index. The client was picked from a dropdown at creation and every participant was recorded, and none of it was findable.
Free-text matter search now reaches the names of the parties: the owning client and every recorded participant. Misspellings are tolerated.
What counsel can now type
GET /app/matters?q=Rustwater Freight Pvt Ltd → the matter, matched on "client"
GET /app/matters?q=Prithvi Banerjee → the matter, matched on "party"
GET /app/matters?q=Prithvi Banerji → the same matter (misspelt)
match_reason stays honest: a query that lands on a participant
reports party, one that lands on the owning client reports
client, and only a hit in the matter’s own text still reports
text. The label is produced by the same SQL arms as the filter,
never invented downstream.
The decision: no denormalised copy
business.matters.search_vector is a generated column, which is
exactly why it has never drifted — every one of its sources is on the matters
row. Party names are not: they are rows in
business.matter_participants and business.clients,
and a generated column cannot reach another table.
The two candidate designs were a denormalised party_text column
folded into the generated vector, or a searchable projection queried
alongside it. This story takes the second shape in its thinnest form:
matter search queries the party rows live, through
correlated, firm-scoped EXISTS arms, and stores no second copy
of any name.
The reason is the failure this feature would otherwise ship with. A
denormalised column has to be chased by every rename and every deletion; the
day one write path forgets, search starts answering with a name that no
longer exists on the matter, silently and with no error anywhere. Reading
the live row makes that state unrepresentable rather than merely tested for.
The concrete costs are paid honestly and are small: the party arm is a second
query path (it already existed — the party_name and
involves filters used it), and the rank merge below is the
ranking decision the projection shape owes.
The rank merge
ts_rank scores only the matter’s own text, so a match found
purely by party name scores zero and would sort below every weak text hit —
backwards, because a party name identifies a matter while dispute
text only describes its topic. Search therefore orders by one
boolean tier — name match first — then the existing ts_rank,
then the existing recency tiebreak. Listings without a query term keep their
ordering byte-for-byte.
Fuzziness, and what was deliberately left alone
Each name column is matched two ways: the escaped leading-wildcard
ILIKE, so a fragment of a name always matches, and pg_trgm’s
% similarity operator, so a misspelling still finds the matter.
Both are served by the gin_trgm_ops indexes added in migration
0234 — matter_participants.name had no index at all for the
pre-existing party filter, and the client name columns carried only plain
btrees.
The 'simple' text-search configuration is untouched. It is
deliberate: an English stemmer mangles Hindi and Indian names, and pg_trgm is
what buys fuzziness here. No matter is reindexed and no table is rewritten by
this change.
Confidentiality
Party names are client-confidential. Both arms carry an explicit
firm_id predicate in addition to RLS, so the tenancy floor holds
even in a session where RLS contributes nothing. The scope is labels only —
participant and client names. Document contents are not searched here and
remain the Context Engine’s job.
Proof
| Scenario | Asserts |
|---|---|
| MD-20 | The matter is found by its client’s name and by its opposing party’s name, neither of which is in its title. |
| MD-21 | A misspelt party name still finds it — the test first asserts the misspelling is not a substring of the real name, so only similarity can match it. |
| MD-22 | Renaming the party makes the old name return nothing and the new name return the matter; removing the party makes the new name return nothing. |
| MD-23 | Another firm’s party name never surfaces in free-text search, including with RLS disabled in the query. |
MD-20, MD-21 and MD-22 were observed red against the pre-change service. MD-23 was observed red against a planted removal of the firm predicate. Every name in the suite is an invented pseudonym; no live-matter person appears in this repository.