MinhVo

Minh Vo

rss feed

Slaying code & making it lit fr fr 🔥 tagline

Hey there 👋 I'm an AI Engineer with 7 years of experience building scalable web and mobile applications. Currently at Neurond AI (May 2025 — present), architecting an Enterprise AI Assistant Platform with multi-tenant RAG on pgvector, multi-provider LLM orchestration, and Azure-native infrastructure. Previously spent 5+ years at SNAPTEC (Sep 2019 — Apr 2025), leading SaaS themes, admin dashboards, and e-commerce platforms — earned the Hero of the Year award in 2021. I specialize in TypeScript, React, Next.js, and AI-Native engineering with Claude Code and Cursor.bio

Back to blogs

Database Vector Search: pgvector vs Pinecone vs Weaviate

Compare vector databases: performance, cost, filtering, and integration options.

Vector DatabasepgvectorPineconeAI

By MinhVo

Introduction

Vector search has become the backbone of modern AI applications. Semantic search, recommendation engines, retrieval-augmented generation (RAG), anomaly detection, and image similarity all rely on finding the most similar vectors in a high-dimensional space. The explosion of embedding models from OpenAI, Cohere, and open-source alternatives has made vector search a requirement, not a luxury.

But choosing a vector database is a critical architectural decision with lasting implications. The three leading options—pgvector (PostgreSQL extension), Pinecone (managed service), and Weaviate (open-source vector database)—take fundamentally different approaches to performance, filtering, cost, and operational complexity. A 2024 benchmark by Qdrant found that query latency can vary by 100x between implementations for the same dataset and recall target.

This guide provides an honest, detailed comparison based on production workloads, not toy examples, helping you choose the right tool for your specific use case.

Vector database architecture

Understanding Vector Search Fundamentals

How Vector Similarity Works

Vector search finds the nearest neighbors to a query vector in high-dimensional space. The most common distance metrics are:

  • Cosine similarity: Measures the angle between vectors (direction, not magnitude). Best for text embeddings.
  • Euclidean distance (L2): Measures straight-line distance. Best for image embeddings.
  • Inner product (dot product): Combines direction and magnitude. Used when magnitude carries meaning.
-- pgvector: Store and query vectors
CREATE EXTENSION IF NOT EXISTS vector;
 
CREATE TABLE documents (
  id SERIAL PRIMARY KEY,
  content TEXT NOT NULL,
  metadata JSONB DEFAULT '{}',
  embedding vector(1536)  -- OpenAI text-embedding-3-small dimension
);
 
-- Create HNSW index (best for most use cases)
CREATE INDEX ON documents USING hnsw (embedding vector_cosine_ops)
  WITH (m = 16, ef_construction = 200);
 
-- Or IVFFlat index (faster to build, good for large datasets)
CREATE INDEX ON documents USING ivfflat (embedding vector_cosine_ops)
  WITH (lists = 100);
 
-- Query: Find 10 most similar documents
SELECT id, content, metadata,
       1 - (embedding <=> $1::vector) as similarity  -- cosine similarity
FROM documents
ORDER BY embedding <=> $1::vector  -- cosine distance
LIMIT 10;

Index Types Compared

Index TypeBuild TimeQuery SpeedMemoryRecall
HNSWSlowFastestHigh95-99%
IVFFlatFastGoodMedium85-95%
Flat (brute force)NoneSlowestLow100%
Product QuantizationMediumFastLow80-90%

Vector similarity search visualization

Architecture Deep Dive

pgvector adds vector data types and similarity search operators directly to PostgreSQL. It's an extension, not a separate service.

-- Hybrid search: Combine vector similarity with metadata filtering
SELECT id, content,
       1 - (embedding <=> $1::vector) as vector_score,
       ts_rank(to_tsvector('english', content), plainto_tsquery('english', $2)) as text_score
FROM documents
WHERE metadata->>'category' = 'technology'  -- Pre-filter
  AND created_at > '2024-01-01'
ORDER BY
  0.7 * (1 - (embedding <=> $1::vector)) +  -- Vector weight
  0.3 * ts_rank(to_tsvector('english', content), plainto_tsquery('english', $2))  -- Text weight
LIMIT 20;
 
-- Streaming search with partial results
-- pgvector supports iterators for large result sets
// Node.js: pgvector integration
import pg from 'pg';
import { OpenAI } from 'openai';
 
