@@ -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,
9094class HNSWFilterWrapper_c : public hnswlib ::BaseFilterFunctor
9195{
9296public:
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
99104private:
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
105123class HNSWDist_c
@@ -211,30 +229,72 @@ class HNSWIndex_c : public HNSWDist_c, public HNSWIndex_i
211229public:
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
219237private:
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
227248HNSWIndex_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+
238298static 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
640703public:
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
654717public:
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
718811bool 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+
768915void 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
780937public:
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 ;
0 commit comments