From badcf17f70d1b4e156739bb03df4db0669678d57 Mon Sep 17 00:00:00 2001 From: Charis <26616127+charislam@users.noreply.github.com> Date: Tue, 6 May 2025 13:11:29 -0400 Subject: [PATCH] feat(content api): add client library api reference search results (#35484) * feat(content api): add client library api reference search results Allow searchDocs results to also return function references from the client library APIs * fix(content api): refine language enum handling --- .../graphql/__snapshots__/route.test.ts.snap | 29 +++++++++ apps/docs/lib/supabase.ts | 9 ++- .../globalSearch/globalSearchModel.ts | 54 ++++++++++------ apps/docs/resources/guide/guideModel.ts | 2 +- .../resources/reference/referenceSDKModel.ts | 61 +++++++++++++++++++ .../resources/reference/referenceSDKSchema.ts | 41 +++++++++++++ apps/docs/resources/rootSchema.ts | 3 +- apps/docs/scripts/search/sources/index.ts | 12 ++-- .../scripts/search/sources/reference-doc.ts | 10 ++- packages/common/database-types.ts | 1 + ...250430202653_return_meta_vector_search.sql | 56 +++++++++++++++++ 11 files changed, 249 insertions(+), 29 deletions(-) create mode 100644 apps/docs/resources/reference/referenceSDKModel.ts create mode 100644 apps/docs/resources/reference/referenceSDKSchema.ts create mode 100644 supabase/migrations/20250430202653_return_meta_vector_search.sql diff --git a/apps/docs/app/api/graphql/__snapshots__/route.test.ts.snap b/apps/docs/app/api/graphql/__snapshots__/route.test.ts.snap index 039adb84244..1e48c527862 100644 --- a/apps/docs/app/api/graphql/__snapshots__/route.test.ts.snap +++ b/apps/docs/app/api/graphql/__snapshots__/route.test.ts.snap @@ -70,6 +70,35 @@ type Subsection { content: String } +""" +A reference document containing a description of a function from a Supabase client library +""" +type ClientLibraryFunctionReference implements SearchResult { + """The title of the document""" + title: String + + """The URL of the document""" + href: String + + """The content of the reference document, as text""" + content: String + + """The programming language for which the function is written""" + language: Language! + + """The name of the function or method""" + methodName: String +} + +enum Language { + JAVASCRIPT + SWIFT + DART + CSHARP + KOTLIN + PYTHON +} + type RootQueryType { """Get the GraphQL schema for this endpoint""" schema: String! diff --git a/apps/docs/lib/supabase.ts b/apps/docs/lib/supabase.ts index ea3843beccd..c65a4ea76bc 100644 --- a/apps/docs/lib/supabase.ts +++ b/apps/docs/lib/supabase.ts @@ -15,8 +15,11 @@ type Database = { Returns: Array< Omit< DatabaseGenerated['public']['Functions']['search_content']['Returns'][number], - 'subsections' - > & { subsections: Array<{ title?: string; href?: string; content?: string }> } + 'subsections' | 'metadata' + > & { + metadata: { language?: string; methodName?: string } + subsections: Array<{ title?: string; href?: string; content?: string }> + } > } } @@ -38,3 +41,5 @@ export function supabase() { return _supabase } + +export type { Database as DatabaseCorrected } diff --git a/apps/docs/resources/globalSearch/globalSearchModel.ts b/apps/docs/resources/globalSearch/globalSearchModel.ts index d94640aeb07..9c1c0936c57 100644 --- a/apps/docs/resources/globalSearch/globalSearchModel.ts +++ b/apps/docs/resources/globalSearch/globalSearchModel.ts @@ -2,8 +2,10 @@ import { type RootQueryTypeSearchDocsArgs } from '~/__generated__/graphql' import { convertPostgrestToApiError, type ApiErrorGeneric } from '~/app/api/utils' import { Result } from '~/features/helpers.fn' import { openAI } from '~/lib/openAi' -import { supabase } from '~/lib/supabase' +import { supabase, type DatabaseCorrected } from '~/lib/supabase' import { GuideModel } from '../guide/guideModel' +import { ReferenceSDKFunctionModel, SDKLanguageValues } from '../reference/referenceSDKModel' +import { SearchResultInterface } from './globalSearchInterface' export abstract class SearchResultModel { static async search( @@ -22,26 +24,42 @@ export abstract class SearchResultModel { max_result: args.limit, }) ) - .map((matches) => - matches - .map(({ type, page_title, href, content, subsections }) => { - switch (type) { - case 'markdown': - return new GuideModel({ - title: page_title, - href, - content, - subsections, - }) - default: - return null - } - }) - .filter(Boolean) - ) + .map((matches) => matches.map(createModelFromMatch).filter(Boolean)) .mapError(convertPostgrestToApiError) return matchResult }) } } + +function createModelFromMatch({ + type, + page_title, + href, + content, + metadata, + subsections, +}: DatabaseCorrected['public']['Functions']['search_content']['Returns'][number]): SearchResultInterface | null { + switch (type) { + case 'markdown': + return new GuideModel({ + title: page_title, + href, + content, + subsections, + }) + case 'reference': + const { language } = metadata + if (SDKLanguageValues.includes(language)) { + return new ReferenceSDKFunctionModel({ + title: page_title, + href, + content, + language, + methodName: metadata.methodName, + }) + } + default: + return null + } +} diff --git a/apps/docs/resources/guide/guideModel.ts b/apps/docs/resources/guide/guideModel.ts index a1ba712600f..5201d7a9cc0 100644 --- a/apps/docs/resources/guide/guideModel.ts +++ b/apps/docs/resources/guide/guideModel.ts @@ -1,4 +1,4 @@ -import { SearchResultInterface } from '../globalSearch/globalSearchInterface' +import { type SearchResultInterface } from '../globalSearch/globalSearchInterface' export class GuideModel implements SearchResultInterface { public title?: string diff --git a/apps/docs/resources/reference/referenceSDKModel.ts b/apps/docs/resources/reference/referenceSDKModel.ts new file mode 100644 index 00000000000..92c6c0488a2 --- /dev/null +++ b/apps/docs/resources/reference/referenceSDKModel.ts @@ -0,0 +1,61 @@ +import { type SearchResultInterface } from '../globalSearch/globalSearchInterface' + +export const SDKLanguages: Record = { + JAVASCRIPT: { + value: 'JavaScript', + pathSection: 'javascript', + }, + SWIFT: { + value: 'Swift', + pathSection: 'swift', + }, + DART: { + value: 'Dart', + pathSection: 'dart', + }, + CSHARP: { + value: 'C#', + pathSection: 'csharp', + }, + KOTLIN: { + value: 'Kotlin', + pathSection: 'kotlin', + }, + PYTHON: { + value: 'Python', + pathSection: 'python', + }, +} + +export const SDKLanguageValues = Object.values(SDKLanguages).map(({ value }) => value) + +export class ReferenceSDKFunctionModel implements SearchResultInterface { + public title?: string + public href?: string + public content?: string + public language: string + public methodName?: string + + constructor({ + title, + href, + content, + language, + methodName, + }: { + title?: string + href?: string + content?: string + language: string + methodName?: string + }) { + this.title = title + this.href = href + this.content = content + this.methodName = methodName + + if (SDKLanguageValues.includes(language)) { + this.language = language + } + } +} diff --git a/apps/docs/resources/reference/referenceSDKSchema.ts b/apps/docs/resources/reference/referenceSDKSchema.ts new file mode 100644 index 00000000000..9608130026c --- /dev/null +++ b/apps/docs/resources/reference/referenceSDKSchema.ts @@ -0,0 +1,41 @@ +import { GraphQLEnumType, GraphQLNonNull, GraphQLObjectType, GraphQLString } from 'graphql' +import { GraphQLInterfaceTypeSearchResult } from '../globalSearch/globalSearchSchema' +import { ReferenceSDKFunctionModel, SDKLanguages } from './referenceSDKModel' + +const GraphQLEnumLanguage = new GraphQLEnumType({ + name: 'Language', + values: Object.keys(SDKLanguages).reduce((acc, key) => { + acc[key] = { value: SDKLanguages[key].value } + return acc + }, {}), +}) + +export const GraphQLObjectTypeReferenceSDKFunction = new GraphQLObjectType({ + name: 'ClientLibraryFunctionReference', + interfaces: [GraphQLInterfaceTypeSearchResult], + isTypeOf: (value: unknown) => value instanceof ReferenceSDKFunctionModel, + description: + 'A reference document containing a description of a function from a Supabase client library', + fields: { + title: { + type: GraphQLString, + description: 'The title of the document', + }, + href: { + type: GraphQLString, + description: 'The URL of the document', + }, + content: { + type: GraphQLString, + description: 'The content of the reference document, as text', + }, + language: { + type: new GraphQLNonNull(GraphQLEnumLanguage), + description: 'The programming language for which the function is written', + }, + methodName: { + type: GraphQLString, + description: 'The name of the function or method', + }, + }, +}) diff --git a/apps/docs/resources/rootSchema.ts b/apps/docs/resources/rootSchema.ts index 1e232857297..6cdf10bbd70 100644 --- a/apps/docs/resources/rootSchema.ts +++ b/apps/docs/resources/rootSchema.ts @@ -8,6 +8,7 @@ import { import { RootQueryTypeResolvers } from '~/__generated__/graphql' import { searchRoot } from './globalSearch/globalSearchResolver' import { GraphQLObjectTypeGuide } from './guide/guideSchema' +import { GraphQLObjectTypeReferenceSDKFunction } from './reference/referenceSDKSchema' const GRAPHQL_FIELD_INTROSPECT = 'schema' as const @@ -34,5 +35,5 @@ export const rootGraphQLSchema = new GraphQLSchema({ ...searchRoot, }, }), - types: [GraphQLObjectTypeGuide], + types: [GraphQLObjectTypeGuide, GraphQLObjectTypeReferenceSDKFunction], }) diff --git a/apps/docs/scripts/search/sources/index.ts b/apps/docs/scripts/search/sources/index.ts index 302115018c8..241841ec35e 100644 --- a/apps/docs/scripts/search/sources/index.ts +++ b/apps/docs/scripts/search/sources/index.ts @@ -56,7 +56,7 @@ export async function fetchJsLibReferenceSource() { return new ClientLibReferenceLoader( 'js-lib', '/reference/javascript', - { title: 'JavaScript Reference' }, + { title: 'JavaScript Reference', language: 'JavaScript' }, 'spec/supabase_js_v2.yml', 'spec/common-client-libs-sections.json' ).load() @@ -66,7 +66,7 @@ export async function fetchDartLibReferenceSource() { return new ClientLibReferenceLoader( 'dart-lib', '/reference/dart', - { title: 'Dart Reference' }, + { title: 'Dart Reference', language: 'Dart' }, 'spec/supabase_dart_v2.yml', 'spec/common-client-libs-sections.json' ).load() @@ -76,7 +76,7 @@ export async function fetchPythonLibReferenceSource() { return new ClientLibReferenceLoader( 'python-lib', '/reference/python', - { title: 'Python Reference' }, + { title: 'Python Reference', language: 'Python' }, 'spec/supabase_py_v2.yml', 'spec/common-client-libs-sections.json' ).load() @@ -86,7 +86,7 @@ export async function fetchCSharpLibReferenceSource() { return new ClientLibReferenceLoader( 'csharp-lib', '/reference/csharp', - { title: 'C# Reference' }, + { title: 'C# Reference', language: 'C#' }, 'spec/supabase_csharp_v0.yml', 'spec/common-client-libs-sections.json' ).load() @@ -96,7 +96,7 @@ export async function fetchSwiftLibReferenceSource() { return new ClientLibReferenceLoader( 'swift-lib', '/reference/swift', - { title: 'Swift Reference' }, + { title: 'Swift Reference', language: 'Swift' }, 'spec/supabase_swift_v2.yml', 'spec/common-client-libs-sections.json' ).load() @@ -106,7 +106,7 @@ export async function fetchKtLibReferenceSource() { return new ClientLibReferenceLoader( 'kt-lib', '/reference/kotlin', - { title: 'Kotlin Reference' }, + { title: 'Kotlin Reference', language: 'Kotlin' }, 'spec/supabase_kt_v1.yml', 'spec/common-client-libs-sections.json' ).load() diff --git a/apps/docs/scripts/search/sources/reference-doc.ts b/apps/docs/scripts/search/sources/reference-doc.ts index 36c54ab3e37..11315fd1c22 100644 --- a/apps/docs/scripts/search/sources/reference-doc.ts +++ b/apps/docs/scripts/search/sources/reference-doc.ts @@ -52,7 +52,7 @@ export abstract class ReferenceLoader extends BaseLoader { `${this.path}/${refSection.slug}`, refSection, specSection, - this.meta + this.enhanceMeta(specSection) ) }) .filter(Boolean) @@ -62,6 +62,9 @@ export abstract class ReferenceLoader extends BaseLoader { abstract getSpecSections(specContents: string): SpecSection[] abstract matchSpecSection(specSections: SpecSection[], id: string): SpecSection + enhanceMeta(section: SpecSection): Json { + return this.meta + } } export abstract class ReferenceSource extends BaseSource { @@ -186,9 +189,14 @@ export class ClientLibReferenceLoader extends ReferenceLoader functionDefinition.id === id) } + + enhanceMeta(section: IFunctionDefinition): Json { + return { ...this.meta, slug: section.id, methodName: section.title } + } } export class ClientLibReferenceSource extends ReferenceSource { diff --git a/packages/common/database-types.ts b/packages/common/database-types.ts index 9a56bfdc619..7941ad59978 100644 --- a/packages/common/database-types.ts +++ b/packages/common/database-types.ts @@ -552,6 +552,7 @@ export type Database = { type: string href: string content: string + metadata: Json subsections: Json[] }[] } diff --git a/supabase/migrations/20250430202653_return_meta_vector_search.sql b/supabase/migrations/20250430202653_return_meta_vector_search.sql new file mode 100644 index 00000000000..4b78d7e13fd --- /dev/null +++ b/supabase/migrations/20250430202653_return_meta_vector_search.sql @@ -0,0 +1,56 @@ +-- Alter the search_content function to also return the page metadata + +drop function search_content; + +create or replace function search_content( + embedding vector(1536), + include_full_content boolean default false, + match_threshold float default 0.78, + max_result int default 30 +) +returns table ( + id bigint, + page_title text, + type text, + href text, + content text, + metadata json, + subsections json[] +) +language sql +set search_path = '' +as $$ + with matched_section as ( + select + *, + row_number() over () as ranking + from public.match_embedding( + embedding, + match_threshold, + max_result + ) + ) + select + page.id, + meta ->> 'title' as page_title, + type, + public.get_full_content_url(type, path, null) as href, + case + when include_full_content + then page.content + else + null + end as content, + meta as metadata, + array_agg( + json_build_object( + 'title', heading, + 'href', public.get_full_content_url(type, path, slug), + 'content', matched_section.content + ) + ) + from matched_section + join public.page on matched_section.page_id = page.id + group by page.id + order by min(ranking); +$$;