Skip to main content
Version: 0.2.3

Custom Storage Adapters

Sercha uses a ports-and-adapters architecture that allows you to replace the default storage implementations. This guide explains how to create custom adapters for SQLite, Xapian, or HNSW by implementing the appropriate port interfaces.

Architecture

Sercha separates storage concerns into distinct interfaces (ports) with pluggable implementations (adapters):

ComponentDefault ImplementationPurpose
Metadata StorageSQLiteDocuments, sources, chunks, sync state
Keyword SearchXapianBM25 full-text search
Vector SearchHNSWlibApproximate nearest neighbour search

Each component has a port interface that your custom adapter must implement.

Port Interfaces

All port interfaces are defined in internal/core/ports/driven/.

Metadata Storage Ports

These interfaces handle structured data persistence:

InterfacePurposeSource
DocumentStoreDocuments and chunksdocstore.go
SourceStoreSource configurationssourcestore.go
SyncStateStoreSync progress trackingsyncstore.go
ExclusionStoreDocument exclusionsexclusionstore.go
AuthorizationStoreOAuth/PAT credentialsauthorization_store.go
ConfigStoreApplication settingsconfigstore.go
SchedulerStoreBackground task statescheduler_store.go

Search Engine Port

The SearchEngine interface provides full-text keyword search:

// internal/core/ports/driven/search.go

type SearchEngine interface {
// Index adds or updates a chunk in the search index.
Index(ctx context.Context, chunk domain.Chunk) error

// Delete removes a chunk from the search index.
Delete(ctx context.Context, chunkID string) error

// Search performs a keyword search and returns matching chunk IDs with scores.
Search(ctx context.Context, query string, limit int) ([]SearchHit, error)

// Close releases resources.
Close() error
}

See: search.go

Vector Index Port

The VectorIndex interface provides semantic similarity search:

// internal/core/ports/driven/vector.go

type VectorIndex interface {
// Add inserts a vector for the given chunk ID.
Add(ctx context.Context, chunkID string, embedding []float32) error

// Delete removes a vector from the index.
Delete(ctx context.Context, chunkID string) error

// Search finds the k nearest neighbours to the query vector.
Search(ctx context.Context, query []float32, k int) ([]VectorHit, error)

// Close releases resources.
Close() error
}

See: vector.go

Current Implementations

SQLite Metadata Store

The default metadata storage uses SQLite with a unified store pattern:

FileDescription
internal/adapters/driven/storage/sqlite/store.goUnified SQLite store
internal/adapters/driven/storage/sqlite/migrations/Database migrations

The store provides accessor methods for individual interfaces:

  • SourceStore()SourceStore
  • DocumentStore()DocumentStore
  • SyncStateStore()SyncStateStore
  • etc.

Xapian Search Engine

Full-text search using Xapian via CGO:

FileDescription
cgo/xapian/xapian.goGo bindings (CGO build)
cgo/xapian/xapian_stub.goStub for non-CGO builds
clib/xapian/C++ wrapper

HNSW Vector Index

Vector similarity search using HNSWlib via CGO:

FileDescription
cgo/hnsw/hnsw.goGo bindings (CGO build)
cgo/hnsw/hnsw_stub.goStub for non-CGO builds
clib/hnsw/C++ wrapper

In-Memory Stores (Testing)

Reference implementations for testing:

FileDescription
internal/adapters/driven/storage/memory/In-memory implementations

Wiring Custom Adapters

Adapters are instantiated and wired in cmd/sercha/main.go.

Replacing SQLite

To use a different metadata store:

// cmd/sercha/main.go

// Replace this:
sqliteStore, err := sqlite.NewStore("")

// With your implementation:
myStore, err := mystore.NewStore("")

// Then get individual interfaces:
sourceStore := myStore.SourceStore()
docStore := myStore.DocumentStore()
// etc.

Replacing Xapian

To use a different search engine:

// cmd/sercha/main.go

// Replace this:
searchEngine, err := xapian.New(xapianPath)

// With your implementation:
searchEngine, err := mysearch.New(searchPath)

Your implementation must satisfy driven.SearchEngine.

Replacing HNSW

To use a different vector index:

// cmd/sercha/main.go

// Vector index is created via the AI initialiser
// See: internal/adapters/driven/ai/initialise.go

The vector index is created as part of AI initialisation. To replace it, modify the ai.Initialise function or create your own initialisation logic.

Key Source Files

FilePurpose
internal/core/ports/driven/All port interface definitions
internal/adapters/driven/storage/sqlite/SQLite implementation
internal/adapters/driven/storage/memory/In-memory implementations (reference)
cgo/xapian/Xapian adapter
cgo/hnsw/HNSW adapter
cmd/sercha/main.goAdapter wiring

Considerations

Interface Compliance

Use Go's interface compliance check pattern:

var _ driven.SearchEngine = (*MySearchEngine)(nil)

This ensures compile-time verification that your type implements the interface.

CGO Dependencies

The default Xapian and HNSW implementations require CGO. If you're replacing them with pure Go implementations, you can build without CGO:

CGO_ENABLED=0 go build ./cmd/sercha

Testing

The in-memory implementations in internal/adapters/driven/storage/memory/ serve as both test doubles and reference implementations.