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.
Files changed (59) hide show
  1. package/LICENSE +23 -0
  2. package/README.md +123 -0
  3. package/adapters/bm25.d.ts +32 -0
  4. package/adapters/bm25.js +119 -0
  5. package/adapters/index.d.ts +14 -0
  6. package/adapters/index.js +17 -0
  7. package/adapters/pgvector.d.ts +21 -0
  8. package/adapters/pgvector.js +125 -0
  9. package/adapters/trgm.d.ts +20 -0
  10. package/adapters/trgm.js +83 -0
  11. package/adapters/tsvector.d.ts +20 -0
  12. package/adapters/tsvector.js +60 -0
  13. package/codecs/bm25-codec.d.ts +42 -0
  14. package/codecs/bm25-codec.js +199 -0
  15. package/codecs/index.d.ts +12 -0
  16. package/codecs/index.js +22 -0
  17. package/codecs/operator-factories.d.ts +22 -0
  18. package/codecs/operator-factories.js +84 -0
  19. package/codecs/tsvector-codec.d.ts +53 -0
  20. package/codecs/tsvector-codec.js +162 -0
  21. package/codecs/vector-codec.d.ts +18 -0
  22. package/codecs/vector-codec.js +116 -0
  23. package/esm/adapters/bm25.d.ts +32 -0
  24. package/esm/adapters/bm25.js +116 -0
  25. package/esm/adapters/index.d.ts +14 -0
  26. package/esm/adapters/index.js +10 -0
  27. package/esm/adapters/pgvector.d.ts +21 -0
  28. package/esm/adapters/pgvector.js +122 -0
  29. package/esm/adapters/trgm.d.ts +20 -0
  30. package/esm/adapters/trgm.js +80 -0
  31. package/esm/adapters/tsvector.d.ts +20 -0
  32. package/esm/adapters/tsvector.js +57 -0
  33. package/esm/codecs/bm25-codec.d.ts +42 -0
  34. package/esm/codecs/bm25-codec.js +160 -0
  35. package/esm/codecs/index.d.ts +12 -0
  36. package/esm/codecs/index.js +10 -0
  37. package/esm/codecs/operator-factories.d.ts +22 -0
  38. package/esm/codecs/operator-factories.js +80 -0
  39. package/esm/codecs/tsvector-codec.d.ts +53 -0
  40. package/esm/codecs/tsvector-codec.js +155 -0
  41. package/esm/codecs/vector-codec.d.ts +18 -0
  42. package/esm/codecs/vector-codec.js +110 -0
  43. package/esm/index.d.ts +40 -0
  44. package/esm/index.js +41 -0
  45. package/esm/plugin.d.ts +50 -0
  46. package/esm/plugin.js +553 -0
  47. package/esm/preset.d.ts +79 -0
  48. package/esm/preset.js +82 -0
  49. package/esm/types.d.ts +171 -0
  50. package/esm/types.js +7 -0
  51. package/index.d.ts +40 -0
  52. package/index.js +60 -0
  53. package/package.json +66 -0
  54. package/plugin.d.ts +50 -0
  55. package/plugin.js +556 -0
  56. package/preset.d.ts +79 -0
  57. package/preset.js +85 -0
  58. package/types.d.ts +171 -0
  59. 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;