Skip to content

Commit 648c382

Browse files
improve perf
1 parent a89eafe commit 648c382

File tree

6 files changed

+134
-18
lines changed

6 files changed

+134
-18
lines changed

Cargo.lock

Lines changed: 51 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

kb_core/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,4 @@ bat = { version = "0.25.0", features = ["build-assets"] }
2727
ignore = "0.4.23"
2828
sha2 = "0.10.9"
2929
hex = "0.4.3"
30+
futures = "0.3.31"

kb_core/src/cli/commands.rs

Lines changed: 40 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,18 @@ use crate::config;
33
use crate::embedding;
44
use crate::llm;
55
use crate::utils;
6-
use std::time::UNIX_EPOCH;
6+
use crate::state::{IndexState, IndexedChunk};
7+
use futures::stream::{FuturesUnordered, StreamExt};
8+
use std::time::{Duration, UNIX_EPOCH};
9+
use tokio::time::sleep;
710
use indicatif::{ProgressBar, ProgressStyle};
811
use reqwest::Client;
912
use std::fs;
1013
use std::path::Path;
1114
use uuid::Uuid;
1215

16+
const BATCH_SIZE: usize = 8;
17+
1318
pub fn handle_config(set_api_key: Option<String>, show: bool) -> anyhow::Result<()> {
1419
let config_path = dirs::config_dir()
1520
.ok_or_else(|| anyhow::anyhow!("Unable to determine config directory"))?
@@ -57,7 +62,7 @@ pub fn handle_config(set_api_key: Option<String>, show: bool) -> anyhow::Result<
5762
}
5863

