
Ha nap mint nap AI-kódoló asszisztensekkel dolgozol, ismered a problémát: a beszélgetési napló — minden munkamenet, minden prompt, minden eszközhívás — csendben több gigabájtos adatbázissá növekszik. A keresés lassúvá válik, a biztonsági mentések örökké tartanak, és amire valójában emlékezni akarsz (az a javítás egy trükkös hibához, az a refaktor-minta, az az egy parancs) ezer másik sor alá temetődik.
Ez a cikk egy olyan példát mutat be, amelyet élesben is használunk: egy önüzemeltetett archívumrendszert, amely az opencode régi munkameneteket kimásolja az élő adatbázisból egy külön, kompakt, teljes szövegű kereshető archívumba — majd egy gyors webes felületen teszi elérhetővé, amit az egész csapat kereshet, egyszeri bejelentkezéssel (SSO) védve.
A legjobb az egészben: az egész unalmas, bevált építőelemekből áll — SQLite, FTS5, egy kis Python-szkript, cron és egy vékony webes réteg. Nincs Elasticsearch-fürt, nincs felügyelt keresőszolgáltatás, nincs szállítói bezártság.
Megmutatom, hogyan építheted meg.
A probléma: az élő adatbázis nem könyvtár

Az AI-kódoló asszisztensek jellemzően egy helyi adatbázisban tárolják a beszélgetési előzményeket. Hónapokig tartó napi használat után az adatbázis felhalmoz:
- Munkameneteket (session) — egyet minden beszélgetéshez
- Üzeneteket — a párbeszédet közted és az asszisztens között
- Részeket (part) — üzenetszegmenseket (szöveg, eszközhívás, kódblokk)
- Eseményeket — belső telemetriai sorokat, amelyek a felfúvódás oroszlánrészét adják
A számok gyorsan összeadódnak. Egy év napi munka könnyedén több tízezer munkamenetet és egymilliónál több eseménysort eredményez. Az élő adatbázis gigabájtosra nő, és minden lekérdezés — beleértve azokat is, amelyeket maga az asszisztens futtat — egyre lassabb lesz.
A kulcsfelismerés: az élő adatbázis munkahely, nem könyvtár. Annak kell benne maradnia, ami friss és aktívan használt. A régi munkamenetek az archívumba tartoznak — oda, ahol kompakt, indexelt, kereshető és örökre olcsón megtartható.
Az architektúra egy pillantásra
A terv három logikai részből áll:
- Az élő rendszer — a termelési adatbázis, kicsiben és gyorsan tartva.
- Az éjszakai export — egy cron-feladat, amely a régi munkameneteket az archívumba másolja, kitisztítja, és opcionálisan megnyesegeti az élő adatbázist.
- Az archívum — egy csak olvasható SQLite-adatbázis teljes szövegű indexszel, amelyet egy kisméretű webalkalmazás szolgál ki csevegés-stílusú felülettel.

