Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions graphile/graphile-search/src/codecs/operator-factories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,20 @@ export function createMatchesOperatorFactory(
* Creates the `similarTo` and `wordSimilarTo` filter operator factories
* for pg_trgm fuzzy text matching. Declared here so they're registered
* via the declarative `connectionFilterOperatorFactories` API.
*
* These operators target 'StringTrgm' (resolved to 'StringTrgmFilter'),
* NOT the global 'String' type. The unified search plugin registers
* 'StringTrgmFilter' and selectively assigns it to string columns on
* tables that qualify for trgm (via intentional search or @trgmSearch tag).
* This prevents trgm operators from appearing on every string field.
*/
export function createTrgmOperatorFactories(): ConnectionFilterOperatorFactory {
return (build) => {
const { sql } = build;

return [
{
typeNames: 'String',
typeNames: 'StringTrgm',
operatorName: 'similarTo',
spec: {
description:
Expand All @@ -81,7 +87,7 @@ export function createTrgmOperatorFactories(): ConnectionFilterOperatorFactory {
},
},
{
typeNames: 'String',
typeNames: 'StringTrgm',
operatorName: 'wordSimilarTo',
spec: {
description:
Expand Down
64 changes: 61 additions & 3 deletions graphile/graphile-search/src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,10 +122,18 @@ export function createUnifiedSearchPlugin(
}
}

// Phase 2: Only run supplementary adapters if at least one primary
// adapter with isIntentionalSearch found columns on this codec.
// Phase 2: Run supplementary adapters if intentional search exists
// OR if the table/column has a @trgmSearch smart tag.
// pgvector (isIntentionalSearch: false) alone won't trigger trgm.
if (hasIntentionalSearch) {
const hasTrgmSearchTag =
// Table-level tag
(codec.extensions as any)?.tags?.trgmSearch ||
// Column-level tag
(codec.attributes && Object.values(codec.attributes as Record<string, any>).some(
(attr: any) => attr?.extensions?.tags?.trgmSearch
));

if (hasIntentionalSearch || hasTrgmSearchTag) {
for (const adapter of supplementaryAdapters) {
const columns = adapter.detectColumns(codec, build);
if (columns.length > 0) {
Expand All @@ -149,6 +157,8 @@ export function createUnifiedSearchPlugin(
'PgConnectionArgFilterAttributesPlugin',
'PgConnectionArgFilterOperatorsPlugin',
'AddConnectionFilterOperatorPlugin',
'ConnectionFilterTypesPlugin',
'ConnectionFilterCustomOperatorsPlugin',
// Allow individual codec plugins to load first (e.g. Bm25CodecPlugin)
'Bm25CodecPlugin',
'VectorCodecPlugin',
Expand Down Expand Up @@ -229,6 +239,34 @@ export function createUnifiedSearchPlugin(
for (const adapter of adapters) {
adapter.registerTypes(build);
}

// Register StringTrgmFilter — a variant of StringFilter that includes
// trgm operators (similarTo, wordSimilarTo). Only string columns on
// tables that qualify for trgm will use this type instead of StringFilter.
const hasTrgmAdapter = adapters.some((a) => a.name === 'trgm');
if (hasTrgmAdapter) {
const DPTYPES = (build as any).dataplanPg?.TYPES;
const textCodec = DPTYPES?.text ?? TYPES.text;
build.registerInputObjectType(
'StringTrgmFilter',
{
pgConnectionFilterOperators: {
isList: false,
pgCodecs: [textCodec],
inputTypeName: 'String',
rangeElementInputTypeName: null,
domainBaseTypeName: null,
},
},
() => ({
description:
'A filter to be used against String fields with pg_trgm support. ' +
'All fields are combined with a logical \u2018and.\u2019',
}),
'UnifiedSearchPlugin (StringTrgmFilter)'
);
}

return _;
},

Expand Down Expand Up @@ -610,6 +648,26 @@ export function createUnifiedSearchPlugin(

let newFields = fields;

// ── StringFilter → StringTrgmFilter type swapping ──
// For tables that qualify for trgm, swap the type of string attribute
// filter fields so they get similarTo/wordSimilarTo operators.
const hasTrgm = adapterColumns.some((ac) => ac.adapter.name === 'trgm');
if (hasTrgm) {
const StringTrgmFilterType = build.getTypeByName('StringTrgmFilter');
const StringFilterType = build.getTypeByName('StringFilter');
if (StringTrgmFilterType && StringFilterType) {
const swapped: Record<string, any> = {};
for (const [key, field] of Object.entries(newFields)) {
if (field && typeof field === 'object' && (field as any).type === StringFilterType) {
swapped[key] = Object.assign({}, field, { type: StringTrgmFilterType });
} else {
swapped[key] = field;
}
}
newFields = swapped;
}
}

for (const { adapter, columns } of adapterColumns) {
for (const column of columns) {
const fieldName = inflection.camelCase(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -666,29 +666,6 @@ input StringFilter {

"""Greater than or equal to the specified value (case-insensitive)."""
greaterThanOrEqualToInsensitive: String

"""
Fuzzy matches using pg_trgm trigram similarity. Tolerates typos and misspellings.
"""
similarTo: TrgmSearchInput

"""
Fuzzy matches using pg_trgm word_similarity. Finds the best matching substring within the column value.
"""
wordSimilarTo: TrgmSearchInput
}

"""
Input for pg_trgm fuzzy text matching. Provide a search value and optional similarity threshold.
"""
input TrgmSearchInput {
"""The text to fuzzy-match against. Typos and misspellings are tolerated."""
value: String!

"""
Minimum similarity threshold (0.0 to 1.0). Higher = stricter matching. Default is 0.3.
"""
threshold: Float
}

"""
Expand Down
65 changes: 0 additions & 65 deletions graphql/test/__tests__/__snapshots__/graphile-test.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -1121,77 +1121,12 @@ based pagination. May not be used with \`last\`.",
"ofType": null,
},
},
{
"defaultValue": null,
"description": "Fuzzy matches using pg_trgm trigram similarity. Tolerates typos and misspellings.",
"name": "similarTo",
"type": {
"kind": "INPUT_OBJECT",
"name": "TrgmSearchInput",
"ofType": null,
},
},
{
"defaultValue": null,
"description": "Fuzzy matches using pg_trgm word_similarity. Finds the best matching substring within the column value.",
"name": "wordSimilarTo",
"type": {
"kind": "INPUT_OBJECT",
"name": "TrgmSearchInput",
"ofType": null,
},
},
],
"interfaces": null,
"kind": "INPUT_OBJECT",
"name": "StringFilter",
"possibleTypes": null,
},
{
"description": "Input for pg_trgm fuzzy text matching. Provide a search value and optional similarity threshold.",
"enumValues": null,
"fields": null,
"inputFields": [
{
"defaultValue": null,
"description": "The text to fuzzy-match against. Typos and misspellings are tolerated.",
"name": "value",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null,
},
},
},
{
"defaultValue": null,
"description": "Minimum similarity threshold (0.0 to 1.0). Higher = stricter matching. Default is 0.3.",
"name": "threshold",
"type": {
"kind": "SCALAR",
"name": "Float",
"ofType": null,
},
},
],
"interfaces": null,
"kind": "INPUT_OBJECT",
"name": "TrgmSearchInput",
"possibleTypes": null,
},
{
"description": "The \`Float\` scalar type represents signed double-precision fractional values as specified by [IEEE 754](https://en.wikipedia.org/wiki/IEEE_floating_point).",
"enumValues": null,
"fields": null,
"inputFields": null,
"interfaces": null,
"kind": "SCALAR",
"name": "Float",
"possibleTypes": null,
},
{
"description": "Methods to use when ordering \`User\`.",
"enumValues": [
Expand Down
Loading