chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:08:54 +08:00
commit 4a4a1fed67
721 changed files with 262090 additions and 0 deletions
+27
View File
@@ -0,0 +1,27 @@
#!/bin/bash
# SessionStart hook: install backend + WebUI dependencies so that
# tests and linters work out of the box in Claude Code on the web.
set -euo pipefail
# Only run in the remote (Claude Code on the web) environment.
if [ "${CLAUDE_CODE_REMOTE:-}" != "true" ]; then
exit 0
fi
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(pwd)}"
cd "$PROJECT_DIR"
# --- Python backend (uv) ---
# `uv sync` is idempotent and reuses the cached .venv across sessions.
if command -v uv >/dev/null 2>&1; then
echo "[session-start] Syncing Python dependencies (api, test extras)..."
uv sync --extra api --extra test
fi
# --- WebUI frontend (bun) ---
if command -v bun >/dev/null 2>&1 && [ -f lightrag_webui/package.json ]; then
echo "[session-start] Installing WebUI dependencies..."
(cd lightrag_webui && bun install --frozen-lockfile)
fi
echo "[session-start] Dependencies ready."
+20
View File
@@ -0,0 +1,20 @@
{
"permissions": {
"allow": [
"Bash(./scripts/test.sh)",
"Bash(./scripts/test.sh *)"
]
},
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/session-start.sh"
}
]
}
]
}
}
+257
View File
@@ -0,0 +1,257 @@
# LightRAG Project Intelligence (.clinerules)
## Project Overview
LightRAG is a mature, production-ready Retrieval-Augmented Generation (RAG) system with comprehensive knowledge graph capabilities. The system has evolved from experimental to production-ready status with extensive functionality across all major components.
## Current System State (August 15, 2025)
- **Status**: Production Ready - Stable and Mature
- **Configuration**: Gemini 2.5 Flash + BAAI/bge-m3 embeddings via custom endpoints
- **Storage**: Default in-memory with file persistence (JsonKVStorage, NetworkXStorage, NanoVectorDBStorage)
- **Language**: Chinese for summaries
- **Workspace**: `space1` for data isolation
- **Authentication**: JWT-based with admin/user accounts
## Critical Implementation Patterns
### 1. Embedding Format Compatibility (CRITICAL)
**Pattern**: Always handle both base64 and raw array embedding formats
**Location**: `lightrag/llm/openai.py` - `openai_embed` function
**Issue**: Custom OpenAI-compatible endpoints return embeddings as raw arrays, not base64 strings
**Solution**:
```python
np.array(dp.embedding, dtype=np.float32) if isinstance(dp.embedding, list)
else np.frombuffer(base64.b64decode(dp.embedding), dtype=np.float32)
```
**Impact**: Document processing fails completely without this dual format support
### 2. Async Pattern Consistency (CRITICAL)
**Pattern**: Always await coroutines before calling methods on the result
**Common Error**: `coroutine.method()` instead of `(await coroutine).method()`
**Locations**: MongoDB implementations, Neo4j operations
**Example**: `await self._data.list_indexes()` then `await cursor.to_list()`
### 3. Storage Layer Data Compatibility (CRITICAL)
**Pattern**: Always filter deprecated/incompatible fields during deserialization
**Common Fields to Remove**: `content`, `_id` (MongoDB), database-specific fields
**Implementation**: `data.pop('field_name', None)` before creating dataclass objects
**Locations**: All storage implementations (JSON, Redis, MongoDB, PostgreSQL)
### 4. Lock Key Generation (CRITICAL)
**Pattern**: Always sort relationship pairs for consistent lock keys
**Implementation**: `sorted_key_parts = sorted([src, tgt])` then `f"{sorted_key_parts[0]}-{sorted_key_parts[1]}"`
**Impact**: Prevents deadlocks in concurrent relationship processing
### 5. Event Loop Management (CRITICAL)
**Pattern**: Handle event loop mismatches during shutdown gracefully
**Implementation**: Timeout + specific RuntimeError handling for "attached to a different loop"
**Location**: Neo4j storage finalization
**Impact**: Prevents application shutdown failures
### 6. Async Generator Lock Management (CRITICAL)
**Pattern**: Never hold locks across async generator yields - create snapshots instead
**Issue**: Holding locks while yielding causes deadlock when consumers need the same lock
**Location**: `lightrag/tools/migrate_llm_cache.py` - `stream_default_caches_json`
**Solution**: Create snapshot of data while holding lock, release lock, then iterate over snapshot
```python
# WRONG - Deadlock prone:
async with storage._storage_lock:
for key, value in storage._data.items():
batch[key] = value
if len(batch) >= batch_size:
yield batch # Lock still held!
# CORRECT - Snapshot approach:
async with storage._storage_lock:
matching_items = [(k, v) for k, v in storage._data.items() if condition]
# Lock released here
for key, value in matching_items:
batch[key] = value
if len(batch) >= batch_size:
yield batch # No lock held
```
**Impact**: Prevents deadlocks in Json→Json migrations and similar scenarios where source/target share locks
**Applicable To**: Any async generator that needs to access shared resources while yielding
## Architecture Patterns
### 1. Dependency Injection
**Pattern**: Pass configuration through object constructors, not direct imports
**Example**: OllamaAPI receives configuration through LightRAG object
**Benefit**: Better testability and modularity
### 2. Memory Bank Documentation
**Pattern**: Maintain comprehensive memory bank for development continuity
**Structure**: Core files (projectbrief.md, activeContext.md, progress.md, etc.)
**Purpose**: Essential for context preservation across development sessions
### 3. Configuration Management
**Pattern**: Centralize defaults in constants.py, use environment variables for runtime config
**Implementation**: Default values in constants, override via .env file
**Benefit**: Consistent configuration across components
## Development Workflow Patterns
### 1. Frontend Development (CRITICAL)
**Package Manager**: **ALWAYS USE BUN** - Never use npm or yarn unless Bun is unavailable
**Commands**:
- `bun install` - Install dependencies
- `bun run dev` - Start development server
- `bun run build` - Build for production
- `bun run lint` - Run linting
- `bun test` - Run tests
- `bun run preview` - Preview production build
**Pattern**: All frontend operations must use Bun commands
**Fallback**: Only use npm/yarn if Bun installation fails
**Testing**: Use `bun test` for all frontend testing
### 2. Bug Fix Approach
1. **Identify root cause** - Don't just fix symptoms
2. **Implement robust solution** - Handle edge cases and format variations
3. **Maintain backward compatibility** - Preserve existing functionality
4. **Add comprehensive error handling** - Graceful degradation
5. **Document the fix** - Update memory bank with technical details
### 3. Feature Implementation
1. **Follow existing patterns** - Maintain architectural consistency
2. **Use dependency injection** - Avoid direct imports between modules
3. **Implement comprehensive error handling** - Handle all failure modes
4. **Add proper logging** - Debug and warning messages
5. **Update documentation** - Memory bank and code comments
6. **Comment Language** - Use English for comments and documentation
### 4. Performance Optimization
1. **Profile before optimizing** - Identify actual bottlenecks
2. **Maintain algorithmic correctness** - Don't sacrifice functionality for speed
3. **Use appropriate data structures** - Match structure to access patterns
4. **Implement caching strategically** - Cache expensive operations
5. **Monitor memory usage** - Prevent memory leaks
### 5. Testing Workflow (CRITICAL)
**Pattern**: All tests must use pytest markers for proper CI/CD execution
**Test Categories**:
- **Offline Tests**: Use `@pytest.mark.offline` - No external dependencies (runs in CI)
- **Integration Tests**: Use `@pytest.mark.integration` - Requires databases/APIs (skipped by default)
**Commands**:
- `pytest tests/ -m offline -v` - CI default (~3 seconds for 21 tests)
- `pytest tests/ --run-integration -v` - Full test suite (all 46 tests)
**Best Practices**:
1. **Prefer offline tests** - Use mocks for LLM, embeddings, databases
2. **Mock external dependencies** - AsyncMock for async functions
3. **Test isolation** - Each test should be independent
4. **Documentation** - Add docstrings explaining purpose and scope
**Configuration**:
- `tests/pytest.ini` - Marker definitions and test discovery
- `tests/conftest.py` - Fixtures and custom options
- `.github/workflows/tests.yml` - CI/CD workflow (Python 3.10/3.11/3.12)
**Documentation**: See `memory-bank/testing-guidelines.md` for complete testing guidelines
**Impact**: Ensures all tests run reliably in CI without external services while maintaining comprehensive integration test coverage for local development
## Technology Stack Intelligence
### 1. LLM Integration
- **Primary**: Gemini 2.5 Flash via custom endpoint
- **Embedding**: BAAI/bge-m3 via custom endpoint
- **Reranking**: BAAI/bge-reranker-v2-m3
- **Pattern**: Always handle multiple provider formats
### 2. Storage Backends
- **Default**: In-memory with file persistence
- **Production Options**: PostgreSQL, MongoDB, Redis, Neo4j
- **Pattern**: Abstract storage interface with multiple implementations
### 3. API Architecture
- **Framework**: FastAPI with Gunicorn for production
- **Authentication**: JWT-based with role support
- **Compatibility**: Ollama-compatible endpoints for easy integration
### 4. Frontend
- **Framework**: React with TypeScript
- **Package Manager**: **BUN (REQUIRED)** - Always use Bun for all frontend operations
- **Build Tool**: Vite with Bun runtime
- **Visualization**: Sigma.js for graph rendering
- **State Management**: React hooks with context
- **Internationalization**: i18next for multi-language support
## Common Pitfalls and Solutions
### 1. Embedding Format Issues
**Pitfall**: Assuming all endpoints return base64-encoded embeddings
**Solution**: Always check format and handle both base64 and raw arrays
### 2. Async/Await Patterns
**Pitfall**: Calling methods on coroutines instead of awaited results
**Solution**: Always await coroutines before accessing their methods
### 3. Data Model Evolution
**Pitfall**: Breaking changes when removing fields from dataclasses
**Solution**: Filter deprecated fields during deserialization, don't break storage
### 4. Concurrency Issues
**Pitfall**: Inconsistent lock key generation causing deadlocks
**Solution**: Always sort keys for deterministic lock ordering
### 5. Event Loop Management
**Pitfall**: Event loop mismatches during shutdown
**Solution**: Implement timeout and specific error handling for loop issues
## Performance Considerations
### 1. Query Context Building
- **Algorithm**: Linear gradient weighted polling for fair resource allocation
- **Optimization**: Round-robin merging to eliminate mode bias
- **Pattern**: Smart chunk selection based on cross-entity occurrence
### 2. Graph Operations
- **Optimization**: Batch operations where possible
- **Pattern**: Use appropriate indexing for large datasets
- **Consideration**: Memory usage with large graphs
### 3. LLM Request Management
- **Pattern**: Priority-based queue for request ordering
- **Optimization**: Connection pooling and retry mechanisms
- **Consideration**: Rate limiting and cost management
## Security Patterns
### 1. Authentication
- **Implementation**: JWT tokens with role-based access
- **Pattern**: Stateless authentication with configurable expiration
- **Security**: Proper token validation and refresh mechanisms
### 2. API Security
- **Pattern**: Input validation and sanitization
- **Implementation**: FastAPI dependency injection for auth
- **Consideration**: Rate limiting and abuse prevention
## Maintenance Guidelines
### 1. Memory Bank Updates
- **Trigger**: After significant changes or bug fixes
- **Pattern**: Update activeContext.md and progress.md
- **Purpose**: Maintain development continuity
### 2. Configuration Management
- **Pattern**: Environment-based configuration with sensible defaults
- **Implementation**: .env files with example templates
- **Consideration**: Security for production deployments
### 3. Error Handling
- **Pattern**: Comprehensive logging with appropriate levels
- **Implementation**: Graceful degradation where possible
- **Consideration**: User-friendly error messages
## Project Evolution Notes
The project has evolved from experimental to production-ready status. Key milestones:
- **Early 2025**: Basic RAG implementation
- **Mid 2025**: Multiple storage backends and LLM providers
- **July 2025**: Major query optimization and algorithm improvements
- **August 2025**: Production-ready stable state
The system now supports enterprise-level deployments with comprehensive functionality across all components.
+69
View File
@@ -0,0 +1,69 @@
# Python-related files and directories
__pycache__
.cache
# Virtual environment directories
*.venv
# Env
env/
*.env*
.env_example
# Distribution / build files
site
dist/
build/
.eggs/
*.egg-info/
*.tgz
*.tar.gz
# Exclude siles and folders
*.yml
.dockerignore
Dockerfile
Makefile
# Exclude other projects
/tests
/scripts
/data
/dickens
/reproduce
/output_complete
/rag_storage
/inputs
# Python version manager file
.python-version
# Reports
*.coverage/
*.log
log/
*.logfire
# Cache
.cache/
.mypy_cache
.pytest_cache
.ruff_cache
.gradio
.logfire
temp/
# MacOS-related files
.DS_Store
# VS Code settings (local configuration files)
.vscode
# file
TODO.md
# Exclude Git-related files
.git
.github
.gitignore
.pre-commit-config.yaml
+6
View File
@@ -0,0 +1,6 @@
# Revisions listed here are skipped by 'git blame' (mechanical, repo-wide
# reformats with no logic changes). Enable locally with:
# git config blame.ignoreRevsFile .git-blame-ignore-revs
#
# style: apply ruff 0.15 formatting across repo
21f4e698e555ace903ffc725e2914fe57b660bb1
+2
View File
@@ -0,0 +1,2 @@
lightrag/api/webui/** binary
lightrag/api/webui/** linguist-generated
+163
View File
@@ -0,0 +1,163 @@
# Contributing to LightRAG
Thank you for your interest in contributing! This guide covers everything you need to get started.
## Table of Contents
- [Ways to Contribute](#ways-to-contribute)
- [Development Setup](#development-setup)
- [Code Style](#code-style)
- [Running Tests](#running-tests)
- [Submitting a Pull Request](#submitting-a-pull-request)
- [Reporting Bugs](#reporting-bugs)
- [Requesting Features](#requesting-features)
---
## Ways to Contribute
- **Bug reports** — open an [issue](https://github.com/HKUDS/LightRAG/issues) using the Bug Report template
- **Feature requests** — open an [issue](https://github.com/HKUDS/LightRAG/issues) using the Feature Request template
- **Documentation** — fix typos, clarify explanations, or add examples
- **Code** — fix bugs, implement features, or add storage/LLM backends
- **Testing** — add test coverage for untested code paths
---
## Development Setup
```bash
# Clone the repository
git clone https://github.com/HKUDS/LightRAG.git
cd LightRAG
# Install in development mode (requires uv)
uv sync
source .venv/bin/activate # Linux/macOS
# .venv\Scripts\activate # Windows
# Install with optional extras as needed
uv sync --extra api # FastAPI server
uv sync --extra test # Test dependencies
uv sync --extra offline-storage # Storage backends
uv sync --extra offline-llm # Additional LLM providers
# Set up pre-commit hooks (run once)
pip install pre-commit
pre-commit install
```
---
## Code Style
This project uses [Ruff](https://docs.astral.sh/ruff/) for formatting and linting, enforced via [pre-commit](https://pre-commit.com/).
### Automatic fixing
Running `pre-commit run --all-files` will automatically fix most style issues:
```bash
# Fix all files
pre-commit run --all-files
# Fix only staged files (faster during development)
pre-commit run
```
### What is checked
| Hook | What it does |
|------|-------------|
| `trailing-whitespace` | Removes trailing whitespace |
| `end-of-file-fixer` | Ensures files end with a newline |
| `requirements-txt-fixer` | Keeps `requirements.txt` entries sorted |
| `ruff-format` | Formats Python code (Black-compatible) |
| `ruff` | Fixes Python lint errors |
### CI check
The same checks run automatically on every pull request. If the CI check fails, run `pre-commit run --all-files` locally, commit the fixes, and push again.
### Language conventions
- **Python code and comments**: English
- **Frontend (WebUI)**: uses i18next for internationalization — add translation keys rather than hardcoding strings
---
## Running Tests
```bash
# Run offline tests (no external services required)
python -m pytest tests
# Run integration tests (requires configured external services)
python -m pytest tests --run-integration
# Run a specific test file
python -m pytest tests/chunker/test_chunking.py
# Keep test artifacts for debugging
python -m pytest tests --keep-artifacts
```
Set `LIGHTRAG_RUN_INTEGRATION=true` as an environment variable as an alternative to `--run-integration`.
---
## Submitting a Pull Request
1. **Fork** the repository and create a branch from `main`:
```bash
git checkout -b fix/your-descriptive-branch-name
```
2. **Make your changes** and ensure:
- Pre-commit checks pass: `pre-commit run --all-files`
- Relevant tests pass: `python -m pytest tests`
- New behavior is covered by tests where applicable
3. **Commit** with a clear message describing *why* the change was made:
```bash
git commit -m "fix: handle permission-only encrypted PDFs without password"
```
4. **Push** and open a pull request against `main`. Fill out the pull request template completely.
5. **Respond to review feedback** — a maintainer will review your PR and may request changes.
### Pull request checklist
- [ ] Changes tested locally
- [ ] Pre-commit checks pass (`pre-commit run --all-files`)
- [ ] Unit/integration tests added or updated where applicable
- [ ] Documentation updated if behavior changes
- [ ] PR description explains the *why*, not just the *what*
---
## Reporting Bugs
Please use the [Bug Report issue template](https://github.com/HKUDS/LightRAG/issues/new?template=bug_report.yml). Include:
- LightRAG version and Python version
- Storage backend and LLM provider being used
- Minimal reproducible example
- Full error traceback
---
## Requesting Features
Please use the [Feature Request issue template](https://github.com/HKUDS/LightRAG/issues/new?template=feature_request.yml). Describe:
- The problem you're trying to solve
- Your proposed solution
- Any alternatives you've considered
---
## Questions
For usage questions, check the [Discussions](https://github.com/HKUDS/LightRAG/discussions) tab or open a [Question issue](https://github.com/HKUDS/LightRAG/issues/new?template=question.yml).
+61
View File
@@ -0,0 +1,61 @@
name: Bug Report
description: File a bug report
title: "[Bug]:"
labels: ["bug", "triage"]
body:
- type: checkboxes
id: existingcheck
attributes:
label: Do you need to file an issue?
description: Please help us manage our time by avoiding duplicates and common bugs with the steps below.
options:
- label: I have searched the existing issues and this bug is not already filed.
- label: I believe this is a legitimate bug, not just a question or feature request.
- type: textarea
id: description
attributes:
label: Describe the bug
description: A clear and concise description of what the bug is.
placeholder: What went wrong?
- type: textarea
id: reproduce
attributes:
label: Steps to reproduce
description: Steps to reproduce the behavior.
placeholder: How can we replicate the issue?
- type: textarea
id: expected_behavior
attributes:
label: Expected Behavior
description: A clear and concise description of what you expected to happen.
placeholder: What should have happened?
- type: textarea
id: configused
attributes:
label: LightRAG Config Used
description: The LightRAG configuration used for the run.
placeholder: The settings content or LightRAG configuration
value: |
# Paste your config here
- type: textarea
id: screenshotslogs
attributes:
label: Logs and screenshots
description: If applicable, add screenshots and logs to help explain your problem.
placeholder: Add logs and screenshots here
- type: textarea
id: additional_information
attributes:
label: Additional Information
description: |
- LightRAG Version: e.g., v0.1.1
- Operating System: e.g., Windows 10, Ubuntu 20.04
- Python Version: e.g., 3.8
- Related Issues: e.g., #1
- Any other relevant information.
value: |
- LightRAG Version:
- Operating System:
- Python Version:
- Related Issues:
+1
View File
@@ -0,0 +1 @@
blank_issues_enabled: false
@@ -0,0 +1,26 @@
name: Feature Request
description: File a feature request
labels: ["enhancement"]
title: "[Feature Request]:"
body:
- type: checkboxes
id: existingcheck
attributes:
label: Do you need to file a feature request?
description: Please help us manage our time by avoiding duplicates and common feature request with the steps below.
options:
- label: I have searched the existing feature request and this feature request is not already filed.
- label: I believe this is a legitimate feature request, not just a question or bug.
- type: textarea
id: feature_request_description
attributes:
label: Feature Request Description
description: A clear and concise description of the feature request you would like.
placeholder: What this feature request add more or improve?
- type: textarea
id: additional_context
attributes:
label: Additional Context
description: Add any other context or screenshots about the feature request here.
placeholder: Any additional information
+26
View File
@@ -0,0 +1,26 @@
name: Question
description: Ask a general question
labels: ["question"]
title: "[Question]:"
body:
- type: checkboxes
id: existingcheck
attributes:
label: Do you need to ask a question?
description: Please help us manage our time by avoiding duplicates and common questions with the steps below.
options:
- label: I have searched the existing question and discussions and this question is not already answered.
- label: I believe this is a legitimate question, not just a bug or feature request.
- type: textarea
id: question
attributes:
label: Your Question
description: A clear and concise description of your question.
placeholder: What is your question?
- type: textarea
id: context
attributes:
label: Additional Context
description: Provide any additional context or details that might help us understand your question better.
placeholder: Add any relevant information here
+206
View File
@@ -0,0 +1,206 @@
# Keep GitHub Actions up to date with GitHub's Dependabot...
# https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot
# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#package-ecosystem
version: 2
updates:
# ============================================================
# GitHub Actions
# PR Strategy:
# - All updates (major/minor/patch): Grouped into a single PR
# ============================================================
- package-ecosystem: github-actions
directory: /
groups:
github-actions:
patterns:
- "*" # Group all Actions updates into a single larger pull request
schedule:
interval: weekly
day: monday
time: "02:00"
timezone: "Asia/Shanghai"
labels:
- "dependencies"
- "github-actions"
open-pull-requests-limit: 2
# ============================================================
# Python (pip) Dependencies
# PR Strategy:
# - Major updates: Individual PR per package (except numpy which is ignored)
# - Minor updates: Grouped by category (llm-providers, storage, etc.)
# - Patch updates: Grouped by category
# ============================================================
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "weekly"
day: "wednesday"
time: "02:00"
timezone: "Asia/Shanghai"
cooldown:
default-days: 5
semver-major-days: 30
semver-minor-days: 7
semver-patch-days: 3
groups:
# Core dependencies - LLM providers and embeddings
llm-providers:
patterns:
- "openai"
- "anthropic"
- "google-*"
- "boto3"
- "botocore"
- "ollama"
update-types:
- "minor"
- "patch"
# Storage backends
storage:
patterns:
- "neo4j"
- "pymongo"
- "redis"
- "psycopg*"
- "asyncpg"
- "milvus*"
- "qdrant*"
update-types:
- "minor"
- "patch"
# Data processing and ML
data-processing:
patterns:
- "numpy"
- "scipy"
- "pandas"
- "tiktoken"
- "transformers"
- "torch*"
update-types:
- "minor"
- "patch"
# Web framework and API
web-framework:
patterns:
- "fastapi"
- "uvicorn"
- "gunicorn"
- "starlette"
- "pydantic*"
update-types:
- "minor"
- "patch"
# Development and testing tools
dev-tools:
patterns:
- "pytest*"
- "ruff"
- "pre-commit"
- "black"
- "mypy"
update-types:
- "minor"
- "patch"
# Minor and patch updates for everything else
python-minor-patch:
patterns:
- "*"
update-types:
- "minor"
- "patch"
ignore:
- dependency-name: "numpy"
update-types:
- "version-update:semver-major"
labels:
- "dependencies"
- "python"
open-pull-requests-limit: 5
# ============================================================
# Frontend (bun) Dependencies
# PR Strategy:
# - Major updates: Individual PR per package
# - Minor updates: Grouped by category (react, ui-components, etc.)
# - Patch updates: Grouped by category
# ============================================================
- package-ecosystem: "bun"
directory: "/lightrag_webui"
schedule:
interval: "weekly"
day: "friday"
time: "02:00"
timezone: "Asia/Shanghai"
cooldown:
default-days: 5
semver-major-days: 30
semver-minor-days: 7
semver-patch-days: 3
groups:
# React ecosystem
react:
patterns:
- "react"
- "react-dom"
- "react-router*"
- "@types/react*"
update-types:
- "minor"
- "patch"
# UI components and styling
ui-components:
patterns:
- "@radix-ui/*"
- "tailwind*"
- "@tailwindcss/*"
- "lucide-react"
- "class-variance-authority"
- "clsx"
update-types:
- "minor"
- "patch"
# Graph visualization
graph-viz:
patterns:
- "sigma"
- "@sigma/*"
- "graphology*"
update-types:
- "minor"
- "patch"
# Build tools and dev dependencies
build-tools:
patterns:
- "vite"
- "@vitejs/*"
- "typescript"
- "eslint*"
- "@eslint/*"
- "typescript-eslint"
- "prettier"
- "prettier-*"
- "@types/bun"
update-types:
- "minor"
- "patch"
# Content rendering libraries (math, diagrams, etc.)
content-rendering:
patterns:
- "katex"
- "mermaid"
update-types:
- "minor"
- "patch"
# All other minor and patch updates
frontend-minor-patch:
patterns:
- "*"
update-types:
- "minor"
- "patch"
labels:
- "dependencies"
- "frontend"
open-pull-requests-limit: 5
+32
View File
@@ -0,0 +1,32 @@
<!--
Thanks for contributing to LightRAG!
Please ensure your pull request is ready for review before submitting.
About this template
This template helps contributors provide a clear and concise description of their changes. Feel free to adjust it as needed.
-->
## Description
[Briefly describe the changes made in this pull request.]
## Related Issues
[Reference any related issues or tasks addressed by this pull request.]
## Changes Made
[List the specific changes made in this pull request.]
## Checklist
- [ ] Changes tested locally
- [ ] Code reviewed
- [ ] Documentation updated (if necessary)
- [ ] Unit tests added (if applicable)
## Additional Notes
[Add any additional notes or context for the reviewer(s).]
+58
View File
@@ -0,0 +1,58 @@
name: "Copilot Setup Steps"
# Automatically run the setup steps when they are changed to allow for easy validation, and
# allow manual testing through the repository's "Actions" tab
on:
workflow_dispatch:
push:
paths:
- .github/workflows/copilot-setup-steps.yml
pull_request:
paths:
- .github/workflows/copilot-setup-steps.yml
jobs:
# The job MUST be called `copilot-setup-steps` or it will not be picked up by Copilot.
copilot-setup-steps:
runs-on: ubuntu-latest
# Timeout after 30 minutes (maximum is 59)
timeout-minutes: 30
# You can define any steps you want, and they will run before the agent starts.
# If you do not check out your code, Copilot will do this for you.
steps:
- name: Checkout code
uses: actions/checkout@v7
- name: Set up Python 3.11
uses: actions/setup-python@v6
with:
python-version: '3.11'
- name: Cache pip packages
uses: actions/cache@v6
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-copilot-${{ hashFiles('**/pyproject.toml') }}
restore-keys: |
${{ runner.os }}-pip-copilot-
${{ runner.os }}-pip-
- name: Install Python dependencies
run: |
python -m pip install --upgrade pip
pip install -e ".[api]"
pip install pytest pytest-asyncio httpx2
- name: Create minimal frontend stub for Copilot agent
run: |
mkdir -p lightrag/api/webui
echo '<!DOCTYPE html><html><head><title>LightRAG - Copilot Agent</title></head><body><h1>Copilot Agent Mode</h1></body></html>' > lightrag/api/webui/index.html
echo "Created minimal frontend stub for Copilot agent environment"
- name: Verify installation
run: |
python --version
pip list | grep lightrag
lightrag-server --help || echo "Note: Server requires .env configuration to run"
+113
View File
@@ -0,0 +1,113 @@
name: Build Lite Docker Image
on:
workflow_dispatch:
inputs:
_notes_:
description: '⚠️ Create lite Docker images only after non-trivial version releases.'
required: false
type: boolean
default: false
permissions:
contents: read
id-token: write
packages: write
jobs:
build-and-push-lite:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.x"
- name: Get latest tag
id: get_tag
run: |
LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
if [ -z "$LATEST_TAG" ]; then
LATEST_TAG="sha-$(git rev-parse --short HEAD)"
echo "No tags found, using commit SHA: $LATEST_TAG"
else
echo "Latest tag found: $LATEST_TAG"
fi
PACKAGE_VERSION="${LATEST_TAG#v}"
echo "tag=$LATEST_TAG" >> $GITHUB_OUTPUT
echo "package_version=$PACKAGE_VERSION" >> $GITHUB_OUTPUT
- name: Prepare lite tag
id: lite_tag
run: |
LITE_TAG="${{ steps.get_tag.outputs.tag }}-lite"
echo "Lite image tag: $LITE_TAG"
echo "lite_tag=$LITE_TAG" >> $GITHUB_OUTPUT
- name: Update version definitions
run: |
python scripts/release/set_version.py --core-version "${{ steps.get_tag.outputs.package_version }}"
grep '__version__ = ' lightrag/_version.py
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Login to GitHub Container Registry
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Install cosign
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
- name: Extract metadata for Docker
id: meta
uses: docker/metadata-action@v6
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=raw,value=${{ steps.lite_tag.outputs.lite_tag }}
type=raw,value=lite
- name: Build and push lite Docker image
id: build-and-push
uses: docker/build-push-action@v7
with:
context: .
file: ./Dockerfile.lite
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=min
- name: Sign lite Docker image
if: steps.build-and-push.outputs.digest != ''
env:
DIGEST: ${{ steps.build-and-push.outputs.digest }}
TAGS: ${{ steps.meta.outputs.tags }}
run: |
set -euo pipefail
echo "Signing manifest digest: $DIGEST"
while IFS= read -r tag; do
if [ -z "$tag" ]; then
continue
fi
echo "Signing ${tag}@${DIGEST}"
cosign sign --yes "${tag}@${DIGEST}"
done <<< "$TAGS"
- name: Output image details
run: |
echo "Lite Docker image built and pushed successfully!"
echo "Image tag: ghcr.io/${{ github.repository }}:${{ steps.lite_tag.outputs.lite_tag }}"
echo "Signed manifest digest: ${{ steps.build-and-push.outputs.digest }}"
echo "Base Git tag used: ${{ steps.get_tag.outputs.tag }}"
+109
View File
@@ -0,0 +1,109 @@
name: Build Test Docker Image manually
on:
workflow_dispatch:
inputs:
_notes_:
description: '⚠️ Please create a new git tag before building the docker image.'
required: false
type: boolean
default: false
permissions:
contents: read
id-token: write
packages: write
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v7
with:
fetch-depth: 0 # Fetch all history for tags
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.x"
- name: Get latest tag
id: get_tag
run: |
# Get the latest tag, fallback to commit SHA if no tags exist
LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
if [ -z "$LATEST_TAG" ]; then
LATEST_TAG="sha-$(git rev-parse --short HEAD)"
echo "No tags found, using commit SHA: $LATEST_TAG"
else
echo "Latest tag found: $LATEST_TAG"
fi
PACKAGE_VERSION="${LATEST_TAG#v}"
echo "tag=$LATEST_TAG" >> $GITHUB_OUTPUT
echo "image_tag=$LATEST_TAG" >> $GITHUB_OUTPUT
echo "package_version=$PACKAGE_VERSION" >> $GITHUB_OUTPUT
- name: Update version definitions
run: |
python scripts/release/set_version.py --core-version "${{ steps.get_tag.outputs.package_version }}"
echo "Updated version definitions with ${{ steps.get_tag.outputs.package_version }}"
grep '__version__ = ' lightrag/_version.py
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Login to GitHub Container Registry
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Install cosign
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
- name: Extract metadata for Docker
id: meta
uses: docker/metadata-action@v6
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=raw,value=${{ steps.get_tag.outputs.tag }}
- name: Build and push Docker image
id: build-and-push
uses: docker/build-push-action@v7
with:
context: .
file: ./Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Sign Docker image
if: steps.build-and-push.outputs.digest != ''
env:
DIGEST: ${{ steps.build-and-push.outputs.digest }}
TAGS: ${{ steps.meta.outputs.tags }}
run: |
set -euo pipefail
echo "Signing manifest digest: $DIGEST"
while IFS= read -r tag; do
if [ -z "$tag" ]; then
continue
fi
echo "Signing ${tag}@${DIGEST}"
cosign sign --yes "${tag}@${DIGEST}"
done <<< "$TAGS"
- name: Output image details
run: |
echo "Docker image built and pushed successfully!"
echo "Image tags:"
echo " - ghcr.io/${{ github.repository }}:${{ steps.get_tag.outputs.tag }}"
echo "Signed manifest digest: ${{ steps.build-and-push.outputs.digest }}"
echo "Latest Git tag used: ${{ steps.get_tag.outputs.tag }}"
+120
View File
@@ -0,0 +1,120 @@
name: Build Latest Docker Image on Release
on:
release:
types: [published]
workflow_dispatch:
permissions:
contents: read
id-token: write
packages: write
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v7
with:
fetch-depth: 0 # Fetch all history for tags
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.x"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Login to GitHub Container Registry
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Install cosign
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
- name: Get latest tag
id: get_tag
run: |
if [ "${{ github.event_name }}" = "release" ] && [ -n "${{ github.event.release.tag_name }}" ]; then
TAG="${{ github.event.release.tag_name }}"
else
TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
fi
if [ -z "$TAG" ]; then
echo "No git tag found for docker publish"
exit 1
fi
PACKAGE_VERSION="${TAG#v}"
echo "Found tag: $TAG"
echo "tag=$TAG" >> $GITHUB_OUTPUT
echo "package_version=$PACKAGE_VERSION" >> $GITHUB_OUTPUT
- name: Check if pre-release
id: check_prerelease
run: |
TAG="${{ steps.get_tag.outputs.tag }}"
if [[ "$TAG" == *"rc"* ]] || [[ "$TAG" == *"dev"* ]]; then
echo "is_prerelease=true" >> $GITHUB_OUTPUT
echo "This is a pre-release version: $TAG"
else
echo "is_prerelease=false" >> $GITHUB_OUTPUT
echo "This is a stable release: $TAG"
fi
- name: Update version definitions
run: |
python scripts/release/set_version.py --core-version "${{ steps.get_tag.outputs.package_version }}"
echo "Updated version definitions with ${{ steps.get_tag.outputs.package_version }}"
grep '__version__ = ' lightrag/_version.py
- name: Extract metadata for Docker
id: meta
uses: docker/metadata-action@v6
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=raw,value=${{ steps.get_tag.outputs.tag }}
type=raw,value=latest,enable=${{ steps.check_prerelease.outputs.is_prerelease == 'false' }}
- name: Build and push Docker image
id: build-and-push
uses: docker/build-push-action@v7
with:
context: .
file: ./Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Sign Docker image
if: steps.build-and-push.outputs.digest != ''
env:
DIGEST: ${{ steps.build-and-push.outputs.digest }}
TAGS: ${{ steps.meta.outputs.tags }}
run: |
set -euo pipefail
echo "Signing manifest digest: $DIGEST"
while IFS= read -r tag; do
if [ -z "$tag" ]; then
continue
fi
echo "Signing ${tag}@${DIGEST}"
cosign sign --yes "${tag}@${DIGEST}"
done <<< "$TAGS"
- name: Output image details
run: |
echo "Docker image built and pushed successfully!"
echo "Image tags:"
echo " - ghcr.io/${{ github.repository }}:${{ steps.get_tag.outputs.tag }}"
echo " - ghcr.io/${{ github.repository }}:latest"
echo "Signed manifest digest: ${{ steps.build-and-push.outputs.digest }}"
echo "Latest Git tag used: ${{ steps.get_tag.outputs.tag }}"
+134
View File
@@ -0,0 +1,134 @@
name: Linting and Formatting
on:
push:
branches: [ main, dev ]
pull_request:
branches: [ main, dev ]
types: [opened, synchronize, reopened, ready_for_review]
permissions:
pull-requests: write
jobs:
lint-and-format:
name: Linting and Formatting
if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }}
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v7
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.x'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install pre-commit
- name: Run pre-commit
id: pre-commit
run: pre-commit run --all-files --show-diff-on-failure
- name: Post fix instructions on failure
if: failure() && steps.pre-commit.outcome == 'failure'
run: |
cat >> "$GITHUB_STEP_SUMMARY" << 'EOF'
## ❌ Linting / Formatting checks failed
Pre-commit found issues in your code. Fix them locally and push again:
```bash
# Install pre-commit (one-time setup)
pip install pre-commit
pre-commit install
# Auto-fix all issues
pre-commit run --all-files
# Commit the fixes
git add -u
git commit -m "fix: apply pre-commit formatting fixes"
git push
```
### What was checked
| Hook | Tool | What it fixes |
|------|------|---------------|
| `trailing-whitespace` | pre-commit-hooks | Removes trailing whitespace |
| `end-of-file-fixer` | pre-commit-hooks | Ensures files end with a newline |
| `requirements-txt-fixer` | pre-commit-hooks | Sorts requirements.txt entries |
| `ruff-format` | Ruff | Auto-formats Python code (like Black) |
| `ruff` | Ruff | Fixes Python lint errors (`--fix`) |
> See the diff above for the exact changes needed.
EOF
- name: Comment on PR with fix instructions
if: failure() && steps.pre-commit.outcome == 'failure' && github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
uses: actions/github-script@v9
with:
script: |
const body = `## ❌ Linting / Formatting checks failed
Pre-commit found issues in your code. Run the following locally, then push again:
\`\`\`bash
# Install pre-commit (one-time setup)
pip install pre-commit
pre-commit install
# Auto-fix all issues
pre-commit run --all-files
# Commit the fixes
git add -u
git commit -m "fix: apply pre-commit formatting fixes"
git push
\`\`\`
<details>
<summary>What was checked</summary>
| Hook | Tool | What it fixes |
|------|------|---------------|
| \`trailing-whitespace\` | pre-commit-hooks | Removes trailing whitespace |
| \`end-of-file-fixer\` | pre-commit-hooks | Ensures files end with a newline |
| \`requirements-txt-fixer\` | pre-commit-hooks | Sorts requirements.txt entries |
| \`ruff-format\` | Ruff | Auto-formats Python code (like Black) |
| \`ruff\` | Ruff | Fixes Python lint errors (\`--fix\`) |
</details>
> See the [workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) for the exact diff of required changes.`;
// Find existing bot comment to avoid duplicates
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});
const existing = comments.find(c =>
c.user.type === 'Bot' && c.body.includes('Linting / Formatting checks failed')
);
if (existing) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body,
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body,
});
}
+101
View File
@@ -0,0 +1,101 @@
name: Upload LightRAG-hku Package
on:
release:
types: [published]
workflow_dispatch:
permissions:
contents: read
jobs:
release-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0 # Fetch all history for tags
# Build frontend WebUI
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Build Frontend WebUI
run: |
cd lightrag_webui
bun install --frozen-lockfile
bun run build
cd ..
- name: Verify Frontend Build
run: |
if [ ! -f "lightrag/api/webui/index.html" ]; then
echo "❌ Error: Frontend build failed - index.html not found"
exit 1
fi
echo "✅ Frontend build verified"
echo "Frontend files:"
ls -lh lightrag/api/webui/ | head -10
- uses: actions/setup-python@v6
with:
python-version: "3.x"
- name: Resolve release version
id: get_version
run: |
if [ "${{ github.event_name }}" = "release" ] && [ -n "${{ github.event.release.tag_name }}" ]; then
TAG="${{ github.event.release.tag_name }}"
else
TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
fi
if [ -z "$TAG" ]; then
echo "No git tag found for release build"
exit 1
fi
PACKAGE_VERSION="${TAG#v}"
echo "Found tag: $TAG"
echo "Package version: $PACKAGE_VERSION"
echo "version=$TAG" >> $GITHUB_OUTPUT
echo "package_version=$PACKAGE_VERSION" >> $GITHUB_OUTPUT
- name: Update version definitions
run: |
python scripts/release/set_version.py --core-version "${{ steps.get_version.outputs.package_version }}"
grep '__version__ = ' lightrag/_version.py
- name: Build release distributions
run: |
python -m pip install build
python -m build
- name: Upload distributions
uses: actions/upload-artifact@v7
with:
name: release-dists
path: dist/
pypi-publish:
runs-on: ubuntu-latest
needs:
- release-build
permissions:
id-token: write
environment:
name: pypi
steps:
- name: Retrieve release distributions
uses: actions/download-artifact@v8
with:
name: release-dists
path: dist/
- name: Publish release distributions to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: dist/
+27
View File
@@ -0,0 +1,27 @@
# .github/workflows/stale.yml
name: Mark stale issues and pull requests
on:
schedule:
- cron: '30 22 * * *' # run at 22:30+08 every day
permissions:
issues: write
pull-requests: write
jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v10
with:
days-before-stale: 90 # 90 days
days-before-close: 7 # 7 days after marked as stale
stale-issue-message: 'This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.'
close-issue-message: 'This issue has been automatically closed because it has not had recent activity. Please open a new issue if you still have this problem.'
stale-pr-message: 'This pull request has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs.'
close-pr-message: 'This pull request has been automatically closed because it has not had recent activity.'
# If there are specific labels, exempt them from being marked as stale, for example:
exempt-issue-labels: 'enhancement,tracked'
# exempt-pr-labels: 'bug,enhancement,help wanted'
repo-token: ${{ secrets.GITHUB_TOKEN }} # token provided by GitHub
+61
View File
@@ -0,0 +1,61 @@
name: Offline Unit Tests
on:
push:
branches: [ main, dev ]
pull_request:
branches: [ main, dev ]
types: [opened, synchronize, reopened, ready_for_review]
jobs:
offline-tests:
name: Offline Tests
if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }}
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.12', '3.14']
steps:
- uses: actions/checkout@v7
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
- name: Cache pip packages
uses: actions/cache@v6
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements*.txt', '**/pyproject.toml') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Install dependencies
run: |
python -m pip install --upgrade pip
# Install optional-storage and optional-llm deps too — the offline
# test suite contains mock-based unit tests that construct real
# data classes from these libraries (qdrant-client, pymongo,
# anthropic, boto3, etc.). Without them, importorskip falls back
# and the tests are silently skipped.
pip install -e ".[api,offline-storage,offline-llm]"
pip install pytest pytest-asyncio httpx2
- name: Run offline tests
run: |
# Run only tests marked as 'offline' (no external dependencies)
# Integration tests requiring databases/APIs are skipped by default
pytest tests/ -m offline -v --tb=short
- name: Upload test results
if: always()
uses: actions/upload-artifact@v7
with:
name: test-results-py${{ matrix.python-version }}
path: |
.pytest_cache/
test-results.xml
retention-days: 7
+93
View File
@@ -0,0 +1,93 @@
# Python-related files
__pycache__/
*.py[cod]
*.egg-info/
.eggs/
*.tgz
*.tar.gz
*.ini
# Virtual Environment
.venv/
venv/
# Enviroment Variable Files
.env
.env.backup.*
# Generated Docker Compose files (output of setup wizard)
docker-compose.*.yml
!docker-compose.podman.yml
# Build / Distribution
dist/
build/
site/
# Logs / Reports
*.log
*.log.*
*.logfire
*.coverage/
log/
# Caches
.cache/
.mypy_cache/
.pytest_cache/
.ruff_cache/
.gradio/
.history/
temp/
# IDE / Editor Files
.idea/
.vscode/
.vscode/settings.json
# Framework-specific files
local_neo4jWorkDir/
neo4jWorkDir/
# Data & Storage
inputs/
output/
rag_storage/
data/
# User cumstomized prompt directory
prompts/entity_type/
# Evaluation results
lightrag/evaluation/results/
# Miscellaneous
.DS_Store
TODO.md
ignore_this.txt
*.ignore.*
# Project-specific files
/dickens*/
/book.txt
/ag2_demo_workdir/
# Frontend build output (built during PyPI release)
/lightrag/api/webui/
# temporary test files in project root
/test_*
# AI Agent files
memory-bank
# Ignore .claude/ by default; only track the shared hook and settings
# (local overrides like settings.local.json stay ignored via .claude/*)
.claude/*
!.claude/settings.json
!.claude/hooks/
# Google Jules
.jules/
# native_parser/docx CLI output (audit JSONL + image dir)
/parse_output/
+28
View File
@@ -0,0 +1,28 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
hooks:
- id: trailing-whitespace
exclude: ^lightrag/api/webui/
- id: end-of-file-fixer
exclude: ^lightrag/api/webui/
- id: requirements-txt-fixer
exclude: ^lightrag/api/webui/
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.15.17
hooks:
- id: ruff-format
exclude: ^lightrag/api/webui/
- id: ruff
args: [--fix, --ignore=E402]
exclude: ^lightrag/api/webui/
- repo: https://github.com/mgedmin/check-manifest
rev: "0.51"
hooks:
- id: check-manifest
stages: [manual]
exclude: ^lightrag/api/webui/
+331
View File
@@ -0,0 +1,331 @@
# Repository Guidelines
## Project Overview
LightRAG is a Retrieval-Augmented Generation (RAG) framework that uses graph-based knowledge representation for enhanced information retrieval. The system extracts entities and relationships from documents, builds a knowledge graph, and uses multiple retrieval modes (`local`, `global`, `hybrid`, `mix`, `naive`) for queries.
## Project Structure
Top-level directories:
- **lightrag/**: Core Python package — see *Module Layout* below.
- **lightrag_webui/**: React 19 + TypeScript client (Bun + Vite + Tailwind). UI components in `src/`.
- **scripts/**: `test.sh` (preferred test runner), `setup/` interactive environment wizard (use `make env-*` rather than calling `setup.sh` directly — see *Configuration > Setup Wizard Outputs*), and release tooling.
- **tests/**: Pytest coverage, organized into subdirectories that mirror `lightrag/` (see *Testing* below for layout). Working datasets stay in `inputs/`, `rag_storage/`, and `temp/`; deployment collateral lives in `docs/`, `k8s-deploy/`, and compose files.
### Module Layout (`lightrag/`)
- **lightrag.py**: Main orchestrator class (`LightRAG`) — assembled from mixins (see *LightRAG class composition*). Hosts `ainsert_custom_kg`, `_insert_done`, `_process_extract_entities`, `_refresh_addon_params_cache`, and `addon_params` accessors. Critical: always call `await rag.initialize_storages()` after instantiation.
- **pipeline.py**: `_PipelineMixin` — owns the document ingestion pipeline (`apipeline_enqueue_documents`, `apipeline_process_enqueue_documents`, `apipeline_process_error_documents`), the `parse_native` / `parse_mineru` / `parse_docling` parser dispatchers, multimodal analysis, validation, and the worker scaffolding.
- **utils_pipeline.py**: Pure helpers shared by the pipeline mixin and other entry points: doc-status field access, document identity (source key, content hash), parsed-artifact path resolution, parser payload normalization, multimodal entity augmentation, and `make_lightrag_doc_content`.
- **llm_roles.py**: `RoleSpec` / `RoleLLMConfig` / `_RoleLLMState` / `ROLES` registry plus `_RoleLLMMixin` — role normalization, builder registration, wrapper rebuild, runtime config update, queue cleanup, sanitized config export, queue status reporting. Route role-specific behavior here rather than into provider modules.
- **storage_migrations.py**: `_StorageMigrationMixin``check_and_migrate_data`, `_migrate_entity_relation_data`, `_migrate_chunk_tracking_storage`.
- **addon_params.py**: `ObservableAddonParams` plus `default_addon_params` / `normalize_addon_params` helpers.
- **operate.py**: Core extraction and query operations including entity/relation extraction, chunking, and multi-mode retrieval logic.
- **base.py**: Abstract base classes for storage backends (`BaseKVStorage`, `BaseVectorStorage`, `BaseGraphStorage`, `BaseDocStatusStorage`).
- **kg/**: Storage implementations (JSON, NetworkX, Neo4j, PostgreSQL, MongoDB, Redis, Milvus, Qdrant, Faiss, Memgraph, OpenSearch, NanoVectorDB). The backend registry (`STORAGE_IMPLEMENTATIONS` / `STORAGES`) lives in `kg/__init__.py`; `kg/factory.py::get_storage_class()` resolves backend classes from configuration.
- **llm/**: LLM and embedding provider bindings (OpenAI, Ollama, Azure, Gemini, Bedrock, Anthropic, etc.). All async with caching support.
- **parser/**: Unified parsing layer. `parser/routing.py` resolves engine and filename hints for `legacy`, `native`, `mineru`, and `docling` flows; `parser/debug.py` provides an offline LightRAG stub for the `parser/cli.py` debug entry point (`python -m lightrag.parser.cli`). Native format parsers live as sibling sub-packages under `parser/` (currently `parser/docx/`); external HTTP-based adapters live under `parser/external/` (`mineru`, `docling`) with shared helpers in `parser/external/_common.py`, `_manifest.py`, `_zip.py`.
- **chunker/**: Chunking strategies (token-size, recursive character, semantic vector, paragraph semantic).
- **api/**: FastAPI service (`lightrag_server.py`) with REST endpoints and Ollama-compatible API; routers under `routers/`, static Swagger assets, packaged WebUI output, and Gunicorn launcher.
## Core Architecture
### LightRAG class composition
`LightRAG` is assembled from focused mixins (split out of the previously monolithic `lightrag.py`):
```
LightRAG → _RoleLLMMixin → _StorageMigrationMixin → _PipelineMixin → object
```
The `@final` decorator on `LightRAG` is preserved — the mixin layering is an internal implementation detail, not an external subclassing surface. The public API (`ainsert`, `aquery`, `ainsert_custom_kg`, `initialize_storages`, etc.) is unchanged. `ainsert_custom_kg` and its internal construction logic, `_insert_done`, `_process_extract_entities`, `_refresh_addon_params_cache`, and the `addon_params` property accessors stay on `LightRAG` itself because they cut across multiple flows or depend on prompt-profile state.
### Storage Layer
LightRAG uses 4 storage types with pluggable backends:
- **KV_STORAGE**: LLM response cache, text chunks, document info
- **VECTOR_STORAGE**: Entity/relation/chunk embeddings
- **GRAPH_STORAGE**: Entity-relation graph structure
- **DOC_STATUS_STORAGE**: Document processing status tracking
Each `LightRAG` instance can pass a `workspace` parameter for data isolation. Implementation differs per storage type:
- **File-based**: subdirectories under `working_dir`.
- **Collection-based**: collection name prefixes.
- **Relational DB**: workspace column filtering.
- **Qdrant**: payload-based partitioning.
### Pipeline concurrency contract
The document ingestion pipeline coordinates concurrent writers through `pipeline_status` (a per-workspace shared dict in `lightrag.kg.shared_storage`). These fields are mutated under `get_namespace_lock("pipeline_status", workspace=...)`:
- **`busy`**: any pipeline-busy state. Set by both the processing loop AND destructive jobs (clear / per-doc delete). On its own, `busy=True` does NOT block enqueue — see `destructive_busy` for the exclusive subset.
- **`destructive_busy`**: the busy job is `/documents/clear` or `/documents/{doc_id}` (delete). These DROP storages and remove input files; a concurrent enqueue accepted in this window would write to storage being torn down and silently lose the document. Reservation and the enqueue last-line guard reject when this is True.
- **`scanning`**: a `/documents/scan` task is running (whole lifecycle: classification + processing). Used by the `/scan` endpoint to refuse overlapping scans. Does NOT on its own block uploads/inserts.
- **`scanning_exclusive`**: True only during the scan task's classification phase, when `run_scanning_process` is reading `doc_status` to classify files (PROCESSED → archive, FAILED-without-`full_docs` → retry-as-new, etc.) and possibly deleting stale stubs. Reservation and the enqueue last-line guard reject when this is set. Cleared before the scan transitions to its processing phase, allowing concurrent uploads to land while scan-driven processing finishes.
- **`pending_enqueues`**: count of `/upload`, `/text`, `/texts` endpoints that have reserved a slot (via `_reserve_enqueue_slot`) but whose bg task has not yet completed. Only the scan endpoint reads this — to refuse starting while uploads are mid-flight.
- **`request_pending`**: a nudge to the running processing loop. Set by either (a) `apipeline_process_enqueue_documents` when called while `busy=True` or (b) `apipeline_enqueue_documents` after writing to `doc_status` while `busy=True`. The loop checks it after each batch and re-queries `doc_status` if set.
Mutual-exclusion rules (all checked atomically inside the lock):
| Operation | Refuses if | Writes |
|---|---|---|
| `_reserve_enqueue_slot` | `scanning_exclusive` or `destructive_busy` | `pending_enqueues++` |
| `apipeline_enqueue_documents` (last-line guard) | (`scanning_exclusive` and not `from_scan`) or `destructive_busy` | — |
| Scan endpoint reservation | `busy or scanning or pending_enqueues > 0` | `scanning = True` |
| `apipeline_process_enqueue_documents` entry | (already busy → set `request_pending`, return) | `busy = True` (NOT `destructive_busy`) |
| `clear_documents` / `delete_document` (synchronous reservation) | `busy or scanning or pending_enqueues > 0` | `busy = True`, `destructive_busy = True` |
The contract permits **concurrent enqueue + processing**: a freshly-uploaded doc lands in `doc_status` while the loop is mid-batch, the loop sees `request_pending` after the current batch, re-queries `doc_status`, and picks up the new PENDING row.
For the rest — write ordering of `full_docs` vs `doc_status`, the workspace-scoped `enqueue_serialize` lock around dedup-and-upsert, and the `from_scan=True` bypass — see the docstrings on `apipeline_enqueue_documents` and `apipeline_process_enqueue_documents` in `lightrag/pipeline.py`.
### Query Modes
- **local**: Context-dependent retrieval focused on specific entities
- **global**: Community/summary-based broad knowledge retrieval
- **hybrid**: Combines local and global
- **naive**: Direct vector search without graph
- **mix**: Integrates KG and vector retrieval (recommended with reranker)
## Development Commands
### Setup
```bash
# Install with uv
uv sync
source .venv/bin/activate # Or: .venv\Scripts\activate on Windows
# Install with API support
uv sync --extra api
# Install specific extras
uv sync --extra offline-storage # Storage backends
uv sync --extra offline-llm # LLM providers
uv sync --extra test # Testing dependencies
```
### API Server
```bash
# Copy and configure environment
cp env.example .env # Edit with your LLM/embedding configs
# Build WebUI
cd lightrag_webui
bun install --frozen-lockfile
bun run build
cd ..
# Run server
lightrag-server # Production
uvicorn lightrag.api.lightrag_server:app --reload # Development
lightrag-gunicorn # Multi-worker (gunicorn)
```
### WebUI
```bash
cd lightrag_webui
bun install --frozen-lockfile # Install dependencies
bun run dev # Dev server (Node + Vite)
bun run dev:bun # Dev server (Bun native)
bun run build # Production build
bun run preview # Preview production build
bun run lint # ESLint over *.ts/tsx/js/jsx
# Testing — Bun built-in runner (NOT Vitest/Jest)
bun test # All tests
bun test --watch # Watch mode
bun test --coverage # With coverage report
bun test src/api/lightrag.test.ts # Single test file
```
### Testing
- Use mock-based tests for external services (Redis, httpx, etc.) — do not depend on live services in unit tests.
- Add regression tests for every bug fix.
- Run the full test suite (or relevant subset) and report pass counts before declaring done.
- Backend tests use pytest; frontend unit tests use Bun's built-in runner — see *WebUI* above.
```bash
# Preferred for fresh shells and automation; resolves PYTHON, venv, uv, .venv, venv, python, python3
./scripts/test.sh tests
# Run specific test file
./scripts/test.sh tests/kg/test_graph_storage.py
# Run with custom workers
./scripts/test.sh tests --test-workers 4
```
- `tests/`: main test suite, mirrors feature folders. Place new tests under the subdirectory matching the module under test:
- `tests/api/{auth,config,routes}/` for FastAPI server tests (auth/token, config loading, route handlers); top-level `tests/api/` for app-wide concerns (path prefixes, Ollama-compatible endpoint).
- `tests/chunker/`, `tests/evaluation/`, `tests/extraction/` for the like-named modules.
- `tests/kg/<backend>_impl/` for backend-specific storage tests, mirroring the `lightrag/kg/<backend>_impl.py` file naming. The `_impl` suffix on every subdirectory keeps the layout uniform and avoids `sys.path` shadowing on names that overlap with top-level PyPI/stdlib packages (`faiss`, `json`, `neo4j`, `networkx`, `redis`) when a test is launched directly via `python tests/kg/...`. Current backends: `faiss_impl/`, `json_impl/`, `memgraph_impl/`, `milvus_impl/`, `mongo_impl/`, `nano_impl/`, `neo4j_impl/`, `networkx_impl/`, `opensearch_impl/`, `postgres_impl/`, `qdrant_impl/`, `redis_impl/`. `tests/kg/` root holds cross-backend tests (`test_graph_storage`, `test_batch_graph_operations`, `test_unified_lock_safety`, `test_file_atomic`).
- `tests/llm/<provider>_impl/` for provider-specific behavior, same `_impl` convention: `bedrock_impl/`, `gemini_impl/`, `ollama_impl/`, `openai_impl/`, `voyageai_impl/`, `zhipu_impl/`. `tests/llm/` root holds cross-provider concerns (embedding, VLM, cache, role).
- `tests/parser/`, `tests/parser/docx/`, `tests/parser/external/{mineru,docling}/` for parser implementations.
- `tests/pipeline/` for ingestion pipeline and doc-status behavior (including `test_pipeline_*`, `test_doc_status_*`, `test_multimodal_*`, `test_graph_keyed_locks`).
- `tests/sidecar/`, `tests/setup/`, `tests/workspace/` for the like-named cross-cutting concerns.
- When adding a new backend or LLM provider, create a new subdirectory plus an empty `__init__.py` rather than dropping the file in the parent directory root.
- Markers (see `tests/pytest.ini`): `offline`, `integration`, `requires_db`, `requires_api`. Integration tests are skipped by default via `-m "not integration"`.
- Integration env vars: `LIGHTRAG_RUN_INTEGRATION=true`, `LIGHTRAG_KEEP_ARTIFACTS=true`, `LIGHTRAG_TEST_WORKERS=4`, plus storage-specific connection strings.
### Linting
```bash
ruff check .
```
## Key Implementation Patterns
### LightRAG Initialization (Critical)
The most common error is forgetting to initialize storages (manifests as `AttributeError: __aenter__` or `KeyError: 'history_messages'`):
```python
import asyncio
from lightrag import LightRAG
from lightrag.llm.openai import gpt_4o_mini_complete, openai_embed
async def main():
rag = LightRAG(
working_dir="./rag_storage",
llm_model_func=gpt_4o_mini_complete,
embedding_func=openai_embed
)
# REQUIRED: Initialize storage backends
await rag.initialize_storages()
# Now safe to use
await rag.ainsert("Your text here")
result = await rag.aquery("Your question", param=QueryParam(mode="hybrid"))
# Cleanup
await rag.finalize_storages()
asyncio.run(main())
```
### Custom Embedding Functions
Use `@wrap_embedding_func_with_attrs` decorator and call `.func` when wrapping (already-decorated functions cannot be wrapped again — access the underlying via `.func`):
```python
from lightrag.utils import wrap_embedding_func_with_attrs
@wrap_embedding_func_with_attrs(embedding_dim=1536, max_token_size=8192)
async def custom_embed(texts: list[str]) -> np.ndarray:
# Call underlying function, not wrapped version
return await openai_embed.func(texts, model="text-embedding-3-large")
# Wrong: EmbeddingFunc(func=openai_embed)
# Right: EmbeddingFunc(func=openai_embed.func)
```
> **Pitfall — switching embedding models**: when changing the embedding model you MUST clear the data directory (optionally keeping `kv_store_llm_response_cache.json` for LLM cache). Existing vectors will not match the new model's space.
### Storage Configuration
Configure via environment variables or constructor params:
```python
# Environment-based (recommended for production)
# See env.example for full list
# Constructor-based
rag = LightRAG(
working_dir="./storage",
workspace="project_name", # For data isolation
kv_storage="PGKVStorage",
vector_storage="PGVectorStorage",
graph_storage="Neo4JStorage",
doc_status_storage="PGDocStatusStorage",
vector_db_storage_cls_kwargs={
"cosine_better_than_threshold": 0.2
}
)
```
### Document Insertion
```python
# Single document
await rag.ainsert("Text content")
# Batch insertion
await rag.ainsert(["Text 1", "Text 2", ...])
# With custom IDs
await rag.ainsert("Text", ids=["doc-123"])
# With file paths (for citation)
await rag.ainsert(["Text 1", "Text 2"], file_paths=["doc1.pdf", "doc2.pdf"])
# Configure batch size
rag = LightRAG(..., max_parallel_insert=4) # Default: 3, max recommended: 10
```
### Query Configuration
```python
from lightrag import QueryParam
result = await rag.aquery(
"Your question",
param=QueryParam(
mode="mix", # Recommended with reranker
top_k=60, # KG entities/relations to retrieve
chunk_top_k=20, # Text chunks to retrieve
max_entity_tokens=6000,
max_relation_tokens=8000,
max_total_tokens=30000,
enable_rerank=True,
user_prompt="Additional instructions for LLM",
stream=False
)
)
```
## Frontend Debugging via Playwright
For WebUI bugs whose symptoms only surface in the rendered DOM — layout/overflow/scrollbar issues, transient flashes, third-party libraries attaching helpers to `<body>` outside React's tree, or end-to-end verification of a fix — drive the running dev server (`http://localhost:5173`) with the `document-skills:webapp-testing` skill instead of reasoning from source alone. Seed state directly via `localStorage` (persist key `settings-storage`, schema in `lightrag_webui/src/stores/settings.ts`) to skip live LLM calls. Use `wait_until="domcontentloaded"` plus a selector wait — Vite dev's long-lived polling makes `networkidle` time out.
## Configuration
### .env Configuration
Primary configuration file for API server. Generate it with `make env-base` or copy `env.example` manually. Key sections:
- Server settings (HOST, PORT, CORS)
- Storage backends (connection strings via environment variables)
- Query parameters (TOP_K, MAX_TOTAL_TOKENS, etc.)
- Reranking configuration (RERANK_BINDING, RERANK_MODEL)
- Authentication (AUTH_ACCOUNTS, LIGHTRAG_API_KEY)
See `env.example` for comprehensive template.
### Setup Wizard Outputs
- Keep `.env` host-usable. Container-only hostnames and staged SSL paths belong in the wizard-managed compose layer, not persisted back into `.env`.
- Treat `docker-compose.final.yml` as generated output assembled from `scripts/setup/templates/*.yml`.
- For setup workflow changes, prefer `make env-*` targets over direct `scripts/setup/setup.sh` calls.
## Code Style
### Language
Comments, backend code, log messages, and Git commit messages in English. Frontend uses i18next for multi-language support.
### Python
- Follow PEP 8 with 4-space indentation
- Use type annotations
- Prefer dataclasses for state management
- Use `lightrag.utils.logger` instead of print
- Async/await patterns throughout
### TypeScript / React (incl. WebUI ESLint)
- Functional components with hooks; PascalCase for components
- 2-space indentation, single quotes (enforced by `@stylistic` rules)
- Tailwind utility-first styling
- ESLint stack: TypeScript-ESLint + React Hooks plugin + Prettier; `@typescript-eslint/no-explicit-any` is disabled (allowed)
## Commit and Pull Request Guidance
- If this repo is a fork of `HKUDS/LightRAG`. Target to `HKUDS/LightRAG` when creating PRs, not the fork's own repo.
- PR descriptions should include: summary, motivation, linked issues if applyed, what's changed, what's broken and how it works.
- Write commit messages (subject and body) in English. Commit messages are repository artifacts — like code comments and log messages — not conversational replies, so they follow the English code-style rule above regardless of any per-conversation working language.
+1
View File
@@ -0,0 +1 @@
Strictly follow the rules in ./AGENTS.md
+136
View File
@@ -0,0 +1,136 @@
# syntax=docker/dockerfile:1
# Frontend build stage
# Build frontend assets on the native build platform to avoid
# cross-architecture emulation issues during multi-platform builds.
FROM --platform=$BUILDPLATFORM oven/bun:1 AS frontend-builder
WORKDIR /app
# Copy frontend source code
COPY lightrag_webui/ ./lightrag_webui/
# Build frontend assets for inclusion in the API package
RUN --mount=type=cache,target=/root/.bun/install/cache \
cd lightrag_webui \
&& bun install --frozen-lockfile \
&& bun run build
# Python build stage - using uv for faster package installation
FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim AS builder
ENV DEBIAN_FRONTEND=noninteractive
ENV UV_SYSTEM_PYTHON=1
ENV UV_COMPILE_BYTECODE=1
WORKDIR /app
# Install system deps (Rust is required by some wheels)
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
curl \
build-essential \
pkg-config \
&& rm -rf /var/lib/apt/lists/* \
&& curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
ENV PATH="/root/.cargo/bin:/root/.local/bin:${PATH}"
# Ensure shared data directory exists for uv caches
RUN mkdir -p /root/.local/share/uv
# Copy project metadata and sources
COPY pyproject.toml .
COPY setup.py .
COPY uv.lock .
# Install base, API, and offline extras without the project to improve caching
RUN --mount=type=cache,target=/root/.local/share/uv \
uv sync --frozen --no-dev --extra api --extra offline --no-install-project --no-editable
# Copy project sources after dependency layer
COPY lightrag/ ./lightrag/
# Include pre-built frontend assets from the previous stage
COPY --from=frontend-builder /app/lightrag/api/webui ./lightrag/api/webui
# Sync project in non-editable mode and ensure pip is available for runtime installs
RUN --mount=type=cache,target=/root/.local/share/uv \
uv sync --frozen --no-dev --extra api --extra offline --no-editable \
&& /app/.venv/bin/python -m ensurepip --upgrade
# Prepare offline cache directory and pre-populate tiktoken data
# Use uv run to execute commands from the virtual environment
RUN mkdir -p /app/data/tiktoken \
&& uv run lightrag-download-cache --cache-dir /app/data/tiktoken || status=$?; \
if [ -n "${status:-}" ] && [ "$status" -ne 0 ] && [ "$status" -ne 2 ]; then exit "$status"; fi
# Final stage
# Pin to bookworm: keeps Python 3.12 (venv compat with the builder stage) while
# avoiding Debian trixie's perl 5.40.x exposure (CVE-2026-12087, no patch yet),
# and aligns the final Debian release with the builder (also bookworm).
FROM python:3.12-slim-bookworm
WORKDIR /app
# Install uv for package management
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
ENV UV_SYSTEM_PYTHON=1
# Copy installed packages and application code
COPY --from=builder /root/.local /root/.local
COPY --from=builder /app/.venv /app/.venv
COPY --from=builder /app/lightrag ./lightrag
COPY pyproject.toml .
COPY setup.py .
COPY uv.lock .
# Ensure the installed scripts are on PATH
ENV PATH=/app/.venv/bin:/root/.local/bin:$PATH
# Install dependencies with uv sync (uses locked versions from uv.lock)
# And ensure pip is available for runtime installs
RUN --mount=type=cache,target=/root/.local/share/uv \
uv sync --frozen --no-dev --extra api --extra offline --no-editable \
&& /app/.venv/bin/python -m ensurepip --upgrade
# Create persistent data directories AFTER package installation
RUN mkdir -p /app/data/rag_storage /app/data/inputs /app/data/prompts /app/data/tiktoken
# Copy offline cache into the newly created directory
COPY --from=builder /app/data/tiktoken /app/data/tiktoken
# Point to the prepared cache
ENV TIKTOKEN_CACHE_DIR=/app/data/tiktoken
ENV WORKING_DIR=/app/data/rag_storage
ENV INPUT_DIR=/app/data/inputs
ENV PROMPT_DIR=/app/data/prompts
# Create a non-root user (CIS Docker 4.1) and install gosu for privilege drop.
# Fixed UID/GID 1000 gives predictable ownership for bind-mounts / PVCs.
# chown -R /app MUST run after every data COPY above so the venv (pipmaster
# installs packages at runtime), data dirs, and the tiktoken cache are writable.
RUN apt-get update \
&& apt-get install -y --no-install-recommends gosu \
&& rm -rf /var/lib/apt/lists/* \
&& groupadd -g 1000 lightrag \
&& useradd -u 1000 -g lightrag -m -d /home/lightrag -s /usr/sbin/nologin lightrag \
&& chown -R lightrag:lightrag /app /home/lightrag
# HOME and cache dirs for the non-root user so pipmaster's runtime pip installs
# never fall back to an unwritable /root or a missing HOME.
ENV HOME=/home/lightrag \
XDG_CACHE_HOME=/home/lightrag/.cache \
PIP_CACHE_DIR=/home/lightrag/.cache/pip \
UV_CACHE_DIR=/home/lightrag/.cache/uv
# Entrypoint starts as root, fixes mount ownership, then drops to lightrag.
COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
# Expose API port
EXPOSE 9621
ENTRYPOINT ["docker-entrypoint.sh"]
CMD ["python", "-m", "lightrag.api.lightrag_server"]
+137
View File
@@ -0,0 +1,137 @@
# syntax=docker/dockerfile:1
# Frontend build stage
# Build frontend assets on the native build platform to avoid
# cross-architecture emulation issues during multi-platform builds.
FROM --platform=$BUILDPLATFORM oven/bun:1 AS frontend-builder
WORKDIR /app
# Copy frontend source code
COPY lightrag_webui/ ./lightrag_webui/
# Build frontend assets for inclusion in the API package
RUN --mount=type=cache,target=/root/.bun/install/cache \
cd lightrag_webui \
&& bun install --frozen-lockfile \
&& bun run build
# Python build stage - using uv for package installation
FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim AS builder
ENV DEBIAN_FRONTEND=noninteractive
ENV UV_SYSTEM_PYTHON=1
ENV UV_COMPILE_BYTECODE=1
WORKDIR /app
# Install system dependencies required by some wheels
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
curl \
build-essential \
pkg-config \
&& rm -rf /var/lib/apt/lists/* \
&& curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
ENV PATH="/root/.cargo/bin:/root/.local/bin:${PATH}"
# Ensure shared data directory exists for uv caches
RUN mkdir -p /root/.local/share/uv
# Copy project metadata and sources
COPY pyproject.toml .
COPY setup.py .
COPY uv.lock .
# Install project dependencies (base + API extras) without the project to improve caching
RUN --mount=type=cache,target=/root/.local/share/uv \
uv sync --frozen --no-dev --extra api --no-install-project --no-editable
# Copy project sources after dependency layer
COPY lightrag/ ./lightrag/
# Include pre-built frontend assets from the previous stage
COPY --from=frontend-builder /app/lightrag/api/webui ./lightrag/api/webui
# Sync project in non-editable mode and ensure pip is available for runtime installs
RUN --mount=type=cache,target=/root/.local/share/uv \
uv sync --frozen --no-dev --extra api --no-editable \
&& /app/.venv/bin/python -m ensurepip --upgrade
# Prepare tiktoken cache directory and pre-populate tokenizer data
# Ignore exit code 2 which indicates assets already cached
RUN mkdir -p /app/data/tiktoken \
&& uv run lightrag-download-cache --cache-dir /app/data/tiktoken || status=$?; \
if [ -n "${status:-}" ] && [ "$status" -ne 0 ] && [ "$status" -ne 2 ]; then exit "$status"; fi
# Final stage
# Pin to bookworm: keeps Python 3.12 (venv compat with the builder stage) while
# avoiding Debian trixie's perl 5.40.x exposure (CVE-2026-12087, no patch yet),
# and aligns the final Debian release with the builder (also bookworm).
FROM python:3.12-slim-bookworm
WORKDIR /app
# Install uv for package management
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
ENV UV_SYSTEM_PYTHON=1
# Copy installed packages and application code
COPY --from=builder /root/.local /root/.local
COPY --from=builder /app/.venv /app/.venv
COPY --from=builder /app/lightrag ./lightrag
COPY pyproject.toml .
COPY setup.py .
COPY uv.lock .
# Ensure the installed scripts are on PATH
ENV PATH=/app/.venv/bin:/root/.local/bin:$PATH
# Sync dependencies inside the final image using uv
# And ensure pip is available for runtime installs
RUN --mount=type=cache,target=/root/.local/share/uv \
uv sync --frozen --no-dev --extra api --no-editable \
&& /app/.venv/bin/python -m ensurepip --upgrade
# Create persistent data directories
RUN mkdir -p /app/data/rag_storage /app/data/inputs /app/data/prompts /app/data/tiktoken
# Copy cached tokenizer assets prepared in the builder stage
COPY --from=builder /app/data/tiktoken /app/data/tiktoken
# Docker data directories
ENV TIKTOKEN_CACHE_DIR=/app/data/tiktoken
ENV WORKING_DIR=/app/data/rag_storage
ENV INPUT_DIR=/app/data/inputs
ENV PROMPT_DIR=/app/data/prompts
# Create a non-root user (CIS Docker 4.1) and install gosu for privilege drop.
# Fixed UID/GID 1000 gives predictable ownership for bind-mounts / PVCs.
# chown -R /app MUST run after every data COPY above so the venv (pipmaster
# installs packages at runtime), data dirs, and the tiktoken cache are writable.
RUN apt-get update \
&& apt-get install -y --no-install-recommends gosu \
&& rm -rf /var/lib/apt/lists/* \
&& groupadd -g 1000 lightrag \
&& useradd -u 1000 -g lightrag -m -d /home/lightrag -s /usr/sbin/nologin lightrag \
&& chown -R lightrag:lightrag /app /home/lightrag
# HOME and cache dirs for the non-root user so pipmaster's runtime pip installs
# never fall back to an unwritable /root or a missing HOME.
ENV HOME=/home/lightrag \
XDG_CACHE_HOME=/home/lightrag/.cache \
PIP_CACHE_DIR=/home/lightrag/.cache/pip \
UV_CACHE_DIR=/home/lightrag/.cache/uv
# Entrypoint starts as root, fixes mount ownership, then drops to lightrag.
COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
# Expose API port
EXPOSE 9621
# Set entrypoint
ENTRYPOINT ["docker-entrypoint.sh"]
CMD ["python", "-m", "lightrag.api.lightrag_server"]
+43
View File
@@ -0,0 +1,43 @@
# Build stage: compile Apache AGE against PostgreSQL 18 on top of pgvector
FROM pgvector/pgvector:pg18-trixie AS build
RUN apt-get update \
&& apt-get install -y --no-install-recommends --no-install-suggests \
ca-certificates \
git \
bison \
build-essential \
flex \
postgresql-server-dev-18
RUN git clone --depth 1 --branch release/PG18/1.7.0 https://github.com/apache/age.git /usr/src/age \
&& cd /usr/src/age \
&& make \
&& make install
# Final stage: Create a final image by copying the files created in the build stage
FROM pgvector/pgvector:pg18-trixie
RUN apt-get update \
&& apt-get install -y --no-install-recommends --no-install-suggests \
locales
RUN echo "en_US.UTF-8 UTF-8" > /etc/locale.gen \
&& locale-gen \
&& update-locale LANG=en_US.UTF-8
ENV LANG=en_US.UTF-8
ENV LC_COLLATE=en_US.UTF-8
ENV LC_CTYPE=en_US.UTF-8
COPY --from=build /usr/lib/postgresql/18/lib/age.so /usr/lib/postgresql/18/lib/
COPY --from=build /usr/share/postgresql/18/extension/age--1.7.0.sql /usr/share/postgresql/18/extension/
COPY --from=build /usr/share/postgresql/18/extension/age.control /usr/share/postgresql/18/extension/
RUN printf '%s\n' \
'CREATE EXTENSION IF NOT EXISTS vector;' \
'CREATE EXTENSION IF NOT EXISTS age CASCADE;' \
> /docker-entrypoint-initdb.d/00-create-extensions.sql
# Note: AGE extension require to be loaded shared_preload_libraries
CMD ["postgres", "-c", "shared_preload_libraries=age"]
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 LightRAG Team
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+5
View File
@@ -0,0 +1,5 @@
include requirements.txt
include lightrag/api/requirements.txt
recursive-include lightrag/api/webui *
recursive-include lightrag/api/static *
recursive-include prompts/samples *
+99
View File
@@ -0,0 +1,99 @@
SHELL := /bin/bash
SETUP_SCRIPT := scripts/setup/setup.sh
SETUP_BASH ?= $(or $(firstword $(wildcard /opt/homebrew/bin/bash /usr/local/bin/bash /opt/local/bin/bash)),$(shell command -v bash 2>/dev/null),bash)
SETUP_OPTS ?=
COLOR_RESET := \033[0m
COLOR_BOLD := \033[1m
COLOR_BLUE := \033[34m
COLOR_GREEN := \033[32m
COLOR_YELLOW := \033[33m
ifeq ($(NO_COLOR),1)
COLOR_RESET :=
COLOR_BOLD :=
COLOR_BLUE :=
COLOR_GREEN :=
COLOR_YELLOW :=
endif
.PHONY: help dev configure env-base env-storage env-server env-validate env-backup env-security-check env-base-rewrite env-storage-rewrite env base storage server validate backup security security-check base-rewrite storage-rewrite
help:
@printf "$(COLOR_BOLD)Interactive setup targets$(COLOR_RESET)\n"
@printf " $(COLOR_GREEN)make dev$(COLOR_RESET) Bootstrap local dev+test+offline env with uv + bun\n"
@printf " $(COLOR_GREEN)make env-base$(COLOR_RESET) Configure LLM, embedding, and reranker (run first)\n"
@printf " $(COLOR_GREEN)make env-storage$(COLOR_RESET) Configure storage backends and databases\n"
@printf " $(COLOR_GREEN)make env-server$(COLOR_RESET) Configure server, security, and SSL\n"
@printf " $(COLOR_GREEN)make env-validate$(COLOR_RESET) Validate existing .env\n"
@printf " $(COLOR_GREEN)make env-security-check$(COLOR_RESET) Audit existing .env for security risks\n"
@printf " $(COLOR_GREEN)make env-backup$(COLOR_RESET) Backup current .env\n"
@printf " $(COLOR_GREEN)make env-base-rewrite$(COLOR_RESET) Force-regenerate wizard-managed compose services during base setup\n"
@printf " $(COLOR_GREEN)make env-storage-rewrite$(COLOR_RESET) Force-regenerate wizard-managed compose services during storage setup\n"
@printf " $(COLOR_GREEN)make base$(COLOR_RESET) Short form of make env-base (all env prefix can be stripped)\n"
@printf "\n"
@printf "$(COLOR_BOLD)Typical workflow$(COLOR_RESET)\n"
@printf " 1. make dev # install backend/test deps and build frontend\n"
@printf " 2. make env-base # set LLM/embedding/reranker\n"
@printf " 3. make env-storage # set storage backends (optional)\n"
@printf " 4. make env-server # set port/security/SSL (optional)\n\n"
@printf "$(COLOR_BOLD)Examples$(COLOR_RESET)\n"
@printf " make dev\n"
@printf " make env-base\n"
@printf " make env-storage SETUP_OPTS=--debug\n"
@printf " make env-server\n\n"
@printf " make env-storage-rewrite\n\n"
@printf " make env-security-check\n\n"
@printf "$(COLOR_BOLD)Compose Output$(COLOR_RESET)\n"
@printf " Bundled service images are defined in scripts/setup/templates/*.yml.\n"
@printf " Compose file output: docker-compose.final.yml\n"
dev:
@if ! command -v uv >/dev/null 2>&1; then \
printf "$(COLOR_YELLOW)uv is required for make dev.$(COLOR_RESET)\n"; \
printf "Install uv first: https://docs.astral.sh/uv/getting-started/installation/\n"; \
printf "Unix/macOS: curl -LsSf https://astral.sh/uv/install.sh | sh\n"; \
printf "Windows: powershell -c \"irm https://astral.sh/uv/install.ps1 | iex\"\n"; \
exit 1; \
fi
@if ! command -v bun >/dev/null 2>&1; then \
printf "$(COLOR_YELLOW)bun is required for make dev.$(COLOR_RESET)\n"; \
printf "Install Bun first: https://bun.sh/docs/installation\n"; \
printf "macOS/Linux: curl -fsSL https://bun.sh/install | bash\n"; \
printf "Windows: powershell -c \"irm bun.sh/install.ps1 | iex\"\n"; \
exit 1; \
fi
@printf "$(COLOR_BLUE)Syncing backend and test dependencies with uv...$(COLOR_RESET)\n"
@uv sync --extra test --extra offline
@printf "$(COLOR_BLUE)Installing frontend dependencies with Bun...$(COLOR_RESET)\n"
@cd lightrag_webui && bun install --frozen-lockfile
@printf "$(COLOR_BLUE)Building frontend assets...$(COLOR_RESET)\n"
@cd lightrag_webui && bun run build
@printf "$(COLOR_GREEN)Development environment is ready.$(COLOR_RESET)\n"
@printf "Next steps:\n"
@printf " source .venv/bin/activate\n"
@printf " make env-base\n"
@printf " lightrag-server\n"
env-base env base configure:
@$(SETUP_BASH) $(SETUP_SCRIPT) --base $(SETUP_OPTS)
env-storage storage:
@$(SETUP_BASH) $(SETUP_SCRIPT) --storage $(SETUP_OPTS)
env-base-rewrite base-rewrite:
@$(SETUP_BASH) $(SETUP_SCRIPT) --base --rewrite-compose $(SETUP_OPTS)
env-storage-rewrite storage-rewrite:
@$(SETUP_BASH) $(SETUP_SCRIPT) --storage --rewrite-compose $(SETUP_OPTS)
env-server server:
@$(SETUP_BASH) $(SETUP_SCRIPT) --server $(SETUP_OPTS)
env-validate validate:
@$(SETUP_BASH) $(SETUP_SCRIPT) --validate $(SETUP_OPTS)
env-security-check security security-check:
@$(SETUP_BASH) $(SETUP_SCRIPT) --security-check $(SETUP_OPTS)
env-backup backup:
@$(SETUP_BASH) $(SETUP_SCRIPT) --backup $(SETUP_OPTS)
+530
View File
@@ -0,0 +1,530 @@
<div align="center">
<div style="margin: 20px 0;">
<img src="./assets/logo.png" width="120" height="120" alt="LightRAG Logo" style="border-radius: 20px; box-shadow: 0 8px 32px rgba(0, 217, 255, 0.3);">
</div>
# 🚀 LightRAG: シンプルかつ高速な検索拡張生成(RAG)
<div align="center">
<a href="https://trendshift.io/repositories/13043" target="_blank"><img src="https://trendshift.io/api/badge/repositories/13043" alt="HKUDS%2FLightRAG | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
</div>
<p>
</p>
<div align="center">
<div style="width: 100%; height: 2px; margin: 20px 0; background: linear-gradient(90deg, transparent, #00d9ff, transparent);"></div>
</div>
<div align="center">
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 15px; padding: 25px; text-align: center;">
<p>
<a href='https://github.com/HKUDS/LightRAG'><img src='https://img.shields.io/badge/🔥Project-Page-00d9ff?style=for-the-badge&logo=github&logoColor=white&labelColor=1a1a2e'></a>
<a href='https://arxiv.org/abs/2410.05779'><img src='https://img.shields.io/badge/📄arXiv-2410.05779-ff6b6b?style=for-the-badge&logo=arxiv&logoColor=white&labelColor=1a1a2e'></a>
<a href="https://github.com/HKUDS/LightRAG/stargazers"><img src='https://img.shields.io/github/stars/HKUDS/LightRAG?color=00d9ff&style=for-the-badge&logo=star&logoColor=white&labelColor=1a1a2e' /></a>
</p>
<p>
<img src="https://img.shields.io/badge/🐍Python-3.10-4ecdc4?style=for-the-badge&logo=python&logoColor=white&labelColor=1a1a2e">
<a href="https://pypi.org/project/lightrag-hku/"><img src="https://img.shields.io/pypi/v/lightrag-hku.svg?style=for-the-badge&logo=pypi&logoColor=white&labelColor=1a1a2e&color=ff6b6b"></a>
</p>
<p>
<a href="https://discord.gg/yF2MmDJyGJ"><img src="https://img.shields.io/badge/💬Discord-Community-7289da?style=for-the-badge&logo=discord&logoColor=white&labelColor=1a1a2e"></a>
<a href="https://github.com/HKUDS/LightRAG/issues/285"><img src="https://img.shields.io/badge/💬WeChat-Group-07c160?style=for-the-badge&logo=wechat&logoColor=white&labelColor=1a1a2e"></a>
</p>
<p>
<a href="README-zh.md"><img src="https://img.shields.io/badge/🇨🇳中文版-1a1a2e?style=for-the-badge"></a>
<a href="README.md"><img src="https://img.shields.io/badge/🇺🇸English-1a1a2e?style=for-the-badge"></a>
<a href="README-ja.md"><img src="https://img.shields.io/badge/🇯🇵日本語版-1a1a2e?style=for-the-badge"></a>
</p>
<p>
<a href="https://pepy.tech/projects/lightrag-hku"><img src="https://static.pepy.tech/personalized-badge/lightrag-hku?period=total&units=INTERNATIONAL_SYSTEM&left_color=BLACK&right_color=GREEN&left_text=downloads"></a>
<a href="https://hvtracker.net/agents/lightrag/"><img src="https://hvtracker.net/badge/lightrag.svg"></a>
</p>
</div>
</div>
</div>
<div align="center" style="margin: 30px 0;">
<img src="https://user-images.githubusercontent.com/74038190/212284100-561aa473-3905-4a80-b561-0d28506553ee.gif" width="800">
</div>
<div align="center" style="margin: 30px 0;">
<img src="./README.assets/b2aaf634151b4706892693ffb43d9093.png" width="800" alt="LightRAG Diagram">
</div>
---
<div align="center">
<table>
<tr>
<td style="vertical-align: middle;">
<img src="./assets/LiteWrite.png"
width="56"
height="56"
alt="LiteWrite"
style="border-radius: 12px;" />
</td>
<td style="vertical-align: middle; padding-left: 12px;">
<a href="https://litewrite.ai">
<img src="https://img.shields.io/badge/🚀%20LiteWrite-AI%20Native%20LaTeX%20Editor-ff6b6b?style=for-the-badge&logoColor=white&labelColor=1a1a2e">
</a>
</td>
</tr>
</table>
</div>
---
## 🎉 ニュース
- [2026.05]🎯[新機能]: **RagAnything を LightRAG に統合**🎉。**MinerU / Docling** サービスによるマルチモーダルコンテンツの解析・抽出に対応。
- [2026.05]🎯[新機能]: 選択可能な4種類のテキストチャンキング戦略を導入: `Fix``Recursive``Vector``Paragraph`
- [2026.05]🎯[新機能]: **ロール別 LLM 設定**に対応。EXTRACT、QUERY、KEYWORDS、VLM の4つの異なるロールに対し、それぞれ独立した LLM 設定が可能。
- [2026.03]🎯[新機能]: **OpenSearch** を統合ストレージバックエンドとして統合し、LightRAG の4つのストレージすべてを包括的にサポート。
- [2026.03]🎯[新機能]: セットアップウィザードを導入。Docker による埋め込み・リランキング・ストレージバックエンドのローカルデプロイに対応。
- [2025.11]🎯[新機能]: **評価のための RAGAS****トレーシングのための Langfuse** を統合。コンテキスト精度メトリクスをサポートするため、クエリ結果とともに取得したコンテキストを返すよう API を更新。
- [2025.10]🎯[スケーラビリティ強化]: 処理上のボトルネックを排除し、**大規模データセットを効率的に**サポート。
- [2025.09]🎯[新機能] Qwen3-30B-A3B などの**オープンソース LLM** に対する知識グラフ抽出精度を向上。
- [2025.08]🎯[新機能] **リランカー**に対応。混合クエリのパフォーマンスを大幅に向上(デフォルトのクエリモードとして設定)。
- [2025.08]🎯[新機能] **ドキュメント削除**機能を追加し、最適なクエリ性能を保つために KG の自動再生成を実施。
- [2025.06]🎯[新リリース] 当チームは [RAG-Anything](https://github.com/HKUDS/RAG-Anything) をリリースしました。テキスト・画像・表・数式をシームレスに処理する**オールインワンのマルチモーダル RAG** システムです。
- [2025.06]🎯[新機能] LightRAG は [RAG-Anything](https://github.com/HKUDS/RAG-Anything) 統合により包括的なマルチモーダルデータ処理に対応しました。PDF、画像、Office ドキュメント、表、数式を含む多様な形式にわたって、シームレスなドキュメント解析と RAG 機能を実現します。詳細は新しい[マルチモーダルセクション](#マルチモーダル機能のアップグレード)を参照してください。
- [2025.03]🎯[新機能] LightRAG は引用機能に対応し、適切な出典の明示とドキュメントのトレーサビリティ向上を実現しました。
- [2025.02]🎯[新機能] MongoDB を統合データ管理のためのオールインワンストレージソリューションとして利用できるようになりました。
- [2025.02]🎯[新リリース] 当チームは [VideoRAG](https://github.com/HKUDS/VideoRAG) をリリースしました。極めて長いコンテキストの動画を理解するための RAG システムです。
- [2025.01]🎯[新リリース] 当チームは [MiniRAG](https://github.com/HKUDS/MiniRAG) をリリースしました。小規模モデルで RAG をよりシンプルにします。
- [2025.01]🎯PostgreSQL をデータ管理のためのオールインワンストレージソリューションとして利用できるようになりました。
- [2024.11]🎯[新リソース] LightRAG の包括的なガイドが [LearnOpenCV](https://learnopencv.com/lightrag) で公開されました。詳細なチュートリアルとベストプラクティスをご覧ください。素晴らしい貢献をいただいたブログ著者に感謝します!
- [2024.11]🎯[新機能] LightRAG WebUI を導入。直感的な Web ベースのダッシュボードを通じて、LightRAG の知識を挿入・クエリ・可視化できるインターフェースです。
- [2024.11]🎯[新機能] [Neo4J をストレージとして利用](https://github.com/HKUDS/LightRAG?tab=readme-ov-file#using-neo4j-for-storage)できるようになり、グラフデータベースのサポートが可能になりました。
- [2024.10]🎯[新機能] [LightRAG 紹介動画](https://youtu.be/oageL-1I0GE)へのリンクを追加しました。LightRAG の機能のウォークスルーです。素晴らしい貢献をいただいた著者に感謝します!
- [2024.10]🎯[新チャンネル] [Discord チャンネル](https://discord.gg/yF2MmDJyGJ)を作成しました!💬 共有・議論・コラボレーションのため、ぜひコミュニティにご参加ください!🎉🎉
<details>
<summary style="font-size: 1.4em; font-weight: bold; cursor: pointer; display: list-item;">
アルゴリズムフローチャート
</summary>
![LightRAG Indexing Flowchart](https://learnopencv.com/wp-content/uploads/2024/11/LightRAG-VectorDB-Json-KV-Store-Indexing-Flowchart-scaled.jpg)
*図1: LightRAG インデックス作成フローチャート - 画像出典: [Source](https://learnopencv.com/lightrag/)*
![LightRAG Retrieval and Querying Flowchart](https://learnopencv.com/wp-content/uploads/2024/11/LightRAG-Querying-Flowchart-Dual-Level-Retrieval-Generation-Knowledge-Graphs-scaled.jpg)
*図2: LightRAG 検索・クエリフローチャート - 画像出典: [Source](https://learnopencv.com/lightrag/)*
</details>
## インストール
**💡 パッケージ管理に uv を使用**: 本プロジェクトでは、高速かつ信頼性の高い Python パッケージ管理のために [uv](https://docs.astral.sh/uv/) を使用しています。まず uv をインストールしてください: `curl -LsSf https://astral.sh/uv/install.sh | sh`Unix/macOS)または `powershell -c "irm https://astral.sh/uv/install.ps1 | iex"`Windows
> **注記**: お好みであれば pip も使用できますが、より良いパフォーマンスと信頼性の高い依存関係管理のため、uv を推奨します。
>
> **📦 オフラインデプロイ**: オフライン環境やエアギャップ環境については、すべての依存関係とキャッシュファイルを事前インストールする手順を記した[オフラインデプロイガイド](./docs/OfflineDeployment.md)を参照してください。
### LightRAG サーバーのインストール
* PyPI からのインストール
```bash
### uv を使って LightRAG サーバーをツールとしてインストール(推奨)
uv tool install "lightrag-hku[api]"
### または pip を使用
# python -m venv .venv
# source .venv/bin/activate # Windows: .venv\Scripts\activate
# pip install "lightrag-hku[api]"
### フロントエンド成果物のビルド
cd lightrag_webui
bun install --frozen-lockfile
bun run build
cd ..
# env ファイルのセットアップ
# env.example ファイルは GitHub リポジトリのルートからダウンロードするか、
# ローカルのソースチェックアウトからコピーして入手してください。
cp env.example .env # .env を自分の LLM・埋め込み設定で更新
# サーバーの起動。デフォルトではすべてのネットワークインターフェース(0.0.0.0)にバインドされます。
# セキュリティ: ネットワークに公開する前に、.env で認証を設定してください
#LIGHTRAG_API_KEY、または AUTH_ACCOUNTS と TOKEN_SECRET の組み合わせ)。ローカル専用
# アクセスの場合は 127.0.0.1 にバインドしてください。認証がない場合、すべてのエンドポイントが公開されます。
# 注記: Ollama 互換の /api/* ルートは、クライアント互換性のためデフォルトで開放されたままです。
# これらにも認証を要求するには WHITELIST_PATHS=/health を設定してください。
lightrag-server
```
* ソースからのインストール
```bash
git clone https://github.com/HKUDS/LightRAG.git
cd LightRAG
# 開発環境のブートストラップ(推奨)
make dev
source .venv/bin/activate # 仮想環境の有効化(Linux/macOS
# Windows の場合: .venv\Scripts\activate
# make dev はテストツールチェーンに加えて、完全なオフラインスタック
#(API、ストレージバックエンド、プロバイダー統合)をインストールし、フロントエンドをビルドします。
# サーバーを起動する前に、make env-base を実行するか env.example を .env にコピーしてください。
# uv による同等の手動手順
# 注記: uv sync は .venv/ ディレクトリに自動的に仮想環境を作成します
uv sync --extra test --extra offline
source .venv/bin/activate # 仮想環境の有効化(Linux/macOS
# Windows の場合: .venv\Scripts\activate
### または仮想環境付きで pip を使用
# python -m venv .venv
# source .venv/bin/activate # Windows: .venv\Scripts\activate
# pip install -e ".[test,offline]"
# フロントエンド成果物のビルド
cd lightrag_webui
bun install --frozen-lockfile
bun run build
cd ..
# env ファイルのセットアップ
make env-base # または: cp env.example .env して手動で更新
# API-WebUI サーバーの起動
lightrag-server
```
* Docker Compose による LightRAG サーバーの起動
```bash
git clone https://github.com/HKUDS/LightRAG.git
cd LightRAG
cp env.example .env # .env を自分の LLM・埋め込み設定で更新
# .env で LLM と埋め込みの設定を変更
docker compose up
```
> LightRAG docker イメージの過去バージョンはこちらで確認できます: [LightRAG Docker Images]( https://github.com/HKUDS/LightRAG/pkgs/container/lightrag)
>
> GitHub Actions により公開された公式 GHCR イメージは、GitHub OIDC を用いた Sigstore Cosign で署名されています。検証コマンドについては [docs/DockerDeployment.md](./docs/DockerDeployment.md#verify-official-ghcr-images-with-cosign) を参照してください。
### セットアップツールによる .env ファイルの作成
`env.example` を手作業で編集する代わりに、対話型のセットアップウィザードを使って設定済みの `.env`、必要に応じて `docker-compose.final.yml` を生成できます:
```bash
make env-base # 必須の最初のステップ: LLM、埋め込み、リランカー
make env-storage # 任意: ストレージバックエンドとデータベースサービス
make env-server # 任意: サーバーポート、認証、SSL
make env-base-rewrite # 任意: ウィザード管理の compose サービスを強制再生成
make env-storage-rewrite # 任意: ウィザード管理の compose サービスを強制再生成
make env-security-check # 任意: 現在の .env のセキュリティリスクを監査
```
各ターゲットの詳細な説明については [docs/InteractiveSetup.md](./docs/InteractiveSetup.md) を参照してください。
## LightRAG について
### 軽量なグラフベース RAG フレームワーク
**LightRAG** は、軽量なナレッジグラフRAGフレームワークであり、Microsoft GraphRAGの効率的な代替手段です。KG(ナレッジグラフ)とベクトル埋め込みを同時に管理する二層アーキテクチャを採用しており、従来のベクトルベースRAGとグラフベースRAGの間にある技術的なギャップを効果的に埋めます。高い拡張性を前提に設計されたLightRAGは、大規模なグラフのインデックス作成および検索における、計算コストの大きさ、応答の遅さ、増分更新コストの高さといった主要課題を解決します。大規模データセットをサポートしながら、30B規模のオープンソース大規模言語モデル(LLM)を用いた場合でも、非常に高いRAG品質を維持できます。
### 機能と利点
- **深いコンテキスト理解:** グラフ構造化インデックスを通じて、LightRAG はエンティティ間の複雑な意味的依存関係を捉え、従来のチャンクベース検索手法に典型的な断片化したコンテキストの限界を克服します。その生成品質とコンテキスト認識は、グローバルな理解や論理的推論を必要とする垂直ドメイン(例: 法律、金融)において特に優れています。
- **卓越した網羅性と多様性:** LightRAG のデュアルレベル検索メカニズムにより、詳細な事実と抽象的な概念を同時に統合できます。これにより、クエリ結果の網羅性と多様性において顕著なパフォーマンスを達成し、複雑なクロスドキュメントクエリの処理に極めて効果的です。
- **極めて高い検索効率と低コスト:** LightRAG は、複雑なクエリに対して非効率なコミュニティレポートやマルチホップ推論に依存しません。これにより、インデックス作成段階とクエリ段階の双方で必要となる LLM 呼び出し回数を大幅に削減し、応答レイテンシと LLM の計算コストを著しく低減します。
- **動的データへの迅速な適応:** LightRAG は、シームレスな増分知識ベース更新をサポートします。新しいデータは標準的なグラフインデックス作成パイプラインを通すだけでローカルグラフを生成し、集合のマージによって既存のグラフに直接統合されます。このプロセスにより、元の構造を破壊したりグローバルインデックスを再構築したりする必要がなくなり、動的なデータ環境におけるリアルタイムな関連性を保証します。ドキュメント削除時には、構築段階での LLM キャッシュを活用して、影響を受けたエンティティ関係を迅速に再構築し、知識ベースの更新効率を大幅に向上させます。
### マルチモーダル機能のアップグレード
バージョン v1.5 から、LightRAG はマルチモーダルドキュメントの分析・検索機能を正式に導入しました:
- **マルチエンジンによるドキュメント解析:** ドキュメント処理パイプラインは MinerU、Docling、Native などの解析エンジンをサポートし、ドキュメントからテキスト・表・数式・画像を高効率に抽出できます。
- **クロスモーダルなエンティティ・関係マッピング:** 統一されたフレームワーク内でクロスモーダルなエンティティ抽出と関係マッピングを実現し、シームレスなインデックス作成とクエリをもたらします。
- **応用シナリオの強化:** まったく新しいマルチモーダル処理パイプラインにより、操作マニュアルや学術論文といったマルチモーダルコンテンツに富むドキュメントに対する RAG 品質が大幅に向上します。
### LightRAG API サーバー
LightRAG サーバーは、LightRAG の機能を探索するための Web ベース UI だけでなく、包括的な REST API も提供します。LightRAG サーバーの詳細については [LightRAG Server](./docs/LightRAG-API-Server.md) を参照してください。
![iShot_2025-03-23_12.40.08](./README.assets/iShot_2025-03-23_12.40.08.png)
## 主要な設定ガイド
### LLM モデルの選択
LightRAG はワークフロー中に4つの異なるロールの LLM/VLM を必要とします。パフォーマンスと処理速度のバランスを取るため、ロールごとに異なる能力と速度のモデルを設定すべきです。LightRAG は、ドキュメントから複雑なエンティティ関係抽出タスクを実行するために LLM を必要とするため、従来の RAG よりも大規模言語モデル(LLM)に対する能力要件が高くなります。クエリ段階では、LLM はエンティティ、関係、テキストチャンクを含む大量の取得情報を処理する必要があります。これには、長くノイズの多いコンテキストの中で高品質な応答を生成する能力がモデルに求められます。詳細なモデル設定については [RoleSpecificLLMConfiguration.md](./docs/RoleSpecificLLMConfiguration.md) を参照してください。
### クエリモードの選択
LightRAG は5つのクエリモードをサポートします:
- **local**: ローカルなコンテキストと特定のエンティティの精密なマッチングに焦点を当てます。知識グラフから候補エンティティとその直接関連する属性を取得します。このモードは、特定の対象、具体的な概念、詳細な事実を狙った Q&A に適しており、関連性が高く詳細なローカルコンテキストのサポートを提供します。
- **global**: マクロなテーマ、クロスドキュメント推論、エンティティ間の深い関係に焦点を当てます。広範なテーマと概念をカバーする関係チェーンを取得します。このモードは、複数のコンテキストにまたがる要約、トレンド分析、複雑な意味的依存関係の理解を必要とするクエリに適しています。
- **hybrid**: local モードと global モードの両方の検索結果をマージします。特定のエンティティとグローバルな関係コンテキストを同時に再現することで、包括的な推論と生成を実行します。
- **naive**: テキストチャンクに基づく従来の RAG 検索です。知識グラフを使用せず、ベクトル類似度に直接依存して元のテキストチャンクから取得します。
- **mix**: local、global、naive モードの検索結果をマージし、最も包括的で豊富な検索結果を提供するフル機能のモードです。
LightRAG のデフォルトのクエリモードは `mix` です。`mix` モードを使用すると、一般に最も理想的なクエリ結果が得られます。`mix` モードは `naive` よりわずかに時間がかかりますが、その他のクエリモードはレイテンシがおおむね同等です。
### 埋め込みモデル
埋め込みモデルを選ぶ際は、その多言語サポート能力に注意してください。LightRAG の検索品質は埋め込みモデルへの依存度が限定的であるため、低次元で高速なモデルを選ぶことを推奨します。通常、`BAAI/bge-m3` で十分です。最良のパフォーマンスを得るため、埋め込みモデルをローカルにデプロイすることを強く推奨します。
**重要な注記**: 埋め込みモデルはドキュメントのインデックス作成前に確定する必要があり、クエリ段階でも同じモデルを使用しなければなりません。一度選択すると、埋め込みモデルは一般に変更できません。変更した場合は、すべてのテキストチャンク、エンティティ、関係を再埋め込みする必要があります。LightRAG は現在、再埋め込みツールを提供していません。一部のストレージバックエンド(例: PostgreSQL)では、テーブルの初回作成時にベクトル次元を定義する必要があるため、埋め込みモデルを変更するにはベクトル関連テーブルを削除し、LightRAG が再作成できるようにする必要があります。
### リランキングの有効化
クエリ段階で Rerank オプションを有効にすると、クエリ品質が大幅に向上します。ただし、Rerank を有効にすると通常 1~2 秒の遅延が生じます。レイテンシを最小化するため、Rerank モデルをローカルにデプロイすることを強く推奨します。設定の詳細については `.env.example` ファイルを参照してください。埋め込みモデルとは異なり、Rerank モデルはクエリ段階でいつでも変更できます。
### ドキュメント処理パイプラインの設定
LightRAG のデフォルトのパイプライン設定では、システムが最高の性能を発揮できません。ドキュメント解析の品質はドキュメントのインデックス作成とクエリに大きく影響します。そのため、MinerU 解析エンジンを有効にし、パイプラインの画像分析機能を有効化するようパイプラインを設定することを推奨します。推奨設定:
```
LIGHTRAG_PARSER=*:native-iteP,*:mineru-iteP,*:legacy-R
VLM_PROCESS_ENABLE=true
VLM_LLM_MODEL=<your_vlm_model_name>
```
クラウドベースの MinerU サービスには利用量・ファイルサイズ・ページ数の制限があるため、ローカルにデプロイした MinerU を使用することを推奨します。ファイル処理パイプラインの設定の詳細については [FileProcessingPipeline.md](./docs/FileProcessingPipeline.md) を参照してください。
### ファイル処理の並行性最適化
大規模なドキュメント処理では、並行性を高める必要があります。ファイルの並行処理に関連する主要な環境変数は以下のとおりです:
- **MAX_ASYNC_LLM/EXTRACT_ASYNC_LLM**: LLM モデルの最大並行数を制御します。
- **MAX_PARALLEL_INSERT**: 並行処理されるファイルの最大数を制御します。1つのファイル内のテキスト・表・数式・画像の処理も並行して行われます。`MAX_PARALLEL_INSERT` は理想的には `MAX_ASYNC_LLM` の約1/3に設定すべきです。
- **MAX_PARALLEL_PARSE_MINERU**: MinerU 解析で並行処理されるファイル数を制御します。
- **MAX_PARALLEL_PARSE_DOCLING**: Docling 解析で並行処理されるファイル数を制御します。
- **EMBEDDING_FUNC_MAX_ASYNC**: 埋め込みモデルの最大並行数を制御します。
- **EMBEDDING_BATCH_NUM**: 埋め込みモデルの各リクエストに含めるテキスト数(1バッチあたりの埋め込み数)を制御します。この数を増やすと、埋め込みモデルへの API 呼び出し回数を大幅に削減でき、埋め込みストレージへのデータ永続化を高速化できます。
```
# 設定例
MAX_ASYNC_LLM=8
MAX_PARALLEL_INSERT=3
EMBEDDING_FUNC_MAX_ASYNC=16
EMBEDDING_BATCH_NUM=32
```
### バックエンドストレージの選択
LightRAG は4種類のバックエンドストレージを必要とします:
- **KV_STORAGE**: LLM 応答キャッシュ、テキストチャンキング結果、エンティティ関係抽出結果などの保存に使用します。
- **VECTOR_STORAGE**: テキストチャンク、エンティティ、関係のベクトル情報の保存に使用します。
- **GRAPH_STORAGE**: 知識グラフの保存に使用します。
- **DOC_STATUS_STORAGE**: ドキュメントリストの保存に使用します。
デフォルトでは、LightRAG のストレージバックエンドはファイル永続化されたインメモリデータベースです。これらのデフォルトストレージは開発・デバッグ用途のみを想定しており、本番環境には適していません。本番環境で、4種類すべてのストレージを単一のバックエンドで扱いたい場合は、PostgreSQL、MongoDB、または OpenSearch を選択できます。あるいは、ベクトルストレージに Milvus や Qdrant、グラフストレージに Neo4j や Memgraph を使用するなど、ベクトルやグラフのストレージに専用データベースを選択することもできます。
### ドキュメント処理に関するその他の重要な設定
ドキュメント挿入段階では、ニーズに応じて以下の環境変数の調整を検討するとよいでしょう:
- **SUMMARY_LANGUAGE**: LLM がエンティティ関係名や要約を出力する際に使用する言語を制御します(例: `Chinese``English`)。
- **ENTITY_EXTRACTION_USE_JSON**: LLM がエンティティ関係抽出を JSON 形式で出力するかどうかを制御します。JSON 形式を使用すると通常より安定した結果が得られますが、より多くのトークンを消費し、やや遅くなることがあります。
- **ENABLE_CONTENT_HEADINGS**: クエリ段階でテキストチャンクのセクション見出し情報を LLM に送信するかどうかを制御します(デフォルトで有効。LLM により多くのコンテキストを提供します)。
- **FORCE_LLM_SUMMARY_ON_MERGE / MAX_SOURCE_IDS_PER_RELATION**: 1つの `entity/relation` が関連付けられるテキストチャンクの最大数を制御します。
- **SOURCE_IDS_LIMIT_METHOD**: ある `entity/relation` が関連テキストチャンク数の上限を超えた後も、エンティティ/関係の説明を更新し続けるかどうかを制御します(デフォルトでは更新を停止します。その時点でエンティティ関係の説明はすでに十分豊富であり、さらなる更新はほとんど価値を加えないためです。更新をスキップすることで知識ベースの構築を大幅に高速化できます)。
- **DEFAULT_MAX_FILE_PATHS**: 1つの `entity/relation` が関連付けられるソースファイルの最大数を制御します。この上限を超えると、新しいファイル名はベクトルストレージに書き込まれなくなります。
### エンティティ・関係抽出時の LLM タイムアウトの解消
エンティティ・関係抽出中の LLM タイムアウトは、通常3つの原因のいずれかに起因します。原因を特定し、対応する対策を適用してください(パラメータは併用できます):
- **モデルが遅い。** 約50トークン/秒を下回るモデルでは、多数のエンティティと関係を含むチャンクを、リクエストがタイムアウトする前に処理しきれない場合があります。`*_LLM_TIMEOUT`(グローバルの `LLM_TIMEOUT`、または抽出フェーズ用のロール別 `EXTRACT_LLM_TIMEOUT`)でタイムアウトを延長してください。実際の実行タイムアウトは設定値の**2倍**になるため、`EXTRACT_LLM_TIMEOUT=300` は最大**600秒**を許容します。
- **チャンクから生成されるエンティティ・関係が多すぎる。** 例えば参考文献のチャンクでは、モデルが膨大な数のレコードを出力し、時間内に完了できないことがあります。`OPENAI_LLM_MAX_TOKENS` または `OPENAI_LLM_MAX_COMPLETION_TOKENS` で出力長を制限してください(正しいパラメータ名は LLM プロバイダーによって異なります。`env.example` を参照)。目安として `max_output_tokens < LLM_TIMEOUT × tokens_per_second`(例: `9000 < 240s × 50 tps`)が有用です。
- **モデルが出力ループに陥る。** 一部のモデル(特にローカル展開された Qwen モデル)は、特定のテキストで際限のない出力ループに陥ることがあります。これが断続的に発生する場合は、ドキュメントを一度再処理するだけで通常は解消します。
- **特に参考文献の場合(P チャンク戦略)。** 段落セマンティック(`P`)チャンク戦略(例: `LIGHTRAG_PARSER=...-iteP`)を使用している場合、`CHUNK_P_DROP_REFERENCES=true` を設定すると、チャンク化の前に末尾の参考文献セクションを自動的に削除します。これにより、参考文献が大量の低価値なエンティティ・関係を生成すること(タイムアウトの一般的な原因)を防ぎます。ファイル名のヒント `paper.[-P(drop_rf=true)].pdf` でファイルごとに有効化することもできます。関連する検出パラメータ(`CHUNK_P_REFERENCES_TAIL_N``CHUNK_P_REFERENCES_HEADINGS`)は `env.example` に記載されています。
### ドキュメントクエリに関するその他の重要な設定
ドキュメントクエリ段階では、ニーズに応じて以下の環境変数の調整を検討するとよいでしょう:
- **MAX_ENTITY_TOKENS / MAX_RELATION_TOKENS / MAX_TOTAL_TOKENS**: LLM コンテキストに送信される取得コンテンツのトークン長を制御します。取得コンテンツは `entities``relations``text chunks` の3つの部分から構成されます。エンティティと関係の長さは独立して制御でき、テキストチャンクの長さは総長からエンティティと関係の長さを差し引いて決まります。
- **ENABLE_CONTENT_HEADINGS**: テキストチャンクが存在するセクション見出しを LLM に送信するかどうかを制御します。デフォルトで有効で、LLM により豊富なコンテキストを提供し、回答品質を向上させます。
- **ENABLE_LLM_CACHE**: クエリ結果をキャッシュするかどうか。デフォルトで有効です。同一のクエリ質問、クエリモード、LLM モデルパラメータであれば同じ結果を返します。
## SDK としての LightRAG の利用
> ⚠️ **プロジェクトへの統合には、LightRAG サーバーが提供する REST API の使用を強く推奨します。** LightRAG SDK は主に組み込みアプリケーションや学術研究・評価目的を想定しています。
### LightRAG SDK のインストール
* ソースコードからのインストール
```bash
cd LightRAG
# 注記: uv sync は .venv/ ディレクトリに自動的に仮想環境を作成します
uv sync
source .venv/bin/activate # 仮想環境の有効化(Linux/macOS
# Windows の場合: .venv\Scripts\activate
# または: pip install -e .
```
* PyPI からのインストール
```bash
uv pip install lightrag-hku
# または: pip install lightrag-hku
```
### LightRAG SDK サンプルコード
LightRAG コアを使い始めるには、`examples` フォルダにあるサンプルコードを参照してください。さらに、ローカルセットアップ手順を案内する[デモ動画](https://www.youtube.com/watch?v=g21royNJ4fw)も用意されています。すでに OpenAI API キーをお持ちであれば、すぐにデモを実行できます:
```bash
### デモコードはプロジェクトフォルダ内で実行してください
cd LightRAG
### OpenAI 用の API-KEY を指定
export OPENAI_API_KEY="sk-...your_opeai_key..."
### Charles Dickens 著「A Christmas Carol」のデモドキュメントをダウンロード
curl https://raw.githubusercontent.com/gusye1234/nano-graphrag/main/tests/mock_data.txt > ./book.txt
### デモコードを実行
python examples/lightrag_openai_demo.py
```
ストリーミング応答の実装例については、`examples/lightrag_openai_compatible_demo.py` を参照してください。実行前に、サンプルコードの LLM と埋め込みの設定を適宜変更してください。
**注記1**: デモプログラムを実行する際は、テストスクリプトによって異なる埋め込みモデルが使用される場合があることに注意してください。別の埋め込みモデルに切り替える場合は、データディレクトリ(`./dickens`)をクリアする必要があります。そうしないとプログラムでエラーが発生する可能性があります。LLM キャッシュを保持したい場合は、データディレクトリをクリアする際に `kv_store_llm_response_cache.json` ファイルを残すことができます。
**注記2**: 公式にサポートされているサンプルコードは `lightrag_openai_demo.py``lightrag_openai_compatible_demo.py` のみです。その他のサンプルファイルはコミュニティによる貢献であり、完全なテストと最適化を経ていません。
### **SDK 利用に関する注記**
SDK の利用に関する詳細な手順については、**[docs/ProgramingWithCore.md](./docs/ProgramingWithCore.md)** を参照してください。一部の LightRAG 機能は REST API では公開されておらず、SDK 経由でのみアクセス可能です。これらの機能は通常、実験的なものであり、将来のバージョンと互換性がない場合があります。
## 論文の結果の再現
LightRAG は、農業、コンピュータサイエンス、法律、混合ドメインにわたって、NaiveRAG、RQ-RAG、HyDE、GraphRAG を一貫して上回ります。完全な評価方法論、プロンプト、再現手順については、**[docs/Reproduce.md](./docs/Reproduce.md)** を参照してください。
**全体性能テーブル**
||**Agriculture**||**CS**||**Legal**||**Mix**||
|----------------------|---------------|------------|------|------------|---------|------------|-------|------------|
||NaiveRAG|**LightRAG**|NaiveRAG|**LightRAG**|NaiveRAG|**LightRAG**|NaiveRAG|**LightRAG**|
|**Comprehensiveness**|32.4%|**67.6%**|38.4%|**61.6%**|16.4%|**83.6%**|38.8%|**61.2%**|
|**Diversity**|23.6%|**76.4%**|38.0%|**62.0%**|13.6%|**86.4%**|32.4%|**67.6%**|
|**Empowerment**|32.4%|**67.6%**|38.8%|**61.2%**|16.4%|**83.6%**|42.8%|**57.2%**|
|**Overall**|32.4%|**67.6%**|38.8%|**61.2%**|15.2%|**84.8%**|40.0%|**60.0%**|
||RQ-RAG|**LightRAG**|RQ-RAG|**LightRAG**|RQ-RAG|**LightRAG**|RQ-RAG|**LightRAG**|
|**Comprehensiveness**|31.6%|**68.4%**|38.8%|**61.2%**|15.2%|**84.8%**|39.2%|**60.8%**|
|**Diversity**|29.2%|**70.8%**|39.2%|**60.8%**|11.6%|**88.4%**|30.8%|**69.2%**|
|**Empowerment**|31.6%|**68.4%**|36.4%|**63.6%**|15.2%|**84.8%**|42.4%|**57.6%**|
|**Overall**|32.4%|**67.6%**|38.0%|**62.0%**|14.4%|**85.6%**|40.0%|**60.0%**|
||HyDE|**LightRAG**|HyDE|**LightRAG**|HyDE|**LightRAG**|HyDE|**LightRAG**|
|**Comprehensiveness**|26.0%|**74.0%**|41.6%|**58.4%**|26.8%|**73.2%**|40.4%|**59.6%**|
|**Diversity**|24.0%|**76.0%**|38.8%|**61.2%**|20.0%|**80.0%**|32.4%|**67.6%**|
|**Empowerment**|25.2%|**74.8%**|40.8%|**59.2%**|26.0%|**74.0%**|46.0%|**54.0%**|
|**Overall**|24.8%|**75.2%**|41.6%|**58.4%**|26.4%|**73.6%**|42.4%|**57.6%**|
||GraphRAG|**LightRAG**|GraphRAG|**LightRAG**|GraphRAG|**LightRAG**|GraphRAG|**LightRAG**|
|**Comprehensiveness**|45.6%|**54.4%**|48.4%|**51.6%**|48.4%|**51.6%**|**50.4%**|49.6%|
|**Diversity**|22.8%|**77.2%**|40.8%|**59.2%**|26.4%|**73.6%**|36.0%|**64.0%**|
|**Empowerment**|41.2%|**58.8%**|45.2%|**54.8%**|43.6%|**56.4%**|**50.8%**|49.2%|
|**Overall**|45.2%|**54.8%**|48.0%|**52.0%**|47.2%|**52.8%**|**50.4%**|49.6%|
## 🔗 関連プロジェクト
*エコシステムと拡張機能*
<div align="center">
<table>
<tr>
<td align="center">
<a href="https://github.com/HKUDS/RAG-Anything">
<div style="width: 100px; height: 100px; background: linear-gradient(135deg, rgba(0, 217, 255, 0.1) 0%, rgba(0, 217, 255, 0.05) 100%); border-radius: 15px; border: 1px solid rgba(0, 217, 255, 0.2); display: flex; align-items: center; justify-content: center; margin-bottom: 10px;">
<span style="font-size: 32px;">📸</span>
</div>
<b>RAG-Anything</b><br>
<sub>マルチモーダル RAG</sub>
</a>
</td>
<td align="center">
<a href="https://github.com/HKUDS/VideoRAG">
<div style="width: 100px; height: 100px; background: linear-gradient(135deg, rgba(0, 217, 255, 0.1) 0%, rgba(0, 217, 255, 0.05) 100%); border-radius: 15px; border: 1px solid rgba(0, 217, 255, 0.2); display: flex; align-items: center; justify-content: center; margin-bottom: 10px;">
<span style="font-size: 32px;">🎥</span>
</div>
<b>VideoRAG</b><br>
<sub>極めて長いコンテキストの動画 RAG</sub>
</a>
</td>
<td align="center">
<a href="https://github.com/HKUDS/MiniRAG">
<div style="width: 100px; height: 100px; background: linear-gradient(135deg, rgba(0, 217, 255, 0.1) 0%, rgba(0, 217, 255, 0.05) 100%); border-radius: 15px; border: 1px solid rgba(0, 217, 255, 0.2); display: flex; align-items: center; justify-content: center; margin-bottom: 10px;">
<span style="font-size: 32px;">✨</span>
</div>
<b>MiniRAG</b><br>
<sub>極めてシンプルな RAG</sub>
</a>
</td>
</tr>
</table>
</div>
---
## ⭐ スター履歴
[![Star History Chart](https://api.star-history.com/svg?repos=HKUDS/LightRAG&type=Date)](https://star-history.com/#HKUDS/LightRAG&Date)
## 🤝 貢献
<div align="center">
バグ修正、新機能、ドキュメントの改善など、あらゆる種類の貢献を歓迎します。<br>
プルリクエストを送信する前に、<a href=".github/CONTRIBUTING.md"><strong>貢献ガイド</strong></a>をお読みください。
</div>
<br>
<div align="center">
貴重な貢献をいただいたすべてのコントリビューターに感謝します。
</div>
<div align="center">
<a href="https://github.com/HKUDS/LightRAG/graphs/contributors">
<img src="https://contrib.rocks/image?repo=HKUDS/LightRAG" style="border-radius: 15px; box-shadow: 0 0 20px rgba(0, 217, 255, 0.3);" />
</a>
</div>
## 📖 引用
```python
@article{guo2024lightrag,
title={LightRAG: Simple and Fast Retrieval-Augmented Generation},
author={Zirui Guo and Lianghao Xia and Yanhua Yu and Tu Ao and Chao Huang},
year={2024},
eprint={2410.05779},
archivePrefix={arXiv},
primaryClass={cs.IR}
}
```
---
<div align="center" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 15px; padding: 30px; margin: 30px 0;">
<div>
<img src="https://user-images.githubusercontent.com/74038190/212284100-561aa473-3905-4a80-b561-0d28506553ee.gif" width="500">
</div>
<div style="margin-top: 20px;">
<a href="https://github.com/HKUDS/LightRAG" style="text-decoration: none;">
<img src="https://img.shields.io/badge/⭐%20Star%20us%20on%20GitHub-1a1a2e?style=for-the-badge&logo=github&logoColor=white">
</a>
<a href="https://github.com/HKUDS/LightRAG/issues" style="text-decoration: none;">
<img src="https://img.shields.io/badge/🐛%20Report%20Issues-ff6b6b?style=for-the-badge&logo=github&logoColor=white">
</a>
<a href="https://github.com/HKUDS/LightRAG/discussions" style="text-decoration: none;">
<img src="https://img.shields.io/badge/💬%20Discussions-4ecdc4?style=for-the-badge&logo=github&logoColor=white">
</a>
</div>
</div>
<div align="center">
<div style="width: 100%; max-width: 600px; margin: 20px auto; padding: 20px; background: linear-gradient(135deg, rgba(0, 217, 255, 0.1) 0%, rgba(0, 217, 255, 0.05) 100%); border-radius: 15px; border: 1px solid rgba(0, 217, 255, 0.2);">
<div style="display: flex; justify-content: center; align-items: center; gap: 15px;">
<span style="font-size: 24px;">⭐</span>
<span style="color: #00d9ff; font-size: 18px;">LightRAG をご覧いただきありがとうございます!</span>
<span style="font-size: 24px;">⭐</span>
</div>
</div>
</div>
+530
View File
@@ -0,0 +1,530 @@
<div align="center">
<div style="margin: 20px 0;">
<img src="./assets/logo.png" width="120" height="120" alt="LightRAG Logo" style="border-radius: 20px; box-shadow: 0 8px 32px rgba(0, 217, 255, 0.3);">
</div>
# 🚀 LightRAG: 简单且快速的检索增强生成(RAG)框架
<div align="center">
<a href="https://trendshift.io/repositories/13043" target="_blank"><img src="https://trendshift.io/api/badge/repositories/13043" alt="HKUDS%2FLightRAG | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
</div>
<p>
</p>
<div align="center">
<div style="width: 100%; height: 2px; margin: 20px 0; background: linear-gradient(90deg, transparent, #00d9ff, transparent);"></div>
</div>
<div align="center">
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 15px; padding: 25px; text-align: center;">
<p>
<a href='https://github.com/HKUDS/LightRAG'><img src='https://img.shields.io/badge/🔥项目-主页-00d9ff?style=for-the-badge&logo=github&logoColor=white&labelColor=1a1a2e'></a>
<a href='https://arxiv.org/abs/2410.05779'><img src='https://img.shields.io/badge/📄arXiv-2410.05779-ff6b6b?style=for-the-badge&logo=arxiv&logoColor=white&labelColor=1a1a2e'></a>
<a href="https://github.com/HKUDS/LightRAG/stargazers"><img src='https://img.shields.io/github/stars/HKUDS/LightRAG?color=00d9ff&style=for-the-badge&logo=star&logoColor=white&labelColor=1a1a2e' /></a>
</p>
<p>
<img src="https://img.shields.io/badge/🐍Python-3.10-4ecdc4?style=for-the-badge&logo=python&logoColor=white&labelColor=1a1a2e">
<a href="https://pypi.org/project/lightrag-hku/"><img src="https://img.shields.io/pypi/v/lightrag-hku.svg?style=for-the-badge&logo=pypi&logoColor=white&labelColor=1a1a2e&color=ff6b6b"></a>
</p>
<p>
<a href="https://discord.gg/yF2MmDJyGJ"><img src="https://img.shields.io/badge/💬Discord-社区-7289da?style=for-the-badge&logo=discord&logoColor=white&labelColor=1a1a2e"></a>
<a href="https://github.com/HKUDS/LightRAG/issues/285"><img src="https://img.shields.io/badge/💬微信群-交流-07c160?style=for-the-badge&logo=wechat&logoColor=white&labelColor=1a1a2e"></a>
</p>
<p>
<a href="README-zh.md"><img src="https://img.shields.io/badge/🇨🇳中文版-1a1a2e?style=for-the-badge"></a>
<a href="README.md"><img src="https://img.shields.io/badge/🇺🇸English-1a1a2e?style=for-the-badge"></a>
<a href="README-ja.md"><img src="https://img.shields.io/badge/🇯🇵日本語版-1a1a2e?style=for-the-badge"></a>
</p>
<p>
<a href="https://pepy.tech/projects/lightrag-hku"><img src="https://static.pepy.tech/personalized-badge/lightrag-hku?period=total&units=INTERNATIONAL_SYSTEM&left_color=BLACK&right_color=GREEN&left_text=downloads"></a>
<a href="https://hvtracker.net/agents/lightrag/"><img src="https://hvtracker.net/badge/lightrag.svg"></a>
</p>
</div>
</div>
</div>
<div align="center" style="margin: 30px 0;">
<img src="https://user-images.githubusercontent.com/74038190/212284100-561aa473-3905-4a80-b561-0d28506553ee.gif" width="800">
</div>
<div align="center" style="margin: 30px 0;">
<img src="./README.assets/b2aaf634151b4706892693ffb43d9093.png" width="800" alt="LightRAG Diagram">
</div>
---
<div align="center">
<table>
<tr>
<td style="vertical-align: middle;">
<img src="./assets/LiteWrite.png"
width="56"
height="56"
alt="LiteWrite"
style="border-radius: 12px;" />
</td>
<td style="vertical-align: middle; padding-left: 12px;">
<a href="https://litewrite.ai">
<img src="https://img.shields.io/badge/🚀%20LiteWrite-AI%20原生%20LaTeX%20编辑器-ff6b6b?style=for-the-badge&logoColor=white&labelColor=1a1a2e">
</a>
</td>
</tr>
</table>
</div>
---
## 🎉 新闻
- [2026.05]🎯[新功能]**将 RagAnything 合并至 LightRAG**🎉。支持通过 **MinerU / Docling** 服务进行多模态内容解析与提取。
- [2026.05]🎯[新功能]:引入四种可选的文本分块策略:`Fix`(固定)、`Recursive`(递归)、`Vector`(向量)和 `Paragraph`(段落语义)。
- [2026.05]🎯[新功能]**支持按角色配置 LLM**,提供四个独立角色:EXTRACT、QUERY、KEYWORDS 和 VLM,每个角色拥有独立的 LLM 设置。
- [2026.03]🎯[新功能]: 集成了 **OpenSearch** 作为统一存储后端,为 LightRAG 的全部四种存储类型提供全面支持。
- [2026.03]🎯[新功能]: 推出交互式安装向导,支持通过 Docker 在本地部署 Embedding、Reranking 及存储后端服务。
- [2025.11]🎯[新功能]: 集成了 **RAGAS 评估**和 **Langfuse 追踪**。更新了 API 以在查询结果中返回召回上下文,支持上下文精度指标。
- [2025.10]🎯[可扩展性增强]: 消除了处理瓶颈,以高效支持**大规模数据集**。
- [2025.09]🎯[新功能]: 显著提升了 Qwen3-30B-A3B 等**开源 LLM** 的知识图谱提取准确性。
- [2025.08]🎯[新功能]: 现已支持 **Reranker**,显著提升混合查询性能(已设为默认查询模式)。
- [2025.08]🎯[新功能]: 添加了**文档删除**功能,并支持自动重新生成知识图谱,以确保最佳查询性能。
- [2025.06]🎯[新发布]: 我们的团队发布了 [RAG-Anything](https://github.com/HKUDS/RAG-Anything) —— 一个用于无缝处理文本、图像、表格和方程式的**全功能多模态 RAG** 系统。
- [2025.06]🎯[新功能]: LightRAG 现已集成 [RAG-Anything](https://github.com/HKUDS/RAG-Anything),支持全面的多模态数据处理,实现对 PDF、图像、Office 文档、表格和公式等多种格式的无缝文档解析和 RAG 能力。详见[多模态文档处理部分](https://github.com/HKUDS/LightRAG/?tab=readme-ov-file#multimodal-document-processing-rag-anything-integration)。
- [2025.03]🎯[新功能]: LightRAG 现已支持引用功能,实现了准确的源归因和增强的文档可追溯性。
- [2025.02]🎯[新功能]: 现在您可以使用 MongoDB 作为一体化存储解决方案,实现统一的数据管理。
- [2025.02]🎯[新发布]: 我们的团队发布了 [VideoRAG](https://github.com/HKUDS/VideoRAG) —— 一个用于理解超长上下文视频的 RAG 系统。
- [2025.01]🎯[新发布]: 我们的团队发布了 [MiniRAG](https://github.com/HKUDS/MiniRAG),使用小型模型简化 RAG。
- [2025.01]🎯现在您可以使用 PostgreSQL 作为一体化存储解决方案进行数据管理。
- [2024.11]🎯[新资源]: LightRAG 的综合指南现已在 [LearnOpenCV](https://learnopencv.com/lightrag) 上发布 —— 探索深入的教程和最佳实践。非常感谢博客作者的杰出贡献!
- [2024.11]🎯[新功能]: 推出 LightRAG WebUI —— 一个允许您通过直观的 Web 界面插入、查询和可视化 LightRAG 知识的仪表板。
- [2024.11]🎯[新功能]: 现在您可以[使用 Neo4J 进行存储](https://github.com/HKUDS/LightRAG?tab=readme-ov-file#using-neo4j-for-storage) —— 开启图数据库支持。
- [2024.10]🎯[新功能]: 我们添加了 [LightRAG 介绍视频](https://youtu.be/oageL-1I0GE) 的链接 —— 演示 LightRAG 的各项功能。感谢作者的杰出贡献!
- [2024.10]🎯[新频道]: 我们创建了一个 [Discord 频道](https://discord.gg/yF2MmDJyGJ)!💬 欢迎加入我们的社区进行分享、讨论和协作! 🎉🎉
<details>
<summary style="font-size: 1.4em; font-weight: bold; cursor: pointer; display: list-item;">
算法流程图
</summary>
![LightRAG索引流程图](https://learnopencv.com/wp-content/uploads/2024/11/LightRAG-VectorDB-Json-KV-Store-Indexing-Flowchart-scaled.jpg)
*图1:LightRAG索引流程图 - 图片来源:[Source](https://learnopencv.com/lightrag/)*
![LightRAG检索和查询流程图](https://learnopencv.com/wp-content/uploads/2024/11/LightRAG-Querying-Flowchart-Dual-Level-Retrieval-Generation-Knowledge-Graphs-scaled.jpg)
*图2:LightRAG检索和查询流程图 - 图片来源:[Source](https://learnopencv.com/lightrag/)*
</details>
## 安装
**💡 使用 uv 进行包管理**: 本项目使用 [uv](https://docs.astral.sh/uv/) 进行快速可靠的 Python 包管理。首先安装 uv: `curl -LsSf https://astral.sh/uv/install.sh | sh` (Unix/macOS) 或 `powershell -c "irm https://astral.sh/uv/install.ps1 | iex"` (Windows)
> **注意**:如果您愿意,也可以使用 pip,但为了获得更好的性能 and 更可靠的依赖管理,建议使用 uv。
>
> **📦 离线部署**: 对于离线或隔离环境,请参阅[离线部署指南](./docs/OfflineDeployment.md),了解预安装所有依赖项和缓存文件的说明。
### 安装LightRAG服务器
* 从PyPI安装
```bash
### 使用 uv 安装 LightRAG 服务器(作为工具,推荐)
uv tool install "lightrag-hku[api]"
### 或使用 pip
# python -m venv .venv
# source .venv/bin/activate # Windows: .venv\Scripts\activate
# pip install "lightrag-hku[api]"
### 构建前端代码
cd lightrag_webui
bun install --frozen-lockfile
bun run build
cd ..
# 配置 env 文件
# 从 GitHub 仓库的根目录上下载 env.example 文件
# 或从本地检出的源代码中获取 env.example 文件
cp env.example .env # 使用你的LLM和Embedding模型访问参数更新.env文件
# 启动 API-WebUI 服务。默认绑定所有网络接口(0.0.0.0)。
# 安全提示:对外网暴露前,请在 .env 中配置认证(LIGHTRAG_API_KEY,或
# AUTH_ACCOUNTS 搭配 TOKEN_SECRET);若仅需本机访问,可绑定 127.0.0.1;
# 否则所有接口都将公开可访问。
# 注意:为兼容 Ollama 客户端,/api/* 路由默认不鉴权;如需对其启用认证,
# 请将 WHITELIST_PATHS 收窄为 /health。
lightrag-server
```
* 从源代码安装
```bash
git clone https://github.com/HKUDS/LightRAG.git
cd LightRAG
# 一键初始化开发环境(推荐)
make dev
source .venv/bin/activate # 激活虚拟环境 (Linux/macOS)
# Windows 系统: .venv\Scripts\activate
# make dev 会安装测试工具链以及完整的离线依赖栈
# API、存储后端与各类 Provider 集成),并构建前端;不会生成 .env。
# 启动服务前请先运行 make env-base,或手动从 env.example 复制并配置 .env。
# 使用 uv 的等价手动步骤
# 注意: uv sync 会自动在 .venv/ 目录创建虚拟环境
uv sync --extra test --extra offline
source .venv/bin/activate # 激活虚拟环境 (Linux/macOS)
# Windows 系统: .venv\Scripts\activate
### 或使用 pip 和虚拟环境
# python -m venv .venv
# source .venv/bin/activate # Windows: .venv\Scripts\activate
# pip install -e ".[test,offline]"
# 构建前端代码
cd lightrag_webui
bun install --frozen-lockfile
bun run build
cd ..
# 配置 env 文件
make env-base # 或: cp env.example .env 后手动修改
# 启动API-WebUI服务
lightrag-server
```
* 使用 Docker Compose 启动 LightRAG 服务器
```bash
git clone https://github.com/HKUDS/LightRAG.git
cd LightRAG
cp env.example .env # 使用你的LLM和Embedding模型访问参数更新.env文件
# modify LLM and Embedding settings in .env
docker compose up
```
> 在此获取LightRAG docker镜像历史版本: [LightRAG Docker Images]( https://github.com/HKUDS/LightRAG/pkgs/container/lightrag)
>
> 由 GitHub Actions 发布到 GHCR 的官方镜像已使用 GitHub OIDC 和 Sigstore Cosign 进行签名。校验方式请参阅 [docs/DockerDeployment.md](./docs/DockerDeployment.md#verify-official-ghcr-images-with-cosign)。
### 使用设置向导创建 .env 文件
除了手动编辑 `env.example` 之外,您还可以使用交互式向导生成配置好的 `.env`,并在需要时生成 `docker-compose.final.yml`
```bash
make env-base # 必跑第一步:配置 LLM、Embedding、Reranker
make env-storage # 可选:配置存储后端和数据库服务
make env-server # 可选:配置服务端口、鉴权和 SSL
make env-base-rewrite # 可选:强制重建向导托管的 compose 服务块
make env-storage-rewrite # 可选:强制重建向导托管的 compose 服务块
make env-security-check # 可选:审计当前 .env 中的安全风险
```
设置向导工具的详细说明请参阅 [docs/InteractiveSetup.md](./docs/InteractiveSetup.md)。
## 关于LightRAG
### 基于图的轻量级RAG框架
**LightRAG** 是一个轻量级的知识图谱 RAG 框架,被视为 Microsoft GraphRAG 的高效替代方案。它采用双层架构来同时管理知识图谱(KG)和向量嵌入,完美填补了传统基于向量的 RAG 与基于图谱的 RAG 之间的技术鸿沟。LightRAG专为高扩展性而设计,有效地解决了大规模图谱索引和查询时计算开销大、响应缓慢以及增量更新成本高等问题;LightRAG在支持大规模数据集的同时,即使搭载 30B开源大语言模型(LLM),也能保持极高的RAG质量。
### 特点与优势
1. **深度上下文理解**:通过图结构索引,LightRAG 能够捕捉实体间复杂的语义依赖关系,克服了传统分块检索方法上下文割裂的缺陷。在需要全局理解或逻辑推理的垂直领域(如法律、金融),其生成质量与上下文感知能力尤为突出。
2. **卓越的全面性与多样性**:LightRAG的双层检索机制使其能够同时整合详细事实与抽象概念,让其在查询结果全面性(Comprehensiveness)和多样性(Diversity)取得卓越的成绩,有效应对复杂的跨文档查询。
3. **极高的检索效率与低成本**:LightRAG不需要依赖低效的社区报告和复杂查询时的多跳推理,大幅度减少了索引和查询阶段对LLM的调用,显著减少了响应延迟与LLM计算成本。
4. **快速适应动态数据**:LightRAG 支持无缝的增量知识库更新。新数据只需经过标准的图索引流程生成局部图谱,即可通过集合合并的方式直接融入现有图谱,无需破坏原有结构或重建全局索引,保证了系统在动态数据环境下的时效性。删除文档时可以利用构建阶段的LLM缓存快速重建受影响的实体关系,大幅度提高了知识库更新效率。
### 多模态能力的升级
从 LightRAG v1.5 版本开始,该框架正式引入了对多模态文档的分析和检索能力:
* **多引擎文档解析:** 其文件处理流水线(Pipeline)支持使用 MinerU、Docling 和 Native 文档解析引擎,可高效提取文档中的文字、表格、公式和图片。
* **跨模态实体与关系映射:** 在统一的框架内实现跨模态的实体提取和关系映射,从而达成无缝的索引与查询。
* **应用场景提升:** 全新的多模态处理流水线能够大幅提高操作说明书、学术论文等含有丰富多模态内容文档的 RAG 质量。
### LightRAG API 服务器
LightRAG 服务器不仅提供给了一个供出选择体验LightRAG功能的Web UI,还提供了一个完整的 `REST API`。有关LightRAG服务器的更多信息,请参阅[LightRAG服务器](./docs/LightRAG-API-Server-zh.md)。
![iShot_2025-03-23_12.40.08](./README.assets/iShot_2025-03-23_12.40.08.png)
## 关键配置说明
### LLM 模型的选择
LightRAG 的工作过程中需要使用到 4 种角色的 LLM/VLM。应该为不同角色的 LLM 配置不同能力和速度的模型,以获得速度和能力之间的平衡。LightRAG 对大型语言模型(LLM)的能力要求会高于传统 RAG,因为它需要 LLM 执行文档中的实体关系抽取任务。在查询阶段,LLM 模型需要处理 LightRAG 召回的实体、关系和文本块等大量信息,需要模型具备在含有噪声的长上下文中作出高质量回答的能力。详细的模型配置请参见 [RoleSpecificLLMConfiguration-zh.md](./docs/RoleSpecificLLMConfiguration-zh.md)
### 查询模式的选择
LightRAG 支持 4 种查询模式:
- **local**:聚焦于局部上下文与具体实体的精准匹配。在知识图谱中检索对应的候选实体及其直接关联属性,适用于针对特定对象、具体概念或细节事实的问答,能够提供高度相关且细致的局部上下文支持。
- **global**:侧重于宏观主题、跨文档推理与实体间的深层关系。检索覆盖广泛主题与概念的关系链,适用于需要跨多个上下文进行总结、趋势分析或理解复杂语义依赖关系的查询。
- **hybrid**:融合 local 和 global 两种模式的检索结果。通过同时召回具体实体与全局关系上下文,进行综合推理与生成。
- **naive**:基于文本块的传统 RAG 检索,不使用知识图谱,直接依赖向量相似性在原始文本块中进行检索。
- **mix**:全功能模式,融合 local、global 和 naive 三种模式的检索结果,提供最为丰富和全面的检索结果。
LightRAG 的默认查询模式为 mix。使用 mix 模式通常可以获得最为理想的查询结果。mix 模式比 naive 耗时略长;其他查询模式在耗时上基本相当。
### Embedding 模型
在选择 Embedding 模型的时候需要注意其对多语言的支持能力。LightRAG 的检索质量对 Embedding 模型的依赖有限,因此建议尽量选择低维度和速度快的模型。通常 `BAAI/bge-m3` 已经足够使用。建议尽量本地部署 Embedding 模型,以获得最好的性能。
**重要提示**:在文档索引前必须确定使用的 Embedding 模型,且在文档查询阶段必须沿用与索引阶段相同的模型。嵌入模型一旦选定通常就不能修改。如果修改的话,需要对所有文本块、实体和关系进行重新嵌入。LightRAG 目前没有提供重新嵌入的工具。有些存储(例如 PostgreSQL)在首次建立数据表的时候需要确定向量维度,因此更换 Embedding 模型后需要删除向量相关库表,以便让 LightRAG 重建新的库表。
### 开启 Rerank 选项
查询阶段开启 Rerank 选项可以显著提高查询的质量。开启 Rerank 通常会引入 1~2 秒的延时。为了降低延时,建议尽量在本地部署 Rerank 模型。Rerank 的相关配置方式请参考 `.env.example` 文件。Rerank 模型与 Embedding 模型不同,可以在查询阶段随时更换。
### 文档处理流水线的配置
LightRAG 的默认流水线配置并不能让系统发挥最好的性能。文件内容解析的好坏会极大地影响文档的索引和查询效果。因此建议配置流水线开启 MinerU 文件解析引擎,并开启流水线的图片分析功能。建议添加的配置为:
```
LIGHTRAG_PARSER=*:native-iteP,*:mineru-iteP,*:legacy-R
VLM_PROCESS_ENABLE=true
VLM_LLM_MODEL=<your_vlm_model_name>
```
由于云端的 MinerU 服务有使用量、文件大小和页数等限制,建议使用本地部署的 MinerU。文件处理流水线的具体配置方法请参考 [FileProcessingPipeline-zh.md](./docs/FileProcessingPipeline-zh.md)
### 文件处理并发优化
对于大规模的文档处理,需要提高文档处理的并发能力。几个涉及文件并发处理性能的关键环境变量包括:
- **MAX_ASYNC_LLM/EXTRACT_ASYNC_LLM**:控制 LLM 模型的最大并发数。
- **MAX_PARALLEL_INSERT**:控制并行处理文件的最大数量。单个文件内的文本、表格、公式、图片之间的处理也会并发进行。`MAX_PARALLEL_INSERT` 应该为 `MAX_ASYNC_LLM` 的 1/3 左右为宜。
- **MAX_PARALLEL_PARSE_MINERU**:控制 MinerU 文件解析的并发处理文件数。
- **MAX_PARALLEL_PARSE_DOCLING**:控制 Docling 文件解析的并发处理文件数。
- **EMBEDDING_FUNC_MAX_ASYNC**:控制嵌入模型的最大并发数。
- **EMBEDDING_BATCH_NUM**:控制每个嵌入模型请求包含的待嵌入文本的数量(每批做多少个嵌入);提高这个数量可以大幅度减少调用嵌入模型的次数,提高嵌入存储的落盘速度。
```
# 设置示例
MAX_ASYNC_LLM=8
MAX_PARALLEL_INSERT=3
EMBEDDING_FUNC_MAX_ASYNC=16
EMBEDDING_BATCH_NUM=32
```
### 后台存储的选择
LightRAG 需要使用到 4 种后台存储类型,分别是:
- **KV_STORAGE**:用于保存 LLM 响应缓存、文本分块结果、实体关系提取结果等信息。
- **VECTOR_STORAGE**:用于保存文本块、实体和关系的向量信息。
- **GRAPH_STORAGE**:用于保存知识图谱。
- **DOC_STATUS_STORAGE**:用于保存文件列表。
LightRAG 的默认存储全部都是基于文件进行持久化的内存数据库。默认存储仅用于开发调试,不适合用于生产环境部署。生产环境如果希望使用同一个后台数据解决 4 种类型的后台存储,可以选择 PostgreSQL、MongoDB 或 OpenSearch。也可以单独为向量存储或图存储选择专业化的数据库,例如使用 Milvus 或 Qdrant 作为向量存储,使用 Neo4j 或 Memgraph 作为图存储。
### 文档处理阶段其他重要配置
在文档插入阶段还有以下环境变量建议根据实际需要进行调整:
- **SUMMARY_LANGUAGE**:控制 LLM 输出实体关系名称和摘要时使用的语言,例如:`Chinese`, `English`
- **ENTITY_EXTRACTION_USE_JSON**:控制 LLM 输出实体关系的时候是否使用 JSON 格式。使用 JSON 格式通常可以获得更加稳定的效果,但是输出需要消耗更多的 Token,速度也会略微慢一些。
- **ENABLE_CONTENT_HEADINGS**:控制查询阶段是否把文本块所属章节标题信息送给LLM(默认允许,为LLM提供更多的上下文信息)
- **FORCE_LLM_SUMMARY_ON_MERGE / MAX_SOURCE_IDS_PER_RELATION**:控制每个`实体/关系`能够最多与多少个文本块保持关联
- **SOURCE_IDS_LIMIT_METHOD**:控制`实体/关系`关联文本块超过限制后是否继续更新实体关系的描述(默认不再更新,因为此时实体关系的描述已经足够丰富,继续更新的意义不大;放弃更新可以极大地提高知识库的构建速度)
- **DEFAULT_MAX_FILE_PATHS**:控制`实体/关系`关联的原始文件的最大数量,超过这个数量之后新的文件名不再写入到向量存储。
### 解决实体关系抽取阶段的 LLM 超时
实体关系抽取阶段的 LLM 超时通常源于以下三种原因之一。先判断原因,再采用对应的解决方案(参数可以组合使用):
- **模型太慢。** 速度低于约 50 tokens/秒的模型,可能无法在请求超时前完成包含大量实体关系的文本块的抽取。可以通过 `*_LLM_TIMEOUT` 增大超时时间——既可以是全局的 `LLM_TIMEOUT`,也可以是抽取阶段专用的角色参数 `EXTRACT_LLM_TIMEOUT`。注意实际的执行超时是所配置值的**两倍**,因此 `EXTRACT_LLM_TIMEOUT=300` 对应最长 **600 秒**
- **文本块产生的实体关系太多。** 例如参考文献文本块会让模型输出极其大量的记录,从而无法在限定时间内完成。可以通过 `OPENAI_LLM_MAX_TOKENS``OPENAI_LLM_MAX_COMPLETION_TOKENS` 限制输出长度(具体参数名取决于 LLM 供应商,详见 `env.example`)。一个实用的估算规则是 `max_output_tokens < LLM_TIMEOUT × 每秒token数`(例如 `9000 < 240s × 50 tps`)。
- **模型存在缺陷,陷入输出死循环。** 某些模型(尤其是本地部署的 Qwen 模型)在遇到特殊文本时偶尔会陷入无尽的输出死循环。如果是偶发情况,通常只需将该文档重新处理一次即可解决。
- **专门针对参考文献(P 分块策略)。** 使用段落语义(`P`)分块策略(例如 `LIGHTRAG_PARSER=...-iteP`)时,设置 `CHUNK_P_DROP_REFERENCES=true` 可在分块前自动删除末尾的参考文献部分,从而避免参考文献产生大量低价值的实体关系(这是导致超时的常见原因)。也可以通过文件名提示 `paper.[-P(drop_rf=true)].pdf` 对单个文件启用;相关的检测参数(`CHUNK_P_REFERENCES_TAIL_N``CHUNK_P_REFERENCES_HEADINGS`)详见 `env.example`
### 文档查询阶段其他重要配置
在文档查询阶段还有以下环境变量建议根据实际需要进行调整:
- **MAX_ENTITY_TOKENS / MAX_RELATION_TOKENS / MAX_TOTAL_TOKENS**:控制召回内容送给LLM上下文的Token长度。召回内容包含`实体``关系``文本块`三部分,实体和关系的长度可以单独控制长度,文本块的长度由总长度减去实体和关系的长度来控制。
- **ENABLE_CONTENT_HEADINGS**:控制是否把文本块所在的章节标题送给LLM;默认开启,可以为LLM提供更加丰富的上下文信息,提高回答质量。
- **ENABLE_LLM_CACHE**:是否允许缓存查询结果。默认开启,相同的查询问题、查询模式、LLM模型参数将返回相同的结果。
## 使用LightRAG SDK
> ⚠️ **如果您希望将LightRAG集成到您的项目中,建议您使用LightRAG Server提供的REST API**。LightRAG SDK通常用于嵌入式应用,或供希望进行研究与评估的学者使用。
### 安装LightRAG SDK
* 从源代码安装
```bash
cd LightRAG
# 注意: uv sync 会自动在 .venv/ 目录创建虚拟环境
uv sync
source .venv/bin/activate # 激活虚拟环境 (Linux/macOS)
# Windows 系统: .venv\Scripts\activate
# 或: pip install -e .
```
* 从PyPI安装
```bash
uv pip install lightrag-hku
# 或: pip install lightrag-hku
```
### LightRAG SDK示例代码
LightRAG核心功能的示例代码请参见`examples`目录。您还可参照[视频](https://www.youtube.com/watch?v=g21royNJ4fw)视频完成环境配置。若已持有OpenAI API密钥,可以通过以下命令运行演示代码:
```bash
### you should run the demo code with project folder
cd LightRAG
### provide your API-KEY for OpenAI
export OPENAI_API_KEY="sk-...your_opeai_key..."
### download the demo document of "A Christmas Carol" by Charles Dickens
curl https://raw.githubusercontent.com/gusye1234/nano-graphrag/main/tests/mock_data.txt > ./book.txt
### run the demo code
python examples/lightrag_openai_demo.py
```
如需流式响应示例的实现代码,请参阅 `examples/lightrag_openai_compatible_demo.py`。运行前,请确保根据需求修改示例代码中的LLM及嵌入模型配置。
**注意1**:在运行demo程序的时候需要注意,不同的测试程序可能使用的是不同的embedding模型,更换不同的embeding模型的时候需要把清空数据目录(`./dickens`),否则层序执行会出错。如果你想保留LLM缓存,可以在清除数据目录时保留`kv_store_llm_response_cache.json`文件。
**注意2**:官方支持的示例代码仅为 `lightrag_openai_demo.py``lightrag_openai_compatible_demo.py` 两个文件。其他示例文件均为社区贡献内容,尚未经过完整测试与优化。
### 使用SDK的注意事项
SDK的使用说明详见 **[docs/ProgramingWithCore.md](./docs/ProgramingWithCore.md)**(英文)。有部份LightRAG功能没有提供 REST API,仅能够通过SDK使用。这部份功能往往是不稳定,不能保证在将来的版本上可以兼容。
## 重现论文结果
LightRAG 在农业、计算机科学、法律和混合等领域均显著优于 NaiveRAG、RQ-RAG、HyDE 和 GraphRAG。完整评估方法论、提示词和复现步骤详见 **[docs/Reproduce.md](./docs/Reproduce.md)**(英文)。
### 总体性能表
||**农业**||**计算机科学**||**法律**||**混合**||
|----------------------|---------------|------------|------|------------|---------|------------|-------|------------|
||NaiveRAG|**LightRAG**|NaiveRAG|**LightRAG**|NaiveRAG|**LightRAG**|NaiveRAG|**LightRAG**|
|**全面性**|32.4%|**67.6%**|38.4%|**61.6%**|16.4%|**83.6%**|38.8%|**61.2%**|
|**多样性**|23.6%|**76.4%**|38.0%|**62.0%**|13.6%|**86.4%**|32.4%|**67.6%**|
|**赋能性**|32.4%|**67.6%**|38.8%|**61.2%**|16.4%|**83.6%**|42.8%|**57.2%**|
|**总体**|32.4%|**67.6%**|38.8%|**61.2%**|15.2%|**84.8%**|40.0%|**60.0%**|
||RQ-RAG|**LightRAG**|RQ-RAG|**LightRAG**|RQ-RAG|**LightRAG**|RQ-RAG|**LightRAG**|
|**全面性**|31.6%|**68.4%**|38.8%|**61.2%**|15.2%|**84.8%**|39.2%|**60.8%**|
|**多样性**|29.2%|**70.8%**|39.2%|**60.8%**|11.6%|**88.4%**|30.8%|**69.2%**|
|**赋能性**|31.6%|**68.4%**|36.4%|**63.6%**|15.2%|**84.8%**|42.4%|**57.6%**|
|**总体**|32.4%|**67.6%**|38.0%|**62.0%**|14.4%|**85.6%**|40.0%|**60.0%**|
||HyDE|**LightRAG**|HyDE|**LightRAG**|HyDE|**LightRAG**|HyDE|**LightRAG**|
|**全面性**|26.0%|**74.0%**|41.6%|**58.4%**|26.8%|**73.2%**|40.4%|**59.6%**|
|**多样性**|24.0%|**76.0%**|38.8%|**61.2%**|20.0%|**80.0%**|32.4%|**67.6%**|
|**赋能性**|25.2%|**74.8%**|40.8%|**59.2%**|26.0%|**74.0%**|46.0%|**54.0%**|
|**总体**|24.8%|**75.2%**|41.6%|**58.4%**|26.4%|**73.6%**|42.4%|**57.6%**|
||GraphRAG|**LightRAG**|GraphRAG|**LightRAG**|GraphRAG|**LightRAG**|GraphRAG|**LightRAG**|
|**全面性**|45.6%|**54.4%**|48.4%|**51.6%**|48.4%|**51.6%**|**50.4%**|49.6%|
|**多样性**|22.8%|**77.2%**|40.8%|**59.2%**|26.4%|**73.6%**|36.0%|**64.0%**|
|**赋能性**|41.2%|**58.8%**|45.2%|**54.8%**|43.6%|**56.4%**|**50.8%**|49.2%|
|**总体**|45.2%|**54.8%**|48.0%|**52.0%**|47.2%|**52.8%**|**50.4%**|49.6%|
## 🔗 相关项目
*生态与扩展*
<div align="center">
<table>
<tr>
<td align="center">
<a href="https://github.com/HKUDS/RAG-Anything">
<div style="width: 100px; height: 100px; background: linear-gradient(135deg, rgba(0, 217, 255, 0.1) 0%, rgba(0, 217, 255, 0.05) 100%); border-radius: 15px; border: 1px solid rgba(0, 217, 255, 0.2); display: flex; align-items: center; justify-content: center; margin-bottom: 10px;">
<span style="font-size: 32px;">📸</span>
</div>
<b>RAG-Anything</b><br>
<sub>多模态 RAG</sub>
</a>
</td>
<td align="center">
<a href="https://github.com/HKUDS/VideoRAG">
<div style="width: 100px; height: 100px; background: linear-gradient(135deg, rgba(0, 217, 255, 0.1) 0%, rgba(0, 217, 255, 0.05) 100%); border-radius: 15px; border: 1px solid rgba(0, 217, 255, 0.2); display: flex; align-items: center; justify-content: center; margin-bottom: 10px;">
<span style="font-size: 32px;">🎥</span>
</div>
<b>VideoRAG</b><br>
<sub>极端长上下文视频 RAG</sub>
</a>
</td>
<td align="center">
<a href="https://github.com/HKUDS/MiniRAG">
<div style="width: 100px; height: 100px; background: linear-gradient(135deg, rgba(0, 217, 255, 0.1) 0%, rgba(0, 217, 255, 0.05) 100%); border-radius: 15px; border: 1px solid rgba(0, 217, 255, 0.2); display: flex; align-items: center; justify-content: center; margin-bottom: 10px;">
<span style="font-size: 32px;">✨</span>
</div>
<b>MiniRAG</b><br>
<sub>极简 RAG</sub>
</a>
</td>
</tr>
</table>
</div>
---
## ⭐ Star 历史
[![Star History Chart](https://api.star-history.com/svg?repos=HKUDS/LightRAG&type=Date)](https://star-history.com/#HKUDS/LightRAG&Date)
## 🤝 贡献
<div align="center">
我们欢迎各种形式的贡献——Bug 修复、新功能、文档改进等。<br>
提交 Pull Request 前,请阅读 <a href=".github/CONTRIBUTING.md"><strong>贡献指南</strong></a>。
</div>
<br>
<div align="center">
我们感谢所有贡献者做出的宝贵贡献。
</div>
<div align="center">
<a href="https://github.com/HKUDS/LightRAG/graphs/contributors">
<img src="https://contrib.rocks/image?repo=HKUDS/LightRAG" style="border-radius: 15px; box-shadow: 0 0 20px rgba(0, 217, 255, 0.3);" />
</a>
</div>
## 📖 引用
```python
@article{guo2024lightrag,
title={LightRAG: Simple and Fast Retrieval-Augmented Generation},
author={Zirui Guo and Lianghao Xia and Yanhua Yu and Tu Ao and Chao Huang},
year={2024},
eprint={2410.05779},
archivePrefix={arXiv},
primaryClass={cs.IR}
}
```
---
<div align="center" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 15px; padding: 30px; margin: 30px 0;">
<div>
<img src="https://user-images.githubusercontent.com/74038190/212284100-561aa473-3905-4a80-b561-0d28506553ee.gif" width="500">
</div>
<div style="margin-top: 20px;">
<a href="https://github.com/HKUDS/LightRAG" style="text-decoration: none;">
<img src="https://img.shields.io/badge/⭐%20在%20GitHub%20上点亮星星-1a1a2e?style=for-the-badge&logo=github&logoColor=white">
</a>
<a href="https://github.com/HKUDS/LightRAG/issues" style="text-decoration: none;">
<img src="https://img.shields.io/badge/🐛%20报告问题-ff6b6b?style=for-the-badge&logo=github&logoColor=white">
</a>
<a href="https://github.com/HKUDS/LightRAG/discussions" style="text-decoration: none;">
<img src="https://img.shields.io/badge/💬%20讨论-4ecdc4?style=for-the-badge&logo=github&logoColor=white">
</a>
</div>
</div>
<div align="center">
<div style="width: 100%; max-width: 600px; margin: 20px auto; padding: 20px; background: linear-gradient(135deg, rgba(0, 217, 255, 0.1) 0%, rgba(0, 217, 255, 0.05) 100%); border-radius: 15px; border: 1px solid rgba(0, 217, 255, 0.2);">
<div style="display: flex; justify-content: center; align-items: center; gap: 15px;">
<span style="font-size: 24px;">⭐</span>
<span style="color: #00d9ff; font-size: 18px;">感谢您访问 LightRAG!</span>
<span style="font-size: 24px;">⭐</span>
</div>
</div>
</div>
Binary file not shown.

After

Width:  |  Height:  |  Size: 471 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 401 KiB

+530
View File
@@ -0,0 +1,530 @@
<div align="center">
<div style="margin: 20px 0;">
<img src="./assets/logo.png" width="120" height="120" alt="LightRAG Logo" style="border-radius: 20px; box-shadow: 0 8px 32px rgba(0, 217, 255, 0.3);">
</div>
# 🚀 LightRAG: Simple and Fast Retrieval-Augmented Generation
<div align="center">
<a href="https://trendshift.io/repositories/13043" target="_blank"><img src="https://trendshift.io/api/badge/repositories/13043" alt="HKUDS%2FLightRAG | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
</div>
<p>
</p>
<div align="center">
<div style="width: 100%; height: 2px; margin: 20px 0; background: linear-gradient(90deg, transparent, #00d9ff, transparent);"></div>
</div>
<div align="center">
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 15px; padding: 25px; text-align: center;">
<p>
<a href='https://github.com/HKUDS/LightRAG'><img src='https://img.shields.io/badge/🔥Project-Page-00d9ff?style=for-the-badge&logo=github&logoColor=white&labelColor=1a1a2e'></a>
<a href='https://arxiv.org/abs/2410.05779'><img src='https://img.shields.io/badge/📄arXiv-2410.05779-ff6b6b?style=for-the-badge&logo=arxiv&logoColor=white&labelColor=1a1a2e'></a>
<a href="https://github.com/HKUDS/LightRAG/stargazers"><img src='https://img.shields.io/github/stars/HKUDS/LightRAG?color=00d9ff&style=for-the-badge&logo=star&logoColor=white&labelColor=1a1a2e' /></a>
</p>
<p>
<img src="https://img.shields.io/badge/🐍Python-3.10-4ecdc4?style=for-the-badge&logo=python&logoColor=white&labelColor=1a1a2e">
<a href="https://pypi.org/project/lightrag-hku/"><img src="https://img.shields.io/pypi/v/lightrag-hku.svg?style=for-the-badge&logo=pypi&logoColor=white&labelColor=1a1a2e&color=ff6b6b"></a>
</p>
<p>
<a href="https://discord.gg/yF2MmDJyGJ"><img src="https://img.shields.io/badge/💬Discord-Community-7289da?style=for-the-badge&logo=discord&logoColor=white&labelColor=1a1a2e"></a>
<a href="https://github.com/HKUDS/LightRAG/issues/285"><img src="https://img.shields.io/badge/💬WeChat-Group-07c160?style=for-the-badge&logo=wechat&logoColor=white&labelColor=1a1a2e"></a>
</p>
<p>
<a href="README-zh.md"><img src="https://img.shields.io/badge/🇨🇳中文版-1a1a2e?style=for-the-badge"></a>
<a href="README.md"><img src="https://img.shields.io/badge/🇺🇸English-1a1a2e?style=for-the-badge"></a>
<a href="README-ja.md"><img src="https://img.shields.io/badge/🇯🇵日本語版-1a1a2e?style=for-the-badge"></a>
</p>
<p>
<a href="https://pepy.tech/projects/lightrag-hku"><img src="https://static.pepy.tech/personalized-badge/lightrag-hku?period=total&units=INTERNATIONAL_SYSTEM&left_color=BLACK&right_color=GREEN&left_text=downloads"></a>
<a href="https://hvtracker.net/agents/lightrag/"><img src="https://hvtracker.net/badge/lightrag.svg"></a>
</p>
</div>
</div>
</div>
<div align="center" style="margin: 30px 0;">
<img src="https://user-images.githubusercontent.com/74038190/212284100-561aa473-3905-4a80-b561-0d28506553ee.gif" width="800">
</div>
<div align="center" style="margin: 30px 0;">
<img src="./README.assets/b2aaf634151b4706892693ffb43d9093.png" width="800" alt="LightRAG Diagram">
</div>
---
<div align="center">
<table>
<tr>
<td style="vertical-align: middle;">
<img src="./assets/LiteWrite.png"
width="56"
height="56"
alt="LiteWrite"
style="border-radius: 12px;" />
</td>
<td style="vertical-align: middle; padding-left: 12px;">
<a href="https://litewrite.ai">
<img src="https://img.shields.io/badge/🚀%20LiteWrite-AI%20Native%20LaTeX%20Editor-ff6b6b?style=for-the-badge&logoColor=white&labelColor=1a1a2e">
</a>
</td>
</tr>
</table>
</div>
---
## 🎉 News
- [2026.05]🎯[New Feature]: **Merge RagAnything into LightRAG**🎉. Multimodal content parsing and extraction via **MinerU / Docling** services.
- [2026.05]🎯[New Feature]: Introducing four selectable text chunking strategies: `Fix`, `Recursive`, `Vector`, and `Paragraph`.
- [2026.05]🎯[New Feature]: **Role-specific LLM configuration** support, 4 distinct roles: EXTRACT, QUERY, KEYWORDS, and VLM, with independent LLM settings.
- [2026.03]🎯[New Feature]: Integrated **OpenSearch** as a unified storage backend, providing comprehensive support for all four LightRAG storage.
- [2026.03]🎯[New Feature]: Introduced a setup wizard. Support for local deployment of embedding, reranking, and storage backends via Docker.
- [2025.11]🎯[New Feature]: Integrated **RAGAS for Evaluation** and **Langfuse for Tracing**. Updated the API to return retrieved contexts alongside query results to support context precision metrics.
- [2025.10]🎯[Scalability Enhancement]: Eliminated processing bottlenecks to support **Large-Scale Datasets Efficiently**.
- [2025.09]🎯[New Feature] Enhances knowledge graph extraction accuracy for **Open-Sourced LLMs** such as Qwen3-30B-A3B.
- [2025.08]🎯[New Feature] **Reranker** is now supported, significantly boosting performance for mixed queries (set as default query mode).
- [2025.08]🎯[New Feature] Added **Document Deletion** with automatic KG regeneration to ensure optimal query performance.
- [2025.06]🎯[New Release] Our team has released [RAG-Anything](https://github.com/HKUDS/RAG-Anything) — an **All-in-One Multimodal RAG** system for seamless processing of text, images, tables, and equations.
- [2025.06]🎯[New Feature] LightRAG now supports comprehensive multimodal data handling through [RAG-Anything](https://github.com/HKUDS/RAG-Anything) integration, enabling seamless document parsing and RAG capabilities across diverse formats including PDFs, images, Office documents, tables, and formulas. Please refer to the new [multimodal section](https://github.com/HKUDS/LightRAG/?tab=readme-ov-file#multimodal-document-processing-rag-anything-integration) for details.
- [2025.03]🎯[New Feature] LightRAG now supports citation functionality, enabling proper source attribution and enhanced document traceability.
- [2025.02]🎯[New Feature] You can now use MongoDB as an all-in-one storage solution for unified data management.
- [2025.02]🎯[New Release] Our team has released [VideoRAG](https://github.com/HKUDS/VideoRAG)-a RAG system for understanding extremely long-context videos
- [2025.01]🎯[New Release] Our team has released [MiniRAG](https://github.com/HKUDS/MiniRAG) making RAG simpler with small models.
- [2025.01]🎯You can now use PostgreSQL as an all-in-one storage solution for data management.
- [2024.11]🎯[New Resource] A comprehensive guide to LightRAG is now available on [LearnOpenCV](https://learnopencv.com/lightrag). — explore in-depth tutorials and best practices. Many thanks to the blog author for this excellent contribution!
- [2024.11]🎯[New Feature] Introducing the LightRAG WebUI — an interface that allows you to insert, query, and visualize LightRAG knowledge through an intuitive web-based dashboard.
- [2024.11]🎯[New Feature] You can now [use Neo4J for Storage](https://github.com/HKUDS/LightRAG?tab=readme-ov-file#using-neo4j-for-storage)-enabling graph database support.
- [2024.10]🎯[New Feature] We've added a link to a [LightRAG Introduction Video](https://youtu.be/oageL-1I0GE). — a walkthrough of LightRAG's capabilities. Thanks to the author for this excellent contribution!
- [2024.10]🎯[New Channel] We have created a [Discord channel](https://discord.gg/yF2MmDJyGJ)!💬 Welcome to join our community for sharing, discussions, and collaboration! 🎉🎉
<details>
<summary style="font-size: 1.4em; font-weight: bold; cursor: pointer; display: list-item;">
Algorithm Flowchart
</summary>
![LightRAG Indexing Flowchart](https://learnopencv.com/wp-content/uploads/2024/11/LightRAG-VectorDB-Json-KV-Store-Indexing-Flowchart-scaled.jpg)
*Figure 1: LightRAG Indexing Flowchart - Img Caption : [Source](https://learnopencv.com/lightrag/)*
![LightRAG Retrieval and Querying Flowchart](https://learnopencv.com/wp-content/uploads/2024/11/LightRAG-Querying-Flowchart-Dual-Level-Retrieval-Generation-Knowledge-Graphs-scaled.jpg)
*Figure 2: LightRAG Retrieval and Querying Flowchart - Img Caption : [Source](https://learnopencv.com/lightrag/)*
</details>
## Installation
**💡 Using uv for Package Management**: This project uses [uv](https://docs.astral.sh/uv/) for fast and reliable Python package management. Install uv first: `curl -LsSf https://astral.sh/uv/install.sh | sh` (Unix/macOS) or `powershell -c "irm https://astral.sh/uv/install.ps1 | iex"` (Windows)
> **Note**: You can also use pip if you prefer, but uv is recommended for better performance and more reliable dependency management.
>
> **📦 Offline Deployment**: For offline or air-gapped environments, see the [Offline Deployment Guide](./docs/OfflineDeployment.md) for instructions on pre-installing all dependencies and cache files.
### Install LightRAG Server
* Install from PyPI
```bash
### Install LightRAG Server as tool using uv (recommended)
uv tool install "lightrag-hku[api]"
### Or using pip
# python -m venv .venv
# source .venv/bin/activate # Windows: .venv\Scripts\activate
# pip install "lightrag-hku[api]"
### Build front-end artifacts
cd lightrag_webui
bun install --frozen-lockfile
bun run build
cd ..
# Setup env file
# Obtain the env.example file by downloading it from the GitHub repository root
# or by copying it from a local source checkout.
cp env.example .env # Update the .env with your LLM and embedding configurations
# Launch the server. It binds to all interfaces (0.0.0.0) by default.
# SECURITY: before exposing it on a network, configure authentication in .env
# (LIGHTRAG_API_KEY, or AUTH_ACCOUNTS together with TOKEN_SECRET), or bind to
# 127.0.0.1 for local-only access; without auth every endpoint is public.
# Note: the Ollama-compatible /api/* routes stay open by default for client
# compatibility; set WHITELIST_PATHS=/health to require auth on them too.
lightrag-server
```
* Installation from Source
```bash
git clone https://github.com/HKUDS/LightRAG.git
cd LightRAG
# Bootstrap the development environment (recommended)
make dev
source .venv/bin/activate # Activate the virtual environment (Linux/macOS)
# Or on Windows: .venv\Scripts\activate
# make dev installs the test toolchain plus the full offline stack
# (API, storage backends, and provider integrations), then builds the frontend.
# Run make env-base or copy env.example to .env before starting the server.
# Equivalent manual steps with uv
# Note: uv sync automatically creates a virtual environment in .venv/
uv sync --extra test --extra offline
source .venv/bin/activate # Activate the virtual environment (Linux/macOS)
# Or on Windows: .venv\Scripts\activate
### Or using pip with virtual environment
# python -m venv .venv
# source .venv/bin/activate # Windows: .venv\Scripts\activate
# pip install -e ".[test,offline]"
# Build front-end artifacts
cd lightrag_webui
bun install --frozen-lockfile
bun run build
cd ..
# setup env file
make env-base # Or: cp env.example .env and update it manually
# Launch API-WebUI server
lightrag-server
```
* Launching the LightRAG Server with Docker Compose
```bash
git clone https://github.com/HKUDS/LightRAG.git
cd LightRAG
cp env.example .env # Update the .env with your LLM and embedding configurations
# modify LLM and Embedding settings in .env
docker compose up
```
> Historical versions of LightRAG docker images can be found here: [LightRAG Docker Images]( https://github.com/HKUDS/LightRAG/pkgs/container/lightrag)
>
> Official GHCR images published by GitHub Actions are signed with Sigstore Cosign using GitHub OIDC. See [docs/DockerDeployment.md](./docs/DockerDeployment.md#verify-official-ghcr-images-with-cosign) for verification commands.
### Create .env File With Setup Tool
Instead of editing `env.example` by hand, use the interactive setup wizard to generate a configured `.env` and, when needed, `docker-compose.final.yml`:
```bash
make env-base # Required first step: LLM, embedding, reranker
make env-storage # Optional: storage backends and database services
make env-server # Optional: server port, auth, and SSL
make env-base-rewrite # Optional: force-regenerate wizard-managed compose services
make env-storage-rewrite # Optional: force-regenerate wizard-managed compose services
make env-security-check # Optional: audit the current .env for security risks
```
For full description of every target see [docs/InteractiveSetup.md](./docs/InteractiveSetup.md).
## About LightRAG
### A Lightweight, Graph-Based RAG Framework
LightRAG is a lightweight knowledge-graph RAG framework and an efficient alternative to Microsoft GraphRAG. It adopts a dual-layer architecture to manage both knowledge graphs (KGs) and vector embeddings, effectively bridging the gap between traditional vector-based RAG and graph-based RAG approaches. Designed for high scalability, LightRAG addresses key challenges in large-scale graph indexing and retrieval, including heavy computational overhead, slow response times, and the high cost of incremental updates. While supporting large datasets, LightRAG can still deliver exceptionally high RAG quality, even when paired with a 30B open-source large language model (LLM).
### Features & Advantages
- **Deep Contextual Understanding:** Through graph-structured indexing, LightRAG captures complex semantic dependencies between entities, overcoming the fragmented context limitations typical of traditional chunk-based retrieval methods. Its generation quality and context awareness are particularly outstanding in vertical domains (e.g., legal, financial) that require global comprehension or logical reasoning.
- **Exceptional Comprehensiveness & Diversity:** LightRAGs dual-level retrieval mechanism allows it to integrate detailed facts and abstract concepts concurrently. This enables the system to achieve remarkable performance in query result comprehensiveness and diversity, making it highly effective at handling complex, cross-document queries.
- **Extreme Retrieval Efficiency & Low Cost:** LightRAG does not rely on inefficient community reports or multi-hop reasoning for complex queries. This drastically reduces the number of LLM calls required during both the indexing and querying phases, significantly lowering response latency and LLM computational costs.
- **Rapid Adaptation to Dynamic Data:** LightRAG supports seamless, incremental knowledge base updates. New data only needs to go through a standard graph indexing pipeline to generate a local graph, which is then directly integrated into the existing graph via set merging. This process eliminates the need to disrupt the original structure or rebuild the global index, ensuring real-time relevance in dynamic data environments. When deleting documents, the system leverages LLM caching from the construction phase to rapidly rebuild affected entity relationships, vastly improving knowledge base update efficiency.
### Multimodal Capability Upgrades
Starting from version v1.5, LightRAG has officially introduced analysis and retrieval capabilities for multimodal documents:
- **Multi-Engine Document Parsing:** Its document processing pipeline supports parsing engines such as MinerU, Docling, and Native, enabling the highly efficient extraction of text, tables, formulas, and images from documents.
- **Cross-Modal Entity & Relation Mapping:** It achieves cross-modal entity extraction and relationship mapping within a unified framework, resulting in seamless indexing and querying.
- **Enhanced Application Scenarios:** The brand-new multimodal processing pipeline significantly improves RAG quality for documents rich in multimodal content, such as operation manuals and academic papers.
### LightRAG API Server
The LightRAG server offers not only a web-based UI for exploring LightRAG functionalities but also a comprehensive REST API. For more information about the LightRAG server, please refer to [LightRAG Server](./docs/LightRAG-API-Server.md).
![iShot_2025-03-23_12.40.08](./README.assets/iShot_2025-03-23_12.40.08.png)
## Key Configuration Guide
### Selecting LLM Models
LightRAG requires LLM/VLMs of four different roles during its workflow. You should configure models with different capabilities and speeds for different roles to strike a balance between performance and processing speed. LightRAG has higher capability requirements for Large Language Models (LLMs) than traditional RAG because it requires LLMs to perform complex entity-relation extraction tasks from documents. During the query phase, the LLM needs to process a large volume of retrieved information, including entities, relationships, and text chunks. This requires the model to have the capability of generating high-quality responses in long, noisy contexts. For detailed model configurations, please refer to [RoleSpecificLLMConfiguration.md](./docs/RoleSpecificLLMConfiguration.md)
### Selecting Query Modes
LightRAG supports five query modes:
- **local**: Focuses on precise matching of local contexts and specific entities. It retrieves candidate entities and their directly associated attributes from the knowledge graph. This mode is suitable for Q&A targeting specific objects, concrete concepts, or detailed facts, providing highly relevant and detailed local context support.
- **global**: Focuses on macro themes, cross-document reasoning, and deep relationships between entities. It retrieves relationship chains covering broad themes and concepts. This mode is suitable for queries that require summarization across multiple contexts, trend analysis, or understanding complex semantic dependencies.
- **hybrid**: Merges the retrieval results of both local and global modes. It performs comprehensive reasoning and generation by simultaneously recalling specific entities and global relationship contexts.
- **naive**: Traditional RAG retrieval based on text chunks. It does not use a knowledge graph and relies directly on vector similarity to retrieve from the original text chunks.
- **mix**: Fully-featured mode that merges retrieval results from local, global, and naive modes to provide the most comprehensive and rich retrieval results.
The default query mode for LightRAG is `mix`. Using `mix` mode generally yields the most ideal query results. The `mix` mode takes slightly longer than `naive`, while other query modes are roughly comparable in latency.
### Embedding Models
When choosing an Embedding model, pay attention to its multilingual support capabilities. Since LightRAG's retrieval quality has limited dependency on the Embedding model, it is recommended to choose low-dimensional and fast models. Typically, `BAAI/bge-m3` is sufficient. We highly recommend deploying the Embedding model locally to achieve the best performance.
**Important Note**: The Embedding model must be determined before document indexing, and the same model must be used in the query phase. Once selected, embedding models generally cannot be changed. If changed, you will need to re-embed all text chunks, entities, and relationships. LightRAG does not currently provide a re-embedding tool. Some storage backends (e.g., PostgreSQL) require the vector dimension to be defined when creating tables for the first time, so changing the Embedding model requires deleting vector-related tables so LightRAG can recreate them.
### Enabling Reranking
Enabling the Rerank option during the query phase can significantly improve query quality. However, enabling Rerank typically introduces a 12 second delay. To minimize latency, it is highly recommended to deploy the Rerank model locally. For configuration details, please refer to the `.env.example` file. Unlike Embedding models, the Rerank model can be changed at any time during the query phase.
### Document Processing Pipeline Configuration
The default pipeline configuration in LightRAG does not allow the system to perform at its best. The quality of document parsing greatly impacts document indexing and querying. Therefore, we recommend configuring the pipeline to enable the MinerU parsing engine and activating the pipeline's image analysis features. Suggested configuration:
```
LIGHTRAG_PARSER=*:native-iteP,*:mineru-iteP,*:legacy-R
VLM_PROCESS_ENABLE=true
VLM_LLM_MODEL=<your_vlm_model_name>
```
Since the cloud-based MinerU service has limitations on usage, file size, and page count, it is recommended to use a locally deployed MinerU. For details on configuring the file processing pipeline, please refer to [FileProcessingPipeline.md](./docs/FileProcessingPipeline.md)
### Concurrency Optimization for File Processing
For large-scale document processing, you need to improve concurrency. Key environment variables related to concurrent file processing include:
- **MAX_ASYNC_LLM/EXTRACT_ASYNC_LLM**: Controls the maximum concurrency for LLM models.
- **MAX_PARALLEL_INSERT**: Controls the maximum number of files processed in parallel. Processing of text, tables, formulas, and images within a single file will also occur concurrently. `MAX_PARALLEL_INSERT` should ideally be set to about 1/3 of `MAX_ASYNC_LLM`.
- **MAX_PARALLEL_PARSE_MINERU**: Controls the number of parallel files processed for MinerU parsing.
- **MAX_PARALLEL_PARSE_DOCLING**: Controls the number of parallel files processed for Docling parsing.
- **EMBEDDING_FUNC_MAX_ASYNC**: Controls the maximum concurrency for embedding models.
- **EMBEDDING_BATCH_NUM**: Controls the number of texts included in each embedding model request (how many embeddings per batch). Increasing this number can significantly reduce the number of API calls to the embedding model and speed up data persistence in the embedding storage.
```
# Sample Configuration
MAX_ASYNC_LLM=8
MAX_PARALLEL_INSERT=3
EMBEDDING_FUNC_MAX_ASYNC=16
EMBEDDING_BATCH_NUM=32
```
### Selecting Backend Storage
LightRAG requires four types of backend storage:
- **KV_STORAGE**: Used to save LLM response caches, text chunking results, entity-relation extraction results, etc.
- **VECTOR_STORAGE**: Used to store vector information for text chunks, entities, and relationships.
- **GRAPH_STORAGE**: Used to save the knowledge graph.
- **DOC_STATUS_STORAGE**: Used to store the document list.
By default, LightRAG's storage backends are file-persisted, in-memory databases. These default storages are intended only for development and debugging, and are not suitable for production. In a production environment, if you prefer a single backend to handle all four storage types, you can choose PostgreSQL, MongoDB, or OpenSearch. Alternatively, you can select specialized databases for vector or graph storage, such as using Milvus or Qdrant for vector storage, and Neo4j or Memgraph for graph storage.
### Other Important Configurations for Document Processing
During the document insertion stage, you may also want to adjust the following environment variables based on your needs:
- **SUMMARY_LANGUAGE**: Controls the language used by the LLM when outputting entity-relation names and summaries, e.g., `Chinese`, `English`.
- **ENTITY_EXTRACTION_USE_JSON**: Controls whether the LLM outputs entity-relation extractions in JSON format. Using JSON format typically yields more stable results, but it consumes more tokens and can be slightly slower.
- **ENABLE_CONTENT_HEADINGS**: Controls whether the section heading information of a text chunk is sent to the LLM during the query stage (enabled by default, providing more context for the LLM).
- **FORCE_LLM_SUMMARY_ON_MERGE / MAX_SOURCE_IDS_PER_RELATION**: Controls the maximum number of text chunks an `entity/relation` can be associated with.
- **SOURCE_IDS_LIMIT_METHOD**: Controls whether to keep updating the entity/relation description once an `entity/relation` exceeds its associated text chunk limit (by default it stops updating, because at that point the entity-relation description is already rich enough and further updates add little value; skipping updates can greatly speed up knowledge base construction).
- **DEFAULT_MAX_FILE_PATHS**: Controls the maximum number of source files an `entity/relation` can be associated with; once this limit is exceeded, new file names are no longer written to the vector storage.
### Resolving LLM Timeouts During Entity-Relation Extraction
LLM timeouts during entity-relation extraction usually trace back to one of three causes. Identify the cause, then apply the matching remedy (the parameters can be combined):
- **The model is slow.** A model running below ~50 tokens/second may be unable to finish a chunk that contains many entities and relations before the request times out. Increase the timeout via `*_LLM_TIMEOUT` — either the global `LLM_TIMEOUT` or the role-specific `EXTRACT_LLM_TIMEOUT` for the extraction phase. Note that the effective execution timeout is **twice** the configured value, so `EXTRACT_LLM_TIMEOUT=300` allows up to **600 seconds**.
- **The chunk produces too many entities and relations.** Reference/bibliography chunks, for example, can make the model emit an enormous number of records that cannot complete in time. Cap the output length with `OPENAI_LLM_MAX_TOKENS` or `OPENAI_LLM_MAX_COMPLETION_TOKENS` (the correct parameter name depends on the LLM provider — see `env.example`). A useful sizing rule is `max_output_tokens < LLM_TIMEOUT × tokens_per_second` (e.g., `9000 < 240s × 50 tps`).
- **The model gets stuck in an output loop.** Some models (locally deployed Qwen models in particular) occasionally fall into an endless-output loop on certain text. When this is intermittent, simply re-processing the document once usually resolves it.
- **References specifically (P chunking strategy).** When using the paragraph-semantic (`P`) chunking strategy (e.g., `LIGHTRAG_PARSER=...-iteP`), set `CHUNK_P_DROP_REFERENCES=true` to automatically drop the trailing reference section before chunking. This prevents references from generating a flood of low-value entities and relations, a common source of timeouts. It can also be enabled per file via the filename hint `paper.[-P(drop_rf=true)].pdf`; related detection knobs (`CHUNK_P_REFERENCES_TAIL_N`, `CHUNK_P_REFERENCES_HEADINGS`) are documented in `env.example`.
### Other Important Configurations for Document Querying
During the document query stage, you may also want to adjust the following environment variables based on your needs:
- **MAX_ENTITY_TOKENS / MAX_RELATION_TOKENS / MAX_TOTAL_TOKENS**: Controls the token length of the retrieved content sent to the LLM context. The retrieved content consists of three parts: `entities`, `relations`, and `text chunks`. The lengths of entities and relations can be controlled independently, while the text chunk length is determined by subtracting the entity and relation lengths from the total length.
- **ENABLE_CONTENT_HEADINGS**: Controls whether the section heading where a text chunk resides is sent to the LLM; enabled by default, providing richer context for the LLM and improving answer quality.
- **ENABLE_LLM_CACHE**: Whether to cache query results. Enabled by default; identical query questions, query modes, and LLM model parameters will return the same result.
## Using LightRAG As SDK
> ⚠️ **For integration into your project, we strongly recommend using the REST API provided by the LightRAG Server.** The LightRAG SDK is primarily intended for embedded applications or academic research and evaluation purposes.
### Install LightRAG SDK
* Install from source code
```bash
cd LightRAG
# 注意: uv sync 会自动在 .venv/ 目录创建虚拟环境
uv sync
source .venv/bin/activate # 激活虚拟环境 (Linux/macOS)
# Windows 系统: .venv\Scripts\activate
# 或: pip install -e .
```
* Install from PyPI
```bash
uv pip install lightrag-hku
# 或: pip install lightrag-hku
```
### LightRAG SDK Sample Code
To get started with LightRAG core, refer to the sample codes available in the `examples` folder. Additionally, a [video demo](https://www.youtube.com/watch?v=g21royNJ4fw) demonstration is provided to guide you through the local setup process. If you already possess an OpenAI API key, you can run the demo right away:
```bash
### you should run the demo code with project folder
cd LightRAG
### provide your API-KEY for OpenAI
export OPENAI_API_KEY="sk-...your_opeai_key..."
### download the demo document of "A Christmas Carol" by Charles Dickens
curl https://raw.githubusercontent.com/gusye1234/nano-graphrag/main/tests/mock_data.txt > ./book.txt
### run the demo code
python examples/lightrag_openai_demo.py
```
For a streaming response implementation example, please see `examples/lightrag_openai_compatible_demo.py`. Prior to execution, ensure you modify the sample code's LLM and embedding configurations accordingly.
**Note 1**: When running the demo program, please be aware that different test scripts may use different embedding models. If you switch to a different embedding model, you must clear the data directory (`./dickens`); otherwise, the program may encounter errors. If you wish to retain the LLM cache, you can preserve the `kv_store_llm_response_cache.json` file while clearing the data directory.
**Note 2**: Only `lightrag_openai_demo.py` and `lightrag_openai_compatible_demo.py` are officially supported sample codes. Other sample files are community contributions that haven't undergone full testing and optimization.
### **Notes on SDK Usage**
For detailed instructions on using the SDK, please refer to **[docs/ProgramingWithCore.md](./docs/ProgramingWithCore.md)**. Some LightRAG features are not exposed via the REST API and are accessible only through the SDK. These features are typically experimental and may not be compatible with future versions.
## Replicating Findings in the Paper
LightRAG consistently outperforms NaiveRAG, RQ-RAG, HyDE, and GraphRAG across agriculture, computer science, legal, and mixed domains. For the full evaluation methodology, prompts, and reproduce steps, see **[docs/Reproduce.md](./docs/Reproduce.md)**.
**Overall Performance Table**
||**Agriculture**||**CS**||**Legal**||**Mix**||
|----------------------|---------------|------------|------|------------|---------|------------|-------|------------|
||NaiveRAG|**LightRAG**|NaiveRAG|**LightRAG**|NaiveRAG|**LightRAG**|NaiveRAG|**LightRAG**|
|**Comprehensiveness**|32.4%|**67.6%**|38.4%|**61.6%**|16.4%|**83.6%**|38.8%|**61.2%**|
|**Diversity**|23.6%|**76.4%**|38.0%|**62.0%**|13.6%|**86.4%**|32.4%|**67.6%**|
|**Empowerment**|32.4%|**67.6%**|38.8%|**61.2%**|16.4%|**83.6%**|42.8%|**57.2%**|
|**Overall**|32.4%|**67.6%**|38.8%|**61.2%**|15.2%|**84.8%**|40.0%|**60.0%**|
||RQ-RAG|**LightRAG**|RQ-RAG|**LightRAG**|RQ-RAG|**LightRAG**|RQ-RAG|**LightRAG**|
|**Comprehensiveness**|31.6%|**68.4%**|38.8%|**61.2%**|15.2%|**84.8%**|39.2%|**60.8%**|
|**Diversity**|29.2%|**70.8%**|39.2%|**60.8%**|11.6%|**88.4%**|30.8%|**69.2%**|
|**Empowerment**|31.6%|**68.4%**|36.4%|**63.6%**|15.2%|**84.8%**|42.4%|**57.6%**|
|**Overall**|32.4%|**67.6%**|38.0%|**62.0%**|14.4%|**85.6%**|40.0%|**60.0%**|
||HyDE|**LightRAG**|HyDE|**LightRAG**|HyDE|**LightRAG**|HyDE|**LightRAG**|
|**Comprehensiveness**|26.0%|**74.0%**|41.6%|**58.4%**|26.8%|**73.2%**|40.4%|**59.6%**|
|**Diversity**|24.0%|**76.0%**|38.8%|**61.2%**|20.0%|**80.0%**|32.4%|**67.6%**|
|**Empowerment**|25.2%|**74.8%**|40.8%|**59.2%**|26.0%|**74.0%**|46.0%|**54.0%**|
|**Overall**|24.8%|**75.2%**|41.6%|**58.4%**|26.4%|**73.6%**|42.4%|**57.6%**|
||GraphRAG|**LightRAG**|GraphRAG|**LightRAG**|GraphRAG|**LightRAG**|GraphRAG|**LightRAG**|
|**Comprehensiveness**|45.6%|**54.4%**|48.4%|**51.6%**|48.4%|**51.6%**|**50.4%**|49.6%|
|**Diversity**|22.8%|**77.2%**|40.8%|**59.2%**|26.4%|**73.6%**|36.0%|**64.0%**|
|**Empowerment**|41.2%|**58.8%**|45.2%|**54.8%**|43.6%|**56.4%**|**50.8%**|49.2%|
|**Overall**|45.2%|**54.8%**|48.0%|**52.0%**|47.2%|**52.8%**|**50.4%**|49.6%|
## 🔗 Related Projects
*Ecosystem & Extensions*
<div align="center">
<table>
<tr>
<td align="center">
<a href="https://github.com/HKUDS/RAG-Anything">
<div style="width: 100px; height: 100px; background: linear-gradient(135deg, rgba(0, 217, 255, 0.1) 0%, rgba(0, 217, 255, 0.05) 100%); border-radius: 15px; border: 1px solid rgba(0, 217, 255, 0.2); display: flex; align-items: center; justify-content: center; margin-bottom: 10px;">
<span style="font-size: 32px;">📸</span>
</div>
<b>RAG-Anything</b><br>
<sub>Multimodal RAG</sub>
</a>
</td>
<td align="center">
<a href="https://github.com/HKUDS/VideoRAG">
<div style="width: 100px; height: 100px; background: linear-gradient(135deg, rgba(0, 217, 255, 0.1) 0%, rgba(0, 217, 255, 0.05) 100%); border-radius: 15px; border: 1px solid rgba(0, 217, 255, 0.2); display: flex; align-items: center; justify-content: center; margin-bottom: 10px;">
<span style="font-size: 32px;">🎥</span>
</div>
<b>VideoRAG</b><br>
<sub>Extreme Long-Context Video RAG</sub>
</a>
</td>
<td align="center">
<a href="https://github.com/HKUDS/MiniRAG">
<div style="width: 100px; height: 100px; background: linear-gradient(135deg, rgba(0, 217, 255, 0.1) 0%, rgba(0, 217, 255, 0.05) 100%); border-radius: 15px; border: 1px solid rgba(0, 217, 255, 0.2); display: flex; align-items: center; justify-content: center; margin-bottom: 10px;">
<span style="font-size: 32px;">✨</span>
</div>
<b>MiniRAG</b><br>
<sub>Extremely Simple RAG</sub>
</a>
</td>
</tr>
</table>
</div>
---
## ⭐ Star History
[![Star History Chart](https://api.star-history.com/svg?repos=HKUDS/LightRAG&type=Date)](https://star-history.com/#HKUDS/LightRAG&Date)
## 🤝 Contribution
<div align="center">
We welcome contributions of all kinds — bug fixes, new features, documentation improvements, and more.<br>
Please read our <a href=".github/CONTRIBUTING.md"><strong>Contributing Guide</strong></a> before submitting a pull request.
</div>
<br>
<div align="center">
We thank all our contributors for their valuable contributions.
</div>
<div align="center">
<a href="https://github.com/HKUDS/LightRAG/graphs/contributors">
<img src="https://contrib.rocks/image?repo=HKUDS/LightRAG" style="border-radius: 15px; box-shadow: 0 0 20px rgba(0, 217, 255, 0.3);" />
</a>
</div>
## 📖 Citation
```python
@article{guo2024lightrag,
title={LightRAG: Simple and Fast Retrieval-Augmented Generation},
author={Zirui Guo and Lianghao Xia and Yanhua Yu and Tu Ao and Chao Huang},
year={2024},
eprint={2410.05779},
archivePrefix={arXiv},
primaryClass={cs.IR}
}
```
---
<div align="center" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 15px; padding: 30px; margin: 30px 0;">
<div>
<img src="https://user-images.githubusercontent.com/74038190/212284100-561aa473-3905-4a80-b561-0d28506553ee.gif" width="500">
</div>
<div style="margin-top: 20px;">
<a href="https://github.com/HKUDS/LightRAG" style="text-decoration: none;">
<img src="https://img.shields.io/badge/⭐%20Star%20us%20on%20GitHub-1a1a2e?style=for-the-badge&logo=github&logoColor=white">
</a>
<a href="https://github.com/HKUDS/LightRAG/issues" style="text-decoration: none;">
<img src="https://img.shields.io/badge/🐛%20Report%20Issues-ff6b6b?style=for-the-badge&logo=github&logoColor=white">
</a>
<a href="https://github.com/HKUDS/LightRAG/discussions" style="text-decoration: none;">
<img src="https://img.shields.io/badge/💬%20Discussions-4ecdc4?style=for-the-badge&logo=github&logoColor=white">
</a>
</div>
</div>
<div align="center">
<div style="width: 100%; max-width: 600px; margin: 20px auto; padding: 20px; background: linear-gradient(135deg, rgba(0, 217, 255, 0.1) 0%, rgba(0, 217, 255, 0.05) 100%); border-radius: 15px; border: 1px solid rgba(0, 217, 255, 0.2);">
<div style="display: flex; justify-content: center; align-items: center; gap: 15px;">
<span style="font-size: 24px;">⭐</span>
<span style="color: #00d9ff; font-size: 18px;">Thank you for visiting LightRAG!</span>
<span style="font-size: 24px;">⭐</span>
</div>
</div>
</div>
+7
View File
@@ -0,0 +1,7 @@
# WeHub 来源说明
- 原始项目:`HKUDS/LightRAG`
- 原始仓库:https://github.com/HKUDS/LightRAG
- 导入方式:上游默认分支的最新快照
- 原作者、版权和许可证信息以原始仓库及本仓库 LICENSE 为准
- 本文件仅用于记录来源,不代表 WeHub 是原项目作者
+18
View File
@@ -0,0 +1,18 @@
# Reporting Security Issues
The LightRAG team and community take security bugs seriously. We appreciate your efforts to responsibly disclose your findings, and will make every effort to acknowledge your contributions.
To report a security issue, please use the GitHub Security Advisory: [Report a Vulnerability](https://github.com/HKUDS/LightRAG/security/advisories/new)
The LightRAG team will send a response indicating the next steps in handling your report. After the initial reply to your report, the security team will keep you informed of the progress towards a fix and full announcement, and may ask for additional information or guidance.
Report security bugs in third-party modules to the person or team maintaining the module.
### Supported Versions
The following versions currently being supported with security updates.
| Version | Supported |
| ------- | ------------------ |
| 1.2.x | :x: |
| 1.3.x | :white_check_mark: |
Binary file not shown.

After

Width:  |  Height:  |  Size: 498 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

+52
View File
@@ -0,0 +1,52 @@
; DEPRECATION WARNING:
; `config.ini` support will be removed in a future release.
; Please move your configuration to `.env` or environment variables.
; This file is kept only as a temporary compatibility example.
[neo4j]
uri = neo4j+s://xxxxxxxx.databases.neo4j.io
username = neo4j
password = your-password
connection_pool_size = 100
connection_timeout = 30.0
connection_acquisition_timeout = 30.0
max_transaction_retry_time = 30.0
max_connection_lifetime = 300.0
liveness_check_timeout = 30.0
keep_alive = true
[mongodb]
uri = mongodb+srv://name:password@your-cluster-address
database = lightrag
[redis]
uri=redis://localhost:6379/1
[qdrant]
uri = http://localhost:16333
[postgres]
host = localhost
port = 5432
user = your_username
password = your_password
database = your_database
# workspace = default
max_connections = 12
vector_index_type = HNSW # HNSW, IVFFLAT or VCHORDRQ
hnsw_m = 16
hnsw_ef = 64
ivfflat_lists = 100
vchordrq_build_options =
vchordrq_probes =
vchordrq_epsilon = 1.9
[memgraph]
uri = bolt://localhost:7687
[milvus]
uri = http://localhost:19530
db_name = lightrag
# user = root
# password = your_password
# token = your_token
+77
View File
@@ -0,0 +1,77 @@
#!/bin/bash
set -e
# Configuration
IMAGE_NAME="ghcr.io/hkuds/lightrag"
DOCKERFILE="Dockerfile"
TAG="latest"
# Get version from git tags
VERSION=$(git describe --tags --abbrev=0 2>/dev/null || echo "dev")
echo "=================================="
echo " Multi-Architecture Docker Build"
echo "=================================="
echo "Image: ${IMAGE_NAME}:${TAG}"
echo "Version: ${VERSION}"
echo "Platforms: linux/amd64, linux/arm64"
echo "=================================="
echo ""
# Check Docker login status (skip if CR_PAT is set for CI/CD)
if [ -z "$CR_PAT" ]; then
if ! docker info 2>/dev/null | grep -q "Username"; then
echo "⚠️ Warning: Not logged in to Docker registry"
echo "Please login first: docker login ghcr.io"
echo "Or set CR_PAT environment variable for automated login"
echo ""
read -p "Continue anyway? (y/n) " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
exit 1
fi
fi
else
echo "Using CR_PAT environment variable for authentication"
fi
# Check if buildx builder exists, create if not
if ! docker buildx ls | grep -q "desktop-linux"; then
echo "Creating buildx builder..."
docker buildx create --name desktop-linux --use
docker buildx inspect --bootstrap
else
echo "Using existing buildx builder: desktop-linux"
docker buildx use desktop-linux
fi
echo ""
echo "Building and pushing multi-architecture image..."
echo ""
# Build and push
docker buildx build \
--platform linux/amd64,linux/arm64 \
--file ${DOCKERFILE} \
--tag ${IMAGE_NAME}:${TAG} \
--tag ${IMAGE_NAME}:${VERSION} \
--push \
.
echo ""
echo "✓ Build and push complete!"
echo ""
echo "Images pushed:"
echo " - ${IMAGE_NAME}:${TAG}"
echo " - ${IMAGE_NAME}:${VERSION}"
echo ""
echo "Verifying multi-architecture manifest..."
echo ""
# Verify
docker buildx imagetools inspect ${IMAGE_NAME}:${TAG}
echo ""
echo "✓ Verification complete!"
echo ""
echo "Pull with: docker pull ${IMAGE_NAME}:${TAG}"
+247
View File
@@ -0,0 +1,247 @@
# Full Docker Compose Deployment Sample Generated by Setup Wizard: `make base` and `make storage`
# This Sample File requires NVIDIA GPU for Milvus and VLLM services.
# Copy `env.docker-compose-full` to `.env` before starting this compose file.
# You can customize your setup using the Setup Wizard; for detailed instructions, please refer to docs/InteractiveSetup.md
services:
lightrag:
image: ghcr.io/hkuds/lightrag:latest
build:
context: .
dockerfile: Dockerfile
tags:
- ghcr.io/hkuds/lightrag:latest
ports:
- "${HOST:-0.0.0.0}:${PORT:-9621}:9621"
volumes:
- ./data/rag_storage:/app/data/rag_storage
- ./data/inputs:/app/data/inputs
- ./config.ini:/app/config.ini
- ./data/prompts:/app/data/prompts
- ./.env:/app/.env
deploy:
restart_policy:
condition: on-failure
max_attempts: 10
extra_hosts:
- "host.docker.internal:host-gateway"
environment:
# The container must listen on 0.0.0.0 to be reachable via the published port.
# SECURITY: set LIGHTRAG_API_KEY (or AUTH_ACCOUNTS with TOKEN_SECRET) in .env
# so the exposed server is authenticated — without it every endpoint is public.
HOST: "0.0.0.0"
PORT: "9621"
EMBEDDING_BINDING_HOST: "http://vllm-embed:8001/v1"
RERANK_BINDING_HOST: "http://vllm-rerank:8000/rerank"
POSTGRES_HOST: "postgres"
POSTGRES_PORT: "5432"
NEO4J_URI: "neo4j://neo4j:7687"
WORKING_DIR: "/app/data/rag_storage"
MILVUS_URI: "http://milvus:19530"
INPUT_DIR: "/app/data/inputs"
PROMPT_DIR: "/app/data/prompts"
MEMGRAPH_URI: "bolt://host.docker.internal:7687"
REDIS_URI: "redis://host.docker.internal:6379"
QDRANT_URL: "http://host.docker.internal:6333"
OPENSEARCH_HOSTS: "host.docker.internal:9200"
MONGO_URI: "mongodb://root:root@host.docker.internal:27017/"
depends_on:
vllm-embed:
condition: service_healthy
vllm-rerank:
condition: service_healthy
postgres:
condition: service_healthy
neo4j:
condition: service_healthy
milvus:
condition: service_healthy
vllm-embed:
image: vllm/vllm-openai:latest
runtime: nvidia
command: >
--model ${VLLM_EMBED_MODEL:-BAAI/bge-m3}
--port ${VLLM_EMBED_PORT:-8001}
--dtype float16
--api-key ${VLLM_EMBED_API_KEY}
${VLLM_EMBED_EXTRA_ARGS:-}
environment:
NVIDIA_VISIBLE_DEVICES: ${NVIDIA_VISIBLE_DEVICES:-all}
NVIDIA_DRIVER_CAPABILITIES: ${NVIDIA_DRIVER_CAPABILITIES:-compute,utility}
ports:
- "${VLLM_EMBED_PORT:-8001}:${VLLM_EMBED_PORT:-8001}"
volumes:
- vllm_embed_cache:/root/.cache/huggingface
ipc: host
healthcheck:
test:
- CMD-SHELL
- 'PORT_HEX="$(printf ''%04X'' ${VLLM_EMBED_PORT:-8001})"; cat /proc/net/tcp /proc/net/tcp6 2>/dev/null | grep -q ":$${PORT_HEX} "'
interval: 5s
timeout: 3s
retries: 120
start_period: 10s
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: all
capabilities: [gpu]
restart: unless-stopped
vllm-rerank:
image: vllm/vllm-openai:latest
runtime: nvidia
command: >
--model ${VLLM_RERANK_MODEL:-BAAI/bge-reranker-v2-m3}
--port ${VLLM_RERANK_PORT:-8000}
--dtype float16
--api-key ${VLLM_RERANK_API_KEY}
${VLLM_RERANK_EXTRA_ARGS:-}
environment:
NVIDIA_VISIBLE_DEVICES: ${NVIDIA_VISIBLE_DEVICES:-all}
NVIDIA_DRIVER_CAPABILITIES: ${NVIDIA_DRIVER_CAPABILITIES:-compute,utility}
ports:
- "${VLLM_RERANK_PORT:-8000}:${VLLM_RERANK_PORT:-8000}"
volumes:
- vllm_rerank_cache:/root/.cache/huggingface
ipc: host
healthcheck:
test:
- CMD-SHELL
- 'PORT_HEX="$(printf ''%04X'' ${VLLM_RERANK_PORT:-8000})"; cat /proc/net/tcp /proc/net/tcp6 2>/dev/null | grep -q ":$${PORT_HEX} "'
interval: 5s
timeout: 3s
retries: 120
start_period: 10s
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: all
capabilities: [gpu]
restart: unless-stopped
postgres:
# this image does not support PGGraphStorage
image: pgvector/pgvector:pg18
# ports:
# - "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql
healthcheck:
test:
- CMD-SHELL
- 'PORT_HEX="$(printf ''%04X'' 5432)"; cat /proc/net/tcp /proc/net/tcp6 2>/dev/null | grep -q ":$${PORT_HEX} "'
interval: 5s
timeout: 3s
retries: 120
start_period: 10s
restart: unless-stopped
environment:
POSTGRES_USER: "rag"
POSTGRES_PASSWORD: "rag"
POSTGRES_DB: "rag"
neo4j:
image: neo4j:5-community
# ports:
# - "7474:7474"
# - "${NEO4J_BOLT_PORT:-7687}:7687"
volumes:
- neo4j_data:/data
healthcheck:
test:
- CMD-SHELL
- 'PORT_HEX="$(printf ''%04X'' 7687)"; cat /proc/net/tcp /proc/net/tcp6 2>/dev/null | grep -q ":$${PORT_HEX} "'
interval: 10s
timeout: 3s
retries: 120
start_period: 10s
restart: unless-stopped
environment:
NEO4J_AUTH: ${NEO4J_USERNAME:?missing}/${NEO4J_PASSWORD:?missing}
NEO4J_dbms_default__database: "neo4j"
milvus:
image: milvusdb/milvus:v2.6.11-gpu
command: ["milvus", "run", "standalone"]
security_opt:
- seccomp:unconfined
environment:
ETCD_ENDPOINTS: milvus-etcd:2379
MINIO_ADDRESS: milvus-minio:9000
MINIO_ACCESS_KEY_ID: "${MINIO_ACCESS_KEY_ID:?missing}"
MINIO_SECRET_ACCESS_KEY: "${MINIO_SECRET_ACCESS_KEY:?missing}"
# ports:
# - "19530:19530"
# - "9091:9091"
volumes:
- milvus_data:/var/lib/milvus
deploy:
resources:
reservations:
devices:
- driver: nvidia
capabilities: ["gpu"]
healthcheck:
test:
- CMD-SHELL
- 'PORT_HEX="$(printf ''%04X'' 19530)"; cat /proc/net/tcp /proc/net/tcp6 2>/dev/null | grep -q ":$${PORT_HEX} "'
interval: 10s
timeout: 3s
retries: 120
start_period: 10s
depends_on:
milvus-etcd:
condition: service_healthy
milvus-minio:
condition: service_healthy
restart: unless-stopped
milvus-etcd:
image: quay.io/coreos/etcd:v3.5.25
environment:
ETCD_AUTO_COMPACTION_MODE: revision
ETCD_AUTO_COMPACTION_RETENTION: "1000"
ETCD_QUOTA_BACKEND_BYTES: "4294967296"
ETCD_SNAPSHOT_COUNT: "50000"
volumes:
- milvus-etcd_data:/etcd
command: >
etcd
-advertise-client-urls=http://0.0.0.0:2379
-listen-client-urls=http://0.0.0.0:2379
-data-dir /etcd
healthcheck:
test: ["CMD", "etcdctl", "endpoint", "health"]
interval: 20s
timeout: 20s
retries: 3
restart: unless-stopped
milvus-minio:
image: minio/minio:RELEASE.2025-09-07T16-13-09Z
environment:
MINIO_ROOT_USER: "${MINIO_ACCESS_KEY_ID:?missing}"
MINIO_ROOT_PASSWORD: "${MINIO_SECRET_ACCESS_KEY:?missing}"
volumes:
- milvus-minio_data:/minio_data
command: minio server /minio_data --console-address ":9001"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
interval: 30s
timeout: 20s
retries: 3
restart: unless-stopped
volumes:
vllm_embed_cache:
vllm_rerank_cache:
postgres_data:
neo4j_data:
milvus_data:
milvus-etcd_data:
milvus-minio_data:
+40
View File
@@ -0,0 +1,40 @@
# Podman-compatible compose file for LightRAG
#
# Usage:
# podman-compose -f docker-compose.podman.yml up -d
#
# Key differences from docker-compose.yml:
# - Uses top-level `restart` instead of `deploy.restart_policy`
# (Podman does not support the deploy block for restart policies)
# - No `extra_hosts` with `host-gateway` (Podman fails on this special
# value; Podman auto-provides host.containers.internal for host access)
# - When connecting to host services (e.g. LLM, embedding, rerank),
# use `host.containers.internal` instead of `host.docker.internal`
# in your .env binding host configuration
services:
lightrag:
image: ghcr.io/hkuds/lightrag:latest
build:
context: .
dockerfile: Dockerfile
tags:
- ghcr.io/hkuds/lightrag:latest
ports:
- "${HOST:-0.0.0.0}:${PORT:-9621}:9621"
volumes:
- ./data/rag_storage:/app/data/rag_storage
- ./data/inputs:/app/data/inputs
- ./data/prompts:/app/data/prompts
- ./config.ini:/app/config.ini
- ./.env:/app/.env
restart: on-failure:10
environment:
WORKING_DIR: "/app/data/rag_storage"
INPUT_DIR: "/app/data/inputs"
PROMPT_DIR: "/app/data/prompts"
# The container must listen on 0.0.0.0 to be reachable via the published port.
# SECURITY: set LIGHTRAG_API_KEY (or AUTH_ACCOUNTS with TOKEN_SECRET) in .env
# so the exposed server is authenticated — without it every endpoint is public.
HOST: "0.0.0.0"
PORT: "9621"
+30
View File
@@ -0,0 +1,30 @@
services:
lightrag:
image: ghcr.io/hkuds/lightrag:latest
build:
context: .
dockerfile: Dockerfile
tags:
- ghcr.io/hkuds/lightrag:latest
ports:
- "${HOST:-0.0.0.0}:${PORT:-9621}:9621"
volumes:
- ./data/rag_storage:/app/data/rag_storage
- ./data/inputs:/app/data/inputs
- ./data/prompts:/app/data/prompts
- ./.env:/app/.env
deploy:
restart_policy:
condition: on-failure
max_attempts: 10
extra_hosts:
- "host.docker.internal:host-gateway"
environment:
WORKING_DIR: "/app/data/rag_storage"
INPUT_DIR: "/app/data/inputs"
PROMPT_DIR: "/app/data/prompts"
# The container must listen on 0.0.0.0 to be reachable via the published port.
# SECURITY: set LIGHTRAG_API_KEY (or AUTH_ACCOUNTS with TOKEN_SECRET) in .env
# so the exposed server is authenticated — without it every endpoint is public.
HOST: "0.0.0.0"
PORT: "9621"
+55
View File
@@ -0,0 +1,55 @@
#!/bin/sh
set -e
# Run the server as the non-root "lightrag" user (CIS Docker 4.1) while staying
# compatible with existing deployments whose bind-mounted data is root-owned.
#
# Image-internal chown cannot fix runtime mounts, so when the container starts
# as root we chown the data dirs and then drop privileges via gosu. When the
# orchestrator already starts us as non-root (compose `user:` / k8s
# `runAsUser`), we skip the chown and exec directly.
# Preserve the pre-split behavior where `docker run <image> --port 9622` appended
# flags to the server. Now that ENTRYPOINT is this script, a first arg starting
# with "-" means the user only passed flags, so prepend the default command.
if [ "${1#-}" != "$1" ]; then
set -- python -m lightrag.api.lightrag_server "$@"
fi
if [ "$(id -u)" = "0" ]; then
# Take ownership of the writable data locations so the dropped-privilege
# process can read/write them, covering bind-mounts/PVCs whose host content
# is root-owned. We create+chown /app/data (the default home for all data)
# plus any custom dirs configured via env (WORKING_DIR/INPUT_DIR/PROMPT_DIR/
# TIKTOKEN_CACHE_DIR/LOG_DIR), so deployments that point these outside
# /app/data keep working. The mkdir matters when only the parent is mounted
# (e.g. a bind mount/PVC at /data with WORKING_DIR=/data/site01/storage): the
# leaf does not exist yet, and once we drop to uid 1000 the server can no
# longer mkdir it under the root-owned parent. Creating it here as root and
# handing it to lightrag avoids that PermissionError.
#
# ERROR_LOG/ACCESS_LOG (gunicorn) are *file* paths, so we chown their parent
# directory rather than the file itself. Unset values and system roots are
# skipped; read-only mounts fail the mkdir/chown harmlessly.
_error_log_dir=""
_access_log_dir=""
[ -n "$ERROR_LOG" ] && _error_log_dir=$(dirname "$ERROR_LOG")
[ -n "$ACCESS_LOG" ] && _access_log_dir=$(dirname "$ACCESS_LOG")
for _d in /app/data "$WORKING_DIR" "$INPUT_DIR" "$PROMPT_DIR" \
"$TIKTOKEN_CACHE_DIR" "$LOG_DIR" "$_error_log_dir" "$_access_log_dir"; do
case "$_d" in
""|.|/|/bin|/boot|/dev|/etc|/home|/lib|/lib64|/proc|/root|/run|/sbin|/sys|/usr|/var) continue ;;
esac
mkdir -p "$_d" 2>/dev/null || true
chown -R lightrag:lightrag "$_d" 2>/dev/null || true
done
# NOTE: we deliberately do NOT chown /app/.env. On a bind-mount that would
# change the *host* file's owner to uid 1000, forcing the host user to sudo
# just to edit their config. .env only needs to be *readable* by uid 1000,
# which the default 0644 already satisfies. A 0600 .env owned by another uid
# must be made readable (chmod/chown on the host) or supplied via env vars
# (compose env_file:/environment:, k8s env/envFrom).
exec gosu lightrag "$@"
fi
exec "$@"
+179
View File
@@ -0,0 +1,179 @@
# Asymmetric Embedding Configuration
LightRAG keeps embedding behavior symmetric by default. Query/document asymmetric
embedding is enabled only when `EMBEDDING_ASYMMETRIC=true` is explicitly set.
This avoids accidental retrieval changes when prefix variables are present in an
environment but the user did not intentionally enable asymmetric embeddings.
Before enabling asymmetric embeddings for any model, check the model's current
model card or provider documentation. Do not infer the right behavior from the
API binding alone: an `openai`-compatible endpoint can serve instruction-free
models, prefix-based models, or provider-specific models behind the same API
shape.
## Reindexing Requirement
Changing asymmetric embedding settings changes the vectors produced for stored
documents and for future queries. After enabling, disabling, or changing any of
these settings, clear the existing LightRAG data for the workspace and re-index
the source files:
- `EMBEDDING_ASYMMETRIC`
- `EMBEDDING_QUERY_PREFIX`
- `EMBEDDING_DOCUMENT_PREFIX`
- Provider task behavior such as Jina `task`, Gemini `task_type`, or VoyageAI
`input_type`
Do not reuse an existing vector store across asymmetric embedding configuration
changes. Mixing vectors generated with different query/document behavior can
make retrieval quality unpredictable.
## Binding Types
LightRAG distinguishes two asymmetric embedding styles:
| Style | Bindings | How asymmetric behavior is applied |
| --- | --- | --- |
| Provider task parameters | `jina`, `gemini`, `voyageai` | LightRAG passes query/document context to the provider-specific `task`, `task_type`, or `input_type` parameter. |
| Text task prefixes | `openai`, `azure_openai`, `ollama` | LightRAG prepends configured text prefixes before calling the embedding API. Use this only when the model card explicitly requires separate query/document prefixes. |
Other server embedding bindings do not currently support
`EMBEDDING_ASYMMETRIC=true`.
## Default: Symmetric Embeddings
When `EMBEDDING_ASYMMETRIC` is unset, LightRAG does not enable asymmetric
embedding behavior, even if prefix variables exist:
```env
# EMBEDDING_ASYMMETRIC is unset
# EMBEDDING_QUERY_PREFIX="search_query: "
# EMBEDDING_DOCUMENT_PREFIX="search_document: "
```
The prefixes are ignored and a warning is logged.
The same is true when the flag is explicitly false:
```env
EMBEDDING_ASYMMETRIC=false
```
## Instruction-Free Models: Keep Symmetric
Some embedding models are instruction-free, sometimes described as using
implicit intent. They are trained to handle query/document matching from the raw
text itself and do not require query/document prefixes or provider task
parameters. For these models, do not set `EMBEDDING_ASYMMETRIC=true`; leave it
unset or set it to `false`, and do not configure `EMBEDDING_QUERY_PREFIX` or
`EMBEDDING_DOCUMENT_PREFIX`.
Common examples that should normally stay in symmetric mode:
| Model family | Example model IDs | Notes |
| --- | --- | --- |
| BGE-M3 | `BAAI/bge-m3` | Use plain text input. Do not add `search_query:` / `search_document:` unless the specific serving wrapper's model card says otherwise. |
| OpenAI Text Embedding 3 | `text-embedding-3-small`, `text-embedding-3-large` | The OpenAI embeddings API uses text input plus the model name; it does not expose a query/document task parameter. |
| Mistral Embed | `mistral-embed` | Use the provider's plain embedding input. Do not invent task prefixes. |
| Alibaba GTE base models | `gte-large`, `gte-large-zh` | Base GTE models use plain text for normal retrieval. This does not apply to newer `instruct` variants such as `gte-Qwen2-1.5B-instruct`; check that model card. |
| Jina Embeddings v2 | `jina-embeddings-v2-base-en`, `jina-embeddings-v2-base-zh` | Jina v2 is plain-text input. Jina v3/v4 are different and use the `task` parameter for retrieval tasks. |
If a model is instruction-free, enabling LightRAG's asymmetric mode can make the
input different from what the model was trained or documented to expect. That can
reduce retrieval quality even though the server starts successfully.
## Provider Task Parameter Bindings
Use this mode for providers that expose separate query/document embedding tasks.
Do not configure prefix variables for these bindings.
Jina example:
```env
EMBEDDING_BINDING=jina
EMBEDDING_ASYMMETRIC=true
EMBEDDING_MODEL=jina-embeddings-v4
```
Gemini example:
```env
EMBEDDING_BINDING=gemini
EMBEDDING_ASYMMETRIC=true
EMBEDDING_MODEL=gemini-embedding-001
```
VoyageAI example:
```env
EMBEDDING_BINDING=voyageai
EMBEDDING_ASYMMETRIC=true
EMBEDDING_MODEL=voyage-3
```
If `EMBEDDING_QUERY_PREFIX` or `EMBEDDING_DOCUMENT_PREFIX` is also configured
for these bindings, LightRAG logs a warning and ignores the prefixes.
## Text Task Prefix Bindings
Use this mode for embedding models that expect task instructions in the input
text, such as models whose card documents prefixes like `search_query:`,
`search_document:`, `query:`, or `passage:`. Do not enable this mode just
because the model is served through `openai`, `azure_openai`, or `ollama`.
Both prefix variables must be explicitly configured:
```env
EMBEDDING_ASYMMETRIC=true
EMBEDDING_QUERY_PREFIX="search_query: "
EMBEDDING_DOCUMENT_PREFIX="search_document: "
```
If one side should intentionally have no prefix, use the sentinel `NO_PREFIX`:
```env
EMBEDDING_ASYMMETRIC=true
EMBEDDING_QUERY_PREFIX="search_query: "
EMBEDDING_DOCUMENT_PREFIX=NO_PREFIX
```
`NO_PREFIX` is converted to an empty string internally. It is different from an
unset variable: it means the side was reviewed and intentionally left without a
prefix.
At least one side must have a non-empty prefix. This is invalid:
```env
EMBEDDING_ASYMMETRIC=true
EMBEDDING_QUERY_PREFIX=NO_PREFIX
EMBEDDING_DOCUMENT_PREFIX=NO_PREFIX
```
## Invalid Empty Prefixes
Do not use an empty environment value for an intentional empty prefix:
```env
EMBEDDING_DOCUMENT_PREFIX=
```
Use `NO_PREFIX` instead. Empty values are rejected because shell, `.env`, and
Docker Compose handling can make empty strings indistinguishable from accidental
missing configuration.
## Validation Summary
| Configuration | Result |
| --- | --- |
| `EMBEDDING_ASYMMETRIC` unset | Symmetric mode; prefixes ignored with a warning. |
| `EMBEDDING_ASYMMETRIC=false` | Symmetric mode; prefixes ignored with a warning. |
| Instruction-free model such as `BAAI/bge-m3`, `text-embedding-3-small`, `mistral-embed`, base GTE, or Jina v2 | Keep symmetric mode; do not configure prefixes or provider tasks unless the model card says to. |
| `EMBEDDING_ASYMMETRIC=true` with `jina`/`gemini`/`voyageai` | Provider task mode; prefixes ignored with a warning. |
| `EMBEDDING_ASYMMETRIC=true` with `openai`/`azure_openai`/`ollama` and both prefix variables configured | Prefix mode. |
| Prefix mode with a missing prefix variable | Startup error; use a real prefix or `NO_PREFIX`. |
| Prefix mode with both sides `NO_PREFIX` | Startup error; no asymmetric behavior would occur. |
| Prefix variable set to an empty value | Startup error; use `NO_PREFIX`. |
Any valid change from one asymmetric embedding configuration to another still
requires clearing the workspace data and re-indexing the source files.
+357
View File
@@ -0,0 +1,357 @@
# LightRAG Docker Deployment
A lightweight Knowledge Graph Retrieval-Augmented Generation system with multiple LLM backend support.
## 🚀 Preparation
### Clone the repository:
```bash
# Linux/MacOS
git clone https://github.com/HKUDS/LightRAG.git
cd LightRAG
```
```powershell
# Windows PowerShell
git clone https://github.com/HKUDS/LightRAG.git
cd LightRAG
```
### Configure your environment:
```bash
# Linux/MacOS
cp env.example .env
# Edit .env with your preferred configuration
```
```powershell
# Windows PowerShell
Copy-Item env.example .env
# Edit .env with your preferred configuration
```
LightRAG can be configured using environment variables in the `.env` file:
**Server Configuration**
- `HOST`: Server host (default: 0.0.0.0)
- `PORT`: Server port (default: 9621)
**LLM Configuration**
- `LLM_BINDING`: LLM backend to use (lollms/ollama/openai)
- `LLM_BINDING_HOST`: LLM server host URL
- `LLM_MODEL`: Model name to use
**Embedding Configuration**
- `EMBEDDING_BINDING`: Embedding backend (lollms/ollama/openai)
- `EMBEDDING_BINDING_HOST`: Embedding server host URL
- `EMBEDDING_MODEL`: Embedding model name
- `EMBEDDING_ASYMMETRIC`: Explicitly enable query/document asymmetric embeddings
- `EMBEDDING_DOCUMENT_PREFIX`: Document prefix for prefix-based asymmetric embeddings (or `NO_PREFIX`)
- `EMBEDDING_QUERY_PREFIX`: Query prefix for prefix-based asymmetric embeddings (or `NO_PREFIX`)
See [Asymmetric Embedding Configuration](./AsymmetricEmbedding.md) for prefix
validation rules and provider-specific behavior.
**RAG Configuration**
- `MAX_ASYNC_LLM`: Maximum async operations (deprecated alias: `MAX_ASYNC`)
- `MAX_TOKENS`: Maximum token size
- `EMBEDDING_DIM`: Embedding dimensions
## 🐳 Docker Deployment
Docker instructions work the same on all platforms with Docker Desktop installed.
### Build Optimization
The Dockerfile uses BuildKit cache mounts to significantly improve build performance:
- **Automatic cache management**: BuildKit is automatically enabled via `# syntax=docker/dockerfile:1` directive
- **Faster rebuilds**: Only downloads changed dependencies when `uv.lock` or `bun.lock` files are modified
- **Efficient package caching**: UV and Bun package downloads are cached across builds
- **No manual configuration needed**: Works out of the box in Docker Compose and GitHub Actions
### Start LightRAG server:
```bash
docker compose up -d
```
If you used the interactive setup, start the generated stack with:
```bash
docker compose -f docker-compose.final.yml up -d
```
The interactive setup keeps `.env` host-usable. Container-only hostnames such as `postgres` or `host.docker.internal`, along with staged SSL paths under `/app/data/certs/`, are injected into the generated `docker-compose.final.yml` for the `lightrag` service instead of being persisted back into `.env`.
On reruns, unchanged wizard-managed service blocks in `docker-compose.final.yml` are preserved by
default. To repair or fully regenerate those managed blocks from the bundled templates, rerun the
matching setup target with `make env-base-rewrite` or `make env-storage-rewrite`.
If the generated stack includes local Milvus, compose resolves `MINIO_ACCESS_KEY_ID` and
`MINIO_SECRET_ACCESS_KEY` at startup from the repo `.env` or exported shell environment. The
generated compose file does not snapshot those values, and `docker compose` exits immediately if
either variable is missing.
Before exposing the generated stack beyond localhost, run:
```bash
make env-security-check
```
That command audits the current `.env` for missing authentication, unsafe whitelist settings, weak
JWT secrets, and other setup-level security risks without rewriting any files.
LightRAG Server uses the following paths for data storage:
```
data/
├── rag_storage/ # RAG data persistence
└── inputs/ # Input documents
```
### Container security (non-root)
The official images run the server as a non-root user (`lightrag`, uid/gid `1000`) to satisfy CIS Docker Benchmark 4.1. The behavior is designed so existing deployments keep working on upgrade:
- The container starts as root, takes ownership of the writable data directories, then drops to uid 1000 via `gosu`. This means **existing root-owned bind-mount / PVC data is adopted automatically** — no manual `chown` is needed when upgrading from an older image.
- Ownership is fixed for `/app/data` **and** any custom locations you set via `WORKING_DIR`, `INPUT_DIR`, `PROMPT_DIR`, or `TIKTOKEN_CACHE_DIR`, so pointing data outside `/app/data` still works.
- If you instead start the container with an explicit non-root user (Compose `user: "1000:1000"` or Kubernetes `runAsUser: 1000`), the startup `chown` is skipped — make sure the mounted directories are already owned by that uid.
- `.env` is **not** chowned, so the host keeps ownership and you can edit it freely. It only needs to be *readable* by uid 1000, which the default `0644` permission satisfies. A `.env` mounted read-only as `0600`/`0400` owned by a different uid will fail to load (clear `PermissionError` at startup); make it readable by uid 1000, or supply configuration via environment variables instead (`env_file:` / `environment:`, or k8s `env` / `envFrom`).
Passing server flags still works as before, e.g. `docker run ghcr.io/hkuds/lightrag:latest --port 9622`.
> Note: the image starts as root and drops privileges at runtime (the pattern used by the official PostgreSQL/Redis images), so the *running process* is non-root while the image's configured `USER` is still root. Scanners that judge CIS 4.1 purely from the `USER` directive may still flag the image even though no server process runs as root.
### Optional: local vLLM embedding and reranker
To run embedding and/or reranking locally with vLLM, run `make env-base` and answer `yes` when prompted to run the embedding model and rerank service locally via Docker.
That configures the embedding service to use `BAAI/bge-m3` on port 8001 with a local vLLM server, and can also add a `vllm-rerank` service on port 8000.
Alternatively, rerun `make env-base` later and enable only the rerank Docker prompt to add the `vllm-rerank` service automatically.
vLLM provides a `v1/rerank` endpoint that works with the `cohere` binding.
Example `docker-compose.override.yml` for GPU hosts (embedding + reranker):
```yaml
services:
vllm-embed:
image: vllm/vllm-openai:latest
runtime: nvidia
command: >
--model BAAI/bge-m3
--port 8001
--dtype float16
ports:
- "8001:8001"
volumes:
- ./data/hf-cache:/root/.cache/huggingface
ipc: host
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: all
capabilities: [gpu]
vllm-rerank:
image: vllm/vllm-openai:latest
runtime: nvidia
command: >
--model BAAI/bge-reranker-v2-m3
--port 8000
--dtype float16
ports:
- "8000:8000"
volumes:
- ./data/hf-cache:/root/.cache/huggingface
ipc: host
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: all
capabilities: [gpu]
```
For CPU-only hosts, use the official CPU image instead:
```yaml
services:
vllm-embed:
image: vllm/vllm-openai-cpu:latest
command: >
--model BAAI/bge-m3
--port 8001
--dtype float32
ports:
- "8001:8001"
volumes:
- ./data/hf-cache:/root/.cache/huggingface
vllm-rerank:
image: vllm/vllm-openai-cpu:latest
command: >
--model BAAI/bge-reranker-v2-m3
--port 8000
--dtype float32
ports:
- "8000:8000"
volumes:
- ./data/hf-cache:/root/.cache/huggingface
```
Add the embedding and rerank config to `.env`:
```bash
EMBEDDING_BINDING=openai
EMBEDDING_MODEL=BAAI/bge-m3
EMBEDDING_DIM=1024
EMBEDDING_BINDING_HOST=http://localhost:8001/v1
EMBEDDING_BINDING_API_KEY=local-key
VLLM_EMBED_DEVICE=cpu
RERANK_BINDING=cohere
RERANK_MODEL=BAAI/bge-reranker-v2-m3
RERANK_BINDING_HOST=http://localhost:8000/rerank
RERANK_BINDING_API_KEY=local-key
VLLM_RERANK_DEVICE=cpu
```
If LightRAG runs in Docker while vLLM runs on the host, the generated compose file rewrites those endpoints to:
```bash
EMBEDDING_BINDING_HOST=http://host.docker.internal:8001/v1
RERANK_BINDING_HOST=http://host.docker.internal:8000/rerank
```
For GPU, set:
```bash
VLLM_EMBED_DEVICE=cuda
VLLM_RERANK_DEVICE=cuda
```
Ensure the NVIDIA Container Toolkit is installed and the host has CUDA drivers available.
The setup wizard uses the CPU image by default for `cpu` device and the GPU image for `cuda` device.
When rerunning `make env-base`, an existing `VLLM_EMBED_DEVICE` / `VLLM_RERANK_DEVICE` value is
preserved instead of being overwritten by a fresh GPU auto-detection result.
Those templates already pin the matching vLLM `--dtype` (`float32` on CPU, `float16` on CUDA), so no separate `VLLM_*_DTYPE` environment variables are needed.
### SSL certificates
The setup wizard stages TLS certificate files under `./data/certs/` before generating the compose file.
This keeps generated host mounts under the same `./data` root used by the default Docker deployment.
### PostgreSQL image
The interactive setup defaults PostgreSQL to `gzdaniel/postgres-for-rag:pg18-age-pgvector`. This image bundles both Apache AGE and pgvector so the generated stack works with `PGGraphStorage` and `PGVectorStorage` without extra extension setup.
The image no longer ships fixed credentials; on first start it creates the user, password, and database from the `POSTGRES_USER` / `POSTGRES_PASSWORD` / `POSTGRES_DB` environment variables. The setup wizard prompts for these values (defaulting to `rag` / `rag` / `lightrag`) and injects them into the generated `docker-compose.final.yml`, so you can choose any user, password, and database name.
**Important Note**: If PGGraphStorage is not required for vector storage, you may replace the upper docker image with the latest official pgvector image `pgvector/pgvector:pg18`. Please note that data file formats are incompatible across different PostgreSQL major versions; once this Docker image is deployed, it cannot be rolled back to a previous version.
#### Build the PostgreSQL image
The default PostgreSQL image can be rebuilt from the repository root with `Dockerfile.postgres`. The Dockerfile starts from `pgvector/pgvector:pg18-trixie`, builds Apache AGE for PostgreSQL 18, and installs an init script that creates both the `vector` and `age` extensions for new databases.
For local development or single-host deployment:
```bash
docker build -f Dockerfile.postgres \
-t gzdaniel/postgres-for-rag:pg18-age-pgvector \
.
```
To publish the image to your own registry, use a private tag:
```bash
docker build -f Dockerfile.postgres \
-t registry.example.com/postgres-for-rag:pg18-age-pgvector \
.
docker push registry.example.com/postgres-for-rag:pg18-age-pgvector
```
For a multi-architecture image:
```bash
docker buildx build \
--platform linux/amd64,linux/arm64 \
-f Dockerfile.postgres \
-t registry.example.com/postgres-for-rag:pg18-age-pgvector \
--push \
.
```
After building a custom tag, update the `postgres.image` value in the generated `docker-compose.final.yml` to point at that tag. On later setup wizard reruns, existing wizard-managed service images are preserved.
### Updates
To update the Docker container:
```bash
docker compose pull
docker compose down
docker compose up
```
### Offline deployment
Software packages requiring `transformers`, `torch`, or `cuda` are not preinstalled in the docker images. Consequently, document extraction tools such as Docling, as well as local LLM models like Hugging Face and LMDeploy, cannot be used in an offline environment. These high-compute-resource-demanding services should not be integrated into LightRAG. Docling will be decoupled and deployed as a standalone service.
## 📦 Build Docker Images
### For local development and testing
```bash
# Build and run with Docker Compose (BuildKit automatically enabled)
docker compose up --build
# Or explicitly enable BuildKit if needed
DOCKER_BUILDKIT=1 docker compose up --build
```
**Note**: BuildKit is automatically enabled by the `# syntax=docker/dockerfile:1` directive in the Dockerfile, ensuring optimal caching performance.
### For production release
**multi-architecture build and push**:
```bash
# Use the provided build script
./docker-build-push.sh
```
**The build script will**:
- Check Docker registry login status
- Create/use buildx builder automatically
- Build for both AMD64 and ARM64 architectures
- Push to GitHub Container Registry (ghcr.io)
- Verify the multi-architecture manifest
**Prerequisites**:
Before building multi-architecture images, ensure you have:
- Docker 20.10+ with Buildx support
- Sufficient disk space (20GB+ recommended for offline image)
- Registry access credentials (if pushing images)
### Verify official GHCR images with Cosign
Official LightRAG images published to GitHub Container Registry by GitHub Actions are signed with Sigstore Cosign using GitHub OIDC keyless signing.
Install `cosign`, then verify the image tag you want to run:
```bash
cosign verify ghcr.io/HKUDS/LightRAG:<tag> \
--certificate-identity-regexp '^https://github.com/HKUDS/LightRAG/.github/workflows/(docker-publish|docker-build-manual|docker-build-lite)\.yml@refs/.+$' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
```
Replace `<tag>` with the version tag you want to validate, for example a release tag, `latest`, `<tag>-lite`, or `lite`.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+216
View File
@@ -0,0 +1,216 @@
# Frontend Build Guide
## Overview
The LightRAG project includes a React-based WebUI frontend. This guide explains how frontend building works in different scenarios.
## Key Principle
- **Git Repository**: Frontend build results are **NOT** included (kept clean)
- **PyPI Package**: Frontend build results **ARE** included (ready to use)
- **Build Tool**: **Bun** is recommended, but **Node.js/npm** is fully supported as a fallback
## Installation Scenarios
### 1. End Users (From PyPI) ✨
**Command:**
```bash
pip install lightrag-hku[api]
```
**What happens:**
- Frontend is already built and included in the package
- No additional steps needed
- Web interface works immediately
---
### 2. Development Mode (Recommended for Contributors) 🔧
**Command:**
```bash
# Clone the repository
git clone https://github.com/HKUDS/LightRAG.git
cd LightRAG
# Install in editable mode (no frontend build required yet)
pip install -e ".[api]"
# Build frontend when needed (can be done anytime)
cd lightrag_webui
bun install --frozen-lockfile
bun run build
cd ..
```
**Advantages:**
- Install first, build later (flexible workflow)
- Changes take effect immediately (symlink mode)
- Frontend can be rebuilt anytime without reinstalling
**How it works:**
- Creates symlinks to source directory
- Frontend build output goes to `lightrag/api/webui/`
- Changes are immediately visible in installed package
---
### 3. Normal Installation (Testing Package Build) 📦
**Command:**
```bash
# Clone the repository
git clone https://github.com/HKUDS/LightRAG.git
cd LightRAG
# ⚠️ MUST build frontend FIRST
cd lightrag_webui
bun install --frozen-lockfile
bun run build
cd ..
# Now install
pip install ".[api]"
```
**What happens:**
- Frontend files are **copied** to site-packages
- Post-build modifications won't affect installed package
- Requires rebuild + reinstall to update
**When to use:**
- Testing complete installation process
- Verifying package configuration
- Simulating PyPI user experience
---
### 4. Creating Distribution Package 🚀
**Command:**
```bash
# Build frontend first
cd lightrag_webui
bun install --frozen-lockfile --production
bun run build
cd ..
# Create distribution packages
python -m build
# Output: dist/lightrag_hku-*.whl and dist/lightrag_hku-*.tar.gz
```
**What happens:**
- `setup.py` checks if frontend is built
- If missing, installation fails with helpful error message
- Generated package includes all frontend files
---
## GitHub Actions (Automated Release)
When creating a release on GitHub:
1. **Automatically builds frontend** using Bun
2. **Verifies** build completed successfully
3. **Creates Python package** with frontend included
4. **Publishes to PyPI** using existing trusted publisher setup
**No manual intervention required!**
---
## Quick Reference
| Scenario | Command | Frontend Required | Can Build After |
|----------|---------|-------------------|-----------------|
| From PyPI | `pip install lightrag-hku[api]` | Included | No (already installed) |
| Development | `pip install -e ".[api]"` | No | ✅ Yes (anytime) |
| Normal Install | `pip install ".[api]"` | ✅ Yes (before) | No (must reinstall) |
| Create Package | `python -m build` | ✅ Yes (before) | N/A |
---
## Bun Installation
If you don't have Bun installed:
```bash
# macOS/Linux
curl -fsSL https://bun.sh/install | bash
# Windows
powershell -c "irm bun.sh/install.ps1 | iex"
```
Official documentation: https://bun.sh
---
## File Structure
```
LightRAG/
├── lightrag_webui/ # Frontend source code
│ ├── src/ # React components
│ ├── package.json # Dependencies
│ └── vite.config.ts # Build configuration
│ └── outDir: ../lightrag/api/webui # Build output
├── lightrag/
│ └── api/
│ └── webui/ # Frontend build output (gitignored)
│ ├── index.html # Built files (after running bun run build)
│ └── assets/ # Built assets
├── setup.py # Build checks
├── pyproject.toml # Package configuration
└── .gitignore # Excludes lightrag/api/webui/* (except .gitkeep)
```
---
## Troubleshooting
### Q: I installed in development mode but the web interface doesn't work
**A:** Build the frontend:
```bash
cd lightrag_webui && bun run build
```
### Q: I built the frontend but it's not in my installed package
**A:** You probably used `pip install .` after building. Either:
- Use `pip install -e ".[api]"` for development
- Or reinstall: `pip uninstall lightrag-hku && pip install ".[api]"`
### Q: Where are the built frontend files?
**A:** In `lightrag/api/webui/` after running `bun run build`
### Q: Can I use npm or yarn instead of Bun?
**A:** Yes. The build scripts (`dev`, `build`, `preview`, `lint`) are runtime-agnostic and work with both Bun and Node.js/npm:
```bash
npm install
npm run build
```
Bun is recommended for speed, but npm is fully supported. Tests (`bun test`) still require Bun.
### Q: Build fails with `Cannot find package '@/lib'`
**A:** This was caused by `vite.config.ts` using a TypeScript path alias (`@/`) that only Bun could resolve at config load time. Update to the latest version where this is fixed with a relative import.
---
## Summary
**PyPI users**: No action needed, frontend included
**Developers**: Use `pip install -e ".[api]"`, build frontend when needed
**CI/CD**: Automatic build in GitHub Actions
**Git**: Frontend build output never committed
For questions or issues, please open a GitHub issue.
+303
View File
@@ -0,0 +1,303 @@
# Interactive Setup Guide
Use the interactive setup wizard when you want LightRAG to guide you through the configuration instead of editing `.env` by hand.
The wizard is exposed through `make` targets:
- `make env-base`
- `make env-storage`
- `make env-server`
- `make env-validate`
- `make env-security-check`
- `make env-backup`
- `make env-base-rewrite`
- `make env-storage-rewrite`
You do not need to call the underlying shell script directly.
## What This Wizard Is For
The setup wizard helps you configure LightRAG in three parts:
- `env-base` sets up the LLM, embedding model, and optional reranker.
- `env-storage` adds or changes storage backends such as PostgreSQL, Neo4j, Redis, Milvus, Qdrant, MongoDB, or Memgraph.
- `env-server` sets server host and port, WebUI labels, authentication, API keys, and SSL.
You can rerun each step later. The wizard loads your existing `.env` and shows current values as defaults, so you only need to change what is different.
## Before You Start
- Run commands from the repository root.
- The `make env-*` targets automatically choose a compatible Bash 4+ interpreter.
- Use the documented `make env-*` targets rather than invoking the setup script yourself.
- `make env-base` is the normal starting point because it creates the initial `.env`.
- `make env-storage` and `make env-server` require an existing `.env`.
- If you choose any wizard-managed Docker service, the wizard also prepares LightRAG for the Docker startup path.
## Choose Your Setup Path
Use this quick guide to decide what to run:
- I want the fastest first run with remote model providers: `make env-base`
- I want embedding or reranking to run locally in Docker: `make env-base`
- I already configured models and now want databases: `make env-storage`
- I already configured models and now want auth, API keys, or SSL: `make env-server`
- I want to check whether my current setup is valid: `make env-validate`
- I want to audit my current setup before exposing it: `make env-security-check`
- I want a standalone backup without changing configuration: `make env-backup`
- I need to repair the generated compose services from the bundled templates: `make env-base-rewrite` or `make env-storage-rewrite`
## Scenario 1: First-Time Local Setup
Use this when you want LightRAG running with the least amount of setup and you already have remote model endpoints or API keys.
**Command**
```bash
make env-base
```
**What the wizard asks**
- LLM provider, model, endpoint, and API key
- Whether the embedding model should run locally via Docker
- If embedding stays remote: embedding provider, model, dimension, endpoint, and API key
- Whether reranking should be enabled
- If reranking is enabled: whether the rerank service should run locally via Docker
- If reranking stays remote: rerank provider, model, endpoint, and API key
**What gets written**
- `.env`
- `docker-compose.final.yml` only if you enabled wizard-managed Docker services
**What to do next**
- If you did not enable wizard-managed Docker services:
```bash
lightrag-server
```
- If you enabled wizard-managed Docker services:
```bash
docker compose -f docker-compose.final.yml up -d
```
## Scenario 2: Local Setup With Docker-Hosted Embedding or Rerank
Use this when you want LightRAG to run local inference services for embedding and/or reranking through Docker.
**Command**
```bash
make env-base
```
**Recommended answers**
- Answer `yes` to `Run embedding model locally via Docker (vLLM)?` if you want local embeddings
- Answer `yes` to `Enable reranking?` and then `yes` to `Run rerank service locally via Docker?` if you want local reranking
**What the wizard asks after you enable local services**
- Embedding model name for local vLLM
- Rerank model name for local vLLM
- Remote LLM details if your main LLM is still external
**What gets written**
- `.env`
- `docker-compose.final.yml` with the selected local services
**What to do next**
```bash
docker compose -f docker-compose.final.yml up -d
```
This starts the generated Docker-based LightRAG stack together with the selected local services.
## Scenario 3: Add Storage After The Base Setup
Use this when you already have `.env` from `make env-base` and now want to switch from default local-file storage to database-backed storage.
**Command**
```bash
make env-storage
```
**Prerequisite**
- `.env` must already exist
**What the wizard asks**
- KV storage backend
- Vector storage backend
- Graph storage backend
- Doc-status storage backend
- For each required database, whether it should run locally via Docker
- For each required database, the needed connection details such as host, URI, port, user, password, database name, or device type
**Important rule**
- `MongoVectorDBStorage` requires Atlas Search / Vector Search support.
- If you choose the wizard-managed Docker MongoDB service, the wizard now provisions MongoDB Atlas Local, so `MongoVectorDBStorage` can run against the local Docker deployment. The generated host-side `MONGO_URI` uses `?directConnection=true`.
- If you do not use the wizard-managed Docker MongoDB service, provide an external Atlas-capable MongoDB endpoint for `MONGO_URI`, such as a `mongodb+srv://` Atlas cluster URI or an Atlas Local `mongodb://...?...directConnection=true` URI.
- For external `mongodb://...?...directConnection=true` URIs, the wizard can only validate the URI format. It cannot determine statically whether the target deployment actually provides Atlas Search / Vector Search support.
**What gets written**
- `.env`
- `docker-compose.final.yml` if you selected wizard-managed storage services
**What to do next**
- If you selected Docker-managed storage services:
```bash
docker compose -f docker-compose.final.yml up -d
```
- If you pointed LightRAG at external databases, make sure those services are reachable before starting LightRAG.
## Scenario 4: Harden A Deployment With Auth And SSL
Use this when you already have `.env` and need to prepare the server for shared or external use.
**Commands**
```bash
make env-server
make env-security-check
```
**Prerequisite**
- `.env` must already exist
**What `env-server` asks**
- Server host and port
- WebUI title and description
- Summary language
- Whether to configure authentication and API key settings
- Auth accounts, JWT secret, token lifetime, API key, and whitelist paths
- Whether to enable SSL/TLS
- SSL certificate file path and SSL key file path
**What gets written**
- `.env`
- `docker-compose.final.yml` may be updated if your current setup already uses wizard-managed Docker services
**What to do next**
- Run `make env-security-check`
- If the stack uses Docker, recreate the LightRAG service with your compose file
- If the stack runs on the host, restart `lightrag-server`
For broader deployment guidance, see [DockerDeployment.md](./DockerDeployment.md).
## Validate, Audit, And Backup
These commands do not walk you through a full setup flow, but they are part of normal operations.
### Validate The Current Configuration
```bash
make env-validate
```
Use this when you want to confirm that the current `.env` is internally consistent. It reports problems such as missing required values, malformed auth settings, invalid URIs, invalid ports, or missing SSL files.
### Audit Security Before Exposure
```bash
make env-security-check
```
Use this before exposing LightRAG beyond localhost. It reports risky setups such as missing authentication, weak or missing JWT secrets, unsafe whitelist settings, or unresolved sensitive placeholders.
### Create A Standalone Backup
```bash
make env-backup
```
Use this when you want a manual backup without running any setup flow.
## Outputs And What They Mean
### `.env`
The wizard writes `.env` in the repository root. This file becomes the current runtime configuration produced by the latest wizard run.
In practice, this means:
- rerunning the wizard updates `.env`
- existing values are reused as defaults on later runs
- you should treat `.env` as the active configuration for the workflow you most recently configured
- before `env-base`, `env-storage`, or `env-server` writes `.env`, the wizard automatically creates a timestamped backup of the existing file when one is present
### `docker-compose.final.yml`
The wizard creates or updates `docker-compose.final.yml` only when you choose wizard-managed Docker services or when an existing wizard-generated compose setup needs to stay aligned with new server settings.
When one of the setup flows is about to replace or remove an existing generated compose file, it automatically creates a timestamped backup first.
For MongoDB-backed storage, the wizard-managed Docker path uses MongoDB Atlas Local rather than MongoDB Community Edition so local Atlas Search / Vector Search workflows are available.
Use this file when starting the generated Docker stack:
```bash
docker compose -f docker-compose.final.yml up -d
```
The base `docker-compose.yml` remains the general project compose file. The generated `docker-compose.final.yml` is the wizard-managed output.
## Troubleshooting And Advanced Notes
- If `make env-storage` or `make env-server` says `.env` is missing, run `make env-base` first.
- You do not need to run `make env-backup` before rerunning `env-base`, `env-storage`, or `env-server`; those flows already back up the existing `.env`, and they also back up the generated compose file before changing it.
- If you need to fully rebuild wizard-managed compose services from the current bundled templates, use `make env-base-rewrite` or `make env-storage-rewrite`.
- If you switch between host-oriented and Docker-oriented workflows, rerun the relevant setup step instead of trying to manually merge old settings.
- If the generated stack includes local Milvus, make sure `MINIO_ACCESS_KEY_ID` and `MINIO_SECRET_ACCESS_KEY` are available before running `docker compose -f docker-compose.final.yml up -d`.
- For Docker deployment details beyond the interactive wizard, see [DockerDeployment.md](./DockerDeployment.md).
## Typical Command Sequences
### Remote models, local server
```bash
make env-base
lightrag-server
```
### Remote LLM, local embedding and rerank in Docker
```bash
make env-base
docker compose -f docker-compose.final.yml up -d
```
### Add storage after the base setup
```bash
make env-base
make env-storage
docker compose -f docker-compose.final.yml up -d
```
### Add security and SSL before exposure
```bash
make env-base
make env-storage
make env-server
make env-security-check
docker compose -f docker-compose.final.yml up -d
```
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 374 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 357 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 530 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 381 KiB

File diff suppressed because it is too large Load Diff
+402
View File
@@ -0,0 +1,402 @@
# LightRAG Sidecar 文件格式说明
本文介绍内解析引擎输出的**LightRAG Sidecar**文件格式。LightRAG 在使用native/mineru/docling这些支持多模态内容解析引擎提取文件内容的时候,会把"正文 + 多模态对象 + 解析元数据"拆开写到一个 `*.parsed/` 目录中,目录内的每个 JSON / JSONL 文件统称为 **sidecar** 文件。Sidecar 是后续流水线(多模态分析 → 多模态 chunk 构造 → 实体抽取 → 文档删除时的缓存清理)唯一可靠的依据。Sidecar的文件格式是LightRAG内置的通用文件交换格式,新的多模态内容提取引擎都需要遵循这个格式。公开**LightRAG Sidecar**文件格式的目的是给社区开发者编写字节的内容解析引擎提供方便。
## 一、概述
| 关注点 | 文件 | 存放内容 | 说明 |
|---|---|---|---|
| 主文件 | `<doc>.blocks.jsonl` | 存放 Block 正文 | 所有 Block 的 content字段内容拼接后形成完整的原文 |
| 图形对象 | `<doc>.drawings.json` | 文件中抽取出来的图形对象 | 送VLM进行分析后回填分析结果 |
| 表格对象 | `<doc>.tables.json` | 文件中抽取出来的表格对象 | 送LLM进行分析后回填分析结果 |
| 公式对象 | `<doc>.equations.json` | 文件中抽取出来的公司对象 | 送LLM进行分析后回填分析结果 |
| 原始图像资源 | `<doc>.blocks.assets/` | 文件中抽取出来的图片原始文件 | 送VLM进行图片分析 |
Sidecar 的设计意图:
- 解析阶段 内容提取引擎(native/mineru/docling) **只**负责生成 `blockid / heading / content / surrounding` 等"客观"字段;
- 多模态分析阶段 (`analyze_multimodal`) 由 LightRAG 写入分析结果 `llm_analyze_result` 字典,可能是首次追加,也可能覆盖已有结果;解析器不应预先填充该字段
## 二、目录布局
```
inputs/space1/__parsed__/<规范文件名>.parsed/
├── <规范文件名>.blocks.jsonl 正文块序列 + 文档级 meta(首行)
├── <规范文件名>.drawings.json 图形 sidecardict 容器,键 = 图形 id
├── <规范文件名>.tables.json 表格 sidecar
├── <规范文件名>.equations.json 公式 sidecar
└── <规范文件名>.blocks.assets/ 原始资源目录(存放drawings.json中的图片文件放这里)
├── image1.wmf
├── image2.wmf
├── image3.wmf
├── image4.png
├── image5.png
├── image6.png
└── image7.emf
```
## 三、blocks.jsonl
`blocks.jsonl` 是按行序列化的 JSON,**第一行 `type="meta"`**,其余每行是一个内容块 `type="content"`
### 3.1 meta 行实例
```json
{
"type": "meta",
"format": "lightrag",
"version": "1.0",
"document_name": "m012-manual.docx",
"document_format": "docx",
"document_hash": "sha256:4840...3f9543d9db0822d2d59",
"table_file": true,
"equation_file": true,
"drawing_file": true,
"asset_dir": true,
"blocks": 39,
"doc_id": "doc-f1bee60173d067d88595c00e7d9b0ce5",
"parse_engine": "native",
"parse_time": "2026-05-13T18:42:25.943490+00:00",
"doc_title": "m012-manual"
}
```
| 字段 | 类型 | 说明 |
|---|---|---|
| `type` | `"meta"` | 行类型,固定值,校验位 |
| `format` | `"lightrag"` | sidecar 大版本族标识 |
| `version` | `str` | sidecar schema 版本 |
| `document_name` | `str` | 规范文件名(含后缀,不含处理指示) |
| `document_format` | `str` | 文件格式(目前以文件后缀表示) |
| `document_hash` | `"sha256:<hex>"` | sidecar 正文指纹,定义为 `SHA-256(merged_text)`,其中 `merged_text` 是所有非空 content 行的 `content` 字段按 `"\n\n"` 拼接后的字符串。供外部消费者快速判断两份 `.parsed/` 是否同源(不必逐行比对 body),并作为 sidecar 文件的自描述内容校验位。注意:LightRAG 入库流水线本身不读此字段,跨文档去重由 `doc_status.content_hash` 单独承担 |
| `table_file` / `equation_file` / `drawing_file` | `bool` | 是否存在对应 sidecar 文件(为真时对应文件必然存在) |
| `asset_dir` | `bool` | 是否存在`blocks.assets`资源目录 |
| `split_option` | `object` | 可选。解析引擎自我记录的元数据(如 `engine_version` 及引擎特定附加信息);引擎无任何记录时(native/markdown 常见情形)整个字段省略。分块不在解析阶段进行(由下游 chunker 负责),故此字段从不含分块参数 |
| `blocks` | `int` | content 行数(不含 meta |
| `doc_id` | `"doc-<md5>"` | 文档全局 id。sidecar item id`im-/tb-/eq-`)使用 `doc_id` 去掉 `doc-` 前缀后的哈希部分,以缩短嵌入正文中的占位标签 |
| `parse_engine` | `str` | 解析引擎`native/mineru/docling/legacy` |
| `parse_time` | `str` | 解析完成时间; 格式:ISO-8601 UTC |
| `doc_title` | `str` | 文档标题(通常为首个 H1);可选 |
| `doc_summary` | `str` | 文档摘要;可选 |
| `doc_attributes` | `object` | 文章扩展属性对象;可选 |
| `bbox_attributes` | `object` | bbox possition全局属性;详见[§八](八、positions) |
> LightRAG要求同一个workspace(知识库)内的文件名(document_name)必须唯一。
### 3.2 content 行
每个 content 行是一个原始文档"块"的最小可寻址单位,至少包含:
```json
{
"type": "content",
"blockid": "b5b5264333943cfd623710da1c36fc93",
"format": "plain_text",
"content": "# 1 产品用途和功能\nMI012模块用于支撑供氧抗荷调节器的供氧抗荷控制功能...",
"heading": "1 产品用途和功能",
"parent_headings": [],
"level": 1,
"session_type": "body",
"table_slice": "none",
"positions": [
{
"type": "paraid",
"range": ["5EA4577A", "6555DDCB"]
}
]
}
```
| 字段 | 含义 |
|---|---|
| `type` | `"content"` |
| `blockid` | 全局唯一的Block ID |
| `format` | 内容形态,目前固定为 `"plain_text"` |
| `content` | 文本内容;**公式和图片此以占位标签出现,表格以带table标签的JSON或HTLM格式出现**(见 3.3)。标题行会按 `level` 渲染 markdown `#` 前缀(后跟一个空格):level 1 → 一个 `#`level 2 → 两个 `#`,……,封顶 6 个(level ≥ 7 的标题仍渲染为 `######`)。若源标题文字本身已以 markdown 前缀开头(1–6 个 `#` 后跟空格),则原样保留、不再重复加前缀。注意 `heading` 字段本身保持干净(不含 `#`)。 |
| `heading` | content所在章节的最高层级标题;heading真实存在时,应该以 markdown 渲染后的标题行出现在content的开头。**每个被识别出的标题都独立成块**:若标题后紧跟正文,正文与该标题合并进同一个块(content = markdown 渲染后的标题行 + 正文);若标题后没有正文(例如其后紧接下一个层级的标题),该标题仍单独成块、content 仅为 markdown 渲染后的标题行。这样可保证所有 Block 的 content 字段拼接后仍能完整、无重叠地还原原文。 |
| `parent_headings` | 字符串数组: 自顶向下的祖先标题列表,不含当前 `heading` |
| `level` | 整数: `heading` 在文档大纲中的层级(`1` = H1 / 一级标题,0表示无标题) |
| `session_type` | Block所处区域:`body` `preface` `TOC` `references` `appendix` |
| `table_slice` | 可选保留字段;表示Block是否仅包括表格片段。目前分析引擎不会拆分长表格。因此本字段固定为 `"none"`(表示表格不会被分片) |
| `table_header` | 可选保留字段;在当前块位表格片段的时候,保存识别出来的表格头。目前不存在 |
| `positions` | `position` 对象数组:标识文本块的版面位置;文本块来与版面的多个位置的时候,则会出现多个`position` 对象。参见[§八](#八、position) |
> - blockid计算方式:`md5(doc_id + ":" + block_index + ":" + heading + ":" + content)`。文档经过分块策略处理得到的 chunk 将保存 blockid 用于溯源 chunk 在s idecar 中的位置。
> - 不关系文档章节结构的分块策略 `F` `R` `V` 使用的就是 content 字段拼接后的内容进行分块。因此需要保证所有 Block 的 content字段合并在一起能够构成完整的文档内容,不会缺少内容,不会出现重叠的内容。
### 3.3 content 内嵌占位标签
为了让 P 分块策略在不破坏多模态对象的前提下对正文做切分,`content` 文本里使用如下三种 XML 风格的占位标签:
| 标签 | 含义 | 标签属性 |
|---|---|---|
| `<table id="tb-…" format="json">…</table>` | 表格占位,包体是表格原始 JSON / HTML | `id` 指向 `tables.json` 里对应 item`format``json` / `html` |
| `<drawing id="im-…" format="png" path="…" src="…" caption="…" />` | 自闭合图形占位 | `id` 指向 `drawings.json``path` 相对 `*.parsed/` 目录;`src` 是原文档里的引用名 |
| `<equation id="eq-…" format="latex" caption="…">…</equation>` | 公式占位 | 行内公式同样用 `<equation format="latex">` 但**不**带 `id`,不会进 sidecar; 仅块公式(独占一行或多行)时携带 `id` |
在实体关系抽取的时候喂给大模型的文本会把 `id / path / src` 等内部属性剥掉,但为保留键属性(`format / caption`)。目的是避免抽取出文章不可见的实体,给抽取结果注入过多的噪声。
### 3.4 blockid 与 chunk sidecar.refs 的对应
葛总分块策略在sidecar文件存在时,会在其输出的每个 chunk 都会带上 `sidecar = {"type": "block", "id": <主来源 blockid>, "refs": [{"type": "block", "id": <blockid>}, …]}`,其中:
- 未合并的 chunk → `sidecar.refs` 只有一个元素,等于该 chunk 来自的 blocks.jsonl 行的 `blockid`
- LevelMerge 合并后的 chunk → `refs` 顺序保留所有来源 `blockid`(去重);
- hard fallback split 后的子 chunk → 共享父 chunk 的 `sidecar`
这条链路是文档级追溯(chunk ↔ block ↔ 原段落 paraId)的基础。
## 四、drawings.json
顶层是 `{"version": "1.0", "drawings": { <id>: <item>, … }}` 形态的 dict 容器,**键 = `id` 字段**,便于按 id 查找。每个 item 形如:
```json
{
"id": "im-f1bee60173d067d88595c00e7d9b0ce5-0004",
"blockid": "2f52b70839d13a936d97955916820147",
"heading": "2.3 结构尺寸及重量",
"parent_headings": ["2 产品说明"],
"format": "png",
"path": "m012-manual.blocks.assets/image4.png",
"src": "",
"caption": "",
"footnotes": [],
"extras": {
"ocr_texts": "图内第一段 OCR 文本\n\n图内第二段 OCR 文本",
"ocr_texts_count": 2
},
"surrounding": {
"leading": "2.3 结构尺寸及重量\n尺寸及重量要求如下:\na) 外廓尺寸长度为:<drawing …",
"trailing": "\n图1 外廓尺寸示意\nb) 重量不大于0.85kg。\nc) 测试结果:实测电路噪声Vpp=1.526mV…"
},
"llm_analyze_result": {
"name": "产品外廓尺寸工程图纸",
"type": "Illustration",
"description": "该图纸为产品的外廓尺寸示意图,展示了一个电子设备或电源模块的三视图设计…",
"analyze_time": 1778697752,
"status": "success",
"message": ""
},
"llm_cache_list": [
"default:analysis:fcf4c4f88227ee1c1bf0ed4394039e37"
]
}
```
| 字段 | 说明 |
|---|---|
| `id` | `im-<doc_hash>-<NNNN>` 形式(`doc_hash``doc_id` 去掉 `doc-` 前缀后的 32 位 md5 |
| `blockid` | 指向产生该图形的 content 行 |
| `heading` | 所在章节标题 |
| `parent_headings` | 字符串数组:自顶向下的祖先标题列表,不含当前 `heading`(与该图形所属 block 在 `blocks.jsonl` 中的同名字段一致) |
| `format` | 原始扩展名(去点):`png` / `jpeg` / `gif` / `webp` / `wmf` / `emf` / … |
| `path` | 相对 `*.parsed/` 目录的资源路径,**永远**指向 `*.blocks.assets/` 内文件 |
| `src` | 原文档里图形的引用别名(多数情况下为空) |
| `caption` | 可见标题(解析器可能留空) |
| `footnotes` | 脚注字符串列表 |
| `surrounding` | 上下文对象:参见[§七](#七、surrounding) |
| `self_ref` | 字符串:可选;解析引擎原始输出中的对象引用(如 Docling JSON Pointer `#/pictures/3`,或 MinerU `content_list.json#/23`),用于溯源时回查原始解析产物中的对应对象(页面位置、原始结构等)。native 等不提供此字段时不输出 |
| `extras` | 对象:可选;引擎专属的旁路字段(如图片中包含的OCR文字等)。不属于 spec 校验范围,下游消费者不应依赖具体键。 |
| `llm_analyze_result` | 模态分析结果对象:详见 [§九](#九、`llm_analyze_result`) (后续会注入到多模态文本块) |
| `llm_cache_list` | 模态分析LLM缓存数组(后续会注入到多模态文本块) |
`extras` 中常见的 drawing 专属键:
| 键 | 说明 |
|---|---|
| `ocr_texts` | 字符串:可选;图形对象内部 OCR 文本,多个段落用空行(`\n\n`)拼接。仅当解析引擎显式把 OCR 文本挂在该 drawing 的 children 下时输出;caption / footnote 不进入此字段。 |
| `ocr_texts_count` | 整数:可选;写入 `ocr_texts` 的非空 OCR 段落数量。 |
**只有图形支持的 raster 格式(png / jpeg / gif / webp)才会进入 VLM 分析**;其他格式(wmf / emf / svg 等)写 `llm_analyze_result.status="skipped"`,下游不生成多模态 chunk,文档继续处理。图片大小超过环境变量`VLM_MAX_IMAGE_BYTES`规定的大小后,图片同样不会进入VLM分析。
> 图片的大小、DPI等信息统一放进 `extras` 对象;不要在 item 顶层引入未声明的字段(比如 `image` / `img_path` 等)。tables / equations 也遵循同样的 `extras` 约定。`self_ref` 是 spec 顶层声明的可选字段,不属于 extras 范围。
## 五、tables.json
顶层是 `{"version": "1.0", "tables": { <id>: <item>, ... }}` 形态的 dict 容器,**键 = `id` 字段**,便于按 id 查找。每个 item 形如:
```json
{
"id": "tb-f1bee60173d067d88595c00e7d9b0ce5-0007",
"blockid": "3f33897b5e105d254addc655f1efbf8c",
"heading": "2.4.4 温度-湿度-高度(随系统进行)",
"parent_headings": ["2 产品说明", "2.4 环境适应性"],
"dimension": [16, 8],
"format": "json",
"content": "[[\"试验步骤\", \"温度(℃)\", \"高度(m)\", \"相对湿度\", \"时间(min)\", \"辅助冷却\", \"系统电源\", \"功能、性能检查\"],…",
"caption": "",
"footnotes": [],
"table_header": "[[\"试验步骤\", \"温度(℃)\", \"高度(m)\", \"相对湿度\", \"时间(min)\", \"辅助冷却\", \"系统电源\", \"功能、性能检查\"]]"
"surrounding": {
"leading": "2.4.4 温度-湿度-高度(随系统进行)\n产品应能承受执行任务期间的温度、湿度、高度环境综合作用…",
"trailing": "\n注:以上步骤重复10个循环。a成品及附件达到温度稳定或240min,以长者为准;b成品及附件达到温度稳定或120min,以长者为准。…"
},
"llm_analyze_result": {
"name": "文档管理元数据表",
"description": "这是一份文档管理信息表,用于记录技术文档的基本元数据和版本控制信息 …",
"analyze_time": 1778697759,
"status": "success",
"message": ""
},
"llm_cache_list": [
"default:analysis:b316aacd40fdca0cb56430870bb89a62"
]
}
```
tables.json 文件的 `blockid` `heading` `parent_headings` `surrounding` `llm_analyze_result` 字段与drawings.json相同。不同或新添加的字段说明如下:
| 字段 | 说明 |
|---|---|
| `id` | `tb-<doc_hash>-<NNNN>` 形式(`doc_hash``doc_id` 去掉 `doc-` 前缀后的 32 位 md5 |
| `dimension` | 整数数组:`[num_rows, num_cols]`,包含表头行 |
| `format` | `"json"` (二维数组) 或 `"html"` (负载 `<table>…</table>` 片段,含起止标签) |
| `content` | 字符串:表格正文,按 `format` 决定结构;这是后续多模态 chunk 真正使用的字符串。 |
| `table_header` | 字符串:可选;识别出来的作为表格头的行内容 |
| `self_ref` | 可选;解析引擎原始输出中的对象引用(如 Docling JSON Pointer `#/tables/2`,或 MinerU `content_list.json#/31`),用于溯源时回查原始解析产物 |
在模态分析阶段,如果`content`字段长度超过大模型的上下文长度时,表格内容会被机械地截断后在喂给模型。
## 六、equations.json
顶层是 `{"version": "1.0", "equations": { <id>: <item>, ... }}` 形态的 dict 容器,**键 = `id` 字段**,便于按 id 查找。每个 item 形如:
```json
{
"id": "eq-f1bee60173d067d88595c00e7d9b0ce5-0001",
"blockid": "2f52b70839d13a936d97955916820147",
"heading": "2.3 结构尺寸及重量",
"parent_headings": ["2 产品说明"],
"format": "latex",
"content": "C=2\\frac{PT}{\\left( {V}_{H}^{2}{V}_{L}^{2} \\right)∗η}",
"caption": "",
"footnotes": [],
"surrounding": {
"leading": "2.3 结构尺寸及重量\n尺寸及重量要求如下:\n …",
"trailing": "\n其中P为供电异常时维持的功率28W,T为期望储能时间,V<sub>H</sub>为电容放电前…"
},
"llm_analyze_result": {
"name": "电容储能时间计算公式",
"description": "该公式用于计算在电源异常情况下维持系统正常工作所需的电容储能值 …",
"analyze_time": 1778697783,
"status": "success",
"message": "",
"equation": "C=2\\cdot\\frac{P\\cdot T}{(V_{H}^{2}-V_{L}^{2})\\cdot\\eta}"
},
"llm_cache_list": [
"default:analysis:fcf4c4f88227ee1c1bf0ed4394039e37"
]
}
```
equations.json 文件的 `blockid` `heading` `parent_headings` `surrounding` `llm_analyze_result` 字段与drawings.json相同。不同或新添加的字段说明如下:
| 字段 | 说明 |
|---|---|
| `id` | `eq-<doc_hash>-<NNNN>` 形式(`doc_hash``doc_id` 去掉 `doc-` 前缀后的 32 位 md5 |
| `format` | 固定为 `"latex"` |
| `content` | 字符串:是**原始** LaTeX(可能包含 Unicode 运算符、外层 `\[ \]`),不包含两头的`$`分割符;模态分析阶段直接读这里 |
| `self_ref` | 可选;解析引擎原始输出中的对象引用(如 Docling JSON Pointer `#/texts/15`,或 MinerU `content_list.json#/45`),用于溯源时回查原始解析产物 |
| `llm_analyze_result.equation` | 字符串:是大模型输出的**规范化**后的 LaTeX公式(外层 `$ / \[ \] / equation` 环境,Unicode 转 LaTeX,不包含联投的`$`分割符),这是后续多模态 chunk 真正使用的字符串; |
在模态分析阶段,如果`content`字段长度超过大模型的上下文长度时,表格内容会被机械地截断后在喂给模型。行内公式(与正文连续的 `<equation format="latex">…</equation>`**不会**保存到 equations.json 文件,它仅会在 blocks 文本里以无 `id` 形式留存。这样做的目的是避免给抽取结果注入过多的噪音。
## 七、surrounding
`surrounding.leading``surrounding.trailing` 是 sidecar item 的可分析上下文窗口,目的是提供图片、表格和公式所在段落的上下文信息,提高多模态分析的质量。**surrounding内容有LightRAG在分析阶段自动注入,不需要在文档解析引擎中主动写入sidecar文件中**。以下是surrounding内容的生成逻辑:
- 取自同一 `blockid` 对应的 content 行文本,以多模态占位标签的位置为切分点;
- 每一侧的 token 上限由环境变量 `SURROUNDING_LEADING_MAX_TOKENS` / `SURROUNDING_TRAILING_MAX_TOKENS` 控制(缺省 `2000`,可独立调整);按 tokenizer 截断,倾向保留靠近目标的句子;
- 文本中保留**同行其他**多模态对象的占位标签,这让模型能感知"图 1 之后还有公式 1"这种上下文;但解析器内部标识符(`id` / `path` / `src` / `refid`)已被 `strip_internal_multimodal_markup_for_extraction` 剥离 —— 与 chunk content 实体抽取前的清理一致,避免噪声进入 VLM/LLM prompt。具体清理规则:
- `<drawing id="im-…" path="…" src="…" caption="Fig 1" />``<drawing caption="Fig 1" />`**没有 caption 的 drawing 整段移除**(标签不再携带任何对模型可见的信息);
- `<table id="tb-…" format="json" caption="…">rows</table>``<table format="json" caption="…">rows</table>`
- `<equation id="eq-…" format="latex">body</equation>``<equation format="latex">body</equation>`
- `<cite type="table" refid="tb-…">表 1</cite>``<cite type="table">表 1</cite>``<cite type="equation" refid="eq-…">公式 2</cite>``<cite type="equation">公式 2</cite>`。仅删 `refid` 属性,保留 `<cite type="…">…</cite>` 包装 —— 让 VLM/LLM 能识别"这是对其他表/公式的引用"而非普通的文本,同时屏蔽 LLM 看不到的解析器内部 id;
- 例外:`tables.json` 类型的 surrounding 在 strip 之前先走 `remove_table_tags`,把所有 `<cite type="table">` 整段移除(分析目标表时不希望被对其他表的悬挂引用干扰);
- 清理发生在 token 预算截断**之前**token 数按"LLM 实际看到的内容"统计,且截断点不会落到未清理的 `id="…"` 属性中间,避免标签结构残缺;
- 当目标对象本身位于 block 起点 / 终点时,对应一侧为 `""` 而不是 `"n/a"`(提示词组装时再把空字符串显示为 `n/a`);
- `enrich_sidecars_with_surrounding` 是幂等的:每次 `analyze_multimodal` 入口都会重新计算并覆盖 `surrounding`,因此修改 `SURROUNDING_LEADING_MAX_TOKENS` / `SURROUNDING_TRAILING_MAX_TOKENS` 后无需手动清理 sidecar,重新执行多模态分析即可按新预算重写。
## 八、positions
`positions`是一个对象数组,用于标识`blockid`的内容来之文件中的哪一个文字,用于内容溯源的时候能够在原始文件中找到和显示对应的内容。当`blockid`的内容是由版面的多个栏目合并而成时,会出现多个`position` 对象,每个`position` 对象对应1个版面方框或栏目。为了适应不同的文档格式的内容定位方式,系统提供了以下几种`position` 对象对象类型。
`position` 对象有多种类型,对象的`type`字段决定了其类型:
* paraid
适用于docx格式文件;按`段落id`paraid)定位内容。`rang`字段指定起止`段落id``charspan`为可选字段,指定内容从段落的m个字符开始到底n个字符结束。不提供`charspan`表示`blockid`为起止段落的全部内容。示例:
```
"positions": [
{
"type": "paraid",
"range": ["5EA4577A", "6555DDCB"]
"charspan": [10,999]
}]
```
* bbox
适用于与PDF格式类似的文件,通过页面矩形位置来标定内容来源的原始位置。bbox支持一下字段:
```
origin: 矩形坐标相对于页面那个位置(可选字段,默认为LEFTTOP,另一个可选值为LEFTBOTTOM
max: 页面布局的长和宽的最大值,坐标按此值归一化以便能准确显示位置(可选字段,为空表示坐标按图片的点阵计算)
anchor: 页码, 页码为字符串,支持罗马数字等非数阿拉伯数字字页码
range: 矩形坐标数组 [h1,w1,h2,w2],例如 [174, 155, 818, 333]
charspan: 内容从标定段落的m个字符开始到底n个字符结束(可选字段)
```
`blocks.jsonl`文件的`meta`行的`bbox_attributes`字段保存的是bbox的全局设置,避免每个`content`行的`positions`对象中重复保存相同的内容。一下是一个典型的`positions`对象示例:
```
"positions": [
{
"type": "bbox",
"anchor": "ii"
"range": [174, 155, 818, 333]
"charspan": [10, 999]
}]
```
* heading
适用于与Markdown格式类似的文件,按标题定位内容。`anchor`是起始标题(标题重复是的处理方式查到markdown anchor规范);`charspan`为可选字段,指定内容从段落的m个字符开始到底n个字符结束。不提供`charspan`表示`blockid`为起止段落的全部内容。
```
"positions": [
{
"type": "heading",
"anchor": "ii"
"range": [174, 155, 818, 333]
"charspan": [10, 999]
}]
```
* absolute
适用于text格式类似的文件,按字符绝对位置定位。`charspan`指定内容从段落的m个字符开始到底n个字符结束。
```
"positions": [
{
"charspan": [10, 999]
}]
```
## 九、`llm_analyze_result`
| `status` | 触发场景 | 字段说明 |
|---|---|---|
| `success` | 模型成功返回合法 JSON 且必需字段齐全 | 图形:`name / type / description`;表格:`name / description`;公式:`name / description / equation` |
| `skipped` | 期跳过多模态分析:图片格式不支持、像素 < `VLM_MIN_IMAGE_PIXEL`(默认 32px)、大于 `VLM_MAX_IMAGE_BYTES`(默认 5 MB)、未启用VLM | `message` 写跳过原因 |
| `failure` | 必需字段缺失、JSON 修复后仍不合法、VLM/EXTRACT role 未配置而对应模态被启用、模型调用异常 | `message` 写诊断 |
补充:
- `analyze_time` 是 epoch 秒,每个 status 都有;
- `message``status="success"` 时**恒为空串**,便于过滤;
- 对已启用模态的 item,每次 `analyze_multimodal` 都会重新计算,并用本次结果覆盖已有的 `llm_analyze_result`(无论原先是 `success``skipped` 还是 `failure`)。这样修正 VLM/EXTRACT 配置后可以直接重试,无须手动清理旧 sidecar 结果。LLM 调用仍会走 analysis cache:如果 cache key 命中,不会再次请求 provider,语义字段通常保持一致,但 `analyze_time` 等运行时字段会被重写。只有 cache miss,例如有效 role 模型 / binding / host、prompt 输入或图片元数据变化后,保存内容才可能与上次不同。
图形 `type` 受 12 项枚举约束(见 [`IMAGE_TYPE_ENUM`](../lightrag/prompt_multimodal.py)`Photo / Illustration / Screenshot / Icon / Chart / Table / Infographic / Flowchart / Chat Log / Wireframe / Texture / Other`);模型若返回枚举外的值,会被规整成 `Other` 而不是失败。
+402
View File
@@ -0,0 +1,402 @@
# LightRAG Sidecar File Format Specification
This document describes the **LightRAG Sidecar** file format that content parsing engines output. When LightRAG uses multimodal-capable content parsing engines such as native/mineru/docling to extract file content, it splits "body text + multimodal objects + parsing metadata" into a `*.parsed/` directory. Each JSON / JSONL file in that directory is collectively called a **sidecar** file. Sidecars are the only reliable source of truth for the subsequent pipeline (multimodal analysis → multimodal chunk construction → entity extraction → cache cleanup on document deletion). The sidecar format is LightRAG's built-in universal file interchange format; new multimodal content extraction engines must follow this format. The purpose of publicly documenting the **LightRAG Sidecar** format is to make it convenient for community developers to write their own content parsing engines.
## 1. Overview
| Concern | File | Contents | Notes |
|---|---|---|---|
| Main file | `<doc>.blocks.jsonl` | Stores block body | Concatenating the `content` fields of all blocks reconstructs the complete original text |
| Drawing objects | `<doc>.drawings.json` | Drawing objects extracted from the file | Sent to a VLM for analysis; analysis results are written back |
| Table objects | `<doc>.tables.json` | Table objects extracted from the file | Sent to an LLM for analysis; analysis results are written back |
| Equation objects | `<doc>.equations.json` | Equation objects extracted from the file | Sent to an LLM for analysis; analysis results are written back |
| Original image assets | `<doc>.blocks.assets/` | Original image files extracted from the document | Sent to a VLM for image analysis |
Design intent of sidecars:
- During the parsing stage, the content extraction engine (native/mineru/docling) is **only** responsible for generating "objective" fields such as `blockid / heading / content / surrounding`;
- During the multimodal analysis stage (`analyze_multimodal`), the analysis result dict `llm_analyze_result` is written by LightRAG and may be appended or overwritten; parsers should not pre-populate it.
## 2. Directory Layout
```
inputs/space1/__parsed__/<canonical filename>.parsed/
├── <canonical filename>.blocks.jsonl body block sequence + document-level meta (first line)
├── <canonical filename>.drawings.json drawing sidecar (dict container, key = drawing id)
├── <canonical filename>.tables.json table sidecar
├── <canonical filename>.equations.json equation sidecar
└── <canonical filename>.blocks.assets/ original asset directory (image files referenced by drawings.json live here)
├── image1.wmf
├── image2.wmf
├── image3.wmf
├── image4.png
├── image5.png
├── image6.png
└── image7.emf
```
## 3. blocks.jsonl
`blocks.jsonl` is JSON serialized line by line. The **first line has `type="meta"`**; every subsequent line is a content block with `type="content"`.
### 3.1 meta line example
```json
{
"type": "meta",
"format": "lightrag",
"version": "1.0",
"document_name": "m012-manual.docx",
"document_format": "docx",
"document_hash": "sha256:4840...3f9543d9db0822d2d59",
"table_file": true,
"equation_file": true,
"drawing_file": true,
"asset_dir": true,
"blocks": 39,
"doc_id": "doc-f1bee60173d067d88595c00e7d9b0ce5",
"parse_engine": "native",
"parse_time": "2026-05-13T18:42:25.943490+00:00",
"doc_title": "m012-manual"
}
```
| Field | Type | Description |
|---|---|---|
| `type` | `"meta"` | Line type, fixed value, sanity check |
| `format` | `"lightrag"` | Sidecar major version family identifier |
| `version` | `str` | Sidecar schema version |
| `document_name` | `str` | Canonical filename (with extension, without processing hints) |
| `document_format` | `str` | File format (currently expressed as the file extension) |
| `document_hash` | `"sha256:<hex>"` | Sidecar body fingerprint, defined as `SHA-256(merged_text)`, where `merged_text` is the concatenation of all non-empty content lines' `content` fields joined by `"\n\n"`. Used by external consumers to quickly determine whether two `.parsed/` directories share the same source (without line-by-line body comparison), and serves as a self-describing content checksum for the sidecar file. Note: the LightRAG ingestion pipeline itself does not read this field; cross-document deduplication is handled separately by `doc_status.content_hash`. |
| `table_file` / `equation_file` / `drawing_file` | `bool` | Whether the corresponding sidecar files exist (when true, the corresponding file must exist) |
| `asset_dir` | `bool` | Whether the `blocks.assets` asset directory exists |
| `split_option` | `object` | Optional. Free-form metadata the parsing engine records about itself (e.g. `engine_version`, engine-specific extras). Omitted entirely when the engine recorded nothing (the common native/markdown case). Chunking is NOT performed at the parse stage — it is the downstream chunker's responsibility — so this field never carries chunking parameters. |
| `blocks` | `int` | Number of content lines (excluding meta) |
| `doc_id` | `"doc-<md5>"` | Global document ID. Sidecar item IDs (`im-/tb-/eq-`) use the hash portion of `doc_id` with the `doc-` prefix removed, in order to shorten the placeholder tags embedded in body text. |
| `parse_engine` | `str` | Parsing engine `native/mineru/docling/legacy` |
| `parse_time` | `str` | Parse completion time; format: ISO-8601 UTC |
| `doc_title` | `str` | Document title (usually the first H1); optional |
| `doc_summary` | `str` | Document summary; optional |
| `doc_attributes` | `object` | Document extended attributes object; optional |
| `bbox_attributes` | `object` | Global bbox position attributes; see [§8](#8-positions) |
> LightRAG requires that filenames (`document_name`) be unique within the same workspace (knowledge base).
### 3.2 content line
Each content line is the minimum addressable unit of an original document "block" and contains at least:
```json
{
"type": "content",
"blockid": "652e5de55805d1d449b400c0d4a95ca8",
"format": "plain_text",
"content": "# 1 Product Purpose and Functions\nThe MI012 module is used to support the oxygen-supply and anti-gravity control function of the oxygen-supply and anti-gravity regulator...",
"heading": "1 Product Purpose and Functions",
"parent_headings": [],
"level": 1,
"session_type": "body",
"table_slice": "none",
"positions": [
{
"type": "paraid",
"range": ["5EA4577A", "6555DDCB"]
}
]
}
```
| Field | Meaning |
|---|---|
| `type` | `"content"` |
| `blockid` | Globally unique Block ID |
| `format` | Content form, currently fixed to `"plain_text"` |
| `content` | Text content; **equations and images appear as placeholder tags here, tables appear as JSON or HTML wrapped in table tags** (see §3.3). The heading line is rendered with a markdown `#` prefix (plus a space) matching `level`: level 1 → one `#`, level 2 → two `#`, …, capped at 6 (a level ≥ 7 heading still renders `######`). If the source heading text already begins with a markdown prefix (16 `#` followed by a space), it is kept verbatim and not prefixed again. Note the `heading` field itself stays clean (no `#`). |
| `heading` | The top-most-level heading of the section containing this content. When `heading` is real, it should also appear at the beginning of `content` as the markdown-rendered heading line. **Every recognized heading starts its own block**: if a heading is immediately followed by body text, that body is merged into the same block (content = markdown-rendered heading line + body); if a heading has no following body (e.g., it is immediately followed by a heading at the next level), it still becomes a standalone block whose content is just the markdown-rendered heading line. This ensures that concatenating the `content` fields of all blocks still reconstructs the complete original text without overlap. |
| `parent_headings` | String array: the top-down list of ancestor headings, excluding the current `heading` |
| `level` | Integer: the level of `heading` in the document outline (`1` = H1 / first-level heading; `0` means no heading) |
| `session_type` | The region the block belongs to: `body` `preface` `TOC` `references` `appendix` |
| `table_slice` | Optional reserved field; indicates whether the block contains only a slice of a table. The current analysis engines do not split long tables, so this field is fixed to `"none"` (meaning the table will not be sliced) |
| `table_header` | Optional reserved field; when the current block is a table slice, this holds the recognized table header. Currently unused. |
| `positions` | Array of `position` objects: identifies the layout position of the text block; when the text block comes from multiple positions in the layout, multiple `position` objects appear. See [§8](#8-positions) |
> - blockid computation: `md5(doc_id + ":" + block_index + ":" + heading + ":" + content)`. Chunks produced by chunking strategies record the blockid for tracing the chunk back to its location in the sidecar.
> - The chunking strategies `F` / `R` / `V` that ignore document section structure operate on the concatenated `content` fields. Therefore, concatenating the `content` fields of all blocks must form the complete document content — no content missing, no content overlapping.
### 3.3 Inline placeholder tags inside content
To let the P chunking strategy split body text without breaking multimodal objects, three XML-style placeholder tags are used inside `content`:
| Tag | Meaning | Tag attributes |
|---|---|---|
| `<table id="tb-…" format="json">…</table>` | Table placeholder; the body is the raw table JSON / HTML | `id` points to the corresponding item in `tables.json`; `format``json` / `html` |
| `<drawing id="im-…" format="png" path="…" src="…" caption="…" />` | Self-closing drawing placeholder | `id` points to `drawings.json`; `path` is relative to the `*.parsed/` directory; `src` is the reference name in the original document |
| `<equation id="eq-…" format="latex" caption="…">…</equation>` | Equation placeholder | Inline equations also use `<equation format="latex">`, but **without** `id`, and are not written to the sidecar; only block equations (occupying one or more entire lines) carry an `id` |
When the text is fed to the LLM during entity/relation extraction, internal attributes such as `id / path / src` are stripped, but key attributes (`format / caption`) are preserved. The goal is to avoid extracting entities that are invisible in the article and injecting too much noise into the extraction results.
### 3.4 Correspondence between blockid and chunk sidecar.refs
When a sidecar file exists, the chunking strategies attach `sidecar = {"type": "block", "id": <primary source blockid>, "refs": [{"type": "block", "id": <blockid>}, …]}` to each output chunk, where:
- Unmerged chunk → `sidecar.refs` has only one element, equal to the `blockid` of the blocks.jsonl line the chunk came from;
- Chunk merged in LevelMerge → `refs` preserves the order of all source `blockid`s (deduplicated);
- Sub-chunks after hard fallback split → share the parent chunk's `sidecar`.
This linkage is the basis for document-level traceability (chunk ↔ block ↔ original paragraph paraId).
## 4. drawings.json
The top level is a dict container of the form `{"version": "1.0", "drawings": { <id>: <item>, … }}`, **keyed by the `id` field** for lookup by id. Each item looks like:
```json
{
"id": "im-f1bee60173d067d88595c00e7d9b0ce5-0004",
"blockid": "2f52b70839d13a936d97955916820147",
"heading": "2.3 Structural Dimensions and Weight",
"parent_headings": ["2 Product Description"],
"format": "png",
"path": "m012-manual.blocks.assets/image4.png",
"src": "",
"caption": "",
"footnotes": [],
"extras": {
"ocr_texts": "First OCR paragraph inside the image\n\nSecond OCR paragraph inside the image",
"ocr_texts_count": 2
},
"surrounding": {
"leading": "2.3 Structural Dimensions and Weight\nDimensional and weight requirements are as follows:\na) Outer dimensions length: <drawing …",
"trailing": "\nFigure 1 Outer dimension schematic\nb) Weight does not exceed 0.85 kg.\nc) Test result: measured circuit noise Vpp=1.526 mV…"
},
"llm_analyze_result": {
"name": "Product outer-dimension engineering drawing",
"type": "Illustration",
"description": "This drawing is a schematic of the product's outer dimensions, presenting three views of an electronic device or power module design…",
"analyze_time": 1778697752,
"status": "success",
"message": ""
},
"llm_cache_list": [
"default:analysis:fcf4c4f88227ee1c1bf0ed4394039e37"
]
}
```
| Field | Description |
|---|---|
| `id` | Form `im-<doc_hash>-<NNNN>` (`doc_hash` is the 32-character md5 portion of `doc_id` with the `doc-` prefix removed) |
| `blockid` | Points to the content line that produced this drawing |
| `heading` | The section heading the drawing belongs to |
| `parent_headings` | String array: the top-down list of ancestor headings, excluding the current `heading` (mirrors the `blocks.jsonl` field of the same name on the block this drawing belongs to) |
| `format` | Original extension (no dot): `png` / `jpeg` / `gif` / `webp` / `wmf` / `emf` / … |
| `path` | Resource path relative to the `*.parsed/` directory; **always** points to a file inside `*.blocks.assets/` |
| `src` | The reference alias of the drawing in the original document (empty in most cases) |
| `caption` | Visible caption (the parser may leave it empty) |
| `footnotes` | List of footnote strings |
| `surrounding` | Context object: see [§7](#7-surrounding) |
| `self_ref` | String, optional; an object reference from the original parsing engine output (e.g., Docling JSON Pointer `#/pictures/3`, or MinerU `content_list.json#/23`), used to look up the original object in the parsing artifacts (page position, original structure, etc.) when tracing back. Not output by `native` and other engines that do not provide this field. |
| `extras` | Object, optional; engine-specific bypass fields (such as OCR text contained inside the image, etc.). Not part of spec validation; downstream consumers should not rely on specific keys. |
| `llm_analyze_result` | Modal analysis result object: see [§9](#9-llm_analyze_result) (will later be injected into the multimodal text block) |
| `llm_cache_list` | LLM cache list for modal analysis (will later be injected into the multimodal text block) |
Common drawing-specific keys inside `extras`:
| Key | Description |
|---|---|
| `ocr_texts` | String, optional; OCR text inside the drawing object, with multiple paragraphs concatenated by blank lines (`\n\n`). Only written when the parsing engine explicitly attaches OCR text under this drawing's children; caption / footnote do not enter this field. |
| `ocr_texts_count` | Integer, optional; number of non-empty OCR paragraphs written into `ocr_texts`. |
**Only raster formats supported by drawings (png / jpeg / gif / webp) enter VLM analysis**; other formats (wmf / emf / svg, etc.) get `llm_analyze_result.status="skipped"`, no multimodal chunk is generated downstream, and document processing continues. Images larger than the size specified by the environment variable `VLM_MAX_IMAGE_BYTES` likewise will not enter VLM analysis.
> Information such as image size and DPI is uniformly placed in the `extras` object; do not introduce undeclared fields (like `image` / `img_path`, etc.) at the item top level. tables / equations follow the same `extras` convention. `self_ref` is a top-level optional field declared by the spec and does not belong to `extras`.
## 5. tables.json
The top level is a dict container of the form `{"version": "1.0", "tables": { <id>: <item>, ... }}`, **keyed by the `id` field** for lookup by id. Each item looks like:
```json
{
"id": "tb-f1bee60173d067d88595c00e7d9b0ce5-0007",
"blockid": "3f33897b5e105d254addc655f1efbf8c",
"heading": "2.4.4 Temperature-Humidity-Altitude (run with the system)",
"parent_headings": ["2 Product Description", "2.4 Environmental Adaptability"],
"dimension": [16, 8],
"format": "json",
"content": "[[\"Step\", \"Temperature (°C)\", \"Altitude (m)\", \"Relative humidity\", \"Time (min)\", \"Auxiliary cooling\", \"System power\", \"Functional/performance check\"],…",
"caption": "",
"footnotes": [],
"table_header": "[[\"Step\", \"Temperature (°C)\", \"Altitude (m)\", \"Relative humidity\", \"Time (min)\", \"Auxiliary cooling\", \"System power\", \"Functional/performance check\"]]"
"surrounding": {
"leading": "2.4.4 Temperature-Humidity-Altitude (run with the system)\nThe product shall withstand the combined temperature, humidity, and altitude environment during mission execution…",
"trailing": "\nNote: the above steps are repeated for 10 cycles. a) Finished product and accessories reach thermal stability or 240 min, whichever is longer; b) Finished product and accessories reach thermal stability or 120 min, whichever is longer.…"
},
"llm_analyze_result": {
"name": "Document management metadata table",
"description": "This is a document management information table used to record basic metadata and version control information for a technical document …",
"analyze_time": 1778697759,
"status": "success",
"message": ""
},
"llm_cache_list": [
"default:analysis:b316aacd40fdca0cb56430870bb89a62"
]
}
```
The `blockid` / `heading` / `parent_headings` / `surrounding` / `llm_analyze_result` fields of tables.json have the same meaning as in drawings.json. Different or newly added fields are described below:
| Field | Description |
|---|---|
| `id` | Form `tb-<doc_hash>-<NNNN>` (`doc_hash` is the 32-character md5 portion of `doc_id` with the `doc-` prefix removed) |
| `dimension` | Integer array: `[num_rows, num_cols]`, including header rows |
| `format` | `"json"` (2D array) or `"html"` (payload `<table>…</table>` fragment including the opening and closing tags) |
| `content` | String: the table body, structured according to `format`; this is the string actually used by the downstream multimodal chunk. |
| `table_header` | String, optional; the recognized row(s) treated as the table header |
| `self_ref` | Optional; object reference from the original parsing engine output (e.g., Docling JSON Pointer `#/tables/2`, or MinerU `content_list.json#/31`), used to look up the original artifact when tracing back |
During the modal analysis stage, when the length of the `content` field exceeds the LLM's context window, the table content is mechanically truncated before being fed to the model.
## 6. equations.json
The top level is a dict container of the form `{"version": "1.0", "equations": { <id>: <item>, ... }}`, **keyed by the `id` field** for lookup by id. Each item looks like:
```json
{
"id": "eq-f1bee60173d067d88595c00e7d9b0ce5-0001",
"blockid": "2f52b70839d13a936d97955916820147",
"heading": "2.3 Structural Dimensions and Weight",
"parent_headings": ["2 Product Description"],
"format": "latex",
"content": "C=2\\frac{PT}{\\left( {V}_{H}^{2}{V}_{L}^{2} \\right)∗η}",
"caption": "",
"footnotes": [],
"surrounding": {
"leading": "2.3 Structural Dimensions and Weight\nDimensional and weight requirements are as follows:\n …",
"trailing": "\nwhere P is the power maintained during power abnormalities 28 W, T is the desired energy-storage time, V<sub>H</sub> is before capacitor discharge…"
},
"llm_analyze_result": {
"name": "Capacitor energy-storage time calculation formula",
"description": "This formula calculates the capacitor energy storage value required to maintain normal system operation during power abnormality …",
"analyze_time": 1778697783,
"status": "success",
"message": "",
"equation": "C=2\\cdot\\frac{P\\cdot T}{(V_{H}^{2}-V_{L}^{2})\\cdot\\eta}"
},
"llm_cache_list": [
"default:analysis:fcf4c4f88227ee1c1bf0ed4394039e37"
]
}
```
The `blockid` / `heading` / `parent_headings` / `surrounding` / `llm_analyze_result` fields of equations.json have the same meaning as in drawings.json. Different or newly added fields are described below:
| Field | Description |
|---|---|
| `id` | Form `eq-<doc_hash>-<NNNN>` (`doc_hash` is the 32-character md5 portion of `doc_id` with the `doc-` prefix removed) |
| `format` | Fixed to `"latex"` |
| `content` | String: the **raw** LaTeX (possibly containing Unicode operators, outer `\[ \]`); does not include the leading/trailing `$` delimiters; read directly by the modal analysis stage |
| `self_ref` | Optional; object reference from the original parsing engine output (e.g., Docling JSON Pointer `#/texts/15`, or MinerU `content_list.json#/45`), used to look up the original artifact when tracing back |
| `llm_analyze_result.equation` | String: the **canonicalized** LaTeX equation output by the LLM (outer `$ / \[ \] / equation` environment, Unicode converted to LaTeX, no leading/trailing `$` delimiters); this is the string actually used by the downstream multimodal chunk. |
During the modal analysis stage, when the length of the `content` field exceeds the LLM's context window, the content is mechanically truncated before being fed to the model. Inline equations (those continuous with the body, as `<equation format="latex">…</equation>`) **are not** saved to equations.json; they remain only in the blocks text without an `id`. The goal is to avoid injecting too much noise into the extraction results.
## 7. surrounding
`surrounding.leading` and `surrounding.trailing` are the analyzable context windows of a sidecar item; their purpose is to provide contextual information about the paragraph containing the image, table, or equation, improving the quality of multimodal analysis. **The surrounding content is automatically injected by LightRAG during the analysis stage; it does not need to be actively written into the sidecar by the document parsing engine.** The generation logic of the surrounding content is as follows:
- Taken from the text of the content line with the same `blockid`, split at the position of the multimodal placeholder tag;
- The token limit on each side is controlled by the environment variables `SURROUNDING_LEADING_MAX_TOKENS` / `SURROUNDING_TRAILING_MAX_TOKENS` (default `2000`, can be tuned independently); truncated by tokenizer, preferring to retain sentences close to the target;
- The text preserves placeholder tags of **other multimodal objects on the same line**, allowing the model to perceive context such as "after Figure 1 there is also Equation 1"; but internal parser identifiers (`id` / `path` / `src` / `refid`) have been stripped by `strip_internal_multimodal_markup_for_extraction` — consistent with chunk content cleanup before entity extraction, to avoid noise entering the VLM/LLM prompt. Specific cleanup rules:
- `<drawing id="im-…" path="…" src="…" caption="Fig 1" />``<drawing caption="Fig 1" />`; **drawings without a caption are removed entirely** (the tag carries no model-visible information anymore);
- `<table id="tb-…" format="json" caption="…">rows</table>``<table format="json" caption="…">rows</table>`;
- `<equation id="eq-…" format="latex">body</equation>``<equation format="latex">body</equation>`;
- `<cite type="table" refid="tb-…">Table 1</cite>``<cite type="table">Table 1</cite>`; `<cite type="equation" refid="eq-…">Equation 2</cite>``<cite type="equation">Equation 2</cite>`. Only the `refid` attribute is removed; the `<cite type="…">…</cite>` wrapper is preserved — letting the VLM/LLM recognize "this is a reference to another table/equation" rather than ordinary text, while hiding the parser-internal id that the LLM cannot see.
- Exception: surrounding of the `tables.json` type first goes through `remove_table_tags` before stripping, removing all `<cite type="table">` blocks entirely (when analyzing the target table, we don't want to be distracted by dangling references to other tables);
- Cleanup happens **before** token-budget truncation: the token count is computed on "what the LLM actually sees", and truncation does not land inside an uncleaned `id="…"` attribute, avoiding broken tag structure;
- When the target object itself sits at the start / end of the block, the corresponding side is `""` instead of `"n/a"` (when assembling the prompt, the empty string is later displayed as `n/a`);
- `enrich_sidecars_with_surrounding` is idempotent: each `analyze_multimodal` entry point recomputes and overwrites `surrounding`, so after changing `SURROUNDING_LEADING_MAX_TOKENS` / `SURROUNDING_TRAILING_MAX_TOKENS` there is no need to manually clean the sidecar — just re-run multimodal analysis and `surrounding` will be rewritten under the new budget.
## 8. positions
`positions` is an array of objects that identifies which piece of text in the file the `blockid` content comes from, allowing the original content to be located and displayed in the source file during content traceability. When the content of a `blockid` is composed of several columns from the layout, multiple `position` objects appear, with each `position` object corresponding to one layout box or column. To accommodate different document formats' content positioning approaches, the system supports the following types of `position` object.
`position` objects have multiple types, and the `type` field determines its type:
* paraid
Applicable to docx-format files; locates content by `paragraph id` (paraid). The `range` field specifies the start and end `paragraph id`s; `charspan` is an optional field specifying that the content starts at character m and ends at character n of the paragraph. When `charspan` is not provided, the `blockid` covers the entire content of the start and end paragraphs. Example:
```
"positions": [
{
"type": "paraid",
"range": ["5EA4577A", "6555DDCB"]
"charspan": [10,999]
}]
```
* bbox
Applicable to PDF-like files; identifies the original position of the content via a rectangle on the page. bbox supports the following fields:
```
origin: Which position the rectangle coordinates are relative to on the page (optional, defaults to LEFTTOP; another option is LEFTBOTTOM)
max: Maximum length and width of the page layout; coordinates are normalized by this value for accurate position display (optional; empty means coordinates are computed by the image's pixel grid)
anchor: Page number, as a string, supporting non-Arabic page numbers such as Roman numerals
range: Rectangle coordinate array [h1, w1, h2, w2], e.g., [174, 155, 818, 333]
charspan: Content starts at character m and ends at character n of the anchored paragraph (optional)
```
The `bbox_attributes` field of the `meta` line in `blocks.jsonl` holds global bbox settings, avoiding repeating the same content in every `content` line's `positions` object. A typical `positions` object example:
```
"positions": [
{
"type": "bbox",
"anchor": "ii"
"range": [174, 155, 818, 333]
"charspan": [10, 999]
}]
```
* heading
Applicable to Markdown-like files; locates content by heading. `anchor` is the starting heading (for handling duplicated headings, refer to the Markdown anchor specification); `charspan` is an optional field specifying that the content starts at character m and ends at character n of the paragraph. When `charspan` is not provided, the `blockid` covers the entire content of the start and end paragraphs.
```
"positions": [
{
"type": "heading",
"anchor": "ii"
"range": [174, 155, 818, 333]
"charspan": [10, 999]
}]
```
* absolute
Applicable to text-like files; locates content by absolute character position. `charspan` specifies that the content starts at character m and ends at character n.
```
"positions": [
{
"charspan": [10, 999]
}]
```
## 9. `llm_analyze_result`
| `status` | Trigger scenario | Field description |
|---|---|---|
| `success` | The model returns valid JSON and all required fields are present | Drawing: `name / type / description`; Table: `name / description`; Equation: `name / description / equation` |
| `skipped` | Multimodal analysis was deliberately skipped: image format unsupported, pixels < `VLM_MIN_IMAGE_PIXEL` (default 32 px), larger than `VLM_MAX_IMAGE_BYTES` (default 5 MB), or VLM not enabled | `message` records the skip reason |
| `failure` | Required fields missing, JSON still invalid after repair, the VLM/EXTRACT role is not configured while the corresponding modality is enabled, or the model invocation throws an exception | `message` records the diagnostic |
Additional notes:
- `analyze_time` is epoch seconds and is present for every status;
- `message` is **always an empty string** when `status="success"`, making filtering convenient;
- Items for enabled modalities are recomputed on each `analyze_multimodal` run, and the current run overwrites any prior `llm_analyze_result` (`success`, `skipped`, or `failure`). This allows operators to fix VLM/EXTRACT configuration and retry without manually clearing stale sidecar results. LLM calls still use the analysis cache: if the cache key matches, the provider is not called and semantic fields usually remain the same, though runtime fields such as `analyze_time` are rewritten. A cache miss, for example after changing the effective role model/binding/host, prompt inputs, or image metadata, can produce different saved content.
Drawing `type` is constrained to a 12-value enum (see [`IMAGE_TYPE_ENUM`](../lightrag/prompt_multimodal.py): `Photo / Illustration / Screenshot / Icon / Chart / Table / Infographic / Flowchart / Chat Log / Wireframe / Texture / Other`); values returned by the model outside the enum are normalized to `Other` rather than failing.
+197
View File
@@ -0,0 +1,197 @@
# Milvus Configuration via vector_db_storage_cls_kwargs
## Overview
Milvus index parameters can be configured through `vector_db_storage_cls_kwargs`, which is the **recommended approach** for framework integration scenarios (e.g., when using RAGAnything or other frameworks built on top of LightRAG).
## Why Use vector_db_storage_cls_kwargs?
**Framework Integration**: Allows configuration to be passed through framework layers without environment variable changes
**Programmatic Configuration**: Set parameters in code rather than relying on environment variables
**Dynamic Configuration**: Different configurations for different RAG instances
**Clean API**: All parameters passed in one place during initialization
## Supported Parameters
All 11 MilvusIndexConfig parameters can be configured via `vector_db_storage_cls_kwargs`:
### Base Configuration
- `index_type`: Index type (AUTOINDEX, HNSW, HNSW_SQ, IVF_FLAT, etc.)
- `metric_type`: Distance metric (COSINE, L2, IP)
### HNSW Parameters
- `hnsw_m`: Number of connections per layer (2-2048, default: 16)
- `hnsw_ef_construction`: Size of dynamic candidate list during construction (default: 360)
- `hnsw_ef`: Size of dynamic candidate list during search (default: 200)
### HNSW_SQ Parameters (requires Milvus 2.6.8+)
- `sq_type`: Quantization type (SQ4U, SQ6, SQ8, BF16, FP16, default: SQ8)
- `sq_refine`: Enable refinement (default: False)
- `sq_refine_type`: Refinement type (SQ6, SQ8, BF16, FP16, FP32, default: FP32)
- `sq_refine_k`: Number of candidates to refine (default: 10)
### IVF Parameters
- `ivf_nlist`: Number of cluster units (1-65536, default: 1024)
- `ivf_nprobe`: Number of units to query (default: 16)
## Configuration Priority
Configuration is resolved in the following order:
1. **Parameters passed via vector_db_storage_cls_kwargs** (highest priority)
2. Environment variables (MILVUS_INDEX_TYPE, etc.)
3. Default values
## Usage Examples
### Basic Configuration
```python
from lightrag import LightRAG
rag = LightRAG(
working_dir="./demo",
vector_storage="MilvusVectorDBStorage",
vector_db_storage_cls_kwargs={
"cosine_better_than_threshold": 0.2,
"index_type": "HNSW",
"metric_type": "COSINE",
"hnsw_m": 32,
"hnsw_ef_construction": 256,
"hnsw_ef": 150,
}
)
```
### RAGAnything Framework Integration
```python
# In RAGAnything framework code:
def create_lightrag_instance(user_config):
"""Create LightRAG instance with user-provided Milvus configuration"""
# User configuration from RAGAnything
milvus_config = {
"cosine_better_than_threshold": user_config.get("threshold", 0.2),
"index_type": user_config.get("index_type", "HNSW"),
"hnsw_m": user_config.get("hnsw_m", 32),
# ... other parameters
}
# Pass configuration to LightRAG
rag = LightRAG(
working_dir=user_config["working_dir"],
vector_storage="MilvusVectorDBStorage",
vector_db_storage_cls_kwargs=milvus_config,
)
return rag
```
### Advanced Configuration with HNSW_SQ
```python
rag = LightRAG(
working_dir="./demo",
vector_storage="MilvusVectorDBStorage",
vector_db_storage_cls_kwargs={
"cosine_better_than_threshold": 0.2,
"index_type": "HNSW_SQ", # Requires Milvus 2.6.8+
"metric_type": "COSINE",
"hnsw_m": 48,
"hnsw_ef_construction": 400,
"hnsw_ef": 200,
"sq_type": "SQ8",
"sq_refine": True,
"sq_refine_type": "FP32",
"sq_refine_k": 20,
}
)
```
### IVF Configuration
```python
rag = LightRAG(
working_dir="./demo",
vector_storage="MilvusVectorDBStorage",
vector_db_storage_cls_kwargs={
"cosine_better_than_threshold": 0.2,
"index_type": "IVF_FLAT",
"metric_type": "L2",
"ivf_nlist": 2048,
"ivf_nprobe": 32,
}
)
```
## Implementation Details
### How It Works
1. When `MilvusVectorDBStorage.__post_init__()` is called:
```python
kwargs = self.global_config.get("vector_db_storage_cls_kwargs", {})
index_config_keys = MilvusIndexConfig.get_config_field_names()
index_config_params = {
k: v for k, v in kwargs.items() if k in index_config_keys
}
self.index_config = MilvusIndexConfig(**index_config_params)
```
2. `MilvusIndexConfig.get_config_field_names()` dynamically extracts all valid parameter names from the dataclass
3. Only valid Milvus index parameters are extracted from kwargs
4. Parameters are passed to `MilvusIndexConfig` which applies defaults and validates them
5. Environment variables are used as fallback for any parameters not provided in kwargs
### Automatic Synchronization
The implementation uses `MilvusIndexConfig.get_config_field_names()` to dynamically extract valid parameters. This means:
- ✅ New parameters added to `MilvusIndexConfig` are **automatically recognized**
- ✅ No need to maintain duplicate parameter lists
- ✅ Single source of truth for configuration parameters
## Testing
The configuration via `vector_db_storage_cls_kwargs` is thoroughly tested:
```bash
# Run all kwargs bridge tests
python -m pytest tests/kg/milvus_impl/test_milvus_kwargs_bridge.py -v
# Test RAGAnything integration scenario specifically
python -m pytest tests/kg/milvus_impl/test_milvus_kwargs_bridge.py::TestMilvusKwargsParameterBridge::test_raganything_framework_integration_scenario -v
# Test all parameters support
python -m pytest tests/kg/milvus_impl/test_milvus_kwargs_bridge.py::TestMilvusKwargsParameterBridge::test_all_milvus_parameters_supported_via_kwargs -v
```
## Examples
See `examples/milvus_kwargs_configuration_demo.py` for a complete working example.
## Backward Compatibility
✅ **100% backward compatible** with existing code
✅ Environment variable configuration still works
✅ All existing tests pass
## FAQ
### Q: Can I mix kwargs and environment variables?
**A:** Yes! Parameters in `vector_db_storage_cls_kwargs` take priority over environment variables.
### Q: What happens to non-Milvus parameters in kwargs?
**A:** They are ignored. Only valid MilvusIndexConfig parameters are extracted. This allows frameworks to pass their own parameters alongside Milvus configuration.
### Q: Do I need to set environment variables?
**A:** No! When using `vector_db_storage_cls_kwargs`, environment variables are optional. They serve as fallback values.
### Q: Is this approach recommended for RAGAnything?
**A:** Yes! This is the **recommended approach** for any framework that builds on top of LightRAG, as it allows clean configuration passing through framework layers.
## References
- Test Suite: `tests/kg/milvus_impl/test_milvus_kwargs_bridge.py`
- Implementation: `lightrag/kg/milvus_impl.py` (lines 1237-1272)
- Example: `examples/milvus_kwargs_configuration_demo.py`
- MilvusIndexConfig: `lightrag/kg/milvus_impl.py` (lines 75-303)
+382
View File
@@ -0,0 +1,382 @@
# Single-Server Multi-Site Deployment
This document explains how to run multiple isolated LightRAG instances behind one host using a reverse proxy (nginx, Traefik, Kubernetes Ingress, …), with **one shared WebUI build** reused by every instance.
> Looking for the basic single-instance Docker setup? See [DockerDeployment.md](./DockerDeployment.md). For frontend build
> mechanics in general, see [FrontendBuildGuide.md](./FrontendBuildGuide.md).
---
## TL;DR
- Set `LIGHTRAG_API_PREFIX` per-instance, on the **backend only**. The WebUI is always mounted at `/webui` (not configurable).
- Build the WebUI **once**. The same artifacts work under any reverse-proxy prefix.
- Point your reverse proxy at each backend, stripping the site prefix before forwarding.
```bash
# One image, two containers, two prefixes — no rebuild.
docker run -e LIGHTRAG_API_PREFIX=/site01 -p 9621:9621 lightrag:latest
docker run -e LIGHTRAG_API_PREFIX=/site02 -p 9622:9621 lightrag:latest
```
---
## Why "build once, deploy many"
Earlier versions of LightRAG baked the site prefix into the JavaScript bundle at build time (via `VITE_API_PREFIX` / `VITE_WEBUI_PREFIX`). Every site that used a different prefix needed its own WebUI build, and reusing a single Docker image across sites required a rebuild step at deploy time. Since the runtime-config-injection refactor:
- **Asset URLs** in `index.html` are emitted as relative paths (`./assets/index-abc.js`). The browser resolves them against the current document URL, so they work under any mount point.
- **API base URL** and **in-app links** read their prefix from `window.__LIGHTRAG_CONFIG__`, which the FastAPI server injects into `index.html` on each response based on its own `LIGHTRAG_API_PREFIX`.
The result: a single `lightrag/api/webui/` directory (or Docker image) is reusable across any number of sites with no per-site build artifact.
---
## How runtime prefix injection works
Each request for `index.html` goes through `SmartStaticFiles` in `lightrag/api/lightrag_server.py`, which:
1. Reads the static `index.html` produced by `bun run build`.
2. Looks for the placeholder comment `<!-- __LIGHTRAG_RUNTIME_CONFIG__ -->`.
3. Replaces it with
`<script>window.__LIGHTRAG_CONFIG__ = {"apiPrefix":"…","webuiPrefix":"…"}</script>`,
computed from the configured `LIGHTRAG_API_PREFIX` (the in-app `/webui` mount is hardcoded server-side).
Sequence — browser request to a site-prefixed instance:
```
Browser nginx uvicorn SmartStaticFiles
│ │ │ │
│ GET /site01/webui/ │ │
│─────────────────►│ │ │
│ │ GET /webui/ (strips /site01) │
│ │──────────────────────►│ │
│ │ │ get_response("") │
│ │ │───────────────────►│
│ │ │ │ inject
│ │ │ │ window.__LIGHTRAG_CONFIG__
│ │ │ │ = { apiPrefix: "/site01",
│ │ │ │ webuiPrefix: "/site01/webui/" }
│ │ │◄───────────────────│
│ │◄──────────────────────│ │
│◄─────────────────│ │ │
│ index.html with injected runtime config
```
The SPA reads the injected config via `src/lib/runtimeConfig.ts` and uses
it for `axios.baseURL`, `fetch()` template strings, the API-docs iframe,
and in-app links.
---
## One backend variable, that's it
| Variable | Default | Meaning |
| --- | --- | --- |
| `LIGHTRAG_API_PREFIX` | `""` | Reverse-proxy mount prefix. The backend accepts both strip and verbatim forwarding — pick whichever fits your proxy stack. Passed to FastAPI as `root_path`. |
The WebUI is always mounted at `/webui` server-side. `window.__LIGHTRAG_CONFIG__.webuiPrefix` is computed as `LIGHTRAG_API_PREFIX + "/webui/"` and injected for the SPA — you do **not** set it yourself.
There are no longer any frontend `VITE_API_PREFIX` / `VITE_WEBUI_PREFIX` variables. Setting them has no effect (they are ignored by the build).
### Forwarding modes: strip and verbatim both work
After setting `LIGHTRAG_API_PREFIX=/site01`, the backend resolves all routes correctly under either forwarding style:
- **Strip** — proxy removes the prefix, backend sees `/webui/` and `/documents/foo`. The nginx example below uses this style.
- **Verbatim** — proxy forwards the request unchanged, backend sees `/site01/webui/` and `/site01/documents/foo`. The Vite dev flow ([Scenario 2](#scenario-2--simulate-a-site-prefix)) and any non-rewriting proxy use this style.
A small ASGI middleware in `create_app` prepends `root_path` to `scope["path"]` whenever the path does not already include it, so plain Routes and Mount sub-apps (the WebUI's `StaticFiles`) both resolve identically in either mode. You do not need to standardize on one — both coexist on the same backend without configuration toggles.
---
## End-to-end example: two sites behind one nginx
### Instance configuration
`site01.env`:
```bash
HOST=0.0.0.0
PORT=9621
LIGHTRAG_API_PREFIX=/site01
WORKING_DIR=/data/site01/storage
INPUT_DIR=/data/site01/inputs
LIGHTRAG_API_KEY=site01-secret
# … LLM / embedding config …
```
`site02.env`:
```bash
HOST=0.0.0.0
PORT=9621
LIGHTRAG_API_PREFIX=/site02
WORKING_DIR=/data/site02/storage
INPUT_DIR=/data/site02/inputs
LIGHTRAG_API_KEY=site02-secret
# … LLM / embedding config …
```
### docker-compose.yml (one image, two services)
```yaml
services:
site01:
image: ghcr.io/hkuds/lightrag:latest
env_file: site01.env
volumes:
- ./data/site01:/data/site01
ports:
- "127.0.0.1:9621:9621"
site02:
image: ghcr.io/hkuds/lightrag:latest
env_file: site02.env
volumes:
- ./data/site02:/data/site02
ports:
- "127.0.0.1:9622:9621"
```
### nginx config
```nginx
server {
listen 443 ssl http2;
server_name host.example.com;
# site01: strips /site01/ before forwarding
location /site01/ {
proxy_pass http://127.0.0.1:9621/;
proxy_set_header X-Forwarded-Prefix /site01;
proxy_set_header Host $host;
proxy_http_version 1.1;
proxy_set_header Connection "";
}
# site02: strips /site02/ before forwarding
location /site02/ {
proxy_pass http://127.0.0.1:9622/;
proxy_set_header X-Forwarded-Prefix /site02;
proxy_set_header Host $host;
proxy_http_version 1.1;
proxy_set_header Connection "";
}
}
```
Browsing `https://host.example.com/site01/webui/` shows site01's WebUI; `https://host.example.com/site02/webui/` shows site02's. The same Docker image serves both — no per-site build artifact, no rebuild on prefix changes.
### What each layer sees
| Layer | site01 GET /webui/ |
| --- | --- |
| Browser address bar | `https://host.example.com/site01/webui/` |
| nginx receives | `/site01/webui/` |
| nginx forwards | `/webui/` |
| FastAPI `root_path` | `/site01` |
| `app.mount` resolves | `/webui/` |
| Injected `apiPrefix` | `/site01` |
| Injected `webuiPrefix` | `/site01/webui/` |
| Asset URLs in HTML | `./assets/index-abc.js` (resolves to `https://host.example.com/site01/webui/assets/index-abc.js`) |
---
## Single-image Docker recipe
The `Dockerfile` builds the WebUI once, with no prefix:
```dockerfile
FROM oven/bun:1 AS webui-build
WORKDIR /src/lightrag_webui
COPY lightrag_webui/package.json lightrag_webui/bun.lock ./
RUN bun install --frozen-lockfile
COPY lightrag_webui/ ./
COPY lightrag/api/webui/.gitkeep /src/lightrag/api/webui/.gitkeep
RUN bun run build
FROM python:3.11-slim
COPY --from=webui-build /src/lightrag/api/webui /app/lightrag/api/webui
# … rest of the image …
```
Run any number of containers from the same image, each with its own prefix:
```bash
# Plain single-instance, no prefix.
docker run --rm -p 9621:9621 lightrag:latest
# Same image, different prefixes — runtime decides.
docker run --rm -e LIGHTRAG_API_PREFIX=/site01 -p 9621:9621 lightrag:latest
docker run --rm -e LIGHTRAG_API_PREFIX=/site02 -p 9622:9621 lightrag:latest
```
### Kubernetes Ingress equivalent
```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: lightrag-multisite
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /$2
spec:
rules:
- host: host.example.com
http:
paths:
- path: /site01(/|$)(.*)
pathType: ImplementationSpecific
backend:
service:
name: lightrag-site01
port: { number: 9621 }
- path: /site02(/|$)(.*)
pathType: ImplementationSpecific
backend:
service:
name: lightrag-site02
port: { number: 9621 }
```
Backends still set `LIGHTRAG_API_PREFIX=/site01` / `=/site02`.
---
## Local development with `bun run dev`
> **Always open `http://localhost:5173/` — root path, no `/webui`, no `/site01` — regardless of which scenario below you're in.**
>
> Vite's dev server serves the SPA at its own root (`/`) no matter what prefix you configure. `VITE_DEV_API_PREFIX` only affects how the SPA composes API URLs *after* the page is loaded, and which paths the dev proxy intercepts; it does **not** change the URL you type in the address bar. Trying to access `localhost:5173/site01/webui/` works (Vite's SPA fallback returns the same `index.html`), but it's not the canonical entry point and only differs cosmetically in the address bar.
>
> This is the deliberate consequence of `base: './'` in [`vite.config.ts`](../lightrag_webui/vite.config.ts) — the same setting that makes one production build reusable across any number of reverse-proxy mount points. Tying the dev URL to a prefix would force the build to bake the prefix back in.
The dev server mirrors production injection: it serves `index.html` via the same `transformIndexHtml` mechanism the FastAPI server uses at request time, so the SPA reads `window.__LIGHTRAG_CONFIG__` in dev exactly the way it does in prod. Only **two** environment variables matter:
| Variable | Purpose | Where it lives |
| --- | --- | --- |
| `VITE_BACKEND_URL` | Where the dev server forwards proxied API calls. | `lightrag_webui/.env*` |
| `VITE_DEV_API_PREFIX` | Prefix to **simulate** (matches the backend LIGHTRAG_API_PREFIX`). Empty → no prefix. | `lightrag_webui/.env*` |
`VITE_DEV_API_PREFIX` injects `apiPrefix` into `window.__LIGHTRAG_CONFIG__` in the browser, mirroring the backend behavior. It also serves as a prefix for `VITE_API_ENDPOINTS`, ensuring correct access to backend APIs. The matching `webuiPrefix` is derived as `${VITE_DEV_API_PREFIX}/webui/` automatically — you don't need a separate variable for it.
Three scenarios cover everything you'll hit:
### Scenario 1 — single-instance dev (no prefix, no proxy)
The default. Don't set anything beyond the existing `.env.development`.
```
Browser ──► localhost:5173 (Vite) ──► localhost:9621 (backend, no prefix)
```
```bash
# lightrag_webui/.env.development (already in repo as sample)
VITE_BACKEND_URL=http://localhost:9621
VITE_API_PROXY=true
VITE_API_ENDPOINTS=/api,/documents,/graphs,/graph,/health,/query,/docs,/redoc,/openapi.json,/login,/auth-status,/static
# VITE_DEV_API_PREFIX= ← leave empty
```
Run:
```bash
lightrag-server # in one terminal, no LIGHTRAG_API_PREFIX
cd lightrag_webui && bun run dev # in another; open http://localhost:5173/
```
### Scenario 2 — simulate a site prefix
You want the SPA to run under `/site01` (or whatever production prefix). Set `VITE_DEV_API_PREFIX=/site01`. Vite injects the matching `window.__LIGHTRAG_CONFIG__` and registers prefixed proxy keys; SPA requests like `fetch("/site01/documents/foo")` are forwarded verbatim to whatever `VITE_BACKEND_URL` points at. The upstream — local backend or production nginx — is responsible for understanding the prefix.
```
Browser ──► localhost:5173 (Vite + HMR)
│ Vite proxy forwards /site01/* verbatim, no rewrite
VITE_BACKEND_URL ──► upstream that knows /site01
```
`.env.local` (gitignored — your personal dev config):
```bash
VITE_BACKEND_URL=… # see "Where to point VITE_BACKEND_URL" below
VITE_API_PROXY=true
VITE_API_ENDPOINTS=/api,/documents,/graphs,/graph,/health,/query,/docs,/redoc,/openapi.json,/login,/auth-status,/static
VITE_DEV_API_PREFIX=/site01
```
Run `bun run dev` and open **`http://localhost:5173/`**. HMR is purely local — the browser only talks to `localhost:5173` for SPA assets, no WebSocket-upgrade config needed on any upstream.
#### Where to point `VITE_BACKEND_URL`
Two options, picked by where the prefix-aware upstream lives. The Vite-side configuration is identical; only this one variable changes.
**A. Local backend with `LIGHTRAG_API_PREFIX=/site01`** (no nginx anywhere) — the simplest setup, two processes on your laptop. Vite's proxy itself plays the role of the reverse proxy.
```bash
VITE_BACKEND_URL=http://localhost:9621
```
```bash
# Terminal 1
LIGHTRAG_API_PREFIX=/site01 lightrag-server
# Terminal 2
cd lightrag_webui && bun run dev
```
The backend's FastAPI `root_path=/site01` accepts the prefixed form natively (Starlette's `get_route_path()` strips `root_path` from the request path before matching), so no extra rewriting is needed on either side.
**B. Real (remote) backend reached through its production nginx** — useful when the actual backend has data / configs that are painful to reproduce locally. nginx already strips `/site01/` before forwarding to the backend; the dev frontend benefits without changing anything in production.
```bash
VITE_BACKEND_URL=https://prod.example.com # or http://10.0.0.5 — the nginx URL
```
The production nginx and backend stay exactly as they are. The flow becomes:
```
SPA fetch /site01/documents/foo
→ Vite forwards to https://prod.example.com/site01/documents/foo
→ nginx matches /site01/, strips it, forwards /documents/foo to backend
→ backend serves it
```
#### Why `VITE_BACKEND_URL` does **not** include `/site01`
Vite forwards the request path **verbatim** (no rewrite). The browser already emits `/site01/documents/foo`, so the URL Vite sends upstream is `${VITE_BACKEND_URL}/site01/documents/foo`. If you set `VITE_BACKEND_URL=https://prod.example.com/site01` you would get `https://prod.example.com/site01/site01/documents/foo` — a duplicated prefix that both nginx and the backend reject. Always point `VITE_BACKEND_URL` at the upstream **root**.
#### Common pitfalls (mostly relevant to option B)
- **HTTPS upstream + self-signed cert**: Vite's proxy rejects by default. Set `proxy: { ..., secure: false }` in `vite.config.ts` to skip cert validation when targeting a staging proxy with a non-public cert.
- **Auth required**: if the upstream requires `LIGHTRAG_API_KEY`, log in via the dev SPA exactly as you would in prod — the auth token flows through the proxy unchanged.
- **CORS errors**: shouldn't happen because the browser sees same-origin requests to `localhost:5173`. If they appear, check that `changeOrigin: true` is in effect (it is, by default in `vite.config.ts`).
### Quick decision matrix
| Scenario | `VITE_BACKEND_URL` | `VITE_DEV_API_PREFIX` | Upstream the dev proxy talks to | Open in browser |
| --- | --- | --- | --- | --- |
| 1. Default single-instance dev | `http://localhost:9621` | unset | local backend, no prefix | `http://localhost:5173/` |
| 2A. Simulate a prefix locally (no nginx) | `http://localhost:9621` | `/site01` | local backend with `LIGHTRAG_API_PREFIX=/site01` | `http://localhost:5173/` |
| 2B. Hit a real backend through its production nginx | `https://prod.example.com` | `/site01` | remote nginx that already strips `/site01/` | `http://localhost:5173/` |
Rows 2A and 2B share **everything except `VITE_BACKEND_URL`** — the choice is purely "is the prefix-aware upstream on my laptop or in production?".
**The "Open in browser" column is always `http://localhost:5173/` — that is the entry point in every dev scenario.** What changes between rows is where the API traffic ultimately lands; the SPA itself is always served from the dev server's root.
---
## Troubleshooting
### Asset URLs 404 when accessing the WebUI
The base URL must end with `/`. Accessing `/site01/webui` (no trailing slash) makes the browser resolve `./assets/foo.js` against `/site01/`, which 404s. The server already redirects the no-slash form to the
slash form; verify the redirect is reaching nginx (check `X-Forwarded-Prefix` and that nginx uses `proxy_pass http://…/` with the trailing slash).
### `apiPrefix` is empty in `window.__LIGHTRAG_CONFIG__` after deploy
View the page source. If you see the literal placeholder `<!-- __LIGHTRAG_RUNTIME_CONFIG__ -->` instead of an injected `<script>` tag, the request did not go through `SmartStaticFiles` — double-check that `lightrag/api/webui/index.html` exists in the running container and that the WebUI mount succeeded (the server logs `WebUI assets mounted at <path>` at startup).
### `bun run dev` proxy returns 404 with `VITE_DEV_API_PREFIX` set
Confirm the backend is also running with the matching `LIGHTRAG_API_PREFIX`. The dev proxy forwards prefixed paths verbatim; if the backend has no prefix configured, it does not register routes under that path.
### I want to disable the WebUI entirely
Don't build the frontend — `lightrag/api/webui/index.html` will not exist and the server will skip the WebUI mount, redirecting `/` and the WebUI path to `/docs` instead. The runtime-config injection is purely opt-in via the existence of the build artifact.
+316
View File
@@ -0,0 +1,316 @@
# LightRAG Offline Deployment Guide
This guide provides comprehensive instructions for deploying LightRAG in offline environments where internet access is limited or unavailable.
If you deploy LightRAG using Docker, there is no need to refer to this document, as the LightRAG Docker image is pre-configured for offline operation.
> Software packages requiring `transformers`, `torch`, or `cuda` will not be included in the offline dependency group. Consequently, document extraction tools such as Docling, as well as local LLM models like Hugging Face and LMDeploy, are outside the scope of offline installation support. These high-compute-resource-demanding services should not be integrated into LightRAG. Docling will be decoupled and deployed as a standalone service.
## Table of Contents
- [Overview](#overview)
- [Quick Start](#quick-start)
- [Layered Dependencies](#layered-dependencies)
- [Tiktoken Cache Management](#tiktoken-cache-management)
- [Complete Offline Deployment Workflow](#complete-offline-deployment-workflow)
- [Troubleshooting](#troubleshooting)
## Overview
LightRAG uses dynamic package installation (`pipmaster`) for optional features based on file types and configurations. In offline environments, these dynamic installations will fail. This guide shows you how to pre-install all necessary dependencies and cache files.
### What Gets Dynamically Installed?
LightRAG dynamically installs packages for:
- **Storage Backends**: `redis`, `neo4j`, `pymilvus`, `pymongo`, `asyncpg`, `qdrant-client`
- **LLM Providers**: `openai`, `anthropic`, `ollama`, `zhipuai`, `aioboto3`, `voyageai`, `llama-index`, `lmdeploy`, `transformers`, `torch`
- **Tiktoken Models**: BPE encoding models downloaded from OpenAI CDN
**Note**: Document processing dependencies (`pypdf`, `python-docx`, `python-pptx`, `openpyxl`) are now pre-installed with the `api` extras group and no longer require dynamic installation.
## Quick Start
### Option 1: Using pip with Offline Extras
```bash
# Online environment: Install all offline dependencies
pip install lightrag-hku[offline]
# Download tiktoken cache
lightrag-download-cache
# Create offline package
pip download lightrag-hku[offline] -d ./offline-packages
tar -czf lightrag-offline.tar.gz ./offline-packages ~/.tiktoken_cache
# Transfer to offline server
scp lightrag-offline.tar.gz user@offline-server:/path/to/
# Offline environment: Install
tar -xzf lightrag-offline.tar.gz
pip install --no-index --find-links=./offline-packages lightrag-hku[offline]
export TIKTOKEN_CACHE_DIR=~/.tiktoken_cache
```
### Option 2: Using Requirements Files
```bash
# Online environment: Download packages
pip download -r requirements-offline.txt -d ./packages
# Transfer to offline server
tar -czf packages.tar.gz ./packages
scp packages.tar.gz user@offline-server:/path/to/
# Offline environment: Install
tar -xzf packages.tar.gz
pip install --no-index --find-links=./packages -r requirements-offline.txt
```
## Layered Dependencies
LightRAG provides flexible dependency groups for different use cases:
### Available Dependency Groups
| Group | Description | Use Case |
| ----- | ----------- | -------- |
| `api` | API server + document processing | FastAPI server with PDF, DOCX, PPTX, XLSX support |
| `offline-storage` | Storage backends | Redis, Neo4j, MongoDB, PostgreSQL, etc. |
| `offline-llm` | LLM providers | OpenAI, Anthropic, Ollama, etc. |
| `offline` | Complete offline package | API + Storage + LLM (all features) |
**Note**: Document processing (PDF, DOCX, PPTX, XLSX) is included in the `api` extras group. The previous `offline-docs` group has been merged into `api` for better integration.
> Software packages requiring `transformers`, `torch`, or `cuda` will not be included in the offline dependency group.
### Installation Examples
```bash
# Install API with document processing
pip install lightrag-hku[api]
# Install API and storage backends
pip install lightrag-hku[api,offline-storage]
# Install all offline dependencies (recommended for offline deployment)
pip install lightrag-hku[offline]
```
### Using Individual Requirements Files
```bash
# Storage backends only
pip install -r requirements-offline-storage.txt
# LLM providers only
pip install -r requirements-offline-llm.txt
# All offline dependencies
pip install -r requirements-offline.txt
```
## Tiktoken Cache Management
Tiktoken downloads BPE encoding models on first use. In offline environments, you must pre-download these models.
### Using the CLI Command
After installing LightRAG, use the built-in command:
```bash
# Download to default location (see output for exact path)
lightrag-download-cache
# Download to specific directory
lightrag-download-cache --cache-dir ./tiktoken_cache
# Download specific models only
lightrag-download-cache --models gpt-4o-mini gpt-4
```
### Default Models Downloaded
- `gpt-4o-mini` (LightRAG default)
- `gpt-4o`
- `gpt-4`
- `gpt-3.5-turbo`
- `text-embedding-ada-002`
- `text-embedding-3-small`
- `text-embedding-3-large`
### Setting Cache Location in Offline Environment
```bash
# Option 1: Environment variable (temporary)
export TIKTOKEN_CACHE_DIR=/path/to/tiktoken_cache
# Option 2: Add to ~/.bashrc or ~/.zshrc (persistent)
echo 'export TIKTOKEN_CACHE_DIR=~/.tiktoken_cache' >> ~/.bashrc
source ~/.bashrc
# Option 3: Copy to default location
cp -r /path/to/tiktoken_cache ~/.tiktoken_cache/
```
## Complete Offline Deployment Workflow
### Step 1: Prepare in Online Environment
```bash
# 1. Install LightRAG with offline dependencies
pip install lightrag-hku[offline]
# 2. Download tiktoken cache
lightrag-download-cache --cache-dir ./offline_cache/tiktoken
# 3. Download all Python packages
pip download lightrag-hku[offline] -d ./offline_cache/packages
# 4. Create archive for transfer
tar -czf lightrag-offline-complete.tar.gz ./offline_cache
# 5. Verify contents
tar -tzf lightrag-offline-complete.tar.gz | head -20
```
### Step 2: Transfer to Offline Environment
```bash
# Using scp
scp lightrag-offline-complete.tar.gz user@offline-server:/tmp/
# Or using USB/physical media
# Copy lightrag-offline-complete.tar.gz to USB drive
```
### Step 3: Install in Offline Environment
```bash
# 1. Extract archive
cd /tmp
tar -xzf lightrag-offline-complete.tar.gz
# 2. Install Python packages
pip install --no-index \
--find-links=/tmp/offline_cache/packages \
lightrag-hku[offline]
# 3. Set up tiktoken cache
mkdir -p ~/.tiktoken_cache
cp -r /tmp/offline_cache/tiktoken/* ~/.tiktoken_cache/
export TIKTOKEN_CACHE_DIR=~/.tiktoken_cache
# 4. Add to shell profile for persistence
echo 'export TIKTOKEN_CACHE_DIR=~/.tiktoken_cache' >> ~/.bashrc
```
### Step 4: Verify Installation
```bash
# Test Python import
python -c "from lightrag import LightRAG; print('✓ LightRAG imported')"
# Test tiktoken
python -c "from lightrag.utils import TiktokenTokenizer; t = TiktokenTokenizer(); print('✓ Tiktoken working')"
# Test optional dependencies (if installed)
python -c "import redis; print('✓ Redis available')"
```
## Troubleshooting
### Issue: Tiktoken fails with network error
**Problem**: `Unable to load tokenizer for model gpt-4o-mini`
**Solution**:
```bash
# Ensure TIKTOKEN_CACHE_DIR is set
echo $TIKTOKEN_CACHE_DIR
# Verify cache files exist
ls -la ~/.tiktoken_cache/
# If empty, you need to download cache in online environment first
```
### Issue: Dynamic package installation fails
**Problem**: `Error installing package xxx`
**Solution**:
```bash
# Pre-install the specific package you need
# For API with document processing:
pip install lightrag-hku[api]
# For storage backends:
pip install lightrag-hku[offline-storage]
# For LLM providers:
pip install lightrag-hku[offline-llm]
```
### Issue: Missing dependencies at runtime
**Problem**: `ModuleNotFoundError: No module named 'xxx'`
**Solution**:
```bash
# Check what you have installed
pip list | grep -i xxx
# Install missing component
pip install lightrag-hku[offline] # Install all offline deps
```
### Issue: Permission denied on tiktoken cache
**Problem**: `PermissionError: [Errno 13] Permission denied`
**Solution**:
```bash
# Ensure cache directory has correct permissions
chmod 755 ~/.tiktoken_cache
chmod 644 ~/.tiktoken_cache/*
# Or use a user-writable directory
export TIKTOKEN_CACHE_DIR=~/my_tiktoken_cache
mkdir -p ~/my_tiktoken_cache
```
## Best Practices
1. **Test in Online Environment First**: Always test your complete setup in an online environment before going offline.
2. **Keep Cache Updated**: Periodically update your offline cache when new models are released.
3. **Document Your Setup**: Keep notes on which optional dependencies you actually need.
4. **Version Pinning**: Consider pinning specific versions in production:
```bash
pip freeze > requirements-production.txt
```
5. **Minimal Installation**: Only install what you need:
```bash
# If you only need API with document processing
pip install lightrag-hku[api]
# Then manually add specific LLM: pip install openai
```
## Additional Resources
- [LightRAG GitHub Repository](https://github.com/HKUDS/LightRAG)
- [Docker Deployment Guide](./DockerDeployment.md)
- [API Server Documentation](./LightRAG-API-Server.md)
## Support
If you encounter issues not covered in this guide:
1. Check the [GitHub Issues](https://github.com/HKUDS/LightRAG/issues)
2. Review the [project documentation](../README.md)
3. Create a new issue with your offline deployment details
+523
View File
@@ -0,0 +1,523 @@
# Paragraph Semantic 分块策略
## 1. 适用场景与策略选择
### 1.1 P 策略要解决什么问题
Paragraph Semantic Chunking(下文简称 **P 策略**)面向 DOCX、PDF 等具有清晰章节结构的文档。其核心目标是:**让分块边界尽可能对齐文档原生的语义边界**(标题、段落、表格行),而不是仅由 token 长度计数决定切点。
P 策略主要解决以下四类问题:
1. **表格语境断裂**:大表被拆分后,首尾切片容易脱离前置说明、后置解释或中间桥接文字,召回时无法独立理解。
2. **层级信息利用不足**:仅看相邻段落的方法无法利用父标题路径、同级条款之间的关系。
3. **细碎章节尺寸失衡**:规章、标准、合同等文档常包含大量 100~300 token 的细碎条款,若不合并则块过短、语义稀薄;若仅按相邻长度合并又会跨主题污染。
4. **长块二次拆分破坏结构**:章节过长时,常规字符切分会忽略表格行边界和标题层级。
P 策略对**任何能生成 `.blocks.jsonl` sidecar 的 parser**`native` / `mineru` / `docling`)产出的结构化产物有效——三者经统一的 `write_sidecar()` 落盘相同结构的 `.blocks.jsonl`(含 `heading` / `level` / `parent_headings`)。仅 `legacy` 引擎不产出 sidecar;无 sidecar 的输入(legacy 路径或解析失败)会自动降级为 R 策略(见 §6)。
### 1.2 P / R / V 三种策略对比
| 维度 | R 策略(Recursive | V 策略(SemanticVector | P 策略(ParagraphSemantic |
|---|---|---|---|
| 切分依据 | 字符分隔符级联(段落 → 换行 → 中文标点 → 空格 → 字符)+ token 预算 | 句子级 embedding 距离阈值(百分位 / 标准差 / 四分位距 / 梯度)寻找语义断层 | 标题 outline level 与 `parent_headings` + 表格行边界 + 锚点 + 层级感知合并 |
| 块大小控制 | `chunk_token_size` 硬上限 | `chunk_token_size` 仅为 advisory ceiling,超限时通过 R 二次切分 | `target_max` 硬上限 + `target_ideal` 软目标 + 表格阈值 + 尾部吸收阈值多重协同 |
| 表格处理 | 不感知表格,可能在表格中间切断 | 不感知表格 | 表格小于 `table_max` 保持完整;大表按 JSON 行数组 / HTML `<tr>` 行边界切片,并重新包裹为合法 `<table>` |
| 表格上下文 | 依赖窗口偶然覆盖 | 依赖 embedding 距离 | 首切片粘连前置说明、末切片粘连后置解释、连续大表桥接文字双向重叠 |
| 块间重叠 | 全局 `chunk_overlap_token_size` | 不会出现重叠 | 章节边界不会重叠;同章节长正文 fallback 到 R 时按 `CHUNK_P_OVERLAP_SIZE` 重叠;连续大表桥接文字可同时进入前后两个表格块 |
| heading 元数据 | 通常无 | 通常无 | 继承或提升 heading;拆分后追加 `[part n]` 后缀;保留 `parent_headings``level` |
| 嵌入计算开销 | 无 | 高(需对每个句子计算 embedding) | 无 |
| 依赖输入 | 任意文本 | 任意文本 + Embedding 模型 | 必须有 `.blocks.jsonl` sidecar`native` / `mineru` / `docling` 任一引擎产出),否则降级为 R |
### 1.3 怎么选
| 场景 | 推荐 | 理由 |
|---|---|---|
| 章节层级清晰(内容解析引擎需要能够生成Sidecar文件) | **P** | 充分利用标题层级与表格行边界,块边界最贴合语义;避免跨主题污染 |
| 文档以散文 / 评论 / 长篇正文为主,没有明确章节结构 | **V** | 按语义相似度切分能在话题切换点形成自然边界,比字符切分更稳定 |
| 输入是纯文本、Markdown、代码、日志,或追求最低算力开销 | **R** | 无嵌入开销,分隔符级联对中英文混合文本足够稳定 |
| 通用配置(不确定文件类型) | **R** | P 在无 sidecar 时自动降级到 RV 在无 Embedding 模型时也降级到 R |
| 标题样式混乱、正文中大量伪标题的文档 | **R****V** | P 依赖 parser 正确识别标题,标题错乱会导致基础块边界偏移 |
| 单行超大表格或不可解析表格 | 任意 | 三种策略最终都会走字符级 fallback;P 仍保留表格上下文粘连优势 |
## 2. 设计目标与核心不变量
P 策略的全部规则都服务于一个目标:**让块边界对齐文档原生语义边界,并让每个块在召回时能被独立理解**。它把这个目标拆成针对三类场景的具体规则(表格、长块、细碎章节),逐条在 §3 展开。无论规则如何组合,以下四条**重叠不变量**始终成立——它们界定了「哪里允许文字复制、哪里绝不允许」:
1. **章节边界不会重叠**:不同 `.blocks.jsonl` 内容行之间的文本绝不会被复制到对方块里,避免“张冠李戴”。
2. **章节内长正文可重叠**:同一个内容行内拆分的多个片段允许按 `chunk_overlap_token_size` 保留 R 风格 overlap,减少长正文中途切断。
3. **表格之间桥接文字可双向重叠**:唯一的跨段落复制场景,专门服务连续大表的上下文保留。
4. **表格行不互相重叠**:行级切片本身是非重叠的,与 R 的 overlap 概念不同。
### 2.1 规则与效果速览
下表把 §3 的每条规则映射到它达到的效果,以及实现它的内部阶段(阶段名同时是代码注释、日志关键字与排查的交叉引用标识,见 §7.6):
| 分块规则 | 达到的效果 | 实现阶段 | 详见 |
|---|---|---|---|
| 标题级基础块 | 块边界对齐文档原生结构,而非 token 计数 | HeadingBlocks | §3.1 |
| 表格完整性 + 行边界切片 | 表格不被从中间截断,切片仍是合法 `<table>` | TableRowSplit | §3.2 |
| 表格上下文粘连(角色 + 桥接双向重叠) | 表格前置说明、后置解释、桥接文字不脱离表格 | TableRowSplit / TableBridge | §3.3 |
| 表头恢复(切分时补回中段/末段表头) | 被拆表的 `middle`/`last` 切片单独召回时不丢失列名,且不触发长度超限 | HeaderRecovery | §3.3.3 |
| 锚点驱动长块再切分 | 超长章节按语义点切分,保留标题层级 | AnchorSplit | §3.4 |
| 无正文标题粘连 | 父标题不与其子内容失散 | HeadingGlue | §3.5 |
| 层级感知合并 | 细碎条款聚到理想大小,又不跨主题污染 | LevelMerge | §3.6 |
| 重叠规则 | 召回上下文充分,但章节/表格边界不“张冠李戴” | 贯穿全程 | §3.7 |
| 尺寸阈值协同 | 大多数块落在 `[target_ideal, target_max]` | 贯穿全程 | §3.8 |
### 2.2 处理流水线总览
上述规则串成一条以 `.blocks.jsonl` 为输入的流水线(**每个 `type == "content"` 行被视为一个标题级基础块**):
```text
DOCX / PDF / PPTX / …
↓ native(docx) / mineru / docling parser —— 按标题输出基础块,不做 token 拆分
.blocks.jsonl + sidecar (.tables.json / .equations.json / .drawings.json / .blocks.assets/)
↓ TableRowSplit:超大表格按行边界切片并赋予 first/middle/last 角色 → §3.2
↓ HeaderRecovery:切分时把重复表头预扣预算后注入中段/末段切片 → §3.3.3
↓ TableBridge:连续大表之间桥接文字双向重叠 → §3.3
↓ AnchorSplit:锚点驱动的长文本块再切分 → §3.4
↓ PartLabeling[part n] 行级来源追溯编号(按原始内容行独立编号,故在跨行合并之前)
↓ HeadingGlue:无正文标题块向前并入严格更深的子块 → §3.5
↓ LevelMerge:层级感知的双相位合并 → §3.6
最终 chunk 列表
```
## 3. 分块规则与效果
### 3.1 标题级基础块——边界对齐文档原生语义 〔HeadingBlocks
**规则**:每个 `.blocks.jsonl``type == "content"` 行就是一个基础块,即「一条标题下的正文作为一个块」。标题识别完全由 **parser** 完成,**P chunker 自身不扫描文档 body、也不判断标题样式**,更不在解析阶段做 token 阈值拆分。
**达到的效果**:块的初始边界天然落在文档大纲结构上(标题切换处),而不是任意 token 位置;后续所有阶段都在这个语义对齐的基础上做加工。
三个能产出 sidecar 的引擎殊途同归地按标题切出基础块,各自得到 `heading` / `level` / `parent_headings`
- **nativedocx**:读取 `styles.xml`,按 `<w:basedOn>` 建立样式继承链回溯有效 `<w:outlineLvl>`;遍历 `document.xml` 段落沿继承链解析大纲级别,原始 outline level 0~8 映射为内部 `level` 1~9;维护 `current_heading_stack`,遇新标题清理不浅于当前 level 的旧标题并计算 `parent_headings`
- **mineru**:按条目的 `text_level > 0``label``title` / `section_header` 检测标题,用 heading_stack 维护父链。
- **docling**`label="title"` → level 1`label="section_header"``item.level + 1`(默认 level 2),同样维护父链。
三者最终都产出统一的 `IRBlock`(携带 `heading` / `level` / `parent_headings`),并由 `write_sidecar()` 落为相同结构的 `.blocks.jsonl`;表格、公式、图形被提取为单行标签(`<table id="..." format="json">...</table>` 等)写入对应 sidecar。所有可识别标题均触发基础块边界,**不**执行 token 阈值拆分。
P chunker 直接读取 `.blocks.jsonl`,每个 content 行作为后续 TableRowSplit/AnchorSplit 的独立处理单元——这也意味着 `[part n]` 编号按每个原始 content 行**独立重置**(见 §3.4 与 §4.4)。
### 3.2 表格完整性与行边界切片——不从中间截断表格 〔TableRowSplit
**规则**token 数不超过 `table_max` 的表格**保持完整**;只有超过 `table_max` 的表格才切片,且**优先按行边界切**,只有收敛到单行仍无法在上限内表达时,整张表才退化为字符级切分。
**达到的效果**:表格永远不会在「单元格中间」被截断;每个切片都重新包裹为合法的 `<table>` 标签,下游解析与 LLM 阅读都能把它当作表格理解,而非破碎的标记片段。
#### 3.2.1 行边界优先切片
- `format="json"`:按 JSON 顶层行数组切片。
- `format="html"`:按 `<tr>...</tr>` 行切片。
- 未显式标注但内容可嗅探为 JSON / HTML 的表格同样按上述规则处理。
切片前预扣 `<table {attrs}></table>` 外壳 token 开销,使重新包裹后的切片尽量不超过 `table_max`。每个切片重新包裹为合法的 `<table>` 标签,便于下游解析。
#### 3.2.2 行级递归二次切片
若某个行子集重新包裹后仍超过 `table_max`,则在该行子集内继续细分。**当切片收敛到单行、且该单行无法在 `target_max` 内与表头并存(单行内容本身超上限,或没超但加上表头后超上限)时,整张表退化为对原始 `<table>` 文本(其 body 天然含表头)的 R 递归字符切分,并打一条 `logger.warning` 警告**——表头内容随原表文本以纯文本形式保留,绝不被静默丢弃,也不产生「部分 `<table>` 切片 + 部分孤立字符片段」的混合输出。不需要注入表头、且单行本身装得进 `target_max` 的切片仍原样保留为合法 `<table>` 标记。该机制使可被行边界表达的表格内容尽量保留合法表格结构。
#### 3.2.3 末片回吞
若表格末片 token 数低于 `table_min_last`,且与前一切片合并后不超过 `table_max`,则将末片回吞至前一切片,减少无效短表格块。
### 3.3 表格上下文粘连——前后说明、桥接与表头不失散 〔TableRowSplit / TableBridge / HeaderRecovery
**规则**:被切片的表格按「首/中/末」角色与周围段落做差异化粘连;连续两张大表之间的短桥接文字按预算**双向**分配到两侧表格块;丢失表头的中段/末段切片在**切分时**就把该表的重复表头拼回自身的 `<table>`(表头 token 已在切分前预扣进每片上限)。
**达到的效果**:表格的**前置说明**进入首切片块、**后置解释**进入末切片块、**桥接文字**同时作为左表后文与右表前文——任一表格切片被召回时都带着足以独立理解的上下文,不会出现「表格在这、解释在另一块」的断裂。被拆表的中段/末段切片即使脱离了承载表头的首切片,也会把表头行重新拼回自身的 `<table>` 开头,使其单独召回时仍能理解每列含义。
#### 3.3.1 表格切片角色与物理粘连
每个表格切片被赋予内部字段 `table_chunk_role`,并按角色决定与周围段落的粘连方式:
| 角色 | 含义 | 粘连策略 |
|---|---|---|
| `first` | 原始表格的首切片 | 追加到当前累积块尾部,使表格**前置说明**与首切片进入同一块 |
| `middle` | 原始表格的中间切片 | 独立输出,避免与无关正文合并 |
| `last` | 原始表格的末切片 | 作为新累积块起点,使**后置解释**自动追加到末切片之后 |
| `none` | 非表格切片或未拆分的完整表格 | 按普通文本块处理 |
`table_chunk_role` 是内部字段,最终输出不会保留,**但在 LevelMerge 中继续作为合并约束使用**(见 §3.6.1)。
#### 3.3.2 连续大表桥接文字双向重叠 TableBridge
当同一原始内容行中出现「大表 A、短桥接文字、大表 B」的模式,且两张表均被拆分时,桥接文字按上下文预算进行双向分配:
1. 将桥接文字按 token 编码。
2. 计算左侧预算 `prev_budget = min(chunk_overlap_token_size, target_max - 左侧末切片当前 token 数)`
3. 计算右侧预算 `next_budget = min(chunk_overlap_token_size, target_max - 右侧首切片当前 token 数)`
4. **若桥接文字长度同时不超过两侧预算**:左右两个表格边界块都包含**完整桥接文字**。
5. **若桥接文字较长**:前缀进入左侧末切片块,后缀进入右侧首切片块;超出两侧预算的中间段独立成为普通文本块。该中间段块**与左右两侧各保留 `chunk_overlap_token_size` 的 R 风格 overlap**:向左回吞已进入左表块的前缀尾部、向右多含已进入右表块的后缀头部。由于每侧前缀/后缀长度本身就 ≤ overlap 预算,overlap 区间会覆盖整段前缀与后缀,**结果中间段块实际承载完整桥接文字**(桥接文字因此从不被切散,只是其首尾**额外**复制进相邻表格块)。overlap 索引始终夹在桥接文字 token 内,**绝不会把 `<table>` 内容拷进中间段块**。
单侧预算还会被限制到不超过 `chunk_token_size / 2`,避免桥接文字主导整个块。
这与普通相邻 chunk overlap 的差异:
- 普通 overlap 按前后顺序复制字符或 token,与边界类型无关。
- TableBridge 机制以表格切片角色为触发条件,把桥接文字同时作为左表后文上下文和右表前文上下文,避免桥接说明只归属一侧表格或被单独切散后难以召回。
#### 3.3.3 中段/末段切片表头恢复 HeaderRecovery
大表按行边界切片后,表头行只保留在**首切片**内;`middle` / `last` 切片因此丢失列名,单独召回时无法判断每列含义。为此在 **TableRowSplit 切分的同时**,把表头行直接拼回非首切片自身的 `<table>`,使每个切片重新成为带表头的完整表格。
1. **表头来源**:解析期已把每张表的「跨页重复表头」写入同目录的 `.tables.json`(条目字段 `table_header`;**只有真正带重复表头的表才有该字段**)。**该字段按表格自身格式原生存储,使合并单元格语义全程存活**:`format="json"` 表存为 JSON 二维数组字符串(如 `[["H1","H2"]]`),`format="html"` 表存为原始 `<thead>…</thead>` 片段(保留 `rowspan` / `colspan`)。P 按待拆 `<table>` 标签保留的 `id` 关联回对应表条目,取其 `table_header`
2. **预扣预算、切分时注入**:表头的 token 数在切分**之前**就从每片 body 上限中预扣(与 `<table {attrs}></table>` 包裹开销一并扣除)。`_split_table_text` 据此切分,再把表头拼回每个非首切片——`format="json"` 切片把表头行 prepend 到行数组,`format="html"` 切片把存储的原始 `<thead>` 片段 **verbatim 拼回 body 开头**(保留 `rowspan` / `colspan` 合并单元格语义,不再展开为无 span 的网格);若 HTML 切片已自带 `<thead>`(切点落在多行表头内部)则跳过,避免重复。切片原有 `attrs`(含首位的 `id`)保持不变。由于已预扣,**切片含表头后仍 ≤ `target_max`**,硬上限由所有下游阶段自然保证,不存在事后回填导致的超限。首切片自带真实表头行,不重复注入。若某切片收敛到单行后已无法在 `target_max` 内同时容纳行内容与表头(见 §3.2.2),则**整张表退化为 R 递归字符切分(含表头)并打 warning**,绝不保留无表头的孤立切片。
3. **绝不臆造表头**——以下情形均不注入:源表在 `.tables.json` 中没有 `table_header` 字段(无重复表头)、`.tables.json` 缺失/不可读、切片已退化为字符级非 `<table>` 片段(无 `id` 可关联),或表格未发生真正的多片切分。
4. **格式一致性硬校验(损坏即报错)**:注入前先判定 `table_header` 的格式(JSON 二维数组 vs `<thead>` 片段)并与待拆表格自身的 `format` 比对。两者明确冲突(如 HTML 表却拿到 JSON 数组表头,反之亦然)意味着 sidecar 已损坏或张冠李戴,此时 **`_split_table_text` 直接 `raise ValueError` 中断该文档分块**,而非用错位表头产出畸形切片。这是刻意的「损坏即硬报错」语义,与第 3 点「表头缺失则静默跳过」相区分——缺失是可容忍的常态,格式冲突是数据损坏信号。
> 因为表头在切分时即进入切片,被拆表的各切片在 LevelMerge 中被**完全冻结、互不重新合并**(见 §3.6.1)——否则把同一张表的两个切片重新合并会在表中重复一次表头。表头**进入 `content`**,计入该 chunk 的 token 数(表头通常很小);不写入 `heading`。
### 3.4 锚点驱动的长块再切分——按语义点切、保留标题 〔AnchorSplit
**规则**:对 TableRowSplit 后仍超过 `target_max` 的内容块,优先在「短段落锚点」处均衡切分,被选中的锚点晋升为子块新标题;无合格锚点时按「表格优先 → 贪心打包 → 字符切分」三级降级。
**达到的效果**:超长章节不是被硬切在任意 token 位置,而是切在短小的小标题/过渡句这类**自然语义点**上,子块继承可读的标题与父标题路径;同时保证算法**永不丢内容**,且尽量遵守用户配置的块大小上限。
#### 3.4.1 短段落锚点
把内容按段落恢复,选择满足以下条件的段落作为候选锚点:
- 段落不是表格(不以 `<table` 开头)。
- 段落文本长度不超过 `max_anchor_candidate_length`100 字符)。
- 段落不是该块的第一个段落(避免递归无法收敛)。
#### 3.4.2 均衡选锚
根据目标子块数量计算理想切分位置,从候选锚点中选择距离理想位置最近的锚点。被选中的锚点**晋升为后续子块的新 `heading`**,原 heading 写入该子块的 `parent_headings`
#### 3.4.3 无锚点降级
若不存在合格锚点:
1. **表格优先**:若块内仍存在超限表格,优先调用 TableRowSplit 的行边界切片。
2. **贪心打包**:其余文本按段落贪心打包到接近 `target_max`
3. **递归字符切分**:单一过长普通文本段落降级到 R 策略(`chunking_by_recursive_character`),使用 `chunk_overlap_token_size` 保持相邻文本片段的连续性。
无锚点 fallback 路径保证算法**不会丢弃内容**,并尽量遵守用户配置的块大小上限。
### 3.5 无正文标题粘连——父标题不与子内容失散 〔HeadingGlue
**规则**:当一个块是 heading-only(只有标题、没有自己的正文)且紧邻的下一块层级**严格更深**时,把它**向前并入**那个更深的子块,并保留较浅的**父标题**身份;其余情况原样留给 LevelMerge。
**达到的效果**:像 `## 2.4`(无正文)这样的父标题,绝不会被单独切成孤块、再被 LevelMerge 向后吞进上一个同级块 `## 2.3` 而与它真正的子内容 `### 2.4.1` 失散——标题始终随其子内容走,标题路径层级无损。
某些章节只有标题、没有自己的正文(heading-only),例如:
```
## 2.3 结构尺寸及重量 ..... (level 2,有正文)
## 2.4 环境适应性指标 level 2heading-only,无正文)
### 2.4.1 概述 (level 3,有正文)
```
若直接进入 LevelMerge`## 2.4` 会作为一个独立的同级小块,被 Phase A 同级合并或尾部整批吸收**向后吞进上一个同级块 `## 2.3` 的末端**,使这个父标题与它真正的子内容 `### 2.4.1` 失散。
因此在 LevelMerge 之前增加一个前置步骤(`_glue_heading_only_blocks`)。当前块为 heading-only`content` 仅由标题行构成,由 `^#{1,6} +` 判定)时,**仅向前粘连**
- **触发条件**:紧邻的下一块层级**严格更深**(`level` 更大),且其 `table_chunk_role``none``first``first` 即「被切大表的首切片」——子节正文若是超大表格,TableRowSplit 切片后其首个产出块的角色为 `first`;紧跟 heading-only 行的只可能是下一行的首个产出块,故角色必为 `none``first``middle`/`last` 只在同一行表格内部出现)。
- **并入 `first` 切片时保留其角色**:把 `## 2.4` 并入 `first` 切片后,合并块**仍标记 `first`**`## 2.4` 标题正是表格的前置上下文,本就该由 `first` 切片承载)。这样 LevelMerge 不会把它向后吸回 `## 2.3``first` 不可被向后吸收),表格边界保护得以维持;`none` 子块的行为与现状完全一致。
- **动作**:向前并入该子块,保留**父标题**身份(`heading` / `level` / `parent_headings` 取自较浅父块)。即 `## 2.4``### 2.4.1` 绑成一块,标题路径仍以 `2.4` 为主——子块 2.4.1 的 `parent_headings` 本就含 2.4,层级信息无损。链式标题(`# 2``## 2.4``### 2.4.1`)沿链折叠、保留**最浅**身份,直到遇到首个含正文的子块。
- **不做向后粘连**:当下一块**不更深**(更浅/同级标题,或已到末尾)时,该 heading-only 块原样留给 LevelMerge。**不会**把它向后并入更深的前块(如 `### 2.3.9`)——把更浅的 `## 2.4` 标题吞进更深的 L3 块会倒置层级(深吞浅)、压低标题层级。这类孤立标题直接交给 LevelMerge 正常处理。
- **保住硬上限**:子块来自 AnchorSplit、本在 `target_max` 内,但前缀拼入父标题行后可能超限。由于下游无人会重切超限块(LevelMerge 只阻止其继续变大),绑定后超限的块在此重切:**先剥离开头的标题行**,正文按**完整 `target_max`** 切分(使后续不含前缀的正文片保持完整预算),再把标题前缀拼回**第一个正文片**。仅当第一个正文片大到放不下前缀时,才单独对它用缩减后的上限再切——因此大前缀不会把整个子节切得过碎。这样标题始终随真实正文,绝不会被单独切成一个 heading-only 孤块(否则 LevelMerge 又会把它向后吸走),且每个产出片段仍 ≤ `target_max`。(退化情形:当前缀本身已吃满上限——极长标题或极小 `chunk_token_size`——无法保持完整,则整块直接切分、对超长标题行做字符级切分;此时 cap 优先于保持标题完整。)
- **不额外回填前块**:由于 `keep="left"` 保留父块的 `level`,绑定后的整体只是个普通小块(并非锁定独立)。是否并回前块 `2.3` 完全沿用 LevelMerge 既有规则——前块仍 < `target_ideal` 时走同级合并,或整体小于 `small_tail_threshold` 时走尾部吸收(即便前块已饱和也能被吸入),二者都以重测后的真实 token ≤ `target_max` 为界。本前置步骤只保证标题**不脱离其子内容**,并不把整体锁为独立块——因此在尺寸允许时让 `2.3 + 2.4 + 2.4.1` 同块正是期望的防过碎行为。
> 边界歧义:正文行若真以 `#␠` 开头会被误判为标题行——这是 `lightrag/parser/_markdown.py` 已记录并接受的同一启发式歧义,实际语料中概率极低。
### 3.6 层级感知合并——细碎条款聚到理想大小、不跨主题污染 〔LevelMerge
**规则**:**自深层级向浅层级处理**,先合并同级小块(Phase A),再尾部整批吸收,最后允许浅层块吸收深层块(Phase B);每次合并都要同时满足尺寸、表格角色、层级、父标题路径四类约束。
**达到的效果**:大量 100~300 token 的细碎条款被合并到接近 `target_ideal` 的尺寸(块不再过短、语义稀薄),同时**绝不把分属不同主题/不同父章节的相邻小块揉在一起**——既治「块过碎」,又防「跨主题污染」。
#### 3.6.1 合并约束(每次合并都要满足)
1. **尺寸约束**:合并后的真实文本 token 数不超过 `target_max`;已达到 `target_ideal` 的块原则上不继续参与普通同级合并。
2. **角色约束(切片冻结)**:被拆表的所有切片 `first` / `middle` / `last` 一律**锁定独立、不参与任何合并**(既不向后吸收、也不被前块吸收、也不进入尾部整批吸收)。原因:表头已在 TableRowSplit 切分时注入各切片,若把同一张表的两个切片重新合并会在表中重复一次表头(§3.3.3)。表格边界的前后说明已在切分阶段粘进首/末切片,冻结不影响上下文粘连,只放弃小的首/末切片块与无关邻块的事后整合。仅 `none`(普通块/未拆分的完整表)可参与合并。
3. **层级约束**:同级合并在相同 `level` 之间发生;跨级吸收只允许浅层吸收深层,**禁止深层反向吸收浅层**。
4. **父标题路径一致性约束**:避免跨主题污染的关键,按合并方向取严格语义——
- **同级合并(Phase A / 尾部吸收)**:两块 `parent_headings` 必须**完全相等**(真·兄弟)。仅 `level` 相同但父链不同(如 `2.4.1``2.5.1`)不允许合并。
- **跨级吸收(Phase B,浅吸深)**:深块必须是浅块的**后代**——浅块的完整标题路径(`parent_headings` + 自身 `heading`,已剥离 `[part n]`)是深块 `parent_headings` 的前缀。浅块吞并不同分支的深块被禁止。
- `parent_headings` 为空(preamble / 无层级输入)的块视为路径相容,放行(无层级可污染)。
#### 3.6.2 Phase A:同级合并
针对当前 level 的相邻块,当**当前块**低于 `target_ideal`、合并后真实 token ≤ `target_max`,且满足上述约束时,合并为一个块(被吸收的邻块不要求低于 `target_ideal`;反向合并时另要求前块也 < `target_ideal`)。
表格切片角色的方向规则(被拆表切片全部冻结,仅 `none` 可合并):
| 块角色 | 可向后吸收下一块 | 可被前一块吸收 |
|---|:-:|:-:|
| `none` | 是 | 是 |
| `first` | 否 | 否 |
| `middle` | 否 | 否 |
| `last` | 否 | 否 |
#### 3.6.3 尾部整批吸收
若一个**普通块(`none`**且已达到 `target_ideal` 的块后面紧跟一串同级小块,且该串小块总 token 数低于 `small_tail_threshold`、合并后真实 token 数不超过 `target_max`,则**一次性吸收**该串小块。遇到**任何被拆表切片**(`first` / `middle` / `last`),或父标题路径发生分叉时停止;被拆表切片自身也不会发起尾部吸收。
#### 3.6.4 Phase B:跨级吸收
对于 Phase A 后仍未饱和的小块,尝试跨级合并,但仅允许浅层吸收深层:
- 当前块比后一块更浅时,当前块可向后吸收后一块。
- 当前块比前一块更深时,前一浅层块可吸收当前块。
- 反方向合并被禁止。
- 被拆表切片(`first` / `middle` / `last`)在跨级阶段同样冻结,不参与合并;仅 `none` 块参与跨级吸收。
#### 3.6.5 合并后真实 token 复测
由于合并时会插入换行连接符,逐块 token 数相加可能低估合并结果。**每次提交合并前,都要对拼接后的真实文本重新计算 token 数**,确认不超过 `target_max` 后再提交。
合并后保留主块的 `heading`。如果多个 part 片段被合并,最终 heading 保留主块的 part 后缀,**不会**额外拼接多个 part 标签。
### 3.7 重叠规则汇总——哪里重叠、哪里绝不
**规则 + 效果**:P 策略对「文字复制(overlap)」有精确的边界划分,既保证召回上下文充分,又杜绝跨章节/跨表格的“张冠李戴”。把散落在各阶段的重叠行为集中如下:
| 场景 | 是否重叠 | 预算 / 机制 | 服务的效果 |
|---|---|---|---|
| 不同 `.blocks.jsonl` 内容行(章节边界) | **绝不重叠** | —— | 章节边界清晰,不张冠李戴 |
| 同一内容行内长正文 fallback 到 R | 可重叠 | `chunk_overlap_token_size` | 长正文中途切断处保持语义连续 |
| 连续大表之间的桥接文字 | 双向重叠 | 两侧各 `min(overlap, …, target_max/2)` | 桥接说明同时作为左右两表的上下文 |
| 桥接长文本的独立中间段块 | 与左右各重叠 | `chunk_overlap_token_size`(夹在桥接 token 内,绝不含 `<table>`) | 中间段与相邻表格块阅读连续 |
| 表格行级切片之间 | **绝不重叠** | —— | 行切片非重叠,避免重复行 |
### 3.8 尺寸阈值协同——大多数块落在 [ideal, max]
**规则**P 策略的阈值不是固定常量,而是按 `chunk_token_size`(记为 N)动态推导,多个阈值协同控制文本块与表格切片的大小。
**达到的效果**:理想分布下,大多数 chunk 落在 `[target_ideal, target_max]` 区间(N=2000 时约 1500~2000 token);明显偏小的块通常只是锁定独立的 `middle` 表格切片或章节边界尾块。
| 名称 | 计算式 | N = 2000 时取值 | 技术含义 |
|---|---|---:|---|
| `target_max` | N | 2000 | 文本块硬上限 |
| `target_ideal` | 0.75 × N | 1500 | 文本块理想目标,达到此值后停止参与普通同级合并 |
| `table_max` | 0.625 × N | 1250 | 表格触发切片阈值 |
| `table_ideal` | 0.375 × N | 750 | 表格切片理想大小 |
| `table_min_last` | 0.32 × `table_max` | 400 | 表格末片回吞阈值(小于此值且能合并则回吞至前一切片) |
| `small_tail_threshold` | 0.125 × N | 250 | 尾部碎块吸收阈值 |
| `max_anchor_candidate_length` | 固定 | 100 字符 | 长块拆分锚点候选段落长度上限 |
比例约束关系:`table_max < target_ideal < target_max``table_ideal < table_max`。这些比例源自审计模式经验值(`大块 8000、小表 5000、理想表 3000、表格尾块 1600`),现按 `chunk_token_size` 等比缩放。
## 4. 输入与输出
### 4.1 输入
`chunking_by_paragraph_semantic()` 接收以下输入:
| 参数 | 来源 | 说明 |
|---|---|---|
| `content` | `full_docs[doc_id].content` | 拼接后的合并文本,用于 sidecar 缺失时降级 |
| `blocks_path` | `full_docs[doc_id].lightrag_document_path` | `.blocks.jsonl` 路径,是 P 策略的主输入 |
| `.tables.json`(隐式) | 由 `blocks_path` 推导(`<base>.blocks.jsonl``<base>.tables.json` | HeaderRecovery(§3.3.3)的表头数据源;缺失时静默跳过表头注入 |
| `chunk_token_size` | `chunk_options.chunk_token_size` / `CHUNK_P_SIZE` | 目标硬上限 N,默认 `2000` |
| `chunk_overlap_token_size` | `CHUNK_P_OVERLAP_SIZE` / `chunk_overlap_token_size` | 同一内容行内长正文 fallback 与表格桥接预算的上限,默认 `100` |
| `drop_references` | hint `drop_references`(别名 `drop_rf`/ `CHUNK_P_DROP_REFERENCES` | 是否在分块前丢弃文末参考文献块,默认 `False`**入队冻结进 `chunk_options`,并记录到 `doc_status.metadata['chunk_opts']`**(开启时记为 `drop_rf=True` |
| `references_tail_n` | `CHUNK_P_REFERENCES_TAIL_N` | 参考文献块只在文末最后 N 个内容块内才被丢弃(安全窗口),默认 `2`;**运行时实时读 env,不快照、不进 metadata** |
| `references_headings` | `CHUNK_P_REFERENCES_HEADINGS`(竖线分隔) | 参考文献标题前缀,默认 `References\|Bibliography\|参考文献`;英文按单词边界、大小写不敏感匹配,`参考文献` 按前缀匹配;**运行时实时读 env,不快照、不进 metadata** |
| `tokenizer` | LightRAG 已解析好的 tokenizer | 所有 token 计数与文本 overlap 截取的基准 |
P 策略**不接收** `split_by_character` / `split_by_character_only`,因为正常路径由标题和段落结构驱动。
**丢弃参考文献(`drop_references`**:开启后,在 HeadingBlocks 之后、TableRowSplit/AnchorSplit/LevelMerge 之前,对从 `blocks.jsonl` 读出的有序内容块做过滤——**同时满足**「位于最后 `references_tail_n` 个块」且「`heading` 命中参考文献前缀」的块被丢弃。只有开关 `drop_references` 可经 per-file hint 设定并冻结进快照/metadata`references_tail_n` / `references_headings` 是纯 env 调参,由 chunker 每次运行时实时读取当前环境变量——改 env 即可即时影响已入队文档的重跑。若丢弃后没有任何含内容的块剩余,则放弃丢弃并告警,避免产出空文档。
### 4.2 `.blocks.jsonl` 约定
P 策略只处理 `type == "content"` 行。每个内容行通常包含:
- `content`:该标题下的正文文本,可能包含普通段落、`<table ... />` 标签、`<equation ... />` 公式、`<drawing ... />` 图形。
- `heading`:当前标题。
- `parent_headings`:父级标题链。
- `level`:标题级别(1~9,对应原始 outline level 0~8)。
- `positions`:原始段落定位(用于追溯)。
- `blockid`:该内容行的稳定标识(可选)。存在时会被带入最终 chunk 的 `sidecar` 字段,供多模态管线与文档删除按源 block 回溯;缺失时(raw / legacy 输入)输出不含 `sidecar`
parser 保证「一条标题下的正文作为一个基础块」(native 经按标题的结构化切分,mineru / docling 经各自 IR builder),不在解析阶段做 token 阈值拆分。表格保持完整插入到 `content` 中。
### 4.3 输出
最终输出为有序 chunk 列表,每个元素:
```python
{
"tokens": int, # 真实 token 数(合并后会复测)
"content": str, # 块文本(可能包含 <table> 标签)
"chunk_order_index": int, # 块顺序索引
"heading": { # 标题元数据(嵌套 dict,非扁平字段)
"level": int, # 标题层级
"heading": str, # 拆分后追加 [part n] 后缀
"parent_headings": list[str], # 父级标题链,不追加后缀
},
# 可选:仅当输入 .blocks.jsonl 行带 blockid 时出现,
# 供多模态管线与文档删除按源 block 回溯。
"sidecar": {
"type": "block",
"id": str, # 主块 blockidrefs[0]
"refs": [{"type": "block", "id": str}, ...], # 去重后的全部源 blockid
},
}
```
注意:`level``parent_headings` 现已收进 `heading` 嵌套 dict,顶层不再单独提供;`[part n]` 后缀落在 `heading["heading"]` 上。
实现内部还会临时使用 `paragraphs``content``table_chunk_role``blockids` 等字段辅助拆分和合并,但**不会**以这些名字进入最终输出(`blockids` 经转换后体现为 `sidecar`)。
### 4.4 `[part n]` 后缀规则
- 同一个原始 `.blocks.jsonl` 内容行被拆成多个片段时,所有片段的 `heading` 字段追加 `[part 1]``[part 2]`
- 未发生拆分的内容行保持原 heading 不变。
- `parent_headings` 不追加后缀。
- 编号在每个原始内容行内**独立重置**(因 PartLabeling 在跨行合并之前编号,见 §2.2)。
- 旧的 `[表格片段N]` 后缀已统一由 `[part n]` 替代。
## 5. 配置项
| 配置 | 默认 | 说明 |
|---|---|---|
| `CHUNK_P_SIZE` | `2000`(未设时使用 `DEFAULT_CHUNK_P_SIZE`**不**沿用 `CHUNK_SIZE` | P 专用 `chunk_token_size`;段落语义合并需要比全局默认更大的上限,因此独立默认而非回退到 `CHUNK_SIZE` |
| `CHUNK_P_OVERLAP_SIZE` | 未设(沿用 `CHUNK_OVERLAP_SIZE` | P 专用 overlap;只影响同一内容行内长正文 fallback 和表格桥接预算,**不**让表格行级切片互相重叠 |
| `CHUNK_OVERLAP_SIZE` / `LightRAG(chunk_overlap_token_size=…)` | `100` | 未设 P 专用 overlap 时的全局兜底 |
配置语法、优先级链、`addon_params["chunker"]` 运行时改值等详见 [FileProcessingConfiguration-zh.md](FileProcessingConfiguration-zh.md) §3。
`P` 是与引擎正交的 chunking 选项(`后缀:引擎-选项`),可与任何产出 sidecar 的引擎组合。启用 P 的典型 `LIGHTRAG_PARSER` 写法:
```bash
# docx 用 nativepdf 用 mineru,其余支持格式用 docling,都启用 P;不支持的格式回退 legacy-R
LIGHTRAG_PARSER=docx:native-teP,pdf:mineru-iteP,*:docling-iteP,*:legacy-R
CHUNK_P_SIZE=2000
CHUNK_P_OVERLAP_SIZE=100
```
(选项位 `i`/`t`/`e` 分别为图/表/公式分析,`P` 为 chunking 策略,可按需组合。)或在单文件覆盖:
```text
my-proposal.[native-P].docx
paper.[mineru-P].pdf
```
## 6. 降级保护——永不丢内容
**规则 + 效果**:P 策略有多层降级保护,任何结构化能力失效时都退到字符级切分,**保证文档仍产生检索块,不因结构化 sidecar 缺失而被静默丢弃**。
| 触发条件 | 降级行为 |
|---|---|
| `blocks_path` 缺失、不可读、无有效 content 行 | 整体降级到 `chunking_by_recursive_character()`,传入解析出的 `chunk_overlap_token_size` |
| TableRowSplit 中表格无法识别 JSON / HTML 结构 | 该表格调用 R 策略字符切分 |
| TableRowSplit 中单行无法在 `target_max` 内与表头并存(单行内容超上限,或加表头后超上限) | **整张表(含表头)退化为 R 策略字符切分,并打 `logger.warning`**;表头内容随原表文本以纯文本保留 |
| AnchorSplit 中长块没有合格短段落锚点 | 表格优先 → 贪心打包 → 单段落超长再降级 R 字符切分 |
| HeaderRecovery 时 `.tables.json` 缺失/不可读、源表无 `table_header` | 跳过表头注入(该表本就无重复表头,不影响其余分块) |
**重要**:整体 fallback 后不再具备标题层级、表格角色和桥接文字双向重叠能力;但能保证文档仍产生检索块。
## 7. 效果检验与调试
### 7.1 检查 sidecar 是否生成
确认 parser 是否成功产生 `.blocks.jsonl`
```bash
ls -l INPUT/__parsed__/<doc>.<ext>.parsed/<doc>.blocks.jsonl
```
若文件不存在或为空,P 策略会整体降级为 R,不会获得 P 的任何收益。常见原因:
- 未给该格式配置能产出 sidecar 的引擎(如 `LIGHTRAG_PARSER=docx:native-...` / `pdf:mineru-...` / `*:docling-...`),实际走了 `legacy` 路径。
- 解析失败(看 `pipeline_status` 错误条目)。
- 该格式不被所选引擎支持(如 native 仅支持 docx;换用 mineru / docling 覆盖更多格式)。
### 7.2 检查 blocks.jsonl 内容
每行一个 JSON,过滤 `type == "content"` 后查看 heading / level / parent_headings 是否符合预期:
```bash
jq -c 'select(.type=="content") | {level, heading, parent_headings}' \
INPUT/__parsed__/<doc>.<ext>.parsed/<doc>.blocks.jsonl | head
```
若 heading 大量为空或 level 异常,说明 parser 没正确识别标题 —— 此时 P 策略的层级合并和锚点提升都会失效。
### 7.3 检查最终 chunks 是否达到预期效果
查看 `text_chunks` 存储中的 chunk 元数据:
```bash
jq '.[] | {heading, level, tokens, parent_headings}' \
rag_storage/kv_store_text_chunks.json | head -30
```
应观察到以下「规则生效」的迹象:
- 大表前后块的 heading 通常对应 `[part 1]` / `[part n]`(§3.2 表格切片发生)。
- 细碎条款被合并到接近 `target_ideal` 的块(§3.6 层级合并生效)。
- `parent_headings` 在不同章节切换处发生跳变,同章节内保持稳定(§3.1 / §3.6 父路径约束)。
- 大多数 chunk 落在 `[target_ideal, target_max]` 区间(§3.8);明显偏小的块通常是 `middle` 表格切片(锁定独立)或紧靠章节边界的尾块。
若出现大量低于 `small_tail_threshold` 的尾块,可能是:
- 父标题路径一致性约束过严(不同 `parent_headings` 的相邻小块无法合并,§3.6.1)。
- 大量 `middle` 表格切片堆积(表格本身就很大)。
### 7.4 常见问题排查
#### 7.4.1 P 没生效,输出与 R 一致
按以下顺序排查:
1. `full_docs[doc_id].process_options` 是否包含 `P`
2. `full_docs[doc_id].parse_format` 是否为 `lightrag`?若为 `raw`,说明走的是 legacy 路径,P 会自动降级到 R。
3. `lightrag_document_path` 指向的 `.blocks.jsonl` 是否存在、是否非空?
4. 日志中是否有 `paragraph_semantic ... fallback to recursive_character` 字样?
#### 7.4.2 表格被切散、前后说明分离(§3.2 / §3.3 未生效)
- 检查表格是否真的被识别为 `<table format="json">``<table format="html">`(看 `.blocks.jsonl`)。未识别格式的表格只能走字符切分,无法启动 TableRowSplit 的角色机制。
- 检查表格 token 数是否真的超过 `table_max`。低于阈值的表格保持完整,不会触发首/中/末切片。
- 若是连续大表,确认两张表之间的桥接文字是否在**同一 content 行**内 —— 跨 content 行的桥接不参与 B.1 双向重叠。
#### 7.4.3 细碎条款没有被合并(§3.6 未生效)
- 检查相邻条款的 `parent_headings` 是否一致:父标题路径一致性约束会阻止跨主题合并。
- 检查 `level` 是否一致:同级合并要求相同 `level`,跨级吸收只允许浅吸深。
- 检查中间是否插入了 `middle` 表格切片:会阻断尾部整批吸收。
#### 7.4.4 出现单个超过 `target_max` 的块
正常情况下 LevelMerge 的真实 token 复测会拒绝超限合并,但以下场景仍可能出现超限块:
- 单行表格自身超过 `target_max`,无锚点可拆,最终走 R 字符切分但单 chunk 仍超限。
- `enforce_chunk_token_limit_before_embedding` 在 embedding 前会做最后的硬切分,下游不会真把超限 chunk 嵌入向量库。
#### 7.4.5 `[part n]` 后缀异常(§3.4 / §4.4
- 同一原始 content 行拆出多片但只看到一个 `[part 1]`:检查是否在 LevelMerge 中被合并 —— 合并后保留主块的 part 后缀,不拼接多个。
- 出现旧式 `[表格片段N]` 后缀:说明使用了旧版 chunker 输出的数据,新版统一为 `[part n]`,需要重新分块。
### 7.5 日志关键字
P 策略相关日志关键字(用于 `grep` 排查):
- `paragraph_semantic` — 模块入口
- `fallback to recursive_character` — 整体或单段落降级
- `table_chunk_role` — 表格角色相关(§3.3
- `bridge` — TableBridge 桥接文字处理(§3.3.2
- `table_header` / `tables.json` — HeaderRecovery 表头恢复(§3.3.3
- `anchor` — AnchorSplit 锚点选择(§3.4
### 7.6 阶段名 ↔ 规则对照
代码注释、docstring、日志与测试中使用下列**阶段名**作为交叉引用标识。「曾用名」列给出旧版字母编号(仍可能出现在历史 commit / issue / PR 讨论中):
| 阶段名 | 曾用名 | 对应规则 | 章节 |
|---|---|---|---|
| `HeadingBlocks` | Stage A | 标题级基础块 | §3.1 |
| `TableRowSplit` | Stage B | 表格完整性与行边界切片 | §3.2 |
| `HeaderRecovery` | Stage B.2 | 切分时为中段/末段切片补回表头 | §3.3.3 |
| `TableBridge` | Stage B.1 | 连续大表桥接文字双向重叠 | §3.3.2 |
| `AnchorSplit` | Stage C | 锚点驱动的长块再切分 | §3.4 |
| `PartLabeling` | Stage C.1 | `[part n]` 行级来源追溯编号 | §4.4 |
| `HeadingGlue` | Stage D 前置 | 无正文标题粘连 | §3.5 |
| `LevelMerge` | Stage D | 层级感知的双相位合并 | §3.6 |
+518
View File
@@ -0,0 +1,518 @@
# Paragraph Semantic Chunking Strategy
## 1. Use Cases and Strategy Selection
### 1.1 What the P Strategy Solves
Paragraph Semantic Chunking (hereafter the **P strategy**) targets documents with a clear sectional structure such as DOCX and PDF. Its core goal: **align chunk boundaries with the document's native semantic boundaries** (headings, paragraphs, table rows) as much as possible, rather than deciding split points purely by token-length counting.
The P strategy mainly addresses these four problems:
1. **Table context fracture**: after a large table is split, the head/tail slices easily detach from their leading explanation, trailing commentary, or intervening bridge text, becoming impossible to understand on their own at recall time.
2. **Underused hierarchy information**: methods that look only at adjacent paragraphs cannot exploit the parent-heading path or the relationships between same-level clauses.
3. **Imbalanced fine-grained section sizes**: regulations, standards, and contracts often contain many 100300 token fine-grained clauses; leaving them unmerged yields chunks that are too short and semantically thin, while merging purely by adjacent length causes cross-topic pollution.
4. **Long-block re-splitting breaks structure**: when a section is too long, ordinary character splitting ignores table-row boundaries and heading levels.
The P strategy is valid for the structured output of **any parser that can produce a `.blocks.jsonl` sidecar** (`native` / `mineru` / `docling`) — all three persist an identically-structured `.blocks.jsonl` (carrying `heading` / `level` / `parent_headings`) through the shared `write_sidecar()`. Only the `legacy` engine produces no sidecar; input without a sidecar (the legacy path or a parse failure) automatically degrades to the R strategy (see §6).
### 1.2 Comparison of the P / R / V Strategies
| Dimension | R Strategy (Recursive) | V Strategy (SemanticVector) | P Strategy (ParagraphSemantic) |
|---|---|---|---|
| Split basis | Cascaded character separators (paragraph → newline → Chinese punctuation → space → character) + token budget | Sentence-level embedding-distance thresholds (percentile / standard deviation / interquartile range / gradient) to find semantic gaps | Heading outline level and `parent_headings` + table-row boundaries + anchors + hierarchy-aware merging |
| Chunk-size control | `chunk_token_size` hard cap | `chunk_token_size` is only an advisory ceiling; over-limit chunks are re-split via R | `target_max` hard cap + `target_ideal` soft target + table thresholds + tail-absorption threshold acting in concert |
| Table handling | Table-unaware; may cut in the middle of a table | Table-unaware | Tables under `table_max` stay whole; large tables are sliced along JSON row arrays / HTML `<tr>` row boundaries and re-wrapped as legal `<table>` |
| Table context | Relies on a window happening to cover it | Relies on embedding distance | First slice glues the leading explanation, last slice glues the trailing commentary, bridge text between consecutive large tables overlaps bidirectionally |
| Inter-chunk overlap | Global `chunk_overlap_token_size` | No overlap occurs | Section boundaries never overlap; long body text within one section that falls back to R overlaps by `CHUNK_P_OVERLAP_SIZE`; bridge text between consecutive large tables can enter both the preceding and following table chunks |
| heading metadata | Usually none | Usually none | Inherits or promotes heading; appends a `[part n]` suffix after splitting; preserves `parent_headings` and `level` |
| Embedding compute cost | None | High (must embed every sentence) | None |
| Required input | Any text | Any text + an embedding model | Must have a `.blocks.jsonl` sidecar (produced by any of `native` / `mineru` / `docling`), otherwise degrades to R |
### 1.3 How to Choose
| Scenario | Recommended | Reason |
|---|---|---|
| Clear section hierarchy (the content-parsing engine must be able to generate a sidecar file) | **P** | Fully exploits heading levels and table-row boundaries; chunk boundaries hug semantics most closely; avoids cross-topic pollution |
| Document is mostly prose / commentary / long-form body with no clear sectional structure | **V** | Splitting by semantic similarity forms natural boundaries at topic-shift points, more stable than character splitting |
| Input is plain text, Markdown, code, or logs, or you want the lowest compute cost | **R** | No embedding cost; cascaded separators are robust enough for mixed Chinese/English text |
| General configuration (file type uncertain) | **R** | P auto-degrades to R when there is no sidecar; V auto-degrades to R when there is no embedding model |
| Documents with messy heading styles or many pseudo-headings in the body | **R** or **V** | P relies on the parser correctly identifying headings; messy headings shift the basic-block boundaries |
| Single-row huge tables or unparseable tables | Any | All three strategies ultimately fall back to character level; P still keeps its table-context-gluing advantage |
## 2. Design Goals and Core Invariants
Every rule of the P strategy serves one goal: **align chunk boundaries with the document's native semantic boundaries, and make each chunk understandable on its own at recall time**. It decomposes this goal into concrete rules for three scenarios (tables, long blocks, fine-grained sections), expanded one by one in §3. No matter how the rules combine, the following four **overlap invariants** always hold — they delimit "where text duplication is allowed, and where it is never allowed":
1. **Section boundaries never overlap**: text between different `.blocks.jsonl` content lines is never copied into each other's chunks, avoiding mis-attribution.
2. **Long body text within a section may overlap**: multiple fragments split from one content line may keep R-style overlap by `chunk_overlap_token_size`, reducing mid-body cuts.
3. **Bridge text between tables may overlap bidirectionally**: the only cross-paragraph duplication scenario, dedicated to preserving context for consecutive large tables.
4. **Table rows never overlap each other**: row-level slicing is itself non-overlapping, distinct from R's overlap concept.
### 2.1 Rule-to-Effect Overview
The table below maps each rule of §3 to the effect it achieves and the internal stage that implements it (stage names double as the cross-reference identifiers in code comments, log keywords, and debugging — see §7.6):
| Chunking rule | Effect achieved | Implementing stage | See |
|---|---|---|---|
| Heading-level basic chunks | Chunk boundaries align with the document's native structure, not token counts | HeadingBlocks | §3.1 |
| Table integrity + row-boundary slicing | Tables are not cut mid-cell; slices remain legal `<table>` | TableRowSplit | §3.2 |
| Table context gluing (roles + bidirectional bridge overlap) | A table's leading explanation, trailing commentary, and bridge text never detach from the table | TableRowSplit / TableBridge | §3.3 |
| Header recovery (re-attach the header to middle/last slices at split time) | A split table's `middle`/`last` slices keep their column names when recalled alone, without ever exceeding the cap | HeaderRecovery | §3.3.3 |
| Anchor-driven long-block re-splitting | Over-long sections are split at semantic points, preserving heading levels | AnchorSplit | §3.4 |
| Body-less heading gluing | A parent heading is never separated from its child content | HeadingGlue | §3.5 |
| Hierarchy-aware merging | Fine-grained clauses are gathered toward the ideal size without cross-topic pollution | LevelMerge | §3.6 |
| Overlap rules | Sufficient recall context, yet section/table boundaries are never mis-attributed | Throughout | §3.7 |
| Size-threshold coordination | Most chunks land in `[target_ideal, target_max]` | Throughout | §3.8 |
### 2.2 Processing Pipeline Overview
The rules above chain into a pipeline that takes `.blocks.jsonl` as input (**each `type == "content"` line is treated as one heading-level basic block**):
```text
DOCX / PDF / PPTX / …
↓ native(docx) / mineru / docling parser —— emit basic blocks by heading, no token splitting
.blocks.jsonl + sidecar (.tables.json / .equations.json / .drawings.json / .blocks.assets/)
↓ TableRowSplit: slice oversized tables along row boundaries and assign first/middle/last roles → §3.2
↓ HeaderRecovery: budget + re-inject the repeating header into middle/last slices during the split → §3.3.3
↓ TableBridge: bidirectional overlap of bridge text between consecutive large tables → §3.3
↓ AnchorSplit: anchor-driven re-splitting of long text chunks → §3.4
↓ PartLabeling: [part n] line-level provenance numbering (numbered per original content line, hence before cross-line merging)
↓ HeadingGlue: glue body-less heading blocks forward into their strictly-deeper child → §3.5
↓ LevelMerge: hierarchy-aware two-phase merging → §3.6
Final chunk list
```
## 3. Chunking Rules and Effects
### 3.1 Heading-Level Basic Chunks — Aligning Boundaries with Native Semantics HeadingBlocks
**Rule**: each `type == "content"` line of `.blocks.jsonl` is a basic block, i.e. "the body under one heading as one block". Heading identification is performed entirely by the **parser**; **the P chunker itself never scans the document body or judges heading styles**, and it does no token-threshold splitting at parse time.
**Effect**: a chunk's initial boundaries fall naturally on the document outline structure (at heading transitions) rather than at arbitrary token positions; every later stage works on top of this semantically aligned basis.
The three sidecar-producing engines all carve out basic blocks by heading, each obtaining `heading` / `level` / `parent_headings`:
- **native (docx)**: reads `styles.xml`, builds the style-inheritance chain via `<w:basedOn>` to recover the effective `<w:outlineLvl>`; walks the `document.xml` paragraphs resolving the outline level along the chain, mapping original outline levels 08 to internal `level` 19; maintains a `current_heading_stack`, clearing old headings no shallower than the current level and computing `parent_headings` on each new heading.
- **mineru**: detects headings by an item's `text_level > 0` or `label` being `title` / `section_header`, using a heading_stack to maintain the parent chain.
- **docling**: `label="title"` → level 1, `label="section_header"``item.level + 1` (default level 2), likewise maintaining the parent chain.
All three ultimately produce a unified `IRBlock` (carrying `heading` / `level` / `parent_headings`), persisted by `write_sidecar()` into an identically-structured `.blocks.jsonl`; tables, equations, and drawings are extracted as single-line tags (`<table id="..." format="json">...</table>` etc.) written to the corresponding sidecar. Every recognizable heading triggers a basic-block boundary, with **no** token-threshold splitting.
The P chunker reads `.blocks.jsonl` directly, treating each content line as an independent processing unit for the subsequent TableRowSplit/AnchorSplit — which also means `[part n]` numbering is **reset independently** per original content line (see §3.4 and §4.4).
### 3.2 Table Integrity and Row-Boundary Slicing — Never Cut a Table Mid-Cell TableRowSplit
**Rule**: a table whose token count does not exceed `table_max` **stays whole**; only a table exceeding `table_max` is sliced, and it is **sliced along row boundaries first** — the whole table degrades to character-level splitting only when a slice has collapsed to a single row that still cannot be expressed within the limit.
**Effect**: a table is never cut in the "middle of a cell"; every slice is re-wrapped as a legal `<table>` tag, so downstream parsing and LLM reading can interpret it as a table rather than as broken markup fragments.
#### 3.2.1 Row-Boundary-First Slicing
- `format="json"`: slice along the top-level JSON row array.
- `format="html"`: slice along `<tr>...</tr>` rows.
- Tables not explicitly tagged but whose content can be sniffed as JSON / HTML are handled by the same rules.
Before slicing, the `<table {attrs}></table>` wrapper token overhead is debited so that re-wrapped slices stay within `table_max` as much as possible. Each slice is re-wrapped as a legal `<table>` tag for easy downstream parsing.
#### 3.2.2 Row-Level Recursive Re-Slicing
If a row subset still exceeds `table_max` after re-wrapping, it is subdivided further within that row subset. **When a slice has converged to a single row that cannot be kept both `≤ target_max` and header-complete (the row's content itself exceeds the cap, or it fits but leaves no room for the header it would need), the whole table degrades to an R recursive character split of the original `<table>` text (whose body still carries the header), and a `logger.warning` is logged** — the header content survives as plain text along with the original table text and is never silently dropped, nor is a "some `<table>` slices + some orphaned character fragments" mixed output produced. A slice that needs no injected header and whose single row fits `target_max` is still kept whole as legal `<table>` markup. This mechanism keeps table content expressible by row boundaries in legal table structure as much as possible.
#### 3.2.3 Last-Slice Swallow-Back
If a table's last slice has a token count below `table_min_last` and merging it with the previous slice does not exceed `table_max`, the last slice is swallowed back into the previous slice, reducing useless short table chunks.
### 3.3 Table Context Gluing — Leading/Trailing Explanations, Bridges, and Headers Stay Attached TableRowSplit / TableBridge / HeaderRecovery
**Rule**: a sliced table glues to surrounding paragraphs differently by "first/middle/last" role; short bridge text between two consecutive large tables is distributed **bidirectionally** to the table chunks on both sides by budget; middle/last slices that lose the header row get the table's repeating header re-injected into their own `<table>` **during the split** (the header's tokens are budgeted out of each slice's cap before splitting).
**Effect**: a table's **leading explanation** enters the first-slice chunk, its **trailing commentary** enters the last-slice chunk, and **bridge text** serves as both the left table's following context and the right table's preceding context — any table slice carries enough context to be understood on its own at recall, with no "table here, explanation in another chunk" fracture. A split table's middle/last slices, even though detached from the first slice that carries the header, get the header row re-injected back at the top of their own `<table>`, so they remain interpretable per-column when recalled alone.
#### 3.3.1 Table Slice Roles and Physical Gluing
Each table slice is given an internal field `table_chunk_role`, and its role determines how it glues to surrounding paragraphs:
| Role | Meaning | Gluing strategy |
|---|---|---|
| `first` | The first slice of the original table | Appended to the tail of the current accumulation block, so the table's **leading explanation** enters the same chunk as the first slice |
| `middle` | A middle slice of the original table | Emitted standalone, avoiding merger with unrelated body text |
| `last` | The last slice of the original table | Starts a fresh accumulation block, so the **trailing commentary** is automatically appended after the last slice |
| `none` | A non-table slice or an unsplit whole table | Handled as an ordinary text chunk |
`table_chunk_role` is an internal field that does not survive into the final output, **but it continues to serve as a merging constraint in LevelMerge** (see §3.6.1).
#### 3.3.2 Bidirectional Bridge-Text Overlap Between Consecutive Large Tables TableBridge
When the pattern "large table A, short bridge text, large table B" occurs within the same original content line and both tables are split, the bridge text is distributed bidirectionally by context budget:
1. Encode the bridge text into tokens.
2. Compute the left budget `prev_budget = min(chunk_overlap_token_size, target_max - current token count of the left last slice)`.
3. Compute the right budget `next_budget = min(chunk_overlap_token_size, target_max - current token count of the right first slice)`.
4. **If the bridge text fits within both side budgets**: both the left and right table boundary chunks contain the **complete bridge text**.
5. **If the bridge text is longer**: the prefix enters the left last-slice chunk, the suffix enters the right first-slice chunk; the middle segment exceeding both budgets becomes a standalone ordinary text chunk. This middle chunk **keeps `chunk_overlap_token_size` of R-style overlap with each side**: extending left to re-include the tail of the prefix that went into the left table chunk, and right to include the head of the suffix that went into the right table chunk. Because each side's prefix/suffix is itself ≤ the overlap budget, the overlap span covers the entire prefix and suffix, so **the middle chunk in effect carries the complete bridge text** (the bridge is therefore never fragmented; only its head/tail are **additionally** copied into the neighbouring table chunks). The overlap indices always stay within the bridge tokens, so **`<table>` content is never copied into the middle chunk**.
A single side's budget is further capped at no more than `chunk_token_size / 2`, so bridge text can never dominate the whole chunk.
How this differs from ordinary adjacent chunk overlap:
- Ordinary overlap copies characters or tokens in sequence, regardless of boundary type.
- The TableBridge mechanism is triggered by table-slice roles, making the bridge text serve simultaneously as the left table's following context and the right table's preceding context, so a bridging explanation is not attributed to only one side's table nor scattered into a separate chunk that is hard to recall.
#### 3.3.3 Header Recovery for Middle/Last Slices HeaderRecovery
After a large table is sliced along row boundaries, the header row stays only in the **first slice**; `middle` / `last` slices thus lose the column names and cannot tell each column's meaning when recalled on their own. To fix this, **during TableRowSplit** the header row is re-injected into the non-first slices' own `<table>`, so every slice becomes a complete header-bearing table.
1. **Header source**: at parse time each table's "cross-page repeating header" is written into the sibling `.tables.json` (entry field `table_header`; **only tables that genuinely carry a repeating header have this field**). **The field is stored in the table's own format so merged-cell semantics survive end-to-end**: a `format="json"` table stores it as a JSON 2-D array string (e.g. `[["H1","H2"]]`), a `format="html"` table stores it as the raw `<thead>…</thead>` fragment (preserving `rowspan` / `colspan`). P traces back to the matching table entry via the `id` preserved on the to-be-split `<table>` tag and takes its `table_header`.
2. **Budgeted reserve, injected at split time**: the header's token cost is reserved out of each slice's body cap **before** splitting (alongside the `<table {attrs}></table>` wrapper overhead). `_split_table_text` splits against that reduced budget, then re-attaches the header into each non-first slice — a `format="json"` slice prepends the header rows to its row array, a `format="html"` slice splices the stored raw `<thead>` fragment **verbatim at the top of the body** (preserving `rowspan` / `colspan` merged-cell semantics, no longer expanding it into a span-less grid); if an HTML slice already carries a `<thead>` (the cut landed inside a multi-row header) injection is skipped to avoid duplication. The slice keeps its original `attrs` (including the leading `id`). Because the room was reserved, **a slice plus its header still stays `≤ target_max`**, so the hard cap is enforced naturally by every downstream stage — there is no late backfill that can overflow the cap. The first slice keeps its own real header row and is not injected again. If a slice has converged to a single row that can no longer hold both the row content and the header within `target_max` (see §3.2.2), **the whole table degrades to an R recursive character split (header included) and a warning is logged** — never leaving an orphaned header-less slice.
3. **Never fabricate a header** — none of the following are injected: the source table has no `table_header` field in `.tables.json` (no repeating header), `.tables.json` is missing/unreadable, the slice has degraded to a character-level non-`<table>` fragment (no `id` to trace), or the table was not actually split into multiple pieces.
4. **Format-consistency hard check (corruption raises)**: before injecting, the `table_header`'s format (JSON 2-D array vs `<thead>` fragment) is classified and compared against the to-be-split table's own `format`. A clear disagreement (e.g. an HTML table fed a JSON-array header, or vice versa) means the sidecar is corrupted or mis-attributed, so **`_split_table_text` raises `ValueError` and aborts chunking of that document** rather than emitting a malformed slice with a mismatched header. This is a deliberate "corruption is a hard error" semantic, distinct from rule 3's "a missing header is silently skipped" — a missing header is a tolerable normal case, a format mismatch is a data-corruption signal.
> Because the header enters the slice at split time, a split table's slices are **completely frozen against LevelMerge — never re-merged with each other** (see §3.6.1); otherwise re-merging two slices of one table would duplicate the header mid-body. The recovered header **enters `content`** and counts toward the chunk's token total (headers are typically tiny); it is **not** stored on `heading`.
### 3.4 Anchor-Driven Long-Block Re-Splitting — Cut at Semantic Points, Keep Headings AnchorSplit
**Rule**: for content blocks that still exceed `target_max` after TableRowSplit, split in a balanced way at "short-paragraph anchors" first, promoting the chosen anchor to the new heading of the sub-block; when no qualifying anchor exists, fall back through a three-tier "table first → greedy packing → character splitting".
**Effect**: an over-long section is not hard-cut at an arbitrary token position but cut at **natural semantic points** like short subheadings/transition sentences, with sub-blocks inheriting a readable heading and parent-heading path; meanwhile the algorithm **never drops content** and respects the user-configured chunk-size cap as much as possible.
#### 3.4.1 Short-Paragraph Anchors
Recover the content into paragraphs and choose paragraphs satisfying all of the following as candidate anchors:
- The paragraph is not a table (does not start with `<table`).
- The paragraph text length does not exceed `max_anchor_candidate_length` (100 characters).
- The paragraph is not the block's first paragraph (so recursion can converge).
#### 3.4.2 Balanced Anchor Selection
Compute the ideal split positions from the target number of sub-blocks, and from the candidate anchors choose the one nearest the ideal position. The chosen anchor is **promoted to the new `heading`** of the following sub-block, and the original heading is written into that sub-block's `parent_headings`.
#### 3.4.3 No-Anchor Fallback
If no qualifying anchor exists:
1. **Table first**: if an over-limit table still exists within the block, invoke TableRowSplit's row-boundary slicing first.
2. **Greedy packing**: pack the remaining text by paragraph greedily up to near `target_max`.
3. **Recursive character splitting**: a single over-long ordinary text paragraph degrades to the R strategy (`chunking_by_recursive_character`), using `chunk_overlap_token_size` to keep adjacent text fragments continuous.
The no-anchor fallback path guarantees the algorithm **does not discard content** and respects the user-configured chunk-size cap as much as possible.
### 3.5 Body-Less Heading Gluing — A Parent Heading Never Separates From Its Child HeadingGlue
**Rule**: when a block is heading-only (only a heading, no body of its own) and the immediately following block is **strictly deeper**, glue it **forward** into that deeper child block while preserving the shallower **parent-heading** identity; all other cases are left as-is for LevelMerge.
**Effect**: a parent heading like `## 2.4` (no body) is never sliced off as a lone chunk and then absorbed backward by LevelMerge into the previous peer chunk `## 2.3`, becoming separated from its actual child content `### 2.4.1` — the heading always travels with its child content, with no loss of heading-path levels.
Some sections have only a heading and no body of their own (heading-only), e.g.:
```
## 2.3 Structural dimensions and weight ..... (level 2, has body)
## 2.4 Environmental adaptability metrics (level 2, heading-only, no body)
### 2.4.1 Overview (level 3, has body)
```
If this went straight into LevelMerge, `## 2.4` would become an independent same-level small block and, via Phase A peer merging or batched tail absorption, be **absorbed backward into the tail of the previous peer block `## 2.3`**, separating this parent heading from its actual child content `### 2.4.1`.
A pre-pass (`_glue_heading_only_blocks`) is therefore inserted before LevelMerge. When the current block is heading-only (`content` consists solely of heading lines, detected by `^#{1,6} +`), it **glues forward only**:
- **Trigger**: the immediately following block is **strictly deeper** (greater `level`) and its `table_chunk_role` is `none` or `first`. A `first` slice is "the first slice of a split large table" — when a subsection's body is an oversized table, TableRowSplit's first emitted block has role `first`; the block right after a heading-only line can only be the next line's first emitted block, so its role must be `none` or `first` (`middle`/`last` only occur inside the same line's table).
- **Keep the `first` role when gluing into a `first` slice**: after gluing `## 2.4` into a `first` slice, the merged block **stays `first`** (the `## 2.4` heading is exactly the preceding context a `first` slice should carry). LevelMerge then will not absorb it backward into `## 2.3` (a `first` slice cannot be absorbed backward), preserving the table-boundary protection; the `none` sub-block behaves exactly as before.
- **Action**: glue forward into that sub-block, preserving the **parent-heading** identity (`heading` / `level` / `parent_headings` taken from the shallower parent block). That is, `## 2.4` and `### 2.4.1` are bonded into one block, the heading path still centered on `2.4` — sub-block 2.4.1's `parent_headings` already contains 2.4, so hierarchy info is lossless. A chained heading (`# 2``## 2.4``### 2.4.1`) collapses along the chain, keeping the **shallowest** identity, until the first sub-block with body content is reached.
- **No backward gluing**: when the next block is **not** deeper (a shallower/sibling heading, or end of list), the heading-only block is left as-is for LevelMerge. It is **not** glued backward into a deeper previous block (e.g. `### 2.3.9`) — absorbing the shallower `## 2.4` heading into a deeper L3 block would invert the hierarchy (deep-absorbs-shallow) and demote the heading's level. Such an orphan heading is handed directly to LevelMerge's normal handling.
- **Hard cap preserved**: the sub-block came out of AnchorSplit within `target_max`, but prepending the parent heading line(s) can tip it over the cap. Since nothing downstream re-splits an over-limit block (LevelMerge only prevents it from growing further), an over-cap bonded block is re-split here: **first peel off the leading heading line(s)**, split the body at the **full `target_max`** (so later prefix-free body pieces keep the full budget), then glue the heading prefix back onto the **first body piece**. Only when the first body piece is too large to also hold the prefix is it alone re-split with a reduced cap — so a large prefix does not over-fragment the whole subsection. This way the heading always travels with real body content and is never sliced off as a heading-only orphan (which LevelMerge would otherwise absorb backward), and every emitted piece is still ≤ `target_max`. (Degenerate case: when the prefix alone fills the cap — a very long title, or a tiny `chunk_token_size` — it cannot be kept whole, so the whole block is split directly and the oversized heading line is character-split; here the cap wins over heading integrity.)
- **No extra backfill into the previous block**: because `keep="left"` preserves the parent's `level`, the bonded whole is just an ordinary small block (not pinned as independent). Whether it merges back into the previous block `2.3` follows LevelMerge's existing rules entirely — peer merging when `2.3` is still < `target_ideal`, or tail absorption when the whole is below `small_tail_threshold` (which can pull it even into an already-saturated previous block), both bounded by the re-measured real token ≤ `target_max`. This pre-pass only guarantees the heading is **never detached from its child content**; it does not lock the whole as an independent block — so letting `2.3 + 2.4 + 2.4.1` share one chunk when size allows is exactly the intended anti-fragmentation behaviour.
> Boundary ambiguity: a body line that genuinely begins with `#␠` would be misjudged as a heading line — this is the same heuristic ambiguity already documented and accepted in `lightrag/parser/_markdown.py`, with very low probability in real corpora.
### 3.6 Hierarchy-Aware Merging — Gather Fine-Grained Clauses to the Ideal Size Without Cross-Topic Pollution LevelMerge
**Rule**: **process from deeper levels to shallower levels** — first merge same-level small blocks (Phase A), then batch-absorb the tail, finally allow shallow blocks to absorb deep blocks (Phase B); every merge must simultaneously satisfy four constraints: size, table role, level, and parent-heading path.
**Effect**: many 100300 token fine-grained clauses are merged toward near `target_ideal` (chunks are no longer too short and semantically thin), while **never lumping together adjacent small blocks that belong to different topics / different parent sections** — curing both "chunks too small" and "cross-topic pollution".
#### 3.6.1 Merging Constraints (every merge must satisfy)
1. **Size constraint**: the merged real text token count does not exceed `target_max`; a block that has reached `target_ideal` in principle no longer participates in ordinary same-level merging.
2. **Role constraint (slice freeze)**: every split-table slice — `first` / `middle` / `last` — is **locked standalone and never participates in any merge** (it neither absorbs forward, nor is absorbed backward, nor joins batched tail absorption). Reason: the repeating header is injected into each slice at TableRowSplit time, so re-merging two slices of one table would duplicate the header mid-body (§3.3.3). A table's boundary explanations were already glued into the first/last slices during the split, so the freeze does not lose context gluing — it only gives up the post-hoc consolidation of small first/last blocks with unrelated neighbours. Only `none` (an ordinary block / an unsplit whole table) may merge.
3. **Level constraint**: same-level merging happens between equal `level`s; cross-level absorption allows only shallow-absorbs-deep, **forbidding deep from absorbing shallow in reverse**.
4. **Parent-heading-path consistency constraint**: the key to avoiding cross-topic pollution, with strict semantics by merge direction —
- **Same-level merging (Phase A / tail absorption)**: the two blocks' `parent_headings` must be **exactly equal** (true siblings). Blocks with the same `level` but different parent chains (e.g. `2.4.1` and `2.5.1`) may not merge.
- **Cross-level absorption (Phase B, shallow-absorbs-deep)**: the deep block must be a **descendant** of the shallow one — the shallow block's full heading path (`parent_headings` + its own `heading`, with any `[part n]` stripped) must be a prefix of the deep block's `parent_headings`. A shallow block absorbing a deep block from a different branch is forbidden.
- Blocks with empty `parent_headings` (preamble / non-hierarchical input) are treated as path-compatible and allowed (no hierarchy to pollute).
#### 3.6.2 Phase A: Peer Merging
For adjacent blocks at the current level, when **the current block** is below `target_ideal`, the merged real token count is ≤ `target_max`, and the constraints above are satisfied, merge them into one block (the absorbed neighbour need not be below `target_ideal`; a backward merge additionally requires the previous block to be < `target_ideal`).
Directional rules by table-slice role (all split-table slices are frozen; only `none` may merge):
| Block role | Can absorb the next block forward | Can be absorbed by the previous block |
|---|:-:|:-:|
| `none` | Yes | Yes |
| `first` | No | No |
| `middle` | No | No |
| `last` | No | No |
#### 3.6.3 Batched Tail Absorption
If an **ordinary (`none`)** block that has reached `target_ideal` is immediately followed by a run of same-level small blocks whose total token count is below `small_tail_threshold` and whose merged real token count does not exceed `target_max`, then **absorb that run in one shot**. Stop on encountering **any split-table slice** (`first` / `middle` / `last`), or when the parent-heading path diverges; a split-table slice never initiates tail absorption either.
#### 3.6.4 Phase B: Cross-Level Absorption
For small blocks still unsaturated after Phase A, attempt cross-level merging, but allow only shallow-absorbs-deep:
- When the current block is shallower than the next block, the current block may absorb the next block forward.
- When the current block is deeper than the previous block, the previous shallower block may absorb the current block.
- Merging in the reverse direction is forbidden.
- Split-table slices (`first` / `middle` / `last`) are likewise frozen in the cross-level stage and do not participate; only `none` blocks take part in cross-level absorption.
#### 3.6.5 Post-Merge Real-Token Re-Measurement
Because merging inserts a newline joiner, summing per-block token counts may underestimate the merged result. **Before committing every merge, recompute the token count on the joined real text** and confirm it does not exceed `target_max` before committing.
After merging, the main block's `heading` is kept. If multiple part fragments are merged, the final heading keeps the main block's part suffix and does **not** additionally concatenate multiple part tags.
### 3.7 Overlap-Rule Summary — Where It Overlaps, Where It Never Does
**Rule + effect**: the P strategy draws a precise boundary on "text duplication (overlap)", ensuring sufficient recall context while ruling out cross-section/cross-table mis-attribution. The overlap behaviours scattered across stages are gathered here:
| Scenario | Overlaps? | Budget / mechanism | Effect served |
|---|---|---|---|
| Different `.blocks.jsonl` content lines (section boundaries) | **Never overlaps** | —— | Clear section boundaries, no mis-attribution |
| Long body text within one content line falling back to R | May overlap | `chunk_overlap_token_size` | Keeps semantic continuity at a mid-body cut |
| Bridge text between consecutive large tables | Bidirectional overlap | `min(overlap, …, target_max/2)` per side | Bridge explanation serves as context for both the left and right tables |
| Standalone middle chunk of a long bridge | Overlaps each side | `chunk_overlap_token_size` (kept within bridge tokens, never includes `<table>`) | The middle reads continuously with the neighbouring table chunks |
| Between table row-level slices | **Never overlaps** | —— | Row slices are non-overlapping, avoiding duplicate rows |
### 3.8 Size-Threshold Coordination — Most Chunks Land in [ideal, max]
**Rule**: the P strategy's thresholds are not fixed constants but derived dynamically from `chunk_token_size` (denoted N); multiple thresholds act in concert to control the size of text chunks and table slices.
**Effect**: under the ideal distribution, most chunks land in the `[target_ideal, target_max]` interval (about 15002000 tokens when N=2000); noticeably small chunks are usually just standalone-locked `middle` table slices or section-boundary tail blocks.
| Name | Formula | Value at N = 2000 | Technical meaning |
|---|---|---:|---|
| `target_max` | N | 2000 | Text-chunk hard cap |
| `target_ideal` | 0.75 × N | 1500 | Text-chunk ideal target; once reached, stops participating in ordinary same-level merging |
| `table_max` | 0.625 × N | 1250 | Table slicing trigger threshold |
| `table_ideal` | 0.375 × N | 750 | Table slice ideal size |
| `table_min_last` | 0.32 × `table_max` | 400 | Table last-slice swallow-back threshold (below this and mergeable → swallowed back into the previous slice) |
| `small_tail_threshold` | 0.125 × N | 250 | Tail-fragment absorption threshold |
| `max_anchor_candidate_length` | Fixed | 100 chars | Upper bound on candidate anchor-paragraph length for long-block splitting |
Proportional constraints: `table_max < target_ideal < target_max`, `table_ideal < table_max`. These ratios come from audit-mode empirical values (`large block 8000, small table 5000, ideal table 3000, table tail block 1600`) and are now scaled proportionally by `chunk_token_size`.
## 4. Input and Output
### 4.1 Input
`chunking_by_paragraph_semantic()` accepts the following inputs:
| Parameter | Source | Description |
|---|---|---|
| `content` | `full_docs[doc_id].content` | The concatenated merged text, used for degradation when the sidecar is missing |
| `blocks_path` | `full_docs[doc_id].lightrag_document_path` | The `.blocks.jsonl` path, the P strategy's main input |
| `.tables.json` (implicit) | Derived from `blocks_path` (`<base>.blocks.jsonl``<base>.tables.json`) | The header source for HeaderRecovery (§3.3.3); silently skipped when missing |
| `chunk_token_size` | `chunk_options.chunk_token_size` / `CHUNK_P_SIZE` | The target hard cap N, default `2000` |
| `chunk_overlap_token_size` | `CHUNK_P_OVERLAP_SIZE` / `chunk_overlap_token_size` | The cap on long-body fallback within one content line and on the table bridge budget, default `100` |
| `tokenizer` | The tokenizer already resolved by LightRAG | The basis for all token counting and text-overlap extraction |
The P strategy **does not accept** `split_by_character` / `split_by_character_only`, because the normal path is driven by heading and paragraph structure.
### 4.2 `.blocks.jsonl` Convention
The P strategy only processes `type == "content"` lines. Each content line typically contains:
- `content`: the body text under that heading, possibly containing ordinary paragraphs, `<table ... />` tags, `<equation ... />` formulas, `<drawing ... />` graphics.
- `heading`: the current heading.
- `parent_headings`: the parent-heading chain.
- `level`: the heading level (19, corresponding to original outline levels 08).
- `positions`: the original paragraph positions (for traceability).
- `blockid`: a stable identifier of this content line (optional). When present, it is carried into the final chunk's `sidecar` field, letting the multimodal pipeline and document deletion trace back by source block; when absent (raw / legacy input), the output contains no `sidecar`.
The parser guarantees "the body under one heading as one basic block" (native via heading-driven structural splitting, mineru / docling via their respective IR builders), with no token-threshold splitting at parse time. Tables stay whole, inserted into `content`.
### 4.3 Output
The final output is an ordered chunk list, each element:
```python
{
"tokens": int, # Real token count (re-measured after merging)
"content": str, # Chunk text (may contain <table> tags)
"chunk_order_index": int, # Chunk order index
"heading": { # Heading metadata (nested dict, not flat fields)
"level": int, # Heading level
"heading": str, # Gets a [part n] suffix after splitting
"parent_headings": list[str], # Parent-heading chain, no suffix appended
},
# Optional: present only when the input .blocks.jsonl line carries a blockid,
# for the multimodal pipeline and document deletion to trace back by source block.
"sidecar": {
"type": "block",
"id": str, # Main-block blockid (refs[0])
"refs": [{"type": "block", "id": str}, ...], # All source blockids, deduplicated
},
}
```
Note: `level` and `parent_headings` are now folded into the nested `heading` dict and are no longer provided at the top level; the `[part n]` suffix lands on `heading["heading"]`. A middle/last table slice's recovered header does not enter `heading`; it is prepended back into the slice's own `<table>` inside the chunk `content` (§3.3.3).
Internally the implementation also uses temporary fields like `paragraphs`, `content`, `table_chunk_role`, `blockids` to aid splitting and merging, but they do **not** enter the final output under those names (`blockids` is materialized as `sidecar` after conversion).
### 4.4 `[part n]` Suffix Rules
- When one original `.blocks.jsonl` content line is split into multiple fragments, every fragment's `heading` field gets `[part 1]`, `[part 2]`, …
- A content line that was not split keeps its original heading.
- `parent_headings` gets no suffix.
- The numbering is **reset independently** within each original content line (because PartLabeling numbers before cross-line merging, see §2.2).
- The legacy `[表格片段N]` suffix has been unified under `[part n]`.
## 5. Configuration
| Config | Default | Description |
|---|---|---|
| `CHUNK_P_SIZE` | `2000` (uses `DEFAULT_CHUNK_P_SIZE` when unset; does **not** inherit `CHUNK_SIZE`) | P-specific `chunk_token_size`; paragraph-semantic merging needs a larger cap than the global default, hence an independent default rather than falling back to `CHUNK_SIZE` |
| `CHUNK_P_OVERLAP_SIZE` | Unset (inherits `CHUNK_OVERLAP_SIZE`) | P-specific overlap; affects only long-body fallback within one content line and the table bridge budget, and does **not** make table row-level slices overlap each other |
| `CHUNK_OVERLAP_SIZE` / `LightRAG(chunk_overlap_token_size=…)` | `100` | Global fallback when no P-specific overlap is set |
For config syntax, the precedence chain, runtime overrides via `addon_params["chunker"]`, etc., see [FileProcessingConfiguration-zh.md](FileProcessingConfiguration-zh.md) §3.
`P` is a chunking option orthogonal to the engine (`suffix:engine-options`) and can combine with any sidecar-producing engine. A typical `LIGHTRAG_PARSER` setup enabling P:
```bash
# docx uses native, pdf uses mineru, other supported formats use docling, all with P; unsupported formats fall back to legacy-R
LIGHTRAG_PARSER=docx:native-teP,pdf:mineru-iteP,*:docling-iteP,*:legacy-R
CHUNK_P_SIZE=2000
CHUNK_P_OVERLAP_SIZE=100
```
(The option flags `i`/`t`/`e` mean image/table/formula analysis respectively, and `P` is the chunking strategy, combinable as needed.) Or override per file:
```text
my-proposal.[native-P].docx
paper.[mineru-P].pdf
```
## 6. Fallback Protection — Never Drop Content
**Rule + effect**: the P strategy has multi-layer fallback protection; whenever a structural capability fails it retreats to character-level splitting, **guaranteeing the document still produces retrieval chunks and is not silently dropped because a structured sidecar is missing**.
| Trigger | Degradation behaviour |
|---|---|
| `blocks_path` missing, unreadable, or with no valid content lines | Degrade wholesale to `chunking_by_recursive_character()`, passing the resolved `chunk_overlap_token_size` |
| TableRowSplit cannot identify a table's JSON / HTML structure | That table uses R-strategy character splitting |
| TableRowSplit finds a single row that cannot be kept within `target_max` alongside its header (the row content exceeds the cap, or it fits but the header would push it over) | **The whole table (header included) degrades to R-strategy character splitting and a `logger.warning` is logged**; the header content survives as plain text along with the original table text |
| AnchorSplit finds a long block with no qualifying short-paragraph anchor | Table first → greedy packing → degrade to R character splitting if a single paragraph is too long |
| HeaderRecovery finds `.tables.json` missing/unreadable, or the source table has no `table_header` | Skip header injection (that table has no repeating header to begin with; does not affect the rest of chunking) |
**Important**: after a wholesale fallback there is no longer heading hierarchy, table roles, or bidirectional bridge-text overlap; but it guarantees the document still produces retrieval chunks.
## 7. Validating Effects and Debugging
### 7.1 Check Whether the Sidecar Was Generated
Confirm the parser successfully produced `.blocks.jsonl`:
```bash
ls -l INPUT/__parsed__/<doc>.<ext>.parsed/<doc>.blocks.jsonl
```
If the file is missing or empty, the P strategy degrades wholesale to R and gains none of P's benefits. Common causes:
- No sidecar-producing engine was configured for that format (e.g. `LIGHTRAG_PARSER=docx:native-...` / `pdf:mineru-...` / `*:docling-...`), so it actually took the `legacy` path.
- Parse failure (check the `pipeline_status` error entries).
- The format is not supported by the chosen engine (e.g. native supports only docx; switch to mineru / docling to cover more formats).
### 7.2 Check the blocks.jsonl Content
One JSON per line; after filtering `type == "content"`, inspect whether heading / level / parent_headings match expectations:
```bash
jq -c 'select(.type=="content") | {level, heading, parent_headings}' \
INPUT/__parsed__/<doc>.<ext>.parsed/<doc>.blocks.jsonl | head
```
If heading is mostly empty or level is abnormal, the parser did not correctly identify headings — in which case the P strategy's hierarchy merging and anchor promotion both fail.
### 7.3 Check Whether the Final Chunks Achieve the Expected Effects
Inspect the chunk metadata in the `text_chunks` store:
```bash
jq '.[] | {heading, level, tokens, parent_headings}' \
rag_storage/kv_store_text_chunks.json | head -30
```
You should observe the following signs of "rules taking effect":
- The heading of chunks around a large table usually corresponds to `[part 1]` / `[part n]` (§3.2 table slicing happened).
- Fine-grained clauses are merged into chunks near `target_ideal` (§3.6 hierarchy merging took effect).
- `parent_headings` jumps at section transitions and stays stable within a section (§3.1 / §3.6 parent-path constraint).
- Most chunks land in the `[target_ideal, target_max]` interval (§3.8); noticeably small chunks are usually `middle` table slices (locked standalone) or tail blocks right at a section boundary.
If many tail blocks below `small_tail_threshold` appear, it may be:
- The parent-heading-path consistency constraint being too strict (adjacent small blocks with different `parent_headings` cannot merge, §3.6.1).
- A pile-up of `middle` table slices (the table itself is very large).
### 7.4 Common Troubleshooting
#### 7.4.1 P Did Not Take Effect; Output Matches R
Check in this order:
1. Does `full_docs[doc_id].process_options` include `P`?
2. Is `full_docs[doc_id].parse_format` equal to `lightrag`? If it is `raw`, the legacy path was taken and P auto-degrades to R.
3. Does the `.blocks.jsonl` pointed to by `lightrag_document_path` exist and is it non-empty?
4. Are there `paragraph_semantic ... fallback to recursive_character` lines in the log?
#### 7.4.2 Table Scattered, Leading/Trailing Explanation Separated (§3.2 / §3.3 not in effect)
- Check whether the table was actually identified as `<table format="json">` or `<table format="html">` (look at `.blocks.jsonl`). A table of unrecognized format can only go through character splitting and cannot start TableRowSplit's role mechanism.
- Check whether the table's token count actually exceeds `table_max`. A table below the threshold stays whole and does not trigger first/middle/last slicing.
- For consecutive large tables, confirm the bridge text between them is within the **same content line** — a bridge across content lines does not participate in TableBridge bidirectional overlap.
#### 7.4.3 Fine-Grained Clauses Not Merged (§3.6 not in effect)
- Check whether adjacent clauses have consistent `parent_headings`: the parent-heading-path consistency constraint blocks cross-topic merging.
- Check whether `level` is consistent: same-level merging requires equal `level`, and cross-level absorption allows only shallow-absorbs-deep.
- Check whether a `middle` table slice is inserted in between: it blocks batched tail absorption.
#### 7.4.4 A Single Chunk Exceeding `target_max` Appears
Normally LevelMerge's real-token re-measurement rejects over-limit merges, but over-limit chunks can still appear in these scenarios:
- A single-row table itself exceeds `target_max`, with no anchor to split on, ultimately going through R character splitting but a single chunk still exceeds the limit.
- `enforce_chunk_token_limit_before_embedding` does a final hard split before embedding, so downstream never actually embeds an over-limit chunk into the vector store.
#### 7.4.5 `[part n]` Suffix Anomalies (§3.4 / §4.4)
- One original content line was split into multiple pieces but only one `[part 1]` is seen: check whether they were merged in LevelMerge — after merging the main block's part suffix is kept and not concatenated.
- A legacy `[表格片段N]` suffix appears: this means data output by an old chunker version; the new version unifies on `[part n]`, so re-chunk.
### 7.5 Log Keywords
P-strategy-related log keywords (for `grep` troubleshooting):
- `paragraph_semantic` — module entry
- `fallback to recursive_character` — wholesale or single-paragraph degradation
- `table_chunk_role` — table-role related (§3.3)
- `bridge` — TableBridge bridge-text handling (§3.3.2)
- `table_header` / `tables.json` — HeaderRecovery header recovery (§3.3.3)
- `anchor` — AnchorSplit anchor selection (§3.4)
### 7.6 Stage Name ↔ Rule Mapping
The following **stage names** are used as cross-reference identifiers in code comments, docstrings, logs, and tests. The "Former name" column gives the old letter scheme (which may still appear in historical commits / issues / PR discussions):
| Stage name | Former name | Corresponding rule | Section |
|---|---|---|---|
| `HeadingBlocks` | Stage A | Heading-level basic chunks | §3.1 |
| `TableRowSplit` | Stage B | Table integrity and row-boundary slicing | §3.2 |
| `HeaderRecovery` | Stage B.2 | Re-attach the header to middle/last slices during the split | §3.3.3 |
| `TableBridge` | Stage B.1 | Bidirectional bridge-text overlap between consecutive large tables | §3.3.2 |
| `AnchorSplit` | Stage C | Anchor-driven long-block re-splitting | §3.4 |
| `PartLabeling` | Stage C.1 | `[part n]` line-level provenance numbering | §4.4 |
| `HeadingGlue` | Stage D pre-pass | Body-less heading gluing | §3.5 |
| `LevelMerge` | Stage D | Hierarchy-aware two-phase merging | §3.6 |
+129
View File
@@ -0,0 +1,129 @@
# Parser CLI Debuger使用指南
本工具用于本地调试 LightRAG 注册表中的任意内容解析引擎(内置 `native` / `legacy` / `mineru` / `docling`,以及通过 `lightrag.parsers` entry point 注册的第三方引擎,见 `docs/ThirdPartyParser-zh.md`),针对**单个文件**触发与 pipeline worker 相同的注册表派发路径(`get_parser(engine).parse(...)`),并把解析产物(sidecar 与 raw 缓存)输出到一个**扁平目录布局**——与生产入库目录相比,区别仅在于:
- **无 `__parsed__/` 中间层**:产物直接落在指定父目录下,便于查看;
- **源文件不会被归档**:源文件保留在原位置(生产路径会把源文件移到 `<INPUT_DIR>/__parsed__/`);
- **raw 缓存只看目录是否存在**:`mineru` / `docling` 的 raw 目录非空即视为有效,跳过 `_manifest.json` 校验。
其余流程(IR 构建、sidecar 写入、对 `full_docs` 的同步逻辑)与生产入库完全一致,便于排查解析阶段问题。
## 命令格式
```bash
python -m lightrag.parser.cli <input_file> \
--engine <engine> \
[-o <sidecar_parent_dir>] \
[--doc-id <doc-id>] \
[--force-reparse] \
[--preview N]
```
| 参数 | 说明 |
|---|---|
| `input_file` | 待解析的源文件路径(位置参数,必填)。文件必须实际存在。 |
| `--engine` | 必填,可选值来自注册表:内置 `native`(仅 `.docx`,本地解析)/ `legacy`(纯文本抽取,无 sidecar/ `mineru`PDF/办公文档,调 MinerU 服务)/ `docling`PDF/办公文档,调 docling-serve),以及任何已注册的第三方引擎。 |
| `-o / --sidecar-parent-dir` | sidecar 与 raw 目录的父目录,默认 = 源文件所在目录。 |
| `--doc-id` | 自定义文档 ID,默认 `doc-<md5(源文件绝对路径)>`(同一文件多次跑结果稳定)。 |
| `--force-reparse` | 仅对外部服务引擎(`mineru` / `docling` 及继承 `ExternalParserBase` 的第三方引擎)生效:清空 raw 目录、强制重新下载与解析。默认行为是 raw 目录非空即复用。 |
| `--preview N` | 解析完成后打印前 N 个 block 的预览(headings + 内容片段),默认 5;`0` 关闭。对无 sidecar 的引擎(如 `legacy`),改为打印解析文本的前 400 字符。 |
## 输出目录布局
以输入 `./inputs/workspace/sample.pdf` + 默认 sidecar 父目录(即 `./inputs/workspace/`)为例:
```
./inputs/workspace/
├── sample.pdf # 原文件,不动
├── sample.pdf.parsed/ # ← sidecar 输出
│ ├── sample.blocks.jsonl # JSONL:首行 meta,后续每行一个 block
│ ├── sample.blocks.assets/ # native 抽取的图片/媒体资产(若有)
│ ├── sample.tables.json # 表格 sidecar(若 IR 含 tables
│ ├── sample.drawings.json # 图纸/图片 sidecar(若 IR 含 drawings
│ └── sample.equations.json # 公式 sidecar(若 IR 含 equations
└── sample.pdf.<engine>_raw/ # ← mineru / docling 的 raw 缓存(native 无此目录)
├── _manifest.json # 由引擎下载流程写入;CLI 缓存校验不读
└── <bundle files> # 引擎特定 raw 产物(content_list.json / *.json / 资产等)
```
`native` 引擎不产生 raw 目录(解析是本地的,无外部服务参与)。
## 典型用例
### A. 本地解析 `.docx`(零网络依赖)
```bash
python -m lightrag.parser.cli ./inputs/workspace/sample.docx --engine native
# 产出:./inputs/workspace/sample.docx.parsed/ (含 blocks.jsonl + assets
```
### B. 用 MinerU 解析 PDF(首次会下载 raw
```bash
# 第一次:下载 raw bundle + 生成 sidecar
python -m lightrag.parser.cli ./inputs/workspace/sample.pdf --engine mineru
# 第二次(无任何修改):raw 目录非空 → 直接复用 → 仅重建 sidecar,速度快
python -m lightrag.parser.cli ./inputs/workspace/sample.pdf --engine mineru
# 日志会显示: [mineru] raw cache hit doc_id=...
```
### C. 用 Docling 解析 PDF + 复用已有 raw 目录
```bash
# 已有 ./inputs/workspace/sample.pdf.docling_raw/ (含 docling 产物的 JSON 等文件)
python -m lightrag.parser.cli ./inputs/workspace/sample.pdf --engine docling
# CLI 不查 manifest,只要 raw 目录非空就跳过 docling-serve 调用
```
> 注:这是旧 `python -m lightrag.parser.external.docling` 调试入口「从已有 raw 重建 sidecar」场景的等价替代——只需把 raw 目录放到约定位置(`<sidecar_parent>/<source>.docling_raw/`)即可触发缓存命中分支。
### D. 输出到自定义目录
```bash
python -m lightrag.parser.cli ./inputs/workspace/sample.docx \
--engine native -o /tmp/debug_sidecar
# 产出:/tmp/debug_sidecar/sample.docx.parsed/
# 原文件 ./inputs/workspace/sample.docx 不会被移动
```
### E. 强制重新解析(清空 raw 后重新下载)
```bash
python -m lightrag.parser.cli ./inputs/workspace/sample.pdf \
--engine docling --force-reparse
# raw 目录被清空 → 重新调 docling-serve 下载 → 重新生成 sidecar
```
## 环境变量
`mineru` / `docling` 引擎在 **缓存未命中**(首次解析或 `--force-reparse`)时会调用外部服务,所需环境变量与生产入库一致:
- **MinerU**`MINERU_API_MODE``local` / `official`)、`MINERU_API_TOKEN``MINERU_LOCAL_ENDPOINT``MINERU_OFFICIAL_ENDPOINT`,可选 `MINERU_ENGINE_VERSION` / `MINERU_MODEL_VERSION` / `MINERU_POLL_INTERVAL_SECONDS` / `MINERU_MAX_POLLS`
- **Docling**`DOCLING_ENDPOINT`,可选 `DOCLING_ENGINE_VERSION` / `DOCLING_DO_OCR` / `DOCLING_FORCE_OCR` / `DOCLING_OCR_ENGINE` / `DOCLING_OCR_PRESET` / `DOCLING_OCR_LANG` / `DOCLING_DO_FORMULA_ENRICHMENT` / `DOCLING_POLL_INTERVAL_SECONDS` / `DOCLING_MAX_POLLS`
详见 [FileProcessingConfiguration-zh.md](./FileProcessingConfiguration-zh.md)。
**缓存命中**时(raw 目录已存在且非空,且未传 `--force-reparse`)无需任何外部服务环境变量——可用于离线复现解析输出。
## 常见排障
| 现象 | 处理 |
|---|---|
| `error: input file does not exist: ...` | 检查 `input_file` 路径,必须是已存在的文件(不是 raw 目录)。 |
| raw 目录存在但 sidecar 内容仍是旧的 | 默认会**复用** raw 重建 sidecar。如果 raw 本身就过期或被替换,加 `--force-reparse` 清空重下。 |
| MinerU 报 `MINERU_API_TOKEN` 缺失 / Docling 连接 `DOCLING_ENDPOINT` 失败 | 缓存未命中触发了外部服务调用——核对对应环境变量;或确认 raw 目录是否非空(命中缓存时无需服务)。 |
| 源文件被意外移动 | 不应发生:CLI 已 mock 归档函数。若复现请提 issue(可能是 pipeline 内增加了新的归档调用点)。 |
| docling 报 `produced zero blocks` | docling raw 中的主 JSON 内容不可解析或为空。检查 raw 目录的 `*.json` 是否合法。 |
## 与生产解析路径的等价性
本 CLI 与 pipeline parse worker 走同一条注册表派发路径——`get_parser(engine).parse(ParseContext(rag, ...))``rag``lightrag/parser/debug.py` 的轻量替身),因此:
- sidecar 字段、命名、内容格式与生产入库完全一致;
- IR 构建器、`write_sidecar` 调用、`_persist_parsed_full_docs` 行为完全一致;
- 三处差异均由 CLI 内的 `monkey-patch` 实现,**不修改任何生产代码**
1. `parsed_artifact_dir_for` → 返回扁平路径(无 `__parsed__/`);
2. 解析器实例的 `is_bundle_valid` → 「raw 非空即有效」(仅外部服务引擎);
3. `archive_docx_source_after_full_docs_sync` → no-op,保留源文件。
可与 `tests/parser/docx/golden/native_docx/` 下的 golden fixture 对比验证(CLI 不冻结时间戳,比对时排除 `created_at` 等时间字段即可)。
+129
View File
@@ -0,0 +1,129 @@
# Parser CLI Debugger Guide
This tool is used to locally debug any parsing engine in LightRAG's registry (the built-in `native` / `legacy` / `mineru` / `docling`, plus third-party engines registered via the `lightrag.parsers` entry point — see `docs/ThirdPartyParser-zh.md`). It drives the same registry dispatch path as the pipeline worker (`get_parser(engine).parse(...)`) for a **single file** and outputs the parsing artifacts (sidecar and raw cache) into a **flat directory layout**. Compared with the production ingestion directory, the only differences are:
- **No `__parsed__/` intermediate layer**: artifacts land directly under the specified parent directory for easy inspection;
- **The source file is not archived**: the source file stays at its original location (the production path moves the source file to `<INPUT_DIR>/__parsed__/`);
- **Raw cache validity only checks directory existence**: any non-empty `mineru` / `docling` raw directory is considered valid, skipping `_manifest.json` validation.
The rest of the flow (IR construction, sidecar writing, `full_docs` synchronization logic) is identical to production ingestion, making it convenient for troubleshooting parsing-stage issues.
## Command Format
```bash
python -m lightrag.parser.cli <input_file> \
--engine <engine> \
[-o <sidecar_parent_dir>] \
[--doc-id <doc-id>] \
[--force-reparse] \
[--preview N]
```
| Argument | Description |
|---|---|
| `input_file` | Path to the source file to parse (positional argument, required). The file must actually exist. |
| `--engine` | Required; choices come from the registry: built-in `native` (only `.docx`, local parsing) / `legacy` (plain-text extraction, no sidecar) / `mineru` (PDF/Office documents, calls MinerU service) / `docling` (PDF/Office documents, calls docling-serve), plus any registered third-party engine. |
| `-o / --sidecar-parent-dir` | Parent directory of the sidecar and raw directories. Defaults to the directory containing the source file. |
| `--doc-id` | Custom document ID. Defaults to `doc-<md5(absolute path of source file)>` (stable across multiple runs on the same file). |
| `--force-reparse` | Effective only for external-service engines (`mineru` / `docling` and third-party engines subclassing `ExternalParserBase`): clears the raw directory and forces re-download and re-parse. By default, a non-empty raw directory is reused. |
| `--preview N` | After parsing completes, prints a preview of the first N blocks (headings + content snippets). Default 5; `0` disables it. For engines without a sidecar (e.g. `legacy`), prints the first 400 characters of the extracted text instead. |
## Output Directory Layout
Taking input `./inputs/workspace/sample.pdf` + the default sidecar parent directory (i.e., `./inputs/workspace/`) as an example:
```
./inputs/workspace/
├── sample.pdf # original file, untouched
├── sample.pdf.parsed/ # ← sidecar output
│ ├── sample.blocks.jsonl # JSONL: first line is meta, each subsequent line is a block
│ ├── sample.blocks.assets/ # image/media assets extracted by native (if any)
│ ├── sample.tables.json # table sidecar (if IR contains tables)
│ ├── sample.drawings.json # drawing/image sidecar (if IR contains drawings)
│ └── sample.equations.json # equation sidecar (if IR contains equations)
└── sample.pdf.<engine>_raw/ # ← raw cache for mineru / docling (native has no such directory)
├── _manifest.json # written by the engine download flow; not read by CLI cache validation
└── <bundle files> # engine-specific raw artifacts (content_list.json / *.json / assets, etc.)
```
The `native` engine does not produce a raw directory (parsing is local, with no external service involved).
## Typical Use Cases
### A. Locally parse a `.docx` (zero network dependency)
```bash
python -m lightrag.parser.cli ./inputs/workspace/sample.docx --engine native
# Output: ./inputs/workspace/sample.docx.parsed/ (contains blocks.jsonl + assets)
```
### B. Parse a PDF with MinerU (raw will be downloaded on first run)
```bash
# First run: download raw bundle + generate sidecar
python -m lightrag.parser.cli ./inputs/workspace/sample.pdf --engine mineru
# Second run (no changes): raw directory non-empty → reused directly → only regenerate sidecar, fast
python -m lightrag.parser.cli ./inputs/workspace/sample.pdf --engine mineru
# The log will show: [mineru] raw cache hit doc_id=...
```
### C. Parse a PDF with Docling + reuse an existing raw directory
```bash
# Existing ./inputs/workspace/sample.pdf.docling_raw/ (contains docling's JSON output, etc.)
python -m lightrag.parser.cli ./inputs/workspace/sample.pdf --engine docling
# The CLI does not check the manifest; as long as the raw directory is non-empty, the docling-serve call is skipped
```
> Note: this is the equivalent replacement for the "rebuild sidecar from an existing raw directory" scenario that used to live in the legacy `python -m lightrag.parser.external.docling` debug entry point — just place the raw directory at the agreed location (`<sidecar_parent>/<source>.docling_raw/`) to trigger the cache-hit branch.
### D. Output to a custom directory
```bash
python -m lightrag.parser.cli ./inputs/workspace/sample.docx \
--engine native -o /tmp/debug_sidecar
# Output: /tmp/debug_sidecar/sample.docx.parsed/
# The source file ./inputs/workspace/sample.docx is not moved
```
### E. Force re-parse (clear raw and re-download)
```bash
python -m lightrag.parser.cli ./inputs/workspace/sample.pdf \
--engine docling --force-reparse
# raw directory is cleared → docling-serve is called again to download → sidecar regenerated
```
## Environment Variables
The `mineru` / `docling` engines call external services when the **cache misses** (first parse or `--force-reparse`); the required environment variables are identical to production ingestion:
- **MinerU**: `MINERU_API_MODE` (`local` / `official`), `MINERU_API_TOKEN`, `MINERU_LOCAL_ENDPOINT` or `MINERU_OFFICIAL_ENDPOINT`, optional `MINERU_ENGINE_VERSION` / `MINERU_MODEL_VERSION` / `MINERU_POLL_INTERVAL_SECONDS` / `MINERU_MAX_POLLS`.
- **Docling**: `DOCLING_ENDPOINT`, optional `DOCLING_ENGINE_VERSION` / `DOCLING_DO_OCR` / `DOCLING_FORCE_OCR` / `DOCLING_OCR_ENGINE` / `DOCLING_OCR_PRESET` / `DOCLING_OCR_LANG` / `DOCLING_DO_FORMULA_ENRICHMENT` / `DOCLING_POLL_INTERVAL_SECONDS` / `DOCLING_MAX_POLLS`.
See [FileProcessingConfiguration.md](./FileProcessingConfiguration.md) for details.
When the **cache is hit** (the raw directory already exists and is non-empty, and `--force-reparse` is not passed), no external service environment variables are needed — this can be used to offline-reproduce parsing output.
## Common Troubleshooting
| Symptom | Action |
|---|---|
| `error: input file does not exist: ...` | Check the `input_file` path; it must be an existing file (not a raw directory). |
| Raw directory exists but sidecar content is still stale | The default behavior is to **reuse** raw and regenerate sidecar. If the raw itself is outdated or has been replaced, add `--force-reparse` to clear and re-download. |
| MinerU reports `MINERU_API_TOKEN` missing / Docling fails to connect to `DOCLING_ENDPOINT` | A cache miss triggered an external service call — verify the corresponding environment variables; or confirm whether the raw directory is non-empty (no service needed when the cache hits). |
| Source file is unexpectedly moved | Should not happen: the CLI has mocked the archive function. If reproducible, please file an issue (a new archive call site may have been added in the pipeline). |
| docling reports `produced zero blocks` | The main JSON content in docling raw is unparseable or empty. Check whether the `*.json` files in the raw directory are valid. |
## Equivalence with the Production Parsing Path
This CLI drives the same registry dispatch path as the pipeline parse worker — `get_parser(engine).parse(ParseContext(rag, ...))` (with `rag` being the lightweight stand-in in `lightrag/parser/debug.py`), so:
- The sidecar fields, naming, and content format are identical to production ingestion;
- The IR builders, `write_sidecar` calls, and `_persist_parsed_full_docs` behavior are identical;
- All three differences are implemented via `monkey-patch` inside the CLI — **no production code is modified**:
1. `parsed_artifact_dir_for` → returns the flat path (no `__parsed__/`);
2. the resolved parser instance's `is_bundle_valid` → "raw is valid if non-empty" (external-service engines only);
3. `archive_docx_source_after_full_docs_sync` → no-op, source file preserved.
Results can be cross-validated against golden fixtures under `tests/parser/docx/golden/native_docx/` (the CLI does not freeze timestamps; just exclude time fields such as `created_at` when comparing).
File diff suppressed because it is too large Load Diff
+235
View File
@@ -0,0 +1,235 @@
# Evaluation Result Reproduce
## Dataset
The dataset used in LightRAG can be downloaded from [TommyChien/UltraDomain](https://huggingface.co/datasets/TommyChien/UltraDomain).
## Generate Query
LightRAG uses the following prompt to generate high-level queries, with the corresponding code in `examples/generate_query.py`.
**Prompt**
```
Given the following description of a dataset:
{description}
Please identify 5 potential users who would engage with this dataset. For each user, list 5 tasks they would perform with this dataset. Then, for each (user, task) combination, generate 5 questions that require a high-level understanding of the entire dataset.
Output the results in the following structure:
- User 1: [user description]
- Task 1: [task description]
- Question 1:
- Question 2:
- Question 3:
- Question 4:
- Question 5:
- Task 2: [task description]
...
- Task 5: [task description]
- User 2: [user description]
...
- User 5: [user description]
...
```
## Batch Eval
To evaluate the performance of two RAG systems on high-level queries, LightRAG uses the following prompt, with the specific code available in `reproduce/batch_eval.py`.
**Prompt**
```
---Role---
You are an expert tasked with evaluating two answers to the same question based on three criteria: **Comprehensiveness**, **Diversity**, and **Empowerment**.
---Goal---
You will evaluate two answers to the same question based on three criteria: **Comprehensiveness**, **Diversity**, and **Empowerment**.
- **Comprehensiveness**: How much detail does the answer provide to cover all aspects and details of the question?
- **Diversity**: How varied and rich is the answer in providing different perspectives and insights on the question?
- **Empowerment**: How well does the answer help the reader understand and make informed judgments about the topic?
For each criterion, choose the better answer (either Answer 1 or Answer 2) and explain why. Then, select an overall winner based on these three categories.
Here is the question:
{query}
Here are the two answers:
**Answer 1:**
{answer1}
**Answer 2:**
{answer2}
Evaluate both answers using the three criteria listed above and provide detailed explanations for each criterion.
Output your evaluation in the following JSON format:
{{
"Comprehensiveness": {{
"Winner": "[Answer 1 or Answer 2]",
"Explanation": "[Provide explanation here]"
}},
"Empowerment": {{
"Winner": "[Answer 1 or Answer 2]",
"Explanation": "[Provide explanation here]"
}},
"Overall Winner": {{
"Winner": "[Answer 1 or Answer 2]",
"Explanation": "[Summarize why this answer is the overall winner based on the three criteria]"
}}
}}
```
## Overall Performance Table
||**Agriculture**||**CS**||**Legal**||**Mix**||
|----------------------|---------------|------------|------|------------|---------|------------|-------|------------|
||NaiveRAG|**LightRAG**|NaiveRAG|**LightRAG**|NaiveRAG|**LightRAG**|NaiveRAG|**LightRAG**|
|**Comprehensiveness**|32.4%|**67.6%**|38.4%|**61.6%**|16.4%|**83.6%**|38.8%|**61.2%**|
|**Diversity**|23.6%|**76.4%**|38.0%|**62.0%**|13.6%|**86.4%**|32.4%|**67.6%**|
|**Empowerment**|32.4%|**67.6%**|38.8%|**61.2%**|16.4%|**83.6%**|42.8%|**57.2%**|
|**Overall**|32.4%|**67.6%**|38.8%|**61.2%**|15.2%|**84.8%**|40.0%|**60.0%**|
||RQ-RAG|**LightRAG**|RQ-RAG|**LightRAG**|RQ-RAG|**LightRAG**|RQ-RAG|**LightRAG**|
|**Comprehensiveness**|31.6%|**68.4%**|38.8%|**61.2%**|15.2%|**84.8%**|39.2%|**60.8%**|
|**Diversity**|29.2%|**70.8%**|39.2%|**60.8%**|11.6%|**88.4%**|30.8%|**69.2%**|
|**Empowerment**|31.6%|**68.4%**|36.4%|**63.6%**|15.2%|**84.8%**|42.4%|**57.6%**|
|**Overall**|32.4%|**67.6%**|38.0%|**62.0%**|14.4%|**85.6%**|40.0%|**60.0%**|
||HyDE|**LightRAG**|HyDE|**LightRAG**|HyDE|**LightRAG**|HyDE|**LightRAG**|
|**Comprehensiveness**|26.0%|**74.0%**|41.6%|**58.4%**|26.8%|**73.2%**|40.4%|**59.6%**|
|**Diversity**|24.0%|**76.0%**|38.8%|**61.2%**|20.0%|**80.0%**|32.4%|**67.6%**|
|**Empowerment**|25.2%|**74.8%**|40.8%|**59.2%**|26.0%|**74.0%**|46.0%|**54.0%**|
|**Overall**|24.8%|**75.2%**|41.6%|**58.4%**|26.4%|**73.6%**|42.4%|**57.6%**|
||GraphRAG|**LightRAG**|GraphRAG|**LightRAG**|GraphRAG|**LightRAG**|GraphRAG|**LightRAG**|
|**Comprehensiveness**|45.6%|**54.4%**|48.4%|**51.6%**|48.4%|**51.6%**|**50.4%**|49.6%|
|**Diversity**|22.8%|**77.2%**|40.8%|**59.2%**|26.4%|**73.6%**|36.0%|**64.0%**|
|**Empowerment**|41.2%|**58.8%**|45.2%|**54.8%**|43.6%|**56.4%**|**50.8%**|49.2%|
|**Overall**|45.2%|**54.8%**|48.0%|**52.0%**|47.2%|**52.8%**|**50.4%**|49.6%|
## Reproduce
All the code can be found in the `./reproduce` directory.
### Step-0 Extract Unique Contexts
First, extract unique contexts from the datasets.
**Code**
```python
def extract_unique_contexts(input_directory, output_directory):
os.makedirs(output_directory, exist_ok=True)
jsonl_files = glob.glob(os.path.join(input_directory, '*.jsonl'))
print(f"Found {len(jsonl_files)} JSONL files.")
for file_path in jsonl_files:
filename = os.path.basename(file_path)
name, ext = os.path.splitext(filename)
output_filename = f"{name}_unique_contexts.json"
output_path = os.path.join(output_directory, output_filename)
unique_contexts_dict = {}
print(f"Processing file: {filename}")
try:
with open(file_path, 'r', encoding='utf-8') as infile:
for line_number, line in enumerate(infile, start=1):
line = line.strip()
if not line:
continue
try:
json_obj = json.loads(line)
context = json_obj.get('context')
if context and context not in unique_contexts_dict:
unique_contexts_dict[context] = None
except json.JSONDecodeError as e:
print(f"JSON decoding error in file {filename} at line {line_number}: {e}")
except FileNotFoundError:
print(f"File not found: {filename}")
continue
except Exception as e:
print(f"An error occurred while processing file {filename}: {e}")
continue
unique_contexts_list = list(unique_contexts_dict.keys())
print(f"There are {len(unique_contexts_list)} unique `context` entries in the file {filename}.")
try:
with open(output_path, 'w', encoding='utf-8') as outfile:
json.dump(unique_contexts_list, outfile, ensure_ascii=False, indent=4)
print(f"Unique `context` entries have been saved to: {output_filename}")
except Exception as e:
print(f"An error occurred while saving to the file {output_filename}: {e}")
print("All files have been processed.")
```
### Step-1 Insert Contexts
Insert the extracted contexts into the LightRAG system.
**Code**
```python
def insert_text(rag, file_path):
with open(file_path, mode='r') as f:
unique_contexts = json.load(f)
retries = 0
max_retries = 3
while retries < max_retries:
try:
rag.insert(unique_contexts)
break
except Exception as e:
retries += 1
print(f"Insertion failed, retrying ({retries}/{max_retries}), error: {e}")
time.sleep(10)
if retries == max_retries:
print("Insertion failed after exceeding the maximum number of retries")
```
### Step-2 Generate Queries
Extract tokens from the first and second half of each context, then combine them as dataset descriptions to generate queries.
**Code**
```python
tokenizer = GPT2Tokenizer.from_pretrained('gpt2')
def get_summary(context, tot_tokens=2000):
tokens = tokenizer.tokenize(context)
half_tokens = tot_tokens // 2
start_tokens = tokens[1000:1000 + half_tokens]
end_tokens = tokens[-(1000 + half_tokens):1000]
summary_tokens = start_tokens + end_tokens
summary = tokenizer.convert_tokens_to_string(summary_tokens)
return summary
```
### Step-3 Query
Extract and query LightRAG with the queries generated in Step-2.
**Code**
```python
def extract_queries(file_path):
with open(file_path, 'r') as f:
data = f.read()
data = data.replace('**', '')
queries = re.findall(r'- Question \d+: (.+)', data)
return queries
```
+376
View File
@@ -0,0 +1,376 @@
# 基于角色的 LLM/VLM 配置指南
LightRAG 支持为不同处理阶段配置不同的 LLM 或 VLM。这个机制适合把低成本模型用于抽取,把更强模型用于最终回答,或为多模态分析单独指定视觉语言模型。
## 角色说明
当前支持四个角色:
| 角色 | 用途 |
| --- | --- |
| `EXTRACT` | 文件插入阶段使用的模型,主要复杂实体关系抽取和摘要总结。建议配置关闭思考模式并具备处理复杂问题的高速模型,模型参数量建议为30B或以上,上下文长度至少为32KB。 |
| `KEYWORD` | 查询阶段关键词抽取,用于检索前的 high-level / low-level keyword 生成。建议配置关闭思考模式的超高速模型,以提高查询阶段的响应速度。模型数量建议为7B或以上。 |
| `QUERY` | 查询阶段用于根据召回内容最终问答用关乎问题。建议配置开启思考模式的高质量模型。模型能力越强,回答的质量会越高。模型参数量建议为30B或以上,上下文长度至少为32KB。 |
| `VLM` | 文件插入阶段用于分析图片。需要配置具有图片识别能力的高质量模型。模型参数量建议为30B或以上。 |
如果某个角色没有专门配置,LightRAG 会使用基础 `LLM_*` 配置。
## 基础 LLM 配置
基础配置定义默认 LLM provider、模型、服务地址、认证信息和并发控制:
```env
LLM_BINDING=openai
LLM_MODEL=gpt-5-mini
LLM_BINDING_HOST=https://api.openai.com/v1
LLM_BINDING_API_KEY=your_api_key
# 所有 LLM 请求的默认超时时间
LLM_TIMEOUT=240
# 所有 LLM 调用的默认最大并发数(MAX_ASYNC 作为兼容旧名仍可用)
MAX_ASYNC_LLM=4
```
常用字段:
| 变量 | 说明 |
| --- | --- |
| `LLM_BINDING` | 基础 LLM provider。支持 `openai``ollama``lollms``azure_openai``bedrock``gemini`。 |
| `LLM_MODEL` | 基础模型名。对 Azure OpenAI 通常使用 deployment 名称。 |
| `LLM_BINDING_HOST` | 基础 provider endpoint。对于 SDK 默认 endpoint,可使用对应 sentinel,例如 `DEFAULT_GEMINI_ENDPOINT``DEFAULT_BEDROCK_ENDPOINT`。 |
| `LLM_BINDING_API_KEY` | 基础 API key。Bedrock 不使用这个字段。 |
| `LLM_TIMEOUT` | 基础 LLM timeout。角色未设置 timeout 时继承它。 |
| `MAX_ASYNC_LLM` | 基础 LLM 最大并发。角色未设置 `{ROLE}_MAX_ASYNC_LLM` 时继承它。`MAX_ASYNC` 作为兼容旧名仍可用。 |
## 角色覆盖变量
每个角色都可以覆盖 binding、模型、endpoint、API key、并发和 timeout
```env
QUERY_LLM_BINDING=openai
QUERY_LLM_MODEL=gpt-5
QUERY_LLM_BINDING_HOST=https://api.openai.com/v1
QUERY_LLM_BINDING_API_KEY=your_query_api_key
QUERY_MAX_ASYNC_LLM=2
QUERY_LLM_TIMEOUT=240
```
变量格式:
| 变量 | 说明 |
| --- | --- |
| `{ROLE}_LLM_BINDING` | 覆盖角色 provider。`ROLE` 可为 `EXTRACT``KEYWORD``QUERY``VLM`。 |
| `{ROLE}_LLM_MODEL` | 覆盖角色模型名。 |
| `{ROLE}_LLM_BINDING_HOST` | 覆盖角色 endpoint。 |
| `{ROLE}_LLM_BINDING_API_KEY` | 覆盖角色 API key。Bedrock 不支持。 |
| `{ROLE}_MAX_ASYNC_LLM` | 覆盖角色最大并发。未设置时继承 `MAX_ASYNC_LLM`。 |
| `{ROLE}_LLM_TIMEOUT` | 覆盖角色 timeout。未设置时继承 `LLM_TIMEOUT`。 |
## Provider 参数覆盖
provider 细项使用下面的格式:
```env
{ROLE}_{PROVIDER_PREFIX}_{FIELD}
```
例如:
```env
# 只覆盖 QUERY 角色的 OpenAI reasoning effort
QUERY_OPENAI_LLM_REASONING_EFFORT=medium
# 只覆盖 EXTRACT 角色的 Bedrock 生成参数
EXTRACT_BEDROCK_LLM_TEMPERATURE=0.0
EXTRACT_BEDROCK_LLM_MAX_TOKENS=2048
# 只覆盖 VLM 角色的 Gemini 生成参数
VLM_GEMINI_LLM_MAX_OUTPUT_TOKENS=4096
VLM_GEMINI_LLM_TEMPERATURE=0.2
```
常见 provider 前缀:
| Provider | 基础参数前缀 | 角色参数示例 |
| --- | --- | --- |
| `openai` / `azure_openai` | `OPENAI_LLM_*` | `QUERY_OPENAI_LLM_REASONING_EFFORT` |
| `ollama` | `OLLAMA_LLM_*` | `EXTRACT_OLLAMA_LLM_NUM_PREDICT` |
| `lollms` | 使用 Ollama 兼容参数集合 | `QUERY_OLLAMA_LLM_TEMPERATURE` |
| `bedrock` | `BEDROCK_LLM_*` | `EXTRACT_BEDROCK_LLM_MAX_TOKENS` |
| `gemini` | `GEMINI_LLM_*` | `VLM_GEMINI_LLM_THINKING_CONFIG` |
## 继承规则
### 同一个 provider 内覆盖
如果角色没有设置 `{ROLE}_LLM_BINDING`,或设置成与基础 `LLM_BINDING` 相同,角色会继承基础配置:
- 未设置 `{ROLE}_LLM_MODEL` 时继承 `LLM_MODEL`
- 未设置 `{ROLE}_LLM_BINDING_HOST` 时继承 `LLM_BINDING_HOST`
- 未设置 `{ROLE}_LLM_BINDING_API_KEY` 时继承 `LLM_BINDING_API_KEY`
- 未设置 `{ROLE}_LLM_TIMEOUT` 时继承 `LLM_TIMEOUT`
- 未设置 `{ROLE}_MAX_ASYNC_LLM` 时继承 `MAX_ASYNC_LLM`
- provider 参数先继承基础 provider options,再叠加角色专属 provider options。
因此,同一个 provider 下只想换模型时,只需要写模型名:
```env
LLM_BINDING=openai
LLM_MODEL=gpt-5-mini
LLM_BINDING_HOST=https://api.openai.com/v1
LLM_BINDING_API_KEY=your_api_key
OPENAI_LLM_REASONING_EFFORT=minimal
# QUERY 继承 host、API key、timeout、并发和 OPENAI_LLM_REASONING_EFFORT
QUERY_LLM_MODEL=gpt-5
```
### 跨 provider 覆盖
如果角色的 `{ROLE}_LLM_BINDING` 与基础 `LLM_BINDING` 不同,就是跨 provider 配置。当前规则是:
- 必须设置 `{ROLE}_LLM_MODEL`
- 非 Bedrock provider 必须设置 `{ROLE}_LLM_BINDING_API_KEY`
- 如果没有设置 `{ROLE}_LLM_BINDING_HOST`LightRAG 会尝试使用该 provider 的默认 host。
- provider 参数不继承基础 provider options,而是从空配置开始,只叠加角色专属 provider options。
示例:基础使用 Ollama,本地抽取;最终回答改用 OpenAI:
```env
LLM_BINDING=ollama
LLM_MODEL=qwen3.5:9b
LLM_BINDING_HOST=http://localhost:11434
OLLAMA_LLM_NUM_CTX=32768
QUERY_LLM_BINDING=openai
QUERY_LLM_MODEL=gpt-5-mini
QUERY_LLM_BINDING_HOST=https://api.openai.com/v1
QUERY_LLM_BINDING_API_KEY=your_openai_api_key
QUERY_OPENAI_LLM_REASONING_EFFORT=minimal
```
跨 provider 时建议显式设置 `{ROLE}_LLM_BINDING_HOST`,避免默认 host 与基础 provider 的 endpoint 混淆。
### Bedrock 认证规则
Bedrock 不使用 `LLM_BINDING_API_KEY`,也不支持 `{ROLE}_LLM_BINDING_API_KEY`。可用认证方式:
- 全局 SigV4`AWS_ACCESS_KEY_ID``AWS_SECRET_ACCESS_KEY``AWS_SESSION_TOKEN``AWS_REGION`
- 角色级 SigV4`{ROLE}_AWS_ACCESS_KEY_ID``{ROLE}_AWS_SECRET_ACCESS_KEY``{ROLE}_AWS_SESSION_TOKEN``{ROLE}_AWS_REGION`
- 进程级 bearer token`AWS_BEARER_TOKEN_BEDROCK`。这是 AWS SDK 进程级设置,不能按角色覆盖。
角色级 Bedrock 示例:
```env
LLM_BINDING=openai
LLM_MODEL=gpt-5-mini
LLM_BINDING_HOST=https://api.openai.com/v1
LLM_BINDING_API_KEY=your_openai_api_key
EXTRACT_LLM_BINDING=bedrock
EXTRACT_LLM_MODEL=us.amazon.nova-lite-v1:0
EXTRACT_LLM_BINDING_HOST=DEFAULT_BEDROCK_ENDPOINT
EXTRACT_AWS_REGION=us-west-2
EXTRACT_AWS_ACCESS_KEY_ID=your_extract_access_key
EXTRACT_AWS_SECRET_ACCESS_KEY=your_extract_secret_key
EXTRACT_AWS_SESSION_TOKEN=your_optional_session_token
EXTRACT_BEDROCK_LLM_TEMPERATURE=0.0
EXTRACT_BEDROCK_LLM_MAX_TOKENS=2048
```
## Provider 行为对照
| Provider | 角色级 host/base_url | 角色级 API key | 认证限制 |
| --- | --- | --- | --- |
| `openai` | 支持,通过 `{ROLE}_LLM_BINDING_HOST` 传给 OpenAI-compatible client。 | 支持 `{ROLE}_LLM_BINDING_API_KEY`,未设置时同 provider 继承基础 `LLM_BINDING_API_KEY`。 | 当前主要是 API key / Bearer 模式。 |
| `ollama` | 支持,通过 `{ROLE}_LLM_BINDING_HOST` 传给 Ollama client。 | 支持 `{ROLE}_LLM_BINDING_API_KEY`,未设置时同 provider 继承基础 key;底层未收到 key 时会再回退 `OLLAMA_API_KEY`。 | Bearer header。 |
| `lollms` | 支持,通过 `{ROLE}_LLM_BINDING_HOST` 作为 `base_url`。 | 支持 `{ROLE}_LLM_BINDING_API_KEY`,未设置时同 provider 继承基础 key。 | Bearer header。 |
| `azure_openai` | 支持,通过 `{ROLE}_LLM_BINDING_HOST` 作为 Azure endpoint。 | 支持 `{ROLE}_LLM_BINDING_API_KEY`,未设置时同 provider 继承基础 key,也可能回退 `AZURE_OPENAI_API_KEY`。 | `AZURE_OPENAI_API_VERSION` 是全局环境变量,不支持角色级覆盖。 |
| `bedrock` | 支持,通过 `{ROLE}_LLM_BINDING_HOST` 作为 `endpoint_url``DEFAULT_BEDROCK_ENDPOINT` 表示交给 AWS SDK 选择。 | 不支持 generic API key。 | 使用全局或角色级 SigV4。`AWS_BEARER_TOKEN_BEDROCK` 是进程级,不能按角色覆盖。 |
| `gemini` | 支持,通过 `{ROLE}_LLM_BINDING_HOST` 传给 Google GenAI client`DEFAULT_GEMINI_ENDPOINT` 表示使用 SDK 默认 endpoint。 | AI Studio 模式支持 `{ROLE}_LLM_BINDING_API_KEY`。 | Vertex AI 由 `GOOGLE_GENAI_USE_VERTEXAI``GOOGLE_CLOUD_PROJECT``GOOGLE_CLOUD_LOCATION``GOOGLE_APPLICATION_CREDENTIALS` 控制,都是进程级设置。 |
## 推荐配置模式
### 1. 同 provider 只更换模型
适合用同一个 OpenAI key 和 endpoint,但让最终回答使用更强模型:
```env
LLM_BINDING=openai
LLM_MODEL=gpt-5-mini
LLM_BINDING_HOST=https://api.openai.com/v1
LLM_BINDING_API_KEY=your_api_key
OPENAI_LLM_REASONING_EFFORT=minimal
QUERY_LLM_MODEL=gpt-5
QUERY_MAX_ASYNC_LLM=2
```
`QUERY` 会继承基础 host、API key 和 `OPENAI_LLM_REASONING_EFFORT`
### 2. 同 provider 更换模型并调整参数
适合基础模型用于抽取,最终回答使用更高 reasoning effort
```env
LLM_BINDING=openai
LLM_MODEL=gpt-5-mini
LLM_BINDING_HOST=https://api.openai.com/v1
LLM_BINDING_API_KEY=your_api_key
OPENAI_LLM_REASONING_EFFORT=minimal
OPENAI_LLM_MAX_COMPLETION_TOKENS=4096
QUERY_LLM_MODEL=gpt-5
QUERY_OPENAI_LLM_REASONING_EFFORT=medium
QUERY_OPENAI_LLM_MAX_COMPLETION_TOKENS=9000
QUERY_LLM_TIMEOUT=240
```
### 3. 同 provider 使用不同 endpoint 和 API key
适合所有角色都走 `openai` binding,但其中一些角色访问 OpenAI 官方接口,另一些角色访问本地 vLLM、SGLang 或 OpenRouter 等 OpenAI-compatible endpoint。下面的例子中:
- `EXTRACT` 使用 OpenAI 官方 `gpt-5-mini`
- `QUERY` 使用 OpenAI 官方 `gpt-5.4`,并使用单独的 OpenAI key。
- `KEYWORD` 使用本地 vLLM 部署的 `Qwen3.5-35B-A3B`
```env
###########################################################################
# Base LLM fallback. Keep it aligned with EXTRACT so unspecified roles still
# have a valid OpenAI configuration.
###########################################################################
LLM_BINDING=openai
LLM_MODEL=gpt-5-mini
LLM_BINDING_HOST=https://api.openai.com/v1
LLM_BINDING_API_KEY=your_extract_openai_api_key
LLM_TIMEOUT=240
MAX_ASYNC_LLM=4
###########################################################################
# IMPORTANT:
# Do not set global OPENAI_LLM_REASONING_EFFORT here if any same-provider role
# points to a local OpenAI-compatible server that does not support it.
# Use role-specific OPENAI options instead.
###########################################################################
# OPENAI_LLM_REASONING_EFFORT=none
###########################################################################
# EXTRACT: OpenAI official API, gpt-5-mini
###########################################################################
EXTRACT_LLM_BINDING=openai
EXTRACT_LLM_MODEL=gpt-5-mini
EXTRACT_LLM_BINDING_HOST=https://api.openai.com/v1
EXTRACT_LLM_BINDING_API_KEY=your_extract_openai_api_key
EXTRACT_OPENAI_LLM_REASONING_EFFORT=low
EXTRACT_OPENAI_LLM_MAX_COMPLETION_TOKENS=4096
EXTRACT_MAX_ASYNC_LLM=4
EXTRACT_LLM_TIMEOUT=180
###########################################################################
# QUERY: OpenAI official API, gpt-5.4, separate API key
###########################################################################
QUERY_LLM_BINDING=openai
QUERY_LLM_MODEL=gpt-5.4
QUERY_LLM_BINDING_HOST=https://api.openai.com/v1
QUERY_LLM_BINDING_API_KEY=your_query_openai_api_key
QUERY_OPENAI_LLM_REASONING_EFFORT=medium
QUERY_OPENAI_LLM_MAX_COMPLETION_TOKENS=9000
QUERY_MAX_ASYNC_LLM=2
QUERY_LLM_TIMEOUT=240
###########################################################################
# KEYWORD: local vLLM OpenAI-compatible endpoint, Qwen3.5-35B-A3B
###########################################################################
KEYWORD_LLM_BINDING=openai
KEYWORD_LLM_MODEL=Qwen3.5-35B-A3B
KEYWORD_LLM_BINDING_HOST=http://localhost:8000/v1
# If vLLM was started with --api-key, use the same value here.
# If vLLM has no auth, still set a non-empty dummy value to avoid falling
# back to the official OpenAI key.
KEYWORD_LLM_BINDING_API_KEY=local-vllm-api-key
KEYWORD_OPENAI_LLM_MAX_TOKENS=2048
# Optional for Qwen-style models served by vLLM when you want to disable thinking.
KEYWORD_OPENAI_LLM_EXTRA_BODY='{"chat_template_kwargs": {"enable_thinking": false}}'
KEYWORD_MAX_ASYNC_LLM=4
KEYWORD_LLM_TIMEOUT=60
```
这个模式不是跨 provider,因为三个角色的 binding 都是 `openai`。LightRAG 会分别把每个角色的 `*_LLM_BINDING_HOST``*_LLM_BINDING_API_KEY` 传给 OpenAI-compatible client。
注意:同 provider 的 provider options 会继承基础 `OPENAI_LLM_*`。如果本地 vLLM 不支持 OpenAI 官方参数,例如 `reasoning_effort`,不要设置全局 `OPENAI_LLM_REASONING_EFFORT`;改用 `EXTRACT_OPENAI_LLM_REASONING_EFFORT``QUERY_OPENAI_LLM_REASONING_EFFORT` 这类角色级变量。
### 4. 某个角色跨 provider
适合基础使用 OpenAI 官方模型,只有关键词抽取使用本地 Ollama:
```env
LLM_BINDING=openai
LLM_MODEL=gpt-5-mini
LLM_BINDING_HOST=https://api.openai.com/v1
LLM_BINDING_API_KEY=your_openai_api_key
OPENAI_LLM_REASONING_EFFORT=medium
KEYWORD_LLM_BINDING=ollama
KEYWORD_LLM_MODEL=qwen3.5:9b
KEYWORD_LLM_BINDING_HOST=http://localhost:11434
KEYWORD_LLM_BINDING_API_KEY=ollama-local-key
KEYWORD_OLLAMA_LLM_NUM_CTX=32768
```
跨 provider 时,Ollama 参数不会继承 OpenAI 参数。`KEYWORD_LLM_BINDING_API_KEY` 对本地 Ollama 通常可以使用占位值;当前跨 provider 校验会要求非 Bedrock 角色显式提供角色级 API key。
### 5. 为 VLM 单独指定多模态模型
适合文本任务使用便宜模型,多模态分析使用视觉语言模型:
```env
VLM_PROCESS_ENABLE=true
LLM_BINDING=openai
LLM_MODEL=gpt-5-mini
LLM_BINDING_HOST=https://api.openai.com/v1
LLM_BINDING_API_KEY=your_api_key
VLM_LLM_BINDING=openai
VLM_LLM_MODEL=gpt-4o
VLM_OPENAI_LLM_MAX_TOKENS=4096
VLM_MAX_ASYNC_LLM=2
VLM_LLM_TIMEOUT=240
```
如果 VLM 使用同一个 provider 和 key,可以省略 `VLM_LLM_BINDING_HOST``VLM_LLM_BINDING_API_KEY`
`VLM_PROCESS_ENABLE` 是多模态分析的总开关:设为 `false` 时,pipeline 会对每个多模态 item 输出 warning 并跳过,不调用 VLM;设为 `true` 时,生效的 VLM binding(设置了 `VLM_LLM_BINDING` 时取该值,否则取 `LLM_BINDING`)必须支持图片输入。当前支持视觉输入的 provider 包括:`openai``azure_openai``gemini``bedrock``ollama``anthropic``lollms` 无法接收图片输入,会在启动时直接报错。
### 6. Bedrock 角色级 SigV4 凭证
适合只有某个角色访问 Bedrock,并使用独立 IAM/STS 凭证:
```env
LLM_BINDING=openai
LLM_MODEL=gpt-5-mini
LLM_BINDING_HOST=https://api.openai.com/v1
LLM_BINDING_API_KEY=your_openai_api_key
QUERY_LLM_BINDING=bedrock
QUERY_LLM_MODEL=us.amazon.nova-lite-v1:0
QUERY_LLM_BINDING_HOST=DEFAULT_BEDROCK_ENDPOINT
QUERY_AWS_REGION=us-east-1
QUERY_AWS_ACCESS_KEY_ID=your_query_access_key
QUERY_AWS_SECRET_ACCESS_KEY=your_query_secret_key
QUERY_AWS_SESSION_TOKEN=your_optional_session_token
QUERY_BEDROCK_LLM_MAX_TOKENS=4096
QUERY_BEDROCK_LLM_TEMPERATURE=0.2
```
不要设置 `QUERY_LLM_BINDING_API_KEY`Bedrock 会拒绝该配置。
## 注意事项
- 同 provider 下,`OPENAI_LLM_REASONING_EFFORT``OPENAI_LLM_MAX_TOKENS``OLLAMA_LLM_NUM_CTX``GEMINI_LLM_THINKING_CONFIG` 等 provider 参数会自动继承。
- 当前没有干净的角色级“取消继承某个 provider 参数”的语义。如果某个同 provider 角色模型不支持基础参数,需要为该角色显式覆盖为可用值,或将它配置成跨 provider,并且只设置该角色支持的 provider 参数。
- `azure_openai``AZURE_OPENAI_DEPLOYMENT``AZURE_OPENAI_API_VERSION` 是全局环境变量。若设置了 `AZURE_OPENAI_DEPLOYMENT`,它可能优先于角色模型名。
- Gemini Vertex AI 模式由进程级 Google 环境变量控制,不能在同一个 LightRAG 进程里让某些角色使用 Vertex AI、另一些角色使用 AI Studio API key。
- `LLM_BINDING_HOST` 在 Docker/Compose 中通常需要使用容器可访问地址,例如 `host.docker.internal`,角色级 host 也遵循相同原则。
- 修改 `.env` 后请重启 LightRAG Server。部分 IDE 终端会预加载 `.env`,建议打开新的终端会话确认环境变量生效。
+376
View File
@@ -0,0 +1,376 @@
# Role-Specific LLM/VLM Configuration Guide
LightRAG supports configuring different LLMs or VLMs for different processing stages. This mechanism is useful when using a lower-cost model for extraction, a stronger model for final answers, or a dedicated vision-language model for multimodal analysis.
## Role Overview
Four roles are currently supported:
| Role | Purpose |
| --- | --- |
| `EXTRACT` | The model used during the file insertion stage, mainly for complex entity/relation extraction and summarization. A fast model with thinking mode disabled and the ability to handle complex problems is recommended; a parameter size of 30B or above and a context length of at least 32KB are suggested. |
| `KEYWORD` | Query-stage keyword extraction for high-level / low-level keyword generation before retrieval. An ultra-fast model with thinking mode disabled is recommended to improve query-stage response speed; a parameter size of 7B or above is suggested. |
| `QUERY` | The query stage, used to produce the final answer to the question based on the recalled content. A high-quality model with thinking mode enabled is recommended; the stronger the model, the higher the answer quality. A parameter size of 30B or above and a context length of at least 32KB are suggested. |
| `VLM` | Used during the file insertion stage to analyze images. A high-quality model with image recognition capability is required; a parameter size of 30B or above is suggested. |
If a role has no dedicated configuration, LightRAG uses the base `LLM_*` configuration.
## Base LLM Configuration
The base configuration defines the default LLM provider, model, service endpoint, authentication information, and concurrency control:
```env
LLM_BINDING=openai
LLM_MODEL=gpt-5-mini
LLM_BINDING_HOST=https://api.openai.com/v1
LLM_BINDING_API_KEY=your_api_key
# Default timeout for all LLM requests
LLM_TIMEOUT=240
# Default maximum concurrency for all LLM calls (MAX_ASYNC is still accepted as a deprecated alias)
MAX_ASYNC_LLM=4
```
Common fields:
| Variable | Description |
| --- | --- |
| `LLM_BINDING` | Base LLM provider. Supported values are `openai`, `ollama`, `lollms`, `azure_openai`, `bedrock`, and `gemini`. |
| `LLM_MODEL` | Base model name. For Azure OpenAI, this is usually the deployment name. |
| `LLM_BINDING_HOST` | Base provider endpoint. For SDK default endpoints, use the corresponding sentinel, such as `DEFAULT_GEMINI_ENDPOINT` or `DEFAULT_BEDROCK_ENDPOINT`. |
| `LLM_BINDING_API_KEY` | Base API key. Bedrock does not use this field. |
| `LLM_TIMEOUT` | Base LLM timeout. A role inherits it when no role timeout is set. |
| `MAX_ASYNC_LLM` | Base maximum LLM concurrency. A role inherits it when `{ROLE}_MAX_ASYNC_LLM` is not set. `MAX_ASYNC` is still accepted as a deprecated alias. |
## Role Override Variables
Each role can override the binding, model, endpoint, API key, concurrency, and timeout:
```env
QUERY_LLM_BINDING=openai
QUERY_LLM_MODEL=gpt-5
QUERY_LLM_BINDING_HOST=https://api.openai.com/v1
QUERY_LLM_BINDING_API_KEY=your_query_api_key
QUERY_MAX_ASYNC_LLM=2
QUERY_LLM_TIMEOUT=240
```
Variable format:
| Variable | Description |
| --- | --- |
| `{ROLE}_LLM_BINDING` | Overrides the role provider. `ROLE` can be `EXTRACT`, `KEYWORD`, `QUERY`, or `VLM`. |
| `{ROLE}_LLM_MODEL` | Overrides the role model name. |
| `{ROLE}_LLM_BINDING_HOST` | Overrides the role endpoint. |
| `{ROLE}_LLM_BINDING_API_KEY` | Overrides the role API key. Bedrock does not support it. |
| `{ROLE}_MAX_ASYNC_LLM` | Overrides the role maximum concurrency. Inherits `MAX_ASYNC_LLM` when unset. |
| `{ROLE}_LLM_TIMEOUT` | Overrides the role timeout. Inherits `LLM_TIMEOUT` when unset. |
## Provider Option Overrides
Provider-specific options use the following format:
```env
{ROLE}_{PROVIDER_PREFIX}_{FIELD}
```
Examples:
```env
# Override only the OpenAI reasoning effort for the QUERY role
QUERY_OPENAI_LLM_REASONING_EFFORT=medium
# Override only Bedrock generation parameters for the EXTRACT role
EXTRACT_BEDROCK_LLM_TEMPERATURE=0.0
EXTRACT_BEDROCK_LLM_MAX_TOKENS=2048
# Override only Gemini generation parameters for the VLM role
VLM_GEMINI_LLM_MAX_OUTPUT_TOKENS=4096
VLM_GEMINI_LLM_TEMPERATURE=0.2
```
Common provider prefixes:
| Provider | Base option prefix | Role option example |
| --- | --- | --- |
| `openai` / `azure_openai` | `OPENAI_LLM_*` | `QUERY_OPENAI_LLM_REASONING_EFFORT` |
| `ollama` | `OLLAMA_LLM_*` | `EXTRACT_OLLAMA_LLM_NUM_PREDICT` |
| `lollms` | Uses the Ollama-compatible option set | `QUERY_OLLAMA_LLM_TEMPERATURE` |
| `bedrock` | `BEDROCK_LLM_*` | `EXTRACT_BEDROCK_LLM_MAX_TOKENS` |
| `gemini` | `GEMINI_LLM_*` | `VLM_GEMINI_LLM_THINKING_CONFIG` |
## Inheritance Rules
### Overrides Within the Same Provider
If a role does not set `{ROLE}_LLM_BINDING`, or sets it to the same value as the base `LLM_BINDING`, the role inherits the base configuration:
- Inherits `LLM_MODEL` when `{ROLE}_LLM_MODEL` is not set.
- Inherits `LLM_BINDING_HOST` when `{ROLE}_LLM_BINDING_HOST` is not set.
- Inherits `LLM_BINDING_API_KEY` when `{ROLE}_LLM_BINDING_API_KEY` is not set.
- Inherits `LLM_TIMEOUT` when `{ROLE}_LLM_TIMEOUT` is not set.
- Inherits `MAX_ASYNC_LLM` when `{ROLE}_MAX_ASYNC_LLM` is not set.
- Provider options first inherit the base provider options, then apply role-specific provider options.
Therefore, when you only want to change the model within the same provider, you only need to set the model name:
```env
LLM_BINDING=openai
LLM_MODEL=gpt-5-mini
LLM_BINDING_HOST=https://api.openai.com/v1
LLM_BINDING_API_KEY=your_api_key
OPENAI_LLM_REASONING_EFFORT=minimal
# QUERY inherits host, API key, timeout, concurrency, and OPENAI_LLM_REASONING_EFFORT
QUERY_LLM_MODEL=gpt-5
```
### Cross-Provider Overrides
If a role's `{ROLE}_LLM_BINDING` differs from the base `LLM_BINDING`, it is a cross-provider configuration. The current rules are:
- `{ROLE}_LLM_MODEL` must be set.
- Non-Bedrock providers must set `{ROLE}_LLM_BINDING_API_KEY`.
- If `{ROLE}_LLM_BINDING_HOST` is not set, LightRAG tries to use that provider's default host.
- Provider options do not inherit base provider options. They start empty and only apply role-specific provider options.
Example: use Ollama as the base for local extraction, then use OpenAI for final answers:
```env
LLM_BINDING=ollama
LLM_MODEL=qwen3.5:9b
LLM_BINDING_HOST=http://localhost:11434
OLLAMA_LLM_NUM_CTX=32768
QUERY_LLM_BINDING=openai
QUERY_LLM_MODEL=gpt-5-mini
QUERY_LLM_BINDING_HOST=https://api.openai.com/v1
QUERY_LLM_BINDING_API_KEY=your_openai_api_key
QUERY_OPENAI_LLM_REASONING_EFFORT=minimal
```
For cross-provider configurations, explicitly setting `{ROLE}_LLM_BINDING_HOST` is recommended to avoid confusion between the default host and the base provider endpoint.
### Bedrock Authentication Rules
Bedrock does not use `LLM_BINDING_API_KEY` and does not support `{ROLE}_LLM_BINDING_API_KEY`. Available authentication methods are:
- Global SigV4: `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_SESSION_TOKEN`, and `AWS_REGION`.
- Role-level SigV4: `{ROLE}_AWS_ACCESS_KEY_ID`, `{ROLE}_AWS_SECRET_ACCESS_KEY`, `{ROLE}_AWS_SESSION_TOKEN`, and `{ROLE}_AWS_REGION`.
- Process-level bearer token: `AWS_BEARER_TOKEN_BEDROCK`. This is an AWS SDK process-level setting and cannot be overridden per role.
Role-level Bedrock example:
```env
LLM_BINDING=openai
LLM_MODEL=gpt-5-mini
LLM_BINDING_HOST=https://api.openai.com/v1
LLM_BINDING_API_KEY=your_openai_api_key
EXTRACT_LLM_BINDING=bedrock
EXTRACT_LLM_MODEL=us.amazon.nova-lite-v1:0
EXTRACT_LLM_BINDING_HOST=DEFAULT_BEDROCK_ENDPOINT
EXTRACT_AWS_REGION=us-west-2
EXTRACT_AWS_ACCESS_KEY_ID=your_extract_access_key
EXTRACT_AWS_SECRET_ACCESS_KEY=your_extract_secret_key
EXTRACT_AWS_SESSION_TOKEN=your_optional_session_token
EXTRACT_BEDROCK_LLM_TEMPERATURE=0.0
EXTRACT_BEDROCK_LLM_MAX_TOKENS=2048
```
## Provider Behavior Matrix
| Provider | Role-level host/base_url | Role-level API key | Authentication limitations |
| --- | --- | --- | --- |
| `openai` | Supported, passed to the OpenAI-compatible client through `{ROLE}_LLM_BINDING_HOST`. | Supports `{ROLE}_LLM_BINDING_API_KEY`; when unset within the same provider, it inherits the base `LLM_BINDING_API_KEY`. | Currently mainly API key / Bearer mode. |
| `ollama` | Supported, passed to the Ollama client through `{ROLE}_LLM_BINDING_HOST`. | Supports `{ROLE}_LLM_BINDING_API_KEY`; when unset within the same provider, it inherits the base key. If no key reaches the lower layer, it falls back to `OLLAMA_API_KEY`. | Bearer header. |
| `lollms` | Supported, using `{ROLE}_LLM_BINDING_HOST` as `base_url`. | Supports `{ROLE}_LLM_BINDING_API_KEY`; when unset within the same provider, it inherits the base key. | Bearer header. |
| `azure_openai` | Supported, using `{ROLE}_LLM_BINDING_HOST` as the Azure endpoint. | Supports `{ROLE}_LLM_BINDING_API_KEY`; when unset within the same provider, it inherits the base key and may also fall back to `AZURE_OPENAI_API_KEY`. | `AZURE_OPENAI_API_VERSION` is a global environment variable and does not support role-level overrides. |
| `bedrock` | Supported, using `{ROLE}_LLM_BINDING_HOST` as `endpoint_url`; `DEFAULT_BEDROCK_ENDPOINT` means letting the AWS SDK choose. | Generic API keys are not supported. | Uses global or role-level SigV4. `AWS_BEARER_TOKEN_BEDROCK` is process-level and cannot be overridden per role. |
| `gemini` | Supported, passed to the Google GenAI client through `{ROLE}_LLM_BINDING_HOST`; `DEFAULT_GEMINI_ENDPOINT` means using the SDK default endpoint. | AI Studio mode supports `{ROLE}_LLM_BINDING_API_KEY`. | Vertex AI is controlled by `GOOGLE_GENAI_USE_VERTEXAI`, `GOOGLE_CLOUD_PROJECT`, `GOOGLE_CLOUD_LOCATION`, and `GOOGLE_APPLICATION_CREDENTIALS`; all are process-level settings. |
## Recommended Configuration Patterns
### 1. Same Provider, Only Change the Model
Suitable when using the same OpenAI key and endpoint, but using a stronger model for final answers:
```env
LLM_BINDING=openai
LLM_MODEL=gpt-5-mini
LLM_BINDING_HOST=https://api.openai.com/v1
LLM_BINDING_API_KEY=your_api_key
OPENAI_LLM_REASONING_EFFORT=minimal
QUERY_LLM_MODEL=gpt-5
QUERY_MAX_ASYNC_LLM=2
```
`QUERY` inherits the base host, API key, and `OPENAI_LLM_REASONING_EFFORT`.
### 2. Same Provider, Change the Model and Tune Options
Suitable when the base model is used for extraction and final answers use a higher reasoning effort:
```env
LLM_BINDING=openai
LLM_MODEL=gpt-5-mini
LLM_BINDING_HOST=https://api.openai.com/v1
LLM_BINDING_API_KEY=your_api_key
OPENAI_LLM_REASONING_EFFORT=minimal
OPENAI_LLM_MAX_COMPLETION_TOKENS=4096
QUERY_LLM_MODEL=gpt-5
QUERY_OPENAI_LLM_REASONING_EFFORT=medium
QUERY_OPENAI_LLM_MAX_COMPLETION_TOKENS=9000
QUERY_LLM_TIMEOUT=240
```
### 3. Same Provider with Different Endpoints and API Keys
Suitable when all roles use the `openai` binding, but some roles access the official OpenAI API while others access a local vLLM, SGLang, OpenRouter, or another OpenAI-compatible endpoint. In the example below:
- `EXTRACT` uses the official OpenAI `gpt-5-mini`.
- `QUERY` uses the official OpenAI `gpt-5.4` with a separate OpenAI key.
- `KEYWORD` uses `Qwen3.5-35B-A3B` deployed by local vLLM.
```env
###########################################################################
# Base LLM fallback. Keep it aligned with EXTRACT so unspecified roles still
# have a valid OpenAI configuration.
###########################################################################
LLM_BINDING=openai
LLM_MODEL=gpt-5-mini
LLM_BINDING_HOST=https://api.openai.com/v1
LLM_BINDING_API_KEY=your_extract_openai_api_key
LLM_TIMEOUT=240
MAX_ASYNC_LLM=4
###########################################################################
# IMPORTANT:
# Do not set global OPENAI_LLM_REASONING_EFFORT here if any same-provider role
# points to a local OpenAI-compatible server that does not support it.
# Use role-specific OPENAI options instead.
###########################################################################
# OPENAI_LLM_REASONING_EFFORT=none
###########################################################################
# EXTRACT: OpenAI official API, gpt-5-mini
###########################################################################
EXTRACT_LLM_BINDING=openai
EXTRACT_LLM_MODEL=gpt-5-mini
EXTRACT_LLM_BINDING_HOST=https://api.openai.com/v1
EXTRACT_LLM_BINDING_API_KEY=your_extract_openai_api_key
EXTRACT_OPENAI_LLM_REASONING_EFFORT=low
EXTRACT_OPENAI_LLM_MAX_COMPLETION_TOKENS=4096
EXTRACT_MAX_ASYNC_LLM=4
EXTRACT_LLM_TIMEOUT=180
###########################################################################
# QUERY: OpenAI official API, gpt-5.4, separate API key
###########################################################################
QUERY_LLM_BINDING=openai
QUERY_LLM_MODEL=gpt-5.4
QUERY_LLM_BINDING_HOST=https://api.openai.com/v1
QUERY_LLM_BINDING_API_KEY=your_query_openai_api_key
QUERY_OPENAI_LLM_REASONING_EFFORT=medium
QUERY_OPENAI_LLM_MAX_COMPLETION_TOKENS=9000
QUERY_MAX_ASYNC_LLM=2
QUERY_LLM_TIMEOUT=240
###########################################################################
# KEYWORD: local vLLM OpenAI-compatible endpoint, Qwen3.5-35B-A3B
###########################################################################
KEYWORD_LLM_BINDING=openai
KEYWORD_LLM_MODEL=Qwen3.5-35B-A3B
KEYWORD_LLM_BINDING_HOST=http://localhost:8000/v1
# If vLLM was started with --api-key, use the same value here.
# If vLLM has no auth, still set a non-empty dummy value to avoid falling
# back to the official OpenAI key.
KEYWORD_LLM_BINDING_API_KEY=local-vllm-api-key
KEYWORD_OPENAI_LLM_MAX_TOKENS=2048
# Optional for Qwen-style models served by vLLM when you want to disable thinking.
KEYWORD_OPENAI_LLM_EXTRA_BODY='{"chat_template_kwargs": {"enable_thinking": false}}'
KEYWORD_MAX_ASYNC_LLM=4
KEYWORD_LLM_TIMEOUT=60
```
This pattern is not cross-provider because all three roles use the `openai` binding. LightRAG passes each role's `*_LLM_BINDING_HOST` and `*_LLM_BINDING_API_KEY` to the OpenAI-compatible client separately.
Note: provider options within the same provider inherit the base `OPENAI_LLM_*`. If the local vLLM server does not support official OpenAI parameters such as `reasoning_effort`, do not set the global `OPENAI_LLM_REASONING_EFFORT`; use role-level variables such as `EXTRACT_OPENAI_LLM_REASONING_EFFORT` and `QUERY_OPENAI_LLM_REASONING_EFFORT` instead.
### 4. One Role Crosses Provider
Suitable when the base uses an official OpenAI model and only keyword extraction uses local Ollama:
```env
LLM_BINDING=openai
LLM_MODEL=gpt-5-mini
LLM_BINDING_HOST=https://api.openai.com/v1
LLM_BINDING_API_KEY=your_openai_api_key
OPENAI_LLM_REASONING_EFFORT=medium
KEYWORD_LLM_BINDING=ollama
KEYWORD_LLM_MODEL=qwen3.5:9b
KEYWORD_LLM_BINDING_HOST=http://localhost:11434
KEYWORD_LLM_BINDING_API_KEY=ollama-local-key
KEYWORD_OLLAMA_LLM_NUM_CTX=32768
```
For cross-provider configurations, Ollama options do not inherit OpenAI options. For local Ollama, `KEYWORD_LLM_BINDING_API_KEY` can usually use a placeholder value; the current cross-provider validation requires non-Bedrock roles to explicitly provide a role-level API key.
### 5. Specify a Dedicated Multimodal Model for VLM
Suitable when text tasks use a cheaper model and multimodal analysis uses a vision-language model:
```env
VLM_PROCESS_ENABLE=true
LLM_BINDING=openai
LLM_MODEL=gpt-5-mini
LLM_BINDING_HOST=https://api.openai.com/v1
LLM_BINDING_API_KEY=your_api_key
VLM_LLM_BINDING=openai
VLM_LLM_MODEL=gpt-4o
VLM_OPENAI_LLM_MAX_TOKENS=4096
VLM_MAX_ASYNC_LLM=2
VLM_LLM_TIMEOUT=240
```
If VLM uses the same provider and key, `VLM_LLM_BINDING_HOST` and `VLM_LLM_BINDING_API_KEY` can be omitted.
`VLM_PROCESS_ENABLE` is the master switch for multimodal analysis. When `false`, the pipeline emits a warning and skips every multimodal item without invoking the VLM. When `true`, the effective VLM binding (`VLM_LLM_BINDING` if set, otherwise `LLM_BINDING`) must support image inputs. The following providers are vision-capable: `openai`, `azure_openai`, `gemini`, `bedrock`, `ollama`, `anthropic`. `lollms` is rejected at startup because it cannot accept image inputs.
### 6. Bedrock Role-Level SigV4 Credentials
Suitable when only one role accesses Bedrock and uses independent IAM/STS credentials:
```env
LLM_BINDING=openai
LLM_MODEL=gpt-5-mini
LLM_BINDING_HOST=https://api.openai.com/v1
LLM_BINDING_API_KEY=your_openai_api_key
QUERY_LLM_BINDING=bedrock
QUERY_LLM_MODEL=us.amazon.nova-lite-v1:0
QUERY_LLM_BINDING_HOST=DEFAULT_BEDROCK_ENDPOINT
QUERY_AWS_REGION=us-east-1
QUERY_AWS_ACCESS_KEY_ID=your_query_access_key
QUERY_AWS_SECRET_ACCESS_KEY=your_query_secret_key
QUERY_AWS_SESSION_TOKEN=your_optional_session_token
QUERY_BEDROCK_LLM_MAX_TOKENS=4096
QUERY_BEDROCK_LLM_TEMPERATURE=0.2
```
Do not set `QUERY_LLM_BINDING_API_KEY`; Bedrock rejects that configuration.
## Caveats
- Within the same provider, provider options such as `OPENAI_LLM_REASONING_EFFORT`, `OPENAI_LLM_MAX_TOKENS`, `OLLAMA_LLM_NUM_CTX`, and `GEMINI_LLM_THINKING_CONFIG` are inherited automatically.
- There is currently no clean role-level semantic for "unsetting an inherited provider option". If a model in a same-provider role does not support a base option, explicitly override that option for the role with a supported value, or configure the role as cross-provider and set only the role-specific provider options it supports.
- `AZURE_OPENAI_DEPLOYMENT` and `AZURE_OPENAI_API_VERSION` for `azure_openai` are global environment variables. If `AZURE_OPENAI_DEPLOYMENT` is set, it may take precedence over the role model name.
- Gemini Vertex AI mode is controlled by process-level Google environment variables. In the same LightRAG process, some roles cannot use Vertex AI while others use AI Studio API keys.
- In Docker/Compose, `LLM_BINDING_HOST` usually needs to use a container-reachable address such as `host.docker.internal`; role-level hosts follow the same principle.
- Restart LightRAG Server after modifying `.env`. Some IDE terminals preload `.env`, so opening a new terminal session is recommended to confirm that environment variables take effect.
+214
View File
@@ -0,0 +1,214 @@
# 第三方 Parser 引擎开发与注册指南
LightRAG 的解析层通过统一的 `BaseParser` 契约 + 中央引擎注册表(`lightrag/parser/registry.py`)派发所有解析引擎。内置引擎(`native` / `legacy` / `mineru` / `docling`)与第三方引擎走完全相同的派发路径:pipeline worker 与调试 CLI 都通过 `get_parser(engine).parse(ParseContext(...))` 驱动,**没有任何针对内置引擎的特判**。因此第三方包只需做两件事:
1. **实现**一个 `BaseParser` 子类;
2. **注册**一个 `ParserSpec`(推荐通过 `lightrag.parsers` entry point 自动发现)。
完成后,该引擎自动获得:独立(或共享)的解析并发池、文件名 hint / `LIGHTRAG_PARSER` 路由规则 / API `parse_engine` 参数三种选择方式、后缀能力校验、以及 `python -m lightrag.parser.cli --engine <name>` 单文件调试支持。
> 架构背景见 RFC #3197;sidecar 文件格式见 `docs/LightRAGSidecarFormat-zh.md`;CLI 用法见 `docs/ParserDebugCLI-zh.md`。
---
## 1. 派发流程总览
```
上传/扫描 → enqueue(PENDING_PARSE, parse_engine=<engine>)
→ pipeline 按 ParserSpec.queue_group 选并发池
→ parse worker: get_parser(engine).parse(ParseContext(rag, doc_id, file_path, content_data))
├─ 成功 → ParseResult → 进入 analyze / chunk / KG 流水线
└─ 抛异常 → 该文档 doc_status=FAILED(只影响这一篇)
```
引擎对单篇文档的全部职责都收敛在一次 `parse(ctx)` 调用里:解析、持久化 `full_docs`、归档源文件、返回结构化结果。
## 2. 实现 Parser
### 2.1 契约(`lightrag/parser/base.py`)
```python
class MyParser(BaseParser):
engine_name = "myengine" # 必须与 ParserSpec.engine_name 一致
async def parse(self, ctx: ParseContext) -> ParseResult: ...
```
`ParseContext` 提供:
| 成员 | 说明 |
|---|---|
| `ctx.rag` | LightRAG 实例(用于 `_persist_parsed_full_docs` 等) |
| `ctx.doc_id` / `ctx.file_path` / `ctx.content_data` | 文档标识、规范化文件路径、`full_docs` 行 |
| `ctx.resolve(engine_name)` | 返回 `ResolvedSource(source_path, document_name, parsed_dir)` —— 解析磁盘源文件路径、规范化文档名、推导 `__parsed__/<base>.parsed/` 产物目录 |
| `ctx.archive_source(path)` | 解析成功并完成 `full_docs` 同步后,把源文件归档进 `__parsed__/` |
`ParseResult` 字段:`doc_id` / `file_path` / `parse_format`(`"raw"``"lightrag"`)/ `content` / `blocks_path`(无 sidecar 则 `""`)/ `parse_engine` / `parse_stage_skipped`(缓存命中等跳过场景)/ `parse_warnings`(非致命警告,会落到 `doc_status.metadata`)。
### 2.2 三条实现路径(按引擎形态选基类)
**A. 纯文本引擎(无 sidecar)— 直接继承 `BaseParser`**
参考 `lightrag/parser/legacy/parser.py`(`LegacyParser`),核心骨架:
```python
class MyTextParser(BaseParser):
engine_name = "myengine"
async def parse(self, ctx: ParseContext) -> ParseResult:
rs = ctx.resolve(self.engine_name)
source = rs.source_path
if not source.is_file():
raise FileNotFoundError(f"myengine source not found: {source}")
text = await asyncio.to_thread(my_extract, source) # CPU 活进线程
if not text.strip():
raise ValueError(f"extracted no usable text from {ctx.file_path}")
await ctx.rag._persist_parsed_full_docs(ctx.doc_id, {
"content": text,
"file_path": ctx.file_path,
"parse_format": FULL_DOCS_FORMAT_RAW,
"parse_engine": self.engine_name,
"update_time": int(time.time()),
})
await ctx.archive_source(str(source))
return ParseResult(
doc_id=ctx.doc_id, file_path=ctx.file_path,
parse_format=FULL_DOCS_FORMAT_RAW, content=text,
blocks_path="", parse_engine=self.engine_name,
)
```
**B. 本地解析、产 sidecar — 继承 `NativeParserBase`**(`lightrag/parser/native_base.py`)
模板固定了「预清理产物目录(带回滚)→ 线程内抽取 → 构建 IR → 写 sidecar → 持久化 → 归档」的完整流程,只需实现两个钩子:
```python
class MyNativeParser(NativeParserBase):
engine_name = "myengine"
def extract(self, source, *, parsed_dir, asset_dir, base_name):
"""同步,在线程中运行;返回 (blocks, warnings, metadata)。
可在 write_sidecar 之前把图片等资产写入 asset_dir。"""
def build_ir(self, blocks, *, document_name, asset_dir_name, metadata) -> IRDoc:
"""blocks → IRDoc(交给统一的 sidecar writer)。"""
```
可选覆写:`validate_source`(默认仅要求文件存在)、`surface_warnings`(把抽取警告映射为 `parse_warnings`)。参考实现:`lightrag/parser/docx/parser.py`
**C. 外部解析服务(下载 raw bundle + 缓存)— 继承 `ExternalParserBase`**(`lightrag/parser/external/_base.py`)
模板固定了「raw 缓存命中检查 → 未命中则清目录重新下载 → 构建 IR → 写 sidecar → 持久化 → 归档」,实现三个钩子 + 两个类属性:
```python
class MyExternalParser(ExternalParserBase):
engine_name = "myengine"
raw_dir_suffix = ".myengine_raw" # raw bundle 目录后缀(以 . 开头)
force_reparse_env = "LIGHTRAG_FORCE_REPARSE_MYENGINE"
def is_bundle_valid(self, raw_dir, source_path) -> bool: ... # 缓存命中检查
async def download_into(self, raw_dir, source_path, *, upload_name): ...
def build_ir(self, raw_dir, document_name) -> IRDoc: ...
```
可选覆写 `validate_ir`(构建后校验,如零 block 报错)。参考实现:`lightrag/parser/external/mineru/parser.py``.../docling/parser.py`
### 2.3 失败语义(重要)
- `parse(ctx)` **抛出任何异常 ⇒ 仅该文档标记 FAILED**,错误信息写入 `doc_status.error_msg`,不影响批内其他文档。
- 解析出**空内容时应抛异常**而不是返回空串——否则会产出一篇零知识文档静默进入 chunking(内置引擎均遵循此约定)。
- worker 在调用引擎前会做后缀守门:`PENDING_PARSE` 文档的后缀不在该引擎 `ParserSpec.suffixes` 内 ⇒ 直接 FAILED,引擎代码不会被调用。
## 3. 声明 `ParserSpec`(能力元数据)
```python
from lightrag.parser.registry import ParserSpec, register_parser
register_parser(ParserSpec(
engine_name="myengine",
impl="my_pkg.parser:MyParser", # "module:Class",get_parser 时才懒加载
suffixes=frozenset({"pdf", "foo"}), # 小写、不带点
queue_group="myengine", # 见下文并发模型
concurrency=int(os.getenv("MAX_PARALLEL_PARSE_MYENGINE", "2")),
# 仅外部服务引擎需要(routing 在 endpoint 未配置时跳过该引擎):
endpoint_configured=lambda: bool(os.getenv("MYENGINE_ENDPOINT", "").strip()),
endpoint_requirement=lambda: "MYENGINE_ENDPOINT",
))
```
| 字段 | 必填 | 说明 |
|---|---|---|
| `engine_name` | ✓ | 注册表键,也是 `--engine` / 文件名 hint / `LIGHTRAG_PARSER` 里的引擎名。**与已有名字相同会覆盖原注册**(包括内置引擎)——除非有意替换实现,请勿与 `native/legacy/mineru/docling` 撞名。 |
| `impl` | ✓ | `"module:Class"` 字符串。注册表只在文档实际解析时才 import 它,**注册阶段绝不能提前 import 实现**(保持能力查询 import-cheap,这是注册表的设计不变量)。 |
| `suffixes` | ✓ | 该引擎能处理的扩展名(小写无点)。用于路由校验与 worker 端后缀守门。 |
| `queue_group` | | 并发池分组,默认 `"native"`(共享 native 池)。独立池填唯一组名。 |
| `concurrency` | | 该组 worker 数(组的唯一 owner 才需要填)。环境变量覆盖由**注册方在注册时自行烘焙**(如上例 `int(os.getenv(...))`),注册值即权威值。 |
| `endpoint_configured` / `endpoint_requirement` | | 零参闭包(只读 env、不发网络)。前者返回该引擎依赖的外部服务是否已配置;后者返回缺失时提示用户的配置项名。本地引擎不用填(默认恒可用)。 |
| `user_selectable` | | 默认 `True``False` 表示内部格式 handler(如 `reuse`/`passthrough`),不会出现在引擎选择面。 |
### 并发模型
- pipeline 每批为**每个 `queue_group` 建一条队列 + 一组 worker**;
- 组的 worker 数:内置组(`native`/`mineru`/`docling`)由 LightRAG 实例字段 `max_parallel_parse_*` 决定(支持构造参数覆盖);第三方独立组取该组唯一 owner spec 的 `concurrency`;一个组**只能有一个**声明了 `concurrency` 的 spec,多个会在批启动时报错;
- `queue_group="native"` 蹭内置池时,`concurrency` 不生效(池大小由 `max_parallel_parse_native` 决定,被忽略的 spec 级 `concurrency` 会在批启动时记录 warning 日志)——本地轻量引擎(如 legacy)适合这种方式,外部服务引擎建议独立组以免慢请求拖住本地解析。
## 4. 注册:entry point 自动发现(推荐)
LightRAG 通过 `lightrag.parsers` entry-point group 自动发现第三方引擎(`lightrag/parser/plugins.py`)。第三方包只需:
**① 在自己的 `pyproject.toml` 声明 entry point:**
```toml
[project.entry-points."lightrag.parsers"]
myengine = "my_pkg.lightrag_plugin:register"
```
**② 提供一个零参注册函数:**
```python
# my_pkg/lightrag_plugin.py —— 保持 import-cheap:不要在这里 import 解析实现
import os
from lightrag.parser.registry import ParserSpec, register_parser
def register() -> None:
register_parser(ParserSpec(
engine_name="myengine",
impl="my_pkg.parser:MyParser", # 实现走懒加载
suffixes=frozenset({"foo"}),
queue_group="myengine",
concurrency=int(os.getenv("MAX_PARALLEL_PARSE_MYENGINE", "2")),
))
```
`pip install my-pkg` 之后即装即用,无需改动 LightRAG 代码:
- **API Server**:`create_app()` 在校验 `LIGHTRAG_PARSER` 路由规则**之前**调用 `load_third_party_parsers()`,因此路由规则可以直接引用第三方引擎名(如 `LIGHTRAG_PARSER="foo:myengine"`)。上传与扫描的文件类型守门**完全由注册表 + 路由实时派生**,判据是"这份文件能否路由到一个支持它的引擎":裸后缀(无 hint)要求 `LIGHTRAG_PARSER` 规则把它指到你的引擎(否则默认 legacy 接不住会被拒,而不是收下后在解析阶段 FAILED);带文件名 hint 的上传(如 `report.[myengine].foo`)无需规则即可通过;endpoint 未配置的引擎其独有后缀不参与(与内置 mineru/docling 的图片格式同一规则)。**实践建议:发布第三方引擎时,在部署说明里让用户配套 `LIGHTRAG_PARSER="foo:myengine"` 规则**,裸文件名上传与目录扫描即自动生效;
- **调试 CLI**:`python -m lightrag.parser.cli sample.foo --engine myengine` 直接可用(`main()` 在构建 `--engine` choices 前加载插件)。无 sidecar 的引擎(`blocks_path=""`)CLI 会打印纯文本摘要而非 blocks 摘要;继承 `ExternalParserBase` 的引擎自动获得 raw 缓存展示与 `--force-reparse` 支持;
- **库内嵌用法**(不经 server/CLI 直接用 LightRAG 类):在构建 pipeline 前自行调用一次:
```python
from lightrag.parser.plugins import load_third_party_parsers
load_third_party_parsers() # 进程内幂等
```
加载语义:每进程幂等(重复调用为 no-op);**单个插件抛异常只会被记录并跳过**,不会影响其他插件或内置引擎,更不会阻断 server 启动——但该引擎将不可用,请关注启动日志中的 `[parser-plugins]` 行。
> 不想发包时也可以跳过 entry point,在自己的启动脚本里直接 `register_parser(...)` 后再启动/调用 LightRAG——注册表是进程内模块单例,效果相同,只是没有"装上即生效"。
## 5. 路由:让文档用上你的引擎
引擎选择优先级(`lightrag/parser/routing.py`):
1. **文件名 hint**:`report.[myengine].foo`(可带处理选项 `report.[myengine-iet].foo`);
2. **`LIGHTRAG_PARSER` 规则**:如 `LIGHTRAG_PARSER="foo:myengine,pdf:mineru"`(按后缀 glob 匹配,首条命中生效);
3. **默认**:`legacy`
API 上传时显式传 `parse_engine="myengine"` 则直接固定该引擎(存入 PENDING_PARSE 行,worker 原样履约;后缀不支持会 FAILED 而非静默回退)。注册了 `endpoint_configured` 的引擎在 endpoint 未配置时会被路由跳过(hint/规则校验也会给出 `endpoint_requirement` 提示)。
## 6. 测试建议
- **单测引擎本体**:绕开 CLI,直接 `get_parser("myengine").parse(ParseContext(fake_rag, doc_id, file_path, content_data))``fake_rag` 只需提供 `_persist_parsed_full_docs` / `_resolve_source_file_for_parser` / `full_docs` / `doc_status`(参考 `lightrag/parser/debug.py``build_debug_rag()`,或 `tests/parser/test_legacy_parser.py` 的极简 `_FakeRag`);
- **注册表是模块级单例**:测试中 `register_parser` 后,用 `finally: registry._REGISTRY.pop("myengine", None)` 清理(参考 `tests/parser/test_registry.py`);
- **entry-point 加载逻辑**:参考 `tests/parser/test_plugins.py`(monkeypatch `lightrag.parser.plugins.entry_points` 注入 fake entry point;`plugins._loaded` 标志需复位)。
+214
View File
@@ -0,0 +1,214 @@
# Third-Party Parser Engine Development and Registration Guide
LightRAG's parsing layer dispatches all parsing engines through a unified `BaseParser` contract plus the central engine registry (`lightrag/parser/registry.py`). Built-in engines (`native` / `legacy` / `mineru` / `docling`) and third-party engines use the exact same dispatch path: both pipeline workers and the debug CLI drive parsing through `get_parser(engine).parse(ParseContext(...))`, with **no special cases for built-in engines**. Therefore, a third-party package only needs to do two things:
1. **Implement** a `BaseParser` subclass;
2. **Register** a `ParserSpec` (automatic discovery through the `lightrag.parsers` entry point is recommended).
Once that is done, the engine automatically gets: a dedicated (or shared) parsing concurrency pool, three engine-selection methods (filename hints / `LIGHTRAG_PARSER` routing rules / API `parse_engine` parameter), suffix capability validation, and single-file debug support through `python -m lightrag.parser.cli --engine <name>`.
> For architecture background, see RFC #3197; for the sidecar file format, see `docs/LightRAGSidecarFormat.md`; for CLI usage, see `docs/ParserDebugCLI.md`.
---
## 1. Dispatch Flow Overview
```
Upload/scan -> enqueue(PENDING_PARSE, parse_engine=<engine>)
-> pipeline selects a concurrency pool by ParserSpec.queue_group
-> parse worker: get_parser(engine).parse(ParseContext(rag, doc_id, file_path, content_data))
+- success -> ParseResult -> enter analyze / chunk / KG pipeline
+- exception -> this document's doc_status=FAILED (only this document is affected)
```
For a single document, all engine responsibilities converge into one `parse(ctx)` call: parsing, persisting `full_docs`, archiving the source file, and returning a structured result.
## 2. Implementing a Parser
### 2.1 Contract (`lightrag/parser/base.py`)
```python
class MyParser(BaseParser):
engine_name = "myengine" # Must match ParserSpec.engine_name
async def parse(self, ctx: ParseContext) -> ParseResult: ...
```
`ParseContext` provides:
| Member | Description |
|---|---|
| `ctx.rag` | LightRAG instance (used for `_persist_parsed_full_docs`, etc.) |
| `ctx.doc_id` / `ctx.file_path` / `ctx.content_data` | Document identifier, normalized file path, and `full_docs` row |
| `ctx.resolve(engine_name)` | Returns `ResolvedSource(source_path, document_name, parsed_dir)`: resolves the on-disk source file path, normalized document name, and derived `__parsed__/<base>.parsed/` artifact directory |
| `ctx.archive_source(path)` | After parsing succeeds and `full_docs` synchronization is complete, archives the source file into `__parsed__/` |
`ParseResult` fields: `doc_id` / `file_path` / `parse_format` (`"raw"` or `"lightrag"`) / `content` / `blocks_path` (`""` when there is no sidecar) / `parse_engine` / `parse_stage_skipped` (skipped scenarios such as cache hits) / `parse_warnings` (non-fatal warnings, persisted to `doc_status.metadata`).
### 2.2 Three Implementation Paths (Choose a Base Class by Engine Type)
**A. Plain-text engine (no sidecar) - inherit `BaseParser` directly**
See `lightrag/parser/legacy/parser.py` (`LegacyParser`). Core skeleton:
```python
class MyTextParser(BaseParser):
engine_name = "myengine"
async def parse(self, ctx: ParseContext) -> ParseResult:
rs = ctx.resolve(self.engine_name)
source = rs.source_path
if not source.is_file():
raise FileNotFoundError(f"myengine source not found: {source}")
text = await asyncio.to_thread(my_extract, source) # Run CPU work in a thread
if not text.strip():
raise ValueError(f"extracted no usable text from {ctx.file_path}")
await ctx.rag._persist_parsed_full_docs(ctx.doc_id, {
"content": text,
"file_path": ctx.file_path,
"parse_format": FULL_DOCS_FORMAT_RAW,
"parse_engine": self.engine_name,
"update_time": int(time.time()),
})
await ctx.archive_source(str(source))
return ParseResult(
doc_id=ctx.doc_id, file_path=ctx.file_path,
parse_format=FULL_DOCS_FORMAT_RAW, content=text,
blocks_path="", parse_engine=self.engine_name,
)
```
**B. Local parser that produces a sidecar - inherit `NativeParserBase`** (`lightrag/parser/native_base.py`)
The template fixes the complete flow of "pre-clean artifact directory (with rollback) -> extract in a thread -> build IR -> write sidecar -> persist -> archive". You only need to implement two hooks:
```python
class MyNativeParser(NativeParserBase):
engine_name = "myengine"
def extract(self, source, *, parsed_dir, asset_dir, base_name):
"""Synchronous, runs in a thread; returns (blocks, warnings, metadata).
Assets such as images can be written to asset_dir before write_sidecar."""
def build_ir(self, blocks, *, document_name, asset_dir_name, metadata) -> IRDoc:
"""blocks -> IRDoc (handed to the shared sidecar writer)."""
```
Optional overrides: `validate_source` (by default only requires the file to exist), `surface_warnings` (maps extraction warnings to `parse_warnings`). Reference implementation: `lightrag/parser/docx/parser.py`.
**C. External parsing service (download raw bundle + cache) - inherit `ExternalParserBase`** (`lightrag/parser/external/_base.py`)
The template fixes the flow of "raw cache-hit check -> if missed, clear the directory and download again -> build IR -> write sidecar -> persist -> archive". Implement three hooks plus two class attributes:
```python
class MyExternalParser(ExternalParserBase):
engine_name = "myengine"
raw_dir_suffix = ".myengine_raw" # Raw bundle directory suffix (starts with .)
force_reparse_env = "LIGHTRAG_FORCE_REPARSE_MYENGINE"
def is_bundle_valid(self, raw_dir, source_path) -> bool: ... # Cache-hit check
async def download_into(self, raw_dir, source_path, *, upload_name): ...
def build_ir(self, raw_dir, document_name) -> IRDoc: ...
```
Optional override: `validate_ir` (post-build validation, for example failing on zero blocks). Reference implementations: `lightrag/parser/external/mineru/parser.py`, `.../docling/parser.py`.
### 2.3 Failure Semantics (Important)
- If `parse(ctx)` **raises any exception, only that document is marked FAILED**. The error message is written to `doc_status.error_msg`, and other documents in the batch are not affected.
- If parsing produces **empty content, raise an exception** instead of returning an empty string. Otherwise, a zero-knowledge document would silently enter chunking (all built-in engines follow this convention).
- Before calling the engine, the worker performs suffix guarding: if a `PENDING_PARSE` document's suffix is not included in that engine's `ParserSpec.suffixes`, the document is FAILED directly and the engine code is never called.
## 3. Declaring `ParserSpec` (Capability Metadata)
```python
from lightrag.parser.registry import ParserSpec, register_parser
register_parser(ParserSpec(
engine_name="myengine",
impl="my_pkg.parser:MyParser", # "module:Class", lazy-loaded by get_parser
suffixes=frozenset({"pdf", "foo"}), # Lowercase, no dot
queue_group="myengine", # See concurrency model below
concurrency=int(os.getenv("MAX_PARALLEL_PARSE_MYENGINE", "2")),
# Required only by external-service engines (routing skips this engine when no endpoint is configured):
endpoint_configured=lambda: bool(os.getenv("MYENGINE_ENDPOINT", "").strip()),
endpoint_requirement=lambda: "MYENGINE_ENDPOINT",
))
```
| Field | Required | Description |
|---|---|---|
| `engine_name` | yes | Registry key; also the engine name used by `--engine`, filename hints, and `LIGHTRAG_PARSER`. **Registering the same name as an existing engine overwrites the previous registration** (including built-in engines). Avoid colliding with `native/legacy/mineru/docling` unless you intentionally want to replace that implementation. |
| `impl` | yes | `"module:Class"` string. The registry imports it only when a document is actually parsed. **The implementation must never be imported early during registration** (capability queries must stay import-cheap; this is a registry design invariant). |
| `suffixes` | yes | File extensions the engine can handle (lowercase, no dot). Used for routing validation and worker-side suffix guarding. |
| `queue_group` | | Concurrency-pool group. Defaults to `"native"` (sharing the native pool). Use a unique group name for a dedicated pool. |
| `concurrency` | | Number of workers for this group (only needed for the group's sole owner). Environment-variable overrides are **baked by the registration code at registration time** (as in the `int(os.getenv(...))` example above); the registered value is authoritative. |
| `endpoint_configured` / `endpoint_requirement` | | Zero-argument closures (read env only, no network calls). The former returns whether the external service required by this engine is configured; the latter returns the configuration item name to show users when it is missing. Local engines do not need these fields (available by default). |
| `user_selectable` | | Defaults to `True`. `False` means an internal format handler (such as `reuse` / `passthrough`) that is not shown as a selectable engine. |
### Concurrency Model
- For each batch, the pipeline creates **one queue plus one worker group for every `queue_group`**;
- Worker count for a group: built-in groups (`native` / `mineru` / `docling`) are determined by LightRAG instance fields `max_parallel_parse_*` (constructor overrides are supported); a third-party dedicated group uses the `concurrency` value from the group's sole owner spec; **only one** spec in a group may declare `concurrency`, otherwise batch startup fails;
- When using `queue_group="native"` to share the built-in pool, `concurrency` does not take effect (pool size is determined by `max_parallel_parse_native`; ignored spec-level `concurrency` values are recorded as warning logs at batch startup). Lightweight local engines (such as `legacy`) fit this mode, while external-service engines should usually use a dedicated group so slow requests do not block local parsing.
## 4. Registration: Automatic Discovery via Entry Point (Recommended)
LightRAG automatically discovers third-party engines through the `lightrag.parsers` entry-point group (`lightrag/parser/plugins.py`). A third-party package only needs to:
**1. Declare the entry point in its own `pyproject.toml`:**
```toml
[project.entry-points."lightrag.parsers"]
myengine = "my_pkg.lightrag_plugin:register"
```
**2. Provide a zero-argument registration function:**
```python
# my_pkg/lightrag_plugin.py - keep imports cheap: do not import parser implementations here
import os
from lightrag.parser.registry import ParserSpec, register_parser
def register() -> None:
register_parser(ParserSpec(
engine_name="myengine",
impl="my_pkg.parser:MyParser", # Implementation is lazy-loaded
suffixes=frozenset({"foo"}),
queue_group="myengine",
concurrency=int(os.getenv("MAX_PARALLEL_PARSE_MYENGINE", "2")),
))
```
After `pip install my-pkg`, it works immediately without changing LightRAG code:
- **API Server**: `create_app()` calls `load_third_party_parsers()` **before** validating `LIGHTRAG_PARSER` routing rules, so routing rules can directly reference third-party engine names (for example, `LIGHTRAG_PARSER="foo:myengine"`). File-type guarding for uploads and scans is **derived entirely from the registry plus routing at runtime**. The criterion is "can this file route to an engine that supports it": a bare suffix (no hint) requires a `LIGHTRAG_PARSER` rule that routes it to your engine (otherwise the default `legacy` engine cannot handle it, so the file is rejected instead of being accepted and later FAILED during parsing); uploads with a filename hint (for example, `report.[myengine].foo`) pass without a rule; unique suffixes of engines whose endpoints are not configured do not participate (the same rule used by built-in mineru/docling image formats). **Practical recommendation: when publishing a third-party engine, tell users to configure the matching `LIGHTRAG_PARSER="foo:myengine"` rule in deployment docs**, so bare-filename uploads and directory scans work automatically;
- **Debug CLI**: `python -m lightrag.parser.cli sample.foo --engine myengine` works directly (`main()` loads plugins before building `--engine` choices). For engines without sidecars (`blocks_path=""`), the CLI prints a plain-text summary instead of a block summary; engines inheriting `ExternalParserBase` automatically get raw cache display and `--force-reparse` support;
- **Embedded library usage** (using the `LightRAG` class directly without going through the server or CLI): call this once before building the pipeline:
```python
from lightrag.parser.plugins import load_third_party_parsers
load_third_party_parsers() # Idempotent within the process
```
Loading semantics: idempotent per process (repeated calls are no-ops); **if a single plugin raises an exception, it is only logged and skipped**. It does not affect other plugins or built-in engines, and it does not block server startup. However, that engine will be unavailable, so watch for `[parser-plugins]` lines in startup logs.
> If you do not want to publish a package, you can skip entry points and directly call `register_parser(...)` in your own startup script before starting/calling LightRAG. The registry is an in-process module singleton, so the effect is the same, just without "install and it works" behavior.
## 5. Routing: Making Documents Use Your Engine
Engine selection priority (`lightrag/parser/routing.py`):
1. **Filename hint**: `report.[myengine].foo` (processing options are allowed, such as `report.[myengine-iet].foo`);
2. **`LIGHTRAG_PARSER` rules**: for example, `LIGHTRAG_PARSER="foo:myengine,pdf:mineru"` (matched by suffix glob; the first match wins);
3. **Default**: `legacy`.
When API upload explicitly passes `parse_engine="myengine"`, that engine is fixed directly (stored in the `PENDING_PARSE` row and honored verbatim by the worker; unsupported suffixes are FAILED instead of silently falling back). Engines that register `endpoint_configured` are skipped by routing when the endpoint is not configured (hint/rule validation also shows the `endpoint_requirement` prompt).
## 6. Testing Recommendations
- **Unit-test the engine itself**: bypass the CLI and directly call `get_parser("myengine").parse(ParseContext(fake_rag, doc_id, file_path, content_data))`. `fake_rag` only needs to provide `_persist_parsed_full_docs` / `_resolve_source_file_for_parser` / `full_docs` / `doc_status` (see `build_debug_rag()` in `lightrag/parser/debug.py`, or the minimal `_FakeRag` in `tests/parser/test_legacy_parser.py`);
- **The registry is a module-level singleton**: after calling `register_parser` in a test, clean up with `finally: registry._REGISTRY.pop("myengine", None)` (see `tests/parser/test_registry.py`);
- **Entry-point loading logic**: see `tests/parser/test_plugins.py` (monkeypatch `lightrag.parser.plugins.entry_points` to inject a fake entry point; the `plugins._loaded` flag must be reset).
+170
View File
@@ -0,0 +1,170 @@
# uv.lock Update Guide
## What is uv.lock?
`uv.lock` is uv's lock file. It captures the exact version of every dependency, including transitive ones, much like:
- Node.js `package-lock.json`
- Rust `Cargo.lock`
- Python Poetry `poetry.lock`
Keeping `uv.lock` in version control guarantees that everyone installs the same dependency set.
## When does uv.lock change?
### Situations where it does *not* change automatically
- Running `uv sync --frozen`
- Building Docker images that call `uv sync --frozen`
- Editing source code without touching dependency metadata
### Situations where it will change
1. **`uv lock` or `uv lock --upgrade`**
```bash
uv lock # Resolve according to current constraints
uv lock --upgrade # Re-resolve and upgrade to the newest compatible releases
```
Use these commands after modifying `pyproject.toml`, when you want fresh dependency versions, or if the lock file was deleted or corrupted.
2. **`uv add`**
```bash
uv add requests # Adds the dependency and updates both files
uv add --dev pytest # Adds a dev dependency
```
`uv add` edits `pyproject.toml` and refreshes `uv.lock` in one step.
3. **`uv remove`**
```bash
uv remove requests
```
This removes the dependency from `pyproject.toml` and rewrites `uv.lock`.
4. **`uv sync` without `--frozen`**
```bash
uv sync
```
Normally this only installs what is already locked. However, if `pyproject.toml` and `uv.lock` disagree or the lock file is missing, uv will regenerate and update `uv.lock`. In CI and production builds you should prefer `uv sync --frozen` to prevent unintended updates.
## Example workflows
### Scenario 1: Add a new dependency
```bash
# Recommended: let uv handle both files
uv add fastapi
git add pyproject.toml uv.lock
git commit -m "Add fastapi dependency"
# Manual alternative
# 1. Edit pyproject.toml
# 2. Regenerate the lock file
uv lock
git add pyproject.toml uv.lock
git commit -m "Add fastapi dependency"
```
### Scenario 2: Relax or tighten a version constraint
```bash
# 1. Edit the requirement in pyproject.toml,
# e.g. openai>=1.0.0,<2.0.0 -> openai>=1.5.0,<2.0.0
# 2. Re-resolve the lock file
uv lock
# 3. Commit both files
git add pyproject.toml uv.lock
git commit -m "Update openai to >=1.5.0"
```
### Scenario 3: Upgrade everything to the newest compatible versions
```bash
uv lock --upgrade
git diff uv.lock
git add uv.lock
git commit -m "Upgrade dependencies to latest compatible versions"
```
### Scenario 4: Teammate syncing the project
```bash
git pull # Fetch latest code and lock file
uv sync --frozen # Install exactly what uv.lock specifies
```
## Using uv.lock in Docker
```dockerfile
RUN uv sync --frozen --no-dev --extra api
```
`--frozen` guarantees reproducible builds because uv will refuse to deviate from the locked versions.
`--extra api` install API server
## Generating a lock file that includes offline dependencies
If you need `uv.lock` to capture the optional offline stacks, regenerate it with the relevant extras enabled:
```bash
uv lock --extra api --extra offline
```
This command resolves the base project requirements plus both the `api` and `offline` optional dependency sets, ensuring downstream `uv sync --frozen --extra api --extra offline` installs work without further resolution.
## Frequently asked questions
- **`uv.lock` is almost 1MB. Does that matter?**
No. The file is read only during dependency resolution.
- **Should we commit `uv.lock`?**
Yes. Commit it so collaborators and CI jobs share the same dependency graph.
- **Deleted the lock file by accident?**
Run `uv lock` to regenerate it from `pyproject.toml`.
- **Can `uv.lock` and `requirements.txt` coexist?**
They can, but maintaining both is redundant. Prefer relying on `uv.lock` alone whenever possible.
- **How do I inspect locked versions?**
```bash
uv tree
grep -A5 'name = "openai"' uv.lock
```
## Best practices
### Recommended
1. Commit `uv.lock` alongside `pyproject.toml`.
2. Use `uv sync --frozen` in CI, Docker, and other reproducible environments.
3. Use plain `uv sync` during local development if you want uv to reconcile the lock for you.
4. Run `uv lock --upgrade` periodically to pick up the latest compatible releases.
5. Regenerate the lock file immediately after changing dependency constraints.
### Avoid
1. Running `uv sync` without `--frozen` in CI or production pipelines.
2. Editing `uv.lock` by hand—uv will overwrite manual edits.
3. Ignoring lock file diffs in code reviews—unexpected dependency changes can break builds.
## Summary
| Command | Updates `uv.lock` | Typical use |
|-----------------------|-------------------|-------------------------------------------|
| `uv lock` | ✅ Yes | After editing constraints |
| `uv lock --upgrade` | ✅ Yes | Upgrade to the newest compatible versions |
| `uv add <pkg>` | ✅ Yes | Add a dependency |
| `uv remove <pkg>` | ✅ Yes | Remove a dependency |
| `uv sync` | ⚠️ Maybe | Local development; can regenerate the lock |
| `uv sync --frozen` | ❌ No | CI/CD, Docker, reproducible builds |
Remember: `uv.lock` only changes when you run a command that tells it to. Keep it in sync with your project and commit it whenever it changes.
File diff suppressed because it is too large Load Diff
+1150
View File
File diff suppressed because it is too large Load Diff
+57
View File
@@ -0,0 +1,57 @@
from openai import OpenAI
# os.environ["OPENAI_API_KEY"] = ""
def openai_complete_if_cache(
model="gpt-4o-mini", prompt=None, system_prompt=None, history_messages=[], **kwargs
) -> str:
openai_client = OpenAI()
messages = []
if system_prompt:
messages.append({"role": "system", "content": system_prompt})
messages.extend(history_messages)
messages.append({"role": "user", "content": prompt})
response = openai_client.chat.completions.create(
model=model, messages=messages, **kwargs
)
if not response.choices or response.choices[0].message is None:
return ""
return response.choices[0].message.content
if __name__ == "__main__":
description = ""
prompt = f"""
Given the following description of a dataset:
{description}
Please identify 5 potential users who would engage with this dataset. For each user, list 5 tasks they would perform with this dataset. Then, for each (user, task) combination, generate 5 questions that require a high-level understanding of the entire dataset.
Output the results in the following structure:
- User 1: [user description]
- Task 1: [task description]
- Question 1:
- Question 2:
- Question 3:
- Question 4:
- Question 5:
- Task 2: [task description]
...
- Task 5: [task description]
- User 2: [user description]
...
- User 5: [user description]
...
"""
result = openai_complete_if_cache(model="gpt-4o-mini", prompt=prompt)
file_path = "./queries.txt"
with open(file_path, "w") as file:
file.write(result)
print(f"Queries written to {file_path}")
+34
View File
@@ -0,0 +1,34 @@
import pipmaster as pm
if not pm.is_installed("pyvis"):
pm.install("pyvis")
if not pm.is_installed("networkx"):
pm.install("networkx")
import networkx as nx
from pyvis.network import Network
import random
# Load the GraphML file
G = nx.read_graphml("./dickens/graph_chunk_entity_relation.graphml")
# Create a Pyvis network
net = Network(height="100vh", notebook=True)
# Convert NetworkX graph to Pyvis network
net.from_nx(G)
# Add colors and title to nodes
for node in net.nodes:
node["color"] = "#{:06x}".format(random.randint(0, 0xFFFFFF))
if "description" in node:
node["title"] = node["description"]
# Add title to edges
for edge in net.edges:
if "description" in edge:
edge["title"] = edge["description"]
# Save and display the network
net.show("knowledge_graph.html")
+186
View File
@@ -0,0 +1,186 @@
import os
import json
import xml.etree.ElementTree as ET
from neo4j import GraphDatabase
# Constants
WORKING_DIR = "./dickens"
BATCH_SIZE_NODES = 500
BATCH_SIZE_EDGES = 100
# Neo4j connection credentials
NEO4J_URI = "bolt://localhost:7687"
NEO4J_USERNAME = "neo4j"
NEO4J_PASSWORD = "your_password"
def xml_to_json(xml_file):
try:
tree = ET.parse(xml_file)
root = tree.getroot()
# Print the root element's tag and attributes to confirm the file has been correctly loaded
print(f"Root element: {root.tag}")
print(f"Root attributes: {root.attrib}")
data = {"nodes": [], "edges": []}
# Use namespace
namespace = {"": "http://graphml.graphdrawing.org/xmlns"}
for node in root.findall(".//node", namespace):
node_data = {
"id": node.get("id").strip('"'),
"entity_type": node.find("./data[@key='d1']", namespace).text.strip('"')
if node.find("./data[@key='d1']", namespace) is not None
else "",
"description": node.find("./data[@key='d2']", namespace).text
if node.find("./data[@key='d2']", namespace) is not None
else "",
"source_id": node.find("./data[@key='d3']", namespace).text
if node.find("./data[@key='d3']", namespace) is not None
else "",
}
data["nodes"].append(node_data)
for edge in root.findall(".//edge", namespace):
edge_data = {
"source": edge.get("source").strip('"'),
"target": edge.get("target").strip('"'),
"weight": float(edge.find("./data[@key='d5']", namespace).text)
if edge.find("./data[@key='d5']", namespace) is not None
else 0.0,
"description": edge.find("./data[@key='d6']", namespace).text
if edge.find("./data[@key='d6']", namespace) is not None
else "",
"keywords": edge.find("./data[@key='d9']", namespace).text
if edge.find("./data[@key='d9']", namespace) is not None
else "",
"source_id": edge.find("./data[@key='d8']", namespace).text
if edge.find("./data[@key='d8']", namespace) is not None
else "",
}
data["edges"].append(edge_data)
# Print the number of nodes and edges found
print(f"Found {len(data['nodes'])} nodes and {len(data['edges'])} edges")
return data
except ET.ParseError as e:
print(f"Error parsing XML file: {e}")
return None
except Exception as e:
print(f"An error occurred: {e}")
return None
def convert_xml_to_json(xml_path, output_path):
"""Converts XML file to JSON and saves the output."""
if not os.path.exists(xml_path):
print(f"Error: File not found - {xml_path}")
return None
json_data = xml_to_json(xml_path)
if json_data:
with open(output_path, "w", encoding="utf-8") as f:
json.dump(json_data, f, ensure_ascii=False, indent=2)
print(f"JSON file created: {output_path}")
return json_data
else:
print("Failed to create JSON data")
return None
def process_in_batches(tx, query, data, batch_size):
"""Process data in batches and execute the given query."""
for i in range(0, len(data), batch_size):
batch = data[i : i + batch_size]
tx.run(query, {"nodes": batch} if "nodes" in query else {"edges": batch})
def main():
# Paths
xml_file = os.path.join(WORKING_DIR, "graph_chunk_entity_relation.graphml")
json_file = os.path.join(WORKING_DIR, "graph_data.json")
# Convert XML to JSON
json_data = convert_xml_to_json(xml_file, json_file)
if json_data is None:
return
# Load nodes and edges
nodes = json_data.get("nodes", [])
edges = json_data.get("edges", [])
# Neo4j queries
create_nodes_query = """
UNWIND $nodes AS node
MERGE (e:Entity {id: node.id})
SET e.entity_type = node.entity_type,
e.description = node.description,
e.source_id = node.source_id,
e.displayName = node.id
REMOVE e:Entity
WITH e, node
CALL apoc.create.addLabels(e, [node.id]) YIELD node AS labeledNode
RETURN count(*)
"""
create_edges_query = """
UNWIND $edges AS edge
MATCH (source {id: edge.source})
MATCH (target {id: edge.target})
WITH source, target, edge,
CASE
WHEN edge.keywords CONTAINS 'lead' THEN 'lead'
WHEN edge.keywords CONTAINS 'participate' THEN 'participate'
WHEN edge.keywords CONTAINS 'uses' THEN 'uses'
WHEN edge.keywords CONTAINS 'located' THEN 'located'
WHEN edge.keywords CONTAINS 'occurs' THEN 'occurs'
ELSE REPLACE(SPLIT(edge.keywords, ',')[0], '\"', '')
END AS relType
CALL apoc.create.relationship(source, relType, {
weight: edge.weight,
description: edge.description,
keywords: edge.keywords,
source_id: edge.source_id
}, target) YIELD rel
RETURN count(*)
"""
set_displayname_and_labels_query = """
MATCH (n)
SET n.displayName = n.id
WITH n
CALL apoc.create.setLabels(n, [n.entity_type]) YIELD node
RETURN count(*)
"""
# Create a Neo4j driver
driver = GraphDatabase.driver(NEO4J_URI, auth=(NEO4J_USERNAME, NEO4J_PASSWORD))
try:
# Execute queries in batches
with driver.session() as session:
# Insert nodes in batches
session.execute_write(
process_in_batches, create_nodes_query, nodes, BATCH_SIZE_NODES
)
# Insert edges in batches
session.execute_write(
process_in_batches, create_edges_query, edges, BATCH_SIZE_EDGES
)
# Set displayName and labels
session.run(set_displayname_and_labels_query)
except Exception as e:
print(f"Error occurred: {e}")
finally:
driver.close()
if __name__ == "__main__":
main()
+166
View File
@@ -0,0 +1,166 @@
"""
Knowledge Graph Visualization with OpenSearch + LightRAG WebUI
This script demonstrates two ways to visualize the knowledge graph
stored in OpenSearch:
1. **WebUI (recommended)**: Opens the LightRAG WebUI in your browser
for interactive graph exploration with search, filtering, and
force-directed layout.
2. **Standalone HTML**: Fetches graph data from the LightRAG Server API
and generates an interactive HTML file using Pyvis, similar to
graph_visual_with_html.py but reading from OpenSearch instead of
a local .graphml file.
Prerequisites:
1. LightRAG Server running with OpenSearch storage:
lightrag-server --host 0.0.0.0 --port 9621
2. Documents already indexed (e.g., via the WebUI or API)
Usage:
# Open WebUI for interactive exploration
python examples/graph_visual_with_opensearch.py
# Generate standalone HTML file
python examples/graph_visual_with_opensearch.py --html
# Custom server URL and output file
python examples/graph_visual_with_opensearch.py --html --server http://localhost:9621 --output my_graph.html
"""
import argparse
import os
import sys
import webbrowser
import pipmaster as pm
if not pm.is_installed("requests"):
pm.install("requests")
if not pm.is_installed("pyvis"):
pm.install("pyvis")
import requests
from pyvis.network import Network
def fetch_graph(server_url: str, label: str = "*", max_nodes: int = 300) -> dict:
"""Fetch knowledge graph data from LightRAG Server API."""
url = f"{server_url}/graphs"
params = {"label": label, "max_nodes": max_nodes}
resp = requests.get(url, params=params, timeout=30)
resp.raise_for_status()
return resp.json()
def generate_html(graph_data: dict, output_file: str) -> str:
"""Generate an interactive HTML visualization from graph data."""
nodes = graph_data.get("nodes", [])
edges = graph_data.get("edges", [])
if not nodes:
print("No nodes found in the graph. Index some documents first.")
sys.exit(1)
print(f"Building visualization: {len(nodes)} nodes, {len(edges)} edges")
net = Network(height="100vh", notebook=False, cdn_resources="in_line")
# Add nodes with colors based on entity type
import hashlib
for node in nodes:
node_id = node.get("id", "")
props = node.get("properties", {})
entity_type = props.get("entity_type", "unknown")
description = props.get("description", "")
# Deterministic color from entity type
color_hash = int(hashlib.md5(entity_type.encode()).hexdigest()[:6], 16)
color = f"#{color_hash:06x}"
net.add_node(
node_id,
label=node_id,
title=f"[{entity_type}] {description[:200]}"
if description
else entity_type,
color=color,
)
# Add edges
for edge in edges:
source = edge.get("source", "")
target = edge.get("target", "")
props = edge.get("properties", {})
rel_type = edge.get("type", "")
description = props.get("description", "")
net.add_edge(
source,
target,
title=f"[{rel_type}] {description[:200]}" if description else rel_type,
label=rel_type,
)
net.save_graph(output_file)
print(f"Graph saved to {output_file}")
return output_file
def main():
parser = argparse.ArgumentParser(
description="Visualize LightRAG knowledge graph from OpenSearch"
)
parser.add_argument(
"--html",
action="store_true",
help="Generate standalone HTML file instead of opening WebUI",
)
parser.add_argument(
"--server",
default="http://localhost:9621",
help="LightRAG Server URL (default: http://localhost:9621)",
)
parser.add_argument(
"--output",
default="knowledge_graph_opensearch.html",
help="Output HTML file (default: knowledge_graph_opensearch.html)",
)
parser.add_argument(
"--label",
default="*",
help="Starting node label, or '*' for all nodes (default: *)",
)
parser.add_argument(
"--max-nodes",
type=int,
default=300,
help="Maximum nodes to fetch (default: 300)",
)
args = parser.parse_args()
# Verify server is running
try:
requests.get(f"{args.server}/health", timeout=5)
except requests.ConnectionError:
print(f"Error: Cannot connect to LightRAG Server at {args.server}")
print("Start the server first: lightrag-server --host 0.0.0.0 --port 9621")
sys.exit(1)
if args.html:
# Generate standalone HTML
graph_data = fetch_graph(args.server, args.label, args.max_nodes)
output = generate_html(graph_data, args.output)
webbrowser.open(f"file://{os.path.abspath(output)}")
else:
# Open WebUI graph explorer
url = f"{args.server}/#/graph"
print(f"Opening LightRAG WebUI graph explorer: {url}")
webbrowser.open(url)
if __name__ == "__main__":
main()
+115
View File
@@ -0,0 +1,115 @@
import os
from lightrag import LightRAG
from lightrag.llm.openai import gpt_4o_mini_complete
#########
# Uncomment the below two lines if running in a jupyter notebook to handle the async nature of rag.insert()
# import nest_asyncio
# nest_asyncio.apply()
#########
WORKING_DIR = "./custom_kg"
if not os.path.exists(WORKING_DIR):
os.mkdir(WORKING_DIR)
rag = LightRAG(
working_dir=WORKING_DIR,
llm_model_func=gpt_4o_mini_complete, # Use gpt_4o_mini_complete LLM model
# llm_model_func=gpt_4o_complete # Optionally, use a stronger model
)
custom_kg = {
"entities": [
{
"entity_name": "CompanyA",
"entity_type": "Organization",
"description": "A major technology company",
"source_id": "Source1",
},
{
"entity_name": "ProductX",
"entity_type": "Product",
"description": "A popular product developed by CompanyA",
"source_id": "Source1",
},
{
"entity_name": "PersonA",
"entity_type": "Person",
"description": "A renowned researcher in AI",
"source_id": "Source2",
},
{
"entity_name": "UniversityB",
"entity_type": "Organization",
"description": "A leading university specializing in technology and sciences",
"source_id": "Source2",
},
{
"entity_name": "CityC",
"entity_type": "Location",
"description": "A large metropolitan city known for its culture and economy",
"source_id": "Source3",
},
{
"entity_name": "EventY",
"entity_type": "Event",
"description": "An annual technology conference held in CityC",
"source_id": "Source3",
},
],
"relationships": [
{
"src_id": "CompanyA",
"tgt_id": "ProductX",
"description": "CompanyA develops ProductX",
"keywords": "develop, produce",
"weight": 1.0,
"source_id": "Source1",
},
{
"src_id": "PersonA",
"tgt_id": "UniversityB",
"description": "PersonA works at UniversityB",
"keywords": "employment, affiliation",
"weight": 0.9,
"source_id": "Source2",
},
{
"src_id": "CityC",
"tgt_id": "EventY",
"description": "EventY is hosted in CityC",
"keywords": "host, location",
"weight": 0.8,
"source_id": "Source3",
},
],
"chunks": [
{
"content": "ProductX, developed by CompanyA, has revolutionized the market with its cutting-edge features.",
"source_id": "Source1",
"source_chunk_index": 0,
},
{
"content": "One outstanding feature of ProductX is its advanced AI capabilities.",
"source_id": "Source1",
"chunk_order_index": 1,
},
{
"content": "PersonA is a prominent researcher at UniversityB, focusing on artificial intelligence and machine learning.",
"source_id": "Source2",
"source_chunk_index": 0,
},
{
"content": "EventY, held in CityC, attracts technology enthusiasts and companies from around the globe.",
"source_id": "Source3",
"source_chunk_index": 0,
},
{
"content": "None",
"source_id": "UNKNOWN",
"source_chunk_index": 0,
},
],
}
rag.insert_custom_kg(custom_kg)
+296
View File
@@ -0,0 +1,296 @@
"""LightRAG + AG2 Multi-Agent Demo.
Demonstrates how AG2 agents can use LightRAG's knowledge graph retrieval
as a tool. Multiple specialized agents collaborate to answer complex
questions over indexed documents.
Architecture:
User -> AG2 GroupChat (Researcher + Analyst + Writer) -> LightRAG queries
- Researcher: uses LightRAG hybrid search to gather facts
- Analyst: uses LightRAG naive (vector) search for complementary results
- Writer: synthesizes findings into a final answer
Requires:
pip install lightrag-hku "ag2[openai]>=0.11.4,<1.0"
export OPENAI_API_KEY="..."
Usage:
python examples/lightrag_ag2_multiagent_demo.py
"""
import asyncio
import json
import os
import shutil
import threading
from autogen import (
AssistantAgent,
GroupChat,
GroupChatManager,
LLMConfig,
UserProxyAgent,
)
from lightrag import LightRAG, QueryParam
from lightrag.llm.openai import gpt_4o_mini_complete, openai_embed
# --- Configuration ---
WORKING_DIR = "./ag2_demo_workdir"
SAMPLE_TEXT = """
Artificial intelligence has transformed multiple industries. Machine learning,
a subset of AI, enables systems to learn from data without explicit programming.
Deep learning, using neural networks with many layers, has achieved breakthroughs
in computer vision, natural language processing, and speech recognition.
Transformer architectures, introduced in the 2017 paper "Attention Is All You Need"
by Vaswani et al., revolutionized NLP. Models like GPT and BERT are built on
transformers. GPT (Generative Pre-trained Transformer) uses decoder-only architecture
for text generation, while BERT (Bidirectional Encoder Representations) uses
encoder-only architecture for understanding tasks.
Retrieval-Augmented Generation (RAG) combines the strengths of retrieval systems
and generative models. Instead of relying solely on parametric knowledge, RAG
systems retrieve relevant documents from a knowledge base and use them as context
for generation. This approach reduces hallucination and enables models to access
up-to-date information.
Knowledge graphs represent information as entities and relationships. When combined
with RAG, knowledge graphs enable structured reasoning over document collections.
LightRAG implements this approach with dual-level retrieval: local search focuses
on specific entities, while global search captures broader themes and relationships.
"""
# --- LightRAG Setup ---
async def setup_lightrag() -> LightRAG:
"""Initialize LightRAG and index sample documents."""
if os.path.exists(WORKING_DIR):
shutil.rmtree(WORKING_DIR)
os.makedirs(WORKING_DIR, exist_ok=True)
rag = LightRAG(
working_dir=WORKING_DIR,
embedding_func=openai_embed,
llm_model_func=gpt_4o_mini_complete,
)
await rag.initialize_storages()
await rag.ainsert(SAMPLE_TEXT)
print("LightRAG initialized and documents indexed.\n")
return rag
# --- Async Bridge ---
# AG2 runs tools in a background thread without an event loop.
# We maintain a dedicated event loop in a separate thread for LightRAG async calls.
_bg_loop: asyncio.AbstractEventLoop = None
def _start_background_loop(loop: asyncio.AbstractEventLoop):
asyncio.set_event_loop(loop)
loop.run_forever()
def _run_async(coro):
"""Submit a coroutine to the background event loop and wait for the result."""
future = asyncio.run_coroutine_threadsafe(coro, _bg_loop)
return future.result(timeout=120)
# --- AG2 Agent Tools ---
# Global reference to LightRAG instance (set in main)
_rag_instance: LightRAG = None
def create_agents():
"""Create AG2 agents with LightRAG tools."""
llm_config = LLMConfig(
{
"model": os.environ.get("OPENAI_MODEL", "gpt-4o-mini"),
"api_key": os.environ["OPENAI_API_KEY"],
"api_type": "openai",
}
)
researcher = AssistantAgent(
name="Researcher",
system_message=(
"You are a research specialist. Use the lightrag_query tool to search "
"the knowledge base. Start with 'hybrid' mode for comprehensive results. "
"If you need specific entity details, use 'local' mode. "
"Present your findings as structured bullet points. "
"Always call the tool -- do NOT answer from your own knowledge."
),
llm_config=llm_config,
)
analyst = AssistantAgent(
name="Analyst",
system_message=(
"You are a knowledge graph analyst. Your FIRST action MUST be calling "
"the lightrag_query tool with mode='naive' to run a direct vector search. "
"This gives different results from the Researcher's hybrid search. "
"After receiving the naive search results, compare them with the "
"Researcher's findings and highlight any additional insights. "
"You MUST call the tool before writing any analysis."
),
llm_config=llm_config,
)
writer = AssistantAgent(
name="Writer",
system_message=(
"You are a technical writer. Synthesize the findings from the "
"Researcher and Analyst into a clear, well-structured answer. "
"Do NOT use the search tool -- work only with what the other agents "
"have found. End your response with TERMINATE."
),
llm_config=llm_config,
)
def is_termination(msg):
return "TERMINATE" in (msg.get("content") or "")
user_proxy = UserProxyAgent(
name="User",
human_input_mode="NEVER",
max_consecutive_auto_reply=10,
code_execution_config=False,
is_termination_msg=is_termination,
)
# --- Register LightRAG as a tool ---
@user_proxy.register_for_execution()
@researcher.register_for_llm(
description=(
"Query the LightRAG knowledge base. "
"mode: 'naive' (simple vector), 'local' (entity-focused), "
"'global' (theme/relationship-focused), 'hybrid' (combined). "
"Returns retrieved context from indexed documents."
)
)
@analyst.register_for_llm(
description=(
"Query the LightRAG knowledge base. "
"mode: 'naive' (simple vector), 'local' (entity-focused), "
"'global' (theme/relationship-focused), 'hybrid' (combined). "
"Returns retrieved context from indexed documents."
)
)
def lightrag_query(query: str, mode: str = "hybrid") -> str:
"""Query LightRAG synchronously (wraps async call)."""
valid_modes = {"naive", "local", "global", "hybrid"}
if mode not in valid_modes:
return json.dumps(
{"error": f"Invalid mode '{mode}'. Use one of: {valid_modes}"}
)
try:
result = _run_async(
_rag_instance.aquery(query, param=QueryParam(mode=mode))
)
return json.dumps({"mode": mode, "query": query, "result": result})
except Exception as e:
return json.dumps({"error": str(e)})
return user_proxy, researcher, analyst, writer
def run_multiagent_query(user_proxy, researcher, analyst, writer, question: str):
"""Run a multi-agent GroupChat to answer a question using LightRAG."""
# Enforce pipeline: Researcher -> Analyst -> Writer.
# func_call_filter (default True) automatically routes tool calls
# to/from user_proxy, so transitions only govern non-tool handoffs.
# User can only start with Researcher; Researcher advances to Analyst;
# Analyst advances to Writer. Writer terminates the conversation.
allowed_transitions = {
user_proxy: [researcher],
researcher: [user_proxy, analyst],
analyst: [user_proxy, writer],
writer: [],
}
group_chat = GroupChat(
agents=[user_proxy, researcher, analyst, writer],
messages=[],
max_round=12,
allowed_or_disallowed_speaker_transitions=allowed_transitions,
speaker_transitions_type="allowed",
)
manager = GroupChatManager(
groupchat=group_chat,
llm_config=LLMConfig(
{
"model": os.environ.get("OPENAI_MODEL", "gpt-4o-mini"),
"api_key": os.environ["OPENAI_API_KEY"],
"api_type": "openai",
}
),
is_termination_msg=lambda msg: "TERMINATE" in (msg.get("content") or ""),
)
print(f"Question: {question}\n{'=' * 60}\n")
user_proxy.run(manager, message=question).process()
print(f"\n{'=' * 60}")
# --- Main ---
def main():
global _rag_instance, _bg_loop
if not os.getenv("OPENAI_API_KEY"):
print(
"Error: OPENAI_API_KEY environment variable is not set.\n"
"Set it by running: export OPENAI_API_KEY='your-openai-api-key'"
)
return
# Start a background event loop for LightRAG async calls.
# AG2 tools run in threads without an event loop, so we need a
# persistent loop that can accept coroutines from any thread.
_bg_loop = asyncio.new_event_loop()
bg_thread = threading.Thread(
target=_start_background_loop, args=(_bg_loop,), daemon=True
)
bg_thread.start()
try:
# Step 1: Set up LightRAG (async, runs on the background loop)
_rag_instance = _run_async(setup_lightrag())
# Step 2: Create AG2 agents with LightRAG tools
user_proxy, researcher, analyst, writer = create_agents()
# Step 3: Ask a complex question
run_multiagent_query(
user_proxy,
researcher,
analyst,
writer,
question=(
"How do transformer architectures relate to RAG systems? "
"What role do knowledge graphs play in improving retrieval quality?"
),
)
except Exception as e:
print(f"An error occurred: {e}")
finally:
if _rag_instance:
_run_async(_rag_instance.finalize_storages())
_bg_loop.call_soon_threadsafe(_bg_loop.stop)
bg_thread.join(timeout=5)
shutil.rmtree(WORKING_DIR, ignore_errors=True)
if __name__ == "__main__":
main()
print("\nDone!")
+125
View File
@@ -0,0 +1,125 @@
import os
import asyncio
from lightrag import LightRAG, QueryParam
from lightrag.utils import EmbeddingFunc
import numpy as np
from dotenv import load_dotenv
import logging
from openai import AzureOpenAI
logging.basicConfig(level=logging.INFO)
load_dotenv()
AZURE_OPENAI_API_VERSION = os.getenv("AZURE_OPENAI_API_VERSION")
AZURE_OPENAI_DEPLOYMENT = os.getenv("AZURE_OPENAI_DEPLOYMENT")
AZURE_OPENAI_API_KEY = os.getenv("AZURE_OPENAI_API_KEY")
AZURE_OPENAI_ENDPOINT = os.getenv("AZURE_OPENAI_ENDPOINT")
AZURE_EMBEDDING_DEPLOYMENT = os.getenv("AZURE_EMBEDDING_DEPLOYMENT")
AZURE_EMBEDDING_API_VERSION = os.getenv("AZURE_EMBEDDING_API_VERSION")
WORKING_DIR = "./dickens"
if os.path.exists(WORKING_DIR):
import shutil
shutil.rmtree(WORKING_DIR)
os.mkdir(WORKING_DIR)
async def llm_model_func(
prompt, system_prompt=None, history_messages=[], keyword_extraction=False, **kwargs
) -> str:
client = AzureOpenAI(
api_key=AZURE_OPENAI_API_KEY,
api_version=AZURE_OPENAI_API_VERSION,
azure_endpoint=AZURE_OPENAI_ENDPOINT,
)
messages = []
if system_prompt:
messages.append({"role": "system", "content": system_prompt})
if history_messages:
messages.extend(history_messages)
messages.append({"role": "user", "content": prompt})
chat_completion = client.chat.completions.create(
model=AZURE_OPENAI_DEPLOYMENT, # model = "deployment_name".
messages=messages,
temperature=kwargs.get("temperature", 0),
top_p=kwargs.get("top_p", 1),
n=kwargs.get("n", 1),
)
if not chat_completion.choices or chat_completion.choices[0].message is None:
return ""
return chat_completion.choices[0].message.content
async def embedding_func(texts: list[str]) -> np.ndarray:
client = AzureOpenAI(
api_key=AZURE_OPENAI_API_KEY,
api_version=AZURE_EMBEDDING_API_VERSION,
azure_endpoint=AZURE_OPENAI_ENDPOINT,
)
embedding = client.embeddings.create(model=AZURE_EMBEDDING_DEPLOYMENT, input=texts)
embeddings = [item.embedding for item in embedding.data]
return np.array(embeddings)
async def test_funcs():
result = await llm_model_func("How are you?")
print("Resposta do llm_model_func: ", result)
result = await embedding_func(["How are you?"])
print("Resultado do embedding_func: ", result.shape)
print("Dimensão da embedding: ", result.shape[1])
asyncio.run(test_funcs())
embedding_dimension = 3072
async def initialize_rag():
rag = LightRAG(
working_dir=WORKING_DIR,
llm_model_func=llm_model_func,
embedding_func=EmbeddingFunc(
embedding_dim=embedding_dimension,
max_token_size=8192,
func=embedding_func,
),
)
await rag.initialize_storages() # Auto-initializes pipeline_status
return rag
def main():
rag = asyncio.run(initialize_rag())
book1 = open("./book_1.txt", encoding="utf-8")
book2 = open("./book_2.txt", encoding="utf-8")
rag.insert([book1.read(), book2.read()])
query_text = "What are the main themes?"
print("Result (Naive):")
print(rag.query(query_text, param=QueryParam(mode="naive")))
print("\nResult (Local):")
print(rag.query(query_text, param=QueryParam(mode="local")))
print("\nResult (Global):")
print(rag.query(query_text, param=QueryParam(mode="global")))
print("\nResult (Hybrid):")
print(rag.query(query_text, param=QueryParam(mode="hybrid")))
if __name__ == "__main__":
main()
+122
View File
@@ -0,0 +1,122 @@
"""
LightRAG Demo with Google Gemini Models
This example demonstrates how to use LightRAG with Google's Gemini 2.0 Flash model
for text generation and the text-embedding-004 model for embeddings.
Prerequisites:
1. Set GEMINI_API_KEY environment variable:
export GEMINI_API_KEY='your-actual-api-key'
2. Prepare a text file named 'book.txt' in the current directory
(or modify BOOK_FILE constant to point to your text file)
Usage:
python examples/lightrag_gemini_demo.py
"""
import os
import asyncio
import nest_asyncio
import numpy as np
from lightrag import LightRAG, QueryParam
from lightrag.llm.gemini import gemini_model_complete, gemini_embed
from lightrag.utils import wrap_embedding_func_with_attrs
nest_asyncio.apply()
WORKING_DIR = "./rag_storage"
BOOK_FILE = "./book.txt"
# Validate API key
GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY")
if not GEMINI_API_KEY:
raise ValueError(
"GEMINI_API_KEY environment variable is not set. "
"Please set it with: export GEMINI_API_KEY='your-api-key'"
)
if not os.path.exists(WORKING_DIR):
os.mkdir(WORKING_DIR)
# --------------------------------------------------
# LLM function
# --------------------------------------------------
async def llm_model_func(prompt, system_prompt=None, history_messages=[], **kwargs):
return await gemini_model_complete(
prompt,
system_prompt=system_prompt,
history_messages=history_messages,
api_key=GEMINI_API_KEY,
model_name="gemini-2.0-flash",
**kwargs,
)
# --------------------------------------------------
# Embedding function
# --------------------------------------------------
@wrap_embedding_func_with_attrs(
embedding_dim=768,
send_dimensions=True,
max_token_size=2048,
model_name="models/text-embedding-004",
)
async def embedding_func(texts: list[str]) -> np.ndarray:
return await gemini_embed.func(
texts, api_key=GEMINI_API_KEY, model="models/text-embedding-004"
)
# --------------------------------------------------
# Initialize RAG
# --------------------------------------------------
async def initialize_rag():
rag = LightRAG(
working_dir=WORKING_DIR,
llm_model_func=llm_model_func,
embedding_func=embedding_func,
llm_model_name="gemini-2.0-flash",
)
# 🔑 REQUIRED
await rag.initialize_storages()
return rag
# --------------------------------------------------
# Main
# --------------------------------------------------
def main():
# Validate book file exists
if not os.path.exists(BOOK_FILE):
raise FileNotFoundError(
f"'{BOOK_FILE}' not found. "
"Please provide a text file to index in the current directory."
)
rag = asyncio.run(initialize_rag())
# Insert text
with open(BOOK_FILE, "r", encoding="utf-8") as f:
rag.insert(f.read())
query = "What are the top themes?"
print("\nNaive Search:")
print(rag.query(query, param=QueryParam(mode="naive")))
print("\nLocal Search:")
print(rag.query(query, param=QueryParam(mode="local")))
print("\nGlobal Search:")
print(rag.query(query, param=QueryParam(mode="global")))
print("\nHybrid Search:")
print(rag.query(query, param=QueryParam(mode="hybrid")))
if __name__ == "__main__":
main()
+178
View File
@@ -0,0 +1,178 @@
"""
LightRAG Demo with PostgreSQL + Google Gemini
This example demonstrates how to use LightRAG with:
- Google Gemini (LLM + Embeddings)
- PostgreSQL-backed storages for:
- Vector storage
- Graph storage
- KV storage
- Document status storage
Prerequisites:
1. PostgreSQL database running and accessible
2. Required tables will be auto-created by LightRAG
3. Set environment variables (example .env):
POSTGRES_HOST=localhost
POSTGRES_PORT=5432
POSTGRES_USER=admin
POSTGRES_PASSWORD=admin
POSTGRES_DATABASE=ai
LIGHTRAG_KV_STORAGE=PGKVStorage
LIGHTRAG_DOC_STATUS_STORAGE=PGDocStatusStorage
LIGHTRAG_GRAPH_STORAGE=PGGraphStorage
LIGHTRAG_VECTOR_STORAGE=PGVectorStorage
GEMINI_API_KEY=your-api-key
4. Prepare a text file to index (default: Data/book-small.txt)
Usage:
python examples/lightrag_postgres_demo.py
"""
import os
import asyncio
import numpy as np
from lightrag import LightRAG, QueryParam
from lightrag.llm.gemini import gemini_model_complete, gemini_embed
from lightrag.utils import setup_logger, wrap_embedding_func_with_attrs
# --------------------------------------------------
# Logger
# --------------------------------------------------
setup_logger("lightrag", level="INFO")
# --------------------------------------------------
# Config
# --------------------------------------------------
WORKING_DIR = "./rag_storage"
BOOK_FILE = "Data/book.txt"
if not os.path.exists(WORKING_DIR):
os.mkdir(WORKING_DIR)
GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
if not GEMINI_API_KEY:
raise ValueError("GEMINI_API_KEY environment variable is not set")
# --------------------------------------------------
# LLM function (Gemini)
# --------------------------------------------------
async def llm_model_func(
prompt,
system_prompt=None,
history_messages=[],
keyword_extraction=False,
**kwargs,
) -> str:
return await gemini_model_complete(
prompt,
system_prompt=system_prompt,
history_messages=history_messages,
api_key=GEMINI_API_KEY,
model_name="gemini-2.0-flash",
**kwargs,
)
# --------------------------------------------------
# Embedding function (Gemini)
# --------------------------------------------------
@wrap_embedding_func_with_attrs(
embedding_dim=768,
max_token_size=2048,
model_name="models/text-embedding-004",
)
async def embedding_func(texts: list[str]) -> np.ndarray:
return await gemini_embed.func(
texts,
api_key=GEMINI_API_KEY,
model="models/text-embedding-004",
)
# --------------------------------------------------
# Initialize RAG with PostgreSQL storages
# --------------------------------------------------
async def initialize_rag() -> LightRAG:
rag = LightRAG(
working_dir=WORKING_DIR,
llm_model_name="gemini-2.0-flash",
llm_model_func=llm_model_func,
embedding_func=embedding_func,
# Performance tuning
embedding_func_max_async=4,
embedding_batch_num=8,
llm_model_max_async=2,
# Chunking
chunk_token_size=1200,
chunk_overlap_token_size=100,
# PostgreSQL-backed storages
graph_storage="PGGraphStorage",
vector_storage="PGVectorStorage",
doc_status_storage="PGDocStatusStorage",
kv_storage="PGKVStorage",
)
# REQUIRED: initialize all storage backends
await rag.initialize_storages()
return rag
# --------------------------------------------------
# Main
# --------------------------------------------------
async def main():
rag = None
try:
print("Initializing LightRAG with PostgreSQL + Gemini...")
rag = await initialize_rag()
if not os.path.exists(BOOK_FILE):
raise FileNotFoundError(
f"'{BOOK_FILE}' not found. Please provide a text file to index."
)
print(f"\nReading document: {BOOK_FILE}")
with open(BOOK_FILE, "r", encoding="utf-8") as f:
content = f.read()
print(f"Loaded document ({len(content)} characters)")
print("\nInserting document into LightRAG (this may take some time)...")
await rag.ainsert(content)
print("Document indexed successfully!")
print("\n" + "=" * 60)
print("Running sample queries")
print("=" * 60)
query = "What are the top themes in this document?"
for mode in ["naive", "local", "global", "hybrid"]:
print(f"\n[{mode.upper()} MODE]")
result = await rag.aquery(query, param=QueryParam(mode=mode))
print(result[:400] + "..." if len(result) > 400 else result)
print("\nRAG system is ready for use!")
except Exception as e:
print("An error occurred:", e)
import traceback
traceback.print_exc()
finally:
if rag is not None:
await rag.finalize_storages()
if __name__ == "__main__":
asyncio.run(main())
+131
View File
@@ -0,0 +1,131 @@
"""
LightRAG Data Isolation Demo: Workspace Management
This example demonstrates how to maintain multiple isolated knowledge bases
within a single application using LightRAG's 'workspace' feature.
Key Concepts:
- Workspace Isolation: Each RAG instance is assigned a unique workspace name,
which ensures that Knowledge Graphs, Vector Databases, and Chunks are
stored in separate, non-conflicting directories.
- Independent Configuration: Different workspaces can utilize different
entity type guidance and document sets simultaneously.
Prerequisites:
1. Set the following environment variables:
- GEMINI_API_KEY: Your Google Gemini API key.
2. Ensure your data directory contains:
- Data/book-small.txt
- Data/HR_policies.txt
Usage:
python lightrag_workspace_demo.py
"""
import os
import asyncio
import numpy as np
from lightrag import LightRAG, QueryParam
from lightrag.llm.gemini import gemini_model_complete, gemini_embed
from lightrag.utils import wrap_embedding_func_with_attrs
async def llm_model_func(
prompt, system_prompt=None, history_messages=[], keyword_extraction=False, **kwargs
) -> str:
"""Wrapper for Gemini LLM completion."""
return await gemini_model_complete(
prompt,
system_prompt=system_prompt,
history_messages=history_messages,
api_key=os.getenv("GEMINI_API_KEY"),
model_name="gemini-2.0-flash-exp",
**kwargs,
)
@wrap_embedding_func_with_attrs(
embedding_dim=768, max_token_size=2048, model_name="models/text-embedding-004"
)
async def embedding_func(texts: list[str]) -> np.ndarray:
"""Wrapper for Gemini embedding model."""
return await gemini_embed.func(
texts, api_key=os.getenv("GEMINI_API_KEY"), model="models/text-embedding-004"
)
async def initialize_rag(
workspace: str = "default_workspace",
) -> LightRAG:
"""
Initializes a LightRAG instance with data isolation.
Entity type guidance can be customized by passing
addon_params={'entity_types_guidance': '...'} to LightRAG.
"""
rag = LightRAG(
workspace=workspace,
llm_model_name="gemini-2.0-flash",
llm_model_func=llm_model_func,
embedding_func=embedding_func,
embedding_func_max_async=4,
embedding_batch_num=8,
llm_model_max_async=2,
)
await rag.initialize_storages()
return rag
async def main():
rag_1 = None
rag_2 = None
try:
# 1. Initialize Isolated Workspaces
# Instance 1: Dedicated to literary analysis
# Instance 2: Dedicated to corporate HR documentation
print("Initializing isolated LightRAG workspaces...")
rag_1 = await initialize_rag("rag_workspace_book")
rag_2 = await initialize_rag("rag_workspace_hr")
# 2. Populate Workspace 1 (Literature)
book_path = "Data/book-small.txt"
if os.path.exists(book_path):
with open(book_path, "r", encoding="utf-8") as f:
print(f"Indexing {book_path} into Literature Workspace...")
await rag_1.ainsert(f.read())
# 3. Populate Workspace 2 (Corporate)
hr_path = "Data/HR_policies.txt"
if os.path.exists(hr_path):
with open(hr_path, "r", encoding="utf-8") as f:
print(f"Indexing {hr_path} into HR Workspace...")
await rag_2.ainsert(f.read())
# 4. Context-Specific Querying
print("\n--- Querying Literature Workspace ---")
res1 = await rag_1.aquery(
"What is the main theme?",
param=QueryParam(mode="hybrid", stream=False),
)
print(f"Book Analysis: {res1[:200]}...")
print("\n--- Querying HR Workspace ---")
res2 = await rag_2.aquery(
"What is the leave policy?", param=QueryParam(mode="hybrid")
)
print(f"HR Response: {res2[:200]}...")
except Exception as e:
print(f"An error occurred: {e}")
finally:
# Finalize storage to safely close DB connections and write buffers
if rag_1:
await rag_1.finalize_storages()
if rag_2:
await rag_2.finalize_storages()
if __name__ == "__main__":
asyncio.run(main())
+219
View File
@@ -0,0 +1,219 @@
import asyncio
import os
import inspect
import logging
import logging.config
from functools import partial
from lightrag import LightRAG, QueryParam
from lightrag.llm.ollama import ollama_model_complete, ollama_embed
from lightrag.utils import EmbeddingFunc, logger, set_verbose_debug
from dotenv import load_dotenv
load_dotenv(dotenv_path=".env", override=False)
WORKING_DIR = "./dickens"
def configure_logging():
"""Configure logging for the application"""
# Reset any existing handlers to ensure clean configuration
for logger_name in ["uvicorn", "uvicorn.access", "uvicorn.error", "lightrag"]:
logger_instance = logging.getLogger(logger_name)
logger_instance.handlers = []
logger_instance.filters = []
# Get log directory path from environment variable or use current directory
log_dir = os.getenv("LOG_DIR", os.getcwd())
log_file_path = os.path.abspath(os.path.join(log_dir, "lightrag_ollama_demo.log"))
print(f"\nLightRAG compatible demo log file: {log_file_path}\n")
os.makedirs(os.path.dirname(log_file_path), exist_ok=True)
# Get log file max size and backup count from environment variables
log_max_bytes = int(os.getenv("LOG_MAX_BYTES", 10485760)) # Default 10MB
log_backup_count = int(os.getenv("LOG_BACKUP_COUNT", 5)) # Default 5 backups
logging.config.dictConfig(
{
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"default": {
"format": "%(levelname)s: %(message)s",
},
"detailed": {
"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
},
},
"handlers": {
"console": {
"formatter": "default",
"class": "logging.StreamHandler",
"stream": "ext://sys.stderr",
},
"file": {
"formatter": "detailed",
"class": "logging.handlers.RotatingFileHandler",
"filename": log_file_path,
"maxBytes": log_max_bytes,
"backupCount": log_backup_count,
"encoding": "utf-8",
},
},
"loggers": {
"lightrag": {
"handlers": ["console", "file"],
"level": "INFO",
"propagate": False,
},
},
}
)
# Set the logger level to INFO
logger.setLevel(logging.INFO)
# Enable verbose debug if needed
set_verbose_debug(os.getenv("VERBOSE_DEBUG", "false").lower() == "true")
if not os.path.exists(WORKING_DIR):
os.mkdir(WORKING_DIR)
async def initialize_rag():
rag = LightRAG(
working_dir=WORKING_DIR,
llm_model_func=ollama_model_complete,
llm_model_name=os.getenv("LLM_MODEL", "qwen2.5-coder:7b"),
summary_max_tokens=8192,
llm_model_kwargs={
"host": os.getenv("LLM_BINDING_HOST", "http://localhost:11434"),
"options": {"num_ctx": 8192},
"timeout": int(os.getenv("TIMEOUT", "300")),
},
# Note: ollama_embed is decorated with @wrap_embedding_func_with_attrs,
# which wraps it in an EmbeddingFunc. Using .func accesses the original
# unwrapped function to avoid double wrapping when we create our own
# EmbeddingFunc with custom configuration (embedding_dim, max_token_size).
embedding_func=EmbeddingFunc(
embedding_dim=int(os.getenv("EMBEDDING_DIM", "1024")),
max_token_size=int(os.getenv("MAX_EMBED_TOKENS", "8192")),
func=partial(
ollama_embed.func, # Access the unwrapped function to avoid double EmbeddingFunc wrapping
embed_model=os.getenv("EMBEDDING_MODEL", "bge-m3:latest"),
host=os.getenv("EMBEDDING_BINDING_HOST", "http://localhost:11434"),
),
),
)
await rag.initialize_storages() # Auto-initializes pipeline_status
return rag
async def print_stream(stream):
async for chunk in stream:
print(chunk, end="", flush=True)
async def main():
try:
# Clear old data files
files_to_delete = [
"graph_chunk_entity_relation.graphml",
"kv_store_doc_status.json",
"kv_store_full_docs.json",
"kv_store_text_chunks.json",
"vdb_chunks.json",
"vdb_entities.json",
"vdb_relationships.json",
]
for file in files_to_delete:
file_path = os.path.join(WORKING_DIR, file)
if os.path.exists(file_path):
os.remove(file_path)
print(f"Deleting old file:: {file_path}")
# Initialize RAG instance
rag = await initialize_rag()
# Test embedding function
test_text = ["This is a test string for embedding."]
embedding = await rag.embedding_func(test_text)
embedding_dim = embedding.shape[1]
print("\n=======================")
print("Test embedding function")
print("========================")
print(f"Test dict: {test_text}")
print(f"Detected embedding dimension: {embedding_dim}\n\n")
with open("./book.txt", "r", encoding="utf-8") as f:
await rag.ainsert(f.read())
# Perform naive search
print("\n=====================")
print("Query mode: naive")
print("=====================")
resp = await rag.aquery(
"What are the top themes in this story?",
param=QueryParam(mode="naive", stream=True),
)
if inspect.isasyncgen(resp):
await print_stream(resp)
else:
print(resp)
# Perform local search
print("\n=====================")
print("Query mode: local")
print("=====================")
resp = await rag.aquery(
"What are the top themes in this story?",
param=QueryParam(mode="local", stream=True),
)
if inspect.isasyncgen(resp):
await print_stream(resp)
else:
print(resp)
# Perform global search
print("\n=====================")
print("Query mode: global")
print("=====================")
resp = await rag.aquery(
"What are the top themes in this story?",
param=QueryParam(mode="global", stream=True),
)
if inspect.isasyncgen(resp):
await print_stream(resp)
else:
print(resp)
# Perform hybrid search
print("\n=====================")
print("Query mode: hybrid")
print("=====================")
resp = await rag.aquery(
"What are the top themes in this story?",
param=QueryParam(mode="hybrid", stream=True),
)
if inspect.isasyncgen(resp):
await print_stream(resp)
else:
print(resp)
except Exception as e:
print(f"An error occurred: {e}")
finally:
if rag:
await rag.llm_response_cache.index_done_callback()
await rag.finalize_storages()
if __name__ == "__main__":
# Configure logging before running the main function
configure_logging()
asyncio.run(main())
print("\nDone!")
+229
View File
@@ -0,0 +1,229 @@
import os
import asyncio
import inspect
import logging
import logging.config
from functools import partial
from lightrag import LightRAG, QueryParam
from lightrag.llm.openai import openai_complete_if_cache
from lightrag.llm.ollama import ollama_embed
from lightrag.utils import EmbeddingFunc, logger, set_verbose_debug
from dotenv import load_dotenv
load_dotenv(dotenv_path=".env", override=False)
WORKING_DIR = "./dickens"
def configure_logging():
"""Configure logging for the application"""
# Reset any existing handlers to ensure clean configuration
for logger_name in ["uvicorn", "uvicorn.access", "uvicorn.error", "lightrag"]:
logger_instance = logging.getLogger(logger_name)
logger_instance.handlers = []
logger_instance.filters = []
# Get log directory path from environment variable or use current directory
log_dir = os.getenv("LOG_DIR", os.getcwd())
log_file_path = os.path.abspath(
os.path.join(log_dir, "lightrag_compatible_demo.log")
)
print(f"\nLightRAG compatible demo log file: {log_file_path}\n")
os.makedirs(os.path.dirname(log_dir), exist_ok=True)
# Get log file max size and backup count from environment variables
log_max_bytes = int(os.getenv("LOG_MAX_BYTES", 10485760)) # Default 10MB
log_backup_count = int(os.getenv("LOG_BACKUP_COUNT", 5)) # Default 5 backups
logging.config.dictConfig(
{
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"default": {
"format": "%(levelname)s: %(message)s",
},
"detailed": {
"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
},
},
"handlers": {
"console": {
"formatter": "default",
"class": "logging.StreamHandler",
"stream": "ext://sys.stderr",
},
"file": {
"formatter": "detailed",
"class": "logging.handlers.RotatingFileHandler",
"filename": log_file_path,
"maxBytes": log_max_bytes,
"backupCount": log_backup_count,
"encoding": "utf-8",
},
},
"loggers": {
"lightrag": {
"handlers": ["console", "file"],
"level": "INFO",
"propagate": False,
},
},
}
)
# Set the logger level to INFO
logger.setLevel(logging.INFO)
# Enable verbose debug if needed
set_verbose_debug(os.getenv("VERBOSE_DEBUG", "false").lower() == "true")
if not os.path.exists(WORKING_DIR):
os.mkdir(WORKING_DIR)
async def llm_model_func(
prompt, system_prompt=None, history_messages=[], keyword_extraction=False, **kwargs
) -> str:
return await openai_complete_if_cache(
os.getenv("LLM_MODEL", "deepseek-chat"),
prompt,
system_prompt=system_prompt,
history_messages=history_messages,
api_key=os.getenv("LLM_BINDING_API_KEY") or os.getenv("OPENAI_API_KEY"),
base_url=os.getenv("LLM_BINDING_HOST", "https://api.deepseek.com"),
**kwargs,
)
async def print_stream(stream):
async for chunk in stream:
if chunk:
print(chunk, end="", flush=True)
async def initialize_rag():
rag = LightRAG(
working_dir=WORKING_DIR,
llm_model_func=llm_model_func,
# Note: ollama_embed is decorated with @wrap_embedding_func_with_attrs,
# which wraps it in an EmbeddingFunc. Using .func accesses the original
# unwrapped function to avoid double wrapping when we create our own
# EmbeddingFunc with custom configuration (embedding_dim, max_token_size).
embedding_func=EmbeddingFunc(
embedding_dim=int(os.getenv("EMBEDDING_DIM", "1024")),
max_token_size=int(os.getenv("MAX_EMBED_TOKENS", "8192")),
func=partial(
ollama_embed.func, # Access the unwrapped function to avoid double EmbeddingFunc wrapping
embed_model=os.getenv("EMBEDDING_MODEL", "bge-m3:latest"),
host=os.getenv("EMBEDDING_BINDING_HOST", "http://localhost:11434"),
),
),
)
await rag.initialize_storages() # Auto-initializes pipeline_status
return rag
async def main():
try:
# Clear old data files
files_to_delete = [
"graph_chunk_entity_relation.graphml",
"kv_store_doc_status.json",
"kv_store_full_docs.json",
"kv_store_text_chunks.json",
"vdb_chunks.json",
"vdb_entities.json",
"vdb_relationships.json",
]
for file in files_to_delete:
file_path = os.path.join(WORKING_DIR, file)
if os.path.exists(file_path):
os.remove(file_path)
print(f"Deleting old file:: {file_path}")
# Initialize RAG instance
rag = await initialize_rag()
# Test embedding function
test_text = ["This is a test string for embedding."]
embedding = await rag.embedding_func(test_text)
embedding_dim = embedding.shape[1]
print("\n=======================")
print("Test embedding function")
print("========================")
print(f"Test dict: {test_text}")
print(f"Detected embedding dimension: {embedding_dim}\n\n")
with open("./book.txt", "r", encoding="utf-8") as f:
await rag.ainsert(f.read())
# Perform naive search
print("\n=====================")
print("Query mode: naive")
print("=====================")
resp = await rag.aquery(
"What are the top themes in this story?",
param=QueryParam(mode="naive", stream=True),
)
if inspect.isasyncgen(resp):
await print_stream(resp)
else:
print(resp)
# Perform local search
print("\n=====================")
print("Query mode: local")
print("=====================")
resp = await rag.aquery(
"What are the top themes in this story?",
param=QueryParam(mode="local", stream=True),
)
if inspect.isasyncgen(resp):
await print_stream(resp)
else:
print(resp)
# Perform global search
print("\n=====================")
print("Query mode: global")
print("=====================")
resp = await rag.aquery(
"What are the top themes in this story?",
param=QueryParam(mode="global", stream=True),
)
if inspect.isasyncgen(resp):
await print_stream(resp)
else:
print(resp)
# Perform hybrid search
print("\n=====================")
print("Query mode: hybrid")
print("=====================")
resp = await rag.aquery(
"What are the top themes in this story?",
param=QueryParam(mode="hybrid", stream=True),
)
if inspect.isasyncgen(resp):
await print_stream(resp)
else:
print(resp)
except Exception as e:
print(f"An error occurred: {e}")
finally:
if rag:
await rag.finalize_storages()
if __name__ == "__main__":
# Configure logging before running the main function
configure_logging()
asyncio.run(main())
print("\nDone!")
+187
View File
@@ -0,0 +1,187 @@
import os
import asyncio
import logging
import logging.config
from lightrag import LightRAG, QueryParam
from lightrag.llm.openai import gpt_4o_mini_complete, openai_embed
from lightrag.utils import logger, set_verbose_debug
WORKING_DIR = "./dickens"
def configure_logging():
"""Configure logging for the application"""
# Reset any existing handlers to ensure clean configuration
for logger_name in ["uvicorn", "uvicorn.access", "uvicorn.error", "lightrag"]:
logger_instance = logging.getLogger(logger_name)
logger_instance.handlers = []
logger_instance.filters = []
# Get log directory path from environment variable or use current directory
log_dir = os.getenv("LOG_DIR", os.getcwd())
log_file_path = os.path.abspath(os.path.join(log_dir, "lightrag_demo.log"))
print(f"\nLightRAG demo log file: {log_file_path}\n")
os.makedirs(os.path.dirname(log_dir), exist_ok=True)
# Get log file max size and backup count from environment variables
log_max_bytes = int(os.getenv("LOG_MAX_BYTES", 10485760)) # Default 10MB
log_backup_count = int(os.getenv("LOG_BACKUP_COUNT", 5)) # Default 5 backups
logging.config.dictConfig(
{
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"default": {
"format": "%(levelname)s: %(message)s",
},
"detailed": {
"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
},
},
"handlers": {
"console": {
"formatter": "default",
"class": "logging.StreamHandler",
"stream": "ext://sys.stderr",
},
"file": {
"formatter": "detailed",
"class": "logging.handlers.RotatingFileHandler",
"filename": log_file_path,
"maxBytes": log_max_bytes,
"backupCount": log_backup_count,
"encoding": "utf-8",
},
},
"loggers": {
"lightrag": {
"handlers": ["console", "file"],
"level": "INFO",
"propagate": False,
},
},
}
)
# Set the logger level to INFO
logger.setLevel(logging.INFO)
# Enable verbose debug if needed
set_verbose_debug(os.getenv("VERBOSE_DEBUG", "false").lower() == "true")
if not os.path.exists(WORKING_DIR):
os.mkdir(WORKING_DIR)
async def initialize_rag():
rag = LightRAG(
working_dir=WORKING_DIR,
embedding_func=openai_embed,
llm_model_func=gpt_4o_mini_complete,
)
await rag.initialize_storages() # Auto-initializes pipeline_status
return rag
async def main():
# Check if OPENAI_API_KEY environment variable exists
if not os.getenv("OPENAI_API_KEY"):
print(
"Error: OPENAI_API_KEY environment variable is not set. Please set this variable before running the program."
)
print("You can set the environment variable by running:")
print(" export OPENAI_API_KEY='your-openai-api-key'")
return # Exit the async function
try:
# Clear old data files
files_to_delete = [
"graph_chunk_entity_relation.graphml",
"kv_store_doc_status.json",
"kv_store_full_docs.json",
"kv_store_text_chunks.json",
"vdb_chunks.json",
"vdb_entities.json",
"vdb_relationships.json",
]
for file in files_to_delete:
file_path = os.path.join(WORKING_DIR, file)
if os.path.exists(file_path):
os.remove(file_path)
print(f"Deleting old file:: {file_path}")
# Initialize RAG instance
rag = await initialize_rag()
# Test embedding function
test_text = ["This is a test string for embedding."]
embedding = await rag.embedding_func(test_text)
embedding_dim = embedding.shape[1]
print("\n=======================")
print("Test embedding function")
print("========================")
print(f"Test dict: {test_text}")
print(f"Detected embedding dimension: {embedding_dim}\n\n")
with open("./book.txt", "r", encoding="utf-8") as f:
await rag.ainsert(f.read())
# Perform naive search
print("\n=====================")
print("Query mode: naive")
print("=====================")
print(
await rag.aquery(
"What are the top themes in this story?", param=QueryParam(mode="naive")
)
)
# Perform local search
print("\n=====================")
print("Query mode: local")
print("=====================")
print(
await rag.aquery(
"What are the top themes in this story?", param=QueryParam(mode="local")
)
)
# Perform global search
print("\n=====================")
print("Query mode: global")
print("=====================")
print(
await rag.aquery(
"What are the top themes in this story?",
param=QueryParam(mode="global"),
)
)
# Perform hybrid search
print("\n=====================")
print("Query mode: hybrid")
print("=====================")
print(
await rag.aquery(
"What are the top themes in this story?",
param=QueryParam(mode="hybrid"),
)
)
except Exception as e:
print(f"An error occurred: {e}")
finally:
if rag:
await rag.finalize_storages()
if __name__ == "__main__":
# Configure logging before running the main function
configure_logging()
asyncio.run(main())
print("\nDone!")
@@ -0,0 +1,108 @@
import os
import asyncio
from lightrag import LightRAG, QueryParam
from lightrag.llm.openai import gpt_4o_mini_complete, openai_embed
from lightrag.utils import EmbeddingFunc
import numpy as np
#########
# Uncomment the below two lines if running in a jupyter notebook to handle the async nature of rag.insert()
# import nest_asyncio
# nest_asyncio.apply()
#########
WORKING_DIR = "./mongodb_test_dir"
if not os.path.exists(WORKING_DIR):
os.mkdir(WORKING_DIR)
os.environ["OPENAI_API_KEY"] = "sk-"
os.environ["MONGO_URI"] = "mongodb://0.0.0.0:27017/?directConnection=true"
os.environ["MONGO_DATABASE"] = "LightRAG"
os.environ["MONGO_KG_COLLECTION"] = "MDB_KG"
# Embedding Configuration and Functions
EMBEDDING_MODEL = os.environ.get("EMBEDDING_MODEL", "text-embedding-3-large")
EMBEDDING_MAX_TOKEN_SIZE = int(os.environ.get("EMBEDDING_MAX_TOKEN_SIZE", 8192))
async def embedding_func(texts: list[str]) -> np.ndarray:
# Note: openai_embed is decorated with @wrap_embedding_func_with_attrs,
# which wraps it in an EmbeddingFunc. Using .func accesses the original
# unwrapped function to avoid double wrapping when we create our own
# EmbeddingFunc with custom configuration in create_embedding_function_instance().
return await openai_embed.func(
texts,
model=EMBEDDING_MODEL,
)
async def get_embedding_dimension():
test_text = ["This is a test sentence."]
embedding = await embedding_func(test_text)
return embedding.shape[1]
async def create_embedding_function_instance():
# Get embedding dimension
embedding_dimension = await get_embedding_dimension()
# Create embedding function instance
return EmbeddingFunc(
embedding_dim=embedding_dimension,
max_token_size=EMBEDDING_MAX_TOKEN_SIZE,
func=embedding_func,
)
async def initialize_rag():
embedding_func_instance = await create_embedding_function_instance()
rag = LightRAG(
working_dir=WORKING_DIR,
llm_model_func=gpt_4o_mini_complete,
embedding_func=embedding_func_instance,
graph_storage="MongoGraphStorage",
log_level="DEBUG",
)
await rag.initialize_storages() # Auto-initializes pipeline_status
return rag
def main():
# Initialize RAG instance
rag = asyncio.run(initialize_rag())
with open("./book.txt", "r", encoding="utf-8") as f:
rag.insert(f.read())
# Perform naive search
print(
rag.query(
"What are the top themes in this story?", param=QueryParam(mode="naive")
)
)
# Perform local search
print(
rag.query(
"What are the top themes in this story?", param=QueryParam(mode="local")
)
)
# Perform global search
print(
rag.query(
"What are the top themes in this story?", param=QueryParam(mode="global")
)
)
# Perform hybrid search
print(
rag.query(
"What are the top themes in this story?", param=QueryParam(mode="hybrid")
)
)
if __name__ == "__main__":
main()
@@ -0,0 +1,178 @@
"""
LightRAG Demo with OpenSearch + OpenAI
This example demonstrates how to use LightRAG with:
- OpenAI (LLM + Embeddings)
- OpenSearch-backed storages for:
- KV storage
- Vector storage (k-NN)
- Graph storage (dual-index nodes + edges)
- Document status storage
Prerequisites:
1. OpenSearch cluster running and accessible (3.x or higher with k-NN plugin)
2. Required indices will be auto-created by LightRAG
3. Set environment variables (example .env):
OPENSEARCH_HOSTS=localhost:9200
OPENSEARCH_USER=admin
OPENSEARCH_PASSWORD=your-password
OPENSEARCH_USE_SSL=false
OPENSEARCH_VERIFY_CERTS=false
OPENAI_API_KEY=your-api-key
4. Prepare a text file to index (default: ./book.txt)
Usage:
python examples/lightrag_openai_opensearch_graph_demo.py
"""
import os
import asyncio
import numpy as np
from lightrag import LightRAG, QueryParam
from lightrag.llm.openai import gpt_4o_mini_complete, openai_embed
from lightrag.utils import setup_logger, EmbeddingFunc
# --------------------------------------------------
# Logger
# --------------------------------------------------
setup_logger("lightrag", level="INFO")
# --------------------------------------------------
# Config
# --------------------------------------------------
WORKING_DIR = "./opensearch_rag_storage"
BOOK_FILE = "./book.txt"
if not os.path.exists(WORKING_DIR):
os.mkdir(WORKING_DIR)
# Replace with your API key, or set via environment variable
if not os.getenv("OPENAI_API_KEY"):
os.environ["OPENAI_API_KEY"] = "sk-"
EMBEDDING_MODEL = os.environ.get("EMBEDDING_MODEL", "text-embedding-3-large")
EMBEDDING_MAX_TOKEN_SIZE = int(os.environ.get("EMBEDDING_MAX_TOKEN_SIZE", 8192))
# --------------------------------------------------
# Embedding function (OpenAI)
# --------------------------------------------------
async def embedding_func(texts: list[str]) -> np.ndarray:
return await openai_embed.func(
texts,
model=EMBEDDING_MODEL,
)
async def get_embedding_dimension():
test_text = ["This is a test sentence."]
embedding = await embedding_func(test_text)
return embedding.shape[1]
async def create_embedding_function_instance():
embedding_dimension = await get_embedding_dimension()
return EmbeddingFunc(
embedding_dim=embedding_dimension,
max_token_size=EMBEDDING_MAX_TOKEN_SIZE,
func=embedding_func,
)
# --------------------------------------------------
# Initialize RAG with OpenSearch storages
# --------------------------------------------------
async def initialize_rag() -> LightRAG:
embedding_func_instance = await create_embedding_function_instance()
rag = LightRAG(
working_dir=WORKING_DIR,
llm_model_func=gpt_4o_mini_complete,
embedding_func=embedding_func_instance,
# OpenSearch-backed storages
kv_storage="OpenSearchKVStorage",
doc_status_storage="OpenSearchDocStatusStorage",
graph_storage="OpenSearchGraphStorage",
vector_storage="OpenSearchVectorDBStorage",
)
# REQUIRED: initialize all storage backends
await rag.initialize_storages()
# Clean previous data so the example is re-runnable
# (LLM response cache is preserved for faster reruns)
for storage in [
rag.full_docs,
rag.text_chunks,
rag.full_entities,
rag.full_relations,
rag.entity_chunks,
rag.relation_chunks,
rag.entities_vdb,
rag.relationships_vdb,
rag.chunks_vdb,
rag.chunk_entity_relation_graph,
rag.doc_status,
]:
await storage.drop()
print("Cleared previous data.")
return rag
# --------------------------------------------------
# Main
# --------------------------------------------------
async def main():
rag = None
try:
print("Initializing LightRAG with OpenSearch + OpenAI...")
rag = await initialize_rag()
if not os.path.exists(BOOK_FILE):
raise FileNotFoundError(
f"'{BOOK_FILE}' not found. Please provide a text file to index."
)
print(f"\nReading document: {BOOK_FILE}")
with open(BOOK_FILE, "r", encoding="utf-8") as f:
content = f.read()
print(f"Loaded document ({len(content)} characters)")
print("\nInserting document into LightRAG (this may take some time)...")
await rag.ainsert(content)
print("Document indexed successfully!")
print("\n" + "=" * 60)
print("Running sample queries")
print("=" * 60)
query = "What are the top themes in this document?"
for mode in ["naive", "local", "global", "hybrid"]:
print(f"\n[{mode.upper()} MODE]")
result = await rag.aquery(query, param=QueryParam(mode=mode))
print(result)
print("\nRAG system is ready for use!")
except Exception as e:
print("An error occurred:", e)
import traceback
traceback.print_exc()
finally:
if rag is not None:
await rag.finalize_storages()
if __name__ == "__main__":
asyncio.run(main())
+180
View File
@@ -0,0 +1,180 @@
"""
LightRAG Demo with vLLM (LLM, Embeddings, and Reranker)
This example demonstrates how to use LightRAG with:
- vLLM-served LLM (OpenAI-compatible API)
- vLLM-served embedding model
- Jina-compatible reranker (also vLLM-served)
Prerequisites:
1. Create a .env file or export environment variables:
- LLM_MODEL
- LLM_BINDING_HOST
- LLM_BINDING_API_KEY
- EMBEDDING_MODEL
- EMBEDDING_BINDING_HOST
- EMBEDDING_BINDING_API_KEY
- EMBEDDING_DIM
- EMBEDDING_TOKEN_LIMIT
- RERANK_MODEL
- RERANK_BINDING_HOST
- RERANK_BINDING_API_KEY
2. Prepare a text file to index (default: Data/book-small.txt)
3. Configure storage backends via environment variables or modify
the storage parameters in initialize_rag() below.
Usage:
python examples/lightrag_vllm_demo.py
"""
import os
import asyncio
from functools import partial
from dotenv import load_dotenv
from lightrag import LightRAG, QueryParam
from lightrag.llm.openai import openai_complete_if_cache, openai_embed
from lightrag.utils import EmbeddingFunc
from lightrag.rerank import jina_rerank
load_dotenv()
# --------------------------------------------------
# Constants
# --------------------------------------------------
WORKING_DIR = "./LightRAG_Data"
BOOK_FILE = "Data/book-small.txt"
# --------------------------------------------------
# LLM function (vLLM, OpenAI-compatible)
# --------------------------------------------------
async def llm_model_func(
prompt, system_prompt=None, history_messages=[], **kwargs
) -> str:
return await openai_complete_if_cache(
model=os.getenv("LLM_MODEL", "Qwen/Qwen3-14B-AWQ"),
prompt=prompt,
system_prompt=system_prompt,
history_messages=history_messages,
base_url=os.getenv("LLM_BINDING_HOST", "http://0.0.0.0:4646/v1"),
api_key=os.getenv("LLM_BINDING_API_KEY", "not_needed"),
timeout=600,
**kwargs,
)
# --------------------------------------------------
# Embedding function (vLLM)
# --------------------------------------------------
vLLM_emb_func = EmbeddingFunc(
model_name=os.getenv("EMBEDDING_MODEL", "Qwen/Qwen3-Embedding-0.6B"),
send_dimensions=False,
embedding_dim=int(os.getenv("EMBEDDING_DIM", 1024)),
max_token_size=int(os.getenv("EMBEDDING_TOKEN_LIMIT", 4096)),
func=partial(
openai_embed.func,
model=os.getenv("EMBEDDING_MODEL", "Qwen/Qwen3-Embedding-0.6B"),
base_url=os.getenv(
"EMBEDDING_BINDING_HOST",
"http://0.0.0.0:1234/v1",
),
api_key=os.getenv("EMBEDDING_BINDING_API_KEY", "not_needed"),
),
)
# --------------------------------------------------
# Reranker (Jina-compatible, vLLM-served)
# --------------------------------------------------
jina_rerank_model_func = partial(
jina_rerank,
model=os.getenv("RERANK_MODEL", "Qwen/Qwen3-Reranker-0.6B"),
api_key=os.getenv("RERANK_BINDING_API_KEY"),
base_url=os.getenv(
"RERANK_BINDING_HOST",
"http://0.0.0.0:3535/v1/rerank",
),
)
# --------------------------------------------------
# Initialize RAG
# --------------------------------------------------
async def initialize_rag():
rag = LightRAG(
working_dir=WORKING_DIR,
llm_model_func=llm_model_func,
embedding_func=vLLM_emb_func,
rerank_model_func=jina_rerank_model_func,
# Storage backends (configurable via environment or modify here)
kv_storage=os.getenv("KV_STORAGE", "PGKVStorage"),
doc_status_storage=os.getenv("DOC_STATUS_STORAGE", "PGDocStatusStorage"),
vector_storage=os.getenv("VECTOR_STORAGE", "PGVectorStorage"),
graph_storage=os.getenv("GRAPH_STORAGE", "Neo4JStorage"),
)
await rag.initialize_storages()
return rag
# --------------------------------------------------
# Main
# --------------------------------------------------
async def main():
rag = None
try:
# Validate book file exists
if not os.path.exists(BOOK_FILE):
raise FileNotFoundError(
f"'{BOOK_FILE}' not found. Please provide a text file to index."
)
rag = await initialize_rag()
# --------------------------------------------------
# Data Ingestion
# --------------------------------------------------
print(f"Indexing {BOOK_FILE}...")
with open(BOOK_FILE, "r", encoding="utf-8") as f:
await rag.ainsert(f.read())
print("Indexing complete.")
# --------------------------------------------------
# Query
# --------------------------------------------------
query = (
"What are the main themes of the book, and how do the key characters "
"evolve throughout the story?"
)
print("\nHybrid Search with Reranking:")
result = await rag.aquery(
query,
param=QueryParam(
mode="hybrid",
stream=False,
enable_rerank=True,
),
)
print("\nResult:\n", result)
except Exception as e:
print(f"An error occurred: {e}")
finally:
if rag:
await rag.finalize_storages()
if __name__ == "__main__":
asyncio.run(main())
print("\nDone!")
@@ -0,0 +1,113 @@
"""
Example: Configuring Milvus Index Parameters via vector_db_storage_cls_kwargs
This example demonstrates how to configure Milvus indexing parameters through
vector_db_storage_cls_kwargs, which is the recommended approach when using
frameworks that build on top of LightRAG (like RAGAnything).
This approach allows configuration to be passed through framework layers without
requiring environment variable changes or direct code modifications.
"""
import os
import asyncio
from lightrag import LightRAG, QueryParam
from lightrag.llm.openai import openai_complete_if_cache, openai_embed
async def main():
# Configure Milvus connection
os.environ["MILVUS_URI"] = "http://localhost:19530"
# os.environ["MILVUS_USER"] = "root"
# os.environ["MILVUS_PASSWORD"] = "your_password"
# os.environ["MILVUS_DB_NAME"] = "lightrag"
# Initialize LightRAG with Milvus index configuration via vector_db_storage_cls_kwargs
# This is the recommended approach for framework integration (e.g., RAGAnything)
rag = LightRAG(
working_dir="./demo_index",
llm_model_func=openai_complete_if_cache,
embedding_func=openai_embed,
# Specify Milvus as the vector storage backend
vector_storage="MilvusVectorDBStorage",
# Configure Milvus indexing parameters via vector_db_storage_cls_kwargs
# These parameters are extracted and passed to MilvusIndexConfig
vector_db_storage_cls_kwargs={
# Required parameter for all vector storage backends
"cosine_better_than_threshold": 0.2,
# Milvus index configuration parameters
# All of these can be configured via vector_db_storage_cls_kwargs
# Index type (AUTOINDEX, HNSW, HNSW_SQ, IVF_FLAT, etc.)
"index_type": "HNSW",
# Distance metric (COSINE, L2, IP)
"metric_type": "COSINE",
# HNSW parameters
"hnsw_m": 32, # Number of connections per layer (2-2048)
"hnsw_ef_construction": 256, # Size of dynamic candidate list during construction
"hnsw_ef": 150, # Size of dynamic candidate list during search
# IVF parameters (used when index_type is IVF_FLAT, IVF_SQ8, IVF_PQ)
# "ivf_nlist": 2048, # Number of cluster units
# "ivf_nprobe": 32, # Number of units to query
# HNSW_SQ parameters (requires Milvus 2.6.8+)
# "sq_type": "SQ8", # Quantization type (SQ4U, SQ6, SQ8, BF16, FP16)
# "sq_refine": True, # Enable refinement
# "sq_refine_type": "FP32", # Refinement type
# "sq_refine_k": 20, # Number of candidates to refine
},
)
# Initialize storage backends
await rag.initialize_storages()
print(
"✅ LightRAG initialized with Milvus index configuration via vector_db_storage_cls_kwargs"
)
print(
f" Index Type: {rag.vector_db_storages['entities'].index_config.index_type}"
)
print(
f" Metric Type: {rag.vector_db_storages['entities'].index_config.metric_type}"
)
print(f" HNSW M: {rag.vector_db_storages['entities'].index_config.hnsw_m}")
print(
f" HNSW EF Construction: {rag.vector_db_storages['entities'].index_config.hnsw_ef_construction}"
)
print(f" HNSW EF: {rag.vector_db_storages['entities'].index_config.hnsw_ef}")
# Example: Insert some text
sample_text = """
LightRAG is a Retrieval-Augmented Generation framework that uses graph-based
knowledge representation for enhanced information retrieval. It supports multiple
vector storage backends including Milvus, which offers advanced indexing options
for optimal performance.
"""
await rag.ainsert(sample_text)
print("\n✅ Sample text inserted")
# Example: Query with different modes
result = await rag.aquery("What is LightRAG?", param=QueryParam(mode="hybrid"))
print(f"\n✅ Query result: {result[:200]}...")
# Cleanup
await rag.finalize_storages()
if __name__ == "__main__":
print("=" * 80)
print("Milvus Configuration via vector_db_storage_cls_kwargs Example")
print("=" * 80)
print()
print("This example shows how to configure Milvus indexing parameters through")
print("vector_db_storage_cls_kwargs, which is ideal for framework integration.")
print()
print("Key Benefits:")
print(" • No environment variable changes required")
print(" • Configuration can be passed through framework layers")
print(" • Perfect for RAGAnything and similar frameworks")
print(" • All 11 index parameters are supported")
print()
print("=" * 80)
print()
asyncio.run(main())
+355
View File
@@ -0,0 +1,355 @@
"""
Integration test for OpenSearch Storage in LightRAG.
Tests all 4 storage types against a live OpenSearch cluster:
- KV Storage: CRUD, filter_keys
- DocStatus Storage: CRUD, pagination (PIT + search_after), status counts
- Graph Storage: nodes, edges, BFS traversal, search_labels
- Vector Storage: k-NN upsert, query, get/delete
Prerequisites:
OpenSearch cluster running with k-NN plugin enabled.
Set env vars: OPENSEARCH_HOSTS, OPENSEARCH_USER, OPENSEARCH_PASSWORD,
OPENSEARCH_USE_SSL, OPENSEARCH_VERIFY_CERTS
Usage:
OPENSEARCH_HOSTS=localhost:9200 OPENSEARCH_USER=admin \
OPENSEARCH_PASSWORD=<password> OPENSEARCH_USE_SSL=true \
OPENSEARCH_VERIFY_CERTS=false python examples/opensearch_storage_demo.py
"""
import asyncio
import numpy as np
from lightrag.kg.opensearch_impl import (
OpenSearchKVStorage,
OpenSearchDocStatusStorage,
OpenSearchGraphStorage,
OpenSearchVectorDBStorage,
ClientManager,
)
from lightrag.kg.shared_storage import initialize_share_data
from lightrag.base import DocStatus
class MockEmbeddingFunc:
"""Mock embedding function for testing."""
def __init__(self, dim=128):
self.embedding_dim = dim
self.max_token_size = 512
self.model_name = "mock-embedding"
async def __call__(self, texts, **kwargs):
return np.random.rand(len(texts), self.embedding_dim).astype(np.float32)
CONFIG = {
"embedding_batch_num": 10,
"max_graph_nodes": 1000,
"vector_db_storage_cls_kwargs": {"cosine_better_than_threshold": 0.2},
}
EMBED = MockEmbeddingFunc()
PASSED = 0
FAILED = 0
def check(condition, msg):
global PASSED, FAILED
if condition:
print(f"{msg}")
PASSED += 1
else:
print(f"{msg}")
FAILED += 1
async def test_connection_manager():
print("\n=== Connection Manager ===")
client1 = await ClientManager.get_client()
client2 = await ClientManager.get_client()
check(client1 is client2, "Singleton pattern (same instance)")
await ClientManager.release_client(client1)
await ClientManager.release_client(client2)
check(True, "Released clients")
async def test_kv_storage():
print("\n=== KV Storage ===")
s = OpenSearchKVStorage(
namespace="integ_kv",
global_config=CONFIG,
embedding_func=EMBED,
workspace="integ",
)
await s.initialize()
try:
await s.upsert({"k1": {"content": "hello"}, "k2": {"content": "world"}})
await s.index_done_callback()
doc = await s.get_by_id("k1")
check(doc is not None and doc.get("content") == "hello", "get_by_id")
docs = await s.get_by_ids(["k1", "k2", "missing"])
check(docs[0] is not None and docs[2] is None, "get_by_ids preserves order")
missing = await s.filter_keys({"k1", "k99"})
check(missing == {"k99"}, f"filter_keys: {missing}")
check(not await s.is_empty(), "is_empty=False")
await s.delete(["k2"])
await s.index_done_callback()
check(await s.get_by_id("k2") is None, "delete + verify")
finally:
await s.drop()
await s.finalize()
async def test_doc_status_storage():
print("\n=== DocStatus Storage ===")
s = OpenSearchDocStatusStorage(
namespace="integ_ds",
global_config=CONFIG,
embedding_func=EMBED,
workspace="integ",
)
await s.initialize()
try:
# Insert docs
await s.upsert(
{
f"d{i}": {
"status": "processed" if i % 2 == 0 else "pending",
"file_path": f"/file{i}.txt",
"content_summary": f"summary {i}",
"content_length": i * 10,
"chunks_count": i,
"created_at": 1000 + i,
"updated_at": 2000 + i,
}
for i in range(20)
}
)
await s.index_done_callback()
# Status counts
counts = await s.get_all_status_counts()
check(counts.get("all") == 20, f"all_status_counts: {counts}")
check(
counts.get("processed") == 10, f"processed count: {counts.get('processed')}"
)
# get_docs_by_status (uses PIT + search_after)
processed = await s.get_docs_by_status(DocStatus.PROCESSED)
check(len(processed) == 10, f"get_docs_by_status(processed): {len(processed)}")
# get_docs_by_track_id (uses PIT + search_after)
await s.upsert(
{
"tracked1": {
"status": "processed",
"file_path": "/t.txt",
"content_summary": "s",
"content_length": 1,
"chunks_count": 1,
"created_at": 100,
"updated_at": 200,
"track_id": "batch-42",
}
}
)
await s.index_done_callback()
tracked = await s.get_docs_by_track_id("batch-42")
check(len(tracked) == 1, f"get_docs_by_track_id: {len(tracked)}")
# Paginated (uses PIT + search_after)
page1, total = await s.get_docs_paginated(page=1, page_size=10)
check(total == 21, f"paginated total: {total}")
check(len(page1) == 10, f"page1 size: {len(page1)}")
page2, _ = await s.get_docs_paginated(page=2, page_size=10)
check(len(page2) == 10, f"page2 size: {len(page2)}")
page3, _ = await s.get_docs_paginated(page=3, page_size=10)
check(len(page3) == 1, f"page3 size: {len(page3)}")
# With status filter
filtered, ftotal = await s.get_docs_paginated(
status_filter=DocStatus.PENDING, page=1, page_size=50
)
check(ftotal == 10, f"filtered total: {ftotal}")
# get_doc_by_file_path
doc = await s.get_doc_by_file_path("/file0.txt")
check(doc is not None and doc["_id"] == "d0", "get_doc_by_file_path")
finally:
await s.drop()
await s.finalize()
async def test_graph_storage():
print("\n=== Graph Storage ===")
s = OpenSearchGraphStorage(
namespace="integ_graph",
global_config=CONFIG,
embedding_func=EMBED,
workspace="integ",
)
await s.initialize()
try:
# Upsert nodes and edges
await s.upsert_node(
"Alice", {"entity_type": "person", "description": "A researcher"}
)
await s.upsert_node(
"Bob", {"entity_type": "person", "description": "A developer"}
)
await s.upsert_node(
"Quantum", {"entity_type": "topic", "description": "Quantum computing"}
)
await s.upsert_edge(
"Alice",
"Bob",
{"relationship": "knows", "weight": "1.0", "keywords": "collab"},
)
await s.upsert_edge(
"Alice",
"Quantum",
{"relationship": "researches", "weight": "2.0", "keywords": "research"},
)
await s.upsert_edge(
"Bob",
"Quantum",
{"relationship": "studies", "weight": "0.5", "keywords": "learning"},
)
await s.index_done_callback()
check(await s.has_node("Alice"), "has_node(Alice)")
check(not await s.has_node("Nobody"), "has_node(Nobody)=False")
check(await s.has_edge("Alice", "Bob"), "has_edge(Alice,Bob)")
node = await s.get_node("Alice")
check(node is not None and node.get("entity_type") == "person", "get_node")
check(node.get("entity_id") == "Alice", "entity_id field present")
check(
await s.node_degree("Alice") == 2,
f"node_degree(Alice)={await s.node_degree('Alice')}",
)
edges = await s.get_node_edges("Alice")
check(len(edges) == 2, f"get_node_edges: {len(edges)}")
# Batch ops
batch = await s.get_nodes_batch(["Alice", "Bob", "Missing"])
check("Alice" in batch and "Missing" not in batch, "get_nodes_batch")
degrees = await s.node_degrees_batch(["Alice", "Bob", "Quantum"])
check(degrees.get("Alice") == 2, f"node_degrees_batch: {degrees}")
# Knowledge graph (BFS)
kg = await s.get_knowledge_graph("Alice", max_depth=2)
check(len(kg.nodes) == 3, f"BFS nodes: {len(kg.nodes)}")
check(len(kg.edges) == 3, f"BFS edges: {len(kg.edges)}")
# get_all_labels (uses PIT)
labels = await s.get_all_labels()
check("Alice" in labels and "Bob" in labels, f"get_all_labels: {labels}")
# get_all_nodes (uses PIT)
all_nodes = await s.get_all_nodes()
check(len(all_nodes) == 3, f"get_all_nodes: {len(all_nodes)}")
# get_all_edges (uses PIT)
all_edges = await s.get_all_edges()
check(len(all_edges) == 3, f"get_all_edges: {len(all_edges)}")
# search_labels
found = await s.search_labels("ali", limit=10)
check("Alice" in found, f"search_labels('ali'): {found}")
# popular_labels
popular = await s.get_popular_labels(limit=10)
check(len(popular) > 0, f"get_popular_labels: {popular}")
# Delete node (cascading)
await s.delete_node("Bob")
await s.index_done_callback()
check(not await s.has_node("Bob"), "delete_node cascade")
check(not await s.has_edge("Alice", "Bob"), "edges removed after delete_node")
print(f" (PPL graphlookup: {s._ppl_graphlookup_available})")
finally:
await s.drop()
await s.finalize()
async def test_vector_storage():
print("\n=== Vector Storage ===")
s = OpenSearchVectorDBStorage(
namespace="integ_vec",
global_config=CONFIG,
embedding_func=EMBED,
workspace="integ",
meta_fields={"content", "entity_name"},
)
await s.initialize()
try:
await s.upsert(
{
"v1": {"content": "apple fruit"},
"v2": {"content": "banana fruit"},
"v3": {"content": "quantum physics"},
}
)
await s.index_done_callback()
results = await s.query("apple", top_k=3)
check(len(results) > 0, f"query returned {len(results)} results")
check(all("distance" in r for r in results), "results have distance")
doc = await s.get_by_id("v1")
check(doc is not None and doc["id"] == "v1", "get_by_id")
docs = await s.get_by_ids(["v1", "v2", "missing"])
check(docs[0] is not None and docs[2] is None, "get_by_ids")
vecs = await s.get_vectors_by_ids(["v1"])
check("v1" in vecs and len(vecs["v1"]) == 128, "get_vectors_by_ids")
await s.delete(["v3"])
await s.index_done_callback()
check(await s.get_by_id("v3") is None, "delete + verify")
finally:
await s.drop()
await s.finalize()
async def main():
print("=" * 60)
print("OpenSearch Storage Integration Tests")
print("=" * 60)
initialize_share_data(workers=1)
try:
await test_connection_manager()
await test_kv_storage()
await test_doc_status_storage()
await test_graph_storage()
await test_vector_storage()
except Exception as e:
print(f"\n✗ Fatal error: {e}")
import traceback
traceback.print_exc()
print(f"\n{'=' * 60}")
print(f"Results: {PASSED} passed, {FAILED} failed")
print(f"{'=' * 60}")
if FAILED > 0:
exit(1)
if __name__ == "__main__":
asyncio.run(main())
+234
View File
@@ -0,0 +1,234 @@
"""
LightRAG Rerank Integration Example
This example demonstrates how to use rerank functionality with LightRAG
to improve retrieval quality across different query modes.
Configuration Required:
1. Set your OpenAI LLM API key and base URL with env vars
LLM_MODEL
LLM_BINDING_HOST
LLM_BINDING_API_KEY
2. Set your OpenAI embedding API key and base URL with env vars:
EMBEDDING_MODEL
EMBEDDING_DIM
EMBEDDING_BINDING_HOST
EMBEDDING_BINDING_API_KEY
3. Set your vLLM deployed AI rerank model setting with env vars:
RERANK_BINDING=cohere
RERANK_MODEL (e.g., answerai-colbert-small-v1 or rerank-v3.5)
RERANK_BINDING_HOST (e.g., https://api.cohere.com/v2/rerank or LiteLLM proxy)
RERANK_BINDING_API_KEY
RERANK_ENABLE_CHUNKING=true (optional, for models with token limits)
RERANK_MAX_TOKENS_PER_DOC=480 (optional, default 4096)
Note: Rerank is controlled per query via the 'enable_rerank' parameter (default: True)
"""
import asyncio
import os
import numpy as np
from lightrag import LightRAG, QueryParam
from lightrag.llm.openai import openai_complete_if_cache, openai_embed
from lightrag.utils import EmbeddingFunc, setup_logger
from functools import partial
from lightrag.rerank import cohere_rerank
# Set up your working directory
WORKING_DIR = "./test_rerank"
setup_logger("test_rerank")
if not os.path.exists(WORKING_DIR):
os.mkdir(WORKING_DIR)
async def llm_model_func(
prompt, system_prompt=None, history_messages=[], **kwargs
) -> str:
return await openai_complete_if_cache(
os.getenv("LLM_MODEL"),
prompt,
system_prompt=system_prompt,
history_messages=history_messages,
api_key=os.getenv("LLM_BINDING_API_KEY"),
base_url=os.getenv("LLM_BINDING_HOST"),
**kwargs,
)
async def embedding_func(texts: list[str]) -> np.ndarray:
return await openai_embed(
texts,
model=os.getenv("EMBEDDING_MODEL"),
api_key=os.getenv("EMBEDDING_BINDING_API_KEY"),
base_url=os.getenv("EMBEDDING_BINDING_HOST"),
)
rerank_model_func = partial(
cohere_rerank,
model=os.getenv("RERANK_MODEL", "rerank-v3.5"),
api_key=os.getenv("RERANK_BINDING_API_KEY"),
base_url=os.getenv("RERANK_BINDING_HOST", "https://api.cohere.com/v2/rerank"),
enable_chunking=os.getenv("RERANK_ENABLE_CHUNKING", "false").lower() == "true",
max_tokens_per_doc=int(os.getenv("RERANK_MAX_TOKENS_PER_DOC", "4096")),
)
async def create_rag_with_rerank():
"""Create LightRAG instance with rerank configuration"""
# Get embedding dimension
test_embedding = await embedding_func(["test"])
embedding_dim = test_embedding.shape[1]
print(f"Detected embedding dimension: {embedding_dim}")
# Method 1: Using custom rerank function
rag = LightRAG(
working_dir=WORKING_DIR,
llm_model_func=llm_model_func,
embedding_func=EmbeddingFunc(
embedding_dim=embedding_dim,
max_token_size=8192,
func=embedding_func,
),
# Rerank Configuration - provide the rerank function
rerank_model_func=rerank_model_func,
)
await rag.initialize_storages() # Auto-initializes pipeline_status
return rag
async def test_rerank_with_different_settings():
"""
Test rerank functionality with different enable_rerank settings
"""
print("\n\n🚀 Setting up LightRAG with Rerank functionality...")
rag = await create_rag_with_rerank()
# Insert sample documents
sample_docs = [
"Reranking improves retrieval quality by re-ordering documents based on relevance.",
"LightRAG is a powerful retrieval-augmented generation system with multiple query modes.",
"Vector databases enable efficient similarity search in high-dimensional embedding spaces.",
"Natural language processing has evolved with large language models and transformers.",
"Machine learning algorithms can learn patterns from data without explicit programming.",
]
print("📄 Inserting sample documents...")
await rag.ainsert(sample_docs)
query = "How does reranking improve retrieval quality?"
print(f"\n🔍 Testing query: '{query}'")
print("=" * 80)
# Test with rerank enabled (default)
print("\n📊 Testing with enable_rerank=True (default):")
result_with_rerank = await rag.aquery(
query,
param=QueryParam(
mode="naive",
top_k=10,
chunk_top_k=5,
enable_rerank=True, # Explicitly enable rerank
),
)
print(f" Result length: {len(result_with_rerank)} characters")
print(f" Preview: {result_with_rerank[:100]}...")
# Test with rerank disabled
print("\n📊 Testing with enable_rerank=False:")
result_without_rerank = await rag.aquery(
query,
param=QueryParam(
mode="naive",
top_k=10,
chunk_top_k=5,
enable_rerank=False, # Disable rerank
),
)
print(f" Result length: {len(result_without_rerank)} characters")
print(f" Preview: {result_without_rerank[:100]}...")
# Test with default settings (enable_rerank defaults to True)
print("\n📊 Testing with default settings (enable_rerank defaults to True):")
result_default = await rag.aquery(
query, param=QueryParam(mode="naive", top_k=10, chunk_top_k=5)
)
print(f" Result length: {len(result_default)} characters")
print(f" Preview: {result_default[:100]}...")
async def test_direct_rerank():
"""Test rerank function directly"""
print("\n🔧 Direct Rerank API Test")
print("=" * 40)
documents = [
"Vector search finds semantically similar documents",
"LightRAG supports advanced reranking capabilities",
"Reranking significantly improves retrieval quality",
"Natural language processing with modern transformers",
"The quick brown fox jumps over the lazy dog",
]
query = "rerank improve quality"
print(f"Query: '{query}'")
print(f"Documents: {len(documents)}")
try:
reranked_results = await rerank_model_func(
query=query,
documents=documents,
top_n=4,
)
print("\n✅ Rerank Results:")
i = 0
for result in reranked_results:
index = result["index"]
score = result["relevance_score"]
content = documents[index]
print(f" {index}. Score: {score:.4f} | {content}...")
i += 1
except Exception as e:
print(f"❌ Rerank failed: {e}")
async def main():
"""Main example function"""
print("🎯 LightRAG Rerank Integration Example")
print("=" * 60)
try:
# Test direct rerank
await test_direct_rerank()
# Test rerank with different enable_rerank settings
await test_rerank_with_different_settings()
print("\n✅ Example completed successfully!")
print("\n💡 Key Points:")
print(" ✓ Rerank is now controlled per query via 'enable_rerank' parameter")
print(" ✓ Default value for enable_rerank is True")
print(" ✓ Rerank function is configured at LightRAG initialization")
print(" ✓ Per-query enable_rerank setting overrides default behavior")
print(
" ✓ If enable_rerank=True but no rerank model is configured, a warning is issued"
)
print(" ✓ Monitor API usage and costs when using rerank services")
except Exception as e:
print(f"\n❌ Example failed: {e}")
import traceback
traceback.print_exc()
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,114 @@
"""
Sometimes you need to switch a storage solution, but you want to save LLM token and time.
This handy script helps you to copy the LLM caches from one storage solution to another.
(Not all the storage impl are supported)
"""
import asyncio
import logging
import os
from dotenv import load_dotenv
from lightrag.kg.postgres_impl import PostgreSQLDB, PGKVStorage
from lightrag.kg.json_kv_impl import JsonKVStorage
from lightrag.namespace import NameSpace
load_dotenv()
ROOT_DIR = os.environ.get("ROOT_DIR")
WORKING_DIR = f"{ROOT_DIR}/dickens"
logging.basicConfig(format="%(levelname)s:%(message)s", level=logging.INFO)
if not os.path.exists(WORKING_DIR):
os.mkdir(WORKING_DIR)
# AGE
os.environ["AGE_GRAPH_NAME"] = "chinese"
postgres_db = PostgreSQLDB(
config={
"host": "localhost",
"port": 15432,
"user": "rag",
"password": "rag",
"database": "r2",
}
)
async def copy_from_postgres_to_json():
await postgres_db.initdb()
from_llm_response_cache = PGKVStorage(
namespace=NameSpace.KV_STORE_LLM_RESPONSE_CACHE,
global_config={"embedding_batch_num": 6},
embedding_func=None,
db=postgres_db,
)
to_llm_response_cache = JsonKVStorage(
namespace=NameSpace.KV_STORE_LLM_RESPONSE_CACHE,
global_config={"working_dir": WORKING_DIR},
embedding_func=None,
)
# Get all cache data using the new flattened structure
all_data = await from_llm_response_cache.get_all()
# Convert flattened data to hierarchical structure for JsonKVStorage
kv = {}
for flattened_key, cache_entry in all_data.items():
# Parse flattened key: {mode}:{cache_type}:{hash}
parts = flattened_key.split(":", 2)
if len(parts) == 3:
mode, cache_type, hash_value = parts
if mode not in kv:
kv[mode] = {}
kv[mode][hash_value] = cache_entry
print(f"Copying {flattened_key} -> {mode}[{hash_value}]")
else:
print(f"Skipping invalid key format: {flattened_key}")
await to_llm_response_cache.upsert(kv)
await to_llm_response_cache.index_done_callback()
print("Mission accomplished!")
async def copy_from_json_to_postgres():
await postgres_db.initdb()
from_llm_response_cache = JsonKVStorage(
namespace=NameSpace.KV_STORE_LLM_RESPONSE_CACHE,
global_config={"working_dir": WORKING_DIR},
embedding_func=None,
)
to_llm_response_cache = PGKVStorage(
namespace=NameSpace.KV_STORE_LLM_RESPONSE_CACHE,
global_config={"embedding_batch_num": 6},
embedding_func=None,
db=postgres_db,
)
# Get all cache data from JsonKVStorage (hierarchical structure)
all_data = await from_llm_response_cache.get_all()
# Convert hierarchical data to flattened structure for PGKVStorage
flattened_data = {}
for mode, mode_data in all_data.items():
print(f"Processing mode: {mode}")
for hash_value, cache_entry in mode_data.items():
# Determine cache_type from cache entry or use default
cache_type = cache_entry.get("cache_type", "extract")
# Create flattened key: {mode}:{cache_type}:{hash}
flattened_key = f"{mode}:{cache_type}:{hash_value}"
flattened_data[flattened_key] = cache_entry
print(f"\tConverting {mode}[{hash_value}] -> {flattened_key}")
# Upsert the flattened data
await to_llm_response_cache.upsert(flattened_data)
print("Mission accomplished!")
if __name__ == "__main__":
asyncio.run(copy_from_json_to_postgres())
@@ -0,0 +1,56 @@
"""
LightRAG meets Amazon Bedrock ⛰️
"""
import os
import logging
from lightrag import LightRAG, QueryParam
from lightrag.llm.bedrock import bedrock_complete, bedrock_embed
from lightrag.utils import EmbeddingFunc
import asyncio
import nest_asyncio
nest_asyncio.apply()
logging.getLogger("aiobotocore").setLevel(logging.WARNING)
WORKING_DIR = "./dickens"
if not os.path.exists(WORKING_DIR):
os.mkdir(WORKING_DIR)
async def initialize_rag():
rag = LightRAG(
working_dir=WORKING_DIR,
llm_model_func=bedrock_complete,
llm_model_name="Anthropic Claude 3 Haiku // Amazon Bedrock",
embedding_func=EmbeddingFunc(
embedding_dim=1024, max_token_size=8192, func=bedrock_embed
),
)
await rag.initialize_storages() # Auto-initializes pipeline_status
return rag
def main():
rag = asyncio.run(initialize_rag())
with open("./book.txt", "r", encoding="utf-8") as f:
rag.insert(f.read())
for mode in ["naive", "local", "global", "hybrid"]:
print("\n+-" + "-" * len(mode) + "-+")
print(f"| {mode.capitalize()} |")
print("+-" + "-" * len(mode) + "-+\n")
print(
rag.query(
"What are the top themes in this story?", param=QueryParam(mode=mode)
)
)
if __name__ == "__main__":
main()
@@ -0,0 +1,354 @@
import asyncio
import os
import inspect
import logging
import logging.config
from lightrag import LightRAG, QueryParam
from lightrag.utils import EmbeddingFunc, logger, set_verbose_debug
import requests
import numpy as np
from dotenv import load_dotenv
"""This code is a modified version of lightrag_openai_demo.py"""
# ideally, as always, env!
load_dotenv(dotenv_path=".env", override=False)
""" ----========= IMPORTANT CHANGE THIS! =========---- """
cloudflare_api_key = "YOUR_API_KEY"
account_id = "YOUR_ACCOUNT ID" # This is unique to your Cloudflare account
# Authomatically changes
api_base_url = f"https://api.cloudflare.com/client/v4/accounts/{account_id}/ai/run/"
# choose an embedding model
EMBEDDING_MODEL = "@cf/baai/bge-m3"
# choose a generative model
LLM_MODEL = "@cf/meta/llama-3.2-3b-instruct"
WORKING_DIR = "../dickens" # you can change output as desired
# Cloudflare init
class CloudflareWorker:
def __init__(
self,
cloudflare_api_key: str,
api_base_url: str,
llm_model_name: str,
embedding_model_name: str,
max_tokens: int = 4080,
max_response_tokens: int = 4080,
):
self.cloudflare_api_key = cloudflare_api_key
self.api_base_url = api_base_url
self.llm_model_name = llm_model_name
self.embedding_model_name = embedding_model_name
self.max_tokens = max_tokens
self.max_response_tokens = max_response_tokens
async def _send_request(self, model_name: str, input_: dict, debug_log: str):
headers = {"Authorization": f"Bearer {self.cloudflare_api_key}"}
print(f"""
data sent to Cloudflare
~~~~~~~~~~~
{debug_log}
""")
try:
response_raw = requests.post(
f"{self.api_base_url}{model_name}", headers=headers, json=input_
).json()
print(f"""
Cloudflare worker responded with:
~~~~~~~~~~~
{str(response_raw)}
""")
result = response_raw.get("result", {})
if "data" in result: # Embedding case
return np.array(result["data"])
if "response" in result: # LLM response
return result["response"]
raise ValueError("Unexpected Cloudflare response format")
except Exception as e:
print(f"""
Cloudflare API returned:
~~~~~~~~~
Error: {e}
""")
input("Press Enter to continue...")
return None
async def query(self, prompt, system_prompt: str = "", **kwargs) -> str:
# since no caching is used and we don't want to mess with everything lightrag, pop the kwarg it is
kwargs.pop("hashing_kv", None)
message = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": prompt},
]
input_ = {
"messages": message,
"max_tokens": self.max_tokens,
"response_token_limit": self.max_response_tokens,
}
return await self._send_request(
self.llm_model_name,
input_,
debug_log=f"\n- model used {self.llm_model_name}\n- system prompt: {system_prompt}\n- query: {prompt}",
)
async def embedding_chunk(self, texts: list[str]) -> np.ndarray:
print(f"""
TEXT inputted
~~~~~
{texts}
""")
input_ = {
"text": texts,
"max_tokens": self.max_tokens,
"response_token_limit": self.max_response_tokens,
}
return await self._send_request(
self.embedding_model_name,
input_,
debug_log=f"\n-llm model name {self.embedding_model_name}\n- texts: {texts}",
)
def configure_logging():
"""Configure logging for the application"""
# Reset any existing handlers to ensure clean configuration
for logger_name in ["uvicorn", "uvicorn.access", "uvicorn.error", "lightrag"]:
logger_instance = logging.getLogger(logger_name)
logger_instance.handlers = []
logger_instance.filters = []
# Get log directory path from environment variable or use current directory
log_dir = os.getenv("LOG_DIR", os.getcwd())
log_file_path = os.path.abspath(
os.path.join(log_dir, "lightrag_cloudflare_worker_demo.log")
)
print(f"\nLightRAG compatible demo log file: {log_file_path}\n")
os.makedirs(os.path.dirname(log_file_path), exist_ok=True)
# Get log file max size and backup count from environment variables
log_max_bytes = int(os.getenv("LOG_MAX_BYTES", 10485760)) # Default 10MB
log_backup_count = int(os.getenv("LOG_BACKUP_COUNT", 5)) # Default 5 backups
logging.config.dictConfig(
{
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"default": {
"format": "%(levelname)s: %(message)s",
},
"detailed": {
"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
},
},
"handlers": {
"console": {
"formatter": "default",
"class": "logging.StreamHandler",
"stream": "ext://sys.stderr",
},
"file": {
"formatter": "detailed",
"class": "logging.handlers.RotatingFileHandler",
"filename": log_file_path,
"maxBytes": log_max_bytes,
"backupCount": log_backup_count,
"encoding": "utf-8",
},
},
"loggers": {
"lightrag": {
"handlers": ["console", "file"],
"level": "INFO",
"propagate": False,
},
},
}
)
# Set the logger level to INFO
logger.setLevel(logging.INFO)
# Enable verbose debug if needed
set_verbose_debug(os.getenv("VERBOSE_DEBUG", "false").lower() == "true")
if not os.path.exists(WORKING_DIR):
os.mkdir(WORKING_DIR)
async def initialize_rag():
cloudflare_worker = CloudflareWorker(
cloudflare_api_key=cloudflare_api_key,
api_base_url=api_base_url,
embedding_model_name=EMBEDDING_MODEL,
llm_model_name=LLM_MODEL,
)
rag = LightRAG(
working_dir=WORKING_DIR,
max_parallel_insert=2,
llm_model_func=cloudflare_worker.query,
llm_model_name=os.getenv("LLM_MODEL", LLM_MODEL),
summary_max_tokens=4080,
embedding_func=EmbeddingFunc(
embedding_dim=int(os.getenv("EMBEDDING_DIM", "1024")),
max_token_size=int(os.getenv("MAX_EMBED_TOKENS", "2048")),
func=lambda texts: cloudflare_worker.embedding_chunk(
texts,
),
),
)
await rag.initialize_storages() # Auto-initializes pipeline_status
return rag
async def print_stream(stream):
async for chunk in stream:
print(chunk, end="", flush=True)
async def main():
try:
# Clear old data files
files_to_delete = [
"graph_chunk_entity_relation.graphml",
"kv_store_doc_status.json",
"kv_store_full_docs.json",
"kv_store_text_chunks.json",
"vdb_chunks.json",
"vdb_entities.json",
"vdb_relationships.json",
]
for file in files_to_delete:
file_path = os.path.join(WORKING_DIR, file)
if os.path.exists(file_path):
os.remove(file_path)
print(f"Deleting old file:: {file_path}")
# Initialize RAG instance
rag = await initialize_rag()
# Test embedding function
test_text = ["This is a test string for embedding."]
embedding = await rag.embedding_func(test_text)
embedding_dim = embedding.shape[1]
print("\n=======================")
print("Test embedding function")
print("========================")
print(f"Test dict: {test_text}")
print(f"Detected embedding dimension: {embedding_dim}\n\n")
# Locate the location of what is needed to be added to the knowledge
# Can add several simultaneously by modifying code
with open("./book.txt", "r", encoding="utf-8") as f:
await rag.ainsert(f.read())
# Perform naive search
print("\n=====================")
print("Query mode: naive")
print("=====================")
resp = await rag.aquery(
"What are the top themes in this story?",
param=QueryParam(mode="naive", stream=True),
)
if inspect.isasyncgen(resp):
await print_stream(resp)
else:
print(resp)
# Perform local search
print("\n=====================")
print("Query mode: local")
print("=====================")
resp = await rag.aquery(
"What are the top themes in this story?",
param=QueryParam(mode="local", stream=True),
)
if inspect.isasyncgen(resp):
await print_stream(resp)
else:
print(resp)
# Perform global search
print("\n=====================")
print("Query mode: global")
print("=====================")
resp = await rag.aquery(
"What are the top themes in this story?",
param=QueryParam(mode="global", stream=True),
)
if inspect.isasyncgen(resp):
await print_stream(resp)
else:
print(resp)
# Perform hybrid search
print("\n=====================")
print("Query mode: hybrid")
print("=====================")
resp = await rag.aquery(
"What are the top themes in this story?",
param=QueryParam(mode="hybrid", stream=True),
)
if inspect.isasyncgen(resp):
await print_stream(resp)
else:
print(resp)
""" FOR TESTING (if you want to test straight away, after building. Uncomment this part"""
"""
print("\n" + "=" * 60)
print("AI ASSISTANT READY!")
print("Ask questions about (your uploaded) regulations")
print("Type 'quit' to exit")
print("=" * 60)
while True:
question = input("\n🔥 Your question: ")
if question.lower() in ['quit', 'exit', 'bye']:
break
print("\nThinking...")
response = await rag.aquery(question, param=QueryParam(mode="hybrid"))
print(f"\nAnswer: {response}")
"""
except Exception as e:
print(f"An error occurred: {e}")
finally:
if rag:
await rag.llm_response_cache.index_done_callback()
await rag.finalize_storages()
if __name__ == "__main__":
# Configure logging before running the main function
configure_logging()
asyncio.run(main())
print("\nDone!")
@@ -0,0 +1,235 @@
import os
import asyncio
import inspect
import logging
import logging.config
from functools import partial
from lightrag import LightRAG, QueryParam
from lightrag.llm.openai import openai_complete_if_cache
from lightrag.llm.ollama import ollama_embed
from lightrag.utils import EmbeddingFunc, logger, set_verbose_debug
from dotenv import load_dotenv
load_dotenv(dotenv_path=".env", override=False)
WORKING_DIR = "./dickens"
def configure_logging():
"""Configure logging for the application"""
# Reset any existing handlers to ensure clean configuration
for logger_name in ["uvicorn", "uvicorn.access", "uvicorn.error", "lightrag"]:
logger_instance = logging.getLogger(logger_name)
logger_instance.handlers = []
logger_instance.filters = []
# Get log directory path from environment variable or use current directory
log_dir = os.getenv("LOG_DIR", os.getcwd())
log_file_path = os.path.abspath(
os.path.join(log_dir, "lightrag_compatible_demo.log")
)
print(f"\nLightRAG compatible demo log file: {log_file_path}\n")
os.makedirs(os.path.dirname(log_file_path), exist_ok=True)
# Get log file max size and backup count from environment variables
log_max_bytes = int(os.getenv("LOG_MAX_BYTES", 10485760)) # Default 10MB
log_backup_count = int(os.getenv("LOG_BACKUP_COUNT", 5)) # Default 5 backups
logging.config.dictConfig(
{
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"default": {
"format": "%(levelname)s: %(message)s",
},
"detailed": {
"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
},
},
"handlers": {
"console": {
"formatter": "default",
"class": "logging.StreamHandler",
"stream": "ext://sys.stderr",
},
"file": {
"formatter": "detailed",
"class": "logging.handlers.RotatingFileHandler",
"filename": log_file_path,
"maxBytes": log_max_bytes,
"backupCount": log_backup_count,
"encoding": "utf-8",
},
},
"loggers": {
"lightrag": {
"handlers": ["console", "file"],
"level": "INFO",
"propagate": False,
},
},
}
)
# Set the logger level to INFO
logger.setLevel(logging.INFO)
# Enable verbose debug if needed
set_verbose_debug(os.getenv("VERBOSE_DEBUG", "false").lower() == "true")
if not os.path.exists(WORKING_DIR):
os.mkdir(WORKING_DIR)
async def llm_model_func(
prompt, system_prompt=None, history_messages=[], keyword_extraction=False, **kwargs
) -> str:
return await openai_complete_if_cache(
os.getenv("LLM_MODEL", "deepseek-chat"),
prompt,
system_prompt=system_prompt,
history_messages=history_messages,
api_key=os.getenv("LLM_BINDING_API_KEY") or os.getenv("OPENAI_API_KEY"),
base_url=os.getenv("LLM_BINDING_HOST", "https://api.deepseek.com"),
**kwargs,
)
async def print_stream(stream):
async for chunk in stream:
if chunk:
print(chunk, end="", flush=True)
async def initialize_rag():
rag = LightRAG(
working_dir=WORKING_DIR,
llm_model_func=llm_model_func,
# Note: ollama_embed is decorated with @wrap_embedding_func_with_attrs,
# which wraps it in an EmbeddingFunc. Using .func accesses the original
# unwrapped function to avoid double wrapping when we create our own
# EmbeddingFunc with custom configuration (embedding_dim, max_token_size and prefixes).
embedding_func=EmbeddingFunc(
embedding_dim=int(os.getenv("EMBEDDING_DIM", "1024")),
max_token_size=int(os.getenv("MAX_EMBED_TOKENS", "8192")),
supports_asymmetric=True,
func=partial(
ollama_embed.func, # Access the unwrapped function to avoid double EmbeddingFunc wrapping
embed_model=os.getenv("EMBEDDING_MODEL", "FRIDA:latest"),
host=os.getenv("EMBEDDING_BINDING_HOST", "http://localhost:11434"),
query_prefix=os.getenv("EMBEDDING_QUERY_PREFIX", "search_query: "),
document_prefix=os.getenv(
"EMBEDDING_DOCUMENT_PREFIX", "search_document: "
),
),
),
)
await rag.initialize_storages() # Auto-initializes pipeline_status
return rag
async def main():
rag = None
try:
# Clear old data files
files_to_delete = [
"graph_chunk_entity_relation.graphml",
"kv_store_doc_status.json",
"kv_store_full_docs.json",
"kv_store_text_chunks.json",
"vdb_chunks.json",
"vdb_entities.json",
"vdb_relationships.json",
]
for file in files_to_delete:
file_path = os.path.join(WORKING_DIR, file)
if os.path.exists(file_path):
os.remove(file_path)
print(f"Deleting old file:: {file_path}")
# Initialize RAG instance
rag = await initialize_rag()
# Test embedding function
test_text = ["This is a test string for embedding."]
embedding = await rag.embedding_func(test_text)
embedding_dim = embedding.shape[1]
print("\n=======================")
print("Test embedding function")
print("========================")
print(f"Test dict: {test_text}")
print(f"Detected embedding dimension: {embedding_dim}\n\n")
with open("./book.txt", "r", encoding="utf-8") as f:
await rag.ainsert(f.read())
# Perform naive search
print("\n=====================")
print("Query mode: naive")
print("=====================")
resp = await rag.aquery(
"What are the top themes in this story?",
param=QueryParam(mode="naive", stream=True),
)
if inspect.isasyncgen(resp):
await print_stream(resp)
else:
print(resp)
# Perform local search
print("\n=====================")
print("Query mode: local")
print("=====================")
resp = await rag.aquery(
"What are the top themes in this story?",
param=QueryParam(mode="local", stream=True),
)
if inspect.isasyncgen(resp):
await print_stream(resp)
else:
print(resp)
# Perform global search
print("\n=====================")
print("Query mode: global")
print("=====================")
resp = await rag.aquery(
"What are the top themes in this story?",
param=QueryParam(mode="global", stream=True),
)
if inspect.isasyncgen(resp):
await print_stream(resp)
else:
print(resp)
# Perform hybrid search
print("\n=====================")
print("Query mode: hybrid")
print("=====================")
resp = await rag.aquery(
"What are the top themes in this story?",
param=QueryParam(mode="hybrid", stream=True),
)
if inspect.isasyncgen(resp):
await print_stream(resp)
else:
print(resp)
except Exception as e:
print(f"An error occurred: {e}")
finally:
if rag:
await rag.finalize_storages()
if __name__ == "__main__":
# Configure logging before running the main function
configure_logging()
asyncio.run(main())
print("\nDone!")
@@ -0,0 +1,79 @@
import os
from lightrag import LightRAG, QueryParam
from lightrag.llm.hf import hf_model_complete, hf_embed
from lightrag.utils import EmbeddingFunc
from transformers import AutoModel, AutoTokenizer
import asyncio
import nest_asyncio
nest_asyncio.apply()
WORKING_DIR = "./dickens"
if not os.path.exists(WORKING_DIR):
os.mkdir(WORKING_DIR)
async def initialize_rag():
rag = LightRAG(
working_dir=WORKING_DIR,
llm_model_func=hf_model_complete,
llm_model_name="meta-llama/Llama-3.1-8B-Instruct",
embedding_func=EmbeddingFunc(
embedding_dim=384,
max_token_size=5000,
func=lambda texts: hf_embed(
texts,
tokenizer=AutoTokenizer.from_pretrained(
"sentence-transformers/all-MiniLM-L6-v2"
),
embed_model=AutoModel.from_pretrained(
"sentence-transformers/all-MiniLM-L6-v2"
),
),
),
)
await rag.initialize_storages() # Auto-initializes pipeline_status
return rag
def main():
rag = asyncio.run(initialize_rag())
with open("./book.txt", "r", encoding="utf-8") as f:
rag.insert(f.read())
# Perform naive search
print(
rag.query(
"What are the top themes in this story?", param=QueryParam(mode="naive")
)
)
# Perform local search
print(
rag.query(
"What are the top themes in this story?", param=QueryParam(mode="local")
)
)
# Perform global search
print(
rag.query(
"What are the top themes in this story?", param=QueryParam(mode="global")
)
)
# Perform hybrid search
print(
rag.query(
"What are the top themes in this story?", param=QueryParam(mode="hybrid")
)
)
if __name__ == "__main__":
main()

Some files were not shown because too many files have changed in this diff Show More