Repository: blevesearch/bleve
Pull request #1874 (2023-08-28 11:39:10)

go fmt


Patch:
diff --git a/analysis/datetime/sanitized/sanitized.go b/analysis/datetime/sanitized/sanitized.go
new file mode 100644
index 00000000..33d271e6
--- /dev/null
+++ b/analysis/datetime/sanitized/sanitized.go
@@ -0,0 +1,127 @@
+//  Copyright (c) 2014 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 sanitized
+
+import (
+	"fmt"
+	"regexp"
+	"time"
+
+	"github.com/blevesearch/bleve/v2/analysis"
+	"github.com/blevesearch/bleve/v2/registry"
+)
+
+const Name = "sanitizedgo"
+
+var validMagicNumbers = map[string]struct{}{
+	"2006":    {},
+	"06":      {}, // Year
+	"01":      {},
+	"1":       {},
+	"_1":      {},
+	"January": {},
+	"Jan":     {}, // Month
+	"02":      {},
+	"2":       {},
+	"_2":      {},
+	"__2":     {},
+	"002":     {},
+	"Monday":  {},
+	"Mon":     {}, // Day
+	"15":      {},
+	"3":       {},
+	"03":      {}, // Hour
+	"4":       {},
+	"04":      {}, // Minute
+	"5":       {},
+	"05":      {}, // Second
+	"0700":    {},
+	"070000":  {},
+	"07":      {},
+	"00":      {},
+	"":        {},
+}
+
+var layoutSplitRegex = regexp.MustCompile("[\\+\\-= :T,Z\\.<>;\\?!`~@#$%\\^&\\*|'\"\\(\\){}\\[\\]/\\\\]")
+
+var layoutStripRegex = regexp.MustCompile(`PM|pm|\.9+|\.0+|MST`)
+
+type DateTimeParser struct {
+	layouts []string
+}
+
+func New(layouts []string) *DateTimeParser {
+	return &DateTimeParser{
+		layouts: layouts,
+	}
+}
+
+func (p *DateTimeParser) ParseDateTime(input string) (time.Time, string, error) {
+	for _, layout := range p.layouts {
+		rv, err := time.Parse(layout, input)
+		if err == nil {
+			return rv, layout, nil
+		}
+	}
+	return time.Time{}, "", analysis.ErrInvalidDateTime
+}
+
+// date time layouts must be a combination of constants specified in golang time package
+// https://pkg.go.dev/time#pkg-constants
+// this validation verifies that only these constants are used in the custom layout
+// for compatibility with the golang time package
+func validateLayout(layout string) bool {
+	// first we strip out commonly used constants
+	// such as "PM" which can be present in the layout
+	// right after a time component, e.g. 03:04PM;
+	// because regex split cannot separate "03:04PM" into
+	// "03:04" and "PM". We also strip out ".9+" and ".0+"
+	// which represent fractional seconds.
+	layout = layoutStripRegex.ReplaceAllString(layout, "")
+	// then we split the layout by non-constant characters
+	// which is a regex and verify that each split is a valid magic number
+	split := layoutSplitRegex.Split(layout, -1)
+	for i := range split {
+		_, found := validMagicNumbers[split[i]]
+		if !found {
+			return false
+		}
+	}
+	return true
+}
+
+func DateTimeParserConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.DateTimeParser, error) {
+	layouts, ok := config["layouts"].([]interface{})
+	if !ok {
+		return nil, fmt.Errorf("must specify layouts")
+	}
+	var layoutStrs []string
+	for _, layout := range layouts {
+		layoutStr, ok := layout.(string)
+		if ok {
+			if !validateLayout(layoutStr) {
+				return nil, fmt.Errorf("invalid datetime parser layout: %s,"+
+					" please refer to https://pkg.go.dev/time#pkg-constants for supported"+
+					" layouts", layoutStr)
+			}
+			layoutStrs = append(layoutStrs, layoutStr)
+		}
+	}
+	return New(layoutStrs), nil
+}
+
+func init() {
+	registry.RegisterDateTimeParser(Name, DateTimeParserConstructor)
+}
diff --git a/analysis/datetime/timestamp/microseconds/microseconds.go b/analysis/datetime/timestamp/microseconds/microseconds.go
new file mode 100644
index 00000000..3efed0ed
--- /dev/null
+++ b/analysis/datetime/timestamp/microseconds/microseconds.go
@@ -0,0 +1,52 @@
+//  Copyright (c) 2023 Couchbase, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// 		http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package microseconds
+
+import (
+	"math"
+	"strconv"
+	"time"
+
+	"github.com/blevesearch/bleve/v2/analysis"
+	"github.com/blevesearch/bleve/v2/registry"
+)
+
+const Name = "unix_micro"
+
+type DateTimeParser struct {
+}
+
+var minBound int64 = math.MinInt64 / 1000
+var maxBound int64 = math.MaxInt64 / 1000
+
+func (p *DateTimeParser) ParseDateTime(input string) (time.Time, string, error) {
+	// unix timestamp is milliseconds since UNIX epoch
+	timestamp, err := strconv.ParseInt(input, 10, 64)
+	if err != nil {
+		return time.Time{}, "", analysis.ErrInvalidTimestampString
+	}
+	if timestamp < minBound || timestamp > maxBound {
+		return time.Time{}, "", analysis.ErrInvalidTimestampRange
+	}
+	return time.UnixMicro(timestamp), "", nil
+}
+
+func DateTimeParserConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.DateTimeParser, error) {
+	return &DateTimeParser{}, nil
+}
+
+func init() {
+	registry.RegisterDateTimeParser(Name, DateTimeParserConstructor)
+}
diff --git a/analysis/datetime/timestamp/milliseconds/milliseconds.go b/analysis/datetime/timestamp/milliseconds/milliseconds.go
new file mode 100644
index 00000000..790153d2
--- /dev/null
+++ b/analysis/datetime/timestamp/milliseconds/milliseconds.go
@@ -0,0 +1,52 @@
+//  Copyright (c) 2023 Couchbase, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// 		http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package milliseconds
+
+import (
+	"math"
+	"strconv"
+	"time"
+
+	"github.com/blevesearch/bleve/v2/analysis"
+	"github.com/blevesearch/bleve/v2/registry"
+)
+
+const Name = "unix_milli"
+
+type DateTimeParser struct {
+}
+
+var minBound int64 = math.MinInt64 / 1000000
+var maxBound int64 = math.MaxInt64 / 1000000
+
+func (p *DateTimeParser) ParseDateTime(input string) (time.Time, string, error) {
+	// unix timestamp is milliseconds since UNIX epoch
+	timestamp, err := strconv.ParseInt(input, 10, 64)
+	if err != nil {
+		return time.Time{}, "", analysis.ErrInvalidTimestampString
+	}
+	if timestamp < minBound || timestamp > maxBound {
+		return time.Time{}, "", analysis.ErrInvalidTimestampRange
+	}
+	return time.UnixMilli(timestamp), "", nil
+}
+
+func DateTimeParserConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.DateTimeParser, error) {
+	return &DateTimeParser{}, nil
+}
+
+func init() {
+	registry.RegisterDateTimeParser(Name, DateTimeParserConstructor)
+}
diff --git a/analysis/datetime/timestamp/nanoseconds/nanoseconds.go b/analysis/datetime/timestamp/nanoseconds/nanoseconds.go
new file mode 100644
index 00000000..de318f3f
--- /dev/null
+++ b/analysis/datetime/timestamp/nanoseconds/nanoseconds.go
@@ -0,0 +1,52 @@
+//  Copyright (c) 2023 Couchbase, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// 		http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package nanoseconds
+
+import (
+	"math"
+	"strconv"
+	"time"
+
+	"github.com/blevesearch/bleve/v2/analysis"
+	"github.com/blevesearch/bleve/v2/registry"
+)
+
+const Name = "unix_nano"
+
+type DateTimeParser struct {
+}
+
+var minBound int64 = math.MinInt64
+var maxBound int64 = math.MaxInt64
+
+func (p *DateTimeParser) ParseDateTime(input string) (time.Time, string, error) {
+	// unix timestamp is milliseconds since UNIX epoch
+	timestamp, err := strconv.ParseInt(input, 10, 64)
+	if err != nil {
+		return time.Time{}, "", analysis.ErrInvalidTimestampString
+	}
+	if timestamp < minBound || timestamp > maxBound {
+		return time.Time{}, "", analysis.ErrInvalidTimestampRange
+	}
+	return time.Unix(0, timestamp), "", nil
+}
+
+func DateTimeParserConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.DateTimeParser, error) {
+	return &DateTimeParser{}, nil
+}
+
+func init() {
+	registry.RegisterDateTimeParser(Name, DateTimeParserConstructor)
+}
diff --git a/analysis/datetime/timestamp/seconds/seconds.go b/analysis/datetime/timestamp/seconds/seconds.go
new file mode 100644
index 00000000..8d3ee3b4
--- /dev/null
+++ b/analysis/datetime/timestamp/seconds/seconds.go
@@ -0,0 +1,52 @@
+//  Copyright (c) 2014 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 seconds
+
+import (
+	"math"
+	"strconv"
+	"time"
+
+	"github.com/blevesearch/bleve/v2/analysis"
+	"github.com/blevesearch/bleve/v2/registry"
+)
+
+const Name = "unix_sec"
+
+type DateTimeParser struct {
+}
+
+var minBound int64 = math.MinInt64 / 1000000000
+var maxBound int64 = math.MaxInt64 / 1000000000
+
+func (p *DateTimeParser) ParseDateTime(input string) (time.Time, string, error) {
+	// unix timestamp is seconds since UNIX epoch
+	timestamp, err := strconv.ParseInt(input, 10, 64)
+	if err != nil {
+		return time.Time{}, "", analysis.ErrInvalidTimestampString
+	}
+	if timestamp < minBound || timestamp > maxBound {
+		return time.Time{}, "", analysis.ErrInvalidTimestampRange
+	}
+	return time.Unix(timestamp, 0), "", nil
+}
+
+func DateTimeParserConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.DateTimeParser, error) {
+	return &DateTimeParser{}, nil
+}
+
+func init() {
+	registry.RegisterDateTimeParser(Name, DateTimeParserConstructor)
+}
diff --git a/analysis/lang/es/stop_filter_es.go b/analysis/lang/es/stop_filter_es.go
index e968153b..4cb79f87 100644
--- a/analysis/lang/es/stop_filter_es.go
+++ b/analysis/lang/es/stop_filter_es.go
@@ -4,7 +4,7 @@
 // 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
