From 227175921f1ca2ea2161cac37fe81434a5d41e9b Mon Sep 17 00:00:00 2001 From: SilasMarvin <19626586+SilasMarvin@users.noreply.github.com> Date: Fri, 15 Mar 2024 09:20:29 -0700 Subject: [PATCH 1/3] Prep for saving events --- pgml-dashboard/src/api/cms.rs | 28 +++-- pgml-dashboard/src/forms.rs | 6 + pgml-dashboard/src/templates/docs.rs | 1 + pgml-dashboard/src/utils/markdown.rs | 27 +++-- pgml-dashboard/static/js/search.js | 105 +++++++++++------- .../templates/components/search.html | 6 +- 6 files changed, 107 insertions(+), 66 deletions(-) diff --git a/pgml-dashboard/src/api/cms.rs b/pgml-dashboard/src/api/cms.rs index 0a58f3b33..53964c29f 100644 --- a/pgml-dashboard/src/api/cms.rs +++ b/pgml-dashboard/src/api/cms.rs @@ -5,6 +5,7 @@ use std::{ use std::str::FromStr; +use rocket::form::Form; use comrak::{format_html_with_plugins, parse_document, Arena, ComrakPlugins}; use lazy_static::lazy_static; use markdown::mdast::Node; @@ -646,9 +647,16 @@ impl Collection { } } + +#[post("/search_event", data = "")] +async fn search_event(search_event: Form, site_search: &State) -> ResponseOk { + eprintln!("WE GOT IT: {:?}", search_event.clicked); + ResponseOk("".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 +696,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 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 + match results { + Ok((_search_id, results)) => results .into_iter() - .map(|document| article_preview::DocMeta::from_document(document)) + .map(article_preview::DocMeta::from_document) .collect::>(), Err(_) => Vec::new(), - }; - - results + } } else { let mut results = Vec::new(); @@ -728,7 +735,7 @@ 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() @@ -896,6 +903,7 @@ pub fn routes() -> Vec { get_docs_asset, get_user_guides, search, + search_event, search_blog ] } 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..f0f22fe12 100644 --- a/pgml-dashboard/src/utils/markdown.rs +++ b/pgml-dashboard/src/utils/markdown.rs @@ -1291,7 +1291,7 @@ impl SiteSearch { 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 +1335,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..37f5036f6 100644 --- a/pgml-dashboard/static/js/search.js +++ b/pgml-dashboard/static/js/search.js @@ -1,48 +1,67 @@ import { - Controller + Controller } from '@hotwired/stimulus' export default class extends Controller { - static targets = [ - 'searchTrigger', - ] - - 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.timer; - } - - search(e) { - clearTimeout(this.timer); - const query = e.currentTarget.value - this.timer = setTimeout(() => { - this.searchFrame.src = `/search?query=${query}` - }, 250); - } - - focusSearchInput = (e) => { - this.searchInput.focus() - this.searchTriggerTarget.blur() - } - - 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) - } + static targets = [ + 'searchTrigger', + ] + + 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.timer; + + // Listen to click events and store clicked results + document.addEventListener("click", function(e) { + const target = e.target.closest(".search-result"); + if (target) { + const resultIndex = target.getAttribute("data-result-index"); + const searchId = target.getAttribute("data-search-id"); + fetch('/search_event', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + search_id: searchId, + clicked: resultIndex, + }), + }); + } + }); + } + + search(e) { + clearTimeout(this.timer); + const query = e.currentTarget.value + this.timer = setTimeout(() => { + this.searchFrame.src = `/search?query=${query}` + }, 250); + } + + focusSearchInput = (e) => { + this.searchInput.focus() + this.searchTriggerTarget.blur() + } + + 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) + } } 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 @@
- <% if query.len() < 1 { %> + <% if query.is_empty() { %>

Type to start searching

<% } else if !results.is_empty() { - for result in results.iter() { + for (i, result) in results.iter().enumerate() { %> - +
<%= result.title %>
<% if !result.snippet.is_empty() { %> From 6a343ceb8c91826220899372ebb0c1572c396473 Mon Sep 17 00:00:00 2001 From: SilasMarvin <19626586+SilasMarvin@users.noreply.github.com> Date: Fri, 15 Mar 2024 13:15:08 -0700 Subject: [PATCH 2/3] Working add search events --- pgml-dashboard/src/api/cms.rs | 9 +++++-- pgml-dashboard/src/utils/markdown.rs | 7 ++++++ pgml-dashboard/static/js/search.js | 37 ++++++++++++++-------------- 3 files changed, 32 insertions(+), 21 deletions(-) diff --git a/pgml-dashboard/src/api/cms.rs b/pgml-dashboard/src/api/cms.rs index 53964c29f..089ff6a26 100644 --- a/pgml-dashboard/src/api/cms.rs +++ b/pgml-dashboard/src/api/cms.rs @@ -650,8 +650,13 @@ impl Collection { #[post("/search_event", data = "")] async fn search_event(search_event: Form, site_search: &State) -> ResponseOk { - eprintln!("WE GOT IT: {:?}", search_event.clicked); - ResponseOk("".to_string()) + 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)] diff --git a/pgml-dashboard/src/utils/markdown.rs b/pgml-dashboard/src/utils/markdown.rs index f0f22fe12..0aed062cc 100644 --- a/pgml-dashboard/src/utils/markdown.rs +++ b/pgml-dashboard/src/utils/markdown.rs @@ -1286,6 +1286,13 @@ 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, diff --git a/pgml-dashboard/static/js/search.js b/pgml-dashboard/static/js/search.js index 37f5036f6..1569790c0 100644 --- a/pgml-dashboard/static/js/search.js +++ b/pgml-dashboard/static/js/search.js @@ -18,24 +18,22 @@ export default class extends Controller { this.timer; - // Listen to click events and store clicked results - document.addEventListener("click", function(e) { - const target = e.target.closest(".search-result"); - if (target) { - const resultIndex = target.getAttribute("data-result-index"); - const searchId = target.getAttribute("data-search-id"); - fetch('/search_event', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - search_id: searchId, - clicked: resultIndex, - }), - }); - } - }); + document.addEventListener("click", this.handle_search_click); + } + + 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, + }); + } } search(e) { @@ -46,7 +44,7 @@ export default class extends Controller { }, 250); } - focusSearchInput = (e) => { + focusSearchInput = () => { this.searchInput.focus() this.searchTriggerTarget.blur() } @@ -63,5 +61,6 @@ export default class extends Controller { disconnect() { this.searchTriggerTarget.removeEventListener('shown.bs.modal', this.focusSearchInput) this.searchTriggerTarget.removeEventListener('hidden.bs.modal', this.updateSearch) + document.removeEventListener("click", this.handle_search_click); } } From 8918309f6c45d1a3bea56118481f1abad58f29c6 Mon Sep 17 00:00:00 2001 From: SilasMarvin <19626586+SilasMarvin@users.noreply.github.com> Date: Fri, 15 Mar 2024 15:06:27 -0700 Subject: [PATCH 3/3] Add search event tracking to the blogs page --- pgml-dashboard/src/api/cms.rs | 34 ++++++++------ .../cards/blog/article_preview/mod.rs | 8 +++- .../cards/blog/article_preview/template.html | 5 ++ .../blog/blog_search/call/call_controller.js | 21 +++++++++ .../pages/blog/blog_search/response/mod.rs | 46 +++++++++++++------ .../pages/blog/landing_page/template.html | 2 +- 6 files changed, 86 insertions(+), 30 deletions(-) diff --git a/pgml-dashboard/src/api/cms.rs b/pgml-dashboard/src/api/cms.rs index 089ff6a26..03adf87d7 100644 --- a/pgml-dashboard/src/api/cms.rs +++ b/pgml-dashboard/src/api/cms.rs @@ -5,10 +5,10 @@ use std::{ use std::str::FromStr; -use rocket::form::Form; 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; @@ -647,14 +647,19 @@ 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 { +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()) + ResponseOk("error".to_string()) } } } @@ -718,15 +723,18 @@ async fn search_blog(query: &str, tag: &str, site_search: &State results - .into_iter() - .map(article_preview::DocMeta::from_document) - .collect::>(), - Err(_) => Vec::new(), + 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(); @@ -737,13 +745,13 @@ async fn search_blog(query: &str, tag: &str, site_search: &State, + 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::>(); %>