← All posts
·16 min read

Claude Code with Qdrant: Vector Search That Stays Accurate

Claude CodeQdrantVector DatabaseEmbeddings
Claude Code with Qdrant: Vector search that stays accurate

Why Qdrant without CLAUDE.md returns wrong results under production load

Qdrant is the leading open-source vector database for production RAG, semantic search, and recommendation systems in 2026. The HNSW index is fast, the filtering is exact rather than approximate, the payload storage is durable, and the API is consistent across Python, TypeScript, Go, and Rust clients. The problem is that Qdrant's correctness depends on configuration decisions that have no obvious wrong choice at small scale but compound into recall failures and query timeouts at production scale. Claude Code generates Qdrant integrations that work on a 10,000-vector test set and degrade silently when the production collection hits 10 million vectors.

The most common Claude defaults that break Qdrant at scale: collections created with vector_size mismatched to the embedding model (a 1536-dim collection for ada-002 embeddings when the model has been switched to text-embedding-3-large which is 3072-dim), distance metrics that do not match the embedding model's training (cosine for normalised embeddings is correct, dot for unnormalised is correct, Euclidean is almost always wrong), payload fields used in filters with no index created on them (filter-then-search degrades to brute force), HNSW parameters left at defaults (ef_construct: 100 is too low for recall in high-dim collections), named vectors omitted on multi-modal collections so you cannot mix text and image embeddings in the same record, and sparse vectors ignored entirely so hybrid search is not available.

This guide covers the CLAUDE.md configuration that locks Claude Code into Qdrant's correct model: explicit vector dimensions matched to the embedding model, the right distance metric per model family, payload indexes created before filter queries hit production, HNSW parameters tuned for the dataset size, and named vectors for multi-modal collections. If you are building a RAG application on top of Qdrant, Claude Code with LangSmith covers the trace correlation that lets you debug bad retrievals. For trace correlation with your LLM observability stack, Claude Code with Langfuse covers the retrieval-step instrumentation.

The Qdrant CLAUDE.md template

The CLAUDE.md at your project root is read at the start of every Claude Code session. For a Qdrant-backed application it needs to declare: the SDK version, the collection naming convention, the vector configuration, the payload schema, the index management policy, and the hard rules that block the mistakes Claude makes most often.

# Qdrant rules

