Skip to content

Commit 7bcede2

Browse files
authored
Merge pull request #841 from constructive-io/devin/1773737091-trgm-scope-fix
feat: scope trgm operators to tables with intentional search (Option C)
2 parents 60f7eca + f47c0c5 commit 7bcede2

4 files changed

Lines changed: 69 additions & 93 deletions

File tree

graphile/graphile-search/src/codecs/operator-factories.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,14 +49,20 @@ export function createMatchesOperatorFactory(
4949
* Creates the `similarTo` and `wordSimilarTo` filter operator factories
5050
* for pg_trgm fuzzy text matching. Declared here so they're registered
5151
* via the declarative `connectionFilterOperatorFactories` API.
52+
*
53+
* These operators target 'StringTrgm' (resolved to 'StringTrgmFilter'),
54+
* NOT the global 'String' type. The unified search plugin registers
55+
* 'StringTrgmFilter' and selectively assigns it to string columns on
56+
* tables that qualify for trgm (via intentional search or @trgmSearch tag).
57+
* This prevents trgm operators from appearing on every string field.
5258
*/
5359
export function createTrgmOperatorFactories(): ConnectionFilterOperatorFactory {
5460
return (build) => {
5561
const { sql } = build;
5662

5763
return [
5864
{
59-
typeNames: 'String',
65+
typeNames: 'StringTrgm',
6066
operatorName: 'similarTo',
6167
spec: {
6268
description:
@@ -81,7 +87,7 @@ export function createTrgmOperatorFactories(): ConnectionFilterOperatorFactory {
8187
},
8288
},
8389
{
84-
typeNames: 'String',
90+
typeNames: 'StringTrgm',
8591
operatorName: 'wordSimilarTo',
8692
spec: {
8793
description:

graphile/graphile-search/src/plugin.ts

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -122,10 +122,18 @@ export function createUnifiedSearchPlugin(
122122
}
123123
}
124124

125-
// Phase 2: Only run supplementary adapters if at least one primary
126-
// adapter with isIntentionalSearch found columns on this codec.
125+
// Phase 2: Run supplementary adapters if intentional search exists
126+
// OR if the table/column has a @trgmSearch smart tag.
127127
// pgvector (isIntentionalSearch: false) alone won't trigger trgm.
128-
if (hasIntentionalSearch) {
128+
const hasTrgmSearchTag =
129+
// Table-level tag
130+
(codec.extensions as any)?.tags?.trgmSearch ||
131+
// Column-level tag
132+
(codec.attributes && Object.values(codec.attributes as Record<string, any>).some(
133+
(attr: any) => attr?.extensions?.tags?.trgmSearch
134+
));
135+
136+
if (hasIntentionalSearch || hasTrgmSearchTag) {
129137
for (const adapter of supplementaryAdapters) {
130138
const columns = adapter.detectColumns(codec, build);
131139
if (columns.length > 0) {
@@ -149,6 +157,8 @@ export function createUnifiedSearchPlugin(
149157
'PgConnectionArgFilterAttributesPlugin',
150158
'PgConnectionArgFilterOperatorsPlugin',
151159
'AddConnectionFilterOperatorPlugin',
160+
'ConnectionFilterTypesPlugin',
161+
'ConnectionFilterCustomOperatorsPlugin',
152162
// Allow individual codec plugins to load first (e.g. Bm25CodecPlugin)
153163
'Bm25CodecPlugin',
154164
'VectorCodecPlugin',
@@ -229,6 +239,34 @@ export function createUnifiedSearchPlugin(
229239
for (const adapter of adapters) {
230240
adapter.registerTypes(build);
231241
}
242+
243+
// Register StringTrgmFilter — a variant of StringFilter that includes
244+
// trgm operators (similarTo, wordSimilarTo). Only string columns on
245+
// tables that qualify for trgm will use this type instead of StringFilter.
246+
const hasTrgmAdapter = adapters.some((a) => a.name === 'trgm');
247+
if (hasTrgmAdapter) {
248+
const DPTYPES = (build as any).dataplanPg?.TYPES;
249+
const textCodec = DPTYPES?.text ?? TYPES.text;
250+
build.registerInputObjectType(
251+
'StringTrgmFilter',
252+
{
253+
pgConnectionFilterOperators: {
254+
isList: false,
255+
pgCodecs: [textCodec],
256+
inputTypeName: 'String',
257+
rangeElementInputTypeName: null,
258+
domainBaseTypeName: null,
259+
},
260+
},
261+
() => ({
262+
description:
263+
'A filter to be used against String fields with pg_trgm support. ' +
264+
'All fields are combined with a logical \u2018and.\u2019',
265+
}),
266+
'UnifiedSearchPlugin (StringTrgmFilter)'
267+
);
268+
}
269+
232270
return _;
233271
},
234272

@@ -610,6 +648,26 @@ export function createUnifiedSearchPlugin(
610648

611649
let newFields = fields;
612650

651+
// ── StringFilter → StringTrgmFilter type swapping ──
652+
// For tables that qualify for trgm, swap the type of string attribute
653+
// filter fields so they get similarTo/wordSimilarTo operators.
654+
const hasTrgm = adapterColumns.some((ac) => ac.adapter.name === 'trgm');
655+
if (hasTrgm) {
656+
const StringTrgmFilterType = build.getTypeByName('StringTrgmFilter');
657+
const StringFilterType = build.getTypeByName('StringFilter');
658+
if (StringTrgmFilterType && StringFilterType) {
659+
const swapped: Record<string, any> = {};
660+
for (const [key, field] of Object.entries(newFields)) {
661+
if (field && typeof field === 'object' && (field as any).type === StringFilterType) {
662+
swapped[key] = Object.assign({}, field, { type: StringTrgmFilterType });
663+
} else {
664+
swapped[key] = field;
665+
}
666+
}
667+
newFields = swapped;
668+
}
669+
}
670+
613671
for (const { adapter, columns } of adapterColumns) {
614672
for (const column of columns) {
615673
const fieldName = inflection.camelCase(

graphql/server-test/__tests__/__snapshots__/schema-snapshot.test.ts.snap

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -666,29 +666,6 @@ input StringFilter {
666666
667667
"""Greater than or equal to the specified value (case-insensitive)."""
668668
greaterThanOrEqualToInsensitive: String
669-
670-
"""
671-
Fuzzy matches using pg_trgm trigram similarity. Tolerates typos and misspellings.
672-
"""
673-
similarTo: TrgmSearchInput
674-
675-
"""
676-
Fuzzy matches using pg_trgm word_similarity. Finds the best matching substring within the column value.
677-
"""
678-
wordSimilarTo: TrgmSearchInput
679-
}
680-
681-
"""
682-
Input for pg_trgm fuzzy text matching. Provide a search value and optional similarity threshold.
683-
"""
684-
input TrgmSearchInput {
685-
"""The text to fuzzy-match against. Typos and misspellings are tolerated."""
686-
value: String!
687-
688-
"""
689-
Minimum similarity threshold (0.0 to 1.0). Higher = stricter matching. Default is 0.3.
690-
"""
691-
threshold: Float
692669
}
693670
694671
"""

graphql/test/__tests__/__snapshots__/graphile-test.test.ts.snap

Lines changed: 0 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1121,77 +1121,12 @@ based pagination. May not be used with \`last\`.",
11211121
"ofType": null,
11221122
},
11231123
},
1124-
{
1125-
"defaultValue": null,
1126-
"description": "Fuzzy matches using pg_trgm trigram similarity. Tolerates typos and misspellings.",
1127-
"name": "similarTo",
1128-
"type": {
1129-
"kind": "INPUT_OBJECT",
1130-
"name": "TrgmSearchInput",
1131-
"ofType": null,
1132-
},
1133-
},
1134-
{
1135-
"defaultValue": null,
1136-
"description": "Fuzzy matches using pg_trgm word_similarity. Finds the best matching substring within the column value.",
1137-
"name": "wordSimilarTo",
1138-
"type": {
1139-
"kind": "INPUT_OBJECT",
1140-
"name": "TrgmSearchInput",
1141-
"ofType": null,
1142-
},
1143-
},
11441124
],
11451125
"interfaces": null,
11461126
"kind": "INPUT_OBJECT",
11471127
"name": "StringFilter",
11481128
"possibleTypes": null,
11491129
},
1150-
{
1151-
"description": "Input for pg_trgm fuzzy text matching. Provide a search value and optional similarity threshold.",
1152-
"enumValues": null,
1153-
"fields": null,
1154-
"inputFields": [
1155-
{
1156-
"defaultValue": null,
1157-
"description": "The text to fuzzy-match against. Typos and misspellings are tolerated.",
1158-
"name": "value",
1159-
"type": {
1160-
"kind": "NON_NULL",
1161-
"name": null,
1162-
"ofType": {
1163-
"kind": "SCALAR",
1164-
"name": "String",
1165-
"ofType": null,
1166-
},
1167-
},
1168-
},
1169-
{
1170-
"defaultValue": null,
1171-
"description": "Minimum similarity threshold (0.0 to 1.0). Higher = stricter matching. Default is 0.3.",
1172-
"name": "threshold",
1173-
"type": {
1174-
"kind": "SCALAR",
1175-
"name": "Float",
1176-
"ofType": null,
1177-
},
1178-
},
1179-
],
1180-
"interfaces": null,
1181-
"kind": "INPUT_OBJECT",
1182-
"name": "TrgmSearchInput",
1183-
"possibleTypes": null,
1184-
},
1185-
{
1186-
"description": "The \`Float\` scalar type represents signed double-precision fractional values as specified by [IEEE 754](https://en.wikipedia.org/wiki/IEEE_floating_point).",
1187-
"enumValues": null,
1188-
"fields": null,
1189-
"inputFields": null,
1190-
"interfaces": null,
1191-
"kind": "SCALAR",
1192-
"name": "Float",
1193-
"possibleTypes": null,
1194-
},
11951130
{
11961131
"description": "Methods to use when ordering \`User\`.",
11971132
"enumValues": [

0 commit comments

Comments
 (0)