graphile-search 1.1.0
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/LICENSE +23 -0
- package/README.md +123 -0
- package/adapters/bm25.d.ts +32 -0
- package/adapters/bm25.js +119 -0
- package/adapters/index.d.ts +14 -0
- package/adapters/index.js +17 -0
- package/adapters/pgvector.d.ts +21 -0
- package/adapters/pgvector.js +125 -0
- package/adapters/trgm.d.ts +20 -0
- package/adapters/trgm.js +83 -0
- package/adapters/tsvector.d.ts +20 -0
- package/adapters/tsvector.js +60 -0
- package/codecs/bm25-codec.d.ts +42 -0
- package/codecs/bm25-codec.js +199 -0
- package/codecs/index.d.ts +12 -0
- package/codecs/index.js +22 -0
- package/codecs/operator-factories.d.ts +22 -0
- package/codecs/operator-factories.js +84 -0
- package/codecs/tsvector-codec.d.ts +53 -0
- package/codecs/tsvector-codec.js +162 -0
- package/codecs/vector-codec.d.ts +18 -0
- package/codecs/vector-codec.js +116 -0
- package/esm/adapters/bm25.d.ts +32 -0
- package/esm/adapters/bm25.js +116 -0
- package/esm/adapters/index.d.ts +14 -0
- package/esm/adapters/index.js +10 -0
- package/esm/adapters/pgvector.d.ts +21 -0
- package/esm/adapters/pgvector.js +122 -0
- package/esm/adapters/trgm.d.ts +20 -0
- package/esm/adapters/trgm.js +80 -0
- package/esm/adapters/tsvector.d.ts +20 -0
- package/esm/adapters/tsvector.js +57 -0
- package/esm/codecs/bm25-codec.d.ts +42 -0
- package/esm/codecs/bm25-codec.js +160 -0
- package/esm/codecs/index.d.ts +12 -0
- package/esm/codecs/index.js +10 -0
- package/esm/codecs/operator-factories.d.ts +22 -0
- package/esm/codecs/operator-factories.js +80 -0
- package/esm/codecs/tsvector-codec.d.ts +53 -0
- package/esm/codecs/tsvector-codec.js +155 -0
- package/esm/codecs/vector-codec.d.ts +18 -0
- package/esm/codecs/vector-codec.js +110 -0
- package/esm/index.d.ts +40 -0
- package/esm/index.js +41 -0
- package/esm/plugin.d.ts +50 -0
- package/esm/plugin.js +553 -0
- package/esm/preset.d.ts +79 -0
- package/esm/preset.js +82 -0
- package/esm/types.d.ts +171 -0
- package/esm/types.js +7 -0
- package/index.d.ts +40 -0
- package/index.js +60 -0
- package/package.json +66 -0
- package/plugin.d.ts +50 -0
- package/plugin.js +556 -0
- package/preset.d.ts +79 -0
- package/preset.js +85 -0
- package/types.d.ts +171 -0
- package/types.js +8 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* VectorCodecPlugin
|
|
4
|
+
*
|
|
5
|
+
* Teaches PostGraphile v5 how to handle the pgvector `vector` type.
|
|
6
|
+
*
|
|
7
|
+
* Without this:
|
|
8
|
+
* - `vector(n)` columns are silently invisible in the schema
|
|
9
|
+
* - SQL functions with `vector` args are skipped entirely
|
|
10
|
+
*
|
|
11
|
+
* Wire format: PostgreSQL sends vector as text `[0.1,0.2,...,0.768]`
|
|
12
|
+
* JavaScript: number[]
|
|
13
|
+
* GraphQL: `Vector` scalar (serialized as [Float])
|
|
14
|
+
*/
|
|
15
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
16
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
17
|
+
};
|
|
18
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
+
exports.VectorCodecPreset = exports.VectorCodecPlugin = void 0;
|
|
20
|
+
require("graphile-build-pg");
|
|
21
|
+
require("graphile-build");
|
|
22
|
+
const pg_sql2_1 = __importDefault(require("pg-sql2"));
|
|
23
|
+
exports.VectorCodecPlugin = {
|
|
24
|
+
name: 'VectorCodecPlugin',
|
|
25
|
+
version: '1.0.0',
|
|
26
|
+
description: 'Registers a codec for the pgvector `vector` type',
|
|
27
|
+
gather: {
|
|
28
|
+
hooks: {
|
|
29
|
+
async pgCodecs_findPgCodec(info, event) {
|
|
30
|
+
if (event.pgCodec)
|
|
31
|
+
return;
|
|
32
|
+
const { pgType: type, serviceName } = event;
|
|
33
|
+
if (type.typname !== 'vector')
|
|
34
|
+
return;
|
|
35
|
+
const typeNamespace = await info.helpers.pgIntrospection.getNamespace(serviceName, type.typnamespace);
|
|
36
|
+
if (!typeNamespace)
|
|
37
|
+
return;
|
|
38
|
+
const schemaName = typeNamespace.nspname;
|
|
39
|
+
event.pgCodec = {
|
|
40
|
+
name: 'vector',
|
|
41
|
+
sqlType: pg_sql2_1.default.identifier(schemaName, 'vector'),
|
|
42
|
+
// PG sends: [0.1,-0.2,...,0.768] -> number[]
|
|
43
|
+
fromPg(value) {
|
|
44
|
+
return value
|
|
45
|
+
.replace(/^\[|\]$/g, '')
|
|
46
|
+
.split(',')
|
|
47
|
+
.map((v) => parseFloat(v.trim()));
|
|
48
|
+
},
|
|
49
|
+
// number[] -> [0.1,-0.2,...,0.768]
|
|
50
|
+
toPg(value) {
|
|
51
|
+
if (!Array.isArray(value))
|
|
52
|
+
throw new Error('vector input must be an array of numbers');
|
|
53
|
+
return `[${value.join(',')}]`;
|
|
54
|
+
},
|
|
55
|
+
attributes: undefined,
|
|
56
|
+
executor: undefined,
|
|
57
|
+
extensions: {
|
|
58
|
+
oid: type._id,
|
|
59
|
+
pg: { serviceName, schemaName, name: 'vector' },
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
schema: {
|
|
66
|
+
hooks: {
|
|
67
|
+
init: {
|
|
68
|
+
before: ['PgCodecs'],
|
|
69
|
+
callback(_, build) {
|
|
70
|
+
const { setGraphQLTypeForPgCodec } = build;
|
|
71
|
+
build.registerScalarType('Vector', {}, () => ({
|
|
72
|
+
description: 'A pgvector embedding — array of floats. ' +
|
|
73
|
+
'Dimensions must match the column (e.g. 768 for nomic-embed-text).',
|
|
74
|
+
serialize(value) {
|
|
75
|
+
if (Array.isArray(value))
|
|
76
|
+
return value;
|
|
77
|
+
if (typeof value === 'string')
|
|
78
|
+
return value.replace(/^\[|\]$/g, '').split(',').map((v) => parseFloat(v.trim()));
|
|
79
|
+
throw new Error('Vector must be an array of numbers');
|
|
80
|
+
},
|
|
81
|
+
parseValue(value) {
|
|
82
|
+
if (Array.isArray(value))
|
|
83
|
+
return value;
|
|
84
|
+
throw new Error('Vector must be an array of numbers');
|
|
85
|
+
},
|
|
86
|
+
parseLiteral(ast) {
|
|
87
|
+
if (ast.kind === 'NullValue')
|
|
88
|
+
return null;
|
|
89
|
+
if (ast.kind === 'ListValue')
|
|
90
|
+
return ast.values.map((v) => {
|
|
91
|
+
if (v.kind === 'FloatValue' || v.kind === 'IntValue')
|
|
92
|
+
return parseFloat(v.value);
|
|
93
|
+
throw new Error('Vector elements must be Float values');
|
|
94
|
+
});
|
|
95
|
+
if (ast.kind === 'StringValue')
|
|
96
|
+
return ast.value.replace(/^\[|\]$/g, '').split(',').map((v) => parseFloat(v.trim()));
|
|
97
|
+
throw new Error('Vector must be a list of floats or a string "[f1,f2,...]"');
|
|
98
|
+
},
|
|
99
|
+
}), 'VectorCodecPlugin registering Vector scalar');
|
|
100
|
+
// Wire codec -> scalar for both input (mutations) and output (queries).
|
|
101
|
+
// Without BOTH, PgAttributesPlugin silently drops the column.
|
|
102
|
+
for (const codec of Object.values(build.input.pgRegistry.pgCodecs)) {
|
|
103
|
+
if (codec.name === 'vector') {
|
|
104
|
+
setGraphQLTypeForPgCodec(codec, 'input', 'Vector');
|
|
105
|
+
setGraphQLTypeForPgCodec(codec, 'output', 'Vector');
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return _;
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
exports.VectorCodecPreset = {
|
|
115
|
+
plugins: [exports.VectorCodecPlugin],
|
|
116
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BM25 Search Adapter
|
|
3
|
+
*
|
|
4
|
+
* Detects text columns with BM25 indexes (via pg_textsearch) and generates
|
|
5
|
+
* BM25 relevance scoring. Wraps the same SQL logic as graphile-bm25.
|
|
6
|
+
*
|
|
7
|
+
* Requires the Bm25CodecPlugin to be loaded first (for index discovery).
|
|
8
|
+
* The adapter reads from the bm25IndexStore populated during the gather phase.
|
|
9
|
+
*/
|
|
10
|
+
import type { SearchAdapter } from '../types';
|
|
11
|
+
/**
|
|
12
|
+
* BM25 index info discovered during gather phase.
|
|
13
|
+
*/
|
|
14
|
+
export interface Bm25IndexInfo {
|
|
15
|
+
schemaName: string;
|
|
16
|
+
tableName: string;
|
|
17
|
+
columnName: string;
|
|
18
|
+
indexName: string;
|
|
19
|
+
}
|
|
20
|
+
export interface Bm25AdapterOptions {
|
|
21
|
+
/**
|
|
22
|
+
* Filter prefix for BM25 filter fields.
|
|
23
|
+
* @default 'bm25'
|
|
24
|
+
*/
|
|
25
|
+
filterPrefix?: string;
|
|
26
|
+
/**
|
|
27
|
+
* External BM25 index store. If not provided, the adapter will attempt
|
|
28
|
+
* to read from the build object's `pgBm25IndexStore`.
|
|
29
|
+
*/
|
|
30
|
+
bm25IndexStore?: Map<string, Bm25IndexInfo>;
|
|
31
|
+
}
|
|
32
|
+
export declare function createBm25Adapter(options?: Bm25AdapterOptions): SearchAdapter;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BM25 Search Adapter
|
|
3
|
+
*
|
|
4
|
+
* Detects text columns with BM25 indexes (via pg_textsearch) and generates
|
|
5
|
+
* BM25 relevance scoring. Wraps the same SQL logic as graphile-bm25.
|
|
6
|
+
*
|
|
7
|
+
* Requires the Bm25CodecPlugin to be loaded first (for index discovery).
|
|
8
|
+
* The adapter reads from the bm25IndexStore populated during the gather phase.
|
|
9
|
+
*/
|
|
10
|
+
import { bm25IndexStore as moduleBm25IndexStore } from '../codecs/bm25-codec';
|
|
11
|
+
function isTextCodec(codec) {
|
|
12
|
+
const name = codec?.name;
|
|
13
|
+
return name === 'text' || name === 'varchar' || name === 'bpchar';
|
|
14
|
+
}
|
|
15
|
+
export function createBm25Adapter(options = {}) {
|
|
16
|
+
const { filterPrefix = 'bm25', bm25IndexStore } = options;
|
|
17
|
+
function getIndexStore(build) {
|
|
18
|
+
if (bm25IndexStore)
|
|
19
|
+
return bm25IndexStore;
|
|
20
|
+
// Try build.pgBm25IndexStore (set by standalone Bm25SearchPlugin's build hook)
|
|
21
|
+
const buildStore = build.pgBm25IndexStore;
|
|
22
|
+
if (buildStore && buildStore.size > 0)
|
|
23
|
+
return buildStore;
|
|
24
|
+
// Fall back to module-level store populated by Bm25CodecPlugin's gather phase
|
|
25
|
+
if (moduleBm25IndexStore && moduleBm25IndexStore.size > 0)
|
|
26
|
+
return moduleBm25IndexStore;
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
function getBm25IndexForAttribute(codec, attributeName, build) {
|
|
30
|
+
const store = getIndexStore(build);
|
|
31
|
+
if (!store)
|
|
32
|
+
return undefined;
|
|
33
|
+
const pg = codec?.extensions?.pg;
|
|
34
|
+
if (!pg)
|
|
35
|
+
return undefined;
|
|
36
|
+
const key = `${pg.schemaName}.${pg.name}.${attributeName}`;
|
|
37
|
+
return store.get(key);
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
name: 'bm25',
|
|
41
|
+
scoreSemantics: {
|
|
42
|
+
metric: 'score',
|
|
43
|
+
lowerIsBetter: true,
|
|
44
|
+
range: null, // unbounded negative
|
|
45
|
+
},
|
|
46
|
+
filterPrefix,
|
|
47
|
+
supportsTextSearch: true,
|
|
48
|
+
buildTextSearchInput(text) {
|
|
49
|
+
// BM25 filter takes { query: string }
|
|
50
|
+
return { query: text };
|
|
51
|
+
},
|
|
52
|
+
detectColumns(codec, build) {
|
|
53
|
+
if (!codec?.attributes)
|
|
54
|
+
return [];
|
|
55
|
+
const columns = [];
|
|
56
|
+
for (const [attributeName, attribute] of Object.entries(codec.attributes)) {
|
|
57
|
+
if (!isTextCodec(attribute.codec))
|
|
58
|
+
continue;
|
|
59
|
+
const bm25Index = getBm25IndexForAttribute(codec, attributeName, build);
|
|
60
|
+
if (!bm25Index)
|
|
61
|
+
continue;
|
|
62
|
+
columns.push({ attributeName, adapterData: bm25Index });
|
|
63
|
+
}
|
|
64
|
+
return columns;
|
|
65
|
+
},
|
|
66
|
+
registerTypes(build) {
|
|
67
|
+
const { graphql: { GraphQLString, GraphQLFloat, GraphQLNonNull }, } = build;
|
|
68
|
+
// Register input type for BM25 search.
|
|
69
|
+
// Wrapped in try/catch because another plugin may have already
|
|
70
|
+
// registered 'Bm25SearchInput'. Graphile throws on duplicate
|
|
71
|
+
// registrations, so we catch and ignore.
|
|
72
|
+
try {
|
|
73
|
+
build.registerInputObjectType('Bm25SearchInput', {}, () => ({
|
|
74
|
+
description: 'Input for BM25 ranked text search. Provide a search query string and optional score threshold.',
|
|
75
|
+
fields: () => ({
|
|
76
|
+
query: {
|
|
77
|
+
type: new GraphQLNonNull(GraphQLString),
|
|
78
|
+
description: 'The search query text. Uses pg_textsearch BM25 ranking.',
|
|
79
|
+
},
|
|
80
|
+
threshold: {
|
|
81
|
+
type: GraphQLFloat,
|
|
82
|
+
description: 'Maximum BM25 score threshold (negative values). Only rows with score <= threshold are returned.',
|
|
83
|
+
},
|
|
84
|
+
}),
|
|
85
|
+
}), 'UnifiedSearchPlugin (bm25 adapter) registering Bm25SearchInput type');
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
// Already registered — safe to ignore
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
getFilterTypeName(_build) {
|
|
92
|
+
return 'Bm25SearchInput';
|
|
93
|
+
},
|
|
94
|
+
buildFilterApply(sql, alias, column, filterValue, _build) {
|
|
95
|
+
if (filterValue == null)
|
|
96
|
+
return null;
|
|
97
|
+
const { query, threshold } = filterValue;
|
|
98
|
+
if (!query || typeof query !== 'string' || query.trim().length === 0)
|
|
99
|
+
return null;
|
|
100
|
+
const bm25Index = column.adapterData;
|
|
101
|
+
const columnExpr = sql `${alias}.${sql.identifier(column.attributeName)}`;
|
|
102
|
+
// Use quoteQualifiedIdentifier to produce the qualified index name
|
|
103
|
+
const qualifiedIndexName = `"${bm25Index.schemaName}"."${bm25Index.indexName}"`;
|
|
104
|
+
const bm25queryExpr = sql `to_bm25query(${sql.value(query)}, ${sql.value(qualifiedIndexName)})`;
|
|
105
|
+
const scoreExpr = sql `(${columnExpr} <@> ${bm25queryExpr})`;
|
|
106
|
+
let whereClause = null;
|
|
107
|
+
if (threshold !== undefined && threshold !== null) {
|
|
108
|
+
whereClause = sql `${scoreExpr} < ${sql.value(threshold)}`;
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
whereClause,
|
|
112
|
+
scoreExpression: scoreExpr,
|
|
113
|
+
};
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Search Adapter Exports
|
|
3
|
+
*
|
|
4
|
+
* Each adapter implements the SearchAdapter interface for a specific
|
|
5
|
+
* search algorithm. They are plain objects — not Graphile plugins.
|
|
6
|
+
*/
|
|
7
|
+
export { createTsvectorAdapter } from './tsvector';
|
|
8
|
+
export type { TsvectorAdapterOptions } from './tsvector';
|
|
9
|
+
export { createBm25Adapter } from './bm25';
|
|
10
|
+
export type { Bm25AdapterOptions, Bm25IndexInfo } from './bm25';
|
|
11
|
+
export { createTrgmAdapter } from './trgm';
|
|
12
|
+
export type { TrgmAdapterOptions } from './trgm';
|
|
13
|
+
export { createPgvectorAdapter } from './pgvector';
|
|
14
|
+
export type { PgvectorAdapterOptions } from './pgvector';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Search Adapter Exports
|
|
3
|
+
*
|
|
4
|
+
* Each adapter implements the SearchAdapter interface for a specific
|
|
5
|
+
* search algorithm. They are plain objects — not Graphile plugins.
|
|
6
|
+
*/
|
|
7
|
+
export { createTsvectorAdapter } from './tsvector';
|
|
8
|
+
export { createBm25Adapter } from './bm25';
|
|
9
|
+
export { createTrgmAdapter } from './trgm';
|
|
10
|
+
export { createPgvectorAdapter } from './pgvector';
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pgvector Search Adapter
|
|
3
|
+
*
|
|
4
|
+
* Detects vector columns and generates distance-based scoring using
|
|
5
|
+
* pgvector operators (<=> cosine, <-> L2, <#> inner product).
|
|
6
|
+
* Wraps the same SQL logic as graphile-pgvector but as a SearchAdapter.
|
|
7
|
+
*/
|
|
8
|
+
import type { SearchAdapter } from '../types';
|
|
9
|
+
export interface PgvectorAdapterOptions {
|
|
10
|
+
/**
|
|
11
|
+
* Filter prefix for vector filter fields.
|
|
12
|
+
* @default 'vector'
|
|
13
|
+
*/
|
|
14
|
+
filterPrefix?: string;
|
|
15
|
+
/**
|
|
16
|
+
* Default similarity metric.
|
|
17
|
+
* @default 'COSINE'
|
|
18
|
+
*/
|
|
19
|
+
defaultMetric?: 'COSINE' | 'L2' | 'IP';
|
|
20
|
+
}
|
|
21
|
+
export declare function createPgvectorAdapter(options?: PgvectorAdapterOptions): SearchAdapter;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pgvector Search Adapter
|
|
3
|
+
*
|
|
4
|
+
* Detects vector columns and generates distance-based scoring using
|
|
5
|
+
* pgvector operators (<=> cosine, <-> L2, <#> inner product).
|
|
6
|
+
* Wraps the same SQL logic as graphile-pgvector but as a SearchAdapter.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* pgvector distance operators.
|
|
10
|
+
*/
|
|
11
|
+
const METRIC_OPERATORS = {
|
|
12
|
+
COSINE: '<=>',
|
|
13
|
+
L2: '<->',
|
|
14
|
+
IP: '<#>',
|
|
15
|
+
};
|
|
16
|
+
function isVectorCodec(codec) {
|
|
17
|
+
return codec?.name === 'vector';
|
|
18
|
+
}
|
|
19
|
+
export function createPgvectorAdapter(options = {}) {
|
|
20
|
+
const { filterPrefix = 'vector', defaultMetric = 'COSINE' } = options;
|
|
21
|
+
return {
|
|
22
|
+
name: 'vector',
|
|
23
|
+
scoreSemantics: {
|
|
24
|
+
metric: 'distance',
|
|
25
|
+
lowerIsBetter: true,
|
|
26
|
+
range: null, // 0 to infinity
|
|
27
|
+
},
|
|
28
|
+
filterPrefix,
|
|
29
|
+
supportsTextSearch: false,
|
|
30
|
+
// pgvector requires a vector array, not plain text — no buildTextSearchInput
|
|
31
|
+
detectColumns(codec, _build) {
|
|
32
|
+
if (!codec?.attributes)
|
|
33
|
+
return [];
|
|
34
|
+
const columns = [];
|
|
35
|
+
for (const [attributeName, attribute] of Object.entries(codec.attributes)) {
|
|
36
|
+
if (isVectorCodec(attribute.codec)) {
|
|
37
|
+
columns.push({ attributeName });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return columns;
|
|
41
|
+
},
|
|
42
|
+
registerTypes(build) {
|
|
43
|
+
const { graphql: { GraphQLList, GraphQLNonNull, GraphQLFloat }, } = build;
|
|
44
|
+
// Register types for vector search.
|
|
45
|
+
// Wrapped in try/catch because the standalone graphile-pgvector plugin may
|
|
46
|
+
// have already registered these types in its own init hook.
|
|
47
|
+
// Graphile throws on duplicate registrations, so we catch and ignore.
|
|
48
|
+
try {
|
|
49
|
+
build.registerEnumType('VectorMetric', {}, () => ({
|
|
50
|
+
description: 'Similarity metric for vector search',
|
|
51
|
+
values: {
|
|
52
|
+
COSINE: {
|
|
53
|
+
value: 'COSINE',
|
|
54
|
+
description: 'Cosine distance (1 - cosine similarity). Range: 0 (identical) to 2 (opposite).',
|
|
55
|
+
},
|
|
56
|
+
L2: {
|
|
57
|
+
value: 'L2',
|
|
58
|
+
description: 'Euclidean (L2) distance. Range: 0 (identical) to infinity.',
|
|
59
|
+
},
|
|
60
|
+
IP: {
|
|
61
|
+
value: 'IP',
|
|
62
|
+
description: 'Negative inner product. Higher (less negative) = more similar.',
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
}), 'UnifiedSearchPlugin (pgvector adapter) registering VectorMetric enum');
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// Already registered by standalone graphile-pgvector plugin — safe to ignore
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
build.registerInputObjectType('VectorNearbyInput', {}, () => ({
|
|
72
|
+
description: 'Input for vector similarity search. Provide a query vector, optional metric, and optional max distance threshold.',
|
|
73
|
+
fields: () => {
|
|
74
|
+
// getTypeByName is safe inside a thunk (fields callback) — called after init is complete
|
|
75
|
+
const VectorMetricEnum = build.getTypeByName('VectorMetric');
|
|
76
|
+
return {
|
|
77
|
+
vector: {
|
|
78
|
+
type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(GraphQLFloat))),
|
|
79
|
+
description: 'Query vector for similarity search.',
|
|
80
|
+
},
|
|
81
|
+
metric: {
|
|
82
|
+
type: VectorMetricEnum,
|
|
83
|
+
description: `Similarity metric to use (default: ${defaultMetric}).`,
|
|
84
|
+
},
|
|
85
|
+
distance: {
|
|
86
|
+
type: GraphQLFloat,
|
|
87
|
+
description: 'Maximum distance threshold. Only rows within this distance are returned.',
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
},
|
|
91
|
+
}), 'UnifiedSearchPlugin (pgvector adapter) registering VectorNearbyInput type');
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
// Already registered by standalone graphile-pgvector plugin — safe to ignore
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
getFilterTypeName(_build) {
|
|
98
|
+
return 'VectorNearbyInput';
|
|
99
|
+
},
|
|
100
|
+
buildFilterApply(sql, alias, column, filterValue, _build) {
|
|
101
|
+
if (filterValue == null)
|
|
102
|
+
return null;
|
|
103
|
+
const { vector, metric, distance } = filterValue;
|
|
104
|
+
if (!vector || !Array.isArray(vector) || vector.length === 0)
|
|
105
|
+
return null;
|
|
106
|
+
const resolvedMetric = metric || defaultMetric;
|
|
107
|
+
const operator = METRIC_OPERATORS[resolvedMetric] || METRIC_OPERATORS.COSINE;
|
|
108
|
+
const vectorString = `[${vector.join(',')}]`;
|
|
109
|
+
const columnExpr = sql `${alias}.${sql.identifier(column.attributeName)}`;
|
|
110
|
+
const vectorExpr = sql `${sql.value(vectorString)}::vector`;
|
|
111
|
+
const distanceExpr = sql `(${columnExpr} ${sql.raw(operator)} ${vectorExpr})`;
|
|
112
|
+
let whereClause = null;
|
|
113
|
+
if (distance !== undefined && distance !== null) {
|
|
114
|
+
whereClause = sql `${distanceExpr} <= ${sql.value(distance)}`;
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
whereClause,
|
|
118
|
+
scoreExpression: distanceExpr,
|
|
119
|
+
};
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pg_trgm Search Adapter
|
|
3
|
+
*
|
|
4
|
+
* Detects text/varchar columns and generates trigram similarity scoring.
|
|
5
|
+
* Wraps the same SQL logic as graphile-trgm but as a SearchAdapter.
|
|
6
|
+
*/
|
|
7
|
+
import type { SearchAdapter } from '../types';
|
|
8
|
+
export interface TrgmAdapterOptions {
|
|
9
|
+
/**
|
|
10
|
+
* Filter prefix for trgm filter fields.
|
|
11
|
+
* @default 'trgm'
|
|
12
|
+
*/
|
|
13
|
+
filterPrefix?: string;
|
|
14
|
+
/**
|
|
15
|
+
* Default similarity threshold (0..1). Higher = stricter matching.
|
|
16
|
+
* @default 0.3
|
|
17
|
+
*/
|
|
18
|
+
defaultThreshold?: number;
|
|
19
|
+
}
|
|
20
|
+
export declare function createTrgmAdapter(options?: TrgmAdapterOptions): SearchAdapter;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pg_trgm Search Adapter
|
|
3
|
+
*
|
|
4
|
+
* Detects text/varchar columns and generates trigram similarity scoring.
|
|
5
|
+
* Wraps the same SQL logic as graphile-trgm but as a SearchAdapter.
|
|
6
|
+
*/
|
|
7
|
+
function isTextCodec(codec) {
|
|
8
|
+
const name = codec?.name;
|
|
9
|
+
return name === 'text' || name === 'varchar' || name === 'bpchar';
|
|
10
|
+
}
|
|
11
|
+
export function createTrgmAdapter(options = {}) {
|
|
12
|
+
const { filterPrefix = 'trgm', defaultThreshold = 0.3 } = options;
|
|
13
|
+
return {
|
|
14
|
+
name: 'trgm',
|
|
15
|
+
scoreSemantics: {
|
|
16
|
+
metric: 'similarity',
|
|
17
|
+
lowerIsBetter: false,
|
|
18
|
+
range: [0, 1],
|
|
19
|
+
},
|
|
20
|
+
filterPrefix,
|
|
21
|
+
supportsTextSearch: true,
|
|
22
|
+
buildTextSearchInput(text) {
|
|
23
|
+
// trgm filter takes { value: string } — threshold uses adapter default
|
|
24
|
+
return { value: text };
|
|
25
|
+
},
|
|
26
|
+
detectColumns(codec, _build) {
|
|
27
|
+
if (!codec?.attributes)
|
|
28
|
+
return [];
|
|
29
|
+
const columns = [];
|
|
30
|
+
for (const [attributeName, attribute] of Object.entries(codec.attributes)) {
|
|
31
|
+
if (isTextCodec(attribute.codec)) {
|
|
32
|
+
columns.push({ attributeName });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return columns;
|
|
36
|
+
},
|
|
37
|
+
registerTypes(build) {
|
|
38
|
+
const { graphql: { GraphQLString, GraphQLFloat, GraphQLNonNull }, } = build;
|
|
39
|
+
// Register input type for trgm search.
|
|
40
|
+
// Wrapped in try/catch because the standalone graphile-trgm plugin may
|
|
41
|
+
// have already registered 'TrgmSearchInput' in its own init hook.
|
|
42
|
+
// Graphile throws on duplicate registrations, so we catch and ignore.
|
|
43
|
+
try {
|
|
44
|
+
build.registerInputObjectType('TrgmSearchInput', {}, () => ({
|
|
45
|
+
description: 'Input for pg_trgm fuzzy text matching. Provide a search value and optional similarity threshold.',
|
|
46
|
+
fields: () => ({
|
|
47
|
+
value: {
|
|
48
|
+
type: new GraphQLNonNull(GraphQLString),
|
|
49
|
+
description: 'The text to fuzzy-match against. Typos and misspellings are tolerated.',
|
|
50
|
+
},
|
|
51
|
+
threshold: {
|
|
52
|
+
type: GraphQLFloat,
|
|
53
|
+
description: `Minimum similarity threshold (0.0 to 1.0). Higher = stricter matching. Default is ${defaultThreshold}.`,
|
|
54
|
+
},
|
|
55
|
+
}),
|
|
56
|
+
}), 'UnifiedSearchPlugin (trgm adapter) registering TrgmSearchInput type');
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
// Already registered by standalone graphile-trgm plugin — safe to ignore
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
getFilterTypeName(_build) {
|
|
63
|
+
return 'TrgmSearchInput';
|
|
64
|
+
},
|
|
65
|
+
buildFilterApply(sql, alias, column, filterValue, _build) {
|
|
66
|
+
if (filterValue == null)
|
|
67
|
+
return null;
|
|
68
|
+
const { value, threshold } = filterValue;
|
|
69
|
+
if (!value || typeof value !== 'string' || value.trim().length === 0)
|
|
70
|
+
return null;
|
|
71
|
+
const th = threshold != null ? threshold : defaultThreshold;
|
|
72
|
+
const columnExpr = sql `${alias}.${sql.identifier(column.attributeName)}`;
|
|
73
|
+
const similarityExpr = sql `similarity(${columnExpr}, ${sql.value(value)})`;
|
|
74
|
+
return {
|
|
75
|
+
whereClause: sql `${similarityExpr} > ${sql.value(th)}`,
|
|
76
|
+
scoreExpression: similarityExpr,
|
|
77
|
+
};
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tsvector Search Adapter
|
|
3
|
+
*
|
|
4
|
+
* Detects tsvector columns and generates ts_rank-based scoring.
|
|
5
|
+
* Wraps the same SQL logic as graphile-tsvector but as a SearchAdapter.
|
|
6
|
+
*/
|
|
7
|
+
import type { SearchAdapter } from '../types';
|
|
8
|
+
export interface TsvectorAdapterOptions {
|
|
9
|
+
/**
|
|
10
|
+
* Filter prefix for tsvector filter fields.
|
|
11
|
+
* @default 'fullText'
|
|
12
|
+
*/
|
|
13
|
+
filterPrefix?: string;
|
|
14
|
+
/**
|
|
15
|
+
* PostgreSQL text search configuration (e.g. 'english', 'simple').
|
|
16
|
+
* @default 'english'
|
|
17
|
+
*/
|
|
18
|
+
tsConfig?: string;
|
|
19
|
+
}
|
|
20
|
+
export declare function createTsvectorAdapter(options?: TsvectorAdapterOptions): SearchAdapter;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tsvector Search Adapter
|
|
3
|
+
*
|
|
4
|
+
* Detects tsvector columns and generates ts_rank-based scoring.
|
|
5
|
+
* Wraps the same SQL logic as graphile-tsvector but as a SearchAdapter.
|
|
6
|
+
*/
|
|
7
|
+
function isTsvectorCodec(codec) {
|
|
8
|
+
return (codec?.extensions?.pg?.schemaName === 'pg_catalog' &&
|
|
9
|
+
codec?.extensions?.pg?.name === 'tsvector');
|
|
10
|
+
}
|
|
11
|
+
export function createTsvectorAdapter(options = {}) {
|
|
12
|
+
const { filterPrefix = 'tsv', tsConfig = 'english' } = options;
|
|
13
|
+
return {
|
|
14
|
+
name: 'tsv',
|
|
15
|
+
scoreSemantics: {
|
|
16
|
+
metric: 'rank',
|
|
17
|
+
lowerIsBetter: false,
|
|
18
|
+
range: [0, 1],
|
|
19
|
+
},
|
|
20
|
+
filterPrefix,
|
|
21
|
+
supportsTextSearch: true,
|
|
22
|
+
buildTextSearchInput(text) {
|
|
23
|
+
// tsvector filter takes a plain string
|
|
24
|
+
return text;
|
|
25
|
+
},
|
|
26
|
+
detectColumns(codec, _build) {
|
|
27
|
+
if (!codec?.attributes)
|
|
28
|
+
return [];
|
|
29
|
+
const columns = [];
|
|
30
|
+
for (const [attributeName, attribute] of Object.entries(codec.attributes)) {
|
|
31
|
+
if (isTsvectorCodec(attribute.codec)) {
|
|
32
|
+
columns.push({ attributeName });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return columns;
|
|
36
|
+
},
|
|
37
|
+
registerTypes(_build) {
|
|
38
|
+
// tsvector uses plain GraphQL String — no custom types needed
|
|
39
|
+
},
|
|
40
|
+
getFilterTypeName(_build) {
|
|
41
|
+
return 'String';
|
|
42
|
+
},
|
|
43
|
+
buildFilterApply(sql, alias, column, filterValue, _build) {
|
|
44
|
+
if (filterValue == null)
|
|
45
|
+
return null;
|
|
46
|
+
const val = typeof filterValue === 'string' ? filterValue : String(filterValue);
|
|
47
|
+
if (val.trim().length === 0)
|
|
48
|
+
return null;
|
|
49
|
+
const tsquery = sql `websearch_to_tsquery(${sql.literal(tsConfig)}, ${sql.value(val)})`;
|
|
50
|
+
const columnExpr = sql `${alias}.${sql.identifier(column.attributeName)}`;
|
|
51
|
+
return {
|
|
52
|
+
whereClause: sql `${columnExpr} @@ ${tsquery}`,
|
|
53
|
+
scoreExpression: sql `ts_rank(${columnExpr}, ${tsquery})`,
|
|
54
|
+
};
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bm25CodecPlugin
|
|
3
|
+
*
|
|
4
|
+
* Teaches PostGraphile v5 how to handle the pg_textsearch `bm25query` type
|
|
5
|
+
* and discovers all BM25 indexes in the database.
|
|
6
|
+
*
|
|
7
|
+
* This plugin:
|
|
8
|
+
* 1. Creates a codec for bm25query via gather.hooks.pgCodecs_findPgCodec
|
|
9
|
+
* 2. Discovers all BM25 indexes via gather.hooks.pgIntrospection_introspection
|
|
10
|
+
* by querying pg_index + pg_am + pg_class + pg_attribute
|
|
11
|
+
* 3. Stores discovered BM25 index info in a module-level Map for use by
|
|
12
|
+
* the BM25 adapter during the schema build phase
|
|
13
|
+
*/
|
|
14
|
+
import 'graphile-build-pg';
|
|
15
|
+
import type { GraphileConfig } from 'graphile-config';
|
|
16
|
+
/**
|
|
17
|
+
* Represents a discovered BM25 index in the database.
|
|
18
|
+
*/
|
|
19
|
+
export interface Bm25IndexInfo {
|
|
20
|
+
/** Schema name (e.g. 'public') */
|
|
21
|
+
schemaName: string;
|
|
22
|
+
/** Table name (e.g. 'documents') */
|
|
23
|
+
tableName: string;
|
|
24
|
+
/** Column name (e.g. 'content') */
|
|
25
|
+
columnName: string;
|
|
26
|
+
/** Index name (e.g. 'docs_idx') — needed for to_bm25query() */
|
|
27
|
+
indexName: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Module-level store for discovered BM25 indexes.
|
|
31
|
+
* Populated during the gather phase, read during the schema build phase.
|
|
32
|
+
*
|
|
33
|
+
* Key: "schemaName.tableName.columnName"
|
|
34
|
+
* Value: Bm25IndexInfo
|
|
35
|
+
*/
|
|
36
|
+
export declare const bm25IndexStore: Map<string, Bm25IndexInfo>;
|
|
37
|
+
/**
|
|
38
|
+
* Whether pg_textsearch extension was detected in the database.
|
|
39
|
+
*/
|
|
40
|
+
export declare let bm25ExtensionDetected: boolean;
|
|
41
|
+
export declare const Bm25CodecPlugin: GraphileConfig.Plugin;
|
|
42
|
+
export declare const Bm25CodecPreset: GraphileConfig.Preset;
|