-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdsa_bm25_server.py
More file actions
122 lines (98 loc) · 3.56 KB
/
Copy pathdsa_bm25_server.py
File metadata and controls
122 lines (98 loc) · 3.56 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
# -*- coding: utf-8 -*-
"""
FastAPI server for querying a BM25S index (DSA / Das Schwarze Auge).
- Expects an index directory that contains the BM25S files AND a sidecar meta.jsonl
with one JSON object per document (same order or with integer 'doc_id').
- Returns RAW search results: for each hit you get {doc_index, score, meta}
Install:
pip install fastapi uvicorn bm25s pydantic
Run:
python dsa_bm25_server.py
# or: uvicorn dsa_bm25_server:app --host 0.0.0.0 --port 8022
Query (example):
curl -s -X POST "http://SERVER_OR_IP:8022/search" \
-H "Content-Type: application/json" \
-d "{\"query\":\"Horasischer Adel und Titel\",\"topk\":10,\"stopwords\":\"de\"}" | jq
"""
import os
import json
from pathlib import Path
from typing import Any, Dict, List, Optional
import bm25s # pip install bm25s
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
# -----------------------
# Config (env overridable)
# -----------------------
INDEX_DIR = os.environ.get("INDEX_DIR", "bm25_index")
DEFAULT_STOPWORDS = os.environ.get("STOPWORDS", None) # e.g., "de"
PORT = int(os.environ.get("PORT", "8022"))
# -----------------------
# Load index + metadata
# -----------------------
index_path = Path(INDEX_DIR)
meta_path = index_path / "meta.jsonl"
if not index_path.exists():
raise RuntimeError(f"Index folder not found: {index_path.resolve()}")
if not meta_path.exists():
raise RuntimeError(f"Missing sidecar meta.jsonl: {meta_path.resolve()}")
# Load meta.jsonl
metas: List[Dict[str, Any]] = []
with meta_path.open("r", encoding="utf-8") as f:
for line in f:
line = line.strip()
if line:
metas.append(json.loads(line))
# If meta has integer 'doc_id', sort by it to align with index order.
if metas and isinstance(metas[0], dict) and "doc_id" in metas[0]:
try:
metas.sort(key=lambda m: int(m["doc_id"]))
except Exception:
# Fallback: keep file order
pass
# Load BM25 retriever (we assume the index was built without storing the corpus)
retriever = bm25s.BM25.load(str(index_path), load_corpus=False)
# -----------------------
# API
# -----------------------
app = FastAPI(title="DSA BM25 API", version="1.0.0")
class SearchRequest(BaseModel):
query: str
topk: int = 10
stopwords: Optional[str] = None # e.g., "de"
@app.get("/health")
def health():
return {
"status": "ok",
"index_dir": str(index_path),
"meta_docs": len(metas),
"default_stopwords": DEFAULT_STOPWORDS,
}
@app.post("/search")
def search(req: SearchRequest):
try:
sw = req.stopwords if req.stopwords is not None else DEFAULT_STOPWORDS
q_tokens = bm25s.tokenize(req.query, stopwords=sw)
docs, scores = retriever.retrieve(q_tokens, k=req.topk) # shapes: (1, k)
results: List[Dict[str, Any]] = []
for doc_idx, score in zip(docs[0], scores[0]):
di = int(doc_idx)
meta = metas[di] if 0 <= di < len(metas) else {}
results.append(
{
"doc_index": di, # raw index ID from BM25
"score": float(score), # raw score
"meta": meta, # raw meta doc (no formatting)
}
)
return {
"query": req.query,
"topk": req.topk,
"stopwords": sw,
"results": results,
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=PORT)