diff options
-rw-r--r-- | base/src/database.rs | 15 | ||||
-rw-r--r-- | import/src/db.rs | 14 | ||||
-rw-r--r-- | server/src/routes/ui/error.rs | 5 | ||||
-rw-r--r-- | server/src/routes/ui/layout.rs | 2 | ||||
-rw-r--r-- | server/src/routes/ui/search.rs | 100 |
5 files changed, 77 insertions, 59 deletions
diff --git a/base/src/database.rs b/base/src/database.rs index 2fabf1c..3971c62 100644 --- a/base/src/database.rs +++ b/base/src/database.rs @@ -33,15 +33,16 @@ pub const T_NODE_IMPORT: TableDefinition<&str, Ser<Vec<(Vec<usize>, Node)>>> = pub struct DataAcid { pub inner: redb::Database, - pub node_index: NodeFulltextIndex, + pub node_index: NodeIndex, } impl DataAcid { pub fn open(path: &Path) -> Result<Self, anyhow::Error> { - info!("database"); - create_dir_all(path)?; - let db = redb::Database::create(path.join("data"))?; - let ft_node = NodeFulltextIndex::new(path)?; + create_dir_all(path).context("creating database directory")?; + info!("opening kv store..."); + let db = redb::Database::create(path.join("data")).context("opening kv store")?; + info!("opening node index..."); + let ft_node = NodeIndex::new(path).context("in node index")?; let r = Self { inner: db, node_index: ft_node, @@ -71,7 +72,7 @@ impl Deref for DataAcid { } } -pub struct NodeFulltextIndex { +pub struct NodeIndex { pub schema: Schema, pub reader: IndexReader, pub writer: RwLock<IndexWriter>, @@ -82,7 +83,7 @@ pub struct NodeFulltextIndex { pub description: Field, pub f_index: Field, } -impl NodeFulltextIndex { +impl NodeIndex { fn new(path: &Path) -> anyhow::Result<Self> { let mut schema = Schema::builder(); let id = schema.add_text_field("id", TEXT | STORED | FAST); diff --git a/import/src/db.rs b/import/src/db.rs index 4c62681..49c2f0e 100644 --- a/import/src/db.rs +++ b/import/src/db.rs @@ -1,11 +1,11 @@ -use std::collections::HashMap; - -use anyhow::anyhow; +use anyhow::{anyhow, Context}; use jellybase::database::{ - doc, DataAcid, ReadableTable, Ser, T_NODE, T_NODE_EXTENDED, T_NODE_IMPORT, + tantivy::{doc, DateTime}, + DataAcid, ReadableTable, Ser, T_NODE, T_NODE_EXTENDED, T_NODE_IMPORT, }; use jellycommon::{ExtendedNode, Node}; use log::info; +use std::collections::HashMap; use std::sync::RwLock; pub(crate) trait ImportStorage: Sync { @@ -150,9 +150,11 @@ impl ImportStorage for MemoryStorage<'_> { self.db.node_index.id => node.public.id.unwrap_or_default(), self.db.node_index.title => node.public.title.unwrap_or_default(), self.db.node_index.description => node.public.description.unwrap_or_default(), - self.db.node_index.releasedate => node.public.release_date.unwrap_or_default(), + self.db.node_index.releasedate => DateTime::from_timestamp_millis(node.public.release_date.unwrap_or_default()), self.db.node_index.f_index => node.public.index.unwrap_or_default() as u64, - ))?; + )) + .context("inserting document")?; + Ok(()) } diff --git a/server/src/routes/ui/error.rs b/server/src/routes/ui/error.rs index 41a2de9..02011fc 100644 --- a/server/src/routes/ui/error.rs +++ b/server/src/routes/ui/error.rs @@ -139,3 +139,8 @@ impl From<jellybase::database::TransactionError> for MyError { MyError(anyhow::anyhow!("database oopsie during transaction: {err}")) } } +impl From<jellybase::database::tantivy::TantivyError> for MyError { + fn from(err: jellybase::database::tantivy::TantivyError) -> Self { + MyError(anyhow::anyhow!("database during search: {err}")) + } +} diff --git a/server/src/routes/ui/layout.rs b/server/src/routes/ui/layout.rs index 07fc70c..1696ac4 100644 --- a/server/src/routes/ui/layout.rs +++ b/server/src/routes/ui/layout.rs @@ -46,7 +46,7 @@ markup::define! { @if let Some(_) = session { a.library[href=uri!(r_library_node("library"))] { "My Library" } " " a.library[href=uri!(r_all_items())] { "All Items" } " " - a.library[href=uri!(r_search(None::<&'static str>))] { "Search" } " " + a.library[href=uri!(r_search(None::<&'static str>, None::<usize>))] { "Search" } " " } div.account { @if let Some(session) = session { diff --git a/server/src/routes/ui/search.rs b/server/src/routes/ui/search.rs index cafa755..f87a13b 100644 --- a/server/src/routes/ui/search.rs +++ b/server/src/routes/ui/search.rs @@ -4,60 +4,68 @@ use super::{ layout::{DynLayoutPage, LayoutPage}, node::NodeCard, }; -use edit_distance::edit_distance; -use jellybase::database::{ - tantivy::query::QueryParser, DataAcid, ReadableTable, T_NODE, T_USER_NODE, +use anyhow::{anyhow, Context}; +use jellybase::{ + database::{ + tantivy::{ + collector::{Count, TopDocs}, + query::QueryParser, + schema::Value, + TantivyDocument, + }, + DataAcid, TableExt, T_NODE, T_USER_NODE, + }, + permission::NodePermissionExt, }; use rocket::{get, State}; +use std::time::Instant; -#[get("/search?<query>")] +#[get("/search?<query>&<page>")] pub async fn r_search<'a>( session: Session, db: &State<DataAcid>, query: Option<&str>, + page: Option<usize>, ) -> MyResult<DynLayoutPage<'a>> { - // let results = if let Some(query) = query { - // let mut items = { - // let txn = db.begin_read()?; - // let nodes = txn.open_table(T_NODE)?; - // let node_users = txn.open_table(T_USER_NODE)?; - // let i = nodes - // .iter()? - // .map(|a| { - // let (x, y) = a.unwrap(); - // let (x, y) = (x.value().to_owned(), y.value().0); - // let z = node_users - // .get(&(session.user.name.as_str(), x.as_str())) - // .unwrap() - // .map(|z| z.value().0) - // .unwrap_or_default(); - // let y = y.public; - // (x, y, z) - // }) - // .collect::<Vec<_>>(); - // drop(nodes); - // i - // }; - // let query = query.to_lowercase(); - // items.sort_by_cached_key(|(_, n, _)| { - // n.title - // .as_ref() - // .map(|x| x.to_lowercase()) - // .unwrap_or_default() - // .split(" ") - // .map(|tok| edit_distance(query.as_str(), tok)) - // .min() - // .unwrap_or(usize::MAX) - // }); - // Some(items.into_iter().take(64).collect::<Vec<_>>()) - // } else { - // None - // }; + let timing = Instant::now(); + let results = if let Some(query) = query { + let query = QueryParser::for_index( + &db.node_index.index, + vec![db.node_index.title, db.node_index.description], + ) + .parse_query(query) + .context("parsing query")?; + + let searcher = db.node_index.reader.searcher(); + let sres = searcher.search( + &query, + &TopDocs::with_limit(32).and_offset(page.unwrap_or_default() * 32), + )?; + let scount = searcher.search(&query, &Count)?; + + let mut results = Vec::new(); + for (_, daddr) in sres { + let doc: TantivyDocument = searcher.doc(daddr)?; + let id = doc.get_first(db.node_index.id).unwrap().as_str().unwrap(); + + let node = T_NODE + .get(&db, id)? + .only_if_permitted(&session.user.permissions) + .ok_or(anyhow!("node does not exist"))? + .public; + let udata = T_USER_NODE + .get(&db, &(session.user.name.as_str(), id))? + .unwrap_or_default(); - let query = QueryParser::for_index(index, vec![]); + results.push((id.to_owned(), node, udata)); + } + Some((scount, results)) + } else { + None + }; + let search_dur = timing.elapsed(); - let searcher = db.node_index.reader.searcher(); - searcher.Ok(LayoutPage { + Ok(LayoutPage { title: "Search".to_string(), class: Some("search"), content: markup::new! { @@ -66,11 +74,13 @@ pub async fn r_search<'a>( input[type="text", name="query", placeholder="Search Term"]; input[type="submit", value="Search"]; } - @if let Some(results) = &results { + @if let Some((count, results)) = &results { h2 { "Results" } + p.stats { @format!("Found {count} nodes in {search_dur:?}.") } ul.children {@for (id, node, udata) in results.iter() { li { @NodeCard { id, node, udata } } }} + // TODO pagination } }, }) |