diff --git a/pgml-dashboard/src/api/cms.rs b/pgml-dashboard/src/api/cms.rs index 0a58f3b33..03adf87d7 100644 --- a/pgml-dashboard/src/api/cms.rs +++ b/pgml-dashboard/src/api/cms.rs @@ -8,6 +8,7 @@ use std::str::FromStr; use comrak::{format_html_with_plugins, parse_document, Arena, ComrakPlugins}; use lazy_static::lazy_static; use markdown::mdast::Node; +use rocket::form::Form; use rocket::{fs::NamedFile, http::uri::Origin, route::Route, State}; use yaml_rust::YamlLoader; @@ -646,9 +647,26 @@ impl Collection { } } +#[post("/search_event", data = "")] +async fn search_event( + search_event: Form, + site_search: &State, +) -> ResponseOk { + match site_search + .add_search_event(search_event.search_id, search_event.clicked) + .await + { + Ok(_) => ResponseOk("ok".to_string()), + Err(e) => { + eprintln!("{:?}", e); + ResponseOk("error".to_string()) + } + } +} + #[get("/search?", rank = 20)] async fn search(query: &str, site_search: &State) -> ResponseOk { - let results = site_search + let (search_id, results) = site_search .search(query, None, None) .await .expect("Error performing search"); @@ -688,6 +706,7 @@ async fn search(query: &str, site_search: &State&", rank = 20)] async fn search_blog(query: &str, tag: &str, site_search: &State) -> ResponseOk { - let tag = if tag.len() > 0 { + let tag = if !tag.is_empty() { Some(Vec::from([tag.to_string()])) } else { None }; // If user is not making a search return all blogs in default design. - let results = if query.len() > 0 || tag.clone().is_some() { + let (search_id, results) = if !query.is_empty() || tag.clone().is_some() { let results = site_search.search(query, Some(DocType::Blog), tag.clone()).await; - let results = match results { - Ok(results) => results - .into_iter() - .map(|document| article_preview::DocMeta::from_document(document)) - .collect::>(), - Err(_) => Vec::new(), - }; - - results + match results { + Ok((search_id, results)) => ( + Some(search_id), + results + .into_iter() + .map(article_preview::DocMeta::from_document) + .collect::>(), + ), + Err(_) => (None, Vec::new()), + } } else { let mut results = Vec::new(); @@ -725,13 +745,13 @@ async fn search_blog(query: &str, tag: &str, site_search: &State 0 || tag.is_some(); + let is_search = !query.is_empty() || tag.is_some(); ResponseOk( - crate::components::pages::blog::blog_search::Response::new() + crate::components::pages::blog::blog_search::Response::new(search_id) .pattern(results, is_search) .render_once() .unwrap(), @@ -896,6 +916,7 @@ pub fn routes() -> Vec { get_docs_asset, get_user_guides, search, + search_event, search_blog ] } diff --git a/pgml-dashboard/src/components/cards/blog/article_preview/mod.rs b/pgml-dashboard/src/components/cards/blog/article_preview/mod.rs index 25de3ac39..1def2554e 100644 --- a/pgml-dashboard/src/components/cards/blog/article_preview/mod.rs +++ b/pgml-dashboard/src/components/cards/blog/article_preview/mod.rs @@ -38,13 +38,17 @@ impl DocMeta { pub struct ArticlePreview { card_type: String, meta: DocMeta, + search_id: Option, + search_result_index: Option, } impl ArticlePreview { - pub fn new(meta: &DocMeta) -> ArticlePreview { + pub fn new(meta: &DocMeta, search_id: Option, search_result_index: Option) -> ArticlePreview { ArticlePreview { card_type: String::from("default"), meta: meta.to_owned(), + search_id, + search_result_index, } } @@ -76,7 +80,7 @@ impl ArticlePreview { pub async fn from_path(path: &str) -> ArticlePreview { let doc = Document::from_path(&PathBuf::from(path)).await.unwrap(); let meta = DocMeta::from_document(doc); - ArticlePreview::new(&meta) + ArticlePreview::new(&meta, None, None) } } diff --git a/pgml-dashboard/src/components/cards/blog/article_preview/template.html b/pgml-dashboard/src/components/cards/blog/article_preview/template.html index 214479ec8..19a49bc0c 100644 --- a/pgml-dashboard/src/components/cards/blog/article_preview/template.html +++ b/pgml-dashboard/src/components/cards/blog/article_preview/template.html @@ -41,7 +41,12 @@

