Skip to main content

Documentation

Vector Search

AI-powered vector similarity search

Vector Search

Find similar data in milliseconds: [0.1, 0.2, 0.3] β†’ Top K Matches

πŸ“š Table of Contents


Implementation note

Stackhouse's vector search (stackhouse/src/storage/vectors.rs, mounted at /v1/vectors) is a REST proxy in front of an external Qdrant instance β€” Stackhouse does not implement its own HNSW index in-process. Distance computation, indexing, and ANN search all happen inside Qdrant; Stackhouse stores the vector column config and forwards requests. There is also an unused, unwired brute-force cosine-similarity implementation in stackhouse/src/ai/vector_collections.rs (over a Postgres JSONB column) that is not reachable from any route β€” don't confuse it with the live Qdrant-backed path described below.

🎯 Concepts

Traditional Keyword Search

Query: "apple"

Results:

  • Apple Inc. (company)
  • apple (fruit)
  • Apple Records (music)

❌ Doesn't understand meaning

Vector Semantic Search

Query: "tech company founded by jobs" β†’ [0.23, -0.45, 0.67, ...]

Results:

  • Apple Inc. (96% similarity) βœ“
  • Microsoft (89% similarity)
  • Google (85% similarity)

βœ… Understands semantic meaning

How It Works

Rendering diagram…

HNSW Algorithm Visualized

Rendering diagram…

Search process:

  1. Start at Layer 2 (entry point)
  2. Greedy search to find closest point
  3. Move to Layer 1, repeat
  4. Move to Layer 0, refine search
  5. Return nearest neighbors

Complexity: O(log n) vs O(n) for brute force


πŸš€ Getting Started

Step 1: Generate Embeddings

First, you need an embedding model. Here are popular options:

# Option 1: sentence-transformers (Python)
from sentence_transformers import SentenceTransformer

model = SentenceTransformer('all-MiniLM-L6-v2')
text = "The quick brown fox jumps over the lazy dog"
embedding = model.encode(text)

print(embedding.shape)  # (384,)
print(embedding[:5])    # [0.23, -0.45, 0.67, 0.12, -0.34]
// Option 2: OpenAI API (Node.js)
const openai = require('openai');

async function getEmbedding(text) {
  const response = await openai.embeddings.create({
    model: "text-embedding-3-small",
    input: text
  });
  return response.data[0].embedding;
}
# Option 3: Use a pre-computed embedding service
curl https://api.embeddings.com/v1/embed \
  -H "Content-Type: application/json" \
  -d '{"text": "Your text here"}'

Step 2: Insert Vectors

# Upsert a vector into the "documents" collection
curl -X POST http://localhost:3000/v1/vectors/documents/upsert \
  -H "Content-Type: application/json" \
  -d '{
    "id": "doc1",
    "embedding": [0.23, -0.45, 0.67, 0.12, -0.34, ...],
    "data": {
      "title": "Introduction to Stackhouse",
      "content": "Stackhouse is a schema-later database...",
      "category": "database",
      "url": "https://stackhouse.dev/intro"
    }
  }'

Response (201 Created):

{
  "success": true,
  "data": { "id": "doc1", "collection": "documents", "dimensions": 5 },
  "message": "Vector upserted successfully"
}

id is optional β€” omit it to get an auto-generated UUID. column defaults to "embedding" and only needs to be set if a collection stores more than one named vector per point.

Step 3: Search for Similar Vectors

curl -X POST http://localhost:3000/v1/vectors/documents/search \
  -H "Content-Type: application/json" \
  -d '{
    "vector": [0.25, -0.43, 0.65, 0.10, -0.30, ...],
    "top_k": 10,
    "metric": "cosine"
  }'

Response:

{
  "success": true,
  "count": 3,
  "collection": "documents",
  "metric": "cosine",
  "data": [
    { "id": "doc1", "similarity": 0.88, "data": { "title": "Introduction to Stackhouse", "category": "database" } },
    { "id": "doc5", "similarity": 0.77, "data": { "title": "Getting Started with Databases", "category": "database" } },
    { "id": "doc12", "similarity": 0.66, "data": { "title": "Python Programming Guide", "category": "programming" } }
  ]
}

πŸ“– API Reference

All routes are mounted under /v1/vectors (stackhouse/src/storage/vectors.rs, create_vector_router). There is no list-all-collections or delete-by-id endpoint β€” only the four routes below exist.

Upsert Vector

POST /v1/vectors/:collection/upsert

Request Body:

