mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-16 22:50:29 +03:00
feat: integrate cloud document search (#5523)
This commit is contained in:
parent
4f4be7eac7
commit
bd5f5f8b9e
@ -20,7 +20,6 @@ class CommandPaletteBloc
|
||||
CommandPaletteBloc() : super(CommandPaletteState.initial()) {
|
||||
_searchListener.start(
|
||||
onResultsChanged: _onResultsChanged,
|
||||
onResultsClosed: _onResultsClosed,
|
||||
);
|
||||
|
||||
_initTrash();
|
||||
@ -35,6 +34,7 @@ class CommandPaletteBloc
|
||||
final TrashListener _trashListener = TrashListener();
|
||||
String? _oldQuery;
|
||||
String? _workspaceId;
|
||||
int _messagesReceived = 0;
|
||||
|
||||
@override
|
||||
Future<void> close() {
|
||||
@ -75,18 +75,22 @@ class CommandPaletteBloc
|
||||
emit(state.copyWith(query: null, isLoading: false, results: []));
|
||||
}
|
||||
},
|
||||
resultsChanged: (results, didClose) {
|
||||
resultsChanged: (results, max) {
|
||||
if (state.query != _oldQuery) {
|
||||
emit(state.copyWith(results: []));
|
||||
_oldQuery = state.query;
|
||||
_messagesReceived = 0;
|
||||
}
|
||||
|
||||
_messagesReceived++;
|
||||
|
||||
final searchResults = _filterDuplicates(results.items);
|
||||
searchResults.sort((a, b) => b.score.compareTo(a.score));
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
results: searchResults,
|
||||
isLoading: !didClose,
|
||||
isLoading: _messagesReceived != max,
|
||||
),
|
||||
);
|
||||
},
|
||||
@ -94,6 +98,9 @@ class CommandPaletteBloc
|
||||
_workspaceId = workspaceId;
|
||||
emit(state.copyWith(results: [], query: '', isLoading: false));
|
||||
},
|
||||
clearSearch: () {
|
||||
emit(state.copyWith(results: [], query: '', isLoading: false));
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
@ -125,6 +132,10 @@ class CommandPaletteBloc
|
||||
final res = [...results];
|
||||
|
||||
for (final item in results) {
|
||||
if (item.data.trim().isEmpty) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final duplicateIndex = currentItems.indexWhere((a) => a.id == item.id);
|
||||
if (duplicateIndex == -1) {
|
||||
continue;
|
||||
@ -145,10 +156,7 @@ class CommandPaletteBloc
|
||||
add(CommandPaletteEvent.performSearch(search: value));
|
||||
|
||||
void _onResultsChanged(RepeatedSearchResultPB results) =>
|
||||
add(CommandPaletteEvent.resultsChanged(results: results));
|
||||
|
||||
void _onResultsClosed(RepeatedSearchResultPB results) =>
|
||||
add(CommandPaletteEvent.resultsChanged(results: results, didClose: true));
|
||||
add(CommandPaletteEvent.resultsChanged(results: results, max: 2));
|
||||
}
|
||||
|
||||
@freezed
|
||||
@ -161,7 +169,7 @@ class CommandPaletteEvent with _$CommandPaletteEvent {
|
||||
|
||||
const factory CommandPaletteEvent.resultsChanged({
|
||||
required RepeatedSearchResultPB results,
|
||||
@Default(false) bool didClose,
|
||||
@Default(1) int max,
|
||||
}) = _ResultsChanged;
|
||||
|
||||
const factory CommandPaletteEvent.trashChanged({
|
||||
@ -171,6 +179,8 @@ class CommandPaletteEvent with _$CommandPaletteEvent {
|
||||
const factory CommandPaletteEvent.workspaceChanged({
|
||||
@Default(null) String? workspaceId,
|
||||
}) = _WorkspaceChanged;
|
||||
|
||||
const factory CommandPaletteEvent.clearSearch() = _ClearSearch;
|
||||
}
|
||||
|
||||
@freezed
|
||||
|
@ -59,13 +59,6 @@ class SearchListener {
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
break;
|
||||
case SearchNotification.DidCloseResults:
|
||||
result.fold(
|
||||
(payload) => _updateDidCloseNotifier?.value =
|
||||
RepeatedSearchResultPB.fromBuffer(payload),
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
@ -133,8 +133,7 @@ class CommandPaletteModal extends StatelessWidget {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SearchField(query: state.query, isLoading: state.isLoading),
|
||||
if ((state.query?.isEmpty ?? true) ||
|
||||
state.isLoading && state.results.isEmpty) ...[
|
||||
if (state.query?.isEmpty ?? true) ...[
|
||||
const Divider(height: 0),
|
||||
Flexible(
|
||||
child: RecentViewsList(
|
||||
@ -150,6 +149,9 @@ class CommandPaletteModal extends StatelessWidget {
|
||||
results: state.results,
|
||||
),
|
||||
),
|
||||
] else if ((state.query?.isNotEmpty ?? false) &&
|
||||
!state.isLoading) ...[
|
||||
const _NoResultsHint(),
|
||||
],
|
||||
_CommandPaletteFooter(
|
||||
shouldShow: state.results.isNotEmpty &&
|
||||
@ -163,6 +165,27 @@ class CommandPaletteModal extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
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});
|
||||
|
||||
@ -177,6 +200,7 @@ class _CommandPaletteFooter extends StatelessWidget {
|
||||
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(
|
||||
|
@ -26,12 +26,12 @@ class RecentViewTile extends StatelessWidget {
|
||||
title: Row(
|
||||
children: [
|
||||
icon,
|
||||
const HSpace(4),
|
||||
const HSpace(6),
|
||||
FlowyText(view.name),
|
||||
],
|
||||
),
|
||||
focusColor: Theme.of(context).colorScheme.primary.withOpacity(0.5),
|
||||
hoverColor: Theme.of(context).colorScheme.primary.withOpacity(0.5),
|
||||
focusColor: Theme.of(context).colorScheme.primary.withOpacity(0.1),
|
||||
hoverColor: Theme.of(context).colorScheme.primary.withOpacity(0.1),
|
||||
onTap: () {
|
||||
onSelected();
|
||||
|
||||
|
@ -1,3 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/workspace/application/recent/recent_views_bloc.dart';
|
||||
@ -6,7 +8,6 @@ import 'package:appflowy/workspace/presentation/command_palette/widgets/recent_v
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class RecentViewsList extends StatelessWidget {
|
||||
@ -51,7 +52,7 @@ class RecentViewsList extends StatelessWidget {
|
||||
: FlowySvg(view.iconData, size: const Size.square(20));
|
||||
|
||||
return RecentViewTile(
|
||||
icon: icon,
|
||||
icon: SizedBox(width: 24, child: icon),
|
||||
view: view,
|
||||
onSelected: onSelected,
|
||||
);
|
||||
|
@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
@ -23,12 +24,24 @@ class SearchField extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _SearchFieldState extends State<SearchField> {
|
||||
final focusNode = FocusNode();
|
||||
late final FocusNode focusNode;
|
||||
late final controller = TextEditingController(text: widget.query);
|
||||
|
||||
@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;
|
||||
},
|
||||
);
|
||||
focusNode.requestFocus();
|
||||
controller.selection = TextSelection(
|
||||
baseOffset: 0,
|
||||
@ -75,34 +88,77 @@ class _SearchFieldState extends State<SearchField> {
|
||||
.textTheme
|
||||
.bodySmall!
|
||||
.copyWith(color: Theme.of(context).colorScheme.error),
|
||||
// TODO(Mathias): Remove beta when support document/database search
|
||||
suffix: FlowyTooltip(
|
||||
message: LocaleKeys.commandPalette_betaTooltip.tr(),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 5,
|
||||
vertical: 1,
|
||||
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,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: AFThemeExtension.of(context).lightGreyHover,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
const HSpace(8),
|
||||
// TODO(Mathias): Remove beta when support database search
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: FlowyText.semibold(
|
||||
LocaleKeys.commandPalette_betaLabel.tr(),
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
counterText: "",
|
||||
focusedBorder: const OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.transparent),
|
||||
borderRadius: Corners.s8Border,
|
||||
borderSide: BorderSide(color: Colors.transparent),
|
||||
),
|
||||
errorBorder: OutlineInputBorder(
|
||||
borderRadius: Corners.s8Border,
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
borderRadius: Corners.s8Border,
|
||||
),
|
||||
),
|
||||
onChanged: (value) => context
|
||||
@ -111,7 +167,6 @@ class _SearchFieldState extends State<SearchField> {
|
||||
),
|
||||
),
|
||||
if (widget.isLoading) ...[
|
||||
const HSpace(12),
|
||||
FlowyTooltip(
|
||||
message: LocaleKeys.commandPalette_loadingTooltip.tr(),
|
||||
child: const SizedBox(
|
||||
@ -125,4 +180,11 @@ class _SearchFieldState extends State<SearchField> {
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _clearSearch() {
|
||||
controller.clear();
|
||||
context
|
||||
.read<CommandPaletteBloc>()
|
||||
.add(const CommandPaletteEvent.clearSearch());
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
@ -8,10 +9,11 @@ import 'package:appflowy/workspace/application/command_palette/search_result_ext
|
||||
import 'package:appflowy_backend/protobuf/flowy-search/result.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
|
||||
class SearchResultTile extends StatelessWidget {
|
||||
class SearchResultTile extends StatefulWidget {
|
||||
const SearchResultTile({
|
||||
super.key,
|
||||
required this.result,
|
||||
@ -24,40 +26,123 @@ class SearchResultTile extends StatelessWidget {
|
||||
final bool isTrashed;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final icon = result.getIcon();
|
||||
State<SearchResultTile> createState() => _SearchResultTileState();
|
||||
}
|
||||
|
||||
return ListTile(
|
||||
dense: true,
|
||||
title: Row(
|
||||
children: [
|
||||
if (icon != null) ...[icon, const HSpace(6)],
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (isTrashed) ...[
|
||||
FlowyText(
|
||||
LocaleKeys.commandPalette_fromTrashHint.tr(),
|
||||
color: AFThemeExtension.of(context).textColor.withAlpha(175),
|
||||
fontSize: 10,
|
||||
),
|
||||
],
|
||||
FlowyText(result.data),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
focusColor: Theme.of(context).colorScheme.primary.withOpacity(0.5),
|
||||
hoverColor: Theme.of(context).colorScheme.primary.withOpacity(0.5),
|
||||
class _SearchResultTileState extends State<SearchResultTile> {
|
||||
bool _hasFocus = false;
|
||||
|
||||
final focusNode = FocusNode();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
focusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final icon = widget.result.getIcon();
|
||||
final cleanedPreview = _cleanPreview(widget.result.preview);
|
||||
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () {
|
||||
onSelected();
|
||||
widget.onSelected();
|
||||
|
||||
getIt<ActionNavigationBloc>().add(
|
||||
ActionNavigationEvent.performAction(
|
||||
action: NavigationAction(objectId: result.viewId),
|
||||
action: NavigationAction(objectId: widget.result.viewId),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Focus(
|
||||
onKeyEvent: (node, event) {
|
||||
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),
|
||||
),
|
||||
);
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
|
||||
return KeyEventResult.ignored;
|
||||
},
|
||||
onFocusChange: (hasFocus) => setState(() => _hasFocus = hasFocus),
|
||||
child: FlowyHover(
|
||||
isSelected: () => _hasFocus,
|
||||
style: HoverStyle(
|
||||
hoverColor: Theme.of(context).colorScheme.primary.withOpacity(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: [
|
||||
if (icon != null) ...[
|
||||
SizedBox(width: 24, child: icon),
|
||||
const HSpace(6),
|
||||
],
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (widget.isTrashed) ...[
|
||||
FlowyText(
|
||||
LocaleKeys.commandPalette_fromTrashHint.tr(),
|
||||
color: AFThemeExtension.of(context)
|
||||
.textColor
|
||||
.withAlpha(175),
|
||||
fontSize: 10,
|
||||
),
|
||||
],
|
||||
FlowyText(widget.result.data),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
if (cleanedPreview.isNotEmpty) ...[
|
||||
const VSpace(4),
|
||||
_DocumentPreview(preview: cleanedPreview),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _cleanPreview(String preview) {
|
||||
return preview.replaceAll('\n', ' ').trim();
|
||||
}
|
||||
}
|
||||
|
||||
class _DocumentPreview extends StatelessWidget {
|
||||
const _DocumentPreview({required this.preview});
|
||||
|
||||
final String preview;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16) +
|
||||
const EdgeInsets.only(left: 14),
|
||||
child: FlowyText.regular(
|
||||
preview,
|
||||
color: Theme.of(context).hintColor,
|
||||
fontSize: 12,
|
||||
maxLines: 3,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
12
frontend/appflowy_tauri/src-tauri/Cargo.lock
generated
12
frontend/appflowy_tauri/src-tauri/Cargo.lock
generated
@ -1330,7 +1330,7 @@ dependencies = [
|
||||
"cssparser-macros",
|
||||
"dtoa-short",
|
||||
"itoa 1.0.6",
|
||||
"phf 0.11.2",
|
||||
"phf 0.8.0",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
@ -1927,6 +1927,7 @@ dependencies = [
|
||||
"flowy-folder",
|
||||
"flowy-folder-pub",
|
||||
"flowy-search",
|
||||
"flowy-search-pub",
|
||||
"flowy-server",
|
||||
"flowy-server-pub",
|
||||
"flowy-sqlite",
|
||||
@ -2216,6 +2217,7 @@ dependencies = [
|
||||
"flowy-user",
|
||||
"futures",
|
||||
"lib-dispatch",
|
||||
"lib-infra",
|
||||
"protobuf",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@ -2232,9 +2234,12 @@ dependencies = [
|
||||
name = "flowy-search-pub"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"client-api",
|
||||
"collab",
|
||||
"collab-folder",
|
||||
"flowy-error",
|
||||
"futures",
|
||||
"lib-infra",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2256,6 +2261,7 @@ dependencies = [
|
||||
"flowy-encrypt",
|
||||
"flowy-error",
|
||||
"flowy-folder-pub",
|
||||
"flowy-search-pub",
|
||||
"flowy-server-pub",
|
||||
"flowy-storage",
|
||||
"flowy-user-pub",
|
||||
@ -4808,7 +4814,7 @@ checksum = "c55e02e35260070b6f716a2423c2ff1c3bb1642ddca6f99e1f26d06268a0e2d2"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"heck 0.4.1",
|
||||
"itertools 0.11.0",
|
||||
"itertools 0.10.5",
|
||||
"log",
|
||||
"multimap",
|
||||
"once_cell",
|
||||
@ -4829,7 +4835,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "efb6c9a1dd1def8e2124d17e83a20af56f1570d6c2d2bd9e266ccb768df3840e"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.11.0",
|
||||
"itertools 0.10.5",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.47",
|
||||
|
4
frontend/appflowy_web/wasm-libs/Cargo.lock
generated
4
frontend/appflowy_web/wasm-libs/Cargo.lock
generated
@ -1519,9 +1519,12 @@ dependencies = [
|
||||
name = "flowy-search-pub"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"client-api",
|
||||
"collab",
|
||||
"collab-folder",
|
||||
"flowy-error",
|
||||
"futures",
|
||||
"lib-infra",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1543,6 +1546,7 @@ dependencies = [
|
||||
"flowy-encrypt",
|
||||
"flowy-error",
|
||||
"flowy-folder-pub",
|
||||
"flowy-search-pub",
|
||||
"flowy-server-pub",
|
||||
"flowy-storage",
|
||||
"flowy-user-pub",
|
||||
|
6
frontend/appflowy_web_app/src-tauri/Cargo.lock
generated
6
frontend/appflowy_web_app/src-tauri/Cargo.lock
generated
@ -1964,6 +1964,7 @@ dependencies = [
|
||||
"flowy-folder",
|
||||
"flowy-folder-pub",
|
||||
"flowy-search",
|
||||
"flowy-search-pub",
|
||||
"flowy-server",
|
||||
"flowy-server-pub",
|
||||
"flowy-sqlite",
|
||||
@ -2253,6 +2254,7 @@ dependencies = [
|
||||
"flowy-user",
|
||||
"futures",
|
||||
"lib-dispatch",
|
||||
"lib-infra",
|
||||
"protobuf",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@ -2269,9 +2271,12 @@ dependencies = [
|
||||
name = "flowy-search-pub"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"client-api",
|
||||
"collab",
|
||||
"collab-folder",
|
||||
"flowy-error",
|
||||
"futures",
|
||||
"lib-infra",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2293,6 +2298,7 @@ dependencies = [
|
||||
"flowy-encrypt",
|
||||
"flowy-error",
|
||||
"flowy-folder-pub",
|
||||
"flowy-search-pub",
|
||||
"flowy-server-pub",
|
||||
"flowy-storage",
|
||||
"flowy-user-pub",
|
||||
|
@ -1876,13 +1876,15 @@
|
||||
"image": "Image"
|
||||
},
|
||||
"commandPalette": {
|
||||
"placeholder": "Type to search for views...",
|
||||
"placeholder": "Type to search...",
|
||||
"bestMatches": "Best matches",
|
||||
"recentHistory": "Recent history",
|
||||
"navigateHint": "to navigate",
|
||||
"loadingTooltip": "We are looking for results...",
|
||||
"betaLabel": "BETA",
|
||||
"betaTooltip": "We currently only support searching for pages",
|
||||
"fromTrashHint": "From trash"
|
||||
"betaTooltip": "We currently only support searching for pages and content in documents",
|
||||
"fromTrashHint": "From trash",
|
||||
"noResultsHint": "We didn't find what you're looking for, try searching for another term.",
|
||||
"clearSearchTooltip": "Clear search field"
|
||||
}
|
||||
}
|
7
frontend/rust-lib/Cargo.lock
generated
7
frontend/rust-lib/Cargo.lock
generated
@ -1761,6 +1761,7 @@ dependencies = [
|
||||
"flowy-folder",
|
||||
"flowy-folder-pub",
|
||||
"flowy-search",
|
||||
"flowy-search-pub",
|
||||
"flowy-server",
|
||||
"flowy-server-pub",
|
||||
"flowy-sqlite",
|
||||
@ -2048,12 +2049,14 @@ dependencies = [
|
||||
"flowy-codegen",
|
||||
"flowy-derive",
|
||||
"flowy-error",
|
||||
"flowy-folder",
|
||||
"flowy-notification",
|
||||
"flowy-search-pub",
|
||||
"flowy-sqlite",
|
||||
"flowy-user",
|
||||
"futures",
|
||||
"lib-dispatch",
|
||||
"lib-infra",
|
||||
"protobuf",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@ -2070,9 +2073,12 @@ dependencies = [
|
||||
name = "flowy-search-pub"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"client-api",
|
||||
"collab",
|
||||
"collab-folder",
|
||||
"flowy-error",
|
||||
"futures",
|
||||
"lib-infra",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2097,6 +2103,7 @@ dependencies = [
|
||||
"flowy-encrypt",
|
||||
"flowy-error",
|
||||
"flowy-folder-pub",
|
||||
"flowy-search-pub",
|
||||
"flowy-server-pub",
|
||||
"flowy-storage",
|
||||
"flowy-user-pub",
|
||||
|
@ -24,6 +24,7 @@ flowy-config = { workspace = true }
|
||||
flowy-date = { workspace = true }
|
||||
collab-integrate = { workspace = true }
|
||||
flowy-search = { workspace = true }
|
||||
flowy-search-pub = { workspace = true }
|
||||
collab-entity = { workspace = true }
|
||||
collab-plugins = { workspace = true }
|
||||
collab = { workspace = true }
|
||||
|
@ -1,12 +1,20 @@
|
||||
use flowy_folder::manager::FolderManager;
|
||||
use flowy_search::document::handler::DocumentSearchHandler;
|
||||
use flowy_search::folder::handler::FolderSearchHandler;
|
||||
use flowy_search::folder::indexer::FolderIndexManagerImpl;
|
||||
use flowy_search::services::manager::SearchManager;
|
||||
use flowy_search_pub::cloud::SearchCloudService;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct SearchDepsResolver();
|
||||
impl SearchDepsResolver {
|
||||
pub async fn resolve(folder_indexer: Arc<FolderIndexManagerImpl>) -> Arc<SearchManager> {
|
||||
pub async fn resolve(
|
||||
folder_indexer: Arc<FolderIndexManagerImpl>,
|
||||
cloud_service: Arc<dyn SearchCloudService>,
|
||||
folder_manager: Arc<FolderManager>,
|
||||
) -> Arc<SearchManager> {
|
||||
let folder_handler = Arc::new(FolderSearchHandler::new(folder_indexer));
|
||||
Arc::new(SearchManager::new(vec![folder_handler]))
|
||||
let document_handler = Arc::new(DocumentSearchHandler::new(cloud_service, folder_manager));
|
||||
Arc::new(SearchManager::new(vec![folder_handler, document_handler]))
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,5 @@
|
||||
use client_api::entity::search_dto::SearchDocumentResponseItem;
|
||||
use flowy_search_pub::cloud::SearchCloudService;
|
||||
use flowy_storage::{ObjectIdentity, ObjectStorageService};
|
||||
use std::sync::Arc;
|
||||
|
||||
@ -601,3 +603,18 @@ impl ChatCloudService for ServerProvider {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl SearchCloudService for ServerProvider {
|
||||
async fn document_search(
|
||||
&self,
|
||||
workspace_id: &str,
|
||||
query: String,
|
||||
) -> Result<Vec<SearchDocumentResponseItem>, FlowyError> {
|
||||
let server = self.get_server()?;
|
||||
match server.search_service() {
|
||||
Some(search_service) => search_service.document_search(workspace_id, query).await,
|
||||
None => Err(FlowyError::internal().with_context("SearchCloudService not found")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -200,7 +200,12 @@ impl AppFlowyCore {
|
||||
)
|
||||
.await;
|
||||
|
||||
let search_manager = SearchDepsResolver::resolve(folder_indexer).await;
|
||||
let search_manager = SearchDepsResolver::resolve(
|
||||
folder_indexer,
|
||||
server_provider.clone(),
|
||||
folder_manager.clone(),
|
||||
)
|
||||
.await;
|
||||
|
||||
(
|
||||
user_manager,
|
||||
|
@ -525,8 +525,8 @@ impl DatabaseManager {
|
||||
.into_iter()
|
||||
.map(|value| {
|
||||
value
|
||||
.into_iter()
|
||||
.map(|(_k, v)| v.to_string())
|
||||
.into_values()
|
||||
.map(|v| v.to_string())
|
||||
.collect::<Vec<String>>()
|
||||
.join(", ")
|
||||
})
|
||||
|
@ -6,7 +6,9 @@ edition = "2021"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
lib-infra = { workspace = true }
|
||||
collab = { workspace = true }
|
||||
collab-folder = { workspace = true }
|
||||
|
||||
flowy-error = { workspace = true }
|
||||
client-api = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
|
12
frontend/rust-lib/flowy-search-pub/src/cloud.rs
Normal file
12
frontend/rust-lib/flowy-search-pub/src/cloud.rs
Normal file
@ -0,0 +1,12 @@
|
||||
use client_api::entity::search_dto::SearchDocumentResponseItem;
|
||||
use flowy_error::FlowyError;
|
||||
use lib_infra::async_trait::async_trait;
|
||||
|
||||
#[async_trait]
|
||||
pub trait SearchCloudService: Send + Sync + 'static {
|
||||
async fn document_search(
|
||||
&self,
|
||||
workspace_id: &str,
|
||||
query: String,
|
||||
) -> Result<Vec<SearchDocumentResponseItem>, FlowyError>;
|
||||
}
|
@ -1 +1,2 @@
|
||||
pub mod cloud;
|
||||
pub mod entities;
|
||||
|
@ -21,10 +21,12 @@ flowy-notification.workspace = true
|
||||
flowy-sqlite.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
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
|
99
frontend/rust-lib/flowy-search/src/document/handler.rs
Normal file
99
frontend/rust-lib/flowy-search/src/document/handler.rs
Normal file
@ -0,0 +1,99 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use flowy_error::FlowyResult;
|
||||
use flowy_folder::{manager::FolderManager, ViewLayout};
|
||||
use flowy_search_pub::cloud::SearchCloudService;
|
||||
use lib_infra::async_trait::async_trait;
|
||||
|
||||
use crate::{
|
||||
entities::{IndexTypePB, ResultIconPB, ResultIconTypePB, SearchFilterPB, SearchResultPB},
|
||||
services::manager::{SearchHandler, SearchType},
|
||||
};
|
||||
|
||||
pub struct DocumentSearchHandler {
|
||||
pub cloud_service: Arc<dyn SearchCloudService>,
|
||||
pub folder_manager: Arc<FolderManager>,
|
||||
}
|
||||
|
||||
impl DocumentSearchHandler {
|
||||
pub fn new(
|
||||
cloud_service: Arc<dyn SearchCloudService>,
|
||||
folder_manager: Arc<FolderManager>,
|
||||
) -> Self {
|
||||
Self {
|
||||
cloud_service,
|
||||
folder_manager,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl SearchHandler for DocumentSearchHandler {
|
||||
fn search_type(&self) -> SearchType {
|
||||
SearchType::Document
|
||||
}
|
||||
|
||||
async fn perform_search(
|
||||
&self,
|
||||
query: String,
|
||||
filter: Option<SearchFilterPB>,
|
||||
) -> FlowyResult<Vec<SearchResultPB>> {
|
||||
let filter = match filter {
|
||||
Some(filter) => filter,
|
||||
None => return Ok(vec![]),
|
||||
};
|
||||
|
||||
let workspace_id = match filter.workspace_id {
|
||||
Some(workspace_id) => workspace_id,
|
||||
None => return Ok(vec![]),
|
||||
};
|
||||
|
||||
let results = self
|
||||
.cloud_service
|
||||
.document_search(&workspace_id, query)
|
||||
.await?;
|
||||
|
||||
// Grab all views from folder cache
|
||||
// Notice that `get_all_view_pb` returns Views that don't include trashed and private views
|
||||
let mut views = self.folder_manager.get_all_views_pb().await?.into_iter();
|
||||
let mut search_results: Vec<SearchResultPB> = vec![];
|
||||
|
||||
for result in results {
|
||||
if let Some(view) = views.find(|v| v.id == result.object_id) {
|
||||
// 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(),
|
||||
})
|
||||
},
|
||||
};
|
||||
|
||||
search_results.push(SearchResultPB {
|
||||
index_type: IndexTypePB::Document,
|
||||
view_id: result.object_id.clone(),
|
||||
id: result.object_id.clone(),
|
||||
data: view.name.clone(),
|
||||
icon,
|
||||
// We reverse the score, the cloud search score is based on
|
||||
// 1 being the worst result, and closer to 0 being good result, that is
|
||||
// the opposite of local search.
|
||||
score: 1.0 - result.score,
|
||||
workspace_id: result.workspace_id,
|
||||
preview: result.preview,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(search_results)
|
||||
}
|
||||
|
||||
/// Ignore for [DocumentSearchHandler]
|
||||
fn index_count(&self) -> u64 {
|
||||
0
|
||||
}
|
||||
}
|
1
frontend/rust-lib/flowy-search/src/document/mod.rs
Normal file
1
frontend/rust-lib/flowy-search/src/document/mod.rs
Normal file
@ -0,0 +1 @@
|
||||
pub mod handler;
|
@ -3,8 +3,9 @@ use flowy_derive::ProtoBuf_Enum;
|
||||
#[derive(ProtoBuf_Enum, Eq, PartialEq, Debug, Clone)]
|
||||
pub enum IndexTypePB {
|
||||
View = 0,
|
||||
DocumentBlock = 1,
|
||||
DatabaseRow = 2,
|
||||
Document = 1,
|
||||
DocumentBlock = 2,
|
||||
DatabaseRow = 3,
|
||||
}
|
||||
|
||||
impl Default for IndexTypePB {
|
||||
|
@ -8,7 +8,7 @@ pub struct SearchResultNotificationPB {
|
||||
pub items: Vec<SearchResultPB>,
|
||||
|
||||
#[pb(index = 2)]
|
||||
pub closed: bool,
|
||||
pub sends: u64,
|
||||
|
||||
#[pb(index = 3, one_of)]
|
||||
pub channel: Option<String>,
|
||||
@ -19,7 +19,6 @@ pub enum SearchNotification {
|
||||
#[default]
|
||||
Unknown = 0,
|
||||
DidUpdateResults = 1,
|
||||
DidCloseResults = 2,
|
||||
}
|
||||
|
||||
impl std::convert::From<SearchNotification> for i32 {
|
||||
@ -32,7 +31,6 @@ impl std::convert::From<i32> for SearchNotification {
|
||||
fn from(notification: i32) -> Self {
|
||||
match notification {
|
||||
1 => SearchNotification::DidUpdateResults,
|
||||
2 => SearchNotification::DidCloseResults,
|
||||
_ => SearchNotification::Unknown,
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
use collab_folder::{IconType, ViewIcon};
|
||||
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
|
||||
use flowy_folder::entities::ViewIconPB;
|
||||
|
||||
use super::IndexTypePB;
|
||||
|
||||
@ -31,6 +32,9 @@ pub struct SearchResultPB {
|
||||
|
||||
#[pb(index = 7)]
|
||||
pub workspace_id: String,
|
||||
|
||||
#[pb(index = 8, one_of)]
|
||||
pub preview: Option<String>,
|
||||
}
|
||||
|
||||
impl SearchResultPB {
|
||||
@ -43,6 +47,7 @@ impl SearchResultPB {
|
||||
icon: self.icon.clone(),
|
||||
score,
|
||||
workspace_id: self.workspace_id.clone(),
|
||||
preview: self.preview.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -122,3 +127,12 @@ impl From<ViewIcon> for ResultIconPB {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ViewIconPB> for ResultIconPB {
|
||||
fn from(val: ViewIconPB) -> Self {
|
||||
ResultIconPB {
|
||||
ty: IconType::from(val.ty).into(),
|
||||
value: val.value,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -30,6 +30,7 @@ impl From<FolderIndexData> for SearchResultPB {
|
||||
score: 0.0,
|
||||
icon,
|
||||
workspace_id: data.workspace_id,
|
||||
preview: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ use crate::{
|
||||
services::manager::{SearchHandler, SearchType},
|
||||
};
|
||||
use flowy_error::FlowyResult;
|
||||
use lib_infra::async_trait::async_trait;
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::indexer::FolderIndexManagerImpl;
|
||||
@ -17,12 +18,13 @@ impl FolderSearchHandler {
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl SearchHandler for FolderSearchHandler {
|
||||
fn search_type(&self) -> SearchType {
|
||||
SearchType::Folder
|
||||
}
|
||||
|
||||
fn perform_search(
|
||||
async fn perform_search(
|
||||
&self,
|
||||
query: String,
|
||||
filter: Option<SearchFilterPB>,
|
||||
|
@ -298,12 +298,9 @@ impl IndexManager for FolderIndexManagerImpl {
|
||||
let wid = workspace_id.clone();
|
||||
af_spawn(async move {
|
||||
while let Ok(msg) = rx.recv().await {
|
||||
tracing::warn!("[Indexer] Message received: {:?}", msg);
|
||||
match msg {
|
||||
IndexContent::Create(value) => match serde_json::from_value::<ViewIndexContent>(value) {
|
||||
Ok(view) => {
|
||||
tracing::warn!("[Indexer] CREATE: {:?}", view);
|
||||
|
||||
let _ = indexer.add_index(IndexableData {
|
||||
id: view.id,
|
||||
data: view.name,
|
||||
@ -316,7 +313,6 @@ impl IndexManager for FolderIndexManagerImpl {
|
||||
},
|
||||
IndexContent::Update(value) => match serde_json::from_value::<ViewIndexContent>(value) {
|
||||
Ok(view) => {
|
||||
tracing::warn!("[Indexer] UPDATE: {:?}", view);
|
||||
let _ = indexer.update_index(IndexableData {
|
||||
id: view.id,
|
||||
data: view.name,
|
||||
@ -328,7 +324,6 @@ impl IndexManager for FolderIndexManagerImpl {
|
||||
Err(err) => tracing::error!("FolderIndexManager error deserialize: {:?}", err),
|
||||
},
|
||||
IndexContent::Delete(ids) => {
|
||||
tracing::warn!("[Indexer] DELETE: {:?}", ids);
|
||||
if let Err(e) = indexer.remove_indices(ids) {
|
||||
tracing::error!("FolderIndexManager error deserialize: {:?}", e);
|
||||
}
|
||||
@ -459,7 +454,6 @@ impl FolderIndexManager for FolderIndexManagerImpl {
|
||||
}
|
||||
},
|
||||
FolderViewChange::Deleted { view_ids } => {
|
||||
tracing::warn!("[Indexer] ViewChange Reached Deleted: {:?}", view_ids);
|
||||
let _ = self.remove_indices(view_ids);
|
||||
},
|
||||
};
|
||||
|
@ -1,3 +1,4 @@
|
||||
pub mod document;
|
||||
pub mod entities;
|
||||
pub mod event_handler;
|
||||
pub mod event_map;
|
||||
|
@ -5,21 +5,27 @@ use super::notifier::{SearchNotifier, SearchResultChanged, SearchResultReceiverR
|
||||
use crate::entities::{SearchFilterPB, SearchResultNotificationPB, SearchResultPB};
|
||||
use flowy_error::FlowyResult;
|
||||
use lib_dispatch::prelude::af_spawn;
|
||||
use tokio::{sync::broadcast, task::spawn_blocking};
|
||||
use lib_infra::async_trait::async_trait;
|
||||
use tokio::sync::broadcast;
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
pub enum SearchType {
|
||||
Folder,
|
||||
Document,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
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
|
||||
fn perform_search(
|
||||
async fn perform_search(
|
||||
&self,
|
||||
query: String,
|
||||
filter: Option<SearchFilterPB>,
|
||||
) -> FlowyResult<Vec<SearchResultPB>>;
|
||||
|
||||
/// returns the number of indexed objects
|
||||
fn index_count(&self) -> u64;
|
||||
}
|
||||
@ -57,25 +63,22 @@ impl SearchManager {
|
||||
filter: Option<SearchFilterPB>,
|
||||
channel: Option<String>,
|
||||
) {
|
||||
let mut sends: usize = 0;
|
||||
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();
|
||||
|
||||
spawn_blocking(move || {
|
||||
let res = handler.perform_search(q, f);
|
||||
sends += 1;
|
||||
af_spawn(async move {
|
||||
let res = handler.perform_search(q, f).await;
|
||||
|
||||
let close = sends == max;
|
||||
let items = res.unwrap_or_default();
|
||||
|
||||
let notification = SearchResultNotificationPB {
|
||||
items,
|
||||
closed: close,
|
||||
sends: max as u64,
|
||||
channel: ch,
|
||||
};
|
||||
|
||||
|
@ -31,15 +31,13 @@ impl SearchResultReceiverRunner {
|
||||
.for_each(|changed| async {
|
||||
match changed {
|
||||
SearchResultChanged::SearchResultUpdate(notification) => {
|
||||
let ty = if notification.closed {
|
||||
SearchNotification::DidCloseResults
|
||||
} else {
|
||||
SearchNotification::DidUpdateResults
|
||||
};
|
||||
|
||||
send_notification(SEARCH_ID, ty, notification.channel.clone())
|
||||
.payload(notification)
|
||||
.send();
|
||||
send_notification(
|
||||
SEARCH_ID,
|
||||
SearchNotification::DidUpdateResults,
|
||||
notification.channel.clone(),
|
||||
)
|
||||
.payload(notification)
|
||||
.send();
|
||||
},
|
||||
}
|
||||
})
|
||||
|
@ -40,6 +40,7 @@ flowy-document-pub = { workspace = true }
|
||||
appflowy-cloud-billing-client = { workspace = true }
|
||||
flowy-error = { workspace = true, features = ["impl_from_serde", "impl_from_reqwest", "impl_from_url", "impl_from_appflowy_cloud"] }
|
||||
flowy-server-pub = { workspace = true }
|
||||
flowy-search-pub = { workspace = true }
|
||||
flowy-encrypt = { workspace = true }
|
||||
flowy-storage = { workspace = true }
|
||||
flowy-chat-pub = { workspace = true }
|
||||
|
@ -3,6 +3,7 @@ pub(crate) use database::*;
|
||||
pub(crate) use document::*;
|
||||
pub(crate) use file_storage::*;
|
||||
pub(crate) use folder::*;
|
||||
pub(crate) use search::*;
|
||||
pub(crate) use user::*;
|
||||
|
||||
mod chat;
|
||||
@ -10,5 +11,6 @@ mod database;
|
||||
mod document;
|
||||
mod file_storage;
|
||||
mod folder;
|
||||
mod search;
|
||||
mod user;
|
||||
mod util;
|
||||
|
40
frontend/rust-lib/flowy-server/src/af_cloud/impls/search.rs
Normal file
40
frontend/rust-lib/flowy-server/src/af_cloud/impls/search.rs
Normal file
@ -0,0 +1,40 @@
|
||||
use client_api::entity::search_dto::SearchDocumentResponseItem;
|
||||
use flowy_error::FlowyError;
|
||||
use flowy_search_pub::cloud::SearchCloudService;
|
||||
use lib_infra::async_trait::async_trait;
|
||||
|
||||
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.
|
||||
const SCORE_LIMIT: f64 = 0.8;
|
||||
const DEFAULT_PREVIEW: u32 = 80;
|
||||
|
||||
#[async_trait]
|
||||
impl<T> SearchCloudService for AFCloudSearchCloudServiceImpl<T>
|
||||
where
|
||||
T: AFServer,
|
||||
{
|
||||
async fn document_search(
|
||||
&self,
|
||||
workspace_id: &str,
|
||||
query: String,
|
||||
) -> Result<Vec<SearchDocumentResponseItem>, FlowyError> {
|
||||
let client = self.inner.try_get_client()?;
|
||||
let result = client
|
||||
.search_documents(workspace_id, &query, 10, DEFAULT_PREVIEW)
|
||||
.await?;
|
||||
|
||||
// Filter out irrelevant results
|
||||
let result = result
|
||||
.into_iter()
|
||||
.filter(|r| r.score < SCORE_LIMIT)
|
||||
.collect();
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
@ -11,6 +11,7 @@ use client_api::ws::{
|
||||
};
|
||||
use client_api::{Client, ClientConfiguration};
|
||||
use flowy_chat_pub::cloud::ChatCloudService;
|
||||
use flowy_search_pub::cloud::SearchCloudService;
|
||||
use flowy_storage::ObjectStorageService;
|
||||
use rand::Rng;
|
||||
use semver::Version;
|
||||
@ -38,6 +39,8 @@ use crate::af_cloud::impls::{
|
||||
|
||||
use crate::AppFlowyServer;
|
||||
|
||||
use super::impls::AFCloudSearchCloudServiceImpl;
|
||||
|
||||
pub(crate) type AFCloudClient = Client;
|
||||
|
||||
pub struct AppFlowyCloudServer {
|
||||
@ -255,6 +258,14 @@ impl AppFlowyServer for AppFlowyCloudServer {
|
||||
};
|
||||
Some(Arc::new(AFCloudFileStorageServiceImpl::new(client)))
|
||||
}
|
||||
|
||||
fn search_service(&self) -> Option<Arc<dyn SearchCloudService>> {
|
||||
let server = AFServerImpl {
|
||||
client: self.get_client(),
|
||||
};
|
||||
|
||||
Some(Arc::new(AFCloudSearchCloudServiceImpl { inner: server }))
|
||||
}
|
||||
}
|
||||
|
||||
/// Spawns a new asynchronous task to handle WebSocket connections based on token state.
|
||||
|
@ -1,3 +1,4 @@
|
||||
use flowy_search_pub::cloud::SearchCloudService;
|
||||
use flowy_storage::ObjectStorageService;
|
||||
use std::sync::Arc;
|
||||
|
||||
@ -70,4 +71,8 @@ impl AppFlowyServer for LocalServer {
|
||||
fn file_storage(&self) -> Option<Arc<dyn ObjectStorageService>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn search_service(&self) -> Option<Arc<dyn SearchCloudService>> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
use client_api::ws::ConnectState;
|
||||
use client_api::ws::WSConnectStateReceiver;
|
||||
use client_api::ws::WebSocketChannel;
|
||||
use flowy_search_pub::cloud::SearchCloudService;
|
||||
use flowy_storage::ObjectStorageService;
|
||||
use std::sync::Arc;
|
||||
|
||||
@ -100,6 +101,10 @@ pub trait AppFlowyServer: Send + Sync + 'static {
|
||||
Arc::new(DefaultChatCloudServiceImpl)
|
||||
}
|
||||
|
||||
/// Bridge for the Cloud AI Search features
|
||||
///
|
||||
fn search_service(&self) -> Option<Arc<dyn SearchCloudService>>;
|
||||
|
||||
/// Manages collaborative objects within a remote storage system. This includes operations such as
|
||||
/// checking storage status, retrieving updates and snapshots, and dispatching updates. The service
|
||||
/// also provides subscription capabilities for real-time updates.
|
||||
|
@ -1,3 +1,4 @@
|
||||
use flowy_search_pub::cloud::SearchCloudService;
|
||||
use flowy_storage::ObjectStorageService;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Weak};
|
||||
@ -194,4 +195,8 @@ impl AppFlowyServer for SupabaseServer {
|
||||
.clone()
|
||||
.map(|s| s as Arc<dyn ObjectStorageService>)
|
||||
}
|
||||
|
||||
fn search_service(&self) -> Option<Arc<dyn SearchCloudService>> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user