graphile-search 1.1.1 → 1.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # graphile-search
2
2
 
3
+ <p align="center" width="100%">
4
+ <img height="250" src="https://raw.githubusercontent.com/constructive-io/constructive/refs/heads/main/assets/outline-logo.svg" />
5
+ </p>
6
+
7
+ <p align="center" width="100%">
8
+ <a href="https://github.com/constructive-io/constructive/actions/workflows/run-tests.yaml">
9
+ <img height="20" src="https://github.com/constructive-io/constructive/actions/workflows/run-tests.yaml/badge.svg" />
10
+ </a>
11
+ <a href="https://github.com/constructive-io/constructive/blob/main/LICENSE"><img height="20" src="https://img.shields.io/badge/license-MIT-blue.svg"/></a>
12
+ <a href="https://www.npmjs.com/package/graphile-search"><img height="20" src="https://img.shields.io/github/package-json/v/constructive-io/constructive?filename=graphile%2Fgraphile-search%2Fpackage.json"/></a>
13
+ </p>
14
+
3
15
  Unified PostGraphile v5 search plugin — abstracts tsvector, BM25, pg_trgm, and pgvector behind a single adapter-based architecture with composite `searchScore`.
4
16
 
5
17
  ## Overview
@@ -29,6 +29,9 @@ function createPgvectorAdapter(options = {}) {
29
29
  range: null, // 0 to infinity
30
30
  },
31
31
  filterPrefix,
32
+ // pgvector operates on embedding vectors, not text search — its presence
33
+ // alone should NOT trigger supplementary adapters like trgm.
34
+ isIntentionalSearch: false,
32
35
  supportsTextSearch: false,
33
36
  // pgvector requires a vector array, not plain text — no buildTextSearchInput
