lodedb 0.1.2__tar.gz → 0.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. {lodedb-0.1.2 → lodedb-0.2.1}/PKG-INFO +190 -7
  2. {lodedb-0.1.2 → lodedb-0.2.1}/README.md +185 -6
  3. {lodedb-0.1.2 → lodedb-0.2.1}/pyproject.toml +5 -1
  4. {lodedb-0.1.2 → lodedb-0.2.1}/src/lodedb/__init__.py +1 -1
  5. {lodedb-0.1.2 → lodedb-0.2.1}/src/lodedb/engine/_commit_manifest.py +17 -2
  6. lodedb-0.2.1/src/lodedb/engine/_filter_plan.py +205 -0
  7. lodedb-0.2.1/src/lodedb/engine/_lexical.py +566 -0
  8. {lodedb-0.1.2 → lodedb-0.2.1}/src/lodedb/engine/core.py +1299 -72
  9. {lodedb-0.1.2 → lodedb-0.2.1}/src/lodedb/engine/index.py +207 -3
  10. lodedb-0.2.1/src/lodedb/engine/lexical_index_store.py +394 -0
  11. lodedb-0.2.1/src/lodedb/graph/__init__.py +13 -0
  12. lodedb-0.2.1/src/lodedb/graph/_store.py +355 -0
  13. lodedb-0.2.1/src/lodedb/graph/knowledge_graph.py +826 -0
  14. {lodedb-0.1.2 → lodedb-0.2.1}/src/lodedb/local/backends.py +16 -0
  15. lodedb-0.2.1/src/lodedb/local/cli.py +518 -0
  16. {lodedb-0.1.2 → lodedb-0.2.1}/src/lodedb/local/db.py +492 -23
  17. {lodedb-0.1.2 → lodedb-0.2.1}/src/lodedb/local/doctor.py +48 -0
  18. lodedb-0.2.1/src/lodedb/local/integrations/__init__.py +17 -0
  19. lodedb-0.2.1/src/lodedb/local/integrations/llama_index.py +468 -0
  20. lodedb-0.2.1/src/lodedb/local/integrations/llama_index_graph.py +404 -0
  21. lodedb-0.2.1/src/lodedb/local/integrations/mem0.py +531 -0
  22. lodedb-0.2.1/src/lodedb/local/mcp_install.py +587 -0
  23. lodedb-0.2.1/src/lodedb/local/mcp_server.py +220 -0
  24. {lodedb-0.1.2 → lodedb-0.2.1}/src/lodedb/local/presets.py +31 -0
  25. lodedb-0.1.2/src/lodedb/local/cli.py +0 -267
  26. lodedb-0.1.2/src/lodedb/local/integrations/__init__.py +0 -10
  27. lodedb-0.1.2/src/lodedb/local/mcp_server.py +0 -158
  28. {lodedb-0.1.2 → lodedb-0.2.1}/LICENSE +0 -0
  29. {lodedb-0.1.2 → lodedb-0.2.1}/NOTICE +0 -0
  30. {lodedb-0.1.2 → lodedb-0.2.1}/src/lodedb/__main__.py +0 -0
  31. {lodedb-0.1.2 → lodedb-0.2.1}/src/lodedb/config.py +0 -0
  32. {lodedb-0.1.2 → lodedb-0.2.1}/src/lodedb/engine/__init__.py +0 -0
  33. {lodedb-0.1.2 → lodedb-0.2.1}/src/lodedb/engine/_atomic_io.py +0 -0
  34. {lodedb-0.1.2 → lodedb-0.2.1}/src/lodedb/engine/_filelock.py +0 -0
  35. {lodedb-0.1.2 → lodedb-0.2.1}/src/lodedb/engine/_predicate.py +0 -0
  36. {lodedb-0.1.2 → lodedb-0.2.1}/src/lodedb/engine/document_text_store.py +0 -0
  37. {lodedb-0.1.2 → lodedb-0.2.1}/src/lodedb/engine/embedding_backends.py +0 -0
  38. {lodedb-0.1.2 → lodedb-0.2.1}/src/lodedb/engine/gpu_turbovec.py +0 -0
  39. {lodedb-0.1.2 → lodedb-0.2.1}/src/lodedb/engine/mps_turbovec.py +0 -0
  40. {lodedb-0.1.2 → lodedb-0.2.1}/src/lodedb/engine/route_profiles.py +0 -0
  41. {lodedb-0.1.2 → lodedb-0.2.1}/src/lodedb/engine/route_registry.py +0 -0
  42. {lodedb-0.1.2 → lodedb-0.2.1}/src/lodedb/engine/runtime_policy.py +0 -0
  43. {lodedb-0.1.2 → lodedb-0.2.1}/src/lodedb/engine/state_journal_store.py +0 -0
  44. {lodedb-0.1.2 → lodedb-0.2.1}/src/lodedb/engine/turbovec_delta_store.py +0 -0
  45. {lodedb-0.1.2 → lodedb-0.2.1}/src/lodedb/engine/turbovec_index.py +0 -0
  46. {lodedb-0.1.2 → lodedb-0.2.1}/src/lodedb/engine/turbovec_resident.py +0 -0
  47. {lodedb-0.1.2 → lodedb-0.2.1}/src/lodedb/local/__init__.py +0 -0
  48. {lodedb-0.1.2 → lodedb-0.2.1}/src/lodedb/local/benchmark.py +0 -0
  49. {lodedb-0.1.2 → lodedb-0.2.1}/src/lodedb/local/integrations/langchain.py +0 -0
  50. {lodedb-0.1.2 → lodedb-0.2.1}/src/lodedb/local/server.py +0 -0
  51. {lodedb-0.1.2 → lodedb-0.2.1}/src/lodedb/py.typed +0 -0
  52. {lodedb-0.1.2 → lodedb-0.2.1}/third_party/turbovec/Cargo.lock +0 -0
  53. {lodedb-0.1.2 → lodedb-0.2.1}/third_party/turbovec/Cargo.toml +0 -0
  54. {lodedb-0.1.2 → lodedb-0.2.1}/third_party/turbovec/turbovec/Cargo.toml +0 -0
  55. {lodedb-0.1.2 → lodedb-0.2.1}/third_party/turbovec/turbovec/README.md +0 -0
  56. {lodedb-0.1.2 → lodedb-0.2.1}/third_party/turbovec/turbovec/build.rs +0 -0
  57. {lodedb-0.1.2 → lodedb-0.2.1}/third_party/turbovec/turbovec/examples/dump_state.rs +0 -0
  58. {lodedb-0.1.2 → lodedb-0.2.1}/third_party/turbovec/turbovec/examples/kernel_xtest.rs +0 -0
  59. {lodedb-0.1.2 → lodedb-0.2.1}/third_party/turbovec/turbovec/src/codebook.rs +0 -0
  60. {lodedb-0.1.2 → lodedb-0.2.1}/third_party/turbovec/turbovec/src/encode.rs +0 -0
  61. {lodedb-0.1.2 → lodedb-0.2.1}/third_party/turbovec/turbovec/src/error.rs +0 -0
  62. {lodedb-0.1.2 → lodedb-0.2.1}/third_party/turbovec/turbovec/src/id_map.rs +0 -0
  63. {lodedb-0.1.2 → lodedb-0.2.1}/third_party/turbovec/turbovec/src/io.rs +0 -0
  64. {lodedb-0.1.2 → lodedb-0.2.1}/third_party/turbovec/turbovec/src/lib.rs +0 -0
  65. {lodedb-0.1.2 → lodedb-0.2.1}/third_party/turbovec/turbovec/src/pack.rs +0 -0
  66. {lodedb-0.1.2 → lodedb-0.2.1}/third_party/turbovec/turbovec/src/rotation.rs +0 -0
  67. {lodedb-0.1.2 → lodedb-0.2.1}/third_party/turbovec/turbovec/src/search.rs +0 -0
  68. {lodedb-0.1.2 → lodedb-0.2.1}/third_party/turbovec/turbovec/tests/codebook.rs +0 -0
  69. {lodedb-0.1.2 → lodedb-0.2.1}/third_party/turbovec/turbovec/tests/concurrent_search.rs +0 -0
  70. {lodedb-0.1.2 → lodedb-0.2.1}/third_party/turbovec/turbovec/tests/distortion.rs +0 -0
  71. {lodedb-0.1.2 → lodedb-0.2.1}/third_party/turbovec/turbovec/tests/encode.rs +0 -0
  72. {lodedb-0.1.2 → lodedb-0.2.1}/third_party/turbovec/turbovec/tests/encoded_rows.rs +0 -0
  73. {lodedb-0.1.2 → lodedb-0.2.1}/third_party/turbovec/turbovec/tests/filtering.rs +0 -0
  74. {lodedb-0.1.2 → lodedb-0.2.1}/third_party/turbovec/turbovec/tests/id_map.rs +0 -0
  75. {lodedb-0.1.2 → lodedb-0.2.1}/third_party/turbovec/turbovec/tests/input_validation.rs +0 -0
  76. {lodedb-0.1.2 → lodedb-0.2.1}/third_party/turbovec/turbovec/tests/io_versioning.rs +0 -0
  77. {lodedb-0.1.2 → lodedb-0.2.1}/third_party/turbovec/turbovec/tests/kernel_correctness.rs +0 -0
  78. {lodedb-0.1.2 → lodedb-0.2.1}/third_party/turbovec/turbovec/tests/lazy_init.rs +0 -0
  79. {lodedb-0.1.2 → lodedb-0.2.1}/third_party/turbovec/turbovec/tests/reconstruction.rs +0 -0
  80. {lodedb-0.1.2 → lodedb-0.2.1}/third_party/turbovec/turbovec/tests/rotation.rs +0 -0
  81. {lodedb-0.1.2 → lodedb-0.2.1}/third_party/turbovec/turbovec/tests/state_sequences.rs +0 -0
  82. {lodedb-0.1.2 → lodedb-0.2.1}/third_party/turbovec/turbovec/tests/swap_remove.rs +0 -0
  83. {lodedb-0.1.2 → lodedb-0.2.1}/third_party/turbovec/turbovec/tests/tqplus_calibration.rs +0 -0
  84. {lodedb-0.1.2 → lodedb-0.2.1}/third_party/turbovec/turbovec-python/Cargo.toml +0 -0
  85. {lodedb-0.1.2 → lodedb-0.2.1}/third_party/turbovec/turbovec-python/README.md +0 -0
  86. {lodedb-0.1.2 → lodedb-0.2.1}/third_party/turbovec/turbovec-python/build.rs +0 -0
  87. {lodedb-0.1.2 → lodedb-0.2.1}/third_party/turbovec/turbovec-python/python/turbovec/__init__.py +0 -0
  88. {lodedb-0.1.2 → lodedb-0.2.1}/third_party/turbovec/turbovec-python/python/turbovec/_dedup.py +0 -0
  89. {lodedb-0.1.2 → lodedb-0.2.1}/third_party/turbovec/turbovec-python/python/turbovec/_persist.py +0 -0
  90. {lodedb-0.1.2 → lodedb-0.2.1}/third_party/turbovec/turbovec-python/python/turbovec/agno.py +0 -0
  91. {lodedb-0.1.2 → lodedb-0.2.1}/third_party/turbovec/turbovec-python/python/turbovec/haystack.py +0 -0
  92. {lodedb-0.1.2 → lodedb-0.2.1}/third_party/turbovec/turbovec-python/python/turbovec/langchain.py +0 -0
  93. {lodedb-0.1.2 → lodedb-0.2.1}/third_party/turbovec/turbovec-python/python/turbovec/llama_index.py +0 -0
  94. {lodedb-0.1.2 → lodedb-0.2.1}/third_party/turbovec/turbovec-python/src/lib.rs +0 -0
  95. {lodedb-0.1.2 → lodedb-0.2.1}/third_party/turbovec/turbovec-python/tests/test_agno.py +0 -0
  96. {lodedb-0.1.2 → lodedb-0.2.1}/third_party/turbovec/turbovec-python/tests/test_dedup.py +0 -0
  97. {lodedb-0.1.2 → lodedb-0.2.1}/third_party/turbovec/turbovec-python/tests/test_filtering.py +0 -0
  98. {lodedb-0.1.2 → lodedb-0.2.1}/third_party/turbovec/turbovec-python/tests/test_haystack.py +0 -0
  99. {lodedb-0.1.2 → lodedb-0.2.1}/third_party/turbovec/turbovec-python/tests/test_id_map.py +0 -0
  100. {lodedb-0.1.2 → lodedb-0.2.1}/third_party/turbovec/turbovec-python/tests/test_index.py +0 -0
  101. {lodedb-0.1.2 → lodedb-0.2.1}/third_party/turbovec/turbovec-python/tests/test_langchain.py +0 -0
  102. {lodedb-0.1.2 → lodedb-0.2.1}/third_party/turbovec/turbovec-python/tests/test_llama_index.py +0 -0
  103. {lodedb-0.1.2 → lodedb-0.2.1}/third_party/turbovec/turbovec-python/tests/test_security.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lodedb
3
- Version: 0.1.2
3
+ Version: 0.2.1
4
4
  Classifier: Development Status :: 4 - Beta
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: Programming Language :: Python :: 3 :: Only
@@ -14,11 +14,15 @@ Requires-Dist: pytest>=8.3.0 ; extra == 'dev'
14
14
  Requires-Dist: ruff==0.15.17 ; extra == 'dev'
15
15
  Requires-Dist: cupy-cuda12x>=13.0.0 ; platform_system == 'Linux' and extra == 'gpu'
16
16
  Requires-Dist: langchain-core>=0.3.0 ; extra == 'langchain'
17
+ Requires-Dist: llama-index-core>=0.11.0 ; extra == 'llama-index'
17
18
  Requires-Dist: mcp>=1.0.0 ; extra == 'mcp'
19
+ Requires-Dist: mem0ai>=2.0.0 ; extra == 'mem0'
18
20
  Provides-Extra: dev
19
21
  Provides-Extra: gpu
20
22
  Provides-Extra: langchain
23
+ Provides-Extra: llama-index
21
24
  Provides-Extra: mcp
25
+ Provides-Extra: mem0
22
26
  License-File: LICENSE
23
27
  License-File: NOTICE
24
28
  Summary: Local-first, privacy-by-default embedded vector database. Your data never leaves your machine.
@@ -62,8 +66,9 @@ Fast on a laptop. Faster on a GPU. Exact every time. Never phones home.
62
66
  - **Private by default**: text, ids, and vectors stay local; telemetry is metrics-only
63
67
  (counts, bytes, latency), never raw payloads.
64
68
  - **Local embeddings**: `sentence-transformers` on CUDA, MPS, or CPU.
65
- - **Batteries included**: a `lodedb` CLI, a loopback dev server, an MCP server, and a
66
- LangChain `VectorStore` adapter.
69
+ - **Batteries included**: a `lodedb` CLI, a loopback dev server, an
70
+ [MCP server](#use-as-an-mcp-server), and LangChain, LlamaIndex, and mem0 adapters
71
+ (`VectorStore`s, plus a LlamaIndex `PropertyGraphStore`).
67
72
 
68
73
  > 🏢 **Enterprise** The LodeDB core is Apache-2.0 and free to use. Enterprise licensing is
69
74
  > available for commercial support, managed and at-scale serving, and on-prem / BYOC
@@ -80,10 +85,43 @@ Python 3.11+, and bundle the TurboVec (Rust) core, so there's nothing to compile
80
85
  the install with `lodedb doctor`. Optional extras:
81
86
 
82
87
  ```bash
83
- pip install "lodedb[gpu]" # GPU-resident scan (Linux/CUDA)
84
- pip install "lodedb[mcp,langchain]" # MCP server + LangChain adapter
88
+ pip install "lodedb[gpu]" # GPU-resident scan (Linux/CUDA)
89
+ pip install "lodedb[mcp,langchain,llama-index,mem0]" # MCP server + LangChain/LlamaIndex/mem0 adapters
85
90
  ```
86
91
 
92
+ Using LodeDB as memory for a coding assistant? After installing the `mcp` extra, register its
93
+ server in one step (details under [Use as an MCP server](#use-as-an-mcp-server)):
94
+
95
+ ```bash
96
+ lodedb mcp install --client claude-code # or: claude-desktop | cursor | lm-studio | codex | all
97
+ ```
98
+
99
+ <details>
100
+ <summary><b>Windows: NVIDIA GPU embeddings</b></summary>
101
+
102
+ On Windows, PyPI serves the CPU-only PyTorch build by default, so `pip install lodedb` (and
103
+ `uv`) leave embeddings on the CPU even on a CUDA machine, and no package metadata can override
104
+ which torch wheel pip resolves. `lodedb doctor` detects this and prints the fix; `lodedb doctor
105
+ --fix` reinstalls the CUDA build for you:
106
+
107
+ ```bash
108
+ lodedb doctor # flags a CPU-only PyTorch on Windows and prints the command
109
+ lodedb doctor --fix # reinstalls the CUDA build so embeddings use your NVIDIA GPU
110
+ ```
111
+
112
+ Or reinstall manually, picking the index for your CUDA version (`cu121`, `cu124`, ...) from the
113
+ [PyTorch install guide](https://pytorch.org/get-started/locally/):
114
+
115
+ ```bash
116
+ pip install torch --force-reinstall --no-deps --index-url https://download.pytorch.org/whl/cu121
117
+ uv pip install torch --reinstall --index-url https://download.pytorch.org/whl/cu121 # with uv
118
+ ```
119
+
120
+ This is Windows-only: the default Linux PyPI wheel already bundles CUDA, and macOS uses CPU
121
+ or MPS.
122
+
123
+ </details>
124
+
87
125
  <details>
88
126
  <summary><b>Build from source</b> (contributors, or a platform without a wheel)</summary>
89
127
 
@@ -93,8 +131,8 @@ Linux). [uv](https://docs.astral.sh/uv/) builds and bundles the core for you:
93
131
  ```bash
94
132
  git clone https://github.com/Egoist-Machines/LodeDB && cd LodeDB
95
133
  uv sync # builds + bundles the TurboVec core via maturin
96
- uv sync --extra mcp --extra langchain # + MCP server, LangChain adapter
97
- uv sync --extra gpu # + GPU-resident scan (Linux/CUDA)
134
+ uv sync --extra mcp --extra langchain --extra llama-index --extra mem0 # + MCP server, adapters
135
+ uv sync --extra gpu # + GPU-resident scan (Linux/CUDA)
98
136
  ```
99
137
 
100
138
  Run with `uv run` (e.g. `uv run lodedb doctor`).
@@ -121,6 +159,9 @@ for hits in db.search_many(["fox", "dog"], k=5): # batched; the GPU can serve
121
159
  db.search("fox", k=5, filter={"topic": "animals"}) # bare scalar = exact
122
160
  db.search("fox", k=5, filter={"$or": [{"topic": "animals"}, {"year": {"$gte": 2020}}]})
123
161
 
162
+ # hybrid search: vector recall plus exact lexical matches the embedding misses
163
+ db.search("E1234", k=5, mode="hybrid") # surfaces error codes, serials, dates in the body
164
+
124
165
  db.get(fox) # -> "the quick brown fox jumps" (text retained by default)
125
166
  db.persist() # durable .tvim/.tvd/.jsd snapshot; replays on reopen
126
167
  ```
@@ -139,6 +180,68 @@ reader.search("fox", k=5) # reads a committed snapshot
139
180
  reader.add("nope") # raises ReadOnlyError
140
181
  ```
141
182
 
183
+ ## Hybrid search
184
+
185
+ Vector search alone misses exact tokens the embedding does not capture: error codes
186
+ (`E1234`), serial numbers (`ABC-123`), dates (`2024-01-15`). Pass `mode="hybrid"` to run a
187
+ lexical BM25 ranker alongside the vector scan and fuse the two ranked lists with Reciprocal
188
+ Rank Fusion. The lexical ranker matches those tokens exactly, so a document whose body carries
189
+ the code is recovered even when the embedding ranks it nowhere near the top.
190
+
191
+ ```python
192
+ db.add("the turbine tripped and reported fault code E1234 overnight", metadata={"unit": "t3"})
193
+
194
+ db.search("E1234", k=5) # mode="vector" (default): may miss the exact code
195
+ db.search("E1234", k=5, mode="hybrid") # BM25 + RRF: surfaces it in the top-k
196
+ db.search("E1234", k=5, mode="lexical") # BM25 ranking alone, no vector scan
197
+ ```
198
+
199
+ ### Prerequisites
200
+
201
+ `mode="hybrid"` and `mode="lexical"` build a BM25 index over your text, so they need a text
202
+ source enabled when you open the database. `mode="vector"` (the default) needs nothing.
203
+
204
+ | Mode | Enable | Source of the BM25 index |
205
+ | --- | --- | --- |
206
+ | `"vector"` (default) | nothing | not used |
207
+ | `"hybrid"`, `"lexical"` | `store_text=True` (on by default) | rebuilt in memory from the retained raw text |
208
+ | `"hybrid"`, `"lexical"` | `index_text=True` | a durable on-disk postings store, no raw text required |
209
+
210
+ Either source is enough and you can enable both. `store_text=True` is the default, so hybrid
211
+ works out of the box. With neither source enabled, a hybrid or lexical query raises a clear,
212
+ actionable error rather than silently degrading.
213
+
214
+ ### How it works
215
+
216
+ A `filter` constrains both rankers, so `mode="hybrid"` with a filter returns the true top-k of
217
+ the matching subset. The vector half of a hybrid query runs on the same scan as `mode="vector"`,
218
+ including the GPU-resident batch scan that serves `search_many`; only the BM25 ranking and the
219
+ fusion run on the CPU, and the vector kernel and on-disk format are untouched. The serving BM25
220
+ index lives in memory and is maintained incrementally: a small mutation folds just the changed
221
+ chunks into the existing index, so a single `add` never forces a full re-tokenization.
222
+
223
+ ### Durable lexical index (`index_text=True`)
224
+
225
+ By default the BM25 index is rebuilt from the retained raw text, so it needs `store_text=True`
226
+ and is re-tokenized on the first hybrid query after opening. Pass `index_text=True` to persist
227
+ it instead: each document's per-chunk terms are captured at `add` time into a dedicated `.tvlex`
228
+ sidecar (a base plus a `.lxd` delta journal, committed O(changed) per write), so hybrid and
229
+ lexical search survive a reopen rebuilt straight from the persisted terms, with no
230
+ re-tokenization and without retaining the raw text at all. The `.tvlex` sidecar holds
231
+ payload-derived terms only and, like the raw-text sidecar, never reaches the redacted artifacts
232
+ or telemetry. The tokenizer lowercases and splits on punctuation but keeps code-like tokens
233
+ whole, so `ABC-123` and `2024-01-15` stay findable as single tokens. Reopen with the same
234
+ `index_text` value you wrote with.
235
+
236
+ ```python
237
+ db = LodeDB(path="./data", index_text=True, store_text=False) # durable lexical index, no raw text
238
+ db.add("the turbine tripped and reported fault code E1234 overnight")
239
+ db.close()
240
+
241
+ reopened = LodeDB(path="./data", index_text=True, store_text=False)
242
+ reopened.search("E1234", k=5, mode="hybrid") # works after reopen, rebuilt from persisted terms
243
+ ```
244
+
142
245
  ## GPU-resident index
143
246
 
144
247
  With the `[gpu]` extra on a CUDA host, LodeDB reconstructs the compact index into an fp16
@@ -227,6 +330,82 @@ lodedb mcp # stdio MCP server for agent memory
227
330
  lodedb benchmark # local, metrics-only benchmark
228
331
  ```
229
332
 
333
+ ## Use as an MCP server
334
+
335
+ LodeDB ships a [Model Context Protocol](https://modelcontextprotocol.io) server, so an agent
336
+ can use a local on-disk database as long-term memory or a RAG store. It runs over stdio, adds
337
+ no storage logic of its own, and your data stays on the machine. Install the extra and point
338
+ your host at `lodedb mcp`:
339
+
340
+ ```bash
341
+ pip install "lodedb[mcp]"
342
+ ```
343
+
344
+ It exposes `lodedb_add`, `lodedb_search`, `lodedb_remove`, and `lodedb_stats`, plus
345
+ `lodedb_get` when text is available. `lodedb_search` returns each hit's stored text alongside
346
+ the score, id, and metadata, so a model can rank and answer in a single call rather than
347
+ chaining a follow-up lookup. It runs [hybrid search](#hybrid-search) (BM25 lexical + vector,
348
+ fused with RRF) by default when text is retained, so exact tokens like error codes and serials
349
+ surface next to semantic matches; with no text retained it falls back to a vector scan. Start
350
+ the server with `--exclude-text` to return metrics only (this also withdraws `lodedb_get`), or
351
+ `--no-store-text` to keep no text on disk at all. `lodedb_stats` is always metrics-only and raw
352
+ query text never leaves the process.
353
+
354
+ ### One command
355
+
356
+ `lodedb mcp install` writes the correct entry to a client's config for you, so you do not have to
357
+ find the file or hand-write the JSON/TOML:
358
+
359
+ ```bash
360
+ lodedb mcp install --client claude-code # or: claude-desktop | cursor | lm-studio | codex | all
361
+ lodedb mcp install --client cursor --path ./data --model bge
362
+ ```
363
+
364
+ It resolves the launch command for your environment, so `command` and `args` are correct even when
365
+ `lodedb` is not on `PATH` (it falls back to the `uv run --project ...` form, then an absolute path to
366
+ the entry point), and it resolves `--path` to an absolute path so the server opens the right
367
+ directory wherever the client starts it. The edit is idempotent (an existing `lodedb` entry is
368
+ updated, never duplicated) and never touches other servers in the file. It passes through the same
369
+ options as `lodedb mcp` (`--path`, `--model`, `--device`, `--exclude-text`, `--no-store-text`);
370
+ `--dry-run` prints the entry and target file without writing, and `lodedb mcp uninstall --client
371
+ <client>` removes it again. Override the config location with `--config <path>` (Claude Desktop and
372
+ LM Studio paths differ per OS), and use `--project <dir>` to write Cursor's project-level
373
+ `.cursor/mcp.json`. For Claude Code it runs `claude mcp add`; for the others it edits the config file
374
+ directly.
375
+
376
+ <details>
377
+ <summary><b>Register by hand</b> (Claude Code, Claude Desktop, Cursor, LM Studio, Codex)</summary>
378
+
379
+ The `lodedb` command must be on the host's `PATH`; if you installed into a virtual environment
380
+ (including a `uv` project) where it isn't, use the `uv run` form at the bottom.
381
+
382
+ **Claude Code, Claude Desktop, Cursor, LM Studio**: add the stdio entry to the host's MCP
383
+ config (`claude_desktop_config.json`, `.cursor/mcp.json`, or LM Studio's `mcp.json`), or run
384
+ `claude mcp add lodedb -- lodedb mcp --path ./data`:
385
+
386
+ ```json
387
+ { "mcpServers": { "lodedb": { "command": "lodedb", "args": ["mcp", "--path", "./data"] } } }
388
+ ```
389
+
390
+ **Codex**: add to `~/.codex/config.toml`:
391
+
392
+ ```toml
393
+ [mcp_servers.lodedb]
394
+ command = "lodedb"
395
+ args = ["mcp", "--path", "./data"]
396
+ ```
397
+
398
+ **From a virtual environment (uv)**, when `lodedb` is not on `PATH`:
399
+
400
+ ```json
401
+ { "mcpServers": { "lodedb": { "command": "uv",
402
+ "args": ["run", "--project", "/path/to/LodeDB", "lodedb", "mcp", "--path", "/path/to/data"] } } }
403
+ ```
404
+
405
+ See [`examples/mcp_config.json`](examples/mcp_config.json) for a copy-paste starting point.
406
+
407
+ </details>
408
+
230
409
  ## Concurrency & durability
231
410
 
232
411
  - **Single writer, many readers, per path.** One handle holds the path open for *writing* at
@@ -255,6 +434,10 @@ lodedb benchmark # local, metrics-only benchmark
255
434
  faster on the measured M1, so the MPS scan stays off by default until newer Apple GPUs are
256
435
  re-measured.
257
436
  - **Single queries run on the CPU**; the GPU serves batched `search_many`.
437
+ - **Hybrid search needs a lexical source and serves from memory.** `mode="hybrid"`/`"lexical"`
438
+ need either `store_text=True` (the index built from raw text) or `index_text=True` (a durable
439
+ `.tvlex` postings store that survives reopens without raw text). The serving index is held in
440
+ memory and maintained incrementally across mutations.
258
441
  - **Single writer per path.** One writer at a time (many concurrent readers), with no live
259
442
  cross-process refresh, on local filesystems only. See
260
443
  [Concurrency & durability](#concurrency--durability).
@@ -28,8 +28,9 @@ Fast on a laptop. Faster on a GPU. Exact every time. Never phones home.
28
28
  - **Private by default**: text, ids, and vectors stay local; telemetry is metrics-only
29
29
  (counts, bytes, latency), never raw payloads.
30
30
  - **Local embeddings**: `sentence-transformers` on CUDA, MPS, or CPU.
31
- - **Batteries included**: a `lodedb` CLI, a loopback dev server, an MCP server, and a
32
- LangChain `VectorStore` adapter.
31
+ - **Batteries included**: a `lodedb` CLI, a loopback dev server, an
32
+ [MCP server](#use-as-an-mcp-server), and LangChain, LlamaIndex, and mem0 adapters
33
+ (`VectorStore`s, plus a LlamaIndex `PropertyGraphStore`).
33
34
 
34
35
  > 🏢 **Enterprise** The LodeDB core is Apache-2.0 and free to use. Enterprise licensing is
35
36
  > available for commercial support, managed and at-scale serving, and on-prem / BYOC
@@ -46,10 +47,43 @@ Python 3.11+, and bundle the TurboVec (Rust) core, so there's nothing to compile
46
47
  the install with `lodedb doctor`. Optional extras:
47
48
 
48
49
  ```bash
49
- pip install "lodedb[gpu]" # GPU-resident scan (Linux/CUDA)
50
- pip install "lodedb[mcp,langchain]" # MCP server + LangChain adapter
50
+ pip install "lodedb[gpu]" # GPU-resident scan (Linux/CUDA)
51
+ pip install "lodedb[mcp,langchain,llama-index,mem0]" # MCP server + LangChain/LlamaIndex/mem0 adapters
51
52
  ```
52
53
 
54
+ Using LodeDB as memory for a coding assistant? After installing the `mcp` extra, register its
55
+ server in one step (details under [Use as an MCP server](#use-as-an-mcp-server)):
56
+
57
+ ```bash
58
+ lodedb mcp install --client claude-code # or: claude-desktop | cursor | lm-studio | codex | all
59
+ ```
60
+
61
+ <details>
62
+ <summary><b>Windows: NVIDIA GPU embeddings</b></summary>
63
+
64
+ On Windows, PyPI serves the CPU-only PyTorch build by default, so `pip install lodedb` (and
65
+ `uv`) leave embeddings on the CPU even on a CUDA machine, and no package metadata can override
66
+ which torch wheel pip resolves. `lodedb doctor` detects this and prints the fix; `lodedb doctor
67
+ --fix` reinstalls the CUDA build for you:
68
+
69
+ ```bash
70
+ lodedb doctor # flags a CPU-only PyTorch on Windows and prints the command
71
+ lodedb doctor --fix # reinstalls the CUDA build so embeddings use your NVIDIA GPU
72
+ ```
73
+
74
+ Or reinstall manually, picking the index for your CUDA version (`cu121`, `cu124`, ...) from the
75
+ [PyTorch install guide](https://pytorch.org/get-started/locally/):
76
+
77
+ ```bash
78
+ pip install torch --force-reinstall --no-deps --index-url https://download.pytorch.org/whl/cu121
79
+ uv pip install torch --reinstall --index-url https://download.pytorch.org/whl/cu121 # with uv
80
+ ```
81
+
82
+ This is Windows-only: the default Linux PyPI wheel already bundles CUDA, and macOS uses CPU
83
+ or MPS.
84
+
85
+ </details>
86
+
53
87
  <details>
54
88
  <summary><b>Build from source</b> (contributors, or a platform without a wheel)</summary>
55
89
 
@@ -59,8 +93,8 @@ Linux). [uv](https://docs.astral.sh/uv/) builds and bundles the core for you:
59
93
  ```bash
60
94
  git clone https://github.com/Egoist-Machines/LodeDB && cd LodeDB
61
95
  uv sync # builds + bundles the TurboVec core via maturin
62
- uv sync --extra mcp --extra langchain # + MCP server, LangChain adapter
63
- uv sync --extra gpu # + GPU-resident scan (Linux/CUDA)
96
+ uv sync --extra mcp --extra langchain --extra llama-index --extra mem0 # + MCP server, adapters
97
+ uv sync --extra gpu # + GPU-resident scan (Linux/CUDA)
64
98
  ```
65
99
 
66
100
  Run with `uv run` (e.g. `uv run lodedb doctor`).
@@ -87,6 +121,9 @@ for hits in db.search_many(["fox", "dog"], k=5): # batched; the GPU can serve
87
121
  db.search("fox", k=5, filter={"topic": "animals"}) # bare scalar = exact
88
122
  db.search("fox", k=5, filter={"$or": [{"topic": "animals"}, {"year": {"$gte": 2020}}]})
89
123
 
124
+ # hybrid search: vector recall plus exact lexical matches the embedding misses
125
+ db.search("E1234", k=5, mode="hybrid") # surfaces error codes, serials, dates in the body
126
+
90
127
  db.get(fox) # -> "the quick brown fox jumps" (text retained by default)
91
128
  db.persist() # durable .tvim/.tvd/.jsd snapshot; replays on reopen
92
129
  ```
@@ -105,6 +142,68 @@ reader.search("fox", k=5) # reads a committed snapshot
105
142
  reader.add("nope") # raises ReadOnlyError
106
143
  ```
107
144
 
145
+ ## Hybrid search
146
+
147
+ Vector search alone misses exact tokens the embedding does not capture: error codes
148
+ (`E1234`), serial numbers (`ABC-123`), dates (`2024-01-15`). Pass `mode="hybrid"` to run a
149
+ lexical BM25 ranker alongside the vector scan and fuse the two ranked lists with Reciprocal
150
+ Rank Fusion. The lexical ranker matches those tokens exactly, so a document whose body carries
151
+ the code is recovered even when the embedding ranks it nowhere near the top.
152
+
153
+ ```python
154
+ db.add("the turbine tripped and reported fault code E1234 overnight", metadata={"unit": "t3"})
155
+
156
+ db.search("E1234", k=5) # mode="vector" (default): may miss the exact code
157
+ db.search("E1234", k=5, mode="hybrid") # BM25 + RRF: surfaces it in the top-k
158
+ db.search("E1234", k=5, mode="lexical") # BM25 ranking alone, no vector scan
159
+ ```
160
+
161
+ ### Prerequisites
162
+
163
+ `mode="hybrid"` and `mode="lexical"` build a BM25 index over your text, so they need a text
164
+ source enabled when you open the database. `mode="vector"` (the default) needs nothing.
165
+
166
+ | Mode | Enable | Source of the BM25 index |
167
+ | --- | --- | --- |
168
+ | `"vector"` (default) | nothing | not used |
169
+ | `"hybrid"`, `"lexical"` | `store_text=True` (on by default) | rebuilt in memory from the retained raw text |
170
+ | `"hybrid"`, `"lexical"` | `index_text=True` | a durable on-disk postings store, no raw text required |
171
+
172
+ Either source is enough and you can enable both. `store_text=True` is the default, so hybrid
173
+ works out of the box. With neither source enabled, a hybrid or lexical query raises a clear,
174
+ actionable error rather than silently degrading.
175
+
176
+ ### How it works
177
+
178
+ A `filter` constrains both rankers, so `mode="hybrid"` with a filter returns the true top-k of
179
+ the matching subset. The vector half of a hybrid query runs on the same scan as `mode="vector"`,
180
+ including the GPU-resident batch scan that serves `search_many`; only the BM25 ranking and the
181
+ fusion run on the CPU, and the vector kernel and on-disk format are untouched. The serving BM25
182
+ index lives in memory and is maintained incrementally: a small mutation folds just the changed
183
+ chunks into the existing index, so a single `add` never forces a full re-tokenization.
184
+
185
+ ### Durable lexical index (`index_text=True`)
186
+
187
+ By default the BM25 index is rebuilt from the retained raw text, so it needs `store_text=True`
188
+ and is re-tokenized on the first hybrid query after opening. Pass `index_text=True` to persist
189
+ it instead: each document's per-chunk terms are captured at `add` time into a dedicated `.tvlex`
190
+ sidecar (a base plus a `.lxd` delta journal, committed O(changed) per write), so hybrid and
191
+ lexical search survive a reopen rebuilt straight from the persisted terms, with no
192
+ re-tokenization and without retaining the raw text at all. The `.tvlex` sidecar holds
193
+ payload-derived terms only and, like the raw-text sidecar, never reaches the redacted artifacts
194
+ or telemetry. The tokenizer lowercases and splits on punctuation but keeps code-like tokens
195
+ whole, so `ABC-123` and `2024-01-15` stay findable as single tokens. Reopen with the same
196
+ `index_text` value you wrote with.
197
+
198
+ ```python
199
+ db = LodeDB(path="./data", index_text=True, store_text=False) # durable lexical index, no raw text
200
+ db.add("the turbine tripped and reported fault code E1234 overnight")
201
+ db.close()
202
+
203
+ reopened = LodeDB(path="./data", index_text=True, store_text=False)
204
+ reopened.search("E1234", k=5, mode="hybrid") # works after reopen, rebuilt from persisted terms
205
+ ```
206
+
108
207
  ## GPU-resident index
109
208
 
110
209
  With the `[gpu]` extra on a CUDA host, LodeDB reconstructs the compact index into an fp16
@@ -193,6 +292,82 @@ lodedb mcp # stdio MCP server for agent memory
193
292
  lodedb benchmark # local, metrics-only benchmark
194
293
  ```
195
294
 
295
+ ## Use as an MCP server
296
+
297
+ LodeDB ships a [Model Context Protocol](https://modelcontextprotocol.io) server, so an agent
298
+ can use a local on-disk database as long-term memory or a RAG store. It runs over stdio, adds
299
+ no storage logic of its own, and your data stays on the machine. Install the extra and point
300
+ your host at `lodedb mcp`:
301
+
302
+ ```bash
303
+ pip install "lodedb[mcp]"
304
+ ```
305
+
306
+ It exposes `lodedb_add`, `lodedb_search`, `lodedb_remove`, and `lodedb_stats`, plus
307
+ `lodedb_get` when text is available. `lodedb_search` returns each hit's stored text alongside
308
+ the score, id, and metadata, so a model can rank and answer in a single call rather than
309
+ chaining a follow-up lookup. It runs [hybrid search](#hybrid-search) (BM25 lexical + vector,
310
+ fused with RRF) by default when text is retained, so exact tokens like error codes and serials
311
+ surface next to semantic matches; with no text retained it falls back to a vector scan. Start
312
+ the server with `--exclude-text` to return metrics only (this also withdraws `lodedb_get`), or
313
+ `--no-store-text` to keep no text on disk at all. `lodedb_stats` is always metrics-only and raw
314
+ query text never leaves the process.
315
+
316
+ ### One command
317
+
318
+ `lodedb mcp install` writes the correct entry to a client's config for you, so you do not have to
319
+ find the file or hand-write the JSON/TOML:
320
+
321
+ ```bash
322
+ lodedb mcp install --client claude-code # or: claude-desktop | cursor | lm-studio | codex | all
323
+ lodedb mcp install --client cursor --path ./data --model bge
324
+ ```
325
+
326
+ It resolves the launch command for your environment, so `command` and `args` are correct even when
327
+ `lodedb` is not on `PATH` (it falls back to the `uv run --project ...` form, then an absolute path to
328
+ the entry point), and it resolves `--path` to an absolute path so the server opens the right
329
+ directory wherever the client starts it. The edit is idempotent (an existing `lodedb` entry is
330
+ updated, never duplicated) and never touches other servers in the file. It passes through the same
331
+ options as `lodedb mcp` (`--path`, `--model`, `--device`, `--exclude-text`, `--no-store-text`);
332
+ `--dry-run` prints the entry and target file without writing, and `lodedb mcp uninstall --client
333
+ <client>` removes it again. Override the config location with `--config <path>` (Claude Desktop and
334
+ LM Studio paths differ per OS), and use `--project <dir>` to write Cursor's project-level
335
+ `.cursor/mcp.json`. For Claude Code it runs `claude mcp add`; for the others it edits the config file
336
+ directly.
337
+
338
+ <details>
339
+ <summary><b>Register by hand</b> (Claude Code, Claude Desktop, Cursor, LM Studio, Codex)</summary>
340
+
341
+ The `lodedb` command must be on the host's `PATH`; if you installed into a virtual environment
342
+ (including a `uv` project) where it isn't, use the `uv run` form at the bottom.
343
+
344
+ **Claude Code, Claude Desktop, Cursor, LM Studio**: add the stdio entry to the host's MCP
345
+ config (`claude_desktop_config.json`, `.cursor/mcp.json`, or LM Studio's `mcp.json`), or run
346
+ `claude mcp add lodedb -- lodedb mcp --path ./data`:
347
+
348
+ ```json
349
+ { "mcpServers": { "lodedb": { "command": "lodedb", "args": ["mcp", "--path", "./data"] } } }
350
+ ```
351
+
352
+ **Codex**: add to `~/.codex/config.toml`:
353
+
354
+ ```toml
355
+ [mcp_servers.lodedb]
356
+ command = "lodedb"
357
+ args = ["mcp", "--path", "./data"]
358
+ ```
359
+
360
+ **From a virtual environment (uv)**, when `lodedb` is not on `PATH`:
361
+
362
+ ```json
363
+ { "mcpServers": { "lodedb": { "command": "uv",
364
+ "args": ["run", "--project", "/path/to/LodeDB", "lodedb", "mcp", "--path", "/path/to/data"] } } }
365
+ ```
366
+
367
+ See [`examples/mcp_config.json`](examples/mcp_config.json) for a copy-paste starting point.
368
+
369
+ </details>
370
+
196
371
  ## Concurrency & durability
197
372
 
198
373
  - **Single writer, many readers, per path.** One handle holds the path open for *writing* at
@@ -221,6 +396,10 @@ lodedb benchmark # local, metrics-only benchmark
221
396
  faster on the measured M1, so the MPS scan stays off by default until newer Apple GPUs are
222
397
  re-measured.
223
398
  - **Single queries run on the CPU**; the GPU serves batched `search_many`.
399
+ - **Hybrid search needs a lexical source and serves from memory.** `mode="hybrid"`/`"lexical"`
400
+ need either `store_text=True` (the index built from raw text) or `index_text=True` (a durable
401
+ `.tvlex` postings store that survives reopens without raw text). The serving index is held in
402
+ memory and maintained incrementally across mutations.
224
403
  - **Single writer per path.** One writer at a time (many concurrent readers), with no live
225
404
  cross-process refresh, on local filesystems only. See
226
405
  [Concurrency & durability](#concurrency--durability).
@@ -5,7 +5,7 @@ build-backend = "maturin"
5
5
  [project]
6
6
  name = "lodedb"
7
7
  # Static (maturin reads it here); keep in sync with src/lodedb/__init__.py __version__.
8
- version = "0.1.2"
8
+ version = "0.2.1"
9
9
  description = "Local-first, privacy-by-default embedded vector database. Your data never leaves your machine."
10
10
  readme = "README.md"
11
11
  requires-python = ">=3.11"
@@ -52,6 +52,10 @@ gpu = ["cupy-cuda12x>=13.0.0; platform_system == 'Linux'"]
52
52
  mcp = ["mcp>=1.0.0"]
53
53
  # Optional LangChain VectorStore adapter.
54
54
  langchain = ["langchain-core>=0.3.0"]
55
+ # Optional LlamaIndex VectorStore adapter.
56
+ llama-index = ["llama-index-core>=0.11.0"]
57
+ # Optional mem0 vector-store adapter.
58
+ mem0 = ["mem0ai>=2.0.0"]
55
59
  # ruff is pinned (not floored) so `ruff format`/`ruff check` are reproducible across
56
60
  # contributors and CI — the formatter's output can change between ruff releases.
57
61
  dev = ["pytest>=8.3.0", "ruff==0.15.17"]
@@ -19,7 +19,7 @@ from lodedb.local import (
19
19
  from lodedb.local.cli import app, main
20
20
 
21
21
  # Keep in sync with `version` in pyproject.toml (the release workflow asserts they match).
22
- __version__ = "0.1.2"
22
+ __version__ = "0.2.1"
23
23
 
24
24
  __all__ = [
25
25
  "LOCAL_MODEL_PRESETS",
@@ -17,6 +17,8 @@ at which a full base was last written):
17
17
  <key>.gen/g<epoch>.tvim.tvim-delta/ its ``.tvd`` vector journal
18
18
  <key>.gen/g<epoch>.tvtext opt-in raw-text base (full id->text map)
19
19
  <key>.gen/g<epoch>.tvtext.tvtext-delta/ its ``.txd`` raw-text journal
20
+ <key>.gen/g<epoch>.tvlex opt-in lexical-index base (id->token lists)
21
+ <key>.gen/g<epoch>.tvlex.tvlex-delta/ its ``.lxd`` lexical-index journal
20
22
 
21
23
  and a single top-level pointer:
22
24
 
@@ -89,6 +91,12 @@ def base_tvtext_path(persistence_dir: str | Path, index_key: str, epoch: int) ->
89
91
  return generation_dir(persistence_dir, index_key) / f"g{int(epoch)}.tvtext"
90
92
 
91
93
 
94
+ def base_tvlex_path(persistence_dir: str | Path, index_key: str, epoch: int) -> Path:
95
+ """Returns the lexical-index base path for one index key and base epoch."""
96
+
97
+ return generation_dir(persistence_dir, index_key) / f"g{int(epoch)}.tvlex"
98
+
99
+
92
100
  def is_commit_manifest_name(name: str) -> bool:
93
101
  """Returns whether a top-level file name is a root commit manifest."""
94
102
 
@@ -173,13 +181,19 @@ def build_commit_body(
173
181
  tvim_manifest: dict[str, Any] | None,
174
182
  tvim_present: bool,
175
183
  tvtext_manifest: dict[str, Any] | None,
184
+ tvlex_manifest: dict[str, Any] | None = None,
176
185
  ) -> dict[str, Any]:
177
186
  """Assembles a root-manifest body capturing one consistent committed generation.
178
187
 
179
188
  ``tvtext_manifest`` is the raw-text journal manifest (base + delta segments)
180
189
  for this committed generation, or ``None`` when raw-text storage is disabled
181
- or the index holds no text. It is pinned by the root exactly like the JSON
182
- and TurboVec manifests, so raw text commits and rolls back atomically.
190
+ or the index holds no text. ``tvlex_manifest`` is the analogous lexical-index
191
+ (BM25 postings) journal manifest, or ``None`` when lexical-index persistence
192
+ is disabled or the index holds no tokens. Both are pinned by the root exactly
193
+ like the JSON and TurboVec manifests, so the text sidecar and the lexical
194
+ sidecar commit and roll back atomically with the generation. ``tvlex`` is an
195
+ optional key read via ``.get`` (like ``tvtext``), so an older reader that
196
+ does not know it ignores it.
183
197
  """
184
198
 
185
199
  return {
@@ -192,6 +206,7 @@ def build_commit_body(
192
206
  "tvim": tvim_manifest,
193
207
  "tvim_present": bool(tvim_present),
194
208
  "tvtext": tvtext_manifest,
209
+ "tvlex": tvlex_manifest,
195
210
  }
196
211
 
197
212