llm_kernel/embedding/
qwen3.rs1use crate::embedding::types::{EmbeddingProvider, EmbeddingResult};
18use crate::error::{KernelError, Result};
19
20pub struct Qwen3Provider {
26 inner: fastembed::Qwen3TextEmbedding,
27 model_id: String,
28 dim: usize,
29}
30
31pub const QWEN3_EMBEDDING_0_6B: &str = "Qwen/Qwen3-Embedding-0.6B";
33
34pub const QWEN3_EMBEDDING_8B: &str = "Qwen/Qwen3-Embedding-8B";
36
37pub const QWEN3_VL_EMBEDDING_2B: &str = "Qwen/Qwen3-VL-Embedding-2B";
39
40const DEFAULT_MAX_LENGTH: usize = 512;
42
43impl Qwen3Provider {
44 pub fn new(model_id: &str) -> Result<Self> {
48 Self::with_options(
49 model_id,
50 candle_core::Device::Cpu,
51 candle_core::DType::F32,
52 DEFAULT_MAX_LENGTH,
53 )
54 }
55
56 pub fn with_options(
58 model_id: &str,
59 device: candle_core::Device,
60 dtype: candle_core::DType,
61 max_length: usize,
62 ) -> Result<Self> {
63 let te = fastembed::Qwen3TextEmbedding::from_hf(model_id, &device, dtype, max_length)
64 .map_err(KernelError::embedding)?;
65 let dim = te.config().hidden_size;
66 Ok(Self {
67 inner: te,
68 model_id: model_id.to_string(),
69 dim,
70 })
71 }
72
73 pub fn model_id(&self) -> &str {
75 &self.model_id
76 }
77}
78
79impl EmbeddingProvider for Qwen3Provider {
80 fn dim(&self) -> usize {
81 self.dim
82 }
83
84 fn name(&self) -> &str {
85 &self.model_id
86 }
87
88 fn embed(&self, text: &str) -> Result<EmbeddingResult> {
89 let embeddings = self.inner.embed(&[text]).map_err(KernelError::embedding)?;
90 let vector = embeddings
91 .into_iter()
92 .next()
93 .ok_or_else(|| KernelError::Embedding("empty embedding output".into()))?;
94
95 let preview = if text.len() > 64 {
96 format!("{}…", &text[..64])
97 } else {
98 text.to_string()
99 };
100 Ok(EmbeddingResult {
101 vector,
102 text_preview: preview,
103 })
104 }
105
106 fn embed_batch(&self, texts: &[&str]) -> Result<Vec<EmbeddingResult>> {
107 if texts.is_empty() {
108 return Ok(vec![]);
109 }
110 let embeddings = self.inner.embed(texts).map_err(KernelError::embedding)?;
111 Ok(embeddings
112 .into_iter()
113 .zip(texts.iter())
114 .map(|(vector, &text)| {
115 let preview = if text.len() > 64 {
116 format!("{}…", &text[..64])
117 } else {
118 text.to_string()
119 };
120 EmbeddingResult {
121 vector,
122 text_preview: preview,
123 }
124 })
125 .collect())
126 }
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132
133 #[test]
134 fn model_id_constants() {
135 assert_eq!(QWEN3_EMBEDDING_0_6B, "Qwen/Qwen3-Embedding-0.6B");
136 assert_eq!(QWEN3_EMBEDDING_8B, "Qwen/Qwen3-Embedding-8B");
137 assert_eq!(QWEN3_VL_EMBEDDING_2B, "Qwen/Qwen3-VL-Embedding-2B");
138 }
139
140 #[test]
141 #[ignore = "requires model download"]
142 fn embed_with_qwen3_0_6b() {
143 let provider = Qwen3Provider::new(QWEN3_EMBEDDING_0_6B).unwrap();
144 let result = provider.embed("hello world").unwrap();
145 assert!(!result.vector.is_empty());
147 assert_eq!(result.vector.len(), provider.dim());
148 }
149
150 #[test]
151 #[ignore = "requires model download"]
152 fn embed_batch_with_qwen3() {
153 let provider = Qwen3Provider::new(QWEN3_EMBEDDING_0_6B).unwrap();
154 let results = provider
155 .embed_batch(&["hello", "world", "foo bar"])
156 .unwrap();
157 assert_eq!(results.len(), 3);
158 for r in &results {
159 assert_eq!(r.vector.len(), provider.dim());
160 }
161 }
162}