+//	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,
diff --git a/analysis/type.go b/analysis/type.go
index 02a8d7e8..e3a7f201 100644
--- a/analysis/type.go
+++ b/analysis/type.go
@@ -99,6 +99,9 @@ func (a *DefaultAnalyzer) Analyze(input []byte) TokenStream {
 
 var ErrInvalidDateTime = fmt.Errorf("unable to parse datetime with any of the layouts")
 
+var ErrInvalidTimestampString = fmt.Errorf("unable to parse timestamp string")
+var ErrInvalidTimestampRange = fmt.Errorf("timestamp out of range")
+
 type DateTimeParser interface {
 	ParseDateTime(string) (time.Time, string, error)
 }
diff --git a/builder.go b/builder.go
index dbb7e3ed..30285a2e 100644
--- a/builder.go
+++ b/builder.go
@@ -15,12 +15,12 @@
 package bleve
 
 import (
-	"encoding/json"
 	"fmt"
 
 	"github.com/blevesearch/bleve/v2/document"
 	"github.com/blevesearch/bleve/v2/index/scorch"
 	"github.com/blevesearch/bleve/v2/mapping"
+	"github.com/blevesearch/bleve/v2/util"
 	index "github.com/blevesearch/bleve_index_api"
 )
 
@@ -63,7 +63,7 @@ func newBuilder(path string, mapping mapping.IndexMapping, config map[string]int
 
 	// the builder does not have an API to interact with internal storage
 	// however we can pass k/v pairs through the config
-	mappingBytes, err := json.Marshal(mapping)
+	mappingBytes, err := util.MarshalJSON(mapping)
 	if err != nil {
 		return nil, err
 	}
diff --git a/config/config.go b/config/config.go
index d098cabb..2f6df4f4 100644
--- a/config/config.go
+++ b/config/config.go
@@ -71,6 +71,11 @@ import (
 	// date time parsers
 	_ "github.com/blevesearch/bleve/v2/analysis/datetime/flexible"
 	_ "github.com/blevesearch/bleve/v2/analysis/datetime/optional"
+	_ "github.com/blevesearch/bleve/v2/analysis/datetime/sanitized"
+	_ "github.com/blevesearch/bleve/v2/analysis/datetime/timestamp/microseconds"
+	_ "github.com/blevesearch/bleve/v2/analysis/datetime/timestamp/milliseconds"
+	_ "github.com/blevesearch/bleve/v2/analysis/datetime/timestamp/nanoseconds"
+	_ "github.com/blevesearch/bleve/v2/analysis/datetime/timestamp/seconds"
 
 	// languages
 	_ "github.com/blevesearch/bleve/v2/analysis/lang/ar"
diff --git a/geo/geo_s2plugin_impl.go b/geo/geo_s2plugin_impl.go
index f743d87f..8a034bd9 100644
--- a/geo/geo_s2plugin_impl.go
+++ b/geo/geo_s2plugin_impl.go
@@ -18,8 +18,8 @@ import (
 	"encoding/json"
 	"sync"
 
+	"github.com/blevesearch/bleve/v2/util"
 	index "github.com/blevesearch/bleve_index_api"
-
 	"github.com/blevesearch/geo/geojson"
 	"github.com/blevesearch/geo/s2"
 )
@@ -224,7 +224,7 @@ func (p *Point) Type() string {
 }
 
 func (p *Point) Value() ([]byte, error) {
-	return json.Marshal(p)
+	return util.MarshalJSON(p)
 }
 
 func (p *Point) Intersects(s index.GeoJSON) (bool, error) {
@@ -267,7 +267,7 @@ func (br *boundedRectangle) Type() string {
 }
 
 func (br *boundedRectangle) Value() ([]byte, error) {
-	return json.Marshal(br)
+	return util.MarshalJSON(br)
 }
 
 func (p *boundedRectangle) Intersects(s index.GeoJSON) (bool, error) {
@@ -309,7 +309,7 @@ func (bp *boundedPolygon) Type() string {
 }
 
 func (bp *boundedPolygon) Value() ([]byte, error) {
-	return json.Marshal(bp)
+	return util.MarshalJSON(bp)
 }
 
 func (p *boundedPolygon) Intersects(s index.GeoJSON) (bool, error) {
@@ -355,7 +355,7 @@ func (p *pointDistance) Type() string {
 }
 
 func (p *pointDistance) Value() ([]byte, error) {
-	return json.Marshal(p)
+	return util.MarshalJSON(p)
 }
 
 func NewPointDistance(centerLat, centerLon,
diff --git a/geo/parse.go b/geo/parse.go
index cc7beddc..01ec1dd8 100644
--- a/geo/parse.go
+++ b/geo/parse.go
@@ -24,15 +24,24 @@ import (
 // interpret it is as geo point.  Supported formats:
 // Container:
 // slice length 2 (GeoJSON)
-//  first element lon, second element lat
+//
+//	first element lon, second element lat
+//
 // string (coordinates separated by comma, or a geohash)
-//  first element lat, second element lon
+//
+//	first element lat, second element lon
+//
 // map[string]interface{}
-//  exact keys lat and lon or lng
+//
+//	exact keys lat and lon or lng
+//
 // struct
-//  w/exported fields case-insensitive match on lat and lon or lng
+//
+//	w/exported fields case-insensitive match on lat and lon or lng
+//
 // struct
-//  satisfying Later and Loner or Lnger interfaces
+//
+//	satisfying Later and Loner or Lnger interfaces
 //
 // in all cases values must be some sort of numeric-like thing: int/uint/float
 func ExtractGeoPoint(thing interface{}) (lon, lat float64, success bool) {
diff --git a/go.mod b/go.mod
index aaffb003..5a922d8d 100644
--- a/go.mod
+++ b/go.mod
@@ -6,7 +6,7 @@ require (
 	github.com/RoaringBitmap/roaring v1.2.3
 	github.com/bits-and-blooms/bitset v1.2.0
 	github.com/blevesearch/bleve_index_api v1.0.6
-	github.com/blevesearch/geo v0.1.17
+	github.com/blevesearch/geo v0.1.18
 	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
diff --git a/go.sum b/go.sum
index dd5d422a..90ebfb29 100644
--- a/go.sum
+++ b/go.sum
@@ -4,8 +4,8 @@ github.com/bits-and-blooms/bitset v1.2.0 h1:Kn4yilvwNtMACtf1eYDlG8H77R07mZSPbMjL
 github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA=
 github.com/blevesearch/bleve_index_api v1.0.6 h1:gyUUxdsrvmW3jVhhYdCVL6h9dCjNT/geNU7PxGn37p8=
 github.com/blevesearch/bleve_index_api v1.0.6/go.mod h1:YXMDwaXFFXwncRS8UobWs7nvo0DmusriM1nztTlj1ms=
-github.com/blevesearch/geo v0.1.17 h1:AguzI6/5mHXapzB0gE9IKWo+wWPHZmXZoscHcjFgAFA=
-github.com/blevesearch/geo v0.1.17/go.mod h1:uRMGWG0HJYfWfFJpK3zTdnnr1K+ksZTuWKhXeSokfnM=
+github.com/blevesearch/geo v0.1.18 h1:Np8jycHTZ5scFe7VEPLrDoHnnb9C4j636ue/CGrhtDw=
+github.com/blevesearch/geo v0.1.18/go.mod h1:uRMGWG0HJYfWfFJpK3zTdnnr1K+ksZTuWKhXeSokfnM=
 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=
diff --git a/http/doc_get.go b/http/doc_get.go
index 34d1e863..328955eb 100644
--- a/http/doc_get.go
+++ b/http/doc_get.go
@@ -17,6 +17,7 @@ package http
 import (
 	"fmt"
 	"net/http"
+	"strconv"
 
 	index "github.com/blevesearch/bleve_index_api"
 )
@@ -91,7 +92,7 @@ func (h *DocGetHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
 			if err == nil {
 				if layout == "" {
 					// layout not set probably means it was indexed as a timestamp
-					newval = d.UnixNano()
+					newval = strconv.FormatInt(d.UnixNano(), 10)
 				} else {
 					newval = d.Format(layout)
 				}
diff --git a/index/scorch/merge.go b/index/scorch/merge.go
index c5f0e79e..92adc3fd 100644
--- a/index/scorch/merge.go
+++ b/index/scorch/merge.go
@@ -16,7 +16,6 @@ package scorch
 
 import (
 	"context"
-	"encoding/json"
 	"fmt"
 	"os"
 	"strings"
@@ -25,6 +24,7 @@ import (
 
 	"github.com/RoaringBitmap/roaring"
 	"github.com/blevesearch/bleve/v2/index/scorch/mergeplan"
+	"github.com/blevesearch/bleve/v2/util"
 	segment "github.com/blevesearch/scorch_segment_api/v2"
 )
 
@@ -198,12 +198,12 @@ func (s *Scorch) parseMergePlannerOptions() (*mergeplan.MergePlanOptions,
 	error) {
 	mergePlannerOptions := mergeplan.DefaultMergePlanOptions
 	if v, ok := s.config["scorchMergePlanOptions"]; ok {
-		b, err := json.Marshal(v)
+		b, err := util.MarshalJSON(v)
 		if err != nil {
 			return &mergePlannerOptions, err
 		}
 
-		err = json.Unmarshal(b, &mergePlannerOptions)
+		err = util.UnmarshalJSON(b, &mergePlannerOptions)
 		if err != nil {
 			return &mergePlannerOptions, err
 		}
diff --git a/index/scorch/persister.go b/index/scorch/persister.go
index b90899f2..217582fe 100644
--- a/index/scorch/persister.go
+++ b/index/scorch/persister.go
@@ -17,7 +17,6 @@ package scorch
 import (
 	"bytes"
 	"encoding/binary"
-	"encoding/json"
 	"fmt"
 	"io"
 	"log"
@@ -31,6 +30,7 @@ import (
 	"time"
 
 	"github.com/RoaringBitmap/roaring"
+	"github.com/blevesearch/bleve/v2/util"
 	index "github.com/blevesearch/bleve_index_api"
 	segment "github.com/blevesearch/scorch_segment_api/v2"
 	bolt "go.etcd.io/bbolt"
@@ -324,12 +324,12 @@ func (s *Scorch) parsePersisterOptions() (*persisterOptions, error) {
 		MemoryPressurePauseThreshold: DefaultMemoryPressurePauseThreshold,
 	}
 	if v, ok := s.config["scorchPersisterOptions"]; ok {
-		b, err := json.Marshal(v)
+		b, err := util.MarshalJSON(v)
 		if err != nil {
 			return &po, err
 		}
 
-		err = json.Unmarshal(b, &po)
+		err = util.UnmarshalJSON(b, &po)
 		if err != nil {
 			return &po, err
 		}
diff --git a/index/scorch/stats.go b/index/scorch/stats.go
index d265e9d0..dc74d9f2 100644
--- a/index/scorch/stats.go
+++ b/index/scorch/stats.go
@@ -15,9 +15,10 @@
 package scorch
 
 import (
-	"encoding/json"
 	"reflect"
 	"sync/atomic"
+
+	"github.com/blevesearch/bleve/v2/util"
 )
 
 // Stats tracks statistics about the index, fields that are
@@ -151,5 +152,5 @@ func (s *Stats) ToMap() map[string]interface{} {
 // MarshalJSON implements json.Marshaler, and in contrast to standard
 // json marshaling provides atomic safety
 func (s *Stats) MarshalJSON() ([]byte, error) {
-	return json.Marshal(s.ToMap())
+	return util.MarshalJSON(s.ToMap())
 }
diff --git a/index/upsidedown/row.go b/index/upsidedown/row.go
index 901a4bcb..fff6d067 100644
--- a/index/upsidedown/row.go
+++ b/index/upsidedown/row.go
@@ -880,6 +880,9 @@ func NewStoredRowK(key []byte) (*StoredRow, error) {
 	}
 
 	rv.doc, err = buf.ReadBytes(ByteSeparator)
+	if err != nil {
+		return nil, err
+	}
 	if len(rv.doc) < 2 { // 1 for min doc id length, 1 for separator
 		err = fmt.Errorf("invalid doc length 0")
 		return nil, err
diff --git a/index/upsidedown/stats.go b/index/upsidedown/stats.go
index c1b4ddc0..5fea7c17 100644
--- a/index/upsidedown/stats.go
+++ b/index/upsidedown/stats.go
@@ -15,9 +15,9 @@
 package upsidedown
 
 import (
-	"encoding/json"
 	"sync/atomic"
 
+	"github.com/blevesearch/bleve/v2/util"
 	"github.com/blevesearch/upsidedown_store_api"
 )
 
@@ -51,5 +51,5 @@ func (i *indexStat) statsMap() map[string]interface{} {
 
 func (i *indexStat) MarshalJSON() ([]byte, error) {
 	m := i.statsMap()
-	return json.Marshal(m)
+	return util.MarshalJSON(m)
 }
diff --git a/index/upsidedown/store/boltdb/stats.go b/index/upsidedown/store/boltdb/stats.go
index e50e5527..d8961832 100644
--- a/index/upsidedown/store/boltdb/stats.go
+++ b/index/upsidedown/store/boltdb/stats.go
@@ -14,7 +14,9 @@
 
 package boltdb
 
-import "encoding/json"
+import (
+	"github.com/blevesearch/bleve/v2/util"
+)
 
 type stats struct {
 	s *Store
@@ -22,5 +24,5 @@ type stats struct {
 
 func (s *stats) MarshalJSON() ([]byte, error) {
 	bs := s.s.db.Stats()
-	return json.Marshal(bs)
+	return util.MarshalJSON(bs)
 }
diff --git a/index/upsidedown/store/metrics/stats.go b/index/upsidedown/store/metrics/stats.go
index c30d2dd6..48881929 100644
--- a/index/upsidedown/store/metrics/stats.go
+++ b/index/upsidedown/store/metrics/stats.go
@@ -15,8 +15,7 @@
 package metrics
 
 import (
-	"encoding/json"
-
+	"github.com/blevesearch/bleve/v2/util"
 	store "github.com/blevesearch/upsidedown_store_api"
 )
 
@@ -47,5 +46,5 @@ func (s *stats) statsMap() map[string]interface{} {
 
 func (s *stats) MarshalJSON() ([]byte, error) {
 	m := s.statsMap()
-	return json.Marshal(m)
+	return util.MarshalJSON(m)
 }
diff --git a/index/upsidedown/store/metrics/store.go b/index/upsidedown/store/metrics/store.go
index f537728f..8b4f2ad3 100644
--- a/index/upsidedown/store/metrics/store.go
+++ b/index/upsidedown/store/metrics/store.go
@@ -26,6 +26,7 @@ import (
 	"time"
 
 	"github.com/blevesearch/bleve/v2/registry"
+	"github.com/blevesearch/bleve/v2/util"
 	"github.com/blevesearch/go-metrics"
 	store "github.com/blevesearch/upsidedown_store_api"
 )
@@ -206,7 +207,7 @@ func (s *Store) WriteJSON(w io.Writer) (err error) {
 				}
 			}
 			var buf []byte
-			buf, err = json.Marshal(se)
+			buf, err = util.MarshalJSON(se)
 			if err == nil {
 				_, err = w.Write(buf)
 				if err != nil {
@@ -226,7 +227,7 @@ func (s *Store) WriteJSON(w io.Writer) (err error) {
 	if o, ok := s.o.(store.KVStoreStats); ok {
 		storeStats := o.Stats()
 		var storeBytes []byte
-		storeBytes, err = json.Marshal(storeStats)
+		storeBytes, err = util.MarshalJSON(storeStats)
 		if err != nil {
 			return
 		}
diff --git a/index/upsidedown/store/moss/lower.go b/index/upsidedown/store/moss/lower.go
index 4e5d40d6..ccbbc862 100644
--- a/index/upsidedown/store/moss/lower.go
+++ b/index/upsidedown/store/moss/lower.go
@@ -18,7 +18,6 @@
 package moss
 
 import (
-	"encoding/json"
 	"fmt"
 	"os"
 	"sync"
@@ -26,6 +25,7 @@ import (
 	"github.com/couchbase/moss"
 
 	"github.com/blevesearch/bleve/v2/registry"
+	"github.com/blevesearch/bleve/v2/util"
 	store "github.com/blevesearch/upsidedown_store_api"
 )
 
@@ -463,12 +463,12 @@ func InitMossStore(config map[string]interface{}, options moss.CollectionOptions
 	}
 	v, ok := config["mossStoreOptions"]
 	if ok {
-		b, err := json.Marshal(v) // Convert from map[string]interface{}.
+		b, err := util.MarshalJSON(v) // Convert from map[string]interface{}.
 		if err != nil {
 			return nil, nil, nil, nil, err
 		}
 
-		err = json.Unmarshal(b, &storeOptions)
+		err = util.UnmarshalJSON(b, &storeOptions)
 		if err != nil {
 			return nil, nil, nil, nil, err
 		}
diff --git a/index/upsidedown/store/moss/stats.go b/index/upsidedown/store/moss/stats.go
index 5ebc4a93..3956bbd4 100644
--- a/index/upsidedown/store/moss/stats.go
+++ b/index/upsidedown/store/moss/stats.go
@@ -15,8 +15,7 @@
 package moss
 
 import (
-	"encoding/json"
-
+	"github.com/blevesearch/bleve/v2/util"
 	store "github.com/blevesearch/upsidedown_store_api"
 )
 
@@ -55,5 +54,5 @@ func (s *stats) statsMap() map[string]interface{} {
 
 func (s *stats) MarshalJSON() ([]byte, error) {
 	m := s.statsMap()
-	return json.Marshal(m)
+	return util.MarshalJSON(m)
 }
diff --git a/index/upsidedown/store/moss/store.go b/index/upsidedown/store/moss/store.go
index 1eb7348f..7c7aa4a0 100644
--- a/index/upsidedown/store/moss/store.go
+++ b/index/upsidedown/store/moss/store.go
@@ -25,6 +25,7 @@ import (
 	"github.com/couchbase/moss"
 
 	"github.com/blevesearch/bleve/v2/registry"
+	"github.com/blevesearch/bleve/v2/util"
 	store "github.com/blevesearch/upsidedown_store_api"
 )
 
@@ -77,13 +78,13 @@ func New(mo store.MergeOperator, config map[string]interface{}) (
 
 	v, ok = config["mossCollectionOptions"]
 	if ok {
-		b, err := json.Marshal(v) // Convert from map[string]interface{}.
+		b, err := util.MarshalJSON(v) // Convert from map[string]interface{}.
 		if err != nil {
 			return nil, fmt.Errorf("moss store,"+
 				" could not marshal config[mossCollectionOptions]: %v, err: %v", v, err)
 		}
 
-		err = json.Unmarshal(b, &options)
+		err = util.UnmarshalJSON(b, &options)
 		if err != nil {
 			return nil, fmt.Errorf("moss store,"+
 				" could not unmarshal config[mossCollectionOptions]: %v, err: %v", v, err)
diff --git a/index/upsidedown/upsidedown.go b/index/upsidedown/upsidedown.go
index 6e14e657..3756422d 100644
--- a/index/upsidedown/upsidedown.go
+++ b/index/upsidedown/upsidedown.go
@@ -135,22 +135,22 @@ func (udc *UpsideDownCouch) loadSchema(kvreader store.KVReader) (err error) {
 }
 
 type rowBuffer struct {
-    buf []byte
+	buf []byte
 }
 
 var rowBufferPool sync.Pool
 
 func GetRowBuffer() *rowBuffer {
-    if rb, ok := rowBufferPool.Get().(*rowBuffer); ok {
-        return rb
-    } else {
-        buf := make([]byte, RowBufferSize)
-        return &rowBuffer{buf: buf}
-    }
+	if rb, ok := rowBufferPool.Get().(*rowBuffer); ok {
+		return rb
+	} else {
+		buf := make([]byte, RowBufferSize)
+		return &rowBuffer{buf: buf}
+	}
 }
 
 func PutRowBuffer(rb *rowBuffer) {
-    rowBufferPool.Put(rb)
+	rowBufferPool.Put(rb)
 }
 
 func (udc *UpsideDownCouch) batchRows(writer store.KVWriter, addRowsAll [][]UpsideDownCouchRow, updateRowsAll [][]UpsideDownCouchRow, deleteRowsAll [][]UpsideDownCouchRow) (err error) {
diff --git a/index_impl.go b/index_impl.go
index b5d6efb9..d5f34a2a 100644
--- a/index_impl.go
+++ b/index_impl.go
@@ -16,11 +16,11 @@ package bleve
 
 import (
 	"context"
-	"encoding/json"
 	"fmt"
 	"io"
 	"os"
 	"path/filepath"
+	"strconv"
 	"sync"
 	"sync/atomic"
 	"time"
@@ -34,7 +34,9 @@ import (
 	"github.com/blevesearch/bleve/v2/search/collector"
 	"github.com/blevesearch/bleve/v2/search/facet"
 	"github.com/blevesearch/bleve/v2/search/highlight"
+	"github.com/blevesearch/bleve/v2/util"
 	index "github.com/blevesearch/bleve_index_api"
+	"github.com/blevesearch/geo/s2"
 )
 
 type indexImpl struct {
@@ -118,7 +120,7 @@ func newIndexUsing(path string, mapping mapping.IndexMapping, indexType string,
 	}(&rv)
 
 	// now persist the mapping
-	mappingBytes, err := json.Marshal(mapping)
+	mappingBytes, err := util.MarshalJSON(mapping)
 	if err != nil {
 		return nil, err
 	}
@@ -201,7 +203,7 @@ func openIndexUsing(path string, runtimeConfig map[string]interface{}) (rv *inde
 	}
 
 	var im *mapping.IndexMappingImpl
-	err = json.Unmarshal(mappingBytes, &im)
+	err = util.UnmarshalJSON(mappingBytes, &im)
 	if err != nil {
 		return nil, fmt.Errorf("error parsing mapping JSON: %v\nmapping contents:\n%s", err, string(mappingBytes))
 	}
@@ -482,6 +484,18 @@ func (i *indexImpl) SearchInContext(ctx context.Context, req *SearchRequest) (sr
 	ctx = context.WithValue(ctx, search.SearchIOStatsCallbackKey,
 		search.SearchIOStatsCallbackFunc(sendBytesRead))
 
+	var bufPool *s2.GeoBufferPool
+	getBufferPool := func() *s2.GeoBufferPool {
+		if bufPool == nil {
+			bufPool = s2.NewGeoBufferPool(search.MaxGeoBufPoolSize, search.MinGeoBufPoolSize)
+		}
+
+		return bufPool
+	}
+
+	ctx = context.WithValue(ctx, search.GeoBufferPoolCallbackKey,
+		search.GeoBufferPoolCallbackFunc(getBufferPool))
+
 	searcher, err := req.Query.Searcher(ctx, indexReader, i.m, search.SearcherOptions{
 		Explain:            req.Explain,
 		IncludeTermVectors: req.IncludeLocations || req.Highlight != nil,
@@ -517,10 +531,23 @@ func (i *indexImpl) SearchInContext(ctx context.Context, req *SearchRequest) (sr
 			} else if facetRequest.DateTimeRanges != nil {
 				// build date range facet
 				facetBuilder := facet.NewDateTimeFacetBuilder(facetRequest.Field, facetRequest.Size)
-				dateTimeParser := i.m.DateTimeParserNamed("")
 				for _, dr := range facetRequest.DateTimeRanges {
-					start, end := dr.ParseDates(dateTimeParser)
-					facetBuilder.AddRange(dr.Name, start, end)
+					dateTimeParserName := defaultDateTimeParser
+					if dr.DateTimeParser != "" {
+						dateTimeParserName = dr.DateTimeParser
+					}
+					dateTimeParser := i.m.DateTimeParserNamed(dateTimeParserName)
+					if dateTimeParser == nil {
+						return nil, fmt.Errorf("no date time parser named `%s` registered", dateTimeParserName)
+					}
+					start, end, startLayout, endLayout, err := dr.ParseDates(dateTimeParser)
+					if err != nil {
+						return nil, fmt.Errorf("ParseDates err: %v, using date time parser named %s", err, dateTimeParserName)
+					}
+					if start.IsZero() && end.IsZero() {
+						return nil, fmt.Errorf("date range query must specify either start, end or both for date range name '%s'", dr.Name)
+					}
+					facetBuilder.AddRange(dr.Name, start, end, startLayout, endLayout)
 				}
 				facetsBuilder.Add(facetName, facetBuilder)
 			} else {
@@ -652,7 +679,7 @@ func LoadAndHighlightFields(hit *search.DocumentMatch, req *SearchRequest,
 								if err == nil {
 									if layout == "" {
 										// layout not set probably means it was indexed as a timestamp
-										value = datetime.UnixNano()
+										value = strconv.FormatInt(datetime.UnixNano(), 10)
 									} else {
 										value = datetime.Format(layout)
 									}
diff --git a/index_meta.go b/index_meta.go
index 90c7fffa..14b88dcb 100644
--- a/index_meta.go
+++ b/index_meta.go
@@ -15,12 +15,12 @@
 package bleve
 
 import (
-	"encoding/json"
 	"fmt"
 	"os"
 	"path/filepath"
 
 	"github.com/blevesearch/bleve/v2/index/upsidedown"
+	"github.com/blevesearch/bleve/v2/util"
 	index "github.com/blevesearch/bleve_index_api"
 )
 
@@ -50,7 +50,7 @@ func openIndexMeta(path string) (*indexMeta, error) {
 		return nil, ErrorIndexMetaMissing
 	}
 	var im indexMeta
-	err = json.Unmarshal(metaBytes, &im)
+	err = util.UnmarshalJSON(metaBytes, &im)
 	if err != nil {
 		return nil, ErrorIndexMetaCorrupt
 	}
@@ -70,7 +70,7 @@ func (i *indexMeta) Save(path string) (err error) {
 		}
 		return err
 	}
-	metaBytes, err := json.Marshal(i)
+	metaBytes, err := util.MarshalJSON(i)
 	if err != nil {
 		return err
 	}
@@ -94,7 +94,7 @@ func (i *indexMeta) Save(path string) (err error) {
 }
 
 func (i *indexMeta) CopyTo(d index.Directory) (err error) {
-	metaBytes, err := json.Marshal(i)
+	metaBytes, err := util.MarshalJSON(i)
 	if err != nil {
 		return err
 	}
diff --git a/mapping/document.go b/mapping/document.go
index 226e7a31..aacaa0a5 100644
--- a/mapping/document.go
+++ b/mapping/document.go
@@ -23,6 +23,7 @@ import (
 	"time"
 
 	"github.com/blevesearch/bleve/v2/registry"
+	"github.com/blevesearch/bleve/v2/util"
 )
 
 // A DocumentMapping describes how a type of document
@@ -238,7 +239,7 @@ func (dm *DocumentMapping) AddFieldMapping(fm *FieldMapping) {
 // UnmarshalJSON offers custom unmarshaling with optional strict validation
 func (dm *DocumentMapping) UnmarshalJSON(data []byte) error {
 	var tmp map[string]json.RawMessage
-	err := json.Unmarshal(data, &tmp)
+	err := util.UnmarshalJSON(data, &tmp)
 	if err != nil {
 		return err
 	}
@@ -251,32 +252,32 @@ func (dm *DocumentMapping) UnmarshalJSON(data []byte) error {
 	for k, v := range tmp {
 		switch k {
 		case "enabled":
-			err := json.Unmarshal(v, &dm.Enabled)
+			err := util.UnmarshalJSON(v, &dm.Enabled)
 			if err != nil {
 				return err
 			}
 		case "dynamic":
-			err := json.Unmarshal(v, &dm.Dynamic)
+			err := util.UnmarshalJSON(v, &dm.Dynamic)
 			if err != nil {
 				return err
 			}
 		case "default_analyzer":
-			err := json.Unmarshal(v, &dm.DefaultAnalyzer)
+			err := util.UnmarshalJSON(v, &dm.DefaultAnalyzer)
 			if err != nil {
 				return err
 			}
 		case "properties":
-			err := json.Unmarshal(v, &dm.Properties)
+			err := util.UnmarshalJSON(v, &dm.Properties)
 			if err != nil {
 				return err
 			}
 		case "fields":
-			err := json.Unmarshal(v, &dm.Fields)
+			err := util.UnmarshalJSON(v, &dm.Fields)
 			if err != nil {
 				return err
 			}
 		case "struct_tag_key":
-			err := json.Unmarshal(v, &dm.StructTagKey)
+			err := util.UnmarshalJSON(v, &dm.StructTagKey)
 			if err != nil {
 				return err
 			}
diff --git a/mapping/field.go b/mapping/field.go
index 9873b1dd..82d51f31 100644
--- a/mapping/field.go
+++ b/mapping/field.go
@@ -20,12 +20,12 @@ import (
 	"net"
 	"time"
 
-	"github.com/blevesearch/bleve/v2/analysis/analyzer/keyword"
-	index "github.com/blevesearch/bleve_index_api"
-
 	"github.com/blevesearch/bleve/v2/analysis"
+	"github.com/blevesearch/bleve/v2/analysis/analyzer/keyword"
 	"github.com/blevesearch/bleve/v2/document"
 	"github.com/blevesearch/bleve/v2/geo"
+	"github.com/blevesearch/bleve/v2/util"
+	index "github.com/blevesearch/bleve_index_api"
 )
 
 // control the default behavior for dynamic fields (those not explicitly mapped)
@@ -390,7 +390,7 @@ func getFieldName(pathString string, path []string, fieldMapping *FieldMapping)
 func (fm *FieldMapping) UnmarshalJSON(data []byte) error {
 
 	var tmp map[string]json.RawMessage
-	err := json.Unmarshal(data, &tmp)
+	err := util.UnmarshalJSON(data, &tmp)
 	if err != nil {
 		return err
 	}
@@ -399,52 +399,52 @@ func (fm *FieldMapping) UnmarshalJSON(data []byte) error {
 	for k, v := range tmp {
 		switch k {
 		case "name":
-			err := json.Unmarshal(v, &fm.Name)
+			err := util.UnmarshalJSON(v, &fm.Name)
 			if err != nil {
 				return err
 			}
 		case "type":
-			err := json.Unmarshal(v, &fm.Type)
+			err := util.UnmarshalJSON(v, &fm.Type)
 			if err != nil {
 				return err
 			}
 		case "analyzer":
-			err := json.Unmarshal(v, &fm.Analyzer)
+			err := util.UnmarshalJSON(v, &fm.Analyzer)
 			if err != nil {
 				return err
 			}
 		case "store":
-			err := json.Unmarshal(v, &fm.Store)
+			err := util.UnmarshalJSON(v, &fm.Store)
 			if err != nil {
 				return err
 			}
 		case "index":
-			err := json.Unmarshal(v, &fm.Index)
+			err := util.UnmarshalJSON(v, &fm.Index)
 			if err != nil {
 				return err
 			}
 		case "include_term_vectors":
-			err := json.Unmarshal(v, &fm.IncludeTermVectors)
+			err := util.UnmarshalJSON(v, &fm.IncludeTermVectors)
 			if err != nil {
 				return err
 			}
 		case "include_in_all":
-			err := json.Unmarshal(v, &fm.IncludeInAll)
+			err := util.UnmarshalJSON(v, &fm.IncludeInAll)
 			if err != nil {
 				return err
 			}
 		case "date_format":
-			err := json.Unmarshal(v, &fm.DateFormat)
+			err := util.UnmarshalJSON(v, &fm.DateFormat)
 			if err != nil {
 				return err
 			}
 		case "docvalues":
-			err := json.Unmarshal(v, &fm.DocValues)
+			err := util.UnmarshalJSON(v, &fm.DocValues)
 			if err != nil {
 				return err
 			}
 		case "skip_freq_norm":
-			err := json.Unmarshal(v, &fm.SkipFreqNorm)
+			err := util.UnmarshalJSON(v, &fm.SkipFreqNorm)
 			if err != nil {
 				return err
 			}
diff --git a/mapping/index.go b/mapping/index.go
index e2ac99f3..0de4147a 100644
--- a/mapping/index.go
+++ b/mapping/index.go
@@ -17,13 +17,14 @@ package mapping
 import (
 	"encoding/json"
 	"fmt"
-	index "github.com/blevesearch/bleve_index_api"
 
 	"github.com/blevesearch/bleve/v2/analysis"
 	"github.com/blevesearch/bleve/v2/analysis/analyzer/standard"
 	"github.com/blevesearch/bleve/v2/analysis/datetime/optional"
 	"github.com/blevesearch/bleve/v2/document"
 	"github.com/blevesearch/bleve/v2/registry"
+	"github.com/blevesearch/bleve/v2/util"
+	index "github.com/blevesearch/bleve_index_api"
 )
 
 var MappingJSONStrict = false
@@ -203,7 +204,7 @@ func (im *IndexMappingImpl) mappingForType(docType string) *DocumentMapping {
 func (im *IndexMappingImpl) UnmarshalJSON(data []byte) error {
 
 	var tmp map[string]json.RawMessage
-	err := json.Unmarshal(data, &tmp)
+	err := util.UnmarshalJSON(data, &tmp)
 	if err != nil {
 		return err
 	}
@@ -226,57 +227,57 @@ func (im *IndexMappingImpl) UnmarshalJSON(data []byte) error {
 	for k, v := range tmp {
 		switch k {
 		case "analysis":
-			err := json.Unmarshal(v, &im.CustomAnalysis)
+			err := util.UnmarshalJSON(v, &im.CustomAnalysis)
 			if err != nil {
 				return err
 			}
 		case "type_field":
-			err := json.Unmarshal(v, &im.TypeField)
+			err := util.UnmarshalJSON(v, &im.TypeField)
 			if err != nil {
 				return err
 			}
 		case "default_type":
-			err := json.Unmarshal(v, &im.DefaultType)
+			err := util.UnmarshalJSON(v, &im.DefaultType)
 			if err != nil {
 				return err
 			}
 		case "default_analyzer":
-			err := json.Unmarshal(v, &im.DefaultAnalyzer)
+			err := util.UnmarshalJSON(v, &im.DefaultAnalyzer)
 			if err != nil {
 				return err
 			}
 		case "default_datetime_parser":
-			err := json.Unmarshal(v, &im.DefaultDateTimeParser)
+			err := util.UnmarshalJSON(v, &im.DefaultDateTimeParser)
 			if err != nil {
 				return err
 			}
 		case "default_field":
-			err := json.Unmarshal(v, &im.DefaultField)
+			err := util.UnmarshalJSON(v, &im.DefaultField)
 			if err != nil {
 				return err
 			}
 		case "default_mapping":
-			err := json.Unmarshal(v, &im.DefaultMapping)
+			err := util.UnmarshalJSON(v, &im.DefaultMapping)
 			if err != nil {
 				return err
 			}
 		case "types":
-			err := json.Unmarshal(v, &im.TypeMapping)
+			err := util.UnmarshalJSON(v, &im.TypeMapping)
 			if err != nil {
 				return err
 			}
 		case "store_dynamic":
-			err := json.Unmarshal(v, &im.StoreDynamic)
+			err := util.UnmarshalJSON(v, &im.StoreDynamic)
 			if err != nil {
 				return err
 			}
 		case "index_dynamic":
-			err := json.Unmarshal(v, &im.IndexDynamic)
+			err := util.UnmarshalJSON(v, &im.IndexDynamic)
 			if err != nil {
 				return err
 			}
 		case "docvalues_dynamic":
-			err := json.Unmarshal(v, &im.DocValuesDynamic)
+			err := util.UnmarshalJSON(v, &im.DocValuesDynamic)
 			if err != nil {
 				return err
 			}
@@ -417,23 +418,6 @@ func (im *IndexMappingImpl) DateTimeParserNamed(name string) analysis.DateTimePa
 	return dateTimeParser
 }
 
-func (im *IndexMappingImpl) datetimeParserNameForPath(path string) string {
-
-	// first we look for explicit mapping on the field
-	for _, docMapping := range im.TypeMapping {
-		pathMapping, _ := docMapping.documentMappingForPath(path)
-		if pathMapping != nil {
-			if len(pathMapping.Fields) > 0 {
-				if pathMapping.Fields[0].Analyzer != "" {
-					return pathMapping.Fields[0].Analyzer
-				}
-			}
-		}
-	}
-
-	return im.DefaultDateTimeParser
-}
-
 func (im *IndexMappingImpl) AnalyzeText(analyzerName string, text []byte) (analysis.TokenStream, error) {
 	analyzer, err := im.cache.AnalyzerNamed(analyzerName)
 	if err != nil {
diff --git a/query.go b/query.go
index 3385ed77..e18026ec 100644
--- a/query.go
+++ b/query.go
@@ -48,7 +48,9 @@ func NewConjunctionQuery(conjuncts ...query.Query) *query.ConjunctionQuery {
 // NewDateRangeQuery creates a new Query for ranges
 // of date values.
 // Date strings are parsed using the DateTimeParser configured in the
-//  top-level config.QueryDateTimeParser
+//
+//	top-level config.QueryDateTimeParser
+//
 // Either, but not both endpoints can be nil.
 func NewDateRangeQuery(start, end time.Time) *query.DateRangeQuery {
 	return query.NewDateRangeQuery(start, end)
@@ -57,7 +59,9 @@ func NewDateRangeQuery(start, end time.Time) *query.DateRangeQuery {
 // NewDateRangeInclusiveQuery creates a new Query for ranges
 // of date values.
 // Date strings are parsed using the DateTimeParser configured in the
-//  top-level config.QueryDateTimeParser
+//
+//	top-level config.QueryDateTimeParser
+//
 // Either, but not both endpoints can be nil.
 // startInclusive and endInclusive control inclusion of the endpoints.
 func NewDateRangeInclusiveQuery(start, end time.Time, startInclusive, endInclusive *bool) *query.DateRangeQuery {
diff --git a/search.go b/search.go
index db5ced3f..8ca0310f 100644
--- a/search.go
+++ b/search.go
@@ -29,63 +29,65 @@ import (
 	"github.com/blevesearch/bleve/v2/search/collector"
 	"github.com/blevesearch/bleve/v2/search/query"
 	"github.com/blevesearch/bleve/v2/size"
+	"github.com/blevesearch/bleve/v2/util"
 )
 
-var reflectStaticSizeSearchResult int
-var reflectStaticSizeSearchStatus int
-
-func init() {
-	var sr SearchResult
-	reflectStaticSizeSearchResult = int(reflect.TypeOf(sr).Size())
-	var ss SearchStatus
-	reflectStaticSizeSearchStatus = int(reflect.TypeOf(ss).Size())
-}
+const defaultDateTimeParser = optional.Name
 
 var cache = registry.NewCache()
 
-const defaultDateTimeParser = optional.Name
+var (
+	reflectStaticSizeSearchResult int
+	reflectStaticSizeSearchStatus int
+)
 
-type numericRange struct {
-	Name string   `json:"name,omitempty"`
-	Min  *float64 `json:"min,omitempty"`
-	Max  *float64 `json:"max,omitempty"`
+func init() {
+	reflectStaticSizeSearchResult = int(reflect.TypeOf(SearchResult{}).Size())
+	reflectStaticSizeSearchStatus = int(reflect.TypeOf(SearchStatus{}).Size())
 }
 
 type dateTimeRange struct {
-	Name        string    `json:"name,omitempty"`
-	Start       time.Time `json:"start,omitempty"`
-	End         time.Time `json:"end,omitempty"`
-	startString *string
-	endString   *string
+	Name           string    `json:"name,omitempty"`
+	Start          time.Time `json:"start,omitempty"`
+	End            time.Time `json:"end,omitempty"`
+	DateTimeParser string    `json:"datetime_parser,omitempty"`
+	startString    *string
+	endString      *string
 }
 
-func (dr *dateTimeRange) ParseDates(dateTimeParser analysis.DateTimeParser) (start, end time.Time) {
+func (dr *dateTimeRange) ParseDates(dateTimeParser analysis.DateTimeParser) (start, end time.Time, startLayout, endLayout string, err error) {
 	start = dr.Start
+	startLayout = time.RFC3339Nano
 	if dr.Start.IsZero() && dr.startString != nil {
-		s, _, err := dateTimeParser.ParseDateTime(*dr.startString)
-		if err == nil {
-			start = s
+		s, layout, parseError := dateTimeParser.ParseDateTime(*dr.startString)
+		if parseError != nil {
+			return start, end, startLayout, endLayout, fmt.Errorf("error parsing start date '%s' for date range name '%s': %v", *dr.startString, dr.Name, parseError)
 		}
+		start = s
+		startLayout = layout
 	}
 	end = dr.End
+	endLayout = time.RFC3339Nano
 	if dr.End.IsZero() && dr.endString != nil {
-		e, _, err := dateTimeParser.ParseDateTime(*dr.endString)
-		if err == nil {
-			end = e
+		e, layout, parseError := dateTimeParser.ParseDateTime(*dr.endString)
+		if parseError != nil {
+			return start, end, startLayout, endLayout, fmt.Errorf("error parsing end date '%s' for date range name '%s': %v", *dr.endString, dr.Name, parseError)
 		}
+		end = e
+		endLayout = layout
 	}
-	return start, end
+	return start, end, startLayout, endLayout, err
 }
 
 func (dr *dateTimeRange) UnmarshalJSON(input []byte) error {
 	var temp struct {
-		Name  string  `json:"name,omitempty"`
-		Start *string `json:"start,omitempty"`
-		End   *string `json:"end,omitempty"`
+		Name           string  `json:"name,omitempty"`
+		Start          *string `json:"start,omitempty"`
+		End            *string `json:"end,omitempty"`
+		DateTimeParser string  `json:"datetime_parser,omitempty"`
 	}
 
-	err := json.Unmarshal(input, &temp)
-	if err != nil {
+	if err := util.UnmarshalJSON(input, &temp); err != nil {
 		return err
 	}
 
@@ -96,23 +98,40 @@ func (dr *dateTimeRange) UnmarshalJSON(input []byte) error {
 	if temp.End != nil {
 		dr.endString = temp.End
 	}
+	if temp.DateTimeParser != "" {
+		dr.DateTimeParser = temp.DateTimeParser
+	}
 
 	return nil
 }
 
 func (dr *dateTimeRange) MarshalJSON() ([]byte, error) {
 	rv := map[string]interface{}{
-		"name":  dr.Name,
-		"start": dr.Start,
-		"end":   dr.End,
+		"name": dr.Name,
 	}
-	if dr.Start.IsZero() && dr.startString != nil {
+
+	if !dr.Start.IsZero() {
+		rv["start"] = dr.Start
+	} else if dr.startString != nil {
 		rv["start"] = dr.startString
 	}
-	if dr.End.IsZero() && dr.endString != nil {
+
+	if !dr.End.IsZero() {
+		rv["end"] = dr.End
+	} else if dr.endString != nil {
 		rv["end"] = dr.endString
 	}
-	return json.Marshal(rv)
+
+	if dr.DateTimeParser != "" {
+		rv["datetime_parser"] = dr.DateTimeParser
+	}
+	return util.MarshalJSON(rv)
+}
+
+type numericRange struct {
+	Name string   `json:"name,omitempty"`
+	Min  *float64 `json:"min,omitempty"`
+	Max  *float64 `json:"max,omitempty"`
 }
 
 // A FacetRequest describes a facet or aggregation
@@ -125,11 +144,21 @@ type FacetRequest struct {
 	DateTimeRanges []*dateTimeRange `json:"date_ranges,omitempty"`
 }
 
+// NewFacetRequest creates a facet on the specified
+// field that limits the number of entries to the
+// specified size.
+func NewFacetRequest(field string, size int) *FacetRequest {
+	return &FacetRequest{
+		Field: field,
+		Size:  size,
+	}
+}
+
 func (fr *FacetRequest) Validate() error {
 	nrCount := len(fr.NumericRanges)
 	drCount := len(fr.DateTimeRanges)
 	if nrCount > 0 && drCount > 0 {
-		return fmt.Errorf("facet can only conain numeric ranges or date ranges, not both")
+		return fmt.Errorf("facet can only contain numeric ranges or date ranges, not both")
 	}
 
 	if nrCount > 0 {
@@ -155,23 +184,21 @@ func (fr *FacetRequest) Validate() error {
 				return fmt.Errorf("date ranges contains duplicate name '%s'", dr.Name)
 			}
 			drNames[dr.Name] = struct{}{}
-			start, end := dr.ParseDates(dateTimeParser)
-			if start.IsZero() && end.IsZero() {
-				return fmt.Errorf("date range query must specify either start, end or both for range name '%s'", dr.Name)
+			if dr.DateTimeParser == "" {
+				// cannot parse the date range dates as the defaultDateTimeParser is overridden
+				// so perform this validation at query time
+				start, end, _, _, err := dr.ParseDates(dateTimeParser)
+				if err != nil {
+					return fmt.Errorf("ParseDates err: %v, using date time parser named %s", err, defaultDateTimeParser)
+				}
+				if start.IsZero() && end.IsZero() {
+					return fmt.Errorf("date range query must specify either start, end or both for range name '%s'", dr.Name)
+				}
 			}
 		}
 	}
-	return nil
-}
 
-// NewFacetRequest creates a facet on the specified
-// field that limits the number of entries to the
-// specified size.
-func NewFacetRequest(field string, size int) *FacetRequest {
-	return &FacetRequest{
-		Field: field,
-		Size:  size,
-	}
+	return nil
 }
 
 // AddDateTimeRange adds a bucket to a field
@@ -186,7 +213,7 @@ func (fr *FacetRequest) AddDateTimeRange(name string, start, end time.Time) {
 }
 
 // AddDateTimeRangeString adds a bucket to a field
-// containing date values.
+// containing date values. Uses defaultDateTimeParser to parse the date strings.
 func (fr *FacetRequest) AddDateTimeRangeString(name string, start, end *string) {
 	if fr.DateTimeRanges == nil {
 		fr.DateTimeRanges = make([]*dateTimeRange, 0, 1)
@@ -195,6 +222,17 @@ func (fr *FacetRequest) AddDateTimeRangeString(name string, start, end *string)
 		&dateTimeRange{Name: name, startString: start, endString: end})
 }
 
+// AddDateTimeRangeString adds a bucket to a field
+// containing date values. Uses the specified parser to parse the date strings.
+// provided the parser is registered in the index mapping.
+func (fr *FacetRequest) AddDateTimeRangeStringWithParser(name string, start, end *string, parser string) {
+	if fr.DateTimeRanges == nil {
+		fr.DateTimeRanges = make([]*dateTimeRange, 0, 1)
+	}
+	fr.DateTimeRanges = append(fr.DateTimeRanges,
+		&dateTimeRange{Name: name, startString: start, endString: end, DateTimeParser: parser})
+}
+
 // AddNumericRange adds a bucket to a field
 // containing numeric values.  Documents with a
 // numeric value falling into this range are
@@ -212,8 +250,7 @@ type FacetsRequest map[string]*FacetRequest
 
 func (fr FacetsRequest) Validate() error {
 	for _, v := range fr {
-		err := v.Validate()
-		if err != nil {
+		if err := v.Validate(); err != nil {
 			return err
 		}
 	}
@@ -269,6 +306,7 @@ func (h *HighlightRequest) AddField(field string) {
 //
 // A special field named "*" can be used to return all fields.
 type SearchRequest struct {
+	ClientContextID  string            `json:"client_context_id,omitempty"`
 	Query            query.Query       `json:"query"`
 	Size             int               `json:"size"`
 	From             int               `json:"from"`
@@ -285,10 +323,13 @@ type SearchRequest struct {
 	sortFunc func(sort.Interface)
 }
 
+func (r *SearchRequest) SetClientContextID(id string) {
+	r.ClientContextID = id
+}
+
 func (r *SearchRequest) Validate() error {
 	if srq, ok := r.Query.(query.ValidatableQuery); ok {
-		err := srq.Validate()
-		if err != nil {
+		if err := srq.Validate(); err != nil {
 			return err
 		}
 	}
@@ -355,23 +396,26 @@ func (r *SearchRequest) SetSearchBefore(before []string) {
 // UnmarshalJSON deserializes a JSON representation of
 // a SearchRequest
 func (r *SearchRequest) UnmarshalJSON(input []byte) error {
-	var temp struct {
-		Q                json.RawMessage   `json:"query"`
-		Size             *int              `json:"size"`
-		From             int               `json:"from"`
-		Highlight        *HighlightRequest `json:"highlight"`
-		Fields           []string          `json:"fields"`
-		Facets           FacetsRequest     `json:"facets"`
-		Explain          bool              `json:"explain"`
-		Sort             []json.RawMessage `json:"sort"`
-		IncludeLocations bool              `json:"includeLocations"`
-		Score            string            `json:"score"`
-		SearchAfter      []string          `json:"search_after"`
-		SearchBefore     []string          `json:"search_before"`
-	}
-
-	err := json.Unmarshal(input, &temp)
-	if err != nil {
+	var (
+		temp struct {
+			ClientContextID  string            `json:"client_context_id"`
+			Q                json.RawMessage   `json:"query"`
+			Size             *int              `json:"size"`
+			From             int               `json:"from"`
+			Highlight        *HighlightRequest `json:"highlight"`
+			Fields           []string          `json:"fields"`
+			Facets           FacetsRequest     `json:"facets"`
+			Explain          bool              `json:"explain"`
+			Sort             []json.RawMessage `json:"sort"`
+			IncludeLocations bool              `json:"includeLocations"`
+			Score            string            `json:"score"`
+			SearchAfter      []string          `json:"search_after"`
+			SearchBefore     []string          `json:"search_before"`
+		}
+		err error
+	)
+
+	if err = util.UnmarshalJSON(input, &temp); err != nil {
 		return err
 	}
 
@@ -383,11 +427,11 @@ func (r *SearchRequest) UnmarshalJSON(input []byte) error {
 	if temp.Sort == nil {
 		r.Sort = search.SortOrder{&search.SortScore{Desc: true}}
 	} else {
-		r.Sort, err = search.ParseSortOrderJSON(temp.Sort)
-		if err != nil {
+		if r.Sort, err = search.ParseSortOrderJSON(temp.Sort); err != nil {
 			return err
 		}
 	}
+	r.ClientContextID = temp.ClientContextID
 	r.From = temp.From
 	r.Explain = temp.Explain
 	r.Highlight = temp.Highlight
@@ -397,8 +441,7 @@ func (r *SearchRequest) UnmarshalJSON(input []byte) error {
 	r.Score = temp.Score
 	r.SearchAfter = temp.SearchAfter
 	r.SearchBefore = temp.SearchBefore
-	r.Query, err = query.ParseQuery(temp.Q)
-	if err != nil {
+	if r.Query, err = query.ParseQuery(temp.Q); err != nil {
 		return err
 	}
 
@@ -443,13 +486,12 @@ func (iem IndexErrMap) MarshalJSON() ([]byte, error) {
 	for k, v := range iem {
 		tmp[k] = v.Error()
 	}
-	return json.Marshal(tmp)
+	return util.MarshalJSON(tmp)
 }
 
 func (iem IndexErrMap) UnmarshalJSON(data []byte) error {
 	var tmp map[string]string
-	err := json.Unmarshal(data, &tmp)
-	if err != nil {
+	if err := util.UnmarshalJSON(data, &tmp); err != nil {
 		return err
 	}
 	for k, v := range tmp {
diff --git a/search/facet/facet_builder_datetime.go b/search/facet/facet_builder_datetime.go
index ff5167f2..c272396b 100644
--- a/search/facet/facet_builder_datetime.go
+++ b/search/facet/facet_builder_datetime.go
@@ -17,6 +17,7 @@ package facet
 import (
 	"reflect"
 	"sort"
+	"strconv"
 	"time"
 
 	"github.com/blevesearch/bleve/v2/numeric"
@@ -35,8 +36,10 @@ func init() {
 }
 
 type dateTimeRange struct {
-	start time.Time
-	end   time.Time
+	start       time.Time
+	end         time.Time
+	startLayout string
+	endLayout   string
 }
 
 type DateTimeFacetBuilder struct {
@@ -75,10 +78,12 @@ func (fb *DateTimeFacetBuilder) Size() int {
 	return sizeInBytes
 }
 
-func (fb *DateTimeFacetBuilder) AddRange(name string, start, end time.Time) {
+func (fb *DateTimeFacetBuilder) AddRange(name string, start, end time.Time, startLayout string, endLayout string) {
 	r := dateTimeRange{
-		start: start,
-		end:   end,
+		start:       start,
+		end:         end,
+		startLayout: startLayout,
+		endLayout:   endLayout,
 	}
 	fb.ranges[name] = &r
 }
@@ -134,11 +139,23 @@ func (fb *DateTimeFacetBuilder) Result() *search.FacetResult {
 			Count: count,
 		}
 		if !dateRange.start.IsZero() {
-			start := dateRange.start.Format(time.RFC3339Nano)
+			var start string
+			if dateRange.startLayout == "" {
+				// layout not set probably means it is probably a timestamp
+				start = strconv.FormatInt(dateRange.start.UnixNano(), 10)
+			} else {
+				start = dateRange.start.Format(dateRange.startLayout)
+			}
 			tf.Start = &start
 		}
 		if !dateRange.end.IsZero() {
-			end := dateRange.end.Format(time.RFC3339Nano)
+			var end string
+			if dateRange.endLayout == "" {
+				// layout not set probably means it is probably a timestamp
+				end = strconv.FormatInt(dateRange.end.UnixNano(), 10)
+			} else {
+				end = dateRange.end.Format(dateRange.endLayout)
+			}
 			tf.End = &end
 		}
 		rv.DateRanges = append(rv.DateRanges, tf)
diff --git a/search/facets_builder.go b/search/facets_builder.go
index de5a1574..ebe785c0 100644
--- a/search/facets_builder.go
+++ b/search/facets_builder.go
@@ -15,11 +15,11 @@
 package search
 
 import (
-	"encoding/json"
 	"reflect"
 	"sort"
 
 	"github.com/blevesearch/bleve/v2/size"
+	"github.com/blevesearch/bleve/v2/util"
 	index "github.com/blevesearch/bleve_index_api"
 )
 
@@ -176,12 +176,12 @@ func (tf *TermFacets) Less(i, j int) bool {
 // To maintain backwards compatibility, we have to implement custom
 // JSON marshalling.
 func (tf *TermFacets) MarshalJSON() ([]byte, error) {
-	return json.Marshal(tf.termFacets)
+	return util.MarshalJSON(tf.termFacets)
 }
 
 func (tf *TermFacets) UnmarshalJSON(b []byte) error {
 	termFacets := []*TermFacet{}
-	err := json.Unmarshal(b, &termFacets)
+	err := util.UnmarshalJSON(b, &termFacets)
 	if err != nil {
 		return err
 	}
diff --git a/search/query/boolean.go b/search/query/boolean.go
index b5e1fdc4..026a5868 100644
--- a/search/query/boolean.go
+++ b/search/query/boolean.go
@@ -22,6 +22,7 @@ import (
 	"github.com/blevesearch/bleve/v2/mapping"
 	"github.com/blevesearch/bleve/v2/search"
 	"github.com/blevesearch/bleve/v2/search/searcher"
+	"github.com/blevesearch/bleve/v2/util"
 	index "github.com/blevesearch/bleve_index_api"
 )
 
@@ -205,7 +206,7 @@ func (q *BooleanQuery) UnmarshalJSON(data []byte) error {
 		MustNot json.RawMessage `json:"must_not,omitempty"`
 		Boost   *Boost          `json:"boost,omitempty"`
 	}{}
-	err := json.Unmarshal(data, &tmp)
+	err := util.UnmarshalJSON(data, &tmp)
 	if err != nil {
 		return err
 	}
diff --git a/search/query/conjunction.go b/search/query/conjunction.go
index 27bec7d6..0565e18f 100644
--- a/search/query/conjunction.go
+++ b/search/query/conjunction.go
@@ -21,6 +21,7 @@ import (
 	"github.com/blevesearch/bleve/v2/mapping"
 	"github.com/blevesearch/bleve/v2/search"
 	"github.com/blevesearch/bleve/v2/search/searcher"
+	"github.com/blevesearch/bleve/v2/util"
 	index "github.com/blevesearch/bleve_index_api"
 )
 
@@ -96,7 +97,7 @@ func (q *ConjunctionQuery) UnmarshalJSON(data []byte) error {
 		Conjuncts []json.RawMessage `json:"conjuncts"`
 		Boost     *Boost            `json:"boost,omitempty"`
 	}{}
-	err := json.Unmarshal(data, &tmp)
+	err := util.UnmarshalJSON(data, &tmp)
 	if err != nil {
 		return err
 	}
diff --git a/search/query/date_range.go b/search/query/date_range.go
index 34844c97..47012fb1 100644
--- a/search/query/date_range.go
+++ b/search/query/date_range.go
@@ -16,7 +16,6 @@ package query
 
 import (
 	"context"
-	"encoding/json"
 	"fmt"
 	"math"
 	"time"
@@ -27,13 +26,14 @@ import (
 	"github.com/blevesearch/bleve/v2/registry"
 	"github.com/blevesearch/bleve/v2/search"
 	"github.com/blevesearch/bleve/v2/search/searcher"
+	"github.com/blevesearch/bleve/v2/util"
 	index "github.com/blevesearch/bleve_index_api"
 )
 
-// QueryDateTimeParser controls the default query date time parser
+// QueryDateTimeParser controls the default query date time parser.
 var QueryDateTimeParser = optional.Name
 
-// QueryDateTimeFormat controls the format when Marshaling to JSON
+// QueryDateTimeFormat controls the format when Marshaling to JSON.
 var QueryDateTimeFormat = time.RFC3339
 
 var cache = registry.NewCache()
@@ -69,7 +69,7 @@ func (t *BleveQueryTime) MarshalJSON() ([]byte, error) {
 
 func (t *BleveQueryTime) UnmarshalJSON(data []byte) error {
 	var timeString string
-	err := json.Unmarshal(data, &timeString)
+	err := util.UnmarshalJSON(data, &timeString)
 	if err != nil {
 		return err
 	}
diff --git a/search/query/date_range_string.go b/search/query/date_range_string.go
new file mode 100644
index 00000000..b5e5c170
--- /dev/null
+++ b/search/query/date_range_string.go
@@ -0,0 +1,176 @@
+//  Copyright (c) 2023 Couchbase, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// 		http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package query
+
+import (
+	"context"
+	"fmt"
+	"math"
+	"time"
+
+	"github.com/blevesearch/bleve/v2/mapping"
+	"github.com/blevesearch/bleve/v2/numeric"
+	"github.com/blevesearch/bleve/v2/search"
+	"github.com/blevesearch/bleve/v2/search/searcher"
+	index "github.com/blevesearch/bleve_index_api"
+)
+
+// DateRangeStringQuery represents a query for a range of date values.
+// Start and End are the range endpoints, as strings.
+// Start and End are parsed using DateTimeParser, which is a custom date time parser
+// defined in the index mapping. If DateTimeParser is not specified, then the
+// top-level config.QueryDateTimeParser is used.
+type DateRangeStringQuery struct {
+	Start          string `json:"start,omitempty"`
+	End            string `json:"end,omitempty"`
+	InclusiveStart *bool  `json:"inclusive_start,omitempty"`
+	InclusiveEnd   *bool  `json:"inclusive_end,omitempty"`
+	FieldVal       string `json:"field,omitempty"`
+	BoostVal       *Boost `json:"boost,omitempty"`
+	DateTimeParser string `json:"datetime_parser,omitempty"`
+}
+
+// NewDateRangeStringQuery creates a new Query for ranges
+// of date values.
+// Date strings are parsed using the DateTimeParser field of the query struct,
+// which is a custom date time parser defined in the index mapping.
+// if DateTimeParser is not specified, then the
+// top-level config.QueryDateTimeParser is used.
+// Either, but not both endpoints can be nil.
+func NewDateRangeStringQuery(start, end string) *DateRangeStringQuery {
+	return NewDateRangeStringInclusiveQuery(start, end, nil, nil)
+}
+
+// NewDateRangeStringQuery creates a new Query for ranges
+// of date values.
+// Date strings are parsed using the DateTimeParser field of the query struct,
+// which is a custom date time parser defined in the index mapping.
+// if DateTimeParser is not specified, then the
+// top-level config.QueryDateTimeParser is used.
+// Either, but not both endpoints can be nil.
+// startInclusive and endInclusive control inclusion of the endpoints.
+func NewDateRangeStringInclusiveQuery(start, end string, startInclusive, endInclusive *bool) *DateRangeStringQuery {
+	return &DateRangeStringQuery{
+		Start:          start,
+		End:            end,
+		InclusiveStart: startInclusive,
+		InclusiveEnd:   endInclusive,
+	}
+}
+
+func (q *DateRangeStringQuery) SetBoost(b float64) {
+	boost := Boost(b)
+	q.BoostVal = &boost
+}
+
+func (q *DateRangeStringQuery) Boost() float64 {
+	return q.BoostVal.Value()
+}
+
+func (q *DateRangeStringQuery) SetField(f string) {
+	q.FieldVal = f
+}
+
+func (q *DateRangeStringQuery) Field() string {
+	return q.FieldVal
+}
+
+func (q *DateRangeStringQuery) SetDateTimeParser(d string) {
+	q.DateTimeParser = d
+}
+
+func (q *DateRangeStringQuery) DateTimeParserName() string {
+	return q.DateTimeParser
+}
+
+func (q *DateRangeStringQuery) 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()
+	}
+
+	dateTimeParserName := QueryDateTimeParser
+	if q.DateTimeParser != "" {
+		dateTimeParserName = q.DateTimeParser
+	}
+	dateTimeParser := m.DateTimeParserNamed(dateTimeParserName)
+	if dateTimeParser == nil {
+		return nil, fmt.Errorf("no dateTimeParser named '%s' registered", dateTimeParserName)
+	}
+
+	var startTime, endTime time.Time
+	var err error
+	if q.Start != "" {
+		startTime, _, err = dateTimeParser.ParseDateTime(q.Start)
+		if err != nil {
+			return nil, fmt.Errorf("%v, date time parser name: %s", err, dateTimeParserName)
+		}
+	}
+	if q.End != "" {
+		endTime, _, err = dateTimeParser.ParseDateTime(q.End)
+		if err != nil {
+			return nil, fmt.Errorf("%v, date time parser name: %s", err, dateTimeParserName)
+		}
+	}
+
+	min, max, err := q.parseEndpoints(startTime, endTime)
+	if err != nil {
+		return nil, err
+	}
+	return searcher.NewNumericRangeSearcher(ctx, i, min, max, q.InclusiveStart, q.InclusiveEnd, field, q.BoostVal.Value(), options)
+}
+
+func (q *DateRangeStringQuery) parseEndpoints(startTime, endTime time.Time) (*float64, *float64, error) {
+	min := math.Inf(-1)
+	max := math.Inf(1)
+
+	if startTime.IsZero() && endTime.IsZero() {
+		return nil, nil, fmt.Errorf("date range query must specify at least one of start/end")
+	}
+
+	if !startTime.IsZero() {
+		if !isDateTimeWithinRange(startTime) {
+			// overflow
+			return nil, nil, fmt.Errorf("invalid/unsupported date range, start: %v", q.Start)
+		}
+		startInt64 := startTime.UnixNano()
+		min = numeric.Int64ToFloat64(startInt64)
+	}
+	if !endTime.IsZero() {
+		if !isDateTimeWithinRange(endTime) {
+			// overflow
+			return nil, nil, fmt.Errorf("invalid/unsupported date range, end: %v", q.End)
+		}
+		endInt64 := endTime.UnixNano()
+		max = numeric.Int64ToFloat64(endInt64)
+	}
+
+	return &min, &max, nil
+}
+
+func (q *DateRangeStringQuery) Validate() error {
+	// either start or end must be specified
+	if q.Start == "" && q.End == "" {
+		return fmt.Errorf("date range query must specify at least one of start/end")
+	}
+	return nil
+}
+
+func isDateTimeWithinRange(t time.Time) bool {
+	if t.Before(MinRFC3339CompatibleTime) || t.After(MaxRFC3339CompatibleTime) {
+		return false
+	}
+	return true
+}
diff --git a/search/query/disjunction.go b/search/query/disjunction.go
index c6cc0d73..f8573d08 100644
--- a/search/query/disjunction.go
+++ b/search/query/disjunction.go
@@ -22,6 +22,7 @@ import (
 	"github.com/blevesearch/bleve/v2/mapping"
 	"github.com/blevesearch/bleve/v2/search"
 	"github.com/blevesearch/bleve/v2/search/searcher"
+	"github.com/blevesearch/bleve/v2/util"
 	index "github.com/blevesearch/bleve_index_api"
 )
 
@@ -107,7 +108,7 @@ func (q *DisjunctionQuery) UnmarshalJSON(data []byte) error {
 		Boost     *Boost            `json:"boost,omitempty"`
 		Min       float64           `json:"min"`
 	}{}
-	err := json.Unmarshal(data, &tmp)
+	err := util.UnmarshalJSON(data, &tmp)
 	if err != nil {
 		return err
 	}
diff --git a/search/query/geo_boundingbox.go b/search/query/geo_boundingbox.go
index 1397c779..feb45d31 100644
--- a/search/query/geo_boundingbox.go
+++ b/search/query/geo_boundingbox.go
@@ -16,13 +16,13 @@ package query
 
 import (
 	"context"
-	"encoding/json"
 	"fmt"
 
 	"github.com/blevesearch/bleve/v2/geo"
 	"github.com/blevesearch/bleve/v2/mapping"
 	"github.com/blevesearch/bleve/v2/search"
 	"github.com/blevesearch/bleve/v2/search/searcher"
+	"github.com/blevesearch/bleve/v2/util"
 	index "github.com/blevesearch/bleve_index_api"
 )
 
@@ -95,7 +95,7 @@ func (q *GeoBoundingBoxQuery) UnmarshalJSON(data []byte) error {
 		FieldVal    string      `json:"field,omitempty"`
 		BoostVal    *Boost      `json:"boost,omitempty"`
 	}{}
-	err := json.Unmarshal(data, &tmp)
+	err := util.UnmarshalJSON(data, &tmp)
 	if err != nil {
 		return err
 	}
diff --git a/search/query/geo_boundingpolygon.go b/search/query/geo_boundingpolygon.go
index baae514d..7f81a7ca 100644
--- a/search/query/geo_boundingpolygon.go
+++ b/search/query/geo_boundingpolygon.go
@@ -16,13 +16,13 @@ package query
 
 import (
 	"context"
-	"encoding/json"
 	"fmt"
 
 	"github.com/blevesearch/bleve/v2/geo"
 	"github.com/blevesearch/bleve/v2/mapping"
 	"github.com/blevesearch/bleve/v2/search"
 	"github.com/blevesearch/bleve/v2/search/searcher"
+	"github.com/blevesearch/bleve/v2/util"
 	index "github.com/blevesearch/bleve_index_api"
 )
 
@@ -76,7 +76,7 @@ func (q *GeoBoundingPolygonQuery) UnmarshalJSON(data []byte) error {
 		FieldVal string        `json:"field,omitempty"`
 		BoostVal *Boost        `json:"boost,omitempty"`
 	}{}
-	err := json.Unmarshal(data, &tmp)
+	err := util.UnmarshalJSON(data, &tmp)
 	if err != nil {
 		return err
 	}
diff --git a/search/query/geo_distance.go b/search/query/geo_distance.go
index 7977d153..2ca09642 100644
--- a/search/query/geo_distance.go
+++ b/search/query/geo_distance.go
@@ -16,13 +16,13 @@ package query
 
 import (
 	"context"
-	"encoding/json"
 	"fmt"
 
 	"github.com/blevesearch/bleve/v2/geo"
 	"github.com/blevesearch/bleve/v2/mapping"
 	"github.com/blevesearch/bleve/v2/search"
 	"github.com/blevesearch/bleve/v2/search/searcher"
+	"github.com/blevesearch/bleve/v2/util"
 	index "github.com/blevesearch/bleve_index_api"
 )
 
@@ -86,7 +86,7 @@ func (q *GeoDistanceQuery) UnmarshalJSON(data []byte) error {
 		FieldVal string      `json:"field,omitempty"`
 		BoostVal *Boost      `json:"boost,omitempty"`
 	}{}
-	err := json.Unmarshal(data, &tmp)
+	err := util.UnmarshalJSON(data, &tmp)
 	if err != nil {
 		return err
 	}
diff --git a/search/query/geo_shape.go b/search/query/geo_shape.go
index 2229dbe9..686f486f 100644
--- a/search/query/geo_shape.go
+++ b/search/query/geo_shape.go
@@ -22,6 +22,7 @@ import (
 	"github.com/blevesearch/bleve/v2/mapping"
 	"github.com/blevesearch/bleve/v2/search"
 	"github.com/blevesearch/bleve/v2/search/searcher"
+	"github.com/blevesearch/bleve/v2/util"
 	index "github.com/blevesearch/bleve_index_api"
 )
 
@@ -123,7 +124,7 @@ func (q *Geometry) UnmarshalJSON(data []byte) error {
 		Relation string          `json:"relation"`
 	}{}
 
-	err := json.Unmarshal(data, &tmp)
+	err := util.UnmarshalJSON(data, &tmp)
 	if err != nil {
 		return err
 	}
diff --git a/search/query/match.go b/search/query/match.go
index 61c00a00..074d11d3 100644
--- a/search/query/match.go
+++ b/search/query/match.go
@@ -16,11 +16,11 @@ package query
 
 import (
 	"context"
-	"encoding/json"
 	"fmt"
 
 	"github.com/blevesearch/bleve/v2/mapping"
 	"github.com/blevesearch/bleve/v2/search"
+	"github.com/blevesearch/bleve/v2/util"
 	index "github.com/blevesearch/bleve_index_api"
 )
 
@@ -46,9 +46,9 @@ const (
 func (o MatchQueryOperator) MarshalJSON() ([]byte, error) {
 	switch o {
 	case MatchQueryOperatorOr:
-		return json.Marshal("or")
+		return util.MarshalJSON("or")
 	case MatchQueryOperatorAnd:
-		return json.Marshal("and")
+		return util.MarshalJSON("and")
 	default:
 		return nil, fmt.Errorf("cannot marshal match operator %d to JSON", o)
 	}
@@ -56,7 +56,7 @@ func (o MatchQueryOperator) MarshalJSON() ([]byte, error) {
 
 func (o *MatchQueryOperator) UnmarshalJSON(data []byte) error {
 	var operatorString string
-	err := json.Unmarshal(data, &operatorString)
+	err := util.UnmarshalJSON(data, &operatorString)
 	if err != nil {
 		return err
 	}
diff --git a/search/query/multi_phrase.go b/search/query/multi_phrase.go
index 67afd907..d1144d90 100644
--- a/search/query/multi_phrase.go
+++ b/search/query/multi_phrase.go
@@ -16,12 +16,12 @@ package query
 
 import (
 	"context"
-	"encoding/json"
 	"fmt"
 
 	"github.com/blevesearch/bleve/v2/mapping"
 	"github.com/blevesearch/bleve/v2/search"
 	"github.com/blevesearch/bleve/v2/search/searcher"
+	"github.com/blevesearch/bleve/v2/util"
 	index "github.com/blevesearch/bleve_index_api"
 )
 
@@ -75,7 +75,7 @@ func (q *MultiPhraseQuery) Validate() error {
 func (q *MultiPhraseQuery) UnmarshalJSON(data []byte) error {
 	type _mphraseQuery MultiPhraseQuery
 	tmp := _mphraseQuery{}
-	err := json.Unmarshal(data, &tmp)
+	err := util.UnmarshalJSON(data, &tmp)
 	if err != nil {
 		return err
 	}
diff --git a/search/query/phrase.go b/search/query/phrase.go
index eff2e241..9092e72d 100644
--- a/search/query/phrase.go
+++ b/search/query/phrase.go
@@ -16,12 +16,12 @@ package query
 
 import (
 	"context"
-	"encoding/json"
 	"fmt"
 
 	"github.com/blevesearch/bleve/v2/mapping"
 	"github.com/blevesearch/bleve/v2/search"
 	"github.com/blevesearch/bleve/v2/search/searcher"
+	"github.com/blevesearch/bleve/v2/util"
 	index "github.com/blevesearch/bleve_index_api"
 )
 
@@ -72,7 +72,7 @@ func (q *PhraseQuery) Validate() error {
 func (q *PhraseQuery) UnmarshalJSON(data []byte) error {
 	type _phraseQuery PhraseQuery
 	tmp := _phraseQuery{}
-	err := json.Unmarshal(data, &tmp)
+	err := util.UnmarshalJSON(data, &tmp)
 	if err != nil {
 		return err
 	}
diff --git a/search/query/query.go b/search/query/query.go
index a4e0f015..eb7b34ad 100644
--- a/search/query/query.go
+++ b/search/query/query.go
@@ -23,6 +23,7 @@ import (
 
 	"github.com/blevesearch/bleve/v2/mapping"
 	"github.com/blevesearch/bleve/v2/search"
+	"github.com/blevesearch/bleve/v2/util"
 	index "github.com/blevesearch/bleve_index_api"
 )
 
@@ -68,7 +69,7 @@ type ValidatableQuery interface {
 // a Query object.
 func ParseQuery(input []byte) (Query, error) {
 	var tmp map[string]interface{}
-	err := json.Unmarshal(input, &tmp)
+	err := util.UnmarshalJSON(input, &tmp)
 	if err != nil {
 		return nil, err
 	}
@@ -78,7 +79,7 @@ func ParseQuery(input []byte) (Query, error) {
 	_, hasTerms := tmp["terms"]
 	if hasFuzziness && !isMatchQuery && !isMatchPhraseQuery && !hasTerms {
 		var rv FuzzyQuery
-		err := json.Unmarshal(input, &rv)
+		err := util.UnmarshalJSON(input, &rv)
 		if err != nil {
 			return nil, err
 		}
@@ -86,7 +87,7 @@ func ParseQuery(input []byte) (Query, error) {
 	}
 	if isMatchQuery {
 		var rv MatchQuery
-		err := json.Unmarshal(input, &rv)
+		err := util.UnmarshalJSON(input, &rv)
 		if err != nil {
 			return nil, err
 		}
@@ -94,7 +95,7 @@ func ParseQuery(input []byte) (Query, error) {
 	}
 	if isMatchPhraseQuery {
 		var rv MatchPhraseQuery
-		err := json.Unmarshal(input, &rv)
+		err := util.UnmarshalJSON(input, &rv)
 		if err != nil {
 			return nil, err
 		}
@@ -102,11 +103,11 @@ func ParseQuery(input []byte) (Query, error) {
 	}
 	if hasTerms {
 		var rv PhraseQuery
-		err := json.Unmarshal(input, &rv)
+		err := util.UnmarshalJSON(input, &rv)
 		if err != nil {
 			// now try multi-phrase
 			var rv2 MultiPhraseQuery
-			err = json.Unmarshal(input, &rv2)
+			err = util.UnmarshalJSON(input, &rv2)
 			if err != nil {
 				return nil, err
 			}
@@ -117,7 +118,7 @@ func ParseQuery(input []byte) (Query, error) {
 	_, isTermQuery := tmp["term"]
 	if isTermQuery {
 		var rv TermQuery
-		err := json.Unmarshal(input, &rv)
+		err := util.UnmarshalJSON(input, &rv)
 		if err != nil {
 			return nil, err
 		}
@@ -128,7 +129,7 @@ func ParseQuery(input []byte) (Query, error) {
 	_, hasMustNot := tmp["must_not"]
 	if hasMust || hasShould || hasMustNot {
 		var rv BooleanQuery
-		err := json.Unmarshal(input, &rv)
+		err := util.UnmarshalJSON(input, &rv)
 		if err != nil {
 			return nil, err
 		}
@@ -137,7 +138,7 @@ func ParseQuery(input []byte) (Query, error) {
 	_, hasConjuncts := tmp["conjuncts"]
 	if hasConjuncts {
 		var rv ConjunctionQuery
-		err := json.Unmarshal(input, &rv)
+		err := util.UnmarshalJSON(input, &rv)
 		if err != nil {
 			return nil, err
 		}
@@ -146,7 +147,7 @@ func ParseQuery(input []byte) (Query, error) {
 	_, hasDisjuncts := tmp["disjuncts"]
 	if hasDisjuncts {
 		var rv DisjunctionQuery
-		err := json.Unmarshal(input, &rv)
+		err := util.UnmarshalJSON(input, &rv)
 		if err != nil {
 			return nil, err
 		}
@@ -156,7 +157,7 @@ func ParseQuery(input []byte) (Query, error) {
 	_, hasSyntaxQuery := tmp["query"]
 	if hasSyntaxQuery {
 		var rv QueryStringQuery
-		err := json.Unmarshal(input, &rv)
+		err := util.UnmarshalJSON(input, &rv)
 		if err != nil {
 			return nil, err
 		}
@@ -166,7 +167,7 @@ func ParseQuery(input []byte) (Query, error) {
 	_, hasMax := tmp["max"].(float64)
 	if hasMin || hasMax {
 		var rv NumericRangeQuery
-		err := json.Unmarshal(input, &rv)
+		err := util.UnmarshalJSON(input, &rv)
 		if err != nil {
 			return nil, err
 		}
@@ -176,7 +177,7 @@ func ParseQuery(input []byte) (Query, error) {
 	_, hasMaxStr := tmp["max"].(string)
 	if hasMinStr || hasMaxStr {
 		var rv TermRangeQuery
-		err := json.Unmarshal(input, &rv)
+		err := util.UnmarshalJSON(input, &rv)
 		if err != nil {
 			return nil, err
 		}
@@ -185,8 +186,8 @@ func ParseQuery(input []byte) (Query, error) {
 	_, hasStart := tmp["start"]
 	_, hasEnd := tmp["end"]
 	if hasStart || hasEnd {
-		var rv DateRangeQuery
-		err := json.Unmarshal(input, &rv)
+		var rv DateRangeStringQuery
+		err := util.UnmarshalJSON(input, &rv)
 		if err != nil {
 			return nil, err
 		}
@@ -195,7 +196,7 @@ func ParseQuery(input []byte) (Query, error) {
 	_, hasPrefix := tmp["prefix"]
 	if hasPrefix {
 		var rv PrefixQuery
-		err := json.Unmarshal(input, &rv)
+		err := util.UnmarshalJSON(input, &rv)
 		if err != nil {
 			return nil, err
 		}
@@ -204,7 +205,7 @@ func ParseQuery(input []byte) (Query, error) {
 	_, hasRegexp := tmp["regexp"]
 	if hasRegexp {
 		var rv RegexpQuery
-		err := json.Unmarshal(input, &rv)
+		err := util.UnmarshalJSON(input, &rv)
 		if err != nil {
 			return nil, err
 		}
@@ -213,7 +214,7 @@ func ParseQuery(input []byte) (Query, error) {
 	_, hasWildcard := tmp["wildcard"]
 	if hasWildcard {
 		var rv WildcardQuery
-		err := json.Unmarshal(input, &rv)
+		err := util.UnmarshalJSON(input, &rv)
 		if err != nil {
 			return nil, err
 		}
@@ -222,7 +223,7 @@ func ParseQuery(input []byte) (Query, error) {
 	_, hasMatchAll := tmp["match_all"]
 	if hasMatchAll {
 		var rv MatchAllQuery
-		err := json.Unmarshal(input, &rv)
+		err := util.UnmarshalJSON(input, &rv)
 		if err != nil {
 			return nil, err
 		}
@@ -231,7 +232,7 @@ func ParseQuery(input []byte) (Query, error) {
 	_, hasMatchNone := tmp["match_none"]
 	if hasMatchNone {
 		var rv MatchNoneQuery
-		err := json.Unmarshal(input, &rv)
+		err := util.UnmarshalJSON(input, &rv)
 		if err != nil {
 			return nil, err
 		}
@@ -240,7 +241,7 @@ func ParseQuery(input []byte) (Query, error) {
 	_, hasDocIds := tmp["ids"]
 	if hasDocIds {
 		var rv DocIDQuery
-		err := json.Unmarshal(input, &rv)
+		err := util.UnmarshalJSON(input, &rv)
 		if err != nil {
 			return nil, err
 		}
@@ -249,7 +250,7 @@ func ParseQuery(input []byte) (Query, error) {
 	_, hasBool := tmp["bool"]
 	if hasBool {
 		var rv BoolFieldQuery
-		err := json.Unmarshal(input, &rv)
+		err := util.UnmarshalJSON(input, &rv)
 		if err != nil {
 			return nil, err
 		}
@@ -259,7 +260,7 @@ func ParseQuery(input []byte) (Query, error) {
 	_, hasBottomRight := tmp["bottom_right"]
 	if hasTopLeft && hasBottomRight {
 		var rv GeoBoundingBoxQuery
-		err := json.Unmarshal(input, &rv)
+		err := util.UnmarshalJSON(input, &rv)
 		if err != nil {
 			return nil, err
 		}
@@ -268,7 +269,7 @@ func ParseQuery(input []byte) (Query, error) {
 	_, hasDistance := tmp["distance"]
 	if hasDistance {
 		var rv GeoDistanceQuery
-		err := json.Unmarshal(input, &rv)
+		err := util.UnmarshalJSON(input, &rv)
 		if err != nil {
 			return nil, err
 		}
@@ -277,7 +278,7 @@ func ParseQuery(input []byte) (Query, error) {
 	_, hasPoints := tmp["polygon_points"]
 	if hasPoints {
 		var rv GeoBoundingPolygonQuery
-		err := json.Unmarshal(input, &rv)
+		err := util.UnmarshalJSON(input, &rv)
 		if err != nil {
 			return nil, err
 		}
@@ -287,7 +288,7 @@ func ParseQuery(input []byte) (Query, error) {
 	_, hasGeo := tmp["geometry"]
 	if hasGeo {
 		var rv GeoShapeQuery
-		err := json.Unmarshal(input, &rv)
+		err := util.UnmarshalJSON(input, &rv)
 		if err != nil {
 			return nil, err
 		}
@@ -297,7 +298,7 @@ func ParseQuery(input []byte) (Query, error) {
 	_, hasCIDR := tmp["cidr"]
 	if hasCIDR {
 		var rv IPRangeQuery
-		err := json.Unmarshal(input, &rv)
+		err := util.UnmarshalJSON(input, &rv)
 		if err != nil {
 			return nil, err
 		}
diff --git a/search/searcher/search_geoshape.go b/search/searcher/search_geoshape.go
index 1107c943..ae113107 100644
--- a/search/searcher/search_geoshape.go
+++ b/search/searcher/search_geoshape.go
@@ -22,6 +22,7 @@ import (
 	"github.com/blevesearch/bleve/v2/search"
 	index "github.com/blevesearch/bleve_index_api"
 	"github.com/blevesearch/geo/geojson"
+	"github.com/blevesearch/geo/s2"
 )
 
 func NewGeoShapeSearcher(ctx context.Context, indexReader index.IndexReader, shape index.GeoJSON,
@@ -70,6 +71,12 @@ func buildRelationFilterOnShapes(ctx context.Context, dvReader index.DocValueRea
 	var dvShapeValue []byte
 	var startReading, finishReading bool
 	var reader *bytes.Reader
+
+	var bufPool *s2.GeoBufferPool
+	if ctx != nil {
+		bufPool = ctx.Value(search.GeoBufferPoolCallbackKey).(search.GeoBufferPoolCallbackFunc)()
+	}
+
 	return func(d *search.DocumentMatch) bool {
 		var found bool
 
@@ -104,7 +111,7 @@ func buildRelationFilterOnShapes(ctx context.Context, dvReader index.DocValueRea
 					// apply the filter once the entire docvalue is finished reading.
 					if finishReading {
 						v, err := geojson.FilterGeoShapesOnRelation(shape,
-							dvShapeValue, relation, &reader)
+							dvShapeValue, relation, &reader, bufPool)
 						if err == nil && v {
 							found = true
 						}
diff --git a/search/sort.go b/search/sort.go
index 3a744af9..b13fa16c 100644
--- a/search/sort.go
+++ b/search/sort.go
@@ -25,6 +25,7 @@ import (
 
 	"github.com/blevesearch/bleve/v2/geo"
 	"github.com/blevesearch/bleve/v2/numeric"
+	"github.com/blevesearch/bleve/v2/util"
 )
 
 var HighTerm = strings.Repeat(string(utf8.MaxRune), 3)
@@ -164,10 +165,10 @@ func ParseSearchSortString(input string) SearchSort {
 func ParseSearchSortJSON(input json.RawMessage) (SearchSort, error) {
 	// first try to parse it as string
 	var sortString string
-	err := json.Unmarshal(input, &sortString)
+	err := util.UnmarshalJSON(input, &sortString)
 	if err != nil {
 		var sortObj map[string]interface{}
-		err = json.Unmarshal(input, &sortObj)
+		err = util.UnmarshalJSON(input, &sortObj)
 		if err != nil {
 			return nil, err
 		}
diff --git a/search/util.go b/search/util.go
index 82aa21ac..b2cb62a2 100644
--- a/search/util.go
+++ b/search/util.go
@@ -14,7 +14,11 @@
 
 package search
 
-import "context"
+import (
+	"context"
+
+	"github.com/blevesearch/geo/s2"
+)
 
 func MergeLocations(locations []FieldTermLocationMap) FieldTermLocationMap {
 	rv := locations[0]
@@ -119,3 +123,13 @@ func RecordSearchCost(ctx context.Context,
 		}
 	}
 }
+
+const GeoBufferPoolCallbackKey = "_geo_buffer_pool_callback_key"
+
+// Assigning the size of the largest buffer in the pool to 24KB and
+// the smallest buffer to 24 bytes. The pools are used to read a
+// sequence of vertices which are always 24 bytes each.
+const MaxGeoBufPoolSize = 24 * 1024
+const MinGeoBufPoolSize = 24
+
+type GeoBufferPoolCallbackFunc func() *s2.GeoBufferPool
diff --git a/util/json.go b/util/json.go
new file mode 100644
index 00000000..8745dd8a
--- /dev/null
+++ b/util/json.go
@@ -0,0 +1,25 @@
+//  Copyright (c) 2023 Couchbase, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// 		http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package util
+
+import (
+	"encoding/json"
+)
+
+// Should only be overwritten during process init()'ialization.
+var (
+	MarshalJSON   = json.Marshal
+	UnmarshalJSON = json.Unmarshal
+)


Test patch:
diff --git a/analysis/datetime/sanitized/sanitized_test.go b/analysis/datetime/sanitized/sanitized_test.go
new file mode 100644
index 00000000..d680b248
--- /dev/null
+++ b/analysis/datetime/sanitized/sanitized_test.go
@@ -0,0 +1,95 @@
+package sanitized
+
+import (
+	"reflect"
+	"testing"
+)
+
+func TestLayoutValidatorRegex(t *testing.T) {
+	splitRegexTests := []struct {
+		input  string
+		output []string
+	}{
+		{
+			input:  "2014-08-03",
+			output: []string{"2014", "08", "03"},
+		},
+		{
+			input:  "2014-08-03T15:59:30",
+			output: []string{"2014", "08", "03", "15", "59", "30"},
+		},
+		{
+			input:  "2014.08-03 15/59`30",
+			output: []string{"2014", "08", "03", "15", "59", "30"},
+		},
+		{
+			input:  "2014/08/03T15:59:30Z08:00",
+			output: []string{"2014", "08", "03", "15", "59", "30", "08", "00"},
+		},
+		{
+			input:  "2014\\08|03T15=59.30.999999999+08*00",
+			output: []string{"2014", "08", "03", "15", "59", "30", "999999999", "08", "00"},
+		},
+		{
+			input:  "2006-01-02T15:04:05.999999999Z07:00",
+			output: []string{"2006", "01", "02", "15", "04", "05", "999999999", "07", "00"},
+		},
+		{
+			input: "A-B C:DTE,FZG.H<I>J;K?L!M`N~O@P#Q$R%S^U&V*W|X'Y\"A(B)C{D}E[F]G/H\\I+J=L",
+			output: []string{"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P",
+				"Q", "R", "S", "U", "V", "W", "X", "Y", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "L"},
+		},
+	}
+	regex := layoutSplitRegex
+	for _, test := range splitRegexTests {
+		t.Run(test.input, func(t *testing.T) {
+			actualOutput := regex.Split(test.input, -1)
+			if !reflect.DeepEqual(actualOutput, test.output) {
+				t.Fatalf("expected output %v, got %v", test.output, actualOutput)
+			}
+		})
+	}
+
+	stripRegexTests := []struct {
+		input  string
+		output string
+	}{
+		{
+			input:  "3PM",
+			output: "3",
+		},
+		{
+			input:  "3.0PM",
+			output: "3",
+		},
+		{
+			input:  "3.9AM",
+			output: "3AM",
+		},
+		{
+			input:  "3.999999999pm",
+			output: "3",
+		},
+		{
+			input:  "2006-01-02T15:04:05.999999999Z07:00MST",
+			output: "2006-01-02T15:04:05Z07:00",
+		},
+		{
+			input:  "Jan _2 15:04:05.0000000+07:00MST",
+			output: "Jan _2 15:04:05+07:00",
+		},
+		{
+			input:  "15:04:05.99PM+07:00MST",
+			output: "15:04:05+07:00",
+		},
+	}
+	regex = layoutStripRegex
+	for _, test := range stripRegexTests {
+		t.Run(test.input, func(t *testing.T) {
+			actualOutput := layoutStripRegex.ReplaceAllString(test.input, "")
+			if !reflect.DeepEqual(actualOutput, test.output) {
+				t.Fatalf("expected output %v, got %v", test.output, actualOutput)
+			}
+		})
+	}
+}
diff --git a/search/query/query_test.go b/search/query/query_test.go
index 0082e8ac..228fb65c 100644
--- a/search/query/query_test.go
+++ b/search/query/query_test.go
@@ -176,7 +176,7 @@ func TestParseQuery(t *testing.T) {
 		{
 			input: []byte(`{"start":"` + startDateStr + `","end":"` + endDateStr + `","field":"desc"}`),
 			output: func() Query {
-				q := NewDateRangeQuery(startDate, endDate)
+				q := NewDateRangeStringQuery(startDateStr, endDateStr)
 				q.SetField("desc")
 				return q
 			}(),
diff --git a/search_test.go b/search_test.go
index cdd5a450..12ebc7bd 100644
--- a/search_test.go
+++ b/search_test.go
@@ -29,6 +29,12 @@ import (
 	"github.com/blevesearch/bleve/v2/analysis/analyzer/standard"
 	html_char_filter "github.com/blevesearch/bleve/v2/analysis/char/html"
 	regexp_char_filter "github.com/blevesearch/bleve/v2/analysis/char/regexp"
+	"github.com/blevesearch/bleve/v2/analysis/datetime/flexible"
+	"github.com/blevesearch/bleve/v2/analysis/datetime/sanitized"
+	"github.com/blevesearch/bleve/v2/analysis/datetime/timestamp/microseconds"
+	"github.com/blevesearch/bleve/v2/analysis/datetime/timestamp/milliseconds"
+	"github.com/blevesearch/bleve/v2/analysis/datetime/timestamp/nanoseconds"
+	"github.com/blevesearch/bleve/v2/analysis/datetime/timestamp/seconds"
 	"github.com/blevesearch/bleve/v2/analysis/token/length"
 	"github.com/blevesearch/bleve/v2/analysis/token/lowercase"
 	"github.com/blevesearch/bleve/v2/analysis/token/shingle"
@@ -280,7 +286,7 @@ func TestUnmarshalingSearchResult(t *testing.T) {
 func TestFacetNumericDateRangeRequests(t *testing.T) {
 	var drMissingErr = fmt.Errorf("date range query must specify either start, end or both for range name 'testName'")
 	var nrMissingErr = fmt.Errorf("numeric range query must specify either min, max or both for range name 'testName'")
-	var drNrErr = fmt.Errorf("facet can only conain numeric ranges or date ranges, not both")
+	var drNrErr = fmt.Errorf("facet can only contain numeric ranges or date ranges, not both")
 	var drNameDupErr = fmt.Errorf("date ranges contains duplicate name 'testName'")
 	var nrNameDupErr = fmt.Errorf("numeric ranges contains duplicate name 'testName'")
 	value := float64(5)
@@ -2355,3 +2361,871 @@ func TestAnalyzerInheritanceForDefaultDynamicMapping(t *testing.T) {
 		t.Fatalf("expected 1 hit, got %d", len(results.Hits))
 	}
 }
+
+func TestCustomDateTimeParserLayoutValidation(t *testing.T) {
+	flexiblegoName := flexible.Name
+	sanitizedgoName := sanitized.Name
+	imap := mapping.NewIndexMapping()
+	correctConfig := map[string]interface{}{
+		"type": sanitizedgoName,
+		"layouts": []interface{}{
+			// some custom layouts
+			"2006-01-02 15:04:05.0000",
+			"2006\\01\\02T03:04:05PM",
+			"2006/01/02",
+			"2006-01-02T15:04:05.999Z0700PMMST",
+			"15:04:05.0000Z07:00 Monday",
+
+			// standard layouts
+			time.Layout,
+			time.ANSIC,
+			time.UnixDate,
+			time.RubyDate,
+			time.RFC822,
+			time.RFC822Z,
+			time.RFC850,
+			time.RFC1123,
+			time.RFC1123Z,
+			time.RFC3339,
+			time.RFC3339Nano,
+			time.Kitchen,
+			time.Stamp,
+			time.StampMilli,
+			time.StampMicro,
+			time.StampNano,
+			"2006-01-02 15:04:05", //time.DateTime
+			"2006-01-02",          //time.DateOnly
+			"15:04:05",            //time.TimeOnly
+
+			// Corrected layouts to the incorrect ones below.
+			"2006-01-02 03:04:05 -0700",
+			"2006-01-02 15:04:05 -0700",
+			"3:04PM",
+			"2006-01-02 15:04:05.000 -0700 MST",
+			"January 2 2006 3:04 PM",
+			"02/Jan/06 3:04PM",
+			"Mon 02 Jan 3:04:05 PM",
+		},
+	}
+
+	// Correct layouts - sanitizedgo should work without errors.
+	err := imap.AddCustomDateTimeParser("custDT", correctConfig)
+	if err != nil {
+		t.Fatalf("expected no error, got: %v", err)
+	}
+	// Flexiblego should work without errors as well.
+	correctConfig["type"] = flexiblegoName
+	err = imap.AddCustomDateTimeParser("custDT_Flexi", correctConfig)
+	if err != nil {
+		t.Fatalf("expected no error, got: %v", err)
+	}
+
+	incorrectLayouts := [][]interface{}{
+		{
+			"2000-03-31 01:33:51 +0300",
+		},
+		{
+			"2006-01-02 15:04:51 +0300",
+		},
+		{
+			"2000-03-31 01:33:05 +0300",
+		},
+		{
+			"4:45PM",
+		},
+		{
+			"2006-01-02 15:04:05.445 -0700 MST",
+		},
+		{
+			"August 20 2001 8:55 AM",
+		},
+		{
+			"28/Jul/23 12:48PM",
+		},
+		{
+			"Tue 22 Aug 6:37:30 AM",
+		},
+	}
+
+	// first check sanitizedgo, should throw error for each of the incorrect layouts.
+	numExpectedErrors := len(incorrectLayouts)
+	numActualErrors := 0
+	for idx, badLayout := range incorrectLayouts {
+		incorrectConfig := map[string]interface{}{
+			"type":    sanitizedgoName,
+			"layouts": badLayout,
+		}
+		err := imap.AddCustomDateTimeParser(fmt.Sprintf("%d_DT", idx), incorrectConfig)
+		if err != nil {
+			numActualErrors++
+		}
+	}
+	// Expecting all layouts to be incorrect, since sanitizedgo is being used.
+	if numActualErrors != numExpectedErrors {
+		t.Fatalf("expected %d errors, got: %d", numExpectedErrors, numActualErrors)
+	}
+
+	// sanity test - flexiblego should still allow the incorrect layouts, for legacy purposes
+	for idx, badLayout := range incorrectLayouts {
+		incorrectConfig := map[string]interface{}{
+			"type":    flexiblegoName,
+			"layouts": badLayout,
+		}
+		err := imap.AddCustomDateTimeParser(fmt.Sprintf("%d_DT_Flexi", idx), incorrectConfig)
+		if err != nil {
+			t.Fatalf("expected no error, got: %v", err)
+		}
+	}
+}
+
+func TestDateRangeStringQuery(t *testing.T) {
+	idxMapping := NewIndexMapping()
+
+	err := idxMapping.AddCustomDateTimeParser("customDT", map[string]interface{}{
+		"type": sanitized.Name,
+		"layouts": []interface{}{
+			"02/01/2006 15:04:05",
+			"2006/01/02 3:04PM",
+		},
+	})
+
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	err = idxMapping.AddCustomDateTimeParser("queryDT", map[string]interface{}{
+		"type": sanitized.Name,
+		"layouts": []interface{}{
+			"02/01/2006 3:04PM",
+		},
+	})
+
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	dtmap := NewDateTimeFieldMapping()
+	dtmap.DateFormat = "customDT"
+	idxMapping.DefaultMapping.AddFieldMappingsAt("date", dtmap)
+
+	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)
+		}
+	}()
+	documents := map[string]map[string]interface{}{
+		"doc1": {
+			"date": "2001/08/20 6:00PM",
+		},
+		"doc2": {
+			"date": "20/08/2001 18:00:20",
+		},
+		"doc3": {
+			"date": "20/08/2001 18:10:00",
+		},
+		"doc4": {
+			"date": "2001/08/20 6:15PM",
+		},
+		"doc5": {
+			"date": "20/08/2001 18:20:00",
+		},
+	}
+
+	batch := idx.NewBatch()
+	for docID, doc := range documents {
+		err := batch.Index(docID, doc)
+		if err != nil {
+			t.Fatal(err)
+		}
+	}
+	err = idx.Batch(batch)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	type testResult struct {
+		docID    string // doc ID of the hit
+		hitField string // fields returned as part of the hit
+	}
+
+	type testStruct struct {
+		start          string
+		end            string
+		field          string
+		dateTimeParser string // name of the custom date time parser to use if nil, use QueryDateTimeParser
+		includeStart   bool
+		includeEnd     bool
+		expectedHits   []testResult
+		err            error
+	}
+
+	testQueries := []testStruct{
+		// test cases with RFC3339 parser and toggling includeStart and includeEnd
+		{
+			start:        "2001-08-20T18:00:00",
+			end:          "2001-08-20T18:10:00",
+			field:        "date",
+			includeStart: true,
+			includeEnd:   true,
+			expectedHits: []testResult{
+				{
+					docID:    "doc1",
+					hitField: "2001/08/20 6:00PM",
+				},
+				{
+					docID:    "doc2",
+					hitField: "20/08/2001 18:00:20",
+				},
+				{
+					docID:    "doc3",
+					hitField: "20/08/2001 18:10:00",
+				},
+			},
+		},
+		{
+			start:        "2001-08-20T18:00:00",
+			end:          "2001-08-20T18:10:00",
+			field:        "date",
+			includeStart: false,
+			includeEnd:   true,
+			expectedHits: []testResult{
+				{
+					docID:    "doc2",
+					hitField: "20/08/2001 18:00:20",
+				},
+				{
+					docID:    "doc3",
+					hitField: "20/08/2001 18:10:00",
+				},
+			},
+		},
+		{
+			start:        "2001-08-20T18:00:00",
+			end:          "2001-08-20T18:10:00",
+			field:        "date",
+			includeStart: false,
+			includeEnd:   false,
+			expectedHits: []testResult{
+				{
+					docID:    "doc2",
+					hitField: "20/08/2001 18:00:20",
+				},
+			},
+		},
+		// test cases with custom parser and omitting start and end
+		{
+			start:          "20/08/2001 18:00:00",
+			end:            "2001/08/20 6:10PM",
+			field:          "date",
+			dateTimeParser: "customDT",
+			includeStart:   true,
+			includeEnd:     true,
+			expectedHits: []testResult{
+				{
+					docID:    "doc1",
+					hitField: "2001/08/20 6:00PM",
+				},
+				{
+					docID:    "doc2",
+					hitField: "20/08/2001 18:00:20",
+				},
+				{
+					docID:    "doc3",
+					hitField: "20/08/2001 18:10:00",
+				},
+			},
+		},
+		{
+			end:            "20/08/2001 18:15:00",
+			field:          "date",
+			dateTimeParser: "customDT",
+			includeStart:   true,
+			includeEnd:     true,
+			expectedHits: []testResult{
+				{
+					docID:    "doc1",
+					hitField: "2001/08/20 6:00PM",
+				},
+				{
+					docID:    "doc2",
+					hitField: "20/08/2001 18:00:20",
+				},
+				{
+					docID:    "doc3",
+					hitField: "20/08/2001 18:10:00",
+				},
+				{
+					docID:    "doc4",
+					hitField: "2001/08/20 6:15PM",
+				},
+			},
+		},
+		{
+			start:          "2001/08/20 6:15PM",
+			field:          "date",
+			dateTimeParser: "customDT",
+			includeStart:   true,
+			includeEnd:     true,
+			expectedHits: []testResult{
+				{
+					docID:    "doc4",
+					hitField: "2001/08/20 6:15PM",
+				},
+				{
+					docID:    "doc5",
+					hitField: "20/08/2001 18:20:00",
+				},
+			},
+		},
+		{
+			start:          "20/08/2001 6:15PM",
+			field:          "date",
+			dateTimeParser: "queryDT",
+			includeStart:   true,
+			includeEnd:     true,
+			expectedHits: []testResult{
+				{
+					docID:    "doc4",
+					hitField: "2001/08/20 6:15PM",
+				},
+				{
+					docID:    "doc5",
+					hitField: "20/08/2001 18:20:00",
+				},
+			},
+		},
+		// error path test cases
+		{
+			field:          "date",
+			dateTimeParser: "customDT",
+			includeStart:   true,
+			includeEnd:     true,
+			err:            fmt.Errorf("date range query must specify at least one of start/end"),
+		},
+		{
+			field:        "date",
+			includeStart: true,
+			includeEnd:   true,
+			err:          fmt.Errorf("date range query must specify at least one of start/end"),
+		},
+		{
+			start:          "2001-08-20T18:00:00",
+			end:            "2001-08-20T18:10:00",
+			field:          "date",
+			dateTimeParser: "customDT",
+			err:            fmt.Errorf("unable to parse datetime with any of the layouts, date time parser name: customDT"),
+		},
+		{
+			start: "3001-08-20T18:00:00",
+			end:   "2001-08-20T18:10:00",
+			field: "date",
+			err:   fmt.Errorf("invalid/unsupported date range, start: 3001-08-20T18:00:00"),
+		},
+		{
+			start:          "2001/08/20 6:00PM",
+			end:            "3001/08/20 6:30PM",
+			field:          "date",
+			dateTimeParser: "customDT",
+			err:            fmt.Errorf("invalid/unsupported date range, end: 3001/08/20 6:30PM"),
+		},
+	}
+
+	for _, dtq := range testQueries {
+		var err error
+		dateQuery := query.NewDateRangeStringInclusiveQuery(dtq.start, dtq.end, &dtq.includeStart, &dtq.includeEnd)
+		dateQuery.SetDateTimeParser(dtq.dateTimeParser)
+		dateQuery.SetField(dtq.field)
+
+		sr := NewSearchRequest(dateQuery)
+		sr.SortBy([]string{dtq.field})
+		sr.Fields = []string{dtq.field}
+
+		res, err := idx.Search(sr)
+		if err != nil {
+			if dtq.err == nil {
+				t.Fatalf("expected no error, got: %v", err)
+			}
+			if dtq.err.Error() != err.Error() {
+				t.Fatalf("expected error: %v, got: %v", dtq.err, err)
+			}
+			continue
+		}
+		if len(res.Hits) != len(dtq.expectedHits) {
+			t.Fatalf("expected %d hits, got %d", len(dtq.expectedHits), len(res.Hits))
+		}
+		for i, hit := range res.Hits {
+			if hit.ID != dtq.expectedHits[i].docID {
+				t.Fatalf("expected docID %s, got %s", dtq.expectedHits[i].docID, hit.ID)
+			}
+			if hit.Fields[dtq.field].(string) != dtq.expectedHits[i].hitField {
+				t.Fatalf("expected hit field %s, got %s", dtq.expectedHits[i].hitField, hit.Fields[dtq.field])
+			}
+		}
+	}
+}
+
+func TestDateRangeFaceQueriesWithCustomDateTimeParser(t *testing.T) {
+	idxMapping := NewIndexMapping()
+
+	err := idxMapping.AddCustomDateTimeParser("customDT", map[string]interface{}{
+		"type": sanitized.Name,
+		"layouts": []interface{}{
+			"02/01/2006 15:04:05",
+			"2006/01/02 3:04PM",
+		},
+	})
+
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	err = idxMapping.AddCustomDateTimeParser("queryDT", map[string]interface{}{
+		"type": sanitized.Name,
+		"layouts": []interface{}{
+			"02/01/2006 3:04PM",
+		},
+	})
+
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	dtmap := NewDateTimeFieldMapping()
+	dtmap.DateFormat = "customDT"
+	idxMapping.DefaultMapping.AddFieldMappingsAt("date", dtmap)
+
+	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)
+		}
+	}()
+	documents := map[string]map[string]interface{}{
+		"doc1": {
+			"date": "2001/08/20 6:00PM",
+		},
+		"doc2": {
+			"date": "20/08/2001 18:00:20",
+		},
+		"doc3": {
+			"date": "20/08/2001 18:10:00",
+		},
+		"doc4": {
+			"date": "2001/08/20 6:15PM",
+		},
+		"doc5": {
+			"date": "20/08/2001 18:20:00",
+		},
+	}
+
+	batch := idx.NewBatch()
+	for docID, doc := range documents {
+		err := batch.Index(docID, doc)
+		if err != nil {
+			t.Fatal(err)
+		}
+	}
+	err = idx.Batch(batch)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	query := NewMatchAllQuery()
+
+	type testFacetResult struct {
+		name  string
+		start string
+		end   string
+		count int
+		err   error
+	}
+
+	type testFacetRequest struct {
+		name   string
+		start  string
+		end    string
+		parser string
+		result testFacetResult
+	}
+
+	tests := []testFacetRequest{
+		{
+			// Test without a query time override of the parser (use default parser)
+			name:  "test",
+			start: "2001-08-20 18:00:00",
+			end:   "2001-08-20 18:10:00",
+			result: testFacetResult{
+				name:  "test",
+				start: "2001-08-20 18:00:00",
+				end:   "2001-08-20 18:10:00",
+				count: 2,
+				err:   nil,
+			},
+		},
+		{
+			name:   "test",
+			start:  "20/08/2001 6:00PM",
+			end:    "20/08/2001 6:10PM",
+			parser: "queryDT",
+			result: testFacetResult{
+				name:  "test",
+				start: "20/08/2001 6:00PM",
+				end:   "20/08/2001 6:10PM",
+				count: 2,
+				err:   nil,
+			},
+		},
+		{
+			name:   "test",
+			start:  "20/08/2001 15:00:00",
+			end:    "2001/08/20 6:10PM",
+			parser: "customDT",
+			result: testFacetResult{
+				name:  "test",
+				start: "20/08/2001 15:00:00",
+				end:   "2001/08/20 6:10PM",
+				count: 2,
+				err:   nil,
+			},
+		},
+		{
+			name:   "test",
+			end:    "2001/08/20 6:15PM",
+			parser: "customDT",
+			result: testFacetResult{
+				name:  "test",
+				end:   "2001/08/20 6:15PM",
+				count: 3,
+				err:   nil,
+			},
+		},
+		{
+			name:   "test",
+			start:  "20/08/2001 6:15PM",
+			parser: "queryDT",
+			result: testFacetResult{
+				name:  "test",
+				start: "20/08/2001 6:15PM",
+				count: 2,
+				err:   nil,
+			},
+		},
+		// some error cases
+		{
+			name:   "test",
+			parser: "queryDT",
+			result: testFacetResult{
+				name: "test",
+				err:  fmt.Errorf("date range query must specify either start, end or both for date range name 'test'"),
+			},
+		},
+		{
+			// default parser is used for the query, but the start time is not in the correct format (RFC3339),
+			// so it should throw an error
+			name:  "test",
+			start: "20/08/2001 6:15PM",
+			result: testFacetResult{
+				name: "test",
+				err:  fmt.Errorf("ParseDates err: error parsing start date '20/08/2001 6:15PM' for date range name 'test': unable to parse datetime with any of the layouts, using date time parser named dateTimeOptional"),
+			},
+		},
+	}
+
+	for _, test := range tests {
+		searchRequest := NewSearchRequest(query)
+
+		fr := NewFacetRequest("date", 100)
+		start := &test.start
+		if test.start == "" {
+			start = nil
+		}
+		end := &test.end
+		if test.end == "" {
+			end = nil
+		}
+
+		fr.AddDateTimeRangeStringWithParser(test.name, start, end, test.parser)
+		searchRequest.AddFacet("dateFacet", fr)
+
+		searchResults, err := idx.Search(searchRequest)
+		if err != nil {
+			if test.result.err == nil {
+				t.Fatalf("Unexpected error: %v", err)
+			}
+			if err.Error() != test.result.err.Error() {
+				t.Fatalf("Expected error %v, got %v", test.result.err, err)
+			}
+			continue
+		}
+		for _, facetResult := range searchResults.Facets {
+			if len(facetResult.DateRanges) != 1 {
+				t.Fatal("Expected 1 date range facet")
+			}
+			result := facetResult.DateRanges[0]
+			if result.Name != test.result.name {
+				t.Fatalf("Expected name %s, got %s", test.result.name, result.Name)
+			}
+			if result.Start != nil && *result.Start != test.result.start {
+				t.Fatalf("Expected start %s, got %s", test.result.start, *result.Start)
+			}
+			if result.End != nil && *result.End != test.result.end {
+				t.Fatalf("Expected end %s, got %s", test.result.end, *result.End)
+			}
+			if result.Start == nil && test.result.start != "" {
+				t.Fatalf("Expected start %s, got nil", test.result.start)
+			}
+			if result.End == nil && test.result.end != "" {
+				t.Fatalf("Expected end %s, got nil", test.result.end)
+			}
+			if result.Count != test.result.count {
+				t.Fatalf("Expected count %d, got %d", test.result.count, result.Count)
+			}
+		}
+	}
+}
+
+func TestDateRangeTimestampQueries(t *testing.T) {
+	tmpIndexPath := createTmpIndexPath(t)
+	defer cleanupTmpIndexPath(t, tmpIndexPath)
+
+	imap := mapping.NewIndexMapping()
+
+	// add a date field with a valid format to the default mapping
+	// for good measure
+
+	dtParserConfig := map[string]interface{}{
+		"type":    flexible.Name,
+		"layouts": []interface{}{"2006/01/02 15:04:05"},
+	}
+	err := imap.AddCustomDateTimeParser("custDT", dtParserConfig)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	dateField := mapping.NewDateTimeFieldMapping()
+	dateField.DateFormat = "custDT"
+
+	unixSecField := mapping.NewDateTimeFieldMapping()
+	unixSecField.DateFormat = seconds.Name
+
+	unixMilliSecField := mapping.NewDateTimeFieldMapping()
+	unixMilliSecField.DateFormat = milliseconds.Name
+
+	unixMicroSecField := mapping.NewDateTimeFieldMapping()
+	unixMicroSecField.DateFormat = microseconds.Name
+
+	unixNanoSecField := mapping.NewDateTimeFieldMapping()
+	unixNanoSecField.DateFormat = nanoseconds.Name
+
+	imap.DefaultMapping.AddFieldMappingsAt("date", dateField)
+	imap.DefaultMapping.AddFieldMappingsAt("seconds", unixSecField)
+	imap.DefaultMapping.AddFieldMappingsAt("milliseconds", unixMilliSecField)
+	imap.DefaultMapping.AddFieldMappingsAt("microseconds", unixMicroSecField)
+	imap.DefaultMapping.AddFieldMappingsAt("nanoseconds", unixNanoSecField)
+
+	idx, err := New(tmpIndexPath, imap)
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer func() {
+		err = idx.Close()
+		if err != nil {
+			t.Fatal(err)
+		}
+	}()
+
+	documents := map[string]map[string]interface{}{
+		"doc1": {
+			"date":         "2001/08/20 03:00:10",
+			"seconds":      "998276410",
+			"milliseconds": "998276410100",
+			"microseconds": "998276410100300",
+			"nanoseconds":  "998276410100300400",
+		},
+		"doc2": {
+			"date":         "2001/08/20 03:00:20",
+			"seconds":      "998276420",
+			"milliseconds": "998276410200",
+			"microseconds": "998276410100400",
+			"nanoseconds":  "998276410100300500",
+		},
+		"doc3": {
+			"date":         "2001/08/20 03:00:30",
+			"seconds":      "998276430",
+			"milliseconds": "998276410300",
+			"microseconds": "998276410100500",
+			"nanoseconds":  "998276410100300600",
+		},
+		"doc4": {
+			"date":         "2001/08/20 03:00:40",
+			"seconds":      "998276440",
+			"milliseconds": "998276410400",
+			"microseconds": "998276410100600",
+			"nanoseconds":  "998276410100300700",
+		},
+		"doc5": {
+			"date":         "2001/08/20 03:00:50",
+			"seconds":      "998276450",
+			"milliseconds": "998276410500",
+			"microseconds": "998276410100700",
+			"nanoseconds":  "998276410100300800",
+		},
+	}
+
+	batch := idx.NewBatch()
+	for docID, doc := range documents {
+		err := batch.Index(docID, doc)
+		if err != nil {
+			t.Fatal(err)
+		}
+	}
+	err = idx.Batch(batch)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	type testResult struct {
+		docID    string // doc ID of the hit
+		hitField string // fields returned as part of the hit
+	}
+	type testStruct struct {
+		start        string
+		end          string
+		field        string
+		expectedHits []testResult
+	}
+
+	testQueries := []testStruct{
+		{
+			start: "2001-08-20T03:00:05",
+			end:   "2001-08-20T03:00:25",
+			field: "date",
+			expectedHits: []testResult{
+				{
+					docID:    "doc1",
+					hitField: "2001/08/20 03:00:10",
+				},
+				{
+					docID:    "doc2",
+					hitField: "2001/08/20 03:00:20",
+				},
+			},
+		},
+		{
+			start: "2001-08-20T03:00:15",
+			end:   "2001-08-20T03:00:35",
+			field: "seconds",
+			expectedHits: []testResult{
+				{
+					docID:    "doc2",
+					hitField: "998276420000000000",
+				},
+				{
+					docID:    "doc3",
+					hitField: "998276430000000000",
+				},
+			},
+		},
+		{
+			start: "2001-08-20T03:00:10.150",
+			end:   "2001-08-20T03:00:10.450",
+			field: "milliseconds",
+			expectedHits: []testResult{
+				{
+					docID:    "doc2",
+					hitField: "998276410200000000",
+				},
+				{
+					docID:    "doc3",
+					hitField: "998276410300000000",
+				},
+				{
+					docID:    "doc4",
+					hitField: "998276410400000000",
+				},
+			},
+		},
+		{
+			start: "2001-08-20T03:00:10.100450",
+			end:   "2001-08-20T03:00:10.100650",
+			field: "microseconds",
+			expectedHits: []testResult{
+				{
+					docID:    "doc3",
+					hitField: "998276410100500000",
+				},
+				{
+					docID:    "doc4",
+					hitField: "998276410100600000",
+				},
+			},
+		},
+		{
+			start: "2001-08-20T03:00:10.100300550",
+			end:   "2001-08-20T03:00:10.100300850",
+			field: "nanoseconds",
+			expectedHits: []testResult{
+				{
+					docID:    "doc3",
+					hitField: "998276410100300600",
+				},
+				{
+					docID:    "doc4",
+					hitField: "998276410100300700",
+				},
+				{
+					docID:    "doc5",
+					hitField: "998276410100300800",
+				},
+			},
+		},
+	}
+	testLayout := "2006-01-02T15:04:05"
+	for _, dtq := range testQueries {
+		startTime, err := time.Parse(testLayout, dtq.start)
+		if err != nil {
+			t.Fatal(err)
+		}
+		endTime, err := time.Parse(testLayout, dtq.end)
+		if err != nil {
+			t.Fatal(err)
+		}
+		drq := NewDateRangeQuery(startTime, endTime)
+		drq.SetField(dtq.field)
+
+		sr := NewSearchRequest(drq)
+		sr.SortBy([]string{dtq.field})
+		sr.Fields = []string{dtq.field}
+
+		res, err := idx.Search(sr)
+		if err != nil {
+			t.Fatal(err)
+		}
+		if len(res.Hits) != len(dtq.expectedHits) {
+			t.Fatalf("expected %d hits, got %d", len(dtq.expectedHits), len(res.Hits))
+		}
+		for i, hit := range res.Hits {
+			if hit.ID != dtq.expectedHits[i].docID {
+				t.Fatalf("expected docID %s, got %s", dtq.expectedHits[i].docID, hit.ID)
+			}
+			if hit.Fields[dtq.field].(string) != dtq.expectedHits[i].hitField {
+				t.Fatalf("expected hit field %s, got %s", dtq.expectedHits[i].hitField, hit.Fields[dtq.field])
+			}
+		}
+	}
+}
diff --git a/test/tests/facet/searches.json b/test/tests/facet/searches.json
index 6752282a..33ac3977 100644
--- a/test/tests/facet/searches.json
+++ b/test/tests/facet/searches.json
@@ -129,12 +129,12 @@
 						{
 							"name": "new",
 							"count": 9,
-							"start": "2012-01-01T00:00:00Z"
+							"start": "2012-01-01"
 						},
 						{
 							"name": "old",
 							"count": 1,
-							"end": "2012-01-01T00:00:00Z"
+							"end": "2012-01-01"
 						}
 					]
 				}