Repository: blevesearch/bleve
Pull request #1936 (2023-12-12 18:05:38)

MB-59102: Merging kNN hits from a distributed index with exact search hits
- Reverts commit https://github.com/blevesearch/bleve/pull/1910, which was an earlier attempt to address this issue.
- Implements the PreSearch Construct in Bleve alias search, enabling a preliminary query to collect metadata from all alias indexes before executing the main search query in MultiSearch. PreSearch gathers KNN results from all alias indexes, selecting the top K results. This facilitates the main Bleve Query to operate within the context of documents that matched the KNN query, ensuring seamless functionality of existing Bleve constructs such as Faceting, Sorting, Pagination, SearchAfter, and SearchBefore.
- Introduces the KNN Collector construct to merge and obtain accurate Top K results from multiple Zap Segments' KNN results.
- Enhances KNN Unit Tests for greater generality.
- Addresses an issue where errors generated within the Top N Document handler were being discarded.
- Resolves an issue where document matches failing to meet the SearchAfter clause weren't being returned to the pool.




Patch:
diff --git a/error.go b/error.go
index 7dd21194..30153eac 100644
--- a/error.go
+++ b/error.go
@@ -26,6 +26,7 @@ const (
 	ErrorUnknownIndexType
 	ErrorEmptyID
 	ErrorIndexReadInconsistency
+	ErrorPreSearchFailed
 )
 
 // Error represents a more strongly typed bleve error for detecting
@@ -47,4 +48,5 @@ var errorMessages = map[Error]string{
 	ErrorUnknownIndexType:       "unknown index type",
 	ErrorEmptyID:                "document ID cannot be empty",
 	ErrorIndexReadInconsistency: "index read inconsistency detected",
+	ErrorPreSearchFailed:        "index pre-search failed",
 }
