-
v0.10.0 Stable
released this
2026-10-21 10:54:05 +00:00 | 6 commits to main since this releaseVSKI v0.10.0
Light Graphs: retiring the Neo4J sidecar
Most tenants that needed graph workloads ran Neo4J alongside VSKI.
Looking at actual usage, 99% of those use cases - agent graphs,
knowledge linking, dependency walking - never touched the advanced
features (Cypher, clustering, multi-hop pattern matching at scale) and
never exceeded 100k edges. What they did incur was the cost of running
and maintaining a separate database service: another deployment unit,
another backup/replication story, another source of truth to keep
consistent with VSKI data, another thing on-call has to know.Light graphs move that workload into VSKI itself. Graphs are now a
first-class entity type alongside collections - same
(dbName, name)namespacing, same access-rule model, same single
SQLite storage engine, same replication - so the graph and the content
it indexes live in one place. For the workloads tenants actually run,
this removes the Neo4J dependency and its engineering overhead
entirely; no new infrastructure ships with it.How it works
A graph is a named, db-scoped registry entry (same tombstone/revive
semantics as collections) whose data lives in two physical tables in
the tenant database:graph_<name>_nodesandgraph_<name>_edges
(labelled, directed edges with optional numeric weight, cascading
deletes on endpoints). SQLite stays the source of truth - no separate
service, nothing new to operate.Traversal performance comes from an in-memory index: on first access
each graph's topology (ids, edge labels, weights - never properties)
is loaded into a compact adjacency index with nodes and edges
interned touint32slots. A fully-warm 100k-node / 100k-edge graph
occupies ~20-25 MB and a worst-case BFS runs in single-digit
milliseconds. Mutations serialize per graph (check + SQL write +
index update under one lock), so size caps cannot be raced and the
index cannot drift from committed state. Cold graphs are evicted LRU
(GRAPH_INDEX_MAX_GRAPHS, default 32) and reload from SQLite on next
touch; replicas revalidate against the database epoch and reload
transparently.API
Registry (mirrors
/api/collections):GET/POST /api/graphs,
GET/PATCH/DELETE /api/graphs/:name(soft delete keeps data;
re-creating the same name revives the graph with its contents).Data & traversal:
GET/POST /api/graphs/:name/nodes,GET/PATCH/DELETE .../nodes/:nodeIdGET/POST /api/graphs/:name/edges,GET/PATCH/DELETE .../edges/:edgeIdGET .../nodes/:nodeId/neighbors?direction=out|in|both&labels=a,b&limit=GET .../nodes/:nodeId/traverse?depth=N&direction=&labels=&limit=(BFS, max depth 10)GET .../path?from=&to=&maxHops=&cost=hops|weight&labels=(BFS or Dijkstra over edge weights)GET .../subgraph?nodes=a,b&hops=N- neighborhood around seed nodesGET .../stats- node/edge counts, label histogram, degree stats, effective caps
Nodes and edges accept single objects or arrays on create; ids are
optional (15-char generated). Deleting a node cascades its edges.Rules
The five collection rule fields apply with collection semantics: empty
rule denies non-admins,"*"is public, admins bypass. List endpoints
evaluate rules per item (non-trivial rules scan ahead to fill the page;
the response carries"filtered": trueand counts only visible items
when that happened). Traversal endpoints are gated byviewRule.Caps
GRAPH_MAX_EDGES/GRAPH_MAX_NODES(default 100000 each) bound every
graph server-wide; a graph may declare a lower per-graph cap via
options.maxEdges/options.maxNodes. Exceeding the cap is a clean
400 (graph edge cap exceeded (...)) from the per-graph write lock.The caps exist to keep the memory story boring: every graph is bounded,
so the total index footprint is bounded by the number of warm graphs,
and nothing about the feature changes VSKI's single-binary, single-file
storage profile.SDKs
- TypeScript/JS (
@vski/sdkv2.1.0): newclient.graph(name, db?)
/client.db(name).graph(name)namespace (node/edge CRUD,
neighbors,traverse,path,subgraph,stats) and
client.settings.graphsregistry namespace. New types:
GraphConfig,GraphNode,GraphEdge,GraphStats, and the
traversal result types. - Python (
vski-sdkv0.4.0): matching surface -client.graph(name, db)/client.db(name).graph(name)(get_nodes/create_nodes/
get_node/update_node/delete_node, the same five for edges,
neighbors,traverse,path,subgraph,stats) plus the
client.settings.graphsregistry namespace andGraphConfig/
GraphNode/GraphEdge/GraphStatsTypedDicts. The sync wrapper
mirrors both (SyncVskiClient.graph(...)). The e2e harness (including
the replica suite) now honoursVSKI_BINARY/VSKI_DIRoverrides, so
it runs against any server checkout; the suite passes on pytest 8 and 9.
Fixes
- Collections: the drop index/view/FTS routes
(DELETE /api/collections/:name/indexes/:indexName,
.../views/:viewName,.../fts/:ftsIndexName) were documented and
implemented but never registered by the router - every call 404ed. They
are wired now; the SDKdrop_view/drop_index/drop_fts_index
methods work against them. - Python SDK docs:
set_viewswas documented as full-list
replacement, but the server only ever adds views - slugs missing
from the new list stay until dropped individually. The docstring now
states the additive semantics and points atdrop_view(which takes
the full materialised<collection>_view_<slug>name).
Compatibility
- No breaking changes. The
graphsregistry table and the unique index
uq_graphs_db_nameare created idempotently indefault.dbon boot;
tenant tables are created per graph on demand. - Existing Neo4J deployments are unaffected; migration to light graphs
is per-workload (create the graph, replay nodes/edges through the
SDK or a workflow) and can be done incrementally. - Graph mutations publish realtime events on the
db:<graph>topic
(subscription UI support follows in the dashboard update). - Dashboard: graph management pages are not part of this release; use
the SDK or the SQL console until the follow-up web update.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads