graphile-search 1.1.0 → 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 +12 -0
- package/adapters/pgvector.js +3 -0
- package/adapters/trgm.d.ts +13 -0
- package/adapters/trgm.js +6 -1
- package/adapters/tsvector.js +6 -2
- package/codecs/tsvector-codec.d.ts +9 -4
- package/codecs/tsvector-codec.js +42 -21
- package/esm/adapters/pgvector.js +3 -0
- package/esm/adapters/trgm.d.ts +13 -0
- package/esm/adapters/trgm.js +6 -1
- package/esm/adapters/tsvector.js +6 -2
- package/esm/codecs/tsvector-codec.d.ts +9 -4
- package/esm/codecs/tsvector-codec.js +42 -21
- package/esm/plugin.js +36 -4
- package/esm/types.d.ts +27 -0
- package/package.json +13 -13
- package/plugin.js +36 -4
- package/types.d.ts +27 -0
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
|
package/adapters/pgvector.js
CHANGED
|
@@ -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) {
|
package/adapters/trgm.d.ts
CHANGED
|
@@ -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,
|
package/adapters/tsvector.js
CHANGED
|
@@ -8,8 +8,12 @@
|
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
9
|
exports.createTsvectorAdapter = createTsvectorAdapter;
|
|
10
10
|
function isTsvectorCodec(codec) {
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
// In graphile-build-pg >= 5.0.0-rc.8, the built-in TYPES.tsvector codec
|
|
12
|
+
// has name === 'tsvector' but does NOT have extensions.pg. We need to
|
|
13
|
+
// match both the built-in codec and our custom codec (for older versions).
|
|
14
|
+
return (codec?.name === 'tsvector' ||
|
|
15
|
+
(codec?.extensions?.pg?.schemaName === 'pg_catalog' &&
|
|
16
|
+
codec?.extensions?.pg?.name === 'tsvector'));
|
|
13
17
|
}
|
|
14
18
|
function createTsvectorAdapter(options = {}) {
|
|
15
19
|
const { filterPrefix = 'tsv', tsConfig = 'english' } = options;
|
|
@@ -2,15 +2,20 @@
|
|
|
2
2
|
* TsvectorCodecPlugin
|
|
3
3
|
*
|
|
4
4
|
* Teaches PostGraphile v5 how to handle PostgreSQL's tsvector and tsquery types.
|
|
5
|
-
* Without this, tsvector columns are invisible to the schema builder and the
|
|
6
|
-
* search plugin cannot generate condition fields.
|
|
7
5
|
*
|
|
8
|
-
*
|
|
6
|
+
* In graphile-build-pg >= 5.0.0-rc.8, tsvector/tsquery codecs are handled
|
|
7
|
+
* natively and tsvector columns are HIDDEN by default (PgAttributesPlugin
|
|
8
|
+
* HIDE_BY_DEFAULT). This plugin:
|
|
9
|
+
*
|
|
9
10
|
* 1. Creates codecs for tsvector/tsquery via gather.hooks.pgCodecs_findPgCodec
|
|
11
|
+
* (kept for backward compatibility with older versions; rc.8+ handles this
|
|
12
|
+
* natively so the hook returns early when event.pgCodec is already set)
|
|
10
13
|
* 2. Registers a custom "FullText" scalar type for tsvector columns
|
|
11
14
|
* 3. Maps tsvector codec to the FullText scalar (isolating filter operators)
|
|
12
15
|
* 4. Maps tsquery codec to GraphQL String
|
|
13
|
-
* 5.
|
|
16
|
+
* 5. Re-enables tsvector columns for select/filterBy so that search plugins
|
|
17
|
+
* can detect and use them (overrides rc.8's HIDE_BY_DEFAULT)
|
|
18
|
+
* 6. Optionally hides tsvector columns from output types
|
|
14
19
|
*/
|
|
15
20
|
import type { GraphileConfig } from 'graphile-config';
|
|
16
21
|
/**
|
package/codecs/tsvector-codec.js
CHANGED
|
@@ -3,15 +3,20 @@
|
|
|
3
3
|
* TsvectorCodecPlugin
|
|
4
4
|
*
|
|
5
5
|
* Teaches PostGraphile v5 how to handle PostgreSQL's tsvector and tsquery types.
|
|
6
|
-
* Without this, tsvector columns are invisible to the schema builder and the
|
|
7
|
-
* search plugin cannot generate condition fields.
|
|
8
6
|
*
|
|
9
|
-
*
|
|
7
|
+
* In graphile-build-pg >= 5.0.0-rc.8, tsvector/tsquery codecs are handled
|
|
8
|
+
* natively and tsvector columns are HIDDEN by default (PgAttributesPlugin
|
|
9
|
+
* HIDE_BY_DEFAULT). This plugin:
|
|
10
|
+
*
|
|
10
11
|
* 1. Creates codecs for tsvector/tsquery via gather.hooks.pgCodecs_findPgCodec
|
|
12
|
+
* (kept for backward compatibility with older versions; rc.8+ handles this
|
|
13
|
+
* natively so the hook returns early when event.pgCodec is already set)
|
|
11
14
|
* 2. Registers a custom "FullText" scalar type for tsvector columns
|
|
12
15
|
* 3. Maps tsvector codec to the FullText scalar (isolating filter operators)
|
|
13
16
|
* 4. Maps tsquery codec to GraphQL String
|
|
14
|
-
* 5.
|
|
17
|
+
* 5. Re-enables tsvector columns for select/filterBy so that search plugins
|
|
18
|
+
* can detect and use them (overrides rc.8's HIDE_BY_DEFAULT)
|
|
19
|
+
* 6. Optionally hides tsvector columns from output types
|
|
15
20
|
*/
|
|
16
21
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
17
22
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
@@ -130,25 +135,41 @@ function createTsvectorCodecPlugin(options = {}) {
|
|
|
130
135
|
},
|
|
131
136
|
},
|
|
132
137
|
},
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
138
|
+
// In graphile-build-pg >= 5.0.0-rc.8, PgAttributesPlugin HIDE_BY_DEFAULT
|
|
139
|
+
// hides tsvector columns entirely (-attribute:select, -attribute:base,
|
|
140
|
+
// -condition:attribute:filterBy, etc.). We need entity behavior to
|
|
141
|
+
// re-enable them so search plugins can detect and use them.
|
|
142
|
+
entityBehavior: {
|
|
143
|
+
pgCodecAttribute: {
|
|
144
|
+
inferred: {
|
|
145
|
+
after: ['default'],
|
|
146
|
+
provides: ['tsvectorCodecPlugin'],
|
|
147
|
+
callback(behavior, [codec, attributeName]) {
|
|
148
|
+
const attr = codec.attributes?.[attributeName];
|
|
149
|
+
const attrCodec = attr?.codec;
|
|
150
|
+
const isTsvector = attrCodec?.name === 'tsvector' ||
|
|
151
|
+
(attrCodec?.extensions?.pg?.schemaName === 'pg_catalog' &&
|
|
152
|
+
attrCodec?.extensions?.pg?.name === 'tsvector');
|
|
153
|
+
if (!isTsvector) {
|
|
154
|
+
return behavior;
|
|
155
|
+
}
|
|
156
|
+
if (hideTsvectorColumns) {
|
|
157
|
+
// User explicitly wants tsvector columns hidden from output
|
|
158
|
+
return [behavior, '-attribute:select'];
|
|
159
|
+
}
|
|
160
|
+
// Re-enable tsvector columns that rc.8 hides by default.
|
|
161
|
+
// The search plugin needs attribute:select to detect columns,
|
|
162
|
+
// and condition:attribute:filterBy for standard filter support.
|
|
163
|
+
return [
|
|
164
|
+
behavior,
|
|
165
|
+
'attribute:select',
|
|
166
|
+
'attribute:base',
|
|
167
|
+
'condition:attribute:filterBy',
|
|
168
|
+
];
|
|
148
169
|
},
|
|
149
170
|
},
|
|
150
|
-
}
|
|
151
|
-
|
|
171
|
+
},
|
|
172
|
+
},
|
|
152
173
|
},
|
|
153
174
|
};
|
|
154
175
|
}
|
package/esm/adapters/pgvector.js
CHANGED
|
@@ -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) {
|
package/esm/adapters/trgm.d.ts
CHANGED
|
@@ -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/esm/adapters/trgm.js
CHANGED
|
@@ -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/adapters/tsvector.js
CHANGED
|
@@ -5,8 +5,12 @@
|
|
|
5
5
|
* Wraps the same SQL logic as graphile-tsvector but as a SearchAdapter.
|
|
6
6
|
*/
|
|
7
7
|
function isTsvectorCodec(codec) {
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
// In graphile-build-pg >= 5.0.0-rc.8, the built-in TYPES.tsvector codec
|
|
9
|
+
// has name === 'tsvector' but does NOT have extensions.pg. We need to
|
|
10
|
+
// match both the built-in codec and our custom codec (for older versions).
|
|
11
|
+
return (codec?.name === 'tsvector' ||
|
|
12
|
+
(codec?.extensions?.pg?.schemaName === 'pg_catalog' &&
|
|
13
|
+
codec?.extensions?.pg?.name === 'tsvector'));
|
|
10
14
|
}
|
|
11
15
|
export function createTsvectorAdapter(options = {}) {
|
|
12
16
|
const { filterPrefix = 'tsv', tsConfig = 'english' } = options;
|
|
@@ -2,15 +2,20 @@
|
|
|
2
2
|
* TsvectorCodecPlugin
|
|
3
3
|
*
|
|
4
4
|
* Teaches PostGraphile v5 how to handle PostgreSQL's tsvector and tsquery types.
|
|
5
|
-
* Without this, tsvector columns are invisible to the schema builder and the
|
|
6
|
-
* search plugin cannot generate condition fields.
|
|
7
5
|
*
|
|
8
|
-
*
|
|
6
|
+
* In graphile-build-pg >= 5.0.0-rc.8, tsvector/tsquery codecs are handled
|
|
7
|
+
* natively and tsvector columns are HIDDEN by default (PgAttributesPlugin
|
|
8
|
+
* HIDE_BY_DEFAULT). This plugin:
|
|
9
|
+
*
|
|
9
10
|
* 1. Creates codecs for tsvector/tsquery via gather.hooks.pgCodecs_findPgCodec
|
|
11
|
+
* (kept for backward compatibility with older versions; rc.8+ handles this
|
|
12
|
+
* natively so the hook returns early when event.pgCodec is already set)
|
|
10
13
|
* 2. Registers a custom "FullText" scalar type for tsvector columns
|
|
11
14
|
* 3. Maps tsvector codec to the FullText scalar (isolating filter operators)
|
|
12
15
|
* 4. Maps tsquery codec to GraphQL String
|
|
13
|
-
* 5.
|
|
16
|
+
* 5. Re-enables tsvector columns for select/filterBy so that search plugins
|
|
17
|
+
* can detect and use them (overrides rc.8's HIDE_BY_DEFAULT)
|
|
18
|
+
* 6. Optionally hides tsvector columns from output types
|
|
14
19
|
*/
|
|
15
20
|
import type { GraphileConfig } from 'graphile-config';
|
|
16
21
|
/**
|
|
@@ -2,15 +2,20 @@
|
|
|
2
2
|
* TsvectorCodecPlugin
|
|
3
3
|
*
|
|
4
4
|
* Teaches PostGraphile v5 how to handle PostgreSQL's tsvector and tsquery types.
|
|
5
|
-
* Without this, tsvector columns are invisible to the schema builder and the
|
|
6
|
-
* search plugin cannot generate condition fields.
|
|
7
5
|
*
|
|
8
|
-
*
|
|
6
|
+
* In graphile-build-pg >= 5.0.0-rc.8, tsvector/tsquery codecs are handled
|
|
7
|
+
* natively and tsvector columns are HIDDEN by default (PgAttributesPlugin
|
|
8
|
+
* HIDE_BY_DEFAULT). This plugin:
|
|
9
|
+
*
|
|
9
10
|
* 1. Creates codecs for tsvector/tsquery via gather.hooks.pgCodecs_findPgCodec
|
|
11
|
+
* (kept for backward compatibility with older versions; rc.8+ handles this
|
|
12
|
+
* natively so the hook returns early when event.pgCodec is already set)
|
|
10
13
|
* 2. Registers a custom "FullText" scalar type for tsvector columns
|
|
11
14
|
* 3. Maps tsvector codec to the FullText scalar (isolating filter operators)
|
|
12
15
|
* 4. Maps tsquery codec to GraphQL String
|
|
13
|
-
* 5.
|
|
16
|
+
* 5. Re-enables tsvector columns for select/filterBy so that search plugins
|
|
17
|
+
* can detect and use them (overrides rc.8's HIDE_BY_DEFAULT)
|
|
18
|
+
* 6. Optionally hides tsvector columns from output types
|
|
14
19
|
*/
|
|
15
20
|
import { GraphQLString } from 'graphql';
|
|
16
21
|
import sql from 'pg-sql2';
|
|
@@ -123,25 +128,41 @@ export function createTsvectorCodecPlugin(options = {}) {
|
|
|
123
128
|
},
|
|
124
129
|
},
|
|
125
130
|
},
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
131
|
+
// In graphile-build-pg >= 5.0.0-rc.8, PgAttributesPlugin HIDE_BY_DEFAULT
|
|
132
|
+
// hides tsvector columns entirely (-attribute:select, -attribute:base,
|
|
133
|
+
// -condition:attribute:filterBy, etc.). We need entity behavior to
|
|
134
|
+
// re-enable them so search plugins can detect and use them.
|
|
135
|
+
entityBehavior: {
|
|
136
|
+
pgCodecAttribute: {
|
|
137
|
+
inferred: {
|
|
138
|
+
after: ['default'],
|
|
139
|
+
provides: ['tsvectorCodecPlugin'],
|
|
140
|
+
callback(behavior, [codec, attributeName]) {
|
|
141
|
+
const attr = codec.attributes?.[attributeName];
|
|
142
|
+
const attrCodec = attr?.codec;
|
|
143
|
+
const isTsvector = attrCodec?.name === 'tsvector' ||
|
|
144
|
+
(attrCodec?.extensions?.pg?.schemaName === 'pg_catalog' &&
|
|
145
|
+
attrCodec?.extensions?.pg?.name === 'tsvector');
|
|
146
|
+
if (!isTsvector) {
|
|
147
|
+
return behavior;
|
|
148
|
+
}
|
|
149
|
+
if (hideTsvectorColumns) {
|
|
150
|
+
// User explicitly wants tsvector columns hidden from output
|
|
151
|
+
return [behavior, '-attribute:select'];
|
|
152
|
+
}
|
|
153
|
+
// Re-enable tsvector columns that rc.8 hides by default.
|
|
154
|
+
// The search plugin needs attribute:select to detect columns,
|
|
155
|
+
// and condition:attribute:filterBy for standard filter support.
|
|
156
|
+
return [
|
|
157
|
+
behavior,
|
|
158
|
+
'attribute:select',
|
|
159
|
+
'attribute:base',
|
|
160
|
+
'condition:attribute:filterBy',
|
|
161
|
+
];
|
|
141
162
|
},
|
|
142
163
|
},
|
|
143
|
-
}
|
|
144
|
-
|
|
164
|
+
},
|
|
165
|
+
},
|
|
145
166
|
},
|
|
146
167
|
};
|
|
147
168
|
}
|
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
|
-
|
|
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
|
-
//
|
|
109
|
-
|
|
110
|
-
|
|
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.
|
|
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,20 +31,20 @@
|
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@types/node": "^22.19.11",
|
|
33
33
|
"@types/pg": "^8.18.0",
|
|
34
|
-
"graphile-connection-filter": "^1.1.
|
|
35
|
-
"graphile-test": "^4.5.
|
|
34
|
+
"graphile-connection-filter": "^1.1.2",
|
|
35
|
+
"graphile-test": "^4.5.4",
|
|
36
36
|
"makage": "^0.1.10",
|
|
37
|
-
"pg": "^8.
|
|
38
|
-
"pgsql-test": "^4.5.
|
|
37
|
+
"pg": "^8.20.0",
|
|
38
|
+
"pgsql-test": "^4.5.4"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
|
-
"@dataplan/pg": "1.0.0-rc.
|
|
42
|
-
"graphile-build": "5.0.0-rc.
|
|
43
|
-
"graphile-build-pg": "5.0.0-rc.
|
|
44
|
-
"graphile-config": "1.0.0-rc.
|
|
45
|
-
"graphql": "
|
|
46
|
-
"pg-sql2": "5.0.0-rc.
|
|
47
|
-
"postgraphile": "5.0.0-rc.
|
|
41
|
+
"@dataplan/pg": "1.0.0-rc.8",
|
|
42
|
+
"graphile-build": "5.0.0-rc.6",
|
|
43
|
+
"graphile-build-pg": "5.0.0-rc.8",
|
|
44
|
+
"graphile-config": "1.0.0-rc.6",
|
|
45
|
+
"graphql": "16.13.0",
|
|
46
|
+
"pg-sql2": "5.0.0-rc.5",
|
|
47
|
+
"postgraphile": "5.0.0-rc.10"
|
|
48
48
|
},
|
|
49
49
|
"keywords": [
|
|
50
50
|
"postgraphile",
|
|
@@ -62,5 +62,5 @@
|
|
|
62
62
|
"hybrid-search",
|
|
63
63
|
"searchScore"
|
|
64
64
|
],
|
|
65
|
-
"gitHead": "
|
|
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
|
-
|
|
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
|
-
//
|
|
112
|
-
|
|
113
|
-
|
|
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).
|