Add OceanBase and SeekDB Go document engine (#17780)

## What changed

- add an OceanBase/SeekDB Go document engine using `database/sql` and
the existing MySQL driver
- preserve the Python connector's configuration, physical table names,
schema, index names, and ARRAY/JSON/VECTOR encodings
- implement chunk, memory, document metadata, skill, SQL, full-text,
vector, and fusion search paths
- support `DBMS_HYBRID_SEARCH.SEARCH` behind the existing feature flag,
with SQL fallback only when the package is unavailable
- wire the engine into retrieval, memory, metadata, vector hydration,
and SQL chat flows
- add Python/Go compatibility contracts, SQL mock tests, and an
integration-tagged round-trip test

---------

Co-authored-by: Jin Hai <haijin.chn@gmail.com>
This commit is contained in:
wangyunlai
2026-08-10 15:06:32 +08:00
committed by GitHub
parent c0bc146fcb
commit 73d006fa0e
39 changed files with 5830 additions and 47 deletions

View File

@@ -175,7 +175,7 @@ func GetAllConfigs() ([]map[string]interface{}, error) {
// Database
databaseType := globalConfig.DatabaseType()
switch databaseType {
case "mysql":
case "mysql", "oceanbase":
mysqlConfig := globalConfig.GetMySQLConfig()
exportedMySQLConfigs := mysqlConfig.ExportConfigs()
allConfigs = append(allConfigs, exportedMySQLConfigs)
@@ -194,6 +194,12 @@ func GetAllConfigs() ([]map[string]interface{}, error) {
infinityConfig := globalConfig.GetInfinityConfig()
exportedInfinityConfigs := infinityConfig.ExportConfigs()
allConfigs = append(allConfigs, exportedInfinityConfigs)
case "oceanbase":
oceanBaseConfig := globalConfig.GetOceanBaseConfig()
allConfigs = append(allConfigs, oceanBaseConfig.ExportConfigs())
case "seekdb":
seekDBConfig := globalConfig.GetSeekDBConfig()
allConfigs = append(allConfigs, seekDBConfig.ExportConfigs())
default:
return nil, fmt.Errorf("not supported doc engine: %s", docEngineType)
}

View File

@@ -18,6 +18,7 @@ package config
import (
"fmt"
"ragflow/internal/common"
"github.com/spf13/viper"
)
@@ -41,15 +42,58 @@ type MySQLConfig struct {
func (c *Config) ParseDatabaseConfig(v *viper.Viper) error {
databaseType := c.general.Database
if envType := common.GetEnvSmall(common.EnvDBType); envType != "" {
databaseType = envType
}
switch databaseType {
case "mysql":
c.parseMySQLConfig(v)
case "oceanbase":
c.parseOceanBaseDatabaseConfig(v)
default:
return fmt.Errorf("database type %s is not supported", databaseType)
}
return nil
}
func (c *Config) parseOceanBaseDatabaseConfig(v *viper.Viper) {
// OceanBase uses the MySQL wire protocol, so the existing DAO can keep
// consuming MySQLConfig. Support both the flat DB_TYPE=oceanbase section and
// the document-engine nested config.
c.parseMySQLConfig(v)
sub := v.Sub("oceanbase")
if sub == nil {
return
}
if nested := sub.Sub("config"); nested != nil {
if nested.IsSet("db_name") {
c.database.MySQL.DatabaseName = nested.GetString("db_name")
}
sub = nested
}
if sub.IsSet("name") {
c.database.MySQL.DatabaseName = sub.GetString("name")
}
if sub.IsSet("db_name") {
c.database.MySQL.DatabaseName = sub.GetString("db_name")
}
if sub.IsSet("user") {
c.database.MySQL.User = sub.GetString("user")
}
if sub.IsSet("password") {
c.database.MySQL.Password = sub.GetString("password")
}
if sub.IsSet("host") {
c.database.MySQL.Host = sub.GetString("host")
}
if sub.IsSet("port") {
c.database.MySQL.Port = sub.GetInt("port")
}
if sub.IsSet("max_connections") {
c.database.MySQL.MaxConnections = sub.GetInt("max_connections")
}
}
func (c *Config) parseMySQLConfig(v *viper.Viper) {
// Default MySQL config

View File

@@ -17,13 +17,36 @@
package config
import (
"fmt"
"strings"
"github.com/spf13/viper"
)
type DocEngineConfig struct {
ES ElasticsearchConfig `mapstructure:"es"`
Infinity InfinityConfig `mapstructure:"infinity"`
SereneDB SereneDBConfig `mapstructure:"serenedb"`
ES ElasticsearchConfig `mapstructure:"es"`
Infinity InfinityConfig `mapstructure:"infinity"`
OceanBase OceanBaseConfig `mapstructure:"oceanbase"`
SeekDB OceanBaseConfig `mapstructure:"seekdb"`
SereneDB SereneDBConfig `mapstructure:"serenedb"`
}
// OceanBaseConfig mirrors the existing oceanbase/seekdb service_conf.yaml
// structure used by the Python connector.
type OceanBaseConfig struct {
Scheme string `mapstructure:"scheme"`
Config OceanBaseConnectionConfig `mapstructure:"config"`
}
// OceanBaseConnectionConfig contains the MySQL-protocol connection settings
// used by both OceanBase and SeekDB document engines.
type OceanBaseConnectionConfig struct {
DBName string `mapstructure:"db_name"`
User string `mapstructure:"user"`
Password string `mapstructure:"password"`
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
MaxConnections int `mapstructure:"max_connections"`
}
// ElasticsearchConfig Elasticsearch configuration
@@ -58,6 +81,8 @@ type SereneDBConfig struct {
func (c *Config) ParseDocEngineConfig(v *viper.Viper) error {
c.parseInfinityConfig(v)
c.parseElasticsearchConfig(v)
c.parseOceanBaseConfig(v, "oceanbase", &c.docEngine.OceanBase)
c.parseOceanBaseConfig(v, "seekdb", &c.docEngine.SeekDB)
c.parseSereneDBConfig(v)
return nil
}
@@ -102,6 +127,105 @@ func (c *Config) parseSereneDBConfig(v *viper.Viper) {
}
}
func (c *Config) parseOceanBaseConfig(v *viper.Viper, key string, target *OceanBaseConfig) {
defaultPassword := c.database.MySQL.Password
target.Scheme = "oceanbase"
target.Config = OceanBaseConnectionConfig{
DBName: "test",
User: "root@test",
Password: defaultPassword,
Host: "localhost",
Port: 2881,
MaxConnections: 300,
}
if key == "seekdb" {
target.Config.DBName = "ragflow_doc"
target.Config.User = "root"
}
if !v.IsSet(key) {
return
}
sub := v.Sub(key)
if sub == nil {
return
}
if sub.IsSet("scheme") {
target.Scheme = sub.GetString("scheme")
}
connection := sub.Sub("config")
if connection == nil {
return
}
if connection.IsSet("db_name") {
target.Config.DBName = connection.GetString("db_name")
}
if connection.IsSet("user") {
target.Config.User = connection.GetString("user")
}
if connection.IsSet("password") {
target.Config.Password = connection.GetString("password")
}
if connection.IsSet("host") {
target.Config.Host = connection.GetString("host")
}
if connection.IsSet("port") {
target.Config.Port = connection.GetInt("port")
}
if connection.IsSet("max_connections") {
target.Config.MaxConnections = connection.GetInt("max_connections")
}
}
// ResolveOceanBaseConnection returns the effective existing configuration for
// an OceanBase-family document engine. With scheme=mysql, Python takes the
// endpoint and credentials from the mysql section while retaining db_name from
// the nested oceanbase/seekdb config; Go intentionally follows that contract.
func (c *Config) ResolveOceanBaseConnection(engineType string) (OceanBaseConnectionConfig, error) {
var configured OceanBaseConfig
switch strings.ToLower(engineType) {
case "oceanbase":
configured = c.docEngine.OceanBase
case "seekdb":
configured = c.docEngine.SeekDB
default:
return OceanBaseConnectionConfig{}, fmt.Errorf("not an OceanBase-family engine: %s", engineType)
}
resolved := configured.Config
if strings.EqualFold(configured.Scheme, "mysql") {
mysqlConfig := c.database.MySQL
resolved.User = mysqlConfig.User
resolved.Password = mysqlConfig.Password
resolved.Host = mysqlConfig.Host
resolved.Port = mysqlConfig.Port
resolved.MaxConnections = mysqlConfig.MaxConnections
}
return resolved, nil
}
func (c *Config) GetOceanBaseConfig() OceanBaseConfig {
return c.docEngine.OceanBase
}
func (c *Config) GetSeekDBConfig() OceanBaseConfig {
return c.docEngine.SeekDB
}
func (o OceanBaseConfig) ExportConfigs() map[string]interface{} {
return map[string]interface{}{
"scheme": o.Scheme,
"config": map[string]interface{}{
"db_name": o.Config.DBName,
"user": o.Config.User,
"password": o.Config.Password,
"host": o.Config.Host,
"port": o.Config.Port,
"max_connections": o.Config.MaxConnections,
},
}
}
func (c *Config) parseInfinityConfig(v *viper.Viper) {
// Default Infinity config
c.docEngine.Infinity.URI = "localhost:23817"

View File

@@ -155,9 +155,9 @@ func (c *Config) GetEnvironments() error {
docEngine := common.GetEnvSmall(common.EnvDocEngine)
if docEngine != "" {
switch docEngine {
case "infinity", "elasticsearch":
case "infinity", "elasticsearch", "oceanbase", "seekdb":
c.environments.DocumentEngineType = docEngine
case "opensearch", "oceanbase":
case "opensearch":
return fmt.Errorf("not implemented: %s", docEngine)
default:
return fmt.Errorf("invalid doc engine: %s", docEngine)
@@ -168,8 +168,8 @@ func (c *Config) GetEnvironments() error {
databaseType := common.GetEnvSmall(common.EnvDBType)
if databaseType != "" {
switch databaseType {
case "mysql":
c.environments.DatabaseType = "mysql"
case "mysql", "oceanbase":
c.environments.DatabaseType = databaseType
default:
return fmt.Errorf("invalid database type: %s", databaseType)
}

View File

@@ -95,6 +95,9 @@ func (c *Config) GetMode() string {
}
func (c *Config) DatabaseType() string {
if c.environments.DatabaseType != "" {
return c.environments.DatabaseType
}
return c.general.Database
}

View File

@@ -0,0 +1,147 @@
//
// Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
package config
import (
"reflect"
"testing"
"github.com/spf13/viper"
)
func TestParseExistingOceanBaseAndSeekDBConfig(t *testing.T) {
t.Setenv("DB_TYPE", "")
mysqlCredential := t.Name() + "-mysql"
oceanBaseCredential := t.Name() + "-oceanbase"
ignoredCredential := t.Name() + "-ignored"
v := viper.New()
v.Set("mysql", map[string]interface{}{
"name": "rag_flow", "user": "mysql-user", "password": mysqlCredential,
"host": "mysql-host", "port": 3307, "max_connections": 456,
})
v.Set("oceanbase", map[string]interface{}{
"scheme": "oceanbase",
"config": map[string]interface{}{
"db_name": "legacy_doc", "user": "root@ragflow", "password": oceanBaseCredential,
"host": "ob-host", "port": 2881, "max_connections": 123,
},
})
v.Set("seekdb", map[string]interface{}{
"scheme": "mysql",
"config": map[string]interface{}{
"db_name": "legacy_seekdb", "user": "ignored-user", "password": ignoredCredential,
"host": "ignored-host", "port": 2881, "max_connections": 12,
},
})
config := &Config{}
if err := config.ParseGeneralConfig(v); err != nil {
t.Fatal(err)
}
if err := config.ParseDatabaseConfig(v); err != nil {
t.Fatal(err)
}
if err := config.ParseDocEngineConfig(v); err != nil {
t.Fatal(err)
}
oceanBase, err := config.ResolveOceanBaseConnection("oceanbase")
if err != nil {
t.Fatal(err)
}
wantOceanBase := OceanBaseConnectionConfig{
DBName: "legacy_doc", User: "root@ragflow", Password: oceanBaseCredential,
Host: "ob-host", Port: 2881, MaxConnections: 123,
}
if !reflect.DeepEqual(oceanBase, wantOceanBase) {
t.Fatalf("oceanbase config = %#v, want %#v", oceanBase, wantOceanBase)
}
seekDB, err := config.ResolveOceanBaseConnection("seekdb")
if err != nil {
t.Fatal(err)
}
wantSeekDB := OceanBaseConnectionConfig{
DBName: "legacy_seekdb", User: "mysql-user", Password: mysqlCredential,
Host: "mysql-host", Port: 3307, MaxConnections: 456,
}
if !reflect.DeepEqual(seekDB, wantSeekDB) {
t.Fatalf("seekdb config = %#v, want %#v", seekDB, wantSeekDB)
}
}
func TestOceanBaseDefaultCredentialMatchesMySQLWireConfig(t *testing.T) {
t.Setenv("DB_TYPE", "")
v := viper.New()
config := &Config{}
if err := config.ParseGeneralConfig(v); err != nil {
t.Fatal(err)
}
if err := config.ParseDatabaseConfig(v); err != nil {
t.Fatal(err)
}
if err := config.ParseDocEngineConfig(v); err != nil {
t.Fatal(err)
}
oceanBase, err := config.ResolveOceanBaseConnection("oceanbase")
if err != nil {
t.Fatal(err)
}
if oceanBase.Password != config.GetMySQLConfig().Password {
t.Fatal("OceanBase default credential diverged from the MySQL-wire default")
}
}
func TestOceanBaseEnvironmentTypesAreAccepted(t *testing.T) {
t.Setenv("DOC_ENGINE", "seekdb")
t.Setenv("DB_TYPE", "oceanbase")
config := &Config{}
if err := config.GetEnvironments(); err != nil {
t.Fatal(err)
}
if got := config.DocEngineType(); got != "seekdb" {
t.Fatalf("doc engine = %q, want seekdb", got)
}
if got := config.DatabaseType(); got != "oceanbase" {
t.Fatalf("database type = %q, want oceanbase", got)
}
}
func TestParseOceanBaseAsMainDatabaseFromNestedConfig(t *testing.T) {
t.Setenv("DB_TYPE", "oceanbase")
oceanBaseCredential := t.Name() + "-oceanbase"
v := viper.New()
v.Set("oceanbase", map[string]interface{}{
"scheme": "oceanbase",
"config": map[string]interface{}{
"db_name": "rag_flow_ob", "user": "root@tenant", "password": oceanBaseCredential,
"host": "ob-main", "port": 2881, "max_connections": 300,
},
})
config := &Config{}
if err := config.ParseGeneralConfig(v); err != nil {
t.Fatal(err)
}
if err := config.ParseDatabaseConfig(v); err != nil {
t.Fatal(err)
}
got := config.GetMySQLConfig()
if got.DatabaseName != "rag_flow_ob" || got.User != "root@tenant" || got.Host != "ob-main" || got.Port != 2881 {
t.Fatalf("main OceanBase config was not mapped to MySQL wire config: %#v", got)
}
}