diff --git a/lektor_search/src/lib.rs b/lektor_search/src/lib.rs index 1b16a0340da685744eb0be241e4e4f96e17b980a..c9f1a91b61bc46504759dfcca5b3f22cf7a520d2 100644 --- a/lektor_search/src/lib.rs +++ b/lektor_search/src/lib.rs @@ -3,7 +3,7 @@ mod search; mod traits; use futures::{prelude::*, stream::FuturesUnordered}; -use lektor_payloads::{Kara, KaraBy}; +use lektor_payloads::{KId, KaraBy}; pub use crate::{batch::*, traits::*}; @@ -13,33 +13,31 @@ pub async fn search<const BATCH_SIZE: usize>( store: &impl KaraStore, extractor: impl KaraIdExtractor, search: Vec<KaraBy>, -) -> Vec<&Kara> { +) -> Vec<KId> { let Some(search) = search::Search::new(search) else { return Default::default(); }; stream::unfold(extractor, |mut state| async move { - if state.is_empty().await { - return None; - } - Some((state.next_id_batch::<BATCH_SIZE>().await, state)) // Get the chuncks of IDs + (!state.is_empty().await).then_some(())?; + Some((state.next_id_batch::<BATCH_SIZE>().await, state)) }) - // - // Get the karas out of the ids - // .then(|ids| store.get_kara_batch(ids)) - .map(Batch::into_array) - // - // Filter karas only if they are matched by the search - // - .map(|karas| karas.map(|maybe| maybe.and_then(|kara| search.matches_and_map(kara)))) - // - // Await the thing, build the return vector. - // + .map(|karas| { + (karas.into_array().into_iter()) + .flat_map(|maybe| maybe.and_then(|kara| search.matches(kara).then_some(kara.id))) + }) .collect::<FuturesUnordered<_>>() .await .into_iter() .flatten() - .flatten() .collect() } + +pub async fn count( + _store: &impl KaraStore, + _extractor: impl KaraIdExtractor, + _search: Vec<KaraBy>, +) -> usize { + todo!() +} diff --git a/lektor_search/src/search.rs b/lektor_search/src/search.rs index 9f8a8d0d27bd72bd2e794bf4d61d42882639eb00..68b259df8dcecf5cde3c992a29949014a864db2b 100644 --- a/lektor_search/src/search.rs +++ b/lektor_search/src/search.rs @@ -36,7 +36,7 @@ impl Search { } /// See if we matches a kara or not. - fn matches(&self, kara: &Kara) -> bool { + pub fn matches(&self, kara: &Kara) -> bool { macro_rules! ensure { ($expr:expr) => {{ if !($expr) { @@ -81,11 +81,6 @@ impl Search { true } - /// If we match a kara (see [Self::matches]), then we return said kara. - pub fn matches_and_map<'a>(&self, kara: &'a Kara) -> Option<&'a Kara> { - self.matches(kara).then_some(kara) - } - fn with_queries(self, queries: Vec<String>) -> Option<Self> { let queries = AhoCorasickBuilder::new() .ascii_case_insensitive(true) diff --git a/lektor_search/src/traits.rs b/lektor_search/src/traits.rs index 94ae174dc5b52828232f9a5a9fa0510ba1aa9b8a..a750f71e438a6ea71fbd08c9cc38cc9828c340b2 100644 --- a/lektor_search/src/traits.rs +++ b/lektor_search/src/traits.rs @@ -1,5 +1,5 @@ use crate::batch::Batch; -use lektor_payloads::{KId, Kara}; +use lektor_payloads::{KId, Kara, KaraBy}; #[allow(async_fn_in_trait)] pub trait KaraIdExtractor { @@ -7,7 +7,13 @@ pub trait KaraIdExtractor { async fn next_id(&mut self) -> Option<KId>; /// Get a next batch of kara id, to reduce any lock usage. - async fn next_id_batch<const SIZE: usize>(&mut self) -> Batch<SIZE, KId>; + async fn next_id_batch<const SIZE: usize>(&mut self) -> Batch<SIZE, KId> { + let mut ret = [None; SIZE]; + for ret in &mut ret { + *ret = self.next_id().await; + } + ret.into() + } /// Get the number of karas to process until the extractor is empty. async fn count(&self) -> usize; @@ -34,4 +40,25 @@ pub trait KaraStore { } Batch::<SIZE, &Kara>::from_array_maybe(ret) } + + /// Search the the database with a subset with the [KaraIdExtractor] and a search filter with a + /// [Vec<KaraBy>] vector. + async fn search<const SIZE: usize>( + &self, + extractor: impl KaraIdExtractor, + search: Vec<KaraBy>, + ) -> Vec<KId> + where + Self: Sized, + { + crate::search::<SIZE>(self, extractor, search).await + } + + /// Count the number of matches. The logic is the same as [KaraStore::search]. + async fn count(&self, extractor: impl KaraIdExtractor, search: Vec<KaraBy>) -> usize + where + Self: Sized, + { + crate::count(self, extractor, search).await + } } diff --git a/lektord/src/app/routes.rs b/lektord/src/app/routes.rs index b158691e89739c36dbad823e61a10035cc188361..d71f918f0ff79c2afd357a8ff80c684471403b2c 100644 --- a/lektord/src/app/routes.rs +++ b/lektord/src/app/routes.rs @@ -7,6 +7,7 @@ use crate::*; use anyhow::anyhow; +use app::search_adaptors; use axum::{ extract::{Path, State}, http::{header::CONTENT_TYPE, HeaderValue}, @@ -15,6 +16,7 @@ use axum::{ }; use lektor_nkdb::*; use lektor_payloads::*; +use lektor_search::KaraStore; use lektor_utils::decode_base64_json; use rand::{seq::SliceRandom, thread_rng}; use std::ops::RangeBounds; @@ -128,30 +130,6 @@ pub(crate) async fn get_kara_by_kid( Ok(kara) } -/// Search some karas from a search set with a regex. We take the json argument as base64 encoded -/// string, we decode it and deserialize it. -#[axum::debug_handler(state = LektorStatePtr)] -pub(crate) async fn search( - State(state): State<LektorStatePtr>, - Path(uri): Path<String>, -) -> Result<Json<Vec<KId>>, LektordError> { - let SearchData { from, regex } = decode_base64_json(uri)?; - // Ok(Json(state.database.search(from, regex).await?)) - todo!() -} - -/// Count karas available in a search set. We take the json argument as base64 encoded -/// string, we decode it and deserialize it. -#[axum::debug_handler(state = LektorStatePtr)] -pub(crate) async fn count( - State(state): State<LektorStatePtr>, - Path(uri): Path<String>, -) -> Result<Json<usize>, LektordError> { - let SearchData { from, regex } = decode_base64_json(uri)?; - // Ok(Json(state.database.count(from, regex).await)) - todo!() -} - /// Get all playlists from the database with their associated informations. #[axum::debug_handler(state = LektorStatePtr)] pub(crate) async fn get_playlists(State(state): State<LektorStatePtr>) -> Json<Vec<(KId, String)>> { @@ -584,3 +562,72 @@ pub(crate) async fn delete_history_range( .await, ) } + +macro_rules! extractor { + ( + match $matched:expr + => { $($match_pat:pat => $match_handle:expr,)+ } + => |$arg:ident| $handle:expr $(,)? + ) => { + async move { + tokio::runtime::Builder::new_current_thread() + .build() + .context("failed to build the runtime")? + .block_on(async move { + match $matched { + $($match_pat => { let $arg = $match_handle; $handle })+ + } + }) + } + }; +} + +/// Search some karas from a search set with a regex. We take the json argument as base64 encoded +/// string, we decode it and deserialize it. +#[axum::debug_handler(state = LektorStatePtr)] +pub(crate) async fn search( + State(state): State<LektorStatePtr>, + Path(uri): Path<String>, +) -> Result<Json<Vec<KId>>, LektordError> { + let SearchData { from, regex } = decode_base64_json(uri)?; + Ok(tokio::spawn(extractor! { + match from + => { + SearchFrom::Database => search_adaptors::database_extractor(&state).await, + SearchFrom::Queue => search_adaptors::queue_extractor(&state), + SearchFrom::History => search_adaptors::history_extractor(&state), + SearchFrom::Playlist(plt) => search_adaptors::playlist_extractor(&state, plt), + } + => |extractor| Ok::<_, anyhow::Error>(Json(search_adaptors::kara_store(&state) + .search::<10>(extractor, regex) + .await + )) + }) + .await + .context("failed to join the task")??) +} + +/// Count karas available in a search set. We take the json argument as base64 encoded +/// string, we decode it and deserialize it. +#[axum::debug_handler(state = LektorStatePtr)] +pub(crate) async fn count( + State(state): State<LektorStatePtr>, + Path(uri): Path<String>, +) -> Result<Json<usize>, LektordError> { + let SearchData { from, regex } = decode_base64_json(uri)?; + Ok(tokio::spawn(extractor! { + match from + => { + SearchFrom::Database => search_adaptors::database_extractor(&state).await, + SearchFrom::Queue => search_adaptors::queue_extractor(&state), + SearchFrom::History => search_adaptors::history_extractor(&state), + SearchFrom::Playlist(plt) => search_adaptors::playlist_extractor(&state, plt), + } + => |extractor| Ok::<_, anyhow::Error>(Json(search_adaptors::kara_store(&state) + .count(extractor, regex) + .await + )) + }) + .await + .context("failed to join the task")??) +} diff --git a/lektord/src/app/search_adaptors.rs b/lektord/src/app/search_adaptors.rs index dd414fe8471d1c63c0b55efb973b75ef438bcd8c..1a4136d0703ed417346a83629421ace260057887 100644 --- a/lektord/src/app/search_adaptors.rs +++ b/lektord/src/app/search_adaptors.rs @@ -18,17 +18,17 @@ pub async fn database_extractor(state: &LektorState) -> impl KaraIdExtractor + u } /// Get a thing to iterate over all the queue. -pub async fn queue_extractor(state: &LektorState) -> impl KaraIdExtractor + use<'_> { +pub fn queue_extractor(state: &LektorState) -> impl KaraIdExtractor + use<'_> { Queue(state, 0) } /// Get a thing to iterate over all the history. -pub async fn history_extractor(state: &LektorState) -> impl KaraIdExtractor + use<'_> { +pub fn history_extractor(state: &LektorState) -> impl KaraIdExtractor + use<'_> { History(state, 0) } /// Get a thing to iterate over all the kara ids of a playlist. -pub async fn playlist_extractor(state: &LektorState, plt: KId) -> impl KaraIdExtractor + use<'_> { +pub fn playlist_extractor(state: &LektorState, plt: KId) -> impl KaraIdExtractor + use<'_> { Playlist { handle: state.database.playlists(), playlist: plt, @@ -37,15 +37,19 @@ pub async fn playlist_extractor(state: &LektorState, plt: KId) -> impl KaraIdExt } /// A database wrapper to implement [lektor_search::KaraStore]. +#[derive(Clone, Copy)] struct KaraStore<'a>(&'a lektor_nkdb::Database); /// A database wrapper to implement [lektor_search::KaraStore] to iterate over the history. +#[derive(Clone, Copy)] struct History<'a>(&'a LektorState, usize); /// A database wrapper to implement [lektor_search::KaraStore] to iterate over the queue. +#[derive(Clone, Copy)] struct Queue<'a>(&'a LektorState, usize); /// A database wrapper to implement [KaraIdExtractor] for a whole epoch. +#[derive(Clone)] struct Database<'a>(EpochIds<'a>, usize); /// A database wrapper to implement [KaraIdExtractor] for a playlist. @@ -60,17 +64,6 @@ impl<'a> KaraIdExtractor for Database<'a> { self.0.next().inspect(|_| self.1 -= 1).copied() } - async fn next_id_batch<const SIZE: usize>(&mut self) -> Batch<SIZE, KId> { - std::iter::from_fn(|| self.0.next().inspect(|_| self.1 -= 1).copied()) - .enumerate() - .take(SIZE) - .fold([None; SIZE], |mut array, (idx, id)| { - array[idx] = Some(id); - array - }) - .into() - } - async fn count(&self) -> usize { self.1 } @@ -78,19 +71,11 @@ impl<'a> KaraIdExtractor for Database<'a> { impl<'a> KaraIdExtractor for History<'a> { async fn next_id(&mut self) -> Option<KId> { - let item = (self.0.queue()) + let mut item = (self.0.queue()) .read(|content, _| content.history((Bound::Included(self.1), Bound::Included(self.1)))) .await; - - match &item[..] { - [] => None, - [id] => { - self.1 += 1; - Some(*id) - } - - _ => unreachable!(), - } + debug_assert!(item.len() <= 1); + item.pop().inspect(|_| self.1 += 1) } async fn next_id_batch<const SIZE: usize>(&mut self) -> Batch<SIZE, KId> { @@ -119,19 +104,11 @@ impl<'a> KaraIdExtractor for History<'a> { impl<'a> KaraIdExtractor for Queue<'a> { async fn next_id(&mut self) -> Option<KId> { - let item = (self.0.queue()) + let mut item = (self.0.queue()) .read(|content, _| content.get((Bound::Included(self.1), Bound::Included(self.1)))) .await; - - match &item[..] { - [] => None, - [(_, id)] => { - self.1 += 1; - Some(*id) - } - - _ => unreachable!(), - } + debug_assert!(item.len() <= 1); + item.pop().inspect(|_| self.1 += 1).map(|(_, id)| id) } async fn next_id_batch<const SIZE: usize>(&mut self) -> Batch<SIZE, KId> {