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.
- soupdb_vector-0.1.0/.gitignore +270 -0
- soupdb_vector-0.1.0/PKG-INFO +13 -0
- soupdb_vector-0.1.0/pyproject.toml +26 -0
- soupdb_vector-0.1.0/soupdb_vector/__init__.py +3 -0
- soupdb_vector-0.1.0/soupdb_vector/indexes/__init__.py +18 -0
- soupdb_vector-0.1.0/soupdb_vector/indexes/_record_file.py +154 -0
- soupdb_vector-0.1.0/soupdb_vector/indexes/brute_force.py +68 -0
- soupdb_vector-0.1.0/soupdb_vector/indexes/constants.py +34 -0
- soupdb_vector-0.1.0/soupdb_vector/indexes/hnsw.py +372 -0
- soupdb_vector-0.1.0/soupdb_vector/indexes/ivf.py +378 -0
- soupdb_vector-0.1.0/soupdb_vector/indexes/kmeans.py +105 -0
- soupdb_vector-0.1.0/soupdb_vector/indexes/metrics.py +76 -0
- soupdb_vector-0.1.0/soupdb_vector/indexes/sql_factory.py +235 -0
- soupdb_vector-0.1.0/soupdb_vector/indexes/store.py +144 -0
- soupdb_vector-0.1.0/soupdb_vector/multimedia/__init__.py +1 -0
- soupdb_vector-0.1.0/soupdb_vector/multimedia/audio_features.py +132 -0
- soupdb_vector-0.1.0/soupdb_vector/multimedia/build_vectors.py +190 -0
- soupdb_vector-0.1.0/soupdb_vector/multimedia/codebook.py +94 -0
- soupdb_vector-0.1.0/soupdb_vector/multimedia/descriptor_cache.py +52 -0
- soupdb_vector-0.1.0/soupdb_vector/multimedia/image_features.py +77 -0
- soupdb_vector-0.1.0/soupdb_vector/multimedia/manifest.py +111 -0
- soupdb_vector-0.1.0/soupdb_vector/multimedia/sql_profiles.py +145 -0
- soupdb_vector-0.1.0/soupdb_vector/multimedia/vector_file.py +88 -0
- soupdb_vector-0.1.0/soupdb_vector/mutable.py +99 -0
- 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,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
|