Skip to main content

headless_lms_models/
course_audiences.rs

1use crate::prelude::*;
2use pgvector::Vector;
3use serde::{Deserialize, Serialize};
4use utoipa::ToSchema;
5
6#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ToSchema)]
7pub struct CourseAudience {
8    pub id: Uuid,
9    pub created_at: DateTime<Utc>,
10    pub updated_at: DateTime<Utc>,
11    pub deleted_at: Option<DateTime<Utc>>,
12    pub course_id: Uuid,
13    pub audience: String,
14    #[schema(value_type = Option<Vec<f32>>)]
15    pub embedding: Option<Vector>,
16}
17
18#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, ToSchema, Hash)]
19pub struct EditCourseAudience {
20    pub id: Uuid,
21    pub course_id: Uuid,
22    pub audience: String,
23}
24
25pub async fn insert_course_audiences(
26    conn: &mut PgConnection,
27    course_id: Uuid,
28    audiences: Vec<String>,
29    embeddings: Vec<Vec<f32>>,
30) -> ModelResult<Vec<CourseAudience>> {
31    let embed_vecs: Vec<Vector> = embeddings.into_iter().map(Vector::from).collect();
32    let res = sqlx::query_as!(
33        CourseAudience,
34        r#"
35INSERT INTO course_audiences (
36    course_id,
37    audience,
38    embedding
39  )
40SELECT $1,
41       t.audience,
42       t.embedding
43FROM UNNEST(
44    $2::text[],
45    $3::vector[]
46) AS t(audience, embedding)
47RETURNING *
48    "#,
49        course_id,
50        &audiences,
51        &embed_vecs as _
52    )
53    .fetch_all(conn)
54    .await?;
55    Ok(res)
56}
57
58pub async fn get_all_edit_course_audiences(
59    conn: &mut PgConnection,
60) -> ModelResult<Vec<EditCourseAudience>> {
61    let res = sqlx::query_as!(
62        EditCourseAudience,
63        "
64SELECT id,
65audience,
66course_id
67FROM course_audiences
68WHERE deleted_at IS NULL
69",
70    )
71    .fetch_all(conn)
72    .await?;
73    Ok(res)
74}
75
76pub async fn get_edit_course_audiences_by_course_id(
77    conn: &mut PgConnection,
78    course_id: Uuid,
79) -> ModelResult<Vec<EditCourseAudience>> {
80    let res = sqlx::query_as!(
81        EditCourseAudience,
82        "
83SELECT id,
84audience,
85course_id
86FROM course_audiences
87WHERE course_id = $1
88AND deleted_at IS NULL
89",
90        course_id
91    )
92    .fetch_all(conn)
93    .await?;
94    Ok(res)
95}
96
97pub async fn get_by_course_id(
98    conn: &mut PgConnection,
99    course_id: Uuid,
100) -> ModelResult<Vec<CourseAudience>> {
101    let res = sqlx::query_as!(
102        CourseAudience,
103        r#"
104SELECT *
105FROM course_audiences
106WHERE course_id = $1
107AND deleted_at IS NULL
108"#,
109        course_id
110    )
111    .fetch_all(conn)
112    .await?;
113    Ok(res)
114}
115
116/// Inserts or updates the course's audiences by id, bringing back deleted ones.
117pub async fn upsert_course_audiences(
118    conn: &mut PgConnection,
119    course_id: Uuid,
120    audience_ids: Vec<Uuid>,
121    updated_audiences: Vec<String>,
122    embeddings: Vec<Vec<f32>>,
123) -> ModelResult<Vec<CourseAudience>> {
124    let embed_vecs: Vec<Vector> = embeddings.into_iter().map(Vector::from).collect();
125
126    let id_count = sqlx::query_scalar!(
127        r#"
128SELECT COUNT(*) AS "count!"
129FROM course_audiences
130WHERE id = ANY($2)
131  AND course_id != $1
132"#,
133        course_id,
134        &audience_ids,
135    )
136    .fetch_one(&mut *conn)
137    .await?;
138
139    if id_count != 0 {
140        return Err(model_err!(
141            InvalidRequest,
142            "Ids of some given audience entries already exists on other courses.".to_string()
143        ));
144    }
145
146    let res = sqlx::query_as!(
147        CourseAudience,
148        "
149INSERT INTO course_audiences (course_id, id, audience, embedding)
150SELECT $1,
151  course_audience.id,
152  course_audience.audience,
153  course_audience.embedding
154FROM UNNEST ($2::UUID [], $3::TEXT [], $4::VECTOR []) AS course_audience(id, audience, embedding) ON CONFLICT (id) DO
155UPDATE
156SET audience = EXCLUDED.audience,
157  embedding = EXCLUDED.embedding,
158  deleted_at = NULL
159WHERE course_audiences.course_id = EXCLUDED.course_id
160RETURNING *
161",
162        course_id,
163        &audience_ids,
164        &updated_audiences,
165        &embed_vecs as _
166    )
167    .fetch_all(&mut *conn)
168    .await?;
169
170    Ok(res)
171}
172
173pub async fn delete_batch(
174    conn: &mut PgConnection,
175    ids_to_delete: Vec<Uuid>,
176) -> ModelResult<Vec<CourseAudience>> {
177    let res = sqlx::query_as!(
178        CourseAudience,
179        r#"
180UPDATE course_audiences
181SET deleted_at = now()
182WHERE id = ANY($1::UUID [])
183AND deleted_at IS NULL
184RETURNING *
185"#,
186        &ids_to_delete
187    )
188    .fetch_all(conn)
189    .await?;
190    Ok(res)
191}
192
193pub async fn get_course_ids_by_audience_vectors(
194    conn: &mut PgConnection,
195    audience_vecs: Vec<Vec<f32>>,
196    audience_keywords: Vec<String>,
197) -> ModelResult<Vec<Uuid>> {
198    let vectors: Vec<Vector> = audience_vecs.into_iter().map(Vector::from).collect();
199    let res = sqlx::query_scalar!(
200        r#"
201SELECT course_id
202FROM (
203    SELECT
204        a.course_id,
205        MIN(a.embedding <#> v.embedding) AS distance
206    FROM course_audiences a
207    CROSS JOIN unnest($1::vector[]) AS v(embedding)
208    WHERE deleted_at IS NULL
209    GROUP BY a.course_id
210    ORDER BY distance ASC
211    LIMIT 5
212) t
213UNION ALL
214SELECT DISTINCT a.course_id
215FROM course_audiences a
216CROSS JOIN unnest($2::text[]) AS k(keyword)
217WHERE deleted_at IS NULL
218AND a.audience % k.keyword
219        "#,
220        &vectors as _,
221        &audience_keywords
222    )
223    .fetch_all(conn)
224    .await?;
225    Ok(res.into_iter().flatten().collect())
226}