soupdb-vector 0.1.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 (25) hide show
  1. soupdb_vector-0.1.0/.gitignore +270 -0
  2. soupdb_vector-0.1.0/PKG-INFO +13 -0
  3. soupdb_vector-0.1.0/pyproject.toml +26 -0
  4. soupdb_vector-0.1.0/soupdb_vector/__init__.py +3 -0
  5. soupdb_vector-0.1.0/soupdb_vector/indexes/__init__.py +18 -0
  6. soupdb_vector-0.1.0/soupdb_vector/indexes/_record_file.py +154 -0
  7. soupdb_vector-0.1.0/soupdb_vector/indexes/brute_force.py +68 -0
  8. soupdb_vector-0.1.0/soupdb_vector/indexes/constants.py +34 -0
  9. soupdb_vector-0.1.0/soupdb_vector/indexes/hnsw.py +372 -0
  10. soupdb_vector-0.1.0/soupdb_vector/indexes/ivf.py +378 -0
  11. soupdb_vector-0.1.0/soupdb_vector/indexes/kmeans.py +105 -0
  12. soupdb_vector-0.1.0/soupdb_vector/indexes/metrics.py +76 -0
  13. soupdb_vector-0.1.0/soupdb_vector/indexes/sql_factory.py +235 -0
  14. soupdb_vector-0.1.0/soupdb_vector/indexes/store.py +144 -0
  15. soupdb_vector-0.1.0/soupdb_vector/multimedia/__init__.py +1 -0
  16. soupdb_vector-0.1.0/soupdb_vector/multimedia/audio_features.py +132 -0
  17. soupdb_vector-0.1.0/soupdb_vector/multimedia/build_vectors.py +190 -0
  18. soupdb_vector-0.1.0/soupdb_vector/multimedia/codebook.py +94 -0
  19. soupdb_vector-0.1.0/soupdb_vector/multimedia/descriptor_cache.py +52 -0
  20. soupdb_vector-0.1.0/soupdb_vector/multimedia/image_features.py +77 -0
  21. soupdb_vector-0.1.0/soupdb_vector/multimedia/manifest.py +111 -0
  22. soupdb_vector-0.1.0/soupdb_vector/multimedia/sql_profiles.py +145 -0
  23. soupdb_vector-0.1.0/soupdb_vector/multimedia/vector_file.py +88 -0
  24. soupdb_vector-0.1.0/soupdb_vector/mutable.py +99 -0
  25. soupdb_vector-0.1.0/soupdb_vector/plugin.py +145 -0
@@ -0,0 +1,270 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[codz]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py.cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+ docs/PROCESAMIENTO-CONSULTAS.md
74
+
75
+ # PyBuilder
76
+ .pybuilder/
77
+ target/
78
+
79
+ # Jupyter Notebook
80
+ .ipynb_checkpoints
81
+
82
+ # IPython
83
+ profile_default/
84
+ ipython_config.py
85
+
86
+ # pyenv
87
+ # For a library or package, you might want to ignore these files since the code is
88
+ # intended to run in multiple environments; otherwise, check them in:
89
+ # .python-version
90
+
91
+ # pipenv
92
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
93
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
94
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
95
+ # install all needed dependencies.
96
+ # Pipfile.lock
97
+
98
+ # UV
99
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
100
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
101
+ # commonly ignored for libraries.
102
+ # uv.lock
103
+
104
+ # poetry
105
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
106
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
107
+ # commonly ignored for libraries.
108
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
109
+ # poetry.lock
110
+ # poetry.toml
111
+
112
+ # pdm
113
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
114
+ # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
115
+ # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
116
+ # pdm.lock
117
+ # pdm.toml
118
+ .pdm-python
119
+ .pdm-build/
120
+
121
+ # pixi
122
+ # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
123
+ # pixi.lock
124
+ # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
125
+ # in the .venv directory. It is recommended not to include this directory in version control.
126
+ .pixi
127
+
128
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
129
+ __pypackages__/
130
+
131
+ # Celery stuff
132
+ celerybeat-schedule
133
+ celerybeat.pid
134
+
135
+ # Redis
136
+ *.rdb
137
+ *.aof
138
+ *.pid
139
+
140
+ # RabbitMQ
141
+ mnesia/
142
+ rabbitmq/
143
+ rabbitmq-data/
144
+
145
+ # ActiveMQ
146
+ activemq-data/
147
+
148
+ # SageMath parsed files
149
+ *.sage.py
150
+
151
+ # Environments
152
+ .env
153
+ .envrc
154
+ .venv
155
+ env/
156
+ venv/
157
+ ENV/
158
+ env.bak/
159
+ venv.bak/
160
+ shell.nix
161
+
162
+ # Spyder project settings
163
+ .spyderproject
164
+ .spyproject
165
+
166
+ # Rope project settings
167
+ .ropeproject
168
+
169
+ # mkdocs documentation
170
+ /site
171
+ /docs/**/*.md
172
+
173
+ # mypy
174
+ .mypy_cache/
175
+ .dmypy.json
176
+ dmypy.json
177
+
178
+ # Pyre type checker
179
+ .pyre/
180
+
181
+ # pytype static type analyzer
182
+ .pytype/
183
+
184
+ # Cython debug symbols
185
+ cython_debug/
186
+
187
+ # PyCharm
188
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
189
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
190
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
191
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
192
+ # .idea/
193
+
194
+ # Abstra
195
+ # Abstra is an AI-powered process automation framework.
196
+ # Ignore directories containing user credentials, local state, and settings.
197
+ # Learn more at https://abstra.io/docs
198
+ .abstra/
199
+
200
+ # Visual Studio Code
201
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
202
+ # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
203
+ # and can be added to the global gitignore or merged into this file. However, if you prefer,
204
+ # you could uncomment the following to ignore the entire vscode folder
205
+ # .vscode/
206
+ # Temporary file for partial code execution
207
+ tempCodeRunnerFile.py
208
+
209
+ # Ruff stuff:
210
+ .ruff_cache/
211
+
212
+ # Artefactos generados
213
+ paper/soupdb.pdf
214
+
215
+ # PyPI configuration file
216
+ .pypirc
217
+
218
+ # Marimo
219
+ marimo/_static/
220
+ marimo/_lsp/
221
+ __marimo__/
222
+
223
+ # Streamlit
224
+ .streamlit/secrets.toml
225
+
226
+ # Agents
227
+ .agents/
228
+ .claude/
229
+ .cursor/
230
+ .codeium/
231
+ .windsurf/
232
+
233
+ # Agentes autónomos y CLI de IA
234
+ .aider*
235
+ .gpt-pilot/
236
+ .openinterpreter/
237
+
238
+ # Caché de LLMs y Bases de Datos Vectoriales Locales
239
+ .langchain/
240
+ .llamaindex/
241
+ *.db
242
+ *.log
243
+ .chroma/
244
+
245
+ # Historiales de chat generados por extensiones (Copilot, Claude, Gemini)
246
+ *.chat.md
247
+ *.chat.txt
248
+ .chat-history/
249
+ .copilot_history/
250
+ copilot-chat/
251
+
252
+ # Configuraciones de agentes locales (para evitar filtrar API keys de OpenAI/Anthropic)
253
+ .llm_config.json
254
+ .oai_keys
255
+ aider.conf.yml
256
+
257
+ # OS
258
+ .DS_Store
259
+ Thumbs.db
260
+
261
+ # IDEs genéricos
262
+ .vscode/*
263
+ !.vscode/settings.json
264
+ !.vscode/tasks.json
265
+ !.vscode/launch.json
266
+ .idea/
267
+
268
+ # Datos multimedia generados (manifests, caché de descriptores, codebooks, vectores.bin).
269
+ # Se regeneran con benchmarks/scripts/{prepare_multimedia_datasets,build_multimedia_vectors}.py
270
+ benchmarks/data/
@@ -0,0 +1,13 @@
1
+ Metadata-Version: 2.5
2
+ Name: soupdb-vector
3
+ Version: 0.1.0
4
+ Summary: SoupDB vector plugin: persistent IVF/HNSW indices and SIFT/MFCC features.
5
+ License-Expression: MIT
6
+ Requires-Python: >=3.11
7
+ Requires-Dist: librosa>=0.11.0
8
+ Requires-Dist: numpy>=2.4.6
9
+ Requires-Dist: opencv-python-headless>=5.0.0.93
10
+ Requires-Dist: scikit-learn>=1.9.1
11
+ Requires-Dist: soundfile>=0.14.0
12
+ Requires-Dist: soupdb
13
+ Requires-Dist: threadpoolctl>=3.7.0
@@ -0,0 +1,26 @@
1
+ [project]
2
+ name = "soupdb-vector"
3
+ version = "0.1.0"
4
+ description = "SoupDB vector plugin: persistent IVF/HNSW indices and SIFT/MFCC features."
5
+ requires-python = ">=3.11"
6
+ license = "MIT"
7
+ dependencies = [
8
+ "soupdb",
9
+ "librosa>=0.11.0",
10
+ "numpy>=2.4.6",
11
+ "opencv-python-headless>=5.0.0.93",
12
+ "scikit-learn>=1.9.1",
13
+ "soundfile>=0.14.0",
14
+ "threadpoolctl>=3.7.0",
15
+ ]
16
+
17
+ [project.entry-points."soupdb.plugins"]
18
+ ivf = "soupdb_vector.plugin:IVFPlugin"
19
+ hnsw = "soupdb_vector.plugin:HNSWPlugin"
20
+
21
+ [build-system]
22
+ requires = ["hatchling"]
23
+ build-backend = "hatchling.build"
24
+
25
+ [tool.hatch.build.targets.wheel]
26
+ packages = ["soupdb_vector"]
@@ -0,0 +1,3 @@
1
+ """SoupDB vector plugin package: IVF/HNSW indices and multimedia features."""
2
+
3
+ __all__ = ["indexes", "multimedia", "plugin"]
@@ -0,0 +1,18 @@
1
+ """Vector indexes for Parte 4: paged vector store, brute force, IVF and HNSW."""
2
+
3
+ from soupdb_vector.indexes.brute_force import BruteForceIndex
4
+ from soupdb_vector.indexes.hnsw import HNSWIndex
5
+ from soupdb_vector.indexes.ivf import IVFIndex
6
+ from soupdb_vector.indexes.metrics import COSINE, EUCLIDEAN, INNER_PRODUCT, METRICS
7
+ from soupdb_vector.indexes.store import VectorStore
8
+
9
+ __all__ = [
10
+ "COSINE",
11
+ "EUCLIDEAN",
12
+ "INNER_PRODUCT",
13
+ "METRICS",
14
+ "BruteForceIndex",
15
+ "HNSWIndex",
16
+ "IVFIndex",
17
+ "VectorStore",
18
+ ]
@@ -0,0 +1,154 @@
1
+ """Array of fixed-size records over pages of a DiskManager.
2
+
3
+ Page 0 is a header (magic, record size, record count and a free area for the
4
+ owner's metadata); record ``i`` lives at page ``1 + i // per_page``. Fixed
5
+ size lets a record be addressed by arithmetic instead of a slot directory,
6
+ which is what vectors and HNSW adjacency lists need.
7
+ """
8
+
9
+ import struct
10
+ from collections.abc import Iterator
11
+
12
+ from engine.storage.buffer_manager import BufferManager
13
+ from engine.storage.disk_manager import DiskManager
14
+
15
+ _HEADER = struct.Struct("<4sHIQ")
16
+ _VERSION = 1
17
+
18
+
19
+ def peek_meta(buffer_manager: BufferManager, magic: bytes, layout: struct.Struct) -> tuple:
20
+ """Read the owner's metadata of an existing file before knowing its record size."""
21
+ frame = buffer_manager.pin(0)
22
+ try:
23
+ stored_magic = _HEADER.unpack_from(frame, 0)[0]
24
+ values = layout.unpack_from(frame, _HEADER.size)
25
+ finally:
26
+ buffer_manager.unpin(0)
27
+ if stored_magic != magic:
28
+ raise ValueError(f"not a {magic!r} file")
29
+ return values
30
+
31
+
32
+ class FixedRecordFile:
33
+ """Append-only array of fixed-size records, read and written in place."""
34
+
35
+ def __init__(
36
+ self,
37
+ disk_manager: DiskManager,
38
+ buffer_manager: BufferManager,
39
+ *,
40
+ record_size: int,
41
+ magic: bytes,
42
+ ) -> None:
43
+ page_size = disk_manager.page_size
44
+ if record_size <= 0 or record_size > page_size:
45
+ raise ValueError(
46
+ f"record of {record_size} bytes does not fit in a {page_size}-byte page"
47
+ )
48
+ if len(magic) != 4:
49
+ raise ValueError("magic must be exactly 4 bytes")
50
+
51
+ self._disk = disk_manager
52
+ self._buffer = buffer_manager
53
+ self._record_size = record_size
54
+ self._per_page = page_size // record_size
55
+ self._magic = magic
56
+
57
+ if disk_manager.page_count == 0:
58
+ disk_manager.allocate_page()
59
+ self._count = 0
60
+ self._write_header()
61
+ else:
62
+ frame = buffer_manager.pin(0)
63
+ try:
64
+ stored_magic, version, stored_size, count = _HEADER.unpack_from(frame, 0)
65
+ finally:
66
+ buffer_manager.unpin(0)
67
+ if stored_magic != magic or version != _VERSION:
68
+ raise ValueError(f"{disk_manager.path} is not a {magic!r} file")
69
+ if stored_size != record_size:
70
+ raise ValueError(f"record size on disk is {stored_size}, expected {record_size}")
71
+ self._count = count
72
+
73
+ def __len__(self) -> int:
74
+ return self._count
75
+
76
+ @property
77
+ def per_page(self) -> int:
78
+ return self._per_page
79
+
80
+ @property
81
+ def meta_capacity(self) -> int:
82
+ return self._disk.page_size - _HEADER.size
83
+
84
+ def read_meta(self, layout: struct.Struct) -> tuple:
85
+ frame = self._buffer.pin(0)
86
+ try:
87
+ return layout.unpack_from(frame, _HEADER.size)
88
+ finally:
89
+ self._buffer.unpin(0)
90
+
91
+ def write_meta(self, layout: struct.Struct, *values: object) -> None:
92
+ if layout.size > self.meta_capacity:
93
+ raise ValueError("metadata does not fit in the header page")
94
+ frame = self._buffer.pin(0)
95
+ layout.pack_into(frame, _HEADER.size, *values)
96
+ self._buffer.unpin(0, dirty=True)
97
+
98
+ def append(self, layout: struct.Struct, *values: object) -> int:
99
+ index = self._count
100
+ page_id, offset = self._locate(index)
101
+ if page_id == self._disk.page_count:
102
+ self._disk.allocate_page()
103
+ frame = self._buffer.pin(page_id)
104
+ layout.pack_into(frame, offset, *values)
105
+ self._buffer.unpin(page_id, dirty=True)
106
+ self._count += 1
107
+ self._write_header()
108
+ return index
109
+
110
+ def unpack(self, index: int, layout: struct.Struct, extra: int = 0) -> tuple:
111
+ """Unpack ``layout`` at byte ``extra`` of record ``index``."""
112
+ page_id, offset = self._locate(self._check(index))
113
+ frame = self._buffer.pin(page_id)
114
+ try:
115
+ return layout.unpack_from(frame, offset + extra)
116
+ finally:
117
+ self._buffer.unpin(page_id)
118
+
119
+ def pack(self, index: int, layout: struct.Struct, *values: object, extra: int = 0) -> None:
120
+ page_id, offset = self._locate(self._check(index))
121
+ frame = self._buffer.pin(page_id)
122
+ layout.pack_into(frame, offset + extra, *values)
123
+ self._buffer.unpin(page_id, dirty=True)
124
+
125
+ def scan(self, layout: struct.Struct) -> Iterator[tuple[int, tuple]]:
126
+ """Yield (index, values) for every record, pinning each page once."""
127
+ index = 0
128
+ page_id = 1
129
+ while index < self._count:
130
+ frame = self._buffer.pin(page_id)
131
+ try:
132
+ in_page = min(self._per_page, self._count - index)
133
+ for slot in range(in_page):
134
+ yield index + slot, layout.unpack_from(frame, slot * self._record_size)
135
+ finally:
136
+ self._buffer.unpin(page_id)
137
+ index += in_page
138
+ page_id += 1
139
+
140
+ def flush(self) -> None:
141
+ self._buffer.flush_all()
142
+
143
+ def _locate(self, index: int) -> tuple[int, int]:
144
+ return 1 + index // self._per_page, (index % self._per_page) * self._record_size
145
+
146
+ def _check(self, index: int) -> int:
147
+ if not 0 <= index < self._count:
148
+ raise IndexError(f"record {index} out of range (count {self._count})")
149
+ return index
150
+
151
+ def _write_header(self) -> None:
152
+ frame = self._buffer.pin(0)
153
+ _HEADER.pack_into(frame, 0, self._magic, _VERSION, self._record_size, self._count)
154
+ self._buffer.unpin(0, dirty=True)
@@ -0,0 +1,68 @@
1
+ """Exact k-NN by scanning every stored vector.
2
+
3
+ It is the sequential baseline of the comparison 2.4.4 and the oracle the
4
+ recall of IVF and HNSW is measured against.
5
+ """
6
+
7
+ import heapq
8
+ from collections.abc import Sequence
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ from engine.common.rid import RID
13
+ from engine.indexes.base import RankedIndex
14
+ from soupdb_vector.indexes.constants import DEFAULT_METRIC, VECTOR_BUFFER_CAPACITY
15
+ from soupdb_vector.indexes.metrics import norm, score_fn
16
+ from soupdb_vector.indexes.store import VectorStore
17
+
18
+
19
+ class BruteForceIndex(RankedIndex):
20
+ """RankedIndex that scores every vector; memory is O(k), not O(N)."""
21
+
22
+ def __init__(self, store: VectorStore) -> None:
23
+ self._store = store
24
+
25
+ @classmethod
26
+ def open(
27
+ cls,
28
+ path: str | Path,
29
+ dim: int | None = None,
30
+ *,
31
+ page_size: int = 4096,
32
+ buffer_capacity: int = VECTOR_BUFFER_CAPACITY,
33
+ ) -> "BruteForceIndex":
34
+ return cls(
35
+ VectorStore.open(path, dim, page_size=page_size, buffer_capacity=buffer_capacity)
36
+ )
37
+
38
+ @property
39
+ def store(self) -> VectorStore:
40
+ return self._store
41
+
42
+ def __len__(self) -> int:
43
+ return len(self._store)
44
+
45
+ def insert(self, vector: Sequence[float], rid: RID) -> None:
46
+ self._store.append(vector, rid)
47
+
48
+ def top_k(
49
+ self, query: Sequence[float], k: int, *, metric: str = DEFAULT_METRIC
50
+ ) -> list[tuple[RID, float]]:
51
+ if k < 0:
52
+ raise ValueError(f"k must be non-negative, got {k}")
53
+ self._store.check_dim(query)
54
+ scorer = score_fn(metric)
55
+ query_norm = norm(query)
56
+ scored = (
57
+ (scorer(query, query_norm, vector, vector_norm), rid)
58
+ for _vid, rid, vector, vector_norm in self._store.scan()
59
+ )
60
+ best = heapq.nlargest(k, scored, key=_by_score)
61
+ return [(rid, value) for value, rid in best]
62
+
63
+ def close(self) -> None:
64
+ self._store.close()
65
+
66
+
67
+ def _by_score(pair: tuple[float, Any]) -> float:
68
+ return pair[0]
@@ -0,0 +1,34 @@
1
+ """Tunable parameters of the vector indexes.
2
+
3
+ Kept apart so the benchmarks can vary them without touching the structures.
4
+ """
5
+
6
+ #: Métrica por defecto cuando la consulta no indica ``metric``.
7
+ DEFAULT_METRIC = "euclidean"
8
+
9
+ #: Páginas en el buffer pool de cada archivo cuando el índice abre sus propios managers.
10
+ VECTOR_BUFFER_CAPACITY = 64
11
+
12
+ #: Número de listas (centroides) del IVF; la regla habitual es ~sqrt(N).
13
+ IVF_NLIST = 32
14
+
15
+ #: Listas que se visitan por consulta.
16
+ IVF_NPROBE = 4
17
+
18
+ #: Iteraciones de Lloyd al entrenar los centroides.
19
+ IVF_KMEANS_ITERATIONS = 20
20
+
21
+ #: Tope de la muestra con la que se entrena K-Means; acota la memoria del entrenamiento.
22
+ IVF_TRAIN_SAMPLE = 10_000
23
+
24
+ #: Vecinos por nodo en las capas superiores de HNSW; la capa 0 guarda el doble.
25
+ HNSW_M = 16
26
+
27
+ #: Tamaño de la lista de candidatos al insertar.
28
+ HNSW_EF_CONSTRUCTION = 100
29
+
30
+ #: Tamaño de la lista de candidatos al consultar.
31
+ HNSW_EF_SEARCH = 64
32
+
33
+ #: Tope de niveles del grafo; con M=16 un nivel 16 es astronómicamente improbable.
34
+ HNSW_MAX_LEVEL = 16