Files
LLM-Server/docs/research/langchain-pgvector-vs-litellm-pgvector.md
T
haylanandClaude-Bot e2a79eab4a 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>
2026-09-02 22:02:58 +02:00

389 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.