overgraph 0.8.0

An absurdly fast embedded graph database. Pure Rust, sub-microsecond reads.
Documentation
# Getting Started with OverGraph

This guide gets you up and running with OverGraph in Python, Node.js, or Rust. You'll open a database, create some nodes and edges, query neighbors, and run a vector search.

For full parameter documentation, see the [API Reference](api-reference.md).

## Install

**Python**
```bash
pip install overgraph
```

**Node.js**
```bash
npm install overgraph
```

**Rust**
```bash
cargo add overgraph
```

## Open a database

A database is a directory on disk. Pass a vector dimension if you want to use dense vector search.

**Python**
```python
from overgraph import OverGraph

db = OverGraph.open("./my-graph", dense_vector_dimension=3)
```

**Node.js**
```javascript
import { OverGraph } from 'overgraph';

const db = OverGraph.open('./my-graph', {
  denseVector: { dimension: 3 },
});
```

**Rust**
```rust
use overgraph::*;
use std::{collections::BTreeMap, path::Path};

let opts = DbOptions {
    dense_vector: Some(DenseVectorConfig {
        dimension: 3,
        metric: DenseMetric::Cosine,
        hnsw: HnswConfig::default(),
    }),
    ..Default::default()
};
let mut db = DatabaseEngine::open(Path::new("./my-graph"), &opts)?;
```

## Choose labels and edge labels

OverGraph uses labels to classify nodes and edge labels to classify edges. They are
ordinary strings at the public API boundary.

## Create nodes and edges

**Python**
```python
project_dense = [0.18, 0.71, 0.39]
project_sparse = [(101, 0.6), (407, 0.8)]

# Also accepts multiple labels: ["User", "Engineer"]
alice = db.upsert_node("User", "alice", props={"role": "engineer"})
bob = db.upsert_node("User", "bob")
project = db.upsert_node("Project", "atlas",
    dense_vector=project_dense,
    sparse_vector=project_sparse)

db.upsert_edge(alice, project, "WORKS_ON")
db.upsert_edge(bob, project, "WORKS_ON", weight=0.5)
```

**Node.js**
```javascript
const projectDense = [0.18, 0.71, 0.39];
const projectSparse = [{ dimension: 101, value: 0.6 }, { dimension: 407, value: 0.8 }];

// Also accepts multiple labels: ['User', 'Engineer']
const alice = db.upsertNode('User', 'alice', { props: { role: 'engineer' } });
const bob = db.upsertNode('User', 'bob');
const project = db.upsertNode('Project', 'atlas', {
  denseVector: projectDense,
  sparseVector: projectSparse,
});

db.upsertEdge(alice, project, 'WORKS_ON');
db.upsertEdge(bob, project, 'WORKS_ON', { weight: 0.5 });
```

**Rust**
```rust
let project_dense = vec![0.18_f32, 0.71, 0.39];
let project_sparse = vec![(101, 0.6_f32), (407, 0.8)];

// Also accepts multiple labels: &["User", "Engineer"]
let alice = db.upsert_node("User", "alice", UpsertNodeOptions {
    props: BTreeMap::from([("role".into(), PropValue::String("engineer".into()))]),
    ..Default::default()
})?;
let bob = db.upsert_node("User", "bob", UpsertNodeOptions::default())?;
let project = db.upsert_node("Project", "atlas", UpsertNodeOptions {
    dense_vector: Some(project_dense),
    sparse_vector: Some(project_sparse),
    ..Default::default()
})?;

db.upsert_edge(alice, project, "WORKS_ON", UpsertEdgeOptions::default())?;
db.upsert_edge(bob, project, "WORKS_ON", UpsertEdgeOptions { weight: 0.5, ..Default::default() })?;
```

Upsert APIs accept either a single label string or a label collection. Each live
`(label, key)` membership points at one node; if every supplied label/key membership
resolves to the same node, the upsert updates that node instead of creating a duplicate.

## Read data back

**Python**
```python
node = db.get_node(alice)
node = db.get_node_by_key("User", "alice")
nodes = db.get_nodes([alice, bob])       # batch read
```

**Node.js**
```javascript
const node = db.getNode(alice);
const node2 = db.getNodeByKey('User', 'alice');
const nodes = db.getNodes([alice, bob]);
```

**Rust**
```rust
let node = db.get_node(alice)?;
let node = db.get_node_by_key("User", "alice")?;
let nodes = db.get_nodes(&[alice, bob])?;
```

## Query neighbors

**Python**
```python
neighbors = db.neighbors(alice, direction="outgoing")
for n in neighbors:
    print(n.node_id, n.weight)
```

