为什么需要私有 AI 搜索引擎?
Google 搜索很好用,但当你需要搜索自己的笔记、技术文档、代码库或者本地文件时,传统搜索引擎就无能为力了。私有 AI 搜索引擎的核心优势:
| 能力 | 传统搜索 | AI 语义搜索 |
|---|---|---|
| 关键词匹配 | ✅ 精确匹配 | ✅ 模糊匹配 + 同义词 |
| 理解意图 | ❌ 只认关键词 | ✅ 理解自然语言 |
| 跨文档推理 | ❌ 单文档 | ✅ 聚合多文档回答 |
| 隐私保护 | ❌ 数据上传云端 | ✅ 纯本地运行 |
| 个人化 | ❌ 通用结果 | ✅ 针对你的内容 |
2026 年,开源生态已经成熟:Qdrant(向量数据库)做存储和检索,Ollama 运行 LLM 做理解与生成,sentence-transformers 做文本向量化。三者结合,一台 4GB 内存的 VPS 就能跑起来。
架构总览
┌──────────────┐ ┌──────────────────┐ ┌──────────────┐
│ 文档/笔记 │ ──► │ Embedding 服务 │ ──► │ Qdrant │
│ Markdown/PDF │ │ (BGE-M3 等模型) │ │ 向量数据库 │
└──────────────┘ └──────────────────┘ └──────────────┘
│
┌──────────────┐ ┌──────────────────┐ │
│ 用户查询 │ ──► │ Query Pipeline │ ──────────┘
│ 自然语言 │ │ + LLM 生成回答 │
└──────────────┘ └──────────────────┘
工作流程:
- 索引阶段:文档 → 切片 → Embedding 向量 → 存入 Qdrant
- 检索阶段:用户问题 → Embedding 向量 → 语义搜索 → 召回最相关片段
- 生成阶段:相关片段 + 问题 → LLM → 自然语言回答 + 引用来源
第一步:部署 Qdrant 向量数据库
Qdrant 是一个用 Rust 编写的高性能向量搜索引擎,支持过滤、分片和多种相似度算法。Docker 一键部署:
# 创建持久化数据目录
mkdir -p ~/qdrant_storage
# 运行 Qdrant
docker run -d --name qdrant \
-p 6333:6333 \
-p 6334:6334 \
-v ~/qdrant_storage:/qdrant/storage \
qdrant/qdrant
# 验证部署
curl http://localhost:6333/health | python3 -m json.tool
# 输出: {"status": "ok"}
端口说明:
6333— HTTP API(RESTful 接口)6334— gRPC API(高性能通信)
创建 Collection(集合)
curl -X PUT http://localhost:6333/collections/my-knowledge \
-H 'Content-Type: application/json' \
-d '{
"vectors": {
"size": 1024,
"distance": "Cosine"
}
}'
size=1024 对应 BGE-M3 等模型的输出维度。Cosine 距离适合语义相似度计算。
第二步:部署 Embedding 服务
Embedding 模型 将文本转换成向量。BGE-M3 是 BAAI 开源的通用多语言 Embedding 模型,支持中文和英文。
推荐使用 sentence-transformers 在本地运行:
# 创建 Python 虚拟环境
python3 -m venv ~/embeddings-env
source ~/embeddings-env/bin/activate
# 安装依赖
pip install sentence-transformers qdrant-client flask gunicorn
# 启动 Embedding API 服务
cat > ~/embedding_server.py << 'EOF'
from flask import Flask, request, jsonify
from sentence_transformers import SentenceTransformer
import os
app = Flask(__name__)
# 加载模型(首次会自动下载,后续缓存到本地)
model_name = os.getenv("EMBEDDING_MODEL", "BAAI/bge-m3")
model = SentenceTransformer(model_name)
@app.route("/embed", methods=["POST"])
def embed():
data = request.json
texts = data.get("texts", [])
if not texts:
return jsonify({"error": "no texts provided"}), 400
# BGE 模型需要在文本前加 instruction 前缀以便检索
if os.getenv("USE_PREFIX", "true").lower() == "true":
texts = [f"为这个句子生成表示以用于检索相关文章:{t}" for t in texts]
embeddings = model.encode(texts, normalize_embeddings=True)
return jsonify({
"embeddings": embeddings.tolist(),
"dimension": embeddings.shape[1]
})
@app.route("/health", methods=["GET"])
def health():
return jsonify({"status": "ok", "model": model_name})
if __name__ == "__main__":
port = int(os.getenv("PORT", 8001))
app.run(host="0.0.0.0", port=port, threaded=True)
EOF
# 使用 Gunicorn 在生产环境运行
pip install gunicorn
gunicorn -w 2 -b 0.0.0.0:8001 embedding_server:app &
⚠️ 内存说明:BGE-M3 模型约占用 2GB 内存。如果 VPS 内存不足(<4GB),可换用轻量模型如
all-MiniLM-L6-v2(384 维,约 500MB)。但请记得修改 Qdrant collection 的size参数。
验证 Embedding 服务
curl http://localhost:8001/embed \
-H 'Content-Type: application/json' \
-d '{"texts": ["什么是向量数据库?", "Qdrant is a vector search engine"]}'
# 返回结果示例:
# {"embeddings": [[0.012, -0.034, ...], [...]], "dimension": 1024}
第三步:文档索引与入库
现在我们需要一个索引工具,把文档切片、向量化后存入 Qdrant。以下是核心脚本:
cat > ~/index_docs.py << 'PYEOF'
#!/usr/bin/env python3
"""将本地文档索引到 Qdrant"""
import os
import glob
import uuid
import hashlib
from qdrant_client import QdrantClient
from qdrant_client.http.models import PointStruct
import requests
# 配置
QDRANT_HOST = os.getenv("QDRANT_HOST", "localhost")
QDRANT_PORT = int(os.getenv("QDRANT_PORT", 6333))
EMBEDDING_URL = os.getenv("EMBEDDING_URL", "http://localhost:8001")
COLLECTION_NAME = os.getenv("COLLECTION", "my-knowledge")
DOC_DIR = os.getenv("DOC_DIR", os.path.expanduser("~/documents"))
# 初始化 Qdrant 客户端
client = QdrantClient(host=QDRANT_HOST, port=QDRANT_PORT)
def chunk_text(text, chunk_size=512, overlap=64):
"""将文本切成有重叠的片段"""
words = list(text)
chunks = []
start = 0
while start < len(words):
end = min(start + chunk_size, len(words))
chunk_text = "".join(words[start:end])
if len(chunk_text.strip()) > 20: # 过滤过短的片段
chunks.append({
"text": chunk_text,
"start": start,
"end": end
})
start += chunk_size - overlap
return chunks
def get_embedding(texts):
"""调用 Embedding API 获取向量"""
resp = requests.post(f"{EMBEDDING_URL}/embed", json={"texts": texts})
resp.raise_for_status()
return resp.json()["embeddings"]
def index_file(filepath):
"""索引单个文件"""
print(f"📄 正在处理: {filepath}")
# 读取文件
with open(filepath, "r", encoding="utf-8", errors="ignore") as f:
content = f.read()
# 获取相对路径作为元数据
rel_path = os.path.relpath(filepath, DOC_DIR)
# 切片
chunks = chunk_text(content)
print(f" 切片数量: {len(chunks)}")
# 批量向量化
texts = [c["text"] for c in chunks]
embeddings = get_embedding(texts)
# 构造 points
points = []
for i, (chunk, emb) in enumerate(zip(chunks, embeddings)):
point_id = str(uuid.uuid5(uuid.NAMESPACE_DNS, f"{rel_path}:{i}"))
points.append(PointStruct(
id=point_id,
vector=emb,
payload={
"text": chunk["text"],
"source": rel_path,
"chunk_index": i
}
))
# 批量入库(每批 100 条)
for batch_start in range(0, len(points), 100):
batch = points[batch_start:batch_start + 100]
client.upsert(
collection_name=COLLECTION_NAME,
points=batch
)
print(f" ✅ 入库 {len(points)} 个片段")
return len(points)
def main():
# 检查 Qdrant 连接
try:
client.get_collection(COLLECTION_NAME)
print(f"✅ 已连接到 Qdrant collection: {COLLECTION_NAME}")
except Exception:
print(f"❌ 无法连接 Qdrant 或 collection 不存在")
return
# 扫描文档目录
total = 0
for ext in ["*.md", "*.txt", "*.rst", "*.html"]:
files = glob.glob(os.path.join(DOC_DIR, "**", ext), recursive=True)
for f in files:
total += index_file(f)
print(f"\n🎉 完成!共索引 {total} 个文本片段")
if __name__ == "__main__":
main()
PYEOF
# 运行索引(假设 ~/documents 下有你需要索引的笔记)
mkdir -p ~/documents
# 放入一些示例文档...
python3 ~/index_docs.py
第四步:智能检索与 AI 问答
有了索引后,我们可以构建搜索和问答功能:
cat > ~/search_engine.py << 'PYEOF'
#!/usr/bin/env python3
"""AI 搜索引擎:语义搜索 + LLM 生成回答"""
import os
import requests
from qdrant_client import QdrantClient
# 配置
QDRANT_HOST = os.getenv("QDRANT_HOST", "localhost")
QDRANT_PORT = int(os.getenv("QDRANT_PORT", 6333))
EMBEDDING_URL = os.getenv("EMBEDDING_URL", "http://localhost:8001")
OLLAMA_URL = os.getenv("OLLAMA_URL", "http://localhost:11434")
COLLECTION_NAME = os.getenv("COLLECTION", "my-knowledge")
LLM_MODEL = os.getenv("LLM_MODEL", "qwen2.5:7b")
TOP_K = int(os.getenv("TOP_K", "5"))
client = QdrantClient(host=QDRANT_HOST, port=QDRANT_PORT)
def get_query_embedding(query):
"""将查询转为向量"""
resp = requests.post(f"{EMBEDDING_URL}/embed", json={
"texts": [query]
})
resp.raise_for_status()
return resp.json()["embeddings"][0]
def search(query, top_k=TOP_K):
"""语义搜索,返回最相关的文档片段"""
query_vector = get_query_embedding(query)
results = client.search(
collection_name=COLLECTION_NAME,
query_vector=query_vector,
limit=top_k,
with_payload=True
)
return results
def generate_answer(query, search_results):
"""用 LLM 基于搜索结果生成回答"""
# 构建上下文
context_parts = []
for i, res in enumerate(search_results, 1):
source = res.payload.get("source", "unknown")
text = res.payload.get("text", "")
context_parts.append(f"[片段 {i}] 来源: {source}\n{text}\n")
context = "\n---\n".join(context_parts)
# 构造 prompt
prompt = f"""你是一个基于个人知识库的 AI 助手。请根据以下参考文档回答用户问题。
参考文档:
{context}
用户问题:{query}
要求:
1. 仅基于参考文档内容回答
2. 如果文档中没有相关信息,请如实告知
3. 在回答中引用相关文档来源
4. 用中文回答
回答:"""
# 调用 Ollama
resp = requests.post(f"{OLLAMA_URL}/api/generate", json={
"model": LLM_MODEL,
"prompt": prompt,
"stream": False,
"options": {
"temperature": 0.3,
"top_k": 40,
"num_ctx": 4096
}
})
resp.raise_for_status()
return resp.json()["response"]
def interactive_mode():
"""交互式搜索"""
print("🔍 AI 搜索引擎已启动")
print(f" 模型: {LLM_MODEL} | 召回: TOP-{TOP_K}")
print(" 输入 'quit' 退出\n")
while True:
query = input("Q: ").strip()
if query.lower() in ("quit", "exit", "q"):
break
if not query:
continue
print(f"\n 🔎 搜索相关文档...")
results = search(query)
print(f" 📄 找到 {len(results)} 个相关片段:")
for r in results:
score = r.score
source = r.payload.get("source", "?")
text_preview = r.payload.get("text", "")[:80].replace("\n", " ")
print(f" [{score:.3f}] {source}: {text_preview}...")
print(f"\n 🤖 正在生成回答...")
answer = generate_answer(query, results)
print(f"\n 📝 回答:\n{answer}\n")
if __name__ == "__main__":
interactive_mode()
PYEOF
测试搜索
# 确保 Ollama 已运行(如果还没装)
docker run -d --name ollama -p 11434:11434 \
-v ollama:/root/.ollama ollama/ollama
docker exec ollama ollama pull qwen2.5:7b
# 运行交互式搜索引擎
python3 ~/search_engine.py
示例搜索:
Q: VPS 上部署 Docker 需要注意哪些安全问题?
🔎 搜索相关文档...
📄 找到 5 个相关片段:
[0.892] docker-security.md: Docker 安全最佳实践——不要以 root 运行容器...
[0.765] vps-setup.md: VPS 基础配置包括防火墙、SSH 密钥登录...
[0.621] docker-compose.md: 使用非 root 用户运行 Docker 守护进程...
🤖 正在生成回答...
📝 回答:
在 VPS 上部署 Docker 需要注意以下安全问题(参考文档):
1. **不要以 root 运行容器**(来源: docker-security.md):始终使用非 root 用户运行容器进程,可以通过 Dockerfile 中的 USER 指令实现。
2. **配置防火墙**(来源: vps-setup.md):仅开放必要端口,使用 ufw 或 iptables 限制访问。
3. **使用安全的基础镜像**:选择官方或经过安全审计的镜像。
4. **定期更新**:保持 Docker Engine 和镜像的更新。
进阶优化
1. 使用 Web UI 替代命令行
推荐使用 Quivr 或 DocsGPT 作为前段界面,它们支持可视化文档管理和搜索:
# Quivr — 开源的 AI 知识库
docker run -d --name quivr \
-p 5050:5050 \
-e SUPABASE_URL=... \
-e OPENAI_API_KEY=... \
stangirard/quivr
2. 定时自动索引
用 cron 定期更新索引,保持知识库新鲜:
# 每 6 小时重新索引一次
echo "0 */6 * * * cd ~ && python3 ~/index_docs.py >> ~/index_log.txt 2>&1" | crontab -
3. 多格式文档支持
用 unstructured 或 markitdown 支持 PDF、Word、Excel 等格式:
pip install unstructured markitdown
python3 -c "
from markitdown import MarkItDown
md = MarkItDown()
result = md.convert('report.pdf')
print(result.text_content)
"
4. 混合搜索(Hybrid Search)
Qdrant 支持全文搜索 + 向量搜索的混合模式,提升精确关键词匹配能力:
from qdrant_client.http.models import Filter, FieldCondition, MatchText
# 创建全文索引
client.create_payload_index(
collection_name=COLLECTION_NAME,
field_name="text",
field_type="text"
)
# 混合搜索
results = client.search(
collection_name=COLLECTION_NAME,
query_vector=query_vector,
query_filter=Filter(
must=[
FieldCondition(
key="text",
match=MatchText(text="Docker安全")
)
]
),
limit=10
)
性能与资源
| 组件 | 内存 | 磁盘 | 说明 |
|---|---|---|---|
| Qdrant | ~200MB | 取决于文档量 | 极省资源 |
| Embedding 服务 (BGE-M3) | ~2GB | ~2GB (模型文件) | 可用轻量模型替代 |
| Ollama (qwen2.5:7b) | ~8GB | ~4GB | 可用 3B 模型替代 |
| 最低配置合计 | ~3.5GB | ~8GB | 使用轻量模型 |
💡 省资源方案:用
all-MiniLM-L6-v2(384 维)替代 BGE-M3,用qwen2.5:3b替代 7B 模型,最低 2GB 内存也能跑。
总结
通过本教程,你已经学会了在 VPS 上搭建一套完整的私有 AI 搜索引擎:
- Qdrant — 高性能向量数据库,负责存储和语义检索
- Embedding 服务 — 将文本和查询转化为向量
- Ollama + LLM — 基于检索结果生成自然语言回答
- 索引管线 — 自动切片、向量化、入库
这套系统的核心价值在于:你的数据永远在你的服务器上。所有处理都在本地完成,没有数据泄露风险,没有 API 费用,还能针对你的内容进行深度定制。
推荐阅读
🔗 相关资源: