From d2776701ea8784e148a8dca257ade7f2b4f70b84 Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Thu, 2 Apr 2026 04:51:42 +0000 Subject: [PATCH] rename fullTextSearch composite filter to unifiedSearch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rename the composite GraphQL filter field from fullTextSearch to unifiedSearch across the plugin, codegen, tests, and skills to disambiguate it from the tsvector-specific fullTextSearchTsv filter. - Plugin: rename field, config option enableFullTextSearch → enableUnifiedSearch - Codegen: update CLI example docs in docs-utils.ts - Tests: update all GraphQL queries and assertions - Skills: update CLI examples in user.md references The full_text_search metaschema DB table and tsvector adapter naming (fullTextSearchTsv, filterPrefix) are intentionally left unchanged. --- .agents/skills/cli-auth/references/user.md | 6 +-- .agents/skills/cli-public/references/user.md | 6 +-- .../search-config-integration.test.ts | 4 +- .../src/__tests__/unified-search.test.ts | 54 +++++++++---------- graphile/graphile-search/src/plugin.ts | 14 ++--- graphile/graphile-search/src/preset.ts | 8 +-- graphile/graphile-search/src/types.ts | 10 ++-- .../codegen/src/core/codegen/docs-utils.ts | 8 +-- graphql/server-test/__tests__/cli-e2e.test.ts | 20 +++---- .../__tests__/search.integration.test.ts | 34 ++++++------ 10 files changed, 82 insertions(+), 82 deletions(-) diff --git a/.agents/skills/cli-auth/references/user.md b/.agents/skills/cli-auth/references/user.md index 6c0e4081d..664de9176 100644 --- a/.agents/skills/cli-auth/references/user.md +++ b/.agents/skills/cli-auth/references/user.md @@ -71,16 +71,16 @@ csdk user list --where.searchTsv "search query" --select title,tsvRank csdk user list --where.trgmDisplayName.value "approximate query" --where.trgmDisplayName.threshold 0.3 --select title,displayNameTrgmSimilarity ``` -### Composite search (fullTextSearch dispatches to all text adapters) +### Composite search (unifiedSearch dispatches to all text adapters) ```bash -csdk user list --where.fullTextSearch "search query" --select title,tsvRank,displayNameTrgmSimilarity,searchScore +csdk user list --where.unifiedSearch "search query" --select title,tsvRank,displayNameTrgmSimilarity,searchScore ``` ### Search with pagination and field projection ```bash -csdk user list --where.fullTextSearch "query" --limit 10 --select id,title,searchScore +csdk user list --where.unifiedSearch "query" --limit 10 --select id,title,searchScore csdk user search "query" --limit 10 --select id,title,searchScore ``` diff --git a/.agents/skills/cli-public/references/user.md b/.agents/skills/cli-public/references/user.md index 6c0e4081d..664de9176 100644 --- a/.agents/skills/cli-public/references/user.md +++ b/.agents/skills/cli-public/references/user.md @@ -71,16 +71,16 @@ csdk user list --where.searchTsv "search query" --select title,tsvRank csdk user list --where.trgmDisplayName.value "approximate query" --where.trgmDisplayName.threshold 0.3 --select title,displayNameTrgmSimilarity ``` -### Composite search (fullTextSearch dispatches to all text adapters) +### Composite search (unifiedSearch dispatches to all text adapters) ```bash -csdk user list --where.fullTextSearch "search query" --select title,tsvRank,displayNameTrgmSimilarity,searchScore +csdk user list --where.unifiedSearch "search query" --select title,tsvRank,displayNameTrgmSimilarity,searchScore ``` ### Search with pagination and field projection ```bash -csdk user list --where.fullTextSearch "query" --limit 10 --select id,title,searchScore +csdk user list --where.unifiedSearch "query" --limit 10 --select id,title,searchScore csdk user search "query" --limit 10 --select id,title,searchScore ``` diff --git a/graphile/graphile-search/src/__tests__/search-config-integration.test.ts b/graphile/graphile-search/src/__tests__/search-config-integration.test.ts index 84b8afa42..405cb93aa 100644 --- a/graphile/graphile-search/src/__tests__/search-config-integration.test.ts +++ b/graphile/graphile-search/src/__tests__/search-config-integration.test.ts @@ -113,7 +113,7 @@ describe('@searchConfig integration tests', () => { createPgvectorAdapter(), ], enableSearchScore: true, - enableFullTextSearch: true, + enableUnifiedSearch: true, }); // Inject @searchConfig on the articles table with custom weights @@ -289,7 +289,7 @@ describe('@searchConfig with sigmoid normalization', () => { createPgvectorAdapter(), ], enableSearchScore: true, - enableFullTextSearch: true, + enableUnifiedSearch: true, }); // Inject @searchConfig with sigmoid normalization diff --git a/graphile/graphile-search/src/__tests__/unified-search.test.ts b/graphile/graphile-search/src/__tests__/unified-search.test.ts index 6f0912287..86973af45 100644 --- a/graphile/graphile-search/src/__tests__/unified-search.test.ts +++ b/graphile/graphile-search/src/__tests__/unified-search.test.ts @@ -58,7 +58,7 @@ describe('graphile-search (unified search plugin)', () => { createPgvectorAdapter(), ], enableSearchScore: true, - enableFullTextSearch: true, + enableUnifiedSearch: true, }); const testPreset = { @@ -585,8 +585,8 @@ describe('graphile-search (unified search plugin)', () => { expect(limitNodes[1].rowId).toBe(allNodes[1].rowId); }); - it('fullTextSearch + per-adapter orderBy: LIMIT returns top results', async () => { - // fullTextSearch dispatches to all text-compatible adapters. + it('unifiedSearch + per-adapter orderBy: LIMIT returns top results', async () => { + // unifiedSearch dispatches to all text-compatible adapters. // Per-adapter orderBy (e.g. BM25 score) still works correctly with LIMIT // because the adapter score is a SQL-level expression. // (Note: SEARCH_SCORE is a JS-computed composite and does not produce @@ -594,7 +594,7 @@ describe('graphile-search (unified search plugin)', () => { const allResult = await query(` query { allDocuments( - where: { fullTextSearch: "machine learning" } + where: { unifiedSearch: "machine learning" } orderBy: BODY_BM25_SCORE_ASC ) { nodes { rowId title bodyBm25Score } @@ -609,7 +609,7 @@ describe('graphile-search (unified search plugin)', () => { const limitResult = await query(` query { allDocuments( - where: { fullTextSearch: "machine learning" } + where: { unifiedSearch: "machine learning" } orderBy: BODY_BM25_SCORE_ASC first: 1 ) { @@ -732,8 +732,8 @@ describe('graphile-search (unified search plugin)', () => { } }); - it('mega query v2: fullTextSearch + searchScore with composite ordering', async () => { - // Mega Query v2 — New-style: uses the unified `fullTextSearch` composite + it('mega query v2: unifiedSearch + searchScore with composite ordering', async () => { + // Mega Query v2 — New-style: uses the unified `unifiedSearch` composite // filter that fans out to all text-compatible algorithms (tsvector, BM25, trgm) // with a single string, plus a manual pgvector filter for semantic search. // Orders by composite searchScore (highest overall relevance first). @@ -741,9 +741,9 @@ describe('graphile-search (unified search plugin)', () => { query MegaQueryV2_UnifiedSearch { allDocuments( where: { - # fullTextSearch: single string fans out to tsvector + BM25 + trgm + # unifiedSearch: single string fans out to tsvector + BM25 + trgm # automatically — no need to specify each algorithm separately - fullTextSearch: "machine learning" + unifiedSearch: "machine learning" # pgvector still needs its own filter (vectors aren't text) vectorEmbedding: { vector: [1, 0, 0], metric: COSINE } @@ -757,7 +757,7 @@ describe('graphile-search (unified search plugin)', () => { title body - # Per-adapter scores — populated by fullTextSearch for text algorithms + # Per-adapter scores — populated by unifiedSearch for text algorithms tsvRank bodyBm25Score titleTrgmSimilarity @@ -788,14 +788,14 @@ describe('graphile-search (unified search plugin)', () => { }); }); - // ─── fullTextSearch composite filter ──────────────────────────────────── + // ─── unifiedSearch composite filter ──────────────────────────────────── - describe('fullTextSearch composite filter', () => { - it('fullTextSearch field exists on the filter type', async () => { + describe('unifiedSearch composite filter', () => { + it('unifiedSearch field exists on the filter type', async () => { const result = await query(` query { allDocuments(where: { - fullTextSearch: "learning" + unifiedSearch: "learning" }) { nodes { title @@ -813,7 +813,7 @@ describe('graphile-search (unified search plugin)', () => { const result = await query(` query { allDocuments(where: { - fullTextSearch: "machine learning" + unifiedSearch: "machine learning" }) { nodes { title @@ -843,7 +843,7 @@ describe('graphile-search (unified search plugin)', () => { const result = await query(` query { allDocuments(where: { - fullTextSearch: "learning" + unifiedSearch: "learning" tsvTsv: "machine" }) { nodes { @@ -856,7 +856,7 @@ describe('graphile-search (unified search plugin)', () => { expect(result.errors).toBeUndefined(); const nodes = result.data?.allDocuments?.nodes ?? []; - // The algorithm-specific filter (tsvTsv) narrows further within the fullTextSearch results + // The algorithm-specific filter (tsvTsv) narrows further within the unifiedSearch results expect(nodes).toBeDefined(); }); @@ -864,7 +864,7 @@ describe('graphile-search (unified search plugin)', () => { const result = await query(` query { allDocuments(where: { - fullTextSearch: "xyzzy_nonexistent_term_12345" + unifiedSearch: "xyzzy_nonexistent_term_12345" }) { nodes { title @@ -908,14 +908,14 @@ describe('graphile-search (unified search plugin)', () => { }); }); -// ─── fullTextSearch disabled (separate suite — needs its own DB connection) ─── +// ─── unifiedSearch disabled (separate suite — needs its own DB connection) ─── -describe('fullTextSearch can be disabled', () => { +describe('unifiedSearch can be disabled', () => { let disabledQuery: QueryFn; let disabledTeardown: (() => Promise) | undefined; beforeAll(async () => { - // Build a plugin with fullTextSearch DISABLED + // Build a plugin with unifiedSearch DISABLED const disabledPlugin = createUnifiedSearchPlugin({ adapters: [ createTsvectorAdapter(), @@ -923,7 +923,7 @@ describe('fullTextSearch can be disabled', () => { createTrgmAdapter({ defaultThreshold: 0.1 }), ], enableSearchScore: true, - enableFullTextSearch: false, + enableUnifiedSearch: false, }); const disabledPreset = { @@ -956,8 +956,8 @@ describe('fullTextSearch can be disabled', () => { } }); - it('fullTextSearch field does NOT exist when disabled', async () => { - // Introspect the filter type to verify fullTextSearch is NOT present. + it('unifiedSearch field does NOT exist when disabled', async () => { + // Introspect the filter type to verify unifiedSearch is NOT present. // NOTE: Grafast silently ignores unknown input fields rather than // returning a GraphQL validation error, so we verify via introspection. const introspection = await disabledQuery(` @@ -973,10 +973,10 @@ describe('fullTextSearch can be disabled', () => { expect(introspection.errors).toBeUndefined(); const filterFields = introspection.data?.__type?.inputFields?.map((f: any) => f.name) ?? []; - // fullTextSearch should NOT be in the filter fields - expect(filterFields).not.toContain('fullTextSearch'); + // unifiedSearch should NOT be in the filter fields + expect(filterFields).not.toContain('unifiedSearch'); - // The per-algorithm fields should still exist (only fullTextSearch is disabled) + // The per-algorithm fields should still exist (only unifiedSearch is disabled) expect(filterFields).toContain('tsvTsv'); expect(filterFields).toContain('bm25Body'); expect(filterFields).toContain('trgmTitle'); diff --git a/graphile/graphile-search/src/plugin.ts b/graphile/graphile-search/src/plugin.ts index 5f77475f0..a35f6a232 100644 --- a/graphile/graphile-search/src/plugin.ts +++ b/graphile/graphile-search/src/plugin.ts @@ -177,7 +177,7 @@ interface AdapterColumnCache { export function createUnifiedSearchPlugin( options: UnifiedSearchOptions ): GraphileConfig.Plugin { - const { adapters, enableSearchScore = true, enableFullTextSearch = true } = options; + const { adapters, enableSearchScore = true, enableUnifiedSearch = true } = options; // Per-codec cache of discovered columns, keyed by codec name const codecCache = new Map(); @@ -857,18 +857,18 @@ export function createUnifiedSearchPlugin( } } - // ── fullTextSearch composite filter ── - // Adds a single `fullTextSearch: String` field that fans out the same + // ── unifiedSearch composite filter ── + // Adds a single `unifiedSearch: String` field that fans out the same // text query to all adapters where supportsTextSearch is true. // WHERE clauses are combined with OR (match ANY algorithm). - if (enableFullTextSearch) { + if (enableUnifiedSearch) { // Collect text-compatible adapters and their columns for this codec const textAdapterColumns = adapterColumns.filter( (ac) => ac.adapter.supportsTextSearch && ac.adapter.buildTextSearchInput ); if (textAdapterColumns.length > 0) { - const fieldName = 'fullTextSearch'; + const fieldName = 'unifiedSearch'; newFields = build.extend( newFields, @@ -880,7 +880,7 @@ export function createUnifiedSearchPlugin( } as any, { description: build.wrapDescription( - 'Composite full-text search. Provide a search string and it will be dispatched ' + + 'Composite unified search. Provide a search string and it will be dispatched ' + 'to all text-compatible search algorithms (tsvector, BM25, pg_trgm) simultaneously. ' + 'Rows matching ANY algorithm are returned. All matching score fields are populated.', 'field' @@ -956,7 +956,7 @@ export function createUnifiedSearchPlugin( } ), }, - `UnifiedSearchPlugin adding fullTextSearch composite filter on '${codec.name}'` + `UnifiedSearchPlugin adding unifiedSearch composite filter on '${codec.name}'` ); } } diff --git a/graphile/graphile-search/src/preset.ts b/graphile/graphile-search/src/preset.ts index 4eb6a813b..9a5189485 100644 --- a/graphile/graphile-search/src/preset.ts +++ b/graphile/graphile-search/src/preset.ts @@ -68,10 +68,10 @@ export interface UnifiedSearchPresetOptions { enableSearchScore?: boolean; /** - * Whether to expose the composite `fullTextSearch` filter field. + * Whether to expose the composite `unifiedSearch` filter field. * @default true */ - enableFullTextSearch?: boolean; + enableUnifiedSearch?: boolean; /** * Custom weights for the composite searchScore. @@ -105,7 +105,7 @@ export function UnifiedSearchPreset( trgm = true, pgvector = true, enableSearchScore = true, - enableFullTextSearch = true, + enableUnifiedSearch = true, searchScoreWeights, fullTextScalarName = 'FullText', tsConfig = 'english', @@ -136,7 +136,7 @@ export function UnifiedSearchPreset( const pluginOptions: UnifiedSearchOptions = { adapters, enableSearchScore, - enableFullTextSearch, + enableUnifiedSearch, searchScoreWeights, }; diff --git a/graphile/graphile-search/src/types.ts b/graphile/graphile-search/src/types.ts index 5d9f49d53..f99f1377f 100644 --- a/graphile/graphile-search/src/types.ts +++ b/graphile/graphile-search/src/types.ts @@ -114,7 +114,7 @@ export interface SearchAdapter { /** * Whether this adapter supports plain text search queries. * If true, the adapter's columns will be included in the automatic - * `fullTextSearch` composite filter that fans out the same text query + * `unifiedSearch` composite filter that fans out the same text query * to all text-compatible adapters simultaneously. * * Adapters that require non-text input (e.g. pgvector needs a vector array) @@ -125,7 +125,7 @@ export interface SearchAdapter { supportsTextSearch?: boolean; /** - * Build the filter value for a text search query dispatched by fullTextSearch. + * Build the filter value for a text search query dispatched by unifiedSearch. * Only called when supportsTextSearch is true. * Converts a plain text string into the adapter-specific filter input format. * @@ -209,14 +209,14 @@ export interface UnifiedSearchOptions { enableSearchScore?: boolean; /** - * Whether to expose the `fullTextSearch` composite filter field. + * Whether to expose the `unifiedSearch` composite filter field. * When enabled, every table with at least one text-compatible adapter gets a - * `fullTextSearch: String` field on its filter type. Providing a value fans + * `unifiedSearch: String` field on its filter type. Providing a value fans * out the same text query to all adapters where `supportsTextSearch: true`, * combining their WHERE clauses with OR (match any algorithm). * @default true */ - enableFullTextSearch?: boolean; + enableUnifiedSearch?: boolean; /** * Custom weights for the composite searchScore. Keys are adapter names, diff --git a/graphql/codegen/src/core/codegen/docs-utils.ts b/graphql/codegen/src/core/codegen/docs-utils.ts index d3c608df4..12181ecf5 100644 --- a/graphql/codegen/src/core/codegen/docs-utils.ts +++ b/graphql/codegen/src/core/codegen/docs-utils.ts @@ -393,7 +393,7 @@ export function buildSearchExamples( } } - // Composite fullTextSearch example (dispatches to all text adapters) + // Composite unifiedSearch example (dispatches to all text adapters) const hasTextSearch = specialGroups.some( (g) => g.category === 'search' && g.fields.some( (f) => f.type.gqlType === 'FullText' || /TrgmSimilarity$/.test(f.name) || /Bm25Score$/.test(f.name), @@ -404,9 +404,9 @@ export function buildSearchExamples( ? `title,${[...new Set(scoreFields)].join(',')}` : 'title'; examples.push({ - description: 'Composite search (fullTextSearch dispatches to all text adapters)', + description: 'Composite search (unifiedSearch dispatches to all text adapters)', code: [ - `${toolName} ${cmd} list --where.fullTextSearch "search query" --select ${fieldsArg}`, + `${toolName} ${cmd} list --where.unifiedSearch "search query" --select ${fieldsArg}`, ], }); } @@ -416,7 +416,7 @@ export function buildSearchExamples( examples.push({ description: 'Search with pagination and field projection', code: [ - `${toolName} ${cmd} list --where.fullTextSearch "query" --limit 10 --select id,title,searchScore`, + `${toolName} ${cmd} list --where.unifiedSearch "query" --limit 10 --select id,title,searchScore`, `${toolName} ${cmd} search "query" --limit 10 --select id,title,searchScore`, ], }); diff --git a/graphql/server-test/__tests__/cli-e2e.test.ts b/graphql/server-test/__tests__/cli-e2e.test.ts index d3e961a03..fbe4cfa20 100644 --- a/graphql/server-test/__tests__/cli-e2e.test.ts +++ b/graphql/server-test/__tests__/cli-e2e.test.ts @@ -18,7 +18,7 @@ * * Suite 2 — Articles (search-seed): * 5 articles with tsvector, pg_trgm, optional pgvector columns - * Tests: tsvector search, trgm fuzzy matching, composite fullTextSearch, + * Tests: tsvector search, trgm fuzzy matching, composite unifiedSearch, * search+pagination, pgvector error handling, schema introspection */ @@ -908,17 +908,17 @@ describe('CLI E2E — search commands against real DB', () => { }); // ========================================================================= - // Test 3: composite fullTextSearch via list --where - // The fullTextSearch filter dispatches to all text-capable adapters. + // Test 3: composite unifiedSearch via list --where + // The unifiedSearch filter dispatches to all text-capable adapters. // ========================================================================= - it('should filter via fullTextSearch composite filter', async () => { - const output = await runCli( - distDir, - tmpHome, - 'article', - 'list', - '--where.fullTextSearch', + it('should filter via unifiedSearch composite filter', async () => { + const output = await runCli( + distDir, + tmpHome, + 'article', + 'list', + '--where.unifiedSearch', 'vector databases', '--select', 'title,searchScore', diff --git a/graphql/server-test/__tests__/search.integration.test.ts b/graphql/server-test/__tests__/search.integration.test.ts index 2a28791a8..f18bbf96e 100644 --- a/graphql/server-test/__tests__/search.integration.test.ts +++ b/graphql/server-test/__tests__/search.integration.test.ts @@ -298,7 +298,7 @@ describe('Unified Search — server integration', () => { }); // =========================================================================== - // Composite: searchScore + fullTextSearch + // Composite: searchScore + unifiedSearch // =========================================================================== describe('composite search', () => { @@ -338,10 +338,10 @@ describe('Unified Search — server integration', () => { expect(res.body.data.articles.nodes[0].searchScore).toBeNull(); }); - it('should filter via fullTextSearch composite filter', async () => { - const res = await postGraphQL({ - query: `{ - articles(where: { fullTextSearch: "vector databases" }) { + it('should filter via unifiedSearch composite filter', async () => { + const res = await postGraphQL({ + query: `{ + articles(where: { unifiedSearch: "vector databases" }) { nodes { title tsvRank searchScore } } }`, @@ -353,7 +353,7 @@ describe('Unified Search — server integration', () => { const nodes = res.body.data.articles.nodes; expect(nodes.length).toBeGreaterThanOrEqual(1); - // fullTextSearch dispatches to all text-capable adapters (tsv + trgm) + // unifiedSearch dispatches to all text-capable adapters (tsv + trgm) const titles = nodes.map((n: { title: string }) => n.title); expect(titles).toContain('Vector Databases and Embeddings'); }); @@ -362,7 +362,7 @@ describe('Unified Search — server integration', () => { const res = await postGraphQL({ query: `{ articles( - where: { fullTextSearch: "PostgreSQL search" } + where: { unifiedSearch: "PostgreSQL search" } orderBy: SEARCH_SCORE_DESC ) { nodes { title searchScore } @@ -469,15 +469,15 @@ describe('Unified Search — server integration', () => { }); // =========================================================================== - // Mega Query v2 — fullTextSearch composite + orderBy SEARCH_SCORE_DESC + // Mega Query v2 — unifiedSearch composite + orderBy SEARCH_SCORE_DESC // =========================================================================== - describe('Mega Query v2 — fullTextSearch composite', () => { - it('should use fullTextSearch + SEARCH_SCORE_DESC', async () => { - const res = await postGraphQL({ - query: `{ - articles( - where: { fullTextSearch: "machine learning" } + describe('Mega Query v2 — unifiedSearch composite', () => { + it('should use unifiedSearch + SEARCH_SCORE_DESC', async () => { + const res = await postGraphQL({ + query: `{ + articles( + where: { unifiedSearch: "machine learning" } orderBy: SEARCH_SCORE_DESC ) { nodes { @@ -515,7 +515,7 @@ describe('Unified Search — server integration', () => { } }); - it('should combine fullTextSearch + vector filter + mixed orderBy', async () => { + it('should combine unifiedSearch + vector filter + mixed orderBy', async () => { if (!hasVector) { console.log('pgvector not available, skipping mega query v2 with vector'); return; @@ -525,7 +525,7 @@ describe('Unified Search — server integration', () => { query: `{ articles( where: { - fullTextSearch: "machine learning" + unifiedSearch: "machine learning" vectorEmbedding: { vector: [0.1, 0.9, 0.3] } } orderBy: [SEARCH_SCORE_DESC, EMBEDDING_VECTOR_DISTANCE_ASC] @@ -621,7 +621,7 @@ describe('Unified Search — server integration', () => { expect(fieldNames).toContain('trgmBody'); // composite - expect(fieldNames).toContain('fullTextSearch'); + expect(fieldNames).toContain('unifiedSearch'); // pgvector (conditional) if (hasVector) {