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
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;
|
package/adapters/bm25.js
ADDED
|
@@ -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;
|
package/adapters/trgm.js
ADDED
|
@@ -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
|
+
}
|