fix: search launch review (#5169)

* fix: support filtering results by workspace

* feat: add search button to sidebar

* fix: reduce view/recent view fetching across application

* feat: add channel to search listener

* feat: clean + localization

* chore: remove redundant code

* fix: disable search

* chore: trigger ci

* chore: disable search in backend

* test: disable search tests for now

* feat: temp disable reliance on folder search

* fix: add debounce to inline actions

* chore: complete future if disposed

* fix: clean code

* chore: disable unused bloc with feature flag

* fix: recent views lazy read

* chore: revert podilfe change

* chore: update logs

* chore: update client api and collab

* chore: fix tst

* chore: fix test & update collab commit

* chore: update collab commit

* test: fix unit tests

* chore: update rust toolchain 1.77

* chore: use opt-level 1

* fix: code review

* chore: clippy

---------

Co-authored-by: nathan <nathan@appflowy.io>
Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
This commit is contained in:
Mathias Mogensen
2024-04-23 15:46:57 +02:00
committed by GitHub
parent bf64b0d2fa
commit 275d0b2ac4
91 changed files with 1057 additions and 848 deletions

View File

@ -1,2 +1,2 @@
proto_input = ["src/event_map.rs", "src/entities.rs"]
proto_input = ["src/event_map.rs", "src/entities"]
event_files = ["src/event_map.rs"]

View File

@ -0,0 +1,30 @@
use flowy_derive::ProtoBuf_Enum;
#[derive(ProtoBuf_Enum, Eq, PartialEq, Debug, Clone)]
pub enum IndexTypePB {
View = 0,
DocumentBlock = 1,
DatabaseRow = 2,
}
impl Default for IndexTypePB {
fn default() -> Self {
Self::View
}
}
impl std::convert::From<IndexTypePB> for i32 {
fn from(notification: IndexTypePB) -> Self {
notification as i32
}
}
impl std::convert::From<i32> for IndexTypePB {
fn from(notification: i32) -> Self {
match notification {
1 => IndexTypePB::View,
2 => IndexTypePB::DocumentBlock,
_ => IndexTypePB::DatabaseRow,
}
}
}

View File

@ -0,0 +1,11 @@
mod index_type;
mod notification;
mod query;
mod result;
mod search_filter;
pub use index_type::*;
pub use notification::*;
pub use query::*;
pub use result::*;
pub use search_filter::*;

View File

@ -0,0 +1,39 @@
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
use super::SearchResultPB;
#[derive(ProtoBuf, Default, Debug, Clone)]
pub struct SearchResultNotificationPB {
#[pb(index = 1)]
pub items: Vec<SearchResultPB>,
#[pb(index = 2)]
pub closed: bool,
#[pb(index = 3, one_of)]
pub channel: Option<String>,
}
#[derive(ProtoBuf_Enum, Debug, Default)]
pub enum SearchNotification {
#[default]
Unknown = 0,
DidUpdateResults = 1,
DidCloseResults = 2,
}
impl std::convert::From<SearchNotification> for i32 {
fn from(notification: SearchNotification) -> Self {
notification as i32
}
}
impl std::convert::From<i32> for SearchNotification {
fn from(notification: i32) -> Self {
match notification {
1 => SearchNotification::DidUpdateResults,
2 => SearchNotification::DidCloseResults,
_ => SearchNotification::Unknown,
}
}
}

View File

@ -0,0 +1,25 @@
use flowy_derive::ProtoBuf;
use super::SearchFilterPB;
#[derive(Eq, PartialEq, ProtoBuf, Default, Debug, Clone)]
pub struct SearchQueryPB {
#[pb(index = 1)]
pub search: String,
#[pb(index = 2, one_of)]
pub limit: Option<i64>,
#[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>,
}

View File

@ -1,14 +1,7 @@
use collab_folder::{IconType, ViewIcon};
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
#[derive(Eq, PartialEq, ProtoBuf, Default, Debug, Clone)]
pub struct SearchQueryPB {
#[pb(index = 1)]
pub search: String,
#[pb(index = 2, one_of)]
pub limit: Option<i64>,
}
use super::IndexTypePB;
#[derive(Debug, Default, ProtoBuf, Clone)]
pub struct RepeatedSearchResultPB {
@ -35,6 +28,9 @@ pub struct SearchResultPB {
#[pb(index = 6)]
pub score: f64,
#[pb(index = 7)]
pub workspace_id: String,
}
impl SearchResultPB {
@ -46,6 +42,7 @@ impl SearchResultPB {
data: self.data.clone(),
icon: self.icon.clone(),
score,
workspace_id: self.workspace_id.clone(),
}
}
}
@ -125,65 +122,3 @@ impl From<ViewIcon> for ResultIconPB {
}
}
}
#[derive(ProtoBuf_Enum, Eq, PartialEq, Debug, Clone)]
pub enum IndexTypePB {
View = 0,
DocumentBlock = 1,
DatabaseRow = 2,
}
impl Default for IndexTypePB {
fn default() -> Self {
Self::View
}
}
impl std::convert::From<IndexTypePB> for i32 {
fn from(notification: IndexTypePB) -> Self {
notification as i32
}
}
impl std::convert::From<i32> for IndexTypePB {
fn from(notification: i32) -> Self {
match notification {
1 => IndexTypePB::View,
2 => IndexTypePB::DocumentBlock,
_ => IndexTypePB::DatabaseRow,
}
}
}
#[derive(ProtoBuf, Default, Debug, Clone)]
pub struct SearchResultNotificationPB {
#[pb(index = 1)]
pub items: Vec<SearchResultPB>,
#[pb(index = 2)]
pub closed: bool,
}
#[derive(ProtoBuf_Enum, Debug, Default)]
pub enum SearchNotification {
#[default]
Unknown = 0,
DidUpdateResults = 1,
DidCloseResults = 2,
}
impl std::convert::From<SearchNotification> for i32 {
fn from(notification: SearchNotification) -> Self {
notification as i32
}
}
impl std::convert::From<i32> for SearchNotification {
fn from(notification: i32) -> Self {
match notification {
1 => SearchNotification::DidUpdateResults,
2 => SearchNotification::DidCloseResults,
_ => SearchNotification::Unknown,
}
}
}

