Unified Storage: Vector storage db setup and migrations (#122751)

* feat(unified-storage): add vector-storage database config section

* feat(unified-storage): add VectorBackend interface and types

* feat(unified-storage): add pgvector schema DDL and init

* feat(unified-storage): add vector SQL templates and request structs

Add 5 SQL template files (upsert, delete, search, get_latest_rv,
create_partition) and queries.go with typed request/response structs
following the sqltemplate pattern used in unified storage.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test(unified-storage): add vector SQL template snapshot tests

Add snapshot tests for all 5 SQL templates (upsert, delete, search,
get_latest_rv, create_partition) using PostgreSQL dialect only via the
mocks.CheckQuerySnapshots framework.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(unified-storage): add pgvectorBackend implementation

Implements the VectorBackend interface using pgvector-go for half-vector
similarity search, the Grafana db.DB/db.Tx abstractions for dbutil
compatibility, and a sync.Map partition cache for idempotent DDL.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test(unified-storage): add pgvectorBackend unit tests

Tests cover sanitizePartitionName, Upsert no-op with empty/nil slice,
Delete (all and stale), and GetLatestRV (value and no-rows cases)
using the existing sqlmock test infrastructure.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor(unified-storage): make vector search metadata filters generic

Replace hardcoded datasource_uids/query_languages filters with a generic
MetadataFilterEntry that works for any JSONB key. The SQL template now
uses {{ range .MetadataFilters }} instead of dashboard-specific conditionals.

* refactor(unified-storage): use migrator pattern for vector schema

Replace raw schema.sql DDL with Grafana's migrator pattern (same as
MigrateResourceStore). Each DDL statement is a tracked migration against
the separate vector database, enabling incremental schema evolution.

* devenv: add pgvector docker block for vector search integration tests

PostgreSQL with pgvector extension on port 5433. Usage:
  make devenv sources=pgvector

* feat(unified-storage): add vector database provider wiring

ProvideVectorBackend connects [database_vector] config to a working
pgvectorBackend: builds connection string, creates xorm engine, runs
migrations, wraps as db.DB. Returns (nil, nil) if not configured.

* test(unified-storage): add pgvector integration tests

Tests run against real PostgreSQL+pgvector via devenv block:
  make devenv sources=pgvector
  PGVECTOR_TEST_DB="..." go test -run TestIntegration ...

Covers: upsert, search with filters, GetLatestRV, delete stale,
upsert overwrite. Skips when PGVECTOR_TEST_DB is not set.

* chore: remove planning docs from repo

* chore: remove trivial vector config tests

* get tests passing

* partition on namespace + model so we dont mix embeddings

* wire up VectorBackend and feature flag

* fix wiring

* gofmt and provider func name change

* docs: spec for tenant watcher/deleter tracing

Performance-oriented tracing design: spans around labelling and
deletion to identify slow tenants/group-resources.

* chore: remove superpowers spec accidentally merged from local main

The docs/superpowers/specs/2026-04-20-tenant-tracing-design.md was
committed to local main and came along during a merge-back into this
branch. Per repo convention, superpowers planning docs stay outside
the repo.

* gofmt

* fix codeowners for pgvector docker block

* fix CI make gen-apps

* make update-workspace

* fix bad comment and modowners

* fix linter errors

* rename ff to vector_backend

* shard by table. Create tables on write. Remove rv dep for embeddings and track it globally in one table until we have a persistent queue

* use halfvec(1024) for embeddings to match what GA uses. Fix some comments.

* add some vector validation

* gofmt

* update partitioning strategy... again. One table per resource, tenants with many resources get their own dedicated partition created dynamically that has hnsw index, all other tenants share a single partition with no hnsw index

* add gin index to metadata when creating new partition

* move promoter into vector backend

* make gen-jsonnet

* use partitioning for resource tables instead. Makes things simpler and makes cross resource search easier

* remove top level default partition - not used

* add some comments

* promoter uses partial index instead of partitions

* shortens hnsw index name so theres more room for resource name since theres a 63 char max

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
owensmallwood 2026-04-28 13:45:35 -06:00 committed by GitHub
parent 3727e122ed
commit e375a40a0f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
49 changed files with 1966 additions and 73 deletions

1
.github/CODEOWNERS vendored
View file

@ -317,6 +317,7 @@
/devenv/docker/blocks/mysql_opendata/ @grafana/data-sources-plugins
/devenv/docker/blocks/mysql_tests/ @grafana/data-sources-plugins
/devenv/docker/blocks/opentsdb/ @grafana/data-sources-plugins
/devenv/docker/blocks/pgvector/ @grafana/grafana-search-and-storage
/devenv/docker/blocks/postgres/ @grafana/data-sources-plugins
/devenv/docker/blocks/postgres_tests/ @grafana/data-sources-plugins
/devenv/docker/blocks/prometheus/ @grafana/data-sources-plugins

View file

@ -888,6 +888,8 @@ github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s=
github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw=
github.com/pgvector/pgvector-go v0.3.0 h1:Ij+Yt78R//uYqs3Zk35evZFvr+G0blW0OUN+Q2D1RWc=
github.com/pgvector/pgvector-go v0.3.0/go.mod h1:duFy+PXWfW7QQd5ibqutBO4GxLsUZ9RVXhFZGIBsWSA=
github.com/pierrec/lz4/v4 v4.1.25 h1:kocOqRffaIbU5djlIBr7Wh+cx82C0vtFb0fOurZHqD0=
github.com/pierrec/lz4/v4 v4.1.25/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=

View file

@ -313,6 +313,7 @@ require (
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/perimeterx/marshmallow v1.1.5 // indirect
github.com/pgvector/pgvector-go v0.3.0 // indirect
github.com/pierrec/lz4/v4 v4.1.25 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/pkg/errors v0.9.1 // indirect

View file

@ -82,6 +82,8 @@ cuelang.org/go v0.11.1/go.mod h1:PBY6XvPUswPPJ2inpvUozP9mebDVTXaeehQikhZPBz0=
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
entgo.io/ent v0.14.3 h1:wokAV/kIlH9TeklJWGGS7AYJdVckr0DloWjIcO9iIIQ=
entgo.io/ent v0.14.3/go.mod h1:aDPE/OziPEu8+OWbzy4UlvWmD2/kbRuWfK2A40hcxJM=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
@ -424,6 +426,10 @@ github.com/go-openapi/testify/v2 v2.4.1 h1:zB34HDKj4tHwyUQHrUkpV0Q0iXQ6dUCOQtIqn
github.com/go-openapi/testify/v2 v2.4.1/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
github.com/go-openapi/validate v0.25.2 h1:12NsfLAwGegqbGWr2CnvT65X/Q2USJipmJ9b7xDJZz0=
github.com/go-openapi/validate v0.25.2/go.mod h1:Pgl1LpPPGFnZ+ys4/hTlDiRYQdI1ocKypgE+8Q8BLfY=
github.com/go-pg/pg/v10 v10.11.0 h1:CMKJqLgTrfpE/aOVeLdybezR2om071Vh38OLZjsyMI0=
github.com/go-pg/pg/v10 v10.11.0/go.mod h1:4BpHRoxE61y4Onpof3x1a2SQvi9c+q1dJnrNdMjsroA=
github.com/go-pg/zerochecker v0.2.0 h1:pp7f72c3DobMWOb2ErtZsnrPaSvHd2W4o9//8HtF4mU=
github.com/go-pg/zerochecker v0.2.0/go.mod h1:NJZ4wKL0NmTtz0GKCoJ8kym6Xn/EQzXRl2OnAe7MmDo=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
@ -708,6 +714,10 @@ github.com/jhump/goprotoc v0.5.0/go.mod h1:VrbvcYrQOrTi3i0Vf+m+oqQWk9l72mjkJCYo7
github.com/jhump/protoreflect v1.11.0/go.mod h1:U7aMIjN0NWq9swDP7xDdoMfRHb35uiuTd3Z9nFXJf5E=
github.com/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5XumQh94=
github.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jmespath-community/go-jmespath v1.1.1 h1:bFikPhsi/FdmlZhVgSCd2jj1e7G/rw+zyQfyg5UF+L4=
github.com/jmespath-community/go-jmespath v1.1.1/go.mod h1:4gOyFJsR/Gk+05RgTKYrifT7tBPWD8Lubtb5jRrfy9I=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
@ -908,6 +918,8 @@ github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s=
github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw=
github.com/pgvector/pgvector-go v0.3.0 h1:Ij+Yt78R//uYqs3Zk35evZFvr+G0blW0OUN+Q2D1RWc=
github.com/pgvector/pgvector-go v0.3.0/go.mod h1:duFy+PXWfW7QQd5ibqutBO4GxLsUZ9RVXhFZGIBsWSA=
github.com/pierrec/lz4/v4 v4.1.25 h1:kocOqRffaIbU5djlIBr7Wh+cx82C0vtFb0fOurZHqD0=
github.com/pierrec/lz4/v4 v4.1.25/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
@ -1040,6 +1052,8 @@ github.com/tjhop/slog-gokit v0.1.6 h1:aRNB5omFUPs/5k40Ao1s7aBZHaKhNNu1JLc9ukF0qB
github.com/tjhop/slog-gokit v0.1.6/go.mod h1:yA48zAHvV+Sg4z4VRyeFyFUNNXd3JY5Zg84u3USICq0=
github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75 h1:6fotK7otjonDflCTK0BCfls4SPy3NcCVb5dqqmbRknE=
github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75/go.mod h1:KO6IkyS8Y3j8OdNO85qEYBsRPuteD+YciPomcXdrMnk=
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo=
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs=
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o=
github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk=
@ -1047,6 +1061,20 @@ github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVK
github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U=
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/uptrace/bun v1.1.12 h1:sOjDVHxNTuM6dNGaba0wUuz7KvDE1BmNu9Gqs2gJSXQ=
github.com/uptrace/bun v1.1.12/go.mod h1:NPG6JGULBeQ9IU6yHp7YGELRa5Agmd7ATZdz4tGZ6z0=
github.com/uptrace/bun/dialect/pgdialect v1.1.12 h1:m/CM1UfOkoBTglGO5CUTKnIKKOApOYxkcP2qn0F9tJk=
github.com/uptrace/bun/dialect/pgdialect v1.1.12/go.mod h1:Ij6WIxQILxLlL2frUBxUBOZJtLElD2QQNDcu/PWDHTc=
github.com/uptrace/bun/driver/pgdriver v1.1.12 h1:3rRWB1GK0psTJrHwxzNfEij2MLibggiLdTqjTtfHc1w=
github.com/uptrace/bun/driver/pgdriver v1.1.12/go.mod h1:ssYUP+qwSEgeDDS1xm2XBip9el1y9Mi5mTAvLoiADLM=
github.com/vmihailenco/bufpool v0.1.11 h1:gOq2WmBrq0i2yW5QJ16ykccQ4wH9UyEsgLm6czKAd94=
github.com/vmihailenco/bufpool v0.1.11/go.mod h1:AFf/MOy3l2CFTKbxwt0mp2MwnqjNEs5H/UxrkA5jxTQ=
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
github.com/vmihailenco/tagparser v0.1.2 h1:gnjoVuB/kljJ5wICEEOpx98oXMWPLj22G67Vbd1qPqc=
github.com/vmihailenco/tagparser v0.1.2/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI=
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
github.com/wk8/go-ordered-map v1.0.0 h1:BV7z+2PaK8LTSd/mWgY12HyMAo5CEgkHqbkVq2thqr8=
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
@ -1717,6 +1745,10 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/postgres v1.5.4 h1:Iyrp9Meh3GmbSuyIAGyjkN+n9K+GHX9b9MqsTL4EJCo=
gorm.io/driver/postgres v1.5.4/go.mod h1:Bgo89+h0CRcdA33Y6frlaHHVuTdOf87pmyzwW9C/BH0=
gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls=
gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
@ -1743,6 +1775,8 @@ k8s.io/kube-openapi v0.0.0-20260127142750-a19766b6e2d4 h1:HhDfevmPS+OalTjQRKbTHp
k8s.io/kube-openapi v0.0.0-20260127142750-a19766b6e2d4/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ=
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck=
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
mellium.im/sasl v0.3.1 h1:wE0LW6g7U83vhvxjC1IY8DnXM+EU095yeo8XClvCdfo=
mellium.im/sasl v0.3.1/go.mod h1:xm59PUYpZHhgQ9ZqoJ5QaCqzWMi8IeS49dhp6plPCzw=
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.32.0 h1:hjG66bI/kqIPX1b2yT6fr/jt+QedtP2fqojG2VrFuVw=

View file

@ -0,0 +1 @@
pgvector_version=pg17

View file

@ -0,0 +1,16 @@
pgvector:
image: pgvector/pgvector:${pgvector_version}
environment:
POSTGRES_USER: grafana
POSTGRES_PASSWORD: password
POSTGRES_DB: grafana_vectors
ports:
- "5433:5432"
volumes:
- ./docker/blocks/pgvector/init.sql:/docker-entrypoint-initdb.d/init.sql
command: postgres -c log_connections=on -c log_disconnections=on -c log_destination=stderr
healthcheck:
test: [ "CMD", "pg_isready", "-q", "-d", "grafana_vectors", "-U", "grafana" ]
timeout: 45s
interval: 10s
retries: 10

View file

@ -0,0 +1 @@
CREATE EXTENSION IF NOT EXISTS vector;

4
go.mod
View file

@ -338,7 +338,7 @@ require (
github.com/ProtonMail/go-crypto v1.3.0 // indirect
github.com/RoaringBitmap/roaring/v2 v2.4.5 // indirect
github.com/Yiling-J/theine-go v0.6.2 // indirect
github.com/agext/levenshtein v1.2.1 // indirect
github.com/agext/levenshtein v1.2.3 // indirect
github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b // indirect
github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 // indirect
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
@ -698,6 +698,8 @@ require (
software.sslmate.com/src/go-pkcs12 v0.7.0 // indirect
)
require github.com/pgvector/pgvector-go v0.3.0 // @grafana/grafana-search-and-storage
replace (
// Use fork of crewjam/saml with fixes for some issues until changes get merged into upstream
github.com/crewjam/saml => github.com/grafana/saml v0.4.15-0.20240917091248-ae3bbdad8a56

38
go.sum
View file

@ -636,6 +636,8 @@ cuelang.org/go v0.11.1/go.mod h1:PBY6XvPUswPPJ2inpvUozP9mebDVTXaeehQikhZPBz0=
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
entgo.io/ent v0.14.3 h1:wokAV/kIlH9TeklJWGGS7AYJdVckr0DloWjIcO9iIIQ=
entgo.io/ent v0.14.3/go.mod h1:aDPE/OziPEu8+OWbzy4UlvWmD2/kbRuWfK2A40hcxJM=
filippo.io/age v1.2.1 h1:X0TZjehAZylOIj4DubWYU1vWQxv9bJpo+Uu2/LGhi1o=
filippo.io/age v1.2.1/go.mod h1:JL9ew2lTN+Pyft4RiNGguFfOpewKwSHm5ayKD/A4004=
gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8=
@ -770,8 +772,8 @@ github.com/Workiva/go-datastructures v1.1.5 h1:5YfhQ4ry7bZc2Mc7R0YZyYwpf5c6t1cEF
github.com/Workiva/go-datastructures v1.1.5/go.mod h1:1yZL+zfsztete+ePzZz/Zb1/t5BnDuE2Ya2MMGhzP6A=
github.com/Yiling-J/theine-go v0.6.2 h1:1GeoXeQ0O0AUkiwj2S9Jc0Mzx+hpqzmqsJ4kIC4M9AY=
github.com/Yiling-J/theine-go v0.6.2/go.mod h1:08QpMa5JZ2pKN+UJCRrCasWYO1IKCdl54Xa836rpmDU=
github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8=
github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo=
github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm/4RlzPXRlREEwqTHAN3T56Bv2ITsFT3gY=
github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19/go.mod h1:T13YZdzov6OU0A1+RfKZiZN9ca6VeKdBdyDV+BY97Tk=
github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
@ -1313,6 +1315,10 @@ github.com/go-openapi/validate v0.25.2 h1:12NsfLAwGegqbGWr2CnvT65X/Q2USJipmJ9b7x
github.com/go-openapi/validate v0.25.2/go.mod h1:Pgl1LpPPGFnZ+ys4/hTlDiRYQdI1ocKypgE+8Q8BLfY=
github.com/go-pdf/fpdf v0.5.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M=
github.com/go-pdf/fpdf v0.6.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M=
github.com/go-pg/pg/v10 v10.11.0 h1:CMKJqLgTrfpE/aOVeLdybezR2om071Vh38OLZjsyMI0=
github.com/go-pg/pg/v10 v10.11.0/go.mod h1:4BpHRoxE61y4Onpof3x1a2SQvi9c+q1dJnrNdMjsroA=
github.com/go-pg/zerochecker v0.2.0 h1:pp7f72c3DobMWOb2ErtZsnrPaSvHd2W4o9//8HtF4mU=
github.com/go-pg/zerochecker v0.2.0/go.mod h1:NJZ4wKL0NmTtz0GKCoJ8kym6Xn/EQzXRl2OnAe7MmDo=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
@ -1796,6 +1802,10 @@ github.com/jhump/goprotoc v0.5.0/go.mod h1:VrbvcYrQOrTi3i0Vf+m+oqQWk9l72mjkJCYo7
github.com/jhump/protoreflect v1.11.0/go.mod h1:U7aMIjN0NWq9swDP7xDdoMfRHb35uiuTd3Z9nFXJf5E=
github.com/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5XumQh94=
github.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jmespath-community/go-jmespath v1.1.1 h1:bFikPhsi/FdmlZhVgSCd2jj1e7G/rw+zyQfyg5UF+L4=
github.com/jmespath-community/go-jmespath v1.1.1/go.mod h1:4gOyFJsR/Gk+05RgTKYrifT7tBPWD8Lubtb5jRrfy9I=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
@ -2142,6 +2152,8 @@ github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s=
github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw=
github.com/pgvector/pgvector-go v0.3.0 h1:Ij+Yt78R//uYqs3Zk35evZFvr+G0blW0OUN+Q2D1RWc=
github.com/pgvector/pgvector-go v0.3.0/go.mod h1:duFy+PXWfW7QQd5ibqutBO4GxLsUZ9RVXhFZGIBsWSA=
github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0=
github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY=
github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=
@ -2423,6 +2435,8 @@ github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9R
github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=
github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75 h1:6fotK7otjonDflCTK0BCfls4SPy3NcCVb5dqqmbRknE=
github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75/go.mod h1:KO6IkyS8Y3j8OdNO85qEYBsRPuteD+YciPomcXdrMnk=
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo=
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs=
github.com/trivago/tgo v1.0.7/go.mod h1:w4dpD+3tzNIIiIfkWWa85w5/B77tlvdZckQ+6PkFnhc=
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
github.com/ua-parser/uap-go v0.0.0-20250213224047-9c035f085b90 h1:rB0J+hLNltG1Qv+UF+MkdFz89XMps5BOAFJN4xWjc+s=
@ -2440,6 +2454,12 @@ github.com/unknwon/com v1.0.1/go.mod h1:tOOxU81rwgoCLoOVVPHb6T/wt8HZygqH5id+GNnl
github.com/unknwon/log v0.0.0-20150304194804-e617c87089d3/go.mod h1:1xEUf2abjfP92w2GZTV+GgaRxXErwRXcClbUwrNJffU=
github.com/unknwon/log v0.0.0-20200308114134-929b1006e34a h1:vcrhXnj9g9PIE+cmZgaPSwOyJ8MAQTRmsgGrB0x5rF4=
github.com/unknwon/log v0.0.0-20200308114134-929b1006e34a/go.mod h1:1xEUf2abjfP92w2GZTV+GgaRxXErwRXcClbUwrNJffU=
github.com/uptrace/bun v1.1.12 h1:sOjDVHxNTuM6dNGaba0wUuz7KvDE1BmNu9Gqs2gJSXQ=
github.com/uptrace/bun v1.1.12/go.mod h1:NPG6JGULBeQ9IU6yHp7YGELRa5Agmd7ATZdz4tGZ6z0=
github.com/uptrace/bun/dialect/pgdialect v1.1.12 h1:m/CM1UfOkoBTglGO5CUTKnIKKOApOYxkcP2qn0F9tJk=
github.com/uptrace/bun/dialect/pgdialect v1.1.12/go.mod h1:Ij6WIxQILxLlL2frUBxUBOZJtLElD2QQNDcu/PWDHTc=
github.com/uptrace/bun/driver/pgdriver v1.1.12 h1:3rRWB1GK0psTJrHwxzNfEij2MLibggiLdTqjTtfHc1w=
github.com/uptrace/bun/driver/pgdriver v1.1.12/go.mod h1:ssYUP+qwSEgeDDS1xm2XBip9el1y9Mi5mTAvLoiADLM=
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/cli v1.22.17 h1:SYzXoiPfQjHBbkYxbew5prZHS1TOLT3ierW8SYLqtVQ=
github.com/urfave/cli v1.22.17/go.mod h1:b0ht0aqgH/6pBYzzxURyrM4xXNgsoT/n2ZzwQiEhNVo=
@ -2447,6 +2467,14 @@ github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU=
github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/vmihailenco/bufpool v0.1.11 h1:gOq2WmBrq0i2yW5QJ16ykccQ4wH9UyEsgLm6czKAd94=
github.com/vmihailenco/bufpool v0.1.11/go.mod h1:AFf/MOy3l2CFTKbxwt0mp2MwnqjNEs5H/UxrkA5jxTQ=
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
github.com/vmihailenco/tagparser v0.1.2 h1:gnjoVuB/kljJ5wICEEOpx98oXMWPLj22G67Vbd1qPqc=
github.com/vmihailenco/tagparser v0.1.2/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI=
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
github.com/vultr/govultr/v2 v2.17.2 h1:gej/rwr91Puc/tgh+j33p/BLR16UrIPnSr+AIwYWZQs=
github.com/vultr/govultr/v2 v2.17.2/go.mod h1:ZFOKGWmgjytfyjeyAdhQlSWwTjh2ig+X49cAp50dzXI=
github.com/wk8/go-ordered-map v1.0.0 h1:BV7z+2PaK8LTSd/mWgY12HyMAo5CEgkHqbkVq2thqr8=
@ -3538,6 +3566,10 @@ gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/postgres v1.5.4 h1:Iyrp9Meh3GmbSuyIAGyjkN+n9K+GHX9b9MqsTL4EJCo=
gorm.io/driver/postgres v1.5.4/go.mod h1:Bgo89+h0CRcdA33Y6frlaHHVuTdOf87pmyzwW9C/BH0=
gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls=
gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
@ -3585,6 +3617,8 @@ k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzk
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
mellium.im/sasl v0.3.1 h1:wE0LW6g7U83vhvxjC1IY8DnXM+EU095yeo8XClvCdfo=
mellium.im/sasl v0.3.1/go.mod h1:xm59PUYpZHhgQ9ZqoJ5QaCqzWMi8IeS49dhp6plPCzw=
modernc.org/cc/v3 v3.36.0/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI=
modernc.org/cc/v3 v3.36.2/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI=
modernc.org/cc/v3 v3.36.3/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI=

View file

@ -1,3 +1,5 @@
ariga.io/atlas v0.32.0 h1:y+77nueMrExLiKlz1CcPKh/nU7VSlWfBbwCShsJyvCw=
ariga.io/atlas v0.32.0/go.mod h1:Oe1xWPuu5q9LzyrWfbZmEZxFYeu4BHTyzfjeW2aZp/w=
atomicgo.dev/assert v0.0.2/go.mod h1:ut4NcI3QDdJtlmAxQULOmA13Gz6e2DWbSAS8RUOmNYQ=
atomicgo.dev/cursor v0.2.0 h1:H6XN5alUJ52FZZUkI7AlJbUc1aW38GWZalpYRPpoPOw=
atomicgo.dev/cursor v0.2.0/go.mod h1:Lr4ZJB3U7DfPPOkbH7/6TOtJ4vFGHlgj1nc+n900IpU=
@ -274,6 +276,8 @@ contrib.go.opencensus.io/exporter/ocagent v0.6.0 h1:Z1n6UAyr0QwM284yUuh5Zd8JlvxU
contrib.go.opencensus.io/exporter/stackdriver v0.13.15-0.20230702191903-2de6d2748484 h1:xRc46S76eyn4ZF3jWX8I+aUSKVLw5EQ1aDvHwfV5W1o=
contrib.go.opencensus.io/exporter/stackdriver v0.13.15-0.20230702191903-2de6d2748484/go.mod h1:uxw+4/0SiKbbVSD/F2tk5pJTdVcfIBBcsQ8gwcu4X+E=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9 h1:VpgP7xuJadIUuKccphEpTJnWhS2jkQyMt6Y7pJCD7fY=
entgo.io/ent v0.14.3 h1:wokAV/kIlH9TeklJWGGS7AYJdVckr0DloWjIcO9iIIQ=
entgo.io/ent v0.14.3/go.mod h1:aDPE/OziPEu8+OWbzy4UlvWmD2/kbRuWfK2A40hcxJM=
gioui.org v0.0.0-20210308172011-57750fc8a0a6 h1:K72hopUosKG3ntOPNG4OzzbuhxGuVf06fa2la1/H/Ho=
gioui.org v0.2.0 h1:RbzDn1h/pCVf/q44ImQSa/J3MIFpY3OWphzT/Tyei+w=
gioui.org v0.2.0/go.mod h1:1H72sKEk/fNFV+l0JNeM2Dt3co3Y4uaQcD+I+/GQ0e4=
@ -394,6 +398,8 @@ github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kk
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
github.com/andybalholm/stroke v0.0.0-20221221101821-bd29b49d73f0 h1:uF5Q/hWnDU1XZeT6CsrRSxHLroUSEYYO3kgES+yd+So=
github.com/andybalholm/stroke v0.0.0-20221221101821-bd29b49d73f0/go.mod h1:ccdDYaY5+gO+cbnQdFxEXqfy0RkoV25H3jLXUDNM3wg=
github.com/ankane/disco-go v0.1.2 h1:Amm1UV3oLttAJM18MW7zofTDAXbuE9BEQUt3YPF6pVI=
github.com/ankane/disco-go v0.1.2/go.mod h1:nkR7DLW+KkXeRRAsWk6poMTpTOWp9/4iKYGDwg8dSS0=
github.com/antchfx/xmlquery v1.4.4 h1:mxMEkdYP3pjKSftxss4nUHfjBhnMk4imGoR96FRY2dg=
github.com/antchfx/xmlquery v1.4.4/go.mod h1:AEPEEPYE9GnA2mj5Ur2L5Q5/2PycJ0N9Fusrx9b12fc=
github.com/antchfx/xpath v1.3.4 h1:1ixrW1VnXd4HurCj7qnqnR0jo14g8JMe20Fshg1Vgz4=
@ -478,6 +484,8 @@ github.com/blevesearch/snowball v0.6.1/go.mod h1:ZF0IBg5vgpeoUhnMza2v0A/z8m1cWPl
github.com/blevesearch/stempel v0.2.0 h1:CYzVPaScODMvgE9o+kf6D4RJ/VRomyi9uHF+PtB+Afc=
github.com/blevesearch/stempel v0.2.0/go.mod h1:wjeTHqQv+nQdbPuJ/YcvOjTInA2EIc6Ks1FoSUzSLvc=
github.com/bmatcuk/doublestar v1.1.1 h1:YroD6BJCZBYx06yYFEWvUuKVWQn3vLLQAVmDmvTSaiQ=
github.com/bmatcuk/doublestar v1.3.4 h1:gPypJ5xD31uhX6Tf54sDPUOBXTqKH4c9aPY66CyQrS0=
github.com/bmatcuk/doublestar v1.3.4/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE=
github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR5wKP38=
github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY=
@ -700,6 +708,8 @@ github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbV
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-openapi/analysis v0.23.0/go.mod h1:9mz9ZWaSlV8TvjQHLl2mUW2PbZtemkE8yA5v22ohupo=
github.com/go-openapi/errors v0.22.2/go.mod h1:+n/5UdIqdVnLIJ6Q9Se8HNGUXYaY6CN8ImWzfi/Gzp0=
github.com/go-openapi/inflect v0.21.0 h1:FoBjBTQEcbg2cJUWX6uwL9OyIW8eqc9k4KhN4lfbeYk=
github.com/go-openapi/inflect v0.21.0/go.mod h1:INezMuUu7SJQc2AyR3WO0DqqYUJSj8Kb4hBd7WtjlAw=
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
github.com/go-openapi/loads v0.22.0/go.mod h1:yLsaTCS92mnSAZX5WWoxszLj0u+Ojl+Zs5Stn1oF+rs=
@ -712,6 +722,10 @@ github.com/go-openapi/testify/enable/yaml/v2 v2.4.0/go.mod h1:14iV8jyyQlinc9StD7
github.com/go-openapi/testify/v2 v2.4.0/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
github.com/go-openapi/validate v0.24.0/go.mod h1:iyeX1sEufmv3nPbBdX3ieNviWnOZaJ1+zquzJEf2BAQ=
github.com/go-pdf/fpdf v0.6.0 h1:MlgtGIfsdMEEQJr2le6b/HNr1ZlQwxyWr77r2aj2U/8=
github.com/go-pg/pg/v10 v10.11.0 h1:CMKJqLgTrfpE/aOVeLdybezR2om071Vh38OLZjsyMI0=
github.com/go-pg/pg/v10 v10.11.0/go.mod h1:4BpHRoxE61y4Onpof3x1a2SQvi9c+q1dJnrNdMjsroA=
github.com/go-pg/zerochecker v0.2.0 h1:pp7f72c3DobMWOb2ErtZsnrPaSvHd2W4o9//8HtF4mU=
github.com/go-pg/zerochecker v0.2.0/go.mod h1:NJZ4wKL0NmTtz0GKCoJ8kym6Xn/EQzXRl2OnAe7MmDo=
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
@ -853,6 +867,10 @@ github.com/jedib0t/go-pretty/v6 v6.6.7 h1:m+LbHpm0aIAPLzLbMfn8dc3Ht8MW7lsSO4MPIt
github.com/jedib0t/go-pretty/v6 v6.6.7/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU=
github.com/jhump/gopoet v0.1.0 h1:gYjOPnzHd2nzB37xYQZxj4EIQNpBrBskRqQQ3q4ZgSg=
github.com/jhump/goprotoc v0.5.0 h1:Y1UgUX+txUznfqcGdDef8ZOVlyQvnV0pKWZH08RmZuo=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/joncrlsn/dque v0.0.0-20211108142734-c2ef48c5192a h1:sfe532Ipn7GX0V6mHdynBk393rDmqgI0QmjLK7ct7TU=
@ -1241,6 +1259,8 @@ github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhso
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0=
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo=
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs=
github.com/trivago/tgo v1.0.7 h1:uaWH/XIy9aWYWpjm2CU3RpcqZXmX2ysQ9/Go+d9gyrM=
github.com/tursodatabase/libsql-client-go v0.0.0-20240902231107-85af5b9d094d h1:dOMI4+zEbDI37KGb0TI44GUAwxHF9cMsIoDTJ7UmgfU=
github.com/tursodatabase/libsql-client-go v0.0.0-20240902231107-85af5b9d094d/go.mod h1:l8xTsYB90uaVdMHXMCxKKLSgw5wLYBwBKKefNIUnm9s=
@ -1269,14 +1289,24 @@ github.com/uber/jaeger-client-go v2.28.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMW
github.com/uber/jaeger-lib v2.2.0+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U=
github.com/urfave/cli/v3 v3.6.2 h1:lQuqiPrZ1cIz8hz+HcrG0TNZFxU70dPZ3Yl+pSrH9A8=
github.com/urfave/cli/v3 v3.6.2/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
github.com/uptrace/bun v1.1.12 h1:sOjDVHxNTuM6dNGaba0wUuz7KvDE1BmNu9Gqs2gJSXQ=
github.com/uptrace/bun v1.1.12/go.mod h1:NPG6JGULBeQ9IU6yHp7YGELRa5Agmd7ATZdz4tGZ6z0=
github.com/uptrace/bun/dialect/pgdialect v1.1.12 h1:m/CM1UfOkoBTglGO5CUTKnIKKOApOYxkcP2qn0F9tJk=
github.com/uptrace/bun/dialect/pgdialect v1.1.12/go.mod h1:Ij6WIxQILxLlL2frUBxUBOZJtLElD2QQNDcu/PWDHTc=
github.com/uptrace/bun/driver/pgdriver v1.1.12 h1:3rRWB1GK0psTJrHwxzNfEij2MLibggiLdTqjTtfHc1w=
github.com/uptrace/bun/driver/pgdriver v1.1.12/go.mod h1:ssYUP+qwSEgeDDS1xm2XBip9el1y9Mi5mTAvLoiADLM=
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/vertica/vertica-sql-go v1.3.3 h1:fL+FKEAEy5ONmsvya2WH5T8bhkvY27y/Ik3ReR2T+Qw=
github.com/vertica/vertica-sql-go v1.3.3/go.mod h1:jnn2GFuv+O2Jcjktb7zyc4Utlbu9YVqpHH/lx63+1M4=
github.com/vertica/vertica-sql-go v1.3.5 h1:IrfH2WIgzZ45yDHyjVFrXU2LuKNIjF5Nwi90a6cfgUI=
github.com/vertica/vertica-sql-go v1.3.5/go.mod h1:jnn2GFuv+O2Jcjktb7zyc4Utlbu9YVqpHH/lx63+1M4=
github.com/vmihailenco/bufpool v0.1.11 h1:gOq2WmBrq0i2yW5QJ16ykccQ4wH9UyEsgLm6czKAd94=
github.com/vmihailenco/bufpool v0.1.11/go.mod h1:AFf/MOy3l2CFTKbxwt0mp2MwnqjNEs5H/UxrkA5jxTQ=
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
github.com/vmihailenco/tagparser v0.1.2 h1:gnjoVuB/kljJ5wICEEOpx98oXMWPLj22G67Vbd1qPqc=
github.com/vmihailenco/tagparser v0.1.2/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI=
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
github.com/willf/bitset v1.1.11 h1:N7Z7E9UvjW+sGsEl7k/SJrvY2reP1A07MrGuCjIOjRE=
@ -1317,6 +1347,8 @@ github.com/yosssi/ace v0.0.5 h1:tUkIP/BLdKqrlrPwcmH0shwEEhTRHoGnc1wFIWmaBUA=
github.com/yosssi/ace v0.0.5/go.mod h1:ALfIzm2vT7t5ZE7uoIZqF3TQ7SAOyupFZnkrF5id+K0=
github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA=
github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
github.com/zclconf/go-cty-yaml v1.1.0 h1:nP+jp0qPHv2IhUVqmQSzjvqAWcObN0KBkUl2rWBdig0=
github.com/zclconf/go-cty-yaml v1.1.0/go.mod h1:9YLUH4g7lOhVWqUbctnVlZ5KLpg7JAprQNgxSZ1Gyxs=
github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM=
github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4=
github.com/zenazn/goji v1.0.1 h1:4lbD8Mx2h7IvloP7r2C0D6ltZP6Ufip8Hn0wmSK5LR8=
@ -1788,6 +1820,10 @@ gopkg.in/resty.v1 v1.12.0 h1:CuXP0Pjfw9rOuY6EP+UvtNvt5DSqHpIxILZKT/quCZI=
gopkg.in/src-d/go-billy.v4 v4.3.2 h1:0SQA1pRztfTFx2miS8sA97XvooFeNOmvUenF4o0EcVg=
gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98=
gopkg.in/vmihailenco/msgpack.v2 v2.9.2 h1:gjPqo9orRVlSAH/065qw3MsFCDpH7fa1KpiizXyllY4=
gorm.io/driver/postgres v1.5.4 h1:Iyrp9Meh3GmbSuyIAGyjkN+n9K+GHX9b9MqsTL4EJCo=
gorm.io/driver/postgres v1.5.4/go.mod h1:Bgo89+h0CRcdA33Y6frlaHHVuTdOf87pmyzwW9C/BH0=
gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls=
gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
honnef.co/go/tools v0.3.2 h1:ytYb4rOqyp1TSa2EPvNVwtPQJctSELKaMyLfqNP4+34=
honnef.co/go/tools v0.3.2/go.mod h1:jzwdWgg7Jdq75wlfblQxO4neNaFFSvgc1tD5Wv8U0Yw=
howett.net/plist v1.0.1 h1:37GdZ8tP09Q35o9ych3ehygcsL+HqKSwzctveSlarvM=
@ -1802,6 +1838,8 @@ k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I=
k8s.io/klog/v2 v2.80.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI=
mellium.im/sasl v0.3.1 h1:wE0LW6g7U83vhvxjC1IY8DnXM+EU095yeo8XClvCdfo=
mellium.im/sasl v0.3.1/go.mod h1:xm59PUYpZHhgQ9ZqoJ5QaCqzWMi8IeS49dhp6plPCzw=
modernc.org/cc/v3 v3.36.3 h1:uISP3F66UlixxWEcKuIWERa4TwrZENHSL8tWxZz8bHg=
modernc.org/cc/v3 v3.41.0/go.mod h1:Ni4zjJYJ04CDOhG7dn640WGfwBzfE0ecX8TyMB0Fv0Y=
modernc.org/ccgo/v3 v3.16.9 h1:AXquSwg7GuMk11pIdw7fmO1Y/ybgazVkMhsZWCV0mHM=

View file

@ -16,6 +16,7 @@ const (
InstrumentationServer string = "instrumentation-server"
GRPCServer string = "grpc-server"
UnifiedBackend string = "unified-backend"
UnifiedVectorBackend string = "unified-vector-backend"
FrontendServer string = "frontend-server"
OperatorServer string = "operator"
)
@ -26,8 +27,8 @@ var dependencyMap = map[string][]string{
GrafanaAPIServer: {InstrumentationServer},
// TODO: remove SearchServerRing once we only use sharding in SearchServer
StorageServer: {UnifiedBackend, InstrumentationServer, GRPCServer, SearchServerRing},
SearchServer: {UnifiedBackend, InstrumentationServer, GRPCServer, SearchServerRing},
StorageServer: {UnifiedBackend, UnifiedVectorBackend, InstrumentationServer, GRPCServer, SearchServerRing},
SearchServer: {UnifiedBackend, UnifiedVectorBackend, InstrumentationServer, GRPCServer, SearchServerRing},
ZanzanaServer: {InstrumentationServer},
AuthnServer: {InstrumentationServer},

View file

@ -34,6 +34,7 @@ import (
"github.com/grafana/grafana/pkg/services/licensing"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/storage/unified/resource"
"github.com/grafana/grafana/pkg/storage/unified/search/vector"
"github.com/grafana/grafana/pkg/storage/unified/sql"
"go.opentelemetry.io/otel"
)
@ -138,6 +139,7 @@ type ModuleServer struct {
isInitialized bool
mtx sync.Mutex
storageBackend resource.StorageBackend
vectorBackend vector.VectorBackend
searchClient resourcepb.ResourceIndexClient
storageMetrics *resource.StorageMetrics
indexMetrics *resource.BleveIndexMetrics
@ -243,6 +245,24 @@ func (s *ModuleServer) Run() error {
return services.NewIdleService(nil, nil).WithName(modules.UnifiedBackend), nil
})
m.RegisterInvisibleModule(modules.UnifiedVectorBackend, func() (services.Service, error) {
// StorageServer owns the schema and runs the promoter; other targets
// get a read-only backend.
ownsSchema := m.IsModuleEnabled(modules.StorageServer)
if s.vectorBackend == nil {
var err error
s.vectorBackend, err = vector.InitVectorBackend(context.Background(), s.cfg, ownsSchema)
if err != nil {
return nil, err
}
}
if s.vectorBackend != nil && ownsSchema {
runFn := func(ctx context.Context) error { return s.vectorBackend.Run(ctx) }
return services.NewBasicService(nil, runFn, nil).WithName(modules.UnifiedVectorBackend), nil
}
return services.NewIdleService(nil, nil).WithName(modules.UnifiedVectorBackend), nil
})
m.RegisterModule(modules.MemberlistKV, s.initMemberlistKV)
m.RegisterModule(modules.SearchServerRing, s.initSearchServerRing)
m.RegisterModule(modules.SearchServerDistributor, func() (services.Service, error) {
@ -286,7 +306,7 @@ func (s *ModuleServer) Run() error {
}
indexMetrics = s.indexMetrics
}
svc, err := sql.ProvideUnifiedStorageGrpcService(s.cfg, s.features, s.log, s.registerer, docBuilders, s.storageMetrics, indexMetrics, s.searchServerRing, s.MemberlistKVConfig, s.httpServerRouter, s.storageBackend, s.searchClient, s.grpcService, s.StorageServiceOptions...)
svc, err := sql.ProvideUnifiedStorageGrpcService(s.cfg, s.features, s.log, s.registerer, docBuilders, s.storageMetrics, indexMetrics, s.searchServerRing, s.MemberlistKVConfig, s.httpServerRouter, s.storageBackend, s.vectorBackend, s.searchClient, s.grpcService, s.StorageServiceOptions...)
if err != nil {
return nil, err
}
@ -317,7 +337,7 @@ func (s *ModuleServer) Run() error {
if err != nil {
return nil, err
}
svc, err := sql.ProvideSearchGRPCService(s.cfg, s.features, s.log, s.registerer, docBuilders, s.indexMetrics, s.searchServerRing, s.MemberlistKVConfig, s.httpServerRouter, s.storageBackend, s.grpcService, s.StorageServiceOptions...)
svc, err := sql.ProvideSearchGRPCService(s.cfg, s.features, s.log, s.registerer, docBuilders, s.indexMetrics, s.searchServerRing, s.MemberlistKVConfig, s.httpServerRouter, s.storageBackend, s.vectorBackend, s.grpcService, s.StorageServiceOptions...)
if err != nil {
return nil, err
}

43
pkg/server/wire_gen.go generated
View file

@ -276,6 +276,7 @@ import (
"github.com/grafana/grafana/pkg/storage/unified/resource"
"github.com/grafana/grafana/pkg/storage/unified/search"
"github.com/grafana/grafana/pkg/storage/unified/search/builders"
"github.com/grafana/grafana/pkg/storage/unified/search/vector"
"github.com/grafana/grafana/pkg/storage/unified/sql"
"github.com/grafana/grafana/pkg/tsdb/azuremonitor"
"github.com/grafana/grafana/pkg/tsdb/cloud-monitoring"
@ -558,15 +559,20 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api
if err != nil {
return nil, err
}
vectorBackend, err := vector.ProvideVectorBackend(cfg)
if err != nil {
return nil, err
}
options := &unified.Options{
Cfg: cfg,
Features: featureToggles,
DB: sqlStore,
Tracer: tracingService,
Reg: registerer,
Authzc: accessClient,
Docs: documentBuilderSupplier,
SecureValues: inlineSecureValueSupport,
Cfg: cfg,
Features: featureToggles,
DB: sqlStore,
Tracer: tracingService,
Reg: registerer,
Authzc: accessClient,
Docs: documentBuilderSupplier,
SecureValues: inlineSecureValueSupport,
VectorBackend: vectorBackend,
}
storageMetrics := resource.ProvideStorageMetrics(registerer)
bleveIndexMetrics := resource.ProvideIndexMetrics(registerer)
@ -1256,15 +1262,20 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac
if err != nil {
return nil, err
}
vectorBackend, err := vector.ProvideVectorBackend(cfg)
if err != nil {
return nil, err
}
options := &unified.Options{
Cfg: cfg,
Features: featureToggles,
DB: sqlStore,
Tracer: tracingService,
Reg: registerer,
Authzc: accessClient,
Docs: documentBuilderSupplier,
SecureValues: inlineSecureValueSupport,
Cfg: cfg,
Features: featureToggles,
DB: sqlStore,
Tracer: tracingService,
Reg: registerer,
Authzc: accessClient,
Docs: documentBuilderSupplier,
SecureValues: inlineSecureValueSupport,
VectorBackend: vectorBackend,
}
storageMetrics := resource.ProvideStorageMetrics(registerer)
bleveIndexMetrics := resource.ProvideIndexMetrics(registerer)

View file

@ -71,6 +71,7 @@ import (
"github.com/grafana/grafana/pkg/storage/unified/resource"
search2 "github.com/grafana/grafana/pkg/storage/unified/search"
"github.com/grafana/grafana/pkg/storage/unified/search/builders"
"github.com/grafana/grafana/pkg/storage/unified/search/vector"
"github.com/grafana/grafana/pkg/storage/unified/sql"
)
@ -154,6 +155,7 @@ var wireExtsBasicSet = wire.NewSet(
wire.Struct(new(unified.Options), "*"),
unified.ProvideUnifiedStorageClient,
sql.ProvideStorageBackend,
vector.ProvideVectorBackend,
builder.ProvideDefaultBuildHandlerChainFuncFromBuilders,
aggregatorrunner.ProvideNoopAggregatorConfigurator,
apisregistry.WireSetExts,

View file

@ -682,19 +682,29 @@ type Cfg struct {
SearchInjectFailuresPercent int
EnableSearch bool
EnableSearchClient bool
OverridesFilePath string
OverridesReloadInterval time.Duration
EnforcedQuotaResources []string
QuotasErrorMessageSupportInfo string
EnableSQLKVBackend bool
EnableSQLKVCompatibilityMode bool
EnableGarbageCollection bool
GarbageCollectionDryRun bool
GarbageCollectionInterval time.Duration
GarbageCollectionBatchSize int
GarbageCollectionBatchWait time.Duration
GarbageCollectionMaxAge time.Duration
DashboardsGarbageCollectionMaxAge time.Duration
// Vector storage (separate pgvector database)
EnableVectorBackend bool
VectorDBHost string
VectorDBPort string
VectorDBName string
VectorDBUser string
VectorDBPassword string
VectorDBSSLMode string
VectorPromotionThreshold int // row count per tenant to trigger leaf promotion
VectorPromoterInterval time.Duration // promoter tick interval; 0 disables
OverridesFilePath string
OverridesReloadInterval time.Duration
EnforcedQuotaResources []string
QuotasErrorMessageSupportInfo string
EnableSQLKVBackend bool
EnableSQLKVCompatibilityMode bool
EnableGarbageCollection bool
GarbageCollectionDryRun bool
GarbageCollectionInterval time.Duration
GarbageCollectionBatchSize int
GarbageCollectionBatchWait time.Duration
GarbageCollectionMaxAge time.Duration
DashboardsGarbageCollectionMaxAge time.Duration
// StorageModeCacheTTL is the TTL for caching statusReader results in the dynamic dualwrite service.
// Default: 5 seconds, 0 or negative means no expiration.
StorageModeCacheTTL time.Duration

View file

@ -174,6 +174,7 @@ func (cfg *Cfg) setUnifiedStorageConfig() {
cfg.SearchInjectFailuresPercent = 100
}
cfg.EnableSearch = section.Key("enable_search").MustBool(true)
cfg.EnableVectorBackend = section.Key("vector_backend").MustBool(false)
cfg.applyMigrationEnforcements()
cfg.EnableSearchClient = section.Key("enable_search_client").MustBool(false)
cfg.MaxPageSizeBytes = section.Key("max_page_size_bytes").MustInt(0)
@ -262,6 +263,17 @@ func (cfg *Cfg) setUnifiedStorageConfig() {
cfg.Logger.Warn("index_snapshot_max_age is smaller than max_file_index_age, overriding", "configured", cfg.IndexSnapshotMaxAge, "max_file_index_age", cfg.MaxFileIndexAge)
cfg.IndexSnapshotMaxAge = cfg.MaxFileIndexAge
}
// Vector storage (separate pgvector database)
vectorSection := cfg.Raw.Section("database_vector")
cfg.VectorDBHost = vectorSection.Key("db_host").String()
cfg.VectorDBPort = vectorSection.Key("db_port").MustString("5432")
cfg.VectorDBName = vectorSection.Key("db_name").String()
cfg.VectorDBUser = vectorSection.Key("db_user").String()
cfg.VectorDBPassword = vectorSection.Key("db_password").String()
cfg.VectorDBSSLMode = vectorSection.Key("db_sslmode").MustString("disable")
cfg.VectorPromotionThreshold = vectorSection.Key("promotion_threshold").MustInt(9999999) // effectively disabled by default
cfg.VectorPromoterInterval = vectorSection.Key("promoter_interval").MustDuration(1 * time.Hour)
}
// applyMigrationEnforcements enforces unified storage migration configs when migrations should run,

View file

@ -33,19 +33,21 @@ import (
"github.com/grafana/grafana/pkg/storage/unified/federated"
"github.com/grafana/grafana/pkg/storage/unified/resource"
"github.com/grafana/grafana/pkg/storage/unified/search"
"github.com/grafana/grafana/pkg/storage/unified/search/vector"
"github.com/grafana/grafana/pkg/storage/unified/sql"
"github.com/grafana/grafana/pkg/util/scheduler"
)
type Options struct {
Cfg *setting.Cfg
Features featuremgmt.FeatureToggles
DB infraDB.DB
Tracer tracing.Tracer
Reg prometheus.Registerer
Authzc types.AccessClient
Docs resource.DocumentBuilderSupplier
SecureValues secrets.InlineSecureValueSupport
Cfg *setting.Cfg
Features featuremgmt.FeatureToggles
DB infraDB.DB
Tracer tracing.Tracer
Reg prometheus.Registerer
Authzc types.AccessClient
Docs resource.DocumentBuilderSupplier
SecureValues secrets.InlineSecureValueSupport
VectorBackend vector.VectorBackend
}
type clientMetrics struct {
@ -67,7 +69,7 @@ func ProvideUnifiedStorageClient(opts *Options,
BlobStoreURL: apiserverCfg.Key("blob_url").MustString(""),
BlobThresholdBytes: apiserverCfg.Key("blob_threshold_bytes").MustInt(options.BlobThresholdDefault),
GrpcClientKeepaliveTime: apiserverCfg.Key("grpc_client_keepalive_time").MustDuration(0),
}, opts.Cfg, opts.Features, opts.DB, opts.Tracer, opts.Reg, opts.Authzc, opts.Docs, storageMetrics, indexMetrics, opts.SecureValues)
}, opts.Cfg, opts.Features, opts.DB, opts.Tracer, opts.Reg, opts.Authzc, opts.Docs, storageMetrics, indexMetrics, opts.SecureValues, opts.VectorBackend)
if err == nil {
// Used to get the folder stats
// Pass cfg directly so the federated client reads the current dual-writer mode
@ -94,6 +96,7 @@ func newClient(opts options.StorageOptions,
storageMetrics *resource.StorageMetrics,
indexMetrics *resource.BleveIndexMetrics,
secure secrets.InlineSecureValueSupport,
vectorBackend vector.VectorBackend,
) (resource.ResourceClient, error) {
ctx := context.Background()
@ -164,6 +167,7 @@ func newClient(opts options.StorageOptions,
serverOptions := sql.ServerOptions{
Backend: backend,
VectorBackend: vectorBackend,
Cfg: cfg,
Tracer: tracer,
Reg: reg,

View file

@ -47,6 +47,7 @@ func TestUnifiedStorageClient(t *testing.T) {
nil,
nil,
nil,
nil,
)
require.NoError(t, err)
@ -81,6 +82,7 @@ func TestUnifiedStorageClient(t *testing.T) {
nil,
nil,
nil,
nil,
)
require.NoError(t, err)

View file

@ -28,6 +28,7 @@ import (
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/storage/unified/resourcepb"
"github.com/grafana/grafana/pkg/storage/unified/search/vector"
"github.com/grafana/grafana/pkg/util/debouncer"
)
@ -136,14 +137,15 @@ type SearchBackend interface {
// searchServer supports indexing+search regardless of implementation.
type searchServer struct {
log log.Logger
storage StorageBackend
search SearchBackend
indexMetrics *BleveIndexMetrics
access types.AccessClient
builders *builderCache
initWorkers int
initMinSize int
log log.Logger
storage StorageBackend
vectorBackend vector.VectorBackend
search SearchBackend
indexMetrics *BleveIndexMetrics
access types.AccessClient
builders *builderCache
initWorkers int
initMinSize int
ownsIndexFn func(key NamespacedResource) (bool, error)
@ -185,7 +187,7 @@ var (
)
// newSearchServer creates a new search server implementation.
func newSearchServer(opts SearchOptions, storage StorageBackend, access types.AccessClient, blob BlobSupport, indexMetrics *BleveIndexMetrics, ownsIndexFn func(key NamespacedResource) (bool, error)) (*searchServer, error) {
func newSearchServer(opts SearchOptions, storage StorageBackend, vectorBackend vector.VectorBackend, access types.AccessClient, blob BlobSupport, indexMetrics *BleveIndexMetrics, ownsIndexFn func(key NamespacedResource) (bool, error)) (*searchServer, error) {
// No backend search support
if opts.Backend == nil {
return nil, nil
@ -208,6 +210,7 @@ func newSearchServer(opts SearchOptions, storage StorageBackend, access types.Ac
s := &searchServer{
access: access,
storage: storage,
vectorBackend: vectorBackend,
search: opts.Backend,
log: log.New("resource-search"),
initWorkers: opts.InitWorkerThreads,

View file

@ -205,7 +205,7 @@ func TestSearchGetOrCreateIndex(t *testing.T) {
InitMinCount: 1, // set min count to default for this test
}
support, err := newSearchServer(opts, storage, nil, nil, nil, nil)
support, err := newSearchServer(opts, storage, nil, nil, nil, nil, nil)
require.NoError(t, err)
require.NotNil(t, support)
@ -261,7 +261,7 @@ func TestSearchGetOrCreateIndexWithIndexUpdate(t *testing.T) {
}
// Enable searchAfterWrite
support, err := newSearchServer(opts, storage, nil, nil, nil, nil)
support, err := newSearchServer(opts, storage, nil, nil, nil, nil, nil)
require.NoError(t, err)
require.NotNil(t, support)
@ -311,7 +311,7 @@ func TestSearchGetOrCreateIndexWithCancellation(t *testing.T) {
InitMinCount: 1, // set min count to default for this test
}
support, err := newSearchServer(opts, storage, nil, nil, nil, nil)
support, err := newSearchServer(opts, storage, nil, nil, nil, nil, nil)
require.NoError(t, err)
require.NotNil(t, support)
@ -662,7 +662,7 @@ func TestFindIndexesForRebuild(t *testing.T) {
BuildVersion: semver.MustParse("6.5.0"), // Running version
}
support, err := newSearchServer(opts, storage, nil, nil, nil, nil)
support, err := newSearchServer(opts, storage, nil, nil, nil, nil, nil)
require.NoError(t, err)
require.NotNil(t, support)
@ -739,7 +739,7 @@ func TestRebuildIndexes(t *testing.T) {
Resources: supplier,
}
support, err := newSearchServer(opts, storage, nil, nil, nil, nil)
support, err := newSearchServer(opts, storage, nil, nil, nil, nil, nil)
require.NoError(t, err)
require.NotNil(t, support)
@ -831,7 +831,7 @@ func TestRebuildIndexes(t *testing.T) {
InitMinCount: 1,
}
support, err := newSearchServer(opts, storage, nil, nil, nil, nil)
support, err := newSearchServer(opts, storage, nil, nil, nil, nil, nil)
require.NoError(t, err)
require.NotNil(t, support)
@ -944,7 +944,7 @@ func TestRebuildIndexesForResource(t *testing.T) {
InitMinCount: 1,
}
support, err := newSearchServer(opts, storage, nil, nil, nil, nil)
support, err := newSearchServer(opts, storage, nil, nil, nil, nil, nil)
require.NoError(t, err)
require.NotNil(t, support)
@ -1025,7 +1025,7 @@ func TestSearchValidatesNegativeLimitAndOffset(t *testing.T) {
InitMinCount: 1,
}
support, err := newSearchServer(opts, nil, nil, nil, nil, nil)
support, err := newSearchServer(opts, nil, nil, nil, nil, nil, nil)
require.NoError(t, err)
require.NotNil(t, support)
@ -1134,7 +1134,7 @@ func TestFindIndexesToRebuildWithJitter(t *testing.T) {
MinBuildVersion: semver.MustParse("5.0.0"),
}
support, err := newSearchServer(opts, storage, nil, nil, nil, nil)
support, err := newSearchServer(opts, storage, nil, nil, nil, nil, nil)
require.NoError(t, err)
require.NotNil(t, support)
@ -1145,7 +1145,7 @@ func TestFindIndexesToRebuildWithJitter(t *testing.T) {
require.Equal(t, numIndexes, len(chsNoJitter))
// Create a second server with the same config to get a fresh rebuild queue.
support2, err := newSearchServer(opts, storage, nil, nil, nil, nil)
support2, err := newSearchServer(opts, storage, nil, nil, nil, nil, nil)
require.NoError(t, err)
// With jitter: some indexes get extra tolerance, so fewer should be queued.

View file

@ -33,6 +33,7 @@ import (
"github.com/grafana/grafana/pkg/infra/log"
secrets "github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
"github.com/grafana/grafana/pkg/storage/unified/resourcepb"
"github.com/grafana/grafana/pkg/storage/unified/search/vector"
"github.com/grafana/grafana/pkg/util/scheduler"
)
@ -337,6 +338,12 @@ type ResourceServerOptions struct {
// BookmarkFrequency controls how often periodic bookmark events are sent to
// Watch clients that set AllowWatchBookmarks. Zero defaults to defaultBookmarkFrequency.
BookmarkFrequency time.Duration
// VectorBackend is the optional pgvector-backed store for semantic search.
// nil when the [unified_storage] vector_backend flag is off. When present,
// the resource and search servers hold a reference for use by future
// write and query paths.
VectorBackend vector.VectorBackend
}
func (opts ResourceServerOptions) bulkBatchOptions() BulkBatchOptions {
@ -365,7 +372,7 @@ func NewUninitializedSearchServer(opts ResourceServerOptions) (SearchServer, err
}
// Create the search server using the search.go factory
searchServer, err := newSearchServer(opts.Search, opts.Backend, opts.AccessClient, blobstore, opts.IndexMetrics, opts.OwnsIndexFn)
searchServer, err := newSearchServer(opts.Search, opts.Backend, opts.VectorBackend, opts.AccessClient, blobstore, opts.IndexMetrics, opts.OwnsIndexFn)
if err != nil || searchServer == nil {
return nil, fmt.Errorf("search server could not be created: %w", err)
}
@ -449,6 +456,7 @@ func NewUninitializedResourceServer(opts ResourceServerOptions) (*server, error)
s := &server{
log: logger,
backend: opts.Backend,
vectorBackend: opts.VectorBackend,
bulkBatchOptions: opts.bulkBatchOptions(),
blob: blobstore,
diagnostics: opts.Diagnostics,
@ -473,7 +481,7 @@ func NewUninitializedResourceServer(opts ResourceServerOptions) (*server, error)
if opts.Search.Resources != nil {
var err error
s.search, err = newSearchServer(opts.Search, s.backend, s.access, s.blob, opts.IndexMetrics, opts.OwnsIndexFn)
s.search, err = newSearchServer(opts.Search, s.backend, opts.VectorBackend, s.access, s.blob, opts.IndexMetrics, opts.OwnsIndexFn)
if err != nil {
return nil, err
}
@ -524,6 +532,7 @@ var _ ResourceServer = &server{}
type server struct {
log log.Logger
backend StorageBackend
vectorBackend vector.VectorBackend
bulkBatchOptions BulkBatchOptions
blob BlobSupport
secure secrets.InlineSecureValueSupport

View file

@ -0,0 +1,6 @@
DELETE FROM embeddings
WHERE {{ .Ident "resource" }} = {{ .Arg .Resource }}
AND {{ .Ident "namespace" }} = {{ .Arg .Namespace }}
AND {{ .Ident "model" }} = {{ .Arg .Model }}
AND {{ .Ident "uid" }} = {{ .Arg .UID }}
;

View file

@ -0,0 +1,7 @@
DELETE FROM embeddings
WHERE {{ .Ident "resource" }} = {{ .Arg .Resource }}
AND {{ .Ident "namespace" }} = {{ .Arg .Namespace }}
AND {{ .Ident "model" }} = {{ .Arg .Model }}
AND {{ .Ident "uid" }} = {{ .Arg .UID }}
AND {{ .Ident "subresource" }} IN ({{ .ArgList .SubresourcesSlice }})
;

View file

@ -0,0 +1,9 @@
SELECT
{{ .Ident "subresource" | .Into .Response.Subresource }},
{{ .Ident "content" | .Into .Response.Content }}
FROM embeddings
WHERE {{ .Ident "resource" }} = {{ .Arg .Resource }}
AND {{ .Ident "namespace" }} = {{ .Arg .Namespace }}
AND {{ .Ident "model" }} = {{ .Arg .Model }}
AND {{ .Ident "uid" }} = {{ .Arg .UID }}
;

View file

@ -0,0 +1,24 @@
SELECT
{{ .Ident "uid" | .Into .Response.UID }},
{{ .Ident "title" | .Into .Response.Title }},
{{ .Ident "subresource" | .Into .Response.Subresource }},
{{ .Ident "content" | .Into .Response.Content }},
{{ .Ident "embedding" }} <=> {{ .Arg .QueryEmbedding }} AS {{ .Ident "score" | .Into .Response.Score }},
{{ .Ident "folder" | .Into .Response.Folder }},
{{ .Ident "metadata" | .Into .Response.Metadata }}
FROM embeddings
WHERE {{ .Ident "resource" }} = {{ .Arg .Resource }}
AND {{ .Ident "namespace" }} = {{ .Arg .Namespace }}
AND {{ .Ident "model" }} = {{ .Arg .Model }}
{{ if .UIDFilter }}
AND {{ .Ident "uid" }} IN ({{ .ArgList .UIDFilterSlice }})
{{ end }}
{{ if .FolderFilter }}
AND {{ .Ident "folder" }} IN ({{ .ArgList .FolderFilterSlice }})
{{ end }}
{{ range .MetadataFilters }}
AND {{ $.Ident "metadata" }} @> {{ $.Arg .JSON }}
{{ end }}
ORDER BY {{ .Ident "embedding" }} <=> {{ .Arg .QueryEmbedding }}
LIMIT {{ .Arg .Limit }}
;

View file

@ -0,0 +1,32 @@
INSERT INTO embeddings (
{{ .Ident "resource" }},
{{ .Ident "namespace" }},
{{ .Ident "model" }},
{{ .Ident "uid" }},
{{ .Ident "title" }},
{{ .Ident "subresource" }},
{{ .Ident "folder" }},
{{ .Ident "content" }},
{{ .Ident "metadata" }},
{{ .Ident "embedding" }}
)
VALUES (
{{ .Arg .Resource }},
{{ .Arg .Vector.Namespace }},
{{ .Arg .Vector.Model }},
{{ .Arg .Vector.UID }},
{{ .Arg .Vector.Title }},
{{ .Arg .Vector.Subresource }},
{{ .Arg .Vector.Folder }},
{{ .Arg .Vector.Content }},
{{ .Arg .Vector.Metadata }},
{{ .Arg .Embedding }}
)
ON CONFLICT ({{ .Ident "resource" }}, {{ .Ident "namespace" }}, {{ .Ident "model" }}, {{ .Ident "uid" }}, {{ .Ident "subresource" }})
DO UPDATE SET
{{ .Ident "title" }} = {{ .Arg .Vector.Title }},
{{ .Ident "folder" }} = {{ .Arg .Vector.Folder }},
{{ .Ident "content" }} = {{ .Arg .Vector.Content }},
{{ .Ident "metadata" }} = {{ .Arg .Vector.Metadata }},
{{ .Ident "embedding" }} = {{ .Arg .Embedding }}
;

View file

@ -0,0 +1,287 @@
package vector
import (
"context"
"encoding/json"
"fmt"
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/storage/unified/sql/db/dbimpl"
"github.com/grafana/grafana/pkg/util/testutil"
"github.com/grafana/grafana/pkg/util/xorm"
)
const testModel = "test-model"
const testResource = "dashboards"
var testSubtree = subtreeName(testResource)
// To run: start the pgvector devenv (localhost:5433) and
//
// PGVECTOR_TEST_DB="host=localhost port=5433 dbname=grafana_vectors user=grafana password=password sslmode=disable" \
// go test -run TestIntegration ./pkg/storage/unified/search/vector/... -v -count=1
func setupIntegrationTest(t *testing.T) (VectorBackend, *xorm.Engine, context.Context) {
t.Helper()
testutil.SkipIntegrationTestInShortMode(t)
connStr := os.Getenv("PGVECTOR_TEST_DB")
if connStr == "" {
t.Skip("PGVECTOR_TEST_DB not set, skipping pgvector integration test")
}
ctx := context.Background()
engine, err := xorm.NewEngine("postgres", connStr)
require.NoError(t, err)
t.Cleanup(func() {
if err := engine.Close(); err != nil {
t.Logf("closing xorm engine: %v", err)
}
})
cfg := setting.NewCfg()
err = MigrateVectorStore(ctx, engine, cfg)
require.NoError(t, err)
database := dbimpl.NewDB(engine.DB().DB, engine.Dialect().DriverName())
// interval=0 keeps Run idle; promotion tests call Promote(ctx) directly.
backend := NewPgvectorBackend(database, 1000, 0)
cleanIntegrationState(t, engine)
return backend, engine, ctx
}
// cleanIntegrationState drops any `integration-test*` partial indexes, clears
// rows, and resets the checkpoint.
func cleanIntegrationState(t *testing.T, engine *xorm.Engine) {
t.Helper()
ctx := context.Background()
indexPrefix := fmt.Sprintf("%s_integration_test", testResource)
rows, err := engine.DB().QueryContext(ctx, `
SELECT c.relname FROM pg_class c
JOIN pg_index i ON i.indexrelid = c.oid
JOIN pg_class t ON t.oid = i.indrelid
WHERE t.relname = $1 AND c.relkind = 'i' AND c.relname LIKE $2`,
testSubtree, indexPrefix+"%")
require.NoError(t, err)
var indexes []string
for rows.Next() {
var n string
require.NoError(t, rows.Scan(&n))
indexes = append(indexes, n)
}
_ = rows.Close()
for _, idx := range indexes {
_, _ = engine.DB().ExecContext(ctx, fmt.Sprintf(`DROP INDEX IF EXISTS %s`, idx))
}
_, _ = engine.DB().ExecContext(ctx,
fmt.Sprintf(`DELETE FROM %s WHERE namespace LIKE 'integration-test%%'`, testSubtree))
_, _ = engine.DB().ExecContext(ctx,
`DELETE FROM vector_promoted WHERE namespace LIKE 'integration-test%'`)
_, _ = engine.DB().ExecContext(ctx,
`UPDATE vector_latest_rv SET latest_rv = 0 WHERE id = 1`)
}
func TestIntegrationVectorUpsertAndSearch(t *testing.T) {
backend, _, ctx := setupIntegrationTest(t)
vectors := []Vector{
{
Namespace: "integration-test", Resource: testResource, UID: "dash-1", Title: "CPU Dashboard", Subresource: "panel/1",
ResourceVersion: 10, Folder: "folder-a",
Content: "CPU usage over time for production servers",
Metadata: json.RawMessage(`{"datasource_uids":["prom-1"],"query_languages":["promql"]}`),
Embedding: makeEmbedding(0.9, 0.1), Model: testModel,
},
{
Namespace: "integration-test", Resource: testResource, UID: "dash-1", Title: "CPU Dashboard", Subresource: "panel/2",
ResourceVersion: 10, Folder: "folder-a",
Content: "Memory usage alerts dashboard",
Metadata: json.RawMessage(`{"datasource_uids":["prom-1"],"query_languages":["promql"]}`),
Embedding: makeEmbedding(0.1, 0.9), Model: testModel,
},
{
Namespace: "integration-test", Resource: testResource, UID: "dash-2", Title: "Logs Dashboard", Subresource: "panel/1",
ResourceVersion: 20, Folder: "folder-b",
Content: "Log volume by service",
Metadata: json.RawMessage(`{"datasource_uids":["loki-1"],"query_languages":["logql"]}`),
Embedding: makeEmbedding(0.5, 0.5), Model: testModel,
},
}
require.NoError(t, backend.Upsert(ctx, vectors))
results, err := backend.Search(ctx, "integration-test", testModel, testResource, makeEmbedding(0.85, 0.15), 10)
require.NoError(t, err)
require.GreaterOrEqual(t, len(results), 3)
assert.Equal(t, "dash-1", results[0].UID)
assert.Equal(t, "CPU Dashboard", results[0].Title)
assert.Equal(t, "panel/1", results[0].Subresource)
results, err = backend.Search(ctx, "integration-test", testModel, testResource, makeEmbedding(0.5, 0.5), 10,
SearchFilter{Field: "uid", Values: []string{"dash-2"}})
require.NoError(t, err)
require.Len(t, results, 1)
assert.Equal(t, "dash-2", results[0].UID)
results, err = backend.Search(ctx, "integration-test", testModel, testResource, makeEmbedding(0.5, 0.5), 10,
SearchFilter{Field: "folder", Values: []string{"folder-a"}})
require.NoError(t, err)
require.Len(t, results, 2)
results, err = backend.Search(ctx, "integration-test", testModel, testResource, makeEmbedding(0.5, 0.5), 10,
SearchFilter{Field: "query_languages", Values: []string{"logql"}})
require.NoError(t, err)
require.Len(t, results, 1)
assert.Equal(t, "dash-2", results[0].UID)
}
func TestIntegrationVectorDeleteSubresources(t *testing.T) {
backend, _, ctx := setupIntegrationTest(t)
err := backend.Upsert(ctx, []Vector{
{Namespace: "integration-test", Resource: testResource, UID: "dash", Title: "Dash", Subresource: "panel/1",
ResourceVersion: 10, Content: "panel one", Metadata: json.RawMessage(`{}`),
Embedding: makeEmbedding(0.5, 0.5), Model: testModel},
{Namespace: "integration-test", Resource: testResource, UID: "dash", Title: "Dash", Subresource: "panel/2",
ResourceVersion: 10, Content: "panel two", Metadata: json.RawMessage(`{}`),
Embedding: makeEmbedding(0.5, 0.5), Model: testModel},
{Namespace: "integration-test", Resource: testResource, UID: "dash", Title: "Dash", Subresource: "panel/3",
ResourceVersion: 10, Content: "panel three", Metadata: json.RawMessage(`{}`),
Embedding: makeEmbedding(0.5, 0.5), Model: testModel},
})
require.NoError(t, err)
stored, err := backend.GetSubresourceContent(ctx, "integration-test", testModel, testResource, "dash")
require.NoError(t, err)
require.Len(t, stored, 3)
current := map[string]string{"panel/1": "panel one"}
var toDelete []string
for sub := range stored {
if _, ok := current[sub]; !ok {
toDelete = append(toDelete, sub)
}
}
require.ElementsMatch(t, []string{"panel/2", "panel/3"}, toDelete)
err = backend.DeleteSubresources(ctx, "integration-test", testModel, testResource, "dash", toDelete)
require.NoError(t, err)
results, err := backend.Search(ctx, "integration-test", testModel, testResource, makeEmbedding(0.5, 0.5), 10)
require.NoError(t, err)
require.Len(t, results, 1)
assert.Equal(t, "panel/1", results[0].Subresource)
err = backend.Delete(ctx, "integration-test", testModel, testResource, "dash")
require.NoError(t, err)
}
func TestIntegrationVectorGetLatestRV(t *testing.T) {
backend, _, ctx := setupIntegrationTest(t)
rv, err := backend.GetLatestRV(ctx)
require.NoError(t, err)
assert.Equal(t, int64(0), rv)
err = backend.Upsert(ctx, []Vector{
{Namespace: "integration-test", Resource: testResource, UID: "rv-test", Title: "RV Test", Subresource: "x",
ResourceVersion: 42, Content: "test content", Metadata: json.RawMessage(`{}`),
Embedding: makeEmbedding(0.5, 0.5), Model: testModel},
})
require.NoError(t, err)
rv, err = backend.GetLatestRV(ctx)
require.NoError(t, err)
assert.Equal(t, int64(42), rv)
}
func TestIntegrationPromoterPromotesLargeTenant(t *testing.T) {
backend, engine, ctx := setupIntegrationTest(t)
const ns = "integration-test-big"
const threshold = 50
const nRows = threshold + 10
vectors := make([]Vector, 0, nRows)
for i := 0; i < nRows; i++ {
vectors = append(vectors, Vector{
Namespace: ns, Resource: testResource, UID: "dash", Title: "Dash", Subresource: fmt.Sprintf("panel/%d", i),
ResourceVersion: int64(i + 1), Content: fmt.Sprintf("content %d", i),
Metadata: json.RawMessage(`{}`), Embedding: makeEmbedding(0.5, 0.5), Model: testModel,
})
}
require.NoError(t, backend.Upsert(ctx, vectors))
idxName := partialHNSWName(testResource, ns)
require.False(t, indexExists(t, engine, idxName))
require.Equal(t, nRows, countRowsIn(t, engine, testSubtree, ns))
database := dbimpl.NewDB(engine.DB().DB, engine.Dialect().DriverName())
promoter := NewPromoter(database, threshold, 0)
require.NoError(t, promoter.Promote(ctx))
require.True(t, indexExists(t, engine, idxName))
require.Equal(t, nRows, countRowsIn(t, engine, testSubtree, ns))
results, err := backend.Search(ctx, ns, testModel, testResource, makeEmbedding(0.5, 0.5), 5)
require.NoError(t, err)
assert.Len(t, results, 5)
}
func TestIntegrationPromoterSkipsSmallTenant(t *testing.T) {
backend, engine, ctx := setupIntegrationTest(t)
const ns = "integration-test-small"
require.NoError(t, backend.Upsert(ctx, []Vector{
{Namespace: ns, Resource: testResource, UID: "dash", Title: "Dash", Subresource: "panel/1",
ResourceVersion: 1, Content: "only row", Metadata: json.RawMessage(`{}`),
Embedding: makeEmbedding(0.1, 0.1), Model: testModel},
}))
database := dbimpl.NewDB(engine.DB().DB, engine.Dialect().DriverName())
promoter := NewPromoter(database, 100, 0)
require.NoError(t, promoter.Promote(ctx))
idxName := partialHNSWName(testResource, ns)
require.False(t, indexExists(t, engine, idxName),
"small tenant should not be promoted")
}
func indexExists(t *testing.T, engine *xorm.Engine, idxName string) bool {
t.Helper()
var exists bool
err := engine.DB().QueryRowContext(context.Background(), `
SELECT EXISTS (
SELECT 1 FROM pg_class c
JOIN pg_index i ON i.indexrelid = c.oid
WHERE c.relname = $1 AND c.relkind = 'i' AND i.indisvalid
)`, idxName).Scan(&exists)
require.NoError(t, err)
return exists
}
func countRowsIn(t *testing.T, engine *xorm.Engine, table, ns string) int {
t.Helper()
var n int
require.NoError(t, engine.DB().QueryRowContext(context.Background(),
fmt.Sprintf(`SELECT COUNT(*) FROM %s WHERE namespace = $1`, table), ns).Scan(&n))
return n
}
// makeEmbedding builds a 1024-dim halfvec with the first two values set.
func makeEmbedding(a, b float32) []float32 {
e := make([]float32, 1024)
e[0] = a
e[1] = b
return e
}

View file

@ -0,0 +1,227 @@
package vector
import (
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
"time"
pgvector "github.com/pgvector/pgvector-go"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/storage/unified/sql/db"
"github.com/grafana/grafana/pkg/storage/unified/sql/dbutil"
"github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate"
)
var _ VectorBackend = (*pgvectorBackend)(nil)
type pgvectorBackend struct {
db db.DB
dialect sqltemplate.Dialect
log log.Logger
promoter *Promoter
}
// NewPgvectorBackend builds a VectorBackend. `promoterInterval=0` leaves the
// promoter idle — safe on any target; schema-owning targets call Run.
func NewPgvectorBackend(database db.DB, promotionThreshold int, promoterInterval time.Duration) *pgvectorBackend {
return &pgvectorBackend{
db: database,
dialect: sqltemplate.PostgreSQL,
log: log.New("vector-pgvector"),
promoter: NewPromoter(database, promotionThreshold, promoterInterval),
}
}
func (b *pgvectorBackend) Run(ctx context.Context) error {
return b.promoter.Run(ctx)
}
// validateResource rejects resources that don't have a sub-tree attached.
// Adding a resource means attaching an `embeddings_<R>` sub-tree.
// TODO dynamically add new partition if for resource if one doesnt exist
func validateResource(resource string) error {
if resource != "dashboards" {
return fmt.Errorf("unsupported resource %q (no embeddings sub-tree provisioned)", resource)
}
return nil
}
func (b *pgvectorBackend) Upsert(ctx context.Context, vectors []Vector) error {
if len(vectors) == 0 {
return nil
}
for i := range vectors {
if err := vectors[i].Validate(); err != nil {
return fmt.Errorf("vector[%d]: %w", i, err)
}
}
// track max rv so we can update global db rv
var batchMaxRV int64
for i := range vectors {
if vectors[i].ResourceVersion > batchMaxRV {
batchMaxRV = vectors[i].ResourceVersion
}
}
return b.db.WithTx(ctx, nil, func(ctx context.Context, tx db.Tx) error {
for i := range vectors {
if err := validateResource(vectors[i].Resource); err != nil {
return fmt.Errorf("vector[%d]: %w", i, err)
}
req := &sqlVectorCollectionUpsertRequest{
SQLTemplate: sqltemplate.New(b.dialect),
Resource: vectors[i].Resource,
Vector: &vectors[i],
Embedding: pgvector.NewHalfVector(vectors[i].Embedding),
}
if _, err := dbutil.Exec(ctx, tx, sqlVectorCollectionUpsert, req); err != nil {
return fmt.Errorf("upsert vector %s/%s: %w", vectors[i].UID, vectors[i].Subresource, err)
}
}
// WHERE clause keeps this monotonic under concurrent writers.
if batchMaxRV > 0 {
if _, err := tx.ExecContext(ctx,
`UPDATE vector_latest_rv SET latest_rv = $1 WHERE id = 1 AND latest_rv < $1`,
batchMaxRV,
); err != nil {
return fmt.Errorf("bump vector_latest_rv: %w", err)
}
}
return nil
})
}
func (b *pgvectorBackend) Delete(ctx context.Context, namespace, model, resource, uid string) error {
if model == "" {
return fmt.Errorf("model must not be empty")
}
if err := validateResource(resource); err != nil {
return err
}
req := &sqlVectorCollectionDeleteRequest{
SQLTemplate: sqltemplate.New(b.dialect),
Resource: resource,
Namespace: namespace,
Model: model,
UID: uid,
}
_, err := dbutil.Exec(ctx, b.db, sqlVectorCollectionDelete, req)
return err
}
func (b *pgvectorBackend) DeleteSubresources(ctx context.Context, namespace, model, resource, uid string, subresources []string) error {
if model == "" {
return fmt.Errorf("model must not be empty")
}
if len(subresources) == 0 {
return nil
}
if err := validateResource(resource); err != nil {
return err
}
req := &sqlVectorCollectionDeleteSubresourcesRequest{
SQLTemplate: sqltemplate.New(b.dialect),
Resource: resource,
Namespace: namespace,
Model: model,
UID: uid,
Subresources: subresources,
}
_, err := dbutil.Exec(ctx, b.db, sqlVectorCollectionDeleteSubresource, req)
return err
}
func (b *pgvectorBackend) GetSubresourceContent(ctx context.Context, namespace, model, resource, uid string) (map[string]string, error) {
if err := validateResource(resource); err != nil {
return nil, err
}
req := &sqlVectorCollectionGetContentRequest{
SQLTemplate: sqltemplate.New(b.dialect),
Resource: resource,
Namespace: namespace,
Model: model,
UID: uid,
Response: &sqlVectorCollectionGetContentResponse{},
}
rows, err := dbutil.Query(ctx, b.db, sqlVectorCollectionGetContent, req)
if err != nil {
return nil, err
}
if len(rows) == 0 {
return nil, nil
}
out := make(map[string]string, len(rows))
for _, r := range rows {
out[r.Subresource] = r.Content
}
return out, nil
}
func (b *pgvectorBackend) Search(ctx context.Context, namespace, model, resource string,
embedding []float32, limit int, filters ...SearchFilter) ([]VectorSearchResult, error) {
if err := validateResource(resource); err != nil {
return nil, err
}
// TODO Search is currently single resource but we will need to support cross-resource search eventually
req := &sqlVectorCollectionSearchRequest{
SQLTemplate: sqltemplate.New(b.dialect),
Resource: resource,
Namespace: namespace,
Model: model,
QueryEmbedding: pgvector.NewHalfVector(embedding),
Limit: int64(limit),
Response: &sqlVectorCollectionSearchResponse{},
}
for _, f := range filters {
switch f.Field {
case "uid":
req.UIDValues = f.Values
case "folder":
req.FolderValues = f.Values
default:
// JSONB containment: metadata @> '{"field":["v1","v2"]}'
j, _ := json.Marshal(map[string][]string{f.Field: f.Values})
req.MetadataFilters = append(req.MetadataFilters, MetadataFilterEntry{JSON: string(j)})
}
}
rows, err := dbutil.Query(ctx, b.db, sqlVectorCollectionSearch, req)
if err != nil {
return nil, err
}
results := make([]VectorSearchResult, len(rows))
for i, row := range rows {
results[i] = VectorSearchResult{
UID: row.UID,
Title: row.Title,
Subresource: row.Subresource,
Content: row.Content,
Score: row.Score,
Folder: row.Folder,
Metadata: row.Metadata,
}
}
return results, nil
}
func (b *pgvectorBackend) GetLatestRV(ctx context.Context) (int64, error) {
var rv int64
row := b.db.QueryRowContext(ctx, `SELECT latest_rv FROM vector_latest_rv WHERE id = 1`)
if err := row.Scan(&rv); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return 0, nil
}
return 0, fmt.Errorf("read vector_latest_rv: %w", err)
}
return rv, nil
}

View file

@ -0,0 +1,153 @@
package vector
import (
"database/sql"
"testing"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/storage/unified/sql/test"
"github.com/grafana/grafana/pkg/util/testutil"
)
func TestVector_Validate(t *testing.T) {
ok := Vector{Namespace: "ns", Model: "m", Resource: "r", UID: "u", Title: "t"}
require.NoError(t, ok.Validate())
cases := []struct {
name string
mutate func(*Vector)
wantErr string
}{
{"empty namespace", func(v *Vector) { v.Namespace = "" }, "namespace must not be empty"},
{"empty model", func(v *Vector) { v.Model = "" }, "model must not be empty"},
{"empty resource", func(v *Vector) { v.Resource = "" }, "resource must not be empty"},
{"empty uid", func(v *Vector) { v.UID = "" }, "uid must not be empty"},
{"empty title", func(v *Vector) { v.Title = "" }, "title must not be empty"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
v := ok
tc.mutate(&v)
err := v.Validate()
require.Error(t, err)
require.Contains(t, err.Error(), tc.wantErr)
})
}
}
func TestValidateResource(t *testing.T) {
cases := []struct {
in string
wantErr bool
}{
{"dashboards", false},
{"folders", true}, // not provisioned yet
{"", true},
}
for _, tc := range cases {
t.Run(tc.in, func(t *testing.T) {
err := validateResource(tc.in)
if tc.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
})
}
}
func TestPgvectorBackend_Upsert_EmptySlice(t *testing.T) {
rdb := test.NewDBProviderNopSQL(t)
backend := NewPgvectorBackend(rdb.DB, 1000, 0)
ctx := testutil.NewDefaultTestContext(t)
require.NoError(t, backend.Upsert(ctx, nil))
require.NoError(t, backend.Upsert(ctx, []Vector{}))
require.NoError(t, rdb.SQLMock.ExpectationsWereMet())
}
func TestPgvectorBackend_Upsert_InvalidVector_Rejected(t *testing.T) {
rdb := test.NewDBProviderNopSQL(t)
backend := NewPgvectorBackend(rdb.DB, 1000, 0)
ctx := testutil.NewDefaultTestContext(t)
err := backend.Upsert(ctx, []Vector{
{Namespace: "ns", Model: "m", Resource: "dashboards", UID: "", Content: "x", Embedding: []float32{0.1}},
})
require.Error(t, err)
require.Contains(t, err.Error(), "uid must not be empty")
require.NoError(t, rdb.SQLMock.ExpectationsWereMet())
}
func TestPgvectorBackend_Upsert_UnknownResource_Rejected(t *testing.T) {
// Unknown resource has no shared table; Upsert errors before any DB work.
rdb := test.NewDBProviderNopSQL(t)
backend := NewPgvectorBackend(rdb.DB, 1000, 0)
ctx := testutil.NewDefaultTestContext(t)
rdb.SQLMock.ExpectBegin()
rdb.SQLMock.ExpectRollback()
err := backend.Upsert(ctx, []Vector{
{Namespace: "ns", Model: "m", Resource: "folders", UID: "x", Title: "t", Embedding: []float32{0.1}},
})
require.Error(t, err)
require.Contains(t, err.Error(), "unsupported resource")
require.NoError(t, rdb.SQLMock.ExpectationsWereMet())
}
func TestPgvectorBackend_Delete_EmptyModel_Rejected(t *testing.T) {
rdb := test.NewDBProviderNopSQL(t)
backend := NewPgvectorBackend(rdb.DB, 1000, 0)
ctx := testutil.NewDefaultTestContext(t)
err := backend.Delete(ctx, "ns", "", "dashboards", "dash-1")
require.Error(t, err)
require.Contains(t, err.Error(), "model must not be empty")
require.NoError(t, rdb.SQLMock.ExpectationsWereMet())
}
func TestPgvectorBackend_DeleteSubresources_EmptySlice_NoOp(t *testing.T) {
rdb := test.NewDBProviderNopSQL(t)
backend := NewPgvectorBackend(rdb.DB, 1000, 0)
ctx := testutil.NewDefaultTestContext(t)
require.NoError(t, backend.DeleteSubresources(ctx, "ns", "m", "dashboards", "dash-1", nil))
require.NoError(t, backend.DeleteSubresources(ctx, "ns", "m", "dashboards", "dash-1", []string{}))
require.NoError(t, rdb.SQLMock.ExpectationsWereMet())
}
func TestPgvectorBackend_GetLatestRV(t *testing.T) {
rdb := test.NewDBProviderNopSQL(t)
backend := NewPgvectorBackend(rdb.DB, 1000, 0)
ctx := testutil.NewDefaultTestContext(t)
rdb.SQLMock.ExpectQuery("SELECT latest_rv FROM vector_latest_rv").
WillReturnRows(rdb.SQLMock.NewRows([]string{"latest_rv"}).AddRow(int64(42)))
rv, err := backend.GetLatestRV(ctx)
require.NoError(t, err)
require.Equal(t, int64(42), rv)
require.NoError(t, rdb.SQLMock.ExpectationsWereMet())
}
func TestPgvectorBackend_GetLatestRV_SeedRowMissing(t *testing.T) {
rdb := test.NewDBProviderNopSQL(t)
backend := NewPgvectorBackend(rdb.DB, 1000, 0)
ctx := testutil.NewDefaultTestContext(t)
rdb.SQLMock.ExpectQuery("SELECT latest_rv FROM vector_latest_rv").
WillReturnError(sql.ErrNoRows)
rv, err := backend.GetLatestRV(ctx)
require.NoError(t, err)
require.Equal(t, int64(0), rv)
require.NoError(t, rdb.SQLMock.ExpectationsWereMet())
}
func TestPartialHNSWName(t *testing.T) {
require.Equal(t, "dashboards_stacks_123_hnsw", partialHNSWName("dashboards", "stacks-123"))
require.Equal(t, "dashboards_weird__name_hnsw", partialHNSWName("dashboards", "weird!!name"))
require.Equal(t, "dashboards_upper_ns_hnsw", partialHNSWName("dashboards", "UPPER-NS"))
}

View file

@ -0,0 +1,263 @@
package vector
import (
"context"
"database/sql"
"errors"
"fmt"
"regexp"
"strings"
"time"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/storage/unified/sql/db"
)
const unifiedParent = "embeddings"
var sanitizeRe = regexp.MustCompile(`[^a-zA-Z0-9]`)
// sanitizeIdentifier folds non-alphanumerics to underscore and lowercases —
// stable across Postgres's case-folding.
func sanitizeIdentifier(s string) string {
return strings.ToLower(sanitizeRe.ReplaceAllString(s, "_"))
}
// subtreeName is the per-resource leaf table: `embeddings_<R>`.
func subtreeName(resource string) string {
return fmt.Sprintf("%s_%s", unifiedParent, resource)
}
// partialHNSWName builds the per-tenant partial HNSW index name. Format is
// `<resource>_<sanitized_namespace>_hnsw`. Postgres caps identifiers at 63
// chars: with `stacks_<int64>` namespaces (max 26 chars), resource names up
// to ~31 chars are safe.
func partialHNSWName(resource, namespace string) string {
return fmt.Sprintf("%s_%s_hnsw", resource, sanitizeIdentifier(namespace))
}
// Promoter creates per-tenant partial HNSW indexes on each resource sub-tree
// for namespaces over the row-count threshold. Timer-driven; off the write
// path.
type Promoter struct {
db db.DB
threshold int
interval time.Duration
log log.Logger
}
func NewPromoter(database db.DB, threshold int, interval time.Duration) *Promoter {
return &Promoter{
db: database,
threshold: threshold,
interval: interval,
log: log.New("vector-promoter"),
}
}
// Run ticks every s.interval until ctx is cancelled. Per-iteration errors
// are logged; the loop keeps running.
func (s *Promoter) Run(ctx context.Context) error {
if s.interval <= 0 {
s.log.Info("vector promoter disabled (interval <= 0)")
<-ctx.Done()
return nil
}
s.log.Info("vector promoter starting", "interval", s.interval, "threshold", s.threshold)
t := time.NewTicker(s.interval)
defer t.Stop()
for {
select {
case <-ctx.Done():
return nil
case <-t.C:
if err := s.Promote(ctx); err != nil {
s.log.Warn("vector promoter iteration failed", "err", err)
}
}
}
}
// Promote runs one pass over every resource sub-tree. Exported for tests.
func (s *Promoter) Promote(ctx context.Context) error {
resources, err := s.discoverResources(ctx)
if err != nil {
return fmt.Errorf("discover resource sub-trees: %w", err)
}
for _, resource := range resources {
if err := s.promoteResource(ctx, resource); err != nil {
return fmt.Errorf("promote %s: %w", resource, err)
}
}
return nil
}
// discoverResources returns resource names from leaf tables attached to the
// unified parent. Filters relkind='r' (regular leaf tables).
func (s *Promoter) discoverResources(ctx context.Context) ([]string, error) {
rows, err := s.db.QueryContext(ctx, `
SELECT c.relname FROM pg_inherits i
JOIN pg_class c ON c.oid = i.inhrelid
JOIN pg_class p ON p.oid = i.inhparent
WHERE p.relname = $1 AND c.relkind = 'r'
`, unifiedParent)
if err != nil {
return nil, err
}
defer func() { _ = rows.Close() }()
prefix := unifiedParent + "_"
var resources []string
for rows.Next() {
var name string
if err := rows.Scan(&name); err != nil {
return nil, err
}
resources = append(resources, strings.TrimPrefix(name, prefix))
}
return resources, rows.Err()
}
func (s *Promoter) promoteResource(ctx context.Context, resource string) error {
subtree := subtreeName(resource)
if err := s.dropInvalidIndexes(ctx, subtree); err != nil {
return err
}
rows, err := s.db.QueryContext(ctx, fmt.Sprintf(
`SELECT namespace FROM %s GROUP BY namespace HAVING COUNT(*) > $1`, subtree),
s.threshold)
if err != nil {
return fmt.Errorf("enumerate tenants in %s: %w", subtree, err)
}
var namespaces []string
for rows.Next() {
var ns string
if err := rows.Scan(&ns); err != nil {
_ = rows.Close()
return err
}
namespaces = append(namespaces, ns)
}
if err := rows.Err(); err != nil {
_ = rows.Close()
return err
}
if err := rows.Close(); err != nil {
return err
}
for _, ns := range namespaces {
if err := ctx.Err(); err != nil {
return err
}
if err := s.promote(ctx, resource, ns); err != nil {
// One bad tenant shouldn't stop the pass.
s.log.Warn("promote failed", "resource", resource, "namespace", ns, "err", err)
}
}
return nil
}
// promote builds a partial HNSW index scoped to (resource, namespace).
// CREATE INDEX CONCURRENTLY can't run in a transaction. Idempotent — skips
// when a valid index already exists.
//
// Concurrency: CREATE INDEX CONCURRENTLY takes only SHARE UPDATE EXCLUSIVE
// on the table, so reads and writes proceed during the build. Build cost
// scales with rows matching the predicate, not the whole table.
func (s *Promoter) promote(ctx context.Context, resource, namespace string) error {
subtree := subtreeName(resource)
idxName := partialHNSWName(resource, namespace)
valid, err := s.indexExistsValid(ctx, idxName)
if err != nil {
return err
}
if valid {
return nil
}
nsLit := "'" + strings.ReplaceAll(namespace, "'", "''") + "'"
ddl := fmt.Sprintf(
`CREATE INDEX CONCURRENTLY IF NOT EXISTS %s
ON %s USING hnsw (embedding halfvec_cosine_ops)
WITH (m = 16, ef_construction = 64)
WHERE namespace = %s`,
idxName, subtree, nsLit,
)
s.log.Info("promoting tenant", "resource", resource, "namespace", namespace, "index", idxName)
if _, err := s.db.ExecContext(ctx, ddl); err != nil {
return fmt.Errorf("create partial index %s: %w", idxName, err)
}
// ANALYZE so the planner's row estimate for the partial index reflects
// reality.
if _, err := s.db.ExecContext(ctx, fmt.Sprintf(`ANALYZE %s`, subtree)); err != nil {
s.log.Warn("ANALYZE after promote failed", "subtree", subtree, "err", err)
}
// Best-effort observability — pg_indexes is the source of truth.
if _, err := s.db.ExecContext(ctx,
`INSERT INTO vector_promoted (namespace, resource, promoted_at)
VALUES ($1, $2, CURRENT_TIMESTAMP)
ON CONFLICT (namespace, resource) DO UPDATE SET promoted_at = EXCLUDED.promoted_at`,
namespace, resource,
); err != nil {
s.log.Warn("vector_promoted upsert failed", "err", err)
}
return nil
}
func (s *Promoter) indexExistsValid(ctx context.Context, idxName string) (bool, error) {
var valid bool
err := s.db.QueryRowContext(ctx, `
SELECT i.indisvalid FROM pg_class c
JOIN pg_index i ON i.indexrelid = c.oid
WHERE c.relname = $1 AND c.relkind = 'i'
`, idxName).Scan(&valid)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return false, nil
}
return false, err
}
return valid, nil
}
// dropInvalidIndexes drops INVALID indexes on the resource sub-tree —
// leftovers from a prior CREATE INDEX CONCURRENTLY that failed mid-build.
func (s *Promoter) dropInvalidIndexes(ctx context.Context, subtree string) error {
rows, err := s.db.QueryContext(ctx, `
SELECT c.relname FROM pg_class c
JOIN pg_index i ON i.indexrelid = c.oid
JOIN pg_class t ON t.oid = i.indrelid
WHERE t.relname = $1 AND c.relkind = 'i' AND NOT i.indisvalid
`, subtree)
if err != nil {
return fmt.Errorf("list invalid indexes on %s: %w", subtree, err)
}
var names []string
for rows.Next() {
var name string
if err := rows.Scan(&name); err != nil {
_ = rows.Close()
return err
}
names = append(names, name)
}
if err := rows.Err(); err != nil {
_ = rows.Close()
return err
}
_ = rows.Close()
for _, n := range names {
s.log.Warn("dropping invalid index", "index", n)
if _, err := s.db.ExecContext(ctx, fmt.Sprintf(`DROP INDEX CONCURRENTLY IF EXISTS %s`, n)); err != nil {
s.log.Warn("DROP INDEX failed", "index", n, "err", err)
}
}
return nil
}

View file

@ -0,0 +1,51 @@
package vector
import (
"context"
"fmt"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/storage/unified/sql/db/dbimpl"
"github.com/grafana/grafana/pkg/util/xorm"
)
func ProvideVectorBackend(cfg *setting.Cfg) (VectorBackend, error) {
return InitVectorBackend(context.Background(), cfg, true)
}
// InitVectorBackend opens the [database_vector] connection and returns a
// pgvectorBackend. runMigrations=true applies schema migrations before
// returning. Returns (nil, nil) when EnableVectorBackend is false.
// Caller starts the promoter via backend.Run(ctx); gate on schema ownership.
func InitVectorBackend(ctx context.Context, cfg *setting.Cfg, runMigrations bool) (VectorBackend, error) {
if !cfg.EnableVectorBackend {
return nil, nil
}
if cfg.VectorDBHost == "" {
return nil, fmt.Errorf("vector backend is enabled but [database_vector] db_host is not set")
}
logger := log.New("vector-db")
connStr := fmt.Sprintf("host=%s port=%s dbname=%s user=%s password=%s sslmode=%s",
cfg.VectorDBHost, cfg.VectorDBPort, cfg.VectorDBName, cfg.VectorDBUser, cfg.VectorDBPassword, cfg.VectorDBSSLMode,
)
engine, err := xorm.NewEngine("postgres", connStr)
if err != nil {
return nil, fmt.Errorf("open vector database: %w", err)
}
if runMigrations {
logger.Info("Running vector database migrations")
if err := MigrateVectorStore(ctx, engine, cfg); err != nil {
return nil, fmt.Errorf("migrate vector database: %w", err)
}
} else {
logger.Info("Skipping vector database migrations")
}
database := dbimpl.NewDB(engine.DB().DB, engine.Dialect().DriverName())
return NewPgvectorBackend(database, cfg.VectorPromotionThreshold, cfg.VectorPromoterInterval), nil
}

View file

@ -0,0 +1,180 @@
package vector
import (
"embed"
"encoding/json"
"fmt"
"reflect"
"text/template"
"github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate"
)
//go:embed data/*.sql
var sqlTemplatesFS embed.FS
var sqlTemplates = template.Must(
template.New("sql").ParseFS(sqlTemplatesFS, `data/*.sql`),
)
func mustTemplate(filename string) *template.Template {
if t := sqlTemplates.Lookup(filename); t != nil {
return t
}
panic(fmt.Sprintf("template file not found: %s", filename))
}
var (
sqlVectorCollectionUpsert = mustTemplate("vector_collection_upsert.sql")
sqlVectorCollectionDelete = mustTemplate("vector_collection_delete.sql")
sqlVectorCollectionDeleteSubresource = mustTemplate("vector_collection_delete_subresources.sql")
sqlVectorCollectionGetContent = mustTemplate("vector_collection_get_content.sql")
sqlVectorCollectionSearch = mustTemplate("vector_collection_search.sql")
)
// All queries target `embeddings` and include `resource = $1 AND
// namespace = $2` so nested partition pruning routes to one leaf.
type sqlVectorCollectionUpsertRequest struct {
sqltemplate.SQLTemplate
Resource string
Vector *Vector
Embedding any // pgvector.HalfVector
}
func (r *sqlVectorCollectionUpsertRequest) Validate() error {
if r.Resource == "" {
return fmt.Errorf("missing resource")
}
if r.Vector == nil {
return fmt.Errorf("missing vector")
}
return nil
}
type sqlVectorCollectionDeleteRequest struct {
sqltemplate.SQLTemplate
Resource string
Namespace string
Model string
UID string
}
func (r *sqlVectorCollectionDeleteRequest) Validate() error {
if r.Resource == "" || r.Namespace == "" || r.Model == "" || r.UID == "" {
return fmt.Errorf("missing required fields")
}
return nil
}
type sqlVectorCollectionDeleteSubresourcesRequest struct {
sqltemplate.SQLTemplate
Resource string
Namespace string
Model string
UID string
Subresources []string
}
func (r *sqlVectorCollectionDeleteSubresourcesRequest) Validate() error {
if r.Resource == "" || r.Namespace == "" || r.Model == "" || r.UID == "" {
return fmt.Errorf("missing required fields")
}
if len(r.Subresources) == 0 {
return fmt.Errorf("subresources must not be empty")
}
return nil
}
func (r *sqlVectorCollectionDeleteSubresourcesRequest) SubresourcesSlice() reflect.Value {
return reflect.ValueOf(r.Subresources)
}
type sqlVectorCollectionGetContentResponse struct {
Subresource string
Content string
}
type sqlVectorCollectionGetContentRequest struct {
sqltemplate.SQLTemplate
Resource string
Namespace string
Model string
UID string
Response *sqlVectorCollectionGetContentResponse
}
func (r *sqlVectorCollectionGetContentRequest) Validate() error {
if r.Resource == "" || r.Namespace == "" || r.Model == "" || r.UID == "" {
return fmt.Errorf("missing required fields")
}
return nil
}
func (r *sqlVectorCollectionGetContentRequest) Results() (*sqlVectorCollectionGetContentResponse, error) {
cp := *r.Response
return &cp, nil
}
type sqlVectorCollectionSearchResponse struct {
UID string
Title string
Subresource string
Content string
Score float64
Folder string
Metadata json.RawMessage
}
// MetadataFilterEntry is a pre-built JSONB containment filter.
type MetadataFilterEntry struct {
JSON string // e.g. `{"datasource_uids":["ds1"]}`
}
type sqlVectorCollectionSearchRequest struct {
sqltemplate.SQLTemplate
Resource string
Namespace string
Model string
QueryEmbedding any // pgvector.HalfVector
Limit int64
Response *sqlVectorCollectionSearchResponse
// nil/empty means no filter on that field.
UIDValues []string
FolderValues []string
MetadataFilters []MetadataFilterEntry
}
func (r *sqlVectorCollectionSearchRequest) Validate() error {
if r.Resource == "" || r.Namespace == "" || r.Model == "" {
return fmt.Errorf("missing required fields")
}
if r.Limit <= 0 {
return fmt.Errorf("limit must be positive")
}
return nil
}
func (r *sqlVectorCollectionSearchRequest) Results() (*sqlVectorCollectionSearchResponse, error) {
// Response is reused across Scan calls; shallow copy is safe because Scan
// allocates a fresh []byte for Metadata.
cp := *r.Response
return &cp, nil
}
func (r *sqlVectorCollectionSearchRequest) UIDFilter() bool {
return len(r.UIDValues) > 0
}
func (r *sqlVectorCollectionSearchRequest) UIDFilterSlice() reflect.Value {
return reflect.ValueOf(r.UIDValues)
}
func (r *sqlVectorCollectionSearchRequest) FolderFilter() bool {
return len(r.FolderValues) > 0
}
func (r *sqlVectorCollectionSearchRequest) FolderFilterSlice() reflect.Value {
return reflect.ValueOf(r.FolderValues)
}

View file

@ -0,0 +1,128 @@
package vector
import (
"encoding/json"
"testing"
"text/template"
pgvector "github.com/pgvector/pgvector-go"
"github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate"
"github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate/mocks"
)
func TestVectorQueries(t *testing.T) {
mocks.CheckQuerySnapshots(t, mocks.TemplateTestSetup{
RootDir: "testdata",
SQLTemplatesFS: sqlTemplatesFS,
Dialects: []sqltemplate.Dialect{sqltemplate.PostgreSQL},
Templates: map[*template.Template][]mocks.TemplateTestCase{
sqlVectorCollectionUpsert: {
{
Name: "simple",
Data: &sqlVectorCollectionUpsertRequest{
SQLTemplate: mocks.NewTestingSQLTemplate(),
Resource: "dashboards",
Vector: &Vector{
Namespace: "stacks-123",
Resource: "dashboards",
UID: "abc-uid",
Title: "CPU Dashboard",
Subresource: "panel/5",
ResourceVersion: 42,
Folder: "folder-uid",
Content: "panel title with queries",
Metadata: json.RawMessage(`{"datasource_uids":["ds1"]}`),
Embedding: []float32{0.1, 0.2, 0.3},
Model: "text-embedding-005",
},
Embedding: pgvector.NewHalfVector([]float32{0.1, 0.2, 0.3}),
},
},
},
sqlVectorCollectionDelete: {
{
Name: "simple",
Data: &sqlVectorCollectionDeleteRequest{
SQLTemplate: mocks.NewTestingSQLTemplate(),
Resource: "dashboards",
Namespace: "stacks-123",
Model: "text-embedding-005",
UID: "abc-uid",
},
},
},
sqlVectorCollectionDeleteSubresource: {
{
Name: "simple",
Data: &sqlVectorCollectionDeleteSubresourcesRequest{
SQLTemplate: mocks.NewTestingSQLTemplate(),
Resource: "dashboards",
Namespace: "stacks-123",
Model: "text-embedding-005",
UID: "abc-uid",
Subresources: []string{"panel/1", "panel/2"},
},
},
},
sqlVectorCollectionGetContent: {
{
Name: "simple",
Data: &sqlVectorCollectionGetContentRequest{
SQLTemplate: mocks.NewTestingSQLTemplate(),
Resource: "dashboards",
Namespace: "stacks-123",
Model: "text-embedding-005",
UID: "abc-uid",
Response: &sqlVectorCollectionGetContentResponse{},
},
},
},
sqlVectorCollectionSearch: {
{
Name: "no filters",
Data: &sqlVectorCollectionSearchRequest{
SQLTemplate: mocks.NewTestingSQLTemplate(),
Resource: "dashboards",
Namespace: "stacks-123",
Model: "text-embedding-005",
QueryEmbedding: []float32{0.1, 0.2, 0.3},
Limit: 10,
Response: &sqlVectorCollectionSearchResponse{},
},
},
{
Name: "with uid filter",
Data: &sqlVectorCollectionSearchRequest{
SQLTemplate: mocks.NewTestingSQLTemplate(),
Resource: "dashboards",
Namespace: "stacks-123",
Model: "text-embedding-005",
QueryEmbedding: []float32{0.1, 0.2, 0.3},
Limit: 10,
UIDValues: []string{"dash-1", "dash-2"},
Response: &sqlVectorCollectionSearchResponse{},
},
},
{
Name: "with all filters",
Data: &sqlVectorCollectionSearchRequest{
SQLTemplate: mocks.NewTestingSQLTemplate(),
Resource: "dashboards",
Namespace: "stacks-123",
Model: "text-embedding-005",
QueryEmbedding: []float32{0.1, 0.2, 0.3},
Limit: 5,
UIDValues: []string{"dash-1"},
FolderValues: []string{"folder-a", "folder-b"},
MetadataFilters: []MetadataFilterEntry{
{JSON: `{"datasource_uids":["ds-uid-1"]}`},
{JSON: `{"query_languages":["promql"]}`},
},
Response: &sqlVectorCollectionSearchResponse{},
},
},
},
},
})
}

View file

@ -0,0 +1,100 @@
package vector
import (
"context"
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util/xorm"
)
// MigrateVectorStore runs migrations for the vector database.
//
// LIST partitioning by resource. Each resource gets a leaf table:
//
// embeddings PARTITION BY LIST (resource)
// └── embeddings_dashboards leaf — all dashboard rows live here
// + partial HNSW per namespace for promoted (big) tenants
// + base GIN(metadata)
//
// Future resource subtrees will be attached at runtime via advisory-lock.
func MigrateVectorStore(ctx context.Context, engine *xorm.Engine, cfg *setting.Cfg) error {
mg := migrator.NewScopedMigrator(engine, cfg, "vector")
mg.AddCreateMigration()
initVectorTables(mg)
sec := cfg.Raw.Section("database_vector")
return mg.RunMigrations(
ctx,
sec.Key("migration_locking").MustBool(true),
sec.Key("locking_attempt_timeout_sec").MustInt(),
)
}
func initVectorTables(mg *migrator.Migrator) {
mg.AddMigration("create pgvector extension",
migrator.NewRawSQLMigration("").Postgres(`CREATE EXTENSION IF NOT EXISTS vector;`))
// (resource, namespace) lead the PK so partition pruning can use it.
// halfvec + nested partitioning aren't expressible via xorm, so raw SQL.
mg.AddMigration("create embeddings parent",
migrator.NewRawSQLMigration("").Postgres(`
CREATE TABLE IF NOT EXISTS embeddings (
id BIGSERIAL,
resource VARCHAR(256) NOT NULL,
namespace VARCHAR(256) NOT NULL,
model VARCHAR(256) NOT NULL,
uid VARCHAR(256) NOT NULL,
title VARCHAR(1024) NOT NULL,
subresource VARCHAR(256) NOT NULL DEFAULT '',
folder VARCHAR(256),
content TEXT NOT NULL,
metadata JSONB,
embedding halfvec(1024) NOT NULL,
PRIMARY KEY (resource, namespace, model, uid, subresource)
) PARTITION BY LIST (resource);
`))
mg.AddMigration("create embeddings_dashboards leaf",
migrator.NewRawSQLMigration("").Postgres(`
CREATE TABLE IF NOT EXISTS embeddings_dashboards
PARTITION OF embeddings
FOR VALUES IN ('dashboards');
`))
mg.AddMigration("create metadata GIN on embeddings_dashboards",
migrator.NewRawSQLMigration("").Postgres(
`CREATE INDEX IF NOT EXISTS embeddings_dashboards_metadata_idx
ON embeddings_dashboards USING GIN (metadata);`,
))
// Observability log. pg_inherits is the source of truth for leaf existence.
vectorPromoted := migrator.Table{
Name: "vector_promoted",
Columns: []*migrator.Column{
{Name: "namespace", Type: migrator.DB_Varchar, Length: 256, Nullable: false, IsPrimaryKey: true},
{Name: "resource", Type: migrator.DB_Varchar, Length: 256, Nullable: false, IsPrimaryKey: true},
{Name: "promoted_at", Type: migrator.DB_TimeStampz, Nullable: false, Default: "CURRENT_TIMESTAMP"},
},
}
mg.AddMigration("create vector_promoted table",
migrator.NewAddTableMigration(vectorPromoted))
// Single-row global checkpoint for startup recovery. Retired once we
// have a persistent queue.
vectorLatestRV := migrator.Table{
Name: "vector_latest_rv",
Columns: []*migrator.Column{
{Name: "id", Type: migrator.DB_Int, IsPrimaryKey: true, Nullable: false},
{Name: "latest_rv", Type: migrator.DB_BigInt, Nullable: false, Default: "0"},
},
}
mg.AddMigration("create vector_latest_rv table",
migrator.NewAddTableMigration(vectorLatestRV))
mg.AddMigration("enforce single-row and seed vector_latest_rv",
migrator.NewRawSQLMigration("").Postgres(`
ALTER TABLE vector_latest_rv ADD CONSTRAINT vector_latest_rv_single CHECK (id = 1);
INSERT INTO vector_latest_rv (id, latest_rv) VALUES (1, 0) ON CONFLICT DO NOTHING;
`))
}

View file

@ -0,0 +1,84 @@
package vector
import (
"context"
"encoding/json"
"errors"
)
// VectorBackend is vector storage isolated per (namespace, model) so an HNSW
// never mixes embeddings from different vector spaces.
type VectorBackend interface {
// Search returns top-N nearest neighbors by cosine distance. Query
// embedding must come from the same model as stored vectors.
Search(ctx context.Context, namespace, model, resource string,
embedding []float32, limit int, filters ...SearchFilter) ([]VectorSearchResult, error)
Upsert(ctx context.Context, vectors []Vector) error
// Delete removes every resource and subresource under `uid`. model must be non-empty.
Delete(ctx context.Context, namespace, model, resource, uid string) error
// DeleteSubresources removes specific subresources under `uid`. Empty
// slice is a no-op. model must be non-empty.
DeleteSubresources(ctx context.Context, namespace, model, resource, uid string, subresources []string) error
// GetSubresourceContent returns subresource → stored content. Callers
// diff against candidate content to skip re-embedding unchanged rows.
// Used for deleting stale subresource embeddings.
GetSubresourceContent(ctx context.Context, namespace, model, resource, uid string) (map[string]string, error)
// GetLatestRV is the global write-pipeline checkpoint. 0 if empty.
GetLatestRV(ctx context.Context) (int64, error)
// Run starts background maintenance (promoter). Gate on schema ownership.
Run(ctx context.Context) error
}
// Vector is one embeddable subresource (e.g. a dashboard panel).
type Vector struct {
Namespace string
Resource string // e.g. "dashboards"
UID string // stable resource identifier (e.g. dashboard UID)
Title string // human-readable title for search results
Subresource string // e.g. "panel/5"
ResourceVersion int64 // feeds the global checkpoint; not stored per-row
Folder string // folder UID for authz filtering
Content string // text that was embedded
Metadata json.RawMessage
Embedding []float32
Model string
}
func (v *Vector) Validate() error {
switch {
case v.Namespace == "":
return errors.New("namespace must not be empty")
case v.Model == "":
return errors.New("model must not be empty")
case v.Resource == "":
return errors.New("resource must not be empty")
case v.UID == "":
return errors.New("uid must not be empty")
case v.Title == "":
return errors.New("title must not be empty")
}
return nil
}
type VectorSearchResult struct {
UID string
Title string
Subresource string
Content string
Score float64
Folder string
Metadata json.RawMessage
}
// SearchFilter constrains results. Field is a top-level column
// ("uid", "folder") or a JSONB metadata key.
type SearchFilter struct {
Field string
Values []string
}

View file

@ -0,0 +1,6 @@
DELETE FROM embeddings
WHERE "resource" = 'dashboards'
AND "namespace" = 'stacks-123'
AND "model" = 'text-embedding-005'
AND "uid" = 'abc-uid'
;

View file

@ -0,0 +1,7 @@
DELETE FROM embeddings
WHERE "resource" = 'dashboards'
AND "namespace" = 'stacks-123'
AND "model" = 'text-embedding-005'
AND "uid" = 'abc-uid'
AND "subresource" IN ('panel/1', 'panel/2')
;

View file

@ -0,0 +1,9 @@
SELECT
"subresource",
"content"
FROM embeddings
WHERE "resource" = 'dashboards'
AND "namespace" = 'stacks-123'
AND "model" = 'text-embedding-005'
AND "uid" = 'abc-uid'
;

View file

@ -0,0 +1,15 @@
SELECT
"uid",
"title",
"subresource",
"content",
"embedding" <=> '[0.1 0.2 0.3]' AS "score",
"folder",
"metadata"
FROM embeddings
WHERE "resource" = 'dashboards'
AND "namespace" = 'stacks-123'
AND "model" = 'text-embedding-005'
ORDER BY "embedding" <=> '[0.1 0.2 0.3]'
LIMIT 10
;

View file

@ -0,0 +1,19 @@
SELECT
"uid",
"title",
"subresource",
"content",
"embedding" <=> '[0.1 0.2 0.3]' AS "score",
"folder",
"metadata"
FROM embeddings
WHERE "resource" = 'dashboards'
AND "namespace" = 'stacks-123'
AND "model" = 'text-embedding-005'
AND "uid" IN ('dash-1')
AND "folder" IN ('folder-a', 'folder-b')
AND "metadata" @> '{"datasource_uids":["ds-uid-1"]}'
AND "metadata" @> '{"query_languages":["promql"]}'
ORDER BY "embedding" <=> '[0.1 0.2 0.3]'
LIMIT 5
;

View file

@ -0,0 +1,16 @@
SELECT
"uid",
"title",
"subresource",
"content",
"embedding" <=> '[0.1 0.2 0.3]' AS "score",
"folder",
"metadata"
FROM embeddings
WHERE "resource" = 'dashboards'
AND "namespace" = 'stacks-123'
AND "model" = 'text-embedding-005'
AND "uid" IN ('dash-1', 'dash-2')
ORDER BY "embedding" <=> '[0.1 0.2 0.3]'
LIMIT 10
;

View file

@ -0,0 +1,32 @@
INSERT INTO embeddings (
"resource",
"namespace",
"model",
"uid",
"title",
"subresource",
"folder",
"content",
"metadata",
"embedding"
)
VALUES (
'dashboards',
'stacks-123',
'text-embedding-005',
'abc-uid',
'CPU Dashboard',
'panel/5',
'folder-uid',
'panel title with queries',
'[123 34 100 97 116 97 115 111 117 114 99 101 95 117 105 100 115 34 58 91 34 100 115 49 34 93 125]',
'[0.1,0.2,0.3]'
)
ON CONFLICT ("resource", "namespace", "model", "uid", "subresource")
DO UPDATE SET
"title" = 'CPU Dashboard',
"folder" = 'folder-uid',
"content" = 'panel title with queries',
"metadata" = '[123 34 100 97 116 97 115 111 117 114 99 101 95 117 105 100 115 34 58 91 34 100 115 49 34 93 125]',
"embedding" = '[0.1,0.2,0.3]'
;

View file

@ -18,6 +18,7 @@ import (
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/storage/unified/resource"
"github.com/grafana/grafana/pkg/storage/unified/resourcepb"
"github.com/grafana/grafana/pkg/storage/unified/search/vector"
)
type QOSEnqueueDequeuer interface {
@ -29,6 +30,7 @@ type QOSEnqueueDequeuer interface {
// ServerOptions contains the options for creating a new ResourceServer
type ServerOptions struct {
Backend resource.StorageBackend
VectorBackend vector.VectorBackend
OverridesService *resource.OverridesService
Cfg *setting.Cfg
Tracer trace.Tracer
@ -59,6 +61,7 @@ func NewUninitializedResourceServer(opts ServerOptions) (resource.ResourceServer
withAccessClient,
withMaxPageSizeBytes,
withBackend,
withVectorBackend,
withQOSQueue,
withOverridesService,
withSearch,
@ -92,6 +95,7 @@ func NewUninitializedSearchServer(opts ServerOptions) (resource.SearchServer, er
withBlobConfig,
withAccessClient,
withBackend,
withVectorBackend,
withSearch,
)
if err != nil {
@ -181,6 +185,13 @@ func withBackend(opts *ServerOptions, resourceOpts *resource.ResourceServerOptio
return nil
}
// withVectorBackend propagates the optional VectorBackend through. nil is
// allowed; callers fall back to non-vector search paths when it's absent.
func withVectorBackend(opts *ServerOptions, resourceOpts *resource.ResourceServerOptions) error {
resourceOpts.VectorBackend = opts.VectorBackend
return nil
}
func withSearchClient(opts *ServerOptions, resourceOpts *resource.ResourceServerOptions) error {
resourceOpts.SearchClient = opts.SearchClient
return nil

View file

@ -31,6 +31,7 @@ import (
"github.com/grafana/grafana/pkg/storage/unified/resource"
"github.com/grafana/grafana/pkg/storage/unified/resourcepb"
"github.com/grafana/grafana/pkg/storage/unified/search"
"github.com/grafana/grafana/pkg/storage/unified/search/vector"
"github.com/grafana/grafana/pkg/util/scheduler"
)
@ -49,6 +50,7 @@ type service struct {
// -- Shared Components
backend resource.StorageBackend
vectorBackend vector.VectorBackend
serverStopper resource.ResourceServerStopper
cfg *setting.Cfg
features featuremgmt.FeatureToggles
@ -97,10 +99,11 @@ func ProvideSearchGRPCService(cfg *setting.Cfg,
memberlistKVConfig kv.Config,
httpServerRouter *mux.Router,
backend resource.StorageBackend,
vectorBackend vector.VectorBackend,
provider grpcserver.Provider,
opts ...ServiceOption,
) (resource.UnifiedStorageGrpcService, error) {
s := newService(cfg, features, log, reg, otel.Tracer("unified-storage"), docBuilders, nil, indexMetrics, searchRing, backend, nil)
s := newService(cfg, features, log, reg, otel.Tracer("unified-storage"), docBuilders, nil, indexMetrics, searchRing, backend, vectorBackend, nil)
for _, opt := range opts {
opt(s)
}
@ -135,11 +138,12 @@ func ProvideUnifiedStorageGrpcService(cfg *setting.Cfg,
memberlistKVConfig kv.Config,
httpServerRouter *mux.Router,
backend resource.StorageBackend,
vectorBackend vector.VectorBackend,
searchClient resourcepb.ResourceIndexClient,
provider grpcserver.Provider,
opts ...ServiceOption,
) (resource.UnifiedStorageGrpcService, error) {
s := newService(cfg, features, log, reg, otel.Tracer("unified-storage"), docBuilders, storageMetrics, indexMetrics, searchRing, backend, searchClient)
s := newService(cfg, features, log, reg, otel.Tracer("unified-storage"), docBuilders, storageMetrics, indexMetrics, searchRing, backend, vectorBackend, searchClient)
for _, opt := range opts {
opt(s)
}
@ -195,12 +199,14 @@ func newService(
indexMetrics *resource.BleveIndexMetrics,
searchRing *ring.Ring,
backend resource.StorageBackend,
vectorBackend vector.VectorBackend,
searchClient resourcepb.ResourceIndexClient,
) *service {
authn := grpcutils.NewAuthenticator(ReadGrpcServerConfig(cfg), tracer)
return &service{
backend: backend,
vectorBackend: vectorBackend,
cfg: cfg,
features: features,
authenticator: authn,

View file

@ -143,7 +143,7 @@ type reFormatting struct {
var formatREs = []reFormatting{
{re: regexp.MustCompile(`\s+`), replacement: " "},
{re: regexp.MustCompile(` ?([+-/*=<>%!~]+) ?`), replacement: " $1 "},
{re: regexp.MustCompile(` ?([+-/*=<>%!~@#]+) ?`), replacement: " $1 "},
{re: regexp.MustCompile(`([([{]) `), replacement: "$1"},
{re: regexp.MustCompile(` ([)\]}])`), replacement: "$1"},
{re: regexp.MustCompile(` ?, ?`), replacement: ", "},

View file

@ -89,3 +89,20 @@ func TestFormatSQL(t *testing.T) {
got)
}
}
func TestFormatSQL_PreservesJSONBOperators(t *testing.T) {
t.Parallel()
cases := map[string]string{
`"m" @> $1`: `"m" @> $1`,
`"m" <@ $1`: `"m" <@ $1`,
`"m" #> $1`: `"m" #> $1`,
`"m" #>> $1`: `"m" #>> $1`,
`"m" @@ $1`: `"m" @@ $1`,
}
for input, expected := range cases {
if got := FormatSQL(input); got != expected {
t.Errorf("input %q: expected %q, got %q", input, expected, got)
}
}
}

View file

@ -233,7 +233,7 @@ func TestClientServer(t *testing.T) {
_ = services.StopAndAwaitTerminated(ctx, grpcService)
})
svc, err := sql.ProvideUnifiedStorageGrpcService(cfg, features, nil, registerer, nil, nil, nil, nil, kv.Config{}, nil, backend, nil, grpcService,
svc, err := sql.ProvideUnifiedStorageGrpcService(cfg, features, nil, registerer, nil, nil, nil, nil, kv.Config{}, nil, backend, nil, nil, grpcService,
sql.WithAuthenticator(func(ctx context.Context) (context.Context, error) {
auth := grpcUtils.Authenticator{Tracer: otel.Tracer("test")}
return auth.Authenticate(ctx)
@ -342,7 +342,7 @@ func TestIntegrationSearchClientServer(t *testing.T) {
grpcService, err := grpcserver.ProvideDSKitService(cfg, otel.Tracer("test-grpc-server"), prometheus.NewPedanticRegistry(), "test-grpc-server")
require.NoError(t, err)
svc, err := sql.ProvideSearchGRPCService(cfg, features, log.New("test"), registerer, docBuilders, nil, nil, kv.Config{}, nil, backend, grpcService,
svc, err := sql.ProvideSearchGRPCService(cfg, features, log.New("test"), registerer, docBuilders, nil, nil, kv.Config{}, nil, backend, nil, grpcService,
sql.WithAuthenticator(func(ctx context.Context) (context.Context, error) {
auth := grpcUtils.Authenticator{Tracer: otel.Tracer("test")}
return auth.Authenticate(ctx)

View file

@ -199,7 +199,7 @@ func newRemoteClient(t *testing.T, backend resource.KVBackend) resource.Resource
grpcService, err := grpcserver.ProvideDSKitService(cfg, otel.Tracer("test"), prometheus.NewPedanticRegistry(), "test")
require.NoError(t, err)
svc, err := sql.ProvideUnifiedStorageGrpcService(cfg, features, log.NewNopLogger(), reg, nil, nil, nil, nil, kv.Config{}, nil, backend, nil, grpcService,
svc, err := sql.ProvideUnifiedStorageGrpcService(cfg, features, log.NewNopLogger(), reg, nil, nil, nil, nil, kv.Config{}, nil, backend, nil, nil, grpcService,
sql.WithAuthenticator(func(ctx context.Context) (context.Context, error) {
auth := grpcUtils.Authenticator{Tracer: otel.Tracer("test")}
return auth.Authenticate(ctx)

View file

@ -181,7 +181,7 @@ func StartGrafanaEnvWithManualCleanup(t *testing.T, grafDir, cfgPath string) (st
require.NoError(t, services.StartAndAwaitRunning(context.Background(), backendService))
storage, err = sql.ProvideUnifiedStorageGrpcService(env.Cfg, env.FeatureToggles,
env.Cfg.Logger, registerer, nil, nil, nil, nil, kv.Config{}, nil, storageBackend, nil, grpcService)
env.Cfg.Logger, registerer, nil, nil, nil, nil, kv.Config{}, nil, storageBackend, nil, nil, grpcService)
require.NoError(t, err)
err = grpcService.StartAsync(ctx)
require.NoError(t, err)