Vector search

near finds the documents whose vector is closest to the one you pass.

A vector<N> field with an @hnsw index holds one embedding a document, and near finds the documents whose vector is closest to the one you pass.

Run in the shell, answers and all; the timings are from one run.

fenec> create collection places (name text, year int @sorted, embed vector<3> @hnsw(cosine));
collection `places` created
fenec> put places [{name: "north", year: 2021, embed: [0, 1, 0]}, {name: "east", year: 2024, embed: [1, 0, 0]}, {name: "up", year: 2024, embed: [0, 0, 1]}, {name: "north-east", year: 2022, embed: [0.7, 0.7, 0]}];
4 records (264.08µs)
fenec> get places select name near embed [0.9, 0.1, 0] limit 2
name        _score
----------  ------
east        0.9939
north-east  0.7809
(2 rows, 90.25µs)
fenec> get places select name where year >= 2024 near embed [0.9, 0.1, 0] limit 2
name  _score
----  ------
east  0.9939
up    0.0000
(2 rows, 199.75µs)
fenec> get places select name near embed [0.9, 0.1, 0] exact limit 1
name  _score
----  ------
east  0.9939
(1 rows, 99.71µs)

The index is @hnsw(cosine), so _score is the cosine similarity: east points where the query does and up is at right angles to it. A where narrows the rows searched and exact checks every vector instead of walking the graph. A real embedding has hundreds of dimensions, vector<768>; three keep the example readable. Words and meaning together, fuse, are under Hybrid search.

The vector clause

near finds the documents whose vector is closest to the one you pass. Add a where to search only the rows that match.

get articles near embed $1 limit 10
get articles where year >= 2024 near embed $1 ef 200 limit 10
get articles near embed $1 exact limit 10
import { connect } from '@fenecdb/web/client'
await db.from('articles').near('embed', [0.9, 0.1, 0]).limit(10).rows();

await db.from('articles')
  .where('year', '>=', 2024)
  .near('embed', [0.9, 0.1, 0], { ef: 200 })
  .limit(10)
  .rows();

await db.from('articles')
  .near('embed', [0.9, 0.1, 0], { exact: true })
  .limit(10)
  .rows();
from fenecdb import Client
db.collection("articles").near("embed", [0.9, 0.1, 0]).limit(10).rows()

(db.collection("articles")
    .where("year", ">=", 2024)
    .near("embed", [0.9, 0.1, 0], ef=200)
    .limit(10)
    .rows())

(db.collection("articles")
    .near("embed", [0.9, 0.1, 0], exact=True)
    .limit(10)
    .rows())
import fenecdb "github.com/fenecdb/fenecdb/integrations/go"
db.From("articles").
	Near("embed", []float64{0.9, 0.1, 0}).
	Limit(10).
	Rows(ctx)

db.From("articles").
	Where("year", ">=", 2024).
	Near("embed", []float64{0.9, 0.1, 0}, fenecdb.Ef(200)).
	Limit(10).
	Rows(ctx)

db.From("articles").
	Near("embed", []float64{0.9, 0.1, 0}, fenecdb.Exact()).
	Limit(10).
	Rows(ctx)
using FenecDb;
await db.From("articles")
    .Near("embed", new[] { 0.9, 0.1, 0 })
    .Limit(10)
    .RowsAsync();

await db.From("articles")
    .Where("year", ">=", 2024)
    .Near("embed", new[] { 0.9, 0.1, 0 }, ef: 200)
    .Limit(10)
    .RowsAsync();

await db.From("articles")
    .Near("embed", new[] { 0.9, 0.1, 0 }, exact: true)
    .Limit(10)
    .RowsAsync();
import FenecDB
try await db.from("articles")
    .near("embed", [0.9, 0.1, 0])
    .limit(10)
    .rows()

try await db.from("articles")
    .where("year", ">=", 2024)
    .near("embed", [0.9, 0.1, 0], ef: 200)
    .limit(10)
    .rows()

try await db.from("articles")
    .near("embed", [0.9, 0.1, 0], exact: true)
    .limit(10)
    .rows()
import com.fenecdb.*
db.from("articles")
    .near("embed", listOf(0.9, 0.1, 0.0))
    .limit(10)
    .rows()

db.from("articles")
    .where("year", ">=", 2024)
    .near("embed", listOf(0.9, 0.1, 0.0), ef = 200)
    .limit(10)
    .rows()

db.from("articles")
    .near("embed", listOf(0.9, 0.1, 0.0), exact = true)
    .limit(10)
    .rows()
import 'package:fenecdb/fenecdb.dart';
await db.from('articles').near('embed', [0.9, 0.1, 0]).limit(10).rows();

await db.from('articles')
    .where('year', '>=', 2024)
    .near('embed', [0.9, 0.1, 0], ef: 200)
    .limit(10)
    .rows();

await db.from('articles')
    .near('embed', [0.9, 0.1, 0], exact: true)
    .limit(10)
    .rows();
sends
get articles near embed $1 limit $2
-- [[0.9, 0.1, 0], 10]
get articles where year >= $1 near embed $2 ef 200 limit $3
-- [2024, [0.9, 0.1, 0], 10]
get articles near embed $1 exact limit $2
-- [[0.9, 0.1, 0], 10]
  • ef trades speed for accuracy for one query.
  • exact checks every vector, which is how you measure recall.
  • Each row gets a _score column.
How ef, exact and a filter are planned

ef raises the search beam for this query only, trading latency for recall. exact replaces the ANN walk with a full scan, which is how you verify recall against ground truth. A query that uses near gains a _score column. Over a sparse<N> field near ranks by dot product and is exact already, so it takes no ef — see Sparse vectors.

A filter next to near is planned, not stacked: the filter's rows are read only until it is clear whether there are more of them than the ANN would measure anyway, and the planner picks between searching that set exactly and running the ANN with each candidate tested against the filter. The mechanism, and the fallback that keeps it correct, is in How it works.

Quantized vector indexes

quant= makes a vector index smaller by storing a compact code for each vector. Scores stay exact, because the final order uses the full vectors.

create collection articles (embed vector<768> @hnsw(cosine, quant=int8))
create index on articles (embed) @hnsw(cosine, quant=bit)
  • int8 is a quarter of the size; bit about a thirtieth, and cosine only.
What int8 and bit codes hold

quant= sets what the index holds of each vector: int8 a byte a component, a quarter of f32, and bit a bit a component, about a thirtieth: the signs of how far the vector lies from the nearest of 128 centres the index learns from its first 2 048 vectors, which it holds whole until then — cosine only, since the vectors are taken at unit length. The documents keep their vectors whole. The codes find the candidates, the beam's ef of them, and the documents' own vectors put those in order, so _score is the exact distance. An int8 code knows how far off it can be, so a candidate is read only while it can still make the page; a bit code does not, and the whole beam is read. A filtered set small enough to search without the walk is ranked by its codes the same way, and exact reads every vector. Bit codes take a wider beam: ef_search is 200 there unless it is named. What they save and what they cost is in How it works.

Sparse vectors

sparse<N> stores the output of a learned sparse model such as SPLADE. near over it finds the documents with the largest dot product, exactly.

create collection passages (body text @text, splade sparse<30522> @inverted)
put passages {body: "...", splade: "{1037:0.84,2003:1.27,7592:2.1}/30522"}
get passages near splade $1 limit 10
get passages match body $1 near splade $2 fuse limit 10
  • It is written as pgvector's sparsevec: {index:value,...}/N, indices from 1.
  • Index it with @inverted.
The sparsevec text form and @inverted

sparse<N> holds a vector of N dimensions by its non-zero entries alone: what a learned sparse model such as SPLADE gives a text, a weight for a few dozen or hundred of its vocabulary's 30 522 words. It is written and read in pgvector's sparsevec text form, {index:value,...}/N with the indices counted from 1, as a string in JSON and in FenecQL; entries come back in index order and zeros are dropped. @inverted indexes it as a word index is indexed, dimension by dimension, and near over it returns the documents with the largest dot product — the exact ones, walked with the pruning match uses — and ranks only documents sharing at least one dimension with the query. A filter is tested as the lists are merged; exact scores every document instead, and fuse takes a sparse near as it takes a dense one.

A hybrid query

A filter beside near. It is not fuse, which ranks words and vectors together: that is Hybrid search.

get articles select title, year
  where year >= 2024 and tags has "rust" and not (title ~ "draft")
  near embed $1 ef 128
  limit 10
import { connect, not } from '@fenecdb/web/client'
await db.from('articles')
  .select('title', 'year')
  .where('year', '>=', 2024)
  .where('tags', 'has', 'rust')
  .where(not({ title: { '~': 'draft' } }))
  .near('embed', [0.9, 0.1, 0], { ef: 128 })
  .limit(10)
  .rows();
from fenecdb import Client, not_
(db.collection("articles")
    .select("title", "year")
    .where("year", ">=", 2024)
    .where("tags", "has", "rust")
    .where(not_({"title": {"~": "draft"}}))
    .near("embed", [0.9, 0.1, 0], ef=128)
    .limit(10)
    .rows())
import fenecdb "github.com/fenecdb/fenecdb/integrations/go"
db.From("articles").
	Select("title", "year").
	Where("year", ">=", 2024).
	Where("tags", "has", "rust").
	WhereCond(fenecdb.Not(fenecdb.Cmp("title", "~", "draft"))).
	Near("embed", []float64{0.9, 0.1, 0}, fenecdb.Ef(128)).
	Limit(10).
	Rows(ctx)
using FenecDb;
await db.From("articles")
    .Select("title", "year")
    .Where("year", ">=", 2024)
    .Where("tags", "has", "rust")
    .Where(Cond.Not(Cond.Cmp("title", "~", "draft")))
    .Near("embed", new[] { 0.9, 0.1, 0 }, ef: 128)
    .Limit(10)
    .RowsAsync();
import FenecDB
try await db.from("articles")
    .select("title", "year")
    .where("year", ">=", 2024)
    .where("tags", "has", "rust")
    .where(.not(.cmp("title", "~", "draft")))
    .near("embed", [0.9, 0.1, 0], ef: 128)
    .limit(10)
    .rows()
import com.fenecdb.*
db.from("articles")
    .select("title", "year")
    .where("year", ">=", 2024)
    .where("tags", "has", "rust")
    .where(Cond.not(Cond.cmp("title", "~", "draft")))
    .near("embed", listOf(0.9, 0.1, 0.0), ef = 128)
    .limit(10)
    .rows()
import 'package:fenecdb/fenecdb.dart';
await db.from('articles')
    .select(['title', 'year'])
    .where('year', '>=', 2024)
    .where('tags', 'has', 'rust')
    .where(Cond.not(Cond.cmp('title', '~', 'draft')))
    .near('embed', [0.9, 0.1, 0], ef: 128)
    .limit(10)
    .rows();
sends
get articles select title, year where year >= $1 and tags has $2 and not (title ~ $3) near embed $4 ef 128 limit $5
-- [2024, "rust", "draft", [0.9, 0.1, 0], 10]