Skip to content

feat: support multiple vector store backends - #11

Open
ashrobertsdragon wants to merge 1 commit into
mainfrom
feature/multi-vector-store
Open

feat: support multiple vector store backends#11
ashrobertsdragon wants to merge 1 commit into
mainfrom
feature/multi-vector-store

Conversation

@ashrobertsdragon

@ashrobertsdragon ashrobertsdragon commented May 12, 2026

Copy link
Copy Markdown
Owner

This PR implements support for multiple vector store backends, decoupling the core RAG logic from specific database implementations.

Key Changes:

  • Vector Store Abstraction: Introduced a VectorStoreAdapter protocol in src/markdown_rag/vector_stores/base.py.
  • Backend Adapters: Implemented adapters for:
    • Postgres (pgvector)
    • Qdrant
    • Chroma
    • Milvus
    • Pinecone
    • MongoDB Atlas
  • Optional Dependencies: Moved database-specific libraries to optional extras in pyproject.toml (postgres, qdrant, chroma, milvus, pinecone, mongodb).
  • Lazy Loading: Refactored main.py and adapters to only load database drivers when the specific store is configured.
  • Documentation:
    • Added docs/vector-stores.md with detailed setup and configuration for all providers.
    • Updated README.md and docs/architecture.md with new features and Mermaid diagrams.

Configuration:

Users can now set the VECTOR_STORE environment variable to any of the supported values and provide the corresponding connection settings.

Resolves #5

Summary by Sourcery

Add a pluggable vector store layer and update configuration and docs to support multiple backend providers.

New Features:

  • Introduce a VectorStoreAdapter protocol and concrete adapters for Postgres, Qdrant, Chroma, Milvus, Pinecone, and MongoDB.
  • Allow selecting the vector store backend via the VECTOR_STORE environment variable and associated connection settings.

Enhancements:

  • Refactor MarkdownRAG to depend on an abstract vector store interface instead of a concrete Postgres implementation.
  • Lazy-load database-specific drivers only when their corresponding backend is configured, and move them into optional extras in pyproject.toml.
  • Update system architecture diagrams and configuration docs to reflect pluggable vector stores and multiple embedding providers.

Build:

  • Define optional dependency extras for each supported vector store backend in pyproject.toml.

Documentation:

  • Add a dedicated vector-stores.md guide documenting all supported backends, installation extras, and configuration options.
  • Expand README and architecture.md to describe pluggable vector stores, updated data flows, and revised environment variables.

@sourcery-ai

sourcery-ai Bot commented May 12, 2026

Copy link
Copy Markdown

Reviewer's Guide

Decouples the core RAG pipeline from a hard‑coded pgvector/Postgres dependency by introducing a VectorStoreAdapter protocol, wiring main/rag to this abstraction, and adding concrete adapters, config, and docs for multiple optional vector store backends (Postgres, Qdrant, Chroma, Milvus, Pinecone, MongoDB).

File-Level Changes

Change Details Files
Introduce a VectorStoreAdapter protocol and backend-specific adapters so RAG talks to vector stores through a uniform interface.
  • Add VectorStoreAdapter protocol defining add_texts, similarity_search, list_filenames, delete_filename, and exists operations.
  • Implement PostgresAdapter that wraps langchain_postgres.PGVector and preserves existing SQLAlchemy-based metadata queries and deletions.
  • Implement QdrantAdapter, ChromaAdapter, MilvusAdapter, PineconeAdapter, and MongoDBAdapter, each delegating core operations to the underlying LangChain store and exposing filename/metadata-based helpers.
src/markdown_rag/vector_stores/base.py
src/markdown_rag/vector_stores/postgres.py
src/markdown_rag/vector_stores/qdrant.py
src/markdown_rag/vector_stores/chroma.py
src/markdown_rag/vector_stores/milvus.py
src/markdown_rag/vector_stores/pinecone.py
src/markdown_rag/vector_stores/mongodb.py
Refactor main startup and RAG core to use the adapter abstraction and support selecting vector store via configuration.
  • Extend Env settings with VECTOR_STORE enum and per-backend connection options for Postgres, Qdrant, Chroma, Milvus, Pinecone, and MongoDB.
  • In main.start_store, lazily import the selected backend’s driver, construct the appropriate LangChain vector store and adapter, and remove the previous hard-coded PGVector/session_factory wiring.
  • Update MarkdownRAG to depend only on a VectorStoreAdapter, delegating existence checks, document listing, and deletion to the adapter instead of direct SQL/PGVector access.
src/markdown_rag/config.py
src/markdown_rag/models.py
src/markdown_rag/main.py
src/markdown_rag/rag.py
Make vector store dependencies optional and document multi-backend configuration and architecture changes.
  • Move langchain-postgres into a postgres extra and add extras for qdrant, chroma, milvus, pinecone, and mongodb, removing the hard dependency from the core package.
  • Add docs/vector-stores.md describing supported backends, required environment variables, and installation of optional extras.
  • Update README and architecture docs (diagrams, flows, configuration tables, TODOs) to reflect pluggable vector stores, Ollama embeddings, and new VECTOR_STORE configuration.
pyproject.toml
README.md
docs/architecture.md
docs/vector-stores.md
uv.lock

Assessment against linked issues

