Documentation
Vector Search
AI-powered vector similarity search
Vector Search
π AI-Native Similarity 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
What is Vector Search?
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
HNSW Algorithm Visualized
Search process:
- Start at Layer 2 (entry point)
- Greedy search to find closest point
- Move to Layer 1, repeat
- Move to Layer 0, refine search
- 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/upsertRequest 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/batchRequest 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/searchRequest 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/infoReturns {"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 direction0= Orthogonal (uncorrelated)-1= Opposite direction
Formula:
similarity = (A Β· B) / (|A| Γ |B|)
distance = 1 - similarityBest 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
| Dimensions | Trade-off |
|---|---|
| 128-384 | Fast, good for text |
| 768-1024 | Better 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
1. Semantic Document Search
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);
}3. Image Similarity Search
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
| Model | Dim | Speed | Quality |
|---|---|---|---|
| all-MiniLM-L6-v2 | 384 | β‘β‘β‘ | βββ |
| all-mpnet-base-v2 | 768 | β‘β‘ | ββββ |
| text-embedding-3-small | 1536 | β‘ | βββββ |
| text-embedding-3-large | 3072 | β‘ | βββββ |
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 manage3. 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
- JavaScript Functions - Process vectors with custom logic
- Realtime - Live vector updates
- Performance Guide - Optimize vector operations
Ready to add AI to your app? Continue to JavaScript Functions π