## Stack
- @qdrant/js-client-rest ^1.x (TypeScript/Node)
- qdrant-client ^1.x (Python)
- Qdrant server 1.10+ (self-hosted) OR Qdrant Cloud
- QDRANT_URL in .env (https://your-cluster.qdrant.io OR http://localhost:6333)
- QDRANT_API_KEY in .env (Cloud only)
- Embedding model: text-embedding-3-large (3072-dim) OR custom

## Project structure
- src/lib/qdrant.ts                , Qdrant client singleton
- src/lib/embeddings.ts            , Embedding model wrapper (single source of truth for dim)
- src/lib/collections/             , Collection schemas (one file per collection)
- src/lib/payload-types.ts         , TypeScript types for payload fields

## Collection configuration (MANDATORY)
Every collection MUST declare:
- vectors.size: matches embedding model output dimension (verify, do NOT guess)
- vectors.distance: 'Cosine' | 'Dot' | 'Euclid' | 'Manhattan' (model-specific)
- on_disk_payload: true (large payloads on disk, not in RAM)
- hnsw_config.m: 16 (default, adequate for most workloads)
- hnsw_config.ef_construct: 200 (raise for high-dim, more thorough indexing)
- optimizers_config.indexing_threshold: 20000 (delay indexing until batch size reached)

## Payload indexes (REQUIRED before filter queries)
Every payload field used in a filter MUST have an index:
- createPayloadIndex({ collection_name, field_name, field_schema: 'keyword' })
- Run AFTER collection creation, AFTER initial bulk insert, BEFORE production traffic
- Without indexes, filters degrade to full-collection scan (latency 100x+ higher)

## Hard rules
- NEVER hardcode vector dimensions, derive from embedding model constant
- NEVER use Euclid for normalised embeddings (Cosine is correct)
- NEVER skip on_disk_payload: true on collections with text payloads >1KB
- NEVER run filter queries against fields without payload indexes
- NEVER upsert with point.id as null, ALWAYS use UUIDs or stable integers
- NEVER set ef_search too high (slows queries); too low (loses recall)
- ALWAYS validate vector length on insert (len(vector) == collection.vectors.size)

Three rules here prevent the majority of production failures Claude generates without them.

The vector dimension rule is the most impactful because the failure mode is silent. If your collection is configured for 1536 dimensions and you upsert 3072-dimensional vectors, Qdrant returns a clear error. But if you upsert a mix where some vectors are 1536 (an older embedding model still cached) and others are 3072, the rejected ones are dropped from the batch with a warning but the operation reports success. The collection ends up with a fraction of the data you think you stored, and search results are missing the rejected points. Deriving the dimension from a single embedding-model constant in code makes this impossible.

The distance metric rule is the most impactful for relevance. The wrong distance metric does not produce an error: it produces wrong results. OpenAI's text-embedding-3-large outputs unit-normalised vectors, so Cosine and Dot product are mathematically identical and either works. But Sentence Transformers' all-mpnet-base-v2 outputs unnormalised vectors, where Cosine and Dot diverge significantly. Choosing the wrong one produces semantic search results that look plausible but are systematically biased toward longer or shorter passages. The correct metric is model-specific. Claude defaults to Cosine in every example, which works for OpenAI but degrades silently for many open-source models.

The payload index rule is the most impactful for production latency. Qdrant's filtering is exact: every filter condition is evaluated against every candidate point before vector similarity scoring. Without a payload index on the filter field, this is a full-collection scan. With a payload index, it is a constant-time lookup. The difference at 10 million points is 50-500ms vs sub-millisecond. Claude omits payload indexes by default because they are an explicit second step after collection creation, not part of the create-collection call.

Install and client setup

Install the SDK:

npm i @qdrant/js-client-rest

Add credentials to your environment file:

# .env.local
QDRANT_URL=https://your-cluster-id.aws.cloud.qdrant.io
QDRANT_API_KEY=your_qdrant_api_key_here

Create the singleton client:

// src/lib/qdrant.ts
import { QdrantClient } from '@qdrant/js-client-rest';

if (!process.env.QDRANT_URL) {
  throw new Error('QDRANT_URL is not defined');
}

export const qdrant = new QdrantClient({
  url: process.env.QDRANT_URL,
  apiKey: process.env.QDRANT_API_KEY,
  timeout: 30000,
});

For self-hosted Qdrant on localhost (no API key required), the constructor still accepts an empty apiKey and the client works against http://localhost:6333.

Define the embedding model and its dimension in a single source of truth:

// src/lib/embeddings.ts
import OpenAI from 'openai';

const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });

export const EMBEDDING_MODEL = 'text-embedding-3-large';
export const EMBEDDING_DIMENSIONS = 3072;
export const EMBEDDING_DISTANCE = 'Cosine' as const;

export async function embed(text: string): Promise<number[]> {
  const response = await openai.embeddings.create({
    model: EMBEDDING_MODEL,
    input: text,
  });
  const vector = response.data[0].embedding;

  if (vector.length !== EMBEDDING_DIMENSIONS) {
    throw new Error(`Embedding dimension mismatch: got ${vector.length}, expected ${EMBEDDING_DIMENSIONS}`);
  }

  return vector;
}

export async function embedBatch(texts: string[]): Promise<number[][]> {
  const response = await openai.embeddings.create({
    model: EMBEDDING_MODEL,
    input: texts,
  });
  return response.data.map(item => item.embedding);
}

The runtime check if (vector.length !== EMBEDDING_DIMENSIONS) is the safety net. If you switch embedding models without updating the constant, the next embed call fails loudly instead of silently dropping points during upsert.

Add the embedding model section to CLAUDE.md:

## Embedding model

- Single source of truth: src/lib/embeddings.ts
- EMBEDDING_MODEL, EMBEDDING_DIMENSIONS, EMBEDDING_DISTANCE as exported constants
- All collection definitions import these constants, NEVER hardcoded dimensions
- embed() function validates vector length on every call
- Switching models: update constants in ONE file, then rebuild collections
- NEVER mix vectors from different models in the same collection

Creating a collection

A production-ready collection with explicit configuration:

// src/lib/collections/documents.ts
import { qdrant } from '@/lib/qdrant';
import { EMBEDDING_DIMENSIONS, EMBEDDING_DISTANCE } from '@/lib/embeddings';

export const DOCUMENTS_COLLECTION = 'documents';

export async function ensureDocumentsCollection() {
  const exists = await qdrant.collectionExists(DOCUMENTS_COLLECTION);

  if (exists.exists) {
    return;
  }

  await qdrant.createCollection(DOCUMENTS_COLLECTION, {
    vectors: {
      size: EMBEDDING_DIMENSIONS,
      distance: EMBEDDING_DISTANCE,
      on_disk: true,
    },
    on_disk_payload: true,
    hnsw_config: {
      m: 16,
      ef_construct: 200,
      full_scan_threshold: 10000,
    },
    optimizers_config: {
      indexing_threshold: 20000,
      memmap_threshold: 200000,
    },
    quantization_config: {
      scalar: {
        type: 'int8',
        always_ram: true,
      },
    },
  });

  // Create payload indexes BEFORE any production traffic
  await qdrant.createPayloadIndex(DOCUMENTS_COLLECTION, {
    field_name: 'category',
    field_schema: 'keyword',
  });

  await qdrant.createPayloadIndex(DOCUMENTS_COLLECTION, {
    field_name: 'tenant_id',
    field_schema: 'keyword',
  });

  await qdrant.createPayloadIndex(DOCUMENTS_COLLECTION, {
    field_name: 'created_at',
    field_schema: 'integer',
  });

  await qdrant.createPayloadIndex(DOCUMENTS_COLLECTION, {
    field_name: 'language',
    field_schema: 'keyword',
  });
}

The configuration fields and their trade-offs:

  • vectors.on_disk: true stores vectors on disk instead of RAM. For collections that fit in RAM, this is slower (one disk read per vector). For collections too large for RAM, this is the only option. Use false only when you have measured RAM usage and confirmed the collection fits.
  • on_disk_payload: true stores the payload (metadata) on disk. Text-heavy payloads should always be on disk. Numeric or short-string payloads can stay in RAM with false.
  • hnsw_config.m: 16 is the number of bidirectional links per node. Higher m gives better recall and higher memory usage. 16 is the default and works for most cases. Raise to 32 for very high-dimensional embeddings (>2000 dim) or for collections where recall is critical.
  • hnsw_config.ef_construct: 200 is the search depth during index construction. Higher values give better index quality at the cost of slower indexing. The default is 100; production deployments with high-dim embeddings (>1500 dim) benefit from 200 or higher.
  • optimizers_config.indexing_threshold: 20000 delays HNSW indexing until 20,000 points are inserted. This is critical for bulk-insert workflows: the alternative (indexing every point on insert) is 10-100x slower for large initial loads.
  • quantization_config.scalar.type: 'int8' enables scalar quantization, which reduces memory usage by 4x at a small recall cost. always_ram: true keeps the quantized vectors in RAM even when the original vectors are on disk.

Add a collection configuration section to CLAUDE.md:

## Collection configuration

- vectors.size: imported from EMBEDDING_DIMENSIONS, NEVER hardcoded
- vectors.distance: imported from EMBEDDING_DISTANCE, NEVER guessed
- on_disk_payload: true for any collection with text payloads
- hnsw_config.m: 16 default, 32 for high-dim (>2000) or recall-critical
- hnsw_config.ef_construct: 200 for production (default 100 too low at scale)
- optimizers_config.indexing_threshold: 20000 to enable fast bulk-insert
- quantization_config.scalar.int8 for memory-constrained deployments
- ALWAYS createPayloadIndex() after collection create for every filter field

Upserting points

Upserting requires a stable point ID, the vector, and the payload. The point ID can be a UUID string or a non-negative integer. The vector must match the collection's vectors.size. The payload is a flat object of JSON-serialisable values.

// src/lib/qdrant-upsert.ts
import { v4 as uuidv4 } from 'uuid';
import { qdrant } from '@/lib/qdrant';
import { embedBatch } from '@/lib/embeddings';
import { DOCUMENTS_COLLECTION } from '@/lib/collections/documents';

interface DocumentInput {
  id?: string;
  text: string;
  category: string;
  tenant_id: string;
  language: string;
  url: string;
  title: string;
  created_at: number;
}

export async function upsertDocuments(documents: DocumentInput[]) {
  const vectors = await embedBatch(documents.map(d => d.text));

  const points = documents.map((doc, i) => ({
    id: doc.id ?? uuidv4(),
    vector: vectors[i],
    payload: {
      text: doc.text,
      category: doc.category,
      tenant_id: doc.tenant_id,
      language: doc.language,
      url: doc.url,
      title: doc.title,
      created_at: doc.created_at,
    },
  }));

  await qdrant.upsert(DOCUMENTS_COLLECTION, {
    points,
    wait: true,
  });
}

The wait: true flag is critical for write-then-read patterns. Without it, Qdrant returns success as soon as the points are queued, before they are visible to search queries. This causes the "I just upserted but the search returns nothing" failure that Claude often generates and cannot diagnose. With wait: true, the call blocks until the points are persisted and searchable.

Batch size matters for upsert performance. Qdrant accepts batches up to several thousand points, but optimal throughput is typically 100-500 points per batch. For very large bulk loads, use a producer-consumer pattern with bounded concurrency:

async function bulkUpsert(documents: DocumentInput[]) {
  const BATCH_SIZE = 200;
  const CONCURRENCY = 4;

  const batches: DocumentInput[][] = [];
  for (let i = 0; i < documents.length; i += BATCH_SIZE) {
    batches.push(documents.slice(i, i + BATCH_SIZE));
  }

  for (let i = 0; i < batches.length; i += CONCURRENCY) {
    const concurrent = batches.slice(i, i + CONCURRENCY);
    await Promise.all(concurrent.map(batch => upsertDocuments(batch)));
  }
}

Add an upsert section to CLAUDE.md:

## Upserting points

- ALWAYS use wait: true for write-then-read patterns
- Batch size: 100-500 points per upsert call (optimal throughput)
- Point IDs: UUID string OR non-negative integer, NEVER null
- vector field MUST match EMBEDDING_DIMENSIONS exactly
- payload field: flat JSON-serialisable object only
- For bulk loads: use producer-consumer with CONCURRENCY: 4 max
- For initial collection load: enable optimizers_config.indexing_threshold: 20000

Searching with filters

Filtered search combines vector similarity with payload constraints. The filter is evaluated first against the indexed payload fields, then vector similarity is computed only against the matching subset.

// src/lib/qdrant-search.ts
import { qdrant } from '@/lib/qdrant';
import { embed } from '@/lib/embeddings';
import { DOCUMENTS_COLLECTION } from '@/lib/collections/documents';

interface SearchOptions {
  query: string;
  tenant_id: string;
  category?: string;
  language?: string;
  limit?: number;
}

export async function searchDocuments(options: SearchOptions) {
  const queryVector = await embed(options.query);

  const filterMust: Array<Record<string, unknown>> = [
    { key: 'tenant_id', match: { value: options.tenant_id } },
  ];

  if (options.category) {
    filterMust.push({ key: 'category', match: { value: options.category } });
  }

  if (options.language) {
    filterMust.push({ key: 'language', match: { value: options.language } });
  }

  const results = await qdrant.search(DOCUMENTS_COLLECTION, {
    vector: queryVector,
    filter: { must: filterMust },
    limit: options.limit ?? 10,
    with_payload: true,
    with_vector: false,
    params: {
      hnsw_ef: 128,
      exact: false,
    },
  });

  return results.map(r => ({
    id: r.id,
    score: r.score,
    title: r.payload?.title as string,
    text: r.payload?.text as string,
    url: r.payload?.url as string,
  }));
}

The filter structure uses three top-level keys: must (all conditions match), should (at least one matches), must_not (none match). Each condition is a { key, match: { value } } object for keyword matching, { key, range: { gte, lte } } for numeric ranges, or { key, geo_bounding_box: {...} } for geo queries.

The search parameters:

  • hnsw_ef: 128 controls search-time recall. Higher values give better recall at higher latency. The default is 128. For high-recall applications, raise to 256 or 512. For low-latency applications, lower to 64.
  • exact: false uses the HNSW index. Set to true for ground-truth comparison or when the collection is small enough that brute force is acceptable.
  • with_payload: true returns the full payload in results. Set to false and fetch payload separately if you only need IDs.
  • with_vector: false excludes the vector from results (the typical case, the vector is large and rarely needed at query time).

Add a search section to CLAUDE.md:

## Searching

- Filters MUST reference payload fields that have payload indexes
- hnsw_ef: 128 default, 256+ for high-recall, 64 for low-latency
- exact: true only for ground-truth comparison, NEVER in production hot path
- with_payload: true to return full payload, false to return IDs only
- with_vector: false in production (vector is large, rarely needed)
- Limit: typical range 10-50, higher values force more expensive operations
- Multi-tenancy: tenant_id MUST be in every search filter (security boundary)