**Node.js**
```javascript
const neighbors = db.neighbors(alice, { direction: 'outgoing' });
for (const n of neighbors) {
  console.log(n.nodeId, n.weight);
}
```

**Rust**
```rust
let neighbors = db.neighbors(alice, &NeighborOptions::default())?;
for n in &neighbors {
    println!("{} {}", n.node_id, n.weight);
}
```

## Vector search

**Python**
```python
query_dense = [0.14, 0.74, 0.36]
query_sparse = [(101, 1.0)]

hits = db.vector_search("hybrid", k=10,
    dense_query=query_dense,
    sparse_query=query_sparse,
    scope_start_node_id=alice,
    scope_max_depth=3)

for hit in hits:
    print(hit.node_id, hit.score)
```

**Node.js**
```javascript
const queryDense = [0.14, 0.74, 0.36];
const querySparse = [{ dimension: 101, value: 1.0 }];

const hits = db.vectorSearch('hybrid', {
  k: 10,
  denseQuery: queryDense,
  sparseQuery: querySparse,
  scope: { startNodeId: alice, maxDepth: 3 },
});

hits.forEach(h => console.log(h.nodeId, h.score));
```

**Rust**
```rust
let query_dense = vec![0.14_f32, 0.74, 0.36];
let query_sparse = vec![(101, 1.0_f32)];

let hits = db.vector_search(&VectorSearchRequest {
    mode: VectorSearchMode::Hybrid,
    dense_query: Some(query_dense),
    sparse_query: Some(query_sparse),
    k: 10,
    label_filter: None,
    ef_search: None,
    scope: Some(VectorSearchScope {
        start_node_id: alice,
        max_depth: 3,
        direction: Direction::Outgoing,
        edge_label_filter: None,
        at_epoch: None,
    }),
    dense_weight: None,
    sparse_weight: None,
    fusion_mode: None,
})?;

for hit in &hits {
    println!("{} {:.4}", hit.node_id, hit.score);
}
```

## Optional: declare property indexes

Property queries work without any extra setup. If a property is hot in your workload, you can declare an optional equality or numeric range index for it. OverGraph will use the declaration-backed path when the index is `Ready`, and otherwise fall back to the same public query API.

**Python**
```python
from overgraph import PropertyRangeBound

db.ensure_node_property_index("User", "role", "equality")
db.ensure_node_property_index("Project", "priority", "range", domain="int")

user_ids = db.find_nodes("User", "role", "engineer")
priority_ids = db.find_nodes_range(
    "Project",
    "priority",
    PropertyRangeBound(1, domain="int"),
    PropertyRangeBound(5, domain="int"),
)
```

**Node.js**
```javascript
db.ensureNodePropertyIndex('User', 'role', { kind: 'equality' });
db.ensureNodePropertyIndex('Project', 'priority', { kind: 'range', domain: 'int' });

const userIds = db.findNodes('User', 'role', 'engineer');
const priorityIds = db.findNodesRange(
  'Project',
  'priority',
  { value: 1, inclusive: true, domain: 'int' },
  { value: 5, inclusive: true, domain: 'int' },
);
```

**Rust**
```rust
db.ensure_node_property_index("User", "role", SecondaryIndexKind::Equality)?;
db.ensure_node_property_index(
    "Project",
    "priority",
    SecondaryIndexKind::Range {
        domain: SecondaryIndexRangeDomain::Int,
    },
)?;

let user_ids = db.find_nodes("User", "role", &PropValue::String("engineer".into()))?;
let lower = PropertyRangeBound::Included(PropValue::Int(1));
let upper = PropertyRangeBound::Included(PropValue::Int(5));
let priority_ids = db.find_nodes_range(
    "Project",
    "priority",
    Some(&lower),
    Some(&upper),
)?;
```

## Close

**Python**
```python
db.close()

# Or use a context manager:
with OverGraph.open("./my-graph") as db:
    db.upsert_node("User", "alice")
```

**Node.js**
```javascript
db.close();
```

**Rust**
```rust
db.close()?;
```

## Async

**Python** - use `AsyncOverGraph`:
```python
from overgraph import AsyncOverGraph

async with await AsyncOverGraph.open("./my-graph") as db:
    alice = await db.upsert_node("User", "alice")
    neighbors = await db.neighbors(alice)
```

**Node.js** - append `Async` to any method:
```javascript
const node = await db.getNodeAsync(alice);
const hits = await db.vectorSearchAsync('hybrid', { k: 10, denseQuery: query });
```

## Next steps

- [API Reference]api-reference.md - every method, parameter, type, and return value across all three languages
- [Architecture Overview]architecture-overview.md - how the storage engine works under the hood