Issue Objective Addressed Explanation
#5 Introduce a vector store abstraction/interface and configuration option so the system can use multiple pluggable vector stores instead of only PostgreSQL + pgvector.
#5 Implement provider-specific adapters for additional vector store backends beyond PostgreSQL, using the common abstraction and ensuring a consistent query interface across providers.
#5 Update documentation to describe the multi-vector-store setup, including provider-specific configuration and environment variables.

Possibly linked issues

  • #: They align: PR adds vector store abstraction, multiple providers, configuration, and docs as requested by the issue.

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@qodo-code-review

Copy link
Copy Markdown

Review Summary by Qodo

Support multiple vector store backends with pluggable adapter pattern

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Implement pluggable vector store architecture supporting 6 backends
  - PostgreSQL (pgvector), Qdrant, Chroma, Milvus, Pinecone, MongoDB
• Move database dependencies to optional extras in pyproject.toml
• Add lazy loading of database drivers based on VECTOR_STORE configuration
• Refactor RAG core logic to use VectorStoreAdapter protocol interface
• Update documentation with vector store setup and configuration guide
Diagram
flowchart LR
  Config["VECTOR_STORE Config"]
  Main["main.py<br/>start_store()"]
  Adapter["VectorStoreAdapter<br/>Protocol"]
  PG["PostgresAdapter"]
  QD["QdrantAdapter"]
  CH["ChromaAdapter"]
  MV["MilvusAdapter"]
  PN["PineconeAdapter"]
  MG["MongoDBAdapter"]
  RAG["MarkdownRAG<br/>Core Logic"]
  
  Config -->|selects backend| Main
  Main -->|lazy loads| PG
  Main -->|lazy loads| QD
  Main -->|lazy loads| CH
  Main -->|lazy loads| MV
  Main -->|lazy loads| PN
  Main -->|lazy loads| MG
  PG -->|implements| Adapter
  QD -->|implements| Adapter
  CH -->|implements| Adapter
  MV -->|implements| Adapter
  PN -->|implements| Adapter
  MG -->|implements| Adapter
  Adapter -->|decouples| RAG
Loading

Grey Divider

File Changes

1. src/markdown_rag/config.py ⚙️ Configuration changes +23/-1

Add vector store configuration fields

src/markdown_rag/config.py


2. src/markdown_rag/models.py ✨ Enhancement +11/-0

Add VectorStoreType enum for backend selection

src/markdown_rag/models.py


3. src/markdown_rag/main.py ✨ Enhancement +164/-16

Implement lazy loading with match statement for all backends

src/markdown_rag/main.py


View more (12)
4. src/markdown_rag/rag.py ✨ Enhancement +5/-63

Refactor to use VectorStoreAdapter protocol interface

src/markdown_rag/rag.py


5. src/markdown_rag/vector_stores/base.py ✨ Enhancement +34/-0

Define VectorStoreAdapter protocol with common interface

src/markdown_rag/vector_stores/base.py


6. src/markdown_rag/vector_stores/postgres.py ✨ Enhancement +109/-0

Implement PostgreSQL pgvector adapter with session management

src/markdown_rag/vector_stores/postgres.py


7. src/markdown_rag/vector_stores/qdrant.py ✨ Enhancement +94/-0

Implement Qdrant adapter with pagination and filtering

src/markdown_rag/vector_stores/qdrant.py


8. src/markdown_rag/vector_stores/chroma.py ✨ Enhancement +62/-0

Implement Chroma adapter with metadata filtering

src/markdown_rag/vector_stores/chroma.py


9. src/markdown_rag/vector_stores/milvus.py ✨ Enhancement +58/-0

Implement Milvus adapter with collection queries

src/markdown_rag/vector_stores/milvus.py


10. src/markdown_rag/vector_stores/pinecone.py ✨ Enhancement +76/-0

Implement Pinecone adapter with index operations

src/markdown_rag/vector_stores/pinecone.py


11. src/markdown_rag/vector_stores/mongodb.py ✨ Enhancement +54/-0

Implement MongoDB Atlas adapter with collection operations

src/markdown_rag/vector_stores/mongodb.py


12. pyproject.toml Dependencies +20/-1

Move database dependencies to optional extras

pyproject.toml


13. README.md 📝 Documentation +11/-11

Update features and installation with vector store options

README.md


14. docs/architecture.md 📝 Documentation +66/-75

Add vector store adapter layer and update diagrams

docs/architecture.md


15. docs/vector-stores.md 📝 Documentation +109/-0

Add comprehensive vector store setup and configuration guide

docs/vector-stores.md


Grey Divider

Qodo Logo

@qodo-code-review

qodo-code-review Bot commented May 12, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (4) 📘 Rule violations (0) 📎 Requirement gaps (2)

Context used

Grey Divider


Action required

1. delete_filename() always returns True 📎 Requirement gap ≡ Correctness
Description
Several new vector store adapters return True from delete_filename() unconditionally, while
others return True only when at least one document was actually deleted. This makes deletion
behavior inconsistent across providers and can mislead callers relying on the boolean result.
Code

src/markdown_rag/vector_stores/milvus.py[R45-48]

+    def delete_filename(self, filename: str) -> bool:
+        """Delete all documents associated with a filename."""
+        self.vector_store.col.delete(expr=f"filename == '{filename}'")
+        return True
Evidence
PR Compliance ID 4 requires consistent query/operation semantics across providers. In this PR,
PostgresAdapter.delete_filename() returns len(result) > 0, while other adapters unconditionally
return True, making the boolean return value inconsistent across vector store providers.

