diff --git a/src/VecSim/algorithms/brute_force/brute_force.h b/src/VecSim/algorithms/brute_force/brute_force.h index 7222b3f4d..fecf3fc42 100644 --- a/src/VecSim/algorithms/brute_force/brute_force.h +++ b/src/VecSim/algorithms/brute_force/brute_force.h @@ -31,7 +31,6 @@ template class BruteForceIndex : public VecSimIndexAbstract { protected: vecsim_stl::vector idToLabelMapping; - RawDataContainer *vectors; idType count; public: @@ -41,7 +40,9 @@ class BruteForceIndex : public VecSimIndexAbstract { size_t indexSize() const override; size_t indexCapacity() const override; std::unique_ptr getVectorsIterator() const; - DataType *getDataByInternalId(idType id) const { return (DataType *)vectors->getElement(id); } + DataType *getDataByInternalId(idType id) const { + return (DataType *)this->vectors->getElement(id); + } VecSimQueryReply *topKQuery(const void *queryBlob, size_t k, VecSimQueryParams *queryParams) const override; VecSimQueryReply *rangeQuery(const void *queryBlob, double radius, @@ -54,7 +55,7 @@ class BruteForceIndex : public VecSimIndexAbstract { bool preferAdHocSearch(size_t subsetSize, size_t k, bool initial_check) const override; labelType getVectorLabel(idType id) const { return idToLabelMapping.at(id); } - const RawDataContainer *getVectorsContainer() const { return vectors; } + const RawDataContainer *getVectorsContainer() const { return this->vectors; } const labelType getLabelByInternalId(idType internal_id) const { return idToLabelMapping.at(internal_id); @@ -71,7 +72,7 @@ class BruteForceIndex : public VecSimIndexAbstract { // without duplicates in tiered index). Caller should hold the flat buffer lock for read. virtual vecsim_stl::set getLabelsSet() const = 0; - virtual ~BruteForceIndex() { delete vectors; } + virtual ~BruteForceIndex() = default; #ifdef BUILD_TESTS /** * @brief Used for testing - store vector(s) data associated with a given label. This function @@ -147,8 +148,6 @@ BruteForceIndex::BruteForceIndex( : VecSimIndexAbstract(abstractInitParams, components), idToLabelMapping(this->allocator), count(0) { assert(VecSimType_sizeof(this->vecType) == sizeof(DataType)); - vectors = new (this->allocator) - DataBlocksContainer(this->blockSize, this->dataSize, this->allocator, this->alignment); } /******************** Implementation **************/ @@ -164,7 +163,7 @@ void BruteForceIndex::appendVector(const void *vector_data, growByBlock(); } // add vector data to vector raw data container - vectors->addElement(processed_blob.get(), id); + this->vectors->addElement(processed_blob.get(), id); // add label to idToLabelMapping setVectorLabel(id, label); @@ -193,10 +192,10 @@ void BruteForceIndex::removeVector(idType id_to_delete) { replaceIdOfLabel(last_idx_label, id_to_delete, last_idx); // Put data of last vector inplace of the deleted vector. - const char *last_vector_data = vectors->getElement(last_idx); - vectors->updateElement(id_to_delete, last_vector_data); + const char *last_vector_data = this->vectors->getElement(last_idx); + this->vectors->updateElement(id_to_delete, last_vector_data); } - vectors->removeElement(last_idx); + this->vectors->removeElement(last_idx); // If we reached to a multiply of a block size, we can reduce meta data structures size. if (this->count % this->blockSize == 0) { @@ -217,7 +216,7 @@ size_t BruteForceIndex::indexCapacity() const { template std::unique_ptr BruteForceIndex::getVectorsIterator() const { - return vectors->getIterator(); + return this->vectors->getIterator(); } template @@ -240,7 +239,7 @@ BruteForceIndex::topKQuery(const void *queryBlob, size_t k, getNewMaxPriorityQueue(); // For vector, compute its scores and update the Top candidates max heap - auto vectors_it = vectors->getIterator(); + auto vectors_it = this->vectors->getIterator(); idType curr_id = 0; while (auto *vector = vectors_it->next()) { if (VECSIM_TIMEOUT(timeoutCtx)) { @@ -285,7 +284,7 @@ BruteForceIndex::rangeQuery(const void *queryBlob, double ra getNewResultsContainer(10); // Use 10 as the initial capacity for the dynamic array. DistType radius_ = DistType(radius); - auto vectors_it = vectors->getIterator(); + auto vectors_it = this->vectors->getIterator(); idType curr_id = 0; const void *processed_query = processed_query_ptr.get(); while (vectors_it->hasNext()) { diff --git a/src/VecSim/algorithms/hnsw/hnsw.h b/src/VecSim/algorithms/hnsw/hnsw.h index 2e67641b9..f4a9ef235 100644 --- a/src/VecSim/algorithms/hnsw/hnsw.h +++ b/src/VecSim/algorithms/hnsw/hnsw.h @@ -12,6 +12,8 @@ #include "VecSim/utils/vecsim_stl.h" #include "VecSim/utils/vec_utils.h" #include "VecSim/containers/data_block.h" +#include "VecSim/containers/raw_data_container_interface.h" +#include "VecSim/containers/data_blocks_container.h" #include "VecSim/containers/vecsim_results_container.h" #include "VecSim/query_result_definitions.h" #include "VecSim/vec_sim_common.h" @@ -110,7 +112,6 @@ class HNSWIndex : public VecSimIndexAbstract, size_t maxLevel; // this is the top level of the entry point's element // Index data - vecsim_stl::vector vectorBlocks; vecsim_stl::vector graphDataBlocks; vecsim_stl::vector idToMetaData; @@ -182,7 +183,7 @@ class HNSWIndex : public VecSimIndexAbstract, void replaceEntryPoint(); void SwapLastIdWithDeletedId(idType element_internal_id, ElementGraphData *last_element, - void *last_element_data); + const void *last_element_data); /** Add vector functions */ // Protected internal function that implements generic single vector insertion. @@ -384,7 +385,7 @@ labelType HNSWIndex::getEntryPointLabel() const { template const char *HNSWIndex::getDataByInternalId(idType internal_id) const { - return vectorBlocks[internal_id / this->blockSize].getElement(internal_id % this->blockSize); + return this->vectors->getElement(internal_id); } template @@ -1130,7 +1131,7 @@ void HNSWIndex::replaceEntryPoint() { template void HNSWIndex::SwapLastIdWithDeletedId(idType element_internal_id, ElementGraphData *last_element, - void *last_element_data) { + const void *last_element_data) { // Swap label - this is relevant when the last element's label exists (it is not marked as // deleted). if (!isMarkedDeleted(curElementCount)) { @@ -1305,12 +1306,6 @@ void HNSWIndex::resizeIndexCommon(size_t new_max_elements) { template void HNSWIndex::growByBlock() { size_t new_max_elements = maxElements + this->blockSize; - - // Validations - assert(vectorBlocks.size() == graphDataBlocks.size()); - assert(vectorBlocks.empty() || vectorBlocks.back().getLength() == this->blockSize); - - vectorBlocks.emplace_back(this->blockSize, this->dataSize, this->allocator, this->alignment); graphDataBlocks.emplace_back(this->blockSize, this->elementGraphDataSize, this->allocator); resizeIndexCommon(new_max_elements); @@ -1320,13 +1315,6 @@ template void HNSWIndex::shrinkByBlock() { assert(maxElements >= this->blockSize); size_t new_max_elements = maxElements - this->blockSize; - - // Validations - assert(vectorBlocks.size() == graphDataBlocks.size()); - assert(!vectorBlocks.empty()); - assert(vectorBlocks.back().getLength() == 0); - - vectorBlocks.pop_back(); graphDataBlocks.pop_back(); resizeIndexCommon(new_max_elements); @@ -1599,9 +1587,8 @@ HNSWIndex::HNSWIndex(const HNSWParams *params, const IndexComponents &components, size_t random_seed) : VecSimIndexAbstract(abstractInitParams, components), - VecSimIndexTombstone(), maxElements(0), vectorBlocks(this->allocator), - graphDataBlocks(this->allocator), idToMetaData(this->allocator), - visitedNodesHandlerPool(0, this->allocator) { + VecSimIndexTombstone(), maxElements(0), graphDataBlocks(this->allocator), + idToMetaData(this->allocator), visitedNodesHandlerPool(0, this->allocator) { M = params->M ? params->M : HNSW_DEFAULT_M; M0 = M * 2; @@ -1673,8 +1660,7 @@ void HNSWIndex::removeAndSwap(idType internalId) { // Get the last element's metadata and data. // If we are deleting the last element, we already destroyed it's metadata. - DataBlock &last_vector_block = vectorBlocks.back(); - auto last_element_data = last_vector_block.removeAndFetchLastElement(); + auto *last_element_data = getDataByInternalId(curElementCount); DataBlock &last_gd_block = graphDataBlocks.back(); auto last_element = (ElementGraphData *)last_gd_block.removeAndFetchLastElement(); @@ -1685,6 +1671,7 @@ void HNSWIndex::removeAndSwap(idType internalId) { // If we need to free a complete block and there is at least one block between the // capacity and the size. + this->vectors->removeElement(curElementCount); if (curElementCount % this->blockSize == 0) { shrinkByBlock(); } @@ -1793,16 +1780,13 @@ HNSWAddVectorState HNSWIndex::storeNewElement(labelType labe if (indexSize() > indexCapacity()) { growByBlock(); } else if (state.newElementId % this->blockSize == 0) { - // If we had an initial capacity, we might have to allocate new blocks for the data and - // meta-data. - this->vectorBlocks.emplace_back(this->blockSize, this->dataSize, this->allocator, - this->alignment); + // If we had an initial capacity, we might have to allocate new blocks for the graph data. this->graphDataBlocks.emplace_back(this->blockSize, this->elementGraphDataSize, this->allocator); } // Insert the new element to the data block - this->vectorBlocks.back().addElement(vector_data); + this->vectors->addElement(vector_data, state.newElementId); this->graphDataBlocks.back().addElement(cur_egd); // We mark id as in process *before* we set it in the label lookup, so that IN_PROCESS flag is // set when checking if label . diff --git a/src/VecSim/algorithms/hnsw/hnsw_serializer.h b/src/VecSim/algorithms/hnsw/hnsw_serializer.h index ba792f981..31bcd47fe 100644 --- a/src/VecSim/algorithms/hnsw/hnsw_serializer.h +++ b/src/VecSim/algorithms/hnsw/hnsw_serializer.h @@ -6,8 +6,8 @@ HNSWIndex::HNSWIndex(std::ifstream &input, const HNSWParams const IndexComponents &components, Serializer::EncodingVersion version) : VecSimIndexAbstract(abstractInitParams, components), Serializer(version), - epsilon(params->epsilon), vectorBlocks(this->allocator), graphDataBlocks(this->allocator), - idToMetaData(this->allocator), visitedNodesHandlerPool(0, this->allocator) { + epsilon(params->epsilon), graphDataBlocks(this->allocator), idToMetaData(this->allocator), + visitedNodesHandlerPool(0, this->allocator) { this->restoreIndexFields(input); this->fieldsValidation(); @@ -23,7 +23,6 @@ HNSWIndex::HNSWIndex(std::ifstream &input, const HNSWParams this->visitedNodesHandlerPool.resize(maxElements); size_t initial_vector_size = maxElements / this->blockSize; - vectorBlocks.reserve(initial_vector_size); graphDataBlocks.reserve(initial_vector_size); } @@ -167,29 +166,16 @@ void HNSWIndex::restoreGraph(std::ifstream &input, EncodingV setVectorId(label, id); } - // Get number of blocks - unsigned int num_blocks = 0; - readBinaryPOD(input, num_blocks); - this->vectorBlocks.reserve(num_blocks); - this->graphDataBlocks.reserve(num_blocks); - - // Get data blocks - for (size_t i = 0; i < num_blocks; i++) { - this->vectorBlocks.emplace_back(this->blockSize, this->dataSize, this->allocator, - this->alignment); - unsigned int block_len = 0; - readBinaryPOD(input, block_len); - for (size_t j = 0; j < block_len; j++) { - auto cur_vec = this->getAllocator()->allocate_unique(this->dataSize); - input.read(static_cast(cur_vec.get()), this->dataSize); - this->vectorBlocks.back().addElement(cur_vec.get()); - } - } + // Todo: create vector data container and load the stored data based on the index storage params + // when other storage types will be available. + dynamic_cast(this->vectors) + ->restoreBlocks(input, this->curElementCount, m_version); // Get graph data blocks ElementGraphData *cur_egt; auto tmpData = this->getAllocator()->allocate_unique(this->elementGraphDataSize); size_t toplevel = 0; + size_t num_blocks = dynamic_cast(this->vectors)->numBlocks(); for (size_t i = 0; i < num_blocks; i++) { this->graphDataBlocks.emplace_back(this->blockSize, this->elementGraphDataSize, this->allocator); @@ -283,22 +269,10 @@ void HNSWIndex::saveGraph(std::ofstream &output) const { writeBinaryPOD(output, flags); } - // Save number of blocks - unsigned int num_blocks = this->vectorBlocks.size(); - writeBinaryPOD(output, num_blocks); - - // Save data blocks - for (size_t i = 0; i < num_blocks; i++) { - auto &block = this->vectorBlocks[i]; - unsigned int block_len = block.getLength(); - writeBinaryPOD(output, block_len); - for (size_t j = 0; j < block_len; j++) { - output.write(block.getElement(j), this->dataSize); - } - } + this->vectors->saveVectorsData(output); // Save graph data blocks - for (size_t i = 0; i < num_blocks; i++) { + for (size_t i = 0; i < this->graphDataBlocks.size(); i++) { auto &block = this->graphDataBlocks[i]; unsigned int block_len = block.getLength(); writeBinaryPOD(output, block_len); diff --git a/src/VecSim/containers/data_blocks_container.cpp b/src/VecSim/containers/data_blocks_container.cpp index 94963c23c..2f4ccbc3e 100644 --- a/src/VecSim/containers/data_blocks_container.cpp +++ b/src/VecSim/containers/data_blocks_container.cpp @@ -1,4 +1,6 @@ #include "data_blocks_container.h" +#include "VecSim/utils/serializer.h" +#include DataBlocksContainer::DataBlocksContainer(size_t blockSize, size_t elementBytesCount, std::shared_ptr allocator, @@ -10,6 +12,8 @@ DataBlocksContainer::~DataBlocksContainer() = default; size_t DataBlocksContainer::size() const { return element_count; } +size_t DataBlocksContainer::capacity() const { return blocks.capacity(); } + size_t DataBlocksContainer::blockSize() const { return block_size; } size_t DataBlocksContainer::elementByteCount() const { return element_bytes_count; } @@ -51,6 +55,59 @@ std::unique_ptr DataBlocksContainer::getIterator() c return std::make_unique(*this); } +#ifdef BUILD_TESTS +void DataBlocksContainer::saveVectorsData(std::ostream &output) const { + // Save data blocks + for (size_t i = 0; i < this->numBlocks(); i++) { + auto &block = this->blocks[i]; + unsigned int block_len = block.getLength(); + for (size_t j = 0; j < block_len; j++) { + output.write(block.getElement(j), this->element_bytes_count); + } + } +} + +void DataBlocksContainer::restoreBlocks(std::istream &input, size_t num_vectors, + Serializer::EncodingVersion version) { + + // Get number of blocks + unsigned int num_blocks = 0; + if (version == Serializer::EncodingVersion_V3) { + // In V3, the number of blocks is serialized, so we need to read it from the file. + Serializer::readBinaryPOD(input, num_blocks); + } else { + // Otherwise, calculate the number of blocks based on the number of vectors. + num_blocks = std::ceil((float)num_vectors / this->block_size); + } + this->blocks.reserve(num_blocks); + + // Get data blocks + for (size_t i = 0; i < num_blocks; i++) { + this->blocks.emplace_back(this->block_size, this->element_bytes_count, this->allocator, + this->alignment); + unsigned int block_len = 0; + if (version == Serializer::EncodingVersion_V3) { + // In V3, the length of each block is serialized, so we need to read it from the file. + Serializer::readBinaryPOD(input, block_len); + } else { + size_t vectors_left = num_vectors - this->element_count; + block_len = vectors_left > this->block_size ? this->block_size : vectors_left; + } + for (size_t j = 0; j < block_len; j++) { + auto cur_vec = this->getAllocator()->allocate_unique(this->element_bytes_count); + input.read(static_cast(cur_vec.get()), + (std::streamsize)this->element_bytes_count); + this->blocks.back().addElement(cur_vec.get()); + this->element_count++; + } + } +} + +void DataBlocksContainer::shrinkToFit() { this->blocks.shrink_to_fit(); } + +size_t DataBlocksContainer::numBlocks() const { return this->blocks.size(); } + +#endif /********************************** Iterator API ************************************************/ DataBlocksContainer::Iterator::Iterator(const DataBlocksContainer &container_) diff --git a/src/VecSim/containers/data_blocks_container.h b/src/VecSim/containers/data_blocks_container.h index e274f89dd..692f663fd 100644 --- a/src/VecSim/containers/data_blocks_container.h +++ b/src/VecSim/containers/data_blocks_container.h @@ -3,6 +3,7 @@ #include "data_block.h" #include "raw_data_container_interface.h" #include "VecSim/memory/vecsim_malloc.h" +#include "VecSim/utils/serializer.h" #include "VecSim/utils/vecsim_stl.h" class DataBlocksContainer : public VecsimBaseObject, public RawDataContainer { @@ -20,6 +21,8 @@ class DataBlocksContainer : public VecsimBaseObject, public RawDataContainer { size_t size() const override; + size_t capacity() const; + size_t blockSize() const; size_t elementByteCount() const; @@ -34,6 +37,15 @@ class DataBlocksContainer : public VecsimBaseObject, public RawDataContainer { std::unique_ptr getIterator() const override; +#ifdef BUILD_TESTS + void saveVectorsData(std::ostream &output) const override; + // Use that in deserialization when file was created with old version (v3) that serialized + // the blocks themselves and not just thw raw vector data. + void restoreBlocks(std::istream &input, size_t num_vectors, Serializer::EncodingVersion); + void shrinkToFit(); + size_t numBlocks() const; +#endif + class Iterator : public RawDataContainer::Iterator { size_t cur_id; const char *cur_element; diff --git a/src/VecSim/containers/raw_data_container_interface.h b/src/VecSim/containers/raw_data_container_interface.h index cd3d80130..2be992bde 100644 --- a/src/VecSim/containers/raw_data_container_interface.h +++ b/src/VecSim/containers/raw_data_container_interface.h @@ -56,4 +56,11 @@ struct RawDataContainer { * Create a new iterator. Should be freed by the iterator's destroctor. */ virtual std::unique_ptr getIterator() const = 0; + +#ifdef BUILD_TESTS + /** + * Save the raw data of all elements in the container to the output stream. + */ + virtual void saveVectorsData(std::ostream &output) const = 0; +#endif }; diff --git a/src/VecSim/index_factories/hnsw_factory.cpp b/src/VecSim/index_factories/hnsw_factory.cpp index 58f7091f6..dbb5843a8 100644 --- a/src/VecSim/index_factories/hnsw_factory.cpp +++ b/src/VecSim/index_factories/hnsw_factory.cpp @@ -115,6 +115,8 @@ size_t EstimateInitialSize(const HNSWParams *params, bool is_normalized) { est += EstimateComponentsMemory(params->metric, is_normalized); est += EstimateInitialSize_ChooseMultiOrSingle(params->multi); } + est += sizeof(DataBlocksContainer) + allocations_overhead; + return est; } diff --git a/src/VecSim/utils/serializer.cpp b/src/VecSim/utils/serializer.cpp index 1faba0e8a..acabe99d3 100644 --- a/src/VecSim/utils/serializer.cpp +++ b/src/VecSim/utils/serializer.cpp @@ -6,8 +6,8 @@ // Persist index into a file in the specified location. void Serializer::saveIndex(const std::string &location) { - // Serializing with V3. - EncodingVersion version = EncodingVersion_V3; + // Serializing with the latest version. + EncodingVersion version = EncodingVersion_V4; std::ofstream output(location, std::ios::binary); writeBinaryPOD(output, version); @@ -31,6 +31,5 @@ Serializer::EncodingVersion Serializer::ReadVersion(std::ifstream &input) { throw std::runtime_error("Cannot load index: bad encoding version: " + std::to_string(version)); } - return version; } diff --git a/src/VecSim/utils/serializer.h b/src/VecSim/utils/serializer.h index 67cf2cf4e..a141f03c3 100644 --- a/src/VecSim/utils/serializer.h +++ b/src/VecSim/utils/serializer.h @@ -9,14 +9,17 @@ class Serializer { typedef enum EncodingVersion { EncodingVersion_DEPRECATED = 2, // Last deprecated version EncodingVersion_V3, + EncodingVersion_V4, EncodingVersion_INVALID, // This should always be last. } EncodingVersion; - Serializer(EncodingVersion version = EncodingVersion_V3) : m_version(version) {} + Serializer(EncodingVersion version = EncodingVersion_V4) : m_version(version) {} // Persist index into a file in the specified location with V3 encoding routine. void saveIndex(const std::string &location); + EncodingVersion getVersion() const { return m_version; } + static EncodingVersion ReadVersion(std::ifstream &input); // Helper functions for serializing the index. diff --git a/src/VecSim/vec_sim_index.h b/src/VecSim/vec_sim_index.h index 08599951b..0bc60ebbc 100644 --- a/src/VecSim/vec_sim_index.h +++ b/src/VecSim/vec_sim_index.h @@ -11,11 +11,13 @@ #include #include "VecSim/memory/vecsim_base.h" #include "VecSim/utils/vec_utils.h" -#include "VecSim/utils/alignment.h" #include "VecSim/spaces/spaces.h" #include "VecSim/spaces/computer/calculator.h" #include "VecSim/spaces/computer/preprocessor_container.h" #include "info_iterator_struct.h" +#include "containers/data_blocks_container.h" +#include "containers/raw_data_container_interface.h" + #include #include @@ -70,12 +72,12 @@ struct VecSimIndexAbstract : public VecSimIndexInterface { // resizing) IndexCalculatorInterface *indexCalculator; // Distance calculator. PreprocessorsContainerAbstract *preprocessors; // Stroage and query preprocessors. - // TODO: remove alignment once datablock is implemented in HNSW - unsigned char alignment; // Alignment hint to allocate vectors with. mutable VecSearchMode lastMode; // The last search mode in RediSearch (used for debug/testing). bool isMulti; // Determines if the index should multi-index or not. void *logCallbackCtx; // Context for the log callback. + RawDataContainer *vectors; // The raw vectors data container. + /** * @brief Get the common info object * @@ -105,9 +107,12 @@ struct VecSimIndexAbstract : public VecSimIndexInterface { dataSize(dim * VecSimType_sizeof(vecType)), metric(params.metric), blockSize(params.blockSize ? params.blockSize : DEFAULT_BLOCK_SIZE), indexCalculator(components.indexCalculator), preprocessors(components.preprocessors), - alignment(preprocessors->getAlignment()), lastMode(EMPTY_MODE), isMulti(params.multi), - logCallbackCtx(params.logCtx), normalize_func(spaces::GetNormalizeFunc()) { + lastMode(EMPTY_MODE), isMulti(params.multi), logCallbackCtx(params.logCtx), + normalize_func(spaces::GetNormalizeFunc()) { + assert(VecSimType_sizeof(vecType)); + this->vectors = new (this->allocator) DataBlocksContainer( + this->blockSize, this->dataSize, this->allocator, this->getAlignment()); } /** @@ -115,6 +120,7 @@ struct VecSimIndexAbstract : public VecSimIndexInterface { * */ virtual ~VecSimIndexAbstract() noexcept { + delete this->vectors; delete indexCalculator; delete preprocessors; } @@ -166,7 +172,7 @@ struct VecSimIndexAbstract : public VecSimIndexInterface { inline VecSimMetric getMetric() const { return metric; } inline size_t getDataSize() const { return dataSize; } inline size_t getBlockSize() const { return blockSize; } - inline auto getAlignment() const { return alignment; } + inline auto getAlignment() const { return this->preprocessors->getAlignment(); } virtual VecSimQueryReply *rangeQuery(const void *queryBlob, double radius, VecSimQueryParams *queryParams) const = 0; diff --git a/tests/unit/data/1k-d4-L2-M8-ef_c10_FLOAT32_multi_100labels.v3 b/tests/unit/data/1k-d4-L2-M8-ef_c10_FLOAT32_multi_100labels.v3 new file mode 100644 index 000000000..e5251df25 Binary files /dev/null and b/tests/unit/data/1k-d4-L2-M8-ef_c10_FLOAT32_multi_100labels.v3 differ diff --git a/tests/unit/data/1k-d4-L2-M8-ef_c10_FLOAT32_single.v3 b/tests/unit/data/1k-d4-L2-M8-ef_c10_FLOAT32_single.v3 new file mode 100644 index 000000000..10a6d65e8 Binary files /dev/null and b/tests/unit/data/1k-d4-L2-M8-ef_c10_FLOAT32_single.v3 differ diff --git a/tests/unit/test_allocator.cpp b/tests/unit/test_allocator.cpp index 4eb389260..42689b6eb 100644 --- a/tests/unit/test_allocator.cpp +++ b/tests/unit/test_allocator.cpp @@ -94,7 +94,6 @@ TYPED_TEST(IndexAllocatorTest, test_bf_index_block_size_1) { .type = TypeParam::get_index_type(), .dim = dim, .metric = VecSimMetric_IP, .blockSize = 1}; auto *bfIndex = dynamic_cast *>( BruteForceFactory::NewIndex(¶ms)); - bfIndex->alignment = 0; // Disable alignment for testing purposes. auto allocator = bfIndex->getAllocator(); TEST_DATA_T vec[128] = {}; uint64_t expectedAllocationSize = sizeof(VecSimAllocator); @@ -413,9 +412,10 @@ TYPED_TEST(IndexAllocatorTest, test_hnsw_reclaim_memory) { expected_mem_delta += (hnswIndex->labelLookup.bucket_count() - prev_bucket_count) * sizeof(size_t); // New blocks allocated - 1 aligned block for vectors and 1 unaligned block for graph data. - expected_mem_delta += 2 * (sizeof(DataBlock) + vecsimAllocationOverhead) + hnswIndex->alignment; + auto *data_blocks = dynamic_cast(hnswIndex->vectors); expected_mem_delta += - (hnswIndex->vectorBlocks.capacity() - hnswIndex->vectorBlocks.size()) * sizeof(DataBlock); + 2 * (sizeof(DataBlock) + vecsimAllocationOverhead) + hnswIndex->getAlignment(); + expected_mem_delta += (data_blocks->capacity() - data_blocks->numBlocks()) * sizeof(DataBlock); expected_mem_delta += (hnswIndex->graphDataBlocks.capacity() - hnswIndex->graphDataBlocks.size()) * sizeof(DataBlock); @@ -430,7 +430,7 @@ TYPED_TEST(IndexAllocatorTest, test_hnsw_reclaim_memory) { ASSERT_EQ(hnswIndex->checkIntegrity().unidirectional_connections, 0); size_t expected_allocation_size = initial_memory_size + accumulated_mem_delta; expected_allocation_size += - (hnswIndex->vectorBlocks.capacity() - hnswIndex->vectorBlocks.size()) * sizeof(DataBlock); + (data_blocks->capacity() - data_blocks->numBlocks()) * sizeof(DataBlock); expected_allocation_size += (hnswIndex->graphDataBlocks.capacity() - hnswIndex->graphDataBlocks.size()) * sizeof(DataBlock); @@ -447,9 +447,9 @@ TYPED_TEST(IndexAllocatorTest, test_hnsw_reclaim_memory) { // (STL unordered_map with hash table implementation), that leaves some empty buckets. size_t hash_table_memory = hnswIndex->labelLookup.bucket_count() * sizeof(size_t); // Data block vectors do not shrink on resize so extra memory is expected. - size_t block_vectors_memory = sizeof(DataBlock) * (hnswIndex->graphDataBlocks.capacity() + - hnswIndex->vectorBlocks.capacity()) + - 2 * vecsimAllocationOverhead; + size_t block_vectors_memory = + sizeof(DataBlock) * (hnswIndex->graphDataBlocks.capacity() + data_blocks->capacity()) + + 2 * vecsimAllocationOverhead; // Current memory should be back as it was initially. The label_lookup hash table is an // exception, since in some platforms, empty buckets remain even when the capacity is set to // zero, while in others the entire capacity reduced to zero (including the header). diff --git a/tests/unit/test_hnsw.cpp b/tests/unit/test_hnsw.cpp index f57a6d3e3..733f21432 100644 --- a/tests/unit/test_hnsw.cpp +++ b/tests/unit/test_hnsw.cpp @@ -1672,14 +1672,12 @@ TYPED_TEST(HNSWTest, HNSWSerializationCurrentVersion) { size_t M = 8; size_t ef = 10; double epsilon = 0.004; - size_t blockSize = 2; bool is_multi[] = {false, true}; - std::string multiToString[] = {"single", "multi_100labels_"}; + std::string multiToString[] = {"single", "multi_100labels"}; HNSWParams params{.type = TypeParam::get_index_type(), .dim = dim, .metric = VecSimMetric_L2, - .blockSize = blockSize, .M = M, .efConstruction = ef, .efRuntime = ef, @@ -1733,12 +1731,13 @@ TYPED_TEST(HNSWTest, HNSWSerializationCurrentVersion) { // Verify that the index was loaded as expected. ASSERT_TRUE(serialized_hnsw_index->checkIntegrity().valid_state); + ASSERT_EQ(serialized_hnsw_index->getVersion(), Serializer::EncodingVersion_V4); VecSimIndexInfo info2 = VecSimIndex_Info(serialized_index); ASSERT_EQ(info2.commonInfo.basicInfo.algo, VecSimAlgo_HNSWLIB); ASSERT_EQ(info2.hnswInfo.M, M); ASSERT_EQ(info2.commonInfo.basicInfo.isMulti, is_multi[i]); - ASSERT_EQ(info2.commonInfo.basicInfo.blockSize, blockSize); + ASSERT_EQ(info2.commonInfo.basicInfo.blockSize, DEFAULT_BLOCK_SIZE); ASSERT_EQ(info2.hnswInfo.efConstruction, ef); ASSERT_EQ(info2.hnswInfo.efRuntime, ef); ASSERT_EQ(info2.commonInfo.indexSize, n); @@ -1765,6 +1764,76 @@ TYPED_TEST(HNSWTest, HNSWSerializationCurrentVersion) { } } +TYPED_TEST(HNSWTest, HNSWSerializationV3) { + if (TypeParam::get_index_type() != VecSimType_FLOAT32) { + GTEST_SKIP(); + } + size_t dim = 4; + size_t n = 1001; + size_t n_labels[] = {n, 100}; + size_t M = 8; + size_t ef = 10; + double epsilon = 0.004; + size_t blockSize = 2; + bool is_multi[] = {false, true}; + std::string multiToString[] = {"single", "multi_100labels"}; + + HNSWParams params{.type = TypeParam::get_index_type(), + .dim = dim, + .metric = VecSimMetric_L2, + .blockSize = blockSize, + .M = M, + .efConstruction = ef, + .efRuntime = ef, + .epsilon = epsilon}; + + // Test for multi and single + + for (size_t i = 0; i < 2; ++i) { + // Set index type. + params.multi = is_multi[i]; + auto file_name = std::string(getenv("ROOT")) + "/tests/unit/data/1k-d4-L2-M8-ef_c10_" + + VecSimType_ToString(TypeParam::get_index_type()) + "_" + multiToString[i] + + ".v3"; + + // Load the index from the file. + VecSimIndex *serialized_index = HNSWFactory::NewIndex(file_name); + auto *serialized_hnsw_index = this->CastToHNSW(serialized_index); + + // Verify that the index was loaded as expected. + ASSERT_EQ(serialized_hnsw_index->getVersion(), Serializer::EncodingVersion_V3); + ASSERT_TRUE(serialized_hnsw_index->checkIntegrity().valid_state); + + VecSimIndexInfo info = VecSimIndex_Info(serialized_index); + ASSERT_EQ(info.commonInfo.basicInfo.algo, VecSimAlgo_HNSWLIB); + ASSERT_EQ(info.hnswInfo.M, M); + ASSERT_EQ(info.commonInfo.basicInfo.isMulti, is_multi[i]); + ASSERT_EQ(info.commonInfo.basicInfo.blockSize, blockSize); + ASSERT_EQ(info.hnswInfo.efConstruction, ef); + ASSERT_EQ(info.hnswInfo.efRuntime, ef); + ASSERT_EQ(info.commonInfo.indexSize, n); + ASSERT_EQ(info.commonInfo.basicInfo.metric, VecSimMetric_L2); + ASSERT_EQ(info.commonInfo.basicInfo.type, TypeParam::get_index_type()); + ASSERT_EQ(info.commonInfo.basicInfo.dim, dim); + ASSERT_EQ(info.commonInfo.indexLabelCount, n_labels[i]); + ASSERT_EQ(info.hnswInfo.epsilon, epsilon); + + // Check the functionality of the loaded index. + + // Add and delete vector + GenerateAndAddVector(serialized_index, dim, n); + + VecSimIndex_DeleteVector(serialized_index, 1); + + size_t n_per_label = n / n_labels[i]; + ASSERT_TRUE(serialized_hnsw_index->checkIntegrity().valid_state); + ASSERT_EQ(VecSimIndex_IndexSize(serialized_index), n + 1 - n_per_label); + + // Clean up. + VecSimIndex_Free(serialized_index); + } +} + TYPED_TEST(HNSWTest, markDelete) { size_t n = 100; size_t k = 11; diff --git a/tests/unit/test_hnsw_tiered.cpp b/tests/unit/test_hnsw_tiered.cpp index ee5fef7d1..198a8293a 100644 --- a/tests/unit/test_hnsw_tiered.cpp +++ b/tests/unit/test_hnsw_tiered.cpp @@ -1824,7 +1824,7 @@ TYPED_TEST(HNSWTieredIndexTest, swapJobBasic) { tiered_index->getHNSWIndex()->resizeLabelLookup(0); // Manually shrink the vectors so that memory would be as it was before we started inserting - tiered_index->getHNSWIndex()->vectorBlocks.shrink_to_fit(); + dynamic_cast(tiered_index->getHNSWIndex()->vectors)->shrinkToFit(); tiered_index->getHNSWIndex()->graphDataBlocks.shrink_to_fit(); tiered_index->getHNSWIndex()->visitedNodesHandlerPool.clearPool();