UnifiedSearch: Introduce a ResourceIndex interface and bleve implementation (#96826)

Co-authored-by: Scott Lepper <scott.lepper@gmail.com>
This commit is contained in:
Ryan McKinley 2024-11-22 16:44:06 +03:00 committed by GitHub
parent bbae396db4
commit c6848d4b68
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 2533 additions and 425 deletions

15
go.mod
View File

@ -38,6 +38,7 @@ require (
github.com/beevik/etree v1.4.1 // @grafana/grafana-backend-group
github.com/benbjohnson/clock v1.3.5 // @grafana/alerting-backend
github.com/blang/semver/v4 v4.0.0 // indirect; @grafana/grafana-developer-enablement-squad
github.com/blevesearch/bleve/v2 v2.4.3 // @grafana/grafana-search-and-storage
github.com/blugelabs/bluge v0.1.9 // @grafana/grafana-backend-group
github.com/blugelabs/bluge_segment_api v0.2.0 // @grafana/grafana-backend-group
github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 // @grafana/grafana-backend-group
@ -480,11 +481,24 @@ require github.com/openzipkin/zipkin-go v0.4.3 // @grafana/oss-big-tent
require (
cloud.google.com/go/longrunning v0.6.0 // indirect
github.com/at-wat/mqtt-go v0.19.4 // indirect
github.com/blevesearch/bleve_index_api v1.1.12 // indirect
github.com/blevesearch/geo v0.1.20 // indirect
github.com/blevesearch/go-faiss v1.0.23 // indirect
github.com/blevesearch/gtreap v0.1.1 // indirect
github.com/blevesearch/scorch_segment_api/v2 v2.2.16 // indirect
github.com/blevesearch/upsidedown_store_api v1.0.2 // indirect
github.com/blevesearch/zapx/v11 v11.3.10 // indirect
github.com/blevesearch/zapx/v12 v12.3.10 // indirect
github.com/blevesearch/zapx/v13 v13.3.10 // indirect
github.com/blevesearch/zapx/v14 v14.3.10 // indirect
github.com/blevesearch/zapx/v15 v15.3.16 // indirect
github.com/blevesearch/zapx/v16 v16.1.8 // indirect
github.com/c2h5oh/datasize v0.0.0-20231215233829-aa82cc1e6500 // indirect
github.com/cespare/xxhash v1.1.0 // indirect
github.com/dolthub/maphash v0.1.0 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/gammazero/deque v0.2.1 // indirect
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551 // indirect
github.com/grafana/jsonparser v0.0.0-20240425183733-ea80629e1a32 // indirect
github.com/grafana/loki/pkg/push v0.0.0-20231124142027-e52380921608 // indirect
github.com/grafana/sqlds/v4 v4.1.0 // indirect
@ -500,6 +514,7 @@ require (
github.com/sercand/kuberesolver/v5 v5.1.1 // indirect
github.com/shadowspore/fossil-delta v0.0.0-20240102155221-e3a8590b820b // indirect
github.com/sony/gobreaker v0.5.0 // indirect
go.etcd.io/bbolt v1.3.10 // indirect
go.opentelemetry.io/collector/featuregate v1.9.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.31.0 // indirect
go4.org/netipx v0.0.0-20230125063823-8449b0a6169f // indirect

28
go.sum
View File

@ -1616,19 +1616,45 @@ github.com/bits-and-blooms/bitset v1.12.0 h1:U/q1fAF7xXRhFCrhROzIfffYnu+dlS38vCZ
github.com/bits-and-blooms/bitset v1.12.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
github.com/blevesearch/bleve/v2 v2.4.3 h1:XDYj+1prgX84L2Cf+V3ojrOPqXxy0qxyd2uLMmeuD+4=
github.com/blevesearch/bleve/v2 v2.4.3/go.mod h1:hEPDPrbYw3vyrm5VOa36GyS4bHWuIf4Fflp7460QQXY=
github.com/blevesearch/bleve_index_api v1.1.12 h1:P4bw9/G/5rulOF7SJ9l4FsDoo7UFJ+5kexNy1RXfegY=
github.com/blevesearch/bleve_index_api v1.1.12/go.mod h1:PbcwjIcRmjhGbkS/lJCpfgVSMROV6TRubGGAODaK1W8=
github.com/blevesearch/geo v0.1.20 h1:paaSpu2Ewh/tn5DKn/FB5SzvH0EWupxHEIwbCk/QPqM=
github.com/blevesearch/geo v0.1.20/go.mod h1:DVG2QjwHNMFmjo+ZgzrIq2sfCh6rIHzy9d9d0B59I6w=
github.com/blevesearch/go-faiss v1.0.23 h1:Wmc5AFwDLKGl2L6mjLX1Da3vCL0EKa2uHHSorcIS1Uc=
github.com/blevesearch/go-faiss v1.0.23/go.mod h1:OMGQwOaRRYxrmeNdMrXJPvVx8gBnvE5RYrr0BahNnkk=
github.com/blevesearch/go-porterstemmer v1.0.3 h1:GtmsqID0aZdCSNiY8SkuPJ12pD4jI+DdXTAn4YRcHCo=
github.com/blevesearch/go-porterstemmer v1.0.3/go.mod h1:angGc5Ht+k2xhJdZi511LtmxuEf0OVpvUUNrwmM1P7M=
github.com/blevesearch/gtreap v0.1.1 h1:2JWigFrzDMR+42WGIN/V2p0cUvn4UP3C4Q5nmaZGW8Y=
github.com/blevesearch/gtreap v0.1.1/go.mod h1:QaQyDRAT51sotthUWAH4Sj08awFSSWzgYICSZ3w0tYk=
github.com/blevesearch/mmap-go v1.0.2/go.mod h1:ol2qBqYaOUsGdm7aRMRrYGgPvnwLe6Y+7LMvAB5IbSA=
github.com/blevesearch/mmap-go v1.0.4 h1:OVhDhT5B/M1HNPpYPBKIEJaD0F3Si+CrEKULGCDPWmc=
github.com/blevesearch/mmap-go v1.0.4/go.mod h1:EWmEAOmdAS9z/pi/+Toxu99DnsbhG1TIxUoRmJw/pSs=
github.com/blevesearch/scorch_segment_api/v2 v2.2.16 h1:uGvKVvG7zvSxCwcm4/ehBa9cCEuZVE+/zvrSl57QUVY=
github.com/blevesearch/scorch_segment_api/v2 v2.2.16/go.mod h1:VF5oHVbIFTu+znY1v30GjSpT5+9YFs9dV2hjvuh34F0=
github.com/blevesearch/segment v0.9.0/go.mod h1:9PfHYUdQCgHktBgvtUOF4x+pc4/l8rdH0u5spnW85UQ=
github.com/blevesearch/segment v0.9.1 h1:+dThDy+Lvgj5JMxhmOVlgFfkUtZV2kw49xax4+jTfSU=
github.com/blevesearch/segment v0.9.1/go.mod h1:zN21iLm7+GnBHWTao9I+Au/7MBiL8pPFtJBJTsk6kQw=
github.com/blevesearch/snowballstem v0.9.0 h1:lMQ189YspGP6sXvZQ4WZ+MLawfV8wOmPoD/iWeNXm8s=
github.com/blevesearch/snowballstem v0.9.0/go.mod h1:PivSj3JMc8WuaFkTSRDW2SlrulNWPl4ABg1tC/hlgLs=
github.com/blevesearch/upsidedown_store_api v1.0.2 h1:U53Q6YoWEARVLd1OYNc9kvhBMGZzVrdmaozG2MfoB+A=
github.com/blevesearch/upsidedown_store_api v1.0.2/go.mod h1:M01mh3Gpfy56Ps/UXHjEO/knbqyQ1Oamg8If49gRwrQ=
github.com/blevesearch/vellum v1.0.5/go.mod h1:atE0EH3fvk43zzS7t1YNdNC7DbmcC3uz+eMD5xZ2OyQ=
github.com/blevesearch/vellum v1.0.10 h1:HGPJDT2bTva12hrHepVT3rOyIKFFF4t7Gf6yMxyMIPI=
github.com/blevesearch/vellum v1.0.10/go.mod h1:ul1oT0FhSMDIExNjIxHqJoGpVrBpKCdgDQNxfqgJt7k=
github.com/blevesearch/zapx/v11 v11.3.10 h1:hvjgj9tZ9DeIqBCxKhi70TtSZYMdcFn7gDb71Xo/fvk=
github.com/blevesearch/zapx/v11 v11.3.10/go.mod h1:0+gW+FaE48fNxoVtMY5ugtNHHof/PxCqh7CnhYdnMzQ=
github.com/blevesearch/zapx/v12 v12.3.10 h1:yHfj3vXLSYmmsBleJFROXuO08mS3L1qDCdDK81jDl8s=
github.com/blevesearch/zapx/v12 v12.3.10/go.mod h1:0yeZg6JhaGxITlsS5co73aqPtM04+ycnI6D1v0mhbCs=
github.com/blevesearch/zapx/v13 v13.3.10 h1:0KY9tuxg06rXxOZHg3DwPJBjniSlqEgVpxIqMGahDE8=
github.com/blevesearch/zapx/v13 v13.3.10/go.mod h1:w2wjSDQ/WBVeEIvP0fvMJZAzDwqwIEzVPnCPrz93yAk=
github.com/blevesearch/zapx/v14 v14.3.10 h1:SG6xlsL+W6YjhX5N3aEiL/2tcWh3DO75Bnz77pSwwKU=
github.com/blevesearch/zapx/v14 v14.3.10/go.mod h1:qqyuR0u230jN1yMmE4FIAuCxmahRQEOehF78m6oTgns=
github.com/blevesearch/zapx/v15 v15.3.16 h1:Ct3rv7FUJPfPk99TI/OofdC+Kpb4IdyfdMH48sb+FmE=
github.com/blevesearch/zapx/v15 v15.3.16/go.mod h1:Turk/TNRKj9es7ZpKK95PS7f6D44Y7fAFy8F4LXQtGg=
github.com/blevesearch/zapx/v16 v16.1.8 h1:Bxzpw6YQpFs7UjoCV1+RvDw6fmAT2GZxldwX8b3wVBM=
github.com/blevesearch/zapx/v16 v16.1.8/go.mod h1:JqQlOqlRVaYDkpLIl3JnKql8u4zKTNlVEa3nLsi0Gn8=
github.com/blugelabs/bluge v0.1.9 h1:bPgXlcsWugrXNjzeoLdOnvfJpHsyODKpYaAndayl/SM=
github.com/blugelabs/bluge v0.1.9/go.mod h1:5d7LktUkQgvbh5Bmi6tPWtvo4+6uRTm6gAwP+5z6FqQ=
github.com/blugelabs/bluge_segment_api v0.2.0 h1:cCX1Y2y8v0LZ7+EEJ6gH7dW6TtVTW4RhG0vp3R+N2Lo=
@ -2073,6 +2099,8 @@ github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2V
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551 h1:gtexQ/VGyN+VVFRXSFiguSNcXmS6rkKT+X7FdIrTtfo=
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4=
github.com/golang/glog v1.1.0/go.mod h1:pfYeQZ3JWZoXTV5sFc986z3HTpwQs9At6P4ImfuP3NQ=

View File

@ -571,6 +571,7 @@ github.com/RoaringBitmap/gocroaring v0.4.0 h1:5nufXUgWpBEUNEJXw7926YAA58ZAQRpWPr
github.com/RoaringBitmap/real-roaring-datasets v0.0.0-20190726190000-eb7c87156f76 h1:ZYlhPbqQFU+AHfgtCdHGDTtRW1a8geZyiE8c6Q+Sl1s=
github.com/RoaringBitmap/roaring v0.9.4 h1:ckvZSX5gwCRaJYBNe7syNawCU5oruY9gQmjXlp4riwo=
github.com/RoaringBitmap/roaring v0.9.4/go.mod h1:icnadbWcNyfEHlYdr+tDlOTih1Bf/h+rzPpv4sbomAA=
github.com/RoaringBitmap/roaring v1.2.3/go.mod h1:plvDsJQpxOC5bw8LRteu/MLWHsHez/3y6cubLI4/1yE=
github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398/go.mod h1:a1uqRtAwp2Xwc6WNPJEufxJ7fx3npB4UV/JOLmbu5I0=
github.com/Shopify/goreferrer v0.0.0-20220729165902-8cddb4f5de06 h1:KkH3I3sJuOLP3TjA/dfr4NAY8bghDwnXiU7cTKxQqo0=
github.com/Shopify/goreferrer v0.0.0-20220729165902-8cddb4f5de06/go.mod h1:7erjKLwalezA0k99cWs5L11HWOAPNjdUZ6RxH1BXbbM=
@ -651,11 +652,13 @@ github.com/benbjohnson/immutable v0.4.0/go.mod h1:iAr8OjJGLnLmVUr9MZ/rz4PWUy6Ouc
github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY=
github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932 h1:mXoPYz/Ul5HYEDvkta6I8/rnYM5gSdSV2tJ6XbZuEtY=
github.com/bits-and-blooms/bitset v1.2.0 h1:Kn4yilvwNtMACtf1eYDlG8H77R07mZSPbMjLyS07ChA=
github.com/blevesearch/bleve_index_api v1.0.6/go.mod h1:YXMDwaXFFXwncRS8UobWs7nvo0DmusriM1nztTlj1ms=
github.com/blevesearch/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:kDy+zgJFJJoJYBvdfBSiZYBbdsUL0XcjHYWezpQBGPA=
github.com/blevesearch/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:9eJDeqxJ3E7WnLebQUlPD7ZjSce7AnDb9vjGmMCbD0A=
github.com/blevesearch/goleveldb v1.0.1 h1:iAtV2Cu5s0GD1lwUiekkFHe2gTMCCNVj2foPclDLIFI=
github.com/blevesearch/goleveldb v1.0.1/go.mod h1:WrU8ltZbIp0wAoig/MHbrPCXSOLpe79nz5lv5nqfYrQ=
github.com/blevesearch/mmap-go v1.0.3/go.mod h1:pYvKl/grLQrBxuaRYgoTssa4rVujYYeenDp++2E+yvs=
github.com/blevesearch/scorch_segment_api/v2 v2.1.6/go.mod h1:nQQYlp51XvoSVxcciBjtvuHPIVjlWrN1hX4qwK2cqdc=
github.com/blevesearch/snowball v0.6.1 h1:cDYjn/NCH+wwt2UdehaLpr2e4BwLIjN4V/TdLsL+B5A=
github.com/blevesearch/snowball v0.6.1/go.mod h1:ZF0IBg5vgpeoUhnMza2v0A/z8m1cWPlwhke08LpNusg=
github.com/blevesearch/stempel v0.2.0 h1:CYzVPaScODMvgE9o+kf6D4RJ/VRomyi9uHF+PtB+Afc=
@ -978,8 +981,6 @@ github.com/grafana/go-json v0.0.0-20241106155216-71a03f133f5c/go.mod h1:oq7eo15S
github.com/grafana/gomemcache v0.0.0-20240229205252-cd6a66d6fb56/go.mod h1:PGk3RjYHpxMM8HFPhKKo+vve3DdlPUELZLSDEFehPuU=
github.com/grafana/grafana-app-sdk v0.19.0/go.mod h1:y0BgzYxc+a7CwOqkwUhN9zXd5cgZJjd2zAbgHEd/xzo=
github.com/grafana/pyroscope-go/godeltaprof v0.1.6/go.mod h1:Tk376Nbldo4Cha9RgiU7ik8WKFkNpfds98aUzS8omLE=
github.com/grafana/pyroscope/api v1.0.0 h1:RWK3kpv8EAnB7JpOqnf//xwE84DdKF03N/iFxpFAoHY=
github.com/grafana/pyroscope/api v1.0.0/go.mod h1:CUrgOgSZDnx4M1mlRoxhrVKkTuKIse9p4FtuPbrGA04=
github.com/grafana/tail v0.0.0-20230510142333-77b18831edf0 h1:bjh0PVYSVVFxzINqPFYJmAmJNrWPgnVjuSdYJGHmtFU=
github.com/grafana/tail v0.0.0-20230510142333-77b18831edf0/go.mod h1:7t5XR+2IA8P2qggOAHTj/GCZfoLBle3OvNSYh1VkRBU=
github.com/grafana/thema v0.0.0-20230511182720-3146087fcc26 h1:HX927q4X1n451pnGb8U0wq74i8PCzuxVjzv7TyD10kc=
@ -1061,6 +1062,7 @@ github.com/jon-whit/go-grpc-prometheus v1.4.0 h1:/wmpGDJcLXuEjXryWhVYEGt9YBRhtLw
github.com/jon-whit/go-grpc-prometheus v1.4.0/go.mod h1:iTPm+Iuhh3IIqR0iGZ91JJEg5ax6YQEe1I0f6vtBuao=
github.com/joncrlsn/dque v0.0.0-20211108142734-c2ef48c5192a h1:sfe532Ipn7GX0V6mHdynBk393rDmqgI0QmjLK7ct7TU=
github.com/joncrlsn/dque v0.0.0-20211108142734-c2ef48c5192a/go.mod h1:dNKs71rs2VJGBAmttu7fouEsRQlRjxy0p1Sx+T5wbpY=
github.com/json-iterator/go v0.0.0-20171115153421-f7279a603ede/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o=
github.com/jsternberg/zap-logfmt v1.2.0 h1:1v+PK4/B48cy8cfQbxL4FmmNZrjnIMr2BsnyEmXqv2o=
github.com/jsternberg/zap-logfmt v1.2.0/go.mod h1:kz+1CUmCutPWABnNkOu9hOHKdT2q3TDYCcsFy9hpqb0=
@ -1293,8 +1295,6 @@ github.com/opentracing/basictracer-go v1.0.0 h1:YyUAhaEfjoWXclZVJ9sGoNct7j4TVk7l
github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5 h1:ZCnq+JUrvXcDVhX/xRolRBZifmabN1HcS1wrPSvxhrU=
github.com/openzipkin/zipkin-go v0.4.2 h1:zjqfqHjUpPmB3c1GlCvvgsM1G4LkvqQbBDueDOCg/jA=
github.com/openzipkin/zipkin-go v0.4.2/go.mod h1:ZeVkFjuuBiSy13y8vpSDCjMi9GoI3hPpCJSBx/EYFhY=
github.com/openzipkin/zipkin-go v0.4.3 h1:9EGwpqkgnwdEIJ+Od7QVSEIH+ocmm5nPat0G7sjsSdg=
github.com/openzipkin/zipkin-go v0.4.3/go.mod h1:M9wCJZFWCo2RiY+o1eBCEMe0Dp2S5LDHcMZmk3RmK7c=
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw=
github.com/oschwald/geoip2-golang v1.11.0 h1:hNENhCn1Uyzhf9PTmquXENiWS6AlxAEnBII6r8krA3w=
github.com/oschwald/geoip2-golang v1.11.0/go.mod h1:P9zG+54KPEFOliZ29i7SeYZ/GM6tfEL+rgSn03hYuUo=
@ -1384,6 +1384,7 @@ github.com/shoenig/test v1.7.1 h1:UJcjSAI3aUKx52kfcfhblgyhZceouhvvs3OYdWgn+PY=
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/sony/gobreaker v0.4.1 h1:oMnRNZXX5j85zso6xCPRNPtmAycat+WcoKbklScLDgQ=
github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g=
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad h1:fiWzISvDn0Csy5H0iwgAuJGQTUpVfEMJJd4nRFXogbc=
github.com/stoewer/parquet-cli v0.0.7 h1:rhdZODIbyMS3twr4OM3am8BPPT5pbfMcHLH93whDM5o=
@ -1473,8 +1474,6 @@ github.com/xlab/treeprint v1.1.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77 h1:ESFSdwYZvkeru3RtdrYueztKhOBCSAAzS4Gf+k0tEow=
github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY=
github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI=
github.com/yalue/merged_fs v1.3.0 h1:qCeh9tMPNy/i8cwDsQTJ5bLr6IRxbs6meakNE5O+wyY=
github.com/yalue/merged_fs v1.3.0/go.mod h1:WqqchfVYQyclV2tnR7wtRhBddzBvLVR83Cjw9BKQw0M=
github.com/ydb-platform/ydb-go-genproto v0.0.0-20240126124512-dbb0e1720dbf h1:ckwNHVo4bv2tqNkgx3W3HANh3ta1j6TR5qw08J1A7Tw=
github.com/ydb-platform/ydb-go-genproto v0.0.0-20240126124512-dbb0e1720dbf/go.mod h1:Er+FePu1dNUieD+XTMDduGpQuCPssK5Q4BjF+IIXJ3I=
github.com/ydb-platform/ydb-go-genproto v0.0.0-20240528144234-5d5a685e41f7 h1:nL8XwD6fSst7xFUirkaWJmE7kM0CdWRYgu6+YQer1d4=
@ -1498,6 +1497,7 @@ github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wK
gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b h1:7gd+rd8P3bqcn/96gOZa3F5dpJr/vEiDQYlNb/y2uNs=
go.einride.tech/aip v0.67.1 h1:d/4TW92OxXBngkSOwWS2CH5rez869KpKMaN44mdxkFI=
go.einride.tech/aip v0.67.1/go.mod h1:ZGX4/zKw8dcgzdLsrvpOOGxfxI2QSk12SlP7d6c0/XI=
go.etcd.io/bbolt v1.3.7/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw=
go.etcd.io/gofail v0.1.0 h1:XItAMIhOojXFQMgrxjnd2EIIHun/d5qL0Pf7FzVTkFg=
go.etcd.io/gofail v0.1.0/go.mod h1:VZBCXYGZhHAinaBiiqYvuDynvahNsAyLFwB3kEHKz1M=
go.opentelemetry.io/collector v0.97.0 h1:qyOju13byHIKEK/JehmTiGMj4pFLa4kDyrOCtTmjHU0=

View File

@ -159,11 +159,8 @@ func NewIndexableDocument(key *ResourceKey, rv int64, obj utils.GrafanaMetaAcces
return doc
}
func StandardDocumentBuilder() DocumentBuilderInfo {
return DocumentBuilderInfo{
Builder: &standardDocumentBuilder{},
Fields: StandardSearchFields(),
}
func StandardDocumentBuilder() DocumentBuilder {
return &standardDocumentBuilder{}
}
type standardDocumentBuilder struct{}
@ -295,6 +292,30 @@ func StandardSearchFields() SearchableDocumentFields {
FreeText: true,
},
},
{
Name: SEARCH_FIELD_TAGS,
Type: ResourceTableColumnDefinition_STRING,
IsArray: true,
Description: "Unique tags",
Properties: &ResourceTableColumnDefinition_Properties{
Filterable: true,
},
},
{
Name: SEARCH_FIELD_FOLDER,
Type: ResourceTableColumnDefinition_STRING,
Description: "Kubernetes name for the folder",
},
{
Name: SEARCH_FIELD_RV,
Type: ResourceTableColumnDefinition_INT64,
Description: "resource version",
},
{
Name: SEARCH_FIELD_CREATED,
Type: ResourceTableColumnDefinition_INT64,
Description: "created timestamp", // date?
},
})
if err != nil {
panic("failed to initialize standard search fields")

View File

@ -12,7 +12,7 @@ import (
func TestStandardDocumentBuilder(t *testing.T) {
ctx := context.Background()
builder := StandardDocumentBuilder().Builder
builder := StandardDocumentBuilder()
body, err := os.ReadFile("testdata/playlist-resource.json")
require.NoError(t, err)

File diff suppressed because it is too large Load Diff

View File

@ -323,6 +323,7 @@ message WatchEvent {
Resource previous = 4;
}
// This will soon be deprecated/replaced with ResourceSearchRequest
message SearchRequest {
// query string for chosen implementation (currently just bleve)
string query = 1;
@ -342,6 +343,92 @@ message SearchRequest {
repeated string filters = 10;
}
// Search within a single resource
message ResourceSearchRequest {
message Sort {
string field = 1;
bool desc = 2; // defaults to ascending
}
message Facet {
string field = 1;
int64 limit = 2;
// For now, only term queries, eventually?
// numeric queries
// date queries
}
// The key must include namespace + group + resource
ListOptions options = 1;
// To search additional resource types, add additional keys to this list
// NOTE: queries will only support federation across kinds with common fields
repeated ResourceKey federated = 2;
// When a query exists, it is parsed and used to influence
// query string for chosen implementation (currently just bleve)
// The score is only relevant when a query exists
string query = 3;
// max results
int64 limit = 4;
// where to start the query (eg, From)
int64 offset = 5;
// sorting
repeated Sort sortBy = 6;
// calculate field statistics
map<string,Facet> facet = 7;
// the return fields (empty will return everything)
repeated string fields = 8;
// explain each result (added to the each row)
bool explain = 9;
}
message ResourceSearchResponse {
message Facet {
string field = 1;
// The distinct terms
int64 total = 2;
// The number of documents that do *not* have this field
int64 missing = 3;
// Top term stats
repeated TermFacet terms = 4;
// numeric range
// date range facets
}
message TermFacet {
string term = 1;
int64 count = 2;
}
// Error details
ErrorResult error = 1;
// All results exist within this key
ResourceKey key = 2;
// Query results
ResourceTable results = 3;
// The total hit count
uint64 total_hits = 4;
// indicates how expensive was the query with respect to bytes read
uint64 query_cost = 5;
// maximum score across all fields
double max_score = 6;
// Facet results
map<string,Facet> facet = 7;
}
message GroupBy {
string name = 1;
int64 limit = 2;
@ -352,6 +439,7 @@ message Group {
int64 count = 2;
}
// This will soon be deprecated/replaced with ResourceSearchResponse
message SearchResponse {
repeated ResourceWrapper items = 1;
repeated Group groups = 2;

View File

@ -12,6 +12,8 @@ import (
"go.opentelemetry.io/otel/trace"
"golang.org/x/sync/errgroup"
"k8s.io/apimachinery/pkg/runtime/schema"
"github.com/grafana/authlib/authz"
)
type NamespacedResource struct {
@ -20,8 +22,52 @@ type NamespacedResource struct {
Resource string
}
// All fields are set
func (s *NamespacedResource) Valid() bool {
return s.Namespace != "" && s.Group != "" && s.Resource != ""
}
type ResourceIndex interface {
// Add a document to the index. Note it may not be searchable until after flush is called
Write(doc *IndexableDocument) error
// Mark a resource as deleted. Note it may not be searchable until after flush is called
Delete(key *ResourceKey) error
// Make sure any changes to the index are flushed and available in the next search/origin calls
Flush() error
// Search within a namespaced resource
// When working with federated queries, the additional indexes will be passed in explicitly
Search(ctx context.Context, access authz.AccessClient, req *ResourceSearchRequest, federate []ResourceIndex) (*ResourceSearchResponse, error)
// Execute an origin query -- access control is not not checked for each item
// NOTE: this will likely be used for provisioning, or it will be removed
Origin(ctx context.Context, req *OriginRequest) (*OriginResponse, error)
}
// SearchBackend contains the technology specific logic to support search
type SearchBackend interface {
// TODO
// This will return nil if the key does not exist
GetIndex(ctx context.Context, key NamespacedResource) (ResourceIndex, error)
// Build an index from scratch
BuildIndex(ctx context.Context,
key NamespacedResource,
// When the size is known, it will be passed along here
// Depending on the size, the backend may choose different options (eg: memory vs disk)
size int64,
// The last known resource version (can be used to know that nothing has changed)
resourceVersion int64,
// The non-standard index fields
fields SearchableDocumentFields,
// The builder will write all documents before returning
builder func(index ResourceIndex) (int64, error),
) (ResourceIndex, error)
}
const tracingPrexfixSearch = "unified_search."
@ -119,7 +165,7 @@ func (s *searchSupport) init(ctx context.Context) error {
return nil
}
func (s *searchSupport) build(ctx context.Context, nsr NamespacedResource, size int64, rv int64) (any, int64, error) {
func (s *searchSupport) build(ctx context.Context, nsr NamespacedResource, size int64, rv int64) (ResourceIndex, int64, error) {
_, span := s.tracer.Start(ctx, tracingPrexfixSearch+"Build")
defer span.End()
@ -127,10 +173,57 @@ func (s *searchSupport) build(ctx context.Context, nsr NamespacedResource, size
if err != nil {
return nil, 0, err
}
fields := s.builders.GetFields(nsr)
s.log.Debug(fmt.Sprintf("TODO, build %+v (size:%d, rv:%d) // builder:%+v\n", nsr, size, rv, builder))
return nil, 0, nil
key := &ResourceKey{
Group: nsr.Group,
Resource: nsr.Resource,
Namespace: nsr.Namespace,
}
index, err := s.search.BuildIndex(ctx, nsr, size, rv, fields, func(index ResourceIndex) (int64, error) {
rv, err = s.storage.ListIterator(ctx, &ListRequest{
Limit: 1000000000000, // big number
Options: &ListOptions{
Key: key,
},
}, func(iter ListIterator) error {
for iter.Next() {
if err = iter.Error(); err != nil {
return err
}
// Update the key name
// Or should we read it from the body?
key.Name = iter.Name()
// Convert it to an indexable document
doc, err := builder.BuildDocument(ctx, key, iter.ResourceVersion(), iter.Value())
if err != nil {
return err
}
// And finally write it to the index
if err = index.Write(doc); err != nil {
return err
}
}
return err
})
return rv, err
})
if err != nil {
return nil, 0, err
}
if err == nil {
err = index.Flush()
}
// rv is the last RV we read. when watching, we must add all events since that time
return index, rv, err
}
type builderCache struct {
@ -140,6 +233,9 @@ type builderCache struct {
// Possible blob support
blob BlobSupport
// searchable fields initialized once on startup
fields map[schema.GroupResource]SearchableDocumentFields
// lookup by group, then resource (namespace)
// This is only modified at startup, so we do not need mutex for access
lookup map[string]map[string]DocumentBuilderInfo
@ -151,6 +247,7 @@ type builderCache struct {
func newBuilderCache(cfg []DocumentBuilderInfo, nsCacheSize int, ttl time.Duration) (*builderCache, error) {
cache := &builderCache{
fields: make(map[schema.GroupResource]SearchableDocumentFields),
lookup: make(map[string]map[string]DocumentBuilderInfo),
ns: expirable.NewLRU[NamespacedResource, DocumentBuilder](nsCacheSize, nil, ttl),
}
@ -173,10 +270,17 @@ func newBuilderCache(cfg []DocumentBuilderInfo, nsCacheSize int, ttl time.Durati
cache.lookup[b.GroupResource.Group] = g
}
g[b.GroupResource.Resource] = b
// Any custom fields
cache.fields[b.GroupResource] = b.Fields
}
return cache, nil
}
func (s *builderCache) GetFields(key NamespacedResource) SearchableDocumentFields {
return s.fields[schema.GroupResource{Group: key.Group, Resource: key.Resource}]
}
// context is typically background. Holds an LRU cache for a
func (s *builderCache) get(ctx context.Context, key NamespacedResource) (DocumentBuilder, error) {
g, ok := s.lookup[key.Group]

View File

@ -7,13 +7,18 @@ import (
"encoding/json"
"fmt"
"io"
"os"
reflect "reflect"
"strconv"
"testing"
"time"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana-plugin-sdk-go/data/utils/jsoniter"
)
@ -74,6 +79,10 @@ func (x *ResourceTable) ToK8s() (metav1.Table, error) {
}
} else if r.Key != nil {
obj := &metav1.PartialObjectMetadata{
TypeMeta: metav1.TypeMeta{
Kind: r.Key.Resource, // :(
APIVersion: r.Key.Group, // :(
},
ObjectMeta: metav1.ObjectMeta{
Name: r.Key.Name,
Namespace: r.Key.Namespace,
@ -102,6 +111,8 @@ type TableBuilder struct {
hasDuplicateNames bool
}
type ResourceColumnEncoder = func(v any) ([]byte, error)
func NewTableBuilder(cols []*ResourceTableColumnDefinition) (*TableBuilder, error) {
table := &TableBuilder{
ResourceTable: ResourceTable{
@ -124,6 +135,15 @@ func NewTableBuilder(cols []*ResourceTableColumnDefinition) (*TableBuilder, erro
return table, err
}
func (x *TableBuilder) Encoders() []ResourceColumnEncoder {
encoders := make([]ResourceColumnEncoder, len(x.Columns))
for i, f := range x.Columns {
v := x.lookup[f.Name]
encoders[i] = v.Encode
}
return encoders
}
func (x *TableBuilder) AddRow(key *ResourceKey, rv int64, vals map[string]any) error {
row := &ResourceTableRow{
Key: key,
@ -395,6 +415,8 @@ func (x *resourceTableColumn) Encode(v any) ([]byte, error) {
f = int64(typed)
case float32:
f = int64(typed)
case float64:
f = int64(typed)
case uint64:
f = int64(typed)
case uint:
@ -547,3 +569,29 @@ func (x *resourceTableColumn) Decode(buff []byte) (any, error) {
}
return v, err
}
// AssertTableSnapshot will match a ResourceTable vs the saved value
func AssertTableSnapshot(t *testing.T, path string, table *ResourceTable) {
t.Helper()
k8sTable, err := table.ToK8s()
require.NoError(t, err, "unable to create table response", path)
actual, err := json.MarshalIndent(k8sTable, "", " ")
require.NoError(t, err, "unable to write table json", path)
// Safe to disable, this is a test.
// nolint:gosec
expected, err := os.ReadFile(path)
if err != nil || len(expected) < 1 {
assert.Fail(t, "missing file: %s", path)
} else if assert.JSONEq(t, string(expected), string(actual)) {
return // everything is OK
}
// Write the snapshot
// Safe to disable, this is a test.
// nolint:gosec
err = os.WriteFile(path, actual, 0600)
require.NoError(t, err)
fmt.Printf("Updated table snapshot: %s\n", path)
}

View File

@ -1,44 +1,15 @@
package resource
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// AssertTableSnapshot will match a ResourceTable vs the saved value
func AssertTableSnapshot(t *testing.T, path string, table *ResourceTable) {
t.Helper()
k8sTable, err := table.ToK8s()
require.NoError(t, err, "unable to create table response", path)
actual, err := json.MarshalIndent(k8sTable, "", " ")
require.NoError(t, err, "unable to write table json", path)
// Safe to disable, this is a test.
// nolint:gosec
expected, err := os.ReadFile(path)
if err != nil || len(expected) < 1 {
assert.Fail(t, "missing file")
} else if assert.JSONEq(t, string(expected), string(actual)) {
return // everything is OK
}
// Write the snapshot
// Safe to disable, this is a test.
// nolint:gosec
err = os.WriteFile(path, actual, 0600)
require.NoError(t, err)
fmt.Printf("Updated table snapshot: %s\n", path)
}
func TestTableFormat(t *testing.T) {
columns := []*ResourceTableColumnDefinition{
{

View File

@ -41,6 +41,8 @@
]
],
"object": {
"kind": "xyz",
"apiVersion": "ggg",
"metadata": {
"name": "aaa",
"namespace": "default",
@ -60,6 +62,8 @@
]
],
"object": {
"kind": "xyz",
"apiVersion": "ggg",
"metadata": {
"name": "bbb",
"namespace": "default",

View File

@ -0,0 +1,547 @@
package search
import (
"context"
"fmt"
"log/slog"
"path/filepath"
"strings"
"sync"
"github.com/blevesearch/bleve/v2"
"github.com/blevesearch/bleve/v2/search"
"github.com/blevesearch/bleve/v2/search/query"
"github.com/prometheus/client_golang/prometheus"
"go.opentelemetry.io/otel/trace"
"k8s.io/apimachinery/pkg/selection"
"github.com/grafana/authlib/authz"
"github.com/grafana/grafana/pkg/storage/unified/resource"
)
const tracingPrexfixBleve = "unified_search.bleve."
var _ resource.SearchBackend = &bleveBackend{}
var _ resource.ResourceIndex = &bleveIndex{}
type bleveOptions struct {
// The root folder where file objects are saved
Root string
// The resource count where values switch from memory to file based
FileThreshold int64
// How big should a batch get before flushing
// ?? not totally sure the units
BatchSize int
}
type bleveBackend struct {
tracer trace.Tracer
log *slog.Logger
opts bleveOptions
// cache info
cache map[resource.NamespacedResource]*bleveIndex
cacheMu sync.RWMutex
}
func NewBleveBackend(opts bleveOptions, tracer trace.Tracer, reg prometheus.Registerer) *bleveBackend {
b := &bleveBackend{
log: slog.Default().With("logger", "bleve-backend"),
tracer: tracer,
cache: make(map[resource.NamespacedResource]*bleveIndex),
opts: opts,
}
if reg != nil {
b.log.Info("TODO, register metrics collectors!")
}
return b
}
// This will return nil if the key does not exist
func (b *bleveBackend) GetIndex(ctx context.Context, key resource.NamespacedResource) (resource.ResourceIndex, error) {
b.cacheMu.RLock()
defer b.cacheMu.RUnlock()
idx, ok := b.cache[key]
if ok {
return idx, nil
}
return nil, nil
}
// Build an index from scratch
func (b *bleveBackend) BuildIndex(ctx context.Context,
key resource.NamespacedResource,
// When the size is known, it will be passed along here
// Depending on the size, the backend may choose different options (eg: memory vs disk)
size int64,
// The last known resource version can be used to know that we can skip calling the builder
resourceVersion int64,
// the non-standard searchable fields
fields resource.SearchableDocumentFields,
// The builder will write all documents before returning
builder func(index resource.ResourceIndex) (int64, error),
) (resource.ResourceIndex, error) {
b.cacheMu.Lock()
defer b.cacheMu.Unlock()
_, span := b.tracer.Start(ctx, tracingPrexfixBleve+"BuildIndex")
defer span.End()
var err error
var index bleve.Index
mapper := getBleveMappings(fields)
if size > b.opts.FileThreshold {
dir := filepath.Join(b.opts.Root, key.Namespace, fmt.Sprintf("%s.%s", key.Resource, key.Group))
index, err = bleve.New(dir, mapper)
if err == nil {
b.log.Info("TODO, check last RV so we can see if the numbers have changed", "dir", dir)
}
} else {
index, err = bleve.NewMemOnly(mapper)
}
if err != nil {
return nil, err
}
// Batch all the changes
idx := &bleveIndex{
key: key,
index: index,
batch: index.NewBatch(),
batchSize: b.opts.BatchSize,
fields: fields,
standard: resource.StandardSearchFields(),
}
idx.allFields, err = getAllFields(idx.standard, fields)
if err != nil {
return nil, err
}
_, err = builder(idx)
if err != nil {
return nil, err
}
// Flush the batch
err = idx.Flush()
if err != nil {
return nil, err
}
b.cache[key] = idx
return idx, nil
}
type bleveIndex struct {
key resource.NamespacedResource
index bleve.Index
standard resource.SearchableDocumentFields
fields resource.SearchableDocumentFields
// The values returned with all
allFields []*resource.ResourceTableColumnDefinition
// only valid in single thread
batch *bleve.Batch
batchSize int // ??? not totally sure the units here
}
// Write implements resource.DocumentIndex.
func (b *bleveIndex) Write(v *resource.IndexableDocument) error {
// remove references (for now!)
v.References = nil
if b.batch != nil {
err := b.batch.Index(v.Key.SearchID(), v)
if err != nil {
return err
}
if b.batch.Size() > b.batchSize {
err = b.index.Batch(b.batch)
b.batch.Reset() // clear the batch
}
return err // nil
}
return b.index.Index(v.Key.SearchID(), v)
}
// Delete implements resource.DocumentIndex.
func (b *bleveIndex) Delete(key *resource.ResourceKey) error {
if b.batch != nil {
return fmt.Errorf("unexpected delete while building batch")
}
return b.index.Delete(key.SearchID())
}
// Flush implements resource.DocumentIndex.
func (b *bleveIndex) Flush() (err error) {
if b.batch != nil {
err = b.index.Batch(b.batch)
b.batch.Reset()
b.batch = nil
}
return err
}
// Origin implements resource.DocumentIndex.
func (b *bleveIndex) Origin(ctx context.Context, req *resource.OriginRequest) (*resource.OriginResponse, error) {
panic("unimplemented")
}
// Search implements resource.DocumentIndex.
func (b *bleveIndex) Search(
ctx context.Context,
access authz.AccessClient,
req *resource.ResourceSearchRequest,
federate []resource.ResourceIndex, // For federated queries, these will match the values in req.federate
) (*resource.ResourceSearchResponse, error) {
if req.Options == nil || req.Options.Key == nil {
return &resource.ResourceSearchResponse{
Error: resource.NewBadRequestError("missing query key"),
}, nil
}
response := &resource.ResourceSearchResponse{
Error: b.verifyKey(req.Options.Key),
}
if response.Error != nil {
return response, nil
}
// Verifies the index federation
index, err := b.getIndex(req, federate)
if err != nil {
return nil, err
}
// convert protobuf request to bleve request
searchrequest, e := toBleveSearchRequest(req, access)
if e != nil {
response.Error = e
return response, nil
}
// Show all fields when nothing is selected
if len(searchrequest.Fields) < 1 && req.Limit > 0 {
f, err := b.index.Fields()
if err != nil {
return nil, err
}
searchrequest.Fields = f
}
res, err := index.Search(searchrequest)
if err != nil {
return nil, err
}
response.TotalHits = res.Total
response.QueryCost = res.Cost
response.MaxScore = res.MaxScore
response.Results, err = b.hitsToTable(searchrequest.Fields, res.Hits, req.Explain)
if err != nil {
return nil, err
}
// Write frame as JSON
//response.Frame, err = frame.MarshalJSON()
if err != nil {
return nil, err
}
// parse the facet fields
for k, v := range res.Facets {
f := &resource.ResourceSearchResponse_Facet{
Field: v.Field,
Total: int64(v.Total),
Missing: int64(v.Missing),
}
if v.Terms != nil {
for _, t := range v.Terms.Terms() {
f.Terms = append(f.Terms, &resource.ResourceSearchResponse_TermFacet{
Term: t.Term,
Count: int64(t.Count),
})
}
}
if response.Facet == nil {
response.Facet = make(map[string]*resource.ResourceSearchResponse_Facet)
}
response.Facet[k] = f
}
return response, nil
}
// make sure the request key matches the index
func (b *bleveIndex) verifyKey(key *resource.ResourceKey) *resource.ErrorResult {
if key.Namespace != b.key.Namespace {
return resource.NewBadRequestError("namespace mismatch (expected " + b.key.Namespace + ")")
}
if key.Group != b.key.Group {
return resource.NewBadRequestError("group mismatch (expected " + b.key.Group + ")")
}
if key.Resource != b.key.Resource {
return resource.NewBadRequestError("resource mismatch (expected " + b.key.Resource + ")")
}
return nil
}
func (b *bleveIndex) getIndex(
req *resource.ResourceSearchRequest,
federate []resource.ResourceIndex,
) (bleve.Index, error) {
if len(req.Federated) != len(federate) {
return nil, fmt.Errorf("federation is misconfigured")
}
// Search across resources using
// https://blevesearch.com/docs/IndexAlias/
if len(federate) > 0 {
all := []bleve.Index{b.index}
for i, extra := range federate {
typedindex, ok := extra.(*bleveIndex)
if !ok {
return nil, fmt.Errorf("federated indexes must be the same type")
}
if typedindex.verifyKey(req.Federated[i]) != nil {
return nil, fmt.Errorf("federated index keys do not match")
}
all = append(all, typedindex.index)
}
return bleve.NewIndexAlias(all...), nil
}
return b.index, nil
}
func toBleveSearchRequest(req *resource.ResourceSearchRequest, access authz.AccessClient) (*bleve.SearchRequest, *resource.ErrorResult) {
searchrequest := &bleve.SearchRequest{
Fields: req.Fields,
Size: int(req.Limit),
From: int(req.Offset),
Explain: req.Explain,
}
// Currently everything is within an AND query
queries := []query.Query{}
if len(req.Options.Labels) > 0 {
for _, v := range req.Options.Labels {
q, err := requirementQuery(v, "labels.")
if err != nil {
return nil, err
}
queries = append(queries, q)
}
}
if len(req.Options.Fields) > 0 {
for _, v := range req.Options.Fields {
q, err := requirementQuery(v, "")
if err != nil {
return nil, err
}
queries = append(queries, q)
}
}
if req.Query != "" {
// ??? Should expose the full power of query parsing here?
// it is great for exploration, but also hard to change in the future
q := bleve.NewQueryStringQuery(req.Query)
queries = append(queries, q)
}
if access != nil {
// TODO AUTHZ!!!!
// Need to add an authz filter into the mix
// See: https://github.com/grafana/grafana/blob/v11.3.0/pkg/services/searchV2/bluge.go
// NOTE, we likely want to pass in the already called checker because the resource server
// will first need to check if we can see anything (or everything!) for this resource
fmt.Printf("TODO... check authorization")
}
switch len(queries) {
case 0:
searchrequest.Query = bleve.NewMatchAllQuery()
case 1:
searchrequest.Query = queries[0]
default:
searchrequest.Query = bleve.NewConjunctionQuery(queries...) // AND
}
for k, v := range req.Facet {
if searchrequest.Facets == nil {
searchrequest.Facets = make(bleve.FacetsRequest)
}
searchrequest.Facets[k] = bleve.NewFacetRequest(v.Field, int(v.Limit))
}
// Add the sort fields
for _, sort := range req.SortBy {
// hardcoded (for now)
if strings.HasPrefix(sort.Field, "stats.") {
searchrequest.Sort = append(searchrequest.Sort, &search.SortField{
Field: sort.Field,
Desc: sort.Desc,
Type: search.SortFieldAsNumber, // force for now!
Mode: search.SortFieldDefault, // ???
Missing: search.SortFieldMissingLast,
})
continue
}
// Default support
input := sort.Field
if sort.Desc {
input = "-" + sort.Field
}
s := search.ParseSearchSortString(input)
searchrequest.Sort = append(searchrequest.Sort, s)
}
// Always sort by *something*, otherwise the order is unstable
if len(searchrequest.Sort) == 0 {
searchrequest.Sort = append(searchrequest.Sort, &search.SortDocID{
Desc: false,
})
}
return searchrequest, nil
}
// Convert a "requirement" into a bleve query
func requirementQuery(req *resource.Requirement, prefix string) (query.Query, *resource.ErrorResult) {
switch selection.Operator(req.Operator) {
case selection.Equals, selection.DoubleEquals:
if len(req.Values) != 1 {
return nil, resource.NewBadRequestError("equals query can have one value")
}
q := query.NewMatchQuery(req.Values[0])
q.FieldVal = prefix + req.Key
return q, nil
case selection.NotEquals:
case selection.DoesNotExist:
case selection.GreaterThan:
case selection.LessThan:
case selection.Exists:
case selection.In:
case selection.NotIn:
}
return nil, resource.NewBadRequestError(
fmt.Sprintf("unsupported query operation (%s %s %v)", req.Key, req.Operator, req.Values),
)
}
func (b *bleveIndex) hitsToTable(selectFields []string, hits search.DocumentMatchCollection, explain bool) (*resource.ResourceTable, error) {
fields := []*resource.ResourceTableColumnDefinition{}
for _, name := range selectFields {
if name == "_all" {
fields = b.allFields
break
}
f := b.standard.Field(name)
if f == nil && b.fields != nil {
f = b.fields.Field(name)
}
if f == nil {
// Labels as a string
if strings.HasPrefix(name, "labels.") {
f = &resource.ResourceTableColumnDefinition{
Name: name,
Type: resource.ResourceTableColumnDefinition_STRING,
}
}
// return nil, fmt.Errorf("unknown response field: " + name)
if f == nil {
continue // OK for now
}
}
fields = append(fields, f)
}
if explain {
fields = append(fields, b.standard.Field(resource.SEARCH_FIELD_EXPLAIN))
}
builder, err := resource.NewTableBuilder(fields)
if err != nil {
return nil, err
}
encoders := builder.Encoders()
table := &resource.ResourceTable{
Columns: fields,
Rows: make([]*resource.ResourceTableRow, hits.Len()),
}
for rowID, match := range hits {
row := &resource.ResourceTableRow{
Key: &resource.ResourceKey{},
Cells: make([][]byte, len(fields)),
}
table.Rows[rowID] = row
err := row.Key.ReadSearchID(match.ID)
if err != nil {
return nil, err
}
for i, f := range fields {
if f.Name == resource.SEARCH_FIELD_ID {
row.Cells[i] = []byte(match.ID)
continue
}
// QUICK QUICK... more options yes
v := match.Fields[f.Name]
if v != nil {
// Encode the value to protobuf
row.Cells[i], err = encoders[i](v)
if err != nil {
return nil, fmt.Errorf("error encoding (row:%d/col:%d) %v %w", rowID, i, v, err)
}
}
}
}
return table, nil
}
func getAllFields(standard resource.SearchableDocumentFields, custom resource.SearchableDocumentFields) ([]*resource.ResourceTableColumnDefinition, error) {
fields := []*resource.ResourceTableColumnDefinition{
standard.Field(resource.SEARCH_FIELD_ID),
standard.Field(resource.SEARCH_FIELD_TITLE),
standard.Field(resource.SEARCH_FIELD_TAGS),
standard.Field(resource.SEARCH_FIELD_FOLDER),
standard.Field(resource.SEARCH_FIELD_RV),
standard.Field(resource.SEARCH_FIELD_CREATED),
}
if custom != nil {
for _, name := range custom.Fields() {
f := custom.Field(name)
if f.Priority > 10 {
continue
}
fields = append(fields, f)
}
}
for _, field := range fields {
if field == nil {
return nil, fmt.Errorf("invalid all field")
}
}
return fields, nil
}

View File

@ -0,0 +1,68 @@
package search
import (
"github.com/blevesearch/bleve/v2"
"github.com/blevesearch/bleve/v2/analysis/analyzer/keyword"
"github.com/blevesearch/bleve/v2/mapping"
"github.com/grafana/grafana/pkg/storage/unified/resource"
)
func getBleveMappings(fields resource.SearchableDocumentFields) mapping.IndexMapping {
mapper := bleve.NewIndexMapping()
mapper.DefaultMapping = getBleveDocMappings(fields)
return mapper
}
func getBleveDocMappings(_ resource.SearchableDocumentFields) *mapping.DocumentMapping {
mapper := bleve.NewDocumentStaticMapping()
mapper.AddFieldMapping(&mapping.FieldMapping{
Name: "title",
Type: "text",
// TODO - if we don't want title to be a keyword, we can use this
// set the title field to use keyword analyzer so it sorts by the whole phrase
// https://github.com/blevesearch/bleve/issues/417#issuecomment-245273022
Analyzer: keyword.Name,
Store: true,
Index: true,
IncludeTermVectors: true,
IncludeInAll: true,
DocValues: false,
})
mapper.AddFieldMapping(&mapping.FieldMapping{
Name: "description",
Type: "text",
Store: true,
Index: true,
IncludeTermVectors: false,
IncludeInAll: false,
DocValues: false,
})
mapper.AddFieldMapping(&mapping.FieldMapping{
Name: "tags",
Type: "text",
Analyzer: keyword.Name,
Store: true,
Index: true,
IncludeTermVectors: false,
IncludeInAll: false,
DocValues: false,
})
mapper.AddFieldMapping(&mapping.FieldMapping{
Name: "folder",
Type: "text",
Analyzer: keyword.Name,
Store: true,
Index: true,
IncludeTermVectors: false,
IncludeInAll: false,
DocValues: true, // will be needed for authz client
})
mapper.Dynamic = true
return mapper
}

View File

@ -0,0 +1,46 @@
package search
import (
"fmt"
"testing"
"github.com/blevesearch/bleve/v2/document"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/apimachinery/utils"
"github.com/grafana/grafana/pkg/storage/unified/resource"
)
func TestDocumentMapping(t *testing.T) {
mappings := getBleveMappings(nil)
data := resource.IndexableDocument{
Title: "title",
Description: "descr",
Tags: []string{"a", "b"},
Created: 12345,
Folder: "xyz",
CreatedBy: "user:ryan",
Labels: map[string]string{
"a": "b",
"x": "y",
},
RV: 1234,
RepoInfo: &utils.ResourceRepositoryInfo{
Name: "nnn",
Path: "ppp",
Hash: "hhh",
},
}
doc := document.NewDocument("id")
err := mappings.MapDocument(doc, data)
require.NoError(t, err)
for _, f := range doc.Fields {
fmt.Printf("%s = %+v\n", f.Name(), f.Value())
}
fmt.Printf("DOC: fields %d\n", len(doc.Fields))
fmt.Printf("DOC: size %d\n", doc.Size())
require.Equal(t, 15, len(doc.Fields))
}

View File

@ -0,0 +1,289 @@
package search
import (
"context"
"encoding/json"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/services/store/kind/dashboard"
"github.com/grafana/grafana/pkg/storage/unified/resource"
)
func TestBleveBackend(t *testing.T) {
dashboardskey := &resource.ResourceKey{
Namespace: "default",
Group: "dashboard.grafana.app",
Resource: "dashboards",
}
folderKey := &resource.ResourceKey{
Namespace: dashboardskey.Namespace,
Group: "folder.grafana.app",
Resource: "folders",
}
tmpdir, err := os.CreateTemp("", "bleve-test")
require.NoError(t, err)
backend := NewBleveBackend(
bleveOptions{
Root: tmpdir.Name(),
FileThreshold: 5, // with more than 5 items we create a file on disk
},
tracing.NewNoopTracerService(),
nil,
)
rv := int64(10)
ctx := context.Background()
var dashboardsIndex resource.ResourceIndex
var foldersIndex resource.ResourceIndex
t.Run("build dashboards", func(t *testing.T) {
key := dashboardskey
info, err := DashboardBuilder(func(ctx context.Context, namespace string, blob resource.BlobSupport) (resource.DocumentBuilder, error) {
return &DashboardDocumentBuilder{
Namespace: namespace,
Blob: blob,
Stats: NewDashboardStatsLookup(nil), // empty stats
DatasourceLookup: dashboard.CreateDatasourceLookup([]*dashboard.DatasourceQueryResult{{}}),
}, nil
})
require.NoError(t, err)
index, err := backend.BuildIndex(ctx, resource.NamespacedResource{
Namespace: key.Namespace,
Group: key.Group,
Resource: key.Resource,
}, 2, rv, info.Fields, func(index resource.ResourceIndex) (int64, error) {
_ = index.Write(&resource.IndexableDocument{
RV: 1,
Key: &resource.ResourceKey{
Name: "aaa",
Namespace: "ns",
Group: "g",
Resource: "dash",
},
Title: "bbb (dash)",
Folder: "xxx",
Fields: map[string]any{
DASHBOARD_LEGACY_ID: 12,
DASHBOARD_PANEL_TYPES: []string{"timeseries", "table"},
DASHBOARD_ERRORS_TODAY: 25,
},
Tags: []string{"aa", "bb"},
})
_ = index.Write(&resource.IndexableDocument{
RV: 2,
Key: &resource.ResourceKey{
Name: "bbb",
Namespace: "ns",
Group: "g",
Resource: "dash",
},
Title: "aaa (dash)",
Folder: "xxx",
Fields: map[string]any{
DASHBOARD_LEGACY_ID: 12,
DASHBOARD_PANEL_TYPES: []string{"timeseries"},
DASHBOARD_ERRORS_TODAY: 40,
},
Tags: []string{"aa"},
Labels: map[string]string{
"region": "east",
},
})
_ = index.Write(&resource.IndexableDocument{
RV: 3,
Key: &resource.ResourceKey{
Name: "ccc",
Namespace: "ns",
Group: "g",
Resource: "dash",
},
Title: "ccc (dash)",
Folder: "xxx",
Fields: map[string]any{
DASHBOARD_LEGACY_ID: 12,
},
Tags: []string{"aa"},
Labels: map[string]string{
"region": "west",
},
})
return rv, nil
})
require.NoError(t, err)
require.NotNil(t, index)
dashboardsIndex = index
rsp, err := index.Search(ctx, nil, &resource.ResourceSearchRequest{
Options: &resource.ListOptions{
Key: key,
},
Limit: 100000,
SortBy: []*resource.ResourceSearchRequest_Sort{
{Field: "title", Desc: true}, // ccc,bbb,aaa
},
Facet: map[string]*resource.ResourceSearchRequest_Facet{
"tags": {
Field: "tags",
Limit: 100,
},
},
}, nil)
require.NoError(t, err)
require.Nil(t, rsp.Error)
require.NotNil(t, rsp.Results)
require.NotNil(t, rsp.Facet)
// Match the results
resource.AssertTableSnapshot(t, filepath.Join("testdata", "manual-dashboard.json"), rsp.Results)
// Get the tags facets
facet, ok := rsp.Facet["tags"]
require.True(t, ok)
disp, err := json.MarshalIndent(facet, "", " ")
require.NoError(t, err)
//fmt.Printf("%s\n", disp)
require.JSONEq(t, `{
"field": "tags",
"total": 4,
"terms": [
{
"term": "aa",
"count": 3
},
{
"term": "bb",
"count": 1
}
]
}`, string(disp))
})
t.Run("build folders", func(t *testing.T) {
key := folderKey
var fields resource.SearchableDocumentFields
index, err := backend.BuildIndex(ctx, resource.NamespacedResource{
Namespace: key.Namespace,
Group: key.Group,
Resource: key.Resource,
}, 2, rv, fields, func(index resource.ResourceIndex) (int64, error) {
_ = index.Write(&resource.IndexableDocument{
RV: 1,
Key: &resource.ResourceKey{
Name: "zzz",
Namespace: "ns",
Group: "g",
Resource: "folder",
},
Title: "zzz (folder)",
})
_ = index.Write(&resource.IndexableDocument{
RV: 2,
Key: &resource.ResourceKey{
Name: "yyy",
Namespace: "ns",
Group: "g",
Resource: "folder",
},
Title: "yyy (folder)",
Labels: map[string]string{
"region": "west",
},
})
return rv, nil
})
require.NoError(t, err)
require.NotNil(t, index)
foldersIndex = index
rsp, err := index.Search(ctx, nil, &resource.ResourceSearchRequest{
Options: &resource.ListOptions{
Key: key,
},
Limit: 100000,
}, nil)
require.NoError(t, err)
require.Nil(t, rsp.Error)
require.NotNil(t, rsp.Results)
require.Nil(t, rsp.Facet)
resource.AssertTableSnapshot(t, filepath.Join("testdata", "manual-folder.json"), rsp.Results)
})
t.Run("simple federation", func(t *testing.T) {
// The other tests must run first to build the indexes
require.NotNil(t, dashboardsIndex)
require.NotNil(t, foldersIndex)
// Use a federated query to get both results together, sorted by title
rsp, err := dashboardsIndex.Search(ctx, nil, &resource.ResourceSearchRequest{
Options: &resource.ListOptions{
Key: dashboardskey,
},
Fields: []string{
"title", "_id",
},
Federated: []*resource.ResourceKey{
folderKey, // This will join in the
},
Limit: 100000,
SortBy: []*resource.ResourceSearchRequest_Sort{
{Field: "title", Desc: false},
},
Facet: map[string]*resource.ResourceSearchRequest_Facet{
"region": {
Field: "labels.region",
Limit: 100,
},
},
}, []resource.ResourceIndex{foldersIndex}) // << note the folder index matches the federation request
require.NoError(t, err)
require.Nil(t, rsp.Error)
require.NotNil(t, rsp.Results)
require.NotNil(t, rsp.Facet)
// Sorted across two indexes
sorted := []string{}
for _, row := range rsp.Results.Rows {
sorted = append(sorted, string(row.Cells[0]))
}
require.Equal(t, []string{
"aaa (dash)",
"bbb (dash)",
"ccc (dash)",
"yyy (folder)",
"zzz (folder)",
}, sorted)
resource.AssertTableSnapshot(t, filepath.Join("testdata", "manual-federated.json"), rsp.Results)
facet, ok := rsp.Facet["region"]
require.True(t, ok)
disp, err := json.MarshalIndent(facet, "", " ")
require.NoError(t, err)
// fmt.Printf("%s\n", disp)
// NOTE, the west values come from *both* dashboards and folders
require.JSONEq(t, `{
"field": "labels.region",
"total": 3,
"missing": 2,
"terms": [
{
"term": "west",
"count": 2
},
{
"term": "east",
"count": 1
}
]
}`, string(disp))
})
}

View File

@ -26,7 +26,11 @@ func (s *StandardDocumentBuilders) GetDocumentBuilders() ([]resource.DocumentBui
})
return []resource.DocumentBuilderInfo{
resource.StandardDocumentBuilder(),
// The default builder
resource.DocumentBuilderInfo{
Builder: resource.StandardDocumentBuilder(),
},
// Dashboard builder
dashboards,
}, err
}

View File

@ -80,7 +80,7 @@ func TestDashboardDocumentBuilder(t *testing.T) {
})
// Standard
builder = resource.StandardDocumentBuilder().Builder
builder = resource.StandardDocumentBuilder()
doSnapshotTests(t, builder, "folder", key, []string{
"aaa",
"bbb",

View File

@ -0,0 +1,143 @@
{
"metadata": {},
"columnDefinitions": [
{
"name": "_id",
"type": "string",
"format": "",
"description": "Unique Identifier. {namespace}/{group}/{resource}/{name}",
"priority": 0
},
{
"name": "title",
"type": "string",
"format": "",
"description": "Display name for the resource",
"priority": 0
},
{
"name": "tags",
"type": "string",
"format": "",
"description": "Unique tags",
"priority": 0
},
{
"name": "folder",
"type": "string",
"format": "",
"description": "Kubernetes name for the folder",
"priority": 0
},
{
"name": "rv",
"type": "number",
"format": "int64",
"description": "resource version",
"priority": 0
},
{
"name": "created",
"type": "number",
"format": "int64",
"description": "created timestamp",
"priority": 0
},
{
"name": "schema_version",
"type": "number",
"format": "int32",
"description": "Numeric version saying when the schema was saved",
"priority": 0
},
{
"name": "link_count",
"type": "number",
"format": "int32",
"description": "How many links appear on the page",
"priority": 0
},
{
"name": "panel_types",
"type": "string",
"format": "",
"description": "How many links appear on the page",
"priority": 0
}
],
"rows": [
{
"cells": [
"ns/g/dash/ccc",
"ccc (dash)",
[
"aa"
],
"xxx",
3,
0,
null,
null,
null
],
"object": {
"kind": "dash",
"apiVersion": "g",
"metadata": {
"name": "ccc",
"namespace": "ns",
"creationTimestamp": null
}
}
},
{
"cells": [
"ns/g/dash/aaa",
"bbb (dash)",
[
"aa",
"bb"
],
"xxx",
1,
0,
null,
null,
null
],
"object": {
"kind": "dash",
"apiVersion": "g",
"metadata": {
"name": "aaa",
"namespace": "ns",
"creationTimestamp": null
}
}
},
{
"cells": [
"ns/g/dash/bbb",
"aaa (dash)",
[
"aa"
],
"xxx",
2,
0,
null,
null,
null
],
"object": {
"kind": "dash",
"apiVersion": "g",
"metadata": {
"name": "bbb",
"namespace": "ns",
"creationTimestamp": null
}
}
}
]
}

View File

@ -0,0 +1,96 @@
{
"metadata": {},
"columnDefinitions": [
{
"name": "title",
"type": "string",
"format": "",
"description": "Display name for the resource",
"priority": 0
},
{
"name": "_id",
"type": "string",
"format": "",
"description": "Unique Identifier. {namespace}/{group}/{resource}/{name}",
"priority": 0
}
],
"rows": [
{
"cells": [
"aaa (dash)",
"ns/g/dash/bbb"
],
"object": {
"kind": "dash",
"apiVersion": "g",
"metadata": {
"name": "bbb",
"namespace": "ns",
"creationTimestamp": null
}
}
},
{
"cells": [
"bbb (dash)",
"ns/g/dash/aaa"
],
"object": {
"kind": "dash",
"apiVersion": "g",
"metadata": {
"name": "aaa",
"namespace": "ns",
"creationTimestamp": null
}
}
},
{
"cells": [
"ccc (dash)",
"ns/g/dash/ccc"
],
"object": {
"kind": "dash",
"apiVersion": "g",
"metadata": {
"name": "ccc",
"namespace": "ns",
"creationTimestamp": null
}
}
},
{
"cells": [
"yyy (folder)",
"ns/g/folder/yyy"
],
"object": {
"kind": "folder",
"apiVersion": "g",
"metadata": {
"name": "yyy",
"namespace": "ns",
"creationTimestamp": null
}
}
},
{
"cells": [
"zzz (folder)",
"ns/g/folder/zzz"
],
"object": {
"kind": "folder",
"apiVersion": "g",
"metadata": {
"name": "zzz",
"namespace": "ns",
"creationTimestamp": null
}
}
}
]
}

View File

@ -0,0 +1,87 @@
{
"metadata": {},
"columnDefinitions": [
{
"name": "_id",
"type": "string",
"format": "",
"description": "Unique Identifier. {namespace}/{group}/{resource}/{name}",
"priority": 0
},
{
"name": "title",
"type": "string",
"format": "",
"description": "Display name for the resource",
"priority": 0
},
{
"name": "tags",
"type": "string",
"format": "",
"description": "Unique tags",
"priority": 0
},
{
"name": "folder",
"type": "string",
"format": "",
"description": "Kubernetes name for the folder",
"priority": 0
},
{
"name": "rv",
"type": "number",
"format": "int64",
"description": "resource version",
"priority": 0
},
{
"name": "created",
"type": "number",
"format": "int64",
"description": "created timestamp",
"priority": 0
}
],
"rows": [
{
"cells": [
"ns/g/folder/yyy",
"yyy (folder)",
null,
null,
2,
0
],
"object": {
"kind": "folder",
"apiVersion": "g",
"metadata": {
"name": "yyy",
"namespace": "ns",
"creationTimestamp": null
}
}
},
{
"cells": [
"ns/g/folder/zzz",
"zzz (folder)",
null,
null,
1,
0
],
"object": {
"kind": "folder",
"apiVersion": "g",
"metadata": {
"name": "zzz",
"namespace": "ns",
"creationTimestamp": null
}
}
}
]
}