diff --git a/backend/app/database/migrations.py b/backend/app/database/migrations.py index 2009ab4..c41f8e0 100644 --- a/backend/app/database/migrations.py +++ b/backend/app/database/migrations.py @@ -120,6 +120,13 @@ MIGRATIONS: list[str] = [ PRIMARY KEY(job_id, revision, options_hash) ); """, + # v5: application-owned search history, shared by web and desktop clients. + """ + CREATE TABLE IF NOT EXISTS search_history ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + query TEXT NOT NULL UNIQUE + ); + """, ] diff --git a/backend/app/routes.py b/backend/app/routes.py index 3b8da63..642e97a 100644 --- a/backend/app/routes.py +++ b/backend/app/routes.py @@ -303,9 +303,24 @@ async def rename_note(note_id: str, request: NoteRenameRequest) -> Note: # Retrieval and chat @router.post("/search", response_model=SearchResponse, tags=["Search"]) async def search_notes(request: SearchRequest) -> SearchResponse: + from app.services import search_history + search_history.record(request.query) return await engine.search(request) +@router.get("/search/history", tags=["Search"]) +async def get_search_history() -> dict[str, list[str]]: + from app.services import search_history + return {"queries": search_history.list_queries()} + + +@router.delete("/search/history", tags=["Search"]) +async def clear_search_history() -> dict[str, list[str]]: + from app.services import search_history + search_history.clear() + return {"queries": []} + + @router.post( "/chat", response_class=StreamingResponse, diff --git a/backend/app/services/search_history.py b/backend/app/services/search_history.py new file mode 100644 index 0000000..34fbb99 --- /dev/null +++ b/backend/app/services/search_history.py @@ -0,0 +1,23 @@ +from contextlib import closing + +from app.database.db import connect, transaction + + +def list_queries(): + with closing(connect()) as conn: + return [row['query'] for row in conn.execute('SELECT query FROM search_history ORDER BY id DESC LIMIT 10')] + + +def record(query: str): + query = query.strip() + if not query: + return + with closing(connect()) as conn, transaction(conn): + conn.execute('DELETE FROM search_history WHERE query=?', (query,)) + conn.execute('INSERT INTO search_history(query) VALUES (?)', (query,)) + conn.execute('DELETE FROM search_history WHERE id NOT IN (SELECT id FROM search_history ORDER BY id DESC LIMIT 10)') + + +def clear(): + with closing(connect()) as conn, transaction(conn): + conn.execute('DELETE FROM search_history') diff --git a/backend/tests/test_search_history.py b/backend/tests/test_search_history.py new file mode 100644 index 0000000..ea68f68 --- /dev/null +++ b/backend/tests/test_search_history.py @@ -0,0 +1,22 @@ +from fastapi.testclient import TestClient + +from app.main import app +from app.services import search_history + + +def test_history_survives_new_clients_and_clear(): + with TestClient(app) as client: + for query in ['first', 'second', ' first ']: + assert client.post('/api/search', json={'query': query, 'mode': 'fts'}).status_code == 200 + assert client.get('/api/search/history').json() == {'queries': ['first', 'second']} + with TestClient(app) as client: + assert client.get('/api/search/history').json() == {'queries': ['first', 'second']} + assert client.delete('/api/search/history').json() == {'queries': []} + assert search_history.list_queries() == [] + + +def test_history_is_bounded_and_blank_queries_are_ignored(): + for number in range(12): + search_history.record(str(number)) + search_history.record(' ') + assert search_history.list_queries() == [str(number) for number in range(11, 1, -1)] diff --git a/docs/contracts/后端接口契约-开发版.md b/docs/contracts/后端接口契约-开发版.md index b2a3ed8..fc18365 100644 --- a/docs/contracts/后端接口契约-开发版.md +++ b/docs/contracts/后端接口契约-开发版.md @@ -32,6 +32,8 @@ | POST | `/api/notes/{note_id}/move` | 移动笔记 | | POST | `/api/notes/{note_id}/rename` | 重命名笔记文件并保留 Note/Block 身份 | | POST | `/api/search` | FTS、Vector 或 Hybrid 检索 | +| GET | `/api/search/history` | 读取当前应用数据库最近 10 条去重搜索记录 | +| DELETE | `/api/search/history` | 清空当前应用数据库的搜索记录 | ### Workspace diff --git a/frontend/src/features/search/SearchView.vue b/frontend/src/features/search/SearchView.vue index 7241f61..e7ca2e5 100644 --- a/frontend/src/features/search/SearchView.vue +++ b/frontend/src/features/search/SearchView.vue @@ -1,5 +1,5 @@