feat(nbstore): add cloud indexer storage (#12245)

close AF-2613
This commit is contained in:
fengmk2
2025-05-19 04:39:39 +00:00
parent a34c3ea200
commit 4e37a1322e
4 changed files with 174 additions and 8 deletions

View File

@@ -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[];

View File

@@ -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<T>,
>(table: T, query: Query<T>, options?: O): Promise<SearchResult<T, O>> {
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<T, O>;
return result;
}
override search$<
T extends keyof IndexerSchema,
const O extends SearchOptions<T>,
>(table: T, query: Query<T>, options?: O): Observable<SearchResult<T, O>> {
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<T>,
>(
table: T,
query: Query<T>,
field: keyof IndexerSchema[T],
options?: O
): Promise<AggregateResult<T, O>> {
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<T, O>;
return result;
}
override aggregate$<
T extends keyof IndexerSchema,
const O extends AggregateOptions<T>,
>(
table: T,
query: Query<T>,
field: keyof IndexerSchema[T],
options?: O
): Observable<AggregateResult<T, O>> {
return new Observable(observer => {
this.aggregate(table, query, field, options)
.then(data => {
observer.next(data);
observer.complete();
})
.catch(error => {
observer.error(error);
});
});
}
override deleteByQuery<T extends keyof IndexerSchema>(
_table: T,
_query: Query<T>
): Promise<void> {
return Promise.resolve();
}
override insert<T extends keyof IndexerSchema>(
_table: T,
_document: IndexerDocument<T>
): Promise<void> {
return Promise.resolve();
}
override delete<T extends keyof IndexerSchema>(
_table: T,
_id: string
): Promise<void> {
return Promise.resolve();
}
override update<T extends keyof IndexerSchema>(
_table: T,
_document: IndexerDocument<T>
): Promise<void> {
return Promise.resolve();
}
override refresh<T extends keyof IndexerSchema>(_table: T): Promise<void> {
return Promise.resolve();
}
}

View File

@@ -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

View File

@@ -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: {