diff --git a/packages/common/nbstore/src/impls/cloud/index.ts b/packages/common/nbstore/src/impls/cloud/index.ts index c8b0cda3e8..2e2e818bb3 100644 --- a/packages/common/nbstore/src/impls/cloud/index.ts +++ b/packages/common/nbstore/src/impls/cloud/index.ts @@ -3,11 +3,13 @@ import { CloudAwarenessStorage } from './awareness'; import { CloudBlobStorage } from './blob'; import { CloudDocStorage } from './doc'; import { StaticCloudDocStorage } from './doc-static'; +import { CloudIndexerStorage } from './indexer'; export * from './awareness'; export * from './blob'; export * from './doc'; export * from './doc-static'; +export * from './indexer'; export * from './socket'; export const cloudStorages = [ @@ -15,4 +17,5 @@ export const cloudStorages = [ StaticCloudDocStorage, CloudBlobStorage, CloudAwarenessStorage, + CloudIndexerStorage, ] satisfies StorageConstructor[]; diff --git a/packages/common/nbstore/src/impls/cloud/indexer.ts b/packages/common/nbstore/src/impls/cloud/indexer.ts new file mode 100644 index 0000000000..5ff20427af --- /dev/null +++ b/packages/common/nbstore/src/impls/cloud/indexer.ts @@ -0,0 +1,142 @@ +import { + type AggregateInput, + indexerAggregateQuery, + indexerSearchQuery, + type SearchInput, +} from '@affine/graphql'; +import { Observable } from 'rxjs'; + +import { + type AggregateOptions, + type AggregateResult, + type IndexerDocument, + type IndexerSchema, + IndexerStorageBase, + type Query, + type SearchOptions, + type SearchResult, +} from '../../storage/indexer'; +import { HttpConnection } from './http'; + +interface CloudIndexerStorageOptions { + serverBaseUrl: string; + id: string; +} + +export class CloudIndexerStorage extends IndexerStorageBase { + static readonly identifier = 'CloudIndexerStorage'; + readonly isReadonly = true; + readonly connection = new HttpConnection(this.options.serverBaseUrl); + + constructor(private readonly options: CloudIndexerStorageOptions) { + super(); + } + + override async search< + T extends keyof IndexerSchema, + const O extends SearchOptions, + >(table: T, query: Query, options?: O): Promise> { + const res = await this.connection.gql({ + query: indexerSearchQuery, + variables: { + id: this.options.id, + input: { + table, + query, + options, + } as SearchInput, + }, + }); + const result = res.workspace.search as unknown as SearchResult; + return result; + } + + override search$< + T extends keyof IndexerSchema, + const O extends SearchOptions, + >(table: T, query: Query, options?: O): Observable> { + return new Observable(observer => { + this.search(table, query, options) + .then(data => { + observer.next(data); + observer.complete(); + }) + .catch(error => { + observer.error(error); + }); + }); + } + + override async aggregate< + T extends keyof IndexerSchema, + const O extends AggregateOptions, + >( + table: T, + query: Query, + field: keyof IndexerSchema[T], + options?: O + ): Promise> { + const res = await this.connection.gql({ + query: indexerAggregateQuery, + variables: { + id: this.options.id, + input: { table, query, field, options } as AggregateInput, + }, + }); + const result = res.workspace.aggregate as unknown as AggregateResult; + return result; + } + + override aggregate$< + T extends keyof IndexerSchema, + const O extends AggregateOptions, + >( + table: T, + query: Query, + field: keyof IndexerSchema[T], + options?: O + ): Observable> { + return new Observable(observer => { + this.aggregate(table, query, field, options) + .then(data => { + observer.next(data); + observer.complete(); + }) + .catch(error => { + observer.error(error); + }); + }); + } + + override deleteByQuery( + _table: T, + _query: Query + ): Promise { + return Promise.resolve(); + } + + override insert( + _table: T, + _document: IndexerDocument + ): Promise { + return Promise.resolve(); + } + + override delete( + _table: T, + _id: string + ): Promise { + return Promise.resolve(); + } + + override update( + _table: T, + _document: IndexerDocument + ): Promise { + return Promise.resolve(); + } + + override refresh(_table: T): Promise { + return Promise.resolve(); + } +} diff --git a/packages/frontend/core/src/modules/feature-flag/constant.ts b/packages/frontend/core/src/modules/feature-flag/constant.ts index 76a2325d38..12058a6b14 100644 --- a/packages/frontend/core/src/modules/feature-flag/constant.ts +++ b/packages/frontend/core/src/modules/feature-flag/constant.ts @@ -3,6 +3,7 @@ import type { FlagInfo } from './types'; // const isNotStableBuild = BUILD_CONFIG.appBuildType !== 'stable'; const isDesktopEnvironment = BUILD_CONFIG.isElectron; const isCanaryBuild = BUILD_CONFIG.appBuildType === 'canary'; +const isBetaBuild = BUILD_CONFIG.appBuildType === 'beta'; const isMobile = BUILD_CONFIG.isMobileEdition; export const AFFINE_FLAGS = { @@ -317,6 +318,13 @@ export const AFFINE_FLAGS = { configurable: isCanaryBuild, defaultState: false, }, + enable_cloud_indexer: { + category: 'affine', + displayName: 'Enable Cloud Indexer', + description: 'Use cloud indexer to search docs', + configurable: isBetaBuild || isCanaryBuild, + defaultState: false, + }, } satisfies { [key in string]: FlagInfo }; // oxlint-disable-next-line no-redeclare diff --git a/packages/frontend/core/src/modules/workspace-engine/impls/cloud.ts b/packages/frontend/core/src/modules/workspace-engine/impls/cloud.ts index 238025049e..68e076711d 100644 --- a/packages/frontend/core/src/modules/workspace-engine/impls/cloud.ts +++ b/packages/frontend/core/src/modules/workspace-engine/impls/cloud.ts @@ -1,3 +1,4 @@ +import { FeatureFlagService } from '@affine/core/modules/feature-flag'; import { DebugLogger } from '@affine/debug'; import { createWorkspaceMutation, @@ -85,6 +86,7 @@ const logger = new DebugLogger('affine:cloud-workspace-flavour-provider'); class CloudWorkspaceFlavourProvider implements WorkspaceFlavourProvider { private readonly authService: AuthService; private readonly graphqlService: GraphQLService; + private readonly featureFlagService: FeatureFlagService; private readonly unsubscribeAccountChanged: () => void; constructor( @@ -93,6 +95,7 @@ class CloudWorkspaceFlavourProvider implements WorkspaceFlavourProvider { ) { this.authService = server.scope.get(AuthService); this.graphqlService = server.scope.get(GraphQLService); + this.featureFlagService = server.scope.get(FeatureFlagService); this.unsubscribeAccountChanged = this.server.scope.eventBus.on( AccountChanged, () => { @@ -474,14 +477,24 @@ class CloudWorkspaceFlavourProvider implements WorkspaceFlavourProvider { id: `${this.flavour}:${workspaceId}`, }, }, - indexer: { - name: 'IndexedDBIndexerStorage', - opts: { - flavour: this.flavour, - type: 'workspace', - id: workspaceId, - }, - }, + indexer: this.featureFlagService.flags.enable_cloud_indexer.value + ? { + name: 'CloudIndexerStorage', + opts: { + flavour: this.flavour, + type: 'workspace', + id: workspaceId, + serverBaseUrl: this.server.serverMetadata.baseUrl, + }, + } + : { + name: 'IndexedDBIndexerStorage', + opts: { + flavour: this.flavour, + type: 'workspace', + id: workspaceId, + }, + }, indexerSync: { name: 'IndexedDBIndexerSyncStorage', opts: {