SkillAgentSearch skills...

vector-search

AI-powered chat application built with Next.js, Convex, AI SDK, and modern web technologies. Features real-time messaging, PWA support, documentation with Fumadocs, and enterprise-ready architecture with Turborepo and Ultracite.

Install / Use

npx skills add tonyoconnell/anubis.chat

Installs into whichever agent you are using.

About this skill
📐

Cursor Rules

Cursor IDE rules (v2)

Quality Score

70/100

Supported Platforms

Cursor

category: backend subcategory: vector-search tags: [qdrant, vectors, embeddings, semantic-search, rag] cursor: context_window: 8192 temperature: 0.5 max_tokens: 4096 model_preference: ["auto"] relations: imports: ["../ai-rag/embeddings.mdc", "./database-schema.mdc"] exports: ["vector-operations", "similarity-search", "hybrid-search"] references: ["./convex-patterns.mdc", "./api-design.mdc"]

Vector Search & Qdrant Integration - v1.9+ Best Practices

Core Architecture Principles

Hybrid Search Strategy: Combine dense vectors (semantic) with sparse vectors (keyword) for optimal results Multi-Tenant Isolation: Strict wallet-based data separation using collection filters Performance Optimization: 97% RAM reduction through quantization and smart indexing Scalable Infrastructure: Horizontal scaling through sharding and replication

Qdrant Client Configuration

Production-Ready Setup

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

export class QdrantService {
  private client: QdrantClient;
  
  constructor() {
    this.client = new QdrantClient({
      url: process.env.QDRANT_URL!,
      apiKey: process.env.QDRANT_API_KEY,
      timeout: 30000, // 30 second timeout
      retries: 3,
    });
  }
  
  // Collection management with proper indexing
  async initializeCollections() {
    const collections = [
      {
        name: 'message_embeddings',
        config: {
          vectors: {
            size: 1536, // OpenAI embedding dimension
            distance: 'Cosine',
            on_disk: true, // Store vectors on disk for large datasets
          },
          optimizers_config: {
            deleted_threshold: 0.2,
            vacuum_min_vector_number: 1000,
            default_segment_number: 0,
            max_segment_size: 20000,
            memmap_threshold: 10000,
            indexing_threshold: 20000,
            payload_indexing_threshold: 10000,
          },
          replication_factor: 2, // High availability
          write_consistency_factor: 1,
          quantization_config: {
            scalar: {
              type: 'int8',
              quantile: 0.99,
              always_ram: true,
            },
          },
        },
      },
      {
        name: 'document_embeddings',
        config: {
          vectors: {
            dense: {
              size: 1536,
              distance: 'Cosine',
              on_disk: true,
            },
            sparse: {
              distance: 'Dot',
              on_disk: false, // Keep sparse vectors in RAM for speed
            },
          },
          // Hybrid search configuration
          quantization_config: {
            binary: {
              always_ram: true,
            },
          },
        },
      },
    ];
    
    for (const { name, config } of collections) {
      try {
        await this.client.getCollection(name);
      } catch {
        await this.client.createCollection(name, config);
        await this.createPayloadIndexes(name);
      }
    }
  }
  
  // Create indexes for wallet isolation and filtering
  private async createPayloadIndexes(collectionName: string) {
    const indexes = [
      { field: 'wallet_address', schema_type: 'keyword' },
      { field: 'chat_id', schema_type: 'keyword' },
      { field: 'message_role', schema_type: 'keyword' },
      { field: 'timestamp', schema_type: 'integer' },
      { field: 'document_type', schema_type: 'keyword' },
      { field: 'is_active', schema_type: 'bool' },
    ];
    
    for (const index of indexes) {
      await this.client.createPayloadIndex(collectionName, index);
    }
  }
}

Message Embedding Operations

Store Chat Message Embeddings

// ✅ Wallet-isolated message embedding storage
export const storeMessageEmbedding = action({
  args: {
    messageId: v.id("messages"),
    walletAddress: v.string(),
    embedding: v.array(v.number()),
    sparseVector: v.optional(v.object({
      indices: v.array(v.number()),
      values: v.array(v.number()),
    })),
  },
  handler: async (ctx, args) => {
    try {
      // Validate message ownership
      const message = await ctx.runQuery(api.messages.getMessage, {
        messageId: args.messageId,
        walletAddress: args.walletAddress,
      });
      
      if (!message) {
        throw new Error("Message not found or access denied");
      }
      
      const qdrant = new QdrantService();
      
      // Prepare point data with comprehensive metadata
      const pointData = {
        id: args.messageId,
        vector: args.sparseVector 
          ? {
              dense: args.embedding,
              sparse: args.sparseVector,
            }
          : args.embedding,
        payload: {
          wallet_address: args.walletAddress,
          chat_id: message.chatId,
          message_role: message.role,
          content: message.content.substring(0, 1000), // Truncate for indexing
          timestamp: message.timestamp,
          token_count: message.tokenCount || 0,
          is_active: true,
          created_at: Date.now(),
        },
      };
      
      // Store in Qdrant with retry logic
      await qdrant.client.upsert('message_embeddings', {
        wait: true,
        points: [pointData],
      });
      
      // Update Convex record with embedding
      await ctx.runMutation(api.messages.updateEmbedding, {
        messageId: args.messageId,
        embedding: args.embedding,
      });
      
      return { success: true };
      
    } catch (error) {
      console.error("Failed to store message embedding:", error);
      throw new Error("Failed to store embedding");
    }
  },
});

Semantic Search for Chat Context

// ✅ Context-aware semantic search for RAG
export const searchSimilarMessages = action({
  args: {
    walletAddress: v.string(),
    query: v.string(),
    chatId: v.optional(v.id("chats")),
    limit: v.optional(v.number()),
    threshold: v.optional(v.number()),
  },
  handler: async (ctx, { walletAddress, query, chatId, limit = 10, threshold = 0.7 }) => {
    try {
      // Generate query embedding
      const queryEmbedding = await generateEmbedding(query);
      
      const qdrant = new QdrantService();
      
      // Build search filters for wallet isolation
      const filters = {
        must: [
          { key: 'wallet_address', match: { value: walletAddress } },
          { key: 'is_active', match: { value: true } },
        ],
      };
      
      // Add chat-specific filter if provided
      if (chatId) {
        filters.must.push({
          key: 'chat_id',
          match: { value: chatId },
        });
      }
      
      // Perform vector search
      const searchResult = await qdrant.client.search('message_embeddings', {
        vector: queryEmbedding,
        filter: filters,
        limit,
        score_threshold: threshold,
        with_payload: true,
        with_vectors: false, // Don't return vectors to save bandwidth
      });
      
      // Enrich results with full message data from Convex
      const enrichedResults = await Promise.all(
        searchResult.map(async (result) => {
          const message = await ctx.runQuery(api.messages.getMessage, {
            messageId: result.id as Id<"messages">,
            walletAddress,
          });
          
          return {
            messageId: result.id,
            score: result.score,
            content: message?.content || result.payload?.content,
            role: message?.role || result.payload?.message_role,
            timestamp: message?.timestamp || result.payload?.timestamp,
            chatId: message?.chatId || result.payload?.chat_id,
          };
        })
      );
      
      return {
        results: enrichedResults,
        query,
        totalFound: searchResult.length,
      };
      
    } catch (error) {
      console.error("Semantic search failed:", error);
      throw new Error("Search failed");
    }
  },
});

Document RAG Integration

Document Chunk Embeddings

// ✅ Store document chunks with hybrid vectors
export const storeDocumentChunks = action({
  args: {
    documentId: v.id("documents"),
    walletAddress: v.string(),
    chunks: v.array(v.object({
      content: v.string(),
      chunkIndex: v.number(),
      startOffset: v.number(),
      endOffset: v.number(),
      metadata: v.optional(v.any()),
    })),
  },
  handler: async (ctx, args) => {
    try {
      // Validate document ownership
      const document = await ctx.runQuery(api.documents.getDocument, {
        documentId: args.documentId,
        walletAddress: args.walletAddress,
      });
      
      if (!document) {
        throw new Error("Document not found or access denied");
      }
      
      const qdrant = new QdrantService();
      const batchSize = 100; // Process in batches
      
      for (let i = 0; i < args.chunks.length; i += batchSize) {
        const batch = args.chunks.slice(i, i + batchSize);
        
        // Generate embeddings for batch
        const embeddings = await Promise.all(
          batch.map(chunk => generateHybridEmbedding(chunk.content))
        );
        
        // Prepare points for Qdrant
        const points = batch.map((chunk, idx) => {
          const chunkId = `${args.documentId}_chunk_${chunk.chunkIndex}`;
          
          return {
            id: chunkId,
            vector: {
              dense: embeddings[idx].dense,
              sparse: embeddings[idx].sparse,
            },
            payload: {
              wallet_address: args.walletAddress,
              document_id: args.documentId,
              chunk_index: chunk.chunkIndex,
              document_type: document.type,
              document_title: document.title,
              content: chunk.content,
              start_offset: chunk.startOffset,
              end_offset: chunk.endOffset,
              metadata: chunk.metadata || {},
              timestamp: Date.now(),
              is_active: true,
            },
          };
        });
        
        // Batch upsert to Qdrant
        await qdrant.client.upsert('document_embeddings', {
          wait: true,
          points,
        });
        
        // Store chunks in Convex
        const chunkIds = await Promise.all(
          batch.map(chunk => ctx.runMutation(api.chunks.createChunk, {
            documentId: args.documentId,
            walletAddress: args.walletAddress,
            content: chunk.content,
            chunkIndex: chunk.chunkIndex,
            startOffset: chunk.startOffset,
            endOffset: chunk.endOffset,
            embedding: embeddings[batch.indexOf(chunk)].dense,
          }))
        );
      }
      
      return { success: true, chunksProcessed: args.chunks.length };
      
    } catch (error) {
      console.error("Failed to store document chunks:", error);
      throw new Error("Failed to process document chunks");
    }
  },
});

Hybrid RAG Search

// ✅ Advanced hybrid search combining dense + sparse vectors
export const hybridRAGSearch = action({
  args: {
    walletAddress: v.string(),
    query: v.string(),
    documentTypes: v.optional(v.array(v.string())),
    limit: v.optional(v.number()),
    alpha: v.optional(v.number()), // Dense/sparse weight balance
  },
  handler: async (ctx, { walletAddress, query, documentTypes, limit = 20, alpha = 0.7 }) => {
    try {
      const qdrant = new QdrantService();
      
      // Generate hybrid query vectors
      const hybridQuery = await generateHybridEmbedding(query);
      
      // Build comprehensive filters
      const filters = {
        must: [
          { key: 'wallet_address', match: { value: walletAddress } },
          { key: 'is_active', match: { value: true } },
        ],
      };
      
      // Add document type filters if specified
      if (documentTypes && documentTypes.length > 0) {
        filters.must.push({
          k

Truncated for display — read the full file on GitHub.

Related Skills

View on GitHub
GitHub Stars0
CategoryCommunication
UpdatedNaNy ago
Forks0

Security Score

68/100

Audited on Invalid Date

2 medium1 low