RagMeUp.Client
0.2.0
See the version list below for details.
dotnet add package RagMeUp.Client --version 0.2.0
NuGet\Install-Package RagMeUp.Client -Version 0.2.0
<PackageReference Include="RagMeUp.Client" Version="0.2.0" />
<PackageVersion Include="RagMeUp.Client" Version="0.2.0" />
<PackageReference Include="RagMeUp.Client" />
paket add RagMeUp.Client --version 0.2.0
#r "nuget: RagMeUp.Client, 0.2.0"
#:package RagMeUp.Client@0.2.0
#addin nuget:?package=RagMeUp.Client&version=0.2.0
#tool nuget:?package=RagMeUp.Client&version=0.2.0
RAG engine
Document RAG over Postgres + pgvector. The engine owns ingestion, embedding, and reranking; the app owns filtered retrieval as direct SQL on tables it owns.
service/ Python FastAPI engine (`ragmeup` package): /embed, /documents, /rerank
client/ C# read-path library (RagMeUp.Client): rank, build passages, rerank, read spans
db/ schema.sql — documents + chunks tables, HNSW + GIN indexes
Each half carries its own Azure DevOps pipeline (*/azure-pipelines.yml) and
releases independently — the client to nuget.org, the engine as a container image.
See CI/CD.
Database
psql "$DATABASE_URL" -f db/schema.sql # needs pgvector >= 0.7.0
Service
Models live on a volume mounted at /models, not baked into the image, so code
changes rebuild and push only small layers. A one-shot seed populates the volume.
cd service
DATABASE_URL=postgres://… docker compose up --build
Compose runs model-seed first (downloads weights to the models volume), then
starts engine offline against it. On OpenShift, the equivalent is a PVC at
/models seeded by an init Job (or initContainer) running
python -m ragmeup.download_models.
Local dev without containers: pip install -r requirements.txt then
DATABASE_URL=… uvicorn ragmeup.main:app (from service/). Models download from
the hub on first run.
Image flavours
One Dockerfile, two bases, selected by FLAVOR — the shared tail (deps, app,
entrypoint) is written once so the flavours cannot drift:
docker build . # cuda, the default — GPU host (~8 GB)
docker build --build-arg FLAVOR=cpu . # cpu — CI and a GPU-less VPS (~2.4 GB)
The CPU flavour installs torch from PyTorch's CPU wheel index; a plain
pip install torch would pull the CUDA build and ~2.5 GB of nvidia-* wheels
that a CPU host can never use.
Configuring a container
Everything is environment variables — the image has no config file. Only
DATABASE_URL is required; every other knob has a working default, and the
model paths are already set in the image.
docker run -d --name rag-engine \
-e DATABASE_URL=postgres://user:pass@db:5432/ragdb \
-e RAG_TORCH_THREADS=4 \
-v models:/models -e HF_HUB_OFFLINE=1 -e TRANSFORMERS_OFFLINE=1 \
-p 127.0.0.1:8000:8000 \
ghcr.io/jgauffin/ragmeup-engine:latest
| Variable | Default | Effect |
|---|---|---|
DATABASE_URL |
required unless RAG_TARGETS is set |
Postgres+pgvector for the engine's own (privileged) ingestion pool. Also the default ingestion target — the database /documents writes to when a request names none. |
RAG_TARGETS |
unset | JSON {name: dsn}, inline or a path to a file, naming the databases one engine may ingest into. Lets several applications share one loaded set of models — see sharing one engine. |
RAG_TARGET_POOL_MIN / RAG_TARGET_POOL_MAX |
1 / 4 |
Connections held per target. The ceiling is unchanged from before; the floor is 1 so an idle target costs one connection, not four. |
HF_HUB_OFFLINE, TRANSFORMERS_OFFLINE |
unset | Set to 1 when /models is seeded, so nothing ever reaches the hub at runtime. |
BGE_M3_PATH |
/models/bge-m3 |
Embedder weights. Set in the image; override only to relocate the volume. |
RERANKER_PATH |
/models/bge-reranker-v2-m3 |
Default reranker weights. Likewise. |
RERANKER_CROSS_ENCODER_PATH |
cross-encoder/ms-marco-MiniLM-L6-v2 |
Weights for the cross-encoder backend. |
RAG_CHUNK_STRATEGY |
whitespace windows | recursive / structure-aware selects the separator-hierarchy splitter. Changing it only affects newly ingested documents. |
RAG_EMBED_DEVICE |
torch's choice | e.g. cuda:0, cpu. Not a model selector: the embedding model is fixed by the schema's column dimensions — swapping it is a re-index event. |
RAG_RERANKER_BACKEND |
multilingual bge | cross-encoder selects the ~22M-param English-only model — a different quality/latency point, much cheaper on CPU. |
RAG_RERANKER_BY_LANGUAGE |
unset | Per-language override, e.g. en:cross-encoder,sv:flag. |
RAG_RERANK_DEVICE |
torch's choice | Device for the reranker specifically. |
RAG_RERANK_MAX_LENGTH |
512 |
Tokens read per (query, passage) pair; the tail is silently dropped. Near-linear on latency — the bluntest dial there is. |
RAG_RERANK_QUANTIZE |
off | 1/true enables int8 dynamic quantization: ~2.8x faster on CPU, but ~1.1 GB more resident memory. Speed for memory, not free. |
RAG_TORCH_THREADS |
every logical core | Cap intra-op threads. Set it to the physical core count on a VPS — see below. |
RAG_ENRICHERS |
none (no-op) | contextual or extraction. Both call an LLM per document at ingest, so cost is opt-in. |
ENRICH_LLM_BASE_URL |
OpenAI | Any OpenAI-compatible endpoint (vLLM, Ollama, LM Studio). |
ENRICH_LLM_API_KEY |
not-needed |
Required for real OpenAI; self-hosted endpoints usually ignore it. |
ENRICH_LLM_MODEL |
gpt-4o-mini |
Model used by the enrichers. |
RAG_EXTRACTION_PROMPT |
built-in template | Inline template, or a path to one, for the extraction enricher. |
RAG_EXTRACTION_FIELDS |
unset | JSON {name: type} map. Set, only declared and correctly-typed keys survive — which is what keeps the jsonb @> filter reliable. |
Two of these are not really optional in production:
- Publish on loopback, not
0.0.0.0. No endpoint is authenticated and/documentswrites with a privileged connection that bypasses your RLS by design. The engine is a trusted internal service — see deployment: trust boundary & scaling. - Set
RAG_TORCH_THREADS. Unset, torch claims every logical core, which over-subscribes SMT pairs (measured: 8 → 16 threads at 0.98x, i.e. slower), and concurrent/rerankcalls then fight for the whole machine.
service/docker-compose.vps.yml is a ready-made
version of all of this for a Linux VPS: it pulls the published image, reserves no
GPU, binds loopback, and reads the knobs above from a .env file.
Client
The app builds an NpgsqlDataSource with UseVector() and points
HttpClient.BaseAddress at the engine. Filters live in the app's own SQL,
which produces the candidate chunk-id set passed to Rank.
var q = await rag.Embed(query); // engine /embed
long[] ids = await app.FilteredCandidates(...); // app's WHERE: RLS, geo, joins
var matches = await rag.Rank(conn, q, query, ids, k: 50); // same `query` as embedded
var passages = rag.BuildPassages(matches);
var top = await rag.Rerank(query, passages, topK: 5);
var spans = await rag.ReadSpans(conn, top); // clean original text for /answer
Methods drop the Async suffix — the whole client is async, so it adds nothing.
Tests
Both sides isolate the expensive boundaries (the GPU models, the database) so the logic around them is tested directly, and a thin layer of integration tests exercises the real SQL against Postgres+pgvector.
A throwaway pgvector instance for the integration tests:
docker run -d --name rag-test-pg -e POSTGRES_PASSWORD=test -e POSTGRES_DB=ragtest \
-p 55432:5432 pgvector/pgvector:pg16
Service (service/) — unit tests need no model and no DB (the model boundary
is mocked); the /documents integration test needs the DB above.
cd service
python -m venv .venv && .venv/Scripts/pip install -r requirements-dev.txt
.venv/Scripts/python -m pytest # unit only (integration auto-skips)
RAG_TEST_DATABASE_URL=postgres://postgres:test@localhost:55432/ragtest \
.venv/Scripts/python -m pytest # + the /documents integration test
Client (client/) — unit tests are pure (no DB/HTTP). The integration tests
use RAG_TEST_DATABASE_URL if set, otherwise spin a pgvector container via
Testcontainers; they auto-skip when neither is available.
cd client
dotnet test tests/RagMeUp.Client.Tests # unit
RAG_TEST_DATABASE_URL=postgres://… \
dotnet test tests/RagMeUp.Client.IntegrationTests # rank/read-path vs real pgvector
Both suites honour the same RAG_TEST_DATABASE_URL, so one local pgvector serves
both. The integration tests reset their tables on start, so point it at a
scratch database, not one with data you care about.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.