Skip to content

README.md - #1

Open
Professor-Codephreak wants to merge 7 commits into
Web3dGuy:mainfrom
GATERAGE:main
Open

README.md#1
Professor-Codephreak wants to merge 7 commits into
Web3dGuy:mainfrom
GATERAGE:main

Conversation

@Professor-Codephreak

Copy link
Copy Markdown

md-embed (c) 2024 web3dguy

A Python script for processing Markdown files, generating embeddings, and storing them in a vector store. This tool allows you to clean, split, and embed Markdown documents using various methods and embedding models.
Features

Data Cleaning: Removes duplicates and filters out unwanted content like '404' pages and lines containing the '©' symbol.
Flexible Input: Supports input from JSON files containing URLs and Markdown data, folders of Markdown files, or single Markdown files.
Document Splitting: Splits documents using Markdown headers or recursive character splitting.
Embedding Options: Supports embedding using HuggingFace or Ollama embeddings.
Vector Store Integration: Stores embeddings in a Chroma vector store for efficient retrieval and analysis.
Customizable Filters: Option to disable filters that remove specific content.
Logging: Generates logs for duplicates and removed files for better traceability.

Installation
Prerequisites

    Python 3.7 or higher
    pip
    Git (optional, for cloning the repository)

Clone the Repository

git clone https://github.com/GATERAGE/mdmbed.git
cd mdmbed

Install Required Packages

Install the required Python packages using pip:

pip install -r requirements.txt

Note: The requirements.txt file should list all the dependencies, such as tqdm, langchain, chromadb, huggingface, etc.
Usage

Run the script using Python:

python md-embed.py [--filters-off]

Command-Line Arguments

--filters-off: Disable filters that remove lines containing '©' and skip files containing both '404' and 'page not found'.

Upon running the script, you will be prompted to choose an input method:

JSON Input File Containing URLs and Markdown Data
Folder of Markdown Files
Single Markdown File

JSON Input File

If you choose Option 1, you will be asked to provide:

Path of the JSON input file: The file should be a JSON array of objects, each containing url and markdown keys.
Path of the output folder: The folder where cleaned Markdown files and logs will be saved.

The script will:

Clean the data by removing duplicates.
Save the cleaned Markdown files to the specified output folder.
Generate a file_to_url.json mapping file.
Display a summary of the processing.

Folder of Markdown Files

If you choose Option 2, you will be asked to provide:

Path of the folder containing Markdown files.

The script will:

Load all .md files from the specified folder.
Optionally filter out unwanted content.
Proceed to document splitting.

Single Markdown File

If you choose Option 3, you will be asked to provide:

Path of the Markdown file.

The script will:

Load the specified Markdown file.
Optionally filter out unwanted content.
Proceed to document splitting.

Document Splitting

After loading the documents, you will be prompted to split them:

