Skip to main content

headless_lms_models/
external_courses.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 ExternalCourse {
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 name: String,
13    pub description: Option<String>,
14    pub url: String,
15    #[schema(value_type = Option<Vec<f32>>)]
16    pub name_embedding: Option<Vector>,
17    #[schema(value_type = Option<Vec<f32>>)]
18    pub description_embedding: Option<Vector>,
19}
20
21#[derive(Debug, Serialize)]
22pub struct ExternalCourseOutput {
23    id: Uuid,
24    name: String,
25    description: Option<String>,
26    url: String,
27}
28/**
29Searches for external courses with a list of given keywords, with both matching its embedding vector to embeddings of external course name and description,
30and doing a keyword search to concatenated name and description tsvector.
31*/
32pub async fn get_external_courses_by_embeddings(
33    conn: &mut PgConnection,
34    keywords: Vec<String>,
35    embeddings: Vec<Vec<f32>>,
36) -> ModelResult<Vec<ExternalCourseOutput>> {
37    let embed_vecs: Vec<Vector> = embeddings.into_iter().map(Vector::from).collect();
38    let res = sqlx::query_as!(
39        ExternalCourseOutput,
40        r#"
41SELECT  t.id AS "id!",
42    t.name AS "name!",
43    t.description,
44    t.url AS "url!"
45FROM (
46    SELECT
47        ec.*,
48        LEAST(MIN(name_embedding <#> v.embedding),
49              MIN(description_embedding <#> v.embedding)) AS distance
50    FROM external_courses ec
51    CROSS JOIN unnest($1::vector[]) AS v(embedding)
52    WHERE deleted_at IS NULL
53    GROUP BY id
54    ORDER BY distance ASC
55    LIMIT 5
56) t
57UNION
58SELECT ec.id,
59       ec.name,
60       ec.description,
61       ec.url
62FROM external_courses ec
63CROSS JOIN unnest($2::text[]) AS k(keyword)
64WHERE deleted_at IS NULL
65AND to_tsvector(
66    'english',
67    ec.name || ' ' || coalesce(ec.description, '')
68) @@ websearch_to_tsquery('english', k.keyword)
69
70      "#,
71        &embed_vecs as _,
72        &keywords,
73    )
74    .fetch_all(conn)
75    .await?;
76    Ok(res)
77}