技术文档混合搜索服务

高性能技术文档混合搜索服务,结合语义搜索和关键词搜索,适用于开发者文档检索。

  • by buzzwomen-dev2
  • Python
  • 本地部署
  • 更新于
在 GitHub 查看 buzzwomen-dev2/docs-mcp-server

使用说明

Docs MCP服务器-技术文档的混合搜索

高性能 模型上下文协议(MCP)服务器 其提供混合搜索功能,结合了:

  • 语义搜索 使用FastEmbed(BAAI/bge-small-en-v1.5)+Qdrant矢量数据库
  • 关键词搜索 使用带有倒排索引的Elasticsearch BM25
  • 加权融合 (默认为0.4 BM25+0.6语义)

专为跨Django、Django REST Framework、Psycopg和其他技术文档的本地开发人员文档搜索而设计。

先决条件

此服务器需要 弹性搜索 和 Qdrant 跑步。最简单的方法是使用Docker:

选项1:使用Docker Compose(推荐)

# Start services (Elasticsearch + Qdrant)
./start-services.sh

# Stop services
./stop-services.sh

脚本将:

  • 在端口9200上启动Elasticsearch
  • 在端口6333上启动Qdrant
  • 等待两种服务都健康
  • 在Docker卷中持久化数据

选项2:手动Docker命令

# Elasticsearch
docker run -d \
  --name docs-elasticsearch \
  -p 9200:9200 \
  -e "discovery.type=single-node" \
  -e "xpack.security.enabled=false" \
  elasticsearch:8.11.0

# Qdrant
docker run -d \
  --name docs-qdrant \
  -p 6333:6333 \
  -p 6334:6334 \
  qdrant/qdrant

选项3:本机安装

  • 弹性搜索: https://www.elastic.co/downloads/elasticsearch
  • Qdrant: https://qdrant.tech/documentation/quick-start/

特性

  • 🔍 混合搜索:将语义理解与关键字精度相结合
  • 🏷️ 元数据:按技术、组件、主题、版本自动标记文档
  • 📊 结构化结果:具有个人和组合分数的Pydantic模型
  • 🚀 双接口:MCP工具+REST API
  • 💾 持久索引:Elasticsearch+Qdrant,带基于磁盘的元数据缓存
  • 📝 智能分块:散文语义(约100个标记),代码结构(80-150行)
  • 🔒 仅限本地主机:默认情况下安全,无外部访问
  • ⚡ 快速嵌入:CPU优化的FastEmbed(无需GPU)

快速开始

1.安装依赖项

# Using uv (recommended)
uv sync

# Or using pip
pip install -e .

2.构建搜索索引

首次设置-索引您的文档:

# Use the build script (recommended)
uv run scripts/build_index.py

# Or index programmatically
uv run python -c "
import asyncio
from pathlib import Path
from search_engine import IndexManager

async def main():
    manager = IndexManager()
    stats = await manager.index_documents([Path('docs')])
    print(f'Indexed {stats["chunks_created"]} chunks from {stats["files_processed"]} files')

asyncio.run(main())
"

或者在启动服务器后使用MCP工具(见下文)。

3.运行服务器

作为MCP服务器(stdio)

uv run server.py

然后在您的MCP客户端(Claude Desktop、VSCode等)中进行配置:

{
  "mcpServers": {
    "docs-server": {
      "command": "uv",
      "args": ["run", "server.py"],
      "cwd": "/path/to/docs-mcp-server"
    }
  }
}

作为HTTP API服务器

uv run server.py --http

访问地址:

  • API http://localhost:8000
  • 文件: http://localhost:8000/docs

4.验证设置

# Run validation script
uv run scripts/test_setup.py

5.使用MCP检查员进行测试

# In one terminal: start server
uv run server.py

# In another terminal: start inspector
npx @modelcontextprotocol/inspector uv run server.py

引导到 http://localhost:5173 并测试工具。

MCP功能

此服务器通过以下方式实现了完整的MCP规范 工具, 资源,以及 提示.

