اعمل محرك بحث محلي داخل ملفات PDF بـ SQLite FTS5
هتطلع من المقال بأداة بحث محلية تفتح مجلد PDF، تفهرس الصفحات في SQLite، وترجع نتائج مرتبة في أقل من ثانية بعد أول فهرسة.
مستوى القارئ: متوسط
المشكلة باختصار
لو عندك 80 أو 120 ملف PDF وفي كل مرة بتفتحهم يدويًا علشان تلاقي جملة معينة، فأنت بتدفع وقتك في المكان الغلط. الطريقة الشائعة هي البحث داخل قارئ PDF أو داخل نظام التشغيل. الطريقة دي بتفشل لما الملفات كثيرة، أو لما تحتاج تعرف الصفحة، أو لما تريد نتائج مرتبة حسب الصلة.
الافتراض إن عندك ملفات PDF نصية وليست صورًا ممسوحة سكانر. لو الملف عبارة عن صور فقط، استخراج النص هيطلع ضعيف أو فاضي، وساعتها تحتاج OCR قبل الخطوات دي.
الفكرة بمثال واضح
ركز في المثال ده: عندك مجلد اسمه docs فيه عقود، فواتير، وملفات توثيق داخلية. بدل ما تفتح كل ملف وتبحث عن كلمة refund policy، السكربت هيقرأ كل صفحة مرة واحدة، يحفظ النص في قاعدة SQLite صغيرة، وبعدها تسألها مباشرة.
SQLite FTS5 هنا مش قاعدة بيانات ضخمة. هو جدول افتراضي داخل SQLite معمول للبحث النصي الكامل. تقدر تسأله بـ MATCH، وترتب النتائج بـ bm25، وتعرض مقتطف صغير بـ snippet. اللي بيحصل فعلاً إن أول تشغيل بيكون أبطأ لأنه بيبني الفهرس. بعد كده البحث نفسه سريع.
الخطوات التنفيذية
- اعمل مجلد للمشروع وحط ملفات PDF داخل
docs/. - ثبّت مكتبة
pypdfلاستخراج النص. - ابنِ جدول FTS5 يخزن اسم الملف، رقم الصفحة، والنص.
- شغّل أمر index مرة عند إضافة ملفات جديدة.
- شغّل أمر search لأي كلمة أو جملة تريدها.
mkdir pdf-search
cd pdf-search
python -m venv .venv
.venv\Scripts\activate
pip install pypdf
mkdir docs
بعدها اعمل ملف search_pdf.py بالكود التالي. الكود مقصود يكون صغير وواضح، مش framework كامل.
import argparse, sqlite3
from pathlib import Path
from pypdf import PdfReader
DB = "pdf_index.db"
def connect():
con = sqlite3.connect(DB)
con.execute("CREATE VIRTUAL TABLE IF NOT EXISTS pages USING fts5(file, page, text)")
return con
def index(folder):
con = connect()
con.execute("DELETE FROM pages")
for pdf in Path(folder).glob("*.pdf"):
reader = PdfReader(str(pdf))
for i, page in enumerate(reader.pages, start=1):
text = page.extract_text() or ""
if text.strip():
con.execute("INSERT INTO pages(file, page, text) VALUES (?, ?, ?)", (pdf.name, i, text))
con.commit()
con.close()
def search(query):
con = connect()
rows = con.execute("""
SELECT file, page, snippet(pages, 2, '[', ']', '...', 18) AS hit
FROM pages
WHERE pages MATCH ?
ORDER BY bm25(pages)
LIMIT 10
""", (query,)).fetchall()
for file, page, hit in rows:
print(f"{file} page {page}: {hit}")
con.close()
parser = argparse.ArgumentParser()
parser.add_argument("command", choices=["index", "search"])
parser.add_argument("value")
args = parser.parse_args()
if args.command == "index":
index(args.value)
else:
search(args.value)
python search_pdf.py index docs
python search_pdf.py search "refund policy"
القياس المتوقع
في سيناريو واقعي عندك 120 ملف PDF، وكل ملف متوسطه 8 صفحات، أول فهرسة ممكن تأخذ 25 إلى 60 ثانية حسب حجم الملفات. بعد الفهرسة، البحث عن كلمة أو جملة غالبًا ينزل من 10 إلى 20 ثانية لو بتفتح الملفات كل مرة، إلى أقل من 500ms على جهاز متوسط. الرقم تقديري، لكنه منطقي لأنك نقلت التكلفة من وقت البحث إلى وقت الفهرسة.
الـ trade-off هنا
بتكسب سرعة وبحث مرتب ومقتطفات مفيدة. بتخسر حاجة واحدة: لازم تعيد الفهرسة لما تضيف ملفات جديدة أو تعدل ملفات قديمة. لو عندك ملفات قليلة جدًا، 5 أو 10 ملفات مثلًا، الأداة دي ممكن تكون زيادة عن الحاجة.
كمان استخراج النص من PDF مش مثالي دائمًا. بعض الملفات فيها جداول غريبة أو أعمدة متعددة. pypdf يوفر استخراج نص مباشر، لكنه لن يفهم كل layout بنفس جودة قارئ بصري متخصص. أفضل طريقة هنا إنك تبدأ بالأداة البسيطة، وبعدها تضيف تحسينات فقط عندما يظهر سبب واضح.
متى لا تستخدم هذه الطريقة
لا تستخدمها لو ملفاتك scanned images، إلا بعد إضافة OCR مثل Tesseract. لا تستخدمها لو تحتاج صلاحيات مستخدمين، مزامنة بين فريق، أو واجهة ويب عامة. في الحالات دي ابني API وخزن الفهرس في محرك مناسب مثل Meilisearch أو OpenSearch. أما لو الاستخدام شخصي أو داخلي صغير، SQLite FTS5 كافي بالظبط.
مصادر اعتمدت عليها
- توثيق SQLite FTS5 الرسمي لشرح
MATCHوbm25وsnippet. - توثيق Python sqlite3 الرسمي لطريقة الاتصال وتنفيذ الاستعلامات.
- توثيق pypdf لاستخراج النص ولملاحظة حدود استخراج النص من PDF.
الخطوة التالية
افتح مجلد فيه 20 ملف PDF حقيقي، شغّل أمر index مرة، ثم جرّب 5 عمليات بحث أنت بتعملها يدويًا كل أسبوع. لو النتائج مفيدة، أضف أمر صغير يعمل re-index تلقائيًا كل صباح.