chore: search with ai summary

This commit is contained in:
Nathan
2025-04-14 10:36:42 +08:00
parent 4997ac99cf
commit 4d172761ce
40 changed files with 1445 additions and 1234 deletions

View File

@ -49,7 +49,7 @@ void main() {
// The score should be higher for "ViewOna" thus it should be shown first
final secondDocumentWidget = tester
.widget(find.byType(SearchResultTile).first) as SearchResultTile;
expect(secondDocumentWidget.result.data, secondDocument);
expect(secondDocumentWidget.item.data, secondDocument);
// Change search to "ViewOne"
await tester.enterText(searchFieldFinder, firstDocument);
@ -59,7 +59,7 @@ void main() {
final firstDocumentWidget = tester.widget(
find.byType(SearchResultTile).first,
) as SearchResultTile;
expect(firstDocumentWidget.result.data, firstDocument);
expect(firstDocumentWidget.item.data, firstDocument);
});
testWidgets('Displaying icons in search results', (tester) async {

View File

@ -2,10 +2,8 @@ import 'dart:async';
import 'package:appflowy/plugins/trash/application/trash_listener.dart';
import 'package:appflowy/plugins/trash/application/trash_service.dart';
import 'package:appflowy/workspace/application/command_palette/search_listener.dart';
import 'package:appflowy/workspace/application/command_palette/search_service.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/trash.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-search/notification.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-search/result.pb.dart';
import 'package:bloc/bloc.dart';
import 'package:flutter/foundation.dart';
@ -13,184 +11,285 @@ import 'package:freezed_annotation/freezed_annotation.dart';
part 'command_palette_bloc.freezed.dart';
const _searchChannel = 'CommandPalette';
class CommandPaletteBloc
extends Bloc<CommandPaletteEvent, CommandPaletteState> {
CommandPaletteBloc() : super(CommandPaletteState.initial()) {
_searchListener.start(
onResultsChanged: _onResultsChanged,
);
// Register event handlers
on<_SearchChanged>(_onSearchChanged);
on<_PerformSearch>(_onPerformSearch);
on<_NewSearchStream>(_onNewSearchStream);
on<_ResultsChanged>(_onResultsChanged);
on<_TrashChanged>(_onTrashChanged);
on<_WorkspaceChanged>(_onWorkspaceChanged);
on<_ClearSearch>(_onClearSearch);
_initTrash();
_dispatch();
}
Timer? _debounceOnChanged;
final TrashService _trashService = TrashService();
final SearchListener _searchListener = SearchListener(
channel: _searchChannel,
);
final TrashListener _trashListener = TrashListener();
String? _oldQuery;
String? _workspaceId;
int _messagesReceived = 0;
@override
Future<void> close() {
_trashListener.close();
_searchListener.stop();
_debounceOnChanged?.cancel();
state.searchResponseStream?.dispose();
return super.close();
}
void _dispatch() {
on<CommandPaletteEvent>((event, emit) async {
event.when(
searchChanged: _debounceOnSearchChanged,
trashChanged: (trash) async {
if (trash != null) {
return emit(state.copyWith(trash: trash));
}
final trashOrFailure = await _trashService.readTrash();
final trashRes = trashOrFailure.fold(
(trash) => trash,
(error) => null,
);
if (trashRes != null) {
emit(state.copyWith(trash: trashRes.items));
}
},
performSearch: (search) async {
if (search.isNotEmpty && search != state.query) {
_oldQuery = state.query;
emit(state.copyWith(query: search, isLoading: true));
await SearchBackendService.performSearch(
search,
workspaceId: _workspaceId,
channel: _searchChannel,
);
} else {
emit(state.copyWith(query: null, isLoading: false, results: []));
}
},
resultsChanged: (results) {
if (state.query != _oldQuery) {
emit(state.copyWith(results: [], isLoading: true));
_oldQuery = state.query;
_messagesReceived = 0;
}
if (state.query != results.query) {
return;
}
_messagesReceived++;
emit(
state.copyWith(
results: _filterDuplicates(results.items),
isLoading: _messagesReceived != results.sends.toInt(),
),
);
},
workspaceChanged: (workspaceId) {
_workspaceId = workspaceId;
emit(state.copyWith(results: [], query: '', isLoading: false));
},
clearSearch: () {
emit(state.copyWith(results: [], query: '', isLoading: false));
},
);
});
}
Future<void> _initTrash() async {
// Start listening for trash updates
_trashListener.start(
trashUpdated: (trashOrFailed) {
final trash = trashOrFailed.toNullable();
add(CommandPaletteEvent.trashChanged(trash: trash));
add(
CommandPaletteEvent.trashChanged(
trash: trashOrFailed.toNullable(),
),
);
},
);
// Read initial trash state and forward results
final trashOrFailure = await _trashService.readTrash();
final trash = trashOrFailure.toNullable();
add(CommandPaletteEvent.trashChanged(trash: trash?.items));
}
void _debounceOnSearchChanged(String value) {
_debounceOnChanged?.cancel();
_debounceOnChanged = Timer(
const Duration(milliseconds: 300),
() => _performSearch(value),
add(
CommandPaletteEvent.trashChanged(
trash: trashOrFailure.toNullable()?.items,
),
);
}
List<SearchResultPB> _filterDuplicates(List<SearchResultPB> results) {
final currentItems = [...state.results];
final res = [...results];
for (final item in results) {
final duplicateIndex = currentItems.indexWhere((a) => a.id == item.id);
if (duplicateIndex == -1) {
continue;
}
final duplicate = currentItems[duplicateIndex];
if (item.score < duplicate.score) {
res.remove(item);
} else {
currentItems.remove(duplicate);
}
}
return res..addAll(currentItems);
FutureOr<void> _onSearchChanged(
_SearchChanged event,
Emitter<CommandPaletteState> emit,
) {
_debounceOnChanged?.cancel();
_debounceOnChanged = Timer(
const Duration(milliseconds: 300),
() {
if (!isClosed) {
add(CommandPaletteEvent.performSearch(search: event.search));
}
},
);
}
void _performSearch(String value) =>
add(CommandPaletteEvent.performSearch(search: value));
FutureOr<void> _onPerformSearch(
_PerformSearch event,
Emitter<CommandPaletteState> emit,
) async {
if (event.search.isNotEmpty && event.search != state.query) {
_oldQuery = state.query;
emit(state.copyWith(query: event.search, isLoading: true));
void _onResultsChanged(SearchResultNotificationPB results) =>
add(CommandPaletteEvent.resultsChanged(results: results));
// Fire off search asynchronously (fire and forget)
unawaited(
SearchBackendService.performSearch(
event.search,
workspaceId: _workspaceId,
).then(
(result) => result.onSuccess((stream) {
if (!isClosed) {
add(CommandPaletteEvent.newSearchStream(stream: stream));
}
}),
),
);
} else {
// Clear state if search is empty or unchanged
emit(
state.copyWith(
query: null,
isLoading: false,
resultItems: [],
resultSummaries: [],
),
);
}
}
FutureOr<void> _onNewSearchStream(
_NewSearchStream event,
Emitter<CommandPaletteState> emit,
) {
state.searchResponseStream?.dispose();
emit(
state.copyWith(
searchId: event.stream.searchId,
searchResponseStream: event.stream,
),
);
event.stream.listen(
onItems: (
List<SearchResponseItemPB> items,
String searchId,
bool isLoading,
) {
if (_isActiveSearch(searchId)) {
add(
CommandPaletteEvent.resultsChanged(
items: items,
searchId: searchId,
isLoading: isLoading,
),
);
}
},
onSummaries: (
List<SearchSummaryPB> summaries,
String searchId,
bool isLoading,
) {
if (_isActiveSearch(searchId)) {
add(
CommandPaletteEvent.resultsChanged(
summaries: summaries,
searchId: searchId,
isLoading: isLoading,
),
);
}
},
onFinished: (String searchId) {
if (_isActiveSearch(searchId)) {
add(
CommandPaletteEvent.resultsChanged(
searchId: searchId,
isLoading: false,
),
);
}
},
);
}
FutureOr<void> _onResultsChanged(
_ResultsChanged event,
Emitter<CommandPaletteState> emit,
) async {
// If query was updated since last emission, clear previous results.
if (state.query != _oldQuery) {
emit(
state.copyWith(
resultItems: [],
resultSummaries: [],
isLoading: event.isLoading,
),
);
_oldQuery = state.query;
}
// Check for outdated search streams
if (state.searchId != event.searchId) return;
final updatedItems =
event.items ?? List<SearchResponseItemPB>.from(state.resultItems);
final updatedSummaries =
event.summaries ?? List<SearchSummaryPB>.from(state.resultSummaries);
emit(
state.copyWith(
resultItems: updatedItems,
resultSummaries: updatedSummaries,
isLoading: event.isLoading,
),
);
}
// Update trash state and, in case of null, retry reading trash from the service
FutureOr<void> _onTrashChanged(
_TrashChanged event,
Emitter<CommandPaletteState> emit,
) async {
if (event.trash != null) {
emit(state.copyWith(trash: event.trash!));
} else {
final trashOrFailure = await _trashService.readTrash();
trashOrFailure.fold((trash) {
emit(state.copyWith(trash: trash.items));
}, (error) {
// Optionally handle error; otherwise, we simply do nothing.
});
}
}
// Update the workspace and clear current search results and query
FutureOr<void> _onWorkspaceChanged(
_WorkspaceChanged event,
Emitter<CommandPaletteState> emit,
) {
_workspaceId = event.workspaceId;
emit(
state.copyWith(
query: '',
resultItems: [],
resultSummaries: [],
isLoading: false,
),
);
}
// Clear search state
FutureOr<void> _onClearSearch(
_ClearSearch event,
Emitter<CommandPaletteState> emit,
) {
emit(
state.copyWith(
query: '',
resultItems: [],
resultSummaries: [],
isLoading: false,
searchId: null,
),
);
}
bool _isActiveSearch(String searchId) =>
!isClosed && state.searchId == searchId;
}
@freezed
class CommandPaletteEvent with _$CommandPaletteEvent {
const factory CommandPaletteEvent.searchChanged({required String search}) =
_SearchChanged;
const factory CommandPaletteEvent.performSearch({required String search}) =
_PerformSearch;
const factory CommandPaletteEvent.newSearchStream({
required SearchResponseStream stream,
}) = _NewSearchStream;
const factory CommandPaletteEvent.resultsChanged({
required SearchResultNotificationPB results,
required String searchId,
required bool isLoading,
List<SearchResponseItemPB>? items,
List<SearchSummaryPB>? summaries,
}) = _ResultsChanged;
const factory CommandPaletteEvent.trashChanged({
@Default(null) List<TrashPB>? trash,
}) = _TrashChanged;
const factory CommandPaletteEvent.workspaceChanged({
@Default(null) String? workspaceId,
}) = _WorkspaceChanged;
const factory CommandPaletteEvent.clearSearch() = _ClearSearch;
}
@freezed
class CommandPaletteState with _$CommandPaletteState {
const CommandPaletteState._();
const factory CommandPaletteState({
@Default(null) String? query,
required List<SearchResultPB> results,
@Default([]) List<SearchResponseItemPB> resultItems,
@Default([]) List<SearchSummaryPB> resultSummaries,
@Default(null) SearchResponseStream? searchResponseStream,
required bool isLoading,
@Default([]) List<TrashPB> trash,
@Default(null) String? searchId,
}) = _CommandPaletteState;
factory CommandPaletteState.initial() =>
const CommandPaletteState(results: [], isLoading: false);
const CommandPaletteState(isLoading: false);
}

View File

@ -1,74 +0,0 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:appflowy/core/notification/search_notification.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-search/notification.pb.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:flowy_infra/notifier.dart';
// Do not modify!
const _searchObjectId = "SEARCH_IDENTIFIER";
class SearchListener {
SearchListener({this.channel});
/// Use this to filter out search results from other channels.
///
/// If null, it will receive search results from all
/// channels, otherwise it will only receive search results from the specified
/// channel.
///
final String? channel;
PublishNotifier<SearchResultNotificationPB>? _updateNotifier =
PublishNotifier();
PublishNotifier<SearchResultNotificationPB>? _updateDidCloseNotifier =
PublishNotifier();
SearchNotificationListener? _listener;
void start({
void Function(SearchResultNotificationPB)? onResultsChanged,
void Function(SearchResultNotificationPB)? onResultsClosed,
}) {
if (onResultsChanged != null) {
_updateNotifier?.addPublishListener(onResultsChanged);
}
if (onResultsClosed != null) {
_updateDidCloseNotifier?.addPublishListener(onResultsClosed);
}
_listener = SearchNotificationListener(
objectId: _searchObjectId,
handler: _handler,
channel: channel,
);
}
void _handler(
SearchNotification ty,
FlowyResult<Uint8List, FlowyError> result,
) {
switch (ty) {
case SearchNotification.DidUpdateResults:
result.fold(
(payload) => _updateNotifier?.value =
SearchResultNotificationPB.fromBuffer(payload),
(err) => Log.error(err),
);
break;
default:
break;
}
}
Future<void> stop() async {
await _listener?.stop();
_updateNotifier?.dispose();
_updateNotifier = null;
_updateDidCloseNotifier?.dispose();
_updateDidCloseNotifier = null;
}
}

View File

@ -5,7 +5,7 @@ import 'package:appflowy_backend/protobuf/flowy-search/result.pb.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter/material.dart';
extension GetIcon on SearchResultPB {
extension GetIcon on SearchResponseItemPB {
Widget? getIcon() {
final iconValue = icon.value, iconType = icon.ty;
if (iconType == ResultIconTypePB.Emoji) {

View File

@ -1,22 +1,107 @@
import 'dart:async';
import 'dart:ffi';
import 'dart:isolate';
import 'dart:typed_data';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-search/notification.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-search/query.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-search/result.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-search/search_filter.pb.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:nanoid/nanoid.dart';
import 'package:fixnum/fixnum.dart';
class SearchBackendService {
static Future<FlowyResult<void, FlowyError>> performSearch(
static Future<FlowyResult<SearchResponseStream, FlowyError>> performSearch(
String keyword, {
String? workspaceId,
String? channel,
}) async {
final searchId = nanoid(6);
final stream = SearchResponseStream(searchId: searchId);
final filter = SearchFilterPB(workspaceId: workspaceId);
final request = SearchQueryPB(
search: keyword,
filter: filter,
channel: channel,
searchId: searchId,
streamPort: Int64(stream.nativePort),
);
return SearchEventSearch(request).send();
unawaited(SearchEventSearch(request).send());
return FlowyResult.success(stream);
}
}
class SearchResponseStream {
SearchResponseStream({required this.searchId}) {
_port.handler = _controller.add;
_subscription = _controller.stream.listen(
(Uint8List data) => _onResultsChanged(data),
);
}
final String searchId;
final RawReceivePort _port = RawReceivePort();
final StreamController<Uint8List> _controller = StreamController.broadcast();
late StreamSubscription<Uint8List> _subscription;
void Function(
List<SearchResponseItemPB> items,
String searchId,
bool isLoading,
)? _onItems;
void Function(
List<SearchSummaryPB> summaries,
String searchId,
bool isLoading,
)? _onSummaries;
void Function(String searchId)? _onFinished;
int get nativePort => _port.sendPort.nativePort;
Future<void> dispose() async {
await _subscription.cancel();
_port.close();
}
void _onResultsChanged(Uint8List data) {
final response = SearchResponsePB.fromBuffer(data);
if (response.hasResult()) {
if (response.result.hasSearchResult()) {
_onItems?.call(
response.result.searchResult.items,
searchId,
response.isLoading,
);
}
if (response.result.hasSearchSummary()) {
_onSummaries?.call(
response.result.searchSummary.items,
searchId,
response.isLoading,
);
}
} else {
_onFinished?.call(searchId);
}
}
void listen({
required void Function(
List<SearchResponseItemPB> items,
String searchId,
bool isLoading,
)? onItems,
required void Function(
List<SearchSummaryPB> summaries,
String searchId,
bool isLoading,
)? onSummaries,
required void Function(String searchId)? onFinished,
}) {
_onItems = onItems;
_onSummaries = onSummaries;
_onFinished = onFinished;
}
}

View File

@ -4,7 +4,6 @@ import 'package:appflowy/workspace/presentation/command_palette/widgets/recent_v
import 'package:appflowy/workspace/presentation/command_palette/widgets/search_field.dart';
import 'package:appflowy/workspace/presentation/command_palette/widgets/search_results_list.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@ -135,11 +134,15 @@ class CommandPaletteModal extends StatelessWidget {
builder: (context, state) => FlowyDialog(
alignment: Alignment.topCenter,
insetPadding: const EdgeInsets.only(top: 100),
constraints: const BoxConstraints(maxHeight: 420, maxWidth: 510),
constraints: const BoxConstraints(
maxHeight: 520,
maxWidth: 510,
minHeight: 420,
),
expandHeight: false,
child: shortcutBuilder(
// Change mainAxisSize to max so Expanded works correctly.
Column(
mainAxisSize: MainAxisSize.min,
children: [
SearchField(query: state.query, isLoading: state.isLoading),
if (state.query?.isEmpty ?? true) ...[
@ -150,23 +153,26 @@ class CommandPaletteModal extends StatelessWidget {
),
),
],
if (state.results.isNotEmpty &&
if (state.resultItems.isNotEmpty &&
(state.query?.isNotEmpty ?? false)) ...[
const Divider(height: 0),
Flexible(
child: SearchResultsList(
child: SearchResultList(
trash: state.trash,
results: state.results,
resultItems: state.resultItems,
resultSummaries: state.resultSummaries,
),
),
] else if ((state.query?.isNotEmpty ?? false) &&
]
// When there are no results and the query is not empty and not loading,
// show the no results message, centered in the available space.
else if ((state.query?.isNotEmpty ?? false) &&
!state.isLoading) ...[
const _NoResultsHint(),
const Divider(height: 0),
Expanded(
child: const _NoResultsHint(),
),
],
_CommandPaletteFooter(
shouldShow: state.results.isNotEmpty &&
(state.query?.isNotEmpty ?? false),
),
],
),
),
@ -175,57 +181,16 @@ class CommandPaletteModal extends StatelessWidget {
}
}
/// Updated _NoResultsHint now centers its content.
class _NoResultsHint extends StatelessWidget {
const _NoResultsHint();
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Divider(height: 0),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: FlowyText.regular(
LocaleKeys.commandPalette_noResultsHint.tr(),
textAlign: TextAlign.left,
),
),
],
);
}
}
class _CommandPaletteFooter extends StatelessWidget {
const _CommandPaletteFooter({required this.shouldShow});
final bool shouldShow;
@override
Widget build(BuildContext context) {
if (!shouldShow) {
return const SizedBox.shrink();
}
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
border: Border(top: BorderSide(color: Theme.of(context).dividerColor)),
),
child: Row(
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 1),
decoration: BoxDecoration(
color: AFThemeExtension.of(context).lightGreyHover,
borderRadius: BorderRadius.circular(4),
),
child: const FlowyText.semibold('TAB', fontSize: 10),
),
const HSpace(4),
FlowyText(LocaleKeys.commandPalette_navigateHint.tr(), fontSize: 11),
],
return Center(
child: FlowyText.regular(
LocaleKeys.commandPalette_noResultsHint.tr(),
textAlign: TextAlign.center,
),
);
}

View File

@ -7,7 +7,6 @@ import 'package:appflowy/workspace/application/command_palette/command_palette_b
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/style_widget/text_field.dart';
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
@ -25,28 +24,31 @@ class SearchField extends StatefulWidget {
class _SearchFieldState extends State<SearchField> {
late final FocusNode focusNode;
late final controller = TextEditingController(text: widget.query);
late final TextEditingController controller;
@override
void initState() {
super.initState();
focusNode = FocusNode(
onKeyEvent: (node, event) {
if (node.hasFocus &&
event is KeyDownEvent &&
event.logicalKey == LogicalKeyboardKey.arrowDown) {
node.nextFocus();
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
},
);
controller = TextEditingController(text: widget.query);
focusNode = FocusNode(onKeyEvent: _handleKeyEvent);
focusNode.requestFocus();
controller.selection = TextSelection(
baseOffset: 0,
extentOffset: controller.text.length,
);
// Update the text selection after the first frame
WidgetsBinding.instance.addPostFrameCallback((_) {
controller.selection = TextSelection(
baseOffset: 0,
extentOffset: controller.text.length,
);
});
}
KeyEventResult _handleKeyEvent(FocusNode node, KeyEvent event) {
if (node.hasFocus &&
event is KeyDownEvent &&
event.logicalKey == LogicalKeyboardKey.arrowDown) {
node.nextFocus();
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
}
@override
@ -56,21 +58,83 @@ class _SearchFieldState extends State<SearchField> {
super.dispose();
}
Widget _buildSuffixIcon(BuildContext context) {
return ValueListenableBuilder<TextEditingValue>(
valueListenable: controller,
builder: (context, value, _) {
final hasText = value.text.trim().isNotEmpty;
final clearIcon = Container(
padding: const EdgeInsets.all(1),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: AFThemeExtension.of(context).lightGreyHover,
),
child: const FlowySvg(
FlowySvgs.close_s,
size: Size.square(16),
),
);
return AnimatedOpacity(
opacity: hasText ? 1.0 : 0.0,
duration: const Duration(milliseconds: 200),
child: hasText
? FlowyTooltip(
message: LocaleKeys.commandPalette_clearSearchTooltip.tr(),
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: _clearSearch,
child: clearIcon,
),
),
)
: clearIcon,
);
},
);
}
@override
Widget build(BuildContext context) {
// Cache theme and text styles
final theme = Theme.of(context);
final textStyle = theme.textTheme.bodySmall?.copyWith(fontSize: 14);
final hintStyle = theme.textTheme.bodySmall?.copyWith(
fontSize: 14,
color: theme.hintColor,
);
// Choose the leading icon based on loading state
final Widget leadingIcon = widget.isLoading
? FlowyTooltip(
message: LocaleKeys.commandPalette_loadingTooltip.tr(),
child: const SizedBox(
width: 20,
height: 20,
child: Padding(
padding: EdgeInsets.all(3.0),
child: CircularProgressIndicator(strokeWidth: 2.0),
),
),
)
: SizedBox(
width: 20,
height: 20,
child: FlowySvg(
FlowySvgs.search_m,
color: theme.hintColor,
),
);
return Row(
children: [
const HSpace(12),
FlowySvg(
FlowySvgs.search_m,
color: Theme.of(context).hintColor,
),
leadingIcon,
Expanded(
child: FlowyTextField(
focusNode: focusNode,
controller: controller,
textStyle:
Theme.of(context).textTheme.bodySmall?.copyWith(fontSize: 14),
textStyle: textStyle,
decoration: InputDecoration(
constraints: const BoxConstraints(maxHeight: 48),
contentPadding: const EdgeInsets.symmetric(horizontal: 12),
@ -80,72 +144,14 @@ class _SearchFieldState extends State<SearchField> {
),
isDense: false,
hintText: LocaleKeys.commandPalette_placeholder.tr(),
hintStyle: Theme.of(context).textTheme.bodySmall?.copyWith(
fontSize: 14,
color: Theme.of(context).hintColor,
),
errorStyle: Theme.of(context)
.textTheme
.bodySmall!
.copyWith(color: Theme.of(context).colorScheme.error),
hintStyle: hintStyle,
errorStyle: theme.textTheme.bodySmall!
.copyWith(color: theme.colorScheme.error),
suffix: Row(
mainAxisSize: MainAxisSize.min,
children: [
AnimatedOpacity(
opacity: controller.text.trim().isNotEmpty ? 1 : 0,
duration: const Duration(milliseconds: 200),
child: Builder(
builder: (context) {
final icon = Container(
padding: const EdgeInsets.all(1),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: AFThemeExtension.of(context).lightGreyHover,
),
child: const FlowySvg(
FlowySvgs.close_s,
size: Size.square(16),
),
);
if (controller.text.isEmpty) {
return icon;
}
return FlowyTooltip(
message:
LocaleKeys.commandPalette_clearSearchTooltip.tr(),
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: controller.text.trim().isNotEmpty
? _clearSearch
: null,
child: icon,
),
),
);
},
),
),
_buildSuffixIcon(context),
const HSpace(8),
FlowyTooltip(
message: LocaleKeys.commandPalette_betaTooltip.tr(),
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 5,
vertical: 2,
),
decoration: BoxDecoration(
color: AFThemeExtension.of(context).lightGreyHover,
borderRadius: BorderRadius.circular(4),
),
child: FlowyText.semibold(
LocaleKeys.commandPalette_betaLabel.tr(),
fontSize: 11,
lineHeight: 1.2,
),
),
),
],
),
counterText: "",
@ -155,9 +161,7 @@ class _SearchFieldState extends State<SearchField> {
),
errorBorder: OutlineInputBorder(
borderRadius: Corners.s8Border,
borderSide: BorderSide(
color: Theme.of(context).colorScheme.error,
),
borderSide: BorderSide(color: theme.colorScheme.error),
),
),
onChanged: (value) => context
@ -165,17 +169,6 @@ class _SearchFieldState extends State<SearchField> {
.add(CommandPaletteEvent.searchChanged(search: value)),
),
),
if (widget.isLoading) ...[
FlowyTooltip(
message: LocaleKeys.commandPalette_loadingTooltip.tr(),
child: const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2.5),
),
),
const HSpace(12),
],
],
);
}

