{"id":74122,"date":"2025-09-07T22:23:51","date_gmt":"2025-09-07T16:53:51","guid":{"rendered":"https:\/\/www.tothenew.com\/blog\/?p=74122"},"modified":"2025-09-09T14:49:42","modified_gmt":"2025-09-09T09:19:42","slug":"migrating-mysql-data-to-elasticsearch-a-practical-guide","status":"publish","type":"post","link":"https:\/\/www.tothenew.com\/blog\/migrating-mysql-data-to-elasticsearch-a-practical-guide\/","title":{"rendered":"Migrating MySQL Data to Elasticsearch: A Practical Guide"},"content":{"rendered":"<h2>Introduction<\/h2>\n<p>Elasticsearch has become a go-to choice for building fast, intelligent search experiences. But what if your source of truth is a relational database like MySQL? In this blog, we\u2019ll walk through how we migrated structured relational data into Elasticsearch using Python \u2014 with company_registry as a working example.<\/p>\n<h2>Why Migrate from MySQL to Elasticsearch?<\/h2>\n<p>MySQL is great for transactional workloads (OLTP) but when it comes to full-text search, autocomplete, typo tolerance, or complex filtering, it struggles. Elasticsearch shines here with:<\/p>\n<ul>\n<li>Flexible mappings for search fields<\/li>\n<li>Support for text analysis (stemming, synonyms, phonetics)<\/li>\n<li>Millisecond-level query performance<\/li>\n<li>Easy scaling for large datasets<\/li>\n<li>To bridge the gap, we need a migration pipeline that fetches data from MySQL, transforms it, and indexes it into Elasticsearch.<\/li>\n<\/ul>\n<div id=\"attachment_74135\" style=\"width: 685px\" class=\"wp-caption alignnone\"><img aria-describedby=\"caption-attachment-74135\" decoding=\"async\" loading=\"lazy\" class=\"size-full wp-image-74135\" src=\"https:\/\/www.tothenew.com\/blog\/wp-ttn-blog\/uploads\/2025\/08\/Screenshot-from-2025-08-23-20-07-35.png\" alt=\"pipeline flow diagram\" width=\"675\" height=\"204\" srcset=\"\/blog\/wp-ttn-blog\/uploads\/2025\/08\/Screenshot-from-2025-08-23-20-07-35.png 675w, \/blog\/wp-ttn-blog\/uploads\/2025\/08\/Screenshot-from-2025-08-23-20-07-35-300x91.png 300w, \/blog\/wp-ttn-blog\/uploads\/2025\/08\/Screenshot-from-2025-08-23-20-07-35-624x189.png 624w\" sizes=\"(max-width: 675px) 100vw, 675px\" \/><p id=\"caption-attachment-74135\" class=\"wp-caption-text\">pipeline flow diagram<\/p><\/div>\n<h2>Setting Up the Basics<\/h2>\n<p>We\u2019ll use:<\/p>\n<ul>\n<li>MySQL as the data source<\/li>\n<li>Elasticsearch (v8+) as the search engine<\/li>\n<li>Python + official Elasticsearch client for migration<\/li>\n<\/ul>\n<div id=\"attachment_74123\" style=\"width: 1390px\" class=\"wp-caption alignnone\"><img aria-describedby=\"caption-attachment-74123\" decoding=\"async\" loading=\"lazy\" class=\"wp-image-74123 \" src=\"https:\/\/www.tothenew.com\/blog\/wp-ttn-blog\/uploads\/2025\/08\/Screenshot-from-2025-08-23-18-09-56-1.png\" alt=\"secure connections to both MySQL and Elasticsearch\" width=\"1380\" height=\"508\" srcset=\"\/blog\/wp-ttn-blog\/uploads\/2025\/08\/Screenshot-from-2025-08-23-18-09-56-1.png 1383w, \/blog\/wp-ttn-blog\/uploads\/2025\/08\/Screenshot-from-2025-08-23-18-09-56-1-300x110.png 300w, \/blog\/wp-ttn-blog\/uploads\/2025\/08\/Screenshot-from-2025-08-23-18-09-56-1-1024x377.png 1024w, \/blog\/wp-ttn-blog\/uploads\/2025\/08\/Screenshot-from-2025-08-23-18-09-56-1-768x283.png 768w, \/blog\/wp-ttn-blog\/uploads\/2025\/08\/Screenshot-from-2025-08-23-18-09-56-1-624x230.png 624w\" sizes=\"(max-width: 1380px) 100vw, 1380px\" \/><p id=\"caption-attachment-74123\" class=\"wp-caption-text\">Here we set up secure connections to both MySQL and Elasticsearch<\/p><\/div>\n<h2>Deleting Old Indices<\/h2>\n<p>Before creating new indices, it\u2019s a good practice to remove any outdated ones. This prevents mapping conflicts or duplicate records.This ensures your migration always starts on a clean slate.<\/p>\n<div id=\"attachment_74130\" style=\"width: 1385px\" class=\"wp-caption alignnone\"><img aria-describedby=\"caption-attachment-74130\" decoding=\"async\" loading=\"lazy\" class=\"size-full wp-image-74130\" src=\"https:\/\/www.tothenew.com\/blog\/wp-ttn-blog\/uploads\/2025\/08\/Screenshot-from-2025-08-23-19-59-43.png\" alt=\"Delete index Data\" width=\"1375\" height=\"164\" srcset=\"\/blog\/wp-ttn-blog\/uploads\/2025\/08\/Screenshot-from-2025-08-23-19-59-43.png 1375w, \/blog\/wp-ttn-blog\/uploads\/2025\/08\/Screenshot-from-2025-08-23-19-59-43-300x36.png 300w, \/blog\/wp-ttn-blog\/uploads\/2025\/08\/Screenshot-from-2025-08-23-19-59-43-1024x122.png 1024w, \/blog\/wp-ttn-blog\/uploads\/2025\/08\/Screenshot-from-2025-08-23-19-59-43-768x92.png 768w, \/blog\/wp-ttn-blog\/uploads\/2025\/08\/Screenshot-from-2025-08-23-19-59-43-624x74.png 624w\" sizes=\"(max-width: 1375px) 100vw, 1375px\" \/><p id=\"caption-attachment-74130\" class=\"wp-caption-text\">Delete index Data<\/p><\/div>\n<h2>Creating Elasticsearch Mappings<\/h2>\n<p>Unlike MySQL, Elasticsearch requires you to define how fields should be indexed. For our company_registry, we use the following mapping:<\/p>\n<div id=\"attachment_74127\" style=\"width: 1365px\" class=\"wp-caption alignnone\"><img aria-describedby=\"caption-attachment-74127\" decoding=\"async\" loading=\"lazy\" class=\"wp-image-74127 size-full\" src=\"https:\/\/www.tothenew.com\/blog\/wp-ttn-blog\/uploads\/2025\/08\/Screenshot-from-2025-08-23-19-40-34.png\" alt=\"company registry index\" width=\"1355\" height=\"486\" srcset=\"\/blog\/wp-ttn-blog\/uploads\/2025\/08\/Screenshot-from-2025-08-23-19-40-34.png 1355w, \/blog\/wp-ttn-blog\/uploads\/2025\/08\/Screenshot-from-2025-08-23-19-40-34-300x108.png 300w, \/blog\/wp-ttn-blog\/uploads\/2025\/08\/Screenshot-from-2025-08-23-19-40-34-1024x367.png 1024w, \/blog\/wp-ttn-blog\/uploads\/2025\/08\/Screenshot-from-2025-08-23-19-40-34-768x275.png 768w, \/blog\/wp-ttn-blog\/uploads\/2025\/08\/Screenshot-from-2025-08-23-19-40-34-624x224.png 624w\" sizes=\"(max-width: 1355px) 100vw, 1355px\" \/><p id=\"caption-attachment-74127\" class=\"wp-caption-text\">company registry index<\/p><\/div>\n<p>This ensures that:<\/p>\n<ul>\n<li>companyName supports both full-text and exact match queries.<\/li>\n<li>excludeFromSearch is a boolean flag.<\/li>\n<li>id is stored as a numeric long.<\/li>\n<\/ul>\n<h2>Transforming the Data<\/h2>\n<p>Raw MySQL data doesn\u2019t always map directly to Elasticsearch. We normalize fields before indexing:<\/p>\n<div id=\"attachment_74128\" style=\"width: 1384px\" class=\"wp-caption alignnone\"><img aria-describedby=\"caption-attachment-74128\" decoding=\"async\" loading=\"lazy\" class=\"size-full wp-image-74128\" src=\"https:\/\/www.tothenew.com\/blog\/wp-ttn-blog\/uploads\/2025\/08\/Screenshot-from-2025-08-23-19-43-38.png\" alt=\"Transforming Data\" width=\"1374\" height=\"99\" srcset=\"\/blog\/wp-ttn-blog\/uploads\/2025\/08\/Screenshot-from-2025-08-23-19-43-38.png 1374w, \/blog\/wp-ttn-blog\/uploads\/2025\/08\/Screenshot-from-2025-08-23-19-43-38-300x22.png 300w, \/blog\/wp-ttn-blog\/uploads\/2025\/08\/Screenshot-from-2025-08-23-19-43-38-1024x74.png 1024w, \/blog\/wp-ttn-blog\/uploads\/2025\/08\/Screenshot-from-2025-08-23-19-43-38-768x55.png 768w, \/blog\/wp-ttn-blog\/uploads\/2025\/08\/Screenshot-from-2025-08-23-19-43-38-624x45.png 624w\" sizes=\"(max-width: 1374px) 100vw, 1374px\" \/><p id=\"caption-attachment-74128\" class=\"wp-caption-text\">Transforming Data<\/p><\/div>\n<h2>Fetching and Indexing Data<\/h2>\n<p>Here\u2019s the migration logic for company_registry:<\/p>\n<ul>\n<li>Fetch rows from MySQL<\/li>\n<li>Apply transformations<\/li>\n<li>Push them into Elasticsearch in bulk (faster than single inserts)<\/li>\n<\/ul>\n<div id=\"attachment_74129\" style=\"width: 1391px\" class=\"wp-caption alignnone\"><img aria-describedby=\"caption-attachment-74129\" decoding=\"async\" loading=\"lazy\" class=\"size-full wp-image-74129\" src=\"https:\/\/www.tothenew.com\/blog\/wp-ttn-blog\/uploads\/2025\/08\/Screenshot-from-2025-08-23-19-46-58.png\" alt=\"Fetching and Indexing Data\" width=\"1381\" height=\"551\" srcset=\"\/blog\/wp-ttn-blog\/uploads\/2025\/08\/Screenshot-from-2025-08-23-19-46-58.png 1381w, \/blog\/wp-ttn-blog\/uploads\/2025\/08\/Screenshot-from-2025-08-23-19-46-58-300x120.png 300w, \/blog\/wp-ttn-blog\/uploads\/2025\/08\/Screenshot-from-2025-08-23-19-46-58-1024x409.png 1024w, \/blog\/wp-ttn-blog\/uploads\/2025\/08\/Screenshot-from-2025-08-23-19-46-58-768x306.png 768w, \/blog\/wp-ttn-blog\/uploads\/2025\/08\/Screenshot-from-2025-08-23-19-46-58-624x249.png 624w\" sizes=\"(max-width: 1381px) 100vw, 1381px\" \/><p id=\"caption-attachment-74129\" class=\"wp-caption-text\">Fetching and Indexing Data<\/p><\/div>\n<h2>Running the Migration<\/h2>\n<p>Finally, execute the migration:<\/p>\n<div id=\"attachment_74134\" style=\"width: 1390px\" class=\"wp-caption alignnone\"><img aria-describedby=\"caption-attachment-74134\" decoding=\"async\" loading=\"lazy\" class=\"size-full wp-image-74134\" src=\"https:\/\/www.tothenew.com\/blog\/wp-ttn-blog\/uploads\/2025\/08\/Screenshot-from-2025-08-23-20-06-19.png\" alt=\"Running Migration\" width=\"1380\" height=\"113\" srcset=\"\/blog\/wp-ttn-blog\/uploads\/2025\/08\/Screenshot-from-2025-08-23-20-06-19.png 1380w, \/blog\/wp-ttn-blog\/uploads\/2025\/08\/Screenshot-from-2025-08-23-20-06-19-300x25.png 300w, \/blog\/wp-ttn-blog\/uploads\/2025\/08\/Screenshot-from-2025-08-23-20-06-19-1024x84.png 1024w, \/blog\/wp-ttn-blog\/uploads\/2025\/08\/Screenshot-from-2025-08-23-20-06-19-768x63.png 768w, \/blog\/wp-ttn-blog\/uploads\/2025\/08\/Screenshot-from-2025-08-23-20-06-19-624x51.png 624w\" sizes=\"(max-width: 1380px) 100vw, 1380px\" \/><p id=\"caption-attachment-74134\" class=\"wp-caption-text\">Running Migration<\/p><\/div>\n<p>When run, this script will:<\/p>\n<ul>\n<li>Delete any old index (if needed)<\/li>\n<li>Recreate the index with proper mappings<\/li>\n<li>Migrate rows from MySQL into Elasticsearch<\/li>\n<\/ul>\n<h2>Key Lessons Learned<\/h2>\n<ul>\n<li>Always define explicit mappings otherwise ES may misinterpret your fields.<\/li>\n<li>Transform booleans &amp; dates \u2014 MySQL often stores them as TINYINT or DATETIME, which ES won\u2019t parse correctly without conversion.<\/li>\n<li>Use bulk indexing for performance.<\/li>\n<li>Index per table \u2014 each MySQL table can map to its own index (e.g., company_registry, user, etc.).<\/li>\n<\/ul>\n<h2>Conclusion<\/h2>\n<p>By building a simple migration pipeline with Python, we successfully moved company_registry data from MySQL into Elasticsearch. Once indexed, the data can be leveraged for autocomplete, fuzzy search, typo handling, and blazing-fast queries \u2014 things MySQL alone struggles with.This approach is flexible: just extend the transformation functions for other tables like user or company_order and you have a reusable ETL pipeline for all your entities.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Introduction Elasticsearch has become a go-to choice for building fast, intelligent search experiences. But what if your source of truth is a relational database like MySQL? In this blog, we\u2019ll walk through how we migrated structured relational data into Elasticsearch using Python \u2014 with company_registry as a working example. Why Migrate from MySQL to Elasticsearch? [&hellip;]<\/p>\n","protected":false},"author":2132,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"iawp_total_views":49},"categories":[446],"tags":[5388,7788,1524,4844,76,1358,332],"aioseo_notices":[],"_links":{"self":[{"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/posts\/74122"}],"collection":[{"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/users\/2132"}],"replies":[{"embeddable":true,"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/comments?post=74122"}],"version-history":[{"count":3,"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/posts\/74122\/revisions"}],"predecessor-version":[{"id":74137,"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/posts\/74122\/revisions\/74137"}],"wp:attachment":[{"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/media?parent=74122"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/categories?post=74122"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/tags?post=74122"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}