Split Method: Choose between markdown or recursive splitting.
Remove Links: Optionally remove links from the Markdown content.
Language: Specify the programming language or language of the content.
Additional Settings:
    For Markdown Splitting:
        Header Levels: Specify which header levels (#, ##, etc.) to split on.
    For Recursive Splitting:
        Chunk Size: Specify the maximum size of each chunk.
        Chunk Overlap: Specify the number of overlapping characters between chunks.

You will have the option to preview the split data before proceeding.
Embedding and Saving

After splitting, you will be prompted to embed and save the documents:

Embedding Method: Choose between huggingface or ollama.
    HuggingFace: Enter the embedding model name (default: all-MiniLM-L6-v2).
    Ollama: Enter the Ollama model name (default: nomic-embed-text).
Persist Directory: Specify the directory to save the vector store database.
Collection Name: Enter a name for the Chroma collection.

The script will:

Embed the documents using the chosen embedding method.
Save the embeddings to a Chroma vector store.
Display information about the saved collections.

Examples
Example 1: Process JSON Input File

python md-embed.py

Choose Input Method: 1

Enter the path of the JSON input file: ./data/input.json
Enter the path of the output folder: ./output

Proceed through the prompts to clean data, split documents, and embed them.
Example 2: Process Folder of Markdown Files with Filters Off

python md-embed.py --filters-off

Choose Input Method: 2

Enter the path of the folder containing markdown files: ./markdown_files

Proceed through the prompts to load, split, and embed the documents.
Contributing

Contributions are welcome! Please follow these steps:

Fork the repository.

Create a new branch:
git checkout -b feature/your-feature-name

Make your changes and commit them:

git commit -m "Add your message"

Push to the branch:

git push origin feature/your-feature-name
Open a Pull Request.

Please make sure your code adheres to the existing style and that all tests pass.
License

This project is licensed under the MIT License.
Acknowledgments
web3dguy
LangChain for text splitting and document handling.
HuggingFace for embedding models.
Chroma for the vector store.
TQDM for progress bars.
The open-source community for continuous support and contributions.

Markdown Processor and Embedder

md-embed processes markdown files, cleans and prepares the data, splits the text into manageable chunks, and creates embeddings for use in vector databases (specifically ChromaDB). It supports multiple input methods and provides options for customizing the splitting and embedding process.

Features

  • Multiple Input Methods:
    • JSON file containing URLs and markdown data
    • Folder of markdown files
    • Single markdown file
  • Data Cleaning:
    • Removes duplicate entries based on URL section titles
    • Handles encoding issues
    • Sanitizes filenames for safe saving
    • Optionally filters out files containing "404" and "page not found" (can be disabled)
    • Removes lines containing the copyright symbol "©"
  • Text Splitting:
    • Markdown Header Splitting: Splits text based on specified markdown header levels (e.g., #, ##). Allows for custom header level selection. Preserves header hierarchy in metadata
    • Recursive Character Text Splitting: Splits text into chunks of specified size and overlap
    • Link Removal: Optionally removes markdown links, keeping only the link text
  • **Embedding Generation:*
    • Supports Hugging Face embeddings (using langchain_huggingface). Defaults to all-MiniLM-L6-v2
    • Supports Ollama embeddings (using langchain_community). Defaults to nomic-embed-text, requires a local Ollama server running at http://localhost:11434
  • Vector Database Integration:
    • Uses ChromaDB (langchain_chroma) to store embeddings and associated metadata
    • Allows specifying the collection name and persistence directory
    • Handles large datasets by processing in batches
  • Logging:
    • Comprehensive logging through the logging module
  • Duplicate Logs:
    • Writes URLs with duplicate sections to a log
  • Removed Files Logs
    • Write to a log files that have been removed due to filters

Requirements

  • Python 3.7+
  • langchain (various components - see import statements)
  • chromadb
  • tqdm
  • beautifulsoup4 (if you were scraping, but this script doesn't actually use it)
  • requests (if you were scraping, but this script doesn't actually use it)

To install the required packages, run:

pip install langchain langchain-chroma langchain-huggingface tqdm
If you are planning to use Ollama, you need to:
Install Ollama by following the instructions provided at Ollama's official website.
Run an Ollama server locally on port 11434

md-embed can be run from the command line. It provides a command-line interface using argparse with the following option:

--filters-off: Disables the "404" and "©" filters

The script will then guide you through a series of interactive prompts to configure the processing:

Input Method Selection: Choose between JSON input, a folder of markdown files, or a single markdown file

Input File/Folder/URL: Provide the path to the input file or folder, as appropriate

Output Folder (for JSON input): Specify the directory where cleaned markdown files will be saved

Data Cleaning Options: The script will show total entires and total duplicates

Language: Specify the primary language of the input files (e.g., "TypeScript", "Python")

Splitting Method: Choose between "markdown" (header-based splitting) and "recursive" (chunk size and overlap)

Markdown Splitting Options (if applicable):

Remove Links: Choose whether to remove markdown links

Header Levels: Specify which header levels to split on (e.g., "1,2,3" for #, ##, and ###). Enter "all" for all header levels

Recursive Splitting Options (if applicable):

Remove Links: Choose whether to remove markdown links

Chunk Size: Specify the desired chunk size (in characters)

Chunk Overlap: Specify the desired chunk overlap (in characters)

Preview Splits: Choose whether to preview the split data ("yes", "full", or "no")

Split Again: You'll be prompted to continue or modify the settings

Embedding Method: Choose between "huggingface" and "ollama"

Embedding Model (Hugging Face): Enter the Hugging Face model name (defaults to all-MiniLM-L6-v2)

Embedding Model (Ollama): Enter the Ollama model name (defaults to nomic-embed-text)

Persistence Directory: Specify the directory where the ChromaDB database will be stored

Collection Name: Choose a name for the ChromaDB collection

Example (JSON Input):

python md-embed.py

Follow the prompts, providing the necessary information (input file, output folder, embedding choices, etc.)

Example (Disabling Filters):

python md-embed.py --filters-off

Cleaned Markdown Files (JSON Input): If using JSON input, the script will save cleaned markdown files to the specified output folder

ChromaDB Database: The script will create a ChromaDB database in the specified persistence directory, containing the embeddings and metadata

Logs: The logs directory will contain logs of removed files (if any) and duplicate entries (if using JSON input)

file_to_url.json: Json file that contains the original URL of each document

Error Handling

The script includes error handling for various scenarios, such as:

Invalid input file/folder paths

File I/O errors

Exceptions during data cleaning, splitting, or embedding

Invalid user input for prompts

Errors are logged using the logging module

Notes

The script assumes that the input JSON data has "url" and "markdown" keys for each entry

The script uses uuid4 to generate unique IDs for each document in the vector database

The script processes in batches to deal with a large number of splits

Disclaimer: This tool is provided "as is" without warranty of any kind. Use it at your own risk. Open source or go away.

Professor-Codephreak and others added 7 commits October 17, 2024 23:49
open source or go away
md-embed.py ends at Chroma, via interactive input() prompts, defaulting to
all-MiniLM-L6-v2 (384) or nomic-embed-text. RAGE runs bge-m3 at 1024 dims in
PostgreSQL with pgvectorscale/StreamingDiskANN. The pipeline

    codebase --BaseGen--> markdown --md-embed--> chunks --> Chroma

therefore stopped one hop short of RAGE in three ways at once: wrong store,
wrong dimension, un-scriptable. ragesink is that hop, prompt-free so it runs
from cron.

  · markdown-aware splitting — headers first, then 512-word packing (the
    production chunk size). In a BaseGen doc every file is a section, so the
    heading carries the path; a '#' inside a fence is a comment, not a heading.
  · index selection reported, not hidden: vectorscale/DiskANN, else
    pgvector/HNSW, else none — and it says which.
  · idempotent WITH an orphan prune. Upsert on (doc_name, chunk_idx) never
    removes anything, so a document that re-chunks shorter leaves stale rows
    above the new count that similarity search still serves as current. Prunes
    chunk_idx >= len(chunks), and only after a successful store: a run that
    wrote nothing is no evidence about the document's shape.

Verified end to end on live Postgres + Ollama: 20 chunks in; the same document
truncated then reported 3/3 stored, pruned 17. Semantic query 'how does the
federation handle a peer that does not answer' returned federate.py first.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhsWQt3fduoCMQgG42Ddg8
OPTIMIZATION. Chunks now carry sha256(model || dims || text). Only chunks whose
hash changed are embedded, so a re-ingest over an unchanged corpus does almost
no work. Measured on 20 chunks, 4-core CPU, no GPU:

    cold ingest          20/20   515.5 s
    re-run unchanged      0/20     0.49 s     ~1050x
    one section edited    1/20    14.0 s

That is the number that decides whether ingestion can go on a timer.

The model and width are INSIDE the hash deliberately: the same text under a
different embedder is not the same vector, and silently keeping the old row is
how a table ends up with two embedding spaces mixed — a failure that yields
plausible nonsense rather than an error.

Embedding now batches 32 chunks per /api/embed call, with automatic fallback to
the older per-chunk /api/embeddings. Honest about the gain: batching cuts round
trips, but on CPU the model's forward pass is nearly all the cost, so it barely
moves the cold number. The hash is what delivers the speedup.

content_hash is added with ADD COLUMN IF NOT EXISTS, so this attaches to an
existing RAGE store with no migration; pre-existing rows have a NULL hash and
re-embed once.

The prune guard is tightened rather than loosened: it now fires when the run has
evidence about the document's shape — it either wrote something, or verified
every chunk as already-current. A run that stored nothing because the embedder
was down still deletes nothing.

TECHNICAL.md documents the whole path end to end: BaseGen's two
retrieval-hostile defaults, header-first splitting and why it matters for
attribution, the hash, the index fallback that announces itself, the upsert +
prune contract, measured costs, the schema, and a verified retrieval example.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhsWQt3fduoCMQgG42Ddg8
…ogress

PACKING. Splitting emitted one chunk per markdown section regardless of size.
On the mindX docs that was 440 chunks averaging 174 words against a 512 cap:
3x the embedding cost, and a corpus fragmented into pieces too small to carry
their own context. Consecutive sections now pack up to chunk_words. Same corpus:
440 -> 197 chunks, 55% fewer. Oversized sections still split, and a split
section repeats its heading on every piece.

OWNERSHIP. Attaching to a store you do not own is normal — a shared RAGE
database grants INSERT/UPDATE without table ownership, and ALTER TABLE then
raises InsufficientPrivilege and aborted the entire run. ensure_schema now tries
the ADD COLUMN once, guarded, and the fallback index paths no longer retry it.
Without content_hash the skip optimisation is simply off and every run
re-embeds: slow but correct, and it SAYS so, because a silent 100x slowdown
looks exactly like a hang.

BUFFERING. Python block-buffers stdout when redirected, so a multi-hour ingest
printed nothing until the buffer filled — indistinguishable from a hang, and I
diagnosed a live run as stalled when it was fine. Progress lines flush.

Also --strip-ext, to name documents the way an existing store already does
(relpath without the extension) rather than forking the convention.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhsWQt3fduoCMQgG42Ddg8
A long ingest over an SSH tunnel WILL see the tunnel flap. Two failures on the
same run, both real, both now handled:

1. The tunnel died while the process kept going, so it spent ~25 s of CPU per
   chunk producing vectors with nowhere to store them. alive() now checks the
   connection BEFORE each embed batch — after is too late, the cost is already
   paid.

2. The connection then died BETWEEN that pre-check and the INSERT, surfacing as
   a raw psycopg2.OperationalError that killed the run at file 4 of 42. A
   pre-check alone cannot close that window, so the write path reconnects and
   retries the file, backing off 5/10/15/20s over RECONNECT_TRIES attempts.
   Files commit atomically, so retrying one is clean, and anything already
   committed is skipped by hash on the way back through.

commit() is guarded the same way: a lost commit reconnects and the file is
redone next run rather than being silently counted as stored.

Verified by the resume this was written for: 3 previously-stored documents
skipped in under a second, ingest continued at file 4.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhsWQt3fduoCMQgG42Ddg8
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant