33 ms·
A type-safe, realtime collaborative Graph Database in a CRDT
- cyanydeez 6mo agoEventually someone will figure out how to use a graph database to allow an agent to efficiency build & cull context to achieve near determinant activities. Seems like one needs a sufficiently powerful schema and a harness that properly builds the graph of agent knowledge, like how ants naturally figure how where sugar is, when that stockpile depletes and shifts to other sources. This looks neat, but if you want it to be used for AI purposes, you might want to show a schema more complicated than a twitter network.
- phpnode 6mo agothe airline graph is more complex, I can show the schema for that if you think it's useful?
- embedding-shape 6mo agoI'd wager the problem is on the side of "LLMs can't value/rank information good enough" rather than "The graph database wasn't flexible/good enough", but I'd be happy to be shown counter-examples. I'm sure once that problem been solved, you can use the built-in map/object of whatever language, and it'll be good enough. Add save/load to disk via JSON and you have long-term persistence too. But since LLMs still aren't clever enough, I don't think the underlying implementation matters too much.
- lmeyerov 5mo agoIt's interesting to think of where the value comes from. Afaict 2 interesting areas: A: One of the main lessons of the RAG era of LLMs was reranked multiretrieval is a great balance of test time, test compute, and quality at the expense of maintaining a few costly index types. Graph ended up a nice little lift when put alongside text, vector, and relational indexing by solving some n-hop use cases. I'm unsure if the juice is worth the squeeze, but it does make some sense as infra. Making and using these flows isn't that conceptually complicated and most pieces have good, simple OSS around them. B: There is another universe of richer KG extraction with even heavier indexing work. I'm less clear on the ROI here in typical benchmarks relative to case A. Imagine going full RDF, vs the simpler property graph queries & ontologies here, and investing in heavy entity resolution etc preprocessing during writes. I don't know how well these improve scores vs regular multiretrieval above, and how easy it is to do at any reasonable scale. In practice, a lot of KG work lives out of the DB and agent, and in a much fancier kg pipeline. So there is a missing layer with less clear proof and a value burden. -- Seperately, we have been thinking about these internally. We have been building gfql , oss gpu cypher queries on dataframes etc without needing a DB -- reuse existing storage tiers by moving into embedded compute tier -- and powering our own LLM usage has been a primary internal use case for us. Our experiences have led us to prioritizing case A as a next step for what the graph engine needs to support inside, and viewing case B as something that should live outside of it in a separate library . This post does make me wonder if case B should move closer into the engine to help streamline things for typical users, akin how solr/lucene/etc helped make elastic into something useful early on for search.
- alansaber 5mo agoI'm conceptually very bullish on B (entity resolution and hierarchy pre-processing during writes). I'm less certain than A and B need to be merged into a single library. Obviously, a search agent should know the properties of the KG being searched, but as the previous poster mentioned, these graph dbs are inherently inaccurate and only form part of the retrieval pattern anyway.
- lmeyerov 5mo agoMaybe it's useful to split out B1) KG pipelines from the choice of B2) simple property graph ontologies & queries vs advanced rdf ontologies and sparql queries It sounds like you are thinking about KG pipelines, but I'm unclear on whether typed property graphs, vs more advanced RDF/SPARQL, is needed in your view on the graph engine side?
- j-pb 5mo agoWorking on exactly that! We're local first, but do distributed sync with iroh. Written in rust and fully open source. Imho having a graph database that is really easy to use and write new cli applications on top of works much better. You don't need strong schema validation so long as you can gracefully ignore what your schema doesn't expect by viewing queries as type/schema declarations. https://github.com/magic-locker/faculties https://github.com/magic-locker/faculties
- local_surfer 5mo agoInteresting! Triblespace seems similar to TerminusDB and the solution presented here - would you mind stating the differences ?
- j-pb 5mo agoSpot on! In one word. Simplicity! TerminusDB is a RDF database with build-in prolog, a heavy focus on succinct data structure indexes, and has a client server model. Triblespace is purposefully not RDF, because RDF has horrible DX, and it does not have a good reconciliation and distributed consistency story. It's virtually impossible to get RDF data into a canonical form [https://www.w3.org/TR/rdf-canon/#how-to-read https://www.w3.org/TR/rdf-canon/#how-to-read] and trivial stuff like equality of two datasets is NP-Hard. Triblespace, while also a data exchange standard like RDF, is closer to Datascript or Datomic. It's a Rust library, and great care has been taken to give it extremely nice DX. In-memory datasets are cheaply clonable, and support efficient set operations. There's macros that integrate fully into the type system to perform data generation and queries. // The entity! macro returns a rooted fragment; merge its facts into // a TribleSet via `+=`. let herbert = ufoid(); let dune = ufoid(); let mut library = TribleSet::new(); library += entity! { &herbert @ literature::firstname: "Frank", literature::lastname: "Herbert", }; library += entity! { &dune @ literature::title: "Dune", literature::author: &herbert, literature::quote: ws.put( "I must not fear. Fear is the mind-killer." ), }; ws.commit(library, "import dune"); // `checkout(..)` returns a Checkout — a TribleSet paired with the // commits that produced it, usable for incremental delta queries. let catalog = ws.checkout(..)?; let title = "Dune"; // Multi-entity join: find quotes by authors of a given title. // `_?author` is a pattern-local variable that joins without projecting. for (f, l, quote) in find!( (first: String, last: String, quote), pattern!(&catalog, [ { _?author @ literature::firstname: ?first, literature::lastname: ?last }, { _?book @ literature::title: title, literature::author: _?author, literature::quote: ?quote } ]) ) { let quote: View<str> = ws.get(quote)?; let quote = quote.as_ref(); println!("'{quote}'\n - from {title} by {f} {l}."); } Data has a fully tracked history like in terminus, but we are overall more CRDT-like with multiple scopes of transactionality. You can store stuff in either S3 or a single local file (for the local file you can union two databases by concatenating them with `cat`). We also have just recently added sync through Iroh. The core idea and main difference between RDF is that RDF is text based and weakly typed, we are binary and strongly typed. We split everything into two basic structures: - the tribles (a pun on binary triple), 64byte units that are split into [16byte entity id | 16byte attribute id | 32byte Value] where the first two are basically high entropy identifiers like UUIDs, and the last is either a Blake3 hash, or an inlined <32byte value, with the type being disambiguated by metadata on the attribute ID (itself represented as more tribles) - blobs, content addressed, arbitrary length It's pretty easy to see why canonical representations are pretty easy for us, we just take all of the tribles, sort them lexicographically, dedup them, store the resulting array in a blob. Done. Everythign else is build up from that. Oh and we also have succinct datastructures, but because those are dense but slower, and immutable, we have a custom 256-ary radix trie to do all of the immutable set operations. The query engine is also custom, we don't have a query planner which gives us 0.5-2.5microseconds of latency for queries depending on the number of joins, with a query engine that is fully extensible via traits in rust.
- plaguuuuuu 5mo agoim pretty sure gastown (the Beads part) stores tasks/memories/whatever in a DAG but I haven't looked into it in detail
- 2ndorderthought 6mo agoCan anyone explain why it is a good idea to make a graphdb in typescript? This not a language flamewar question, more of an implementation details question. Though typescript is pretty fast, and the language is flexible, we all know how demanding graph databases are. How hard they are to shard, etc. It seems like this could be a performance trap. Are there successful rbdms or nosql databases out there written in typescript? Also why is everything about LLMs now? Can't we discuss technologies for their face value anymore. It's getting kind of old to me personally.
- phpnode 5mo agoI needed it to be possible to run the graph in the browser and cloudflare workers, so TS was a natural fit here. It was built as an experiment into end to end type safety - nothing to do with LLMs, but it ended up being useful in the product I'm building. It's not designed for large data sets.
- 2ndorderthought 5mo agoMakes sense thanks for explaining the use case. The LLM question was only because of the comments at the time of the post. The query syntax looks nice by the way.
- phpnode 5mo agothanks, it was as close to Gremlin[0] as I could get without losing type safety (Gremlin is untyped) [0] https://tinkerpop.apache.org/ https://tinkerpop.apache.org/
- rglullis 5mo ago> It's not designed for large data sets. How large is large, here? Tens of thousands of triples? Hundreds? Millions? I'm working on a local-first browser extension for ActivityPub, and currently I am parsing the JSON-LD and storing the triples in specialized tables on pglite to be able to make fast queries on that data. It would be amazing to ditch the whole thing and just deal with triples based on the expanded JSON-LD, but I wonder how the performance would be. While using the browser extension for a week, the store accumulated ~90k thousand JSON-lD documents, which would probably mean 5 times as many triples. Storage wise is okay (~300MB), but I think that a graph database would only be useful to manage "hot data", not a whole archive of user activity.
- lo1tuma 5mo ago15 years ago I was a big fan of this chaining methods pattern. These days I don’t like it anymore. Especially when it comes to unit-testing and implementing fake objects it becomes quite cumbersome to setup the exact same interface.
- phpnode 5mo agounfortunately it's unavoidable if you want to preserve type safety. I did consider parsing Cypher in typescript types, but it's not worth the effort and it's not possible to do safely.
- rounce 5mo agoWhy not with a pipe that returns a function, the type of which is determined by the args of the pipe? That is possible to make typesafe in TS. That way you can have both APIs where the chained version is just wrapping successive pipe calls.
- brianbcarter 5mo agoCypher-over-Gremlin is a smart call — LLMs can write Cypher, makes the MCP angle viable in a new way. How dos Yjs handle schema migrations? If I add a property to a vertex type that existing peers have cached, does it conflict or drop the unknown field?
- phpnode 5mo agoInstances that have not updated to the latest version of the schema will ignore the additional property but will not break or conflict.
- tevon 5mo agoThe CRDT enables eventual consistency on these schema updates, so a new field will be eventually consistent
- kkollsg 5mo agoA cypher interface for an MCP is really efficient. I'm running multiple MCPs that way. The trick is also exposing a describe-style tool the model calls before writing queries. Having a schema-introspective tool means you don't have to deal with the schema directly. The knowledge graph deals with it itself, and lets the agent iteratively investigate the graph and then write a query that gives it exactly what it needs. I'm running MCPs across pretty different domains including legal, oil and gas, and codebases. It's surprisingly versatile.
- llmradar 5mo ago[dead]
- AlotOfReading 5mo agoI'm not terribly familiar with graph databases, but perhaps someone who is can explain the advantage of this awfully complicated seeming design. There's gremlin, cypher, yjs, and zod, all of which I understand are different languages for different problems. What's the advantage of using all these different things in one system? You can do all of this in datalog. You get strong eventual consistency naturally. LLMs know how to write it. It's type safe. JS implementations exist [0]. [0] https://github.com/tonsky/datascript https://github.com/tonsky/datascript
- phpnode 5mo agoGremlin-like API gives end to end type safety if you're querying the database from TypeScript. This was the original motivation for the library. Zod/Valibot/ArkType/Standard Schema support because you need a way to define your schema and this allows for that at runtime and compile time. Y.js as a backing store because I needed to support offline sync, branching/forking, and I use Y.js for collaborative editing in my product, so I needed to be able to store the various CRDT types as properties within the graph. e.g. you can have a `description` property on your vertices or edges that is backed by a Y.Text or Y.XmlElement Cypher because until the arrival of codemode it wasn't feasible to have LLMs write queries using the Gremlin-like API and LLMs already know Cypher. Most of all though, this was an experiment that ended up being useful.
- UltraSane 5mo agoThe advantage for property graph databases using Cypher query language is that the queries for things like "show me all systems connected to this system by links greater than 10Gbps up to n hops away" are vastly easier to write and faster to complete compared to SQL and relational databases. Cypher lets you easily search for arbitrary graph patters and the result is also a graph, not a denormalized table.
- cmrdporcupine 5mo agoParent commenter was asking compare to datalog (not SQL) which eats recursive graph transitions like this for lunch, making the queries very elegant to read ... while still staying relational. I'm personally of the opinion that "graph databases" should be relational databases; the relational model can subsume "graph" queries, but for all the reasons Codd laid out back in the 60s... network (aka connected graph) databases cannot do the latter. Let the query planner figure out the connectivity story, not a hardcoded data model. % 1. Base case: Directly connected systems (1 hop) with bandwidth > 10 fast_path(StartSys, EndSys, 1) :- link(StartSys, EndSys, Bandwidth), Bandwidth > 10. % 2. Recursive case: N-hop connections via an intermediate system fast_path(StartSys, EndSys, Hops) :- fast_path(StartSys, IntermediateSys, PrevHops), link(IntermediateSys, EndSys, Bandwidth), Bandwidth > 10, Hops = PrevHops + 1. % 3. The Query: Find all systems connected to 'System_A' within 5 hops ?- fast_path('System_A', TargetSystem, Hops), Hops <= 5. or in RelationalAI's "Rel" language, such as I remember it, this is AI assisted it could be wrong: // 1. Base case: Directly connected systems (1 hop) def fast_path(start_sys, end_sys, hops) = exists(bw: link(start_sys, end_sys, bw) and bw > 10 and hops = 1) // 2. Recursive case: Traverse to the next system def fast_path(start_sys, end_sys, hops) = exists(mid_sys, prev_hops, bw: fast_path(start_sys, mid_sys, prev_hops) and link(mid_sys, end_sys, bw) and bw > 10 and hops = prev_hops + 1) // 3. The Query: Select targets connected to "System_A" within 5 hops def output(target_sys, hops) = fast_path("System_A", target_sys, hops) and hops <= 5 https://www.relational.ai/post/graph-normal-form https://www.relational.ai/post/graph-normal-form https://www.dataversity.net/articles/say-hello-to-graph-normal-form-gnf/ https://www.dataversity.net/articles/say-hello-to-graph-norm... ... That said, modern SQL can do this just fine, just... much harder to read. WITH RECURSIVE fast_path AS ( -- 1. Base case: Directly connected systems from our starting node SELECT start_sys, end_sys, 1 AS hops FROM link WHERE start_sys = 'System_A' AND bandwidth > 10 UNION ALL -- 2. Recursive case: Traverse to the next system SELECT fp.start_sys, l.end_sys, fp.hops + 1 FROM fast_path fp JOIN link l ON fp.end_sys = l.start_sys WHERE l.bandwidth > 10 AND fp.hops < 5 ) -- 3. The Query: Select the generated graph paths SELECT * FROM fast_path;
- esafak 5mo agoGot benchmarks?
- cush 5mo agoThe page keeps crashing on safari
- 40four 5mo agoI’ve recently gotten obsessed with local first app architecture, so I’m really digging into CRDT and trying to get familiar with it. So this looks very interesting to me. Thanks for posting!
- rs545837 5mo agoOh this is cool. The Yjs as storage backend trick is clever, you basically get CRDT sync for free without having to build your own replication layer. And the pluggable storage means you can develop against in-memory and then flip to YGraph for collab mode without touching your queries. That's a nice developer experience. The live queries also caught my eye. Having traversals auto reexecute when data changes sounds straightforward until you realize the underlying data is being merged from multiple peers concurrently. Getting that right without stale reads or phantom edges is genuinely hard. I've been researching on something like this in a similar space but for source code, therefore built a tool called Weave(https://github.com/Ataraxy-Labs/weave https://github.com/Ataraxy-Labs/weave) for entity level merges for git. Instead of merging lines of text, it extracts functions, classes, and methods, builds a dependency graph between them, and merges at that level. Seeing codemix makes me think there might be something interesting here. Right now our entity graph and our CRDT state are two separate things. The graph lives our analysis engine and the CRDT lives in different crate. If something like @codemix/graph could unify those, you'd have a single data structure where the entity dependency graph is the CRDT.
- _alphageek 5mo ago[dead]