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.chatInstalls into whichever agent you are using.
Cursor Rules
Cursor IDE rules (v2)
Quality Score
Category
CommunicationSupported Platforms
Skill content
View source on GitHubcategory: 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
AstrBot
40.8kAI Agent Assistant & development framework that integrates lots of IM platforms, LLMs, plugins and AI feature, and can be your openclaw alternative. ✨
cmux
27.3kOpen source Ghostty-based macOS terminal with vertical tabs and notifications for AI coding agents. Built for multitasking, organization, and programmability.
context7
62.3kContext7 Platform -- Up-to-date code documentation for LLMs and AI code editors
lemonade
5.8kLemonade helps users discover and run local AI apps by serving optimized LLMs right from their own GPUs and NPUs. Join our discord: https://discord.gg/5xXzkMu8Zk
Security Score
Audited on Invalid Date
