Repository: blevesearch/bleve
Pull request #1724 (2022-08-24 05:31:03)

When using cjk analyzer with `output_unigram`, an unigram before Japanese punctuation is not indexed
## Summary
When I do indexing and searching Japanese text with cjk analyzer with `output_unigram` config, the word before Japanese punctuation characters (e.g.: `、`,`。`) cannot be searched. For example, I got no results with the search query `ちは` from sentence of `こんにちは、世界`, but I got a result with the search query `ちは` from sentence of `こんにちは世界`.

I look into this, and I found that CJKBigramFilter doesn't flush unigram before punctuations correctly. This PR fixed it.

---

Sample code to reproduce this issue is here:
https://gist.github.com/kaakaa/ca8c20821ef610b098851f487eb61ea5


Patch:
diff --git a/README.md b/README.md
index f4ade0ca..e10454da 100644
--- a/README.md
+++ b/README.md
@@ -16,23 +16,26 @@ modern text indexing in go - [blevesearch.com](http://www.blevesearch.com/)
 * Index any go data structure (including JSON)
 * Intelligent defaults backed up by powerful configuration
 * Supported field types:
-    * Text, Numeric, Date
+    * Text, Numeric, Datetime, Boolean
 * Supported query types:
-    * Term, Phrase, Match, Match Phrase, Prefix
-    * Conjunction, Disjunction, Boolean
-    * Numeric Range, Date Range
+    * Term, Phrase, Match, Match Phrase, Prefix, Fuzzy
+    * Conjunction, Disjunction, Boolean (must/should/must_not)
+    * Term Range, Numeric Range, Date Range
     * [Geo Spatial](https://github.com/blevesearch/bleve/blob/master/geo/README.md)
-    * Simple query [syntax](http://www.blevesearch.com/docs/Query-String-Query/) for human entry
-* tf-idf Scoring
+    * Simple [query string syntax](http://www.blevesearch.com/docs/Query-String-Query/) for human entry
+* [tf-idf](https://en.wikipedia.org/wiki/Tf-idf) Scoring
+* Boosting
 * Search result match highlighting
-* Supports Aggregating Facets:
+* Aggregations/faceting support:
     * Terms Facet
     * Numeric Range Facet
     * Date Range Facet
 
-## Discussion
+## Discussion/Issues
 
-Discuss usage and development of bleve in the [google group](https://groups.google.com/forum/#!forum/bleve).
+Discuss usage/development of bleve and/or report issues here:
+* [Github issues](https://github.com/blevesearch/bleve/issues)
+* [Google group](https://groups.google.com/forum/#!forum/bleve)
 
 ## Indexing
 
@@ -102,7 +105,7 @@ Use "bleve [command] --help" for more information about a command.
 
 ## Text Analysis Wizard
 
-https://bleveanalysis.couchbase.com
+[bleveanalysis.couchbase.com](https://bleveanalysis.couchbase.com)
 
 ## License
 
diff --git a/analysis/analyzer/custom/custom.go b/analysis/analyzer/custom/custom.go
index 70e7a269..5e28c95a 100644
--- a/analysis/analyzer/custom/custom.go
+++ b/analysis/analyzer/custom/custom.go
@@ -23,7 +23,7 @@ import (
 
 const Name = "custom"
 
-func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (*analysis.Analyzer, error) {
+func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.Analyzer, error) {
 
 	var err error
 	var charFilters []analysis.CharFilter
@@ -88,7 +88,7 @@ func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (
 		}
 	}
 
-	rv := analysis.Analyzer{
+	rv := analysis.DefaultAnalyzer{
 		Tokenizer: tokenizer,
 	}
 	if charFilters != nil {
diff --git a/analysis/analyzer/keyword/keyword.go b/analysis/analyzer/keyword/keyword.go
index 473c2947..6bb56d6f 100644
--- a/analysis/analyzer/keyword/keyword.go
+++ b/analysis/analyzer/keyword/keyword.go
@@ -22,12 +22,12 @@ import (
 
 const Name = "keyword"
 
-func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (*analysis.Analyzer, error) {
+func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.Analyzer, error) {
 	keywordTokenizer, err := cache.TokenizerNamed(single.Name)
 	if err != nil {
 		return nil, err
 	}
-	rv := analysis.Analyzer{
+	rv := analysis.DefaultAnalyzer{
 		Tokenizer: keywordTokenizer,
 	}
 	return &rv, nil
diff --git a/analysis/analyzer/simple/simple.go b/analysis/analyzer/simple/simple.go
index 46a715c2..6954e5d5 100644
--- a/analysis/analyzer/simple/simple.go
+++ b/analysis/analyzer/simple/simple.go
@@ -23,7 +23,7 @@ import (
 
 const Name = "simple"
 
-func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (*analysis.Analyzer, error) {
+func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.Analyzer, error) {
 	tokenizer, err := cache.TokenizerNamed(letter.Name)
 	if err != nil {
 		return nil, err
@@ -32,7 +32,7 @@ func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (
 	if err != nil {
 		return nil, err
 	}
-	rv := analysis.Analyzer{
+	rv := analysis.DefaultAnalyzer{
 		Tokenizer: tokenizer,
 		TokenFilters: []analysis.TokenFilter{
 			toLowerFilter,
diff --git a/analysis/analyzer/standard/standard.go b/analysis/analyzer/standard/standard.go
index 80a481be..96387bd7 100644
--- a/analysis/analyzer/standard/standard.go
+++ b/analysis/analyzer/standard/standard.go
@@ -24,7 +24,7 @@ import (
 
 const Name = "standard"
 
-func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (*analysis.Analyzer, error) {
+func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.Analyzer, error) {
 	tokenizer, err := cache.TokenizerNamed(unicode.Name)
 	if err != nil {
 		return nil, err
@@ -37,7 +37,7 @@ func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (
 	if err != nil {
 		return nil, err
 	}
-	rv := analysis.Analyzer{
+	rv := analysis.DefaultAnalyzer{
 		Tokenizer: tokenizer,
 		TokenFilters: []analysis.TokenFilter{
 			toLowerFilter,
diff --git a/analysis/analyzer/web/web.go b/analysis/analyzer/web/web.go
index 5d4145bd..1dea3405 100644
--- a/analysis/analyzer/web/web.go
+++ b/analysis/analyzer/web/web.go
@@ -24,7 +24,7 @@ import (
 
 const Name = "web"
 
-func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (*analysis.Analyzer, error) {
+func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.Analyzer, error) {
 	tokenizer, err := cache.TokenizerNamed(web.Name)
 	if err != nil {
 		return nil, err
@@ -37,7 +37,7 @@ func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (
 	if err != nil {
 		return nil, err
 	}
-	rv := analysis.Analyzer{
+	rv := analysis.DefaultAnalyzer{
 		Tokenizer: tokenizer,
 		TokenFilters: []analysis.TokenFilter{
 			toLowerFilter,
diff --git a/analysis/lang/ar/analyzer_ar.go b/analysis/lang/ar/analyzer_ar.go
index e6f1f8f3..e15cd253 100644
--- a/analysis/lang/ar/analyzer_ar.go
+++ b/analysis/lang/ar/analyzer_ar.go
@@ -25,7 +25,7 @@ import (
 
 const AnalyzerName = "ar"
 
-func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (*analysis.Analyzer, error) {
+func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.Analyzer, error) {
 	tokenizer, err := cache.TokenizerNamed(unicode.Name)
 	if err != nil {
 		return nil, err
@@ -47,7 +47,7 @@ func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (
 	if err != nil {
 		return nil, err
 	}
-	rv := analysis.Analyzer{
+	rv := analysis.DefaultAnalyzer{
 		Tokenizer: tokenizer,
 		TokenFilters: []analysis.TokenFilter{
 			toLowerFilter,
diff --git a/analysis/lang/cjk/analyzer_cjk.go b/analysis/lang/cjk/analyzer_cjk.go
index d7c1142c..7e932ce5 100644
--- a/analysis/lang/cjk/analyzer_cjk.go
+++ b/analysis/lang/cjk/analyzer_cjk.go
@@ -24,7 +24,7 @@ import (
 
 const AnalyzerName = "cjk"
 
-func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (*analysis.Analyzer, error) {
+func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.Analyzer, error) {
 	tokenizer, err := cache.TokenizerNamed(unicode.Name)
 	if err != nil {
 		return nil, err
@@ -41,7 +41,7 @@ func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (
 	if err != nil {
 		return nil, err
 	}
-	rv := analysis.Analyzer{
+	rv := analysis.DefaultAnalyzer{
 		Tokenizer: tokenizer,
 		TokenFilters: []analysis.TokenFilter{
 			widthFilter,
diff --git a/analysis/lang/cjk/cjk_bigram.go b/analysis/lang/cjk/cjk_bigram.go
index fcb80151..14fe6594 100644
--- a/analysis/lang/cjk/cjk_bigram.go
+++ b/analysis/lang/cjk/cjk_bigram.go
@@ -78,9 +78,11 @@ func (s *CJKBigramFilter) Filter(input analysis.TokenStream) analysis.TokenStrea
 				if itemsInRing < 2 {
 					itemsInRing++
 				}
+				builtUnigram := false
 				if itemsInRing > 1 && s.outputUnigram {
 					unigram := s.buildUnigram(r, &itemsInRing, outputPos)
 					if unigram != nil {
+						builtUnigram = true
 						rv = append(rv, unigram)
 					}
 				}
@@ -89,6 +91,11 @@ func (s *CJKBigramFilter) Filter(input analysis.TokenStream) analysis.TokenStrea
 					rv = append(rv, bigramToken)
 					outputPos++
 				}
+
+				// prev token should be removed if unigram was built
+				if builtUnigram {
+					itemsInRing--
+				}
 			}
 
 		} else {
diff --git a/analysis/lang/ckb/analyzer_ckb.go b/analysis/lang/ckb/analyzer_ckb.go
index a71b0f82..9b9532d2 100644
--- a/analysis/lang/ckb/analyzer_ckb.go
+++ b/analysis/lang/ckb/analyzer_ckb.go
@@ -23,7 +23,7 @@ import (
 
 const AnalyzerName = "ckb"
 
-func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (*analysis.Analyzer, error) {
+func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.Analyzer, error) {
 	unicodeTokenizer, err := cache.TokenizerNamed(unicode.Name)
 	if err != nil {
 		return nil, err
@@ -44,7 +44,7 @@ func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (
 	if err != nil {
 		return nil, err
 	}
-	rv := analysis.Analyzer{
+	rv := analysis.DefaultAnalyzer{
 		Tokenizer: unicodeTokenizer,
 		TokenFilters: []analysis.TokenFilter{
 			normCkbFilter,
diff --git a/analysis/lang/da/analyzer_da.go b/analysis/lang/da/analyzer_da.go
index 5ee8428f..f286fbc4 100644
--- a/analysis/lang/da/analyzer_da.go
+++ b/analysis/lang/da/analyzer_da.go
@@ -23,7 +23,7 @@ import (
 
 const AnalyzerName = "da"
 
-func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (*analysis.Analyzer, error) {
+func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.Analyzer, error) {
 	unicodeTokenizer, err := cache.TokenizerNamed(unicode.Name)
 	if err != nil {
 		return nil, err
@@ -40,7 +40,7 @@ func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (
 	if err != nil {
 		return nil, err
 	}
-	rv := analysis.Analyzer{
+	rv := analysis.DefaultAnalyzer{
 		Tokenizer: unicodeTokenizer,
 		TokenFilters: []analysis.TokenFilter{
 			toLowerFilter,
diff --git a/analysis/lang/de/analyzer_de.go b/analysis/lang/de/analyzer_de.go
index 632ab8df..34ded33d 100644
--- a/analysis/lang/de/analyzer_de.go
+++ b/analysis/lang/de/analyzer_de.go
@@ -23,7 +23,7 @@ import (
 
 const AnalyzerName = "de"
 
-func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (*analysis.Analyzer, error) {
+func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.Analyzer, error) {
 	unicodeTokenizer, err := cache.TokenizerNamed(unicode.Name)
 	if err != nil {
 		return nil, err
@@ -44,7 +44,7 @@ func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (
 	if err != nil {
 		return nil, err
 	}
-	rv := analysis.Analyzer{
+	rv := analysis.DefaultAnalyzer{
 		Tokenizer: unicodeTokenizer,
 		TokenFilters: []analysis.TokenFilter{
 			toLowerFilter,
diff --git a/analysis/lang/en/analyzer_en.go b/analysis/lang/en/analyzer_en.go
index 7a4ae586..44a8d4c2 100644
--- a/analysis/lang/en/analyzer_en.go
+++ b/analysis/lang/en/analyzer_en.go
@@ -32,7 +32,7 @@ import (
 
 const AnalyzerName = "en"
 
-func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (*analysis.Analyzer, error) {
+func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.Analyzer, error) {
 	tokenizer, err := cache.TokenizerNamed(unicode.Name)
 	if err != nil {
 		return nil, err
@@ -53,7 +53,7 @@ func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (
 	if err != nil {
 		return nil, err
 	}
-	rv := analysis.Analyzer{
+	rv := analysis.DefaultAnalyzer{
 		Tokenizer: tokenizer,
 		TokenFilters: []analysis.TokenFilter{
 			possEnFilter,
diff --git a/analysis/lang/es/analyzer_es.go b/analysis/lang/es/analyzer_es.go
index a260aae6..e6fcd080 100644
--- a/analysis/lang/es/analyzer_es.go
+++ b/analysis/lang/es/analyzer_es.go
@@ -25,7 +25,7 @@ import (
 const AnalyzerName = "es"
 
 func AnalyzerConstructor(config map[string]interface{},
-	cache *registry.Cache) (*analysis.Analyzer, error) {
+	cache *registry.Cache) (analysis.Analyzer, error) {
 	unicodeTokenizer, err := cache.TokenizerNamed(unicode.Name)
 	if err != nil {
 		return nil, err
@@ -42,7 +42,7 @@ func AnalyzerConstructor(config map[string]interface{},
 	if err != nil {
 		return nil, err
 	}
-	rv := analysis.Analyzer{
+	rv := analysis.DefaultAnalyzer{
 		Tokenizer: unicodeTokenizer,
 		TokenFilters: []analysis.TokenFilter{
 			toLowerFilter,
diff --git a/analysis/lang/fa/analyzer_fa.go b/analysis/lang/fa/analyzer_fa.go
index 8dcf9d51..104ac6df 100644
--- a/analysis/lang/fa/analyzer_fa.go
+++ b/analysis/lang/fa/analyzer_fa.go
@@ -26,7 +26,7 @@ import (
 
 const AnalyzerName = "fa"
 
-func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (*analysis.Analyzer, error) {
+func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.Analyzer, error) {
 	zFilter, err := cache.CharFilterNamed(zerowidthnonjoiner.Name)
 	if err != nil {
 		return nil, err
@@ -51,7 +51,7 @@ func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (
 	if err != nil {
 		return nil, err
 	}
-	rv := analysis.Analyzer{
+	rv := analysis.DefaultAnalyzer{
 		CharFilters: []analysis.CharFilter{
 			zFilter,
 		},
diff --git a/analysis/lang/fi/analyzer_fi.go b/analysis/lang/fi/analyzer_fi.go
index 5b73b0c9..c68cc2c1 100644
--- a/analysis/lang/fi/analyzer_fi.go
+++ b/analysis/lang/fi/analyzer_fi.go
@@ -24,7 +24,7 @@ import (
 
 const AnalyzerName = "fi"
 
-func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (*analysis.Analyzer, error) {
+func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.Analyzer, error) {
 	unicodeTokenizer, err := cache.TokenizerNamed(unicode.Name)
 	if err != nil {
 		return nil, err
@@ -41,7 +41,7 @@ func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (
 	if err != nil {
 		return nil, err
 	}
-	rv := analysis.Analyzer{
+	rv := analysis.DefaultAnalyzer{
 		Tokenizer: unicodeTokenizer,
 		TokenFilters: []analysis.TokenFilter{
 			toLowerFilter,
diff --git a/analysis/lang/fr/analyzer_fr.go b/analysis/lang/fr/analyzer_fr.go
index 138eda8a..6f773a1b 100644
--- a/analysis/lang/fr/analyzer_fr.go
+++ b/analysis/lang/fr/analyzer_fr.go
@@ -24,7 +24,7 @@ import (
 
 const AnalyzerName = "fr"
 
-func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (*analysis.Analyzer, error) {
+func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.Analyzer, error) {
 	tokenizer, err := cache.TokenizerNamed(unicode.Name)
 	if err != nil {
 		return nil, err
@@ -45,7 +45,7 @@ func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (
 	if err != nil {
 		return nil, err
 	}
-	rv := analysis.Analyzer{
+	rv := analysis.DefaultAnalyzer{
 		Tokenizer: tokenizer,
 		TokenFilters: []analysis.TokenFilter{
 			toLowerFilter,
diff --git a/analysis/lang/hi/analyzer_hi.go b/analysis/lang/hi/analyzer_hi.go
index 717bb814..8d4f6232 100644
--- a/analysis/lang/hi/analyzer_hi.go
+++ b/analysis/lang/hi/analyzer_hi.go
@@ -25,7 +25,7 @@ import (
 
 const AnalyzerName = "hi"
 
-func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (*analysis.Analyzer, error) {
+func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.Analyzer, error) {
 	tokenizer, err := cache.TokenizerNamed(unicode.Name)
 	if err != nil {
 		return nil, err
@@ -50,7 +50,7 @@ func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (
 	if err != nil {
 		return nil, err
 	}
-	rv := analysis.Analyzer{
+	rv := analysis.DefaultAnalyzer{
 		Tokenizer: tokenizer,
 		TokenFilters: []analysis.TokenFilter{
 			toLowerFilter,
diff --git a/analysis/lang/hr/analyzer_hr.go b/analysis/lang/hr/analyzer_hr.go
index c4100d96..b0654a29 100644
--- a/analysis/lang/hr/analyzer_hr.go
+++ b/analysis/lang/hr/analyzer_hr.go
@@ -26,7 +26,7 @@ import (
 
 const AnalyzerName = "hr"
 
-func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (*analysis.Analyzer, error) {
+func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.Analyzer, error) {
 	unicodeTokenizer, err := cache.TokenizerNamed(unicode.Name)
 	if err != nil {
 		return nil, err
@@ -47,7 +47,7 @@ func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (
 	if err != nil {
 		return nil, err
 	}
-	rv := analysis.Analyzer{
+	rv := analysis.DefaultAnalyzer{
 		Tokenizer: unicodeTokenizer,
 		TokenFilters: []analysis.TokenFilter{
 			toLowerFilter,
diff --git a/analysis/lang/hu/analyzer_hu.go b/analysis/lang/hu/analyzer_hu.go
index 5935b7d4..3f5517d0 100644
--- a/analysis/lang/hu/analyzer_hu.go
+++ b/analysis/lang/hu/analyzer_hu.go
@@ -24,7 +24,7 @@ import (
 
 const AnalyzerName = "hu"
 
-func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (*analysis.Analyzer, error) {
+func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.Analyzer, error) {
 	unicodeTokenizer, err := cache.TokenizerNamed(unicode.Name)
 	if err != nil {
 		return nil, err
@@ -41,7 +41,7 @@ func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (
 	if err != nil {
 		return nil, err
 	}
-	rv := analysis.Analyzer{
+	rv := analysis.DefaultAnalyzer{
 		Tokenizer: unicodeTokenizer,
 		TokenFilters: []analysis.TokenFilter{
 			toLowerFilter,
diff --git a/analysis/lang/it/analyzer_it.go b/analysis/lang/it/analyzer_it.go
index f86d8b8d..256b22a2 100644
--- a/analysis/lang/it/analyzer_it.go
+++ b/analysis/lang/it/analyzer_it.go
@@ -24,7 +24,7 @@ import (
 
 const AnalyzerName = "it"
 
-func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (*analysis.Analyzer, error) {
+func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.Analyzer, error) {
 	tokenizer, err := cache.TokenizerNamed(unicode.Name)
 	if err != nil {
 		return nil, err
@@ -45,7 +45,7 @@ func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (
 	if err != nil {
 		return nil, err
 	}
-	rv := analysis.Analyzer{
+	rv := analysis.DefaultAnalyzer{
 		Tokenizer: tokenizer,
 		TokenFilters: []analysis.TokenFilter{
 			toLowerFilter,
diff --git a/analysis/lang/nl/analyzer_nl.go b/analysis/lang/nl/analyzer_nl.go
index e8a195b2..321aabea 100644
--- a/analysis/lang/nl/analyzer_nl.go
+++ b/analysis/lang/nl/analyzer_nl.go
@@ -24,7 +24,7 @@ import (
 
 const AnalyzerName = "nl"
 
-func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (*analysis.Analyzer, error) {
+func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.Analyzer, error) {
 	unicodeTokenizer, err := cache.TokenizerNamed(unicode.Name)
 	if err != nil {
 		return nil, err
@@ -41,7 +41,7 @@ func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (
 	if err != nil {
 		return nil, err
 	}
-	rv := analysis.Analyzer{
+	rv := analysis.DefaultAnalyzer{
 		Tokenizer: unicodeTokenizer,
 		TokenFilters: []analysis.TokenFilter{
 			toLowerFilter,
diff --git a/analysis/lang/no/analyzer_no.go b/analysis/lang/no/analyzer_no.go
index 7876195c..f036dc45 100644
--- a/analysis/lang/no/analyzer_no.go
+++ b/analysis/lang/no/analyzer_no.go
@@ -24,7 +24,7 @@ import (
 
 const AnalyzerName = "no"
 
-func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (*analysis.Analyzer, error) {
+func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.Analyzer, error) {
 	unicodeTokenizer, err := cache.TokenizerNamed(unicode.Name)
 	if err != nil {
 		return nil, err
@@ -41,7 +41,7 @@ func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (
 	if err != nil {
 		return nil, err
 	}
-	rv := analysis.Analyzer{
+	rv := analysis.DefaultAnalyzer{
 		Tokenizer: unicodeTokenizer,
 		TokenFilters: []analysis.TokenFilter{
 			toLowerFilter,
diff --git a/analysis/lang/pt/analyzer_pt.go b/analysis/lang/pt/analyzer_pt.go
index c8814028..ad99fac3 100644
--- a/analysis/lang/pt/analyzer_pt.go
+++ b/analysis/lang/pt/analyzer_pt.go
@@ -24,7 +24,7 @@ import (
 
 const AnalyzerName = "pt"
 
-func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (*analysis.Analyzer, error) {
+func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.Analyzer, error) {
 	tokenizer, err := cache.TokenizerNamed(unicode.Name)
 	if err != nil {
 		return nil, err
@@ -41,7 +41,7 @@ func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (
 	if err != nil {
 		return nil, err
 	}
-	rv := analysis.Analyzer{
+	rv := analysis.DefaultAnalyzer{
 		Tokenizer: tokenizer,
 		TokenFilters: []analysis.TokenFilter{
 			toLowerFilter,
diff --git a/analysis/lang/ro/analyzer_ro.go b/analysis/lang/ro/analyzer_ro.go
index 24c7bffe..5fff2a98 100644
--- a/analysis/lang/ro/analyzer_ro.go
+++ b/analysis/lang/ro/analyzer_ro.go
@@ -24,7 +24,7 @@ import (
 
 const AnalyzerName = "ro"
 
-func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (*analysis.Analyzer, error) {
+func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.Analyzer, error) {
 	unicodeTokenizer, err := cache.TokenizerNamed(unicode.Name)
 	if err != nil {
 		return nil, err
@@ -41,7 +41,7 @@ func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (
 	if err != nil {
 		return nil, err
 	}
-	rv := analysis.Analyzer{
+	rv := analysis.DefaultAnalyzer{
 		Tokenizer: unicodeTokenizer,
 		TokenFilters: []analysis.TokenFilter{
 			toLowerFilter,
diff --git a/analysis/lang/ru/analyzer_ru.go b/analysis/lang/ru/analyzer_ru.go
index 3d591aff..3126392c 100644
--- a/analysis/lang/ru/analyzer_ru.go
+++ b/analysis/lang/ru/analyzer_ru.go
@@ -24,7 +24,7 @@ import (
 
 const AnalyzerName = "ru"
 
-func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (*analysis.Analyzer, error) {
+func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.Analyzer, error) {
 	tokenizer, err := cache.TokenizerNamed(unicode.Name)
 	if err != nil {
 		return nil, err
@@ -41,7 +41,7 @@ func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (
 	if err != nil {
 		return nil, err
 	}
-	rv := analysis.Analyzer{
+	rv := analysis.DefaultAnalyzer{
 		Tokenizer: tokenizer,
 		TokenFilters: []analysis.TokenFilter{
 			toLowerFilter,
diff --git a/analysis/lang/sv/analyzer_sv.go b/analysis/lang/sv/analyzer_sv.go
index 7d987b32..38ddb308 100644
--- a/analysis/lang/sv/analyzer_sv.go
+++ b/analysis/lang/sv/analyzer_sv.go
@@ -24,7 +24,7 @@ import (
 
 const AnalyzerName = "sv"
 
-func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (*analysis.Analyzer, error) {
+func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.Analyzer, error) {
 	unicodeTokenizer, err := cache.TokenizerNamed(unicode.Name)
 	if err != nil {
 		return nil, err
@@ -41,7 +41,7 @@ func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (
 	if err != nil {
 		return nil, err
 	}
-	rv := analysis.Analyzer{
+	rv := analysis.DefaultAnalyzer{
 		Tokenizer: unicodeTokenizer,
 		TokenFilters: []analysis.TokenFilter{
 			toLowerFilter,
diff --git a/analysis/lang/tr/analyzer_tr.go b/analysis/lang/tr/analyzer_tr.go
index 64c6b3f8..401609c4 100644
--- a/analysis/lang/tr/analyzer_tr.go
+++ b/analysis/lang/tr/analyzer_tr.go
@@ -25,7 +25,7 @@ import (
 
 const AnalyzerName = "tr"
 
-func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (*analysis.Analyzer, error) {
+func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.Analyzer, error) {
 	unicodeTokenizer, err := cache.TokenizerNamed(unicode.Name)
 	if err != nil {
 		return nil, err
@@ -46,7 +46,7 @@ func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (
 	if err != nil {
 		return nil, err
 	}
-	rv := analysis.Analyzer{
+	rv := analysis.DefaultAnalyzer{
 		Tokenizer: unicodeTokenizer,
 		TokenFilters: []analysis.TokenFilter{
 			aposFilter,
diff --git a/analysis/type.go b/analysis/type.go
index 1fa26308..9e7bfa16 100644
--- a/analysis/type.go
+++ b/analysis/type.go
@@ -72,13 +72,17 @@ type TokenFilter interface {
 	Filter(TokenStream) TokenStream
 }
 
-type Analyzer struct {
+type Analyzer interface {
+	Analyze([]byte) TokenStream
+}
+
+type DefaultAnalyzer struct {
 	CharFilters  []CharFilter
 	Tokenizer    Tokenizer
 	TokenFilters []TokenFilter
 }
 
-func (a *Analyzer) Analyze(input []byte) TokenStream {
+func (a *DefaultAnalyzer) Analyze(input []byte) TokenStream {
 	if a.CharFilters != nil {
 		for _, cf := range a.CharFilters {
 			input = cf.Filter(input)
diff --git a/document/document.go b/document/document.go
index 1a6050f0..54fd6d44 100644
--- a/document/document.go
+++ b/document/document.go
@@ -30,9 +30,14 @@ func init() {
 }
 
 type Document struct {
-	id              string  `json:"id"`
-	Fields          []Field `json:"fields"`
-	CompositeFields []*CompositeField
+	id               string  `json:"id"`
+	Fields           []Field `json:"fields"`
+	CompositeFields  []*CompositeField
+	StoredFieldsSize uint64
+}
+
+func (d *Document) StoredFieldsBytes() uint64 {
+	return d.StoredFieldsSize
 }
 
 func NewDocument(id string) *Document {
diff --git a/document/field_text.go b/document/field_text.go
index 924de532..fddc59d0 100644
--- a/document/field_text.go
+++ b/document/field_text.go
@@ -36,7 +36,7 @@ type TextField struct {
 	name              string
 	arrayPositions    []uint64
 	options           index.FieldIndexingOptions
-	analyzer          *analysis.Analyzer
+	analyzer          analysis.Analyzer
 	value             []byte
 	numPlainTextBytes uint64
 	length            int
@@ -100,7 +100,7 @@ func (t *TextField) Analyze() {
 	t.frequencies = analysis.TokenFrequency(tokens, t.arrayPositions, t.options)
 }
 
-func (t *TextField) Analyzer() *analysis.Analyzer {
+func (t *TextField) Analyzer() analysis.Analyzer {
 	return t.analyzer
 }
 
@@ -134,7 +134,7 @@ func NewTextFieldWithIndexingOptions(name string, arrayPositions []uint64, value
 	}
 }
 
-func NewTextFieldWithAnalyzer(name string, arrayPositions []uint64, value []byte, analyzer *analysis.Analyzer) *TextField {
+func NewTextFieldWithAnalyzer(name string, arrayPositions []uint64, value []byte, analyzer analysis.Analyzer) *TextField {
 	return &TextField{
 		name:              name,
 		arrayPositions:    arrayPositions,
@@ -145,7 +145,7 @@ func NewTextFieldWithAnalyzer(name string, arrayPositions []uint64, value []byte
 	}
 }
 
-func NewTextFieldCustom(name string, arrayPositions []uint64, value []byte, options index.FieldIndexingOptions, analyzer *analysis.Analyzer) *TextField {
+func NewTextFieldCustom(name string, arrayPositions []uint64, value []byte, options index.FieldIndexingOptions, analyzer analysis.Analyzer) *TextField {
 	return &TextField{
 		name:              name,
 		arrayPositions:    arrayPositions,
diff --git a/go.mod b/go.mod
index 2db0ad3c..8f0366b6 100644
--- a/go.mod
+++ b/go.mod
@@ -1,30 +1,42 @@
 module github.com/blevesearch/bleve/v2
 
-go 1.13
+go 1.18
 
 require (
 	github.com/RoaringBitmap/roaring v0.9.4
 	github.com/bits-and-blooms/bitset v1.2.0
-	github.com/blevesearch/bleve_index_api v1.0.3
-	github.com/blevesearch/geo v0.1.14
+	github.com/blevesearch/bleve_index_api v1.0.5
+	github.com/blevesearch/geo v0.1.16
 	github.com/blevesearch/go-metrics v0.0.0-20201227073835-cf1acfcdf475
 	github.com/blevesearch/go-porterstemmer v1.0.3
 	github.com/blevesearch/goleveldb v1.0.1
 	github.com/blevesearch/gtreap v0.1.1
-	github.com/blevesearch/scorch_segment_api/v2 v2.1.2
+	github.com/blevesearch/scorch_segment_api/v2 v2.1.4
 	github.com/blevesearch/segment v0.9.0
 	github.com/blevesearch/snowball v0.6.1
 	github.com/blevesearch/snowballstem v0.9.0
 	github.com/blevesearch/upsidedown_store_api v1.0.1
-	github.com/blevesearch/vellum v1.0.8
-	github.com/blevesearch/zapx/v11 v11.3.5
-	github.com/blevesearch/zapx/v12 v12.3.5
-	github.com/blevesearch/zapx/v13 v13.3.5
-	github.com/blevesearch/zapx/v14 v14.3.5
-	github.com/blevesearch/zapx/v15 v15.3.5
+	github.com/blevesearch/vellum v1.0.9
+	github.com/blevesearch/zapx/v11 v11.3.7
+	github.com/blevesearch/zapx/v12 v12.3.7
+	github.com/blevesearch/zapx/v13 v13.3.7
+	github.com/blevesearch/zapx/v14 v14.3.7
+	github.com/blevesearch/zapx/v15 v15.3.8
 	github.com/couchbase/moss v0.2.0
 	github.com/golang/protobuf v1.3.2
-	github.com/spf13/cobra v0.0.5
+	github.com/spf13/cobra v1.4.0
 	go.etcd.io/bbolt v1.3.5
 	golang.org/x/text v0.3.7
 )
+
+require (
+	github.com/blevesearch/mmap-go v1.0.4 // indirect
+	github.com/couchbase/ghistogram v0.1.0 // indirect
+	github.com/golang/geo v0.0.0-20210211234256-740aa86cb551 // indirect
+	github.com/golang/snappy v0.0.1 // indirect
+	github.com/inconshreveable/mousetrap v1.0.0 // indirect
+	github.com/json-iterator/go v0.0.0-20171115153421-f7279a603ede // indirect
+	github.com/mschoch/smat v0.2.0 // indirect
+	github.com/spf13/pflag v1.0.5 // indirect
+	golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect
+)
diff --git a/go.sum b/go.sum
index f0f7d83e..f4f96025 100644
--- a/go.sum
+++ b/go.sum
@@ -1,13 +1,11 @@
-github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/RoaringBitmap/roaring v0.9.4 h1:ckvZSX5gwCRaJYBNe7syNawCU5oruY9gQmjXlp4riwo=
 github.com/RoaringBitmap/roaring v0.9.4/go.mod h1:icnadbWcNyfEHlYdr+tDlOTih1Bf/h+rzPpv4sbomAA=
-github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
 github.com/bits-and-blooms/bitset v1.2.0 h1:Kn4yilvwNtMACtf1eYDlG8H77R07mZSPbMjLyS07ChA=
 github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA=
-github.com/blevesearch/bleve_index_api v1.0.3 h1:DDSWaPXOZZJ2BB73ZTWjKxydAugjwywcqU+91AAqcAg=
-github.com/blevesearch/bleve_index_api v1.0.3/go.mod h1:fiwKS0xLEm+gBRgv5mumf0dhgFr2mDgZah1pqv1c1M4=
-github.com/blevesearch/geo v0.1.14 h1:TTDpJN6l9ck/cUYbXSn4aCElNls0Whe44rcQKsB7EfU=
-github.com/blevesearch/geo v0.1.14/go.mod h1:cRIvqCdk3cgMhGeHNNe6yPzb+w56otxbfo1FBJfR2Pc=
+github.com/blevesearch/bleve_index_api v1.0.5 h1:Lc986kpC4Z0/n1g3gg8ul7H+lxgOQPcXb9SxvQGu+tw=
+github.com/blevesearch/bleve_index_api v1.0.5/go.mod h1:YXMDwaXFFXwncRS8UobWs7nvo0DmusriM1nztTlj1ms=
+github.com/blevesearch/geo v0.1.16 h1:unVaqUmlwprk56596OQRkGjtq1VZ8XFWSARj+h2cIBY=
+github.com/blevesearch/geo v0.1.16/go.mod h1:a1OlySNE+oDQ5qY0vJGYNoLIsMpbKbx8dnmuRP8D7H0=
 github.com/blevesearch/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:kDy+zgJFJJoJYBvdfBSiZYBbdsUL0XcjHYWezpQBGPA=
 github.com/blevesearch/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:9eJDeqxJ3E7WnLebQUlPD7ZjSce7AnDb9vjGmMCbD0A=
 github.com/blevesearch/go-porterstemmer v1.0.3 h1:GtmsqID0aZdCSNiY8SkuPJ12pD4jI+DdXTAn4YRcHCo=
@@ -19,8 +17,8 @@ github.com/blevesearch/gtreap v0.1.1/go.mod h1:QaQyDRAT51sotthUWAH4Sj08awFSSWzgY
 github.com/blevesearch/mmap-go v1.0.2/go.mod h1:ol2qBqYaOUsGdm7aRMRrYGgPvnwLe6Y+7LMvAB5IbSA=
 github.com/blevesearch/mmap-go v1.0.4 h1:OVhDhT5B/M1HNPpYPBKIEJaD0F3Si+CrEKULGCDPWmc=
 github.com/blevesearch/mmap-go v1.0.4/go.mod h1:EWmEAOmdAS9z/pi/+Toxu99DnsbhG1TIxUoRmJw/pSs=
-github.com/blevesearch/scorch_segment_api/v2 v2.1.2 h1:TAte9VZLWda5WAVlZTTZ+GCzEHqGJb4iB2aiZSA6Iv8=
-github.com/blevesearch/scorch_segment_api/v2 v2.1.2/go.mod h1:rvoQXZGq8drq7vXbNeyiRzdEOwZkjkiYGf1822i6CRA=
+github.com/blevesearch/scorch_segment_api/v2 v2.1.4 h1:LmGmo5twU3gV+natJbKmOktS9eMhokPGKWuR+jX84vk=
+github.com/blevesearch/scorch_segment_api/v2 v2.1.4/go.mod h1:PgVnbbg/t1UkgezPDu8EHLi1BHQ17xUwsFdU6NnOYS0=
 github.com/blevesearch/segment v0.9.0 h1:5lG7yBCx98or7gK2cHMKPukPZ/31Kag7nONpoBt22Ac=
 github.com/blevesearch/segment v0.9.0/go.mod h1:9PfHYUdQCgHktBgvtUOF4x+pc4/l8rdH0u5spnW85UQ=
 github.com/blevesearch/snowball v0.6.1 h1:cDYjn/NCH+wwt2UdehaLpr2e4BwLIjN4V/TdLsL+B5A=
@@ -29,30 +27,25 @@ github.com/blevesearch/snowballstem v0.9.0 h1:lMQ189YspGP6sXvZQ4WZ+MLawfV8wOmPoD
 github.com/blevesearch/snowballstem v0.9.0/go.mod h1:PivSj3JMc8WuaFkTSRDW2SlrulNWPl4ABg1tC/hlgLs=
 github.com/blevesearch/upsidedown_store_api v1.0.1 h1:1SYRwyoFLwG3sj0ed89RLtM15amfX2pXlYbFOnF8zNU=
 github.com/blevesearch/upsidedown_store_api v1.0.1/go.mod h1:MQDVGpHZrpe3Uy26zJBf/a8h0FZY6xJbthIMm8myH2Q=
-github.com/blevesearch/vellum v1.0.8 h1:iMGh4lfxza4BnWO/UJTMPlI3HsK9YawjPv+TteVa9ck=
-github.com/blevesearch/vellum v1.0.8/go.mod h1:+cpRi/tqq49xUYSQN2P7A5zNSNrS+MscLeeaZ3J46UA=
-github.com/blevesearch/zapx/v11 v11.3.5 h1:eBQWQ7huA+mzm0sAGnZDwgGGli7S45EO+N+ObFWssbI=
-github.com/blevesearch/zapx/v11 v11.3.5/go.mod h1:5UdIa/HRMdeRCiLQOyFESsnqBGiip7vQmYReA9toevU=
-github.com/blevesearch/zapx/v12 v12.3.5 h1:5pX2hU+R1aZihT7ac1dNWh1n4wqkIM9pZzWp0ANED9s=
-github.com/blevesearch/zapx/v12 v12.3.5/go.mod h1:ANcthYRZQycpbRut/6ArF5gP5HxQyJqiFcuJCBju/ss=
-github.com/blevesearch/zapx/v13 v13.3.5 h1:eJ3gbD+Nu8p36/O6lhfdvWQ4pxsGYSuTOBrLLPVWJ74=
-github.com/blevesearch/zapx/v13 v13.3.5/go.mod h1:FV+dRnScFgKnRDIp08RQL4JhVXt1x2HE3AOzqYa6fjo=
-github.com/blevesearch/zapx/v14 v14.3.5 h1:hEvVjZaagFCvOUJrlFQ6/Z6Jjy0opM3g7TMEo58TwP4=
-github.com/blevesearch/zapx/v14 v14.3.5/go.mod h1:954A/eKFb+pg/ncIYWLWCKY+mIjReM9FGTGIO2Wu1cU=
-github.com/blevesearch/zapx/v15 v15.3.5 h1:NVD0qq8vRk66ImJn1KloXT5ckqPDUZT7VbVJs9jKlac=
-github.com/blevesearch/zapx/v15 v15.3.5/go.mod h1:QMUh2hXCaYIWFKPYGavq/Iga2zbHWZ9DZAa9uFbWyvg=
-github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
-github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
-github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
+github.com/blevesearch/vellum v1.0.9 h1:PL+NWVk3dDGPCV0hoDu9XLLJgqU4E5s/dOeEJByQ2uQ=
+github.com/blevesearch/vellum v1.0.9/go.mod h1:ul1oT0FhSMDIExNjIxHqJoGpVrBpKCdgDQNxfqgJt7k=
+github.com/blevesearch/zapx/v11 v11.3.7 h1:Y6yIAF/DVPiqZUA/jNgSLXmqewfzwHzuwfKyfdG+Xaw=
+github.com/blevesearch/zapx/v11 v11.3.7/go.mod h1:Xk9Z69AoAWIOvWudNDMlxJDqSYGf90LS0EfnaAIvXCA=
+github.com/blevesearch/zapx/v12 v12.3.7 h1:DfQ6rsmZfEK4PzzJJRXjiM6AObG02+HWvprlXQ1Y7eI=
+github.com/blevesearch/zapx/v12 v12.3.7/go.mod h1:SgEtYIBGvM0mgIBn2/tQE/5SdrPXaJUaT/kVqpAPxm0=
+github.com/blevesearch/zapx/v13 v13.3.7 h1:igIQg5eKmjw168I7av0Vtwedf7kHnQro/M+ubM4d2l8=
+github.com/blevesearch/zapx/v13 v13.3.7/go.mod h1:yyrB4kJ0OT75UPZwT/zS+Ru0/jYKorCOOSY5dBzAy+s=
+github.com/blevesearch/zapx/v14 v14.3.7 h1:gfe+fbWslDWP/evHLtp/GOvmNM3sw1BbqD7LhycBX20=
+github.com/blevesearch/zapx/v14 v14.3.7/go.mod h1:9J/RbOkqZ1KSjmkOes03AkETX7hrXT0sFMpWH4ewC4w=
+github.com/blevesearch/zapx/v15 v15.3.8 h1:q4uMngBHzL1IIhRc8AJUEkj6dGOE3u1l3phLu7hq8uk=
+github.com/blevesearch/zapx/v15 v15.3.8/go.mod h1:m7Y6m8soYUvS7MjN9eKlz1xrLCcmqfFadmu7GhWIrLY=
 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=
 github.com/couchbase/moss v0.2.0/go.mod h1:9MaHIaRuy9pvLPUJxB8sh8OrLfyDczECVL37grCIubs=
-github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
+github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
-github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
 github.com/golang/geo v0.0.0-20210211234256-740aa86cb551 h1:gtexQ/VGyN+VVFRXSFiguSNcXmS6rkKT+X7FdIrTtfo=
 github.com/golang/geo v0.0.0-20210211234256-740aa86cb551/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
@@ -64,16 +57,12 @@ github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
 github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
 github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
-github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
 github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
 github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
 github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
 github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
 github.com/json-iterator/go v0.0.0-20171115153421-f7279a603ede h1:YrgBGwxMRK0Vq0WSCWFaZUnTsrA/PZE/xs1QZh+/edg=
 github.com/json-iterator/go v0.0.0-20171115153421-f7279a603ede/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
-github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
-github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
-github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
 github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM=
 github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw=
 github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
@@ -81,33 +70,23 @@ github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs=
 github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU=
 github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
-github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
-github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
-github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
-github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s=
-github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
-github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
-github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
-github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
-github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
+github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/spf13/cobra v1.4.0 h1:y+wJpx64xcgO1V+RcnwW0LEHxTKRi2ZDPSBjWnrg88Q=
+github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g=
+github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
+github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
 github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
-github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
 go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0=
 go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
-golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/net v0.0.0-20180906233101-161cd47e91fd h1:nTDtHvHSdCn1m6ITfMRqtOd/9+7a3s8RBNOZ3eYZzJA=
 golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20181221143128-b4a75ba826a6/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k=
@@ -115,15 +94,14 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
-golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
 gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
 gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/index/scorch/empty.go b/index/scorch/empty.go
index 4d475553..34619d42 100644
--- a/index/scorch/empty.go
+++ b/index/scorch/empty.go
@@ -30,4 +30,12 @@ func (e *emptyPostingsIterator) Size() int {
 	return 0
 }
 
+func (e *emptyPostingsIterator) BytesRead() uint64 {
+	return 0
+}
+
+func (e *emptyPostingsIterator) ResetBytesRead(uint64) {}
+
+func (e *emptyPostingsIterator) BytesWritten() uint64 { return 0 }
+
 var anEmptyPostingsIterator = &emptyPostingsIterator{}
diff --git a/index/scorch/merge.go b/index/scorch/merge.go
index eb94c12b..9f1774b6 100644
--- a/index/scorch/merge.go
+++ b/index/scorch/merge.go
@@ -354,12 +354,8 @@ func (s *Scorch) planMergeAtSnapshot(ctx context.Context,
 				return err
 			}
 
-			switch segI := seg.(type) {
-			case segment.DiskStatsReporter:
-				totalBytesRead := segI.BytesRead() + prevBytesReadTotal
-				segI.ResetBytesRead(totalBytesRead)
-				seg = segI.(segment.Segment)
-			}
+			totalBytesRead := seg.BytesRead() + prevBytesReadTotal
+			seg.ResetBytesRead(totalBytesRead)
 
 			oldNewDocNums = make(map[uint64][]uint64)
 			for i, segNewDocNums := range newDocNums {
@@ -438,9 +434,7 @@ type segmentMerge struct {
 func cumulateBytesRead(sbs []segment.Segment) uint64 {
 	var rv uint64
 	for _, seg := range sbs {
-		if segI, diskStatsAvailable := seg.(segment.DiskStatsReporter); diskStatsAvailable {
-			rv += segI.BytesRead()
-		}
+		rv += seg.BytesRead()
 	}
 	return rv
 }
diff --git a/index/scorch/scorch.go b/index/scorch/scorch.go
index 6eeaabfd..413684cf 100644
--- a/index/scorch/scorch.go
+++ b/index/scorch/scorch.go
@@ -146,6 +146,7 @@ func NewScorch(storeName string,
 	if ok {
 		rv.onAsyncError = RegistryAsyncErrorCallbacks[aecbName]
 	}
+
 	return rv, nil
 }
 
diff --git a/index/scorch/snapshot_index.go b/index/scorch/snapshot_index.go
index 511b9ef6..4618fd82 100644
--- a/index/scorch/snapshot_index.go
+++ b/index/scorch/snapshot_index.go
@@ -16,6 +16,7 @@ package scorch
 
 import (
 	"container/heap"
+	"context"
 	"encoding/binary"
 	"fmt"
 	"os"
@@ -60,8 +61,6 @@ var reflectStaticSizeIndexSnapshot int
 // in the kvConfig.
 var DefaultFieldTFRCacheThreshold uint64 = 10
 
-type diskStatsReporter segment.DiskStatsReporter
-
 func init() {
 	var is interface{} = IndexSnapshot{}
 	reflectStaticSizeIndexSnapshot = int(reflect.TypeOf(is).Size())
@@ -141,25 +140,20 @@ func (i *IndexSnapshot) updateSize() {
 	}
 }
 
-func (i *IndexSnapshot) newIndexSnapshotFieldDict(field string,
+func (is *IndexSnapshot) newIndexSnapshotFieldDict(field string,
 	makeItr func(i segment.TermDictionary) segment.DictionaryIterator,
 	randomLookup bool) (*IndexSnapshotFieldDict, error) {
 
 	results := make(chan *asynchSegmentResult)
-	for index, segment := range i.segment {
-		go func(index int, segment *SegmentSnapshot) {
-			var prevBytesRead uint64
-			seg, diskStatsAvailable := segment.segment.(diskStatsReporter)
-			if diskStatsAvailable {
-				prevBytesRead = seg.BytesRead()
-			}
-			dict, err := segment.segment.Dictionary(field)
+	var totalBytesRead uint64
+	for _, s := range is.segment {
+		go func(s *SegmentSnapshot) {
+			dict, err := s.segment.Dictionary(field)
 			if err != nil {
 				results <- &asynchSegmentResult{err: err}
 			} else {
-				if diskStatsAvailable {
-					atomic.AddUint64(&i.parent.stats.TotBytesReadAtQueryTime,
-						seg.BytesRead()-prevBytesRead)
+				if dictStats, ok := dict.(segment.DiskStatsReporter); ok {
+					atomic.AddUint64(&totalBytesRead, dictStats.BytesRead())
 				}
 				if randomLookup {
 					results <- &asynchSegmentResult{dict: dict}
@@ -167,15 +161,15 @@ func (i *IndexSnapshot) newIndexSnapshotFieldDict(field string,
 					results <- &asynchSegmentResult{dictItr: makeItr(dict)}
 				}
 			}
-		}(index, segment)
+		}(s)
 	}
 
 	var err error
 	rv := &IndexSnapshotFieldDict{
-		snapshot: i,
-		cursors:  make([]*segmentDictCursor, 0, len(i.segment)),
+		snapshot: is,
+		cursors:  make([]*segmentDictCursor, 0, len(is.segment)),
 	}
-	for count := 0; count < len(i.segment); count++ {
+	for count := 0; count < len(is.segment); count++ {
 		asr := <-results
 		if asr.err != nil && err == nil {
 			err = asr.err
@@ -198,6 +192,7 @@ func (i *IndexSnapshot) newIndexSnapshotFieldDict(field string,
 			}
 		}
 	}
+	rv.bytesRead = totalBytesRead
 	// after ensuring we've read all items on channel
 	if err != nil {
 		return nil, err
@@ -211,9 +206,9 @@ func (i *IndexSnapshot) newIndexSnapshotFieldDict(field string,
 	return rv, nil
 }
 
-func (i *IndexSnapshot) FieldDict(field string) (index.FieldDict, error) {
-	return i.newIndexSnapshotFieldDict(field, func(i segment.TermDictionary) segment.DictionaryIterator {
-		return i.AutomatonIterator(nil, nil, nil)
+func (is *IndexSnapshot) FieldDict(field string) (index.FieldDict, error) {
+	return is.newIndexSnapshotFieldDict(field, func(is segment.TermDictionary) segment.DictionaryIterator {
+		return is.AutomatonIterator(nil, nil, nil)
 	}, false)
 }
 
@@ -238,11 +233,11 @@ func calculateExclusiveEndFromInclusiveEnd(inclusiveEnd []byte) []byte {
 	return rv
 }
 
-func (i *IndexSnapshot) FieldDictRange(field string, startTerm []byte,
+func (is *IndexSnapshot) FieldDictRange(field string, startTerm []byte,
 	endTerm []byte) (index.FieldDict, error) {
-	return i.newIndexSnapshotFieldDict(field, func(i segment.TermDictionary) segment.DictionaryIterator {
+	return is.newIndexSnapshotFieldDict(field, func(is segment.TermDictionary) segment.DictionaryIterator {
 		endTermExclusive := calculateExclusiveEndFromInclusiveEnd(endTerm)
-		return i.AutomatonIterator(nil, startTerm, endTermExclusive)
+		return is.AutomatonIterator(nil, startTerm, endTermExclusive)
 	}, false)
 }
 
@@ -264,15 +259,15 @@ func calculateExclusiveEndFromPrefix(in []byte) []byte {
 	return nil
 }
 
-func (i *IndexSnapshot) FieldDictPrefix(field string,
+func (is *IndexSnapshot) FieldDictPrefix(field string,
 	termPrefix []byte) (index.FieldDict, error) {
 	termPrefixEnd := calculateExclusiveEndFromPrefix(termPrefix)
-	return i.newIndexSnapshotFieldDict(field, func(i segment.TermDictionary) segment.DictionaryIterator {
-		return i.AutomatonIterator(nil, termPrefix, termPrefixEnd)
+	return is.newIndexSnapshotFieldDict(field, func(is segment.TermDictionary) segment.DictionaryIterator {
+		return is.AutomatonIterator(nil, termPrefix, termPrefixEnd)
 	}, false)
 }
 
-func (i *IndexSnapshot) FieldDictRegexp(field string,
+func (is *IndexSnapshot) FieldDictRegexp(field string,
 	termRegex string) (index.FieldDict, error) {
 	// TODO: potential optimization where the literal prefix represents the,
 	//       entire regexp, allowing us to use PrefixIterator(prefixTerm)?
@@ -282,12 +277,12 @@ func (i *IndexSnapshot) FieldDictRegexp(field string,
 		return nil, err
 	}
 
-	return i.newIndexSnapshotFieldDict(field, func(i segment.TermDictionary) segment.DictionaryIterator {
-		return i.AutomatonIterator(a, prefixBeg, prefixEnd)
+	return is.newIndexSnapshotFieldDict(field, func(is segment.TermDictionary) segment.DictionaryIterator {
+		return is.AutomatonIterator(a, prefixBeg, prefixEnd)
 	}, false)
 }
 
-func (i *IndexSnapshot) getLevAutomaton(term string,
+func (is *IndexSnapshot) getLevAutomaton(term string,
 	fuzziness uint8) (vellum.Automaton, error) {
 	if fuzziness == 1 {
 		return lb1.BuildDfa(term, fuzziness)
@@ -297,9 +292,9 @@ func (i *IndexSnapshot) getLevAutomaton(term string,
 	return nil, fmt.Errorf("fuzziness exceeds the max limit")
 }
 
-func (i *IndexSnapshot) FieldDictFuzzy(field string,
+func (is *IndexSnapshot) FieldDictFuzzy(field string,
 	term string, fuzziness int, prefix string) (index.FieldDict, error) {
-	a, err := i.getLevAutomaton(term, uint8(fuzziness))
+	a, err := is.getLevAutomaton(term, uint8(fuzziness))
 	if err != nil {
 		return nil, err
 	}
@@ -310,18 +305,18 @@ func (i *IndexSnapshot) FieldDictFuzzy(field string,
 		prefixEnd = calculateExclusiveEndFromPrefix(prefixBeg)
 	}
 
-	return i.newIndexSnapshotFieldDict(field, func(i segment.TermDictionary) segment.DictionaryIterator {
-		return i.AutomatonIterator(a, prefixBeg, prefixEnd)
+	return is.newIndexSnapshotFieldDict(field, func(is segment.TermDictionary) segment.DictionaryIterator {
+		return is.AutomatonIterator(a, prefixBeg, prefixEnd)
 	}, false)
 }
 
-func (i *IndexSnapshot) FieldDictContains(field string) (index.FieldDictContains, error) {
-	return i.newIndexSnapshotFieldDict(field, nil, true)
+func (is *IndexSnapshot) FieldDictContains(field string) (index.FieldDictContains, error) {
+	return is.newIndexSnapshotFieldDict(field, nil, true)
 }
 
-func (i *IndexSnapshot) DocIDReaderAll() (index.DocIDReader, error) {
+func (is *IndexSnapshot) DocIDReaderAll() (index.DocIDReader, error) {
 	results := make(chan *asynchSegmentResult)
-	for index, segment := range i.segment {
+	for index, segment := range is.segment {
 		go func(index int, segment *SegmentSnapshot) {
 			results <- &asynchSegmentResult{
 				index: index,
@@ -330,12 +325,12 @@ func (i *IndexSnapshot) DocIDReaderAll() (index.DocIDReader, error) {
 		}(index, segment)
 	}
 
-	return i.newDocIDReader(results)
+	return is.newDocIDReader(results)
 }
 
-func (i *IndexSnapshot) DocIDReaderOnly(ids []string) (index.DocIDReader, error) {
+func (is *IndexSnapshot) DocIDReaderOnly(ids []string) (index.DocIDReader, error) {
 	results := make(chan *asynchSegmentResult)
-	for index, segment := range i.segment {
+	for index, segment := range is.segment {
 		go func(index int, segment *SegmentSnapshot) {
 			docs, err := segment.DocNumbers(ids)
 			if err != nil {
@@ -349,16 +344,16 @@ func (i *IndexSnapshot) DocIDReaderOnly(ids []string) (index.DocIDReader, error)
 		}(index, segment)
 	}
 
-	return i.newDocIDReader(results)
+	return is.newDocIDReader(results)
 }
 
-func (i *IndexSnapshot) newDocIDReader(results chan *asynchSegmentResult) (index.DocIDReader, error) {
+func (is *IndexSnapshot) newDocIDReader(results chan *asynchSegmentResult) (index.DocIDReader, error) {
 	rv := &IndexSnapshotDocIDReader{
-		snapshot:  i,
-		iterators: make([]roaring.IntIterable, len(i.segment)),
+		snapshot:  is,
+		iterators: make([]roaring.IntIterable, len(is.segment)),
 	}
 	var err error
-	for count := 0; count < len(i.segment); count++ {
+	for count := 0; count < len(is.segment); count++ {
 		asr := <-results
 		if asr.err != nil {
 			if err == nil {
@@ -377,11 +372,11 @@ func (i *IndexSnapshot) newDocIDReader(results chan *asynchSegmentResult) (index
 	return rv, nil
 }
 
-func (i *IndexSnapshot) Fields() ([]string, error) {
+func (is *IndexSnapshot) Fields() ([]string, error) {
 	// FIXME not making this concurrent for now as it's not used in hot path
 	// of any searches at the moment (just a debug aid)
 	fieldsMap := map[string]struct{}{}
-	for _, segment := range i.segment {
+	for _, segment := range is.segment {
 		fields := segment.Fields()
 		for _, field := range fields {
 			fieldsMap[field] = struct{}{}
@@ -394,21 +389,21 @@ func (i *IndexSnapshot) Fields() ([]string, error) {
 	return rv, nil
 }
 
-func (i *IndexSnapshot) GetInternal(key []byte) ([]byte, error) {
-	return i.internal[string(key)], nil
+func (is *IndexSnapshot) GetInternal(key []byte) ([]byte, error) {
+	return is.internal[string(key)], nil
 }
 
-func (i *IndexSnapshot) DocCount() (uint64, error) {
+func (is *IndexSnapshot) DocCount() (uint64, error) {
 	var rv uint64
-	for _, segment := range i.segment {
+	for _, segment := range is.segment {
 		rv += segment.Count()
 	}
 	return rv, nil
 }
 
-func (i *IndexSnapshot) Document(id string) (rv index.Document, err error) {
+func (is *IndexSnapshot) Document(id string) (rv index.Document, err error) {
 	// FIXME could be done more efficiently directly, but reusing for simplicity
-	tfr, err := i.TermFieldReader([]byte(id), "_id", false, false, false)
+	tfr, err := is.TermFieldReader(nil, []byte(id), "_id", false, false, false)
 	if err != nil {
 		return nil, err
 	}
@@ -432,19 +427,20 @@ func (i *IndexSnapshot) Document(id string) (rv index.Document, err error) {
 	if err != nil {
 		return nil, err
 	}
-	segmentIndex, localDocNum := i.segmentIndexAndLocalDocNumFromGlobal(docNum)
+	segmentIndex, localDocNum := is.segmentIndexAndLocalDocNumFromGlobal(docNum)
 
 	rvd := document.NewDocument(id)
-	var prevBytesRead uint64
-	seg, diskStatsAvailable := i.segment[segmentIndex].segment.(segment.DiskStatsReporter)
-	if diskStatsAvailable {
-		prevBytesRead = seg.BytesRead()
-	}
-	err = i.segment[segmentIndex].VisitDocument(localDocNum, func(name string, typ byte, val []byte, pos []uint64) bool {
+
+	err = is.segment[segmentIndex].VisitDocument(localDocNum, func(name string, typ byte, val []byte, pos []uint64) bool {
 		if name == "_id" {
 			return true
 		}
 
+		// track uncompressed stored fields bytes as part of IO stats.
+		// However, ideally we'd need to track the compressed on-disk value
+		// Keeping that TODO for now until we have a cleaner way.
+		rvd.StoredFieldsSize += uint64(len(val))
+
 		// copy value, array positions to preserve them beyond the scope of this callback
 		value := append([]byte(nil), val...)
 		arrayPos := append([]uint64(nil), pos...)
@@ -471,31 +467,28 @@ func (i *IndexSnapshot) Document(id string) (rv index.Document, err error) {
 	if err != nil {
 		return nil, err
 	}
-	if diskStatsAvailable {
-		delta := seg.BytesRead() - prevBytesRead
-		atomic.AddUint64(&i.parent.stats.TotBytesReadAtQueryTime, delta)
-	}
+
 	return rvd, nil
 }
 
-func (i *IndexSnapshot) segmentIndexAndLocalDocNumFromGlobal(docNum uint64) (int, uint64) {
-	segmentIndex := sort.Search(len(i.offsets),
+func (is *IndexSnapshot) segmentIndexAndLocalDocNumFromGlobal(docNum uint64) (int, uint64) {
+	segmentIndex := sort.Search(len(is.offsets),
 		func(x int) bool {
-			return i.offsets[x] > docNum
+			return is.offsets[x] > docNum
 		}) - 1
 
-	localDocNum := docNum - i.offsets[segmentIndex]
+	localDocNum := docNum - is.offsets[segmentIndex]
 	return int(segmentIndex), localDocNum
 }
 
-func (i *IndexSnapshot) ExternalID(id index.IndexInternalID) (string, error) {
+func (is *IndexSnapshot) ExternalID(id index.IndexInternalID) (string, error) {
 	docNum, err := docInternalToNumber(id)
 	if err != nil {
 		return "", err
 	}
-	segmentIndex, localDocNum := i.segmentIndexAndLocalDocNumFromGlobal(docNum)
+	segmentIndex, localDocNum := is.segmentIndexAndLocalDocNumFromGlobal(docNum)
 
-	v, err := i.segment[segmentIndex].DocID(localDocNum)
+	v, err := is.segment[segmentIndex].DocID(localDocNum)
 	if err != nil {
 		return "", err
 	}
@@ -506,9 +499,9 @@ func (i *IndexSnapshot) ExternalID(id index.IndexInternalID) (string, error) {
 	return string(v), nil
 }
 
-func (i *IndexSnapshot) InternalID(id string) (rv index.IndexInternalID, err error) {
+func (is *IndexSnapshot) InternalID(id string) (rv index.IndexInternalID, err error) {
 	// FIXME could be done more efficiently directly, but reusing for simplicity
-	tfr, err := i.TermFieldReader([]byte(id), "_id", false, false, false)
+	tfr, err := is.TermFieldReader(nil, []byte(id), "_id", false, false, false)
 	if err != nil {
 		return nil, err
 	}
@@ -526,10 +519,11 @@ func (i *IndexSnapshot) InternalID(id string) (rv index.IndexInternalID, err err
 	return next.ID, nil
 }
 
-func (is *IndexSnapshot) TermFieldReader(term []byte, field string, includeFreq,
+func (is *IndexSnapshot) TermFieldReader(ctx context.Context, term []byte, field string, includeFreq,
 	includeNorm, includeTermVectors bool) (index.TermFieldReader, error) {
 	rv := is.allocTermFieldReaderDicts(field)
 
+	rv.ctx = ctx
 	rv.term = term
 	rv.field = field
 	rv.snapshot = is
@@ -548,85 +542,79 @@ func (is *IndexSnapshot) TermFieldReader(term []byte, field string, includeFreq,
 
 	if rv.dicts == nil {
 		rv.dicts = make([]segment.TermDictionary, len(is.segment))
-		for i, segment := range is.segment {
-			var prevBytesRead uint64
-			segP, diskStatsAvailable := segment.segment.(diskStatsReporter)
-			if diskStatsAvailable {
-				prevBytesRead = segP.BytesRead()
-			}
-			dict, err := segment.segment.Dictionary(field)
+		for i, s := range is.segment {
+			segBytesRead := s.segment.BytesRead()
+			rv.incrementBytesRead(segBytesRead)
+			dict, err := s.segment.Dictionary(field)
 			if err != nil {
 				return nil, err
 			}
-			if diskStatsAvailable {
-				atomic.AddUint64(&is.parent.stats.TotBytesReadAtQueryTime, segP.BytesRead()-prevBytesRead)
+			if dictStats, ok := dict.(segment.DiskStatsReporter); ok {
+				bytesRead := dictStats.BytesRead()
+				rv.incrementBytesRead(bytesRead)
 			}
 			rv.dicts[i] = dict
 		}
 	}
 
-	for i, segment := range is.segment {
+	for i, s := range is.segment {
 		var prevBytesReadPL uint64
-		if postings, diskStatsAvailable := rv.postings[i].(diskStatsReporter); diskStatsAvailable {
-			prevBytesReadPL = postings.BytesRead()
+		if rv.postings[i] != nil {
+			prevBytesReadPL = rv.postings[i].BytesRead()
 		}
-		pl, err := rv.dicts[i].PostingsList(term, segment.deleted, rv.postings[i])
+		pl, err := rv.dicts[i].PostingsList(term, s.deleted, rv.postings[i])
 		if err != nil {
 			return nil, err
 		}
 		rv.postings[i] = pl
 
 		var prevBytesReadItr uint64
-		if itr, diskStatsAvailable := rv.iterators[i].(diskStatsReporter); diskStatsAvailable {
-			prevBytesReadItr = itr.BytesRead()
+		if rv.iterators[i] != nil {
+			prevBytesReadItr = rv.iterators[i].BytesRead()
 		}
 		rv.iterators[i] = pl.Iterator(includeFreq, includeNorm, includeTermVectors, rv.iterators[i])
 
-		if postings, diskStatsAvailable := pl.(diskStatsReporter); diskStatsAvailable &&
-			prevBytesReadPL < postings.BytesRead() {
-			atomic.AddUint64(&is.parent.stats.TotBytesReadAtQueryTime,
-				postings.BytesRead()-prevBytesReadPL)
+		if bytesRead := rv.postings[i].BytesRead(); prevBytesReadPL < bytesRead {
+			rv.incrementBytesRead(bytesRead - prevBytesReadPL)
 		}
 
-		if itr, diskStatsAvailable := rv.iterators[i].(diskStatsReporter); diskStatsAvailable &&
-			prevBytesReadItr < itr.BytesRead() {
-			atomic.AddUint64(&is.parent.stats.TotBytesReadAtQueryTime,
-				itr.BytesRead()-prevBytesReadItr)
+		if bytesRead := rv.iterators[i].BytesRead(); prevBytesReadItr < bytesRead {
+			rv.incrementBytesRead(bytesRead - prevBytesReadItr)
 		}
 	}
 	atomic.AddUint64(&is.parent.stats.TotTermSearchersStarted, uint64(1))
 	return rv, nil
 }
 
-func (i *IndexSnapshot) allocTermFieldReaderDicts(field string) (tfr *IndexSnapshotTermFieldReader) {
-	i.m2.Lock()
-	if i.fieldTFRs != nil {
-		tfrs := i.fieldTFRs[field]
+func (is *IndexSnapshot) allocTermFieldReaderDicts(field string) (tfr *IndexSnapshotTermFieldReader) {
+	is.m2.Lock()
+	if is.fieldTFRs != nil {
+		tfrs := is.fieldTFRs[field]
 		last := len(tfrs) - 1
 		if last >= 0 {
 			tfr = tfrs[last]
 			tfrs[last] = nil
-			i.fieldTFRs[field] = tfrs[:last]
-			i.m2.Unlock()
+			is.fieldTFRs[field] = tfrs[:last]
+			is.m2.Unlock()
 			return
 		}
 	}
-	i.m2.Unlock()
+	is.m2.Unlock()
 	return &IndexSnapshotTermFieldReader{
 		recycle: true,
 	}
 }
 
-func (i *IndexSnapshot) getFieldTFRCacheThreshold() uint64 {
-	if i.parent.config != nil {
-		if _, ok := i.parent.config["FieldTFRCacheThreshold"]; ok {
-			return i.parent.config["FieldTFRCacheThreshold"].(uint64)
+func (is *IndexSnapshot) getFieldTFRCacheThreshold() uint64 {
+	if is.parent.config != nil {
+		if _, ok := is.parent.config["FieldTFRCacheThreshold"]; ok {
+			return is.parent.config["FieldTFRCacheThreshold"].(uint64)
 		}
 	}
 	return DefaultFieldTFRCacheThreshold
 }
 
-func (i *IndexSnapshot) recycleTermFieldReader(tfr *IndexSnapshotTermFieldReader) {
+func (is *IndexSnapshot) recycleTermFieldReader(tfr *IndexSnapshotTermFieldReader) {
 	if !tfr.recycle {
 		// Do not recycle an optimized unadorned term field reader (used for
 		// ConjunctionUnadorned or DisjunctionUnadorned), during when a fresh
@@ -635,22 +623,23 @@ func (i *IndexSnapshot) recycleTermFieldReader(tfr *IndexSnapshotTermFieldReader
 		return
 	}
 
-	i.parent.rootLock.RLock()
-	obsolete := i.parent.root != i
-	i.parent.rootLock.RUnlock()
+	is.parent.rootLock.RLock()
+	obsolete := is.parent.root != is
+	is.parent.rootLock.RUnlock()
 	if obsolete {
 		// if we're not the current root (mutations happened), don't bother recycling
 		return
 	}
 
-	i.m2.Lock()
-	if i.fieldTFRs == nil {
-		i.fieldTFRs = map[string][]*IndexSnapshotTermFieldReader{}
+	is.m2.Lock()
+	if is.fieldTFRs == nil {
+		is.fieldTFRs = map[string][]*IndexSnapshotTermFieldReader{}
 	}
-	if uint64(len(i.fieldTFRs[tfr.field])) < i.getFieldTFRCacheThreshold() {
-		i.fieldTFRs[tfr.field] = append(i.fieldTFRs[tfr.field], tfr)
+	if uint64(len(is.fieldTFRs[tfr.field])) < is.getFieldTFRCacheThreshold() {
+		tfr.bytesRead = 0
+		is.fieldTFRs[tfr.field] = append(is.fieldTFRs[tfr.field], tfr)
 	}
-	i.m2.Unlock()
+	is.m2.Unlock()
 }
 
 func docNumberToBytes(buf []byte, in uint64) []byte {
@@ -672,11 +661,11 @@ func docInternalToNumber(in index.IndexInternalID) (uint64, error) {
 	return binary.BigEndian.Uint64(in), nil
 }
 
-func (i *IndexSnapshot) documentVisitFieldTermsOnSegment(
+func (is *IndexSnapshot) documentVisitFieldTermsOnSegment(
 	segmentIndex int, localDocNum uint64, fields []string, cFields []string,
 	visitor index.DocValueVisitor, dvs segment.DocVisitState) (
 	cFieldsOut []string, dvsOut segment.DocVisitState, err error) {
-	ss := i.segment[segmentIndex]
+	ss := is.segment[segmentIndex]
 
 	var vFields []string // fields that are visitable via the segment
 
@@ -711,18 +700,10 @@ func (i *IndexSnapshot) documentVisitFieldTermsOnSegment(
 	}
 
 	if ssvOk && ssv != nil && len(vFields) > 0 {
-		var prevBytesRead uint64
-		ssvp, diskStatsAvailable := ssv.(segment.DiskStatsReporter)
-		if diskStatsAvailable {
-			prevBytesRead = ssvp.BytesRead()
-		}
 		dvs, err = ssv.VisitDocValues(localDocNum, fields, visitor, dvs)
 		if err != nil {
 			return nil, nil, err
 		}
-		if diskStatsAvailable {
-			atomic.AddUint64(&i.parent.stats.TotBytesReadAtQueryTime, ssvp.BytesRead()-prevBytesRead)
-		}
 	}
 
 	if errCh != nil {
@@ -739,9 +720,9 @@ func (i *IndexSnapshot) documentVisitFieldTermsOnSegment(
 	return cFields, dvs, nil
 }
 
-func (i *IndexSnapshot) DocValueReader(fields []string) (
+func (is *IndexSnapshot) DocValueReader(fields []string) (
 	index.DocValueReader, error) {
-	return &DocValueReader{i: i, fields: fields, currSegmentIndex: -1}, nil
+	return &DocValueReader{i: is, fields: fields, currSegmentIndex: -1}, nil
 }
 
 type DocValueReader struct {
@@ -751,6 +732,13 @@ type DocValueReader struct {
 
 	currSegmentIndex int
 	currCachedFields []string
+
+	totalBytesRead uint64
+	bytesRead      uint64
+}
+
+func (dvr *DocValueReader) BytesRead() uint64 {
+	return dvr.totalBytesRead + dvr.bytesRead
 }
 
 func (dvr *DocValueReader) VisitDocValues(id index.IndexInternalID,
@@ -768,15 +756,20 @@ func (dvr *DocValueReader) VisitDocValues(id index.IndexInternalID,
 	if dvr.currSegmentIndex != segmentIndex {
 		dvr.currSegmentIndex = segmentIndex
 		dvr.currCachedFields = nil
+		dvr.totalBytesRead += dvr.bytesRead
+		dvr.bytesRead = 0
 	}
 
 	dvr.currCachedFields, dvr.dvs, err = dvr.i.documentVisitFieldTermsOnSegment(
 		dvr.currSegmentIndex, localDocNum, dvr.fields, dvr.currCachedFields, visitor, dvr.dvs)
 
+	if dvr.dvs != nil {
+		dvr.bytesRead = dvr.dvs.BytesRead()
+	}
 	return err
 }
 
-func (i *IndexSnapshot) DumpAll() chan interface{} {
+func (is *IndexSnapshot) DumpAll() chan interface{} {
 	rv := make(chan interface{})
 	go func() {
 		close(rv)
@@ -784,7 +777,7 @@ func (i *IndexSnapshot) DumpAll() chan interface{} {
 	return rv
 }
 
-func (i *IndexSnapshot) DumpDoc(id string) chan interface{} {
+func (is *IndexSnapshot) DumpDoc(id string) chan interface{} {
 	rv := make(chan interface{})
 	go func() {
 		close(rv)
@@ -792,7 +785,7 @@ func (i *IndexSnapshot) DumpDoc(id string) chan interface{} {
 	return rv
 }
 
-func (i *IndexSnapshot) DumpFields() chan interface{} {
+func (is *IndexSnapshot) DumpFields() chan interface{} {
 	rv := make(chan interface{})
 	go func() {
 		close(rv)
@@ -800,10 +793,10 @@ func (i *IndexSnapshot) DumpFields() chan interface{} {
 	return rv
 }
 
-func (i *IndexSnapshot) diskSegmentsPaths() map[string]struct{} {
-	rv := make(map[string]struct{}, len(i.segment))
-	for _, segmentSnapshot := range i.segment {
-		if seg, ok := segmentSnapshot.segment.(segment.PersistedSegment); ok {
+func (is *IndexSnapshot) diskSegmentsPaths() map[string]struct{} {
+	rv := make(map[string]struct{}, len(is.segment))
+	for _, s := range is.segment {
+		if seg, ok := s.segment.(segment.PersistedSegment); ok {
 			rv[seg.Path()] = struct{}{}
 		}
 	}
@@ -812,12 +805,12 @@ func (i *IndexSnapshot) diskSegmentsPaths() map[string]struct{} {
 
 // reClaimableDocsRatio gives a ratio about the obsoleted or
 // reclaimable documents present in a given index snapshot.
-func (i *IndexSnapshot) reClaimableDocsRatio() float64 {
+func (is *IndexSnapshot) reClaimableDocsRatio() float64 {
 	var totalCount, liveCount uint64
-	for _, segmentSnapshot := range i.segment {
-		if _, ok := segmentSnapshot.segment.(segment.PersistedSegment); ok {
-			totalCount += uint64(segmentSnapshot.FullSize())
-			liveCount += uint64(segmentSnapshot.Count())
+	for _, s := range is.segment {
+		if _, ok := s.segment.(segment.PersistedSegment); ok {
+			totalCount += uint64(s.FullSize())
+			liveCount += uint64(s.Count())
 		}
 	}
 
@@ -846,7 +839,7 @@ OUTER:
 	return rv
 }
 
-func (i *IndexSnapshot) CopyTo(d index.Directory) error {
+func (is *IndexSnapshot) CopyTo(d index.Directory) error {
 	// get the root bolt file.
 	w, err := d.GetWriter(filepath.Join("store", "root.bolt"))
 	if err != nil || w == nil {
@@ -874,7 +867,7 @@ func (i *IndexSnapshot) CopyTo(d index.Directory) error {
 		return err
 	}
 
-	_, _, err = prepareBoltSnapshot(i, tx, "", i.parent.segPlugin, d)
+	_, _, err = prepareBoltSnapshot(is, tx, "", is.parent.segPlugin, d)
 	if err != nil {
 		_ = tx.Rollback()
 		return fmt.Errorf("error backing up index snapshot: %v", err)
@@ -889,12 +882,16 @@ func (i *IndexSnapshot) CopyTo(d index.Directory) error {
 	return copyBolt.Sync()
 }
 
-func (i *IndexSnapshot) GetSpatialAnalyzerPlugin(typ string) (
+func (is *IndexSnapshot) UpdateIOStats(val uint64) {
+	atomic.AddUint64(&is.parent.stats.TotBytesReadAtQueryTime, val)
+}
+
+func (is *IndexSnapshot) GetSpatialAnalyzerPlugin(typ string) (
 	index.SpatialAnalyzerPlugin, error) {
 	var rv index.SpatialAnalyzerPlugin
-	i.m.Lock()
-	rv = i.parent.spatialPlugin
-	i.m.Unlock()
+	is.m.Lock()
+	rv = is.parent.spatialPlugin
+	is.m.Unlock()
 
 	if rv == nil {
 		return nil, fmt.Errorf("no spatial plugin type: %s found", typ)
diff --git a/index/scorch/snapshot_index_dict.go b/index/scorch/snapshot_index_dict.go
index 0a2bd232..658aa814 100644
--- a/index/scorch/snapshot_index_dict.go
+++ b/index/scorch/snapshot_index_dict.go
@@ -28,9 +28,14 @@ type segmentDictCursor struct {
 }
 
 type IndexSnapshotFieldDict struct {
-	snapshot *IndexSnapshot
-	cursors  []*segmentDictCursor
-	entry    index.DictEntry
+	snapshot  *IndexSnapshot
+	cursors   []*segmentDictCursor
+	entry     index.DictEntry
+	bytesRead uint64
+}
+
+func (i *IndexSnapshotFieldDict) BytesRead() uint64 {
+	return i.bytesRead
 }
 
 func (i *IndexSnapshotFieldDict) Len() int { return len(i.cursors) }
diff --git a/index/scorch/snapshot_index_tfr.go b/index/scorch/snapshot_index_tfr.go
index 7283b137..349620c7 100644
--- a/index/scorch/snapshot_index_tfr.go
+++ b/index/scorch/snapshot_index_tfr.go
@@ -16,10 +16,12 @@ package scorch
 
 import (
 	"bytes"
+	"context"
 	"fmt"
 	"reflect"
 	"sync/atomic"
 
+	"github.com/blevesearch/bleve/v2/search"
 	"github.com/blevesearch/bleve/v2/size"
 	index "github.com/blevesearch/bleve_index_api"
 	segment "github.com/blevesearch/scorch_segment_api/v2"
@@ -46,6 +48,12 @@ type IndexSnapshotTermFieldReader struct {
 	currPosting        segment.Posting
 	currID             index.IndexInternalID
 	recycle            bool
+	bytesRead          uint64
+	ctx                context.Context
+}
+
+func (i *IndexSnapshotTermFieldReader) incrementBytesRead(val uint64) {
+	i.bytesRead += val
 }
 
 func (i *IndexSnapshotTermFieldReader) Size() int {
@@ -76,11 +84,7 @@ func (i *IndexSnapshotTermFieldReader) Next(preAlloced *index.TermFieldDoc) (*in
 	}
 	// find the next hit
 	for i.segmentOffset < len(i.iterators) {
-		prevBytesRead := uint64(0)
-		itr, diskStatsAvailable := i.iterators[i.segmentOffset].(segment.DiskStatsReporter)
-		if diskStatsAvailable {
-			prevBytesRead = itr.BytesRead()
-		}
+		prevBytesRead := i.iterators[i.segmentOffset].BytesRead()
 		next, err := i.iterators[i.segmentOffset].Next()
 		if err != nil {
 			return nil, err
@@ -98,9 +102,8 @@ func (i *IndexSnapshotTermFieldReader) Next(preAlloced *index.TermFieldDoc) (*in
 			// this is because there are chances of having a series of loadChunk calls,
 			// and they have to be added together before sending the bytesRead at this point
 			// upstream.
-			if diskStatsAvailable {
-				delta := itr.BytesRead() - prevBytesRead
-				atomic.AddUint64(&i.snapshot.parent.stats.TotBytesReadAtQueryTime, uint64(delta))
+			if delta := i.iterators[i.segmentOffset].BytesRead() - prevBytesRead; delta > 0 {
+				i.incrementBytesRead(delta)
 			}
 
 			return rv, nil
@@ -143,7 +146,7 @@ func (i *IndexSnapshotTermFieldReader) Advance(ID index.IndexInternalID, preAllo
 	// FIXME do something better
 	// for now, if we need to seek backwards, then restart from the beginning
 	if i.currPosting != nil && bytes.Compare(i.currID, ID) >= 0 {
-		i2, err := i.snapshot.TermFieldReader(i.term, i.field,
+		i2, err := i.snapshot.TermFieldReader(nil, i.term, i.field,
 			i.includeFreq, i.includeNorm, i.includeTermVectors)
 		if err != nil {
 			return nil, err
@@ -194,6 +197,15 @@ func (i *IndexSnapshotTermFieldReader) Count() uint64 {
 }
 
 func (i *IndexSnapshotTermFieldReader) Close() error {
+	if i.ctx != nil {
+		statsCallbackFn := i.ctx.Value(search.SearchIOStatsCallbackKey)
+		if statsCallbackFn != nil {
+			// essentially before you close the TFR, you must report this
+			// reader's bytesRead value
+			statsCallbackFn.(search.SearchIOStatsCallbackFunc)(i.bytesRead)
+		}
+	}
+
 	if i.snapshot != nil {
 		atomic.AddUint64(&i.snapshot.parent.stats.TotTermSearchersFinished, uint64(1))
 		i.snapshot.recycleTermFieldReader(i)
diff --git a/index/scorch/unadorned.go b/index/scorch/unadorned.go
index 855b8136..8221b23e 100644
--- a/index/scorch/unadorned.go
+++ b/index/scorch/unadorned.go
@@ -15,10 +15,11 @@
 package scorch
 
 import (
-	"github.com/RoaringBitmap/roaring"
-	segment "github.com/blevesearch/scorch_segment_api/v2"
 	"math"
 	"reflect"
+
+	"github.com/RoaringBitmap/roaring"
+	segment "github.com/blevesearch/scorch_segment_api/v2"
 )
 
 var reflectStaticSizeUnadornedPostingsIteratorBitmap int
@@ -72,6 +73,16 @@ func (i *unadornedPostingsIteratorBitmap) Size() int {
 	return reflectStaticSizeUnadornedPostingsIteratorBitmap
 }
 
+func (i *unadornedPostingsIteratorBitmap) BytesRead() uint64 {
+	return 0
+}
+
+func (i *unadornedPostingsIteratorBitmap) BytesWritten() uint64 {
+	return 0
+}
+
+func (i *unadornedPostingsIteratorBitmap) ResetBytesRead(uint64) {}
+
 func (i *unadornedPostingsIteratorBitmap) ActualBitmap() *roaring.Bitmap {
 	return i.actualBM
 }
@@ -132,6 +143,16 @@ func (i *unadornedPostingsIterator1Hit) Size() int {
 	return reflectStaticSizeUnadornedPostingsIterator1Hit
 }
 
+func (i *unadornedPostingsIterator1Hit) BytesRead() uint64 {
+	return 0
+}
+
+func (i *unadornedPostingsIterator1Hit) BytesWritten() uint64 {
+	return 0
+}
+
+func (i *unadornedPostingsIterator1Hit) ResetBytesRead(uint64) {}
+
 func newUnadornedPostingsIteratorFrom1Hit(docNum1Hit uint64) segment.PostingsIterator {
 	return &unadornedPostingsIterator1Hit{
 		docNum1Hit,
diff --git a/index/upsidedown/field_dict.go b/index/upsidedown/field_dict.go
index c4be5774..4875680c 100644
--- a/index/upsidedown/field_dict.go
+++ b/index/upsidedown/field_dict.go
@@ -51,6 +51,10 @@ func newUpsideDownCouchFieldDict(indexReader *IndexReader, field uint16, startTe
 
 }
 
+func (r *UpsideDownCouchFieldDict) BytesRead() uint64 {
+	return 0
+}
+
 func (r *UpsideDownCouchFieldDict) Next() (*index.DictEntry, error) {
 	key, val, valid := r.iterator.Current()
 	if !valid {
diff --git a/index/upsidedown/index_reader.go b/index/upsidedown/index_reader.go
index ff0986d5..5c164fc8 100644
--- a/index/upsidedown/index_reader.go
+++ b/index/upsidedown/index_reader.go
@@ -15,6 +15,7 @@
 package upsidedown
 
 import (
+	"context"
 	"reflect"
 
 	"github.com/blevesearch/bleve/v2/document"
@@ -35,7 +36,7 @@ type IndexReader struct {
 	docCount uint64
 }
 
-func (i *IndexReader) TermFieldReader(term []byte, fieldName string, includeFreq, includeNorm, includeTermVectors bool) (index.TermFieldReader, error) {
+func (i *IndexReader) TermFieldReader(ctx context.Context, term []byte, fieldName string, includeFreq, includeNorm, includeTermVectors bool) (index.TermFieldReader, error) {
 	fieldIndex, fieldExists := i.index.fieldCache.FieldNamed(fieldName, false)
 	if fieldExists {
 		return newUpsideDownCouchTermFieldReader(i, term, uint16(fieldIndex), includeFreq, includeNorm, includeTermVectors)
@@ -223,3 +224,5 @@ func (dvr *DocValueReader) VisitDocValues(id index.IndexInternalID,
 	visitor index.DocValueVisitor) error {
 	return dvr.i.documentVisitFieldTerms(id, dvr.fields, visitor)
 }
+
+func (dvr *DocValueReader) BytesRead() uint64 { return 0 }
diff --git a/index/upsidedown/store/goleveldb/batch.go b/index/upsidedown/store/goleveldb/batch.go
index 9307c090..15365886 100644
--- a/index/upsidedown/store/goleveldb/batch.go
+++ b/index/upsidedown/store/goleveldb/batch.go
@@ -15,8 +15,8 @@
 package goleveldb
 
 import (
-	store "github.com/blevesearch/upsidedown_store_api"
 	"github.com/blevesearch/goleveldb/leveldb"
+	store "github.com/blevesearch/upsidedown_store_api"
 )
 
 type Batch struct {
diff --git a/index/upsidedown/store/goleveldb/reader.go b/index/upsidedown/store/goleveldb/reader.go
index 3d8b4bb4..9fa3a0ef 100644
--- a/index/upsidedown/store/goleveldb/reader.go
+++ b/index/upsidedown/store/goleveldb/reader.go
@@ -15,9 +15,9 @@
 package goleveldb
 
 import (
-	store "github.com/blevesearch/upsidedown_store_api"
 	"github.com/blevesearch/goleveldb/leveldb"
 	"github.com/blevesearch/goleveldb/leveldb/util"
+	store "github.com/blevesearch/upsidedown_store_api"
 )
 
 type Reader struct {
diff --git a/index/upsidedown/store/goleveldb/store.go b/index/upsidedown/store/goleveldb/store.go
index 9546df02..6e8fbfd1 100644
--- a/index/upsidedown/store/goleveldb/store.go
+++ b/index/upsidedown/store/goleveldb/store.go
@@ -20,10 +20,10 @@ import (
 	"os"
 
 	"github.com/blevesearch/bleve/v2/registry"
-	store "github.com/blevesearch/upsidedown_store_api"
 	"github.com/blevesearch/goleveldb/leveldb"
 	"github.com/blevesearch/goleveldb/leveldb/opt"
 	"github.com/blevesearch/goleveldb/leveldb/util"
+	store "github.com/blevesearch/upsidedown_store_api"
 )
 
 const (
diff --git a/index/upsidedown/store/goleveldb/writer.go b/index/upsidedown/store/goleveldb/writer.go
index ed2ccabb..8dfd30cf 100644
--- a/index/upsidedown/store/goleveldb/writer.go
+++ b/index/upsidedown/store/goleveldb/writer.go
@@ -17,8 +17,8 @@ package goleveldb
 import (
 	"fmt"
 
-	store "github.com/blevesearch/upsidedown_store_api"
 	"github.com/blevesearch/goleveldb/leveldb"
+	store "github.com/blevesearch/upsidedown_store_api"
 )
 
 type Writer struct {
diff --git a/index/upsidedown/store/gtreap/store.go b/index/upsidedown/store/gtreap/store.go
index 760ea121..3cc7eb9a 100644
--- a/index/upsidedown/store/gtreap/store.go
+++ b/index/upsidedown/store/gtreap/store.go
@@ -25,8 +25,8 @@ import (
 	"sync"
 
 	"github.com/blevesearch/bleve/v2/registry"
-	"github.com/blevesearch/upsidedown_store_api"
 	"github.com/blevesearch/gtreap"
+	"github.com/blevesearch/upsidedown_store_api"
 )
 
 const Name = "gtreap"
diff --git a/index/upsidedown/store/metrics/store.go b/index/upsidedown/store/metrics/store.go
index abdf4fcd..f537728f 100644
--- a/index/upsidedown/store/metrics/store.go
+++ b/index/upsidedown/store/metrics/store.go
@@ -26,8 +26,8 @@ import (
 	"time"
 
 	"github.com/blevesearch/bleve/v2/registry"
-	store "github.com/blevesearch/upsidedown_store_api"
 	"github.com/blevesearch/go-metrics"
+	store "github.com/blevesearch/upsidedown_store_api"
 )
 
 const Name = "metrics"
diff --git a/index_alias_impl.go b/index_alias_impl.go
index 5a4dc5a4..a73dd6b8 100644
--- a/index_alias_impl.go
+++ b/index_alias_impl.go
@@ -602,6 +602,10 @@ type indexAliasImplFieldDict struct {
 	fieldDict index.FieldDict
 }
 
+func (f *indexAliasImplFieldDict) BytesRead() uint64 {
+	return f.fieldDict.BytesRead()
+}
+
 func (f *indexAliasImplFieldDict) Next() (*index.DictEntry, error) {
 	return f.fieldDict.Next()
 }
diff --git a/index_impl.go b/index_impl.go
index 1945568e..c5a0c46f 100644
--- a/index_impl.go
+++ b/index_impl.go
@@ -26,6 +26,7 @@ import (
 	"time"
 
 	"github.com/blevesearch/bleve/v2/document"
+	"github.com/blevesearch/bleve/v2/index/scorch"
 	"github.com/blevesearch/bleve/v2/index/upsidedown"
 	"github.com/blevesearch/bleve/v2/mapping"
 	"github.com/blevesearch/bleve/v2/registry"
@@ -468,7 +469,20 @@ func (i *indexImpl) SearchInContext(ctx context.Context, req *SearchRequest) (sr
 		}
 	}()
 
-	searcher, err := req.Query.Searcher(indexReader, i.m, search.SearcherOptions{
+	// This callback and variable handles the tracking of bytes read
+	//  1. as part of creation of tfr and its Next() calls which is
+	//     accounted by invoking this callback when the TFR is closed.
+	//  2. the docvalues portion (accounted in collector) and the retrieval
+	//     of stored fields bytes (by LoadAndHighlightFields)
+	var totalBytesRead uint64
+	sendBytesRead := func(bytesRead uint64) {
+		totalBytesRead += bytesRead
+	}
+
+	ctx = context.WithValue(ctx, search.SearchIOStatsCallbackKey,
+		search.SearchIOStatsCallbackFunc(sendBytesRead))
+
+	searcher, err := req.Query.Searcher(ctx, indexReader, i.m, search.SearcherOptions{
 		Explain:            req.Explain,
 		IncludeTermVectors: req.IncludeLocations || req.Highlight != nil,
 		Score:              req.Score,
@@ -480,6 +494,12 @@ func (i *indexImpl) SearchInContext(ctx context.Context, req *SearchRequest) (sr
 		if serr := searcher.Close(); err == nil && serr != nil {
 			err = serr
 		}
+		if sr != nil {
+			sr.BytesRead = totalBytesRead
+		}
+		if sr, ok := indexReader.(*scorch.IndexSnapshot); ok {
+			sr.UpdateIOStats(totalBytesRead)
+		}
 	}()
 
 	if req.Facets != nil {
@@ -558,10 +578,11 @@ func (i *indexImpl) SearchInContext(ctx context.Context, req *SearchRequest) (sr
 		if i.name != "" {
 			hit.Index = i.name
 		}
-		err = LoadAndHighlightFields(hit, req, i.name, indexReader, highlighter)
+		err, storedFieldsBytes := LoadAndHighlightFields(hit, req, i.name, indexReader, highlighter)
 		if err != nil {
 			return nil, err
 		}
+		totalBytesRead += storedFieldsBytes
 	}
 
 	atomic.AddUint64(&i.stats.searches, 1)
@@ -600,9 +621,11 @@ func (i *indexImpl) SearchInContext(ctx context.Context, req *SearchRequest) (sr
 
 func LoadAndHighlightFields(hit *search.DocumentMatch, req *SearchRequest,
 	indexName string, r index.IndexReader,
-	highlighter highlight.Highlighter) error {
+	highlighter highlight.Highlighter) (error, uint64) {
+	var totalStoredFieldsBytes uint64
 	if len(req.Fields) > 0 || highlighter != nil {
 		doc, err := r.Document(hit.ID)
+		totalStoredFieldsBytes = doc.StoredFieldsBytes()
 		if err == nil && doc != nil {
 			if len(req.Fields) > 0 {
 				fieldsToLoad := deDuplicate(req.Fields)
@@ -666,11 +689,11 @@ func LoadAndHighlightFields(hit *search.DocumentMatch, req *SearchRequest,
 		} else if doc == nil {
 			// unexpected case, a doc ID that was found as a search hit
 			// was unable to be found during document lookup
-			return ErrorIndexReadInconsistency
+			return ErrorIndexReadInconsistency, 0
 		}
 	}
 
-	return nil
+	return nil, totalStoredFieldsBytes
 }
 
 // Fields returns the name of all the fields this
@@ -870,6 +893,10 @@ type indexImplFieldDict struct {
 	fieldDict   index.FieldDict
 }
 
+func (f *indexImplFieldDict) BytesRead() uint64 {
+	return f.fieldDict.BytesRead()
+}
+
 func (f *indexImplFieldDict) Next() (*index.DictEntry, error) {
 	return f.fieldDict.Next()
 }
diff --git a/mapping.go b/mapping.go
index c8db45df..723105a2 100644
--- a/mapping.go
+++ b/mapping.go
@@ -70,6 +70,10 @@ func NewGeoPointFieldMapping() *mapping.FieldMapping {
 	return mapping.NewGeoPointFieldMapping()
 }
 
+func NewGeoShapeFieldMapping() *mapping.FieldMapping {
+	return mapping.NewGeoShapeFieldMapping()
+}
+
 func NewIPFieldMapping() *mapping.FieldMapping {
 	return mapping.NewIPFieldMapping()
 }
diff --git a/mapping/field.go b/mapping/field.go
index 8ed81caf..511782ac 100644
--- a/mapping/field.go
+++ b/mapping/field.go
@@ -172,24 +172,25 @@ func NewGeoPointFieldMapping() *FieldMapping {
 	}
 }
 
-// NewIPFieldMapping returns a default field mapping for IP points
-func NewIPFieldMapping() *FieldMapping {
+// NewGeoShapeFieldMapping returns a default field mapping
+// for geoshapes
+func NewGeoShapeFieldMapping() *FieldMapping {
 	return &FieldMapping{
-		Type:         "IP",
+		Type:         "geoshape",
 		Store:        true,
 		Index:        true,
 		IncludeInAll: true,
+		DocValues:    true,
 	}
 }
 
-// NewGeoShapeFieldMapping returns a default field mapping
-// for geoshapes
-func NewGeoShapeFieldMapping() *FieldMapping {
+// NewIPFieldMapping returns a default field mapping for IP points
+func NewIPFieldMapping() *FieldMapping {
 	return &FieldMapping{
-		Type:         "geoshape",
+		Type:         "IP",
+		Store:        true,
 		Index:        true,
 		IncludeInAll: true,
-		DocValues:    true,
 	}
 }
 
@@ -362,7 +363,7 @@ func (fm *FieldMapping) processGeoShape(propertyMightBeGeoShape interface{},
 	}
 }
 
-func (fm *FieldMapping) analyzerForField(path []string, context *walkContext) *analysis.Analyzer {
+func (fm *FieldMapping) analyzerForField(path []string, context *walkContext) analysis.Analyzer {
 	analyzerName := fm.Analyzer
 	if analyzerName == "" {
 		analyzerName = context.dm.defaultAnalyzerName(path)
diff --git a/mapping/index.go b/mapping/index.go
index c3434338..f97442ff 100644
--- a/mapping/index.go
+++ b/mapping/index.go
@@ -386,7 +386,7 @@ func (im *IndexMappingImpl) AnalyzerNameForPath(path string) string {
 	return im.DefaultAnalyzer
 }
 
-func (im *IndexMappingImpl) AnalyzerNamed(name string) *analysis.Analyzer {
+func (im *IndexMappingImpl) AnalyzerNamed(name string) analysis.Analyzer {
 	analyzer, err := im.cache.AnalyzerNamed(name)
 	if err != nil {
 		logger.Printf("error using analyzer named: %s", name)
diff --git a/mapping/mapping.go b/mapping/mapping.go
index bcf6749a..2506352e 100644
--- a/mapping/mapping.go
+++ b/mapping/mapping.go
@@ -54,5 +54,5 @@ type IndexMapping interface {
 	DefaultSearchField() string
 
 	AnalyzerNameForPath(path string) string
-	AnalyzerNamed(name string) *analysis.Analyzer
+	AnalyzerNamed(name string) analysis.Analyzer
 }
diff --git a/registry/analyzer.go b/registry/analyzer.go
index 6c0d73b3..f4753bc1 100644
--- a/registry/analyzer.go
+++ b/registry/analyzer.go
@@ -28,7 +28,7 @@ func RegisterAnalyzer(name string, constructor AnalyzerConstructor) {
 	analyzers[name] = constructor
 }
 
-type AnalyzerConstructor func(config map[string]interface{}, cache *Cache) (*analysis.Analyzer, error)
+type AnalyzerConstructor func(config map[string]interface{}, cache *Cache) (analysis.Analyzer, error)
 type AnalyzerRegistry map[string]AnalyzerConstructor
 
 type AnalyzerCache struct {
@@ -53,15 +53,15 @@ func AnalyzerBuild(name string, config map[string]interface{}, cache *Cache) (in
 	return analyzer, nil
 }
 
-func (c *AnalyzerCache) AnalyzerNamed(name string, cache *Cache) (*analysis.Analyzer, error) {
+func (c *AnalyzerCache) AnalyzerNamed(name string, cache *Cache) (analysis.Analyzer, error) {
 	item, err := c.ItemNamed(name, cache, AnalyzerBuild)
 	if err != nil {
 		return nil, err
 	}
-	return item.(*analysis.Analyzer), nil
+	return item.(analysis.Analyzer), nil
 }
 
-func (c *AnalyzerCache) DefineAnalyzer(name string, typ string, config map[string]interface{}, cache *Cache) (*analysis.Analyzer, error) {
+func (c *AnalyzerCache) DefineAnalyzer(name string, typ string, config map[string]interface{}, cache *Cache) (analysis.Analyzer, error) {
 	item, err := c.DefineItem(name, typ, config, cache, AnalyzerBuild)
 	if err != nil {
 		if err == ErrAlreadyDefined {
@@ -69,7 +69,7 @@ func (c *AnalyzerCache) DefineAnalyzer(name string, typ string, config map[strin
 		}
 		return nil, err
 	}
-	return item.(*analysis.Analyzer), nil
+	return item.(analysis.Analyzer), nil
 }
 
 func AnalyzerTypesAndInstances() ([]string, []string) {
diff --git a/registry/registry.go b/registry/registry.go
index 02125e64..1954d089 100644
--- a/registry/registry.go
+++ b/registry/registry.go
@@ -123,11 +123,11 @@ func (c *Cache) DefineTokenFilter(name string, config map[string]interface{}) (a
 	return c.TokenFilters.DefineTokenFilter(name, typ, config, c)
 }
 
-func (c *Cache) AnalyzerNamed(name string) (*analysis.Analyzer, error) {
+func (c *Cache) AnalyzerNamed(name string) (analysis.Analyzer, error) {
 	return c.Analyzers.AnalyzerNamed(name, c)
 }
 
-func (c *Cache) DefineAnalyzer(name string, config map[string]interface{}) (*analysis.Analyzer, error) {
+func (c *Cache) DefineAnalyzer(name string, config map[string]interface{}) (analysis.Analyzer, error) {
 	typ, err := typeFromConfig(config)
 	if err != nil {
 		return nil, err
diff --git a/search.go b/search.go
index 2276aa01..2178d1b8 100644
--- a/search.go
+++ b/search.go
@@ -486,13 +486,14 @@ func (ss *SearchStatus) Merge(other *SearchStatus) {
 // A SearchResult describes the results of executing
 // a SearchRequest.
 type SearchResult struct {
-	Status   *SearchStatus                  `json:"status"`
-	Request  *SearchRequest                 `json:"request"`
-	Hits     search.DocumentMatchCollection `json:"hits"`
-	Total    uint64                         `json:"total_hits"`
-	MaxScore float64                        `json:"max_score"`
-	Took     time.Duration                  `json:"took"`
-	Facets   search.FacetResults            `json:"facets"`
+	Status    *SearchStatus                  `json:"status"`
+	Request   *SearchRequest                 `json:"request"`
+	Hits      search.DocumentMatchCollection `json:"hits"`
+	Total     uint64                         `json:"total_hits"`
+	BytesRead uint64                         `json:"bytesRead,omitempty"`
+	MaxScore  float64                        `json:"max_score"`
+	Took      time.Duration                  `json:"took"`
+	Facets    search.FacetResults            `json:"facets"`
 }
 
 func (sr *SearchResult) Size() int {
@@ -559,6 +560,7 @@ func (sr *SearchResult) Merge(other *SearchResult) {
 	sr.Status.Merge(other.Status)
 	sr.Hits = append(sr.Hits, other.Hits...)
 	sr.Total += other.Total
+	sr.BytesRead += other.BytesRead
 	if other.MaxScore > sr.MaxScore {
 		sr.MaxScore = other.MaxScore
 	}
diff --git a/search/collector/topn.go b/search/collector/topn.go
index 13d31e06..4d19cd45 100644
--- a/search/collector/topn.go
+++ b/search/collector/topn.go
@@ -54,6 +54,7 @@ type TopNCollector struct {
 	size          int
 	skip          int
 	total         uint64
+	bytesRead     uint64
 	maxScore      float64
 	took          time.Duration
 	sort          search.SortOrder
@@ -197,7 +198,6 @@ func (hc *TopNCollector) Collect(ctx context.Context, searcher search.Searcher,
 	}
 
 	hc.needDocIds = hc.needDocIds || loadID
-
 	select {
 	case <-ctx.Done():
 		return ctx.Err()
@@ -226,6 +226,14 @@ func (hc *TopNCollector) Collect(ctx context.Context, searcher search.Searcher,
 		next, err = searcher.Next(searchContext)
 	}
 
+	statsCallbackFn := ctx.Value(search.SearchIOStatsCallbackKey)
+	if statsCallbackFn != nil {
+		// hc.bytesRead corresponds to the
+		// total bytes read as part of docValues being read every hit
+		// which must be accounted by invoking the callback.
+		statsCallbackFn.(search.SearchIOStatsCallbackFunc)(hc.bytesRead)
+	}
+
 	// help finalize/flush the results in case
 	// of custom document match handlers.
 	err = dmHandler(nil)
@@ -351,6 +359,8 @@ func (hc *TopNCollector) visitFieldTerms(reader index.IndexReader, d *search.Doc
 		hc.facetsBuilder.EndDoc()
 	}
 
+	hc.bytesRead += hc.dvReader.BytesRead()
+
 	return err
 }
 
diff --git a/search/query/bool_field.go b/search/query/bool_field.go
index 0272a2fe..5aa7bb8a 100644
--- a/search/query/bool_field.go
+++ b/search/query/bool_field.go
@@ -15,6 +15,8 @@
 package query
 
 import (
+	"context"
+
 	"github.com/blevesearch/bleve/v2/mapping"
 	"github.com/blevesearch/bleve/v2/search"
 	"github.com/blevesearch/bleve/v2/search/searcher"
@@ -51,7 +53,7 @@ func (q *BoolFieldQuery) Field() string {
 	return q.FieldVal
 }
 
-func (q *BoolFieldQuery) Searcher(i index.IndexReader, m mapping.IndexMapping, options search.SearcherOptions) (search.Searcher, error) {
+func (q *BoolFieldQuery) Searcher(ctx context.Context, i index.IndexReader, m mapping.IndexMapping, options search.SearcherOptions) (search.Searcher, error) {
 	field := q.FieldVal
 	if q.FieldVal == "" {
 		field = m.DefaultSearchField()
@@ -60,5 +62,5 @@ func (q *BoolFieldQuery) Searcher(i index.IndexReader, m mapping.IndexMapping, o
 	if q.Bool {
 		term = "T"
 	}
-	return searcher.NewTermSearcher(i, term, field, q.BoostVal.Value(), options)
+	return searcher.NewTermSearcher(ctx, i, term, field, q.BoostVal.Value(), options)
 }
diff --git a/search/query/boolean.go b/search/query/boolean.go
index b9c504f8..b5e1fdc4 100644
--- a/search/query/boolean.go
+++ b/search/query/boolean.go
@@ -15,6 +15,7 @@
 package query
 
 import (
+	"context"
 	"encoding/json"
 	"fmt"
 
@@ -113,11 +114,11 @@ func (q *BooleanQuery) Boost() float64 {
 	return q.BoostVal.Value()
 }
 
-func (q *BooleanQuery) Searcher(i index.IndexReader, m mapping.IndexMapping, options search.SearcherOptions) (search.Searcher, error) {
+func (q *BooleanQuery) Searcher(ctx context.Context, i index.IndexReader, m mapping.IndexMapping, options search.SearcherOptions) (search.Searcher, error) {
 	var err error
 	var mustNotSearcher search.Searcher
 	if q.MustNot != nil {
-		mustNotSearcher, err = q.MustNot.Searcher(i, m, options)
+		mustNotSearcher, err = q.MustNot.Searcher(ctx, i, m, options)
 		if err != nil {
 			return nil, err
 		}
@@ -129,7 +130,7 @@ func (q *BooleanQuery) Searcher(i index.IndexReader, m mapping.IndexMapping, opt
 
 	var mustSearcher search.Searcher
 	if q.Must != nil {
-		mustSearcher, err = q.Must.Searcher(i, m, options)
+		mustSearcher, err = q.Must.Searcher(ctx, i, m, options)
 		if err != nil {
 			return nil, err
 		}
@@ -141,7 +142,7 @@ func (q *BooleanQuery) Searcher(i index.IndexReader, m mapping.IndexMapping, opt
 
 	var shouldSearcher search.Searcher
 	if q.Should != nil {
-		shouldSearcher, err = q.Should.Searcher(i, m, options)
+		shouldSearcher, err = q.Should.Searcher(ctx, i, m, options)
 		if err != nil {
 			return nil, err
 		}
@@ -158,7 +159,7 @@ func (q *BooleanQuery) Searcher(i index.IndexReader, m mapping.IndexMapping, opt
 
 	// if only mustNotSearcher, start with MatchAll
 	if mustSearcher == nil && shouldSearcher == nil && mustNotSearcher != nil {
-		mustSearcher, err = searcher.NewMatchAllSearcher(i, 1.0, options)
+		mustSearcher, err = searcher.NewMatchAllSearcher(ctx, i, 1.0, options)
 		if err != nil {
 			return nil, err
 		}
@@ -169,7 +170,7 @@ func (q *BooleanQuery) Searcher(i index.IndexReader, m mapping.IndexMapping, opt
 		return shouldSearcher, nil
 	}
 
-	return searcher.NewBooleanSearcher(i, mustSearcher, shouldSearcher, mustNotSearcher, options)
+	return searcher.NewBooleanSearcher(ctx, i, mustSearcher, shouldSearcher, mustNotSearcher, options)
 }
 
 func (q *BooleanQuery) Validate() error {
diff --git a/search/query/conjunction.go b/search/query/conjunction.go
index 7d647646..27bec7d6 100644
--- a/search/query/conjunction.go
+++ b/search/query/conjunction.go
@@ -15,6 +15,7 @@
 package query
 
 import (
+	"context"
 	"encoding/json"
 
 	"github.com/blevesearch/bleve/v2/mapping"
@@ -52,10 +53,10 @@ func (q *ConjunctionQuery) AddQuery(aq ...Query) {
 	}
 }
 
-func (q *ConjunctionQuery) Searcher(i index.IndexReader, m mapping.IndexMapping, options search.SearcherOptions) (search.Searcher, error) {
+func (q *ConjunctionQuery) Searcher(ctx context.Context, i index.IndexReader, m mapping.IndexMapping, options search.SearcherOptions) (search.Searcher, error) {
 	ss := make([]search.Searcher, 0, len(q.Conjuncts))
 	for _, conjunct := range q.Conjuncts {
-		sr, err := conjunct.Searcher(i, m, options)
+		sr, err := conjunct.Searcher(ctx, i, m, options)
 		if err != nil {
 			for _, searcher := range ss {
 				if searcher != nil {
@@ -75,7 +76,7 @@ func (q *ConjunctionQuery) Searcher(i index.IndexReader, m mapping.IndexMapping,
 		return searcher.NewMatchNoneSearcher(i)
 	}
 
-	return searcher.NewConjunctionSearcher(i, ss, options)
+	return searcher.NewConjunctionSearcher(ctx, i, ss, options)
 }
 
 func (q *ConjunctionQuery) Validate() error {
diff --git a/search/query/date_range.go b/search/query/date_range.go
index 290786dd..ef18f2fb 100644
--- a/search/query/date_range.go
+++ b/search/query/date_range.go
@@ -15,6 +15,7 @@
 package query
 
 import (
+	"context"
 	"encoding/json"
 	"fmt"
 	"math"
@@ -133,7 +134,7 @@ func (q *DateRangeQuery) Field() string {
 	return q.FieldVal
 }
 
-func (q *DateRangeQuery) Searcher(i index.IndexReader, m mapping.IndexMapping, options search.SearcherOptions) (search.Searcher, error) {
+func (q *DateRangeQuery) Searcher(ctx context.Context, i index.IndexReader, m mapping.IndexMapping, options search.SearcherOptions) (search.Searcher, error) {
 	min, max, err := q.parseEndpoints()
 	if err != nil {
 		return nil, err
@@ -144,7 +145,7 @@ func (q *DateRangeQuery) Searcher(i index.IndexReader, m mapping.IndexMapping, o
 		field = m.DefaultSearchField()
 	}
 
-	return searcher.NewNumericRangeSearcher(i, min, max, q.InclusiveStart, q.InclusiveEnd, field, q.BoostVal.Value(), options)
+	return searcher.NewNumericRangeSearcher(ctx, i, min, max, q.InclusiveStart, q.InclusiveEnd, field, q.BoostVal.Value(), options)
 }
 
 func (q *DateRangeQuery) parseEndpoints() (*float64, *float64, error) {
diff --git a/search/query/disjunction.go b/search/query/disjunction.go
index 50957fa6..c6cc0d73 100644
--- a/search/query/disjunction.go
+++ b/search/query/disjunction.go
@@ -15,6 +15,7 @@
 package query
 
 import (
+	"context"
 	"encoding/json"
 	"fmt"
 
@@ -58,11 +59,11 @@ func (q *DisjunctionQuery) SetMin(m float64) {
 	q.Min = m
 }
 
-func (q *DisjunctionQuery) Searcher(i index.IndexReader, m mapping.IndexMapping,
+func (q *DisjunctionQuery) Searcher(ctx context.Context, i index.IndexReader, m mapping.IndexMapping,
 	options search.SearcherOptions) (search.Searcher, error) {
 	ss := make([]search.Searcher, 0, len(q.Disjuncts))
 	for _, disjunct := range q.Disjuncts {
-		sr, err := disjunct.Searcher(i, m, options)
+		sr, err := disjunct.Searcher(ctx, i, m, options)
 		if err != nil {
 			for _, searcher := range ss {
 				if searcher != nil {
@@ -82,7 +83,7 @@ func (q *DisjunctionQuery) Searcher(i index.IndexReader, m mapping.IndexMapping,
 		return searcher.NewMatchNoneSearcher(i)
 	}
 
-	return searcher.NewDisjunctionSearcher(i, ss, q.Min, options)
+	return searcher.NewDisjunctionSearcher(ctx, i, ss, q.Min, options)
 }
 
 func (q *DisjunctionQuery) Validate() error {
diff --git a/search/query/docid.go b/search/query/docid.go
index 1d273394..7116f391 100644
--- a/search/query/docid.go
+++ b/search/query/docid.go
@@ -15,6 +15,8 @@
 package query
 
 import (
+	"context"
+
 	"github.com/blevesearch/bleve/v2/mapping"
 	"github.com/blevesearch/bleve/v2/search"
 	"github.com/blevesearch/bleve/v2/search/searcher"
@@ -44,6 +46,6 @@ func (q *DocIDQuery) Boost() float64 {
 	return q.BoostVal.Value()
 }
 
-func (q *DocIDQuery) Searcher(i index.IndexReader, m mapping.IndexMapping, options search.SearcherOptions) (search.Searcher, error) {
-	return searcher.NewDocIDSearcher(i, q.IDs, q.BoostVal.Value(), options)
+func (q *DocIDQuery) Searcher(ctx context.Context, i index.IndexReader, m mapping.IndexMapping, options search.SearcherOptions) (search.Searcher, error) {
+	return searcher.NewDocIDSearcher(ctx, i, q.IDs, q.BoostVal.Value(), options)
 }
diff --git a/search/query/fuzzy.go b/search/query/fuzzy.go
index aceaa802..f24eb0c2 100644
--- a/search/query/fuzzy.go
+++ b/search/query/fuzzy.go
@@ -15,6 +15,8 @@
 package query
 
 import (
+	"context"
+
 	"github.com/blevesearch/bleve/v2/mapping"
 	"github.com/blevesearch/bleve/v2/search"
 	"github.com/blevesearch/bleve/v2/search/searcher"
@@ -68,10 +70,10 @@ func (q *FuzzyQuery) SetPrefix(p int) {
 	q.Prefix = p
 }
 
-func (q *FuzzyQuery) Searcher(i index.IndexReader, m mapping.IndexMapping, options search.SearcherOptions) (search.Searcher, error) {
+func (q *FuzzyQuery) Searcher(ctx context.Context, i index.IndexReader, m mapping.IndexMapping, options search.SearcherOptions) (search.Searcher, error) {
 	field := q.FieldVal
 	if q.FieldVal == "" {
 		field = m.DefaultSearchField()
 	}
-	return searcher.NewFuzzySearcher(i, q.Term, q.Prefix, q.Fuzziness, field, q.BoostVal.Value(), options)
+	return searcher.NewFuzzySearcher(ctx, i, q.Term, q.Prefix, q.Fuzziness, field, q.BoostVal.Value(), options)
 }
diff --git a/search/query/geo_boundingbox.go b/search/query/geo_boundingbox.go
index be4b5a8b..ac912539 100644
--- a/search/query/geo_boundingbox.go
+++ b/search/query/geo_boundingbox.go
@@ -15,6 +15,7 @@
 package query
 
 import (
+	"context"
 	"encoding/json"
 	"fmt"
 
@@ -56,7 +57,7 @@ func (q *GeoBoundingBoxQuery) Field() string {
 	return q.FieldVal
 }
 
-func (q *GeoBoundingBoxQuery) Searcher(i index.IndexReader, m mapping.IndexMapping, options search.SearcherOptions) (search.Searcher, error) {
+func (q *GeoBoundingBoxQuery) Searcher(ctx context.Context, i index.IndexReader, m mapping.IndexMapping, options search.SearcherOptions) (search.Searcher, error) {
 	field := q.FieldVal
 	if q.FieldVal == "" {
 		field = m.DefaultSearchField()
@@ -65,20 +66,20 @@ func (q *GeoBoundingBoxQuery) Searcher(i index.IndexReader, m mapping.IndexMappi
 	if q.BottomRight[0] < q.TopLeft[0] {
 		// cross date line, rewrite as two parts
 
-		leftSearcher, err := searcher.NewGeoBoundingBoxSearcher(i, -180, q.BottomRight[1], q.BottomRight[0], q.TopLeft[1], field, q.BoostVal.Value(), options, true)
+		leftSearcher, err := searcher.NewGeoBoundingBoxSearcher(ctx, i, -180, q.BottomRight[1], q.BottomRight[0], q.TopLeft[1], field, q.BoostVal.Value(), options, true)
 		if err != nil {
 			return nil, err
 		}
-		rightSearcher, err := searcher.NewGeoBoundingBoxSearcher(i, q.TopLeft[0], q.BottomRight[1], 180, q.TopLeft[1], field, q.BoostVal.Value(), options, true)
+		rightSearcher, err := searcher.NewGeoBoundingBoxSearcher(ctx, i, q.TopLeft[0], q.BottomRight[1], 180, q.TopLeft[1], field, q.BoostVal.Value(), options, true)
 		if err != nil {
 			_ = leftSearcher.Close()
 			return nil, err
 		}
 
-		return searcher.NewDisjunctionSearcher(i, []search.Searcher{leftSearcher, rightSearcher}, 0, options)
+		return searcher.NewDisjunctionSearcher(ctx, i, []search.Searcher{leftSearcher, rightSearcher}, 0, options)
 	}
 
-	return searcher.NewGeoBoundingBoxSearcher(i, q.TopLeft[0], q.BottomRight[1], q.BottomRight[0], q.TopLeft[1], field, q.BoostVal.Value(), options, true)
+	return searcher.NewGeoBoundingBoxSearcher(ctx, i, q.TopLeft[0], q.BottomRight[1], q.BottomRight[0], q.TopLeft[1], field, q.BoostVal.Value(), options, true)
 }
 
 func (q *GeoBoundingBoxQuery) Validate() error {
diff --git a/search/query/geo_boundingpolygon.go b/search/query/geo_boundingpolygon.go
index abb8ccd7..467f39b2 100644
--- a/search/query/geo_boundingpolygon.go
+++ b/search/query/geo_boundingpolygon.go
@@ -15,6 +15,7 @@
 package query
 
 import (
+	"context"
 	"encoding/json"
 	"fmt"
 
@@ -53,14 +54,14 @@ func (q *GeoBoundingPolygonQuery) Field() string {
 	return q.FieldVal
 }
 
-func (q *GeoBoundingPolygonQuery) Searcher(i index.IndexReader,
+func (q *GeoBoundingPolygonQuery) Searcher(ctx context.Context, i index.IndexReader,
 	m mapping.IndexMapping, options search.SearcherOptions) (search.Searcher, error) {
 	field := q.FieldVal
 	if q.FieldVal == "" {
 		field = m.DefaultSearchField()
 	}
 
-	return searcher.NewGeoBoundedPolygonSearcher(i, q.Points, field, q.BoostVal.Value(), options)
+	return searcher.NewGeoBoundedPolygonSearcher(ctx, i, q.Points, field, q.BoostVal.Value(), options)
 }
 
 func (q *GeoBoundingPolygonQuery) Validate() error {
diff --git a/search/query/geo_distance.go b/search/query/geo_distance.go
index d5174c22..f05bf672 100644
--- a/search/query/geo_distance.go
+++ b/search/query/geo_distance.go
@@ -15,6 +15,7 @@
 package query
 
 import (
+	"context"
 	"encoding/json"
 	"fmt"
 
@@ -56,7 +57,7 @@ func (q *GeoDistanceQuery) Field() string {
 	return q.FieldVal
 }
 
-func (q *GeoDistanceQuery) Searcher(i index.IndexReader, m mapping.IndexMapping,
+func (q *GeoDistanceQuery) Searcher(ctx context.Context, i index.IndexReader, m mapping.IndexMapping,
 	options search.SearcherOptions) (search.Searcher, error) {
 	field := q.FieldVal
 	if q.FieldVal == "" {
@@ -68,7 +69,7 @@ func (q *GeoDistanceQuery) Searcher(i index.IndexReader, m mapping.IndexMapping,
 		return nil, err
 	}
 
-	return searcher.NewGeoPointDistanceSearcher(i, q.Location[0], q.Location[1],
+	return searcher.NewGeoPointDistanceSearcher(ctx, i, q.Location[0], q.Location[1],
 		dist, field, q.BoostVal.Value(), options)
 }
 
diff --git a/search/query/geo_shape.go b/search/query/geo_shape.go
index aff5d2f9..a63ec80f 100644
--- a/search/query/geo_shape.go
+++ b/search/query/geo_shape.go
@@ -15,6 +15,7 @@
 package query
 
 import (
+	"context"
 	"encoding/json"
 
 	"github.com/blevesearch/bleve/v2/geo"
@@ -99,14 +100,14 @@ func (q *GeoShapeQuery) Field() string {
 	return q.FieldVal
 }
 
-func (q *GeoShapeQuery) Searcher(i index.IndexReader,
+func (q *GeoShapeQuery) Searcher(ctx context.Context, i index.IndexReader,
 	m mapping.IndexMapping, options search.SearcherOptions) (search.Searcher, error) {
 	field := q.FieldVal
 	if q.FieldVal == "" {
 		field = m.DefaultSearchField()
 	}
 
-	return searcher.NewGeoShapeSearcher(i, q.Geometry.Shape, q.Geometry.Relation, field,
+	return searcher.NewGeoShapeSearcher(ctx, i, q.Geometry.Shape, q.Geometry.Relation, field,
 		q.BoostVal.Value(), options)
 }
 
diff --git a/search/query/ip_range.go b/search/query/ip_range.go
index b02b5ed2..68577cc9 100644
--- a/search/query/ip_range.go
+++ b/search/query/ip_range.go
@@ -15,6 +15,7 @@
 package query
 
 import (
+	"context"
 	"fmt"
 	"net"
 
@@ -53,7 +54,7 @@ func (q *IPRangeQuery) Field() string {
 	return q.FieldVal
 }
 
-func (q *IPRangeQuery) Searcher(i index.IndexReader, m mapping.IndexMapping, options search.SearcherOptions) (search.Searcher, error) {
+func (q *IPRangeQuery) Searcher(ctx context.Context, i index.IndexReader, m mapping.IndexMapping, options search.SearcherOptions) (search.Searcher, error) {
 	field := q.FieldVal
 	if q.FieldVal == "" {
 		field = m.DefaultSearchField()
@@ -65,9 +66,9 @@ func (q *IPRangeQuery) Searcher(i index.IndexReader, m mapping.IndexMapping, opt
 			return nil, err
 		}
 		// If we are searching for a specific ip rather than members of a network, just use a term search.
-		return searcher.NewTermSearcherBytes(i, ip.To16(), field, q.BoostVal.Value(), options)
+		return searcher.NewTermSearcherBytes(ctx, i, ip.To16(), field, q.BoostVal.Value(), options)
 	}
-	return searcher.NewIPRangeSearcher(i, ipNet, field, q.BoostVal.Value(), options)
+	return searcher.NewIPRangeSearcher(ctx, i, ipNet, field, q.BoostVal.Value(), options)
 }
 
 func (q *IPRangeQuery) Validate() error {
diff --git a/search/query/match.go b/search/query/match.go
index da1dc091..61c00a00 100644
--- a/search/query/match.go
+++ b/search/query/match.go
@@ -15,6 +15,7 @@
 package query
 
 import (
+	"context"
 	"encoding/json"
 	"fmt"
 
@@ -114,7 +115,7 @@ func (q *MatchQuery) SetOperator(operator MatchQueryOperator) {
 	q.Operator = operator
 }
 
-func (q *MatchQuery) Searcher(i index.IndexReader, m mapping.IndexMapping, options search.SearcherOptions) (search.Searcher, error) {
+func (q *MatchQuery) Searcher(ctx context.Context, i index.IndexReader, m mapping.IndexMapping, options search.SearcherOptions) (search.Searcher, error) {
 
 	field := q.FieldVal
 	if q.FieldVal == "" {
@@ -160,17 +161,17 @@ func (q *MatchQuery) Searcher(i index.IndexReader, m mapping.IndexMapping, optio
 			shouldQuery := NewDisjunctionQuery(tqs)
 			shouldQuery.SetMin(1)
 			shouldQuery.SetBoost(q.BoostVal.Value())
-			return shouldQuery.Searcher(i, m, options)
+			return shouldQuery.Searcher(ctx, i, m, options)
 
 		case MatchQueryOperatorAnd:
 			mustQuery := NewConjunctionQuery(tqs)
 			mustQuery.SetBoost(q.BoostVal.Value())
-			return mustQuery.Searcher(i, m, options)
+			return mustQuery.Searcher(ctx, i, m, options)
 
 		default:
 			return nil, fmt.Errorf("unhandled operator %d", q.Operator)
 		}
 	}
 	noneQuery := NewMatchNoneQuery()
-	return noneQuery.Searcher(i, m, options)
+	return noneQuery.Searcher(ctx, i, m, options)
 }
diff --git a/search/query/match_all.go b/search/query/match_all.go
index a31f25ab..e88825ae 100644
--- a/search/query/match_all.go
+++ b/search/query/match_all.go
@@ -15,6 +15,7 @@
 package query
 
 import (
+	"context"
 	"encoding/json"
 
 	"github.com/blevesearch/bleve/v2/mapping"
@@ -42,8 +43,8 @@ func (q *MatchAllQuery) Boost() float64 {
 	return q.BoostVal.Value()
 }
 
-func (q *MatchAllQuery) Searcher(i index.IndexReader, m mapping.IndexMapping, options search.SearcherOptions) (search.Searcher, error) {
-	return searcher.NewMatchAllSearcher(i, q.BoostVal.Value(), options)
+func (q *MatchAllQuery) Searcher(ctx context.Context, i index.IndexReader, m mapping.IndexMapping, options search.SearcherOptions) (search.Searcher, error) {
+	return searcher.NewMatchAllSearcher(ctx, i, q.BoostVal.Value(), options)
 }
 
 func (q *MatchAllQuery) MarshalJSON() ([]byte, error) {
diff --git a/search/query/match_none.go b/search/query/match_none.go
index 69b44186..cb65a725 100644
--- a/search/query/match_none.go
+++ b/search/query/match_none.go
@@ -15,6 +15,7 @@
 package query
 
 import (
+	"context"
 	"encoding/json"
 
 	"github.com/blevesearch/bleve/v2/mapping"
@@ -42,7 +43,7 @@ func (q *MatchNoneQuery) Boost() float64 {
 	return q.BoostVal.Value()
 }
 
-func (q *MatchNoneQuery) Searcher(i index.IndexReader, m mapping.IndexMapping, options search.SearcherOptions) (search.Searcher, error) {
+func (q *MatchNoneQuery) Searcher(ctx context.Context, i index.IndexReader, m mapping.IndexMapping, options search.SearcherOptions) (search.Searcher, error) {
 	return searcher.NewMatchNoneSearcher(i)
 }
 
diff --git a/search/query/match_phrase.go b/search/query/match_phrase.go
index 057245fb..fa8ac720 100644
--- a/search/query/match_phrase.go
+++ b/search/query/match_phrase.go
@@ -15,6 +15,7 @@
 package query
 
 import (
+	"context"
 	"fmt"
 
 	"github.com/blevesearch/bleve/v2/analysis"
@@ -61,7 +62,7 @@ func (q *MatchPhraseQuery) Field() string {
 	return q.FieldVal
 }
 
-func (q *MatchPhraseQuery) Searcher(i index.IndexReader, m mapping.IndexMapping, options search.SearcherOptions) (search.Searcher, error) {
+func (q *MatchPhraseQuery) Searcher(ctx context.Context, i index.IndexReader, m mapping.IndexMapping, options search.SearcherOptions) (search.Searcher, error) {
 	field := q.FieldVal
 	if q.FieldVal == "" {
 		field = m.DefaultSearchField()
@@ -83,10 +84,10 @@ func (q *MatchPhraseQuery) Searcher(i index.IndexReader, m mapping.IndexMapping,
 		phrase := tokenStreamToPhrase(tokens)
 		phraseQuery := NewMultiPhraseQuery(phrase, field)
 		phraseQuery.SetBoost(q.BoostVal.Value())
-		return phraseQuery.Searcher(i, m, options)
+		return phraseQuery.Searcher(ctx, i, m, options)
 	}
 	noneQuery := NewMatchNoneQuery()
-	return noneQuery.Searcher(i, m, options)
+	return noneQuery.Searcher(ctx, i, m, options)
 }
 
 func tokenStreamToPhrase(tokens analysis.TokenStream) [][]string {
diff --git a/search/query/multi_phrase.go b/search/query/multi_phrase.go
index d75dc0c8..2887be16 100644
--- a/search/query/multi_phrase.go
+++ b/search/query/multi_phrase.go
@@ -15,6 +15,7 @@
 package query
 
 import (
+	"context"
 	"encoding/json"
 	"fmt"
 
@@ -55,8 +56,8 @@ func (q *MultiPhraseQuery) Boost() float64 {
 	return q.BoostVal.Value()
 }
 
-func (q *MultiPhraseQuery) Searcher(i index.IndexReader, m mapping.IndexMapping, options search.SearcherOptions) (search.Searcher, error) {
-	return searcher.NewMultiPhraseSearcher(i, q.Terms, q.Field, options)
+func (q *MultiPhraseQuery) Searcher(ctx context.Context, i index.IndexReader, m mapping.IndexMapping, options search.SearcherOptions) (search.Searcher, error) {
+	return searcher.NewMultiPhraseSearcher(ctx, i, q.Terms, q.Field, options)
 }
 
 func (q *MultiPhraseQuery) Validate() error {
diff --git a/search/query/numeric_range.go b/search/query/numeric_range.go
index a1fe7b6c..ad247416 100644
--- a/search/query/numeric_range.go
+++ b/search/query/numeric_range.go
@@ -15,6 +15,7 @@
 package query
 
 import (
+	"context"
 	"fmt"
 
 	"github.com/blevesearch/bleve/v2/mapping"
@@ -71,12 +72,12 @@ func (q *NumericRangeQuery) Field() string {
 	return q.FieldVal
 }
 
-func (q *NumericRangeQuery) Searcher(i index.IndexReader, m mapping.IndexMapping, options search.SearcherOptions) (search.Searcher, error) {
+func (q *NumericRangeQuery) Searcher(ctx context.Context, i index.IndexReader, m mapping.IndexMapping, options search.SearcherOptions) (search.Searcher, error) {
 	field := q.FieldVal
 	if q.FieldVal == "" {
 		field = m.DefaultSearchField()
 	}
-	return searcher.NewNumericRangeSearcher(i, q.Min, q.Max, q.InclusiveMin, q.InclusiveMax, field, q.BoostVal.Value(), options)
+	return searcher.NewNumericRangeSearcher(ctx, i, q.Min, q.Max, q.InclusiveMin, q.InclusiveMax, field, q.BoostVal.Value(), options)
 }
 
 func (q *NumericRangeQuery) Validate() error {
diff --git a/search/query/phrase.go b/search/query/phrase.go
index d6da1185..207e66b1 100644
--- a/search/query/phrase.go
+++ b/search/query/phrase.go
@@ -15,6 +15,7 @@
 package query
 
 import (
+	"context"
 	"encoding/json"
 	"fmt"
 
@@ -52,8 +53,8 @@ func (q *PhraseQuery) Boost() float64 {
 	return q.BoostVal.Value()
 }
 
-func (q *PhraseQuery) Searcher(i index.IndexReader, m mapping.IndexMapping, options search.SearcherOptions) (search.Searcher, error) {
-	return searcher.NewPhraseSearcher(i, q.Terms, q.Field, options)
+func (q *PhraseQuery) Searcher(ctx context.Context, i index.IndexReader, m mapping.IndexMapping, options search.SearcherOptions) (search.Searcher, error) {
+	return searcher.NewPhraseSearcher(ctx, i, q.Terms, q.Field, options)
 }
 
 func (q *PhraseQuery) Validate() error {
diff --git a/search/query/prefix.go b/search/query/prefix.go
index 05dc40c0..debbbc1e 100644
--- a/search/query/prefix.go
+++ b/search/query/prefix.go
@@ -15,6 +15,8 @@
 package query
 
 import (
+	"context"
+
 	"github.com/blevesearch/bleve/v2/mapping"
 	"github.com/blevesearch/bleve/v2/search"
 	"github.com/blevesearch/bleve/v2/search/searcher"
@@ -53,10 +55,10 @@ func (q *PrefixQuery) Field() string {
 	return q.FieldVal
 }
 
-func (q *PrefixQuery) Searcher(i index.IndexReader, m mapping.IndexMapping, options search.SearcherOptions) (search.Searcher, error) {
+func (q *PrefixQuery) Searcher(ctx context.Context, i index.IndexReader, m mapping.IndexMapping, options search.SearcherOptions) (search.Searcher, error) {
 	field := q.FieldVal
 	if q.FieldVal == "" {
 		field = m.DefaultSearchField()
 	}
-	return searcher.NewTermPrefixSearcher(i, q.Prefix, field, q.BoostVal.Value(), options)
+	return searcher.NewTermPrefixSearcher(ctx, i, q.Prefix, field, q.BoostVal.Value(), options)
 }
diff --git a/search/query/query.go b/search/query/query.go
index 60add3b6..df560534 100644
--- a/search/query/query.go
+++ b/search/query/query.go
@@ -15,6 +15,7 @@
 package query
 
 import (
+	"context"
 	"encoding/json"
 	"fmt"
 	"io/ioutil"
@@ -36,7 +37,7 @@ func SetLog(l *log.Logger) {
 // A Query represents a description of the type
 // and parameters for a query into the index.
 type Query interface {
-	Searcher(i index.IndexReader, m mapping.IndexMapping,
+	Searcher(ctx context.Context, i index.IndexReader, m mapping.IndexMapping,
 		options search.SearcherOptions) (search.Searcher, error)
 }
 
@@ -292,6 +293,17 @@ func ParseQuery(input []byte) (Query, error) {
 		}
 		return &rv, nil
 	}
+
+	_, hasCIDR := tmp["cidr"]
+	if hasCIDR {
+		var rv IPRangeQuery
+		err := json.Unmarshal(input, &rv)
+		if err != nil {
+			return nil, err
+		}
+		return &rv, nil
+	}
+
 	return nil, fmt.Errorf("unknown query type")
 }
 
diff --git a/search/query/query_string.go b/search/query/query_string.go
index fe1680c5..42bb598b 100644
--- a/search/query/query_string.go
+++ b/search/query/query_string.go
@@ -15,6 +15,8 @@
 package query
 
 import (
+	"context"
+
 	"github.com/blevesearch/bleve/v2/mapping"
 	"github.com/blevesearch/bleve/v2/search"
 	index "github.com/blevesearch/bleve_index_api"
@@ -47,12 +49,12 @@ func (q *QueryStringQuery) Parse() (Query, error) {
 	return parseQuerySyntax(q.Query)
 }
 
-func (q *QueryStringQuery) Searcher(i index.IndexReader, m mapping.IndexMapping, options search.SearcherOptions) (search.Searcher, error) {
+func (q *QueryStringQuery) Searcher(ctx context.Context, i index.IndexReader, m mapping.IndexMapping, options search.SearcherOptions) (search.Searcher, error) {
 	newQuery, err := parseQuerySyntax(q.Query)
 	if err != nil {
 		return nil, err
 	}
-	return newQuery.Searcher(i, m, options)
+	return newQuery.Searcher(ctx, i, m, options)
 }
 
 func (q *QueryStringQuery) Validate() error {
diff --git a/search/query/regexp.go b/search/query/regexp.go
index ba744ec1..6b3da955 100644
--- a/search/query/regexp.go
+++ b/search/query/regexp.go
@@ -15,6 +15,7 @@
 package query
 
 import (
+	"context"
 	"strings"
 
 	"github.com/blevesearch/bleve/v2/mapping"
@@ -57,7 +58,7 @@ func (q *RegexpQuery) Field() string {
 	return q.FieldVal
 }
 
-func (q *RegexpQuery) Searcher(i index.IndexReader, m mapping.IndexMapping, options search.SearcherOptions) (search.Searcher, error) {
+func (q *RegexpQuery) Searcher(ctx context.Context, i index.IndexReader, m mapping.IndexMapping, options search.SearcherOptions) (search.Searcher, error) {
 	field := q.FieldVal
 	if q.FieldVal == "" {
 		field = m.DefaultSearchField()
@@ -72,7 +73,7 @@ func (q *RegexpQuery) Searcher(i index.IndexReader, m mapping.IndexMapping, opti
 		actualRegexp = actualRegexp[1:] // remove leading ^
 	}
 
-	return searcher.NewRegexpStringSearcher(i, actualRegexp, field,
+	return searcher.NewRegexpStringSearcher(ctx, i, actualRegexp, field,
 		q.BoostVal.Value(), options)
 }
 
diff --git a/search/query/term.go b/search/query/term.go
index 82958bb0..5c6af396 100644
--- a/search/query/term.go
+++ b/search/query/term.go
@@ -15,6 +15,8 @@
 package query
 
 import (
+	"context"
+
 	"github.com/blevesearch/bleve/v2/mapping"
 	"github.com/blevesearch/bleve/v2/search"
 	"github.com/blevesearch/bleve/v2/search/searcher"
@@ -52,10 +54,10 @@ func (q *TermQuery) Field() string {
 	return q.FieldVal
 }
 
-func (q *TermQuery) Searcher(i index.IndexReader, m mapping.IndexMapping, options search.SearcherOptions) (search.Searcher, error) {
+func (q *TermQuery) Searcher(ctx context.Context, i index.IndexReader, m mapping.IndexMapping, options search.SearcherOptions) (search.Searcher, error) {
 	field := q.FieldVal
 	if q.FieldVal == "" {
 		field = m.DefaultSearchField()
 	}
-	return searcher.NewTermSearcher(i, q.Term, field, q.BoostVal.Value(), options)
+	return searcher.NewTermSearcher(ctx, i, q.Term, field, q.BoostVal.Value(), options)
 }
diff --git a/search/query/term_range.go b/search/query/term_range.go
index 3edfa695..4dc3a34b 100644
--- a/search/query/term_range.go
+++ b/search/query/term_range.go
@@ -15,6 +15,7 @@
 package query
 
 import (
+	"context"
 	"fmt"
 
 	"github.com/blevesearch/bleve/v2/mapping"
@@ -71,7 +72,7 @@ func (q *TermRangeQuery) Field() string {
 	return q.FieldVal
 }
 
-func (q *TermRangeQuery) Searcher(i index.IndexReader, m mapping.IndexMapping, options search.SearcherOptions) (search.Searcher, error) {
+func (q *TermRangeQuery) Searcher(ctx context.Context, i index.IndexReader, m mapping.IndexMapping, options search.SearcherOptions) (search.Searcher, error) {
 	field := q.FieldVal
 	if q.FieldVal == "" {
 		field = m.DefaultSearchField()
@@ -84,7 +85,7 @@ func (q *TermRangeQuery) Searcher(i index.IndexReader, m mapping.IndexMapping, o
 	if q.Max != "" {
 		maxTerm = []byte(q.Max)
 	}
-	return searcher.NewTermRangeSearcher(i, minTerm, maxTerm, q.InclusiveMin, q.InclusiveMax, field, q.BoostVal.Value(), options)
+	return searcher.NewTermRangeSearcher(ctx, i, minTerm, maxTerm, q.InclusiveMin, q.InclusiveMax, field, q.BoostVal.Value(), options)
 }
 
 func (q *TermRangeQuery) Validate() error {
diff --git a/search/query/wildcard.go b/search/query/wildcard.go
index 7713a9ac..f04f3f2e 100644
--- a/search/query/wildcard.go
+++ b/search/query/wildcard.go
@@ -15,6 +15,7 @@
 package query
 
 import (
+	"context"
 	"strings"
 
 	"github.com/blevesearch/bleve/v2/mapping"
@@ -76,7 +77,7 @@ func (q *WildcardQuery) Field() string {
 	return q.FieldVal
 }
 
-func (q *WildcardQuery) Searcher(i index.IndexReader, m mapping.IndexMapping, options search.SearcherOptions) (search.Searcher, error) {
+func (q *WildcardQuery) Searcher(ctx context.Context, i index.IndexReader, m mapping.IndexMapping, options search.SearcherOptions) (search.Searcher, error) {
 	field := q.FieldVal
 	if q.FieldVal == "" {
 		field = m.DefaultSearchField()
@@ -84,7 +85,7 @@ func (q *WildcardQuery) Searcher(i index.IndexReader, m mapping.IndexMapping, op
 
 	regexpString := wildcardRegexpReplacer.Replace(q.Wildcard)
 
-	return searcher.NewRegexpStringSearcher(i, regexpString, field,
+	return searcher.NewRegexpStringSearcher(ctx, i, regexpString, field,
 		q.BoostVal.Value(), options)
 }
 
diff --git a/search/scorer/scorer_term.go b/search/scorer/scorer_term.go
index ca268648..7b60eda4 100644
--- a/search/scorer/scorer_term.go
+++ b/search/scorer/scorer_term.go
@@ -198,6 +198,5 @@ func (s *TermQueryScorer) Score(ctx *search.SearchContext, termMatch *index.Term
 				})
 		}
 	}
-
 	return rv
 }
diff --git a/search/search.go b/search/search.go
index d2dd3371..69d8945f 100644
--- a/search/search.go
+++ b/search/search.go
@@ -27,6 +27,10 @@ var reflectStaticSizeDocumentMatch int
 var reflectStaticSizeSearchContext int
 var reflectStaticSizeLocation int
 
+const SearchIOStatsCallbackKey = "_search_io_stats_callback_key"
+
+type SearchIOStatsCallbackFunc func(uint64)
+
 func init() {
 	var dm DocumentMatch
 	reflectStaticSizeDocumentMatch = int(reflect.TypeOf(dm).Size())
diff --git a/search/searcher/search_boolean.go b/search/searcher/search_boolean.go
index ef9093c2..bf207f81 100644
--- a/search/searcher/search_boolean.go
+++ b/search/searcher/search_boolean.go
@@ -15,6 +15,7 @@
 package searcher
 
 import (
+	"context"
 	"math"
 	"reflect"
 
@@ -48,7 +49,7 @@ type BooleanSearcher struct {
 	done            bool
 }
 
-func NewBooleanSearcher(indexReader index.IndexReader, mustSearcher search.Searcher, shouldSearcher search.Searcher, mustNotSearcher search.Searcher, options search.SearcherOptions) (*BooleanSearcher, error) {
+func NewBooleanSearcher(ctx context.Context, indexReader index.IndexReader, mustSearcher search.Searcher, shouldSearcher search.Searcher, mustNotSearcher search.Searcher, options search.SearcherOptions) (*BooleanSearcher, error) {
 	// build our searcher
 	rv := BooleanSearcher{
 		indexReader:     indexReader,
diff --git a/search/searcher/search_conjunction.go b/search/searcher/search_conjunction.go
index 5fe59b90..19ef199a 100644
--- a/search/searcher/search_conjunction.go
+++ b/search/searcher/search_conjunction.go
@@ -15,6 +15,7 @@
 package searcher
 
 import (
+	"context"
 	"math"
 	"reflect"
 	"sort"
@@ -41,9 +42,10 @@ type ConjunctionSearcher struct {
 	scorer      *scorer.ConjunctionQueryScorer
 	initialized bool
 	options     search.SearcherOptions
+	bytesRead   uint64
 }
 
-func NewConjunctionSearcher(indexReader index.IndexReader,
+func NewConjunctionSearcher(ctx context.Context, indexReader index.IndexReader,
 	qsearchers []search.Searcher, options search.SearcherOptions) (
 	search.Searcher, error) {
 	// build the sorted downstream searchers
@@ -57,7 +59,7 @@ func NewConjunctionSearcher(indexReader index.IndexReader,
 	// do not need extra information like freq-norm's or term vectors
 	if len(searchers) > 1 &&
 		options.Score == "none" && !options.IncludeTermVectors {
-		rv, err := optimizeCompositeSearcher("conjunction:unadorned",
+		rv, err := optimizeCompositeSearcher(ctx, "conjunction:unadorned",
 			indexReader, searchers, options)
 		if err != nil || rv != nil {
 			return rv, err
@@ -76,7 +78,7 @@ func NewConjunctionSearcher(indexReader index.IndexReader,
 
 	// attempt push-down conjunction optimization when there's >1 searchers
 	if len(searchers) > 1 {
-		rv, err := optimizeCompositeSearcher("conjunction",
+		rv, err := optimizeCompositeSearcher(ctx, "conjunction",
 			indexReader, searchers, options)
 		if err != nil || rv != nil {
 			return rv, err
diff --git a/search/searcher/search_disjunction.go b/search/searcher/search_disjunction.go
index 4cee4684..606a157a 100644
--- a/search/searcher/search_disjunction.go
+++ b/search/searcher/search_disjunction.go
@@ -15,7 +15,9 @@
 package searcher
 
 import (
+	"context"
 	"fmt"
+
 	"github.com/blevesearch/bleve/v2/search"
 	index "github.com/blevesearch/bleve_index_api"
 )
@@ -30,10 +32,10 @@ var DisjunctionMaxClauseCount = 0
 // slice implementation to a heap implementation.
 var DisjunctionHeapTakeover = 10
 
-func NewDisjunctionSearcher(indexReader index.IndexReader,
+func NewDisjunctionSearcher(ctx context.Context, indexReader index.IndexReader,
 	qsearchers []search.Searcher, min float64, options search.SearcherOptions) (
 	search.Searcher, error) {
-	return newDisjunctionSearcher(indexReader, qsearchers, min, options, true)
+	return newDisjunctionSearcher(ctx, indexReader, qsearchers, min, options, true)
 }
 
 func optionsDisjunctionOptimizable(options search.SearcherOptions) bool {
@@ -41,7 +43,7 @@ func optionsDisjunctionOptimizable(options search.SearcherOptions) bool {
 	return rv
 }
 
-func newDisjunctionSearcher(indexReader index.IndexReader,
+func newDisjunctionSearcher(ctx context.Context, indexReader index.IndexReader,
 	qsearchers []search.Searcher, min float64, options search.SearcherOptions,
 	limit bool) (search.Searcher, error) {
 	// attempt the "unadorned" disjunction optimization only when we
@@ -49,7 +51,7 @@ func newDisjunctionSearcher(indexReader index.IndexReader,
 	// and the requested min is simple
 	if len(qsearchers) > 1 && min <= 1 &&
 		optionsDisjunctionOptimizable(options) {
-		rv, err := optimizeCompositeSearcher("disjunction:unadorned",
+		rv, err := optimizeCompositeSearcher(ctx, "disjunction:unadorned",
 			indexReader, qsearchers, options)
 		if err != nil || rv != nil {
 			return rv, err
@@ -57,14 +59,14 @@ func newDisjunctionSearcher(indexReader index.IndexReader,
 	}
 
 	if len(qsearchers) > DisjunctionHeapTakeover {
-		return newDisjunctionHeapSearcher(indexReader, qsearchers, min, options,
+		return newDisjunctionHeapSearcher(ctx, indexReader, qsearchers, min, options,
 			limit)
 	}
-	return newDisjunctionSliceSearcher(indexReader, qsearchers, min, options,
+	return newDisjunctionSliceSearcher(ctx, indexReader, qsearchers, min, options,
 		limit)
 }
 
-func optimizeCompositeSearcher(optimizationKind string,
+func optimizeCompositeSearcher(ctx context.Context, optimizationKind string,
 	indexReader index.IndexReader, qsearchers []search.Searcher,
 	options search.SearcherOptions) (search.Searcher, error) {
 	var octx index.OptimizableContext
diff --git a/search/searcher/search_disjunction_heap.go b/search/searcher/search_disjunction_heap.go
index bf945976..02358383 100644
--- a/search/searcher/search_disjunction_heap.go
+++ b/search/searcher/search_disjunction_heap.go
@@ -17,6 +17,7 @@ package searcher
 import (
 	"bytes"
 	"container/heap"
+	"context"
 	"math"
 	"reflect"
 
@@ -55,9 +56,11 @@ type DisjunctionHeapSearcher struct {
 
 	matching      []*search.DocumentMatch
 	matchingCurrs []*SearcherCurr
+
+	bytesRead uint64
 }
 
-func newDisjunctionHeapSearcher(indexReader index.IndexReader,
+func newDisjunctionHeapSearcher(ctx context.Context, indexReader index.IndexReader,
 	searchers []search.Searcher, min float64, options search.SearcherOptions,
 	limit bool) (
 	*DisjunctionHeapSearcher, error) {
diff --git a/search/searcher/search_disjunction_slice.go b/search/searcher/search_disjunction_slice.go
index 79fee9f4..6958cf49 100644
--- a/search/searcher/search_disjunction_slice.go
+++ b/search/searcher/search_disjunction_slice.go
@@ -15,6 +15,7 @@
 package searcher
 
 import (
+	"context"
 	"math"
 	"reflect"
 	"sort"
@@ -43,9 +44,10 @@ type DisjunctionSliceSearcher struct {
 	matching     []*search.DocumentMatch
 	matchingIdxs []int
 	initialized  bool
+	bytesRead    uint64
 }
 
-func newDisjunctionSliceSearcher(indexReader index.IndexReader,
+func newDisjunctionSliceSearcher(ctx context.Context, indexReader index.IndexReader,
 	qsearchers []search.Searcher, min float64, options search.SearcherOptions,
 	limit bool) (
 	*DisjunctionSliceSearcher, error) {
@@ -156,7 +158,6 @@ func (s *DisjunctionSliceSearcher) updateMatches() error {
 				matchingIdxs = matchingIdxs[:0]
 			}
 		}
-
 		matching = append(matching, curr)
 		matchingIdxs = append(matchingIdxs, i)
 	}
diff --git a/search/searcher/search_docid.go b/search/searcher/search_docid.go
index 2d90ae16..720fd323 100644
--- a/search/searcher/search_docid.go
+++ b/search/searcher/search_docid.go
@@ -15,6 +15,7 @@
 package searcher
 
 import (
+	"context"
 	"reflect"
 
 	"github.com/blevesearch/bleve/v2/search"
@@ -37,7 +38,7 @@ type DocIDSearcher struct {
 	count  int
 }
 
-func NewDocIDSearcher(indexReader index.IndexReader, ids []string, boost float64,
+func NewDocIDSearcher(ctx context.Context, indexReader index.IndexReader, ids []string, boost float64,
 	options search.SearcherOptions) (searcher *DocIDSearcher, err error) {
 
 	reader, err := indexReader.DocIDReaderOnly(ids)
diff --git a/search/searcher/search_filter.go b/search/searcher/search_filter.go
index 9cab0f78..4e4dd5ea 100644
--- a/search/searcher/search_filter.go
+++ b/search/searcher/search_filter.go
@@ -15,6 +15,7 @@
 package searcher
 
 import (
+	"context"
 	"reflect"
 
 	"github.com/blevesearch/bleve/v2/search"
@@ -41,7 +42,7 @@ type FilteringSearcher struct {
 	accept FilterFunc
 }
 
-func NewFilteringSearcher(s search.Searcher, filter FilterFunc) *FilteringSearcher {
+func NewFilteringSearcher(ctx context.Context, s search.Searcher, filter FilterFunc) *FilteringSearcher {
 	return &FilteringSearcher{
 		child:  s,
 		accept: filter,
diff --git a/search/searcher/search_fuzzy.go b/search/searcher/search_fuzzy.go
index aab67010..9423b611 100644
--- a/search/searcher/search_fuzzy.go
+++ b/search/searcher/search_fuzzy.go
@@ -15,6 +15,7 @@
 package searcher
 
 import (
+	"context"
 	"fmt"
 
 	"github.com/blevesearch/bleve/v2/search"
@@ -23,7 +24,7 @@ import (
 
 var MaxFuzziness = 2
 
-func NewFuzzySearcher(indexReader index.IndexReader, term string,
+func NewFuzzySearcher(ctx context.Context, indexReader index.IndexReader, term string,
 	prefix, fuzziness int, field string, boost float64,
 	options search.SearcherOptions) (search.Searcher, error) {
 
@@ -44,19 +45,47 @@ func NewFuzzySearcher(indexReader index.IndexReader, term string,
 			break
 		}
 	}
-	candidateTerms, err := findFuzzyCandidateTerms(indexReader, term, fuzziness,
+	fuzzyCandidates, err := findFuzzyCandidateTerms(indexReader, term, fuzziness,
 		field, prefixTerm)
 	if err != nil {
 		return nil, err
 	}
 
-	return NewMultiTermSearcher(indexReader, candidateTerms, field,
+	var candidates []string
+	var dictBytesRead uint64
+	if fuzzyCandidates != nil {
+		candidates = fuzzyCandidates.candidates
+		dictBytesRead = fuzzyCandidates.bytesRead
+	}
+
+	if ctx != nil {
+		reportIOStats(dictBytesRead, ctx)
+	}
+
+	return NewMultiTermSearcher(ctx, indexReader, candidates, field,
 		boost, options, true)
 }
 
+type fuzzyCandidates struct {
+	candidates []string
+	bytesRead  uint64
+}
+
+func reportIOStats(bytesRead uint64, ctx context.Context) {
+	// The fuzzy, regexp like queries essentially load a dictionary,
+	// which potentially incurs a cost that must be accounted by
+	// using the callback to report the value.
+	statsCallbackFn := ctx.Value(search.SearchIOStatsCallbackKey)
+	if statsCallbackFn != nil {
+		statsCallbackFn.(search.SearchIOStatsCallbackFunc)(bytesRead)
+	}
+}
+
 func findFuzzyCandidateTerms(indexReader index.IndexReader, term string,
-	fuzziness int, field, prefixTerm string) (rv []string, err error) {
-	rv = make([]string, 0)
+	fuzziness int, field, prefixTerm string) (rv *fuzzyCandidates, err error) {
+	rv = &fuzzyCandidates{
+		candidates: make([]string, 0),
+	}
 
 	// in case of advanced reader implementations directly call
 	// the levenshtein automaton based iterator to collect the
@@ -73,12 +102,14 @@ func findFuzzyCandidateTerms(indexReader index.IndexReader, term string,
 		}()
 		tfd, err := fieldDict.Next()
 		for err == nil && tfd != nil {
-			rv = append(rv, tfd.Term)
-			if tooManyClauses(len(rv)) {
-				return nil, tooManyClausesErr(field, len(rv))
+			rv.candidates = append(rv.candidates, tfd.Term)
+			if tooManyClauses(len(rv.candidates)) {
+				return nil, tooManyClausesErr(field, len(rv.candidates))
 			}
 			tfd, err = fieldDict.Next()
 		}
+
+		rv.bytesRead = fieldDict.BytesRead()
 		return rv, err
 	}
 
@@ -105,13 +136,14 @@ func findFuzzyCandidateTerms(indexReader index.IndexReader, term string,
 		var exceeded bool
 		ld, exceeded, reuse = search.LevenshteinDistanceMaxReuseSlice(term, tfd.Term, fuzziness, reuse)
 		if !exceeded && ld <= fuzziness {
-			rv = append(rv, tfd.Term)
-			if tooManyClauses(len(rv)) {
-				return nil, tooManyClausesErr(field, len(rv))
+			rv.candidates = append(rv.candidates, tfd.Term)
+			if tooManyClauses(len(rv.candidates)) {
+				return nil, tooManyClausesErr(field, len(rv.candidates))
 			}
 		}
 		tfd, err = fieldDict.Next()
 	}
 
+	rv.bytesRead = fieldDict.BytesRead()
 	return rv, err
 }
diff --git a/search/searcher/search_geoboundingbox.go b/search/searcher/search_geoboundingbox.go
index e438bdd3..05ca1bf9 100644
--- a/search/searcher/search_geoboundingbox.go
+++ b/search/searcher/search_geoboundingbox.go
@@ -15,6 +15,8 @@
 package searcher
 
 import (
+	"context"
+
 	"github.com/blevesearch/bleve/v2/document"
 	"github.com/blevesearch/bleve/v2/geo"
 	"github.com/blevesearch/bleve/v2/numeric"
@@ -27,7 +29,7 @@ type filterFunc func(key []byte) bool
 var GeoBitsShift1 = geo.GeoBits << 1
 var GeoBitsShift1Minus1 = GeoBitsShift1 - 1
 
-func NewGeoBoundingBoxSearcher(indexReader index.IndexReader, minLon, minLat,
+func NewGeoBoundingBoxSearcher(ctx context.Context, indexReader index.IndexReader, minLon, minLat,
 	maxLon, maxLat float64, field string, boost float64,
 	options search.SearcherOptions, checkBoundaries bool) (
 	search.Searcher, error) {
@@ -36,7 +38,7 @@ func NewGeoBoundingBoxSearcher(indexReader index.IndexReader, minLon, minLat,
 		if err == nil {
 			terms := sp.GetQueryTokens(geo.NewBoundedRectangle(minLat,
 				minLon, maxLat, maxLon))
-			boxSearcher, err := NewMultiTermSearcher(indexReader,
+			boxSearcher, err := NewMultiTermSearcher(ctx, indexReader,
 				terms, field, boost, options, false)
 			if err != nil {
 				return nil, err
@@ -47,7 +49,7 @@ func NewGeoBoundingBoxSearcher(indexReader index.IndexReader, minLon, minLat,
 				return nil, err
 			}
 
-			return NewFilteringSearcher(boxSearcher, buildRectFilter(dvReader,
+			return NewFilteringSearcher(ctx, boxSearcher, buildRectFilter(dvReader,
 				field, minLon, minLat, maxLon, maxLat)), nil
 		}
 	}
@@ -63,7 +65,7 @@ func NewGeoBoundingBoxSearcher(indexReader index.IndexReader, minLon, minLat,
 	}
 
 	// do math to produce list of terms needed for this search
-	onBoundaryTerms, notOnBoundaryTerms, err := ComputeGeoRange(0, GeoBitsShift1Minus1,
+	onBoundaryTerms, notOnBoundaryTerms, err := ComputeGeoRange(nil, 0, GeoBitsShift1Minus1,
 		minLon, minLat, maxLon, maxLat, checkBoundaries, indexReader, field)
 	if err != nil {
 		return nil, err
@@ -76,13 +78,13 @@ func NewGeoBoundingBoxSearcher(indexReader index.IndexReader, minLon, minLat,
 	}
 
 	if len(onBoundaryTerms) > 0 {
-		rawOnBoundarySearcher, err := NewMultiTermSearcherBytes(indexReader,
+		rawOnBoundarySearcher, err := NewMultiTermSearcherBytes(ctx, indexReader,
 			onBoundaryTerms, field, boost, options, false)
 		if err != nil {
 			return nil, err
 		}
 		// add filter to check points near the boundary
-		onBoundarySearcher = NewFilteringSearcher(rawOnBoundarySearcher,
+		onBoundarySearcher = NewFilteringSearcher(ctx, rawOnBoundarySearcher,
 			buildRectFilter(dvReader, field, minLon, minLat, maxLon, maxLat))
 		openedSearchers = append(openedSearchers, onBoundarySearcher)
 	}
@@ -90,7 +92,7 @@ func NewGeoBoundingBoxSearcher(indexReader index.IndexReader, minLon, minLat,
 	var notOnBoundarySearcher search.Searcher
 	if len(notOnBoundaryTerms) > 0 {
 		var err error
-		notOnBoundarySearcher, err = NewMultiTermSearcherBytes(indexReader,
+		notOnBoundarySearcher, err = NewMultiTermSearcherBytes(ctx, indexReader,
 			notOnBoundaryTerms, field, boost, options, false)
 		if err != nil {
 			cleanupOpenedSearchers()
@@ -100,7 +102,7 @@ func NewGeoBoundingBoxSearcher(indexReader index.IndexReader, minLon, minLat,
 	}
 
 	if onBoundarySearcher != nil && notOnBoundarySearcher != nil {
-		rv, err := NewDisjunctionSearcher(indexReader,
+		rv, err := NewDisjunctionSearcher(ctx, indexReader,
 			[]search.Searcher{
 				onBoundarySearcher,
 				notOnBoundarySearcher,
@@ -125,12 +127,12 @@ var geoDetailLevel = ((geo.GeoBits << 1) - geoMaxShift) / 2
 
 type closeFunc func() error
 
-func ComputeGeoRange(term uint64, shift uint,
+func ComputeGeoRange(ctx context.Context, term uint64, shift uint,
 	sminLon, sminLat, smaxLon, smaxLat float64, checkBoundaries bool,
 	indexReader index.IndexReader, field string) (
 	onBoundary [][]byte, notOnBoundary [][]byte, err error) {
 
-	isIndexed, closeF, err := buildIsIndexedFunc(indexReader, field)
+	isIndexed, closeF, err := buildIsIndexedFunc(ctx, indexReader, field)
 	if closeF != nil {
 		defer func() {
 			cerr := closeF()
@@ -156,7 +158,7 @@ func ComputeGeoRange(term uint64, shift uint,
 	return grc.onBoundary, grc.notOnBoundary, nil
 }
 
-func buildIsIndexedFunc(indexReader index.IndexReader, field string) (isIndexed filterFunc, closeF closeFunc, err error) {
+func buildIsIndexedFunc(ctx context.Context, indexReader index.IndexReader, field string) (isIndexed filterFunc, closeF closeFunc, err error) {
 	if irr, ok := indexReader.(index.IndexReaderContains); ok {
 		fieldDict, err := irr.FieldDictContains(field)
 		if err != nil {
@@ -179,7 +181,7 @@ func buildIsIndexedFunc(indexReader index.IndexReader, field string) (isIndexed
 		}
 	} else if indexReader != nil {
 		isIndexed = func(term []byte) bool {
-			reader, err := indexReader.TermFieldReader(term, field, false, false, false)
+			reader, err := indexReader.TermFieldReader(ctx, term, field, false, false, false)
 			if err != nil || reader == nil {
 				return false
 			}
diff --git a/search/searcher/search_geopointdistance.go b/search/searcher/search_geopointdistance.go
index 8301fbb4..01ed2092 100644
--- a/search/searcher/search_geopointdistance.go
+++ b/search/searcher/search_geopointdistance.go
@@ -15,13 +15,15 @@
 package searcher
 
 import (
+	"context"
+
 	"github.com/blevesearch/bleve/v2/geo"
 	"github.com/blevesearch/bleve/v2/numeric"
 	"github.com/blevesearch/bleve/v2/search"
 	index "github.com/blevesearch/bleve_index_api"
 )
 
-func NewGeoPointDistanceSearcher(indexReader index.IndexReader, centerLon,
+func NewGeoPointDistanceSearcher(ctx context.Context, indexReader index.IndexReader, centerLon,
 	centerLat, dist float64, field string, boost float64,
 	options search.SearcherOptions) (search.Searcher, error) {
 	var rectSearcher search.Searcher
@@ -30,7 +32,7 @@ func NewGeoPointDistanceSearcher(indexReader index.IndexReader, centerLon,
 		if err == nil {
 			terms := sp.GetQueryTokens(geo.NewPointDistance(centerLat,
 				centerLon, dist))
-			rectSearcher, err = NewMultiTermSearcher(indexReader, terms,
+			rectSearcher, err = NewMultiTermSearcher(ctx, indexReader, terms,
 				field, boost, options, false)
 			if err != nil {
 				return nil, err
@@ -49,7 +51,7 @@ func NewGeoPointDistanceSearcher(indexReader index.IndexReader, centerLon,
 		}
 
 		// build a searcher for the box
-		rectSearcher, err = boxSearcher(indexReader,
+		rectSearcher, err = boxSearcher(ctx, indexReader,
 			topLeftLon, topLeftLat, bottomRightLon, bottomRightLat,
 			field, boost, options, false)
 		if err != nil {
@@ -63,27 +65,27 @@ func NewGeoPointDistanceSearcher(indexReader index.IndexReader, centerLon,
 	}
 
 	// wrap it in a filtering searcher which checks the actual distance
-	return NewFilteringSearcher(rectSearcher,
+	return NewFilteringSearcher(ctx, rectSearcher,
 		buildDistFilter(dvReader, field, centerLon, centerLat, dist)), nil
 }
 
 // boxSearcher builds a searcher for the described bounding box
 // if the desired box crosses the dateline, it is automatically split into
 // two boxes joined through a disjunction searcher
-func boxSearcher(indexReader index.IndexReader,
+func boxSearcher(ctx context.Context, indexReader index.IndexReader,
 	topLeftLon, topLeftLat, bottomRightLon, bottomRightLat float64,
 	field string, boost float64, options search.SearcherOptions, checkBoundaries bool) (
 	search.Searcher, error) {
 	if bottomRightLon < topLeftLon {
 		// cross date line, rewrite as two parts
 
-		leftSearcher, err := NewGeoBoundingBoxSearcher(indexReader,
+		leftSearcher, err := NewGeoBoundingBoxSearcher(ctx, indexReader,
 			-180, bottomRightLat, bottomRightLon, topLeftLat,
 			field, boost, options, checkBoundaries)
 		if err != nil {
 			return nil, err
 		}
-		rightSearcher, err := NewGeoBoundingBoxSearcher(indexReader,
+		rightSearcher, err := NewGeoBoundingBoxSearcher(ctx, indexReader,
 			topLeftLon, bottomRightLat, 180, topLeftLat, field, boost, options,
 			checkBoundaries)
 		if err != nil {
@@ -91,7 +93,7 @@ func boxSearcher(indexReader index.IndexReader,
 			return nil, err
 		}
 
-		boxSearcher, err := NewDisjunctionSearcher(indexReader,
+		boxSearcher, err := NewDisjunctionSearcher(ctx, indexReader,
 			[]search.Searcher{leftSearcher, rightSearcher}, 0, options)
 		if err != nil {
 			_ = leftSearcher.Close()
@@ -102,7 +104,7 @@ func boxSearcher(indexReader index.IndexReader,
 	}
 
 	// build geoboundingbox searcher for that bounding box
-	boxSearcher, err := NewGeoBoundingBoxSearcher(indexReader,
+	boxSearcher, err := NewGeoBoundingBoxSearcher(ctx, indexReader,
 		topLeftLon, bottomRightLat, bottomRightLon, topLeftLat, field, boost,
 		options, checkBoundaries)
 	if err != nil {
diff --git a/search/searcher/search_geopolygon.go b/search/searcher/search_geopolygon.go
index ce523432..1d6538ad 100644
--- a/search/searcher/search_geopolygon.go
+++ b/search/searcher/search_geopolygon.go
@@ -15,6 +15,7 @@
 package searcher
 
 import (
+	"context"
 	"fmt"
 	"math"
 
@@ -24,7 +25,7 @@ import (
 	index "github.com/blevesearch/bleve_index_api"
 )
 
-func NewGeoBoundedPolygonSearcher(indexReader index.IndexReader,
+func NewGeoBoundedPolygonSearcher(ctx context.Context, indexReader index.IndexReader,
 	coordinates []geo.Point, field string, boost float64,
 	options search.SearcherOptions) (search.Searcher, error) {
 	if len(coordinates) < 3 {
@@ -36,7 +37,7 @@ func NewGeoBoundedPolygonSearcher(indexReader index.IndexReader,
 		tp, err := sr.GetSpatialAnalyzerPlugin("s2")
 		if err == nil {
 			terms := tp.GetQueryTokens(geo.NewBoundedPolygon(coordinates))
-			rectSearcher, err = NewMultiTermSearcher(indexReader, terms,
+			rectSearcher, err = NewMultiTermSearcher(ctx, indexReader, terms,
 				field, boost, options, false)
 			if err != nil {
 				return nil, err
@@ -55,7 +56,7 @@ func NewGeoBoundedPolygonSearcher(indexReader index.IndexReader,
 		}
 
 		// build a searcher for the bounding box on the polygon
-		rectSearcher, err = boxSearcher(indexReader,
+		rectSearcher, err = boxSearcher(ctx, indexReader,
 			topLeftLon, topLeftLat, bottomRightLon, bottomRightLat,
 			field, boost, options, true)
 		if err != nil {
@@ -69,7 +70,7 @@ func NewGeoBoundedPolygonSearcher(indexReader index.IndexReader,
 	}
 
 	// wrap it in a filtering searcher that checks for the polygon inclusivity
-	return NewFilteringSearcher(rectSearcher,
+	return NewFilteringSearcher(ctx, rectSearcher,
 		buildPolygonFilter(dvReader, field, coordinates)), nil
 }
 
diff --git a/search/searcher/search_geoshape.go b/search/searcher/search_geoshape.go
index 9f03d89c..d2c6b1c5 100644
--- a/search/searcher/search_geoshape.go
+++ b/search/searcher/search_geoshape.go
@@ -16,6 +16,7 @@ package searcher
 
 import (
 	"bytes"
+	"context"
 
 	"github.com/blevesearch/bleve/v2/geo"
 	"github.com/blevesearch/bleve/v2/search"
@@ -23,7 +24,7 @@ import (
 	"github.com/blevesearch/geo/geojson"
 )
 
-func NewGeoShapeSearcher(indexReader index.IndexReader, shape index.GeoJSON,
+func NewGeoShapeSearcher(ctx context.Context, indexReader index.IndexReader, shape index.GeoJSON,
 	relation string, field string, boost float64,
 	options search.SearcherOptions) (search.Searcher, error) {
 	var err error
@@ -41,7 +42,7 @@ func NewGeoShapeSearcher(indexReader index.IndexReader, shape index.GeoJSON,
 
 	// obtain the query tokens.
 	terms := spatialPlugin.GetQueryTokens(shape)
-	mSearcher, err := NewMultiTermSearcher(indexReader, terms,
+	mSearcher, err := NewMultiTermSearcher(ctx, indexReader, terms,
 		field, boost, options, false)
 	if err != nil {
 		return nil, err
@@ -52,7 +53,7 @@ func NewGeoShapeSearcher(indexReader index.IndexReader, shape index.GeoJSON,
 		return nil, err
 	}
 
-	return NewFilteringSearcher(mSearcher,
+	return NewFilteringSearcher(ctx, mSearcher,
 		buildRelationFilterOnShapes(dvReader, field, relation, shape)), nil
 
 }
diff --git a/search/searcher/search_ip_range.go b/search/searcher/search_ip_range.go
index d62f0580..38266206 100644
--- a/search/searcher/search_ip_range.go
+++ b/search/searcher/search_ip_range.go
@@ -15,6 +15,7 @@
 package searcher
 
 import (
+	"context"
 	"net"
 
 	"github.com/blevesearch/bleve/v2/search"
@@ -39,7 +40,7 @@ func netLimits(n *net.IPNet) (lo net.IP, hi net.IP) {
 	return lo, hi
 }
 
-func NewIPRangeSearcher(indexReader index.IndexReader, ipNet *net.IPNet,
+func NewIPRangeSearcher(ctx context.Context, indexReader index.IndexReader, ipNet *net.IPNet,
 	field string, boost float64, options search.SearcherOptions) (
 	search.Searcher, error) {
 
@@ -63,5 +64,5 @@ func NewIPRangeSearcher(indexReader index.IndexReader, ipNet *net.IPNet,
 		return nil, err
 	}
 
-	return NewMultiTermSearcher(indexReader, terms, field, boost, options, true)
+	return NewMultiTermSearcher(ctx, indexReader, terms, field, boost, options, true)
 }
diff --git a/search/searcher/search_match_all.go b/search/searcher/search_match_all.go
index db8306ee..57d8d072 100644
--- a/search/searcher/search_match_all.go
+++ b/search/searcher/search_match_all.go
@@ -15,6 +15,7 @@
 package searcher
 
 import (
+	"context"
 	"reflect"
 
 	"github.com/blevesearch/bleve/v2/search"
@@ -37,7 +38,7 @@ type MatchAllSearcher struct {
 	count       uint64
 }
 
-func NewMatchAllSearcher(indexReader index.IndexReader, boost float64, options search.SearcherOptions) (*MatchAllSearcher, error) {
+func NewMatchAllSearcher(ctx context.Context, indexReader index.IndexReader, boost float64, options search.SearcherOptions) (*MatchAllSearcher, error) {
 	reader, err := indexReader.DocIDReaderAll()
 	if err != nil {
 		return nil, err
@@ -48,6 +49,7 @@ func NewMatchAllSearcher(indexReader index.IndexReader, boost float64, options s
 		return nil, err
 	}
 	scorer := scorer.NewConstantScorer(1.0, boost, options)
+
 	return &MatchAllSearcher{
 		indexReader: indexReader,
 		reader:      reader,
diff --git a/search/searcher/search_multi_term.go b/search/searcher/search_multi_term.go
index 523bf4b5..913f99f5 100644
--- a/search/searcher/search_multi_term.go
+++ b/search/searcher/search_multi_term.go
@@ -15,41 +15,43 @@
 package searcher
 
 import (
+	"context"
 	"fmt"
+
 	"github.com/blevesearch/bleve/v2/search"
 	index "github.com/blevesearch/bleve_index_api"
 )
 
-func NewMultiTermSearcher(indexReader index.IndexReader, terms []string,
+func NewMultiTermSearcher(ctx context.Context, indexReader index.IndexReader, terms []string,
 	field string, boost float64, options search.SearcherOptions, limit bool) (
 	search.Searcher, error) {
 
 	if tooManyClauses(len(terms)) {
 		if optionsDisjunctionOptimizable(options) {
-			return optimizeMultiTermSearcher(indexReader, terms, field, boost, options)
+			return optimizeMultiTermSearcher(ctx, indexReader, terms, field, boost, options)
 		}
 		if limit {
 			return nil, tooManyClausesErr(field, len(terms))
 		}
 	}
 
-	qsearchers, err := makeBatchSearchers(indexReader, terms, field, boost, options)
+	qsearchers, err := makeBatchSearchers(ctx, indexReader, terms, field, boost, options)
 	if err != nil {
 		return nil, err
 	}
 
 	// build disjunction searcher of these ranges
-	return newMultiTermSearcherInternal(indexReader, qsearchers, field, boost,
+	return newMultiTermSearcherInternal(ctx, indexReader, qsearchers, field, boost,
 		options, limit)
 }
 
-func NewMultiTermSearcherBytes(indexReader index.IndexReader, terms [][]byte,
+func NewMultiTermSearcherBytes(ctx context.Context, indexReader index.IndexReader, terms [][]byte,
 	field string, boost float64, options search.SearcherOptions, limit bool) (
 	search.Searcher, error) {
 
 	if tooManyClauses(len(terms)) {
 		if optionsDisjunctionOptimizable(options) {
-			return optimizeMultiTermSearcherBytes(indexReader, terms, field, boost, options)
+			return optimizeMultiTermSearcherBytes(ctx, indexReader, terms, field, boost, options)
 		}
 
 		if limit {
@@ -57,23 +59,23 @@ func NewMultiTermSearcherBytes(indexReader index.IndexReader, terms [][]byte,
 		}
 	}
 
-	qsearchers, err := makeBatchSearchersBytes(indexReader, terms, field, boost, options)
+	qsearchers, err := makeBatchSearchersBytes(ctx, indexReader, terms, field, boost, options)
 	if err != nil {
 		return nil, err
 	}
 
 	// build disjunction searcher of these ranges
-	return newMultiTermSearcherInternal(indexReader, qsearchers, field, boost,
+	return newMultiTermSearcherInternal(ctx, indexReader, qsearchers, field, boost,
 		options, limit)
 }
 
-func newMultiTermSearcherInternal(indexReader index.IndexReader,
+func newMultiTermSearcherInternal(ctx context.Context, indexReader index.IndexReader,
 	searchers []search.Searcher, field string, boost float64,
 	options search.SearcherOptions, limit bool) (
 	search.Searcher, error) {
 
 	// build disjunction searcher of these ranges
-	searcher, err := newDisjunctionSearcher(indexReader, searchers, 0, options,
+	searcher, err := newDisjunctionSearcher(ctx, indexReader, searchers, 0, options,
 		limit)
 	if err != nil {
 		for _, s := range searchers {
@@ -85,7 +87,7 @@ func newMultiTermSearcherInternal(indexReader index.IndexReader,
 	return searcher, nil
 }
 
-func optimizeMultiTermSearcher(indexReader index.IndexReader, terms []string,
+func optimizeMultiTermSearcher(ctx context.Context, indexReader index.IndexReader, terms []string,
 	field string, boost float64, options search.SearcherOptions) (
 	search.Searcher, error) {
 	var finalSearcher search.Searcher
@@ -98,7 +100,7 @@ func optimizeMultiTermSearcher(indexReader index.IndexReader, terms []string,
 			batchTerms = terms
 			terms = nil
 		}
-		batch, err := makeBatchSearchers(indexReader, batchTerms, field, boost, options)
+		batch, err := makeBatchSearchers(ctx, indexReader, batchTerms, field, boost, options)
 		if err != nil {
 			return nil, err
 		}
@@ -112,7 +114,7 @@ func optimizeMultiTermSearcher(indexReader index.IndexReader, terms []string,
 				}
 			}
 		}
-		finalSearcher, err = optimizeCompositeSearcher("disjunction:unadorned",
+		finalSearcher, err = optimizeCompositeSearcher(ctx, "disjunction:unadorned",
 			indexReader, batch, options)
 		// all searchers in batch should be closed, regardless of error or optimization failure
 		// either we're returning, or continuing and only finalSearcher is needed for next loop
@@ -127,7 +129,7 @@ func optimizeMultiTermSearcher(indexReader index.IndexReader, terms []string,
 	return finalSearcher, nil
 }
 
-func makeBatchSearchers(indexReader index.IndexReader, terms []string, field string,
+func makeBatchSearchers(ctx context.Context, indexReader index.IndexReader, terms []string, field string,
 	boost float64, options search.SearcherOptions) ([]search.Searcher, error) {
 
 	qsearchers := make([]search.Searcher, len(terms))
@@ -140,7 +142,7 @@ func makeBatchSearchers(indexReader index.IndexReader, terms []string, field str
 	}
 	for i, term := range terms {
 		var err error
-		qsearchers[i], err = NewTermSearcher(indexReader, term, field, boost, options)
+		qsearchers[i], err = NewTermSearcher(ctx, indexReader, term, field, boost, options)
 		if err != nil {
 			qsearchersClose()
 			return nil, err
@@ -149,7 +151,7 @@ func makeBatchSearchers(indexReader index.IndexReader, terms []string, field str
 	return qsearchers, nil
 }
 
-func optimizeMultiTermSearcherBytes(indexReader index.IndexReader, terms [][]byte,
+func optimizeMultiTermSearcherBytes(ctx context.Context, indexReader index.IndexReader, terms [][]byte,
 	field string, boost float64, options search.SearcherOptions) (
 	search.Searcher, error) {
 
@@ -163,7 +165,7 @@ func optimizeMultiTermSearcherBytes(indexReader index.IndexReader, terms [][]byt
 			batchTerms = terms
 			terms = nil
 		}
-		batch, err := makeBatchSearchersBytes(indexReader, batchTerms, field, boost, options)
+		batch, err := makeBatchSearchersBytes(ctx, indexReader, batchTerms, field, boost, options)
 		if err != nil {
 			return nil, err
 		}
@@ -177,7 +179,7 @@ func optimizeMultiTermSearcherBytes(indexReader index.IndexReader, terms [][]byt
 				}
 			}
 		}
-		finalSearcher, err = optimizeCompositeSearcher("disjunction:unadorned",
+		finalSearcher, err = optimizeCompositeSearcher(ctx, "disjunction:unadorned",
 			indexReader, batch, options)
 		// all searchers in batch should be closed, regardless of error or optimization failure
 		// either we're returning, or continuing and only finalSearcher is needed for next loop
@@ -192,7 +194,7 @@ func optimizeMultiTermSearcherBytes(indexReader index.IndexReader, terms [][]byt
 	return finalSearcher, nil
 }
 
-func makeBatchSearchersBytes(indexReader index.IndexReader, terms [][]byte, field string,
+func makeBatchSearchersBytes(ctx context.Context, indexReader index.IndexReader, terms [][]byte, field string,
 	boost float64, options search.SearcherOptions) ([]search.Searcher, error) {
 
 	qsearchers := make([]search.Searcher, len(terms))
@@ -205,7 +207,7 @@ func makeBatchSearchersBytes(indexReader index.IndexReader, terms [][]byte, fiel
 	}
 	for i, term := range terms {
 		var err error
-		qsearchers[i], err = NewTermSearcherBytes(indexReader, term, field, boost, options)
+		qsearchers[i], err = NewTermSearcherBytes(ctx, indexReader, term, field, boost, options)
 		if err != nil {
 			qsearchersClose()
 			return nil, err
diff --git a/search/searcher/search_numeric_range.go b/search/searcher/search_numeric_range.go
index 6ab5147b..68728c94 100644
--- a/search/searcher/search_numeric_range.go
+++ b/search/searcher/search_numeric_range.go
@@ -16,6 +16,7 @@ package searcher
 
 import (
 	"bytes"
+	"context"
 	"math"
 	"sort"
 
@@ -24,7 +25,7 @@ import (
 	index "github.com/blevesearch/bleve_index_api"
 )
 
-func NewNumericRangeSearcher(indexReader index.IndexReader,
+func NewNumericRangeSearcher(ctx context.Context, indexReader index.IndexReader,
 	min *float64, max *float64, inclusiveMin, inclusiveMax *bool, field string,
 	boost float64, options search.SearcherOptions) (search.Searcher, error) {
 	// account for unbounded edges
@@ -55,6 +56,7 @@ func NewNumericRangeSearcher(indexReader index.IndexReader,
 	}
 
 	var fieldDict index.FieldDictContains
+	var dictBytesRead uint64
 	var isIndexed filterFunc
 	var err error
 	if irr, ok := indexReader.(index.IndexReaderContains); ok {
@@ -67,6 +69,8 @@ func NewNumericRangeSearcher(indexReader index.IndexReader,
 			found, err := fieldDict.Contains(term)
 			return err == nil && found
 		}
+
+		dictBytesRead = fieldDict.BytesRead()
 	}
 
 	// FIXME hard-coded precision, should match field declaration
@@ -81,10 +85,16 @@ func NewNumericRangeSearcher(indexReader index.IndexReader,
 	}
 
 	if len(terms) < 1 {
+		// reporting back the IO stats with respect to the dictionary
+		// loaded, using the context
+		if ctx != nil {
+			reportIOStats(dictBytesRead, ctx)
+		}
+
 		// cannot return MatchNoneSearcher because of interaction with
 		// commit f391b991c20f02681bacd197afc6d8aed444e132
-		return NewMultiTermSearcherBytes(indexReader, terms, field, boost, options,
-			true)
+		return NewMultiTermSearcherBytes(ctx, indexReader, terms, field,
+			boost, options, true)
 	}
 
 	// for upside_down
@@ -99,8 +109,12 @@ func NewNumericRangeSearcher(indexReader index.IndexReader,
 		return nil, tooManyClausesErr(field, len(terms))
 	}
 
-	return NewMultiTermSearcherBytes(indexReader, terms, field, boost, options,
-		true)
+	if ctx != nil {
+		reportIOStats(dictBytesRead, ctx)
+	}
+
+	return NewMultiTermSearcherBytes(ctx, indexReader, terms, field,
+		boost, options, true)
 }
 
 func filterCandidateTerms(indexReader index.IndexReader,
diff --git a/search/searcher/search_phrase.go b/search/searcher/search_phrase.go
index c262fd91..087ad768 100644
--- a/search/searcher/search_phrase.go
+++ b/search/searcher/search_phrase.go
@@ -15,6 +15,7 @@
 package searcher
 
 import (
+	"context"
 	"fmt"
 	"math"
 	"reflect"
@@ -63,22 +64,22 @@ func (s *PhraseSearcher) Size() int {
 	return sizeInBytes
 }
 
-func NewPhraseSearcher(indexReader index.IndexReader, terms []string, field string, options search.SearcherOptions) (*PhraseSearcher, error) {
+func NewPhraseSearcher(ctx context.Context, indexReader index.IndexReader, terms []string, field string, options search.SearcherOptions) (*PhraseSearcher, error) {
 	// turn flat terms []string into [][]string
 	mterms := make([][]string, len(terms))
 	for i, term := range terms {
 		mterms[i] = []string{term}
 	}
-	return NewMultiPhraseSearcher(indexReader, mterms, field, options)
+	return NewMultiPhraseSearcher(ctx, indexReader, mterms, field, options)
 }
 
-func NewMultiPhraseSearcher(indexReader index.IndexReader, terms [][]string, field string, options search.SearcherOptions) (*PhraseSearcher, error) {
+func NewMultiPhraseSearcher(ctx context.Context, indexReader index.IndexReader, terms [][]string, field string, options search.SearcherOptions) (*PhraseSearcher, error) {
 	options.IncludeTermVectors = true
 	var termPositionSearchers []search.Searcher
 	for _, termPos := range terms {
 		if len(termPos) == 1 && termPos[0] != "" {
 			// single term
-			ts, err := NewTermSearcher(indexReader, termPos[0], field, 1.0, options)
+			ts, err := NewTermSearcher(ctx, indexReader, termPos[0], field, 1.0, options)
 			if err != nil {
 				// close any searchers already opened
 				for _, ts := range termPositionSearchers {
@@ -94,7 +95,7 @@ func NewMultiPhraseSearcher(indexReader index.IndexReader, terms [][]string, fie
 				if term == "" {
 					continue
 				}
-				ts, err := NewTermSearcher(indexReader, term, field, 1.0, options)
+				ts, err := NewTermSearcher(ctx, indexReader, term, field, 1.0, options)
 				if err != nil {
 					// close any searchers already opened
 					for _, ts := range termPositionSearchers {
@@ -104,7 +105,7 @@ func NewMultiPhraseSearcher(indexReader index.IndexReader, terms [][]string, fie
 				}
 				termSearchers = append(termSearchers, ts)
 			}
-			disjunction, err := NewDisjunctionSearcher(indexReader, termSearchers, 1, options)
+			disjunction, err := NewDisjunctionSearcher(ctx, indexReader, termSearchers, 1, options)
 			if err != nil {
 				// close any searchers already opened
 				for _, ts := range termPositionSearchers {
@@ -116,7 +117,7 @@ func NewMultiPhraseSearcher(indexReader index.IndexReader, terms [][]string, fie
 		}
 	}
 
-	mustSearcher, err := NewConjunctionSearcher(indexReader, termPositionSearchers, options)
+	mustSearcher, err := NewConjunctionSearcher(ctx, indexReader, termPositionSearchers, options)
 	if err != nil {
 		// close any searchers already opened
 		for _, ts := range termPositionSearchers {
diff --git a/search/searcher/search_regexp.go b/search/searcher/search_regexp.go
index 81b1cf73..b419d547 100644
--- a/search/searcher/search_regexp.go
+++ b/search/searcher/search_regexp.go
@@ -15,6 +15,7 @@
 package searcher
 
 import (
+	"context"
 	"regexp"
 
 	"github.com/blevesearch/bleve/v2/search"
@@ -34,7 +35,7 @@ type Regexp interface {
 
 // NewRegexpStringSearcher is similar to NewRegexpSearcher, but
 // additionally optimizes for index readers that handle regexp's.
-func NewRegexpStringSearcher(indexReader index.IndexReader, pattern string,
+func NewRegexpStringSearcher(ctx context.Context, indexReader index.IndexReader, pattern string,
 	field string, boost float64, options search.SearcherOptions) (
 	search.Searcher, error) {
 	ir, ok := indexReader.(index.IndexReaderRegexp)
@@ -44,7 +45,7 @@ func NewRegexpStringSearcher(indexReader index.IndexReader, pattern string,
 			return nil, err
 		}
 
-		return NewRegexpSearcher(indexReader, r, field, boost, options)
+		return NewRegexpSearcher(ctx, indexReader, r, field, boost, options)
 	}
 
 	fieldDict, err := ir.FieldDictRegexp(field, pattern)
@@ -68,7 +69,7 @@ func NewRegexpStringSearcher(indexReader index.IndexReader, pattern string,
 		return nil, err
 	}
 
-	return NewMultiTermSearcher(indexReader, candidateTerms, field, boost,
+	return NewMultiTermSearcher(ctx, indexReader, candidateTerms, field, boost,
 		options, true)
 }
 
@@ -77,31 +78,47 @@ func NewRegexpStringSearcher(indexReader index.IndexReader, pattern string,
 // matching the entire term.  The provided regexp SHOULD NOT start with ^
 // or end with $ as this can intefere with the implementation.  Separately,
 // matches will be checked to ensure they match the entire term.
-func NewRegexpSearcher(indexReader index.IndexReader, pattern Regexp,
+func NewRegexpSearcher(ctx context.Context, indexReader index.IndexReader, pattern Regexp,
 	field string, boost float64, options search.SearcherOptions) (
 	search.Searcher, error) {
 	var candidateTerms []string
-
+	var regexpCandidates *regexpCandidates
 	prefixTerm, complete := pattern.LiteralPrefix()
 	if complete {
 		// there is no pattern
 		candidateTerms = []string{prefixTerm}
 	} else {
 		var err error
-		candidateTerms, err = findRegexpCandidateTerms(indexReader, pattern, field,
+		regexpCandidates, err = findRegexpCandidateTerms(indexReader, pattern, field,
 			prefixTerm)
 		if err != nil {
 			return nil, err
 		}
 	}
+	var dictBytesRead uint64
+	if regexpCandidates != nil {
+		candidateTerms = regexpCandidates.candidates
+		dictBytesRead = regexpCandidates.bytesRead
+	}
+
+	if ctx != nil {
+		reportIOStats(dictBytesRead, ctx)
+	}
 
-	return NewMultiTermSearcher(indexReader, candidateTerms, field, boost,
+	return NewMultiTermSearcher(ctx, indexReader, candidateTerms, field, boost,
 		options, true)
 }
 
+type regexpCandidates struct {
+	candidates []string
+	bytesRead  uint64
+}
+
 func findRegexpCandidateTerms(indexReader index.IndexReader,
-	pattern Regexp, field, prefixTerm string) (rv []string, err error) {
-	rv = make([]string, 0)
+	pattern Regexp, field, prefixTerm string) (rv *regexpCandidates, err error) {
+	rv = &regexpCandidates{
+		candidates: make([]string, 0),
+	}
 	var fieldDict index.FieldDict
 	if len(prefixTerm) > 0 {
 		fieldDict, err = indexReader.FieldDictPrefix(field, []byte(prefixTerm))
@@ -119,13 +136,13 @@ func findRegexpCandidateTerms(indexReader index.IndexReader,
 	for err == nil && tfd != nil {
 		matchPos := pattern.FindStringIndex(tfd.Term)
 		if matchPos != nil && matchPos[0] == 0 && matchPos[1] == len(tfd.Term) {
-			rv = append(rv, tfd.Term)
-			if tooManyClauses(len(rv)) {
-				return rv, tooManyClausesErr(field, len(rv))
+			rv.candidates = append(rv.candidates, tfd.Term)
+			if tooManyClauses(len(rv.candidates)) {
+				return rv, tooManyClausesErr(field, len(rv.candidates))
 			}
 		}
 		tfd, err = fieldDict.Next()
 	}
-
+	rv.bytesRead = fieldDict.BytesRead()
 	return rv, err
 }
diff --git a/search/searcher/search_term.go b/search/searcher/search_term.go
index 55c18d16..db18e537 100644
--- a/search/searcher/search_term.go
+++ b/search/searcher/search_term.go
@@ -15,6 +15,7 @@
 package searcher
 
 import (
+	"context"
 	"reflect"
 
 	"github.com/blevesearch/bleve/v2/search"
@@ -37,13 +38,13 @@ type TermSearcher struct {
 	tfd         index.TermFieldDoc
 }
 
-func NewTermSearcher(indexReader index.IndexReader, term string, field string, boost float64, options search.SearcherOptions) (*TermSearcher, error) {
-	return NewTermSearcherBytes(indexReader, []byte(term), field, boost, options)
+func NewTermSearcher(ctx context.Context, indexReader index.IndexReader, term string, field string, boost float64, options search.SearcherOptions) (*TermSearcher, error) {
+	return NewTermSearcherBytes(ctx, indexReader, []byte(term), field, boost, options)
 }
 
-func NewTermSearcherBytes(indexReader index.IndexReader, term []byte, field string, boost float64, options search.SearcherOptions) (*TermSearcher, error) {
+func NewTermSearcherBytes(ctx context.Context, indexReader index.IndexReader, term []byte, field string, boost float64, options search.SearcherOptions) (*TermSearcher, error) {
 	needFreqNorm := options.Score != "none"
-	reader, err := indexReader.TermFieldReader(term, field, needFreqNorm, needFreqNorm, options.IncludeTermVectors)
+	reader, err := indexReader.TermFieldReader(ctx, term, field, needFreqNorm, needFreqNorm, options.IncludeTermVectors)
 	if err != nil {
 		return nil, err
 	}
diff --git a/search/searcher/search_term_prefix.go b/search/searcher/search_term_prefix.go
index a01b1869..89f836a5 100644
--- a/search/searcher/search_term_prefix.go
+++ b/search/searcher/search_term_prefix.go
@@ -15,11 +15,13 @@
 package searcher
 
 import (
+	"context"
+
 	"github.com/blevesearch/bleve/v2/search"
 	index "github.com/blevesearch/bleve_index_api"
 )
 
-func NewTermPrefixSearcher(indexReader index.IndexReader, prefix string,
+func NewTermPrefixSearcher(ctx context.Context, indexReader index.IndexReader, prefix string,
 	field string, boost float64, options search.SearcherOptions) (
 	search.Searcher, error) {
 	// find the terms with this prefix
@@ -46,5 +48,9 @@ func NewTermPrefixSearcher(indexReader index.IndexReader, prefix string,
 		return nil, err
 	}
 
-	return NewMultiTermSearcher(indexReader, terms, field, boost, options, true)
+	if ctx != nil {
+		reportIOStats(fieldDict.BytesRead(), ctx)
+	}
+
+	return NewMultiTermSearcher(ctx, indexReader, terms, field, boost, options, true)
 }
diff --git a/search/searcher/search_term_range.go b/search/searcher/search_term_range.go
index 5ef58f76..a2fb4e99 100644
--- a/search/searcher/search_term_range.go
+++ b/search/searcher/search_term_range.go
@@ -15,11 +15,13 @@
 package searcher
 
 import (
+	"context"
+
 	"github.com/blevesearch/bleve/v2/search"
 	index "github.com/blevesearch/bleve_index_api"
 )
 
-func NewTermRangeSearcher(indexReader index.IndexReader,
+func NewTermRangeSearcher(ctx context.Context, indexReader index.IndexReader,
 	min, max []byte, inclusiveMin, inclusiveMax *bool, field string,
 	boost float64, options search.SearcherOptions) (search.Searcher, error) {
 
@@ -81,5 +83,9 @@ func NewTermRangeSearcher(indexReader index.IndexReader,
 		terms = terms[:len(terms)-1]
 	}
 
-	return NewMultiTermSearcher(indexReader, terms, field, boost, options, true)
+	if ctx != nil {
+		reportIOStats(fieldDict.BytesRead(), ctx)
+	}
+
+	return NewMultiTermSearcher(ctx, indexReader, terms, field, boost, options, true)
 }


Test patch:
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 454eb48e..1acdd987 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -8,7 +8,7 @@ jobs:
   test:
     strategy:
       matrix:
-        go-version: [1.14.x, 1.15.x, 1.16.x, 1.17.x, 1.18.x]
+        go-version: [1.17.x, 1.18.x, 1.19.x]
         platform: [ubuntu-latest, macos-latest, windows-latest]
     runs-on: ${{ matrix.platform }}
     steps:
diff --git a/analysis/lang/cjk/cjk_bigram_test.go b/analysis/lang/cjk/cjk_bigram_test.go
index d1b5d000..81bb3c8c 100644
--- a/analysis/lang/cjk/cjk_bigram_test.go
+++ b/analysis/lang/cjk/cjk_bigram_test.go
@@ -309,6 +309,147 @@ func TestCJKBigramFilter(t *testing.T) {
 				},
 			},
 		},
+		{
+			// Assuming that `、` is removed by unicode tokenizer from `こんにちは、世界`
+			outputUnigram: true,
+			input: analysis.TokenStream{
+				&analysis.Token{
+					Term:     []byte("こ"),
+					Type:     analysis.Ideographic,
+					Position: 1,
+					Start:    0,
+					End:      3,
+				},
+				&analysis.Token{
+					Term:     []byte("ん"),
+					Type:     analysis.Ideographic,
+					Position: 2,
+					Start:    3,
+					End:      6,
+				},
+				&analysis.Token{
+					Term:     []byte("に"),
+					Type:     analysis.Ideographic,
+					Position: 3,
+					Start:    6,
+					End:      9,
+				},
+				&analysis.Token{
+					Term:     []byte("ち"),
+					Type:     analysis.Ideographic,
+					Position: 4,
+					Start:    9,
+					End:      12,
+				},
+				&analysis.Token{
+					Term:     []byte("は"),
+					Type:     analysis.Ideographic,
+					Position: 5,
+					Start:    12,
+					End:      15,
+				},
+				&analysis.Token{
+					Term:     []byte("世"),
+					Type:     analysis.Ideographic,
+					Position: 7,
+					Start:    18,
+					End:      21,
+				},
+				&analysis.Token{
+					Term:     []byte("界"),
+					Type:     analysis.Ideographic,
+					Position: 8,
+					Start:    21,
+					End:      24,
+				},
+			},
+			output: analysis.TokenStream{
+				&analysis.Token{
+					Term:     []byte("こ"),
+					Type:     analysis.Single,
+					Position: 1,
+					Start:    0,
+					End:      3,
+				},
+				&analysis.Token{
+					Term:     []byte("こん"),
+					Type:     analysis.Double,
+					Position: 1,
+					Start:    0,
+					End:      6,
+				},
+				&analysis.Token{
+					Term:     []byte("ん"),
+					Type:     analysis.Single,
+					Position: 2,
+					Start:    3,
+					End:      6,
+				},
+				&analysis.Token{
+					Term:     []byte("んに"),
+					Type:     analysis.Double,
+					Position: 2,
+					Start:    3,
+					End:      9,
+				},
+				&analysis.Token{
+					Term:     []byte("に"),
+					Type:     analysis.Single,
+					Position: 3,
+					Start:    6,
+					End:      9,
+				},
+				&analysis.Token{
+					Term:     []byte("にち"),
+					Type:     analysis.Double,
+					Position: 3,
+					Start:    6,
+					End:      12,
+				},
+				&analysis.Token{
+					Term:     []byte("ち"),
+					Type:     analysis.Single,
+					Position: 4,
+					Start:    9,
+					End:      12,
+				},
+				&analysis.Token{
+					Term:     []byte("ちは"),
+					Type:     analysis.Double,
+					Position: 4,
+					Start:    9,
+					End:      15,
+				},
+				&analysis.Token{
+					Term:     []byte("は"),
+					Type:     analysis.Single,
+					Position: 5,
+					Start:    12,
+					End:      15,
+				},
+				&analysis.Token{
+					Term:     []byte("世"),
+					Type:     analysis.Single,
+					Position: 6,
+					Start:    18,
+					End:      21,
+				},
+				&analysis.Token{
+					Term:     []byte("世界"),
+					Type:     analysis.Double,
+					Position: 6,
+					Start:    18,
+					End:      24,
+				},
+				&analysis.Token{
+					Term:     []byte("界"),
+					Type:     analysis.Single,
+					Position: 7,
+					Start:    21,
+					End:      24,
+				},
+			},
+		},
 		{
 			outputUnigram: false,
 			input: analysis.TokenStream{
diff --git a/analysis/lang/ckb/sorani_stemmer_filter_test.go b/analysis/lang/ckb/sorani_stemmer_filter_test.go
index c0d847c4..4dd9a9fd 100644
--- a/analysis/lang/ckb/sorani_stemmer_filter_test.go
+++ b/analysis/lang/ckb/sorani_stemmer_filter_test.go
@@ -26,7 +26,7 @@ func TestSoraniStemmerFilter(t *testing.T) {
 
 	// in order to match the lucene tests
 	// we will test with an analyzer, not just the stemmer
-	analyzer := analysis.Analyzer{
+	analyzer := analysis.DefaultAnalyzer{
 		Tokenizer: single.NewSingleTokenTokenizer(),
 		TokenFilters: []analysis.TokenFilter{
 			NewSoraniNormalizeFilter(),
diff --git a/index/scorch/builder_test.go b/index/scorch/builder_test.go
index aaff060e..82309e38 100644
--- a/index/scorch/builder_test.go
+++ b/index/scorch/builder_test.go
@@ -102,7 +102,7 @@ func checkIndex(t *testing.T, path string, term []byte, field string, expectCoun
 	}
 
 	// run a search for hello
-	tfr, err := r.TermFieldReader(term, field, false, false, false)
+	tfr, err := r.TermFieldReader(nil, term, field, false, false, false)
 	if err != nil {
 		t.Errorf("error accessing term field reader: %v", err)
 	} else {
diff --git a/index/scorch/reader_test.go b/index/scorch/reader_test.go
index e1bca049..0eebd92b 100644
--- a/index/scorch/reader_test.go
+++ b/index/scorch/reader_test.go
@@ -82,7 +82,7 @@ func TestIndexReader(t *testing.T) {
 	}()
 
 	// first look for a term that doesn't exist
-	reader, err := indexReader.TermFieldReader([]byte("nope"), "name", true, true, true)
+	reader, err := indexReader.TermFieldReader(nil, []byte("nope"), "name", true, true, true)
 	if err != nil {
 		t.Errorf("Error accessing term field reader: %v", err)
 	}
@@ -95,7 +95,7 @@ func TestIndexReader(t *testing.T) {
 		t.Fatal(err)
 	}
 
-	reader, err = indexReader.TermFieldReader([]byte("test"), "name", true, true, true)
+	reader, err = indexReader.TermFieldReader(nil, []byte("test"), "name", true, true, true)
 	if err != nil {
 		t.Errorf("Error accessing term field reader: %v", err)
 	}
@@ -145,7 +145,7 @@ func TestIndexReader(t *testing.T) {
 			},
 		},
 	}
-	tfr, err := indexReader.TermFieldReader([]byte("rice"), "desc", true, true, true)
+	tfr, err := indexReader.TermFieldReader(nil, []byte("rice"), "desc", true, true, true)
 	if err != nil {
 		t.Errorf("unexpected error: %v", err)
 	}
@@ -153,6 +153,7 @@ func TestIndexReader(t *testing.T) {
 	if err != nil {
 		t.Errorf("unexpected error: %v", err)
 	}
+
 	if !reflect.DeepEqual(expectedMatch, match) {
 		t.Errorf("got %#v, expected %#v", match, expectedMatch)
 	}
@@ -162,7 +163,7 @@ func TestIndexReader(t *testing.T) {
 	}
 
 	// now test usage of advance
-	reader, err = indexReader.TermFieldReader([]byte("test"), "name", true, true, true)
+	reader, err = indexReader.TermFieldReader(nil, []byte("test"), "name", true, true, true)
 	if err != nil {
 		t.Errorf("Error accessing term field reader: %v", err)
 	}
@@ -193,7 +194,7 @@ func TestIndexReader(t *testing.T) {
 	}
 
 	// now test creating a reader for a field that doesn't exist
-	reader, err = indexReader.TermFieldReader([]byte("water"), "doesnotexist", true, true, true)
+	reader, err = indexReader.TermFieldReader(nil, []byte("water"), "doesnotexist", true, true, true)
 	if err != nil {
 		t.Errorf("Error accessing term field reader: %v", err)
 	}
diff --git a/index/scorch/scorch_test.go b/index/scorch/scorch_test.go
index 20a7028f..8c0d6ccf 100644
--- a/index/scorch/scorch_test.go
+++ b/index/scorch/scorch_test.go
@@ -60,7 +60,7 @@ func CreateConfig(name string) map[string]interface{} {
 	return rv
 }
 
-var testAnalyzer = &analysis.Analyzer{
+var testAnalyzer = &analysis.DefaultAnalyzer{
 	Tokenizer: regexpTokenizer.NewRegexpTokenizer(regexp.MustCompile(`\w+`)),
 }
 
@@ -1528,7 +1528,7 @@ func TestIndexTermReaderCompositeFields(t *testing.T) {
 		}
 	}()
 
-	termFieldReader, err := indexReader.TermFieldReader([]byte("mister"), "_all", true, true, true)
+	termFieldReader, err := indexReader.TermFieldReader(nil, []byte("mister"), "_all", true, true, true)
 	if err != nil {
 		t.Error(err)
 	}
@@ -2480,7 +2480,7 @@ func TestIndexSeekBackwardsStats(t *testing.T) {
 	}
 	defer reader.Close()
 
-	tfr, err := reader.TermFieldReader([]byte("cat"), "name", false, false, false)
+	tfr, err := reader.TermFieldReader(nil, []byte("cat"), "name", false, false, false)
 	if err != nil {
 		t.Fatalf("error getting term field readyer for name/cat: %v", err)
 	}
diff --git a/index/upsidedown/reader_test.go b/index/upsidedown/reader_test.go
index 9aac40a3..933723c9 100644
--- a/index/upsidedown/reader_test.go
+++ b/index/upsidedown/reader_test.go
@@ -77,7 +77,7 @@ func TestIndexReader(t *testing.T) {
 	}()
 
 	// first look for a term that doesn't exist
-	reader, err := indexReader.TermFieldReader([]byte("nope"), "name", true, true, true)
+	reader, err := indexReader.TermFieldReader(nil, []byte("nope"), "name", true, true, true)
 	if err != nil {
 		t.Errorf("Error accessing term field reader: %v", err)
 	}
@@ -90,7 +90,7 @@ func TestIndexReader(t *testing.T) {
 		t.Fatal(err)
 	}
 
-	reader, err = indexReader.TermFieldReader([]byte("test"), "name", true, true, true)
+	reader, err = indexReader.TermFieldReader(nil, []byte("test"), "name", true, true, true)
 	if err != nil {
 		t.Errorf("Error accessing term field reader: %v", err)
 	}
@@ -128,7 +128,7 @@ func TestIndexReader(t *testing.T) {
 			},
 		},
 	}
-	tfr, err := indexReader.TermFieldReader([]byte("rice"), "desc", true, true, true)
+	tfr, err := indexReader.TermFieldReader(nil, []byte("rice"), "desc", true, true, true)
 	if err != nil {
 		t.Errorf("unexpected error: %v", err)
 	}
@@ -145,7 +145,7 @@ func TestIndexReader(t *testing.T) {
 	}
 
 	// now test usage of advance
-	reader, err = indexReader.TermFieldReader([]byte("test"), "name", true, true, true)
+	reader, err = indexReader.TermFieldReader(nil, []byte("test"), "name", true, true, true)
 	if err != nil {
 		t.Errorf("Error accessing term field reader: %v", err)
 	}
@@ -173,7 +173,7 @@ func TestIndexReader(t *testing.T) {
 	}
 
 	// now test creating a reader for a field that doesn't exist
-	reader, err = indexReader.TermFieldReader([]byte("water"), "doesnotexist", true, true, true)
+	reader, err = indexReader.TermFieldReader(nil, []byte("water"), "doesnotexist", true, true, true)
 	if err != nil {
 		t.Errorf("Error accessing term field reader: %v", err)
 	}
diff --git a/index/upsidedown/upsidedown_test.go b/index/upsidedown/upsidedown_test.go
index 726caa6c..9b9ab909 100644
--- a/index/upsidedown/upsidedown_test.go
+++ b/index/upsidedown/upsidedown_test.go
@@ -34,7 +34,7 @@ import (
 	index "github.com/blevesearch/bleve_index_api"
 )
 
-var testAnalyzer = &analysis.Analyzer{
+var testAnalyzer = &analysis.DefaultAnalyzer{
 	Tokenizer: regexpTokenizer.NewRegexpTokenizer(regexp.MustCompile(`\w+`)),
 }
 
@@ -1239,7 +1239,7 @@ func TestIndexTermReaderCompositeFields(t *testing.T) {
 		}
 	}()
 
-	termFieldReader, err := indexReader.TermFieldReader([]byte("mister"), "_all", true, true, true)
+	termFieldReader, err := indexReader.TermFieldReader(nil, []byte("mister"), "_all", true, true, true)
 	if err != nil {
 		t.Error(err)
 	}
diff --git a/index_test.go b/index_test.go
index 590a132b..93389420 100644
--- a/index_test.go
+++ b/index_test.go
@@ -237,7 +237,7 @@ func approxSame(actual, expected uint64) bool {
 		return b - a
 	}
 
-	return float64(modulus(actual, expected))/float64(expected) < float64(0.25)
+	return float64(modulus(actual, expected))/float64(expected) < float64(0.30)
 }
 
 func checkStatsOnIndexedBatch(indexPath string, indexMapping mapping.IndexMapping,
@@ -371,6 +371,7 @@ func TestBytesRead(t *testing.T) {
 	typeFieldMapping := NewTextFieldMapping()
 	typeFieldMapping.Store = false
 	documentMapping.AddFieldMappingsAt("type", typeFieldMapping)
+
 	idx, err := NewUsing(tmpIndexPath, indexMapping, Config.DefaultIndexType, Config.DefaultMemKVStore, nil)
 	if err != nil {
 		t.Fatal(err)
@@ -394,27 +395,27 @@ func TestBytesRead(t *testing.T) {
 	query := NewQueryStringQuery("united")
 	searchRequest := NewSearchRequestOptions(query, int(10), 0, true)
 
-	_, err = idx.Search(searchRequest)
+	res, err := idx.Search(searchRequest)
 	if err != nil {
 		t.Error(err)
 	}
 	stats, _ := idx.StatsMap()["index"].(map[string]interface{})
 	prevBytesRead, _ := stats["num_bytes_read_at_query_time"].(uint64)
-	if prevBytesRead != 28618 {
-		t.Fatalf("expected bytes read for query string 28618, got %v",
+	if prevBytesRead != 32349 && res.BytesRead == prevBytesRead {
+		t.Fatalf("expected bytes read for query string 32349, got %v",
 			prevBytesRead)
 	}
 
 	// subsequent queries on the same field results in lesser amount
 	// of bytes read because the segment static and dictionary is reused and not
 	// loaded from mmap'd filed
-	_, err = idx.Search(searchRequest)
+	res, err = idx.Search(searchRequest)
 	if err != nil {
 		t.Error(err)
 	}
 	stats, _ = idx.StatsMap()["index"].(map[string]interface{})
 	bytesRead, _ := stats["num_bytes_read_at_query_time"].(uint64)
-	if bytesRead-prevBytesRead != 23 {
+	if bytesRead-prevBytesRead != 23 && res.BytesRead == bytesRead-prevBytesRead {
 		t.Fatalf("expected bytes read for query string 23, got %v",
 			bytesRead-prevBytesRead)
 	}
@@ -424,14 +425,14 @@ func TestBytesRead(t *testing.T) {
 	fuzz.FieldVal = "reviews.content"
 	fuzz.Fuzziness = 2
 	searchRequest = NewSearchRequest(fuzz)
-	_, err = idx.Search(searchRequest)
+	res, err = idx.Search(searchRequest)
 	if err != nil {
 		t.Error(err)
 	}
 	stats, _ = idx.StatsMap()["index"].(map[string]interface{})
 	bytesRead, _ = stats["num_bytes_read_at_query_time"].(uint64)
-	if bytesRead-prevBytesRead != 16556 {
-		t.Fatalf("expected bytes read for fuzzy query is 16556, got %v\n",
+	if bytesRead-prevBytesRead != 206545 && res.BytesRead == bytesRead-prevBytesRead {
+		t.Fatalf("expected bytes read for fuzzy query is 206545, got %v",
 			bytesRead-prevBytesRead)
 	}
 	prevBytesRead = bytesRead
@@ -440,64 +441,65 @@ func TestBytesRead(t *testing.T) {
 	query = NewQueryStringQuery("united")
 	searchRequest = NewSearchRequestOptions(query, int(0), 0, true)
 	searchRequest.AddFacet("types", typeFacet)
-	_, err = idx.Search(searchRequest)
+	res, err = idx.Search(searchRequest)
 	if err != nil {
 		t.Error(err)
 	}
 
 	stats, _ = idx.StatsMap()["index"].(map[string]interface{})
 	bytesRead, _ = stats["num_bytes_read_at_query_time"].(uint64)
-	if !approxSame(bytesRead-prevBytesRead, 259) {
-		t.Fatalf("expected bytes read for faceted query is 259, got %v",
+	if !approxSame(bytesRead-prevBytesRead, 150) && res.BytesRead == bytesRead-prevBytesRead {
+		t.Fatalf("expected bytes read for faceted query is around 150, got %v",
 			bytesRead-prevBytesRead)
 	}
 	prevBytesRead = bytesRead
 
-	min := float64(8000)
-	max := float64(8010)
+	min := float64(8660)
+	max := float64(8665)
 	numRangeQuery := NewNumericRangeQuery(&min, &max)
 	numRangeQuery.FieldVal = "id"
 	searchRequest = NewSearchRequestOptions(numRangeQuery, int(10), 0, true)
-	_, err = idx.Search(searchRequest)
+	res, err = idx.Search(searchRequest)
 	if err != nil {
 		t.Error(err)
 	}
 
 	stats, _ = idx.StatsMap()["index"].(map[string]interface{})
 	bytesRead, _ = stats["num_bytes_read_at_query_time"].(uint64)
-	if bytesRead-prevBytesRead != 1678 {
-		t.Fatalf("expected bytes read for numeric range query is 1678, got %v",
+	if bytesRead-prevBytesRead != 54945 && res.BytesRead == bytesRead-prevBytesRead {
+		t.Fatalf("expected bytes read for numeric range query is 54945, got %v",
 			bytesRead-prevBytesRead)
 	}
 	prevBytesRead = bytesRead
 
 	searchRequest = NewSearchRequestOptions(query, int(10), 0, true)
 	searchRequest.Highlight = &HighlightRequest{}
-	_, err = idx.Search(searchRequest)
+	res, err = idx.Search(searchRequest)
 	if err != nil {
 		t.Error(err)
 	}
 
 	stats, _ = idx.StatsMap()["index"].(map[string]interface{})
 	bytesRead, _ = stats["num_bytes_read_at_query_time"].(uint64)
-	if bytesRead-prevBytesRead != 676 {
-		t.Fatalf("expected bytes read for query with highlighter is 676, got %v",
+	if bytesRead-prevBytesRead != 60 && res.BytesRead == bytesRead-prevBytesRead {
+		t.Fatalf("expected bytes read for query with highlighter is 60, got %v",
 			bytesRead-prevBytesRead)
 	}
 	prevBytesRead = bytesRead
 
-	disQuery := NewDisjunctionQuery(NewMatchQuery("united"), NewMatchQuery("hotel"))
+	disQuery := NewDisjunctionQuery(NewMatchQuery("hotel"), NewMatchQuery("united"))
 	searchRequest = NewSearchRequestOptions(disQuery, int(10), 0, true)
-	_, err = idx.Search(searchRequest)
+	res, err = idx.Search(searchRequest)
 	if err != nil {
 		t.Error(err)
 	}
 	// expectation is that the bytes read is roughly equal to sum of sub queries in
-	// the disjunction query plus sum static value
+	// the disjunction query plus the segment loading portion for the second subquery
+	// since it's created afresh and not reused
 	stats, _ = idx.StatsMap()["index"].(map[string]interface{})
 	bytesRead, _ = stats["num_bytes_read_at_query_time"].(uint64)
-	if bytesRead-prevBytesRead != 77 {
-		t.Fatalf("expected bytes read for disjunction query is 77, got %v",
+	if bytesRead-prevBytesRead != 18090 && res.BytesRead == bytesRead-prevBytesRead {
+		t.Fatalf("expected bytes read for disjunction query is 18090, got %v",
 			bytesRead-prevBytesRead)
 	}
 }
@@ -571,33 +573,33 @@ func TestBytesReadStored(t *testing.T) {
 	query := NewTermQuery("hotel")
 	query.FieldVal = "reviews.content"
 	searchRequest := NewSearchRequestOptions(query, int(10), 0, true)
-	_, err = idx.Search(searchRequest)
+	res, err := idx.Search(searchRequest)
 	if err != nil {
 		t.Error(err)
 	}
 
 	stats, _ := idx.StatsMap()["index"].(map[string]interface{})
 	bytesRead, _ := stats["num_bytes_read_at_query_time"].(uint64)
-	if bytesRead != 15792 {
-		t.Fatalf("expected the bytes read stat to be around 15792, got %v", err)
+	if bytesRead != 25928 && bytesRead == res.BytesRead {
+		t.Fatalf("expected the bytes read stat to be around 25928, got %v", bytesRead)
 	}
 	prevBytesRead := bytesRead
 
 	searchRequest = NewSearchRequestOptions(query, int(10), 0, true)
-	_, err = idx.Search(searchRequest)
+	res, err = idx.Search(searchRequest)
 	if err != nil {
 		t.Error(err)
 	}
 	stats, _ = idx.StatsMap()["index"].(map[string]interface{})
 	bytesRead, _ = stats["num_bytes_read_at_query_time"].(uint64)
-	if bytesRead-prevBytesRead != 15 {
-		t.Fatalf("expected the bytes read stat to be around 15, got %v", err)
+	if bytesRead-prevBytesRead != 15 && bytesRead-prevBytesRead == res.BytesRead {
+		t.Fatalf("expected the bytes read stat to be around 15, got %v", bytesRead-prevBytesRead)
 	}
 	prevBytesRead = bytesRead
 
 	searchRequest = NewSearchRequestOptions(query, int(10), 0, true)
 	searchRequest.Fields = []string{"*"}
-	_, err = idx.Search(searchRequest)
+	res, err = idx.Search(searchRequest)
 	if err != nil {
 		t.Error(err)
 	}
@@ -605,8 +607,9 @@ func TestBytesReadStored(t *testing.T) {
 	stats, _ = idx.StatsMap()["index"].(map[string]interface{})
 	bytesRead, _ = stats["num_bytes_read_at_query_time"].(uint64)
 
-	if bytesRead-prevBytesRead != 38278 {
-		t.Fatalf("expected the bytes read stat to be around 38278, got %v", err)
+	if bytesRead-prevBytesRead != 26478 && bytesRead-prevBytesRead == res.BytesRead {
+		t.Fatalf("expected the bytes read stat to be around 26478, got %v",
+			bytesRead-prevBytesRead)
 	}
 	idx.Close()
 	cleanupTmpIndexPath(t, tmpIndexPath)
@@ -641,40 +644,39 @@ func TestBytesReadStored(t *testing.T) {
 	query = NewTermQuery("hotel")
 	query.FieldVal = "type"
 	searchRequest = NewSearchRequestOptions(query, int(10), 0, true)
-	_, err = idx1.Search(searchRequest)
+	res, err = idx1.Search(searchRequest)
 	if err != nil {
 		t.Error(err)
 	}
 
 	stats, _ = idx1.StatsMap()["index"].(map[string]interface{})
 	bytesRead, _ = stats["num_bytes_read_at_query_time"].(uint64)
-	if bytesRead != 167 {
-		t.Fatalf("expected the bytes read stat to be around 167, got %v", bytesRead-prevBytesRead)
+	if bytesRead != 18114 && bytesRead == res.BytesRead {
+		t.Fatalf("expected the bytes read stat to be around 18114, got %v", bytesRead)
 	}
 	prevBytesRead = bytesRead
 
-	_, err = idx1.Search(searchRequest)
+	res, err = idx1.Search(searchRequest)
 	if err != nil {
 		t.Error(err)
 	}
 	stats, _ = idx1.StatsMap()["index"].(map[string]interface{})
 	bytesRead, _ = stats["num_bytes_read_at_query_time"].(uint64)
-	if bytesRead-prevBytesRead != 12 {
-		t.Fatalf("expected the bytes read stat to be around 12, got %v", err)
+	if bytesRead-prevBytesRead != 12 && bytesRead-prevBytesRead == res.BytesRead {
+		t.Fatalf("expected the bytes read stat to be around 12, got %v", bytesRead-prevBytesRead)
 	}
 	prevBytesRead = bytesRead
 
 	searchRequest.Fields = []string{"*"}
-	_, err = idx1.Search(searchRequest)
+	res, err = idx1.Search(searchRequest)
 	if err != nil {
 		t.Error(err)
 	}
 
 	stats, _ = idx1.StatsMap()["index"].(map[string]interface{})
 	bytesRead, _ = stats["num_bytes_read_at_query_time"].(uint64)
-
-	if bytesRead-prevBytesRead != 646 {
-		t.Fatalf("expected the bytes read stat to be around 646, got %v", err)
+	if bytesRead-prevBytesRead != 42 && bytesRead-prevBytesRead == res.BytesRead {
+		t.Fatalf("expected the bytes read stat to be around 42, got %v", bytesRead-prevBytesRead)
 	}
 }
 
@@ -815,9 +817,9 @@ type slowQuery struct {
 	delay  time.Duration
 }
 
-func (s *slowQuery) Searcher(i index.IndexReader, m mapping.IndexMapping, options search.SearcherOptions) (search.Searcher, error) {
+func (s *slowQuery) Searcher(ctx context.Context, i index.IndexReader, m mapping.IndexMapping, options search.SearcherOptions) (search.Searcher, error) {
 	time.Sleep(s.delay)
-	return s.actual.Searcher(i, m, options)
+	return s.actual.Searcher(ctx, i, m, options)
 }
 
 func TestSlowSearch(t *testing.T) {
diff --git a/query_bench_test.go b/query_bench_test.go
new file mode 100644
index 00000000..22977095
--- /dev/null
+++ b/query_bench_test.go
@@ -0,0 +1,393 @@
+//  Copyright (c) 2022 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.
+
+package bleve
+
+import (
+	"strconv"
+	"testing"
+	"time"
+
+	"github.com/blevesearch/bleve/v2/analysis/analyzer/keyword"
+	"github.com/blevesearch/bleve/v2/mapping"
+)
+
+func BenchmarkQueryTerm(b *testing.B) {
+	tmpIndexPath := createTmpIndexPath(b)
+	defer cleanupTmpIndexPath(b, tmpIndexPath)
+
+	fm := mapping.NewTextFieldMapping()
+	fm.Analyzer = keyword.Name
+	dmap := mapping.NewDocumentMapping()
+	dmap.AddFieldMappingsAt("text", fm)
+	imap := mapping.NewIndexMapping()
+	imap.DefaultMapping = dmap
+
+	idx, err := New(tmpIndexPath, imap)
+	if err != nil {
+		b.Fatal(err)
+	}
+
+	defer func() {
+		err = idx.Close()
+		if err != nil {
+			b.Fatal(err)
+		}
+	}()
+
+	members := []string{"abc", "abcdef", "ghi", "jkl", "jklmno"}
+	for i := 0; i < 100; i++ {
+		if err = idx.Index(strconv.Itoa(i),
+			map[string]interface{}{"text": members[i%len(members)]}); err != nil {
+			b.Fatal(err)
+		}
+	}
+
+	b.ReportAllocs()
+	b.ResetTimer()
+
+	for i := 0; i < b.N; i++ {
+		q := NewTermQuery(members[i%len(members)])
+		q.SetField("text")
+		req := NewSearchRequest(q)
+		if _, err = idx.Search(req); err != nil {
+			b.Fatal(err)
+		}
+	}
+}
+
+func BenchmarkQueryTermRange(b *testing.B) {
+	tmpIndexPath := createTmpIndexPath(b)
+	defer cleanupTmpIndexPath(b, tmpIndexPath)
+
+	fm := mapping.NewTextFieldMapping()
+	fm.Analyzer = keyword.Name
+	dmap := mapping.NewDocumentMapping()
+	dmap.AddFieldMappingsAt("text", fm)
+	imap := mapping.NewIndexMapping()
+	imap.DefaultMapping = dmap
+
+	idx, err := New(tmpIndexPath, imap)
+	if err != nil {
+		b.Fatal(err)
+	}
+
+	defer func() {
+		err = idx.Close()
+		if err != nil {
+			b.Fatal(err)
+		}
+	}()
+
+	members := []string{"abc", "abcdef", "ghi", "jkl", "jklmno"}
+	for i := 0; i < 100; i++ {
+		if err = idx.Index(strconv.Itoa(i),
+			map[string]interface{}{"text": members[i%len(members)]}); err != nil {
+			b.Fatal(err)
+		}
+	}
+
+	b.ReportAllocs()
+	b.ResetTimer()
+
+	inclusive := true
+	for i := 0; i < b.N; i++ {
+		q := NewTermRangeInclusiveQuery(
+			members[i%(len(members)-2)],
+			members[(i+2)%(len(members)-2)],
+			&inclusive,
+			&inclusive,
+		)
+		q.SetField("text")
+		req := NewSearchRequest(q)
+		if _, err = idx.Search(req); err != nil {
+			b.Fatal(err)
+		}
+	}
+}
+
+func BenchmarkQueryWildcard(b *testing.B) {
+	tmpIndexPath := createTmpIndexPath(b)
+	defer cleanupTmpIndexPath(b, tmpIndexPath)
+
+	fm := mapping.NewTextFieldMapping()
+	fm.Analyzer = keyword.Name
+	dmap := mapping.NewDocumentMapping()
+	dmap.AddFieldMappingsAt("text", fm)
+	imap := mapping.NewIndexMapping()
+	imap.DefaultMapping = dmap
+
+	idx, err := New(tmpIndexPath, imap)
+	if err != nil {
+		b.Fatal(err)
+	}
+
+	defer func() {
+		err = idx.Close()
+		if err != nil {
+			b.Fatal(err)
+		}
+	}()
+
+	members := []string{"abc", "abcdef", "ghi", "jkl", "jklmno"}
+	for i := 0; i < 100; i++ {
+		if err = idx.Index(strconv.Itoa(i),
+			map[string]interface{}{"text": members[i%len(members)]}); err != nil {
+			b.Fatal(err)
+		}
+	}
+
+	wildcards := []string{"ab*", "jk*"}
+
+	b.ReportAllocs()
+	b.ResetTimer()
+
+	for i := 0; i < b.N; i++ {
+		q := NewWildcardQuery(wildcards[i%len(wildcards)])
+		q.SetField("text")
+		req := NewSearchRequest(q)
+		if _, err = idx.Search(req); err != nil {
+			b.Fatal(err)
+		}
+	}
+}
+
+func BenchmarkQueryNumericRange(b *testing.B) {
+	tmpIndexPath := createTmpIndexPath(b)
+	defer cleanupTmpIndexPath(b, tmpIndexPath)
+
+	fm := mapping.NewNumericFieldMapping()
+	dmap := mapping.NewDocumentMapping()
+	dmap.AddFieldMappingsAt("number", fm)
+	imap := mapping.NewIndexMapping()
+	imap.DefaultMapping = dmap
+
+	idx, err := New(tmpIndexPath, imap)
+	if err != nil {
+		b.Fatal(err)
+	}
+
+	defer func() {
+		err = idx.Close()
+		if err != nil {
+			b.Fatal(err)
+		}
+	}()
+
+	for i := 0; i < 100; i++ {
+		if err = idx.Index(strconv.Itoa(i),
+			map[string]interface{}{"number": i}); err != nil {
+			b.Fatal(err)
+		}
+	}
+
+	b.ReportAllocs()
+	b.ResetTimer()
+
+	inclusive := true
+	for i := 0; i < b.N; i++ {
+		start := float64(i % 90)
+		end := float64((i + 10) % 90)
+		q := NewNumericRangeInclusiveQuery(&start, &end, &inclusive, &inclusive)
+		q.SetField("number")
+		req := NewSearchRequest(q)
+		if _, err = idx.Search(req); err != nil {
+			b.Fatal(err)
+		}
+	}
+}
+
+func BenchmarkQueryDateRange(b *testing.B) {
+	tmpIndexPath := createTmpIndexPath(b)
+	defer cleanupTmpIndexPath(b, tmpIndexPath)
+
+	fm := mapping.NewDateTimeFieldMapping()
+	dmap := mapping.NewDocumentMapping()
+	dmap.AddFieldMappingsAt("date", fm)
+	imap := mapping.NewIndexMapping()
+	imap.DefaultMapping = dmap
+
+	idx, err := New(tmpIndexPath, imap)
+	if err != nil {
+		b.Fatal(err)
+	}
+
+	defer func() {
+		err = idx.Close()
+		if err != nil {
+			b.Fatal(err)
+		}
+	}()
+
+	members := []string{
+		"2022-11-16T18:45:45Z",
+		"2022-11-17T18:45:45Z",
+		"2022-11-18T18:45:45Z",
+		"2022-11-19T18:45:45Z",
+		"2022-11-20T18:45:45Z",
+	}
+	for i := 0; i < 100; i++ {
+		if err = idx.Index(strconv.Itoa(i),
+			map[string]interface{}{"date": members[i%len(members)]}); err != nil {
+			b.Fatal(err)
+		}
+	}
+
+	b.ReportAllocs()
+	b.ResetTimer()
+
+	inclusive := true
+	for i := 0; i < b.N; i++ {
+		start, _ := time.Parse("2006-01-02T12:00:00Z", members[i%(len(members)-2)])
+		end, _ := time.Parse("2006-01-02T12:00:00Z", members[(i+2)%(len(members)-2)])
+		q := NewDateRangeInclusiveQuery(start, end, &inclusive, &inclusive)
+		q.SetField("date")
+		req := NewSearchRequest(q)
+		if _, err = idx.Search(req); err != nil {
+			b.Fatal(err)
+		}
+	}
+}
+
+func BenchmarkQueryGeoDistance(b *testing.B) {
+	tmpIndexPath := createTmpIndexPath(b)
+	defer cleanupTmpIndexPath(b, tmpIndexPath)
+
+	fm := mapping.NewGeoPointFieldMapping()
+	dmap := mapping.NewDocumentMapping()
+	dmap.AddFieldMappingsAt("geo", fm)
+	imap := mapping.NewIndexMapping()
+	imap.DefaultMapping = dmap
+
+	idx, err := New(tmpIndexPath, imap)
+	if err != nil {
+		b.Fatal(err)
+	}
+
+	defer func() {
+		err = idx.Close()
+		if err != nil {
+			b.Fatal(err)
+		}
+	}()
+
+	members := [][]float64{
+		{-121.96713072883645, 37.380331474621045},
+		{-97.75518866579938, 30.38974491308761},
+		{-0.08653451918110022, 51.51063984942306},
+		{-2.230759791360498, 53.481514330841236},
+		{77.59542326042589, 12.97215865921956},
+	}
+	for i := 0; i < 100; i++ {
+		if err = idx.Index(strconv.Itoa(i),
+			map[string]interface{}{"geo": members[i%len(members)]}); err != nil {
+			b.Fatal(err)
+		}
+	}
+
+	b.ReportAllocs()
+	b.ResetTimer()
+
+	for i := 0; i < b.N; i++ {
+		coordinates := members[i%len(members)]
+		q := NewGeoDistanceQuery(coordinates[0], coordinates[1], "1mi")
+		q.SetField("geo")
+		req := NewSearchRequest(q)
+		if _, err = idx.Search(req); err != nil {
+			b.Fatal(err)
+		}
+	}
+}
+
+func BenchmarkQueryGeoBoundingBox(b *testing.B) {
+	tmpIndexPath := createTmpIndexPath(b)
+	defer cleanupTmpIndexPath(b, tmpIndexPath)
+
+	fm := mapping.NewGeoPointFieldMapping()
+	dmap := mapping.NewDocumentMapping()
+	dmap.AddFieldMappingsAt("geo", fm)
+	imap := mapping.NewIndexMapping()
+	imap.DefaultMapping = dmap
+
+	idx, err := New(tmpIndexPath, imap)
+	if err != nil {
+		b.Fatal(err)
+	}
+
+	defer func() {
+		err = idx.Close()
+		if err != nil {
+			b.Fatal(err)
+		}
+	}()
+
+	members := [][]float64{
+		{-121.96713072883645, 37.380331474621045},
+		{-97.75518866579938, 30.38974491308761},
+		{-0.08653451918110022, 51.51063984942306},
+		{-2.230759791360498, 53.481514330841236},
+		{77.59542326042589, 12.97215865921956},
+	}
+	for i := 0; i < 100; i++ {
+		if err = idx.Index(strconv.Itoa(i),
+			map[string]interface{}{"geo": members[i%len(members)]}); err != nil {
+			b.Fatal(err)
+		}
+	}
+
+	boundingBoxes := []struct {
+		topLeft     []float64
+		bottomRight []float64
+	}{
+		{
+			topLeft:     []float64{-122.14424992609722, 37.49751487670511},
+			bottomRight: []float64{-121.78076546622579, 37.26963069737202},
+		},
+		{
+			topLeft:     []float64{-97.85362236226437, 30.473743975245725},
+			bottomRight: []float64{-97.58691085968482, 30.285211697102895},
+		},
+		{
+			topLeft:     []float64{-0.28538822102223094, 51.61106497119687},
+			bottomRight: []float64{0.16776748108466677, 51.395702237541286},
+		},
+		{
+			topLeft:     []float64{-2.373683904907921, 53.54371945714075},
+			bottomRight: []float64{-2.134365533113197, 53.41788831720595},
+		},
+		{
+			topLeft:     []float64{77.52617635172015, 13.037587208986437},
+			bottomRight: []float64{77.66508989028102, 12.924426170584738},
+		},
+	}
+
+	b.ReportAllocs()
+	b.ResetTimer()
+
+	for i := 0; i < b.N; i++ {
+		topLeftCoordinates := boundingBoxes[i%len(boundingBoxes)].topLeft
+		bottomRightCoordinates := boundingBoxes[i%len(boundingBoxes)].bottomRight
+		q := NewGeoBoundingBoxQuery(
+			topLeftCoordinates[0],
+			topLeftCoordinates[1],
+			bottomRightCoordinates[0],
+			bottomRightCoordinates[1],
+		)
+		q.SetField("geo")
+		req := NewSearchRequest(q)
+		if _, err = idx.Search(req); err != nil {
+			b.Fatal(err)
+		}
+	}
+}
diff --git a/search/collector/search_test.go b/search/collector/search_test.go
index 0d7cd09b..1f6f8821 100644
--- a/search/collector/search_test.go
+++ b/search/collector/search_test.go
@@ -15,6 +15,7 @@
 package collector
 
 import (
+	"context"
 	"reflect"
 
 	"github.com/blevesearch/bleve/v2/search"
@@ -26,6 +27,14 @@ type stubSearcher struct {
 	matches []*search.DocumentMatch
 }
 
+func (ss *stubSearcher) SetBytesRead(val uint64) {
+
+}
+
+func (ss *stubSearcher) BytesRead() uint64 {
+	return 0
+}
+
 func (ss *stubSearcher) Size() int {
 	sizeInBytes := int(reflect.TypeOf(*ss).Size())
 
@@ -93,7 +102,7 @@ func (sr *stubReader) Size() int {
 	return 0
 }
 
-func (sr *stubReader) TermFieldReader(term []byte, field string, includeFreq, includeNorm, includeTermVectors bool) (index.TermFieldReader, error) {
+func (sr *stubReader) TermFieldReader(ctx context.Context, term []byte, field string, includeFreq, includeNorm, includeTermVectors bool) (index.TermFieldReader, error) {
 	return nil, nil
 }
 
@@ -173,3 +182,6 @@ type DocValueReader struct {
 func (dvr *DocValueReader) VisitDocValues(id index.IndexInternalID, visitor index.DocValueVisitor) error {
 	return dvr.i.DocumentVisitFieldTerms(id, dvr.fields, visitor)
 }
+func (dvr *DocValueReader) BytesRead() uint64 {
+	return 0
+}
diff --git a/search/query/query_test.go b/search/query/query_test.go
index 1794f16d..0082e8ac 100644
--- a/search/query/query_test.go
+++ b/search/query/query_test.go
@@ -205,6 +205,14 @@ func TestParseQuery(t *testing.T) {
 			input:  []byte(`{"bool": true}`),
 			output: NewBoolFieldQuery(true),
 		},
+		{
+			input: []byte(`{"field": "x", "cidr": "1.2.3.0/4"}`),
+			output: func() Query {
+				q := NewIPRangeQuery("1.2.3.0/4")
+				q.SetField("x")
+				return q
+			}(),
+		},
 		{
 			input:  []byte(`{"madeitup":"queryhere"}`),
 			output: nil,
diff --git a/search/searcher/base_test.go b/search/searcher/base_test.go
index a028db39..6f80bf65 100644
--- a/search/searcher/base_test.go
+++ b/search/searcher/base_test.go
@@ -77,7 +77,7 @@ func initTwoDocs(twoDocIndex index.Index) {
 }
 
 // create a simpler analyzer which will support these tests
-var testAnalyzer = &analysis.Analyzer{
+var testAnalyzer = &analysis.DefaultAnalyzer{
 	Tokenizer: regexpTokenizer.NewRegexpTokenizer(regexp.MustCompile(`\w+`)),
 }
 
diff --git a/search/searcher/search_boolean_test.go b/search/searcher/search_boolean_test.go
index d5f4875b..f1a748f0 100644
--- a/search/searcher/search_boolean_test.go
+++ b/search/searcher/search_boolean_test.go
@@ -40,210 +40,210 @@ func TestBooleanSearch(t *testing.T) {
 	explainTrue := search.SearcherOptions{Explain: true}
 
 	// test 0
-	beerTermSearcher, err := NewTermSearcher(twoDocIndexReader, "beer", "desc", 1.0, explainTrue)
+	beerTermSearcher, err := NewTermSearcher(nil, twoDocIndexReader, "beer", "desc", 1.0, explainTrue)
 	if err != nil {
 		t.Fatal(err)
 	}
-	mustSearcher, err := NewConjunctionSearcher(twoDocIndexReader, []search.Searcher{beerTermSearcher}, explainTrue)
+	mustSearcher, err := NewConjunctionSearcher(nil, twoDocIndexReader, []search.Searcher{beerTermSearcher}, explainTrue)
 	if err != nil {
 		t.Fatal(err)
 	}
-	martyTermSearcher, err := NewTermSearcher(twoDocIndexReader, "marty", "name", 1.0, explainTrue)
+	martyTermSearcher, err := NewTermSearcher(nil, twoDocIndexReader, "marty", "name", 1.0, explainTrue)
 	if err != nil {
 		t.Fatal(err)
 	}
-	dustinTermSearcher, err := NewTermSearcher(twoDocIndexReader, "dustin", "name", 1.0, explainTrue)
+	dustinTermSearcher, err := NewTermSearcher(nil, twoDocIndexReader, "dustin", "name", 1.0, explainTrue)
 	if err != nil {
 		t.Fatal(err)
 	}
-	shouldSearcher, err := NewDisjunctionSearcher(twoDocIndexReader, []search.Searcher{martyTermSearcher, dustinTermSearcher}, 0, explainTrue)
+	shouldSearcher, err := NewDisjunctionSearcher(nil, twoDocIndexReader, []search.Searcher{martyTermSearcher, dustinTermSearcher}, 0, explainTrue)
 	if err != nil {
 		t.Fatal(err)
 	}
-	steveTermSearcher, err := NewTermSearcher(twoDocIndexReader, "steve", "name", 1.0, explainTrue)
+	steveTermSearcher, err := NewTermSearcher(nil, twoDocIndexReader, "steve", "name", 1.0, explainTrue)
 	if err != nil {
 		t.Fatal(err)
 	}
-	mustNotSearcher, err := NewDisjunctionSearcher(twoDocIndexReader, []search.Searcher{steveTermSearcher}, 0, explainTrue)
+	mustNotSearcher, err := NewDisjunctionSearcher(nil, twoDocIndexReader, []search.Searcher{steveTermSearcher}, 0, explainTrue)
 	if err != nil {
 		t.Fatal(err)
 	}
-	booleanSearcher, err := NewBooleanSearcher(twoDocIndexReader, mustSearcher, shouldSearcher, mustNotSearcher, explainTrue)
+	booleanSearcher, err := NewBooleanSearcher(nil, twoDocIndexReader, mustSearcher, shouldSearcher, mustNotSearcher, explainTrue)
 	if err != nil {
 		t.Fatal(err)
 	}
 
 	// test 1
-	martyTermSearcher2, err := NewTermSearcher(twoDocIndexReader, "marty", "name", 1.0, explainTrue)
+	martyTermSearcher2, err := NewTermSearcher(nil, twoDocIndexReader, "marty", "name", 1.0, explainTrue)
 	if err != nil {
 		t.Fatal(err)
 	}
-	dustinTermSearcher2, err := NewTermSearcher(twoDocIndexReader, "dustin", "name", 1.0, explainTrue)
+	dustinTermSearcher2, err := NewTermSearcher(nil, twoDocIndexReader, "dustin", "name", 1.0, explainTrue)
 	if err != nil {
 		t.Fatal(err)
 	}
-	shouldSearcher2, err := NewDisjunctionSearcher(twoDocIndexReader, []search.Searcher{martyTermSearcher2, dustinTermSearcher2}, 0, explainTrue)
+	shouldSearcher2, err := NewDisjunctionSearcher(nil, twoDocIndexReader, []search.Searcher{martyTermSearcher2, dustinTermSearcher2}, 0, explainTrue)
 	if err != nil {
 		t.Fatal(err)
 	}
-	steveTermSearcher2, err := NewTermSearcher(twoDocIndexReader, "steve", "name", 1.0, explainTrue)
+	steveTermSearcher2, err := NewTermSearcher(nil, twoDocIndexReader, "steve", "name", 1.0, explainTrue)
 	if err != nil {
 		t.Fatal(err)
 	}
-	mustNotSearcher2, err := NewDisjunctionSearcher(twoDocIndexReader, []search.Searcher{steveTermSearcher2}, 0, explainTrue)
+	mustNotSearcher2, err := NewDisjunctionSearcher(nil, twoDocIndexReader, []search.Searcher{steveTermSearcher2}, 0, explainTrue)
 	if err != nil {
 		t.Fatal(err)
 	}
-	booleanSearcher2, err := NewBooleanSearcher(twoDocIndexReader, nil, shouldSearcher2, mustNotSearcher2, explainTrue)
+	booleanSearcher2, err := NewBooleanSearcher(nil, twoDocIndexReader, nil, shouldSearcher2, mustNotSearcher2, explainTrue)
 	if err != nil {
 		t.Fatal(err)
 	}
 
 	// test 2
-	steveTermSearcher3, err := NewTermSearcher(twoDocIndexReader, "steve", "name", 1.0, explainTrue)
+	steveTermSearcher3, err := NewTermSearcher(nil, twoDocIndexReader, "steve", "name", 1.0, explainTrue)
 	if err != nil {
 		t.Fatal(err)
 	}
-	mustNotSearcher3, err := NewDisjunctionSearcher(twoDocIndexReader, []search.Searcher{steveTermSearcher3}, 0, explainTrue)
+	mustNotSearcher3, err := NewDisjunctionSearcher(nil, twoDocIndexReader, []search.Searcher{steveTermSearcher3}, 0, explainTrue)
 	if err != nil {
 		t.Fatal(err)
 	}
-	booleanSearcher3, err := NewBooleanSearcher(twoDocIndexReader, nil, nil, mustNotSearcher3, explainTrue)
+	booleanSearcher3, err := NewBooleanSearcher(nil, twoDocIndexReader, nil, nil, mustNotSearcher3, explainTrue)
 	if err != nil {
 		t.Fatal(err)
 	}
 
 	// test 3
-	beerTermSearcher4, err := NewTermSearcher(twoDocIndexReader, "beer", "desc", 1.0, explainTrue)
+	beerTermSearcher4, err := NewTermSearcher(nil, twoDocIndexReader, "beer", "desc", 1.0, explainTrue)
 	if err != nil {
 		t.Fatal(err)
 	}
-	mustSearcher4, err := NewConjunctionSearcher(twoDocIndexReader, []search.Searcher{beerTermSearcher4}, explainTrue)
+	mustSearcher4, err := NewConjunctionSearcher(nil, twoDocIndexReader, []search.Searcher{beerTermSearcher4}, explainTrue)
 	if err != nil {
 		t.Fatal(err)
 	}
-	steveTermSearcher4, err := NewTermSearcher(twoDocIndexReader, "steve", "name", 1.0, explainTrue)
+	steveTermSearcher4, err := NewTermSearcher(nil, twoDocIndexReader, "steve", "name", 1.0, explainTrue)
 	if err != nil {
 		t.Fatal(err)
 	}
-	mustNotSearcher4, err := NewDisjunctionSearcher(twoDocIndexReader, []search.Searcher{steveTermSearcher4}, 0, explainTrue)
+	mustNotSearcher4, err := NewDisjunctionSearcher(nil, twoDocIndexReader, []search.Searcher{steveTermSearcher4}, 0, explainTrue)
 	if err != nil {
 		t.Fatal(err)
 	}
-	booleanSearcher4, err := NewBooleanSearcher(twoDocIndexReader, mustSearcher4, nil, mustNotSearcher4, explainTrue)
+	booleanSearcher4, err := NewBooleanSearcher(nil, twoDocIndexReader, mustSearcher4, nil, mustNotSearcher4, explainTrue)
 	if err != nil {
 		t.Fatal(err)
 	}
 
 	// test 4
-	beerTermSearcher5, err := NewTermSearcher(twoDocIndexReader, "beer", "desc", 1.0, explainTrue)
+	beerTermSearcher5, err := NewTermSearcher(nil, twoDocIndexReader, "beer", "desc", 1.0, explainTrue)
 	if err != nil {
 		t.Fatal(err)
 	}
-	mustSearcher5, err := NewConjunctionSearcher(twoDocIndexReader, []search.Searcher{beerTermSearcher5}, explainTrue)
+	mustSearcher5, err := NewConjunctionSearcher(nil, twoDocIndexReader, []search.Searcher{beerTermSearcher5}, explainTrue)
 	if err != nil {
 		t.Fatal(err)
 	}
-	steveTermSearcher5, err := NewTermSearcher(twoDocIndexReader, "steve", "name", 1.0, explainTrue)
+	steveTermSearcher5, err := NewTermSearcher(nil, twoDocIndexReader, "steve", "name", 1.0, explainTrue)
 	if err != nil {
 		t.Fatal(err)
 	}
-	martyTermSearcher5, err := NewTermSearcher(twoDocIndexReader, "marty", "name", 1.0, explainTrue)
+	martyTermSearcher5, err := NewTermSearcher(nil, twoDocIndexReader, "marty", "name", 1.0, explainTrue)
 	if err != nil {
 		t.Fatal(err)
 	}
-	mustNotSearcher5, err := NewDisjunctionSearcher(twoDocIndexReader, []search.Searcher{steveTermSearcher5, martyTermSearcher5}, 0, explainTrue)
+	mustNotSearcher5, err := NewDisjunctionSearcher(nil, twoDocIndexReader, []search.Searcher{steveTermSearcher5, martyTermSearcher5}, 0, explainTrue)
 	if err != nil {
 		t.Fatal(err)
 	}
-	booleanSearcher5, err := NewBooleanSearcher(twoDocIndexReader, mustSearcher5, nil, mustNotSearcher5, explainTrue)
+	booleanSearcher5, err := NewBooleanSearcher(nil, twoDocIndexReader, mustSearcher5, nil, mustNotSearcher5, explainTrue)
 	if err != nil {
 		t.Fatal(err)
 	}
 
 	// test 5
-	beerTermSearcher6, err := NewTermSearcher(twoDocIndexReader, "beer", "desc", 1.0, explainTrue)
+	beerTermSearcher6, err := NewTermSearcher(nil, twoDocIndexReader, "beer", "desc", 1.0, explainTrue)
 	if err != nil {
 		t.Fatal(err)
 	}
-	mustSearcher6, err := NewConjunctionSearcher(twoDocIndexReader, []search.Searcher{beerTermSearcher6}, explainTrue)
+	mustSearcher6, err := NewConjunctionSearcher(nil, twoDocIndexReader, []search.Searcher{beerTermSearcher6}, explainTrue)
 	if err != nil {
 		t.Fatal(err)
 	}
-	martyTermSearcher6, err := NewTermSearcher(twoDocIndexReader, "marty", "name", 1.0, explainTrue)
+	martyTermSearcher6, err := NewTermSearcher(nil, twoDocIndexReader, "marty", "name", 1.0, explainTrue)
 	if err != nil {
 		t.Fatal(err)
 	}
-	dustinTermSearcher6, err := NewTermSearcher(twoDocIndexReader, "dustin", "name", 1.0, explainTrue)
+	dustinTermSearcher6, err := NewTermSearcher(nil, twoDocIndexReader, "dustin", "name", 1.0, explainTrue)
 	if err != nil {
 		t.Fatal(err)
 	}
-	shouldSearcher6, err := NewDisjunctionSearcher(twoDocIndexReader, []search.Searcher{martyTermSearcher6, dustinTermSearcher6}, 2, explainTrue)
+	shouldSearcher6, err := NewDisjunctionSearcher(nil, twoDocIndexReader, []search.Searcher{martyTermSearcher6, dustinTermSearcher6}, 2, explainTrue)
 	if err != nil {
 		t.Fatal(err)
 	}
-	booleanSearcher6, err := NewBooleanSearcher(twoDocIndexReader, mustSearcher6, shouldSearcher6, nil, explainTrue)
+	booleanSearcher6, err := NewBooleanSearcher(nil, twoDocIndexReader, mustSearcher6, shouldSearcher6, nil, explainTrue)
 	if err != nil {
 		t.Fatal(err)
 	}
 
 	// test 6
-	beerTermSearcher7, err := NewTermSearcher(twoDocIndexReader, "beer", "desc", 1.0, explainTrue)
+	beerTermSearcher7, err := NewTermSearcher(nil, twoDocIndexReader, "beer", "desc", 1.0, explainTrue)
 	if err != nil {
 		t.Fatal(err)
 	}
-	mustSearcher7, err := NewConjunctionSearcher(twoDocIndexReader, []search.Searcher{beerTermSearcher7}, explainTrue)
+	mustSearcher7, err := NewConjunctionSearcher(nil, twoDocIndexReader, []search.Searcher{beerTermSearcher7}, explainTrue)
 	if err != nil {
 		t.Fatal(err)
 	}
-	booleanSearcher7, err := NewBooleanSearcher(twoDocIndexReader, mustSearcher7, nil, nil, explainTrue)
+	booleanSearcher7, err := NewBooleanSearcher(nil, twoDocIndexReader, mustSearcher7, nil, nil, explainTrue)
 	if err != nil {
 		t.Fatal(err)
 	}
-	martyTermSearcher7, err := NewTermSearcher(twoDocIndexReader, "marty", "name", 5.0, explainTrue)
+	martyTermSearcher7, err := NewTermSearcher(nil, twoDocIndexReader, "marty", "name", 5.0, explainTrue)
 	if err != nil {
 		t.Fatal(err)
 	}
-	conjunctionSearcher7, err := NewConjunctionSearcher(twoDocIndexReader, []search.Searcher{martyTermSearcher7, booleanSearcher7}, explainTrue)
+	conjunctionSearcher7, err := NewConjunctionSearcher(nil, twoDocIndexReader, []search.Searcher{martyTermSearcher7, booleanSearcher7}, explainTrue)
 
 	// test 7
-	beerTermSearcher8, err := NewTermSearcher(twoDocIndexReader, "beer", "desc", 1.0, explainTrue)
+	beerTermSearcher8, err := NewTermSearcher(nil, twoDocIndexReader, "beer", "desc", 1.0, explainTrue)
 	if err != nil {
 		t.Fatal(err)
 	}
-	mustSearcher8, err := NewConjunctionSearcher(twoDocIndexReader, []search.Searcher{beerTermSearcher8}, explainTrue)
+	mustSearcher8, err := NewConjunctionSearcher(nil, twoDocIndexReader, []search.Searcher{beerTermSearcher8}, explainTrue)
 	if err != nil {
 		t.Fatal(err)
 	}
-	martyTermSearcher8, err := NewTermSearcher(twoDocIndexReader, "marty", "name", 1.0, explainTrue)
+	martyTermSearcher8, err := NewTermSearcher(nil, twoDocIndexReader, "marty", "name", 1.0, explainTrue)
 	if err != nil {
 		t.Fatal(err)
 	}
-	dustinTermSearcher8, err := NewTermSearcher(twoDocIndexReader, "dustin", "name", 1.0, explainTrue)
+	dustinTermSearcher8, err := NewTermSearcher(nil, twoDocIndexReader, "dustin", "name", 1.0, explainTrue)
 	if err != nil {
 		t.Fatal(err)
 	}
-	shouldSearcher8, err := NewDisjunctionSearcher(twoDocIndexReader, []search.Searcher{martyTermSearcher8, dustinTermSearcher8}, 0, explainTrue)
+	shouldSearcher8, err := NewDisjunctionSearcher(nil, twoDocIndexReader, []search.Searcher{martyTermSearcher8, dustinTermSearcher8}, 0, explainTrue)
 	if err != nil {
 		t.Fatal(err)
 	}
-	steveTermSearcher8, err := NewTermSearcher(twoDocIndexReader, "steve", "name", 1.0, explainTrue)
+	steveTermSearcher8, err := NewTermSearcher(nil, twoDocIndexReader, "steve", "name", 1.0, explainTrue)
 	if err != nil {
 		t.Fatal(err)
 	}
-	mustNotSearcher8, err := NewDisjunctionSearcher(twoDocIndexReader, []search.Searcher{steveTermSearcher8}, 0, explainTrue)
+	mustNotSearcher8, err := NewDisjunctionSearcher(nil, twoDocIndexReader, []search.Searcher{steveTermSearcher8}, 0, explainTrue)
 	if err != nil {
 		t.Fatal(err)
 	}
-	booleanSearcher8, err := NewBooleanSearcher(twoDocIndexReader, mustSearcher8, shouldSearcher8, mustNotSearcher8, explainTrue)
+	booleanSearcher8, err := NewBooleanSearcher(nil, twoDocIndexReader, mustSearcher8, shouldSearcher8, mustNotSearcher8, explainTrue)
 	if err != nil {
 		t.Fatal(err)
 	}
-	dustinTermSearcher8a, err := NewTermSearcher(twoDocIndexReader, "dustin", "name", 5.0, explainTrue)
+	dustinTermSearcher8a, err := NewTermSearcher(nil, twoDocIndexReader, "dustin", "name", 5.0, explainTrue)
 	if err != nil {
 		t.Fatal(err)
 	}
-	conjunctionSearcher8, err := NewConjunctionSearcher(twoDocIndexReader, []search.Searcher{booleanSearcher8, dustinTermSearcher8a}, explainTrue)
+	conjunctionSearcher8, err := NewConjunctionSearcher(nil, twoDocIndexReader, []search.Searcher{booleanSearcher8, dustinTermSearcher8a}, explainTrue)
 	if err != nil {
 		t.Fatal(err)
 	}
diff --git a/search/searcher/search_conjunction_test.go b/search/searcher/search_conjunction_test.go
index 747b3a01..d09c4efa 100644
--- a/search/searcher/search_conjunction_test.go
+++ b/search/searcher/search_conjunction_test.go
@@ -40,93 +40,93 @@ func TestConjunctionSearch(t *testing.T) {
 	explainTrue := search.SearcherOptions{Explain: true}
 
 	// test 0
-	beerTermSearcher, err := NewTermSearcher(twoDocIndexReader, "beer", "desc", 1.0, explainTrue)
+	beerTermSearcher, err := NewTermSearcher(nil, twoDocIndexReader, "beer", "desc", 1.0, explainTrue)
 	if err != nil {
 		t.Fatal(err)
 	}
-	martyTermSearcher, err := NewTermSearcher(twoDocIndexReader, "marty", "name", 5.0, explainTrue)
+	martyTermSearcher, err := NewTermSearcher(nil, twoDocIndexReader, "marty", "name", 5.0, explainTrue)
 	if err != nil {
 		t.Fatal(err)
 	}
-	beerAndMartySearcher, err := NewConjunctionSearcher(twoDocIndexReader, []search.Searcher{beerTermSearcher, martyTermSearcher}, explainTrue)
+	beerAndMartySearcher, err := NewConjunctionSearcher(nil, twoDocIndexReader, []search.Searcher{beerTermSearcher, martyTermSearcher}, explainTrue)
 	if err != nil {
 		t.Fatal(err)
 	}
 
 	// test 1
-	angstTermSearcher, err := NewTermSearcher(twoDocIndexReader, "angst", "desc", 1.0, explainTrue)
+	angstTermSearcher, err := NewTermSearcher(nil, twoDocIndexReader, "angst", "desc", 1.0, explainTrue)
 	if err != nil {
 		t.Fatal(err)
 	}
-	beerTermSearcher2, err := NewTermSearcher(twoDocIndexReader, "beer", "desc", 1.0, explainTrue)
+	beerTermSearcher2, err := NewTermSearcher(nil, twoDocIndexReader, "beer", "desc", 1.0, explainTrue)
 	if err != nil {
 		t.Fatal(err)
 	}
-	angstAndBeerSearcher, err := NewConjunctionSearcher(twoDocIndexReader, []search.Searcher{angstTermSearcher, beerTermSearcher2}, explainTrue)
+	angstAndBeerSearcher, err := NewConjunctionSearcher(nil, twoDocIndexReader, []search.Searcher{angstTermSearcher, beerTermSearcher2}, explainTrue)
 	if err != nil {
 		t.Fatal(err)
 	}
 
 	// test 2
-	beerTermSearcher3, err := NewTermSearcher(twoDocIndexReader, "beer", "desc", 1.0, explainTrue)
+	beerTermSearcher3, err := NewTermSearcher(nil, twoDocIndexReader, "beer", "desc", 1.0, explainTrue)
 	if err != nil {
 		t.Fatal(err)
 	}
-	jackTermSearcher, err := NewTermSearcher(twoDocIndexReader, "jack", "name", 5.0, explainTrue)
+	jackTermSearcher, err := NewTermSearcher(nil, twoDocIndexReader, "jack", "name", 5.0, explainTrue)
 	if err != nil {
 		t.Fatal(err)
 	}
-	beerAndJackSearcher, err := NewConjunctionSearcher(twoDocIndexReader, []search.Searcher{beerTermSearcher3, jackTermSearcher}, explainTrue)
+	beerAndJackSearcher, err := NewConjunctionSearcher(nil, twoDocIndexReader, []search.Searcher{beerTermSearcher3, jackTermSearcher}, explainTrue)
 	if err != nil {
 		t.Fatal(err)
 	}
 
 	// test 3
-	beerTermSearcher4, err := NewTermSearcher(twoDocIndexReader, "beer", "desc", 1.0, explainTrue)
+	beerTermSearcher4, err := NewTermSearcher(nil, twoDocIndexReader, "beer", "desc", 1.0, explainTrue)
 	if err != nil {
 		t.Fatal(err)
 	}
-	misterTermSearcher, err := NewTermSearcher(twoDocIndexReader, "mister", "title", 5.0, explainTrue)
+	misterTermSearcher, err := NewTermSearcher(nil, twoDocIndexReader, "mister", "title", 5.0, explainTrue)
 	if err != nil {
 		t.Fatal(err)
 	}
-	beerAndMisterSearcher, err := NewConjunctionSearcher(twoDocIndexReader, []search.Searcher{beerTermSearcher4, misterTermSearcher}, explainTrue)
+	beerAndMisterSearcher, err := NewConjunctionSearcher(nil, twoDocIndexReader, []search.Searcher{beerTermSearcher4, misterTermSearcher}, explainTrue)
 	if err != nil {
 		t.Fatal(err)
 	}
 
 	// test 4
-	couchbaseTermSearcher, err := NewTermSearcher(twoDocIndexReader, "couchbase", "street", 1.0, explainTrue)
+	couchbaseTermSearcher, err := NewTermSearcher(nil, twoDocIndexReader, "couchbase", "street", 1.0, explainTrue)
 	if err != nil {
 		t.Fatal(err)
 	}
-	misterTermSearcher2, err := NewTermSearcher(twoDocIndexReader, "mister", "title", 5.0, explainTrue)
+	misterTermSearcher2, err := NewTermSearcher(nil, twoDocIndexReader, "mister", "title", 5.0, explainTrue)
 	if err != nil {
 		t.Fatal(err)
 	}
-	couchbaseAndMisterSearcher, err := NewConjunctionSearcher(twoDocIndexReader, []search.Searcher{couchbaseTermSearcher, misterTermSearcher2}, explainTrue)
+	couchbaseAndMisterSearcher, err := NewConjunctionSearcher(nil, twoDocIndexReader, []search.Searcher{couchbaseTermSearcher, misterTermSearcher2}, explainTrue)
 	if err != nil {
 		t.Fatal(err)
 	}
 
 	// test 5
-	beerTermSearcher5, err := NewTermSearcher(twoDocIndexReader, "beer", "desc", 5.0, explainTrue)
+	beerTermSearcher5, err := NewTermSearcher(nil, twoDocIndexReader, "beer", "desc", 5.0, explainTrue)
 	if err != nil {
 		t.Fatal(err)
 	}
-	couchbaseTermSearcher2, err := NewTermSearcher(twoDocIndexReader, "couchbase", "street", 1.0, explainTrue)
+	couchbaseTermSearcher2, err := NewTermSearcher(nil, twoDocIndexReader, "couchbase", "street", 1.0, explainTrue)
 	if err != nil {
 		t.Fatal(err)
 	}
-	misterTermSearcher3, err := NewTermSearcher(twoDocIndexReader, "mister", "title", 5.0, explainTrue)
+	misterTermSearcher3, err := NewTermSearcher(nil, twoDocIndexReader, "mister", "title", 5.0, explainTrue)
 	if err != nil {
 		t.Fatal(err)
 	}
-	couchbaseAndMisterSearcher2, err := NewConjunctionSearcher(twoDocIndexReader, []search.Searcher{couchbaseTermSearcher2, misterTermSearcher3}, explainTrue)
+	couchbaseAndMisterSearcher2, err := NewConjunctionSearcher(nil, twoDocIndexReader, []search.Searcher{couchbaseTermSearcher2, misterTermSearcher3}, explainTrue)
 	if err != nil {
 		t.Fatal(err)
 	}
-	beerAndCouchbaseAndMisterSearcher, err := NewConjunctionSearcher(twoDocIndexReader, []search.Searcher{beerTermSearcher5, couchbaseAndMisterSearcher2}, explainTrue)
+	beerAndCouchbaseAndMisterSearcher, err := NewConjunctionSearcher(nil, twoDocIndexReader, []search.Searcher{beerTermSearcher5, couchbaseAndMisterSearcher2}, explainTrue)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -353,7 +353,7 @@ func testScorchCompositeSearchOptimizationsHelper(
 			field := ft[0]
 			term := ft[1]
 
-			searcher, err := NewTermSearcher(indexReader, term, field, 1.0, searcherOptions)
+			searcher, err := NewTermSearcher(nil, indexReader, term, field, 1.0, searcherOptions)
 			if err != nil {
 				t.Fatal(err)
 			}
@@ -365,9 +365,9 @@ func testScorchCompositeSearchOptimizationsHelper(
 		var cs search.Searcher
 		var err error
 		if compositeKind == "conjunction" {
-			cs, err = NewConjunctionSearcher(indexReader, searchers, searcherOptions)
+			cs, err = NewConjunctionSearcher(nil, indexReader, searchers, searcherOptions)
 		} else {
-			cs, err = NewDisjunctionSearcher(indexReader, searchers, 0, searcherOptions)
+			cs, err = NewDisjunctionSearcher(nil, indexReader, searchers, 0, searcherOptions)
 		}
 		if err != nil {
 			t.Fatal(err)
diff --git a/search/searcher/search_disjunction_test.go b/search/searcher/search_disjunction_test.go
index b4bf3167..ed69bc04 100644
--- a/search/searcher/search_disjunction_test.go
+++ b/search/searcher/search_disjunction_test.go
@@ -36,37 +36,37 @@ func TestDisjunctionSearch(t *testing.T) {
 
 	explainTrue := search.SearcherOptions{Explain: true}
 
-	martyTermSearcher, err := NewTermSearcher(twoDocIndexReader, "marty", "name", 1.0, explainTrue)
+	martyTermSearcher, err := NewTermSearcher(nil, twoDocIndexReader, "marty", "name", 1.0, explainTrue)
 	if err != nil {
 		t.Fatal(err)
 	}
-	dustinTermSearcher, err := NewTermSearcher(twoDocIndexReader, "dustin", "name", 1.0, explainTrue)
+	dustinTermSearcher, err := NewTermSearcher(nil, twoDocIndexReader, "dustin", "name", 1.0, explainTrue)
 	if err != nil {
 		t.Fatal(err)
 	}
-	martyOrDustinSearcher, err := NewDisjunctionSearcher(twoDocIndexReader, []search.Searcher{martyTermSearcher, dustinTermSearcher}, 0, explainTrue)
+	martyOrDustinSearcher, err := NewDisjunctionSearcher(nil, twoDocIndexReader, []search.Searcher{martyTermSearcher, dustinTermSearcher}, 0, explainTrue)
 	if err != nil {
 		t.Fatal(err)
 	}
 
-	martyTermSearcher2, err := NewTermSearcher(twoDocIndexReader, "marty", "name", 1.0, explainTrue)
+	martyTermSearcher2, err := NewTermSearcher(nil, twoDocIndexReader, "marty", "name", 1.0, explainTrue)
 	if err != nil {
 		t.Fatal(err)
 	}
-	dustinTermSearcher2, err := NewTermSearcher(twoDocIndexReader, "dustin", "name", 1.0, explainTrue)
+	dustinTermSearcher2, err := NewTermSearcher(nil, twoDocIndexReader, "dustin", "name", 1.0, explainTrue)
 	if err != nil {
 		t.Fatal(err)
 	}
-	martyOrDustinSearcher2, err := NewDisjunctionSearcher(twoDocIndexReader, []search.Searcher{martyTermSearcher2, dustinTermSearcher2}, 0, explainTrue)
+	martyOrDustinSearcher2, err := NewDisjunctionSearcher(nil, twoDocIndexReader, []search.Searcher{martyTermSearcher2, dustinTermSearcher2}, 0, explainTrue)
 	if err != nil {
 		t.Fatal(err)
 	}
 
-	raviTermSearcher, err := NewTermSearcher(twoDocIndexReader, "ravi", "name", 1.0, explainTrue)
+	raviTermSearcher, err := NewTermSearcher(nil, twoDocIndexReader, "ravi", "name", 1.0, explainTrue)
 	if err != nil {
 		t.Fatal(err)
 	}
-	nestedRaviOrMartyOrDustinSearcher, err := NewDisjunctionSearcher(twoDocIndexReader, []search.Searcher{raviTermSearcher, martyOrDustinSearcher2}, 0, explainTrue)
+	nestedRaviOrMartyOrDustinSearcher, err := NewDisjunctionSearcher(nil, twoDocIndexReader, []search.Searcher{raviTermSearcher, martyOrDustinSearcher2}, 0, explainTrue)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -159,15 +159,15 @@ func TestDisjunctionAdvance(t *testing.T) {
 
 	explainTrue := search.SearcherOptions{Explain: true}
 
-	martyTermSearcher, err := NewTermSearcher(twoDocIndexReader, "marty", "name", 1.0, explainTrue)
+	martyTermSearcher, err := NewTermSearcher(nil, twoDocIndexReader, "marty", "name", 1.0, explainTrue)
 	if err != nil {
 		t.Fatal(err)
 	}
-	dustinTermSearcher, err := NewTermSearcher(twoDocIndexReader, "dustin", "name", 1.0, explainTrue)
+	dustinTermSearcher, err := NewTermSearcher(nil, twoDocIndexReader, "dustin", "name", 1.0, explainTrue)
 	if err != nil {
 		t.Fatal(err)
 	}
-	martyOrDustinSearcher, err := NewDisjunctionSearcher(twoDocIndexReader, []search.Searcher{martyTermSearcher, dustinTermSearcher}, 0, explainTrue)
+	martyOrDustinSearcher, err := NewDisjunctionSearcher(nil, twoDocIndexReader, []search.Searcher{martyTermSearcher, dustinTermSearcher}, 0, explainTrue)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -206,19 +206,19 @@ func TestDisjunctionSearchTooMany(t *testing.T) {
 
 	explainTrue := search.SearcherOptions{Explain: true}
 
-	martyTermSearcher, err := NewTermSearcher(twoDocIndexReader, "marty", "name", 1.0, explainTrue)
+	martyTermSearcher, err := NewTermSearcher(nil, twoDocIndexReader, "marty", "name", 1.0, explainTrue)
 	if err != nil {
 		t.Fatal(err)
 	}
-	dustinTermSearcher, err := NewTermSearcher(twoDocIndexReader, "dustin", "name", 1.0, explainTrue)
+	dustinTermSearcher, err := NewTermSearcher(nil, twoDocIndexReader, "dustin", "name", 1.0, explainTrue)
 	if err != nil {
 		t.Fatal(err)
 	}
-	steveTermSearcher, err := NewTermSearcher(twoDocIndexReader, "steve", "name", 1.0, explainTrue)
+	steveTermSearcher, err := NewTermSearcher(nil, twoDocIndexReader, "steve", "name", 1.0, explainTrue)
 	if err != nil {
 		t.Fatal(err)
 	}
-	_, err = NewDisjunctionSearcher(twoDocIndexReader, []search.Searcher{martyTermSearcher, dustinTermSearcher, steveTermSearcher}, 0, explainTrue)
+	_, err = NewDisjunctionSearcher(nil, twoDocIndexReader, []search.Searcher{martyTermSearcher, dustinTermSearcher, steveTermSearcher}, 0, explainTrue)
 	if err == nil {
 		t.Fatal(err)
 	}
diff --git a/search/searcher/search_docid_test.go b/search/searcher/search_docid_test.go
index 01893d8f..7727de7f 100644
--- a/search/searcher/search_docid_test.go
+++ b/search/searcher/search_docid_test.go
@@ -61,7 +61,7 @@ func testDocIDSearcher(t *testing.T, indexed, searched, wanted []string) {
 
 	explainOff := search.SearcherOptions{Explain: false}
 
-	searcher, err := NewDocIDSearcher(indexReader, searched, 1.0, explainOff)
+	searcher, err := NewDocIDSearcher(nil, indexReader, searched, 1.0, explainOff)
 	if err != nil {
 		t.Fatal(err)
 	}
diff --git a/search/searcher/search_fuzzy_test.go b/search/searcher/search_fuzzy_test.go
index 38cc0452..e6d4c2e4 100644
--- a/search/searcher/search_fuzzy_test.go
+++ b/search/searcher/search_fuzzy_test.go
@@ -36,22 +36,22 @@ func TestFuzzySearch(t *testing.T) {
 
 	explainTrue := search.SearcherOptions{Explain: true}
 
-	fuzzySearcherbeet, err := NewFuzzySearcher(twoDocIndexReader, "beet", 0, 1, "desc", 1.0, explainTrue)
+	fuzzySearcherbeet, err := NewFuzzySearcher(nil, twoDocIndexReader, "beet", 0, 1, "desc", 1.0, explainTrue)
 	if err != nil {
 		t.Fatal(err)
 	}
 
-	fuzzySearcherdouches, err := NewFuzzySearcher(twoDocIndexReader, "douches", 0, 2, "desc", 1.0, explainTrue)
+	fuzzySearcherdouches, err := NewFuzzySearcher(nil, twoDocIndexReader, "douches", 0, 2, "desc", 1.0, explainTrue)
 	if err != nil {
 		t.Fatal(err)
 	}
 
-	fuzzySearcheraplee, err := NewFuzzySearcher(twoDocIndexReader, "aplee", 0, 2, "desc", 1.0, explainTrue)
+	fuzzySearcheraplee, err := NewFuzzySearcher(nil, twoDocIndexReader, "aplee", 0, 2, "desc", 1.0, explainTrue)
 	if err != nil {
 		t.Fatal(err)
 	}
 
-	fuzzySearcherprefix, err := NewFuzzySearcher(twoDocIndexReader, "water", 3, 2, "desc", 1.0, explainTrue)
+	fuzzySearcherprefix, err := NewFuzzySearcher(nil, twoDocIndexReader, "water", 3, 2, "desc", 1.0, explainTrue)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -143,12 +143,12 @@ func TestFuzzySearch(t *testing.T) {
 
 func TestFuzzySearchLimitErrors(t *testing.T) {
 	explainTrue := search.SearcherOptions{Explain: true}
-	_, err := NewFuzzySearcher(nil, "water", 3, 3, "desc", 1.0, explainTrue)
+	_, err := NewFuzzySearcher(nil, nil, "water", 3, 3, "desc", 1.0, explainTrue)
 	if err == nil {
 		t.Fatal("`fuzziness exceeds max (2)` error expected")
 	}
 
-	_, err = NewFuzzySearcher(nil, "water", 3, -1, "desc", 1.0, explainTrue)
+	_, err = NewFuzzySearcher(nil, nil, "water", 3, -1, "desc", 1.0, explainTrue)
 	if err == nil {
 		t.Fatal("`invalid fuzziness, negative` error expected")
 	}
diff --git a/search/searcher/search_geoboundingbox_test.go b/search/searcher/search_geoboundingbox_test.go
index c6d45a7c..5f190796 100644
--- a/search/searcher/search_geoboundingbox_test.go
+++ b/search/searcher/search_geoboundingbox_test.go
@@ -73,7 +73,7 @@ func TestGeoBoundingBox(t *testing.T) {
 
 func testGeoBoundingBoxSearch(i index.IndexReader, minLon, minLat, maxLon, maxLat float64, field string) ([]string, error) {
 	var rv []string
-	gbs, err := NewGeoBoundingBoxSearcher(i, minLon, minLat, maxLon, maxLat, field, 1.0, search.SearcherOptions{}, true)
+	gbs, err := NewGeoBoundingBoxSearcher(nil, i, minLon, minLat, maxLon, maxLat, field, 1.0, search.SearcherOptions{}, true)
 	if err != nil {
 		return nil, err
 	}
@@ -184,7 +184,7 @@ func TestComputeGeoRange(t *testing.T) {
 	}
 
 	for testi, test := range tests {
-		onBoundaryRes, offBoundaryRes, err := ComputeGeoRange(0, GeoBitsShift1Minus1,
+		onBoundaryRes, offBoundaryRes, err := ComputeGeoRange(nil, 0, GeoBitsShift1Minus1,
 			-1.0*test.degs, -1.0*test.degs, test.degs, test.degs, true, nil, "")
 		if (err != nil) != (test.err != "") {
 			t.Errorf("test: %+v, err: %v", test, err)
@@ -245,7 +245,7 @@ func benchmarkComputeGeoRange(b *testing.B,
 
 	for i := 0; i < b.N; i++ {
 		onBoundaryRes, offBoundaryRes, err :=
-			ComputeGeoRange(0, GeoBitsShift1Minus1, minLon, minLat, maxLon, maxLat, checkBoundaries, nil, "")
+			ComputeGeoRange(nil, 0, GeoBitsShift1Minus1, minLon, minLat, maxLon, maxLat, checkBoundaries, nil, "")
 		if err != nil {
 			b.Fatalf("expected no err")
 		}
diff --git a/search/searcher/search_geopointdistance_test.go b/search/searcher/search_geopointdistance_test.go
index 739e8750..22394348 100644
--- a/search/searcher/search_geopointdistance_test.go
+++ b/search/searcher/search_geopointdistance_test.go
@@ -66,7 +66,7 @@ func TestGeoPointDistanceSearcher(t *testing.T) {
 
 func testGeoPointDistanceSearch(i index.IndexReader, centerLon, centerLat, dist float64, field string) ([]string, error) {
 	var rv []string
-	gds, err := NewGeoPointDistanceSearcher(i, centerLon, centerLat, dist, field, 1.0, search.SearcherOptions{})
+	gds, err := NewGeoPointDistanceSearcher(nil, i, centerLon, centerLat, dist, field, 1.0, search.SearcherOptions{})
 	if err != nil {
 		return nil, err
 	}
@@ -112,7 +112,7 @@ func TestGeoPointDistanceCompare(t *testing.T) {
 		compare := func(desc string,
 			minLon, minLat, maxLon, maxLat float64, checkBoundaries bool) {
 			// do math to produce list of terms needed for this search
-			onBoundaryRes, offBoundaryRes, err := ComputeGeoRange(0, GeoBitsShift1Minus1,
+			onBoundaryRes, offBoundaryRes, err := ComputeGeoRange(nil, 0, GeoBitsShift1Minus1,
 				minLon, minLat, maxLon, maxLat, checkBoundaries, nil, "")
 			if err != nil {
 				t.Fatal(err)
diff --git a/search/searcher/search_geopolygon_test.go b/search/searcher/search_geopolygon_test.go
index 75d50be2..2b300be3 100644
--- a/search/searcher/search_geopolygon_test.go
+++ b/search/searcher/search_geopolygon_test.go
@@ -149,7 +149,7 @@ func TestGeoRectanglePolygon(t *testing.T) {
 
 func testGeoPolygonSearch(i index.IndexReader, polygon []geo.Point, field string) ([]string, error) {
 	var rv []string
-	gbs, err := NewGeoBoundedPolygonSearcher(i, polygon, field, 1.0, search.SearcherOptions{})
+	gbs, err := NewGeoBoundedPolygonSearcher(nil, i, polygon, field, 1.0, search.SearcherOptions{})
 	if err != nil {
 		return nil, err
 	}
diff --git a/search/searcher/search_geoshape_circle_test.go b/search/searcher/search_geoshape_circle_test.go
index 1ff50a5e..a5d2b562 100644
--- a/search/searcher/search_geoshape_circle_test.go
+++ b/search/searcher/search_geoshape_circle_test.go
@@ -201,7 +201,7 @@ func runGeoShapeCircleRelationQuery(relation string, i index.IndexReader,
 	var rv []string
 	s := geo.NewGeoCircle(points, radius)
 
-	gbs, err := NewGeoShapeSearcher(i, s, relation, field, 1.0, search.SearcherOptions{})
+	gbs, err := NewGeoShapeSearcher(nil, i, s, relation, field, 1.0, search.SearcherOptions{})
 	if err != nil {
 		return nil, err
 	}
diff --git a/search/searcher/search_geoshape_envelope_test.go b/search/searcher/search_geoshape_envelope_test.go
index 7d3ff976..22a53bde 100644
--- a/search/searcher/search_geoshape_envelope_test.go
+++ b/search/searcher/search_geoshape_envelope_test.go
@@ -221,7 +221,7 @@ func runGeoShapeEnvelopeRelationQuery(relation string, i index.IndexReader,
 	var rv []string
 	s := geo.NewGeoEnvelope(points)
 
-	gbs, err := NewGeoShapeSearcher(i, s, relation, field, 1.0, search.SearcherOptions{})
+	gbs, err := NewGeoShapeSearcher(nil, i, s, relation, field, 1.0, search.SearcherOptions{})
 	if err != nil {
 		return nil, err
 	}
diff --git a/search/searcher/search_geoshape_geometrycollection_test.go b/search/searcher/search_geoshape_geometrycollection_test.go
index 16fca089..bba53f21 100644
--- a/search/searcher/search_geoshape_geometrycollection_test.go
+++ b/search/searcher/search_geoshape_geometrycollection_test.go
@@ -286,7 +286,7 @@ func runGeoShapeGeometryCollectionRelationQuery(relation string, i index.IndexRe
 		return nil, err
 	}
 
-	gbs, err := NewGeoShapeSearcher(i, s, relation, field, 1.0, search.SearcherOptions{})
+	gbs, err := NewGeoShapeSearcher(nil, i, s, relation, field, 1.0, search.SearcherOptions{})
 	if err != nil {
 		return nil, err
 	}
diff --git a/search/searcher/search_geoshape_linestring_test.go b/search/searcher/search_geoshape_linestring_test.go
index bc0e416b..b222a281 100644
--- a/search/searcher/search_geoshape_linestring_test.go
+++ b/search/searcher/search_geoshape_linestring_test.go
@@ -254,7 +254,7 @@ func runGeoShapeLinestringQueryWithRelation(relation string, i index.IndexReader
 func executeSearch(relation string, i index.IndexReader,
 	s index.GeoJSON, field string) ([]string, error) {
 	var rv []string
-	gbs, err := NewGeoShapeSearcher(i, s, relation, field, 1.0, search.SearcherOptions{})
+	gbs, err := NewGeoShapeSearcher(nil, i, s, relation, field, 1.0, search.SearcherOptions{})
 	if err != nil {
 		return nil, err
 	}
diff --git a/search/searcher/search_geoshape_points_test.go b/search/searcher/search_geoshape_points_test.go
index b0efed6c..adaf465e 100644
--- a/search/searcher/search_geoshape_points_test.go
+++ b/search/searcher/search_geoshape_points_test.go
@@ -297,7 +297,7 @@ func runGeoShapePointRelationQuery(relation string, multi bool,
 		s = geo.NewGeoJsonPoint(points[0])
 	}
 
-	gbs, err := NewGeoShapeSearcher(i, s, relation, field, 1.0, search.SearcherOptions{})
+	gbs, err := NewGeoShapeSearcher(nil, i, s, relation, field, 1.0, search.SearcherOptions{})
 	if err != nil {
 		return nil, err
 	}
diff --git a/search/searcher/search_geoshape_polygon_test.go b/search/searcher/search_geoshape_polygon_test.go
index 115c9860..c322ee20 100644
--- a/search/searcher/search_geoshape_polygon_test.go
+++ b/search/searcher/search_geoshape_polygon_test.go
@@ -300,7 +300,7 @@ func runGeoShapePolygonQueryWithRelation(relation string, i index.IndexReader,
 	var rv []string
 	s := geo.NewGeoJsonPolygon(points)
 
-	gbs, err := NewGeoShapeSearcher(i, s, relation, field, 1.0, search.SearcherOptions{})
+	gbs, err := NewGeoShapeSearcher(nil, i, s, relation, field, 1.0, search.SearcherOptions{})
 	if err != nil {
 		return nil, err
 	}
@@ -607,7 +607,7 @@ func runGeoShapeMultiPolygonQueryWithRelation(relation string,
 	var rv []string
 	s := geo.NewGeoJsonMultiPolygon(points)
 
-	gbs, err := NewGeoShapeSearcher(i, s, relation,
+	gbs, err := NewGeoShapeSearcher(nil, i, s, relation,
 		field, 1.0, search.SearcherOptions{})
 	if err != nil {
 		return nil, err
diff --git a/search/searcher/search_match_all_test.go b/search/searcher/search_match_all_test.go
index 819d5463..74e72c32 100644
--- a/search/searcher/search_match_all_test.go
+++ b/search/searcher/search_match_all_test.go
@@ -36,12 +36,12 @@ func TestMatchAllSearch(t *testing.T) {
 
 	explainTrue := search.SearcherOptions{Explain: true}
 
-	allSearcher, err := NewMatchAllSearcher(twoDocIndexReader, 1.0, explainTrue)
+	allSearcher, err := NewMatchAllSearcher(nil, twoDocIndexReader, 1.0, explainTrue)
 	if err != nil {
 		t.Fatal(err)
 	}
 
-	allSearcher2, err := NewMatchAllSearcher(twoDocIndexReader, 1.2, explainTrue)
+	allSearcher2, err := NewMatchAllSearcher(nil, twoDocIndexReader, 1.2, explainTrue)
 	if err != nil {
 		t.Fatal(err)
 	}
diff --git a/search/searcher/search_phrase_test.go b/search/searcher/search_phrase_test.go
index e83ea35e..f74d6561 100644
--- a/search/searcher/search_phrase_test.go
+++ b/search/searcher/search_phrase_test.go
@@ -36,7 +36,7 @@ func TestPhraseSearch(t *testing.T) {
 	}()
 
 	soptions := search.SearcherOptions{Explain: true, IncludeTermVectors: true}
-	phraseSearcher, err := NewPhraseSearcher(twoDocIndexReader, []string{"angst", "beer"}, "desc", soptions)
+	phraseSearcher, err := NewPhraseSearcher(nil, twoDocIndexReader, []string{"angst", "beer"}, "desc", soptions)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -130,7 +130,7 @@ func TestMultiPhraseSearch(t *testing.T) {
 		if err != nil {
 			t.Error(err)
 		}
-		searcher, err := NewMultiPhraseSearcher(reader, test.phrase, "desc", soptions)
+		searcher, err := NewMultiPhraseSearcher(nil, reader, test.phrase, "desc", soptions)
 		if err != nil {
 			t.Error(err)
 		}
diff --git a/search/searcher/search_regexp_test.go b/search/searcher/search_regexp_test.go
index 9f65fd2e..863547ed 100644
--- a/search/searcher/search_regexp_test.go
+++ b/search/searcher/search_regexp_test.go
@@ -76,7 +76,7 @@ func searcherMaker(t *testing.T, ir index.IndexReader, re, field string) search.
 		t.Fatal(err)
 	}
 
-	regexpSearcher, err := NewRegexpSearcher(ir, pattern, field, 1.0,
+	regexpSearcher, err := NewRegexpSearcher(nil, ir, pattern, field, 1.0,
 		search.SearcherOptions{Explain: true})
 	if err != nil {
 		t.Fatal(err)
@@ -86,7 +86,7 @@ func searcherMaker(t *testing.T, ir index.IndexReader, re, field string) search.
 }
 
 func searcherStringMaker(t *testing.T, ir index.IndexReader, re, field string) search.Searcher {
-	regexpSearcher, err := NewRegexpStringSearcher(ir, re, field, 1.0,
+	regexpSearcher, err := NewRegexpStringSearcher(nil, ir, re, field, 1.0,
 		search.SearcherOptions{Explain: true})
 	if err != nil {
 		t.Fatal(err)
diff --git a/search/searcher/search_term_range_test.go b/search/searcher/search_term_range_test.go
index 126e03ee..0d199cb8 100644
--- a/search/searcher/search_term_range_test.go
+++ b/search/searcher/search_term_range_test.go
@@ -15,13 +15,14 @@
 package searcher
 
 import (
-	"github.com/blevesearch/bleve/v2/index/scorch"
 	"io/ioutil"
 	"os"
 	"reflect"
 	"sort"
 	"testing"
 
+	"github.com/blevesearch/bleve/v2/index/scorch"
+
 	"github.com/blevesearch/bleve/v2/search"
 )
 
@@ -174,7 +175,7 @@ func TestTermRangeSearch(t *testing.T) {
 
 	for _, test := range tests {
 
-		searcher, err := NewTermRangeSearcher(twoDocIndexReader, test.min, test.max,
+		searcher, err := NewTermRangeSearcher(nil, twoDocIndexReader, test.min, test.max,
 			&test.inclusiveMin, &test.inclusiveMax, test.field, 1.0, search.SearcherOptions{Explain: true})
 		if err != nil {
 			t.Fatal(err)
@@ -232,7 +233,7 @@ func TestTermRangeSearchTooManyTerms(t *testing.T) {
 
 	var want = []string{"1", "3", "4", "5"}
 	var truth = true
-	searcher, err := NewTermRangeSearcher(scorchReader, []byte("bobert"), []byte("ravi"),
+	searcher, err := NewTermRangeSearcher(nil, scorchReader, []byte("bobert"), []byte("ravi"),
 		&truth, &truth, "name", 1.0, search.SearcherOptions{Score: "none", IncludeTermVectors: false})
 	if err != nil {
 		t.Fatal(err)
diff --git a/search/searcher/search_term_test.go b/search/searcher/search_term_test.go
index d7a05359..0c08d4c5 100644
--- a/search/searcher/search_term_test.go
+++ b/search/searcher/search_term_test.go
@@ -118,7 +118,7 @@ func TestTermSearcher(t *testing.T) {
 		}
 	}()
 
-	searcher, err := NewTermSearcher(indexReader, queryTerm, queryField, queryBoost, queryExplain)
+	searcher, err := NewTermSearcher(nil, indexReader, queryTerm, queryField, queryBoost, queryExplain)
 	if err != nil {
 		t.Fatal(err)
 	}
diff --git a/search_test.go b/search_test.go
index ee8846a0..e4a880a9 100644
--- a/search_test.go
+++ b/search_test.go
@@ -17,7 +17,6 @@ package bleve
 import (
 	"encoding/json"
 	"fmt"
-	index "github.com/blevesearch/bleve_index_api"
 	"reflect"
 	"strconv"
 	"strings"
@@ -36,6 +35,7 @@ import (
 	"github.com/blevesearch/bleve/v2/analysis/tokenizer/single"
 	"github.com/blevesearch/bleve/v2/analysis/tokenizer/whitespace"
 	"github.com/blevesearch/bleve/v2/document"
+	"github.com/blevesearch/bleve/v2/geo"
 	"github.com/blevesearch/bleve/v2/index/scorch"
 	"github.com/blevesearch/bleve/v2/index/upsidedown"
 	"github.com/blevesearch/bleve/v2/mapping"
@@ -43,6 +43,7 @@ import (
 	"github.com/blevesearch/bleve/v2/search/highlight/highlighter/ansi"
 	"github.com/blevesearch/bleve/v2/search/highlight/highlighter/html"
 	"github.com/blevesearch/bleve/v2/search/query"
+	index "github.com/blevesearch/bleve_index_api"
 )
 
 func TestSearchResultString(t *testing.T) {
@@ -481,7 +482,7 @@ func TestNestedBooleanSearchers(t *testing.T) {
 		doc.Fields = []document.Field{
 			document.NewTextFieldCustom("hostname", []uint64{}, []byte(hostname),
 				index.IndexField,
-				&analysis.Analyzer{
+				&analysis.DefaultAnalyzer{
 					Tokenizer: single.NewSingleTokenTokenizer(),
 					TokenFilters: []analysis.TokenFilter{
 						lowercase.NewLowerCaseFilter(),
@@ -1889,3 +1890,213 @@ func TestHightlightingWithHTMLCharacterFilter(t *testing.T) {
 			gotFragment, expectedFragment)
 	}
 }
+
+func TestIPRangeQuery(t *testing.T) {
+	idxMapping := NewIndexMapping()
+	im := NewIPFieldMapping()
+	dmap := mapping.NewDocumentMapping()
+	dmap.AddFieldMappingsAt("ip_content", im)
+	idxMapping.DefaultMapping = dmap
+
+	tmpIndexPath := createTmpIndexPath(t)
+	defer cleanupTmpIndexPath(t, tmpIndexPath)
+
+	idx, err := New(tmpIndexPath, idxMapping)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	defer func() {
+		err = idx.Close()
+		if err != nil {
+			t.Fatal(err)
+		}
+	}()
+
+	ipContent := "192.168.10.11"
+	if err = idx.Index("doc", map[string]string{
+		"ip_content": ipContent,
+	}); err != nil {
+		t.Fatal(err)
+	}
+
+	q := query.NewIPRangeQuery("192.168.10.0/24")
+	q.SetField("ip_content")
+	sr := NewSearchRequest(q)
+
+	searchResults, err := idx.Search(sr)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if len(searchResults.Hits) != 1 ||
+		searchResults.Hits[0].ID != "doc" {
+		t.Fatal("Expected the 1 result - doc")
+	}
+}
+
+func TestGeoShapePolygonContainsPoint(t *testing.T) {
+	fm := mapping.NewGeoShapeFieldMapping()
+	dmap := mapping.NewDocumentMapping()
+	dmap.AddFieldMappingsAt("geometry", fm)
+
+	idxMapping := NewIndexMapping()
+	idxMapping.DefaultMapping = dmap
+
+	tmpIndexPath := createTmpIndexPath(t)
+	defer cleanupTmpIndexPath(t, tmpIndexPath)
+
+	idx, err := New(tmpIndexPath, idxMapping)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	defer func() {
+		err = idx.Close()
+		if err != nil {
+			t.Fatal(err)
+		}
+	}()
+
+	// Polygon coordinates to be ordered in counter-clock-wise order
+	// for the outer loop, and holes to follow clock-wise order.
+	// See: https://www.rfc-editor.org/rfc/rfc7946.html#section-3.1.6
+
+	one := []byte(`{
+		"geometry":{
+			"type":"Polygon",
+			"coordinates":[[
+				[4.8089,46.9307],
+				[4.8223,46.8915],
+				[4.8149,46.886],
+				[4.8252,46.8647],
+				[4.8305,46.8531],
+				[4.8506,46.8509],
+				[4.8574,46.8621],
+				[4.8576,46.8769],
+				[4.8753,46.8774],
+				[4.8909,46.8519],
+				[4.8837,46.8485],
+				[4.9014,46.8318],
+				[4.9067,46.8179],
+				[4.8986,46.8122],
+				[4.9081,46.7969],
+				[4.9535,46.8254],
+				[4.9577,46.8053],
+				[5.0201,46.821],
+				[5.0357,46.8207],
+				[5.0656,46.8434],
+				[5.0955,46.8411],
+				[5.1149,46.8435],
+				[5.1259,46.8395],
+				[5.1433,46.8463],
+				[5.1415,46.8589],
+				[5.1533,46.873],
+				[5.138,46.8843],
+				[5.1525,46.9012],
+				[5.1485,46.9165],
+				[5.1582,46.926],
+				[5.1882,46.9251],
+				[5.2039,46.9129],
+				[5.2223,46.9175],
+				[5.2168,46.926],
+				[5.2338,46.9316],
+				[5.228,46.9505],
+				[5.2078,46.9722],
+				[5.2117,46.98],
+				[5.1961,46.9783],
+				[5.1663,46.9638],
+				[5.1213,46.9634],
+				[5.1086,46.9596],
+				[5.0729,46.9604],
+				[5.0731,46.9668],
+				[5.0493,46.9817],
+				[5.0034,46.9722],
+				[4.9852,46.9585],
+				[4.9479,46.9664],
+				[4.8943,46.9663],
+				[4.8937,46.951],
+				[4.8534,46.9458],
+				[4.8089,46.9307]
+			]]
+		}
+	}`)
+
+	two := []byte(`{
+		"geometry":{
+			"type":"Polygon",
+			"coordinates":[[
+				[2.2266,48.7816],
+				[2.2266,48.7761],
+				[2.2288,48.7745],
+				[2.2717,48.7905],
+				[2.2799,48.8109],
+				[2.3013,48.8251],
+				[2.2894,48.8283],
+				[2.2726,48.8144],
+				[2.2518,48.8164],
+				[2.255,48.8101],
+				[2.2348,48.7954],
+				[2.2266,48.7816]
+			]]
+		}
+	}`)
+
+	var doc1, doc2 map[string]interface{}
+
+	if err = json.Unmarshal(one, &doc1); err != nil {
+		t.Fatal(err)
+	}
+	if err = idx.Index("doc1", doc1); err != nil {
+		t.Fatal(err)
+	}
+
+	if err = json.Unmarshal(two, &doc2); err != nil {
+		t.Fatal(err)
+	}
+	if err = idx.Index("doc2", doc2); err != nil {
+		t.Fatal(err)
+	}
+
+	for testi, test := range []struct {
+		coordinates []float64
+		expectHits  []string
+	}{
+		{
+			coordinates: []float64{5, 46.9},
+			expectHits:  []string{"doc1"},
+		},
+		{
+			coordinates: []float64{1.5, 48.2},
+		},
+	} {
+		q, err := query.NewGeoShapeQuery(
+			[][][][]float64{{{test.coordinates}}},
+			geo.PointType,
+			"contains",
+		)
+		if err != nil {
+			t.Fatalf("test: %d, query err: %v", testi+1, err)
+		}
+		q.SetField("geometry")
+
+		res, err := idx.Search(NewSearchRequest(q))
+		if err != nil {
+			t.Fatalf("test: %d, search err: %v", testi+1, err)
+		}
+
+		if len(res.Hits) != len(test.expectHits) {
+			t.Errorf("test: %d, unexpected hits: %v", testi+1, len(res.Hits))
+		}
+
+	OUTER:
+		for _, expect := range test.expectHits {
+			for _, got := range res.Hits {
+				if got.ID == expect {
+					continue OUTER
+				}
+			}
+			t.Errorf("test: %d, couldn't get: %v", testi+1, expect)
+		}
+	}
+}
diff --git a/test/versus_test.go b/test/versus_test.go
index eef6407a..795e3785 100644
--- a/test/versus_test.go
+++ b/test/versus_test.go
@@ -349,6 +349,7 @@ func testVersusSearches(vt *VersusTest, searchTemplates []string, idxA, idxB ble
 					t.Errorf("\n    doc: %d, body: %s", idx, strings.Join(vt.Bodies[idx], " "))
 				}
 			}
+
 			if !reflect.DeepEqual(hitsA, hitsB) {
 				t.Errorf("=========\nsearch: (%d) %s,\n res hits mismatch,\n len(hitsA): %d,\n len(hitsB): %d",
 					i, bufBytes, len(hitsA), len(hitsB))
@@ -358,6 +359,8 @@ func testVersusSearches(vt *VersusTest, searchTemplates []string, idxA, idxB ble
 
 			resA.Hits = nil
 			resB.Hits = nil
+			resA.BytesRead = 0
+			resB.BytesRead = 0
 
 			if !reflect.DeepEqual(resA, resB) {
 				resAj, _ := json.Marshal(resA)