elph_core/floppy/
builder.rs1use anyhow::{Result, bail};
2
3use super::embed::{FastEmbedOptions, create_fastembed};
4
5#[cfg(feature = "fastembed")]
6use super::embed::DEFAULT_EMBED_MODEL;
7#[cfg(feature = "fastembed")]
8use super::embed::{embedding_dims, resolve_embedding_model};
9use super::paths::FloppyPaths;
10use super::store::{EmbedFn, MemoryStore, noop_embedder};
11use super::types::{FloppyConfig, VectorType};
12use super::util::DEFAULT_EMBEDDING_DIMS;
13
14pub struct FloppyBuilder {
16 config: FloppyConfig,
17 custom_embed: Option<EmbedFn>,
18 fastembed_opts: Option<FastEmbedOptions>,
19}
20
21impl FloppyBuilder {
22 pub fn new(db_path: impl Into<String>, session_id: impl Into<String>) -> Self {
23 Self {
24 config: FloppyConfig::new(db_path, session_id),
25 custom_embed: None,
26 fastembed_opts: None,
27 }
28 }
29
30 pub fn from_paths(paths: &FloppyPaths, session_id: impl Into<String>) -> Self {
31 Self::new(paths.db_path_string(), session_id)
32 }
33
34 pub fn vector_type(mut self, vector_type: VectorType) -> Self {
35 self.config = self.config.vector_type(vector_type);
36 self
37 }
38
39 pub fn dimensions(mut self, dimensions: u32) -> Self {
40 self.config = self.config.dimensions(dimensions);
41 self
42 }
43
44 pub fn top_k(mut self, top_k: u32) -> Self {
45 self.config = self.config.top_k(top_k);
46 self
47 }
48
49 pub fn learning_rate(mut self, learning_rate: f64) -> Self {
50 self.config = self.config.learning_rate(learning_rate);
51 self
52 }
53
54 pub fn decay_rate(mut self, decay_rate: f64) -> Self {
55 self.config = self.config.decay_rate(decay_rate);
56 self
57 }
58
59 pub fn apply_migrations(mut self, apply: bool) -> Self {
61 self.config = self.config.apply_migrations(apply);
62 self
63 }
64
65 pub fn embed(mut self, embed: EmbedFn) -> Self {
67 self.custom_embed = Some(embed);
68 self.fastembed_opts = None;
69 self
70 }
71
72 pub fn noop_embed(mut self) -> Self {
74 let dims = self.config.dimensions.unwrap_or(DEFAULT_EMBEDDING_DIMS);
75 self.custom_embed = Some(noop_embedder(dims));
76 self.fastembed_opts = None;
77 self
78 }
79
80 #[cfg(feature = "fastembed")]
82 pub fn fastembed(mut self, options: FastEmbedOptions) -> Result<Self> {
83 let model_name = options.model.as_deref().unwrap_or(DEFAULT_EMBED_MODEL);
84 let dims = resolve_embedding_model(model_name, options.quantized)
85 .map(|m| embedding_dims(&m))
86 .map_err(|e| anyhow::anyhow!("{e}"))?;
87 self.config = self.config.dimensions(dims);
88 self.fastembed_opts = Some(options);
89 self.custom_embed = None;
90 Ok(self)
91 }
92
93 #[cfg(not(feature = "fastembed"))]
94 pub fn fastembed(self, _options: FastEmbedOptions) -> Result<Self> {
95 bail!("fastembed backend requires the `fastembed` feature on this crate");
96 }
97
98 pub fn build(self) -> Result<MemoryStore> {
99 let embed = match (self.custom_embed, self.fastembed_opts) {
100 (Some(e), None) => e,
101 (None, Some(opts)) => create_fastembed(opts)?,
102 (None, None) => {
103 bail!("embedder required: call .embed(), .noop_embed(), or .fastembed()");
104 }
105 (Some(_), Some(_)) => bail!("cannot set both a custom embedder and fastembed options"),
106 };
107 Ok(MemoryStore::new(self.config, embed))
108 }
109}
110
111impl FloppyPaths {
112 pub fn builder(&self, session_id: impl Into<String>) -> FloppyBuilder {
114 FloppyBuilder::from_paths(self, session_id)
115 }
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121 use std::sync::Arc;
122
123 fn mock_embed() -> EmbedFn {
124 Arc::new(|_| Box::pin(async { Ok(vec![1.0, 0.0, 0.0, 0.0]) }))
125 }
126
127 #[test]
128 fn builder_requires_embedder() {
129 let dir = tempfile::tempdir().expect("tempdir");
130 let db = dir.path().join("t.db").to_string_lossy().into_owned();
131 match FloppyBuilder::new(db, "s").build() {
132 Err(e) => assert!(e.to_string().contains("embedder required")),
133 Ok(_) => panic!("expected embedder required error"),
134 }
135 }
136
137 #[test]
138 fn builder_with_custom_embed() {
139 let dir = tempfile::tempdir().expect("tempdir");
140 let db = dir.path().join("t.db").to_string_lossy().into_owned();
141 let store = FloppyBuilder::new(db, "s")
142 .dimensions(4)
143 .embed(mock_embed())
144 .build()
145 .expect("build");
146 assert_eq!(store.dimensions(), 4);
147 }
148
149 #[test]
150 fn from_paths_sets_db_location() {
151 let paths = FloppyPaths::project_local();
152 let store = paths.builder("sess").dimensions(4).noop_embed().build().expect("build");
153 assert_eq!(store.dimensions(), 4);
154 }
155}