Files
hollywood/04-code/Colab-Kentucky Kernel Archive Vector.md
T

9.8 KiB

created, tags, updated
created tags updated
2026-08-06T15:15:00
NotebookLM
colab
vector
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.