Vector Embeddings and Hybrid Search

Status: Available since v0.48.0 (VEC-01)
Requires: pgvector extension (required). An OpenAI-compatible embedding API is required for automatic embedding generation; manual float4[] insertion works without it.
SQL: pg_ripple.store_embedding(), pg_ripple.vector_search(), pg_ripple.hybrid_search()
HTTP: POST /sparql (supports pg:similar() inside SPARQL SELECT)
Degraded: Without pgvector, all embedding storage and pg:similar() queries fail at query time.


Vector search and graph queries answer different questions. Vector search is good at "things that look like X"; graph queries are good at "things in this exact relationship to X". Real-world questions usually need both — "prescriptions semantically similar to ibuprofen, taken by patients in the cardiology cohort". pg_ripple does both in one query.

This chapter is the practical reference. It covers:

  • Storing vector embeddings alongside RDF entities.
  • Building HNSW indexes with pgvector.
  • Running pure similarity, pure SPARQL, and hybrid (RRF) search.
  • The pg:similar() SPARQL function — vector search inside a SPARQL pattern.
  • Federating to external vector stores (Weaviate, Qdrant, Pinecone, remote pgvector).

For higher-level decision-making, start with AI Overview. For an end-to-end RAG pipeline, see RAG Pipeline.


Setup

-- 1. pgvector is required.
CREATE EXTENSION IF NOT EXISTS vector;

-- 2. Point pg_ripple at an OpenAI-compatible embedding API.
ALTER SYSTEM SET pg_ripple.embedding_api_url      = 'https://api.openai.com/v1';
ALTER SYSTEM SET pg_ripple.embedding_api_key_env  = 'OPENAI_API_KEY';
ALTER SYSTEM SET pg_ripple.embedding_model        = 'text-embedding-3-small';
ALTER SYSTEM SET pg_ripple.embedding_dimensions   = 1536;
SELECT pg_reload_conf();

API keys are read from the named environment variable at call time and never stored in the database.


Embedding entities

Every entity that has an rdfs:label (or skos:prefLabel, or any of the configured label predicates) can be embedded:

-- Bulk-embed every labelled entity.
SELECT pg_ripple.embed_entities();

-- Or embed only one named graph.
SELECT pg_ripple.embed_entities('https://example.org/products');

-- Manually store a precomputed vector.
SELECT pg_ripple.store_embedding(
    iri    := '<https://example.org/aspirin>',
    vec    := '[...1536 floats...]'::vector,
    model  := 'text-embedding-3-small'
);

Set pg_ripple.use_graph_context = on to enrich each entity's embedding input with its 1-hop graph neighbourhood — this dramatically improves recall on entities whose labels alone are ambiguous ("Apple" the company vs the fruit).

Set pg_ripple.auto_embed = on to enqueue any newly-inserted labelled entity for background embedding. The merge-worker drains the queue.


The three search modes

1. Pure similarity

SELECT entity_iri, similarity
FROM pg_ripple.similar_entities('headache medication', k := 10);

Equivalent to the cosine-distance HNSW lookup you would write by hand against _pg_ripple.embeddings, but with text-to-vector handled for you.

2. Pure SPARQL

SELECT * FROM pg_ripple.sparql($$
    SELECT ?drug WHERE {
        ?drug a <https://example.org/Drug> ;
              <https://example.org/treats> <https://example.org/headache> .
    }
$$);

3. Hybrid (Reciprocal Rank Fusion)

Hybrid search returns the fused ranking of (a) the SPARQL query's top-k matches and (b) the vector query's top-k matches, using Reciprocal Rank Fusion. This catches both the exact relational matches and the fuzzy semantic neighbours.

SELECT entity_iri, score
FROM pg_ripple.hybrid_search(
    sparql := 'SELECT ?d WHERE { ?d a <https://example.org/Drug> }',
    text   := 'headache medication',
    k      := 10,
    alpha  := 0.5  -- 0.0 = pure SPARQL, 1.0 = pure vector
);

alpha is the relative weight given to the vector ranking. Tune by inspection; 0.5 is a sensible default.


pg:similar() — vector search inside SPARQL

The pg:similar() SPARQL function returns the cosine similarity between an entity and a free-text query. It is callable in BIND, FILTER, and ORDER BY.

PREFIX pg: <http://pg-ripple.io/fn/>

SELECT ?drug ?score WHERE {
    ?drug a <https://example.org/Drug> .
    BIND(pg:similar(?drug, "anti-inflammatory") AS ?score)
    FILTER(?score > 0.7)
}
ORDER BY DESC(?score)
LIMIT 20

This is the most expressive form — graph constraints and similarity score live in the same query plan, with no client-side post-processing.


Vector federation — Weaviate, Qdrant, Pinecone, remote pgvector

If you already operate a vector store and do not want to migrate, register it as a vector federation endpoint. hybrid_search() will blend results from local pg_ripple and the remote service.

SELECT pg_ripple.register_vector_endpoint(
    url      := 'https://qdrant.internal:6333',
    api_type := 'qdrant'
);

SELECT pg_ripple.register_vector_endpoint('https://weaviate.internal:8080', 'weaviate');
SELECT pg_ripple.register_vector_endpoint('https://my-index.pinecone.io', 'pinecone');

Supported api_type values: pgvector, weaviate, qdrant, pinecone. Endpoints are stored in _pg_ripple.vector_endpoints and register_vector_endpoint() is idempotent.

A federated hybrid_search() call automatically fans out, gathers per-endpoint top-k results, and re-fuses them.


GUCs at a glance

GUCDefaultPurpose
pg_ripple.pgvector_enabledonMaster switch. When off, vector functions emit a WARNING and return zero rows.
pg_ripple.embedding_api_urlemptyOpenAI-compatible endpoint base URL
pg_ripple.embedding_api_key_envPG_RIPPLE_EMBEDDING_API_KEYEnv var name for the API key
pg_ripple.embedding_modeltext-embedding-3-smallDefault model
pg_ripple.embedding_dimensions1536Vector size
pg_ripple.embedding_batch_size100API batch size for embed_entities()
pg_ripple.use_graph_contextoffInclude 1-hop neighbours in embedding input
pg_ripple.auto_embedoffBackground-embed newly-inserted entities

Tuning HNSW

The HNSW index built by pg_ripple is a standard pgvector index on _pg_ripple.embeddings(vector vector_cosine_ops). Tune it as you would any pgvector index:

SettingDefaultEffect
m (build)16Higher = better recall, more memory
ef_construction (build)64Higher = better recall, slower build
hnsw.ef_search (query)40Higher = better recall, slower queries

See Vector Index Trade-offs for measured benchmarks across these knobs.


Graceful degradation

Every vector function in pg_ripple checks for pgvector at call time. If pgvector is missing, or pgvector_enabled = off, the function emits one WARNING and returns zero rows. Your application code can call vector functions in environments (CI, dev) that do not have pgvector without crashing.


See also

Further reading