Skip to content

Vector Stores API

ai4rag talks to vector databases through direct clients selected by a typed configuration object. A config carries the connection details for a single backend, and get_vector_store instantiates the matching store — the backend is chosen entirely from config.provider, so no separate type string is needed. Three backends are supported today:

Backend Config Provider Store Hybrid search
Milvus (remote server only) MilvusConfig "milvus" MilvusVectorStore ✅ server-side dense + BM25
Milvus Lite (embedded, local file only) MilvusLiteConfig "milvus_lite" MilvusVectorStore ✅ embedded dense + BM25
PostgreSQL + pgvector PGVectorConfig "pgvector" PGVectorStore ✅ dense + full-text

MilvusConfig and MilvusLiteConfig both construct a MilvusVectorStore, but they are separate, mutually-exclusive config classes rather than two modes of one config:

Why MilvusConfig and MilvusLiteConfig are separate — and why that matters

MilvusConfig.uri is validated to be an http(s):// URL and raises ValueError for anything else (a bare host, a local file path, an empty string). This is a deliberate safety fix: previously, a mistyped or unreachable MILVUS_URI could be silently interpreted as a local file path, creating an unintended local Milvus Lite database instead of failing — dangerous in production, where it could mask a misconfigured deployment. That silent fallback is no longer possible: a bad MILVUS_URI now fails loudly at construction time.

To use the embedded engine, opt in explicitly with MilvusLiteConfig(db_path="./ai4rag.db") (or MilvusLiteConfig() for the default path, DEFAULT_MILVUS_LITE_DB_PATH = "./ai4rag_milvus_lite.db"). MilvusLiteConfig validates the inverse — it rejects http(s):// values in db_path, since those belong in MilvusConfig.

Milvus Lite limitations

Milvus Lite is intended for local development, tests, and small-scale workloads, not production. It computes BM25 statistics segment-locally rather than corpus-wide, so hybrid-search ranking fidelity (and any benchmark/HPO scores measured against it) may not transfer exactly to a production Milvus server; and it serializes writes, so only one process should open a given .db file at a time. For production or large corpora, use a remote Milvus server (MilvusConfig), Zilliz Cloud, or pgvector.

Every config is a frozen dataclass exposing a from_env() classmethod, so connection details (and secrets) can be sourced from environment variables and never embedded in generated artefacts.

Base Vector Store

base_vector_store

Classes

BaseVectorStore

BaseVectorStore(embedding_model: BaseEmbeddingModel, config: BaseVectorStoreConfig, distance_metric: str, collection_name: str | None = None)

Bases: ABC

Abstract class defining interface for VectorStore in the ai4rag experiment. Single instance defines 1 collection/index that can be used to store or retrieve data.

Parameters:

  • embedding_model (BaseEmbeddingModel) –

    Model used to embed documents and queries.

  • config (BaseVectorStoreConfig) –

    Backend-specific connection parameters.

  • distance_metric (str) –

    Metric used to measure similarity between vectors.

  • collection_name (str | None, default: None ) –

    Existing collection to reuse; must start with the ai4rag prefix. When None, a new compliant name is generated (see :func:ai4rag.rag.vector_store.utils.resolve_collection_name).

Source code in ai4rag/rag/vector_store/base_vector_store.py
def __init__(
    self,
    embedding_model: BaseEmbeddingModel,
    config: BaseVectorStoreConfig,
    distance_metric: str,
    collection_name: str | None = None,
):
    """Initialize the state shared by every concrete vector store.

    Parameters
    ----------
    embedding_model : BaseEmbeddingModel
        Model used to embed documents and queries.
    config : BaseVectorStoreConfig
        Backend-specific connection parameters.
    distance_metric : str
        Metric used to measure similarity between vectors.
    collection_name : str | None, default=None
        Existing collection to reuse; must start with the ``ai4rag`` prefix.
        When ``None``, a new compliant name is generated (see
        :func:`ai4rag.rag.vector_store.utils.resolve_collection_name`).
    """
    self.embedding_model = embedding_model
    self._config = config
    self.distance_metric = distance_metric
    self._collection_name = resolve_collection_name(collection_name)
Attributes
collection_name property
collection_name: str

The resolved collection name — reused when supplied, otherwise generated.

Guaranteed to start with :data:~ai4rag.rag.vector_store.utils.COLLECTION_NAME_PREFIX and to be a valid, length-bounded identifier usable as both a backend collection name and a physical SQL table name.

Methods:
search abstractmethod
search(query: str, k: int, **kwargs) -> list[AI4RAGChunk]

Search for the chunks relevant to the query. The method used will be simple similarity search.

Parameters:

  • query (str) –

    Question / query for which the similarity search will be executed.

  • k (int) –

    Number of chunks to be returned as a result of similarity search

  • **kwargs (Any, default: {} ) –

    Backend-specific search options (e.g. metadata filters or hybrid search parameters). Ignored by backends that do not support them.

Returns:

  • list[AI4RAGChunk] –

    List of chunks with content and metadata.

Source code in ai4rag/rag/vector_store/base_vector_store.py
@abstractmethod
def search(self, query: str, k: int, **kwargs) -> list[AI4RAGChunk]:
    """
    Search for the chunks relevant to the query.
    The method used will be simple similarity search.

    Parameters
    ----------
    query : str
        Question / query for which the similarity search will be executed.

    k : int
        Number of chunks to be returned as a result of similarity search

    **kwargs : Any
        Backend-specific search options (e.g. metadata filters or hybrid
        search parameters). Ignored by backends that do not support them.

    Returns
    -------
    list[AI4RAGChunk]
        List of chunks with content and metadata.
    """
add_documents abstractmethod
add_documents(documents: Sequence[AI4RAGChunk]) -> None

Add documents to the collection.

Parameters:

  • documents (Sequence[AI4RAGChunk]) –

    Chunks to add to the collection.

Source code in ai4rag/rag/vector_store/base_vector_store.py
@abstractmethod
def add_documents(self, documents: Sequence[AI4RAGChunk]) -> None:
    """
    Add documents to the collection.

    Parameters
    ----------
    documents : Sequence[AI4RAGChunk]
        Chunks to add to the collection.
    """
close
close() -> None

Release backend resources held by this store (connections, clients).

A no-op by default. Concrete stores that hold a real connection or client (e.g. :class:~ai4rag.rag.vector_store.pgvector.PGVectorStore, :class:~ai4rag.rag.vector_store.milvus.MilvusVectorStore) override this to release it; callers should call close() (or use the store as a context manager) once they are done searching or indexing, since the store performs no automatic cleanup on garbage collection. Idempotent: safe to call more than once.

Source code in ai4rag/rag/vector_store/base_vector_store.py
def close(self) -> None:
    """Release backend resources held by this store (connections, clients).

    A no-op by default. Concrete stores that hold a real connection or
    client (e.g. :class:`~ai4rag.rag.vector_store.pgvector.PGVectorStore`,
    :class:`~ai4rag.rag.vector_store.milvus.MilvusVectorStore`) override this
    to release it; callers should call ``close()`` (or use the store as a
    context manager) once they are done searching or indexing, since the
    store performs no automatic cleanup on garbage collection. Idempotent:
    safe to call more than once.
    """

Functions:

Configuration

config

Classes

BaseVectorStoreConfig dataclass

BaseVectorStoreConfig(*, provider: str)

Bases: ABC

Base config shared by every vector store backend.

Attributes:

  • provider (str) –

    Backend discriminator ("milvus", "milvus_lite", or "pgvector") used by :func:ai4rag.rag.vector_store.get_vector_store.get_vector_store to select the concrete store class.

Methods:
from_env abstractmethod classmethod
from_env() -> BaseVectorStoreConfig

Create config from environment variables.

Source code in ai4rag/rag/vector_store/config.py
@classmethod
@abstractmethod
def from_env(cls) -> "BaseVectorStoreConfig":
    """Create config from environment variables."""

MilvusConfig dataclass

MilvusConfig(*, provider: str = 'milvus', uri: str, token: str | None = None, server_cert: str | None = None)

Bases: BaseVectorStoreConfig

Connection parameters for a remote Milvus server.

This config targets a running Milvus (or Zilliz Cloud) instance reached over gRPC. For an embedded, local, zero-server database use :class:MilvusLiteConfig instead — the two are deliberately separate so that a mistyped or unreachable server uri fails loudly rather than silently spinning up a throwaway local database (a dangerous surprise in production).

To enforce that, uri must be an http:// or https:// URL; anything else (a bare host, a file path, an empty string) is rejected at construction. TLS is driven by the scheme: https:// opens a secure gRPC channel, http:// stays plaintext. When a remote endpoint presents a certificate signed by a self-signed or private CA, pass the CA/server certificate as PEM text via server_cert; :class:~ai4rag.rag.vector_store.milvus.MilvusVectorStore materializes it to a temporary file for pymilvus to verify against. Endpoints with publicly trusted certificates need no server_cert.

Parameters:

  • uri (str) –

    Milvus server endpoint. Must start with http:// (plaintext) or https:// (TLS), e.g. https://host:19530.

  • token (str | None, default: None ) –

    Authentication token ("user:password"). None for unauthenticated.

  • server_cert (str | None, default: None ) –

    PEM-encoded server/CA certificate used to verify a TLS connection. Required only for self-signed or private-CA endpoints; leave None when the server uses a publicly trusted certificate.

  • provider (str, default: "milvus" ) –

    Name of the provider used in the system.

Attributes:

  • env_vars (ClassVar[tuple[tuple[str, str], ...]]) –

    (name, description) pairs for the environment variables consulted by :meth:from_env. Exposed for documentation and notebook generation.

Raises:

  • ValueError –

    If uri is not an http:// or https:// URL.

Methods:
__post_init__
__post_init__() -> None

Reject any uri that is not an explicit Milvus server URL.

Guards against the footgun where an incorrect uri (a typo, a bare hostname, or a stray path) is silently interpreted by MilvusClient as a local Milvus Lite database file, creating a throwaway store instead of connecting to the intended server. Local, embedded use must go through :class:MilvusLiteConfig.

Source code in ai4rag/rag/vector_store/config.py
def __post_init__(self) -> None:
    """Reject any ``uri`` that is not an explicit Milvus server URL.

    Guards against the footgun where an incorrect ``uri`` (a typo, a bare
    hostname, or a stray path) is silently interpreted by ``MilvusClient`` as
    a local Milvus Lite database file, creating a throwaway store instead of
    connecting to the intended server. Local, embedded use must go through
    :class:`MilvusLiteConfig`.
    """
    if not _is_server_url(self.uri):
        raise ValueError(
            f"MilvusConfig.uri must be a Milvus server URL starting with 'http://' or 'https://', "
            f"got {self.uri!r}. For a local, embedded database use MilvusLiteConfig(db_path=...) "
            "(provider 'milvus_lite') instead."
        )
from_env classmethod
from_env() -> MilvusConfig

Build config from MILVUS_* environment variables.

Reads MILVUS_URI (required), plus the optional MILVUS_TOKEN and MILVUS_SERVER_CERT. MILVUS_SERVER_CERT holds the PEM certificate text itself, not a filesystem path.

Returns:

  • MilvusConfig –

    Config populated from the MILVUS_* environment variables.

Raises:

  • KeyError –

    If the required MILVUS_URI variable is not set.

  • ValueError –

    If MILVUS_URI is not an http:///https:// URL.

Source code in ai4rag/rag/vector_store/config.py
@classmethod
def from_env(cls) -> "MilvusConfig":
    """Build config from ``MILVUS_*`` environment variables.

    Reads ``MILVUS_URI`` (required), plus the optional ``MILVUS_TOKEN`` and
    ``MILVUS_SERVER_CERT``. ``MILVUS_SERVER_CERT`` holds the PEM certificate
    text itself, not a filesystem path.

    Returns
    -------
    MilvusConfig
        Config populated from the ``MILVUS_*`` environment variables.

    Raises
    ------
    KeyError
        If the required ``MILVUS_URI`` variable is not set.
    ValueError
        If ``MILVUS_URI`` is not an ``http://``/``https://`` URL.
    """
    return cls(
        uri=os.environ["MILVUS_URI"],
        token=os.environ.get("MILVUS_TOKEN"),
        server_cert=os.environ.get("MILVUS_SERVER_CERT"),
    )

MilvusLiteConfig dataclass

MilvusLiteConfig(*, provider: str = 'milvus_lite', db_path: str = DEFAULT_MILVUS_LITE_DB_PATH)

Bases: BaseVectorStoreConfig

Connection parameters for an embedded, local Milvus Lite database.

Milvus Lite is the zero-server Milvus engine bundled with pymilvus[milvus-lite]; it stores everything in a single local file and is the recommended lightweight option for local development, tests, and small-scale workloads (prototyping, up to roughly one million vectors) — not production serving. For a remote server use :class:MilvusConfig.

Choosing the embedded engine is explicit: it happens only when this config is used (provider "milvus_lite"), never as a silent fallback from a misconfigured :class:MilvusConfig.

Parameters:

  • db_path (str, default: :data:`DEFAULT_MILVUS_LITE_DB_PATH` ) –

    Local filesystem path to the Milvus Lite database file. Created on first use; a relative path resolves against the current working directory.

  • provider (str, default: "milvus_lite" ) –

    Name of the provider used in the system.

Attributes:

  • env_vars (ClassVar[tuple[tuple[str, str], ...]]) –

    (name, description) pairs for the environment variables consulted by :meth:from_env. Exposed for documentation and notebook generation.

Raises:

  • ValueError –

    If db_path is empty/blank, or looks like a server URL (http:///https://).

Methods:
__post_init__
__post_init__() -> None

Reject a db_path that is blank or is actually a server URL.

The symmetric guard to :meth:MilvusConfig.__post_init__: a value like https://host:19530 is a server endpoint, not a local database file, and belongs in :class:MilvusConfig. An empty or whitespace-only path is rejected here too, rather than being handed to MilvusClient where it would surface as an opaque, hard-to-trace pymilvus error.

Source code in ai4rag/rag/vector_store/config.py
def __post_init__(self) -> None:
    """Reject a ``db_path`` that is blank or is actually a server URL.

    The symmetric guard to :meth:`MilvusConfig.__post_init__`: a value like
    ``https://host:19530`` is a server endpoint, not a local database file,
    and belongs in :class:`MilvusConfig`. An empty or whitespace-only path
    is rejected here too, rather than being handed to ``MilvusClient`` where
    it would surface as an opaque, hard-to-trace pymilvus error.
    """
    if not isinstance(self.db_path, str) or not self.db_path.strip():
        raise ValueError(
            f"MilvusLiteConfig.db_path must be a non-empty local filesystem path, got {self.db_path!r}."
        )
    if _is_server_url(self.db_path):
        raise ValueError(
            f"MilvusLiteConfig.db_path must be a local filesystem path, not a server URL, "
            f"got {self.db_path!r}. For a remote Milvus server use MilvusConfig(uri=...) "
            "(provider 'milvus') instead."
        )
from_env classmethod
from_env() -> MilvusLiteConfig

Build config from the MILVUS_LITE_DB_PATH environment variable.

An unset variable falls back to :data:DEFAULT_MILVUS_LITE_DB_PATH.

Returns:

  • MilvusLiteConfig –

    Config populated from MILVUS_LITE_DB_PATH (or the default path).

Source code in ai4rag/rag/vector_store/config.py
@classmethod
def from_env(cls) -> "MilvusLiteConfig":
    """Build config from the ``MILVUS_LITE_DB_PATH`` environment variable.

    An unset variable falls back to :data:`DEFAULT_MILVUS_LITE_DB_PATH`.

    Returns
    -------
    MilvusLiteConfig
        Config populated from ``MILVUS_LITE_DB_PATH`` (or the default path).
    """
    return cls(db_path=os.environ.get("MILVUS_LITE_DB_PATH", DEFAULT_MILVUS_LITE_DB_PATH))

PGVectorConfig dataclass

PGVectorConfig(*, provider: str = 'pgvector', host: str = 'localhost', port: int = 5432, dbname: str = 'postgres', user: str = 'postgres', password: str | None = None, pool_max_size: int = 10)

Bases: BaseVectorStoreConfig

Connection parameters for a PostgreSQL + pgvector instance.

Parameters:

  • host (str, default: 'localhost' ) –

    PostgreSQL host address.

  • port (int, default: 5432 ) –

    PostgreSQL port.

  • dbname (str, default: 'postgres' ) –

    Database name.

  • user (str, default: 'postgres' ) –

    Database user.

  • password (str | None, default: None ) –

    Database password. None for trust/peer auth.

  • pool_max_size (int, default: 10 ) –

    Maximum number of concurrent connections the store's connection pool will open. The pool starts lean and grows lazily on demand, so this is a ceiling, not an eagerly-held count; it should be set to at least the maximum number of concurrent search()/add_documents() calls the caller will issue against this store, or those calls will queue for a slot and can eventually time out.

  • provider (str, default: "pgvector" ) –

    Name of the provider used in the system.

Attributes:

  • env_vars (ClassVar[tuple[tuple[str, str], ...]]) –

    (name, description) pairs for the environment variables consulted by :meth:from_env. Exposed for documentation and notebook generation.

Methods:
from_env classmethod
from_env() -> PGVectorConfig

Build config from PGVECTOR_* environment variables.

Reads PGVECTOR_HOST, PGVECTOR_PORT, PGVECTOR_DB, PGVECTOR_USER and PGVECTOR_PASSWORD. Unset variables fall back to the local-PostgreSQL defaults; PGVECTOR_PASSWORD defaults to None for trust/peer authentication.

Returns:

  • PGVectorConfig –

    Config populated from the PGVECTOR_* environment variables.

Source code in ai4rag/rag/vector_store/config.py
@classmethod
def from_env(cls) -> "PGVectorConfig":
    """Build config from ``PGVECTOR_*`` environment variables.

    Reads ``PGVECTOR_HOST``, ``PGVECTOR_PORT``, ``PGVECTOR_DB``,
    ``PGVECTOR_USER`` and ``PGVECTOR_PASSWORD``. Unset variables fall back to
    the local-PostgreSQL defaults; ``PGVECTOR_PASSWORD`` defaults to ``None``
    for trust/peer authentication.

    Returns
    -------
    PGVectorConfig
        Config populated from the ``PGVECTOR_*`` environment variables.
    """
    return cls(
        host=os.environ.get("PGVECTOR_HOST", "localhost"),
        port=int(os.environ.get("PGVECTOR_PORT", "5432")),
        dbname=os.environ.get("PGVECTOR_DB", "postgres"),
        user=os.environ.get("PGVECTOR_USER", "postgres"),
        password=os.environ.get("PGVECTOR_PASSWORD"),
    )

Functions:

get_vector_store_config

get_vector_store_config(provider: str) -> BaseVectorStoreConfig

Build a vector store config for provider from environment variables.

Companion to :func:ai4rag.rag.vector_store.get_vector_store.get_vector_store: given only a provider discriminator, it selects the matching config class and populates it from that backend's *_ENV variables via from_env. Keeping connection details in the environment means secrets never have to be embedded in generated artefacts (e.g. pattern notebooks).

Parameters:

  • provider (str) –

    Backend discriminator, one of "milvus", "milvus_lite" or "pgvector".

Returns:

  • BaseVectorStoreConfig –

    A config instance of the class matching provider, populated from the environment.

Raises:

  • ValueError –

    If provider names an unsupported backend.

  • KeyError –

    If a variable required by the selected backend's from_env is unset (e.g. MILVUS_URI for Milvus).

Examples:

>>> config = get_vector_store_config("milvus")  # reads MILVUS_URI, ...
>>> store = get_vector_store(embedding_model, config, collection_name="ai4rag_docs")
Source code in ai4rag/rag/vector_store/config.py
def get_vector_store_config(provider: str) -> BaseVectorStoreConfig:
    """Build a vector store config for *provider* from environment variables.

    Companion to :func:`ai4rag.rag.vector_store.get_vector_store.get_vector_store`:
    given only a provider discriminator, it selects the matching config class and
    populates it from that backend's ``*_ENV`` variables via ``from_env``. Keeping
    connection details in the environment means secrets never have to be embedded
    in generated artefacts (e.g. pattern notebooks).

    Parameters
    ----------
    provider : str
        Backend discriminator, one of ``"milvus"``, ``"milvus_lite"`` or
        ``"pgvector"``.

    Returns
    -------
    BaseVectorStoreConfig
        A config instance of the class matching *provider*, populated from the
        environment.

    Raises
    ------
    ValueError
        If *provider* names an unsupported backend.
    KeyError
        If a variable required by the selected backend's ``from_env`` is unset
        (e.g. ``MILVUS_URI`` for Milvus).

    Examples
    --------
    >>> config = get_vector_store_config("milvus")  # reads MILVUS_URI, ...
    >>> store = get_vector_store(embedding_model, config, collection_name="ai4rag_docs")
    """
    return _resolve_config_cls(provider).from_env()

get_vector_store_env_vars

get_vector_store_env_vars(provider: str) -> tuple[tuple[str, str], ...]

Return the environment variables consulted by provider's from_env.

Parameters:

  • provider (str) –

    Backend discriminator, one of "milvus", "milvus_lite" or "pgvector".

Returns:

  • tuple[tuple[str, str], ...] –

    (name, description) pairs, in the order they should be presented to a user. Descriptions note whether each variable is required or optional.

Raises:

  • ValueError –

    If provider names an unsupported backend.

Source code in ai4rag/rag/vector_store/config.py
def get_vector_store_env_vars(provider: str) -> tuple[tuple[str, str], ...]:
    """Return the environment variables consulted by *provider*'s ``from_env``.

    Parameters
    ----------
    provider : str
        Backend discriminator, one of ``"milvus"``, ``"milvus_lite"`` or
        ``"pgvector"``.

    Returns
    -------
    tuple[tuple[str, str], ...]
        ``(name, description)`` pairs, in the order they should be presented to
        a user. Descriptions note whether each variable is required or optional.

    Raises
    ------
    ValueError
        If *provider* names an unsupported backend.
    """
    return _resolve_config_cls(provider).env_vars

Store Selection

get_vector_store

Classes

Functions:

get_vector_store

get_vector_store(embedding_model: BaseEmbeddingModel, config: BaseVectorStoreConfig, collection_name: str | None = None) -> BaseVectorStore

Get vector store of desired type with chosen settings.

The backend is selected by config.provider, so the vector store type is fully determined by which config class is passed in — no separate type string is required.

Parameters:

  • embedding_model (BaseEmbeddingModel) –

    Embedding model used for embeddings creation.

  • config (MilvusConfig | MilvusLiteConfig | PGVectorConfig) –

    Connection config for the chosen backend. :class:MilvusConfig targets a remote Milvus server; :class:MilvusLiteConfig selects the embedded, local Milvus Lite engine.

  • collection_name (str | None, default: None ) –

    Name of an existing collection to reuse. When omitted, a new name is generated following the ai4rag naming convention (see :func:ai4rag.rag.vector_store.utils.generate_collection_name).

Returns:

Raises:

  • TypeError –

    If config is not the config class matching its provider (e.g. a provider="milvus" config that is not a :class:MilvusConfig).

  • ValueError –

    If config.provider names an unsupported backend.

Source code in ai4rag/rag/vector_store/get_vector_store.py
def get_vector_store(
    embedding_model: BaseEmbeddingModel,
    config: BaseVectorStoreConfig,
    collection_name: str | None = None,
) -> BaseVectorStore:
    """Get vector store of desired type with chosen settings.

    The backend is selected by ``config.provider``, so the vector store type
    is fully determined by which config class is passed in — no separate
    type string is required.

    Parameters
    ----------
    embedding_model : BaseEmbeddingModel
        Embedding model used for embeddings creation.

    config : MilvusConfig | MilvusLiteConfig | PGVectorConfig
        Connection config for the chosen backend. :class:`MilvusConfig` targets a
        remote Milvus server; :class:`MilvusLiteConfig` selects the embedded,
        local Milvus Lite engine.

    collection_name : str | None, default=None
        Name of an existing collection to reuse. When omitted, a new name
        is generated following the ai4rag naming convention (see
        :func:`ai4rag.rag.vector_store.utils.generate_collection_name`).

    Returns
    -------
    BaseVectorStore
        Instance of the vector store.

    Raises
    ------
    TypeError
        If ``config`` is not the config class matching its ``provider`` (e.g. a
        ``provider="milvus"`` config that is not a :class:`MilvusConfig`).
    ValueError
        If ``config.provider`` names an unsupported backend.
    """

    match config.provider:
        case "milvus":
            if not isinstance(config, MilvusConfig):
                raise TypeError("MilvusConfig is required when provider='milvus'.")

            from .milvus import MilvusVectorStore

            return MilvusVectorStore(
                embedding_model=embedding_model,
                config=config,
                collection_name=collection_name,
            )

        case "milvus_lite":
            if not isinstance(config, MilvusLiteConfig):
                raise TypeError("MilvusLiteConfig is required when provider='milvus_lite'.")

            from .milvus import MilvusVectorStore

            return MilvusVectorStore(
                embedding_model=embedding_model,
                config=config,
                collection_name=collection_name,
            )

        case "pgvector":
            if not isinstance(config, PGVectorConfig):
                raise TypeError("PGVectorConfig is required when provider='pgvector'.")

            from .pgvector import PGVectorStore

            return PGVectorStore(
                embedding_model=embedding_model,
                config=config,
                collection_name=collection_name,
            )

        case _:
            raise ValueError(f"Vector store provider '{config.provider}' is not supported.")

Milvus

milvus

Classes

MilvusVectorStore

MilvusVectorStore(embedding_model: BaseEmbeddingModel, config: MilvusConfig | MilvusLiteConfig, distance_metric: str = 'cosine', collection_name: str | None = None)

Bases: BaseVectorStore

Vector store backed by Milvus via pymilvus (remote server or Milvus Lite).

A single store class serves both deployment styles, selected by the config type: :class:~ai4rag.rag.vector_store.config.MilvusConfig connects to a remote server, while :class:~ai4rag.rag.vector_store.config.MilvusLiteConfig opens the embedded, local Milvus Lite engine. Both support pure vector search and hybrid search (dense + BM25 sparse) with RRF or weighted reranking, using Milvus native server-side fusion.

Parameters:

  • embedding_model (BaseEmbeddingModel) –

    Model used to embed documents and queries.

  • config (MilvusConfig | MilvusLiteConfig) –

    Connection parameters. A :class:MilvusConfig connects to a remote server (TLS via an https:// URI; config.server_cert supplies a self-signed CA certificate, materialized to a temporary file and passed to MilvusClient as server_pem_path). A :class:MilvusLiteConfig opens the embedded engine backed by its local db_path.

  • distance_metric (str, default: 'cosine' ) –

    Distance metric for vector similarity (default "cosine").

  • collection_name (str | None, default: None ) –

    Existing collection to reuse; must start with the ai4rag prefix. When omitted, a new compliant name is generated (see :func:ai4rag.rag.vector_store.utils.resolve_collection_name).

The MilvusClient is built according to the config type: a :class:MilvusLiteConfig opens the embedded engine at its local db_path; a :class:MilvusConfig connects to a remote server and, when config.server_cert is set, materializes its PEM text to a temporary file (see :func:_materialize_server_cert) passed as server_pem_path for TLS verification. The target collection — with its dense, sparse/BM25, and JSON fields — is created only when it does not already exist.

Parameters:

  • embedding_model (BaseEmbeddingModel) –

    Model used to embed documents and queries.

  • config (MilvusConfig | MilvusLiteConfig) –

    Connection parameters for a remote Milvus server or the embedded Milvus Lite engine.

  • distance_metric (str, default: "cosine" ) –

    Distance metric used for dense vector similarity.

  • collection_name (str | None, default: None ) –

    Existing collection to reuse; must start with the ai4rag prefix. When omitted, a new compliant name is generated.

Source code in ai4rag/rag/vector_store/milvus.py
def __init__(
    self,
    embedding_model: BaseEmbeddingModel,
    config: MilvusConfig | MilvusLiteConfig,
    distance_metric: str = "cosine",
    collection_name: str | None = None,
):
    """Initialize the store, open a client, and ensure the collection exists.

    The ``MilvusClient`` is built according to the config type: a
    :class:`MilvusLiteConfig` opens the embedded engine at its local
    ``db_path``; a :class:`MilvusConfig` connects to a remote server and, when
    ``config.server_cert`` is set, materializes its PEM text to a temporary
    file (see :func:`_materialize_server_cert`) passed as ``server_pem_path``
    for TLS verification. The target collection — with its dense, sparse/BM25,
    and JSON fields — is created only when it does not already exist.

    Parameters
    ----------
    embedding_model : BaseEmbeddingModel
        Model used to embed documents and queries.
    config : MilvusConfig | MilvusLiteConfig
        Connection parameters for a remote Milvus server or the embedded
        Milvus Lite engine.
    distance_metric : str, default="cosine"
        Distance metric used for dense vector similarity.
    collection_name : str | None, default=None
        Existing collection to reuse; must start with the ``ai4rag`` prefix.
        When omitted, a new compliant name is generated.
    """
    super().__init__(embedding_model, config, distance_metric, collection_name)
    self._embedding_dimension = resolve_embedding_dimension(self.embedding_model)

    self._client = MilvusClient(**self._build_connect_kwargs(config))

    if not self._client.has_collection(self._collection_name):
        self._create_collection()
Methods:
search
search(query: str, k: int = 5, include_scores: bool = False, search_mode: str = 'vector', ranker_strategy: str | None = None, ranker_k: int | None = None, ranker_alpha: float | None = None, **kwargs) -> list[AI4RAGChunk] | list[tuple[AI4RAGChunk, float]]

Search for chunks relevant to query.

Parameters:

  • query (str) –

    Search query text.

  • k (int, default: 5 ) –

    Number of results to return.

  • include_scores (bool, default: False ) –

    Whether to include similarity scores in the results.

  • search_mode (str, default: "vector" ) –

    "vector" for dense-only search or "hybrid" for dense + BM25 sparse search.

  • ranker_strategy (str | None, default: None ) –

    Hybrid ranker: "rrf", "weighted", or "normalized".

  • ranker_k (int | None, default: None ) –

    RRF smoothing constant (k).

  • ranker_alpha (float | None, default: None ) –

    Weighted blend factor (0 = keyword, 1 = vector).

  • **kwargs (Any, default: {} ) –

    Accepted for interface compatibility; ignored by this backend.

Returns:

  • list[AI4RAGChunk] | list[tuple[AI4RAGChunk, float]] –

    Matched chunks, optionally paired with their scores.

Source code in ai4rag/rag/vector_store/milvus.py
def search(
    self,
    query: str,
    k: int = 5,
    include_scores: bool = False,
    search_mode: str = "vector",
    ranker_strategy: str | None = None,
    ranker_k: int | None = None,
    ranker_alpha: float | None = None,
    **kwargs,
) -> list[AI4RAGChunk] | list[tuple[AI4RAGChunk, float]]:
    """Search for chunks relevant to *query*.

    Parameters
    ----------
    query : str
        Search query text.
    k : int, default=5
        Number of results to return.
    include_scores : bool, default=False
        Whether to include similarity scores in the results.
    search_mode : str, default="vector"
        ``"vector"`` for dense-only search or ``"hybrid"`` for dense + BM25
        sparse search.
    ranker_strategy : str | None, default=None
        Hybrid ranker: ``"rrf"``, ``"weighted"``, or ``"normalized"``.
    ranker_k : int | None, default=None
        RRF smoothing constant (``k``).
    ranker_alpha : float | None, default=None
        Weighted blend factor (``0`` = keyword, ``1`` = vector).
    **kwargs : Any
        Accepted for interface compatibility; ignored by this backend.

    Returns
    -------
    list[AI4RAGChunk] | list[tuple[AI4RAGChunk, float]]
        Matched chunks, optionally paired with their scores.
    """
    validate_search_params(search_mode, ranker_strategy, ranker_k, ranker_alpha)

    if search_mode == "hybrid":
        return self._search_hybrid(query, k, include_scores, ranker_strategy, ranker_k, ranker_alpha)
    return self._search_vector(query, k, include_scores)
add_documents
add_documents(documents: list[AI4RAGChunk], **kwargs) -> None

Embed, deduplicate, and upsert chunks into Milvus.

Duplicate chunk_id values within documents are skipped (first occurrence wins) and logged. Rows are upserted in batches.

Parameters:

  • documents (list[AI4RAGChunk]) –

    Chunks to be embedded and stored.

  • **kwargs (Any, default: {} ) –

    Optional overrides. batch_size (int) sets the upsert batch size (default :attr:_BATCH_SIZE).

Source code in ai4rag/rag/vector_store/milvus.py
def add_documents(self, documents: list[AI4RAGChunk], **kwargs) -> None:
    """Embed, deduplicate, and upsert chunks into Milvus.

    Duplicate ``chunk_id`` values within *documents* are skipped (first
    occurrence wins) and logged. Rows are upserted in batches.

    Parameters
    ----------
    documents : list[AI4RAGChunk]
        Chunks to be embedded and stored.
    **kwargs : Any
        Optional overrides. ``batch_size`` (int) sets the upsert batch size
        (default :attr:`_BATCH_SIZE`).
    """
    if not documents:
        return

    embeddings = self.embedding_model.embed_documents([doc.text for doc in documents])

    data: list[dict[str, Any]] = []
    for doc, embedding in iter_unique_chunks(documents, embeddings):
        data.append(
            {
                "chunk_id": doc.chunk_id,
                "content": doc.text,
                "vector": embedding,
                "metadata": doc.metadata,
            }
        )

    batch_size = kwargs.get("batch_size", self._BATCH_SIZE)
    for idx in range(0, len(data), batch_size):
        self._client.upsert(self._collection_name, data=data[idx : idx + batch_size])
clean_collection
clean_collection() -> None

Drop the Milvus collection.

Source code in ai4rag/rag/vector_store/milvus.py
def clean_collection(self) -> None:
    """Drop the Milvus collection."""
    self._client.drop_collection(self._collection_name)
close
close() -> None

Close the underlying Milvus client connection.

The temporary TLS certificate file (when one was materialized) is intentionally not removed here: pymilvus can reconnect an idle channel from a background thread and re-read server_pem_path after close(), so the file is kept for the process lifetime and cleaned at interpreter exit by :func:_cleanup_server_certs.

Source code in ai4rag/rag/vector_store/milvus.py
def close(self) -> None:
    """Close the underlying Milvus client connection.

    The temporary TLS certificate file (when one was materialized) is
    intentionally *not* removed here: pymilvus can reconnect an idle channel
    from a background thread and re-read ``server_pem_path`` after ``close()``,
    so the file is kept for the process lifetime and cleaned at interpreter
    exit by :func:`_cleanup_server_certs`.
    """
    self._client.close()

Functions:

PGVector

pgvector

Classes

PGVectorStore

PGVectorStore(embedding_model: BaseEmbeddingModel, config: PGVectorConfig, distance_metric: str = 'cosine', collection_name: str | None = None)

Bases: BaseVectorStore

Vector store backed by PostgreSQL with the pgvector extension.

Supports pure vector search and hybrid search (dense vector + tsvector full-text) with RRF or weighted reranking via in-memory fusion.

Driven by asyncpg rather than psycopg: asyncpg speaks the Postgres wire protocol itself instead of wrapping the libpq C library, so it ships as a normal self-contained wheel with no system libpq dependency and none of psycopg[binary]'s bundled-OpenSSL conflicts. Every public method here stays synchronous (matching :class:BaseVectorStore and every other backend) by dispatching onto one dedicated background event loop — see :meth:_run — so callers never need to know asyncpg is involved.

Parameters:

  • embedding_model (BaseEmbeddingModel) –

    Model used to embed documents and queries.

  • config (PGVectorConfig) –

    Connection parameters for the PostgreSQL server.

  • distance_metric (str, default: 'cosine' ) –

    Distance metric (default "cosine"). One of "cosine", "l2", "l1", "inner_product".

  • collection_name (str | None, default: None ) –

    Existing collection to reuse; must start with the ai4rag prefix. The name is used verbatim as the PostgreSQL table name. When omitted, a new compliant name is generated (see :func:ai4rag.rag.vector_store.utils.resolve_collection_name).

Resolves the distance metric to its pgvector operator and index opclass. The connection pool and backing table are created lazily on the first DB access (see :meth:_ensure_db) so that :meth:add_documents can embed documents — the slow step — before any idle connection is opened. HNSW and GIN indexes are deferred further, to the first search (see :meth:_ensure_indexes), avoiding per-row HNSW maintenance during bulk inserts.

Parameters:

  • embedding_model (BaseEmbeddingModel) –

    Model used to embed documents and queries.

  • config (PGVectorConfig) –

    Connection parameters for the PostgreSQL server.

  • distance_metric (str, default: "cosine" ) –

    Distance metric. One of "cosine", "l2", "l1", "inner_product".

  • collection_name (str | None, default: None ) –

    Existing collection to reuse; must start with the ai4rag prefix and is used verbatim as the table name. When omitted, a new compliant name is generated.

Raises:

  • ValueError –

    If distance_metric is not one of the supported metrics.

Source code in ai4rag/rag/vector_store/pgvector.py
def __init__(
    self,
    embedding_model: BaseEmbeddingModel,
    config: PGVectorConfig,
    distance_metric: str = "cosine",
    collection_name: str | None = None,
):
    """Validate parameters and prepare the store for use.

    Resolves the distance metric to its pgvector operator and index opclass.
    The connection pool and backing table are created lazily on the first DB
    access (see :meth:`_ensure_db`) so that :meth:`add_documents` can embed
    documents — the slow step — before any idle connection is opened.
    HNSW and GIN indexes are deferred further, to the first search (see
    :meth:`_ensure_indexes`), avoiding per-row HNSW maintenance during bulk
    inserts.

    Parameters
    ----------
    embedding_model : BaseEmbeddingModel
        Model used to embed documents and queries.
    config : PGVectorConfig
        Connection parameters for the PostgreSQL server.
    distance_metric : str, default="cosine"
        Distance metric. One of ``"cosine"``, ``"l2"``, ``"l1"``,
        ``"inner_product"``.
    collection_name : str | None, default=None
        Existing collection to reuse; must start with the ``ai4rag`` prefix
        and is used verbatim as the table name. When omitted, a new compliant
        name is generated.

    Raises
    ------
    ValueError
        If ``distance_metric`` is not one of the supported metrics.
    """
    super().__init__(embedding_model, config, distance_metric, collection_name)
    self._embedding_dimension = resolve_embedding_dimension(self.embedding_model)
    if self._embedding_dimension > self._MAX_INDEXABLE_DIMENSION:
        logger.warning(
            "Embedding dimension %d exceeds pgvector's %d-dimension limit for HNSW "
            "indexes; searches on collection %r will use an exact sequential scan "
            "instead of an approximate nearest-neighbor index.",
            self._embedding_dimension,
            self._MAX_INDEXABLE_DIMENSION,
            self._collection_name,
        )

    distance_key = distance_metric.lower()
    if distance_key not in self._DISTANCE_METRIC_TO_OPERATOR:
        raise ValueError(
            f"Unsupported distance metric '{distance_metric}'. "
            f"Must be one of {list(self._DISTANCE_METRIC_TO_OPERATOR)}."
        )
    self._distance_key = distance_key
    self._distance_operator = self._DISTANCE_METRIC_TO_OPERATOR[distance_key]
    self._index_ops = self._DISTANCE_METRIC_TO_INDEX_OPS[distance_key]

    # Indexes are built lazily after documents are loaded (see ``_ensure_indexes``),
    # not at connection time: maintaining an HNSW graph on every insert is the
    # memory-heavy path that can trigger the server-side OOM killer on large batches.
    # search() is called concurrently across threads (see config.pool_max_size above),
    # so the flag guarding this one-time DDL needs a lock, not just a bare check.
    self._indexes_built = False
    self._indexes_lock = threading.Lock()

    # Pool and table are created lazily on first DB access so that
    # add_documents() can embed documents (the slow step) before any
    # connection is opened. _db_lock guards one-time loop/pool/table init.
    self._db: _LoopBoundPool | None = None
    self._table_created: bool = False
    self._db_lock = threading.Lock()

    # Guards the handoff between _run() (reader) and close() (writer): close()
    # must not stop/close the loop while another thread's _run() call is still
    # dispatched on it. See _run() and close() for the drain protocol this
    # implements.
    self._inflight = _InFlightTracker()
Methods:
search
search(query: str, k: int, include_scores: bool = False, search_mode: str = 'vector', ranker_strategy: str | None = None, ranker_k: int | None = None, ranker_alpha: float | None = None, **kwargs) -> list[AI4RAGChunk] | list[tuple[AI4RAGChunk, float]]

Search for chunks relevant to query.

Parameters:

  • query (str) –

    Search query text.

  • k (int) –

    Number of results to return.

  • include_scores (bool, default: False ) –

    Whether to include similarity scores.

  • search_mode (str, default: "vector" ) –

    "vector" for dense-only search or "hybrid" for dense + full-text search.

  • ranker_strategy (str | None, default: None ) –

    Hybrid ranker: "rrf", "weighted", or "normalized".

  • ranker_k (int | None, default: None ) –

    RRF smoothing constant (k).

  • ranker_alpha (float | None, default: None ) –

    Weighted blend factor (0 = keyword, 1 = vector).

  • **kwargs (Any, default: {} ) –

    Accepted for interface compatibility; ignored by this backend.

Returns:

  • list[AI4RAGChunk] | list[tuple[AI4RAGChunk, float]] –

    Matched chunks, optionally paired with their scores.

Source code in ai4rag/rag/vector_store/pgvector.py
def search(
    self,
    query: str,
    k: int,
    include_scores: bool = False,
    search_mode: str = "vector",
    ranker_strategy: str | None = None,
    ranker_k: int | None = None,
    ranker_alpha: float | None = None,
    **kwargs,
) -> list[AI4RAGChunk] | list[tuple[AI4RAGChunk, float]]:
    """Search for chunks relevant to *query*.

    Parameters
    ----------
    query : str
        Search query text.
    k : int
        Number of results to return.
    include_scores : bool, default=False
        Whether to include similarity scores.
    search_mode : str, default="vector"
        ``"vector"`` for dense-only search or ``"hybrid"`` for dense +
        full-text search.
    ranker_strategy : str | None, default=None
        Hybrid ranker: ``"rrf"``, ``"weighted"``, or ``"normalized"``.
    ranker_k : int | None, default=None
        RRF smoothing constant (``k``).
    ranker_alpha : float | None, default=None
        Weighted blend factor (``0`` = keyword, ``1`` = vector).
    **kwargs : Any
        Accepted for interface compatibility; ignored by this backend.

    Returns
    -------
    list[AI4RAGChunk] | list[tuple[AI4RAGChunk, float]]
        Matched chunks, optionally paired with their scores.
    """
    validate_search_params(search_mode, ranker_strategy, ranker_k, ranker_alpha)
    self._ensure_indexes()

    if search_mode == "hybrid":
        return self._search_hybrid(query, k, include_scores, ranker_strategy, ranker_k, ranker_alpha)
    return self._search_vector(query, k, include_scores)
add_documents
add_documents(documents: list[AI4RAGChunk], **kwargs) -> None

Embed, deduplicate, and upsert chunks into PGVector.

Duplicate chunk_id values within documents are skipped (first occurrence wins) and logged. Rows are upserted in batches, each with a one-shot retry on a dropped connection.

Parameters:

  • documents (list[AI4RAGChunk]) –

    Chunks to be embedded and stored.

  • **kwargs (Any, default: {} ) –

    Optional overrides. batch_size (int) sets the insert batch size (default :attr:_BATCH_SIZE).

Source code in ai4rag/rag/vector_store/pgvector.py
def add_documents(self, documents: list[AI4RAGChunk], **kwargs) -> None:
    """Embed, deduplicate, and upsert chunks into PGVector.

    Duplicate ``chunk_id`` values within *documents* are skipped (first
    occurrence wins) and logged. Rows are upserted in batches, each with a
    one-shot retry on a dropped connection.

    Parameters
    ----------
    documents : list[AI4RAGChunk]
        Chunks to be embedded and stored.
    **kwargs : Any
        Optional overrides. ``batch_size`` (int) sets the insert batch size
        (default :attr:`_BATCH_SIZE`).
    """
    if not documents:
        return

    # embed_documents() is a synchronous, network-bound call: it runs here, on
    # the caller's own thread, before anything is dispatched to the shared
    # event loop (see the matching note in _search_vector).
    embeddings = self.embedding_model.embed_documents([doc.text for doc in documents])
    pool = self._ensure_db()

    values: list[tuple[str, dict, list[float], str, str]] = []
    for doc, embedding in iter_unique_chunks(documents, embeddings):
        values.append((doc.chunk_id, doc.metadata, embedding, doc.text, doc.text))

    batch_size = kwargs.get("batch_size", self._BATCH_SIZE)
    for idx in range(0, len(values), batch_size):
        self._insert_batch_with_retry(pool, values[idx : idx + batch_size])
clean_collection
clean_collection() -> None

Drop the PostgreSQL table.

Source code in ai4rag/rag/vector_store/pgvector.py
def clean_collection(self) -> None:
    """Drop the PostgreSQL table."""
    pool = self._ensure_pool()
    self._run_with_retry(lambda: self._drop_table(pool))
close
close() -> None

Close the connection pool and stop the store's background event loop.

Idempotent: a second call sees :attr:_db already None and returns immediately, rather than dispatching onto a loop that has already stopped — which would hang forever waiting for a callback the stopped loop can never run.

Safe to call while another thread is mid-:meth:_run: :attr:_db is cleared up front (under :attr:_inflight) so no new call can start, but the loop and pool are only stopped once every call that already started has finished — see :meth:_run. A call that started before this method clears :attr:_db still completes normally; one that starts after gets a clear RuntimeError instead of racing the teardown.

Source code in ai4rag/rag/vector_store/pgvector.py
def close(self) -> None:
    """Close the connection pool and stop the store's background event loop.

    Idempotent: a second call sees :attr:`_db` already ``None`` and returns
    immediately, rather than dispatching onto a loop that has already
    stopped — which would hang forever waiting for a callback the stopped
    loop can never run.

    Safe to call while another thread is mid-:meth:`_run`: :attr:`_db` is
    cleared up front (under :attr:`_inflight`) so no *new* call can start,
    but the loop and pool are only stopped once every call that already
    started has finished — see :meth:`_run`. A call that started before
    this method clears :attr:`_db` still completes normally; one that
    starts after gets a clear ``RuntimeError`` instead of racing the teardown.
    """
    with self._inflight.cond:
        if self._db is None:
            return
        db = self._db
        self._db = None
        while self._inflight.count > 0:
            self._inflight.cond.wait()
    self._dispatch(db.loop, db.pool.close())
    db.loop.call_soon_threadsafe(db.loop.stop)
    db.thread.join()
    db.loop.close()

Functions:

Hybrid Search Reranking

Milvus fuses dense and sparse results server-side, while PGVector combines dense similarity with PostgreSQL full-text search in memory using the reranker below.

reranker

Classes

WeightedInMemoryAggregator

Combines vector and keyword search scores using reranking strategies.

Methods:
weighted_rerank staticmethod
weighted_rerank(vector_scores: dict[str, float], keyword_scores: dict[str, float], alpha: float = 0.5) -> dict[str, float]

Weighted average of normalized vector and keyword scores.

Parameters:

  • vector_scores (dict[str, float]) –

    Scores from vector search keyed by chunk id.

  • keyword_scores (dict[str, float]) –

    Scores from keyword search keyed by chunk id.

  • alpha (float, default: 0.5 ) –

    Blend factor: 0 = keyword only, 1 = vector only.

Returns:

  • dict[str, float] –

    Combined scores keyed by chunk id.

Source code in ai4rag/rag/vector_store/reranker.py
@staticmethod
def weighted_rerank(
    vector_scores: dict[str, float],
    keyword_scores: dict[str, float],
    alpha: float = 0.5,
) -> dict[str, float]:
    """Weighted average of normalized vector and keyword scores.

    Parameters
    ----------
    vector_scores : dict[str, float]
        Scores from vector search keyed by chunk id.
    keyword_scores : dict[str, float]
        Scores from keyword search keyed by chunk id.
    alpha : float
        Blend factor: ``0`` = keyword only, ``1`` = vector only.

    Returns
    -------
    dict[str, float]
        Combined scores keyed by chunk id.
    """
    all_ids = set(vector_scores) | set(keyword_scores)
    norm_vec = WeightedInMemoryAggregator._normalize_scores(vector_scores)
    norm_kw = WeightedInMemoryAggregator._normalize_scores(keyword_scores)
    return {
        doc_id: (1 - alpha) * norm_kw.get(doc_id, 0.0) + alpha * norm_vec.get(doc_id, 0.0) for doc_id in all_ids
    }
rrf_rerank staticmethod
rrf_rerank(vector_scores: dict[str, float], keyword_scores: dict[str, float], k: float = 60.0) -> dict[str, float]

Reciprocal Rank Fusion of vector and keyword search results.

Parameters:

  • vector_scores (dict[str, float]) –

    Scores from vector search keyed by chunk id.

  • keyword_scores (dict[str, float]) –

    Scores from keyword search keyed by chunk id.

  • k (float, default: 60.0 ) –

    RRF smoothing constant (default 60.0).

Returns:

  • dict[str, float] –

    Fused RRF scores keyed by chunk id.

Source code in ai4rag/rag/vector_store/reranker.py
@staticmethod
def rrf_rerank(
    vector_scores: dict[str, float],
    keyword_scores: dict[str, float],
    k: float = 60.0,
) -> dict[str, float]:
    """Reciprocal Rank Fusion of vector and keyword search results.

    Parameters
    ----------
    vector_scores : dict[str, float]
        Scores from vector search keyed by chunk id.
    keyword_scores : dict[str, float]
        Scores from keyword search keyed by chunk id.
    k : float
        RRF smoothing constant (default ``60.0``).

    Returns
    -------
    dict[str, float]
        Fused RRF scores keyed by chunk id.
    """
    vector_ranks = {
        doc_id: i + 1
        for i, (doc_id, _) in enumerate(sorted(vector_scores.items(), key=lambda x: x[1], reverse=True))
    }
    keyword_ranks = {
        doc_id: i + 1
        for i, (doc_id, _) in enumerate(sorted(keyword_scores.items(), key=lambda x: x[1], reverse=True))
    }

    all_ids = set(vector_scores) | set(keyword_scores)
    return {
        doc_id: 1.0 / (k + vector_ranks.get(doc_id, float("inf")))
        + 1.0 / (k + keyword_ranks.get(doc_id, float("inf")))
        for doc_id in all_ids
    }
combine_search_results staticmethod
combine_search_results(vector_scores: dict[str, float], keyword_scores: dict[str, float], reranker_type: str = 'rrf', reranker_params: dict[str, Any] | None = None) -> dict[str, float]

Dispatch to the appropriate reranking strategy.

Parameters:

  • vector_scores (dict[str, float]) –

    Scores from vector search keyed by chunk id.

  • keyword_scores (dict[str, float]) –

    Scores from keyword search keyed by chunk id.

  • reranker_type (str, default: 'rrf' ) –

    "rrf", "weighted", or "normalized" (falls through to RRF).

  • reranker_params (dict[str, Any] | None, default: None ) –

    Strategy-specific params: {"k": float} for RRF, {"alpha": float} for weighted.

Returns:

  • dict[str, float] –

    Combined scores keyed by chunk id, produced by the selected strategy.

Source code in ai4rag/rag/vector_store/reranker.py
@staticmethod
def combine_search_results(
    vector_scores: dict[str, float],
    keyword_scores: dict[str, float],
    reranker_type: str = "rrf",
    reranker_params: dict[str, Any] | None = None,
) -> dict[str, float]:
    """Dispatch to the appropriate reranking strategy.

    Parameters
    ----------
    vector_scores : dict[str, float]
        Scores from vector search keyed by chunk id.
    keyword_scores : dict[str, float]
        Scores from keyword search keyed by chunk id.
    reranker_type : str
        ``"rrf"``, ``"weighted"``, or ``"normalized"`` (falls through to RRF).
    reranker_params : dict[str, Any] | None
        Strategy-specific params: ``{"k": float}`` for RRF,
        ``{"alpha": float}`` for weighted.

    Returns
    -------
    dict[str, float]
        Combined scores keyed by chunk id, produced by the selected strategy.
    """
    if reranker_params is None:
        reranker_params = {}

    if reranker_type == "weighted":
        alpha = reranker_params.get("alpha", 0.5)
        return WeightedInMemoryAggregator.weighted_rerank(vector_scores, keyword_scores, alpha)

    k = reranker_params.get("k", 60.0)
    return WeightedInMemoryAggregator.rrf_rerank(vector_scores, keyword_scores, k)

Collection Naming & Search Utilities

Collections follow the ai4rag_<timestamp>_<suffix> convention and are capped at 63 characters. Pass an existing name via collection_name to reuse a collection.

utils

Functions:

generate_collection_name

generate_collection_name() -> str

Generate a unique vector store collection name.

Follows the convention <prefix>_<UTC timestamp>_<8 random chars>, e.g. ai4rag_20260728153000_zxcvbnml.

Returns:

  • str –

    A unique, convention-following collection name.

Source code in ai4rag/rag/vector_store/utils.py
def generate_collection_name() -> str:
    """Generate a unique vector store collection name.

    Follows the convention ``<prefix>_<UTC timestamp>_<8 random chars>``,
    e.g. ``ai4rag_20260728153000_zxcvbnml``.

    Returns
    -------
    str
        A unique, convention-following collection name.
    """
    timestamp = datetime.now(tz=timezone.utc).strftime("%Y%m%d%H%M%S")
    suffix = "".join(secrets.choice(_COLLECTION_NAME_SUFFIX_ALPHABET) for _ in range(_COLLECTION_NAME_SUFFIX_LENGTH))
    return f"{COLLECTION_NAME_PREFIX}_{timestamp}_{suffix}"

resolve_collection_name

resolve_collection_name(collection_name: str | None) -> str

Resolve, validate, and sanitize a vector store collection name.

Single entry point every backend uses to turn the optional caller-supplied collection_name into the concrete name it will create or reuse. It enforces the invariants that keep ai4rag stores safe and portable:

  • Auto-generation — when collection_name is None a fresh, convention-following name is generated (see :func:generate_collection_name).
  • Namespace guard — a caller-supplied name must start with :data:COLLECTION_NAME_PREFIX. This is the isolation boundary guaranteeing ai4rag only ever touches its own tables/collections; a non-compliant name is rejected rather than silently coerced, so mistakes surface immediately.
  • Identifier safety — the name is sanitized into a valid identifier (see :func:sanitize_collection_name) and bounded to :data:_MAX_COLLECTION_NAME_LENGTH, so it is usable verbatim as a backend collection and as a physical SQL table name.

Parameters:

  • collection_name (str | None) –

    Existing collection name to reuse, or None to generate a new one.

Returns:

  • str –

    The resolved, sanitized collection name.

Raises:

  • ValueError –

    If collection_name does not start with :data:COLLECTION_NAME_PREFIX, or if it exceeds :data:_MAX_COLLECTION_NAME_LENGTH characters.

Source code in ai4rag/rag/vector_store/utils.py
def resolve_collection_name(collection_name: str | None) -> str:
    """Resolve, validate, and sanitize a vector store collection name.

    Single entry point every backend uses to turn the optional caller-supplied
    ``collection_name`` into the concrete name it will create or reuse. It
    enforces the invariants that keep ai4rag stores safe and portable:

    * **Auto-generation** — when ``collection_name`` is ``None`` a fresh,
      convention-following name is generated (see :func:`generate_collection_name`).
    * **Namespace guard** — a caller-supplied name must start with
      :data:`COLLECTION_NAME_PREFIX`. This is the isolation boundary guaranteeing
      ai4rag only ever touches its own tables/collections; a non-compliant name
      is rejected rather than silently coerced, so mistakes surface immediately.
    * **Identifier safety** — the name is sanitized into a valid identifier
      (see :func:`sanitize_collection_name`) and bounded to
      :data:`_MAX_COLLECTION_NAME_LENGTH`, so it is usable verbatim as a backend
      collection *and* as a physical SQL table name.

    Parameters
    ----------
    collection_name : str | None
        Existing collection name to reuse, or ``None`` to generate a new one.

    Returns
    -------
    str
        The resolved, sanitized collection name.

    Raises
    ------
    ValueError
        If ``collection_name`` does not start with :data:`COLLECTION_NAME_PREFIX`,
        or if it exceeds :data:`_MAX_COLLECTION_NAME_LENGTH` characters.
    """
    if collection_name is None:
        return generate_collection_name()

    if not collection_name.startswith(COLLECTION_NAME_PREFIX):
        raise ValueError(
            f"Collection name {collection_name!r} must start with '{COLLECTION_NAME_PREFIX}'. "
            "This prefix namespaces ai4rag-managed collections so the store never "
            "reuses or drops data it does not own."
        )

    sanitized = sanitize_collection_name(collection_name)
    if len(sanitized) > _MAX_COLLECTION_NAME_LENGTH:
        raise ValueError(
            f"Collection name {sanitized!r} exceeds the maximum length of " f"{_MAX_COLLECTION_NAME_LENGTH} characters."
        )
    return sanitized

sanitize_collection_name

sanitize_collection_name(name: str) -> str

Coerce a name into a valid identifier for every supported backend.

Replaces non-alphanumeric characters (except underscores) with underscores, so the result is usable verbatim as both a backend collection name and a physical SQL table name.

Parameters:

  • name (str) –

    Raw collection name to sanitize.

Returns:

  • str –

    The sanitized, identifier-safe collection name.

Source code in ai4rag/rag/vector_store/utils.py
def sanitize_collection_name(name: str) -> str:
    """Coerce a name into a valid identifier for every supported backend.

    Replaces non-alphanumeric characters (except underscores) with underscores,
    so the result is usable verbatim as both a backend collection name and a
    physical SQL table name.

    Parameters
    ----------
    name : str
        Raw collection name to sanitize.

    Returns
    -------
    str
        The sanitized, identifier-safe collection name.
    """
    return re.sub(r"[^a-zA-Z0-9_]", "_", name)

validate_search_params

validate_search_params(search_mode: str, ranker_strategy: str | None, ranker_k: int | None, ranker_alpha: float | None) -> None

Validate the search mode and hybrid ranker parameter combination.

Backend-agnostic guard shared by every hybrid-capable vector store (e.g. Milvus, PGVector): it enforces that ranker parameters are only supplied for a hybrid search and that each is paired with its matching strategy, before any backend-specific query is issued.

Parameters:

  • search_mode (str) –

    How the search should be conducted. "vector" for dense embedding search only, or "hybrid" for both sparse & dense (hybrid) search.

  • ranker_strategy (str | None) –

    Reranking strategy (function) used with hybrid search. One of "rrf", "weighted", or "normalized". Must be unset for non-hybrid search.

  • ranker_k (int | None) –

    The smoothing constant in Reciprocal Rank Fusion (RRF). Valid only with ranker_strategy="rrf".

  • ranker_alpha (float | None) –

    Weighting coefficient that determines how much the system trusts semantic (vector) search versus lexical (keyword/BM25) search. Valid only with ranker_strategy="weighted".

Raises:

  • ValueError –

    If search_mode is unknown, if any ranker parameter is supplied for a non-hybrid search, if ranker_strategy is missing or invalid for a hybrid search, or if ranker_k/ranker_alpha are paired with the wrong strategy.

Source code in ai4rag/rag/vector_store/utils.py
def validate_search_params(
    search_mode: str,
    ranker_strategy: str | None,
    ranker_k: int | None,
    ranker_alpha: float | None,
) -> None:
    """Validate the search mode and hybrid ranker parameter combination.

    Backend-agnostic guard shared by every hybrid-capable vector store (e.g.
    Milvus, PGVector): it enforces that ranker parameters are only supplied for a
    hybrid search and that each is paired with its matching strategy, before any
    backend-specific query is issued.

    Parameters
    ----------
    search_mode : str
        How the search should be conducted. ``"vector"`` for dense embedding
        search only, or ``"hybrid"`` for both sparse & dense (hybrid) search.
    ranker_strategy : str | None
        Reranking strategy (function) used with hybrid search. One of
        ``"rrf"``, ``"weighted"``, or ``"normalized"``. Must be unset for
        non-hybrid search.
    ranker_k : int | None
        The smoothing constant in Reciprocal Rank Fusion (RRF). Valid only
        with ``ranker_strategy="rrf"``.
    ranker_alpha : float | None
        Weighting coefficient that determines how much the system trusts
        semantic (vector) search versus lexical (keyword/BM25) search. Valid
        only with ``ranker_strategy="weighted"``.

    Raises
    ------
    ValueError
        If ``search_mode`` is unknown, if any ranker parameter is supplied
        for a non-hybrid search, if ``ranker_strategy`` is missing or invalid
        for a hybrid search, or if ``ranker_k``/``ranker_alpha`` are paired
        with the wrong strategy.
    """
    if search_mode not in _VALID_SEARCH_MODES:
        raise ValueError(f"Invalid search_mode '{search_mode}'. Must be one of {_VALID_SEARCH_MODES}.")

    has_strategy = ranker_strategy is not None and ranker_strategy != ""
    has_k = ranker_k is not None and ranker_k > 0
    has_alpha = ranker_alpha is not None and ranker_alpha != 1

    if search_mode != "hybrid":
        if has_strategy:
            raise ValueError(
                f"ranker_strategy='{ranker_strategy}' is only valid when search_mode='hybrid', "
                f"but search_mode='{search_mode}'."
            )
        if has_k:
            raise ValueError(
                f"ranker_k={ranker_k} is only valid when search_mode='hybrid', but search_mode='{search_mode}'."
            )
        if has_alpha:
            raise ValueError(
                f"ranker_alpha={ranker_alpha} is only valid when search_mode='hybrid', "
                f"but search_mode='{search_mode}'."
            )
    else:
        if not has_strategy:
            raise ValueError("ranker_strategy must be set when search_mode='hybrid'.")
        if ranker_strategy not in _VALID_RANKER_STRATEGIES:
            raise ValueError(f"Invalid ranker_strategy='{ranker_strategy}'. Must be one of {_VALID_RANKER_STRATEGIES}.")
        if has_k and ranker_strategy != "rrf":
            raise ValueError(
                f"ranker_k={ranker_k} is only valid when ranker_strategy='rrf', "
                f"but ranker_strategy='{ranker_strategy}'."
            )
        if has_alpha and ranker_strategy != "weighted":
            raise ValueError(
                f"ranker_alpha={ranker_alpha} is only valid when ranker_strategy='weighted', "
                f"but ranker_strategy='{ranker_strategy}'."
            )