# Riksdagen Sökplattform – Teknisk Översikt
En plattform för att utforska riksdagsdebatter och metadata. Backenden bygger på FastAPI, ArangoDB (med ArangoSearch), samt ChromaDB för vektorsök i chatten. Frontenden är en React/Vite-applikation skriven i TypeScript. LLM-funktionalitet kopplas in via `_llm`-paketet.
---
## 📋 Innehållsförteckning
1. [Systemöversikt](#systemöversikt)
2. [Projektstruktur](#projektstruktur)
3. [Backend-arkitektur](#backend-arkitektur)
4. [Frontend-arkitektur](#frontend-arkitektur)
5. [Dataflöden](#dataflöden)
6. [Viktiga Python- och TypeScript-komponenter](#viktiga-python--och-typescript-komponenter)
7. [ArangoDB och ArangoSearch](#arangodb-och-arangosearch)
8. [ChromaDB för vektorsök](#chromadb-för-vektorsök)
9. [LLM-integration](#llm-integration)
10. [Utvecklingsflöde](#utvecklingsflöde)
11. [Felsökning och tips](#felsökning-och-tips)
---
## 🏗️ Systemöversikt
```
┌─────────────┐ HTTP ┌──────────────┐ Python ┌───────────────┐
│ Browser │ ─────────▶ │ FastAPI │ ───────────▶ │ ArangoDB │
│ (React) │ ◀───────── │ backend │ ◀─────────── │ (ArangoSearch) │
└─────────────┘ └──────┬───────┘ └───────────────┘
│
│ embeddings / metadata
▼
┌──────────────┐
│ ChromaDB │
└──────┬───────┘
│
▼
┌──────────────┐
│ _llm │ (OpenAI API-kompatibel klient)
└──────────────┘
```
- **ArangoDB**: primär databas. Sök görs via ArangoSearch-vyer i stället för SQL.
- **ChromaDB**: håller embeddings för chatten och mer avancerad semantisk sökning.
- **LLM**: genererar chattsvar med hjälp av `_llm.LLM` (kopplad till t.ex. vLLM/OpenAI API).
---
## 📁 Projektstruktur
```
riksdagen/
├── arango_client.py # Initierar Arango-klienten (används av backend & scripts)
├── backend/
│ ├── app.py # FastAPI-app, routerregistrering
│ ├── search.py # SearchService med ArangoSearch-frågor
│ ├── chat.py # Chat-endpoints (LLM + Chroma)
│ ├── vector.py # Hjälpfunktioner för vektorsök (omfattar Chroma-anrop)
│ └── ... # Övriga routrar, scheman och konfigurationer
├── frontend/
│ ├── src/
│ │ ├── App.tsx # Inträde, växlar mellan sök/chat-lägen
│ │ ├── api.ts # Axios-klient mot `/api/*`
│ │ ├── types.ts # TypeScript-typer för API-svar
│ │ ├── components/ # UI-komponenter (SearchPanel, ChatPanel m.fl.)
│ │ └── styles.css # Globala stilar
│ └── vite.config.ts
├── scripts/
│ ├── ingest_arguments.py # Exempel: läser in data till ArangoDB
│ └── build_embeddings.py # Skapar embeddings och för in i Chroma
├── _arango/
│ ├── _arango.py # Wrapper kring python-arango (förbättrad initiering)
│ ├── queries/ # Samling av AQL/ArangoSearch-frågor
│ └── helpers.py # Hjälpfunktioner för AQL-byggande
├── _chromadb/
│ ├── client.py # Instansierar ChromaDB-klient
│ ├── collections.py # Hanterar samlingar (persistens, insättning, sök)
│ └── embeddings.py # Hjälp för integrering mot embeddingsgenerator
├── _llm/
│ └── llm.py # LLM-wrapper (OpenAI API-kompatibelt gränssnitt)
└── README.md
```
> **Tips:** Utforska `_arango`, `_chromadb` och `_llm` för att förstå hur databaskopplingar och LLM-anrop är kapslade.
---
## 🔧 Backend-arkitektur
### `backend/app.py`
- Skapar FastAPI-instansen och exponerar endpoints under `/api`.
- Registrerar t.ex. `search_router`, `chat_router`, `meta_router`.
- Konfigurerar CORS så att frontenden kan nå backenden under utveckling och produktion.
### `backend/search.py`
- Innehåller klassen **`SearchService`**.
- `SearchService` använder `arango_client` och `_arango`-hjälpare för att köra ArangoSearch-frågor.
- Vanliga metoder (namn kan skilja något, läs filen för exakt signatur):
- `search(...)`: bygger en ArangoSearch AQL-query med filter (parti, år, talare) samt scoring via `BM25()` eller `TFIDF()`.
- `get_metadata(...)`: hämtar distinkta värden till frontendens filter.
- `get_document_by_id(...)`: används för detaljerad visning/snippets.
- **Viktigt:** ingen SQL används. AQL byggs dynamiskt och exekveras mot en ArangoSearch-vy (se `_arango/queries`).
### `backend/chat.py`
- Hanterar endpointen `POST /api/chat`.
- Steg i chatthanteringen:
1. Plockar senaste användarfrågan.
2. Anropar ChromaDB (via `_chromadb`) för semantisk återhämtning.
3. Kompletterar kontext med Arango-sökresultat om relevant.
4. Bygger prompt och anropar `_llm.LLM`.
5. Returnerar AI-svaret plus metadata (t.ex. vilka dokument som användes).
- Har ofta även `POST /api/vector-search` som låter frontenden testa Chroma-resultat separat.
### `backend/vector.py` (eller motsvarande modul)
- Innehåller hjälpfunktioner för att:
- Normalisera text.
- Skicka query till ChromaDB och hämta top-k embeddings-matchningar.
- Eventuellt uppdatera/återskapa Chroma-samlingar.
---
## ⚛️ Frontend-arkitektur
- **`App.tsx`**: växlar mellan sök- och chat-läge (t.ex. via `const [mode, setMode] = useState<'search' | 'chat'>('search')`).
- **`api.ts`**: axios-klient som exporterar:
- `searchTalks(params)` → `POST /api/search`.
- `chat(messages, strategy)` → `POST /api/chat`.
- `vectorSearch(query)` → `POST /api/vector-search`.
- `getMeta()` → `GET /api/meta`.
- **`components/SearchPanel.tsx`**:
- Kontrollerade inputs för fritext, år, parti m.m.
- Använder `react-query` (`useMutation/useQuery`) för att trigga backend-anrop.
- Visar resultat i `ResultsTable.tsx`.
- **`components/ChatPanel.tsx`**:
- Håller konversation i state (lista av `{ role, content }`).
- Skickar hela historiken till chat-endpointen.
- Renderar streamen av svar.
- **`types.ts`**:
- Speglar backendens Pydantic-scheman med TypeScript-interfaces (`SearchRequest`, `SearchResponse`, `TalkResult`, `ChatRequest`, etc.).
- Bra översättning mellan Python-modeller och TypeScript.
---
## 🔄 Dataflöden
### Sökning via ArangoSearch
1. **Frontend** samlar filtervärden (t.ex. `query="skola"`, `party="S"`, `year=2023`).
2. `api.searchTalks` gör POST till `/api/search`.
3. **FastAPI** tar emot request → skapar `SearchService`.
4. `SearchService.search`:
- Bygger AQL med `SEARCH`-klasuler mot en ArangoSearch-vy (exempel: `SEARCH ANALYZER(doc.anforande_text, "text_sv") LIKE "%skola%"`).
- Använder `SORT BM25(doc)` eller `SORT doc.datum` beroende på begärda sorteringsparametrar.
- Begränsar resultat (`LIMIT offset, limit`) och returnerar träffar + total count.
5. Backend serialiserar dokumenten till JSON (inklusive metadata som färger, snippet, etc.).
6. **Frontend** renderar resultat och uppdaterar paginering/statistikpaneler.
### Chatt med LLM
1. Användaren skriver fråga i chat-panelen.
2. `api.chat` skickar en lista av meddelanden (`[{ role: 'user', content: ... }, ...]`).
3. `backend/chat.py`:
- Tar ut senaste användarfrågan.
- `vector_search(...)` i samma modul eller via `_chromadb` hämtar semantiskt relevanta avsnitt.
- Kan komplettera svar med `SearchService`-resultat för att säkerställa faktabas.
- Lägger samman kontext och anropar `_llm.LLM`.
4. AI-svaret skickas tillbaka till frontenden och läggs på konversationen.
---
## 🧩 Viktiga Python- och TypeScript-komponenter
| Fil | Funktion |
|-----|----------|
| `arango_client.py` | Central Arango-klient. Återanvänds i backend och scripts. |
| `_arango/_arango.py` | Wrapper som läser miljövariabler, öppnar anslutningar och ger helpers för views/collections. |
| `_arango/queries/*` | Samlade AQL-snippets för sökning, metadata, detaljer. Läs dessa för att förstå exakta fält och filtreringslogik. |
| `_chromadb/client.py` | Skapar en Chroma-klient (kan vara persistent eller in-memory beroende på config). |
| `_chromadb/collections.py` | Hjälpfunktioner för att skapa/hämta Chroma-samlingar och göra `query`/`add`. |
| `_llm/llm.py` | LLM-wrapper. `LLM(model='vllm')` returnerar en klient med `.chat(messages=...)`. |
| `backend/search.py` | `SearchService` med AQL/ArangoSearch-hantering och responsformatering. |
| `backend/chat.py` | Chat-endpoints, orchestrerar Chroma + LLM + (valfritt) Arango-sök. |
| `frontend/src/api.ts` | Samlar alla HTTP-anrop till backend. Lätt att mocka i tester. |
| `frontend/src/components/*` | React-komponenter. Kommentera gärna i koden var data kommer ifrån och vad som skickas tillbaka till backend. |
---
## 🗄️ ArangoDB och ArangoSearch
- **Data** ligger i ArangoDB-kollektioner, ofta `talks`, `speakers`, `debates`, etc.
- **Views**: en ArangoSearch-vy (t.ex. `talks_view`) indexerar textfält med svenska analyser (`text_sv`). Detta gör att `SEARCH`-frågor kan använda ranking (BM25/TFIDF) och filtrera på fält.
- **AQL-byggstenar** (se `_arango/queries`):
- `FOR doc IN talks_view`
- `SEARCH ANALYZER(doc.anforande_text, "text_sv") LIKE "%..."%`
- `FILTER doc.party == @party`
- `SORT BM25(doc) DESC`
- `LIMIT @offset, @limit`
- **Metadata**: Distinkta listor (ex. partier, år) hämtas via AQL `COLLECT`.
> **Ny i ArangoSearch?** Läs igenom `_arango/queries` och Arangos officiella exempel. Notera att ArangoSearch skiljer sig från vanliga AQL-index genom att all textindexering sker i vyn, inte kollektionen.
---
## 🧠 ChromaDB för vektorsök
- `_chromadb/client.py` konfigurerar anslutningen (lokal fil, DuckDB + Parquet som standard).
- Embeddings genereras i `scripts/build_embeddings.py`:
1. Hämtar tal från ArangoDB med `arango_client`.
2. Delar upp text i chunkar (se `_chromadb/embeddings.py` för heuristiken).
3. Kör embeddingsgenerator (vanligen via `_llm` eller egen modul).
4. Lagrar chunkar + metadata (`talk_id`, `speaker`, `party`, `source`) i Chroma-samlingen.
- Vid chat:
- `vector_search` skickar användarfrågan som embedding till Chroma (`collection.query(query_texts=[...], n_results=top_k)`).
- Resultatet innehåller `ids`, `documents`, `metadatas`, `distances` som matas in i prompten.
---
## 🤖 LLM-integration
- `_llm/llm.py` är huvudgränssnittet. Grundmönster:
```python
from _llm import LLM
def generate_answer(prompt: str) -> str:
llm = LLM(model="vllm")
messages = [
{"role": "system", "content": "Du är en assistent som förklarar riksdagsdebatter sakligt."},
{"role": "user", "content": prompt},
]
response = llm.chat(messages=messages)
return response
```
- Miljövariabler (`CHAT_MODEL`, `LLM_BASE_URL`, `LLM_API_KEY`) styr målmodell och endpoint.
- Chat-endpointen täcker redan vanlig användning. För experiment: återanvänd samma mönster men bygg tydlig prompt av kontexten du hämtar.
> Undvik onödiga `try/except`. Fel bubbla upp så att du ser den faktiska orsaken (t.ex. nätverksproblem, fel API-nyckel).
---
## 🛠️ Utvecklingsflöde
| Steg | Befallning | Kommentar |
|------|------------|-----------|
| Starta backend i utvecklingsläge | `uvicorn backend.app:app --reload --port 8000` | Körs i projektroten. Automatiskt reload vid filändring. |
| Starta frontend i dev-läge | `cd frontend && npm run dev` | Öppna http://localhost:5173 |
| Bygg frontend för produktion | `make frontend` | Skapar `frontend/dist`. |
| Ladda om nginx (om du serverar statiskt) | `sudo systemctl reload nginx` | Efter ny frontend-build. |
| Kör embeddings-script | `python -m scripts.build_embeddings --limit 500` | Uppdaterar Chroma-samlingen. |
| Kör anpassade Arango-script | `python -m scripts.ingest_arguments` | Exempel, kontrollera scriptens docstrings. |
---
## 🧪 Felsökning och tips
- **Arango-anslutning**: kör `python arango_client.py` för att testa att lösenord och URL stämmer.
- **Chroma-data**: öppna `_chromadb/client.py` och kontrollera `persist_directory`. Rensa katalogen om du behöver resetta.
- **Chat-svar saknar fakta**: kontrollera att `build_embeddings.py` har körts nyligen och att Chroma-samlingen innehåller rätt metadata.
- **React/TypeScript**:
- Använd `useQuery` (för GET) och `useMutation` (för POST).
- Håll komponenter små och bryt ut delkomponenter vid behov (t.ex. `FilterControls`, `ChatMessageList`).
- Typsätt props, state och API-svar med `types.ts` för bättre editorstöd.
- **Python-stil**:
- Lägg till type hints på funktioner (ex: `def search(self, params: SearchRequest) -> SearchResponse:`).
- Skriv docstrings som beskriver syfte, parametrar och returvärden.
- Undvik generella `try/except`; analysera felen vid körning och fixa orsaken.
---
## 🤔 Nästa steg
1. **Kommentera kod**: lägg till docstrings i `SearchService` och chat-hjälpare.
2. **Testa ArangoQueries**: skapa enhetstester som kör små AQL-scenarier med testdata.
3. **Frontend-guidning**: överväg inline-kommentarer i React-komponenterna för att förklara state-hantering.
4. **Observabilitet**: logga vilka ArangoSearch-queries som körs vid olika filterkombinationer.
---
## 📎 Referenser
- [ArangoDB Documentation – ArangoSearch](https://www.arangodb.com/docs/stable/arangosearch.html)
- [ChromaDB Python API](https://docs.trychroma.com/)
- [FastAPI – Depends och Pydantic](https://fastapi.tiangolo.com/)
- [React + TypeScript Cheatsheets](https://react-typescript-cheatsheet.netlify.app/)
- `batch_embed()` – Processar flera texter samtidigt
**Exempel**:
```python
def get_embedding(text: str, model: str = "embeddinggemma") -> List[float]:
"""
Genererar en embedding-vektor för given text.
Args:
text: Texten att embedda
model: Ollama-modell (default: embeddinggemma)
Returns:
Lista med floats (3072 dimensioner för embeddinggemma)
"""
# Anropar http://192.168.1.11:11434/api/embeddings
# Returnerar vektor
```
#### `backend/services/vector_store.py`
Hanterar pgvector-operationer:
- `create_embeddings_table()` – Skapar `talk_embeddings`-tabell
- `store_embedding()` – Sparar embedding i databas
- `query_similar_talks()` – Semantisk sökning
---
### Database: `backend/database.py`
**Viktigt**: Denna fil skapar databaskopplingen.
```python
# Skapar engine (kopplingen till PostgreSQL)
engine = create_engine(DATABASE_URL)
# Aktiverar pgvector-extension
with engine.connect() as conn:
conn.execute(text("CREATE EXTENSION IF NOT EXISTS vector"))
# Session factory – används för att skapa databas-sessioner
SessionLocal = sessionmaker(bind=engine)
# Dependency för FastAPI – ger varje endpoint en databas-session
def get_db():
db = SessionLocal()
try:
yield db # Ger sessionen till endpointen
finally:
db.close() # Stänger sessionen efter request
```
**Hur används det**:
```python
@router.post("/api/search")
def search_endpoint(request: SearchRequest, db: Session = Depends(get_db)):
# db är nu en aktiv databas-session
results = search_talks(db, ...)
```
---
## ⚛️ Frontend-arkitektur
### Huvudfil: `frontend/src/App.tsx`
Detta är root-komponenten som renderas i `index.html`.
```typescript
function App() {
const [mode, setMode] = useState<'search' | 'chat' | 'vector'>('search');
// mode styr vilket läge användaren är i:
// - 'search': Vanlig nyckelordssökning
// - 'chat': AI-chattläge
// - 'vector': Semantisk vektorsökning
return (
{/* Knappar för att växla läge */}
{/* Visar olika komponenter baserat på mode */}
{mode === 'search' && }
{mode === 'chat' && }
{mode === 'vector' && }
);
}
```
**Viktiga React-koncept**:
- `useState`: Skapar state-variabel som kan ändras. När `mode` ändras, re-renderar React komponenten.
- `{ }`: JavaScript-uttryck inuti JSX (React's HTML-liknande syntax)
- `&&`: Villkorlig rendering – `condition && ` renderar bara om condition är true
---
### API-klient: `frontend/src/api.ts`
Detta är hur frontend pratar med backend.
```typescript
import axios from 'axios';
// Base URL – i produktion går detta via nginx reverse proxy
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || '/api';
// axios-instans med konfigurerad base URL
const api = axios.create({
baseURL: API_BASE_URL,
headers: { 'Content-Type': 'application/json' }
});
// Funktion för att söka
export async function searchTalks(params: SearchParams): Promise {
const response = await api.post('/search', params);
return response.data; // Backend returnerar JSON
}
// Funktion för att chatta
export async function chat(messages: ChatMessage[]): Promise {
const response = await api.post('/chat', { messages });
return response.data;
}
```
**TypeScript-koncept**:
- `async/await`: Hanterar asynkrona operationer (API-anrop)
- `Promise`: Representerar ett framtida värde av typ T
- `interface`: Definierar shape av objekt (se `types.ts`)
---
### Komponenter
#### `frontend/src/components/SearchPanel.tsx`
Sökformulär med filter.
**State-hantering**:
```typescript
const [query, setQuery] = useState(''); // Fritextsök
const [party, setParty] = useState(''); // Valt parti
const [year, setYear] = useState(''); // Valt år
// ...fler filter
// react-query mutation för att söka
const searchMutation = useMutation({
mutationFn: searchTalks,
onSuccess: (data) => {
// data innehåller SearchResponse från backend
// Skickas till ResultsTable via props
}
});
// När användaren klickar "Sök"
const handleSearch = () => {
searchMutation.mutate({
query,
party,
year,
// ...andra filter
});
};
```
**React Query (`useMutation`)**:
- Hanterar API-anrop automatiskt
- Ger tillgång till `isLoading`, `error`, `data`
- Cachas resultat för snabbare återanvändning
**JSX-exempel**:
```tsx
setQuery(e.target.value)}
placeholder="Sök efter innehåll..."
/>
```
**Hur det fungerar**:
1. `value={query}` – Input-fältet visar värdet av `query` state
2. `onChange` – När användaren skriver, uppdateras `query` med `setQuery()`
3. Detta är "controlled component" pattern i React
---
#### `frontend/src/components/ResultsTable.tsx`
Visar sökresultat i tabell.
```typescript
interface ResultsTableProps {
results: TalkResult[]; // Array av anföranden från backend
total: int; // Totalt antal träffar
isLoading: boolean; // Om data laddas
}
function ResultsTable({ results, total, isLoading }: ResultsTableProps) {
if (isLoading) return
Laddar...
;
if (results.length === 0) return
Inga resultat
;
return (
Datum
Talare
Parti
Innehåll
{results.map((talk) => (
{talk.datum}
{talk.talare}
{talk.parti}
{talk.snippet}
))}
);
}
```
**Viktigt**:
- `.map()`: Loopar över array och returnerar JSX för varje element
- `key={talk.id}`: React behöver unika keys för att effektivt uppdatera listan
- Props nedåt: Parent (SearchPanel) skickar data till child (ResultsTable)
---
#### `frontend/src/components/ChatPanel.tsx`
AI-chattgränssnitt.
**State**:
```typescript
const [messages, setMessages] = useState([
{ role: 'assistant', content: 'Hej! Vad vill du veta om riksdagsdebatter?' }
]);
const [input, setInput] = useState(''); // Användarens input
const chatMutation = useMutation({
mutationFn: (msgs: ChatMessage[]) => chat(msgs),
onSuccess: (response) => {
// Lägg till AI-svaret i messages
setMessages(prev => [...prev, { role: 'assistant', content: response.reply }]);
}
});
const handleSend = () => {
// Lägg till användarens meddelande
const newMessages = [...messages, { role: 'user', content: input }];
setMessages(newMessages);
setInput(''); // Töm input-fält
// Skicka till backend
chatMutation.mutate(newMessages);
};
```
**Rendering**:
```tsx