{
  "id": "string",             // Optional: omit for an auto-generated UUID
  "embedding": [float, ...],  // Required: the vector
  "data": {...},              // Optional: payload stored alongside the vector
  "column": "embedding"       // Optional: named vector column (default: "embedding")
}

Example:

curl -X POST http://localhost:3000/v1/vectors/products/upsert \
  -H "Content-Type: application/json" \
  -d '{
    "id": "prod_12345",
    "embedding": [0.12, 0.34, -0.56, ...],
    "data": {
      "name": "Wireless Headphones",
      "price": 99.99,
      "category": "Electronics"
    }
  }'

Batch Upsert

POST /v1/vectors/:collection/batch

Request Body:

{ "records": [ { "id": "...", "embedding": [...], "data": {...} }, ... ] }

Returns {"success": true, "data": {"ids": [...], "collection": "...", "count": N}}. Errors with 400 if records is empty.

Search Vectors

POST /v1/vectors/:collection/search

Request Body:

{
  "vector": [float, ...],   // Required: query vector
  "top_k": 10,               // Optional (default: 10)
  "metric": "cosine",        // Optional: "cosine" | "l2" | "inner_product" (default: "cosine")
  "filters": {...},          // Optional: metadata filter, forwarded to Qdrant
  "column": "embedding"      // Optional: named vector column (default: "embedding")
}

Response:

{
  "success": true,
  "count": 1,
  "collection": "products",
  "metric": "cosine",
  "data": [
    { "id": "prod_12345", "similarity": 0.95, "data": { "name": "Wireless Headphones", "price": 99.99 } }
  ]
}

Collection Info

GET /v1/vectors/:collection/info

Returns {"success": true, "data": [...], "collection": "..."} with per-vector-column metadata (table, column, dimensions, index_type, row_count).


πŸ“ Distance Metrics

Set via the metric field on a search request (DistanceMetric in stackhouse/src/storage/vectors.rs); Qdrant performs the actual computation. Three metrics are supported: cosine (default), l2 (Euclidean), and inner_product (alias dot).

Cosine Similarity (Default)

Measures the angle between two vectors. Range: [-1, 1]

  • 1 = Identical direction
  • 0 = Orthogonal (uncorrelated)
  • -1 = Opposite direction

Formula:

similarity = (A Β· B) / (|A| Γ— |B|)
distance = 1 - similarity

Best for:

  • βœ… Semantic similarity
  • βœ… Text embeddings
  • βœ… Recommendation systems

Example: A = [1, 0, 0], B = [1, 0, 0] β†’ Similarity = 1.0 (same direction), Distance = 0.0

Euclidean Distance

Measures straight-line distance. Range: [0, ∞)

  • 0 = Identical
  • Larger = More different

Formula:

distance = √Σ(Aᡒ - Bᡒ)²

Best for:

  • βœ… Geometric data
  • βœ… Image embeddings
  • βœ… Physical coordinates

Example: A = [0, 0], B = [3, 4] β†’ Distance = 5.0 (Pythagorean theorem)

Choosing the Right Metric

Use Cosine Similarity when:

βœ… Comparing text/documents βœ… Magnitude doesn't matter βœ… Using semantic embeddings

Use Euclidean Distance when:

βœ… Physical distance matters βœ… Working with coordinates βœ… Image feature vectors

Comparison: A = [1, 2, 3], B = [2, 4, 6] (A Γ— 2) β†’ Cosine: 0 (same direction), Euclidean: 3.74 (different)


⚑ Performance

Benchmarks

There is no bundled benchmark suite for this path (see Benchmarks for what Stackhouse does measure β€” it does not currently include vector search). Search performance and index-build time are governed entirely by the external Qdrant deployment's own HNSW implementation, its configured ef_construct/m parameters, and hardware β€” not by anything in Stackhouse's code β€” so no specific latency/recall numbers are quoted here. Consult Qdrant's own published benchmarks for representative figures, and measure against your own Qdrant deployment before relying on any number for capacity planning.

Optimization Tips

1. Vector Dimensionality

DimensionsTrade-off
128-384Fast, good for text
768-1024Better accuracy
1536+Best quality, slower

2. Index Size

  • More vectors = Better accuracy, slower search
  • Consider sharding for >10M vectors

3. K Value

  • Small K (5-10): Fast
  • Large K (50-100): More comprehensive

4. Batch Insertions

for (const doc of documents) {
  await insertVector(doc);
}

// BETTER:
await insertVectorBatch(docs);

πŸ’‘ Use Cases

import requests

STACKHOUSE_URL = "http://localhost:8080"

# Index documents
documents = [
    {
        "id": "doc1",
        "text": "Stackhouse is a schema-later database",
        "vector": encode("Stackhouse is a schema-later database")
    },
    {
        "id": "doc2",
        "text": "Python is a programming language",
        "vector": encode("Python is a programming language")
    }
]

# Insert vectors
for doc in documents:
    requests.post(
        f"{STACKHOUSE_URL}/v1/vectors/docs/upsert",
        json={
            "id": doc["id"],
            "embedding": doc["vector"],
            "data": {"text": doc["text"]}
        }
    )

# Semantic search
query = "database that adapts to my data"
query_vector = encode(query)

response = requests.post(
    f"{STACKHOUSE_URL}/v1/vectors/docs/search",
    json={"vector": query_vector, "top_k": 5}
)

print(response.json())
# Returns: {"success": true, "data": [{"id": "doc1", "similarity": 0.85, ...}], ...}

2. Product Recommendations

// Find similar products
async function recommendProducts(productId) {
  // Get product vector
  const product = await getVector('products', productId);

  // Search for similar products
  const response = await fetch(
    'http://localhost:3000/v1/vectors/products/search',
    {
      method: 'POST',
      headers: {'Content-Type': 'application/json'},
      body: JSON.stringify({
        vector: product.vector,
        top_k: 10
      })
    }
  );

  const results = await response.json();

  // Filter out the same product
  return results.data.filter(r => r.id !== productId);
}
from PIL import Image
import torchvision.models as models
import torchvision.transforms as transforms

# Load pre-trained ResNet
resnet = models.resnet50(pretrained=True)
resnet.eval()

# Transform and extract features
transform = transforms.Compose([
    transforms.Resize(256),
    transforms.CenterCrop(224),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406],
                       std=[0.229, 0.224, 0.225])
])

def extract_features(image_path):
    image = Image.open(image_path)
    image = transform(image).unsqueeze(0)
    with torch.no_grad():
        features = resnet(image)
    return features.flatten().tolist()

# Index images
for img_path in glob("images/*.jpg"):
    features = extract_features(img_path)
    requests.post(
        f"{STACKHOUSE_URL}/v1/vectors/images/upsert",
        json={
            "id": img_path,
            "embedding": features,
            "data": {"path": img_path}
        }
    )

# Search similar images
query_features = extract_features("query.jpg")
response = requests.post(
    f"{STACKHOUSE_URL}/v1/vectors/images/search",
    json={"vector": query_features, "top_k": 10}
)

4. RAG (Retrieval Augmented Generation)

import openai

def rag_query(question):
    # 1. Encode question
    question_vector = encode(question)

    # 2. Retrieve relevant documents
    response = requests.post(
        f"{STACKHOUSE_URL}/v1/vectors/knowledge_base/search",
        json={"vector": question_vector, "top_k": 5}
    )

    context = "\n".join([
        r["data"]["text"]
        for r in response.json()["data"]
    ])

    # 3. Generate answer with context
    completion = openai.ChatCompletion.create(
        model="gpt-4",
        messages=[
            {"role": "system", "content": "Answer using this context:\n" + context},
            {"role": "user", "content": question}
        ]
    )

    return completion.choices[0].message.content

πŸŽ“ Best Practices

1. Embedding Model Selection

ModelDimSpeedQuality
all-MiniLM-L6-v2384⚑⚑⚑⭐⭐⭐
all-mpnet-base-v2768⚑⚑⭐⭐⭐⭐
text-embedding-3-small1536⚑⭐⭐⭐⭐⭐
text-embedding-3-large3072⚑⭐⭐⭐⭐⭐

Recommendations:

  • Start with all-MiniLM-L6-v2 (fast, good enough)
  • Upgrade to OpenAI for production
  • Use consistent model across all data

2. Index Organization

# βœ… GOOD: Separate indexes by use case
/v1/vectors/documents     # Text search
/v1/vectors/products      # Product recommendations
/v1/vectors/users         # User similarity

# ❌ BAD: Everything in one index
/v1/vectors/everything    # Harder to manage

3. Metadata Design

# βœ… GOOD: Rich payload data for filtering
{
  "id": "doc123",
  "embedding": [...],
  "data": {
    "title": "...",
    "category": "tech",
    "created_at": "2025-01-03",
    "author": "alice",
    "tags": ["database", "rust", "performance"]
  }
}

# ❌ BAD: Minimal payload data
{
  "id": "doc123",
  "embedding": [...],
  "data": {"title": "..."}
}

πŸ“š Further Reading


Ready to add AI to your app? Continue to JavaScript Functions πŸš€