Ez a szétválasztás négy, élesben fontos tulajdonságot ad:
| Tulajdonság | Hogyan érjük el |
|---|---|
| Az élő adatbázis gyors marad | A régi munkameneteket éjszakánként (vagy ütemezve) eltávolítjuk |
| Semmi sem vész el soha | A nyesés csak olyan munkameneteket töröl, amelyek már az archívumban vannak |
| A keresés azonnali | FTS5 teljes szövegű index az archívumon |
| Az archívum biztonságosan megosztható | A titkokat már azelőtt kitakarjuk, hogy elhagynák az élő adatbázist |
Az éjszakai export-feladat
Az élő adatbázis biztonságos olvasása
Az első szabály: soha ne zárold le az élő adatbázist. Ha az alkalmazás WAL módban (write-ahead logging) használja az SQLite-ot, külön folyamatból csak olvasásra nyithatod meg az adatbázist, és olvashatsz, miközben az alkalmazás ír. Nincs állásidő, nincs blokkolás.
import sqlite3
LIVE_DB = "file:/path/to/live.db?mode=ro" # csak olvasható kapcsolat
conn = sqlite3.connect(LIVE_DB, uri=True)
A vízjel: növekményes, idempotens, újraindításbiztos
Minden éjjel mindent kimásolni pazarlás. Ehelyett vízjelet követünk nyomon: a legutóbb exportált munkamenet időbélyegét. Minden futáskor csak a vízjelnél újabb munkameneteket exportálunk.
def get_watermark(archive_conn):
row = archive_conn.execute(
"SELECT value FROM archive_meta WHERE key = 'watermark'"
).fetchone()
return row[0] if row else 0
def set_watermark(archive_conn, ts):
archive_conn.execute(
"INSERT OR REPLACE INTO archive_meta (key, value) VALUES ('watermark', ?)",
(ts,),
)
Két részlet teszi ezt robusztussá:
- Idempotens írások —
INSERT OR REPLACE-t használunk, így a futás közben módosult munkamenet újraexportálása egyszerűen felülírja a régit. A feladat kétszeri futtatása mindig biztonságos. - Hibazáró vízjel — a vízjel csak egy sikeresen befejezett köteg után lép tovább. Ha egy köteg félúton meghiúsul, semmi sem marad ki; a következő futás újrapróbálja.
def export_new_sessions(live_conn, archive_conn, watermark):
cursor = live_conn.execute(
"""
SELECT id, title, created_at, updated_at
FROM session
WHERE updated_at > ?
ORDER BY updated_at
""",
(watermark,),
)
for batch in iter_batches(cursor, size=500):
for session in batch:
export_session(live_conn, archive_conn, session)
archive_conn.execute("BEGIN")
for session in batch:
upsert_session(archive_conn, session)
new_watermark = batch[-1]["updated_at"]
set_watermark(archive_conn, new_watermark)
archive_conn.commit() # a vízjel csak siker esetén lép tovább
Kitakarás: adatvédelem tervezéssel
Itt rontják el a legtöbb archívumot. Ha a beszélgetéseket szó szerint másolod, akkor API-kulcsokat, jelszavakat, tokeneket és titkokat is másolsz, amelyek felbukkantak a beszélgetésben — és olyan helyre teszed őket, amelyet megoszthatsz a csapattal, biztonsági mentésbe kerülhet, vagy indexelhető.
A mi szabályunk: érzékeny adat soha nem kerül be az archívumba. Minden exportált sort egy kulcstudatos tisztítóval vizsgálunk, amely felismeri és eltávolítja a titkos mintákat:
import re
SECRET_PATTERNS = [
re.compile(r"(?i)(password|passwd|pwd|token|secret|api[_-]?key|private[_-]?key)\s*[=:]\s*\S+"),
re.compile(r"sk-[A-Za-z0-9]{20,}"), # API-kulcs előtagok
re.compile(r"(?i)(bearer|basic)\s+[A-Za-z0-9._~+/=-]+"),
re.compile(r"AKIA[0-9A-Z]{16}"), # felhő-hozzáférési kulcsok
]
def redact_text(text: str) -> str:
for pattern in SECRET_PATTERNS:
text = pattern.sub("[kitakarva]", text)
return text
Két további döntés, ami sokat számít:
- Az eszközbemenetek és -kimenetek teljesen kimaradnak. A mi sémánkban az eszközhívás-sorok csak az eszköz nevét és egy csonkolt, kitakart bemeneti részletet őrzik. A teljes bemenet/kimenet — ahol a titkok rejtőznek — soha nem hagyja el az élő adatbázist. Ez tudatos kompromisszum: lemondasz az eszközhívás visszajátszásáról, de lemondasz a hitelesítő adatok kiszivárgásának lehetőségéről is.
- Hibazáró tisztítók. Ha a kitakarás valami szokatlanba ütközik (mélyen beágyazott JSON, váratlan típusok), biztonságos irányba hibázik: a tartalom kimarad, nem megy át nyersen.
Az archívum sémája
Az archívum-adatbázis kicsi, jól meghatározott és könnyen átlátható. Csak a fontos táblákat tükrözi, egy fontos kiegészítéssel: egy keresési tartalomtáblát, amely minden munkamenetet egyetlen kereshető sorba aggregál.
-- A kereshető tartalom: soronként egy munkamenet
CREATE TABLE session_search (
id INTEGER PRIMARY KEY,
title TEXT,
agent TEXT,
model TEXT,
body TEXT, -- az összes üzenet összefűzve új sorokkal
time_updated INTEGER,
message_count INTEGER
);
-- Metaadatok (vízjel stb.)
CREATE TABLE archive_meta (
key TEXT PRIMARY KEY,
value TEXT
);

