FlureeLabs

Vector Search

Vector search enables similarity search using embedding vectors, supporting use cases like:

  • Semantic search: Find similar meanings, not just keywords
  • Recommendations: Find similar products, content, users
  • Image search: Find similar images by visual features
  • Anomaly detection: Find unusual patterns

Fluree supports two complementary approaches:

  1. Inline similarity functions -- compute dotProduct, cosineSimilarity, or euclideanDistance directly in queries using bind. No external index required.
  2. HNSW vector indexes -- build dedicated approximate-nearest-neighbor (ANN) indexes for large-scale similarity search using the f:* query pattern.

The @vector Datatype

Why a dedicated datatype?

In RDF, a plain JSON array like [0.5, 0.5, 0.0] is decomposed into individual values. Duplicate elements can be deduplicated, and ordering is not guaranteed. This breaks embedding vectors. The @vector datatype tells Fluree to store the array as a single, ordered, fixed-length vector.

@vector is a shorthand for the full IRI https://ns.flur.ee/db#embeddingVector, which can also be written as f:embeddingVector when the Fluree namespace prefix is declared in your @context.

Storage: f32 precision contract

All @vector values are stored as IEEE-754 binary32 (f32) arrays. This means:

  • Each element in your JSON array is quantized to f32 at ingest time
  • Values that are not representable as finite f32 (NaN, Infinity, values exceeding f32 range) are rejected
  • Round-trip reads return the f32-quantized values (e.g., 0.1 in JSON becomes 0.10000000149011612 after f32 quantization)
  • This provides a compact, cache-friendly representation optimized for SIMD similarity computation

If you need higher precision (f64) or different vector formats (sparse, integer), store them as a custom RDF datatype string.

Inserting vectors (JSON-LD)

Use "@type": "@vector" to annotate a numeric array as a vector:

{
  "@context": {
    "ex": "http://example.org/"
  },
  "@graph": [
    {
      "@id": "ex:doc1",
      "@type": "ex:Document",
      "ex:embedding": {
        "@value": [0.1, 0.2, 0.3, 0.4],
        "@type": "@vector"
      }
    }
  ]
}

You can also use the full IRI or the f: prefix form, which is equivalent:

{
  "@context": {
    "ex": "http://example.org/",
    "f": "https://ns.flur.ee/db#"
  },
  "@graph": [
    {
      "@id": "ex:doc1",
      "ex:embedding": {
        "@value": [0.1, 0.2, 0.3, 0.4],
        "@type": "f:embeddingVector"
      }
    }
  ]
}

Incorrect -- plain array (will not work for similarity):

{
  "@id": "ex:doc1",
  "ex:embedding": [0.1, 0.2, 0.3, 0.4]
}

Plain arrays are decomposed into individual RDF values where duplicates may be removed and order is lost.

Inserting vectors (Turtle / SPARQL UPDATE)

In Turtle and SPARQL UPDATE, the @vector shorthand is not available. Use the f:embeddingVector datatype IRI with the standard ^^ typed-literal syntax:

PREFIX ex: <http://example.org/>
PREFIX f: <https://ns.flur.ee/db#>

INSERT DATA {
  ex:doc1 ex:embedding "[0.1, 0.2, 0.3, 0.4]"^^f:embeddingVector .
}

The vector is represented as a JSON array string with the ^^f:embeddingVector datatype annotation.

Multiple vectors per entity

An entity can have multiple vectors on the same property:

{
  "@id": "ex:doc1",
  "ex:embedding": [
    {"@value": [0.1, 0.9], "@type": "@vector"},
    {"@value": [0.2, 0.8], "@type": "@vector"}
  ]
}

Each vector produces separate rows in query results.

Vector literals in query VALUES clauses

When passing a vector literal in a query values clause, use the full IRI or the f: prefix form -- the @vector shorthand is only resolved in the transaction parser:

"values": [
  ["?queryVec"],
  [{"@value": [0.7, 0.6], "@type": "f:embeddingVector"}]
]

Or with the full IRI:

"values": [
  ["?queryVec"],
  [{"@value": [0.7, 0.6], "@type": "https://ns.flur.ee/db#embeddingVector"}]
]

