# 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