Named vectors for multi-modal collections

Named vectors let one collection store multiple embedding types per point. The canonical use case is multi-modal search: each document has a text embedding and an image embedding, and queries can search either or both.

await qdrant.createCollection('multimodal_documents', {
  vectors: {
    text: {
      size: 3072,
      distance: 'Cosine',
      on_disk: true,
    },
    image: {
      size: 512,
      distance: 'Cosine',
      on_disk: true,
    },
  },
  on_disk_payload: true,
});

// Upserting with named vectors
await qdrant.upsert('multimodal_documents', {
  points: [
    {
      id: uuidv4(),
      vector: {
        text: textEmbedding,
        image: imageEmbedding,
      },
      payload: { title, url },
    },
  ],
  wait: true,
});

// Searching by text vector only
const textResults = await qdrant.search('multimodal_documents', {
  vector: { name: 'text', vector: queryTextEmbedding },
  limit: 10,
});

// Searching by image vector only
const imageResults = await qdrant.search('multimodal_documents', {
  vector: { name: 'image', vector: queryImageEmbedding },
  limit: 10,
});

The trade-off: a multi-modal collection is more complex to query and reason about than two single-modal collections. Use named vectors when you genuinely need atomic upsert across modalities (the text and image must be inserted together or not at all) or when you need to combine modalities in a single ranked result list.

Add a named vectors section to CLAUDE.md:

## Named vectors (multi-modal)

- Use ONLY when you need atomic insert across modalities
- Each named vector has its own size, distance, on_disk settings
- Upsert: vector: { name1: vec1, name2: vec2 }
- Search: vector: { name: 'name1', vector: queryVec }
- Two single-modal collections are simpler if atomicity is not required
- NEVER mix dimension counts across named vectors of the same name

Sparse vectors for hybrid search

Hybrid search combines dense vector similarity (semantic) with sparse vector similarity (keyword, BM25-like). Qdrant supports sparse vectors natively as named vector slots.

await qdrant.createCollection('documents_hybrid', {
  vectors: {
    dense: {
      size: 3072,
      distance: 'Cosine',
    },
  },
  sparse_vectors: {
    sparse: {},
  },
  on_disk_payload: true,
});

// Upserting with both dense and sparse vectors
await qdrant.upsert('documents_hybrid', {
  points: [
    {
      id: uuidv4(),
      vector: {
        dense: denseEmbedding,
        sparse: { indices: sparseIndices, values: sparseValues },
      },
      payload: { title, text },
    },
  ],
  wait: true,
});

// Hybrid search with fusion
const results = await qdrant.query('documents_hybrid', {
  prefetch: [
    {
      query: denseQueryEmbedding,
      using: 'dense',
      limit: 50,
    },
    {
      query: { indices: sparseQueryIndices, values: sparseQueryValues },
      using: 'sparse',
      limit: 50,
    },
  ],
  query: { fusion: 'rrf' },
  limit: 10,
  with_payload: true,
});

The prefetch array runs both searches independently and produces a candidate set. The query: { fusion: 'rrf' } step combines the candidate sets using Reciprocal Rank Fusion, which is the standard hybrid-search ranking algorithm. The final limit: 10 returns the top 10 after fusion.

Sparse vectors are typically computed by a separate model (SPLADE, BM25 with vocabulary tokenization, or domain-specific keyword extractors). The exact pipeline is application-specific. The Qdrant side is identical: indices and values arrays.

Add a hybrid search section to CLAUDE.md:

## Hybrid search (sparse + dense)

- Declare sparse_vectors in collection config
- Upsert: vector includes both dense (named) and sparse (named)
- Query with prefetch arrays for both vectors, then fusion: 'rrf'
- Sparse computed by SPLADE or BM25-style model, NOT by Qdrant
- prefetch limits should be 5-10x final limit for good fusion candidate pool
- Hybrid search is slower than dense-only, use only when keyword precision matters

Snapshots and backups

Production Qdrant collections need periodic snapshots. Qdrant supports collection-level snapshots that capture the full vector index and payload state.

// Create a snapshot
const snapshot = await qdrant.createSnapshot(DOCUMENTS_COLLECTION);

// Download a snapshot
const snapshotInfo = await qdrant.listSnapshots(DOCUMENTS_COLLECTION);