5964
pub async fn handle_index(client: &Client, path: &Path) -> anyhow::Result<()> {
60-
let paths = crate::utils::collect_files(path)?;
65+
let paths = utils::collect_files(path)?;
6166
let total_files = paths.len() as u64;
6267
let pb = ProgressBar::new(total_files);
6368
pb.set_style(
@@ -66,16 +71,16 @@ pub async fn handle_index(client: &Client, path: &Path) -> anyhow::Result<()> {
6671
.tick_chars("⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏ "),
6772
);
6873

69-
let config_dir = crate::config::get_config_dir()?;
70-
let mut state = crate::state::IndexState::load(&config_dir)?;
74+
let config_dir = config::get_config_dir()?;
75+
let mut state = IndexState::load(&config_dir)?;
7176

7277
for path in paths {
7378
pb.set_message(format!("Indexing {}", path.display()));
7479
let metadata = fs::metadata(&path)?;
7580
let modified = metadata.modified()?.duration_since(UNIX_EPOCH)?.as_secs();
7681
let file_str = path.to_string_lossy().to_string();
7782

78-
// Skip if the file hasn't changed
83+
// Skip if file unchanged
7984
if let Some(prev) = state.get_last_modified(&file_str) {
8085
if prev == modified {
8186
pb.inc(1);
@@ -84,30 +89,50 @@ pub async fn handle_index(client: &Client, path: &Path) -> anyhow::Result<()> {
8489
}
8590

8691
let content = fs::read_to_string(&path)?;
87-
let chunks = crate::utils::chunk_text(&content);
92+
let chunks = utils::chunk_text(&content);
8893
let prev_chunks = state.get_file_chunks(&file_str).cloned().unwrap_or_default();
89-
let mut new_chunks = vec![];
94+
let mut new_chunks = Vec::new();
95+
let mut chunk_info = Vec::new();
9096

9197
for chunk in &chunks {
9298
if chunk.trim().is_empty() || chunk.len() > 100_000 {
9399
continue;
94100
}
95101

96-
let hash = crate::state::IndexState::hash_chunk(chunk);
97-
if crate::state::IndexState::has_chunk(&prev_chunks, &hash) {
102+
let hash = IndexState::hash_chunk(chunk);
103+
if IndexState::has_chunk(&prev_chunks, &hash) {
98104
continue;
99105
}
100106

101-
let id = Uuid::new_v4().to_string();
102-
let embedding = crate::embedding::get_embedding(client, chunk).await?;
103-
crate::chroma::send_to_chroma(client, &id, chunk, &embedding, &path, &pb).await?;
107+
chunk_info.push((chunk.clone(), hash));
108+
}
109+
110+
for batch in chunk_info.chunks(BATCH_SIZE) {
111+
let mut tasks = FuturesUnordered::new();
112+
113+
for (chunk, hash) in batch.iter().cloned() {
114+
let client = client.clone();
115+
let path = path.to_path_buf();
116+
let pb = pb.clone();
117+
tasks.push(async move {
118+
sleep(Duration::from_millis(100)).await;
119+
let embedding = embedding::get_embedding(&client, &chunk).await?;
120+
let id = Uuid::new_v4().to_string();
121+
chroma::send_to_chroma(&client, &id, &chunk, &embedding, &path, &pb).await?;
122+
Ok::<_, anyhow::Error>(IndexedChunk { id, hash })
123+
});
124+
}
104125

105-
new_chunks.push(crate::state::IndexedChunk { id, hash });
126+
while let Some(result) = tasks.next().await {
127+
if let Ok(chunk) = result {
128+
new_chunks.push(chunk);
129+
}
130+
}
106131
}
107132

108133
if !new_chunks.is_empty() {
109134
let mut updated_chunks = prev_chunks.clone();
110-
let mut removed_chunks = vec![];
135+
let mut removed_chunks = Vec::new();
111136

112137
updated_chunks.retain(|c| {
113138
let keep = new_chunks.iter().all(|n| n.hash != c.hash);
@@ -121,7 +146,7 @@ pub async fn handle_index(client: &Client, path: &Path) -> anyhow::Result<()> {
121146
state.update_file_chunks(&file_str, updated_chunks, modified);
122147

123148
for chunk in removed_chunks {
124-
crate::chroma::delete_chunk(client, &chunk.id).await?;
149+
chroma::delete_chunk(client, &chunk.id).await?;
125150
}
126151
}
127152

kb_core/src/embedding/mod.rs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,47 @@ struct EmbeddingData {
1818
embedding: Vec<f32>,
1919
}
2020

21+
pub async fn get_embeddings(client: &Client, texts: &[String]) -> anyhow::Result<Vec<Vec<f32>>> {
22+
let config = config::load_config()?;
23+
let api_key = config::get_openai_api_key()?;
24+
25+
let body = serde_json::json!({
26+
"model": config.openai_embedding_model,
27+
"input": texts
28+
});
29+
30+
let res = client
31+
.post("https://api.openai.com/v1/embeddings")
32+
.bearer_auth(api_key)
33+
.json(&body)
34+
.send()
35+
.await?;
36+
37+
let status = res.status();
38+
let text_body = res.text().await?;
39+
40+
if !status.is_success() {
41+
eprintln!("❌ OpenAI error: HTTP {} - {}", status, text_body);
42+
anyhow::bail!("Embedding batch failed");
43+
}
44+
45+
let parsed: serde_json::Value = serde_json::from_str(&text_body)?;
46+
let data = parsed["data"]
47+
.as_array()
48+
.ok_or_else(|| anyhow::anyhow!("Invalid embedding response format"))?;
49+
50+
Ok(data.iter()
51+
.map(|v| {
52+
v["embedding"]
53+
.as_array()
54+
.unwrap_or(&vec![])
55+
.iter()
56+
.map(|f| f.as_f64().unwrap_or_default() as f32)
57+
.collect()
58+
})
59+
.collect())
60+
}
61+
2162
pub async fn get_embedding(client: &Client, text: &str) -> anyhow::Result<Vec<f32>> {
2263
let config = config::load_config()?;
2364
let body = EmbeddingRequest {

kb_core/src/llm/mod.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,8 @@ pub async fn get_llm_response(
1212
let config_dir = config::get_config_dir()?;
1313
let mut state = QueryState::load(&config_dir)?;
1414

15-
// Generate context hash from context chunks
1615
let context_hash = hash_query_context(prompt, context_chunks);
1716

18-
// Return cached answer if it exists
1917
if let Some(cached) = state.get_cached_answer(prompt, &context_hash) {
2018
return Ok(cached);
2119
}

kb_core/src/utils/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ pub fn collect_files(root: &Path) -> anyhow::Result<Vec<PathBuf>> {
5858
}
5959
} else {
6060
let walker = WalkBuilder::new(root)
61-
.add_custom_ignore_filename(".kbignore") // Optional
61+
.add_custom_ignore_filename(".kbignore")
6262
.hidden(false)
6363
.build();
6464

0 commit comments

Comments
 (0)