










在之前的文章中,我们完成了 SQLServer 2025 向量数据库的搭建和基本配置。本文将深入介绍服务层和前端部分的三个核心模块:Embedding 生成、数据入库流程以及前端交互关键代码,完成一个基于SQLServer 2025的向量数据库的RAG系统流程。
Embedding 是将自然语言转换为数值向量的关键步骤。我们使用 Ollama 本地部署的 nomic-embed-text 模型来完成这一任务。
import httpx
import numpy as np
from typing import List
from vector_config import VectorDBConfig
class OllamaEmbeddingService:
def __init__(self):
self.base_url = VectorDBConfig.OLLAMA_BASE_URL
self.model = VectorDBConfig.OLLAMA_EMBEDDING_MODEL
self.dimension = VectorDBConfig.VECTOR_DIMENSION
async def get_embedding(self, text: str) -> List[float]:
async with httpx.AsyncClient(timeout=60.0) as client:
response = await client.post(
f"{self.base_url}/api/embeddings",
json={"model": self.model, "prompt": text}
)
response.raise_for_status()
data = response.json()
return data.get("embedding", [])
@staticmethod
def chunk_text(text: str, chunk_size: int = 500, chunk_overlap: int = 50) -> List[str]:
if len(text) <= chunk_size:
return [text]
chunks = []
start = 0
while start < len(text):
end = start + chunk_size
chunk = text[start:end]
if chunk.strip():
chunks.append(chunk)
start = end - chunk_overlap
return chunks
@staticmethod
def compute_hash(text: str) -> str:
import hashlib
return hashlib.md5(text.encode()).hexdigest()
embedding_service = OllamaEmbeddingService()
异步 HTTP 请求(httpx.AsyncClient)
httpx 库而非 requests,因为它原生支持异步操作async with 确保请求完成后正确关闭连接,避免资源泄漏文本分块策略(chunk_text)
chunk_overlap 参数实现块间重叠,尽量保留上下文连贯性文本向量化(get_embedding)
内容去重(compute_hash)
数据入库是连接前端和后端的关键环节,涉及文档分块、向量化和持久化存储。
from fastapi import APIRouter, HTTPException
from typing import List
from rag_models import DocumentCreate, DocumentResponse, IngestRequest, IngestResponse, TextChunkCreate
from vector_dao import DocumentDAO, TextChunkDAO, VectorDAO
from embedding_service import embedding_service
router = APIRouter(prefix="/api/rag/documents", tags=["rag-documents"])
@router.post("/ingest", response_model=IngestResponse)
async def ingest_document(ingest_req: IngestRequest):
# 1. 创建文档记录
doc = DocumentCreate(
title=ingest_req.title,
content=ingest_req.content,
source=ingest_req.source
)
doc_id = DocumentDAO.create_document(doc)
if not doc_id:
raise HTTPException(status_code=400, detail="Failed to create document")
# 2. 文本分块
chunks = embedding_service.chunk_text(
ingest_req.content,
chunk_size=ingest_req.chunk_size,
chunk_overlap=ingest_req.chunk_overlap
)
chunks_created = 0
vectors_created = 0
# 3. 批量处理每个块
for i, chunk_text in enumerate(chunks):
chunk_hash = embedding_service.compute_hash(chunk_text)
chunk_id = TextChunkDAO.create_chunk(TextChunkCreate(
document_id=doc_id,
chunk_index=i,
chunk_text=chunk_text,
chunk_hash=chunk_hash
))
if chunk_id:
chunks_created += 1
# 4. 生成 Embedding
embedding = await embedding_service.get_embedding(chunk_text)
vector_id = VectorDAO.create_vector(chunk_id, embedding)
if vector_id:
vectors_created += 1
return IngestResponse(
document_id=doc_id,
chunks_created=chunks_created,
vectors_created=vectors_created
)
事务处理设计
数据模型三层架构
异步 vs 同步混合使用
get_embedding 是异步的(网络 IO)<script setup>
import { ref } from 'vue'
import { ElMessage } from 'element-plus'
import api from '../services/api'
const queryText = ref('')
const topK = ref(5)
const minScore = ref(0.0)
const searching = ref(false)
const searched = ref(false)
const searchResults = ref([])
const total = ref(0)
const doSearch = async () => {
if (!queryText.value.trim()) {
ElMessage.warning('请输入检索内容')
return
}
searching.value = true
searched.value = true
try {
const response = await api.searchDocuments(queryText.value, topK.value, minScore.value)
searchResults.value = response.results || []
total.value = response.total || 0
} catch (error) {
ElMessage.error('检索失败')
searchResults.value = []
total.value = 0
} finally {
searching.value = false
}
}
</script>
响应式状态管理
ref 管理状态searching 状态控制按钮加载动画,防止重复点击searched 状态区分初始状态和搜索后状态错误处理模式
try-catch-finally 经典模式:
try:执行核心逻辑catch:处理异常,清空结果finally:无论成功失败都重置 loading 状态用户体验优化
ElMessage 显示操作结果<script setup>
import { ref, onMounted, watch } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import api from '../services/api'
const props = defineProps({
refreshTrigger: Number
})
const emit = defineEmits(['document-deleted'])
const docForm = ref({
title: '',
content: '',
source: '',
chunk_size: 500,
chunk_overlap: 50
})
const addDocument = async () => {
if (!docForm.value.title || !docForm.value.content) {
ElMessage.warning('请填写标题和内容')
return
}
ingesting.value = true
try {
await api.ingestDocument(docForm.value)
ElMessage.success('文档导入成功')
showAddDialog.value = false
docForm.value = { title: '', content: '', source: '', chunk_size: 500, chunk_overlap: 50 }
loadDocuments()
} catch (error) {
ElMessage.error('文档导入失败')
} finally {
ingesting.value = false
}
}
watch(() => props.refreshTrigger, loadDocuments)
onMounted(loadDocuments)
</script>
父子组件通信
defineProps 接收父组件传递的 refreshTriggerdefineEmits 定义事件,通知父组件刷新数据watch 监听触发器变化,自动重新加载文档列表表单重置机制
生命周期钩子
onMounted:组件挂载时自动加载数据import axios from 'axios'
const api = axios.create({
baseURL: '/api',
timeout: 10000
})
api.interceptors.response.use(
response => response.data,
error => {
console.error('API Error:', error)
return Promise.reject(error)
}
)
export default {
searchDocuments(queryText, topK = 5, minScore = 0.0) {
return api.post('/rag/search/', {
query_text: queryText,
top_k: topK,
min_score: minScore
})
},
ingestDocument(ingestData) {
return api.post('/rag/documents/ingest', ingestData)
},
getDocuments(limit = 100, offset = 0) {
const params = new URLSearchParams({ limit, offset })
return api.get(`/rag/documents/?${params}`)
}
}
Axios 实例化
baseURL: '/api' 配合 Vite 代理,解决跨域问题响应拦截器
response.data.data,代码更简洁函数参数设计
URLSearchParams 构建查询字符串,避免手动拼接命名约定
queryText)query_text)文档导入流程
前端表单 → api.ingestDocument() → 后端 /ingest 接口
→ DocumentDAO.create_document() → TextChunkDAO.create_chunk()
→ embedding_service.get_embedding() → VectorDAO.create_vector()
向量检索流程
前端搜索框 → api.searchDocuments() → 后端 /search 接口
→ embedding_service.get_embedding() → VectorDAO.vector_search()
→ VECTOR_SEARCH SQL → 返回结果
为了掩饰核心功能,简化周边功能,所以暂时没有封装文档上传的功能,文章的内容可以通过UI进行输入,系统会自动将其分块并且转换为向量并存储到数据库中。



此篇只讲述了服务层和前端关键处的几段代码,完整的项目代码可以从以下站点获取:
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。