-
Notifications
You must be signed in to change notification settings - Fork 97
Description
🐛 Bug Report: HNSW Index Segmentation Fault
Severity: Critical (P0)
Status: Open
Affects: ruvector-postgres v0.1.0 (latest from Docker Hub)
Reporter: Mark Allen, NexaDental CTO
Date: 2026-01-28
Executive Summary
HNSW indexes on ruvector(384) columns cause PostgreSQL to crash with a segmentation fault when executing similarity queries with parameterized query vectors. The crash is 100% reproducible with production datasets (tested with 6,975 rows).
Key finding: The error message "HNSW: Could not extract query vector, using zeros" indicates the HNSW extension cannot parse query parameters, falls back to a zero vector, then crashes during the search operation.
Environment
Software Versions
- Docker Image:
ruvnet/ruvector-postgres:latest(pulled 2026-01-27) - PostgreSQL: 17.7 (Debian 17.7-3.pgdg12+1)
- ruvector Extension: v0.1.0
- Host: Linux (Azure DevContainers/Codespaces)
Database Schema
```sql
CREATE TABLE kb_chunks (
id SERIAL PRIMARY KEY,
document_id INTEGER NOT NULL,
content TEXT NOT NULL,
chunk_index INTEGER NOT NULL,
embedding ruvector(384), -- THIS COLUMN
metadata JSONB,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- HNSW Index (causes crash)
CREATE INDEX idx_kb_chunks_embedding_hnsw
ON kb_chunks
USING hnsw (embedding ruvector_cosine_ops)
WITH (m = 16, ef_construction = 64);
```
Dataset Characteristics
- Total rows: 6,975 chunks
- Embeddings: 100% populated (no NULL values)
- Dimensions: 384 per embedding
- Data type:
ruvector(384) - Source: Production knowledge base embeddings
Reproduction Steps
1. Create Table and Index
```sql
CREATE TABLE kb_chunks (
id SERIAL PRIMARY KEY,
content TEXT NOT NULL,
embedding ruvector(384)
);
CREATE INDEX idx_kb_chunks_embedding_hnsw
ON kb_chunks
USING hnsw (embedding ruvector_cosine_ops)
WITH (m = 16, ef_construction = 64);
```
2. Insert Sample Data
```sql
INSERT INTO kb_chunks (content, embedding) VALUES
('Sample text 1', '[-0.0390654,0.047992118,0.050985366,...]'::ruvector(384)),
('Sample text 2', '[0.012345,0.067890,0.034567,...]'::ruvector(384));
-- Repeat for multiple rows
```
3. Execute Query with Parameter
```sql
-- Query that causes crash
SELECT
id,
content,
1 - (embedding <=> $1::ruvector(384)) as similarity
FROM kb_chunks
WHERE embedding IS NOT NULL
ORDER BY embedding <=> $1::ruvector(384)
LIMIT 10;
-- Execute with parameter
-- Parameters: ["[-0.0390654,0.047992118,...]"]
```
4. Observe Crash
```
WARNING: HNSW: Could not extract query vector, using zeros
WARNING: HNSW v2: Bitmap scans not supported for k-NN queries
server closed the connection unexpectedly
This probably means the server terminated abnormally
before or while processing the request.
The connection to the server was lost. Attempting reset: Failed.
ERROR: server process (PID XXXXX) terminated by signal 11: Segmentation fault
```
Error Analysis
Error Sequence
- Query submitted with parameterized ruvector
- HNSW attempts to parse query vector from parameter → FAILS
- Falls back to zero vector:
"Could not extract query vector, using zeros" - HNSW search executes with invalid zero vector
- Segmentation fault → PostgreSQL crashes
Key Error Messages
Warning 1: Query Vector Parsing Failure
```
WARNING: HNSW: Could not extract query vector, using zeros
```
- HNSW extension fails to extract query vector from PostgreSQL parameter binding
- Falls back to zero vector (invalid for hyperbolic space)
- Zero vector causes crash in HNSW search algorithm
Warning 2: Bitmap Scan (Informational)
```
WARNING: HNSW v2: Bitmap scans not supported for k-NN queries
```
- Expected behavior, not related to crash
Fatal Error: Segmentation Fault
```
ERROR: server process terminated by signal 11: Segmentation fault
```
- Hard crash due to memory access violation
- Likely during HNSW search with invalid zero vector
- No graceful error handling
What Works vs. What Doesn't
✅ Working Scenarios
1. Sequential Scan (No HNSW Index)
```sql
-- Drop HNSW index
DROP INDEX idx_kb_chunks_embedding_hnsw;
-- Convert to real[] and use hyperbolic distance
ALTER TABLE kb_chunks ALTER COLUMN embedding TYPE real[];
SELECT id, content,
ruvector_poincare_distance(embedding, $1::real[], -1.0) as distance
FROM kb_chunks
ORDER BY distance ASC
LIMIT 10;
-- Result: ✅ Works perfectly (~1.6s for 6,975 rows)
```
2. HNSW on Empty Table
```sql
-- Works on tables with 0 rows (no actual search)
CREATE INDEX idx_empty_table_hnsw
ON empty_table
USING hnsw (embedding ruvector_cosine_ops);
-- Result: ✅ No crash (never searches)
```
❌ Failing Scenarios
1. HNSW with Parameterized Query (Main Bug)
```sql
SELECT id, 1 - (embedding <=> $1::ruvector(384)) as similarity
FROM kb_chunks
ORDER BY embedding <=> $1::ruvector(384)
LIMIT 10;
-- Parameters: ["[-0.039,0.048,...]"]
-- Result: ❌ Segmentation fault (100% reproducible)
```
2. HNSW with Prepared Statements
```sql
PREPARE search AS
SELECT id FROM kb_chunks
ORDER BY embedding <=> $1
LIMIT 10;
EXECUTE search('[-0.039,...]'::ruvector(384));
-- Result: ❌ Segmentation fault
```
3. HNSW with Application Drivers (Node.js pg)
```javascript
const result = await pool.query(
'SELECT id FROM kb_chunks ORDER BY embedding <=> $1 LIMIT 10',
['[-0.039,0.048,...]']
);
// Result: ❌ Segmentation fault
```
Parameter Format Testing
All tested parameter formats cause crashes:
| Format | Example | Result |
|---|---|---|
| String with brackets | "[-0.039,0.048,...]" |
❌ Crash |
| String with cast | "[-0.039,...]"::ruvector(384) |
❌ Crash |
| Direct literal | '[-0.039,...]'::ruvector(384) |
❌ Crash |
| Array format | '{-0.039,0.048,...}' |
❌ Parse error |
Conclusion: HNSW extension cannot parse ANY parameterized query vector format.
Root Cause Hypothesis
Most Likely: Parameter Binding Bug in HNSW Extension
Problem: The HNSW extension's query vector extraction logic doesn't handle PostgreSQL's parameter binding mechanism correctly.
Evidence:
- Error message explicitly states parsing failure
- Works with literal values (no parameters)
- Crashes with ALL parameterized formats
- PostgreSQL 17.7 parameter binding works fine elsewhere
Hypothetical code location:
```c
// In HNSW extension source (pseudocode)
Vector extractQueryVector(Datum param) {
// BUG: This parsing logic fails for bound parameters
char* str = DatumGetCString(param);
if (!parseRuvectorString(str)) {
elog(WARNING, "HNSW: Could not extract query vector, using zeros");
return createZeroVector(); // Invalid for hyperbolic space!
}
}
```
Why it crashes:
- Zero vector is invalid in hyperbolic/Poincaré space
- Causes division by zero or NaN in distance calculations
- HNSW search doesn't validate query vector
- Results in memory access violation (segfault)
Impact Assessment
Production Impact: HIGH
- Current state: Cannot use HNSW indexes on ruvector columns in production
- Performance loss: 10-15x slower queries (1.6s vs 100-200ms with HNSW)
- Workaround required: Sequential scans with hyperbolic distance
- Scale limitations: Performance degrades linearly with dataset size
Use Case Impact
| Use Case | Impact | Workaround Viability |
|---|---|---|
| Semantic search (< 10k vectors) | Medium | Sequential scan acceptable (1-2s) |
| Semantic search (10k-100k vectors) | High | Sequential scan too slow (10-20s) |
| Real-time applications | Critical | Cannot meet latency requirements |
| Batch processing | Low | Can tolerate slower queries |
Why This Blocks Production Adoption
Modern applications use parameterized queries:
- ORMs use parameters - Prisma, TypeORM, Django ORM, etc.
- Prepared statements are standard - Best practice for SQL injection prevention
- Application drivers use parameters - pg (Node.js), psycopg (Python), etc.
Bottom line: Without parameter support, HNSW indexes are unusable in real applications.
Requested Fix
Short-Term (Critical)
-
Fix parameter binding in HNSW extension
- Properly extract query vector from PostgreSQL bound parameters
- Use
DatumGetRuvector()or equivalent for proper type handling - Support standard PostgreSQL parameter binding mechanisms
-
Add validation to prevent crashes
```c
if (isZeroVector(queryVector) || !isValidVector(queryVector)) {
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("Invalid query vector for HNSW search")));
}
``` -
Replace segfaults with graceful errors
- Catch memory access violations
- Return PostgreSQL ERROR instead of crashing
- Log detailed debugging information
-
Add tests for parameterized queries
- Test prepared statements
- Test pg/psycopg parameter binding
- Test various query vector formats
Long-Term (Enhancement)
-
Improve parameter parsing robustness
- Support multiple input formats (brackets, braces, arrays)
- Auto-detect and convert parameter types
- Better error messages for malformed inputs
-
Add comprehensive query validation
- Validate query vector dimensions match index
- Validate vector is not zero/invalid for hyperbolic space
- Validate vector norm is within acceptable range
-
Performance optimizations
- Cache parsed query vectors
- Optimize parameter extraction path
- Reduce overhead of type conversions
Workaround (Implemented)
Current production workaround:
```sql
-- Drop HNSW indexes
DROP INDEX IF EXISTS idx_kb_chunks_embedding_hnsw;
-- Revert to real[] type
ALTER TABLE kb_chunks
ALTER COLUMN embedding TYPE real[]
USING ('{' || substring(embedding::text from 2 for length(embedding::text) - 2) || '}')::real[];
-- Use hyperbolic distance with sequential scan
SELECT
c.id,
c.content,
ruvector_poincare_distance(c.embedding, $1::real[], -1.0) as distance
FROM kb_chunks c
WHERE c.embedding IS NOT NULL
ORDER BY distance ASC
LIMIT $2;
-- Performance: ~1.6s P95 for 6,975 rows
```
Workaround effectiveness:
- ✅ 100% stable (no crashes)
- ✅ Acceptable performance for current dataset size
⚠️ Will not scale beyond ~50k vectors- ❌ Loses 10-15x performance potential
Additional Information
Contact Information
- Reporter: Mark Allen, CTO @ NexaDental
- Email: mark@nexadental.com
- GitHub: @markallenthornton
Available for:
- Providing additional debug information
- Testing patches quickly (CI/CD ready)
- Sharing anonymized production data
- Video call to demonstrate issue
- Contributing to documentation
Suggested Investigation Paths
- Review parameter extraction code in HNSW extension
- Compare with pgvector's parameter handling (reference implementation)
- Add debug logging to track parameter flow through extension
- Test with various PostgreSQL client libraries
- Review memory management around query vector lifecycle
- Add unit tests for parameter binding scenarios
Why We Care
ruvector-postgres is an excellent extension with powerful features (hyperbolic embeddings, HNSW indexes, SIMD optimization). This bug is the only blocker preventing production adoption at scale. The NexaDental team is committed to helping resolve this and ensuring ruvector-postgres succeeds in the ecosystem.
We're happy to:
- Test patches quickly (have reproducible test case)
- Provide anonymized production data for testing
- Contribute documentation improvements
- Fund bounties for critical fixes (if that helps accelerate resolution)
Technical Details for Debugging
Reproducible Test Case
```sql
-- Minimal reproduction (works with even 1 row)
CREATE TABLE test_hnsw (
id SERIAL PRIMARY KEY,
embedding ruvector(384)
);
CREATE INDEX test_hnsw_idx
ON test_hnsw
USING hnsw (embedding ruvector_cosine_ops);
INSERT INTO test_hnsw (embedding)
VALUES ('[-0.039,0.048,0.051,<...384 dimensions...>]'::ruvector(384));
-- This crashes:
SELECT * FROM test_hnsw
ORDER BY embedding <=> $1::ruvector(384)
LIMIT 1;
-- Parameter: ["[-0.039,0.048,0.051,...]"]
```
Expected Behavior
Query should:
- Extract query vector from parameter
$1 - Cast to
ruvector(384)type - Execute HNSW approximate nearest neighbor search
- Return top-k results ordered by cosine distance
Actual Behavior
- Parameter extraction fails
- Falls back to zero vector
- HNSW search with zero vector causes segmentation fault
- PostgreSQL process crashes
Priority: P0 (Critical) - Complete blocker for production HNSW usage
Thank you for creating and maintaining ruvector-postgres! 🙏
Last Updated: 2026-01-28
Full Bug Report: Available at request