chunking_test / modules /chunker.py
leilaghomashchi's picture
Upload 5 files
dfbf6c3 verified
"""
🔪 تقسیم متن به Chunks
Smart text chunking with overlap for large documents
"""
import logging
from typing import List, Dict
from .utils import count_tokens, split_sentences, get_last_n_tokens
logger = logging.getLogger(__name__)
class TextChunker:
"""
کلاس برای تقسیم هوشمند متن به chunks
ویژگی‌ها:
- تقسیم بر اساس تعداد tokens (نه کاراکتر)
- همپوشانی (overlap) بین chunks برای جلوگیری از split شدن entities
- تقسیم بر اساس جملات (نه کاراکتر) برای حفظ معنا
- metadata کامل برای هر chunk
"""
def __init__(
self,
chunk_size: int = 1000, # tokens
overlap: int = 150 # tokens
):
"""
مقداردهی اولیه chunker
Args:
chunk_size: حداکثر سایز هر chunk به tokens
overlap: تعداد tokens همپوشانی بین chunks
Examples:
>>> chunker = TextChunker(chunk_size=1000, overlap=150)
"""
if chunk_size <= 0:
raise ValueError("chunk_size باید بزرگتر از 0 باشد")
if overlap < 0:
raise ValueError("overlap نمی‌تواند منفی باشد")
if overlap >= chunk_size:
raise ValueError("overlap باید کوچکتر از chunk_size باشد")
self.chunk_size = chunk_size
self.overlap = overlap
logger.info(
f"✅ TextChunker initialized: "
f"chunk_size={chunk_size} tokens, "
f"overlap={overlap} tokens"
)
def create_chunks(self, text: str) -> List[Dict]:
"""
تقسیم متن به chunks با همپوشانی
Args:
text: متن ورودی
Returns:
لیستی از chunks با metadata:
[
{
"chunk_id": "chunk_01",
"text": "...",
"start_char": 0,
"end_char": 5000,
"tokens": 1000
},
...
]
Examples:
>>> chunker = TextChunker(chunk_size=500, overlap=100)
>>> chunks = chunker.create_chunks("متن بلند...")
>>> len(chunks)
3
"""
if not text or not text.strip():
logger.warning("⚠️ متن خالی - بازگشت لیست خالی")
return []
logger.info("🔪 شروع chunking...")
logger.info(f"📏 طول متن: {len(text)} کاراکتر")
# تقسیم به جملات
sentences = split_sentences(text)
if not sentences:
# اگر جمله‌ای شناسایی نشد، کل متن را یک chunk در نظر بگیر
logger.warning("⚠️ جمله‌ای شناسایی نشد - کل متن یک chunk")
return [self._create_chunk_metadata(
chunk_id=1,
text=text,
start_char=0,
end_char=len(text),
tokens=count_tokens(text)
)]
logger.info(f"📝 تعداد جملات: {len(sentences)}")
chunks = []
current_chunk = ""
current_tokens = 0
current_start_char = 0
for sentence in sentences:
sentence_tokens = count_tokens(sentence)
# آیا اضافه کردن این جمله chunk را بیش از حد بزرگ می‌کند؟
if current_tokens + sentence_tokens > self.chunk_size and current_chunk:
# ذخیره chunk فعلی
chunk_text = current_chunk.strip()
chunks.append(self._create_chunk_metadata(
chunk_id=len(chunks) + 1,
text=chunk_text,
start_char=current_start_char,
end_char=current_start_char + len(chunk_text),
tokens=current_tokens
))
# شروع chunk جدید با overlap
overlap_text = get_last_n_tokens(current_chunk, self.overlap)
# محاسبه موقعیت شروع جدید
current_start_char = current_start_char + len(current_chunk) - len(overlap_text)
# شروع chunk جدید
current_chunk = overlap_text + " " + sentence
current_tokens = count_tokens(current_chunk)
else:
# اضافه کردن جمله به chunk فعلی
if current_chunk:
current_chunk += " "
current_chunk += sentence
current_tokens += sentence_tokens
# ذخیره chunk آخر
if current_chunk:
chunk_text = current_chunk.strip()
chunks.append(self._create_chunk_metadata(
chunk_id=len(chunks) + 1,
text=chunk_text,
start_char=current_start_char,
end_char=current_start_char + len(chunk_text),
tokens=current_tokens
))
# لاگ نتیجه
logger.info(f"✅ تقسیم به {len(chunks)} chunk انجام شد")
for chunk in chunks:
logger.info(
f" {chunk['chunk_id']}: "
f"{chunk['tokens']} tokens, "
f"{len(chunk['text'])} chars"
)
return chunks
def _create_chunk_metadata(
self,
chunk_id: int,
text: str,
start_char: int,
end_char: int,
tokens: int
) -> Dict:
"""
ساخت metadata برای یک chunk
Args:
chunk_id: شماره chunk
text: متن chunk
start_char: موقعیت شروع در متن اصلی (کاراکتر)
end_char: موقعیت پایان در متن اصلی (کاراکتر)
tokens: تعداد tokens
Returns:
دیکشنری حاوی metadata
"""
return {
"chunk_id": f"chunk_{chunk_id:02d}",
"text": text,
"start_char": start_char,
"end_char": end_char,
"tokens": tokens,
"length": len(text)
}
def validate_chunks(self, chunks: List[Dict]) -> bool:
"""
اعتبارسنجی chunks
بررسی می‌کند که:
- تمام chunks معتبر هستند
- هیچ chunk خالی نیست
- overlaps درست هستند
Args:
chunks: لیست chunks
Returns:
True اگر همه چیز درست باشد
"""
if not chunks:
return True
for chunk in chunks:
# بررسی فیلدهای ضروری
required_fields = ["chunk_id", "text", "start_char", "end_char", "tokens"]
for field in required_fields:
if field not in chunk:
logger.error(f"❌ Chunk {chunk.get('chunk_id', '?')} فیلد {field} ندارد")
return False
# بررسی خالی نبودن
if not chunk["text"].strip():
logger.error(f"❌ Chunk {chunk['chunk_id']} خالی است")
return False
# بررسی tokens
if chunk["tokens"] <= 0:
logger.error(f"❌ Chunk {chunk['chunk_id']} تعداد tokens نامعتبر دارد")
return False
logger.info("✅ تمام chunks معتبر هستند")
return True
# ✅ تست‌های سریع
if __name__ == "__main__":
print("=" * 60)
print("🧪 Testing Chunker Module")
print("=" * 60)
# تست 1: Chunking ساده
print("\n📊 Test 1: Simple Chunking")
test_text = """
شرکت پارس در سال گذشته فروش 50 میلیارد ریال داشت. این رقم نسبت به سال قبل رشد 15 درصدی دارد.
علی احمدی مدیرعامل شرکت است. شرکت صبا نیز در همین بازار فعالیت می‌کند.
شرکت صبا فروش 30 میلیارد ریال داشت. مریم کریمی مدیر مالی این شرکت است.
همکاری بین دو شرکت در دستور کار است. قرارداد به ارزش 20 میلیارد ریال است.
سرمایه‌گذاری 10 میلیارد ریال انجام خواهد شد. بازده پیش‌بینی شده 25 درصد است.
"""
chunker = TextChunker(chunk_size=100, overlap=20)
chunks = chunker.create_chunks(test_text)
print(f" متن اصلی: {len(test_text)} کاراکتر")
print(f" تعداد chunks: {len(chunks)}")
for chunk in chunks:
print(f"\n {chunk['chunk_id']}:")
print(f" Tokens: {chunk['tokens']}")
print(f" Length: {chunk['length']} chars")
print(f" Position: [{chunk['start_char']}, {chunk['end_char']}]")
print(f" Preview: {chunk['text'][:80]}...")
# تست 2: Validation
print("\n📊 Test 2: Chunk Validation")
is_valid = chunker.validate_chunks(chunks)
print(f" Valid: {is_valid}")
# تست 3: متن خیلی کوتاه
print("\n📊 Test 3: Very Short Text")
short_text = "این یک متن کوتاه است."
short_chunks = chunker.create_chunks(short_text)
print(f" تعداد chunks: {len(short_chunks)}")
# تست 4: متن خالی
print("\n📊 Test 4: Empty Text")
empty_chunks = chunker.create_chunks("")
print(f" تعداد chunks: {len(empty_chunks)}")
print("\n" + "=" * 60)
print("✅ All tests completed!")
print("=" * 60)