Inline Similarity Functions (JSON-LD Query)

Fluree provides three vector similarity functions that can be used in bind expressions within JSON-LD queries. These compute similarity scores directly during query execution without requiring a pre-built index.

Function names are case-insensitive; dotProduct, dotproduct, and dot_product are all equivalent.

dotProduct

Computes the dot product (inner product) of two vectors. Higher scores indicate greater similarity when vectors represent aligned directions.

{
  "@context": {
    "ex": "http://example.org/ns/",
    "f": "https://ns.flur.ee/db#"
  },
  "select": ["?doc", "?score"],
  "values": [
    ["?queryVec"],
    [{"@value": [0.7, 0.6], "@type": "f:embeddingVector"}]
  ],
  "where": [
    {"@id": "?doc", "ex:embedding": "?vec"},
    ["bind", "?score", "(dotProduct ?vec ?queryVec)"]
  ],
  "orderBy": [["desc", "?score"]],
  "limit": 10
}

Score range: (-inf, +inf). Best when vector magnitude encodes importance.

cosineSimilarity

Computes the cosine of the angle between two vectors. Ignores magnitude, focusing purely on directional similarity.

{
  "@context": {
    "ex": "http://example.org/ns/",
    "f": "https://ns.flur.ee/db#"
  },
  "select": ["?doc", "?score"],
  "values": [
    ["?queryVec"],
    [{"@value": [0.7, 0.6], "@type": "f:embeddingVector"}]
  ],
  "where": [
    {"@id": "?doc", "ex:embedding": "?vec"},
    ["bind", "?score", "(cosineSimilarity ?vec ?queryVec)"]
  ],
  "orderBy": [["desc", "?score"]],
  "limit": 10
}

Score range: [-1, 1] (1 = identical direction, 0 = orthogonal, -1 = opposite). Returns null if either vector has zero magnitude. Best for text embeddings and normalized vectors.

euclideanDistance

Computes the L2 (straight-line) distance between two vectors. Lower scores indicate greater similarity.

{
  "@context": {
    "ex": "http://example.org/ns/",
    "f": "https://ns.flur.ee/db#"
  },
  "select": ["?doc", "?distance"],
  "values": [
    ["?queryVec"],
    [{"@value": [0.7, 0.6], "@type": "f:embeddingVector"}]
  ],
  "where": [
    {"@id": "?doc", "ex:embedding": "?vec"},
    ["bind", "?distance", "(euclideanDistance ?vec ?queryVec)"]
  ],
  "orderBy": "?distance",
  "limit": 10
}

Score range: [0, +inf) (0 = identical). Best for geometric similarity and when absolute position matters.

Alternative array syntax

The similarity functions also accept array form instead of the S-expression string:

["bind", "?score", ["dotProduct", "?vec", "?queryVec"]]

This is equivalent to:

["bind", "?score", "(dotProduct ?vec ?queryVec)"]

Filtering by score threshold

Combine bind with filter to return only results above a similarity threshold:

{
  "@context": {
    "ex": "http://example.org/ns/",
    "f": "https://ns.flur.ee/db#"
  },
  "select": ["?doc", "?score"],
  "values": [
    ["?queryVec"],
    [{"@value": [0.7, 0.6], "@type": "f:embeddingVector"}]
  ],
  "where": [
    {"@id": "?doc", "ex:embedding": "?vec"},
    ["bind", "?score", "(dotProduct ?vec ?queryVec)"],
    ["filter", "(> ?score 0.7)"]
  ]
}

Combining with graph patterns

Vector similarity can be combined with standard graph patterns to filter by type, property values, or relationships:

{
  "@context": {
    "ex": "http://example.org/ns/",
    "f": "https://ns.flur.ee/db#"
  },
  "select": ["?doc", "?title", "?score"],
  "values": [
    ["?queryVec"],
    [{"@value": [0.9, 0.1, 0.05], "@type": "f:embeddingVector"}]
  ],
  "where": [
    {"@id": "?doc", "@type": "ex:Article", "ex:title": "?title", "ex:embedding": "?vec"},
    ["bind", "?score", "(cosineSimilarity ?vec ?queryVec)"],
    ["filter", "(> ?score 0.5)"]
  ],
  "orderBy": [["desc", "?score"]],
  "limit": 5
}

Using a stored vector as the query vector

Instead of providing a literal vector, you can use a stored entity's vector:

{
  "@context": {
    "ex": "http://example.org/ns/"
  },
  "select": ["?similar", "?score"],
  "where": [
    {"@id": "ex:reference-doc", "ex:embedding": "?queryVec"},
    {"@id": "?similar", "ex:embedding": "?vec"},
    ["filter", "(!= ?similar ex:reference-doc)"],
    ["bind", "?score", "(cosineSimilarity ?vec ?queryVec)"]
  ],
  "orderBy": [["desc", "?score"]],
  "limit": 10
}

Mixed datatypes

If a property contains both vector and non-vector values, the similarity functions return null for non-vector bindings:

{
  "@graph": [
    {"@id": "ex:a", "ex:data": {"@value": [0.6, 0.5], "@type": "@vector"}},
    {"@id": "ex:b", "ex:data": "Not a vector"}
  ]
}

Querying with dotProduct on ?data will return a numeric score for ex:a and null for ex:b.

SPARQL support

Inline vector similarity functions (dotProduct, cosineSimilarity, euclideanDistance) are available in both JSON-LD Query and SPARQL. In SPARQL, use them as built-in function calls within BIND expressions:

dotProduct (SPARQL)

PREFIX ex: <http://example.org/ns/>
PREFIX f: <https://ns.flur.ee/db#>

SELECT ?doc ?score
WHERE {
  VALUES ?queryVec { "[0.7, 0.6]"^^f:embeddingVector }
  ?doc ex:embedding ?vec ;
       ex:title ?title .
  BIND(dotProduct(?vec, ?queryVec) AS ?score)
}
ORDER BY DESC(?score)
LIMIT 10

cosineSimilarity (SPARQL)

PREFIX ex: <http://example.org/ns/>
PREFIX f: <https://ns.flur.ee/db#>

SELECT ?doc ?score
WHERE {
  VALUES ?queryVec { "[0.88, 0.12, 0.08]"^^f:embeddingVector }
  ?doc a ex:Article ;
       ex:embedding ?vec ;
       ex:title ?title .
  BIND(cosineSimilarity(?vec, ?queryVec) AS ?score)
  FILTER(?score > 0.5)
}
ORDER BY DESC(?score)
LIMIT 5

euclideanDistance (SPARQL)

PREFIX ex: <http://example.org/ns/>
PREFIX f: <https://ns.flur.ee/db#>

SELECT ?doc ?distance
WHERE {
  VALUES ?queryVec { "[0.7, 0.6]"^^f:embeddingVector }
  ?doc ex:embedding ?vec .
  BIND(euclideanDistance(?vec, ?queryVec) AS ?distance)
}
ORDER BY ?distance
LIMIT 10

Vector literals in SPARQL

In SPARQL, vectors are passed as JSON array strings with the ^^f:embeddingVector typed literal syntax:

VALUES ?queryVec { "[0.1, 0.2, 0.3]"^^f:embeddingVector }

Or with the full IRI:

