Skip to content
Reider85Public

About

Next-generation hybrid search — PostgreSQL + Qdrant Lexical + vector channels, transactional outbox, cross-encoder rerank, OTel tracing, A/B framework, and nightly offline eval — production-ready out of the box.

Topics

Resources

Contributing

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

74 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

🔍 OriaHS

Next-generation hybrid search — PostgreSQL + Qdrant

Lexical + vector channels, transactional outbox, cross-encoder rerank, OTel tracing, A/B framework, and nightly offline eval — production-ready out of the box.

License: Apache 2.0 Python 3.11+ FastAPI PostgreSQL 16 Qdrant 1.x Redis 7 uv OpenTelemetry Status: Production-Ready

Features · Architecture · Quick Start · Phases · API · Observability · Testing · Contributing


📖 About

OriaHS is a hybrid search backend that combines two orthogonal ranking channels:

  • 🔤 Lexical channel — PostgreSQL 16 + pg_trgm + tsvector (BM25-style + trigrams, exact matches, rare terms).
  • 🧠 Vector channel — Qdrant 1.x + bge-m3 (1024-dim, multilingual, kNN over dense vectors, semantic similarity).
  • 🔀 Fusion layer — RRF, weighted fusion, cross-encoder rerank (bge-reranker-v2-m3), MMR diversification.
  • 🛡 Safety nets — transactional outbox + reconciler, circuit breaker, degraded mode, adaptive throttle, chaos engineering.
  • 📊 Data-driven quality — nightly offline eval with regression gate, A/B framework with sticky distribution, shadow traffic.
  • 🔭 Full observability — Prometheus + Grafana (8 dashboards) + OpenTelemetry → Tempo + Loki (trace↔log correlation).

Channels are synchronized through dual-write with outbox reconciliation: Postgres is the source of truth, Qdrant is populated by a background reconciler worker with exponential backoff and a dead state for unrecoverable errors. SLO lag — ≤ 5 seconds.