// Restore from snapshot
await qdrant.recoverFromSnapshot(DOCUMENTS_COLLECTION, {
  location: 'http://your-storage/snapshot.tar',
  priority: 'snapshot',
});

For production deployments, snapshots should run on a schedule (daily for small collections, hourly for high-churn collections) and ship to durable object storage (S3, GCS) separate from the Qdrant nodes themselves.

Common Claude Code mistakes with Qdrant

Six patterns Claude generates incorrectly without CLAUDE.md constraints, with the correct replacement for each.

1. Hardcoded vector dimension

Claude generates: vectors: { size: 1536, distance: 'Cosine' } with a dimension that does not match the current embedding model.

Correct pattern: vectors: { size: EMBEDDING_DIMENSIONS, distance: EMBEDDING_DISTANCE } with constants imported from a single source.

2. Missing payload indexes

Claude generates: collection creation followed by search calls with filters, with no createPayloadIndex() step.

Correct pattern: createPayloadIndex() for every payload field used in filters, called after collection creation.

3. Missing wait: true

Claude generates: await qdrant.upsert(name, { points }) followed by an immediate search.

Correct pattern: await qdrant.upsert(name, { points, wait: true }) to ensure points are searchable.

4. Null point IDs

Claude generates: points: [{ id: null, vector, payload }] letting Qdrant assign.

Correct pattern: points: [{ id: uuidv4(), vector, payload }] with stable client-generated IDs.

5. on_disk_payload omitted

Claude generates: collection creation with no on_disk_payload flag for collections with large text payloads.

Correct pattern: on_disk_payload: true for any collection with text payloads over 1KB.

6. Wrong distance metric

Claude generates: distance: 'Cosine' regardless of embedding model.

Correct pattern: distance: EMBEDDING_DISTANCE derived from the model's training (Cosine for OpenAI, often Dot for unnormalised Sentence Transformers).

Add a common mistakes section to CLAUDE.md with these six pairs.

Permission hooks for Qdrant scripts

A Qdrant-backed project accumulates scripts: bulk loaders, reindexers, snapshot creators, collection deleters. Some are read-only. Some are destructive. Permission hooks gate the destructive ones.

In .claude/settings.local.json:

{
  "permissions": {
    "allow": [
      "Bash(node scripts/search-test.js*)",
      "Bash(node scripts/count-points.js*)",
      "Bash(node scripts/list-collections.js*)"
    ],
    "deny": [
      "Bash(node scripts/delete-collection.js*)",
      "Bash(node scripts/bulk-delete.js*)",
      "Bash(node scripts/reindex.js*)",
      "Bash(node scripts/restore-snapshot.js*)"
    ]
  }
}

Reading collections and running search tests are safe operations. Deleting collections, bulk-deleting points, reindexing, and restoring from snapshots can destroy production data. The deny list forces Claude to surface those operations as prompts rather than running them as part of an automated workflow.

Building Qdrant integrations that stay accurate at scale

The Qdrant CLAUDE.md in this guide produces vector-search integrations where the collection configuration matches the embedding model exactly, payload indexes exist for every filter field before production traffic, HNSW parameters are tuned for the dataset size and dimensionality, upserts are durable before reads, and the search calls use the right combination of hnsw_ef, exact, and with_payload for the access pattern.

The underlying principle is the same as any database integration with Claude Code. Qdrant without a CLAUDE.md produces code that works in development with a tiny collection and degrades silently as the data grows: searches that return wrong results because the distance metric is wrong for the embedding model, filter queries that time out because the payload fields have no indexes, bulk loads that complete in 10x the expected time because indexing fires on every insert, and points that go missing because dimension mismatches are silently dropped from batches. The CLAUDE.md template removes each failure mode by making the correct pattern the only pattern Claude can generate.

For the next layer of RAG application development, Qdrant works alongside Claude Code with Langfuse for retrieval-step trace instrumentation, and Claudify includes a Qdrant-specific CLAUDE.md template with the embedding-model single-source-of-truth pattern, payload index discipline, HNSW tuning guide, named-vector and sparse-vector patterns, and all six common-mistake rules pre-configured.

Get Claudify. Ship vector search that holds up at production scale.

More like this

Ready to upgrade your Claude Code setup?

Get Claudify
Featured on Dofollow.Tools AI Toolz Dir Claudify - Featured on Startup Fame