← Back to work

Open source data tool · 2018–2020

A Configurable MySQL-to-Elasticsearch Sync Engine

A Python data synchronization engine I created when the available tools I evaluated were difficult to adapt to project-specific processing.

Full SQL loads, row-based binlog events, and scheduled queries entered one configurable transformation pipeline with explicit resume checkpoints.

Role
Creator and maintainer
Proof
  • 281 GitHub stars
  • 66 forks
  • 2026-08-28 snapshot
Stack
  • Python
  • MySQL
  • Elasticsearch
  • Redis

DATA SYSTEM

INITBINLOGCRON
Configurable pipeline
→ Elasticsearch↘ Redis checkpoints

01 / System

What did I build?

I created mysqlsmom, a Python engine for synchronizing MySQL data into Elasticsearch through three operating modes: an initial SQL load, a row-based binlog stream, or a scheduled query based on update time.

Each mode entered the same configurable filtering and transformation path before bulk destination writes, while Redis retained the progress needed by the incremental modes to resume.

mysqlsmom overview showing INIT full loads, BINLOG row events, and CRON time-based queries entering one ordered transformation pipeline, then bulk-writing to multiple Elasticsearch indexes while Redis separately stores BINLOG and CRON resume checkpoints.
Three input modes, one configurable processing path, and checkpoints kept separate from destination data.
Read the diagram description

INIT reads an initial dataset through SQL. BINLOG consumes row-based INSERT, UPDATE, and DELETE events, while CRON performs time-based incremental queries. All three modes converge on normalization, action and watched conditions, a row filter, and ordered transformation handlers. A handler sequence can select or rename fields, set document identifiers, split values, run project code, and turn one input row into multiple Elasticsearch documents. The destination performs bulk upsert or delete operations and can address more than one index. Redis is deliberately outside the destination path: BINLOG stores its current file and position there, while CRON stores its last query start so either mode can resume after restart. The diagram does not claim exactly-once delivery or unverified performance characteristics.

mysqlsmom overview

mysqlsmom overview showing INIT full loads, BINLOG row events, and CRON time-based queries entering one ordered transformation pipeline, then bulk-writing to multiple Elasticsearch indexes while Redis separately stores BINLOG and CRON resume checkpoints.

02 / Context

Why did I build it?

At the time, the synchronization tools I evaluated were difficult to adapt to the custom processing I needed between a source row and an Elasticsearch document.

I wanted a smaller Python tool that could handle initial loading, ongoing synchronization, and project-specific transformation through one understandable configuration model.

03 / Design

What was actually hard?

Connecting MySQL and Elasticsearch was not the difficult part. The challenge was giving three different inputs consistent processing semantics while preserving the meaning of inserts, updates, and deletes.

Filtering, ordered transformations, one-to-many output, destination actions, and restart progress also had to compose without becoming a collection of mode-specific branches.

The useful boundary was not database to database. It was source → pipeline → destination, with progress tracked separately.

04 / Open source

Did other developers find it useful?

The public repository had 281 stars and 66 forks in the verified 2026-08-28 snapshot. Public issues also record real installation, integration, compatibility, and extension attempts.

Those are useful signals of developer interest, but they do not establish how many companies used the project or whether any particular deployment was production-critical.

05 / Inputs

How did data enter the engine?

INIT ran a configured SQL query for the initial load. BINLOG listened to row-based insert, update, and delete events. CRON periodically queried rows using an update-time boundary.

The trigger and resume behavior differed, but downstream processing stayed consistent.

THREE INPUT CONTRACTS

Different triggers, one processing boundary

  1. 01INITInitial full loadConfigured SQL query
  2. 02BINLOGReal-time incremental changesRow-based INSERT / UPDATE / DELETE
  3. 03CRONScheduled incremental changesUpdate-time query
CONVERGENormalized eventshared downstream semantics

06 / Pipeline

How did the transformation pipeline work?

An event first passed action and watched-field conditions, then a row filter and an ordered list of handlers. Handlers could select fields, rename them, set an Elasticsearch document id, split values, perform an additional query, or call project code.

A handler could return more than one output, so one source row could become multiple destination documents without changing the source reader.

TRANSFORMATION PIPELINE

Project-specific work stays between source and destination

  1. 01Normalize
  2. 02Action / watched
  3. 03Row filter
  4. 04Ordered handlers
  5. 05Bulk destination action
  • select fields
  • rename fields
  • set document id
  • split values
  • additional SQL
  • custom script

FAN-OUTOne source row can produce multiple Elasticsearch documents

ACTIONSBulk upsert · Bulk delete

One source table can feed multiple Elasticsearch indexes

07 / Recovery

How did synchronization resume after restart?

BINLOG stored its current log filename and position in Redis. CRON stored the previous query start associated with its configuration and query.

These values told each incremental mode where to continue. Redis held progress metadata—not the documents being synchronized—and the project did not claim exactly-once delivery.

RESUME STATE

Progress metadata stays outside destination documents

CHECKPOINT STORERedisresume after restart
  1. BINLOG
    Log filename + positionresumes row-event stream
  2. CRON
    Last query startresumes time-based query window

Redis stores where incremental work should continue. Elasticsearch remains the document destination; exactly-once delivery is not claimed.

08 / Destination

How flexible was the destination model?

A configuration could cover multiple source tables, and the same source table could feed more than one Elasticsearch index. The destination translated pipeline output into bulk upsert or delete actions.

Custom row filters and handlers carried project differences, leaving the source modes and destination boundary reusable.

09 / Reflection

What would I change today?

This is a historical project: its Python 2.7 runtime and Elasticsearch APIs are now outdated, and parts of the configuration and runtime are more tightly coupled than I would choose today.

I would add types, restart-focused integration tests, modern error handling, and clearer Source, Pipeline, Destination, and Checkpoint interfaces. I would still keep the central idea: custom data work belongs in a composable pipeline rather than in each reader.