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
package/LICENSE ADDED
@@ -0,0 +1,23 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 Dan Lynch <pyramation@gmail.com>
4
+ Copyright (c) 2025 Constructive <developers@constructive.io>
5
+ Copyright (c) 2020-present, Interweb, Inc.
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ of this software and associated documentation files (the "Software"), to deal
9
+ in the Software without restriction, including without limitation the rights
10
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ copies of the Software, and to permit persons to whom the Software is
12
+ furnished to do so, subject to the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be included in all
15
+ copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,123 @@
1
+ # graphile-search
2
+
3
+ Unified PostGraphile v5 search plugin — abstracts tsvector, BM25, pg_trgm, and pgvector behind a single adapter-based architecture with composite `searchScore`.
4
+
5
+ ## Overview
6
+
7
+ Instead of separate plugins per algorithm, `graphile-search` uses an **adapter pattern** where each search algorithm (tsvector, BM25, pg_trgm, pgvector) implements a ~50-line adapter. A single core plugin iterates all adapters and wires them into the Graphile v5 hook system.
8
+
9
+ ## Usage
10
+
11
+ ```typescript
12
+ import { UnifiedSearchPreset } from 'graphile-search';
13
+
14
+ const preset = {
15
+ extends: [
16
+ UnifiedSearchPreset(),
17
+ ],
18
+ };
19
+ ```
20
+
21
+ ### Custom configuration
22
+
23
+ ```typescript
24
+ import { UnifiedSearchPreset } from 'graphile-search';
25
+
26
+ const preset = {
27
+ extends: [
28
+ UnifiedSearchPreset({
29
+ tsvector: { filterPrefix: 'fullText', tsConfig: 'english' },
30
+ bm25: true,
31
+ trgm: { defaultThreshold: 0.2 },
32
+ pgvector: { defaultMetric: 'COSINE' },
33
+ searchScoreWeights: { bm25: 0.5, trgm: 0.3, tsv: 0.2 },
34
+ }),
35
+ ],
36
+ };
37
+ ```
38
+
39
+ ## Features
40
+
41
+ - **4 search algorithms** via adapters: tsvector (ts_rank), BM25 (pg_textsearch), pg_trgm (similarity), pgvector (distance)
42
+ - **Per-algorithm score fields**: `{column}{Algorithm}{Metric}` (e.g. `bodyBm25Score`, `titleTrgmSimilarity`)
43
+ - **Composite `searchScore`**: Normalized 0..1 aggregating all active search signals
44
+ - **OrderBy enums**: `{COLUMN}_{ALGORITHM}_{METRIC}_ASC/DESC` + `SEARCH_SCORE_ASC/DESC`
45
+ - **Filter fields**: `{algorithm}{Column}` on connection filter input types
46
+ - **Hybrid search**: Combine multiple algorithms in a single query
47
+ - **Zero config**: Auto-discovers columns and indexes per adapter
48
+
49
+ ## Architecture
50
+
51
+ ```
52
+ ┌─────────────────────────────────────┐
53
+ │ Unified Search Plugin │
54
+ │ (iterates adapters, wires hooks) │
55
+ ├─────────────────────────────────────┤
56
+ │ Adapter: tsvector │ Adapter: bm25 │
57
+ │ Adapter: trgm │ Adapter: vector│
58
+ └─────────────────────────────────────┘
59
+ ```
60
+
61
+ Each adapter implements the `SearchAdapter` interface:
62
+ - `detectColumns()` — discover eligible columns on a table
63
+ - `registerTypes()` — register custom GraphQL input types
64
+ - `getFilterTypeName()` — return the filter input type name
65
+ - `buildFilterApply()` — generate WHERE + score SQL fragments
66
+
67
+ ---
68
+
69
+ ## Education and Tutorials
70
+
71
+ 1. 🚀 [Quickstart: Getting Up and Running](https://constructive.io/learn/quickstart)
72
+ Get started with modular databases in minutes. Install prerequisites and deploy your first module.
73
+
74
+ 2. 📦 [Modular PostgreSQL Development with Database Packages](https://constructive.io/learn/modular-postgres)
75
+ Learn to organize PostgreSQL projects with pgpm workspaces and reusable database modules.
76
+
77
+ 3. ✏️ [Authoring Database Changes](https://constructive.io/learn/authoring-database-changes)
78
+ Master the workflow for adding, organizing, and managing database changes with pgpm.
79
+
80
+ 4. 🧪 [End-to-End PostgreSQL Testing with TypeScript](https://constructive.io/learn/e2e-postgres-testing)
81
+ Master end-to-end PostgreSQL testing with ephemeral databases, RLS testing, and CI/CD automation.
82
+
83
+ 5. ⚡ [Supabase Testing](https://constructive.io/learn/supabase)
84
+ Use TypeScript-first tools to test Supabase projects with realistic RLS, policies, and auth contexts.
85
+
86
+ 6. 💧 [Drizzle ORM Testing](https://constructive.io/learn/drizzle-testing)
87
+ Run full-stack tests with Drizzle ORM, including database setup, teardown, and RLS enforcement.
88
+
89
+ 7. 🔧 [Troubleshooting](https://constructive.io/learn/troubleshooting)
90
+ Common issues and solutions for pgpm, PostgreSQL, and testing.
91
+
92
+ ## Related Constructive Tooling
93
+
94
+ ### 📦 Package Management
95
+
96
+ * [pgpm](https://github.com/constructive-io/constructive/tree/main/pgpm/pgpm): **🖥️ PostgreSQL Package Manager** for modular Postgres development. Works with database workspaces, scaffolding, migrations, seeding, and installing database packages.
97
+
98
+ ### 🧪 Testing
99
+
100
+ * [pgsql-test](https://github.com/constructive-io/constructive/tree/main/postgres/pgsql-test): **📊 Isolated testing environments** with per-test transaction rollbacks—ideal for integration tests, complex migrations, and RLS simulation.
101
+ * [pgsql-seed](https://github.com/constructive-io/constructive/tree/main/postgres/pgsql-seed): **🌱 PostgreSQL seeding utilities** for CSV, JSON, SQL data loading, and pgpm deployment.
102
+ * [supabase-test](https://github.com/constructive-io/constructive/tree/main/postgres/supabase-test): **🧪 Supabase-native test harness** preconfigured for the local Supabase stack—per-test rollbacks, JWT/role context helpers, and CI/GitHub Actions ready.
103
+ * [graphile-test](https://github.com/constructive-io/constructive/tree/main/graphile/graphile-test): **🔐 Authentication mocking** for Graphile-focused test helpers and emulating row-level security contexts.
104
+ * [pg-query-context](https://github.com/constructive-io/constructive/tree/main/postgres/pg-query-context): **🔒 Session context injection** to add session-local context (e.g., `SET LOCAL`) into queries—ideal for setting `role`, `jwt.claims`, and other session settings.
105
+
106
+ ### 🧠 Parsing & AST
107
+
108
+ * [pgsql-parser](https://www.npmjs.com/package/pgsql-parser): **🔄 SQL conversion engine** that interprets and converts PostgreSQL syntax.
109
+ * [libpg-query-node](https://www.npmjs.com/package/libpg-query): **🌉 Node.js bindings** for `libpg_query`, converting SQL into parse trees.
110
+ * [pg-proto-parser](https://www.npmjs.com/package/pg-proto-parser): **📦 Protobuf parser** for parsing PostgreSQL Protocol Buffers definitions to generate TypeScript interfaces, utility functions, and JSON mappings for enums.
111
+ * [@pgsql/enums](https://www.npmjs.com/package/@pgsql/enums): **🏷️ TypeScript enums** for PostgreSQL AST for safe and ergonomic parsing logic.
112
+ * [@pgsql/types](https://www.npmjs.com/package/@pgsql/types): **📝 Type definitions** for PostgreSQL AST nodes in TypeScript.
113
+ * [@pgsql/utils](https://www.npmjs.com/package/@pgsql/utils): **🛠️ AST utilities** for constructing and transforming PostgreSQL syntax trees.
114
+
115
+ ## Credits
116
+
117
+ **🛠 Built by the [Constructive](https://constructive.io) team — creators of modular Postgres tooling for secure, composable backends. If you like our work, contribute on [GitHub](https://github.com/constructive-io).**
118
+
119
+ ## Disclaimer
120
+
121
+ AS DESCRIBED IN THE LICENSES, THE SOFTWARE IS PROVIDED "AS IS", AT YOUR OWN RISK, AND WITHOUT WARRANTIES OF ANY KIND.
122
+
123
+ No developer or entity involved in creating this software will be liable for any claims or damages whatsoever associated with your use, inability to use, or your interaction with other users of the code, including any direct, indirect, incidental, special, exemplary, punitive or consequential damages, or loss of profits, cryptocurrencies, tokens, or anything else of value.
@@ -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,119 @@
1
+ "use strict";
2
+ /**
3
+ * BM25 Search Adapter
4
+ *
5
+ * Detects text columns with BM25 indexes (via pg_textsearch) and generates
6
+ * BM25 relevance scoring. Wraps the same SQL logic as graphile-bm25.
7
+ *
8
+ * Requires the Bm25CodecPlugin to be loaded first (for index discovery).
9
+ * The adapter reads from the bm25IndexStore populated during the gather phase.
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.createBm25Adapter = createBm25Adapter;
13
+ const bm25_codec_1 = require("../codecs/bm25-codec");
14
+ function isTextCodec(codec) {
15
+ const name = codec?.name;
16
+ return name === 'text' || name === 'varchar' || name === 'bpchar';
17
+ }
18
+ function createBm25Adapter(options = {}) {
19
+ const { filterPrefix = 'bm25', bm25IndexStore } = options;
20
+ function getIndexStore(build) {
21
+ if (bm25IndexStore)
22
+ return bm25IndexStore;
23
+ // Try build.pgBm25IndexStore (set by standalone Bm25SearchPlugin's build hook)
24
+ const buildStore = build.pgBm25IndexStore;
25
+ if (buildStore && buildStore.size > 0)
26
+ return buildStore;
27
+ // Fall back to module-level store populated by Bm25CodecPlugin's gather phase
28
+ if (bm25_codec_1.bm25IndexStore && bm25_codec_1.bm25IndexStore.size > 0)
29
+ return bm25_codec_1.bm25IndexStore;
30
+ return undefined;
31
+ }
32
+ function getBm25IndexForAttribute(codec, attributeName, build) {
33
+ const store = getIndexStore(build);
34
+ if (!store)
35
+ return undefined;
36
+ const pg = codec?.extensions?.pg;
37
+ if (!pg)
38
+ return undefined;
39
+ const key = `${pg.schemaName}.${pg.name}.${attributeName}`;
40
+ return store.get(key);
41
+ }
42
+ return {
43
+ name: 'bm25',
44
+ scoreSemantics: {
45
+ metric: 'score',
46
+ lowerIsBetter: true,
47
+ range: null, // unbounded negative
48
+ },
49
+ filterPrefix,
50
+ supportsTextSearch: true,
51
+ buildTextSearchInput(text) {
52
+ // BM25 filter takes { query: string }
53
+ return { query: text };
54
+ },
55
+ detectColumns(codec, build) {
56
+ if (!codec?.attributes)
57
+ return [];
58
+ const columns = [];
59
+ for (const [attributeName, attribute] of Object.entries(codec.attributes)) {
60
+ if (!isTextCodec(attribute.codec))
61
+ continue;
62
+ const bm25Index = getBm25IndexForAttribute(codec, attributeName, build);
63
+ if (!bm25Index)
64
+ continue;
65
+ columns.push({ attributeName, adapterData: bm25Index });
66
+ }
67
+ return columns;
68
+ },
69
+ registerTypes(build) {
70
+ const { graphql: { GraphQLString, GraphQLFloat, GraphQLNonNull }, } = build;
71
+ // Register input type for BM25 search.
72
+ // Wrapped in try/catch because another plugin may have already
73
+ // registered 'Bm25SearchInput'. Graphile throws on duplicate
74
+ // registrations, so we catch and ignore.
75
+ try {
76
+ build.registerInputObjectType('Bm25SearchInput', {}, () => ({
77
+ description: 'Input for BM25 ranked text search. Provide a search query string and optional score threshold.',
78
+ fields: () => ({
79
+ query: {
80
+ type: new GraphQLNonNull(GraphQLString),
81
+ description: 'The search query text. Uses pg_textsearch BM25 ranking.',
82
+ },
83
+ threshold: {
84
+ type: GraphQLFloat,
85
+ description: 'Maximum BM25 score threshold (negative values). Only rows with score <= threshold are returned.',
86
+ },
87
+ }),
88
+ }), 'UnifiedSearchPlugin (bm25 adapter) registering Bm25SearchInput type');
89
+ }
90
+ catch {
91
+ // Already registered — safe to ignore
92
+ }
93
+ },
94
+ getFilterTypeName(_build) {
95
+ return 'Bm25SearchInput';
96
+ },
97
+ buildFilterApply(sql, alias, column, filterValue, _build) {
98
+ if (filterValue == null)
99
+ return null;
100
+ const { query, threshold } = filterValue;
101
+ if (!query || typeof query !== 'string' || query.trim().length === 0)
102
+ return null;
103
+ const bm25Index = column.adapterData;
104
+ const columnExpr = sql `${alias}.${sql.identifier(column.attributeName)}`;
105
+ // Use quoteQualifiedIdentifier to produce the qualified index name
106
+ const qualifiedIndexName = `"${bm25Index.schemaName}"."${bm25Index.indexName}"`;
107
+ const bm25queryExpr = sql `to_bm25query(${sql.value(query)}, ${sql.value(qualifiedIndexName)})`;
108
+ const scoreExpr = sql `(${columnExpr} <@> ${bm25queryExpr})`;
109
+ let whereClause = null;
110
+ if (threshold !== undefined && threshold !== null) {
111
+ whereClause = sql `${scoreExpr} < ${sql.value(threshold)}`;
112
+ }
113
+ return {
114
+ whereClause,
115
+ scoreExpression: scoreExpr,
116
+ };
117
+ },
118
+ };
119
+ }
@@ -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,17 @@
1
+ "use strict";
2
+ /**
3
+ * Search Adapter Exports
4
+ *
5
+ * Each adapter implements the SearchAdapter interface for a specific
6
+ * search algorithm. They are plain objects — not Graphile plugins.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.createPgvectorAdapter = exports.createTrgmAdapter = exports.createBm25Adapter = exports.createTsvectorAdapter = void 0;
10
+ var tsvector_1 = require("./tsvector");
11
+ Object.defineProperty(exports, "createTsvectorAdapter", { enumerable: true, get: function () { return tsvector_1.createTsvectorAdapter; } });
12
+ var bm25_1 = require("./bm25");
13
+ Object.defineProperty(exports, "createBm25Adapter", { enumerable: true, get: function () { return bm25_1.createBm25Adapter; } });
14
+ var trgm_1 = require("./trgm");
15
+ Object.defineProperty(exports, "createTrgmAdapter", { enumerable: true, get: function () { return trgm_1.createTrgmAdapter; } });
16
+ var pgvector_1 = require("./pgvector");
17
+ Object.defineProperty(exports, "createPgvectorAdapter", { enumerable: true, get: function () { return pgvector_1.createPgvectorAdapter; } });
@@ -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,125 @@
1
+ "use strict";
2
+ /**
3
+ * pgvector Search Adapter
4
+ *
5
+ * Detects vector columns and generates distance-based scoring using
6
+ * pgvector operators (<=> cosine, <-> L2, <#> inner product).
7
+ * Wraps the same SQL logic as graphile-pgvector but as a SearchAdapter.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.createPgvectorAdapter = createPgvectorAdapter;
11
+ /**
12
+ * pgvector distance operators.
13
+ */
14
+ const METRIC_OPERATORS = {
15
+ COSINE: '<=>',
16
+ L2: '<->',
17
+ IP: '<#>',
18
+ };
19
+ function isVectorCodec(codec) {
20
+ return codec?.name === 'vector';
21
+ }
22
+ function createPgvectorAdapter(options = {}) {
23
+ const { filterPrefix = 'vector', defaultMetric = 'COSINE' } = options;
24
+ return {
25
+ name: 'vector',
26
+ scoreSemantics: {
27
+ metric: 'distance',
28
+ lowerIsBetter: true,
29
+ range: null, // 0 to infinity
30
+ },
31
+ filterPrefix,
32
+ supportsTextSearch: false,
33
+ // pgvector requires a vector array, not plain text — no buildTextSearchInput
34
+ detectColumns(codec, _build) {
35
+ if (!codec?.attributes)
36
+ return [];
37
+ const columns = [];
38
+ for (const [attributeName, attribute] of Object.entries(codec.attributes)) {
39
+ if (isVectorCodec(attribute.codec)) {
40
+ columns.push({ attributeName });
41
+ }
42
+ }
43
+ return columns;
44
+ },
45
+ registerTypes(build) {
46
+ const { graphql: { GraphQLList, GraphQLNonNull, GraphQLFloat }, } = build;
47
+ // Register types for vector search.
48
+ // Wrapped in try/catch because the standalone graphile-pgvector plugin may
49
+ // have already registered these types in its own init hook.
50
+ // Graphile throws on duplicate registrations, so we catch and ignore.
51
+ try {
52
+ build.registerEnumType('VectorMetric', {}, () => ({
53
+ description: 'Similarity metric for vector search',
54
+ values: {
55
+ COSINE: {
56
+ value: 'COSINE',
57
+ description: 'Cosine distance (1 - cosine similarity). Range: 0 (identical) to 2 (opposite).',
58
+ },
59
+ L2: {
60
+ value: 'L2',
61
+ description: 'Euclidean (L2) distance. Range: 0 (identical) to infinity.',
62
+ },
63
+ IP: {
64
+ value: 'IP',
65
+ description: 'Negative inner product. Higher (less negative) = more similar.',
66
+ },
67
+ },
68
+ }), 'UnifiedSearchPlugin (pgvector adapter) registering VectorMetric enum');
69
+ }
70
+ catch {
71
+ // Already registered by standalone graphile-pgvector plugin — safe to ignore
72
+ }
73
+ try {
74
+ build.registerInputObjectType('VectorNearbyInput', {}, () => ({
75
+ description: 'Input for vector similarity search. Provide a query vector, optional metric, and optional max distance threshold.',
76
+ fields: () => {
77
+ // getTypeByName is safe inside a thunk (fields callback) — called after init is complete
78
+ const VectorMetricEnum = build.getTypeByName('VectorMetric');
79
+ return {
80
+ vector: {
81
+ type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(GraphQLFloat))),
82
+ description: 'Query vector for similarity search.',
83
+ },
84
+ metric: {
85
+ type: VectorMetricEnum,
86
+ description: `Similarity metric to use (default: ${defaultMetric}).`,
87
+ },
88
+ distance: {
89
+ type: GraphQLFloat,
90
+ description: 'Maximum distance threshold. Only rows within this distance are returned.',
91
+ },
92
+ };
93
+ },
94
+ }), 'UnifiedSearchPlugin (pgvector adapter) registering VectorNearbyInput type');
95
+ }
96
+ catch {
97
+ // Already registered by standalone graphile-pgvector plugin — safe to ignore
98
+ }
99
+ },
100
+ getFilterTypeName(_build) {
101
+ return 'VectorNearbyInput';
102
+ },
103
+ buildFilterApply(sql, alias, column, filterValue, _build) {
104
+ if (filterValue == null)
105
+ return null;
106
+ const { vector, metric, distance } = filterValue;
107
+ if (!vector || !Array.isArray(vector) || vector.length === 0)
108
+ return null;
109
+ const resolvedMetric = metric || defaultMetric;
110
+ const operator = METRIC_OPERATORS[resolvedMetric] || METRIC_OPERATORS.COSINE;
111
+ const vectorString = `[${vector.join(',')}]`;
112
+ const columnExpr = sql `${alias}.${sql.identifier(column.attributeName)}`;
113
+ const vectorExpr = sql `${sql.value(vectorString)}::vector`;
114
+ const distanceExpr = sql `(${columnExpr} ${sql.raw(operator)} ${vectorExpr})`;
115
+ let whereClause = null;
116
+ if (distance !== undefined && distance !== null) {
117
+ whereClause = sql `${distanceExpr} <= ${sql.value(distance)}`;
118
+ }
119
+ return {
120
+ whereClause,
121
+ scoreExpression: distanceExpr,
122
+ };
123
+ },
124
+ };
125
+ }
@@ -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,83 @@
1
+ "use strict";
2
+ /**
3
+ * pg_trgm Search Adapter
4
+ *
5
+ * Detects text/varchar columns and generates trigram similarity scoring.
6
+ * Wraps the same SQL logic as graphile-trgm but as a SearchAdapter.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.createTrgmAdapter = createTrgmAdapter;
10
+ function isTextCodec(codec) {
11
+ const name = codec?.name;
12
+ return name === 'text' || name === 'varchar' || name === 'bpchar';
13
+ }
14
+ function createTrgmAdapter(options = {}) {
15
+ const { filterPrefix = 'trgm', defaultThreshold = 0.3 } = options;
16
+ return {
17
+ name: 'trgm',
18
+ scoreSemantics: {
19
+ metric: 'similarity',
20
+ lowerIsBetter: false,
21
+ range: [0, 1],
22
+ },
23
+ filterPrefix,
24
+ supportsTextSearch: true,
25
+ buildTextSearchInput(text) {
26
+ // trgm filter takes { value: string } — threshold uses adapter default
27
+ return { value: text };
28
+ },
29
+ detectColumns(codec, _build) {
30
+ if (!codec?.attributes)
31
+ return [];
32
+ const columns = [];
33
+ for (const [attributeName, attribute] of Object.entries(codec.attributes)) {
34
+ if (isTextCodec(attribute.codec)) {
35
+ columns.push({ attributeName });
36
+ }
37
+ }
38
+ return columns;
39
+ },
40
+ registerTypes(build) {
41
+ const { graphql: { GraphQLString, GraphQLFloat, GraphQLNonNull }, } = build;
42
+ // Register input type for trgm search.
43
+ // Wrapped in try/catch because the standalone graphile-trgm plugin may
44
+ // have already registered 'TrgmSearchInput' in its own init hook.
45
+ // Graphile throws on duplicate registrations, so we catch and ignore.
46
+ try {
47
+ build.registerInputObjectType('TrgmSearchInput', {}, () => ({
48
+ description: 'Input for pg_trgm fuzzy text matching. Provide a search value and optional similarity threshold.',
49
+ fields: () => ({
50
+ value: {
51
+ type: new GraphQLNonNull(GraphQLString),
52
+ description: 'The text to fuzzy-match against. Typos and misspellings are tolerated.',
53
+ },
54
+ threshold: {
55
+ type: GraphQLFloat,
56
+ description: `Minimum similarity threshold (0.0 to 1.0). Higher = stricter matching. Default is ${defaultThreshold}.`,
57
+ },
58
+ }),
59
+ }), 'UnifiedSearchPlugin (trgm adapter) registering TrgmSearchInput type');
60
+ }
61
+ catch {
62
+ // Already registered by standalone graphile-trgm plugin — safe to ignore
63
+ }
64
+ },
65
+ getFilterTypeName(_build) {
66
+ return 'TrgmSearchInput';
67
+ },
68
+ buildFilterApply(sql, alias, column, filterValue, _build) {
69
+ if (filterValue == null)
70
+ return null;
71
+ const { value, threshold } = filterValue;
72
+ if (!value || typeof value !== 'string' || value.trim().length === 0)
73
+ return null;
74
+ const th = threshold != null ? threshold : defaultThreshold;
75
+ const columnExpr = sql `${alias}.${sql.identifier(column.attributeName)}`;
76
+ const similarityExpr = sql `similarity(${columnExpr}, ${sql.value(value)})`;
77
+ return {
78
+ whereClause: sql `${similarityExpr} > ${sql.value(th)}`,
79
+ scoreExpression: similarityExpr,
80
+ };
81
+ },
82
+ };
83
+ }
@@ -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,60 @@
1
+ "use strict";
2
+ /**
3
+ * tsvector Search Adapter
4
+ *
5
+ * Detects tsvector columns and generates ts_rank-based scoring.
6
+ * Wraps the same SQL logic as graphile-tsvector but as a SearchAdapter.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.createTsvectorAdapter = createTsvectorAdapter;
10
+ function isTsvectorCodec(codec) {
11
+ return (codec?.extensions?.pg?.schemaName === 'pg_catalog' &&
12
+ codec?.extensions?.pg?.name === 'tsvector');
13
+ }
14
+ function createTsvectorAdapter(options = {}) {
15
+ const { filterPrefix = 'tsv', tsConfig = 'english' } = options;
16
+ return {
17
+ name: 'tsv',
18
+ scoreSemantics: {
19
+ metric: 'rank',
20
+ lowerIsBetter: false,
21
+ range: [0, 1],
22
+ },
23
+ filterPrefix,
24
+ supportsTextSearch: true,
25
+ buildTextSearchInput(text) {
26
+ // tsvector filter takes a plain string
27
+ return text;
28
+ },
29
+ detectColumns(codec, _build) {
30
+ if (!codec?.attributes)
31
+ return [];
32
+ const columns = [];
33
+ for (const [attributeName, attribute] of Object.entries(codec.attributes)) {
34
+ if (isTsvectorCodec(attribute.codec)) {
35
+ columns.push({ attributeName });
36
+ }
37
+ }
38
+ return columns;
39
+ },
40
+ registerTypes(_build) {
41
+ // tsvector uses plain GraphQL String — no custom types needed
42
+ },
43
+ getFilterTypeName(_build) {
44
+ return 'String';
45
+ },
46
+ buildFilterApply(sql, alias, column, filterValue, _build) {
47
+ if (filterValue == null)
48
+ return null;
49
+ const val = typeof filterValue === 'string' ? filterValue : String(filterValue);
50
+ if (val.trim().length === 0)
51
+ return null;
52
+ const tsquery = sql `websearch_to_tsquery(${sql.literal(tsConfig)}, ${sql.value(val)})`;
53
+ const columnExpr = sql `${alias}.${sql.identifier(column.attributeName)}`;
54
+ return {
55
+ whereClause: sql `${columnExpr} @@ ${tsquery}`,
56
+ scoreExpression: sql `ts_rank(${columnExpr}, ${tsquery})`,
57
+ };
58
+ },
59
+ };
60
+ }