{}

); %> +<% +if let (Some(search_id), Some(search_result_index)) = (search_id, search_result_index) { %> +
+<% } else { %>
+<% } %> <% if card_type == String::from("featured") {%>
diff --git a/pgml-dashboard/src/components/pages/blog/blog_search/call/call_controller.js b/pgml-dashboard/src/components/pages/blog/blog_search/call/call_controller.js index 79a4bd368..68fbaaacc 100644 --- a/pgml-dashboard/src/components/pages/blog/blog_search/call/call_controller.js +++ b/pgml-dashboard/src/components/pages/blog/blog_search/call/call_controller.js @@ -10,6 +10,23 @@ export default class extends Controller { connect() { this.timer; this.tags = ""; + + document.addEventListener("click", this.handle_search_click); + } + + handle_search_click(e) { + const target = e.target.closest(".blog-search-result"); + if (target) { + const resultIndex = target.getAttribute("data-result-index"); + const searchId = target.getAttribute("data-search-id"); + const formData = new FormData(); + formData.append("search_id", searchId); + formData.append("clicked", resultIndex); + fetch('/search_event', { + method: 'POST', + body: formData, + }); + } } search() { @@ -49,4 +66,8 @@ export default class extends Controller { this.tags = ""; this.search(); } + + disconnect() { + document.removeEventListener("click", this.handle_search_click); + } } diff --git a/pgml-dashboard/src/components/pages/blog/blog_search/response/mod.rs b/pgml-dashboard/src/components/pages/blog/blog_search/response/mod.rs index ac8a89af1..5091ff85e 100644 --- a/pgml-dashboard/src/components/pages/blog/blog_search/response/mod.rs +++ b/pgml-dashboard/src/components/pages/blog/blog_search/response/mod.rs @@ -6,11 +6,15 @@ use sailfish::TemplateOnce; #[template(path = "pages/blog/blog_search/response/template.html")] pub struct Response { html: Vec, + search_id: Option, } impl Response { - pub fn new() -> Response { - Response { html: Vec::new() } + pub fn new(search_id: Option) -> Response { + Response { + html: Vec::new(), + search_id, + } } pub fn pattern(mut self, mut articles: Vec, is_search: bool) -> Response { @@ -53,7 +57,8 @@ impl Response { }; articles.reverse(); - while articles.len() > 0 { + let mut search_result_index = 0; + while !articles.is_empty() { // Get the row pattern or repeat the last two row patterns. let pattern = match layout.get(cycle) { Some(pattern) => pattern, @@ -74,11 +79,12 @@ impl Response { for (i, doc) in row.into_iter().enumerate() { let template = pattern[i]; html.push( - ArticlePreview::new(&doc.unwrap()) + ArticlePreview::new(&doc.unwrap(), self.search_id, Some(search_result_index)) .card_type(template) .render_once() .unwrap(), - ) + ); + search_result_index += 1; } } else { html.push(format!( @@ -101,24 +107,36 @@ impl Response { {}
"#, - ArticlePreview::new(&row[0].clone().unwrap()) + ArticlePreview::new(&row[0].clone().unwrap(), self.search_id, Some(search_result_index)) .big() .render_once() .unwrap(), - ArticlePreview::new(&row[1].clone().unwrap()).render_once().unwrap(), - ArticlePreview::new(&row[2].clone().unwrap()).render_once().unwrap(), - ArticlePreview::new(&row[0].clone().unwrap()).render_once().unwrap(), - ArticlePreview::new(&row[1].clone().unwrap()).render_once().unwrap(), - ArticlePreview::new(&row[2].clone().unwrap()).render_once().unwrap() - )) + ArticlePreview::new(&row[1].clone().unwrap(), self.search_id, Some(search_result_index + 1)) + .render_once() + .unwrap(), + ArticlePreview::new(&row[2].clone().unwrap(), self.search_id, Some(search_result_index + 2)) + .render_once() + .unwrap(), + ArticlePreview::new(&row[0].clone().unwrap(), self.search_id, Some(search_result_index + 3)) + .render_once() + .unwrap(), + ArticlePreview::new(&row[1].clone().unwrap(), self.search_id, Some(search_result_index + 4)) + .render_once() + .unwrap(), + ArticlePreview::new(&row[2].clone().unwrap(), self.search_id, Some(search_result_index + 5)) + .render_once() + .unwrap() + )); + search_result_index += 6; } } else { html.push( - ArticlePreview::new(&articles.pop().unwrap()) + ArticlePreview::new(&articles.pop().unwrap(), self.search_id, Some(search_result_index)) .card_type("default") .render_once() .unwrap(), - ) + ); + search_result_index += 1; } cycle += 1; } diff --git a/pgml-dashboard/src/components/pages/blog/landing_page/template.html b/pgml-dashboard/src/components/pages/blog/landing_page/template.html index c52f1c628..af8cb9505 100644 --- a/pgml-dashboard/src/components/pages/blog/landing_page/template.html +++ b/pgml-dashboard/src/components/pages/blog/landing_page/template.html @@ -7,7 +7,7 @@ use crate::utils::config::standalone_dashboard; let cards = featured_cards.iter().map(|card| { - ArticlePreview::new(card).featured().render_once().unwrap() + ArticlePreview::new(card, None, None).featured().render_once().unwrap() }).collect::>(); %> diff --git a/pgml-dashboard/src/forms.rs b/pgml-dashboard/src/forms.rs index 22f94f264..53ff66008 100644 --- a/pgml-dashboard/src/forms.rs +++ b/pgml-dashboard/src/forms.rs @@ -30,3 +30,9 @@ pub struct ChatbotPostData { #[serde(rename = "knowledgeBase")] pub knowledge_base: u8, } + +#[derive(FromForm)] +pub struct SearchEvent { + pub search_id: i64, + pub clicked: i64 +} diff --git a/pgml-dashboard/src/templates/docs.rs b/pgml-dashboard/src/templates/docs.rs index 36a101c07..67d7e77a1 100644 --- a/pgml-dashboard/src/templates/docs.rs +++ b/pgml-dashboard/src/templates/docs.rs @@ -8,6 +8,7 @@ use crate::utils::markdown::SearchResult; #[derive(TemplateOnce)] #[template(path = "components/search.html")] pub struct Search { + pub search_id: i64, pub query: String, pub results: Vec, } diff --git a/pgml-dashboard/src/utils/markdown.rs b/pgml-dashboard/src/utils/markdown.rs index 424dc81e0..0aed062cc 100644 --- a/pgml-dashboard/src/utils/markdown.rs +++ b/pgml-dashboard/src/utils/markdown.rs @@ -1286,12 +1286,19 @@ impl SiteSearch { .collect() } + pub async fn add_search_event(&self, search_id: i64, search_result: i64) -> anyhow::Result<()> { + self.collection.add_search_event(search_id, search_result + 1, serde_json::json!({ + "clicked": true + }).into(), &self.pipeline).await?; + Ok(()) + } + pub async fn search( &self, query: &str, doc_type: Option, doc_tags: Option>, - ) -> anyhow::Result> { + ) -> anyhow::Result<(i64, Vec)> { let mut search = serde_json::json!({ "query": { // "full_text_search": { @@ -1335,15 +1342,22 @@ impl SiteSearch { } let results = self.collection.search_local(search.into(), &self.pipeline).await?; - results["results"] - .as_array() - .context("Error getting results from search")? - .iter() - .map(|r| { - let document: Document = serde_json::from_value(r["document"].clone())?; - Ok(document) - }) - .collect() + let search_id = results["search_id"] + .as_i64() + .context("Error getting search_id from search")?; + + Ok(( + search_id, + results["results"] + .as_array() + .context("Error getting results from search")? + .iter() + .map(|r| { + let document: Document = serde_json::from_value(r["document"].clone())?; + anyhow::Ok(document) + }) + .collect::>>()?, + )) } pub async fn build(&mut self) -> anyhow::Result<()> { diff --git a/pgml-dashboard/static/js/search.js b/pgml-dashboard/static/js/search.js index 02bd989b9..1569790c0 100644 --- a/pgml-dashboard/static/js/search.js +++ b/pgml-dashboard/static/js/search.js @@ -1,48 +1,66 @@ import { - Controller + Controller } from '@hotwired/stimulus' export default class extends Controller { - static targets = [ - 'searchTrigger', - ] + static targets = [ + 'searchTrigger', + ] - connect() { - this.target = document.getElementById("search"); - this.searchInput = document.getElementById("search-input"); - this.searchFrame = document.getElementById("search-results") + connect() { + this.target = document.getElementById("search"); + this.searchInput = document.getElementById("search-input"); + this.searchFrame = document.getElementById("search-results") - this.target.addEventListener('shown.bs.modal', this.focusSearchInput) - this.target.addEventListener('hidden.bs.modal', this.updateSearch) - this.searchInput.addEventListener('input', (e) => this.search(e)) + this.target.addEventListener('shown.bs.modal', this.focusSearchInput) + this.target.addEventListener('hidden.bs.modal', this.updateSearch) + this.searchInput.addEventListener('input', (e) => this.search(e)) - this.timer; - } + this.timer; - search(e) { - clearTimeout(this.timer); - const query = e.currentTarget.value - this.timer = setTimeout(() => { - this.searchFrame.src = `/search?query=${query}` - }, 250); - } + document.addEventListener("click", this.handle_search_click); + } - focusSearchInput = (e) => { - this.searchInput.focus() - this.searchTriggerTarget.blur() + handle_search_click(e) { + const target = e.target.closest(".search-result"); + if (target) { + const resultIndex = target.getAttribute("data-result-index"); + const searchId = target.getAttribute("data-search-id"); + const formData = new FormData(); + formData.append("search_id", searchId); + formData.append("clicked", resultIndex); + fetch('/search_event', { + method: 'POST', + body: formData, + }); } + } - updateSearch = () => { - this.searchTriggerTarget.value = this.searchInput.value - } + search(e) { + clearTimeout(this.timer); + const query = e.currentTarget.value + this.timer = setTimeout(() => { + this.searchFrame.src = `/search?query=${query}` + }, 250); + } - openSearch = (e) => { - new bootstrap.Modal(this.target).show() - this.searchInput.value = e.currentTarget.value - } + focusSearchInput = () => { + this.searchInput.focus() + this.searchTriggerTarget.blur() + } - disconnect() { - this.searchTriggerTarget.removeEventListener('shown.bs.modal', this.focusSearchInput) - this.searchTriggerTarget.removeEventListener('hidden.bs.modal', this.updateSearch) - } + updateSearch = () => { + this.searchTriggerTarget.value = this.searchInput.value + } + + openSearch = (e) => { + new bootstrap.Modal(this.target).show() + this.searchInput.value = e.currentTarget.value + } + + disconnect() { + this.searchTriggerTarget.removeEventListener('shown.bs.modal', this.focusSearchInput) + this.searchTriggerTarget.removeEventListener('hidden.bs.modal', this.updateSearch) + document.removeEventListener("click", this.handle_search_click); + } } diff --git a/pgml-dashboard/templates/components/search.html b/pgml-dashboard/templates/components/search.html index 5fa45bd1e..4d795631e 100644 --- a/pgml-dashboard/templates/components/search.html +++ b/pgml-dashboard/templates/components/search.html @@ -1,11 +1,11 @@