View File

@ -16,12 +16,12 @@ import 'package:flutter/services.dart';
class SearchResultTile extends StatefulWidget {
const SearchResultTile({
super.key,
required this.result,
required this.item,
required this.onSelected,
this.isTrashed = false,
});
final SearchResultPB result;
final SearchResponseItemPB item;
final VoidCallback onSelected;
final bool isTrashed;
@ -31,7 +31,6 @@ class SearchResultTile extends StatefulWidget {
class _SearchResultTileState extends State<SearchResultTile> {
bool _hasFocus = false;
final focusNode = FocusNode();
@override
@ -40,104 +39,132 @@ class _SearchResultTileState extends State<SearchResultTile> {
super.dispose();
}
/// Helper to handle the selection action.
void _handleSelection() {
widget.onSelected();
getIt<ActionNavigationBloc>().add(
ActionNavigationEvent.performAction(
action: NavigationAction(objectId: widget.item.viewId),
),
);
}
/// Helper to clean up preview text.
String _cleanPreview(String preview) {
return preview.replaceAll('\n', ' ').trim();
}
@override
Widget build(BuildContext context) {
final title = widget.result.data.orDefault(
final title = widget.item.data.orDefault(
LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
);
final icon = widget.result.getIcon();
final cleanedPreview = _cleanPreview(widget.result.preview);
final icon = widget.item.getIcon();
final cleanedPreview = _cleanPreview(widget.item.preview);
final hasPreview = cleanedPreview.isNotEmpty;
final trashHintText =
widget.isTrashed ? LocaleKeys.commandPalette_fromTrashHint.tr() : null;
// Build the tile content based on preview availability.
Widget tileContent;
if (hasPreview) {
tileContent = Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
if (icon != null) ...[
SizedBox(width: 24, child: icon),
const HSpace(6),
],
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (widget.isTrashed)
FlowyText(
trashHintText!,
color: AFThemeExtension.of(context)
.textColor
.withAlpha(175),
fontSize: 10,
),
FlowyText(
title,
overflow: TextOverflow.ellipsis,
),
],
),
),
],
),
const VSpace(4),
_DocumentPreview(preview: cleanedPreview),
],
);
} else {
tileContent = Row(
mainAxisSize: MainAxisSize.min,
children: [
if (icon != null) ...[
SizedBox(width: 24, child: icon),
const HSpace(6),
],
Flexible(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (widget.isTrashed)
FlowyText(
trashHintText!,
color:
AFThemeExtension.of(context).textColor.withAlpha(175),
fontSize: 10,
),
FlowyText(
title,
overflow: TextOverflow.ellipsis,
),
],
),
),
],
);
}
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
widget.onSelected();
getIt<ActionNavigationBloc>().add(
ActionNavigationEvent.performAction(
action: NavigationAction(objectId: widget.result.viewId),
),
);
},
onTap: _handleSelection,
child: Focus(
focusNode: focusNode,
onKeyEvent: (node, event) {
if (event is! KeyDownEvent) {
return KeyEventResult.ignored;
}
if (event is! KeyDownEvent) return KeyEventResult.ignored;
if (event.logicalKey == LogicalKeyboardKey.enter) {
widget.onSelected();
getIt<ActionNavigationBloc>().add(
ActionNavigationEvent.performAction(
action: NavigationAction(objectId: widget.result.viewId),
),
);
_handleSelection();
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
},
onFocusChange: (hasFocus) => setState(() => _hasFocus = hasFocus),
child: FlowyHover(
isSelected: () => _hasFocus,
style: HoverStyle(
borderRadius: BorderRadius.circular(8),
hoverColor:
Theme.of(context).colorScheme.primary.withValues(alpha: 0.1),
foregroundColorOnHover: AFThemeExtension.of(context).textColor,
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
// page icon
if (icon != null) ...[
SizedBox(width: 24, child: icon),
const HSpace(6),
],
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// if the result is trashed, show a hint
if (widget.isTrashed) ...[
FlowyText(
LocaleKeys.commandPalette_fromTrashHint.tr(),
color: AFThemeExtension.of(context)
.textColor
.withAlpha(175),
fontSize: 10,
),
],
// page title
FlowyText(
title,
overflow: TextOverflow.ellipsis,
),
],
),
),
],
),
// content preview
if (cleanedPreview.isNotEmpty) ...[
const VSpace(4),
_DocumentPreview(preview: cleanedPreview),
],
],
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 6),
child: ConstrainedBox(
constraints: const BoxConstraints(minHeight: 30),
child: tileContent,
),
),
),
),
);
}
String _cleanPreview(String preview) {
return preview.replaceAll('\n', ' ').trim();
}
}
class _DocumentPreview extends StatelessWidget {
@ -147,9 +174,9 @@ class _DocumentPreview extends StatelessWidget {
@override
Widget build(BuildContext context) {
// Combine the horizontal padding for clarity:
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16) +
const EdgeInsets.only(left: 14),
padding: const EdgeInsets.fromLTRB(30, 0, 16, 0),
child: FlowyText.regular(
preview,
color: Theme.of(context).hintColor,

View File

@ -7,41 +7,99 @@ import 'package:appflowy_backend/protobuf/flowy-search/result.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
class SearchResultsList extends StatelessWidget {
const SearchResultsList({
super.key,
class SearchResultList extends StatelessWidget {
const SearchResultList({
required this.trash,
required this.results,
required this.resultItems,
required this.resultSummaries,
super.key,
});
final List<TrashPB> trash;
final List<SearchResultPB> results;
final List<SearchResponseItemPB> resultItems;
final List<SearchSummaryPB> resultSummaries;
Widget _buildSectionHeader(String title) => Padding(
padding: const EdgeInsets.symmetric(vertical: 8) +
const EdgeInsets.only(left: 8),
child: Opacity(
opacity: 0.6,
child: FlowyText(title, fontSize: 12),
),
);
Widget _buildSummariesSection() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSectionHeader(LocaleKeys.commandPalette_aiSummary.tr()),
ListView.separated(
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
itemCount: resultSummaries.length,
separatorBuilder: (_, __) => const Divider(height: 0),
itemBuilder: (_, index) => SearchSummaryTile(
summary: resultSummaries[index],
),
),
],
);
}
Widget _buildResultsSection(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 8),
_buildSectionHeader(LocaleKeys.commandPalette_bestMatches.tr()),
ListView.separated(
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
itemCount: resultItems.length,
separatorBuilder: (_, __) => const Divider(height: 0),
itemBuilder: (_, index) {
final item = resultItems[index];
return SearchResultTile(
item: item,
onSelected: () => FlowyOverlay.pop(context),
isTrashed: trash.any((t) => t.id == item.viewId),
);
},
),
],
);
}
@override
Widget build(BuildContext context) {
return ListView.separated(
shrinkWrap: true,
physics: const ClampingScrollPhysics(),
separatorBuilder: (_, __) => const Divider(height: 0),
itemCount: results.length + 1,
itemBuilder: (_, index) {
if (index == 0) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8) +
const EdgeInsets.only(left: 16),
child: FlowyText(
LocaleKeys.commandPalette_bestMatches.tr(),
),
);
}
final result = results[index - 1];
return SearchResultTile(
result: result,
onSelected: () => FlowyOverlay.pop(context),
isTrashed: trash.any((t) => t.id == result.viewId),
);
},
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 6),
child: ListView(
shrinkWrap: true,
physics: const ClampingScrollPhysics(),
children: [
if (resultSummaries.isNotEmpty) _buildSummariesSection(),
const VSpace(10),
if (resultItems.isNotEmpty) _buildResultsSection(context),
],
),
);
}
}
class SearchSummaryTile extends StatelessWidget {
const SearchSummaryTile({required this.summary, super.key});
final SearchSummaryPB summary;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: FlowyText(
summary.content,
maxLines: 10,
),
);
}
}

