├── .gitignore ├── README.md ├── import.sh ├── pom.xml └── src ├── main ├── assemblies │ └── plugin.xml ├── java │ └── org │ │ └── elasticsearch │ │ ├── plugin │ │ └── river │ │ │ └── mysql │ │ │ └── MysqlRiverPlugin.java │ │ └── river │ │ └── mysql │ │ ├── MysqlRiver.java │ │ └── MysqlRiverModule.java └── resources │ └── es-plugin.properties └── site ├── checkstyle_include.xml └── findbugs_exclude.xml /.gitignore: -------------------------------------------------------------------------------- 1 | /.settings 2 | /.classpath 3 | /.project 4 | *.class 5 | # Package Files # 6 | *.jar 7 | *.war 8 | *.ear -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Elasticsearch-MySQL-River 2 | ========================= 3 | 4 | An Elasticsearch river modeled to work like the Solr MySQL import feature 5 | 6 | There is an alternative plugin for jdbc connections which you could also try at: 7 | https://github.com/jprante/elasticsearch-river-jdbc 8 | 9 | # Building 10 | 11 | To build the plugin you need to have maven installed. With that in mind simply check out the project and run "mvn package" in the project directory. The plugin should then be available under target/release as a .zip file. 12 | 13 | # Installation 14 | 15 | Just copy the .zip file on the elasticsearch server should be using the plugin and run the "plugin" script coming with elasticsearch in the bin folder. 16 | 17 | An Exmaple how one would call the plugin script: 18 | 19 | /my/elasticsearch/bin/plugin install river-mysql -url file:///path/to/plugin/river-mysql.zip 20 | 21 | The plugin needs to be installed on all nodes of the ES cluster. 22 | 23 | for more info on plugins check out http://www.elasticsearch.org/guide/reference/modules/plugins.html 24 | 25 | # Usage 26 | 27 | Check out the import.sh script, which is used to initialize the mysql river with all necessary config data. 28 | 29 | More info on how to use rivers can be found here: http://www.elasticsearch.org/guide/reference/river/ 30 | -------------------------------------------------------------------------------- /import.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | JSON=$(cat < 2 | 4 | Elastichsearch MySQL River 5 | 4.0.0 6 | org.elasticsearch 7 | rivers-mysql 8 | jar 9 | Mysql River for ElasticSearch 10 | 2012 11 | 1.0.0-SNAPSHOT 12 | 13 | 14 | scm:git://github.com/mallocator/Elasticsearch-MySQL-River.git 15 | scm:git://github.com/mallocator/Elasticsearch-MySQL-River.git 16 | scm:git://github.com/mallocator/Elasticsearch-MySQL-River.git 17 | 18 | 19 | 20 | UTF-8 21 | yyyy-MM-dd HH:mm 22 | 1.7 23 | 24 | 1.0.0.RC1 25 | 5.1.28 26 | 27 | 6.8.1 28 | 1.5 29 | 30 | 2.4 31 | 3.1 32 | 2.4 33 | 2.14.1 34 | 35 | 2.5.3 36 | 2.11 37 | 38 | 39 | 40 | 41 | 42 | org.elasticsearch 43 | elasticsearch 44 | ${version.elasticsearch} 45 | provided 46 | 47 | 48 | mysql 49 | mysql-connector-java 50 | ${version.mysql} 51 | 52 | 53 | 54 | 55 | com.googlecode.jmockit 56 | jmockit 57 | ${version.jmockit} 58 | test 59 | 60 | 61 | 62 | org.testng 63 | testng 64 | ${version.testng} 65 | test 66 | 67 | 68 | 69 | 70 | 71 | 72 | org.apache.maven.plugins 73 | maven-surefire-plugin 74 | ${version.maven.surefire} 75 | 76 | -javaagent:"${settings.localRepository}"/com/googlecode/jmockit/jmockit/${version.jmockit}/jmockit-${version.jmockit}.jar 77 | -Xmx512m -XX:-UseSplitVerifier 78 | once 79 | 80 | 81 | reporter 82 | org.testng.reporters.XMLReporter 83 | 84 | 85 | 86 | 87 | 88 | org.apache.maven.plugins 89 | maven-compiler-plugin 90 | ${version.maven.compiler} 91 | 92 | ${java.version} 93 | ${java.version} 94 | ${java.version} 95 | ${java.version} 96 | -Xlint 97 | true 98 | true 99 | 100 | 101 | 102 | org.apache.maven.plugins 103 | maven-jar-plugin 104 | ${version.maven.jar} 105 | 106 | 107 | 108 | true 109 | 110 | 111 | ${project.artifactId} 112 | ${maven.build.timestamp} 113 | ${buildNumber} 114 | 115 | 116 | 117 | 118 | 119 | maven-assembly-plugin 120 | ${version.maven.assembly} 121 | 122 | false 123 | ${project.build.directory}/releases/ 124 | 125 | ${basedir}/src/main/assemblies/plugin.xml 126 | 127 | 128 | 129 | 130 | package 131 | 132 | single 133 | 134 | 135 | 136 | 137 | 138 | org.codehaus.mojo 139 | findbugs-maven-plugin 140 | ${version.findbugs} 141 | 142 | ${basedir}/src/site/findbugs_exclude.xml 143 | false 144 | true 145 | true 146 | true 147 | 148 | 149 | 150 | install 151 | 152 | findbugs 153 | 154 | 155 | 156 | 157 | 158 | org.apache.maven.plugins 159 | maven-checkstyle-plugin 160 | ${version.checkstyle} 161 | 162 | ${basedir}/src/site/checkstyle_include.xml 163 | 164 | 165 | 166 | install 167 | 168 | checkstyle 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | -------------------------------------------------------------------------------- /src/main/assemblies/plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | plugin 4 | 5 | zip 6 | 7 | false 8 | 9 | 10 | / 11 | true 12 | true 13 | 14 | org.elasticsearch:elasticsearch 15 | 16 | 17 | 18 | / 19 | true 20 | true 21 | 22 | mysql:mysql-connector-java 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/main/java/org/elasticsearch/plugin/river/mysql/MysqlRiverPlugin.java: -------------------------------------------------------------------------------- 1 | package org.elasticsearch.plugin.river.mysql; 2 | 3 | import org.elasticsearch.common.inject.Inject; 4 | import org.elasticsearch.plugins.AbstractPlugin; 5 | 6 | /** 7 | * Class for registering the MySQL river plugin. 8 | * 9 | * @author Ravi Gairola (mallox@pyxzl.net) 10 | */ 11 | public class MysqlRiverPlugin extends AbstractPlugin { 12 | 13 | @Inject 14 | public MysqlRiverPlugin() {} 15 | 16 | @Override 17 | public String name() { 18 | return "river-mysql"; 19 | } 20 | 21 | @Override 22 | public String description() { 23 | return "River MySQL Plugin"; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/org/elasticsearch/river/mysql/MysqlRiver.java: -------------------------------------------------------------------------------- 1 | package org.elasticsearch.river.mysql; 2 | 3 | import java.security.InvalidParameterException; 4 | import java.sql.Connection; 5 | import java.sql.DriverManager; 6 | import java.sql.ResultSet; 7 | import java.sql.ResultSetMetaData; 8 | import java.sql.SQLException; 9 | import java.sql.Statement; 10 | import java.util.HashMap; 11 | import java.util.Map; 12 | 13 | import org.elasticsearch.ElasticsearchException; 14 | import org.elasticsearch.ExceptionsHelper; 15 | import org.elasticsearch.action.index.IndexRequest.OpType; 16 | import org.elasticsearch.action.index.IndexRequestBuilder; 17 | import org.elasticsearch.action.support.replication.ReplicationType; 18 | import org.elasticsearch.client.Client; 19 | import org.elasticsearch.common.inject.Inject; 20 | import org.elasticsearch.common.util.concurrent.EsExecutors; 21 | import org.elasticsearch.common.xcontent.support.XContentMapValues; 22 | import org.elasticsearch.index.query.QueryBuilders; 23 | import org.elasticsearch.indices.IndexAlreadyExistsException; 24 | import org.elasticsearch.river.AbstractRiverComponent; 25 | import org.elasticsearch.river.River; 26 | import org.elasticsearch.river.RiverName; 27 | import org.elasticsearch.river.RiverSettings; 28 | 29 | /** 30 | * The class that performs the actual import. 31 | * 32 | * @author Ravi Gairola (mallox@pyxzl.net) 33 | */ 34 | public class MysqlRiver extends AbstractRiverComponent implements River { 35 | private final Client esClient; 36 | private final String index; 37 | private final String type; 38 | private volatile Thread thread; 39 | private boolean stopThread; 40 | 41 | private final String url; 42 | private final String username; 43 | private final String password; 44 | private final String query; 45 | private final String uniqueIdField; 46 | private final boolean deleteOldEntries; 47 | private final long interval; 48 | 49 | @Inject 50 | public MysqlRiver(final RiverName riverName, final RiverSettings settings, final Client esClient) { 51 | super(riverName, settings); 52 | this.esClient = esClient; 53 | this.logger.info("Creating MySQL Stream River"); 54 | 55 | this.index = readConfig("index", riverName.name()); 56 | this.type = readConfig("type", "data"); 57 | this.url = "jdbc:mysql://" + readConfig("hostname") + "/" + readConfig("database"); 58 | this.username = readConfig("username"); 59 | this.password = readConfig("password"); 60 | this.query = readConfig("query"); 61 | this.uniqueIdField = readConfig("uniqueIdField", null); 62 | this.deleteOldEntries = Boolean.parseBoolean(readConfig("deleteOldEntries", "true")); 63 | this.interval = Long.parseLong(readConfig("interval", "600000")); 64 | } 65 | 66 | private String readConfig(final String config) { 67 | final String result = readConfig(config, null); 68 | if (result == null) { 69 | this.logger.error("Unable to read required config {}. Aborting!", config); 70 | throw new InvalidParameterException("Unable to read required config " + config); 71 | } 72 | return result; 73 | } 74 | 75 | @SuppressWarnings({ "unchecked" }) 76 | private String readConfig(final String config, final String defaultValue) { 77 | if (this.settings.settings().containsKey("mysql")) { 78 | Map mysqlSettings = (Map) this.settings.settings().get("mysql"); 79 | return XContentMapValues.nodeStringValue(mysqlSettings.get(config), defaultValue); 80 | } 81 | return defaultValue; 82 | } 83 | 84 | @Override 85 | public void start() { 86 | this.logger.info("starting mysql stream"); 87 | try { 88 | this.esClient.admin() 89 | .indices() 90 | .prepareCreate(this.index) 91 | .addMapping(this.type, "{\"" + this.type + "\":{\"_timestamp\":{\"enabled\":true}}}") 92 | .execute() 93 | .actionGet(); 94 | this.logger.info("Created Index {} with _timestamp mapping for {}", this.index, this.type); 95 | } catch (Exception e) { 96 | if (ExceptionsHelper.unwrapCause(e) instanceof IndexAlreadyExistsException) { 97 | this.logger.debug("Not creating Index {} as it already exists", this.index); 98 | } 99 | else if (ExceptionsHelper.unwrapCause(e) instanceof ElasticsearchException) { 100 | this.logger.debug("Mapping {}.{} already exists and will not be created", this.index, this.type); 101 | } 102 | else { 103 | this.logger.warn("failed to create index [{}], disabling river...", e, this.index); 104 | return; 105 | } 106 | } 107 | 108 | try { 109 | this.esClient.admin() 110 | .indices() 111 | .preparePutMapping(this.index) 112 | .setType(this.type) 113 | .setSource("{\"" + this.type + "\":{\"_timestamp\":{\"enabled\":true}}}") 114 | .setIgnoreConflicts(true) 115 | .execute() 116 | .actionGet(); 117 | } catch (ElasticsearchException e) { 118 | this.logger.debug("Mapping already exists for index {} and type {}", this.index, this.type); 119 | } 120 | 121 | if (this.thread == null) { 122 | this.thread = EsExecutors.daemonThreadFactory(this.settings.globalSettings(), "mysql_slurper").newThread(new Parser()); 123 | this.thread.start(); 124 | } 125 | } 126 | 127 | @Override 128 | public void close() { 129 | this.logger.info("Closing MySQL river"); 130 | this.stopThread = true; 131 | this.thread = null; 132 | } 133 | 134 | /** 135 | * Parser that asynchronously writes the data from MySQL into ElasticSearch. 136 | * 137 | * @author Ravi Gairola (mallox@pyxzl.net) 138 | */ 139 | private final class Parser extends Thread { 140 | private Parser() {} 141 | 142 | @Override 143 | public void run() { 144 | MysqlRiver.this.logger.info("Mysql Import Thread has started"); 145 | long lastRun = 0; 146 | while (!MysqlRiver.this.stopThread) { 147 | if (lastRun + MysqlRiver.this.interval < System.currentTimeMillis()) { 148 | lastRun = System.currentTimeMillis(); 149 | parse(); 150 | if (MysqlRiver.this.interval <= 0) { 151 | break; 152 | } 153 | if (!MysqlRiver.this.stopThread) { 154 | MysqlRiver.this.logger.info("Mysql Import Thread is waiting for {} Seconds until the next run", 155 | MysqlRiver.this.interval / 1000); 156 | } 157 | } 158 | try { 159 | sleep(1000); 160 | } catch (InterruptedException e) { 161 | MysqlRiver.this.logger.trace("Thread sleep cycle has been interrupted", e); 162 | } 163 | } 164 | MysqlRiver.this.logger.info("Mysql Import Thread has finished"); 165 | } 166 | 167 | private void parse() throws ElasticsearchException { 168 | Connection con = null; 169 | Statement st = null; 170 | ResultSet rs = null; 171 | 172 | try { 173 | Class.forName("com.mysql.jdbc.Driver"); 174 | con = DriverManager.getConnection(MysqlRiver.this.url, MysqlRiver.this.username, MysqlRiver.this.password); 175 | st = con.createStatement(); 176 | rs = st.executeQuery(MysqlRiver.this.query); 177 | final ResultSetMetaData md = rs.getMetaData(); 178 | 179 | rs.last(); 180 | final int size = rs.getRow(); 181 | String timestamp = String.valueOf((int) (System.currentTimeMillis() / 1000)); 182 | int progress = 0; 183 | MysqlRiver.this.logger.info("Got {} results from MySQL database", size); 184 | 185 | if (size == 0) { 186 | MysqlRiver.this.logger.warn("Got 0 results from database. Aborting before we do some damage and remove still valid entries."); 187 | return; 188 | } 189 | rs.beforeFirst(); 190 | while (rs.next() && !MysqlRiver.this.stopThread) { 191 | final HashMap rowMap = new HashMap(); 192 | for (int i = 1; i <= md.getColumnCount(); i++) { 193 | rowMap.put(md.getColumnName(i), rs.getString(i)); 194 | } 195 | IndexRequestBuilder builder = MysqlRiver.this.esClient.prepareIndex(MysqlRiver.this.index, MysqlRiver.this.type); 196 | if (MysqlRiver.this.uniqueIdField != null) { 197 | builder.setId((String) rowMap.get(MysqlRiver.this.uniqueIdField)); 198 | } 199 | builder.setOpType(OpType.INDEX) 200 | .setReplicationType(ReplicationType.ASYNC) 201 | .setOperationThreaded(true) 202 | .setTimestamp(timestamp) 203 | .setSource(rowMap) 204 | .execute() 205 | .actionGet(); 206 | if (++progress % 100 == 0) { 207 | MysqlRiver.this.logger.debug("Processed {} entries ({} percent done)", 208 | progress, 209 | Math.round((float) progress / (float) size * 100f)); 210 | } 211 | } 212 | MysqlRiver.this.logger.info("Imported {} entries into ElasticSeach from MySQL!", size); 213 | if (MysqlRiver.this.deleteOldEntries) { 214 | MysqlRiver.this.logger.info("Removing old MySQL entries from ElasticSearch!"); 215 | MysqlRiver.this.esClient.prepareDeleteByQuery(MysqlRiver.this.index) 216 | .setTypes(MysqlRiver.this.type) 217 | .setQuery(QueryBuilders.rangeQuery("_timestamp").lt(timestamp)) 218 | .execute() 219 | .actionGet(); 220 | MysqlRiver.this.logger.info("Old MySQL entries have been removed from ElasticSearch!"); 221 | } 222 | else { 223 | MysqlRiver.this.logger.info("Not removing old MySQL entries from ElasticSearch"); 224 | } 225 | MysqlRiver.this.logger.info("MySQL river has been completed"); 226 | return; 227 | } catch (SQLException ex) { 228 | MysqlRiver.this.logger.error("Error trying to read data frm MySQL database!", ex); 229 | } catch (ClassNotFoundException ex) { 230 | MysqlRiver.this.logger.error("Error trying to load MySQL driver!", ex); 231 | } finally { 232 | try { 233 | if (rs != null) { 234 | rs.close(); 235 | } 236 | if (st != null) { 237 | st.close(); 238 | } 239 | if (con != null) { 240 | con.close(); 241 | } 242 | } catch (SQLException ex) { 243 | MysqlRiver.this.logger.warn("Error closing MySQL connection properly.", ex); 244 | } 245 | } 246 | } 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /src/main/java/org/elasticsearch/river/mysql/MysqlRiverModule.java: -------------------------------------------------------------------------------- 1 | package org.elasticsearch.river.mysql; 2 | 3 | import org.elasticsearch.common.inject.AbstractModule; 4 | import org.elasticsearch.river.River; 5 | 6 | /** 7 | * The Module that performs the actual binding of the MySQL module. 8 | * 9 | * @author Ravi Gairola (ravig@motorola.com) 10 | */ 11 | public class MysqlRiverModule extends AbstractModule { 12 | 13 | @Override 14 | protected void configure() { 15 | bind(River.class).to(MysqlRiver.class).asEagerSingleton(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/resources/es-plugin.properties: -------------------------------------------------------------------------------- 1 | plugin=org.elasticsearch.plugin.river.mysql.MysqlRiverPlugin -------------------------------------------------------------------------------- /src/site/checkstyle_include.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | -------------------------------------------------------------------------------- /src/site/findbugs_exclude.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | mockit.MockUp.MockUp 19 | 20 | 21 | --------------------------------------------------------------------------------