nind 2.2.0__tar.gz → 2.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 (176) hide show
  1. nind-2.2.1/.gitignore +9 -0
  2. {nind-2.2.0 → nind-2.2.1}/CLAUDE.md +1 -1
  3. {nind-2.2.0 → nind-2.2.1}/CMakeLists.txt +1 -1
  4. {nind-2.2.0 → nind-2.2.1}/PKG-INFO +1 -1
  5. nind-2.2.1/bench/README.md +189 -0
  6. nind-2.2.1/bench/index_bulk.py +328 -0
  7. nind-2.2.1/bench/lookup_latency_bench.py +142 -0
  8. nind-2.2.1/bench/msmarco/convert_to_nind.py +61 -0
  9. nind-2.2.1/bench/run_bulk_indexing_benchmark.sh +42 -0
  10. nind-2.2.1/bench/run_indexing_benchmark.sh +93 -0
  11. {nind-2.2.0 → nind-2.2.1}/pyproject.toml +2 -2
  12. {nind-2.2.0 → nind-2.2.1}/src/cpp/NindPy/bindings.cpp +183 -44
  13. {nind-2.2.0 → nind-2.2.1}/src/py/nind/NindFile.py +1 -1
  14. {nind-2.2.0 → nind-2.2.1}/src/py/nind/NindIndex.py +1 -1
  15. {nind-2.2.0 → nind-2.2.1}/src/py/nind/NindLexiconindex.py +1 -1
  16. {nind-2.2.0 → nind-2.2.1}/src/py/nind/NindLocalindex.py +1 -1
  17. {nind-2.2.0 → nind-2.2.1}/src/py/nind/NindPadFile.py +1 -1
  18. {nind-2.2.0 → nind-2.2.1}/src/py/nind/NindRetrolexicon.py +1 -1
  19. {nind-2.2.0 → nind-2.2.1}/src/py/nind/NindTermindex.py +1 -1
  20. {nind-2.2.0 → nind-2.2.1}/src/py/nind/Nind_dumpDocument.py +1 -1
  21. {nind-2.2.0 → nind-2.2.1}/src/py/nind/Nind_search.py +1 -1
  22. {nind-2.2.0 → nind-2.2.1}/src/py/nind/Nind_trouveMotsSansOccurrence.py +1 -1
  23. {nind-2.2.0 → nind-2.2.1}/src/py/nind/Nind_trouveNombresPremiers.py +1 -1
  24. {nind-2.2.0 → nind-2.2.1}/src/py/nind/amose/NindIdentifiantsClef.py +1 -1
  25. {nind-2.2.0 → nind-2.2.1}/src/py/nind/nind_engine.py +27 -7
  26. nind-2.2.1/tst/py/test_native.py +273 -0
  27. {nind-2.2.0 → nind-2.2.1}/tst/py/test_nind_engine.py +12 -0
  28. {nind-2.2.0 → nind-2.2.1}/uv.lock +1 -1
  29. nind-2.2.0/.gitignore +0 -6
  30. nind-2.2.0/tst/py/test_native.py +0 -115
  31. {nind-2.2.0 → nind-2.2.1}/.github/workflows/build.yml +0 -0
  32. {nind-2.2.0 → nind-2.2.1}/.github/workflows/docs.yml +0 -0
  33. {nind-2.2.0 → nind-2.2.1}/.github/workflows/publish-pypi.yml +0 -0
  34. {nind-2.2.0 → nind-2.2.1}/AGENTS.md +0 -0
  35. {nind-2.2.0 → nind-2.2.1}/Dockerfile-ubuntu24.04 +0 -0
  36. {nind-2.2.0 → nind-2.2.1}/LICENCE.md +0 -0
  37. {nind-2.2.0 → nind-2.2.1}/NindConfig-version-src.cmake +0 -0
  38. {nind-2.2.0 → nind-2.2.1}/NindConfig.cmake +0 -0
  39. {nind-2.2.0 → nind-2.2.1}/README.md +0 -0
  40. {nind-2.2.0 → nind-2.2.1}/SystemSpecificInformations.cmake +0 -0
  41. {nind-2.2.0 → nind-2.2.1}/cicd/extract_package.sh +0 -0
  42. {nind-2.2.0 → nind-2.2.1}/docs/api/engine.md +0 -0
  43. {nind-2.2.0 → nind-2.2.1}/docs/api/index.md +0 -0
  44. {nind-2.2.0 → nind-2.2.1}/docs/api/readers.md +0 -0
  45. {nind-2.2.0 → nind-2.2.1}/docs/architecture.md +0 -0
  46. {nind-2.2.0 → nind-2.2.1}/docs/cli.md +0 -0
  47. {nind-2.2.0 → nind-2.2.1}/docs/conf.py +0 -0
  48. {nind-2.2.0 → nind-2.2.1}/docs/index.md +0 -0
  49. {nind-2.2.0 → nind-2.2.1}/docs/quickstart.md +0 -0
  50. {nind-2.2.0 → nind-2.2.1}/docs/requirements.txt +0 -0
  51. {nind-2.2.0 → nind-2.2.1}/gbuild.sh +0 -0
  52. {nind-2.2.0 → nind-2.2.1}/src/CMakeLists.txt +0 -0
  53. {nind-2.2.0 → nind-2.2.1}/src/cpp/CMakeLists.txt +0 -0
  54. {nind-2.2.0 → nind-2.2.1}/src/cpp/Doxyfile +0 -0
  55. {nind-2.2.0 → nind-2.2.1}/src/cpp/NindAmose/CMakeLists.txt +0 -0
  56. {nind-2.2.0 → nind-2.2.1}/src/cpp/NindAmose/NindLexiconAmose.cpp +0 -0
  57. {nind-2.2.0 → nind-2.2.1}/src/cpp/NindAmose/NindLexiconAmose.h +0 -0
  58. {nind-2.2.0 → nind-2.2.1}/src/cpp/NindAmose/NindLocalAmose.cpp +0 -0
  59. {nind-2.2.0 → nind-2.2.1}/src/cpp/NindAmose/NindLocalAmose.h +0 -0
  60. {nind-2.2.0 → nind-2.2.1}/src/cpp/NindAmose/NindTermAmose.cpp +0 -0
  61. {nind-2.2.0 → nind-2.2.1}/src/cpp/NindAmose/NindTermAmose.h +0 -0
  62. {nind-2.2.0 → nind-2.2.1}/src/cpp/NindBasics/CMakeLists.txt +0 -0
  63. {nind-2.2.0 → nind-2.2.1}/src/cpp/NindBasics/NindFile.cpp +0 -0
  64. {nind-2.2.0 → nind-2.2.1}/src/cpp/NindBasics/NindFile.h +0 -0
  65. {nind-2.2.0 → nind-2.2.1}/src/cpp/NindBasics/NindPadFile.cpp +0 -0
  66. {nind-2.2.0 → nind-2.2.1}/src/cpp/NindBasics/NindPadFile.h +0 -0
  67. {nind-2.2.0 → nind-2.2.1}/src/cpp/NindBasics/NindSignalCatcher.cpp +0 -0
  68. {nind-2.2.0 → nind-2.2.1}/src/cpp/NindBasics/NindSignalCatcher.h +0 -0
  69. {nind-2.2.0 → nind-2.2.1}/src/cpp/NindCommonExport.h +0 -0
  70. {nind-2.2.0 → nind-2.2.1}/src/cpp/NindExceptions.h +0 -0
  71. {nind-2.2.0 → nind-2.2.1}/src/cpp/NindIndex/CMakeLists.txt +0 -0
  72. {nind-2.2.0 → nind-2.2.1}/src/cpp/NindIndex/NindIndex.cpp +0 -0
  73. {nind-2.2.0 → nind-2.2.1}/src/cpp/NindIndex/NindIndex.h +0 -0
  74. {nind-2.2.0 → nind-2.2.1}/src/cpp/NindIndex/NindLexiconIndex.cpp +0 -0
  75. {nind-2.2.0 → nind-2.2.1}/src/cpp/NindIndex/NindLexiconIndex.h +0 -0
  76. {nind-2.2.0 → nind-2.2.1}/src/cpp/NindIndex/NindLexiconIndexK.cpp +0 -0
  77. {nind-2.2.0 → nind-2.2.1}/src/cpp/NindIndex/NindLexiconIndexK.h +0 -0
  78. {nind-2.2.0 → nind-2.2.1}/src/cpp/NindIndex/NindLocalIndex.cpp +0 -0
  79. {nind-2.2.0 → nind-2.2.1}/src/cpp/NindIndex/NindLocalIndex.h +0 -0
  80. {nind-2.2.0 → nind-2.2.1}/src/cpp/NindIndex/NindTermIndex.cpp +0 -0
  81. {nind-2.2.0 → nind-2.2.1}/src/cpp/NindIndex/NindTermIndex.h +0 -0
  82. {nind-2.2.0 → nind-2.2.1}/src/cpp/NindLexicon/CMakeLists.txt +0 -0
  83. {nind-2.2.0 → nind-2.2.1}/src/cpp/NindLexicon/NindLexicon.cpp +0 -0
  84. {nind-2.2.0 → nind-2.2.1}/src/cpp/NindLexicon/NindLexicon.h +0 -0
  85. {nind-2.2.0 → nind-2.2.1}/src/cpp/NindLexicon/NindLexiconFile.cpp +0 -0
  86. {nind-2.2.0 → nind-2.2.1}/src/cpp/NindLexicon/NindLexiconFile.h +0 -0
  87. {nind-2.2.0 → nind-2.2.1}/src/cpp/NindPy/CMakeLists.txt +0 -0
  88. {nind-2.2.0 → nind-2.2.1}/src/cpp/NindRetrolexicon/CMakeLists.txt +0 -0
  89. {nind-2.2.0 → nind-2.2.1}/src/cpp/NindRetrolexicon/NindRetrolexicon.cpp +0 -0
  90. {nind-2.2.0 → nind-2.2.1}/src/cpp/NindRetrolexicon/NindRetrolexicon.h +0 -0
  91. {nind-2.2.0 → nind-2.2.1}/src/py/CMakeLists.txt +0 -0
  92. {nind-2.2.0 → nind-2.2.1}/src/py/Nind_checkAndRepair.py +0 -0
  93. {nind-2.2.0 → nind-2.2.1}/src/py/Nind_checkDocument.py +0 -0
  94. {nind-2.2.0 → nind-2.2.1}/src/py/Nind_checkRejtroAndLexicon.py +0 -0
  95. {nind-2.2.0 → nind-2.2.1}/src/py/nind/__init__.py +0 -0
  96. {nind-2.2.0 → nind-2.2.1}/src/py/nind/amose/CMakeLists.txt +0 -0
  97. {nind-2.2.0 → nind-2.2.1}/src/py/nind/amose/LatXmlParser.py +0 -0
  98. {nind-2.2.0 → nind-2.2.1}/src/py/nind/amose/Nind_convertitLuceneDump.py +0 -0
  99. {nind-2.2.0 → nind-2.2.1}/src/py/nind/amose/Nind_convertitSampleDump.py +0 -0
  100. {nind-2.2.0 → nind-2.2.1}/src/py/nind/amose/Nind_convertitXmlClef.py +0 -0
  101. {nind-2.2.0 → nind-2.2.1}/tst/CMakeLists.txt +0 -0
  102. {nind-2.2.0 → nind-2.2.1}/tst/cpp/CMakeLists.txt +0 -0
  103. {nind-2.2.0 → nind-2.2.1}/tst/cpp/NindAmose/CMakeLists.txt +0 -0
  104. {nind-2.2.0 → nind-2.2.1}/tst/cpp/NindAmose/NindAmose_effaceDocument.cpp +0 -0
  105. {nind-2.2.0 → nind-2.2.1}/tst/cpp/NindAmose/NindAmose_indexeCorpus.cpp +0 -0
  106. {nind-2.2.0 → nind-2.2.1}/tst/cpp/NindAmose/NindAmose_litTexteAnalysej.cpp +0 -0
  107. {nind-2.2.0 → nind-2.2.1}/tst/cpp/NindAmose/NindAmose_litTexteAnalysej.h +0 -0
  108. {nind-2.2.0 → nind-2.2.1}/tst/cpp/NindAmose/NindAmose_testLecteur.cpp +0 -0
  109. {nind-2.2.0 → nind-2.2.1}/tst/cpp/NindAmose/NindAmose_testSynchro.cpp +0 -0
  110. {nind-2.2.0 → nind-2.2.1}/tst/cpp/NindAmose/Nind_indexeCorpus.cpp +0 -0
  111. {nind-2.2.0 → nind-2.2.1}/tst/cpp/NindBasics/CMakeLists.txt +0 -0
  112. {nind-2.2.0 → nind-2.2.1}/tst/cpp/NindBasics/NindBasics_testLecture.cpp +0 -0
  113. {nind-2.2.0 → nind-2.2.1}/tst/cpp/NindIndex/CMakeLists.txt +0 -0
  114. {nind-2.2.0 → nind-2.2.1}/tst/cpp/NindIndex/NindDate.cpp +0 -0
  115. {nind-2.2.0 → nind-2.2.1}/tst/cpp/NindIndex/NindDate.h +0 -0
  116. {nind-2.2.0 → nind-2.2.1}/tst/cpp/NindIndex/NindFichiers.cpp +0 -0
  117. {nind-2.2.0 → nind-2.2.1}/tst/cpp/NindIndex/NindFichiers.h +0 -0
  118. {nind-2.2.0 → nind-2.2.1}/tst/cpp/NindIndex/NindIndexTest.cpp +0 -0
  119. {nind-2.2.0 → nind-2.2.1}/tst/cpp/NindIndex/NindIndexTest.h +0 -0
  120. {nind-2.2.0 → nind-2.2.1}/tst/cpp/NindIndex/NindIndex_indexe.cpp +0 -0
  121. {nind-2.2.0 → nind-2.2.1}/tst/cpp/NindIndex/NindIndex_indexe.h +0 -0
  122. {nind-2.2.0 → nind-2.2.1}/tst/cpp/NindIndex/NindIndex_litDumpS2.cpp +0 -0
  123. {nind-2.2.0 → nind-2.2.1}/tst/cpp/NindIndex/NindIndex_litDumpS2.h +0 -0
  124. {nind-2.2.0 → nind-2.2.1}/tst/cpp/NindIndex/NindIndex_testEcrivain.cpp +0 -0
  125. {nind-2.2.0 → nind-2.2.1}/tst/cpp/NindIndex/NindIndex_testLecteur.cpp +0 -0
  126. {nind-2.2.0 → nind-2.2.1}/tst/cpp/NindIndex/NindIndex_testSynchro.cpp +0 -0
  127. {nind-2.2.0 → nind-2.2.1}/tst/cpp/NindIndex/NindLexiconIndex_testEcrivain.cpp +0 -0
  128. {nind-2.2.0 → nind-2.2.1}/tst/cpp/NindIndex/NindLexiconIndex_testLecteur.cpp +0 -0
  129. {nind-2.2.0 → nind-2.2.1}/tst/cpp/NindIndex/NindTermIndex_testEcrivain.cpp +0 -0
  130. {nind-2.2.0 → nind-2.2.1}/tst/cpp/NindIndex/NindTermIndex_testLecteur.cpp +0 -0
  131. {nind-2.2.0 → nind-2.2.1}/tst/cpp/NindIndex/Nind_testLateconNumber.cpp +0 -0
  132. {nind-2.2.0 → nind-2.2.1}/tst/cpp/NindIndex/Nind_testNumbersInLocalIndexes.cpp +0 -0
  133. {nind-2.2.0 → nind-2.2.1}/tst/cpp/NindLexicon/CMakeLists.txt +0 -0
  134. {nind-2.2.0 → nind-2.2.1}/tst/cpp/NindLexicon/NindLexicon_testEcrivain.cpp +0 -0
  135. {nind-2.2.0 → nind-2.2.1}/tst/cpp/NindLexicon/NindLexicon_testInter.cpp +0 -0
  136. {nind-2.2.0 → nind-2.2.1}/tst/cpp/NindLexicon/NindLexicon_testLecteur.cpp +0 -0
  137. {nind-2.2.0 → nind-2.2.1}/tst/cpp/NindLexicon/NindLexicon_testLimites.cpp +0 -0
  138. {nind-2.2.0 → nind-2.2.1}/tst/cpp/NindLexicon/NindLexicon_testLimitesB.cpp +0 -0
  139. {nind-2.2.0 → nind-2.2.1}/tst/cpp/NindLexicon/NindLexicon_testLimitesC.cpp +0 -0
  140. {nind-2.2.0 → nind-2.2.1}/tst/cpp/NindLexicon/NindLexicon_testMemoire.cpp +0 -0
  141. {nind-2.2.0 → nind-2.2.1}/tst/cpp/NindSearch/CMakeLists.txt +0 -0
  142. {nind-2.2.0 → nind-2.2.1}/tst/cpp/NindSearch/NindGetDocTerms.cpp +0 -0
  143. {nind-2.2.0 → nind-2.2.1}/tst/cpp/NindSearch/NindSearch.cpp +0 -0
  144. {nind-2.2.0 → nind-2.2.1}/tst/cpp/NindSearch/NindSearchAmose.cpp +0 -0
  145. {nind-2.2.0 → nind-2.2.1}/tst/cpp/NindTests/CMakeLists.txt +0 -0
  146. {nind-2.2.0 → nind-2.2.1}/tst/cpp/NindTests/NindTestControlC.cpp +0 -0
  147. {nind-2.2.0 → nind-2.2.1}/tst/cpp/NindTests/Nind_test1.cpp +0 -0
  148. {nind-2.2.0 → nind-2.2.1}/tst/cpp/NindTests/Nind_testSynchro.cpp +0 -0
  149. {nind-2.2.0 → nind-2.2.1}/tst/cpp/unit/CMakeLists.txt +0 -0
  150. {nind-2.2.0 → nind-2.2.1}/tst/cpp/unit/NindAmose/CMakeLists.txt +0 -0
  151. {nind-2.2.0 → nind-2.2.1}/tst/cpp/unit/NindAmose/NindLexiconAmoseTest.cpp +0 -0
  152. {nind-2.2.0 → nind-2.2.1}/tst/cpp/unit/NindAmose/NindLocalAmoseTest.cpp +0 -0
  153. {nind-2.2.0 → nind-2.2.1}/tst/cpp/unit/NindAmose/NindTermAmoseTest.cpp +0 -0
  154. {nind-2.2.0 → nind-2.2.1}/tst/cpp/unit/NindBasics/CMakeLists.txt +0 -0
  155. {nind-2.2.0 → nind-2.2.1}/tst/cpp/unit/NindBasics/NindBasicsRobustnessTest.cpp +0 -0
  156. {nind-2.2.0 → nind-2.2.1}/tst/cpp/unit/NindBasics/NindFileTest.cpp +0 -0
  157. {nind-2.2.0 → nind-2.2.1}/tst/cpp/unit/NindBasics/NindPadFileTest.cpp +0 -0
  158. {nind-2.2.0 → nind-2.2.1}/tst/cpp/unit/NindIndex/CMakeLists.txt +0 -0
  159. {nind-2.2.0 → nind-2.2.1}/tst/cpp/unit/NindIndex/NindIndexRobustnessTest.cpp +0 -0
  160. {nind-2.2.0 → nind-2.2.1}/tst/cpp/unit/NindIndex/NindLexiconIndexTest.cpp +0 -0
  161. {nind-2.2.0 → nind-2.2.1}/tst/cpp/unit/NindIndex/NindLocalIndexTest.cpp +0 -0
  162. {nind-2.2.0 → nind-2.2.1}/tst/cpp/unit/NindIndex/NindTermIndexTest.cpp +0 -0
  163. {nind-2.2.0 → nind-2.2.1}/tst/cpp/unit/NindLexicon/CMakeLists.txt +0 -0
  164. {nind-2.2.0 → nind-2.2.1}/tst/cpp/unit/NindLexicon/NindLexiconFileTest.cpp +0 -0
  165. {nind-2.2.0 → nind-2.2.1}/tst/cpp/unit/NindLexicon/NindLexiconTest.cpp +0 -0
  166. {nind-2.2.0 → nind-2.2.1}/tst/cpp/unit/NindRetrolexicon/CMakeLists.txt +0 -0
  167. {nind-2.2.0 → nind-2.2.1}/tst/cpp/unit/NindRetrolexicon/NindRetrolexiconTest.cpp +0 -0
  168. {nind-2.2.0 → nind-2.2.1}/tst/cpp/unit/TestFileBytes.h +0 -0
  169. {nind-2.2.0 → nind-2.2.1}/tst/cpp/unit/TestTempDir.h +0 -0
  170. {nind-2.2.0 → nind-2.2.1}/tst/cpp/unit/doctest.h +0 -0
  171. {nind-2.2.0 → nind-2.2.1}/tst/cpp/unit/doctest_main.cpp +0 -0
  172. {nind-2.2.0 → nind-2.2.1}/tst/py/test_nindfile.py +0 -0
  173. {nind-2.2.0 → nind-2.2.1}/tst/py/test_nindlexiconindex.py +0 -0
  174. {nind-2.2.0 → nind-2.2.1}/tst/py/test_nindlocalindex.py +0 -0
  175. {nind-2.2.0 → nind-2.2.1}/tst/py/test_nindretrolexicon.py +0 -0
  176. {nind-2.2.0 → nind-2.2.1}/tst/py/test_nindtermindex.py +0 -0
