docs(research): LangChain+pgvector-direct vs litellm-pgvector for issue #25

Verdict: switch. See docs/research/langchain-pgvector-vs-litellm-pgvector.md
for full reasoning, LOC comparison, and a concrete replacement service spec.
Research/doc only — no compose/config/vendor changes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-02 22:02:58 +02:00
co-authored by Claude-Bot
parent 7247674a9f
commit e2a79eab4a
@@ -0,0 +1,388 @@
# Research: LangChain+pgvector-direct RAG vs. the litellm-pgvector connector (for Gitea issue #25)
**Question:** Is a LangChain + pgvector-direct retrieval architecture (LangChain's
`PGVector` class talking to Postgres directly, LiteLLM used only for
provider-abstracted chat/embedding calls) a better fit for this stack than the
already-built `litellm-pgvector` connector service (`vendor/litellm-pgvector/`,
issue #23's resolution)?
**Bottom line: yes — switch.** Replace `litellm-pgvector` /
`vendor/litellm-pgvector/` with a small hand-written FastAPI service (roughly
80120 lines, one new dependency set: `langchain-postgres`, `langchain-openai`,
`fastapi`, `uvicorn`) that wraps LangChain's `PGVector` class and calls
LiteLLM's OpenAI-compatible `/v1/embeddings` endpoint for embeddings. This
keeps "memory served from the gateway layer" in spirit (LiteLLM still does all
chat/embedding model calls; only the retrieval *query* now goes through a
LangChain-based service instead of a vendored 754-line Prisma-backed
connector), removes 754 lines of unvetted vendored source and a Prisma
migration dependency, and — critically — removes the *guessed and unverified*
`vector_store_registry` / `custom_llm_provider: pg_vector` config that issue
#24 flagged as never confirmed against a live deploy. `pgvector-db` (plain
`pgvector/pgvector:pg16`) is reusable completely as-is; only the schema
management strategy changes (LangChain manages its own two tables instead of
Prisma migrations).
## 1. Verifying the markaicode article against LangChain's actual docs
The article (<https://markaicode.com/stack/litellm-langchain-stack/>) proposes
three services — LangServe (port 8000, wraps a LangChain chain as REST),
LiteLLM (port 4000), and Postgres+pgvector (port 5432) — and shows:
```python
vectorstore = PGVector(
connection_string="postgresql://user:pass@pgvector:5432/vectordb",
embedding_function=your_embeddings,
collection_name="documents",
)
```
**This snippet uses the deprecated class.** `connection_string` and
`embedding_function` are constructor args of
`langchain_community.vectorstores.pgvector.PGVector`, the older integration
that ships inside the general-purpose `langchain-community` package. The
current, actively-maintained integration lives in its own package,
`langchain-postgres`, as `langchain_postgres.vectorstores.PGVector`, with a
different constructor:
```python
from langchain_postgres import PGVector
from langchain_openai import OpenAIEmbeddings
vector_store = PGVector(
embeddings=embeddings, # not embedding_function
collection_name="my_docs",
connection="postgresql+psycopg://user:pass@host:5432/db", # not connection_string; psycopg3 DSN
use_jsonb=True,
)
```
Confirmed constructor signature (`embeddings`, `connection`, `collection_name`,
`use_jsonb`, `create_extension`, `async_mode`) via
[reference.langchain.com/python/langchain-postgres/vectorstores/PGVector](https://reference.langchain.com/python/langchain-postgres/vectorstores/PGVector)
and the setup walkthrough at
[docs.langchain.com/oss/python/integrations/vectorstores/pgvector](https://docs.langchain.com/oss/python/integrations/vectorstores/pgvector).
Two concrete corrections to the article's shape:
- **Package**: `langchain-postgres` is a separate pip install
(`pip install -qU langchain-postgres`), not part of `langchain_community`.
The docs page does not say the community class is formally deprecated, but
it is the older/legacy path — the actively-documented integration page for
Postgres+pgvector points at `langchain_postgres.vectorstores.PGVector`.
- **Driver**: `langchain-postgres` requires the `psycopg` (psycopg3) driver
and a `postgresql+psycopg://` DSN, not `psycopg2` / plain `postgresql://`.
This repo's `pgvector-db` service doesn't care (it's just Postgres), but the
new service's `requirements.txt` needs `psycopg[binary]`, not
`psycopg2-binary` (which `vendor/litellm-pgvector/requirements.txt` uses).
- **`create_extension`** defaults to `True``PGVector` runs `CREATE
EXTENSION IF NOT EXISTS vector` itself on first connect. Source: same API
reference page above. This is the same requirement `litellm-pgvector`'s
Prisma schema has (Postgres user needs `CREATE` privilege) — **no change in
privilege requirements**, just who issues the DDL.
- The article's LangServe wrapper (`add_routes()`) is a real LangChain
component but adds a fourth abstraction (LangServe app → LangChain
chain/retriever → PGVector → Postgres) for what this stack needs from only
two HTTP endpoints (ingest, query). A plain FastAPI app calling `PGVector`
methods directly is simpler and is what's specced below (§5) — LangServe is
overkill for a two-endpoint internal service.
There is also a v2, engine-based API, `langchain_postgres.v2.vectorstores.PGVectorStore`
(with native async via `PGEngine`/`AsyncPGVectorStore`), documented at
[reference.langchain.com/python/integrations/langchain_postgres](https://reference.langchain.com/python/integrations/langchain_postgres/).
It's newer and adds first-class async, but the plain `PGVector` class is the
one shown on LangChain's main pgvector integration page, is simpler to set up
(no separate `PGEngine` object), and this service has no need for the async
throughput `PGVectorStore` targets (single ingest script, occasional queries).
**Recommendation: use `langchain_postgres.vectorstores.PGVector`, not
`PGVectorStore`** — revisit only if query volume ever demands async
throughput.
## 2. Complexity comparison: vendor/litellm-pgvector vs. a LangChain-direct service
`vendor/litellm-pgvector/` as it exists in this repo today:
| File | Lines |
|---|---|
| `main.py` | 521 |
| `embedding_service.py` | 89 |
| `models.py` | 85 |
| `config.py` | 59 |
| `prisma/schema.prisma` | 39 |
| **Total (Python + schema)** | **793** |
Plus: a `Dockerfile` that runs `prisma generate` at build time, a
`requirements.txt` pulling in `prisma==0.11.0` (which itself vendors a Rust
query-engine binary download at install time — a second network dependency
inside the Docker build, on top of the git-context build that already failed
once per the vendoring rationale in `docker-compose.yml`/`VENDORED.md`), and
hand-rolled raw-SQL string building for every endpoint (see `main.py` lines
86128, 219320, 322402, 405511 — table/column names are
f-string-interpolated from `settings.table_names`/`settings.db_fields`, not
parameterized, though the *values* are parameterized). None of this has been
smoke-tested against a live deploy (issue #24).
A LangChain-direct replacement needs to expose exactly two HTTP endpoints
(`POST /ingest`, `POST /query` — see §5) as a thin wrapper around library
calls that already do the embedding + SQL work:
- `langchain_postgres.PGVector.add_texts(texts, metadatas)` — ingestion,
replaces `main.py`'s `create_embedding`/`create_embeddings_batch` (176
lines) and all of `models.py`'s embedding request/response schemas.
- `langchain_postgres.PGVector.similarity_search_with_relevance_scores(query,
k)` — query, replaces `search_vector_store` (100 lines) and
`generate_query_embedding`/`embedding_service.py` (89 lines) — LangChain's
`OpenAIEmbeddings` (from `langchain-openai`, pointed at
`base_url=http://litellm:4000/v1`) does the embedding call instead of a
hand-written `httpx` call to LiteLLM.
- No `create_vector_store`/`list_vector_stores` needed — one Postgres
"collection" (LangChain's term, one row in its `langchain_pg_collection`
table) is enough for this repo's single `memory-and-notes` use case;
`collection_name="memory-and-notes"` passed once at construction covers it.
- No Prisma, no generated query-engine binary, no `prisma/schema.prisma` —
`PGVector` creates and manages its own two tables
(`langchain_pg_collection`, `langchain_pg_embedding`) via SQLAlchemy on
first use.
Estimated replacement size: **~90120 lines of Python** across the FastAPI app
+ Pydantic request/response models (a `/ingest` and `/query` endpoint, an
`Embeddings`-backed `PGVector` instance built once at startup, an API-key
`Depends()` check reused almost verbatim from `vendor/litellm-pgvector/main.py`
lines 5055) — roughly an **85% reduction** from the vendored connector's 793
lines, with zero Prisma/migration surface and no hand-built raw SQL (parameter
binding, vector-string formatting, and JSONB metadata handling are all
internal to `PGVector`, not re-implemented per endpoint as in `main.py`).
**This does not eliminate the need for a small custom service** — a bare
`PGVector.similarity_search()` call is a Python library call, not an HTTP
endpoint, and this stack's callers (the ingest script, Claude Code hooks
described in `docs/memory-knowledgebase.md`) need HTTP. Something still has to
be that thin wrapper; it's just ~85% smaller and has no ORM/migration layer.
## 3. Does this still count as "memory served from the gateway layer"?
Yes, with the same division of responsibility issue #21 established, just a
narrower one for the connector. LiteLLM (`litellm` container) remains the
**only** thing every client (Open WebUI, Claude Code, anything else behind the
proxy) talks to for chat and embedding *model* calls — nothing changes there.
What moves is which service holds the *retrieval* logic: today it's
`litellm-pgvector` (itself calling back into `litellm:4000` for embeddings,
per `docker-compose.yml` lines 241246); under this alternative it's the new
LangChain-based service, which **also** calls back into `litellm:4000/v1` for
embeddings (via `langchain-openai`'s `OpenAIEmbeddings(base_url="http://litellm:4000/v1")`,
since LiteLLM exposes an OpenAI-compatible API — confirmed by this repo's
existing `local-embedding` `model_list` entry in `litellm-config.yaml` already
being OpenAI-route-compatible). The client-facing shape is identical: no
client (Open WebUI, a Claude Code hook, `ingest-memory.sh`) talks to Postgres
directly, and no client bypasses LiteLLM for the actual embedding/generation
model calls. Only the internal retrieval connector's implementation changes,
from a vendored third-party FastAPI+Prisma app to an in-repo FastAPI+LangChain
app. This does not contradict #21's standing decision.
One nuance: today, `vector_store_registry` makes LiteLLM itself the thing a
chat completion's `tools: [{"type": "file_search", ...}]` call reaches (per
`docs/research/litellm-knowledgebase.md` §1) — LiteLLM intercepts the tool
call and proxies to the registered `pg_vector` backend. **Under the
alternative, that specific mechanism goes away**: the new service is not a
`vector_store_registry` provider (there is no `langchain_postgres`/custom
provider type in LiteLLM's `vector_store_registry`, and building one would
reintroduce exactly the custom-connector complexity this ticket is trying to
shed). Retrieval becomes a direct call to the new service's `/query` endpoint
(or a client-side RAG step: query the service, splice results into the prompt
before calling `litellm`) rather than an in-band `file_search` tool call
LiteLLM resolves itself. This is a real, if modest, capability reduction:
`{"type": "file_search", "vector_store_ids": [...]}` inside a `/chat/completions`
call to `litellm` stops working; callers move to a two-step "search then
send" pattern instead. Given `vector_store_registry`'s exact field shape was
never confirmed working (issue #24) and this is the only capability lost,
this is judged an acceptable trade for the complexity and reliability win.
## 4. Ingestion, embedding-server, pgvector-db reuse
**`embedding-server`** (llama.cpp, `nomic-embed-text-v1.5`, `docker-compose.yml`
lines 3866): fully reusable as-is. It already serves an OpenAI-compatible
`/v1/embeddings` route (llama.cpp's `--embeddings` flag, per
`docs/research/litellm-knowledgebase.md` §3), and `litellm-config.yaml`
already exposes it through LiteLLM as the `local-embedding` model
(`api_base: http://embedding-server:8080/v1`). The new service reaches it the
same way `litellm-pgvector` does today — indirectly, by calling
`http://litellm:4000/v1/embeddings` with `model=local-embedding` — via
`langchain_openai.OpenAIEmbeddings(model="local-embedding",
base_url="http://litellm:4000/v1", api_key=<virtual key>)`. `OpenAIEmbeddings`
is LangChain's standard `Embeddings` implementation for any OpenAI-compatible
endpoint (no custom `Embeddings` subclass needed) — confirmed via the
constructor example in
[docs.langchain.com/oss/python/integrations/vectorstores/pgvector](https://docs.langchain.com/oss/python/integrations/vectorstores/pgvector),
which pairs `OpenAIEmbeddings` with `PGVector` directly.
**`scripts/ingest-memory.sh`** changes, but modestly: same per-line chunking
of `data/memory.md`/`data/claude-legacy-memory.md` (the ponytail comment in
the current script — one chunk per non-empty, non-heading line, no real
chunking logic needed — still holds), but it now `curl`s the new service's
`POST /ingest` instead of `litellm-pgvector`'s two-step
`POST /v1/vector_stores` + `POST /v1/vector_stores/{id}/embeddings/batch`.
Concretely: build one JSON array of `{content, metadata}` per file (same
shape the script already builds) and POST it once to `/ingest`; the new
service calls `PGVector.add_texts(texts, metadatas)` internally, which
embeds and inserts in one call — no separate "create the store" step, since
`collection_name` is fixed at service startup and `PGVector` creates the
collection row implicitly on first `add_texts`. The no-dedup caveat in the
current script's header comment carries over unchanged (`add_texts` always
inserts; nothing in `PGVector` deduplicates by content).
**`pgvector-db`** (`pgvector/pgvector:pg16`, `docker-compose.yml` lines
198214): reusable completely as-is — same image, same "separate Postgres
instance from `litellm-db`" rationale, no compose changes needed to the
service definition itself. Only the *schema* changes: `litellm-pgvector`'s
Prisma schema (`vendor/litellm-pgvector/prisma/schema.prisma`, 39 lines)
defines its own `vector_stores`/embeddings tables with configurable
table/field names (`config.py`'s `settings.table_names`/`db_fields`), applied
via Prisma migrations at container startup (the exact behavior of which,
per issue #24's ponytail note, was never smoke-tested). `PGVector` instead
auto-creates two fixed tables — `langchain_pg_collection` and
`langchain_pg_embedding` — via SQLAlchemy `Base.metadata.create_all()`-style
setup on first connection, no separate migration step or migration tool
needed. This is a strictly simpler schema-management story: no Prisma CLI,
no `prisma generate` build step, no migration files to track. The existing
`litellm_pgvector`/`PGVECTOR_DB_PASSWORD` database and role stay — the new
service just needs its own `DATABASE_URL` pointed at the same database (or a
fresh one; reusing `litellm_pgvector` is simplest since no data has been
proven-loaded into it yet per issue #24).
**`litellm-pgvector` (container) and `vendor/litellm-pgvector/` (source
tree)**: removed if this verdict is adopted. Nothing else in the repo depends
on the vendored source once the new service replaces it.
## 5. Concrete replacement spec (not implemented — spec only)
**New compose service**, replacing the `litellm-pgvector` block:
```yaml
memory-retrieval: # name TBD; "litellm-pgvector" freed up
build:
context: ./services/memory-retrieval # new small in-repo dir, not vendored
container_name: memory-retrieval
depends_on:
pgvector-db:
condition: service_healthy
litellm:
condition: service_healthy
environment:
- DATABASE_URL=postgresql+psycopg://litellm_pgvector:${PGVECTOR_DB_PASSWORD}@pgvector-db:5432/litellm_pgvector
- LITELLM_BASE_URL=http://litellm:4000/v1
- LITELLM_API_KEY=${MEMORY_RETRIEVAL_EMBEDDING_KEY} # new virtual key, same pattern as LITELLM_PGVECTOR_EMBEDDING_KEY today
- EMBEDDING_MODEL=local-embedding
- SERVER_API_KEY=${MEMORY_RETRIEVAL_API_KEY} # replaces LITELLM_PGVECTOR_API_KEY
- COLLECTION_NAME=memory-and-notes
expose:
- "8000"
restart: unless-stopped
networks: [ai-stack]
```
Note `DATABASE_URL` changes scheme to `postgresql+psycopg://` (psycopg3 DSN
`langchain-postgres` requires — see §1), not the `postgresql://` scheme
`litellm-pgvector` uses today.
**Dockerfile shape** (`services/memory-retrieval/Dockerfile`): same base image
family as today's connector — `python:3.11-slim` — `pip install` the
requirements, copy the app, `CMD ["uvicorn", "main:app", "--host", "0.0.0.0",
"--port", "8000"]`. No `prisma generate` step, no `build-essential`/`postgresql-client`
needed at the OS-package level (no Prisma binary to build against; `psycopg[binary]`
ships prebuilt wheels).
**`requirements.txt`**: `fastapi`, `uvicorn[standard]`, `langchain-postgres`,
`langchain-openai`, `psycopg[binary]`, `pydantic` — five packages vs. the
current ten in `vendor/litellm-pgvector/requirements.txt`, and no `prisma`.
**Endpoints** (2, vs. today's 5 — create store, list stores, search, create
embedding, create embeddings batch):
- `POST /ingest` — body: `{"chunks": [{"content": "...", "metadata": {...}}]}`.
Calls `PGVector.add_texts(texts=[...], metadatas=[...])`. Replaces both
`ingest-memory.sh`'s "create store" call (no longer needed — collection is
implicit) and its batch-embeddings call.
- `POST /query` — body: `{"query": "...", "k": 5}`. Calls
`PGVector.similarity_search_with_relevance_scores(query, k=k)`, returns
`{"results": [{"content": ..., "metadata": ..., "score": ...}]}`.
- `GET /health` — same trivial liveness check the current connector has
(`main.py` lines 514517), kept for compose `depends_on`/`healthcheck` use
if desired.
Bearer-token auth via `SERVER_API_KEY`, same `Depends()` pattern as
`vendor/litellm-pgvector/main.py` lines 4755 — that part of the existing
connector is fine as-is and can be copied over close to verbatim.
**`litellm-config.yaml` change**: remove the `vector_store_registry` block
entirely (lines 4761 today) — per §3, the new service isn't a
`vector_store_registry` provider, so there's nothing to register. The
`local-embedding` `model_list` entry (lines 2535) is unchanged and still
needed — it's what the new service's `OpenAIEmbeddings` client calls through
`litellm`.
## 6. Verdict
**Switch to LangChain-direct.** This does invalidate part of issue #23's
resolution and the `litellm-pgvector` container currently in
`docker-compose.yml` — say so explicitly for scoping a follow-up
implementation ticket:
- **Torn out**: `litellm-pgvector` service block in `docker-compose.yml`,
`vendor/litellm-pgvector/` (entire tree), the `vector_store_registry` block
in `litellm-config.yaml`, `LITELLM_PGVECTOR_API_KEY`/
`LITELLM_PGVECTOR_EMBEDDING_KEY` env vars (replaced by new-service
equivalents).
- **Survives unchanged**: `pgvector-db` (compose block as-is), `embedding-server`
(compose block as-is), the `local-embedding` `model_list` entry in
`litellm-config.yaml`, the overall "LiteLLM does all model calls" gateway
boundary from issue #21.
- **New**: a small in-repo `services/memory-retrieval/` FastAPI+LangChain
app (~90120 lines, spec in §5), a new compose service block, a rewritten
`scripts/ingest-memory.sh` pointed at `/ingest` instead of
`litellm-pgvector`'s two-step API, and one new virtual key
(`MEMORY_RETRIEVAL_EMBEDDING_KEY`) issued the same way
`LITELLM_PGVECTOR_EMBEDDING_KEY` was per `docs/proxy-key-onboarding.md`.
- **Capability trade-off** (§3): the OpenAI Assistants-style `file_search`
tool call directly against `litellm`'s `/chat/completions` stops working
(no `vector_store_registry` entry to resolve it); callers move to querying
the new service directly and splicing results into the prompt themselves.
This mechanism was never confirmed working in the first place (issue #24),
so it's a loss of unverified capability, not working functionality.
This is not implemented here — this document is research/spec only, per the
ticket's scope. A follow-up implementation ticket should: write
`services/memory-retrieval/`, update `docker-compose.yml` and
`litellm-config.yaml` per §5, rewrite `scripts/ingest-memory.sh`, delete
`vendor/litellm-pgvector/`, and update `docs/memory-knowledgebase.md`.
## 7. Repo context read for this research
- `docker-compose.yml` — `embedding-server`, `pgvector-db`, `litellm-pgvector`
service definitions (build context, env vars, network).
- `litellm-config.yaml` — `local-embedding` model entry, `vector_store_registry`
block.
- `docs/research/litellm-knowledgebase.md` — prior research establishing
LiteLLM's native vector-store feature has no Qdrant provider and that
`litellm-pgvector` was the only self-hosted path found at the time; its §1,
§3, §4 are cited above for the `file_search` tool-call mechanism and
embedding-model wiring.
- `docs/memory-knowledgebase.md` — current user-facing setup doc for the
knowledgebase/memory feature.
- `scripts/ingest-memory.sh` — current ingestion mechanism and its no-dedup
caveat.
- `vendor/litellm-pgvector/{main.py,embedding_service.py,models.py,config.py,
prisma/schema.prisma,requirements.txt,Dockerfile}` — read in full to
characterize size (793 total lines Python+schema) and shape (raw-SQL
f-string-built queries per endpoint, Prisma-managed schema, calls back to
LiteLLM's `/embeddings` for query embedding).
- Gitea issue #25 (this ticket) and its parent #21 — standing decision that
memory is served from the gateway layer, not a client.
- Gitea issue #23/#24 (referenced by #25) — original `litellm-pgvector`
adoption and its unverified-against-live-deploy caveat.
## 8. Primary sources consulted
- [markaicode.com/stack/litellm-langchain-stack/](https://markaicode.com/stack/litellm-langchain-stack/) — the alternative surfaced by the user; verified and corrected above (§1).
- [reference.langchain.com/python/langchain-postgres/vectorstores/PGVector](https://reference.langchain.com/python/langchain-postgres/vectorstores/PGVector) — `PGVector` constructor signature, `create_extension` default.
- [docs.langchain.com/oss/python/integrations/vectorstores/pgvector](https://docs.langchain.com/oss/python/integrations/vectorstores/pgvector) — install instructions, `postgresql+psycopg://` DSN requirement, `OpenAIEmbeddings` + `PGVector` pairing example.
- [github.com/langchain-ai/langchain-postgres](https://github.com/langchain-ai/langchain-postgres) — package README; async-function-pairing note; `PGVector` deprecated in v0.0.14+ in favor of `PGVectorStore` (v2 API).
- [reference.langchain.com/python/integrations/langchain_postgres/](https://reference.langchain.com/python/integrations/langchain_postgres/) — v2 `PGVectorStore`/`PGEngine`/`AsyncPGVectorStore` API, compared against `PGVector` in §1.
- `vendor/litellm-pgvector/README.md` and source files (this repo) — connector's existing endpoint/config shape, cited for the LOC comparison in §2.