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 10await 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();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())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)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();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()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()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();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]eftrades speed for accuracy for one query.exactchecks every vector, which is how you measure recall.- Each row gets a
_scorecolumn.
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)int8is a quarter of the size;bitabout 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 10await 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();(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())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)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();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()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()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();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]