@@ -25,6 +25,7 @@ use pizza_engine::store::{DiskStore, DiskStoreSnapshot};
2525use pizza_engine:: writer:: Writer ;
2626use pizza_engine:: { doc, Engine , EngineBuilder } ;
2727use serde_json:: Value as Json ;
28+ use std:: path:: Path ;
2829use std:: path:: PathBuf ;
2930use tauri:: { async_runtime, AppHandle , Manager , Runtime } ;
3031use tauri_plugin_fs_pro:: { icon, metadata, name, IconOptions } ;
@@ -47,6 +48,8 @@ const TAURI_STORE_APP_ALIAS: &str = "app_alias";
4748const TAURI_STORE_KEY_SEARCH_PATH : & str = "search_path" ;
4849const TAURI_STORE_KEY_DISABLED_APP_LIST : & str = "disabled_app_list" ;
4950
51+ const INDEX_DIR : & str = "local_application_index" ;
52+
5053/// We use this as:
5154///
5255/// 1. querysource ID
@@ -209,6 +212,86 @@ impl SearchSourceState for ApplicationSearchSourceState {
209212 }
210213}
211214
215+ /// Index applications if they have not been indexed (by checking if `app_index_dir` exists).
216+ async fn index_applications_if_not_indexed < R : Runtime > (
217+ tauri_app_handle : & AppHandle < R > ,
218+ app_index_dir : & Path ,
219+ ) -> anyhow:: Result < ApplicationSearchSourceState > {
220+ let index_exists = app_index_dir. exists ( ) ;
221+
222+ let mut pizza_engine_builder = EngineBuilder :: new ( ) ;
223+ let disk_store = DiskStore :: new ( & app_index_dir) ?;
224+ pizza_engine_builder. set_data_store ( disk_store) ;
225+
226+ let mut schema = Schema :: new ( ) ;
227+ let field_app_name = Property :: builder ( FieldType :: Text ) . build ( ) ;
228+ schema
229+ . add_property ( FIELD_APP_NAME , field_app_name)
230+ . expect ( "no collision could happen" ) ;
231+ let property_icon = Property :: builder ( FieldType :: Text ) . index ( false ) . build ( ) ;
232+ schema
233+ . add_property ( FIELD_ICON_PATH , property_icon)
234+ . expect ( "no collision could happen" ) ;
235+ schema
236+ . add_property ( FIELD_APP_ALIAS , Property :: as_text ( None ) )
237+ . expect ( "no collision could happen" ) ;
238+ schema. freeze ( ) ;
239+ pizza_engine_builder. set_schema ( schema) ;
240+
241+ let pizza_engine = pizza_engine_builder
242+ . build ( )
243+ . unwrap_or_else ( |e| panic ! ( "failed to build Pizza engine due to [{}]" , e) ) ;
244+ pizza_engine. start ( ) ;
245+ let mut writer = pizza_engine. acquire_writer ( ) ;
246+
247+ if !index_exists {
248+ let default_search_path = get_default_search_paths ( ) ;
249+ let apps = list_app_in ( default_search_path) . map_err ( |str| anyhow:: anyhow!( str ) ) ?;
250+
251+ for app in apps. iter ( ) {
252+ let app_path = get_app_path ( app) ;
253+ let app_name = get_app_name ( app) . await ;
254+ let app_icon_path = get_app_icon_path ( & tauri_app_handle, app)
255+ . await
256+ . map_err ( |str| anyhow:: anyhow!( str ) ) ?;
257+ let app_alias = get_app_alias ( & tauri_app_handle, & app_path) . unwrap_or ( String :: new ( ) ) ;
258+
259+ if app_name. is_empty ( ) || app_name. eq ( & tauri_app_handle. package_info ( ) . name ) {
260+ continue ;
261+ }
262+
263+ // You cannot write `app_name.clone()` within the `doc!()` macro, we should fix this.
264+ let app_name_clone = app_name. clone ( ) ;
265+ let app_path_clone = app_path. clone ( ) ;
266+ let document = doc ! ( app_path_clone, {
267+ FIELD_APP_NAME => app_name_clone,
268+ FIELD_ICON_PATH => app_icon_path,
269+ FIELD_APP_ALIAS => app_alias,
270+ }
271+ ) ;
272+
273+ // We don't error out because one failure won't break the whole thing
274+ if let Err ( e) = writer. create_document ( document) . await {
275+ warn ! (
276+ "failed to index application [app name: '{}', app path: '{}'] due to error [{}]" , app_name, app_path, e
277+ )
278+ }
279+ }
280+
281+ writer. commit ( ) ?;
282+ }
283+
284+ let snapshot = pizza_engine. create_snapshot ( ) ;
285+ let searcher = pizza_engine. acquire_searcher ( ) ;
286+
287+ Ok ( ApplicationSearchSourceState {
288+ searcher,
289+ snapshot,
290+ engine : pizza_engine,
291+ writer,
292+ } )
293+ }
294+
212295/// Upon application start, index all the applications found in the `get_default_search_paths()`.
213296struct IndexAllApplicationsTask < R : Runtime > {
214297 tauri_app_handle : AppHandle < R > ,
@@ -228,87 +311,47 @@ impl<R: Runtime> Task for IndexAllApplicationsTask<R> {
228311 . path ( )
229312 . app_data_dir ( )
230313 . expect ( "failed to find the local dir" ) ;
231- app_index_dir. push ( "local_application_index" ) ;
232-
233- let index_exists = app_index_dir. exists ( ) ;
234-
235- let mut pizza_engine_builder = EngineBuilder :: new ( ) ;
236- let disk_store = task_exec_try ! ( DiskStore :: new( & app_index_dir) , callback) ;
237- pizza_engine_builder. set_data_store ( disk_store) ;
238-
239- let mut schema = Schema :: new ( ) ;
240- let field_app_name = Property :: builder ( FieldType :: Text ) . build ( ) ;
241- schema
242- . add_property ( FIELD_APP_NAME , field_app_name)
243- . expect ( "no collision could happen" ) ;
244- let property_icon = Property :: builder ( FieldType :: Text ) . index ( false ) . build ( ) ;
245- schema
246- . add_property ( FIELD_ICON_PATH , property_icon)
247- . expect ( "no collision could happen" ) ;
248- schema
249- . add_property ( FIELD_APP_ALIAS , Property :: as_text ( None ) )
250- . expect ( "no collision could happen" ) ;
251- schema. freeze ( ) ;
252- pizza_engine_builder. set_schema ( schema) ;
253-
254- let pizza_engine = pizza_engine_builder
255- . build ( )
256- . unwrap_or_else ( |e| panic ! ( "failed to build Pizza engine due to [{}]" , e) ) ;
257- pizza_engine. start ( ) ;
258- let mut writer = pizza_engine. acquire_writer ( ) ;
259-
260- if !index_exists {
261- let default_search_path = get_default_search_paths ( ) ;
262- let apps = task_exec_try ! ( list_app_in( default_search_path) , callback) ;
263-
264- for app in apps. iter ( ) {
265- let app_path = get_app_path ( app) ;
266- let app_name = get_app_name ( app) . await ;
267- let app_icon_path = task_exec_try ! (
268- get_app_icon_path( & self . tauri_app_handle, app) . await ,
269- callback
270- ) ;
271- let app_alias =
272- get_app_alias ( & self . tauri_app_handle , & app_path) . unwrap_or ( String :: new ( ) ) ;
273-
274- if app_name. is_empty ( ) || app_name. eq ( & self . tauri_app_handle . package_info ( ) . name ) {
275- continue ;
276- }
277-
278- // You cannot write `app_name.clone()` within the `doc!()` macro, we should fix this.
279- let app_name_clone = app_name. clone ( ) ;
280- let app_path_clone = app_path. clone ( ) ;
281- let document = doc ! ( app_path_clone, {
282- FIELD_APP_NAME => app_name_clone,
283- FIELD_ICON_PATH => app_icon_path,
284- FIELD_APP_ALIAS => app_alias,
285- }
286- ) ;
287-
288- // We don't error out because one failure won't break the whole thing
289- if let Err ( e) = writer. create_document ( document) . await {
290- warn ! (
291- "failed to index application [app name: '{}', app path: '{}'] due to error [{}]" , app_name, app_path, e
292- )
293- }
294- }
314+ app_index_dir. push ( INDEX_DIR ) ;
315+ let app_search_source_state = task_exec_try ! (
316+ index_applications_if_not_indexed( & self . tauri_app_handle, & app_index_dir) . await ,
317+ callback
318+ ) ;
319+ * state = Some ( Box :: new ( app_search_source_state) ) ;
320+ callback. send ( Ok ( ( ) ) ) . expect ( "rx dropped" ) ;
321+ }
322+ }
295323
296- task_exec_try ! ( writer. commit( ) , callback) ;
297- }
324+ struct ReindexAllApplicationsTask < R : Runtime > {
325+ tauri_app_handle : AppHandle < R > ,
326+ callback : Option < tokio:: sync:: oneshot:: Sender < Result < ( ) , String > > > ,
327+ }
298328
299- let snapshot = pizza_engine. create_snapshot ( ) ;
300- let searcher = pizza_engine. acquire_searcher ( ) ;
329+ #[ async_trait:: async_trait( ?Send ) ]
330+ impl < R : Runtime > Task for ReindexAllApplicationsTask < R > {
331+ fn search_source_id ( & self ) -> & ' static str {
332+ APPLICATION_SEARCH_SOURCE_ID
333+ }
301334
302- let state_to_store = Box :: new ( ApplicationSearchSourceState {
303- searcher,
304- snapshot,
305- engine : pizza_engine,
306- writer,
307- } ) as Box < dyn SearchSourceState > ;
335+ async fn exec ( & mut self , state : & mut Option < Box < dyn SearchSourceState > > ) {
336+ let callback = self . callback . take ( ) . unwrap ( ) ;
308337
309- * state = Some ( state_to_store) ;
338+ // Clear the state
339+ * state = None ;
340+ let mut app_index_dir = self
341+ . tauri_app_handle
342+ . path ( )
343+ . app_data_dir ( )
344+ . expect ( "failed to find the local dir" ) ;
345+ app_index_dir. push ( INDEX_DIR ) ;
346+ task_exec_try ! ( tokio:: fs:: remove_dir_all( & app_index_dir) . await , callback) ;
310347
311- callback. send ( Ok ( ( ) ) ) . unwrap ( ) ;
348+ // Then re-index the apps
349+ let app_search_source_state = task_exec_try ! (
350+ index_applications_if_not_indexed( & self . tauri_app_handle, & app_index_dir) . await ,
351+ callback
352+ ) ;
353+ * state = Some ( Box :: new ( app_search_source_state) ) ;
354+ callback. send ( Ok ( ( ) ) ) . expect ( "rx dropped" ) ;
312355 }
313356}
314357
@@ -326,6 +369,23 @@ impl<R: Runtime> Task for SearchApplicationsTask<R> {
326369
327370 async fn exec ( & mut self , state : & mut Option < Box < dyn SearchSourceState > > ) {
328371 let callback = self . callback . take ( ) . unwrap ( ) ;
372+
373+ let Some ( state) = state. as_mut ( ) else {
374+ let empty_hits = SearchResult {
375+ tracing_id : String :: new ( ) ,
376+ explains : None ,
377+ total_hits : 0 ,
378+ hits : None ,
379+ } ;
380+
381+ let rx_dropped_error = callback. send ( Ok ( empty_hits) ) . is_err ( ) ;
382+ if rx_dropped_error {
383+ warn ! ( "failed to send local app search result back because the corresponding channel receiver end has been unexpected dropped, which could happen due to a low query timeout" )
384+ }
385+
386+ return ;
387+ } ;
388+
329389 let disabled_app_list = get_disabled_app_list ( & self . tauri_app_handle ) ;
330390
331391 // TODO: search via alias, implement this when Pizza engine supports update
@@ -344,8 +404,6 @@ impl<R: Runtime> Task for SearchApplicationsTask<R> {
344404 "{{ \" query\" : {{ \" bool\" : {{ \" should\" : [ {{ \" match\" : {{ \" {FIELD_APP_NAME}\" : {:?} }} }}, {{ \" prefix\" : {{ \" {FIELD_APP_NAME}\" : {:?} }} }} ] }} }} }}" , self . query_string, self . query_string) ;
345405
346406 let state = state
347- . as_mut ( )
348- . expect ( "should be set before" )
349407 . as_mut_any ( )
350408 . downcast_mut :: < ApplicationSearchSourceState > ( )
351409 . unwrap ( ) ;
@@ -1105,3 +1163,30 @@ pub async fn get_app_metadata(app_name: String, app_path: String) -> Result<AppM
11051163 last_opened,
11061164 } )
11071165}
1166+
1167+ #[ tauri:: command]
1168+ pub async fn reindex_applications < R : Runtime > (
1169+ tauri_app_handle : AppHandle < R > ,
1170+ ) -> Result < ( ) , String > {
1171+ let ( tx, rx) = tokio:: sync:: oneshot:: channel ( ) ;
1172+ let reindex_applications_task = ReindexAllApplicationsTask {
1173+ tauri_app_handle : tauri_app_handle. clone ( ) ,
1174+ callback : Some ( tx) ,
1175+ } ;
1176+
1177+ RUNTIME_TX
1178+ . get ( )
1179+ . unwrap ( )
1180+ . send ( Box :: new ( reindex_applications_task) )
1181+ . unwrap ( ) ;
1182+
1183+ let reindexing_applications_result = rx. await . unwrap ( ) ;
1184+ if let Err ( ref e) = reindexing_applications_result {
1185+ error ! (
1186+ "re-indexing local applications failed, app search won't work, error [{}]" ,
1187+ e
1188+ )
1189+ }
1190+
1191+ reindexing_applications_result
1192+ }
0 commit comments