9.8 KiB
9.8 KiB
created, tags, updated
| created | tags | updated | |||
|---|---|---|---|---|---|
| 2026-08-06T15:15:00 |
|
2026-08-06T15:15:00 |
REFACTORED COLAB NOTEBOOK - Free Tier Compatible
Kentucky Kernel Archive → Vector DB Pipeline
CELL 1: SETUP & MOUNT (DO THIS FIRST)
## @title SETUP (RUN ONCE)
## Install everything upfront
!pip install internetarchive chromadb sentence-transformers
## Mount Drive once, reuse everywhere
from google.colab import drive
drive.mount('/content/drive')
print("✅ Setup complete. Drive mounted.")
CELL 2: CONFIGURE PATHS (USER INPUT)
## @title CONFIG - Enter Your Settings
## User inputs
creator_name = input("Enter creator name (e.g., 'The Mt. Sterling Advocate'): ").strip()
test_mode = input("Test mode? (y/n, tests with 50 issues): ").strip().lower() == 'y'
## Paths
BASE_DOWNLOAD = f'/content/{creator_name.replace(" ", "_")}_Archive'
CLEANED_JSON = f'/content/drive/MyDrive/{creator_name.replace(" ", "_")}_Cleaned/cleaned_pages.json'
VECTORDB_PATH = f'/content/drive/MyDrive/{creator_name.replace(" ", "_")}_VectorDB'
## Create directories
import os
os.makedirs(os.path.dirname(CLEANED_JSON), exist_ok=True)
os.makedirs(BASE_DOWNLOAD, exist_ok=True)
print(f"✓ Creator: {creator_name}")
print(f"✓ Download to: {BASE_DOWNLOAD}")
print(f"✓ Cleaned JSON: {CLEANED_JSON}")
print(f"✓ Vector DB: {VECTORDB_PATH}")
print(f"✓ Test Mode: {test_mode}")
CELL 3: DOWNLOAD FROM ARCHIVE.ORG
## @title DOWNLOAD (This takes 30-45 min)
from internetarchive import search_items, get_item, get_session
from concurrent.futures import ThreadPoolExecutor, as_completed
ia_session = get_session()
ia_session.mount_http_adapter()
## Search
query = f'creator:"{creator_name}"'
print(f"Searching: {query}")
search = search_items(query, archive_session=ia_session)
identifiers = [result['identifier'] for result in search]
if test_mode:
identifiers = identifiers[:50] # Test with 50 only
total_items = len(identifiers)
print(f"Found {total_items} issues. Starting download with 2 workers (safe)...\n")
def fast_download(identifier):
target_folder = os.path.join(BASE_DOWNLOAD, identifier)
if os.path.exists(target_folder) and len(os.listdir(target_folder)) >= 3:
return f"⏭️ Skipped: {identifier}"
try:
item = get_item(identifier, archive_session=ia_session)
item.download(
destdir=BASE_DOWNLOAD,
glob_pattern=['*meta.xml', '*djvu.xml', '*djvu.txt'],
ignore_existing=True,
retries=3
)
return f"✅ Downloaded: {identifier}"
except Exception as e:
return f"❌ Failed: {identifier} - {str(e)[:50]}"
## Use 2 workers (safer than 4 on Free Tier)
completed = 0
failed = []
with ThreadPoolExecutor(max_workers=2) as executor:
futures = {executor.submit(fast_download, identifier): identifier for identifier in identifiers}
for future in as_completed(futures):
completed += 1
result = future.result()
if "❌" in result:
failed.append(result)
if completed % 25 == 0 or completed == total_items:
print(f"[{completed}/{total_items}] {result[:60]}")
print(f"\n✅ Download complete. {len(failed)} failures.")
if failed:
print("Failed items:")
for f in failed[:5]:
print(f" {f}")
CELL 4: PARSE XML & CLEAN TEXT (CPU-BASED)
## @title PARSE & CLEAN
import xml.etree.ElementTree as ET
import json
import pandas as pd # ← Use pandas, not cudf
import glob
print("Scanning for DJVU XML files...")
djvu_files = glob.glob(os.path.join(BASE_DOWNLOAD, '**/*djvu.xml'), recursive=True)
print(f"Found {len(djvu_files)} XML files.")
page_data_list = []
failed_issues = []
for djvu_path in djvu_files:
folder_name = os.path.basename(os.path.dirname(djvu_path))
try:
tree = ET.parse(djvu_path)
root = tree.getroot()
pages = root.findall('.//OBJECT')
for page_index, page in enumerate(pages):
page_text = []
for word in page.findall('.//WORD'):
if word.text:
page_text.append(word.text)
raw_page = " ".join(page_text)
if raw_page.strip():
page_data_list.append({
"issue_id": folder_name,
"page_number": page_index + 1,
"text": raw_page
})
except Exception as e:
failed_issues.append((folder_name, str(e)))
print(f"Extracted {len(page_data_list)} pages, {len(failed_issues)} failures.")
## CLEANING: Use pandas (CPU) - faster than GPU for regex
print("Cleaning text with regex...")
texts_df = pd.DataFrame({'text': [p['text'] for p in page_data_list]})
## Fix hyphenation
texts_df['text'] = texts_df['text'].str.replace(r'-\s*\n\s*', '', regex=True)
## Remove garbage OCR chars
texts_df['text'] = texts_df['text'].str.replace(r'[^a-zA-Z0-9\s.,;:\'"!?()-]', '', regex=True)
## Normalize spaces
texts_df['text'] = texts_df['text'].str.replace(r'\s+', ' ', regex=True)
## Reattach cleaned text
for i, clean_text in enumerate(texts_df['text'].tolist()):
page_data_list[i]['text'] = clean_text
## Save
print(f"Saving {len(page_data_list)} pages to JSON...")
with open(CLEANED_JSON, 'w', encoding='utf-8') as f:
json.dump(page_data_list, f, indent=2)
print(f"✅ Complete! {len(page_data_list)} clean pages saved.")
CELL 5: BUILD VECTOR DB (GPU-Accelerated Embeddings)
## @title BUILD VECTOR DB (30-60 min - may timeout, that's ok)
import json
import chromadb
from chromadb.utils import embedding_functions
print("Loading cleaned pages...")
with open(CLEANED_JSON, 'r', encoding='utf-8') as f:
pages_data = json.load(f)
print(f"Loaded {len(pages_data)} pages.")
## Initialize ChromaDB with GPU embeddings
print("Initializing ChromaDB with GPU embeddings...")
os.makedirs(VECTORDB_PATH, exist_ok=True)
gpu_ef = embedding_functions.SentenceTransformerEmbeddingFunction(
model_name="all-MiniLM-L6-v2",
device="cuda" # GPU accelerated
)
chroma_client = chromadb.PersistentClient(path=VECTORDB_PATH)
collection = chroma_client.get_or_create_collection(
name="historical_news",
embedding_function=gpu_ef
)
## Chunking function
def chunk_text(text, chunk_size=800, overlap=150):
chunks = []
start = 0
while start < len(text):
end = min(start + chunk_size, len(text))
chunks.append(text[start:end])
start += chunk_size - overlap
return chunks
## Extract & chunk
documents = []
metadatas = []
ids = []
print("Chunking and preparing documents...")
for page in pages_data:
issue_id = page.get('issue_id', 'unknown')
page_num = page.get('page_number', 0)
text = page.get('text', '')
if not text.strip():
continue
chunks = chunk_text(text)
for chunk_idx, chunk_text_data in enumerate(chunks):
documents.append(chunk_text_data)
metadatas.append({
"issue_id": issue_id,
"page_number": page_num,
"chunk_id": chunk_idx
})
ids.append(f"{issue_id}_p{page_num}_c{chunk_idx}")
total_chunks = len(documents)
print(f"Prepared {total_chunks} chunks.")
## Save checkpoint before starting (in case it times out)
checkpoint = {
'total_chunks': total_chunks,
'vectorized': 0,
'started_at': str(__import__('datetime').datetime.now())
}
checkpoint_file = os.path.join(VECTORDB_PATH, 'progress.json')
print("Adding to vector DB...")
BATCH_SIZE = 250
for b in range(0, total_chunks, BATCH_SIZE):
try:
collection.add(
documents=documents[b:b+BATCH_SIZE],
metadatas=metadatas[b:b+BATCH_SIZE],
ids=ids[b:b+BATCH_SIZE]
)
checkpoint['vectorized'] = b + BATCH_SIZE
if (b + BATCH_SIZE) % 1000 == 0 or (b + BATCH_SIZE) >= total_chunks:
with open(checkpoint_file, 'w') as f:
json.dump(checkpoint, f)
print(f"✓ {b + BATCH_SIZE}/{total_chunks} chunks vectorized")
except Exception as e:
print(f"❌ Error at batch {b}: {e}")
print(f"Saved progress checkpoint. DB is recoverable.")
break
print(f"✅ Vector DB complete! Stored at: {VECTORDB_PATH}")
print(f"Query it with: chroma_client.get_collection('historical_news')")
CELL 6: TEST THE VECTOR DB (Optional)
## @title TEST QUERY
import chromadb
db_path = VECTORDB_PATH
client = chromadb.PersistentClient(path=db_path)
collection = client.get_collection("historical_news")
## Test query
test_query = input("Enter a search term (e.g., 'railroad accident'): ").strip()
results = collection.query(
query_texts=[test_query],
n_results=5
)
print(f"\n🔍 Top 5 results for: '{test_query}'\n")
for i, (doc, metadata, distance) in enumerate(zip(
results['documents'][0],
results['metadatas'][0],
results['distances'][0]
)):
print(f"{i+1}. Issue: {metadata['issue_id']}, Page {metadata['page_number']}")
print(f" Relevance score: {1 - distance:.2f}")
print(f" Text: {doc[:150]}...\n")
KEY CHANGES FROM ORIGINAL
| Issue | Original | Fixed |
|---|---|---|
| cudf | ❌ Will crash | ✅ Use pandas (faster anyway) |
| Drive mounts | 3x (wasteful) | 1x (efficient) |
| GPU usage | Regex on GPU (slow) | Embeddings on GPU (fast) |
| Hardcoded paths | ❌ Not flexible | ✅ User input + parameterized |
| Error handling | Silent fails | ✅ Logged failures |
| Checkpointing | None | ✅ Progress saved |
| Workers | 4 (risky) | 2 (safe) |
| Test mode | N/A | ✅ Optional test with 50 issues |
| Memory | All in RAM | ✅ Batch processing |
EXPECTED RESULTS
- Download: 30-45 min (1756 issues)
- Parse & Clean: 10-15 min
- Vectorize: 45-90 min (may timeout on Free Tier around 70 min mark)
- Total: ~2.5 hours
If vectorization times out, your progress is saved and resumable in next session.