use anyhow::{Context, Result};
use libsql::{Connection, params};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Category {
pub name: String,
pub category_type: CategoryType,
pub count: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum CategoryType {
Domain,
TopicTag,
ContentType,
Language,
}
impl CategoryType {
pub fn as_str(&self) -> &'static str {
match self {
CategoryType::Domain => "domain",
CategoryType::TopicTag => "topictag",
CategoryType::ContentType => "contenttype",
CategoryType::Language => "language",
}
}
}
pub struct CategoryService {
conn: Connection,
}
impl CategoryService {
pub fn new(conn: Connection) -> Self {
Self { conn }
}
pub async fn top_categories(&self, limit: usize) -> Result<Vec<Category>> {
let mut categories = Vec::new();
match self.top_domains(limit).await {
Ok(mut d) => categories.append(&mut d),
Err(e) => tracing::warn!("top domains failed: {}", e),
}
categories.sort_by(|a, b| b.count.cmp(&a.count).then_with(|| a.name.cmp(&b.name)));
categories.truncate(limit);
Ok(categories)
}
async fn top_domains(&self, limit: usize) -> Result<Vec<Category>> {
let mut rows = self
.conn
.query(
r#"
SELECT domain, COUNT(*) AS cnt
FROM url_nodes
WHERE crawled = 1 AND domain != ''
GROUP BY domain
ORDER BY cnt DESC
LIMIT ?1
"#,
params![limit.clamp(1, 500) as i64],
)
.await
.context("query top domains")?;
let mut out = Vec::new();
loop {
let row = match rows.next().await {
Ok(Some(r)) => r,
Ok(None) => break,
Err(e) => return Err(e).context("iterate top domains"),
};
let name: String = row.get(0).unwrap_or_default();
let count: i64 = row.get(1).unwrap_or(0);
if !name.is_empty() && count > 0 {
out.push(Category {
name,
category_type: CategoryType::Domain,
count: count as u64,
});
}
}
Ok(out)
}
}