const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL });
const openai = new OpenAI();
 
async function semanticSearch(query: string, limit: number = 10) {
  // Generate embedding for the query
  const embedding = await openai.embeddings.create({
    model: 'text-embedding-3-small',
    input: query,
  });
 
  const queryVector = embedding.data[0].embedding;
 
  // Search with pgvector
  const result = await pool.query(
    `SELECT id, content, metadata,
            1 - (embedding <=> $1::vector) as similarity
     FROM documents
     ORDER BY embedding <=> $1::vector
     LIMIT $2`,
    [JSON.stringify(queryVector), limit]
  );
 
  return result.rows;
}
 
// Batch embedding and storage
async function indexDocuments(documents: Array<{ id: string; content: string }>) {
  const BATCH_SIZE = 100;
 
  for (let i = 0; i < documents.length; i += BATCH_SIZE) {
    const batch = documents.slice(i, i + BATCH_SIZE);
 
    const embeddings = await openai.embeddings.create({
      model: 'text-embedding-3-small',
      input: batch.map(d => d.content),
    });
 
    // Batch update using unnest
    await pool.query(
      `UPDATE documents SET embedding = e.embedding
       FROM unnest($1::int[], $2::vector[]) AS e(id, embedding)
       WHERE documents.id = e.id`,
      [
        batch.map(d => d.id),
        embeddings.data.map(e => JSON.stringify(e.embedding)),
      ]
    );
  }
}

Pinecone: Fully Managed Vector Database

Pinecone is a cloud-native, fully managed vector database that abstracts away all infrastructure concerns.

// Pinecone client
import { Pinecone } from '@pinecone-database/pinecone';
 
const pinecone = new Pinecone({
  apiKey: process.env.PINECONE_API_KEY!,
});
 
const index = pinecone.index('documents');
 
// Upsert vectors
async function upsertDocuments(docs: Array<{ id: string; values: number[]; metadata: any }>) {
  await index.upsert(
    docs.map(doc => ({
      id: doc.id,
      values: doc.values,
      metadata: doc.metadata,
    }))
  );
}
 
// Query with metadata filtering
async function search(queryVector: number[], filter: any, topK: number = 10) {
  const results = await index.query({
    vector: queryVector,
    topK,
    includeMetadata: true,
    filter: {
      category: { $eq: 'technology' },
      created_at: { $gte: '2024-01-01' },
    },
  });
 
  return results.matches;
}
 
// Namespace isolation (multi-tenant)
async function searchTenant(tenantId: string, queryVector: number[]) {
  const ns = index.namespace(tenantId);
  const results = await ns.query({
    vector: queryVector,
    topK: 10,
    includeMetadata: true,
  });
  return results.matches;
}

Weaviate: Open-Source Vector Database

Weaviate is an open-source vector database with built-in vectorization, hybrid search, and a GraphQL API.

// Weaviate client with TypeScript
import weaviate, { WeaviateClient } from 'weaviate-ts-client';
 
const client: WeaviateClient = weaviate.client({
  scheme: 'https',
  host: 'your-cluster.weaviate.network',
  apiKey: new weaviate.ApiKey(process.env.WEAVIATE_API_KEY!),
});
 
// Create schema with automatic vectorization
async function createSchema() {
  await client.schema
    .classCreator()
    .withClass({
      class: 'Document',
      vectorizer: 'text2vec-openai',  // Built-in vectorization
      moduleConfig: {
        'text2vec-openai': {
          model: 'text-embedding-3-small',
        },
      },
      properties: [
        { name: 'content', dataType: ['text'] },
        { name: 'category', dataType: ['text'] },
        { name: 'createdAt', dataType: ['date'] },
      ],
    })
    .do();
}
 
// Hybrid search: Combine vector and keyword search
async function hybridSearch(query: string, category?: string) {
  let queryBuilder = client.graphql
    .get()
    .withClassName('Document')
    .withHybrid({
      query,
      alpha: 0.75,  // 0 = pure keyword, 1 = pure vector
    })
    .withFields('content category createdAt _additional { score }')
    .withLimit(20);
 
  if (category) {
    queryBuilder = queryBuilder.withWhere({
      path: ['category'],
      operator: 'Equal',
      valueText: category,
    });
  }
 
  const result = await queryBuilder.do();
  return result.data.Get.Document;
}
 
// Batch import with automatic vectorization
async function batchImport(documents: Array<{ content: string; category: string }>) {
  let batcher = client.batch.objectsBatcher();
  let counter = 0;
 
  for (const doc of documents) {
    batcher = batcher.withObject({
      class: 'Document',
      properties: {
        content: doc.content,
        category: doc.category,
        createdAt: new Date().toISOString(),
      },
    });
 
    counter++;
    if (counter % 100 === 0) {
      await batcher.do();
      batcher = client.batch.objectsBatcher();
    }
  }
 
  if (counter % 100 !== 0) {
    await batcher.do();
  }
}

Vector database comparison

Real-World Use Cases

Use Case 1: RAG (Retrieval-Augmented Generation) Pipeline

// Complete RAG pipeline with pgvector
import { OpenAI } from 'openai';
 
const openai = new OpenAI();
 
async function ragQuery(question: string): Promise<string> {
  // Step 1: Retrieve relevant documents
  const embedding = await openai.embeddings.create({
    model: 'text-embedding-3-small',
    input: question,
  });
 
  const docs = await pool.query(
    `SELECT content, 1 - (embedding <=> $1::vector) as similarity
     FROM documents
     ORDER BY embedding <=> $1::vector
     LIMIT 5`,
    [JSON.stringify(embedding.data[0].embedding)]
  );
 
  // Step 2: Generate answer with context
  const context = docs.rows.map(d => d.content).join('\n\n');
 
  const completion = await openai.chat.completions.create({
    model: 'gpt-4',
    messages: [
      {
        role: 'system',
        content: `Answer the question based on the following context:\n\n${context}`,
      },
      { role: 'user', content: question },
    ],
  });
 
  return completion.choices[0].message.content!;
}

Use Case 2: E-Commerce Product Recommendations

// Similar product recommendations using Weaviate
async function getSimilarProducts(productId: string, limit: number = 10) {
  const result = await client.graphql
    .get()
    .withClassName('Product')
    .withNearObject({ id: productId })
    .withFields('name price imageUrl category _additional { distance }')
    .withLimit(limit)
    .do();
 
  return result.data.Get.Product;
}
 
// Multi-modal search: Find products similar to an image
async function searchByImage(imageBase64: string) {
  const result = await client.graphql
    .get()
    .withClassName('Product')
    .withNearImage({ image: imageBase64 })
    .withFields('name price imageUrl _additional { distance }')
    .withLimit(20)
    .do();
 
  return result.data.Get.Product;
}

Use Case 3: Document Deduplication at Scale

// Find near-duplicate documents using cosine similarity
async function findDuplicates(threshold: number = 0.95) {
  const duplicates = await pool.query(`
    WITH candidates AS (
      SELECT a.id as id1, b.id as id2,
             1 - (a.embedding <=> b.embedding) as similarity
      FROM documents a
      JOIN documents b ON a.id < b.id
      WHERE 1 - (a.embedding <=> b.embedding) > $1
      ORDER BY similarity DESC
      LIMIT 1000
    )
    SELECT * FROM candidates
  `, [threshold]);
 
  return duplicates.rows;
}

Best Practices for Production

  1. Choose pgvector if you're already on PostgreSQL: No additional infrastructure, same backup/replication, and you can join vector data with relational data in a single query.
  2. Choose Pinecone for simplicity: Zero operational overhead, automatic scaling, and built-in filtering. Best for teams without database expertise.
  3. Choose Weaviate for advanced features: Built-in vectorization, hybrid search, and multi-modal support. Best for complex AI applications.
  4. Use HNSW indexes for most workloads: Best query speed vs recall tradeoff. Use IVFFlat only when HNSW memory is prohibitive.
  5. Batch your embeddings: Generate embeddings in batches of 100-1000. Individual API calls are 10-100x more expensive per embedding.
  6. Pre-filter before vector search: In pgvector, WHERE clauses execute before the vector search, reducing the search space significantly.
  7. Monitor index memory: HNSW indexes can be 2-4x the size of the raw vectors. Plan disk and memory accordingly.
  8. Use appropriate dimensions: text-embedding-3-small (1536d) is sufficient for most text search. Don't use 3072d unless you need the extra precision.

Common Pitfalls and Solutions

PitfallImpactSolution
No index on vectorsFull scan on every query (O(n))Create HNSW or IVFFlat index
Wrong distance metricPoor search qualityUse cosine for text, L2 for images
Too few ef_constructionLow recallIncrease to 200+ for production
Generating embeddings synchronously100ms+ per documentBatch with async processing
Stale embeddings after content updateSearch returns outdated resultsRe-embed on content change

Performance Optimization

-- pgvector performance tuning
SET hnsw.ef_search = 100;  -- Increase for better recall (default 40)
SET ivfflat.probes = 10;   -- Increase for better recall (default 1)
 
-- Monitor index size
SELECT
  schemaname || '.' || tablename as table,
  pg_size_pretty(pg_total_relation_size(schemaname || '.' || tablename)) as total_size,
  pg_size_pretty(pg_relation_size(schemaname || '.' || tablename)) as table_size,
  pg_size_pretty(pg_indexes_size(schemaname || '.' || tablename)) as index_size
FROM pg_tables
WHERE tablename = 'documents';
 
-- Vacuum after bulk operations
VACUUM ANALYZE documents;

Comparison with Alternatives

FeaturepgvectorPineconeWeaviateQdrant
DeploymentSelf-hosted (PG extension)Managed onlySelf-hosted or CloudSelf-hosted or Cloud
Max dimensions2000200006553565535
FilteringSQL WHEREMetadata filterGraphQL wherePayload filter
Hybrid searchManual (SQL)NoBuilt-inBuilt-in
Multi-tenancySchema-basedNamespacesTenant classesCollections
Cost modelInfrastructurePer-query + storageInfrastructure or managedInfrastructure or managed
Backup/RestorePostgreSQL nativeAutomaticManual or CloudManual or Cloud
Best forPG ecosystem teamsSimple managed needsComplex AI appsHigh-performance self-hosted

Testing Strategies

describe('Vector Search Quality', () => {
  test('semantic search returns relevant results', async () => {
    const results = await semanticSearch('how to optimize database queries');
    expect(results.length).toBeGreaterThan(0);
    expect(results[0].similarity).toBeGreaterThan(0.8);
  });
 
  test('hybrid search combines vector and keyword signals', async () => {
    const vectorResults = await semanticSearch('machine learning');
    const hybridResults = await hybridSearch('machine learning');
 
    // Hybrid should be at least as good as pure vector
    expect(hybridResults.length).toBeGreaterThanOrEqual(vectorResults.length);
  });
 
  test('filtered search respects metadata constraints', async () => {
    const results = await semanticSearch('database', { category: 'technology' });
    results.forEach(r => {
      expect(r.metadata.category).toBe('technology');
    });
  });
});

Future Outlook

The vector database space is evolving rapidly. PostgreSQL is adding native vector search improvements (pgvector 0.7+ with better HNSW performance). Pinecone is expanding its serverless offering. Weaviate is adding more built-in vectorization models. The trend is toward hybrid search—combining vector similarity with keyword search and metadata filtering in a single query.

Database Performance Tuning

Database performance tuning is an iterative process that requires understanding your query patterns, data distribution, and access frequencies. Small improvements in query performance can have outsized impacts on application responsiveness, especially for frequently executed queries.

Query Plan Analysis

Always analyze query execution plans before optimizing. In PostgreSQL, use EXPLAIN ANALYZE to see both the planned and actual execution statistics:

EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT u.name, COUNT(o.id) as order_count
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
WHERE u.created_at > '2024-01-01'
GROUP BY u.id, u.name
ORDER BY order_count DESC
LIMIT 20;

Look for sequential scans on large tables (indicating missing indexes), high cost estimates for nested loops (suggesting join strategy issues), and excessive buffer reads (indicating insufficient memory allocation or poor data locality).

Index Optimization Strategy

Creating the right indexes requires understanding your query patterns. Use pg_stat_user_tables and pg_stat_user_indexes to identify tables with high sequential scan ratios and unused indexes:

-- Find tables with high sequential scan ratio
SELECT schemaname, relname, seq_scan, idx_scan,
       CASE WHEN seq_scan + idx_scan > 0
            THEN round(100.0 * idx_scan / (seq_scan + idx_scan), 1)
            ELSE 0 END as idx_usage_pct
FROM pg_stat_user_tables
WHERE seq_scan + idx_scan > 100
ORDER BY idx_usage_pct ASC;
 
-- Find unused indexes consuming disk space
SELECT indexrelname, idx_scan, pg_size_pretty(pg_relation_size(indexrelid))
FROM pg_stat_user_indexes
WHERE idx_scan < 10 AND indexrelname NOT LIKE '%pkey%'
ORDER BY pg_relation_size(indexrelid) DESC;

Connection Pooling Best Practices

Proper connection pooling prevents connection exhaustion under load. Configure your pool size based on your CPU core count and query characteristics:

Optimal pool size = (CPU cores * 2) + effective_spindle_count

For modern SSD-based systems, this typically means a pool size of 2 * CPU_cores + 1. Setting the pool too large wastes memory and increases context switching overhead, while setting it too small causes query queuing under load.

Community Resources and Further Learning

The technology landscape evolves rapidly, making continuous learning essential for maintaining expertise. Building a systematic approach to staying current with developments in your technology stack ensures you can leverage new features and avoid deprecated patterns.

Curated Learning Pathways

Rather than consuming content randomly, create structured learning pathways aligned with your current projects and career goals. Start with official documentation and specification documents, which provide the most accurate and comprehensive information. Follow this with hands-on tutorials and workshops that reinforce concepts through practical application.

Technical blogs from framework maintainers and core team members often provide deeper insights into design decisions and upcoming features. Subscribe to the official blogs of your primary frameworks and libraries to stay ahead of breaking changes and deprecation timelines.

Contributing to Open Source

Contributing to open-source projects in your technology stack provides unparalleled learning opportunities. Start with documentation improvements and bug reports, then progress to fixing small issues tagged as "good first issue" in your favorite projects. This direct engagement with maintainers and the codebase accelerates your understanding far beyond what passive learning can achieve.

# Setting up for contribution
git clone https://github.com/project/repository.git
cd repository
git checkout -b fix/issue-description
 
# Run the project's contribution setup
npm run setup:dev
npm run test  # Ensure tests pass before making changes
 
# Make your changes, then run the full test suite
npm run test:full
npm run lint
npm run build
 
# Submit your contribution
git add -A
git commit -m "fix: description of the fix
 
Closes #1234"
git push origin fix/issue-description

Building a Technical Knowledge Base

Maintain a personal knowledge base that captures insights, solutions, and patterns you discover during your work. Tools like Obsidian, Notion, or even a simple Markdown repository can serve as an external memory that grows more valuable over time.

Organize your notes by topic rather than chronologically, and include code examples, links to relevant documentation, and explanations of why certain approaches work better than others. When you encounter a particularly insightful article or conference talk, write a summary that captures the key takeaways and how they apply to your current projects.

Follow key conferences and their published talks to stay informed about emerging patterns and best practices. Many conferences publish recorded talks on YouTube within weeks of the event, making world-class technical content freely accessible.

Join relevant Discord servers, Slack communities, and forums where practitioners discuss real-world challenges and solutions. These communities provide early warning about emerging issues and access to collective wisdom that isn't available through formal documentation.

Mentorship and Knowledge Sharing

Teaching others is one of the most effective ways to deepen your own understanding. Consider writing technical blog posts, giving talks at local meetups, or mentoring junior developers. The process of explaining concepts to others forces you to organize your knowledge and identify gaps in your understanding.

Pair programming sessions with colleagues of different experience levels create mutual learning opportunities. Senior developers gain fresh perspectives on problems they've solved the same way for years, while junior developers benefit from exposure to production-grade thinking and decision-making processes.

Conclusion

Vector search is essential for modern AI applications. Key takeaways:

  1. pgvector is the pragmatic choice for PostgreSQL teams—no additional infrastructure, full SQL power, and improving performance with each release
  2. Pinecone eliminates operational overhead but limits flexibility and creates vendor lock-in
  3. Weaviate offers the most features (built-in vectorization, hybrid search) but requires more operational expertise
  4. Always benchmark with your actual data and query patterns—generic benchmarks are misleading
  5. Hybrid search (vector + keyword + filter) consistently outperforms pure vector search for real-world applications

Start with pgvector if you're on PostgreSQL—it's good enough for 90% of use cases and avoids the complexity of a separate vector database. Migrate to a specialized solution only when you hit specific performance or feature limitations.