View File

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

View File

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

View File

@ -8,6 +8,7 @@ pub struct FolderIndexData {
pub title: String,
pub icon: String,
pub icon_ty: i64,
pub workspace_id: String,
}
impl From<FolderIndexData> for SearchResultPB {
@ -28,6 +29,7 @@ impl From<FolderIndexData> for SearchResultPB {
data: data.title,
score: 0.0,
icon,
workspace_id: data.workspace_id,
}
}
}

View File

@ -1,5 +1,7 @@
use crate::entities::SearchResultPB;
use crate::services::manager::{SearchHandler, SearchType};
use crate::{
entities::{SearchFilterPB, SearchResultPB},
services::manager::{SearchHandler, SearchType},
};
use flowy_error::FlowyResult;
use std::sync::Arc;
@ -20,8 +22,20 @@ impl SearchHandler for FolderSearchHandler {
SearchType::Folder
}
fn perform_search(&self, query: String) -> FlowyResult<Vec<SearchResultPB>> {
self.index_manager.search(query)
fn perform_search(
&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);
}
}
Ok(results)
}
fn index_count(&self) -> u64 {

View File

@ -8,8 +8,11 @@ use std::{
};
use crate::{
entities::ResultIconTypePB,
folder::schema::{FolderSchema, FOLDER_ICON_FIELD_NAME, FOLDER_TITLE_FIELD_NAME},
entities::{ResultIconTypePB, SearchFilterPB, SearchResultPB},
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,
},
};
use collab::core::collab::{IndexContent, IndexContentReceiver};
use collab_folder::{View, ViewIcon, ViewIndexContent, ViewLayout};
@ -23,12 +26,7 @@ use tantivy::{
IndexWriter, Term,
};
use crate::entities::SearchResultPB;
use super::{
entities::FolderIndexData,
schema::{FOLDER_ICON_TY_FIELD_NAME, FOLDER_ID_FIELD_NAME},
};
use super::entities::FolderIndexData;
#[derive(Clone)]
pub struct FolderIndexManagerImpl {
@ -41,7 +39,15 @@ pub struct FolderIndexManagerImpl {
const FOLDER_INDEX_DIR: &str = "folder_index";
impl FolderIndexManagerImpl {
pub fn new(auth_user: Weak<AuthenticateUser>) -> Self {
pub fn new(auth_user: Option<Weak<AuthenticateUser>>) -> Self {
// TODO(Mathias): Temporarily disable seaerch
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();
@ -130,15 +136,19 @@ impl FolderIndexManagerImpl {
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,
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.clone(),
]);
}
@ -206,7 +216,11 @@ impl FolderIndexManagerImpl {
(icon, icon_ty)
}
pub fn search(&self, query: String) -> Result<Vec<SearchResultPB>, FlowyError> {
pub fn search(
&self,
query: String,
_filter: Option<SearchFilterPB>,
) -> Result<Vec<SearchResultPB>, FlowyError> {
let folder_schema = self.get_folder_schema()?;
let index = match &self.index {
@ -222,11 +236,7 @@ impl FolderIndexManagerImpl {
let title_field = folder_schema.schema.get_field(FOLDER_TITLE_FIELD_NAME)?;
let length = query.len();
let distance: u8 = match length {
_ if length > 4 => 2,
_ if length > 2 => 1,
_ => 0,
};
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);
@ -273,8 +283,9 @@ impl IndexManager for FolderIndexManagerImpl {
.unwrap_or(false)
}
fn set_index_content_receiver(&self, mut rx: IndexContentReceiver) {
fn set_index_content_receiver(&self, mut rx: IndexContentReceiver, workspace_id: String) {
let indexer = self.clone();
let wid = workspace_id.clone();
af_spawn(async move {
while let Ok(msg) = rx.recv().await {
match msg {
@ -285,6 +296,7 @@ impl IndexManager for FolderIndexManagerImpl {
data: view.name,
icon: view.icon,
layout: view.layout,
workspace_id: wid.clone(),
});
},
Err(err) => tracing::error!("FolderIndexManager error deserialize: {:?}", err),
@ -296,6 +308,7 @@ impl IndexManager for FolderIndexManagerImpl {
data: view.name,
icon: view.icon,
layout: view.layout,
workspace_id: wid.clone(),
});
},
Err(err) => tracing::error!("FolderIndexManager error deserialize: {:?}", err),
@ -317,7 +330,11 @@ impl IndexManager for FolderIndexManagerImpl {
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 icon_ty_field: tantivy::schema::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)?;
let delete_term = Term::from_field_text(id_field, &data.id.clone());
@ -332,6 +349,7 @@ impl IndexManager for FolderIndexManagerImpl {
title_field => data.data,
icon_field => icon.unwrap_or_default(),
icon_ty_field => icon_ty,
workspace_id_field => data.workspace_id.clone(),
]);
index_writer.commit()?;
@ -364,6 +382,9 @@ impl IndexManager for FolderIndexManagerImpl {
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)?;
let (icon, icon_ty) = self.extract_icon(data.icon, data.layout);
@ -373,6 +394,7 @@ impl IndexManager for FolderIndexManagerImpl {
title_field => data.data,
icon_field => icon.unwrap_or_default(),
icon_ty_field => icon_ty,
workspace_id_field => data.workspace_id,
]);
index_writer.commit()?;
@ -386,7 +408,7 @@ impl IndexManager for FolderIndexManagerImpl {
}
impl FolderIndexManager for FolderIndexManagerImpl {
fn index_all_views(&self, views: Vec<View>) {
fn index_all_views(&self, views: Vec<View>, workspace_id: String) {
let indexable_data = views
.into_iter()
.map(|view| IndexableData {
@ -394,6 +416,7 @@ impl FolderIndexManager for FolderIndexManagerImpl {
data: view.name,
icon: view.icon,
layout: view.layout,
workspace_id: workspace_id.clone(),
})
.collect();

View File

@ -4,6 +4,7 @@ pub const FOLDER_ID_FIELD_NAME: &str = "id";
pub const FOLDER_TITLE_FIELD_NAME: &str = "title";
pub const FOLDER_ICON_FIELD_NAME: &str = "icon";
pub const FOLDER_ICON_TY_FIELD_NAME: &str = "icon_ty";
pub const FOLDER_WORKSPACE_ID_FIELD_NAME: &str = "workspace_id";
#[derive(Clone)]
pub struct FolderSchema {
@ -33,6 +34,10 @@ impl FolderSchema {
tantivy::schema::TEXT | tantivy::schema::STORED,
);
schema_builder.add_i64_field(FOLDER_ICON_TY_FIELD_NAME, tantivy::schema::STORED);
schema_builder.add_text_field(
FOLDER_WORKSPACE_ID_FIELD_NAME,
tantivy::schema::TEXT | tantivy::schema::STORED,
);
let schema = schema_builder.build();

View File

@ -5,7 +5,7 @@ use flowy_error::FlowyResult;
use lib_dispatch::prelude::af_spawn;
use tokio::{sync::broadcast, task::spawn_blocking};
use crate::entities::{SearchResultNotificationPB, SearchResultPB};
use crate::entities::{SearchFilterPB, SearchResultNotificationPB, SearchResultPB};
use super::notifier::{SearchNotifier, SearchResultChanged, SearchResultReceiverRunner};
@ -18,7 +18,11 @@ 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(&self, query: String) -> FlowyResult<Vec<SearchResultPB>>;
fn perform_search(
&self,
query: String,
filter: Option<SearchFilterPB>,
) -> FlowyResult<Vec<SearchResultPB>>;
/// returns the number of indexed objects
fn index_count(&self) -> u64;
}
@ -50,17 +54,24 @@ impl SearchManager {
self.handlers.get(&search_type)
}
pub fn perform_search(&self, query: String) {
pub fn perform_search(
&self,
query: String,
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);
let res = handler.perform_search(q, f);
sends += 1;
let close = sends == max;
@ -68,6 +79,7 @@ impl SearchManager {
let notification = SearchResultNotificationPB {
items,
closed: close,
channel: ch,
};
let _ = notifier.send(SearchResultChanged::SearchResultUpdate(notification));

View File

@ -37,7 +37,7 @@ impl SearchResultReceiverRunner {
SearchNotification::DidUpdateResults
};
send_notification(SEARCH_ID, ty)
send_notification(SEARCH_ID, ty, notification.channel.clone())
.payload(notification)
.send();
},
@ -48,6 +48,16 @@ impl SearchResultReceiverRunner {
}
#[tracing::instrument(level = "trace")]
pub fn send_notification(id: &str, ty: SearchNotification) -> NotificationBuilder {
NotificationBuilder::new(id, ty, SEARCH_OBSERVABLE_SOURCE)
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)
}