jmfts 0.2.0__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 (206) hide show
  1. jmfts-0.2.0/LICENSE +21 -0
  2. jmfts-0.2.0/PKG-INFO +297 -0
  3. jmfts-0.2.0/README.md +235 -0
  4. jmfts-0.2.0/jmfts.egg-info/PKG-INFO +297 -0
  5. jmfts-0.2.0/jmfts.egg-info/SOURCES.txt +204 -0
  6. jmfts-0.2.0/jmfts.egg-info/dependency_links.txt +1 -0
  7. jmfts-0.2.0/jmfts.egg-info/entry_points.txt +5 -0
  8. jmfts-0.2.0/jmfts.egg-info/requires.txt +39 -0
  9. jmfts-0.2.0/jmfts.egg-info/top_level.txt +2 -0
  10. jmfts-0.2.0/jmfts_batch/README.md +161 -0
  11. jmfts-0.2.0/jmfts_batch/__init__.py +37 -0
  12. jmfts-0.2.0/jmfts_batch/cli.py +270 -0
  13. jmfts-0.2.0/jmfts_batch/provider.py +168 -0
  14. jmfts-0.2.0/jmfts_batch/providers/__init__.py +5 -0
  15. jmfts-0.2.0/jmfts_batch/providers/anthropic_api.py +200 -0
  16. jmfts-0.2.0/jmfts_batch/providers/mock.py +291 -0
  17. jmfts-0.2.0/jmfts_batch/providers/openai_api.py +215 -0
  18. jmfts-0.2.0/jmfts_batch/summarize.py +182 -0
  19. jmfts-0.2.0/jmfts_batch/worker.py +510 -0
  20. jmfts-0.2.0/jmfts_core/__init__.py +3 -0
  21. jmfts-0.2.0/jmfts_core/access.py +302 -0
  22. jmfts-0.2.0/jmfts_core/arxiv_fetch.py +117 -0
  23. jmfts-0.2.0/jmfts_core/chunking.py +363 -0
  24. jmfts-0.2.0/jmfts_core/citation_tasks.py +263 -0
  25. jmfts-0.2.0/jmfts_core/client.py +139 -0
  26. jmfts-0.2.0/jmfts_core/config.py +350 -0
  27. jmfts-0.2.0/jmfts_core/conversation_ingest.py +674 -0
  28. jmfts-0.2.0/jmfts_core/database.py +95 -0
  29. jmfts-0.2.0/jmfts_core/db_setup.py +139 -0
  30. jmfts-0.2.0/jmfts_core/embed_tasks.py +86 -0
  31. jmfts-0.2.0/jmfts_core/embedder.py +342 -0
  32. jmfts-0.2.0/jmfts_core/embedding.py +612 -0
  33. jmfts-0.2.0/jmfts_core/explain_wire.py +70 -0
  34. jmfts-0.2.0/jmfts_core/fact_extraction.py +560 -0
  35. jmfts-0.2.0/jmfts_core/graph_analysis.py +894 -0
  36. jmfts-0.2.0/jmfts_core/ingest_options.py +342 -0
  37. jmfts-0.2.0/jmfts_core/ingest_tasks.py +1232 -0
  38. jmfts-0.2.0/jmfts_core/ingest_worker.py +492 -0
  39. jmfts-0.2.0/jmfts_core/lint.py +328 -0
  40. jmfts-0.2.0/jmfts_core/llm_utils.py +22 -0
  41. jmfts-0.2.0/jmfts_core/models/__init__.py +34 -0
  42. jmfts-0.2.0/jmfts_core/models/document.py +216 -0
  43. jmfts-0.2.0/jmfts_core/models/document_blob.py +72 -0
  44. jmfts-0.2.0/jmfts_core/models/principal.py +76 -0
  45. jmfts-0.2.0/jmfts_core/models/search_context.py +47 -0
  46. jmfts-0.2.0/jmfts_core/models/search_index.py +101 -0
  47. jmfts-0.2.0/jmfts_core/models/task_queue.py +253 -0
  48. jmfts-0.2.0/jmfts_core/models/token_embedding.py +63 -0
  49. jmfts-0.2.0/jmfts_core/models/triple.py +161 -0
  50. jmfts-0.2.0/jmfts_core/models/usetype_presentation.py +46 -0
  51. jmfts-0.2.0/jmfts_core/office/__init__.py +104 -0
  52. jmfts-0.2.0/jmfts_core/office/extract.py +390 -0
  53. jmfts-0.2.0/jmfts_core/pdf_extraction.py +629 -0
  54. jmfts-0.2.0/jmfts_core/pipeline.py +1292 -0
  55. jmfts-0.2.0/jmfts_core/principal_context.py +56 -0
  56. jmfts-0.2.0/jmfts_core/probe.py +1209 -0
  57. jmfts-0.2.0/jmfts_core/query_router.py +236 -0
  58. jmfts-0.2.0/jmfts_core/registry.py +110 -0
  59. jmfts-0.2.0/jmfts_core/repositories/__init__.py +13 -0
  60. jmfts-0.2.0/jmfts_core/repositories/blob.py +269 -0
  61. jmfts-0.2.0/jmfts_core/repositories/document.py +1094 -0
  62. jmfts-0.2.0/jmfts_core/repositories/search.py +1375 -0
  63. jmfts-0.2.0/jmfts_core/repositories/search_context.py +77 -0
  64. jmfts-0.2.0/jmfts_core/repositories/task_queue.py +927 -0
  65. jmfts-0.2.0/jmfts_core/repositories/triple.py +367 -0
  66. jmfts-0.2.0/jmfts_core/repositories/usetype_presentation.py +124 -0
  67. jmfts-0.2.0/jmfts_core/repositories/view.py +271 -0
  68. jmfts-0.2.0/jmfts_core/reranker.py +161 -0
  69. jmfts-0.2.0/jmfts_core/rest/__init__.py +1 -0
  70. jmfts-0.2.0/jmfts_core/rest/auth.py +227 -0
  71. jmfts-0.2.0/jmfts_core/rest/main.py +406 -0
  72. jmfts-0.2.0/jmfts_core/rest/routers/__init__.py +1 -0
  73. jmfts-0.2.0/jmfts_core/rest/routers/conversations.py +12 -0
  74. jmfts-0.2.0/jmfts_core/rest/routers/documents.py +15 -0
  75. jmfts-0.2.0/jmfts_core/rest/routers/graph.py +13 -0
  76. jmfts-0.2.0/jmfts_core/rest/routers/indexes.py +14 -0
  77. jmfts-0.2.0/jmfts_core/rest/routers/runner.py +147 -0
  78. jmfts-0.2.0/jmfts_core/rest/routers/search.py +15 -0
  79. jmfts-0.2.0/jmfts_core/rest/routers/search_contexts.py +14 -0
  80. jmfts-0.2.0/jmfts_core/rest/routers/templates.py +18 -0
  81. jmfts-0.2.0/jmfts_core/rest/routers/triples.py +14 -0
  82. jmfts-0.2.0/jmfts_core/rest/routers/usetype_presentations.py +15 -0
  83. jmfts-0.2.0/jmfts_core/rest/routers/view.py +15 -0
  84. jmfts-0.2.0/jmfts_core/rest/schemas.py +305 -0
  85. jmfts-0.2.0/jmfts_core/rest/wiring.py +282 -0
  86. jmfts-0.2.0/jmfts_core/rollup_tasks.py +778 -0
  87. jmfts-0.2.0/jmfts_core/segmentation.py +144 -0
  88. jmfts-0.2.0/jmfts_core/services/__init__.py +36 -0
  89. jmfts-0.2.0/jmfts_core/services/access_service.py +263 -0
  90. jmfts-0.2.0/jmfts_core/services/conversation_service.py +163 -0
  91. jmfts-0.2.0/jmfts_core/services/document_service.py +1050 -0
  92. jmfts-0.2.0/jmfts_core/services/graph_service.py +407 -0
  93. jmfts-0.2.0/jmfts_core/services/index_service.py +219 -0
  94. jmfts-0.2.0/jmfts_core/services/ingest_service.py +810 -0
  95. jmfts-0.2.0/jmfts_core/services/search_context_service.py +131 -0
  96. jmfts-0.2.0/jmfts_core/services/search_service.py +663 -0
  97. jmfts-0.2.0/jmfts_core/services/template_service.py +320 -0
  98. jmfts-0.2.0/jmfts_core/services/triple_service.py +441 -0
  99. jmfts-0.2.0/jmfts_core/services/usetype_presentation_service.py +149 -0
  100. jmfts-0.2.0/jmfts_core/services/view_service.py +244 -0
  101. jmfts-0.2.0/jmfts_core/settling.py +421 -0
  102. jmfts-0.2.0/jmfts_core/sql/__init__.py +76 -0
  103. jmfts-0.2.0/jmfts_core/sql/migrations/002_temporal_triples.sql +33 -0
  104. jmfts-0.2.0/jmfts_core/sql/migrations/003_purge_ghost_predicates.sql +47 -0
  105. jmfts-0.2.0/jmfts_core/sql/migrations/004_document_position.sql +34 -0
  106. jmfts-0.2.0/jmfts_core/sql/migrations/005_document_event_time.sql +46 -0
  107. jmfts-0.2.0/jmfts_core/sql/migrations/006_access_control.sql +65 -0
  108. jmfts-0.2.0/jmfts_core/sql/migrations/007_term_postings_doc_index.sql +32 -0
  109. jmfts-0.2.0/jmfts_core/sql/migrations/008_document_settled_lifecycle.sql +131 -0
  110. jmfts-0.2.0/jmfts_core/sql/migrations/009_document_blobs.sql +85 -0
  111. jmfts-0.2.0/jmfts_core/sql/migrations/010_task_queue.sql +146 -0
  112. jmfts-0.2.0/jmfts_core/sql/migrations/011_task_queue_heartbeat.sql +58 -0
  113. jmfts-0.2.0/jmfts_core/sql/migrations/012_task_queue_batched.sql +78 -0
  114. jmfts-0.2.0/jmfts_core/sql/schema.sql +592 -0
  115. jmfts-0.2.0/jmfts_core/structural_splitting.py +306 -0
  116. jmfts-0.2.0/jmfts_core/structure_tasks.py +996 -0
  117. jmfts-0.2.0/jmfts_core/summarization.py +588 -0
  118. jmfts-0.2.0/jmfts_core/synthesis.py +97 -0
  119. jmfts-0.2.0/jmfts_core/task_errors.py +130 -0
  120. jmfts-0.2.0/jmfts_core/task_routing.py +131 -0
  121. jmfts-0.2.0/jmfts_core/token_selection.py +551 -0
  122. jmfts-0.2.0/jmfts_core/unit_of_work.py +95 -0
  123. jmfts-0.2.0/jmfts_core/url_fetch.py +116 -0
  124. jmfts-0.2.0/jmfts_core/view_renderer.py +154 -0
  125. jmfts-0.2.0/jmfts_core/worker.py +330 -0
  126. jmfts-0.2.0/pyproject.toml +302 -0
  127. jmfts-0.2.0/setup.cfg +4 -0
  128. jmfts-0.2.0/tests/test_access_control.py +251 -0
  129. jmfts-0.2.0/tests/test_access_edges.py +138 -0
  130. jmfts-0.2.0/tests/test_access_management.py +206 -0
  131. jmfts-0.2.0/tests/test_access_write_gate.py +183 -0
  132. jmfts-0.2.0/tests/test_api_auth.py +133 -0
  133. jmfts-0.2.0/tests/test_api_parity.py +302 -0
  134. jmfts-0.2.0/tests/test_batch_worker.py +736 -0
  135. jmfts-0.2.0/tests/test_bm25_index_lock.py +50 -0
  136. jmfts-0.2.0/tests/test_bulk_ingest_errors.py +348 -0
  137. jmfts-0.2.0/tests/test_chunking.py +425 -0
  138. jmfts-0.2.0/tests/test_citation_task.py +401 -0
  139. jmfts-0.2.0/tests/test_client_codegen.py +227 -0
  140. jmfts-0.2.0/tests/test_client_roundtrip.py +140 -0
  141. jmfts-0.2.0/tests/test_conversation_ingest.py +557 -0
  142. jmfts-0.2.0/tests/test_coreference.py +113 -0
  143. jmfts-0.2.0/tests/test_dedup_create.py +90 -0
  144. jmfts-0.2.0/tests/test_delete_link.py +87 -0
  145. jmfts-0.2.0/tests/test_doc_response_coverage.py +67 -0
  146. jmfts-0.2.0/tests/test_document_lifecycle.py +427 -0
  147. jmfts-0.2.0/tests/test_document_position.py +169 -0
  148. jmfts-0.2.0/tests/test_embed_task.py +335 -0
  149. jmfts-0.2.0/tests/test_ensonet_openai_compat.py +628 -0
  150. jmfts-0.2.0/tests/test_equal_weight_rrf.py +76 -0
  151. jmfts-0.2.0/tests/test_fact_extraction.py +485 -0
  152. jmfts-0.2.0/tests/test_file_upload.py +1260 -0
  153. jmfts-0.2.0/tests/test_graph_analysis.py +162 -0
  154. jmfts-0.2.0/tests/test_graph_neighbors.py +116 -0
  155. jmfts-0.2.0/tests/test_hybrid_rerank.py +149 -0
  156. jmfts-0.2.0/tests/test_importance_from_salience.py +64 -0
  157. jmfts-0.2.0/tests/test_ingest_analyze.py +551 -0
  158. jmfts-0.2.0/tests/test_ingest_attempts.py +491 -0
  159. jmfts-0.2.0/tests/test_ingest_explain.py +567 -0
  160. jmfts-0.2.0/tests/test_ingest_options.py +476 -0
  161. jmfts-0.2.0/tests/test_ingest_scheduling_races.py +509 -0
  162. jmfts-0.2.0/tests/test_ingest_text.py +421 -0
  163. jmfts-0.2.0/tests/test_ingest_worker.py +1320 -0
  164. jmfts-0.2.0/tests/test_known_defects.py +346 -0
  165. jmfts-0.2.0/tests/test_lint.py +53 -0
  166. jmfts-0.2.0/tests/test_local_client.py +141 -0
  167. jmfts-0.2.0/tests/test_maxsim_rerank_wiring.py +97 -0
  168. jmfts-0.2.0/tests/test_no_host_addresses.py +140 -0
  169. jmfts-0.2.0/tests/test_office_extraction.py +384 -0
  170. jmfts-0.2.0/tests/test_office_packaging.py +209 -0
  171. jmfts-0.2.0/tests/test_openapi_docs.py +193 -0
  172. jmfts-0.2.0/tests/test_packaging.py +266 -0
  173. jmfts-0.2.0/tests/test_pdf_extraction.py +649 -0
  174. jmfts-0.2.0/tests/test_pipeline.py +536 -0
  175. jmfts-0.2.0/tests/test_pipeline_idempotent.py +36 -0
  176. jmfts-0.2.0/tests/test_probe_office.py +884 -0
  177. jmfts-0.2.0/tests/test_query_router.py +97 -0
  178. jmfts-0.2.0/tests/test_raptor.py +686 -0
  179. jmfts-0.2.0/tests/test_readme_links.py +186 -0
  180. jmfts-0.2.0/tests/test_recall_as_of.py +118 -0
  181. jmfts-0.2.0/tests/test_regression_api.py +596 -0
  182. jmfts-0.2.0/tests/test_regression_ingestion.py +407 -0
  183. jmfts-0.2.0/tests/test_regression_knowledge_graph.py +500 -0
  184. jmfts-0.2.0/tests/test_regression_search.py +496 -0
  185. jmfts-0.2.0/tests/test_reparent_verb.py +97 -0
  186. jmfts-0.2.0/tests/test_reranker.py +480 -0
  187. jmfts-0.2.0/tests/test_rollup_pelt.py +564 -0
  188. jmfts-0.2.0/tests/test_runner_auth.py +184 -0
  189. jmfts-0.2.0/tests/test_runner_embed.py +277 -0
  190. jmfts-0.2.0/tests/test_search_contexts.py +167 -0
  191. jmfts-0.2.0/tests/test_search_regression.py +825 -0
  192. jmfts-0.2.0/tests/test_segment_ingest.py +224 -0
  193. jmfts-0.2.0/tests/test_segmentation.py +249 -0
  194. jmfts-0.2.0/tests/test_settling.py +590 -0
  195. jmfts-0.2.0/tests/test_structural_splitting.py +320 -0
  196. jmfts-0.2.0/tests/test_structure_tasks.py +391 -0
  197. jmfts-0.2.0/tests/test_supersede_race.py +110 -0
  198. jmfts-0.2.0/tests/test_synthesis.py +342 -0
  199. jmfts-0.2.0/tests/test_task_queue.py +1020 -0
  200. jmfts-0.2.0/tests/test_templates.py +311 -0
  201. jmfts-0.2.0/tests/test_thin_worker.py +558 -0
  202. jmfts-0.2.0/tests/test_thread_safety.py +123 -0
  203. jmfts-0.2.0/tests/test_unit_of_work.py +169 -0
  204. jmfts-0.2.0/tests/test_upsert_triple_verb.py +109 -0
  205. jmfts-0.2.0/tests/test_view_renderer.py +160 -0
  206. jmfts-0.2.0/tests/test_write_races.py +165 -0
jmfts-0.2.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Fight Fire with Fire Robotics, LLC
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
jmfts-0.2.0/PKG-INFO ADDED
@@ -0,0 +1,297 @@
1
+ Metadata-Version: 2.4
2
+ Name: jmfts
3
+ Version: 0.2.0
4
+ Summary: John McCardle's Fusion Tree Search - A focused retrieval appliance with matryoshka embeddings and late interaction
5
+ Author: Fight Fire with Fire Robotics, LLC
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/jmccardle/jmfts
8
+ Project-URL: Repository, https://github.com/jmccardle/jmfts
9
+ Project-URL: Issues, https://github.com/jmccardle/jmfts/issues
10
+ Keywords: retrieval,embeddings,colbert,bm25,pgvector,rag
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: Operating System :: POSIX :: Linux
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
20
+ Classifier: Topic :: Database :: Database Engines/Servers
21
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
22
+ Classifier: Topic :: Text Processing :: Indexing
23
+ Requires-Python: >=3.11
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE
26
+ Requires-Dist: jmfts-client==0.2.0
27
+ Requires-Dist: fastapi>=0.109.0
28
+ Requires-Dist: python-multipart>=0.0.9
29
+ Requires-Dist: uvicorn[standard]>=0.27.0
30
+ Requires-Dist: sqlalchemy>=2.0.25
31
+ Requires-Dist: psycopg2-binary>=2.9.9
32
+ Requires-Dist: pgvector>=0.2.4
33
+ Requires-Dist: pydantic>=2.5.0
34
+ Requires-Dist: pydantic-settings>=2.1.0
35
+ Requires-Dist: transformers>=4.40
36
+ Requires-Dist: numpy>=1.26.0
37
+ Requires-Dist: httpx>=0.26.0
38
+ Requires-Dist: ruptures>=1.1.8
39
+ Requires-Dist: python-igraph>=0.11.0
40
+ Requires-Dist: leidenalg>=0.10.0
41
+ Requires-Dist: markdown-it-py>=3.0.0
42
+ Requires-Dist: pymupdf>=1.24
43
+ Requires-Dist: markdownify>=0.11
44
+ Requires-Dist: olefile>=0.47
45
+ Provides-Extra: embed
46
+ Requires-Dist: torch>=2.1.0; extra == "embed"
47
+ Requires-Dist: sentence-transformers>=2.2.2; extra == "embed"
48
+ Provides-Extra: office
49
+ Requires-Dist: python-docx>=1.1.0; extra == "office"
50
+ Requires-Dist: python-pptx>=0.6.23; extra == "office"
51
+ Requires-Dist: openpyxl>=3.1.0; extra == "office"
52
+ Provides-Extra: convert
53
+ Requires-Dist: unoserver>=3.0; extra == "convert"
54
+ Provides-Extra: dev
55
+ Requires-Dist: jmfts[embed]; extra == "dev"
56
+ Requires-Dist: jmfts[office]; extra == "dev"
57
+ Requires-Dist: pytest>=7.4.0; extra == "dev"
58
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == "dev"
59
+ Requires-Dist: black>=24.1.0; extra == "dev"
60
+ Requires-Dist: ruff>=0.1.0; extra == "dev"
61
+ Dynamic: license-file
62
+
63
+ # JMFTS
64
+
65
+ JMFTS (John McCardle's Fusion Tree Search) is a retrieval appliance: a
66
+ research-focused PostgreSQL + pgvector service combining matryoshka
67
+ embeddings, ColBERT-style late interaction (MaxSim), and BM25 into hybrid
68
+ search over a tree-structured document store.
69
+
70
+ Documents form a tree (`parent_id` + a materialized `path`), carry typed
71
+ cross-references to each other, and can hold temporal subject-predicate-object
72
+ triples with supersession instead of deletion. Five retrieval methods sit on
73
+ top: `vector`, `bm25`, `fulltext`, `maxsim`, and `hybrid` (a weighted/RRF
74
+ combination), plus an `auto` router. Ingestion pipelines (`markdown`,
75
+ `conversation`, `raw`, `transcript`, `wiki:url`, `wiki:arxiv`, `wiki:pdf`) are
76
+ idempotent on `(content_hash, parent_id)`.
77
+
78
+ Uploaded files are identified by their bytes, not their extension, and each
79
+ format is read into markdown before anything indexes it. Today that is `.pdf`,
80
+ plain text and markdown, HTML, and — with the `office` extra — `.docx` and
81
+ `.pptx`. An `.xlsx` is identified and described but not yet read. What a given
82
+ file would do is a question you can ask before uploading it: `POST /ingest/explain`
83
+ returns the plan, task by task, with the reason for every row that will not run.
84
+
85
+ ## Status
86
+
87
+ Alpha. The retrieval and ingestion paths are exercised by a large test suite and
88
+ by benchmark runs; the interfaces are still moving.
89
+
90
+ **Measured, nDCG@10 on four standard BEIR datasets:**
91
+
92
+ | dataset | vector | bm25 | hybrid |
93
+ |---|---|---|---|
94
+ | SciFact | 0.6769 | 0.6595 | **0.7090** |
95
+ | NFCorpus | 0.3077 | 0.3043 | **0.3255** |
96
+ | FiQA | **0.3908** | 0.2363 | 0.3992 |
97
+ | TREC-COVID | **0.841** | 0.580 | 0.837 |
98
+ | *average* | 0.554 | 0.445 | **0.568** |
99
+
100
+ Hybrid wins three of the four; vector alone wins TREC-COVID, where our BM25
101
+ (0.580) sits below the canonical Anserini BEIR baseline of roughly 0.656.
102
+ Per-dataset weight tuning moves SciFact and NFCorpus by under a point. The full
103
+ breakdown, the caveats, and the comparisons against published ColBERTv2 and
104
+ SPLADE++ numbers are recorded in the development repository, not in this tree.
105
+
106
+ **One thing worth knowing before you rely on it.** `?rerank=true` is unmeasured.
107
+ The cross-encoder second stage loads a standard `CrossEncoder`
108
+ (`JMFTS_RERANKER_MODEL`, default `cross-encoder/ms-marco-MiniLM-L-6-v2`), and it
109
+ surfaces load and scoring errors rather than silently falling back to the
110
+ first-stage ranking. But that default was chosen for size and CPU viability, not
111
+ for measured retrieval quality — `vector→crossenc@100` has never been swept
112
+ against `vector→maxsim@200`. Treat reranked ordering as unvalidated. The
113
+ `maxsim` rerank method reads token embeddings you already stored and loads no
114
+ model at all.
115
+
116
+ ## Install and run
117
+
118
+ You need PostgreSQL 14 or newer with the `pgvector` extension available. JMFTS
119
+ does not install or manage it.
120
+
121
+ ```bash
122
+ pip install jmfts # base: no torch, cannot embed by itself
123
+ pip install 'jmfts[embed]' # + the model stack, for a single appliance
124
+ jmfts-init-db # create the database and load the schema
125
+ jmfts-server # serve on 0.0.0.0:8100
126
+ jmfts-server --port 9000 # ...or override one setting for this run
127
+ jmfts-server --help # what the JMFTS_* defaults currently resolve to
128
+ ```
129
+
130
+ For a working PostgreSQL and API in one command, the compose file brings up
131
+ `pgvector/pgvector:pg16` alongside the API:
132
+
133
+ ```bash
134
+ docker compose up
135
+ ```
136
+
137
+ From a checkout, for development:
138
+
139
+ ```bash
140
+ pip install -e ./jmfts-client # the client distribution; jmfts depends on it
141
+ pip install -e ".[dev]" # editable, with pytest/black/ruff; implies [embed]
142
+ uvicorn jmfts_core.rest.main:app --host 0.0.0.0 --port 8100 --reload
143
+ ```
144
+
145
+ This tree builds two distributions, and the first line is not optional. `jmfts` declares
146
+ `jmfts-client==0.2.0`, which is not on PyPI yet, so the second line alone fails to
147
+ resolve it.
148
+
149
+ ### Reading and driving the API
150
+
151
+ `/docs` is Swagger UI over the live route table — 100 operations, grouped by tag, with the
152
+ request and response schemas. `/redoc` is the same document laid out for reading, and
153
+ `/openapi.json` is the document itself.
154
+
155
+ **A generated client already exists — do not write your own against `/openapi.json`.**
156
+ `pip install jmfts-client` gives you every one of those operations as a Python method, with
157
+ the request and response models the appliance itself validates against:
158
+
159
+ ```python
160
+ from jmfts_client import RemoteJmftsClient
161
+ from jmfts_client.contracts import DocumentCreate, HybridSearchRequest
162
+
163
+ with RemoteJmftsClient("http://localhost:8100", token="...") as jmfts:
164
+ jmfts.create_document(DocumentCreate(title="Ada", content="Ada Lovelace"))
165
+ hits = jmfts.hybrid_search(HybridSearchRequest(query="Ada", limit=10))
166
+ ```
167
+
168
+ It carries `httpx` and `pydantic` and nothing else, so calling an appliance does not mean
169
+ installing one. Nobody writes those methods: a service method marked `@expose` becomes a
170
+ REST route, an entry in this OpenAPI document, a method on the in-process
171
+ `LocalJmftsClient`, and a method there — four views of one definition, with a test holding
172
+ each of them to it. See `jmfts-client/README.md`.
173
+
174
+ Press **Authorize** and paste `JMFTS_API_TOKEN` before trying an operation; the page names
175
+ which of the two credentials each one takes, since `/runner/*` uses `JMFTS_RUNNER_KEY`
176
+ instead and `GET /health` needs neither. All three pages answer without a token — a browser
177
+ navigating to a page cannot send an `Authorization` header, so gating them would close the
178
+ page rather than protect it. They expose the interface; reaching anything they describe
179
+ still costs a token.
180
+
181
+ Both pages load Swagger UI and ReDoc from `cdn.jsdelivr.net`, which is FastAPI's default. A
182
+ host with no route to the internet renders a blank page and must read `/openapi.json`
183
+ directly, or be given locally served copies of those assets.
184
+
185
+ ### The office readers are an extra
186
+
187
+ **`pip install jmfts` reads PDF, text, markdown and HTML. `.docx` and `.pptx` need
188
+ `[office]`:**
189
+
190
+ ```bash
191
+ pip install 'jmfts[office]' # python-docx, python-pptx, openpyxl
192
+ ```
193
+
194
+ The split is the same one the model stack draws, for the same reason. A base install
195
+ already *identifies* an office file and reports what it declares — how many slides, which
196
+ sheets, whether it carries macros or an unplaced member — because that runs on `zipfile`
197
+ alone and `probe` must always run. Opening one to get the text out is what needs the
198
+ readers. So a storage-side worker that never ingests office files is correctly installed
199
+ and correctly has no `python-docx`, and asking one to read a `.docx` raises
200
+ `OfficeStackNotInstalled`, which names the extra rather than reading as a broken
201
+ environment.
202
+
203
+ ### The model is an extra
204
+
205
+ **`pip install jmfts` does not install torch.** Base JMFTS is storage, retrieval, the
206
+ tree, BM25, the queue and the whole ingest pipeline; the only step that needs an
207
+ accelerator is producing vectors, and that step can be somebody else's. Measured:
208
+ 584 MB installed, against 5.2 GB with the model stack.
209
+
210
+ Add `[embed]` when *this* process should run the model — because it serves `/search`,
211
+ because it serves `/runner` for others, or because it is a single appliance doing both:
212
+
213
+ ```bash
214
+ pip install 'jmfts[embed]' # CUDA build of torch
215
+ # or, for CPU — the wheel index is an install-time choice, so it is two steps:
216
+ pip install torch --index-url https://download.pytorch.org/whl/cpu
217
+ pip install 'jmfts[embed]'
218
+ ```
219
+
220
+ Without it, an install can still *measure* text — `check_fit`, the chunker, matryoshka
221
+ truncation are all tokenizer and numpy — and asks another JMFTS for the vectors:
222
+
223
+ ```bash
224
+ JMFTS_RUNNER_URL=http://the-gpu-box:8100 JMFTS_RUNNER_KEY=<shared secret> jmfts-worker
225
+ ```
226
+
227
+ That is the intended shape for an ingest worker, and the reason the split exists: a fleet
228
+ is mostly storage-side workers, and none of them need several GB of CUDA to split text and
229
+ write rows. Asking for vectors with no model and no runner raises `ModelStackNotInstalled`,
230
+ which names both ways out — it never silently degrades. See `deploy/README.md`.
231
+
232
+ `./scripts/check_base_install.sh` proves the claim rather than asserting it: it builds an
233
+ empty virtualenv, installs base JMFTS into it, and checks there that the API and the
234
+ worker import, the tokenizer half works, asking for a vector raises, and no office reader
235
+ is reachable.
236
+
237
+ ### Configuration
238
+
239
+ Configuration is environment-variable driven (`JMFTS_` prefix) — see
240
+ `.env.example` for the full list, grouped by concern (DB, embedding model,
241
+ token selection, BM25 tuning, auth/CORS, the worker and the runner).
242
+
243
+ Run the test suite against an isolated, throwaway database (never the real
244
+ appliance DB):
245
+
246
+ ```bash
247
+ ./scripts/run_tests_docker.sh # easiest: throwaway pgvector + CPU embed
248
+ pytest # native: needs `ALTER ROLE jmfts CREATEDB`
249
+ ```
250
+
251
+ ## Layout
252
+
253
+ ```
254
+ jmfts_core/ the library: services, repositories, ORM models, office readers
255
+ jmfts_core/rest/ FastAPI surface — routers generated from the @expose registry
256
+ jmfts_core/sql/ schema.sql and the incremental migrations, shipped in the package
257
+ jmfts-client/ the second distribution: wire contracts + the generated HTTP client
258
+ jmfts_batch/ batch summarization over an OpenAI- or Anthropic-style batch API
259
+ scripts/ CLI clients over the REST API
260
+ deploy/ Kubernetes manifests for a worker fleet, and KEDA scaling on queue depth
261
+ plugin/ Claude Code plugin exposing JMFTS as an agent's durable memory
262
+ tests/ pytest suite against an ephemeral database, plus the fidelity corpus
263
+ ```
264
+
265
+ ## Where to go next
266
+
267
+ - **Running a worker fleet**: `deploy/README.md` — what routes where, badges, the
268
+ thin worker that does not hold the model, and the one way a badged fleet can
269
+ stall.
270
+ - **Contributing / architecture**: `CLAUDE.md` — the architecture diagram,
271
+ data flow, code style, and the search repository's internals (vector, BM25,
272
+ MaxSim, hybrid).
273
+ - **Using JMFTS as an agent's memory**: `plugin/jmfts/` — a Claude Code
274
+ plugin exposing JMFTS as durable cross-session memory via search/ingest/
275
+ read/explore/analyze skills. `plugin/jmfts/skills/jmfts/SKILL.md` is the
276
+ concept overview.
277
+ - **The batch worker**: `jmfts_batch/README.md` — consuming `summarize:llm`
278
+ through an external batch API.
279
+ - **Calling JMFTS from Python**: `jmfts-client/README.md` — the second
280
+ distribution, and what a generated verb is generated from.
281
+
282
+ ## A note on documentation
283
+
284
+ The design documents this code was written against — most importantly the
285
+ ingest specification that dozens of source comments cite by section number —
286
+ are not in this release. They are being refined for publication separately.
287
+ Comments referring to INGEST_SPEC.md, OFFICE_SPEC.md, CORPUS.md,
288
+ KNOWN-DEFECTS.md and ROADMAP.md point at documents that will land later; the
289
+ code stands on its own in the meantime.
290
+
291
+ Those names are deliberately not written as links. There is nothing in this
292
+ tree for them to point at, and marking them up as paths would promise
293
+ otherwise.
294
+
295
+ ## Licence
296
+
297
+ MIT. Copyright (c) 2026 Fight Fire with Fire Robotics, LLC. See `LICENSE`.
jmfts-0.2.0/README.md ADDED
@@ -0,0 +1,235 @@
1
+ # JMFTS
2
+
3
+ JMFTS (John McCardle's Fusion Tree Search) is a retrieval appliance: a
4
+ research-focused PostgreSQL + pgvector service combining matryoshka
5
+ embeddings, ColBERT-style late interaction (MaxSim), and BM25 into hybrid
6
+ search over a tree-structured document store.
7
+
8
+ Documents form a tree (`parent_id` + a materialized `path`), carry typed
9
+ cross-references to each other, and can hold temporal subject-predicate-object
10
+ triples with supersession instead of deletion. Five retrieval methods sit on
11
+ top: `vector`, `bm25`, `fulltext`, `maxsim`, and `hybrid` (a weighted/RRF
12
+ combination), plus an `auto` router. Ingestion pipelines (`markdown`,
13
+ `conversation`, `raw`, `transcript`, `wiki:url`, `wiki:arxiv`, `wiki:pdf`) are
14
+ idempotent on `(content_hash, parent_id)`.
15
+
16
+ Uploaded files are identified by their bytes, not their extension, and each
17
+ format is read into markdown before anything indexes it. Today that is `.pdf`,
18
+ plain text and markdown, HTML, and — with the `office` extra — `.docx` and
19
+ `.pptx`. An `.xlsx` is identified and described but not yet read. What a given
20
+ file would do is a question you can ask before uploading it: `POST /ingest/explain`
21
+ returns the plan, task by task, with the reason for every row that will not run.
22
+
23
+ ## Status
24
+
25
+ Alpha. The retrieval and ingestion paths are exercised by a large test suite and
26
+ by benchmark runs; the interfaces are still moving.
27
+
28
+ **Measured, nDCG@10 on four standard BEIR datasets:**
29
+
30
+ | dataset | vector | bm25 | hybrid |
31
+ |---|---|---|---|
32
+ | SciFact | 0.6769 | 0.6595 | **0.7090** |
33
+ | NFCorpus | 0.3077 | 0.3043 | **0.3255** |
34
+ | FiQA | **0.3908** | 0.2363 | 0.3992 |
35
+ | TREC-COVID | **0.841** | 0.580 | 0.837 |
36
+ | *average* | 0.554 | 0.445 | **0.568** |
37
+
38
+ Hybrid wins three of the four; vector alone wins TREC-COVID, where our BM25
39
+ (0.580) sits below the canonical Anserini BEIR baseline of roughly 0.656.
40
+ Per-dataset weight tuning moves SciFact and NFCorpus by under a point. The full
41
+ breakdown, the caveats, and the comparisons against published ColBERTv2 and
42
+ SPLADE++ numbers are recorded in the development repository, not in this tree.
43
+
44
+ **One thing worth knowing before you rely on it.** `?rerank=true` is unmeasured.
45
+ The cross-encoder second stage loads a standard `CrossEncoder`
46
+ (`JMFTS_RERANKER_MODEL`, default `cross-encoder/ms-marco-MiniLM-L-6-v2`), and it
47
+ surfaces load and scoring errors rather than silently falling back to the
48
+ first-stage ranking. But that default was chosen for size and CPU viability, not
49
+ for measured retrieval quality — `vector→crossenc@100` has never been swept
50
+ against `vector→maxsim@200`. Treat reranked ordering as unvalidated. The
51
+ `maxsim` rerank method reads token embeddings you already stored and loads no
52
+ model at all.
53
+
54
+ ## Install and run
55
+
56
+ You need PostgreSQL 14 or newer with the `pgvector` extension available. JMFTS
57
+ does not install or manage it.
58
+
59
+ ```bash
60
+ pip install jmfts # base: no torch, cannot embed by itself
61
+ pip install 'jmfts[embed]' # + the model stack, for a single appliance
62
+ jmfts-init-db # create the database and load the schema
63
+ jmfts-server # serve on 0.0.0.0:8100
64
+ jmfts-server --port 9000 # ...or override one setting for this run
65
+ jmfts-server --help # what the JMFTS_* defaults currently resolve to
66
+ ```
67
+
68
+ For a working PostgreSQL and API in one command, the compose file brings up
69
+ `pgvector/pgvector:pg16` alongside the API:
70
+
71
+ ```bash
72
+ docker compose up
73
+ ```
74
+
75
+ From a checkout, for development:
76
+
77
+ ```bash
78
+ pip install -e ./jmfts-client # the client distribution; jmfts depends on it
79
+ pip install -e ".[dev]" # editable, with pytest/black/ruff; implies [embed]
80
+ uvicorn jmfts_core.rest.main:app --host 0.0.0.0 --port 8100 --reload
81
+ ```
82
+
83
+ This tree builds two distributions, and the first line is not optional. `jmfts` declares
84
+ `jmfts-client==0.2.0`, which is not on PyPI yet, so the second line alone fails to
85
+ resolve it.
86
+
87
+ ### Reading and driving the API
88
+
89
+ `/docs` is Swagger UI over the live route table — 100 operations, grouped by tag, with the
90
+ request and response schemas. `/redoc` is the same document laid out for reading, and
91
+ `/openapi.json` is the document itself.
92
+
93
+ **A generated client already exists — do not write your own against `/openapi.json`.**
94
+ `pip install jmfts-client` gives you every one of those operations as a Python method, with
95
+ the request and response models the appliance itself validates against:
96
+
97
+ ```python
98
+ from jmfts_client import RemoteJmftsClient
99
+ from jmfts_client.contracts import DocumentCreate, HybridSearchRequest
100
+
101
+ with RemoteJmftsClient("http://localhost:8100", token="...") as jmfts:
102
+ jmfts.create_document(DocumentCreate(title="Ada", content="Ada Lovelace"))
103
+ hits = jmfts.hybrid_search(HybridSearchRequest(query="Ada", limit=10))
104
+ ```
105
+
106
+ It carries `httpx` and `pydantic` and nothing else, so calling an appliance does not mean
107
+ installing one. Nobody writes those methods: a service method marked `@expose` becomes a
108
+ REST route, an entry in this OpenAPI document, a method on the in-process
109
+ `LocalJmftsClient`, and a method there — four views of one definition, with a test holding
110
+ each of them to it. See `jmfts-client/README.md`.
111
+
112
+ Press **Authorize** and paste `JMFTS_API_TOKEN` before trying an operation; the page names
113
+ which of the two credentials each one takes, since `/runner/*` uses `JMFTS_RUNNER_KEY`
114
+ instead and `GET /health` needs neither. All three pages answer without a token — a browser
115
+ navigating to a page cannot send an `Authorization` header, so gating them would close the
116
+ page rather than protect it. They expose the interface; reaching anything they describe
117
+ still costs a token.
118
+
119
+ Both pages load Swagger UI and ReDoc from `cdn.jsdelivr.net`, which is FastAPI's default. A
120
+ host with no route to the internet renders a blank page and must read `/openapi.json`
121
+ directly, or be given locally served copies of those assets.
122
+
123
+ ### The office readers are an extra
124
+
125
+ **`pip install jmfts` reads PDF, text, markdown and HTML. `.docx` and `.pptx` need
126
+ `[office]`:**
127
+
128
+ ```bash
129
+ pip install 'jmfts[office]' # python-docx, python-pptx, openpyxl
130
+ ```
131
+
132
+ The split is the same one the model stack draws, for the same reason. A base install
133
+ already *identifies* an office file and reports what it declares — how many slides, which
134
+ sheets, whether it carries macros or an unplaced member — because that runs on `zipfile`
135
+ alone and `probe` must always run. Opening one to get the text out is what needs the
136
+ readers. So a storage-side worker that never ingests office files is correctly installed
137
+ and correctly has no `python-docx`, and asking one to read a `.docx` raises
138
+ `OfficeStackNotInstalled`, which names the extra rather than reading as a broken
139
+ environment.
140
+
141
+ ### The model is an extra
142
+
143
+ **`pip install jmfts` does not install torch.** Base JMFTS is storage, retrieval, the
144
+ tree, BM25, the queue and the whole ingest pipeline; the only step that needs an
145
+ accelerator is producing vectors, and that step can be somebody else's. Measured:
146
+ 584 MB installed, against 5.2 GB with the model stack.
147
+
148
+ Add `[embed]` when *this* process should run the model — because it serves `/search`,
149
+ because it serves `/runner` for others, or because it is a single appliance doing both:
150
+
151
+ ```bash
152
+ pip install 'jmfts[embed]' # CUDA build of torch
153
+ # or, for CPU — the wheel index is an install-time choice, so it is two steps:
154
+ pip install torch --index-url https://download.pytorch.org/whl/cpu
155
+ pip install 'jmfts[embed]'
156
+ ```
157
+
158
+ Without it, an install can still *measure* text — `check_fit`, the chunker, matryoshka
159
+ truncation are all tokenizer and numpy — and asks another JMFTS for the vectors:
160
+
161
+ ```bash
162
+ JMFTS_RUNNER_URL=http://the-gpu-box:8100 JMFTS_RUNNER_KEY=<shared secret> jmfts-worker
163
+ ```
164
+
165
+ That is the intended shape for an ingest worker, and the reason the split exists: a fleet
166
+ is mostly storage-side workers, and none of them need several GB of CUDA to split text and
167
+ write rows. Asking for vectors with no model and no runner raises `ModelStackNotInstalled`,
168
+ which names both ways out — it never silently degrades. See `deploy/README.md`.
169
+
170
+ `./scripts/check_base_install.sh` proves the claim rather than asserting it: it builds an
171
+ empty virtualenv, installs base JMFTS into it, and checks there that the API and the
172
+ worker import, the tokenizer half works, asking for a vector raises, and no office reader
173
+ is reachable.
174
+
175
+ ### Configuration
176
+
177
+ Configuration is environment-variable driven (`JMFTS_` prefix) — see
178
+ `.env.example` for the full list, grouped by concern (DB, embedding model,
179
+ token selection, BM25 tuning, auth/CORS, the worker and the runner).
180
+
181
+ Run the test suite against an isolated, throwaway database (never the real
182
+ appliance DB):
183
+
184
+ ```bash
185
+ ./scripts/run_tests_docker.sh # easiest: throwaway pgvector + CPU embed
186
+ pytest # native: needs `ALTER ROLE jmfts CREATEDB`
187
+ ```
188
+
189
+ ## Layout
190
+
191
+ ```
192
+ jmfts_core/ the library: services, repositories, ORM models, office readers
193
+ jmfts_core/rest/ FastAPI surface — routers generated from the @expose registry
194
+ jmfts_core/sql/ schema.sql and the incremental migrations, shipped in the package
195
+ jmfts-client/ the second distribution: wire contracts + the generated HTTP client
196
+ jmfts_batch/ batch summarization over an OpenAI- or Anthropic-style batch API
197
+ scripts/ CLI clients over the REST API
198
+ deploy/ Kubernetes manifests for a worker fleet, and KEDA scaling on queue depth
199
+ plugin/ Claude Code plugin exposing JMFTS as an agent's durable memory
200
+ tests/ pytest suite against an ephemeral database, plus the fidelity corpus
201
+ ```
202
+
203
+ ## Where to go next
204
+
205
+ - **Running a worker fleet**: `deploy/README.md` — what routes where, badges, the
206
+ thin worker that does not hold the model, and the one way a badged fleet can
207
+ stall.
208
+ - **Contributing / architecture**: `CLAUDE.md` — the architecture diagram,
209
+ data flow, code style, and the search repository's internals (vector, BM25,
210
+ MaxSim, hybrid).
211
+ - **Using JMFTS as an agent's memory**: `plugin/jmfts/` — a Claude Code
212
+ plugin exposing JMFTS as durable cross-session memory via search/ingest/
213
+ read/explore/analyze skills. `plugin/jmfts/skills/jmfts/SKILL.md` is the
214
+ concept overview.
215
+ - **The batch worker**: `jmfts_batch/README.md` — consuming `summarize:llm`
216
+ through an external batch API.
217
+ - **Calling JMFTS from Python**: `jmfts-client/README.md` — the second
218
+ distribution, and what a generated verb is generated from.
219
+
220
+ ## A note on documentation
221
+
222
+ The design documents this code was written against — most importantly the
223
+ ingest specification that dozens of source comments cite by section number —
224
+ are not in this release. They are being refined for publication separately.
225
+ Comments referring to INGEST_SPEC.md, OFFICE_SPEC.md, CORPUS.md,
226
+ KNOWN-DEFECTS.md and ROADMAP.md point at documents that will land later; the
227
+ code stands on its own in the meantime.
228
+
229
+ Those names are deliberately not written as links. There is nothing in this
230
+ tree for them to point at, and marking them up as paths would promise
231
+ otherwise.
232
+
233
+ ## Licence
234
+
235
+ MIT. Copyright (c) 2026 Fight Fire with Fire Robotics, LLC. See `LICENSE`.