Azonnali keresés SQLite FTS5-tel
Itt válik az archívum igazán hasznossá. Az SQLite beépített FTS5-tel rendelkezik, egy teljes szövegű keresőmotorral, amely több tízezer dokumentumra is elég gyors, és nem igényel külső szolgáltatást.
Külső tartalmú táblák: kis index, azonnali lekérdezések
Az okos trükk a külső tartalmú (external-content) FTS5-tábla. Ahelyett, hogy a tartalmat megdupláznánk az indexben, az FTS-tábla csak az invertált indexet tárolja, és szükség esetén a fő táblából húzza be a tartalmat. Az index kicsi marad, az írások olcsók.
CREATE VIRTUAL TABLE session_search_fts USING fts5(
title, agent, model, body,
content='session_search', -- külső tartalomtábla
content_rowid='id'
);
A keresés egyszerű MATCH-lekérdezés:
SELECT s.id, s.title, s.body, s.time_updated
FROM session_search s
JOIN session_search_fts f ON f.rowid = s.id
WHERE session_search_fts MATCH 'sqlite fts5 optimization'
ORDER BY rank
LIMIT 20;
Az index szinkronban tartása triggerekkel
Mivel az FTS-tábla külső tartalmú, a szinkronizálást magadnak kell elvégezned. A bevált módszer a tartalomtáblán lévő triggerek:
CREATE TRIGGER session_search_ai AFTER INSERT ON session_search BEGIN
INSERT INTO session_search_fts(rowid, title, agent, model, body)
VALUES (new.id, new.title, new.agent, new.model, new.body);
END;
CREATE TRIGGER session_search_ad AFTER DELETE ON session_search BEGIN
INSERT INTO session_search_fts(session_search_fts, rowid, title, agent, model, body)
VALUES ('delete', old.id, old.title, old.agent, old.model, old.body);
END;
CREATE TRIGGER session_search_au AFTER UPDATE ON session_search BEGIN
INSERT INTO session_search_fts(session_search_fts, rowid, title, agent, model, body)
VALUES ('delete', old.id, old.title, old.agent, old.model, old.body);
INSERT INTO session_search_fts(rowid, title, agent, model, body)
VALUES (new.id, new.title, new.agent, new.model, new.body);
END;
Az optimalizálás, ami mindent megváltoztatott
Van egy FTS5-tel kapcsolatos buktató, ami rengeteg hibakeresésünkbe került: a töredezettség. Ahogy sorokat szúrsz be és törölsz, az FTS-index sok kis szegmenst halmoz fel. A lekérdezések — főleg a COUNT(*) — ezredmásodpercekről másodpercekre romlanak.
A megoldás egyetlen parancs, minden export után:
INSERT INTO session_search_fts(session_search_fts) VALUES ('optimize');
A mi esetünkben ez a COUNT(*)-ot az indexen 20 másodpercről 3 ezredmásodpercre vitte le. Ha a keresőfelületed valaha lassúnak tűnik, először ezt futtasd.
Az élő adatbázis nyesése (óvatosan)
A második fázis opcionális, de ajánlott: a régi munkamenetek törlése az élő adatbázisból, hogy az kicsi maradjon. A biztonsági szabály abszolút:
Soha ne törölj olyan munkamenetet, amely nincs már az archívumban.
És egy gyakorlati szabály: életkorától függetlenül mindig tartsd meg a legutóbbi N munkamenetet. Senki aktív beszélgetését nem szabad félbeszakítani.
def prune_sessions(live_conn, archive_conn, keep_days=30, floor=30):
now = int(time.time())
cutoff = now - keep_days * 86400
# Azok a munkamenetek, amelyek RÉGIEK ÉS archívumban vannak
cursor = live_conn.execute(
"""
SELECT s.id FROM session s
WHERE s.updated_at < ?
AND s.id IN (SELECT id FROM archive.session) -- archívumban?
ORDER BY s.updated_at DESC
LIMIT -1 OFFSET ? -- a padló megmarad
""",
(cutoff, floor),
)
ids = [row[0] for row in cursor]
delete_sessions(live_conn, ids) # kötegenként, FK kaszkáddal
A tényleges törlésnek tiszteletben kell tartania a külső kulcsokat — a munkamenet törlése kaszkádoljon az üzeneteire és részeire, különben árva sorokat hagysz hátra, és az adatbázis valójában nem is zsugorodik.
Tipp: ha az alkalmazás táblái nem deklarálják a
FOREIGN KEY ... ON DELETE CASCADEszabályt, töröld a gyermeksorokat explicit módon (üzenet → rész → munkamenet), vagy használj egyetlen tranzakciótPRAGMA foreign_keys = ONkapcsolóval.
A webes réteg: Datasette + csevegés-stílusú SPA
Miért Datasette?
Az archívum kiszolgálásához a Datasette-t használjuk — egy kisméretű Python-webalkalmazást, amely az SQLite-adatbázisokat böngészhető, lekérdezhető weboldalakká változtatja, ingyenes JSON API-val. Ezt adja:
- Böngészős felületet a táblákhoz (nagyszerű hibakereséshez)
- JSON API-t (
/db.json,?sql=...&_shape=array) a frontendhez - Bővítményrendszert a hitelesítéshez, átirányításokhoz és egyedi viselkedéshez
- Csak olvasható kiszolgálást konfigurálható lekérdezési időkorlátokkal
Csevegés-stílusú frontend (a rész, amit a felhasználók látnak)
A nyers adatbázis-felület adminoknak jó, csapatnak nem. Egy kisméretű egyoldalas alkalmazást építettünk, amely magának az asszisztensnek a csevegésélményét utánozza:
- Munkamenet-nézet — munkamenetkártyák lapozható listája (cím, ügynök, modell, utolsó frissítés, üzenetszám).
- Keresés — teljes szövegű keresőmező; az eredmények kiemelt részletekkel mutatják a munkameneteket.
- Beszélgetés-nézet — kattints egy munkamenetre, és olvasd úgy, mint egy csevegést: a felhasználó üzenetei jobbra, az asszisztensé balra, összecsukható gondolkodás, eszközhívás-kártyák.
- Ugrás a találatra — amikor keresési eredményből nyitsz meg egy munkamenetet, az oldal pontosan a találatot tartalmazó üzenethez görget, és felvillantja azt.

A frontend kizárólag a JSON API-val beszél:
async function search(query, offset = 0) {
const sql = `
SELECT s.id, s.title, s.agent, s.model, s.time_updated, s.message_count,
snippet(session_search_fts, 5, '[', ']', '…', 12) AS snippet
FROM session_search s
JOIN session_search_fts ON session_search_fts.rowid = s.id
WHERE session_search_fts MATCH :q
ORDER BY rank LIMIT 20 OFFSET :offset`;
const params = new URLSearchParams({ sql, q: query, _shape: 'array', offset });
const res = await fetch(`/api/database.json?${params}`);
return res.json();
}
Megjegyzés a teljesítményről: a Datasette (és a legtöbb SQLite-webes réteg) lehetővé teszi a
sql_time_limit_msés gyorsítótár-beállítások megadását. Nagy FTS-indexnél adj bőkezű lekérdezési időkorlátot (pl. 20 mp), hogy a hideg gyorsítótárú lekérdezések ne 504-es hibát adjanak.
Egyszeri bejelentkezés: OIDC a proxy megérintése nélkül

Az archívum érzékeny előzményeket tartalmazhat, ezért az anonim hozzáférést tiltani kell, a csapattagok pedig a meglévő céges fiókjukkal jelentkezzenek be. A tiszta megoldás az OIDC a saját identitásszolgáltatód ellen (Authentik, Keycloak, Okta, Entra ID — bármelyik OpenID Connect szolgáltató).
Két bevett megközelítés létezik:
- Fordított proxy-szintű hitelesítés — a proxy (nginx, HAProxy, Traefik) kezeli az OIDC-t, és fejlécekben adja át az azonosságot. Erőteljes, de módosítja a proxy konfigurációját, és ütközhet más útvonalakkal.
- Alkalmazásszintű OIDC-bővítmény — maga az alkalmazás valósítja meg az OIDC-klienst. Egyáltalán nincs proxy-módosítás. Mi ezt választottuk.
A Datasette-hez nem létezett karbantartott OIDC-bővítmény, ezért írtunk egy kicsit (~150 sor), a jól ismert datasette-auth0 bővítmény mintájára (ugyanazt a mintát használja a datasette-auth-osm projekt is). Azért ilyen kicsi, mert a platform saját primitívjeit használja újra:
datasette.sign()a cookie-k és az OIDC-állapotparaméter aláírásáhozdatasette.set_cookie()httponly,secure,samesite="lax"beállításokkal- a szabványos
actor_from_requesthorog a bejelentkezett szereplő beállításához
Az OIDC-folyamat 100 sorban
import secrets
import httpx
from datasette.utils.asgi import Response
AUTH_STATE_COOKIE = "auth-state"
async def start_login(datasette, request):
# 1. Véletlen állapot generálása, cookie-ba aláírva
state = secrets.token_hex(16)
signed = datasette.sign({"state": state}, namespace="authentik-oidc")
response = Response.redirect(authorization_url(state))
response.set_cookie(
AUTH_STATE_COOKIE,
signed,
httponly=True,
secure=True,
samesite="lax",
)
return response
async def callback(datasette, request):
# 2. Állapot ellenőrzése (állandó idejű!), kód becserélése tokenekre
expected = datasette.unsign(request.cookies.get(AUTH_STATE_COOKIE),
namespace="authentik-oidc")
if not expected or not secrets.compare_digest(
expected["state"], request.args.get("state", "")
):
return Response.text("Érvénytelen állapot", status=400)
code = request.args.get("code")
token_resp = await _exchange_code(datasette, code)
profile = await _fetch_profile(datasette, token_resp["access_token"])
# 3. Aláírt szereplő-cookie beállítása — a felhasználó bejelentkezett
actor = {"id": f"authentik-{profile['sub']}", "name": profile.get("name")}
response = Response.redirect("/")
response.set_cookie(
"ds_actor",
datasette.sign({"a": actor, "e": expiry}, namespace="actor"),
httponly=True, secure=True, samesite="lax",
)
return response
A három, igazán fontos biztonsági részlet:
| Részlet | Miért |
|---|---|
| Aláírt állapotparaméter | Bejelentkezési CSRF megelőzése — a támadó nem tud hamis visszahívást kovácsolni |
secrets.compare_digest | Állandó idejű összehasonlítás, nincs időzítési mellékcsatorna |
| HttpOnly + Secure + SameSite=Lax cookie-k | Nincs JavaScript-hozzáférés, csak HTTPS, CSRF-ellenálló |
A végpont-buktata, ami egy délutánunkba került
A különböző identitásszolgáltatók más-más elérési úton teszik elérhetővé az OIDC-végpontjaikat. A mi szolgáltatónk az authorize, token és userinfo végpontokat az alkalmazás-slug nélkül szolgálta ki — a JWKS (JSON Web Key Set) URL-t viszont vele együtt. A naiv megvalósítás, amely a végpontokat az issuer URL szöveges összefűzésével vezeti le, eltörik.
A robusztus megközelítés: minden végpontot a felfedezési dokumentumból (.well-known/openid-configuration) származtass indításkor, és soha ne tippelj:
async def discover(config):
async with httpx.AsyncClient(timeout=10) as client:
r = await client.get(config["issuer"].rstrip("/") + "/.well-known/openid-configuration")
r.raise_for_status()
doc = r.json()
return {
"authorization_endpoint": doc["authorization_endpoint"],
"token_endpoint": doc["token_endpoint"],
"userinfo_endpoint": doc["userinfo_endpoint"],
}
Csoporttal korlátozott hozzáférés
Az egyszeri bejelentkezés önmagában még nem elég — jogosultságszabályozás is kell. Az identitásszolgáltató gondoskodjon róla, hogy csak egy adott csoport tagjai jelentkezhessenek be az archívumba:
- Hozz létre egy csoportot (pl.
archive-users) az identitásszolgáltatóban. - Kösd az alkalmazás hozzáférési szabályzatát ehhez a csoporthoz.
- A felhasználókat a csoporthoz adod/veszed el — kódváltoztatás, újratelepítés nélkül.
A mi beállításunkban az alkalmazás engedélyezési szabálya egyszerűen „bármely hitelesített szereplő beléphet” — a szolgáltató dönti el, hogy ki hitelesülhet. Így az alkalmazás ostoba marad, a szabályzat pedig központosított.
Minden együtt
A cron-feladat
Egyetlen cron-bejegyzés hajtja az egész folyamatot:
# Archívum-export + nyesés — éjszakánként 02:00-kor
0 2 * * * root /opt/archive/run.sh >> /var/log/archive.log 2>&1
A run.sh pedig szándékosan unalmas:
#!/bin/bash
set -euo pipefail
python3 /opt/archive/archive_sessions.py # növekményes export + FTS-újraépítés
python3 /opt/archive/archive_sessions.py --prune # régi, archivált munkamenetek törlése
A teljes séma, egy fájlban
Íme a teljes archívum-indító SQL — másold ki, igazítsd az oszlopokat, és fut is:
PRAGMA journal_mode = WAL;
CREATE TABLE IF NOT EXISTS session (
id INTEGER PRIMARY KEY,
title TEXT,
agent TEXT,
model TEXT,
created_at INTEGER,
updated_at INTEGER
);
CREATE TABLE IF NOT EXISTS message (
id INTEGER PRIMARY KEY,
session_id INTEGER NOT NULL REFERENCES session(id) ON DELETE CASCADE,
role TEXT, -- 'user' | 'assistant' | 'tool'
content TEXT,
created_at INTEGER
);
CREATE TABLE IF NOT EXISTS session_search (
id INTEGER PRIMARY KEY,
title TEXT,
agent TEXT,
model TEXT,
body TEXT,
time_updated INTEGER,
message_count INTEGER
);
CREATE TABLE IF NOT EXISTS archive_meta (
key TEXT PRIMARY KEY,
value TEXT
);
CREATE VIRTUAL TABLE IF NOT EXISTS session_search_fts USING fts5(
title, agent, model, body,
content='session_search',
content_rowid='id'
);
-- (add ide a 4. fejezet három szinkronizáló triggerét)
A teljes export-folyamat, elejétől a végéig
def run_export(live_path, archive_path):
live = sqlite3.connect(f"file:{live_path}?mode=ro", uri=True)
archive = sqlite3.connect(archive_path)
watermark = get_watermark(archive)
for batch in fetch_new_sessions(live, watermark):
for s in batch:
scrub_and_export(live, archive, s) # a kitakarás itt történik
rebuild_search_rows(archive) # session_search frissítése
archive.commit()
set_watermark(archive, batch[-1]["updated_at"])
archive.commit()
archive.execute("INSERT INTO session_search_fts(session_search_fts) VALUES ('optimize')")
archive.commit()
print("Export kész.")
Tanulságok
Ha valami hasonlót építesz, íme a tanulságok, amelyeket bárcsak az első naptól tudtunk volna:
- Használj WAL módot mindenhol. A csak olvasható archívumok és az egyidejű írók problémája teljesen megszűnik.
INSERT OR REPLACE+ vízjel = ingyenes idempotencia. A feladat újrafuttatása mindig biztonságos; ettől a cron-hibák unalmassá válnak ijesztő helyett.- Futtasd az
'optimize'parancsot az FTS-indexen minden export után. A töredezettség a keresési teljesítmény néma gyilkosa. A miCOUNT(*)-unk 20 mp-ről 3 ms-ra ment le. - Takarj ki tárolás előtt, ne utána. Az a titok, amely soha nem kerül be az archívumba, onnan ki sem szivároghat. A lekérdezéskor történő tisztítás csapda.
- Az eszközbemeneteket/-kimeneteket hagyd ki teljesen. Ott laknak a titkok. Tartsd meg a neveket és a csonkolt, kitakart részleteket.
- Csak azt nyesd, ami archiválva van. A „soha ne törölj nem archivált adatot” szabály a különbség archívum és adatvesztési incidens között.
- Tartsd meg a frissességi padlót. Életkoruktól függetlenül mindig tartsd meg a legutóbbi N munkamenetet.
- Az OIDC-végpontokat a felfedezésből származtasd, soha ne szövegösszefűzésből. A szolgáltatók különböznek, és ez elviheti a délutánodat.
- Adj bőkezű SQL-időkorlátot a hideg gyorsítótárhoz. A 20 másodperces korlát semmibe sem kerül, és megelőzi a félelmetes 504-eseket újraindítás vagy nagy import után.
- A felület nézzen ki úgy, mint a szerszám. A csevegés-stílusú frontend azonnal ismerős mindenkinek, aki AI-asszisztenseket használ — ettől lesz használatbavétel.
Összegzés
Nincs szükséged keresőfürtre egy valóban hasznos beszélgetésarchívumhoz. SQLite-tal, FTS5-tel, egy Python-szkripttel, cron-nal és egy vékony webes réteggel ezt kapod:
- Egy élő adatbázist, amely gyors és kicsi marad
- Egy kompakt, teljes szövegű kereshető archívumot, amely mindent örökre megőriz
- Egy csevegés-stílusú felületet, amelyet a csapatod tényleg használni fog
- SSO-védett hozzáférést csoportos jogosultságszabályozással
- Nulla szállítói bezártságot, nulla rendszeres költséget és teljes ellenőrzést az adataid felett
A teljes folyamat — export, kitakarás, indexelés, kiszolgálás, keresés — körülbelül 500 sor Python és egy kisméretű frontend. Minden éjjel felügyelet nélkül fut, és a növekvő beszélgetési előzmények halmazát kereshető intézményi emlékezetté változtatja.