VALUES ?queryVec { "[0.1, 0.2, 0.3]"^^<https://ns.flur.ee/db#embeddingVector> }

Function name variants

Function names are case-insensitive in SPARQL. All of these are equivalent:

  • dotProduct, DOTPRODUCT, dot_product
  • cosineSimilarity, COSINESIMILARITY, cosine_similarity
  • euclideanDistance, EUCLIDEANDISTANCE, euclidean_distance

HNSW Vector Indexes

For large-scale similarity search, Fluree provides dedicated HNSW (Hierarchical Navigable Small World) vector indexes. These are approximate nearest-neighbor (ANN) indexes that trade exact results for dramatically faster query times on large datasets.

Vector indexes are implemented using embedded usearch following the same architecture as BM25:

  • Embedded in-process HNSW indexes (no external service required)
  • Remote mode via dedicated search service (fluree-search-httpd)
  • Snapshot-based persistence with watermarks
  • Incremental sync for efficient updates
  • Feature-gated via vector feature flag

v1 limitation: HNSW vector search is head-only. Time-travel queries (e.g. @t:) are not supported.

Creating Vector Indexes

HTTP/Docker users: there is no HTTP endpoint for creating vector indexes today. Index creation is Rust-API-only. To use HNSW vector search from an HTTP-only deployment, create the index using a Rust program (or the Rust API embedded in your application) against the same storage path your Fluree server reads, then run queries normally via POST /v1/fluree/query.

Rust API

use fluree_db_api::{FlureeBuilder, VectorCreateConfig};
use fluree_db_query::vector::DistanceMetric;

let fluree = FlureeBuilder::memory().build_memory();

// Create indexing query to select documents with embeddings
let indexing_query = json!({
    "@context": { "ex": "http://example.org/" },
    "where": [{ "@id": "?x", "@type": "ex:Document" }],
    "select": { "?x": ["@id", "ex:embedding"] }
});

// Create vector index
let config = VectorCreateConfig::new(
    "doc-embeddings",           // index name
    "mydb:main",                // source ledger
    indexing_query,             // what to index
    "ex:embedding",             // embedding property
    768                         // dimensions
)
.with_metric(DistanceMetric::Cosine);

let result = fluree.create_vector_index(config).await?;
println!("Indexed {} vectors", result.vector_count);

Configuration Options

OptionDescriptionDefault
nameIndex name (creates graph source ID name:branch)Required
ledgerSource ledger ID (name:branch)Required
queryJSON-LD query selecting documentsRequired
embedding_propertyProperty containing embeddingsRequired
dimensionsVector dimensionsRequired
metricDistance metric (Cosine, Dot, Euclidean)Cosine
connectivityHNSW M parameter16
expansion_addefConstruction parameter128
expansion_searchefSearch parameter64

Query Syntax

Vector index search uses the f:* pattern syntax in WHERE clauses:

{
  "@context": {
    "ex": "http://example.org/",
    "f": "https://ns.flur.ee/db#"
  },
  "from": "mydb:main",
  "where": [
    {
      "f:graphSource": "doc-embeddings:main",
      "f:queryVector": [0.1, 0.2, 0.3],
      "f:distanceMetric": "cosine",
      "f:searchLimit": 10,
      "f:searchResult": {
        "f:resultId": "?doc",
        "f:resultScore": "?score"
      }
    }
  ],
  "select": ["?doc", "?score"]
}

Query Parameters

ParameterDescriptionRequired
f:graphSourceVector index aliasYes
f:queryVectorQuery vector (array or variable)Yes
f:distanceMetricDistance metric ("cosine", "dot", "euclidean")No (uses index default)
f:searchLimitMaximum resultsNo
f:searchResultResult binding (variable or object)Yes
f:syncBeforeQueryWait for index sync before queryNo (default: false)
f:timeoutMsQuery timeout in msNo

Result Binding

Simple variable binding:

"f:searchResult": "?doc"

Structured binding with score and ledger:

"f:searchResult": {
  "f:resultId": "?doc",
  "f:resultScore": "?similarity",
  "f:resultLedger": "?source"
}

Variable Query Vectors

Query vector can be a variable bound earlier:

{
  "where": [
    { "@id": "ex:reference-doc", "ex:embedding": "?queryVec" },
    {
      "f:graphSource": "embeddings:main",
      "f:queryVector": "?queryVec",
      "f:searchLimit": 5,
      "f:searchResult": "?similar"
    }
  ]
}

Index Maintenance

Sync Updates

After committing new data, sync the vector index:

let sync_result = fluree.sync_vector_index("doc-embeddings:main").await?;
println!("Upserted: {}, Removed: {}", sync_result.upserted, sync_result.removed);

Full Resync

Rebuild the entire index from scratch:

let resync_result = fluree.resync_vector_index("doc-embeddings:main").await?;

Check Staleness

let check = fluree.check_vector_staleness("doc-embeddings:main").await?;
if check.is_stale {
    println!("Index is {} commits behind", check.commits_behind);
}

Drop Index

fluree.drop_vector_index("doc-embeddings:main").await?;

Distance Metrics

Cosine (Default)

Measures angle between vectors. Best for:

  • Text embeddings (e.g., sentence transformers)
  • Normalized vectors
  • When magnitude doesn't matter

Score range: [-1, 1] (1 = identical, 0 = orthogonal, -1 = opposite)

For unit-normalized vectors, cosine similarity equals dot product. Fluree's SIMD kernels exploit this for faster computation when vectors are pre-normalized.

Dot Product

Measures alignment and magnitude. Best for:

  • Maximum inner product search (MIPS)
  • When vector magnitude encodes importance

Score range: (-inf, +inf)

Euclidean (L2)

Measures straight-line distance. Best for:

  • Geometric similarity
  • Image feature vectors
  • When absolute position matters

Raw score range: [0, +inf). In HNSW index results, normalized to (0, 1] via 1 / (1 + distance).

Note: In HNSW index results (f:* queries), all metrics are normalized to "higher is better". In inline similarity functions, euclideanDistance returns the raw L2 distance (lower = more similar).

Deployment Modes

Vector indexes support two deployment topologies: searching in-process (embedded) or via a dedicated fluree-search-httpd service that mounts the same storage. Both topologies use identical distance-metric computation, score normalization, and snapshot serialization, so results are identical.

Embedded Mode (Default)

The vector index is loaded and searched within the same process as the Fluree server. No additional services. This is the default and is appropriate for most deployments.

Dedicated Search Service

For large indexes or when you want search traffic isolated from the main Fluree process, run the standalone fluree-search-httpd binary on the same storage volume and have your application send vector requests directly to it.

Note: Today, vector search is invoked from a Fluree query (the f:graphSource / f:queryVector pattern) using the embedded path — the main Fluree server does not yet route those queries to a remote service. The dedicated service is reachable directly via its own POST /v1/search API (the same protocol BM25 uses), which is suitable for applications that issue vector queries outside of a Fluree query context. Transparent delegation from inside a Fluree query is a planned follow-up; the wiring is in place but the deployment config is not yet persisted by create_vector_index.

See Remote Search Service for fluree-search-httpd configuration, env vars, the request/response protocol (vector and vector_similar_to query kinds), and Docker deployment.

Performance and Scaling

The importance of binary indexing

Fluree's binary columnar index dramatically accelerates vector queries. Queries against novelty-only (unindexed) data perform a linear scan through the in-memory commit log, while indexed queries read pre-sorted, cache-friendly columnar data. Ensure background indexing is running for production workloads -- the difference is substantial.

The following benchmarks use 768-dimensional vectors (typical for transformer embeddings like sentence-transformers or OpenAI text-embedding-3-small) on Apple M-series hardware:

Novelty-only (no binary index)

ScenarioVectorsQuery timeThroughput
Scan all1,0009.9 ms~101K vec/s
Scan all5,00045.1 ms~111K vec/s
Filtered + score1,000 (75 pass filter)13.5 ms~5.5K vec/s
Filtered + score5,000 (402 pass filter)62.1 ms~6.5K vec/s

With binary index

ScenarioVectorsQuery timeThroughputSpeedup vs novelty
Scan all1,0001.68 ms~595K vec/s5.9x
Scan all5,0007.69 ms~650K vec/s5.9x
Filtered + score1,000 (75 pass filter)533 us~141K vec/s25x
Filtered + score5,000 (402 pass filter)2.40 ms~168K vec/s26x

Key takeaways:

  • Unfiltered scans are ~6x faster with the binary index
  • Filtered queries (where graph patterns reduce the candidate set before scoring) are ~25x faster -- the index enables efficient predicate-first access that avoids loading irrelevant vectors entirely
  • At 5,000 vectors, a filtered indexed query completes in 2.4 ms -- well within interactive latency budgets

Inline similarity functions (flat scan)

  • Best for: Small to medium datasets, ad-hoc similarity queries, prototyping
  • Complexity: O(n) linear scan -- computes similarity against every matching vector
  • Advantage: No index setup required, works immediately after insert
  • SIMD acceleration: Fluree uses runtime-detected SIMD kernels (SSE2/AVX on x86_64, NEON on ARM) for vectorized dot/cosine/L2 computation
  • Normalized embedding optimization: For unit-normalized vectors (most transformer embeddings), cosine similarity reduces to a dot product, avoiding magnitude computation entirely

When to consider HNSW

Inline similarity functions perform a brute-force scan over all candidate vectors. This scales linearly and remains fast for moderate datasets, but at larger scales an HNSW index provides O(log n) approximate nearest-neighbor search.

Rule of thumb:

Vector count (per property)Recommendation
< 100KFlat scan works well, especially with binary indexing. Sub-100ms queries typical.
100K -- 1MStart evaluating HNSW. Flat scan may still be acceptable depending on latency target and hardware, but HNSW will provide more consistent low-latency results.
1M -- 10MHNSW strongly recommended for interactive latency. Flat scan can work if vectors are memory-resident and you can tolerate ~1-2 second queries.
> 10MHNSW (or other ANN index) is the default recommendation. Flat scan becomes I/O- and cache-bound for low-latency use cases.

Factors that shift the crossover:

  • Hardware: Fast NVMe / large RAM pushes the threshold higher; object storage (S3) pulls it lower
  • Latency target: A 50 ms budget favors HNSW earlier than a 2-second budget
  • Filter selectivity: If graph patterns reduce candidates to a small fraction before scoring, flat scan remains viable at higher counts
  • Normalized embeddings: Cosine-as-dot-product is faster, pushing the threshold higher
  • Binary indexing: An indexed dataset scans ~6x faster than novelty-only, effectively raising the flat-scan ceiling

HNSW vector indexes

  • Best for: Large datasets (100K+ vectors), production similarity search with strict latency requirements
  • Complexity: O(log n) approximate nearest neighbor
  • Space: ~1.5x embedding size + IRI mapping overhead
  • Updates: Incremental via affected-subject tracking

Tuning parameters

ParameterEffectTrade-off
connectivity (M)Graph connectivityHigher = better recall, more memory
expansion_add (efConstruction)Build-time search widthHigher = better index quality, slower build
expansion_search (efSearch)Query-time search widthHigher = better recall, slower queries

Feature Flag

The HNSW vector index functionality requires the vector feature:

[dependencies]
fluree-db-api = { version = "0.1", features = ["vector"] }

Inline similarity functions (dotProduct, cosineSimilarity, euclideanDistance) and the @vector datatype are available without feature flags.

Complete Example: Semantic Search

1. Insert documents with embeddings:

{
  "@context": {
    "ex": "http://example.org/",
    "f": "https://ns.flur.ee/db#"
  },
  "@graph": [
    {
      "@id": "ex:doc1",
      "@type": "ex:Article",
      "ex:title": "Introduction to Machine Learning",
      "ex:embedding": {"@value": [0.9, 0.1, 0.05], "@type": "@vector"}
    },
    {
      "@id": "ex:doc2",
      "@type": "ex:Article",
      "ex:title": "Database Design Patterns",
      "ex:embedding": {"@value": [0.1, 0.8, 0.1], "@type": "@vector"}
    },
    {
      "@id": "ex:doc3",
      "@type": "ex:Article",
      "ex:title": "Neural Network Architectures",
      "ex:embedding": {"@value": [0.85, 0.15, 0.1], "@type": "@vector"}
    }
  ]
}

2. Query -- find articles similar to a "machine learning" embedding:

{
  "@context": {
    "ex": "http://example.org/",
    "f": "https://ns.flur.ee/db#"
  },
  "select": ["?title", "?score"],
  "values": [
    ["?queryVec"],
    [{"@value": [0.88, 0.12, 0.08], "@type": "f:embeddingVector"}]
  ],
  "where": [
    {"@id": "?doc", "@type": "ex:Article", "ex:title": "?title", "ex:embedding": "?vec"},
    ["bind", "?score", "(cosineSimilarity ?vec ?queryVec)"]
  ],
  "orderBy": [["desc", "?score"]],
  "limit": 5
}

Expected results (ordered by similarity):

  1. "Introduction to Machine Learning" -- highest cosine similarity
  2. "Neural Network Architectures" -- similar domain
  3. "Database Design Patterns" -- different domain, lower score

Related Documentation