Project: RuVector-Postgres SPARQL Extension Date: December 2025 Status: Research Phase
This document outlines the implementation strategy for adding SPARQL query capabilities to RuVector-Postgres, enabling semantic graph queries alongside existing vector search operations.
┌─────────────────────────────────────────────────────────────┐
│ SPARQL Interface │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Query Parser │ │ Query Algebra│ │ SQL Generator│ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ RDF Triple Store Layer │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Triple Store │ │ Indexes │ │ Named Graphs │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ PostgreSQL Layer │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Tables │ │ Indexes │ │ Functions │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────┘
-- Main triple store table
CREATE TABLE ruvector_rdf_triples (
id BIGSERIAL PRIMARY KEY,
-- Subject
subject TEXT NOT NULL,
subject_type VARCHAR(10) NOT NULL CHECK (subject_type IN ('iri', 'bnode')),
-- Predicate (always IRI)
predicate TEXT NOT NULL,
-- Object
object TEXT NOT NULL,
object_type VARCHAR(10) NOT NULL CHECK (object_type IN ('iri', 'literal', 'bnode')),
object_datatype TEXT,
object_language VARCHAR(20),
-- Named graph (NULL = default graph)
graph TEXT,
-- Metadata
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
modified_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Indexes for all access patterns
CREATE INDEX idx_rdf_spo ON ruvector_rdf_triples(subject, predicate, object);
CREATE INDEX idx_rdf_pos ON ruvector_rdf_triples(predicate, object, subject);
CREATE INDEX idx_rdf_osp ON ruvector_rdf_triples(object, subject, predicate);
CREATE INDEX idx_rdf_graph ON ruvector_rdf_triples(graph) WHERE graph IS NOT NULL;
CREATE INDEX idx_rdf_predicate ON ruvector_rdf_triples(predicate);
-- Full-text search on literals
CREATE INDEX idx_rdf_object_text ON ruvector_rdf_triples
USING GIN(to_tsvector('english', object))
WHERE object_type = 'literal';
-- Namespace prefix mapping
CREATE TABLE ruvector_rdf_namespaces (
prefix VARCHAR(50) PRIMARY KEY,
namespace TEXT NOT NULL UNIQUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Named graph metadata
CREATE TABLE ruvector_rdf_graphs (
graph_iri TEXT PRIMARY KEY,
label TEXT,
description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
modified_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);-- RDF term type
CREATE TYPE ruvector_rdf_term AS (
value TEXT,
term_type VARCHAR(10), -- 'iri', 'literal', 'bnode'
datatype TEXT,
language VARCHAR(20)
);
-- SPARQL result binding
CREATE TYPE ruvector_sparql_binding AS (
variable TEXT,
term ruvector_rdf_term
);-- Add a triple
CREATE FUNCTION ruvector_rdf_add_triple(
subject TEXT,
subject_type VARCHAR(10),
predicate TEXT,
object TEXT,
object_type VARCHAR(10),
object_datatype TEXT DEFAULT NULL,
object_language VARCHAR(20) DEFAULT NULL,
graph TEXT DEFAULT NULL
) RETURNS BIGINT;
-- Delete triples matching pattern
CREATE FUNCTION ruvector_rdf_delete_triple(
subject TEXT DEFAULT NULL,
predicate TEXT DEFAULT NULL,
object TEXT DEFAULT NULL,
graph TEXT DEFAULT NULL
) RETURNS INTEGER;
-- Check if triple exists
CREATE FUNCTION ruvector_rdf_has_triple(
subject TEXT,
predicate TEXT,
object TEXT,
graph TEXT DEFAULT NULL
) RETURNS BOOLEAN;
-- Get all triples for subject
CREATE FUNCTION ruvector_rdf_get_triples(
subject TEXT,
graph TEXT DEFAULT NULL
) RETURNS TABLE (
predicate TEXT,
object TEXT,
object_type VARCHAR(10),
object_datatype TEXT,
object_language VARCHAR(20)
);-- Register namespace prefix
CREATE FUNCTION ruvector_rdf_register_prefix(
prefix VARCHAR(50),
namespace TEXT
) RETURNS VOID;
-- Resolve prefixed name to IRI
CREATE FUNCTION ruvector_rdf_expand_prefix(
prefixed_name TEXT
) RETURNS TEXT;
-- Shorten IRI to prefixed name
CREATE FUNCTION ruvector_rdf_compact_iri(
iri TEXT
) RETURNS TEXT;-- Execute SPARQL SELECT query
CREATE FUNCTION ruvector_sparql_query(
query TEXT,
parameters JSONB DEFAULT NULL
) RETURNS TABLE (
bindings JSONB
);
-- Execute SPARQL ASK query
CREATE FUNCTION ruvector_sparql_ask(
query TEXT,
parameters JSONB DEFAULT NULL
) RETURNS BOOLEAN;
-- Execute SPARQL CONSTRUCT query
CREATE FUNCTION ruvector_sparql_construct(
query TEXT,
parameters JSONB DEFAULT NULL
) RETURNS TABLE (
subject TEXT,
predicate TEXT,
object TEXT,
object_type VARCHAR(10)
);
-- Execute SPARQL DESCRIBE query
CREATE FUNCTION ruvector_sparql_describe(
resource TEXT,
graph TEXT DEFAULT NULL
) RETURNS TABLE (
predicate TEXT,
object TEXT,
object_type VARCHAR(10)
);-- Execute SPARQL UPDATE
CREATE FUNCTION ruvector_sparql_update(
update_query TEXT
) RETURNS INTEGER;
-- Bulk insert from N-Triples/Turtle
CREATE FUNCTION ruvector_rdf_load(
data TEXT,
format VARCHAR(20), -- 'ntriples', 'turtle', 'rdfxml'
graph TEXT DEFAULT NULL
) RETURNS INTEGER;SPARQL:
?person foaf:name ?name .
?person foaf:age ?age .SQL:
SELECT
t1.subject AS person,
t1.object AS name,
t2.object AS age
FROM ruvector_rdf_triples t1
JOIN ruvector_rdf_triples t2
ON t1.subject = t2.subject
WHERE t1.predicate = 'http://xmlns.com/foaf/0.1/name'
AND t2.predicate = 'http://xmlns.com/foaf/0.1/age'
AND t1.object_type = 'literal'
AND t2.object_type = 'literal';SPARQL:
?person foaf:name ?name .
OPTIONAL { ?person foaf:email ?email }SQL:
SELECT
t1.subject AS person,
t1.object AS name,
t2.object AS email
FROM ruvector_rdf_triples t1
LEFT JOIN ruvector_rdf_triples t2
ON t1.subject = t2.subject
AND t2.predicate = 'http://xmlns.com/foaf/0.1/email'
WHERE t1.predicate = 'http://xmlns.com/foaf/0.1/name';SPARQL:
{ ?x foaf:name ?name }
UNION
{ ?x rdfs:label ?name }SQL:
SELECT subject AS x, object AS name
FROM ruvector_rdf_triples
WHERE predicate = 'http://xmlns.com/foaf/0.1/name'
UNION ALL
SELECT subject AS x, object AS name
FROM ruvector_rdf_triples
WHERE predicate = 'http://www.w3.org/2000/01/rdf-schema#label';SPARQL:
?person foaf:age ?age .
FILTER(?age >= 18 && ?age < 65)SQL:
SELECT
subject AS person,
object AS age
FROM ruvector_rdf_triples
WHERE predicate = 'http://xmlns.com/foaf/0.1/age'
AND object_type = 'literal'
AND object_datatype = 'http://www.w3.org/2001/XMLSchema#integer'
AND CAST(object AS INTEGER) >= 18
AND CAST(object AS INTEGER) < 65;SPARQL:
?person foaf:knows+ ?friend .SQL (with CTE):
WITH RECURSIVE transitive AS (
-- Base case: direct connections
SELECT subject, object
FROM ruvector_rdf_triples
WHERE predicate = 'http://xmlns.com/foaf/0.1/knows'
UNION
-- Recursive case: follow chains
SELECT t.subject, r.object
FROM ruvector_rdf_triples t
JOIN transitive r ON t.object = r.subject
WHERE t.predicate = 'http://xmlns.com/foaf/0.1/knows'
)
SELECT subject AS person, object AS friend
FROM transitive;SPARQL:
SELECT ?company (COUNT(?employee) AS ?count) (AVG(?salary) AS ?avg)
WHERE {
?employee foaf:workplaceHomepage ?company .
?employee ex:salary ?salary .
}
GROUP BY ?company
HAVING (COUNT(?employee) >= 10)SQL:
SELECT
t1.object AS company,
COUNT(*) AS count,
AVG(CAST(t2.object AS NUMERIC)) AS avg
FROM ruvector_rdf_triples t1
JOIN ruvector_rdf_triples t2
ON t1.subject = t2.subject
WHERE t1.predicate = 'http://xmlns.com/foaf/0.1/workplaceHomepage'
AND t2.predicate = 'http://example.org/salary'
AND t2.object_type = 'literal'
GROUP BY t1.object
HAVING COUNT(*) >= 10;-- Predicate statistics
CREATE TABLE ruvector_rdf_stats (
predicate TEXT PRIMARY KEY,
triple_count BIGINT,
distinct_subjects BIGINT,
distinct_objects BIGINT,
avg_object_length NUMERIC,
last_updated TIMESTAMP
);
-- Update statistics
CREATE FUNCTION ruvector_rdf_update_stats() RETURNS VOID AS $$
BEGIN
DELETE FROM ruvector_rdf_stats;
INSERT INTO ruvector_rdf_stats
SELECT
predicate,
COUNT(*) as triple_count,
COUNT(DISTINCT subject) as distinct_subjects,
COUNT(DISTINCT object) as distinct_objects,
AVG(LENGTH(object)) as avg_object_length,
CURRENT_TIMESTAMP
FROM ruvector_rdf_triples
GROUP BY predicate;
END;
$$ LANGUAGE plpgsql;Use statistics to order joins by selectivity:
- Most selective (fewest results) first
- Predicates with fewer distinct values
- Literal objects before IRI objects
-- Materialize common transitive closures
CREATE MATERIALIZED VIEW ruvector_rdf_knows_closure AS
WITH RECURSIVE transitive AS (
SELECT subject, object, 1 as depth
FROM ruvector_rdf_triples
WHERE predicate = 'http://xmlns.com/foaf/0.1/knows'
UNION
SELECT t.subject, r.object, r.depth + 1
FROM ruvector_rdf_triples t
JOIN transitive r ON t.object = r.subject
WHERE t.predicate = 'http://xmlns.com/foaf/0.1/knows'
AND r.depth < 10 -- Limit depth
)
SELECT * FROM transitive;
CREATE INDEX idx_knows_closure_so ON ruvector_rdf_knows_closure(subject, object);-- Query cache
CREATE TABLE ruvector_sparql_cache (
query_hash TEXT PRIMARY KEY,
query TEXT,
plan JSONB,
result JSONB,
created_at TIMESTAMP,
hit_count INTEGER DEFAULT 0,
avg_exec_time INTERVAL
);-- Function to combine SPARQL with vector similarity
CREATE FUNCTION ruvector_sparql_vector_search(
sparql_query TEXT,
embedding_predicate TEXT,
query_vector ruvector,
similarity_threshold FLOAT,
top_k INTEGER
) RETURNS TABLE (
subject TEXT,
bindings JSONB,
similarity FLOAT
);Example Usage:
-- Find similar people based on semantic description
SELECT * FROM ruvector_sparql_vector_search(
'SELECT ?person ?name ?interests
WHERE {
?person foaf:name ?name .
?person ex:interests ?interests .
?person ex:embedding ?embedding .
}',
'http://example.org/embedding',
'[0.15, 0.25, ...]'::ruvector,
0.8,
10
);-- Store both RDF triples and embeddings
INSERT INTO ruvector_rdf_triples (subject, predicate, object, object_type)
VALUES
('http://example.org/alice', 'http://xmlns.com/foaf/0.1/name', 'Alice', 'literal'),
('http://example.org/alice', 'http://xmlns.com/foaf/0.1/age', '30', 'literal');
-- Add vector embedding using RuVector
CREATE TABLE person_embeddings (
person_iri TEXT PRIMARY KEY,
embedding ruvector(384)
);
INSERT INTO person_embeddings VALUES
('http://example.org/alice', '[0.1, 0.2, ...]'::ruvector);
-- Query combining both
SELECT
r.subject AS person,
r.object AS name,
v.embedding <=> $1::ruvector AS similarity
FROM ruvector_rdf_triples r
JOIN person_embeddings v ON r.subject = v.person_iri
WHERE r.predicate = 'http://xmlns.com/foaf/0.1/name'
AND v.embedding <=> $1::ruvector < 0.5
ORDER BY similarity
LIMIT 10;Support for SERVICE keyword to query remote endpoints:
CREATE FUNCTION ruvector_sparql_federated_query(
query TEXT,
remote_endpoints JSONB
) RETURNS TABLE (bindings JSONB);-- SPARQL query with full-text search
CREATE FUNCTION ruvector_sparql_text_search(
search_term TEXT,
language TEXT DEFAULT 'english'
) RETURNS TABLE (
subject TEXT,
predicate TEXT,
object TEXT,
rank FLOAT
);-- Spatial predicates
CREATE FUNCTION ruvector_geo_within(
point1 GEOMETRY,
point2 GEOMETRY,
distance_meters FLOAT
) RETURNS BOOLEAN;-- Simple RDFS entailment
CREATE FUNCTION ruvector_rdf_infer_rdfs() RETURNS INTEGER;
-- Materialize inferred triples
CREATE TABLE ruvector_rdf_inferred (
LIKE ruvector_rdf_triples INCLUDING ALL,
inference_rule TEXT
);- Design and implement triple store schema
- Create basic RDF manipulation functions
- Implement namespace management
- Build indexes for all access patterns
- SPARQL 1.1 query parser (using Rust crate like
sparql-grammar) - Parse PREFIX declarations
- Parse SELECT, ASK, CONSTRUCT, DESCRIBE queries
- Parse WHERE clauses with BGP, OPTIONAL, UNION, FILTER
- Translate parsed queries to SPARQL algebra
- Implement BGP, Join, LeftJoin, Union, Filter operators
- Handle property paths
- Support subqueries
- Translate algebra to PostgreSQL SQL
- Optimize join ordering using statistics
- Generate CTEs for property paths
- Handle aggregates and solution modifiers
- Execute generated SQL
- Format results as JSON/XML/CSV/TSV
- Implement result streaming for large datasets
- Add query timeout and resource limits
- Implement INSERT DATA, DELETE DATA
- Implement DELETE/INSERT with WHERE
- Implement LOAD, CLEAR, CREATE, DROP
- Transaction support for updates
- Query result caching
- Statistics-based query planning
- Materialized property path views
- Prepared statement support
- Hybrid SPARQL + vector similarity queries
- Semantic search with knowledge graphs
- Vector embeddings in RDF
- Combined ranking (semantic + vector)
- Unit tests for all components
- Integration tests with W3C SPARQL test suite
- Performance benchmarks
- User documentation and examples
-- Test basic triple insertion
DO $$
DECLARE
triple_id BIGINT;
BEGIN
triple_id := ruvector_rdf_add_triple(
'http://example.org/alice',
'iri',
'http://xmlns.com/foaf/0.1/name',
'Alice',
'literal'
);
ASSERT triple_id IS NOT NULL, 'Triple insertion failed';
END $$;Implement tests from:
- SPARQL 1.1 Query Test Cases
- SPARQL 1.1 Update Test Cases
- Property Path Test Cases
-- Benchmark query execution time
CREATE FUNCTION benchmark_sparql_query(
query TEXT,
iterations INTEGER DEFAULT 100
) RETURNS TABLE (
avg_time INTERVAL,
min_time INTERVAL,
max_time INTERVAL,
stddev_time INTERVAL
);docs/research/sparql/
├── SPARQL_SPECIFICATION.md # Complete SPARQL 1.1 spec
├── IMPLEMENTATION_GUIDE.md # This document
├── API_REFERENCE.md # SQL function reference
├── EXAMPLES.md # Usage examples
├── PERFORMANCE_TUNING.md # Optimization guide
└── MIGRATION_GUIDE.md # Migration from other triple stores
| Operation | Target | Notes |
|---|---|---|
| Simple BGP (3 patterns) | < 10ms | With proper indexes |
| Complex query (joins + filters) | < 100ms | 1M triples |
| Property path (depth 5) | < 500ms | 1M triples |
| Aggregate query | < 200ms | GROUP BY over 100K groups |
| INSERT DATA (1000 triples) | < 100ms | Bulk insert |
| DELETE/INSERT (pattern) | < 500ms | Affects 10K triples |
- SQL Injection Prevention: Parameterized queries only
- Resource Limits: Query timeout, memory limits
- Access Control: Row-level security on triple store
- Audit Logging: Log all UPDATE operations
- Rate Limiting: Prevent DoS via complex queries
sparql-parseroroxigraph- SPARQL parsingpgrx- PostgreSQL extension frameworkserde_json- JSON serializationregex- FILTER regex support
plpgsql- Procedural languagepg_trgm- Trigram text searchbtree_gin/btree_gist- Advanced indexing
- SPARQL 1.2 Support: When specification is finalized
- SHACL Validation: Shape constraint language
- GraphQL Interface: Map GraphQL to SPARQL
- Streaming Updates: Real-time triple stream processing
- Distributed Queries: Federate across multiple databases
- Machine Learning: Train embeddings from knowledge graph
- SPARQL Specification Document
- RuVector PostgreSQL Extension
- W3C SPARQL 1.1 Test Suite
- Apache Jena Documentation
- Oxigraph Implementation
Status: Research Complete - Ready for Implementation
Next Steps:
- Review implementation guide with team
- Create GitHub issues for each phase
- Set up development environment
- Begin Phase 1 implementation