Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [4.7.0] - 2026-03-30


### Added

- **Semantic chunk labeling** (#600): Chunks now include a `chunk_type` field identifying the semantic nature of the content (e.g., `paragraph`, `heading`, `list_item`, `table_cell`, `code_block`). Supported across all 11 language bindings with updated E2E test parity.
- **Unified InternalDocument architecture**: All extractors now return a canonical `InternalDocument` with typed elements, relationships, images, and tables. Replaces format-specific intermediate representations.
- **Unified rendering layer**: New `new_markdown.rs` renderer produces CommonMark from `InternalDocument`, supporting headings, lists, tables, code blocks, formulas, footnotes, images, and inline annotations (bold, italic, links).
- **PDF structure pipeline**: Full rewrite of PDF extraction using `page.text().all()` for clean text, char-indexed font metadata for heading/bold detection, segment-based paragraph gap detection, and pdfium segment bounding boxes for precise paragraph regions.
Expand Down
1 change: 1 addition & 0 deletions crates/kreuzberg-cli/src/commands/embed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ pub fn embed_command(texts: Vec<String>, preset: &str, format: WireFormat) -> Re
.enumerate()
.map(|(idx, text)| Chunk {
content: text.clone(),
chunk_type: Default::default(),
embedding: None,
metadata: ChunkMetadata {
byte_start: 0,
Expand Down
1 change: 1 addition & 0 deletions crates/kreuzberg-ffi/benches/result_view_benchmark.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ fn create_test_result(content_size: usize, chunk_count: usize) -> ExtractionResu
};
Chunk {
content: content[start..end].to_string(),
chunk_type: Default::default(),
embedding: None,
metadata: ChunkMetadata {
byte_start: start,
Expand Down
1 change: 1 addition & 0 deletions crates/kreuzberg-ffi/src/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -679,6 +679,7 @@ mod tests {

let chunk = Chunk {
content: "Chunk content".to_string(),
chunk_type: Default::default(),
embedding: None,
metadata: ChunkMetadata {
byte_start: 0,
Expand Down
2 changes: 2 additions & 0 deletions crates/kreuzberg-ffi/src/result.rs
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,7 @@ mod tests {
chunks: Some(vec![
kreuzberg::types::Chunk {
content: "Chunk 1".to_string(),
chunk_type: Default::default(),
embedding: None,
metadata: kreuzberg::types::ChunkMetadata {
byte_start: 0,
Expand All @@ -411,6 +412,7 @@ mod tests {
},
kreuzberg::types::Chunk {
content: "Chunk 2".to_string(),
chunk_type: Default::default(),
embedding: None,
metadata: kreuzberg::types::ChunkMetadata {
byte_start: 8,
Expand Down
2 changes: 2 additions & 0 deletions crates/kreuzberg-ffi/src/result_view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,7 @@ mod tests {
chunks: Some(vec![
kreuzberg::types::Chunk {
content: "Chunk 1".to_string(),
chunk_type: Default::default(),
embedding: None,
metadata: kreuzberg::types::ChunkMetadata {
byte_start: 0,
Expand All @@ -437,6 +438,7 @@ mod tests {
},
kreuzberg::types::Chunk {
content: "Chunk 2".to_string(),
chunk_type: Default::default(),
embedding: None,
metadata: kreuzberg::types::ChunkMetadata {
byte_start: 8,
Expand Down
7 changes: 7 additions & 0 deletions crates/kreuzberg-node/src/result.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ pub struct JsChunkMetadata {
#[derive(serde::Serialize, serde::Deserialize, Clone)]
pub struct JsChunk {
pub content: String,
pub chunk_type: String,
#[napi(ts_type = "number[] | undefined")]
pub embedding: Option<Vec<f64>>,
pub metadata: JsChunkMetadata,
Expand Down Expand Up @@ -587,6 +588,10 @@ impl TryFrom<RustExtractionResult> for JsExtractionResult {

js_chunks.push(JsChunk {
content: chunk.content,
chunk_type: serde_json::to_value(chunk.chunk_type)
.ok()
.and_then(|v| v.as_str().map(String::from))
.unwrap_or_else(|| "unknown".to_string()),
embedding,
metadata,
});
Expand Down Expand Up @@ -759,6 +764,8 @@ impl TryFrom<JsExtractionResult> for RustExtractionResult {

rust_chunks.push(RustChunk {
content: chunk.content,
chunk_type: serde_json::from_value(serde_json::Value::String(chunk.chunk_type))
.unwrap_or_default(),
embedding,
metadata: RustChunkMetadata {
byte_start: chunk.metadata.byte_start as usize,
Expand Down
6 changes: 6 additions & 0 deletions crates/kreuzberg-php/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1227,6 +1227,8 @@ pub struct TextChunk {
#[php(prop)]
pub content: String,
#[php(prop)]
pub chunk_type: String,
#[php(prop)]
pub embedding: Option<Vec<f32>>,
pub metadata: ChunkMetadata,
}
Expand All @@ -1244,6 +1246,10 @@ impl TextChunk {
pub fn from_rust(chunk: kreuzberg::Chunk) -> PhpResult<Self> {
Ok(Self {
content: chunk.content,
chunk_type: serde_json::to_value(chunk.chunk_type)
.ok()
.and_then(|v| v.as_str().map(String::from))
.unwrap_or_else(|| "unknown".to_string()),
embedding: chunk.embedding,
metadata: ChunkMetadata::from_rust(chunk.metadata)?,
})
Expand Down
7 changes: 7 additions & 0 deletions crates/kreuzberg-py/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,10 @@ impl ExtractionResult {

let py_chunk = PyChunk {
content: chunk.content,
chunk_type: serde_json::to_value(chunk.chunk_type)
.ok()
.and_then(|v| v.as_str().map(String::from))
.unwrap_or_else(|| "unknown".to_string()),
embedding,
metadata: chunk_metadata_dict.unbind(),
};
Expand Down Expand Up @@ -869,6 +873,9 @@ pub struct PyChunk {
#[pyo3(get)]
pub content: String,

#[pyo3(get)]
pub chunk_type: String,

#[pyo3(get)]
pub embedding: Option<Py<PyList>>,

Expand Down
1 change: 1 addition & 0 deletions crates/kreuzberg/src/api/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,7 @@ pub async fn embed_handler(JsonApi(request): JsonApi<EmbedRequest>) -> Result<Js
.enumerate()
.map(|(idx, text)| Chunk {
content: text.clone(),
chunk_type: Default::default(),
embedding: None,
metadata: ChunkMetadata {
byte_start: 0,
Expand Down
2 changes: 2 additions & 0 deletions crates/kreuzberg/src/chunking/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use crate::types::{Chunk, ChunkMetadata, PageBoundary};
use text_splitter::{Characters, ChunkCapacity, ChunkConfig};

use super::boundaries::calculate_page_range;
use super::classifier::classify_chunk;

/// Build a ChunkConfig from chunking parameters.
///
Expand Down Expand Up @@ -78,6 +79,7 @@ where

chunks.push(Chunk {
content: chunk_text.to_string(),
chunk_type: classify_chunk(chunk_text, None),
embedding: None,
metadata: ChunkMetadata {
byte_start,
Expand Down
Loading
Loading