View File

@ -2693,6 +2693,7 @@
"commandPalette": {
"placeholder": "Search or ask a question...",
"bestMatches": "Best matches",
"aiSummary": "AI summary",
"recentHistory": "Recent history",
"navigateHint": "to navigate",
"loadingTooltip": "We are looking for results...",

View File

@ -345,7 +345,7 @@ dependencies = [
[[package]]
name = "af-local-ai"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=9d731d89ea2e0fd764da2effa8a456210c5a39c3#9d731d89ea2e0fd764da2effa8a456210c5a39c3"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=093d3f4916a7b924ac66b4f0a9d81cc5fcc72eaa#093d3f4916a7b924ac66b4f0a9d81cc5fcc72eaa"
dependencies = [
"af-plugin",
"anyhow",
@ -362,7 +362,7 @@ dependencies = [
[[package]]
name = "af-mcp"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=9d731d89ea2e0fd764da2effa8a456210c5a39c3#9d731d89ea2e0fd764da2effa8a456210c5a39c3"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=093d3f4916a7b924ac66b4f0a9d81cc5fcc72eaa#093d3f4916a7b924ac66b4f0a9d81cc5fcc72eaa"
dependencies = [
"anyhow",
"futures-util",
@ -376,7 +376,7 @@ dependencies = [
[[package]]
name = "af-plugin"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=9d731d89ea2e0fd764da2effa8a456210c5a39c3#9d731d89ea2e0fd764da2effa8a456210c5a39c3"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=093d3f4916a7b924ac66b4f0a9d81cc5fcc72eaa#093d3f4916a7b924ac66b4f0a9d81cc5fcc72eaa"
dependencies = [
"anyhow",
"cfg-if",
@ -493,7 +493,7 @@ checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f"
[[package]]
name = "app-error"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f300884#f300884dde348ce855712b8e5a3ff3066682d160"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6#c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6"
dependencies = [
"anyhow",
"bincode",
@ -513,7 +513,7 @@ dependencies = [
[[package]]
name = "appflowy-ai-client"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f300884#f300884dde348ce855712b8e5a3ff3066682d160"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6#c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6"
dependencies = [
"anyhow",
"bytes",
@ -604,7 +604,7 @@ dependencies = [
"backoff",
"base64 0.21.5",
"bytes",
"derive_builder",
"derive_builder 0.12.0",
"futures",
"rand 0.8.5",
"reqwest 0.11.27",
@ -632,9 +632,9 @@ dependencies = [
[[package]]
name = "async-stream"
version = "0.3.5"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51"
checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476"
dependencies = [
"async-stream-impl",
"futures-core",
@ -643,9 +643,9 @@ dependencies = [
[[package]]
name = "async-stream-impl"
version = "0.3.5"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193"
checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d"
dependencies = [
"proc-macro2",
"quote",
@ -887,6 +887,31 @@ dependencies = [
"generic-array",
]
[[package]]
name = "bon"
version = "3.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92c5f8abc69af414cbd6f2103bb668b91e584072f2105e4b38bed79b6ad0975f"
dependencies = [
"bon-macros",
"rustversion",
]
[[package]]
name = "bon-macros"
version = "3.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b69edf39b6f321cb2699a93fc20c256adb839719c42676d03f7aa975e4e5581d"
dependencies = [
"darling 0.20.11",
"ident_case",
"prettyplease",
"proc-macro2",
"quote",
"rustversion",
"syn 2.0.94",
]
[[package]]
name = "borsh"
version = "1.5.1"
@ -1134,7 +1159,7 @@ dependencies = [
[[package]]
name = "client-api"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f300884#f300884dde348ce855712b8e5a3ff3066682d160"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6#c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6"
dependencies = [
"again",
"anyhow",
@ -1189,7 +1214,7 @@ dependencies = [
[[package]]
name = "client-api-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f300884#f300884dde348ce855712b8e5a3ff3066682d160"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6#c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6"
dependencies = [
"collab-entity",
"collab-rt-entity",
@ -1202,7 +1227,7 @@ dependencies = [
[[package]]
name = "client-websocket"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f300884#f300884dde348ce855712b8e5a3ff3066682d160"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6#c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6"
dependencies = [
"futures-channel",
"futures-util",
@ -1474,7 +1499,7 @@ dependencies = [
[[package]]
name = "collab-rt-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f300884#f300884dde348ce855712b8e5a3ff3066682d160"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6#c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6"
dependencies = [
"anyhow",
"bincode",
@ -1496,7 +1521,7 @@ dependencies = [
[[package]]
name = "collab-rt-protocol"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f300884#f300884dde348ce855712b8e5a3ff3066682d160"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6#c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6"
dependencies = [
"anyhow",
"async-trait",
@ -1761,7 +1786,7 @@ dependencies = [
"cssparser-macros",
"dtoa-short",
"itoa",
"phf 0.8.0",
"phf 0.11.2",
"smallvec",
]
@ -1817,12 +1842,12 @@ dependencies = [
[[package]]
name = "darling"
version = "0.20.8"
version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54e36fcd13ed84ffdfda6f5be89b31287cbb80c439841fe69e04841435464391"
checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
dependencies = [
"darling_core 0.20.8",
"darling_macro 0.20.8",
"darling_core 0.20.11",
"darling_macro 0.20.11",
]
[[package]]
@ -1841,15 +1866,15 @@ dependencies = [
[[package]]
name = "darling_core"
version = "0.20.8"
version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c2cf1c23a687a1feeb728783b993c4e1ad83d99f351801977dd809b48d0a70f"
checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e"
dependencies = [
"fnv",
"ident_case",
"proc-macro2",
"quote",
"strsim 0.10.0",
"strsim 0.11.1",
"syn 2.0.94",
]
@ -1866,11 +1891,11 @@ dependencies = [
[[package]]
name = "darling_macro"
version = "0.20.8"
version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f"
checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
dependencies = [
"darling_core 0.20.8",
"darling_core 0.20.11",
"quote",
"syn 2.0.94",
]
@ -1944,7 +1969,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308"
[[package]]
name = "database-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f300884#f300884dde348ce855712b8e5a3ff3066682d160"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6#c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6"
dependencies = [
"bincode",
"bytes",
@ -2027,7 +2052,16 @@ version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d67778784b508018359cbc8696edb3db78160bab2c2a28ba7f56ef6932997f8"
dependencies = [
"derive_builder_macro",
"derive_builder_macro 0.12.0",
]
[[package]]
name = "derive_builder"
version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947"
dependencies = [
"derive_builder_macro 0.20.2",
]
[[package]]
@ -2042,16 +2076,38 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "derive_builder_core"
version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8"
dependencies = [
"darling 0.20.11",
"proc-macro2",
"quote",
"syn 2.0.94",
]
[[package]]
name = "derive_builder_macro"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebcda35c7a396850a55ffeac740804b40ffec779b98fffbb1738f4033f0ee79e"
dependencies = [
"derive_builder_core",
"derive_builder_core 0.12.0",
"syn 1.0.109",
]
[[package]]
name = "derive_builder_macro"
version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c"
dependencies = [
"derive_builder_core 0.20.2",
"syn 2.0.94",
]
[[package]]
name = "derive_more"
version = "0.99.17"
@ -2162,9 +2218,9 @@ checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f"
[[package]]
name = "downcast-rs"
version = "1.2.0"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650"
checksum = "ea8a8b81cacc08888170eef4d13b775126db426d0b348bee9d18c2c1eaf123cf"
[[package]]
name = "dtoa"
@ -2484,7 +2540,6 @@ dependencies = [
"lib-dispatch",
"lib-infra",
"log",
"md5",
"notify",
"pin-project",
"protobuf",
@ -2501,8 +2556,6 @@ dependencies = [
"tracing-subscriber",
"uuid",
"validator 0.18.1",
"zip 2.2.0",
"zip-extensions",
]
[[package]]
@ -2857,15 +2910,16 @@ dependencies = [
name = "flowy-search"
version = "0.1.0"
dependencies = [
"allo-isolate",
"async-stream",
"bytes",
"collab",
"collab-folder",
"derive_builder 0.20.2",
"flowy-codegen",
"flowy-derive",
"flowy-error",
"flowy-folder",
"flowy-notification",
"flowy-search-pub",
"flowy-user",
"futures",
@ -2874,11 +2928,11 @@ dependencies = [
"protobuf",
"serde",
"serde_json",
"strsim 0.11.0",
"strsim 0.11.1",
"strum_macros 0.26.1",
"tantivy",
"tempfile",
"tokio",
"tokio-stream",
"tracing",
"uuid",
]
@ -3290,19 +3344,6 @@ dependencies = [
"byteorder",
]
[[package]]
name = "generator"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5cc16584ff22b460a382b7feec54b23d2908d858152e5739a120b949293bd74e"
dependencies = [
"cc",
"libc",
"log",
"rustversion",
"windows 0.48.0",
]
[[package]]
name = "generic-array"
version = "0.14.7"
@ -3418,7 +3459,7 @@ dependencies = [
[[package]]
name = "gotrue"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f300884#f300884dde348ce855712b8e5a3ff3066682d160"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6#c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6"
dependencies = [
"anyhow",
"getrandom 0.2.10",
@ -3433,7 +3474,7 @@ dependencies = [
[[package]]
name = "gotrue-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f300884#f300884dde348ce855712b8e5a3ff3066682d160"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6#c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6"
dependencies = [
"app-error",
"jsonwebtoken",
@ -3527,12 +3568,6 @@ version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
[[package]]
name = "hermit-abi"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b"
[[package]]
name = "hermit-abi"
version = "0.5.0"
@ -3802,6 +3837,15 @@ dependencies = [
"tracing",
]
[[package]]
name = "hyperloglogplus"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "621debdf94dcac33e50475fdd76d34d5ea9c0362a834b9db08c3024696c1fbe3"
dependencies = [
"serde",
]
[[package]]
name = "iana-time-zone"
version = "0.1.61"
@ -4054,7 +4098,7 @@ dependencies = [
[[package]]
name = "infra"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f300884#f300884dde348ce855712b8e5a3ff3066682d160"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6#c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6"
dependencies = [
"anyhow",
"bytes",
@ -4104,9 +4148,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
dependencies = [
"cfg-if",
"js-sys",
"wasm-bindgen",
"web-sys",
]
[[package]]
@ -4121,7 +4162,7 @@ version = "0.4.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9"
dependencies = [
"hermit-abi 0.5.0",
"hermit-abi",
"libc",
"windows-sys 0.59.0",
]
@ -4144,6 +4185,15 @@ dependencies = [
"either",
]
[[package]]
name = "itertools"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "1.0.9"
@ -4416,20 +4466,6 @@ version = "0.4.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
[[package]]
name = "loom"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff50ecb28bb86013e935fb6683ab1f6d3a20016f123c76fd4c27470076ac30f5"
dependencies = [
"cfg-if",
"generator",
"pin-utils",
"scoped-tls",
"tracing",
"tracing-subscriber",
]
[[package]]
name = "lru"
version = "0.12.3"
@ -4608,11 +4644,10 @@ checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771"
[[package]]
name = "measure_time"
version = "0.8.2"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56220900f1a0923789ecd6bf25fbae8af3b2f1ff3e9e297fc9b6b8674dd4d852"
checksum = "51c55d61e72fc3ab704396c5fa16f4c184db37978ae4e94ca8959693a235fc0e"
dependencies = [
"instant",
"log",
]
@ -4876,16 +4911,6 @@ dependencies = [
"libm",
]
[[package]]
name = "num_cpus"
version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
dependencies = [
"hermit-abi 0.3.2",
"libc",
]
[[package]]
name = "object"
version = "0.32.1"
@ -4903,12 +4928,9 @@ checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
name = "oneshot"
version = "0.1.6"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f6640c6bda7731b1fdbab747981a0f896dd1fedaf9f4a53fa237a04a84431f4"
dependencies = [
"loom",
]
checksum = "b4ce411919553d3f9fa53a0880544cda985a112117a0444d5ff1e870a893d6ea"
[[package]]
name = "opaque-debug"
@ -4988,9 +5010,9 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]]
name = "ownedbytes"
version = "0.7.0"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3a059efb063b8f425b948e042e6b9bd85edfe60e913630ed727b23e2dfcc558"
checksum = "2fbd56f7631767e61784dc43f8580f403f4475bd4aaa4da003e6295e1bab4a7e"
dependencies = [
"stable_deref_trait",
]
@ -5167,7 +5189,7 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12"
dependencies = [
"phf_macros",
"phf_macros 0.8.0",
"phf_shared 0.8.0",
"proc-macro-hack",
]
@ -5187,6 +5209,7 @@ version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc"
dependencies = [
"phf_macros 0.11.3",
"phf_shared 0.11.2",
]
@ -5254,6 +5277,19 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "phf_macros"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216"
dependencies = [
"phf_generator 0.11.2",
"phf_shared 0.11.2",
"proc-macro2",
"quote",
"syn 2.0.94",
]
[[package]]
name = "phf_shared"
version = "0.8.0"
@ -6482,12 +6518,6 @@ dependencies = [
"parking_lot 0.12.1",
]
[[package]]
name = "scoped-tls"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
[[package]]
name = "scopeguard"
version = "1.2.0"
@ -6606,18 +6636,18 @@ dependencies = [
[[package]]
name = "serde"
version = "1.0.210"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a"
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.210"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f"
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
dependencies = [
"proc-macro2",
"quote",
@ -6754,7 +6784,7 @@ dependencies = [
[[package]]
name = "shared-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f300884#f300884dde348ce855712b8e5a3ff3066682d160"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6#c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6"
dependencies = [
"anyhow",
"app-error",
@ -6844,9 +6874,9 @@ checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d"
[[package]]
name = "sketches-ddsketch"
version = "0.2.2"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85636c14b73d81f541e525f585c0a2109e6744e1565b5c1668e31c70c10ed65c"
checksum = "c1e9a774a6c28142ac54bb25d25562e6bcf957493a184f15ad4eebccb23e410a"
dependencies = [
"serde",
]
@ -6967,9 +6997,9 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "strsim"
version = "0.11.0"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "strum"
@ -7093,7 +7123,7 @@ dependencies = [
"ntapi",
"once_cell",
"rayon",
"windows 0.52.0",
"windows",
]
[[package]]
@ -7146,14 +7176,15 @@ checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417"
[[package]]
name = "tantivy"
version = "0.22.0"
version = "0.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8d0582f186c0a6d55655d24543f15e43607299425c5ad8352c242b914b31856"
checksum = "b21ad8b222d71c57aa979353ed702f0bc6d97e66d368962cbded57fbd19eedd7"
dependencies = [
"aho-corasick",
"arc-swap",
"base64 0.22.1",
"bitpacking",
"bon",
"byteorder",
"census",
"crc32fast",
@ -7163,20 +7194,20 @@ dependencies = [
"fnv",
"fs4",
"htmlescape",
"itertools 0.12.1",
"hyperloglogplus",
"itertools 0.14.0",
"levenshtein_automata",
"log",
"lru",
"lz4_flex",
"measure_time",
"memmap2",
"num_cpus",
"once_cell",
"oneshot",
"rayon",
"regex",
"rust-stemmers",
"rustc-hash 1.1.0",
"rustc-hash 2.1.0",
"serde",
"serde_json",
"sketches-ddsketch",
@ -7189,7 +7220,7 @@ dependencies = [
"tantivy-stacker",
"tantivy-tokenizer-api",
"tempfile",
"thiserror 1.0.64",
"thiserror 2.0.9",
"time",
"uuid",
"winapi",
@ -7197,22 +7228,22 @@ dependencies = [
[[package]]
name = "tantivy-bitpacker"
version = "0.6.0"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "284899c2325d6832203ac6ff5891b297fc5239c3dc754c5bc1977855b23c10df"
checksum = "1adc286a39e089ae9938935cd488d7d34f14502544a36607effd2239ff0e2494"
dependencies = [
"bitpacking",
]
[[package]]
name = "tantivy-columnar"
version = "0.3.0"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12722224ffbe346c7fec3275c699e508fd0d4710e629e933d5736ec524a1f44e"
checksum = "6300428e0c104c4f7db6f95b466a6f5c1b9aece094ec57cdd365337908dc7344"
dependencies = [
"downcast-rs",
"fastdivide",
"itertools 0.12.1",
"itertools 0.14.0",
"serde",
"tantivy-bitpacker",
"tantivy-common",
@ -7222,9 +7253,9 @@ dependencies = [
[[package]]
name = "tantivy-common"
version = "0.7.0"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8019e3cabcfd20a1380b491e13ff42f57bb38bf97c3d5fa5c07e50816e0621f4"
checksum = "e91b6ea6090ce03dc72c27d0619e77185d26cc3b20775966c346c6d4f7e99d7f"
dependencies = [
"async-trait",
"byteorder",
@ -7246,19 +7277,23 @@ dependencies = [
[[package]]
name = "tantivy-query-grammar"
version = "0.22.0"
version = "0.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "847434d4af57b32e309f4ab1b4f1707a6c566656264caa427ff4285c4d9d0b82"
checksum = "e810cdeeebca57fc3f7bfec5f85fdbea9031b2ac9b990eb5ff49b371d52bbe6a"
dependencies = [
"nom",
"serde",
"serde_json",
]
[[package]]
name = "tantivy-sstable"
version = "0.3.0"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c69578242e8e9fc989119f522ba5b49a38ac20f576fc778035b96cc94f41f98e"
checksum = "709f22c08a4c90e1b36711c1c6cad5ae21b20b093e535b69b18783dd2cb99416"
dependencies = [
"futures-util",
"itertools 0.14.0",
"tantivy-bitpacker",
"tantivy-common",
"tantivy-fst",
@ -7267,9 +7302,9 @@ dependencies = [
[[package]]
name = "tantivy-stacker"
version = "0.3.0"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c56d6ff5591fc332739b3ce7035b57995a3ce29a93ffd6012660e0949c956ea8"
checksum = "2bcdebb267671311d1e8891fd9d1301803fdb8ad21ba22e0a30d0cab49ba59c1"
dependencies = [
"murmurhash32",
"rand_distr",
@ -7278,9 +7313,9 @@ dependencies = [
[[package]]
name = "tantivy-tokenizer-api"
version = "0.3.0"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a0dcade25819a89cfe6f17d932c9cedff11989936bf6dd4f336d50392053b04"
checksum = "dfa942fcee81e213e09715bbce8734ae2180070b97b33839a795ba1de201547d"
dependencies = [
"serde",
]
@ -7303,14 +7338,15 @@ dependencies = [
[[package]]
name = "tempfile"
version = "3.10.0"
version = "3.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a365e8cd18e44762ef95d87f284f4b5cd04107fec2ff3052bd6a3e6069669e67"
checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64"
dependencies = [
"cfg-if",
"fastrand",
"once_cell",
"rustix",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@ -8213,7 +8249,7 @@ version = "0.18.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df0bcf92720c40105ac4b2dda2a4ea3aa717d4d6a862cc217da653a4bd5c6b10"
dependencies = [
"darling 0.20.8",
"darling 0.20.11",
"once_cell",
"proc-macro-error",
"proc-macro2",
@ -8227,7 +8263,7 @@ version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bac855a2ce6f843beb229757e6e570a42e837bcb15e5f449dd48d5747d41bf77"
dependencies = [
"darling 0.20.8",
"darling 0.20.11",
"once_cell",
"proc-macro-error2",
"proc-macro2",
@ -8489,15 +8525,6 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f"
dependencies = [
"windows-targets 0.48.5",
]
[[package]]
name = "windows"
version = "0.52.0"
@ -9008,15 +9035,6 @@ dependencies = [
"zstd 0.13.2",
]
[[package]]
name = "zip-extensions"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb0a99499b3497d765525c5d05e3ade9ca4a731c184365c19472c3fd6ba86341"
dependencies = [
"zip 2.2.0",
]
[[package]]
name = "zopfli"
version = "0.8.1"

View File

@ -97,14 +97,16 @@ validator = { version = "0.18", features = ["derive"] }
tokio-util = "0.7.11"
zip = "2.2.0"
dashmap = "6.0.1"
derive_builder = "0.20.2"
tantivy = { version = "0.24.0" }
# Please using the following command to update the revision id
# Current directory: frontend
# Run the script.add_workspace_members:
# scripts/tool/update_client_api_rev.sh new_rev_id
# ⚠️⚠️⚠️️
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "f300884" }
client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "f300884" }
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6" }
client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6" }
[profile.dev]
opt-level = 0
@ -152,6 +154,6 @@ collab-importer = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFl
# To update the commit ID, run:
# scripts/tool/update_local_ai_rev.sh new_rev_id
# ⚠️⚠️⚠️️
af-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "9d731d89ea2e0fd764da2effa8a456210c5a39c3" }
af-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "9d731d89ea2e0fd764da2effa8a456210c5a39c3" }
af-mcp = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "9d731d89ea2e0fd764da2effa8a456210c5a39c3" }
af-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "093d3f4916a7b924ac66b4f0a9d81cc5fcc72eaa" }
af-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "093d3f4916a7b924ac66b4f0a9d81cc5fcc72eaa" }
af-mcp = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "093d3f4916a7b924ac66b4f0a9d81cc5fcc72eaa" }

View File

@ -41,9 +41,6 @@ reqwest = { version = "0.11.27", features = ["json"] }
sha2 = "0.10.7"
base64 = "0.21.5"
futures-util = "0.3.30"
md5 = "0.7.0"
zip = { workspace = true, features = ["deflate"] }
zip-extensions = "0.8.0"
pin-project = "1.1.5"
flowy-storage-pub = { workspace = true }
collab-integrate.workspace = true

View File

@ -1,6 +1,6 @@
use crate::server_layer::{Server, ServerProvider};
use client_api::collab_sync::{SinkConfig, SyncObject, SyncPlugin};
use client_api::entity::ai_dto::RepeatedRelatedQuestion;
use client_api::entity::search_dto::SearchDocumentResponseItem;
use client_api::entity::workspace_dto::PublishInfoView;
use client_api::entity::PublishInfo;
use collab::core::origin::{CollabClient, CollabOrigin};
@ -10,6 +10,9 @@ use collab_entity::CollabType;
use collab_integrate::collab_builder::{
CollabCloudPluginProvider, CollabPluginProviderContext, CollabPluginProviderType,
};
use flowy_ai_pub::cloud::search_dto::{
SearchDocumentResponseItem, SearchResult, SearchSummaryResult,
};
use flowy_ai_pub::cloud::{
AIModel, ChatCloudService, ChatMessage, ChatMessageMetadata, ChatMessageType, ChatSettings,
CompleteTextParams, LocalAIConfig, MessageCursor, ModelList, RepeatedChatMessage, ResponseFormat,
@ -46,8 +49,6 @@ use tracing::log::error;
use tracing::{debug, info};
use uuid::Uuid;
use crate::server_layer::{Server, ServerProvider};
#[async_trait]
impl StorageCloudService for ServerProvider {
async fn get_object_url(&self, object_id: ObjectIdentity) -> Result<String, FlowyError> {
@ -874,4 +875,21 @@ impl SearchCloudService for ServerProvider {
None => Err(FlowyError::internal().with_context("SearchCloudService not found")),
}
}
async fn generate_search_summary(
&self,
workspace_id: &Uuid,
query: String,
search_results: Vec<SearchResult>,
) -> Result<SearchSummaryResult, FlowyError> {
let server = self.get_server()?;
match server.search_service() {
Some(search_service) => {
search_service
.generate_search_summary(workspace_id, query, search_results)
.await
},
None => Err(FlowyError::internal().with_context("SearchCloudService not found")),
}
}
}

View File

@ -82,12 +82,13 @@ impl UserWorkspaceService for UserWorkspaceServiceImpl {
Ok(())
}
fn did_delete_workspace(&self, workspace_id: &Uuid) -> FlowyResult<()> {
async fn did_delete_workspace(&self, workspace_id: &Uuid) -> FlowyResult<()> {
// The remove_indices_for_workspace should not block the deletion of the workspace
// Log the error and continue
if let Err(err) = self
.folder_manager
.remove_indices_for_workspace(workspace_id)
.await
{
info!("Error removing indices for workspace: {}", err);
}

View File

@ -165,9 +165,9 @@ impl AppFlowyCore {
collab_builder
.set_snapshot_persistence(Arc::new(SnapshotDBImpl(Arc::downgrade(&authenticate_user))));
let folder_indexer = Arc::new(FolderIndexManagerImpl::new(Some(Arc::downgrade(
let folder_indexer = Arc::new(FolderIndexManagerImpl::new(Arc::downgrade(
&authenticate_user,
))));
)));
let folder_manager = FolderDepsResolver::resolve(
Arc::downgrade(&authenticate_user),

View File

@ -33,7 +33,7 @@ collab-document = { workspace = true, optional = true }
collab-plugins = { workspace = true, optional = true }
collab-folder = { workspace = true, optional = true }
client-api = { workspace = true, optional = true }
tantivy = { version = "0.22.0", optional = true }
tantivy = { workspace = true, optional = true }
uuid.workspace = true
[features]

View File

@ -2051,10 +2051,11 @@ impl FolderManager {
.collect()
}
pub fn remove_indices_for_workspace(&self, workspace_id: &Uuid) -> FlowyResult<()> {
pub async fn remove_indices_for_workspace(&self, workspace_id: &Uuid) -> FlowyResult<()> {
self
.folder_indexer
.remove_indices_for_workspace(*workspace_id)?;
.remove_indices_for_workspace(*workspace_id)
.await?;
Ok(())
}

View File

@ -115,8 +115,9 @@ impl FolderManager {
let index_content_rx = folder.subscribe_index_content();
self
.folder_indexer
.set_index_content_receiver(index_content_rx, *workspace_id);
self.handle_index_folder(*workspace_id, &folder);
.set_index_content_receiver(index_content_rx, *workspace_id)
.await;
self.handle_index_folder(*workspace_id, &folder).await;
folder_state_rx
};
@ -137,6 +138,8 @@ impl FolderManager {
Arc::downgrade(&self.user),
);
self.folder_indexer.initialize().await;
Ok(())
}
@ -166,7 +169,7 @@ impl FolderManager {
Ok(folder)
}
fn handle_index_folder(&self, workspace_id: Uuid, folder: &Folder) {
async fn handle_index_folder(&self, workspace_id: Uuid, folder: &Folder) {
let mut index_all = true;
let encoded_collab = self
@ -191,12 +194,11 @@ impl FolderManager {
if index_all {
let views = folder.get_all_views();
let folder_indexer = self.folder_indexer.clone();
let _ = folder_indexer
.remove_indices_for_workspace(workspace_id)
.await;
// We spawn a blocking task to index all views in the folder
spawn_blocking(move || {
// We remove old indexes just in case
let _ = folder_indexer.remove_indices_for_workspace(workspace_id);
// We index all views from the workspace
folder_indexer.index_all_views(views, workspace_id);
});
}

View File

@ -1,4 +1,6 @@
use client_api::entity::search_dto::SearchDocumentResponseItem;
pub use client_api::entity::search_dto::{
SearchDocumentResponseItem, SearchResult, SearchSummaryResult,
};
use flowy_error::FlowyError;
use lib_infra::async_trait::async_trait;
use uuid::Uuid;
@ -10,4 +12,11 @@ pub trait SearchCloudService: Send + Sync + 'static {
workspace_id: &Uuid,
query: String,
) -> Result<Vec<SearchDocumentResponseItem>, FlowyError>;
async fn generate_search_summary(
&self,
workspace_id: &Uuid,
query: String,
search_results: Vec<SearchResult>,
) -> Result<SearchSummaryResult, FlowyError>;
}

View File

@ -1,9 +1,9 @@
use std::any::Any;
use std::sync::Arc;
use collab::core::collab::IndexContentReceiver;
use collab_folder::{folder_diff::FolderViewChange, View, ViewIcon, ViewLayout};
use flowy_error::FlowyError;
use lib_infra::async_trait::async_trait;
use uuid::Uuid;
pub struct IndexableData {
@ -26,19 +26,22 @@ impl IndexableData {
}
}
#[async_trait]
pub trait IndexManager: Send + Sync {
fn set_index_content_receiver(&self, rx: IndexContentReceiver, workspace_id: Uuid);
fn add_index(&self, data: IndexableData) -> Result<(), FlowyError>;
fn update_index(&self, data: IndexableData) -> Result<(), FlowyError>;
fn remove_indices(&self, ids: Vec<String>) -> Result<(), FlowyError>;
fn remove_indices_for_workspace(&self, workspace_id: Uuid) -> Result<(), FlowyError>;
fn is_indexed(&self) -> bool;
fn as_any(&self) -> &dyn Any;
async fn set_index_content_receiver(&self, rx: IndexContentReceiver, workspace_id: Uuid);
async fn add_index(&self, data: IndexableData) -> Result<(), FlowyError>;
async fn update_index(&self, data: IndexableData) -> Result<(), FlowyError>;
async fn remove_indices(&self, ids: Vec<String>) -> Result<(), FlowyError>;
async fn remove_indices_for_workspace(&self, workspace_id: Uuid) -> Result<(), FlowyError>;
async fn is_indexed(&self) -> bool;
}
#[async_trait]
pub trait FolderIndexManager: IndexManager {
async fn initialize(&self);
fn index_all_views(&self, views: Vec<Arc<View>>, workspace_id: Uuid);
fn index_view_changes(
&self,
views: Vec<Arc<View>>,

View File

@ -17,13 +17,10 @@ flowy-error = { workspace = true, features = [
"impl_from_tantivy",
"impl_from_serde",
] }
flowy-notification.workspace = true
flowy-user.workspace = true
flowy-search-pub.workspace = true
flowy-folder = { workspace = true }
bytes.workspace = true
futures.workspace = true
lib-dispatch.workspace = true
lib-infra = { workspace = true }
protobuf.workspace = true
@ -31,18 +28,18 @@ serde.workspace = true
serde_json.workspace = true
tokio = { workspace = true, features = ["full", "rt-multi-thread", "tracing"] }
tracing.workspace = true
async-stream = "0.3.4"
derive_builder.workspace = true
strsim = "0.11.0"
strum_macros = "0.26.1"
tantivy = { version = "0.22.0" }
tantivy.workspace = true
uuid.workspace = true
allo-isolate = { version = "^0.1", features = ["catch-unwind"] }
futures.workspace = true
tokio-stream.workspace = true
async-stream = "0.3.6"
[build-dependencies]
flowy-codegen.workspace = true
[dev-dependencies]
tempfile = "3.10.0"
[features]
dart = ["flowy-codegen/dart"]

View File

@ -1,16 +1,22 @@
use flowy_error::FlowyResult;
use flowy_folder::{manager::FolderManager, ViewLayout};
use flowy_search_pub::cloud::SearchCloudService;
use lib_infra::async_trait::async_trait;
use std::str::FromStr;
use std::sync::Arc;
use tracing::{trace, warn};
use uuid::Uuid;
use crate::entities::{
CreateSearchResultPBArgs, RepeatedSearchResponseItemPB, RepeatedSearchSummaryPB, SearchResultPB,
SearchSourcePB, SearchSummaryPB,
};
use crate::{
entities::{IndexTypePB, ResultIconPB, ResultIconTypePB, SearchFilterPB, SearchResultPB},
entities::{IndexTypePB, ResultIconPB, ResultIconTypePB, SearchFilterPB, SearchResponseItemPB},
services::manager::{SearchHandler, SearchType},
};
use async_stream::stream;
use flowy_error::FlowyResult;
use flowy_folder::{manager::FolderManager, ViewLayout};
use flowy_search_pub::cloud::{SearchCloudService, SearchResult};
use lib_infra::async_trait::async_trait;
use std::pin::Pin;
use std::str::FromStr;
use std::sync::Arc;
use tokio_stream::{self, Stream};
use tracing::{trace, warn};
use uuid::Uuid;
pub struct DocumentSearchHandler {
pub cloud_service: Arc<dyn SearchCloudService>,
@ -39,65 +45,122 @@ impl SearchHandler for DocumentSearchHandler {
&self,
query: String,
filter: Option<SearchFilterPB>,
) -> FlowyResult<Vec<SearchResultPB>> {
let filter = match filter {
Some(filter) => filter,
None => return Ok(vec![]),
};
) -> Pin<Box<dyn Stream<Item = FlowyResult<SearchResultPB>> + Send + 'static>> {
let cloud_service = self.cloud_service.clone();
let folder_manager = self.folder_manager.clone();
let workspace_id = match filter.workspace_id {
Some(workspace_id) => workspace_id,
None => return Ok(vec![]),
};
Box::pin(stream! {
let filter = match filter {
Some(f) => f,
None => {
yield Ok(CreateSearchResultPBArgs::default().build().unwrap());
return;
},
};
let workspace_id = Uuid::from_str(&workspace_id)?;
let results = self
.cloud_service
.document_search(&workspace_id, query)
.await?;
trace!("[Search] remote search results: {:?}", results);
let workspace_id = match Uuid::from_str(&filter.workspace_id) {
Ok(id) => id,
Err(e) => {
yield Err(e.into());
return;
}
};
// Grab all views from folder cache
// Notice that `get_all_view_pb` returns Views that don't include trashed and private views
let views = self.folder_manager.get_all_views_pb().await?;
let mut search_results: Vec<SearchResultPB> = vec![];
let views = match folder_manager.get_all_views_pb().await {
Ok(views) => views,
Err(e) => {
yield Err(e);
return;
},
};
for result in results {
if let Some(view) = views.iter().find(|v| v.id == result.object_id.to_string()) {
// If there is no View for the result, we don't add it to the results
// If possible we will extract the icon to display for the result
let icon: Option<ResultIconPB> = match view.icon.clone() {
Some(view_icon) => Some(ResultIconPB::from(view_icon)),
None => {
let view_layout_ty: i64 = ViewLayout::from(view.layout.clone()).into();
Some(ResultIconPB {
ty: ResultIconTypePB::Icon,
value: view_layout_ty.to_string(),
})
},
};
let result_items = match cloud_service.document_search(&workspace_id, query.clone()).await {
Ok(items) => items,
Err(e) => {
yield Err(e);
return;
},
};
search_results.push(SearchResultPB {
index_type: IndexTypePB::Document,
view_id: result.object_id.to_string(),
id: result.object_id.to_string(),
data: view.name.clone(),
icon,
score: result.score,
workspace_id: result.workspace_id.to_string(),
preview: result.preview,
});
} else {
warn!("No view found for search result: {:?}", result);
trace!("[Search] search results: {:?}", result_items);
let summary_input = result_items
.iter()
.map(|v| SearchResult {
object_id: v.object_id,
content: v.content.clone(),
})
.collect::<Vec<_>>();
let mut items: Vec<SearchResponseItemPB> = Vec::new();
for item in result_items.iter() {
if let Some(view) = views.iter().find(|v| v.id == item.object_id.to_string()) {
let icon: Option<ResultIconPB> = match view.icon.clone() {
Some(view_icon) => Some(ResultIconPB::from(view_icon)),
None => {
let view_layout_ty: i64 = ViewLayout::from(view.layout.clone()).into();
Some(ResultIconPB {
ty: ResultIconTypePB::Icon,
value: view_layout_ty.to_string(),
})
},
};
items.push(SearchResponseItemPB {
index_type: IndexTypePB::Document,
view_id: item.object_id.to_string(),
id: item.object_id.to_string(),
data: view.name.clone(),
icon,
score: item.score,
workspace_id: item.workspace_id.to_string(),
preview: item.preview.clone(),
});
} else {
warn!("No view found for search result: {:?}", item);
}
}
}
trace!("[Search] showing results: {:?}", search_results);
Ok(search_results)
}
let search_result = RepeatedSearchResponseItemPB {
items,
};
yield Ok(
CreateSearchResultPBArgs::default()
.search_result(Some(search_result))
.build()
.unwrap(),
);
/// Ignore for [DocumentSearchHandler]
fn index_count(&self) -> u64 {
0
// Search summary generation.
match cloud_service.generate_search_summary(&workspace_id, query.clone(), summary_input).await {
Ok(summary_result) => {
trace!("[Search] search summary: {:?}", summary_result);
let summaries: Vec<SearchSummaryPB> = summary_result
.summaries
.into_iter()
.filter_map(|v| {
v.metadata.as_object().and_then(|object| {
let id = object.get("id")?.as_str()?.to_string();
let source = object.get("source")?.as_str()?.to_string();
let metadata = SearchSourcePB {id, source };
Some(SearchSummaryPB { content: v.content, metadata: Some(metadata) })
})
})
.collect();
let summary_result = RepeatedSearchSummaryPB {
items: summaries,
};
yield Ok(
CreateSearchResultPBArgs::default()
.search_summary(Some(summary_result))
.build()
.unwrap(),
);
}
Err(e) => {
warn!("Failed to generate search summary: {:?}", e);
}
}
})
}
}

View File

@ -1,20 +1,16 @@
use super::SearchResultPB;
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
use super::SearchResultPB;
#[derive(ProtoBuf, Default, Debug, Clone)]
pub struct SearchResultNotificationPB {
#[pb(index = 1)]
pub items: Vec<SearchResultPB>,
pub struct SearchResponsePB {
#[pb(index = 1, one_of)]
pub result: Option<SearchResultPB>,
#[pb(index = 2)]
pub sends: u64,
pub search_id: String,
#[pb(index = 3, one_of)]
pub channel: Option<String>,
#[pb(index = 4)]
pub query: String,
#[pb(index = 3)]
pub is_loading: bool,
}
#[derive(ProtoBuf_Enum, Debug, Default)]

View File

@ -13,13 +13,9 @@ pub struct SearchQueryPB {
#[pb(index = 3, one_of)]
pub filter: Option<SearchFilterPB>,
/// Used to identify the channel of the search
///
/// This can be used to have multiple search notification listeners in place.
/// It is up to the client to decide how to handle this.
///
/// If not set, then no channel is used.
///
#[pb(index = 4, one_of)]
pub channel: Option<String>,
#[pb(index = 4)]
pub search_id: String,
#[pb(index = 5)]
pub stream_port: i64,
}

View File

@ -1,17 +1,54 @@
use super::IndexTypePB;
use collab_folder::{IconType, ViewIcon};
use derive_builder::Builder;
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
use flowy_folder::entities::ViewIconPB;
use super::IndexTypePB;
#[derive(Debug, Default, ProtoBuf, Builder, Clone)]
#[builder(name = "CreateSearchResultPBArgs")]
#[builder(pattern = "mutable")]
pub struct SearchResultPB {
#[pb(index = 1, one_of)]
#[builder(default)]
pub search_result: Option<RepeatedSearchResponseItemPB>,
#[derive(Debug, Default, ProtoBuf, Clone)]
pub struct RepeatedSearchResultPB {
#[pb(index = 1)]
pub items: Vec<SearchResultPB>,
#[pb(index = 2, one_of)]
#[builder(default)]
pub search_summary: Option<RepeatedSearchSummaryPB>,
}
#[derive(ProtoBuf, Default, Debug, Clone)]
pub struct SearchResultPB {
pub struct RepeatedSearchSummaryPB {
#[pb(index = 1)]
pub items: Vec<SearchSummaryPB>,
}
#[derive(ProtoBuf, Default, Debug, Clone)]
pub struct SearchSummaryPB {
#[pb(index = 1)]
pub content: String,
#[pb(index = 2, one_of)]
pub metadata: Option<SearchSourcePB>,
}
#[derive(ProtoBuf, Default, Debug, Clone)]
pub struct SearchSourcePB {
#[pb(index = 1)]
pub id: String,
#[pb(index = 2)]
pub source: String,
}
#[derive(ProtoBuf, Default, Debug, Clone)]
pub struct RepeatedSearchResponseItemPB {
#[pb(index = 1)]
pub items: Vec<SearchResponseItemPB>,
}
#[derive(ProtoBuf, Default, Debug, Clone)]
pub struct SearchResponseItemPB {
#[pb(index = 1)]
pub index_type: IndexTypePB,
@ -37,9 +74,9 @@ pub struct SearchResultPB {
pub preview: Option<String>,
}
impl SearchResultPB {
impl SearchResponseItemPB {
pub fn with_score(&self, score: f64) -> Self {
SearchResultPB {
SearchResponseItemPB {
index_type: self.index_type.clone(),
view_id: self.view_id.clone(),
id: self.id.clone(),

View File

@ -2,6 +2,6 @@ use flowy_derive::ProtoBuf;
#[derive(Eq, PartialEq, ProtoBuf, Default, Debug, Clone)]
pub struct SearchFilterPB {
#[pb(index = 1, one_of)]
pub workspace_id: Option<String>,
#[pb(index = 1)]
pub workspace_id: String,
}

View File

@ -21,7 +21,14 @@ pub(crate) async fn search_handler(
) -> Result<(), FlowyError> {
let query = data.into_inner();
let manager = upgrade_manager(manager)?;
manager.perform_search(query.search, query.filter, query.channel);
manager
.perform_search(
query.search,
query.stream_port,
query.filter,
query.search_id,
)
.await;
Ok(())
}

View File

@ -1,6 +1,6 @@
use serde::{Deserialize, Serialize};
use crate::entities::{IndexTypePB, ResultIconPB, SearchResultPB};
use crate::entities::{IndexTypePB, ResultIconPB, SearchResponseItemPB};
#[derive(Debug, Serialize, Deserialize)]
pub struct FolderIndexData {
@ -11,7 +11,7 @@ pub struct FolderIndexData {
pub workspace_id: String,
}
impl From<FolderIndexData> for SearchResultPB {
impl From<FolderIndexData> for SearchResponseItemPB {
fn from(data: FolderIndexData) -> Self {
let icon = if data.icon.is_empty() {
None

View File

@ -1,12 +1,14 @@
use crate::{
entities::{SearchFilterPB, SearchResultPB},
services::manager::{SearchHandler, SearchType},
use super::indexer::FolderIndexManagerImpl;
use crate::entities::{
CreateSearchResultPBArgs, RepeatedSearchResponseItemPB, SearchFilterPB, SearchResultPB,
};
use crate::services::manager::{SearchHandler, SearchType};
use async_stream::stream;
use flowy_error::FlowyResult;
use lib_infra::async_trait::async_trait;
use std::pin::Pin;
use std::sync::Arc;
use super::indexer::FolderIndexManagerImpl;
use tokio_stream::{self, Stream};
pub struct FolderSearchHandler {
pub index_manager: Arc<FolderIndexManagerImpl>,
@ -28,19 +30,26 @@ impl SearchHandler for FolderSearchHandler {
&self,
query: String,
filter: Option<SearchFilterPB>,
) -> FlowyResult<Vec<SearchResultPB>> {
let mut results = self.index_manager.search(query, filter.clone())?;
if let Some(filter) = filter {
if let Some(workspace_id) = filter.workspace_id {
// Filter results by workspace ID
results.retain(|result| result.workspace_id == workspace_id);
}
}
) -> Pin<Box<dyn Stream<Item = FlowyResult<SearchResultPB>> + Send + 'static>> {
let index_manager = self.index_manager.clone();
Ok(results)
}
Box::pin(stream! {
// Perform search (if search() returns a Result)
let mut items = match index_manager.search(query).await {
Ok(items) => items,
Err(err) => {
yield Err(err);
return;
}
};
fn index_count(&self) -> u64 {
self.index_manager.num_docs()
if let Some(filter) = filter {
items.retain(|result| result.workspace_id == filter.workspace_id);
}
// Build the search result.
let search_result = RepeatedSearchResponseItemPB {items};
yield Ok(CreateSearchResultPBArgs::default().search_result(Some(search_result)).build().unwrap())
})
}
}

View File

@ -1,14 +1,5 @@
use std::{
any::Any,
collections::HashMap,
fs,
ops::Deref,
path::Path,
sync::{Arc, Mutex, MutexGuard, Weak},
};
use crate::{
entities::{ResultIconTypePB, SearchFilterPB, SearchResultPB},
entities::SearchResponseItemPB,
folder::schema::{
FolderSchema, FOLDER_ICON_FIELD_NAME, FOLDER_ICON_TY_FIELD_NAME, FOLDER_ID_FIELD_NAME,
FOLDER_TITLE_FIELD_NAME, FOLDER_WORKSPACE_ID_FIELD_NAME,
@ -19,172 +10,88 @@ use collab_folder::{folder_diff::FolderViewChange, View, ViewIcon, ViewIndexCont
use flowy_error::{FlowyError, FlowyResult};
use flowy_search_pub::entities::{FolderIndexManager, IndexManager, IndexableData};
use flowy_user::services::authenticate_user::AuthenticateUser;
use std::sync::{Arc, Weak};
use std::{collections::HashMap, fs};
use super::entities::FolderIndexData;
use crate::entities::ResultIconTypePB;
use lib_infra::async_trait::async_trait;
use strsim::levenshtein;
use tantivy::{
collector::TopDocs, directory::MmapDirectory, doc, query::QueryParser, schema::Field, Document,
Index, IndexReader, IndexWriter, TantivyDocument, Term,
};
use tokio::sync::RwLock;
use tracing::{error, info};
use uuid::Uuid;
#[derive(Clone)]
pub struct FolderIndexManagerImpl {
folder_schema: Option<FolderSchema>,
index: Option<Index>,
index_reader: Option<IndexReader>,
index_writer: Option<Arc<Mutex<IndexWriter>>>,
pub struct TantivyState {
pub index: Index,
pub folder_schema: FolderSchema,
pub index_reader: IndexReader,
pub index_writer: IndexWriter,
}
const FOLDER_INDEX_DIR: &str = "folder_index";
#[derive(Clone)]
pub struct FolderIndexManagerImpl {
auth_user: Weak<AuthenticateUser>,
state: Arc<RwLock<Option<TantivyState>>>,
}
impl FolderIndexManagerImpl {
pub fn new(auth_user: Option<Weak<AuthenticateUser>>) -> Self {
let auth_user = match auth_user {
Some(auth_user) => auth_user,
None => {
return FolderIndexManagerImpl::empty();
},
};
// AuthenticateUser is required to get the index path
let authenticate_user = auth_user.upgrade();
// Storage path is the users data path with an index directory
// Eg. /usr/flowy-data/indexes
let storage_path = match authenticate_user {
Some(auth_user) => auth_user.get_index_path(),
None => {
tracing::error!("FolderIndexManager: AuthenticateUser is not available");
return FolderIndexManagerImpl::empty();
},
};
// We check if the `folder_index` directory exists, if not we create it
let index_path = storage_path.join(Path::new(FOLDER_INDEX_DIR));
if !index_path.exists() {
let res = fs::create_dir_all(&index_path);
if let Err(e) = res {
tracing::error!(
"FolderIndexManager failed to create index directory: {:?}",
e
);
return FolderIndexManagerImpl::empty();
}
}
// The folder schema is used to define the fields of the index along
// with how they are stored and if the field is indexed
let folder_schema = FolderSchema::new();
// We open the existing or newly created folder_index directory
// This is required by the Tantivy Index, as it will use it to store
// and read index data
let index = match MmapDirectory::open(index_path) {
// We open or create an index that takes the directory r/w and the schema.
Ok(dir) => match Index::open_or_create(dir, folder_schema.schema.clone()) {
Ok(index) => index,
Err(e) => {
tracing::error!("FolderIndexManager failed to open index: {:?}", e);
return FolderIndexManagerImpl::empty();
},
},
Err(e) => {
tracing::error!("FolderIndexManager failed to open index directory: {:?}", e);
return FolderIndexManagerImpl::empty();
},
};
// We only need one IndexReader per index
let index_reader = index.reader();
let index_writer = index.writer(50_000_000);
let (index_reader, index_writer) = match (index_reader, index_writer) {
(Ok(reader), Ok(writer)) => (reader, writer),
_ => {
tracing::error!("FolderIndexManager failed to instantiate index writer and/or reader");
return FolderIndexManagerImpl::empty();
},
};
pub fn new(auth_user: Weak<AuthenticateUser>) -> Self {
Self {
folder_schema: Some(folder_schema),
index: Some(index),
index_reader: Some(index_reader),
index_writer: Some(Arc::new(Mutex::new(index_writer))),
auth_user,
state: Arc::new(RwLock::new(None)),
}
}
fn index_all(&self, indexes: Vec<IndexableData>) -> Result<(), FlowyError> {
if indexes.is_empty() {
return Ok(());
async fn with_writer<F, R>(&self, f: F) -> FlowyResult<R>
where
F: FnOnce(&mut IndexWriter, &FolderSchema) -> FlowyResult<R>,
{
let mut lock = self.state.write().await;
if let Some(ref mut state) = *lock {
f(&mut state.index_writer, &state.folder_schema)
} else {
Err(FlowyError::internal().with_context("Index not initialized. Call initialize first"))
}
}
/// Initializes the state using the workspace directory.
async fn initialize_with_workspace(&self) -> FlowyResult<()> {
let auth_user = self
.auth_user
.upgrade()
.ok_or_else(|| FlowyError::internal().with_context("AuthenticateUser is not available"))?;
let index_path = auth_user.get_index_path()?.join(FOLDER_INDEX_DIR);
if !index_path.exists() {
fs::create_dir_all(&index_path).map_err(|e| {
error!("Failed to create folder index directory: {:?}", e);
FlowyError::internal().with_context("Failed to create folder index")
})?;
}
let mut index_writer = self.get_index_writer()?;
let folder_schema = self.get_folder_schema()?;
info!("Folder indexer initialized at: {:?}", index_path);
let folder_schema = FolderSchema::new();
let dir = MmapDirectory::open(index_path)?;
let index = Index::open_or_create(dir, folder_schema.schema.clone())?;
let index_reader = index.reader()?;
let index_writer = index.writer(50_000_000)?;
let id_field = folder_schema.schema.get_field(FOLDER_ID_FIELD_NAME)?;
let title_field = folder_schema.schema.get_field(FOLDER_TITLE_FIELD_NAME)?;
let icon_field = folder_schema.schema.get_field(FOLDER_ICON_FIELD_NAME)?;
let icon_ty_field = folder_schema.schema.get_field(FOLDER_ICON_TY_FIELD_NAME)?;
let workspace_id_field = folder_schema
.schema
.get_field(FOLDER_WORKSPACE_ID_FIELD_NAME)?;
for data in indexes {
let (icon, icon_ty) = self.extract_icon(data.icon, data.layout);
let _ = index_writer.add_document(doc![
id_field => data.id.clone(),
title_field => data.data.clone(),
icon_field => icon.unwrap_or_default(),
icon_ty_field => icon_ty,
workspace_id_field => data.workspace_id.to_string(),
]);
}
index_writer.commit()?;
*self.state.write().await = Some(TantivyState {
index,
folder_schema,
index_reader,
index_writer,
});
Ok(())
}
pub fn num_docs(&self) -> u64 {
self
.index_reader
.clone()
.map(|reader| reader.searcher().num_docs())
.unwrap_or(0)
}
fn empty() -> Self {
Self {
folder_schema: None,
index: None,
index_reader: None,
index_writer: None,
}
}
fn get_index_writer(&self) -> FlowyResult<MutexGuard<IndexWriter>> {
match &self.index_writer {
Some(index_writer) => match index_writer.deref().lock() {
Ok(writer) => Ok(writer),
Err(e) => {
tracing::error!("FolderIndexManager failed to lock index writer: {:?}", e);
Err(FlowyError::folder_index_manager_unavailable())
},
},
None => Err(FlowyError::folder_index_manager_unavailable()),
}
}
fn get_folder_schema(&self) -> FlowyResult<FolderSchema> {
match &self.folder_schema {
Some(folder_schema) => Ok(folder_schema.clone()),
None => Err(FlowyError::folder_index_manager_unavailable()),
}
}
fn extract_icon(
&self,
view_icon: Option<ViewIcon>,
@ -200,100 +107,68 @@ impl FolderIndexManagerImpl {
icon = Some(view_icon.value);
} else {
icon_ty = ResultIconTypePB::Icon.into();
let layout_ty: i64 = view_layout.into();
let layout_ty = view_layout as i64;
icon = Some(layout_ty.to_string());
}
(icon, icon_ty)
}
pub fn search(
&self,
query: String,
_filter: Option<SearchFilterPB>,
) -> Result<Vec<SearchResultPB>, FlowyError> {
let folder_schema = self.get_folder_schema()?;
let (index, index_reader) = self
.index
.as_ref()
.zip(self.index_reader.as_ref())
.ok_or_else(FlowyError::folder_index_manager_unavailable)?;
let title_field = folder_schema.schema.get_field(FOLDER_TITLE_FIELD_NAME)?;
let length = query.len();
let distance: u8 = if length >= 2 { 2 } else { 1 };
let mut query_parser = QueryParser::for_index(&index.clone(), vec![title_field]);
query_parser.set_field_fuzzy(title_field, true, distance, true);
let built_query = query_parser.parse_query(&query.clone())?;
let searcher = index_reader.searcher();
let mut search_results: Vec<SearchResultPB> = vec![];
let top_docs = searcher.search(&built_query, &TopDocs::with_limit(10))?;
for (_score, doc_address) in top_docs {
let retrieved_doc: TantivyDocument = searcher.doc(doc_address)?;
let mut content = HashMap::new();
let named_doc = retrieved_doc.to_named_doc(&folder_schema.schema);
for (k, v) in named_doc.0 {
content.insert(k, v[0].clone());
}
if content.is_empty() {
continue;
}
let s = serde_json::to_string(&content)?;
let result: SearchResultPB = serde_json::from_str::<FolderIndexData>(&s)?.into();
let score = self.score_result(&query, &result.data);
search_results.push(result.with_score(score));
}
Ok(search_results)
}
// Score result by distance
fn score_result(&self, query: &str, term: &str) -> f64 {
let distance = levenshtein(query, term) as f64;
1.0 / (distance + 1.0)
}
fn get_schema_fields(&self) -> Result<(Field, Field, Field, Field, Field), FlowyError> {
let folder_schema = match self.folder_schema.clone() {
Some(schema) => schema,
_ => return Err(FlowyError::folder_index_manager_unavailable()),
};
/// Simple implementation to index all given data by spawning async tasks.
fn index_all(&self, data_vec: Vec<IndexableData>) -> Result<(), FlowyError> {
for data in data_vec {
let indexer = self.clone();
tokio::spawn(async move {
let _ = indexer.add_index(data).await;
});
}
Ok(())
}
let id_field = folder_schema.schema.get_field(FOLDER_ID_FIELD_NAME)?;
let title_field = folder_schema.schema.get_field(FOLDER_TITLE_FIELD_NAME)?;
let icon_field = folder_schema.schema.get_field(FOLDER_ICON_FIELD_NAME)?;
let icon_ty_field = folder_schema.schema.get_field(FOLDER_ICON_TY_FIELD_NAME)?;
let workspace_id_field = folder_schema
.schema
.get_field(FOLDER_WORKSPACE_ID_FIELD_NAME)?;
/// Searches the index using the given query string.
pub async fn search(&self, query: String) -> Result<Vec<SearchResponseItemPB>, FlowyError> {
let lock = self.state.read().await;
let state = lock
.as_ref()
.ok_or_else(FlowyError::folder_index_manager_unavailable)?;
let schema = &state.folder_schema;
let index = &state.index;
let reader = &state.index_reader;
Ok((
id_field,
title_field,
icon_field,
icon_ty_field,
workspace_id_field,
))
let title_field = schema.schema.get_field(FOLDER_TITLE_FIELD_NAME)?;
let mut parser = QueryParser::for_index(index, vec![title_field]);
parser.set_field_fuzzy(title_field, true, 2, true);
let built_query = parser.parse_query(&query)?;
let searcher = reader.searcher();
let top_docs = searcher.search(&built_query, &TopDocs::with_limit(10))?;
let mut results = Vec::new();
for (_score, doc_address) in top_docs {
let doc: TantivyDocument = searcher.doc(doc_address)?;
let named_doc = doc.to_named_doc(&schema.schema);
let mut content = HashMap::new();
for (k, v) in named_doc.0 {
content.insert(k, v[0].clone());
}
if !content.is_empty() {
let s = serde_json::to_string(&content)?;
let result: SearchResponseItemPB = serde_json::from_str::<FolderIndexData>(&s)?.into();
results.push(result.with_score(self.score_result(&query, &result.data) as f64));
}
}
Ok(results)
}
}
#[async_trait]
impl IndexManager for FolderIndexManagerImpl {
fn is_indexed(&self) -> bool {
self
.index_reader
.clone()
.map(|reader| reader.searcher().num_docs() > 0)
.unwrap_or(false)
}
fn set_index_content_receiver(&self, mut rx: IndexContentReceiver, workspace_id: Uuid) {
async fn set_index_content_receiver(&self, mut rx: IndexContentReceiver, workspace_id: Uuid) {
let indexer = self.clone();
let wid = workspace_id;
tokio::spawn(async move {
@ -301,31 +176,35 @@ impl IndexManager for FolderIndexManagerImpl {
match msg {
IndexContent::Create(value) => match serde_json::from_value::<ViewIndexContent>(value) {
Ok(view) => {
let _ = indexer.add_index(IndexableData {
id: view.id,
data: view.name,
icon: view.icon,
layout: view.layout,
workspace_id: wid,
});
let _ = indexer
.add_index(IndexableData {
id: view.id,
data: view.name,
icon: view.icon,
layout: view.layout,
workspace_id: wid,
})
.await;
},
Err(err) => tracing::error!("FolderIndexManager error deserialize: {:?}", err),
Err(err) => tracing::error!("FolderIndexManager error deserialize (create): {:?}", err),
},
IndexContent::Update(value) => match serde_json::from_value::<ViewIndexContent>(value) {
Ok(view) => {
let _ = indexer.update_index(IndexableData {
id: view.id,
data: view.name,
icon: view.icon,
layout: view.layout,
workspace_id: wid,
});
let _ = indexer
.update_index(IndexableData {
id: view.id,
data: view.name,
icon: view.icon,
layout: view.layout,
workspace_id: wid,
})
.await;
},
Err(err) => tracing::error!("FolderIndexManager error deserialize: {:?}", err),
Err(err) => tracing::error!("FolderIndexManager error deserialize (update): {:?}", err),
},
IndexContent::Delete(ids) => {
if let Err(e) = indexer.remove_indices(ids) {
tracing::error!("FolderIndexManager error deserialize: {:?}", e);
if let Err(e) = indexer.remove_indices(ids).await {
tracing::error!("FolderIndexManager error (delete): {:?}", e);
}
},
}
@ -333,100 +212,108 @@ impl IndexManager for FolderIndexManagerImpl {
});
}
fn update_index(&self, data: IndexableData) -> Result<(), FlowyError> {
let mut index_writer = self.get_index_writer()?;
let (id_field, title_field, icon_field, icon_ty_field, workspace_id_field) =
self.get_schema_fields()?;
let delete_term = Term::from_field_text(id_field, &data.id.clone());
// Remove old index
index_writer.delete_term(delete_term);
async fn add_index(&self, data: IndexableData) -> Result<(), FlowyError> {
let (icon, icon_ty) = self.extract_icon(data.icon, data.layout);
// Add new index
let _ = index_writer.add_document(doc![
id_field => data.id.clone(),
title_field => data.data,
icon_field => icon.unwrap_or_default(),
icon_ty_field => icon_ty,
workspace_id_field => data.workspace_id.to_string(),
]);
index_writer.commit()?;
Ok(())
}
fn remove_indices(&self, ids: Vec<String>) -> Result<(), FlowyError> {
let mut index_writer = self.get_index_writer()?;
let folder_schema = self.get_folder_schema()?;
let id_field = folder_schema.schema.get_field(FOLDER_ID_FIELD_NAME)?;
for id in ids {
let delete_term = Term::from_field_text(id_field, &id);
index_writer.delete_term(delete_term);
}
index_writer.commit()?;
Ok(())
}
fn add_index(&self, data: IndexableData) -> Result<(), FlowyError> {
let mut index_writer = self.get_index_writer()?;
let (id_field, title_field, icon_field, icon_ty_field, workspace_id_field) =
self.get_schema_fields()?;
let (icon, icon_ty) = self.extract_icon(data.icon, data.layout);
// Add new index
let _ = index_writer.add_document(doc![
id_field => data.id,
title_field => data.data,
icon_field => icon.unwrap_or_default(),
icon_ty_field => icon_ty,
workspace_id_field => data.workspace_id.to_string(),
]);
index_writer.commit()?;
Ok(())
}
/// Removes all indexes that are related by workspace id. This is useful
/// for cleaning indexes when eg. removing/leaving a workspace.
///
fn remove_indices_for_workspace(&self, workspace_id: Uuid) -> Result<(), FlowyError> {
let mut index_writer = self.get_index_writer()?;
let folder_schema = self.get_folder_schema()?;
let id_field = folder_schema
.schema
.get_field(FOLDER_WORKSPACE_ID_FIELD_NAME)?;
let delete_term = Term::from_field_text(id_field, &workspace_id.to_string());
index_writer.delete_term(delete_term);
index_writer.commit()?;
Ok(())
}
fn as_any(&self) -> &dyn Any {
self
.with_writer(|index_writer, folder_schema| {
let (id_field, title_field, icon_field, icon_ty_field, workspace_id_field) =
get_schema_fields(folder_schema)?;
let _ = index_writer.add_document(doc![
id_field => data.id,
title_field => data.data,
icon_field => icon.unwrap_or_default(),
icon_ty_field => icon_ty,
workspace_id_field => data.workspace_id.to_string(),
]);
index_writer.commit()?;
Ok(())
})
.await?;
Ok(())
}
async fn update_index(&self, data: IndexableData) -> Result<(), FlowyError> {
self
.with_writer(|index_writer, folder_schema| {
let (id_field, title_field, icon_field, icon_ty_field, workspace_id_field) =
get_schema_fields(folder_schema)?;
let delete_term = Term::from_field_text(id_field, &data.id);
index_writer.delete_term(delete_term);
let (icon, icon_ty) = self.extract_icon(data.icon, data.layout);
let _ = index_writer.add_document(doc![
id_field => data.id,
title_field => data.data,
icon_field => icon.unwrap_or_default(),
icon_ty_field => icon_ty,
workspace_id_field => data.workspace_id.to_string(),
]);
index_writer.commit()?;
Ok(())
})
.await?;
Ok(())
}
async fn remove_indices(&self, ids: Vec<String>) -> Result<(), FlowyError> {
self
.with_writer(|index_writer, folder_schema| {
let id_field = folder_schema.schema.get_field(FOLDER_ID_FIELD_NAME)?;
for id in ids {
let delete_term = Term::from_field_text(id_field, &id);
index_writer.delete_term(delete_term);
}
index_writer.commit()?;
Ok(())
})
.await?;
Ok(())
}
async fn remove_indices_for_workspace(&self, workspace_id: Uuid) -> Result<(), FlowyError> {
self
.with_writer(|index_writer, folder_schema| {
let id_field = folder_schema
.schema
.get_field(FOLDER_WORKSPACE_ID_FIELD_NAME)?;
let delete_term = Term::from_field_text(id_field, &workspace_id.to_string());
index_writer.delete_term(delete_term);
index_writer.commit()?;
Ok(())
})
.await?;
Ok(())
}
async fn is_indexed(&self) -> bool {
let lock = self.state.read().await;
if let Some(ref state) = *lock {
state.index_reader.searcher().num_docs() > 0
} else {
false
}
}
}
#[async_trait]
impl FolderIndexManager for FolderIndexManagerImpl {
async fn initialize(&self) {
if let Err(e) = self.initialize_with_workspace().await {
error!("Failed to initialize FolderIndexManager: {:?}", e);
}
}
fn index_all_views(&self, views: Vec<Arc<View>>, workspace_id: Uuid) {
let indexable_data = views
.into_iter()
.map(|view| IndexableData::from_view(view, workspace_id))
.collect();
let _ = self.index_all(indexable_data);
}
@ -440,23 +327,50 @@ impl FolderIndexManager for FolderIndexManagerImpl {
for change in changes {
match change {
FolderViewChange::Inserted { view_id } => {
let view = views_iter.find(|view| view.id == view_id);
if let Some(view) = view {
if let Some(view) = views_iter.find(|view| view.id == view_id) {
let indexable_data = IndexableData::from_view(view, workspace_id);
let _ = self.add_index(indexable_data);
let f = self.clone();
tokio::spawn(async move {
let _ = f.add_index(indexable_data).await;
});
}
},
FolderViewChange::Updated { view_id } => {
let view = views_iter.find(|view| view.id == view_id);
if let Some(view) = view {
if let Some(view) = views_iter.find(|view| view.id == view_id) {
let indexable_data = IndexableData::from_view(view, workspace_id);
let _ = self.update_index(indexable_data);
let f = self.clone();
tokio::spawn(async move {
let _ = f.update_index(indexable_data).await;
});
}
},
FolderViewChange::Deleted { view_ids } => {
let _ = self.remove_indices(view_ids);
let f = self.clone();
tokio::spawn(async move {
let _ = f.remove_indices(view_ids).await;
});
},
};
}
}
}
}
fn get_schema_fields(
folder_schema: &FolderSchema,
) -> Result<(Field, Field, Field, Field, Field), FlowyError> {
let id_field = folder_schema.schema.get_field(FOLDER_ID_FIELD_NAME)?;
let title_field = folder_schema.schema.get_field(FOLDER_TITLE_FIELD_NAME)?;
let icon_field = folder_schema.schema.get_field(FOLDER_ICON_FIELD_NAME)?;
let icon_ty_field = folder_schema.schema.get_field(FOLDER_ICON_TY_FIELD_NAME)?;
let workspace_id_field = folder_schema
.schema
.get_field(FOLDER_WORKSPACE_ID_FIELD_NAME)?;
Ok((
id_field,
title_field,
icon_field,
icon_ty_field,
workspace_id_field,
))
}

View File

@ -1,12 +1,13 @@
use std::collections::HashMap;
use std::sync::Arc;
use super::notifier::{SearchNotifier, SearchResultChanged, SearchResultReceiverRunner};
use crate::entities::{SearchFilterPB, SearchResultNotificationPB, SearchResultPB};
use crate::entities::{SearchFilterPB, SearchResponsePB, SearchResultPB};
use allo_isolate::Isolate;
use flowy_error::FlowyResult;
use lib_infra::async_trait::async_trait;
use tokio::sync::broadcast;
use lib_infra::isolate_stream::{IsolateSink, SinkExt};
use std::collections::HashMap;
use std::pin::Pin;
use std::sync::Arc;
use tokio_stream::{self, Stream, StreamExt};
use tracing::{error, trace};
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub enum SearchType {
@ -19,15 +20,12 @@ pub trait SearchHandler: Send + Sync + 'static {
/// returns the type of search this handler is responsible for
fn search_type(&self) -> SearchType;
/// performs a search and returns the results
/// performs a search and returns a stream of results
async fn perform_search(
&self,
query: String,
filter: Option<SearchFilterPB>,
) -> FlowyResult<Vec<SearchResultPB>>;
/// returns the number of indexed objects
fn index_count(&self) -> u64;
) -> Pin<Box<dyn Stream<Item = FlowyResult<SearchResultPB>> + Send + 'static>>;
}
/// The [SearchManager] is used to inject multiple [SearchHandler]'s
@ -36,7 +34,7 @@ pub trait SearchHandler: Send + Sync + 'static {
///
pub struct SearchManager {
pub handlers: HashMap<SearchType, Arc<dyn SearchHandler>>,
notifier: SearchNotifier,
current_search: Arc<tokio::sync::Mutex<Option<String>>>, // Track current search
}
impl SearchManager {
@ -46,45 +44,88 @@ impl SearchManager {
.map(|handler| (handler.search_type(), handler))
.collect();
// Initialize Search Notifier
let (notifier, _) = broadcast::channel(100);
tokio::spawn(SearchResultReceiverRunner(Some(notifier.subscribe())).run());
Self { handlers, notifier }
Self {
handlers,
current_search: Arc::new(tokio::sync::Mutex::new(None)),
}
}
pub fn get_handler(&self, search_type: SearchType) -> Option<&Arc<dyn SearchHandler>> {
self.handlers.get(&search_type)
}
pub fn perform_search(
pub async fn perform_search(
&self,
query: String,
stream_port: i64,
filter: Option<SearchFilterPB>,
channel: Option<String>,
search_id: String,
) {
let max: usize = self.handlers.len();
let handlers = self.handlers.clone();
for (_, handler) in handlers {
let q = query.clone();
let f = filter.clone();
let ch = channel.clone();
let notifier = self.notifier.clone();
tokio::spawn(async move {
let res = handler.perform_search(q.clone(), f).await;
let items = res.unwrap_or_default();
let notification = SearchResultNotificationPB {
items,
sends: max as u64,
channel: ch,
query: q,
};
let _ = notifier.send(SearchResultChanged::SearchResultUpdate(notification));
});
// Cancel previous search by updating current_search
{
let mut current = self.current_search.lock().await;
*current = Some(search_id.clone());
}
let handlers = self.handlers.clone();
let sink = IsolateSink::new(Isolate::new(stream_port));
let mut join_handles = vec![];
let current_search = self.current_search.clone();
for (_, handler) in handlers {
let mut clone_sink = sink.clone();
let query = query.clone();
let filter = filter.clone();
let search_id = search_id.clone();
let current_search = current_search.clone();
let handle = tokio::spawn(async move {
let mut stream = handler.perform_search(query.clone(), filter).await;
while let Some(result) = stream.next().await {
if !is_current_search(&current_search, &search_id).await {
trace!("[Search] search changed, cancel search: {}", query);
break;
}
if let Ok(result) = result {
let resp = SearchResponsePB {
result: Some(result),
search_id: search_id.clone(),
is_loading: true,
};
if let Ok::<Vec<u8>, _>(data) = resp.try_into() {
if let Err(err) = clone_sink.send(data).await {
error!("Failed to send search result: {}", err);
break;
}
}
}
}
if !is_current_search(&current_search, &search_id).await {
trace!("[Search] search changed, cancel search: {}", query);
return;
}
let resp = SearchResponsePB {
result: None,
search_id: search_id.clone(),
is_loading: true,
};
if let Ok::<Vec<u8>, _>(data) = resp.try_into() {
let _ = clone_sink.send(data).await;
}
});
join_handles.push(handle);
}
futures::future::join_all(join_handles).await;
}
}
async fn is_current_search(
current_search: &Arc<tokio::sync::Mutex<Option<String>>>,
search_id: &str,
) -> bool {
let current = current_search.lock().await;
current.as_ref().map_or(false, |id| id == search_id)
}

View File

@ -1,2 +1 @@
pub mod manager;
pub mod notifier;

View File

@ -1,61 +0,0 @@
use async_stream::stream;
use flowy_notification::NotificationBuilder;
use futures::stream::StreamExt;
use tokio::sync::broadcast;
use crate::entities::{SearchNotification, SearchResultNotificationPB};
const SEARCH_OBSERVABLE_SOURCE: &str = "Search";
const SEARCH_ID: &str = "SEARCH_IDENTIFIER";
#[derive(Clone)]
pub enum SearchResultChanged {
SearchResultUpdate(SearchResultNotificationPB),
}
pub type SearchNotifier = broadcast::Sender<SearchResultChanged>;
pub(crate) struct SearchResultReceiverRunner(
pub(crate) Option<broadcast::Receiver<SearchResultChanged>>,
);
impl SearchResultReceiverRunner {
pub(crate) async fn run(mut self) {
let mut receiver = self.0.take().expect("Only take once");
let stream = stream! {
while let Ok(changed) = receiver.recv().await {
yield changed;
}
};
stream
.for_each(|changed| async {
match changed {
SearchResultChanged::SearchResultUpdate(notification) => {
send_notification(
SEARCH_ID,
SearchNotification::DidUpdateResults,
notification.channel.clone(),
)
.payload(notification)
.send();
},
}
})
.await;
}
}
#[tracing::instrument(level = "trace")]
pub fn send_notification(
id: &str,
ty: SearchNotification,
channel: Option<String>,
) -> NotificationBuilder {
let observable_source = &format!(
"{}{}",
SEARCH_OBSERVABLE_SOURCE,
channel.unwrap_or_default()
);
NotificationBuilder::new(id, ty, observable_source)
}

View File

@ -1,19 +1,16 @@
use client_api::entity::search_dto::SearchDocumentResponseItem;
use crate::af_cloud::AFServer;
use flowy_ai_pub::cloud::search_dto::{
SearchDocumentResponseItem, SearchResult, SearchSummaryResult,
};
use flowy_error::FlowyError;
use flowy_search_pub::cloud::SearchCloudService;
use lib_infra::async_trait::async_trait;
use uuid::Uuid;
use crate::af_cloud::AFServer;
pub(crate) struct AFCloudSearchCloudServiceImpl<T> {
pub inner: T,
}
// The limit of what the score should be for results, used to
// filter out irrelevant results.
// https://community.openai.com/t/rule-of-thumb-cosine-similarity-thresholds/693670/5
const SCORE_LIMIT: f64 = 0.3;
const DEFAULT_PREVIEW: u32 = 80;
#[async_trait]
@ -28,14 +25,22 @@ where
) -> Result<Vec<SearchDocumentResponseItem>, FlowyError> {
let client = self.inner.try_get_client()?;
let result = client
.search_documents(workspace_id, &query, 10, DEFAULT_PREVIEW)
.search_documents(workspace_id, &query, 10, DEFAULT_PREVIEW, None)
.await?;
// Filter out irrelevant results
let result = result
.into_iter()
.filter(|r| r.score > SCORE_LIMIT)
.collect();
Ok(result)
}
async fn generate_search_summary(
&self,
workspace_id: &Uuid,
query: String,
search_results: Vec<SearchResult>,
) -> Result<SearchSummaryResult, FlowyError> {
let client = self.inner.try_get_client()?;
let result = client
.generate_search_summary(workspace_id, &query, search_results)
.await?;
Ok(result)
}

View File

@ -127,7 +127,7 @@ where
let try_get_client = self.server.try_get_client();
let client = try_get_client?;
let response = client.sign_in_password(&email, &password).await?;
Ok(response)
Ok(response.gotrue_response)
}
async fn sign_in_with_magic_link(

View File

@ -20,5 +20,5 @@ pub trait UserWorkspaceService: Send + Sync {
) -> FlowyResult<()>;
/// Removes local indexes when a workspace is left/deleted
fn did_delete_workspace(&self, workspace_id: &Uuid) -> FlowyResult<()>;
async fn did_delete_workspace(&self, workspace_id: &Uuid) -> FlowyResult<()>;
}

View File

@ -93,9 +93,9 @@ impl AuthenticateUser {
self.database.get_connection(uid)
}
pub fn get_index_path(&self) -> PathBuf {
let uid = self.user_id().unwrap_or(0);
PathBuf::from(self.user_paths.user_data_dir(uid)).join("indexes")
pub fn get_index_path(&self) -> FlowyResult<PathBuf> {
let uid = self.user_id()?;
Ok(PathBuf::from(self.user_paths.user_data_dir(uid)).join("indexes"))
}
pub fn get_user_data_dir(&self) -> FlowyResult<PathBuf> {

View File

@ -279,6 +279,7 @@ impl UserManager {
self
.user_workspace_service
.did_delete_workspace(workspace_id)
.await
}
#[instrument(level = "info", skip(self), err)]
@ -295,7 +296,8 @@ impl UserManager {
self
.user_workspace_service
.did_delete_workspace(workspace_id)?;
.did_delete_workspace(workspace_id)
.await?;
Ok(())
}

View File

@ -7,6 +7,7 @@ use std::pin::Pin;
use std::task::{Context, Poll};
#[pin_project]
#[derive(Clone, Debug)]
pub struct IsolateSink {
isolate: Isolate,
}