34
37
  detectColumns(codec, _build) {
@@ -16,5 +16,18 @@ export interface TrgmAdapterOptions {
16
16
  * @default 0.3
17
17
  */
18
18
  defaultThreshold?: number;
19
+ /**
20
+ * When true, trgm only activates on tables that have an "intentional"
21
+ * search column detected by another adapter (e.g. a tsvector column or
22
+ * a BM25 index). This prevents trgm similarity fields from being added
23
+ * to every table with text columns.
24
+ *
25
+ * The plugin's `getAdapterColumns` orchestrates this by running
26
+ * non-supplementary adapters first, then only running supplementary
27
+ * adapters on codecs that already have search columns.
28
+ *
29
+ * @default true
30
+ */
31
+ requireIntentionalSearch?: boolean;
19
32
  }
20
33
  export declare function createTrgmAdapter(options?: TrgmAdapterOptions): SearchAdapter;
package/adapters/trgm.js CHANGED
@@ -12,9 +12,14 @@ function isTextCodec(codec) {
12
12
  return name === 'text' || name === 'varchar' || name === 'bpchar';
13
13
  }
14
14
  function createTrgmAdapter(options = {}) {
15
- const { filterPrefix = 'trgm', defaultThreshold = 0.3 } = options;
15
+ const { filterPrefix = 'trgm', defaultThreshold = 0.3, requireIntentionalSearch = true, } = options;
16
16
  return {
17
17
  name: 'trgm',
18
+ /**
19
+ * When true, this adapter is "supplementary" — it only activates on
20
+ * tables that already have columns detected by a non-supplementary adapter.
21
+ */
22
+ isSupplementary: requireIntentionalSearch,
18
23
  scoreSemantics: {
19
24
  metric: 'similarity',
20
25
  lowerIsBetter: false,
@@ -26,6 +26,9 @@ export function createPgvectorAdapter(options = {}) {
26
26
  range: null, // 0 to infinity
27
27
  },
28
28
  filterPrefix,
29
+ // pgvector operates on embedding vectors, not text search — its presence
30
+ // alone should NOT trigger supplementary adapters like trgm.
31
+ isIntentionalSearch: false,
29
32
  supportsTextSearch: false,
30
33
  // pgvector requires a vector array, not plain text — no buildTextSearchInput
31
34
  detectColumns(codec, _build) {
@@ -16,5 +16,18 @@ export interface TrgmAdapterOptions {
16
16
  * @default 0.3
17
17
  */
18
18
  defaultThreshold?: number;
19
+ /**
20
+ * When true, trgm only activates on tables that have an "intentional"
21
+ * search column detected by another adapter (e.g. a tsvector column or
22
+ * a BM25 index). This prevents trgm similarity fields from being added
23
+ * to every table with text columns.
24
+ *
25
+ * The plugin's `getAdapterColumns` orchestrates this by running
26
+ * non-supplementary adapters first, then only running supplementary
27
+ * adapters on codecs that already have search columns.
28
+ *
29
+ * @default true
30
+ */
31
+ requireIntentionalSearch?: boolean;
19
32
  }
20
33
  export declare function createTrgmAdapter(options?: TrgmAdapterOptions): SearchAdapter;
@@ -9,9 +9,14 @@ function isTextCodec(codec) {
9
9
  return name === 'text' || name === 'varchar' || name === 'bpchar';
10
10
  }
11
11
  export function createTrgmAdapter(options = {}) {
12
- const { filterPrefix = 'trgm', defaultThreshold = 0.3 } = options;
12
+ const { filterPrefix = 'trgm', defaultThreshold = 0.3, requireIntentionalSearch = true, } = options;
13
13
  return {
14
14
  name: 'trgm',
15
+ /**
16
+ * When true, this adapter is "supplementary" — it only activates on
17
+ * tables that already have columns detected by a non-supplementary adapter.
18
+ */
19
+ isSupplementary: requireIntentionalSearch,
15
20
  scoreSemantics: {
16
21
  metric: 'similarity',
17
22
  lowerIsBetter: false,
package/esm/plugin.js CHANGED
@@ -30,17 +30,46 @@ export function createUnifiedSearchPlugin(options) {
30
30
  const codecCache = new Map();
31
31
  /**
32
32
  * Get (or compute) the adapter columns for a given codec.
33
+ *
34
+ * Runs non-supplementary adapters first (e.g. tsvector, BM25, pgvector).
35
+ * Supplementary adapters (e.g. trgm with requireIntentionalSearch) are only
36
+ * run if at least one adapter with `isIntentionalSearch: true` found columns.
37
+ *
38
+ * This distinction matters because pgvector (embeddings) is NOT intentional
39
+ * text search — its presence alone should not trigger trgm similarity fields.
40
+ * Only tsvector and BM25, which represent explicit search infrastructure,
41
+ * count as intentional search.
33
42
  */
34
43
  function getAdapterColumns(codec, build) {
35
44
  const cacheKey = codec.name;
36
45
  if (codecCache.has(cacheKey)) {
37
46
  return codecCache.get(cacheKey);
38
47
  }
48
+ const primaryAdapters = adapters.filter((a) => !a.isSupplementary);
49
+ const supplementaryAdapters = adapters.filter((a) => a.isSupplementary);
50
+ // Phase 1: Run non-supplementary adapters (tsvector, BM25, pgvector, etc.)
39
51
  const results = [];
40
- for (const adapter of adapters) {
52
+ let hasIntentionalSearch = false;
53
+ for (const adapter of primaryAdapters) {
41
54
  const columns = adapter.detectColumns(codec, build);
42
55
  if (columns.length > 0) {
43
56
  results.push({ adapter, columns });
57
+ // Track whether any "intentional search" adapter found columns.
58
+ // isIntentionalSearch defaults to true when not explicitly set.
59
+ if (adapter.isIntentionalSearch !== false) {
60
+ hasIntentionalSearch = true;
61
+ }
62
+ }
63
+ }
64
+ // Phase 2: Only run supplementary adapters if at least one primary
65
+ // adapter with isIntentionalSearch found columns on this codec.
66
+ // pgvector (isIntentionalSearch: false) alone won't trigger trgm.
67
+ if (hasIntentionalSearch) {
68
+ for (const adapter of supplementaryAdapters) {
69
+ const columns = adapter.detectColumns(codec, build);
70
+ if (columns.length > 0) {
71
+ results.push({ adapter, columns });
72
+ }
44
73
  }
45
74
  }
46
75
  codecCache.set(cacheKey, results);
@@ -105,9 +134,12 @@ export function createUnifiedSearchPlugin(options) {
105
134
  provides: ['default'],
106
135
  before: ['inferred', 'override', 'PgAttributesPlugin'],
107
136
  callback(behavior, [codec, attributeName], build) {
108
- // Check if any adapter claims this column
109
- for (const adapter of adapters) {
110
- const columns = adapter.detectColumns(codec, build);
137
+ // Use getAdapterColumns which respects isSupplementary logic,
138
+ // so trgm columns only appear when intentional search exists
139
+ if (!codec?.attributes)
140
+ return behavior;
141
+ const adapterColumns = getAdapterColumns(codec, build);
142
+ for (const { columns } of adapterColumns) {
111
143
  if (columns.some((c) => c.attributeName === attributeName)) {
112
144
  return [
113
145
  'unifiedSearch:orderBy',
package/esm/types.d.ts CHANGED
@@ -64,6 +64,33 @@ export interface SearchAdapter {
64
64
  name: string;
65
65
  /** Score semantics for this algorithm. */
66
66
  scoreSemantics: ScoreSemantics;
67
+ /**
68
+ * When true, this adapter is "supplementary" — it only activates on
69
+ * tables that already have at least one column detected by an adapter
70
+ * whose `isIntentionalSearch` is true (e.g. tsvector or BM25).
71
+ *
72
+ * This prevents adapters like pg_trgm from adding similarity fields
73
+ * to every table with text columns when there is no intentional search setup.
74
+ *
75
+ * pgvector (embeddings) does NOT count as intentional search because it
76
+ * operates on vector columns, not text search — so its presence alone
77
+ * won't trigger supplementary adapters.
78
+ *
79
+ * @default false
80
+ */
81
+ isSupplementary?: boolean;
82
+ /**
83
+ * When true, this adapter represents "intentional search" — its presence
84
+ * on a table signals that the table was explicitly set up for search and
85
+ * should trigger supplementary adapters (e.g. trgm).
86
+ *
87
+ * Adapters that check for real infrastructure (tsvector columns, BM25
88
+ * indexes) should set this to true. Adapters that operate on a different
89
+ * domain (pgvector embeddings) should set this to false.
90
+ *
91
+ * @default true
92
+ */
93
+ isIntentionalSearch?: boolean;
67
94
  /**
68
95
  * The filter prefix used for filter field names on the connection filter input.
69
96
  * The field name is: `{filterPrefix}{ColumnName}` (camelCase).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "graphile-search",
3
- "version": "1.1.1",
3
+ "version": "1.1.2",
4
4
  "description": "Unified PostGraphile v5 search plugin — abstracts tsvector, BM25, pg_trgm, and pgvector behind a single adapter-based architecture with composite searchScore",
5
5
  "author": "Constructive <developers@constructive.io>",
6
6
  "homepage": "https://github.com/constructive-io/constructive",
@@ -31,11 +31,11 @@
31
31
  "devDependencies": {
32
32
  "@types/node": "^22.19.11",
33
33
  "@types/pg": "^8.18.0",
34
- "graphile-connection-filter": "^1.1.1",
35
- "graphile-test": "^4.5.3",
34
+ "graphile-connection-filter": "^1.1.2",
35
+ "graphile-test": "^4.5.4",
36
36
  "makage": "^0.1.10",
37
37
  "pg": "^8.20.0",
38
- "pgsql-test": "^4.5.3"
38
+ "pgsql-test": "^4.5.4"
39
39
  },
40
40
  "peerDependencies": {
41
41
  "@dataplan/pg": "1.0.0-rc.8",
@@ -62,5 +62,5 @@
62
62
  "hybrid-search",
63
63
  "searchScore"
64
64
  ],
65
- "gitHead": "21fd7c2c30663548cf15aa448c1935ab56e5497d"
65
+ "gitHead": "8afe6b19da82facbe5f3365762ba52888af5b3c9"
66
66
  }
package/plugin.js CHANGED
@@ -33,17 +33,46 @@ function createUnifiedSearchPlugin(options) {
33
33
  const codecCache = new Map();
34
34
  /**
35
35
  * Get (or compute) the adapter columns for a given codec.
36
+ *
37
+ * Runs non-supplementary adapters first (e.g. tsvector, BM25, pgvector).
38
+ * Supplementary adapters (e.g. trgm with requireIntentionalSearch) are only
39
+ * run if at least one adapter with `isIntentionalSearch: true` found columns.
40
+ *
41
+ * This distinction matters because pgvector (embeddings) is NOT intentional
42
+ * text search — its presence alone should not trigger trgm similarity fields.
43
+ * Only tsvector and BM25, which represent explicit search infrastructure,
44
+ * count as intentional search.
36
45
  */
37
46
  function getAdapterColumns(codec, build) {
38
47
  const cacheKey = codec.name;
39
48
  if (codecCache.has(cacheKey)) {
40
49
  return codecCache.get(cacheKey);
41
50
  }
51
+ const primaryAdapters = adapters.filter((a) => !a.isSupplementary);
52
+ const supplementaryAdapters = adapters.filter((a) => a.isSupplementary);
53
+ // Phase 1: Run non-supplementary adapters (tsvector, BM25, pgvector, etc.)
42
54
  const results = [];
43
- for (const adapter of adapters) {
55
+ let hasIntentionalSearch = false;
56
+ for (const adapter of primaryAdapters) {
44
57
  const columns = adapter.detectColumns(codec, build);
45
58
  if (columns.length > 0) {
46
59
  results.push({ adapter, columns });
60
+ // Track whether any "intentional search" adapter found columns.
61
+ // isIntentionalSearch defaults to true when not explicitly set.
62
+ if (adapter.isIntentionalSearch !== false) {
63
+ hasIntentionalSearch = true;
64
+ }
65
+ }
66
+ }
67
+ // Phase 2: Only run supplementary adapters if at least one primary
68
+ // adapter with isIntentionalSearch found columns on this codec.
69
+ // pgvector (isIntentionalSearch: false) alone won't trigger trgm.
70
+ if (hasIntentionalSearch) {
71
+ for (const adapter of supplementaryAdapters) {
72
+ const columns = adapter.detectColumns(codec, build);
73
+ if (columns.length > 0) {
74
+ results.push({ adapter, columns });
75
+ }
47
76
  }
48
77
  }
49
78
  codecCache.set(cacheKey, results);
@@ -108,9 +137,12 @@ function createUnifiedSearchPlugin(options) {
108
137
  provides: ['default'],
109
138
  before: ['inferred', 'override', 'PgAttributesPlugin'],
110
139
  callback(behavior, [codec, attributeName], build) {
111
- // Check if any adapter claims this column
112
- for (const adapter of adapters) {
113
- const columns = adapter.detectColumns(codec, build);
140
+ // Use getAdapterColumns which respects isSupplementary logic,
141
+ // so trgm columns only appear when intentional search exists
142
+ if (!codec?.attributes)
143
+ return behavior;
144
+ const adapterColumns = getAdapterColumns(codec, build);
145
+ for (const { columns } of adapterColumns) {
114
146
  if (columns.some((c) => c.attributeName === attributeName)) {
115
147
  return [
116
148
  'unifiedSearch:orderBy',
package/types.d.ts CHANGED
@@ -64,6 +64,33 @@ export interface SearchAdapter {
64
64
  name: string;
65
65
  /** Score semantics for this algorithm. */
66
66
  scoreSemantics: ScoreSemantics;
67
+ /**
68
+ * When true, this adapter is "supplementary" — it only activates on
69
+ * tables that already have at least one column detected by an adapter
70
+ * whose `isIntentionalSearch` is true (e.g. tsvector or BM25).
71
+ *
72
+ * This prevents adapters like pg_trgm from adding similarity fields
73
+ * to every table with text columns when there is no intentional search setup.
74
+ *
75
+ * pgvector (embeddings) does NOT count as intentional search because it
76
+ * operates on vector columns, not text search — so its presence alone
77
+ * won't trigger supplementary adapters.
78
+ *
79
+ * @default false
80
+ */
81
+ isSupplementary?: boolean;
82
+ /**
83
+ * When true, this adapter represents "intentional search" — its presence
84
+ * on a table signals that the table was explicitly set up for search and
85
+ * should trigger supplementary adapters (e.g. trgm).
86
+ *
87
+ * Adapters that check for real infrastructure (tsvector columns, BM25
88
+ * indexes) should set this to true. Adapters that operate on a different
89
+ * domain (pgvector embeddings) should set this to false.
90
+ *
91
+ * @default true
92
+ */
93
+ isIntentionalSearch?: boolean;
67
94
  /**
68
95
  * The filter prefix used for filter field names on the connection filter input.
69
96
  * The field name is: `{filterPrefix}{ColumnName}` (camelCase).