diff --git a/Cargo.lock b/Cargo.lock index 5da171cc2e..081b438d9c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4875,6 +4875,7 @@ dependencies = [ "tauri-plugin-single-instance", "tauri-plugin-store", "tauri-plugin-store2", + "tauri-plugin-tantivy", "tauri-plugin-template", "tauri-plugin-tracing", "tauri-plugin-tray", @@ -19243,14 +19244,18 @@ name = "tauri-plugin-tantivy" version = "0.1.0" dependencies = [ "serde", + "serde_json", "specta", "specta-typescript", "tantivy", "tauri", "tauri-plugin", + "tauri-plugin-path2", "tauri-specta", + "tempfile", "thiserror 2.0.17", "tokio", + "tracing", ] [[package]] diff --git a/plugins/tantivy/Cargo.toml b/plugins/tantivy/Cargo.toml index 38005955c8..c0c4d8208d 100644 --- a/plugins/tantivy/Cargo.toml +++ b/plugins/tantivy/Cargo.toml @@ -5,22 +5,27 @@ authors = ["You"] edition = "2024" exclude = ["/js", "/node_modules"] links = "tauri-plugin-tantivy" -description = "" +description = "Full-text search plugin using Tantivy" [build-dependencies] tauri-plugin = { workspace = true, features = ["build"] } [dev-dependencies] specta-typescript = { workspace = true } +tempfile = { workspace = true } tokio = { workspace = true, features = ["macros"] } [dependencies] tantivy = "0.22" tauri = { workspace = true, features = ["test"] } +tauri-plugin-path2 = { workspace = true } tauri-specta = { workspace = true, features = ["derive", "typescript"] } serde = { workspace = true } +serde_json = { workspace = true } specta = { workspace = true } thiserror = { workspace = true } +tokio = { workspace = true } +tracing = { workspace = true } diff --git a/plugins/tantivy/js/bindings.gen.ts b/plugins/tantivy/js/bindings.gen.ts index 1c87ef3e0f..eb9cc267e8 100644 --- a/plugins/tantivy/js/bindings.gen.ts +++ b/plugins/tantivy/js/bindings.gen.ts @@ -1,77 +1,165 @@ // @ts-nocheck + +// This file was generated by [tauri-specta](https://github.com/oscartbeaumont/tauri-specta). Do not edit this file manually. + +/** user-defined commands **/ + + +export const commands = { +async ping() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("plugin:tantivy|ping") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async init() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("plugin:tantivy|init") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async addDocument(doc: SearchDocument) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("plugin:tantivy|add_document", { doc }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async updateDocument(doc: SearchDocument) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("plugin:tantivy|update_document", { doc }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async deleteDocument(id: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("plugin:tantivy|delete_document", { id }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async commit() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("plugin:tantivy|commit") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async search(query: string, filters: SearchFilters | null, limit: number | null) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("plugin:tantivy|search", { query, filters, limit }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async searchFuzzy(query: string, filters: SearchFilters | null, limit: number | null, distance: number | null) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("plugin:tantivy|search_fuzzy", { query, filters, limit, distance }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async clear() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("plugin:tantivy|clear") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async count() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("plugin:tantivy|count") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +} +} + /** user-defined events **/ + + + /** user-defined constants **/ + + + /** user-defined types **/ + +export type CreatedAtFilter = { gte: number | null; lte: number | null; gt: number | null; lt: number | null; eq: number | null } +export type SearchDocument = { id: string; doc_type: string; title: string; content: string; created_at: number } +export type SearchFilters = { created_at: CreatedAtFilter | null } +export type SearchHit = { score: number; document: SearchDocument } +export type SearchResult = { hits: SearchHit[] } + /** tauri-specta globals **/ + import { - Channel as TAURI_CHANNEL, - invoke as TAURI_INVOKE, + invoke as TAURI_INVOKE, + Channel as TAURI_CHANNEL, } from "@tauri-apps/api/core"; import * as TAURI_API_EVENT from "@tauri-apps/api/event"; import { type WebviewWindow as __WebviewWindow__ } from "@tauri-apps/api/webviewWindow"; -// This file was generated by [tauri-specta](https://github.com/oscartbeaumont/tauri-specta). Do not edit this file manually. - -/** user-defined commands **/ - -export const commands = { - async ping(): Promise> { - try { - return { status: "ok", data: await TAURI_INVOKE("plugin:tantivy|ping") }; - } catch (e) { - if (e instanceof Error) throw e; - else return { status: "error", error: e as any }; - } - }, -}; - type __EventObj__ = { - listen: ( - cb: TAURI_API_EVENT.EventCallback, - ) => ReturnType>; - once: ( - cb: TAURI_API_EVENT.EventCallback, - ) => ReturnType>; - emit: null extends T - ? (payload?: T) => ReturnType - : (payload: T) => ReturnType; + listen: ( + cb: TAURI_API_EVENT.EventCallback, + ) => ReturnType>; + once: ( + cb: TAURI_API_EVENT.EventCallback, + ) => ReturnType>; + emit: null extends T + ? (payload?: T) => ReturnType + : (payload: T) => ReturnType; }; export type Result = - | { status: "ok"; data: T } - | { status: "error"; error: E }; + | { status: "ok"; data: T } + | { status: "error"; error: E }; function __makeEvents__>( - mappings: Record, + mappings: Record, ) { - return new Proxy( - {} as unknown as { - [K in keyof T]: __EventObj__ & { - (handle: __WebviewWindow__): __EventObj__; - }; - }, - { - get: (_, event) => { - const name = mappings[event as keyof T]; - - return new Proxy((() => {}) as any, { - apply: (_, __, [window]: [__WebviewWindow__]) => ({ - listen: (arg: any) => window.listen(name, arg), - once: (arg: any) => window.once(name, arg), - emit: (arg: any) => window.emit(name, arg), - }), - get: (_, command: keyof __EventObj__) => { - switch (command) { - case "listen": - return (arg: any) => TAURI_API_EVENT.listen(name, arg); - case "once": - return (arg: any) => TAURI_API_EVENT.once(name, arg); - case "emit": - return (arg: any) => TAURI_API_EVENT.emit(name, arg); - } - }, - }); - }, - }, - ); + return new Proxy( + {} as unknown as { + [K in keyof T]: __EventObj__ & { + (handle: __WebviewWindow__): __EventObj__; + }; + }, + { + get: (_, event) => { + const name = mappings[event as keyof T]; + + return new Proxy((() => {}) as any, { + apply: (_, __, [window]: [__WebviewWindow__]) => ({ + listen: (arg: any) => window.listen(name, arg), + once: (arg: any) => window.once(name, arg), + emit: (arg: any) => window.emit(name, arg), + }), + get: (_, command: keyof __EventObj__) => { + switch (command) { + case "listen": + return (arg: any) => TAURI_API_EVENT.listen(name, arg); + case "once": + return (arg: any) => TAURI_API_EVENT.once(name, arg); + case "emit": + return (arg: any) => TAURI_API_EVENT.emit(name, arg); + } + }, + }); + }, + }, + ); } diff --git a/plugins/tantivy/src/commands.rs b/plugins/tantivy/src/commands.rs index 7af8de6bcf..776d3cf302 100644 --- a/plugins/tantivy/src/commands.rs +++ b/plugins/tantivy/src/commands.rs @@ -1,7 +1,96 @@ -use crate::TantivyPluginExt; +use crate::{SearchDocument, SearchFilters, SearchResult, TantivyPluginExt}; #[tauri::command] #[specta::specta] pub(crate) async fn ping(app: tauri::AppHandle) -> Result<(), String> { app.tantivy().ping().map_err(|e| e.to_string()) } + +#[tauri::command] +#[specta::specta] +pub(crate) async fn init(app: tauri::AppHandle) -> Result<(), String> { + app.tantivy().init().await.map_err(|e| e.to_string()) +} + +#[tauri::command] +#[specta::specta] +pub(crate) async fn add_document( + app: tauri::AppHandle, + doc: SearchDocument, +) -> Result<(), String> { + app.tantivy() + .add_document(doc) + .await + .map_err(|e| e.to_string()) +} + +#[tauri::command] +#[specta::specta] +pub(crate) async fn update_document( + app: tauri::AppHandle, + doc: SearchDocument, +) -> Result<(), String> { + app.tantivy() + .update_document(doc) + .await + .map_err(|e| e.to_string()) +} + +#[tauri::command] +#[specta::specta] +pub(crate) async fn delete_document( + app: tauri::AppHandle, + id: String, +) -> Result<(), String> { + app.tantivy() + .delete_document(id) + .await + .map_err(|e| e.to_string()) +} + +#[tauri::command] +#[specta::specta] +pub(crate) async fn commit(app: tauri::AppHandle) -> Result<(), String> { + app.tantivy().commit().await.map_err(|e| e.to_string()) +} + +#[tauri::command] +#[specta::specta] +pub(crate) async fn search( + app: tauri::AppHandle, + query: String, + filters: Option, + limit: Option, +) -> Result { + app.tantivy() + .search(query, filters, limit.unwrap_or(100)) + .await + .map_err(|e| e.to_string()) +} + +#[tauri::command] +#[specta::specta] +pub(crate) async fn search_fuzzy( + app: tauri::AppHandle, + query: String, + filters: Option, + limit: Option, + distance: Option, +) -> Result { + app.tantivy() + .search_fuzzy(query, filters, limit.unwrap_or(100), distance.unwrap_or(1)) + .await + .map_err(|e| e.to_string()) +} + +#[tauri::command] +#[specta::specta] +pub(crate) async fn clear(app: tauri::AppHandle) -> Result<(), String> { + app.tantivy().clear().await.map_err(|e| e.to_string()) +} + +#[tauri::command] +#[specta::specta] +pub(crate) async fn count(app: tauri::AppHandle) -> Result { + app.tantivy().count().await.map_err(|e| e.to_string()) +} diff --git a/plugins/tantivy/src/error.rs b/plugins/tantivy/src/error.rs index 53206e2ee5..d70733437c 100644 --- a/plugins/tantivy/src/error.rs +++ b/plugins/tantivy/src/error.rs @@ -6,6 +6,20 @@ pub type Result = std::result::Result; pub enum Error { #[error(transparent)] Io(#[from] std::io::Error), + #[error(transparent)] + Tantivy(#[from] tantivy::TantivyError), + #[error(transparent)] + QueryParser(#[from] tantivy::query::QueryParserError), + #[error(transparent)] + Tauri(#[from] tauri::Error), + #[error(transparent)] + Path2(#[from] tauri_plugin_path2::Error), + #[error("Index not initialized")] + IndexNotInitialized, + #[error("Document not found: {0}")] + DocumentNotFound(String), + #[error("Invalid document type: {0}")] + InvalidDocumentType(String), } impl Serialize for Error { diff --git a/plugins/tantivy/src/ext.rs b/plugins/tantivy/src/ext.rs index a05a2f76f2..ce7834d6bd 100644 --- a/plugins/tantivy/src/ext.rs +++ b/plugins/tantivy/src/ext.rs @@ -1,5 +1,13 @@ +use tantivy::collector::TopDocs; +use tantivy::query::{BooleanQuery, FuzzyTermQuery, Occur, Query, QueryParser, RangeQuery}; +use tantivy::schema::{FAST, Field, STORED, STRING, Schema, TEXT, Value}; +use tantivy::{Index, ReloadPolicy, TantivyDocument, Term}; +use tauri_plugin_path2::Path2PluginExt; + +use crate::{IndexState, SearchDocument, SearchFilters, SearchHit, SearchResult}; + pub struct Tantivy<'a, R: tauri::Runtime, M: tauri::Manager> { - _manager: &'a M, + manager: &'a M, _runtime: std::marker::PhantomData R>, } @@ -7,6 +15,268 @@ impl<'a, R: tauri::Runtime, M: tauri::Manager> Tantivy<'a, R, M> { pub fn ping(&self) -> Result<(), crate::Error> { Ok(()) } + + pub async fn init(&self) -> Result<(), crate::Error> { + let base = self.manager.app_handle().path2().base()?; + let index_path = base.join("search_index"); + + std::fs::create_dir_all(&index_path)?; + + let state = self.manager.state::(); + let mut guard = state.inner.lock().await; + + if guard.index.is_some() { + return Ok(()); + } + + let schema = build_schema(); + let index = if index_path.join("meta.json").exists() { + Index::open_in_dir(&index_path)? + } else { + Index::create_in_dir(&index_path, schema.clone())? + }; + + let reader = index + .reader_builder() + .reload_policy(ReloadPolicy::OnCommitWithDelay) + .try_into()?; + + let writer = index.writer(50_000_000)?; + + guard.schema = Some(schema); + guard.index = Some(index); + guard.reader = Some(reader); + guard.writer = Some(writer); + + tracing::info!("Tantivy search index initialized at {:?}", index_path); + Ok(()) + } + + pub async fn add_document(&self, doc: SearchDocument) -> Result<(), crate::Error> { + let state = self.manager.state::(); + let mut guard = state.inner.lock().await; + + let schema = guard + .schema + .clone() + .ok_or(crate::Error::IndexNotInitialized)?; + let writer = guard + .writer + .as_mut() + .ok_or(crate::Error::IndexNotInitialized)?; + + let fields = get_fields(&schema); + let tantivy_doc = create_tantivy_document(&schema, &fields, &doc)?; + + writer.add_document(tantivy_doc)?; + Ok(()) + } + + pub async fn update_document(&self, doc: SearchDocument) -> Result<(), crate::Error> { + let state = self.manager.state::(); + let mut guard = state.inner.lock().await; + + let schema = guard + .schema + .clone() + .ok_or(crate::Error::IndexNotInitialized)?; + let writer = guard + .writer + .as_mut() + .ok_or(crate::Error::IndexNotInitialized)?; + + let fields = get_fields(&schema); + let id_term = Term::from_field_text(fields.id, &doc.id); + + writer.delete_term(id_term); + + let tantivy_doc = create_tantivy_document(&schema, &fields, &doc)?; + writer.add_document(tantivy_doc)?; + Ok(()) + } + + pub async fn delete_document(&self, id: String) -> Result<(), crate::Error> { + let state = self.manager.state::(); + let mut guard = state.inner.lock().await; + + let schema = guard + .schema + .clone() + .ok_or(crate::Error::IndexNotInitialized)?; + let writer = guard + .writer + .as_mut() + .ok_or(crate::Error::IndexNotInitialized)?; + + let fields = get_fields(&schema); + let id_term = Term::from_field_text(fields.id, &id); + + writer.delete_term(id_term); + Ok(()) + } + + pub async fn commit(&self) -> Result<(), crate::Error> { + let state = self.manager.state::(); + let mut guard = state.inner.lock().await; + + let writer = guard + .writer + .as_mut() + .ok_or(crate::Error::IndexNotInitialized)?; + writer.commit()?; + Ok(()) + } + + pub async fn search( + &self, + query: String, + filters: Option, + limit: usize, + ) -> Result { + let state = self.manager.state::(); + let guard = state.inner.lock().await; + + let schema = guard + .schema + .as_ref() + .ok_or(crate::Error::IndexNotInitialized)?; + let index = guard + .index + .as_ref() + .ok_or(crate::Error::IndexNotInitialized)?; + let reader = guard + .reader + .as_ref() + .ok_or(crate::Error::IndexNotInitialized)?; + + let fields = get_fields(schema); + let searcher = reader.searcher(); + + let query_parser = QueryParser::for_index(index, vec![fields.title, fields.content]); + let mut parsed_query = query_parser.parse_query(&query)?; + + if let Some(ref filter) = filters { + if let Some(ref created_at_filter) = filter.created_at { + let range_query = + build_created_at_range_query(fields.created_at, created_at_filter); + if let Some(rq) = range_query { + parsed_query = Box::new(BooleanQuery::new(vec![ + (Occur::Must, parsed_query), + (Occur::Must, rq), + ])); + } + } + } + + let top_docs = searcher.search(&parsed_query, &TopDocs::with_limit(limit))?; + + let mut hits = Vec::new(); + for (score, doc_address) in top_docs { + let retrieved_doc: TantivyDocument = searcher.doc(doc_address)?; + + if let Some(search_doc) = extract_search_document(schema, &fields, &retrieved_doc) { + hits.push(SearchHit { + score, + document: search_doc, + }); + } + } + + Ok(SearchResult { hits }) + } + + pub async fn search_fuzzy( + &self, + query: String, + filters: Option, + limit: usize, + distance: u8, + ) -> Result { + let state = self.manager.state::(); + let guard = state.inner.lock().await; + + let schema = guard + .schema + .as_ref() + .ok_or(crate::Error::IndexNotInitialized)?; + let reader = guard + .reader + .as_ref() + .ok_or(crate::Error::IndexNotInitialized)?; + + let fields = get_fields(schema); + let searcher = reader.searcher(); + + let terms: Vec<&str> = query.split_whitespace().collect(); + let mut subqueries: Vec<(Occur, Box)> = Vec::new(); + + for term in terms { + let title_fuzzy = + FuzzyTermQuery::new(Term::from_field_text(fields.title, term), distance, true); + let content_fuzzy = + FuzzyTermQuery::new(Term::from_field_text(fields.content, term), distance, true); + + subqueries.push((Occur::Should, Box::new(title_fuzzy))); + subqueries.push((Occur::Should, Box::new(content_fuzzy))); + } + + let mut combined_query: Box = Box::new(BooleanQuery::new(subqueries)); + + if let Some(ref filter) = filters { + if let Some(ref created_at_filter) = filter.created_at { + let range_query = + build_created_at_range_query(fields.created_at, created_at_filter); + if let Some(rq) = range_query { + combined_query = Box::new(BooleanQuery::new(vec![ + (Occur::Must, combined_query), + (Occur::Must, rq), + ])); + } + } + } + + let top_docs = searcher.search(&combined_query, &TopDocs::with_limit(limit))?; + + let mut hits = Vec::new(); + for (score, doc_address) in top_docs { + let retrieved_doc: TantivyDocument = searcher.doc(doc_address)?; + + if let Some(search_doc) = extract_search_document(schema, &fields, &retrieved_doc) { + hits.push(SearchHit { + score, + document: search_doc, + }); + } + } + + Ok(SearchResult { hits }) + } + + pub async fn clear(&self) -> Result<(), crate::Error> { + let state = self.manager.state::(); + let mut guard = state.inner.lock().await; + + let writer = guard + .writer + .as_mut() + .ok_or(crate::Error::IndexNotInitialized)?; + writer.delete_all_documents()?; + writer.commit()?; + Ok(()) + } + + pub async fn count(&self) -> Result { + let state = self.manager.state::(); + let guard = state.inner.lock().await; + + let reader = guard + .reader + .as_ref() + .ok_or(crate::Error::IndexNotInitialized)?; + let searcher = reader.searcher(); + + Ok(searcher.num_docs()) + } } pub trait TantivyPluginExt { @@ -21,8 +291,92 @@ impl> TantivyPluginExt for T { Self: Sized, { Tantivy { - _manager: self, + manager: self, _runtime: std::marker::PhantomData, } } } + +fn build_schema() -> Schema { + let mut schema_builder = Schema::builder(); + schema_builder.add_text_field("id", STRING | STORED); + schema_builder.add_text_field("doc_type", STRING | STORED); + schema_builder.add_text_field("title", TEXT | STORED); + schema_builder.add_text_field("content", TEXT | STORED); + schema_builder.add_i64_field("created_at", FAST | STORED); + schema_builder.build() +} + +struct SchemaFields { + id: Field, + doc_type: Field, + title: Field, + content: Field, + created_at: Field, +} + +fn get_fields(schema: &Schema) -> SchemaFields { + SchemaFields { + id: schema.get_field("id").unwrap(), + doc_type: schema.get_field("doc_type").unwrap(), + title: schema.get_field("title").unwrap(), + content: schema.get_field("content").unwrap(), + created_at: schema.get_field("created_at").unwrap(), + } +} + +fn create_tantivy_document( + _schema: &Schema, + fields: &SchemaFields, + doc: &SearchDocument, +) -> Result { + let mut tantivy_doc = TantivyDocument::new(); + tantivy_doc.add_text(fields.id, &doc.id); + tantivy_doc.add_text(fields.doc_type, &doc.doc_type); + tantivy_doc.add_text(fields.title, &doc.title); + tantivy_doc.add_text(fields.content, &doc.content); + tantivy_doc.add_i64(fields.created_at, doc.created_at); + Ok(tantivy_doc) +} + +fn extract_search_document( + _schema: &Schema, + fields: &SchemaFields, + doc: &TantivyDocument, +) -> Option { + let id = doc.get_first(fields.id)?.as_str()?.to_string(); + let doc_type = doc.get_first(fields.doc_type)?.as_str()?.to_string(); + let title = doc.get_first(fields.title)?.as_str()?.to_string(); + let content = doc.get_first(fields.content)?.as_str()?.to_string(); + let created_at = doc.get_first(fields.created_at)?.as_i64()?; + + Some(SearchDocument { + id, + doc_type, + title, + content, + created_at, + }) +} + +fn build_created_at_range_query( + _field: Field, + filter: &crate::CreatedAtFilter, +) -> Option> { + let lower = filter.gte.or(filter.gt.map(|v| v.saturating_add(1))).unwrap_or(i64::MIN); + let upper = filter.lte.or(filter.lt.map(|v| v.saturating_sub(1))).unwrap_or(i64::MAX); + + if let Some(eq) = filter.eq { + Some(Box::new(RangeQuery::new_i64( + "created_at".to_string(), + eq..(eq + 1), + ))) + } else if lower != i64::MIN || upper != i64::MAX { + Some(Box::new(RangeQuery::new_i64( + "created_at".to_string(), + lower..(upper.saturating_add(1)), + ))) + } else { + None + } +} diff --git a/plugins/tantivy/src/lib.rs b/plugins/tantivy/src/lib.rs index 678b957f8a..8cc4b97570 100644 --- a/plugins/tantivy/src/lib.rs +++ b/plugins/tantivy/src/lib.rs @@ -2,16 +2,85 @@ mod commands; mod error; mod ext; +use serde::{Deserialize, Serialize}; +use tantivy::schema::Schema; +use tantivy::{Index, IndexReader, IndexWriter}; +use tauri::Manager; +use tokio::sync::Mutex; + pub use error::{Error, Result}; pub use ext::*; const PLUGIN_NAME: &str = "tantivy"; +#[derive(Debug, Clone, Serialize, Deserialize, specta::Type)] +pub struct SearchDocument { + pub id: String, + pub doc_type: String, + pub title: String, + pub content: String, + pub created_at: i64, +} + +#[derive(Debug, Clone, Serialize, Deserialize, specta::Type)] +pub struct SearchHit { + pub score: f32, + pub document: SearchDocument, +} + +#[derive(Debug, Clone, Serialize, Deserialize, specta::Type)] +pub struct SearchResult { + pub hits: Vec, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize, specta::Type)] +pub struct CreatedAtFilter { + pub gte: Option, + pub lte: Option, + pub gt: Option, + pub lt: Option, + pub eq: Option, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize, specta::Type)] +pub struct SearchFilters { + pub created_at: Option, +} + +#[derive(Default)] +pub struct IndexStateInner { + pub schema: Option, + pub index: Option, + pub reader: Option, + pub writer: Option, +} + +pub struct IndexState { + pub inner: Mutex, +} + +impl Default for IndexState { + fn default() -> Self { + Self { + inner: Mutex::new(IndexStateInner::default()), + } + } +} + fn make_specta_builder() -> tauri_specta::Builder { tauri_specta::Builder::::new() .plugin_name(PLUGIN_NAME) .commands(tauri_specta::collect_commands![ commands::ping::, + commands::init::, + commands::add_document::, + commands::update_document::, + commands::delete_document::, + commands::commit::, + commands::search::, + commands::search_fuzzy::, + commands::clear::, + commands::count::, ]) .error_handling(tauri_specta::ErrorHandlingMode::Result) } @@ -21,7 +90,10 @@ pub fn init() -> tauri::plugin::TauriPlugin { tauri::plugin::Builder::new(PLUGIN_NAME) .invoke_handler(specta_builder.invoke_handler()) - .setup(|_app, _api| Ok(())) + .setup(|app, _api| { + app.manage(IndexState::default()); + Ok(()) + }) .build() }