Skip to content

Commit 5f1b59b

Browse files
committed
feat: added support for multiple knn vectors per document per attribute
1 parent dbc40aa commit 5f1b59b

5 files changed

Lines changed: 215 additions & 37 deletions

File tree

cmake/GetHNSW.cmake

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1414
# See the License for the specific language governing permissions and
1515
# limitations under the License.
16-
set ( HNSW_GITHUB "https://github.com/manticoresoftware/hnswlib/archive/091f3dd.zip" )
16+
set ( HNSW_GITHUB "https://github.com/manticoresoftware/hnswlib/archive/1e1bffe.zip" )
1717
set ( HNSW_BUNDLEZIP "${LIBS_BUNDLE}/hnswlib-0.7.0.tar.gz" )
1818

1919
cmake_minimum_required ( VERSION 3.17 FATAL_ERROR )

knn/knn.cpp

Lines changed: 187 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ static void LoadSettings ( IndexSettings_t & tSettings, FileReader_c & tReader,
4141

4242
tSettings.m_iHNSWM = tReader.Read_uint32();
4343
tSettings.m_iHNSWEFConstruction = tReader.Read_uint32();
44+
45+
if ( uVersion>=4 )
46+
tSettings.m_bMulti = !!tReader.Read_uint32();
4447
}
4548

4649

@@ -70,6 +73,7 @@ static void SaveSettings ( const IndexSettings_t & tSettings, FileWriter_c & tWr
7073
tWriter.Write_uint32 ( (int)tSettings.m_eQuantization );
7174
tWriter.Write_uint32 ( tSettings.m_iHNSWM );
7275
tWriter.Write_uint32 ( tSettings.m_iHNSWEFConstruction );
76+
tWriter.Write_uint32 ( tSettings.m_bMulti ? 1 : 0 );
7377
}
7478

7579

@@ -90,16 +94,30 @@ static void SaveQuantizationSettings ( const QuantizationSettings_t & tSettings,
9094
class HNSWFilterWrapper_c : public hnswlib::BaseFilterFunctor
9195
{
9296
public:
93-
HNSWFilterWrapper_c ( KNNFilter_i * pFilter ) : m_pFilter ( pFilter ) {}
97+
HNSWFilterWrapper_c ( KNNFilter_i * pFilter, const uint32_t * pVidToRowid = nullptr ) : m_pFilter ( pFilter ), m_pVidToRowid ( pVidToRowid ) {}
9498
virtual ~HNSWFilterWrapper_c() = default;
9599

96-
bool operator() ( hnswlib::labeltype id ) override { return m_pFilter->IsAllowed ( (uint32_t)id ); }
97-
long long getFilterCount() const override { return (long long)m_pFilter->GetFilterCount(); }
100+
bool operator() ( hnswlib::labeltype id ) override { return m_pFilter->IsAllowed ( m_pVidToRowid ? m_pVidToRowid[id] : (uint32_t)id ); }
101+
long long getFilterCount() const override;
102+
void SetVectorsPerDoc ( float fVectorsPerDoc ) { m_fVectorsPerDoc = fVectorsPerDoc; }
98103

99104
private:
100-
KNNFilter_i * m_pFilter = nullptr;
105+
KNNFilter_i * m_pFilter = nullptr;
106+
const uint32_t * m_pVidToRowid = nullptr;
107+
float m_fVectorsPerDoc = 1.0f;
101108
};
102109

110+
111+
long long HNSWFilterWrapper_c::getFilterCount() const
112+
{
113+
int64_t iCount = m_pFilter->GetFilterCount();
114+
if ( iCount<0 )
115+
return -1; // unknown cardinality, scaling is meaningless
116+
117+
// hnswlib weighs this against vectors; the daemon's estimate counts documents, hence the scale
118+
return (long long)( iCount*m_fVectorsPerDoc );
119+
}
120+
103121
/////////////////////////////////////////////////////////////////////
104122

105123
class HNSWDist_c
@@ -211,30 +229,72 @@ class HNSWIndex_c : public HNSWDist_c, public HNSWIndex_i
211229
public:
212230
HNSWIndex_c ( const std::string & sName, int64_t iNumElements, const knn::IndexSettings_t & tSettings, const QuantizationSettings_t & tQuantSettings, ScalarQuantizer_i * pQuantizer );
213231

214-
bool Load ( FileReader_c & tReader, std::string & sError ) override { return m_pAlg->loadIndex ( tReader, m_pSpace.get(), sError ); }
232+
bool Load ( FileReader_c & tReader, std::string & sError ) override;
215233
const std::string & GetName() const override { return m_sName; }
216234
void Search ( std::vector<DocDist_t> & dResults, const Span_T<float> & dData, int64_t iResults, int iEf, std::vector<uint8_t> & dQuantized, int64_t * pDistanceComputations = nullptr, KNNFilter_i * pFilter = nullptr, HNSWTerminationPolicy_e ePolicy = HNSWTerminationPolicy_e::NONE ) const override;
217-
bool ShouldUseFullscan ( int64_t iResults, int iEf, int64_t iFilterCount ) const override { return m_pAlg->shouldBypassHnswForFilteredSearch ( iResults, (long long)iFilterCount, iEf ); }
235+
bool ShouldUseFullscan ( int64_t iResults, int iEf, int64_t iFilterCount ) const override;
218236

219237
private:
220238
std::string m_sName;
221239
std::unique_ptr<hnswlib::HierarchicalNSW<float>> m_pAlg;
222240
std::unique_ptr<ScalarQuantizer_i> m_pQuantizer;
223241
DistFuncId_e m_eDistFuncId = DistFuncId_e::NONE;
242+
bool m_bMulti = false;
243+
std::vector<uint32_t> m_dVidToRowid; // [vector id] -> rowid. Empty in scalar mode
244+
float m_fVectorsPerDoc = 1.0f; // average vectors per document, used to convert the daemon's document-space filter estimates into the vector space for the hnsw
224245
};
225246

