prism_documents. Every read and write operation is filtered by userId so that users only ever access their own data.
The Qdrant client is initialized in lib/qdrant.ts using the @qdrant/js-client-rest package:
Collection configuration
Theprism_documents collection is created on first use by initializeCollection. If the collection already exists, the function ensures all payload indexes are present (index creation is idempotent — Qdrant returns an error if the index exists, which initializeCollection handles silently).
| Setting | Value | Notes |
|---|---|---|
| Collection name | prism_documents | Single collection for all users |
| Vector size | 768 | Matches Gemini text-embedding-004 output |
| Distance | Cosine | Normalized similarity for semantic text matching |
| Indexing threshold | 10,000 | Qdrant builds HNSW index after this many points |
Payload indexes
Four keyword indexes are created at collection initialization. These make Qdrant’s filtered search efficient — without them, filtered queries require a full collection scan.| Field | Schema | Used in |
|---|---|---|
userId | keyword | All search, scroll, and delete operations |
documentId | keyword | Chunk deletion, recommendations scroll |
documentType | keyword | Optional filter in search and chat |
category | keyword | Optional filter in search |
Point structure
Each indexed chunk becomes a point in Qdrant with the following shape:batchIndexChunks (used by the /api/documents/index route) assigns a fresh crypto.randomUUID() to each point. indexDocumentChunk (the single-chunk variant) uses a deterministic ID of {documentId}_chunk_{chunkIndex}, which makes it safe to re-index individual chunks by upserting.Indexing
batchIndexChunks
The primary indexing path. Called after batchGenerateEmbeddings produces all embeddings for a document:
qdrantClient.upsert, the function validates every embedding:
Similarity search
searchSimilarChunks
- A diagnostic search for the top 3 results with no score threshold — logged for debugging.
- The actual search with
score_thresholdapplied and the requestedlimit.
Score thresholds by call site
Different parts of Prism use different thresholds to balance precision and recall:| Call site | scoreThreshold | limit | Reason |
|---|---|---|---|
/api/chat | 0.4 | 5 | Lower threshold ensures enough context for RAG even when the question is loosely worded |
/api/search/semantic | 0.5 (default) | 10 (default) | Caller-configurable; default balances precision and coverage |
searchSimilarChunks default | 0.7 | 5 | High precision for direct API use |
Document recommendations
getRecommendations finds documents that are semantically similar to a given document. It does not require a text query — instead it uses the document’s own vector as the search probe.
Scroll for the source vector
Fetches the first chunk of the source document from Qdrant using the REST scroll API, filtered by both
documentId and userId, with with_vector: true to retrieve the raw embedding.Search for similar chunks
Runs
qdrantClient.search using the retrieved vector as the query, excluding the source document with a must_not filter on documentId.Aggregate by document
Groups matching chunks by
documentId, tracking the highest chunk score per document and keeping the top 3 matching chunks for display.Chunk deletion
When a document is deleted from Appwrite Storage,deleteDocumentChunks removes all associated Qdrant points:
documentId payload index makes this a fast keyed delete rather than a scan. The deleteDocument function in lib/appwrite.ts calls /api/documents/delete after successfully removing the file from storage. If the Qdrant call fails, the storage file is already gone — the error is logged as a warning but does not surface to the user.
Collection stats
getCollectionStats returns a lightweight summary of the prism_documents collection: