From 65761c9a9c74b2a70f81fd86f680ea720380e296 Mon Sep 17 00:00:00 2001 From: Likith B Date: Tue, 8 Sep 2026 19:37:53 +0530 Subject: [PATCH] MB-73805: Geo Optimizations --- geov2/buffer_pool.go | 57 +++++++++++++++++++++++++++++++++++++++ geov2/query_contains.go | 18 ++++++++++++- geov2/query_intersects.go | 18 ++++++++++++- geov2/query_within.go | 18 ++++++++++++- 4 files changed, 108 insertions(+), 3 deletions(-) create mode 100644 geov2/buffer_pool.go diff --git a/geov2/buffer_pool.go b/geov2/buffer_pool.go new file mode 100644 index 000000000..b9bbce9a6 --- /dev/null +++ b/geov2/buffer_pool.go @@ -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) } +} diff --git a/geov2/query_contains.go b/geov2/query_contains.go index 08b5f2a92..25ff0051d 100644 --- a/geov2/query_contains.go +++ b/geov2/query_contains.go @@ -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" ) @@ -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) { @@ -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 } diff --git a/geov2/query_intersects.go b/geov2/query_intersects.go index fc3756a6c..cd7c7ae85 100644 --- a/geov2/query_intersects.go +++ b/geov2/query_intersects.go @@ -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" ) @@ -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) { @@ -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 } diff --git a/geov2/query_within.go b/geov2/query_within.go index 913a1675e..2f544186b 100644 --- a/geov2/query_within.go +++ b/geov2/query_within.go @@ -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" ) @@ -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) { @@ -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 }