nind-2.2.1/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ *.kdev4
2
+ build/
3
+ *~
4
+ *.pyc
5
+ Dist*/
6
+ docs/_build/
7
+ bench/results/
8
+ bench/msmarco/*
9
+ !bench/msmarco/*.py
@@ -105,7 +105,7 @@ Parallels the C++ layering with lowercase-suffixed module names for the index cl
105
105
  - `NindFile.py` — low-level binary codec. Historically read-only-oriented (the `litXxx` getters have long existed for every format), but now has a full writer counterpart too: `ejcritNombre1/3/4/5`, `ejcritNombreULat`/`ejcritNombreSLat` (both varint encodings, full tier range matching the readers and the C++ implementation), `ejcritChaine`, `ejcritZejros`.
106
106
  - `NindPadFile.py` — the shared "pad file" envelope (header, indirection block(s), specifics, identification trailer); read-only, no writer (mirrors `NindBasics::NindPadFile`, but nothing in Python subclasses it as a writer the way `NindIndex`/`NindRetrolexicon` do in C++).
107
107
  - `NindRetrolexicon.py`, `NindLexiconindex.py`, `NindTermindex.py`, `NindLocalindex.py` — ergonomic read-only Python wrappers around the corresponding `NindIndex`-family C++ classes; their hot-path lookups delegate to `nind._native`, as does the shared structural-diagnostic base (`analyseFichierPadFile`/`analyseFichierIndex`, inherited from `NindPadFile.py`/`NindIndex.py`). The remaining, per-format diagnostic/analysis methods (`dumpeFichier`, `debogueIndex`, `donneCollisions`, `donneMax`, `donneClef`, `afficheTerme`, `afficheDocument`, and each class's own `analyseFichierXxx` beyond the shared base) remain hand-rolled pure-Python parsing, since `nind._native` doesn't expose that introspection surface yet (see "Planned follow-up work" below). `NindLexiconindex` is hash-bucketed (`clefB(word) % nombreIndirection`), the other two are directly indexed by id.
108
- - `nind_engine.py` — `NindIndexer` (writer) and `NindEngine` (BM25-style search) built on top of the above. `NindIndexer` is the *only* Python writer for the index-family formats (`.nindlexiconindex`/`.nindtermindex`/`.nindlocalindex`) — it builds them via `nind._native`'s `is_writer=True` constructors. It does not write `.nindretrolexicon` (not needed by `NindEngine`, which only supports simple, non-compound words). Indexing is parallel but every file keeps a single writer: files are tokenized in worker processes (`workers` argument of `index_files`; in-process below `PARALLEL_MIN_FILES` files), then the term and local index files are written concurrently from two threads through the bulk bindings `NindTermIndex.set_postings`/`NindLocalIndex.set_local_def_from_term_ids`, which take plain ints (building `Document`/`Term`/`Localisation` objects in Python cost ~20× the write itself) and release the GIL. `set_local_def_from_term_ids` splits a term's positions into entries of ≤255 (`<nbreLocalisations>` is one byte).
108
+ - `nind_engine.py` — `NindIndexer` (writer) and `NindEngine` (BM25-style search) built on top of the above. `NindIndexer` is the *only* Python writer for the index-family formats (`.nindlexiconindex`/`.nindtermindex`/`.nindlocalindex`) — it builds them via `nind._native`'s `is_writer=True` constructors. It does not write `.nindretrolexicon` (not needed by `NindEngine`, which only supports simple, non-compound words). Indexing is parallel but every file keeps a single writer: files are tokenized in worker processes (`workers` argument of `index_files`; in-process below `PARALLEL_MIN_FILES` files), then the term and local index files are written concurrently from two threads through the flat-buffer bulk bindings `NindTermIndex.set_term_defs_arrays`/`NindLocalIndex.set_local_defs_arrays`, which take `array('I')` buffers (building `Document`/`Term`/`Localisation` objects in Python cost ~20× the write itself) and release the GIL. The `*_local_def*_arrays` writers split a term's positions into entries of ≤255 (`<nbreLocalisations>` is one byte).
109
109
  - `Nind_*.py` at the package root — standalone CLI scripts (dump a document, convert corpora, search, diagnostics)
110
110
  - `amose/` — Amose-specific conversion/parsing scripts (Lucene dump, XML-CLEF, sample corpora)
111
111
 
@@ -113,7 +113,7 @@ endif ()
113
113
 
114
114
  ########### setting project verion number ###############
115
115
 
116
- set(NIND_VERSION "2.2.0")
116
+ set(NIND_VERSION "2.2.1")
117
117
 
118
118
  string(REGEX REPLACE "^([0-9]+)\\.[0-9]+\\.[0-9]+$" "\\1" NIND_VERSION_MAJOR "${NIND_VERSION}")
119
119
  string(REGEX REPLACE "^[0-9]+\\.([0-9]+)\\.[0-9]+$" "\\1" NIND_VERSION_MINOR "${NIND_VERSION}")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: nind
3
- Version: 2.2.0
3
+ Version: 2.2.1
4
4
  Summary: Python frontend for the Nind Indexing Module, with its native C++ index stack built in
5
5
  Requires-Python: >=3.8
6
6
  Description-Content-Type: text/markdown
@@ -0,0 +1,189 @@
1
+ # nind performance benchmarks
2
+
3
+ nind is a flat-file indexer, not a full search engine, so it's benchmarked
4
+ here like a storage/indexing engine rather than with an anserini/pyserini-style
5
+ retrieval-effectiveness harness (nDCG/MAP against relevance judgments) — see
6
+ the design discussion this came out of for why. Three things are measured:
7
+
8
+ 1. **Indexing throughput & on-disk index size**, across a corpus-size sweep —
9
+ using nind's efficient *bulk-build* write pattern.
10
+ 2. **The cost of incremental, per-document indexing** into a growing index —
11
+ a different, also-real usage pattern, with a real and important caveat
12
+ (see below).
13
+ 3. **Term/document lookup latency**, through the same `nind._native`-backed
14
+ Python reader classes real callers use.
15
+
16
+ Retrieval effectiveness (nDCG/MAP vs. qrels) is out of scope: `NindEngine`
17
+ would first need stable external document ids and real inverted-list-driven
18
+ candidate generation instead of its current O(N·|Q|) full scan.
19
+
20
+ ## Corpus: MS MARCO Passage Ranking
21
+
22
+ Freely downloadable, no license/registration required, and its passage ids
23
+ are already a dense integer sequence from 0 — a good match for nind's
24
+ document-identifier model. Get `collection.tsv` (one passage per line,
25
+ `pid<TAB>text`, ~8.84M rows) from the official MS MARCO Passage Ranking
26
+ release — see <https://github.com/microsoft/MSMARCO-Passage-Ranking> for the
27
+ current download instructions/links (verify the link is still current;
28
+ dataset hosting has moved before).
29
+
30
+ ## Prerequisites
31
+
32
+ ```bash
33
+ # Python side: builds nind._native and installs the reader/writer classes.
34
+ # This alone is enough for the bulk-build benchmark (1) and lookup latency (3).
35
+ uv sync
36
+
37
+ # C++ side, only needed for the incremental-indexing benchmark (2):
38
+ NIND_DIST=/path/to/dist ./gbuild.sh -m Release # Release, not Debug - Debug timings are misleading
39
+ # if cmake/ninja aren't installed system-wide, `uv tool install cmake ninja` works too
40
+ ```
41
+
42
+ ## 1. Indexing throughput & size (bulk build)
43
+
44
+ ```bash
45
+ ./bench/run_bulk_indexing_benchmark.sh /path/to/collection.tsv 10000,100000,1000000
46
+ ```
47
+
48
+ For each size in the comma-separated list, this converts the first N
49
+ passages to nind's corpus text format (`bench/msmarco/convert_to_nind.py`,
50
+ offsetting `pid` by +1 since nind treats document id 0 as a sentinel
51
+ elsewhere), then runs `bench/index_bulk.py`, which writes each term's
52
+ definition and each document's local definition to disk **exactly once**,
53
+ via `nind._native`'s `NindLexiconIndex`/`NindTermIndex`/`NindLocalIndex`
54
+ writers (the same write-once pattern `nind.nind_engine.NindIndexer` uses),
55
+ reading from the compact bulk corpus file instead of one file per document.
56
+
57
+ The corpus is split into line-aligned byte ranges handled by a pool of worker
58
+ processes (`--workers`, default: all cores); the main process only drives
59
+ the nind writers, which are single-file and single-threaded:
60
+
61
+ 1. *pass 1* (workers): vocabulary and highest document id per chunk; the
62
+ main process then writes the lexicon, terms added in sorted order;
63
+ 2. *pass 2* (workers): parsing and term→id mapping. Each chunk's documents
64
+ come back as flat `array('I')` buffers, and the main process writes them
65
+ in document order with one `set_local_defs_arrays` call per chunk. Each
66
+ worker also spills its chunk's postings to a temporary file next to the
67
+ index, sorted by term id and split into term-id ranges (SPIMI-style), so
68
+ no process holds the whole inverted index;
69
+ 3. *pass 3* (workers): one term-id range each, merging that range's slice
70
+ of every spill file; the main process writes the terms in id order with
71
+ one `set_term_defs_arrays` call per range.
72
+
73
+ The output is byte-identical to a serial accumulate-in-memory build, apart
74
+ from the lexicon creation timestamp in each file's identification trailer.
75
+ Wall-clock time (`time.perf_counter`), peak RSS of the main process and of
76
+ the largest worker (`resource.getrusage`), and the index file sizes go into
77
+ one row of `bench/results/indexing_bulk.csv`; per-phase timings are printed
78
+ to stderr.
79
+
80
+ Measured on the actual MS MARCO data (48-core machine; the `workers` column
81
+ of the CSV records the pool size of each run):
82
+
83
+ | docs | workers | wall | docs/s | main peak RSS | largest worker RSS | index size |
84
+ |---:|---:|---:|---:|---:|---:|---:|
85
+ | 10K | 24 | 1.0 s | 10,400 | 33 MB | 23 MB | 5.5 MB |
86
+ | 100K | 24 | 5.0 s | 20,100 | 87 MB | 42 MB | 46 MB |
87
+ | 1M | 24 | 42 s | 23,800 | 675 MB | 174 MB | 455 MB |
88
+ | 8.84M (full) | 48 | 255 s | 34,700 | 3.1 GB | 573 MB | 4.1 GB |
89
+
90
+ For comparison, the earlier serial version of this script (whole corpus
91
+ accumulated in RAM as Python objects) did ~2,700-3,000 docs/s and needed
92
+ 7.9 GB at 1M docs; its rows are kept in
93
+ `bench/results/indexing_bulk_serial_v1.csv`.
94
+
95
+ The main process is the bottleneck: at 1M docs, 16 and 48 workers take the
96
+ same time. Its time goes almost entirely into the C++ writers, which make
97
+ every entry durable on its own (~13 system calls per definition written:
98
+ seeks, reads of the indirection entry, `fwrite`+`fflush`, and signal-handler
99
+ swaps around each Ctrl-C-safe critical section). A faster bulk load would
100
+ need a C++-side bulk mode (e.g. buffered indirection and sequential writes).
101
+ The main process's memory is mostly worker results waiting to be written;
102
+ at most `2 × workers` chunks (~8 MB of corpus each) are in flight.
103
+
104
+ Don't run two builds of the same index at once: they'd delete and
105
+ overwrite each other's files, so `index_bulk.py` takes a lock on
106
+ `<index-base>.lock` and refuses to start if it's held.
107
+
108
+ Start with a small size (e.g. 10000) as a smoke test before scaling up.
109
+
110
+ ## 2. Incremental per-document indexing cost
111
+
112
+ ```bash
113
+ NIND_DIST=/path/to/dist ./bench/run_indexing_benchmark.sh \
114
+ /path/to/collection.tsv 10000,100000
115
+ ```
116
+
117
+ This drives `Nind_indexeCorpus` instead, which indexes one document at a
118
+ time against the on-disk files as it reads them — a different, also-real
119
+ usage pattern (e.g. appending new documents to an already-large index over
120
+ time, rather than building it once from scratch). **Its scaling is much
121
+ worse: on the actual MS MARCO data, going from 10K to 100K documents made it
122
+ ~89x slower, not ~10x.**
123
+
124
+ The reason is structural, not a bug in this particular tool:
125
+ `NindTermIndex::setTermDef` (`src/cpp/NindIndex/NindTermIndex.h:80-88`) only
126
+ supports writing a term's *entire* postings list, no incremental append.
127
+ `Nind_indexeCorpus` calls `getTermDef`+`setTermDef` for every term in every
128
+ document (`tst/cpp/NindAmose/Nind_indexeCorpus.cpp:142-150`), so a term
129
+ appearing in *df* documents has its (growing) postings list read and
130
+ rewritten *df* times — total cost is dominated by common terms and scales
131
+ roughly O(N²) in corpus size. **Don't run this one past ~100K documents** —
132
+ it won't finish in reasonable time and the quadratic trend is already clear
133
+ by then. It exists to document a real limitation (streaming/incremental
134
+ indexing into a large existing base gets expensive), not as nind's headline
135
+ throughput number — use benchmark 1 for that.
136
+
137
+ Same output shape as before: one row per size in
138
+ `bench/results/indexing.csv`.
139
+
140
+ ## 3. Lookup latency
141
+
142
+ Run against an index built by either benchmark above (prefer benchmark 1's
143
+ `docs_bulk` index — it's the one built the way nind is actually meant to be
144
+ built at scale):
145
+
146
+ ```bash
147
+ uv run python3 bench/lookup_latency_bench.py \
148
+ bench/results/msmarco_10000/docs.txt \
149
+ bench/results/msmarco_10000/docs_bulk \
150
+ 10000
151
+ ```
152
+
153
+ Samples random terms and document ids from the corpus text file, times
154
+ repeated calls to `NindLexiconindex.donneIdentifiant` (term→id),
155
+ `NindTermindex.donneListeTermesCG` (id→postings), and
156
+ `NindLocalindex.donneListeTermes` (doc→local terms), and appends
157
+ mean/median/p95/p99 latency and throughput to `bench/results/lookup.csv`.
158
+ Run it once per size point to see how lookup latency scales with index size.
159
+
160
+ By default each operation's sample set is run once untimed first
161
+ (`--warmup 1`), so the numbers are steady-state lookup cost. With
162
+ `--warmup 0` you mostly measure first-touch page-cache misses on a freshly
163
+ opened file instead: on MS MARCO that made term→id look like it grew from
164
+ ~3µs (10K docs) to ~175µs median (1M docs). Don't run it concurrently with an
165
+ indexing benchmark: I/O contention skews the cold numbers further.
166
+
167
+ Warm, on the bulk-built MS MARCO indexes (median / p99, µs):
168
+
169
+ | docs | term→id | id→postings | doc→local terms |
170
+ |---:|---:|---:|---:|
171
+ | 10K | 2.2 / 3.9 | 2.7 / 9.6 | 15.4 / 28.7 |
172
+ | 100K | 2.7 / 5.4 | 3.2 / 22 | 16.7 / 31.3 |
173
+ | 1M | 3.0 / 5.6 | 3.8 / 107 | 19.5 / 34.0 |
174
+ | 8.84M | 3.1 / 5.1 | 3.7 / 284 | 19.9 / 36.1 |
175
+
176
+ Lookups stay essentially flat from 10K to 8.84M documents: the lexicon's
177
+ hash-bucket count is sized to the vocabulary, so buckets don't lengthen, and
178
+ the term and local indexes are addressed directly by id. The id→postings tail
179
+ is the few very frequent terms in the sample, whose postings lists are long.
180
+
181
+ ## Notes
182
+
183
+ - `gbuild.sh` installs binaries with an empty RPATH (see the top-level
184
+ `CMakeLists.txt`), so `run_indexing_benchmark.sh` adds `<bin-dir>/../lib`
185
+ to `LD_LIBRARY_PATH` itself — no manual setup needed.
186
+ - `bench/results/` is git-ignored; it's regenerated by these scripts. Both
187
+ indexing benchmarks reuse the same `bench/results/msmarco_<size>/docs.txt`
188
+ when present, so running benchmark 2 after benchmark 1 (or vice versa)
189
+ doesn't reconvert the corpus.
@@ -0,0 +1,328 @@
1
+ #!/usr/bin/env python3
2
+ """Bulk-build indexing throughput benchmark, using nind's efficient write
3
+ pattern: each term's definition and each document's local definition is
4
+ written to disk exactly once (as nind.nind_engine.NindIndexer does) - as
5
+ opposed to bench/run_indexing_benchmark.sh, which drives Nind_indexeCorpus
6
+ and does a read-modify-write of each term's growing postings list on every
7
+ document (see bench/README.md for why that shows ~quadratic scaling).
8
+
9
+ The corpus is split into line-aligned byte ranges processed by a pool of
10
+ worker processes; the main process only drives the (single-file,
11
+ single-threaded) nind writers, handing them a whole chunk's documents or a
12
+ whole term-id range's terms per call, as flat buffers (nind._native's
13
+ set_local_defs_arrays/set_term_defs_arrays, which write with the GIL released,
14
+ so the main process keeps receiving worker results meanwhile):
15
+
16
+ - pass 1 (workers): per-chunk vocabulary and highest document id;
17
+ - main: builds the lexicon, terms added in sorted order;
18
+ - pass 2 (workers): parse and map terms to ids; each document's occurrences
19
+ come back as flat arrays and the main process writes its local definition
20
+ right away, in document order. Each worker also spills its chunk's
21
+ postings to a temporary file, sorted by term id and split into term-id
22
+ ranges (SPIMI-style), so no process ever holds the whole inverted index;
23
+ - pass 3 (workers): one term-id range each, merging that range's slice of
24
+ every chunk file into per-term postings; the main process writes them in
25
+ term id order.
26
+
27
+ The output is byte-identical to a serial accumulate-then-write build (apart
28
+ from the lexicon creation timestamp in each file's identification trailer).
29
+
30
+ Reads the same "one line per document" corpus format produced by
31
+ msmarco/convert_to_nind.py (so it works on the compact bulk file, not
32
+ one-file-per-document like NindIndexer.index_files expects).
33
+
34
+ usage: index_bulk.py <docs.txt> <index-base> <size-label> [--workers N] [--csv PATH]
35
+ <index-base> is the .nind* file path prefix to write, e.g. .../docs
36
+ """
37
+ import argparse
38
+ from array import array
39
+ import bisect
40
+ import fcntl
41
+ import gc
42
+ import multiprocessing
43
+ import os
44
+ import resource
45
+ import sys
46
+ import tempfile
47
+ import time
48
+ from collections import defaultdict, deque
49
+ from itertools import islice
50
+ from pathlib import Path
51
+
52
+ sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src" / "py" / "nind"))
53
+ from nind import _native as native
54
+
55
+ # state inherited by forked workers (set in the main process before the pool
56
+ # that needs it is created, so it's shared copy-on-write, not pickled)
57
+ _term_to_id = None
58
+ _spill_dir = None
59
+ _range_bounds = None # term id lower bounds of each pass-3 range, ascending
60
+
61
+ CHUNK_BYTES = 8 * 1024 * 1024 # target corpus bytes per chunk
62
+
63
+
64
+ def bounded_imap(pool, fn, tasks, window):
65
+ """Like pool.imap (results in task order), but with at most `window`
66
+ tasks in flight: the main process, which consumes the results, is the
67
+ bottleneck, and pool.imap would otherwise queue every finished result
68
+ in its memory."""
69
+ tasks = iter(tasks)
70
+ pending = deque(pool.apply_async(fn, (task,)) for task in islice(tasks, window))
71
+ while pending:
72
+ result = pending.popleft().get()
73
+ for task in islice(tasks, 1):
74
+ pending.append(pool.apply_async(fn, (task,)))
75
+ yield result
76
+
77
+
78
+ def parse_lines(text):
79
+ """Yields (doc_id, [(term, position, length), ...]) per non-empty line."""
80
+ for line in text.splitlines():
81
+ fields = line.split()
82
+ if not fields:
83
+ continue
84
+ occurrences = []
85
+ for term, loc in zip(fields[1::2], fields[2::2]):
86
+ pos_str, len_str = loc.split(",")
87
+ occurrences.append((term, int(pos_str), int(len_str)))
88
+ yield int(fields[0]), occurrences
89
+
90
+
91
+ def read_chunk(docs_path, start, end):
92
+ with open(docs_path, "rb") as f:
93
+ f.seek(start)
94
+ return f.read(end - start).decode("utf-8")
95
+
96
+
97
+ def split_chunks(docs_path, chunks_nb):
98
+ """Line-aligned byte ranges covering the whole file, in file order."""
99
+ size = os.path.getsize(docs_path)
100
+ bounds = [0]
101
+ with open(docs_path, "rb") as f:
102
+ for i in range(1, chunks_nb):
103
+ f.seek(max(bounds[-1], size * i // chunks_nb))
104
+ f.readline()
105
+ pos = f.tell()
106
+ if pos >= size:
107
+ break
108
+ if pos > bounds[-1]:
109
+ bounds.append(pos)
110
+ bounds.append(size)
111
+ return list(zip(bounds[:-1], bounds[1:]))
112
+
113
+
114
+ def pass1_worker(task):
115
+ docs_path, start, end = task
116
+ vocab = set()
117
+ max_doc_id = 0
118
+ for doc_id, occurrences in parse_lines(read_chunk(docs_path, start, end)):
119
+ max_doc_id = max(max_doc_id, doc_id)
120
+ vocab.update(term for term, _pos, _len in occurrences)
121
+ return vocab, max_doc_id
122
+
123
+
124
+ def pass2_worker(task):
125
+ """Returns the chunk's documents as flat arrays for set_local_def_arrays
126
+ and spills its postings, as (term_id, doc_id, freq) triples sorted by
127
+ term id, to <spill_dir>/<chunk_no>; triple offsets of each pass-3 range
128
+ are returned so pass 3 can read just its slice."""
129
+ chunk_no, docs_path, start, end = task
130
+ doc_ids = array("I")
131
+ doc_ends = array("I") # end offset of each doc in the three arrays below
132
+ term_ids, positions, lengths = array("I"), array("I"), array("I")
133
+ postings = [] # (term_id, doc_id, freq)
134
+ tokens = 0
135
+ for doc_id, occurrences in parse_lines(read_chunk(docs_path, start, end)):
136
+ counts = defaultdict(int)
137
+ for term, pos, length in occurrences:
138
+ tid = _term_to_id[term]
139
+ counts[tid] += 1
140
+ term_ids.append(tid)
141
+ positions.append(pos)
142
+ lengths.append(length)
143
+ tokens += len(occurrences)
144
+ doc_ids.append(doc_id)
145
+ doc_ends.append(len(term_ids))
146
+ postings.extend((tid, doc_id, freq) for tid, freq in counts.items())
147
+ # documents are in ascending id order within the file, so a stable sort
148
+ # on term id keeps each term's documents ascending
149
+ postings.sort(key=lambda p: p[0])
150
+ triples = array("I")
151
+ for p in postings:
152
+ triples.extend(p)
153
+ with open(os.path.join(_spill_dir, str(chunk_no)), "wb") as f:
154
+ triples.tofile(f)
155
+ sorted_tids = [p[0] for p in postings]
156
+ range_offsets = [bisect.bisect_left(sorted_tids, bound) for bound in _range_bounds]
157
+ range_offsets.append(len(postings))
158
+ return doc_ids, doc_ends, term_ids, positions, lengths, tokens, range_offsets
159
+
160
+
161
+ def pass3_worker(task):
162
+ """Merges one term-id range across all chunk spill files, in chunk
163
+ (= document) order; returns the range's terms (ascending) as flat arrays
164
+ for set_term_defs_arrays: term ids, end offset of each term's postings,
165
+ and the concatenated postings' doc ids and frequencies."""
166
+ range_no, chunk_slices = task
167
+ by_term = {}
168
+ for chunk_no, first, last in chunk_slices:
169
+ if last == first:
170
+ continue
171
+ triples = array("I")
172
+ with open(os.path.join(_spill_dir, str(chunk_no)), "rb") as f:
173
+ f.seek(first * 3 * triples.itemsize)
174
+ triples.fromfile(f, (last - first) * 3)
175
+ for i in range(0, len(triples), 3):
176
+ tid = triples[i]
177
+ entry = by_term.get(tid)
178
+ if entry is None:
179
+ entry = by_term[tid] = (array("I"), array("I"))
180
+ entry[0].append(triples[i + 1])
181
+ entry[1].append(triples[i + 2])
182
+ term_ids, term_ends, doc_ids, freqs = array("I"), array("I"), array("I"), array("I")
183
+ for tid in sorted(by_term):
184
+ docs, term_freqs = by_term.pop(tid)
185
+ term_ids.append(tid)
186
+ doc_ids.extend(docs)
187
+ freqs.extend(term_freqs)
188
+ term_ends.append(len(doc_ids))
189
+ return term_ids, term_ends, doc_ids, freqs
190
+
191
+
192
+ def build_index(docs_path, index_base, workers):
193
+ global _term_to_id, _spill_dir, _range_bounds
194
+ ctx = multiprocessing.get_context("fork")
195
+ phase_start = time.perf_counter()
196
+
197
+ def phase_done(name):
198
+ nonlocal phase_start
199
+ now = time.perf_counter()
200
+ print(f" {name}: {now - phase_start:.2f}s", file=sys.stderr)
201
+ phase_start = now
202
+ chunks = split_chunks(docs_path, max(workers * 4, os.path.getsize(docs_path) // CHUNK_BYTES))
203
+ window = workers * 2
204
+
205
+ # pass 1: vocabulary (NindLexiconIndex needs the entry count up front) and
206
+ # the highest document id (so NindLocalIndex can be opened before pass 2)
207
+ vocab = set()
208
+ max_doc_id = 0
209
+ with ctx.Pool(workers) as pool:
210
+ for chunk_vocab, chunk_max in pool.imap_unordered(
211
+ pass1_worker, [(docs_path, s, e) for s, e in chunks]):
212
+ vocab |= chunk_vocab
213
+ max_doc_id = max(max_doc_id, chunk_max)
214
+ terms_sorted = sorted(vocab)
215
+ del vocab
216
+ phase_done("pass 1 (vocabulary)")
217
+
218
+ lexicon_writer = native.NindLexiconIndex(index_base, is_writer=True,
219
+ indirection_bloc_size=max(1, len(terms_sorted)))
220
+ _term_to_id = {term: lexicon_writer.add_word([term]) for term in terms_sorted}
221
+ del terms_sorted
222
+ vocab_size = len(_term_to_id)
223
+ lexicon_identification = lexicon_writer.get_identification()
224
+ del lexicon_writer
225
+ phase_done("lexicon write")
226
+
227
+ max_term_id = max(_term_to_id.values(), default=0)
228
+ ranges_nb = workers * 4
229
+ _range_bounds = [1 + (max_term_id * r) // ranges_nb for r in range(ranges_nb)]
230
+
231
+ docs = 0
232
+ tokens_total = 0
233
+ local_writer = native.NindLocalIndex(index_base, is_writer=True,
234
+ lexicon_identification=lexicon_identification,
235
+ indirection_bloc_size=max_doc_id + 1)
236
+ term_writer = native.NindTermIndex(index_base, is_writer=True,
237
+ lexicon_identification=lexicon_identification,
238
+ specifics_number=0,
239
+ indirection_bloc_size=max_term_id + 1)
240
+ spill_parent = os.path.dirname(os.path.abspath(index_base))
241
+ with tempfile.TemporaryDirectory(prefix="nind_spill_", dir=spill_parent) as spill_dir:
242
+ _spill_dir = spill_dir
243
+ # keep the inherited lexicon out of the (forked) workers' GC scans, so
244
+ # it isn't copied page by page into each of them
245
+ gc.freeze()
246
+ with ctx.Pool(workers) as pool:
247
+ # pass 2: chunks come back in file order, so local definitions are
248
+ # written in document order
249
+ all_range_offsets = []
250
+ for doc_ids, doc_ends, term_ids, positions, lengths, tokens, range_offsets in bounded_imap(
251
+ pool, pass2_worker, [(i, docs_path, s, e) for i, (s, e) in enumerate(chunks)], window):
252
+ local_writer.set_local_defs_arrays(doc_ids, doc_ends, term_ids, positions, lengths,
253
+ lexicon_identification)
254
+ docs += len(doc_ids)
255
+ tokens_total += tokens
256
+ all_range_offsets.append(range_offsets)
257
+ del local_writer
258
+ phase_done("pass 2 (local index write + postings spill)")
259
+
260
+ # pass 3: ranges come back in term id order
261
+ tasks = [(r, [(c, offsets[r], offsets[r + 1]) for c, offsets in enumerate(all_range_offsets)])
262
+ for r in range(ranges_nb)]
263
+ for term_ids, term_ends, doc_ids, freqs in bounded_imap(pool, pass3_worker, tasks, window):
264
+ term_writer.set_term_defs_arrays(term_ids, term_ends, doc_ids, freqs, lexicon_identification)
265
+ del term_writer
266
+ phase_done("pass 3 (postings merge + term index write)")
267
+ gc.unfreeze()
268
+
269
+ return docs, tokens_total, vocab_size
270
+
271
+
272
+ def main():
273
+ parser = argparse.ArgumentParser(description=__doc__)
274
+ parser.add_argument("docs_file")
275
+ parser.add_argument("index_base")
276
+ parser.add_argument("size_label")
277
+ parser.add_argument("--workers", type=int, default=os.cpu_count() or 1)
278
+ parser.add_argument("--csv", default=None)
279
+ args = parser.parse_args()
280
+
281
+ csv_path = Path(args.csv) if args.csv else Path(__file__).resolve().parent / "results" / "indexing_bulk.csv"
282
+ csv_path.parent.mkdir(parents=True, exist_ok=True)
283
+ new_file = not csv_path.exists()
284
+
285
+ # two runs writing the same index would delete/overwrite each other's
286
+ # files and fail (or worse) in the middle; refuse to start instead
287
+ lock_file = open(args.index_base + ".lock", "w")
288
+ try:
289
+ fcntl.flock(lock_file, fcntl.LOCK_EX | fcntl.LOCK_NB)
290
+ except BlockingIOError:
291
+ sys.exit(f"index_bulk.py: another run is already writing {args.index_base}.* (lock held on "
292
+ f"{args.index_base}.lock)")
293
+
294
+ docs_file_size = Path(args.docs_file).stat().st_size
295
+ start = time.perf_counter()
296
+ docs, tokens_total, vocab = build_index(args.docs_file, args.index_base, max(1, args.workers))
297
+ wall_seconds = time.perf_counter() - start
298
+ # process-lifetime peak RSS of the main process, and of the largest
299
+ # worker; accurate for this benchmark since it's run as a fresh
300
+ # subprocess per corpus size, doing nothing else beforehand
301
+ peak_rss_kb = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
302
+ peak_worker_rss_kb = resource.getrusage(resource.RUSAGE_CHILDREN).ru_maxrss
303
+
304
+ index_bytes = 0
305
+ for ext in (".nindlexiconindex", ".nindtermindex", ".nindlocalindex"):
306
+ p = Path(args.index_base + ext)
307
+ if p.exists():
308
+ index_bytes += p.stat().st_size
309
+
310
+ docs_per_sec = docs / wall_seconds if wall_seconds > 0 else docs
311
+ mb_per_sec = (docs_file_size / 1048576) / wall_seconds if wall_seconds > 0 else docs_file_size / 1048576
312
+
313
+ with open(csv_path, "a", encoding="utf-8") as f:
314
+ if new_file:
315
+ f.write("size,workers,docs,tokens,vocab,input_bytes,wall_seconds,docs_per_sec,mb_per_sec,"
316
+ "peak_rss_kb,peak_worker_rss_kb,index_bytes\n")
317
+ f.write(f"{args.size_label},{args.workers},{docs},{tokens_total},{vocab},{docs_file_size},"
318
+ f"{wall_seconds:.3f},{docs_per_sec:.3f},{mb_per_sec:.5f},"
319
+ f"{peak_rss_kb},{peak_worker_rss_kb},{index_bytes}\n")
320
+
321
+ print(f"docs={docs} workers={args.workers} wall={wall_seconds:.2f}s docs/sec={docs_per_sec:.1f} "
322
+ f"MB/sec={mb_per_sec:.3f} peak_rss={peak_rss_kb}KB peak_worker_rss={peak_worker_rss_kb}KB "
323
+ f"index_bytes={index_bytes}", file=sys.stderr)
324
+ print(f"Results appended to {csv_path}", file=sys.stderr)
325
+
326
+
327
+ if __name__ == "__main__":
328
+ main()