diff --git a/go.mod b/go.mod
index aa84a441..b07ae5dc 100644
--- a/go.mod
+++ b/go.mod
@@ -23,7 +23,7 @@ require (
 	github.com/blevesearch/zapx/v13 v13.3.10
 	github.com/blevesearch/zapx/v14 v14.3.10
 	github.com/blevesearch/zapx/v15 v15.3.13
-	github.com/blevesearch/zapx/v16 v16.0.0
+	github.com/blevesearch/zapx/v16 v16.0.1-0.20231215153441-18737168eeaf
 	github.com/couchbase/moss v0.2.0
 	github.com/golang/protobuf v1.3.2
 	github.com/spf13/cobra v1.7.0
diff --git a/go.sum b/go.sum
index f5a6df39..b642ee70 100644
--- a/go.sum
+++ b/go.sum
@@ -43,8 +43,8 @@ github.com/blevesearch/zapx/v14 v14.3.10 h1:SG6xlsL+W6YjhX5N3aEiL/2tcWh3DO75Bnz7
 github.com/blevesearch/zapx/v14 v14.3.10/go.mod h1:qqyuR0u230jN1yMmE4FIAuCxmahRQEOehF78m6oTgns=
 github.com/blevesearch/zapx/v15 v15.3.13 h1:6EkfaZiPlAxqXz0neniq35my6S48QI94W/wyhnpDHHQ=
 github.com/blevesearch/zapx/v15 v15.3.13/go.mod h1:Turk/TNRKj9es7ZpKK95PS7f6D44Y7fAFy8F4LXQtGg=
-github.com/blevesearch/zapx/v16 v16.0.0 h1:M6VNBe7iexKWK/UPIHtng2bM93p6R45ihruwm82eh8w=
-github.com/blevesearch/zapx/v16 v16.0.0/go.mod h1:WIOW42mP6pIF9TyeUYG3u7KIhy0ml8Hnt3R5O925tjI=
+github.com/blevesearch/zapx/v16 v16.0.1-0.20231215153441-18737168eeaf h1:7+PlR8PP6if2xSJ5e+dZNIK0oSm04vSIyEfFjydYIBo=
+github.com/blevesearch/zapx/v16 v16.0.1-0.20231215153441-18737168eeaf/go.mod h1:WIOW42mP6pIF9TyeUYG3u7KIhy0ml8Hnt3R5O925tjI=
 github.com/couchbase/ghistogram v0.1.0 h1:b95QcQTCzjTUocDXp/uMgSNQi8oj1tGwnJ4bODWZnps=
 github.com/couchbase/ghistogram v0.1.0/go.mod h1:s1Jhy76zqfEecpNWJfWUiKZookAFaiGOEoyzgHt9i7k=
 github.com/couchbase/moss v0.2.0 h1:VCYrMzFwEryyhRSeI+/b3tRBSeTpi/8gn5Kf6dxqn+o=
diff --git a/index/scorch/optimize_knn.go b/index/scorch/optimize_knn.go
index 92b0bdd6..03e9cfdb 100644
--- a/index/scorch/optimize_knn.go
+++ b/index/scorch/optimize_knn.go
@@ -74,6 +74,12 @@ func (o *OptimizeVR) Finish() error {
 func (s *IndexSnapshotVectorReader) VectorOptimize(
 	octx index.VectorOptimizableContext) (index.VectorOptimizableContext, error) {
 
+	if s.snapshot.parent.segPlugin.Version() < VectorSeachSupportedSegmentVersion {
+		return nil, fmt.Errorf("vector search not supported for this index, "+
+			"index's segment version %v, supported segment version for vector search %v",
+			s.snapshot.parent.segPlugin.Version(), VectorSeachSupportedSegmentVersion)
+	}
+
 	if octx == nil {
 		octx = &OptimizeVR{snapshot: s.snapshot,
 			vrs: make(map[string][]*IndexSnapshotVectorReader),
diff --git a/index/scorch/snapshot_index_vr.go b/index/scorch/snapshot_index_vr.go
index 9c4de256..70f2d693 100644
--- a/index/scorch/snapshot_index_vr.go
+++ b/index/scorch/snapshot_index_vr.go
@@ -28,6 +28,8 @@ import (
 	segment_api "github.com/blevesearch/scorch_segment_api/v2"
 )
 
+const VectorSeachSupportedSegmentVersion = 16
+
 var reflectStaticSizeIndexSnapshotVectorReader int
 
 func init() {
diff --git a/index_alias_impl.go b/index_alias_impl.go
index 4942eb1a..28f20775 100644
--- a/index_alias_impl.go
+++ b/index_alias_impl.go
@@ -21,6 +21,7 @@ import (
 
 	"github.com/blevesearch/bleve/v2/mapping"
 	"github.com/blevesearch/bleve/v2/search"
+	"github.com/blevesearch/bleve/v2/search/query"
 	index "github.com/blevesearch/bleve_index_api"
 )
 
@@ -160,12 +161,58 @@ func (i *indexAliasImpl) SearchInContext(ctx context.Context, req *SearchRequest
 	if len(i.indexes) < 1 {
 		return nil, ErrorAliasEmpty
 	}
+	if _, ok := ctx.Value(search.PreSearchKey).(bool); ok {
+		// since presearchKey is set, it means that the request
+		// is being executed as part of a presearch, which
+		// indicates that this index alias is set as an Index
+		// in another alias, so we need to do a presearch search
+		// and NOT a real search
+		return PreSearchDataSearch(ctx, req, i.indexes...)
+	}
+
+	// at this point we know we are doing a real search
+	// either after a presearch is done, or directly
+	// on the alias
+
+	// check if request has preSearchData which would indicate that the
+	// request has already been preSearched and we can skip the
+	// preSearch step now, we call an optional function to
+	// redistribute the preSearchData to the individual indexes
+	// if necessary
+	var preSearchData []map[string]interface{}
+	if req.PreSearchData != nil {
+		if requestHasKNN(req) {
+			preSearchData = make([]map[string]interface{}, len(i.indexes))
+			for i := 0; i < len(preSearchData); i++ {
+				preSearchData[i] = make(map[string]interface{})
+			}
+			redistributeKNNPreSearchData(req, preSearchData)
+		}
+	}
 
 	// short circuit the simple case
 	if len(i.indexes) == 1 {
+		if preSearchData != nil {
+			req.PreSearchData = preSearchData[0]
+		}
 		return i.indexes[0].SearchInContext(ctx, req)
 	}
-	return MultiSearch(ctx, req, i.indexes...)
+
+	// at this stage we know we have multiple indexes
+	// check if preSearchData needs to be gathered from all indexes
+	// before executing the query
+	var err error
+	// only perform presearch if
+	//  - the request does not already have preSearchData
+	//  - the request requires presearch
+	if req.PreSearchData == nil && PreSearchRequired(ctx, req) {
+		preSearchData, err = PreSearch(ctx, req, i.indexes...)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	return MultiSearch(ctx, req, preSearchData, i.indexes...)
 }
 
 func (i *indexAliasImpl) Fields() ([]string, error) {
@@ -428,19 +475,136 @@ func (i *indexAliasImpl) Swap(in, out []Index) {
 // the actual final results.
 // Perhaps that part needs to be optional,
 // could be slower in remote usages.
-func createChildSearchRequest(req *SearchRequest) *SearchRequest {
-	return copySearchRequest(req)
+func createChildSearchRequest(req *SearchRequest, preSearchData map[string]interface{}) *SearchRequest {
+	return copySearchRequest(req, preSearchData)
 }
 
 type asyncSearchResult struct {
-	Name   string
-	Result *SearchResult
-	Err    error
+	Name     string
+	IndexNum int
+	Result   *SearchResult
+	Err      error
+}
+
+func PreSearchRequired(ctx context.Context, req *SearchRequest) bool {
+	return requestHasKNN(req)
+}
+
+func PreSearch(ctx context.Context, req *SearchRequest, indexes ...Index) ([]map[string]interface{}, error) {
+	// create a dummy request with a match none query
+	// since we only care about the preSearchData in PreSearch
+	dummyRequest := &SearchRequest{
+		Query: query.NewMatchNoneQuery(),
+	}
+	newCtx := context.WithValue(ctx, search.PreSearchKey, true)
+	if requestHasKNN(req) {
+		addKnnToDummyRequest(dummyRequest, req)
+	}
+	res, err := PreSearchDataSearch(newCtx, dummyRequest, indexes...)
+	if err != nil {
+		return nil, err
+	}
+	preSearchData, err := mergePreSearchData(req, res, len(indexes))
+	if err != nil {
+		return nil, err
+	}
+	return preSearchData, nil
+}
+
+func tagHitsWithIndexNum(sr *SearchResult, indexNum int) {
+	for _, hit := range sr.Hits {
+		hit.IndexId = append(hit.IndexId, indexNum)
+	}
+}
+
+func mergePreSearchData(req *SearchRequest, res *SearchResult, numIndexes int) ([]map[string]interface{}, error) {
+	mergedOut := make([]map[string]interface{}, numIndexes)
+	for i := 0; i < len(mergedOut); i++ {
+		mergedOut[i] = make(map[string]interface{})
+	}
+	if requestHasKNN(req) {
+		mergeKNNDocumentMatches(req, res.Hits, mergedOut)
+	}
+	return mergedOut, nil
+}
+
+func PreSearchDataSearch(ctx context.Context, req *SearchRequest, indexes ...Index) (*SearchResult, error) {
+	searchStart := time.Now()
+	asyncResults := make(chan *asyncSearchResult, len(indexes))
+
+	// run search on each index in separate go routine
+	var waitGroup sync.WaitGroup
+
+	var searchChildIndex = func(in Index, childReq *SearchRequest, idx int) {
+		rv := asyncSearchResult{Name: in.Name(), IndexNum: idx}
+		rv.Result, rv.Err = in.SearchInContext(ctx, childReq)
+		asyncResults <- &rv
+		waitGroup.Done()
+	}
+
+	waitGroup.Add(len(indexes))
+	for idx, in := range indexes {
+		go searchChildIndex(in, createChildSearchRequest(req, nil), idx)
+	}
+
+	// on another go routine, close after finished
+	go func() {
+		waitGroup.Wait()
+		close(asyncResults)
+	}()
+
+	var sr *SearchResult
+	indexErrors := make(map[string]error)
+
+	for asr := range asyncResults {
+		if asr.Err == nil {
+			if sr == nil {
+				// first result
+				sr = asr.Result
+				tagHitsWithIndexNum(sr, asr.IndexNum)
+			} else {
+				// merge with previous
+				tagHitsWithIndexNum(asr.Result, asr.IndexNum)
+				sr.Merge(asr.Result)
+			}
+		} else {
+			indexErrors[asr.Name] = asr.Err
+		}
+	}
+
+	// merge just concatenated all the hits
+	// now lets clean it up
+
+	// handle case where no results were successful
+	if sr == nil {
+		sr = &SearchResult{
+			Status: &SearchStatus{
+				Errors: make(map[string]error),
+			},
+		}
+	}
+
+	searchDuration := time.Since(searchStart)
+	sr.Took = searchDuration
+
+	// fix up errors
+	if len(indexErrors) > 0 {
+		if sr.Status.Errors == nil {
+			sr.Status.Errors = make(map[string]error)
+		}
+		for indexName, indexErr := range indexErrors {
+			sr.Status.Errors[indexName] = indexErr
+			sr.Status.Total++
+			sr.Status.Failed++
+		}
+	}
+
+	return sr, nil
 }
 
 // MultiSearch executes a SearchRequest across multiple Index objects,
 // then merges the results.  The indexes must honor any ctx deadline.
-func MultiSearch(ctx context.Context, req *SearchRequest, indexes ...Index) (*SearchResult, error) {
+func MultiSearch(ctx context.Context, req *SearchRequest, preSearchData []map[string]interface{}, indexes ...Index) (*SearchResult, error) {
 
 	searchStart := time.Now()
 	asyncResults := make(chan *asyncSearchResult, len(indexes))
@@ -452,10 +616,6 @@ func MultiSearch(ctx context.Context, req *SearchRequest, indexes ...Index) (*Se
 		req.SearchAfter = req.SearchBefore
 		req.SearchBefore = nil
 	}
-	originalSize := req.Size
-	if len(indexes) > 1 {
-		req.Size = adjustRequestSizeForKNN(req, len(indexes))
-	}
 
 	// run search on each index in separate go routine
 	var waitGroup sync.WaitGroup
@@ -468,8 +628,12 @@ func MultiSearch(ctx context.Context, req *SearchRequest, indexes ...Index) (*Se
 	}
 
 	waitGroup.Add(len(indexes))
-	for _, in := range indexes {
-		go searchChildIndex(in, createChildSearchRequest(req))
+	for idx, in := range indexes {
+		var md map[string]interface{}
+		if preSearchData != nil {
+			md = preSearchData[idx]
+		}
+		go searchChildIndex(in, createChildSearchRequest(req, md))
 	}
 
 	// on another go routine, close after finished
@@ -494,7 +658,6 @@ func MultiSearch(ctx context.Context, req *SearchRequest, indexes ...Index) (*Se
 			indexErrors[asr.Name] = asr.Err
 		}
 	}
-	req.Size = originalSize
 
 	// merge just concatenated all the hits
 	// now lets clean it up
@@ -508,10 +671,6 @@ func MultiSearch(ctx context.Context, req *SearchRequest, indexes ...Index) (*Se
 		}
 	}
 
-	if len(indexes) > 1 {
-		mergeKNNResults(req, sr)
-	}
-
 	sortFunc := req.SortFunc()
 	// sort all hits with the requested order
 	if len(req.Sort) > 0 {
diff --git a/index_impl.go b/index_impl.go
index 30a33ee6..7b9bfc90 100644
--- a/index_impl.go
+++ b/index_impl.go
@@ -433,6 +433,25 @@ func memNeededForSearch(req *SearchRequest,
 	return uint64(estimate)
 }
 
+func (i *indexImpl) preSearch(ctx context.Context, req *SearchRequest, reader index.IndexReader) (*SearchResult, error) {
+	var knnHits []*search.DocumentMatch
+	var err error
+	if requestHasKNN(req) {
+		knnHits, err = i.runKnnCollector(ctx, req, reader, true)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	return &SearchResult{
+		Status: &SearchStatus{
+			Total:      1,
+			Successful: 1,
+		},
+		Hits: knnHits,
+	}, nil
+}
+
 // SearchInContext executes a search request operation within the provided
 // Context. Returns a SearchResult object or an error.
 func (i *indexImpl) SearchInContext(ctx context.Context, req *SearchRequest) (sr *SearchResult, err error) {
@@ -445,6 +464,25 @@ func (i *indexImpl) SearchInContext(ctx context.Context, req *SearchRequest) (sr
 		return nil, ErrorIndexClosed
 	}
 
+	// open a reader for this search
+	indexReader, err := i.i.Reader()
+	if err != nil {
+		return nil, fmt.Errorf("error opening index reader %v", err)
+	}
+	defer func() {
+		if cerr := indexReader.Close(); err == nil && cerr != nil {
+			err = cerr
+		}
+	}()
+
+	if _, ok := ctx.Value(search.PreSearchKey).(bool); ok {
+		preSearchResult, err := i.preSearch(ctx, req, indexReader)
+		if err != nil {
+			return nil, err
+		}
+		return preSearchResult, nil
+	}
+
 	var reverseQueryExecution bool
 	if req.SearchBefore != nil {
 		reverseQueryExecution = true
@@ -460,16 +498,31 @@ func (i *indexImpl) SearchInContext(ctx context.Context, req *SearchRequest) (sr
 		coll = collector.NewTopNCollector(req.Size, req.From, req.Sort)
 	}
 
-	// open a reader for this search
-	indexReader, err := i.i.Reader()
-	if err != nil {
-		return nil, fmt.Errorf("error opening index reader %v", err)
+	var knnHits []*search.DocumentMatch
+	var ok bool
+	var skipKnnCollector bool
+	if req.PreSearchData != nil {
+		for k, v := range req.PreSearchData {
+			switch k {
+			case search.KnnPreSearchDataKey:
+				if v != nil {
+					knnHits, ok = v.([]*search.DocumentMatch)
+					if !ok {
+						return nil, fmt.Errorf("knn preSearchData must be of type []*search.DocumentMatch")
+					}
+				}
+				skipKnnCollector = true
+			}
+		}
 	}
-	defer func() {
-		if cerr := indexReader.Close(); err == nil && cerr != nil {
-			err = cerr
+	if !skipKnnCollector && requestHasKNN(req) {
+		knnHits, err = i.runKnnCollector(ctx, req, indexReader, false)
+		if err != nil {
+			return nil, err
 		}
-	}()
+	}
+
+	setKnnHitsInCollector(knnHits, req, coll)
 
 	// This callback and variable handles the tracking of bytes read
 	//  1. as part of creation of tfr and its Next() calls which is
@@ -496,14 +549,7 @@ func (i *indexImpl) SearchInContext(ctx context.Context, req *SearchRequest) (sr
 	ctx = context.WithValue(ctx, search.GeoBufferPoolCallbackKey,
 		search.GeoBufferPoolCallbackFunc(getBufferPool))
 
-	// Using a query to get results from KNN queries
-	// and the original query based on the KNN operator.
-	searchQuery, err := queryWithKNN(req)
-	if err != nil {
-		return nil, err
-	}
-
-	searcher, err := searchQuery.Searcher(ctx, indexReader, i.m, search.SearcherOptions{
+	searcher, err := req.Query.Searcher(ctx, indexReader, i.m, search.SearcherOptions{
 		Explain:            req.Explain,
 		IncludeTermVectors: req.IncludeLocations || req.Highlight != nil,
 		Score:              req.Score,
@@ -657,7 +703,6 @@ func (i *indexImpl) SearchInContext(ctx context.Context, req *SearchRequest) (sr
 		Took:     searchDuration,
 		Facets:   coll.FacetResults(),
 	}
-	mergeKNNResults(req, rv)
 	return rv, nil
 }
 
diff --git a/mapping/document.go b/mapping/document.go
index 9f5aea58..2a702f32 100644
--- a/mapping/document.go
+++ b/mapping/document.go
@@ -50,7 +50,8 @@ type DocumentMapping struct {
 	StructTagKey string `json:"struct_tag_key,omitempty"`
 }
 
-func (dm *DocumentMapping) Validate(cache *registry.Cache) error {
+func (dm *DocumentMapping) Validate(cache *registry.Cache,
+	parentName string, fieldAliasCtx map[string]*FieldMapping) error {
 	var err error
 	if dm.DefaultAnalyzer != "" {
 		_, err := cache.AnalyzerNamed(dm.DefaultAnalyzer)
@@ -58,8 +59,12 @@ func (dm *DocumentMapping) Validate(cache *registry.Cache) error {
 			return err
 		}
 	}
-	for _, property := range dm.Properties {
-		err = property.Validate(cache)
+	for propertyName, property := range dm.Properties {
+		newParent := propertyName
+		if parentName != "" {
+			newParent = fmt.Sprintf("%s.%s", parentName, propertyName)
+		}
+		err = property.Validate(cache, newParent, fieldAliasCtx)
 		if err != nil {
 			return err
 		}
@@ -78,21 +83,24 @@ func (dm *DocumentMapping) Validate(cache *registry.Cache) error {
 			}
 		}
 
-		err := validateFieldType(field.Type)
+		err := validateFieldMapping(field, parentName, fieldAliasCtx)
 		if err != nil {
 			return err
 		}
-
-		if field.Type == "vector" {
-			err := validateVectorField(field)
-			if err != nil {
-				return err
-			}
-		}
 	}
 	return nil
 }
 
+func validateFieldType(field *FieldMapping) error {
+	switch field.Type {
+	case "text", "datetime", "number", "boolean", "geopoint", "geoshape", "IP":
+		return nil
+	default:
+		return fmt.Errorf("field: '%s', unknown field type: '%s'",
+			field.Name, field.Type)
+	}
+}
+
 // analyzerNameForPath attempts to first find the field
 // described by this path, then returns the analyzer
 // configured for that field
diff --git a/mapping/index.go b/mapping/index.go
index 1c08bc58..171ee1a7 100644
--- a/mapping/index.go
+++ b/mapping/index.go
@@ -174,12 +174,14 @@ func (im *IndexMappingImpl) Validate() error {
 	if err != nil {
 		return err
 	}
-	err = im.DefaultMapping.Validate(im.cache)
+
+	fieldAliasCtx := make(map[string]*FieldMapping)
+	err = im.DefaultMapping.Validate(im.cache, "", fieldAliasCtx)
 	if err != nil {
 		return err
 	}
 	for _, docMapping := range im.TypeMapping {
-		err = docMapping.Validate(im.cache)
+		err = docMapping.Validate(im.cache, "", fieldAliasCtx)
 		if err != nil {
 			return err
 		}
diff --git a/mapping/mapping_no_vectors.go b/mapping/mapping_no_vectors.go
index f4987596..b5e033a6 100644
--- a/mapping/mapping_no_vectors.go
+++ b/mapping/mapping_no_vectors.go
@@ -17,8 +17,6 @@
 
 package mapping
 
-import "fmt"
-
 func NewVectorFieldMapping() *FieldMapping {
 	return nil
 }
@@ -31,16 +29,7 @@ func (fm *FieldMapping) processVector(propertyMightBeVector interface{},
 // -----------------------------------------------------------------------------
 // document validation functions
 
-func validateVectorField(fieldMapping *FieldMapping) error {
-	return nil
-}
-
-func validateFieldType(fieldType string) error {
-	switch fieldType {
-	case "text", "datetime", "number", "boolean", "geopoint", "geoshape", "IP":
-	default:
-		return fmt.Errorf("unknown field type: '%s'", fieldType)
-	}
-
-	return nil
+func validateFieldMapping(field *FieldMapping, parentName string,
+	fieldAliasCtx map[string]*FieldMapping) error {
+	return validateFieldType(field)
 }
diff --git a/mapping/mapping_vectors.go b/mapping/mapping_vectors.go
index 748c2eea..1e926826 100644
--- a/mapping/mapping_vectors.go
+++ b/mapping/mapping_vectors.go
@@ -26,6 +26,12 @@ import (
 	index "github.com/blevesearch/bleve_index_api"
 )
 
+// Min and Max allowed dimensions for a vector field
+const (
+	MinVectorDims = 1
+	MaxVectorDims = 2048
+)
+
 func NewVectorFieldMapping() *FieldMapping {
 	return &FieldMapping{
 		Type:         "vector",
@@ -37,56 +43,121 @@ func NewVectorFieldMapping() *FieldMapping {
 	}
 }
 
-func (fm *FieldMapping) processVector(propertyMightBeVector interface{},
-	pathString string, path []string, indexes []uint64, context *walkContext) {
-	propertyVal := reflect.ValueOf(propertyMightBeVector)
-	if !propertyVal.IsValid() {
-		return
+// validate and process a flat vector
+func processFlatVector(vecV reflect.Value, dims int) ([]float32, bool) {
+	if vecV.Len() != dims {
+		return nil, false
 	}
 
-	// Validating the length of the vector is required here, in order to
-	// help zapx in deciding the shape of the batch of vectors to be indexed.
-	if propertyVal.Kind() == reflect.Slice && propertyVal.Len() == fm.Dims {
-		vector := make([]float32, propertyVal.Len())
-		isVectorValid := true
-		for i := 0; i < propertyVal.Len(); i++ {
-			item := propertyVal.Index(i)
-			if item.CanInterface() {
-				itemVal := item.Interface()
-				itemFloat, ok := util.ExtractNumericValFloat32(itemVal)
-				if !ok {
-					isVectorValid = false
-					break
-				}
-				vector[i] = itemFloat
-			}
+	rv := make([]float32, dims)
+	for i := 0; i < vecV.Len(); i++ {
+		item := vecV.Index(i)
+		if !item.CanInterface() {
+			return nil, false
+		}
+		itemI := item.Interface()
+		itemFloat, ok := util.ExtractNumericValFloat32(itemI)
+		if !ok {
+			return nil, false
 		}
-		// Even if one of the vector elements is not a float32, we do not index
-		// this field and return silently
-		if !isVectorValid {
-			return
+		rv[i] = itemFloat
+	}
+
+	return rv, true
+}
+
+// validate and process a vector
+// max supported depth of nesting is 2 ([][]float32)
+func processVector(vecI interface{}, dims int) ([]float32, bool) {
+	vecV := reflect.ValueOf(vecI)
+	if !vecV.IsValid() || vecV.Kind() != reflect.Slice || vecV.Len() == 0 {
+		return nil, false
+	}
+
+	// Let's examine the first element (head) of the vector.
+	// If head is a slice, then vector is nested, otherwise flat.
+	head := vecV.Index(0)
+	if !head.CanInterface() {
+		return nil, false
+	}
+	headI := head.Interface()
+	headV := reflect.ValueOf(headI)
+	if !headV.IsValid() {
+		return nil, false
+	}
+	if headV.Kind() != reflect.Slice { // vector is flat
+		return processFlatVector(vecV, dims)
+	}
+
+	// # process nested vector
+
+	// pre-allocate memory for the flattened vector
+	// so that we can use copy() later
+	rv := make([]float32, dims*vecV.Len())
+
+	for i := 0; i < vecV.Len(); i++ {
+		subVec := vecV.Index(i)
+		if !subVec.CanInterface() {
+			return nil, false
+		}
+		subVecI := subVec.Interface()
+		subVecV := reflect.ValueOf(subVecI)
+		if !subVecV.IsValid() {
+			return nil, false
+		}
+
+		if subVecV.Kind() != reflect.Slice {
+			return nil, false
+		}
+
+		flatVector, ok := processFlatVector(subVecV, dims)
+		if !ok {
+			return nil, false
 		}
 
-		fieldName := getFieldName(pathString, path, fm)
-		options := fm.Options()
-		field := document.NewVectorFieldWithIndexingOptions(fieldName,
-			indexes, vector, fm.Dims, fm.Similarity, options)
-		context.doc.AddField(field)
+		copy(rv[i*dims:(i+1)*dims], flatVector)
+	}
+
+	return rv, true
+}
 
-		// "_all" composite field is not applicable for vector field
-		context.excludedFromAll = append(context.excludedFromAll, fieldName)
+func (fm *FieldMapping) processVector(propertyMightBeVector interface{},
+	pathString string, path []string, indexes []uint64, context *walkContext) {
+	vector, ok := processVector(propertyMightBeVector, fm.Dims)
+	// Don't add field to document if vector is invalid
+	if !ok {
+		return
 	}
+
+	fieldName := getFieldName(pathString, path, fm)
+	options := fm.Options()
+	field := document.NewVectorFieldWithIndexingOptions(fieldName,
+		indexes, vector, fm.Dims, fm.Similarity, options)
+	context.doc.AddField(field)
+
+	// "_all" composite field is not applicable for vector field
+	context.excludedFromAll = append(context.excludedFromAll, fieldName)
 }
 
 // -----------------------------------------------------------------------------
 // document validation functions
 
-func validateVectorField(field *FieldMapping) error {
-	if field.Dims <= 0 || field.Dims > 2048 {
-		return fmt.Errorf("invalid vector dimension,"+
-			" value should be in range (%d, %d)", 0, 2048)
+func validateFieldMapping(field *FieldMapping, parentName string,
+	fieldAliasCtx map[string]*FieldMapping) error {
+	switch field.Type {
+	case "vector":
+		return validateVectorFieldAlias(field, parentName, fieldAliasCtx)
+	default: // non-vector field
+		return validateFieldType(field)
 	}
+}
 
+func validateVectorFieldAlias(field *FieldMapping, parentName string,
+	fieldAliasCtx map[string]*FieldMapping) error {
+
+	if field.Name == "" {
+		field.Name = parentName
+	}
 	if field.Similarity == "" {
 		field.Similarity = index.DefaultSimilarityMetric
 	}
@@ -99,21 +170,40 @@ func validateVectorField(field *FieldMapping) error {
 	field.DocValues = false
 	field.SkipFreqNorm = true
 
+	// # If alias is present, validate the field options as per the alias
+	// note: reading from a nil map is safe
+	if fieldAlias, ok := fieldAliasCtx[field.Name]; ok {
+		if field.Dims != fieldAlias.Dims {
+			return fmt.Errorf("field: '%s', invalid alias "+
+				"(different dimensions %d and %d)", fieldAlias.Name, field.Dims,
+				fieldAlias.Dims)
+		}
+
+		if field.Similarity != fieldAlias.Similarity {
+			return fmt.Errorf("field: '%s', invalid alias "+
+				"(different similarity values %s and %s)", fieldAlias.Name,
+				field.Similarity, fieldAlias.Similarity)
+		}
+
+		return nil
+	}
+
+	// # Validate field options
+
+	if field.Dims < MinVectorDims || field.Dims > MaxVectorDims {
+		return fmt.Errorf("field: '%s', invalid vector dimension: %d,"+
+			" value should be in range (%d, %d)", field.Name, field.Dims,
+			MinVectorDims, MaxVectorDims)
+	}
+
 	if _, ok := index.SupportedSimilarityMetrics[field.Similarity]; !ok {
-		return fmt.Errorf("invalid similarity metric: '%s', "+
-			"valid metrics are: %+v", field.Similarity,
+		return fmt.Errorf("field: '%s', invalid similarity "+
+			"metric: '%s', valid metrics are: %+v", field.Name, field.Similarity,
 			reflect.ValueOf(index.SupportedSimilarityMetrics).MapKeys())
 	}
 
-	return nil
-}
-
-func validateFieldType(fieldType string) error {
-	switch fieldType {
-	case "text", "datetime", "number", "boolean", "geopoint", "geoshape",
-		"IP", "vector":
-	default:
-		return fmt.Errorf("unknown field type: '%s'", fieldType)
+	if fieldAliasCtx != nil { // writing to a nil map is unsafe
+		fieldAliasCtx[field.Name] = field
 	}
 
 	return nil
diff --git a/search/collector.go b/search/collector.go
index 38e34fe7..e81219e5 100644
--- a/search/collector.go
+++ b/search/collector.go
@@ -44,9 +44,15 @@ type MakeDocumentMatchHandlerKeyType string
 var MakeDocumentMatchHandlerKey = MakeDocumentMatchHandlerKeyType(
 	"MakeDocumentMatchHandlerKey")
 
+var MakeKNNDocumentMatchHandlerKey = MakeDocumentMatchHandlerKeyType(
+	"MakeKNNDocumentMatchHandlerKey")
+
 // MakeDocumentMatchHandler is an optional DocumentMatchHandler
 // builder function which the applications can pass to bleve.
 // These builder methods gives a DocumentMatchHandler function
 // to bleve, which it will invoke on every document matches.
 type MakeDocumentMatchHandler func(ctx *SearchContext) (
 	callback DocumentMatchHandler, loadID bool, err error)
+
+type MakeKNNDocumentMatchHandler func(ctx *SearchContext) (
+	callback DocumentMatchHandler, err error)
diff --git a/search/collector/knn.go b/search/collector/knn.go
new file mode 100644
index 00000000..e162aa6c
--- /dev/null
+++ b/search/collector/knn.go
@@ -0,0 +1,260 @@
+//  Copyright (c) 2023 Couchbase, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// 		http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//go:build vectors
+// +build vectors
+
+package collector
+
+import (
+	"context"
+	"time"
+
+	"github.com/blevesearch/bleve/v2/search"
+	index "github.com/blevesearch/bleve_index_api"
+)
+
+type collectStoreKNN struct {
+	internalHeaps []collectorStore
+	kValues       []int64
+	allHits       map[*search.DocumentMatch]struct{}
+	ejectedDocs   map[*search.DocumentMatch]struct{}
+}
+
+func newStoreKNN(internalHeaps []collectorStore, kValues []int64) *collectStoreKNN {
+	return &collectStoreKNN{
+		internalHeaps: internalHeaps,
+		kValues:       kValues,
+		ejectedDocs:   make(map[*search.DocumentMatch]struct{}),
+		allHits:       make(map[*search.DocumentMatch]struct{}),
+	}
+}
+
+// Adds a document to the collector store and returns the documents that were ejected
+// from the store. The documents that were ejected from the store are the ones that
+// were not in the top K documents for any of the heaps.
+// These document are put back into the pool document match pool in the KNN Collector.
+func (c *collectStoreKNN) AddDocument(doc *search.DocumentMatch) []*search.DocumentMatch {
+	for heapIdx := 0; heapIdx < len(c.internalHeaps); heapIdx++ {
+		if _, ok := doc.ScoreBreakdown[heapIdx]; !ok {
+			continue
+		}
+		ejectedDoc := c.internalHeaps[heapIdx].AddNotExceedingSize(doc, int(c.kValues[heapIdx]))
+		if ejectedDoc != nil {
+			delete(ejectedDoc.ScoreBreakdown, heapIdx)
+			c.ejectedDocs[ejectedDoc] = struct{}{}
+		}
+	}
+	var rv []*search.DocumentMatch
+	for doc := range c.ejectedDocs {
+		if len(doc.ScoreBreakdown) == 0 {
+			rv = append(rv, doc)
+			delete(c.allHits, doc)
+		}
+		// clear out the ejectedDocs map to reuse it in the next AddDocument call
+		delete(c.ejectedDocs, doc)
+	}
+	if len(doc.ScoreBreakdown) > 0 {
+		c.allHits[doc] = struct{}{}
+	}
+	return rv
+}
+
+func (c *collectStoreKNN) AllHits() map[*search.DocumentMatch]struct{} {
+	return c.allHits
+}
+
+func (c *collectStoreKNN) Final(fixup collectorFixup) (search.DocumentMatchCollection, error) {
+	size := len(c.allHits)
+	if size <= 0 {
+		return make(search.DocumentMatchCollection, 0), nil
+	}
+	rv := make(search.DocumentMatchCollection, size)
+	i := 0
+	for doc := range c.allHits {
+		err := fixup(doc)
+		if err != nil {
+			return nil, err
+		}
+		rv[i] = doc
+		i++
+	}
+	return rv, nil
+}
+
+func MakeKNNDocMatchHandler(ctx *search.SearchContext) (search.DocumentMatchHandler, error) {
+	var hc *KNNCollector
+	var ok bool
+	if hc, ok = ctx.Collector.(*KNNCollector); ok {
+		return func(d *search.DocumentMatch) error {
+			if d == nil {
+				return nil
+			}
+			toRelease := hc.knnStore.AddDocument(d)
+			for _, doc := range toRelease {
+				ctx.DocumentMatchPool.Put(doc)
+			}
+			return nil
+		}, nil
+	}
+	return nil, nil
+}
+
+func GetNewKNNCollectorStore(kArray []int64) *collectStoreKNN {
+	internalHeaps := make([]collectorStore, len(kArray))
+	for knnIdx, k := range kArray {
+		// TODO - Check if the datatype of k can be made into an int instead of int64
+		idx := knnIdx
+		internalHeaps[idx] = getOptimalCollectorStore(int(k), 0, func(i, j *search.DocumentMatch) int {
+			if i.ScoreBreakdown[idx] < j.ScoreBreakdown[idx] {
+				return 1
+			}
+			return -1
+		})
+	}
+	return newStoreKNN(internalHeaps, kArray)
+}
+
+// implements Collector interface
+type KNNCollector struct {
+	knnStore *collectStoreKNN
+	size     int
+	total    uint64
+	took     time.Duration
+	results  search.DocumentMatchCollection
+	maxScore float64
+}
+
+func NewKNNCollector(kArray []int64, size int64) *KNNCollector {
+	return &KNNCollector{
+		knnStore: GetNewKNNCollectorStore(kArray),
+		size:     int(size),
+	}
+}
+
+func (hc *KNNCollector) Collect(ctx context.Context, searcher search.Searcher, reader index.IndexReader) error {
+	startTime := time.Now()
+	var err error
+	var next *search.DocumentMatch
+
+	// pre-allocate enough space in the DocumentMatchPool
+	// unless the sum of K is too large, then cap it
+	// everything should still work, just allocates DocumentMatches on demand
+	backingSize := hc.size
+	if backingSize > PreAllocSizeSkipCap {
+		backingSize = PreAllocSizeSkipCap + 1
+	}
+	searchContext := &search.SearchContext{
+		DocumentMatchPool: search.NewDocumentMatchPool(backingSize+searcher.DocumentMatchPoolSize(), 0),
+		Collector:         hc,
+		IndexReader:       reader,
+	}
+
+	dmHandlerMakerKNN := MakeKNNDocMatchHandler
+	if cv := ctx.Value(search.MakeKNNDocumentMatchHandlerKey); cv != nil {
+		dmHandlerMakerKNN = cv.(search.MakeKNNDocumentMatchHandler)
+	}
+	// use the application given builder for making the custom document match
+	// handler and perform callbacks/invocations on the newly made handler.
+	dmHandler, err := dmHandlerMakerKNN(searchContext)
+	if err != nil {
+		return err
+	}
+	select {
+	case <-ctx.Done():
+		search.RecordSearchCost(ctx, search.AbortM, 0)
+		return ctx.Err()
+	default:
+		next, err = searcher.Next(searchContext)
+	}
+	for err == nil && next != nil {
+		if hc.total%CheckDoneEvery == 0 {
+			select {
+			case <-ctx.Done():
+				search.RecordSearchCost(ctx, search.AbortM, 0)
+				return ctx.Err()
+			default:
+			}
+		}
+		hc.total++
+
+		err = dmHandler(next)
+		if err != nil {
+			break
+		}
+
+		next, err = searcher.Next(searchContext)
+	}
+	if err != nil {
+		return err
+	}
+
+	// help finalize/flush the results in case
+	// of custom document match handlers.
+	err = dmHandler(nil)
+	if err != nil {
+		return err
+	}
+
+	// compute search duration
+	hc.took = time.Since(startTime)
+
+	// finalize actual results
+	err = hc.finalizeResults(reader)
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+func (hc *KNNCollector) finalizeResults(r index.IndexReader) error {
+	var err error
+	hc.results, err = hc.knnStore.Final(func(doc *search.DocumentMatch) error {
+		if doc.ID == "" {
+			// look up the id since we need it for lookup
+			var err error
+			doc.ID, err = r.ExternalID(doc.IndexInternalID)
+			if err != nil {
+				return err
+			}
+		}
+		return nil
+	})
+	return err
+}
+
+func (hc *KNNCollector) Results() search.DocumentMatchCollection {
+	return hc.results
+}
+
+func (hc *KNNCollector) Total() uint64 {
+	return hc.total
+}
+
+func (hc *KNNCollector) MaxScore() float64 {
+	return hc.maxScore
+}
+
+func (hc *KNNCollector) Took() time.Duration {
+	return hc.took
+}
+
+func (hc *KNNCollector) SetFacetsBuilder(facetsBuilder *search.FacetsBuilder) {
+	// facet unsupported for vector search
+}
+
+func (hc *KNNCollector) FacetResults() search.FacetResults {
+	// facet unsupported for vector search
+	return nil
+}
diff --git a/search/collector/topn.go b/search/collector/topn.go
index 270d5f92..27ef7d84 100644
--- a/search/collector/topn.go
+++ b/search/collector/topn.go
@@ -72,6 +72,10 @@ type TopNCollector struct {
 	updateFieldVisitor        index.DocValueVisitor
 	dvReader                  index.DocValueReader
 	searchAfter               *search.DocumentMatch
+
+	knnHits          map[string]*search.DocumentMatch
+	computeNewScore  func(d *search.DocumentMatch, knnHit *search.DocumentMatch) float64
+	isKnnOperatorAnd func() bool
 }
 
 // CheckDoneEvery controls how frequently we check the context deadline
@@ -110,6 +114,22 @@ func NewTopNCollectorAfter(size int, sort search.SortOrder, after []string) *Top
 func newTopNCollector(size int, skip int, sort search.SortOrder) *TopNCollector {
 	hc := &TopNCollector{size: size, skip: skip, sort: sort}
 
+	hc.store = getOptimalCollectorStore(size, skip, func(i, j *search.DocumentMatch) int {
+		return hc.sort.Compare(hc.cachedScoring, hc.cachedDesc, i, j)
+	})
+
+	// these lookups traverse an interface, so do once up-front
+	if sort.RequiresDocID() {
+		hc.needDocIds = true
+	}
+	hc.neededFields = sort.RequiredFields()
+	hc.cachedScoring = sort.CacheIsScore()
+	hc.cachedDesc = sort.CacheDescending()
+
+	return hc
+}
+
+func getOptimalCollectorStore(size, skip int, comparator collectorCompare) collectorStore {
 	// pre-allocate space on the store to avoid reslicing
 	// unless the size + skip is too large, then cap it
 	// everything should still work, just reslices as necessary
@@ -119,24 +139,10 @@ func newTopNCollector(size int, skip int, sort search.SortOrder) *TopNCollector
 	}
 
 	if size+skip > 10 {
-		hc.store = newStoreHeap(backingSize, func(i, j *search.DocumentMatch) int {
-			return hc.sort.Compare(hc.cachedScoring, hc.cachedDesc, i, j)
-		})
+		return newStoreHeap(backingSize, comparator)
 	} else {
-		hc.store = newStoreSlice(backingSize, func(i, j *search.DocumentMatch) int {
-			return hc.sort.Compare(hc.cachedScoring, hc.cachedDesc, i, j)
-		})
+		return newStoreSlice(backingSize, comparator)
 	}
-
-	// these lookups traverse an interface, so do once up-front
-	if sort.RequiresDocID() {
-		hc.needDocIds = true
-	}
-	hc.neededFields = sort.RequiredFields()
-	hc.cachedScoring = sort.CacheIsScore()
-	hc.cachedDesc = sort.CacheDescending()
-
-	return hc
 }
 
 func (hc *TopNCollector) Size() int {
@@ -205,6 +211,7 @@ func (hc *TopNCollector) Collect(ctx context.Context, searcher search.Searcher,
 	default:
 		next, err = searcher.Next(searchContext)
 	}
+	var skipped bool
 	for err == nil && next != nil {
 		if hc.total%CheckDoneEvery == 0 {
 			select {
@@ -215,6 +222,15 @@ func (hc *TopNCollector) Collect(ctx context.Context, searcher search.Searcher,
 			}
 		}
 
+		skipped, err = hc.adjustDocumentMatch(searchContext, reader, next)
+		if err != nil {
+			break
+		}
+		if skipped {
+			searchContext.DocumentMatchPool.Put(next)
+			next, err = searcher.Next(searchContext)
+			continue
+		}
 		err = hc.prepareDocumentMatch(searchContext, reader, next)
 		if err != nil {
 			break
@@ -227,6 +243,26 @@ func (hc *TopNCollector) Collect(ctx context.Context, searcher search.Searcher,
 
 		next, err = searcher.Next(searchContext)
 	}
+	if err != nil {
+		return err
+	}
+	if hc.knnHits != nil && !hc.isKnnOperatorAnd() {
+		// if the operator is OR, then we may have some knn hits
+		// left that did not match any of the top N tf-idf hits
+		// we need to add them to the collector store to consider
+		// them as well.
+		for _, knnDoc := range hc.knnHits {
+			knnDoc.Score = hc.computeNewScore(nil, knnDoc)
+			err = hc.prepareDocumentMatch(searchContext, reader, knnDoc)
+			if err != nil {
+				return err
+			}
+			err = dmHandler(knnDoc)
+			if err != nil {
+				return err
+			}
+		}
+	}
 
 	statsCallbackFn := ctx.Value(search.SearchIOStatsCallbackKey)
 	if statsCallbackFn != nil {
@@ -258,6 +294,28 @@ func (hc *TopNCollector) Collect(ctx context.Context, searcher search.Searcher,
 
 var sortByScoreOpt = []string{"_score"}
 
+func (hc *TopNCollector) adjustDocumentMatch(ctx *search.SearchContext,
+	reader index.IndexReader, d *search.DocumentMatch) (skipped bool, err error) {
+
+	if hc.knnHits != nil {
+		if hc.needDocIds {
+			d.ID, err = reader.ExternalID(d.IndexInternalID)
+			if err != nil {
+				return false, err
+			}
+		}
+		if knnHit, ok := hc.knnHits[d.ID]; ok {
+			d.Score = hc.computeNewScore(d, knnHit)
+			delete(hc.knnHits, d.ID)
+		} else if hc.isKnnOperatorAnd() {
+			// if the operator is AND, then we need to skip this hit
+			// since it did not match the knn query
+			return true, nil
+		}
+	}
+	return false, nil
+}
+
 func (hc *TopNCollector) prepareDocumentMatch(ctx *search.SearchContext,
 	reader index.IndexReader, d *search.DocumentMatch) (err error) {
 
@@ -279,7 +337,7 @@ func (hc *TopNCollector) prepareDocumentMatch(ctx *search.SearchContext,
 	}
 
 	// see if we need to load ID (at this early stage, for example to sort on it)
-	if hc.needDocIds {
+	if hc.needDocIds && d.ID == "" {
 		d.ID, err = reader.ExternalID(d.IndexInternalID)
 		if err != nil {
 			return err
@@ -314,6 +372,7 @@ func MakeTopNDocumentMatchHandler(
 				// but we want to allow for exact match, so we pretend
 				hc.searchAfter.HitNumber = d.HitNumber
 				if hc.sort.Compare(hc.cachedScoring, hc.cachedDesc, d, hc.searchAfter) <= 0 {
+					ctx.DocumentMatchPool.Put(d)
 					return nil
 				}
 			}
@@ -357,6 +416,15 @@ func (hc *TopNCollector) visitFieldTerms(reader index.IndexReader, d *search.Doc
 	if hc.facetsBuilder != nil {
 		hc.facetsBuilder.StartDoc()
 	}
+	if d.ID != "" && d.IndexInternalID == nil {
+		// this document may have been sent over as preSearchData and
+		// we need to look up the internal id to visit the doc values for it
+		var err error
+		d.IndexInternalID, err = reader.InternalID(d.ID)
+		if err != nil {
+			return err
+		}
+	}
 
 	err := hc.dvReader.VisitDocValues(d.IndexInternalID, hc.updateFieldVisitor)
 	if hc.facetsBuilder != nil {
@@ -435,3 +503,25 @@ func (hc *TopNCollector) FacetResults() search.FacetResults {
 	}
 	return nil
 }
+
+func (hc *TopNCollector) SetKNNHits(knnHits search.DocumentMatchCollection,
+	newScoreComputer func(d *search.DocumentMatch, knnHit *search.DocumentMatch) float64,
+	isOperatorAnd func() bool) {
+
+	hc.knnHits = make(map[string]*search.DocumentMatch, len(knnHits))
+	for _, hit := range knnHits {
+		hc.knnHits[hit.ID] = hit
+	}
+	hc.computeNewScore = newScoreComputer
+	hc.isKnnOperatorAnd = isOperatorAnd
+	// now that we have the knnHits
+	// when we run the top N search for just the
+	// basic Query object, we need to load the docIds
+	// for the hits -> this is because there might be overlap
+	// or common documents between the knnHits and the top N hits
+	// which indicates that a document matches both a knn query
+	// and the top N query. This means we need to improve the score
+	// of the doc match received as part of the top N search to also incorporate
+	// the score from the knn search.
+	hc.needDocIds = true
+}
diff --git a/search/query/query.go b/search/query/query.go
index eb7b34ad..f0172b76 100644
--- a/search/query/query.go
+++ b/search/query/query.go
@@ -65,6 +65,36 @@ type ValidatableQuery interface {
 	Validate() error
 }
 
+// ParseQuery deserializes a JSON representation of
+// a PreSearchData object.
+func ParsePreSearchData(input []byte) (map[string]interface{}, error) {
+	var rv map[string]interface{}
+
+	var tmp map[string]json.RawMessage
+	err := util.UnmarshalJSON(input, &tmp)
+	if err != nil {
+		return nil, err
+	}
+
+	for k, v := range tmp {
+		switch k {
+		case search.KnnPreSearchDataKey:
+			var value []*search.DocumentMatch
+			if v != nil {
+				err := util.UnmarshalJSON(v, &value)
+				if err != nil {
+					return nil, err
+				}
+			}
+			if rv == nil {
+				rv = make(map[string]interface{})
+			}
+			rv[search.KnnPreSearchDataKey] = value
+		}
+	}
+	return rv, nil
+}
+
 // ParseQuery deserializes a JSON representation of
 // a Query object.
 func ParseQuery(input []byte) (Query, error) {
diff --git a/search/scorer/scorer_knn.go b/search/scorer/scorer_knn.go
index 74ba538d..8cf4a7fd 100644
--- a/search/scorer/scorer_knn.go
+++ b/search/scorer/scorer_knn.go
@@ -40,7 +40,6 @@ type KNNQueryScorer struct {
 	queryBoost             float64
 	queryNorm              float64
 	options                search.SearcherOptions
-	includeScore           bool
 	similarityMetric       string
 	queryWeightExplanation *search.Explanation
 }
@@ -54,7 +53,6 @@ func NewKNNQueryScorer(queryVector []float32, queryField string, queryBoost floa
 		queryBoost:       queryBoost,
 		queryWeight:      1.0,
 		options:          options,
-		includeScore:     options.Score != "none",
 		similarityMetric: similarityMetric,
 	}
 }
@@ -66,65 +64,55 @@ const maxKNNScore = math.MaxFloat64
 func (sqs *KNNQueryScorer) Score(ctx *search.SearchContext,
 	knnMatch *index.VectorDoc) *search.DocumentMatch {
 	rv := ctx.DocumentMatchPool.Get()
-
-	if sqs.includeScore || sqs.options.Explain {
-		var scoreExplanation *search.Explanation
-		score := knnMatch.Score
-		if sqs.similarityMetric == index.EuclideanDistance {
-			// in case of euclidean distance being the distance metric,
-			// an exact vector (perfect match), would return distance = 0
-			if score == 0 {
-				score = maxKNNScore
-			} else {
-				// euclidean distances need to be inverted to work with
-				// tf-idf scoring
-				score = 1.0 / score
-			}
+	var scoreExplanation *search.Explanation
+	score := knnMatch.Score
+	if sqs.similarityMetric == index.EuclideanDistance {
+		// in case of euclidean distance being the distance metric,
+		// an exact vector (perfect match), would return distance = 0
+		if score == 0 {
+			score = maxKNNScore
+		} else {
+			// euclidean distances need to be inverted to work with
+			// tf-idf scoring
+			score = 1.0 / score
 		}
-
+	}
+	if sqs.options.Explain {
+		childExplanations := make([]*search.Explanation, 1)
+		childExplanations[0] = &search.Explanation{
+			Value: score,
+			Message: fmt.Sprintf("vector(field(%s:%s) with similarity_metric(%s)=%e",
+				sqs.queryField, knnMatch.ID, sqs.similarityMetric, score),
+		}
+		scoreExplanation = &search.Explanation{
+			Value: score,
+			Message: fmt.Sprintf("fieldWeight(%s in doc %s), score of:",
+				sqs.queryField, knnMatch.ID),
+			Children: childExplanations,
+		}
+	}
+	// if the query weight isn't 1, multiply
+	if sqs.queryWeight != 1.0 && score != maxKNNScore {
+		score = score * sqs.queryWeight
 		if sqs.options.Explain {
-			childExplanations := make([]*search.Explanation, 1)
-			childExplanations[0] = &search.Explanation{
-				Value: score,
-				Message: fmt.Sprintf("vector(field(%s:%s) with similarity_metric(%s)=%e",
-					sqs.queryField, knnMatch.ID, sqs.similarityMetric, score),
-			}
+			childExplanations := make([]*search.Explanation, 2)
+			childExplanations[0] = sqs.queryWeightExplanation
+			childExplanations[1] = scoreExplanation
 			scoreExplanation = &search.Explanation{
 				Value: score,
-				Message: fmt.Sprintf("fieldWeight(%s in doc %s), score of:",
-					sqs.queryField, knnMatch.ID),
+				// Product of score * weight
+				// Avoid adding the query vector to the explanation since vectors
+				// can get quite large.
+				Message: fmt.Sprintf("weight(%s:query Vector^%f in %s), product of:",
+					sqs.queryField, sqs.queryBoost, knnMatch.ID),
 				Children: childExplanations,
 			}
 		}
-
-		// if the query weight isn't 1, multiply
-		if sqs.queryWeight != 1.0 && score != maxKNNScore {
-			score = score * sqs.queryWeight
-			if sqs.options.Explain {
-				childExplanations := make([]*search.Explanation, 2)
-				childExplanations[0] = sqs.queryWeightExplanation
-				childExplanations[1] = scoreExplanation
-				scoreExplanation = &search.Explanation{
-					Value: score,
-					// Product of score * weight
-					// Avoid adding the query vector to the explanation since vectors
-					// can get quite large.
-					Message: fmt.Sprintf("weight(%s:query Vector^%f in %s), product of:",
-						sqs.queryField, sqs.queryBoost, knnMatch.ID),
-					Children: childExplanations,
-				}
-			}
-		}
-
-		if sqs.includeScore {
-			rv.Score = score
-		}
-
-		if sqs.options.Explain {
-			rv.Expl = scoreExplanation
-		}
 	}
-
+	rv.Score = score
+	if sqs.options.Explain {
+		rv.Expl = scoreExplanation
+	}
 	rv.IndexInternalID = append(rv.IndexInternalID, knnMatch.ID...)
 	return rv
 }
diff --git a/search/search.go b/search/search.go
index 5e8bb7e7..e66b0012 100644
--- a/search/search.go
+++ b/search/search.go
@@ -181,6 +181,14 @@ type DocumentMatch struct {
 	// in the DisjunctionQuery or ConjunctionQuery. The map value is the
 	// sub-score for that sub-query.
 	ScoreBreakdown map[int]float64 `json:"score_breakdown,omitempty"`
+
+	// internal variable used in PreSearch phase of search in alias
+	// indicated the index id of the index that this match came from
+	// used in vector search.
+	// it is a stack of index ids, the top of the stack is the index id
+	// of the index that this match came from
+	// of the current alias view, used in alias of aliases scenario
+	IndexId []int `json:"index_id,omitempty"`
 }
 
 func (dm *DocumentMatch) AddFieldValue(name string, value interface{}) {
@@ -342,7 +350,7 @@ func (dm *DocumentMatch) Complete(prealloc []Location) []Location {
 }
 
 func (dm *DocumentMatch) String() string {
-	return fmt.Sprintf("[%s-%f]", string(dm.IndexInternalID), dm.Score)
+	return fmt.Sprintf("[%s-%f]", dm.ID, dm.Score)
 }
 
 type DocumentMatchCollection []*DocumentMatch
diff --git a/search/searcher/search_conjunction.go b/search/searcher/search_conjunction.go
index 593ee96e..3b93bbff 100644
--- a/search/searcher/search_conjunction.go
+++ b/search/searcher/search_conjunction.go
@@ -16,6 +16,7 @@ package searcher
 
 import (
 	"context"
+	"math"
 	"reflect"
 	"sort"
 
@@ -33,17 +34,16 @@ func init() {
 }
 
 type ConjunctionSearcher struct {
-	indexReader     index.IndexReader
-	searchers       []search.Searcher
-	originalPos     []int
-	queryNorm       float64
-	queryNormForKNN float64
-	currs           []*search.DocumentMatch
-	maxIDIdx        int
-	scorer          *scorer.ConjunctionQueryScorer
-	initialized     bool
-	options         search.SearcherOptions
-	bytesRead       uint64
+	indexReader index.IndexReader
+	searchers   []search.Searcher
+	originalPos []int
+	queryNorm   float64
+	currs       []*search.DocumentMatch
+	maxIDIdx    int
+	scorer      *scorer.ConjunctionQueryScorer
+	initialized bool
+	options     search.SearcherOptions
+	bytesRead   uint64
 }
 
 func NewConjunctionSearcher(ctx context.Context, indexReader index.IndexReader,
@@ -125,6 +125,20 @@ func NewConjunctionSearcher(ctx context.Context, indexReader index.IndexReader,
 	return &c, nil
 }
 
+func (s *ConjunctionSearcher) computeQueryNorm() {
+	// first calculate sum of squared weights
+	sumOfSquaredWeights := 0.0
+	for _, searcher := range s.searchers {
+		sumOfSquaredWeights += searcher.Weight()
+	}
+	// now compute query norm from this
+	s.queryNorm = 1.0 / math.Sqrt(sumOfSquaredWeights)
+	// finally tell all the downstream searchers the norm
+	for _, searcher := range s.searchers {
+		searcher.SetQueryNorm(s.queryNorm)
+	}
+}
+
 func (s *ConjunctionSearcher) Size() int {
 	sizeInBytes := reflectStaticSizeConjunctionSearcher + size.SizeOfPtr +
 		s.scorer.Size()
diff --git a/search/searcher/search_disjunction_heap.go b/search/searcher/search_disjunction_heap.go
index d84af296..c8f6bb26 100644
--- a/search/searcher/search_disjunction_heap.go
+++ b/search/searcher/search_disjunction_heap.go
@@ -18,6 +18,7 @@ import (
 	"bytes"
 	"container/heap"
 	"context"
+	"math"
 	"reflect"
 
 	"github.com/blevesearch/bleve/v2/search"
@@ -92,6 +93,20 @@ func newDisjunctionHeapSearcher(ctx context.Context, indexReader index.IndexRead
 	return &rv, nil
 }
 
+func (s *DisjunctionHeapSearcher) computeQueryNorm() {
+	// first calculate sum of squared weights
+	sumOfSquaredWeights := 0.0
+	for _, searcher := range s.searchers {
+		sumOfSquaredWeights += searcher.Weight()
+	}
+	// now compute query norm from this
+	s.queryNorm = 1.0 / math.Sqrt(sumOfSquaredWeights)
+	// finally tell all the downstream searchers the norm
+	for _, searcher := range s.searchers {
+		searcher.SetQueryNorm(s.queryNorm)
+	}
+}
+
 func (s *DisjunctionHeapSearcher) Size() int {
 	sizeInBytes := reflectStaticSizeDisjunctionHeapSearcher + size.SizeOfPtr +
 		s.scorer.Size()
diff --git a/search/searcher/search_disjunction_slice.go b/search/searcher/search_disjunction_slice.go
index 245adf59..62ff0737 100644
--- a/search/searcher/search_disjunction_slice.go
+++ b/search/searcher/search_disjunction_slice.go
@@ -16,6 +16,7 @@ package searcher
 
 import (
 	"context"
+	"math"
 	"reflect"
 	"sort"
 
@@ -102,6 +103,20 @@ func newDisjunctionSliceSearcher(ctx context.Context, indexReader index.IndexRea
 	return &rv, nil
 }
 
+func (s *DisjunctionSliceSearcher) computeQueryNorm() {
+	// first calculate sum of squared weights
+	sumOfSquaredWeights := 0.0
+	for _, searcher := range s.searchers {
+		sumOfSquaredWeights += searcher.Weight()
+	}
+	// now compute query norm from this
+	s.queryNorm = 1.0 / math.Sqrt(sumOfSquaredWeights)
+	// finally tell all the downstream searchers the norm
+	for _, searcher := range s.searchers {
+		searcher.SetQueryNorm(s.queryNorm)
+	}
+}
+
 func (s *DisjunctionSliceSearcher) Size() int {
 	sizeInBytes := reflectStaticSizeDisjunctionSliceSearcher + size.SizeOfPtr +
 		s.scorer.Size()
diff --git a/search/searcher/util_knn.go b/search/searcher/util_knn.go
index 87716848..552e3143 100644
--- a/search/searcher/util_knn.go
+++ b/search/searcher/util_knn.go
@@ -19,66 +19,11 @@ package searcher
 
 import (
 	"context"
-	"math"
 
 	"github.com/blevesearch/bleve/v2/search"
 	index "github.com/blevesearch/bleve_index_api"
 )
 
-// util func used by both disjunction and conjunction searchers
-// to compute the query norm.
-// This follows a separate code path from the non-knn version
-// because we need to separate out the weights from the KNN searchers
-// and the rest of the searchers to make the knn
-// score completely independent of tf-idf.
-// the sumOfSquaredWeights depends on the tf-idf weights
-// and using the same value for knn searchers will make the
-// knn score dependent on tf-idf.
-func computeQueryNorm(searchers []search.Searcher) (float64, float64) {
-	var queryNorm float64
-	var queryNormForKNN float64
-	// first calculate sum of squared weights
-	sumOfSquaredWeights := 0.0
-
-	sumOfSquaredWeightsForKNN := 0.0
-
-	for _, searcher := range searchers {
-		if knnSearcher, ok := searcher.(*KNNSearcher); ok {
-			sumOfSquaredWeightsForKNN += knnSearcher.Weight()
-		} else {
-			sumOfSquaredWeights += searcher.Weight()
-		}
-	}
-	// now compute query norm from this
-	if sumOfSquaredWeights != 0.0 {
-		queryNorm = 1.0 / math.Sqrt(sumOfSquaredWeights)
-	}
-	if sumOfSquaredWeightsForKNN != 0.0 {
-		queryNormForKNN = 1.0 / math.Sqrt(sumOfSquaredWeightsForKNN)
-	}
-	// finally tell all the downstream searchers the norm
-	for _, searcher := range searchers {
-		if knnSearcher, ok := searcher.(*KNNSearcher); ok {
-			knnSearcher.SetQueryNorm(queryNormForKNN)
-		} else {
-			searcher.SetQueryNorm(queryNorm)
-		}
-	}
-	return queryNorm, queryNormForKNN
-}
-
-func (s *DisjunctionSliceSearcher) computeQueryNorm() {
-	s.queryNorm, s.queryNormForKNN = computeQueryNorm(s.searchers)
-}
-
-func (s *DisjunctionHeapSearcher) computeQueryNorm() {
-	s.queryNorm, s.queryNormForKNN = computeQueryNorm(s.searchers)
-}
-
-func (s *ConjunctionSearcher) computeQueryNorm() {
-	s.queryNorm, s.queryNormForKNN = computeQueryNorm(s.searchers)
-}
-
 func optimizeCompositeSearcher(ctx context.Context, optimizationKind string,
 	indexReader index.IndexReader, qsearchers []search.Searcher,
 	options search.SearcherOptions) (search.Searcher, error) {
diff --git a/search/searcher/util_no_knn.go b/search/searcher/util_no_knn.go
index bd836d27..0fb947ae 100644
--- a/search/searcher/util_no_knn.go
+++ b/search/searcher/util_no_knn.go
@@ -19,54 +19,11 @@ package searcher
 
 import (
 	"context"
-	"math"
 
 	"github.com/blevesearch/bleve/v2/search"
 	index "github.com/blevesearch/bleve_index_api"
 )
 
-func (s *DisjunctionSliceSearcher) computeQueryNorm() {
-	// first calculate sum of squared weights
-	sumOfSquaredWeights := 0.0
-	for _, searcher := range s.searchers {
-		sumOfSquaredWeights += searcher.Weight()
-	}
-	// now compute query norm from this
-	s.queryNorm = 1.0 / math.Sqrt(sumOfSquaredWeights)
-	// finally tell all the downstream searchers the norm
-	for _, searcher := range s.searchers {
-		searcher.SetQueryNorm(s.queryNorm)
-	}
-}
-
-func (s *DisjunctionHeapSearcher) computeQueryNorm() {
-	// first calculate sum of squared weights
-	sumOfSquaredWeights := 0.0
-	for _, searcher := range s.searchers {
-		sumOfSquaredWeights += searcher.Weight()
-	}
-	// now compute query norm from this
-	s.queryNorm = 1.0 / math.Sqrt(sumOfSquaredWeights)
-	// finally tell all the downstream searchers the norm
-	for _, searcher := range s.searchers {
-		searcher.SetQueryNorm(s.queryNorm)
-	}
-}
-
-func (s *ConjunctionSearcher) computeQueryNorm() {
-	// first calculate sum of squared weights
-	sumOfSquaredWeights := 0.0
-	for _, searcher := range s.searchers {
-		sumOfSquaredWeights += searcher.Weight()
-	}
-	// now compute query norm from this
-	s.queryNorm = 1.0 / math.Sqrt(sumOfSquaredWeights)
-	// finally tell all the downstream searchers the norm
-	for _, searcher := range s.searchers {
-		searcher.SetQueryNorm(s.queryNorm)
-	}
-}
-
 func optimizeCompositeSearcher(ctx context.Context, optimizationKind string,
 	indexReader index.IndexReader, qsearchers []search.Searcher,
 	options search.SearcherOptions) (search.Searcher, error) {
diff --git a/search/util.go b/search/util.go
index e5712420..1b9e5211 100644
--- a/search/util.go
+++ b/search/util.go
@@ -134,3 +134,7 @@ const MaxGeoBufPoolSize = 24 * 1024
 const MinGeoBufPoolSize = 24
 
 type GeoBufferPoolCallbackFunc func() *s2.GeoBufferPool
+
+const KnnPreSearchDataKey = "_knn_pre_search_data_key"
+
+const PreSearchKey = "_presearch_key"
diff --git a/search_knn.go b/search_knn.go
index ee2d3f64..f2f39369 100644
--- a/search_knn.go
+++ b/search_knn.go
@@ -18,13 +18,15 @@
 package bleve
 
 import (
-	"container/heap"
+	"context"
 	"encoding/json"
 	"fmt"
 	"sort"
 
 	"github.com/blevesearch/bleve/v2/search"
+	"github.com/blevesearch/bleve/v2/search/collector"
 	"github.com/blevesearch/bleve/v2/search/query"
+	index "github.com/blevesearch/bleve_index_api"
 )
 
 type knnOperator string
@@ -46,6 +48,18 @@ type SearchRequest struct {
 	KNN         []*KNNRequest `json:"knn"`
 	KNNOperator knnOperator   `json:"knn_operator"`
 
+	// PreSearchData will be a  map that will be used
+	// in the second phase of any 2-phase search, to provide additional
+	// context to the second phase. This is useful in the case of index
+	// aliases where the first phase will gather the PreSearchData from all
+	// the indexes in the alias, and the second phase will use that
+	// PreSearchData to perform the actual search.
+	// The currently accepted map configuration is:
+	//
+	// "_knn_pre_search_data_key": []*search.DocumentMatch
+
+	PreSearchData map[string]interface{} `json:"pre_search_data,omitempty"`
+
 	sortFunc func(sort.Interface)
 }
 
@@ -88,6 +102,7 @@ func (r *SearchRequest) UnmarshalJSON(input []byte) error {
 		SearchBefore     []string          `json:"search_before"`
 		KNN              []*KNNRequest     `json:"knn"`
 		KNNOperator      knnOperator       `json:"knn_operator"`
+		PreSearchData    json.RawMessage   `json:"pre_search_data"`
 	}
 
 	err := json.Unmarshal(input, &temp)
@@ -135,13 +150,20 @@ func (r *SearchRequest) UnmarshalJSON(input []byte) error {
 		r.KNNOperator = knnOperatorOr
 	}
 
+	if temp.PreSearchData != nil {
+		r.PreSearchData, err = query.ParsePreSearchData(temp.PreSearchData)
+		if err != nil {
+			return err
+		}
+	}
+
 	return nil
 
 }
 
 // -----------------------------------------------------------------------------
 
-func copySearchRequest(req *SearchRequest) *SearchRequest {
+func copySearchRequest(req *SearchRequest, preSearchData map[string]interface{}) *SearchRequest {
 	rv := SearchRequest{
 		Query:            req.Query,
 		Size:             req.Size + req.From,
@@ -157,6 +179,7 @@ func copySearchRequest(req *SearchRequest) *SearchRequest {
 		SearchBefore:     req.SearchBefore,
 		KNN:              req.KNN,
 		KNNOperator:      req.KNNOperator,
+		PreSearchData:    preSearchData,
 	}
 	return &rv
 
@@ -167,146 +190,196 @@ var (
 	knnOperatorOr  = knnOperator("or")
 )
 
-func queryWithKNN(req *SearchRequest) (query.Query, error) {
-	if len(req.KNN) > 0 {
-		subQueries := []query.Query{req.Query}
-		for _, knn := range req.KNN {
-			if knn != nil {
-				knnQuery := query.NewKNNQuery(knn.Vector)
-				knnQuery.SetFieldVal(knn.Field)
-				knnQuery.SetK(knn.K)
-				knnQuery.SetBoost(knn.Boost.Value())
-				subQueries = append(subQueries, knnQuery)
-			}
+func createKNNQuery(req *SearchRequest) (query.Query, []int64, int64, error) {
+	if requestHasKNN(req) {
+		// first perform validation
+		err := validateKNN(req)
+		if err != nil {
+			return nil, nil, 0, err
 		}
-		if req.KNNOperator == knnOperatorAnd {
-			rv := query.NewConjunctionQuery(subQueries)
-			rv.RetrieveScoreBreakdown(true)
-			return rv, nil
-		} else if req.KNNOperator == knnOperatorOr || req.KNNOperator == "" {
-			rv := query.NewDisjunctionQuery(subQueries)
-			rv.RetrieveScoreBreakdown(true)
-			return rv, nil
-		} else {
-			return nil, fmt.Errorf("unknown knn operator: %s", req.KNNOperator)
+		var subQueries []query.Query
+		kArray := make([]int64, 0, len(req.KNN))
+		sumOfK := int64(0)
+		for _, knn := range req.KNN {
+			knnQuery := query.NewKNNQuery(knn.Vector)
+			knnQuery.SetFieldVal(knn.Field)
+			knnQuery.SetK(knn.K)
+			knnQuery.SetBoost(knn.Boost.Value())
+			subQueries = append(subQueries, knnQuery)
+			kArray = append(kArray, knn.K)
+			sumOfK += knn.K
 		}
+		rv := query.NewDisjunctionQuery(subQueries)
+		rv.RetrieveScoreBreakdown(true)
+		return rv, kArray, sumOfK, nil
 	}
-	return req.Query, nil
+	return nil, nil, 0, nil
 }
 
 func validateKNN(req *SearchRequest) error {
+	if req.KNN != nil &&
+		req.KNNOperator != "" &&
+		req.KNNOperator != knnOperatorOr &&
+		req.KNNOperator != knnOperatorAnd {
+		return fmt.Errorf("unknown knn operator: %s", req.KNNOperator)
+	}
 	for _, q := range req.KNN {
+		if q == nil {
+			return fmt.Errorf("knn query cannot be nil")
+		}
 		if q.K <= 0 || len(q.Vector) == 0 {
 			return fmt.Errorf("k must be greater than 0 and vector must be non-empty")
 		}
 	}
+	switch req.KNNOperator {
+	case knnOperatorAnd, knnOperatorOr, "":
+		// Valid cases, do nothing
+	default:
+		return fmt.Errorf("knn_operator must be either 'and' / 'or'")
+	}
 	return nil
 }
 
-func mergeKNNResults(req *SearchRequest, sr *SearchResult) {
-	if len(req.KNN) > 0 {
-		mergeKNN(req, sr)
+func (i *indexImpl) runKnnCollector(ctx context.Context, req *SearchRequest, reader index.IndexReader, preSearch bool) ([]*search.DocumentMatch, error) {
+	KNNQuery, kArray, sumOfK, err := createKNNQuery(req)
+	if err != nil {
+		return nil, err
 	}
-}
-
-func adjustRequestSizeForKNN(req *SearchRequest, numIndexPartitions int) int {
-	var adjustedSize int
-	if req != nil {
-		adjustedSize = req.Size
-		if len(req.KNN) > 0 {
-			var minSizeReq int64
-			for _, knn := range req.KNN {
-				minSizeReq += knn.K
+	knnSearcher, err := KNNQuery.Searcher(ctx, reader, i.m, search.SearcherOptions{
+		Explain: req.Explain,
+	})
+	if err != nil {
+		return nil, err
+	}
+	knnCollector := collector.NewKNNCollector(kArray, sumOfK)
+	err = knnCollector.Collect(ctx, knnSearcher, reader)
+	if err != nil {
+		return nil, err
+	}
+	knnHits := knnCollector.Results()
+	if !preSearch {
+		if req.KNNOperator == knnOperatorAnd {
+			idx := 0
+			for _, hit := range knnHits {
+				if len(hit.ScoreBreakdown) == len(kArray) {
+					knnHits[idx] = hit
+					idx++
+				}
 			}
-			minSizeReq *= int64(numIndexPartitions)
-			if int64(adjustedSize) < minSizeReq {
-				adjustedSize = int(minSizeReq)
+			knnHits = knnHits[:idx]
+		}
+		if req.Score == "none" {
+			for _, hit := range knnHits {
+				hit.Score = 0.0
+				hit.ScoreBreakdown = nil
 			}
 		}
 	}
-	return adjustedSize
+	return knnHits, nil
 }
 
-// heap impl
-type scoreHeap struct {
-	scoreMaps []map[int]float64
-	sortIndex int
+func setKnnHitsInCollector(knnHits []*search.DocumentMatch, req *SearchRequest, coll *collector.TopNCollector) {
+	if len(knnHits) > 0 {
+		coll.SetKNNHits(knnHits,
+			func(tdIdfDocMatch *search.DocumentMatch, knnMatch *search.DocumentMatch) float64 {
+				totalScore := 0.0
+				if tdIdfDocMatch != nil {
+					totalScore += tdIdfDocMatch.Score
+				}
+				for _, score := range knnMatch.ScoreBreakdown {
+					totalScore += score
+				}
+				return totalScore
+			},
+			func() bool {
+				return req.KNNOperator == knnOperatorAnd
+			},
+		)
+	}
 }
 
-func (s *scoreHeap) Len() int { return len(s.scoreMaps) }
-
-func (s *scoreHeap) Less(i, j int) bool {
-	return (s.scoreMaps[i])[s.sortIndex] > (s.scoreMaps[j])[s.sortIndex]
+func mergeKNNDocumentMatches(req *SearchRequest, knnHits []*search.DocumentMatch, mergeOut []map[string]interface{}) {
+	kArray := make([]int64, len(req.KNN))
+	for i, knnReq := range req.KNN {
+		kArray[i] = knnReq.K
+	}
+	knnStore := collector.GetNewKNNCollectorStore(kArray)
+	for _, hit := range knnHits {
+		knnStore.AddDocument(hit)
+	}
+	mergedKNNhits := knnStore.AllHits()
+	if req.KNNOperator == knnOperatorAnd {
+		for hit := range mergedKNNhits {
+			if len(hit.ScoreBreakdown) != len(req.KNN) {
+				delete(mergedKNNhits, hit)
+			}
+		}
+	}
+	if req.Score == "none" {
+		for hit := range mergedKNNhits {
+			hit.Score = 0.0
+			hit.ScoreBreakdown = nil
+		}
+	}
+	indexNumToDocMatchList := make(map[int][]*search.DocumentMatch)
+	for docMatch := range mergedKNNhits {
+		distributeKNNHit(docMatch, indexNumToDocMatchList)
+	}
+	for i := 0; i < len(mergeOut); i++ {
+		mergeOut[i][search.KnnPreSearchDataKey] = indexNumToDocMatchList[i]
+	}
 }
 
-func (s *scoreHeap) Swap(i, j int) {
-	s.scoreMaps[i], s.scoreMaps[j] = s.scoreMaps[j], s.scoreMaps[i]
+// when we are setting KNN hits in the preSearchData, we need to make sure that
+// the KNN hit goes to the right index. This is because the KNN hits are
+// collected from all the indexes in the alias, but the preSearchData is
+// specific to each index. If alias A1 contains indexes I1 and I2 and
+// the KNN hits collected from both I1 and I2, and merged to get top K
+// hits, then the top K hits need to be distributed to I1 and I2,
+// so that the preSearchData for I1 contains the top K hits from I1 and
+// the preSearchData for I2 contains the top K hits from I2.
+func distributeKNNHit(docMatch *search.DocumentMatch, indexNumToDocMatchList map[int][]*search.DocumentMatch) {
+	top := docMatch.IndexId[len(docMatch.IndexId)-1]
+	docMatch.IndexId = docMatch.IndexId[:len(docMatch.IndexId)-1]
+	indexNumToDocMatchList[top] = append(indexNumToDocMatchList[top], docMatch)
 }
 
-func (s *scoreHeap) Push(x interface{}) {
-	s.scoreMaps = append(s.scoreMaps, x.(map[int]float64))
+func requestHasKNN(req *SearchRequest) bool {
+	return len(req.KNN) > 0
 }
 
-func (s *scoreHeap) Pop() interface{} {
-	old := s.scoreMaps
-	n := len(old)
-	x := old[n-1]
-	s.scoreMaps = old[0 : n-1]
-	return x
+func addKnnToDummyRequest(dummyReq *SearchRequest, realReq *SearchRequest) {
+	dummyReq.KNN = realReq.KNN
+	dummyReq.KNNOperator = knnOperatorOr
 }
 
-func mergeKNN(req *SearchRequest, sr *SearchResult) {
-	maxHeap := &scoreHeap{
-		scoreMaps: make([]map[int]float64, 0),
+// the preSearchData for KNN is a list of DocumentMatch objects
+// that need to be redistributed to the right index.
+// This is used only in the case of an alias tree, where the indexes
+// are at the leaves of the tree, and the master alias is at the root.
+// At each level of the tree, the preSearchData needs to be redistributed
+// to the indexes/aliases at that level. Because the preSearchData is
+// specific to each final index at the leaf.
+func redistributeKNNPreSearchData(req *SearchRequest, mergedOut []map[string]interface{}) error {
+	knnHits, ok := req.PreSearchData[search.KnnPreSearchDataKey].([]*search.DocumentMatch)
+	if !ok {
+		return fmt.Errorf("preSearchData does not have knn preSearchData for redistribution")
 	}
-	for i := 0; i < len(req.KNN); i++ {
-		kVal := req.KNN[i].K
-		maxHeap.sortIndex = i + 1
-		for _, hit := range sr.Hits {
-			heap.Push(maxHeap, hit.ScoreBreakdown)
-		}
-		for maxHeap.Len() > 0 {
-			scBreakdown := heap.Pop(maxHeap).(map[int]float64)
-			if kVal > 0 {
-				kVal--
-			} else {
-				delete(scBreakdown, maxHeap.sortIndex)
-			}
-		}
+	indexNumToDocMatchList := make(map[int][]*search.DocumentMatch)
+	for _, docMatch := range knnHits {
+		distributeKNNHit(docMatch, indexNumToDocMatchList)
 	}
-	maxScore := 0.0
-	var numHitsDropped uint64
-	numTotalSearchers := float64(len(req.KNN) + 1)
-	hitIdx := 0
-	for _, hit := range sr.Hits {
-		numMatchedSearchers := float64(len(hit.ScoreBreakdown))
-		newScore := recomputeTotalScore(req.KNNOperator, hit, numMatchedSearchers, numTotalSearchers)
-		if newScore > 0 {
-			hit.Score = newScore
-			if newScore > maxScore {
-				maxScore = newScore
+	for i := 0; i < len(mergedOut); i++ {
+		newMD := make(map[string]interface{})
+		for k, v := range req.PreSearchData {
+			switch k {
+			case search.KnnPreSearchDataKey:
+				newMD[k] = indexNumToDocMatchList[i]
+			default:
+				newMD[k] = v
 			}
-			sr.Hits[hitIdx] = hit
-			hitIdx++
-		} else {
-			numHitsDropped++
 		}
+		mergedOut[i] = newMD
 	}
-	sr.Hits = sr.Hits[:hitIdx]
-	sr.MaxScore = maxScore
-	sr.Total -= numHitsDropped
-}
-
-func recomputeTotalScore(operator knnOperator, hit *search.DocumentMatch, numMatchedSearchers, numTotalSearchers float64) float64 {
-	if operator == knnOperatorAnd && numMatchedSearchers != numTotalSearchers {
-		return 0
-	}
-	var totalScore float64
-	for _, score := range hit.ScoreBreakdown {
-		totalScore += score
-	}
-	if operator == knnOperatorOr || operator == "" {
-		totalScore *= (numMatchedSearchers / numTotalSearchers)
-	}
-	return totalScore
+	return nil
 }
diff --git a/search_no_knn.go b/search_no_knn.go
index 5b6af7f5..4e2450df 100644
--- a/search_no_knn.go
+++ b/search_no_knn.go
@@ -18,11 +18,14 @@
 package bleve
 
 import (
+	"context"
 	"encoding/json"
 	"sort"
 
 	"github.com/blevesearch/bleve/v2/search"
+	"github.com/blevesearch/bleve/v2/search/collector"
 	"github.com/blevesearch/bleve/v2/search/query"
+	index "github.com/blevesearch/bleve_index_api"
 )
 
 // A SearchRequest describes all the parameters
@@ -60,6 +63,18 @@ type SearchRequest struct {
 	SearchAfter      []string          `json:"search_after"`
 	SearchBefore     []string          `json:"search_before"`
 
+	// PreSearchData will be a  map that will be used
+	// in the second phase of any 2-phase search, to provide additional
+	// context to the second phase. This is useful in the case of index
+	// aliases where the first phase will gather the PreSearchData from all
+	// the indexes in the alias, and the second phase will use that
+	// PreSearchData to perform the actual search.
+	// The currently accepted map configuration is:
+	//
+	// "_knn_pre_search_data_key": []*search.DocumentMatch
+
+	PreSearchData map[string]interface{} `json:"pre_search_data,omitempty"`
+
 	sortFunc func(sort.Interface)
 }
 
@@ -79,6 +94,7 @@ func (r *SearchRequest) UnmarshalJSON(input []byte) error {
 		Score            string            `json:"score"`
 		SearchAfter      []string          `json:"search_after"`
 		SearchBefore     []string          `json:"search_before"`
+		PreSearchData    json.RawMessage   `json:"pre_search_data"`
 	}
 
 	err := json.Unmarshal(input, &temp)
@@ -119,6 +135,12 @@ func (r *SearchRequest) UnmarshalJSON(input []byte) error {
 	if r.From < 0 {
 		r.From = 0
 	}
+	if temp.PreSearchData != nil {
+		r.PreSearchData, err = query.ParsePreSearchData(temp.PreSearchData)
+		if err != nil {
+			return err
+		}
+	}
 
 	return nil
 
@@ -126,7 +148,7 @@ func (r *SearchRequest) UnmarshalJSON(input []byte) error {
 
 // -----------------------------------------------------------------------------
 
-func copySearchRequest(req *SearchRequest) *SearchRequest {
+func copySearchRequest(req *SearchRequest, preSearchData map[string]interface{}) *SearchRequest {
 	rv := SearchRequest{
 		Query:            req.Query,
 		Size:             req.Size + req.From,
@@ -140,25 +162,32 @@ func copySearchRequest(req *SearchRequest) *SearchRequest {
 		Score:            req.Score,
 		SearchAfter:      req.SearchAfter,
 		SearchBefore:     req.SearchBefore,
+		PreSearchData:    preSearchData,
 	}
 	return &rv
 }
 
-func queryWithKNN(req *SearchRequest) (query.Query, error) {
-	return req.Query, nil
-}
-
 func validateKNN(req *SearchRequest) error {
 	return nil
 }
 
-func mergeKNNResults(req *SearchRequest, sr *SearchResult) {
-	// no-op
+func (i *indexImpl) runKnnCollector(ctx context.Context, req *SearchRequest, reader index.IndexReader, preSearch bool) ([]*search.DocumentMatch, error) {
+	return nil, nil
 }
 
-func adjustRequestSizeForKNN(req *SearchRequest, numIndexPartitions int) int {
-	if req != nil {
-		return req.Size
-	}
-	return 0
+func setKnnHitsInCollector(knnHits []*search.DocumentMatch, req *SearchRequest, coll *collector.TopNCollector) {
+}
+
+func requestHasKNN(req *SearchRequest) bool {
+	return false
+}
+
+func addKnnToDummyRequest(dummyReq *SearchRequest, realReq *SearchRequest) {
+}
+
+func mergeKNNDocumentMatches(req *SearchRequest, knnHits []*search.DocumentMatch, mergeOut []map[string]interface{}) {
+}
+
+func redistributeKNNPreSearchData(req *SearchRequest, mergedOut []map[string]interface{}) error {
+	return nil
 }
diff --git a/util/extract.go b/util/extract.go
index f8e61546..e963d0c3 100644
--- a/util/extract.go
+++ b/util/extract.go
@@ -15,6 +15,7 @@
 package util
 
 import (
+	"math"
 	"reflect"
 )
 
@@ -24,13 +25,13 @@ func ExtractNumericValFloat64(v interface{}) (float64, bool) {
 	if !val.IsValid() {
 		return 0, false
 	}
-	typ := val.Type()
-	switch typ.Kind() {
-	case reflect.Float32, reflect.Float64:
+
+	switch {
+	case val.CanFloat():
 		return val.Float(), true
-	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+	case val.CanInt():
 		return float64(val.Int()), true
-	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
+	case val.CanUint():
 		return float64(val.Uint()), true
 	}
 
@@ -43,13 +44,17 @@ func ExtractNumericValFloat32(v interface{}) (float32, bool) {
 	if !val.IsValid() {
 		return 0, false
 	}
-	typ := val.Type()
-	switch typ.Kind() {
-	case reflect.Float32, reflect.Float64:
-		return float32(val.Float()), true
-	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+
+	switch {
+	case val.CanFloat():
+		floatVal := val.Float()
+		if floatVal > math.MaxFloat32 {
+			return 0, false
+		}
+		return float32(floatVal), true
+	case val.CanInt():
 		return float32(val.Int()), true
-	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
+	case val.CanUint():
 		return float32(val.Uint()), true
 	}
 


Test patch:
diff --git a/index_alias_impl_test.go b/index_alias_impl_test.go
index 1b6ae55f..fc5686fb 100644
--- a/index_alias_impl_test.go
+++ b/index_alias_impl_test.go
@@ -594,7 +594,7 @@ func TestMultiSearchNoError(t *testing.T) {
 		MaxScore: 2.0,
 	}
 
-	results, err := MultiSearch(context.Background(), sr, ei1, ei2)
+	results, err := MultiSearch(context.Background(), sr, nil, ei1, ei2)
 	if err != nil {
 		t.Error(err)
 	}
@@ -625,7 +625,7 @@ func TestMultiSearchSomeError(t *testing.T) {
 	}}
 	ei2 := &stubIndex{name: "ei2", err: fmt.Errorf("deliberate error")}
 	sr := NewSearchRequest(NewTermQuery("test"))
-	res, err := MultiSearch(context.Background(), sr, ei1, ei2)
+	res, err := MultiSearch(context.Background(), sr, nil, ei1, ei2)
 	if err != nil {
 		t.Errorf("expected no error, got %v", err)
 	}
@@ -652,7 +652,7 @@ func TestMultiSearchAllError(t *testing.T) {
 	ei1 := &stubIndex{name: "ei1", err: fmt.Errorf("deliberate error")}
 	ei2 := &stubIndex{name: "ei2", err: fmt.Errorf("deliberate error")}
 	sr := NewSearchRequest(NewTermQuery("test"))
-	res, err := MultiSearch(context.Background(), sr, ei1, ei2)
+	res, err := MultiSearch(context.Background(), sr, nil, ei1, ei2)
 	if err != nil {
 		t.Errorf("expected no error, got %v", err)
 	}
@@ -708,7 +708,7 @@ func TestMultiSearchSecondPage(t *testing.T) {
 		checkRequest: checkRequest,
 	}
 	sr := NewSearchRequestOptions(NewTermQuery("test"), 10, 10, false)
-	_, err := MultiSearch(context.Background(), sr, ei1, ei2)
+	_, err := MultiSearch(context.Background(), sr, nil, ei1, ei2)
 	if err != nil {
 		t.Errorf("unexpected error %v", err)
 	}
@@ -786,7 +786,7 @@ func TestMultiSearchTimeout(t *testing.T) {
 	defer cancel()
 	query := NewTermQuery("test")
 	sr := NewSearchRequest(query)
-	res, err := MultiSearch(ctx, sr, ei1, ei2)
+	res, err := MultiSearch(ctx, sr, nil, ei1, ei2)
 	if err != nil {
 		t.Errorf("expected no error, got %v", err)
 	}
@@ -806,7 +806,7 @@ func TestMultiSearchTimeout(t *testing.T) {
 	// now run a search again with an absurdly low timeout (should timeout)
 	ctx, cancel = context.WithTimeout(context.Background(), 1*time.Microsecond)
 	defer cancel()
-	res, err = MultiSearch(ctx, sr, ei1, ei2)
+	res, err = MultiSearch(ctx, sr, nil, ei1, ei2)
 	if err != nil {
 		t.Errorf("expected no error, got %v", err)
 	}
@@ -833,7 +833,7 @@ func TestMultiSearchTimeout(t *testing.T) {
 	// now run a search again with a normal timeout, but cancel it first
 	ctx, cancel = context.WithTimeout(context.Background(), 5*time.Second)
 	cancel()
-	res, err = MultiSearch(ctx, sr, ei1, ei2)
+	res, err = MultiSearch(ctx, sr, nil, ei1, ei2)
 	if err != nil {
 		t.Errorf("expected no error, got %v", err)
 	}
@@ -968,7 +968,7 @@ func TestMultiSearchTimeoutPartial(t *testing.T) {
 		MaxScore: 2.0,
 	}
 
-	res, err := MultiSearch(ctx, sr, ei1, ei2, ei3)
+	res, err := MultiSearch(ctx, sr, nil, ei1, ei2, ei3)
 	if err != nil {
 		t.Fatalf("expected no err, got %v", err)
 	}
@@ -1222,7 +1222,7 @@ func TestMultiSearchCustomSort(t *testing.T) {
 		MaxScore: 3.0,
 	}
 
-	results, err := MultiSearch(context.Background(), sr, ei1, ei2)
+	results, err := MultiSearch(context.Background(), sr, nil, ei1, ei2)
 	if err != nil {
 		t.Error(err)
 	}
diff --git a/index_test.go b/index_test.go
index 3923f89d..c6cc0793 100644
--- a/index_test.go
+++ b/index_test.go
@@ -274,7 +274,6 @@ func checkStatsOnIndexedBatch(indexPath string, indexMapping mapping.IndexMappin
 	return statValError
 }
 
-/*
 func TestBytesWritten(t *testing.T) {
 	tmpIndexPath := createTmpIndexPath(t)
 
@@ -305,7 +304,7 @@ func TestBytesWritten(t *testing.T) {
 	typeFieldMapping.DocValues = false
 	documentMapping.AddFieldMappingsAt("type", typeFieldMapping)
 
-	err = checkStatsOnIndexedBatch(tmpIndexPath, indexMapping, 37767)
+	err = checkStatsOnIndexedBatch(tmpIndexPath, indexMapping, 57273)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -314,7 +313,7 @@ func TestBytesWritten(t *testing.T) {
 	contentFieldMapping.Store = true
 	tmpIndexPath1 := createTmpIndexPath(t)
 
-	err := checkStatsOnIndexedBatch(tmpIndexPath1, indexMapping, 56582)
+	err := checkStatsOnIndexedBatch(tmpIndexPath1, indexMapping, 76069)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -324,7 +323,7 @@ func TestBytesWritten(t *testing.T) {
 	contentFieldMapping.IncludeInAll = true
 	tmpIndexPath2 := createTmpIndexPath(t)
 
-	err = checkStatsOnIndexedBatch(tmpIndexPath2, indexMapping, 44714)
+	err = checkStatsOnIndexedBatch(tmpIndexPath2, indexMapping, 68875)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -334,7 +333,7 @@ func TestBytesWritten(t *testing.T) {
 	contentFieldMapping.IncludeTermVectors = true
 	tmpIndexPath3 := createTmpIndexPath(t)
 
-	err = checkStatsOnIndexedBatch(tmpIndexPath3, indexMapping, 59479)
+	err = checkStatsOnIndexedBatch(tmpIndexPath3, indexMapping, 78985)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -344,7 +343,7 @@ func TestBytesWritten(t *testing.T) {
 	contentFieldMapping.DocValues = true
 	tmpIndexPath4 := createTmpIndexPath(t)
 
-	err = checkStatsOnIndexedBatch(tmpIndexPath4, indexMapping, 44722)
+	err = checkStatsOnIndexedBatch(tmpIndexPath4, indexMapping, 64228)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -402,8 +401,8 @@ func TestBytesRead(t *testing.T) {
 	}
 	stats, _ := idx.StatsMap()["index"].(map[string]interface{})
 	prevBytesRead, _ := stats["num_bytes_read_at_query_time"].(uint64)
-	if prevBytesRead != 36066 && res.Cost == prevBytesRead {
-		t.Fatalf("expected bytes read for query string 32349, got %v",
+	if prevBytesRead != 21639 && res.Cost == prevBytesRead {
+		t.Fatalf("expected bytes read for query string 21639, got %v",
 			prevBytesRead)
 	}
 
@@ -554,8 +553,8 @@ func TestBytesReadStored(t *testing.T) {
 
 	stats, _ := idx.StatsMap()["index"].(map[string]interface{})
 	bytesRead, _ := stats["num_bytes_read_at_query_time"].(uint64)
-	if bytesRead != 25928 && bytesRead == res.Cost {
-		t.Fatalf("expected the bytes read stat to be around 25928, got %v", bytesRead)
+	if bytesRead != 11501 && bytesRead == res.Cost {
+		t.Fatalf("expected the bytes read stat to be around 11501, got %v", bytesRead)
 	}
 	prevBytesRead := bytesRead
 
@@ -625,8 +624,8 @@ func TestBytesReadStored(t *testing.T) {
 
 	stats, _ = idx1.StatsMap()["index"].(map[string]interface{})
 	bytesRead, _ = stats["num_bytes_read_at_query_time"].(uint64)
-	if bytesRead != 18114 && bytesRead == res.Cost {
-		t.Fatalf("expected the bytes read stat to be around 18114, got %v", bytesRead)
+	if bytesRead != 3687 && bytesRead == res.Cost {
+		t.Fatalf("expected the bytes read stat to be around 3687, got %v", bytesRead)
 	}
 	prevBytesRead = bytesRead
 
@@ -653,7 +652,6 @@ func TestBytesReadStored(t *testing.T) {
 		t.Fatalf("expected the bytes read stat to be around 42, got %v", bytesRead-prevBytesRead)
 	}
 }
-*/
 
 func getBatchFromData(idx Index, fileName string) (*Batch, error) {
 	pwd, err := os.Getwd()
diff --git a/mapping/mapping_vectors_test.go b/mapping/mapping_vectors_test.go
new file mode 100644
index 00000000..8c04d03a
--- /dev/null
+++ b/mapping/mapping_vectors_test.go
@@ -0,0 +1,297 @@
+//  Copyright (c) 2023 Couchbase, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// 		http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//go:build vectors
+// +build vectors
+
+package mapping
+
+import "testing"
+
+func TestVectorFieldAliasValidation(t *testing.T) {
+	tests := []struct {
+		// input
+		name       string // name of the test
+		mappingStr string // index mapping json string
+
+		// expected output
+		expValidity bool   // validity of the mapping
+		errMsg      string // error message, given expValidity is false
+	}{
+		{
+			name: "test1",
+			mappingStr: `
+				{
+					"default_mapping": {
+						"properties": {
+							"cityVec": {
+								"fields": [
+									{
+										"type": "vector",
+										"dims": 3
+									},
+									{
+										"name": "cityVec",
+										"type": "vector",
+										"dims": 4
+									}
+								]
+							}
+						}
+					}
+				}`,
+			expValidity: false,
+			errMsg:      `field: 'cityVec', invalid alias (different dimensions 4 and 3)`,
+		},
+		{
+			name: "test2",
+			mappingStr: `
+				{
+					"default_mapping": {
+						"properties": {
+							"cityVec": {
+								"fields": [
+									{
+										"type": "vector",
+										"dims": 3,
+										"similarity": "l2_norm"
+									},
+									{
+										"name": "cityVec",
+										"type": "vector",
+										"dims": 3,
+										"similarity": "dot_product"
+									}
+								]
+							}
+						}
+					}
+				}`,
+			expValidity: false,
+			errMsg:      `field: 'cityVec', invalid alias (different similarity values dot_product and l2_norm)`,
+		},
+		{
+			name: "test3",
+			mappingStr: `
+				{
+					"default_mapping": {
+						"properties": {
+							"cityVec": {
+								"fields": [
+									{
+										"type": "vector",
+										"dims": 3
+									},
+									{
+										"name": "cityVec",
+										"type": "vector",
+										"dims": 3
+									}
+								]
+							}
+						}
+					}
+				}`,
+			expValidity: true,
+			errMsg:      "",
+		},
+		{
+			name: "test4",
+			mappingStr: `
+				{
+					"default_mapping": {
+						"properties": {
+							"cityVec": {
+								"fields": [
+									{
+										"name": "vecData",
+										"type": "vector",
+										"dims": 4
+									}
+								]
+							},
+							"countryVec": {
+								"fields": [
+									{
+										"name": "vecData",
+										"type": "vector",
+										"dims": 3
+									}
+								]
+							}
+						}
+					}
+				}`,
+			expValidity: false,
+			errMsg:      `field: 'vecData', invalid alias (different dimensions 3 and 4)`,
+		},
+		{
+			name: "test5",
+			mappingStr: `
+				{
+					"default_mapping": {
+						"properties": {
+							"cityVec": {
+								"fields": [
+									{
+										"name": "vecData",
+										"type": "vector",
+										"dims": 3
+									}
+								]
+							}
+						}
+					},
+					"types": {
+						"type1": {
+							"properties": {
+								"cityVec": {
+									"fields": [
+										{
+											"name": "vecData",
+											"type": "vector",
+											"dims": 4
+										}
+									]
+								}
+							}
+						}
+					}
+				}`,
+			expValidity: false,
+			errMsg:      `field: 'vecData', invalid alias (different dimensions 4 and 3)`,
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			im := NewIndexMapping()
+			err := im.UnmarshalJSON([]byte(test.mappingStr))
+			if err != nil {
+				t.Fatalf("failed to unmarshal index mapping: %v", err)
+			}
+
+			err = im.Validate()
+			isValid := err == nil
+			if test.expValidity != isValid {
+				t.Fatalf("validity mismatch, expected: %v, got: %v",
+					test.expValidity, isValid)
+			}
+
+			if !isValid && err.Error() != test.errMsg {
+				t.Fatalf("invalid error message, expected: %v, got: %v",
+					test.errMsg, err.Error())
+			}
+		})
+	}
+}
+
+// A test case for processVector function
+type vectorTest struct {
+	// Input
+
+	ipVec interface{} // input vector
+	dims  int         // dimensionality of input vector
+
+	// Expected Output
+
+	expValidity bool      // expected validity of the input
+	expOpVec    []float32 // expected output vector, given the input is valid
+}
+
+func TestProcessVector(t *testing.T) {
+	// Note: while creating vectors, we are using []any instead of []float32,
+	// this is done to enhance our test coverage.
+	// When we unmarshal a vector from a JSON, we get []any, not []float32.
+	tests := []vectorTest{
+		// # Flat vectors
+
+		// ## numeric cases
+		// (all numeric elements)
+		{[]any{1, 2.2, 3}, 3, true, []float32{1, 2.2, 3}}, // len==dims
+		{[]any{1, 2.2, 3}, 2, false, nil},                 // len>dims
+		{[]any{1, 2.2, 3}, 4, false, nil},                 // len<dims
+
+		// ## imposter cases
+		// (len==dims, some elements are non-numeric)
+		{[]any{1, 2, "three"}, 3, false, nil},    // string
+		{[]any{1, nil, 3}, 3, false, nil},        // nil
+		{[]any{nil, 1}, 2, false, nil},           // nil head
+		{[]any{1, 2, struct{}{}}, 3, false, nil}, // struct
+
+		// non-slice cases
+		// (vector is of types other than slice)
+		{nil, 1, false, nil},
+		{struct{}{}, 1, false, nil},
+		{1, 1, false, nil},
+
+		// # Nested vectors
+
+		// ## numeric cases
+		// (all numeric elements)
+		{[]any{[]any{1, 2, 3}, []any{4, 5, 6}}, 3, true,
+			[]float32{1, 2, 3, 4, 5, 6}}, // len==dims
+		{[]any{[]any{1, 2, 3}}, 3, true, []float32{1, 2, 3}}, // len==dims
+		{[]any{[]any{1, 2, 3}}, 4, false, nil},               // len>dims
+		{[]any{[]any{1, 2, 3}}, 2, false, nil},               // len<dims
+
+		// ## imposter cases
+		// some inner vectors are short
+		{[]any{[]any{1, 2, 3}, []any{4, 5}}, 3, false, nil},
+		// some inner vectors are long
+		{[]any{[]any{1, 2, 3}, []any{4, 5, 6, 7}}, 3, false, nil},
+		// contains string
+		{[]any{[]any{1, 2, "three"}, []any{4, 5, 6}}, 3, false, nil},
+		// contains nil
+		{[]any{[]any{1, 2, nil}, []any{4, 5, 6}}, 3, false, nil},
+
+		// non-slice cases (inner vectors)
+		{[]any{[]any{1, 2, 3}, nil}, 3, false, nil},        // nil
+		{[]any{nil, []any{1, 2, 3}}, 3, false, nil},        // nil head
+		{[]any{[]any{1, 2, 3}, struct{}{}}, 3, false, nil}, // struct
+		{[]any{[]any{1, 2, 3}, 4}, 3, false, nil},          // int
+	}
+
+	for _, test := range tests {
+		opVec, valid := processVector(test.ipVec, test.dims)
+
+		// check the validity of the input, as returned by processVector,
+		// against the expected validity.
+		if valid != test.expValidity {
+			t.Errorf("validity mismatch, ipVec:%v, dims:%v, expected:%v, got:%v",
+				test.ipVec, test.dims, test.expValidity, valid)
+			t.Fail()
+		}
+
+		// If input vector is valid, check the correctness of the output vector
+		// against the expected output vector.
+		if valid {
+			if len(opVec) != len(test.expOpVec) {
+				t.Errorf("output vector mismatch, ipVec:%v, dims:%v, "+
+					"expected:%v, got:%v", test.ipVec, test.dims, test.expOpVec,
+					opVec)
+				t.Fail()
+			}
+
+			for i := 0; i < len(opVec); i++ {
+				if opVec[i] != test.expOpVec[i] {
+					t.Errorf("output vector mismatch, ipVec:%v, dims:%v, "+
+						"expected:%v, got:%v", test.ipVec, test.dims, test.expOpVec,
+						opVec)
+					t.Fail()
+				}
+			}
+		}
+	}
+}
diff --git a/search_knn_test.go b/search_knn_test.go
index 769f3c8f..f182e60f 100644
--- a/search_knn_test.go
+++ b/search_knn_test.go
@@ -20,13 +20,13 @@ package bleve
 import (
 	"archive/zip"
 	"encoding/json"
-	"math"
 	"math/rand"
 	"strconv"
 	"testing"
 
 	"github.com/blevesearch/bleve/v2/analysis/lang/en"
 	"github.com/blevesearch/bleve/v2/mapping"
+	"github.com/blevesearch/bleve/v2/search"
 	"github.com/blevesearch/bleve/v2/search/query"
 	index "github.com/blevesearch/bleve_index_api"
 )
@@ -37,12 +37,117 @@ const testQueryFileName = "knn_queries.json"
 
 const testDatasetDims = 384
 
-func TestSimilaritySearchPartitionedIndexRandomized(t *testing.T) {
-	runKNNTest(t, true)
-}
+var knnOperators []knnOperator = []knnOperator{knnOperatorAnd, knnOperatorOr}
+
+func TestSimilaritySearchPartitionedIndex(t *testing.T) {
+	dataset, searchRequests, err := readDatasetAndQueries(testInputCompressedFile)
+	if err != nil {
+		t.Fatal(err)
+	}
+	documents := makeDatasetIntoDocuments(dataset)
+	contentFieldMapping := NewTextFieldMapping()
+	contentFieldMapping.Analyzer = en.AnalyzerName
+
+	vecFieldMappingL2 := mapping.NewVectorFieldMapping()
+	vecFieldMappingL2.Dims = testDatasetDims
+	vecFieldMappingL2.Similarity = index.EuclideanDistance
+
+	indexMappingL2Norm := NewIndexMapping()
+	indexMappingL2Norm.DefaultMapping.AddFieldMappingsAt("content", contentFieldMapping)
+	indexMappingL2Norm.DefaultMapping.AddFieldMappingsAt("vector", vecFieldMappingL2)
+
+	vecFieldMappingDot := mapping.NewVectorFieldMapping()
+	vecFieldMappingDot.Dims = testDatasetDims
+	vecFieldMappingDot.Similarity = index.CosineSimilarity
+
+	indexMappingDotProduct := NewIndexMapping()
+	indexMappingDotProduct.DefaultMapping.AddFieldMappingsAt("content", contentFieldMapping)
+	indexMappingDotProduct.DefaultMapping.AddFieldMappingsAt("vector", vecFieldMappingDot)
 
-func TestSimilaritySearchPartitionedIndexNotRandomized(t *testing.T) {
-	runKNNTest(t, false)
+	type testCase struct {
+		testType           string
+		queryIndex         int
+		numIndexPartitions int
+		mapping            mapping.IndexMapping
+	}
+
+	testCases := []testCase{
+		// l2 norm similarity
+		{
+			testType:           "multi_partition:match_none:oneKNNreq:k=3",
+			queryIndex:         0,
+			numIndexPartitions: 4,
+			mapping:            indexMappingL2Norm,
+		},
+		{
+			testType:           "multi_partition:match_none:oneKNNreq:k=2",
+			queryIndex:         0,
+			numIndexPartitions: 10,
+			mapping:            indexMappingL2Norm,
+		},
+		{
+			testType:           "multi_partition:match:oneKNNreq:k=2",
+			queryIndex:         1,
+			numIndexPartitions: 5,
+			mapping:            indexMappingL2Norm,
+		},
+		{
+			testType:           "multi_partition:disjunction:twoKNNreq:k=2,2",
+			queryIndex:         2,
+			numIndexPartitions: 4,
+			mapping:            indexMappingL2Norm,
+		},
+		// dot product similarity
+		{
+			testType:           "multi_partition:match_none:oneKNNreq:k=3",
+			queryIndex:         0,
+			numIndexPartitions: 4,
+			mapping:            indexMappingDotProduct,
+		},
+		{
+			testType:           "multi_partition:match_none:oneKNNreq:k=2",
+			queryIndex:         0,
+			numIndexPartitions: 10,
+			mapping:            indexMappingDotProduct,
+		},
+		{
+			testType:           "multi_partition:match:oneKNNreq:k=2",
+			queryIndex:         1,
+			numIndexPartitions: 5,
+			mapping:            indexMappingDotProduct,
+		},
+		{
+			testType:           "multi_partition:disjunction:twoKNNreq:k=2,2",
+			queryIndex:         2,
+			numIndexPartitions: 4,
+			mapping:            indexMappingDotProduct,
+		},
+	}
+
+	index := NewIndexAlias()
+	for testCaseNum, testCase := range testCases {
+		for _, operator := range knnOperators {
+			index.indexes = make([]Index, 0)
+			query := searchRequests[testCase.queryIndex]
+			query.AddKNNOperator(operator)
+
+			indexPaths := createPartitionedIndex(documents, index, 1, testCase.mapping, t)
+			controlResult, err := index.Search(query)
+			if err != nil {
+				t.Fatal(err)
+			}
+			cleanUp(t, indexPaths, index.indexes...)
+
+			index.indexes = make([]Index, 0)
+			indexPaths = createPartitionedIndex(documents, index, testCase.numIndexPartitions, testCase.mapping, t)
+			experimentalResult, err := index.Search(query)
+			if err != nil {
+				t.Fatal(err)
+			}
+			verifyResult(t, controlResult, experimentalResult, testCaseNum, true)
+			cleanUp(t, indexPaths, index.indexes...)
+		}
+	}
 }
 
 type testDocument struct {
@@ -186,73 +291,69 @@ func createMultipleSegmentsIndex(documents []map[string]interface{}, index Index
 	return nil
 }
 
-// Fisher-Yates shuffle
-func shuffleDocuments(documents []map[string]interface{}) []map[string]interface{} {
-	for i := range documents {
-		j := i + rand.Intn(len(documents)-i)
-		documents[i], documents[j] = documents[j], documents[i]
-	}
-	return documents
-}
-
 func truncateScore(score float64) float64 {
 	return float64(int(score*1e6)) / 1e6
 }
 
-type testResult struct {
-	score          float64
-	scoreBreakdown map[int]float64
-}
-
-func verifyResult(t *testing.T, actualResult *SearchResult, expectedResult map[string]testResult, randomizeDocuments bool, testCaseNum int, skipScoreCheck bool) {
-	if len(actualResult.Hits) != len(expectedResult) {
-		t.Fatalf("testcase %d failed: expected %d results, got %d", testCaseNum, len(expectedResult), len(actualResult.Hits))
+func verifyResult(t *testing.T, controlResult *SearchResult, experimentalResult *SearchResult, testCaseNum int, verifyOnlyDocIDs bool) {
+	if len(controlResult.Hits) != len(experimentalResult.Hits) {
+		t.Fatalf("testcase %d failed: expected %d results, got %d", testCaseNum, len(controlResult.Hits), len(experimentalResult.Hits))
 	}
-	if skipScoreCheck {
-		return
+	if controlResult.Total != experimentalResult.Total {
+		t.Errorf("test case #%d: expected total hits to be %d, got %d", testCaseNum, controlResult.Total, experimentalResult.Total)
 	}
-	for i, hit := range actualResult.Hits {
-		var expectedHit testResult
-		var ok bool
-		if expectedHit, ok = expectedResult[hit.ID]; !ok {
-			t.Fatalf("testcase %d failed: unexpected result %s", testCaseNum, hit.ID)
+	if verifyOnlyDocIDs {
+		// in multi partitioned index, we cannot be sure of the score or the ordering of the hits as the tf-idf scores are localized to each partition
+		// so we only check the ids
+		controlMap := make(map[string]struct{})
+		experimentalMap := make(map[string]struct{})
+		for _, hit := range controlResult.Hits {
+			controlMap[hit.ID] = struct{}{}
 		}
-		// Truncate to 6 decimal places
-		actualScore := truncateScore(hit.Score)
-		expectScore := truncateScore(expectedHit.score)
-		if !randomizeDocuments && expectScore != actualScore {
-			t.Fatalf("testcase %d failed: expected hit %d to have score %f, got %f", testCaseNum, i, expectedHit.score, hit.Score)
+		for _, hit := range experimentalResult.Hits {
+			experimentalMap[hit.ID] = struct{}{}
 		}
-		if len(hit.ScoreBreakdown) != len(expectedHit.scoreBreakdown) {
-			t.Fatalf("testcase %d failed: expected hit %d to have %d score breakdowns, got %d", testCaseNum, i, len(expectedHit.scoreBreakdown), len(hit.ScoreBreakdown))
+		if len(controlMap) != len(experimentalMap) {
+			t.Fatalf("testcase %d failed: expected %d results, got %d", testCaseNum, len(controlMap), len(experimentalMap))
 		}
-		if !randomizeDocuments {
-			actualScore := truncateScore(hit.ScoreBreakdown[0])
-			expectScore := truncateScore(expectedHit.scoreBreakdown[0])
-			if expectScore != actualScore {
-				t.Fatalf("testcase %d failed: expected hit %d to have score breakdown %f, got %f", testCaseNum, i, expectedHit.scoreBreakdown[0], hit.ScoreBreakdown[0])
+		for id := range controlMap {
+			if _, ok := experimentalMap[id]; !ok {
+				t.Fatalf("testcase %d failed: expected id %s to be in experimental result", testCaseNum, id)
 			}
 		}
-		for j := 1; j < len(hit.ScoreBreakdown); j++ {
-			// Truncate to 6 decimal places
-			actualScore := truncateScore(hit.ScoreBreakdown[j])
-			expectScore := truncateScore(expectedHit.scoreBreakdown[j])
-			if expectScore != actualScore {
-				t.Fatalf("testcase %d failed: expected hit %d to have score breakdown %f, got %f", testCaseNum, i, expectedHit.scoreBreakdown[j], hit.ScoreBreakdown[j])
-			}
+		return
+	}
+
+	for i := 0; i < len(controlResult.Hits); i++ {
+		if controlResult.Hits[i].ID != experimentalResult.Hits[i].ID {
+			t.Fatalf("testcase %d failed: expected hit %d to have id %s, got %s", testCaseNum, i, controlResult.Hits[i].ID, experimentalResult.Hits[i].ID)
+		}
+		// Truncate to 6 decimal places
+		actualScore := truncateScore(experimentalResult.Hits[i].Score)
+		expectScore := truncateScore(controlResult.Hits[i].Score)
+		if expectScore != actualScore {
+			t.Fatalf("testcase %d failed: expected hit %d to have score %f, got %f", testCaseNum, i, expectScore, actualScore)
 		}
 	}
-}
+	if truncateScore(controlResult.MaxScore) != truncateScore(experimentalResult.MaxScore) {
+		t.Errorf("test case #%d: expected maxScore to be %f, got %f", testCaseNum, controlResult.MaxScore, experimentalResult.MaxScore)
+	}
 
-func runKNNTest(t *testing.T, randomizeDocuments bool) {
+}
+func TestSimilaritySearchMultipleSegments(t *testing.T) {
+	// to run this test you must first add the line
+	// 				return nil
+	// in the scorch.go file just before these two lines
+	// 				s.asyncTasks.Add(1)
+	//				go s.mergerLoop()
+	// this is to prevent the merger from running and merging the segments
+	// before we can test the search on multiple segments
 	dataset, searchRequests, err := readDatasetAndQueries(testInputCompressedFile)
 	if err != nil {
 		t.Fatal(err)
 	}
 	documents := makeDatasetIntoDocuments(dataset)
-	if randomizeDocuments {
-		documents = shuffleDocuments(documents)
-	}
+
 	contentFieldMapping := NewTextFieldMapping()
 	contentFieldMapping.Analyzer = en.AnalyzerName
 
@@ -260,780 +361,181 @@ func runKNNTest(t *testing.T, randomizeDocuments bool) {
 	vecFieldMappingL2.Dims = testDatasetDims
 	vecFieldMappingL2.Similarity = index.EuclideanDistance
 
-	indexMappingL2Norm := NewIndexMapping()
-	indexMappingL2Norm.DefaultMapping.AddFieldMappingsAt("content", contentFieldMapping)
-	indexMappingL2Norm.DefaultMapping.AddFieldMappingsAt("vector", vecFieldMappingL2)
-
 	vecFieldMappingDot := mapping.NewVectorFieldMapping()
 	vecFieldMappingDot.Dims = testDatasetDims
 	vecFieldMappingDot.Similarity = index.CosineSimilarity
 
+	indexMappingL2Norm := NewIndexMapping()
+	indexMappingL2Norm.DefaultMapping.AddFieldMappingsAt("content", contentFieldMapping)
+	indexMappingL2Norm.DefaultMapping.AddFieldMappingsAt("vector", vecFieldMappingL2)
+
 	indexMappingDotProduct := NewIndexMapping()
 	indexMappingDotProduct.DefaultMapping.AddFieldMappingsAt("content", contentFieldMapping)
-	indexMappingDotProduct.DefaultMapping.AddFieldMappingsAt("vector", vecFieldMappingDot)
-
-	type testCase struct {
-		testType           string
-		queryIndex         int
-		numIndexPartitions int
-		mapping            mapping.IndexMapping
-		expectedResults    map[string]testResult
-	}
+	indexMappingDotProduct.DefaultMapping.AddFieldMappingsAt("vector", vecFieldMappingL2)
 
-	testCases := []testCase{
-		// l2 norm similarity
-		{
-			testType:           "single_partition:match_none:oneKNNreq:k=3",
-			queryIndex:         0,
-			numIndexPartitions: 1,
-			mapping:            indexMappingL2Norm,
-			expectedResults: map[string]testResult{
-				"doc29": {
-					score:          0.5547758085810349,
-					scoreBreakdown: map[int]float64{1: 1.1095516171620698},
-				},
-				"doc23": {
-					score:          0.3817633037007331,
-					scoreBreakdown: map[int]float64{1: 0.7635266074014662},
-				},
-				"doc28": {
-					score:          0.33983667469689355,
-					scoreBreakdown: map[int]float64{1: 0.6796733493937871},
-				},
-			},
-		},
-		{
-			testType:           "multi_partition:match_none:oneKNNreq:k=3",
-			queryIndex:         0,
-			numIndexPartitions: 4,
-			mapping:            indexMappingL2Norm,
-			expectedResults: map[string]testResult{
-				"doc29": {
-					score:          0.5547758085810349,
-					scoreBreakdown: map[int]float64{1: 1.1095516171620698},
-				},
-				"doc23": {
-					score:          0.3817633037007331,
-					scoreBreakdown: map[int]float64{1: 0.7635266074014662},
-				},
-				"doc28": {
-					score:          0.33983667469689355,
-					scoreBreakdown: map[int]float64{1: 0.6796733493937871},
-				},
-			},
-		},
-		{
-			testType:           "multi_partition:match_none:oneKNNreq:k=2",
-			queryIndex:         0,
-			numIndexPartitions: 10,
-			mapping:            indexMappingL2Norm,
-			expectedResults: map[string]testResult{
-				"doc29": {
-					score:          0.5547758085810349,
-					scoreBreakdown: map[int]float64{1: 1.1095516171620698},
-				},
-				"doc23": {
-					score:          0.3817633037007331,
-					scoreBreakdown: map[int]float64{1: 0.7635266074014662},
-				},
-				"doc28": {
-					score:          0.33983667469689355,
-					scoreBreakdown: map[int]float64{1: 0.6796733493937871},
-				},
-			},
-		},
-		{
-			testType:           "single_partition:match:oneKNNreq:k=2",
-			queryIndex:         1,
-			numIndexPartitions: 1,
-			mapping:            indexMappingL2Norm,
-			expectedResults: map[string]testResult{
-				"doc29": {
-					score:          1.8859816084399936,
-					scoreBreakdown: map[int]float64{0: 0.7764299912779237, 1: 1.1095516171620698},
-				},
-				"doc23": {
-					score:          1.8615644255330264,
-					scoreBreakdown: map[int]float64{0: 1.0980378181315602, 1: 0.7635266074014662},
-				},
-				"doc27": {
-					score:          0.4640056648691007,
-					scoreBreakdown: map[int]float64{0: 0.9280113297382014},
-				},
-				"doc28": {
-					score:          0.434037555556026,
-					scoreBreakdown: map[int]float64{0: 0.868075111112052},
-				},
-				"doc30": {
-					score:          0.38821499563896184,
-					scoreBreakdown: map[int]float64{0: 0.7764299912779237},
-				},
-				"doc24": {
-					score:          0.38821499563896184,
-					scoreBreakdown: map[int]float64{0: 0.7764299912779237},
-				},
-			},
-		},
-		{
-			testType:           "multi_partition:match:oneKNNreq:k=2",
-			queryIndex:         1,
-			numIndexPartitions: 5,
-			mapping:            indexMappingL2Norm,
-			expectedResults: map[string]testResult{
-				"doc23": {
-					score:          1.5207250366637521,
-					scoreBreakdown: map[int]float64{0: 0.7571984292622859, 1: 0.7635266074014662},
-				},
-				"doc29": {
-					score:          1.4834345192674083,
-					scoreBreakdown: map[int]float64{0: 0.3738829021053385, 1: 1.1095516171620698},
-				},
-				"doc24": {
-					score:          0.2677100734235977,
-					scoreBreakdown: map[int]float64{0: 0.5354201468471954},
-				},
-				"doc27": {
-					score:          0.22343776840593196,
-					scoreBreakdown: map[int]float64{0: 0.4468755368118639},
-				},
-				"doc28": {
-					score:          0.20900689401100958,
-					scoreBreakdown: map[int]float64{0: 0.41801378802201916},
-				},
-				"doc30": {
-					score:          0.18694145105266924,
-					scoreBreakdown: map[int]float64{0: 0.3738829021053385},
-				},
-			},
-		},
-		{
-			testType:           "single_partition:disjunction:twoKNNreq:k=2,2",
-			queryIndex:         2,
-			numIndexPartitions: 1,
-			mapping:            indexMappingL2Norm,
-			expectedResults: map[string]testResult{
-				"doc7": {
-					score:          math.MaxFloat64 / 3.0,
-					scoreBreakdown: map[int]float64{2: math.MaxFloat64},
-				},
-				"doc29": {
-					score:          0.6774608026082964,
-					scoreBreakdown: map[int]float64{0: 0.23161973134064517, 1: 0.7845714725717996},
-				},
-				"doc23": {
-					score:          0.5783030702431613,
-					scoreBreakdown: map[int]float64{0: 0.32755976365480655, 1: 0.5398948417099355},
-				},
-				"doc3": {
-					score:          0.2550334160459894,
-					scoreBreakdown: map[int]float64{0: 0.7651002481379682},
-				},
-				"doc13": {
-					score:          0.2208654210738964,
-					scoreBreakdown: map[int]float64{0: 0.6625962632216892},
-				},
-				"doc5": {
-					score:          0.21180931116413285,
-					scoreBreakdown: map[int]float64{2: 0.6354279334923986},
-				},
-				"doc27": {
-					score:          0.09227950890170131,
-					scoreBreakdown: map[int]float64{0: 0.2768385267051039},
-				},
-				"doc28": {
-					score:          0.0863195764709126,
-					scoreBreakdown: map[int]float64{0: 0.2589587294127378},
-				},
-				"doc30": {
-					score:          0.07720657711354839,
-					scoreBreakdown: map[int]float64{0: 0.23161973134064517},
-				},
-				"doc24": {
-					score:          0.07720657711354839,
-					scoreBreakdown: map[int]float64{0: 0.23161973134064517},
-				},
-			},
-		},
-		{
-			testType:           "multi_partition:disjunction:twoKNNreq:k=2,2",
-			queryIndex:         2,
-			numIndexPartitions: 4,
-			mapping:            indexMappingL2Norm,
-			expectedResults: map[string]testResult{
-				"doc7": {
-					score:          math.MaxFloat64 / 3.0,
-					scoreBreakdown: map[int]float64{2: math.MaxFloat64},
-				},
-				"doc29": {
-					score:          0.567426591648309,
-					scoreBreakdown: map[int]float64{0: 0.06656841490066398, 1: 0.7845714725717996},
-				},
-				"doc23": {
-					score:          0.5639255136185979,
-					scoreBreakdown: map[int]float64{0: 0.3059934287179615, 1: 0.5398948417099355},
-				},
-				"doc5": {
-					score:          0.21180931116413285,
-					scoreBreakdown: map[int]float64{2: 0.6354279334923986},
-				},
-				"doc3": {
-					score:          0.14064944169372873,
-					scoreBreakdown: map[int]float64{0: 0.4219483250811862},
-				},
-				"doc13": {
-					score:          0.12180599172106943,
-					scoreBreakdown: map[int]float64{0: 0.3654179751632083},
-				},
-				"doc27": {
-					score:          0.026521491065731144,
-					scoreBreakdown: map[int]float64{0: 0.07956447319719343},
-				},
-				"doc28": {
-					score:          0.024808583220893122,
-					scoreBreakdown: map[int]float64{0: 0.07442574966267937},
-				},
-				"doc30": {
-					score:          0.02218947163355466,
-					scoreBreakdown: map[int]float64{0: 0.06656841490066398},
-				},
-				"doc24": {
-					score:          0.02218947163355466,
-					scoreBreakdown: map[int]float64{0: 0.06656841490066398},
-				},
-			},
-		},
-		{
-			// control:
-			// from = 0
-			// size = 8
-			testType:           "pagination",
-			queryIndex:         3,
-			numIndexPartitions: 4,
-			mapping:            indexMappingL2Norm,
-			expectedResults: map[string]testResult{
-				"doc24": {
-					score:          1.22027994094805,
-					scoreBreakdown: map[int]float64{0: 0.027736154383370196, 1: 0.3471022633855392, 2: 0.5085619451465123, 3: 0.33687957803262836},
-				},
-				"doc17": {
-					score:          0.7851856993753307,
-					scoreBreakdown: map[int]float64{0: 0.3367753689069724, 2: 0.3892791754255179, 3: 0.320859721501284},
-				},
-				"doc21": {
-					score:          0.5927148028393034,
-					scoreBreakdown: map[int]float64{0: 0.06974846263723515, 2: 0.3914133076090359, 3: 0.3291246335394669},
-				},
-				"doc14": {
-					score:          0.45680756875853035,
-					scoreBreakdown: map[int]float64{0: 0.5968461853543279, 3: 0.31676895216273276},
-				},
-				"doc25": {
-					score:          0.292014972318407,
-					scoreBreakdown: map[int]float64{0: 0.17861510907524708, 2: 0.405414835561567},
-				},
-				"doc23": {
-					score:          0.24706850662359503,
-					scoreBreakdown: map[int]float64{0: 0.09761951136424651, 2: 0.39651750188294355},
-				},
-				"doc15": {
-					score:          0.24489276164017085,
-					scoreBreakdown: map[int]float64{0: 0.17216818679645968, 3: 0.317617336483882},
-				},
-				"doc5": {
-					score:          0.10331722282971788,
-					scoreBreakdown: map[int]float64{1: 0.4132688913188715},
-				},
-			},
-		},
+	testCases := []struct {
+		numSegments int
+		queryIndex  int
+		mapping     mapping.IndexMapping
+		scoreValue  string
+	}{
+		// // L2 norm similarity
 		{
-			// experimental:
-			// from = 0
-			// size = 3
-			testType:           "pagination",
-			queryIndex:         4,
-			numIndexPartitions: 4,
-			mapping:            indexMappingL2Norm,
-			expectedResults: map[string]testResult{
-				"doc24": {
-					score:          1.22027994094805,
-					scoreBreakdown: map[int]float64{0: 0.027736154383370196, 1: 0.3471022633855392, 2: 0.5085619451465123, 3: 0.33687957803262836},
-				},
-				"doc17": {
-					score:          0.7851856993753307,
-					scoreBreakdown: map[int]float64{0: 0.3367753689069724, 2: 0.3892791754255179, 3: 0.320859721501284},
-				},
-				"doc21": {
-					score:          0.5927148028393034,
-					scoreBreakdown: map[int]float64{0: 0.06974846263723515, 2: 0.3914133076090359, 3: 0.3291246335394669},
-				},
-			},
+			numSegments: 6,
+			queryIndex:  0,
+			mapping:     indexMappingL2Norm,
 		},
 		{
-			// from = 3
-			// size = 3
-			testType:           "pagination",
-			queryIndex:         5,
-			numIndexPartitions: 4,
-			mapping:            indexMappingL2Norm,
-			expectedResults: map[string]testResult{
-				"doc14": {
-					score:          0.45680756875853035,
-					scoreBreakdown: map[int]float64{0: 0.5968461853543279, 3: 0.31676895216273276},
-				},
-				"doc25": {
-					score:          0.292014972318407,
-					scoreBreakdown: map[int]float64{0: 0.17861510907524708, 2: 0.405414835561567},
-				},
-				"doc23": {
-					score:          0.24706850662359503,
-					scoreBreakdown: map[int]float64{0: 0.09761951136424651, 2: 0.39651750188294355},
-				},
-			},
+			numSegments: 7,
+			queryIndex:  1,
+			mapping:     indexMappingL2Norm,
 		},
-		// dot product similarity
 		{
-			testType:           "single_partition:match_none:oneKNNreq:k=3",
-			queryIndex:         0,
-			numIndexPartitions: 1,
-			mapping:            indexMappingDotProduct,
-			expectedResults: map[string]testResult{
-				"doc29": {
-					score:          0.2746838331222534,
-					scoreBreakdown: map[int]float64{1: 0.5493676662445068},
-				},
-				"doc23": {
-					score:          0.17257216572761536,
-					scoreBreakdown: map[int]float64{1: 0.3451443314552307},
-				},
-				"doc28": {
-					score:          0.13217630982398987,
-					scoreBreakdown: map[int]float64{1: 0.26435261964797974},
-				},
-			},
+			numSegments: 8,
+			queryIndex:  2,
+			mapping:     indexMappingL2Norm,
 		},
 		{
-			testType:           "multi_partition:match_none:oneKNNreq:k=3",
-			queryIndex:         0,
-			numIndexPartitions: 4,
-			mapping:            indexMappingDotProduct,
-			expectedResults: map[string]testResult{
-				"doc29": {
-					score:          0.2746838331222534,
-					scoreBreakdown: map[int]float64{1: 0.5493676662445068},
-				},
-				"doc23": {
-					score:          0.17257216572761536,
-					scoreBreakdown: map[int]float64{1: 0.3451443314552307},
-				},
-				"doc28": {
-					score:          0.13217630982398987,
-					scoreBreakdown: map[int]float64{1: 0.26435261964797974},
-				},
-			},
+			numSegments: 9,
+			queryIndex:  3,
+			mapping:     indexMappingL2Norm,
 		},
 		{
-			testType:           "multi_partition:match_none:oneKNNreq:k=2",
-			queryIndex:         0,
-			numIndexPartitions: 10,
-			mapping:            indexMappingDotProduct,
-			expectedResults: map[string]testResult{
-				"doc29": {
-					score:          0.2746838331222534,
-					scoreBreakdown: map[int]float64{1: 0.5493676662445068},
-				},
-				"doc23": {
-					score:          0.17257216572761536,
-					scoreBreakdown: map[int]float64{1: 0.3451443314552307},
-				},
-				"doc28": {
-					score:          0.13217630982398987,
-					scoreBreakdown: map[int]float64{1: 0.26435261964797974},
-				},
-			},
+			numSegments: 10,
+			queryIndex:  4,
+			mapping:     indexMappingL2Norm,
 		},
 		{
-			testType:           "single_partition:match:oneKNNreq:k=2",
-			queryIndex:         1,
-			numIndexPartitions: 1,
-			mapping:            indexMappingDotProduct,
-			expectedResults: map[string]testResult{
-				"doc23": {
-					score:          1.443182149586791,
-					scoreBreakdown: map[int]float64{0: 1.0980378181315602, 1: 0.3451443314552307},
-				},
-				"doc29": {
-					score:          1.3257976575224304,
-					scoreBreakdown: map[int]float64{0: 0.7764299912779237, 1: 0.5493676662445068},
-				},
-				"doc27": {
-					score:          0.4640056648691007,
-					scoreBreakdown: map[int]float64{0: 0.9280113297382014},
-				},
-				"doc28": {
-					score:          0.434037555556026,
-					scoreBreakdown: map[int]float64{0: 0.868075111112052},
-				},
-				"doc30": {
-					score:          0.38821499563896184,
-					scoreBreakdown: map[int]float64{0: 0.7764299912779237},
-				},
-				"doc24": {
-					score:          0.38821499563896184,
-					scoreBreakdown: map[int]float64{0: 0.7764299912779237},
-				},
-			},
+			numSegments: 11,
+			queryIndex:  5,
+			mapping:     indexMappingL2Norm,
 		},
+		// dot_product similarity
 		{
-			testType:           "multi_partition:match:oneKNNreq:k=2",
-			queryIndex:         1,
-			numIndexPartitions: 5,
-			mapping:            indexMappingDotProduct,
-			expectedResults: map[string]testResult{
-				"doc23": {
-					score:          1.1023427607175167,
-					scoreBreakdown: map[int]float64{0: 0.7571984292622859, 1: 0.3451443314552307},
-				},
-				"doc29": {
-					score:          0.9232505683498453,
-					scoreBreakdown: map[int]float64{0: 0.3738829021053385, 1: 0.5493676662445068},
-				},
-				"doc24": {
-					score:          0.2677100734235977,
-					scoreBreakdown: map[int]float64{0: 0.5354201468471954},
-				},
-				"doc27": {
-					score:          0.22343776840593196,
-					scoreBreakdown: map[int]float64{0: 0.4468755368118639},
-				},
-				"doc28": {
-					score:          0.20900689401100958,
-					scoreBreakdown: map[int]float64{0: 0.41801378802201916},
-				},
-				"doc30": {
-					score:          0.18694145105266924,
-					scoreBreakdown: map[int]float64{0: 0.3738829021053385},
-				},
-			},
+			numSegments: 6,
+			queryIndex:  0,
+			mapping:     indexMappingDotProduct,
 		},
 		{
-			testType:           "single_partition:disjunction:twoKNNreq:k=2,2",
-			queryIndex:         2,
-			numIndexPartitions: 1,
-			mapping:            indexMappingDotProduct,
-			expectedResults: map[string]testResult{
-				"doc29": {
-					score:          0.4133875556711759,
-					scoreBreakdown: map[int]float64{0: 0.23161973134064517, 1: 0.38846160216611875},
-				},
-				"doc23": {
-					score:          0.3810757739432651,
-					scoreBreakdown: map[int]float64{0: 0.32755976365480655, 1: 0.24405389726009102},
-				},
-				"doc3": {
-					score:          0.2550334160459894,
-					scoreBreakdown: map[int]float64{0: 0.7651002481379682},
-				},
-				"doc7": {
-					score:          0.23570219015076832,
-					scoreBreakdown: map[int]float64{2: 0.707106570452305},
-				},
-				"doc13": {
-					score:          0.2208654210738964,
-					scoreBreakdown: map[int]float64{0: 0.6625962632216892},
-				},
-				"doc5": {
-					score:          0.10455702372648192,
-					scoreBreakdown: map[int]float64{2: 0.31367107117944576},
-				},
-				"doc27": {
-					score:          0.09227950890170131,
-					scoreBreakdown: map[int]float64{0: 0.2768385267051039},
-				},
-				"doc28": {
-					score:          0.0863195764709126,
-					scoreBreakdown: map[int]float64{0: 0.2589587294127378},
-				},
-				"doc30": {
-					score:          0.07720657711354839,
-					scoreBreakdown: map[int]float64{0: 0.23161973134064517},
-				},
-				"doc24": {
-					score:          0.07720657711354839,
-					scoreBreakdown: map[int]float64{0: 0.23161973134064517},
-				},
-			},
+			numSegments: 7,
+			queryIndex:  1,
+			mapping:     indexMappingDotProduct,
 		},
 		{
-			testType:           "multi_partition:disjunction:twoKNNreq:k=2,2",
-			queryIndex:         2,
-			numIndexPartitions: 4,
-			mapping:            indexMappingDotProduct,
-			expectedResults: map[string]testResult{
-				"doc23": {
-					score:          0.36669821731870167,
-					scoreBreakdown: map[int]float64{0: 0.3059934287179615, 1: 0.24405389726009102},
-				},
-				"doc29": {
-					score:          0.30335334471118847,
-					scoreBreakdown: map[int]float64{0: 0.06656841490066398, 1: 0.38846160216611875},
-				},
-				"doc7": {
-					score:          0.23570219015076832,
-					scoreBreakdown: map[int]float64{2: 0.707106570452305},
-				},
-				"doc3": {
-					score:          0.14064944169372873,
-					scoreBreakdown: map[int]float64{0: 0.4219483250811862},
-				},
-				"doc13": {
-					score:          0.12180599172106943,
-					scoreBreakdown: map[int]float64{0: 0.3654179751632083},
-				},
-				"doc5": {
-					score:          0.10455702372648192,
-					scoreBreakdown: map[int]float64{2: 0.31367107117944576},
-				},
-				"doc27": {
-					score:          0.026521491065731144,
-					scoreBreakdown: map[int]float64{0: 0.07956447319719343},
-				},
-				"doc28": {
-					score:          0.024808583220893122,
-					scoreBreakdown: map[int]float64{0: 0.07442574966267937},
-				},
-				"doc30": {
-					score:          0.02218947163355466,
-					scoreBreakdown: map[int]float64{0: 0.06656841490066398},
-				},
-				"doc24": {
-					score:          0.02218947163355466,
-					scoreBreakdown: map[int]float64{0: 0.06656841490066398},
-				},
-			},
+			numSegments: 8,
+			queryIndex:  2,
+			mapping:     indexMappingDotProduct,
 		},
 		{
-			// control:
-			// from = 0
-			// size = 8
-			testType:           "pagination",
-			queryIndex:         3,
-			numIndexPartitions: 4,
-			mapping:            indexMappingDotProduct,
-			expectedResults: map[string]testResult{
-				"doc24": {
-					score:          0.45716299530348536,
-					scoreBreakdown: map[int]float64{0: 0.027736154383370196, 1: 0.09718442379837483, 2: 0.24962871627005187, 3: 0.08261370085168844},
-				},
-				"doc17": {
-					score:          0.40792221729717437,
-					scoreBreakdown: map[int]float64{0: 0.3367753689069724, 2: 0.14920840064225363, 3: 0.05791252018033977},
-				},
-				"doc14": {
-					score:          0.3240253778614369,
-					scoreBreakdown: map[int]float64{0: 0.5968461853543279, 3: 0.05120457036854595},
-				},
-				"doc21": {
-					score:          0.2191859820036201,
-					scoreBreakdown: map[int]float64{0: 0.06974846263723515, 2: 0.1515430653633034, 3: 0.0709564480042883},
-				},
-				"doc25": {
-					score:          0.1724318546817751,
-					scoreBreakdown: map[int]float64{0: 0.17861510907524708, 2: 0.16624860028830313},
-				},
-				"doc23": {
-					score:          0.12732176553007302,
-					scoreBreakdown: map[int]float64{0: 0.09761951136424651, 2: 0.15702401969589952},
-				},
-				"doc15": {
-					score:          0.11238897955499198,
-					scoreBreakdown: map[int]float64{0: 0.17216818679645968, 3: 0.052609772313524296},
-				},
-				"doc20": {
-					score:          0.06759830898809112,
-					scoreBreakdown: map[int]float64{0: 0.2703932359523645},
-				},
-			},
+			numSegments: 9,
+			queryIndex:  3,
+			mapping:     indexMappingDotProduct,
 		},
 		{
-			// experimental:
-			// from = 0
-			// size = 3
-			testType:           "pagination",
-			queryIndex:         4,
-			numIndexPartitions: 4,
-			mapping:            indexMappingDotProduct,
-			expectedResults: map[string]testResult{
-				"doc24": {
-					score:          0.45716299530348536,
-					scoreBreakdown: map[int]float64{0: 0.027736154383370196, 1: 0.09718442379837483, 2: 0.24962871627005187, 3: 0.08261370085168844},
-				},
-				"doc17": {
-					score:          0.40792221729717437,
-					scoreBreakdown: map[int]float64{0: 0.3367753689069724, 2: 0.14920840064225363, 3: 0.05791252018033977},
-				},
-				"doc14": {
-					score:          0.3240253778614369,
-					scoreBreakdown: map[int]float64{0: 0.5968461853543279, 3: 0.05120457036854595},
-				},
-			},
+			numSegments: 10,
+			queryIndex:  4,
+			mapping:     indexMappingDotProduct,
 		},
 		{
-			// from = 3
-			// size = 3
-			testType:           "pagination",
-			queryIndex:         5,
-			numIndexPartitions: 4,
-			mapping:            indexMappingDotProduct,
-			expectedResults: map[string]testResult{
-				"doc21": {
-					score:          0.2191859820036201,
-					scoreBreakdown: map[int]float64{0: 0.06974846263723515, 2: 0.1515430653633034, 3: 0.0709564480042883},
-				},
-				"doc25": {
-					score:          0.1724318546817751,
-					scoreBreakdown: map[int]float64{0: 0.17861510907524708, 2: 0.16624860028830313},
-				},
-				"doc23": {
-					score:          0.12732176553007302,
-					scoreBreakdown: map[int]float64{0: 0.09761951136424651, 2: 0.15702401969589952},
-				},
-			},
+			numSegments: 11,
+			queryIndex:  5,
+			mapping:     indexMappingDotProduct,
 		},
-	}
-
-	index := NewIndexAlias()
-	for testCaseNum, testCase := range testCases {
-		index.indexes = make([]Index, 0)
-		indexPaths := createPartitionedIndex(documents, index, testCase.numIndexPartitions, testCase.mapping, t)
-		query := searchRequests[testCase.queryIndex]
-		res, err := index.Search(query)
-		if err != nil {
-			t.Fatal(err)
-		}
-		// pagination test case -> scores are not deterministic
-		skipScoreCheck := testCase.testType == "pagination"
-		verifyResult(t, res, testCase.expectedResults, randomizeDocuments, testCaseNum, skipScoreCheck)
-		cleanUp(t, indexPaths, index.indexes...)
-	}
-}
-
-func getExpectedResultFromSearchResult(res *SearchResult) map[string]testResult {
-	rv := make(map[string]testResult)
-	for _, hit := range res.Hits {
-		rv[hit.ID] = testResult{
-			score:          hit.Score,
-			scoreBreakdown: hit.ScoreBreakdown,
-		}
-	}
-	return rv
-}
-func TestSimilaritySearchMultipleSegments(t *testing.T) {
-	dataset, searchRequests, err := readDatasetAndQueries(testInputCompressedFile)
-	if err != nil {
-		t.Fatal(err)
-	}
-	documents := makeDatasetIntoDocuments(dataset)
-
-	contentFieldMapping := NewTextFieldMapping()
-	contentFieldMapping.Analyzer = en.AnalyzerName
-
-	vecFieldMappingL2 := mapping.NewVectorFieldMapping()
-	vecFieldMappingL2.Dims = testDatasetDims
-	vecFieldMappingL2.Similarity = index.EuclideanDistance
-
-	vecFieldMappingDot := mapping.NewVectorFieldMapping()
-	vecFieldMappingDot.Dims = testDatasetDims
-	vecFieldMappingDot.Similarity = index.CosineSimilarity
-
-	indexMappingL2Norm := NewIndexMapping()
-	indexMappingL2Norm.DefaultMapping.AddFieldMappingsAt("content", contentFieldMapping)
-	indexMappingL2Norm.DefaultMapping.AddFieldMappingsAt("vector", vecFieldMappingL2)
-
-	indexMappingDotProduct := NewIndexMapping()
-	indexMappingDotProduct.DefaultMapping.AddFieldMappingsAt("content", contentFieldMapping)
-	indexMappingDotProduct.DefaultMapping.AddFieldMappingsAt("vector", vecFieldMappingL2)
-
-	testCases := []struct {
-		numSegments int
-		queryIndex  int
-		mapping     mapping.IndexMapping
-	}{
-		// L2 norm similarity
+		// score none test
 		{
-			numSegments: 6,
+			numSegments: 3,
 			queryIndex:  0,
 			mapping:     indexMappingL2Norm,
+			scoreValue:  "none",
 		},
 		{
 			numSegments: 7,
 			queryIndex:  1,
 			mapping:     indexMappingL2Norm,
+			scoreValue:  "none",
 		},
 		{
 			numSegments: 8,
 			queryIndex:  2,
 			mapping:     indexMappingL2Norm,
+			scoreValue:  "none",
 		},
 		{
-			numSegments: 6,
+			numSegments: 3,
 			queryIndex:  0,
 			mapping:     indexMappingDotProduct,
+			scoreValue:  "none",
 		},
 		{
 			numSegments: 7,
 			queryIndex:  1,
 			mapping:     indexMappingDotProduct,
+			scoreValue:  "none",
 		},
 		{
 			numSegments: 8,
 			queryIndex:  2,
 			mapping:     indexMappingDotProduct,
+			scoreValue:  "none",
 		},
 	}
 	for testCaseNum, testCase := range testCases {
-		// run single segment test first
-		tmpIndexPath := createTmpIndexPath(t)
-		index, err := New(tmpIndexPath, testCase.mapping)
-		if err != nil {
-			t.Fatal(err)
-		}
-		query := searchRequests[testCase.queryIndex]
-		err = createMultipleSegmentsIndex(documents, index, 1)
-		if err != nil {
-			t.Fatal(err)
-		}
-		res, err := index.Search(query)
-		if err != nil {
-			t.Fatal(err)
-		}
-		expectedResult := getExpectedResultFromSearchResult(res)
-		err = index.Close()
-		if err != nil {
-			t.Fatal(err)
-		}
-		cleanupTmpIndexPath(t, tmpIndexPath)
+		for _, operator := range knnOperators {
+			// run single segment test first
+			tmpIndexPath := createTmpIndexPath(t)
+			index, err := New(tmpIndexPath, testCase.mapping)
+			if err != nil {
+				t.Fatal(err)
+			}
+			query := searchRequests[testCase.queryIndex]
+			query.Sort = search.SortOrder{&search.SortScore{Desc: true}, &search.SortDocID{Desc: true}}
+			query.AddKNNOperator(operator)
+			err = createMultipleSegmentsIndex(documents, index, 1)
+			if err != nil {
+				t.Fatal(err)
+			}
+			controlResult, err := index.Search(query)
+			if err != nil {
+				t.Fatal(err)
+			}
+			if testCase.scoreValue == "none" {
+				query.Score = testCase.scoreValue
+				expectedResultScoreNone, err := index.Search(query)
+				if err != nil {
+					t.Fatal(err)
+				}
+				verifyResult(t, controlResult, expectedResultScoreNone, testCaseNum, true)
+				query.Score = ""
+			}
+			err = index.Close()
+			if err != nil {
+				t.Fatal(err)
+			}
+			cleanupTmpIndexPath(t, tmpIndexPath)
 
-		// run multiple segments test
-		tmpIndexPath = createTmpIndexPath(t)
-		index, err = New(tmpIndexPath, testCase.mapping)
-		if err != nil {
-			t.Fatal(err)
-		}
-		query = searchRequests[testCase.queryIndex]
-		err = createMultipleSegmentsIndex(documents, index, testCase.numSegments)
-		if err != nil {
-			t.Fatal(err)
-		}
-		actualResult, err := index.Search(query)
-		if err != nil {
-			t.Fatal(err)
-		}
-		verifyResult(t, actualResult, expectedResult, false, testCaseNum, false)
-		err = index.Close()
-		if err != nil {
-			t.Fatal(err)
+			// run multiple segments test
+			tmpIndexPath = createTmpIndexPath(t)
+			index, err = New(tmpIndexPath, testCase.mapping)
+			if err != nil {
+				t.Fatal(err)
+			}
+			query = searchRequests[testCase.queryIndex]
+			query.AddKNNOperator(operator)
+			err = createMultipleSegmentsIndex(documents, index, testCase.numSegments)
+			if err != nil {
+				t.Fatal(err)
+			}
+			experimentalResult, err := index.Search(query)
+			if err != nil {
+				t.Fatal(err)
+			}
+			verifyResult(t, controlResult, experimentalResult, testCaseNum, false)
+			err = index.Close()
+			if err != nil {
+				t.Fatal(err)
+			}
+			cleanupTmpIndexPath(t, tmpIndexPath)
 		}
-		cleanupTmpIndexPath(t, tmpIndexPath)
 	}
 }
 
@@ -1109,29 +611,23 @@ func TestKNNOperator(t *testing.T) {
 
 	// Conjunction
 	searchRequest.AddKNNOperator(knnOperatorAnd)
-	conjunction, err := queryWithKNN(searchRequest)
+	conjunction, _, _, err := createKNNQuery(searchRequest)
 	if err != nil {
 		t.Fatalf("unexpected error for AND knn operator")
 	}
 
-	conj, ok := conjunction.(*query.ConjunctionQuery)
+	conj, ok := conjunction.(*query.DisjunctionQuery)
 	if !ok {
-		t.Fatalf("expected conjunction query")
+		t.Fatalf("expected disjunction query")
 	}
 
-	if len(conj.Conjuncts) == 3 {
-		_, ok := conj.Conjuncts[0].(*query.TermQuery)
-		if !ok {
-			t.Fatalf("expected first query to be a term query,"+
-				" but it's %T", conj.Conjuncts[0])
-		}
-	} else {
-		t.Fatalf("expected 3 conjuncts")
+	if len(conj.Disjuncts) != 2 {
+		t.Fatalf("expected 2 disjuncts")
 	}
 
 	// Disjunction
 	searchRequest.AddKNNOperator(knnOperatorOr)
-	disjunction, err := queryWithKNN(searchRequest)
+	disjunction, _, _, err := createKNNQuery(searchRequest)
 	if err != nil {
 		t.Fatalf("unexpected error for OR knn operator")
 	}
@@ -1141,20 +637,103 @@ func TestKNNOperator(t *testing.T) {
 		t.Fatalf("expected disjunction query")
 	}
 
-	if len(disj.Disjuncts) == 3 {
-		_, ok := disj.Disjuncts[0].(*query.TermQuery)
-		if !ok {
-			t.Fatalf("expected first query to be a term query,"+
-				" but it's %T", conj.Conjuncts[0])
-		}
-	} else {
-		t.Fatalf("expected 3 disjuncts")
+	if len(disj.Disjuncts) != 2 {
+		t.Fatalf("expected 2 disjuncts")
 	}
 
 	// Incorrect operator.
 	searchRequest.AddKNNOperator("bs_op")
-	searchRequest.Query, err = queryWithKNN(searchRequest)
+	searchRequest.Query, _, _, err = createKNNQuery(searchRequest)
 	if err == nil {
 		t.Fatalf("expected error for incorrect knn operator")
 	}
 }
+
+// -----------------------------------------------------------------------------
+// Test nested vectors
+
+func TestNestedVectors(t *testing.T) {
+	tmpIndexPath := createTmpIndexPath(t)
+	defer cleanupTmpIndexPath(t, tmpIndexPath)
+
+	const dims = 3
+	const k = 1 // one nearest neighbor
+	const vecFieldName = "vecData"
+
+	dataset := map[string]map[string]interface{}{ // docID -> Doc
+		"doc1": {
+			vecFieldName: []float32{100, 100, 100},
+		},
+		"doc2": {
+			vecFieldName: [][]float32{{0, 0, 0}, {1000, 1000, 1000}},
+		},
+	}
+
+	// Index mapping
+	indexMapping := NewIndexMapping()
+	vm := mapping.NewVectorFieldMapping()
+	vm.Dims = dims
+	vm.Similarity = "l2_norm"
+	indexMapping.DefaultMapping.AddFieldMappingsAt(vecFieldName, vm)
+
+	// Create index and upload documents
+	index, err := New(tmpIndexPath, indexMapping)
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer func() {
+		err := index.Close()
+		if err != nil {
+			t.Fatal(err)
+		}
+	}()
+
+	batch := index.NewBatch()
+	for docID, doc := range dataset {
+		batch.Index(docID, doc)
+	}
+
+	err = index.Batch(batch)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// Run searches
+
+	tests := []struct {
+		queryVec      []float32
+		expectedDocID string
+	}{
+		{
+			queryVec:      []float32{100, 100, 100},
+			expectedDocID: "doc1",
+		},
+		{
+			queryVec:      []float32{0, 0, 0},
+			expectedDocID: "doc2",
+		},
+		{
+			queryVec:      []float32{1000, 1000, 1000},
+			expectedDocID: "doc2",
+		},
+	}
+
+	for _, test := range tests {
+		searchReq := NewSearchRequest(query.NewMatchNoneQuery())
+		searchReq.AddKNN(vecFieldName, test.queryVec, k, 1000)
+
+		res, err := index.Search(searchReq)
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		if len(res.Hits) != 1 {
+			t.Fatalf("expected 1 hit, got %d", len(res.Hits))
+		}
+
+		if res.Hits[0].ID != test.expectedDocID {
+			t.Fatalf("expected docID %s, got %s", test.expectedDocID,
+				res.Hits[0].ID)
+		}
+	}
+}