Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 22 additions & 5 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,14 @@ jobs:
- `issundb-rest`: IssunDB with a REST API server
- `issundb-mcp`: IssunDB with an MCP server

### A Note on the Windows ARM64 Build

The `windows-arm64` binaries are built without the `hnsw` feature, because the C SIMD
library behind it does not compile for that target. Vector search there is an exact
scan: it returns the true nearest neighbors rather than approximate ones, so recall is
higher, but the search is linear in the number of vectors instead of sublinear and
quantization is ignored. Every other platform uses the HNSW index. Nothing else differs.

### Container Image

The same release is published as a container image to GHCR:
Expand Down Expand Up @@ -95,7 +103,19 @@ jobs:
runner: windows-11-arm
archive_ext: zip
content_type: application/zip
cflags: "-DNK_TARGET_HASWELL=0 -DNK_TARGET_SKYLAKE=0 -DNK_TARGET_ICELAKE=0 -DNK_TARGET_GENOA=0 -DNK_TARGET_DIAMOND=0 -DNK_TARGET_SAPPHIRE=0 -DNK_TARGET_SAPPHIREAMX=0 -DNK_TARGET_GRANITEAMX=0 -DNK_TARGET_TURIN=0 -DNK_TARGET_ALDER=0 -DNK_TARGET_SIERRA=0"
# `usearch` pulls in `numkong`, whose C dispatch files fail to compile for
# aarch64-pc-windows-msvc: `winnt.h` reports "No Target Architecture", meaning the
# compiler it is driven with defines none of the `_M_*` architecture macros. The
# `-DNK_TARGET_*=0` flags that used to be set here were a no-op, since numkong's own
# build script already sets every one of them to 0 on this target.
#
# So this target is built without `hnsw`, which drops usearch and numkong from the
# graph entirely and selects the pure-Rust exact vector index. That index returns the
# true nearest neighbors under the same distance conventions, so results are exact
# rather than approximate; what it gives up is the sublinear search and quantization.
# Remove this once numkong builds here, and the target goes back to HNSW with no other
# change.
build_flags: "--no-default-features --features lmdb"
steps:
- name: Checkout repository
uses: actions/checkout@v4
Expand All @@ -120,10 +140,7 @@ jobs:
run: echo "RUSTFLAGS=-C link-arg=-Wl,-ld_classic" >> $GITHUB_ENV

- name: Build release binaries
run: cargo build --release -p issundb-cli -p issundb-rest -p issundb-mcp
env:
CFLAGS_aarch64_pc_windows_msvc: ${{ matrix.cflags || '' }}
CFLAGS_aarch64-pc-windows-msvc: ${{ matrix.cflags || '' }}
run: cargo build --release -p issundb-cli -p issundb-rest -p issundb-mcp ${{ matrix.build_flags || '' }}

- name: Package release asset (Unix)
if: runner.os != 'Windows'
Expand Down
197 changes: 76 additions & 121 deletions AGENTS.md

Large diffs are not rendered by default.

24 changes: 12 additions & 12 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 7 additions & 7 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ exclude = ["benchmarks/ladybugdb-compare"]


[workspace.package]
version = "0.1.0-alpha.20"
version = "0.1.0-alpha.21"
edition = "2024"
authors = ["Hassan Abedi <hassan.abedi.t+issundb@gmail.com>"]
license = "MIT OR Apache-2.0"
Expand All @@ -18,12 +18,12 @@ rust-version = "1.85.0"


