fenecdb

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

Typesbool 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)
Metricscosine l2 dot
Operators= != < <= > >=, ~ text contains, has list contains, in [..], is null
Logicand or notand binds tighter
Parameters$1, $2, … as in PostgreSQL
Functionslower upper len coalesce now timestamp cosine l2 dot norm normalize, plus plugins
Orderingorder year desc, title asc — a tie on the first key is decided by the second; id can be ordered too
Countingcount returns one row with one column; it does not combine with select, near, order, limit or offset
Ceilingsnear 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 10

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.

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 10

Counting

get articles where year >= 2024 count
-- one row, one column

Update and delete

set articles {year: 2025} where id = 42
del articles where year < 2000

Inspecting the database

collections
describe articles
compact articles

compact 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.