工具

search

语义+关键字匹配的混合搜索。

参数:

  • query (必填):搜索查询文本
  • tech (可选):按技术筛选(django/drf/psycopg)
  • component (可选):按组件/模块筛选
  • top_k (默认值10):结果数量

例子:

search(
    query="How does Django auth middleware work?",
    tech="django",
    top_k=5
)

退货: 列表 SearchResultModel 与:

  • chunk_id:唯一标识符
  • content:块状文本(截断为500个字符)
  • source_path:原始文件路径
  • tech, component, topic, version, file_type:元数据
  • bm25_score, semantic_score, hybrid_score:个人和综合得分

retrieve

按ID获取特定块。

参数:

  • chunk_id (必填):来自搜索结果

退货: 满的 SearchResultModel 内容完整。

reindex_docs

构建或重建搜索索引。使用校验和跳过未更改的文件。

参数:

  • paths (可选):要索引的特定文件/目录路径
  • tech (可选):索引特定技术(django/drf/psycopg)

示例:

# Reindex all docs (incremental - skips unchanged)
reindex_docs()

# Index only Django docs
reindex_docs(tech="django")

# Index specific paths
reindex_docs(paths=["docs/django-6.0/ref"])

退货: 统计数据 files_processed, chunks_created, errors.

注: 默认情况下,这将执行增量更新。要强制完全重建,请使用带有 --force 旗帜。

get_index_stats

获取指数统计数据和健康状况。

退货: IndexStats 包括块计数、模型信息、技术等。

list_sources

列出所有索引源文件。

参数:

  • tech (可选):按技术筛选

退货: 列表 SourceInfo 包含文件元数据和块计数。

search_docs (已弃用)

仅搜索传统关键字。使用 search 而是为了获得更好的结果。

list_docs

显示技术的目录结构。

list_technologies

列出可用的文档集。

资源

MCP资源提供对文档的直接访问,以用作上下文:

doc://{tech}/{path}

访问原始文档文件:

doc://django/ref/models/fields.md
doc://drf/api-guide/serializers.md

chunk://{chunk_id}

从搜索结果中访问索引块:

chunk://middleware_3_1234

VS代码中的用法:选择 添加上下文 > MCP资源 在聊天视图中。

提示

预配置的搜索提示以斜线命令形式提供:

  • /mcp.docsSearch.find_authentication_docs -身份验证和授权文档
  • /mcp.docsSearch.find_database_docs -数据库和ORM文档
  • /mcp.docsSearch.find_api_docs -API和序列化文档

每个提示接受一个 tech 参数(django/drf/psycopg)。

VS代码中的用法:类型 / 在聊天输入中查看可用的MCP提示。

REST API端点

所有端点都绑定到 127.0.0.1:8000 (仅限本地主机)。

POST /search

curl -X POST http://localhost:8000/search \
  -H "Content-Type: application/json" \
  -d '{
    "query": "Django middleware authentication",
    "tech": "django",
    "top_k": 10
  }'

GET /retrieve/{chunk_id}

curl http://localhost:8000/retrieve/middleware_3_1234

GET /status

curl http://localhost:8000/status

GET /sources?tech=django

curl http://localhost:8000/sources?tech=django

POST /reindex

curl -X POST http://localhost:8000/reindex \
  -H "Content-Type: application/json" \
  -d '{"tech": "django"}'

配置

环境变量

所有配置都通过环境变量进行管理。在运行服务器或生成脚本之前设置这些。

核心设置

变量 默认值 描述
INDEX_DIR .index 元数据缓存目录(校验和、缓存数据)
EMBEDDING_MODEL BAAI/bge-small-en-v1.5 语义嵌入的FastEmbed模型
CHUNK_SIZE_TOKENS 100 语义拆分令牌中的目标块大小

搜索权重

变量 默认值 描述
BM25_WEIGHT 0.4 BM25关键词得分权重(0.0-1.0)
SEMANTIC_WEIGHT 0.6 语义相似度得分权重(0.0-1.0)

注: 权重之和应为1.0。对于关键字较多的查询,增加BM25,增加概念匹配的语义。

Elasticsearch配置

变量 默认值 描述
ES_HOST http://localhost:9200 Elasticsearch服务器URL
ES_INDEX_NAME docs_index BM25文档的索引名称

Qdrant配置

变量 默认值 描述
QDRANT_HOST localhost Qdrant服务器主机
QDRANT_PORT 6333 Qdrant gRPC端口
QDRANT_COLLECTION docs_collection 向量嵌入的集合名称

Redis配置(已准备好,尚未使用)

变量 默认值 描述
REDIS_HOST localhost Redis服务器主机(用于未来缓存)
REDIS_PORT 6379 Redis服务器端口
REDIS_ENABLED false 启用Redis缓存(当前未使用)

生成脚本标志

这 scripts/build_index.py 脚本支持以下命令行标志:

建立索引

# Index all documentation
uv run scripts/build_index.py

# Index specific technology
uv run scripts/build_index.py --tech django
uv run scripts/build_index.py --tech drf
uv run scripts/build_index.py --tech psycopg

# Force reindex (ignore checksums, rebuild everything)
uv run scripts/build_index.py --force

# Index specific paths
uv run scripts/build_index.py --paths docs/django-6.0/ref docs/django-6.0/topics

清除索引

# Clear ALL documentation from index
uv run scripts/build_index.py --clear

# Clear specific technology only (keeps others intact)
uv run scripts/build_index.py --tech django --clear
uv run scripts/build_index.py --tech drf --clear

警告: --clear 没有 --tech 将删除所有索引文档。使用 --tech 标记以选择性清除。

联合作战

# Clear Django docs and reindex them
uv run scripts/build_index.py --tech django --clear
uv run scripts/build_index.py --tech django

# Force rebuild specific technology
uv run scripts/build_index.py --tech drf --force

配置示例

概念查询的语义权重高:

export BM25_WEIGHT=0.3
export SEMANTIC_WEIGHT=0.7
uv run server.py

BM25权重高,可实现精确的关键字匹配:

export BM25_WEIGHT=0.7
export SEMANTIC_WEIGHT=0.3
uv run server.py

自定义嵌入模型:

export EMBEDDING_MODEL="sentence-transformers/all-mpnet-base-v2"
uv run server.py

大块以获得更多上下文:

export CHUNK_SIZE_TOKENS=200
uv run scripts/build_index.py --force  # Rebuild with new chunk size

自定义Elasticsearch实例:

export ES_HOST="http://192.168.1.100:9200"
export ES_INDEX_NAME="my_docs"
python server.py

查询示例

根据以下常见的开发人员查询调整混合权重:

"How does Django auth middleware work?"
"What is the DRF request lifecycle?"
"Configure Nuxt SSR"
"Redis cache invalidation flow"
"psycopg transaction patterns"
"Django model field options"
"DRF serializer validation"

建筑

┌─────────────────┐
│   MCP Client    │  (Claude, VSCode, etc)
│  or HTTP Client │
└────────┬────────┘
         │
         v
┌─────────────────┐
│   server.py     │  FastMCP + FastAPI
│   (Tools/API)   │  • Lifespan context manager
│                 │  • Context[ServerSession, AppContext]
│                 │  • Structured Pydantic outputs
└────────┬────────┘
         │
         v
┌────────────────────────────────────────┐
│         search_engine.py                 │
│         (IndexManager)                   │
│  ┌──────────────┐   ┌───────────────┐ │
│  │ Elasticsearch │   │    Qdrant     │ │
│  │  BM25 Index  │   │ Vector Index │ │
│  │  (Inverted)  │   │    (HNSW)    │ │
│  └──────────────┘   └───────────────┘ │
│           │                  │          │
│           v                  v          │
│      ┌────────────────────────┐   │
│      │   FastEmbed (CPU)      │   │
│      │ bge-small-en-v1.5    │   │
│      │    384-dim vectors    │   │
│      └────────────────────────┘   │
└────────────────────────────────────────┘
         │
         v
    ┌─────────┐
    │ .index/ │  (Metadata cache)
    └─────────┘

