A high-performance caching layer for RDF triple stores built on dynamic, compressed K2-trees. It stores RDF triples in a space-efficient way using one K2-tree per predicate, supports SPARQL Basic Graph Pattern (BGP) evaluation, and exposes a TCP server with protobuf-based messaging for client integration.
Developed as a Master's thesis at Universidad de Chile.
📄 Master thesis | Direct Download — PDF
- Overview
- Architecture
- Prerequisites
- Building
- Indexing Pipeline
- Running the Cache Server
- Project Structure
- Testing
- Benchmarks & Experiments
- Docker
- Protocol (Protobuf Messages)
- Citation
- License
RDFCacheK2 is a caching system designed for RDF graph databases. Its core idea is to represent RDF triples compactly using K2-trees — a compressed data structure for sparse binary matrices — where each RDF predicate gets its own K2-tree storing (subject, object) pairs.
Key features:
- Compact storage: K2-trees compress sparse adjacency matrices, achieving significant space savings over raw triple storage.
- Dynamic updates: Unlike static K2-tree implementations, this system supports insert and delete operations on the trees at runtime.
- Cache eviction policies: Supports LRU, Frequency-based, and no-caching strategies with configurable memory budgets.
- SPARQL BGP evaluation: Includes a query engine that evaluates Basic Graph Patterns with join operators (cross-product, intersection, hash join) directly over the K2-tree structures.
- TCP server with protobuf protocol: Exposes the cache as a network service with support for triple pattern streaming, BGP queries, batch updates, and query cancellation.
- Fully Indexed Cache (FIC): An optional second-level cache that materializes small predicates into direct subject→objects and object→subjects maps for fast lookup.
┌─────────────────────────────────────────────────────────────────────┐
│ Client (Protobuf/TCP) │
└────────────────────────────────┬────────────────────────────────────┘
│
┌────────────▼────────────┐
│ CacheServer │
│ (TCP listener + workers)│
└────────────┬────────────┘
│
┌────────────▼────────────┐
│ TaskProcessor │
│ (request routing) │
└──┬─────────┬─────────┬──┘
│ │ │
┌──────────▼──┐ ┌───▼────┐ ┌──▼──────────┐
│ Triple │ │ BGP │ │ Update │
│ Pattern │ │ Query │ │ Session │
│ Streaming │ │ Engine │ │ (add/del) │
└──────┬──────┘ └───┬────┘ └──────┬──────┘
│ │ │
┌──────▼────────────▼──────────────▼──────┐
│ CacheContainer │
│ ┌──────────────────────────────────┐ │
│ │ PredicatesCacheManager │ │
│ │ (per-predicate K2-tree loading) │ │
│ └──────────────┬───────────────────┘ │
│ ┌──────────────▼───────────────────┐ │
│ │ Replacement Policy │ │
│ │ (LRU / Frequency / None) │ │
│ └──────────────────────────────────┘ │
│ ┌──────────────────────────────────┐ │
│ │ Fully Indexed Cache (FIC) │ │
│ │ (optional, small predicates) │ │
│ └──────────────────────────────────┘ │
│ ┌──────────────────────────────────┐ │
│ │ NodeIds Manager │ │
│ │ (RDF term ↔ numeric ID mapping) │ │
│ └──────────────────────────────────┘ │
└─────────────────────────────────────────┘
│
┌──────────▼──────────┐
│ K2-tree Index File │
│ (on-disk serialized │
│ per-predicate │
│ K2-trees) │
└─────────────────────┘
- C++17 compiler (GCC or Clang)
- CMake ≥ 3.14
- Protobuf (protoc + libraries)
- OpenSSL
- libcurl
- Google Test (optional, for tests)
- Make
- Git (to fetch dependencies)
On Debian/Ubuntu:
sudo apt-get install build-essential cmake protobuf-compiler libprotobuf-dev \
libssl-dev libcurl4-openssl-dev libgtest-dev gitOn macOS (Homebrew):
brew install cmake protobuf openssl curl googletestThe project depends on three external libraries, fetched automatically:
| Library | Description |
|---|---|
| c-k2tree-dyn | Dynamic K2-tree C implementation (git submodule) |
| ntparser | N-Triples file parser |
| external-sort | External merge-sort for large triple files |
make allThis runs three stages:
proto-build— Generates C++ and Java protobuf code from.protofilesbuild-libs— Fetches dependencies and builds the NT parserbuild— CMake configure + compile (Release mode)
make all-debugmake re # clean + release build
make re-debug # clean + debug buildSet CACHE_CORES_BUILD to control parallelism (defaults to nproc):
CACHE_CORES_BUILD=8 make allBefore running the cache server, RDF data must be indexed into the K2-tree format. The pipeline converts N-Triples (.nt) files into a compact binary index.
Use build_index_all.py to run the full pipeline:
python3 build_index_all.py-
Convert triples to binary format:
./build/convert_triples_to_binary <input.nt> <output.bin>
-
External sort the binary triples (by predicate):
./build/external_sort <input.bin> <output_sorted.bin>
-
Build the K2-tree index:
./build/build_k2tree_index <sorted_triples.bin> <output_index_file>
See template_config.ini for indexing parameters:
[Indexing]
K2Tree_TreeDepth=32 # Tree depth (32 or 64)
K2Tree_CutDepth=10 # Depth at which to cut internal nodes
K2Tree_MaxNodesCount=256 # Max nodes per leaf block
SortingWorkers=4 # Parallel sorting threads
SortingMemoryBudget=25% # Memory budget for sorting| Tool | Description |
|---|---|
print_k2tree_index |
Inspect/dump the contents of a K2-tree index file |
print_nodeids |
Display the RDF term ↔ numeric ID mappings |
dataset_stats |
Print dataset statistics |
debug_read_k2trees_metadata |
Dump index metadata for debugging |
traverse_k2tree_band |
Traverse a K2-tree band (row/column slice) |
search_paths |
Search N-length paths through K2-trees |
random_dataset_gen |
Generate random binary triple datasets |
random_nt_dataset_generate |
Generate random N-Triples datasets |
generate_triples.py |
Generate a simple 10M-triple test dataset |
The cache server loads a K2-tree index into memory and serves queries over TCP using protobuf messages.
./build/cache_server \
--index-file <path_to_index> \ # K2-tree index file (required)
--node-ids-file <path> \ # Node IDs file (required)
--mapped-node-ids-file <path> \ # Mapped node IDs file (required)
--node-ids-logs-file <path> \ # Node IDs log file (required)
--memory-budget <bytes> \ # Memory budget in bytes (required)
--port <port> \ # TCP port to listen on (required)
--workers <count> \ # Number of worker threads (required)
--replacement-strategy <strategy> \ # lru | frequency | none (required)
--update-log-filename <path> \ # Update log file (required)
--timeout-ms <ms> \ # Query timeout in milliseconds (required)
--fic \ # Enable Fully Indexed Cache (optional)
--sort-results # Sort query results (optional)Example:
./build/cache_server \
-I ./data/my_index.k2idx \
-N ./data/node_ids.bin \
-M ./data/mapped_node_ids.bin \
-L ./data/node_ids_logs.bin \
-m 1073741824 \
-p 9090 \
-w 4 \
-R lru \
-U ./data/update_log.bin \
-T 30000 \
-F -S| Strategy | Description |
|---|---|
lru |
Evicts least-recently-used predicates first |
frequency |
Evicts least-frequently-accessed predicates first |
none |
No eviction — loads all predicates eagerly |
RDFCacheK2/
├── core/ # Core data structures and logic
│ ├── k2tree/ # K2TreeMixed — dynamic compressed K2-tree
│ │ ├── K2TreeMixed.hpp/cpp # Insert, remove, has, scan, serialize/deserialize
│ │ ├── K2TreeBulkOp.* # Bulk operation wrapper with reusable state
│ │ ├── FullScanner.* # Iterate all (subject, object) points
│ │ ├── BandScanner.* # Iterate a row or column slice
│ │ └── EmptyScanner.* # No-op scanner
│ ├── builder/ # Index construction pipeline
│ │ ├── PredicatesIndexFileBuilder.* # Build index file from sorted triples
│ │ ├── K2TreesFeedFromSortedTriplesFeed.* # Group triples → K2-trees
│ │ └── TriplesFeedSBPFromIstream.* # Stream reader for sorted triples
│ ├── fic/ # Fully Indexed Cache (optional 2nd-level cache)
│ │ ├── FullyIndexedCacheImpl.* # LRU cache of materialized predicates
│ │ ├── FullyIndexedPredicate.* # Materialized subject↔object maps
│ │ ├── CacheDataManager.* # Load/unload predicates from index
│ │ └── NoFIC.* # No-op implementation
│ ├── manager/ # Predicate cache management
│ │ ├── PredicatesIndexCacheMD.* # Index metadata + K2-tree loading
│ │ └── PredicatesCacheManagerImpl.* # High-level predicate management
│ ├── replacement/ # Cache eviction policies
│ │ ├── CacheReplacement.* # Generic eviction engine
│ │ ├── LRUReplacementStrategy.* # Recency-based eviction
│ │ ├── FrequencyReplacementStrategy.*# Frequency-based eviction
│ │ └── NoCachingReplacement.* # No eviction (load everything)
│ ├── updating/ # Update logging and merge
│ ├── nodeids/ # RDF term ↔ numeric ID mapping
│ └── algorithms/ # Path search algorithms
│
├── cache/ # Cache container (bootstrap & facade)
│ ├── CacheContainer.* # Runtime facade (manager + policy + FIC)
│ └── CacheContainerFactory.* # Factory with strategy-based construction
│
├── query_engine/ # SPARQL BGP query evaluation
│ ├── BGPMessage.* # BGP query AST (variables + triple patterns)
│ ├── query_processing/
│ │ ├── BGPProcessor.* # Query planner: resolves IDs, builds iterators
│ │ ├── BGPOpsFactory.* # Selects join operator by variable layout
│ │ ├── VarIndexManager.* # Variable name ↔ index mapping
│ │ └── iterators/ # Query result iterators
│ │ ├── BGPIterator.* # Runs operator chain, yields result rows
│ │ ├── QueryIterator.* # Abstract result iterator
│ │ └── bgpops/ # BGP join operators
│ │ ├── OneVarCPBGPOp # Single-variable cross-product
│ │ ├── OneVarIntersectBGPOp # Single-variable intersection
│ │ ├── TwoVarCProductBGPOp # Two-variable cross-product
│ │ ├── TwoVarJoinOneBGPOp # Two-variable hash join
│ │ ├── TwoVarIntersectionBGPOp # Two-variable intersection
│ │ └── TripleExistenceBGPOP # Existence check (no variables)
│ └── TimeControl.* # Query timeout / cancellation control
│
├── network/ # TCP server and streaming layer
│ ├── server/
│ │ ├── CacheServer.* # TCP listener + worker pool
│ │ ├── conn/TCPServerConnection.* # Socket bind/listen/accept
│ │ ├── tasks/
│ │ │ ├── TaskProcessor.* # Request routing interface
│ │ │ ├── CacheServerTaskProcessor.* # Full implementation
│ │ │ └── ServerTask.* # Per-connection state machine
│ │ ├── session/UpdaterSession.* # Batch update session (add/delete)
│ │ └── replacement/ # Deferred replacement tasks
│ ├── streaming/ # Triple pattern + BGP result streaming
│ │ ├── TripleMatchesPartStreamer.* # Chooses streamer strategy
│ │ ├── StreamerFromCachedSource.* # Stream from FIC
│ │ ├── TriplePatternMatchingStreamer.* # Stream from K2-tree scanner
│ │ ├── TPMSortedStreamer.* # Sorted streaming with pagination
│ │ └── BGPStreamer.* # BGP join result streaming
│ ├── scanner/ # Cached-source scanners
│ └── messages/ # Protobuf message parsing utilities
│
├── memory/ # Memory management
│ ├── MemoryManager.* # Singleton manager with segment tracking
│ ├── MemorySegment.* # Linear byte-region allocator
│ └── MemoryPool.hpp # Thread-safe pooled allocator
│
├── utils/ # Utility libraries
│ ├── I_IOStream.hpp # Abstract I/O stream interfaces
│ ├── FileRWHandler.* # File-backed I/O implementations
│ ├── serialization_util.* # Endian-safe binary serialization
│ ├── hashing.* # MD5/SHA hash helpers
│ └── sort/ # External sort for node-ID generation
│
├── algorithms/ # Parallel algorithms
│ ├── ParallelWorker.hpp # Thread pool implementation
│ └── triple_external_sort.hpp # External merge-sort for triples
│
├── proto/ # Protobuf message definitions
│ ├── message_type.proto # Request/response message types enum
│ ├── request_msg.proto # Client request messages
│ ├── response_msg.proto # Server response messages
│ └── sparql_tree.proto # SPARQL algebra AST
│
├── scripts/ # CLI tool entry points
├── benchmarks/ # Performance benchmarks
├── experiments/ # Validation & correctness experiments
├── test/ # Google Test unit/integration tests
├── docker/ # Docker build configuration
├── lib/ # External dependencies (submodules)
│ ├── c-k2tree-dyn/ # Dynamic K2-tree C library
│ ├── ntparser/ # N-Triples parser
│ └── external-sort/ # External merge-sort library
├── cmake/ # CMake modules
├── bash/ # Shell scripts (Docker builds, etc.)
├── build_index_all.py # Full indexing pipeline orchestrator
├── generate_triples.py # Test dataset generator
├── template_config.ini # Indexing configuration template
├── Makefile # Top-level build orchestration
└── CMakeLists.txt # CMake build configuration
Tests use Google Test and are built automatically when GTest is found.
# Run all tests
cd build && ctest -j$(nproc) .
# Run a specific test
./build/k2tree_tests
./build/bgp_processor_test
./build/cache_replacement_test| Category | Tests |
|---|---|
| K2-tree core | k2tree_tests, k2tree_mixed_test, k2tree_serialization_test |
| Serialization | serialization_test, predicates_metadata_serialization_test |
| Cache/eviction | cache_replacement_test, fully_indexed_cache_test |
| Updates | update_log_test |
| Node IDs | node_ids_dyn_mapper_test, nodeids_manager_impl_test, nodeids_build_flow_test, nodes_map_test |
| Query/BGP | bgp_ops_test, bgp_processor_test, bgp_streamer_test |
| Network | server_task_test |
| Path search | search_paths_test |
| Sorting | sort_results_test, test_external_sort |
| Memory | memory_segment_test |
| c-k2tree-dyn | block_test, block_leak_test, morton_code_test, bitvector_test, k2node_test, lazy_scan_test, block_delete_test, k2node_delete_test |
./build/insertion_speed # Time 1M inserts/removes across configs
./build/k2tree_mixed_benchmark # Random bulk insert + serialization size
./build/k2tree_size_benchmark # Space usage analysis
./build/lazy_scan_benchmark # Scan performance
./build/cluster_insertion_speed # Clustered insertion performance
./build/compare_64_32_depths_performance # 32 vs 64-depth tree comparison
./build/points_size_relationship # Points count vs size relationship./build/build_from_raw_points # Build tree, validate scans, test serialization round-trips
./build/validate_k2tree_correctness # Randomized correctness validation
./build/deserialize_k2tree # Deserialize and dump tree contentsA Docker-based build and deployment setup is included.
# First, create a code bundle
make bundle-code
# Build the image
cd docker
docker build -t rdfcachek2 .| Argument | Default | Description |
|---|---|---|
debug |
FALSE |
Set to TRUE for a debug build (includes gdb, vim) |
CACHE_CORES_BUILD |
1 |
Number of parallel build cores |
DISABLE_TEST |
FALSE |
Set to TRUE to skip running tests during build |
docker build --build-arg CACHE_CORES_BUILD=4 --build-arg debug=TRUE -t rdfcachek2:debug .The client-server communication uses length-prefixed protobuf messages over TCP. The protocol supports:
| Message Type | Description |
|---|---|
RUN_QUERY |
Execute a SPARQL query |
STREAM_REQUEST_TRIPLE_PATTERN |
Start streaming triples matching a pattern |
STREAM_CONTINUE_TRIPLE_PATTERN |
Continue a paginated triple stream |
REQUEST_BGP_JOIN |
Execute a BGP join query |
REQUEST_MORE_BGP_JOIN |
Continue paginated BGP results |
CACHE_REQUEST_START_UPDATE_TRIPLES |
Begin an update batch session |
TRIPLES_UPDATE_BATCH |
Send a batch of add/delete operations |
CACHE_DONE_UPDATE_TRIPLES |
Commit the update batch |
SYNC_LOGS_WITH_INDEXES_REQUEST |
Sync update logs with index files |
CANCEL_QUERY |
Cancel a running query |
CONNECTION_END |
Close the connection |
The full SPARQL algebra AST is defined in proto/sparql_tree.proto, supporting Project, Join, BGP, Filter, Optional, Minus, Union, Distinct, Order, Slice, GroupBy, and more.
If you use this software, please cite:
@mastersthesis{miranda2024rdfcachek2,
title = {A compact and dynamic caching system for RDF graph databases},
author = {Miranda, Cristóbal},
school = {Universidad de Chile},
year = {2024},
url = {https://repositorio.uchile.cl/handle/2250/203843}
}See the repository for license information.