-
Notifications
You must be signed in to change notification settings - Fork 11
Expand file tree
/
Copy pathingestion.py
More file actions
2972 lines (2795 loc) · 123 KB
/
Copy pathingestion.py
File metadata and controls
2972 lines (2795 loc) · 123 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
"""
Vector Search ingestion Utilities
This contains the ingestion implementation for different TileDB Vector Search algorithms.
It enables:
- Local ingestion:
- Multi-threaded execution that can leverage all the available local computing resources.
- Distributed ingestion:
- Distributed ingestion execution with multiple workers in TileDB Cloud. This can be used
to ingest large datasets and speedup ingestion latency.
"""
import enum
from functools import partial
from typing import Any, Mapping, Optional, Tuple
import numpy as np
from tiledb.cloud.dag import Mode
from tiledb.vector_search._tiledbvspy import *
from tiledb.vector_search.storage_formats import STORAGE_VERSION
from tiledb.vector_search.storage_formats import validate_storage_version
from tiledb.vector_search.utils import add_to_group
from tiledb.vector_search.utils import is_type_erased_index
from tiledb.vector_search.utils import to_temporal_policy
class TrainingSamplingPolicy(enum.Enum):
FIRST_N = 1
RANDOM = 2
def __str__(self):
return self.name.replace("_", " ").title()
def ingest(
index_type: str,
index_uri: str,
*,
input_vectors: Optional[np.ndarray] = None,
source_uri: Optional[str] = None,
source_type: Optional[str] = None,
external_ids: Optional[np.array] = None,
external_ids_uri: Optional[str] = "",
external_ids_type: Optional[str] = None,
updates_uri: Optional[str] = None,
index_timestamp: Optional[int] = None,
config: Optional[Mapping[str, Any]] = None,
namespace: Optional[str] = None,
size: int = -1,
partitions: int = -1,
num_subspaces: int = -1,
l_build: int = -1,
r_max_degree: int = -1,
training_sampling_policy: TrainingSamplingPolicy = TrainingSamplingPolicy.RANDOM,
copy_centroids_uri: Optional[str] = None,
training_sample_size: int = -1,
training_input_vectors: Optional[np.ndarray] = None,
training_source_uri: Optional[str] = None,
training_source_type: Optional[str] = None,
workers: int = -1,
input_vectors_per_work_item: int = -1,
max_tasks_per_stage: int = -1,
input_vectors_per_work_item_during_sampling: int = -1,
max_sampling_tasks: int = -1,
storage_version: str = STORAGE_VERSION,
verbose: bool = False,
trace_id: Optional[str] = None,
use_sklearn: bool = True,
mode: Mode = Mode.LOCAL,
acn: Optional[str] = None,
ingest_resources: Optional[Mapping[str, Any]] = None,
consolidate_partition_resources: Optional[Mapping[str, Any]] = None,
copy_centroids_resources: Optional[Mapping[str, Any]] = None,
random_sample_resources: Optional[Mapping[str, Any]] = None,
kmeans_resources: Optional[Mapping[str, Any]] = None,
compute_new_centroids_resources: Optional[Mapping[str, Any]] = None,
assign_points_and_partial_new_centroids_resources: Optional[
Mapping[str, Any]
] = None,
write_centroids_resources: Optional[Mapping[str, Any]] = None,
partial_index_resources: Optional[Mapping[str, Any]] = None,
**kwargs,
):
"""
Ingest vectors into TileDB.
Parameters
----------
index_type: str
Type of vector index (FLAT, IVF_FLAT, IVF_PQ, VAMANA).
index_uri: str
Vector index URI (stored as TileDB group).
input_vectors: np.ndarray
Input vectors, if this is provided it takes precedence over `source_uri` and `source_type`.
source_uri: str
Vectors source URI.
source_type: str
Type of the source vectors. If left empty it is auto-detected.
external_ids: np.array
Input vector `external_ids`, if this is provided it takes precedence over `external_ids_uri` and `external_ids_type`.
external_ids_uri: str
Source URI for `external_ids`.
external_ids_type: str
File type of external_ids_uri. If left empty it is auto-detected.
updates_uri: str
Updates array URI. Used for consolidation of updates.
index_timestamp: int
Timestamp to use for writing and reading data. By default it uses the current unix ms timestamp.
config: Optional[Mapping[str, Any]]
TileDB config dictionary.
namespace: str
TileDB-Cloud namespace to use for Cloud execution.
size: int
Number of input vectors, if not provided use the full size of the input dataset.
If provided, we filter the first vectors from the input source.
partitions: int
For IVF indexes, the number of partitions to load the data with, if not provided, is auto-configured based on the dataset size.
num_subspaces: int
For PQ encoded indexes, the number of subspaces to use in the PQ encoding. We will divide the dimensions into
num_subspaces parts, and PQ encode each part separately. This means dimensions must
be divisible by num_subspaces.
l_build: int
For Vamana indexes, the number of neighbors considered for each node during construction of the graph. Larger values will take more time to build but result in indices that provide higher recall for the same search complexity. l_build should be >= r_max_degree unless you need to build indices quickly and can compromise on quality.
Typically between 75 and 200. If not provided, use the default value of 100.
r_max_degree: int
For Vamana indexes, the maximum degree for each node in the final graph. Larger values will result in larger indices and longer indexing times, but better search quality.
Typically between 60 and 150. If not provided, use the default value of 64.
copy_centroids_uri: str
TileDB array URI to copy centroids from, if not provided, centroids are build running `k-means`.
training_sample_size: int
Sample size to use for computing `k-means`. If not provided, is auto-configured based on the dataset sizes.
Should not be provided if training_source_uri is provided.
training_input_vectors: np.ndarray
Training input vectors, if this is provided it takes precedence over `training_source_uri` and `training_source_type`.
Should not be provided if `training_sample_size` or `training_source_uri` are provided.
training_source_uri: str
The source URI to use for training centroids when building a `IVF_FLAT` vector index.
If not provided, the first `training_sample_size` vectors from `source_uri` are used.
Should not be provided if training_sample_size or training_input_vectors is provided.
training_source_type: str
Type of the training source data in `training_source_uri`.
If left empty, is auto-detected. Should only be provided when `training_source_uri` is provided.
workers: int
Number of distributed workers to use for vector ingestion.
If not provided, is auto-configured based on the dataset size.
input_vectors_per_work_item: int
Number of vectors per ingestion work item.
If not provided, is auto-configured.
max_tasks_per_stage: int
Max number of tasks per execution stage of ingestion.
If not provided, is auto-configured.
input_vectors_per_work_item_during_sampling: int
Number of vectors per sample ingestion work item.
iIf not provided, is auto-configured.
Only valid with `training_sampling_policy=TrainingSamplingPolicy.RANDOM`.
max_sampling_tasks: int
Max number of tasks per execution stage of sampling.
If not provided, is auto-configured
Only valid with `training_sampling_policy=TrainingSamplingPolicy.RANDOM`.
storage_version: str
Vector index storage format version. If not provided, defaults to the latest version.
verbose: bool
Enables verbose logging.
trace_id: Optional[str]
trace ID for logging.
use_sklearn: bool
Whether to use scikit-learn's implementation of k-means clustering instead of
tiledb.vector_search's.
mode: Mode
Execution mode, defaults to `LOCAL` use `BATCH` for distributed execution.
acn: Optional[str]
Access credential name to be used when running in BATCH mode for object store access
ingest_resources: Optional[Mapping[str, Any]]
Resources to request when performing vector ingestion, only applies to BATCH mode
consolidate_partition_resources: Optional[Mapping[str, Any]]
Resources to request when performing consolidation of a partition, only applies to BATCH mode
copy_centroids_resources: Optional[Mapping[str, Any]]
Resources to request when performing copy of centroids from input array to output array, only applies to BATCH mode
random_sample_resources: Optional[Mapping[str, Any]]
Resources to request when performing random sample selection, only applies to BATCH mode
kmeans_resources: Optional[Mapping[str, Any]]
Resources to request when performing kmeans task, only applies to BATCH mode
compute_new_centroids_resources: Optional[Mapping[str, Any]]
Resources to request when performing centroid computation, only applies to BATCH mode
assign_points_and_partial_new_centroids_resources: Optional[Mapping[str, Any]]
Resources to request when performing the computation of partial centroids, only applies to BATCH mode
write_centroids_resources: Optional[Mapping[str, Any]]
Resources to request when performing the write of centroids, only applies to BATCH mode
partial_index_resources: Optional[Mapping[str, Any]]
Resources to request when performing the computation of partial indexing, only applies to BATCH mode
"""
import enum
import json
import logging
import math
import multiprocessing
import os
import random
import string
import time
from typing import Any, Mapping
import numpy as np
import tiledb
from tiledb.cloud import dag
from tiledb.cloud.rest_api import models
from tiledb.cloud.utilities import get_logger
from tiledb.cloud.utilities import set_aws_context
from tiledb.vector_search import flat_index
from tiledb.vector_search import ivf_flat_index
from tiledb.vector_search import ivf_pq_index
from tiledb.vector_search import vamana_index
from tiledb.vector_search.storage_formats import storage_formats
validate_storage_version(storage_version)
if source_type and not source_uri:
raise ValueError("source_type should not be provided without source_uri")
if source_uri and input_vectors:
raise ValueError("source_uri should not be provided alongside input_vectors")
if source_type and input_vectors:
raise ValueError("source_type should not be provided alongside input_vectors")
if training_source_uri and training_sample_size != -1:
raise ValueError(
"training_source_uri and training_sample_size should not both be provided"
)
if training_source_uri and training_input_vectors is not None:
raise ValueError(
"training_source_uri and training_input_vectors should not both be provided"
)
if training_input_vectors is not None and training_sample_size != -1:
raise ValueError(
"training_input_vectors and training_sample_size should not both be provided"
)
if training_input_vectors is not None and training_source_type:
raise ValueError(
"training_input_vectors and training_source_type should not both be provided"
)
if training_source_type and not training_source_uri:
raise ValueError(
"training_source_type should not be provided without training_source_uri"
)
if training_sample_size < -1:
raise ValueError(
"training_sample_size should either be positive or -1 (to auto-configure based on the dataset sizes)"
)
if copy_centroids_uri is not None and training_sample_size != -1:
raise ValueError(
"training_sample_size should not be provided alongside copy_centroids_uri"
)
if copy_centroids_uri is not None and partitions == -1:
raise ValueError(
"partitions should be provided if copy_centroids_uri is provided (set partitions to the number of centroids in copy_centroids_uri)"
)
if index_type != "IVF_FLAT" and training_sample_size != -1:
raise ValueError(
"training_sample_size should only be provided with index_type IVF_FLAT"
)
for variable in [
"copy_centroids_uri",
"training_input_vectors",
"training_source_uri",
"training_source_type",
]:
if index_type != "IVF_FLAT" and locals().get(variable) is not None:
raise ValueError(
f"{variable} should only be provided with index_type IVF_FLAT"
)
for variable in [
"copy_centroids_uri",
"training_input_vectors",
"training_source_uri",
"training_source_type",
]:
if (
training_sampling_policy != TrainingSamplingPolicy.FIRST_N
and locals().get(variable) is not None
):
raise ValueError(
f"{variable} should not provided alonside training_sampling_policy"
)
# use index_group_uri for internal clarity
index_group_uri = index_uri
CENTROIDS_ARRAY_NAME = storage_formats[storage_version]["CENTROIDS_ARRAY_NAME"]
INDEX_ARRAY_NAME = storage_formats[storage_version]["INDEX_ARRAY_NAME"]
IDS_ARRAY_NAME = storage_formats[storage_version]["IDS_ARRAY_NAME"]
PARTS_ARRAY_NAME = storage_formats[storage_version]["PARTS_ARRAY_NAME"]
INPUT_VECTORS_ARRAY_NAME = storage_formats[storage_version][
"INPUT_VECTORS_ARRAY_NAME"
]
TRAINING_INPUT_VECTORS_ARRAY_NAME = storage_formats[storage_version][
"TRAINING_INPUT_VECTORS_ARRAY_NAME"
]
EXTERNAL_IDS_ARRAY_NAME = storage_formats[storage_version][
"EXTERNAL_IDS_ARRAY_NAME"
]
PARTIAL_WRITE_ARRAY_DIR = (
storage_formats[storage_version]["PARTIAL_WRITE_ARRAY_DIR"]
+ "_"
+ "".join(random.choices(string.ascii_letters, k=10))
)
DEFAULT_ATTR_FILTERS = storage_formats[storage_version]["DEFAULT_ATTR_FILTERS"]
# This is used to auto-configure `input_vectors_per_work_item`
# We set it to a size that based on testing allowed ingestion to complete without
# OOM errors in small local servers (<16GB). This also avoids large TileDB write
# operations that can cause a problem with the REST server.
DEFAULT_PARTITION_BYTE_SIZE = 2**30 # 1GB
# This is used to auto-configure the resources of vector ingestion DAG tasks.
# It is not actually enforcing a maximum vector partition size as the client can
# override `input_vectors_per_work_item` and the UDF resources.
MAX_PARTITION_BYTE_SIZE = 2**31 # 2GB
VECTORS_PER_SAMPLE_WORK_ITEM = 1000000
MAX_TASKS_PER_STAGE = 100
# This is used to auto-configure `kmeans_resources`, `training_sample_size`
# and `partitions` selected for different datasets. It is based on the kmeans
# algorithm complexity which is O(vectors * dimensions * centroids).
# We set this to a value that based on testing allowed kmeans to complete
# without OOM errors in small local servers (<16GB).
MAX_CENTRALISED_KMEANS_COMPLEXITY = (
1000000 * 1024 * 10000
) # 1M vectors of 1024 float32 values, 10k partitions
CENTRALISED_KMEANS_MAX_SAMPLE_SIZE = 1000000
DEFAULT_IMG_NAME = "3.9-vectorsearch"
MAX_INT32 = 2**31 - 1
class SourceType(enum.Enum):
"""SourceType of input vectors"""
TILEDB_ARRAY = enum.auto()
U8BIN = enum.auto()
def __str__(self):
return self.name.replace("_", " ").title()
def setup(
config: Optional[Mapping[str, Any]] = None,
verbose: bool = False,
) -> logging.Logger:
"""
Set the default TileDB context, OS environment variables for AWS,
and return a logger instance.
:param config: config dictionary, defaults to None
:param verbose: verbose logging, defaults to False
:return: logger instance
"""
try:
tiledb.default_ctx(config)
except tiledb.TileDBError:
# Ignore error if the default context was already set
pass
set_aws_context(config)
level = logging.DEBUG if verbose else logging.NOTSET
logger = get_logger(level)
logger.debug(
"tiledb.cloud=%s, tiledb=%s, libtiledb=%s",
tiledb.cloud.version.version,
tiledb.version(),
tiledb.libtiledb.version(),
)
return logger
def autodetect_source_type(source_uri: str) -> str:
if source_uri.endswith(".u8bin"):
return "U8BIN"
elif source_uri.endswith(".f32bin"):
return "F32BIN"
elif source_uri.endswith(".fvecs"):
return "FVEC"
elif source_uri.endswith(".ivecs"):
return "IVEC"
elif source_uri.endswith(".bvecs"):
return "BVEC"
else:
return "TILEDB_ARRAY"
def read_source_metadata(
source_uri: str, source_type: Optional[str] = None
) -> Tuple[int, int, np.dtype]:
if source_type == "TILEDB_ARRAY":
schema = tiledb.ArraySchema.load(source_uri)
size = schema.domain.dim(1).domain[1] + 1
dimensions = schema.domain.dim(0).domain[1] + 1
return size, dimensions, schema.attr(0).dtype
if source_type == "TILEDB_SPARSE_ARRAY":
schema = tiledb.ArraySchema.load(source_uri)
size = schema.domain.dim(0).domain[1] + 1
dimensions = schema.domain.dim(1).domain[1] + 1
return size, dimensions, schema.attr(0).dtype
if source_type == "TILEDB_PARTITIONED_ARRAY":
with tiledb.open(source_uri, "r", config=config) as source_array:
q = source_array.query(attrs=("vectors_shape",), coords=True)
nonempty_object_array_domain = source_array.nonempty_domain()
partition_shapes = q[
nonempty_object_array_domain[0][0] : nonempty_object_array_domain[
0
][1]
+ 1
]["vectors_shape"]
size = 0
for partition_shape in partition_shapes:
size += partition_shape[0]
dimensions = partition_shape[1]
return size, dimensions, source_array.schema.attr("vectors").dtype
elif source_type == "U8BIN":
vfs = tiledb.VFS()
with vfs.open(source_uri, "rb") as f:
size = int.from_bytes(f.read(4), "little")
dimensions = int.from_bytes(f.read(4), "little")
return size, dimensions, np.uint8
elif source_type == "F32BIN":
vfs = tiledb.VFS()
with vfs.open(source_uri, "rb") as f:
size = int.from_bytes(f.read(4), "little")
dimensions = int.from_bytes(f.read(4), "little")
return size, dimensions, np.float32
elif source_type == "FVEC":
vfs = tiledb.VFS()
with vfs.open(source_uri, "rb") as f:
dimensions = int.from_bytes(f.read(4), "little")
vector_size = 4 + dimensions * 4
f.seek(0, os.SEEK_END)
file_size = f.tell()
size = int(file_size / vector_size)
return size, dimensions, np.float32
elif source_type == "IVEC":
vfs = tiledb.VFS()
with vfs.open(source_uri, "rb") as f:
dimensions = int.from_bytes(f.read(4), "little")
vector_size = 4 + dimensions * 4
f.seek(0, os.SEEK_END)
file_size = f.tell()
size = int(file_size / vector_size)
return size, dimensions, np.int32
elif source_type == "BVEC":
vfs = tiledb.VFS()
with vfs.open(source_uri, "rb") as f:
dimensions = int.from_bytes(f.read(4), "little")
vector_size = 4 + dimensions
f.seek(0, os.SEEK_END)
file_size = f.tell()
size = int(file_size / vector_size)
return size, dimensions, np.uint8
else:
raise ValueError(
f"Not supported source_type {source_type} - valid types are [TILEDB_ARRAY, TILEDB_SPARSE_ARRAY, U8BIN, F32BIN, FVEC, IVEC, BVEC]"
)
def create_array(
group: tiledb.Group,
size: int,
dimensions: int,
vector_type: np.dtype,
array_name: str,
) -> str:
input_vectors_array_uri = f"{group.uri}/{array_name}"
if tiledb.array_exists(input_vectors_array_uri):
raise ValueError(f"Array exists {input_vectors_array_uri}")
tile_size = min(
size,
int(
flat_index.TILE_SIZE_BYTES / np.dtype(vector_type).itemsize / dimensions
),
)
logger.debug("Creating input vectors array")
input_vectors_array_rows_dim = tiledb.Dim(
name="rows",
domain=(0, dimensions - 1),
tile=dimensions,
dtype=np.dtype(np.int32),
)
input_vectors_array_cols_dim = tiledb.Dim(
name="cols",
domain=(0, size - 1),
tile=tile_size,
dtype=np.dtype(np.int32),
)
input_vectors_array_dom = tiledb.Domain(
input_vectors_array_rows_dim, input_vectors_array_cols_dim
)
input_vectors_array_attr = tiledb.Attr(
name="values", dtype=vector_type, filters=DEFAULT_ATTR_FILTERS
)
input_vectors_array_schema = tiledb.ArraySchema(
domain=input_vectors_array_dom,
sparse=False,
attrs=[input_vectors_array_attr],
cell_order="col-major",
tile_order="col-major",
)
logger.debug(input_vectors_array_schema)
tiledb.Array.create(input_vectors_array_uri, input_vectors_array_schema)
add_to_group(group, input_vectors_array_uri, array_name)
return input_vectors_array_uri
def write_input_vectors(
group: tiledb.Group,
input_vectors: np.ndarray,
size: int,
dimensions: int,
vector_type: np.dtype,
array_name: str,
) -> str:
input_vectors_array_uri = create_array(
group=group,
size=size,
dimensions=dimensions,
vector_type=vector_type,
array_name=array_name,
)
input_vectors_array = tiledb.open(
input_vectors_array_uri, "w", timestamp=index_timestamp
)
input_vectors_array[:, :] = np.transpose(input_vectors)
input_vectors_array.close()
return input_vectors_array_uri
def write_external_ids(
group: tiledb.Group,
external_ids: np.array,
size: int,
partitions: int,
) -> str:
external_ids_array_uri = f"{group.uri}/{EXTERNAL_IDS_ARRAY_NAME}"
if tiledb.array_exists(external_ids_array_uri):
raise ValueError(f"Array exists {external_ids_array_uri}")
logger.debug("Creating external IDs array")
ids_array_rows_dim = tiledb.Dim(
name="rows",
domain=(0, size - 1),
tile=int(size / partitions),
dtype=np.dtype(np.int32),
)
ids_array_dom = tiledb.Domain(ids_array_rows_dim)
ids_attr = tiledb.Attr(
name="values",
dtype=np.dtype(np.uint64),
filters=DEFAULT_ATTR_FILTERS,
)
ids_schema = tiledb.ArraySchema(
domain=ids_array_dom,
sparse=False,
attrs=[ids_attr],
capacity=int(size / partitions),
cell_order="col-major",
tile_order="col-major",
)
logger.debug(ids_schema)
tiledb.Array.create(external_ids_array_uri, ids_schema)
add_to_group(group, external_ids_array_uri, EXTERNAL_IDS_ARRAY_NAME)
external_ids_array = tiledb.open(
external_ids_array_uri, "w", timestamp=index_timestamp
)
external_ids_array[:] = external_ids
external_ids_array.close()
return external_ids_array_uri
def create_temp_data_group(
group: tiledb.Group,
) -> tiledb.Group:
partial_write_array_dir_uri = f"{group.uri}/{PARTIAL_WRITE_ARRAY_DIR}"
try:
tiledb.group_create(partial_write_array_dir_uri)
add_to_group(group, partial_write_array_dir_uri, PARTIAL_WRITE_ARRAY_DIR)
except tiledb.TileDBError as err:
message = str(err)
if "already exists" not in message:
raise err
return tiledb.Group(partial_write_array_dir_uri, "w")
def create_partial_write_array_group(
temp_data_group: tiledb.Group,
vector_type: np.dtype,
dimensions: int,
filters: Any,
create_index_array: bool,
) -> str:
tile_size = int(
ivf_flat_index.TILE_SIZE_BYTES / np.dtype(vector_type).itemsize / dimensions
)
partial_write_array_index_uri = f"{temp_data_group.uri}/{INDEX_ARRAY_NAME}"
partial_write_array_ids_uri = f"{temp_data_group.uri}/{IDS_ARRAY_NAME}"
partial_write_array_parts_uri = f"{temp_data_group.uri}/{PARTS_ARRAY_NAME}"
if create_index_array:
try:
tiledb.group_create(partial_write_array_index_uri)
except tiledb.TileDBError as err:
message = str(err)
if "already exists" in message:
logger.debug(
f"Group '{partial_write_array_index_uri}' already exists"
)
raise err
add_to_group(
temp_data_group,
partial_write_array_index_uri,
INDEX_ARRAY_NAME,
)
if not tiledb.array_exists(partial_write_array_ids_uri):
logger.debug("Creating temp ids array")
ids_array_rows_dim = tiledb.Dim(
name="rows",
domain=(0, MAX_INT32),
tile=tile_size,
dtype=np.dtype(np.int32),
)
ids_array_dom = tiledb.Domain(ids_array_rows_dim)
ids_attr = tiledb.Attr(
name="values",
dtype=np.dtype(np.uint64),
filters=filters,
)
ids_schema = tiledb.ArraySchema(
domain=ids_array_dom,
sparse=False,
attrs=[ids_attr],
capacity=tile_size,
cell_order="col-major",
tile_order="col-major",
)
logger.debug(ids_schema)
tiledb.Array.create(partial_write_array_ids_uri, ids_schema)
add_to_group(
temp_data_group,
partial_write_array_ids_uri,
IDS_ARRAY_NAME,
)
if not tiledb.array_exists(partial_write_array_parts_uri):
logger.debug("Creating temp parts array")
parts_array_rows_dim = tiledb.Dim(
name="rows",
domain=(0, dimensions - 1),
tile=dimensions,
dtype=np.dtype(np.int32),
)
parts_array_cols_dim = tiledb.Dim(
name="cols",
domain=(0, MAX_INT32),
tile=tile_size,
dtype=np.dtype(np.int32),
)
parts_array_dom = tiledb.Domain(parts_array_rows_dim, parts_array_cols_dim)
parts_attr = tiledb.Attr(name="values", dtype=vector_type, filters=filters)
parts_schema = tiledb.ArraySchema(
domain=parts_array_dom,
sparse=False,
attrs=[parts_attr],
cell_order="col-major",
tile_order="col-major",
)
logger.debug(parts_schema)
logger.debug(partial_write_array_parts_uri)
tiledb.Array.create(partial_write_array_parts_uri, parts_schema)
add_to_group(
temp_data_group,
partial_write_array_parts_uri,
PARTS_ARRAY_NAME,
)
return partial_write_array_index_uri
def create_arrays(
group: tiledb.Group,
temp_data_group: tiledb.Group,
arrays_created: bool,
index_type: str,
dimensions: int,
input_vectors_work_items: int,
vector_type: np.dtype,
logger: logging.Logger,
storage_version: str,
) -> None:
if index_type == "FLAT":
if not arrays_created:
flat_index.create(
uri=group.uri,
dimensions=dimensions,
vector_type=vector_type,
group_exists=True,
config=config,
storage_version=storage_version,
)
elif index_type == "IVF_FLAT":
if not arrays_created:
ivf_flat_index.create(
uri=group.uri,
dimensions=dimensions,
vector_type=vector_type,
group_exists=True,
config=config,
storage_version=storage_version,
)
partial_write_array_index_uri = create_partial_write_array_group(
temp_data_group=temp_data_group,
vector_type=vector_type,
dimensions=dimensions,
filters=DEFAULT_ATTR_FILTERS,
create_index_array=True,
)
partial_write_array_index_group = tiledb.Group(
partial_write_array_index_uri, "w"
)
for part in range(input_vectors_work_items):
part_index_uri = partial_write_array_index_uri + "/" + str(part)
if not tiledb.array_exists(part_index_uri):
logger.debug(f"Creating part array {part_index_uri}")
index_array_rows_dim = tiledb.Dim(
name="rows",
domain=(0, partitions),
tile=partitions,
dtype=np.dtype(np.int32),
)
index_array_dom = tiledb.Domain(index_array_rows_dim)
index_attr = tiledb.Attr(
name="values",
dtype=np.dtype(np.uint64),
filters=DEFAULT_ATTR_FILTERS,
)
index_schema = tiledb.ArraySchema(
domain=index_array_dom,
sparse=False,
attrs=[index_attr],
capacity=partitions,
cell_order="col-major",
tile_order="col-major",
)
logger.debug(index_schema)
tiledb.Array.create(part_index_uri, index_schema)
add_to_group(
partial_write_array_index_group, part_index_uri, str(part)
)
if updates_uri is not None:
part_index_uri = partial_write_array_index_uri + "/additions"
if not tiledb.array_exists(part_index_uri):
logger.debug(f"Creating part array {part_index_uri}")
index_array_rows_dim = tiledb.Dim(
name="rows",
domain=(0, partitions),
tile=partitions,
dtype=np.dtype(np.int32),
)
index_array_dom = tiledb.Domain(index_array_rows_dim)
index_attr = tiledb.Attr(
name="values",
dtype=np.dtype(np.uint64),
filters=DEFAULT_ATTR_FILTERS,
)
index_schema = tiledb.ArraySchema(
domain=index_array_dom,
sparse=False,
attrs=[index_attr],
capacity=partitions,
cell_order="col-major",
tile_order="col-major",
)
logger.debug(index_schema)
tiledb.Array.create(part_index_uri, index_schema)
add_to_group(
partial_write_array_index_group, part_index_uri, "additions"
)
partial_write_array_index_group.close()
# Note that we don't create type-erased indexes (i.e. Vamana) here. Instead we create them
# at very start of ingest() in C++.
elif not is_type_erased_index(index_type):
raise ValueError(f"Not supported index_type {index_type}")
def read_external_ids(
external_ids_uri: str,
external_ids_type: str,
start_pos: int,
end_pos: int,
config: Optional[Mapping[str, Any]] = None,
verbose: bool = False,
trace_id: Optional[str] = None,
) -> np.array:
logger = setup(config, verbose)
logger.debug(
"Reading external_ids start_pos: %i, end_pos: %i", start_pos, end_pos
)
if external_ids_uri == "":
return np.arange(start_pos, end_pos).astype(np.uint64)
if external_ids_type == "TILEDB_ARRAY":
with tiledb.open(
external_ids_uri, mode="r", timestamp=index_timestamp
) as external_ids_array:
return external_ids_array[start_pos:end_pos]["values"]
elif source_type == "TILEDB_PARTITIONED_ARRAY":
with tiledb.open(source_uri, "r") as source_array:
q = source_array.query(attrs=("vectors_shape",), coords=True)
nonempty_object_array_domain = source_array.nonempty_domain()
partitions = q[
nonempty_object_array_domain[0][0] : nonempty_object_array_domain[
0
][1]
+ 1
]
partition_idx_start = 0
partition_idx_end = 0
i = 0
external_ids = None
for partition_shape in partitions["vectors_shape"]:
partition_id = partitions["partition_id"][i]
partition_idx_end += partition_shape[0]
intersection_start = max(start_pos, partition_idx_start)
intersection_end = min(end_pos, partition_idx_end)
if intersection_start < intersection_end:
crop_start = intersection_start - partition_idx_start
crop_end = intersection_end - partition_idx_start
qv = source_array.query(attrs=("external_ids",), coords=True)
partition_external_ids = qv[partition_id : partition_id + 1][
"external_ids"
][0][crop_start:crop_end]
if external_ids is None:
external_ids = partition_external_ids
else:
external_ids = np.concatenate(
(external_ids, partition_external_ids)
)
partition_idx_start = partition_idx_end
i += 1
return external_ids
elif external_ids_type == "U64BIN":
vfs = tiledb.VFS()
read_size = end_pos - start_pos
read_offset = start_pos + 8
with vfs.open(external_ids_uri, "rb") as f:
f.seek(read_offset)
return np.reshape(
np.frombuffer(
f.read(read_size),
count=read_size,
dtype=np.uint64,
).astype(np.uint64),
(read_size),
)
def read_additions(
updates_uri: str,
config: Optional[Mapping[str, Any]] = None,
verbose: bool = False,
trace_id: Optional[str] = None,
) -> (np.ndarray, np.array):
if updates_uri is None:
return None, None
logger = setup(config, verbose)
logger.debug("Reading additions vectors")
with tiledb.open(
updates_uri,
mode="r",
timestamp=(previous_ingestion_timestamp, index_timestamp),
) as updates_array:
q = updates_array.query(attrs=("vector",), coords=True)
data = q[:]
additions_filter = [len(item) > 0 for item in data["vector"]]
filtered_vectors = data["vector"][additions_filter]
if len(filtered_vectors) == 0:
return None, None
else:
return (
np.vstack(data["vector"][additions_filter]),
data["external_id"][additions_filter],
)
def read_updated_ids(
updates_uri: str,
config: Optional[Mapping[str, Any]] = None,
verbose: bool = False,
trace_id: Optional[str] = None,
) -> np.array:
if updates_uri is None:
return np.array([], np.uint64)
logger = setup(config, verbose)
logger.debug("Reading updated vector ids")
with tiledb.open(
updates_uri,
mode="r",
timestamp=(previous_ingestion_timestamp, index_timestamp),
) as updates_array:
q = updates_array.query(attrs=("vector",), coords=True)
data = q[:]
return data["external_id"]
def read_input_vectors(
source_uri: str,
source_type: str,
vector_type: np.dtype,
dimensions: int,
start_pos: int,
end_pos: int,
config: Optional[Mapping[str, Any]] = None,
verbose: bool = False,
trace_id: Optional[str] = None,
) -> np.ndarray:
logger = setup(config, verbose)
logger.debug(
"Reading input vectors start_pos: %i, end_pos: %i", start_pos, end_pos
)
if source_type == "TILEDB_ARRAY":
with tiledb.open(
source_uri, mode="r", timestamp=index_timestamp
) as src_array:
src_array_schema = src_array.schema
return np.transpose(
src_array[0:dimensions, start_pos:end_pos][
src_array_schema.attr(0).name
]
).copy(order="C")
if source_type == "TILEDB_SPARSE_ARRAY":
from scipy.sparse import coo_matrix
with tiledb.open(
source_uri, mode="r", timestamp=index_timestamp
) as src_array:
src_array_schema = src_array.schema
data = src_array[start_pos:end_pos, 0:dimensions]
return coo_matrix(
(
data[src_array_schema.attr(0).name],
(
data[src_array_schema.domain.dim(0).name] - start_pos,
data[src_array_schema.domain.dim(1).name],
),
)
).toarray()
elif source_type == "TILEDB_PARTITIONED_ARRAY":
with tiledb.open(
source_uri, "r", timestamp=index_timestamp, config=config
) as source_array:
q = source_array.query(attrs=("vectors_shape",), coords=True)
nonempty_object_array_domain = source_array.nonempty_domain()
partitions = q[
nonempty_object_array_domain[0][0] : nonempty_object_array_domain[
0
][1]
+ 1
]
partition_idx_start = 0
partition_idx_end = 0
i = 0
vectors = None
for partition_shape in partitions["vectors_shape"]:
partition_id = partitions["partition_id"][i]
partition_idx_end += partition_shape[0]
intersection_start = max(start_pos, partition_idx_start)
intersection_end = min(end_pos, partition_idx_end)
if intersection_start < intersection_end:
crop_start = intersection_start - partition_idx_start
crop_end = intersection_end - partition_idx_start
qv = source_array.query(attrs=("vectors",), coords=True)
partition_vectors = np.reshape(
qv[partition_id : partition_id + 1]["vectors"][0],
partition_shape,
)[crop_start:crop_end]
if vectors is None:
vectors = partition_vectors
else:
vectors = np.concatenate((vectors, partition_vectors))
partition_idx_start = partition_idx_end
i += 1
return vectors
elif source_type == "U8BIN":
vfs = tiledb.VFS()
read_size = end_pos - start_pos
read_offset = start_pos * dimensions + 8
with vfs.open(source_uri, "rb") as f:
f.seek(read_offset)
return np.reshape(
np.frombuffer(