1use anyhow::Context;
2use serde::Deserialize;
3use serde_json::json;
4use sqlx::{Executor, PgConnection, Pool, Postgres, Transaction};
5use std::collections::HashMap;
6use tracing::instrument;
7
8use crate::debug_sqlx_query;
9use crate::{
10 collection::ProjectInfo,
11 model::{Model, ModelRuntime},
12 models, queries, query_builder,
13 remote_embeddings::build_remote_embeddings,
14 splitter::Splitter,
15 types::{DateTime, Json, TryToNumeric},
16};
17
18#[cfg(feature = "rust_bridge")]
19use rust_bridge::{alias, alias_methods};
20
21#[cfg(feature = "python")]
22use crate::types::JsonPython;
23
24#[cfg(feature = "c")]
25use crate::languages::c::JsonC;
26
27type ParsedSchema = HashMap<String, FieldAction>;
28
29#[derive(Deserialize)]
30#[serde(deny_unknown_fields)]
31struct ValidSplitterAction {
32 model: Option<String>,
33 parameters: Option<Json>,
34}
35
36#[derive(Deserialize)]
37#[serde(deny_unknown_fields)]
38struct ValidEmbedAction {
39 model: String,
40 source: Option<String>,
41 parameters: Option<Json>,
42 hnsw: Option<Json>,
43}
44
45#[derive(Deserialize, Debug, Clone)]
46#[serde(deny_unknown_fields)]
47pub struct FullTextSearchAction {
48 configuration: String,
49}
50
51#[derive(Deserialize)]
52#[serde(deny_unknown_fields)]
53struct ValidFieldAction {
54 splitter: Option<ValidSplitterAction>,
55 semantic_search: Option<ValidEmbedAction>,
56 full_text_search: Option<FullTextSearchAction>,
57}
58
59#[allow(clippy::upper_case_acronyms)]
60#[derive(Debug, Clone)]
61pub struct HNSW {
62 m: u64,
63 ef_construction: u64,
64}
65
66impl Default for HNSW {
67 fn default() -> Self {
68 Self {
69 m: 16,
70 ef_construction: 64,
71 }
72 }
73}
74
75impl TryFrom<Json> for HNSW {
76 type Error = anyhow::Error;
77 fn try_from(value: Json) -> anyhow::Result<Self> {
78 let m = if !value["m"].is_null() {
79 value["m"]
80 .try_to_u64()
81 .context("hnsw.m must be an integer")?
82 } else {
83 16
84 };
85 let ef_construction = if !value["ef_construction"].is_null() {
86 value["ef_construction"]
87 .try_to_u64()
88 .context("hnsw.ef_construction must be an integer")?
89 } else {
90 64
91 };
92 Ok(Self { m, ef_construction })
93 }
94}
95
96#[derive(Debug, Clone)]
97pub struct SplitterAction {
98 pub model: Splitter,
99}
100
101#[derive(Debug, Clone)]
102pub struct SemanticSearchAction {
103 pub model: Model,
104 pub hnsw: HNSW,
105}
106
107#[derive(Debug, Clone)]
108pub struct FieldAction {
109 pub splitter: Option<SplitterAction>,
110 pub semantic_search: Option<SemanticSearchAction>,
111 pub full_text_search: Option<FullTextSearchAction>,
112}
113
114impl TryFrom<ValidFieldAction> for FieldAction {
115 type Error = anyhow::Error;
116 fn try_from(value: ValidFieldAction) -> Result<Self, Self::Error> {
117 let embed = value
118 .semantic_search
119 .map(|v| {
120 let model = Model::new(Some(v.model), v.source, v.parameters);
121 let hnsw = v
122 .hnsw
123 .map(HNSW::try_from)
124 .unwrap_or_else(|| Ok(HNSW::default()))?;
125 anyhow::Ok(SemanticSearchAction { model, hnsw })
126 })
127 .transpose()?;
128 let splitter = value
129 .splitter
130 .map(|v| {
131 let splitter = Splitter::new(v.model, v.parameters);
132 anyhow::Ok(SplitterAction { model: splitter })
133 })
134 .transpose()?;
135 Ok(Self {
136 splitter,
137 semantic_search: embed,
138 full_text_search: value.full_text_search,
139 })
140 }
141}
142
143#[derive(Debug, Clone)]
144pub struct InvividualSyncStatus {
145 pub synced: i64,
146 pub not_synced: i64,
147 pub total: i64,
148}
149
150impl From<InvividualSyncStatus> for Json {
151 fn from(value: InvividualSyncStatus) -> Self {
152 serde_json::json!({
153 "synced": value.synced,
154 "not_synced": value.not_synced,
155 "total": value.total,
156 })
157 .into()
158 }
159}
160
161impl From<Json> for InvividualSyncStatus {
162 fn from(value: Json) -> Self {
163 Self {
164 synced: value["synced"]
165 .as_i64()
166 .expect("The synced field is not an integer"),
167 not_synced: value["not_synced"]
168 .as_i64()
169 .expect("The not_synced field is not an integer"),
170 total: value["total"]
171 .as_i64()
172 .expect("The total field is not an integer"),
173 }
174 }
175}
176
177#[derive(Debug, Clone)]
178#[allow(dead_code)]
179pub struct PipelineDatabaseData {
180 pub id: i64,
181 pub created_at: DateTime,
182}
183
184#[cfg_attr(feature = "rust_bridge", derive(alias))]
186#[derive(Debug, Clone)]
187pub struct Pipeline {
188 pub(crate) name: String,
189 pub(crate) schema: Option<Json>,
190 pub(crate) parsed_schema: Option<ParsedSchema>,
191 database_data: Option<PipelineDatabaseData>,
192}
193
194fn json_to_schema(schema: &Json) -> anyhow::Result<ParsedSchema> {
195 schema
196 .as_object()
197 .context("Schema object must be a JSON object")?
198 .iter()
199 .try_fold(ParsedSchema::new(), |mut acc, (key, value)| {
200 if acc.contains_key(key) {
201 Err(anyhow::anyhow!("Schema contains duplicate keys"))
202 } else {
203 let action: ValidFieldAction = serde_json::from_value(value.to_owned())?;
205 acc.insert(key.to_owned(), action.try_into()?);
207 Ok(acc)
208 }
209 })
210}
211
212#[cfg_attr(feature = "rust_bridge", alias_methods(new))]
213impl Pipeline {
214 pub fn new(name: &str, schema: Option<Json>) -> anyhow::Result<Self> {
220 let parsed_schema = schema.as_ref().map(json_to_schema).transpose()?;
221 Ok(Self {
222 name: name.to_string(),
223 schema,
224 parsed_schema,
225 database_data: None,
226 })
227 }
228
229 #[instrument(skip(self))]
231 pub(crate) async fn get_status(
232 &mut self,
233 project_info: &ProjectInfo,
234 pool: &Pool<Postgres>,
235 ) -> anyhow::Result<Json> {
236 let parsed_schema = self
237 .parsed_schema
238 .as_ref()
239 .context("Pipeline must have schema to get status")?;
240
241 let mut results = json!({});
242
243 let schema = format!("{}_{}", project_info.name, self.name);
244 let documents_table_name = format!("{}.documents", project_info.name);
245 for (key, value) in parsed_schema.iter() {
246 let chunks_table_name = format!("{schema}.{key}_chunks");
247
248 results[key] = json!({});
249
250 if value.splitter.is_some() {
251 let chunks_status: (Option<i64>, Option<i64>) = sqlx::query_as(&query_builder!(
252 "SELECT (SELECT COUNT(DISTINCT document_id) FROM %s), COUNT(id) FROM %s",
253 chunks_table_name,
254 documents_table_name
255 ))
256 .fetch_one(pool)
257 .await?;
258 results[key]["chunks"] = json!({
259 "synced": chunks_status.0.unwrap_or(0),
260 "not_synced": chunks_status.1.unwrap_or(0) - chunks_status.0.unwrap_or(0),
261 "total": chunks_status.1.unwrap_or(0),
262 });
263 }
264
265 if value.semantic_search.is_some() {
266 let embeddings_table_name = format!("{schema}.{key}_embeddings");
267 let embeddings_status: (Option<i64>, Option<i64>) =
268 sqlx::query_as(&query_builder!(
269 "SELECT (SELECT count(*) FROM %s), (SELECT count(*) FROM %s)",
270 embeddings_table_name,
271 chunks_table_name
272 ))
273 .fetch_one(pool)
274 .await?;
275 results[key]["embeddings"] = json!({
276 "synced": embeddings_status.0.unwrap_or(0),
277 "not_synced": embeddings_status.1.unwrap_or(0) - embeddings_status.0.unwrap_or(0),
278 "total": embeddings_status.1.unwrap_or(0),
279 });
280 }
281
282 if value.full_text_search.is_some() {
283 let tsvectors_table_name = format!("{schema}.{key}_tsvectors");
284 let tsvectors_status: (Option<i64>, Option<i64>) = sqlx::query_as(&query_builder!(
285 "SELECT (SELECT count(*) FROM %s), (SELECT count(*) FROM %s)",
286 tsvectors_table_name,
287 chunks_table_name
288 ))
289 .fetch_one(pool)
290 .await?;
291 results[key]["tsvectors"] = json!({
292 "synced": tsvectors_status.0.unwrap_or(0),
293 "not_synced": tsvectors_status.1.unwrap_or(0) - tsvectors_status.0.unwrap_or(0),
294 "total": tsvectors_status.1.unwrap_or(0),
295 });
296 }
297 }
298 Ok(results.into())
299 }
300
301 #[instrument(skip(self))]
302 pub(crate) async fn verify_in_database(
303 &mut self,
304 project_info: &ProjectInfo,
305 throw_if_exists: bool,
306 pool: &Pool<Postgres>,
307 ) -> anyhow::Result<()> {
308 if self.database_data.is_none() {
309 let pipeline: Option<models::Pipeline> = sqlx::query_as(&query_builder!(
310 "SELECT * FROM %s WHERE name = $1",
311 format!("{}.pipelines", project_info.name)
312 ))
313 .bind(&self.name)
314 .fetch_optional(pool)
315 .await?;
316
317 let pipeline = if let Some(pipeline) = pipeline {
318 if throw_if_exists {
319 anyhow::bail!("Pipeline {} already exists. You do not need to add this pipeline to the collection as it has already been added.", pipeline.name);
320 }
321
322 let mut parsed_schema = json_to_schema(&pipeline.schema)?;
323
324 for (_key, value) in parsed_schema.iter_mut() {
325 if let Some(splitter) = &mut value.splitter {
326 splitter
327 .model
328 .verify_in_database(project_info, false, pool)
329 .await?;
330 }
331 if let Some(embed) = &mut value.semantic_search {
332 embed
333 .model
334 .verify_in_database(project_info, false, pool)
335 .await?;
336 }
337 }
338 self.schema = Some(pipeline.schema.clone());
339 self.parsed_schema = Some(parsed_schema);
340
341 pipeline
342 } else {
343 let schema = self
344 .schema
345 .as_ref()
346 .context("Pipeline must have schema to store in database")?;
347 let mut parsed_schema = json_to_schema(schema)?;
348
349 for (_key, value) in parsed_schema.iter_mut() {
350 if let Some(splitter) = &mut value.splitter {
351 splitter
352 .model
353 .verify_in_database(project_info, false, pool)
354 .await?;
355 }
356 if let Some(embed) = &mut value.semantic_search {
357 embed
358 .model
359 .verify_in_database(project_info, false, pool)
360 .await?;
361 }
362 }
363 self.parsed_schema = Some(parsed_schema);
364
365 let mut transaction = pool.begin().await?;
368 let pipeline = sqlx::query_as(&query_builder!(
369 "INSERT INTO %s (name, schema) VALUES ($1, $2) RETURNING *",
370 format!("{}.pipelines", project_info.name)
371 ))
372 .bind(&self.name)
373 .bind(&self.schema)
374 .fetch_one(&mut *transaction)
375 .await?;
376 self.create_tables(project_info, &mut transaction).await?;
377 transaction.commit().await?;
378
379 pipeline
380 };
381 self.database_data = Some(PipelineDatabaseData {
382 id: pipeline.id,
383 created_at: pipeline.created_at,
384 })
385 }
386 Ok(())
387 }
388
389 #[instrument(skip(self))]
390 async fn create_tables(
391 &mut self,
392 project_info: &ProjectInfo,
393 transaction: &mut Transaction<'_, Postgres>,
394 ) -> anyhow::Result<()> {
395 let collection_name = &project_info.name;
396 let documents_table_name = format!("{}.documents", collection_name);
397
398 let schema = format!("{}_{}", collection_name, self.name);
399
400 transaction
401 .execute(query_builder!("CREATE SCHEMA IF NOT EXISTS %s", schema).as_str())
402 .await?;
403
404 let parsed_schema = self
405 .parsed_schema
406 .as_ref()
407 .context("Pipeline must have schema to create_tables")?;
408
409 let searches_table_name = format!("{schema}.searches");
410 transaction
411 .execute(
412 query_builder!(
413 queries::CREATE_PIPELINES_SEARCHES_TABLE,
414 searches_table_name
415 )
416 .as_str(),
417 )
418 .await?;
419
420 let search_results_table_name = format!("{schema}.search_results");
421 transaction
422 .execute(
423 query_builder!(
424 queries::CREATE_PIPELINES_SEARCH_RESULTS_TABLE,
425 search_results_table_name,
426 &searches_table_name,
427 &documents_table_name
428 )
429 .as_str(),
430 )
431 .await?;
432 transaction
433 .execute(
434 query_builder!(
435 queries::CREATE_INDEX,
436 "",
437 "search_results_search_id_rank_index",
438 search_results_table_name,
439 "search_id, rank"
440 )
441 .as_str(),
442 )
443 .await?;
444
445 let search_events_table_name = format!("{schema}.search_events");
446 transaction
447 .execute(
448 query_builder!(
449 queries::CREATE_PIPELINES_SEARCH_EVENTS_TABLE,
450 search_events_table_name,
451 &search_results_table_name
452 )
453 .as_str(),
454 )
455 .await?;
456
457 for (key, value) in parsed_schema.iter() {
458 let chunks_table_name = format!("{}.{}_chunks", schema, key);
459 transaction
460 .execute(
461 query_builder!(
462 queries::CREATE_CHUNKS_TABLE,
463 chunks_table_name,
464 documents_table_name
465 )
466 .as_str(),
467 )
468 .await?;
469 let index_name = format!("{}_pipeline_chunk_document_id_index", key);
470 transaction
471 .execute(
472 query_builder!(
473 queries::CREATE_INDEX,
474 "",
475 index_name,
476 chunks_table_name,
477 "document_id"
478 )
479 .as_str(),
480 )
481 .await?;
482
483 if let Some(embed) = &value.semantic_search {
484 let embeddings_table_name = format!("{}.{}_embeddings", schema, key);
485 let embedding_length = match &embed.model.runtime {
486 ModelRuntime::Python => {
487 let embedding: (Vec<f32>,) = sqlx::query_as(
488 "SELECT embedding from pgml.embed(transformer => $1, text => 'Hello, World!', kwargs => $2) as embedding")
489 .bind(&embed.model.name)
490 .bind(&embed.model.parameters)
491 .fetch_one(&mut **transaction).await?;
492 embedding.0.len() as i64
493 }
494 t => {
495 let remote_embeddings = build_remote_embeddings(
496 t.to_owned(),
497 &embed.model.name,
498 Some(&embed.model.parameters),
499 )?;
500 remote_embeddings.get_embedding_size().await?
501 }
502 };
503
504 sqlx::query(&query_builder!(
506 queries::CREATE_EMBEDDINGS_TABLE,
507 &embeddings_table_name,
508 chunks_table_name,
509 embedding_length
510 ))
511 .execute(&mut **transaction)
512 .await?;
513 let index_name = format!("{}_pipeline_embedding_chunk_id_index", key);
514 transaction
515 .execute(
516 query_builder!(
517 queries::CREATE_INDEX,
518 "",
519 index_name,
520 &embeddings_table_name,
521 "chunk_id"
522 )
523 .as_str(),
524 )
525 .await?;
526 let index_with_parameters = format!(
527 "WITH (m = {}, ef_construction = {})",
528 embed.hnsw.m, embed.hnsw.ef_construction
529 );
530 let index_name = format!("{}_pipeline_embedding_hnsw_vector_index", key);
531 transaction
532 .execute(
533 query_builder!(
534 queries::CREATE_INDEX_USING_HNSW,
535 "",
536 index_name,
537 &embeddings_table_name,
538 "embedding vector_cosine_ops",
539 index_with_parameters
540 )
541 .as_str(),
542 )
543 .await?;
544 }
545
546 if value.full_text_search.is_some() {
548 let tsvectors_table_name = format!("{}.{}_tsvectors", schema, key);
549 transaction
550 .execute(
551 query_builder!(
552 queries::CREATE_CHUNKS_TSVECTORS_TABLE,
553 tsvectors_table_name,
554 chunks_table_name
555 )
556 .as_str(),
557 )
558 .await?;
559 let index_name = format!("{}_pipeline_tsvector_chunk_id_index", key);
560 transaction
561 .execute(
562 query_builder!(
563 queries::CREATE_INDEX,
564 "",
565 index_name,
566 tsvectors_table_name,
567 "chunk_id"
568 )
569 .as_str(),
570 )
571 .await?;
572 let index_name = format!("{}_pipeline_tsvector_index", key);
573 transaction
574 .execute(
575 query_builder!(
576 queries::CREATE_INDEX_USING_GIN,
577 "",
578 index_name,
579 tsvectors_table_name,
580 "ts"
581 )
582 .as_str(),
583 )
584 .await?;
585 }
586 }
587 Ok(())
588 }
589
590 #[instrument(skip(self))]
591 pub(crate) async fn sync_documents(
592 &mut self,
593 document_ids: Vec<i64>,
594 project_info: &ProjectInfo,
595 transaction: &mut Transaction<'static, Postgres>,
596 ) -> anyhow::Result<()> {
597 let parsed_schema = self
599 .parsed_schema
600 .as_ref()
601 .context("Pipeline must have schema to execute")?;
602
603 for (key, value) in parsed_schema.iter() {
604 let chunk_ids = self
605 .sync_chunks_for_documents(
606 key,
607 value.splitter.as_ref().map(|v| &v.model),
608 &document_ids,
609 project_info,
610 transaction,
611 )
612 .await?;
613 if !chunk_ids.is_empty() {
614 if let Some(embed) = &value.semantic_search {
615 self.sync_embeddings_for_chunks(
616 key,
617 &embed.model,
618 &chunk_ids,
619 project_info,
620 transaction,
621 )
622 .await?;
623 }
624 if let Some(full_text_search) = &value.full_text_search {
625 self.sync_tsvectors_for_chunks(
626 key,
627 &full_text_search.configuration,
628 &chunk_ids,
629 project_info,
630 transaction,
631 )
632 .await?;
633 }
634 }
635 }
636 Ok(())
637 }
638
639 #[instrument(skip(self))]
640 async fn sync_chunks_for_documents(
641 &self,
642 key: &str,
643 splitter: Option<&Splitter>,
644 document_ids: &Vec<i64>,
645 project_info: &ProjectInfo,
646 transaction: &mut Transaction<'static, Postgres>,
647 ) -> anyhow::Result<Vec<i64>> {
648 let chunks_table_name = format!("{}_{}.{}_chunks", project_info.name, self.name, key);
649 let documents_table_name = format!("{}.documents", project_info.name);
650 let json_key_query = format!("document->>'{}'", key);
651
652 if let Some(splitter) = splitter {
653 let splitter_database_data = splitter
654 .database_data
655 .as_ref()
656 .context("Splitter must be verified to sync chunks")?;
657 let query = query_builder!(
658 queries::GENERATE_CHUNKS_FOR_DOCUMENT_IDS_WITH_SPLITTER,
659 &json_key_query,
660 documents_table_name,
661 &chunks_table_name,
662 &chunks_table_name,
663 &chunks_table_name
664 );
665 debug_sqlx_query!(
666 GENERATE_CHUNKS_FOR_DOCUMENT_IDS_WITH_SPLITTER,
667 query,
668 splitter_database_data.id,
669 document_ids
670 );
671 sqlx::query_scalar(&query)
672 .bind(splitter_database_data.id)
673 .bind(document_ids)
674 .fetch_all(&mut **transaction)
675 .await
676 .map_err(anyhow::Error::msg)
677 } else {
678 let query = query_builder!(
679 queries::GENERATE_CHUNKS_FOR_DOCUMENT_IDS,
680 &chunks_table_name,
681 &json_key_query,
682 &documents_table_name,
683 &chunks_table_name,
684 &json_key_query
685 );
686 debug_sqlx_query!(GENERATE_CHUNKS_FOR_DOCUMENT_IDS, query, document_ids);
687 sqlx::query_scalar(&query)
688 .bind(document_ids)
689 .fetch_all(&mut **transaction)
690 .await
691 .map_err(anyhow::Error::msg)
692 }
693 }
694
695 #[instrument(skip(self))]
696 async fn sync_embeddings_for_chunks(
697 &self,
698 key: &str,
699 model: &Model,
700 chunk_ids: &Vec<i64>,
701 project_info: &ProjectInfo,
702 transaction: &mut Transaction<'static, Postgres>,
703 ) -> anyhow::Result<()> {
704 let mut parameters = model.parameters.clone();
706 parameters
707 .as_object_mut()
708 .context("Model parameters must be an object")?
709 .remove("name");
710
711 let chunks_table_name = format!("{}_{}.{}_chunks", project_info.name, self.name, key);
712 let embeddings_table_name =
713 format!("{}_{}.{}_embeddings", project_info.name, self.name, key);
714
715 match model.runtime {
716 ModelRuntime::Python => {
717 let query = query_builder!(
718 queries::GENERATE_EMBEDDINGS_FOR_CHUNK_IDS,
719 embeddings_table_name,
720 chunks_table_name
721 );
722 debug_sqlx_query!(
723 GENERATE_EMBEDDINGS_FOR_CHUNK_IDS,
724 query,
725 model.name,
726 parameters.0,
727 chunk_ids
728 );
729 sqlx::query(&query)
730 .bind(&model.name)
731 .bind(¶meters)
732 .bind(chunk_ids)
733 .execute(&mut **transaction)
734 .await?;
735 }
736 r => {
737 let remote_embeddings = build_remote_embeddings(r, &model.name, Some(¶meters))?;
738 remote_embeddings
739 .generate_embeddings(
740 &embeddings_table_name,
741 &chunks_table_name,
742 Some(chunk_ids),
743 transaction,
744 )
745 .await?;
746 }
747 }
748 Ok(())
749 }
750
751 #[instrument(skip(self))]
752 async fn sync_tsvectors_for_chunks(
753 &self,
754 key: &str,
755 configuration: &str,
756 chunk_ids: &Vec<i64>,
757 project_info: &ProjectInfo,
758 transaction: &mut Transaction<'static, Postgres>,
759 ) -> anyhow::Result<()> {
760 let chunks_table_name = format!("{}_{}.{}_chunks", project_info.name, self.name, key);
761 let tsvectors_table_name = format!("{}_{}.{}_tsvectors", project_info.name, self.name, key);
762 let query = query_builder!(
763 queries::GENERATE_TSVECTORS_FOR_CHUNK_IDS,
764 tsvectors_table_name,
765 configuration,
766 chunks_table_name
767 );
768 debug_sqlx_query!(GENERATE_TSVECTORS_FOR_CHUNK_IDS, query, chunk_ids);
769 sqlx::query(&query)
770 .bind(chunk_ids)
771 .execute(&mut **transaction)
772 .await?;
773 Ok(())
774 }
775
776 #[instrument(skip(self))]
777 pub(crate) async fn resync(
778 &mut self,
779 project_info: &ProjectInfo,
780 connection: &mut PgConnection,
781 ) -> anyhow::Result<()> {
782 let parsed_schema = self
784 .parsed_schema
785 .as_ref()
786 .context("Pipeline must have schema to execute")?;
787 for (key, _value) in parsed_schema.iter() {
789 let chunks_table_name = format!("{}_{}.{}_chunks", project_info.name, self.name, key);
790 connection
791 .execute(query_builder!("DELETE FROM %s CASCADE", chunks_table_name).as_str())
792 .await?;
793 }
794 for (key, value) in parsed_schema.iter() {
795 self.resync_chunks(
796 key,
797 value.splitter.as_ref().map(|v| &v.model),
798 project_info,
799 connection,
800 )
801 .await?;
802 if let Some(embed) = &value.semantic_search {
803 self.resync_embeddings(key, &embed.model, project_info, connection)
804 .await?;
805 }
806 if let Some(full_text_search) = &value.full_text_search {
807 self.resync_tsvectors(
808 key,
809 &full_text_search.configuration,
810 project_info,
811 connection,
812 )
813 .await?;
814 }
815 }
816 Ok(())
817 }
818
819 #[instrument(skip(self))]
820 async fn resync_chunks(
821 &self,
822 key: &str,
823 splitter: Option<&Splitter>,
824 project_info: &ProjectInfo,
825 connection: &mut PgConnection,
826 ) -> anyhow::Result<()> {
827 let chunks_table_name = format!("{}_{}.{}_chunks", project_info.name, self.name, key);
828 let documents_table_name = format!("{}.documents", project_info.name);
829 let json_key_query = format!("document->>'{}'", key);
830
831 if let Some(splitter) = splitter {
832 let splitter_database_data = splitter
833 .database_data
834 .as_ref()
835 .context("Splitter must be verified to sync chunks")?;
836 let query = query_builder!(
837 queries::GENERATE_CHUNKS_WITH_SPLITTER,
838 &json_key_query,
839 &documents_table_name,
840 &chunks_table_name,
841 &chunks_table_name
842 );
843 debug_sqlx_query!(
844 GENERATE_CHUNKS_WITH_SPLITTER,
845 query,
846 splitter_database_data.id
847 );
848 sqlx::query(&query)
849 .bind(splitter_database_data.id)
850 .execute(connection)
851 .await?;
852 } else {
853 let query = query_builder!(
854 queries::GENERATE_CHUNKS,
855 &chunks_table_name,
856 &json_key_query,
857 &documents_table_name
858 );
859 debug_sqlx_query!(GENERATE_CHUNKS, query);
860 sqlx::query(&query).execute(connection).await?;
861 }
862 Ok(())
863 }
864
865 #[instrument(skip(self))]
866 async fn resync_embeddings(
867 &self,
868 key: &str,
869 model: &Model,
870 project_info: &ProjectInfo,
871 connection: &mut PgConnection,
872 ) -> anyhow::Result<()> {
873 let mut parameters = model.parameters.clone();
875 parameters
876 .as_object_mut()
877 .context("Model parameters must be an object")?
878 .remove("name");
879
880 let chunks_table_name = format!("{}_{}.{}_chunks", project_info.name, self.name, key);
881 let embeddings_table_name =
882 format!("{}_{}.{}_embeddings", project_info.name, self.name, key);
883
884 match model.runtime {
885 ModelRuntime::Python => {
886 let query = query_builder!(
887 queries::GENERATE_EMBEDDINGS,
888 embeddings_table_name,
889 chunks_table_name
890 );
891 debug_sqlx_query!(GENERATE_EMBEDDINGS, query, model.name, parameters.0);
892 sqlx::query(&query)
893 .bind(&model.name)
894 .bind(¶meters)
895 .execute(connection)
896 .await?;
897 }
898 r => {
899 let remote_embeddings = build_remote_embeddings(r, &model.name, Some(¶meters))?;
900 remote_embeddings
901 .generate_embeddings(
902 &embeddings_table_name,
903 &chunks_table_name,
904 None,
905 connection,
906 )
907 .await?;
908 }
909 }
910 Ok(())
911 }
912
913 #[instrument(skip(self))]
914 async fn resync_tsvectors(
915 &self,
916 key: &str,
917 configuration: &str,
918 project_info: &ProjectInfo,
919 connection: &mut PgConnection,
920 ) -> anyhow::Result<()> {
921 let chunks_table_name = format!("{}_{}.{}_chunks", project_info.name, self.name, key);
922 let tsvectors_table_name = format!("{}_{}.{}_tsvectors", project_info.name, self.name, key);
923
924 let query = query_builder!(
925 queries::GENERATE_TSVECTORS,
926 tsvectors_table_name,
927 configuration,
928 chunks_table_name
929 );
930 debug_sqlx_query!(GENERATE_TSVECTORS, query);
931 sqlx::query(&query).execute(connection).await?;
932 Ok(())
933 }
934
935 #[instrument(skip(self))]
936 pub(crate) async fn get_parsed_schema(
937 &mut self,
938 project_info: &ProjectInfo,
939 pool: &Pool<Postgres>,
940 ) -> anyhow::Result<ParsedSchema> {
941 self.verify_in_database(project_info, false, pool).await?;
942 Ok(self.parsed_schema.as_ref().unwrap().clone())
943 }
944
945 #[instrument]
946 pub(crate) async fn create_pipelines_table(
947 project_info: &ProjectInfo,
948 conn: &mut PgConnection,
949 ) -> anyhow::Result<()> {
950 let pipelines_table_name = format!("{}.pipelines", project_info.name);
951 sqlx::query(&query_builder!(
952 queries::PIPELINES_TABLE,
953 pipelines_table_name
954 ))
955 .execute(&mut *conn)
956 .await?;
957 conn.execute(
958 query_builder!(
959 queries::CREATE_INDEX,
960 "",
961 "pipeline_name_index",
962 pipelines_table_name,
963 "name"
964 )
965 .as_str(),
966 )
967 .await?;
968 Ok(())
969 }
970}
971
972impl TryFrom<models::Pipeline> for Pipeline {
973 type Error = anyhow::Error;
974 fn try_from(value: models::Pipeline) -> anyhow::Result<Self> {
975 let parsed_schema = json_to_schema(&value.schema).unwrap();
976 Ok(Self {
979 name: value.name,
980 schema: Some(value.schema),
981 parsed_schema: Some(parsed_schema),
982 database_data: None,
983 })
984 }
985}