FenecQL
A small query language with vectors in the core. It is not
SQL and does not try to be, though the familiar select … from word
order is accepted.
Statements
create collection [if not exists] <name> ( <field> <type> [@index], ... )
drop collection [if exists] <name>
create index [if not exists] on <name> (<field>) @index
put <name> { field: value, ... } -- or [ {...}, {...} ]
get <name> [select a, b] [where <expr>]
[near <field> <vector> [ef N] [exact]]
[order <field> [asc|desc], ...] [limit N] [offset N]
get <name> [where <expr>] count -- number of matching rows
select a, b from <name> [where ...] -- the classic SQL order works too
set <name> { field: value } [where <expr>]
del <name> [where <expr>]
collections | describe <name> | compact [<name>]The id field is automatic. Supplying id inside a
put turns it into an upsert.
Reference
| Types | bool int float text bytes timestamp vector<N[, f16]> [type] |
| Indexes | @hash, @hnsw(metric, m=.., ef_construction=.., ef_search=..) |
| Defaults | @hnsw(cosine, m=16, ef_construction=200, ef_search=100) |
| Metrics | cosine l2 dot |
| Operators | = != < <= > >=, ~ text contains, has list contains, in [..], is null |
| Logic | and or not — and binds tighter |
| Parameters | $1, $2, … as in PostgreSQL |
| Functions | lower upper len coalesce now timestamp cosine l2 dot norm normalize, plus plugins |
| Ordering | order year desc, title asc — a tie on the first key is decided by the second; id can be ordered too |
| Counting | count returns one row with one column; it does not combine with select, near, order, limit or offset |
| Ceilings | near at most 10 000 rows (limit + offset); expression depth 512 levels — see Limits |
The vector clause
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 10ef 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.
A filter next to near is planned, not stacked: the filter set
is extracted first and the planner picks between scanning it directly and
running the ANN with a membership test. The mechanism, and the fallback that
keeps it correct, is in How
it works.
Indexes
create collection articles (
title text,
year int @hash,
embed vector<768> @hnsw(cosine, m=16, ef_construction=200)
)
create index on articles (embed) @hnsw(cosine)Only @hash equalities inside an and chain
reach an index. Every other predicate — >=,
~, has, in, and anything under an
or — is a full scan: each matching row is decoded and evaluated.
There is no text index. order has no top-k either: every match
is sorted and then limit applies.
Time
timestamp holds UTC epoch milliseconds as an
i64. Writes accept both text and numbers; a text literal in a
comparison is parsed.
put events {name: "login", t: "2026-09-19T12:34:56Z"}
put events {name: "logout", t: 1758285296000} -- epoch ms
get events where t >= "2026-01-01" and t < now()The representation is always ISO-8601
(2026-09-19T12:34:56.789Z). On the wire fenec-pg
reports PostgreSQL's own output format
(2026-09-19 12:34:56.789+00) and the timestamptz OID,
because client parsers expect the server format. The calendar conversion is
integer arithmetic including leap-year and century rules — no table, no
dependency.
It exists as a separate type rather than an alias over int
because ResultSet does not carry the schema, so an alias would
show every client a raw number.
Bulk loading
For a bulk load, build the index afterwards. The write path becomes a pure append and the graph is built in one parallel pass.
create collection docs (title text, embed vector<768>)
put docs [ ... 100 000 documents ... ]
create index on docs (embed) @hnsw(cosine)Worked examples
A hybrid query
get articles select title, year
where year >= 2024 and tags has "rust" and not (title ~ "draft")
near embed $1 ef 128
limit 10Counting
get articles where year >= 2024 count
-- one row, one columnUpdate and delete
set articles {year: 2025} where id = 42
del articles where year < 2000Inspecting the database
collections
describe articles
compact articlescompact is a full rebuild rather than a garbage collection:
every index is built from scratch even with zero dead bytes, and writes block
throughout. See Limits.