[workspace.dependencies]
issundb = { path = "crates/issundb", version = "0.1.0-alpha.20", default-features = false }
issundb-core = { path = "crates/issundb-core", version = "0.1.0-alpha.20", default-features = false }
issundb-vector = { path = "crates/issundb-vector", version = "0.1.0-alpha.20", default-features = false }
issundb-text = { path = "crates/issundb-text", version = "0.1.0-alpha.20", default-features = false }
issundb-retrieval = { path = "crates/issundb-retrieval", version = "0.1.0-alpha.20", default-features = false }
issundb-cypher = { path = "crates/issundb-cypher", version = "0.1.0-alpha.20", default-features = false }
issundb = { path = "crates/issundb", version = "0.1.0-alpha.21", default-features = false }
issundb-core = { path = "crates/issundb-core", version = "0.1.0-alpha.21", default-features = false }
issundb-vector = { path = "crates/issundb-vector", version = "0.1.0-alpha.21", default-features = false }
issundb-text = { path = "crates/issundb-text", version = "0.1.0-alpha.21", default-features = false }
issundb-retrieval = { path = "crates/issundb-retrieval", version = "0.1.0-alpha.21", default-features = false }
issundb-cypher = { path = "crates/issundb-cypher", version = "0.1.0-alpha.21", default-features = false }
heed = "0.22"
colored = "3.1.1"
byteorder = "1"
Expand Down
6 changes: 5 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,7 @@ playground-build: check-wasm-bindgen check-wasm-stack ## Build the browser modul
@echo "Building issundb-wasm for wasm32-unknown-unknown (in-memory storage, exact vector index)..."
@$(WASM_BUILD)
@echo "Generating the JavaScript glue into $(PLAYGROUND_DIR)/pkg..."
@rm -rf $(PLAYGROUND_DIR)/pkg
@wasm-bindgen $(WASM_ARTIFACT) --out-dir $(PLAYGROUND_DIR)/pkg --target web --no-typescript
@cp docs/assets/logo.svg $(PLAYGROUND_DIR)/logo.svg
@ls -l $(PLAYGROUND_DIR)/pkg
Expand All @@ -289,6 +290,7 @@ playground-build: check-wasm-bindgen check-wasm-stack ## Build the browser modul
playground-check: check-wasm-bindgen check-wasm-stack ## Run every playground demo through the compiled module
@echo "Building the module for Node..."
@$(WASM_BUILD)
@rm -rf $(PLAYGROUND_NODE_PKG)
@wasm-bindgen $(WASM_ARTIFACT) --out-dir $(PLAYGROUND_NODE_PKG) --target nodejs --no-typescript
@echo "Running the demo catalog..."
@node $(SCRIPTS_DIR)/check_playground.mjs
Expand All @@ -298,7 +300,9 @@ playground-serve: ## Serve the playground at http://localhost:$(PLAYGROUND_PORT)
@test -f $(PLAYGROUND_DIR)/pkg/issundb_wasm.js || \
{ echo "No module in $(PLAYGROUND_DIR)/pkg. Run 'make playground-build' first."; exit 1; }
@echo "Serving $(PLAYGROUND_DIR) at http://localhost:$(PLAYGROUND_PORT) (Ctrl-C to stop)..."
@python3 -m http.server $(PLAYGROUND_PORT) --directory $(PLAYGROUND_DIR)
@python3 -c 'import functools, http.server as h; \
C = type("C", (h.SimpleHTTPRequestHandler,), {"end_headers": lambda s: (s.send_header("Cache-Control", "no-store"), h.SimpleHTTPRequestHandler.end_headers(s))}); \
h.test(HandlerClass=functools.partial(C, directory="$(PLAYGROUND_DIR)"), port=$(PLAYGROUND_PORT))'

# An exported RUSTFLAGS replaces the `[target.wasm32-unknown-unknown] rustflags` in
# .cargo/config.toml rather than merging with it, which drops the 16 MB stack the inline
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ To use IssunDB in your Rust project, add the dependency to your `Cargo.toml`:

```toml
[dependencies]
issundb = "0.1.0-alpha.20"
issundb = "0.1.0-alpha.21"
serde_json = "1.0"
```

Expand Down
12 changes: 6 additions & 6 deletions benchmarks/ladybugdb-compare/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 6 additions & 1 deletion crates/issundb-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,13 @@ name = "issundb-cli"
path = "src/main.rs"
doc = false

[features]
default = ["lmdb", "hnsw"]
lmdb = ["issundb/lmdb"]
hnsw = ["issundb/hnsw"]

[dependencies]
issundb = { workspace = true, features = ["lmdb", "hnsw"] }
issundb = { workspace = true }
colored.workspace = true
rmp-serde.workspace = true
serde_json = "1"
Expand Down
7 changes: 7 additions & 0 deletions crates/issundb-core/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,10 @@ the freshness path.
- A kernel that needs a per-edge property the snapshot does not carry reads it from storage per call. That is deliberate for the weight-*property*
algorithms (`spanning_forest`, `shortest_path_top_k`, `maximum_flow`), which take the property name as an argument: there is no fixed key to preload.

- The snapshot is published through `arc-swap` and paired with two counters, `write_gen` and `snapshot_gen`, whose comparison is the freshness
condition every consumer tests. The write path also fills a `GraphDelta` buffer, whose only consumers are the property column caches; the CSR itself
is rebuilt rather than patched.

## In-memory Property Columns

