refactor: SearchEngine interface, remove nilable qdrantStore

This commit is contained in:
didier committed 2026-06-05 16:02:31 -04:00
1 parent 98aa7dd68c
commit ffdc023065
5 files changed
+134 -118

No files matched your search

+68 -91
View File
@@ -30,43 +30,10 @@ func main() {
}
defer logger.Sync()
qdrantURL := os.Getenv("QDRANT_URL")
openAIKey := requireEnv(logger, "OPENAI_API_KEY")
token := requireEnv(logger, "MCP_TOKEN")
embeddingModel := os.Getenv("EMBEDDING_MODEL")
embeddingBaseURL := os.Getenv("EMBEDDING_BASE_URL")
openAIKey := requireEnv(logger, "OPENAI_API_KEY")
port := envOrDefault("PORT", "8080")
qdrantPort := 6334
if p, err := strconv.Atoi(envOrDefault("QDRANT_PORT", "6334")); err == nil {
qdrantPort = p
}
qdrantAPIKey := os.Getenv("QDRANT_API_KEY")
qdrantTLS := os.Getenv("QDRANT_TLS") == "true"
var qdrantStr *qdrantStore
if qdrantURL != "" {
qdrantClient, err := qdrant.NewClient(&qdrant.Config{
Host: qdrantURL,
Port: qdrantPort,
APIKey: qdrantAPIKey,
UseTLS: qdrantTLS,
})
if err != nil {
logger.Fatal("failed to create qdrant client", zap.Error(err))
}
defer qdrantClient.Close()
emb := newEmbedder(openAIKey, embeddingModel, embeddingBaseURL)
qdrantStr = newQdrantStore(qdrantClient, emb)
if err := qdrantStr.ensureCollection(context.Background()); err != nil {
logger.Fatal("failed to ensure collection", zap.Error(err))
}
} else {
logger.Warn("QDRANT_URL not set, using SQLite FTS5 for search")
}
sqlitePath := envOrDefault("SQLITE_PATH", "flashcard.db")
sqliteStore, err := newSqliteStore(sqlitePath)
if err != nil {
@@ -74,6 +41,8 @@ func main() {
}
defer sqliteStore.close()
searchEngine := createSearchEngine(logger, sqliteStore, openAIKey)
mcpServer := server.NewMCPServer(
"flashcard-mcp",
version,
@@ -81,10 +50,10 @@ func main() {
server.WithLogging(),
)
registerStoreFlashcard(mcpServer, sqliteStore, qdrantStr, logger)
registerSearchFlashcard(mcpServer, sqliteStore, qdrantStr)
registerUpdateFlashcard(mcpServer, sqliteStore, qdrantStr, logger)
registerDeleteFlashcard(mcpServer, sqliteStore, qdrantStr, logger)
registerStoreFlashcard(mcpServer, sqliteStore, searchEngine, logger)
registerSearchFlashcard(mcpServer, searchEngine)
registerUpdateFlashcard(mcpServer, sqliteStore, searchEngine, logger)
registerDeleteFlashcard(mcpServer, sqliteStore, searchEngine, logger)
streamableServer := server.NewStreamableHTTPServer(mcpServer)
@@ -128,7 +97,44 @@ func main() {
}
}
func registerStoreFlashcard(mcpServer *server.MCPServer, sqlite *sqliteStore, qdrant *qdrantStore, logger *zap.Logger) {
func createSearchEngine(logger *zap.Logger, sqliteStore *sqliteStore, openAIKey string) SearchEngine {
qdrantURL := os.Getenv("QDRANT_URL")
if qdrantURL == "" {
logger.Warn("QDRANT_URL not set, using SQLite FTS5 for search")
return newFTS5Engine(sqliteStore)
}
embeddingModel := os.Getenv("EMBEDDING_MODEL")
embeddingBaseURL := os.Getenv("EMBEDDING_BASE_URL")
qdrantPort := 6334
if p, err := strconv.Atoi(envOrDefault("QDRANT_PORT", "6334")); err == nil {
qdrantPort = p
}
qdrantAPIKey := os.Getenv("QDRANT_API_KEY")
qdrantTLS := os.Getenv("QDRANT_TLS") == "true"
client, err := qdrant.NewClient(&qdrant.Config{
Host: qdrantURL,
Port: qdrantPort,
APIKey: qdrantAPIKey,
UseTLS: qdrantTLS,
})
if err != nil {
logger.Fatal("failed to create qdrant client", zap.Error(err))
}
emb := newEmbedder(openAIKey, embeddingModel, embeddingBaseURL)
store := newQdrantStore(client, emb, sqliteStore, logger)
if err := store.ensureCollection(context.Background()); err != nil {
logger.Fatal("failed to ensure collection", zap.Error(err))
}
return store
}
func registerStoreFlashcard(mcpServer *server.MCPServer, sqlite *sqliteStore, engine SearchEngine, logger *zap.Logger) {
mcpServer.AddTool(
mcp.NewTool("store_flashcard",
mcp.WithDescription("Store a flashcard for later retrieval. Use this to persist useful information discovered during a session."),
@@ -155,11 +161,11 @@ func registerStoreFlashcard(mcpServer *server.MCPServer, sqlite *sqliteStore, qd
mcp.Description("Optional context — what triggered this"),
),
),
handleStoreFlashcard(sqlite, qdrant, logger),
handleStoreFlashcard(sqlite, engine, logger),
)
}
func registerSearchFlashcard(mcpServer *server.MCPServer, sqlite *sqliteStore, qdrant *qdrantStore) {
func registerSearchFlashcard(mcpServer *server.MCPServer, engine SearchEngine) {
mcpServer.AddTool(
mcp.NewTool("search_flashcard",
mcp.WithDescription("Search previously stored flashcards using semantic search. Returns the most relevant entries for the given query."),
@@ -171,11 +177,11 @@ func registerSearchFlashcard(mcpServer *server.MCPServer, sqlite *sqliteStore, q
mcp.Description("Maximum number of results to return (default: 5)"),
),
),
handleSearchFlashcard(sqlite, qdrant),
handleSearchFlashcard(engine),
)
}
func registerUpdateFlashcard(mcpServer *server.MCPServer, sqlite *sqliteStore, qdrant *qdrantStore, logger *zap.Logger) {
func registerUpdateFlashcard(mcpServer *server.MCPServer, sqlite *sqliteStore, engine SearchEngine, logger *zap.Logger) {
mcpServer.AddTool(
mcp.NewTool("update_flashcard",
mcp.WithDescription("Update an existing flashcard. Creates a new version while preserving history."),
@@ -202,11 +208,11 @@ func registerUpdateFlashcard(mcpServer *server.MCPServer, sqlite *sqliteStore, q
mcp.Description("Optional context — what triggered this"),
),
),
handleUpdateFlashcard(sqlite, qdrant, logger),
handleUpdateFlashcard(sqlite, engine, logger),
)
}
func handleUpdateFlashcard(sqlite *sqliteStore, qdrant *qdrantStore, logger *zap.Logger) server.ToolHandlerFunc {
func handleUpdateFlashcard(sqlite *sqliteStore, engine SearchEngine, logger *zap.Logger) server.ToolHandlerFunc {
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
args, ok := request.Params.Arguments.(map[string]any)
if !ok {
@@ -258,16 +264,11 @@ func handleUpdateFlashcard(sqlite *sqliteStore, qdrant *qdrantStore, logger *zap
return mcp.NewToolResultError(fmt.Sprintf("failed to persist update: %v", err)), nil
}
if err := sqlite.deleteFTS(ctx, latest.ID); err != nil {
logger.Warn("fts delete failed", zap.Int64("id", latest.ID), zap.Error(err))
if err := engine.Delete(ctx, latest.ID, publicID); err != nil {
logger.Warn("search engine delete failed", zap.Int64("id", latest.ID), zap.Error(err))
}
if err := sqlite.indexFTS(ctx, id, note); err != nil {
return mcp.NewToolResultError(fmt.Sprintf("fts index failed: %v", err)), nil
}
if qdrant != nil {
if err := qdrant.index(ctx, id, publicID, version, note); err != nil {
logger.Warn("qdrant index failed (entry in sqlite)", zap.Int64("id", id), zap.Error(err))
}
if err := engine.Index(ctx, id, publicID, version, note); err != nil {
return mcp.NewToolResultError(fmt.Sprintf("search engine index failed: %v", err)), nil
}
entry, err := sqlite.getByID(ctx, id)
@@ -284,20 +285,20 @@ func handleUpdateFlashcard(sqlite *sqliteStore, qdrant *qdrantStore, logger *zap
}
}
func registerDeleteFlashcard(mcpServer *server.MCPServer, sqlite *sqliteStore, qdrant *qdrantStore, logger *zap.Logger) {
func registerDeleteFlashcard(mcpServer *server.MCPServer, sqlite *sqliteStore, engine SearchEngine, logger *zap.Logger) {
mcpServer.AddTool(
mcp.NewTool("delete_flashcard",
mcp.WithDescription("Delete a flashcard. Creates a tombstone version and removes it from the vector index."),
mcp.WithDescription("Delete a flashcard. Creates a tombstone version and removes it from the search index."),
mcp.WithNumber("public_id",
mcp.Required(),
mcp.Description("Public identifier of the flashcard to delete"),
),
),
handleDeleteFlashcard(sqlite, qdrant, logger),
handleDeleteFlashcard(sqlite, engine, logger),
)
}
func handleDeleteFlashcard(sqlite *sqliteStore, qdrant *qdrantStore, logger *zap.Logger) server.ToolHandlerFunc {
func handleDeleteFlashcard(sqlite *sqliteStore, engine SearchEngine, logger *zap.Logger) server.ToolHandlerFunc {
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
args, ok := request.Params.Arguments.(map[string]any)
if !ok {
@@ -324,20 +325,15 @@ func handleDeleteFlashcard(sqlite *sqliteStore, qdrant *qdrantStore, logger *zap
return mcp.NewToolResultError(fmt.Sprintf("failed to tombstone: %v", err)), nil
}
if err := sqlite.deleteFTS(ctx, latest.ID); err != nil {
logger.Warn("fts delete failed", zap.Int64("id", latest.ID), zap.Error(err))
}
if qdrant != nil {
if err := qdrant.delete(ctx, publicID); err != nil {
logger.Warn("qdrant delete failed (tombstone in sqlite)", zap.Int64("public_id", publicID), zap.Error(err))
}
if err := engine.Delete(ctx, latest.ID, publicID); err != nil {
logger.Warn("search engine delete failed", zap.Int64("id", latest.ID), zap.Error(err))
}
return mcp.NewToolResultText(fmt.Sprintf("flashcard %d deleted", publicID)), nil
}
}
func handleStoreFlashcard(sqlite *sqliteStore, qdrant *qdrantStore, logger *zap.Logger) server.ToolHandlerFunc {
func handleStoreFlashcard(sqlite *sqliteStore, engine SearchEngine, logger *zap.Logger) server.ToolHandlerFunc {
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
args, ok := request.Params.Arguments.(map[string]any)
if !ok {
@@ -364,13 +360,8 @@ func handleStoreFlashcard(sqlite *sqliteStore, qdrant *qdrantStore, logger *zap.
if publicID == 0 {
publicID = id
}
if err := sqlite.indexFTS(ctx, id, note); err != nil {
return mcp.NewToolResultError(fmt.Sprintf("fts index failed: %v", err)), nil
}
if qdrant != nil {
if err := qdrant.index(ctx, id, publicID, 1, note); err != nil {
logger.Warn("qdrant index failed (entry in sqlite)", zap.Int64("id", id), zap.Error(err))
}
if err := engine.Index(ctx, id, publicID, 1, note); err != nil {
return mcp.NewToolResultError(fmt.Sprintf("search engine index failed: %v", err)), nil
}
entry, err := sqlite.getByID(ctx, id)
@@ -387,7 +378,7 @@ func handleStoreFlashcard(sqlite *sqliteStore, qdrant *qdrantStore, logger *zap.
}
}
func handleSearchFlashcard(sqlite *sqliteStore, qdrant *qdrantStore) server.ToolHandlerFunc {
func handleSearchFlashcard(engine SearchEngine) server.ToolHandlerFunc {
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
args, ok := request.Params.Arguments.(map[string]any)
if !ok {
@@ -411,23 +402,9 @@ func handleSearchFlashcard(sqlite *sqliteStore, qdrant *qdrantStore) server.Tool
}
}
var results []searchResult
var err error
if qdrant != nil {
results, err = qdrant.search(ctx, query, limit)
if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("search failed: %v", err)), nil
}
for i, r := range results {
if cached, err := sqlite.getByID(ctx, r.Entry.ID); err == nil && cached != nil {
results[i].Entry = *cached
}
}
} else {
results, err = sqlite.searchFTS(ctx, query, limit)
if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("search failed: %v", err)), nil
}
results, err := engine.Search(ctx, query, limit)
if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("search failed: %v", err)), nil
}
output, err := json.MarshalIndent(results, "", " ")