📐 The architecture is built on TRIZ principles (Altshuller's 40 inventive principles): 38 TRIZ-gates are distributed across 4 maturity phases and embedded into the code from day one, not "later". See analitics/ARCHITECT.md and analitics/ROADMAP.md for details.


✨ Features

🔤 Lexical channel (PostgreSQL 16)

  • tsvector GENERATED column with GIN index for full-text search.
  • pg_trgm trigrams for fuzzy matching and typo tolerance.
  • Partial index WHERE deleted_at IS NULL — saves space and speeds up scans.
  • JSONB indexes on attributes (jsonb_path_ops) and tags (GIN array).

🧠 Vector channel (Qdrant 1.x)

  • Collection documents: 1024-dim Cosine, shard_number=4, replication_factor=2, write_consistency=majority.
  • HNSW: m=16, ef_construct=200, full_scan_threshold=10000.
  • Scalar int8 quantization, quantile=0.99, always_ram=true.
  • Payload indexes on tenant_id, language, tags, attributes.category, attributes.price, model_name+model_rev, created_at.

🔀 Fusion & Rerank

  • RRF (Reciprocal Rank Fusion, k=60) — default.
  • Weighted fusion — α·BM25 + (1-α)·cosine, min-max normalization, α=0.5.
  • Cross-encoder rerank — bge-reranker-v2-m3 on top-50, p99 ≤ 350 ms.
  • Speculative rerank — cross-encoder runs in parallel with the Qdrant query on lex top-10, saving ~80 ms on p99.
  • MMR diversification — anti-bubble, λ=0.7, adaptive λ on diversity_score drop.

🛡 Safety nets

  • Transactional outbox + 30-second reconciler with SKIP LOCKED, exponential backoff, dead state.
  • Digest worker — hourly aggregation of dead records by (tenant_id, model_name, error_type).
  • Circuit breaker for reranker (rolling window 60s, half-open reopen).
  • Degraded mode — automatic fallback to lex-only on Qdrant failure + partial_result() on DeadlineExceeded.
  • Adaptive throttle — 202 Accepted when pending_count > 50k, full reindex when > 100k.
  • Push-down filters — highly selective filters go to Postgres, Qdrant receives a pre-filtered candidate list (3–5× speedup).

📊 Data-driven quality

  • Nightly offline eval — Recall@10, nDCG@10, MRR; regression gate (Recall ↓2%, nDCG ↓1%).
  • A/B framework — sticky by user_id (consistent hashing), sequential testing.
  • Shadow traffic — parallel computation of new strategy with logging into search_events.variant.
  • Personalization — profile_vector (EMA on click), contextual profiles (device/time/session).
  • Adaptive K — K_lexical/K_vector adapt to tenant_search_stats (doc_count, avg_query_len).

🔭 Observability

  • Prometheus metrics (/metrics): latency, throughput, lag, cache hit rate, circuit breaker state, eval metrics, throttle.
  • Grafana: 4 MVP dashboards + 4 Critical dashboards (8 total).
  • OpenTelemetry SDK → Tempo; trace↔log correlation by trace_id.
  • Structured JSON logs with request_id, tenant_id, trace_id.

🧪 Engineering quality

  • async SQLAlchemy 2.x (asyncpg), no sync engine.
  • Alembic migrations (idempotent, with downgrade).
  • Schema drift contract tests — QdrantPayload ↔ documents Postgres, CI gate.
  • Chaos engineering weekly — kill Qdrant / delay reranker / fill Redis, postcondition checks in CI.
  • Migration framework — blue-green collections, 1% canary reindex, snapshot-on-reindex, rollback.

🏗 Architecture

flowchart LR
    Client([Client])
    subgraph API[API Layer - FastAPI]
        Search[Search Orchestrator]
        Index[Index API]
    end
    subgraph Stores[Storage]
        PG[(PostgreSQL 16<br/>pg_trgm + pgvector)]
        QD[(Qdrant 1.x<br/>bge-m3 1024-dim)]
        Redis[(Redis 7<br/>embeddings + facets cache)]
    end
    subgraph Workers[Background]
        Emb[Embedding Worker]
        RR[Reranker Service<br/>bge-reranker-v2-m3]
        Rec[Reconciler<br/>outbox → Qdrant]
        Dig[Digest Worker<br/>dead-letter aggregator]
    end
    subgraph Obs[Observability]
        OTel[OTel Collector]
        Prom[Prometheus]
        Graf[Grafana 8 dashboards]
        Tempo[Tempo traces]
        Loki[Loki logs]
    end

    Client -->|/search| Search
    Client -->|/index| Index
    Search -->|BM25 + filters| PG
    Search -->|kNN + payload filter| QD
    Search -->|query embedding cache| Redis
    Search -->|rerank top-K| RR
    Search -.->|metrics/traces| OTel
    Index -->|1. INSERT doc + outbox| PG
    Index -->|2. enqueue| Emb
    Emb -->|3. upsert vector + payload| QD
    Emb -->|4. mark outbox done| PG
    Rec -->|scan failed outbox| PG
    Rec -->|retry upsert| QD
    Dig -->|hourly aggregate dead| PG
    PG -.->|metrics| OTel
    QD -.->|metrics| OTel
    OTel --> Prom --> Graf
    OTel --> Tempo
    OTel --> Loki
Loading

📦 Stack

Layer Technology Version
API FastAPI + Uvicorn (gunicorn workers) async
ORM SQLAlchemy 2.x (async) + asyncpg —
Settings pydantic-settings env > .env
Lexical store PostgreSQL + pg_trgm + pgvector 16
Vector store Qdrant (HNSW + payload filters) 1.x
Cache / queue Redis Streams 7
Embeddings BAAI/bge-m3 (1024-dim multilingual) —
Rerank BAAI/bge-reranker-v2-m3 (cross-encoder) —
Metrics Prometheus + Grafana 2 / latest
Tracing OpenTelemetry SDK → Tempo —
Logs Structured JSON → Loki —
Migrations Alembic (async engine) —
Package manager uv latest
Python CPython 3.11+

🚀 Quick Start

1. Bring up the infrastructure

# bash / Git Bash / WSL
./scripts/infra_up.sh

# PowerShell
.\scripts\infra_up.ps1

The script is idempotent and runs the full path from zero to a working stack:

  1. Creates .env from .env.example (if it doesn't exist).
  2. Builds the application image and brings up the containers.
  3. Waits for healthchecks.
  4. Creates PG extensions (vector — it's not in any migration).
  5. Applies migrations alembic upgrade head.
  6. Creates the documents collection via scripts/init_qdrant.py.
  7. Verifies GET /health/ready and prints a summary of URLs.
🔧 Common flags
./scripts/infra_up.sh --no-app            # only postgres, qdrant, redis, prometheus, grafana
./scripts/infra_up.sh --rebuild           # rebuild image without cache
./scripts/infra_up.sh --seed-eval         # load eval corpus and wait for Qdrant sync
./scripts/infra_up.sh --seed-eval --nightly  # + run the nightly offline eval
📋 Manual variant (without the script)
cp .env.example .env
docker compose --profile app up -d
docker compose run --rm migrate          # alembic upgrade head
docker compose run --rm init-qdrant      # scripts/init_qdrant.py
docker compose exec postgres psql -U postgres -d orlahs -c "CREATE EXTENSION IF NOT EXISTS vector;"

2. Tear down the infrastructure

./scripts/infra_down.sh              # rollback schema + stop, volumes preserved
./scripts/infra_down.sh -v            # same, but volumes are deleted (data is lost)
./scripts/infra_down.sh -v --purge    # + remove image and build cache

⚠️ alembic downgrade base runs before stopping the containers — otherwise the DB is unavailable and rollback is impossible. PostgreSQL extensions are preserved on rollback (by design in 001_documents.downgrade()).

3. Local development (hot-reload)

uv sync                 # creates .venv and installs dependencies
uv run uvicorn app.main:app --reload --port 8000

Verify: curl http://localhost:8000/health/live → {"status":"alive"} ✅


🌐 Ports and services

Service Port (.env) Purpose
api API_PORT=8000 REST API, /metrics, /health/*
postgres POSTGRES_PORT=5433 documents, outbox, eval datasets, search_events
qdrant QDRANT_PORT=6333 vector index (gRPC QDRANT_GRPC_PORT=6334)
redis REDIS_PORT=6379 embeddings cache, Streams queue, facets cache
prometheus PROMETHEUS_PORT=9090 data source for Grafana dashboards
grafana GRAFANA_PORT=3000 4 MVP + 4 Critical dashboards
tempo — distributed traces (OTel export)
loki — structured logs (trace_id correlation)

ℹ️ POSTGRES_PORT in .env.example is 5433 because 5432 is often taken by a local PostgreSQL. Inside the compose network it's always postgres:5432.

📦 Compose profiles

Profile Services When to use
— (default) postgres, qdrant, redis, prometheus, grafana, migrate, init-qdrant infrastructure and one-shot jobs
app api, reconciler, digest full stack (default in infra_up.sh)
tools seed-eval loading the eval corpus for C-07
nightly nightly-eval nightly offline eval (C-07), one-shot job

reconciler and digest are separate processes (python -m app.reconciler.worker / python -m app.reconciler.digest): Qdrant is populated by the outbox reconciler, not by the API.


🎯 Development phases

The project follows a 4-category maturity methodology: each phase is a self-contained deliverable, after which the system remains in an operationally correct state.

✅ MVP — minimal working loop

Goal: end-to-end hybrid search: lex+vec, RRF, dual-write+outbox. NFR: p99 /search ≤ 200 ms, ≥ 500 doc/min indexing, ≥ 100 RPS, lag ≤ 30s, SLA 99%.

ID Component Artifact
P-00 Project bootstrap docker-compose.yml, pyproject.toml, app/ skeleton, alembic init
P-01 PG documents (source of truth) alembic/versions/001_documents.py, app/db/models/document.py
P-02 PG search_outbox (with dead state) alembic/versions/002_outbox.py, app/db/models/outbox.py
P-03 PG embedding_models (with is_active) alembic/versions/003_embedding_models.py, seed bge-m3-v1
P-04 Qdrant collection documents qdrant_collections.yaml, scripts/init_qdrant.py, app/search/qdrant_payload.py
P-05 Embedding service bge-m3 (batching 32) app/embedding/service.py
P-06 Embedding cache Redis (content-hash + query + fast-path) app/embedding/cache.py
P-07 POST /index (dual-write + outbox + Redis Streams) app/api/routes/index.py
P-08 DELETE /index/{doc_id} (soft-delete + outbox op='delete') app/api/routes/index.py
P-09 Lexical search (ts_rank + pg_trgm + filters, K=50) app/search/lexical.py
P-10 Vector search (Qdrant kNN + payload filter, K=50) app/search/vector.py
P-11 POST /search orchestrator + RRF (k=60) app/api/routes/search.py, app/search/fusion.py
P-12 GET /health/* + /metrics app/api/routes/health.py
P-13 Reconciler worker (30s poll, retry, dead) app/reconciler/worker.py
P-14 Digest worker (hourly dead-letter aggregation) app/reconciler/digest.py, migration 004_dead_digest
P-15 Observability baseline (Prometheus + structured JSON logs) app/observability/metrics.py
P-16 Grafana dashboards (4 MVP) grafana/provisioning/*.json
P-17 E2E + DoD check tests/e2e/test_mvp_dod.py, scripts/run_dod_check.sh
📐 TRIZ-gates MVP (6/6 embedded)
TRIZ principle Gate Task
§11 Cushion in advance dead state in search_outbox P-02
§22 Turn harm into benefit Dead-digest hourly aggregator P-14
§16 Partial / excess action Fast-path by content_hash P-06, P-07
§25 Self-service Reconciler finds its own work P-13
§19 Periodic action Reconciler runs on a 30s schedule P-13
§35 Parameter change content_hash in documents schema P-01

✅ Critical — production-safe baseline

Goal: add reranker, safety nets, and offline eval — production rollout without unacceptable risks. NFR: p99 /search ≤ 150 ms (no rerank) / ≤ 350 ms (with rerank), ≥ 1000 doc/min, ≥ 200 RPS, lag ≤ 10s, SLA 99.5%.

ID Component Artifact
C-00 Phase bootstrap — extend app/config.py (6 sections) + eval dataset app/config.py, eval/
C-01 Cross-encoder reranker bge-reranker-v2-m3 (batching) app/reranker/service.py
C-02 Weighted fusion (α·BM25 + (1-α)·cosine, min-max norm, α=0.5) app/search/fusion.py::weighted_fuse
C-03 Circuit breaker for reranker (rolling window 60s, half-open) app/reranker/circuit_breaker.py
C-04 Speculative rerank (parallel rerank on lex top-10) app/search/speculative.py
C-05 Search API integration (updated orchestrator) app/api/routes/search.py, SearchOrchestrator
C-06 Facets in PostgreSQL (top-N after fusion) app/search/facets.py, SearchResponse.facets
C-07 Nightly offline eval (Recall@10, nDCG@10, MRR + regression gate) app/eval/nightly.py, migration 005_eval_datasets, CI gate
C-08 wait_for_index endpoint (polling pending → done) app/api/routes/index.py::index_status
C-09 Degraded mode (lex-only fallback + partial_result()) feature-flag vector_search_enabled
C-10 Push-down filters (selectivity via reltuples, 3–5× speedup) app/search/pushdown.py
C-11 Adaptive throttle (202 Accepted when pending_count > 50k) app/api/routes/index.py
C-12 Observability additions (+15 metrics) app/observability/metrics.py
C-13 Grafana Critical dashboards (4) grafana/provisioning/dashboards/critical-*.json
C-14 E2E + DoD Critical tests/e2e/test_critical_dod.py, scripts/run_dod_check_critical.sh
📐 TRIZ-gates Critical (9/9 embedded)
TRIZ principle Gate Task
§9 Preliminary anti-action Circuit breaker for reranker C-03
§19 Periodic action Circuit breaker with 60s window C-03
§21 Skip through Speculative rerank C-04
§27 Cheap short-lived wait_for_index_token C-08
§16 Partial / excess action partial_result() on DeadlineExceeded C-09
§9 Preliminary anti-action degraded flag + vector_search_enabled C-09
§23 Feedback Nightly eval → regression → release block C-07
§21 Skip through 202 Accepted when pending_count > threshold C-11
§17 Another dimension Push-down filters C-10

✅ Production-Ready — full-scale operation

Goal: data-driven quality management, distributed tracing, zero-downtime embedding-model migrations, chaos engineering. NFR: p99 /search ≤ 120 ms (no rerank), ≥ 2000 doc/min, ≥ 500 RPS, lag ≤ 5s, SLA 99.9%, cache hit ≥ 60%.

ID Component Artifact
PR-00 Phase bootstrap — configuration for all PR sections extend app/config.py, module stubs
PR-01 Personalization — users + profile_vector (EMA on click) migration 006_users, app/services/personalization.py
PR-02 search_events — daily partitioning + S3 archive (Parquet, 90 days) migration, daily partition cron, S3 archival
PR-03 A/B framework (sticky by user_id, sequential testing) app/services/ab_testing.py, experiment_id in SearchRequest
PR-04 MMR anti-bubble diversification (λ=0.7, adaptive λ) app/search/diversify.py, diversify=true in API
PR-05 Contextual personalization (device / time_of_day / session) SearchContext, multiple profile vectors
PR-06 Distributed tracing OpenTelemetry → Tempo (trace↔log correlation) app/observability/tracing.py, OTel SDK
PR-07 Embedding-model migration framework (blue-green + 1% canary + snapshot + rollback) app/services/migration.py, CLI python -m app.migration
PR-08 Shadow traffic (parallel computation of new strategy) app/services/shadow_traffic.py, search_events.variant
PR-09 Chaos engineering weekly (kill Qdrant / delay reranker / fill Redis) tests/chaos/*.py, weekly cron, postcondition checks
PR-10 Adaptive K by tenant_search_stats migration tenant_search_stats, app/search/adaptive_k.py
PR-11 Incremental facets (Redis cache facets:{query_hash}:{page}, TTL 10 min) app/search/facets_incremental.py
PR-12 Enhanced multilingual (fasttext-langid, mixed token-level, cyrtranslit, NFC) app/search/langid_fasttext.py
PR-13 Asymmetric indexing (chunking 512/50, weighted avg of sections title × 3) app/embedding/chunking.py, max(score) aggregation by doc_id
PR-14 Schema drift contract tests (QdrantPayload ↔ documents Postgres, CI gate) tests/contract/test_schema_drift.py
PR-15 Negative cache for embeddings (Redis null cache TTL 60s) extend app/embedding/cache.py
📐 TRIZ-gates Production-Ready (15/15 embedded)
TRIZ principle Gate Task
§13 Do it in reverse MMR anti-bubble PR-04
§17 Another dimension Contextual personalization PR-05
§15 Dynamism Adaptive α in EMA PR-01
§22 Turn harm into benefit Shadow traffic PR-08
§9 Preliminary anti-action Chaos engineering weekly PR-09
§10 Preliminary action Canary reindex (1%) PR-07
§11 Cushion in advance Snapshot-on-reindex PR-07
§26 Copying Blue-green collections PR-07
§17 Another dimension Distributed tracing (metrics ↔ logs ↔ traces) PR-06
§23 Feedback Adaptive K by tenant_search_stats PR-10
§3 Local quality Incremental facets PR-11
§1 Segmentation Mixed queries (token-level) PR-12
§3 Local quality Asymmetric indexing PR-13
§35 Parameter change NFC normalization PR-12
§16 Partial / excess action Negative cache PR-15

⏳ Improvements — scaling (trigger-based)

The phase is triggered by signals (> 10M documents, > 10k RPS, > 4 weeks of click data, etc.). It is not a "mandatory finale".

ID Trigger Component
IM-01 ≥ 4 weeks of click data Learned ranking LambdaMART (LightGBM/XGBoost pairwise, ONNX export)
IM-02 Cross-encoder stable for 1 week Cascade fusion selection (RRF → conditional rerank → optional learned)
IM-03 ≥ 1 enterprise tenant Tenant tiering A/B/C (per-tier SLO/rate-limit)
IM-04 IM-03 activated Cost-of-durability per tier (write_consistency=1 for Tier C)
IM-05 > 1M documents or > 500 RPS PostgreSQL scaling (1 primary + 2 replicas, PgBouncer)
IM-06 > 10M vectors Qdrant scaling (shard_number=8, optional PQ quantization)
IM-07 Any scaling trigger API Layer autoscaling (HPA CPU 70%, rate-limiting)
IM-08 XLEN > 10000 sustained Embedding Worker autoscaling (4–8 workers per GPU)
IM-09 > 10 GB cache / HA Redis Cluster (3 master + 3 replica)
IM-10 p99 on replica > 1.5× average Connection-aware routing (pg_stat_activity, hysteresis 30s)
IM-11 > 100M documents / > 10k write RPS CDC migration Debezium → Kafka
IM-12 Optional Adaptive k in RRF (k = 30 + 10·log(doc_count))
IM-13 Optional Outbox → CDC evolution

📋 Full list of tasks and statuses — see analitics/BACKLOG.md.


🔌 API contracts

Endpoint Method Purpose
/search POST Hybrid search: lex+vec, RRF/weighted, optional rerank/personalize/diversify
/index POST Index document (dual-write + outbox + Redis Streams) → {doc_id, status, indexed_at}
/index/{doc_id} DELETE Soft-delete + outbox op='delete'
/index/status/{token} GET Read-your-writes polling (pending → done → dead)
/health/live GET Process alive → {"status":"alive"}
/health/ready GET Postgres + Qdrant + outbox lag < threshold
/metrics GET Prometheus exposition
📝 Example: POST /search
// Request
{
  "query": "hybrid search postgres",
  "tenant_id": "uuid",
  "user_id": "uuid",         // optional, for personalization and A/B
  "filters": {
    "language": "en",
    "tags": ["news", "tech"],
    "attributes": { "category": "AI" }
  },
  "top_k": 20,
  "fusion": "rrf",           // rrf | weighted
  "rerank": true,             // cross-encoder on top-50
  "diversify": true,          // MMR anti-bubble
  "personalize": true,        // profile-based rerank
  "explain": false,
  "timeout_ms": 350,
  "experiment_id": "ab-2026-11-rrf-vs-weighted"
}

// Response
{
  "results": [
    { "doc_id": "...", "score": 0.92, "payload": {...}, "debug": {...} }
  ],
  "facets": { "category": [{"value": "AI", "count": 42}] },
  "degraded": false,
  "variant": "rrf",
  "latency_ms": 187
}

📈 Observability

Metrics (Prometheus → /metrics)

MVP: search_latency_ms, index_lag_seconds, qdrant_upsert_errors_total, embedding_cache_hit_rate, dead_letter_count.

Critical: reranker_latency_ms, circuit_breaker_state, circuit_breaker_opened_total, eval_recall_at_10, eval_ndcg_at_10, search_degraded_total, qdrant_pushdown_rate_total, outbox_throttled_total.

Source: app/observability/metrics.py.

Grafana dashboards (8)

  • 4 MVP: throughput, latency, error rate, outbox lag.
  • 4 Critical: reranker health, circuit breaker, eval regression, pushdown effectiveness.

Distributed tracing (OpenTelemetry)

  • Span search.request with attributes query_lang, fusion, experiment_id, user_id_hashed.
  • Child spans: pg.lexical, qdrant.vector, redis.cache_lookup, rerank, personalize, pg.facets.
  • Export to Tempo via OTel Collector.
  • Logs in Loki are correlated with traces by trace_id — three observability dimensions (metrics ↔ logs ↔ traces).

🧪 Testing and quality

uv run ruff check app/ tests/ scripts/      # lint
uv run mypy app/                            # type checking
uv run pytest                               # unit + integration + e2e
./scripts/run_dod_check.sh                  # MVP DoD (P-17)
./scripts/run_dod_check_critical.sh         # Critical DoD (C-14) + NFR
Test type Location What it covers
Unit tests/unit/ module business logic
Integration tests/integration/ PG + Qdrant + Redis
E2E tests/e2e/ full DoD scenarios
Contract tests/contract/test_schema_drift.py QdrantPayload ↔ documents drift
Chaos tests/chaos/*.py kill Qdrant, delay reranker, fill Redis

📁 Project structure

.
├── app/
│   ├── main.py                  # FastAPI app factory create_app()
│   ├── config.py                # pydantic-settings (env > .env)
│   ├── api/routes/              # index, search, health
│   ├── search/                  # orchestrator, lexical, vector, fusion,
│   │                            # rerank, facets, pushdown, diversify,
│   │                            # speculative, adaptive_k, langid_fasttext
│   ├── reranker/                # cross-encoder + circuit breaker (C-01, C-03)
│   ├── embedding/               # service, cache, chunking (PR-13)
│   ├── services/                # personalization, ab_testing, migration,
│   │                            # shadow_traffic
│   ├── eval/                    # nightly-eval (C-07)
│   ├── reconciler/              # outbox worker + dead-letter digest
│   ├── observability/           # metrics, tracing (OTel)
│   └── db/                      # session, models
├── alembic/versions/            # SQL migrations (async engine)
├── monitoring/prometheus/       # prometheus.yml
├── grafana/provisioning/        # datasources + 8 dashboards
├── eval/                        # eval datasets and scripts
├── docs/                        # schema_drift.md and others
├── analitics/                   # ARCHITECT, ROADMAP, BACKLOG, *-PROMPTS
├── scripts/                     # infra_up/down, init_qdrant, seed_eval, dod_check
├── tests/                       # unit / integration / e2e / contract / chaos
├── .github/workflows/           # CI (chaos weekly)
├── docker-compose.yml           # postgres + qdrant + redis + prometheus + grafana + app
├── Dockerfile                   # shared image for api/reconciler/digest/jobs
├── qdrant_collections.yaml      # documents collection config (ARCHITECT §17.1)
├── pyproject.toml               # dependencies (uv)
├── .env.example                 # all env variables with examples
└── alembic.ini

🤝 Conventions

  • ✅ Async SQLAlchemy only (asyncpg), no sync engine.
  • ✅ All ports/hosts go through pydantic-settings, no hardcodes.
  • ✅ Migrations only through Alembic (idempotent, with downgrade).
  • ✅ Each phase is a self-contained deliverable; the system remains operationally correct after each phase.
  • ✅ TRIZ-gates are embedded from day one, not "later".
  • ✅ Commits follow conventional commits (feat:, fix:, chore:, docs:).

📚 Documentation

File Description
analitics/ARCHITECT.md Architecture spec v2.0 (TRIZ-applied)
analitics/ROADMAP.md Roadmap across 4 maturity phases
analitics/BACKLOG.md Consolidated backlog of all tasks and statuses
analitics/MVP-PROMPTS.md Prompts P-00..P-17
analitics/CRITICAL-PROMPTS.md Prompts C-00..C-14
analitics/PROD-PROMPTS.md Prompts PR-00..PR-15
analitics/BUGFIXES-PROMPTS.md Bugfix prompts
docs/schema_drift.md Schema drift contract (PR-14)
C03_IMPLEMENTATION_REPORT.md Circuit breaker report (C-03)
AGENTS.md Guideline for AI agents
CONTRIBUTING.md Contributor guide

🤝 Contributing

We welcome contributions! Please read CONTRIBUTING.md before creating an issue or pull request.

Key principles:

  • 🔒 Follow phase-based development (don't mix tasks from different phases in a single PR).
  • 🧪 Every PR must pass ruff, mypy, pytest + the DoD check of the corresponding phase.
  • 📝 Use conventional commits (feat: PR-XX ..., fix: C-XX ...).
  • 🏗 When adding a new feature, document it in the corresponding *-PROMPTS.md and update BACKLOG.md.

📄 License

Distributed under the Apache License 2.0. See LICENSE.

Copyright 2026 OriaHS Contributors

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

Made with ❤️ for hybrid search

📈 Report Bug · 💡 Request Feature · 📚 Read the Docs

About

Next-generation hybrid search — PostgreSQL + Qdrant Lexical + vector channels, transactional outbox, cross-encoder rerank, OTel tracing, A/B framework, and nightly offline eval — production-ready out of the box.

Topics

Resources

Contributing

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages