import type { CursorPage } from '../pagination.js';
import type { EmbeddingProvider } from '../spi/embedding-provider.js';
import type { Passage } from '../spi/retriever.js';
import { EmbeddingRetriever } from './embedding-retriever.js';
import type { IndexedDocument, MetadataPatch, VectorRecord, VectorSearchOptions, VectorStore } from './vector-store.js';
/** Similarity → Qdrant distance name. Cosine (default), Dot (inner), Euclid (l2). */
export type QdrantMetric = 'cosine' | 'inner' | 'l2';
export interface QdrantStoreOptions {
    /** Collection name. Default `agent_rag_chunks`. */
    collection?: string;
    /** Embedding width — must match the model (1536 for text-embedding-3-small). Default 1536. */
    dimension?: number;
    /** Similarity metric. Default `cosine`. */
    metric?: QdrantMetric;
    /**
     * Maximum points per upsert request. Large sources become MANY chunks (a ~200-page PDF is
     * ~700 points); sending them in a single request blows the client's default timeout
     * (`@qdrant/js-client-rest`, 300s) because a multi-megabyte body stalls on the way. Slicing into
     * batches keeps every request small and predictable. Default 100 (measured at ~4-6s per batch of
     * 100). Zero or negative → no slicing.
     */
    upsertBatchSize?: number;
}
/** A Qdrant point (UUID id, vector, payload). */
export interface QdrantPoint {
    id: string;
    vector: number[];
    payload: Record<string, unknown>;
}
/**
 * A Qdrant filter condition (the subset this store uses). A field condition matches on `match`
 * (exact value, set membership, exclusion); an {@link QdrantIsEmptyCondition} matches the
 * ABSENCE — field missing, null or empty list — which is what lets a filter express the
 * fallback buckets an admin aggregation needs (a chunk with no `metadata.tipo`, a document
 * keyed by NO field of a priority chain).
 */
export interface QdrantFieldCondition {
    key: string;
    match: {
        value: unknown;
    } | {
        any: unknown[];
    } | {
        except: unknown[];
    };
}
export interface QdrantIsEmptyCondition {
    is_empty: {
        key: string;
    };
}
export type QdrantCondition = QdrantFieldCondition | QdrantIsEmptyCondition;
/**
 * A Qdrant filter. `must` is ANDed, `must_not` negates (so `must_not: [{ is_empty: … }]` reads
 * "field PRESENT"), and `should` requires at least one match when present — together the boolean
 * shape a server-side GROUP BY over a priority chain (COALESCE) needs without fetching the
 * points to evaluate it client-side.
 */
export interface QdrantFilter {
    must?: QdrantCondition[];
    must_not?: QdrantCondition[];
    should?: QdrantCondition[];
}
/**
 * The subset of the `@qdrant/js-client-rest` client {@link QdrantStore} uses — structural, so the
 * package stays an OPTIONAL peer (the factory imports the real client and hands it in) and tests can
 * pass a fake. Mirrors pgvector's `LucidDatabaseLike`.
 */
export interface QdrantClientLike {
    getCollections(): Promise<{
        collections: {
            name: string;
        }[];
    }>;
    createCollection(name: string, config: {
        vectors: {
            size: number;
            distance: string;
        };
    }): Promise<unknown>;
    upsert(collection: string, args: {
        points: QdrantPoint[];
    }): Promise<unknown>;
    query(collection: string, args: {
        query: number[];
        limit: number;
        with_payload: boolean;
        filter?: QdrantFilter;
        score_threshold?: number;
    }): Promise<{
        points: {
            score: number;
            payload?: Record<string, unknown>;
        }[];
    }>;
    delete(collection: string, args: {
        filter?: QdrantFilter;
        points?: string[];
        wait?: boolean;
    }): Promise<unknown>;
    scroll(collection: string, args: {
        filter?: QdrantFilter;
        /** `true` for the whole payload, or a key list to transfer only those keys. */
        with_payload: boolean | string[];
        with_vector: boolean;
        limit: number;
        offset?: unknown;
    }): Promise<{
        points: {
            payload?: Record<string, unknown>;
        }[];
        next_page_offset?: unknown;
    }>;
    /**
     * Set payload keys on specific points, leaving every other key — and **the vector** — alone. Backs
     * {@link QdrantStore.updateMetadata}; the real `@qdrant/js-client-rest` provides it.
     *
     * OPTIONAL here, mirroring the `expire?` precedent on this package's Redis client shim, so adding it
     * cannot break a host that hand-rolled this structural interface. `updateMetadata` throws a named
     * error rather than degrading if it is absent: the alternative degradation would be scroll-with-vector
     * plus `upsert`, which rewrites the embedding — the exact thing the method promises not to do.
     */
    setPayload?(collection: string, args: {
        payload: Record<string, unknown>;
        points: string[];
        wait?: boolean;
    }): Promise<unknown>;
    /**
     * Count the points matching a filter WITHOUT transferring them. Backs the chunk count
     * {@link QdrantStore.removeWhere} resolves to — Qdrant's delete response carries no count, and this is
     * an aggregate, not an enumeration: no payloads, no vectors, one cheap round trip. Optional for the
     * same reason as {@link QdrantClientLike.setPayload}; the real `@qdrant/js-client-rest` provides it.
     */
    count?(collection: string, args: {
        filter?: QdrantFilter;
        exact?: boolean;
    }): Promise<{
        count: number;
    }>;
    /**
     * Server-side GROUP BY over one indexed payload key (`POST /collections/{c}/facet`): one
     * `{ value, count }` per distinct value, narrowed by the same {@link QdrantFilter} the other
     * calls use — WITHOUT transferring the points. This is the aggregate an inspection panel needs
     * ("how many chunks per document, per field value"); a scroll-based emulation is O(collection)
     * round trips per request and stops being a request at corpus scale.
     *
     * OPTIONAL, same structural-shim reason as {@link QdrantClientLike.count}; the real
     * `@qdrant/js-client-rest` (>= 1.18 against a server >= 1.10) provides it. Requires a payload
     * index on the field — {@link QdrantStore.facetValues} provisions one.
     */
    facet?(collection: string, args: {
        key: string;
        limit?: number;
        filter?: QdrantFilter;
        exact?: boolean;
    }): Promise<{
        hits: FacetHit[];
    }>;
    /**
     * Index one payload field for filtering/faceting (`is_tenant` is NOT set — no performance
     * promise beyond plain indexing). OPTIONAL for the same reason as
     * {@link QdrantClientLike.facet}; the real client provides it.
     */
    createPayloadIndex?(collection: string, args: {
        field_name: string;
        field_schema: {
            type: 'keyword';
        };
        wait?: boolean;
    }): Promise<unknown>;
}
/** One row of a {@link QdrantClientLike.facet} response: a distinct value and its exact count. */
export interface FacetHit {
    value: string | number | boolean;
    count: number;
}
/**
 * Translates a metadata filter (`Record<string, unknown>`) into a {@link QdrantFilter}, preserving
 * token-ACL semantics: a scalar is an exact match; an array is match-any (set membership — it mirrors
 * pgvector's `jsonb_exists_any`, since Qdrant's `any` tests intersection against a scalar OR an array
 * field); an empty array denies everything (`any: []` never matches). Keys target `metadata.<k>` (the
 * payload nests metadata under `metadata`). Multiple keys are ANDed into `must`. Empty or absent →
 * undefined.
 */
export declare function buildQdrantFilter(filter: Record<string, unknown> | undefined): QdrantFilter | undefined;
/** UUIDv5(NAMESPACE, name) via SHA-1 — deterministic, with no external dependency. */
export declare function chunkIdToPointId(chunkId: string): string;
/**
 * A {@link VectorStore} over Qdrant — the twin of {@link import('./pg-vector-store.js').PgVectorStore}
 * for managed vector databases. It talks to the client through the structural {@link QdrantClientLike}
 * (so `@qdrant/js-client-rest` stays an optional peer). One collection, scoped by payload filter. The
 * chunk id (`${documentId}#<n>`) becomes the point's UUIDv5 and travels back in the payload.
 */
export declare class QdrantStore implements VectorStore {
    private readonly client;
    private readonly collection;
    private readonly dimension;
    private readonly metric;
    private readonly upsertBatchSize;
    constructor(client: QdrantClientLike, options?: QdrantStoreOptions);
    /** Idempotent: creates the collection (dimension + metric) if it does not exist yet. */
    ensureCollection(): Promise<void>;
    upsert(records: VectorRecord[]): Promise<void>;
    search(embedding: number[], options: VectorSearchOptions): Promise<Passage[]>;
    remove(documentId: string): Promise<void>;
    /**
     * {@link VectorStore.updateMetadata} — rewrite every chunk's metadata for one document, touching
     * neither `text` nor the **vector**.
     *
     * Qdrant is the one adapter here that cannot do the merge server-side. `set_payload` merges at the TOP
     * level of a point's payload, and this store nests the caller's metadata one level down (under
     * `metadata`, so a filter key is `metadata.<k>` — see {@link buildQdrantFilter}), so a top-level set of
     * `metadata` replaces the whole object rather than merging into it. The merge therefore happens in JS:
     * scroll the document's points for their current metadata, apply the shared
     * {@link import('./vector-store.js').applyMetadataPatch}, write back. Cost: one scroll (payload only,
     * **`with_vector: false`**, and only the two keys needed) plus one `set_payload` per distinct resulting
     * metadata — which is ONE for any corpus built by `chunkDocuments`, since that stamps the same document
     * metadata on every chunk.
     *
     * The vector is untouched by construction, not by care: `set_payload` has no vector field, and the
     * points are never re-`upsert`ed (upsert is the call that would carry a vector). The scroll does not
     * even fetch the vectors, so this method never has one in hand to write back.
     *
     * Point ids are re-derived with {@link chunkIdToPointId} from the chunk id in the payload, so the
     * scroll needs no point id from the response and this store keeps working against a client whose
     * `scroll` returns only payloads.
     *
     * `wait: true` because the returned count is a claim that the writes landed; Qdrant's default is to
     * acknowledge before applying.
     */
    updateMetadata(documentId: string, patch: MetadataPatch): Promise<number>;
    /**
     * {@link VectorStore.listDocumentIds} — the same scroll {@link QdrantStore.listDocuments} does, but
     * transferring ONE payload key (`documentId`) instead of the whole payload, so the per-chunk metadata
     * blob never crosses the wire or gets parsed. Qdrant has no `DISTINCT`, so the collapse still happens
     * here; what this saves is the payload, which is the bulk of it. Filter comes from the same
     * {@link buildQdrantFilter} `search` uses.
     */
    listDocumentIds(filter?: Record<string, unknown>): Promise<string[]>;
    /**
     * {@link VectorStore.removeWhere} — ONE filtered `delete`, no enumeration: Qdrant deletes by filter
     * server-side, so this never scrolls the points it is about to remove.
     *
     * Filter parity with {@link QdrantStore.search} is by construction — the same
     * {@link buildQdrantFilter} call produces the predicate for both, so `metadata.<k>` keying, scalar
     * `match.value`, array `match.any` set-membership and the empty-array deny are the same object here as
     * there.
     *
     * It DOES cost one extra round trip that memory and pgvector do not: Qdrant's delete response carries
     * no count, so the chunk count comes from a preceding `count` (an aggregate — no payloads, no vectors,
     * not an enumeration). Consequence worth knowing: the count is taken just before the delete, so under
     * concurrent writes it is a very slightly stale count of what matched, where memory and pgvector return
     * an exact count from the deleting statement itself. The delete is filter-scoped either way; only the
     * reported number can drift.
     */
    removeWhere(filter: Record<string, unknown>): Promise<number>;
    listDocuments(filter?: Record<string, unknown>): Promise<IndexedDocument[]>;
    /**
     * OPTIONAL capability. Server-side GROUP BY + exact count over one payload field — the
     * aggregate an inspection panel reads per document ("chunks per value of `metadata.numeroProcesso`",
     * values of the top-level `source`), optionally narrowed by a RAW {@link QdrantFilter} (the
     * metadata-`Record` filters of {@link VectorStore.search} cannot express the absence a priority
     * chain needs; raw filters can, via `is_empty`/`must_not`/`should`).
     *
     * Qdrant requires a payload index on the facetted field. When the server complains, this method
     * PROVISIONS a `keyword` index (idempotent, `wait: true`) and retries the facet exactly once —
     * the first call on a fresh collection pays one index build, every later call is a plain facet.
     * If the client lacks `facet` (too old) the error says so; if it lacks `createPayloadIndex` the
     * original server error propagates instead of a silent degrade.
     *
     * Counts are the server's FAST default (per-segment indexed statistics). `exact: true` asks for
     * the expensive recount and can cost tens of seconds per call on a corpus of hundreds of
     * thousands of points — which for a panel that fans out into a dozen facets is the difference
     * between a page load and a hang. It stays opt-in for reconciliation-shaped uses.
     *
     * `first` is the number of distinct values to return (default 200) — spelled as in
     * {@link import('../pagination.js').CursorParams} so page sizes are named the same thing across the
     * ecosystem. It is deliberately NOT a {@link import('../pagination.js').CursorPage}: Qdrant's facet
     * endpoint returns the top-`first` values in one shot and hands back **no continuation token**, so a
     * page envelope here would advertise a `nextCursor` that could only ever be `null`. Faceting is a
     * capped aggregate; {@link QdrantStore.scrollChunks} is the paginated surface.
     */
    facetValues(field: string, options?: {
        filter?: QdrantFilter;
        first?: number;
        exact?: boolean;
    }): Promise<FacetHit[]>;
    /**
     * OPTIONAL capability. Count chunks matching a RAW {@link QdrantFilter} without transferring
     * any — the aggregate behind a panel row like "chunks with NO key field at all". Requires a
     * client with `count` (the real one has it; {@link QdrantStore.removeWhere} already depends on
     * it). The count is the server's fast default; `exact: true` forces the expensive recount
     * (same trade-off as {@link QdrantStore.facetValues}).
     */
    countChunks(options?: {
        filter?: QdrantFilter;
        exact?: boolean;
    }): Promise<number>;
    /**
     * OPTIONAL capability. Enumerate chunk PAYLOADS matching a RAW {@link QdrantFilter} — the
     * read side of the same server-side narrowing {@link QdrantStore.facetValues} counts. Vectors
     * never cross the wire; `payloadKeys` narrows the transfer further.
     *
     * Unlike {@link VectorStore.listDocuments} this is per CHUNK (not collapsed per document) and
     * speaks the raw filter language — it exists for inspection/deletion panels, where the caller
     * knows the exact payload shape it stored.
     *
     * **Cursor-paginated**, in the ecosystem's forward-only interface (see
     * {@link import('../pagination.js').CursorParams}): `{ after, first }` in, a
     * {@link import('../pagination.js').CursorPage} out. ONE round trip per call — `first` (default
     * 256) is the Qdrant scroll `limit`, and `nextCursor` wraps Qdrant's own opaque
     * `next_page_offset`. Qdrant's scroll only ever resumes *forward* from that offset, so
     * `prevCursor` is always `null` and `hasPrev` always `false`.
     *
     * The cursor is OPAQUE — {@link encodeScrollCursor} base64url's the raw offset precisely so
     * callers cannot come to depend on it being a Qdrant point id. Hand it back verbatim.
     *
     * To drain the whole match, loop while `hasNext`:
     *
     * ```ts
     * const all: Record<string, unknown>[] = [];
     * let after: string | undefined;
     * do {
     *   const page = await store.scrollChunks({ filter, ...(after ? { after } : {}) });
     *   all.push(...page.items);
     *   after = page.nextCursor ?? undefined;
     * } while (after !== undefined);
     * ```
     */
    scrollChunks(options?: {
        filter?: QdrantFilter;
        payloadKeys?: string[];
        after?: string;
        first?: number;
    }): Promise<CursorPage<Record<string, unknown>>>;
}
/**
 * Wrap Qdrant's `next_page_offset` (a point id — string, number or UUID, the server's business) in an
 * opaque, URL-safe cursor: base64url of its JSON. Mirrors how `@adonis-agora/filter` encodes its
 * keyset cursors, and for the same reason — a cursor a caller can read is a cursor a caller will
 * eventually construct.
 */
export declare function encodeScrollCursor(offset: unknown): string;
/**
 * Unwrap a cursor produced by {@link encodeScrollCursor}. Throws on a malformed one rather than
 * silently restarting from the first page: a scroll that quietly rewinds re-emits chunks a
 * deletion panel already processed, which is worse than a loud failure.
 */
export declare function decodeScrollCursor(cursor: string): unknown;
/** A {@link import('../spi/retriever.js').Retriever} over a {@link QdrantStore}: embeds the query,
 *  then searches Qdrant. Twin of `PgVectorRetriever`. */
export declare class QdrantRetriever extends EmbeddingRetriever {
    constructor(embedder: EmbeddingProvider, store: QdrantStore);
}
//# sourceMappingURL=qdrant-store.d.ts.map