Update.
This commit is contained in:
parent
1614776afa
commit
08b3600836
109
main.py
109
main.py
@ -110,44 +110,17 @@ async def init_search_index():
|
||||
""")
|
||||
db.close()
|
||||
|
||||
@app.get("/api/search")
|
||||
async def search_notes():
|
||||
q = request.args.get("q", "")
|
||||
tag = request.args.get("tag", "")
|
||||
async with db_session() as db:
|
||||
# Step 1: full‑text match → candidate note IDs
|
||||
fts_rows = list(db.query("SELECT note_id FROM notes_fts WHERE notes_fts MATCH :query", query=q))
|
||||
note_ids = {r["note_id"] for r in fts_rows}
|
||||
if not note_ids:
|
||||
return [] # early exit – no matches
|
||||
|
||||
# Step 2: optional tag filtering (intersection)
|
||||
if tag:
|
||||
tag = [t.strip() for t in tag if t.strip()]
|
||||
if not tag:
|
||||
raise HTTPException(400, "Tag filter provided but empty after stripping")
|
||||
tagged_ids = {
|
||||
nt["note_id"]
|
||||
for t in tag
|
||||
for nt in db['note_tags'].find(tag=t)
|
||||
}
|
||||
note_ids &= tagged_ids
|
||||
|
||||
# Fetch & serialize
|
||||
rows = [db['notes'].find_one(id=nid) for nid in note_ids]
|
||||
rows.sort(key=lambda r: r["updated_at"], reverse=True)
|
||||
return [await _serialize_note(r) for r in rows if r]
|
||||
|
||||
|
||||
|
||||
async def _serialize_note(row: Dict[str, Any]) -> Dict[str, Any]:
|
||||
if not row:
|
||||
return {}
|
||||
note_id = row["id"]
|
||||
score = row.get("score")
|
||||
async with db_session() as db:
|
||||
atts = list(db['attachments'].find(note_id=note_id))
|
||||
tags = [rt["tag"] for rt in db['note_tags'].find(note_id=note_id)]
|
||||
return {
|
||||
result = {
|
||||
"id": note_id,
|
||||
"title": row.get("title", ""),
|
||||
"body": row.get("body", ""),
|
||||
@ -156,36 +129,90 @@ async def _serialize_note(row: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"attachments": atts,
|
||||
"tags": tags,
|
||||
}
|
||||
if score is not None:
|
||||
result["score"] = score
|
||||
return result
|
||||
|
||||
|
||||
|
||||
|
||||
@app.get("/api/search")
|
||||
async def search_notes(q: str = "", tag: Optional[str] = None):
|
||||
"""
|
||||
Full-text search with prefix matching and BM25 scoring.
|
||||
Optional tag filter.
|
||||
"""
|
||||
q = q.strip()
|
||||
if not q:
|
||||
return []
|
||||
# build an FTS5 prefix query: each term appended with '*'
|
||||
terms = [t for t in q.split() if t]
|
||||
fts_query = " ".join(f"{t}*" for t in terms)
|
||||
|
||||
async with db_session() as db:
|
||||
if tag:
|
||||
rows = list(db.query("""
|
||||
SELECT notes.*, bm25(notes_fts) AS score
|
||||
FROM notes_fts
|
||||
JOIN notes ON notes_fts.rowid = notes.id
|
||||
JOIN note_tags ON notes.id = note_tags.note_id
|
||||
WHERE notes_fts MATCH :q AND note_tags.tag = :tag
|
||||
GROUP BY notes.id
|
||||
ORDER BY score
|
||||
""", q=fts_query, tag=tag))
|
||||
else:
|
||||
rows = list(db.query("""
|
||||
SELECT notes.*, bm25(notes_fts) AS score
|
||||
FROM notes_fts
|
||||
JOIN notes ON notes_fts.rowid = notes.id
|
||||
WHERE notes_fts MATCH :q
|
||||
ORDER BY score
|
||||
""", q=fts_query))
|
||||
|
||||
return [await _serialize_note(r) for r in rows]
|
||||
|
||||
|
||||
@app.get("/api/notes")
|
||||
async def list_notes(tag: Optional[str] = None, search: Optional[str] = None):
|
||||
"""
|
||||
List notes. Supports:
|
||||
List notes.
|
||||
Supports:
|
||||
- ?tag=foo to filter by tag
|
||||
- ?search=term to full-text-search title+body
|
||||
- ?search=term to full-text-search title+body with prefix & scoring
|
||||
"""
|
||||
async with db_session() as db:
|
||||
if search:
|
||||
# FTS5 MATCH query
|
||||
rows = list(db.query(
|
||||
"SELECT notes.* FROM notes_fts "
|
||||
"JOIN notes ON notes_fts.rowid = notes.id "
|
||||
"WHERE notes_fts MATCH :q",
|
||||
q=search
|
||||
))
|
||||
terms = [t for t in search.split() if t]
|
||||
fts_query = " ".join(f"{t}*" for t in terms)
|
||||
if tag:
|
||||
rows = list(db.query("""
|
||||
SELECT notes.*, bm25(notes_fts) AS score
|
||||
FROM notes_fts
|
||||
JOIN notes ON notes_fts.rowid = notes.id
|
||||
JOIN note_tags ON notes.id = note_tags.note_id
|
||||
WHERE notes_fts MATCH :q AND note_tags.tag = :tag
|
||||
GROUP BY notes.id
|
||||
ORDER BY score
|
||||
""", q=fts_query, tag=tag))
|
||||
else:
|
||||
rows = list(db.query("""
|
||||
SELECT notes.*, bm25(notes_fts) AS score
|
||||
FROM notes_fts
|
||||
JOIN notes ON notes_fts.rowid = notes.id
|
||||
WHERE notes_fts MATCH :q
|
||||
ORDER BY score
|
||||
""", q=fts_query))
|
||||
elif tag:
|
||||
note_ids = [nt["note_id"] for nt in db['note_tags'].find(tag=tag)]
|
||||
rows = [db['notes'].find_one(id=nid) for nid in note_ids]
|
||||
else:
|
||||
rows = list(db['notes'].all())
|
||||
|
||||
# sort & serialize
|
||||
rows = [r for r in rows if r]
|
||||
# If no FTS scoring, sort by creation date
|
||||
if rows and "score" not in rows[0]:
|
||||
rows.sort(key=lambda r: r["created_at"], reverse=True)
|
||||
return [await _serialize_note(r) for r in rows]
|
||||
|
||||
return [await _serialize_note(r) for r in rows]
|
||||
|
||||
|
||||
@app.post("/api/notes")
|
||||
|
Loading…
Reference in New Issue
Block a user