- config.py: DATABASE_URL 默认值改为 SQLite - database.py: 移除 PostgreSQL 分支,简化为纯 SQLite - models/base.py: 移除 pgvector 导入和条件分支 - search_service.py: 移除 _search_postgres 方法 - import_service.py: 移除 pgvector 相关代码 - requirements.txt: 移除 asyncpg/alembic/pgvector 依赖 - pyproject.toml: 同步移除相关依赖 - docker-compose.yml: 移除 db 服务,- 删除 alembic.ini/Dockerfile.db/sql 目录 - README.md: 更新文档,移除 PostgreSQL 相关内容 适合 NAS 等资源受限环境的轻量级部署
99 lines
3.2 KiB
Python
99 lines
3.2 KiB
Python
"""
|
|
语义搜索服务
|
|
|
|
SQLite 数据库,使用 LIKE 关键词搜索。
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import time
|
|
from typing import List
|
|
|
|
from sqlalchemy import text
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.schemas.search import SearchRequest, SearchResult, SearchResponse
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class SearchService:
|
|
"""语义搜索服务"""
|
|
|
|
def __init__(self, db: AsyncSession):
|
|
self.db = db
|
|
|
|
async def search(self, request: SearchRequest) -> SearchResponse:
|
|
"""
|
|
执行搜索:使用 LIKE 关键词搜索
|
|
"""
|
|
start_time = time.time()
|
|
|
|
# LLM 查询扩展(可选)
|
|
expanded_queries = [request.query]
|
|
try:
|
|
from app.services.llm_service import LLMService
|
|
expanded_queries = await LLMService.expand_query(request.query)
|
|
except Exception as exc:
|
|
logger.debug("查询扩展跳过: %s", exc)
|
|
|
|
results: list[SearchResult] = []
|
|
seen_chunk_ids = set()
|
|
|
|
for q in expanded_queries:
|
|
like_pattern = f"%{q}%"
|
|
sql = text("""
|
|
SELECT
|
|
kc.id AS chunk_id,
|
|
kp.id AS page_id,
|
|
kp.title AS page_title,
|
|
kc.content,
|
|
kp.course_name,
|
|
kp.teacher_name,
|
|
kp.live_date
|
|
FROM knowledge_chunks kc
|
|
JOIN knowledge_pages kp ON kc.page_id = kp.id
|
|
WHERE kc.content LIKE :pattern
|
|
AND (:course IS NULL OR kp.course_name = :course)
|
|
AND (:teacher IS NULL OR kp.teacher_name = :teacher)
|
|
ORDER BY kc.id
|
|
LIMIT :limit
|
|
""")
|
|
params = {
|
|
"pattern": like_pattern,
|
|
"course": request.course_name,
|
|
"teacher": request.teacher_name,
|
|
"limit": request.top_k,
|
|
}
|
|
result = await self.db.execute(sql, params)
|
|
rows = result.fetchall()
|
|
|
|
for row in rows:
|
|
if row.chunk_id not in seen_chunk_ids:
|
|
seen_chunk_ids.add(row.chunk_id)
|
|
# 简单的相关性评分:关键词出现次数
|
|
score = min(1.0, row.content.lower().count(q.lower()) * 0.2 + 0.5)
|
|
results.append(SearchResult(
|
|
chunk_id=row.chunk_id,
|
|
page_id=row.page_id,
|
|
page_title=row.page_title,
|
|
content=row.content,
|
|
score=round(score, 4),
|
|
course_name=row.course_name,
|
|
teacher_name=row.teacher_name,
|
|
live_date=row.live_date,
|
|
highlight=None,
|
|
))
|
|
|
|
results.sort(key=lambda x: x.score, reverse=True)
|
|
results = results[:request.top_k]
|
|
|
|
elapsed_ms = (time.time() - start_time) * 1000
|
|
return SearchResponse(
|
|
query=request.query,
|
|
total=len(results),
|
|
results=results,
|
|
elapsed_ms=round(elapsed_ms, 2),
|
|
)
|