aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--database/src/kv/index.rs116
-rw-r--r--server/src/routes/mod.rs8
-rw-r--r--server/src/routes/search.rs76
3 files changed, 150 insertions, 50 deletions
diff --git a/database/src/kv/index.rs b/database/src/kv/index.rs
index 0881cb7..469a6e8 100644
--- a/database/src/kv/index.rs
+++ b/database/src/kv/index.rs
@@ -15,7 +15,7 @@ use crate::{
};
use anyhow::{Result, bail};
use jellyobject::Object;
-use std::iter::empty;
+use std::{collections::HashSet, iter::empty};
pub fn update_index(
txn: &mut dyn jellykv::Transaction,
@@ -31,11 +31,7 @@ pub fn update_index(
SortKey::None => {
for mut k in ks {
k.extend(row.to_be_bytes());
- if remove {
- txn.del(&k)?;
- } else {
- txn.set(&k, &[])?;
- }
+ index_marker(txn, &k, remove)?;
}
}
SortKey::Random => {
@@ -50,13 +46,7 @@ pub fn update_index(
}
SortKey::Count => {
for k in ks {
- let mut c = read_counter(txn, &k, 0)?;
- if remove {
- c -= 1;
- } else {
- c += 1;
- }
- write_counter(txn, &k, c)?;
+ index_counter(txn, &k, remove)?;
}
}
SortKey::Value(path, multi_behaviour) => {
@@ -65,19 +55,54 @@ pub fn update_index(
for mut k in ks {
k.extend(value);
k.extend(row.to_be_bytes());
- if remove {
- txn.del(&k)?;
- } else {
- txn.set(&k, &[])?;
- }
+ index_marker(txn, &k, remove)?;
+ }
+ }
+ }
+ SortKey::Text(path) => {
+ let mut tokens = HashSet::new();
+ for val in path.get_matching_values(ob) {
+ for tok in text_tokenizer(val) {
+ tokens.insert(tok);
+ }
+ }
+ for &tok in &tokens {
+ for mut k in ks.clone() {
+ k.push(0);
+ k.extend(tok);
+ index_counter(txn, &k, remove)?;
+ }
+ for mut k in ks.clone() {
+ k.push(1);
+ k.extend(tok);
+ k.extend(row.to_be_bytes());
+ index_marker(txn, &k, remove)?;
}
}
}
- SortKey::Text(_) => todo!(),
}
Ok(())
}
+pub fn index_counter(txn: &mut dyn jellykv::Transaction, k: &[u8], remove: bool) -> Result<()> {
+ let mut c = read_counter(txn, &k, 0)?;
+ if remove && c > 0 {
+ c -= 1;
+ } else {
+ c += 1;
+ }
+ write_counter(txn, &k, c)?;
+ Ok(())
+}
+
+pub fn index_marker(txn: &mut dyn jellykv::Transaction, k: &[u8], remove: bool) -> Result<()> {
+ if remove {
+ txn.del(&k)
+ } else {
+ txn.set(&k, &[])
+ }
+}
+
pub fn read_count_index(txn: &dyn jellykv::Transaction, prefix: Vec<u8>) -> Result<u64> {
read_counter(txn, &prefix, 0)
}
@@ -155,10 +180,61 @@ pub fn iter_index<'a>(
}),
)
}
- Sort::TextSearch(_, _) => todo!(),
+ Sort::TextSearch(_, text) => {
+ let search_tokens = text_tokenizer(text.as_bytes())
+ .map(|e| e.to_owned())
+ .collect::<Vec<_>>();
+ let mut min_tok = Vec::new();
+ let mut min_count = u64::MAX;
+ for token in &search_tokens {
+ let mut k = prefix.clone();
+ k.push(0);
+ k.extend(token);
+ let count = read_counter(txn, &k, 0)?;
+ if count < min_count {
+ min_count = count;
+ min_tok = token.to_owned()
+ }
+ }
+ let mut min_token_prefix = prefix.clone();
+ min_token_prefix.push(1);
+ min_token_prefix.extend(&min_tok);
+ Box::new(
+ PrefixIterator {
+ inner: txn.iter(&min_token_prefix, false)?,
+ prefix: min_token_prefix.into(),
+ }
+ .flat_map(move |k| {
+ let k = match k {
+ Ok(k) => k,
+ Err(e) => return Some(Err(e)),
+ };
+ let rn = RowNum::from_be_bytes(k[k.len() - 8..].try_into().unwrap());
+ for token in &search_tokens {
+ let mut k = prefix.clone();
+ k.push(1);
+ k.extend(token);
+ k.extend(rn.to_be_bytes());
+ let v = match txn.get(&k) {
+ Ok(v) => v,
+ Err(e) => return Some(Err(e)),
+ };
+ if v.is_none() {
+ return None;
+ }
+ }
+ Some(anyhow::Ok((rn, Vec::new())))
+ }),
+ )
+ }
})
}
+fn text_tokenizer(text: &[u8]) -> impl Iterator<Item = &[u8]> {
+ text.split(|x| matches!(*x, b' ' | b',' | b':' | b'/' | b'+' | b'&'))
+ .filter(|x| !x.is_empty())
+}
+
fn inc_key(mut k: Vec<u8>) -> Vec<u8> {
for v in k.iter_mut().rev() {
let (nv, carry) = v.overflowing_add(1);
diff --git a/server/src/routes/mod.rs b/server/src/routes/mod.rs
index 959971a..05b8025 100644
--- a/server/src/routes/mod.rs
+++ b/server/src/routes/mod.rs
@@ -16,6 +16,7 @@ pub mod items;
pub mod node;
pub mod player;
pub mod playersync;
+pub mod search;
pub mod stream;
pub mod style;
pub mod userdata;
@@ -44,7 +45,7 @@ use self::{
stream::r_stream,
style::{r_assets_css, r_assets_font, r_assets_js, r_assets_js_map},
};
-use crate::State;
+use crate::{State, routes::search::r_search};
use rocket::{
Build, Config, Rocket, catchers, fairing::AdHoc, fs::FileServer, http::Header, routes,
shield::Shield,
@@ -102,9 +103,9 @@ pub(super) fn build_rocket(state: Arc<State>) -> Rocket<Build> {
r_admin_log_stream,
r_admin_log,
r_admin_new_user,
- r_admin_users,
- r_admin_user,
r_admin_user_remove,
+ r_admin_user,
+ r_admin_users,
r_api_root,
r_assets_css,
r_assets_font,
@@ -119,6 +120,7 @@ pub(super) fn build_rocket(state: Arc<State>) -> Rocket<Build> {
r_node,
r_player,
r_playersync,
+ r_search,
r_stream,
r_version,
// Compat
diff --git a/server/src/routes/search.rs b/server/src/routes/search.rs
index 8ec2697..8726b8e 100644
--- a/server/src/routes/search.rs
+++ b/server/src/routes/search.rs
@@ -3,35 +3,57 @@
which is licensed under the GNU Affero General Public License (version 3); see /COPYING.
Copyright (C) 2026 metamuffin <metamuffin.org>
*/
+
+use std::{borrow::Cow, time::Instant};
+
use super::error::MyResult;
use crate::request_info::RequestInfo;
-use anyhow::anyhow;
-use rocket::{Either, get, response::content::RawHtml, serde::json::Json};
+use jellycommon::{
+ jellyobject::{EMPTY, Path},
+ *,
+};
+use jellydb::{Filter, Query, Sort};
+use jellyui::components::items::Items;
+use log::info;
+use rocket::{get, response::content::RawHtml};
+
+#[get("/search?<q>")]
+pub async fn r_search(ri: RequestInfo<'_>, q: Option<&str>) -> MyResult<RawHtml<String>> {
+ ri.require_user()?;
+
+ let q = q.unwrap();
+
+ let mut items = Vec::new();
+ let t = Instant::now();
+ ri.state.database.transaction(&mut |txn| {
+ let rows = txn
+ .query(Query {
+ filter: Filter::Match(Path(vec![NO_VISIBILITY.0]), VISI_VISIBLE.into()),
+ sort: Sort::TextSearch(Path(vec![NO_TITLE.0]), q.to_owned()),
+ ..Default::default()
+ })?
+ .take(64)
+ .collect::<Result<Vec<_>, _>>()?;
-#[get("/search?<query>&<page>")]
-pub async fn r_search(
- ri: RequestInfo<'_>,
- query: Option<&str>,
- page: Option<usize>,
-) -> MyResult<RawHtml<String>> {
- // let r = query
- // .map(|query| search(&ri.session, query, page))
- // .transpose()?;
+ items.clear();
+ for (r, _is) in rows {
+ let node = txn.get(r)?.unwrap();
+ items.push(node);
+ }
+ Ok(())
+ })?;
+ info!("search {q:?} took {:?}", t.elapsed());
- // Ok(if ri.accept.is_json() {
- // let Some(r) = r else {
- // Err(anyhow!("no query"))?
- // };
- // Either::Right(Json(r))
- // } else {
- // Either::Left(RawHtml(render_page(
- // &SearchPage {
- // lang: &ri.lang,
- // query: &query.map(|s| s.to_string()),
- // r,
- // },
- // ri.render_info(),
- // )))
- // })
- todo!()
+ Ok(ri.respond_ui(&Items {
+ items: &items
+ .iter()
+ .map(|node| Nku {
+ node: Cow::Borrowed(node),
+ userdata: Cow::Borrowed(EMPTY),
+ role: None,
+ })
+ .collect::<Vec<_>>(),
+ ri: &ri.render_info(),
+ cont: None,
+ }))
}