Query interface remains consistent across all supported vector store providers
src/markdown_rag/vector_stores/postgres.py[62-84]
src/markdown_rag/vector_stores/milvus.py[45-48]
src/markdown_rag/vector_stores/qdrant.py[60-75]
src/markdown_rag/vector_stores/mongodb.py[42-46]
src/markdown_rag/vector_stores/pinecone.py[65-68]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`VectorStoreAdapter.delete_filename()` is expected to provide consistent semantics across providers, but multiple adapters always return `True` even when nothing may have been deleted. This breaks uniform behavior and can cause incorrect UX/logic (e.g., reporting a delete succeeded when no documents matched).

## Issue Context
`MarkdownRAG.delete_document()` directly returns `self.vector_store.delete_filename(filename)`, so this inconsistency propagates to user-visible behavior.

## Fix Focus Areas
- src/markdown_rag/vector_stores/milvus.py[45-48]
- src/markdown_rag/vector_stores/qdrant.py[60-75]
- src/markdown_rag/vector_stores/mongodb.py[42-46]
- src/markdown_rag/vector_stores/pinecone.py[65-68]

## Notes
Update each adapter to return `True` only if at least one record/vector was deleted (or otherwise document a consistent contract, e.g., `True` means the delete operation executed successfully and include a separate count).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. PineconeAdapter hardcodes vector size 📎 Requirement gap ☼ Reliability
Description
PineconeAdapter.list_filenames() queries Pinecone using a hardcoded 1536-dimension dummy vector,
which can fail for indexes built with different embedding dimensions and diverges from other
providers’ dimension-agnostic listing behavior. It also uses top_k=10000, which may be expensive
on large indexes, undermining a consistent provider-agnostic interface.
Code

src/markdown_rag/vector_stores/pinecone.py[R51-55]

+        results = index.query(
+            vector=[0] * 1536, # Dummy vector size
+            top_k=10000,
+            include_metadata=True
+        )
Evidence
Compliance ID 4 requires consistent operation semantics across vector stores, but the Pinecone
adapter implements list_filenames() by issuing a vector query with a fixed-size dummy vector (`[0]
* 1536`), meaning the method can error if the Pinecone index dimension differs from 1536 depending
on the chosen embedding model/index configuration. This risk is heightened because the repository
supports multiple embedding engines and configurable models (e.g., via GOOGLE_MODEL /
OLLAMA_MODEL) without any guarantee in code that embeddings are 1536-dimensional, and the
implementation’s large top_k request further makes behavior and cost characteristics diverge
across backends and index sizes.

Query interface remains consistent across all supported vector store providers
src/markdown_rag/vector_stores/pinecone.py[35-55]
src/markdown_rag/config.py[132-170]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`PineconeAdapter.list_filenames()` currently issues a Pinecone vector query using a dummy vector with a hardcoded length of `1536`, which can raise runtime errors when the Pinecone index dimension differs due to configurable embedding models. The method also requests `top_k=10000`, which can be unnecessarily expensive for large indexes and further undermines consistent, provider-agnostic behavior.

## Issue Context
- Compliance requires consistent operation semantics across vector store providers.
- Other adapters can list filenames without assuming embedding dimensionality, but the Pinecone implementation depends on a vector query and thus implicitly assumes an embedding dimension.
- Embedding model selection is configurable (e.g., `GOOGLE_MODEL` / `OLLAMA_MODEL`), and there is no code ensuring those embeddings (or the Pinecone index) are 1536-dimensional.

## Fix Focus Areas
- src/markdown_rag/vector_stores/pinecone.py[35-63]
- src/markdown_rag/config.py[132-170]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Eager PGVector import 🐞 Bug ☼ Reliability
Description
src/markdown_rag/main.py still imports langchain_postgres.PGVector at module import time, so
installs without the postgres extra will crash before the VECTOR_STORE match/case can lazy-load
backends. This defeats the PR’s optional-deps design and prevents using non-Postgres stores without
also installing Postgres deps.
Code

src/markdown_rag/main.py[R37-45]

+        case VectorStoreType.PGVECTOR:
+            try:
+                from langchain_postgres import PGVector
+            except ImportError:
+                logger.error(
+                    "Postgres support is not installed. "
+                    "Please install with 'uv add markdown-rag --optional postgres'"
+                )
+                sys.exit(1)
Evidence
langchain-postgres is no longer a base dependency (now an optional extra), but main.py imports
it unconditionally, so the process will fail during module import in environments that didn’t
install the postgres extra.

pyproject.toml[18-28]
src/markdown_rag/main.py[1-10]
src/markdown_rag/main.py[35-45]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`src/markdown_rag/main.py` imports `langchain_postgres.PGVector` at module import time, but `langchain-postgres` was moved to an optional extra. This causes an immediate `ImportError` for users who install without the `postgres` extra (even if they configure `VECTOR_STORE` to `qdrant/chroma/milvus/pinecone/mongodb`).

### Issue Context
The code already has a `try/except ImportError` lazy import inside the `VectorStoreType.PGVECTOR` match/case branch, but the unconditional import at the top triggers first.

### Fix Focus Areas
- src/markdown_rag/main.py[1-10]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (2)
4. Postgres password always required 🐞 Bug ≡ Correctness
Description
Env.POSTGRES_PASSWORD is required in the base settings class, so configuration validation fails even
when VECTOR_STORE is not pgvector. This blocks startup for Qdrant/Chroma/Milvus/Pinecone/MongoDB
unless users still provide POSTGRES_PASSWORD.
Code