实施说明:

  • 寿命管理:IndexManager通过初始化一次 @asynccontextmanager 并注入到工具上下文中
  • 上下文模式:工具接收 Context[ServerSession, AppContext] 用于访问共享状态
  • 结构化输出:所有结果均作为经过验证的Pydantic模型返回
  • 安全:仅本地主机绑定,用于监控的查询日志记录

文档来源

当前文档集 docs/:

  • Django 6.0 (约300个文件):模型、视图、ORM、中间件、管理
  • Django REST框架3.16.1 (约30个文件):序列化程序、视图、身份验证
  • 心理学第3.3.1页 (约20个文件):PostgreSQL适配器,连接池

支持: .md, .txt, .rst, .py, .json, .yaml, .yml, .html

演出

初始索引

  • 350-400个文件→ 2-5 分钟
  • 生成嵌入(384维FastEmbed,CPU优化)
  • 构建Elasticsearch BM25倒排索引+Qdrant HNSW向量索引
  • 内存使用情况: 嵌入生成时峰值约500MB-1GB
  • 磁盘使用情况: ~50-100MB .index/ 缓存+Qdrant向量

增量更新

系统使用校验和来检测文件更改,并跳过对未更改文件的重新索引:

# First run: indexes all files
uv run scripts/build_index.py
# Processing 350 files...

# Second run: skips unchanged files
uv run scripts/build_index.py
# Skipped 350 files (unchanged)

# After modifying one file
uv run scripts/build_index.py
# Skipped 349 files (unchanged)
# Processing 1 file...

校验和行为:

  • 每10个文件增量保存一次,以防止崩溃时丢失
  • 存储在 .index/checksums.json
  • 使用 --force 标记以绕过校验和验证并重建所有内容

搜索延迟

  • 语义搜索(Qdrant): ~50-100ms(HNSW索引查找)
  • BM25搜索(Elasticsearch): ~10-20ms(倒置指数)
  • 混合融合: ~5ms(分数归一化和组合)
  • 每次查询总计: ~100-150ms

已实施优化

  1. 批量处理:

    • 嵌入:每批50个块(防止OOM)
    • Qdrant上传:每批100分
    • Elasticsearch索引:每批500个文档
  2. 并行操作:

    • 单独的读/写锁允许在索引期间进行搜索
    • 所有网络操作的异步I/O
  3. 资源管理:

    • 缓存写入前的磁盘空间检查(最小1GB)
    • 批之间的速率限制(0.1秒延迟)
    • 增量检查点保存
  4. 内存效率:

    • 流式文件处理
    • 处理后批量清理
    • 无需GPU(FastEmbed CPU优化)

系统要求

  • CPU: 现代多核(建议使用4+核)
  • 猛撞: 最低2GB,建议4GB
  • 磁盘: 500MB可用于索引和缓存
  • 服务:
    • 本地主机上的Elasticsearch 8.x+:9200
    • 本地主机上的Qdrant 1.x+:6333
  • GPU: 不需要(FastEmbed使用CPU)

故障排除

Docker服务未运行

症状: Elasticsearch或Qdrant的连接错误

解决方案:

# Check service status
docker ps

# Start services if not running
docker-compose up -d

# Check logs
docker-compose logs elasticsearch
docker-compose logs qdrant

索引为空

症状: get_index_stats 显示0个块

解决:

# Check status via REST API
curl http://localhost:8000/status

# Rebuild index manually
uv run scripts/build_index.py

# Or via Python
uv run python -c "
import asyncio
from pathlib import Path
from search_engine import IndexManager

asyncio.run(IndexManager().index_documents([Path('docs')]))
"

构建脚本崩溃(退出143/SIGTERM)

症状: 索引过程终止于 Exit Code 143 或内存不足

原因:

  • 系统OOM(内存不足)杀手终止进程
  • RAM不足,无法进行大批量处理

解决:

  1. 减少批量大小 (已在当前版本中优化):

    • 嵌入:每批50块
    • Qdrant:每批100分
    • Elasticsearch:每批500个文档
  2. 关闭内存密集型应用程序 索引期间

  3. 监控内存使用情况:

    # Watch memory in real-time
    watch -n 1 free -h
    
    # Monitor indexing process
    ps aux | grep build_index
    
  4. 小批量指标:

    # Index one technology at a time
    uv run scripts/build_index.py --tech django
    uv run scripts/build_index.py --tech drf
    uv run scripts/build_index.py --tech psycopg
    

语义搜索返回所有分数=0

症状: 搜索结果显示 semantic_score: 0.0 对于所有结果

原因:

  • Qdrant向量未正确索引
  • MCP客户端缓存陈旧
  • 重新索引后服务器未重新启动

解决:

  1. 重新启动服务器:

    # Stop server (Ctrl+C)
    # Restart
    uv run server.py
    
  2. 验证Qdrant是否有向量:

    curl http://localhost:6333/collections/docs_collection
    # Check "points_count" > 0 and "vectors_count" > 0
    
  3. 直接测试语义搜索:

    uv run python -c "
    

导入异步 从search_engine导入IndexManager

异步定义测试(): manager=索引管理器() wait manager初始化() results=等待管理器.搜索('Django身份验证',top_k=5) 对于结果中的r: 打印(f’语义:{r.Semantic_score},BM25:{r.BM25_score}’)

asyncio.run(test()) "