226247

227248
HNSWIndex_c::HNSWIndex_c ( const std::string & sName, int64_t iNumElements, const knn::IndexSettings_t & tSettings, const QuantizationSettings_t & tQuantSettings, ScalarQuantizer_i * pQuantizer )
228249
: HNSWDist_c ( tSettings.m_iDims, tSettings.m_eHNSWSimilarity, tSettings.m_eQuantization, false )
229250
, m_sName ( sName )
230251
, m_pQuantizer ( pQuantizer )
252+
, m_bMulti ( tSettings.m_bMulti )
231253
{
232254
m_pSpace->SetQuantizationSettings(*pQuantizer);
233255
m_eDistFuncId = m_pSpace->GetDistFuncId();
234256
m_pAlg = std::make_unique<hnswlib::HierarchicalNSW<float>>( m_pSpace.get(), iNumElements, tSettings.m_iHNSWM, tSettings.m_iHNSWEFConstruction );
235257
}
236258

237259

260+
bool HNSWIndex_c::Load ( FileReader_c & tReader, std::string & sError )
261+
{
262+
if ( m_bMulti )
263+
{
264+
const uint64_t uNumVectors = tReader.Read_uint64();
265+
m_dVidToRowid.resize ( (size_t)uNumVectors );
266+
for ( auto & i : m_dVidToRowid )
267+
i = tReader.Read_uint32();
268+
}
269+
270+
if ( !m_pAlg->loadIndex ( tReader, m_pSpace.get(), sError ) )
271+
return false;
272+
273+
assert ( !m_bMulti || m_dVidToRowid.size()==m_pAlg->cur_element_count );
274+
275+
if ( !m_dVidToRowid.empty() )
276+
{
277+
// graph needs the map because it needs to collect one result per doc (group)
278+
m_pAlg->setGroupMap ( m_dVidToRowid.data() );
279+
280+
const int64_t iDocs = (int64_t)m_dVidToRowid.back() + 1;
281+
m_fVectorsPerDoc = float ( (double)m_dVidToRowid.size() / (double)iDocs );
282+
}
283+
284+
return true;
285+
}
286+
287+
288+
bool HNSWIndex_c::ShouldUseFullscan ( int64_t iResults, int iEf, int64_t iFilterCount ) const
289+
{
290+
// iFilterCount is a doc estimate; need to conver it to vectors first
291+
if ( iFilterCount>=0 )
292+
iFilterCount = (int64_t)( iFilterCount*m_fVectorsPerDoc );
293+
294+
return m_pAlg->shouldBypassHnswForFilteredSearch ( iResults, (long long)iFilterCount, iEf );
295+
}
296+
297+
238298
static void ExtractResults ( std::vector<std::pair<float, hnswlib::labeltype>> && dRaw, std::vector<DocDist_t> & dResults )
239299
{
240300
dResults.resize(0);
@@ -470,7 +530,10 @@ void HNSWIndex_c::Search ( std::vector<DocDist_t> & dResults, const Span_T<float
470530

471531
std::unique_ptr<HNSWFilterWrapper_c> pFilterWrapper;
472532
if ( pFilter )
473-
pFilterWrapper = std::make_unique<HNSWFilterWrapper_c>(pFilter);
533+
{
534+
pFilterWrapper = std::make_unique<HNSWFilterWrapper_c> ( pFilter, m_dVidToRowid.empty() ? nullptr : m_dVidToRowid.data() );
535+
pFilterWrapper->SetVectorsPerDoc ( m_fVectorsPerDoc );
536+
}
474537

475538
size_t iSearchEf = iEf;
476539
long iBeforeDistanceComputations = 0;
@@ -561,7 +624,7 @@ bool KNN_c::Load ( const std::string & sFilename, std::string & sError )
561624
return false;
562625

563626
uint32_t uVersion = tReader.Read_uint32();
564-
if ( uVersion < 2 )
627+
if ( uVersion < 2 || uVersion > STORAGE_VERSION )
565628
{
566629
sError = FormatStr ( "Unable to load KNN index: %s is v.%d, binary is v.%d", sFilename.c_str(), uVersion, STORAGE_VERSION );
567630
return false;
@@ -640,7 +703,7 @@ class HNSWIndexBuilder_i
640703
public:
641704
virtual ~HNSWIndexBuilder_i() = default;
642705

643-
virtual void Train ( const util::Span_T<float> & dData ) = 0;
706+
virtual void Train ( uint32_t uRowID, const util::Span_T<float> & dData ) = 0;
644707
virtual bool FinalizeTraining ( std::string & sError ) = 0;
645708
virtual bool AddDoc ( uint32_t uRowID, const util::Span_T<float> & dData, BuildContext_t & tBuildCtx, std::string & sError ) = 0;
646709
virtual void Save ( FileWriter_c & tWriter ) = 0;
@@ -654,7 +717,7 @@ class HNSWIndexBuilder_c : public HNSWIndexBuilder_i, public HNSWDist_c
654717
public:
655718
HNSWIndexBuilder_c ( const AttrWithSettings_t & tAttr, int64_t iNumElements, ScalarQuantizer_i * pQuantizer );
656719

657-
void Train ( const util::Span_T<float> & dData ) override;
720+
void Train ( uint32_t uRowID, const util::Span_T<float> & dData ) override;
658721
bool FinalizeTraining ( std::string & sError ) override;
659722
bool AddDoc ( uint32_t uRowID, const util::Span_T<float> & dData, BuildContext_t & tBuildCtx, std::string & sError ) override;
660723
void Save ( FileWriter_c & tWriter ) override;
@@ -665,14 +728,20 @@ class HNSWIndexBuilder_c : public HNSWIndexBuilder_i, public HNSWDist_c
665728
using AddPoint_fn = void (*) ( hnswlib::HierarchicalNSW<float> &, const void *, uint32_t );
666729

667730
template <typename DistFn>
668-
static void AddPointTyped ( hnswlib::HierarchicalNSW<float> & tAlg, const void * pVec, uint32_t uRowID ) { tAlg.template addPoint<DistFn, false> ( pVec, (size_t)uRowID, -1 ); }
669-
static void AddPointFallback ( hnswlib::HierarchicalNSW<float> & tAlg, const void * pVec, uint32_t uRowID ) { tAlg.addPoint ( pVec, (size_t)uRowID ); }
731+
static void AddPointTyped ( hnswlib::HierarchicalNSW<float> & tAlg, const void * pVec, uint32_t uVecID ) { tAlg.template addPoint<DistFn, false> ( pVec, (size_t)uVecID, -1 ); }
732+
static void AddPointFallback ( hnswlib::HierarchicalNSW<float> & tAlg, const void * pVec, uint32_t uVecID ) { tAlg.addPoint ( pVec, (size_t)uVecID ); }
670733
AddPoint_fn SelectAddPointFn() const;
671734

735+
bool AddVector ( uint32_t uVecID, const util::Span_T<float> & dVec, BuildContext_t & tBuildCtx );
736+
672737
AttrWithSettings_t m_tAttr;
673738
std::unique_ptr<ScalarQuantizer_i> m_pQuantizer;
674739
std::unique_ptr<hnswlib::HierarchicalNSW<float>> m_pAlg;
675740
AddPoint_fn m_fnAddPoint = AddPointFallback;
741+
742+
bool m_bCountOverflow = false; // a row held more vectors than a 32-bit id can address
743+
std::vector<uint32_t> m_dCounts; // [rowid] -> num vectors in that row
744+
std::vector<int64_t> m_dBase; // [rowid] -> first vector id of that row
676745
};
677746

678747

@@ -703,20 +772,69 @@ HNSWIndexBuilder_c::HNSWIndexBuilder_c ( const AttrWithSettings_t & tAttr, int64
703772
, m_tAttr ( tAttr )
704773
, m_pQuantizer ( pQuantizer )
705774
{
706-
m_pAlg = std::make_unique<hnswlib::HierarchicalNSW<float>>( m_pSpace.get(), iNumElements, m_tAttr.m_iHNSWM, m_tAttr.m_iHNSWEFConstruction );
775+
// we don't know total number of vectors in multi mode, so we can't allocate the graph yet - do it in FinalizeTraining()
776+
if ( m_tAttr.m_bMulti )
777+
m_dCounts.resize ( (size_t)iNumElements, 0 );
778+
else
779+
m_pAlg = std::make_unique<hnswlib::HierarchicalNSW<float>>( m_pSpace.get(), iNumElements, m_tAttr.m_iHNSWM, m_tAttr.m_iHNSWEFConstruction );
780+
707781
m_fnAddPoint = SelectAddPointFn();
708782
}
709783

710784

711-
void HNSWIndexBuilder_c::Train ( const util::Span_T<float> & dData )
785+
void HNSWIndexBuilder_c::Train ( uint32_t uRowID, const util::Span_T<float> & dData )
712786
{
787+
if ( m_tAttr.m_bMulti )
788+
{
789+
assert ( uRowID<m_dCounts.size() );
790+
assert ( m_tAttr.m_iDims>0 && !( dData.size() % (size_t)m_tAttr.m_iDims ) );
791+
792+
const size_t uCount = dData.size() / (size_t)m_tAttr.m_iDims;
793+
if ( uCount>(size_t)UINT32_MAX )
794+
m_bCountOverflow = true;
795+
796+
m_dCounts[uRowID] = (uint32_t)uCount;
797+
798+
// train the quantizer per vector slot
799+
if ( m_pQuantizer )
800+
for ( size_t i = 0; i < dData.size(); i += (size_t)m_tAttr.m_iDims )
801+
m_pQuantizer->Train ( { dData.data()+i, (size_t)m_tAttr.m_iDims } );
802+
803+
return;
804+
}
805+
713806
if ( m_pQuantizer )
714807
m_pQuantizer->Train(dData);
715808
}
716809

717810

718811
bool HNSWIndexBuilder_c::FinalizeTraining ( std::string & sError )
719812
{
813+
if ( m_tAttr.m_bMulti )
814+
{
815+
int64_t iTotal = 0;
816+
m_dBase.resize ( m_dCounts.size()+1 );
817+
for ( size_t i = 0; i < m_dCounts.size(); i++ )
818+
{
819+
m_dBase[i] = iTotal;
820+
iTotal += m_dCounts[i];
821+
}
822+
823+
m_dBase[m_dCounts.size()] = iTotal;
824+
825+
if ( m_bCountOverflow || iTotal > (int64_t)UINT32_MAX )
826+
{
827+
sError = FormatStr ( "HNSW error: index '%s' needs %lld vectors but at most %u are addressable per chunk; store fewer vectors per document, or split the table so chunks stay smaller", m_tAttr.m_sName.c_str(), (long long)iTotal, (unsigned)UINT32_MAX );
828+
return false;
829+
}
830+
831+
m_pAlg = std::make_unique<hnswlib::HierarchicalNSW<float>>( m_pSpace.get(), iTotal, m_tAttr.m_iHNSWM, m_tAttr.m_iHNSWEFConstruction );
832+
833+
// nothing to encode if there are no vectors at all; skip sizing the quantizer temp buffer
834+
if ( m_pQuantizer && iTotal )
835+
m_pQuantizer->SetTotalVectors(iTotal);
836+
}
837+
720838
if ( !m_pQuantizer )
721839
return true;
722840

@@ -731,45 +849,84 @@ bool HNSWIndexBuilder_c::FinalizeTraining ( std::string & sError )
731849
}
732850

733851

734-
bool HNSWIndexBuilder_c::AddDoc ( uint32_t uRowID, const util::Span_T<float> & dData, BuildContext_t & tBuildCtx, std::string & sError )
852+
bool HNSWIndexBuilder_c::AddVector ( uint32_t uVecID, const util::Span_T<float> & dVec, BuildContext_t & tBuildCtx )
735853
{
736-
if ( dData.size()!=(size_t)m_tAttr.m_iDims )
737-
{
738-
sError = FormatStr ( "HNSW error: data has %llu values, index '%s' needs %d values", dData.size(), m_tAttr.m_sName.c_str(), m_tAttr.m_iDims );
739-
return false;
740-
}
741-
742-
assert ( !m_pQuantizer || m_pQuantizer->IsFinalized() );
743-
744-
Span_T<float> dToAdd = dData;
854+
Span_T<float> dToAdd = dVec;
745855
if ( m_tAttr.m_eHNSWSimilarity==HNSWSimilarity_e::COSINE )
746856
{
747-
tBuildCtx.m_dNormalized.resize ( dData.size() );
748-
memcpy ( tBuildCtx.m_dNormalized.data(), dData.data(), dData.size()*sizeof(dData[0] ) );
857+
// PER VECTOR. Normalizing a whole multi-vector row as one long vector would scale every slot
858+
// by the wrong magnitude and silently corrupt every cosine distance in the index.
859+
tBuildCtx.m_dNormalized.resize ( dVec.size() );
860+
memcpy ( tBuildCtx.m_dNormalized.data(), dVec.data(), dVec.size()*sizeof(dVec[0]) );
749861
VecNormalize ( tBuildCtx.m_dNormalized );
750862
dToAdd = tBuildCtx.m_dNormalized;
751863
}
752864

753865
const void * pVec = nullptr;
754866
if ( m_pQuantizer )
755867
{
756-
m_pQuantizer->Encode ( uRowID, dToAdd, tBuildCtx.m_dQuantized, tBuildCtx.m_dQuantizedForQuery );
868+
m_pQuantizer->Encode ( uVecID, dToAdd, tBuildCtx.m_dQuantized, tBuildCtx.m_dQuantizedForQuery );
757869
pVec = (void*)tBuildCtx.m_dQuantized.data();
758870
}
759871
else
760872
pVec = (void*)dToAdd.data();
761873

762-
m_fnAddPoint ( *m_pAlg, pVec, uRowID );
763-
874+
m_fnAddPoint ( *m_pAlg, pVec, uVecID );
764875
return true;
765876
}
766877

767878

879+
bool HNSWIndexBuilder_c::AddDoc ( uint32_t uRowID, const util::Span_T<float> & dData, BuildContext_t & tBuildCtx, std::string & sError )
880+
{
881+
assert ( !m_pQuantizer || m_pQuantizer->IsFinalized() );
882+
883+
const size_t uDims = (size_t)m_tAttr.m_iDims;
884+
885+
if ( m_tAttr.m_bMulti )
886+
{
887+
if ( dData.size() % uDims )
888+
{
889+
sError = FormatStr ( "HNSW error: data has %llu values, index '%s' needs a multiple of %d", dData.size(), m_tAttr.m_sName.c_str(), m_tAttr.m_iDims );
890+
return false;
891+
}
892+
893+
const size_t uCount = dData.size() / uDims;
894+
assert ( uRowID+1<m_dBase.size() );
895+
assert ( m_dBase[uRowID] + (int64_t)uCount == m_dBase[uRowID+1] ); // Train() saw the same row
896+
897+
const int64_t iBase = m_dBase[uRowID];
898+
for ( size_t i = 0; i < uCount; i++ )
899+
if ( !AddVector ( (uint32_t)( iBase + (int64_t)i ), { dData.data() + i*uDims, uDims }, tBuildCtx ) )
900+
return false;
901+
902+
return true;
903+
}
904+
905+
if ( dData.size()!=uDims )
906+
{
907+
sError = FormatStr ( "HNSW error: data has %llu values, index '%s' needs %d values", dData.size(), m_tAttr.m_sName.c_str(), m_tAttr.m_iDims );
908+
return false;
909+
}
910+
911+
return AddVector ( uRowID, dData, tBuildCtx );
912+
}
913+
914+
768915
void HNSWIndexBuilder_c::Save ( FileWriter_c & tWriter )
769916
{
770917
if ( m_pQuantizer )
771918
m_pQuantizer->FinalizeEncoding();
772919

920+
if ( m_tAttr.m_bMulti )
921+
{
922+
const int64_t iTotal = m_dBase.empty() ? 0 : m_dBase.back();
923+
tWriter.Write_uint64 ( (uint64_t)iTotal );
924+
925+
for ( size_t uRowID = 0; uRowID < m_dCounts.size(); uRowID++ )
926+
for ( uint32_t i = 0; i < m_dCounts[uRowID]; i++ )
927+
tWriter.Write_uint32 ( (uint32_t)uRowID );
928+
}
929+
773930
m_pAlg->saveIndex(tWriter);
774931
}
775932

@@ -780,7 +937,7 @@ class HNSWBuilder_c : public Builder_i
780937
public:
781938
HNSWBuilder_c ( const Schema_t & tSchema, int64_t iNumElements, const std::string & sTmpFilename );
782939

783-
void Train ( int iAttr, uint32_t uRowID, const util::Span_T<float> & dData ) override { m_dIndexes[iAttr]->Train(dData); }
940+
void Train ( int iAttr, uint32_t uRowID, const util::Span_T<float> & dData ) override { m_dIndexes[iAttr]->Train ( uRowID, dData ); }
784941
bool SetAttr ( int iAttr, uint32_t uRowID, const util::Span_T<float> & dData, BuildContext_t & tBuildCtx ) override { return m_dIndexes[iAttr]->AddDoc ( uRowID, dData, tBuildCtx, tBuildCtx.m_sError ); }
785942
bool FinalizeTraining ( std::string & sError ) override;
786943
bool Save ( const std::string & sFilename, size_t tBufferSize, std::string & sError ) override;

knn/knn.h

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@
2626
namespace knn
2727
{
2828

29-
static const int LIB_VERSION = 16;
30-
static const uint32_t STORAGE_VERSION = 3;
29+
static const int LIB_VERSION = 17;
30+
static const uint32_t STORAGE_VERSION = 4;
3131

3232
enum class HNSWSimilarity_e
3333
{
@@ -52,6 +52,7 @@ struct IndexSettings_t
5252
Quantization_e m_eQuantization = Quantization_e::NONE;
5353
int m_iHNSWM = 16;
5454
int m_iHNSWEFConstruction = 200;
55+
bool m_bMulti = false;
5556
};
5657

5758
struct ModelSettings_t

0 commit comments

Comments
 (0)