src/markdown_rag/config.py[R28-34]

+    VECTOR_STORE: VectorStoreType = Field(default=VectorStoreType.PGVECTOR)
+
    POSTGRES_USER: str = Field(default="postgres")
    POSTGRES_PASSWORD: SecretStr = Field(default=...)
    POSTGRES_HOST: str = Field(default="localhost")
    POSTGRES_PORT: str = Field(default="5432")
    POSTGRES_DB: str | None = Field(default=None)
Evidence
Pydantic validates required fields at settings instantiation (env_class()), and the code
constructs settings before selecting a vector store backend, so a missing POSTGRES_PASSWORD prevents
non-Postgres operation.

src/markdown_rag/config.py[23-34]
src/markdown_rag/main.py[200-206]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`POSTGRES_PASSWORD` is declared as required on the base `Env` settings class, so Pydantic validates it on every startup regardless of `VECTOR_STORE`. This makes non-Postgres backends unusable unless Postgres credentials are still provided.

### Issue Context
`main()` instantiates the `Env` subclass before `start_store()` selects the vector store backend.

### Fix Focus Areas
- src/markdown_rag/config.py[23-35]
- src/markdown_rag/main.py[200-206]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Milvus expr string injection 🐞 Bug ⛨ Security
Description
MilvusAdapter.delete_filename and exists build Milvus expr strings via direct interpolation, so
filenames/metadata containing quotes can break the expression or alter its meaning. This can cause
failed deletes/dedup checks or unintended query behavior.
Code

src/markdown_rag/vector_stores/milvus.py[R45-58]

+    def delete_filename(self, filename: str) -> bool:
+        """Delete all documents associated with a filename."""
+        self.vector_store.col.delete(expr=f"filename == '{filename}'")
+        return True
+
+    def exists(self, metadata: dict[str, str]) -> bool:
+        """Check if documents with given metadata exist."""
+        expr = " and ".join([f"{k} == '{v}'" for k, v in metadata.items()])
+        res = self.vector_store.col.query(
+            expr=expr,
+            limit=1,
+            output_fields=["pk"]
+        )
+        return len(res) > 0
Evidence
The adapter interpolates unescaped values into Milvus expressions, and those values come directly
from file paths used as metadata during ingestion, making malformed expressions and injection
possible.

src/markdown_rag/vector_stores/milvus.py[45-58]
src/markdown_rag/rag.py[71-77]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`MilvusAdapter` constructs Milvus filter expressions using raw string interpolation (e.g., `filename == '{filename}'`). If `filename` (derived from filesystem-relative paths) contains `'` or other special characters, the expression becomes invalid or can be manipulated.

### Issue Context
`MarkdownRAG` uses filesystem-derived relative paths as the `filename` metadata key and passes it into `exists()` and `delete_filename()`.

### Fix Focus Areas
- src/markdown_rag/vector_stores/milvus.py[45-58]
- src/markdown_rag/rag.py[71-77]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

6. Pinecone exists embeds each file 🐞 Bug ➹ Performance
Description
PineconeAdapter.exists performs a similarity_search("dummy") to check metadata existence, causing an
embedding request per file during ingest. This can significantly slow ingestion and consume
rate-limited/paid embedding calls despite being a pure metadata dedup check.
Code

src/markdown_rag/vector_stores/pinecone.py[R70-76]

+    def exists(self, metadata: dict[str, str]) -> bool:
+        """Check if documents with given metadata exist."""
+        # Use similarity search with filter to check existence
+        docs = self.vector_store.similarity_search(
+            "dummy", k=1, filter=metadata
+        )
+        return len(docs) > 0
Evidence
Ingestion performs an exists() call for every file based on filename metadata, and the Pinecone
implementation translates that into a similarity search over an embedded query string, adding extra
embedding work per file.

src/markdown_rag/rag.py[67-87]
src/markdown_rag/vector_stores/pinecone.py[70-76]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`PineconeAdapter.exists()` uses `similarity_search("dummy", k=1, filter=metadata)` which forces an embedding computation. During ingestion, this runs once per file for deduplication and can create substantial extra embedding calls and delay.

### Issue Context
`MarkdownRAG._add_document()` checks `exists({"filename": filename})` before ingesting each file.

### Fix Focus Areas
- src/markdown_rag/vector_stores/pinecone.py[70-76]
- src/markdown_rag/rag.py[67-87]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 4 issues, and left some high level feedback:

  • The PineconeAdapter’s list_filenames and exists methods rely on a hard-coded dummy vector, large top_k, and a get_pinecone_index() call, which may be brittle and inefficient for larger indexes; consider either using Pinecone’s metadata filtering APIs directly (without embedding) or clearly documenting and constraining this implementation (e.g., configurable vector size/top_k) to avoid performance surprises.
  • In MilvusAdapter.exists the filter expression is built via string interpolation (e.g., f"{k} == '{v}'"), which will break if values contain quotes and is hard to make safe; prefer using Milvus’ recommended expression-building patterns or sanitizing/escaping values before constructing the expression.
  • The start_store function in main.py has grown into a large match block with backend-specific wiring; consider extracting a create_vector_store_adapter(env, embeddings, directory) factory to keep main focused on orchestration and make adding/removing backends simpler.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `PineconeAdapter`’s `list_filenames` and `exists` methods rely on a hard-coded dummy vector, large `top_k`, and a `get_pinecone_index()` call, which may be brittle and inefficient for larger indexes; consider either using Pinecone’s metadata filtering APIs directly (without embedding) or clearly documenting and constraining this implementation (e.g., configurable vector size/top_k) to avoid performance surprises.
