use crate::embed::protocol::{BatchId, EmbedRequest, EmbedResponse};
#[derive(Debug, Clone)]
pub struct BatchConfig {
pub max_frame_size: usize,
pub max_text_size: usize,
}
impl Default for BatchConfig {
fn default() -> Self {
Self {
max_frame_size: 16 * 1024 * 1024, max_text_size: 1024 * 1024, }
}
}
#[derive(Debug, Clone)]
pub struct SubBatch {
pub batch_id: BatchId,
pub index: usize,
pub total: usize,
pub request: EmbedRequest,
}
#[derive(Debug)]
pub enum SplitResult {
Single(EmbedRequest),
Split(Vec<SubBatch>),
}
#[derive(Debug)]
pub struct StitchedResponse {
pub vectors: Vec<f32>,
pub count: usize,
pub dimension: usize,
}
pub fn split_request(
batch_id: BatchId,
request: EmbedRequest,
config: &BatchConfig,
) -> SplitResult {
let texts: Vec<String> = request
.texts
.into_iter()
.map(|t| truncate_text(t, config.max_text_size))
.collect();
if texts.is_empty() {
return SplitResult::Single(EmbedRequest {
texts: vec![],
expected_dim: request.expected_dim,
});
}
let estimated_size = estimate_request_size(&texts);
if estimated_size <= config.max_frame_size {
return SplitResult::Single(EmbedRequest {
texts,
expected_dim: request.expected_dim,
});
}
let mut sub_batches: Vec<Vec<String>> = Vec::new();
let mut current_texts = Vec::new();
let mut current_size = 0usize;
for text in texts {
let text_size = text.len() + 16;
if !current_texts.is_empty() && current_size + text_size > config.max_frame_size {
sub_batches.push(std::mem::take(&mut current_texts));
current_size = 0;
}
let text = if current_texts.is_empty() && text_size > config.max_frame_size {
let max_content = config.max_frame_size.saturating_sub(16); tracing::warn!(
original_len = text.len(),
truncated_to = max_content,
"single text exceeds max_frame_size, truncating further"
);
truncate_text(text, max_content)
} else {
text
};
let text_size = text.len() + 16;
current_texts.push(text);
current_size += text_size;
}
if !current_texts.is_empty() {
sub_batches.push(current_texts);
}
if sub_batches.len() <= 1 {
let texts = sub_batches.into_iter().next().unwrap_or_default();
return SplitResult::Single(EmbedRequest {
texts,
expected_dim: request.expected_dim,
});
}
let total = sub_batches.len();
let sub_batches: Vec<SubBatch> = sub_batches
.into_iter()
.enumerate()
.map(|(index, texts)| SubBatch {
batch_id,
index,
total,
request: EmbedRequest {
texts,
expected_dim: request.expected_dim,
},
})
.collect();
SplitResult::Split(sub_batches)
}
pub fn stitch_responses(responses: Vec<EmbedResponse>) -> anyhow::Result<StitchedResponse> {
if responses.is_empty() {
return Ok(StitchedResponse {
vectors: vec![],
count: 0,
dimension: 0,
});
}
let dimension = responses[0].dimension;
let total_elements: usize = responses.iter().map(|r| r.vectors.len()).sum();
let mut all_vectors = Vec::with_capacity(total_elements);
let mut total_count = 0;
for response in responses {
if response.dimension != dimension {
anyhow::bail!(
"dimension mismatch during response stitching: expected {}, got {}",
dimension,
response.dimension
);
}
all_vectors.extend(response.vectors);
total_count += response.count;
}
Ok(StitchedResponse {
vectors: all_vectors,
count: total_count,
dimension,
})
}
pub fn truncate_text(text: String, max_size: usize) -> String {
if text.len() <= max_size {
return text;
}
let mut end = max_size;
while !text.is_char_boundary(end) && end > 0 {
end -= 1;
}
tracing::warn!(
original_len = text.len(),
truncated_len = end,
"truncated oversized text before IPC framing"
);
text[..end].to_string()
}
const PER_TEXT_SERIALIZE_OVERHEAD: usize = 16;
const FRAME_HEADER_OVERHEAD: usize = 128;
fn estimate_request_size(texts: &[String]) -> usize {
let text_bytes: usize = texts.iter().map(|t| t.len()).sum();
let overhead = texts.len() * PER_TEXT_SERIALIZE_OVERHEAD + FRAME_HEADER_OVERHEAD;
text_bytes + overhead
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_split_request_small_batch() {
let config = BatchConfig::default();
let request = EmbedRequest {
texts: vec!["hello".to_string(), "world".to_string()],
expected_dim: 4,
};
let result = split_request(BatchId::new(1), request, &config);
match result {
SplitResult::Single(req) => {
assert_eq!(req.texts.len(), 2);
assert_eq!(req.expected_dim, 4);
}
SplitResult::Split(_) => panic!("small batch should not be split"),
}
}
#[test]
fn test_split_request_empty_batch() {
let config = BatchConfig::default();
let request = EmbedRequest {
texts: vec![],
expected_dim: 4,
};
let result = split_request(BatchId::new(1), request, &config);
match result {
SplitResult::Single(req) => {
assert!(req.texts.is_empty());
}
SplitResult::Split(_) => panic!("empty batch should not be split"),
}
}
#[test]
fn test_split_request_oversized_batch() {
let config = BatchConfig {
max_frame_size: 200, max_text_size: 1024,
};
let texts: Vec<String> = (0..20)
.map(|i| format!("text number {} with some padding", i))
.collect();
let request = EmbedRequest {
texts,
expected_dim: 4,
};
let result = split_request(BatchId::new(1), request, &config);
match result {
SplitResult::Single(_) => {
}
SplitResult::Split(sub_batches) => {
assert!(sub_batches.len() > 1);
for sb in &sub_batches {
assert_eq!(sb.batch_id, BatchId::new(1));
}
let total_texts: usize = sub_batches.iter().map(|sb| sb.request.texts.len()).sum();
assert_eq!(total_texts, 20);
}
}
}
#[test]
fn test_split_request_preserves_batch_id() {
let config = BatchConfig {
max_frame_size: 100,
max_text_size: 1024,
};
let texts: Vec<String> = (0..10)
.map(|i| format!("a somewhat longer text number {} here", i))
.collect();
let request = EmbedRequest {
texts,
expected_dim: 8,
};
let batch_id = BatchId::new(42);
let result = split_request(batch_id, request, &config);
if let SplitResult::Split(sub_batches) = result {
for sb in &sub_batches {
assert_eq!(sb.batch_id, batch_id);
assert_eq!(sb.request.expected_dim, 8);
}
}
}
#[test]
fn test_stitch_responses_empty() {
let result = stitch_responses(vec![]).unwrap();
assert_eq!(result.count, 0);
assert!(result.vectors.is_empty());
}
#[test]
fn test_stitch_responses_single() {
let response = EmbedResponse::new(vec![1.0, 2.0, 3.0, 4.0], 1, 4);
let result = stitch_responses(vec![response]).unwrap();
assert_eq!(result.count, 1);
assert_eq!(result.dimension, 4);
assert_eq!(result.vectors, vec![1.0, 2.0, 3.0, 4.0]);
}
#[test]
fn test_stitch_responses_multiple() {
let r1 = EmbedResponse::new(vec![1.0, 2.0, 3.0, 4.0], 1, 4);
let r2 = EmbedResponse::new(vec![5.0, 6.0, 7.0, 8.0], 1, 4);
let r3 = EmbedResponse::new(vec![9.0, 10.0, 11.0, 12.0], 1, 4);
let result = stitch_responses(vec![r1, r2, r3]).unwrap();
assert_eq!(result.count, 3);
assert_eq!(result.dimension, 4);
assert_eq!(
result.vectors,
vec![
1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0
]
);
}
#[test]
fn test_stitch_preserves_ordering() {
let r1 = EmbedResponse::new(vec![1.0, 2.0], 1, 2);
let r2 = EmbedResponse::new(vec![3.0, 4.0], 1, 2);
let r3 = EmbedResponse::new(vec![5.0, 6.0], 1, 2);
let result = stitch_responses(vec![r1, r2, r3]).unwrap();
assert_eq!(result.vectors, vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
}
#[test]
fn test_truncate_text_within_limit() {
let result = truncate_text("hello".to_string(), 100);
assert_eq!(result, "hello");
}
#[test]
fn test_truncate_text_exceeds_limit() {
let result = truncate_text("hello world".to_string(), 5);
assert_eq!(result, "hello");
}
#[test]
fn test_truncate_text_unicode_boundary() {
let result = truncate_text("héllo wörld".to_string(), 4);
assert!(result.len() <= 4);
assert!(result.is_char_boundary(result.len()));
}
#[test]
fn test_truncate_text_exact_boundary() {
let result = truncate_text("hello".to_string(), 5);
assert_eq!(result, "hello");
}
#[test]
fn test_estimate_request_size() {
let texts = vec!["hello".to_string(), "world".to_string()];
let size = estimate_request_size(&texts);
assert!(size > 0);
assert!(size < 1000); }
#[test]
fn test_batch_config_default() {
let config = BatchConfig::default();
assert_eq!(config.max_frame_size, 16 * 1024 * 1024);
assert_eq!(config.max_text_size, 1024 * 1024);
}
#[test]
fn test_split_and_stitch_roundtrip() {
let config = BatchConfig {
max_frame_size: 200,
max_text_size: 1024,
};
let texts: Vec<String> = (0..20)
.map(|i| format!("text number {} with enough content to matter", i))
.collect();
let dim = 4;
let request = EmbedRequest {
texts: texts.clone(),
expected_dim: dim,
};
let batch_id = BatchId::new(99);
let split = split_request(batch_id, request, &config);
match split {
SplitResult::Single(req) => {
assert_eq!(req.texts.len(), texts.len());
}
SplitResult::Split(sub_batches) => {
let responses: Vec<EmbedResponse> = sub_batches
.iter()
.map(|sb| {
let count = sb.request.texts.len();
EmbedResponse::new(vec![0.0f32; count * dim], count, dim)
})
.collect();
let stitched = stitch_responses(responses).unwrap();
assert_eq!(stitched.count, texts.len());
assert_eq!(stitched.dimension, dim);
assert_eq!(stitched.vectors.len(), texts.len() * dim);
}
}
}
}