-
Notifications
You must be signed in to change notification settings - Fork 1
aql_functions_reference
ThemisDB Query Language (AQL) - Die einzige Abfragesprache, die Graph, Vector, Relational, Geo und File in einer einheitlichen Syntax vereint.
Version: 1.3
Stand: Dezember 2024
Funktionen: ~355
Kategorien: 13
- Alleinstellungsmerkmale
- Architektur-Übersicht
- Vergleich mit anderen Datenbanken
- Funktionskategorien
- Syntax-Grundlagen
- String-Funktionen (~20 Funktionen)
- Math-Funktionen (~30 Funktionen) ⭐ Erweitert
- Array-Funktionen (~20 Funktionen)
- Date-Funktionen (~45 Funktionen)
- Document-Funktionen (~20 Funktionen)
- Collection-Funktionen (~25 Funktionen)
- Logical-Funktionen (~20 Funktionen) ⭐ Neu
- Geo-Funktionen (~25 Funktionen)
- CRS-Funktionen (Koordinatentransformation) (~10 Funktionen)
- Vector-Funktionen (~20 Funktionen)
- Graph-Funktionen (~15 Funktionen)
- Relational-Funktionen (~25 Funktionen)
- File-Funktionen (~20 Funktionen)
- Security-Funktionen (~15 Funktionen)
- Excel-kompatible Funktionen (~30 Funktionen) ⭐ Neu
- Praxisbeispiele nach Branche
- Performance-Optimierung
- Benchmarks ⭐ Neu
- Fehlerbehandlung
- FAQ - Häufige Fragen
- Migrations-Leitfäden
- Glossar
ThemisDB ist die erste und einzige Datenbank, die echte Multi-Model-Queries in einer einheitlichen Abfragesprache ermöglicht. Während andere Datenbanken einzelne Stärken haben, vereint ThemisDB alle Paradigmen nahtlos.
| Feature | ThemisDB | Neo4j | PostgreSQL | MongoDB | Pinecone | ArangoDB |
|---|---|---|---|---|---|---|
| Unified Query Language | ✅ Eine Syntax für alles | ❌ Cypher only | ❌ SQL only | ❌ MQL only | ❌ API only | |
| Native Graph + Vector | ✅ Integriert | ❌ Plugin | ❌ Extension | ❌ Atlas Search | ✅ Vector only | |
| Geo + Graph kombiniert | ✅ ST_* + SHORTEST_PATH | ❌ Separat | ✅ PostGIS | ✅ GeoJSON | ❌ | |
| BPMN Process Mining | ✅ Native | ❌ | ❌ | ❌ | ❌ | ❌ |
| CRS Transformation | ✅ ETRS89/UTM/WGS84 | ❌ | ✅ PostGIS | ❌ | ❌ | ❌ |
| Multi-Model in einer Query | ✅ Vollständig | ❌ | ❌ | ❌ | ❌ | |
| Window Functions | ✅ ROW_NUMBER, LAG, LEAD | ❌ | ✅ | ❌ | ❌ | ❌ |
| File/MIME Operations | ✅ Native | ❌ | ❌ | ❌ | ❌ | ❌ |
| Helmert Transformation | ✅ 7-Parameter | ❌ | ✅ PostGIS | ❌ | ❌ | ❌ |
Das Problem: In traditionellen Architekturen müssen Sie mehrere Systeme kombinieren:
- PostgreSQL für relationale Daten
- Neo4j für Graphen
- Pinecone/Weaviate für Vektoren
- Elasticsearch für Volltextsuche
- Redis für Caching
Die ThemisDB-Lösung:
-- Eine Query, die Graph, Vector, Geo und Relational kombiniert
FOR customer IN customers
-- Geo: Kunden im Umkreis von 10 km
FILTER GEO_DISTANCE(customer.location, @myLocation) < 10000
-- Vector: Ähnliche Interessen (ML-Embedding)
LET similarity = COSINE_SIMILARITY(customer.interests_embedding, @myInterests)
FILTER similarity > 0.8
-- Graph: Verbindungen über Empfehlungsnetzwerk
FOR connection IN 1..3 OUTBOUND customer knows
FILTER connection.active == true
-- Relational: Aggregation und Window Functions
LET orderStats = (
FOR order IN orders
FILTER order.customer_id == customer._key
COLLECT AGGREGATE
total = SUM(order.amount),
count = COUNT(1)
RETURN { total, count }
)
RETURN {
customer,
similarity,
connection,
orderStats,
distance_km: GEO_DISTANCE(customer.location, @myLocation) / 1000
}
Vergleich: Das gleiche in anderen Systemen
| System | Erforderliche Queries/Calls | Komplexität |
|---|---|---|
| ThemisDB | 1 Query | ⭐ Einfach |
| PostgreSQL + PostGIS + pgvector | 3 Queries + Application Join | ⭐⭐⭐ Komplex |
| Neo4j + Pinecone | 2 Systeme + 2 API-Calls + Application Join | ⭐⭐⭐ Komplex |
| MongoDB Atlas | Aggregation Pipeline + Atlas Search + $graphLookup | ⭐⭐⭐⭐ Sehr komplex |
ThemisDB kann Prozesse aus Event-Logs entdecken und als BPMN-Diagramme exportieren.
-- Prozesse aus Audit-Logs entdecken
LET events = (
FOR e IN audit_logs
FILTER e.timestamp >= DATE_SUBTRACT(DATE_NOW(), 90, "days")
SORT e.case_id, e.timestamp
RETURN {
case_id: e.case_id,
activity: e.activity,
timestamp: e.timestamp,
resource: e.user_id
}
)
LET process = DISCOVER_PROCESS(events, "case_id", "activity", "timestamp")
RETURN {
-- Entdeckte Aktivitäten
activities: process.activities,
-- Übergänge zwischen Aktivitäten
transitions: process.transitions,
-- Prozessvarianten (unterschiedliche Pfade)
variants: process.variants,
-- Engpässe identifizieren
bottlenecks: process.bottlenecks,
-- BPMN-Export
bpmn_xml: EXPORT_BPMN(process)
}
Anwendungsfälle:
- 🏥 Krankenhaus: Patientenpfade optimieren
- 🏭 Fertigung: Produktionsprozesse analysieren
- 🏦 Bank: Kreditanträge beschleunigen
- 📦 Logistik: Lieferketten visualisieren
ThemisDB transformiert zwischen allen gängigen Koordinatensystemen - inklusive der komplexen Helmert-7-Parameter-Transformation für historische Datensätze.
-- Beispiel: Katasterdaten aus verschiedenen Epochen harmonisieren
-- 1. Historische Gauß-Krüger Daten (DHDN, Bessel-Ellipsoid)
FOR parcel IN historic_parcels_gk
LET wgs84 = ST_TRANSFORM(parcel.geometry, 31467, 4326)
UPDATE parcel WITH { geometry_wgs84: wgs84 } IN historic_parcels_gk
-- 2. Aktuelle ETRS89/UTM Daten
FOR parcel IN modern_parcels_utm
LET wgs84 = ST_TRANSFORM(parcel.geometry, 25832, 4326)
UPDATE parcel WITH { geometry_wgs84: wgs84 } IN modern_parcels_utm
-- 3. Kombinierte Abfrage über alle Epochen
FOR parcel IN UNION(historic_parcels_gk, modern_parcels_utm)
LET center = ST_CENTROID(parcel.geometry_wgs84)
FILTER ST_CONTAINS(@searchArea, center)
RETURN {
id: parcel.id,
original_crs: parcel.original_srid,
area_sqm: ST_AREA(parcel.geometry),
center: { lat: ST_Y(center), lon: ST_X(center) }
}
Unterstützte Transformationen:
| Von | Nach | Methode |
|---|---|---|
| EPSG:31466-31469 (Gauß-Krüger) | EPSG:4326 (WGS84) | Helmert 7-Parameter |
| EPSG:25831-25833 (ETRS89/UTM) | EPSG:4326 (WGS84) | Transverse Mercator |
| EPSG:32631-32633 (WGS84/UTM) | EPSG:4326 (WGS84) | Transverse Mercator |
| EPSG:3857 (Web Mercator) | EPSG:4326 (WGS84) | Spherical Mercator |
| EPSG:4258 (ETRS89) | EPSG:4326 (WGS84) | Identity (praktisch gleich) |
ThemisDB speichert und durchsucht Embeddings von beliebigen ML-Modellen nativ.
-- OpenAI Embeddings (1536 Dimensionen)
INSERT {
text: "Künstliche Intelligenz revolutioniert die Medizin",
embedding: [0.0123, -0.0456, 0.0789, ...], -- 1536 Werte
source: "research_paper",
published: DATE_NOW()
} INTO documents
-- Cohere Embeddings (1024 Dimensionen)
INSERT {
text: "AI transforms healthcare",
embedding: [0.0234, 0.0567, -0.0890, ...], -- 1024 Werte
source: "news_article"
} INTO documents
-- Lokale Sentence-Transformers (384 Dimensionen)
INSERT {
text: "Machine Learning im Gesundheitswesen",
embedding: [0.1234, 0.5678, 0.9012, ...], -- 384 Werte
model: "all-MiniLM-L6-v2"
} INTO documents
-- Hybride Suche: Semantisch + Keyword + Geo + Zeit
FOR doc IN documents
LET semantic_score = COSINE_SIMILARITY(doc.embedding, @queryEmbedding)
LET keyword_match = CONTAINS(LOWER(doc.text), LOWER(@searchTerm))
LET geo_score = doc.location ? 1 / (1 + GEO_DISTANCE(doc.location, @userLocation) / 10000) : 0
LET recency_score = 1 / (1 + DATE_DIFF(doc.published, DATE_NOW(), "days") / 30)
-- Kombinierter Relevanz-Score
LET combined_score = (
semantic_score * 0.5 +
(keyword_match ? 0.2 : 0) +
geo_score * 0.15 +
recency_score * 0.15
)
FILTER semantic_score > 0.7 OR keyword_match
SORT combined_score DESC
LIMIT 20
RETURN {
doc,
scores: { semantic: semantic_score, geo: geo_score, recency: recency_score },
combined_score
}
ThemisDB kombiniert die Eleganz von Cypher mit der Vertrautheit von SQL.
-- Finde Influencer im Netzwerk mit mehreren Kriterien
FOR influencer IN users
-- Graph: Follower-Netzwerk analysieren
LET followers = (
FOR f IN 1..1 INBOUND influencer follows
RETURN f
)
LET follower_count = LENGTH(followers)
-- Graph: Reichweite (2-Hop Netzwerk)
LET reach = (
FOR r IN 1..2 INBOUND influencer follows
RETURN DISTINCT r
)
LET reach_count = LENGTH(reach)
-- Zentralitätsmaße
LET pagerank = PAGERANK(influencer, "follows", 0.85)
LET clustering = CLUSTERING_COEFFICIENT(influencer, "follows")
-- Geo: Durchschnittliche Entfernung der Follower
LET avg_follower_distance = AVG(
FOR f IN followers
FILTER f.location != null
RETURN GEO_DISTANCE(f.location, influencer.location)
)
-- Relational: Engagement-Statistiken
LET engagement = (
FOR post IN posts
FILTER post.author_id == influencer._key
FILTER DATE_DIFF(post.created, DATE_NOW(), "days") <= 30
COLLECT AGGREGATE
posts = COUNT(1),
likes = SUM(post.likes),
comments = SUM(post.comments),
shares = SUM(post.shares)
RETURN { posts, likes, comments, shares }
)[0]
FILTER follower_count >= 1000
SORT pagerank DESC
LIMIT 100
RETURN {
username: influencer.username,
follower_count,
reach_count,
pagerank,
clustering_coefficient: clustering,
avg_follower_distance_km: avg_follower_distance / 1000,
engagement,
influence_score: pagerank * LOG(follower_count + 1) * (engagement.likes / (engagement.posts + 1))
}
ThemisDB unterstützt vollständige Window Functions wie PostgreSQL.
-- Umsatzanalyse mit Window Functions
FOR sale IN sales
LET sale_date = DATE_TIMESTAMP(sale.created_at)
-- Gruppierung nach Region und Monat
COLLECT
region = sale.region,
month = DATE_TRUNC(sale_date, "month")
AGGREGATE
revenue = SUM(sale.amount),
orders = COUNT(1),
avg_order = AVG(sale.amount)
-- Window Functions
LET prev_month_revenue = LAG(revenue, 1) OVER (PARTITION BY region ORDER BY month)
LET next_month_revenue = LEAD(revenue, 1) OVER (PARTITION BY region ORDER BY month)
LET running_total = RUNNING_SUM(revenue) OVER (PARTITION BY region ORDER BY month)
LET rank_in_region = ROW_NUMBER() OVER (PARTITION BY region ORDER BY revenue DESC)
LET percentile_rank = PERCENT_RANK() OVER (ORDER BY revenue)
-- Berechnete Metriken
LET mom_growth = prev_month_revenue ? ((revenue - prev_month_revenue) / prev_month_revenue * 100) : null
LET yoy_revenue = LAG(revenue, 12) OVER (PARTITION BY region ORDER BY month)
LET yoy_growth = yoy_revenue ? ((revenue - yoy_revenue) / yoy_revenue * 100) : null
SORT region, month
RETURN {
region,
month: DATE_FORMAT(month, "%Y-%m"),
revenue,
orders,
avg_order: ROUND(avg_order, 2),
prev_month_revenue,
mom_growth: mom_growth ? CONCAT(ROUND(mom_growth, 1), "%") : "N/A",
yoy_growth: yoy_growth ? CONCAT(ROUND(yoy_growth, 1), "%") : "N/A",
running_total,
rank_in_region,
percentile: ROUND(percentile_rank * 100, 1)
}
ThemisDB kann mit Dateimetadaten direkt im Query arbeiten.
-- Datei-Repository analysieren
FOR file IN files
LET path_parts = PATH_SPLIT(file.path)
LET ext = FILE_EXT(file.name)
LET mime = MIME_TYPE(file.name)
LET size_human = FORMAT_FILESIZE(file.size)
-- Kategorisierung
LET category = (
IS_IMAGE(file.name) ? "images" :
IS_VIDEO(file.name) ? "videos" :
IS_AUDIO(file.name) ? "audio" :
IS_DOCUMENT(file.name) ? "documents" :
ext IN ["zip", "tar", "gz", "7z"] ? "archives" :
ext IN ["js", "py", "java", "cpp", "h"] ? "code" :
"other"
)
-- Duplikat-Erkennung via Hash
COLLECT
hash = file.content_hash
INTO duplicates
LET is_duplicate = LENGTH(duplicates) > 1
FOR dup IN duplicates
LET f = dup.file
RETURN {
path: f.path,
name: f.name,
extension: ext,
mime_type: mime,
size: f.size,
size_human,
category,
is_duplicate,
duplicate_count: LENGTH(duplicates),
created: f.created_at,
modified: f.modified_at
}
┌──────────────────────────────────────────────────────────────────────┐
│ AQL Query │
│ FOR doc IN collection FILTER ... LET x = FUNC(...) RETURN ... │
└────────────────────────────────┬─────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────┐
│ Query Parser │
│ - Lexikalische Analyse │
│ - Syntaxbaum (AST) erstellen │
│ - Semantische Validierung │
└────────────────────────────────┬─────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────┐
│ Query Optimizer │
│ - Index-Auswahl │
│ - Join-Reihenfolge │
│ - Filter-Pushdown │
│ - Subquery-Optimierung │
└────────────────────────────────┬─────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────┐
│ Execution Engine │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ FunctionRegistry │ │
│ │ ┌─────────┬─────────┬─────────┬─────────┬─────────────────┐ │ │
│ │ │ String │ Math │ Array │ Date │ Document │ │ │
│ │ │ (~15) │ (~25) │ (~20) │ (~15) │ (~20) │ │ │
│ │ ├─────────┼─────────┼─────────┼─────────┼─────────────────┤ │ │
│ │ │ Geo │ CRS │ Vector │ Graph │ Relational │ │ │
│ │ │ (~25) │ (~10) │ (~20) │ (~15) │ (~25) │ │ │
│ │ ├─────────┴─────────┴─────────┴─────────┴─────────────────┤ │ │
│ │ │ File (~20) │ │ │
│ │ └──────────────────────────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────────────────────────────┘ │
└────────────────────────────────┬─────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────┐
│ Storage Engines │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────────────┐ │
│ │ Document │ │ Graph │ │ Vector │ │ Geo (R-Tree) │ │
│ │ Store │ │ Index │ │ Index │ │ │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────────────────┘ │
└──────────────────────────────────────────────────────────────────────┘
Jede AQL-Funktion ist eine eigene Klasse mit definierter Schnittstelle:
// Interface für alle Funktionen
class IFunction {
public:
virtual ~IFunction() = default;
// Funktionsname (z.B. "LENGTH", "ST_DISTANCE")
virtual std::string getName() const = 0;
// Beschreibung für Dokumentation
virtual std::string getDescription() const = 0;
// Erlaubte Signaturen
virtual std::vector<FunctionSignature> getSignatures() const = 0;
// Ausführung
virtual JsonValue execute(
const std::vector<JsonValue>& args,
const FunctionContext& ctx
) const = 0;
};
// Beispiel: LENGTH-Funktion
class LengthFunction : public IFunction {
public:
std::string getName() const override { return "LENGTH"; }
std::string getDescription() const override {
return "Returns length of string, array, or object";
}
std::vector<FunctionSignature> getSignatures() const override {
return {
{ {ArgType::STRING}, ReturnType::NUMBER },
{ {ArgType::ARRAY}, ReturnType::NUMBER },
{ {ArgType::OBJECT}, ReturnType::NUMBER }
};
}
JsonValue execute(const std::vector<JsonValue>& args, const FunctionContext& ctx) const override {
if (args[0].isString()) return args[0].asString().length();
if (args[0].isArray()) return args[0].asArray().size();
if (args[0].isObject()) return args[0].asObject().size();
throw FunctionError("LENGTH requires string, array, or object");
}
};Vorteile dieses Designs:
- ✅ Single Responsibility: Jede Funktion in eigener Klasse
- ✅ Open/Closed: Neue Funktionen ohne Änderung bestehenden Codes
- ✅ Testbarkeit: Jede Funktion isoliert testbar
- ✅ Dokumentation: Automatisch aus Metadaten generierbar
- ✅ Plugin-fähig: Externe Funktionen registrierbar
| Aufgabe | ThemisDB AQL | Neo4j Cypher | Anmerkung |
|---|---|---|---|
| Einfache Traversierung | FOR v IN 1..5 OUTBOUND start knows RETURN v |
MATCH (start)-[:knows*1..5]->(v) RETURN v |
Ähnliche Syntax |
| Mit Geo-Filter | FILTER GEO_DISTANCE(v.loc, @point) < 1000 |
❌ Nicht möglich ohne Plugin | ThemisDB: Native Integration |
| Mit Vector-Similarity | LET sim = COSINE_SIMILARITY(v.emb, @vec) |
❌ Braucht externes System | ThemisDB: Native Vektorsuche |
| Shortest Path | SHORTEST_PATH(a, b, "knows") |
shortestPath((a)-[:knows*]-(b)) |
Beide nativ unterstützt |
| Aggregation | COLLECT ... AGGREGATE SUM(), AVG() |
WITH ... COLLECT |
ThemisDB: SQL-ähnlicher |
| Window Functions | ROW_NUMBER() OVER (...) |
❌ Nicht verfügbar | ThemisDB exklusiv |
| Subqueries | LET x = (FOR ...) |
CALL { ... } |
Beide unterstützt |
| CRS Transformation | ST_TRANSFORM(geom, 25832, 4326) |
❌ Nicht verfügbar | ThemisDB exklusiv |
Migration von Cypher zu AQL:
// Neo4j Cypher
MATCH (p:Person)-[:KNOWS*1..3]->(friend:Person)
WHERE p.name = 'Alice'
AND friend.age > 30
RETURN friend.name, friend.age
ORDER BY friend.age DESC
LIMIT 10// ThemisDB AQL
FOR p IN persons
FILTER p.name == "Alice"
FOR friend IN 1..3 OUTBOUND p knows
FILTER friend.age > 30
SORT friend.age DESC
LIMIT 10
RETURN { name: friend.name, age: friend.age }
| Aufgabe | ThemisDB AQL | PostgreSQL | Anmerkung |
|---|---|---|---|
| JSON-Dokumente | Native |
jsonb Typ |
ThemisDB: Schema-frei |
| Graph-Traversal | FOR v IN OUTBOUND |
WITH RECURSIVE (komplex!) |
ThemisDB: Eleganter |
| Vector-Search | COSINE_SIMILARITY() |
pgvector Extension |
Beide gut |
| Geo-Operationen |
ST_* Funktionen |
PostGIS Extension | Beide OGC-kompatibel |
| CRS Transformation | ST_TRANSFORM() |
PostGIS ST_Transform()
|
Beide vollständig |
| Window Functions |
ROW_NUMBER, LAG, LEAD
|
Vollständig | Beide vollständig |
| Alles kombiniert | ✅ Eine Query | ❌ Mehrere Queries/CTEs | ThemisDB: Einfacher |
Migration von SQL zu AQL:
-- PostgreSQL
SELECT c.name, o.total,
ROW_NUMBER() OVER (ORDER BY o.total DESC) as rank
FROM customers c
JOIN (
SELECT customer_id, SUM(amount) as total
FROM orders
GROUP BY customer_id
) o ON c.id = o.customer_id
WHERE o.total > 1000
ORDER BY o.total DESC;-- ThemisDB AQL
FOR c IN customers
LET orderTotal = SUM(
FOR o IN orders
FILTER o.customer_id == c._key
RETURN o.amount
)
FILTER orderTotal > 1000
LET rank = ROW_NUMBER() OVER (ORDER BY orderTotal DESC)
SORT orderTotal DESC
RETURN { name: c.name, total: orderTotal, rank }
| Aufgabe | ThemisDB AQL | MongoDB | Anmerkung |
|---|---|---|---|
| Syntax | SQL-ähnlich, lesbar | JSON-basiert, verschachtelt | ThemisDB: Lesbarer |
| Graph | Native Traversal |
$graphLookup (limitiert) |
ThemisDB: Mächtiger |
| Aggregation |
COLLECT, AGGREGATE
|
Pipeline Stages | Beide mächtig |
| Joins | FOR ... FOR |
$lookup |
ThemisDB: Flexibler |
| Window Functions |
ROW_NUMBER, LAG, LEAD
|
❌ Nicht verfügbar | ThemisDB exklusiv |
| Vector Search | Native | Atlas Search (Cloud) | ThemisDB: On-premise möglich |
| Geo | OGC ST_* Funktionen | GeoJSON-basiert | Beide gut |
Migration von MongoDB Aggregation zu AQL:
// MongoDB Aggregation Pipeline
db.orders.aggregate([
{ $match: { status: "completed" } },
{ $group: {
_id: "$customer_id",
total: { $sum: "$amount" },
count: { $sum: 1 }
}},
{ $lookup: {
from: "customers",
localField: "_id",
foreignField: "_id",
as: "customer"
}},
{ $unwind: "$customer" },
{ $sort: { total: -1 } },
{ $limit: 10 },
{ $project: {
customerName: "$customer.name",
total: 1,
count: 1
}}
])// ThemisDB AQL - Deutlich lesbarer
FOR o IN orders
FILTER o.status == "completed"
COLLECT customerId = o.customer_id
AGGREGATE
total = SUM(o.amount),
count = COUNT(1)
LET customer = DOCUMENT("customers", customerId)
SORT total DESC
LIMIT 10
RETURN {
customerName: customer.name,
total,
count
}
| Aufgabe | ThemisDB AQL | ArangoDB AQL | Anmerkung |
|---|---|---|---|
| Graph Traversal | Identische Syntax | Identische Syntax | Kompatibel |
| Vector Search | Native | Nur über Views | ThemisDB: Einfacher |
| CRS Transformation |
ST_TRANSFORM() mit Helmert |
Nur WGS84 | ThemisDB: Vollständiger |
| Process Mining | Native BPMN | ❌ Nicht verfügbar | ThemisDB exklusiv |
| Window Functions |
ROW_NUMBER, LAG, LEAD
|
❌ Nicht verfügbar | ThemisDB exklusiv |
| File Functions |
MIME_TYPE, PATH_*
|
❌ Nicht verfügbar | ThemisDB exklusiv |
| Aufgabe | ThemisDB | Pinecone/Weaviate | Anmerkung |
|---|---|---|---|
| Vector Search | COSINE_SIMILARITY() |
Native API | Beide schnell |
| Metadata Filter | AQL Filter-Syntax | Eigene Filter-Syntax | ThemisDB: Mächtiger |
| Joins | Native | ❌ Nicht möglich | ThemisDB exklusiv |
| Geo-Filter | GEO_DISTANCE() |
Limitiert | ThemisDB: Vollständig |
| Graph | Native Traversal | ❌ Nicht möglich | ThemisDB exklusiv |
| Aggregation | COLLECT AGGREGATE |
❌ Nur Count | ThemisDB: Vollständig |
Typische Pinecone-Abfrage in ThemisDB:
# Pinecone Python
results = index.query(
vector=query_embedding,
top_k=10,
filter={"category": "electronics", "price": {"$lt": 100}}
)-- ThemisDB AQL - Mit zusätzlichen Möglichkeiten
FOR doc IN products
FILTER doc.category == "electronics"
FILTER doc.price < 100
LET similarity = COSINE_SIMILARITY(doc.embedding, @queryEmbedding)
FILTER similarity > 0.7
SORT similarity DESC
LIMIT 10
-- Zusätzlich: Ähnliche Produkte über Graph
LET related = (
FOR r IN 1..2 OUTBOUND doc similar_to
RETURN r.name
)
-- Zusätzlich: Durchschnittliche Bewertung
LET avgRating = AVG(
FOR review IN reviews
FILTER review.product_id == doc._key
RETURN review.rating
)
RETURN {
doc,
similarity,
related: SLICE(related, 0, 5),
avgRating
}
ThemisDB bietet ~210 Funktionen in 11 Kategorien:
| Kategorie | Anzahl | Beschreibung | Haupt-Anwendungsfälle |
|---|---|---|---|
| String | ~15 | Textmanipulation, Pattern Matching, Fuzzy Search | Datenbereinigung, Suche, Validierung |
| Math | ~25 | Arithmetik, Trigonometrie, Statistik | Berechnungen, Analysen, ML-Features |
| Array | ~20 | Listen-Operationen, Set-Funktionen | Datenstruktur-Manipulation |
| Date | ~15 | Datum/Zeit-Verarbeitung, Formatierung | Zeitreihen, Berichte, Scheduling |
| Document | ~20 | Objektmanipulation, Typ-Prüfungen | Schema-Validierung, Transformation |
| Geo | ~25 | Räumliche Operationen (OGC-kompatibel) | GIS, Location-Services, Routing |
| CRS | ~10 | Koordinatentransformationen | Vermessung, Kataster, Kartografie |
| Vector | ~20 | ML-Embeddings, Ähnlichkeitssuche | Semantic Search, Recommendations |
| Graph | ~15 | Traversierung, Zentralität, Pfade | Social Networks, Fraud Detection |
| Relational | ~25 | SQL-Joins, Aggregation, Window | Business Analytics, Reporting |
| File | ~20 | Pfade, MIME-Typen, Dateigrößen | Document Management, Storage |
// Iteration über Collection
FOR variable IN collection
// Filterung
FILTER variable.field == "value"
FILTER variable.number > 10
// Berechnungen und Zwischenvariablen
LET computed = FUNCTION(variable.field)
LET subquery = (
FOR sub IN other_collection
FILTER sub.ref == variable._key
RETURN sub
)
// Sortierung
SORT variable.field ASC
// Limitierung
LIMIT 10
// Rückgabe
RETURN {
id: variable._key,
computed,
subquery
}
AQL unterstützt vollständig verschachtelte inline Object- und Array-Literale im RETURN:
// Einfaches Objekt-Literal
RETURN { name: "Alice", age: 30 }
// Verschachteltes Objekt mit Array
RETURN {
user: {
name: doc.name,
contact: {
email: doc.email,
phone: doc.phone
}
},
tags: ["active", "premium"],
metadata: {
created: NOW(),
version: 1
}
}
// Array von Objekten
RETURN [
{ type: "primary", value: doc.email },
{ type: "secondary", value: doc.phone }
]
// Dynamische Verschachtelung mit Funktionen
FOR user IN users
LET orders = (FOR o IN orders FILTER o.user == user._key RETURN o)
RETURN {
profile: {
id: user._key,
name: CONCAT(user.firstName, " ", user.lastName),
initials: CONCAT(SUBSTRING(user.firstName, 0, 1), SUBSTRING(user.lastName, 0, 1))
},
statistics: {
orderCount: LENGTH(orders),
totalSpent: SUM(orders[*].amount),
avgOrder: AVG(orders[*].amount)
},
recentOrders: (
FOR o IN orders
SORT o.date DESC
LIMIT 3
RETURN { id: o._key, date: o.date, amount: o.amount }
),
flags: [
user.isActive ? "active" : "inactive",
LENGTH(orders) > 10 ? "frequent" : "occasional"
]
}
Wichtig: Die Syntax ist JSON-ähnlich, aber mit AQL-Erweiterungen:
- Schlüssel ohne Anführungszeichen:
{ name: "value" }statt{ "name": "value" } - Shorthand-Syntax:
{ name, age }entspricht{ name: name, age: age } - Ausdrücke als Werte:
{ total: SUM(values) } - Ternäre Operatoren:
{ status: isActive ? "on" : "off" }
| Kategorie | Operatoren | Beispiel |
|---|---|---|
| Vergleich |
==, !=, <, >, <=, >=
|
x == 5 |
| Logisch |
AND, OR, NOT, !
|
a > 5 AND b < 10 |
| Arithmetisch |
+, -, *, /, %
|
price * quantity |
| String |
+ (Konkatenation) |
firstName + " " + lastName |
| Ternär | ? : |
age >= 18 ? "adult" : "minor" |
| In |
IN, NOT IN
|
status IN ["active", "pending"] |
| Like | LIKE |
name LIKE "A%" |
| Range | .. |
FOR i IN 1..10 |
| Typ | Beispiel | AQL-Literal |
|---|---|---|
| null | Kein Wert | null |
| Boolean | Wahr/Falsch |
true, false
|
| Number | Ganz-/Fließkommazahl |
42, 3.14, -17
|
| String | Text |
"Hello", 'World'
|
| Array | Liste |
[1, 2, 3], ["a", "b"]
|
| Object | Dokument | { key: "value", num: 42 } |
// LET für Zwischenvariablen
LET x = 5
LET greeting = CONCAT("Hello, ", @userName)
LET result = (FOR doc IN docs RETURN doc.value)
// @ für Parameter (Query-Bindung)
FOR user IN users
FILTER user.age >= @minAge
FILTER user.country == @country
RETURN user
// Korrelierte Subquery
FOR user IN users
LET orders = (
FOR order IN orders
FILTER order.user_id == user._key
SORT order.date DESC
LIMIT 5
RETURN order
)
RETURN { user, recentOrders: orders }
// Aggregierte Subquery
FOR user IN users
LET totalSpent = SUM(
FOR order IN orders
FILTER order.user_id == user._key
RETURN order.amount
)
RETURN { user, totalSpent }
Die String-Funktionen bieten umfassende Textmanipulation von einfacher Verkettung bis zu Fuzzy-Matching mit Levenshtein-Distanz.
| Funktion | Beschreibung | Beispiel-Rückgabe |
|---|---|---|
LENGTH(str) |
Länge eines Strings | 11 |
CONCAT(...) |
Strings verketten | "Hello World" |
SUBSTRING(str, start, len) |
Teilstring extrahieren | "llo" |
UPPER(str) |
Großschreibung | "HELLO" |
LOWER(str) |
Kleinschreibung | "hello" |
TRIM(str) |
Leerzeichen entfernen | "text" |
LTRIM(str) |
Links trimmen | "text " |
RTRIM(str) |
Rechts trimmen | " text" |
SPLIT(str, sep) |
String teilen | ["a", "b", "c"] |
CONTAINS(str, search) |
Enthält Substring? |
true / false
|
STARTS_WITH(str, prefix) |
Beginnt mit? |
true / false
|
ENDS_WITH(str, suffix) |
Endet mit? |
true / false
|
REPLACE(str, old, new) |
Ersetzen | "Hello World" |
REGEX_TEST(str, pattern) |
Regex-Match? |
true / false
|
REGEX_REPLACE(str, pattern, replacement) |
Regex-Ersetzung | "processed" |
LEVENSHTEIN_DISTANCE(a, b) |
Edit-Distanz | 3 |
Gibt die Länge eines Strings, Arrays oder Objekts zurück.
Signatur:
LENGTH(value) → number
Parameter:
| Parameter | Typ | Beschreibung |
|---|---|---|
value |
string | array | object | Der zu messende Wert |
Rückgabewert: number - Die Länge
Verhalten nach Typ:
- String: Anzahl der UTF-8 Zeichen (nicht Bytes!)
- Array: Anzahl der Elemente
- Object: Anzahl der Eigenschaften (Top-Level)
Beispiele:
-- String-Länge
RETURN LENGTH("Hello World")
// Ergebnis: 11
-- Unicode-Strings (Zeichen, nicht Bytes)
RETURN LENGTH("Hëllö Wörld")
// Ergebnis: 11
RETURN LENGTH("你好世界")
// Ergebnis: 4
-- Emoji-Strings
RETURN LENGTH("👋🌍")
// Ergebnis: 2
-- Array-Länge
RETURN LENGTH([1, 2, 3, 4, 5])
// Ergebnis: 5
RETURN LENGTH([])
// Ergebnis: 0
-- Objekt-Eigenschaften zählen
RETURN LENGTH({ name: "Max", age: 30 })
// Ergebnis: 2
-- Verschachtelte Objekte (nur Top-Level)
RETURN LENGTH({ user: { name: "Max", age: 30 }, active: true })
// Ergebnis: 2 (nicht 3!)
-- Null-Handling
RETURN LENGTH(null)
// Ergebnis: 0
Praxisbeispiele:
-- Kurze Beschreibungen finden
FOR product IN products
FILTER LENGTH(product.description) < 50
RETURN { id: product._key, description: product.description }
-- Validierung: Mindestlänge
FOR user IN users
FILTER LENGTH(user.password_hash) < 60
RETURN { user: user.email, warning: "Passwort-Hash zu kurz" }
-- Array-Größen analysieren
FOR order IN orders
LET itemCount = LENGTH(order.items)
COLLECT bucket = FLOOR(itemCount / 5) * 5
AGGREGATE count = COUNT(1)
SORT bucket
RETURN { items: CONCAT(bucket, "-", bucket + 4), orders: count }
Edge Cases:
RETURN LENGTH("") // 0
RETURN LENGTH(" ") // 1
RETURN LENGTH(" ") // 3
RETURN LENGTH([null, null]) // 2 (Elemente zählen, auch null)
RETURN LENGTH({}) // 0
Verbindet mehrere Werte zu einem String. Null-Werte werden als leerer String behandelt.
Signatur:
CONCAT(value1, value2, ...) → string
CONCAT(array) → string
Parameter:
| Parameter | Typ | Beschreibung |
|---|---|---|
values |
any... | Beliebig viele Werte |
array |
array | Array von Werten |
Rückgabewert: string - Der verkettete String
Typ-Konvertierung:
- string: Unverändert
- number: Als Dezimalzahl
-
boolean:
"true"oder"false" -
null: Leerer String
"" - array: JSON-Darstellung
- object: JSON-Darstellung
Beispiele:
-- Einfache Verkettung
RETURN CONCAT("Hello", " ", "World")
// Ergebnis: "Hello World"
-- Mit Variablen
FOR user IN users
RETURN CONCAT(user.firstName, " ", user.lastName)
// Ergebnis: "Max Mustermann"
-- Zahlen einbinden
RETURN CONCAT("Preis: ", 42.50, " EUR")
// Ergebnis: "Preis: 42.5 EUR"
-- Null-Handling (ignoriert null)
RETURN CONCAT("Hello", null, "World")
// Ergebnis: "HelloWorld"
-- Array als Eingabe
RETURN CONCAT(["Hello", " ", "World"])
// Ergebnis: "Hello World"
-- Boolean einbinden
RETURN CONCAT("Status: ", true)
// Ergebnis: "Status: true"
Praxisbeispiele:
-- Vollständige Adresse generieren
FOR customer IN customers
LET address = CONCAT(
customer.street, " ", customer.houseNumber, "\n",
customer.zipCode, " ", customer.city, "\n",
customer.country
)
RETURN { customer: customer.name, address }
-- URL generieren
FOR product IN products
LET url = CONCAT(
"https://shop.example.com/products/",
product.category, "/",
product.slug
)
RETURN { product: product.name, url }
-- Log-Nachricht erstellen
FOR event IN events
LET logLine = CONCAT(
"[", DATE_FORMAT(event.timestamp, "%Y-%m-%d %H:%M:%S"), "] ",
"[", UPPER(event.level), "] ",
event.message
)
RETURN logLine
// Ergebnis: "[2024-01-15 10:30:45] [ERROR] Connection timeout"
Verwandte Funktionen:
-
CONCAT_SEPARATOR()- Mit Trennzeichen -
+Operator - Alternative für zwei Strings
Extrahiert einen Teilstring ab einer Position.
Signatur:
SUBSTRING(str, start) → string
SUBSTRING(str, start, length) → string
Parameter:
| Parameter | Typ | Beschreibung | Default |
|---|---|---|---|
str |
string | Quell-String | - |
start |
number | Startposition (0-basiert) | - |
length |
number | Anzahl Zeichen | Bis Ende |
Rückgabewert: string - Der extrahierte Teilstring
Besonderheiten:
- Negative
start-Werte: Vom Ende zählen -
length> verfügbare Zeichen: Bis Ende -
start> String-Länge: Leerer String
Beispiele:
-- Einfache Extraktion
RETURN SUBSTRING("ThemisDB", 0, 6)
// Ergebnis: "Themis"
RETURN SUBSTRING("ThemisDB", 6)
// Ergebnis: "DB"
-- Negative Startposition
RETURN SUBSTRING("ThemisDB", -2)
// Ergebnis: "DB"
RETURN SUBSTRING("ThemisDB", -5, 3)
// Ergebnis: "mis"
-- Über String-Ende hinaus
RETURN SUBSTRING("Hello", 3, 100)
// Ergebnis: "lo"
-- Leeres Ergebnis
RETURN SUBSTRING("Hello", 10)
// Ergebnis: ""
Praxisbeispiele:
-- Vorwahl extrahieren (deutsche Telefonnummern)
FOR contact IN contacts
LET phone = contact.phone
LET areaCode = (
STARTS_WITH(phone, "+49") ? SUBSTRING(phone, 3, 4) :
STARTS_WITH(phone, "0") ? SUBSTRING(phone, 0, 4) :
null
)
RETURN { name: contact.name, phone, areaCode }
-- Dateiname kürzen für Anzeige
FOR file IN files
LET shortName = LENGTH(file.name) > 30
? CONCAT(SUBSTRING(file.name, 0, 27), "...")
: file.name
RETURN { id: file._key, displayName: shortName }
-- Jahr aus Datumsstring extrahieren
FOR event IN events
LET year = SUBSTRING(event.date_string, 0, 4)
COLLECT year = year
AGGREGATE count = COUNT(1)
RETURN { year, count }
Konvertiert String zu Groß- oder Kleinschreibung.
Signatur:
UPPER(str) → string
LOWER(str) → string
Parameter:
| Parameter | Typ | Beschreibung |
|---|---|---|
str |
string | Der zu konvertierende String |
Rückgabewert: string - Der konvertierte String
Unicode-Unterstützung: Vollständige Unicode-Unterstützung inklusive:
- Deutsche Umlaute (ä→Ä, ö→Ö, ü→Ü, ß→SS)
- Akzentzeichen (é→É, ñ→Ñ)
- Griechische, kyrillische und andere Alphabete
Beispiele:
RETURN UPPER("hello world")
// Ergebnis: "HELLO WORLD"
RETURN LOWER("HELLO WORLD")
// Ergebnis: "hello world"
-- Deutsche Umlaute
RETURN UPPER("größe")
// Ergebnis: "GRÖSSE" (ß → SS)
RETURN LOWER("GRÖSSE")
// Ergebnis: "grösse"
-- Mixed Case normalisieren
RETURN LOWER("ThEmIsDb")
// Ergebnis: "themisdb"
Praxisbeispiele:
-- Case-insensitive Suche
FOR product IN products
FILTER LOWER(product.name) == LOWER(@searchTerm)
RETURN product
-- E-Mail-Normalisierung
FOR user IN users
UPDATE user WITH { email: LOWER(user.email) } IN users
-- Titel-Formatierung (First Letter Uppercase)
FOR article IN articles
LET words = SPLIT(article.title, " ")
LET formatted = (
FOR word IN words
RETURN CONCAT(UPPER(SUBSTRING(word, 0, 1)), LOWER(SUBSTRING(word, 1)))
)
RETURN {
original: article.title,
formatted: CONCAT_SEPARATOR(" ", formatted)
}
Entfernt Leerzeichen oder spezifische Zeichen von Strings.
Signatur:
TRIM(str) → string
TRIM(str, chars) → string
LTRIM(str) → string
LTRIM(str, chars) → string
RTRIM(str) → string
RTRIM(str, chars) → string
Parameter:
| Parameter | Typ | Beschreibung | Default |
|---|---|---|---|
str |
string | Der zu trimmende String | - |
chars |
string | Zu entfernende Zeichen | Whitespace |
Rückgabewert: string - Der getrimmte String
Whitespace-Definition: Standardmäßig werden entfernt: Space, Tab, Newline, Carriage Return, Form Feed
Beispiele:
-- Basis-Trim
RETURN TRIM(" hello ")
// Ergebnis: "hello"
RETURN LTRIM(" hello ")
// Ergebnis: "hello "
RETURN RTRIM(" hello ")
// Ergebnis: " hello"
-- Newlines entfernen
RETURN TRIM(" \n hello \n ")
// Ergebnis: "hello"
-- Spezifische Zeichen entfernen
RETURN TRIM("---hello---", "-")
// Ergebnis: "hello"
RETURN LTRIM("000123", "0")
// Ergebnis: "123"
RETURN RTRIM("price$$$", "$")
// Ergebnis: "price"
-- Mehrere Zeichen
RETURN TRIM("<<hello>>", "<>")
// Ergebnis: "hello"
Praxisbeispiele:
-- Datenbereinigung bei Import
FOR record IN raw_import
UPDATE record WITH {
name: TRIM(record.name),
email: TRIM(LOWER(record.email)),
phone: TRIM(REPLACE(record.phone, " ", ""))
} IN raw_import
-- CSV-Werte bereinigen
FOR line IN csv_lines
LET values = SPLIT(line, ",")
LET cleaned = (FOR v IN values RETURN TRIM(v))
RETURN cleaned
-- Führende Nullen entfernen (z.B. Artikelnummern)
FOR product IN products
LET cleanedSku = LTRIM(product.sku, "0")
UPDATE product WITH { sku_clean: cleanedSku } IN products
Teilt einen String in ein Array anhand eines Trennzeichens.
Signatur:
SPLIT(str, separator) → array
SPLIT(str, separator, limit) → array
Parameter:
| Parameter | Typ | Beschreibung | Default |
|---|---|---|---|
str |
string | Der zu teilende String | - |
separator |
string | Das Trennzeichen | - |
limit |
number | Maximale Teile | Unbegrenzt |
Rückgabewert: array - Array von Strings
Besonderheiten:
- Leerer Separator teilt in einzelne Zeichen
- Separator am Ende erzeugt leeres Element
- Aufeinanderfolgende Separatoren erzeugen leere Elemente
Beispiele:
-- Einfaches Teilen
RETURN SPLIT("a,b,c", ",")
// Ergebnis: ["a", "b", "c"]
-- Mit Limit
RETURN SPLIT("a,b,c,d,e", ",", 3)
// Ergebnis: ["a", "b", "c,d,e"]
-- Leere Elemente
RETURN SPLIT("a,,b", ",")
// Ergebnis: ["a", "", "b"]
-- Leerer String
RETURN SPLIT("", ",")
// Ergebnis: [""]
-- In Zeichen teilen
RETURN SPLIT("hello", "")
// Ergebnis: ["h", "e", "l", "l", "o"]
Praxisbeispiele:
-- E-Mail-Domain extrahieren
FOR user IN users
LET parts = SPLIT(user.email, "@")
RETURN {
user: parts[0],
domain: parts[1],
topLevel: LAST(SPLIT(parts[1], "."))
}
-- Tags parsen (kommasepariert)
FOR article IN articles
LET tags = (
FOR tag IN SPLIT(article.tags_string, ",")
RETURN TRIM(tag)
)
RETURN { title: article.title, tags }
-- Pfad-Komponenten
FOR file IN files
LET pathParts = SPLIT(file.path, "/")
RETURN {
file: LAST(pathParts),
folder: NTH(pathParts, LENGTH(pathParts) - 2),
depth: LENGTH(pathParts) - 1
}
-- IP-Adresse parsen
FOR log IN access_logs
LET octets = SPLIT(log.ip_address, ".")
FILTER TO_NUMBER(octets[0]) == 192
FILTER TO_NUMBER(octets[1]) == 168
RETURN log
Prüft ob ein String einen Teilstring enthält oder mit bestimmten Zeichen beginnt/endet.
Signatur:
CONTAINS(str, search) → bool
CONTAINS(str, search, returnIndex) → bool | number
STARTS_WITH(str, prefix) → bool
ENDS_WITH(str, suffix) → bool
Parameter:
| Parameter | Typ | Beschreibung |
|---|---|---|
str |
string | Der zu durchsuchende String |
search/prefix/suffix
|
string | Der gesuchte String |
returnIndex |
boolean | Wenn true, Position statt bool |
Rückgabewert: bool oder number (Position, -1 wenn nicht gefunden)
Beispiele:
-- Einfache Prüfung
RETURN CONTAINS("Hello World", "World")
// Ergebnis: true
RETURN CONTAINS("Hello World", "world")
// Ergebnis: false (case-sensitive!)
-- Position zurückgeben
RETURN CONTAINS("Hello World", "World", true)
// Ergebnis: 6
RETURN CONTAINS("Hello World", "xyz", true)
// Ergebnis: -1
-- Prefix-Prüfung
RETURN STARTS_WITH("ThemisDB", "Themis")
// Ergebnis: true
RETURN STARTS_WITH("ThemisDB", "themis")
// Ergebnis: false
-- Suffix-Prüfung
RETURN ENDS_WITH("document.pdf", ".pdf")
// Ergebnis: true
RETURN ENDS_WITH("document.pdf", ".PDF")
// Ergebnis: false
Praxisbeispiele:
-- Case-insensitive Suche
FOR product IN products
FILTER CONTAINS(LOWER(product.description), LOWER(@searchTerm))
RETURN product
-- Dateitypen filtern
FOR file IN files
FILTER ENDS_WITH(LOWER(file.name), ".pdf") OR
ENDS_WITH(LOWER(file.name), ".doc") OR
ENDS_WITH(LOWER(file.name), ".docx")
RETURN file
-- URLs analysieren
FOR url IN urls
LET isSecure = STARTS_WITH(url.href, "https://")
LET domain = (
LET withoutProtocol = STARTS_WITH(url.href, "https://")
? SUBSTRING(url.href, 8)
: SUBSTRING(url.href, 7)
LET endPos = CONTAINS(withoutProtocol, "/", true)
RETURN endPos > 0 ? SUBSTRING(withoutProtocol, 0, endPos) : withoutProtocol
)
RETURN { url: url.href, isSecure, domain }
-- E-Mail-Validation (einfach)
FOR user IN users
LET email = user.email
FILTER CONTAINS(email, "@")
FILTER CONTAINS(email, ".")
FILTER NOT STARTS_WITH(email, "@")
FILTER NOT ENDS_WITH(email, ".")
RETURN user
Ersetzt alle Vorkommen eines Substrings.
Signatur:
REPLACE(str, search, replacement) → string
REPLACE(str, search, replacement, limit) → string
Parameter:
| Parameter | Typ | Beschreibung | Default |
|---|---|---|---|
str |
string | Der Quell-String | - |
search |
string | Der zu ersetzende String | - |
replacement |
string | Der Ersetzungs-String | - |
limit |
number | Max. Ersetzungen | Alle |
Rückgabewert: string - Der modifizierte String
Beispiele:
-- Einfache Ersetzung
RETURN REPLACE("Hello World", "World", "ThemisDB")
// Ergebnis: "Hello ThemisDB"
-- Mehrfache Ersetzung
RETURN REPLACE("aaa", "a", "b")
// Ergebnis: "bbb"
-- Mit Limit
RETURN REPLACE("aaa", "a", "b", 2)
// Ergebnis: "bba"
-- Zeichen entfernen
RETURN REPLACE("Hello World", " ", "")
// Ergebnis: "HelloWorld"
-- Leerer Ersetzungsstring
RETURN REPLACE("H-e-l-l-o", "-", "")
// Ergebnis: "Hello"
Praxisbeispiele:
-- Telefonnummern normalisieren
FOR contact IN contacts
LET phone = contact.phone
LET normalized = REPLACE(REPLACE(REPLACE(
phone, " ", ""), "-", ""), "/", "")
UPDATE contact WITH { phone_normalized: normalized } IN contacts
-- Slugs generieren
FOR article IN articles
LET slug = LOWER(REPLACE(REPLACE(REPLACE(
article.title, " ", "-"), "ä", "ae"), "ü", "ue"))
UPDATE article WITH { slug } IN articles
-- Sensible Daten maskieren
FOR user IN users
LET maskedEmail = CONCAT(
SUBSTRING(user.email, 0, 2),
REPLACE(SUBSTRING(user.email, 2, CONTAINS(user.email, "@", true) - 2),
REGEX_REPLACE(SUBSTRING(user.email, 2, CONTAINS(user.email, "@", true) - 2), ".", "*")),
SUBSTRING(user.email, CONTAINS(user.email, "@", true))
)
RETURN { id: user._key, maskedEmail }
Reguläre Ausdrücke für Pattern Matching und Ersetzung.
Signatur:
REGEX_TEST(str, pattern) → bool
REGEX_TEST(str, pattern, ignoreCase) → bool
REGEX_REPLACE(str, pattern, replacement) → string
REGEX_REPLACE(str, pattern, replacement, ignoreCase) → string
Parameter:
| Parameter | Typ | Beschreibung | Default |
|---|---|---|---|
str |
string | Der zu prüfende String | - |
pattern |
string | Regex-Pattern | - |
replacement |
string | Ersetzungs-String | - |
ignoreCase |
boolean | Case-insensitive | false |
Rückgabewert: bool oder string
Regex-Syntax: ThemisDB unterstützt ECMAScript-kompatible reguläre Ausdrücke:
| Pattern | Bedeutung |
|---|---|
. |
Beliebiges Zeichen |
* |
0 oder mehr |
+ |
1 oder mehr |
? |
0 oder 1 |
^ |
Zeilenanfang |
$ |
Zeilenende |
[abc] |
Zeichenklasse |
[^abc] |
Negierte Zeichenklasse |
\d |
Ziffer |
\w |
Wortzeichen |
\s |
Whitespace |
(...) |
Gruppe |
\1, \2
|
Rückreferenz |
Beispiele:
-- E-Mail-Validierung
RETURN REGEX_TEST("user@example.com", "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$")
// Ergebnis: true
RETURN REGEX_TEST("invalid-email", "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$")
// Ergebnis: false
-- Telefonnummer-Format (deutsch)
RETURN REGEX_TEST("+49 123 456789", "^\\+49\\s*\\d{3,}\\s*\\d+$")
// Ergebnis: true
-- Case-insensitive
RETURN REGEX_TEST("Hello World", "hello", true)
// Ergebnis: true
-- Ersetzung mit Gruppen
RETURN REGEX_REPLACE("John Doe", "^(\\w+)\\s+(\\w+)$", "$2, $1")
// Ergebnis: "Doe, John"
-- Alle Zahlen entfernen
RETURN REGEX_REPLACE("abc123def456", "\\d+", "")
// Ergebnis: "abcdef"
-- Mehrere Leerzeichen zu einem
RETURN REGEX_REPLACE("Hello World", "\\s+", " ")
// Ergebnis: "Hello World"
Praxisbeispiele:
-- Verschiedene Datumsformate erkennen
FOR doc IN documents
LET dateStr = doc.date_field
LET format = (
REGEX_TEST(dateStr, "^\\d{4}-\\d{2}-\\d{2}$") ? "ISO" :
REGEX_TEST(dateStr, "^\\d{2}\\.\\d{2}\\.\\d{4}$") ? "DE" :
REGEX_TEST(dateStr, "^\\d{2}/\\d{2}/\\d{4}$") ? "US" :
"UNKNOWN"
)
RETURN { dateStr, format }
-- PLZ validieren (Deutschland)
FOR customer IN customers
LET validPLZ = REGEX_TEST(customer.zip, "^\\d{5}$")
FILTER NOT validPLZ
RETURN { customer: customer.name, invalidZip: customer.zip }
-- Kreditkartennummern maskieren
FOR transaction IN transactions
LET maskedCC = REGEX_REPLACE(
transaction.card_number,
"^(\\d{4})\\d{8}(\\d{4})$",
"$1********$2"
)
RETURN { id: transaction._key, card: maskedCC }
-- HTML-Tags entfernen
FOR article IN articles
LET plainText = REGEX_REPLACE(article.content, "<[^>]+>", "")
RETURN { title: article.title, plainText: SUBSTRING(plainText, 0, 200) }
Performance-Hinweis:
Regex-Operationen sind rechenintensiv. Für einfache Suchen ist CONTAINS() deutlich schneller.
Berechnet die Edit-Distanz (minimale Anzahl von Einfügungen, Löschungen, Ersetzungen) zwischen zwei Strings.
Signatur:
LEVENSHTEIN_DISTANCE(str1, str2) → number
Parameter:
| Parameter | Typ | Beschreibung |
|---|---|---|
str1 |
string | Erster String |
str2 |
string | Zweiter String |
Rückgabewert: number - Die Levenshtein-Distanz (0 = identisch)
Anwendungsfälle:
- Tippfehler-Toleranz
- Fuzzy-Suche
- Ähnlichkeitsanalyse
- Plagiatserkennung
Beispiele:
-- Identische Strings
RETURN LEVENSHTEIN_DISTANCE("hello", "hello")
// Ergebnis: 0
-- Ein Buchstabe anders
RETURN LEVENSHTEIN_DISTANCE("hello", "hallo")
// Ergebnis: 1
-- Klassisches Beispiel
RETURN LEVENSHTEIN_DISTANCE("kitten", "sitting")
// Ergebnis: 3 (k→s, e→i, +g)
-- Unterschiedliche Längen
RETURN LEVENSHTEIN_DISTANCE("abc", "abcdef")
// Ergebnis: 3
-- Komplett unterschiedlich
RETURN LEVENSHTEIN_DISTANCE("abc", "xyz")
// Ergebnis: 3
Praxisbeispiele:
-- Tippfehler-tolerante Suche
FOR product IN products
LET distance = LEVENSHTEIN_DISTANCE(LOWER(product.name), LOWER(@searchTerm))
FILTER distance <= 2 -- Max. 2 Änderungen
SORT distance ASC, product.popularity DESC
LIMIT 10
RETURN { product, distance, match: distance == 0 ? "exact" : "fuzzy" }
-- "Did you mean?" Vorschläge
LET searchTerm = "Thims"
FOR keyword IN search_keywords
LET distance = LEVENSHTEIN_DISTANCE(LOWER(keyword.term), LOWER(searchTerm))
FILTER distance > 0 AND distance <= 2
SORT distance ASC, keyword.frequency DESC
LIMIT 5
RETURN keyword.term
-- Duplikaterkennung
FOR doc1 IN documents
FOR doc2 IN documents
FILTER doc1._key < doc2._key -- Vermeidet Doppelvergleiche
LET titleSim = 1 - (LEVENSHTEIN_DISTANCE(doc1.title, doc2.title) /
MAX(LENGTH(doc1.title), LENGTH(doc2.title)))
FILTER titleSim > 0.8
RETURN {
doc1: doc1.title,
doc2: doc2.title,
similarity: ROUND(titleSim * 100)
}
-- Normalisierte Ähnlichkeit (0-1)
LET str1 = "ThemisDB"
LET str2 = "ThemisDatabase"
LET distance = LEVENSHTEIN_DISTANCE(str1, str2)
LET maxLen = MAX(LENGTH(str1), LENGTH(str2))
LET similarity = 1 - (distance / maxLen)
RETURN { str1, str2, distance, similarity: ROUND(similarity, 2) }
// Ergebnis: { str1: "ThemisDB", str2: "ThemisDatabase", distance: 6, similarity: 0.57 }
Performance-Hinweis: Levenshtein-Berechnung ist O(n*m) mit n und m als String-Längen. Für große Datensätze:
- Erst mit
LENGTH()vorfiltern - Erst mit
STARTS_WITH()vorfiltern - Index auf normalisierte Versionen erstellen
RETURN ABS(-42) -- 42
RETURN CEIL(4.2) -- 5
RETURN FLOOR(4.8) -- 4
RETURN ROUND(4.5) -- 5
RETURN SQRT(16) -- 4
RETURN POW(2, 10) -- 1024
RETURN LOG(2.718281828) -- ~1
RETURN LOG10(1000) -- 3
RETURN EXP(1) -- ~2.718
RETURN SIN(PI() / 2) -- 1
RETURN COS(0) -- 1
RETURN TAN(PI() / 4) -- ~1
RETURN ATAN2(1, 1) -- ~0.785 (45°)
-- Grad zu Radian und zurück
RETURN RADIANS(180) -- ~3.14159
RETURN DEGREES(PI()) -- 180
RETURN RANDOM() -- 0.0 bis 1.0
RETURN RAND_INT(1, 100) -- Zufällige Ganzzahl 1-100
-- Zufällige Stichprobe
FOR doc IN large_collection
FILTER RANDOM() < 0.01 -- 1% Stichprobe
RETURN doc
RETURN MIN(5, 3, 8, 1) -- 1
RETURN MAX(5, 3, 8, 1) -- 8
RETURN SUM([1, 2, 3, 4]) -- 10
RETURN AVG([1, 2, 3, 4]) -- 2.5
LET arr = [1, 2, 3, 4, 5]
RETURN FIRST(arr) -- 1
RETURN LAST(arr) -- 5
RETURN NTH(arr, 2) -- 3 (0-basiert)
LET arr = [1, 2, 3]
RETURN PUSH(arr, 4) -- [1, 2, 3, 4]
RETURN POP(arr) -- [1, 2]
RETURN SHIFT(arr) -- [2, 3]
RETURN UNSHIFT(arr, 0) -- [0, 1, 2, 3]
RETURN SLICE([1, 2, 3, 4, 5], 1, 3) -- [2, 3, 4]
RETURN FLATTEN([[1, 2], [3, 4]]) -- [1, 2, 3, 4]
RETURN UNIQUE([1, 2, 2, 3, 3, 3]) -- [1, 2, 3]
RETURN SORTED([3, 1, 4, 1, 5]) -- [1, 1, 3, 4, 5]
RETURN REVERSE_ARRAY([1, 2, 3]) -- [3, 2, 1]
LET a = [1, 2, 3]
LET b = [2, 3, 4]
RETURN UNION(a, b) -- [1, 2, 3, 4]
RETURN INTERSECTION(a, b) -- [2, 3]
RETURN MINUS(a, b) -- [1]
RETURN POSITION([10, 20, 30], 20) -- 1
RETURN COUNT([1, 2, 3, 4, 5]) -- 5
RETURN RANGE(1, 5) -- [1, 2, 3, 4, 5]
RETURN RANGE(0, 10, 2) -- [0, 2, 4, 6, 8, 10]
ThemisDB bietet ~45 Date-Funktionen mit SQL-kompatibler Syntax und erweiterten Arbeitstag-Berechnungen.
-- Grundfunktionen
RETURN DATE_NOW() -- Unix Timestamp in ms
RETURN NOW() -- SQL-Standard Alias
RETURN CURRENT_TIMESTAMP() -- SQL-Standard
RETURN CURRENT_DATE() -- Nur Datum (00:00:00 UTC)
RETURN CURRENT_TIME() -- Zeit seit Mitternacht (ms)
-- Praktische Helper
RETURN TODAY() -- Start von heute (00:00:00)
RETURN YESTERDAY() -- Start von gestern
RETURN TOMORROW() -- Start von morgen
-- DB-kompatible Aliase
RETURN GETDATE() -- SQL Server kompatibel
RETURN SYSDATE() -- Oracle kompatibel
RETURN UNIX_TIMESTAMP() -- MySQL kompatibel (Sekunden)
LET ts = DATE_TIMESTAMP("2024-06-15T14:30:00Z")
RETURN DATE_YEAR(ts) -- 2024
RETURN DATE_MONTH(ts) -- 6
RETURN DATE_DAY(ts) -- 15
RETURN DATE_HOUR(ts) -- 14
RETURN DATE_MINUTE(ts) -- 30
RETURN DATE_SECOND(ts) -- 0
RETURN DATE_MILLISECOND(ts) -- 0
RETURN DATE_DAYOFWEEK(ts) -- 6 (Samstag, 0=Sonntag)
RETURN DATE_DAYOFYEAR(ts) -- 167
RETURN DATE_QUARTER(ts) -- 2
RETURN DATE_WEEK(ts) -- 24 (ISO-Kalenderwoche)
-- Datum aus Komponenten
RETURN MAKE_DATE(2024, 12, 25) -- Weihnachten 2024
RETURN MAKE_DATETIME(2024, 12, 24, 18, 0) -- Heiligabend 18:00
RETURN MAKE_TIME(14, 30, 0) -- 14:30:00
-- Umrechnung
RETURN FROM_UNIXTIME(1700000000) -- Sekunden zu Timestamp
RETURN EPOCH_SECONDS(1700000000000) -- ms zu Sekunden
-- Einfache Syntax für relative Zeit
FOR event IN events
FILTER event.date >= NOW() - DAYS(7) -- Letzte 7 Tage
RETURN event
-- Alle Interval-Funktionen
RETURN YEARS(1) -- 1 Jahr in ms (~31,557,600,000)
RETURN MONTHS(3) -- 3 Monate in ms
RETURN WEEKS(2) -- 2 Wochen in ms
RETURN DAYS(10) -- 10 Tage in ms
RETURN HOURS(24) -- 24 Stunden in ms
RETURN MINUTES(30) -- 30 Minuten in ms
RETURN SECONDS(60) -- 60 Sekunden in ms
-- Flexibles INTERVAL
RETURN INTERVAL(6, "months") -- Wie MONTHS(6)
RETURN INTERVAL(2.5, "weeks") -- 2.5 Wochen
-- Praktische Kombination
FOR order IN orders
FILTER order.created >= TODAY() - WEEKS(2) -- Letzte 2 Wochen
FILTER order.deadline < TOMORROW() -- Fällig bis morgen
RETURN order
LET now = DATE_NOW()
-- Mit DATE_ADD/SUBTRACT
RETURN DATE_ADD(now, 7, "days")
RETURN DATE_SUBTRACT(now, 1, "months")
-- Mit Interval-Funktionen (eleganter)
RETURN now + DAYS(7) -- 7 Tage in die Zukunft
RETURN now - MONTHS(1) -- 1 Monat zurück
RETURN now + YEARS(1) -- In einem Jahr
-- Differenz berechnen
LET start = DATE_TIMESTAMP("2024-01-01")
LET end = DATE_TIMESTAMP("2024-12-31")
RETURN DATE_DIFF(start, end, "days") -- 365
RETURN DATE_DIFF(start, end, "months") -- 12
RETURN DATE_DIFF(start, end, "weeks") -- 52
-- Arbeitstage mit Feiertags-Kalender
LET holidays = HOLIDAYS("DE_2024") -- Deutsche Feiertage 2024
LET start = MAKE_DATE(2024, 12, 1)
LET end = MAKE_DATE(2024, 12, 31)
-- Arbeitstage zählen (Mo-Fr, ohne Feiertage)
RETURN WORKDAYS(start, end, holidays) -- ~18 Arbeitstage
-- Arbeitstage addieren (Lieferzeit-Berechnung)
LET orderDate = NOW()
LET deliveryDate = WORKDAYS_ADD(orderDate, 10, holidays)
RETURN DATE_FORMAT(deliveryDate, "%Y-%m-%d")
-- Prüffunktionen
RETURN IS_WEEKEND(MAKE_DATE(2024, 12, 25)) -- true (Mittwoch? false)
RETURN IS_WORKDAY(MAKE_DATE(2024, 12, 25), holidays) -- false (Feiertag)
-- Liste aller verfügbaren Kalender
RETURN LIST_CALENDARS()
-- ["DE_2024", "DE_2025", "AT_2024", "CH_2024", "US_FEDERAL_2024", ...]
-- Kalender laden
LET deHolidays = HOLIDAYS("DE_2024") -- Deutschland 2024
LET atHolidays = HOLIDAYS("AT_2024") -- Österreich 2024
LET chHolidays = HOLIDAYS("CH_2024") -- Schweiz 2024
LET usHolidays = HOLIDAYS("US_FEDERAL_2024") -- USA Federal 2024
LET ukHolidays = HOLIDAYS("UK_2024") -- Großbritannien 2024
LET frHolidays = HOLIDAYS("FR_2024") -- Frankreich 2024
-- Kalender zusammenführen (Firma mit Standorten DE + AT)
LET companyHolidays = HOLIDAYS("DE_2024", "AT_2024")
-- Inline-Feiertage definieren
LET customHolidays = HOLIDAYS("2024-12-23", "2024-12-27", "2024-12-30")
-- Feiertage in Zeitraum filtern
RETURN HOLIDAYS_BETWEEN("DE_2024", MAKE_DATE(2024, 12, 1), MAKE_DATE(2024, 12, 31))
-- Schaltjahr prüfen
RETURN DATE_LEAPYEAR(2024) -- true
RETURN DATE_LEAPYEAR(2023) -- false
-- Tage im Monat
RETURN DATE_DAYS_IN_MONTH(2024, 2) -- 29 (Schaltjahr)
RETURN DATE_DAYS_IN_MONTH(2023, 2) -- 28
-- Wochenanfang/-ende
RETURN DATE_START_OF_WEEK(NOW()) -- Montag 00:00
RETURN DATE_START_OF_WEEK(NOW(), 0) -- Sonntag 00:00 (US-Style)
RETURN DATE_END_OF_MONTH(NOW()) -- Letzter Tag des Monats
-- Alter berechnen
FOR person IN persons
LET age = AGE(person.birthdate)
FILTER age >= 18
RETURN { name: person.name, age }
-- Datums-Vergleich
RETURN DATE_COMPARE(date1, date2) -- -1, 0, oder 1
RETURN DATE_BETWEEN(checkDate, start, end) -- true/false
LET ts = DATE_TIMESTAMP("2024-06-15T14:30:00Z")
RETURN DATE_FORMAT(ts, "%Y-%m-%d") -- "2024-06-15"
RETURN DATE_FORMAT(ts, "%d.%m.%Y %H:%M") -- "15.06.2024 14:30"
RETURN DATE_ISO8601(ts) -- "2024-06-15T14:30:00Z"
-- Auf Zeiteinheit runden
RETURN DATE_TRUNC(ts, "year") -- "2024-01-01T00:00:00Z"
RETURN DATE_TRUNC(ts, "month") -- "2024-06-01T00:00:00Z"
RETURN DATE_TRUNC(ts, "week") -- Wochenanfang
RETURN DATE_TRUNC(ts, "day") -- "2024-06-15T00:00:00Z"
RETURN DATE_TRUNC(ts, "hour") -- "2024-06-15T14:00:00Z"
Praxisbeispiel: Aktivität der letzten 30 Tage
FOR event IN events
FILTER event.created_at >= NOW() - DAYS(30) -- Elegante Syntax
COLLECT week = DATE_TRUNC(event.created_at, "week")
AGGREGATE count = COUNT(1)
SORT week ASC
RETURN { week: DATE_FORMAT(week, "%Y-W%V"), count }
Praxisbeispiel: Fälligkeitsdatum mit Arbeitstagen
LET holidays = HOLIDAYS("DE_2024")
FOR order IN orders
FILTER order.status == "pending"
LET dueDate = WORKDAYS_ADD(order.created_at, 10, holidays)
LET overdue = dueDate < NOW()
RETURN {
orderId: order._key,
createdAt: DATE_FORMAT(order.created_at, "%d.%m.%Y"),
dueDate: DATE_FORMAT(dueDate, "%d.%m.%Y"),
overdue,
daysRemaining: overdue ? 0 : WORKDAYS(NOW(), dueDate, holidays)
}
-- Einzelnes Dokument
LET customer = DOCUMENT("customers", "customer123")
RETURN customer
-- Referenz auflösen
FOR order IN orders
LET customer = DOCUMENT("customers", order.customer_id)
RETURN { order, customerName: customer.name }
-- Objekte zusammenführen
LET defaults = { status: "active", role: "user" }
LET user = { name: "Max", role: "admin" }
RETURN MERGE(defaults, user) -- { status: "active", role: "admin", name: "Max" }
-- Rekursives Merge
LET a = { settings: { theme: "dark", lang: "de" } }
LET b = { settings: { lang: "en" } }
RETURN MERGE_RECURSIVE(a, b) -- { settings: { theme: "dark", lang: "en" } }
-- Eigenschaften entfernen
RETURN UNSET({ a: 1, b: 2, c: 3 }, ["b", "c"]) -- { a: 1 }
-- Nur bestimmte Eigenschaften behalten
RETURN KEEP({ a: 1, b: 2, c: 3 }, ["a", "b"]) -- { a: 1, b: 2 }
LET doc = { name: "Max", age: 30 }
RETURN HAS(doc, "name") -- true
RETURN HAS(doc, "email") -- false
RETURN ATTRIBUTES(doc) -- ["name", "age"]
RETURN VALUES(doc) -- ["Max", 30]
RETURN TYPENAME(null) -- "null"
RETURN TYPENAME(42) -- "number"
RETURN TYPENAME("hello") -- "string"
RETURN TYPENAME([1, 2]) -- "array"
RETURN TYPENAME({a: 1}) -- "object"
RETURN IS_NULL(null) -- true
RETURN IS_NUMBER(42) -- true
RETURN IS_STRING("hello") -- true
RETURN IS_ARRAY([1, 2]) -- true
RETURN IS_OBJECT({a: 1}) -- true
RETURN TO_NUMBER("42.5") -- 42.5
RETURN TO_STRING(42) -- "42"
RETURN TO_BOOL(1) -- true
RETURN TO_BOOL("") -- false
RETURN TO_ARRAY("hello") -- ["hello"]
ThemisDB bietet ~25 Collection-Funktionen für das Erstellen und Manipulieren von Arrays, Objekten und JSON-Daten mit JSON-Native Support.
-- Einfaches Array erstellen
RETURN ARRAY(1, 2, 3) -- [1, 2, 3]
RETURN ARRAY("a", "b", "c") -- ["a", "b", "c"]
RETURN ARRAY() -- []
-- JSON-Native Parsing ⭐
RETURN ARRAY('[1, 2, 3]') -- [1, 2, 3] (JSON-String geparst)
RETURN ARRAY('[1, 2]', '[3, 4]') -- [[1, 2], [3, 4]]
-- Set (unique values)
RETURN SET(1, 2, 2, 3, 3, 3) -- [1, 2, 3]
RETURN SET("a", "b", "a") -- ["a", "b"]
-- Tuple (feste Größe)
RETURN TUPLE(x, y, z) -- [x, y, z]
RETURN PAIR("key", "value") -- ["key", "value"]
-- Range
RETURN RANGE(0, 5) -- [0, 1, 2, 3, 4]
RETURN RANGE(1, 10, 2) -- [1, 3, 5, 7, 9]
RETURN RANGE(10, 0, -1) -- [10, 9, 8, ..., 1]
-- Repeat
RETURN REPEAT(0, 5) -- [0, 0, 0, 0, 0]
RETURN REPEAT("x", 3) -- ["x", "x", "x"]
-- Key-Value Paare
RETURN DICT("name", "Alice", "age", 30)
-- {"name": "Alice", "age": 30}
RETURN DICT("x", 1, "y", 2, "z", 3)
-- {"x": 1, "y": 2, "z": 3}
-- JSON-Native Parsing ⭐
RETURN DICT('{"name": "Alice", "age": 30}')
-- {"name": "Alice", "age": 30}
-- Verschachtelt
RETURN DICT("person", DICT('{"name": "Bob"}'), "active", true)
-- {"person": {"name": "Bob"}, "active": true}
-- OBJECT Alias
RETURN OBJECT("key", "value") -- {"key": "value"}
-- JSON parsen
RETURN JSON('[1, 2, 3]') -- [1, 2, 3]
RETURN JSON('{"name": "Alice"}') -- {"name": "Alice"}
RETURN JSON('null') -- null
RETURN JSON('123') -- 123
-- JSON serialisieren
RETURN TO_JSON([1, 2, 3]) -- "[1,2,3]"
RETURN TO_JSON({name: "Alice"}) -- '{"name":"Alice"}'
RETURN TO_JSON(doc, true) -- Pretty-printed (mit Indentation)
-- JSON validieren
RETURN JSON_VALID('[1, 2, 3]') -- true
RETURN JSON_VALID('not json') -- false
RETURN JSON_VALID('{"incomplete":') -- false
-- JSON-Typ ermitteln
RETURN JSON_TYPE([1, 2]) -- "array"
RETURN JSON_TYPE({a: 1}) -- "object"
RETURN JSON_TYPE(123) -- "number"
RETURN JSON_TYPE("text") -- "string"
RETURN JSON_TYPE(null) -- "null"
RETURN JSON_TYPE(true) -- "boolean"
-- Object zu Array
RETURN KEYS({a: 1, b: 2, c: 3}) -- ["a", "b", "c"]
RETURN ENTRIES({a: 1, b: 2}) -- [["a", 1], ["b", 2]]
-- Array zu Object
RETURN FROM_ENTRIES([["a", 1], ["b", 2]]) -- {a: 1, b: 2}
-- String zu Array (splitting)
RETURN LIST("a,b,c") -- ["a", "b", "c"]
RETURN LIST("a;b;c") -- ["a", "b", "c"]
RETURN LIST("a\nb\nc") -- ["a", "b", "c"]
-- Object Values zu Array
RETURN LIST({a: 1, b: 2}) -- [1, 2]
-- Feiertags-Kalender laden
LET holidays = HOLIDAYS("DE_2024") -- Deutsche Feiertage 2024
-- Verfügbare Kalender anzeigen
RETURN LIST_CALENDARS()
-- ["DE_2024", "DE_2025", "AT_2024", "CH_2024",
-- "US_FEDERAL_2024", "US_FEDERAL_2025", "UK_2024", "FR_2024",
-- "NONE", "WEEKENDS_ONLY"]
-- Kalender zusammenführen
LET combined = HOLIDAYS("DE_2024", "AT_2024") -- DE + AT Feiertage
-- Inline-Feiertage (Betriebsferien)
LET companyHolidays = HOLIDAYS("2024-12-23", "2024-12-27", "2024-12-30")
-- Feiertage in Zeitraum
LET december = HOLIDAYS_BETWEEN("DE_2024", MAKE_DATE(2024,12,1), MAKE_DATE(2024,12,31))
-- [1735084800000, 1735171200000] // 25.12. und 26.12.
-- Mit WORKDAYS kombinieren
FOR project IN projects
LET holidays = HOLIDAYS("DE_2024")
LET workdays = WORKDAYS(project.start, project.deadline, holidays)
RETURN { project: project.name, workdays }
| Kalender | Region | Jahr | Beschreibung |
|---|---|---|---|
DE_2024 |
Deutschland | 2024 | Bundesweite Feiertage |
DE_2025 |
Deutschland | 2025 | Bundesweite Feiertage |
AT_2024 |
Österreich | 2024 | Nationale Feiertage |
CH_2024 |
Schweiz | 2024 | Bundesfeiertage |
US_FEDERAL_2024 |
USA | 2024 | Federal Holidays |
US_FEDERAL_2025 |
USA | 2025 | Federal Holidays |
UK_2024 |
Großbritannien | 2024 | Bank Holidays |
FR_2024 |
Frankreich | 2024 | Jours fériés |
NONE |
- | - | Leerer Kalender |
WEEKENDS_ONLY |
- | - | Nur Wochenenden |
Praxisbeispiel: Lieferzeit-Berechnung
LET holidays = HOLIDAYS("DE_2024")
FOR order IN orders
FILTER order.status == "shipped"
LET estimatedDelivery = WORKDAYS_ADD(order.shipped_at, 3, holidays)
LET isLate = estimatedDelivery < NOW() AND order.delivered_at == null
RETURN {
orderId: order._key,
shippedAt: DATE_FORMAT(order.shipped_at, "%d.%m.%Y"),
estimatedDelivery: DATE_FORMAT(estimatedDelivery, "%d.%m.%Y"),
isLate,
customer: order.customer_name
}
Praxisbeispiel: JSON-Daten aus API verarbeiten
-- Externe API-Daten (als JSON-String empfangen)
LET apiResponse = '{"users": [{"name": "Alice"}, {"name": "Bob"}]}'
LET data = JSON(apiResponse)
FOR user IN data.users
INSERT user INTO users
-- Oder direkt mit DICT
LET config = DICT('{"theme": "dark", "language": "de"}')
RETURN config.theme -- "dark"
-- Punkt erstellen
LET point = ST_POINT(8.6821, 50.1109) -- Frankfurt Hauptbahnhof
-- LineString erstellen
LET route = ST_LINESTRING([
[8.6821, 50.1109], -- Frankfurt
[11.5820, 48.1351], -- München
[13.4050, 52.5200] -- Berlin
])
-- Polygon erstellen
LET area = ST_POLYGON([[
[8.0, 50.0],
[9.0, 50.0],
[9.0, 51.0],
[8.0, 51.0],
[8.0, 50.0]
]])
-- Distanz in Metern (Haversine für WGS84)
LET frankfurt = ST_POINT(8.6821, 50.1109)
LET berlin = ST_POINT(13.4050, 52.5200)
RETURN GEO_DISTANCE(frankfurt, berlin) -- ~423 km
-- Alle Filialen im Umkreis von 10 km
FOR store IN stores
LET dist = GEO_DISTANCE(store.location, @myLocation)
FILTER dist <= 10000
SORT dist ASC
RETURN { store, distance_km: dist / 1000 }
-- Punkt in Polygon?
LET point = ST_POINT(8.5, 50.5)
LET region = ST_POLYGON([[[8, 50], [9, 50], [9, 51], [8, 51], [8, 50]]])
RETURN ST_CONTAINS(region, point) -- true
-- Geometrien schneiden sich?
RETURN ST_INTERSECTS(polygon1, polygon2)
-- Punkt innerhalb Distanz?
RETURN ST_DWITHIN(point1, point2, 1000) -- innerhalb 1 km
-- GeoJSON zu WKT
LET point = ST_POINT(8.6821, 50.1109)
RETURN ST_ASTEXT(point) -- "POINT(8.6821 50.1109)"
-- WKT zu GeoJSON
LET geom = ST_GEOMFROMTEXT("POLYGON((0 0, 1 0, 1 1, 0 1, 0 0))")
RETURN ST_ASGEOJSON(geom)
-- GeoJSON String parsen
LET geom = ST_GEOMFROMGEOJSON('{"type":"Point","coordinates":[8.6821,50.1109]}')
-- 3D-Punkt
LET point3d = ST_POINT(8.6821, 50.1109, 150) -- mit Höhe 150m
RETURN ST_HASZ(point3d) -- true
RETURN ST_Z(point3d) -- 150
-- 3D-Distanz
RETURN ST_3DDISTANCE(point1, point2)
-- Z-Werte in Bereich?
RETURN ST_ZBETWEEN(geometry, 100, 200)
-- Auf 2D reduzieren
RETURN ST_FORCE2D(point3d)
-- Puffer um Punkt (Quadrat)
LET point = ST_POINT(8.6821, 50.1109)
LET buffer = ST_BUFFER(point, 0.01) -- ~1 km Puffer
-- Geometrien vereinigen
RETURN ST_UNION(polygon1, polygon2)
-- Zentroid berechnen
RETURN ST_CENTROID(polygon)
-- Bounding Box
RETURN ST_ENVELOPE(polygon)
ThemisDB unterstützt die Transformation zwischen verschiedenen Koordinatenreferenzsystemen (CRS), was für GIS-Anwendungen und Vermessungsdaten essentiell ist.
| EPSG | Name | Verwendung |
|---|---|---|
| 4326 | WGS84 | GPS, Google Maps, weltweit |
| 4258 | ETRS89 | Europäisches Referenzsystem |
| 25831-25833 | ETRS89/UTM 31-33N | Deutschland, metrische Koordinaten |
| 32631-32633 | WGS84/UTM 31-33N | Globale UTM-Zonen |
| 31466-31469 | DHDN/Gauß-Krüger | Historische deutsche Daten |
| 3857 | Web Mercator | OpenStreetMap, Google Maps Tiles |
-- UTM-Koordinaten (Vermessungsdaten) zu GPS konvertieren
LET utmPoint = ST_POINT(500000, 5600000) -- UTM Zone 32N
LET wgs84Point = ST_TRANSFORM(utmPoint, 25832, 4326)
RETURN {
lat: ST_Y(wgs84Point), -- ~50.5°
lon: ST_X(wgs84Point) -- ~8.9°
}
-- Gauß-Krüger (alte Katasterdaten) zu WGS84
FOR parcel IN historic_parcels
LET modernGeom = ST_TRANSFORM(parcel.geometry, 31467, 4326)
UPDATE parcel WITH { geometry_wgs84: modernGeom } IN historic_parcels
-- Welche UTM-Zone für einen Längengrad?
RETURN UTM_ZONE(8.6821) -- 32
-- EPSG-Code für UTM-Zone
RETURN UTM_EPSG(32, true) -- 25832 (ETRS89/UTM 32N)
RETURN UTM_EPSG(32, false) -- 32632 (WGS84/UTM 32N)
RETURN CRS_NAME(4326) -- "WGS 84"
RETURN CRS_NAME(25832) -- "ETRS89 / UTM zone 32N"
RETURN CRS_IS_GEOGRAPHIC(4326) -- true (Grad-Koordinaten)
RETURN CRS_IS_PROJECTED(25832) -- true (Meter-Koordinaten)
Praxisbeispiel: Grundstücksdaten harmonisieren
-- Verschiedene Quellen mit unterschiedlichen Koordinatensystemen vereinheitlichen
FOR parcel IN all_parcels
LET normalizedGeom = (
parcel.srid == 4326 ? parcel.geometry :
parcel.srid == 25832 ? ST_TRANSFORM(parcel.geometry, 25832, 4326) :
parcel.srid == 31467 ? ST_TRANSFORM(parcel.geometry, 31467, 4326) :
null
)
LET center = ST_CENTROID(normalizedGeom)
RETURN {
id: parcel.id,
original_srid: parcel.srid,
area_sqm: ST_AREA(ST_TRANSFORM(normalizedGeom, 4326, UTM_EPSG(UTM_ZONE(ST_X(center)), true))),
center: { lat: ST_Y(center), lon: ST_X(center) }
}
ThemisDB integriert Vektor-Operationen nativ für Embeddings aus ML-Modellen.
LET vec1 = [0.1, 0.2, 0.3, 0.4]
LET vec2 = [0.15, 0.25, 0.28, 0.45]
-- Kosinus-Ähnlichkeit (0-1, höher = ähnlicher)
RETURN COSINE_SIMILARITY(vec1, vec2) -- ~0.996
-- Euklidische Distanz (niedriger = ähnlicher)
RETURN EUCLIDEAN_DISTANCE(vec1, vec2) -- ~0.095
-- Dot Product
RETURN DOT_PRODUCT(vec1, vec2) -- 0.309
-- Manhattan-Distanz
RETURN MANHATTAN_DISTANCE(vec1, vec2) -- 0.17
-- Chebyshev-Distanz (Maximum)
RETURN CHEBYSHEV_DISTANCE(vec1, vec2) -- 0.05
Findet die k ähnlichsten Dokumente.
-- Top 10 ähnliche Produkte finden
FOR product IN products
LET sim = SIMILARITY(product.embedding, @queryEmbedding, 10)
FILTER sim > 0.8
SORT sim DESC
RETURN { product, similarity: sim }
-- L2-Normalisierung (Länge = 1)
RETURN L2_NORMALIZE([3, 4]) -- [0.6, 0.8]
-- Min-Max-Normalisierung (0-1 Bereich)
RETURN MIN_MAX_NORMALIZE([10, 20, 30]) -- [0, 0.5, 1]
LET v1 = [1, 2, 3]
LET v2 = [4, 5, 6]
RETURN VECTOR_ADD(v1, v2) -- [5, 7, 9]
RETURN VECTOR_SUB(v1, v2) -- [-3, -3, -3]
RETURN VECTOR_MUL(v1, v2) -- [4, 10, 18] (elementweise)
RETURN VECTOR_SCALE(v1, 2) -- [2, 4, 6]
LET v = [1, 2, 3, 4, 5]
RETURN VECTOR_SUM(v) -- 15
RETURN VECTOR_AVG(v) -- 3
RETURN VECTOR_NORM(v) -- ~7.416 (L2-Norm)
RETURN VECTOR_DIM(v) -- 5
RETURN VECTOR_MIN(v) -- 1
RETURN VECTOR_MAX(v) -- 5
RETURN VECTOR_ZEROS(5) -- [0, 0, 0, 0, 0]
RETURN VECTOR_ONES(3) -- [1, 1, 1]
RETURN VECTOR_RANDOM(4) -- [0.23, 0.87, 0.12, 0.56]
RETURN VECTOR_SLICE([1,2,3,4], 1, 3) -- [2, 3]
RETURN VECTOR_CONCAT([1,2], [3,4]) -- [1, 2, 3, 4]
Praxisbeispiel: Semantische Suche mit Kontext
-- Finde ähnliche Artikel mit Geo- und Zeit-Kontext
FOR article IN articles
LET semanticSim = COSINE_SIMILARITY(article.embedding, @queryEmbedding)
LET geoDist = GEO_DISTANCE(article.location, @userLocation)
LET recency = 1 / (1 + DATE_DIFF(article.published, DATE_NOW(), "days"))
-- Kombinierter Score
LET score = (semanticSim * 0.6) + ((1 - geoDist/100000) * 0.2) + (recency * 0.2)
FILTER semanticSim > 0.7
SORT score DESC
LIMIT 20
RETURN { article, score, semanticSim, geoDist, recency }
ThemisDB bietet native Graph-Unterstützung ohne separate Query-Sprache.
-- Direkte Freunde
FOR friend IN 1..1 OUTBOUND @startUser knows
RETURN friend
-- Freunde von Freunden (2 Hops)
FOR connection IN 1..2 OUTBOUND @startUser knows
RETURN DISTINCT connection
-- Mit Tiefenbegrenzung
FOR v, e, p IN 1..5 OUTBOUND @startNode follows
RETURN { vertex: v, edge: e, path: p }
-- Kürzester Weg zwischen zwei Personen
LET path = SHORTEST_PATH(@personA, @personB, "knows")
RETURN {
length: LENGTH(path.vertices),
vertices: path.vertices,
edges: path.edges
}
-- Wie viele Hops entfernt?
RETURN GRAPH_DISTANCE(@user1, @user2, "follows") -- z.B. 3
-- Sind zwei Knoten überhaupt verbunden?
IF GRAPH_CONNECTED(@nodeA, @nodeB, "links")
RETURN "Verbunden"
ELSE
RETURN "Nicht verbunden"
-- Degree Centrality (wie viele Verbindungen?)
FOR user IN users
LET centrality = DEGREE_CENTRALITY(user, "follows")
SORT centrality DESC
LIMIT 10
RETURN { user: user.name, centrality }
-- PageRank (Wichtigkeit im Netzwerk)
FOR page IN pages
LET rank = PAGERANK(page, "links", 0.85)
SORT rank DESC
LIMIT 100
RETURN { url: page.url, pagerank: rank }
-- Wie stark sind die Freunde eines Users untereinander verbunden?
FOR user IN users
LET clustering = CLUSTERING_COEFFICIENT(user, "knows")
FILTER clustering > 0.5 -- Stark vernetzte Communities
RETURN { user: user.name, clustering }
-- Finde alle zusammenhängenden Teilgraphen
LET components = CONNECTED_COMPONENTS("users", "knows")
FOR comp IN components
RETURN {
size: LENGTH(comp.members),
members: comp.members
}
Praxisbeispiel: Influencer-Analyse
-- Finde Top-Influencer mit Geo-Reichweite
FOR user IN users
LET followers = (FOR f IN 1..1 INBOUND user follows RETURN f)
LET followerCount = LENGTH(followers)
LET avgDistance = AVG(
FOR f IN followers
RETURN GEO_DISTANCE(f.location, user.location)
)
LET pagerank = PAGERANK(user, "follows")
FILTER followerCount >= 1000
SORT pagerank DESC
LIMIT 50
RETURN {
user: user.name,
followers: followerCount,
avgReachKm: avgDistance / 1000,
pagerank,
influenceScore: pagerank * LOG(followerCount) * LOG(avgDistance/1000 + 1)
}
ThemisDB bietet SQL-ähnliche Funktionen für relationale Operationen.
FOR order IN orders
COLLECT customer = order.customer_id
AGGREGATE
total = SUM(order.amount),
count = COUNT(1),
avg = AVG(order.amount),
distinct_products = COUNT_DISTINCT(order.product_id)
RETURN { customer, total, count, avg, distinct_products }
FOR sale IN sales
COLLECT region = sale.region
AGGREGATE
median = MEDIAN(sale.amount),
stddev = STDDEV(sale.amount),
variance = VARIANCE(sale.amount),
p95 = PERCENTILE(sale.amount, 95)
RETURN { region, median, stddev, variance, p95 }
FOR order IN orders
COLLECT customer = order.customer_id
AGGREGATE products = GROUP_CONCAT(order.product_name, ", ")
RETURN { customer, products }
-- Ergebnis: { customer: "C1", products: "Laptop, Mouse, Keyboard" }
-- COALESCE: Erster nicht-null Wert
RETURN COALESCE(null, null, "default", "other") -- "default"
-- NULLIF: Null wenn gleich
RETURN NULLIF(10, 10) -- null
RETURN NULLIF(10, 20) -- 10
-- GREATEST / LEAST
RETURN GREATEST(5, 3, 8, 1) -- 8
RETURN LEAST(5, 3, 8, 1) -- 1
-- IF
RETURN IF(age >= 18, "adult", "minor")
-- INNER JOIN Equivalent
FOR order IN orders
FOR customer IN customers
FILTER order.customer_id == customer._key
RETURN { order, customer }
-- LEFT JOIN mit LOOKUP
FOR order IN orders
LET customer = LOOKUP("customers", order.customer_id)
RETURN { order, customer } -- customer kann null sein
-- ROW_NUMBER, RANK, DENSE_RANK
FOR sale IN sales
SORT sale.region, sale.amount DESC
LET rowNum = ROW_NUMBER() OVER (PARTITION BY sale.region ORDER BY sale.amount DESC)
FILTER rowNum <= 3 -- Top 3 pro Region
RETURN { sale, rank: rowNum }
-- LAG / LEAD (vorheriger/nächster Wert)
FOR sale IN sales
SORT sale.date
LET prevAmount = LAG(sale.amount, 1) OVER (ORDER BY sale.date)
LET nextAmount = LEAD(sale.amount, 1) OVER (ORDER BY sale.date)
LET growth = (sale.amount - prevAmount) / prevAmount * 100
RETURN { date: sale.date, amount: sale.amount, growth }
-- RUNNING_SUM (kumulativ)
FOR sale IN sales
SORT sale.date
LET runningTotal = RUNNING_SUM(sale.amount) OVER (ORDER BY sale.date)
RETURN { date: sale.date, amount: sale.amount, runningTotal }
Praxisbeispiel: Umsatz-Dashboard
FOR sale IN sales
LET saleDate = DATE_TIMESTAMP(sale.created_at)
FILTER saleDate >= DATE_SUBTRACT(DATE_NOW(), 365, "days")
COLLECT
month = DATE_TRUNC(saleDate, "month"),
region = sale.region
AGGREGATE
revenue = SUM(sale.amount),
orders = COUNT(1),
avgOrder = AVG(sale.amount),
topProducts = GROUP_CONCAT(sale.product, ", ")
LET prevMonth = LAG(revenue, 1) OVER (PARTITION BY region ORDER BY month)
LET growth = prevMonth ? ((revenue - prevMonth) / prevMonth * 100) : null
SORT region, month
RETURN {
month: DATE_FORMAT(month, "%Y-%m"),
region,
revenue,
orders,
avgOrder,
growth: growth ? CONCAT(ROUND(growth, 1), "%") : "N/A",
topProducts: SUBSTRING(topProducts, 0, 100)
}
-- Pfade zusammenfügen
RETURN PATH_JOIN("/home", "user", "documents") -- "/home/user/documents"
-- Verzeichnis extrahieren
RETURN PATH_DIRNAME("/home/user/file.txt") -- "/home/user"
-- Dateiname extrahieren
RETURN PATH_BASENAME("/home/user/file.txt") -- "file.txt"
-- Erweiterung extrahieren
RETURN PATH_EXTENSION("/home/user/file.txt") -- "txt"
-- Pfad normalisieren
RETURN PATH_NORMALIZE("/home/./user/../admin/./file.txt") -- "/home/admin/file.txt"
-- Dateiname ohne Erweiterung
RETURN FILENAME_WITHOUT_EXT("document.pdf") -- "document"
-- Erweiterung extrahieren
RETURN FILE_EXT("photo.jpg") -- "jpg"
-- Dateinamen bereinigen (für Upload-Sicherheit)
RETURN SANITIZE_FILENAME("my file (1).txt") -- "my_file_1_.txt"
RETURN SANITIZE_FILENAME("../../../etc/passwd") -- "etc_passwd"
-- MIME-Typ ermitteln
RETURN MIME_TYPE("document.pdf") -- "application/pdf"
RETURN MIME_TYPE("photo.jpg") -- "image/jpeg"
RETURN MIME_TYPE("video.mp4") -- "video/mp4"
RETURN MIME_TYPE("data.json") -- "application/json"
-- Typ-Prüfungen
RETURN IS_IMAGE("photo.jpg") -- true
RETURN IS_VIDEO("movie.mp4") -- true
RETURN IS_AUDIO("song.mp3") -- true
RETURN IS_DOCUMENT("report.pdf") -- true
-- Größe formatieren
RETURN FORMAT_FILESIZE(1024) -- "1 KB"
RETURN FORMAT_FILESIZE(1048576) -- "1 MB"
RETURN FORMAT_FILESIZE(1073741824) -- "1 GB"
RETURN FORMAT_FILESIZE(1536000) -- "1.46 MB"
-- Größe parsen
RETURN PARSE_FILESIZE("1.5 GB") -- 1610612736
RETURN PARSE_FILESIZE("500 KB") -- 512000
Praxisbeispiel: Datei-Inventar
FOR file IN files
LET ext = FILE_EXT(file.name)
LET mimeType = MIME_TYPE(file.name)
LET sizeFormatted = FORMAT_FILESIZE(file.size)
LET category = (
IS_IMAGE(file.name) ? "Bilder" :
IS_VIDEO(file.name) ? "Videos" :
IS_AUDIO(file.name) ? "Audio" :
IS_DOCUMENT(file.name) ? "Dokumente" :
"Sonstige"
)
COLLECT cat = category
AGGREGATE
count = COUNT(1),
totalSize = SUM(file.size),
extensions = GROUP_CONCAT(DISTINCT ext, ", ")
RETURN {
category: cat,
count,
totalSize: FORMAT_FILESIZE(totalSize),
extensions
}
Wenn mindestens einer dieser Punkte zutrifft:
- Sie brauchen Graph-Traversierung UND Geo-Queries
- Sie haben ML-Embeddings und brauchen Ähnlichkeitssuche
- Sie möchten Prozesse aus Event-Logs entdecken
- Sie haben Daten in verschiedenen Koordinatensystemen
- Sie möchten keine 5 verschiedenen Systeme integrieren
Bei PostgreSQL würden Sie brauchen:
- PostGIS für Geo
- pgvector für Vectors
-
WITH RECURSIVEfür Graphen (komplex!) - Mehrere Extensions koordinieren
Für Multi-Model Queries: Ja. Eine Query die Graph + Geo + Vector kombiniert ist in ThemisDB nativ optimiert. In MongoDB müssten Sie:
-
\$graphLookupfür Graph - Atlas Search für Vectors
-
\$geoNearfür Geo - Alles in einer komplexen Aggregation-Pipeline kombinieren
Für einfache CRUD: Vergleichbar.
Ja! Speichern Sie Embeddings als Arrays:
-- Embedding speichern
INSERT {
text: "Hello World",
embedding: [0.1, 0.2, 0.3, ...] -- 1536 Dimensionen für OpenAI
} INTO documents
-- Ähnlichkeitssuche
FOR doc IN documents
LET sim = COSINE_SIMILARITY(doc.embedding, @queryEmbedding)
FILTER sim > 0.8
SORT sim DESC
RETURN doc
Unterstützte Embedding-Dimensionen: Beliebig (OpenAI, Cohere, lokale Modelle).
-- ETRS89/UTM Zone 32N (Deutschland) zu WGS84
LET utmPoint = ST_POINT(500000, 5600000)
LET gpsPoint = ST_TRANSFORM(utmPoint, 25832, 4326)
RETURN {
lat: ST_Y(gpsPoint),
lon: ST_X(gpsPoint)
}
Typische Hinweise:
- Werte wie
8.6821, 50.1109→ WGS84 (EPSG:4326) - Werte wie
500000, 5600000→ UTM (z.B. EPSG:25832) - Werte wie
3500000, 5600000→ Gauß-Krüger (z.B. EPSG:31467)
-- Edge-Collection erstellen
INSERT { _from: "users/alice", _to: "users/bob", since: "2020-01-01" } INTO knows
-- Traversieren
FOR friend IN 1..1 OUTBOUND "users/alice" knows
RETURN friend
LET path = SHORTEST_PATH(@from, @to, "roads", { weightAttribute: "distance" })
RETURN {
totalDistance: SUM(path.edges[*].distance),
route: path.vertices[*].name
}
- Index erstellen:
CREATE INDEX idx_embedding ON documents(embedding) TYPE vector
- LIMIT früh verwenden:
FOR doc IN documents
LET sim = COSINE_SIMILARITY(doc.embedding, @query)
SORT sim DESC
LIMIT 100 -- So früh wie möglich
RETURN doc
- Vorfiltern:
FOR doc IN documents
FILTER doc.category == "tech" -- Erst filtern, dann Similarity
LET sim = COSINE_SIMILARITY(doc.embedding, @query)
...
- Geo-Index erstellen:
CREATE INDEX idx_location ON stores(location) TYPE geo
- ST_DWithin statt Post-Filter:
-- Langsam (alle laden, dann filtern)
FOR store IN stores
FILTER GEO_DISTANCE(store.location, @point) < 10000
-- Schnell (Index nutzen)
FOR store IN stores
FILTER ST_DWITHIN(store.location, @point, 10000)
-
Dokumente exportieren:
mongoexport - In ThemisDB importieren:
themisdb import --collection users --file users.json- Queries anpassen:
| MongoDB | ThemisDB AQL |
|---|---|
db.users.find({age: {\$gt: 18}}) |
FOR u IN users FILTER u.age > 18 RETURN u |
\$lookup |
FOR ... FOR ... FILTER |
\$graphLookup |
FOR v IN OUTBOUND |
- Knoten exportieren: Als JSON
- Kanten exportieren: Als Edge-Collection
- Cypher zu AQL:
| Cypher | ThemisDB AQL |
|---|---|
MATCH (p:Person) |
FOR p IN persons |
(a)-[:KNOWS]->(b) |
FOR b IN OUTBOUND a knows |
shortestPath() |
SHORTEST_PATH() |
ThemisDB verfügt über ein umfassendes integriertes Security-Modul. Die AQL-Funktionen ermöglichen Validierung, Sanitization und Maskierung direkt in Queries.
-- Format-Validierung
FILTER IS_EMAIL(doc.email) -- E-Mail-Format prüfen
FILTER IS_URL(doc.website) -- URL-Format prüfen
FILTER IS_UUID(doc._key) -- UUID-Format prüfen
FILTER IS_IP(doc.ip_address) -- IP-Adresse prüfen
-- Schema-Validierung (registrierte Schemas)
LET result = VALIDATE(doc, "user_schema")
RETURN result.valid -- true/false
RETURN result.errors -- ["field: error message", ...]
-- Schnelle Boolean-Prüfung
FILTER IS_VALID(doc, "order_schema")
-- Daten bereinigen
LET clean = SANITIZE(userInput)
-- Mit Optionen
LET clean = SANITIZE(data, {
escapeHtml: true,
escapeSql: true,
trimStrings: true,
maxStringLength: 1000
})
-- Einzelnen String bereinigen
LET cleanName = SANITIZE_STRING(userName, {escapeHtml: true})
-- Injection-Prüfung
FILTER NOT HAS_INJECTION(userInput)
-- Einzelwert maskieren
RETURN MASK(password, "password") -- "********"
RETURN MASK(email, "email") -- "j***@example.com"
RETURN MASK(ccNumber, "credit_card") -- "************1234"
RETURN MASK(ssn, "ssn") -- "***-**-1234"
RETURN MASK(phone, "phone") -- "*******1234"
-- Mehrere Felder maskieren
LET safeUser = MASK_FIELDS(user, {
password: "password",
ssn: "ssn",
creditCard: "credit_card"
})
-- Auto-Erkennung von sensitiven Feldern
LET safeData = AUTO_MASK(userData) -- Erkennt password, api_key, etc.
-- Felder entfernen
LET publicUser = REDACT(user, ["password", "ssn", "internal_id"])
-- Checksumme berechnen
LET hash = CHECKSUM(doc) -- "a1b2c3d4..."
-- Checksumme verifizieren
FILTER VERIFY_CHECKSUM(doc, doc._checksum)
-- Signatur erstellen (HMAC)
LET signature = SIGN(doc, @secretKey)
-- Signatur verifizieren
FILTER VERIFY_SIGNATURE(doc, doc._signature, @secretKey)
Praxisbeispiel: API-Response mit maskierten Daten
FOR user IN users
FILTER user._key == @userId
-- Sensitive Felder maskieren vor Rückgabe
LET safeUser = REDACT(user, ["password_hash", "api_keys", "internal_notes"])
LET maskedUser = MASK_FIELDS(safeUser, {
email: "email",
phone: "phone"
})
RETURN maskedUser
Praxisbeispiel: Input-Validierung vor Insert
LET userInput = @input
-- Validieren
LET validation = VALIDATE(userInput, "new_user_schema")
FILTER validation.valid
-- Sanitizen
LET clean = SANITIZE(userInput, {trimStrings: true})
-- Injection-Check
FILTER NOT HAS_INJECTION(clean.name)
FILTER NOT HAS_INJECTION(clean.bio)
-- Sicher speichern
INSERT clean INTO users
RETURN NEW
ThemisDB AQL bietet:
✅ ~255 Funktionen in 13 Kategorien
✅ Einheitliche Syntax für alle Datenmodelle
✅ Native Multi-Model Queries (Graph + Vector + Geo + Relational)
✅ Vollständige CRS-Unterstützung (ETRS89, UTM, WGS84, Gauß-Krüger)
✅ SQL-kompatible Aggregation und Window Functions
✅ ~45 Date-Funktionen mit Interval-Syntax und Arbeitstagen
✅ JSON-Native ARRAY/DICT Konstruktoren
✅ Built-in Feiertags-Kalender (DE, AT, CH, US, UK, FR)
✅ Integrierte Security-Funktionen (Validation, Sanitization, Masking)
✅ Prozess-Mining aus Event-Daten
✅ Keine Vendor Lock-in bei Query-Sprache
| Feature | Beschreibung |
|---|---|
| Interval-Syntax |
NOW() - DAYS(7) statt DATE_SUBTRACT(DATE_NOW(), 7, "days")
|
| Arbeitstage |
WORKDAYS(), WORKDAYS_ADD(), IS_WORKDAY()
|
| Holiday-Kalender |
HOLIDAYS("DE_2024") mit Built-in Kalendern |
| JSON-Native |
ARRAY('[1,2,3]'), DICT('{"a":1}') parsen JSON-Strings |
| Security-Funktionen |
VALIDATE(), SANITIZE(), MASK(), REDACT()
|
| Format-Validierung |
IS_EMAIL(), IS_URL(), IS_UUID(), IS_IP()
|
Nächste Schritte:
Die Logical-Funktionen bieten Excel-kompatible logische Operationen für Bedingungen und Verzweigungen.
Gibt true zurück, wenn alle Argumente wahr sind.
-- Prüfe ob alle Bedingungen erfüllt sind
RETURN AND(age >= 18, hasLicense, !suspended) -- true/false
-- In FILTER verwenden
FOR user IN users
FILTER AND(user.active, user.verified, user.age >= 18)
RETURN user
Gibt true zurück, wenn mindestens ein Argument wahr ist.
-- Premium oder VIP Kunde
FILTER OR(customer.isPremium, customer.isVIP)
Negiert einen Wert.
FILTER NOT(user.suspended)
Exklusives Oder - genau ein Wert muss wahr sein.
-- Entweder Student ODER Rentner, aber nicht beides
FILTER XOR(person.isStudent, person.isRetired)
Gibt einen Wert abhängig von einer Bedingung zurück.
LET status = IF(order.paid, "Bezahlt", "Ausstehend")
LET discount = IF(customer.isPremium, 0.2, 0.0)
Mehrere Bedingungen prüfen und entsprechenden Wert zurückgeben.
LET grade = IFS(
score >= 90, "A",
score >= 80, "B",
score >= 70, "C",
score >= 60, "D",
true, "F"
)
Switch-Case Logik.
LET monthName = SWITCH(month,
1, "Januar",
2, "Februar",
3, "März",
"Unbekannt"
)
Wählt einen Wert basierend auf einem Index (1-basiert).
LET dayName = CHOOSE(dayOfWeek, "Mo", "Di", "Mi", "Do", "Fr", "Sa", "So")
Prüft ob alle Elemente wahr sind.
LET allPassed = ALL([test1.passed, test2.passed, test3.passed])
Prüft ob mindestens ein Element wahr ist.
LET hasWarnings = ANY(checks[*].hasWarning)
Prüft ob kein Element wahr ist.
LET noErrors = NONE(results[*].hasError)
Zählt Elemente die eine Bedingung erfüllen.
LET highScores = COUNT_IF(scores, ">", 80)
LET activeUsers = COUNT_IF(users[*].status, "==", "active")
Summiert Elemente die eine Bedingung erfüllen.
LET totalPremium = SUM_IF(orders[*].amount, ">", 100)
Gibt fallback zurück wenn value null oder error ist.
LET safeValue = IFERROR(riskyCalculation, 0)
LET safeName = IFERROR(user.name, "Unbekannt")
Gibt fallback zurück wenn value null ist (NA = Not Available).
LET displayName = IFNA(user.nickname, user.fullName)
Diese Funktionen bieten vertraute Excel-Funktionalität für Benutzer, die von Tabellenkalkulationen kommen.
Vertikale Suche in einer Tabelle.
| Parameter | Typ | Beschreibung |
|---|---|---|
| searchValue | any | Der zu suchende Wert |
| table | array | 2D-Array als Tabelle |
| columnIndex | number | Spaltenindex (1-basiert) |
| rangeLookup | boolean | false = exakte Übereinstimmung (Standard) |
LET employees = [
["E001", "Alice", 50000],
["E002", "Bob", 60000],
["E003", "Carol", 55000]
]
LET salary = VLOOKUP("E002", employees, 3) -- Ergebnis: 60000
Horizontale Suche in einer Tabelle.
LET headers = [
["Q1", "Q2", "Q3", "Q4"],
[1000, 1200, 1100, 1500]
]
LET q3Revenue = HLOOKUP("Q3", headers, 2) -- Ergebnis: 1100
Gibt einen Wert aus einem Array zurück.
LET colors = ["Rot", "Grün", "Blau"]
RETURN INDEX(colors, 2) -- Ergebnis: "Grün"
-- 2D Array
LET matrix = [[1, 2], [3, 4], [5, 6]]
RETURN INDEX(matrix, 2, 1) -- Ergebnis: 3
Findet die Position eines Wertes.
LET products = ["Apple", "Banana", "Cherry"]
RETURN MATCH("Banana", products) -- Ergebnis: 2
Wandelt Text in Titel-Case um.
RETURN PROPER("hello world") -- "Hello World"
RETURN PROPER("JOHN DOE") -- "John Doe"
RETURN PROPER("mÜNCHEN") -- "München"
Ersetzt Text (ohne Regex).
RETURN SUBSTITUTE("Mr Blue", "Blue", "Green") -- "Mr Green"
RETURN SUBSTITUTE("a-a-a", "a", "b", 2) -- "a-b-a" (nur 2. Vorkommen)
Wiederholt Text n-mal.
RETURN REPT("*", 5) -- "*****"
RETURN REPT("Ha", 3) -- "HaHaHa"
Vergleicht Texte (case-sensitiv).
RETURN EXACT("Hello", "Hello") -- true
RETURN EXACT("Hello", "hello") -- false
Formatiert einen Wert als Text.
RETURN TEXT(1234.567, "0.00") -- "1234.57"
RETURN TEXT(0.75, "0%") -- "75%"
RETURN TEXT(DATE_NOW(), "YYYY-MM-DD") -- "2024-12-01"
Konvertiert Text in eine Zahl.
RETURN VALUE("123.45") -- 123.45
RETURN VALUE("1,234") -- 1234
Multipliziert Arrays elementweise und summiert.
LET prices = [10, 20, 30]
LET quantities = [5, 3, 2]
RETURN SUMPRODUCT(prices, quantities) -- 10*5 + 20*3 + 30*2 = 170
Durchschnitt mit Bedingung.
LET sales = [100, 200, 150, 300, 50]
RETURN AVERAGEIF(sales, ">", 100) -- (200 + 150 + 300) / 3 = 216.67
Rang eines Wertes in einer Liste.
LET scores = [80, 90, 70, 100, 85]
RETURN RANK(90, scores) -- 2 (2. höchster, absteigend)
RETURN RANK(90, scores, 1) -- 4 (4. niedrigster, aufsteigend)
Gibt den k-größten Wert zurück.
LET values = [5, 2, 8, 1, 9, 3]
RETURN LARGE(values, 1) -- 9 (größter)
RETURN LARGE(values, 2) -- 8 (zweitgrößter)
RETURN LARGE(values, 3) -- 5 (drittgrößter)
Gibt den k-kleinsten Wert zurück.
LET values = [5, 2, 8, 1, 9, 3]
RETURN SMALL(values, 1) -- 1 (kleinster)
RETURN SMALL(values, 2) -- 2 (zweitkleinster)
Häufigster Wert (Modus).
LET ratings = [4, 5, 4, 3, 4, 5, 4]
RETURN MODE(ratings) -- 4 (kommt am häufigsten vor)
Produkt aller Werte.
RETURN PRODUCT(2, 3, 4) -- 24
RETURN PRODUCT([2, 3, 4]) -- 24
Fakultät.
RETURN FACT(5) -- 120 (5! = 5*4*3*2*1)
RETURN FACT(0) -- 1
Rest der Division (Modulo).
RETURN MOD(17, 5) -- 2
RETURN MOD(10, 3) -- 1
Ganzzahliger Anteil der Division.
RETURN QUOTIENT(17, 5) -- 3
RETURN QUOTIENT(10, 3) -- 3
Prüft ob Wert ein Fehler ist.
RETURN ISERROR(1/0) -- true
RETURN ISERROR(null) -- true
RETURN ISERROR(42) -- false
Prüft ob Wert leer ist.
RETURN ISBLANK("") -- true
RETURN ISBLANK(null) -- true
RETURN ISBLANK("text") -- false
Prüft ob Wert ein Text ist.
RETURN ISTEXT("hello") -- true
RETURN ISTEXT(123) -- false
Prüft ob Wert eine Zahl ist.
RETURN ISNUMBER(123) -- true
RETURN ISNUMBER(3.14) -- true
RETURN ISNUMBER("123") -- false
Prüft ob Wert ein Boolean ist.
RETURN ISLOGICAL(true) -- true
RETURN ISLOGICAL(false) -- true
RETURN ISLOGICAL(1) -- false
Gibt den Typ als Nummer zurück (Excel-kompatibel).
| Rückgabewert | Typ |
|---|---|
| 1 | Number |
| 2 | Text |
| 4 | Boolean |
| 16 | Error/Null |
| 64 | Array |
| 128 | Object |
RETURN TYPE(123) -- 1
RETURN TYPE("hello") -- 2
RETURN TYPE(true) -- 4
RETURN TYPE([1, 2, 3]) -- 64
Konvertiert Wert zu Zahl.
RETURN N(true) -- 1
RETURN N(false) -- 0
RETURN N(123) -- 123
RETURN N("text") -- 0
Gibt Text zurück, wenn Wert Text ist, sonst leerer String.
RETURN T("hello") -- "hello"
RETURN T(123) -- ""
RETURN T(true) -- ""
Berechnet die periodische Zahlung für ein Darlehen.
| Parameter | Typ | Beschreibung |
|---|---|---|
| rate | number | Zinssatz pro Periode |
| nper | number | Anzahl der Perioden |
| pv | number | Barwert (Darlehensbetrag) |
| fv | number | Endwert (Standard: 0) |
| type | number | 0 = Ende der Periode, 1 = Anfang |
-- Monatliche Rate für 200.000€ Hypothek, 6% p.a., 30 Jahre
LET monthlyPayment = PMT(0.06/12, 360, 200000)
-- Ergebnis: -1199.10 (negative Zahl = Auszahlung)
Berechnet den Endwert einer Investition.
-- Endwert bei 100€/Monat, 5% p.a., 10 Jahre
LET futureValue = FV(0.05/12, 120, -100)
-- Ergebnis: ~15,528€
Berechnet den Barwert.
-- Barwert einer Rente: 1000€/Monat, 5% p.a., 20 Jahre
LET presentValue = PV(0.05/12, 240, -1000)
-- Ergebnis: ~151,525€
Berechnet den Nettobarwert (Net Present Value).
LET cashflows = [-100000, 30000, 40000, 50000, 60000]
LET npv = NPV(0.10, cashflows)
-- Positiver NPV = rentable Investition
Die Performance der AQL-Funktionen wurde mit Google Benchmark gemessen.
| Funktion | Operationen/s | Latenz (ns) | Komplexität |
|---|---|---|---|
| LENGTH | 50M | 20 | O(1) |
| CONCAT | 10M | 100 | O(n) |
| REGEX_TEST | 2M | 500 | O(n) |
| LEVENSHTEIN_DISTANCE | 500K | 2000 | O(n*m) |
| SUM (1000 Elemente) | 1M | 1000 | O(n) |
| UNIQUE (1000 Elemente) | 100K | 10000 | O(n log n) |
| SORTED (1000 Elemente) | 50K | 20000 | O(n log n) |
| GEO_DISTANCE | 5M | 200 | O(1) |
| ST_TRANSFORM | 500K | 2000 | O(1) |
| COSINE_SIMILARITY (1000 Dim) | 1M | 1000 | O(n) |
| SHORTEST_PATH (100 Nodes) | 10K | 100000 | O(V + E) |
| PAGERANK | 1K | 1000000 | O(k*E) |
| VLOOKUP (1000 Rows) | 100K | 10000 | O(n) |
| PMT | 10M | 100 | O(1) |
# Build benchmarks
cmake -DBUILD_BENCHMARKS=ON ..
make bench_aql_functions
# Ausführen
./bench_aql_functions --benchmark_format=json > results.json- Indizierte Lookups statt VLOOKUP bei großen Datenmengen
- Vektorisierte Operationen für Array-Funktionen nutzen
- CRS-Transformationen cachen wenn möglich
- Graph-Algorithmen profitieren von Indexierung
Die Security-Funktionen bieten umfassende Validierung, Sanitization und Maskierung für sichere Datenverarbeitung. Sie integrieren sich mit dem bestehenden ThemisDB Security-Modul.
Validiert E-Mail-Adressen nach RFC 5322 (vereinfacht).
-- E-Mail-Validierung
FOR user IN users
LET valid = IS_EMAIL(user.email)
RETURN { email: user.email, valid }
-- Beispiele
IS_EMAIL("user@example.com") → true
IS_EMAIL("user.name@sub.domain.com") → true
IS_EMAIL("invalid") → false
IS_EMAIL("@missing.local") → false
Validiert URLs (http, https, ftp).
-- URL-Validierung
IS_URL("https://example.com/path?query=1") → true
IS_URL("ftp://files.example.com") → true
IS_URL("not-a-url") → false
IS_URL("javascript:alert(1)") → false
Validiert UUIDs (Version 1-5).
IS_UUID("550e8400-e29b-41d4-a716-446655440000") → true
IS_UUID("550e8400-e29b-61d4-a716-446655440000") → false (Version 6 nicht gültig)
IS_UUID("invalid") → false
Validiert IP-Adressen (IPv4/IPv6).
-- IPv4
IS_IP("192.168.1.1") → true
IS_IP("10.0.0.1") → true
IS_IP("192.168.1.1", 4) → true
-- IPv6
IS_IP("::1") → true
IS_IP("::1", 6) → true
IS_IP("192.168.1.1", 6) → false
-- Ungültig
IS_IP("999.999.999.999") → false
IS_IP("not-an-ip") → false
Validiert Telefonnummern.
IS_PHONE("+49 123 456789") → true
IS_PHONE("0123456789") → true
IS_PHONE("+1 555 123 4567") → true
IS_PHONE("abc") → false
Validiert IBAN mit Prüfsumme (Mod 97).
IS_IBAN("DE89370400440532013000") → true
IS_IBAN("DE89 3704 0044 0532 0130 00") → true (Leerzeichen erlaubt)
IS_IBAN("DE00000000000000000000") → false (ungültige Prüfsumme)
Validiert Kreditkartennummern mit Luhn-Algorithmus.
IS_CREDIT_CARD("4532015112830366") → true (Visa)
IS_CREDIT_CARD("5425233430109903") → true (Mastercard)
IS_CREDIT_CARD("1234 5678 9012 3456") → false
Bereinigt Eingaben für verschiedene Kontexte.
| Type | Beschreibung |
|---|---|
| "html" | HTML-Entitäten escapen (Standard) |
| "sql" | SQL-Injection verhindern |
| "json" | JSON-Sonderzeichen escapen |
| "filename" | Sichere Dateinamen |
-- HTML
SANITIZE("<script>alert('xss')</script>", "html")
→ "<script>alert('xss')</script>"
-- SQL
SANITIZE("O'Brien", "sql")
→ "O''Brien"
-- JSON
SANITIZE("Line1\nLine2", "json")
→ "Line1\\nLine2"
-- Filename
SANITIZE("../../../etc/passwd", "filename")
→ "etcpasswd"
Erkennt potenzielle Injection-Angriffe.
| Type | Erkannte Muster |
|---|---|
| "sql" | DROP, DELETE, UNION SELECT, --, etc. |
| "xss" | <script>, javascript:, onerror=, etc. |
| "path" | ../, .., %2e%2e |
| "cmd" | ;, |
| "all" | Alle Typen (Standard) |
-- SQL Injection
HAS_INJECTION("1'; DROP TABLE users--", "sql") → true
HAS_INJECTION("' OR '1'='1", "sql") → true
HAS_INJECTION("normal input", "sql") → false
-- XSS
HAS_INJECTION("<script>alert(1)</script>", "xss") → true
HAS_INJECTION("javascript:void(0)", "xss") → true
-- Path Traversal
HAS_INJECTION("../../../etc/passwd", "path") → true
-- Praxis: Input-Validierung
FOR input IN user_inputs
LET unsafe = HAS_INJECTION(input.value)
FILTER unsafe == false
RETURN input
Maskiert Zeichen in einem String.
-- Standard: Alles maskieren
MASK("secret") → "******"
-- Zeige erste und letzte Zeichen
MASK("1234567890", 0, 4) → "******7890"
MASK("secret", 1, 1) → "s****t"
-- Anderes Maskierungszeichen
MASK("password", 2, 2, '#') → "pa####rd"
Maskiert E-Mail-Adressen intelligent.
MASK_EMAIL("john.doe@example.com")
→ "j******e@e*****e.com"
MASK_EMAIL("a@b.de")
→ "a@b.de" (zu kurz zum Maskieren)
Zeigt nur die letzten 4 Ziffern.
MASK_CREDIT_CARD("4532015112830366")
→ "************0366"
MASK_CREDIT_CARD("4532 0151 1283 0366")
→ "************0366"
Zeigt Ländercode und letzte 4 Zeichen.
MASK_IBAN("DE89370400440532013000")
→ "DE**************3000"
Berechnet einen Hash-Wert (nicht-kryptografisch).
-- FNV-1a (Standard, schnell)
HASH("password") → "af63bd4c8601b7df"
-- DJB2
HASH("password", "djb2") → "7c9e6679350367a3"
-- Praxis: Deduplizierung
FOR doc IN documents
LET hash = HASH(doc.content)
COLLECT h = hash INTO grouped
RETURN { hash: h, count: LENGTH(grouped) }
Berechnet eine Prüfsumme.
-- CRC32 (Standard)
CHECKSUM("hello world") → 222957957
-- Adler32
CHECKSUM("hello world", "adler32") → 436929629
-- Praxis: Datenintegrität
FOR file IN files
LET stored = file.checksum
LET current = CHECKSUM(file.content)
RETURN { name: file.name, valid: stored == current }
LET input = {
email: @email,
phone: @phone,
password: @password
}
-- Validierung
LET validEmail = IS_EMAIL(input.email)
LET validPhone = IS_PHONE(input.phone)
LET noInjection = NOT HAS_INJECTION(input.email)
FILTER validEmail AND validPhone AND noInjection
-- Registrierung mit maskierten Daten im Log
INSERT {
email: input.email,
phone: input.phone,
_log: {
maskedEmail: MASK_EMAIL(input.email),
maskedPhone: MASK(input.phone, 0, 4)
}
} INTO users
FOR customer IN customers
RETURN {
id: customer._key,
email: MASK_EMAIL(customer.email),
phone: MASK(customer.phone, 0, 4),
iban: MASK_IBAN(customer.iban),
creditCard: MASK_CREDIT_CARD(customer.creditCard)
}
LET searchTerm = @query
-- Prüfe auf Injection
LET isSafe = NOT HAS_INJECTION(searchTerm)
FILTER isSafe
-- Sanitize für sichere Verwendung
LET cleanTerm = SANITIZE(searchTerm, "html")
FOR doc IN FULLTEXT(products, "description", cleanTerm)
RETURN doc
- Security-Funktionen erweitert: IS_IBAN, IS_CREDIT_CARD, IS_PHONE, IS_IP hinzugefügt
- Maskierung: MASK_EMAIL, MASK_CREDIT_CARD, MASK_IBAN
- Injection-Erkennung: HAS_INJECTION mit sql, xss, path, cmd Typen
- Hashing: HASH (FNV-1a, DJB2), CHECKSUM (CRC32, Adler32)
- Insgesamt: ~355 Funktionen in 13 Kategorien
- Collection-Funktionen (ARRAY, DICT, JSON, HOLIDAYS)
- Logical-Funktionen (AND, OR, IF, SWITCH, ALL, ANY)
- Excel-kompatible Funktionen (VLOOKUP, SUMPRODUCT, PMT)
- Date-Funktionen erweitert (NOW, TODAY, INTERVAL, WORKDAYS)
- CRS-Transformationen (ST_TRANSFORM, ETRS89/UTM)
- Initiale Version mit ~210 Funktionen
ThemisDB ermöglicht die Modellierung und Ausführung von Geschäftsprozessen mit vollständiger Datenmanipulation. Dieser Abschnitt zeigt, wie CRUD-Operationen (Create, Read, Update, Delete) in typischen Verwaltungsprozessen implementiert werden.
- Prozess als Graph: Jeder Prozess wird als Graph mit Knoten (Aktivitäten) und Kanten (Übergänge) modelliert
- Dokumente als Prozessinstanzen: Jede Prozessinstanz ist ein Dokument mit Status und Variablen
- Transaktionale Updates: Statusänderungen sind atomar und nachvollziehbar
- Audit-Trail: Alle Änderungen werden protokolliert
-- Neuen Antrag erstellen
LET antrag = {
_key: UUID(),
typ: "bauantrag",
status: "eingegangen",
antragsteller: @personId,
eingangsdatum: NOW(),
daten: {
grundstueck: @grundstueckId,
bauvorhaben: @bauvorhaben,
geschosszahl: @geschosse
},
dokumente: [],
sachbearbeiter: null,
_created: NOW(),
_history: [{
zeitpunkt: NOW(),
aktion: "eingereicht",
benutzer: @benutzer,
status: "eingegangen"
}]
}
INSERT antrag INTO antraege
RETURN NEW
-- Automatische Zuweisung nach Arbeitslast
LET freieSachbearbeiter = (
FOR sb IN sachbearbeiter
FILTER sb.abteilung == "bauamt"
LET offeneAntraege = LENGTH(
FOR a IN antraege
FILTER a.sachbearbeiter == sb._key
FILTER a.status NOT IN ["abgeschlossen", "abgelehnt"]
RETURN 1
)
SORT offeneAntraege ASC
LIMIT 1
RETURN sb._key
)
UPDATE @antragId WITH {
status: "in_bearbeitung",
sachbearbeiter: FIRST(freieSachbearbeiter),
_history: PUSH(OLD._history, {
zeitpunkt: NOW(),
aktion: "zugewiesen",
benutzer: "system",
status: "in_bearbeitung",
sachbearbeiter: FIRST(freieSachbearbeiter)
})
} IN antraege
RETURN NEW
-- Dokument zum Antrag hinzufügen
LET dokument = {
id: UUID(),
typ: @dokumentTyp,
name: SANITIZE(@filename, "filename"),
hochgeladen: NOW(),
von: @benutzer,
pfad: @speicherpfad,
mime: MIME_TYPE(@filename),
checksum: CHECKSUM(@inhalt)
}
-- Validierung
FILTER IS_VALID(@dokumentTyp, ["lageplan", "bauplan", "statik", "brandschutz"])
FILTER NOT HAS_INJECTION(@filename, "path")
UPDATE @antragId WITH {
dokumente: PUSH(OLD.dokumente, dokument),
_history: PUSH(OLD._history, {
zeitpunkt: NOW(),
aktion: "dokument_hinzugefuegt",
benutzer: @benutzer,
dokument: dokument.name
})
} IN antraege
RETURN NEW
-- Antrag lesen und Prüfungsergebnis eintragen
FOR antrag IN antraege
FILTER antrag._key == @antragId
-- Vollständigkeitsprüfung
LET erforderlicheDokumente = ["lageplan", "bauplan", "statik"]
LET vorhandeneDokumente = antrag.dokumente[*].typ
LET fehlend = MINUS(erforderlicheDokumente, vorhandeneDokumente)
LET vollstaendig = LENGTH(fehlend) == 0
-- Geo-Prüfung: Liegt Grundstück im Bebauungsplan-Gebiet?
LET grundstueck = DOCUMENT("grundstuecke", antrag.daten.grundstueck)
LET imBebauungsplan = (
FOR bp IN bebauungsplaene
FILTER ST_CONTAINS(bp.gebiet, grundstueck.geometrie)
RETURN bp
)
-- Ergebnis speichern
UPDATE antrag WITH {
pruefung: {
vollstaendig: vollstaendig,
fehlendeUnterlagen: fehlend,
bebauungsplan: FIRST(imBebauungsplan),
geprueft: NOW(),
pruefer: @benutzer
},
status: vollstaendig ? "geprueft" : "unvollstaendig",
_history: PUSH(antrag._history, {
zeitpunkt: NOW(),
aktion: "geprueft",
benutzer: @benutzer,
ergebnis: vollstaendig ? "vollstaendig" : "unvollstaendig"
})
} IN antraege
RETURN NEW
-- Entscheidung treffen
UPDATE @antragId WITH {
status: @entscheidung, -- "genehmigt" oder "abgelehnt"
entscheidung: {
datum: NOW(),
entscheider: @benutzer,
begruendung: @begruendung,
auflagen: @auflagen
},
_history: PUSH(OLD._history, {
zeitpunkt: NOW(),
aktion: "entschieden",
benutzer: @benutzer,
status: @entscheidung,
begruendung: @begruendung
})
} IN antraege
RETURN NEW
-- Antrag archivieren
LET antrag = DOCUMENT("antraege", @antragId)
-- Archiv-Eintrag erstellen
INSERT {
_key: antrag._key,
originalDaten: antrag,
archiviertAm: NOW(),
aufbewahrungsbis: DATE_ADD(NOW(), YEARS(10)),
kategorie: "bauantraege"
} INTO archiv
-- Original-Status aktualisieren
UPDATE @antragId WITH {
status: "archiviert",
archivReferenz: antrag._key,
_history: PUSH(OLD._history, {
zeitpunkt: NOW(),
aktion: "archiviert",
benutzer: @benutzer
})
} IN antraege
RETURN { archiviert: true, referenz: antrag._key }
FOR antrag IN antraege
FILTER antrag.status NOT IN ["abgeschlossen", "archiviert"]
COLLECT status = antrag.status WITH COUNT INTO anzahl
RETURN { status, anzahl }
FOR antrag IN antraege
FILTER antrag.status == "genehmigt"
LET eingangsdatum = antrag.eingangsdatum
LET entscheidungsdatum = antrag.entscheidung.datum
LET bearbeitungstage = WORKDAYS(eingangsdatum, entscheidungsdatum, HOLIDAYS("DE_2024"))
COLLECT typ = antrag.typ
AGGREGATE avgTage = AVG(bearbeitungstage), maxTage = MAX(bearbeitungstage)
RETURN { typ, avgTage, maxTage }
FOR sb IN sachbearbeiter
LET antraege = (
FOR a IN antraege
FILTER a.sachbearbeiter == sb._key
FILTER a.entscheidung != null
RETURN a
)
LET bearbeitungszeiten = (
FOR a IN antraege
LET tage = WORKDAYS(a.eingangsdatum, a.entscheidung.datum, HOLIDAYS("DE_2024"))
RETURN tage
)
RETURN {
sachbearbeiter: sb.name,
abgeschlossen: LENGTH(antraege),
avgBearbeitungszeit: AVG(bearbeitungszeiten),
genehmigungsquote: LENGTH(antraege[* FILTER CURRENT.status == "genehmigt"]) / LENGTH(antraege)
}
ThemisDB kann den aktuellen Prozessstand mit einem generischen Prozessmodell abgleichen (Conformance Checking) und Vorhersagen über das weitere Vorgehen sowie Zeitziele treffen.
Ein generisches Prozessmodell wird als Graph gespeichert:
-- Prozessmodell-Knoten (Aktivitäten)
INSERT [
{ _key: "start", name: "Antrag eingegangen", typ: "start",
avgDauer: 0, stdDauer: 0 },
{ _key: "pruefung", name: "Formale Prüfung", typ: "activity",
avgDauer: 2, stdDauer: 0.5 },
{ _key: "zuweisung", name: "Sachbearbeiter zuweisen", typ: "activity",
avgDauer: 1, stdDauer: 0.3 },
{ _key: "fachpruefung", name: "Fachliche Prüfung", typ: "activity",
avgDauer: 10, stdDauer: 3 },
{ _key: "nachforderung", name: "Unterlagen nachfordern", typ: "activity",
avgDauer: 14, stdDauer: 7 },
{ _key: "entscheidung", name: "Entscheidung treffen", typ: "gateway_xor",
avgDauer: 3, stdDauer: 1 },
{ _key: "genehmigt", name: "Genehmigung erteilt", typ: "end",
avgDauer: 1, stdDauer: 0.2 },
{ _key: "abgelehnt", name: "Antrag abgelehnt", typ: "end",
avgDauer: 1, stdDauer: 0.2 }
] INTO prozess_aktivitaeten
-- Prozessmodell-Kanten (Übergänge mit Wahrscheinlichkeiten)
INSERT [
{ _from: "prozess_aktivitaeten/start", _to: "prozess_aktivitaeten/pruefung",
wahrscheinlichkeit: 1.0 },
{ _from: "prozess_aktivitaeten/pruefung", _to: "prozess_aktivitaeten/zuweisung",
wahrscheinlichkeit: 0.7, bedingung: "vollstaendig" },
{ _from: "prozess_aktivitaeten/pruefung", _to: "prozess_aktivitaeten/nachforderung",
wahrscheinlichkeit: 0.3, bedingung: "unvollstaendig" },
{ _from: "prozess_aktivitaeten/nachforderung", _to: "prozess_aktivitaeten/pruefung",
wahrscheinlichkeit: 1.0 },
{ _from: "prozess_aktivitaeten/zuweisung", _to: "prozess_aktivitaeten/fachpruefung",
wahrscheinlichkeit: 1.0 },
{ _from: "prozess_aktivitaeten/fachpruefung", _to: "prozess_aktivitaeten/entscheidung",
wahrscheinlichkeit: 1.0 },
{ _from: "prozess_aktivitaeten/entscheidung", _to: "prozess_aktivitaeten/genehmigt",
wahrscheinlichkeit: 0.75 },
{ _from: "prozess_aktivitaeten/entscheidung", _to: "prozess_aktivitaeten/abgelehnt",
wahrscheinlichkeit: 0.25 }
] INTO prozess_uebergaenge
-- Prüfe ob ein Antrag dem Prozessmodell folgt
FOR antrag IN antraege
FILTER antrag._key == @antragId
-- Hole alle durchlaufenen Schritte aus History
LET durchlaufeneSchritte = (
FOR h IN antrag._history
SORT h.zeitpunkt ASC
RETURN h.aktion
)
-- Mapping: Aktion -> Prozessaktivität
LET statusMapping = {
"eingereicht": "start",
"geprueft": "pruefung",
"unvollstaendig": "nachforderung",
"zugewiesen": "zuweisung",
"fachlich_geprueft": "fachpruefung",
"entschieden": "entscheidung",
"genehmigt": "genehmigt",
"abgelehnt": "abgelehnt"
}
LET durchlaufeneAktivitaeten = (
FOR schritt IN durchlaufeneSchritte
LET aktivitaet = statusMapping[schritt]
FILTER aktivitaet != null
RETURN aktivitaet
)
-- Prüfe jeden Übergang auf Konformität
LET uebergangspruefung = (
FOR i IN 0..LENGTH(durchlaufeneAktivitaeten)-2
LET von = durchlaufeneAktivitaeten[i]
LET nach = durchlaufeneAktivitaeten[i+1]
-- Suche erlaubten Übergang im Modell
LET erlaubt = FIRST(
FOR ue IN prozess_uebergaenge
FILTER ue._from == CONCAT("prozess_aktivitaeten/", von)
FILTER ue._to == CONCAT("prozess_aktivitaeten/", nach)
RETURN true
)
RETURN {
von: von,
nach: nach,
konform: erlaubt == true,
index: i
}
)
LET alleKonform = ALL(uebergangspruefung[*].konform)
LET abweichungen = uebergangspruefung[* FILTER NOT CURRENT.konform]
RETURN {
antragId: antrag._key,
aktuellerStatus: antrag.status,
durchlaufeneSchritte: durchlaufeneAktivitaeten,
konformitaet: {
istKonform: alleKonform,
abweichungen: abweichungen,
konformitaetsgrad: (LENGTH(uebergangspruefung) - LENGTH(abweichungen))
/ MAX(LENGTH(uebergangspruefung), 1)
}
}
-- Vorhersage für einen Antrag basierend auf aktuellem Status
FOR antrag IN antraege
FILTER antrag._key == @antragId
-- Aktuelle Position im Prozess
LET aktuelleAktivitaet = CASE antrag.status
WHEN "eingegangen" THEN "start"
WHEN "in_bearbeitung" THEN "zuweisung"
WHEN "geprueft" THEN "fachpruefung"
WHEN "fachlich_geprueft" THEN "entscheidung"
ELSE antrag.status
END
-- Alle möglichen Pfade zum Ende berechnen (BFS)
LET moeglichePfade = (
FOR v, e, p IN 1..10 OUTBOUND
CONCAT("prozess_aktivitaeten/", aktuelleAktivitaet)
prozess_uebergaenge
OPTIONS { uniqueVertices: "path" }
FILTER v.typ IN ["end"]
LET pfadAktivitaeten = p.vertices[*].name
LET pfadDauern = p.vertices[*].avgDauer
LET pfadStdDauern = p.vertices[*].stdDauer
LET wahrscheinlichkeiten = p.edges[*].wahrscheinlichkeit
-- Gesamtwahrscheinlichkeit des Pfads
LET pfadWahrscheinlichkeit = PRODUCT(wahrscheinlichkeiten)
-- Erwartete Dauer (Summe der Aktivitätsdauern)
LET erwarteteDauer = SUM(pfadDauern)
-- Standardabweichung (Wurzel der Summe der Varianzen)
LET varianz = SUM(
FOR i IN 0..LENGTH(pfadStdDauern)-1
RETURN POW(pfadStdDauern[i], 2)
)
LET stdAbweichung = SQRT(varianz)
RETURN {
ziel: LAST(pfadAktivitaeten),
pfad: pfadAktivitaeten,
schritte: LENGTH(pfadAktivitaeten) - 1,
wahrscheinlichkeit: pfadWahrscheinlichkeit,
erwarteteDauerTage: erwarteteDauer,
stdAbweichungTage: stdAbweichung,
konfidenzintervall95: {
min: MAX(0, erwarteteDauer - 1.96 * stdAbweichung),
max: erwarteteDauer + 1.96 * stdAbweichung
}
}
)
-- Sortiere nach Wahrscheinlichkeit
LET sortiertePfade = (
FOR p IN moeglichePfade
SORT p.wahrscheinlichkeit DESC
RETURN p
)
-- Berechne gewichtete Durchschnittsdauer
LET gewichteteDauer = SUM(
FOR p IN moeglichePfade
RETURN p.erwarteteDauerTage * p.wahrscheinlichkeit
)
-- Nächster wahrscheinlichster Schritt
LET naechsterSchritt = FIRST(
FOR ue IN prozess_uebergaenge
FILTER ue._from == CONCAT("prozess_aktivitaeten/", aktuelleAktivitaet)
SORT ue.wahrscheinlichkeit DESC
LET zielAktivitaet = DOCUMENT(ue._to)
RETURN {
aktivitaet: zielAktivitaet.name,
wahrscheinlichkeit: ue.wahrscheinlichkeit,
erwarteteDauer: zielAktivitaet.avgDauer
}
)
-- Bisherige Bearbeitungszeit
LET bisherigeDauer = WORKDAYS(
antrag.eingangsdatum,
NOW(),
HOLIDAYS("DE_2024")
)
-- Zeitziel berechnen
LET zeitziel = WORKDAYS_ADD(
NOW(),
ROUND(gewichteteDauer),
HOLIDAYS("DE_2024")
)
RETURN {
antragId: antrag._key,
aktuellerStatus: antrag.status,
aktuelleAktivitaet: aktuelleAktivitaet,
bisherigeDauerTage: bisherigeDauer,
vorhersage: {
naechsterSchritt: naechsterSchritt,
erwartetesEnde: {
wahrscheinlichstesZiel: FIRST(sortiertePfade).ziel,
wahrscheinlichkeit: FIRST(sortiertePfade).wahrscheinlichkeit,
restdauerTage: ROUND(gewichteteDauer),
zeitziel: DATE_FORMAT(zeitziel, "%Y-%m-%d"),
konfidenzintervall: {
optimistisch: DATE_FORMAT(
WORKDAYS_ADD(NOW(), ROUND(FIRST(sortiertePfade).konfidenzintervall95.min), HOLIDAYS("DE_2024")),
"%Y-%m-%d"
),
pessimistisch: DATE_FORMAT(
WORKDAYS_ADD(NOW(), ROUND(FIRST(sortiertePfade).konfidenzintervall95.max), HOLIDAYS("DE_2024")),
"%Y-%m-%d"
)
}
},
moeglichePfade: sortiertePfade
}
}
{
"antragId": "BA-2024-001234",
"aktuellerStatus": "in_bearbeitung",
"aktuelleAktivitaet": "zuweisung",
"bisherigeDauerTage": 5,
"vorhersage": {
"naechsterSchritt": {
"aktivitaet": "Fachliche Prüfung",
"wahrscheinlichkeit": 1.0,
"erwarteteDauer": 10
},
"erwartetesEnde": {
"wahrscheinlichstesZiel": "Genehmigung erteilt",
"wahrscheinlichkeit": 0.75,
"restdauerTage": 14,
"zeitziel": "2024-12-20",
"konfidenzintervall": {
"optimistisch": "2024-12-16",
"pessimistisch": "2024-12-27"
}
},
"moeglichePfade": [
{
"ziel": "Genehmigung erteilt",
"pfad": ["Sachbearbeiter zuweisen", "Fachliche Prüfung", "Entscheidung treffen", "Genehmigung erteilt"],
"schritte": 3,
"wahrscheinlichkeit": 0.75,
"erwarteteDauerTage": 14,
"konfidenzintervall95": { "min": 10.2, "max": 17.8 }
},
{
"ziel": "Antrag abgelehnt",
"pfad": ["Sachbearbeiter zuweisen", "Fachliche Prüfung", "Entscheidung treffen", "Antrag abgelehnt"],
"schritte": 3,
"wahrscheinlichkeit": 0.25,
"erwarteteDauerTage": 14,
"konfidenzintervall95": { "min": 10.2, "max": 17.8 }
}
]
}
}-- Vorhersage für alle offenen Anträge mit Priorisierung
FOR antrag IN antraege
FILTER antrag.status NOT IN ["genehmigt", "abgelehnt", "archiviert"]
LET aktuelleAktivitaet = CASE antrag.status
WHEN "eingegangen" THEN "start"
WHEN "in_bearbeitung" THEN "zuweisung"
WHEN "geprueft" THEN "fachpruefung"
ELSE "fachpruefung"
END
-- Kürzester Pfad zum Ende
LET kuerzesterPfad = FIRST(
FOR v, e, p IN 1..10 OUTBOUND
CONCAT("prozess_aktivitaeten/", aktuelleAktivitaet)
prozess_uebergaenge
OPTIONS { uniqueVertices: "path" }
FILTER v.typ IN ["end"]
LET dauer = SUM(p.vertices[*].avgDauer)
SORT dauer ASC
LIMIT 1
RETURN dauer
)
LET bisherigeDauer = WORKDAYS(antrag.eingangsdatum, NOW(), HOLIDAYS("DE_2024"))
LET gesamtDauer = bisherigeDauer + kuerzesterPfad
-- Frist aus Antragsdaten (z.B. gesetzliche Bearbeitungsfrist)
LET frist = antrag.frist ?? 30 -- Standard: 30 Arbeitstage
LET zeitPuffer = frist - gesamtDauer
SORT zeitPuffer ASC -- Dringendste zuerst
RETURN {
antragId: antrag._key,
typ: antrag.typ,
status: antrag.status,
bisherigeDauer: bisherigeDauer,
restdauer: kuerzesterPfad,
gesamtdauer: gesamtDauer,
frist: frist,
zeitPuffer: zeitPuffer,
ampel: CASE
WHEN zeitPuffer < 0 THEN "rot"
WHEN zeitPuffer < 5 THEN "gelb"
ELSE "gruen"
END,
zeitziel: DATE_FORMAT(
WORKDAYS_ADD(NOW(), kuerzesterPfad, HOLIDAYS("DE_2024")),
"%Y-%m-%d"
)
}
-- Lerne Übergangswahrscheinlichkeiten aus abgeschlossenen Anträgen
LET historischeAntraege = (
FOR a IN antraege
FILTER a.status IN ["genehmigt", "abgelehnt", "archiviert"]
FILTER a._history != null
RETURN a
)
-- Zähle alle Übergänge
LET uebergangszaehlung = (
FOR antrag IN historischeAntraege
FOR i IN 0..LENGTH(antrag._history)-2
LET von = antrag._history[i].aktion
LET nach = antrag._history[i+1].aktion
LET dauer = WORKDAYS(
antrag._history[i].zeitpunkt,
antrag._history[i+1].zeitpunkt,
HOLIDAYS("DE_2024")
)
COLLECT vonAktion = von, nachAktion = nach
AGGREGATE
anzahl = COUNT(1),
avgDauer = AVG(dauer),
stdDauer = STDDEV(dauer)
RETURN {
von: vonAktion,
nach: nachAktion,
anzahl: anzahl,
avgDauer: avgDauer,
stdDauer: stdDauer
}
)
-- Berechne Wahrscheinlichkeiten pro Ausgangsknoten
LET mitWahrscheinlichkeiten = (
FOR ue IN uebergangszaehlung
LET gesamtVonKnoten = SUM(
FOR other IN uebergangszaehlung
FILTER other.von == ue.von
RETURN other.anzahl
)
RETURN MERGE(ue, {
wahrscheinlichkeit: ue.anzahl / gesamtVonKnoten
})
)
RETURN {
analysierteAntraege: LENGTH(historischeAntraege),
uebergaenge: mitWahrscheinlichkeiten
}
-- Echtzeit-Dashboard: Anträge mit SLA-Risiko
FOR antrag IN antraege
FILTER antrag.status NOT IN ["genehmigt", "abgelehnt", "archiviert"]
LET bisherigeDauer = WORKDAYS(antrag.eingangsdatum, NOW(), HOLIDAYS("DE_2024"))
-- Durchschnittliche Restdauer aus historischen Daten
LET avgRestdauer = FIRST(
FOR hist IN antraege
FILTER hist.status IN ["genehmigt", "abgelehnt"]
FILTER hist.typ == antrag.typ
LET gesamtDauer = WORKDAYS(hist.eingangsdatum, hist.entscheidung.datum, HOLIDAYS("DE_2024"))
COLLECT AGGREGATE avg = AVG(gesamtDauer)
RETURN avg - bisherigeDauer
) ?? 10
LET slaFrist = antrag.slaFrist ?? 20
LET voraussichtlichesDatum = WORKDAYS_ADD(NOW(), ROUND(avgRestdauer), HOLIDAYS("DE_2024"))
LET slaEingehalten = bisherigeDauer + avgRestdauer <= slaFrist
FILTER NOT slaEingehalten OR bisherigeDauer > slaFrist * 0.7 -- Warnung ab 70%
SORT (slaFrist - bisherigeDauer) ASC
RETURN {
antragId: antrag._key,
typ: antrag.typ,
sachbearbeiter: antrag.sachbearbeiter,
bisherigeDauer: bisherigeDauer,
slaFrist: slaFrist,
slaVerbrauch: ROUND(bisherigeDauer / slaFrist * 100) || "%",
voraussichtlichesDatum: DATE_FORMAT(voraussichtlichesDatum, "%Y-%m-%d"),
slaPrognose: slaEingehalten ? "OK" : "GEFÄHRDET",
handlungsbedarf: CASE
WHEN bisherigeDauer > slaFrist THEN "ÜBERFÄLLIG"
WHEN bisherigeDauer > slaFrist * 0.9 THEN "KRITISCH"
WHEN bisherigeDauer > slaFrist * 0.7 THEN "WARNUNG"
ELSE "OK"
END
}
Dieses Kapitel zeigt, wie mit vorhandenen AQL-Mitteln ein vollautomatisiertes Meilensteinmodell auf Prozess-Nodes implementiert werden kann.
-- Collection: _milestones
-- Definiert alle möglichen Meilensteine für einen Prozesstyp
INSERT {
_key: "M1_EINGEGANGEN",
process_type: "bauantrag",
name: "Antrag eingegangen",
description: "Der Antrag wurde formal entgegengenommen",
trigger_activity: "antrag_einreichen",
sla_hours: 1,
sla_type: "business_hours", -- calendar_hours | business_hours
is_critical: true,
is_reportable: true,
sequence: 1,
notify_on_reach: ["antragsteller"],
notify_on_overdue: ["teamleiter", "antragsteller"]
} INTO _milestones
INSERT {
_key: "M2_VOLLSTAENDIG",
process_type: "bauantrag",
name: "Vollständigkeitsprüfung abgeschlossen",
description: "Alle erforderlichen Unterlagen liegen vor",
trigger_activity: "vollstaendigkeit_pruefen",
sla_hours: 24,
sla_type: "business_hours",
is_critical: true,
is_reportable: true,
sequence: 2,
depends_on: "M1_EINGEGANGEN",
notify_on_reach: ["antragsteller"],
notify_on_overdue: ["sachbearbeiter", "teamleiter"]
} INTO _milestones
INSERT {
_key: "M3_FACHPRUEFUNG",
process_type: "bauantrag",
name: "Fachliche Prüfung abgeschlossen",
description: "Technische und rechtliche Prüfung durchgeführt",
trigger_activity: "fachlich_pruefen",
sla_hours: 120, -- 15 Arbeitstage
sla_type: "business_hours",
is_critical: true,
is_reportable: true,
sequence: 3,
depends_on: "M2_VOLLSTAENDIG"
} INTO _milestones
INSERT {
_key: "M4_ENTSCHEIDUNG",
process_type: "bauantrag",
name: "Entscheidung getroffen",
description: "Genehmigung oder Ablehnung beschlossen",
trigger_activity: "entscheiden",
sla_hours: 160, -- 20 Arbeitstage
sla_type: "business_hours",
is_critical: true,
is_reportable: true,
sequence: 4,
depends_on: "M3_FACHPRUEFUNG"
} INTO _milestones
INSERT {
_key: "M5_ZUGESTELLT",
process_type: "bauantrag",
name: "Bescheid zugestellt",
description: "Der Bescheid wurde dem Antragsteller zugestellt",
trigger_activity: "bescheid_versenden",
sla_hours: 168, -- 21 Arbeitstage (gesetzliche Frist)
sla_type: "business_hours",
is_critical: true,
is_reportable: true,
sequence: 5,
depends_on: "M4_ENTSCHEIDUNG"
} INTO _milestones
-- Collection: _milestone_instances
-- Tracking-Datensätze für jeden Vorgang
{
_key: "MI-V2024-0001-M1",
milestone_id: "M1_EINGEGANGEN",
vorgang_id: "V-2024-0001",
process_type: "bauantrag",
-- Zeitplanung
created_at: 1704060000000, -- Wann wurde Instanz erstellt
due_date: 1704063600000, -- Wann muss Meilenstein erreicht sein
reached_at: 1704061800000, -- Wann wurde er tatsächlich erreicht (null wenn noch offen)
-- Status
status: "reached", -- pending | reached | overdue | skipped | cancelled
-- Metriken
planned_duration_hours: 1,
actual_duration_hours: 0.5,
delay_hours: 0,
-- Kontext
reached_by_user: "mueller",
notes: "Antrag vollständig eingereicht"
}
-- Beim Anlegen eines neuen Vorgangs: Alle Meilensteine initialisieren
LET vorgang = DOCUMENT("antraege", "V-2024-0001")
LET processType = vorgang.process_type
LET startTime = vorgang.created_at
LET holidays = HOLIDAYS("DE_2024")
-- Alle Meilensteine für diesen Prozesstyp laden
FOR milestone IN _milestones
FILTER milestone.process_type == processType
SORT milestone.sequence ASC
-- Kumulierte SLA-Zeit berechnen (jeder Meilenstein baut auf dem vorherigen auf)
LET previousMilestones = (
FOR m IN _milestones
FILTER m.process_type == processType
FILTER m.sequence < milestone.sequence
RETURN m.sla_hours
)
LET cumulativeSlaHours = SUM(previousMilestones) + milestone.sla_hours
-- Due Date berechnen (mit Arbeitstagen)
LET dueDate = milestone.sla_type == "business_hours"
? WORKDAYS_ADD(startTime, CEIL(cumulativeSlaHours / 8), holidays)
: startTime + (cumulativeSlaHours * 3600 * 1000)
-- Meilenstein-Instanz erstellen
INSERT {
_key: CONCAT("MI-", vorgang._key, "-", milestone._key),
milestone_id: milestone._key,
vorgang_id: vorgang._key,
process_type: processType,
created_at: NOW(),
due_date: dueDate,
reached_at: null,
status: "pending",
planned_duration_hours: milestone.sla_hours,
actual_duration_hours: null,
delay_hours: null,
sequence: milestone.sequence
} INTO _milestone_instances
RETURN {
milestone: milestone.name,
due: DATE_FORMAT(dueDate, "%Y-%m-%d %H:%M"),
sla_hours: milestone.sla_hours
}
-- Wenn eine Aktivität abgeschlossen wird: Entsprechenden Meilenstein aktualisieren
LET vorgangId = "V-2024-0001"
LET activity = "vollstaendigkeit_pruefen"
LET completedAt = NOW()
LET completedBy = "mueller"
-- Meilenstein finden, der durch diese Aktivität getriggert wird
LET milestone = FIRST(
FOR m IN _milestones
FILTER m.trigger_activity == activity
RETURN m
)
-- Meilenstein-Instanz aktualisieren
FOR mi IN _milestone_instances
FILTER mi.vorgang_id == vorgangId
FILTER mi.milestone_id == milestone._key
LET actualDuration = (completedAt - mi.created_at) / (1000 * 3600)
LET delay = MAX([0, actualDuration - mi.planned_duration_hours])
LET newStatus = completedAt <= mi.due_date ? "reached" : "overdue"
UPDATE mi WITH {
reached_at: completedAt,
status: newStatus,
actual_duration_hours: actualDuration,
delay_hours: delay,
reached_by_user: completedBy
} IN _milestone_instances
RETURN {
milestone: milestone.name,
status: newStatus,
on_time: newStatus == "reached",
delay_hours: delay
}
-- Übersicht aller Vorgänge mit aktuellem Meilenstein-Status
FOR vorgang IN antraege
FILTER vorgang.status != "abgeschlossen"
-- Alle Meilenstein-Instanzen für diesen Vorgang
LET milestones = (
FOR mi IN _milestone_instances
FILTER mi.vorgang_id == vorgang._key
SORT mi.sequence ASC
FOR m IN _milestones
FILTER m._key == mi.milestone_id
LET verbleibendeZeit = mi.status == "pending"
? (mi.due_date - NOW()) / (1000 * 3600)
: null
LET ampel = mi.status == "reached" ? "grün"
: mi.status == "overdue" ? "rot"
: verbleibendeZeit < 0 ? "rot"
: verbleibendeZeit < 8 ? "gelb"
: "grün"
RETURN {
name: m.name,
sequence: mi.sequence,
status: mi.status,
due: DATE_FORMAT(mi.due_date, "%Y-%m-%d %H:%M"),
reached: mi.reached_at ? DATE_FORMAT(mi.reached_at, "%Y-%m-%d %H:%M") : null,
remaining_hours: ROUND(verbleibendeZeit, 1),
delay_hours: mi.delay_hours,
ampel: ampel,
is_critical: m.is_critical
}
)
-- Nächster fälliger Meilenstein
LET naechsterMeilenstein = FIRST(
FOR ms IN milestones
FILTER ms.status == "pending"
SORT ms.sequence ASC
LIMIT 1
RETURN ms
)
-- Gesamtstatus berechnen
LET hatUeberfaellige = LENGTH(
FOR ms IN milestones FILTER ms.ampel == "rot" RETURN 1
) > 0
LET hatWarnungen = LENGTH(
FOR ms IN milestones FILTER ms.ampel == "gelb" RETURN 1
) > 0
RETURN {
vorgang_id: vorgang._key,
antragsteller: vorgang.antragsteller,
typ: vorgang.process_type,
gesamtstatus: hatUeberfaellige ? "KRITISCH" : hatWarnungen ? "WARNUNG" : "OK",
naechster_meilenstein: naechsterMeilenstein,
meilensteine: milestones,
fortschritt: CONCAT(
LENGTH(FOR ms IN milestones FILTER ms.status == "reached" RETURN 1),
" / ",
LENGTH(milestones)
)
}
-- Aggregierte Statistiken für alle Meilensteine eines Typs
FOR milestone IN _milestones
FILTER milestone.process_type == "bauantrag"
LET instances = (
FOR mi IN _milestone_instances
FILTER mi.milestone_id == milestone._key
RETURN mi
)
LET reached = (FOR i IN instances FILTER i.status == "reached" RETURN i)
LET overdue = (FOR i IN instances FILTER i.status == "overdue" RETURN i)
LET pending = (FOR i IN instances FILTER i.status == "pending" RETURN i)
LET slaEinhaltung = LENGTH(instances) > 0
? ROUND(LENGTH(reached) * 100 / LENGTH(instances), 1)
: null
LET avgDuration = LENGTH(reached) > 0
? ROUND(AVG(FOR r IN reached RETURN r.actual_duration_hours), 1)
: null
LET avgDelay = LENGTH(overdue) > 0
? ROUND(AVG(FOR o IN overdue RETURN o.delay_hours), 1)
: null
RETURN {
meilenstein: milestone.name,
sla_stunden: milestone.sla_hours,
erreicht: LENGTH(reached),
ueberfaellig: LENGTH(overdue),
offen: LENGTH(pending),
gesamt: LENGTH(instances),
sla_einhaltung_prozent: slaEinhaltung,
durchschnittliche_dauer_stunden: avgDuration,
durchschnittliche_verzoegerung_stunden: avgDelay
}
-- Täglich ausführen: Finde gefährdete kritische Meilensteine und eskaliere
LET warnschwelleStunden = 8 -- 1 Arbeitstag Vorlauf
FOR mi IN _milestone_instances
FILTER mi.status == "pending"
FOR m IN _milestones
FILTER m._key == mi.milestone_id
FILTER m.is_critical == true
LET verbleibendeStunden = (mi.due_date - NOW()) / (1000 * 3600)
-- Eskalieren wenn weniger als Schwellwert übrig
FILTER verbleibendeStunden < warnschwelleStunden AND verbleibendeStunden > 0
-- Vorgang laden
LET vorgang = DOCUMENT("antraege", mi.vorgang_id)
-- Eskalationsnachricht erstellen
INSERT {
_key: CONCAT("ESK-", mi._key, "-", DATE_FORMAT(NOW(), "%Y%m%d")),
type: "milestone_warning",
milestone_id: mi.milestone_id,
milestone_name: m.name,
vorgang_id: mi.vorgang_id,
due_date: mi.due_date,
remaining_hours: ROUND(verbleibendeStunden, 1),
severity: verbleibendeStunden < 4 ? "high" : "medium",
recipients: m.notify_on_overdue,
created_at: NOW(),
message: CONCAT(
"Meilenstein '", m.name, "' für Vorgang ", mi.vorgang_id,
" läuft in ", ROUND(verbleibendeStunden, 1), " Stunden ab!"
)
} INTO _escalations OPTIONS { ignoreErrors: true }
RETURN {
vorgang: mi.vorgang_id,
meilenstein: m.name,
ablauf_in_stunden: ROUND(verbleibendeStunden, 1),
empfaenger: m.notify_on_overdue
}
-- Vollständiger Meilenstein-Report für einen einzelnen Vorgang
LET vorgangId = "V-2024-0001"
LET vorgang = DOCUMENT("antraege", vorgangId)
LET meilensteine = (
FOR mi IN _milestone_instances
FILTER mi.vorgang_id == vorgangId
SORT mi.sequence ASC
FOR m IN _milestones
FILTER m._key == mi.milestone_id
RETURN {
nr: mi.sequence,
name: m.name,
beschreibung: m.description,
sla_stunden: m.sla_hours,
faellig_am: DATE_FORMAT(mi.due_date, "%d.%m.%Y %H:%M"),
erreicht_am: mi.reached_at ? DATE_FORMAT(mi.reached_at, "%d.%m.%Y %H:%M") : "-",
status: mi.status,
dauer_stunden: mi.actual_duration_hours,
verzoegerung_stunden: mi.delay_hours,
bearbeiter: mi.reached_by_user
}
)
LET erreicht = LENGTH(FOR ms IN meilensteine FILTER ms.status == "reached" RETURN 1)
LET gesamt = LENGTH(meilensteine)
LET onTime = LENGTH(FOR ms IN meilensteine FILTER ms.status == "reached" AND ms.verzoegerung_stunden == 0 RETURN 1)
RETURN {
vorgang: {
id: vorgangId,
typ: vorgang.process_type,
antragsteller: vorgang.antragsteller,
eingereicht_am: DATE_FORMAT(vorgang.created_at, "%d.%m.%Y")
},
fortschritt: {
erreicht: erreicht,
gesamt: gesamt,
prozent: ROUND(erreicht * 100 / gesamt),
on_time: onTime,
verspaetet: erreicht - onTime
},
meilensteine: meilensteine,
naechster_schritt: FIRST(
FOR ms IN meilensteine
FILTER ms.status == "pending"
LIMIT 1
RETURN ms
)
}
-- Prognose der Gesamtdurchlaufzeit basierend auf bisherigem Fortschritt
LET vorgangId = "V-2024-0001"
LET meilensteine = (
FOR mi IN _milestone_instances
FILTER mi.vorgang_id == vorgangId
SORT mi.sequence ASC
FOR m IN _milestones
FILTER m._key == mi.milestone_id
RETURN MERGE(mi, { sla_hours: m.sla_hours, name: m.name })
)
-- Bisherige Performance berechnen
LET erreicht = (FOR ms IN meilensteine FILTER ms.status == "reached" RETURN ms)
LET offen = (FOR ms IN meilensteine FILTER ms.status == "pending" RETURN ms)
LET durchschnittlicheAbweichung = LENGTH(erreicht) > 0
? AVG(FOR e IN erreicht RETURN (e.actual_duration_hours - e.sla_hours) / e.sla_hours)
: 0
-- Restdauer prognostizieren
LET geplanteSlaRestStunden = SUM(FOR o IN offen RETURN o.sla_hours)
LET prognostizierteRestStunden = geplanteSlaRestStunden * (1 + durchschnittlicheAbweichung)
-- Enddatum prognostizieren
LET jetzt = NOW()
LET holidays = HOLIDAYS("DE_2024")
LET prognostiziertesEnde = WORKDAYS_ADD(jetzt, CEIL(prognostizierteRestStunden / 8), holidays)
RETURN {
vorgang_id: vorgangId,
meilensteine_erreicht: LENGTH(erreicht),
meilensteine_offen: LENGTH(offen),
durchschnittliche_abweichung_prozent: ROUND(durchschnittlicheAbweichung * 100, 1),
geplante_rest_stunden: geplanteSlaRestStunden,
prognostizierte_rest_stunden: ROUND(prognostizierteRestStunden, 1),
prognostiziertes_ende: DATE_FORMAT(prognostiziertesEnde, "%d.%m.%Y"),
konfidenz: durchschnittlicheAbweichung < 0.1 ? "hoch"
: durchschnittlicheAbweichung < 0.3 ? "mittel"
: "niedrig"
}
A: AQL ist eine Multi-Model-Abfragesprache, die SQL-ähnliche Syntax mit Graph-, Vector- und Geo-Operationen kombiniert:
| Aspekt | SQL | AQL |
|---|---|---|
| Iteration | SELECT ... FROM table |
FOR doc IN collection |
| Filter | WHERE column = value |
FILTER doc.field == value |
| Joins | JOIN table2 ON ... |
Graph-Traversierung oder FOR ... IN
|
| Aggregation | GROUP BY |
COLLECT ... AGGREGATE |
| Updates | UPDATE table SET ... |
UPDATE doc WITH {...} IN collection |
| Multi-Model | Nicht möglich | Graph + Vector + Geo in einer Query |
A: Beide erzeugen Arrays, aber mit unterschiedlichen Fähigkeiten:
-- Native Syntax (für statische Werte)
LET arr = [1, 2, 3]
-- ARRAY() Funktion (für JSON-Parsing)
LET arr = ARRAY('[1, 2, 3]') -- parst JSON-String!
LET arr = ARRAY(singleValue) -- Typ-Coercion
Empfehlung: Native Syntax für Literale, Funktionen für dynamische Konstruktion.
A: Excel- und SQL-kompatible Aliase:
| Alias | Ziel-Funktion | Herkunft |
|---|---|---|
| CEILING | CEIL | Excel |
| CONCATENATE | CONCAT | Excel |
| LEN | LENGTH | Excel/SQL |
| MID | SUBSTRING | Excel |
| LCASE | LOWER | SQL |
| UCASE | UPPER | SQL |
| POWER | POW | SQL |
| OBJECT | DICT | Alternative |
A: ThemisDB unterstützt ACID-Transaktionen:
-- Transaktion mit mehreren Operationen
LET transfer = (
-- Abbuchung
UPDATE "konto_a" WITH { saldo: OLD.saldo - @betrag } IN konten
-- Gutschrift
UPDATE "konto_b" WITH { saldo: OLD.saldo + @betrag } IN konten
)
RETURN { success: true }
Bei Fehlern wird die gesamte Transaktion zurückgerollt.
A: Nutzen Sie das _history-Feld-Pattern:
UPDATE docId WITH {
feldname: neuerWert,
_history: PUSH(OLD._history, {
zeitpunkt: NOW(),
feld: "feldname",
alterWert: OLD.feldname,
neuerWert: neuerWert,
benutzer: @benutzer,
grund: @aenderungsgrund
})
} IN collection
A: Kombinieren Sie Validierungsfunktionen:
LET validierung = {
emailGueltig: IS_EMAIL(@email),
telefonGueltig: IS_PHONE(@telefon),
keineInjection: NOT HAS_INJECTION(CONCAT(@email, @name), "sql"),
pflichtfelderVorhanden: @name != null AND @email != null
}
LET alleGueltig = ALL(VALUES(validierung))
FILTER alleGueltig
INSERT { ... } INTO collection
RETURN { success: true }
A: Markieren statt löschen:
-- Soft-Delete
UPDATE docId WITH {
_deleted: true,
_deletedAt: NOW(),
_deletedBy: @benutzer
} IN collection
-- Normale Abfragen ignorieren gelöschte
FOR doc IN collection
FILTER doc._deleted != true
RETURN doc
-- Gelöschte wiederherstellen
UPDATE docId WITH {
_deleted: null,
_deletedAt: null,
_deletedBy: null,
_restoredAt: NOW(),
_restoredBy: @benutzer
} IN collection
A: Befolgen Sie diese Regeln:
- Indizes nutzen: FILTER-Bedingungen auf indizierte Felder
- Früh filtern: FILTER vor aufwändigen Operationen
- LIMIT verwenden: Ergebnismenge begrenzen
- Projektionen: Nur benötigte Felder zurückgeben
- Subqueries vermeiden: Wenn möglich durch JOINs ersetzen
-- Schlecht
FOR doc IN large_collection
LET related = (FOR r IN other FILTER r.parent == doc._key RETURN r)
RETURN { doc, related }
-- Besser
FOR doc IN large_collection
FOR related IN other
FILTER related.parent == doc._key
COLLECT parentDoc = doc INTO relatedDocs
RETURN { doc: parentDoc, related: relatedDocs[*].related }
A: Index-fähige Funktionen:
| Funktion | Index-Typ | Beschreibung |
|---|---|---|
| GEO_DISTANCE | Geo-Index | Räumliche Suche |
| GEO_CONTAINS | Geo-Index | Enthaltensein |
| FULLTEXT | Volltext-Index | Textsuche |
| SIMILARITY | Vector-Index | Ähnlichkeitssuche |
| DOCUMENT | Primary-Key | Direktzugriff |
A: ThemisDB verwendet Bind-Parameter:
-- NIEMALS so (unsicher!)
FOR doc IN collection
FILTER doc.name == "${userInput}" -- GEFÄHRLICH!
-- IMMER so (sicher)
FOR doc IN collection
FILTER doc.name == @userInput -- Bind-Parameter
Zusätzlich: HAS_INJECTION() zur Validierung.
A: Nutzen Sie die MASK-Funktionen:
LET logEntry = {
email: MASK_EMAIL(user.email), -- u***@example.com
kreditkarte: MASK_CREDIT_CARD(cc), -- ****-****-****-1234
iban: MASK_IBAN(iban), -- DE**************4321
telefon: MASK(telefon, 0, 4) -- +49 *** ****4567
}
INSERT logEntry INTO audit_log
A: AQL-Syntax ist weitgehend kompatibel. Hauptunterschiede:
-- ArangoDB -- ThemisDB
DOCUMENT(collection, key) -- DOCUMENT(collection, key) ✓
FOR v IN 1..3 OUTBOUND -- FOR v IN 1..3 OUTBOUND ✓
COLLECT ... INTO groups -- COLLECT ... INTO groups ✓
-- Neue ThemisDB-Features
ST_TRANSFORM(geom, 25832) -- CRS-Transformation
SIMILARITY(vec1, vec2, k) -- Vector-Suche
WORKDAYS(start, end, holidays) -- Arbeitstage
HOLIDAYS("DE_2024") -- Feiertags-Kalender
A: Syntax-Übersetzung:
-- Cypher
MATCH (p:Person)-[:KNOWS]->(f:Person)
WHERE p.name = 'Alice'
RETURN f.name-- AQL
FOR p IN persons
FILTER p.name == 'Alice'
FOR f IN 1..1 OUTBOUND p knows
RETURN f.name
A: Aggregation-Pipeline zu AQL:
// MongoDB
db.orders.aggregate([
{ $match: { status: "completed" } },
{ $group: { _id: "$customerId", total: { $sum: "$amount" } } },
{ $sort: { total: -1 } }
])-- AQL
FOR order IN orders
FILTER order.status == "completed"
COLLECT customerId = order.customerId
AGGREGATE total = SUM(order.amount)
SORT total DESC
RETURN { customerId, total }
ThemisDB v1.3.4 | GitHub | Documentation | Discussions | License
Last synced: January 02, 2026 | Commit: 6add659
Version: 1.3.0 | Stand: Dezember 2025
- Übersicht
- Home
- Dokumentations-Index
- Quick Reference
- Sachstandsbericht 2025
- Features
- Roadmap
- Ecosystem Overview
- Strategische Übersicht
- Geo/Relational Storage
- RocksDB Storage
- MVCC Design
- Transaktionen
- Time-Series
- Memory Tuning
- Chain of Thought Storage
- Query Engine & AQL
- AQL Syntax
- Explain & Profile
- Rekursive Pfadabfragen
- Temporale Graphen
- Zeitbereichs-Abfragen
- Semantischer Cache
- Hybrid Queries (Phase 1.5)
- AQL Hybrid Queries
- Hybrid Queries README
- Hybrid Query Benchmarks
- Subquery Quick Reference
- Subquery Implementation
- Content Pipeline
- Architektur-Details
- Ingestion
- JSON Ingestion Spec
- Enterprise Ingestion Interface
- Geo-Processor Design
- Image-Processor Design
- Hybrid Search Design
- Fulltext API
- Hybrid Fusion API
- Stemming
- Performance Tuning
- Migration Guide
- Future Work
- Pagination Benchmarks
- Enterprise README
- Scalability Features
- HTTP Client Pool
- Build Guide
- Implementation Status
- Final Report
- Integration Analysis
- Enterprise Strategy
- Verschlüsselungsstrategie
- Verschlüsselungsdeployment
- Spaltenverschlüsselung
- Encryption Next Steps
- Multi-Party Encryption
- Key Rotation Strategy
- Security Encryption Gap Analysis
- Audit Logging
- Audit & Retention
- Compliance Audit
- Compliance
- Extended Compliance Features
- Governance-Strategie
- Compliance-Integration
- Governance Usage
- Security/Compliance Review
- Threat Model
- Security Hardening Guide
- Security Audit Checklist
- Security Audit Report
- Security Implementation
- Development README
- Code Quality Pipeline
- Developers Guide
- Cost Models
- Todo Liste
- Tool Todo
- Core Feature Todo
- Priorities
- Implementation Status
- Roadmap
- Future Work
- Next Steps Analysis
- AQL LET Implementation
- Development Audit
- Sprint Summary (2025-11-17)
- WAL Archiving
- Search Gap Analysis
- Source Documentation Plan
- Changefeed README
- Changefeed CMake Patch
- Changefeed OpenAPI
- Changefeed OpenAPI Auth
- Changefeed SSE Examples
- Changefeed Test Harness
- Changefeed Tests
- Dokumentations-Inventar
- Documentation Summary
- Documentation TODO
- Documentation Gap Analysis
- Documentation Consolidation
- Documentation Final Status
- Documentation Phase 3
- Documentation Cleanup Validation
- API
- Authentication
- Cache
- CDC
- Content
- Geo
- Governance
- Index
- LLM
- Query
- Security
- Server
- Storage
- Time Series
- Transaction
- Utils
Vollständige Dokumentation: https://makr-code.github.io/ThemisDB/