mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-23 13:29:02 +08: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:
@@ -9,7 +9,11 @@ import type { PeerStorageOptions } from './types';
|
||||
|
||||
export type { BlobSyncState } from './blob';
|
||||
export type { DocSyncDocState, DocSyncState } from './doc';
|
||||
export type { IndexerDocSyncState, IndexerSyncState } from './indexer';
|
||||
export type {
|
||||
IndexerDocSyncState,
|
||||
IndexerPreferOptions,
|
||||
IndexerSyncState,
|
||||
} from './indexer';
|
||||
|
||||
export interface SyncState {
|
||||
doc?: DocSyncState;
|
||||
@@ -65,7 +69,19 @@ export class Sync {
|
||||
])
|
||||
),
|
||||
});
|
||||
this.indexer = new IndexerSyncImpl(doc, indexer, indexerSync);
|
||||
this.indexer = new IndexerSyncImpl(
|
||||
doc,
|
||||
{
|
||||
local: indexer,
|
||||
remotes: Object.fromEntries(
|
||||
Object.entries(storages.remotes).map(([peerId, remote]) => [
|
||||
peerId,
|
||||
remote.get('indexer'),
|
||||
])
|
||||
),
|
||||
},
|
||||
indexerSync
|
||||
);
|
||||
|
||||
this.state$ = this.doc.state$.pipe(map(doc => ({ doc })));
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { readAllDocsFromRootDoc } from '@affine/reader';
|
||||
import { omit } from 'lodash-es';
|
||||
import {
|
||||
filter,
|
||||
first,
|
||||
@@ -7,21 +8,33 @@ import {
|
||||
ReplaySubject,
|
||||
share,
|
||||
Subject,
|
||||
switchMap,
|
||||
throttleTime,
|
||||
} from 'rxjs';
|
||||
import { applyUpdate, Doc as YDoc } from 'yjs';
|
||||
|
||||
import {
|
||||
type AggregateOptions,
|
||||
type AggregateResult,
|
||||
type DocStorage,
|
||||
IndexerDocument,
|
||||
type IndexerSchema,
|
||||
type IndexerStorage,
|
||||
type Query,
|
||||
type SearchOptions,
|
||||
type SearchResult,
|
||||
} from '../../storage';
|
||||
import { DummyIndexerStorage } from '../../storage/dummy/indexer';
|
||||
import type { IndexerSyncStorage } from '../../storage/indexer-sync';
|
||||
import { AsyncPriorityQueue } from '../../utils/async-priority-queue';
|
||||
import { fromPromise } from '../../utils/from-promise';
|
||||
import { takeUntilAbort } from '../../utils/take-until-abort';
|
||||
import { MANUALLY_STOP, throwIfAborted } from '../../utils/throw-if-aborted';
|
||||
import type { PeerStorageOptions } from '../types';
|
||||
import { crawlingDocData } from './crawler';
|
||||
|
||||
export type IndexerPreferOptions = 'local' | 'remote';
|
||||
|
||||
export interface IndexerSyncState {
|
||||
/**
|
||||
* Number of documents currently in the indexing queue
|
||||
@@ -59,6 +72,35 @@ export interface IndexerSync {
|
||||
addPriority(docId: string, priority: number): () => void;
|
||||
waitForCompleted(signal?: AbortSignal): Promise<void>;
|
||||
waitForDocCompleted(docId: string, signal?: AbortSignal): Promise<void>;
|
||||
|
||||
search<T extends keyof IndexerSchema, const O extends SearchOptions<T>>(
|
||||
table: T,
|
||||
query: Query<T>,
|
||||
options?: O & { prefer?: IndexerPreferOptions }
|
||||
): Promise<SearchResult<T, O>>;
|
||||
|
||||
aggregate<T extends keyof IndexerSchema, const O extends AggregateOptions<T>>(
|
||||
table: T,
|
||||
query: Query<T>,
|
||||
field: keyof IndexerSchema[T],
|
||||
options?: O & { prefer?: IndexerPreferOptions }
|
||||
): Promise<AggregateResult<T, O>>;
|
||||
|
||||
search$<T extends keyof IndexerSchema, const O extends SearchOptions<T>>(
|
||||
table: T,
|
||||
query: Query<T>,
|
||||
options?: O & { prefer?: IndexerPreferOptions }
|
||||
): Observable<SearchResult<T, O>>;
|
||||
|
||||
aggregate$<
|
||||
T extends keyof IndexerSchema,
|
||||
const O extends AggregateOptions<T>,
|
||||
>(
|
||||
table: T,
|
||||
query: Query<T>,
|
||||
field: keyof IndexerSchema[T],
|
||||
options?: O & { prefer?: IndexerPreferOptions }
|
||||
): Observable<AggregateResult<T, O>>;
|
||||
}
|
||||
|
||||
export class IndexerSyncImpl implements IndexerSync {
|
||||
@@ -70,6 +112,9 @@ export class IndexerSyncImpl implements IndexerSync {
|
||||
private readonly rootDocId = this.doc.spaceId;
|
||||
private readonly status = new IndexerSyncStatus(this.rootDocId);
|
||||
|
||||
private readonly indexer: IndexerStorage;
|
||||
private readonly remote?: IndexerStorage;
|
||||
|
||||
state$ = this.status.state$.pipe(
|
||||
// throttle the state to 1 second to avoid spamming the UI
|
||||
throttleTime(1000, undefined, {
|
||||
@@ -106,9 +151,13 @@ export class IndexerSyncImpl implements IndexerSync {
|
||||
|
||||
constructor(
|
||||
readonly doc: DocStorage,
|
||||
readonly indexer: IndexerStorage,
|
||||
readonly peers: PeerStorageOptions<IndexerStorage>,
|
||||
readonly indexerSync: IndexerSyncStorage
|
||||
) {}
|
||||
) {
|
||||
// sync feature only works on local indexer
|
||||
this.indexer = this.peers.local;
|
||||
this.remote = Object.values(this.peers.remotes).find(remote => !!remote);
|
||||
}
|
||||
|
||||
start() {
|
||||
if (this.abort) {
|
||||
@@ -439,6 +488,116 @@ export class IndexerSyncImpl implements IndexerSync {
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
async search<T extends keyof IndexerSchema, const O extends SearchOptions<T>>(
|
||||
table: T,
|
||||
query: Query<T>,
|
||||
options?: O & { prefer?: IndexerPreferOptions }
|
||||
): Promise<SearchResult<T, O>> {
|
||||
if (
|
||||
options?.prefer === 'remote' &&
|
||||
this.remote &&
|
||||
!(this.remote instanceof DummyIndexerStorage)
|
||||
) {
|
||||
await this.remote.connection.waitForConnected();
|
||||
return await this.remote.search(table, query, omit(options, 'prefer'));
|
||||
} else {
|
||||
await this.indexer.connection.waitForConnected();
|
||||
return await this.indexer.search(table, query, omit(options, 'prefer'));
|
||||
}
|
||||
}
|
||||
|
||||
async aggregate<
|
||||
T extends keyof IndexerSchema,
|
||||
const O extends AggregateOptions<T>,
|
||||
>(
|
||||
table: T,
|
||||
query: Query<T>,
|
||||
field: keyof IndexerSchema[T],
|
||||
options?: O & { prefer?: IndexerPreferOptions }
|
||||
): Promise<AggregateResult<T, O>> {
|
||||
if (
|
||||
options?.prefer === 'remote' &&
|
||||
this.remote &&
|
||||
!(this.remote instanceof DummyIndexerStorage)
|
||||
) {
|
||||
await this.remote.connection.waitForConnected();
|
||||
return await this.remote.aggregate(
|
||||
table,
|
||||
query,
|
||||
field,
|
||||
omit(options, 'prefer')
|
||||
);
|
||||
} else {
|
||||
await this.indexer.connection.waitForConnected();
|
||||
return await this.indexer.aggregate(
|
||||
table,
|
||||
query,
|
||||
field,
|
||||
omit(options, 'prefer')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
search$<T extends keyof IndexerSchema, const O extends SearchOptions<T>>(
|
||||
table: T,
|
||||
query: Query<T>,
|
||||
options?: O & { prefer?: IndexerPreferOptions }
|
||||
): Observable<SearchResult<T, O>> {
|
||||
if (
|
||||
options?.prefer === 'remote' &&
|
||||
this.remote &&
|
||||
!(this.remote instanceof DummyIndexerStorage)
|
||||
) {
|
||||
const remote = this.remote;
|
||||
return fromPromise(signal =>
|
||||
remote.connection.waitForConnected(signal)
|
||||
).pipe(
|
||||
switchMap(() => remote.search$(table, query, omit(options, 'prefer')))
|
||||
);
|
||||
} else {
|
||||
return fromPromise(signal =>
|
||||
this.indexer.connection.waitForConnected(signal)
|
||||
).pipe(
|
||||
switchMap(() =>
|
||||
this.indexer.search$(table, query, omit(options, 'prefer'))
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
aggregate$<
|
||||
T extends keyof IndexerSchema,
|
||||
const O extends AggregateOptions<T>,
|
||||
>(
|
||||
table: T,
|
||||
query: Query<T>,
|
||||
field: keyof IndexerSchema[T],
|
||||
options?: O & { prefer?: IndexerPreferOptions }
|
||||
): Observable<AggregateResult<T, O>> {
|
||||
if (
|
||||
options?.prefer === 'remote' &&
|
||||
this.remote &&
|
||||
!(this.remote instanceof DummyIndexerStorage)
|
||||
) {
|
||||
const remote = this.remote;
|
||||
return fromPromise(signal =>
|
||||
remote.connection.waitForConnected(signal)
|
||||
).pipe(
|
||||
switchMap(() =>
|
||||
remote.aggregate$(table, query, field, omit(options, 'prefer'))
|
||||
)
|
||||
);
|
||||
} else {
|
||||
return fromPromise(signal =>
|
||||
this.indexer.connection.waitForConnected(signal)
|
||||
).pipe(
|
||||
switchMap(() =>
|
||||
this.indexer.aggregate$(table, query, field, omit(options, 'prefer'))
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class IndexerSyncStatus {
|
||||
|
||||
Reference in New Issue
Block a user