4. **Rebuild Qdrant collection:**
```bash
uv run scripts/build_index.py --clear
uv run scripts/build_index.py

意外清除所有文件

症状: 用过的 --tech X --clear 预计只清除技术X,但所有文档均已删除

原因: 旧版本的脚本有不正确的清晰逻辑

当前行为(固定):

  • --clear 单独:清除所有文档
  • --tech django --clear:仅清除Django文档,保留DRF和Psycopg
  • --tech drf --clear:仅清除DRF文档

恢复:

# Reindex everything
uv run scripts/build_index.py

# Or reindex specific techs
uv run scripts/build_index.py --tech django
uv run scripts/build_index.py --tech drf
uv run scripts/build_index.py --tech psycopg

搜索质量差

症状: 结果与预期不符或缺少相关文件

解决:

  1. 检查查询日志 在 .index/search_queries.log 查看查询的处理方式

  2. 调整混合搜索权重:

    • 关键字密集型查询 (确切术语、函数名称):增加 BM25_WEIGHT
      export BM25_WEIGHT=0.7
      export SEMANTIC_WEIGHT=0.3
      
    • 概念查询 (如何,最佳做法):增加 SEMANTIC_WEIGHT
      export BM25_WEIGHT=0.3
      export SEMANTIC_WEIGHT=0.7
      
  3. 尝试不同的嵌入模型:

    # Larger, more accurate (slower)
    export EMBEDDING_MODEL="sentence-transformers/all-mpnet-base-v2"
    
    # Smaller, faster (less accurate)
    export EMBEDDING_MODEL="sentence-transformers/all-MiniLM-L6-v2"
    
    # Rebuild with new model
    uv run scripts/build_index.py --force
    
  4. 增加结果计数:

    search(query="...", top_k=20)  # Default is 10
    

磁盘空间问题

症状: 索引构建失败,出现磁盘写入错误

原因: 磁盘空间不足,无法写入元数据缓存

解决方案:

  • 在写入缓存之前,系统会检查至少1GB的可用空间
  • 如果需要,请释放磁盘空间:
    df -h  # Check available space
    docker system prune  # Clean Docker artifacts
    

嵌入时间过长

解决:

  • FastEmbed已经进行了CPU优化和批处理(一次50个块)
  • 尝试较小的型号: export EMBEDDING_MODEL="sentence-transformers/all-MiniLM-L6-v2"
  • 验证Elasticsearch和Qdrant是否正在运行: docker ps
  • 检查CPU使用率是否没有瓶颈: top 或 htop

缺少等待错误

症状: RuntimeWarning: coroutine 'get_stats' was never awaited

原因: 异步函数调用时没有 await

解决方案: 已在当前版本中修复。所有异步调用都已正确等待:

# Correct (current version)
stats = await manager.get_stats()

# Incorrect (would cause warning)
stats = manager.get_stats()

发展

本地缓存目录(.index/)

这 .index/ 目录包含本地元数据缓存,应该 非 致力于Git:

.index/
├── checksums.json          # File checksums for incremental indexing
├── metadata_cache.pkl      # Pickled chunk metadata
└── search_queries.log      # Query history for debugging

Git配置:

# Already in .gitignore
echo ".index/" >> .gitignore

为什么不承诺?

  • 包含特定于机器的路径
  • 大型二进制pickle文件
  • 首次运行时自动重建
  • Elasticsearch和Qdrant保存实际索引

在新机器上重建:

# Clone repo
git clone <repo-url>
cd docs-mcp-server

# Start services
docker-compose up -d

# Build index (will create .index/ automatically)
uv run scripts/build_index.py

运行测试

# Quick sanity check (30 seconds)
uv run tests/smoke_test.py

# Comprehensive test suite (8-10 minutes)
uv run tests/run_comprehensive_tests.py

# Individual test suites
uv run tests/test_search_engine.py      # Core functionality
uv run tests/test_score_accuracy.py     # Score validation
uv run tests/test_integration.py        # Integration tests
uv run tests/test_mcp_accuracy.py       # 50+ query tests

# See tests/README.md for full documentation

测试覆盖范围:

  • ✅ 核心搜索功能(20+测试)
  • ✅ 分数准确性和阈值(15+次测试)
  • ✅ 集成工作流程(7+测试)
  • ✅ 真实世界查询(50+个查询)
  • ✅ 混合评分验证
  • ✅ 技术/部件过滤器
  • ✅ 边缘情况和错误处理

调试日志记录

启用调试输出:

# Set environment variable
export PYTHONVERBOSE=1

# Or add to Python code
uv run python -c "import logging; logging.basicConfig(level=logging.DEBUG)"

调试语义搜索:

# Already enabled in search_engine.py
# Check logs for:
# - "Semantic search returned X results"
# - "Normalized semantic scores: [...]"
# - "Final hybrid scores: [...]"

检查查询日志

# Watch queries in real-time
tail -f .index/search_queries.log

# Analyze query patterns
grep "query=" .index/search_queries.log | head -20

测试更改

修改搜索权重后:

# Update environment
export BM25_WEIGHT=0.7
export SEMANTIC_WEIGHT=0.3

# Restart server (no reindex needed)
uv run server.py

修改块大小后:

# Update environment
export CHUNK_SIZE_TOKENS=200

# Must rebuild index with new chunks
uv run scripts/build_index.py --force

修改嵌入模型后:

# Update environment
export EMBEDDING_MODEL="sentence-transformers/all-mpnet-base-v2"

# Must rebuild with new embeddings
uv run scripts/build_index.py --force

未来的增强功能

MVP后的改进:

  • []用于top-k细化的本地重新排序器(BAAI/bge重新排序器)
  • []增量更新的文档差异
  • []自动重新索引的文件监视器
  • []版本感知过滤(Django 4 vs 5)
  • []带同义词的查询扩展
  • []使用情况分析仪表板

贡献

  1. 分叉存储库
  2. 创建要素分支
  3. 进行更改
  4. 使用MCP检查员进行测试
  5. 提交拉取请求

支持

  • GitHub问题:[报告错误或请求功能]
  • MCP文件:https://modelcontextprotocol.io/
  • 句子转换:https://www.sbert.net/

内置: