-
+
+
-
Loading…
+
Loading…
-
-
{{ emptyState.title }}
-
{{ emptyState.subtitle }}
-
-
-
-
-
-
-
-
-
-
-
-
+
+
{{ emptyState.title }}
+
{{ emptyState.subtitle }}
-
-
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
diff --git a/frontend/src/views/SearchView.vue b/frontend/src/views/SearchView.vue
new file mode 100644
index 0000000..25643c1
--- /dev/null
+++ b/frontend/src/views/SearchView.vue
@@ -0,0 +1,67 @@
+
+
+
+
+
+ Results for {{ query }}
+ Type in the search box to find your notes.
+
+
+
Searching…
+
+
+
No matches
+
Nothing found for "{{ query }}".
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/thoughtsync/notes.py b/src/thoughtsync/notes.py
index c2993e9..4da07e9 100644
--- a/src/thoughtsync/notes.py
+++ b/src/thoughtsync/notes.py
@@ -4,7 +4,7 @@ import uuid
from datetime import datetime, timezone
from quart import Blueprint, g, jsonify, request
-from sqlalchemy import delete, select
+from sqlalchemy import delete, func, literal_column, select
from .acl import visible_to_user
from .auth import login_required
@@ -93,6 +93,36 @@ async def list_notes():
return jsonify({"notes": out})
+@bp.get("/search")
+@login_required
+async def search_notes():
+ q = (request.args.get("q") or "").strip()
+ if not q:
+ return jsonify({"notes": []})
+ async with session_scope() as db:
+ tsquery = func.websearch_to_tsquery("english", q)
+ # search_vector is a generated column (migration 0005), not mapped on the ORM.
+ search_col = literal_column("notes.search_vector")
+ stmt = (
+ select(Note)
+ .where(
+ visible_to_user("note", Note.owner_id, Note.id, g.user_id),
+ Note.deleted_at.is_(None),
+ search_col.op("@@")(tsquery),
+ )
+ .order_by(func.ts_rank(search_col, tsquery).desc(), Note.updated_at.desc())
+ .limit(100)
+ )
+ notes = (await db.scalars(stmt)).all()
+ labels_map = await _labels_for_notes(db, [n.id for n in notes])
+ out = []
+ for n in notes:
+ data = n.serialize()
+ data["labels"] = labels_map.get(n.id, [])
+ out.append(data)
+ return jsonify({"notes": out})
+
+
@bp.post("")
@login_required
async def create_note():
diff --git a/tests/test_notes.py b/tests/test_notes.py
index 6620b9c..ded1555 100644
--- a/tests/test_notes.py
+++ b/tests/test_notes.py
@@ -50,3 +50,9 @@ async def test_notes_create_requires_auth(app):
client = app.test_client()
resp = await client.post("/api/notes", json={"body": "hi"})
assert resp.status_code == 401
+
+
+async def test_search_requires_auth(app):
+ client = app.test_client()
+ resp = await client.get("/api/notes/search?q=hello")
+ assert resp.status_code == 401