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
116pub 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}