`columns.rs` holds a typed, in-memory columnar view of scalar properties used as the hot read path for property gathers and aggregations.
Expand All @@ -155,6 +159,9 @@ It is derived from LMDB, like the CSR snapshot, and follows the same write-LMDB-
- `PropColumns<S: ColumnSource>` stores one typed column per property (Int, Float, Bool, dict-encoded Str, or a JSON fallback) over a dense
`id -> index` map. `NodeSource` and `EdgeSource` implement `ColumnSource`, so nodes and edges share one generic store; `Graph` holds
`prop_columns: ColumnsCache<NodeSource>` and `edge_columns: ColumnsCache<EdgeSource>`.
- A column is `Int`, `Float`, `Bool`, a dictionary-encoded `Str`, or the exact-semantics `Json` fallback, and single-property reads come through
`Graph::node_prop_json`. The per-property statistics (`PropStats`: bounds, an equi-depth histogram, and the most common values) are computed lazily
beside the columns and invalidated by the same post-commit patch.
- `ColumnsCache<S>` builds lazily from one full `scan_all`, but a read does not necessarily cause that build, and the distinction is deliberate. A
request for at most `SMALL_GATHER_MAX` entities is served as point reads straight from storage while the columns are absent
(`should_serve_directly`), because building every column is one full scan and that is the wrong answer to a request for a handful of entities. Those
Expand Down
62 changes: 62 additions & 0 deletions crates/issundb-core/src/graph/algo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1048,6 +1048,68 @@ impl Graph {
self.with_snapshot(|snap| self.betweenness_centrality_kernel(snap))
}

/// Computes the closeness centrality for all nodes, in the Wasserman-Faust form
/// that stays meaningful on a disconnected graph. See
/// [`Graph::closeness_centrality_kernel`].
pub fn closeness_centrality(&self) -> Result<HashMap<NodeId, f64>, Error> {
self.with_snapshot(|snap| self.closeness_centrality_kernel(snap))
}

/// Computes the eigenvector centrality for all nodes by power iteration.
///
/// Bounded rather than fallible: it stops early once the L2 change falls below
/// `tolerance` and otherwise returns the estimate after `iterations` rounds. See
/// [`Graph::eigenvector_centrality_kernel`].
pub fn eigenvector_centrality(
&self,
iterations: u32,
tolerance: f64,
) -> Result<HashMap<NodeId, f64>, Error> {
self.with_snapshot(|snap| self.eigenvector_centrality_kernel(snap, iterations, tolerance))
}

/// Computes the Katz centrality for all nodes.
///
/// `alpha` must be below the reciprocal of the largest eigenvalue or the series
/// diverges; see [`Graph::katz_centrality_kernel`] for what happens if it is not.
pub fn katz_centrality(
&self,
alpha: f64,
beta: f64,
iterations: u32,
tolerance: f64,
) -> Result<HashMap<NodeId, f64>, Error> {
self.with_snapshot(|snap| {
self.katz_centrality_kernel(snap, alpha, beta, iterations, tolerance)
})
}

/// Computes the local clustering coefficient for all nodes, reading the graph as
/// undirected over distinct neighbors. See
/// [`Graph::clustering_coefficient_kernel`].
pub fn clustering_coefficient(&self) -> Result<HashMap<NodeId, f64>, Error> {
self.with_snapshot(|snap| self.clustering_coefficient_kernel(snap))
}

/// Detects communities by the Louvain method, returning the community of every
/// node. The community id is the smallest node id it contains, and only the
/// induced partition is contractual. See [`Graph::louvain_kernel`].
pub fn louvain(&self) -> Result<HashMap<NodeId, u64>, Error> {
self.with_snapshot(|snap| self.louvain_kernel(snap))
}

/// Scores how likely `a` and `b` are to become connected, under one of the
/// neighborhood heuristics. A node the snapshot does not know scores zero rather
/// than erroring. See [`Graph::link_prediction_kernel`].
pub fn link_prediction_score(
&self,
a: NodeId,
b: NodeId,
metric: LinkPredictionMetric,
) -> Result<f64, Error> {
self.with_snapshot(|snap| Ok(self.link_prediction_kernel(snap, a, b, metric)))
}

/// Computes the strongly connected components (SCC) of the graph using Tarjan's algorithm.
pub fn strongly_connected_components(&self) -> Result<HashMap<NodeId, u64>, Error> {
self.with_snapshot(|snap| self.strongly_connected_components_kernel(snap))
Expand Down
Loading
Loading