rishik1111's picture
feat: Add VisionQuest Indic RAG core services, vector search, safety guardrails & web UI
83dc8bf
Raw
History Blame Contribute Delete
5.08 kB
"""
Semantic Chunking Strategy.
For long documents:
1. Splits text into sentences.
2. Computes consecutive sentence embedding cosine distances using the embedding model.
3. Splits at distance spikes (topic transitions) rather than arbitrary token counts.
4. Applies 10-20% token overlap across semantic chunk boundaries.
"""
from typing import Any, Dict, List, Optional
import numpy as np
import config
from doc_chunking.metadata import (
Chunk,
split_sentences_multilingual,
calculate_overlap_tokens,
estimate_token_count,
)
def cosine_distance(vec_a: np.ndarray, vec_b: np.ndarray) -> float:
"""Compute cosine distance between two 1D vectors."""
dot = np.dot(vec_a, vec_b)
norm_a = np.linalg.norm(vec_a)
norm_b = np.linalg.norm(vec_b)
if norm_a == 0 or norm_b == 0:
return 1.0
similarity = dot / (norm_a * norm_b)
return float(max(0.0, min(2.0, 1.0 - similarity)))
def chunk_document_semantic(
doc_dict: Dict[str, Any],
embedder_func=None,
distance_threshold: float = config.SEMANTIC_SIMILARITY_THRESHOLD,
) -> List[Chunk]:
"""
Split a document based on embedding cosine distance spikes across consecutive sentences.
"""
full_text = doc_dict.get("text", "")
doc_id = doc_dict.get("doc_id", "doc_unknown")
source_lang = doc_dict.get("source_lang", "en")
title = doc_dict.get("title", "")
sentences = split_sentences_multilingual(full_text)
if not sentences:
return []
if len(sentences) == 1:
text = sentences[0]
return [
Chunk(
chunk_id=f"{doc_id}_sem_0000",
text=text,
embed_text=text,
chunk_strategy="semantic",
source_lang=source_lang,
token_count=estimate_token_count(text),
doc_id=doc_id,
metadata={"title": title, "cluster_id": 0},
)
]
# If embedder_func is provided, compute true sentence embeddings;
# otherwise fallback to token-based lexical similarity
sentence_vectors = None
if embedder_func is not None:
try:
# embedder_func accepts a list of texts and returns numpy array (N, D)
sentence_vectors = embedder_func(sentences)
except Exception:
sentence_vectors = None
# Identify split boundaries
split_indices = [0]
if sentence_vectors is not None and len(sentence_vectors) == len(sentences):
distances = []
for i in range(len(sentences) - 1):
d = cosine_distance(sentence_vectors[i], sentence_vectors[i + 1])
distances.append(d)
# Calculate dynamic threshold if distances exist
if distances:
mean_d = float(np.mean(distances))
std_d = float(np.std(distances))
dynamic_thresh = max(distance_threshold, mean_d + 0.5 * std_d)
for i, d in enumerate(distances):
if d >= dynamic_thresh:
split_indices.append(i + 1)
else:
# Fallback heuristic: paragraph or length-based boundary detection
cur_len = 0
for i, s in enumerate(sentences):
cur_len += len(s.split())
if cur_len >= 80 and i > 0:
split_indices.append(i)
cur_len = 0
if split_indices[-1] != len(sentences):
split_indices.append(len(sentences))
# Group sentences into semantic chunks and attach token overlap
chunks: List[Chunk] = []
prev_tail_overlap = ""
for idx in range(len(split_indices) - 1):
start_i = split_indices[idx]
end_i = split_indices[idx + 1]
group_sentences = sentences[start_i:end_i]
raw_group_text = " ".join(group_sentences)
if prev_tail_overlap:
chunk_text = f"{prev_tail_overlap} {raw_group_text}"
else:
chunk_text = raw_group_text
chunk_id = f"{doc_id}_sem_{idx:04d}"
chunk = Chunk(
chunk_id=chunk_id,
text=chunk_text,
embed_text=raw_group_text,
chunk_strategy="semantic",
source_lang=source_lang,
token_count=estimate_token_count(chunk_text),
doc_id=doc_id,
metadata={
"cluster_index": idx,
"title": title,
"sentence_range": [start_i, end_i],
},
)
chunks.append(chunk)
prev_tail_overlap = calculate_overlap_tokens(
raw_group_text, overlap_percent=config.CHUNK_OVERLAP_PERCENT
)
return chunks
def process_longdocs_semantic(
longdocs: List[Dict[str, Any]], embedder_func=None
) -> List[Chunk]:
"""
Process long documents using semantic boundary splitting.
"""
all_chunks = []
for doc in longdocs:
all_chunks.extend(chunk_document_semantic(doc, embedder_func=embedder_func))
return all_chunks