Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions geov2/buffer_pool.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Copyright (c) 2026 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 geov2

import (
"sync"

"github.com/blevesearch/geo/s2"
)

// Bounds on the scratch buffers handed to shape decoding, matching the sizes the
// non-v2 geo shape searcher uses (search.MaxGeoBufPoolSize and
// search.MinGeoBufPoolSize). They are repeated here rather than imported so that
// this package does not have to depend on the search package, which would put a
// dependency on search behind the document package.
const (
maxGeoBufPoolSize = 24 * 1024
minGeoBufPoolSize = 24
)

// A GeoBufferPool holds the scratch buffers that shape decoding reads vertices
// through. The buffers inside one are allocated on demand and then kept, so a
// pool is only worth having if it is used more than a handful of times: building
// a fresh one for a single shape costs more than decoding that shape without a
// pool at all.
//
// These are therefore kept process-wide rather than built per query or per
// segment, so that the buffers survive across both and the cost of allocating
// them is paid a few times in total. A pool is not safe for concurrent use, so a
// caller takes one for its own use and returns it when done.
var geoBufferPools = sync.Pool{
New: func() interface{} {
return s2.NewGeoBufferPool(maxGeoBufPoolSize, minGeoBufPoolSize)
},
}

// shapeDecodeBuffers hands out a pool for decoding stored shapes, along with the
// function that returns it. It is deliberately called at the point of the first
// decode rather than up front: a query whose cell coverage settles every
// document never decodes a shape, and should not pay for a pool.
func shapeDecodeBuffers() (*s2.GeoBufferPool, func()) {
pool := geoBufferPools.Get().(*s2.GeoBufferPool)

return pool, func() { geoBufferPools.Put(pool) }
}
18 changes: 17 additions & 1 deletion geov2/query_contains.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"github.com/blevesearch/bleve/v2/util"
index "github.com/blevesearch/bleve_index_api"
"github.com/blevesearch/geo/geojson"
"github.com/blevesearch/geo/s2"
segment "github.com/blevesearch/scorch_segment_api/v2"
)

Expand Down Expand Up @@ -89,6 +90,17 @@ func (cq *containsQuery) Evaluate(geoData segment.GeoShapeV2Data) *util.Bitset {

var reader *bytes.Reader

// Scratch buffers for decoding stored shapes, taken on the first decode that
// actually happens and returned at the end of the call. Evaluate runs on one
// goroutine per segment, so the pool is this call's alone while it holds it.
var bufPool *s2.GeoBufferPool
var releaseBufPool func()
defer func() {
if releaseBufPool != nil {
releaseBufPool()
}
}()

// filter out any maybeHits that do not have a bounding box that
// contains the query bounding box
boxFilter := func(docNum int) {
Expand Down Expand Up @@ -117,7 +129,11 @@ func (cq *containsQuery) Evaluate(geoData segment.GeoShapeV2Data) *util.Bitset {
return
}

docShape, err := geojson.ExtractShapesFromBytes(docShapeBytes, &reader, nil)
if bufPool == nil {
bufPool, releaseBufPool = shapeDecodeBuffers()
}

docShape, err := geojson.ExtractShapesFromBytes(docShapeBytes, &reader, bufPool)
if err != nil {
return
}
Expand Down
18 changes: 17 additions & 1 deletion geov2/query_intersects.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"github.com/blevesearch/bleve/v2/util"
index "github.com/blevesearch/bleve_index_api"
"github.com/blevesearch/geo/geojson"
"github.com/blevesearch/geo/s2"
segment "github.com/blevesearch/scorch_segment_api/v2"
)

Expand Down Expand Up @@ -93,6 +94,17 @@ func (iq *intersectsQuery) Evaluate(geoData segment.GeoShapeV2Data) *util.Bitset

var reader *bytes.Reader

// Scratch buffers for decoding stored shapes, taken on the first decode that
// actually happens and returned at the end of the call. Evaluate runs on one
// goroutine per segment, so the pool is this call's alone while it holds it.
var bufPool *s2.GeoBufferPool
var releaseBufPool func()
defer func() {
if releaseBufPool != nil {
releaseBufPool()
}
}()

// filter out any maybeHits that do not have a bounding box that
// intersects the query bounding box
boxFilter := func(docNum int) {
Expand Down Expand Up @@ -121,7 +133,11 @@ func (iq *intersectsQuery) Evaluate(geoData segment.GeoShapeV2Data) *util.Bitset
return
}

docShape, err := geojson.ExtractShapesFromBytes(docShapeBytes, &reader, nil)
if bufPool == nil {
bufPool, releaseBufPool = shapeDecodeBuffers()
}

docShape, err := geojson.ExtractShapesFromBytes(docShapeBytes, &reader, bufPool)
if err != nil {
return
}
Expand Down
18 changes: 17 additions & 1 deletion geov2/query_within.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"github.com/blevesearch/bleve/v2/util"
index "github.com/blevesearch/bleve_index_api"
"github.com/blevesearch/geo/geojson"
"github.com/blevesearch/geo/s2"
segment "github.com/blevesearch/scorch_segment_api/v2"
)

Expand Down Expand Up @@ -94,6 +95,17 @@ func (wq *withinQuery) Evaluate(geoData segment.GeoShapeV2Data) *util.Bitset {

var reader *bytes.Reader

// Scratch buffers for decoding stored shapes, taken on the first decode that
// actually happens and returned at the end of the call. Evaluate runs on one
// goroutine per segment, so the pool is this call's alone while it holds it.
var bufPool *s2.GeoBufferPool
var releaseBufPool func()
defer func() {
if releaseBufPool != nil {
releaseBufPool()
}
}()

// filter out any maybeHits that do not have a bounding box that
// is within the query bounding box
boxFilter := func(docNum int) {
Expand Down Expand Up @@ -122,7 +134,11 @@ func (wq *withinQuery) Evaluate(geoData segment.GeoShapeV2Data) *util.Bitset {
return
}

docShape, err := geojson.ExtractShapesFromBytes(docShapeBytes, &reader, nil)
if bufPool == nil {
bufPool, releaseBufPool = shapeDecodeBuffers()
}

docShape, err := geojson.ExtractShapesFromBytes(docShapeBytes, &reader, bufPool)
if err != nil {
return
}
Expand Down
Loading