mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-13 21:05:19 +00:00
feat(core): use cloud indexer for search (#12899)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Added enhanced error handling and user-friendly error messages in quick search and document search menus. - Introduced loading state indicators for search operations. - Quick Search now provides explicit error feedback in the UI. - **Improvements** - Search and aggregation operations can now prefer remote or local indexers based on user or system preference. - Streamlined indexer logic for more consistent and reliable search experiences. - Refined error handling in messaging and synchronization layers for improved stability. - Enhanced error object handling in messaging for clearer error propagation. - Updated cloud workspace storage to always use IndexedDB locally and CloudIndexer remotely. - Shifted indexer operations to use synchronized indexer layer for better consistency. - Simplified indexer client by consolidating storage and sync layers. - Improved error propagation in messaging handlers by wrapping error objects. - Updated document search to prioritize remote indexer results by default. - **Bug Fixes** - Improved robustness of search features by handling errors gracefully and preventing potential runtime issues. - **Style** - Added new styles for displaying error messages in search interfaces. - **Chores** - Removed the obsolete "Enable Cloud Indexer" feature flag; cloud indexer behavior is now always enabled where applicable. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -112,6 +112,7 @@ export class DocsSearchService extends Service {
|
||||
},
|
||||
],
|
||||
},
|
||||
prefer: 'remote',
|
||||
}
|
||||
)
|
||||
.pipe(
|
||||
|
||||
@@ -3,7 +3,6 @@ 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 = {
|
||||
@@ -266,13 +265,6 @@ 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,
|
||||
},
|
||||
enable_adapter_panel: {
|
||||
category: 'affine',
|
||||
displayName:
|
||||
|
||||
@@ -25,6 +25,11 @@ export class QuickSearch extends Entity {
|
||||
.flat()
|
||||
.map(items => items.flat());
|
||||
|
||||
readonly error$ = this.state$
|
||||
.map(s => s?.sessions.map(session => session.error$) ?? [])
|
||||
.flat()
|
||||
.map(items => items.find(v => !!v) ?? null);
|
||||
|
||||
readonly show$ = this.state$.map(s => !!s);
|
||||
|
||||
readonly options$ = this.state$.map(s => s?.options);
|
||||
|
||||
@@ -6,11 +6,12 @@ import {
|
||||
onStart,
|
||||
} from '@toeverything/infra';
|
||||
import { truncate } from 'lodash-es';
|
||||
import { map, of, switchMap, tap, throttleTime } from 'rxjs';
|
||||
import { catchError, EMPTY, map, of, switchMap, tap, throttleTime } from 'rxjs';
|
||||
|
||||
import type { DocRecord, DocsService } from '../../doc';
|
||||
import type { DocDisplayMetaService } from '../../doc-display-meta';
|
||||
import type { DocsSearchService } from '../../docs-search';
|
||||
import type { WorkspaceService } from '../../workspace';
|
||||
import type { QuickSearchSession } from '../providers/quick-search-provider';
|
||||
import type { QuickSearchItem } from '../types/item';
|
||||
|
||||
@@ -26,6 +27,7 @@ export class DocsQuickSearchSession
|
||||
implements QuickSearchSession<'docs', DocsPayload>
|
||||
{
|
||||
constructor(
|
||||
private readonly workspaceService: WorkspaceService,
|
||||
private readonly docsSearchService: DocsSearchService,
|
||||
private readonly docsService: DocsService,
|
||||
private readonly docDisplayMetaService: DocDisplayMetaService
|
||||
@@ -41,10 +43,17 @@ export class DocsQuickSearchSession
|
||||
|
||||
private readonly isQueryLoading$ = new LiveData(false);
|
||||
|
||||
isCloudWorkspace = this.workspaceService.workspace.flavour !== 'local';
|
||||
|
||||
isLoading$ = LiveData.computed(get => {
|
||||
return get(this.isIndexerLoading$) || get(this.isQueryLoading$);
|
||||
return (
|
||||
(this.isCloudWorkspace ? false : get(this.isIndexerLoading$)) ||
|
||||
get(this.isQueryLoading$)
|
||||
);
|
||||
});
|
||||
|
||||
error$ = new LiveData<any>(null);
|
||||
|
||||
query$ = new LiveData('');
|
||||
|
||||
items$ = new LiveData<QuickSearchItem<'docs', DocsPayload>[]>([]);
|
||||
@@ -102,9 +111,16 @@ export class DocsQuickSearchSession
|
||||
this.isQueryLoading$.next(false);
|
||||
}),
|
||||
onStart(() => {
|
||||
this.error$.next(null);
|
||||
this.items$.next([]);
|
||||
this.isQueryLoading$.next(true);
|
||||
}),
|
||||
catchError(err => {
|
||||
this.error$.next(err instanceof Error ? err.message : err);
|
||||
this.items$.next([]);
|
||||
this.isQueryLoading$.next(false);
|
||||
return EMPTY;
|
||||
}),
|
||||
onComplete(() => {})
|
||||
);
|
||||
})
|
||||
|
||||
@@ -55,6 +55,7 @@ export function configureQuickSearchModule(framework: Framework) {
|
||||
.entity(QuickSearch)
|
||||
.entity(CommandsQuickSearchSession, [GlobalContextService])
|
||||
.entity(DocsQuickSearchSession, [
|
||||
WorkspaceService,
|
||||
DocsSearchService,
|
||||
DocsService,
|
||||
DocDisplayMetaService,
|
||||
|
||||
@@ -8,7 +8,7 @@ export type QuickSearchFunction<S, P> = (
|
||||
|
||||
export interface QuickSearchSession<S, P> {
|
||||
items$: LiveData<QuickSearchItem<S, P>[]>;
|
||||
isError$?: LiveData<boolean>;
|
||||
error$?: LiveData<any>;
|
||||
isLoading$?: LiveData<boolean>;
|
||||
loadingProgress$?: LiveData<number>;
|
||||
hasMore$?: LiveData<boolean>;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { globalStyle, style } from '@vanilla-extract/css';
|
||||
|
||||
export const root = style({});
|
||||
@@ -195,3 +196,9 @@ export const itemSubtitle = style({
|
||||
fontWeight: 400,
|
||||
textAlign: 'justify',
|
||||
});
|
||||
|
||||
export const errorMessage = style({
|
||||
padding: '0px 8px 8px',
|
||||
fontSize: cssVar('fontXs'),
|
||||
color: cssVarV2('status/error'),
|
||||
});
|
||||
|
||||
@@ -24,6 +24,7 @@ export const CMDK = ({
|
||||
className,
|
||||
query,
|
||||
groups: newGroups = [],
|
||||
error,
|
||||
inputLabel,
|
||||
placeholder,
|
||||
loading: newLoading = false,
|
||||
@@ -33,6 +34,7 @@ export const CMDK = ({
|
||||
}: React.PropsWithChildren<{
|
||||
className?: string;
|
||||
query: string;
|
||||
error?: ReactNode;
|
||||
inputLabel?: ReactNode;
|
||||
placeholder?: string;
|
||||
loading?: boolean;
|
||||
@@ -200,6 +202,7 @@ export const CMDK = ({
|
||||
</div>
|
||||
|
||||
<Command.List ref={listRef} data-opening={opening ? true : undefined}>
|
||||
{error && <p className={styles.errorMessage}>{error}</p>}
|
||||
{groups.map(({ group, items }) => {
|
||||
return (
|
||||
<CMDKGroup
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { UserFriendlyError } from '@affine/error';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { useLiveData, useServices } from '@toeverything/infra';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
@@ -18,6 +19,7 @@ export const QuickSearchContainer = () => {
|
||||
const loading = useLiveData(quickSearch.isLoading$);
|
||||
const loadingProgress = useLiveData(quickSearch.loadingProgress$);
|
||||
const items = useLiveData(quickSearch.items$);
|
||||
const error = useLiveData(quickSearch.error$);
|
||||
const options = useLiveData(quickSearch.options$);
|
||||
const i18n = useI18n();
|
||||
|
||||
@@ -79,6 +81,7 @@ export const QuickSearchContainer = () => {
|
||||
<CMDK
|
||||
query={query}
|
||||
groups={groups}
|
||||
error={error ? UserFriendlyError.fromAny(error).message : null}
|
||||
loading={loading}
|
||||
loadingProgress={loadingProgress}
|
||||
onQueryChange={handleChangeQuery}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { TagMeta } from '@affine/core/components/page-list';
|
||||
import { UserFriendlyError } from '@affine/error';
|
||||
import { I18n } from '@affine/i18n';
|
||||
import { createSignalFromObservable } from '@blocksuite/affine/shared/utils';
|
||||
import type { DocMeta } from '@blocksuite/affine/store';
|
||||
@@ -6,14 +7,14 @@ import type {
|
||||
LinkedMenuGroup,
|
||||
LinkedMenuItem,
|
||||
} from '@blocksuite/affine/widgets/linked-doc';
|
||||
import { CollectionsIcon } from '@blocksuite/icons/lit';
|
||||
import { computed } from '@preact/signals-core';
|
||||
import { CollectionsIcon, WarningIcon } from '@blocksuite/icons/lit';
|
||||
import { computed, signal } from '@preact/signals-core';
|
||||
import { Service } from '@toeverything/infra';
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import Fuse, { type FuseResultMatch } from 'fuse.js';
|
||||
import { html } from 'lit';
|
||||
import { unsafeHTML } from 'lit/directives/unsafe-html.js';
|
||||
import { map } from 'rxjs';
|
||||
import { catchError, map, of } from 'rxjs';
|
||||
|
||||
import type { CollectionMeta, CollectionService } from '../../collection';
|
||||
import type { DocDisplayMetaService } from '../../doc-display-meta';
|
||||
@@ -87,6 +88,7 @@ export class SearchMenuService extends Service {
|
||||
): LinkedMenuGroup {
|
||||
const currentWorkspace = this.workspaceService.workspace;
|
||||
const rawMetas = currentWorkspace.docCollection.meta.docMetas;
|
||||
const loading = signal(true);
|
||||
const { signal: docsSignal, cleanup: cleanupDocs } =
|
||||
createSignalFromObservable(
|
||||
this.searchDocs$(query).pipe(
|
||||
@@ -108,7 +110,26 @@ export class SearchMenuService extends Service {
|
||||
);
|
||||
})
|
||||
.filter(m => !!m);
|
||||
loading.value = false;
|
||||
return docs;
|
||||
}),
|
||||
catchError(err => {
|
||||
loading.value = false;
|
||||
const userFriendlyError = UserFriendlyError.fromAny(err);
|
||||
return of([
|
||||
{
|
||||
name: html`<span style="color: ${cssVarV2('status/error')}"
|
||||
>${I18n.t(
|
||||
`error.${userFriendlyError.name}`,
|
||||
userFriendlyError.data
|
||||
)}</span
|
||||
>`,
|
||||
key: 'error',
|
||||
icon: WarningIcon(),
|
||||
disabled: true,
|
||||
action: () => {},
|
||||
},
|
||||
]);
|
||||
})
|
||||
),
|
||||
[]
|
||||
@@ -129,6 +150,7 @@ export class SearchMenuService extends Service {
|
||||
name: I18n.t('com.affine.editor.at-menu.link-to-doc', {
|
||||
query,
|
||||
}),
|
||||
loading: loading,
|
||||
items: docsSignal,
|
||||
maxDisplay: MAX_DOCS,
|
||||
overflowText,
|
||||
@@ -170,7 +192,7 @@ export class SearchMenuService extends Service {
|
||||
return {
|
||||
id,
|
||||
title,
|
||||
highlights: node.highlights.title[0],
|
||||
highlights: node.highlights?.title?.[0],
|
||||
};
|
||||
})
|
||||
)
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||
import { DebugLogger } from '@affine/debug';
|
||||
import {
|
||||
createWorkspaceMutation,
|
||||
@@ -7,6 +6,7 @@ import {
|
||||
getWorkspacesQuery,
|
||||
Permission,
|
||||
ServerDeploymentType,
|
||||
ServerFeature,
|
||||
} from '@affine/graphql';
|
||||
import type {
|
||||
BlobStorage,
|
||||
@@ -86,7 +86,6 @@ 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(
|
||||
@@ -95,7 +94,6 @@ 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,
|
||||
() => {
|
||||
@@ -477,24 +475,14 @@ class CloudWorkspaceFlavourProvider implements WorkspaceFlavourProvider {
|
||||
id: `${this.flavour}:${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,
|
||||
},
|
||||
},
|
||||
indexer: {
|
||||
name: 'IndexedDBIndexerStorage',
|
||||
opts: {
|
||||
flavour: this.flavour,
|
||||
type: 'workspace',
|
||||
id: workspaceId,
|
||||
},
|
||||
},
|
||||
indexerSync: {
|
||||
name: 'IndexedDBIndexerSyncStorage',
|
||||
opts: {
|
||||
@@ -535,6 +523,19 @@ class CloudWorkspaceFlavourProvider implements WorkspaceFlavourProvider {
|
||||
ServerDeploymentType.Selfhosted,
|
||||
},
|
||||
},
|
||||
indexer: this.server.config$.value.features.includes(
|
||||
ServerFeature.Indexer
|
||||
)
|
||||
? {
|
||||
name: 'CloudIndexerStorage',
|
||||
opts: {
|
||||
flavour: this.flavour,
|
||||
type: 'workspace',
|
||||
id: workspaceId,
|
||||
serverBaseUrl: this.server.serverMetadata.baseUrl,
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
v1: {
|
||||
doc: this.DocStorageV1Type
|
||||
|
||||
Reference in New Issue
Block a user