- In `MilvusAdapter.exists` the filter expression is built via string interpolation (e.g., `f"{k} == '{v}'"`), which will break if values contain quotes and is hard to make safe; prefer using Milvus’ recommended expression-building patterns or sanitizing/escaping values before constructing the expression.
- The `start_store` function in `main.py` has grown into a large `match` block with backend-specific wiring; consider extracting a `create_vector_store_adapter(env, embeddings, directory)` factory to keep `main` focused on orchestration and make adding/removing backends simpler.

## Individual Comments

### Comment 1
<location path="src/markdown_rag/vector_stores/pinecone.py" line_range="44-52" />
<code_context>
+        # A common workaround is to use a zero vector for a broad search.
+        # Note: This might be slow for very large datasets.
+        
+        # Access the raw index client
+        index = self.vector_store.get_pinecone_index()
+        
+        # We can't easily list all without a query in standard Pinecone.
+        # Using a workaround: list all by prefix if possible, or query with zero vector.
+        # Here we'll just try to fetch a large sample.
+        
+        results = index.query(
+            vector=[0] * 1536, # Dummy vector size
+            top_k=10000,
+            include_metadata=True
</code_context>
<issue_to_address>
**issue (performance):** Avoid hardcoding the Pinecone embedding dimension and large broad queries in list_filenames.

This assumes a 1536‑dimensional embedding and issues a single `top_k=10000` query, which ties this code to a specific model and can become slow/expensive on large indexes. It also depends on `get_pinecone_index()`, which may not be a stable public API. Consider deriving the dimension from index metadata or the embeddings object and iterating with pagination over the raw client (e.g., `list` or repeated `query` calls with smaller `top_k`) instead of one broad dummy‑vector query. If full refactoring isn’t feasible now, at least make the dimension configurable rather than hard‑coded.
</issue_to_address>

### Comment 2
<location path="src/markdown_rag/vector_stores/pinecone.py" line_range="73-74" />
<code_context>
+    def exists(self, metadata: dict[str, str]) -> bool:
+        """Check if documents with given metadata exist."""
+        # Use similarity search with filter to check existence
+        docs = self.vector_store.similarity_search(
+            "dummy", k=1, filter=metadata
+        )
+        return len(docs) > 0
</code_context>
<issue_to_address>
**suggestion (performance):** Using similarity_search with a dummy query for existence checks is likely inefficient.

This implementation always performs an embedding + similarity query just to check presence, which can add avoidable latency and cost if `exists` is called often. Prefer using the underlying Pinecone index client for a minimal `query` with a fixed/cheap vector, or (if available) a metadata-only lookup. As another option, you could make this existence check optional/best-effort for Pinecone-backed stores to avoid unnecessary embedding calls.
</issue_to_address>

### Comment 3
<location path="src/markdown_rag/vector_stores/milvus.py" line_range="47-52" />
<code_context>
+
+    def delete_filename(self, filename: str) -> bool:
+        """Delete all documents associated with a filename."""
+        self.vector_store.col.delete(expr=f"filename == '{filename}'")
+        return True
+
+    def exists(self, metadata: dict[str, str]) -> bool:
+        """Check if documents with given metadata exist."""
+        expr = " and ".join([f"{k} == '{v}'" for k, v in metadata.items()])
+        res = self.vector_store.col.query(
+            expr=expr,
</code_context>
<issue_to_address>
**🚨 issue (security):** String-interpolated expressions for Milvus queries risk quoting bugs and injection issues.

`delete_filename` and `exists` build Milvus `expr` strings directly from user data via f-strings (e.g., `filename == '{filename}'`, `" and ".join([f"{k} == '{v}'" ...])`). If any value contains quotes or special characters, the query can break or be exploitable. These values should be safely escaped or validated, ideally via a Milvus query builder or parameterized API rather than manual string concatenation.
</issue_to_address>

### Comment 4
<location path="src/markdown_rag/vector_stores/pinecone.py" line_range="52" />
<code_context>
+        # Here we'll just try to fetch a large sample.
+        
+        results = index.query(
+            vector=[0] * 1536, # Dummy vector size
+            top_k=10000,
+            include_metadata=True
</code_context>
<issue_to_address>
**issue (complexity):** Consider refactoring the Pinecone adapter to centralize the dummy vector logic, infer the embedding dimension dynamically, and use direct Pinecone queries for metadata checks instead of similarity_search.

You can reduce the “surprising” complexity without dropping functionality by:

1. Removing the hard‑coded embedding dimension.
2. Avoiding similarity_search for non‑similarity operations.
3. Centralizing the “dummy query” behavior in a small internal helper so callers see a clear, honest API.

### 1. Avoid hard‑coded 1536 and expose the approximation

Derive the dimension once from the configured embeddings instead of baking in `1536`, and make the “broad scan” behavior explicit:

```python
class PineconeAdapter(VectorStoreAdapter):
    def __init__(self, vector_store: "PineconeVectorStore"):
        self.vector_store = vector_store
        # Infer dimension from the configured embeddings
        sample_vec = self.vector_store._embedding.embed_query("")  # or similar API
        self._dim = len(sample_vec)

    def _dummy_vector(self) -> list[float]:
        # Single place where we define the "broad search" dummy vector
        return [0.0] * self._dim

    def list_filenames(self) -> list[str]:
        """Best-effort listing of unique filenames using a broad query.

        Note: This uses a dummy vector to approximate "scan all" and may be slow
        for large indexes.
        """
        index = self.vector_store.get_pinecone_index()
        results = index.query(
            vector=self._dummy_vector(),
            top_k=10_000,
            include_metadata=True,
        )

        filenames: set[str] = set()
        for match in results.get("matches", []):
            metadata = match.get("metadata") or {}
            filename = metadata.get("filename")
            if filename:
                filenames.add(filename)

        return sorted(filenames)
```

This removes the magic `1536`, keeps the behavior, and documents that it is an approximation with potential performance cost.

### 2. Use Pinecone query directly for `exists`, not similarity_search

You can keep semantics but avoid the “dummy text” similarity_search and make the metadata‑filter behavior obvious:

```python
class PineconeAdapter(VectorStoreAdapter):
    ...

    def exists(self, metadata: dict[str, str]) -> bool:
        """Check if documents with given metadata exist.

        This relies on a minimal Pinecone query with a dummy vector, using only
        the metadata filter for existence.
        """
        index = self.vector_store.get_pinecone_index()
        res = index.query(
            vector=self._dummy_vector(),
            top_k=1,
            filter=metadata,
            include_metadata=False,
        )
        return bool(res.get("matches"))
```

This:

- Decouples existence from a semantic “similarity search” API.
- Keeps the same functional behavior (metadata‑filtered query with `top_k=1`).
- Moves all the “hackiness” into `_dummy_vector` + clear docstrings, so callers aren’t surprised by magic dimensions or dummy queries.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +44 to +52
# Access the raw index client
index = self.vector_store.get_pinecone_index()

# We can't easily list all without a query in standard Pinecone.
# Using a workaround: list all by prefix if possible, or query with zero vector.
# Here we'll just try to fetch a large sample.

results = index.query(
vector=[0] * 1536, # Dummy vector size

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (performance): Avoid hardcoding the Pinecone embedding dimension and large broad queries in list_filenames.

This assumes a 1536‑dimensional embedding and issues a single top_k=10000 query, which ties this code to a specific model and can become slow/expensive on large indexes. It also depends on get_pinecone_index(), which may not be a stable public API. Consider deriving the dimension from index metadata or the embeddings object and iterating with pagination over the raw client (e.g., list or repeated query calls with smaller top_k) instead of one broad dummy‑vector query. If full refactoring isn’t feasible now, at least make the dimension configurable rather than hard‑coded.

Comment on lines +73 to +74
docs = self.vector_store.similarity_search(
"dummy", k=1, filter=metadata

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (performance): Using similarity_search with a dummy query for existence checks is likely inefficient.

This implementation always performs an embedding + similarity query just to check presence, which can add avoidable latency and cost if exists is called often. Prefer using the underlying Pinecone index client for a minimal query with a fixed/cheap vector, or (if available) a metadata-only lookup. As another option, you could make this existence check optional/best-effort for Pinecone-backed stores to avoid unnecessary embedding calls.

Comment on lines +47 to +52
self.vector_store.col.delete(expr=f"filename == '{filename}'")
return True

def exists(self, metadata: dict[str, str]) -> bool:
"""Check if documents with given metadata exist."""
expr = " and ".join([f"{k} == '{v}'" for k, v in metadata.items()])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚨 issue (security): String-interpolated expressions for Milvus queries risk quoting bugs and injection issues.

delete_filename and exists build Milvus expr strings directly from user data via f-strings (e.g., filename == '{filename}', " and ".join([f"{k} == '{v}'" ...])). If any value contains quotes or special characters, the query can break or be exploitable. These values should be safely escaped or validated, ideally via a Milvus query builder or parameterized API rather than manual string concatenation.

# Here we'll just try to fetch a large sample.

results = index.query(
vector=[0] * 1536, # Dummy vector size

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (complexity): Consider refactoring the Pinecone adapter to centralize the dummy vector logic, infer the embedding dimension dynamically, and use direct Pinecone queries for metadata checks instead of similarity_search.

You can reduce the “surprising” complexity without dropping functionality by:

  1. Removing the hard‑coded embedding dimension.
  2. Avoiding similarity_search for non‑similarity operations.
  3. Centralizing the “dummy query” behavior in a small internal helper so callers see a clear, honest API.

1. Avoid hard‑coded 1536 and expose the approximation

Derive the dimension once from the configured embeddings instead of baking in 1536, and make the “broad scan” behavior explicit:

class PineconeAdapter(VectorStoreAdapter):
    def __init__(self, vector_store: "PineconeVectorStore"):
        self.vector_store = vector_store
        # Infer dimension from the configured embeddings
        sample_vec = self.vector_store._embedding.embed_query("")  # or similar API
        self._dim = len(sample_vec)

    def _dummy_vector(self) -> list[float]:
        # Single place where we define the "broad search" dummy vector
        return [0.0] * self._dim

    def list_filenames(self) -> list[str]:
        """Best-effort listing of unique filenames using a broad query.

        Note: This uses a dummy vector to approximate "scan all" and may be slow
        for large indexes.
        """
        index = self.vector_store.get_pinecone_index()
        results = index.query(
            vector=self._dummy_vector(),
            top_k=10_000,
            include_metadata=True,
        )

        filenames: set[str] = set()
        for match in results.get("matches", []):
            metadata = match.get("metadata") or {}
            filename = metadata.get("filename")
            if filename:
                filenames.add(filename)

        return sorted(filenames)

This removes the magic 1536, keeps the behavior, and documents that it is an approximation with potential performance cost.

2. Use Pinecone query directly for exists, not similarity_search

You can keep semantics but avoid the “dummy text” similarity_search and make the metadata‑filter behavior obvious:

class PineconeAdapter(VectorStoreAdapter):
    ...

    def exists(self, metadata: dict[str, str]) -> bool:
        """Check if documents with given metadata exist.

        This relies on a minimal Pinecone query with a dummy vector, using only
        the metadata filter for existence.
        """
        index = self.vector_store.get_pinecone_index()
        res = index.query(
            vector=self._dummy_vector(),
            top_k=1,
            filter=metadata,
            include_metadata=False,
        )
        return bool(res.get("matches"))

This:

  • Decouples existence from a semantic “similarity search” API.
  • Keeps the same functional behavior (metadata‑filtered query with top_k=1).
  • Moves all the “hackiness” into _dummy_vector + clear docstrings, so callers aren’t surprised by magic dimensions or dummy queries.

Comment on lines +45 to +48
def delete_filename(self, filename: str) -> bool:
"""Delete all documents associated with a filename."""
self.vector_store.col.delete(expr=f"filename == '{filename}'")
return True

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. delete_filename() always returns true 📎 Requirement gap ≡ Correctness

Several new vector store adapters return True from delete_filename() unconditionally, while
others return True only when at least one document was actually deleted. This makes deletion
behavior inconsistent across providers and can mislead callers relying on the boolean result.
Agent Prompt
## Issue description
`VectorStoreAdapter.delete_filename()` is expected to provide consistent semantics across providers, but multiple adapters always return `True` even when nothing may have been deleted. This breaks uniform behavior and can cause incorrect UX/logic (e.g., reporting a delete succeeded when no documents matched).

## Issue Context
`MarkdownRAG.delete_document()` directly returns `self.vector_store.delete_filename(filename)`, so this inconsistency propagates to user-visible behavior.

## Fix Focus Areas
- src/markdown_rag/vector_stores/milvus.py[45-48]
- src/markdown_rag/vector_stores/qdrant.py[60-75]
- src/markdown_rag/vector_stores/mongodb.py[42-46]
- src/markdown_rag/vector_stores/pinecone.py[65-68]

## Notes
Update each adapter to return `True` only if at least one record/vector was deleted (or otherwise document a consistent contract, e.g., `True` means the delete operation executed successfully and include a separate count).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +51 to +55
results = index.query(
vector=[0] * 1536, # Dummy vector size
top_k=10000,
include_metadata=True
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

2. pineconeadapter hardcodes vector size 📎 Requirement gap ☼ Reliability

PineconeAdapter.list_filenames() queries Pinecone using a hardcoded 1536-dimension dummy vector,
which can fail for indexes built with different embedding dimensions and diverges from other
providers’ dimension-agnostic listing behavior. It also uses top_k=10000, which may be expensive
on large indexes, undermining a consistent provider-agnostic interface.
Agent Prompt
## Issue description
`PineconeAdapter.list_filenames()` currently issues a Pinecone vector query using a dummy vector with a hardcoded length of `1536`, which can raise runtime errors when the Pinecone index dimension differs due to configurable embedding models. The method also requests `top_k=10000`, which can be unnecessarily expensive for large indexes and further undermines consistent, provider-agnostic behavior.

## Issue Context
- Compliance requires consistent operation semantics across vector store providers.
- Other adapters can list filenames without assuming embedding dimensionality, but the Pinecone implementation depends on a vector query and thus implicitly assumes an embedding dimension.
- Embedding model selection is configurable (e.g., `GOOGLE_MODEL` / `OLLAMA_MODEL`), and there is no code ensuring those embeddings (or the Pinecone index) are 1536-dimensional.

## Fix Focus Areas
- src/markdown_rag/vector_stores/pinecone.py[35-63]
- src/markdown_rag/config.py[132-170]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread src/markdown_rag/main.py
Comment on lines +37 to +45
case VectorStoreType.PGVECTOR:
try:
from langchain_postgres import PGVector
except ImportError:
logger.error(
"Postgres support is not installed. "
"Please install with 'uv add markdown-rag --optional postgres'"
)
sys.exit(1)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

3. Eager pgvector import 🐞 Bug ☼ Reliability

src/markdown_rag/main.py still imports langchain_postgres.PGVector at module import time, so
installs without the postgres extra will crash before the VECTOR_STORE match/case can lazy-load
backends. This defeats the PR’s optional-deps design and prevents using non-Postgres stores without
also installing Postgres deps.
Agent Prompt
### Issue description
`src/markdown_rag/main.py` imports `langchain_postgres.PGVector` at module import time, but `langchain-postgres` was moved to an optional extra. This causes an immediate `ImportError` for users who install without the `postgres` extra (even if they configure `VECTOR_STORE` to `qdrant/chroma/milvus/pinecone/mongodb`).

### Issue Context
The code already has a `try/except ImportError` lazy import inside the `VectorStoreType.PGVECTOR` match/case branch, but the unconditional import at the top triggers first.

### Fix Focus Areas
- src/markdown_rag/main.py[1-10]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +28 to 34
VECTOR_STORE: VectorStoreType = Field(default=VectorStoreType.PGVECTOR)

POSTGRES_USER: str = Field(default="postgres")
POSTGRES_PASSWORD: SecretStr = Field(default=...)
POSTGRES_HOST: str = Field(default="localhost")
POSTGRES_PORT: str = Field(default="5432")
POSTGRES_DB: str | None = Field(default=None)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

4. Postgres password always required 🐞 Bug ≡ Correctness

Env.POSTGRES_PASSWORD is required in the base settings class, so configuration validation fails even
when VECTOR_STORE is not pgvector. This blocks startup for Qdrant/Chroma/Milvus/Pinecone/MongoDB
unless users still provide POSTGRES_PASSWORD.
Agent Prompt
### Issue description
`POSTGRES_PASSWORD` is declared as required on the base `Env` settings class, so Pydantic validates it on every startup regardless of `VECTOR_STORE`. This makes non-Postgres backends unusable unless Postgres credentials are still provided.

### Issue Context
`main()` instantiates the `Env` subclass before `start_store()` selects the vector store backend.

### Fix Focus Areas
- src/markdown_rag/config.py[23-35]
- src/markdown_rag/main.py[200-206]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +45 to +58
def delete_filename(self, filename: str) -> bool:
"""Delete all documents associated with a filename."""
self.vector_store.col.delete(expr=f"filename == '{filename}'")
return True

def exists(self, metadata: dict[str, str]) -> bool:
"""Check if documents with given metadata exist."""
expr = " and ".join([f"{k} == '{v}'" for k, v in metadata.items()])
res = self.vector_store.col.query(
expr=expr,
limit=1,
output_fields=["pk"]
)
return len(res) > 0

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

5. Milvus expr string injection 🐞 Bug ⛨ Security

MilvusAdapter.delete_filename and exists build Milvus expr strings via direct interpolation, so
filenames/metadata containing quotes can break the expression or alter its meaning. This can cause
failed deletes/dedup checks or unintended query behavior.
Agent Prompt
### Issue description
`MilvusAdapter` constructs Milvus filter expressions using raw string interpolation (e.g., `filename == '{filename}'`). If `filename` (derived from filesystem-relative paths) contains `'` or other special characters, the expression becomes invalid or can be manipulated.

### Issue Context
`MarkdownRAG` uses filesystem-derived relative paths as the `filename` metadata key and passes it into `exists()` and `delete_filename()`.

### Fix Focus Areas
- src/markdown_rag/vector_stores/milvus.py[45-58]
- src/markdown_rag/rag.py[71-77]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a pluggable vector store architecture, enabling support for multiple backends such as Qdrant, Chroma, Milvus, Pinecone, and MongoDB alongside the existing PostgreSQL implementation. The core RAG logic has been refactored to use a VectorStoreAdapter protocol, and dependencies are now managed via optional extras. Review feedback identifies critical security vulnerabilities regarding string injection in the Milvus adapter, logic errors in the return values for document deletion in Milvus and MongoDB, and a hardcoded vector dimension in the Pinecone adapter that should be dynamically retrieved.

Comment on lines +47 to +48
self.vector_store.col.delete(expr=f"filename == '{filename}'")
return True

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

The deletion expression is vulnerable to injection if the filename contains single quotes. Additionally, the protocol expects a boolean indicating if any documents were deleted. Milvus delete returns a MutationResult which contains a delete_count.

Suggested change
self.vector_store.col.delete(expr=f"filename == '{filename}'")
return True
safe_filename = filename.replace("'", "''")
res = self.vector_store.col.delete(expr=f"filename == '{safe_filename}'")
return res.delete_count > 0
References
  1. Code changes must pass MyPy type checking, even if a suggestion is functionally correct at runtime.


def exists(self, metadata: dict[str, str]) -> bool:
"""Check if documents with given metadata exist."""
expr = " and ".join([f"{k} == '{v}'" for k, v in metadata.items()])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

The query expression is vulnerable to injection if metadata values contain single quotes. Values should be sanitized by escaping single quotes.

Suggested change
expr = " and ".join([f"{k} == '{v}'" for k, v in metadata.items()])
expr = " and ".join([f"{k} == '{str(v).replace("'", "''")}'" for k, v in metadata.items()])
References
  1. Code changes must pass MyPy type checking, even if a suggestion is functionally correct at runtime.

Comment on lines +52 to +53
vector=[0] * 1536, # Dummy vector size
top_k=10000,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The vector dimension is hardcoded to 1536, which will cause a runtime error if the index was created with a different dimension. You should retrieve the dimension from the index statistics. Using getattr ensures MyPy compatibility as the response object from Pinecone is not a dictionary.

        stats = index.describe_index_stats()
        dimension = getattr(stats, "dimension", 1536)
        results = index.query(
            vector=[0.0] * dimension,
References
  1. Code changes must pass MyPy type checking, even if a suggestion is functionally correct at runtime.

Comment on lines +45 to +46
collection.delete_many({"metadata.filename": filename})
return True

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The method should return a boolean indicating whether any documents were actually deleted, rather than always returning True.

Suggested change
collection.delete_many({"metadata.filename": filename})
return True
result = collection.delete_many({"metadata.filename": filename})
return result.deleted_count > 0
References
  1. Code changes must pass MyPy type checking, even if a suggestion is functionally correct at runtime.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add support for other vector stores

1 participant